diff --git a/src/main/java/com/budwk/app/sys/services/impl/SysDataUserPullServiceImpl.java b/src/main/java/com/budwk/app/sys/services/impl/SysDataUserPullServiceImpl.java index e3b67c4a..7b435aa3 100644 --- a/src/main/java/com/budwk/app/sys/services/impl/SysDataUserPullServiceImpl.java +++ b/src/main/java/com/budwk/app/sys/services/impl/SysDataUserPullServiceImpl.java @@ -193,13 +193,23 @@ public class SysDataUserPullServiceImpl extends BaseServiceImpl // 判断财务信息有没有值, if (financeUserMap.containsKey(sysUser.getLoginname())) { NutMap nutMap = financeUserMap.get(sysUser.getLoginname()); - sysUser.setMember(true); - sysUser.setWelfareMember(true); - sysUser.setPreparationMemberFee(BigDecimal.valueOf(nutMap.getDouble("preparationMemberFee"))); - sysUser.setContractMemberFee(BigDecimal.valueOf(nutMap.getDouble("contractMemberFee"))); + if(BigDecimal.valueOf(nutMap.getDouble("preparationMemberFee")).doubleValue()>0 + || BigDecimal.valueOf(nutMap.getDouble("contractMemberFee")).doubleValue()>0){ + sysUser.setMember(true); + sysUser.setWelfareMember(true); + sysUser.setPreparationMemberFee(BigDecimal.valueOf(nutMap.getDouble("preparationMemberFee"))); + sysUser.setContractMemberFee(BigDecimal.valueOf(nutMap.getDouble("contractMemberFee"))); + } else { + sysUser.setMember(false); + sysUser.setWelfareMember(false); + sysUser.setPreparationMemberFee(new BigDecimal(0)); + sysUser.setContractMemberFee(new BigDecimal(0)); + } } else { sysUser.setMember(false); sysUser.setWelfareMember(false); + sysUser.setPreparationMemberFee(new BigDecimal(0)); + sysUser.setContractMemberFee(new BigDecimal(0)); } return sysUser; diff --git a/src/main/java/com/budwk/app/zhgh/activity/culture/controller/ActivityCultureInfoManageController.java b/src/main/java/com/budwk/app/zhgh/activity/culture/controller/ActivityCultureInfoManageController.java index 47d43564..33664b1c 100644 --- a/src/main/java/com/budwk/app/zhgh/activity/culture/controller/ActivityCultureInfoManageController.java +++ b/src/main/java/com/budwk/app/zhgh/activity/culture/controller/ActivityCultureInfoManageController.java @@ -127,7 +127,7 @@ public class ActivityCultureInfoManageController { @At @Ok("json:full") @ApiOperation("通知报名人员") - @SaCheckPermission("trainSignUp.manage") + @SaCheckPermission("infoManage.school.manage") public Result sendNotify(@Param("activityId") String activityId, @Param("content") String content) { if (StrUtil.isBlank(activityId) || StrUtil.isBlank(content)) { return Result.error("参数不完整"); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourBranchUserAssignmentController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourBranchUserAssignmentController.java new file mode 100644 index 00000000..1dba1fd8 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourBranchUserAssignmentController.java @@ -0,0 +1,176 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.annotation.SLog; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.result.Result; +import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.json.Json; +import org.nutz.lang.Lang; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.Param; + +import java.util.Collections; +import java.util.List; + +@IocBean +@Ok("json:full") +@At("/platform/tour/branchUserAssignment") +public class TourBranchUserAssignmentController { + + @Inject + private TourUserAssignmentService tourUserAssignmentService; + + /** + * 分工会人员分配列表入口。 + */ + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/branchUserAssignment/index.html") + @SaCheckPermission("tour.branchUserAssignment") + public void index() { + } + + /** + * 分页查询当前登录人所在分工会的分配记录,列表数据只来自人员分配表。 + */ + @At + @SaCheckPermission("tour.branchUserAssignment") + public Result pageData(PageForm pageForm, Integer year, String settingId, String matterId, + String personType, String keyword) { + return Result.success(tourUserAssignmentService.branchAssignmentPage(pageForm, year, settingId, matterId, + personType, keyword)); + } + + /** + * 分页查询候选人员,候选范围由疗休养配置可参加人员范围和当前登录人所在分工会共同决定。 + */ + @At + @SaCheckPermission("tour.branchUserAssignment") + public Result candidatePageData(PageForm pageForm, String settingId, String keyword) { + return Result.success(tourUserAssignmentService.branchCandidatePage(pageForm, settingId, keyword)); + } + + /** + * 查询可用于分配的疗休养配置。 + */ + @At + @SaCheckPermission("tour.branchUserAssignment") + public Result settingOptions(Integer year) { + return Result.success(tourUserAssignmentService.listEnabledSettingOptions(year)); + } + + /** + * 查询分工会可分配线路选项,选项携带事项、线路和旅行社快照信息。 + */ + @At + @SaCheckPermission("tour.branchUserAssignment") + public Result matterOptions(String settingId) { + return Result.success(tourUserAssignmentService.listMatterOptions(settingId)); + } + + /** + * 查询当前登录人所在分工会在指定配置下的名额使用情况。 + */ + @At + @SaCheckPermission("tour.branchUserAssignment") + public Result quotaInfo(String settingId) { + return Result.success(tourUserAssignmentService.branchQuotaInfo(settingId)); + } + + /** + * 保存分工会人员分配,保存时数据来源固定为 BRANCH_UNION。 + */ + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.branchUserAssignment") + @SLog(type = "tour", tag = "疗休养分工会人员分配", msg = "保存分工会人员分配") + public Result doAssign(String settingId, String matterId, String personType, @Param("userIds") String userIds) { + List parsedUserIds; + try { + parsedUserIds = parseUserIds(userIds); + } catch (Exception e) { + return Result.error("人员参数错误"); + } + try { + return Result.success(tourUserAssignmentService.assignBranchUsers(settingId, matterId, personType, parsedUserIds)); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } + } + + /** + * 切换当前分工会人员分配记录的正式/替补状态,具体名额和台账保护规则由 service 统一处理。 + */ + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.branchUserAssignment") + @SLog(type = "tour", tag = "疗休养分工会人员分配", msg = "切换分工会人员类型") + public Result switchPersonType(String id, String personType) { + try { + return Result.success(tourUserAssignmentService.switchCurrentBranchPersonType(id, personType)); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } + } + + /** + * 恢复当前分工会已取消退出的人员分配记录,只恢复状态,不恢复已删除台账。 + */ + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.branchUserAssignment.cancelRestore") + @SLog(type = "tour", tag = "疗休养分工会人员分配", msg = "取消退出分工会人员分配") + public Result restoreCancel(String id) { + try { + tourUserAssignmentService.restoreCurrentBranchCancelledAssignment(id); + return Result.success(); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } + } + + /** + * 删除前检查该分配记录是否已经存在对应报名台账。 + */ + @At + @SaCheckPermission("tour.branchUserAssignment") + public Result deleteInfo(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + return Result.success(tourUserAssignmentService.branchDeleteInfo(id)); + } + + /** + * 删除分工会人员分配记录,只删除 BRANCH_UNION 来源的数据。 + */ + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.branchUserAssignment") + @SLog(type = "tour", tag = "疗休养分工会人员分配", msg = "删除分工会人员分配") + public Result doDelete(String id, Boolean deleteLedger) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + try { + tourUserAssignmentService.deleteCurrentBranchAssignment(id, Boolean.TRUE.equals(deleteLedger)); + return Result.success(); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } + } + + private List parseUserIds(String userIds) { + if (StrUtil.isBlank(userIds)) { + return Collections.emptyList(); + } + List list = Json.fromJsonAsList(String.class, userIds); + return Lang.isEmpty(list) ? Collections.emptyList() : list; + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourGroupController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourGroupController.java index 13232d43..9c3c3ab9 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourGroupController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourGroupController.java @@ -213,6 +213,7 @@ public class TourGroupController { List list = listSql.getList(NutMap.class); fillSignupMobile(list); + fillSignupFamilies(list); Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list); return Result.success(pagination); } @@ -362,6 +363,50 @@ public class TourGroupController { } } + /** + * 给报名人员列表挂载家属子列表;当前家属台账未保存手机号,mobile 字段先返回空值供前端占位。 + */ + private void fillSignupFamilies(List list) { + if (list == null || list.isEmpty()) { + return; + } + List ledgerIds = new ArrayList<>(); + for (NutMap item : list) { + String ledgerId = item.getString("id", ""); + if (StrUtil.isNotBlank(ledgerId)) { + ledgerIds.add(ledgerId); + } + } + if (ledgerIds.isEmpty()) { + return; + } + Sql sql = Sqls.create(""" + SELECT + ledgerId, + familyName, + age, + gender, + '' AS mobile, + idCard, + relationship + FROM tour_ledger_family + WHERE delFlag = 0 + AND ledgerId IN (@ledgerIds) + ORDER BY createdAt ASC + """); + sql.setParam("ledgerIds", ledgerIds.toArray(new String[0])); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + + Map> familyMap = new HashMap<>(); + for (NutMap family : sql.getList(NutMap.class)) { + familyMap.computeIfAbsent(family.getString("ledgerId", ""), key -> new ArrayList<>()).add(family); + } + for (NutMap item : list) { + item.put("families", familyMap.getOrDefault(item.getString("id", ""), List.of())); + } + } + private Workbook buildParticipantsWorkbook(NutMap matter, List list) { Workbook workbook = new HSSFWorkbook(); Sheet sheet = workbook.createSheet("参加人员名单"); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLeaveApplyController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLeaveApplyController.java new file mode 100644 index 00000000..7c596d00 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLeaveApplyController.java @@ -0,0 +1,78 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.budwk.app.base.annotation.SLog; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.result.Result; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLeaveApplyService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +@IocBean +@Ok("json:full") +@At("/platform/tour/leaveApply") +public class TourLeaveApplyController { + + @Inject + private TourLeaveApplyService leaveApplyService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/leaveApply/index.html") + @SaCheckPermission("tour.leaveApply") + public void index() { + } + + /** + * 查询人员分配表中的退出取消数据,旧退出申请表不再作为业务来源。 + */ + @At + @SaCheckPermission("tour.leaveApply") + public Result pageData(PageForm pageForm, String keyword, String unionName, String status) { + return Result.success(leaveApplyService.pageData(pageForm, keyword, unionName, status)); + } + + /** + * 查询当前登录人在取消管理页的可操作权限,供前端控制按钮显示。 + */ + @At + @SaCheckPermission("tour.leaveApply") + public Result permissionInfo() { + return Result.success(leaveApplyService.permissionInfo()); + } + + /** + * 取消人员分配记录:标记已退出,并同步删除该人员对应路线的报名台账。 + */ + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.leaveApply") + @SLog(type = "tour", tag = "疗休养退出取消", msg = "取消疗休养人员分配") + public Result doCancel(String id) { + try { + leaveApplyService.cancelAssignment(id); + return Result.success(); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } + } + + /** + * 恢复人员分配记录退出状态,只恢复人员分配表状态,不恢复已删除台账。 + */ + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.branchUserAssignment.cancelRestore") + @SLog(type = "tour", tag = "疗休养退出取消", msg = "恢复疗休养人员分配退出状态") + public Result doRestore(String id) { + try { + leaveApplyService.restoreAssignment(id); + return Result.success(); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLedgerController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLedgerController.java index 7bbbbdf4..4a6206c5 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLedgerController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLedgerController.java @@ -124,6 +124,10 @@ public class TourLedgerController { IF(IFNULL(l.directFamilyUnitLine, 0) = 1 OR dr.id IS NOT NULL, 1, 0) AS directFamilyUnitLine, dr.id AS directRelativeId, COALESCE(NULLIF(l.lineName, ''), t.lineName) AS currentLineName, + COALESCE(NULLIF(vu.sex, ''), t.gender, '') AS currentGender, + vu.age AS age, + COALESCE(NULLIF(vu.idCard, ''), t.idCard, '') AS currentIdCard, + COALESCE(NULLIF(vu.mobile, ''), '') AS mobile, IFNULL(f.familyCount, 0) AS familyCount, CASE WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' @@ -141,6 +145,7 @@ public class TourLedgerController { LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 LEFT JOIN tour_ledger_direct_relative dr ON dr.ledgerId = t.id AND dr.delFlag = 0 + LEFT JOIN vw_user vu ON vu.loginname = t.jobNo LEFT JOIN ( SELECT `year`, @@ -172,7 +177,11 @@ public class TourLedgerController { tourLedgerService.dao().execute(listSql); var list = listSql.getList(NutMap.class); - list.forEach(item -> item.put("lineName", item.getString("currentLineName"))); + list.forEach(item -> { + item.put("lineName", item.getString("currentLineName")); + item.put("gender", item.getString("currentGender")); + item.put("idCard", item.getString("currentIdCard")); + }); Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list); return Result.success(pagination); } @@ -313,6 +322,114 @@ public class TourLedgerController { CommonDownloadUtil.download("参加人员导入模板.xlsx", workbook, response); } + /** + * 按当前查询条件导出台账列表,导出字段与页面列表显示字样保持一致。 + */ + @At + @SaCheckPermission("tour.ledger") + @Ok("void") + public void exportLedgerData(Integer startYear, Integer endYear, String keyword, String unionId, String lineId, + String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly, + HttpServletResponse response) { + List entities = new ArrayList<>(); + entities.add(new ExcelExportEntity("工号", "jobNo", 16)); + entities.add(new ExcelExportEntity("姓名", "userName", 14)); + entities.add(new ExcelExportEntity("性别", "gender", 10)); + entities.add(new ExcelExportEntity("年龄", "age", 10)); + entities.add(new ExcelExportEntity("身份证号", "idCard", 24)); + entities.add(new ExcelExportEntity("手机号", "mobile", 18)); + entities.add(new ExcelExportEntity("所在单位", "unitName", 26)); + entities.add(new ExcelExportEntity("乘车地点", "boardingPlace", 18)); + entities.add(new ExcelExportEntity("报名线路", "lineName", 30)); + entities.add(new ExcelExportEntity("出行时段", "travelPeriod", 30)); + entities.add(new ExcelExportEntity("线路类型", "lineType", 16)); + entities.add(new ExcelExportEntity("报名时间", "signupTime", 22)); + entities.add(new ExcelExportEntity("携带家属", "familyCountText", 14)); + entities.add(new ExcelExportEntity("是否参加", "joinedText", 14)); + entities.add(new ExcelExportEntity("报销超出费用", "overCostReimbursedText", 18)); + + List rows = queryLedgerExportRows(startYear, endYear, keyword, unionId, lineId, travelPeriod, + lineType, directFamilyOnly, overCostOnly); + rows.forEach(row -> { + Integer familyCount = row.getInt("familyCount"); + row.put("familyCountText", (familyCount == null ? 0 : familyCount) + "人"); + row.put("joinedText", Boolean.TRUE.equals(row.getBoolean("joined")) ? "是" : "否"); + row.put("overCostReimbursedText", Boolean.TRUE.equals(row.getBoolean("overCostReimbursed")) ? "是" : "否"); + }); + + ExportParams exportParams = new ExportParams(); + exportParams.setSheetName("疗休养台账"); + exportParams.setType(ExcelType.XSSF); + Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, rows); + CommonDownloadUtil.download("疗休养台账数据.xlsx", workbook, response); + } + + private List queryLedgerExportRows(Integer startYear, Integer endYear, String keyword, String unionId, String lineId, + String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly) { + Cnd cnd = buildQueryCnd(startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, directFamilyOnly, overCostOnly); + Sql sql = Sqls.create(""" + SELECT + t.id, + t.jobNo, + t.userName, + COALESCE(NULLIF(vu.sex, ''), t.gender, '') AS gender, + vu.age AS age, + COALESCE(NULLIF(vu.idCard, ''), t.idCard, '') AS idCard, + COALESCE(NULLIF(vu.mobile, ''), '') AS mobile, + t.unitName, + t.boardingPlace, + COALESCE(NULLIF(l.lineName, ''), t.lineName, '') AS lineName, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE tp.travelPeriod + END AS travelPeriod, + t.lineType, + t.signupTime, + IFNULL(f.familyCount, 0) AS familyCount, + t.joined, + t.overCostReimbursed + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + LEFT JOIN ( + SELECT ledgerId, COUNT(1) AS familyCount + FROM tour_ledger_family + WHERE delFlag = 0 + GROUP BY ledgerId + ) f ON f.ledgerId = t.id + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + LEFT JOIN tour_ledger_direct_relative dr ON dr.ledgerId = t.id AND dr.delFlag = 0 + LEFT JOIN vw_user vu ON vu.loginname = t.jobNo + LEFT JOIN ( + SELECT + `year`, + lineId, + GROUP_CONCAT( + DISTINCT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE NULL + END + ORDER BY travelStartTime ASC + SEPARATOR ';' + ) AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND lineId IS NOT NULL + AND lineId <> '' + GROUP BY `year`, lineId + ) tp ON (t.matterId IS NULL OR t.matterId = '') AND tp.`year` = t.`year` AND tp.lineId = t.lineId + $condition + GROUP BY t.id + ORDER BY t.signupTime DESC, t.createdAt DESC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return sql.getList(NutMap.class); + } + @At @SaCheckPermission("tour.ledger") @Ok("void") diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMatterController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMatterController.java index 9905aaa2..45f227b8 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMatterController.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMatterController.java @@ -263,6 +263,19 @@ public class TourMatterController { .addv("maxGroupPeople", setting.getMaxGroupPeople())); } + @At + @SaCheckPermission("tour.matter") + public Result settingBoardingPlaces(String settingId) { + if (StrUtil.isBlank(settingId)) { + return Result.error("参数错误"); + } + TourSetting setting = tourMatterService.dao().fetch(TourSetting.class, settingId); + if (setting == null) { + return Result.error("疗休养配置不存在"); + } + return Result.success(NutMap.NEW().addv("boardingPlace", StrUtil.blankToDefault(setting.getBoardingPlace(), "[]"))); + } + @At @Aop(TransAop.READ_COMMITTED) @SaCheckPermission("tour.matter") @@ -277,6 +290,8 @@ public class TourMatterController { return Result.error("事项不存在"); } oldMatter.setLineId(matter.getLineId()); + // 默认乘车地点来自疗休养配置中的乘车地点列表,后续代报名和自主报名会优先带出该值。 + oldMatter.setDefaultBoardingPlace(matter.getDefaultBoardingPlace()); oldMatter.setSignupStartTime(matter.getSignupStartTime()); oldMatter.setSignupEndTime(matter.getSignupEndTime()); oldMatter.setTravelStartTime(matter.getTravelStartTime()); 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 bbe52fe2..5d6e5e0b 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 @@ -31,6 +31,7 @@ import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeSer import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService; import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService; import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService; import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.dao.Cnd; import org.nutz.dao.Sqls; @@ -84,6 +85,9 @@ public class TourMySignupController { @Inject private TourMatterService tourMatterService; + @Inject + private TourUserAssignmentService tourUserAssignmentService; + @Inject private SysDictService sysDictService; @@ -284,6 +288,7 @@ public class TourMySignupController { m.`year`, m.matterName, m.unionId AS matterUnionId, + m.defaultBoardingPlace, m.travelStartTime, m.travelEndTime, CASE @@ -299,7 +304,8 @@ public class TourMySignupController { a.agencyName AS travelAgencyName, IFNULL(lot.allowOverReimbursement, 0) AS allowOverReimbursement, s.allowFamily, - IFNULL(s.fillBedInfo, 1) AS fillBedInfo + IFNULL(s.fillBedInfo, 1) AS fillBedInfo, + s.boardingPlace AS boardingPlaceOptions FROM tour_matter m INNER JOIN tour_line l ON l.id = m.lineId LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId @@ -408,6 +414,15 @@ public class TourMySignupController { return Result.success(sql.getList(Sys_dict.class)); } + /** + * 我的报名修改接口,PC 和 H5 共用。 + * 修改成功后更新疗休养台账,并回填当前用户已存在的人员分配记录事项快照。 + * + * @param ledger 报名台账主信息 + * @param families 家属信息 JSON 数组 + * @param directRelative 直系亲属线路报名信息 JSON + * @return 修改结果;直系亲属线路、超额报销等场景会返回待审核提示 + */ @At @Aop(TransAop.READ_COMMITTED) @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) @@ -440,6 +455,10 @@ public class TourMySignupController { TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId()); boolean directFamilyLine = line != null && Boolean.TRUE.equals(line.getDirectFamilyUnitLine()); TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId()); + Result boardingPlaceResult = normalizeBoardingPlace(ledger, matter, setting); + if (boardingPlaceResult != null) { + return boardingPlaceResult; + } if (setting != null && Boolean.FALSE.equals(setting.getFillBedInfo())) { clearBedInfo(ledger, familyList); } @@ -468,6 +487,8 @@ public class TourMySignupController { fillStaffInfo(ledger); tourLedgerService.updateIgnoreNull(ledger); + // 报名台账更新后,仅回填已存在的人员分配记录事项信息,不新增记录、不改变分配来源。 + tourUserAssignmentService.backfillExistingAssignmentAfterSignup(matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(), ledger.getBoardingPlace()); tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId())); tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); if (Lang.isNotEmpty(familyList)) { @@ -511,6 +532,7 @@ public class TourMySignupController { tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId())); tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId())); + clearAssignmentMatterAfterCancel(ledger); return Result.success().addMsg("取消报名成功"); } @@ -526,9 +548,21 @@ public class TourMySignupController { tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId())); tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId())); + clearAssignmentMatterAfterCancel(ledger); return Result.success().addMsg("取消报名成功"); } + private void clearAssignmentMatterAfterCancel(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getMatterId())) { + return; + } + TourMatter matter = tourMatterService.fetch(ledger.getMatterId()); + if (matter == null || StrUtil.isBlank(matter.getSettingId())) { + return; + } + tourUserAssignmentService.clearExistingAssignmentMatterAfterCancel(matter.getSettingId(), SecurityUtil.getUserId()); + } + @At @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) public Result lineOptions(Integer startYear, Integer endYear) { @@ -590,6 +624,60 @@ public class TourMySignupController { return null; } + /** + * 修改报名时重新校验乘车地点,确保提交值仍来自当前疗休养配置的乘车地点列表。 + */ + private Result normalizeBoardingPlace(TourLedger ledger, TourMatter matter, TourSetting setting) { + if (ledger == null || setting == null) { + return null; + } + List options = parseBoardingPlaceOptions(setting.getBoardingPlace()); + if (Lang.isEmpty(options)) { + ledger.setBoardingPlace(""); + return null; + } + String boardingPlace = StrUtil.blankToDefault(ledger.getBoardingPlace(), matter == null ? "" : matter.getDefaultBoardingPlace()); + if (StrUtil.isBlank(boardingPlace)) { + return Result.error("请选择乘车地点"); + } + if (!options.contains(boardingPlace)) { + return Result.error("乘车地点不在当前疗休养配置范围内"); + } + ledger.setBoardingPlace(boardingPlace); + return null; + } + + private List parseBoardingPlaceOptions(String value) { + if (StrUtil.isBlank(value)) { + return Collections.emptyList(); + } + try { + List list = Json.fromJson(List.class, value); + if (Lang.isEmpty(list)) { + return Collections.emptyList(); + } + return list.stream().map(item -> { + if (item instanceof String) { + return ((String) item).trim(); + } + if (item instanceof java.util.Map) { + Object name = ((java.util.Map) item).get("name"); + return name == null ? "" : String.valueOf(name).trim(); + } + return ""; + }) + .filter(StrUtil::isNotBlank) + .distinct() + .collect(Collectors.toList()); + } catch (Exception e) { + return java.util.Arrays.stream(value.split(",")) + .map(String::trim) + .filter(StrUtil::isNotBlank) + .distinct() + .collect(Collectors.toList()); + } + } + private Result checkSignupRule(TourLedger ledger, TourMatter matter, TourLedger oldLedger) { if (matter == null || Boolean.TRUE.equals(matter.getDelFlag()) || !Boolean.TRUE.equals(matter.getEnabled())) { return Result.error("报名出行时段不存在或已停用"); diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSchoolUserAssignmentController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSchoolUserAssignmentController.java new file mode 100644 index 00000000..df99b13d --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSchoolUserAssignmentController.java @@ -0,0 +1,207 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.annotation.SLog; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.result.Result; +import com.budwk.app.zhgh.dayofficework.tour.models.TourUserAssignment; +import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.json.Json; +import org.nutz.lang.Lang; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.Param; + +import java.util.Collections; +import java.util.List; + +@IocBean +@Ok("json:full") +@At("/platform/tour/schoolUserAssignment") +public class TourSchoolUserAssignmentController { + + @Inject + private TourUserAssignmentService tourUserAssignmentService; + + /** + * 校工会人员分配列表入口。 + */ + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/schoolUserAssignment/index.html") + @SaCheckPermission("tour.schoolUserAssignment") + public void index() { + } + + /** + * 分页查询校工会分配记录,列表数据只来自人员分配表。 + */ + @At + @SaCheckPermission("tour.schoolUserAssignment") + public Result pageData(PageForm pageForm, Integer year, String settingId, String matterId, String unionId, + String personType, String keyword) { + return Result.success(tourUserAssignmentService.schoolAssignmentPage(pageForm, year, settingId, matterId, + unionId, personType, keyword)); + } + + /** + * 分页查询候选人员,候选范围由疗休养配置的可参加人员范围决定。 + */ + @At + @SaCheckPermission("tour.schoolUserAssignment") + public Result candidatePageData(PageForm pageForm, String settingId, String unionId, String keyword) { + return Result.success(tourUserAssignmentService.schoolCandidatePage(pageForm, settingId, unionId, keyword)); + } + + /** + * 查询可用于分配的疗休养配置。 + */ + @At + @SaCheckPermission("tour.schoolUserAssignment") + public Result settingOptions(Integer year) { + return Result.success(tourUserAssignmentService.listEnabledSettingOptions(year)); + } + + /** + * 查询校工会可分配线路选项,选项携带事项、线路和旅行社快照信息。 + */ + @At + @SaCheckPermission("tour.schoolUserAssignment") + public Result matterOptions(String settingId) { + return Result.success(tourUserAssignmentService.listMatterOptions(settingId)); + } + + /** + * 查询分工会选项,供筛选和候选人员过滤使用。 + */ + @At + @SaCheckPermission("tour.schoolUserAssignment") + public Result unionOptions() { + return Result.success(tourUserAssignmentService.listUnionOptions()); + } + + /** + * 保存校工会人员分配,保存时数据来源固定为 SCHOOL_UNION。 + */ + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.schoolUserAssignment") + @SLog(type = "tour", tag = "疗休养校工会人员分配", msg = "保存校工会人员分配") + public Result doAssign(String settingId, String matterId, String personType, @Param("userIds") String userIds) { + List parsedUserIds; + try { + parsedUserIds = parseUserIds(userIds); + } catch (Exception e) { + return Result.error("人员参数错误"); + } + try { + return Result.success(tourUserAssignmentService.assignSchoolUsers(settingId, matterId, personType, parsedUserIds)); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } + } + + /** + * 保存校工会人员分配明细,支持弹窗候选列表中每个人单独选择分配线路。 + */ + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.schoolUserAssignment") + @SLog(type = "tour", tag = "疗休养校工会人员分配", msg = "保存校工会人员分配明细") + public Result doAssignItems(String settingId, @Param("assignItems") String assignItems) { + List parsedItems; + try { + parsedItems = parseAssignItems(assignItems); + } catch (Exception e) { + return Result.error("人员分配明细参数错误"); + } + try { + return Result.success(tourUserAssignmentService.assignSchoolUsers(settingId, parsedItems)); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } + } + + /** + * 给校工会已分配人员补选分配线路,具体回写分配表和写台账逻辑由 service 统一处理。 + */ + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.schoolUserAssignment") + @SLog(type = "tour", tag = "疗休养校工会人员分配", msg = "选择校工会分配事项") + public Result selectMatter(String id, String matterId) { + try { + return Result.success(tourUserAssignmentService.selectSchoolAssignmentMatter(id, matterId)); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } + } + + /** + * 恢复校工会已取消退出的人员分配记录,只恢复状态,不恢复已删除台账。 + */ + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.schoolUserAssignment.cancelRestore") + @SLog(type = "tour", tag = "疗休养校工会人员分配", msg = "取消退出校工会人员分配") + public Result restoreCancel(String id) { + try { + tourUserAssignmentService.restoreCancelledAssignmentBySource(id, TourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION); + return Result.success(); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } + } + + /** + * 删除前检查该分配记录是否已经存在对应报名台账。 + */ + @At + @SaCheckPermission("tour.schoolUserAssignment") + public Result deleteInfo(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + return Result.success(tourUserAssignmentService.schoolDeleteInfo(id)); + } + + /** + * 删除校工会人员分配记录,只删除 SCHOOL_UNION 来源的数据。 + */ + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.schoolUserAssignment") + @SLog(type = "tour", tag = "疗休养校工会人员分配", msg = "删除校工会人员分配") + public Result doDelete(String id, Boolean deleteLedger) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + try { + tourUserAssignmentService.deleteBySource(id, TourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION, Boolean.TRUE.equals(deleteLedger)); + return Result.success(); + } catch (IllegalArgumentException e) { + return Result.error(e.getMessage()); + } + } + + private List parseUserIds(String userIds) { + if (StrUtil.isBlank(userIds)) { + return Collections.emptyList(); + } + List list = Json.fromJsonAsList(String.class, userIds); + return Lang.isEmpty(list) ? Collections.emptyList() : list; + } + + private List parseAssignItems(String assignItems) { + if (StrUtil.isBlank(assignItems)) { + return Collections.emptyList(); + } + List list = Json.fromJsonAsList(NutMap.class, assignItems); + return Lang.isEmpty(list) ? Collections.emptyList() : list; + } +} 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..a58966a8 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 @@ -9,6 +9,7 @@ import com.budwk.app.base.result.Result; import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting; import com.budwk.app.zhgh.dayofficework.tour.models.TourSettingLot; import com.budwk.app.zhgh.dayofficework.tour.service.TourSettingService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService; import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.dao.Cnd; import org.nutz.ioc.aop.Aop; @@ -16,6 +17,7 @@ import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.json.Json; import org.nutz.lang.Lang; +import org.nutz.lang.util.NutMap; import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Param; @@ -29,9 +31,15 @@ import java.util.stream.Collectors; @At("/platform/tour/setting") public class TourSettingController { + private static final String SIGNUP_ELIGIBILITY_MODE_SCOPE_GROUP = "SCOPE_GROUP"; + private static final String SIGNUP_ELIGIBILITY_MODE_ASSIGNED_USER = "ASSIGNED_USER"; + @Inject private TourSettingService tourSettingService; + @Inject + private TourUserAssignmentService tourUserAssignmentService; + @At("") @Ok("beetl:/platform/zhgh/dayofficework/Tour/setting/index.html") @SaCheckPermission("tour.setting") @@ -69,6 +77,7 @@ 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 +98,31 @@ public class TourSettingController { Cnd lotCnd = Cnd.NEW(); lotCnd.desc(TourSettingLot::getLotValue); tourSettingService.dao().fetchLinks(tourSetting, "lots", lotCnd); + tourSetting.setUnionQuotas(tourSettingService.listUnionQuotaRows(tourSetting.getId())); return Result.success(tourSetting); } + @At + @SaCheckPermission("tour.setting") + public Result unionQuotaRows(String settingId) { + // 新增配置时 settingId 为空,service 会返回所有分工会的空名额行;编辑时合并已保存名额。 + return Result.success(tourSettingService.listUnionQuotaRows(settingId)); + } + + @At + @SaCheckPermission("tour.setting") + public Result unionQuotaOverview(String settingId) { + int schoolFormalAssignedCount = tourUserAssignmentService.countSchoolFormalAssignedUsers(settingId); + return Result.success(NutMap.NEW().addv("schoolFormalAssignedCount", schoolFormalAssignedCount)); + } + @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) { @@ -114,6 +139,13 @@ public class TourSettingController { } // 布尔值给默认值,避免前端未传时出现空状态。 + int schoolFormalAssignedCount = tourUserAssignmentService.countSchoolFormalAssignedUsers(tourSetting.getId()); + int branchTotalQuota = Math.max(defaultInt(tourSetting.getTravelPeopleQuota()) - schoolFormalAssignedCount, 0); + String quotaLimitMessage = tourSettingService.checkBranchFormalQuotaLimit(unionQuotas, branchTotalQuota); + if (StrUtil.isNotBlank(quotaLimitMessage)) { + return Result.error(quotaLimitMessage); + } + if (tourSetting.getEnabled() == null) { tourSetting.setEnabled(true); } @@ -123,6 +155,10 @@ public class TourSettingController { if (tourSetting.getFillBedInfo() == null) { tourSetting.setFillBedInfo(true); } + // 报名资格校验方式默认按人员分配表校验,后续报名校验切换会读取该配置。 + if (StrUtil.isBlank(tourSetting.getSignupEligibilityMode())) { + tourSetting.setSignupEligibilityMode(SIGNUP_ELIGIBILITY_MODE_ASSIGNED_USER); + } if (StrUtil.isBlank(tourSetting.getOutProvinceRatioType())) { tourSetting.setOutProvinceRatioType("当年报名人数"); } @@ -148,6 +184,7 @@ public class TourSettingController { } else { tourSettingService.insertWith(tourSetting, "lots"); } + tourSettingService.saveUnionQuotas(tourSetting.getId(), unionQuotas); } else { // 编辑时先处理页面删除的标段,再保存配置和当前标段行。 if (Lang.isNotEmpty(lotDeleteList)) { @@ -155,6 +192,7 @@ public class TourSettingController { } tourSettingService.updateIgnoreNull(tourSetting); saveLots(tourSetting); + tourSettingService.saveUnionQuotas(tourSetting.getId(), unionQuotas); } return Result.success(); } @@ -168,6 +206,8 @@ public class TourSettingController { return Result.error("参数错误"); } tourSettingService.dao().clear(TourSettingLot.class, Cnd.where(TourSettingLot::getSettingId, "=", id)); + tourSettingService.clearUnionQuotas(id); + tourUserAssignmentService.clearBySettingId(id); tourSettingService.delete(id); return Result.success(); } @@ -205,6 +245,18 @@ public class TourSettingController { if (StrUtil.isBlank(tourSetting.getConfigName())) { return Result.error("配置名称不能为空"); } + Result boardingPlaceCheck = normalizeBoardingPlace(tourSetting); + if (boardingPlaceCheck != null) { + return boardingPlaceCheck; + } + if (tourSetting.getTravelPeopleQuota() != null && tourSetting.getTravelPeopleQuota() < 0) { + return Result.error("出行人数指标不能小于0"); + } + if (StrUtil.isNotBlank(tourSetting.getSignupEligibilityMode()) + && !SIGNUP_ELIGIBILITY_MODE_SCOPE_GROUP.equals(tourSetting.getSignupEligibilityMode()) + && !SIGNUP_ELIGIBILITY_MODE_ASSIGNED_USER.equals(tourSetting.getSignupEligibilityMode())) { + return Result.error("报名资格校验方式不正确"); + } if (tourSetting.getMinGroupPeople() != null && tourSetting.getMaxGroupPeople() != null && tourSetting.getMinGroupPeople() > tourSetting.getMaxGroupPeople()) { return Result.error("最少成团人数不能大于最多成团人数"); @@ -227,6 +279,33 @@ public class TourSettingController { return null; } + private int defaultInt(Integer value) { + return value == null || value < 0 ? 0 : value; + } + + private Result normalizeBoardingPlace(TourSetting tourSetting) { + if (StrUtil.isBlank(tourSetting.getBoardingPlace())) { + tourSetting.setBoardingPlace("[]"); + return null; + } + try { + List rows = Json.fromJsonAsList(NutMap.class, tourSetting.getBoardingPlace()); + if (Lang.isEmpty(rows)) { + tourSetting.setBoardingPlace("[]"); + return null; + } + // 乘车路线以 JSON 数组保存,只保留有效路线名称,避免页面空行写入配置。 + List normalizedRows = rows.stream() + .filter(item -> item != null && StrUtil.isNotBlank(item.getString("name"))) + .map(item -> NutMap.NEW().addv("name", item.getString("name").trim())) + .collect(Collectors.toList()); + tourSetting.setBoardingPlace(Json.toJson(normalizedRows)); + return null; + } catch (Exception e) { + return Result.error("乘车路线格式不正确"); + } + } + private Result checkLots(List lots) { if (Lang.isEmpty(lots)) { return null; 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 fff48ec1..e28ee2d1 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 @@ -30,6 +30,7 @@ import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeSer import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService; import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService; import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService; import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.dao.Cnd; import org.nutz.dao.Sqls; @@ -63,6 +64,9 @@ public class TourSignupController { @Inject private TourMatterService tourMatterService; + @Inject + private TourUserAssignmentService tourUserAssignmentService; + @Inject private TourLedgerService tourLedgerService; @@ -405,6 +409,7 @@ public class TourSignupController { m.`year`, m.matterName, m.unionId AS matterUnionId, + m.defaultBoardingPlace, m.travelStartTime, m.travelEndTime, m.contactName, @@ -421,7 +426,8 @@ public class TourSignupController { a.agencyName AS travelAgencyName, IFNULL(lot.allowOverReimbursement, 0) AS allowOverReimbursement, s.allowFamily, - IFNULL(s.fillBedInfo, 1) AS fillBedInfo + IFNULL(s.fillBedInfo, 1) AS fillBedInfo, + s.boardingPlace AS boardingPlaceOptions FROM tour_matter m INNER JOIN tour_line l ON l.id = m.lineId LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId @@ -484,6 +490,13 @@ public class TourSignupController { .addv("directRelative", directRelative)); } + /** + * 报名前校验接口,PC 和 H5 共用。 + * 用于在进入报名表单前校验事项、线路、配置状态、人员范围、重复报名和线路报名规则。 + * + * @param matterId 疗休养事项ID + * @return 可报名返回成功;不可报名返回对应业务提示 + */ @At @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) public Result signupEligibilityNotice(String matterId) { @@ -502,9 +515,13 @@ public class TourSignupController { if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) { return Result.error("报名事项配置不存在或已停用"); } - Integer activityGroupId = parseInteger(setting.getActivityGroupId()); - if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) { - return Result.error("您不在本次疗休养报名范围内"); + Result timeResult = checkSignupTime(matter); + if (timeResult != null) { + return timeResult; + } + Result scopeResult = checkSignupEligibilityScope(setting); + if (scopeResult != null) { + return scopeResult; } TourLedger oldLedger = fetchCurrentUserMatterLedger(matter.getId()); CycleAllowedTimes cycleAllowedTimes = calculateCycleAllowedTimes(matter, setting, oldLedger); @@ -532,7 +549,7 @@ public class TourSignupController { .addv("currentLineCost", cycleTotalCost.currentCost()) .addv("message", buildCycleTotalCostNotice(cycleTotalCost))); } - if (!isOutProvinceLine(line.getLineType())) { + if (isAssignedUserEligibility(setting) || !isOutProvinceLine(line.getLineType())) { return Result.success(NutMap.NEW().addv("canApply", true).addv("noticeRequired", false)); } OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger); @@ -570,9 +587,13 @@ public class TourSignupController { if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) { return Result.error("报名事项配置不存在或已停用"); } - Integer activityGroupId = parseInteger(setting.getActivityGroupId()); - if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) { - return Result.error("您不在本次疗休养报名范围内"); + Result timeResult = checkSignupTime(matter); + if (timeResult != null) { + return timeResult; + } + Result scopeResult = checkSignupEligibilityScope(setting); + if (scopeResult != null) { + return scopeResult; } TourLedger oldLedger = fetchCurrentUserMatterLedger(matter.getId()); OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger); @@ -627,6 +648,15 @@ public class TourSignupController { return Result.success(sql.getList(Sys_dict.class)); } + /** + * 报名提交接口,PC 和 H5 共用。 + * 提交成功后写入或更新疗休养台账,并回填当前用户已存在的人员分配记录事项快照。 + * + * @param ledger 报名台账主信息 + * @param families 家属信息 JSON 数组 + * @param directRelative 直系亲属线路报名信息 JSON + * @return 报名或修改结果;直系亲属线路、超额报销等场景会返回待审核提示 + */ @At @Aop(TransAop.READ_COMMITTED) @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) @@ -668,6 +698,10 @@ public class TourSignupController { TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId()); boolean directFamilyLine = line != null && Boolean.TRUE.equals(line.getDirectFamilyUnitLine()); TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId()); + Result boardingPlaceResult = normalizeBoardingPlace(ledger, matter, setting); + if (boardingPlaceResult != null) { + return boardingPlaceResult; + } if (setting != null && Boolean.FALSE.equals(setting.getFillBedInfo())) { clearBedInfo(ledger, familyList); } @@ -707,6 +741,8 @@ public class TourSignupController { } else { tourLedgerService.insert(ledger); } + // 报名台账落库后,仅回填已存在的人员分配记录事项信息,不新增记录、不改变分配来源。 + tourUserAssignmentService.backfillExistingAssignmentAfterSignup(matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(), ledger.getBoardingPlace()); if (Lang.isNotEmpty(familyList)) { familyList.forEach(item -> { item.setLedgerId(ledger.getId()); @@ -754,9 +790,21 @@ public class TourSignupController { tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId())); tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId())); + clearAssignmentMatterAfterCancel(ledger); return Result.success().addMsg("取消报名成功"); } + private void clearAssignmentMatterAfterCancel(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getMatterId())) { + return; + } + TourMatter matter = tourMatterService.fetch(ledger.getMatterId()); + if (matter == null || StrUtil.isBlank(matter.getSettingId())) { + return; + } + tourUserAssignmentService.clearExistingAssignmentMatterAfterCancel(matter.getSettingId(), SecurityUtil.getUserId()); + } + private Cnd buildQueryCnd(Integer year, String lineName, String lineType, String unionId) { Cnd cnd = Cnd.NEW(); cnd.and("m.delFlag", "=", false); @@ -883,19 +931,13 @@ public class TourSignupController { if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) { return Result.error("报名事项配置不存在或已停用"); } - Integer activityGroupId = parseInteger(setting.getActivityGroupId()); - if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) { - return Result.error("您不在本次疗休养报名范围内"); + Result timeResult = checkSignupTime(matter); + if (timeResult != null) { + return timeResult; } - if (StrUtil.isBlank(matter.getSignupStartTime()) || StrUtil.isBlank(matter.getSignupEndTime())) { - return Result.error("报名时间未配置"); - } - String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); - if (now.compareTo(normalizeDateTime(matter.getSignupStartTime(), false)) < 0) { - return Result.error("报名未开始"); - } - if (now.compareTo(normalizeDateTime(matter.getSignupEndTime(), true)) > 0) { - return Result.error("报名已结束"); + Result scopeResult = checkSignupEligibilityScope(setting); + if (scopeResult != null) { + return scopeResult; } CycleAllowedTimes cycleAllowedTimes = calculateCycleAllowedTimes(matter, setting, oldLedger); if (!cycleAllowedTimes.canApply()) { @@ -906,7 +948,7 @@ public class TourSignupController { return Result.error(buildCycleTotalCostNotice(cycleTotalCost)); } String jobNo = currentJobNo(); - if (isOutProvinceLine(line.getLineType())) { + if (!isAssignedUserEligibility(setting) && isOutProvinceLine(line.getLineType())) { int startYear = setting.getCycleStartYear() == null ? matter.getYear() : setting.getCycleStartYear(); int endYear = LocalDate.now().getYear(); Cnd outProvinceCnd = Cnd.where(TourLedger::getDelFlag, "=", false) @@ -1111,6 +1153,101 @@ public class TourSignupController { return "当年报名人数"; } + private boolean isAssignedUserEligibility(TourSetting setting) { + return setting != null && "ASSIGNED_USER".equals(setting.getSignupEligibilityMode()); + } + + /** + * 统一校验疗休养事项报名时间,确保资格名单校验在时间校验通过后再执行。 + */ + private Result checkSignupTime(TourMatter matter) { + if (matter == null || StrUtil.isBlank(matter.getSignupStartTime()) || StrUtil.isBlank(matter.getSignupEndTime())) { + return Result.error("报名时间未配置"); + } + String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + if (now.compareTo(normalizeDateTime(matter.getSignupStartTime(), false)) < 0) { + return Result.error("报名未开始"); + } + if (now.compareTo(normalizeDateTime(matter.getSignupEndTime(), true)) > 0) { + return Result.error("报名已结束"); + } + return null; + } + + /** + * 报名乘车地点以疗休养配置中的乘车地点列表为准。 + * 页面未传值时优先使用事项默认乘车地点,避免历史页面或代入默认值时丢失乘车地点。 + */ + private Result normalizeBoardingPlace(TourLedger ledger, TourMatter matter, TourSetting setting) { + if (ledger == null || setting == null) { + return null; + } + List options = parseBoardingPlaceOptions(setting.getBoardingPlace()); + if (Lang.isEmpty(options)) { + ledger.setBoardingPlace(""); + return null; + } + String boardingPlace = StrUtil.blankToDefault(ledger.getBoardingPlace(), matter == null ? "" : matter.getDefaultBoardingPlace()); + if (StrUtil.isBlank(boardingPlace)) { + return Result.error("请选择乘车地点"); + } + if (!options.contains(boardingPlace)) { + return Result.error("乘车地点不在当前疗休养配置范围内"); + } + ledger.setBoardingPlace(boardingPlace); + return null; + } + + private List parseBoardingPlaceOptions(String value) { + if (StrUtil.isBlank(value)) { + return Collections.emptyList(); + } + try { + List list = Json.fromJson(List.class, value); + if (Lang.isEmpty(list)) { + return Collections.emptyList(); + } + return list.stream().map(item -> { + if (item instanceof String) { + return ((String) item).trim(); + } + if (item instanceof java.util.Map) { + Object name = ((java.util.Map) item).get("name"); + return name == null ? "" : String.valueOf(name).trim(); + } + return ""; + }) + .filter(StrUtil::isNotBlank) + .distinct() + .collect(Collectors.toList()); + } catch (Exception e) { + return java.util.Arrays.stream(value.split(",")) + .map(String::trim) + .filter(StrUtil::isNotBlank) + .distinct() + .collect(Collectors.toList()); + } + } + + /** + * 根据疗休养配置的报名资格方式切换校验来源。 + * ASSIGNED_USER 模式读取人员分配表,并要求当前用户为正式人员;否则沿用可参加人员范围校验。 + */ + private Result checkSignupEligibilityScope(TourSetting setting) { + if (isAssignedUserEligibility(setting)) { + NutMap assignmentResult = tourUserAssignmentService.checkFormalAssignedUser(setting.getId(), SecurityUtil.getUserId()); + if (!assignmentResult.getBoolean("canApply")) { + return Result.error(defaultIfBlank(assignmentResult.getString("message"), "您不在本次疗休养人员分配名单中,无法报名")); + } + return null; + } + Integer activityGroupId = parseInteger(setting.getActivityGroupId()); + if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) { + return Result.error("您不在本次疗休养报名范围内"); + } + return null; + } + private String buildOutProvinceQuotaNotice(OutProvinceQuota quota) { String suffix = quota.canApply() ? "您可以报名,或您稍后报名" : "请稍后报名"; if ("当年报名人数".equals(quota.ratioType())) { diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLeaveApply.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLeaveApply.java new file mode 100644 index 00000000..8f342211 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLeaveApply.java @@ -0,0 +1,103 @@ +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.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_leave_apply") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("疗休养退出申请") +public class TourLeaveApply extends BaseModel implements Serializable { + + public static final String STATUS_PENDING = "PENDING"; + public static final String STATUS_APPROVED = "APPROVED"; + public static final String STATUS_REJECTED = "REJECTED"; + + @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 matterId; + + @Column + @Comment("线路ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String lineId; + + @Column + @Comment("工号") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String jobNo; + + @Column + @Comment("姓名") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String userName; + + @Column + @Comment("用户ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String userId; + + @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.TEXT) + private String reason; + + @Column + @Comment("状态") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String status; + + @Column + @Comment("审核人ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String auditBy; + + @Column + @Comment("审核人姓名") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String auditName; + + @Column + @Comment("审核时间") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String auditTime; + + @Column + @Comment("审核备注") + @ColDefine(type = ColType.TEXT) + private String auditRemark; +} 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..7fde2dbd 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 @@ -111,6 +111,11 @@ public class TourLedger extends BaseModel implements Serializable { @ColDefine(type = ColType.VARCHAR, width = 100) private String travelAgencyName; + @Column + @Comment("乘车地点") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String boardingPlace; + @Column @Comment("是否携带家属") @ColDefine(type = ColType.BOOLEAN) diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourMatter.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourMatter.java index f06aec8a..54a061de 100644 --- a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourMatter.java +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourMatter.java @@ -71,6 +71,11 @@ public class TourMatter extends BaseModel implements Serializable { @ColDefine(type = ColType.VARCHAR, width = 32) private String lineId; + @Column + @Comment("默认乘车地点") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String defaultBoardingPlace; + @Column @Comment("报名开始时间") @ColDefine(type = ColType.VARCHAR, width = 20) 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..447c025d 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,11 +43,28 @@ public class TourSetting extends BaseModel implements Serializable { @ColDefine(type = ColType.VARCHAR, width = 50) private String tourType; + @Column + @Comment("乘车地点列表JSON") + @ColDefine(type = ColType.TEXT) + private String boardingPlace; + + @Column + @Comment("出行人数指标") + @ColDefine(type = ColType.INT) + @Default("0") + private Integer travelPeopleQuota; + @Column @Comment("可参加人员范围ID") @ColDefine(type = ColType.VARCHAR, width = 32) private String activityGroupId; + @Column + @Comment("报名资格校验方式") + @ColDefine(type = ColType.VARCHAR, width = 30) + @Default("'ASSIGNED_USER'") + private String signupEligibilityMode; + @Column @Comment("排序编号") @ColDefine(type = ColType.INT) @@ -127,6 +144,11 @@ public class TourSetting extends BaseModel implements Serializable { @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..1108154e --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSettingUnionQuota.java @@ -0,0 +1,66 @@ +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 formalQuota; + + @Column + @Comment("替补人员名额") + @ColDefine(type = ColType.INT) + @Default("0") + private Integer backupQuota; + + /** + * 页面展示用实时会员数,不落库。 + */ + private Integer memberCount; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourUserAssignment.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourUserAssignment.java new file mode 100644 index 00000000..465a5398 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourUserAssignment.java @@ -0,0 +1,155 @@ +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_user_assignment") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("疗休养人员分配") +public class TourUserAssignment extends BaseModel implements Serializable { + + public static final String ASSIGN_SOURCE_SCHOOL_UNION = "SCHOOL_UNION"; + public static final String ASSIGN_SOURCE_BRANCH_UNION = "BRANCH_UNION"; + public static final String PERSON_TYPE_FORMAL = "FORMAL"; + public static final String PERSON_TYPE_BACKUP = "BACKUP"; + + @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 matterId; + + @Column + @Comment("疗休养事项名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String matterName; + + @Column + @Comment("旅行社ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String travelAgencyId; + + @Column + @Comment("旅行社名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String travelAgencyName; + + @Column + @Comment("线路ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String lineId; + + @Column + @Comment("线路名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String lineName; + + @Column + @Comment("乘车地点") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String boardingPlace; + + @Column + @Comment("人员ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String userId; + + @Column + @Comment("工号") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String loginName; + + @Column + @Comment("姓名") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String userName; + + @Column + @Comment("性别") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String gender; + + @Column + @Comment("手机号") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String mobile; + + @Column + @Comment("所在单位ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String unitId; + + @Column + @Comment("所在单位") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String unitName; + + @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.VARCHAR, width = 30) + private String assignSource; + + @Column + @Comment("人员类型") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String personType; + + @Column + @Comment("是否已退出") + @ColDefine(type = ColType.BOOLEAN) + @Default("0") + private Boolean cancelled; + + @Column + @Comment("分配人ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String assignedBy; + + @Column + @Comment("分配人姓名") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String assignedName; + + @Column + @Comment("分配时间") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String assignedAt; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLeaveApplyService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLeaveApplyService.java new file mode 100644 index 00000000..f576bfb4 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLeaveApplyService.java @@ -0,0 +1,31 @@ +package com.budwk.app.zhgh.dayofficework.tour.service; + +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLeaveApply; +import org.nutz.lang.util.NutMap; + +public interface TourLeaveApplyService extends BaseService { + + /** + * 分页查询人员分配表中的退出/取消管理数据,关键词同时匹配工号和姓名。 + * 数据权限:系统管理员、校工会主席看全部;分工会主席看本分工会;普通用户看本人。 + */ + Pagination pageData(PageForm pageForm, String keyword, String unionName, String status); + + /** + * 查询当前登录人在退出取消管理页的按钮权限。 + */ + NutMap permissionInfo(); + + /** + * 取消指定人员分配记录,同时删除对应报名台账并标记已退出。 + */ + void cancelAssignment(String id); + + /** + * 恢复指定人员分配记录的退出状态,不恢复已删除台账。 + */ + void restoreAssignment(String id); +} 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..335f18c9 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,41 @@ 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 branchTotalQuota 分工会总名额 + * @return 通过返回空字符串;不通过返回业务提示 + */ + String checkBranchFormalQuotaLimit(String unionQuotas, int branchTotalQuota); + + /** + * 清理指定疗休养配置下的分工会名额。 + * + * @param settingId 疗休养配置ID + */ + void clearUnionQuotas(String settingId); } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourUserAssignmentService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourUserAssignmentService.java new file mode 100644 index 00000000..9c2b675e --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourUserAssignmentService.java @@ -0,0 +1,285 @@ +package com.budwk.app.zhgh.dayofficework.tour.service; + +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourUserAssignment; +import org.nutz.lang.util.NutMap; + +import java.util.List; + +public interface TourUserAssignmentService extends BaseService { + + /** + * 分页查询校工会人员分配记录,列表只读取人员分配表,不读取报名台账。 + * + * @param pageForm 分页、排序参数 + * @param year 疗休养年度 + * @param settingId 疗休养配置ID + * @param matterId 疗休养事项ID + * @param unionId 所属分工会ID + * @param personType 人员类型:FORMAL 正式人员,BACKUP 替补人员 + * @param keyword 姓名或工号关键字 + * @return 校工会分配记录分页数据 + */ + Pagination schoolAssignmentPage(PageForm pageForm, Integer year, String settingId, String matterId, + String unionId, String personType, String keyword); + + /** + * 分页查询校工会可分配候选人,候选人来自疗休养配置的可参加人员范围,并排除同一配置下已分配人员。 + * + * @param pageForm 分页、排序参数 + * @param settingId 疗休养配置ID + * @param unionId 所属分工会ID + * @param keyword 姓名或工号关键字 + * @return 可分配候选人分页数据 + */ + Pagination schoolCandidatePage(PageForm pageForm, String settingId, String unionId, String keyword); + + /** + * 分页查询当前登录人所在分工会的人员分配记录,列表只读取人员分配表,不读取报名台账。 + * + * @param pageForm 分页、排序参数 + * @param year 疗休养年度 + * @param settingId 疗休养配置ID + * @param matterId 疗休养事项ID + * @param personType 人员类型:FORMAL 正式人员,BACKUP 替补人员 + * @param keyword 姓名或工号关键字 + * @return 分工会分配记录分页数据 + */ + Pagination branchAssignmentPage(PageForm pageForm, Integer year, String settingId, String matterId, + String personType, String keyword); + + /** + * 分页查询当前登录人所在分工会可分配候选人,候选人来自疗休养配置的可参加人员范围。 + * + * @param pageForm 分页、排序参数 + * @param settingId 疗休养配置ID + * @param keyword 姓名或工号关键字 + * @return 当前分工会可分配候选人分页数据 + */ + Pagination branchCandidatePage(PageForm pageForm, String settingId, String keyword); + + /** + * 查询启用的疗休养配置选项,供人员分配列表筛选和分配弹窗复用。 + * + * @param year 疗休养年度 + * @return 配置选项列表 + */ + List listEnabledSettingOptions(Integer year); + + /** + * 查询指定配置下已配置线路的分配线路选项,用于人员分配时指定事项、线路和旅行社。 + * + * @param settingId 疗休养配置ID + * @return 事项选项列表 + */ + List listMatterOptions(String settingId); + + /** + * 查询分工会选项,供校工会分配候选人和列表筛选使用。 + * + * @return 分工会选项列表 + */ + List listUnionOptions(); + + /** + * 保存校工会人员分配。保存前会重新按配置可参加人员范围过滤,防止写入范围外人员。 + * 分配线路为可选;正式人员选择线路时视为代报名并同步写入疗休养台账,替补人员不能分配线路。 + * + * @param settingId 疗休养配置ID + * @param matterId 疗休养事项ID,可为空 + * @param personType 人员类型:FORMAL 正式人员,BACKUP 替补人员 + * @param userIds 待分配人员ID列表 + * @return 保存结果,包含实际分配数量、跳过数量和代报名台账写入数量 + */ + NutMap assignSchoolUsers(String settingId, String matterId, String personType, List userIds); + + /** + * 保存校工会人员分配,支持每个候选人员单独选择分配线路。 + * assignItems 中每项包含 userId、matterId;matterId 为空时只保存人员分配,不写台账。 + * + * @param settingId 疗休养配置ID + * @param assignItems 人员和分配线路明细 + * @return 保存结果,包含实际分配数量、跳过数量和代报名台账写入数量 + */ + NutMap assignSchoolUsers(String settingId, List assignItems); + + /** + * 给校工会已分配的正式人员补选分配线路,并按代报名写入疗休养台账。 + * 只处理 SCHOOL_UNION 来源且尚未选择线路的分配记录,避免覆盖既有台账。 + * + * @param id 人员分配记录ID + * @param matterId 疗休养事项ID + * @return 处理结果,包含台账写入数量 + */ + NutMap selectSchoolAssignmentMatter(String id, String matterId); + + /** + * 保存分工会人员分配。保存前会重新按配置可参加人员范围和当前登录人所在分工会过滤,并校验正式/替补名额。 + * 疗休养事项为可选;正式人员选择事项时视为代报名并同步写入疗休养台账,替补人员不能分配事项。 + * + * @param settingId 疗休养配置ID + * @param matterId 疗休养事项ID,可为空 + * @param personType 人员类型:FORMAL 正式人员,BACKUP 替补人员 + * @param userIds 待分配人员ID列表 + * @return 保存结果,包含实际分配数量、跳过数量、代报名台账写入数量和剩余名额 + */ + NutMap assignBranchUsers(String settingId, String matterId, String personType, List userIds); + + /** + * 切换当前分工会人员分配记录的正式/替补状态。 + * 切换时会校验当前登录人所在分工会、分配来源和目标类型剩余名额;已分配事项的正式人员不能切换为替补。 + * + * @param id 人员分配记录ID + * @param personType 目标人员类型:FORMAL 正式人员,BACKUP 替补人员 + * @return 切换后的人员类型和最新名额信息 + */ + NutMap switchCurrentBranchPersonType(String id, String personType); + + /** + * 用户报名写入台账后,回填该用户在同一疗休养配置下已存在的人员分配记录。 + * 仅更新事项、线路、旅行社快照字段,不新增记录,不修改 assignSource 和人员类型。 + * + * @param settingId 疗休养配置ID + * @param matterId 疗休养事项ID + * @param userId 报名用户ID + * @param boardingPlace 用户报名时最终选择的乘车地点 + * @return 更新记录数 + */ + int backfillExistingAssignmentAfterSignup(String settingId, String matterId, String userId, String boardingPlace); + + /** + * 用户取消报名删除台账后,清空该用户在同一疗休养配置下已存在人员分配记录的事项快照。 + * 仅清空事项、线路、旅行社字段,不删除记录,不修改 assignSource 和人员类型。 + * + * @param settingId 疗休养配置ID + * @param userId 报名用户ID + * @return 更新记录数 + */ + int clearExistingAssignmentMatterAfterCancel(String settingId, String userId); + + /** + * 查询当前登录人所在分工会在指定疗休养配置下的名额使用情况。 + * + * @param settingId 疗休养配置ID + * @return 正式/替补名额、已用名额和剩余名额 + */ + NutMap branchQuotaInfo(String settingId); + + /** + * 查询校工会人员分配记录是否已存在对应报名台账,用于删除前二次确认。 + * + * @param id 人员分配记录ID + * @return hasLedger 表示是否存在台账,ledgerCount 表示对应台账数量 + */ + NutMap schoolDeleteInfo(String id); + + /** + * 查询当前分工会人员分配记录是否已存在对应报名台账,用于删除前二次确认。 + * + * @param id 人员分配记录ID + * @return hasLedger 表示是否存在台账,ledgerCount 表示对应台账数量 + */ + NutMap branchDeleteInfo(String id); + + /** + * 删除指定来源的人员分配记录,避免校工会页面误删分工会分配的数据。 + * + * @param id 人员分配记录ID + * @param assignSource 分配来源 + */ + void deleteBySource(String id, String assignSource); + + /** + * 删除指定来源的人员分配记录,可选择是否同步删除该分配记录对应的报名台账。 + * + * @param id 人员分配记录ID + * @param assignSource 分配来源 + * @param deleteLedger 是否同步删除 matterId + loginName 对应的台账及明细 + */ + void deleteBySource(String id, String assignSource, boolean deleteLedger); + + /** + * 删除当前登录人所在分工会的人员分配记录,避免分工会页面删除其它分工会的数据。 + * + * @param id 人员分配记录ID + */ + void deleteCurrentBranchAssignment(String id); + + /** + * 删除当前登录人所在分工会的人员分配记录,可选择是否同步删除该分配记录对应的报名台账。 + * + * @param id 人员分配记录ID + * @param deleteLedger 是否同步删除 matterId + loginName 对应的台账及明细 + */ + void deleteCurrentBranchAssignment(String id, boolean deleteLedger); + + /** + * 判断用户是否已存在于指定疗休养配置的人员分配表中。 + * + * @param settingId 疗休养配置ID + * @param userId 用户ID + * @return 存在返回 true,不存在返回 false + */ + boolean existsAssignedUser(String settingId, String userId); + + /** + * 校验用户是否为指定疗休养配置下的正式分配人员。 + * 用于报名资格方式为 ASSIGNED_USER 时,拦截未分配人员和替补人员。 + * 已退出人员也会被拦截,避免取消后再次报名。 + * + * @param settingId 疗休养配置ID + * @param userId 用户ID + * @return canApply 表示是否可报名,message 表示不可报名原因 + */ + NutMap checkFormalAssignedUser(String settingId, String userId); + + /** + * 实时统计指定疗休养配置下校工会来源的正式分配人数。 + * 已退出人员不再占用工会名额,统计时需要排除。 + * + * @param settingId 疗休养配置ID + * @return 当前配置下校工会正式人员分配数量 + */ + int countSchoolFormalAssignedUsers(String settingId); + + /** + * 将人员分配记录标记为已退出,并删除该记录对应事项下的报名台账。 + * 取消前会校验路线出行开始时间,只有出行开始前指定天数之前允许取消。 + * + * @param id 人员分配记录ID + */ + void cancelAssignment(String id); + + /** + * 将人员分配记录从已退出恢复为未退出。 + * 只恢复人员分配表状态,不自动恢复已删除的报名台账。 + * + * @param id 人员分配记录ID + */ + void restoreCancelledAssignment(String id); + + /** + * 按分配来源恢复已退出人员,避免校工会和分工会页面互相恢复对方数据。 + * + * @param id 人员分配记录ID + * @param assignSource 分配来源 + */ + void restoreCancelledAssignmentBySource(String id, String assignSource); + + /** + * 恢复当前登录人所在分工会的已退出人员分配记录。 + * 用于分工会人员分配列表的“取消退出”按钮,防止跨分工会恢复数据。 + * + * @param id 人员分配记录ID + */ + void restoreCurrentBranchCancelledAssignment(String id); + + /** + * 按疗休养配置清理人员分配数据,供删除配置或后续重置分配时复用。 + * + * @param settingId 疗休养配置ID + */ + void clearBySettingId(String settingId); +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLeaveApplyServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLeaveApplyServiceImpl.java new file mode 100644 index 00000000..bb66fa13 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLeaveApplyServiceImpl.java @@ -0,0 +1,169 @@ +package com.budwk.app.zhgh.dayofficework.tour.service.impl; + +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.constant.RoleConstant; +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.web.commons.auth.utils.AuthUtil; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLeaveApply; +import com.budwk.app.zhgh.dayofficework.tour.models.TourUserAssignment; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLeaveApplyService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.util.cri.SqlExpressionGroup; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; + +@IocBean(args = {"refer:dao"}) +public class TourLeaveApplyServiceImpl extends BaseServiceImpl implements TourLeaveApplyService { + + @Inject + private TourUserAssignmentService tourUserAssignmentService; + + public TourLeaveApplyServiceImpl(Dao dao) { + super(dao); + } + + @Override + public Pagination pageData(PageForm pageForm, String keyword, String unionName, String status) { + Sql sql = Sqls.create(""" + SELECT + a.*, + s.`year` AS `year`, + s.configName AS settingName, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE '' + END AS travelPeriod, + m.travelStartTime AS travelStartTime, + IF(IFNULL(a.cancelled, 0) = 1, 'CANCELLED', 'NORMAL') AS cancelStatus + FROM tour_user_assignment a + LEFT JOIN tour_setting s ON s.id = a.settingId + LEFT JOIN tour_matter m ON m.id = a.matterId AND m.delFlag = 0 + $condition + """); + Cnd cnd = Cnd.NEW(); + cnd.and("a.delFlag", "=", false); + // 退出取消只针对已选路线的数据,未分配路线的人员没有出行时间和台账可处理。 + cnd.and("a.matterId", "is not", null); + cnd.and("a.matterId", "<>", ""); + cnd.and("a.lineId", "is not", null); + cnd.and("a.lineId", "<>", ""); + cnd.and("a.personType", "=", TourUserAssignment.PERSON_TYPE_FORMAL); + applyDataPermission(cnd); + if (StrUtil.isNotBlank(keyword)) { + SqlExpressionGroup keywordGroup = new SqlExpressionGroup(); + keywordGroup.orLike("a.loginName", keyword); + keywordGroup.orLike("a.userName", keyword); + cnd.and(keywordGroup); + } + cnd.and(Cnd.likeEX("a.unionName", unionName)); + if ("CANCELLED".equals(status)) { + cnd.and("a.cancelled", "=", true); + } else if ("NORMAL".equals(status)) { + cnd.and(Cnd.exps("a.cancelled", "=", false).or("a.cancelled", "is", null)); + } + applyOrder(cnd, pageForm); + sql.setCondition(cnd); + return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); + } + + @Override + public NutMap permissionInfo() { + return NutMap.NEW() + .addv("isAllDataRole", isAllDataRole()) + .addv("isBranchUnionChairman", AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())) + .addv("canRestore", StpUtil.hasPermission("tour.branchUserAssignment.cancelRestore")); + } + + @Override + public void cancelAssignment(String id) { + checkCanOperateAssignment(id); + tourUserAssignmentService.cancelAssignment(id); + } + + @Override + public void restoreAssignment(String id) { + checkCanOperateAssignment(id); + tourUserAssignmentService.restoreCancelledAssignment(id); + } + + /** + * 退出取消管理读取人员分配表,按用户角色收窄可见范围。 + */ + private void applyDataPermission(Cnd cnd) { + if (isAllDataRole()) { + return; + } + if (AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())) { + cnd.andEX("a.unionId", "=", SecurityUtil.getUnionId()); + return; + } + cnd.and("a.userId", "=", SecurityUtil.getUserId()); + } + + private boolean isAllDataRole() { + return AuthUtil.hasRole(RoleConstant.SYSADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_CHAIRMAN.name()); + } + + /** + * 操作前按同一数据权限校验,避免前端绕过列表直接提交其它人员记录。 + */ + private void checkCanOperateAssignment(String id) { + if (StrUtil.isBlank(id)) { + throw new IllegalArgumentException("参数错误"); + } + Cnd cnd = Cnd.where(TourUserAssignment::getId, "=", id) + .and(TourUserAssignment::getDelFlag, "=", false); + if (!isAllDataRole()) { + if (AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())) { + cnd.and(TourUserAssignment::getUnionId, "=", SecurityUtil.getUnionId()); + } else { + cnd.and(TourUserAssignment::getUserId, "=", SecurityUtil.getUserId()); + } + } + if (dao().count(TourUserAssignment.class, cnd) <= 0) { + throw new IllegalArgumentException("人员分配记录不存在或无权操作"); + } + } + + /** + * 仅开放页面使用到的排序字段,避免前端传入任意字段名影响查询。 + */ + private void applyOrder(Cnd cnd, PageForm pageForm) { + String orderName = pageForm.getPageOrderName(); + String orderBy = pageForm.getPageOrderBy(); + boolean descending = "descending".equals(orderBy); + if ("loginName".equals(orderName)) { + order(cnd, "a.loginName", descending); + } else if ("userName".equals(orderName)) { + order(cnd, "a.userName", descending); + } else if ("unionName".equals(orderName)) { + order(cnd, "a.unionName", descending); + } else if ("cancelStatus".equals(orderName)) { + order(cnd, "a.cancelled", descending); + } else if ("assignedAt".equals(orderName)) { + order(cnd, "a.assignedAt", descending); + } else { + cnd.desc("a.assignedAt"); + cnd.desc("a.createdAt"); + } + } + + private void order(Cnd cnd, String field, boolean descending) { + if (descending) { + cnd.desc(field); + } else { + cnd.asc(field); + } + } +} 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..c2b360c3 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,10 +1,23 @@ 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.Map; +import java.util.stream.Collectors; @IocBean(args = {"refer:dao"}) public class TourSettingServiceImpl extends BaseServiceImpl implements TourSettingService { @@ -12,4 +25,109 @@ public class TourSettingServiceImpl extends BaseServiceImpl impleme 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.formalQuota, 0) AS formalQuota, + COALESCE(quota.backupQuota, 0) AS backupQuota, + COALESCE(user_count.memberCount, 0) AS memberCount + FROM sys_union un + LEFT JOIN tour_setting_union_quota quota + ON quota.unionId = un.id + AND quota.settingId = @settingId + LEFT JOIN ( + SELECT unionId, COUNT(1) AS memberCount + FROM vw_user + WHERE member = 1 + GROUP BY unionId + ) user_count ON user_count.unionId = un.id + ORDER BY un.unionCode ASC + """); + sql.setParam("settingId", settingId == null ? "" : 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.setFormalQuota(defaultInt(row.getInt("formalQuota"))); + quota.setBackupQuota(defaultInt(row.getInt("backupQuota"))); + quota.setMemberCount(defaultInt(row.getInt("memberCount"))); + 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.NEW()).stream() + .collect(Collectors.toMap(Sys_union::getId, item -> item, (a, b) -> a)); + List saveList = quotaList.stream() + .filter(item -> item != null && StrUtil.isNotBlank(item.getUnionId())) + .map(item -> normalizeUnionQuota(settingId, item, unionMap)) + .filter(item -> item.getFormalQuota() > 0 || item.getBackupQuota() > 0) + .collect(Collectors.toList()); + if (Lang.isNotEmpty(saveList)) { + dao().insert(saveList); + } + } + + @Override + public String checkBranchFormalQuotaLimit(String unionQuotas, int branchTotalQuota) { + if (StrUtil.isBlank(unionQuotas)) { + return ""; + } + List quotaList = Json.fromJsonAsList(TourSettingUnionQuota.class, unionQuotas); + if (Lang.isEmpty(quotaList)) { + return ""; + } + int formalTotal = quotaList.stream() + .filter(item -> item != null) + .mapToInt(item -> defaultInt(item.getFormalQuota())) + .sum(); + if (formalTotal > branchTotalQuota) { + return "当前正式人员总数 " + formalTotal + ",分工会总名额 " + branchTotalQuota + ",分配后总人数不能超过分工会总名额"; + } + 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 == null ? item.getUnionName() : union.getName()); + quota.setFormalQuota(defaultInt(item.getFormalQuota())); + quota.setBackupQuota(defaultInt(item.getBackupQuota())); + return quota; + } + + private Integer defaultInt(Integer value) { + return value == null || value < 0 ? 0 : value; + } } diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourUserAssignmentServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourUserAssignmentServiceImpl.java new file mode 100644 index 00000000..2cc3a167 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourUserAssignmentServiceImpl.java @@ -0,0 +1,1103 @@ +package com.budwk.app.zhgh.dayofficework.tour.service.impl; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.sys.models.Sys_config; +import com.budwk.app.sys.services.SysConfigService; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.dayofficework.tour.models.*; +import com.budwk.app.zhgh.dayofficework.tour.service.TourUserAssignmentService; +import org.nutz.dao.Chain; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.util.cri.SqlExpressionGroup; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.Lang; +import org.nutz.lang.util.NutMap; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.stream.Collectors; + +@IocBean(args = {"refer:dao"}) +public class TourUserAssignmentServiceImpl extends BaseServiceImpl implements TourUserAssignmentService { + + private static final String TOUR_CANCEL_DEADLINE_DAYS_KEY = "tourLeaveCancelDays"; + + @Inject + private SysConfigService sysConfigService; + + public TourUserAssignmentServiceImpl(Dao dao) { + super(dao); + } + + @Override + public Pagination schoolAssignmentPage(PageForm pageForm, Integer year, String settingId, String matterId, + String unionId, String personType, String keyword) { + Sql sql = Sqls.create(""" + SELECT + a.*, + s.`year` AS `year`, + s.configName AS settingName, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE '' + END AS travelPeriod + FROM tour_user_assignment a + LEFT JOIN tour_setting s ON s.id = a.settingId + LEFT JOIN tour_matter m ON m.id = a.matterId AND m.delFlag = 0 + $condition + """); + Cnd cnd = Cnd.NEW(); + cnd.and("a.delFlag", "=", false); + cnd.and("a.assignSource", "=", TourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION); + cnd.andEX("s.`year`", "=", year); + cnd.andEX("a.settingId", "=", settingId); + cnd.andEX("a.matterId", "=", matterId); + cnd.andEX("a.unionId", "=", unionId); + cnd.andEX("a.personType", "=", personType); + if (StrUtil.isNotBlank(keyword)) { + SqlExpressionGroup group = new SqlExpressionGroup(); + group.orLike("a.userName", keyword); + group.orLike("a.loginName", keyword); + cnd.and(group); + } + appendAssignmentOrder(cnd, pageForm); + sql.setCondition(cnd); + return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); + } + + @Override + public Pagination schoolCandidatePage(PageForm pageForm, String settingId, String unionId, String keyword) { + TourSetting setting = fetchSettingForAssignment(settingId); + if (setting == null || StrUtil.isBlank(setting.getActivityGroupId())) { + return emptyPagination(pageForm); + } + Sql sql = buildCandidateSql(settingId); + Cnd cnd = buildCandidateCnd(settingId, setting.getActivityGroupId(), unionId, keyword); + cnd.asc("u.unionname"); + cnd.asc("u.unitname"); + cnd.asc("u.loginname"); + sql.setCondition(cnd); + return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); + } + + @Override + public Pagination branchAssignmentPage(PageForm pageForm, Integer year, String settingId, String matterId, + String personType, String keyword) { + String unionId = SecurityUtil.getUnionId(); + if (StrUtil.isBlank(unionId)) { + return emptyPagination(pageForm); + } + return assignmentPage(pageForm, year, settingId, matterId, unionId, personType, keyword, + TourUserAssignment.ASSIGN_SOURCE_BRANCH_UNION); + } + + @Override + public Pagination branchCandidatePage(PageForm pageForm, String settingId, String keyword) { + String unionId = SecurityUtil.getUnionId(); + if (StrUtil.isBlank(unionId)) { + return emptyPagination(pageForm); + } + return schoolCandidatePage(pageForm, settingId, unionId, keyword); + } + + @Override + public List listEnabledSettingOptions(Integer year) { + Sql sql = Sqls.create(""" + SELECT id, `year`, configName, activityGroupId + FROM tour_setting + $condition + ORDER BY `year` DESC, sortNo ASC, createdAt DESC + """); + Cnd cnd = Cnd.NEW(); + cnd.and("delFlag", "=", false); + cnd.and("enabled", "=", true); + cnd.andEX("`year`", "=", year); + sql.setCondition(cnd); + return listMap(sql); + } + + @Override + public List listMatterOptions(String settingId) { + if (StrUtil.isBlank(settingId)) { + return Collections.emptyList(); + } + Sql sql = Sqls.create(""" + SELECT + m.id AS matterId, + m.matterName, + m.`year` AS `year`, + m.settingId, + m.lineId, + l.lineName, + l.lineType, + l.travelAgencyId, + ta.agencyName AS travelAgencyName + FROM tour_matter m + LEFT JOIN tour_line l ON l.id = m.lineId + LEFT JOIN tour_travel_agency ta ON ta.id = l.travelAgencyId + $condition + ORDER BY m.`year` DESC, m.createdAt DESC + """); + Cnd cnd = Cnd.NEW(); + cnd.and("m.delFlag", "=", false); + cnd.and("m.enabled", "=", true); + cnd.and("m.settingId", "=", settingId); + cnd.and("l.id", "is not", null); + cnd.and("l.delFlag", "=", false); + cnd.and("l.enabled", "=", true); + sql.setCondition(cnd); + return listMap(sql); + } + + @Override + public List listUnionOptions() { + Sql sql = Sqls.create(""" + SELECT id, name, unionCode + FROM sys_union + ORDER BY unionCode ASC, name ASC + """); + return listMap(sql); + } + + @Override + public NutMap assignSchoolUsers(String settingId, String matterId, String personType, List userIds) { + List distinctUserIds = normalizeUserIds(userIds); + if (StrUtil.isBlank(settingId) || Lang.isEmpty(distinctUserIds)) { + throw new IllegalArgumentException("请选择疗休养配置和分配人员"); + } + // 校工会人员分配均按正式人员处理,避免前端绕过页面提交替补类型。 + String normalizedPersonType = TourUserAssignment.PERSON_TYPE_FORMAL; + NutMap matterInfo = prepareMatterInfoForAssignment(settingId, matterId, normalizedPersonType); + TourSetting setting = fetchSettingForAssignment(settingId); + if (setting == null) { + throw new IllegalArgumentException("疗休养配置不存在"); + } + if (StrUtil.isBlank(setting.getActivityGroupId())) { + throw new IllegalArgumentException("疗休养配置未设置可参加人员范围"); + } + + // 保存前重新按“配置人员范围 + 未分配”查询,防止前端绕过候选列表提交范围外人员。 + Sql userSql = buildCandidateSql(settingId); + Cnd userCnd = buildCandidateCnd(settingId, setting.getActivityGroupId(), null, null); + userCnd.and("u.id", "in", distinctUserIds); + userSql.setCondition(userCnd); + List candidateUsers = listMap(userSql); + List assignments = candidateUsers.stream() + .map(user -> buildSchoolAssignment(settingId, matterInfo, normalizedPersonType, user)) + .collect(Collectors.toList()); + if (Lang.isNotEmpty(assignments)) { + dao().insert(assignments); + } + // 分配线路为空时只写人员分配表;选择分配线路时按代报名写入台账。 + int ledgerCount = insertProxySignupLedgers(matterInfo, candidateUsers); + return NutMap.NEW() + .addv("assignCount", assignments.size()) + .addv("skipCount", distinctUserIds.size() - assignments.size()) + .addv("ledgerCount", ledgerCount); + } + + @Override + public NutMap assignSchoolUsers(String settingId, List assignItems) { + if (StrUtil.isBlank(settingId) || Lang.isEmpty(assignItems)) { + throw new IllegalArgumentException("请选择疗休养配置和分配人员"); + } + TourSetting setting = fetchSettingForAssignment(settingId); + if (setting == null) { + throw new IllegalArgumentException("疗休养配置不存在"); + } + if (StrUtil.isBlank(setting.getActivityGroupId())) { + throw new IllegalArgumentException("疗休养配置未设置可参加人员范围"); + } + + Map matterByUserId = new HashMap<>(); + List userIds = assignItems.stream() + .map(item -> item == null ? "" : item.getString("userId", "")) + .filter(StrUtil::isNotBlank) + .distinct() + .collect(Collectors.toList()); + assignItems.forEach(item -> { + if (item == null) { + return; + } + String userId = item.getString("userId", ""); + if (StrUtil.isBlank(userId)) { + return; + } + matterByUserId.put(userId, item.getString("matterId", "")); + }); + if (Lang.isEmpty(userIds)) { + throw new IllegalArgumentException("请选择分配人员"); + } + + Map matterInfoMap = new HashMap<>(); + matterByUserId.values().stream() + .filter(StrUtil::isNotBlank) + .distinct() + .forEach(matterId -> { + NutMap matterInfo = fetchMatterInfo(settingId, matterId); + if (matterInfo == null) { + throw new IllegalArgumentException("分配线路不存在或未配置线路"); + } + matterInfoMap.put(matterId, matterInfo); + }); + + // 保存前重新按“配置人员范围 + 未分配”查询,防止前端绕过候选列表提交范围外人员。 + Sql userSql = buildCandidateSql(settingId); + Cnd userCnd = buildCandidateCnd(settingId, setting.getActivityGroupId(), null, null); + userCnd.and("u.id", "in", userIds); + userSql.setCondition(userCnd); + List candidateUsers = listMap(userSql); + + List assignments = candidateUsers.stream() + .map(user -> { + String userId = user.getString("userId", ""); + NutMap matterInfo = matterInfoMap.get(matterByUserId.get(userId)); + return buildSchoolAssignment(settingId, matterInfo, TourUserAssignment.PERSON_TYPE_FORMAL, user); + }) + .collect(Collectors.toList()); + if (Lang.isNotEmpty(assignments)) { + dao().insert(assignments); + } + + int ledgerCount = 0; + for (Map.Entry entry : matterInfoMap.entrySet()) { + String matterId = entry.getKey(); + List users = candidateUsers.stream() + .filter(user -> matterId.equals(matterByUserId.get(user.getString("userId", "")))) + .collect(Collectors.toList()); + ledgerCount += insertProxySignupLedgers(entry.getValue(), users); + } + return NutMap.NEW() + .addv("assignCount", assignments.size()) + .addv("skipCount", userIds.size() - assignments.size()) + .addv("ledgerCount", ledgerCount); + } + + @Override + public NutMap selectSchoolAssignmentMatter(String id, String matterId) { + if (StrUtil.isBlank(id) || StrUtil.isBlank(matterId)) { + throw new IllegalArgumentException("请选择人员分配记录和分配线路"); + } + TourUserAssignment assignment = fetch(Cnd.where(TourUserAssignment::getId, "=", id) + .and(TourUserAssignment::getAssignSource, "=", TourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION) + .and(TourUserAssignment::getDelFlag, "=", false)); + if (assignment == null) { + throw new IllegalArgumentException("人员分配记录不存在"); + } + if (StrUtil.isNotBlank(assignment.getMatterId())) { + throw new IllegalArgumentException("该人员已选择分配线路"); + } + String personType = normalizePersonType(assignment.getPersonType()); + if (!TourUserAssignment.PERSON_TYPE_FORMAL.equals(personType)) { + throw new IllegalArgumentException("只有正式人员可以选择分配线路"); + } + NutMap matterInfo = fetchMatterInfo(assignment.getSettingId(), matterId); + if (matterInfo == null) { + throw new IllegalArgumentException("分配线路不存在或未配置线路"); + } + // 补选事项时同时回写人员分配表事项快照,后续列表筛选无需再跨表追溯事项信息。 + update(Chain.make("matterId", matterInfo.getString("matterId")) + .add("matterName", matterInfo.getString("matterName")) + .add("lineId", matterInfo.getString("lineId")) + .add("lineName", matterInfo.getString("lineName")) + .add("travelAgencyId", matterInfo.getString("travelAgencyId")) + .add("travelAgencyName", matterInfo.getString("travelAgencyName")) + .add("boardingPlace", matterInfo.getString("defaultBoardingPlace")), + Cnd.where(TourUserAssignment::getId, "=", id) + .and(TourUserAssignment::getAssignSource, "=", TourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION)); + int ledgerCount = insertProxySignupLedgers(matterInfo, Collections.singletonList(fetchAssignmentUserSnapshot(assignment))); + return NutMap.NEW().addv("ledgerCount", ledgerCount); + } + + @Override + public NutMap assignBranchUsers(String settingId, String matterId, String personType, List userIds) { + String unionId = SecurityUtil.getUnionId(); + if (StrUtil.isBlank(unionId)) { + throw new IllegalArgumentException("当前登录人未绑定分工会"); + } + List distinctUserIds = normalizeUserIds(userIds); + if (StrUtil.isBlank(settingId) || Lang.isEmpty(distinctUserIds)) { + throw new IllegalArgumentException("请选择疗休养配置和分配人员"); + } + String normalizedPersonType = normalizePersonType(personType); + NutMap matterInfo = prepareMatterInfoForAssignment(settingId, matterId, normalizedPersonType); + TourSetting setting = fetchSettingForAssignment(settingId); + if (setting == null) { + throw new IllegalArgumentException("疗休养配置不存在"); + } + if (StrUtil.isBlank(setting.getActivityGroupId())) { + throw new IllegalArgumentException("疗休养配置未设置可参加人员范围"); + } + NutMap quotaInfo = branchQuotaInfo(settingId); + int remainingQuota = defaultInt(quotaInfo.getInt(remainingQuotaKey(normalizedPersonType))); + if (remainingQuota <= 0) { + throw new IllegalArgumentException("当前分工会该人员类型暂无剩余名额"); + } + + // 保存前重新按“配置人员范围 + 当前分工会 + 未分配”查询,防止前端提交范围外或其它分工会人员。 + Sql userSql = buildCandidateSql(settingId); + Cnd userCnd = buildCandidateCnd(settingId, setting.getActivityGroupId(), unionId, null); + userCnd.and("u.id", "in", distinctUserIds); + userSql.setCondition(userCnd); + List candidateUsers = listMap(userSql); + if (candidateUsers.size() > remainingQuota) { + throw new IllegalArgumentException("选择人数超过当前分工会剩余名额"); + } + List assignments = candidateUsers.stream() + .map(user -> buildAssignment(settingId, matterInfo, normalizedPersonType, user, + TourUserAssignment.ASSIGN_SOURCE_BRANCH_UNION)) + .collect(Collectors.toList()); + if (Lang.isNotEmpty(assignments)) { + dao().insert(assignments); + } + // 事项为空时只写人员分配表;正式人员选择事项时按代报名写入台账。 + int ledgerCount = insertProxySignupLedgers(matterInfo, candidateUsers); + NutMap newQuotaInfo = branchQuotaInfo(settingId); + return NutMap.NEW() + .addv("assignCount", assignments.size()) + .addv("skipCount", distinctUserIds.size() - assignments.size()) + .addv("ledgerCount", ledgerCount) + .addv("formalRemaining", defaultInt(newQuotaInfo.getInt("formalRemaining"))) + .addv("backupRemaining", defaultInt(newQuotaInfo.getInt("backupRemaining"))); + } + + @Override + public NutMap branchQuotaInfo(String settingId) { + String unionId = SecurityUtil.getUnionId(); + if (StrUtil.isBlank(settingId) || StrUtil.isBlank(unionId)) { + return emptyQuotaInfo(); + } + Sql sql = Sqls.create(""" + SELECT + COALESCE(q.formalQuota, 0) AS formalQuota, + COALESCE(q.backupQuota, 0) AS backupQuota, + COALESCE(used_quota.formalUsed, 0) AS formalUsed, + COALESCE(used_quota.backupUsed, 0) AS backupUsed + FROM tour_setting_union_quota q + LEFT JOIN ( + SELECT + settingId, + unionId, + SUM(CASE WHEN personType = @formalType THEN 1 ELSE 0 END) AS formalUsed, + SUM(CASE WHEN personType = @backupType THEN 1 ELSE 0 END) AS backupUsed + FROM tour_user_assignment + WHERE delFlag = 0 + AND assignSource = @assignSource + AND settingId = @settingId + AND unionId = @unionId + AND (cancelled = 0 OR cancelled IS NULL) + GROUP BY settingId, unionId + ) used_quota ON used_quota.settingId = q.settingId AND used_quota.unionId = q.unionId + WHERE q.settingId = @settingId + AND q.unionId = @unionId + LIMIT 1 + """); + sql.setParam("formalType", TourUserAssignment.PERSON_TYPE_FORMAL); + sql.setParam("backupType", TourUserAssignment.PERSON_TYPE_BACKUP); + sql.setParam("assignSource", TourUserAssignment.ASSIGN_SOURCE_BRANCH_UNION); + sql.setParam("settingId", settingId); + sql.setParam("unionId", unionId); + List rows = listMap(sql); + NutMap quotaInfo = Lang.isEmpty(rows) ? emptyQuotaInfo() : rows.get(0); + int formalQuota = defaultInt(quotaInfo.getInt("formalQuota")); + int backupQuota = defaultInt(quotaInfo.getInt("backupQuota")); + int formalUsed = defaultInt(quotaInfo.getInt("formalUsed")); + int backupUsed = defaultInt(quotaInfo.getInt("backupUsed")); + return NutMap.NEW() + .addv("formalQuota", formalQuota) + .addv("backupQuota", backupQuota) + .addv("formalUsed", formalUsed) + .addv("backupUsed", backupUsed) + .addv("formalRemaining", Math.max(formalQuota - formalUsed, 0)) + .addv("backupRemaining", Math.max(backupQuota - backupUsed, 0)); + } + + @Override + public NutMap switchCurrentBranchPersonType(String id, String personType) { + String unionId = SecurityUtil.getUnionId(); + if (StrUtil.isBlank(id) || StrUtil.isBlank(unionId)) { + throw new IllegalArgumentException("参数错误"); + } + String targetPersonType = normalizePersonType(personType); + TourUserAssignment assignment = fetch(Cnd.where(TourUserAssignment::getId, "=", id) + .and(TourUserAssignment::getAssignSource, "=", TourUserAssignment.ASSIGN_SOURCE_BRANCH_UNION) + .and(TourUserAssignment::getUnionId, "=", unionId) + .and(TourUserAssignment::getDelFlag, "=", false)); + if (assignment == null) { + throw new IllegalArgumentException("人员分配记录不存在"); + } + String currentPersonType = normalizePersonType(assignment.getPersonType()); + if (currentPersonType.equals(targetPersonType)) { + return branchTypeSwitchResult(targetPersonType, branchQuotaInfo(assignment.getSettingId())); + } + // 已退出人员保留台账痕迹时允许转为替补;未退出且已生成台账的人员仍禁止降为替补。 + if (TourUserAssignment.PERSON_TYPE_BACKUP.equals(targetPersonType) + && StrUtil.isNotBlank(assignment.getMatterId()) + && !Boolean.TRUE.equals(assignment.getCancelled())) { + throw new IllegalArgumentException("已分配疗休养事项并生成台账,不能切换为替补人员"); + } + NutMap quotaInfo = branchQuotaInfo(assignment.getSettingId()); + int remainingQuota = defaultInt(quotaInfo.getInt(remainingQuotaKey(targetPersonType))); + if (remainingQuota <= 0) { + throw new IllegalArgumentException("当前分工会目标人员类型暂无剩余名额"); + } + // 切换为替补后不允许继续保留报名台账,删除范围限制在当前分配记录对应的事项和工号。 + update(Chain.make("personType", targetPersonType), Cnd.where(TourUserAssignment::getId, "=", id) + .and(TourUserAssignment::getAssignSource, "=", TourUserAssignment.ASSIGN_SOURCE_BRANCH_UNION) + .and(TourUserAssignment::getUnionId, "=", unionId)); + if (TourUserAssignment.PERSON_TYPE_BACKUP.equals(targetPersonType)) { + deleteAssignmentLedgers(assignment); + } + return branchTypeSwitchResult(targetPersonType, branchQuotaInfo(assignment.getSettingId())); + } + + @Override + public int backfillExistingAssignmentAfterSignup(String settingId, String matterId, String userId, String boardingPlace) { + if (StrUtil.isBlank(settingId) || StrUtil.isBlank(matterId) || StrUtil.isBlank(userId)) { + return 0; + } + NutMap matterInfo = fetchMatterInfo(settingId, matterId); + if (matterInfo == null) { + return 0; + } + // 用户报名后只回填已存在的人员分配记录,不新增来源,不修改 assignSource 和人员类型。 + return update(Chain.make("matterId", matterInfo.getString("matterId")) + .add("matterName", matterInfo.getString("matterName")) + .add("lineId", matterInfo.getString("lineId")) + .add("lineName", matterInfo.getString("lineName")) + .add("travelAgencyId", matterInfo.getString("travelAgencyId")) + .add("travelAgencyName", matterInfo.getString("travelAgencyName")) + .add("boardingPlace", StrUtil.blankToDefault(boardingPlace, matterInfo.getString("defaultBoardingPlace"))), + Cnd.where(TourUserAssignment::getSettingId, "=", settingId) + .and(TourUserAssignment::getUserId, "=", userId) + .and(TourUserAssignment::getDelFlag, "=", false)); + } + + @Override + public int clearExistingAssignmentMatterAfterCancel(String settingId, String userId) { + if (StrUtil.isBlank(settingId) || StrUtil.isBlank(userId)) { + return 0; + } + // 取消报名只清空报名回填的事项快照,不删除人员分配记录,也不改变来源和人员类型。 + return update(Chain.make("matterId", null) + .add("matterName", null) + .add("lineId", null) + .add("lineName", null) + .add("travelAgencyId", null) + .add("travelAgencyName", null) + .add("boardingPlace", null), + Cnd.where(TourUserAssignment::getSettingId, "=", settingId) + .and(TourUserAssignment::getUserId, "=", userId) + .and(TourUserAssignment::getDelFlag, "=", false)); + } + + @Override + public NutMap schoolDeleteInfo(String id) { + return assignmentDeleteInfo(fetchAssignmentBySource(id, TourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION)); + } + + @Override + public NutMap branchDeleteInfo(String id) { + return assignmentDeleteInfo(fetchCurrentBranchAssignment(id)); + } + + @Override + public void deleteBySource(String id, String assignSource) { + deleteBySource(id, assignSource, false); + } + + @Override + public void deleteBySource(String id, String assignSource, boolean deleteLedger) { + TourUserAssignment assignment = fetchAssignmentBySource(id, assignSource); + deleteAssignmentWithLedgerOption(assignment, deleteLedger); + } + + @Override + public void deleteCurrentBranchAssignment(String id) { + deleteCurrentBranchAssignment(id, false); + } + + @Override + public void deleteCurrentBranchAssignment(String id, boolean deleteLedger) { + TourUserAssignment assignment = fetchCurrentBranchAssignment(id); + deleteAssignmentWithLedgerOption(assignment, deleteLedger); + } + + @Override + public boolean existsAssignedUser(String settingId, String userId) { + if (StrUtil.isBlank(settingId) || StrUtil.isBlank(userId)) { + return false; + } + return count(Cnd.where(TourUserAssignment::getSettingId, "=", settingId) + .and(TourUserAssignment::getUserId, "=", userId) + .and(TourUserAssignment::getDelFlag, "=", false)) > 0; + } + + @Override + public NutMap checkFormalAssignedUser(String settingId, String userId) { + NutMap result = NutMap.NEW().addv("canApply", false); + if (StrUtil.isBlank(settingId) || StrUtil.isBlank(userId)) { + return result.addv("message", "报名资格参数不完整"); + } + List assignments = query(Cnd.where(TourUserAssignment::getSettingId, "=", settingId) + .and(TourUserAssignment::getUserId, "=", userId) + .and(TourUserAssignment::getDelFlag, "=", false)); + if (Lang.isEmpty(assignments)) { + return result.addv("message", "您不在本次疗休养人员分配名单中,无法报名"); + } + boolean hasFormal = assignments.stream() + .anyMatch(item -> TourUserAssignment.PERSON_TYPE_FORMAL.equals(item.getPersonType())); + if (!hasFormal) { + return result.addv("message", "您当前为替补人员,暂不能报名"); + } + boolean hasAvailableFormal = assignments.stream() + .anyMatch(item -> TourUserAssignment.PERSON_TYPE_FORMAL.equals(item.getPersonType()) + && !Boolean.TRUE.equals(item.getCancelled())); + if (!hasAvailableFormal) { + return result.addv("message", "您已取消本次疗休养报名,不能再次报名"); + } + return result.setv("canApply", true).addv("message", ""); + } + + @Override + public int countSchoolFormalAssignedUsers(String settingId) { + if (StrUtil.isBlank(settingId)) { + return 0; + } + return count(Cnd.where(TourUserAssignment::getDelFlag, "=", false) + .and(TourUserAssignment::getSettingId, "=", settingId) + .and(TourUserAssignment::getAssignSource, "=", TourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION) + .and(TourUserAssignment::getPersonType, "=", TourUserAssignment.PERSON_TYPE_FORMAL) + // 已退出的校工会正式人员不再占用分工会名额计算中的校工会分配数量。 + .and(Cnd.exps(TourUserAssignment::getCancelled, "=", false) + .or(TourUserAssignment::getCancelled, "is", null))); + } + + @Override + public void cancelAssignment(String id) { + TourUserAssignment assignment = fetch(Cnd.where(TourUserAssignment::getId, "=", id) + .and(TourUserAssignment::getDelFlag, "=", false)); + if (assignment == null) { + throw new IllegalArgumentException("人员分配记录不存在"); + } + if (Boolean.TRUE.equals(assignment.getCancelled())) { + throw new IllegalArgumentException("该人员已取消退出"); + } + checkAssignmentCancelDeadline(assignment); + // 取消退出时删除对应事项台账,再标记人员分配表状态,防止后续移动端再次报名。 + deleteAssignmentLedgers(assignment); + update(Chain.make("cancelled", true), + Cnd.where(TourUserAssignment::getId, "=", assignment.getId())); + } + + @Override + public void restoreCancelledAssignment(String id) { + TourUserAssignment assignment = fetch(Cnd.where(TourUserAssignment::getId, "=", id) + .and(TourUserAssignment::getDelFlag, "=", false)); + if (assignment == null) { + throw new IllegalArgumentException("人员分配记录不存在"); + } + restoreCancelledAssignment(assignment); + } + + @Override + public void restoreCancelledAssignmentBySource(String id, String assignSource) { + TourUserAssignment assignment = fetchAssignmentBySource(id, assignSource); + if (assignment == null) { + throw new IllegalArgumentException("人员分配记录不存在"); + } + restoreCancelledAssignment(assignment); + } + + @Override + public void restoreCurrentBranchCancelledAssignment(String id) { + TourUserAssignment assignment = fetchCurrentBranchAssignment(id); + if (assignment == null) { + throw new IllegalArgumentException("人员分配记录不存在"); + } + checkRestoreBranchQuota(assignment); + restoreCancelledAssignment(assignment); + } + + @Override + public void clearBySettingId(String settingId) { + if (StrUtil.isNotBlank(settingId)) { + clear(Cnd.where(TourUserAssignment::getSettingId, "=", settingId)); + } + } + + private Pagination assignmentPage(PageForm pageForm, Integer year, String settingId, String matterId, + String unionId, String personType, String keyword, String assignSource) { + Sql sql = Sqls.create(""" + SELECT + a.*, + s.`year` AS `year`, + s.configName AS settingName, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE '' + END AS travelPeriod + FROM tour_user_assignment a + LEFT JOIN tour_setting s ON s.id = a.settingId + LEFT JOIN tour_matter m ON m.id = a.matterId AND m.delFlag = 0 + $condition + """); + Cnd cnd = Cnd.NEW(); + cnd.and("a.delFlag", "=", false); + cnd.and("a.assignSource", "=", assignSource); + cnd.andEX("s.`year`", "=", year); + cnd.andEX("a.settingId", "=", settingId); + cnd.andEX("a.matterId", "=", matterId); + cnd.andEX("a.unionId", "=", unionId); + cnd.andEX("a.personType", "=", personType); + if (StrUtil.isNotBlank(keyword)) { + SqlExpressionGroup group = new SqlExpressionGroup(); + group.orLike("a.userName", keyword); + group.orLike("a.loginName", keyword); + cnd.and(group); + } + appendAssignmentOrder(cnd, pageForm); + sql.setCondition(cnd); + return listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); + } + + private Sql buildCandidateSql(String settingId) { + return Sqls.create(""" + SELECT + u.id AS userId, + u.loginname AS loginName, + u.username AS userName, + u.sex AS gender, + u.idcard AS idCard, + u.mobile AS mobile, + u.unitid AS unitId, + u.unitname AS unitName, + u.unionid AS unionId, + u.unionname AS unionName + FROM activity_user_scope aus + LEFT JOIN vw_user u ON u.id = aus.userId + LEFT JOIN tour_user_assignment a + ON a.settingId = @settingId + AND a.userId = aus.userId + AND a.delFlag = 0 + $condition + """).setParam("settingId", settingId); + } + + private Cnd buildCandidateCnd(String settingId, String activityGroupId, String unionId, String keyword) { + Cnd cnd = Cnd.NEW(); + cnd.and("aus.groupId", "=", activityGroupId); + cnd.and("u.id", "is not", null); + cnd.and("a.id", "is", null); + cnd.andEX("u.unionid", "=", unionId); + if (StrUtil.isNotBlank(keyword)) { + SqlExpressionGroup group = new SqlExpressionGroup(); + group.orLike("u.username", keyword); + group.orLike("u.loginname", keyword); + cnd.and(group); + } + return cnd; + } + + private NutMap assignmentDeleteInfo(TourUserAssignment assignment) { + int ledgerCount = countAssignmentLedgers(assignment); + return NutMap.NEW() + .addv("hasLedger", ledgerCount > 0) + .addv("ledgerCount", ledgerCount); + } + + private TourUserAssignment fetchAssignmentBySource(String id, String assignSource) { + if (StrUtil.isBlank(id) || StrUtil.isBlank(assignSource)) { + return null; + } + return fetch(Cnd.where(TourUserAssignment::getId, "=", id) + .and(TourUserAssignment::getAssignSource, "=", assignSource) + .and(TourUserAssignment::getDelFlag, "=", false)); + } + + private TourUserAssignment fetchCurrentBranchAssignment(String id) { + String unionId = SecurityUtil.getUnionId(); + if (StrUtil.isBlank(id) || StrUtil.isBlank(unionId)) { + return null; + } + return fetch(Cnd.where(TourUserAssignment::getId, "=", id) + .and(TourUserAssignment::getAssignSource, "=", TourUserAssignment.ASSIGN_SOURCE_BRANCH_UNION) + .and(TourUserAssignment::getUnionId, "=", unionId) + .and(TourUserAssignment::getDelFlag, "=", false)); + } + + private void deleteAssignmentWithLedgerOption(TourUserAssignment assignment, boolean deleteLedger) { + if (assignment == null) { + return; + } + int ledgerCount = countAssignmentLedgers(assignment); + if (ledgerCount > 0 && !deleteLedger) { + throw new IllegalArgumentException("该人员已报名,请确认是否同步删除台账数据"); + } + if (ledgerCount > 0) { + // 台账删除范围严格限制在当前分配记录对应的事项和工号,避免影响同人其它疗休养报名。 + deleteAssignmentLedgers(assignment); + } + clear(Cnd.where(TourUserAssignment::getId, "=", assignment.getId()) + .and(TourUserAssignment::getAssignSource, "=", assignment.getAssignSource())); + } + + private int countAssignmentLedgers(TourUserAssignment assignment) { + if (assignment == null || StrUtil.isBlank(assignment.getMatterId()) || StrUtil.isBlank(assignment.getLoginName())) { + return 0; + } + return dao().count(TourLedger.class, Cnd.where(TourLedger::getMatterId, "=", assignment.getMatterId()) + .and(TourLedger::getJobNo, "=", assignment.getLoginName()) + .and(TourLedger::getDelFlag, "=", false)); + } + + private void deleteAssignmentLedgers(TourUserAssignment assignment) { + List ledgers = dao().query(TourLedger.class, Cnd.where(TourLedger::getMatterId, "=", assignment.getMatterId()) + .and(TourLedger::getJobNo, "=", assignment.getLoginName()) + .and(TourLedger::getDelFlag, "=", false)); + if (Lang.isEmpty(ledgers)) { + return; + } + List ledgerIds = ledgers.stream() + .map(TourLedger::getId) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toList()); + if (Lang.isEmpty(ledgerIds)) { + return; + } + dao().clear(TourLedgerFamily.class, Cnd.where(TourLedgerFamily::getLedgerId, "in", ledgerIds)); + dao().clear(TourLedgerDirectRelative.class, Cnd.where(TourLedgerDirectRelative::getLedgerId, "in", ledgerIds)); + dao().clear(TourLedger.class, Cnd.where(TourLedger::getId, "in", ledgerIds)); + } + + private void restoreCancelledAssignment(TourUserAssignment assignment) { + if (!Boolean.TRUE.equals(assignment.getCancelled())) { + throw new IllegalArgumentException("该人员未取消退出,无需恢复"); + } + // 取消退出仅恢复人员分配状态,台账需由用户重新报名或管理员重新选择分配线路生成。 + update(Chain.make("cancelled", false), + Cnd.where(TourUserAssignment::getId, "=", assignment.getId())); + } + + private void checkRestoreBranchQuota(TourUserAssignment assignment) { + if (!Boolean.TRUE.equals(assignment.getCancelled())) { + return; + } + String personType = normalizePersonType(assignment.getPersonType()); + NutMap quotaInfo = branchQuotaInfo(assignment.getSettingId()); + int remainingQuota = defaultInt(quotaInfo.getInt(remainingQuotaKey(personType))); + // 恢复已退出人员会重新占用当前人员类型名额,因此恢复前必须按正式/替补剩余名额校验。 + if (remainingQuota <= 0) { + throw new IllegalArgumentException("当前分工会目标人员类型暂无剩余名额"); + } + } + + private void checkAssignmentCancelDeadline(TourUserAssignment assignment) { + if (StrUtil.isBlank(assignment.getMatterId())) { + throw new IllegalArgumentException("该人员未分配路线,无法取消退出"); + } + TourMatter matter = dao().fetch(TourMatter.class, Cnd.where(TourMatter::getId, "=", assignment.getMatterId()) + .and(TourMatter::getDelFlag, "=", false)); + if (matter == null) { + throw new IllegalArgumentException("分配路线不存在,无法取消退出"); + } + LocalDateTime travelStartTime = parseTravelStartTime(matter.getTravelStartTime()); + int days = getCancelDeadlineDays(); + LocalDateTime deadline = travelStartTime.minusDays(days); + if (LocalDateTime.now().isAfter(deadline)) { + throw new IllegalArgumentException("当前路线已超过取消时间,需在出行开始前" + days + "天取消"); + } + } + + private int getCancelDeadlineDays() { + try { + Sys_config config = sysConfigService == null ? null : sysConfigService.getValueByKey(TOUR_CANCEL_DEADLINE_DAYS_KEY); + String value = config == null ? "" : config.getConfigValue(); + int days = Integer.parseInt(StrUtil.blankToDefault(value, "15")); + return days < 0 ? 15 : days; + } catch (Exception e) { + return 15; + } + } + + private LocalDateTime parseTravelStartTime(String travelStartTime) { + if (StrUtil.isBlank(travelStartTime)) { + throw new IllegalArgumentException("分配路线未设置出行开始时间,无法取消退出"); + } + String normalizedTime = travelStartTime.trim(); + try { + if (normalizedTime.length() <= 10) { + return LocalDate.parse(normalizedTime.substring(0, 10), DateTimeFormatter.ofPattern("yyyy-MM-dd")).atStartOfDay(); + } + return LocalDateTime.parse(normalizedTime.substring(0, 19), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("分配路线出行开始时间格式不正确,无法取消退出"); + } + } + + private TourSetting fetchSettingForAssignment(String settingId) { + if (StrUtil.isBlank(settingId)) { + return null; + } + return dao().fetch(TourSetting.class, Cnd.where(TourSetting::getId, "=", settingId) + .and(TourSetting::getDelFlag, "=", false)); + } + + private NutMap fetchMatterInfo(String settingId, String matterId) { + Sql sql = Sqls.create(""" + SELECT + m.id AS matterId, + m.matterName, + m.`year` AS `year`, + m.settingId, + m.defaultBoardingPlace, + m.lineId, + l.lineName, + l.lineType, + l.travelAgencyId, + ta.agencyName AS travelAgencyName + FROM tour_matter m + LEFT JOIN tour_line l ON l.id = m.lineId + LEFT JOIN tour_travel_agency ta ON ta.id = l.travelAgencyId + $condition + """); + Cnd cnd = Cnd.NEW(); + cnd.and("m.delFlag", "=", false); + cnd.and("m.enabled", "=", true); + cnd.and("m.id", "=", matterId); + cnd.and("m.settingId", "=", settingId); + cnd.and("l.id", "is not", null); + cnd.and("l.delFlag", "=", false); + cnd.and("l.enabled", "=", true); + sql.setCondition(cnd); + List list = listMap(sql); + return Lang.isEmpty(list) ? null : list.get(0); + } + + private TourUserAssignment buildSchoolAssignment(String settingId, NutMap matterInfo, String personType, NutMap user) { + return buildAssignment(settingId, matterInfo, personType, user, TourUserAssignment.ASSIGN_SOURCE_SCHOOL_UNION); + } + + private TourUserAssignment buildAssignment(String settingId, NutMap matterInfo, String personType, NutMap user, String assignSource) { + TourUserAssignment assignment = new TourUserAssignment(); + assignment.setSettingId(settingId); + fillMatterSnapshot(assignment, matterInfo); + assignment.setUserId(user.getString("userId")); + assignment.setLoginName(user.getString("loginName")); + assignment.setUserName(user.getString("userName")); + assignment.setGender(user.getString("gender")); + assignment.setMobile(user.getString("mobile")); + assignment.setUnitId(user.getString("unitId")); + assignment.setUnitName(user.getString("unitName")); + assignment.setUnionId(user.getString("unionId")); + assignment.setUnionName(user.getString("unionName")); + assignment.setAssignSource(assignSource); + assignment.setPersonType(personType); + assignment.setAssignedBy(SecurityUtil.getUserId()); + assignment.setAssignedName(SecurityUtil.getUserUsername()); + assignment.setAssignedAt(DateUtil.now()); + return assignment; + } + + private void fillMatterSnapshot(TourUserAssignment assignment, NutMap matterInfo) { + if (matterInfo == null) { + return; + } + assignment.setMatterId(matterInfo.getString("matterId")); + assignment.setMatterName(matterInfo.getString("matterName")); + assignment.setLineId(matterInfo.getString("lineId")); + assignment.setLineName(matterInfo.getString("lineName")); + assignment.setTravelAgencyId(matterInfo.getString("travelAgencyId")); + assignment.setTravelAgencyName(matterInfo.getString("travelAgencyName")); + assignment.setBoardingPlace(matterInfo.getString("defaultBoardingPlace")); + } + + private NutMap prepareMatterInfoForAssignment(String settingId, String matterId, String personType) { + if (TourUserAssignment.PERSON_TYPE_BACKUP.equals(personType) && StrUtil.isNotBlank(matterId)) { + throw new IllegalArgumentException("替补人员不能分配疗休养事项"); + } + if (StrUtil.isBlank(matterId)) { + return null; + } + NutMap matterInfo = fetchMatterInfo(settingId, matterId); + if (matterInfo == null) { + throw new IllegalArgumentException("疗休养事项不存在或未配置线路"); + } + return matterInfo; + } + + private int insertProxySignupLedgers(NutMap matterInfo, List users) { + if (matterInfo == null || Lang.isEmpty(users)) { + return 0; + } + List ledgers = users.stream() + // 已存在同事项台账时不重复插入,避免覆盖用户自主报名或历史代报名记录。 + .filter(user -> !existsMatterLedger(matterInfo.getString("matterId"), user.getString("loginName"))) + .map(user -> buildProxySignupLedger(matterInfo, user)) + .collect(Collectors.toList()); + if (Lang.isEmpty(ledgers)) { + return 0; + } + dao().insert(ledgers); + return ledgers.size(); + } + + private boolean existsMatterLedger(String matterId, String loginName) { + if (StrUtil.isBlank(matterId) || StrUtil.isBlank(loginName)) { + return false; + } + return dao().count(TourLedger.class, Cnd.where(TourLedger::getMatterId, "=", matterId) + .and(TourLedger::getJobNo, "=", loginName) + .and(TourLedger::getDelFlag, "=", false)) > 0; + } + + private NutMap fetchAssignmentUserSnapshot(TourUserAssignment assignment) { + Sql sql = Sqls.create(""" + SELECT + u.id AS userId, + u.loginname AS loginName, + u.username AS userName, + u.sex AS gender, + u.idcard AS idCard, + u.unitid AS unitId, + u.unitname AS unitName, + u.unionid AS unionId, + u.unionname AS unionName + FROM vw_user u + $condition + """); + Cnd cnd = Cnd.NEW(); + cnd.andEX("u.id", "=", assignment.getUserId()); + cnd.andEX("u.loginname", "=", assignment.getLoginName()); + sql.setCondition(cnd); + List users = listMap(sql); + if (Lang.isNotEmpty(users)) { + return users.get(0); + } + // 视图中找不到人员时使用分配表快照兜底,保证补选事项不会因非关键字段缺失中断。 + return NutMap.NEW() + .addv("userId", assignment.getUserId()) + .addv("loginName", assignment.getLoginName()) + .addv("userName", assignment.getUserName()) + .addv("gender", assignment.getGender()) + .addv("idCard", "") + .addv("unitId", assignment.getUnitId()) + .addv("unitName", assignment.getUnitName()) + .addv("unionId", assignment.getUnionId()) + .addv("unionName", assignment.getUnionName()); + } + + private TourLedger buildProxySignupLedger(NutMap matterInfo, NutMap user) { + TourLedger ledger = new TourLedger(); + ledger.setYear(matterInfo.getInt("year")); + ledger.setJobNo(user.getString("loginName")); + ledger.setUserName(user.getString("userName")); + ledger.setGender(user.getString("gender")); + ledger.setIdCard(user.getString("idCard")); + ledger.setUnitId(user.getString("unitId")); + ledger.setUnitName(user.getString("unitName")); + ledger.setUnionId(user.getString("unionId")); + ledger.setUnionName(user.getString("unionName")); + ledger.setSignupTime(DateUtil.now()); + ledger.setMatterId(matterInfo.getString("matterId")); + ledger.setLineId(matterInfo.getString("lineId")); + ledger.setLineName(matterInfo.getString("lineName")); + ledger.setLineType(matterInfo.getString("lineType")); + ledger.setTravelAgencyId(matterInfo.getString("travelAgencyId")); + ledger.setTravelAgencyName(matterInfo.getString("travelAgencyName")); + ledger.setBoardingPlace(matterInfo.getString("defaultBoardingPlace")); + ledger.setHasFamily(false); + ledger.setJoined(false); + ledger.setReimbursed(false); + ledger.setOverCostReimbursed(false); + return ledger; + } + + private NutMap branchTypeSwitchResult(String personType, NutMap quotaInfo) { + return NutMap.NEW() + .addv("personType", personType) + .addv("formalRemaining", defaultInt(quotaInfo.getInt("formalRemaining"))) + .addv("backupRemaining", defaultInt(quotaInfo.getInt("backupRemaining"))); + } + + private void appendAssignmentOrder(Cnd cnd, PageForm pageForm) { + String orderName = pageForm == null ? "" : pageForm.getPageOrderName(); + String orderBy = pageForm != null && "ascending".equals(pageForm.getPageOrderBy()) ? "asc" : "desc"; + if ("year".equals(orderName)) { + cnd.orderBy("s.`year`", orderBy); + } else if ("settingName".equals(orderName)) { + cnd.orderBy("s.configName", orderBy); + } else if ("matterName".equals(orderName)) { + cnd.orderBy("a.matterName", orderBy); + } else if ("unionName".equals(orderName)) { + cnd.orderBy("a.unionName", orderBy); + } else if ("userName".equals(orderName)) { + cnd.orderBy("a.userName", orderBy); + } else if ("lineName".equals(orderName)) { + cnd.orderBy("a.lineName", orderBy); + } else if ("personType".equals(orderName)) { + cnd.orderBy("a.personType", orderBy); + } else if ("assignedAt".equals(orderName)) { + cnd.orderBy("a.assignedAt", orderBy); + } + cnd.desc("a.assignedAt"); + cnd.desc("a.createdAt"); + } + + private String normalizePersonType(String personType) { + if (TourUserAssignment.PERSON_TYPE_BACKUP.equals(personType)) { + return TourUserAssignment.PERSON_TYPE_BACKUP; + } + return TourUserAssignment.PERSON_TYPE_FORMAL; + } + + private List normalizeUserIds(List userIds) { + if (Lang.isEmpty(userIds)) { + return Collections.emptyList(); + } + return userIds.stream() + .filter(StrUtil::isNotBlank) + .distinct() + .collect(Collectors.toList()); + } + + private Pagination emptyPagination(PageForm pageForm) { + int pageNumber = pageForm == null || pageForm.getPageNumber() == null ? 1 : pageForm.getPageNumber(); + int pageSize = pageForm == null || pageForm.getPageSize() == null ? 10 : pageForm.getPageSize(); + return new Pagination<>(pageNumber, pageSize, 0, Collections.emptyList()); + } + + private NutMap emptyQuotaInfo() { + return NutMap.NEW() + .addv("formalQuota", 0) + .addv("backupQuota", 0) + .addv("formalUsed", 0) + .addv("backupUsed", 0) + .addv("formalRemaining", 0) + .addv("backupRemaining", 0); + } + + private String remainingQuotaKey(String personType) { + return TourUserAssignment.PERSON_TYPE_BACKUP.equals(personType) ? "backupRemaining" : "formalRemaining"; + } + + private int defaultInt(Integer value) { + return value == null || value < 0 ? 0 : value; + } +} diff --git a/src/main/resources/db/tour_boarding_place_fields.sql b/src/main/resources/db/tour_boarding_place_fields.sql new file mode 100644 index 00000000..7e6a039d --- /dev/null +++ b/src/main/resources/db/tour_boarding_place_fields.sql @@ -0,0 +1,13 @@ +-- 疗休养乘车地点字段升级脚本。 +-- 配置表保存乘车地点列表 JSON;事项、人员分配、台账保存最终/默认乘车地点。 +ALTER TABLE `tour_setting` + MODIFY COLUMN `boardingPlace` text COMMENT '乘车地点列表JSON'; + +ALTER TABLE `tour_matter` + ADD COLUMN `defaultBoardingPlace` varchar(100) DEFAULT NULL COMMENT '默认乘车地点' AFTER `lineId`; + +ALTER TABLE `tour_user_assignment` + ADD COLUMN `boardingPlace` varchar(100) DEFAULT NULL COMMENT '乘车地点' AFTER `lineName`; + +ALTER TABLE `tour_ledger` + ADD COLUMN `boardingPlace` varchar(100) DEFAULT NULL COMMENT '乘车地点' AFTER `travelAgencyName`; diff --git a/src/main/resources/db/tour_leave_apply_create.sql b/src/main/resources/db/tour_leave_apply_create.sql new file mode 100644 index 00000000..3cc1b54a --- /dev/null +++ b/src/main/resources/db/tour_leave_apply_create.sql @@ -0,0 +1,29 @@ +-- 疗休养退出申请表。 +CREATE TABLE IF NOT EXISTS `tour_leave_apply` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `matterId` varchar(32) DEFAULT NULL COMMENT '疗休养事项ID', + `lineId` varchar(32) DEFAULT NULL COMMENT '线路ID', + `jobNo` varchar(50) DEFAULT NULL COMMENT '工号', + `userName` varchar(100) DEFAULT NULL COMMENT '姓名', + `userId` varchar(32) DEFAULT NULL COMMENT '用户ID', + `unionId` varchar(32) DEFAULT NULL COMMENT '所属工会ID', + `unionName` varchar(100) DEFAULT NULL COMMENT '所属工会', + `reason` text COMMENT '不能参加原因', + `status` varchar(30) DEFAULT NULL COMMENT '状态', + `auditBy` varchar(32) DEFAULT NULL COMMENT '审核人ID', + `auditName` varchar(100) DEFAULT NULL COMMENT '审核人姓名', + `auditTime` varchar(30) DEFAULT NULL COMMENT '审核时间', + `auditRemark` text 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`), + KEY `idx_tour_leave_apply_user` (`userId`), + KEY `idx_tour_leave_apply_job_no` (`jobNo`), + KEY `idx_tour_leave_apply_union` (`unionId`), + KEY `idx_tour_leave_apply_status` (`status`), + KEY `idx_tour_leave_apply_matter` (`matterId`), + KEY `idx_tour_leave_apply_line` (`lineId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疗休养退出申请'; diff --git a/src/main/resources/db/tour_matter_create.sql b/src/main/resources/db/tour_matter_create.sql index 7a883bfe..ae202a18 100644 --- a/src/main/resources/db/tour_matter_create.sql +++ b/src/main/resources/db/tour_matter_create.sql @@ -9,6 +9,17 @@ CREATE TABLE IF NOT EXISTS `tour_matter` ( `unionId` varchar(32) DEFAULT NULL COMMENT '所属工会ID', `organizationType` varchar(100) DEFAULT NULL COMMENT '组织形式', `mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图', + `lineId` varchar(32) DEFAULT NULL COMMENT '线路ID', + `defaultBoardingPlace` varchar(100) DEFAULT NULL COMMENT '默认乘车地点', + `signupStartTime` varchar(20) DEFAULT NULL COMMENT '报名开始时间', + `signupEndTime` varchar(20) DEFAULT NULL COMMENT '报名结束时间', + `travelStartTime` varchar(20) DEFAULT NULL COMMENT '出行开始时间', + `travelEndTime` varchar(20) DEFAULT NULL COMMENT '出行结束时间', + `contactName` varchar(30) DEFAULT NULL COMMENT '联系人', + `contactPhone` varchar(30) DEFAULT NULL COMMENT '联系方式', + `minGroupPeople` int DEFAULT NULL COMMENT '最少成团人数', + `maxGroupPeople` int DEFAULT NULL COMMENT '最多成团人数', + `estimatedCost` decimal(10,2) DEFAULT NULL COMMENT '预计费用', `enabled` tinyint(1) DEFAULT 1 COMMENT '事项状态', `createdBy` varchar(32) DEFAULT NULL COMMENT '创建人', `createdAt` bigint DEFAULT NULL COMMENT '创建时间', @@ -21,5 +32,6 @@ CREATE TABLE IF NOT EXISTS `tour_matter` ( KEY `idx_tour_matter_creator` (`creatorUserId`), KEY `idx_tour_matter_setting` (`settingId`), KEY `idx_tour_matter_union` (`unionId`), - KEY `idx_tour_matter_org_type` (`organizationType`) + KEY `idx_tour_matter_org_type` (`organizationType`), + KEY `idx_tour_matter_line` (`lineId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项'; diff --git a/src/main/resources/db/tour_setting_add_boarding_place_quota.sql b/src/main/resources/db/tour_setting_add_boarding_place_quota.sql new file mode 100644 index 00000000..8a9a082d --- /dev/null +++ b/src/main/resources/db/tour_setting_add_boarding_place_quota.sql @@ -0,0 +1,4 @@ +-- 疗休养配置新增乘车地点、出行人数指标。 +ALTER TABLE `tour_setting` + ADD COLUMN `boardingPlace` text COMMENT '乘车地点列表JSON' AFTER `tourType`, + ADD COLUMN `travelPeopleQuota` int DEFAULT 0 COMMENT '出行人数指标' AFTER `boardingPlace`; diff --git a/src/main/resources/db/tour_setting_add_signup_eligibility_mode.sql b/src/main/resources/db/tour_setting_add_signup_eligibility_mode.sql new file mode 100644 index 00000000..8d4ff879 --- /dev/null +++ b/src/main/resources/db/tour_setting_add_signup_eligibility_mode.sql @@ -0,0 +1,2 @@ +ALTER TABLE `tour_setting` + ADD COLUMN `signupEligibilityMode` varchar(30) DEFAULT 'ASSIGNED_USER' COMMENT '报名资格校验方式' AFTER `activityGroupId`; diff --git a/src/main/resources/db/tour_setting_create.sql b/src/main/resources/db/tour_setting_create.sql index 4dd97880..05caa494 100644 --- a/src/main/resources/db/tour_setting_create.sql +++ b/src/main/resources/db/tour_setting_create.sql @@ -5,7 +5,10 @@ CREATE TABLE IF NOT EXISTS `tour_setting` ( `year` int DEFAULT NULL COMMENT '年度', `configName` varchar(100) DEFAULT NULL COMMENT '疗休养配置名称', `tourType` varchar(50) DEFAULT NULL COMMENT '疗休养类型', + `boardingPlace` text COMMENT '乘车地点列表JSON', + `travelPeopleQuota` int DEFAULT 0 COMMENT '出行人数指标', `activityGroupId` varchar(32) DEFAULT NULL COMMENT '可参加人员范围ID', + `signupEligibilityMode` varchar(30) DEFAULT 'ASSIGNED_USER' COMMENT '报名资格校验方式', `sortNo` int DEFAULT NULL COMMENT '排序编号', `minGroupPeople` int DEFAULT NULL COMMENT '最少成团人数', `maxGroupPeople` int DEFAULT NULL COMMENT '最多成团人数', @@ -51,6 +54,65 @@ 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 '分工会名称', + `formalQuota` int DEFAULT 0 COMMENT '正式人员名额', + `backupQuota` 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`), + 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_user_assignment` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `settingId` varchar(32) DEFAULT NULL COMMENT '疗休养配置ID', + `matterId` varchar(32) DEFAULT NULL COMMENT '疗休养事项ID', + `matterName` varchar(100) DEFAULT NULL COMMENT '疗休养事项名称', + `travelAgencyId` varchar(32) DEFAULT NULL COMMENT '旅行社ID', + `travelAgencyName` varchar(100) DEFAULT NULL COMMENT '旅行社名称', + `lineId` varchar(32) DEFAULT NULL COMMENT '线路ID', + `lineName` varchar(100) DEFAULT NULL COMMENT '线路名称', + `boardingPlace` varchar(100) DEFAULT NULL COMMENT '乘车地点', + `userId` varchar(32) DEFAULT NULL COMMENT '人员ID', + `loginName` varchar(50) DEFAULT NULL COMMENT '工号', + `userName` varchar(100) DEFAULT NULL COMMENT '姓名', + `gender` varchar(20) DEFAULT NULL COMMENT '性别', + `mobile` varchar(30) DEFAULT NULL COMMENT '手机号', + `unitId` varchar(32) DEFAULT NULL COMMENT '所在单位ID', + `unitName` varchar(100) DEFAULT NULL COMMENT '所在单位', + `unionId` varchar(32) DEFAULT NULL COMMENT '所在分工会ID', + `unionName` varchar(100) DEFAULT NULL COMMENT '所在分工会', + `assignSource` varchar(30) DEFAULT NULL COMMENT '分配来源', + `personType` varchar(30) DEFAULT NULL COMMENT '人员类型', + `cancelled` tinyint(1) DEFAULT 0 COMMENT '是否已退出', + `assignedBy` varchar(32) DEFAULT NULL COMMENT '分配人ID', + `assignedName` varchar(100) DEFAULT NULL COMMENT '分配人姓名', + `assignedAt` varchar(30) DEFAULT NULL 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_user_assignment_setting_user` (`settingId`, `userId`), + KEY `idx_tour_user_assignment_setting` (`settingId`), + KEY `idx_tour_user_assignment_user` (`userId`), + KEY `idx_tour_user_assignment_union` (`unionId`), + KEY `idx_tour_user_assignment_source` (`assignSource`), + KEY `idx_tour_user_assignment_person_type` (`personType`), + KEY `idx_tour_user_assignment_cancelled` (`cancelled`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疗休养人员分配'; + -- 旅行社管理表。 CREATE TABLE IF NOT EXISTS `tour_travel_agency` ( `id` varchar(32) NOT NULL COMMENT 'ID', @@ -118,6 +180,17 @@ CREATE TABLE IF NOT EXISTS `tour_matter` ( `unionId` varchar(32) DEFAULT NULL COMMENT '所属工会ID', `organizationType` varchar(100) DEFAULT NULL COMMENT '组织形式', `mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图', + `lineId` varchar(32) DEFAULT NULL COMMENT '线路ID', + `defaultBoardingPlace` varchar(100) DEFAULT NULL COMMENT '默认乘车地点', + `signupStartTime` varchar(20) DEFAULT NULL COMMENT '报名开始时间', + `signupEndTime` varchar(20) DEFAULT NULL COMMENT '报名结束时间', + `travelStartTime` varchar(20) DEFAULT NULL COMMENT '出行开始时间', + `travelEndTime` varchar(20) DEFAULT NULL COMMENT '出行结束时间', + `contactName` varchar(30) DEFAULT NULL COMMENT '联系人', + `contactPhone` varchar(30) DEFAULT NULL COMMENT '联系方式', + `minGroupPeople` int DEFAULT NULL COMMENT '最少成团人数', + `maxGroupPeople` int DEFAULT NULL COMMENT '最多成团人数', + `estimatedCost` decimal(10,2) DEFAULT NULL COMMENT '预计费用', `enabled` tinyint(1) DEFAULT 1 COMMENT '事项状态', `createdBy` varchar(32) DEFAULT NULL COMMENT '创建人', `createdAt` bigint DEFAULT NULL COMMENT '创建时间', @@ -130,7 +203,8 @@ CREATE TABLE IF NOT EXISTS `tour_matter` ( KEY `idx_tour_matter_creator` (`creatorUserId`), KEY `idx_tour_matter_setting` (`settingId`), KEY `idx_tour_matter_union` (`unionId`), - KEY `idx_tour_matter_org_type` (`organizationType`) + KEY `idx_tour_matter_org_type` (`organizationType`), + KEY `idx_tour_matter_line` (`lineId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项'; -- 疗休养事项批次表。 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..fc8e04bd --- /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 '分工会名称', + `formalQuota` int DEFAULT 0 COMMENT '正式人员名额', + `backupQuota` 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`), + 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/db/tour_user_assignment_add_cancelled.sql b/src/main/resources/db/tour_user_assignment_add_cancelled.sql new file mode 100644 index 00000000..36ecadab --- /dev/null +++ b/src/main/resources/db/tour_user_assignment_add_cancelled.sql @@ -0,0 +1,4 @@ +-- 疗休养人员分配新增退出状态。 +ALTER TABLE `tour_user_assignment` + ADD COLUMN `cancelled` tinyint(1) DEFAULT 0 COMMENT '是否已退出' AFTER `personType`, + ADD KEY `idx_tour_user_assignment_cancelled` (`cancelled`); diff --git a/src/main/resources/db/tour_user_assignment_create.sql b/src/main/resources/db/tour_user_assignment_create.sql new file mode 100644 index 00000000..0beb81ff --- /dev/null +++ b/src/main/resources/db/tour_user_assignment_create.sql @@ -0,0 +1,40 @@ +-- 疗休养人员分配表。 +CREATE TABLE IF NOT EXISTS `tour_user_assignment` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `settingId` varchar(32) DEFAULT NULL COMMENT '疗休养配置ID', + `matterId` varchar(32) DEFAULT NULL COMMENT '疗休养事项ID', + `matterName` varchar(100) DEFAULT NULL COMMENT '疗休养事项名称', + `travelAgencyId` varchar(32) DEFAULT NULL COMMENT '旅行社ID', + `travelAgencyName` varchar(100) DEFAULT NULL COMMENT '旅行社名称', + `lineId` varchar(32) DEFAULT NULL COMMENT '线路ID', + `lineName` varchar(100) DEFAULT NULL COMMENT '线路名称', + `boardingPlace` varchar(100) DEFAULT NULL COMMENT '乘车地点', + `userId` varchar(32) DEFAULT NULL COMMENT '人员ID', + `loginName` varchar(50) DEFAULT NULL COMMENT '工号', + `userName` varchar(100) DEFAULT NULL COMMENT '姓名', + `gender` varchar(20) DEFAULT NULL COMMENT '性别', + `mobile` varchar(30) DEFAULT NULL COMMENT '手机号', + `unitId` varchar(32) DEFAULT NULL COMMENT '所在单位ID', + `unitName` varchar(100) DEFAULT NULL COMMENT '所在单位', + `unionId` varchar(32) DEFAULT NULL COMMENT '所在分工会ID', + `unionName` varchar(100) DEFAULT NULL COMMENT '所在分工会', + `assignSource` varchar(30) DEFAULT NULL COMMENT '分配来源', + `personType` varchar(30) DEFAULT NULL COMMENT '人员类型', + `cancelled` tinyint(1) DEFAULT 0 COMMENT '是否已退出', + `assignedBy` varchar(32) DEFAULT NULL COMMENT '分配人ID', + `assignedName` varchar(100) DEFAULT NULL COMMENT '分配人姓名', + `assignedAt` varchar(30) DEFAULT NULL 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_user_assignment_setting_user` (`settingId`, `userId`), + KEY `idx_tour_user_assignment_setting` (`settingId`), + KEY `idx_tour_user_assignment_user` (`userId`), + KEY `idx_tour_user_assignment_union` (`unionId`), + KEY `idx_tour_user_assignment_source` (`assignSource`), + KEY `idx_tour_user_assignment_person_type` (`personType`), + KEY `idx_tour_user_assignment_cancelled` (`cancelled`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='疗休养人员分配'; diff --git a/src/main/resources/static/components/plugins/sysPageFormSearch/search.vue b/src/main/resources/static/components/plugins/sysPageFormSearch/search.vue index 6eec2130..d99ed158 100644 --- a/src/main/resources/static/components/plugins/sysPageFormSearch/search.vue +++ b/src/main/resources/static/components/plugins/sysPageFormSearch/search.vue @@ -1,7 +1,7 @@