From cbc0dcf67d0fb45caf69f4e86a89fbb08a84610b Mon Sep 17 00:00:00 2001 From: zhangrui Date: Tue, 14 Jul 2026 17:41:39 +0800 Subject: [PATCH 1/2] =?UTF-8?q?change=E6=99=AE=E6=83=A0=E7=96=97=E4=BC=91?= =?UTF-8?q?=E5=85=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/TourMySignupController.java | 8 ++++ .../controller/TourSettingController.java | 3 ++ .../tour/controller/TourSignupController.java | 23 ++++++++++ .../tour/models/TourSetting.java | 5 +++ .../tour/service/TourLedgerService.java | 12 ++++++ .../service/impl/TourLedgerServiceImpl.java | 42 +++++++++++++++++++ src/main/resources/db/tour_setting_create.sql | 1 + .../dayofficework/Tour/setting/index.html | 19 +++++++++ 8 files changed, 113 insertions(+) diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMySignupController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMySignupController.java index 104984e2..02913aec 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMySignupController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMySignupController.java @@ -667,6 +667,14 @@ public class TourMySignupController { } } if (isInProvinceLine(line.getLineType())) { + // 修改报名时排除当前台账,再按实际参加记录校验省内线路间隔。 + String inProvinceMessage = tourLedgerService.checkInProvinceParticipation( + jobNo, + setting.getInProvinceYears(), + oldLedger == null ? null : oldLedger.getId()); + if (StrUtil.isNotBlank(inProvinceMessage)) { + return Result.error(inProvinceMessage); + } Cnd sameYearCnd = Cnd.where(TourLedger::getDelFlag, "=", false) .and(TourLedger::getJobNo, "=", jobNo) .and(TourLedger::getYear, "=", matter.getYear()); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSettingController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSettingController.java index a91a061d..67aa165c 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSettingController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSettingController.java @@ -241,6 +241,9 @@ public class TourSettingController { && tourSetting.getMinGroupPeople() > tourSetting.getMaxGroupPeople()) { return Result.error("最少成团人数不能大于最多成团人数"); } + if (tourSetting.getInProvinceYears() != null && tourSetting.getInProvinceYears() < 0) { + return Result.error("省内间隔年限必须为非负整数"); + } if (tourSetting.getCycleStartYear() != null && tourSetting.getCycleEndYear() != null && tourSetting.getCycleStartYear() > tourSetting.getCycleEndYear()) { return Result.error("周期开始年度不能大于周期结束年度"); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSignupController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSignupController.java index 53bf71d6..846ed2dd 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSignupController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSignupController.java @@ -531,6 +531,21 @@ public class TourSignupController { .addv("currentLineCost", cycleTotalCost.currentCost()) .addv("message", buildCycleTotalCostNotice(cycleTotalCost))); } + if (isInProvinceLine(line.getLineType())) { + // 进入报名表单前先校验省内参加间隔,最终提交时还会再次执行相同规则。 + String inProvinceMessage = tourLedgerService.checkInProvinceParticipation( + currentJobNo(), + setting.getInProvinceYears(), + oldLedger == null ? null : oldLedger.getId()); + if (StrUtil.isNotBlank(inProvinceMessage)) { + return Result.success(NutMap.NEW() + .addv("canApply", false) + .addv("noticeRequired", true) + .addv("noticeType", "inProvinceYears") + .addv("message", inProvinceMessage)); + } + return Result.success(NutMap.NEW().addv("canApply", true).addv("noticeRequired", false)); + } if (!isOutProvinceLine(line.getLineType())) { return Result.success(NutMap.NEW().addv("canApply", true).addv("noticeRequired", false)); } @@ -940,6 +955,14 @@ public class TourSignupController { } } if (isInProvinceLine(line.getLineType())) { + // 最终提交以台账中的实际参加记录为准,避免绕过资格提示接口直接报名。 + String inProvinceMessage = tourLedgerService.checkInProvinceParticipation( + jobNo, + setting.getInProvinceYears(), + oldLedger == null ? null : oldLedger.getId()); + if (StrUtil.isNotBlank(inProvinceMessage)) { + return Result.error(inProvinceMessage); + } Cnd sameYearCnd = Cnd.where(TourLedger::getDelFlag, "=", false) .and(TourLedger::getJobNo, "=", jobNo) .and(TourLedger::getYear, "=", matter.getYear()); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSetting.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSetting.java index d910d4aa..8b0b1db8 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSetting.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSetting.java @@ -69,6 +69,11 @@ public class TourSetting extends BaseModel implements Serializable { @ColDefine(type = ColType.INT) private Integer maxGroupPeople; + @Column + @Comment("省内几年去一次") + @ColDefine(type = ColType.INT) + private Integer inProvinceYears; + @Column @Comment("省外几年去一次") @ColDefine(type = ColType.INT) diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerService.java index f8cea93f..296c95dc 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerService.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerService.java @@ -8,6 +8,18 @@ import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; */ public interface TourLedgerService extends BaseService { + /** + * 校验当前人员在省内间隔周期内是否已经实际参加过省内线路。 + * + * @param jobNo 当前登录人工号 + * @param inProvinceYears 包含当前年份在内的省内限制年数,小于等于0表示不限制 + * @param excludeLedgerId 修改报名时需要排除的当前台账ID,新增时允许为空 + * @return 校验通过返回空字符串,否则返回不能报名的业务提示 + */ + String checkInProvinceParticipation(String jobNo, + Integer inProvinceYears, + String excludeLedgerId); + /** * 在同一事务中锁定分工会名额、校验并发容量并写入报名台账。 * diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerServiceImpl.java index ff9820db..0b63a786 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerServiceImpl.java @@ -14,6 +14,8 @@ import org.nutz.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.lang.util.NutMap; +import java.time.LocalDate; + /** * 疗休养报名台账服务实现。 */ @@ -24,6 +26,46 @@ public class TourLedgerServiceImpl extends BaseServiceImpl implement super(dao); } + @Override + public String checkInProvinceParticipation(String jobNo, + Integer inProvinceYears, + String excludeLedgerId) { + if (inProvinceYears == null || inProvinceYears <= 0) { + return ""; + } + if (StrUtil.isBlank(jobNo)) { + return "未查询到当前登录人工号,不能校验省内线路报名资格"; + } + int currentYear = LocalDate.now().getYear(); + // N年按包含当前年份在内的N个自然年度计算,例如2026年的3年周期为2024至2026年。 + long calculatedStartYear = (long) currentYear - inProvinceYears + 1L; + int startYear = calculatedStartYear < 0 ? 0 : (int) calculatedStartYear; + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger + WHERE delFlag = 0 + AND joined = 1 + AND jobNo = @jobNo + AND `year` >= @startYear + AND `year` <= @currentYear + AND lineType IN ('省内线路', '省内') + """ + excludeSql); + sql.setParam("jobNo", jobNo); + sql.setParam("startYear", startYear); + sql.setParam("currentYear", currentYear); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + dao().execute(sql); + if (sql.getInt() <= 0) { + return ""; + } + return "您在 " + startYear + " 至 " + currentYear + + " 年内已参加过省内线路,省内线路每 " + inProvinceYears + " 年可参加一次,当前不能报名"; + } + @Override @Aop(TransAop.READ_COMMITTED) public String saveWithUnionSignupQuota(TourLedger ledger, diff --git a/src/main/resources/db/tour_setting_create.sql b/src/main/resources/db/tour_setting_create.sql index 4dd97880..8e41e475 100644 --- a/src/main/resources/db/tour_setting_create.sql +++ b/src/main/resources/db/tour_setting_create.sql @@ -9,6 +9,7 @@ CREATE TABLE IF NOT EXISTS `tour_setting` ( `sortNo` int DEFAULT NULL COMMENT '排序编号', `minGroupPeople` int DEFAULT NULL COMMENT '最少成团人数', `maxGroupPeople` int DEFAULT NULL COMMENT '最多成团人数', + `inProvinceYears` int DEFAULT 0 COMMENT '省内几年去一次', `outProvinceYears` int DEFAULT NULL COMMENT '省外几年去一次', `outProvinceRatio` decimal(10,2) DEFAULT NULL COMMENT '省外人数占比', `outProvinceRatioType` varchar(50) DEFAULT '当年报名人数' COMMENT '省外人数占比类型', diff --git a/src/main/resources/views/platform/zhgh/dayofficework/Tour/setting/index.html b/src/main/resources/views/platform/zhgh/dayofficework/Tour/setting/index.html index 77e9777f..6cee45b5 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/Tour/setting/index.html +++ b/src/main/resources/views/platform/zhgh/dayofficework/Tour/setting/index.html @@ -154,6 +154,14 @@ layout("/layouts/platform.html"){
+ + + + + + +
+
@@ -364,6 +372,14 @@ layout("/layouts/platform.html"){ callback() } } + const checkInProvinceYears = (rule, value, callback) => { + if (value !== null && value !== undefined && value !== "" + && (!Number.isInteger(Number(value)) || Number(value) < 0)) { + callback(new Error("省内间隔年限必须为非负整数")) + } else { + callback() + } + } return { title: "", dialogVisible: false, @@ -388,6 +404,7 @@ layout("/layouts/platform.html"){ travelPeopleQuota: [{required: true, message: "必填", trigger: ["blur", "change"]}], minGroupPeople: [{validator: checkGroupPeople, trigger: ["blur", "change"]}], maxGroupPeople: [{validator: checkGroupPeople, trigger: ["blur", "change"]}], + inProvinceYears: [{validator: checkInProvinceYears, trigger: ["blur", "change"]}], outProvinceRatioType: [{required: true, message: "必填", trigger: ["blur", "change"]}], outProvinceFixedPeople: [{validator: checkOutProvinceFixedPeople, trigger: ["blur", "change"]}], cycleStartYear: [{validator: checkCycleYear, trigger: ["blur", "change"]}], @@ -539,6 +556,7 @@ layout("/layouts/platform.html"){ sortNo: 0, minGroupPeople: 0, maxGroupPeople: 0, + inProvinceYears: 0, outProvinceYears: 0, outProvinceRatio: 0, outProvinceRatioType: "当年报名人数", @@ -632,6 +650,7 @@ layout("/layouts/platform.html"){ this.formData = Object.assign({ travelPeopleQuota: 0, activityGroupId: "", + inProvinceYears: 0, outProvinceRatioType: "当年报名人数", outProvinceFixedPeople: 0, cycleStartYear: "", From fb25ad45a84644661198c25e301cdf5b07694707 Mon Sep 17 00:00:00 2001 From: zhouhefeng Date: Tue, 14 Jul 2026 18:05:53 +0800 Subject: [PATCH 2/2] commit --- ...ProposalExportComprehensiveController.java | 31 +++ .../transact/ProposalWriteController.java | 16 ++ .../controller/vo/ProposalWriteTypeVO.java | 21 ++ .../proposal/models/ProposalType.java | 2 +- .../service/ProposalExportService.java | 8 + .../service/ProposalWriteService.java | 17 ++ .../common/ProposalCommonServiceImpl.java | 2 +- .../impl/ProposalExportServiceImpl.java | 248 ++++++++++++++++++ .../impl/ProposalWriteServiceImpl.java | 68 +++++ .../TeacherCongressSessionController.java | 12 +- .../models/Teacher_congress_session.java | 6 +- .../TeacherCongressSessionService.java | 7 + .../TeacherCongressSessionServiceImpl.java | 125 ++++++++- .../proposal/export/comprehensive/index.html | 37 ++- .../proposal/transact/write/index.html | 20 +- .../prepare/session/index.html | 56 +++- .../proposal/transact/write/index.html | 29 +- 17 files changed, 660 insertions(+), 45 deletions(-) create mode 100644 src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/vo/ProposalWriteTypeVO.java diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/export/ProposalExportComprehensiveController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/export/ProposalExportComprehensiveController.java index aa9fabaa..fb37eeff 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/export/ProposalExportComprehensiveController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/export/ProposalExportComprehensiveController.java @@ -7,6 +7,7 @@ import com.budwk.app.base.param.ExportTableColumns; import com.budwk.app.base.result.Result; import com.budwk.app.zhgh.democratic.proposal.param.ProposalQueryComprehensiveParam; import com.budwk.app.zhgh.democratic.proposal.service.ProposalExportService; +import com.budwk.app.zhgh.democratic.proposal.service.ProposalWriteService; import com.budwk.app.zhgh.democratic.proposal.service.common.ProposalCommonService; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; @@ -32,6 +33,8 @@ public class ProposalExportComprehensiveController { private ProposalExportService proposalExportService; @Inject private ProposalCommonService proposalCommonService; + @Inject + private ProposalWriteService proposalWriteService; @At("") @Ok("beetl:/platform/zhgh/democratic/proposal/export/comprehensive/index.html") @@ -83,6 +86,19 @@ public class ProposalExportComprehensiveController { return Result.success(pagination); } + /** + * 查询综合导出页面当前教代会届次配置的提案类别。 + * + * @param sessionId 教代会届次ID,用于读取该届次配置的提案类别 + * @return VO 列表,每项包含筛选使用的 id 和页面展示使用的 name + */ + @At + @SaCheckPermission("proposal.export.comprehensive") + @ApiOperation("查询当前届次提案类别") + public Result listSessionProposalTypes(@Valid String sessionId) { + return Result.success(proposalWriteService.listSessionProposalTypes(sessionId)); + } + @At @Ok("void") @@ -92,6 +108,21 @@ public class ProposalExportComprehensiveController { proposalExportService.exportSummaryAsExcel(pageForm, response); } + /** + * 按办理单位导出提案承办单位表压缩包。 + * + * @param pageForm 综合导出页面查询参数,其中 sessionId 为必传的教代会届次ID + * @param response HTTP 响应,返回内容为包含多个 XSSF Excel 的 ZIP 文件 + */ + @At + @Ok("void") + @ApiOperation("按办理单位导出提案承办单位表压缩包") + @SaCheckPermission("proposal.query.comprehensive") + public void exportUndertakeUnitTablesAsZip(@Valid @Param("pageForm") ProposalQueryComprehensiveParam pageForm, + HttpServletResponse response) { + proposalExportService.exportUndertakeUnitTablesAsZip(pageForm, response); + } + @At @Ok("void") diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalWriteController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalWriteController.java index 52f0da5e..c05c1a5b 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalWriteController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/transact/ProposalWriteController.java @@ -76,6 +76,19 @@ public class ProposalWriteController { } + /** + * 查询当前教代会届次可用于撰写提案的类型。 + * + * @param sessionId 教代会届次ID + * @return VO 列表,每项包含保存使用的 id 和页面展示使用的 name + */ + @At + @SaCheckPermission("proposal.write") + @ApiOperation("查询届次可用提案类型") + public Result listSessionProposalTypes(@Valid String sessionId) { + return Result.success(proposalWriteService.listSessionProposalTypes(sessionId)); + } + @At @SaCheckPermission("proposal.write") @@ -84,6 +97,7 @@ public class ProposalWriteController { @SLog(tag = "提案管理系统-我的提案", msg = "保存提案") public Result save(@Param("info") ProposalInfo proposalInfo) { proposalWriteService.checkCurrentUserDelegate(proposalInfo.getSessionId()); + proposalWriteService.validateProposalType(proposalInfo.getSessionId(), proposalInfo.getTypeId()); if (StrUtil.isBlank(proposalInfo.getCode())) { proposalInfo.setCode(proposalWriteService.generateProposalCode(proposalInfo.getSessionId(), proposalInfo.getDelegationId())); } @@ -100,6 +114,7 @@ public class ProposalWriteController { @SLog(tag = "提案管理系统-我的提案", msg = "提交提案") public Result submit(@Param("info") ProposalInfo proposalInfo) { proposalWriteService.checkCurrentUserDelegate(proposalInfo.getSessionId()); + proposalWriteService.validateProposalType(proposalInfo.getSessionId(), proposalInfo.getTypeId()); if (StrUtil.isBlank(proposalInfo.getCode())) { proposalInfo.setCode(proposalWriteService.generateProposalCode(proposalInfo.getSessionId(), proposalInfo.getDelegationId())); } @@ -130,6 +145,7 @@ public class ProposalWriteController { @ApiOperation("重新提交提案") @SLog(tag = "提案管理系统-我的提案", msg = "重新提交提案") public Result submitAgain(@Param("info") ProposalInfo proposalInfo, @Param("taskId") Long taskId) { + proposalWriteService.validateProposalType(proposalInfo.getSessionId(), proposalInfo.getTypeId()); dao.insertOrUpdate(proposalInfo); Dict dict = Dict.create(); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/vo/ProposalWriteTypeVO.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/vo/ProposalWriteTypeVO.java new file mode 100644 index 00000000..d21a37a1 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/vo/ProposalWriteTypeVO.java @@ -0,0 +1,21 @@ +package com.budwk.app.zhgh.democratic.proposal.controller.vo; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; + +/** + * 撰写提案时当前教代会届次允许选择的提案类型。 + */ +@Data +@AllArgsConstructor +@ApiModel("届次可用提案类型") +public class ProposalWriteTypeVO { + + @ApiModelProperty("提案类型ID,保存提案时写入 proposal_info.typeId") + private Integer id; + + @ApiModelProperty("提案类型名称,来源于当前教代会届次的提案类型配置") + private String name; +} diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/models/ProposalType.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/models/ProposalType.java index f4968382..0f807be1 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/models/ProposalType.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/models/ProposalType.java @@ -25,7 +25,7 @@ public class ProposalType extends BaseModel { @Column @Comment("名称") - @ColDefine(type = ColType.VARCHAR, width = 10) + @ColDefine(type = ColType.VARCHAR, width = 50) private String name; @Column diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalExportService.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalExportService.java index ad65c3df..0015ea1a 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalExportService.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalExportService.java @@ -34,6 +34,14 @@ public interface ProposalExportService extends BaseService { */ void exportSummaryAsExcel(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response); + /** + * 按办理单位导出提案承办单位表压缩包。 + * + * @param pageForm 综合导出页面查询参数;sessionId 应传当前教代会届次ID,其他条件沿用页面查询条件 + * @param response HTTP 响应,方法直接输出 ZIP;ZIP 中每个有提案的单位对应一个 XSSF Excel + */ + void exportUndertakeUnitTablesAsZip(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response); + /** * 导出提案立案汇总表excel * diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalWriteService.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalWriteService.java index 10a932c4..ae07bd50 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalWriteService.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/ProposalWriteService.java @@ -2,6 +2,7 @@ package com.budwk.app.zhgh.democratic.proposal.service; import com.budwk.app.base.service.BaseService; import com.budwk.app.sys.models.Sys_dict; +import com.budwk.app.zhgh.democratic.proposal.controller.vo.ProposalWriteTypeVO; import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo; import javax.validation.Valid; @@ -10,6 +11,22 @@ import java.util.Map; public interface ProposalWriteService extends BaseService { + /** + * 查询指定教代会届次允许选择的提案类型。 + * + * @param sessionId 教代会届次ID + * @return 提案类型 VO 列表,id 用于保存,name 用于页面展示 + */ + List listSessionProposalTypes(String sessionId); + + /** + * 校验所选提案类型是否属于当前教代会届次。 + * + * @param sessionId 教代会届次ID + * @param typeId 提案类型ID + */ + void validateProposalType(String sessionId, Integer typeId); + List listSource(String sessionId); /** diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/common/ProposalCommonServiceImpl.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/common/ProposalCommonServiceImpl.java index f7fb81b2..7487dc25 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/common/ProposalCommonServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/common/ProposalCommonServiceImpl.java @@ -158,7 +158,7 @@ public class ProposalCommonServiceImpl extends BaseServiceImpl imp pages.put("preAudit", standardOrderColumns()); pages.put("committeeFiling", orderColumns("code", "info.code", "name", "info.name", "typeName", "type.name", "sessionName", "tcs.fullName", "caseFilingResult", "info.caseFilingResult", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state")); pages.put("caseCheck", orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "curTaskName", "curTaskName", "instanceState", "ins.state", "finishTime", "t.finishTime")); - pages.put("exportComprehensive", orderColumns("code", "info.code", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "caseFilingType", "info.caseFilingType", "merge", "merge", "brief", "info.brief", "measures", "info.measures", "undertakeUnits", "masterUnitName", "curTaskName", "curTaskName", "instanceState", "ins.state")); + pages.put("exportComprehensive", orderColumns("code", "info.code", "caseFilingCode", "info.caseFilingCode", "name", "info.name", "createUserName", "info.createUserName", "typeName", "type.name", "sessionName", "tcs.fullName", "delegationName", "tcd.name", "caseFilingResult", "info.caseFilingResult", "caseFilingType", "info.caseFilingType", "merge", "merge", "brief", "info.brief", "measures", "info.measures", "undertakeUnits", "masterUnitName", "curTaskName", "curTaskName", "instanceState", "ins.state")); pages.put("exportSingleCustom", standardOrderColumns()); pages.put("configUnit", orderColumns("name", "t1.name", "code", "t1.code", "unitLeader", "u2.username")); pages.put("configType", orderColumns("name", "name", "code", "code")); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalExportServiceImpl.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalExportServiceImpl.java index 5b23f4fd..721fdc83 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalExportServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalExportServiceImpl.java @@ -4,8 +4,10 @@ import cn.afterturn.easypoi.excel.ExcelExportUtil; import cn.afterturn.easypoi.excel.entity.ExportParams; import cn.afterturn.easypoi.excel.entity.enmus.ExcelType; import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity; +import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HtmlUtil; +import com.budwk.app.base.exception.BaseException; import com.budwk.app.base.param.ExportTableColumns; import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.utils.CommonDownloadUtil; @@ -27,7 +29,15 @@ import com.deepoove.poi.XWPFTemplate; import com.deepoove.poi.config.Configure; import com.deepoove.poi.plugin.table.LoopRowTableRenderPolicy; import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.CellRangeAddress; import org.apache.poi.xwpf.usermodel.BreakType; import org.apache.poi.xwpf.usermodel.IBodyElement; import org.apache.poi.xwpf.usermodel.XWPFDocument; @@ -51,6 +61,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -61,6 +73,9 @@ import java.util.zip.ZipOutputStream; @Slf4j public class ProposalExportServiceImpl extends BaseServiceImpl implements ProposalExportService { + private static final String WORK_SUGGESTION_SECTION_MARKER = "__WORK_SUGGESTION_SECTION__"; + private static final int UNDERTAKE_UNIT_EXPORT_COLUMN_COUNT = 8; + @Inject private ProposalCommonService proposalCommonService; @Inject @@ -246,6 +261,239 @@ public class ProposalExportServiceImpl extends BaseServiceImpl imp CommonDownloadUtil.download(title + ".xlsx", workbook, response); } + /** + * 按办理单位生成提案承办单位表。查询时忽略页面单个承办单位条件,其他查询条件继续生效; + * 同一提案涉及多个单位时,会进入每个相关单位的 Excel,并保留完整主办、协办单位信息。 + * + * @param pageForm 综合导出页面查询参数,sessionId 必须为当前教代会届次ID + * @param response HTTP 响应,直接输出包含多个 XSSF Excel 的 ZIP 文件 + */ + @Override + public void exportUndertakeUnitTablesAsZip(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response) { + if (pageForm == null || StrUtil.isBlank(pageForm.getSessionId())) { + throw new BaseException("请选择教代会"); + } + Teacher_congress_session session = dao().fetch(Teacher_congress_session.class, pageForm.getSessionId()); + if (session == null) { + throw new BaseException("教代会届次不存在"); + } + + List unitRows = queryUndertakeUnitExportRows(pageForm); + if (Lang.isEmpty(unitRows)) { + throw new BaseException("暂无可导出的承办单位提案数据"); + } + + Map proposalRows = new LinkedHashMap<>(); + Map unitNames = new LinkedHashMap<>(); + Map> unitProposalIds = new LinkedHashMap<>(); + for (NutMap row : unitRows) { + String proposalId = row.getString("id"); + String unitId = row.getString("unitId"); + if (StrUtil.isBlank(proposalId) || StrUtil.isBlank(unitId)) { + continue; + } + String unitName = StrUtil.blankToDefault(row.getString("unitName"), "未命名单位"); + NutMap proposalRow = proposalRows.computeIfAbsent(proposalId, key -> NutMap.NEW() + .addv("id", proposalId) + .addv("code", row.getString("code")) + .addv("caseFilingCode", row.getString("caseFilingCode")) + .addv("name", row.getString("name")) + .addv("createUserName", row.getString("createUserName")) + .addv("caseFilingResult", row.getString("caseFilingResult")) + .addv("masterUnitNames", new LinkedHashSet()) + .addv("slaveUnitNames", new LinkedHashSet())); + getUnitNameSet(proposalRow, Boolean.TRUE.equals(row.getBoolean("isMaster")) + ? "masterUnitNames" : "slaveUnitNames").add(unitName); + unitNames.putIfAbsent(unitId, unitName); + unitProposalIds.computeIfAbsent(unitId, key -> new LinkedHashSet<>()).add(proposalId); + } + if (unitProposalIds.isEmpty()) { + throw new BaseException("暂无可导出的承办单位提案数据"); + } + + String title = StrUtil.format("{}第{}教职工代表大会第{}会议提案承办单位表", + Globals.AppName, session.getJ(), session.getC()); + ByteArrayOutputStream zipBytes = new ByteArrayOutputStream(); + Map zipEntryNameCounts = new HashMap<>(); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(zipBytes)) { + for (Map.Entry> unitEntry : unitProposalIds.entrySet()) { + String unitName = unitNames.get(unitEntry.getKey()); + List excelRows = buildUndertakeUnitExcelRows(unitEntry.getValue(), proposalRows); + try (Workbook workbook = buildUndertakeUnitWorkbook(title, unitName, excelRows); + ByteArrayOutputStream workbookBytes = new ByteArrayOutputStream()) { + workbook.write(workbookBytes); + String baseEntryName = "提案承办单位表-" + sanitizeZipFileName(unitName); + int sameNameCount = zipEntryNameCounts.merge(baseEntryName, 1, Integer::sum); + String entryName = baseEntryName + (sameNameCount > 1 ? "-" + sameNameCount : "") + ".xlsx"; + zipOutputStream.putNextEntry(new ZipEntry(entryName)); + workbookBytes.writeTo(zipOutputStream); + zipOutputStream.closeEntry(); + } + } + zipOutputStream.finish(); + } catch (IOException e) { + log.error("按办理单位导出提案承办单位表失败:{}", e.getMessage(), e); + throw new BaseException("导出提案承办单位表失败"); + } + CommonDownloadUtil.download(sanitizeZipFileName(title) + ".zip", zipBytes.toByteArray(), response); + } + + /** + * 查询当前届次已确定立案或作为工作建议的提案及其全部办理单位。 + * 页面承办单位单选条件会被清空,避免只生成一个单位文件;其他页面条件继续沿用。 + */ + private List queryUndertakeUnitExportRows(ProposalQueryComprehensiveParam pageForm) { + ProposalQueryComprehensiveParam exportPageForm = new ProposalQueryComprehensiveParam(); + BeanUtil.copyProperties(pageForm, exportPageForm); + exportPageForm.setUnderTakeUnitId(null); + + Sql sql = Sqls.create(""" + SELECT DISTINCT + info.id, + info.code, + info.caseFilingCode, + info.name, + info.createUserName, + info.caseFilingResult, + pru.unitId, + pru.unitName, + pru.isMaster + FROM + proposal_info info + LEFT JOIN proposal_type type ON type.id = info.typeId + LEFT JOIN teacher_congress_session tcs ON tcs.id = info.sessionId + LEFT JOIN teacher_congress_delegation tcd ON tcd.id = info.delegationId + LEFT JOIN teacher_congress_delegate tcde ON tcde.loginName = info.createUserLoginName + INNER JOIN proposal_reply_unit pru ON pru.proposalId = info.id + $condition + """); + Cnd cnd = Cnd.where("info.sessionId", "=", pageForm.getSessionId()); + cnd.and("info.caseFilingResult", "in", List.of("CONFIRM_FILING", "SUGGESTION")); + ProposalQueryComprehensiveParam.buildSearch(cnd, exportPageForm); + cnd.asc("info.code"); + cnd.asc("pru.unitName"); + sql.setCondition(cnd); + return listMap(sql); + } + + /** + * 将某个办理单位涉及的提案整理为 Excel 行。确定立案在前,工作建议通过标记行单独分组。 + */ + private List buildUndertakeUnitExcelRows(LinkedHashSet proposalIds, + Map proposalRows) { + List filingRows = new ArrayList<>(); + List suggestionRows = new ArrayList<>(); + for (String proposalId : proposalIds) { + NutMap proposalRow = proposalRows.get(proposalId); + if (proposalRow == null) { + continue; + } + LinkedHashSet masterUnitNames = getUnitNameSet(proposalRow, "masterUnitNames"); + LinkedHashSet slaveUnitNames = getUnitNameSet(proposalRow, "slaveUnitNames"); + NutMap excelRow = NutMap.NEW() + .addv("code", proposalRow.getString("code")) + .addv("caseFilingCode", proposalRow.getString("caseFilingCode")) + .addv("name", proposalRow.getString("name")) + .addv("createUserName", proposalRow.getString("createUserName")) + .addv("masterUnitNames", "") + .addv("slaveUnitNames", "") + .addv("workSuggestionUnitNames", "") + .addv("remark", ""); + if ("CONFIRM_FILING".equals(proposalRow.getString("caseFilingResult"))) { + excelRow.put("masterUnitNames", String.join(",", masterUnitNames)); + excelRow.put("slaveUnitNames", String.join(",", slaveUnitNames)); + filingRows.add(excelRow); + } else { + LinkedHashSet workSuggestionUnitNames = new LinkedHashSet<>(masterUnitNames); + workSuggestionUnitNames.addAll(slaveUnitNames); + excelRow.put("workSuggestionUnitNames", String.join(",", workSuggestionUnitNames)); + suggestionRows.add(excelRow); + } + } + if (!suggestionRows.isEmpty()) { + filingRows.add(NutMap.NEW() + .addv("code", WORK_SUGGESTION_SECTION_MARKER) + .addv("caseFilingCode", "") + .addv("name", "") + .addv("createUserName", "") + .addv("masterUnitNames", "") + .addv("slaveUnitNames", "") + .addv("workSuggestionUnitNames", "") + .addv("remark", "")); + filingRows.addAll(suggestionRows); + } + return filingRows; + } + + /** + * 创建单个办理单位的 XSSF Excel,并把工作建议标记行合并为分组标题。 + */ + private Workbook buildUndertakeUnitWorkbook(String title, String unitName, List excelRows) { + List exportEntities = new ArrayList<>(); + exportEntities.add(new ExcelExportEntity("提案编号", "code", 18)); + exportEntities.add(new ExcelExportEntity("立案编号", "caseFilingCode", 18)); + exportEntities.add(new ExcelExportEntity("提案名称", "name", 45)); + exportEntities.add(new ExcelExportEntity("提案人", "createUserName", 15)); + exportEntities.add(new ExcelExportEntity("主办单位", "masterUnitNames", 28)); + exportEntities.add(new ExcelExportEntity("协办单位", "slaveUnitNames", 28)); + exportEntities.add(new ExcelExportEntity("工作建议送达单位", "workSuggestionUnitNames", 32)); + exportEntities.add(new ExcelExportEntity("备注", "remark", 20)); + for (ExcelExportEntity exportEntity : exportEntities) { + exportEntity.setWrap(true); + } + + ExportParams exportParams = new ExportParams(); + exportParams.setType(ExcelType.XSSF); + exportParams.setTitle(title); + exportParams.setSecondTitle("(" + unitName + ")"); + exportParams.setSheetName("提案承办单位表"); + exportParams.setHeaderHeight(30); + exportParams.setHeight((short) 24); + Workbook workbook = ExcelExportUtil.exportExcel(exportParams, exportEntities, excelRows); + mergeWorkSuggestionSection(workbook); + return workbook; + } + + /** + * 查找工作建议标记行,将八个单元格合并并设置为居中的分组标题。 + */ + private void mergeWorkSuggestionSection(Workbook workbook) { + Sheet sheet = workbook.getSheetAt(0); + for (Row row : sheet) { + Cell firstCell = row.getCell(0); + if (firstCell == null || !WORK_SUGGESTION_SECTION_MARKER.equals(firstCell.toString())) { + continue; + } + CellStyle sectionStyle = workbook.createCellStyle(); + sectionStyle.cloneStyleFrom(firstCell.getCellStyle()); + sectionStyle.setAlignment(HorizontalAlignment.CENTER); + sectionStyle.setVerticalAlignment(VerticalAlignment.CENTER); + Font sectionFont = workbook.createFont(); + sectionFont.setFontName("宋体"); + sectionFont.setFontHeightInPoints((short) 11); + sectionFont.setBold(true); + sectionStyle.setFont(sectionFont); + for (int columnIndex = 0; columnIndex < UNDERTAKE_UNIT_EXPORT_COLUMN_COUNT; columnIndex++) { + Cell cell = row.getCell(columnIndex); + if (cell == null) { + cell = row.createCell(columnIndex); + } + cell.setCellValue(columnIndex == 0 ? "作为工作建议的提案" : ""); + cell.setCellStyle(sectionStyle); + } + row.setHeightInPoints(24); + sheet.addMergedRegion(new CellRangeAddress(row.getRowNum(), row.getRowNum(), 0, + UNDERTAKE_UNIT_EXPORT_COLUMN_COUNT - 1)); + return; + } + } + + /** 获取提案行中用于去重且保持顺序的办理单位名称集合。 */ + @SuppressWarnings("unchecked") + private LinkedHashSet getUnitNameSet(NutMap proposalRow, String key) { + return (LinkedHashSet) proposalRow.get(key); + } + @Override public void exportProposalRegisterSummaryAsExcel(ProposalQueryComprehensiveParam pageForm, HttpServletResponse response) { Sql sql = exportComprehensiveSql(pageForm); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalWriteServiceImpl.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalWriteServiceImpl.java index a4e08b4e..adc755b5 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalWriteServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/service/impl/ProposalWriteServiceImpl.java @@ -4,6 +4,7 @@ import cn.hutool.core.convert.Convert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.StrUtil; import com.budwk.app.base.constant.RoleConstant; +import com.budwk.app.base.exception.BaseException; import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.sys.models.Sys_dict; import com.budwk.app.sys.models.Sys_user; @@ -11,6 +12,7 @@ import com.budwk.app.sys.services.SysDictService; import com.budwk.app.sys.services.SysUserService; import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.zhgh.democratic.proposal.constants.ProposalState; +import com.budwk.app.zhgh.democratic.proposal.controller.vo.ProposalWriteTypeVO; import com.budwk.app.zhgh.democratic.proposal.models.ProposalInfo; import com.budwk.app.zhgh.democratic.proposal.models.ProposalType; import com.budwk.app.zhgh.democratic.proposal.service.ProposalWriteService; @@ -24,6 +26,7 @@ import org.nutz.dao.Sqls; import org.nutz.dao.sql.Sql; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.json.Json; import org.nutz.lang.Strings; import java.util.ArrayList; @@ -44,6 +47,70 @@ public class ProposalWriteServiceImpl extends BaseServiceImpl impl super(dao); } + @Override + public List listSessionProposalTypes(String sessionId) { + List typeNames = getSessionProposalTypeNames(sessionId); + if (typeNames.isEmpty()) { + return List.of(); + } + List proposalTypes = dao().query(ProposalType.class, + Cnd.where(ProposalType::getName, "in", typeNames)); + List result = new ArrayList<>(); + for (String typeName : typeNames) { + ProposalType proposalType = proposalTypes.stream() + .filter(item -> typeName.equals(item.getName())) + .findFirst() + .orElse(null); + if (proposalType != null) { + result.add(new ProposalWriteTypeVO(proposalType.getId(), proposalType.getName())); + } + } + return result; + } + + @Override + public void validateProposalType(String sessionId, Integer typeId) { + if (typeId == null) { + throw new BaseException("请选择提案类型"); + } + ProposalType proposalType = dao().fetch(ProposalType.class, typeId); + if (proposalType == null || !getSessionProposalTypeNames(sessionId).contains(proposalType.getName())) { + throw new BaseException("所选提案类型不属于当前教代会届次"); + } + } + + /** + * 解析指定届次配置的提案类型。标准数据为 JSON 数组,同时兼容历史单值字符串。 + * + * @param sessionId 教代会届次ID + * @return 按届次配置顺序排列的提案类型名称;未配置时返回空列表 + */ + private List getSessionProposalTypeNames(String sessionId) { + if (StrUtil.isBlank(sessionId)) { + throw new BaseException("所属教代会不能为空"); + } + Teacher_congress_session session = dao().fetch(Teacher_congress_session.class, sessionId); + if (session == null) { + throw new BaseException("所属教代会不存在"); + } + if (StrUtil.isBlank(session.getProposalType())) { + return List.of(); + } + String proposalTypeValue = StrUtil.trim(session.getProposalType()); + if (!proposalTypeValue.startsWith("[")) { + return List.of(proposalTypeValue); + } + try { + return Json.fromJsonAsList(String.class, proposalTypeValue).stream() + .map(StrUtil::trim) + .filter(StrUtil::isNotBlank) + .distinct() + .toList(); + } catch (Exception e) { + throw new BaseException("教代会届次提案类型配置格式不正确"); + } + } + @Override public List listSource(String sessionId) { Sql sql = Sqls.create(""" @@ -224,6 +291,7 @@ public class ProposalWriteServiceImpl extends BaseServiceImpl impl throw new RuntimeException("没有开启的教代会"); } Teacher_congress_session teacherCongressSession = teacherCongressSessions.get(0); + validateProposalType(teacherCongressSession.getId(), proposalTypeObj.getId()); // 保存到数据库 ProposalInfo proposalInfo = new ProposalInfo(); diff --git a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/controller/TeacherCongressSessionController.java b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/controller/TeacherCongressSessionController.java index 301bf59d..92bdb5cf 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/controller/TeacherCongressSessionController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/controller/TeacherCongressSessionController.java @@ -1,7 +1,6 @@ package com.budwk.app.zhgh.democratic.teachercongress.prepare.controller; import cn.dev33.satoken.annotation.SaCheckPermission; -import com.budwk.app.base.exception.BaseException; import com.budwk.app.base.page.Pagination; import com.budwk.app.base.param.PageForm; import com.budwk.app.base.result.Result; @@ -87,16 +86,7 @@ public class TeacherCongressSessionController { @SaCheckPermission("tc.prepare.session") @ApiOperation(value = "修改届次信息") public Result update(@Valid Teacher_congress_session session) { - int count = dao.count(Teacher_congress_session.class, - Cnd.where(Teacher_congress_session::getJ, "=", session.getJ()) - .and(Teacher_congress_session::getC, "=", session.getC()) - .and(Teacher_congress_session::getId, "!=", session.getId()) - ); - if (count > 0) { - throw new BaseException("请勿重复创建"); - } - session.setFullName("第" + session.getJ() + "第" + session.getC()); - dao.updateIgnoreNull(session); + teacherCongressSessionService.updateSession(session); return Result.success(); } diff --git a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/models/Teacher_congress_session.java b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/models/Teacher_congress_session.java index 4740f845..caaadb81 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/models/Teacher_congress_session.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/models/Teacher_congress_session.java @@ -44,10 +44,10 @@ public class Teacher_congress_session extends BaseModel { private String fullName; @Column - @Comment("提案类型") - @ColDefine(type = ColType.VARCHAR, width = 50) + @Comment("提案类型(JSON数组)") + @ColDefine(type = ColType.VARCHAR, width = 1000) @NotBlank(message = "提案类型不能为空") - @Size(max = 50, message = "提案类型最多50个字") + @Size(max = 1000, message = "提案类型数据不能超过1000个字符") private String proposalType; @Column diff --git a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/service/TeacherCongressSessionService.java b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/service/TeacherCongressSessionService.java index f568cf82..8e61c20a 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/service/TeacherCongressSessionService.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/teachercongress/prepare/service/TeacherCongressSessionService.java @@ -20,5 +20,12 @@ public interface TeacherCongressSessionService extends BaseService '' ORDER BY proposalType"); sql.setCallback(Sqls.callback.strList()); dao().execute(sql); - return sql.getList(String.class).stream() + LinkedHashSet proposalTypes = new LinkedHashSet<>(); + for (String proposalTypeValue : sql.getList(String.class)) { + for (String proposalType : parseProposalTypes(proposalTypeValue)) { + String normalizedProposalType = StrUtil.trim(proposalType); + if (StrUtil.isNotBlank(normalizedProposalType)) { + proposalTypes.add(normalizedProposalType); + } + } + } + return proposalTypes.stream() + .sorted() .map(TeacherCongressSessionProposalTypeVO::new) .toList(); } @@ -74,6 +88,7 @@ public class TeacherCongressSessionServiceImpl extends BaseServiceImpl 0) { @@ -257,6 +272,114 @@ public class TeacherCongressSessionServiceImpl extends BaseServiceImpl values = parseProposalTypes(session.getProposalType()); + LinkedHashSet normalizedValues = new LinkedHashSet<>(); + for (String value : values) { + String normalizedValue = StrUtil.trim(value); + if (StrUtil.isBlank(normalizedValue)) { + continue; + } + if (normalizedValue.length() > 50) { + throw new BaseException("单个提案类型不能超过50个字符"); + } + normalizedValues.add(normalizedValue); + } + if (normalizedValues.isEmpty()) { + throw new BaseException("请至少选择或输入一个提案类型"); + } + + String proposalTypeJson = Json.toJson(new ArrayList<>(normalizedValues)); + if (proposalTypeJson.length() > 1000) { + throw new BaseException("提案类型数据不能超过1000个字符"); + } + session.setProposalType(proposalTypeJson); + syncProposalTypes(normalizedValues); + } + + /** + * 将届次中新输入的类型同步到全局提案类型表,以便继续使用 proposal_info.typeId + * 以及现有列表、统计和导出的关联查询。已有同名类型直接复用,不删除历史类型。 + * + * @param proposalTypeNames 当前届次规范化、去重后的提案类型名称 + */ + private void syncProposalTypes(LinkedHashSet proposalTypeNames) { + ProposalType lastType = dao().fetch(ProposalType.class, Cnd.NEW().desc(ProposalType::getSort)); + int nextSort = lastType == null || lastType.getSort() == null ? 1 : lastType.getSort() + 1; + for (String proposalTypeName : proposalTypeNames) { + ProposalType proposalType = dao().fetch(ProposalType.class, + Cnd.where(ProposalType::getName, "=", proposalTypeName)); + if (proposalType != null) { + continue; + } + ProposalType newProposalType = new ProposalType(); + newProposalType.setCode(generateProposalTypeCode()); + newProposalType.setName(proposalTypeName); + newProposalType.setSort(nextSort++); + dao().insert(newProposalType); + } + } + + /** + * 生成不超过 10 个字符的内部提案类型编码。 + * + * @return 以 TC 开头且在 proposal_type 表中未使用的编码 + */ + private String generateProposalTypeCode() { + String code; + do { + code = "TC" + R.UU32().substring(0, 8).toUpperCase(); + } while (dao().count(ProposalType.class, Cnd.where(ProposalType::getCode, "=", code)) > 0); + return code; + } + + /** + * 解析届次中保存的提案类型。标准数据为 JSON 字符串数组,同时兼容调整前可能存在的单值数据。 + * + * @param proposalTypeValue 数据库字段值或前端提交的 JSON 字符串 + * @return 提案类型字符串列表;空值返回空列表 + */ + private List parseProposalTypes(String proposalTypeValue) { + if (StrUtil.isBlank(proposalTypeValue)) { + return List.of(); + } + String value = proposalTypeValue.trim(); + if (!value.startsWith("[")) { + return List.of(value); + } + try { + return Json.fromJsonAsList(String.class, value); + } catch (Exception e) { + throw new BaseException("提案类型数据格式不正确"); + } + } + @Override @Aop(TransAop.READ_COMMITTED) public void deleteSession(String id) { diff --git a/src/main/resources/views/platform/zhgh/democratic/proposal/export/comprehensive/index.html b/src/main/resources/views/platform/zhgh/democratic/proposal/export/comprehensive/index.html index f8233d47..8441e7ab 100644 --- a/src/main/resources/views/platform/zhgh/democratic/proposal/export/comprehensive/index.html +++ b/src/main/resources/views/platform/zhgh/democratic/proposal/export/comprehensive/index.html @@ -88,7 +88,7 @@ layout("/layouts/platform.html"){
@@ -125,6 +125,7 @@ layout("/layouts/platform.html"){ 导出反馈表压缩包 导出汇总表 + 导出承办单位表压缩包 自定义导出 @@ -205,6 +206,7 @@ layout("/layouts/platform.html"){ }, tableColumns: [ {label: "提案编号", prop: "code"}, + {label: "立案编号", prop: "caseFilingCode"}, {label: "提案名称", prop: "name"}, {label: "提案人", prop: "createUserName"}, {label: "提案类别", prop: "typeName"}, @@ -290,6 +292,15 @@ layout("/layouts/platform.html"){ pageForm: JSON.stringify(this.pageForm) }) }, + exportUndertakeUnitTablesAsZip() { + if (!this.pageForm.sessionId) { + this.$message.warning("请选择教代会") + return + } + this.$downLoad(loc() + "/exportUndertakeUnitTablesAsZip", { + pageForm: JSON.stringify(this.pageForm) + }) + }, exportProposalRegisterSummaryAsExcel() { this.$downLoad("/platform/proposal/export/comprehensive/exportProposalRegisterSummaryAsExcel", { pageForm: JSON.stringify(this.pageForm) @@ -313,15 +324,25 @@ layout("/layouts/platform.html"){ }, listProposalType() { - this.$axios.post("/platform/proposal/common/listProposalType").then((res) => { + if (!this.pageForm.sessionId) { + this.$set(this, "typeOptions", []) + return Promise.resolve() + } + return this.$axios.post(loc() + "/listSessionProposalTypes", { + sessionId: this.pageForm.sessionId + }).then((res) => { if (res.code === 0) { - this.typeOptions = res.data + this.$set(this, "typeOptions", res.data) } }) }, - async meetingChange(val) { - this.pageForm.delegationId = null + meetingChange(val) { + // 切换届次后旧类别不再适用,需清空选择并加载当前届次类别 + this.$set(this.pageForm, "typeIds", []) + this.$set(this, "typeOptions", []) + this.listProposalType() + this.$set(this.pageForm, "delegationId", null) this.listDelegation() }, listDelegation() { @@ -334,9 +355,10 @@ layout("/layouts/platform.html"){ listSession() { this.$axios.post("/platform/proposal/common/listSession").then((res) => { if (res.code === 0) { - this.sessionOptions = res.data - if (this.sessionOptions) { + this.$set(this, "sessionOptions", res.data) + if (this.sessionOptions && this.sessionOptions.length > 0) { this.$set(this.pageForm, "sessionId", this.sessionOptions[0].id) + this.listProposalType() this.listDelegation() this.pageData() } @@ -379,7 +401,6 @@ layout("/layouts/platform.html"){ this.listUnderTake() this.listUnion() this.listUnit() - this.listProposalType() } }) diff --git a/src/main/resources/views/platform/zhgh/democratic/proposal/transact/write/index.html b/src/main/resources/views/platform/zhgh/democratic/proposal/transact/write/index.html index 8c3d2be8..b4638741 100644 --- a/src/main/resources/views/platform/zhgh/democratic/proposal/transact/write/index.html +++ b/src/main/resources/views/platform/zhgh/democratic/proposal/transact/write/index.html @@ -101,7 +101,7 @@ layout("/layouts/platform.html"){ - @@ -270,7 +270,7 @@ layout("/layouts/platform.html"){ delegationId: [{required: true, message: "请选择所属代表团", trigger: ["blur", "change"]}], sessionId: [{required: true, message: "请选择所属教代会", trigger: ["blur", "change"]}], committeeId: [{required: true, message: "请选择所属委员会", trigger: ["blur", "change"]}], - typeId: [{required: true, message: "请选择提案类别", trigger: ["blur", "change"]}], + typeId: [{required: true, message: "请选择提案类型", trigger: ["blur", "change"]}], suggestUnits: [{required: true, message: "请选择建议办理单位", trigger: ["blur", "change"]}], sign: [{required: true, message: "请扫描二维码进行签字", trigger: ["blur", "change"]}], excerpt: [{required: true, message: "请填写", trigger: ["blur", "change"]}], @@ -446,9 +446,15 @@ layout("/layouts/platform.html"){ } }, - // 获取提案类别 + // 根据当前教代会届次获取允许撰写的提案类型 listProposalType() { - this.$axios.post("/platform/proposal/common/listProposalType").then((res) => { + if (!this.formData.sessionId) { + this.typeOptions = [] + return Promise.resolve() + } + return this.$axios.post("/platform/proposal/write/listSessionProposalTypes", { + sessionId: this.formData.sessionId + }).then((res) => { if (res.code === 0) { this.typeOptions = res.data } @@ -466,6 +472,10 @@ layout("/layouts/platform.html"){ //教代会change async meetingChange(val) { + // 切换届次后,原提案类型不再有效,需按新届次重新选择 + this.$set(this.formData, "typeId", null) + this.typeOptions = [] + this.listProposalType() this.formData.delegationId = null this.formData.committeeId = null this.listDelegation() @@ -506,6 +516,7 @@ layout("/layouts/platform.html"){ this.checkWriteTime() } await this.listDelegation() + this.listProposalType() // await this.listSource() } }) @@ -578,7 +589,6 @@ layout("/layouts/platform.html"){ created() { this.init() this.listSuggestUnit() - this.listProposalType() this.getProposalConfig() } }) diff --git a/src/main/resources/views/platform/zhgh/democratic/teachercongress/prepare/session/index.html b/src/main/resources/views/platform/zhgh/democratic/teachercongress/prepare/session/index.html index 5f6d2be9..3bf67ffd 100644 --- a/src/main/resources/views/platform/zhgh/democratic/teachercongress/prepare/session/index.html +++ b/src/main/resources/views/platform/zhgh/democratic/teachercongress/prepare/session/index.html @@ -35,7 +35,12 @@ layout("/layouts/platform.html"){ - + + + @@ -79,9 +84,9 @@ layout("/layouts/platform.html"){ - + - @@ -143,6 +148,23 @@ layout("/layouts/platform.html"){ el: "#app", mixins: [initTableMixins], data() { + // 多选项至少保留一项,单个自定义类型最多 50 个字符,序列化后总长度不超过数据库字段限制。 + const validateProposalTypes = (rule, value, callback) => { + const proposalTypes = value || [] + if (!proposalTypes.length) { + callback(new Error("请至少选择或输入一个提案类型")) + return + } + if (proposalTypes.some((item) => item && item.trim().length > 50)) { + callback(new Error("单个提案类型最多50个字符")) + return + } + if (JSON.stringify(proposalTypes).length > 1000) { + callback(new Error("提案类型数据不能超过1000个字符")) + return + } + callback() + } return { dialogFormVisible: false, formRules: { @@ -150,8 +172,7 @@ layout("/layouts/platform.html"){ j: [{required: true, message: "必填", trigger: ["change", "blur"]}], c: [{required: true, message: "必填", trigger: ["change", "blur"]}], proposalType: [ - {required: true, message: "请选择或输入提案类型", trigger: ["change", "blur"]}, - {max: 50, message: "提案类型最多50个字", trigger: ["change", "blur"]} + {validator: validateProposalTypes, trigger: ["change", "blur"]} ], enable: [{required: true, message: "必填", trigger: ["change", "blur"]}], description: [{required: true, message: "必填", trigger: ["change", "blur"]}], @@ -167,7 +188,7 @@ layout("/layouts/platform.html"){ this.dialogFormVisible = true this.$nextTick(() => { this.formData = {} - this.$set(this.formData, "proposalType", "") + this.$set(this.formData, "proposalType", []) }) }, openEdit(id) { @@ -175,6 +196,7 @@ layout("/layouts/platform.html"){ this.$axios.post(loc() + "/findOne", {id}).then((res) => { if (res.code === 0) { res.data.year = res.data.year.toString() + this.$set(res.data, "proposalType", this.parseProposalTypes(res.data.proposalType)) this.formData = res.data } }) @@ -184,7 +206,11 @@ layout("/layouts/platform.html"){ this.$refs.formRef.validate((valid) => { if (valid) { const loading = createLoading() - this.$axios.post(loc() + (this.formData.id ? "/update" : "/insert"), this.formData).then((res) => { + const submitData = { + ...this.formData, + proposalType: JSON.stringify(this.formData.proposalType || []) + } + this.$axios.post(loc() + (this.formData.id ? "/update" : "/insert"), submitData).then((res) => { if (res.code === 0) { this.dialogFormVisible = false this.$message.success(res.msg) @@ -198,6 +224,22 @@ layout("/layouts/platform.html"){ }) }, + // 数据库存储 JSON 数组;同时兼容字段调整前可能存在的单值内容。 + parseProposalTypes(proposalType) { + if (Array.isArray(proposalType)) { + return proposalType + } + if (!proposalType) { + return [] + } + try { + const values = JSON.parse(proposalType) + return Array.isArray(values) ? values : [proposalType] + } catch (e) { + return [proposalType] + } + }, + doDelete(id) { this.$confirm("您确定要删除吗?", "提示", { confirmButtonText: "确定", diff --git a/src/main/resources/views/platform/zhghh5/democratic/proposal/transact/write/index.html b/src/main/resources/views/platform/zhghh5/democratic/proposal/transact/write/index.html index 28db6adc..c671b056 100644 --- a/src/main/resources/views/platform/zhghh5/democratic/proposal/transact/write/index.html +++ b/src/main/resources/views/platform/zhghh5/democratic/proposal/transact/write/index.html @@ -210,10 +210,10 @@ layout("/layouts/platform_h5.html"){ is-link readonly name="typeName" - label="提案类别" - placeholder="请选择提案类别" + label="提案类型" + placeholder="请选择提案类型" @click="showTypePicker = true" - :rules="[{ required: true, message: '请选择提案类别' }]" + :rules="[{ required: true, message: '请选择提案类型' }]" required > @@ -411,7 +411,7 @@ layout("/layouts/platform_h5.html"){ }, onTypeConfirm(value) { - this.formData.typeId = value.value; + this.$set(this.formData, 'typeId', value.value); this.$set(this.formData, 'typeName', value.text); this.showTypePicker = false; }, @@ -589,9 +589,15 @@ layout("/layouts/platform_h5.html"){ } }, - // 获取提案类别 + // 根据当前教代会届次获取允许撰写的提案类型 listProposalType() { - this.$axios.post("/platform/proposal/common/listProposalType").then((res) => { + if (!this.formData.sessionId) { + this.typeOptions = []; + return Promise.resolve(); + } + return this.$axios.post("/platform/proposal/write/listSessionProposalTypes", { + sessionId: this.formData.sessionId + }).then((res) => { if (res.code === 0) { this.typeOptions = res.data; } @@ -609,6 +615,11 @@ layout("/layouts/platform_h5.html"){ //教代会change async meetingChange(val) { + // 切换届次后清空旧值,并加载新届次允许选择的提案类型 + this.$set(this.formData, "typeId", null); + this.$set(this.formData, "typeName", ""); + this.typeOptions = []; + this.listProposalType(); this.formData.delegationId = null; this.formData.committeeId = null; this.formData.delegationName = ''; @@ -646,6 +657,7 @@ layout("/layouts/platform_h5.html"){ if (!isModify && this.sessionOptions && this.sessionOptions.length > 0) { this.$set(this.formData, "sessionId", this.sessionOptions[0].id); this.$set(this.formData, "sessionName", this.sessionOptions[0].fullName); + this.listProposalType(); await this.listDelegation(); await this.listSource(); // 获取代表团 @@ -655,7 +667,9 @@ layout("/layouts/platform_h5.html"){ } else { await this.listDelegation(); await this.listSource(); - this.echoPicker() + this.listProposalType().then(() => { + this.echoPicker(); + }); } } }); @@ -742,7 +756,6 @@ layout("/layouts/platform_h5.html"){ created() { this.init(); this.listSuggestUnit(); - this.listProposalType(); this.getProposalConfig(); }, watch: {