diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLineController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLineController.java index b6758ac0..d2a5127d 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLineController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLineController.java @@ -209,7 +209,7 @@ public class TourLineController { return Result.error("创建年度不能为空"); } if (StrUtil.isBlank(line.getTravelAgencyId())) { - return Result.error("旅行社名称不能为空"); + return Result.error("服务单位名称不能为空"); } if (StrUtil.isBlank(line.getLineName())) { return Result.error("线路名称不能为空"); 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 06d75f2c..104984e2 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 @@ -466,7 +466,13 @@ public class TourMySignupController { ledger.setSignupTime(defaultIfBlank(oldLedger.getSignupTime(), ledger.getSignupTime())); fillStaffInfo(ledger); - tourLedgerService.updateIgnoreNull(ledger); + // 修改报名也重新锁定并校验当前分工会名额,兼容人员所属分工会发生变化的场景。 + String quotaMessage = tourLedgerService.saveWithUnionSignupQuota( + ledger, matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(), true); + if (StrUtil.isNotBlank(quotaMessage)) { + return Result.error(quotaMessage); + } + tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId())); tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); if (Lang.isNotEmpty(familyList)) { 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 08dab7e9..a91a061d 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 @@ -69,6 +69,8 @@ public class TourSettingController { Cnd lotCnd = Cnd.NEW(); lotCnd.desc(TourSettingLot::getLotValue); tourSettingService.dao().fetchLinks(tourSetting, "lots", lotCnd); + // 编辑时同时返回分工会人员数量,确保页面按当前组织和人员总数完整回显。 + tourSetting.setUnionQuotas(tourSettingService.listUnionQuotaRows(id)); return Result.success(tourSetting); } @@ -89,15 +91,30 @@ public class TourSettingController { Cnd lotCnd = Cnd.NEW(); lotCnd.desc(TourSettingLot::getLotValue); tourSettingService.dao().fetchLinks(tourSetting, "lots", lotCnd); + // 延用上一年配置时带出原分配数量,前端会清除子表ID后作为新配置提交。 + tourSetting.setUnionQuotas(tourSettingService.listUnionQuotaRows(tourSetting.getId())); return Result.success(tourSetting); } + /** + * 查询人员分配页签所需的全部分工会、当前人员总数和已保存人员数量。 + * + * @param settingId 疗休养配置ID,新增时为空 + * @return 分工会人员分配行 + */ + @At + @SaCheckPermission("tour.setting") + public Result unionQuotaRows(String settingId) { + return Result.success(tourSettingService.listUnionQuotaRows(settingId)); + } + @At @Aop(TransAop.READ_COMMITTED) @SaCheckPermission("tour.setting") @SLog(type = "tour", tag = "疗休养设置", msg = "保存疗休养配置") public Result doSubmit(TourSetting tourSetting, @Param(value = "lots") String lots, + @Param(value = "unionQuotas") String unionQuotas, @Param(value = "lotDeleteList") String[] lotDeleteList) { Result checkResult = check(tourSetting); if (checkResult != null) { @@ -113,6 +130,14 @@ public class TourSettingController { return Result.error("同年度下配置名称已存在"); } + // 后端再次校验分配总量,避免绕过页面直接提交超出出行人数指标的数据。 + int travelPeopleQuota = tourSetting.getTravelPeopleQuota() == null + ? 0 : tourSetting.getTravelPeopleQuota(); + String quotaLimitMessage = tourSettingService.checkUnionQuotaLimit(unionQuotas, travelPeopleQuota); + if (StrUtil.isNotBlank(quotaLimitMessage)) { + return Result.error(quotaLimitMessage); + } + // 布尔值给默认值,避免前端未传时出现空状态。 if (tourSetting.getEnabled() == null) { tourSetting.setEnabled(true); @@ -148,6 +173,7 @@ public class TourSettingController { } else { tourSettingService.insertWith(tourSetting, "lots"); } + tourSettingService.saveUnionQuotas(tourSetting.getId(), unionQuotas); } else { // 编辑时先处理页面删除的标段,再保存配置和当前标段行。 if (Lang.isNotEmpty(lotDeleteList)) { @@ -155,6 +181,7 @@ public class TourSettingController { } tourSettingService.updateIgnoreNull(tourSetting); saveLots(tourSetting); + tourSettingService.saveUnionQuotas(tourSetting.getId(), unionQuotas); } return Result.success(); } @@ -168,6 +195,8 @@ public class TourSettingController { return Result.error("参数错误"); } tourSettingService.dao().clear(TourSettingLot.class, Cnd.where(TourSettingLot::getSettingId, "=", id)); + // 配置删除时同步清理人员分配,避免遗留不可达的子表数据。 + tourSettingService.clearUnionQuotas(id); tourSettingService.delete(id); return Result.success(); } @@ -205,6 +234,9 @@ public class TourSettingController { if (StrUtil.isBlank(tourSetting.getConfigName())) { return Result.error("配置名称不能为空"); } + if (tourSetting.getTravelPeopleQuota() == null || tourSetting.getTravelPeopleQuota() < 0) { + return Result.error("出行人数指标必须为非负整数"); + } if (tourSetting.getMinGroupPeople() != null && tourSetting.getMaxGroupPeople() != null && tourSetting.getMinGroupPeople() > tourSetting.getMaxGroupPeople()) { 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 9876437a..53bf71d6 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 @@ -699,12 +699,16 @@ public class TourSignupController { ledger.setOverCostReimbursed(Boolean.TRUE.equals(ledger.getOverCostReimbursed())); fillStaffInfo(ledger); + // PC和移动端共用本接口;在写入台账前锁定分工会名额,防止并发报名超过人员分配数量。 + String quotaMessage = tourLedgerService.saveWithUnionSignupQuota( + ledger, matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(), update); + if (StrUtil.isNotBlank(quotaMessage)) { + return Result.error(quotaMessage); + } + if (update) { - tourLedgerService.updateIgnoreNull(ledger); tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId())); tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); - } else { - tourLedgerService.insert(ledger); } if (Lang.isNotEmpty(familyList)) { familyList.forEach(item -> { diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourTravelAgencyController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourTravelAgencyController.java index 900a571a..60db6f95 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourTravelAgencyController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourTravelAgencyController.java @@ -58,13 +58,13 @@ public class TourTravelAgencyController { return Result.error("参数错误"); } TourTravelAgency agency = travelAgencyService.fetch(id); - return agency == null ? Result.error("旅行社不存在") : Result.success(agency); + return agency == null ? Result.error("服务单位不存在") : Result.success(agency); } @At @Aop(TransAop.READ_COMMITTED) @SaCheckPermission("tour.travelAgency") - @SLog(type = "tour", tag = "旅行社管理", msg = "保存旅行社信息") + @SLog(type = "tour", tag = "服务单位管理", msg = "保存服务单位信息") public Result doSubmit(TourTravelAgency agency) { Result checkResult = check(agency); if (checkResult != null) { @@ -77,7 +77,7 @@ public class TourTravelAgencyController { sameCodeCnd.and(TourTravelAgency::getId, "<>", agency.getId()); } if (travelAgencyService.count(sameCodeCnd) > 0) { - return Result.error("同年度下旅行社编号已存在"); + return Result.error("同年度下服务单位编号已存在"); } if (agency.getEnabled() == null) { agency.setEnabled(true); @@ -93,7 +93,7 @@ public class TourTravelAgencyController { @At @Aop(TransAop.READ_COMMITTED) @SaCheckPermission("tour.travelAgency") - @SLog(type = "tour", tag = "旅行社管理", msg = "删除旅行社信息") + @SLog(type = "tour", tag = "服务单位管理", msg = "删除服务单位信息") public Result doDelete(String id) { if (StrUtil.isBlank(id)) { return Result.error("参数错误"); @@ -110,10 +110,10 @@ public class TourTravelAgencyController { return Result.error("年度不能为空"); } if (StrUtil.isBlank(agency.getAgencyName())) { - return Result.error("旅行社名称不能为空"); + return Result.error("服务单位名称不能为空"); } if (StrUtil.isBlank(agency.getAgencyCode())) { - return Result.error("旅行社编号不能为空"); + return Result.error("服务单位编号不能为空"); } if (StrUtil.isBlank(agency.getContactName())) { return Result.error("联系人不能为空"); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedger.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedger.java index 0e1af548..eada8569 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedger.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedger.java @@ -102,12 +102,12 @@ public class TourLedger extends BaseModel implements Serializable { private String hotelName; @Column - @Comment("旅行社ID") + @Comment("服务单位ID") @ColDefine(type = ColType.VARCHAR, width = 32) private String travelAgencyId; @Column - @Comment("旅行社") + @Comment("服务单位") @ColDefine(type = ColType.VARCHAR, width = 100) private String travelAgencyName; diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLine.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLine.java index 9fbfaef4..0f761c32 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLine.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLine.java @@ -42,7 +42,7 @@ public class TourLine extends BaseModel implements Serializable { private String lineName; @Column - @Comment("旅行社ID") + @Comment("服务单位ID") @ColDefine(type = ColType.VARCHAR, width = 32) private String travelAgencyId; 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 37e212d4..d910d4aa 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 @@ -43,6 +43,12 @@ public class TourSetting extends BaseModel implements Serializable { @ColDefine(type = ColType.VARCHAR, width = 50) private String tourType; + @Column + @Comment("出行人数指标") + @ColDefine(type = ColType.INT) + @Default("0") + private Integer travelPeopleQuota; + @Column @Comment("可参加人员范围ID") @ColDefine(type = ColType.VARCHAR, width = 32) @@ -122,11 +128,16 @@ public class TourSetting extends BaseModel implements Serializable { private Boolean enabled; /** - * 标段管理沿用老疗休养配置的子表设计,后续线路、旅行社等模块可通过标段ID继续关联。 + * 标段管理沿用老疗休养配置的子表设计,后续线路、服务单位等模块可通过标段ID继续关联。 */ @Many(field = "settingId") private List lots; + /** + * 分工会人员分配,仅用于配置弹窗回显与提交,不作为 tour_setting 表字段保存。 + */ + private List unionQuotas; + @Column @Comment("服务须知") @ColDefine(type = ColType.TEXT) diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSettingUnionQuota.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSettingUnionQuota.java new file mode 100644 index 00000000..2c95da11 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSettingUnionQuota.java @@ -0,0 +1,60 @@ +package com.budwk.app.zhgh.dayofficework.tour.models; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.nutz.dao.entity.annotation.ColDefine; +import org.nutz.dao.entity.annotation.ColType; +import org.nutz.dao.entity.annotation.Column; +import org.nutz.dao.entity.annotation.Comment; +import org.nutz.dao.entity.annotation.Default; +import org.nutz.dao.entity.annotation.Name; +import org.nutz.dao.entity.annotation.Table; +import org.nutz.dao.entity.annotation.TableMeta; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serializable; + +/** + * 疗休养配置下的分工会人员数量分配。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("tour_setting_union_quota") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("疗休养配置分工会人员分配") +public class TourSettingUnionQuota extends BaseModel implements Serializable { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("所属疗休养配置ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String settingId; + + @Column + @Comment("分工会ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String unionId; + + @Column + @Comment("分工会名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String unionName; + + @Column + @Comment("人员数量") + @ColDefine(type = ColType.INT) + @Default("0") + private Integer peopleQuota; + + /** + * 当前分工会人员总数,仅用于页面分配参考,不写入数据库。 + */ + private Integer personCount; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourTravelAgency.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourTravelAgency.java index 88f0865c..6778850f 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourTravelAgency.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourTravelAgency.java @@ -9,14 +9,14 @@ import org.nutz.dao.interceptor.annotation.PrevInsert; import java.io.Serializable; /** - * 旅行社管理。 - * 先维护疗休养线路创建会复用的旅行社基础信息,后续线路模块可通过旅行社ID关联。 + * 服务单位管理。 + * 先维护疗休养线路创建会复用的服务单位基础信息,后续线路模块可通过服务单位ID关联。 */ @Data @EqualsAndHashCode(callSuper = true) @Table("tour_travel_agency") @TableMeta("{'mysql-charset':'utf8mb4'}") -@Comment("普惠疗休养旅行社") +@Comment("普惠疗休养服务单位") public class TourTravelAgency extends BaseModel implements Serializable { @Column @@ -32,12 +32,12 @@ public class TourTravelAgency extends BaseModel implements Serializable { private Integer year; @Column - @Comment("旅行社编号") + @Comment("服务单位编号") @ColDefine(type = ColType.VARCHAR, width = 50) private String agencyCode; @Column - @Comment("旅行社名称") + @Comment("服务单位名称") @ColDefine(type = ColType.VARCHAR, width = 100) private String agencyName; 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 076a5dd4..f8cea93f 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 @@ -3,5 +3,24 @@ package com.budwk.app.zhgh.dayofficework.tour.service; import com.budwk.app.base.service.BaseService; import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; +/** + * 疗休养报名台账服务,统一处理报名记录及并发名额校验。 + */ public interface TourLedgerService extends BaseService { + + /** + * 在同一事务中锁定分工会名额、校验并发容量并写入报名台账。 + * + * @param ledger 待新增或修改的报名台账 + * @param settingId 疗休养配置ID + * @param matterId 当前报名事项ID + * @param userId 当前登录用户ID + * @param update true 表示修改已有报名,false 表示新增报名 + * @return 保存成功返回空字符串,否则返回业务提示 + */ + String saveWithUnionSignupQuota(TourLedger ledger, + String settingId, + String matterId, + String userId, + boolean update); } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourSettingService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourSettingService.java index ca1cb4ef..b30b5624 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourSettingService.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourSettingService.java @@ -2,6 +2,44 @@ package com.budwk.app.zhgh.dayofficework.tour.service; import com.budwk.app.base.service.BaseService; import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting; +import com.budwk.app.zhgh.dayofficework.tour.models.TourSettingUnionQuota; +import java.util.List; + +/** + * 疗休养配置服务,统一处理配置及分工会人员数量分配业务。 + */ public interface TourSettingService extends BaseService { + + /** + * 查询全部分工会在指定配置下的人员数量,并实时补充分工会人员总数。 + * + * @param settingId 疗休养配置ID,新增配置时允许为空 + * @return 按分工会编码排序的人员分配行 + */ + List listUnionQuotaRows(String settingId); + + /** + * 保存指定配置的分工会人员数量,提交内容为 JSON 数组字符串。 + * + * @param settingId 疗休养配置ID + * @param unionQuotas 分工会人员分配 JSON + */ + void saveUnionQuotas(String settingId, String unionQuotas); + + /** + * 校验各分工会人员数量是否均为非负整数且合计不超过出行人数指标。 + * + * @param unionQuotas 分工会人员分配 JSON + * @param travelPeopleQuota 出行人数指标 + * @return 校验通过返回空字符串,否则返回业务提示 + */ + String checkUnionQuotaLimit(String unionQuotas, int travelPeopleQuota); + + /** + * 清除指定配置下的全部分工会人员分配。 + * + * @param settingId 疗休养配置ID + */ + void clearUnionQuotas(String settingId); } 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 88c2f668..ff9820db 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 @@ -1,15 +1,136 @@ package com.budwk.app.zhgh.dayofficework.tour.service.impl; +import cn.hutool.core.util.StrUtil; import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.sys.views.View_user; import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; import org.nutz.dao.Dao; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; +/** + * 疗休养报名台账服务实现。 + */ @IocBean(args = {"refer:dao"}) public class TourLedgerServiceImpl extends BaseServiceImpl implements TourLedgerService { public TourLedgerServiceImpl(Dao dao) { super(dao); } + + @Override + @Aop(TransAop.READ_COMMITTED) + public String saveWithUnionSignupQuota(TourLedger ledger, + String settingId, + String matterId, + String userId, + boolean update) { + if (ledger == null) { + return "报名数据不能为空"; + } + String quotaMessage = lockAndCheckUnionSignupQuota( + settingId, matterId, userId, update ? ledger.getId() : null); + if (StrUtil.isNotBlank(quotaMessage)) { + return quotaMessage; + } + // 名额校验通过后立即在同一事务内写入台账,事务提交前始终持有分工会名额行锁。 + if (update) { + updateIgnoreNull(ledger); + } else { + insert(ledger); + } + return ""; + } + + /** + * 锁定当前配置和登录人所属分工会的名额行,并校验报名后是否超过人员数量。 + */ + private String lockAndCheckUnionSignupQuota(String settingId, + String matterId, + String userId, + String excludeLedgerId) { + if (StrUtil.isBlank(settingId) || StrUtil.isBlank(matterId)) { + return "疗休养配置或报名事项不存在"; + } + // 分工会信息从用户视图读取,不接受报名表单传入的工会ID,防止跨工会占用名额。 + View_user user = dao().fetch(View_user.class, Cnd.where(View_user::getId, "=", userId)); + if (user == null || StrUtil.isBlank(user.getUnionId()) || StrUtil.isBlank(user.getLoginname())) { + return "未查询到您所属的分工会,不能报名"; + } + + // FOR UPDATE 串行化同一配置、同一分工会的抢名额请求,不同分工会之间互不阻塞。 + Sql quotaSql = Sqls.create(""" + SELECT peopleQuota + FROM tour_setting_union_quota + WHERE settingId = @settingId + AND unionId = @unionId + AND delFlag = 0 + LIMIT 1 + FOR UPDATE + """); + quotaSql.setParam("settingId", settingId); + quotaSql.setParam("unionId", user.getUnionId()); + quotaSql.setCallback(Sqls.callback.map()); + dao().execute(quotaSql); + NutMap quotaRow = quotaSql.getObject(NutMap.class); + if (quotaRow == null || quotaRow.isEmpty()) { + return "您所在分工会未分配报名名额"; + } + int peopleQuota = quotaRow.getInt("peopleQuota", 0); + if (peopleQuota <= 0) { + return "您所在分工会的报名名额已满"; + } + + // 取得名额锁后再次检查同事项重复报名,关闭双击或并发请求绕过前置查询的时间窗口。 + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND id <> @excludeLedgerId"; + Sql duplicateSql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger + WHERE delFlag = 0 + AND matterId = @matterId + AND jobNo = @jobNo + """ + excludeSql); + duplicateSql.setParam("matterId", matterId); + duplicateSql.setParam("jobNo", user.getLoginname()); + if (StrUtil.isNotBlank(excludeLedgerId)) { + duplicateSql.setParam("excludeLedgerId", excludeLedgerId); + } + duplicateSql.setCallback(Sqls.callback.integer()); + dao().execute(duplicateSql); + if (duplicateSql.getInt() > 0) { + return "您已报名当前出行时段,请勿重复提交"; + } + + // 名额按配置统计,覆盖该配置下的全部事项;待审核报名也占用名额,取消后因台账删除自动释放。 + String countExcludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND ledger.id <> @excludeLedgerId"; + Sql countSql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger ledger + INNER JOIN tour_matter matter + ON matter.id = ledger.matterId + AND matter.delFlag = 0 + WHERE ledger.delFlag = 0 + AND matter.settingId = @settingId + AND ledger.unionId = @unionId + """ + countExcludeSql); + countSql.setParam("settingId", settingId); + countSql.setParam("unionId", user.getUnionId()); + if (StrUtil.isNotBlank(excludeLedgerId)) { + countSql.setParam("excludeLedgerId", excludeLedgerId); + } + countSql.setCallback(Sqls.callback.integer()); + dao().execute(countSql); + int signupCount = countSql.getInt(); + if (signupCount + 1 > peopleQuota) { + return "您所在分工会的报名名额已满(已报名 " + signupCount + + " 人,分配 " + peopleQuota + " 人)"; + } + return ""; + } } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourSettingServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourSettingServiceImpl.java index 68f57fcf..296fc684 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourSettingServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourSettingServiceImpl.java @@ -1,15 +1,172 @@ package com.budwk.app.zhgh.dayofficework.tour.service.impl; +import cn.hutool.core.util.StrUtil; import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.sys.models.Sys_union; import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting; +import com.budwk.app.zhgh.dayofficework.tour.models.TourSettingUnionQuota; import com.budwk.app.zhgh.dayofficework.tour.service.TourSettingService; +import org.nutz.dao.Cnd; import org.nutz.dao.Dao; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.json.Json; +import org.nutz.lang.Lang; +import org.nutz.lang.util.NutMap; +import java.util.List; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * 疗休养配置服务实现,负责分工会人员分配的查询、校验和持久化。 + */ @IocBean(args = {"refer:dao"}) public class TourSettingServiceImpl extends BaseServiceImpl implements TourSettingService { public TourSettingServiceImpl(Dao dao) { super(dao); } + + @Override + public List listUnionQuotaRows(String settingId) { + // 一次查询分工会、已保存数量和实时人员总数,避免逐个工会统计产生 N+1 查询。 + Sql sql = Sqls.create(""" + SELECT + quota.id AS id, + @settingId AS settingId, + un.id AS unionId, + un.name AS unionName, + COALESCE(quota.peopleQuota, 0) AS peopleQuota, + COALESCE(user_count.personCount, 0) AS personCount + FROM sys_union un + LEFT JOIN tour_setting_union_quota quota + ON quota.unionId = un.id + AND quota.settingId = @settingId + AND COALESCE(quota.delFlag, 0) = 0 + LEFT JOIN ( + SELECT unionId, COUNT(1) AS personCount + FROM vw_user + GROUP BY unionId + ) user_count ON user_count.unionId = un.id + WHERE COALESCE(un.delFlag, 0) = 0 + ORDER BY un.unionCode ASC + """); + sql.setParam("settingId", StrUtil.blankToDefault(settingId, "")); + List rows = listMap(sql); + return rows.stream().map(row -> { + TourSettingUnionQuota quota = new TourSettingUnionQuota(); + quota.setId(row.getString("id")); + quota.setSettingId(row.getString("settingId")); + quota.setUnionId(row.getString("unionId")); + quota.setUnionName(row.getString("unionName")); + quota.setPeopleQuota(toNonNegativeInteger(row.getInt("peopleQuota"))); + quota.setPersonCount(toNonNegativeInteger(row.getInt("personCount"))); + return quota; + }).collect(Collectors.toList()); + } + + @Override + public void saveUnionQuotas(String settingId, String unionQuotas) { + if (StrUtil.isBlank(settingId)) { + return; + } + clearUnionQuotas(settingId); + if (StrUtil.isBlank(unionQuotas)) { + return; + } + List quotaList = Json.fromJsonAsList(TourSettingUnionQuota.class, unionQuotas); + if (Lang.isEmpty(quotaList)) { + return; + } + // 工会名称以当前组织表为准,防止前端篡改或历史名称继续写入。 + Map unionMap = dao().query(Sys_union.class, + Cnd.where(Sys_union::getDelFlag, "=", false)).stream() + .collect(Collectors.toMap(Sys_union::getId, item -> item, (first, second) -> first)); + List saveList = quotaList.stream() + .filter(item -> item != null + && StrUtil.isNotBlank(item.getUnionId()) + && unionMap.containsKey(item.getUnionId())) + .map(item -> normalizeUnionQuota(settingId, item, unionMap)) + .filter(item -> item.getPeopleQuota() > 0) + .collect(Collectors.toList()); + if (Lang.isNotEmpty(saveList)) { + dao().insert(saveList); + } + } + + @Override + public String checkUnionQuotaLimit(String unionQuotas, int travelPeopleQuota) { + if (StrUtil.isBlank(unionQuotas)) { + return ""; + } + final List quotaList; + try { + quotaList = Json.fromJsonAsList(TourSettingUnionQuota.class, unionQuotas); + } catch (Exception e) { + return "人员分配数据格式不正确"; + } + if (Lang.isEmpty(quotaList)) { + return ""; + } + long total = 0L; + Set unionIds = new HashSet<>(); + Set validUnionIds = dao().query(Sys_union.class, + Cnd.where(Sys_union::getDelFlag, "=", false)).stream() + .map(Sys_union::getId) + .collect(Collectors.toSet()); + for (int index = 0; index < quotaList.size(); index++) { + TourSettingUnionQuota quota = quotaList.get(index); + if (quota == null || quota.getPeopleQuota() == null) { + continue; + } + // 工会ID必须来自当前组织表且不能重复,防止伪造行绕过总量和组织范围校验。 + if (StrUtil.isBlank(quota.getUnionId()) + || !validUnionIds.contains(quota.getUnionId()) + || !unionIds.add(quota.getUnionId())) { + return "第" + (index + 1) + "行分工会信息无效或重复"; + } + if (quota.getPeopleQuota() < 0) { + return "第" + (index + 1) + "行人员数量必须为非负整数"; + } + total += quota.getPeopleQuota(); + } + if (total > travelPeopleQuota) { + return "当前人员数量合计 " + total + ",不能超过出行人数指标 " + travelPeopleQuota; + } + return ""; + } + + @Override + public void clearUnionQuotas(String settingId) { + if (StrUtil.isNotBlank(settingId)) { + dao().clear(TourSettingUnionQuota.class, + Cnd.where(TourSettingUnionQuota::getSettingId, "=", settingId)); + } + } + + /** + * 将前端分配行转换为可信的持久化对象,只接受当前组织表中的工会名称。 + */ + private TourSettingUnionQuota normalizeUnionQuota(String settingId, + TourSettingUnionQuota item, + Map unionMap) { + Sys_union union = unionMap.get(item.getUnionId()); + TourSettingUnionQuota quota = new TourSettingUnionQuota(); + quota.setSettingId(settingId); + quota.setUnionId(item.getUnionId()); + quota.setUnionName(union.getName()); + quota.setPeopleQuota(toNonNegativeInteger(item.getPeopleQuota())); + return quota; + } + + /** + * 将空值或负数统一转换为零,保证页面统计和数据库保存使用相同口径。 + */ + private Integer toNonNegativeInteger(Integer value) { + return value == null || value < 0 ? 0 : value; + } } diff --git a/src/main/resources/db/menu/init_tour_menu.sql b/src/main/resources/db/menu/init_tour_menu.sql index 0042dd69..b1af6f7f 100644 --- a/src/main/resources/db/menu/init_tour_menu.sql +++ b/src/main/resources/db/menu/init_tour_menu.sql @@ -40,7 +40,7 @@ WHERE p.permission = 'tour' AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.setting') t); INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) -SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10003', p.id, CONCAT(p.path, '0002'), '旅行社管理', 'Travel Agency', 'menu', '/platform/tour/travelAgency', 'data-pjax', '', 1, 0, 'tour.travelAgency', NULL, 2, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0 +SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10003', p.id, CONCAT(p.path, '0002'), '服务单位管理', 'Service Unit', 'menu', '/platform/tour/travelAgency', 'data-pjax', '', 1, 0, 'tour.travelAgency', NULL, 2, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'f', 0, 0 FROM sys_menu p WHERE p.permission = 'tour' AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.travelAgency') t); diff --git a/src/main/resources/db/tour_setting_add_travel_people_quota.sql b/src/main/resources/db/tour_setting_add_travel_people_quota.sql new file mode 100644 index 00000000..fb773462 --- /dev/null +++ b/src/main/resources/db/tour_setting_add_travel_people_quota.sql @@ -0,0 +1,3 @@ +-- 疗休养配置增加出行人数指标,历史配置默认按 0 处理。 +ALTER TABLE `tour_setting` + ADD COLUMN `travelPeopleQuota` int DEFAULT 0 COMMENT '出行人数指标' AFTER `tourType`; diff --git a/src/main/resources/db/tour_setting_create.sql b/src/main/resources/db/tour_setting_create.sql index 4dd97880..24c71650 100644 --- a/src/main/resources/db/tour_setting_create.sql +++ b/src/main/resources/db/tour_setting_create.sql @@ -5,6 +5,7 @@ CREATE TABLE IF NOT EXISTS `tour_setting` ( `year` int DEFAULT NULL COMMENT '年度', `configName` varchar(100) DEFAULT NULL COMMENT '疗休养配置名称', `tourType` varchar(50) DEFAULT NULL COMMENT '疗休养类型', + `travelPeopleQuota` int DEFAULT 0 COMMENT '出行人数指标', `activityGroupId` varchar(32) DEFAULT NULL COMMENT '可参加人员范围ID', `sortNo` int DEFAULT NULL COMMENT '排序编号', `minGroupPeople` int DEFAULT NULL COMMENT '最少成团人数', @@ -33,7 +34,7 @@ CREATE TABLE IF NOT EXISTS `tour_setting` ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养配置'; -- 疗休养配置标段表。 --- 标段先挂在配置上维护,后续线路、旅行社、报名等模块可继续通过 lotId 做业务关联。 +-- 标段先挂在配置上维护,后续线路、服务单位、报名等模块可继续通过 lotId 做业务关联。 CREATE TABLE IF NOT EXISTS `tour_setting_lot` ( `id` varchar(32) NOT NULL COMMENT 'ID', `settingId` varchar(32) DEFAULT NULL COMMENT '所属配置ID', @@ -51,12 +52,30 @@ CREATE TABLE IF NOT EXISTS `tour_setting_lot` ( KEY `idx_tour_setting_lot_value` (`lotValue`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养配置标段'; --- 旅行社管理表。 +-- 疗休养配置分工会人员分配表。 +CREATE TABLE IF NOT EXISTS `tour_setting_union_quota` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `settingId` varchar(32) DEFAULT NULL COMMENT '所属疗休养配置ID', + `unionId` varchar(32) DEFAULT NULL COMMENT '分工会ID', + `unionName` varchar(100) DEFAULT NULL COMMENT '分工会名称', + `peopleQuota` int DEFAULT 0 COMMENT '人员数量', + `createdBy` varchar(32) DEFAULT NULL COMMENT '创建人', + `createdAt` bigint DEFAULT NULL COMMENT '创建时间', + `updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人', + `updatedAt` bigint DEFAULT NULL COMMENT '修改时间', + `delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_tour_setting_union_quota_setting_union` (`settingId`, `unionId`), + KEY `idx_tour_setting_union_quota_setting` (`settingId`), + KEY `idx_tour_setting_union_quota_union` (`unionId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疗休养配置分工会人员分配'; + +-- 服务单位管理表。 CREATE TABLE IF NOT EXISTS `tour_travel_agency` ( `id` varchar(32) NOT NULL COMMENT 'ID', `year` int DEFAULT NULL COMMENT '年度', - `agencyCode` varchar(50) DEFAULT NULL COMMENT '旅行社编号', - `agencyName` varchar(100) DEFAULT NULL COMMENT '旅行社名称', + `agencyCode` varchar(50) DEFAULT NULL COMMENT '服务单位编号', + `agencyName` varchar(100) DEFAULT NULL COMMENT '服务单位名称', `contactName` varchar(30) DEFAULT NULL COMMENT '联系人', `contactPhone` varchar(30) DEFAULT NULL COMMENT '联系人手机', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', @@ -72,7 +91,7 @@ CREATE TABLE IF NOT EXISTS `tour_travel_agency` ( UNIQUE KEY `uk_tour_travel_agency_year_code` (`year`, `agencyCode`), KEY `idx_tour_travel_agency_name` (`agencyName`), KEY `idx_tour_travel_agency_contact` (`contactName`, `contactPhone`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养旅行社'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养服务单位'; -- 线路管理表。 CREATE TABLE IF NOT EXISTS `tour_line` ( @@ -80,7 +99,7 @@ CREATE TABLE IF NOT EXISTS `tour_line` ( `year` int DEFAULT NULL COMMENT '创建年度', `lineCode` varchar(50) DEFAULT NULL COMMENT '线路编号', `lineName` varchar(100) DEFAULT NULL COMMENT '线路名称', - `travelAgencyId` varchar(32) DEFAULT NULL COMMENT '旅行社ID', + `travelAgencyId` varchar(32) DEFAULT NULL COMMENT '服务单位ID', `creatorUserId` varchar(32) DEFAULT NULL COMMENT '创建人ID', `creatorName` varchar(100) DEFAULT NULL COMMENT '创建人', `unitId` varchar(32) DEFAULT NULL COMMENT '所在单位ID', diff --git a/src/main/resources/db/tour_setting_union_quota_create.sql b/src/main/resources/db/tour_setting_union_quota_create.sql new file mode 100644 index 00000000..598d8b54 --- /dev/null +++ b/src/main/resources/db/tour_setting_union_quota_create.sql @@ -0,0 +1,17 @@ +-- 疗休养配置分工会人员分配表,人员数量合计受配置的出行人数指标约束。 +CREATE TABLE IF NOT EXISTS `tour_setting_union_quota` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `settingId` varchar(32) DEFAULT NULL COMMENT '所属疗休养配置ID', + `unionId` varchar(32) DEFAULT NULL COMMENT '分工会ID', + `unionName` varchar(100) DEFAULT NULL COMMENT '分工会名称', + `peopleQuota` int DEFAULT 0 COMMENT '人员数量', + `createdBy` varchar(32) DEFAULT NULL COMMENT '创建人', + `createdAt` bigint DEFAULT NULL COMMENT '创建时间', + `updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人', + `updatedAt` bigint DEFAULT NULL COMMENT '修改时间', + `delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_tour_setting_union_quota_setting_union` (`settingId`, `unionId`), + KEY `idx_tour_setting_union_quota_setting` (`settingId`), + KEY `idx_tour_setting_union_quota_union` (`unionId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疗休养配置分工会人员分配'; diff --git a/src/main/resources/views/platform/zhgh/dayofficework/Tour/ledger/index.html b/src/main/resources/views/platform/zhgh/dayofficework/Tour/ledger/index.html index 1dbbc148..d1f021c9 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/Tour/ledger/index.html +++ b/src/main/resources/views/platform/zhgh/dayofficework/Tour/ledger/index.html @@ -174,7 +174,7 @@ layout("/layouts/platform.html"){ {{ detail.lineName || '' }} {{ detail.lineType || '' }} {{ detail.hotelName || '' }} - {{ detail.travelAgencyName || '' }} + {{ detail.travelAgencyName || '' }} {{ detail.travelPeriod || '' }} {{ detail.bedType || '' }} {{ detail.bedInfo || '' }} diff --git a/src/main/resources/views/platform/zhgh/dayofficework/Tour/mysignup/index.html b/src/main/resources/views/platform/zhgh/dayofficework/Tour/mysignup/index.html index 535e9195..260fcb3f 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/Tour/mysignup/index.html +++ b/src/main/resources/views/platform/zhgh/dayofficework/Tour/mysignup/index.html @@ -114,7 +114,7 @@ layout("/layouts/platform.html"){ {{ detail.lineName || '' }} {{ detail.lineType || '' }} {{ detail.hotelName || '' }} - {{ detail.travelAgencyName || '' }} + {{ detail.travelAgencyName || '' }} {{ detail.travelPeriod || '' }} {{ detail.bedType || '' }} {{ detail.bedInfo || '' }} @@ -255,7 +255,7 @@ layout("/layouts/platform.html"){ - + diff --git a/src/main/resources/views/platform/zhgh/dayofficework/Tour/route/index.html b/src/main/resources/views/platform/zhgh/dayofficework/Tour/route/index.html index 5a1aada2..1b52f22a 100644 --- a/src/main/resources/views/platform/zhgh/dayofficework/Tour/route/index.html +++ b/src/main/resources/views/platform/zhgh/dayofficework/Tour/route/index.html @@ -112,8 +112,8 @@ layout("/layouts/platform.html"){ - - + + @@ -187,7 +187,7 @@ layout("/layouts/platform.html"){ - + {{ detail.travelPeriod || '' }} {{ detail.lineType || '' }} {{ detail.hotelName || '' }} - {{ detail.travelAgencyName || '' }} + {{ detail.travelAgencyName || '' }} {{ familyText() }} 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 b73c65a1..77e9777f 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 @@ -83,6 +83,7 @@ layout("/layouts/platform.html"){ :visible.sync="dialogVisible" :close-on-click-modal="false" width="64.8%" + custom-class="tour-setting-dialog" @closed="destroyEditor">
@@ -145,6 +146,11 @@ layout("/layouts/platform.html"){ + + + + +
@@ -267,6 +273,47 @@ layout("/layouts/platform.html"){ + +
+
+
+ 出行人数指标 + {{ travelPeopleQuota }} +
+
+ 分工会总人数 + {{ branchTotalPersonCount }} +
+ + + 按比例分配 +
+ + + + + + + + +
+
@@ -331,10 +378,14 @@ layout("/layouts/platform.html"){ activityGroupList: [], formData: {}, lotDeleteList: [], + quotaAllocate: { + ratio: null + }, inheritLoading: false, formRules: { year: [{required: true, message: "必填", trigger: ["blur", "change"]}], configName: [{required: true, message: "必填", trigger: ["blur", "change"]}], + travelPeopleQuota: [{required: true, message: "必填", trigger: ["blur", "change"]}], minGroupPeople: [{validator: checkGroupPeople, trigger: ["blur", "change"]}], maxGroupPeople: [{validator: checkGroupPeople, trigger: ["blur", "change"]}], outProvinceRatioType: [{required: true, message: "必填", trigger: ["blur", "change"]}], @@ -347,12 +398,117 @@ layout("/layouts/platform.html"){ components: { "drawer-user-scope": httpVueLoader("/components/module/activity/DrawerUserScope.vue") }, + computed: { + // 出行人数指标统一按非负整数参与页面统计和分配上限校验。 + travelPeopleQuota() { + return this.toNonNegativeInteger(this.formData.travelPeopleQuota) + }, + // 人员总数使用后端实时统计结果,仅作为按比例分配的计算基数。 + branchTotalPersonCount() { + const rows = this.formData.unionQuotas || [] + return rows.reduce((sum, row) => sum + this.toNonNegativeInteger(row.personCount), 0) + } + }, + watch: { + "formData.travelPeopleQuota": function() { + this.refreshQuotaRatio() + } + }, methods: { - async getActivityGroup() { - const {data} = await this.$axios.post("/platform/activity/basic/scope/getActivityUserScopeGroup") - this.activityGroupList = (data || []).map((item) => { + // 加载分工会人员分配行;新增时传空配置ID,编辑时合并已保存数量。 + loadUnionQuotaRows(settingId) { + this.$axios.post(loc() + "/unionQuotaRows", {settingId: settingId || ""}).then((res) => { + if (res.code === 0) { + this.$set(this.formData, "unionQuotas", this.normalizeUnionQuotaRows(res.data || [])) + this.refreshQuotaRatio() + } else { + this.$message.warning(res.msg || "查询人员分配失败") + } + }) + }, + // 将接口和历史数据中的空值规范为非负整数,避免合计结果出现 NaN。 + normalizeUnionQuotaRows(rows) { + return (rows || []).map(item => { return Object.assign({}, item, { - groupId: item.groupId === null || item.groupId === undefined ? "" : String(item.groupId) + peopleQuota: this.toNonNegativeInteger(item.peopleQuota), + personCount: this.toNonNegativeInteger(item.personCount) + }) + }) + }, + toNonNegativeInteger(value) { + const numberValue = parseInt(value, 10) + if (isNaN(numberValue) || numberValue < 0) { + return 0 + } + return numberValue + }, + toNumber(value) { + const numberValue = parseFloat(value) + return isNaN(numberValue) ? null : numberValue + }, + truncateDecimal(value, precision) { + const times = Math.pow(10, precision) + return Math.floor(value * times) / times + }, + // 默认比例为出行人数指标除以分工会总人数,并按参考项目保留两位后直接截断。 + refreshQuotaRatio() { + if (this.branchTotalPersonCount <= 0 || this.travelPeopleQuota <= 0) { + this.quotaAllocate.ratio = null + return + } + this.quotaAllocate.ratio = this.truncateDecimal(this.travelPeopleQuota / this.branchTotalPersonCount, 2) + }, + unionQuotaSummary(param) { + const columns = param.columns || [] + const rows = this.formData.unionQuotas || [] + return columns.map((column, index) => { + if (index === 0) { + return "合计" + } + if (column.property === "personCount") { + return rows.reduce((sum, row) => sum + this.toNonNegativeInteger(row.personCount), 0) + } + if (column.label === "人员数量") { + return rows.reduce((sum, row) => sum + this.toNonNegativeInteger(row.peopleQuota), 0) + } + return "" + }) + }, + allocateQuotaByRatio() { + const rows = this.formData.unionQuotas || [] + const ratio = this.toNumber(this.quotaAllocate.ratio) + if (!rows.length) { + this.$message.warning("暂无分工会数据") + return + } + if (ratio === null || this.quotaAllocate.ratio === "" || this.quotaAllocate.ratio === undefined) { + this.$message.warning("比例为空,未分配") + return + } + if (ratio < 0) { + this.$message.warning("比例不能为负数") + return + } + // 按比例分配时小数直接舍去,不补余数,保持与参考项目一致。 + const nextQuotas = rows.map(row => { + return Math.floor(ratio * this.toNonNegativeInteger(row.personCount)) + }) + const quotaTotal = nextQuotas.reduce((sum, value) => sum + value, 0) + if (quotaTotal > this.travelPeopleQuota) { + this.$message.warning("当前人员数量合计 " + quotaTotal + ",不能超过出行人数指标 " + this.travelPeopleQuota) + return + } + nextQuotas.forEach((peopleQuota, index) => { + this.$set(rows[index], "peopleQuota", peopleQuota) + }) + }, + getActivityGroup() { + return this.$axios.post("/platform/activity/basic/scope/getActivityUserScopeGroup").then((res) => { + const data = res.data + this.activityGroupList = (data || []).map((item) => { + return Object.assign({}, item, { + groupId: item.groupId === null || item.groupId === undefined ? "" : String(item.groupId) + }) }) }) }, @@ -378,6 +534,7 @@ layout("/layouts/platform.html"){ year: moment().format("YYYY"), configName: "", tourType: "", + travelPeopleQuota: 0, activityGroupId: "", sortNo: 0, minGroupPeople: 0, @@ -394,6 +551,7 @@ layout("/layouts/platform.html"){ fillBedInfo: true, enabled: true, lots: [], + unionQuotas: [], serviceNotice: "" } }, @@ -404,6 +562,7 @@ layout("/layouts/platform.html"){ // 新建时给出默认年度和开关值,减少校工会管理员录入成本。 this.formData = this.defaultFormData() this.dialogVisible = true + this.loadUnionQuotaRows("") this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate()) }, inheritPreviousYearInfo() { @@ -428,11 +587,19 @@ layout("/layouts/platform.html"){ allowOverReimbursement: !!item.allowOverReimbursement } }) + // 延用上一年分配数量,但清除原配置和子表ID,保存时生成当前年度数据。 + const inheritedUnionQuotas = this.normalizeUnionQuotaRows(previous.unionQuotas || []).map(item => { + return Object.assign({}, item, { + id: "", + settingId: "" + }) + }) this.formData = Object.assign(this.defaultFormData(), previous, { year: String(currentYear), cycleStartYear: previous.cycleStartYear ? String(previous.cycleStartYear) : "", cycleEndYear: previous.cycleEndYear ? String(previous.cycleEndYear) : "", - lots: inheritedLots + lots: inheritedLots, + unionQuotas: inheritedUnionQuotas }) delete this.formData.id delete this.formData.createdAt @@ -463,6 +630,7 @@ layout("/layouts/platform.html"){ this.$axios.post(loc() + "/detail", {id: row.id}).then((res) => { if (res.code === 0) { this.formData = Object.assign({ + travelPeopleQuota: 0, activityGroupId: "", outProvinceRatioType: "当年报名人数", outProvinceFixedPeople: 0, @@ -474,6 +642,7 @@ layout("/layouts/platform.html"){ fillBedInfo: true, enabled: true, lots: [], + unionQuotas: [], serviceNotice: "" }, res.data || {}) this.formData.year = this.formData.year ? String(this.formData.year) : "" @@ -491,6 +660,7 @@ layout("/layouts/platform.html"){ this.formData.lots = (this.formData.lots || []).map(item => Object.assign({}, item, { allowOverReimbursement: !!item.allowOverReimbursement })) + this.formData.unionQuotas = this.normalizeUnionQuotaRows(this.formData.unionQuotas || []) this.dialogVisible = true this.$nextTick(() => this.$refs.form && this.$refs.form.clearValidate()) } else { @@ -502,6 +672,7 @@ layout("/layouts/platform.html"){ this.$refs.form.validate((valid) => { if (!valid) return if (!this.validateLots()) return + if (!this.validateUnionQuotas()) return this.submitLoading = true // Nutz 对子表集合按字符串化 JSON 绑定更稳定,和体检项目维护的提交方式保持一致。 const submitData = JSON.parse(JSON.stringify(this.formData)) @@ -509,6 +680,7 @@ layout("/layouts/platform.html"){ submitData.outProvinceFixedPeople = 0 } submitData.lots = JSON.stringify(this.formData.lots || []) + submitData.unionQuotas = JSON.stringify(this.formData.unionQuotas || []) submitData.lotDeleteList = JSON.stringify(this.lotDeleteList) this.$axios.post(loc() + "/doSubmit", submitData).then((res) => { this.submitLoading = false @@ -562,6 +734,29 @@ layout("/layouts/platform.html"){ } return true }, + // 提交前统一人员数量格式,并校验分配合计不超过出行人数指标。 + validateUnionQuotas() { + const rows = this.formData.unionQuotas || [] + for (let i = 0; i < rows.length; i++) { + const row = rows[i] + if (row.peopleQuota === null || row.peopleQuota === undefined || row.peopleQuota === "") { + row.peopleQuota = 0 + } + if (!/^\d+$/.test(String(row.peopleQuota))) { + this.$message.warning("第" + (i + 1) + "行人员数量必须为非负整数") + return false + } + this.$set(row, "peopleQuota", this.toNonNegativeInteger(row.peopleQuota)) + } + const quotaTotal = rows.reduce((sum, row) => { + return sum + this.toNonNegativeInteger(row.peopleQuota) + }, 0) + if (quotaTotal > this.travelPeopleQuota) { + this.$message.warning("当前人员数量合计 " + quotaTotal + ",不能超过出行人数指标 " + this.travelPeopleQuota) + return false + } + return true + }, deleteLot(index, row) { this.$confirm("确定删除该标段吗?", "提示", { confirmButtonText: "确定", @@ -593,6 +788,9 @@ layout("/layouts/platform.html"){ destroyEditor() { this.formData = {} this.lotDeleteList = [] + this.quotaAllocate = { + ratio: null + } this.inheritLoading = false this.activeTab = "basic" } @@ -627,6 +825,94 @@ layout("/layouts/platform.html"){ border-top: 1px dashed #dcdfe6; margin: 2px 0 18px; } + + .tour-quota-toolbar { + align-items: center; + display: flex; + flex-shrink: 0; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 10px; + } + + .tour-quota-input { + width: 160px; + } + + .tour-quota-stat { + align-items: center; + border: 1px solid #dcdfe6; + border-radius: 4px; + display: inline-flex; + gap: 8px; + height: 36px; + padding: 0 10px; + } + + .tour-quota-stat span { + color: #606266; + } + + .tour-quota-stat strong { + color: #303133; + font-weight: 600; + } + + .tour-tab-fill { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + } + + .tour-tab-table { + flex: 1; + min-height: 0; + } + + .tour-setting-dialog { + display: flex; + flex-direction: column; + height: 85vh; + max-height: 85vh; + } + + .tour-setting-dialog .el-dialog__header, + .tour-setting-dialog .el-dialog__footer, + .tour-setting-dialog .el-tabs__header { + flex-shrink: 0; + } + + .tour-setting-dialog .el-dialog__body { + flex: 1; + min-height: 0; + overflow: hidden; + padding-bottom: 0; + } + + .tour-setting-dialog .el-form, + .tour-setting-dialog .el-tabs { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; + } + + .tour-setting-dialog .el-tabs { + flex: 1; + } + + .tour-setting-dialog .el-tabs__content { + flex: 1; + min-height: 0; + overflow-x: hidden; + overflow-y: auto; + padding: 0 6px 18px 0; + } + + .tour-setting-dialog .el-tab-pane { + height: 100%; + } @@ -196,8 +206,18 @@ layout("/layouts/platform_tour_signup_h5.html"){
+ + @@ -217,6 +237,8 @@ layout("/layouts/platform_tour_signup_h5.html"){ pdfPageCount: 0, pdfRenderedPages: 0, pdfRenderToken: 0, + readCountdown: 10, + readCountdownTimer: null, bannerList: [ "/assets/platform/images/tour/tour-h5-banner-1.jpg", "/assets/platform/images/tour/tour-h5-banner-2.jpg" @@ -228,7 +250,33 @@ layout("/layouts/platform_tour_signup_h5.html"){ } } }, + computed: { + // 倒计时期间在按钮中展示剩余秒数,结束后恢复原确认文案。 + readButtonText() { + return this.readCountdown > 0 ? "我已阅读(" + this.readCountdown + "秒)" : "我已阅读" + } + }, methods: { + // 启动十秒阅读倒计时,重复调用前先清理旧计时器,避免并发计时。 + startReadCountdown() { + this.clearReadCountdown() + this.readCountdown = 10 + this.readCountdownTimer = window.setInterval(() => { + if (this.readCountdown <= 1) { + this.readCountdown = 0 + this.clearReadCountdown() + return + } + this.readCountdown -= 1 + }, 1000) + }, + // 页面离开或倒计时结束时释放计时器,避免页面销毁后继续更新状态。 + clearReadCountdown() { + if (this.readCountdownTimer !== null) { + window.clearInterval(this.readCountdownTimer) + this.readCountdownTimer = null + } + }, parseContentHref(content) { if (!content) { return "" @@ -386,13 +434,19 @@ layout("/layouts/platform_tour_signup_h5.html"){ }) }, confirmRead() { + // 方法内再次校验倒计时,防止通过脚本触发禁用按钮的点击事件。 + if (this.readCountdown > 0) { + return + } window.location.href = "/platform/tour/signup/h5/signup" } }, created() { + this.startReadCountdown() this.loadServiceNotice() }, beforeDestroy() { + this.clearReadCountdown() this.clearPdfPreview() } }) diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/list.html b/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/list.html index fdba8668..7614fa26 100644 --- a/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/list.html +++ b/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/list.html @@ -470,8 +470,8 @@ layout("/layouts/platform_tour_signup_h5.html"){
出行开始时间:{{ row.travelStartTime || '暂无' }}
出行结束时间:{{ row.travelEndTime || '暂无' }}
-
diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/tour/unionApproval/index.html b/src/main/resources/views/platform/zhghh5/dayofficework/tour/unionApproval/index.html index 9bad0223..c7a7f446 100644 --- a/src/main/resources/views/platform/zhghh5/dayofficework/tour/unionApproval/index.html +++ b/src/main/resources/views/platform/zhghh5/dayofficework/tour/unionApproval/index.html @@ -214,7 +214,7 @@ layout("/layouts/platform_h5.html"){ - +