diff --git a/src/main/java/com/budwk/app/base/constant/RoleConstant.java b/src/main/java/com/budwk/app/base/constant/RoleConstant.java index 7318a097..abf4d84b 100644 --- a/src/main/java/com/budwk/app/base/constant/RoleConstant.java +++ b/src/main/java/com/budwk/app/base/constant/RoleConstant.java @@ -50,6 +50,7 @@ RoleConstant { BRANCH_UNION_WENTI_WY("分工会文体委员"), BRANCH_UNION_SHENGGHUO_WY("分工会生活委员"), BRANCH_UNION_TIAOJIE_WY("分工会调解委员"), + BRANCH_UNION_WENTI_SPORTS("分工会文体福利委员"), UNIT_PARTY_SECRETARY("单位党委书记"), diff --git a/src/main/java/com/budwk/app/sys/services/impl/SysDataUnitPullServiceImpl.java b/src/main/java/com/budwk/app/sys/services/impl/SysDataUnitPullServiceImpl.java index 2693aef8..c54f424a 100644 --- a/src/main/java/com/budwk/app/sys/services/impl/SysDataUnitPullServiceImpl.java +++ b/src/main/java/com/budwk/app/sys/services/impl/SysDataUnitPullServiceImpl.java @@ -15,6 +15,7 @@ import com.budwk.app.sys.utils.DataCenterUtil; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; import org.nutz.dao.Dao; import org.nutz.dao.Sqls; import org.nutz.dao.sql.Sql; @@ -84,10 +85,15 @@ public class SysDataUnitPullServiceImpl extends BaseServiceImpl implem // 需要更新的单位 List updateList = new ArrayList<>(); + List sysUnits = dao().query(Sys_unit.class, Cnd.NEW()); + // 拉取过来的数据,全部的单位,一万四千多条,这里只保留 “部门类” List data = jsonBody.getJSONArray("data").stream() .map(o -> (JSONObject) o) - .filter(row -> StrUtil.isNotBlank(row.getStr("bmlb")) && Objects.equals("隶属部门类", row.getStr("bmlb"))) + .filter(row -> + sysUnits.stream().anyMatch(e -> row.getStr("zzjgdm").equals(e.getId())) + || (StrUtil.isNotBlank(row.getStr("bmlb")) && Objects.equals("隶属部门类", row.getStr("bmlb"))) + ) .toList(); Map api2db = Arrays.stream(Api2UnitFiledMap.values()) 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/basic/models/ActivityUserScope.java b/src/main/java/com/budwk/app/zhgh/activity/basic/models/ActivityUserScope.java index e2517cd0..c71ff5bc 100644 --- a/src/main/java/com/budwk/app/zhgh/activity/basic/models/ActivityUserScope.java +++ b/src/main/java/com/budwk/app/zhgh/activity/basic/models/ActivityUserScope.java @@ -20,7 +20,8 @@ import java.io.Serializable; @Comment("活动人员范围设置") @TableIndexes({ @Index(name = "INDEX_ACTIVITY_USER_SCOPE_GROUPID", fields = {"groupId"}, unique = false), - @Index(name = "INDEX_ACTIVITY_USER_SCOPE_USERID", fields = {"userId"}, unique = false) + @Index(name = "INDEX_ACTIVITY_USER_SCOPE_USERID", fields = {"userId"}, unique = false), + @Index(name = "INDEX_ACTIVITY_USER_SCOPE_GROUPID_USERID", fields = {"groupId", "userId"}, unique = false) }) public class ActivityUserScope extends BaseModel implements Serializable { @@ -46,6 +47,16 @@ public class ActivityUserScope extends BaseModel implements Serializable { @Comment("userid") private String userId; + @Column + @ColDefine(type = ColType.INT, width = 2) + @Comment("分组类型 1.结果分组 2.SQL条件分组") + private Integer groupType; + + @Column + @ColDefine(customType = "longtext") + @Comment("SQL条件分组保存的查询SQL") + private String groupSql; + @Column @ColDefine(type = ColType.VARCHAR, width = 32) @Comment("创建人") diff --git a/src/main/java/com/budwk/app/zhgh/activity/basic/service/ActivityBasicScopeService.java b/src/main/java/com/budwk/app/zhgh/activity/basic/service/ActivityBasicScopeService.java index 9e253678..e1c5121a 100644 --- a/src/main/java/com/budwk/app/zhgh/activity/basic/service/ActivityBasicScopeService.java +++ b/src/main/java/com/budwk/app/zhgh/activity/basic/service/ActivityBasicScopeService.java @@ -21,4 +21,49 @@ public interface ActivityBasicScopeService extends BaseService list) ; + /** + * 查询分组基础信息。 + * SQL条件分组与结果分组都只从同一张 activity_user_scope 表读取。 + */ + ActivityUserScope getGroupInfo(Integer groupId); + + /** + * 查询分组类型,未配置时默认按结果分组处理。 + */ + Integer getGroupType(Integer groupId); + + /** + * 构造分组对应的人员ID子查询。 + * 结果分组直接取 activity_user_scope.userId,SQL条件分组则按保存的 groupSql 动态生成查询。 + */ + Sql buildGroupUserIdSubSql(Integer groupId); + + /** + * 构造可直接拼接到业务SQL中的人员ID子查询文本。 + * 仅用于已有大量原生SQL场景,避免每个模块重复拼接 groupSql 逻辑。 + */ + String buildGroupUserIdSubSqlText(Integer groupId); + + /** + * 查询分组当前命中的人员ID列表。 + */ + List listGroupUserIds(Integer groupId); + + /** + * 判断指定用户是否在某个分组内。 + * SQL条件分组会按保存的 groupSql 动态校验,而不是依赖落库的 userId 明细。 + */ + boolean isUserInGroup(Integer groupId, String userId); + + /** + * 批量过滤出当前用户可见的分组ID。 + * 用于列表页先查活动、再按活动分组做二次权限过滤的场景。 + */ + List filterGroupIdsByUser(List groupIds, String userId); + + /** + * SQL条件分组删除人员时,不再删除主表记录,而是把排除条件追加到已保存的 groupSql 中。 + */ + void excludeUsersFromSqlGroup(Integer groupId, List userIds); + } diff --git a/src/main/java/com/budwk/app/zhgh/activity/basic/service/impl/ActivityBasicScopeServiceImpl.java b/src/main/java/com/budwk/app/zhgh/activity/basic/service/impl/ActivityBasicScopeServiceImpl.java index 9a4905b1..945d879e 100644 --- a/src/main/java/com/budwk/app/zhgh/activity/basic/service/impl/ActivityBasicScopeServiceImpl.java +++ b/src/main/java/com/budwk/app/zhgh/activity/basic/service/impl/ActivityBasicScopeServiceImpl.java @@ -1,6 +1,7 @@ package com.budwk.app.zhgh.activity.basic.service.impl; import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.util.StrUtil; import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.utils.DateUtil; import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope; @@ -8,17 +9,20 @@ import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService; import lombok.extern.slf4j.Slf4j; import org.nutz.dao.Cnd; import org.nutz.dao.Dao; +import org.nutz.dao.Sqls; import org.nutz.dao.impl.NutTxDao; import org.nutz.dao.sql.Sql; import org.nutz.ioc.loader.annotation.IocBean; import java.util.ArrayList; +import java.util.LinkedHashSet; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; /** * @author zxy @@ -29,6 +33,7 @@ import java.util.concurrent.atomic.AtomicInteger; @Slf4j public class ActivityBasicScopeServiceImpl extends BaseServiceImpl implements ActivityBasicScopeService { + private static final int GROUP_TYPE_RESULT = 1; public ActivityBasicScopeServiceImpl(Dao dao) { super(dao); @@ -80,4 +85,122 @@ public class ActivityBasicScopeServiceImpl extends BaseServiceImpl list = dao().query(ActivityUserScope.class, Cnd.where("groupId", "=", groupId).desc("id")); + return CollectionUtil.isEmpty(list) ? null : list.get(0); + } + + @Override + public Integer getGroupType(Integer groupId) { + ActivityUserScope userScope = getGroupInfo(groupId); + if (userScope == null || userScope.getGroupType() == null) { + return GROUP_TYPE_RESULT; + } + return userScope.getGroupType(); + } + + @Override + public Sql buildGroupUserIdSubSql(Integer groupId) { + return Sqls.create(buildGroupUserIdSubSqlText(groupId)); + } + + @Override + public String buildGroupUserIdSubSqlText(Integer groupId) { + ActivityUserScope groupInfo = getGroupInfo(groupId); + if (groupInfo == null) { + return "SELECT NULL AS userId WHERE 1 = 0"; + } + if (GROUP_TYPE_RESULT == getGroupType(groupId)) { + return "SELECT userId FROM activity_user_scope WHERE groupId = " + groupId + " AND userId IS NOT NULL"; + } + if (StrUtil.isBlank(groupInfo.getGroupSql())) { + return "SELECT NULL AS userId WHERE 1 = 0"; + } + return """ + SELECT DISTINCT + u.id AS userId + FROM + `vw_user` u + LEFT JOIN sys_user_role sur ON sur.userid = u.id + LEFT JOIN club_user clubuser ON clubuser.userid = u.id + WHERE + """ + groupInfo.getGroupSql(); + } + + @Override + public List listGroupUserIds(Integer groupId) { + Sql sql = Sqls.queryString(buildGroupUserIdSubSqlText(groupId)); + dao().execute(sql); + String[] userIds = (String[]) sql.getResult(); + return userIds == null ? new ArrayList<>() : List.of(userIds); + } + + @Override + public boolean isUserInGroup(Integer groupId, String userId) { + if (groupId == null || StrUtil.isBlank(userId)) { + return false; + } + if (GROUP_TYPE_RESULT == getGroupType(groupId)) { + return dao().count(ActivityUserScope.class, Cnd.where("groupId", "=", groupId).and("userId", "=", userId)) > 0; + } + ActivityUserScope groupInfo = getGroupInfo(groupId); + if (groupInfo == null || StrUtil.isBlank(groupInfo.getGroupSql())) { + return false; + } + Sql sql = Sqls.create(""" + SELECT + COUNT(1) + FROM + `vw_user` u + LEFT JOIN sys_user_role sur ON sur.userid = u.id + LEFT JOIN club_user clubuser ON clubuser.userid = u.id + WHERE + u.id = @userId + AND + """ + "(" + groupInfo.getGroupSql() + ")"); + sql.setParam("userId", userId); + return count(sql) > 0; + } + + @Override + public List filterGroupIdsByUser(List groupIds, String userId) { + LinkedHashSet result = new LinkedHashSet<>(); + if (CollectionUtil.isEmpty(groupIds) || StrUtil.isBlank(userId)) { + return new ArrayList<>(result); + } + groupIds.stream() + .filter(groupId -> groupId != null) + .distinct() + .forEach(groupId -> { + if (isUserInGroup(groupId, userId)) { + result.add(groupId); + } + }); + return new ArrayList<>(result); + } + + @Override + public void excludeUsersFromSqlGroup(Integer groupId, List userIds) { + ActivityUserScope groupInfo = getGroupInfo(groupId); + if (groupInfo == null || getGroupType(groupId) != 2 || CollectionUtil.isEmpty(userIds) || StrUtil.isBlank(groupInfo.getGroupSql())) { + return; + } + String excludeUserSql = userIds.stream() + .filter(StrUtil::isNotBlank) + .distinct() + .map(userId -> "'" + StrUtil.replace(userId, "'", "''") + "'") + .collect(Collectors.joining(",")); + if (StrUtil.isBlank(excludeUserSql)) { + return; + } + // SQL分组只有一条主记录,删除人员时通过追加排除条件持久化删除结果。 + String groupSql = "(" + groupInfo.getGroupSql() + ") AND u.id NOT IN (" + excludeUserSql + ")"; + groupInfo.setGroupSql(groupSql); + dao().updateIgnoreNull(groupInfo); + } } 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 new file mode 100644 index 00000000..9c3c3ab9 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourGroupController.java @@ -0,0 +1,670 @@ +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.constant.RoleConstant; +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.result.Result; +import com.budwk.app.base.utils.CommonDownloadUtil; +import com.budwk.app.sys.models.Sys_union; +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.service.TourLedgerService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.CellRangeAddress; +import org.nutz.dao.Cnd; +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; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +import javax.servlet.http.HttpServletResponse; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@IocBean +@Ok("json:full") +@At("/platform/tour/group") +public class TourGroupController { + + @Inject + private TourMatterService tourMatterService; + + @Inject + private TourLedgerService tourLedgerService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/group/index.html") + @SaCheckPermission("tour.group") + public void index() { + } + + @At + @SaCheckPermission("tour.group") + public Result pageData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId) { + if (StrUtil.isBlank(pageForm.getPageOrderName())) { + pageForm.defaultSortAsc("lineName"); + } + Cnd cnd = buildQueryCnd(year, lineName, lineType, unionId); + + Sql countSql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_union u ON u.id = m.unionId + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourMatterService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + m.id AS matterId, + m.`year`, + m.matterName, + m.unionId, + COALESCE(u.name, '校工会') AS unionName, + m.lineId, + l.lineName, + l.lineType, + l.directFamilyUnitLine, + m.travelStartTime, + m.travelEndTime, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE '' + END AS travelPeriod, + m.minGroupPeople, + m.maxGroupPeople, + IFNULL(sc.signupCount, 0) AS signupCount, + CASE + WHEN m.maxGroupPeople IS NOT NULL AND m.maxGroupPeople > 0 AND IFNULL(sc.signupCount, 0) > m.maxGroupPeople THEN 'over' + WHEN m.minGroupPeople IS NOT NULL AND m.minGroupPeople > 0 AND IFNULL(sc.signupCount, 0) >= m.minGroupPeople THEN 'formed' + ELSE 'unformed' + END AS groupStatus, + CASE + WHEN m.maxGroupPeople IS NOT NULL AND m.maxGroupPeople > 0 AND IFNULL(sc.signupCount, 0) > m.maxGroupPeople THEN '超员' + WHEN m.minGroupPeople IS NOT NULL AND m.minGroupPeople > 0 AND IFNULL(sc.signupCount, 0) >= m.minGroupPeople THEN '已成团' + ELSE '未成团' + END AS groupStatusName + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_union u ON u.id = m.unionId + LEFT JOIN ( + SELECT t.matterId, SUM(1 + IFNULL(f.familyCount, 0)) AS signupCount + FROM tour_ledger t + LEFT JOIN ( + SELECT ledgerId, COUNT(1) AS familyCount + FROM tour_ledger_family + WHERE delFlag = 0 + GROUP BY ledgerId + ) f ON f.ledgerId = t.id + WHERE t.delFlag = 0 + AND t.matterId IS NOT NULL + AND t.matterId <> '' + GROUP BY t.matterId + ) sc ON sc.matterId = m.id + $condition + ORDER BY $orderColumn $orderBy, m.`year` DESC, m.travelStartTime ASC, l.lineName ASC, m.createdAt DESC + """); + listSql.setCondition(cnd); + listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc"); + listSql.setPager(tourMatterService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourMatterService.dao().execute(listSql); + + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class)); + return Result.success(pagination); + } + + @At + @SaCheckPermission("tour.group") + public Result unionOptions() { + Cnd cnd = Cnd.NEW(); + cnd.asc("unionCode"); + cnd.asc("name"); + return Result.success(tourMatterService.dao().query(Sys_union.class, cnd)); + } + + @At + @SaCheckPermission("tour.group") + public Result lineTypeOptions(Integer year) { + Cnd cnd = buildQueryCnd(year, null, null, null); + Sql sql = Sqls.create(""" + SELECT DISTINCT l.lineType + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_union u ON u.id = m.unionId + $condition + ORDER BY l.lineType ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourMatterService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.group") + public Result signupPageData(PageForm pageForm, String matterId, String keyword, String unionId) { + if (StrUtil.isBlank(matterId)) { + return Result.error("参数错误"); + } + if (StrUtil.isBlank(pageForm.getPageOrderName())) { + pageForm.defaultSortDesc("signupTime"); + } + Cnd cnd = buildSignupQueryCnd(matterId, keyword, unionId); + Sql countSql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger t + INNER JOIN tour_matter m ON m.id = t.matterId + INNER JOIN tour_line l ON l.id = m.lineId + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + t.id, + t.jobNo, + t.userName, + '' AS mobile, + t.idCard, + t.unionId, + t.unionName, + t.signupTime + FROM tour_ledger t + INNER JOIN tour_matter m ON m.id = t.matterId + INNER JOIN tour_line l ON l.id = m.lineId + $condition + ORDER BY $orderColumn $orderBy, t.signupTime DESC, t.createdAt DESC + """); + listSql.setCondition(cnd); + listSql.setVar("orderColumn", getSignupOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "ascending".equals(pageForm.getPageOrderBy()) ? "asc" : "desc"); + listSql.setPager(tourLedgerService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(listSql); + + List list = listSql.getList(NutMap.class); + fillSignupMobile(list); + fillSignupFamilies(list); + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list); + return Result.success(pagination); + } + + @At + @Ok("void") + @SaCheckPermission("tour.group") + public void exportParticipants(String matterId, HttpServletResponse response) { + if (StrUtil.isBlank(matterId)) { + return; + } + NutMap matter = fetchExportMatter(matterId); + if (matter == null || matter.isEmpty()) { + return; + } + List list = queryExportParticipants(matterId); + Workbook workbook = buildParticipantsWorkbook(matter, list); + String lineName = StrUtil.blankToDefault(matter.getString("lineName", ""), "线路"); + CommonDownloadUtil.download(lineName + "参加人员名单.xls", workbook, response); + } + + private Cnd buildSignupQueryCnd(String matterId, String keyword, String unionId) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.and("t.matterId", "=", matterId); + cnd.and("m.delFlag", "=", false); + cnd.and("m.enabled", "=", true); + cnd.and("m.lineId", "IS NOT", null); + cnd.and("m.lineId", "<>", ""); + cnd.and("l.delFlag", "=", false); + cnd.and("l.enabled", "=", true); + if (StrUtil.isNotBlank(keyword)) { + SqlExpressionGroup keywordGroup = new SqlExpressionGroup(); + keywordGroup.orLike("t.userName", keyword.trim()); + keywordGroup.orLike("t.jobNo", keyword.trim()); + cnd.and(keywordGroup); + } + cnd.andEX("t.unionId", "=", unionId); + applyMatterDataScope(cnd); + return cnd; + } + + private NutMap fetchExportMatter(String matterId) { + Cnd cnd = Cnd.NEW(); + cnd.and("m.delFlag", "=", false); + cnd.and("m.enabled", "=", true); + cnd.and("m.id", "=", matterId); + cnd.and("m.lineId", "IS NOT", null); + cnd.and("m.lineId", "<>", ""); + cnd.and("l.delFlag", "=", false); + cnd.and("l.enabled", "=", true); + applyMatterDataScope(cnd); + + Sql sql = Sqls.create(""" + SELECT + m.id AS matterId, + m.`year`, + m.unionId, + COALESCE(u.name, '校工会') AS unionName, + l.lineName, + l.lineType, + m.travelStartTime, + m.travelEndTime, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE '' + END AS travelPeriod + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_union u ON u.id = m.unionId + $condition + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.map()); + tourMatterService.dao().execute(sql); + return sql.getObject(NutMap.class); + } + + private List queryExportParticipants(String matterId) { + Cnd cnd = buildSignupQueryCnd(matterId, null, null); + Sql sql = Sqls.create(""" + SELECT + t.id AS ledgerId, + t.jobNo, + t.userName, + t.idCard, + vu.mobile, + t.unionName, + l.lineName, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE '' + END AS travelPeriod, + t.signupTime + FROM tour_ledger t + INNER JOIN tour_matter m ON m.id = t.matterId + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN vw_user vu ON vu.loginname = t.jobNo + $condition + ORDER BY t.signupTime DESC, t.createdAt DESC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + List list = sql.getList(NutMap.class); + fillExportFamilies(list); + return list; + } + + private void fillExportFamilies(List list) { + if (list == null || list.isEmpty()) { + return; + } + List ledgerIds = new ArrayList<>(); + for (NutMap item : list) { + String ledgerId = item.getString("ledgerId", ""); + if (StrUtil.isNotBlank(ledgerId)) { + ledgerIds.add(ledgerId); + } + } + if (ledgerIds.isEmpty()) { + return; + } + Sql sql = Sqls.create(""" + SELECT + ledgerId, + familyName, + relationship, + idCard + 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("ledgerId", ""), List.of())); + } + } + + /** + * 给报名人员列表挂载家属子列表;当前家属台账未保存手机号,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("参加人员名单"); + sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 10)); + sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, 10)); + + double[] widths = {6, 14, 12, 22, 16, 24, 34, 26, 12, 12, 16}; + for (int i = 0; i < widths.length; i++) { + sheet.setColumnWidth(i, (int) (widths[i] * 256)); + } + + CellStyle titleStyle = createStyle(workbook, "黑体", (short) 16, true, HorizontalAlignment.CENTER, false, false); + CellStyle unionStyle = createStyle(workbook, "宋体", (short) 12, true, HorizontalAlignment.LEFT, false, false); + CellStyle headerStyle = createStyle(workbook, "宋体", (short) 11, true, HorizontalAlignment.CENTER, true, true); + CellStyle bodyStyle = createStyle(workbook, "宋体", (short) 11, false, HorizontalAlignment.CENTER, true, true); + + Row titleRow = sheet.createRow(0); + titleRow.setHeightInPoints(30); + setCell(titleRow, 0, StrUtil.blankToDefault(matter.getString("lineName", ""), ""), titleStyle); + fillMergedCells(titleRow, 1, 10, titleStyle); + + Row unionRow = sheet.createRow(1); + unionRow.setHeightInPoints(26); + setCell(unionRow, 0, "分工会:" + StrUtil.blankToDefault(matter.getString("unionName", ""), ""), unionStyle); + fillMergedCells(unionRow, 1, 10, unionStyle); + + String[] headers = {"序号", "工号", "姓名", "身份证号码", "电话号码", "所属分工会", "所选线路名称", "疗休养时间", "亲属关系", "姓名", "备注"}; + Row headerRow = sheet.createRow(2); + headerRow.setHeightInPoints(35); + for (int i = 0; i < headers.length; i++) { + setCell(headerRow, i, headers[i], headerStyle); + } + + int rowIndex = 3; + int seq = 1; + for (NutMap item : list == null ? List.of() : list) { + Row staffRow = sheet.createRow(rowIndex++); + staffRow.setHeightInPoints(24); + setParticipantRow(staffRow, seq++, item.getString("jobNo", ""), item.getString("userName", ""), + item.getString("idCard", ""), item.getString("mobile", ""), item.getString("unionName", ""), + item.getString("lineName", matter.getString("lineName", "")), item.getString("travelPeriod", matter.getString("travelPeriod", "")), + "", item.getString("userName", ""), bodyStyle); + + Object familiesObj = item.get("families"); + if (familiesObj instanceof List families) { + for (Object obj : families) { + if (!(obj instanceof NutMap family)) { + continue; + } + Row familyRow = sheet.createRow(rowIndex++); + familyRow.setHeightInPoints(24); + setParticipantRow(familyRow, seq++, "", family.getString("familyName", ""), + family.getString("idCard", ""), "", "", + item.getString("lineName", matter.getString("lineName", "")), + item.getString("travelPeriod", matter.getString("travelPeriod", "")), + family.getString("relationship", ""), item.getString("userName", ""), bodyStyle); + } + } + } + + int minRows = Math.max(rowIndex, 23); + while (rowIndex < minRows) { + Row row = sheet.createRow(rowIndex++); + row.setHeightInPoints(24); + for (int i = 0; i < headers.length; i++) { + setCell(row, i, "", bodyStyle); + } + } + return workbook; + } + + private void setParticipantRow(Row row, int seq, String jobNo, String userName, String idCard, String mobile, + String unionName, String lineName, String travelPeriod, String relationship, + String participantName, CellStyle style) { + setCell(row, 0, String.valueOf(seq), style); + setCell(row, 1, jobNo, style); + setCell(row, 2, userName, style); + setCell(row, 3, idCard, style); + setCell(row, 4, mobile, style); + setCell(row, 5, unionName, style); + setCell(row, 6, lineName, style); + setCell(row, 7, travelPeriod, style); + setCell(row, 8, relationship, style); + setCell(row, 9, participantName, style); + setCell(row, 10, "", style); + } + + private CellStyle createStyle(Workbook workbook, String fontName, short fontSize, boolean bold, HorizontalAlignment alignment, boolean wrap, boolean border) { + Font font = workbook.createFont(); + font.setFontName(fontName); + font.setFontHeightInPoints(fontSize); + font.setBold(bold); + + CellStyle style = workbook.createCellStyle(); + style.setFont(font); + style.setAlignment(alignment); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setWrapText(wrap); + if (border) { + style.setBorderLeft(BorderStyle.THIN); + style.setBorderTop(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + } + return style; + } + + private void setCell(Row row, int col, String value, CellStyle style) { + Cell cell = row.createCell(col); + cell.setCellValue(StrUtil.blankToDefault(value, "")); + cell.setCellStyle(style); + } + + private void fillMergedCells(Row row, int startCol, int endCol, CellStyle style) { + for (int i = startCol; i <= endCol; i++) { + setCell(row, i, "", style); + } + } + + private void fillSignupMobile(List list) { + if (list == null || list.isEmpty()) { + return; + } + List jobNos = new ArrayList<>(); + for (NutMap item : list) { + String jobNo = item.getString("jobNo", ""); + if (StrUtil.isNotBlank(jobNo) && !jobNos.contains(jobNo)) { + jobNos.add(jobNo); + } + } + if (jobNos.isEmpty()) { + return; + } + + Sql sql = Sqls.create("SELECT loginname, mobile FROM vw_user WHERE loginname IN (@jobNos)"); + sql.setParam("jobNos", jobNos.toArray(new String[0])); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + + Map mobileMap = new HashMap<>(); + for (NutMap user : sql.getList(NutMap.class)) { + mobileMap.put(user.getString("loginname", ""), user.getString("mobile", "")); + } + for (NutMap item : list) { + item.put("mobile", mobileMap.getOrDefault(item.getString("jobNo", ""), "")); + } + } + + private Cnd buildQueryCnd(Integer year, String lineName, String lineType, String unionId) { + Cnd cnd = Cnd.NEW(); + cnd.and("m.delFlag", "=", false); + cnd.and("m.enabled", "=", true); + cnd.and("m.lineId", "IS NOT", null); + cnd.and("m.lineId", "<>", ""); + cnd.and("l.delFlag", "=", false); + cnd.and("l.enabled", "=", true); + cnd.andEX("m.`year`", "=", year == null ? LocalDate.now().getYear() : year); + cnd.and(Cnd.likeEX("l.lineName", lineName)); + cnd.andEX("l.lineType", "=", lineType); + cnd.andEX("m.unionId", "=", unionId); + applyMatterDataScope(cnd); + return cnd; + } + + /** + * 线路成团按事项归属控制数据范围:校工会查看全部;分工会查看本工会事项;其它角色仅查看自己创建的事项。 + */ + private void applyMatterDataScope(Cnd cnd) { + if (hasSchoolUnionScope()) { + return; + } + if (hasBranchUnionScope()) { + cnd.and("m.unionId", "=", SecurityUtil.getUnionId()); + return; + } + SqlExpressionGroup creatorGroup = new SqlExpressionGroup(); + creatorGroup.or("m.creatorUserId", "=", SecurityUtil.getUserId()); + creatorGroup.or("m.createdBy", "=", SecurityUtil.getUserId()); + cnd.and(creatorGroup); + } + + private boolean hasSchoolUnionScope() { + return AuthUtil.hasRole(RoleConstant.SYSADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_CHAIRMAN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_VICE_CHAIRMAN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_WELFARE_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_MEMBER_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_TC_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_WC_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_DC_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_PROPOSAL_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_CLUB_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.SCHOOL_UNION_ACTIVITY_ADMIN.name()); + } + + private boolean hasBranchUnionScope() { + return AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ADMIN.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_VICE_CHAIRMAN.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_OPERATOR.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_GROUP_LEADER.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ARTICLE_WRITER.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_WY.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_ZUZHI_WY.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_XUANCHUAN_WY.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_NVGONG_WY.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_QINGNIAN_WY.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_WENTI_WY.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_SHENGGHUO_WY.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_TIAOJIE_WY.name()) + || AuthUtil.hasRole(RoleConstant.BRANCH_UNION_WENTI_SPORTS.name()); + } + + private String getOrderColumn(String orderName) { + if ("lineName".equals(orderName)) { + return "l.lineName"; + } + if ("travelPeriod".equals(orderName)) { + return "m.travelStartTime"; + } + if ("matterName".equals(orderName)) { + return "m.matterName"; + } + if ("unionName".equals(orderName)) { + return "u.name"; + } + if ("lineType".equals(orderName)) { + return "l.lineType"; + } + if ("signupCount".equals(orderName)) { + return "signupCount"; + } + if ("minGroupPeople".equals(orderName)) { + return "m.minGroupPeople"; + } + if ("maxGroupPeople".equals(orderName)) { + return "m.maxGroupPeople"; + } + if ("groupStatusName".equals(orderName)) { + return "groupStatus"; + } + return "l.lineName"; + } + + private String getSignupOrderColumn(String orderName) { + if ("jobNo".equals(orderName)) { + return "t.jobNo"; + } + if ("userName".equals(orderName)) { + return "t.userName"; + } + if ("idCard".equals(orderName)) { + return "t.idCard"; + } + if ("unionName".equals(orderName)) { + return "t.unionName"; + } + return "t.signupTime"; + } +} 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 new file mode 100644 index 00000000..4a6206c5 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLedgerController.java @@ -0,0 +1,1076 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.afterturn.easypoi.excel.ExcelExportUtil; +import cn.afterturn.easypoi.excel.ExcelImportUtil; +import cn.afterturn.easypoi.excel.entity.ExportParams; +import cn.afterturn.easypoi.excel.entity.ImportParams; +import cn.afterturn.easypoi.excel.entity.enmus.ExcelType; +import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity; +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.annotation.SLog; +import com.budwk.app.base.model.ExcelImportRes; +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.result.Result; +import com.budwk.app.base.utils.CommonDownloadUtil; +import com.budwk.app.flow.enums.ProcessInstanceStateEnum; +import com.budwk.app.sys.models.Sys_union; +import com.budwk.app.sys.views.View_user; +import com.budwk.app.zhgh.dayofficework.tour.mode.TourLedgerImportExcelMode; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLine; +import com.budwk.app.zhgh.dayofficework.tour.models.TourTravelAgency; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.IndexedColors; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.util.CellRangeAddress; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Chain; +import org.nutz.dao.Cnd; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.util.cri.SqlExpressionGroup; +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.util.NutMap; +import org.nutz.mvc.annotation.AdaptBy; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.Param; +import org.nutz.mvc.upload.TempFile; +import org.nutz.mvc.upload.UploadAdaptor; + +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedOutputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +@IocBean +@Ok("json:full") +@At("/platform/tour/ledger") +public class TourLedgerController { + + @Inject + private TourLedgerService tourLedgerService; + + @Inject + private TourLedgerFamilyService tourLedgerFamilyService; + + @Inject + private TourLedgerDirectRelativeService tourLedgerDirectRelativeService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/ledger/index.html") + @SaCheckPermission("tour.ledger") + public void index() { + } + + @At + @SaCheckPermission("tour.ledger") + public Result pageData(PageForm pageForm, 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 countSql = Sqls.create(""" + SELECT COUNT(DISTINCT t.id) + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = 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 + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + t.*, + ins.id AS instanceId, + ins.state AS instanceState, + ins.processDefineId AS instanceProcessDefineId, + 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, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE tp.travelPeriod + END AS travelPeriod + 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 $orderColumn $orderBy, t.`year` DESC, t.signupTime DESC, t.createdAt DESC + """); + listSql.setCondition(cnd); + listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc"); + listSql.setPager(tourLedgerService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(listSql); + + var list = listSql.getList(NutMap.class); + 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); + } + + @At + @SaCheckPermission("tour.ledger") + public Result detail(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + TourLedger ledger = tourLedgerService.fetch(id); + if (ledger == null) { + return Result.error("台账记录不存在"); + } + ledger.setLineName(getCurrentLineName(ledger)); + Cnd familyCnd = Cnd.NEW(); + familyCnd.and(TourLedgerFamily::getLedgerId, "=", id); + familyCnd.and(TourLedgerFamily::getDelFlag, "=", false); + familyCnd.asc(TourLedgerFamily::getCreatedAt); + TourLedgerDirectRelative directRelative = tourLedgerDirectRelativeService.fetch( + Cnd.where(TourLedgerDirectRelative::getDelFlag, "=", false) + .and(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); + return Result.success(NutMap.NEW() + .addv("ledger", ledger) + .addv("travelPeriod", getTravelPeriod(ledger)) + .addv("families", tourLedgerFamilyService.query(familyCnd)) + .addv("directRelative", directRelative) + .addv("fillBedInfo", isFillBedInfo(ledger)) + .addv("directFamilyUnitLine", isDirectFamilyUnitLine(ledger))); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.ledger") + @SLog(type = "tour", tag = "疗休养台账", msg = "删除疗休养台账") + public Result doDelete(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + TourLedger ledger = tourLedgerService.fetch(id); + if (ledger == null) { + return Result.error("台账记录不存在"); + } + tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", id)); + tourLedgerService.delete(id); + return Result.success(); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.ledger") + @SLog(type = "tour", tag = "疗休养台账", msg = "设置参加人员") + public Result setParticipants(@Param("ids") String ids) { + List idList = StrUtil.isBlank(ids) ? Collections.emptyList() : Json.fromJsonAsList(String.class, ids); + if (idList.isEmpty()) { + return Result.error("请选择要设置的参加人员"); + } + tourLedgerService.dao().update(TourLedger.class, Chain.make("joined", true), Cnd.where("id", "in", idList)); + return Result.success(); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.ledger") + @SLog(type = "tour", tag = "疗休养台账", msg = "参加人员导入") + @AdaptBy(type = UploadAdaptor.class, args = {"ioc:fileUpload"}) + public Result importParticipants(@Param("file") TempFile file) { + if (file == null) { + return Result.error("请选择导入文件"); + } + List importRows = ExcelImportUtil.importExcel(file.getFile(), TourLedgerImportExcelMode.class, new ImportParams()); + ExcelImportRes excelImportRes = new ExcelImportRes<>(); + excelImportRes.setTotalRecords(importRows.size()); + + for (int i = 0; i < importRows.size(); i++) { + TourLedgerImportExcelMode row = importRows.get(i); + if (!validateImportRow(row, i)) { + continue; + } + Boolean joined = parseJoined(row.getJoined()); + if (joined == null) { + row.setErrInfo("是否参加只能填写1、0、是、否、true、false、已参加、未参加", i); + continue; + } + TourLine line = fetchImportLine(row); + if (line == null) { + row.setErrInfo("未找到唯一匹配的线路", i); + continue; + } + List ledgers = queryImportLedgerCandidates(row); + if (ledgers.isEmpty()) { + Result createResult = insertImportLedger(row, line, joined); + if (createResult.isSuccess()) { + continue; + } + row.setErrInfo(createResult.getMsg(), i); + continue; + } + TourLedger ledger = resolveImportLedger(row, ledgers, line); + if (ledger == null) { + row.setErrInfo("匹配到多条台账记录,报名时间或线路未能唯一匹配,请核对年度、工号、报名时间、线路名称", i); + continue; + } + Chain chain = Chain.make("joined", joined) + .add("lineId", line.getId()) + .add("lineName", line.getLineName()) + .add("lineType", line.getLineType()); + String signupTime = normalizeSignupTime(row.getSignupTime()); + if (StrUtil.isNotBlank(signupTime)) { + chain.add("signupTime", signupTime); + } + tourLedgerService.dao().update(TourLedger.class, chain, Cnd.where("id", "=", ledger.getId())); + } + + excelImportRes.setErrorDetails(importRows.stream().filter(s -> StrUtil.isNotBlank(s.getErrMsg())).collect(Collectors.toList())); + excelImportRes.setFailedCount(excelImportRes.getErrorDetails().size()); + excelImportRes.setSuccessCount(Math.max(importRows.size() - excelImportRes.getFailedCount(), 0)); + return Result.success(excelImportRes); + } + + @At + @SaCheckPermission("tour.ledger") + @Ok("void") + public void downloadTemplate(HttpServletResponse response) { + List entities = new ArrayList<>(); + entities.add(new ExcelExportEntity("年度", "year", 12)); + entities.add(new ExcelExportEntity("工号", "jobNo", 18)); + entities.add(new ExcelExportEntity("姓名", "userName", 18)); + entities.add(new ExcelExportEntity("报名时间", "signupTime", 24)); + entities.add(new ExcelExportEntity("线路名称", "lineName", 30)); + entities.add(new ExcelExportEntity("线路类型", "lineType", 18)); + entities.add(new ExcelExportEntity("是否参加", "joined", 14)); + + ExportParams exportParams = new ExportParams(); + exportParams.setSheetName("tour_ledger"); + exportParams.setType(ExcelType.XSSF); + Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entities, Collections.emptyList()); + 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") + public void exportUnionSignupZip(Integer startYear, Integer endYear, String keyword, String unionId, String lineId, + String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly, + HttpServletResponse response) throws Exception { + List rows = queryUnionSignupRows(startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, + directFamilyOnly, overCostOnly); + Map> unionRows = groupRowsByUnion(rows); + + String zipName = URLEncoder.encode("疗休养分工会报名汇总表.zip", StandardCharsets.UTF_8.toString()); + response.setContentType("application/zip"); + response.setHeader("Content-Disposition", "attachment;filename=" + zipName + ";filename*=UTF-8''" + zipName); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(new BufferedOutputStream(response.getOutputStream()))) { + if (unionRows.isEmpty()) { + zipOutputStream.putNextEntry(new ZipEntry("无数据报名汇总表.xls")); + try (Workbook workbook = buildUnionSignupWorkbook("无数据", Collections.emptyList())) { + workbook.write(zipOutputStream); + } + zipOutputStream.closeEntry(); + } else { + for (Map.Entry> entry : unionRows.entrySet()) { + String unionName = entry.getValue().isEmpty() ? entry.getKey() : entry.getValue().get(0).getString("unionName", entry.getKey()); + String fileName = safeFileName(unionName + "报名汇总表.xls"); + zipOutputStream.putNextEntry(new ZipEntry(fileName)); + try (Workbook workbook = buildUnionSignupWorkbook(unionName, entry.getValue())) { + workbook.write(zipOutputStream); + } + zipOutputStream.closeEntry(); + } + } + zipOutputStream.flush(); + } + } + + private List queryUnionSignupRows(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, + t.idCard, + t.unionId, + COALESCE(NULLIF(t.unionName, ''), su.name, vu.unionName, '') AS unionName, + COALESCE(NULLIF(su.unionCode, ''), vu.unionCode, '') AS unionCode, + COALESCE(NULLIF(vu.mobile, ''), '') AS mobile, + 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 + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = 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 sys_union su ON su.id = t.unionId + 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 unionCode ASC, unionName ASC, t.signupTime DESC, t.createdAt DESC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return sql.getList(NutMap.class); + } + + private Map> groupRowsByUnion(List rows) { + Map> unionRows = new LinkedHashMap<>(); + for (NutMap row : rows) { + String unionName = StrUtil.blankToDefault(row.getString("unionName"), "未分工会"); + String unionCode = StrUtil.blankToDefault(row.getString("unionCode"), ""); + String key = StrUtil.isBlank(unionCode) ? unionName : unionCode + "_" + unionName; + unionRows.computeIfAbsent(key, item -> new ArrayList<>()).add(row); + } + return unionRows; + } + + private Workbook buildUnionSignupWorkbook(String unionName, List rows) { + HSSFWorkbook workbook = new HSSFWorkbook(); + Sheet sheet = workbook.createSheet("报名汇总表"); + sheet.setColumnWidth(0, 8 * 256); + sheet.setColumnWidth(1, 14 * 256); + sheet.setColumnWidth(2, 14 * 256); + sheet.setColumnWidth(3, 24 * 256); + sheet.setColumnWidth(4, 16 * 256); + sheet.setColumnWidth(5, 24 * 256); + sheet.setColumnWidth(6, 36 * 256); + sheet.setColumnWidth(7, 26 * 256); + sheet.setColumnWidth(8, 18 * 256); + + CellStyle titleStyle = createUnionSignupTitleStyle(workbook); + CellStyle headerStyle = createUnionSignupHeaderStyle(workbook); + CellStyle bodyStyle = createUnionSignupBodyStyle(workbook); + + Row titleRow = sheet.createRow(0); + titleRow.setHeightInPoints(28); + Cell titleCell = titleRow.createCell(0); + titleCell.setCellValue(StrUtil.blankToDefault(unionName, "未分工会") + "报名汇总表"); + titleCell.setCellStyle(titleStyle); + sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 8)); + for (int i = 1; i <= 8; i++) { + titleRow.createCell(i).setCellStyle(titleStyle); + } + + Row headerRow = sheet.createRow(1); + headerRow.setHeightInPoints(24); + String[] headers = {"序号", "工号", "姓名", "身份证号码", "电话号码", "所属分工会", "所选线路名称", "疗休养时间", "备注"}; + for (int i = 0; i < headers.length; i++) { + setCell(headerRow, i, headers[i], headerStyle); + } + + int rowIndex = 2; + int seq = 1; + for (NutMap row : rows) { + Row dataRow = sheet.createRow(rowIndex++); + dataRow.setHeightInPoints(22); + setCell(dataRow, 0, String.valueOf(seq++), bodyStyle); + setCell(dataRow, 1, row.getString("jobNo", ""), bodyStyle); + setCell(dataRow, 2, row.getString("userName", ""), bodyStyle); + setCell(dataRow, 3, row.getString("idCard", ""), bodyStyle); + setCell(dataRow, 4, row.getString("mobile", ""), bodyStyle); + setCell(dataRow, 5, row.getString("unionName", ""), bodyStyle); + setCell(dataRow, 6, row.getString("lineName", ""), bodyStyle); + setCell(dataRow, 7, row.getString("travelPeriod", ""), bodyStyle); + setCell(dataRow, 8, "", bodyStyle); + } + return workbook; + } + + private CellStyle createUnionSignupTitleStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setBorderTop(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + style.setBorderLeft(BorderStyle.THIN); + Font font = workbook.createFont(); + font.setBold(true); + font.setFontHeightInPoints((short) 16); + style.setFont(font); + return style; + } + + private CellStyle createUnionSignupHeaderStyle(Workbook workbook) { + CellStyle style = createUnionSignupBodyStyle(workbook); + style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex()); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + Font font = workbook.createFont(); + font.setBold(true); + style.setFont(font); + return style; + } + + private CellStyle createUnionSignupBodyStyle(Workbook workbook) { + CellStyle style = workbook.createCellStyle(); + style.setAlignment(HorizontalAlignment.CENTER); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setWrapText(true); + style.setBorderTop(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + style.setBorderLeft(BorderStyle.THIN); + return style; + } + + private void setCell(Row row, int index, String value, CellStyle style) { + Cell cell = row.createCell(index); + cell.setCellValue(StrUtil.blankToDefault(value, "")); + cell.setCellStyle(style); + } + + private String safeFileName(String fileName) { + String safeName = StrUtil.blankToDefault(fileName, "报名汇总表.xls") + .replaceAll("[\\\\/:*?\"<>|]", "、"); + return safeName.length() > 120 ? safeName.substring(0, 120) : safeName; + } + + private boolean validateImportRow(TourLedgerImportExcelMode row, int index) { + if (row.getYear() == null) { + row.setErrInfo("年度为空", index); + return false; + } + if (StrUtil.isBlank(row.getJobNo())) { + row.setErrInfo("工号为空", index); + return false; + } + if (StrUtil.isBlank(row.getLineName())) { + row.setErrInfo("线路名称为空", index); + return false; + } + if (StrUtil.isBlank(row.getLineType())) { + row.setErrInfo("线路类型为空", index); + return false; + } + if (StrUtil.isBlank(row.getJoined())) { + row.setErrInfo("是否参加为空", index); + return false; + } + return true; + } + + private TourLine fetchImportLine(TourLedgerImportExcelMode row) { + Cnd cnd = Cnd.where(TourLine::getDelFlag, "=", false) + .and(TourLine::getYear, "=", row.getYear()) + .and(TourLine::getLineName, "=", row.getLineName().trim()) + .and(TourLine::getLineType, "=", row.getLineType().trim()); + List lines = tourLedgerService.dao().query(TourLine.class, cnd); + return lines.size() == 1 ? lines.get(0) : null; + } + + private List queryImportLedgerCandidates(TourLedgerImportExcelMode row) { + Cnd cnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getYear, "=", row.getYear()) + .and(TourLedger::getJobNo, "=", row.getJobNo().trim()); + return tourLedgerService.query(cnd); + } + + private TourLedger resolveImportLedger(TourLedgerImportExcelMode row, List ledgers, TourLine line) { + if (ledgers.size() == 1) { + return ledgers.get(0); + } + List matched = ledgers; + if (row.getSignupTime() != null) { + String importSignupTime = normalizeSignupTime(row.getSignupTime()); + matched = matched.stream() + .filter(item -> signupTimeEquals(importSignupTime, item.getSignupTime())) + .collect(Collectors.toList()); + if (matched.size() == 1) { + return matched.get(0); + } + } + matched = ledgers.stream() + .filter(item -> ledgerLineEquals(item, line)) + .collect(Collectors.toList()); + return matched.size() == 1 ? matched.get(0) : null; + } + + private boolean ledgerLineEquals(TourLedger ledger, TourLine line) { + if (ledger == null || line == null) { + return false; + } + if (StrUtil.isNotBlank(ledger.getLineId()) && ledger.getLineId().equals(line.getId())) { + return true; + } + return StrUtil.equals(ledger.getLineName(), line.getLineName()) + && StrUtil.equals(ledger.getLineType(), line.getLineType()); + } + + private Result insertImportLedger(TourLedgerImportExcelMode row, TourLine line, Boolean joined) { + View_user user = fetchImportUser(row.getJobNo()); + if (user == null) { + return Result.error("系统用户中未找到该工号"); + } + TourLedger ledger = new TourLedger(); + ledger.setYear(row.getYear()); + ledger.setJobNo(StrUtil.blankToDefault(user.getLoginname(), row.getJobNo().trim())); + ledger.setUserName(StrUtil.blankToDefault(user.getUsername(), row.getUserName())); + ledger.setGender(user.getSex()); + ledger.setIdCard(user.getIdCard()); + ledger.setUnitId(user.getUnitId()); + ledger.setUnitName(user.getUnitName()); + ledger.setUnionId(user.getUnionId()); + ledger.setUnionName(user.getUnionName()); + ledger.setSignupTime(normalizeSignupTime(row.getSignupTime())); + ledger.setLineId(line.getId()); + ledger.setLineName(line.getLineName()); + ledger.setLineType(line.getLineType()); + ledger.setTravelAgencyId(line.getTravelAgencyId()); + ledger.setTravelAgencyName(getTravelAgencyName(line.getTravelAgencyId())); + ledger.setHasFamily(false); + ledger.setJoined(joined); + ledger.setReimbursed(false); + ledger.setOverCostReimbursed(false); + tourLedgerService.insert(ledger); + return Result.success(); + } + + private View_user fetchImportUser(String jobNo) { + if (StrUtil.isBlank(jobNo)) { + return null; + } + return tourLedgerService.dao().fetch(View_user.class, Cnd.where(View_user::getLoginname, "=", jobNo.trim())); + } + + private String getTravelAgencyName(String travelAgencyId) { + if (StrUtil.isBlank(travelAgencyId)) { + return ""; + } + TourTravelAgency agency = tourLedgerService.dao().fetch(TourTravelAgency.class, travelAgencyId); + return agency == null ? "" : StrUtil.blankToDefault(agency.getAgencyName(), ""); + } + + private boolean signupTimeEquals(String importSignupTime, String ledgerSignupTime) { + if (StrUtil.isBlank(importSignupTime) || StrUtil.isBlank(ledgerSignupTime)) { + return false; + } + String ledgerTime = normalizeSignupTime(ledgerSignupTime); + if (importSignupTime.equals(ledgerTime)) { + return true; + } + return importSignupTime.length() == 16 && ledgerTime.startsWith(importSignupTime) + || ledgerTime.length() == 16 && importSignupTime.startsWith(ledgerTime); + } + + private String normalizeSignupTime(Object value) { + if (value instanceof Date date) { + return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date); + } + String text = StrUtil.trimToEmpty(value == null ? "" : String.valueOf(value)).replace('T', ' '); + if (StrUtil.isBlank(text)) { + return ""; + } + text = text.replaceAll("\\.0$", ""); + String[] dateTimePatterns = { + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd HH:mm", + "yyyy/M/d H:mm:ss", + "yyyy/M/d H:mm", + "M/d/yy H:mm:ss", + "M/d/yy H:mm", + "M/d/yyyy H:mm:ss", + "M/d/yyyy H:mm" + }; + for (String pattern : dateTimePatterns) { + try { + LocalDateTime dateTime = LocalDateTime.parse(text, DateTimeFormatter.ofPattern(pattern)); + return dateTime.format(DateTimeFormatter.ofPattern(pattern.contains(":ss") ? "yyyy-MM-dd HH:mm:ss" : "yyyy-MM-dd HH:mm")); + } catch (DateTimeParseException ignored) { + } + } + String[] datePatterns = {"yyyy-MM-dd", "yyyy/M/d", "M/d/yy", "M/d/yyyy"}; + for (String pattern : datePatterns) { + try { + LocalDate date = LocalDate.parse(text, DateTimeFormatter.ofPattern(pattern)); + return date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")); + } catch (DateTimeParseException ignored) { + } + } + return text; + } + + private Boolean parseJoined(String value) { + String joined = StrUtil.trimToEmpty(value).toLowerCase(); + if ("1".equals(joined) || "1.0".equals(joined) || "是".equals(joined) || "true".equals(joined) || "已参加".equals(joined)) { + return true; + } + if ("0".equals(joined) || "0.0".equals(joined) || "否".equals(joined) || "false".equals(joined) || "未参加".equals(joined)) { + return false; + } + return null; + } + + @At + @SaCheckPermission("tour.ledger") + public Result unionOptions() { + Cnd cnd = Cnd.NEW(); + cnd.asc("unionCode"); + cnd.asc("name"); + return Result.success(tourLedgerService.dao().query(Sys_union.class, cnd)); + } + + @At + @SaCheckPermission("tour.ledger") + public Result lineOptions(Integer startYear, Integer endYear, String travelPeriod, String lineType, String unionId, String keyword, Boolean directFamilyOnly, Boolean overCostOnly) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.and("t.lineName", "IS NOT", null); + cnd.and("t.lineName", "<>", ""); + cnd.andEX("t.unionId", "=", unionId); + cnd.andEX("t.lineType", "=", lineType); + appendTravelPeriodFilter(cnd, travelPeriod); + appendKeywordFilter(cnd, keyword); + appendScopeFilter(cnd, directFamilyOnly, overCostOnly); + appendApprovedWorkflowFilter(cnd); + + Sql sql = Sqls.create(""" + SELECT DISTINCT + CASE + WHEN IFNULL(t.lineId, '') <> '' THEN t.lineId + ELSE CONCAT('legacy:', t.lineName) + END AS lineId, + COALESCE(NULLIF(l.lineName, ''), t.lineName) AS lineName + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = 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 + $condition + ORDER BY lineName ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.ledger") + public Result travelPeriodOptions(Integer startYear, Integer endYear, String lineId, String lineType, String unionId, String keyword, Boolean directFamilyOnly, Boolean overCostOnly) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.and("m.travelStartTime", "IS NOT", null); + cnd.and("m.travelStartTime", "<>", ""); + cnd.and("m.travelEndTime", "IS NOT", null); + cnd.and("m.travelEndTime", "<>", ""); + cnd.andEX("t.unionId", "=", unionId); + cnd.andEX("t.lineType", "=", lineType); + appendLineFilter(cnd, lineId); + appendKeywordFilter(cnd, keyword); + appendScopeFilter(cnd, directFamilyOnly, overCostOnly); + appendApprovedWorkflowFilter(cnd); + + Sql sql = Sqls.create(""" + SELECT DISTINCT CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) AS travelPeriod + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = 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 + $condition + ORDER BY travelPeriod ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + private Cnd buildQueryCnd(Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType, Boolean directFamilyOnly, Boolean overCostOnly) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.andEX("t.unionId", "=", unionId); + cnd.andEX("t.lineType", "=", lineType); + appendLineFilter(cnd, lineId); + appendTravelPeriodFilter(cnd, travelPeriod); + appendKeywordFilter(cnd, keyword); + appendScopeFilter(cnd, directFamilyOnly, overCostOnly); + appendApprovedWorkflowFilter(cnd); + return cnd; + } + + private void appendApprovedWorkflowFilter(Cnd cnd) { + SqlExpressionGroup group = new SqlExpressionGroup(); + group.or("ins.id", "IS", null); + group.or("ins.state", "=", ProcessInstanceStateEnum.FINISHED.getCode()); + cnd.and(group); + } + + private void appendScopeFilter(Cnd cnd, Boolean directFamilyOnly, Boolean overCostOnly) { + if (Boolean.TRUE.equals(directFamilyOnly)) { + cnd.and("IF(IFNULL(l.directFamilyUnitLine, 0) = 1 OR dr.id IS NOT NULL, 1, 0)", "=", 1); + } + if (Boolean.TRUE.equals(overCostOnly)) { + cnd.and("t.overCostReimbursed", "=", true); + } + } + + private void appendKeywordFilter(Cnd cnd, String keyword) { + if (StrUtil.isNotBlank(keyword)) { + SqlExpressionGroup group = new SqlExpressionGroup(); + group.orLike("t.userName", keyword.trim()); + group.orLike("t.jobNo", keyword.trim()); + cnd.and(group); + } + } + + private String getOrderColumn(String orderName) { + if ("year".equals(orderName)) { + return "t.`year`"; + } + if ("jobNo".equals(orderName)) { + return "t.jobNo"; + } + if ("userName".equals(orderName)) { + return "t.userName"; + } + if ("lineName".equals(orderName)) { + return "COALESCE(NULLIF(l.lineName, ''), t.lineName)"; + } + if ("travelPeriod".equals(orderName)) { + return "COALESCE(m.travelStartTime, tp.travelPeriod)"; + } + if ("lineType".equals(orderName)) { + return "t.lineType"; + } + if ("signupTime".equals(orderName)) { + return "t.signupTime"; + } + if ("familyCount".equals(orderName)) { + return "familyCount"; + } + if ("joined".equals(orderName)) { + return "t.joined"; + } + return "t.`year`"; + } + + private String getTravelPeriod(TourLedger ledger) { + if (ledger == null || ledger.getYear() == null || StrUtil.isBlank(ledger.getLineId())) { + return ""; + } + if (StrUtil.isNotBlank(ledger.getMatterId())) { + Sql exactSql = Sqls.create(""" + SELECT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE '' + END AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND id = @matterId + """); + exactSql.setParam("matterId", ledger.getMatterId()); + exactSql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(exactSql); + return exactSql.getString(); + } + Sql sql = Sqls.create(""" + SELECT 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 `year` = @year + AND lineId = @lineId + """); + sql.setParam("year", ledger.getYear()); + sql.setParam("lineId", ledger.getLineId()); + sql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(sql); + return sql.getString(); + } + + private boolean isDirectFamilyUnitLine(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getLineId())) { + return false; + } + Sql sql = Sqls.create(""" + SELECT IFNULL(directFamilyUnitLine, 0) + FROM tour_line + WHERE delFlag = 0 + AND id = @lineId + """); + sql.setParam("lineId", ledger.getLineId()); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt() > 0; + } + + private boolean isFillBedInfo(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getMatterId())) { + return true; + } + Sql sql = Sqls.create(""" + SELECT IFNULL(MAX(s.fillBedInfo), 1) + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_setting s ON s.id = m.settingId AND s.delFlag = 0 + WHERE t.id = @ledgerId + """); + sql.setParam("ledgerId", ledger.getId()); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt() > 0; + } + + private void appendTravelPeriodFilter(Cnd cnd, String travelPeriod) { + if (StrUtil.isBlank(travelPeriod)) { + return; + } + cnd.and("CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)", "=", travelPeriod); + } + + private void appendLineFilter(Cnd cnd, String lineId) { + if (StrUtil.isBlank(lineId)) { + return; + } + if (lineId.startsWith("legacy:")) { + SqlExpressionGroup legacyGroup = new SqlExpressionGroup(); + legacyGroup.or("t.lineId", "IS", null); + legacyGroup.or("t.lineId", "=", ""); + cnd.and(legacyGroup); + cnd.and("t.lineName", "=", lineId.substring("legacy:".length())); + return; + } + cnd.and("t.lineId", "=", lineId); + } + + private String getCurrentLineName(TourLedger ledger) { + if (ledger == null) { + return ""; + } + if (StrUtil.isBlank(ledger.getLineId())) { + return ledger.getLineName(); + } + Sql sql = Sqls.create(""" + SELECT lineName + FROM tour_line + WHERE delFlag = 0 + AND id = @lineId + """); + sql.setParam("lineId", ledger.getLineId()); + sql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(sql); + return StrUtil.blankToDefault(sql.getString(), ledger.getLineName()); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLineController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLineController.java new file mode 100644 index 00000000..b6758ac0 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLineController.java @@ -0,0 +1,297 @@ +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.constant.RoleConstant; +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.result.Result; +import com.budwk.app.sys.models.Sys_dict; +import com.budwk.app.sys.services.SysDictService; +import com.budwk.app.sys.views.View_user; +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.TourLine; +import com.budwk.app.zhgh.dayofficework.tour.models.TourTravelAgency; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLineService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.util.cri.SqlExpressionGroup; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@IocBean +@Ok("json:full") +@At("/platform/tour/route") +public class TourLineController { + + @Inject + private TourLineService tourLineService; + + @Inject + private SysDictService sysDictService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/route/index.html") + @SaCheckPermission("tour.route") + public void index() { + } + + @At + @SaCheckPermission("tour.route") + public Result pageData(PageForm pageForm, Integer year, String lineName, String lotId) { + if (StrUtil.isBlank(pageForm.getPageOrderName())) { + pageForm.defaultSortDesc("enabled"); + } + + Cnd cnd = Cnd.NEW(); + cnd.and("l.delFlag", "=", false); + cnd.andEX("l.`year`", "=", year); + cnd.and(Cnd.likeEX("l.lineName", lineName)); + cnd.andEX("l.lotId", "=", lotId); + boolean sysAdmin = AuthUtil.hasRole(RoleConstant.SYSADMIN.name()); + boolean branchUnionScope = !sysAdmin && AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name()); + applyDataScope(cnd, sysAdmin, branchUnionScope); + String creatorJoin = branchUnionScope ? """ + LEFT JOIN sys_unit lineUnit ON lineUnit.id = l.unitId + LEFT JOIN sys_user creatorUser ON creatorUser.id = COALESCE(NULLIF(l.creatorUserId, ''), l.createdBy) + LEFT JOIN sys_unit creatorUnit ON creatorUnit.id = creatorUser.unitId + """ : ""; + + Sql countSql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_line l + $creatorJoin + $condition + """); + countSql.setVar("creatorJoin", creatorJoin); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourLineService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + l.*, + a.agencyName AS travelAgencyName, + lot.lotName AS lotName + FROM tour_line l + LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId + LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId + $creatorJoin + $condition + ORDER BY $orderColumn $orderBy, l.`year` DESC, l.lineCode ASC, l.createdAt DESC + """); + listSql.setVar("creatorJoin", creatorJoin); + listSql.setCondition(cnd); + listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc"); + listSql.setPager(tourLineService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourLineService.dao().execute(listSql); + + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class)); + return Result.success(pagination); + } + + @At + @SaCheckPermission("tour.route") + public Result detail(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + TourLine line = tourLineService.fetch(id); + return line == null ? Result.error("线路不存在") : Result.success(line); + } + + @At + @SaCheckPermission("tour.route") + public Result travelAgencyOptions(Integer year) { + Cnd cnd = Cnd.where(TourTravelAgency::getEnabled, "=", true); + cnd.andEX(TourTravelAgency::getYear, "=", year); + cnd.asc(TourTravelAgency::getAgencyCode).asc(TourTravelAgency::getAgencyName); + return Result.success(tourLineService.dao().query(TourTravelAgency.class, cnd)); + } + + @At + @SaCheckPermission("tour.route") + public Result lotOptions(Integer year) { + Cnd cnd = Cnd.NEW(); + cnd.andEX("s.`year`", "=", year); + Sql sql = Sqls.create(""" + SELECT lot.id, lot.lotName, lot.lotValue, lot.activityCost + FROM tour_setting_lot lot + INNER JOIN tour_setting s ON s.id = lot.settingId + $condition + ORDER BY lot.lotValue DESC, lot.lotName ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLineService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.route") + public Result lineTypeOptions() { + List list = sysDictService.getSubListByCode("lineType"); + if (list == null) { + return Result.success(Collections.emptyList()); + } + return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList())); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.route") + @SLog(type = "tour", tag = "线路管理", msg = "保存线路信息") + public Result doSubmit(TourLine line) { + Result checkResult = check(line); + if (checkResult != null) { + return checkResult; + } + + Cnd sameCodeCnd = Cnd.where(TourLine::getYear, "=", line.getYear()) + .and(TourLine::getLineCode, "=", line.getLineCode()); + if (StrUtil.isNotBlank(line.getId())) { + sameCodeCnd.and(TourLine::getId, "<>", line.getId()); + } + if (tourLineService.count(sameCodeCnd) > 0) { + return Result.error("同年度下线路编号已存在"); + } + if (line.getEnabled() == null) { + line.setEnabled(true); + } + if (line.getOpenFlag() == null) { + line.setOpenFlag(true); + } + if (line.getDirectFamilyUnitLine() == null) { + line.setDirectFamilyUnitLine(false); + } + if (StrUtil.isBlank(line.getId())) { + fillCreatorInfo(line, null); + tourLineService.insert(line); + } else { + fillCreatorInfo(line, tourLineService.fetch(line.getId())); + tourLineService.updateIgnoreNull(line); + } + return Result.success(); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.route") + @SLog(type = "tour", tag = "线路管理", msg = "删除线路信息") + public Result doDelete(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + tourLineService.delete(id); + return Result.success(); + } + + private Result check(TourLine line) { + if (line == null) { + return Result.error("参数错误"); + } + if (line.getYear() == null) { + return Result.error("创建年度不能为空"); + } + if (StrUtil.isBlank(line.getTravelAgencyId())) { + return Result.error("旅行社名称不能为空"); + } + if (StrUtil.isBlank(line.getLineName())) { + return Result.error("线路名称不能为空"); + } + if (StrUtil.isBlank(line.getLineCode())) { + return Result.error("线路编号不能为空"); + } + if (StrUtil.isBlank(line.getLineType())) { + return Result.error("线路类型不能为空"); + } + if (StrUtil.isBlank(line.getLotId())) { + return Result.error("时间标段不能为空"); + } + if (StrUtil.isBlank(line.getMobileThumb())) { + return Result.error("移动端缩略图不能为空"); + } + return null; + } + + /** + * 创建人和所在单位由当前登录人生成,编辑时保留原创建信息。 + */ + private void fillCreatorInfo(TourLine line, TourLine oldLine) { + if (oldLine != null) { + line.setCreatorUserId(defaultIfBlank(oldLine.getCreatorUserId(), SecurityUtil.getUserId())); + line.setCreatorName(defaultIfBlank(oldLine.getCreatorName(), SecurityUtil.getUserUsername())); + line.setUnitId(defaultIfBlank(oldLine.getUnitId(), SecurityUtil.getUnitId())); + line.setUnitName(defaultIfBlank(oldLine.getUnitName(), getCurrentUnitName())); + return; + } + line.setCreatorUserId(SecurityUtil.getUserId()); + line.setCreatorName(SecurityUtil.getUserUsername()); + line.setUnitId(SecurityUtil.getUnitId()); + line.setUnitName(getCurrentUnitName()); + } + + private String getCurrentUnitName() { + View_user user = tourLineService.dao().fetch(View_user.class, Cnd.where("id", "=", SecurityUtil.getUserId())); + return user == null ? "" : defaultIfBlank(user.getUnitName(), ""); + } + + private String defaultIfBlank(String value, String defaultValue) { + return StrUtil.isBlank(value) ? defaultValue : value; + } + + /** + * 线路数据范围: + * SYSADMIN 可查看全部;分工会主席查看本工会创建的线路;其它角色保持原逻辑,仅查看自己创建的线路。 + */ + private void applyDataScope(Cnd cnd, boolean sysAdmin, boolean branchUnionScope) { + if (sysAdmin) { + return; + } + if (branchUnionScope) { + SqlExpressionGroup unionGroup = new SqlExpressionGroup(); + unionGroup.or("lineUnit.unionId", "=", SecurityUtil.getUnionId()); + unionGroup.or("creatorUnit.unionId", "=", SecurityUtil.getUnionId()); + cnd.and(unionGroup); + return; + } + SqlExpressionGroup creatorGroup = new SqlExpressionGroup(); + creatorGroup.or("l.creatorUserId", "=", SecurityUtil.getUserId()); + creatorGroup.or("l.createdBy", "=", SecurityUtil.getUserId()); + cnd.and(creatorGroup); + } + + private String getOrderColumn(String orderName) { + if ("year".equals(orderName)) { + return "l.`year`"; + } + if ("lineCode".equals(orderName)) { + return "l.lineCode"; + } + if ("lotName".equals(orderName)) { + return "lot.lotName"; + } + if ("lineType".equals(orderName)) { + return "l.lineType"; + } + if ("enabled".equals(orderName)) { + return "l.enabled"; + } + return "l.`year`"; + } +} 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 new file mode 100644 index 00000000..45f227b8 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMatterController.java @@ -0,0 +1,456 @@ +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.constant.RoleConstant; +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.result.Result; +import com.budwk.app.sys.models.Sys_dict; +import com.budwk.app.sys.models.Sys_union; +import com.budwk.app.sys.services.SysDictService; +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.TourLedger; +import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter; +import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting; +import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.util.cri.SqlExpressionGroup; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@IocBean +@Ok("json:full") +@At("/platform/tour/matter") +public class TourMatterController { + + private static final String MOBILE_PATTERN = "^1[3-9]\\d{9}$"; + + @Inject + private TourMatterService tourMatterService; + + @Inject + private SysDictService sysDictService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/matter/index.html") + @SaCheckPermission("tour.matter") + public void index() { + } + + @At + @SaCheckPermission("tour.matter") + public Result pageData(PageForm pageForm, Integer year, String matterName, String unionId, String organizationType) { + Cnd cnd = Cnd.NEW(); + cnd.and("m.delFlag", "=", false); + cnd.andEX("m.`year`", "=", year); + cnd.and(Cnd.likeEX("m.matterName", matterName)); + cnd.andEX("m.unionId", "=", unionId); + cnd.andEX("m.organizationType", "=", organizationType); + applyMatterDataScope(cnd); + + Sql countSql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_matter m + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourMatterService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + m.*, + s.configName AS settingName, + u.name AS unionName, + d.name AS organizationTypeName, + l.lineName AS lineName, + l.lineType AS lineType, + l.directFamilyUnitLine AS directFamilyUnitLine + FROM tour_matter m + LEFT JOIN tour_setting s ON s.id = m.settingId + LEFT JOIN sys_union u ON u.id = m.unionId + LEFT JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_dict d ON d.`code` = m.organizationType + AND d.parentId = (SELECT id FROM sys_dict WHERE `code` = 'organizationType' LIMIT 1) + $condition + ORDER BY $orderColumn $orderBy, m.`year` DESC, m.createdAt DESC + """); + listSql.setCondition(cnd); + listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc"); + listSql.setPager(tourMatterService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourMatterService.dao().execute(listSql); + + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class)); + return Result.success(pagination); + } + + @At + @SaCheckPermission("tour.matter") + public Result detail(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + TourMatter matter = tourMatterService.fetch(id); + return matter == null ? Result.error("事项不存在") : Result.success(matter); + } + + @At + @SaCheckPermission("tour.matter") + public Result settingOptions(Integer year) { + Cnd cnd = Cnd.where(TourSetting::getEnabled, "=", true); + cnd.andEX(TourSetting::getYear, "=", year); + cnd.desc(TourSetting::getCreatedAt); + cnd.desc(TourSetting::getUpdatedAt); + cnd.asc(TourSetting::getConfigName); + return Result.success(tourMatterService.dao().query(TourSetting.class, cnd)); + } + + @At + @SaCheckPermission("tour.matter") + public Result unionOptions() { + Cnd cnd = Cnd.NEW(); + cnd.asc("unionCode"); + cnd.asc("name"); + return Result.success(tourMatterService.dao().query(Sys_union.class, cnd)); + } + + @At + @SaCheckPermission("tour.matter") + public Result lineOptions(Integer year) { + Cnd cnd = Cnd.NEW(); + cnd.and("l.delFlag", "=", false); + cnd.and("l.enabled", "=", true); + cnd.and("l.openFlag", "=", true); + + Sql sql = Sqls.create(""" + SELECT + l.*, + lot.lotName AS lotName, + lot.lotValue AS lotValue, + lot.lotValue AS lotDays, + lot.activityCost AS activityCost + FROM tour_line l + LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId + $condition + ORDER BY l.lineCode ASC, l.lineName ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourMatterService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.matter") + public Result organizationTypeOptions() { + List list = sysDictService.getSubListByCode("organizationType"); + if (list == null) { + return Result.success(Collections.emptyList()); + } + return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList())); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.matter") + @SLog(type = "tour", tag = "疗休养事项", msg = "保存疗休养事项") + public Result doSubmit(TourMatter matter) { + Result checkResult = check(matter); + if (checkResult != null) { + return checkResult; + } + + Cnd sameNameCnd = Cnd.where(TourMatter::getYear, "=", matter.getYear()) + .and(TourMatter::getMatterName, "=", matter.getMatterName()); + if (StrUtil.isNotBlank(matter.getId())) { + sameNameCnd.and(TourMatter::getId, "<>", matter.getId()); + } + if (tourMatterService.count(sameNameCnd) > 0) { + return Result.error("同年度下事项名称已存在"); + } + if (matter.getEnabled() == null) { + matter.setEnabled(true); + } + fillCreatorAndUnion(matter); + if (StrUtil.isBlank(matter.getId())) { + tourMatterService.insert(matter); + } else { + tourMatterService.updateIgnoreNull(matter); + } + return Result.success(); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.matter") + @SLog(type = "tour", tag = "疗休养事项", msg = "删除疗休养事项") + public Result doDelete(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + TourMatter matter = tourMatterService.fetch(id); + if (matter == null) { + return Result.error("事项不存在"); + } + long signupCount = countSignup(id); + if (signupCount > 0) { + return Result.error("当前事项对应线路已有人员报名,不能删除"); + } + tourMatterService.delete(id); + return Result.success(); + } + + @At + @SaCheckPermission("tour.matter") + public Result deleteInfo(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + TourMatter matter = tourMatterService.fetch(id); + if (matter == null) { + return Result.error("事项不存在"); + } + long signupCount = countSignup(id); + return Result.success(NutMap.NEW() + .addv("signupCount", signupCount) + .addv("canDelete", signupCount <= 0) + .addv("message", signupCount > 0 ? "当前事项已有人员报名,不能删除" : "当前事项暂无人员报名,可删除")); + } + + @At + @SaCheckPermission("tour.matter") + public Result lineConfig(String matterId) { + if (StrUtil.isBlank(matterId)) { + return Result.error("参数错误"); + } + TourMatter matter = tourMatterService.fetch(matterId); + return matter == null ? Result.error("事项不存在") : Result.success(matter); + } + + @At + @SaCheckPermission("tour.matter") + public Result settingPeople(String matterId) { + if (StrUtil.isBlank(matterId)) { + return Result.error("参数错误"); + } + TourMatter matter = tourMatterService.fetch(matterId); + if (matter == null || StrUtil.isBlank(matter.getSettingId())) { + return Result.success(NutMap.NEW()); + } + TourSetting setting = tourMatterService.dao().fetch(TourSetting.class, matter.getSettingId()); + if (setting == null) { + return Result.success(NutMap.NEW()); + } + return Result.success(NutMap.NEW() + .addv("minGroupPeople", setting.getMinGroupPeople()) + .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") + @SLog(type = "tour", tag = "疗休养事项", msg = "保存事项线路配置") + public Result lineConfigDoSubmit(TourMatter matter) { + Result checkResult = checkLineConfig(matter); + if (checkResult != null) { + return checkResult; + } + TourMatter oldMatter = tourMatterService.fetch(matter.getId()); + if (oldMatter == null) { + return Result.error("事项不存在"); + } + oldMatter.setLineId(matter.getLineId()); + // 默认乘车地点来自疗休养配置中的乘车地点列表,后续代报名和自主报名会优先带出该值。 + oldMatter.setDefaultBoardingPlace(matter.getDefaultBoardingPlace()); + oldMatter.setSignupStartTime(matter.getSignupStartTime()); + oldMatter.setSignupEndTime(matter.getSignupEndTime()); + oldMatter.setTravelStartTime(matter.getTravelStartTime()); + oldMatter.setTravelEndTime(matter.getTravelEndTime()); + oldMatter.setContactName(matter.getContactName()); + oldMatter.setContactPhone(matter.getContactPhone()); + oldMatter.setMinGroupPeople(matter.getMinGroupPeople()); + oldMatter.setMaxGroupPeople(matter.getMaxGroupPeople()); + oldMatter.setEstimatedCost(matter.getEstimatedCost()); + tourMatterService.update(oldMatter); + return Result.success(); + } + + private Result check(TourMatter matter) { + if (matter == null) { + return Result.error("参数错误"); + } + if (matter.getYear() == null) { + return Result.error("年度不能为空"); + } + if (StrUtil.isBlank(matter.getMatterName())) { + return Result.error("事项名称不能为空"); + } + if (StrUtil.isBlank(matter.getSettingId())) { + return Result.error("疗休养配置不能为空"); + } + if (StrUtil.isBlank(matter.getOrganizationType())) { + return Result.error("组织形式不能为空"); + } + if (!"schoolUnion".equals(matter.getOrganizationType()) && StrUtil.isBlank(matter.getUnionId())) { + return Result.error("所属工会不能为空"); + } + return null; + } + + private Result checkLineConfig(TourMatter matter) { + if (matter == null) { + return Result.error("参数错误"); + } + if (StrUtil.isBlank(matter.getId())) { + return Result.error("事项不能为空"); + } + if (StrUtil.isBlank(matter.getLineId())) { + return Result.error("线路不能为空"); + } + if (StrUtil.isBlank(matter.getSignupStartTime()) || StrUtil.isBlank(matter.getSignupEndTime()) + || StrUtil.isBlank(matter.getTravelStartTime()) || StrUtil.isBlank(matter.getTravelEndTime())) { + return Result.error("时间不能为空"); + } + if (matter.getSignupStartTime().compareTo(matter.getSignupEndTime()) >= 0) { + return Result.error("报名开始时间必须小于报名结束时间"); + } + if (matter.getTravelStartTime().compareTo(matter.getTravelEndTime()) > 0) { + return Result.error("出行开始时间不能晚于出行结束时间"); + } + if (matter.getSignupEndTime().compareTo(matter.getTravelStartTime()) >= 0) { + return Result.error("报名结束时间必须小于出行开始时间"); + } + if (StrUtil.isBlank(matter.getContactName())) { + return Result.error("联系人不能为空"); + } + if (StrUtil.isBlank(matter.getContactPhone()) || !matter.getContactPhone().matches(MOBILE_PATTERN)) { + return Result.error("联系方式格式不正确"); + } + if (matter.getMinGroupPeople() == null || matter.getMinGroupPeople() <= 0) { + return Result.error("最少成团人数必须大于0"); + } + if (matter.getMaxGroupPeople() == null || matter.getMaxGroupPeople() <= 0) { + return Result.error("最多成团人数必须大于0"); + } + if (matter.getMinGroupPeople() > matter.getMaxGroupPeople()) { + return Result.error("最少成团人数不能大于最多成团人数"); + } + if (matter.getEstimatedCost() == null || matter.getEstimatedCost().signum() < 0) { + return Result.error("预计费用不能小于0"); + } + return null; + } + + private void fillCreatorAndUnion(TourMatter matter) { + if (StrUtil.isBlank(matter.getId())) { + matter.setCreatorUserId(SecurityUtil.getUserId()); + matter.setCreatorName(SecurityUtil.getUserUsername()); + } else { + TourMatter oldMatter = tourMatterService.fetch(matter.getId()); + if (oldMatter != null) { + matter.setCreatorUserId(oldMatter.getCreatorUserId()); + matter.setCreatorName(oldMatter.getCreatorName()); + } + } + if ("schoolUnion".equals(matter.getOrganizationType())) { + matter.setUnionId(""); + } + } + + private long countSignup(String matterId) { + return tourMatterService.dao().count(TourLedger.class, Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getMatterId, "=", matterId)); + } + + /** + * 事项数据范围:SYSADMIN 查看全部;分工会主席查看本工会事项;其它角色仅查看自己创建的事项。 + */ + private void applyMatterDataScope(Cnd cnd) { + if (AuthUtil.hasRole(RoleConstant.SYSADMIN.name())) { + return; + } + if (AuthUtil.hasRole(RoleConstant.BRANCH_UNION_CHAIRMAN.name())) { + cnd.and("m.unionId", "=", SecurityUtil.getUnionId()); + return; + } + SqlExpressionGroup creatorGroup = new SqlExpressionGroup(); + creatorGroup.or("m.creatorUserId", "=", SecurityUtil.getUserId()); + creatorGroup.or("m.createdBy", "=", SecurityUtil.getUserId()); + cnd.and(creatorGroup); + } + + /** + * 事项批次选择线路时沿用线路管理的数据范围,避免在事项页面选到权限外线路。 + */ + private void applyLineDataScope(Cnd cnd, boolean sysAdmin, boolean branchUnionScope) { + if (sysAdmin) { + return; + } + if (branchUnionScope) { + SqlExpressionGroup unionGroup = new SqlExpressionGroup(); + unionGroup.or("lineUnit.unionId", "=", SecurityUtil.getUnionId()); + unionGroup.or("creatorUnit.unionId", "=", SecurityUtil.getUnionId()); + cnd.and(unionGroup); + return; + } + SqlExpressionGroup creatorGroup = new SqlExpressionGroup(); + creatorGroup.or("l.creatorUserId", "=", SecurityUtil.getUserId()); + creatorGroup.or("l.createdBy", "=", SecurityUtil.getUserId()); + cnd.and(creatorGroup); + } + + private String getOrderColumn(String orderName) { + if ("year".equals(orderName)) { + return "m.`year`"; + } + if ("matterName".equals(orderName)) { + return "m.matterName"; + } + if ("unionName".equals(orderName)) { + return "u.name"; + } + if ("lineName".equals(orderName)) { + return "l.lineName"; + } + if ("settingName".equals(orderName)) { + return "s.configName"; + } + if ("organizationTypeName".equals(orderName)) { + return "d.location"; + } + if ("enabled".equals(orderName)) { + return "m.enabled"; + } + return "m.`year`"; + } +} 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 new file mode 100644 index 00000000..5d6e5e0b --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMySignupController.java @@ -0,0 +1,1934 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaMode; +import cn.hutool.core.lang.Dict; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.annotation.SLog; +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.result.Result; +import com.budwk.app.base.utils.CommonDownloadUtil; +import com.budwk.app.flow.constant.FlowConst; +import com.budwk.app.flow.engine.FlowEngine; +import com.budwk.app.flow.entity.ProcessInstance; +import com.budwk.app.flow.entity.ProcessTask; +import com.budwk.app.flow.enums.ProcessInstanceStateEnum; +import com.budwk.app.flow.enums.ProcessSubmitTypeEnum; +import com.budwk.app.flow.enums.ProcessTaskStateEnum; +import com.budwk.app.sys.views.View_user; +import com.budwk.app.sys.models.Sys_dict; +import com.budwk.app.sys.services.SysDictService; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLine; +import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter; +import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeService; +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; +import org.nutz.dao.sql.Sql; +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 org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDType0Font; + +import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@IocBean +@Ok("json:full") +@At("/platform/tour/mysignup") +public class TourMySignupController { + + private static final String DIRECT_FAMILY_WORKFLOW_KEY = "LXYBZXQSXL"; + + @Inject + private TourLedgerService tourLedgerService; + + @Inject + private TourLedgerFamilyService tourLedgerFamilyService; + + @Inject + private TourLedgerDirectRelativeService tourLedgerDirectRelativeService; + + @Inject + private TourMatterService tourMatterService; + + @Inject + private TourUserAssignmentService tourUserAssignmentService; + + @Inject + private SysDictService sysDictService; + + @Inject + private ActivityBasicScopeService activityBasicScopeService; + + @Inject + private FlowEngine flowEngine; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/mysignup/index.html") + @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) + public void index() { + } + + @At("/h5") + @Ok("beetl:/platform/zhghh5/dayofficework/tour/mysignup/index.html") + @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) + public void h5() { + } + + @At + @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) + public Result pageData(PageForm pageForm, Integer startYear, Integer endYear, String lineName) { + Cnd cnd = buildQueryCnd(startYear, endYear, lineName); + + Sql countSql = Sqls.create(""" + SELECT COUNT(DISTINCT t.id) + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + LEFT JOIN wf_process_task task ON task.processInstanceId = ins.id AND task.taskState = 10 + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + t.*, + ins.id AS instanceId, + ins.businessNo, + ins.state AS instanceState, + ins.variable AS instanceVariable, + ins.processDefineId AS instanceProcessDefineId, + task.id AS taskId, + task.taskName AS taskKey, + task.displayName AS taskName, + task.taskType, + task.performType AS taskPerformType, + task.taskState, + task.finishTime, + task.taskParentId, + task.variable AS taskVariable, + IF((SELECT taskName FROM wf_process_task tt WHERE tt.id = task.taskParentId) = 'startTask', 1, 0) AS canRevoke, + (SELECT MAX(id) FROM wf_process_task WHERE processInstanceId = ins.id AND taskName = 'startTask' AND taskState IN (10, 20)) AS startTaskId, + IF(IFNULL(l.directFamilyUnitLine, 0) = 1 OR dr.id IS NOT NULL, 1, 0) AS directFamilyUnitLine, + dr.id AS directRelativeId, + IFNULL(GROUP_CONCAT(DISTINCT task.displayName), IF(ins.id IS NULL, '', '结束')) AS curTaskName, + IFNULL(f.familyCount, 0) AS familyCount, + COALESCE(m.travelEndTime, tp.travelEndTime) AS travelEndTime, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE tp.travelPeriod + END AS travelPeriod + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + LEFT JOIN wf_process_task task ON task.processInstanceId = ins.id AND task.taskState = 10 + 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 ( + 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 ( + 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, + MAX(travelEndTime) AS travelEndTime + 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 $orderColumn $orderBy, t.`year` DESC, t.signupTime DESC, t.createdAt DESC + """); + listSql.setCondition(cnd); + listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc"); + listSql.setPager(tourLedgerService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(listSql); + + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class)); + return Result.success(pagination); + } + + @At + @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) + public Result detail(String id) { + TourLedger ledger = fetchOwnLedger(id); + if (ledger == null) { + return Result.error("报名记录不存在"); + } + Cnd familyCnd = Cnd.where(TourLedgerFamily::getDelFlag, "=", false) + .and(TourLedgerFamily::getLedgerId, "=", ledger.getId()); + familyCnd.asc(TourLedgerFamily::getCreatedAt); + TourLedgerDirectRelative directRelative = tourLedgerDirectRelativeService.fetch( + Cnd.where(TourLedgerDirectRelative::getDelFlag, "=", false) + .and(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); + NutMap signupConfig = getSignupConfig(ledger); + return Result.success(NutMap.NEW() + .addv("ledger", ledger) + .addv("travelPeriod", getTravelPeriod(ledger)) + .addv("families", tourLedgerFamilyService.query(familyCnd)) + .addv("directRelative", directRelative) + .addv("allowFamily", signupConfig.getBoolean("allowFamily", false)) + .addv("fillBedInfo", signupConfig.getBoolean("fillBedInfo", true)) + .addv("directFamilyUnitLine", signupConfig.getBoolean("directFamilyUnitLine", false))); + } + + @At + @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) + public Result exportInfo(String id) { + NutMap exportInfo = buildExportInfo(id); + if (exportInfo == null) { + return Result.error("报名记录不存在"); + } + return Result.success(exportInfo); + } + + @At + @Ok("void") + @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) + public void exportPdf(String id, HttpServletResponse response) { + NutMap exportInfo = buildExportInfo(id); + if (exportInfo == null) { + throw new IllegalArgumentException("报名记录不存在"); + } + byte[] pdfBytes = buildExportPdf(exportInfo); + String fileName = exportInfo.getString("userName", "申请人") + "-5天外超出部分疗休养费用由单位承担申请书.pdf"; + CommonDownloadUtil.download(fileName, pdfBytes, response); + } + + @At + @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) + public Result directRelativeExportInfo(String id) { + NutMap exportInfo = buildDirectRelativeExportInfo(id); + if (exportInfo == null) { + return Result.error("直系亲属线路报名记录不存在"); + } + return Result.success(exportInfo); + } + + @At + @Ok("void") + @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) + public void directRelativeExportPdf(String id, HttpServletResponse response) { + NutMap exportInfo = buildDirectRelativeExportInfo(id); + if (exportInfo == null) { + throw new IllegalArgumentException("直系亲属线路报名记录不存在"); + } + byte[] pdfBytes = buildDirectRelativeExportPdf(exportInfo); + String fileName = exportInfo.getString("userName", "申请人") + "-直系亲属单位疗休养报销申请书.pdf"; + CommonDownloadUtil.download(fileName, pdfBytes, response); + } + + @At + @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) + public Result signupDetail(String id) { + TourLedger ledger = fetchOwnLedger(id); + if (ledger == null) { + return Result.error("报名记录不存在"); + } + if (StrUtil.isBlank(ledger.getMatterId())) { + return Result.error("历史报名记录缺少事项信息,不能修改"); + } + Sql sql = Sqls.create(""" + SELECT + m.id AS matterId, + m.`year`, + m.matterName, + m.unionId AS matterUnionId, + m.defaultBoardingPlace, + m.travelStartTime, + m.travelEndTime, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE '' + END AS travelPeriod, + l.id AS lineId, + l.lineName, + l.lineType, + l.directFamilyUnitLine, + l.travelAgencyId, + a.agencyName AS travelAgencyName, + IFNULL(lot.allowOverReimbursement, 0) AS allowOverReimbursement, + s.allowFamily, + 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 + LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId + LEFT JOIN tour_setting s ON s.id = m.settingId + WHERE m.delFlag = 0 + AND m.enabled = 1 + AND m.id = @matterId + """); + sql.setParam("matterId", ledger.getMatterId()); + sql.setCallback(Sqls.callback.map()); + tourMatterService.dao().execute(sql); + NutMap matter = sql.getObject(NutMap.class); + if (matter == null || matter.isEmpty()) { + return Result.error("报名事项不存在或已停用"); + } + if (isTravelEnded(matter.getString("travelEndTime"))) { + return Result.error("线路出行已结束,不能修改"); + } + Cnd familyCnd = Cnd.where(TourLedgerFamily::getDelFlag, "=", false) + .and(TourLedgerFamily::getLedgerId, "=", ledger.getId()); + familyCnd.asc(TourLedgerFamily::getCreatedAt); + TourLedgerDirectRelative directRelative = tourLedgerDirectRelativeService.fetch(Cnd.where(TourLedgerDirectRelative::getDelFlag, "=", false) + .and(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); + return Result.success(NutMap.NEW() + .addv("matter", matter) + .addv("ledger", ledger) + .addv("families", tourLedgerFamilyService.query(familyCnd)) + .addv("directRelative", directRelative)); + } + + @At + @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) + public Result directRelativeOptions() { + List list = sysDictService.getSubListByCode("directRelative"); + if (list == null) { + return Result.success(Collections.emptyList()); + } + return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList())); + } + + @At + @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) + public Result outProvinceQuotaNotice(String matterId) { + if (StrUtil.isBlank(matterId)) { + return Result.error("参数错误"); + } + TourMatter matter = tourMatterService.fetch(matterId); + if (matter == null || Boolean.TRUE.equals(matter.getDelFlag()) || !Boolean.TRUE.equals(matter.getEnabled())) { + return Result.error("报名出行时段不存在或已停用"); + } + TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId()); + if (line == null || Boolean.TRUE.equals(line.getDelFlag()) || !Boolean.TRUE.equals(line.getEnabled())) { + return Result.error("报名线路不存在或已停用"); + } + if (!isOutProvinceLine(line.getLineType())) { + return Result.success(NutMap.NEW().addv("canApply", true).addv("noticeRequired", false)); + } + TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId()); + 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("您不在本次疗休养报名范围内"); + } + TourLedger oldLedger = fetchCurrentUserMatterLedger(matter.getId()); + OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger); + return Result.success(NutMap.NEW() + .addv("noticeRequired", true) + .addv("canApply", quota.canApply()) + .addv("ratioType", quota.ratioType()) + .addv("basePeople", quota.basePeople()) + .addv("totalSignupPeople", quota.totalSignupPeople()) + .addv("outProvinceSignupPeople", quota.outProvinceSignupPeople()) + .addv("allowPeople", quota.allowPeople()) + .addv("message", buildOutProvinceQuotaNotice(quota))); + } + + @At + @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) + public Result bedTypeOptions() { + List list = sysDictService.getSubListByCode("bedType"); + if (list == null) { + return Result.success(Collections.emptyList()); + } + return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList())); + } + + @At + @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) + public Result familyRelationshipOptions() { + Sql sql = Sqls.create(""" + SELECT d.* + FROM sys_dict p + INNER JOIN sys_dict d ON d.path LIKE CONCAT(p.path, '%') + WHERE p.code = 'familyRelationship' + AND d.id <> p.id + AND d.disabled = 0 + AND d.hasChildren = 0 + ORDER BY d.path ASC, d.location ASC + """); + sql.setCallback(Sqls.callback.entities()); + sql.setEntity(tourLedgerService.dao().getEntity(Sys_dict.class)); + tourLedgerService.dao().execute(sql); + 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) + public Result doSignup(TourLedger ledger, @Param("families") String families, @Param("directRelative") String directRelative) { + Result checkResult = checkSignup(ledger); + if (checkResult != null) { + return checkResult; + } + if (StrUtil.isBlank(ledger.getId())) { + return Result.error("报名记录不存在"); + } + TourLedger oldLedger = fetchOwnLedger(ledger.getId()); + if (oldLedger == null) { + return Result.error("报名记录不存在"); + } + List familyList = parseFamilies(families); + Result familyResult = checkFamilies(familyList); + if (familyResult != null) { + return familyResult; + } + TourMatter matter = tourMatterService.fetch(ledger.getMatterId()); + Result ruleResult = checkSignupRule(ledger, matter, oldLedger); + if (ruleResult != null) { + return ruleResult; + } + Result maxPeopleResult = checkMaxGroupPeople(matter, oldLedger, familyList); + if (maxPeopleResult != null) { + return maxPeopleResult; + } + 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); + } + if (!allowOverCostReimbursement(line)) { + ledger.setOverCostReimbursed(false); + } + ledger.setOverCostReimbursed(Boolean.TRUE.equals(ledger.getOverCostReimbursed())); + boolean approvalRequired = directFamilyLine || Boolean.TRUE.equals(ledger.getOverCostReimbursed()); + TourLedgerDirectRelative directRelativeInfo = parseDirectRelative(directRelative); + Result directRelativeResult = checkDirectRelative(directFamilyLine, directRelativeInfo); + if (directRelativeResult != null) { + return directRelativeResult; + } + if (approvalRequired) { + Result workflowCheckResult = checkSignupApprovalWorkflowCanSubmit(ledger.getId()); + if (workflowCheckResult != null) { + return workflowCheckResult; + } + } + + ledger.setYear(matter.getYear()); + ledger.setHasFamily(Lang.isNotEmpty(familyList)); + ledger.setJoined(oldLedger.getJoined()); + ledger.setReimbursed(oldLedger.getReimbursed()); + ledger.setSignupTime(defaultIfBlank(oldLedger.getSignupTime(), ledger.getSignupTime())); + 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)) { + familyList.forEach(item -> { + item.setLedgerId(ledger.getId()); + item.setStaffJobNo(ledger.getJobNo()); + item.setStaffName(ledger.getUserName()); + }); + tourLedgerFamilyService.insert(familyList); + } + if (directFamilyLine) { + directRelativeInfo.setId(null); + directRelativeInfo.setLedgerId(ledger.getId()); + directRelativeInfo.setLineId(line.getId()); + tourLedgerDirectRelativeService.insert(directRelativeInfo); + } + if (approvalRequired) { + Result workflowResult = startOrContinueSignupApprovalWorkflow(ledger, directFamilyLine ? directRelativeInfo : null, matter, line); + if (workflowResult != null) { + return workflowResult; + } + } + return Result.success().addMsg(approvalRequired ? "修改已提交,等待审核" : "修改成功"); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) + public Result doCancelSignup(String ledgerId, String matterId) { + TourLedger ledger = fetchOwnLedger(ledgerId); + if (ledger == null && StrUtil.isNotBlank(matterId)) { + Cnd cnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getMatterId, "=", matterId) + .and(TourLedger::getJobNo, "=", currentJobNo()); + cnd.desc(TourLedger::getCreatedAt); + ledger = tourLedgerService.fetch(cnd); + } + if (ledger == null) { + return Result.error("报名记录不存在"); + } + 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("取消报名成功"); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission(value = {"tour.mysignup", "h5.tour.mysignup"}, mode = SaMode.OR) + @SLog(type = "tour", tag = "我的疗休养报名", msg = "删除我的疗休养报名") + public Result doDelete(String id) { + TourLedger ledger = fetchOwnLedger(id); + if (ledger == null) { + return Result.error("报名记录不存在"); + } + 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) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.and("t.jobNo", "=", currentJobNo()); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear); + cnd.and("t.lineName", "IS NOT", null); + cnd.and("t.lineName", "<>", ""); + + Sql sql = Sqls.create(""" + SELECT DISTINCT t.lineName + FROM tour_ledger t + $condition + ORDER BY t.lineName ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + private Cnd buildQueryCnd(Integer startYear, Integer endYear, String lineName) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.and("t.jobNo", "=", currentJobNo()); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear); + cnd.andEX("t.lineName", "=", lineName); + return cnd; + } + + private TourLedger fetchOwnLedger(String id) { + if (StrUtil.isBlank(id)) { + return null; + } + return tourLedgerService.fetch(Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getId, "=", id) + .and(TourLedger::getJobNo, "=", currentJobNo())); + } + + private Result checkSignup(TourLedger ledger) { + if (ledger == null) { + return Result.error("参数错误"); + } + if (ledger.getYear() == null) { + return Result.error("年度不能为空"); + } + if (StrUtil.isBlank(ledger.getLineId())) { + return Result.error("报名线路不能为空"); + } + if (StrUtil.isBlank(ledger.getMatterId())) { + return Result.error("报名出行时段不能为空"); + } + if (StrUtil.isBlank(ledger.getLineName())) { + 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()); + } + } + + 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("报名出行时段不存在或已停用"); + } + if (isTravelEnded(matter.getTravelEndTime())) { + return Result.error("线路出行已结束,不能修改"); + } + if (!ledger.getLineId().equals(matter.getLineId())) { + return Result.error("报名线路与出行时段不匹配"); + } + TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId()); + if (line == null || Boolean.TRUE.equals(line.getDelFlag()) || !Boolean.TRUE.equals(line.getEnabled())) { + return Result.error("报名线路不存在或已停用"); + } + TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId()); + 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("您不在本次疗休养报名范围内"); + } + 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("报名已结束"); + } + CycleAllowedTimes cycleAllowedTimes = calculateCycleAllowedTimes(matter, setting, oldLedger); + if (!cycleAllowedTimes.canApply()) { + return Result.error(buildCycleAllowedTimesNotice(cycleAllowedTimes)); + } + CycleTotalCost cycleTotalCost = calculateCycleTotalCost(matter, setting, line, oldLedger); + if (!cycleTotalCost.canApply()) { + return Result.error(buildCycleTotalCostNotice(cycleTotalCost)); + } + String jobNo = currentJobNo(); + if (isOutProvinceLine(line.getLineType())) { + int startYear = setting.getCycleStartYear() == null ? matter.getYear() : setting.getCycleStartYear(); + int endYear = LocalDate.now().getYear(); + Cnd outProvinceCnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getJobNo, "=", jobNo) + .and(TourLedger::getYear, ">=", startYear) + .and(TourLedger::getYear, "<=", endYear) + .and(TourLedger::getLineType, "in", List.of("省外线路", "省外")); + if (oldLedger != null) { + outProvinceCnd.and(TourLedger::getId, "<>", oldLedger.getId()); + } + if (tourLedgerService.count(outProvinceCnd) > 0) { + return Result.error("您已参加过省外线路,不能报名省外线路,请选择省内线路"); + } + OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger); + if (!quota.canApply()) { + return Result.error(buildOutProvinceQuotaNotice(quota)); + } + Cnd sameYearDomesticCnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getJobNo, "=", jobNo) + .and(TourLedger::getYear, "=", matter.getYear()) + .and(TourLedger::getLineType, "in", List.of("省内线路", "省内", "国内线路", "国内")); + if (oldLedger != null) { + sameYearDomesticCnd.and(TourLedger::getId, "<>", oldLedger.getId()); + } + TourLedger existingDomesticLedger = fetchLatestLedger(sameYearDomesticCnd); + if (existingDomesticLedger != null) { + return Result.error(buildSelectedLineMessage(existingDomesticLedger)); + } + } + if (isInProvinceLine(line.getLineType())) { + Cnd sameYearCnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getJobNo, "=", jobNo) + .and(TourLedger::getYear, "=", matter.getYear()); + if (oldLedger != null) { + sameYearCnd.and(TourLedger::getId, "<>", oldLedger.getId()); + } + TourLedger existingLedger = fetchLatestLedger(sameYearCnd); + if (existingLedger != null) { + return Result.error(buildSelectedLineMessage(existingLedger)); + } + } + return null; + } + + private TourLedger fetchLatestLedger(Cnd cnd) { + cnd.desc(TourLedger::getSignupTime); + cnd.desc(TourLedger::getCreatedAt); + return tourLedgerService.fetch(cnd); + } + + private TourLedger fetchCurrentUserMatterLedger(String matterId) { + if (StrUtil.isBlank(matterId)) { + return null; + } + Cnd cnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getMatterId, "=", matterId) + .and(TourLedger::getJobNo, "=", currentJobNo()); + cnd.desc(TourLedger::getCreatedAt); + return tourLedgerService.fetch(cnd); + } + + private CycleAllowedTimes calculateCycleAllowedTimes(TourMatter matter, TourSetting setting, TourLedger oldLedger) { + int allowedTimes = setting == null || setting.getCycleAllowedTimes() == null ? 0 : setting.getCycleAllowedTimes(); + int endYear = LocalDate.now().getYear(); + int startYear = setting == null || setting.getCycleStartYear() == null ? (matter == null || matter.getYear() == null ? endYear : matter.getYear()) : setting.getCycleStartYear(); + String excludeLedgerId = oldLedger == null ? null : oldLedger.getId(); + int joinedTimes = allowedTimes <= 0 ? 0 : countCycleJoinedTimes(currentJobNo(), startYear, endYear, excludeLedgerId); + return new CycleAllowedTimes(startYear, endYear, allowedTimes, joinedTimes); + } + + private int countCycleJoinedTimes(String jobNo, int startYear, int endYear, String excludeLedgerId) { + if (StrUtil.isBlank(jobNo)) { + return 0; + } + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger + WHERE delFlag = 0 + AND joined = 1 + AND jobNo = @jobNo + AND `year` >= @startYear + AND `year` <= @endYear + """ + excludeSql); + sql.setParam("jobNo", jobNo); + sql.setParam("startYear", startYear); + sql.setParam("endYear", endYear); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private String buildCycleAllowedTimesNotice(CycleAllowedTimes cycleAllowedTimes) { + return "您在 " + cycleAllowedTimes.startYear() + " 至 " + cycleAllowedTimes.endYear() + + " 周期内已参加 " + cycleAllowedTimes.joinedTimes() + " 次,周期允许 " + + cycleAllowedTimes.allowedTimes() + " 次,不能报名"; + } + + private CycleTotalCost calculateCycleTotalCost(TourMatter matter, TourSetting setting, TourLine line, TourLedger oldLedger) { + int totalCost = setting == null || setting.getCycleTotalCost() == null ? 0 : setting.getCycleTotalCost(); + int endYear = LocalDate.now().getYear(); + int startYear = setting == null || setting.getCycleStartYear() == null ? (matter == null || matter.getYear() == null ? endYear : matter.getYear()) : setting.getCycleStartYear(); + String excludeLedgerId = oldLedger == null ? null : oldLedger.getId(); + int joinedCost = totalCost <= 0 ? 0 : countCycleJoinedCost(currentJobNo(), startYear, endYear, excludeLedgerId); + int currentCost = totalCost <= 0 ? 0 : getLineActivityCost(line); + return new CycleTotalCost(startYear, endYear, totalCost, joinedCost, currentCost); + } + + private int countCycleJoinedCost(String jobNo, int startYear, int endYear, String excludeLedgerId) { + if (StrUtil.isBlank(jobNo)) { + return 0; + } + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT IFNULL(SUM(IFNULL(lot.activityCost, 0)), 0) + FROM tour_ledger t + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId AND lot.delFlag = 0 + WHERE t.delFlag = 0 + AND t.joined = 1 + AND t.jobNo = @jobNo + AND t.`year` >= @startYear + AND t.`year` <= @endYear + """ + excludeSql); + sql.setParam("jobNo", jobNo); + sql.setParam("startYear", startYear); + sql.setParam("endYear", endYear); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private int getLineActivityCost(TourLine line) { + if (line == null || StrUtil.isBlank(line.getLotId())) { + return 0; + } + Sql sql = Sqls.create(""" + SELECT IFNULL(activityCost, 0) AS activityCost + FROM tour_setting_lot + WHERE delFlag = 0 + AND id = @lotId + """); + sql.setParam("lotId", line.getLotId()); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private String buildCycleTotalCostNotice(CycleTotalCost cycleTotalCost) { + return "您在 " + cycleTotalCost.startYear() + " 至 " + cycleTotalCost.endYear() + + " 周期内已参加线路费用合计 " + cycleTotalCost.joinedCost() + + " 元,本次线路费用 " + cycleTotalCost.currentCost() + + " 元,周期内总费用 " + cycleTotalCost.totalCost() + " 元,超过限制,不能报名"; + } + + private OutProvinceQuota calculateOutProvinceQuota(TourMatter matter, TourSetting setting, TourLedger oldLedger) { + String ratioType = normalizeOutProvinceRatioType(setting == null ? null : setting.getOutProvinceRatioType()); + BigDecimal originalRatio = setting == null ? null : setting.getOutProvinceRatio(); + boolean oneThirdRatio = isOneThirdRatio(originalRatio); + BigDecimal ratio = oneThirdRatio ? BigDecimal.ONE.divide(BigDecimal.valueOf(3), 12, RoundingMode.HALF_UP) + : (originalRatio == null ? BigDecimal.ZERO : originalRatio); + int year = matter == null || matter.getYear() == null ? LocalDate.now().getYear() : matter.getYear(); + String excludeLedgerId = oldLedger == null ? null : oldLedger.getId(); + int totalSignupPeople = countYearSignupPeople(year, excludeLedgerId); + int outProvinceSignupPeople = countYearOutProvinceSignupPeople(year, excludeLedgerId); + int basePeople; + if ("可参加教职工人数".equals(ratioType)) { + Integer groupId = parseInteger(setting == null ? null : setting.getActivityGroupId()); + basePeople = countActivityGroupPeople(groupId); + } else if ("固定人数".equals(ratioType)) { + basePeople = setting == null || setting.getOutProvinceFixedPeople() == null ? 0 : setting.getOutProvinceFixedPeople(); + } else { + basePeople = totalSignupPeople; + } + int quotaPeople = oneThirdRatio + ? basePeople / 3 + : ratio.multiply(BigDecimal.valueOf(basePeople)).setScale(0, RoundingMode.FLOOR).intValue(); + int allowPeople = quotaPeople - outProvinceSignupPeople; + return new OutProvinceQuota(ratioType, ratio, basePeople, totalSignupPeople, outProvinceSignupPeople, allowPeople); + } + + private boolean isOneThirdRatio(BigDecimal ratio) { + if (ratio == null) { + return false; + } + return ratio.compareTo(BigDecimal.valueOf(0.33)) >= 0 && ratio.compareTo(BigDecimal.valueOf(0.3333)) <= 0; + } + + private String normalizeOutProvinceRatioType(String ratioType) { + if ("当年参加人数".equals(ratioType) || StrUtil.isBlank(ratioType)) { + return "当年报名人数"; + } + if ("可参加教职工人数".equals(ratioType) || "固定人数".equals(ratioType)) { + return ratioType; + } + return "当年报名人数"; + } + + private String buildOutProvinceQuotaNotice(OutProvinceQuota quota) { + String suffix = quota.canApply() ? "您可以报名,或您稍后报名" : "请稍后报名"; + if ("当年报名人数".equals(quota.ratioType())) { + return "当前报名人数是" + quota.totalSignupPeople() + "人,已经报名省外线路人数是" + + quota.outProvinceSignupPeople() + "人,按照省外人数不超过" + quota.ratioText() + + "的规则,当前时间允许报名人数是" + quota.allowPeople() + "人," + suffix; + } + if ("可参加教职工人数".equals(quota.ratioType())) { + return "今年有权限报名人数是" + quota.basePeople() + "人,已经报名省外线路人数是" + + quota.outProvinceSignupPeople() + "人,按照省外人数不超过" + quota.ratioText() + + "的规则,当前时间允许报名人数是" + quota.allowPeople() + "人," + suffix; + } + return "按照省外人数不超过" + quota.ratioText() + + "的规则,当前时间允许报名人数是" + quota.allowPeople() + "人," + suffix; + } + + private int countYearSignupPeople(int year, String excludeLedgerId) { + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + WHERE t.delFlag = 0 + AND t.`year` = @year + AND (ins.id IS NULL OR ins.state = @finishedState) + """ + excludeSql); + sql.setParam("year", year); + sql.setParam("finishedState", ProcessInstanceStateEnum.FINISHED.getCode()); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private int countYearOutProvinceSignupPeople(int year, String excludeLedgerId) { + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + WHERE t.delFlag = 0 + AND t.`year` = @year + AND t.lineType IN ('省外线路', '省外') + AND (ins.id IS NULL OR ins.state = @finishedState) + """ + excludeSql); + sql.setParam("year", year); + sql.setParam("finishedState", ProcessInstanceStateEnum.FINISHED.getCode()); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private int countActivityGroupPeople(Integer groupId) { + if (groupId == null) { + return 0; + } + Sql sql = Sqls.create("SELECT COUNT(1) FROM (" + activityBasicScopeService.buildGroupUserIdSubSqlText(groupId) + ") scope_users"); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private boolean isOutProvinceLine(String lineType) { + return "省外线路".equals(lineType) || "省外".equals(lineType); + } + + private boolean isInProvinceLine(String lineType) { + return "省内线路".equals(lineType) || "省内".equals(lineType); + } + + private Integer parseInteger(String value) { + if (StrUtil.isBlank(value)) { + return null; + } + try { + return Integer.valueOf(value); + } catch (NumberFormatException e) { + return null; + } + } + + private String normalizeDateTime(String value, boolean endOfDay) { + if (StrUtil.isBlank(value)) { + return ""; + } + String trimmed = value.trim(); + if (trimmed.length() == 10) { + return trimmed + (endOfDay ? " 23:59:59" : " 00:00:00"); + } + if (trimmed.length() == 16) { + return trimmed + ":00"; + } + return trimmed; + } + + private boolean isTravelEnded(String travelEndTime) { + if (StrUtil.isBlank(travelEndTime)) { + return false; + } + String now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + return now.compareTo(normalizeDateTime(travelEndTime, true)) > 0; + } + + private void fillStaffInfo(TourLedger ledger) { + View_user user = tourLedgerService.dao().fetch(View_user.class, Cnd.where(View_user::getId, "=", SecurityUtil.getUserId())); + ledger.setJobNo(user == null ? SecurityUtil.getUserLoginname() : defaultIfBlank(user.getLoginname(), SecurityUtil.getUserLoginname())); + ledger.setUserName(user == null ? SecurityUtil.getUserUsername() : defaultIfBlank(user.getUsername(), SecurityUtil.getUserUsername())); + ledger.setGender(user == null ? ledger.getGender() : defaultIfBlank(user.getSex(), ledger.getGender())); + ledger.setIdCard(user == null ? ledger.getIdCard() : defaultIfBlank(user.getIdCard(), ledger.getIdCard())); + ledger.setUnitId(user == null ? SecurityUtil.getUnitId() : defaultIfBlank(user.getUnitId(), SecurityUtil.getUnitId())); + ledger.setUnitName(user == null ? ledger.getUnitName() : defaultIfBlank(user.getUnitName(), ledger.getUnitName())); + ledger.setUnionId(user == null ? SecurityUtil.getUnionId() : defaultIfBlank(user.getUnionId(), ledger.getUnionId())); + ledger.setUnionName(user == null ? ledger.getUnionName() : defaultIfBlank(user.getUnionName(), ledger.getUnionName())); + } + + private void clearBedInfo(TourLedger ledger, List familyList) { + if (ledger != null) { + ledger.setBedType(""); + ledger.setBedInfo(""); + ledger.setIntendedRoommate(""); + } + if (Lang.isNotEmpty(familyList)) { + familyList.forEach(item -> { + item.setBedType(""); + item.setBedInfo(""); + item.setIntendedRoommate(""); + }); + } + } + + private List parseFamilies(String families) { + if (StrUtil.isBlank(families)) { + return Collections.emptyList(); + } + List list = Json.fromJsonAsList(TourLedgerFamily.class, families); + if (Lang.isEmpty(list)) { + return Collections.emptyList(); + } + return list.stream() + .filter(item -> item != null && StrUtil.isNotBlank(item.getFamilyName())) + .collect(Collectors.toList()); + } + + private Result checkFamilies(List familyList) { + if (Lang.isEmpty(familyList)) { + return null; + } + for (TourLedgerFamily family : familyList) { + if (family == null) { + continue; + } + if (StrUtil.isBlank(family.getFamilyName()) || StrUtil.isBlank(family.getGender()) + || StrUtil.isBlank(family.getIdCard()) || StrUtil.isBlank(family.getRelationship())) { + return Result.error("请完善亲属姓名、性别、身份证号码和关系"); + } + String idCard = family.getIdCard().trim().toUpperCase(); + if (!isValidIdCard(idCard)) { + return Result.error("请输入正确的身份证号码"); + } + family.setIdCard(idCard); + } + return null; + } + + private boolean isValidIdCard(String idCard) { + return StrUtil.isNotBlank(idCard) + && idCard.matches("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dX]$"); + } + + private Result checkMaxGroupPeople(TourMatter matter, TourLedger oldLedger, List familyList) { + if (matter == null || matter.getMaxGroupPeople() == null || matter.getMaxGroupPeople() <= 0) { + return null; + } + int currentPeople = 1 + (Lang.isEmpty(familyList) ? 0 : familyList.size()); + int signedPeople = countSignupPeople(matter.getId(), oldLedger == null ? null : oldLedger.getId()); + int totalPeople = signedPeople + currentPeople; + if (totalPeople > matter.getMaxGroupPeople()) { + return Result.error("当前报名人数 " + signedPeople + " 人,本次报名 " + currentPeople + + " 人,最多成团人数 " + matter.getMaxGroupPeople() + " 人,报名后将超过人数上限,不能提交"); + } + return null; + } + + private int countSignupPeople(String matterId, String excludeLedgerId) { + if (StrUtil.isBlank(matterId)) { + return 0; + } + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT IFNULL(SUM(1 + IFNULL(f.familyCount, 0)), 0) AS signupPeople + FROM tour_ledger t + LEFT JOIN ( + SELECT ledgerId, COUNT(1) AS familyCount + FROM tour_ledger_family + WHERE delFlag = 0 + GROUP BY ledgerId + ) f ON f.ledgerId = t.id + WHERE t.delFlag = 0 + AND t.matterId = @matterId + """ + excludeSql); + sql.setParam("matterId", matterId); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private String buildSelectedLineMessage(TourLedger ledger) { + String selectedLineName = getLedgerLineName(ledger); + return "您已经选择【" + selectedLineName + "】,取消后再重新选择。"; + } + + private String getLedgerLineName(TourLedger ledger) { + if (ledger == null) { + return ""; + } + if (StrUtil.isNotBlank(ledger.getLineName())) { + return ledger.getLineName(); + } + if (StrUtil.isBlank(ledger.getLineId())) { + return ""; + } + TourLine line = tourLedgerService.dao().fetch(TourLine.class, ledger.getLineId()); + return line == null ? "" : defaultIfBlank(line.getLineName(), ""); + } + + private record CycleAllowedTimes(int startYear, + int endYear, + int allowedTimes, + int joinedTimes) { + boolean canApply() { + return allowedTimes <= 0 || joinedTimes < allowedTimes; + } + } + + private record CycleTotalCost(int startYear, + int endYear, + int totalCost, + int joinedCost, + int currentCost) { + boolean canApply() { + return totalCost <= 0 || joinedCost + currentCost <= totalCost; + } + } + + private record OutProvinceQuota(String ratioType, + BigDecimal ratio, + int basePeople, + int totalSignupPeople, + int outProvinceSignupPeople, + int allowPeople) { + boolean canApply() { + return allowPeople > 0; + } + + String ratioText() { + if (ratio == null || BigDecimal.ZERO.compareTo(ratio) == 0) { + return "0"; + } + BigDecimal oneThird = BigDecimal.ONE.divide(BigDecimal.valueOf(3), 8, RoundingMode.HALF_UP); + if (ratio.subtract(oneThird).abs().compareTo(BigDecimal.valueOf(0.01)) <= 0) { + return "1/3"; + } + return ratio.stripTrailingZeros().toPlainString(); + } + } + + private TourLedgerDirectRelative parseDirectRelative(String directRelative) { + if (StrUtil.isBlank(directRelative)) { + return null; + } + return Json.fromJson(TourLedgerDirectRelative.class, directRelative); + } + + private Result checkDirectRelative(boolean directFamilyLine, TourLedgerDirectRelative directRelativeInfo) { + if (!directFamilyLine) { + return null; + } + if (directRelativeInfo == null) { + return Result.error("请填写直系亲属线路信息"); + } + if (StrUtil.isBlank(directRelativeInfo.getRelativeName())) { + return Result.error("亲属姓名不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getUnitName())) { + return Result.error("亲属所在单位不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getRelationshipCode())) { + return Result.error("亲属关系不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getLineName())) { + return Result.error("直系亲属线路名称不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getTravelStartTime())) { + return Result.error("出行开始日期不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getTravelEndTime())) { + return Result.error("出行结束日期不能为空"); + } + List directRelativeOptions = sysDictService.getSubListByCode("directRelative"); + Sys_dict dict = directRelativeOptions == null ? null : directRelativeOptions.stream() + .filter(item -> directRelativeInfo.getRelationshipCode().equals(item.getCode()) && !item.isDisabled()) + .findFirst() + .orElse(null); + if (dict == null) { + return Result.error("亲属关系无效"); + } + directRelativeInfo.setRelationshipName(defaultIfBlank(dict.getName(), directRelativeInfo.getRelationshipName())); + return null; + } + + private boolean allowOverCostReimbursement(TourLine line) { + if (line == null || StrUtil.isBlank(line.getLotId())) { + return false; + } + Sql sql = Sqls.create(""" + SELECT IFNULL(allowOverReimbursement, 0) AS allowOverReimbursement + FROM tour_setting_lot + WHERE id = @lotId + """); + sql.setParam("lotId", line.getLotId()); + sql.setCallback(Sqls.callback.map()); + tourLedgerService.dao().execute(sql); + NutMap lot = sql.getObject(NutMap.class); + return lot != null && lot.getBoolean("allowOverReimbursement", false); + } + + private Result startOrContinueSignupApprovalWorkflow(TourLedger ledger, TourLedgerDirectRelative directRelativeInfo, TourMatter matter, TourLine line) { + ProcessInstance instance = tourLedgerService.dao().fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", ledger.getId()) + .and(ProcessInstance::getState, "<>", ProcessInstanceStateEnum.ABANDON.getCode()) + .desc(ProcessInstance::getCreatedAt)); + Dict args = buildSignupApprovalArgs(ledger, directRelativeInfo, matter, line); + if (instance == null) { + ProcessInstance newInstance = flowEngine.startProcessInstanceByKey(DIRECT_FAMILY_WORKFLOW_KEY, ledger.getId(), SecurityUtil.getUserId(), args); + List doingTaskList = flowEngine.processTaskService().getDoingTaskList(newInstance.getId(), null); + for (ProcessTask task : doingTaskList) { + flowEngine.executeProcessTask(task.getId(), SecurityUtil.getUserId(), args); + } + return null; + } + if (!ProcessInstanceStateEnum.DOING.getCode().equals(instance.getState())) { + return Result.error("当前审批流程状态不允许修改提交"); + } + ProcessTask startTask = tourLedgerService.dao().fetch(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", instance.getId()) + .and(ProcessTask::getTaskName, "=", "startTask") + .and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.DOING.getCode())); + if (startTask == null) { + return Result.error("当前审批流程节点不允许修改提交"); + } + flowEngine.executeProcessTask(startTask.getId(), SecurityUtil.getUserId(), args); + return null; + } + + private Result checkSignupApprovalWorkflowCanSubmit(String ledgerId) { + ProcessInstance instance = tourLedgerService.dao().fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", ledgerId) + .and(ProcessInstance::getState, "<>", ProcessInstanceStateEnum.ABANDON.getCode()) + .desc(ProcessInstance::getCreatedAt)); + if (instance == null) { + return null; + } + if (!ProcessInstanceStateEnum.DOING.getCode().equals(instance.getState())) { + return Result.error("当前审批流程状态不允许修改提交"); + } + ProcessTask startTask = tourLedgerService.dao().fetch(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", instance.getId()) + .and(ProcessTask::getTaskName, "=", "startTask") + .and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.DOING.getCode())); + if (startTask == null) { + return Result.error("当前审批流程节点不允许修改提交"); + } + return null; + } + + private Dict buildSignupApprovalArgs(TourLedger ledger, TourLedgerDirectRelative directRelativeInfo, TourMatter matter, TourLine line) { + Dict args = Dict.create(); + args.set(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.APPLY.getCode()); + args.set(FlowConst.FORM_DATA, NutMap.NEW() + .addv("ledger", ledger) + .addv("directRelative", directRelativeInfo) + .addv("matter", matter) + .addv("line", line)); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "matterId", ledger.getMatterId()); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "lineId", ledger.getLineId()); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "lineName", ledger.getLineName()); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "directFamilyUnitLine", line != null && Boolean.TRUE.equals(line.getDirectFamilyUnitLine())); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "overCostReimbursed", Boolean.TRUE.equals(ledger.getOverCostReimbursed())); + return args; + } + + private String getTravelPeriod(TourLedger ledger) { + if (ledger == null || ledger.getYear() == null || StrUtil.isBlank(ledger.getLineId())) { + return ""; + } + if (StrUtil.isNotBlank(ledger.getMatterId())) { + Sql exactSql = Sqls.create(""" + SELECT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE '' + END AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND id = @matterId + """); + exactSql.setParam("matterId", ledger.getMatterId()); + exactSql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(exactSql); + return exactSql.getString(); + } + Sql sql = Sqls.create(""" + SELECT 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 `year` = @year + AND lineId = @lineId + """); + sql.setParam("year", ledger.getYear()); + sql.setParam("lineId", ledger.getLineId()); + sql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(sql); + return sql.getString(); + } + + private NutMap getSignupConfig(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getId())) { + return NutMap.NEW().addv("allowFamily", false).addv("fillBedInfo", true).addv("directFamilyUnitLine", false); + } + Sql sql = Sqls.create(""" + SELECT + IFNULL(s.allowFamily, 0) AS allowFamily, + IFNULL(s.fillBedInfo, 1) AS fillBedInfo, + IFNULL(l.directFamilyUnitLine, 0) AS directFamilyUnitLine + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_setting s ON s.id = m.settingId AND s.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + WHERE t.id = @ledgerId + """); + sql.setParam("ledgerId", ledger.getId()); + sql.setCallback(Sqls.callback.map()); + tourLedgerService.dao().execute(sql); + NutMap map = sql.getObject(NutMap.class); + if (map == null || map.isEmpty()) { + return NutMap.NEW().addv("allowFamily", false).addv("fillBedInfo", true).addv("directFamilyUnitLine", false); + } + return map; + } + + private NutMap buildExportInfo(String id) { + TourLedger ledger = fetchOwnLedger(id); + if (ledger == null || !Boolean.TRUE.equals(ledger.getOverCostReimbursed())) { + return null; + } + + NutMap matterInfo = getMatterExportInfo(ledger); + String travelStartTime = matterInfo.getString("travelStartTime", ""); + String travelEndTime = matterInfo.getString("travelEndTime", ""); + String travelPeriod = StrUtil.isNotBlank(travelStartTime) && StrUtil.isNotBlank(travelEndTime) + ? travelStartTime + " 至 " + travelEndTime + : getTravelPeriod(ledger); + if ((StrUtil.isBlank(travelStartTime) || StrUtil.isBlank(travelEndTime)) && StrUtil.isNotBlank(travelPeriod)) { + String[] dateRange = parseTravelPeriodRange(travelPeriod); + travelStartTime = firstNotBlank(travelStartTime, dateRange[0]); + travelEndTime = firstNotBlank(travelEndTime, dateRange[1]); + } + + NutMap approvalTimes = getApprovalTimes(ledger.getId()); + Integer totalDays = calcDays(travelStartTime, travelEndTime); + + return NutMap.NEW() + .addv("id", ledger.getId()) + .addv("userName", StrUtil.blankToDefault(ledger.getUserName(), "")) + .addv("unionName", StrUtil.blankToDefault(ledger.getUnionName(), "")) + .addv("lineName", StrUtil.blankToDefault(ledger.getLineName(), "")) + .addv("travelPeriod", StrUtil.blankToDefault(travelPeriod, "")) + .addv("travelStartTime", travelStartTime) + .addv("travelEndTime", travelEndTime) + .addv("travelStartYear", parseDatePart(travelStartTime, "year")) + .addv("travelStartMonth", parseDatePart(travelStartTime, "month")) + .addv("travelStartDay", parseDatePart(travelStartTime, "day")) + .addv("travelEndYear", parseDatePart(travelEndTime, "year")) + .addv("travelEndMonth", parseDatePart(travelEndTime, "month")) + .addv("travelEndDay", parseDatePart(travelEndTime, "day")) + .addv("totalDays", totalDays == null ? "" : totalDays) + .addv("estimatedCost", formatAmount(matterInfo.get("estimatedCost"))) + .addv("applyDate", firstNotBlank(approvalTimes.getString("applyDate", ""), ledger.getSignupTime())) + .addv("unionAuditorName", approvalTimes.getString("unionAuditorName", "")) + .addv("unionAuditDate", approvalTimes.getString("unionAuditDate", "")) + .addv("schoolUnionAuditDate", approvalTimes.getString("schoolUnionAuditDate", "")); + } + + private NutMap buildDirectRelativeExportInfo(String id) { + TourLedger ledger = fetchOwnLedger(id); + if (ledger == null) { + return null; + } + NutMap signupConfig = getSignupConfig(ledger); + if (!signupConfig.getBoolean("directFamilyUnitLine", false)) { + return null; + } + TourLedgerDirectRelative directRelative = tourLedgerDirectRelativeService.fetch(Cnd.where(TourLedgerDirectRelative::getDelFlag, "=", false) + .and(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); + if (directRelative == null) { + return null; + } + + NutMap matterInfo = getMatterExportInfo(ledger); + String travelStartTime = firstNotBlank(directRelative.getTravelStartTime(), matterInfo.getString("travelStartTime", "")); + String travelEndTime = firstNotBlank(directRelative.getTravelEndTime(), matterInfo.getString("travelEndTime", "")); + String travelPeriod = StrUtil.isNotBlank(travelStartTime) && StrUtil.isNotBlank(travelEndTime) + ? travelStartTime + " 至 " + travelEndTime + : getTravelPeriod(ledger); + if ((StrUtil.isBlank(travelStartTime) || StrUtil.isBlank(travelEndTime)) && StrUtil.isNotBlank(travelPeriod)) { + String[] dateRange = parseTravelPeriodRange(travelPeriod); + travelStartTime = firstNotBlank(travelStartTime, dateRange[0]); + travelEndTime = firstNotBlank(travelEndTime, dateRange[1]); + } + NutMap approvalTimes = getApprovalTimes(ledger.getId()); + + return NutMap.NEW() + .addv("id", ledger.getId()) + .addv("userName", StrUtil.blankToDefault(ledger.getUserName(), "")) + .addv("unionName", StrUtil.blankToDefault(ledger.getUnionName(), "")) + .addv("relativeName", StrUtil.blankToDefault(directRelative.getRelativeName(), "")) + .addv("relativeUnitName", StrUtil.blankToDefault(directRelative.getUnitName(), "")) + .addv("relationshipName", StrUtil.blankToDefault(directRelative.getRelationshipName(), "")) + .addv("lineName", firstNotBlank(directRelative.getLineName(), ledger.getLineName())) + .addv("travelPeriod", StrUtil.blankToDefault(travelPeriod, "")) + .addv("travelStartTime", travelStartTime) + .addv("travelEndTime", travelEndTime) + .addv("travelStartYear", parseDatePart(travelStartTime, "year")) + .addv("travelStartMonth", parseDatePart(travelStartTime, "month")) + .addv("travelStartDay", parseDatePart(travelStartTime, "day")) + .addv("travelEndYear", parseDatePart(travelEndTime, "year")) + .addv("travelEndMonth", parseDatePart(travelEndTime, "month")) + .addv("travelEndDay", parseDatePart(travelEndTime, "day")) + .addv("applyDate", firstNotBlank(approvalTimes.getString("applyDate", ""), ledger.getSignupTime())) + .addv("unionAuditorName", approvalTimes.getString("unionAuditorName", "")) + .addv("unionAuditDate", approvalTimes.getString("unionAuditDate", "")) + .addv("schoolUnionAuditDate", approvalTimes.getString("schoolUnionAuditDate", "")) + .addv("relativeUnitAuditDate", ""); + } + + private NutMap getMatterExportInfo(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getMatterId())) { + return NutMap.NEW(); + } + Sql sql = Sqls.create(""" + SELECT + m.travelStartTime, + m.travelEndTime, + m.estimatedCost + FROM tour_matter m + WHERE m.delFlag = 0 + AND m.id = @matterId + """); + sql.setParam("matterId", ledger.getMatterId()); + sql.setCallback(Sqls.callback.map()); + tourMatterService.dao().execute(sql); + NutMap map = sql.getObject(NutMap.class); + return map == null ? NutMap.NEW() : map; + } + + private NutMap getApprovalTimes(String ledgerId) { + NutMap result = NutMap.NEW(); + if (StrUtil.isBlank(ledgerId)) { + return result; + } + Sql sql = Sqls.create(""" + SELECT + t.displayName, + t.variable, + DATE_FORMAT(t.finishTime, '%Y-%m-%d') AS finishDate + FROM wf_process_task t + INNER JOIN wf_process_instance ins ON ins.id = t.processInstanceId + WHERE ins.businessNo = @ledgerId + AND t.taskState = @finished + AND t.displayName IN ('申请', '分工会审核', '校工会审核') + ORDER BY t.finishTime ASC + """); + sql.setParam("ledgerId", ledgerId); + sql.setParam("finished", ProcessTaskStateEnum.FINISHED.getCode()); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + for (NutMap item : sql.getList(NutMap.class)) { + String displayName = item.getString("displayName", ""); + String finishDate = item.getString("finishDate", ""); + if ("申请".equals(displayName) && StrUtil.isBlank(result.getString("applyDate", ""))) { + result.put("applyDate", finishDate); + } else if ("分工会审核".equals(displayName)) { + result.put("unionAuditDate", finishDate); + result.put("unionAuditorName", parseTaskUserName(item.getString("variable", ""))); + } else if ("校工会审核".equals(displayName)) { + result.put("schoolUnionAuditDate", finishDate); + } + } + return result; + } + + private String parseTaskUserName(String variable) { + if (StrUtil.isBlank(variable)) { + return ""; + } + try { + NutMap map = Json.fromJson(NutMap.class, variable); + if (map == null) { + return ""; + } + return firstNotBlank( + map.getString(FlowConst.TASK_FORM_DATA_PREFIX + "userName", ""), + map.getString("userName", "") + ); + } catch (Exception e) { + return ""; + } + } + + private Integer calcDays(String start, String end) { + if (StrUtil.isBlank(start) || StrUtil.isBlank(end)) { + return null; + } + try { + LocalDate startDate = LocalDate.parse(start.substring(0, 10)); + LocalDate endDate = LocalDate.parse(end.substring(0, 10)); + return Math.toIntExact(ChronoUnit.DAYS.between(startDate, endDate) + 1); + } catch (Exception e) { + return null; + } + } + + private String parseDatePart(String date, String part) { + if (StrUtil.isBlank(date) || date.length() < 10) { + return ""; + } + try { + LocalDate localDate = LocalDate.parse(date.substring(0, 10)); + if ("year".equals(part)) { + return String.valueOf(localDate.getYear()); + } + return "month".equals(part) ? String.valueOf(localDate.getMonthValue()) : String.valueOf(localDate.getDayOfMonth()); + } catch (Exception e) { + return ""; + } + } + + private String[] parseTravelPeriodRange(String travelPeriod) { + if (StrUtil.isBlank(travelPeriod)) { + return new String[]{"", ""}; + } + String[] parts = travelPeriod.split("至"); + if (parts.length < 2) { + return new String[]{"", ""}; + } + return new String[]{normalizeDate(parts[0]), normalizeDate(parts[1])}; + } + + private String normalizeDate(String value) { + if (StrUtil.isBlank(value)) { + return ""; + } + String date = value.trim(); + return date.length() >= 10 ? date.substring(0, 10) : date; + } + + private String formatAmount(Object value) { + if (value == null) { + return ""; + } + try { + BigDecimal amount = new BigDecimal(String.valueOf(value)); + return amount.stripTrailingZeros().toPlainString(); + } catch (Exception e) { + return String.valueOf(value); + } + } + + private String firstNotBlank(String first, String second) { + return StrUtil.isNotBlank(first) ? first : StrUtil.blankToDefault(second, ""); + } + + private byte[] buildExportPdf(NutMap data) { + try (PDDocument document = new PDDocument(); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + PDType0Font font = loadChineseFont(document, false); + PDType0Font boldFont = loadChineseFont(document, true); + try (PDPageContentStream content = new PDPageContentStream(document, page)) { + PdfWriter writer = new PdfWriter(content, font, boldFont); + writer.center("5天外超出部分疗休养费用由单位承担申请书", 18, 710, true); + writer.text("学校工会:", 18, 90, 665, false); + + List p1 = new ArrayList<>(); + p1.add(PdfSegment.text("本人为")); + p1.add(PdfSegment.field(data.getString("unionName", ""), 54)); + p1.add(PdfSegment.text("的")); + p1.add(PdfSegment.field(data.getString("userName", ""), 54)); + p1.add(PdfSegment.text(",拟参加")); + p1.add(PdfSegment.field(data.getString("unionName", ""), 54)); + p1.add(PdfSegment.text("于")); + p1.add(PdfSegment.field(data.getString("travelStartYear", ""), 42)); + p1.add(PdfSegment.text("年")); + p1.add(PdfSegment.field(data.getString("travelStartMonth", ""), 28)); + p1.add(PdfSegment.text("月")); + p1.add(PdfSegment.field(data.getString("travelStartDay", ""), 28)); + p1.add(PdfSegment.text("日至")); + p1.add(PdfSegment.field(data.getString("travelEndYear", ""), 42)); + p1.add(PdfSegment.text("年")); + p1.add(PdfSegment.field(data.getString("travelEndMonth", ""), 28)); + p1.add(PdfSegment.text("月")); + p1.add(PdfSegment.field(data.getString("travelEndDay", ""), 28)); + p1.add(PdfSegment.text("日组织的")); + p1.add(PdfSegment.field(data.getString("lineName", ""), 72)); + p1.add(PdfSegment.text("线路疗休养,预计总时长")); + p1.add(PdfSegment.field(data.getString("totalDays", ""), 28)); + p1.add(PdfSegment.text("天(含在途时间)、总费用预计")); + p1.add(PdfSegment.field(data.getString("estimatedCost", ""), 48)); + p1.add(PdfSegment.text("元,本人申请5天以外(不含5天)超出部分费用由学校承担。")); + float paragraphEndY = writer.paragraph(p1, 90, 640, 420, 18, 27f, 36); + writer.text("特此申请,请批准。", 18, 126, paragraphEndY - 27f, false); + + writer.text("申请人:", 18, 300, paragraphEndY - 82, false); + writer.text(data.getString("userName", ""), 18, 372, paragraphEndY - 82, false); + drawPlainDateLine(writer, data.getString("applyDate", ""), 330, paragraphEndY - 109, 18); + + writer.text("分工会主席签字:", 18, 246, paragraphEndY - 136, false); + writer.text(data.getString("unionAuditorName", ""), 18, 390, paragraphEndY - 136, false); + drawPlainDateLine(writer, data.getString("unionAuditDate", ""), 330, paragraphEndY - 163, 18); + + writer.text("学校工会:盖章", 18, 300, paragraphEndY - 190, false); + drawPlainDateLine(writer, data.getString("schoolUnionAuditDate", ""), 330, paragraphEndY - 217, 18); + } + document.save(out); + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("导出PDF失败", e); + } + } + + private byte[] buildDirectRelativeExportPdf(NutMap data) { + try (PDDocument document = new PDDocument(); + ByteArrayOutputStream out = new ByteArrayOutputStream()) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + PDType0Font font = loadChineseFont(document, false); + PDType0Font boldFont = loadChineseFont(document, true); + try (PDPageContentStream content = new PDPageContentStream(document, page)) { + PdfWriter writer = new PdfWriter(content, font, boldFont); + writer.center("本人随同参加直系亲属单位疗休养活动暨", 18, 760, true); + writer.center("疗休养费用由我校按规定标准给予报销申请书", 18, 733, true); + writer.text("学校工会:", 15, 90, 680, false); + + List p1 = new ArrayList<>(); + p1.add(PdfSegment.text("我本人是")); + p1.add(PdfSegment.field(data.getString("unionName", ""), 54)); + p1.add(PdfSegment.text("的")); + p1.add(PdfSegment.field(data.getString("userName", ""), 54)); + p1.add(PdfSegment.text(",我申请随同参加本人直系亲属(亲属关系为")); + p1.add(PdfSegment.field(data.getString("relationshipName", ""), 42)); + p1.add(PdfSegment.text(")姓名")); + p1.add(PdfSegment.field(data.getString("relativeName", ""), 54)); + p1.add(PdfSegment.text("所在单位")); + p1.add(PdfSegment.field(data.getString("relativeUnitName", ""), 72)); + p1.add(PdfSegment.text("于")); + p1.add(PdfSegment.field(data.getString("travelStartYear", ""), 42)); + p1.add(PdfSegment.text("年")); + p1.add(PdfSegment.field(data.getString("travelStartMonth", ""), 28)); + p1.add(PdfSegment.text("月")); + p1.add(PdfSegment.field(data.getString("travelStartDay", ""), 28)); + p1.add(PdfSegment.text("日至")); + p1.add(PdfSegment.field(data.getString("travelEndYear", ""), 42)); + p1.add(PdfSegment.text("年")); + p1.add(PdfSegment.field(data.getString("travelEndMonth", ""), 28)); + p1.add(PdfSegment.text("月")); + p1.add(PdfSegment.field(data.getString("travelEndDay", ""), 28)); + p1.add(PdfSegment.text("日期间所开展的")); + p1.add(PdfSegment.field(data.getString("lineName", ""), 72)); + p1.add(PdfSegment.text("线路疗休养活动;同时,申请本人此次疗休养费用由我校按规定标准给予报销。")); + float paragraphEndY = writer.paragraph(p1, 90, 635, 420, 15, 22.5f, 30); + writer.text("特此申请,请批准。", 15, 90, paragraphEndY - 22.5f, false); + + float signX = 250; + float signY = paragraphEndY - 80; + writer.text("申请人签名:", 15, signX, signY, false); + writer.text(data.getString("userName", ""), 15, signX + 90, signY, false); + drawPlainDateLine(writer, data.getString("applyDate", ""), signX + 45, signY - 31, 15); + + signY -= 78; + writer.text("分工会主席签字(盖章):", 15, signX - 60, signY, false); + writer.text(data.getString("unionAuditorName", ""), 15, signX + 135, signY, false); + drawPlainDateLine(writer, data.getString("unionAuditDate", ""), signX + 45, signY - 31, 15); + + signY -= 78; + writer.text("学校工会:盖章", 15, signX + 20, signY, false); + drawPlainDateLine(writer, data.getString("schoolUnionAuditDate", ""), signX + 45, signY - 31, 15); + + signY -= 78; + writer.text("直系亲属所在单位工会:盖章", 15, signX - 35, signY, false); + drawPlainDateLine(writer, data.getString("relativeUnitAuditDate", ""), signX + 45, signY - 31, 15); + } + document.save(out); + return out.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("导出PDF失败", e); + } + } + + private void drawDateLine(PdfWriter writer, String date, float x, float y) throws IOException { + writer.text("时间:", 14, x, y, false); + drawDate(writer, date, x + 48, y, 14); + } + + private void drawPlainDateLine(PdfWriter writer, String date, float x, float y, float size) throws IOException { + writer.text("时间:", size, x, y, false); + writer.text(normalizeDate(date), size, x + size * 3, y, false); + } + + private void drawDate(PdfWriter writer, String date, float x, float y, float size) throws IOException { + writer.field(parseDatePart(date, "year"), size, x, y, 42); + writer.text("年", size, x + 44, y, false); + writer.field(parseDatePart(date, "month"), size, x + 68, y, 28); + writer.text("月", size, x + 98, y, false); + writer.field(parseDatePart(date, "day"), size, x + 122, y, 28); + writer.text("日", size, x + 152, y, false); + } + + private PDType0Font loadChineseFont(PDDocument document, boolean bold) throws IOException { + String[] paths = bold + ? new String[]{"C:/Windows/Fonts/simhei.ttf", "C:/Windows/Fonts/msyhbd.ttf", "C:/Windows/Fonts/simsun.ttf"} + : new String[]{"C:/Windows/Fonts/simfang.ttf", "C:/Windows/Fonts/simsun.ttf", "C:/Windows/Fonts/msyh.ttf"}; + for (String path : paths) { + File file = new File(path); + if (file.exists()) { + return PDType0Font.load(document, file); + } + } + throw new IOException("未找到中文字体文件"); + } + + private static class PdfSegment { + private final String text; + private final boolean bold; + private final boolean underline; + private final float minWidth; + + private PdfSegment(String text, boolean bold, boolean underline, float minWidth) { + this.text = text == null ? "" : text; + this.bold = bold; + this.underline = underline; + this.minWidth = minWidth; + } + + private static PdfSegment text(String text) { + return new PdfSegment(text, false, false, 0); + } + + private static PdfSegment field(String text, float minWidth) { + return new PdfSegment(text, false, true, minWidth); + } + } + + private static class PdfWriter { + private final PDPageContentStream content; + private final PDType0Font font; + private final PDType0Font boldFont; + + private PdfWriter(PDPageContentStream content, PDType0Font font, PDType0Font boldFont) { + this.content = content; + this.font = font; + this.boldFont = boldFont; + } + + private void center(String text, float size, float y, boolean bold) throws IOException { + PDType0Font useFont = bold ? boldFont : font; + float width = useFont.getStringWidth(text) / 1000 * size; + text(text, size, (PDRectangle.A4.getWidth() - width) / 2, y, bold); + } + + private void text(String text, float size, float x, float y, boolean bold) throws IOException { + if (StrUtil.isBlank(text)) { + return; + } + content.beginText(); + content.setFont(bold ? boldFont : font, size); + content.newLineAtOffset(x, y); + content.showText(text); + content.endText(); + } + + private void field(String text, float size, float x, float y, float minWidth) throws IOException { + PDType0Font useFont = font; + float textWidth = StrUtil.isBlank(text) ? 0 : useFont.getStringWidth(text) / 1000 * size; + field(text, size, x, y, Math.max(minWidth, textWidth + 6), textWidth); + } + + private void field(String text, float size, float x, float y, float width, float textWidth) throws IOException { + content.moveTo(x, y - 3); + content.lineTo(x + width, y - 3); + content.stroke(); + if (StrUtil.isNotBlank(text)) { + text(text, size, x + Math.max(3, (width - textWidth) / 2), y, false); + } + } + + private float paragraph(List segments, float x, float y, float maxWidth, float size, float lineHeight, float firstLineIndent) throws IOException { + float cursorX = x + firstLineIndent; + float cursorY = y; + boolean lineStart = true; + for (PdfSegment segment : segments) { + if (segment.underline) { + float segmentWidth = fieldWidth(segment.text, size, segment.minWidth); + if (!lineStart && cursorX + segmentWidth > x + maxWidth) { + cursorX = x; + cursorY -= lineHeight; + lineStart = true; + } + if (lineStart && cursorX + segmentWidth > x + maxWidth && StrUtil.isNotBlank(segment.text)) { + segmentWidth = Math.max(segment.minWidth, x + maxWidth - cursorX); + } + float textWidth = StrUtil.isBlank(segment.text) ? 0 : font.getStringWidth(segment.text) / 1000 * size; + field(segment.text, size, cursorX, cursorY, segmentWidth, textWidth); + cursorX += segmentWidth; + lineStart = false; + continue; + } + for (int i = 0; i < segment.text.length(); i++) { + String ch = String.valueOf(segment.text.charAt(i)); + PDType0Font useFont = segment.bold ? boldFont : font; + float charWidth = useFont.getStringWidth(ch) / 1000 * size; + if (!lineStart && cursorX + charWidth > x + maxWidth) { + cursorX = x; + cursorY -= lineHeight; + lineStart = true; + } + text(ch, size, cursorX, cursorY, segment.bold); + cursorX += charWidth; + lineStart = false; + } + } + return cursorY; + } + + private float fieldWidth(String text, float size, float minWidth) throws IOException { + float textWidth = StrUtil.isBlank(text) ? 0 : font.getStringWidth(text) / 1000 * size; + return Math.max(minWidth, textWidth + 6); + } + } + + private String defaultIfBlank(String value, String defaultValue) { + return StrUtil.isBlank(value) ? defaultValue : value; + } + + private String currentJobNo() { + View_user user = tourLedgerService.dao().fetch(View_user.class, Cnd.where(View_user::getId, "=", SecurityUtil.getUserId())); + return user == null ? SecurityUtil.getUserLoginname() : defaultIfBlank(user.getLoginname(), SecurityUtil.getUserLoginname()); + } + + private String getOrderColumn(String orderName) { + if ("year".equals(orderName)) { + return "t.`year`"; + } + if ("jobNo".equals(orderName)) { + return "t.jobNo"; + } + if ("userName".equals(orderName)) { + return "t.userName"; + } + if ("lineName".equals(orderName)) { + return "t.lineName"; + } + if ("travelPeriod".equals(orderName)) { + return "COALESCE(m.travelStartTime, tp.travelPeriod)"; + } + if ("lineType".equals(orderName)) { + return "t.lineType"; + } + if ("signupTime".equals(orderName)) { + return "t.signupTime"; + } + if ("familyCount".equals(orderName)) { + return "familyCount"; + } + if ("joined".equals(orderName)) { + return "t.joined"; + } + if ("curTaskName".equals(orderName)) { + return "curTaskName"; + } + if ("instanceState".equals(orderName)) { + return "ins.state"; + } + return "t.signupTime"; + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourPlaceholderController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourPlaceholderController.java new file mode 100644 index 00000000..1d669e76 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourPlaceholderController.java @@ -0,0 +1,13 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.mvc.annotation.At; + +/** + * 分阶段建设的菜单占位入口。 + */ +@IocBean +@At("/platform/tour") +public class TourPlaceholderController { + +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSchoolUnionApprovalController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSchoolUnionApprovalController.java new file mode 100644 index 00000000..1c6b7467 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSchoolUnionApprovalController.java @@ -0,0 +1,581 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaMode; +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.result.Result; +import com.budwk.app.flow.enums.ProcessTaskStateEnum; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService; +import org.nutz.dao.Cnd; +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; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +import java.time.LocalDate; +import java.util.List; + +@IocBean +@Ok("json:full") +@At("/platform/tour/schoolUnionApproval") +public class TourSchoolUnionApprovalController { + + private static final String WORKFLOW_KEY = "LXYBZXQSXL"; + private static final String TASK_DISPLAY_NAME = "校工会审核"; + + @Inject + private TourLedgerService tourLedgerService; + + @Inject + private TourLedgerFamilyService tourLedgerFamilyService; + + @Inject + private TourLedgerDirectRelativeService tourLedgerDirectRelativeService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/schoolUnionApproval/index.html") + @SaCheckPermission(value = {"tour.schoolUnionApproval", "h5.tour.schoolUnionApproval"}, mode = SaMode.OR) + public void index() { + } + + @At("/h5") + @Ok("beetl:/platform/zhghh5/dayofficework/tour/schoolUnionApproval/index.html") + @SaCheckPermission(value = {"tour.schoolUnionApproval", "h5.tour.schoolUnionApproval"}, mode = SaMode.OR) + public void h5() { + } + + @At + @SaCheckPermission(value = {"tour.schoolUnionApproval", "h5.tour.schoolUnionApproval"}, mode = SaMode.OR) + public Result pageData(PageForm pageForm, Boolean audit, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType); + + Sql countSql = Sqls.create(""" + SELECT COUNT(DISTINCT task.id) + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + 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 + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + t.*, + ins.id AS instanceId, + ins.businessNo, + ins.state AS instanceState, + ins.variable AS instanceVariable, + ins.processDefineId AS instanceProcessDefineId, + task.id AS taskId, + task.taskName AS taskKey, + task.displayName AS taskName, + task.taskType, + task.performType AS taskPerformType, + task.taskState, + task.finishTime, + task.taskParentId, + task.variable AS taskVariable, + IFNULL(GROUP_CONCAT(DISTINCT nt.displayName), '结束') AS curTaskName, + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS currentLineName, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE tp.travelPeriod + END AS travelPeriod, + IFNULL(f.familyCount, 0) AS familyCount + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10 + 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 sys_union mu ON mu.id = m.unionId + 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 task.id + ORDER BY $orderColumn $orderBy, task.createdAt DESC, t.signupTime DESC, t.createdAt DESC + """); + listSql.setCondition(cnd); + listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "ascending".equals(pageForm.getPageOrderBy()) ? "asc" : "desc"); + listSql.setPager(tourLedgerService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(listSql); + + List list = listSql.getList(NutMap.class); + list.forEach(item -> item.put("lineName", item.getString("currentLineName"))); + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list); + return Result.success(pagination); + } + + @At + @SaCheckPermission(value = {"tour.schoolUnionApproval", "h5.tour.schoolUnionApproval"}, mode = SaMode.OR) + public Result detail(String id) { + TourLedger ledger = fetchAuditLedger(id); + if (ledger == null) { + return Result.error("报名记录不存在或无权查看"); + } + ledger.setLineName(getCurrentLineName(ledger)); + Cnd familyCnd = Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()) + .and(TourLedgerFamily::getDelFlag, "=", false); + familyCnd.asc(TourLedgerFamily::getCreatedAt); + TourLedgerDirectRelative directRelative = tourLedgerDirectRelativeService.fetch( + Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()) + .and(TourLedgerDirectRelative::getDelFlag, "=", false)); + NutMap signupConfig = getSignupConfig(ledger); + return Result.success(NutMap.NEW() + .addv("ledger", ledger) + .addv("travelPeriod", getTravelPeriod(ledger)) + .addv("families", tourLedgerFamilyService.query(familyCnd)) + .addv("directRelative", directRelative) + .addv("allowFamily", signupConfig.getBoolean("allowFamily", false)) + .addv("fillBedInfo", signupConfig.getBoolean("fillBedInfo", true)) + .addv("directFamilyUnitLine", signupConfig.getBoolean("directFamilyUnitLine", false))); + } + + @At + @SaCheckPermission(value = {"tour.schoolUnionApproval", "h5.tour.schoolUnionApproval"}, mode = SaMode.OR) + public Result unionOptions(Boolean audit, Integer startYear, Integer endYear, String keyword, String lineId, String travelPeriod, String lineType) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, null, lineId, travelPeriod, lineType); + cnd.and("t.unionId", "IS NOT", null); + cnd.and("t.unionId", "<>", ""); + + Sql sql = Sqls.create(""" + SELECT DISTINCT + t.unionId AS id, + COALESCE(NULLIF(u.name, ''), t.unionName) AS name, + u.unionCode + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + 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 sys_union u ON u.id = t.unionId + $condition + ORDER BY u.unionCode ASC, name ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission(value = {"tour.schoolUnionApproval", "h5.tour.schoolUnionApproval"}, mode = SaMode.OR) + public Result lineOptions(Boolean audit, Integer startYear, Integer endYear, String travelPeriod, String lineType, String unionId, String keyword) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, null, travelPeriod, lineType); + cnd.and("t.lineName", "IS NOT", null); + cnd.and("t.lineName", "<>", ""); + + Sql sql = Sqls.create(""" + SELECT DISTINCT + CASE + WHEN IFNULL(t.lineId, '') <> '' THEN CONCAT(t.lineId, '|', IFNULL(m.unionId, '')) + ELSE CONCAT('legacy:', t.lineName, '|', IFNULL(m.unionId, '')) + END AS lineId, + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS lineName + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + 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 sys_union mu ON mu.id = m.unionId + $condition + ORDER BY lineName ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission(value = {"tour.schoolUnionApproval", "h5.tour.schoolUnionApproval"}, mode = SaMode.OR) + public Result travelPeriodOptions(Boolean audit, Integer startYear, Integer endYear, String lineId, String lineType, String unionId, String keyword) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, lineId, null, lineType); + cnd.and("m.travelStartTime", "IS NOT", null); + cnd.and("m.travelStartTime", "<>", ""); + cnd.and("m.travelEndTime", "IS NOT", null); + cnd.and("m.travelEndTime", "<>", ""); + + Sql sql = Sqls.create(""" + SELECT DISTINCT CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) AS travelPeriod + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + 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 + $condition + ORDER BY travelPeriod ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission(value = {"tour.schoolUnionApproval", "h5.tour.schoolUnionApproval"}, mode = SaMode.OR) + public Result lineTypeOptions(Boolean audit, Integer startYear, Integer endYear, String lineId, String travelPeriod, String unionId, String keyword) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, lineId, travelPeriod, null); + cnd.and("t.lineType", "IS NOT", null); + cnd.and("t.lineType", "<>", ""); + + Sql sql = Sqls.create(""" + SELECT DISTINCT t.lineType + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + 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 + $condition + ORDER BY t.lineType ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + private Cnd buildAuditCnd(Boolean audit, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.and("def.name", "=", WORKFLOW_KEY); + cnd.and("task.displayName", "=", TASK_DISPLAY_NAME); + cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId())); + appendAuditStateFilter(cnd, audit); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.andEX("t.unionId", "=", unionId); + cnd.andEX("t.lineType", "=", lineType); + appendLineFilter(cnd, lineId); + appendTravelPeriodFilter(cnd, travelPeriod); + appendKeywordFilter(cnd, keyword); + return cnd; + } + + private void appendAuditStateFilter(Cnd cnd, Boolean audit) { + if (Boolean.TRUE.equals(audit)) { + cnd.and("task.taskState", "in", List.of( + ProcessTaskStateEnum.FINISHED.getCode(), + ProcessTaskStateEnum.WITHDRAW.getCode(), + ProcessTaskStateEnum.INTERRUPT.getCode())); + return; + } + cnd.and("task.taskState", "=", ProcessTaskStateEnum.DOING.getCode()); + } + + private void appendKeywordFilter(Cnd cnd, String keyword) { + if (StrUtil.isBlank(keyword)) { + return; + } + SqlExpressionGroup group = new SqlExpressionGroup(); + group.orLike("t.userName", keyword.trim()); + group.orLike("t.jobNo", keyword.trim()); + group.orLike("t.lineName", keyword.trim()); + group.orLike("l.lineName", keyword.trim()); + cnd.and(group); + } + + private void appendTravelPeriodFilter(Cnd cnd, String travelPeriod) { + if (StrUtil.isBlank(travelPeriod)) { + return; + } + cnd.and("CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)", "=", travelPeriod); + } + + private void appendLineFilter(Cnd cnd, String lineId) { + if (StrUtil.isBlank(lineId)) { + return; + } + LineFilter lineFilter = parseLineFilter(lineId); + if (lineFilter.getLineId().startsWith("legacy:")) { + SqlExpressionGroup legacyGroup = new SqlExpressionGroup(); + legacyGroup.or("t.lineId", "IS", null); + legacyGroup.or("t.lineId", "=", ""); + cnd.and(legacyGroup); + cnd.and("t.lineName", "=", lineFilter.getLineId().substring("legacy:".length())); + } else { + cnd.and("t.lineId", "=", lineFilter.getLineId()); + } + appendMatterUnionFilter(cnd, lineFilter.getUnionId()); + } + + private LineFilter parseLineFilter(String value) { + int delimiterIndex = value.indexOf("|"); + if (delimiterIndex < 0) { + return new LineFilter(value, null); + } + return new LineFilter(value.substring(0, delimiterIndex), value.substring(delimiterIndex + 1)); + } + + private void appendMatterUnionFilter(Cnd cnd, String unionId) { + if (unionId == null) { + return; + } + if (StrUtil.isBlank(unionId)) { + SqlExpressionGroup group = new SqlExpressionGroup(); + group.or("m.unionId", "IS", null); + group.or("m.unionId", "=", ""); + cnd.and(group); + return; + } + cnd.and("m.unionId", "=", unionId); + } + + private String getOrderColumn(String orderName) { + if ("jobNo".equals(orderName)) { + return "t.jobNo"; + } + if ("userName".equals(orderName)) { + return "t.userName"; + } + if ("lineName".equals(orderName)) { + return """ + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END + """; + } + if ("travelPeriod".equals(orderName)) { + return "COALESCE(m.travelStartTime, tp.travelPeriod)"; + } + if ("lineType".equals(orderName)) { + return "t.lineType"; + } + if ("signupTime".equals(orderName)) { + return "t.signupTime"; + } + if ("familyCount".equals(orderName)) { + return "familyCount"; + } + if ("instanceState".equals(orderName)) { + return "ins.state"; + } + return "task.createdAt"; + } + + private TourLedger fetchAuditLedger(String id) { + if (StrUtil.isBlank(id)) { + return null; + } + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + WHERE t.delFlag = 0 + AND t.id = @id + AND def.name = @workflowKey + AND task.displayName = @taskDisplayName + AND ta.actorId = @actorId + """); + sql.setParam("id", id); + sql.setParam("workflowKey", WORKFLOW_KEY); + sql.setParam("taskDisplayName", TASK_DISPLAY_NAME); + sql.setParam("actorId", SecurityUtil.getUserId()); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt() > 0 ? tourLedgerService.fetch(id) : null; + } + + private String getCurrentLineName(TourLedger ledger) { + if (ledger == null) { + return ""; + } + Sql sql = Sqls.create(""" + SELECT CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS lineName + FROM tour_ledger t + 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 sys_union mu ON mu.id = m.unionId + WHERE t.id = @ledgerId + """); + sql.setParam("ledgerId", ledger.getId()); + sql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(sql); + return StrUtil.blankToDefault(sql.getString(), ledger.getLineName()); + } + + private String getTravelPeriod(TourLedger ledger) { + if (ledger == null || ledger.getYear() == null || StrUtil.isBlank(ledger.getLineId())) { + return ""; + } + if (StrUtil.isNotBlank(ledger.getMatterId())) { + Sql exactSql = Sqls.create(""" + SELECT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE '' + END AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND id = @matterId + """); + exactSql.setParam("matterId", ledger.getMatterId()); + exactSql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(exactSql); + return exactSql.getString(); + } + Sql sql = Sqls.create(""" + SELECT 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 `year` = @year + AND lineId = @lineId + """); + sql.setParam("year", ledger.getYear()); + sql.setParam("lineId", ledger.getLineId()); + sql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(sql); + return sql.getString(); + } + + private NutMap getSignupConfig(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getLineId())) { + return NutMap.NEW().addv("allowFamily", false).addv("fillBedInfo", true).addv("directFamilyUnitLine", false); + } + Sql sql = Sqls.create(""" + SELECT + IFNULL(s.allowFamily, 0) AS allowFamily, + IFNULL(s.fillBedInfo, 1) AS fillBedInfo, + IFNULL(l.directFamilyUnitLine, 0) AS directFamilyUnitLine + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_setting s ON s.id = m.settingId AND s.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + WHERE t.id = @ledgerId + """); + sql.setParam("ledgerId", ledger.getId()); + sql.setCallback(Sqls.callback.map()); + tourLedgerService.dao().execute(sql); + NutMap map = sql.getObject(NutMap.class); + if (map == null || map.isEmpty()) { + return NutMap.NEW().addv("allowFamily", false).addv("fillBedInfo", true).addv("directFamilyUnitLine", false); + } + return map; + } + + private static class LineFilter { + private final String lineId; + private final String unionId; + + private LineFilter(String lineId, String unionId) { + this.lineId = lineId; + this.unionId = unionId; + } + + private String getLineId() { + return lineId; + } + + private String getUnionId() { + return unionId; + } + } +} 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 new file mode 100644 index 00000000..a58966a8 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSettingController.java @@ -0,0 +1,325 @@ +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.page.Pagination; +import com.budwk.app.base.param.PageForm; +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; +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; +import java.util.stream.Collectors; + +@IocBean +@Ok("json:full") +@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") + public void index() { + } + + @At + @SaCheckPermission("tour.setting") + public Result pageData(PageForm pageForm, Integer year, String configName) { + Cnd cnd = Cnd.NEW(); + cnd.andEX(TourSetting::getYear, "=", year); + cnd.and(Cnd.likeEX(TourSetting::getConfigName, configName)); + + cnd.desc(TourSetting::getYear).asc(TourSetting::getSortNo).desc(TourSetting::getCreatedAt); + Pagination pagination = tourSettingService.listPage( + pageForm.getPageNumber(), + pageForm.getPageSize(), + TourSetting.class, + cnd + ); + return Result.success(pagination); + } + + @At + @SaCheckPermission("tour.setting") + public Result detail(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + TourSetting tourSetting = tourSettingService.fetch(id); + if (tourSetting == null) { + return Result.error("配置不存在"); + } + // 编辑页面需要一起带出标段,按标段值倒序保持与老疗休养配置一致。 + Cnd lotCnd = Cnd.NEW(); + lotCnd.desc(TourSettingLot::getLotValue); + tourSettingService.dao().fetchLinks(tourSetting, "lots", lotCnd); + tourSetting.setUnionQuotas(tourSettingService.listUnionQuotaRows(id)); + return Result.success(tourSetting); + } + + @At + @SaCheckPermission("tour.setting") + public Result previousYearInfo(Integer year) { + if (year == null) { + return Result.error("请先选择年度"); + } + List settings = tourSettingService.query(Cnd.where(TourSetting::getYear, "=", year - 1) + .and(TourSetting::getDelFlag, "=", false) + .desc(TourSetting::getUpdatedAt) + .desc(TourSetting::getCreatedAt)); + if (Lang.isEmpty(settings)) { + return Result.error("未找到上一年度配置"); + } + TourSetting tourSetting = settings.get(0); + 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) { + return checkResult; + } + + Cnd sameNameCnd = Cnd.where(TourSetting::getYear, "=", tourSetting.getYear()) + .and(TourSetting::getConfigName, "=", tourSetting.getConfigName()); + if (StrUtil.isNotBlank(tourSetting.getId())) { + sameNameCnd.and(TourSetting::getId, "<>", tourSetting.getId()); + } + if (tourSettingService.count(sameNameCnd) > 0) { + return Result.error("同年度下配置名称已存在"); + } + + // 布尔值给默认值,避免前端未传时出现空状态。 + 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); + } + if (tourSetting.getAllowFamily() == null) { + tourSetting.setAllowFamily(false); + } + 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("当年报名人数"); + } + if (!"固定人数".equals(tourSetting.getOutProvinceRatioType())) { + tourSetting.setOutProvinceFixedPeople(0); + } + // 前端按项目既有约定把子表数组序列化提交,这里显式解析,避免自动绑定漏掉标段。 + if (StrUtil.isNotBlank(lots)) { + try { + tourSetting.setLots(Json.fromJsonAsList(TourSettingLot.class, lots)); + } catch (Exception e) { + return Result.error("标段值和标段费用必须为整数"); + } + } + tourSetting.setLots(normalizeLots(tourSetting)); + Result lotCheckResult = checkLots(tourSetting.getLots()); + if (lotCheckResult != null) { + return lotCheckResult; + } + if (StrUtil.isBlank(tourSetting.getId())) { + if (Lang.isEmpty(tourSetting.getLots())) { + tourSettingService.insert(tourSetting); + } else { + tourSettingService.insertWith(tourSetting, "lots"); + } + tourSettingService.saveUnionQuotas(tourSetting.getId(), unionQuotas); + } else { + // 编辑时先处理页面删除的标段,再保存配置和当前标段行。 + if (Lang.isNotEmpty(lotDeleteList)) { + tourSettingService.dao().clear(TourSettingLot.class, Cnd.where(TourSettingLot::getId, "in", lotDeleteList)); + } + tourSettingService.updateIgnoreNull(tourSetting); + saveLots(tourSetting); + tourSettingService.saveUnionQuotas(tourSetting.getId(), unionQuotas); + } + return Result.success(); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.setting") + @SLog(type = "tour", tag = "疗休养设置", msg = "删除疗休养配置") + public Result doDelete(String id) { + if (StrUtil.isBlank(id)) { + 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(); + } + + private List normalizeLots(TourSetting tourSetting) { + if (tourSetting == null || Lang.isEmpty(tourSetting.getLots())) { + return Collections.emptyList(); + } + // 过滤前端空行,避免误写入只有ID或空字段的标段记录。 + return tourSetting.getLots().stream() + .filter(item -> item != null + && (StrUtil.isNotBlank(item.getLotName()) + || StrUtil.isNotBlank(item.getLotValue()) + || item.getActivityCost() != null)) + .peek(item -> item.setSettingId(tourSetting.getId())) + .collect(Collectors.toList()); + } + + private void saveLots(TourSetting tourSetting) { + List lots = tourSetting.getLots(); + if (Lang.isEmpty(lots)) { + return; + } + lots.forEach(item -> item.setSettingId(tourSetting.getId())); + tourSettingService.dao().insertOrUpdate(lots); + } + + private Result check(TourSetting tourSetting) { + if (tourSetting == null) { + return Result.error("参数错误"); + } + if (tourSetting.getYear() == null) { + return Result.error("年度不能为空"); + } + 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("最少成团人数不能大于最多成团人数"); + } + if (tourSetting.getCycleStartYear() != null && tourSetting.getCycleEndYear() != null + && tourSetting.getCycleStartYear() > tourSetting.getCycleEndYear()) { + return Result.error("周期开始年度不能大于周期结束年度"); + } + if (StrUtil.isNotBlank(tourSetting.getOutProvinceRatioType()) + && !"当年参加人数".equals(tourSetting.getOutProvinceRatioType()) + && !"当年报名人数".equals(tourSetting.getOutProvinceRatioType()) + && !"可参加教职工人数".equals(tourSetting.getOutProvinceRatioType()) + && !"固定人数".equals(tourSetting.getOutProvinceRatioType())) { + return Result.error("省外人数占比类型不正确"); + } + if ("固定人数".equals(tourSetting.getOutProvinceRatioType()) + && (tourSetting.getOutProvinceFixedPeople() == null || tourSetting.getOutProvinceFixedPeople() < 0)) { + return Result.error("固定人数必须大于等于0"); + } + 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; + } + for (int i = 0; i < lots.size(); i++) { + TourSettingLot lot = lots.get(i); + String rowNo = "第" + (i + 1) + "行"; + if (StrUtil.isBlank(lot.getLotValue()) || !lot.getLotValue().matches("^\\d+$")) { + return Result.error(rowNo + "标段值必须为整数"); + } + if (lot.getActivityCost() == null || lot.getActivityCost() < 0) { + return Result.error(rowNo + "标段费用必须为整数"); + } + } + 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 new file mode 100644 index 00000000..e28ee2d1 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSignupController.java @@ -0,0 +1,1624 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaMode; +import cn.hutool.core.lang.Dict; +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.result.Result; +import com.budwk.app.flow.constant.FlowConst; +import com.budwk.app.flow.engine.FlowEngine; +import com.budwk.app.flow.entity.ProcessInstance; +import com.budwk.app.flow.entity.ProcessTask; +import com.budwk.app.flow.enums.ProcessInstanceStateEnum; +import com.budwk.app.flow.enums.ProcessSubmitTypeEnum; +import com.budwk.app.flow.enums.ProcessTaskStateEnum; +import com.budwk.app.sys.models.Sys_dict; +import com.budwk.app.sys.models.Sys_union; +import com.budwk.app.sys.services.SysDictService; +import com.budwk.app.sys.views.View_user; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.activity.basic.service.ActivityBasicScopeService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLine; +import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter; +import com.budwk.app.zhgh.dayofficework.tour.models.TourSetting; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeService; +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; +import org.nutz.dao.sql.Sql; +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.math.BigDecimal; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@IocBean +@Ok("json:full") +@At("/platform/tour/signup") +public class TourSignupController { + + private static final String DIRECT_FAMILY_WORKFLOW_KEY = "LXYBZXQSXL"; + + @Inject + private TourMatterService tourMatterService; + + @Inject + private TourUserAssignmentService tourUserAssignmentService; + + @Inject + private TourLedgerService tourLedgerService; + + @Inject + private TourLedgerFamilyService tourLedgerFamilyService; + + @Inject + private TourLedgerDirectRelativeService tourLedgerDirectRelativeService; + + @Inject + private SysDictService sysDictService; + + @Inject + private ActivityBasicScopeService activityBasicScopeService; + + @Inject + private FlowEngine flowEngine; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/signup/index.html") + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public void index() { + } + + @At("/h5") + @Ok("beetl:/platform/zhghh5/dayofficework/tour/signup/index.html") + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public void h5() { + } + + @At("/h5/serviceNotice") + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public Result h5ServiceNotice() { + int currentYear = LocalDate.now().getYear(); + Cnd cnd = Cnd.where(TourSetting::getDelFlag, "=", false) + .and(TourSetting::getEnabled, "=", true) + .and(TourSetting::getYear, "=", currentYear); + cnd.asc(TourSetting::getSortNo); + cnd.desc(TourSetting::getCreatedAt); + TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, cnd); + if (setting == null) { + return Result.success(NutMap.NEW() + .addv("year", currentYear) + .addv("configName", "") + .addv("serviceNotice", "")); + } + return Result.success(NutMap.NEW() + .addv("year", setting.getYear()) + .addv("configName", defaultIfBlank(setting.getConfigName(), "")) + .addv("serviceNotice", defaultIfBlank(setting.getServiceNotice(), ""))); + } + + @At("/h5/signup") + @Ok("beetl:/platform/zhghh5/dayofficework/tour/signup/list.html") + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public void h5Signup() { + } + + @At("/h5/apply") + @Ok("beetl:/platform/zhghh5/dayofficework/tour/signup/apply.html") + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public void h5Apply() { + } + + @At("/h5/lineInfo") + @Ok("beetl:/platform/zhghh5/dayofficework/tour/signup/lineInfo.html") + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public void h5LineInfo() { + } + + @At("/h5/pageData") + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public Result h5PageData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId, Boolean directFamilyUnitLine) { + if (StrUtil.isBlank(pageForm.getPageOrderName())) { + pageForm.defaultSortAsc("lineName"); + } + Cnd cnd = buildQueryCnd(year, lineName, lineType, unionId); + cnd.andEX("l.directFamilyUnitLine", "=", directFamilyUnitLine); + String currentJobNo = currentJobNo(); + + Sql countSql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_union u ON u.id = m.unionId + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourMatterService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + m.id AS matterId, + m.`year`, + m.unionId, + COALESCE(u.name, '校工会') AS unionName, + m.lineId, + l.lineName, + l.lineType, + l.directFamilyUnitLine, + l.mobileThumb AS lineMobileThumb, + a.agencyName AS travelAgencyName, + a.contactPhone, + a.mobileThumb AS agencyMobileThumb, + lot.lotName, + m.travelStartTime, + m.travelEndTime, + IFNULL(sc.signupCount, 0) AS signupCount, + IFNULL(sc.familyCount, 0) AS familyCount, + my.ledgerId, + my.instanceId, + my.instanceState, + my.taskKey, + my.startTaskId, + IFNULL(my.canRevoke, 0) AS canRevoke, + CASE WHEN my.ledgerId IS NULL THEN 0 ELSE 1 END AS signed + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_union u ON u.id = m.unionId + LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId + LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId + LEFT JOIN ( + SELECT + t.matterId, + COUNT(1) + SUM(IFNULL(f.familyCount, 0)) AS signupCount, + SUM(IFNULL(f.familyCount, 0)) AS familyCount + FROM tour_ledger t + LEFT JOIN ( + SELECT ledgerId, COUNT(1) AS familyCount + FROM tour_ledger_family + WHERE delFlag = 0 + GROUP BY ledgerId + ) f ON f.ledgerId = t.id + WHERE t.delFlag = 0 + AND t.matterId IS NOT NULL + AND t.matterId <> '' + GROUP BY t.matterId + ) sc ON sc.matterId = m.id + LEFT JOIN ( + SELECT + t.matterId, + t.jobNo, + MAX(t.id) AS ledgerId, + MAX(ins.id) AS instanceId, + MAX(ins.state) AS instanceState, + MAX(task.taskName) AS taskKey, + MAX((SELECT MAX(st.id) FROM wf_process_task st WHERE st.processInstanceId = ins.id AND st.taskName = 'startTask' AND st.taskState IN (10, 20))) AS startTaskId, + MAX(IF((SELECT taskName FROM wf_process_task tt WHERE tt.id = task.taskParentId) = 'startTask', 1, 0)) AS canRevoke + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id AND ins.state <> @abandonState + LEFT JOIN wf_process_task task ON task.processInstanceId = ins.id AND task.taskState = 10 + WHERE t.delFlag = 0 + AND t.jobNo = @currentJobNo + AND t.matterId IS NOT NULL + AND t.matterId <> '' + GROUP BY t.matterId, t.jobNo + ) my ON my.matterId = m.id + $condition + ORDER BY $orderColumn $orderBy, m.`year` DESC, m.travelStartTime ASC, l.lineName ASC, m.createdAt DESC + """); + listSql.setCondition(cnd); + listSql.setParam("currentJobNo", currentJobNo); + listSql.setParam("abandonState", ProcessInstanceStateEnum.ABANDON.getCode()); + listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc"); + listSql.setPager(tourMatterService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourMatterService.dao().execute(listSql); + + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class)); + return Result.success(pagination); + } + + @At + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public Result pageData(PageForm pageForm, Integer year, String lineName, String lineType, String unionId) { + if (StrUtil.isBlank(pageForm.getPageOrderName())) { + pageForm.defaultSortAsc("lineName"); + } + Cnd cnd = buildQueryCnd(year, lineName, lineType, unionId); + String currentJobNo = currentJobNo(); + + Sql countSql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_union u ON u.id = m.unionId + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourMatterService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + m.id AS matterId, + m.`year`, + m.matterName, + m.minGroupPeople, + m.maxGroupPeople, + m.unionId, + COALESCE(u.name, '校工会') AS unionName, + m.lineId, + l.lineName, + l.lineType, + l.directFamilyUnitLine, + IFNULL(lot.allowOverReimbursement, 0) AS allowOverReimbursement, + m.travelStartTime, + m.travelEndTime, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE '' + END AS travelPeriod, + IFNULL(sc.signupCount, 0) AS signupCount, + my.ledgerId, + my.instanceId, + my.instanceState, + my.taskKey, + my.startTaskId, + IFNULL(my.canRevoke, 0) AS canRevoke, + CASE WHEN my.ledgerId IS NULL THEN 0 ELSE 1 END AS signed + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_union u ON u.id = m.unionId + LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId + LEFT JOIN ( + SELECT t.matterId, SUM(1 + IFNULL(f.familyCount, 0)) AS signupCount + FROM tour_ledger t + LEFT JOIN ( + SELECT ledgerId, COUNT(1) AS familyCount + FROM tour_ledger_family + WHERE delFlag = 0 + GROUP BY ledgerId + ) f ON f.ledgerId = t.id + WHERE t.delFlag = 0 + AND t.matterId IS NOT NULL + AND t.matterId <> '' + GROUP BY t.matterId + ) sc ON sc.matterId = m.id + LEFT JOIN ( + SELECT + t.matterId, + t.jobNo, + MAX(t.id) AS ledgerId, + MAX(ins.id) AS instanceId, + MAX(ins.state) AS instanceState, + MAX(task.taskName) AS taskKey, + MAX((SELECT MAX(st.id) FROM wf_process_task st WHERE st.processInstanceId = ins.id AND st.taskName = 'startTask' AND st.taskState IN (10, 20))) AS startTaskId, + MAX(IF((SELECT taskName FROM wf_process_task tt WHERE tt.id = task.taskParentId) = 'startTask', 1, 0)) AS canRevoke + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id AND ins.state <> @abandonState + LEFT JOIN wf_process_task task ON task.processInstanceId = ins.id AND task.taskState = 10 + WHERE t.delFlag = 0 + AND t.jobNo = @currentJobNo + AND t.matterId IS NOT NULL + AND t.matterId <> '' + GROUP BY t.matterId, t.jobNo + ) my ON my.matterId = m.id + $condition + ORDER BY $orderColumn $orderBy, m.`year` DESC, m.travelStartTime ASC, l.lineName ASC, m.createdAt DESC + """); + listSql.setCondition(cnd); + listSql.setParam("currentJobNo", currentJobNo); + listSql.setParam("abandonState", ProcessInstanceStateEnum.ABANDON.getCode()); + listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc"); + listSql.setPager(tourMatterService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourMatterService.dao().execute(listSql); + + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, listSql.getList(NutMap.class)); + return Result.success(pagination); + } + + @At + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public Result unionOptions() { + Cnd cnd = Cnd.NEW(); + cnd.asc("unionCode"); + cnd.asc("name"); + return Result.success(tourMatterService.dao().query(Sys_union.class, cnd)); + } + + @At + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public Result lineTypeOptions(Integer year) { + Cnd cnd = buildQueryCnd(year, null, null, null); + Sql sql = Sqls.create(""" + SELECT DISTINCT l.lineType + FROM tour_matter m + INNER JOIN tour_line l ON l.id = m.lineId + LEFT JOIN sys_union u ON u.id = m.unionId + $condition + ORDER BY l.lineType ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourMatterService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public Result lineDetail(String lineId) { + if (StrUtil.isBlank(lineId)) { + return Result.error("参数错误"); + } + Sql sql = Sqls.create(""" + SELECT + l.*, + a.agencyName AS travelAgencyName, + a.contactPhone, + lot.lotName AS lotName + FROM tour_line l + LEFT JOIN tour_travel_agency a ON a.id = l.travelAgencyId + LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId + WHERE l.delFlag = 0 + AND l.id = @lineId + """); + sql.setParam("lineId", lineId); + sql.setCallback(Sqls.callback.map()); + tourMatterService.dao().execute(sql); + NutMap line = sql.getObject(NutMap.class); + return line == null || line.isEmpty() ? Result.error("线路不存在") : Result.success(line); + } + + @At + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public Result signupDetail(String matterId) { + if (StrUtil.isBlank(matterId)) { + return Result.error("参数错误"); + } + Sql sql = Sqls.create(""" + SELECT + m.id AS matterId, + m.`year`, + m.matterName, + m.unionId AS matterUnionId, + m.defaultBoardingPlace, + m.travelStartTime, + m.travelEndTime, + m.contactName, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE '' + END AS travelPeriod, + l.id AS lineId, + l.lineName, + l.lineType, + l.directFamilyUnitLine, + l.travelAgencyId, + a.agencyName AS travelAgencyName, + IFNULL(lot.allowOverReimbursement, 0) AS allowOverReimbursement, + s.allowFamily, + 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 + LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId + LEFT JOIN tour_setting s ON s.id = m.settingId + WHERE m.delFlag = 0 + AND m.enabled = 1 + AND m.id = @matterId + """); + sql.setParam("matterId", matterId); + sql.setCallback(Sqls.callback.map()); + tourMatterService.dao().execute(sql); + NutMap matter = sql.getObject(NutMap.class); + if (matter == null || matter.isEmpty()) { + return Result.error("报名线路不存在"); + } + View_user user = tourMatterService.dao().fetch(View_user.class, Cnd.where(View_user::getId, "=", SecurityUtil.getUserId())); + NutMap staff = NutMap.NEW() + .addv("jobNo", user == null ? SecurityUtil.getUserLoginname() : defaultIfBlank(user.getLoginname(), SecurityUtil.getUserLoginname())) + .addv("userName", user == null ? SecurityUtil.getUserUsername() : defaultIfBlank(user.getUsername(), SecurityUtil.getUserUsername())) + .addv("gender", user == null ? "" : defaultIfBlank(user.getSex(), "")) + .addv("idCard", user == null ? "" : defaultIfBlank(user.getIdCard(), "")) + .addv("mobile", user == null ? "" : defaultIfBlank(user.getMobile(), "")) + .addv("unitId", user == null ? SecurityUtil.getUnitId() : defaultIfBlank(user.getUnitId(), SecurityUtil.getUnitId())) + .addv("unitName", user == null ? "" : defaultIfBlank(user.getUnitName(), "")) + .addv("unionId", user == null ? SecurityUtil.getUnionId() : defaultIfBlank(user.getUnionId(), SecurityUtil.getUnionId())) + .addv("unionName", user == null ? "" : defaultIfBlank(user.getUnionName(), "")); + Cnd ledgerCnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getMatterId, "=", matter.getString("matterId")) + .and(TourLedger::getJobNo, "=", staff.getString("jobNo")); + ledgerCnd.desc(TourLedger::getCreatedAt); + TourLedger ledger = tourLedgerService.fetch(ledgerCnd); + TourMatter matterEntity = tourMatterService.fetch(matter.getString("matterId")); + TourLedger ruleLedger = new TourLedger(); + ruleLedger.setId(ledger == null ? null : ledger.getId()); + ruleLedger.setYear(matterEntity == null ? matter.getInt("year") : matterEntity.getYear()); + ruleLedger.setMatterId(matter.getString("matterId")); + ruleLedger.setLineId(matter.getString("lineId")); + ruleLedger.setLineName(matter.getString("lineName")); + Result ruleResult = checkSignupRule(ruleLedger, matterEntity, ledger); + if (ruleResult != null) { + return ruleResult; + } + List families = Collections.emptyList(); + TourLedgerDirectRelative directRelative = null; + if (ledger != null) { + Cnd familyCnd = Cnd.where(TourLedgerFamily::getDelFlag, "=", false) + .and(TourLedgerFamily::getLedgerId, "=", ledger.getId()); + familyCnd.asc(TourLedgerFamily::getCreatedAt); + families = tourLedgerFamilyService.query(familyCnd); + directRelative = tourLedgerDirectRelativeService.fetch(Cnd.where(TourLedgerDirectRelative::getDelFlag, "=", false) + .and(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); + } + return Result.success(NutMap.NEW() + .addv("matter", matter) + .addv("staff", staff) + .addv("ledger", ledger) + .addv("process", getSignupProcessInfo(ledger)) + .addv("families", families) + .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) { + if (StrUtil.isBlank(matterId)) { + return Result.error("参数错误"); + } + TourMatter matter = tourMatterService.fetch(matterId); + if (matter == null || Boolean.TRUE.equals(matter.getDelFlag()) || !Boolean.TRUE.equals(matter.getEnabled())) { + return Result.error("报名出行时段不存在或已停用"); + } + TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId()); + if (line == null || Boolean.TRUE.equals(line.getDelFlag()) || !Boolean.TRUE.equals(line.getEnabled())) { + return Result.error("报名线路不存在或已停用"); + } + TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId()); + if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) { + 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); + if (!cycleAllowedTimes.canApply()) { + return Result.success(NutMap.NEW() + .addv("canApply", false) + .addv("noticeRequired", true) + .addv("noticeType", "cycleAllowedTimes") + .addv("cycleStartYear", cycleAllowedTimes.startYear()) + .addv("cycleEndYear", cycleAllowedTimes.endYear()) + .addv("cycleAllowedTimes", cycleAllowedTimes.allowedTimes()) + .addv("cycleJoinedTimes", cycleAllowedTimes.joinedTimes()) + .addv("message", buildCycleAllowedTimesNotice(cycleAllowedTimes))); + } + CycleTotalCost cycleTotalCost = calculateCycleTotalCost(matter, setting, line, oldLedger); + if (!cycleTotalCost.canApply()) { + return Result.success(NutMap.NEW() + .addv("canApply", false) + .addv("noticeRequired", true) + .addv("noticeType", "cycleTotalCost") + .addv("cycleStartYear", cycleTotalCost.startYear()) + .addv("cycleEndYear", cycleTotalCost.endYear()) + .addv("cycleTotalCost", cycleTotalCost.totalCost()) + .addv("cycleJoinedCost", cycleTotalCost.joinedCost()) + .addv("currentLineCost", cycleTotalCost.currentCost()) + .addv("message", buildCycleTotalCostNotice(cycleTotalCost))); + } + if (isAssignedUserEligibility(setting) || !isOutProvinceLine(line.getLineType())) { + return Result.success(NutMap.NEW().addv("canApply", true).addv("noticeRequired", false)); + } + OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger); + return Result.success(NutMap.NEW() + .addv("noticeRequired", true) + .addv("noticeType", "outProvinceQuota") + .addv("canApply", quota.canApply()) + .addv("ratioType", quota.ratioType()) + .addv("basePeople", quota.basePeople()) + .addv("totalSignupPeople", quota.totalSignupPeople()) + .addv("outProvinceSignupPeople", quota.outProvinceSignupPeople()) + .addv("allowPeople", quota.allowPeople()) + .addv("ratioText", quota.ratioText()) + .addv("message", buildOutProvinceQuotaNotice(quota))); + } + + @At + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public Result outProvinceQuotaNotice(String matterId) { + if (StrUtil.isBlank(matterId)) { + return Result.error("参数错误"); + } + TourMatter matter = tourMatterService.fetch(matterId); + if (matter == null || Boolean.TRUE.equals(matter.getDelFlag()) || !Boolean.TRUE.equals(matter.getEnabled())) { + return Result.error("报名出行时段不存在或已停用"); + } + TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId()); + if (line == null || Boolean.TRUE.equals(line.getDelFlag()) || !Boolean.TRUE.equals(line.getEnabled())) { + return Result.error("报名线路不存在或已停用"); + } + if (!isOutProvinceLine(line.getLineType())) { + return Result.success(NutMap.NEW().addv("canApply", true).addv("noticeRequired", false)); + } + TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId()); + if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) { + 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); + return Result.success(NutMap.NEW() + .addv("noticeRequired", true) + .addv("canApply", quota.canApply()) + .addv("ratioType", quota.ratioType()) + .addv("basePeople", quota.basePeople()) + .addv("totalSignupPeople", quota.totalSignupPeople()) + .addv("outProvinceSignupPeople", quota.outProvinceSignupPeople()) + .addv("allowPeople", quota.allowPeople()) + .addv("ratioText", quota.ratioText()) + .addv("message", buildOutProvinceQuotaNotice(quota))); + } + + @At + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public Result directRelativeOptions() { + List list = sysDictService.getSubListByCode("directRelative"); + if (list == null) { + return Result.success(Collections.emptyList()); + } + return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList())); + } + + @At + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public Result bedTypeOptions() { + List list = sysDictService.getSubListByCode("bedType"); + if (list == null) { + return Result.success(Collections.emptyList()); + } + return Result.success(list.stream().filter(item -> !item.isDisabled()).collect(Collectors.toList())); + } + + @At + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public Result familyRelationshipOptions() { + Sql sql = Sqls.create(""" + SELECT d.* + FROM sys_dict p + INNER JOIN sys_dict d ON d.path LIKE CONCAT(p.path, '%') + WHERE p.code = 'familyRelationship' + AND d.id <> p.id + AND d.disabled = 0 + AND d.hasChildren = 0 + ORDER BY d.path ASC, d.location ASC + """); + sql.setCallback(Sqls.callback.entities()); + sql.setEntity(tourLedgerService.dao().getEntity(Sys_dict.class)); + tourLedgerService.dao().execute(sql); + 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) + public Result doSignup(TourLedger ledger, @Param("families") String families, @Param("directRelative") String directRelative) { + Result checkResult = checkSignup(ledger); + if (checkResult != null) { + return checkResult; + } + List familyList = parseFamilies(families); + Result familyResult = checkFamilies(familyList); + if (familyResult != null) { + return familyResult; + } + TourLedgerDirectRelative directRelativeInfo = parseDirectRelative(directRelative); + boolean update = StrUtil.isNotBlank(ledger.getId()); + TourLedger oldLedger = update ? tourLedgerService.fetch(ledger.getId()) : null; + if (update && oldLedger == null) { + return Result.error("报名记录不存在"); + } + if (update && !currentJobNo().equals(oldLedger.getJobNo())) { + return Result.error("不能修改他人的报名记录"); + } + TourMatter matter = tourMatterService.fetch(ledger.getMatterId()); + Result ruleResult = checkSignupRule(ledger, matter, update ? oldLedger : null); + if (ruleResult != null) { + return ruleResult; + } + if (!update) { + oldLedger = fetchCurrentUserMatterLedger(ledger.getMatterId()); + if (oldLedger != null) { + ledger.setId(oldLedger.getId()); + update = true; + } + } + Result maxPeopleResult = checkMaxGroupPeople(matter, oldLedger, familyList); + if (maxPeopleResult != null) { + return maxPeopleResult; + } + 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); + } + if (!allowOverCostReimbursement(line)) { + ledger.setOverCostReimbursed(false); + } + boolean overCostReimbursed = Boolean.TRUE.equals(ledger.getOverCostReimbursed()); + boolean approvalRequired = directFamilyLine || overCostReimbursed; + Result directRelativeResult = checkDirectRelative(directFamilyLine, directRelativeInfo); + if (directRelativeResult != null) { + return directRelativeResult; + } + if (update) { + Result workflowCheckResult = checkSignupApprovalWorkflowCanSubmit(ledger.getId()); + if (workflowCheckResult != null) { + return workflowCheckResult; + } + } + ledger.setYear(matter.getYear()); + ledger.setHasFamily(Lang.isNotEmpty(familyList)); + if (!update) { + ledger.setJoined(false); + ledger.setReimbursed(false); + ledger.setSignupTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + } else { + ledger.setJoined(oldLedger.getJoined()); + ledger.setReimbursed(oldLedger.getReimbursed()); + ledger.setSignupTime(defaultIfBlank(oldLedger.getSignupTime(), ledger.getSignupTime())); + } + ledger.setOverCostReimbursed(Boolean.TRUE.equals(ledger.getOverCostReimbursed())); + fillStaffInfo(ledger); + + if (update) { + tourLedgerService.updateIgnoreNull(ledger); + tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId())); + tourLedgerDirectRelativeService.clear(Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); + } else { + tourLedgerService.insert(ledger); + } + // 报名台账落库后,仅回填已存在的人员分配记录事项信息,不新增记录、不改变分配来源。 + tourUserAssignmentService.backfillExistingAssignmentAfterSignup(matter.getSettingId(), matter.getId(), SecurityUtil.getUserId(), ledger.getBoardingPlace()); + if (Lang.isNotEmpty(familyList)) { + familyList.forEach(item -> { + item.setLedgerId(ledger.getId()); + item.setStaffJobNo(ledger.getJobNo()); + item.setStaffName(ledger.getUserName()); + }); + tourLedgerFamilyService.insert(familyList); + } + if (directFamilyLine) { + directRelativeInfo.setId(null); + directRelativeInfo.setLedgerId(ledger.getId()); + directRelativeInfo.setLineId(line.getId()); + tourLedgerDirectRelativeService.insert(directRelativeInfo); + } + if (approvalRequired) { + Result workflowResult = startOrContinueSignupApprovalWorkflow(ledger, directFamilyLine ? directRelativeInfo : null, matter, line); + if (workflowResult != null) { + return workflowResult; + } + } + return Result.success().addMsg(approvalRequired ? (update ? "修改已提交,等待审核" : "申请已提交,等待审核") : (update ? "修改成功" : "报名成功")); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission(value = {"tour.signup", "h5.tour.signup"}, mode = SaMode.OR) + public Result doCancelSignup(String ledgerId, String matterId) { + TourLedger ledger = null; + if (StrUtil.isNotBlank(ledgerId)) { + ledger = tourLedgerService.fetch(ledgerId); + } + if (ledger == null && StrUtil.isNotBlank(matterId)) { + Cnd cancelCnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getMatterId, "=", matterId) + .and(TourLedger::getJobNo, "=", currentJobNo()); + cancelCnd.desc(TourLedger::getCreatedAt); + ledger = tourLedgerService.fetch(cancelCnd); + } + if (ledger == null || Boolean.TRUE.equals(ledger.getDelFlag())) { + return Result.error("报名记录不存在"); + } + if (!currentJobNo().equals(ledger.getJobNo())) { + return Result.error("不能取消他人的报名记录"); + } + 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); + cnd.and("m.enabled", "=", true); + cnd.and("m.lineId", "IS NOT", null); + cnd.and("m.lineId", "<>", ""); + cnd.and("l.delFlag", "=", false); + cnd.and("l.enabled", "=", true); + cnd.andEX("m.`year`", "=", year == null ? LocalDate.now().getYear() : year); + cnd.and(Cnd.likeEX("l.lineName", lineName)); + cnd.andEX("l.lineType", "=", lineType); + cnd.andEX("m.unionId", "=", unionId); + return cnd; + } + + private Result checkSignup(TourLedger ledger) { + if (ledger == null) { + return Result.error("参数错误"); + } + if (ledger.getYear() == null) { + return Result.error("年度不能为空"); + } + if (StrUtil.isBlank(ledger.getLineId())) { + return Result.error("报名线路不能为空"); + } + if (StrUtil.isBlank(ledger.getMatterId())) { + return Result.error("报名出行时段不能为空"); + } + if (StrUtil.isBlank(ledger.getLineName())) { + return Result.error("报名线路不能为空"); + } + return null; + } + + private Result checkDirectRelative(boolean directFamilyLine, TourLedgerDirectRelative directRelativeInfo) { + if (!directFamilyLine) { + return null; + } + if (directRelativeInfo == null) { + return Result.error("请填写直系亲属线路信息"); + } + if (StrUtil.isBlank(directRelativeInfo.getRelativeName())) { + return Result.error("亲属姓名不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getUnitName())) { + return Result.error("亲属所在单位不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getRelationshipCode())) { + return Result.error("亲属关系不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getLineName())) { + return Result.error("直系亲属线路名称不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getTravelStartTime())) { + return Result.error("出行开始日期不能为空"); + } + if (StrUtil.isBlank(directRelativeInfo.getTravelEndTime())) { + return Result.error("出行结束日期不能为空"); + } + List directRelativeOptions = sysDictService.getSubListByCode("directRelative"); + Sys_dict dict = directRelativeOptions == null ? null : directRelativeOptions.stream() + .filter(item -> directRelativeInfo.getRelationshipCode().equals(item.getCode()) && !item.isDisabled()) + .findFirst() + .orElse(null); + if (dict == null) { + return Result.error("亲属关系无效"); + } + directRelativeInfo.setRelationshipName(defaultIfBlank(dict.getName(), directRelativeInfo.getRelationshipName())); + return null; + } + + private Result checkFamilies(List familyList) { + if (Lang.isEmpty(familyList)) { + return null; + } + for (TourLedgerFamily family : familyList) { + if (family == null) { + continue; + } + if (StrUtil.isBlank(family.getFamilyName()) || StrUtil.isBlank(family.getGender()) + || StrUtil.isBlank(family.getIdCard()) || StrUtil.isBlank(family.getRelationship())) { + return Result.error("请完善亲属姓名、性别、身份证号码和关系"); + } + String idCard = family.getIdCard().trim().toUpperCase(); + if (!isValidIdCard(idCard)) { + return Result.error("请输入正确的身份证号码"); + } + family.setIdCard(idCard); + } + return null; + } + + private Result checkMaxGroupPeople(TourMatter matter, TourLedger oldLedger, List familyList) { + if (matter == null || matter.getMaxGroupPeople() == null || matter.getMaxGroupPeople() <= 0) { + return null; + } + int currentPeople = 1 + (Lang.isEmpty(familyList) ? 0 : familyList.size()); + int signedPeople = countSignupPeople(matter.getId(), oldLedger == null ? null : oldLedger.getId()); + int totalPeople = signedPeople + currentPeople; + if (totalPeople > matter.getMaxGroupPeople()) { + return Result.error("当前报名人数 " + signedPeople + " 人,本次报名 " + currentPeople + + " 人,最多成团人数 " + matter.getMaxGroupPeople() + " 人,报名后将超过人数上限,不能提交"); + } + return null; + } + + private boolean isValidIdCard(String idCard) { + return StrUtil.isNotBlank(idCard) + && idCard.matches("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[\\dX]$"); + } + + 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("报名出行时段不存在或已停用"); + } + if (!ledger.getLineId().equals(matter.getLineId())) { + return Result.error("报名线路与出行时段不匹配"); + } + TourLine line = tourLedgerService.dao().fetch(TourLine.class, matter.getLineId()); + if (line == null || Boolean.TRUE.equals(line.getDelFlag()) || !Boolean.TRUE.equals(line.getEnabled())) { + return Result.error("报名线路不存在或已停用"); + } + TourSetting setting = tourLedgerService.dao().fetch(TourSetting.class, matter.getSettingId()); + if (setting == null || Boolean.TRUE.equals(setting.getDelFlag()) || !Boolean.TRUE.equals(setting.getEnabled())) { + return Result.error("报名事项配置不存在或已停用"); + } + Result timeResult = checkSignupTime(matter); + if (timeResult != null) { + return timeResult; + } + Result scopeResult = checkSignupEligibilityScope(setting); + if (scopeResult != null) { + return scopeResult; + } + CycleAllowedTimes cycleAllowedTimes = calculateCycleAllowedTimes(matter, setting, oldLedger); + if (!cycleAllowedTimes.canApply()) { + return Result.error(buildCycleAllowedTimesNotice(cycleAllowedTimes)); + } + CycleTotalCost cycleTotalCost = calculateCycleTotalCost(matter, setting, line, oldLedger); + if (!cycleTotalCost.canApply()) { + return Result.error(buildCycleTotalCostNotice(cycleTotalCost)); + } + String jobNo = currentJobNo(); + 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) + .and(TourLedger::getJobNo, "=", jobNo) + .and(TourLedger::getYear, ">=", startYear) + .and(TourLedger::getYear, "<=", endYear) + .and(TourLedger::getLineType, "in", List.of("省外线路", "省外")); + if (oldLedger != null) { + outProvinceCnd.and(TourLedger::getId, "<>", oldLedger.getId()); + } + if (tourLedgerService.count(outProvinceCnd) > 0) { + return Result.error("您已参加过省外线路,不能报名省外线路,请选择省内线路"); + } + OutProvinceQuota quota = calculateOutProvinceQuota(matter, setting, oldLedger); + if (!quota.canApply()) { + return Result.error(buildOutProvinceQuotaNotice(quota)); + } + Cnd sameYearDomesticCnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getJobNo, "=", jobNo) + .and(TourLedger::getYear, "=", matter.getYear()) + .and(TourLedger::getLineType, "in", List.of("省内线路", "省内", "国内线路", "国内")); + if (oldLedger != null) { + sameYearDomesticCnd.and(TourLedger::getId, "<>", oldLedger.getId()); + } + TourLedger existingDomesticLedger = fetchLatestLedger(sameYearDomesticCnd); + if (existingDomesticLedger != null) { + return Result.error(buildSelectedLineMessage(existingDomesticLedger)); + } + } + if (isInProvinceLine(line.getLineType())) { + Cnd sameYearCnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getJobNo, "=", jobNo) + .and(TourLedger::getYear, "=", matter.getYear()); + if (oldLedger != null) { + sameYearCnd.and(TourLedger::getId, "<>", oldLedger.getId()); + } + TourLedger existingLedger = fetchLatestLedger(sameYearCnd); + if (existingLedger != null) { + return Result.error(buildSelectedLineMessage(existingLedger)); + } + } + return null; + } + + private TourLedger fetchLatestLedger(Cnd cnd) { + cnd.desc(TourLedger::getSignupTime); + cnd.desc(TourLedger::getCreatedAt); + return tourLedgerService.fetch(cnd); + } + + private TourLedger fetchCurrentUserMatterLedger(String matterId) { + if (StrUtil.isBlank(matterId)) { + return null; + } + Cnd cnd = Cnd.where(TourLedger::getDelFlag, "=", false) + .and(TourLedger::getMatterId, "=", matterId) + .and(TourLedger::getJobNo, "=", currentJobNo()); + cnd.desc(TourLedger::getCreatedAt); + return tourLedgerService.fetch(cnd); + } + + private CycleAllowedTimes calculateCycleAllowedTimes(TourMatter matter, TourSetting setting, TourLedger oldLedger) { + int allowedTimes = setting == null || setting.getCycleAllowedTimes() == null ? 0 : setting.getCycleAllowedTimes(); + int endYear = LocalDate.now().getYear(); + int startYear = setting == null || setting.getCycleStartYear() == null ? (matter == null || matter.getYear() == null ? endYear : matter.getYear()) : setting.getCycleStartYear(); + String excludeLedgerId = oldLedger == null ? null : oldLedger.getId(); + int joinedTimes = allowedTimes <= 0 ? 0 : countCycleJoinedTimes(currentJobNo(), startYear, endYear, excludeLedgerId); + return new CycleAllowedTimes(startYear, endYear, allowedTimes, joinedTimes); + } + + private int countCycleJoinedTimes(String jobNo, int startYear, int endYear, String excludeLedgerId) { + if (StrUtil.isBlank(jobNo)) { + return 0; + } + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger + WHERE delFlag = 0 + AND joined = 1 + AND jobNo = @jobNo + AND `year` >= @startYear + AND `year` <= @endYear + """ + excludeSql); + sql.setParam("jobNo", jobNo); + sql.setParam("startYear", startYear); + sql.setParam("endYear", endYear); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private String buildCycleAllowedTimesNotice(CycleAllowedTimes cycleAllowedTimes) { + return "您在 " + cycleAllowedTimes.startYear() + " 至 " + cycleAllowedTimes.endYear() + + " 周期内已参加 " + cycleAllowedTimes.joinedTimes() + " 次,周期允许 " + + cycleAllowedTimes.allowedTimes() + " 次,不能报名"; + } + + private CycleTotalCost calculateCycleTotalCost(TourMatter matter, TourSetting setting, TourLine line, TourLedger oldLedger) { + int totalCost = setting == null || setting.getCycleTotalCost() == null ? 0 : setting.getCycleTotalCost(); + int endYear = LocalDate.now().getYear(); + int startYear = setting == null || setting.getCycleStartYear() == null ? (matter == null || matter.getYear() == null ? endYear : matter.getYear()) : setting.getCycleStartYear(); + String excludeLedgerId = oldLedger == null ? null : oldLedger.getId(); + int joinedCost = totalCost <= 0 ? 0 : countCycleJoinedCost(currentJobNo(), startYear, endYear, excludeLedgerId); + int currentCost = totalCost <= 0 ? 0 : getLineActivityCost(line); + return new CycleTotalCost(startYear, endYear, totalCost, joinedCost, currentCost); + } + + private int countCycleJoinedCost(String jobNo, int startYear, int endYear, String excludeLedgerId) { + if (StrUtil.isBlank(jobNo)) { + return 0; + } + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT IFNULL(SUM(IFNULL(lot.activityCost, 0)), 0) + FROM tour_ledger t + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + LEFT JOIN tour_setting_lot lot ON lot.id = l.lotId AND lot.delFlag = 0 + WHERE t.delFlag = 0 + AND t.joined = 1 + AND t.jobNo = @jobNo + AND t.`year` >= @startYear + AND t.`year` <= @endYear + """ + excludeSql); + sql.setParam("jobNo", jobNo); + sql.setParam("startYear", startYear); + sql.setParam("endYear", endYear); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private int getLineActivityCost(TourLine line) { + if (line == null || StrUtil.isBlank(line.getLotId())) { + return 0; + } + Sql sql = Sqls.create(""" + SELECT IFNULL(activityCost, 0) AS activityCost + FROM tour_setting_lot + WHERE delFlag = 0 + AND id = @lotId + """); + sql.setParam("lotId", line.getLotId()); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private String buildCycleTotalCostNotice(CycleTotalCost cycleTotalCost) { + return "您在 " + cycleTotalCost.startYear() + " 至 " + cycleTotalCost.endYear() + + " 周期内已参加线路费用合计 " + cycleTotalCost.joinedCost() + + " 元,本次线路费用 " + cycleTotalCost.currentCost() + + " 元,周期内总费用 " + cycleTotalCost.totalCost() + " 元,超过限制,不能报名"; + } + + private OutProvinceQuota calculateOutProvinceQuota(TourMatter matter, TourSetting setting, TourLedger oldLedger) { + String ratioType = normalizeOutProvinceRatioType(setting == null ? null : setting.getOutProvinceRatioType()); + BigDecimal originalRatio = setting == null ? null : setting.getOutProvinceRatio(); + boolean oneThirdRatio = isOneThirdRatio(originalRatio); + BigDecimal ratio = oneThirdRatio ? BigDecimal.ONE.divide(BigDecimal.valueOf(3), 12, RoundingMode.HALF_UP) + : (originalRatio == null ? BigDecimal.ZERO : originalRatio); + int year = matter == null || matter.getYear() == null ? LocalDate.now().getYear() : matter.getYear(); + String excludeLedgerId = oldLedger == null ? null : oldLedger.getId(); + int totalSignupPeople = countYearSignupPeople(year, excludeLedgerId); + int outProvinceSignupPeople = countYearOutProvinceSignupPeople(year, excludeLedgerId); + int basePeople; + if ("可参加教职工人数".equals(ratioType)) { + Integer groupId = parseInteger(setting == null ? null : setting.getActivityGroupId()); + basePeople = countActivityGroupPeople(groupId); + } else if ("固定人数".equals(ratioType)) { + basePeople = setting == null || setting.getOutProvinceFixedPeople() == null ? 0 : setting.getOutProvinceFixedPeople(); + } else { + basePeople = totalSignupPeople; + } + int quotaPeople = oneThirdRatio + ? basePeople / 3 + : ratio.multiply(BigDecimal.valueOf(basePeople)).setScale(0, RoundingMode.FLOOR).intValue(); + int allowPeople = quotaPeople - outProvinceSignupPeople; + return new OutProvinceQuota(ratioType, ratio, basePeople, totalSignupPeople, outProvinceSignupPeople, allowPeople); + } + + private boolean isOneThirdRatio(BigDecimal ratio) { + if (ratio == null) { + return false; + } + return ratio.compareTo(BigDecimal.valueOf(0.33)) >= 0 && ratio.compareTo(BigDecimal.valueOf(0.3333)) <= 0; + } + + private String normalizeOutProvinceRatioType(String ratioType) { + if ("当年参加人数".equals(ratioType) || StrUtil.isBlank(ratioType)) { + return "当年报名人数"; + } + if ("可参加教职工人数".equals(ratioType) || "固定人数".equals(ratioType)) { + return ratioType; + } + 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())) { + return "当前报名人数是" + quota.totalSignupPeople() + "人,已经报名省外线路人数是" + + quota.outProvinceSignupPeople() + "人,按照省外人数不超过" + quota.ratioText() + + "的规则,当前时间允许报名人数是" + quota.allowPeople() + "人," + suffix; + } + if ("可参加教职工人数".equals(quota.ratioType())) { + return "今年有权限报名人数是" + quota.basePeople() + "人,已经报名省外线路人数是" + + quota.outProvinceSignupPeople() + "人,按照省外人数不超过" + quota.ratioText() + + "的规则,当前时间允许报名人数是" + quota.allowPeople() + "人," + suffix; + } + return "按照省外人数不超过" + quota.ratioText() + + "的规则,当前时间允许报名人数是" + quota.allowPeople() + "人," + suffix; + } + + private int countYearSignupPeople(int year, String excludeLedgerId) { + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + WHERE t.delFlag = 0 + AND t.`year` = @year + AND (ins.id IS NULL OR ins.state = @finishedState) + """ + excludeSql); + sql.setParam("year", year); + sql.setParam("finishedState", ProcessInstanceStateEnum.FINISHED.getCode()); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private int countYearOutProvinceSignupPeople(int year, String excludeLedgerId) { + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + WHERE t.delFlag = 0 + AND t.`year` = @year + AND t.lineType IN ('省外线路', '省外') + AND (ins.id IS NULL OR ins.state = @finishedState) + """ + excludeSql); + sql.setParam("year", year); + sql.setParam("finishedState", ProcessInstanceStateEnum.FINISHED.getCode()); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private int countActivityGroupPeople(Integer groupId) { + if (groupId == null) { + return 0; + } + Sql sql = Sqls.create("SELECT COUNT(1) FROM (" + activityBasicScopeService.buildGroupUserIdSubSqlText(groupId) + ") scope_users"); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private int countSignupPeople(String matterId, String excludeLedgerId) { + if (StrUtil.isBlank(matterId)) { + return 0; + } + String excludeSql = StrUtil.isBlank(excludeLedgerId) ? "" : " AND t.id <> @excludeLedgerId"; + Sql sql = Sqls.create(""" + SELECT IFNULL(SUM(1 + IFNULL(f.familyCount, 0)), 0) AS signupPeople + FROM tour_ledger t + LEFT JOIN ( + SELECT ledgerId, COUNT(1) AS familyCount + FROM tour_ledger_family + WHERE delFlag = 0 + GROUP BY ledgerId + ) f ON f.ledgerId = t.id + WHERE t.delFlag = 0 + AND t.matterId = @matterId + """ + excludeSql); + sql.setParam("matterId", matterId); + if (StrUtil.isNotBlank(excludeLedgerId)) { + sql.setParam("excludeLedgerId", excludeLedgerId); + } + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt(); + } + + private String buildSelectedLineMessage(TourLedger ledger) { + String selectedLineName = getLedgerLineName(ledger); + return "您已经选择【" + selectedLineName + "】,取消后再重新选择。"; + } + + private String getLedgerLineName(TourLedger ledger) { + if (ledger == null) { + return ""; + } + if (StrUtil.isNotBlank(ledger.getLineName())) { + return ledger.getLineName(); + } + if (StrUtil.isBlank(ledger.getLineId())) { + return ""; + } + TourLine line = tourLedgerService.dao().fetch(TourLine.class, ledger.getLineId()); + return line == null ? "" : defaultIfBlank(line.getLineName(), ""); + } + + private record CycleAllowedTimes(int startYear, + int endYear, + int allowedTimes, + int joinedTimes) { + boolean canApply() { + return allowedTimes <= 0 || joinedTimes < allowedTimes; + } + } + + private record CycleTotalCost(int startYear, + int endYear, + int totalCost, + int joinedCost, + int currentCost) { + boolean canApply() { + return totalCost <= 0 || joinedCost + currentCost <= totalCost; + } + } + + private boolean isOutProvinceLine(String lineType) { + return "省外线路".equals(lineType) || "省外".equals(lineType); + } + + private boolean isInProvinceLine(String lineType) { + return "省内线路".equals(lineType) || "省内".equals(lineType); + } + + private boolean allowOverCostReimbursement(TourLine line) { + if (line == null || StrUtil.isBlank(line.getLotId())) { + return false; + } + Sql sql = Sqls.create(""" + SELECT IFNULL(allowOverReimbursement, 0) AS allowOverReimbursement + FROM tour_setting_lot + WHERE id = @lotId + """); + sql.setParam("lotId", line.getLotId()); + sql.setCallback(Sqls.callback.map()); + tourLedgerService.dao().execute(sql); + NutMap lot = sql.getObject(NutMap.class); + return lot != null && lot.getBoolean("allowOverReimbursement", false); + } + + private record OutProvinceQuota(String ratioType, + BigDecimal ratio, + int basePeople, + int totalSignupPeople, + int outProvinceSignupPeople, + int allowPeople) { + boolean canApply() { + return allowPeople > 0; + } + + String ratioText() { + if (ratio == null || BigDecimal.ZERO.compareTo(ratio) == 0) { + return "0"; + } + BigDecimal oneThird = BigDecimal.ONE.divide(BigDecimal.valueOf(3), 8, RoundingMode.HALF_UP); + if (ratio.subtract(oneThird).abs().compareTo(BigDecimal.valueOf(0.01)) <= 0) { + return "1/3"; + } + return ratio.stripTrailingZeros().toPlainString(); + } + } + + private Integer parseInteger(String value) { + if (StrUtil.isBlank(value)) { + return null; + } + try { + return Integer.valueOf(value); + } catch (NumberFormatException e) { + return null; + } + } + + private String normalizeDateTime(String value, boolean endOfDay) { + if (StrUtil.isBlank(value)) { + return ""; + } + String trimmed = value.trim(); + if (trimmed.length() == 10) { + return trimmed + (endOfDay ? " 23:59:59" : " 00:00:00"); + } + if (trimmed.length() == 16) { + return trimmed + ":00"; + } + return trimmed; + } + + private void fillStaffInfo(TourLedger ledger) { + View_user user = tourLedgerService.dao().fetch(View_user.class, Cnd.where(View_user::getId, "=", SecurityUtil.getUserId())); + ledger.setJobNo(user == null ? SecurityUtil.getUserLoginname() : defaultIfBlank(user.getLoginname(), SecurityUtil.getUserLoginname())); + ledger.setUserName(user == null ? SecurityUtil.getUserUsername() : defaultIfBlank(user.getUsername(), SecurityUtil.getUserUsername())); + ledger.setGender(user == null ? ledger.getGender() : defaultIfBlank(user.getSex(), ledger.getGender())); + ledger.setIdCard(user == null ? ledger.getIdCard() : defaultIfBlank(user.getIdCard(), ledger.getIdCard())); + ledger.setUnitId(user == null ? SecurityUtil.getUnitId() : defaultIfBlank(user.getUnitId(), SecurityUtil.getUnitId())); + ledger.setUnitName(user == null ? ledger.getUnitName() : defaultIfBlank(user.getUnitName(), ledger.getUnitName())); + ledger.setUnionId(user == null ? SecurityUtil.getUnionId() : defaultIfBlank(user.getUnionId(), SecurityUtil.getUnionId())); + ledger.setUnionName(user == null ? ledger.getUnionName() : defaultIfBlank(user.getUnionName(), ledger.getUnionName())); + } + + private List parseFamilies(String families) { + if (StrUtil.isBlank(families)) { + return Collections.emptyList(); + } + List list = Json.fromJsonAsList(TourLedgerFamily.class, families); + if (Lang.isEmpty(list)) { + return Collections.emptyList(); + } + return list.stream() + .filter(item -> item != null && StrUtil.isNotBlank(item.getFamilyName())) + .collect(Collectors.toList()); + } + + private TourLedgerDirectRelative parseDirectRelative(String directRelative) { + if (StrUtil.isBlank(directRelative)) { + return null; + } + return Json.fromJson(TourLedgerDirectRelative.class, directRelative); + } + + private void clearBedInfo(TourLedger ledger, List familyList) { + if (ledger != null) { + ledger.setBedType(""); + ledger.setBedInfo(""); + ledger.setIntendedRoommate(""); + } + if (Lang.isNotEmpty(familyList)) { + familyList.forEach(item -> { + item.setBedType(""); + item.setBedInfo(""); + item.setIntendedRoommate(""); + }); + } + } + + private NutMap getSignupProcessInfo(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getId())) { + return NutMap.NEW(); + } + Sql sql = Sqls.create(""" + SELECT + ins.id AS instanceId, + ins.state AS instanceState, + task.taskName AS taskKey, + (SELECT MAX(st.id) FROM wf_process_task st WHERE st.processInstanceId = ins.id AND st.taskName = 'startTask' AND st.taskState IN (10, 20)) AS startTaskId, + IF((SELECT taskName FROM wf_process_task tt WHERE tt.id = task.taskParentId) = 'startTask', 1, 0) AS canRevoke + FROM wf_process_instance ins + LEFT JOIN wf_process_task task ON task.processInstanceId = ins.id AND task.taskState = @doingState + WHERE ins.businessNo = @ledgerId + AND ins.state <> @abandonState + ORDER BY ins.createdAt DESC + LIMIT 1 + """); + sql.setParam("ledgerId", ledger.getId()); + sql.setParam("doingState", ProcessTaskStateEnum.DOING.getCode()); + sql.setParam("abandonState", ProcessInstanceStateEnum.ABANDON.getCode()); + sql.setCallback(Sqls.callback.map()); + tourLedgerService.dao().execute(sql); + NutMap process = sql.getObject(NutMap.class); + return process == null ? NutMap.NEW() : process; + } + + private Result startOrContinueSignupApprovalWorkflow(TourLedger ledger, TourLedgerDirectRelative directRelativeInfo, TourMatter matter, TourLine line) { + ProcessInstance instance = tourLedgerService.dao().fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", ledger.getId()) + .and(ProcessInstance::getState, "<>", ProcessInstanceStateEnum.ABANDON.getCode()) + .desc(ProcessInstance::getCreatedAt)); + Dict args = buildSignupApprovalArgs(ledger, directRelativeInfo, matter, line); + if (instance == null) { + ProcessInstance newInstance = flowEngine.startProcessInstanceByKey(DIRECT_FAMILY_WORKFLOW_KEY, ledger.getId(), SecurityUtil.getUserId(), args); + List doingTaskList = flowEngine.processTaskService().getDoingTaskList(newInstance.getId(), null); + for (ProcessTask task : doingTaskList) { + flowEngine.executeProcessTask(task.getId(), SecurityUtil.getUserId(), args); + } + return null; + } + if (!ProcessInstanceStateEnum.DOING.getCode().equals(instance.getState())) { + return Result.error("当前审批流程状态不允许修改提交"); + } + ProcessTask startTask = tourLedgerService.dao().fetch(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", instance.getId()) + .and(ProcessTask::getTaskName, "=", "startTask") + .and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.DOING.getCode())); + if (startTask == null) { + return Result.error("当前审批流程节点不允许修改提交"); + } + flowEngine.executeProcessTask(startTask.getId(), SecurityUtil.getUserId(), args); + return null; + } + + private Result checkSignupApprovalWorkflowCanSubmit(String ledgerId) { + ProcessInstance instance = tourLedgerService.dao().fetch(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "=", ledgerId) + .and(ProcessInstance::getState, "<>", ProcessInstanceStateEnum.ABANDON.getCode()) + .desc(ProcessInstance::getCreatedAt)); + if (instance == null) { + return null; + } + if (!ProcessInstanceStateEnum.DOING.getCode().equals(instance.getState())) { + return Result.error("当前审批流程状态不允许修改提交"); + } + ProcessTask startTask = tourLedgerService.dao().fetch(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "=", instance.getId()) + .and(ProcessTask::getTaskName, "=", "startTask") + .and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.DOING.getCode())); + if (startTask == null) { + return Result.error("当前审批流程节点不允许修改提交"); + } + return null; + } + + private Dict buildSignupApprovalArgs(TourLedger ledger, TourLedgerDirectRelative directRelativeInfo, TourMatter matter, TourLine line) { + Dict args = Dict.create(); + args.set(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.APPLY.getCode()); + args.set(FlowConst.FORM_DATA, NutMap.NEW() + .addv("ledger", ledger) + .addv("directRelative", directRelativeInfo) + .addv("matter", matter) + .addv("line", line)); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "matterId", ledger.getMatterId()); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "lineId", ledger.getLineId()); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "lineName", ledger.getLineName()); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "directFamilyUnitLine", line != null && Boolean.TRUE.equals(line.getDirectFamilyUnitLine())); + args.set(FlowConst.TASK_FORM_DATA_PREFIX + "overCostReimbursed", Boolean.TRUE.equals(ledger.getOverCostReimbursed())); + return args; + } + + private String defaultIfBlank(String value, String defaultValue) { + return StrUtil.isBlank(value) ? defaultValue : value; + } + + private String currentJobNo() { + View_user user = tourMatterService.dao().fetch(View_user.class, Cnd.where(View_user::getId, "=", SecurityUtil.getUserId())); + return user == null ? SecurityUtil.getUserLoginname() : defaultIfBlank(user.getLoginname(), SecurityUtil.getUserLoginname()); + } + + private String getOrderColumn(String orderName) { + if ("lineName".equals(orderName)) { + return "l.lineName"; + } + if ("travelPeriod".equals(orderName)) { + return "m.travelStartTime"; + } + if ("matterName".equals(orderName)) { + return "m.matterName"; + } + if ("unionName".equals(orderName)) { + return "u.name"; + } + if ("lineType".equals(orderName)) { + return "l.lineType"; + } + if ("minGroupPeople".equals(orderName)) { + return "m.minGroupPeople"; + } + if ("maxGroupPeople".equals(orderName)) { + return "m.maxGroupPeople"; + } + if ("signupCount".equals(orderName)) { + return "signupCount"; + } + return "l.lineName"; + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourTravelAgencyController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourTravelAgencyController.java new file mode 100644 index 00000000..900a571a --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourTravelAgencyController.java @@ -0,0 +1,160 @@ +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.page.Pagination; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.result.Result; +import com.budwk.app.zhgh.dayofficework.tour.models.TourTravelAgency; +import com.budwk.app.zhgh.dayofficework.tour.service.TourTravelAgencyService; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +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/travelAgency") +public class TourTravelAgencyController { + + private static final String MOBILE_PATTERN = "^1[3-9]\\d{9}$"; + private static final String EMAIL_PATTERN = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$"; + + @Inject + private TourTravelAgencyService travelAgencyService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/travelAgency/index.html") + @SaCheckPermission("tour.travelAgency") + public void index() { + } + + @At + @SaCheckPermission("tour.travelAgency") + public Result pageData(PageForm pageForm, Integer year, String agencyName, String contactName, String contactPhone) { + Cnd cnd = Cnd.NEW(); + cnd.andEX(TourTravelAgency::getYear, "=", year); + cnd.and(Cnd.likeEX(TourTravelAgency::getAgencyName, agencyName)); + cnd.and(Cnd.likeEX(TourTravelAgency::getContactName, contactName)); + cnd.and(Cnd.likeEX(TourTravelAgency::getContactPhone, contactPhone)); + applyOrder(cnd, pageForm); + Pagination pagination = travelAgencyService.listPage( + pageForm.getPageNumber(), + pageForm.getPageSize(), + TourTravelAgency.class, + cnd + ); + return Result.success(pagination); + } + + @At + @SaCheckPermission("tour.travelAgency") + public Result detail(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + TourTravelAgency agency = travelAgencyService.fetch(id); + return agency == null ? Result.error("旅行社不存在") : Result.success(agency); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.travelAgency") + @SLog(type = "tour", tag = "旅行社管理", msg = "保存旅行社信息") + public Result doSubmit(TourTravelAgency agency) { + Result checkResult = check(agency); + if (checkResult != null) { + return checkResult; + } + + Cnd sameCodeCnd = Cnd.where(TourTravelAgency::getYear, "=", agency.getYear()) + .and(TourTravelAgency::getAgencyCode, "=", agency.getAgencyCode()); + if (StrUtil.isNotBlank(agency.getId())) { + sameCodeCnd.and(TourTravelAgency::getId, "<>", agency.getId()); + } + if (travelAgencyService.count(sameCodeCnd) > 0) { + return Result.error("同年度下旅行社编号已存在"); + } + if (agency.getEnabled() == null) { + agency.setEnabled(true); + } + if (StrUtil.isBlank(agency.getId())) { + travelAgencyService.insert(agency); + } else { + travelAgencyService.updateIgnoreNull(agency); + } + return Result.success(); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.travelAgency") + @SLog(type = "tour", tag = "旅行社管理", msg = "删除旅行社信息") + public Result doDelete(String id) { + if (StrUtil.isBlank(id)) { + return Result.error("参数错误"); + } + travelAgencyService.delete(id); + return Result.success(); + } + + private Result check(TourTravelAgency agency) { + if (agency == null) { + return Result.error("参数错误"); + } + if (agency.getYear() == null) { + return Result.error("年度不能为空"); + } + if (StrUtil.isBlank(agency.getAgencyName())) { + return Result.error("旅行社名称不能为空"); + } + if (StrUtil.isBlank(agency.getAgencyCode())) { + return Result.error("旅行社编号不能为空"); + } + if (StrUtil.isBlank(agency.getContactName())) { + return Result.error("联系人不能为空"); + } + if (StrUtil.isBlank(agency.getContactPhone())) { + return Result.error("联系人手机不能为空"); + } + if (!agency.getContactPhone().matches(MOBILE_PATTERN)) { + return Result.error("联系人手机格式不正确"); + } + if (StrUtil.isBlank(agency.getEmail())) { + return Result.error("邮箱不能为空"); + } + if (!agency.getEmail().matches(EMAIL_PATTERN)) { + return Result.error("邮箱格式不正确"); + } + return null; + } + + private void applyOrder(Cnd cnd, PageForm pageForm) { + String orderName = pageForm.getPageOrderName(); + String orderBy = pageForm.getPageOrderBy(); + if (StrUtil.isBlank(orderName)) { + cnd.asc(TourTravelAgency::getYear).asc(TourTravelAgency::getAgencyCode).desc(TourTravelAgency::getCreatedAt); + return; + } + boolean descending = "descending".equals(orderBy); + if ("year".equals(orderName)) { + if (descending) { + cnd.desc(TourTravelAgency::getYear); + } else { + cnd.asc(TourTravelAgency::getYear); + } + } else if ("agencyCode".equals(orderName)) { + if (descending) { + cnd.desc(TourTravelAgency::getAgencyCode); + } else { + cnd.asc(TourTravelAgency::getAgencyCode); + } + } else { + cnd.asc(TourTravelAgency::getYear).asc(TourTravelAgency::getAgencyCode).desc(TourTravelAgency::getCreatedAt); + } + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourUnionApprovalController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourUnionApprovalController.java new file mode 100644 index 00000000..4a222e60 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourUnionApprovalController.java @@ -0,0 +1,581 @@ +package com.budwk.app.zhgh.dayofficework.tour.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaMode; +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.result.Result; +import com.budwk.app.flow.enums.ProcessTaskStateEnum; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService; +import org.nutz.dao.Cnd; +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; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +import java.time.LocalDate; +import java.util.List; + +@IocBean +@Ok("json:full") +@At("/platform/tour/unionApproval") +public class TourUnionApprovalController { + + private static final String WORKFLOW_KEY = "LXYBZXQSXL"; + private static final String TASK_DISPLAY_NAME = "分工会审核"; + + @Inject + private TourLedgerService tourLedgerService; + + @Inject + private TourLedgerFamilyService tourLedgerFamilyService; + + @Inject + private TourLedgerDirectRelativeService tourLedgerDirectRelativeService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/unionApproval/index.html") + @SaCheckPermission(value = {"tour.unionApproval", "h5.tour.unionApproval"}, mode = SaMode.OR) + public void index() { + } + + @At("/h5") + @Ok("beetl:/platform/zhghh5/dayofficework/tour/unionApproval/index.html") + @SaCheckPermission(value = {"tour.unionApproval", "h5.tour.unionApproval"}, mode = SaMode.OR) + public void h5() { + } + + @At + @SaCheckPermission(value = {"tour.unionApproval", "h5.tour.unionApproval"}, mode = SaMode.OR) + public Result pageData(PageForm pageForm, Boolean audit, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType); + + Sql countSql = Sqls.create(""" + SELECT COUNT(DISTINCT task.id) + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + 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 + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + t.*, + ins.id AS instanceId, + ins.businessNo, + ins.state AS instanceState, + ins.variable AS instanceVariable, + ins.processDefineId AS instanceProcessDefineId, + task.id AS taskId, + task.taskName AS taskKey, + task.displayName AS taskName, + task.taskType, + task.performType AS taskPerformType, + task.taskState, + task.finishTime, + task.taskParentId, + task.variable AS taskVariable, + IFNULL(GROUP_CONCAT(DISTINCT nt.displayName), '结束') AS curTaskName, + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS currentLineName, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE tp.travelPeriod + END AS travelPeriod, + IFNULL(f.familyCount, 0) AS familyCount + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10 + 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 sys_union mu ON mu.id = m.unionId + 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 task.id + ORDER BY $orderColumn $orderBy, task.createdAt DESC, t.signupTime DESC, t.createdAt DESC + """); + listSql.setCondition(cnd); + listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "ascending".equals(pageForm.getPageOrderBy()) ? "asc" : "desc"); + listSql.setPager(tourLedgerService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(listSql); + + List list = listSql.getList(NutMap.class); + list.forEach(item -> item.put("lineName", item.getString("currentLineName"))); + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list); + return Result.success(pagination); + } + + @At + @SaCheckPermission(value = {"tour.unionApproval", "h5.tour.unionApproval"}, mode = SaMode.OR) + public Result detail(String id) { + TourLedger ledger = fetchAuditLedger(id); + if (ledger == null) { + return Result.error("报名记录不存在或无权查看"); + } + ledger.setLineName(getCurrentLineName(ledger)); + Cnd familyCnd = Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId()) + .and(TourLedgerFamily::getDelFlag, "=", false); + familyCnd.asc(TourLedgerFamily::getCreatedAt); + TourLedgerDirectRelative directRelative = tourLedgerDirectRelativeService.fetch( + Cnd.where(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId()) + .and(TourLedgerDirectRelative::getDelFlag, "=", false)); + NutMap signupConfig = getSignupConfig(ledger); + return Result.success(NutMap.NEW() + .addv("ledger", ledger) + .addv("travelPeriod", getTravelPeriod(ledger)) + .addv("families", tourLedgerFamilyService.query(familyCnd)) + .addv("directRelative", directRelative) + .addv("allowFamily", signupConfig.getBoolean("allowFamily", false)) + .addv("fillBedInfo", signupConfig.getBoolean("fillBedInfo", true)) + .addv("directFamilyUnitLine", signupConfig.getBoolean("directFamilyUnitLine", false))); + } + + @At + @SaCheckPermission(value = {"tour.unionApproval", "h5.tour.unionApproval"}, mode = SaMode.OR) + public Result unionOptions(Boolean audit, Integer startYear, Integer endYear, String keyword, String lineId, String travelPeriod, String lineType) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, null, lineId, travelPeriod, lineType); + cnd.and("t.unionId", "IS NOT", null); + cnd.and("t.unionId", "<>", ""); + + Sql sql = Sqls.create(""" + SELECT DISTINCT + t.unionId AS id, + COALESCE(NULLIF(u.name, ''), t.unionName) AS name, + u.unionCode + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + 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 sys_union u ON u.id = t.unionId + $condition + ORDER BY u.unionCode ASC, name ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission(value = {"tour.unionApproval", "h5.tour.unionApproval"}, mode = SaMode.OR) + public Result lineOptions(Boolean audit, Integer startYear, Integer endYear, String travelPeriod, String lineType, String unionId, String keyword) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, null, travelPeriod, lineType); + cnd.and("t.lineName", "IS NOT", null); + cnd.and("t.lineName", "<>", ""); + + Sql sql = Sqls.create(""" + SELECT DISTINCT + CASE + WHEN IFNULL(t.lineId, '') <> '' THEN CONCAT(t.lineId, '|', IFNULL(m.unionId, '')) + ELSE CONCAT('legacy:', t.lineName, '|', IFNULL(m.unionId, '')) + END AS lineId, + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS lineName + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + 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 sys_union mu ON mu.id = m.unionId + $condition + ORDER BY lineName ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission(value = {"tour.unionApproval", "h5.tour.unionApproval"}, mode = SaMode.OR) + public Result travelPeriodOptions(Boolean audit, Integer startYear, Integer endYear, String lineId, String lineType, String unionId, String keyword) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, lineId, null, lineType); + cnd.and("m.travelStartTime", "IS NOT", null); + cnd.and("m.travelStartTime", "<>", ""); + cnd.and("m.travelEndTime", "IS NOT", null); + cnd.and("m.travelEndTime", "<>", ""); + + Sql sql = Sqls.create(""" + SELECT DISTINCT CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) AS travelPeriod + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + 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 + $condition + ORDER BY travelPeriod ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission(value = {"tour.unionApproval", "h5.tour.unionApproval"}, mode = SaMode.OR) + public Result lineTypeOptions(Boolean audit, Integer startYear, Integer endYear, String lineId, String travelPeriod, String unionId, String keyword) { + Cnd cnd = buildAuditCnd(audit, startYear, endYear, keyword, unionId, lineId, travelPeriod, null); + cnd.and("t.lineType", "IS NOT", null); + cnd.and("t.lineType", "<>", ""); + + Sql sql = Sqls.create(""" + SELECT DISTINCT t.lineType + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + 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 + $condition + ORDER BY t.lineType ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + private Cnd buildAuditCnd(Boolean audit, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.and("def.name", "=", WORKFLOW_KEY); + cnd.and("task.displayName", "=", TASK_DISPLAY_NAME); + cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId())); + appendAuditStateFilter(cnd, audit); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.andEX("t.unionId", "=", unionId); + cnd.andEX("t.lineType", "=", lineType); + appendLineFilter(cnd, lineId); + appendTravelPeriodFilter(cnd, travelPeriod); + appendKeywordFilter(cnd, keyword); + return cnd; + } + + private void appendAuditStateFilter(Cnd cnd, Boolean audit) { + if (Boolean.TRUE.equals(audit)) { + cnd.and("task.taskState", "in", List.of( + ProcessTaskStateEnum.FINISHED.getCode(), + ProcessTaskStateEnum.WITHDRAW.getCode(), + ProcessTaskStateEnum.INTERRUPT.getCode())); + return; + } + cnd.and("task.taskState", "=", ProcessTaskStateEnum.DOING.getCode()); + } + + private void appendKeywordFilter(Cnd cnd, String keyword) { + if (StrUtil.isBlank(keyword)) { + return; + } + SqlExpressionGroup group = new SqlExpressionGroup(); + group.orLike("t.userName", keyword.trim()); + group.orLike("t.jobNo", keyword.trim()); + group.orLike("t.lineName", keyword.trim()); + group.orLike("l.lineName", keyword.trim()); + cnd.and(group); + } + + private void appendTravelPeriodFilter(Cnd cnd, String travelPeriod) { + if (StrUtil.isBlank(travelPeriod)) { + return; + } + cnd.and("CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)", "=", travelPeriod); + } + + private void appendLineFilter(Cnd cnd, String lineId) { + if (StrUtil.isBlank(lineId)) { + return; + } + LineFilter lineFilter = parseLineFilter(lineId); + if (lineFilter.getLineId().startsWith("legacy:")) { + SqlExpressionGroup legacyGroup = new SqlExpressionGroup(); + legacyGroup.or("t.lineId", "IS", null); + legacyGroup.or("t.lineId", "=", ""); + cnd.and(legacyGroup); + cnd.and("t.lineName", "=", lineFilter.getLineId().substring("legacy:".length())); + } else { + cnd.and("t.lineId", "=", lineFilter.getLineId()); + } + appendMatterUnionFilter(cnd, lineFilter.getUnionId()); + } + + private LineFilter parseLineFilter(String value) { + int delimiterIndex = value.indexOf("|"); + if (delimiterIndex < 0) { + return new LineFilter(value, null); + } + return new LineFilter(value.substring(0, delimiterIndex), value.substring(delimiterIndex + 1)); + } + + private void appendMatterUnionFilter(Cnd cnd, String unionId) { + if (unionId == null) { + return; + } + if (StrUtil.isBlank(unionId)) { + SqlExpressionGroup group = new SqlExpressionGroup(); + group.or("m.unionId", "IS", null); + group.or("m.unionId", "=", ""); + cnd.and(group); + return; + } + cnd.and("m.unionId", "=", unionId); + } + + private String getOrderColumn(String orderName) { + if ("jobNo".equals(orderName)) { + return "t.jobNo"; + } + if ("userName".equals(orderName)) { + return "t.userName"; + } + if ("lineName".equals(orderName)) { + return """ + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END + """; + } + if ("travelPeriod".equals(orderName)) { + return "COALESCE(m.travelStartTime, tp.travelPeriod)"; + } + if ("lineType".equals(orderName)) { + return "t.lineType"; + } + if ("signupTime".equals(orderName)) { + return "t.signupTime"; + } + if ("familyCount".equals(orderName)) { + return "familyCount"; + } + if ("instanceState".equals(orderName)) { + return "ins.state"; + } + return "task.createdAt"; + } + + private TourLedger fetchAuditLedger(String id) { + if (StrUtil.isBlank(id)) { + return null; + } + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM wf_process_task task + LEFT JOIN wf_process_instance ins ON ins.id = task.processInstanceId + LEFT JOIN wf_process_define def ON def.id = ins.processDefineId + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = task.id + INNER JOIN tour_ledger t ON t.id = ins.businessNo + WHERE t.delFlag = 0 + AND t.id = @id + AND def.name = @workflowKey + AND task.displayName = @taskDisplayName + AND ta.actorId = @actorId + """); + sql.setParam("id", id); + sql.setParam("workflowKey", WORKFLOW_KEY); + sql.setParam("taskDisplayName", TASK_DISPLAY_NAME); + sql.setParam("actorId", SecurityUtil.getUserId()); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt() > 0 ? tourLedgerService.fetch(id) : null; + } + + private String getCurrentLineName(TourLedger ledger) { + if (ledger == null) { + return ""; + } + Sql sql = Sqls.create(""" + SELECT CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS lineName + FROM tour_ledger t + 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 sys_union mu ON mu.id = m.unionId + WHERE t.id = @ledgerId + """); + sql.setParam("ledgerId", ledger.getId()); + sql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(sql); + return StrUtil.blankToDefault(sql.getString(), ledger.getLineName()); + } + + private String getTravelPeriod(TourLedger ledger) { + if (ledger == null || ledger.getYear() == null || StrUtil.isBlank(ledger.getLineId())) { + return ""; + } + if (StrUtil.isNotBlank(ledger.getMatterId())) { + Sql exactSql = Sqls.create(""" + SELECT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE '' + END AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND id = @matterId + """); + exactSql.setParam("matterId", ledger.getMatterId()); + exactSql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(exactSql); + return exactSql.getString(); + } + Sql sql = Sqls.create(""" + SELECT 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 `year` = @year + AND lineId = @lineId + """); + sql.setParam("year", ledger.getYear()); + sql.setParam("lineId", ledger.getLineId()); + sql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(sql); + return sql.getString(); + } + + private NutMap getSignupConfig(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getLineId())) { + return NutMap.NEW().addv("allowFamily", false).addv("fillBedInfo", true).addv("directFamilyUnitLine", false); + } + Sql sql = Sqls.create(""" + SELECT + IFNULL(s.allowFamily, 0) AS allowFamily, + IFNULL(s.fillBedInfo, 1) AS fillBedInfo, + IFNULL(l.directFamilyUnitLine, 0) AS directFamilyUnitLine + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_setting s ON s.id = m.settingId AND s.delFlag = 0 + LEFT JOIN tour_line l ON l.id = t.lineId AND l.delFlag = 0 + WHERE t.id = @ledgerId + """); + sql.setParam("ledgerId", ledger.getId()); + sql.setCallback(Sqls.callback.map()); + tourLedgerService.dao().execute(sql); + NutMap map = sql.getObject(NutMap.class); + if (map == null || map.isEmpty()) { + return NutMap.NEW().addv("allowFamily", false).addv("fillBedInfo", true).addv("directFamilyUnitLine", false); + } + return map; + } + + private static class LineFilter { + private final String lineId; + private final String unionId; + + private LineFilter(String lineId, String unionId) { + this.lineId = lineId; + this.unionId = unionId; + } + + private String getLineId() { + return lineId; + } + + private String getUnionId() { + return unionId; + } + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourUnionLedgerController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourUnionLedgerController.java new file mode 100644 index 00000000..068f047e --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourUnionLedgerController.java @@ -0,0 +1,905 @@ +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.page.Pagination; +import com.budwk.app.base.param.PageForm; +import com.budwk.app.base.result.Result; +import com.budwk.app.base.utils.CommonDownloadUtil; +import com.budwk.app.sys.models.Sys_union; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService; +import org.apache.poi.hssf.usermodel.HSSFWorkbook; +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.ss.util.CellRangeAddress; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.dao.util.cri.SqlExpressionGroup; +import org.nutz.ioc.aop.Aop; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.ioc.loader.annotation.IocBean; +import org.nutz.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; + +import javax.servlet.http.HttpServletResponse; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.List; + +@IocBean +@Ok("json:full") +@At("/platform/tour/unionledger") +public class TourUnionLedgerController { + + @Inject + private TourLedgerService tourLedgerService; + + @Inject + private TourLedgerFamilyService tourLedgerFamilyService; + + @Inject + private TourLedgerDirectRelativeService tourLedgerDirectRelativeService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/Tour/unionledger/index.html") + @SaCheckPermission("tour.unionledger") + public void index() { + } + + @At + @SaCheckPermission("tour.unionledger") + public Result pageData(PageForm pageForm, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType, String scopeType, Boolean overCostOnly) { + String currentUnionId = currentUnionId(); + if (StrUtil.isBlank(currentUnionId)) { + return Result.error("当前登录人所属工会不能为空"); + } + Cnd cnd = buildQueryCnd(currentUnionId, startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, scopeType, overCostOnly); + + Sql countSql = Sqls.create(""" + SELECT COUNT(DISTINCT t.id) + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + LEFT JOIN wf_process_task task ON task.processInstanceId = ins.id AND task.taskState = 10 + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + $condition + """); + countSql.setCondition(cnd); + countSql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(countSql); + int count = countSql.getInt(); + + Sql listSql = Sqls.create(""" + SELECT + t.*, + ins.id AS instanceId, + ins.state AS instanceState, + ins.processDefineId AS instanceProcessDefineId, + IFNULL(l.directFamilyUnitLine, 0) AS directFamilyUnitLine, + IFNULL(GROUP_CONCAT(DISTINCT task.displayName), IF(ins.id IS NULL, '', '结束')) AS curTaskName, + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS currentLineName, + IFNULL(f.familyCount, 0) AS familyCount, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE tp.travelPeriod + END AS travelPeriod + FROM tour_ledger t + LEFT JOIN wf_process_instance ins ON ins.businessNo = t.id + LEFT JOIN wf_process_task task ON task.processInstanceId = ins.id AND task.taskState = 10 + 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 sys_union mu ON mu.id = m.unionId + 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 $orderColumn $orderBy, t.`year` DESC, t.signupTime DESC, t.createdAt DESC + """); + listSql.setCondition(cnd); + listSql.setVar("orderColumn", getOrderColumn(pageForm.getPageOrderName())); + listSql.setVar("orderBy", "descending".equals(pageForm.getPageOrderBy()) ? "desc" : "asc"); + listSql.setPager(tourLedgerService.dao().createPager(pageForm.getPageNumber(), pageForm.getPageSize())); + listSql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(listSql); + + var list = listSql.getList(NutMap.class); + list.forEach(item -> item.put("lineName", item.getString("currentLineName"))); + Pagination pagination = new Pagination<>(pageForm.getPageNumber(), pageForm.getPageSize(), count, list); + return Result.success(pagination); + } + + @At + @Ok("void") + @SaCheckPermission("tour.unionledger") + public void exportOverCostSummary(PageForm pageForm, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType, String scopeType, HttpServletResponse response) { + String currentUnionId = currentUnionId(); + if (StrUtil.isBlank(currentUnionId)) { + return; + } + Cnd cnd = buildQueryCnd(currentUnionId, startYear, endYear, keyword, unionId, lineId, travelPeriod, lineType, scopeType, true); + List list = queryOverCostSummaryList(cnd, pageForm); + Workbook workbook = buildOverCostSummaryWorkbook(currentUnionName(), list); + CommonDownloadUtil.download("5天外超出部分疗休养费用由单位承担申请人员汇总表.xls", workbook, response); + } + + @At + @SaCheckPermission("tour.unionledger") + public Result detail(String id) { + TourLedger ledger = fetchScopedLedger(id); + if (ledger == null) { + return Result.error("台账记录不存在或无权查看"); + } + ledger.setLineName(getCurrentLineName(ledger)); + Cnd familyCnd = Cnd.where(TourLedgerFamily::getLedgerId, "=", id) + .and(TourLedgerFamily::getDelFlag, "=", false); + familyCnd.asc(TourLedgerFamily::getCreatedAt); + TourLedgerDirectRelative directRelative = tourLedgerDirectRelativeService.fetch( + Cnd.where(TourLedgerDirectRelative::getDelFlag, "=", false) + .and(TourLedgerDirectRelative::getLedgerId, "=", ledger.getId())); + return Result.success(NutMap.NEW() + .addv("ledger", ledger) + .addv("travelPeriod", getTravelPeriod(ledger)) + .addv("families", tourLedgerFamilyService.query(familyCnd)) + .addv("directRelative", directRelative) + .addv("fillBedInfo", isFillBedInfo(ledger)) + .addv("directFamilyUnitLine", isDirectFamilyUnitLine(ledger))); + } + + @At + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("tour.unionledger") + @SLog(type = "tour", tag = "分工会疗休养台账", msg = "删除分工会疗休养台账") + public Result doDelete(String id) { + TourLedger ledger = fetchScopedLedger(id); + if (ledger == null) { + return Result.error("台账记录不存在或无权删除"); + } + tourLedgerFamilyService.clear(Cnd.where(TourLedgerFamily::getLedgerId, "=", ledger.getId())); + tourLedgerService.clear(Cnd.where(TourLedger::getId, "=", ledger.getId())); + return Result.success(); + } + + @At + @SaCheckPermission("tour.unionledger") + public Result unionOptions(Integer startYear, Integer endYear) { + String currentUnionId = currentUnionId(); + if (StrUtil.isBlank(currentUnionId)) { + return Result.error("当前登录人所属工会不能为空"); + } + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.and("t.unionId", "IS NOT", null); + cnd.and("t.unionId", "<>", ""); + applyUnionLedgerScope(cnd, currentUnionId); + + Sql sql = Sqls.create(""" + SELECT DISTINCT + t.unionId AS id, + COALESCE(NULLIF(u.name, ''), t.unionName) AS name, + u.unionCode + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN sys_union u ON u.id = t.unionId + $condition + ORDER BY + CASE WHEN t.unionId = @currentUnionId THEN 0 ELSE 1 END, + u.unionCode ASC, + name ASC + """); + sql.setCondition(cnd); + sql.setParam("currentUnionId", currentUnionId); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.unionledger") + public Result lineOptions(Integer startYear, Integer endYear, String travelPeriod, String lineType, String unionId, String keyword, String scopeType) { + String currentUnionId = currentUnionId(); + if (StrUtil.isBlank(currentUnionId)) { + return Result.error("当前登录人所属工会不能为空"); + } + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.and("t.lineName", "IS NOT", null); + cnd.and("t.lineName", "<>", ""); + cnd.andEX("t.unionId", "=", unionId); + cnd.andEX("t.lineType", "=", lineType); + appendTravelPeriodFilter(cnd, travelPeriod); + appendScopeFilter(cnd, currentUnionId, scopeType); + appendKeywordFilter(cnd, keyword); + + Sql sql = Sqls.create(""" + SELECT DISTINCT + CASE + WHEN IFNULL(t.lineId, '') <> '' THEN CONCAT(t.lineId, '|', IFNULL(m.unionId, '')) + ELSE CONCAT('legacy:', t.lineName, '|', IFNULL(m.unionId, '')) + END AS lineId, + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS lineName + FROM tour_ledger t + 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 sys_union mu ON mu.id = m.unionId + $condition + ORDER BY lineName ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.unionledger") + public Result travelPeriodOptions(Integer startYear, Integer endYear, String lineId, String lineType, String unionId, String keyword, String scopeType) { + String currentUnionId = currentUnionId(); + if (StrUtil.isBlank(currentUnionId)) { + return Result.error("当前登录人所属工会不能为空"); + } + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.and("m.travelStartTime", "IS NOT", null); + cnd.and("m.travelStartTime", "<>", ""); + cnd.and("m.travelEndTime", "IS NOT", null); + cnd.and("m.travelEndTime", "<>", ""); + cnd.andEX("t.unionId", "=", unionId); + cnd.andEX("t.lineType", "=", lineType); + appendLineFilter(cnd, lineId); + appendScopeFilter(cnd, currentUnionId, scopeType); + appendKeywordFilter(cnd, keyword); + + Sql sql = Sqls.create(""" + SELECT DISTINCT CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) AS travelPeriod + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + $condition + ORDER BY travelPeriod ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.unionledger") + public Result lineTypeOptions(Integer startYear, Integer endYear, String lineId, String travelPeriod, String unionId, String keyword, String scopeType) { + String currentUnionId = currentUnionId(); + if (StrUtil.isBlank(currentUnionId)) { + return Result.error("当前登录人所属工会不能为空"); + } + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.and("t.lineType", "IS NOT", null); + cnd.and("t.lineType", "<>", ""); + cnd.andEX("t.unionId", "=", unionId); + appendLineFilter(cnd, lineId); + appendTravelPeriodFilter(cnd, travelPeriod); + appendScopeFilter(cnd, currentUnionId, scopeType); + appendKeywordFilter(cnd, keyword); + + Sql sql = Sqls.create(""" + SELECT DISTINCT t.lineType + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + $condition + ORDER BY t.lineType ASC + """); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + return Result.success(sql.getList(NutMap.class)); + } + + @At + @SaCheckPermission("tour.unionledger") + public Result summaryStats(Integer startYear, Integer endYear, String unionId, String lineId, String travelPeriod, String lineType) { + String currentUnionId = currentUnionId(); + if (StrUtil.isBlank(currentUnionId)) { + return Result.error("当前登录人所属工会不能为空"); + } + Cnd ownCnd = buildSummaryCnd(currentUnionId, startYear, endYear, unionId, lineId, travelPeriod, lineType, "ownUnionJoined"); + Cnd organizedCnd = buildSummaryCnd(currentUnionId, startYear, endYear, unionId, lineId, travelPeriod, lineType, "organizedLineJoined"); + + Sql ownSql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + $condition + """); + ownSql.setCondition(ownCnd); + ownSql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(ownSql); + + Sql organizedSql = Sqls.create(""" + SELECT CAST(IFNULL(SUM(1 + IFNULL(f.familyCount, 0)), 0) AS SIGNED) + FROM tour_ledger t + INNER JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN ( + SELECT ledgerId, COUNT(1) AS familyCount + FROM tour_ledger_family + WHERE delFlag = 0 + GROUP BY ledgerId + ) f ON f.ledgerId = t.id + $condition + """); + organizedSql.setCondition(organizedCnd); + organizedSql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(organizedSql); + + return Result.success(NutMap.NEW() + .addv("ownUnionJoinedStaffCount", ownSql.getInt()) + .addv("organizedLineJoinedTotalCount", organizedSql.getInt())); + } + + private List queryOverCostSummaryList(Cnd cnd, PageForm pageForm) { + Sql sql = Sqls.create(""" + SELECT + t.id, + t.userName, + t.idCard, + t.unionName, + vu.mobile, + m.travelStartTime, + m.travelEndTime, + m.estimatedCost, + CASE + WHEN IFNULL(m.travelStartTime, '') <> '' AND IFNULL(m.travelEndTime, '') <> '' + THEN CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime) + ELSE tp.travelPeriod + END AS travelPeriod, + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS currentLineName + FROM tour_ledger t + 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 sys_union mu ON mu.id = m.unionId + 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 + ORDER BY $orderColumn $orderBy, t.`year` DESC, t.signupTime DESC, t.createdAt DESC + """); + sql.setCondition(cnd); + sql.setVar("orderColumn", getOrderColumn(pageForm == null ? null : pageForm.getPageOrderName())); + sql.setVar("orderBy", pageForm != null && "ascending".equals(pageForm.getPageOrderBy()) ? "asc" : "desc"); + sql.setCallback(Sqls.callback.maps()); + tourLedgerService.dao().execute(sql); + List list = sql.getList(NutMap.class); + for (NutMap item : list) { + item.put("lineName", item.getString("currentLineName")); + item.put("totalDays", calcDays(item.getString("travelStartTime", ""), item.getString("travelEndTime", ""))); + item.put("estimatedCostText", formatAmount(item.get("estimatedCost"))); + } + return list; + } + + private Workbook buildOverCostSummaryWorkbook(String unionName, List list) { + Workbook workbook = new HSSFWorkbook(); + Sheet sheet = workbook.createSheet("汇总表"); + sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 9)); + sheet.addMergedRegion(new CellRangeAddress(1, 1, 0, 9)); + sheet.addMergedRegion(new CellRangeAddress(2, 2, 0, 9)); + + double[] widths = {5.14, 14.14, 21.29, 15.71, 27, 40, 50.43, 13.71, 22.57, 15.43}; + for (int i = 0; i < widths.length; i++) { + sheet.setColumnWidth(i, (int) (widths[i] * 256)); + } + + CellStyle attachStyle = createStyle(workbook, "宋体", (short) 12, false, HorizontalAlignment.LEFT, false, false); + CellStyle titleStyle = createStyle(workbook, "黑体", (short) 18, true, HorizontalAlignment.CENTER, false, false); + CellStyle unionStyle = createStyle(workbook, "仿宋_GB2312", (short) 14, true, HorizontalAlignment.LEFT, false, false); + unionStyle.setBorderBottom(BorderStyle.THIN); + CellStyle headerStyle = createStyle(workbook, "宋体", (short) 12, true, HorizontalAlignment.CENTER, true, true); + CellStyle bodyStyle = createStyle(workbook, "宋体", (short) 12, false, HorizontalAlignment.CENTER, false, true); + + Row row1 = sheet.createRow(0); + row1.setHeightInPoints(32); + setCell(row1, 0, "附件12", attachStyle); + fillMergedCells(row1, 1, 9, attachStyle); + + Row row2 = sheet.createRow(1); + row2.setHeightInPoints(32); + setCell(row2, 0, "5天外超出部分疗休养费用由单位承担申请人员汇总表", titleStyle); + fillMergedCells(row2, 1, 9, titleStyle); + + Row row3 = sheet.createRow(2); + row3.setHeightInPoints(32); + setCell(row3, 0, "分工会:" + StrUtil.blankToDefault(unionName, ""), unionStyle); + fillMergedCells(row3, 1, 9, unionStyle); + + String[] headers = {"序号", "姓名", "身份证号码", "电话号码", "所属分工会", "所选线路名称", "疗休养时间", "疗休养时长", "疗休养费用总额(元)", "备注"}; + Row header = sheet.createRow(3); + header.setHeightInPoints(43); + for (int i = 0; i < headers.length; i++) { + setCell(header, i, headers[i], headerStyle); + } + + int rowCount = Math.max(list == null ? 0 : list.size(), 20); + for (int i = 0; i < rowCount; i++) { + Row row = sheet.createRow(i + 4); + row.setHeightInPoints(25); + NutMap item = list != null && i < list.size() ? list.get(i) : null; + setCell(row, 0, String.valueOf(i + 1), bodyStyle); + setCell(row, 1, item == null ? "" : item.getString("userName", ""), bodyStyle); + setCell(row, 2, item == null ? "" : item.getString("idCard", ""), bodyStyle); + setCell(row, 3, item == null ? "" : item.getString("mobile", ""), bodyStyle); + setCell(row, 4, item == null ? "" : item.getString("unionName", ""), bodyStyle); + setCell(row, 5, item == null ? "" : item.getString("lineName", ""), bodyStyle); + setCell(row, 6, item == null ? "" : item.getString("travelPeriod", ""), bodyStyle); + setCell(row, 7, item == null ? "" : item.getString("totalDays", ""), bodyStyle); + setCell(row, 8, item == null ? "" : item.getString("estimatedCostText", ""), bodyStyle); + setCell(row, 9, "", bodyStyle); + } + return workbook; + } + + private CellStyle createStyle(Workbook workbook, String fontName, short fontSize, boolean bold, HorizontalAlignment alignment, boolean wrap, boolean border) { + Font font = workbook.createFont(); + font.setFontName(fontName); + font.setFontHeightInPoints(fontSize); + font.setBold(bold); + + CellStyle style = workbook.createCellStyle(); + style.setFont(font); + style.setAlignment(alignment); + style.setVerticalAlignment(VerticalAlignment.CENTER); + style.setWrapText(wrap); + if (border) { + style.setBorderLeft(BorderStyle.THIN); + style.setBorderTop(BorderStyle.THIN); + style.setBorderRight(BorderStyle.THIN); + style.setBorderBottom(BorderStyle.THIN); + } + return style; + } + + private void setCell(Row row, int col, String value, CellStyle style) { + Cell cell = row.createCell(col); + cell.setCellValue(StrUtil.blankToDefault(value, "")); + cell.setCellStyle(style); + } + + private void fillMergedCells(Row row, int startCol, int endCol, CellStyle style) { + for (int i = startCol; i <= endCol; i++) { + setCell(row, i, "", style); + } + } + + private Integer calcDays(String start, String end) { + if (StrUtil.isBlank(start) || StrUtil.isBlank(end)) { + return null; + } + try { + LocalDate startDate = LocalDate.parse(start.substring(0, 10)); + LocalDate endDate = LocalDate.parse(end.substring(0, 10)); + return Math.toIntExact(ChronoUnit.DAYS.between(startDate, endDate) + 1); + } catch (Exception e) { + return null; + } + } + + private String formatAmount(Object value) { + if (value == null) { + return ""; + } + try { + BigDecimal amount = new BigDecimal(String.valueOf(value)); + return amount.stripTrailingZeros().toPlainString(); + } catch (Exception e) { + return String.valueOf(value); + } + } + + private Cnd buildQueryCnd(String currentUnionId, Integer startYear, Integer endYear, String keyword, String unionId, String lineId, String travelPeriod, String lineType, String scopeType, Boolean overCostOnly) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.andEX("t.unionId", "=", unionId); + cnd.andEX("t.lineType", "=", lineType); + if (Boolean.TRUE.equals(overCostOnly)) { + cnd.and("t.overCostReimbursed", "=", true); + } + appendLineFilter(cnd, lineId); + appendTravelPeriodFilter(cnd, travelPeriod); + appendScopeFilter(cnd, currentUnionId, scopeType); + appendKeywordFilter(cnd, keyword); + return cnd; + } + + private Cnd buildSummaryCnd(String currentUnionId, Integer startYear, Integer endYear, String unionId, String lineId, String travelPeriod, String lineType, String scopeType) { + Cnd cnd = Cnd.NEW(); + cnd.and("t.delFlag", "=", false); + cnd.andEX("t.`year`", ">=", startYear); + cnd.andEX("t.`year`", "<=", endYear == null ? LocalDate.now().getYear() : endYear); + cnd.andEX("t.unionId", "=", unionId); + cnd.andEX("t.lineType", "=", lineType); + appendLineFilter(cnd, lineId); + appendTravelPeriodFilter(cnd, travelPeriod); + appendScopeFilter(cnd, currentUnionId, scopeType); + return cnd; + } + + private void appendKeywordFilter(Cnd cnd, String keyword) { + if (StrUtil.isNotBlank(keyword)) { + SqlExpressionGroup group = new SqlExpressionGroup(); + group.orLike("t.userName", keyword.trim()); + group.orLike("t.jobNo", keyword.trim()); + cnd.and(group); + } + } + + private void applyUnionLedgerScope(Cnd cnd, String currentUnionId) { + SqlExpressionGroup group = new SqlExpressionGroup(); + group.or("t.unionId", "=", currentUnionId); + group.or("m.unionId", "=", currentUnionId); + cnd.and(group); + } + + private void appendScopeFilter(Cnd cnd, String currentUnionId, String scopeType) { + if ("ownUnionJoined".equals(scopeType)) { + cnd.and("t.unionId", "=", currentUnionId); + return; + } + if ("organizedLineJoined".equals(scopeType)) { + cnd.and("m.unionId", "=", currentUnionId); + return; + } + applyUnionLedgerScope(cnd, currentUnionId); + } + + private String buildYearCondition(Integer startYear, Integer endYear) { + StringBuilder builder = new StringBuilder(); + if (startYear != null) { + builder.append(" AND t.`year` >= ").append(startYear); + } + if (endYear != null) { + builder.append(" AND t.`year` <= ").append(endYear); + } + return builder.toString(); + } + + private TourLedger fetchScopedLedger(String id) { + if (StrUtil.isBlank(id) || StrUtil.isBlank(currentUnionId())) { + return null; + } + Sql sql = Sqls.create(""" + SELECT COUNT(1) + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + WHERE t.delFlag = 0 + AND t.id = @id + AND (t.unionId = @unionId OR m.unionId = @unionId) + """); + sql.setParam("id", id); + sql.setParam("unionId", currentUnionId()); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt() > 0 ? tourLedgerService.fetch(id) : null; + } + + private String getTravelPeriod(TourLedger ledger) { + if (ledger == null || ledger.getYear() == null || StrUtil.isBlank(ledger.getLineId())) { + return ""; + } + if (StrUtil.isNotBlank(ledger.getMatterId())) { + Sql exactSql = Sqls.create(""" + SELECT CASE + WHEN IFNULL(travelStartTime, '') <> '' AND IFNULL(travelEndTime, '') <> '' + THEN CONCAT(travelStartTime, ' 至 ', travelEndTime) + ELSE '' + END AS travelPeriod + FROM tour_matter + WHERE delFlag = 0 + AND id = @matterId + """); + exactSql.setParam("matterId", ledger.getMatterId()); + exactSql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(exactSql); + return exactSql.getString(); + } + Sql sql = Sqls.create(""" + SELECT 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 `year` = @year + AND lineId = @lineId + """); + sql.setParam("year", ledger.getYear()); + sql.setParam("lineId", ledger.getLineId()); + sql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(sql); + return sql.getString(); + } + + private boolean isDirectFamilyUnitLine(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getLineId())) { + return false; + } + Sql sql = Sqls.create(""" + SELECT IFNULL(directFamilyUnitLine, 0) + FROM tour_line + WHERE delFlag = 0 + AND id = @lineId + """); + sql.setParam("lineId", ledger.getLineId()); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt() > 0; + } + + private boolean isFillBedInfo(TourLedger ledger) { + if (ledger == null || StrUtil.isBlank(ledger.getMatterId())) { + return true; + } + Sql sql = Sqls.create(""" + SELECT IFNULL(MAX(s.fillBedInfo), 1) + FROM tour_ledger t + LEFT JOIN tour_matter m ON m.id = t.matterId AND m.delFlag = 0 + LEFT JOIN tour_setting s ON s.id = m.settingId AND s.delFlag = 0 + WHERE t.id = @ledgerId + """); + sql.setParam("ledgerId", ledger.getId()); + sql.setCallback(Sqls.callback.integer()); + tourLedgerService.dao().execute(sql); + return sql.getInt() > 0; + } + + private void appendTravelPeriodFilter(Cnd cnd, String travelPeriod) { + if (StrUtil.isBlank(travelPeriod)) { + return; + } + cnd.and("CONCAT(m.travelStartTime, ' 至 ', m.travelEndTime)", "=", travelPeriod); + } + + private void appendLineFilter(Cnd cnd, String lineId) { + if (StrUtil.isBlank(lineId)) { + return; + } + LineFilter lineFilter = parseLineFilter(lineId); + if (lineFilter.getLineId().startsWith("legacy:")) { + SqlExpressionGroup legacyGroup = new SqlExpressionGroup(); + legacyGroup.or("t.lineId", "IS", null); + legacyGroup.or("t.lineId", "=", ""); + cnd.and(legacyGroup); + cnd.and("t.lineName", "=", lineFilter.getLineId().substring("legacy:".length())); + } else { + cnd.and("t.lineId", "=", lineFilter.getLineId()); + } + appendMatterUnionFilter(cnd, lineFilter.getUnionId()); + } + + private LineFilter parseLineFilter(String value) { + int delimiterIndex = value.indexOf("|"); + if (delimiterIndex < 0) { + return new LineFilter(value, null); + } + return new LineFilter(value.substring(0, delimiterIndex), value.substring(delimiterIndex + 1)); + } + + private void appendMatterUnionFilter(Cnd cnd, String unionId) { + if (unionId == null) { + return; + } + if (StrUtil.isBlank(unionId)) { + SqlExpressionGroup group = new SqlExpressionGroup(); + group.or("m.unionId", "IS", null); + group.or("m.unionId", "=", ""); + cnd.and(group); + return; + } + cnd.and("m.unionId", "=", unionId); + } + + private String getCurrentLineName(TourLedger ledger) { + if (ledger == null) { + return ""; + } + Sql sql = Sqls.create(""" + SELECT CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END AS lineName + FROM tour_ledger t + 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 sys_union mu ON mu.id = m.unionId + WHERE t.id = @ledgerId + """); + sql.setParam("ledgerId", ledger.getId()); + sql.setCallback(Sqls.callback.str()); + tourLedgerService.dao().execute(sql); + return StrUtil.blankToDefault(sql.getString(), ledger.getLineName()); + } + + private static class LineFilter { + private final String lineId; + private final String unionId; + + private LineFilter(String lineId, String unionId) { + this.lineId = lineId; + this.unionId = unionId; + } + + private String getLineId() { + return lineId; + } + + private String getUnionId() { + return unionId; + } + } + + private String currentUnionId() { + return SecurityUtil.getUnionId(); + } + + private String currentUnionName() { + String unionId = currentUnionId(); + if (StrUtil.isBlank(unionId)) { + return ""; + } + Sys_union union = tourLedgerService.dao().fetch(Sys_union.class, unionId); + return union == null ? "" : StrUtil.blankToDefault(union.getName(), ""); + } + + private String getOrderColumn(String orderName) { + if ("year".equals(orderName)) { + return "t.`year`"; + } + if ("jobNo".equals(orderName)) { + return "t.jobNo"; + } + if ("userName".equals(orderName)) { + return "t.userName"; + } + if ("lineName".equals(orderName)) { + return """ + CASE + WHEN m.id IS NULL THEN COALESCE(NULLIF(l.lineName, ''), t.lineName) + ELSE CONCAT( + COALESCE(NULLIF(l.lineName, ''), t.lineName), + '(', + CASE + WHEN IFNULL(m.unionId, '') = '' THEN '校工会' + ELSE IFNULL(mu.name, '') + END, + ')' + ) + END + """; + } + if ("travelPeriod".equals(orderName)) { + return "COALESCE(m.travelStartTime, tp.travelPeriod)"; + } + if ("lineType".equals(orderName)) { + return "t.lineType"; + } + if ("signupTime".equals(orderName)) { + return "t.signupTime"; + } + if ("familyCount".equals(orderName)) { + return "familyCount"; + } + if ("joined".equals(orderName)) { + return "t.joined"; + } + return "t.`year`"; + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/mode/TourLedgerImportExcelMode.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/mode/TourLedgerImportExcelMode.java new file mode 100644 index 00000000..b19393d0 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/mode/TourLedgerImportExcelMode.java @@ -0,0 +1,34 @@ +package com.budwk.app.zhgh.dayofficework.tour.mode; + +import cn.afterturn.easypoi.excel.annotation.Excel; +import com.budwk.app.base.model.ExcelImportError; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.util.Date; + +@EqualsAndHashCode(callSuper = true) +@Data +public class TourLedgerImportExcelMode extends ExcelImportError { + + @Excel(name = "年度") + private Integer year; + + @Excel(name = "工号") + private String jobNo; + + @Excel(name = "姓名") + private String userName; + + @Excel(name = "报名时间", format = "yyyy-MM-dd HH:mm:ss") + private Date signupTime; + + @Excel(name = "线路名称") + private String lineName; + + @Excel(name = "线路类型") + private String lineType; + + @Excel(name = "是否参加") + private String joined; +} 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 new file mode 100644 index 00000000..7fde2dbd --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedger.java @@ -0,0 +1,157 @@ +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.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serializable; + +/** + * 疗休养教职工报名台账。 + * 后续报名模块完成后,将已报名或已参加的教职工写入本表,台账页负责跨年度查询和详情查看。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("tour_ledger") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("普惠疗休养台账") +public class TourLedger extends BaseModel implements Serializable { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("年度") + @ColDefine(type = ColType.INT, width = 4) + private Integer year; + + @Column + @Comment("工号") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String jobNo; + + @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 idCard; + + @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 signupTime; + + @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 = 100) + private String lineName; + + @Column + @Comment("线路类型") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String lineType; + + @Column + @Comment("报名酒店") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String hotelName; + + @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("乘车地点") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String boardingPlace; + + @Column + @Comment("是否携带家属") + @ColDefine(type = ColType.BOOLEAN) + @Default("0") + private Boolean hasFamily; + + @Column + @Comment("意向拼床人") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String intendedRoommate; + + @Column + @Comment("床型") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String bedType; + + @Column + @Comment("床位信息") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String bedInfo; + + @Column + @Comment("是否参加") + @ColDefine(type = ColType.BOOLEAN) + @Default("0") + private Boolean joined; + + @Column + @Comment("是否报销") + @ColDefine(type = ColType.BOOLEAN) + @Default("0") + private Boolean reimbursed; + + @Column + @Comment("报销超出费用") + @ColDefine(type = ColType.BOOLEAN) + @Default("0") + private Boolean overCostReimbursed; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedgerDirectRelative.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedgerDirectRelative.java new file mode 100644 index 00000000..7021209a --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedgerDirectRelative.java @@ -0,0 +1,73 @@ +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.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serializable; + +/** + * 直系亲属线路报名信息。 + * 与普通携带亲属信息分表存放,用于直系亲属线路的专属申请信息。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("tour_ledger_direct_relative") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("普惠疗休养直系亲属线路报名信息") +public class TourLedgerDirectRelative 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 ledgerId; + + @Column + @Comment("亲属姓名") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String relativeName; + + @Column + @Comment("所在单位") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String unitName; + + @Column + @Comment("亲属关系编码") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String relationshipCode; + + @Column + @Comment("亲属关系") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String relationshipName; + + @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 = 20) + private String travelStartTime; + + @Column + @Comment("出行结束日期") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String travelEndTime; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedgerFamily.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedgerFamily.java new file mode 100644 index 00000000..87c18078 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedgerFamily.java @@ -0,0 +1,83 @@ +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.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serializable; + +/** + * 疗休养台账家属信息。 + * 与教职工台账通过 ledgerId 关联,用于查看教职工携带家属的历史记录。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("tour_ledger_family") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("普惠疗休养台账家属信息") +public class TourLedgerFamily 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 ledgerId; + + @Column + @Comment("教职工工号") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String staffJobNo; + + @Column + @Comment("教职工姓名") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String staffName; + + @Column + @Comment("家属姓名") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String familyName; + + @Column + @Comment("性别") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String gender; + + @Column + @Comment("身份证号码") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String idCard; + + @Column + @Comment("年龄") + @ColDefine(type = ColType.INT, width = 3) + private Integer age; + + @Column + @Comment("床型") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String bedType; + + @Column + @Comment("床位") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String bedInfo; + + @Column + @Comment("意向拼床人") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String intendedRoommate; + + @Column + @Comment("关系") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String relationship; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLine.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLine.java new file mode 100644 index 00000000..9fbfaef4 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLine.java @@ -0,0 +1,106 @@ +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.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serializable; + +/** + * 疗休养线路管理。 + * 当前阶段先维护线路基础信息,后续报名、台账等模块可通过线路ID继续关联。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("tour_line") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("普惠疗休养线路") +public class TourLine extends BaseModel implements Serializable { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("创建年度") + @ColDefine(type = ColType.INT, width = 4) + private Integer year; + + @Column + @Comment("线路编号") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String lineCode; + + @Column + @Comment("线路名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String lineName; + + @Column + @Comment("旅行社ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String travelAgencyId; + + @Column + @Comment("创建人ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String creatorUserId; + + @Column + @Comment("创建人") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String creatorName; + + @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("线路类型") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String lineType; + + @Column + @Comment("时间标段ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String lotId; + + @Column + @Comment("线路内容") + @ColDefine(type = ColType.TEXT) + private String lineContent; + + @Column + @Comment("移动端缩略图") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String mobileThumb; + + @Column + @Comment("是否对外开放") + @ColDefine(type = ColType.BOOLEAN) + @Default("1") + private Boolean openFlag; + + @Column + @Comment("是否直系亲属线路") + @ColDefine(type = ColType.BOOLEAN) + @Default("0") + private Boolean directFamilyUnitLine; + + @Column + @Comment("激活状态") + @ColDefine(type = ColType.BOOLEAN) + @Default("1") + private Boolean enabled; +} 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 new file mode 100644 index 00000000..54a061de --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourMatter.java @@ -0,0 +1,129 @@ +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.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * 疗休养事项。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("tour_matter") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("普惠疗休养事项") +public class TourMatter extends BaseModel implements Serializable { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("年度") + @ColDefine(type = ColType.INT, width = 4) + private Integer year; + + @Column + @Comment("事项名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String matterName; + + @Column + @Comment("疗休养配置ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String settingId; + + @Column + @Comment("创建人ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String creatorUserId; + + @Column + @Comment("创建人") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String creatorName; + + @Column + @Comment("所属工会ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String unionId; + + @Column + @Comment("组织形式") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String organizationType; + + @Column + @Comment("移动端缩略图") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String mobileThumb; + + @Column + @Comment("线路ID") + @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) + private String signupStartTime; + + @Column + @Comment("报名结束时间") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String signupEndTime; + + @Column + @Comment("出行开始时间") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String travelStartTime; + + @Column + @Comment("出行结束时间") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String travelEndTime; + + @Column + @Comment("联系人") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String contactName; + + @Column + @Comment("联系方式") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String contactPhone; + + @Column + @Comment("最少成团人数") + @ColDefine(type = ColType.INT) + private Integer minGroupPeople; + + @Column + @Comment("最多成团人数") + @ColDefine(type = ColType.INT) + private Integer maxGroupPeople; + + @Column + @Comment("预计费用") + @ColDefine(customType = "decimal(10,2)") + private BigDecimal estimatedCost; + + @Column + @Comment("事项状态") + @ColDefine(type = ColType.BOOLEAN) + @Default("1") + private Boolean enabled; +} 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 new file mode 100644 index 00000000..447c025d --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSetting.java @@ -0,0 +1,156 @@ +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.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.List; + +/** + * 疗休养基础配置。 + * 这里先沉淀创建事项会复用的基础字段,后续线路、报名等阶段可以继续通过配置ID关联扩展。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("tour_setting") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("普惠疗休养配置") +public class TourSetting extends BaseModel implements Serializable { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("年度") + @ColDefine(type = ColType.INT, width = 4) + private Integer year; + + @Column + @Comment("疗休养配置名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String configName; + + @Column + @Comment("疗休养类型") + @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) + private Integer sortNo; + + @Column + @Comment("最少成团人数") + @ColDefine(type = ColType.INT) + private Integer minGroupPeople; + + @Column + @Comment("最多成团人数") + @ColDefine(type = ColType.INT) + private Integer maxGroupPeople; + + @Column + @Comment("省外几年去一次") + @ColDefine(type = ColType.INT) + private Integer outProvinceYears; + + @Column + @Comment("省外人数占比") + @ColDefine(customType = "decimal(10,2)") + private BigDecimal outProvinceRatio; + + @Column + @Comment("省外人数占比类型") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String outProvinceRatioType; + + @Column + @Comment("省外固定人数") + @ColDefine(type = ColType.INT) + private Integer outProvinceFixedPeople; + + @Column + @Comment("周期开始年度") + @ColDefine(type = ColType.INT, width = 4) + private Integer cycleStartYear; + + @Column + @Comment("周期结束年度") + @ColDefine(type = ColType.INT, width = 4) + private Integer cycleEndYear; + + @Column + @Comment("周期内总费用") + @ColDefine(type = ColType.INT) + private Integer cycleTotalCost; + + @Column + @Comment("周期允许次数") + @ColDefine(type = ColType.INT) + private Integer cycleAllowedTimes; + + @Column + @Comment("是否允许携带家属") + @ColDefine(type = ColType.BOOLEAN) + @Default("0") + private Boolean allowFamily; + + @Column + @Comment("是否填报床位信息") + @ColDefine(type = ColType.BOOLEAN) + @Default("1") + private Boolean fillBedInfo; + + @Column + @Comment("是否启用") + @ColDefine(type = ColType.BOOLEAN) + @Default("1") + private Boolean enabled; + + /** + * 标段管理沿用老疗休养配置的子表设计,后续线路、旅行社等模块可通过标段ID继续关联。 + */ + @Many(field = "settingId") + private List lots; + + /** + * 分工会名额分配,仅用于配置弹窗回显与提交,不作为 tour_setting 表字段保存。 + */ + private List unionQuotas; + + @Column + @Comment("服务须知") + @ColDefine(type = ColType.TEXT) + private String serviceNotice; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSettingLot.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSettingLot.java new file mode 100644 index 00000000..9b502365 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSettingLot.java @@ -0,0 +1,53 @@ +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.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serializable; + +/** + * 疗休养配置标段。 + * 标段从基础配置中拆成子表,便于后续线路、目的地、报名等模块复用同一标段口径。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("tour_setting_lot") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("普惠疗休养配置标段") +public class TourSettingLot 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("标段名称") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String lotName; + + @Column + @Comment("标段值") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String lotValue; + + @Column + @Comment("标段费用") + @ColDefine(type = ColType.INT) + private Integer activityCost; + + @Column + @Comment("允许超出报销") + @ColDefine(type = ColType.BOOLEAN) + private Boolean allowOverReimbursement; +} 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/TourTravelAgency.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourTravelAgency.java new file mode 100644 index 00000000..88f0865c --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourTravelAgency.java @@ -0,0 +1,74 @@ +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.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +import java.io.Serializable; + +/** + * 旅行社管理。 + * 先维护疗休养线路创建会复用的旅行社基础信息,后续线路模块可通过旅行社ID关联。 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Table("tour_travel_agency") +@TableMeta("{'mysql-charset':'utf8mb4'}") +@Comment("普惠疗休养旅行社") +public class TourTravelAgency extends BaseModel implements Serializable { + + @Column + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @Comment("年度") + @ColDefine(type = ColType.INT, width = 4) + private Integer year; + + @Column + @Comment("旅行社编号") + @ColDefine(type = ColType.VARCHAR, width = 50) + private String agencyCode; + + @Column + @Comment("旅行社名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String agencyName; + + @Column + @Comment("联系人") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String contactName; + + @Column + @Comment("联系人手机") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String contactPhone; + + @Column + @Comment("邮箱") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String email; + + @Column + @Comment("备注") + @ColDefine(type = ColType.TEXT) + private String remark; + + @Column + @Comment("移动端缩略图") + @ColDefine(type = ColType.VARCHAR, width = 500) + private String mobileThumb; + + @Column + @Comment("激活状态") + @ColDefine(type = ColType.BOOLEAN) + @Default("1") + private Boolean enabled; +} 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/TourLedgerDirectRelativeService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerDirectRelativeService.java new file mode 100644 index 00000000..f92bb263 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerDirectRelativeService.java @@ -0,0 +1,7 @@ +package com.budwk.app.zhgh.dayofficework.tour.service; + +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative; + +public interface TourLedgerDirectRelativeService extends BaseService { +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerFamilyService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerFamilyService.java new file mode 100644 index 00000000..f1ce741f --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerFamilyService.java @@ -0,0 +1,7 @@ +package com.budwk.app.zhgh.dayofficework.tour.service; + +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily; + +public interface TourLedgerFamilyService extends BaseService { +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerService.java new file mode 100644 index 00000000..076a5dd4 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerService.java @@ -0,0 +1,7 @@ +package com.budwk.app.zhgh.dayofficework.tour.service; + +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; + +public interface TourLedgerService extends BaseService { +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLineService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLineService.java new file mode 100644 index 00000000..240cf45c --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLineService.java @@ -0,0 +1,7 @@ +package com.budwk.app.zhgh.dayofficework.tour.service; + +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLine; + +public interface TourLineService extends BaseService { +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourMatterService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourMatterService.java new file mode 100644 index 00000000..8171b59b --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourMatterService.java @@ -0,0 +1,7 @@ +package com.budwk.app.zhgh.dayofficework.tour.service; + +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter; + +public interface TourMatterService extends BaseService { +} 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 new file mode 100644 index 00000000..335f18c9 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourSettingService.java @@ -0,0 +1,42 @@ +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/TourTravelAgencyService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourTravelAgencyService.java new file mode 100644 index 00000000..f90ba290 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourTravelAgencyService.java @@ -0,0 +1,7 @@ +package com.budwk.app.zhgh.dayofficework.tour.service; + +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.tour.models.TourTravelAgency; + +public interface TourTravelAgencyService extends BaseService { +} 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/TourLedgerDirectRelativeServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerDirectRelativeServiceImpl.java new file mode 100644 index 00000000..8da9d20b --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerDirectRelativeServiceImpl.java @@ -0,0 +1,15 @@ +package com.budwk.app.zhgh.dayofficework.tour.service.impl; + +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerDirectRelative; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerDirectRelativeService; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.IocBean; + +@IocBean(args = {"refer:dao"}) +public class TourLedgerDirectRelativeServiceImpl extends BaseServiceImpl implements TourLedgerDirectRelativeService { + + public TourLedgerDirectRelativeServiceImpl(Dao dao) { + super(dao); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerFamilyServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerFamilyServiceImpl.java new file mode 100644 index 00000000..0e045b61 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerFamilyServiceImpl.java @@ -0,0 +1,15 @@ +package com.budwk.app.zhgh.dayofficework.tour.service.impl; + +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedgerFamily; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerFamilyService; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.IocBean; + +@IocBean(args = {"refer:dao"}) +public class TourLedgerFamilyServiceImpl extends BaseServiceImpl implements TourLedgerFamilyService { + + public TourLedgerFamilyServiceImpl(Dao dao) { + super(dao); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerServiceImpl.java new file mode 100644 index 00000000..88c2f668 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerServiceImpl.java @@ -0,0 +1,15 @@ +package com.budwk.app.zhgh.dayofficework.tour.service.impl; + +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLedger; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLedgerService; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.IocBean; + +@IocBean(args = {"refer:dao"}) +public class TourLedgerServiceImpl extends BaseServiceImpl implements TourLedgerService { + + public TourLedgerServiceImpl(Dao dao) { + super(dao); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLineServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLineServiceImpl.java new file mode 100644 index 00000000..c06e6cf4 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLineServiceImpl.java @@ -0,0 +1,15 @@ +package com.budwk.app.zhgh.dayofficework.tour.service.impl; + +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.zhgh.dayofficework.tour.models.TourLine; +import com.budwk.app.zhgh.dayofficework.tour.service.TourLineService; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.IocBean; + +@IocBean(args = {"refer:dao"}) +public class TourLineServiceImpl extends BaseServiceImpl implements TourLineService { + + public TourLineServiceImpl(Dao dao) { + super(dao); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourMatterServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourMatterServiceImpl.java new file mode 100644 index 00000000..c7e10dbe --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourMatterServiceImpl.java @@ -0,0 +1,15 @@ +package com.budwk.app.zhgh.dayofficework.tour.service.impl; + +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.zhgh.dayofficework.tour.models.TourMatter; +import com.budwk.app.zhgh.dayofficework.tour.service.TourMatterService; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.IocBean; + +@IocBean(args = {"refer:dao"}) +public class TourMatterServiceImpl extends BaseServiceImpl implements TourMatterService { + + public TourMatterServiceImpl(Dao dao) { + super(dao); + } +} 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 new file mode 100644 index 00000000..c2b360c3 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourSettingServiceImpl.java @@ -0,0 +1,133 @@ +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 { + + 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/TourTravelAgencyServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourTravelAgencyServiceImpl.java new file mode 100644 index 00000000..5122784e --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourTravelAgencyServiceImpl.java @@ -0,0 +1,15 @@ +package com.budwk.app.zhgh.dayofficework.tour.service.impl; + +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.zhgh.dayofficework.tour.models.TourTravelAgency; +import com.budwk.app.zhgh.dayofficework.tour.service.TourTravelAgencyService; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.IocBean; + +@IocBean(args = {"refer:dao"}) +public class TourTravelAgencyServiceImpl extends BaseServiceImpl implements TourTravelAgencyService { + + public TourTravelAgencyServiceImpl(Dao dao) { + super(dao); + } +} 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/java/com/budwk/app/zhgh/democratic/proposal/controller/common/ProposalCommonController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/common/ProposalCommonController.java index 4cad0e13..6edfef2e 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/common/ProposalCommonController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/common/ProposalCommonController.java @@ -82,7 +82,9 @@ public class ProposalCommonController { @SaCheckLogin @ApiOperation("承办单位列表") public Result listUnderTake() { - List list = dao.query(ProposalUndertake.class, Cnd.NEW()); + Cnd cnd = Cnd.NEW(); + cnd.and("enable","=", true); + List list = dao.query(ProposalUndertake.class, cnd); return Result.success(list); } diff --git a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/config/ProposalConfigUnitController.java b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/config/ProposalConfigUnitController.java index 1034f6ec..905907ff 100644 --- a/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/config/ProposalConfigUnitController.java +++ b/src/main/java/com/budwk/app/zhgh/democratic/proposal/controller/config/ProposalConfigUnitController.java @@ -1,6 +1,7 @@ package com.budwk.app.zhgh.democratic.proposal.controller.config; import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.hutool.core.collection.CollectionUtil; import cn.hutool.core.lang.tree.Tree; import cn.hutool.core.lang.tree.TreeNode; import cn.hutool.core.lang.tree.TreeUtil; @@ -20,10 +21,12 @@ import com.budwk.app.zhgh.democratic.proposal.models.ProposalConfig; import com.budwk.app.zhgh.democratic.proposal.models.ProposalUndertake; import lombok.extern.slf4j.Slf4j; import org.nutz.aop.interceptor.ioc.TransAop; +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.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; @@ -37,6 +40,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * 提案承办单位配置 @@ -98,6 +102,7 @@ public class ProposalConfigUnitController { cnd.where().orLike("u2.loginname", pageForm.getSearchKeyword()); cnd.where().orLike("t1.name", pageForm.getSearchKeyword()); } + cnd.and("t1.enable","=", true); cnd.asc("t1.code"); sql.setCondition(cnd); Pagination pagination = baseService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); @@ -124,6 +129,20 @@ public class ProposalConfigUnitController { return proposalUndertake; }).toList(); dao.fastInsert(proposalUndertakes); + + dao.update(ProposalUndertake.class, Chain.make("enable",true), Cnd.where("1","=","1")); + Cnd cnd = Cnd.NEW(); + SqlExpressionGroup group = Cnd.NEW().where(); + group.and("delFlag", "=", true); + cnd.and(group); + List delFlagUnit = dao.query(Sys_unit.class, cnd); + if(CollectionUtil.isNotEmpty(delFlagUnit)){ + List unitCodeList = delFlagUnit.stream().map(Sys_unit::getUnitcode).collect(Collectors.toList()); + Cnd cndUnit = Cnd.NEW(); + cndUnit.where().andInStrList("code", unitCodeList); + dao.update(ProposalUndertake.class, Chain.make("enable",false), cndUnit); + } + return Result.success(); } diff --git a/src/main/resources/db/activity_user_scope_add_group_type_sql.sql b/src/main/resources/db/activity_user_scope_add_group_type_sql.sql new file mode 100644 index 00000000..34008fac --- /dev/null +++ b/src/main/resources/db/activity_user_scope_add_group_type_sql.sql @@ -0,0 +1,8 @@ +ALTER TABLE activity_user_scope + ADD COLUMN groupType INT(2) NULL COMMENT '分组类型 1.结果分组 2.SQL条件分组' AFTER userId; + +ALTER TABLE activity_user_scope + ADD COLUMN groupSql LONGTEXT NULL COMMENT 'SQL条件分组保存的查询SQL' AFTER groupType; + +ALTER TABLE activity_user_scope + ADD INDEX INDEX_ACTIVITY_USER_SCOPE_GROUPID_USERID (groupId, userId); diff --git a/src/main/resources/db/menu/init_tour_menu.sql b/src/main/resources/db/menu/init_tour_menu.sql new file mode 100644 index 00000000..0042dd69 --- /dev/null +++ b/src/main/resources/db/menu/init_tour_menu.sql @@ -0,0 +1,122 @@ +-- 普惠疗休养平台电脑端菜单。 +-- 使用 permission 做幂等判断,避免同一环境重复执行时插入重复菜单。 +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bb1a10001', + '', + LPAD(IFNULL(MAX(CAST(path AS UNSIGNED)), 0) + 1, 4, '0'), + '普惠疗休养', + 'Tour', + 'menu', + '', + '', + 'ti-map-alt', + 1, + 0, + 'tour', + NULL, + 991, + 1, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + '', + UNIX_TIMESTAMP(NOW()) * 1000, + 0, + 'PC', + NULL, + NULL, + 'p', + 0, + 0 +FROM sys_menu +WHERE (parentId = '' OR parentId IS NULL) + AND CHAR_LENGTH(path) = 4 +HAVING NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10002', p.id, CONCAT(p.path, '0001'), '疗休养设置', 'Tour Setting', 'menu', '/platform/tour/setting', 'data-pjax', '', 1, 0, 'tour.setting', NULL, 1, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0 +FROM sys_menu p +WHERE p.permission = 'tour' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.setting') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10003', p.id, CONCAT(p.path, '0002'), '旅行社管理', 'Travel Agency', 'menu', '/platform/tour/travelAgency', 'data-pjax', '', 1, 0, 'tour.travelAgency', NULL, 2, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0 +FROM sys_menu p +WHERE p.permission = 'tour' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.travelAgency') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10004', p.id, CONCAT(p.path, '0003'), '线路管理', 'Route Manage', 'menu', '/platform/tour/route', 'data-pjax', '', 1, 0, 'tour.route', NULL, 3, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'x', 0, 0 +FROM sys_menu p +WHERE p.permission = 'tour' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.route') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10005', p.id, CONCAT(p.path, '0004'), '疗休养事项', 'Tour Matter', 'menu', '/platform/tour/matter', 'data-pjax', '', 1, 0, 'tour.matter', NULL, 4, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0 +FROM sys_menu p +WHERE p.permission = 'tour' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.matter') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10006', p.id, CONCAT(p.path, '0005'), '疗休养报名', 'Tour Signup', 'menu', '/platform/tour/signup', 'data-pjax', '', 1, 0, 'tour.signup', NULL, 5, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0 +FROM sys_menu p +WHERE p.permission = 'tour' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.signup') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10008', p.id, CONCAT(p.path, '0006'), '我的报名', 'My Signup', 'menu', '/platform/tour/mysignup', 'data-pjax', '', 1, 0, 'tour.mysignup', NULL, 6, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'w', 0, 0 +FROM sys_menu p +WHERE p.permission = 'tour' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.mysignup') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10009', p.id, CONCAT(p.path, '0007'), '分工会查询', 'Union Ledger', 'menu', '/platform/tour/unionledger', 'data-pjax', '', 1, 0, 'tour.unionledger', NULL, 7, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'f', 0, 0 +FROM sys_menu p +WHERE p.permission = 'tour' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.unionledger') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10011', p.id, CONCAT(p.path, '0010'), '分工会审核', 'Union Approval', 'menu', '/platform/tour/unionApproval', 'data-pjax', '', 1, 0, 'tour.unionApproval', NULL, 8, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'f', 0, 0 +FROM sys_menu p +WHERE p.permission = 'tour' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.unionApproval') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10012', p.id, CONCAT(p.path, '0011'), '校工会审核', 'School Union Approval', 'menu', '/platform/tour/schoolUnionApproval', 'data-pjax', '', 1, 0, 'tour.schoolUnionApproval', NULL, 9, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'x', 0, 0 +FROM sys_menu p +WHERE p.permission = 'tour' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.schoolUnionApproval') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10007', p.id, CONCAT(p.path, '0008'), '疗休养台账', 'Tour Ledger', 'menu', '/platform/tour/ledger', 'data-pjax', '', 1, 0, 'tour.ledger', NULL, 10, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'l', 0, 0 +FROM sys_menu p +WHERE p.permission = 'tour' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.ledger') t); + +INSERT INTO sys_menu (id, parentId, path, name, aliasName, type, href, target, icon, showit, disabled, permission, note, location, hasChildren, createdBy, createdAt, updatedBy, updatedAt, delFlag, platform, moduleId, picIcon, initialPinyinName, isRecommendApp, isRecommendService) +SELECT 'b7c63f0c73a44a9a8c4f3d3bb1a10010', p.id, CONCAT(p.path, '0009'), '线路成团', 'Tour Group', 'menu', '/platform/tour/group', 'data-pjax', '', 1, 0, 'tour.group', NULL, 11, 0, '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0, 'PC', NULL, NULL, 'x', 0, 0 +FROM sys_menu p +WHERE p.permission = 'tour' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM sys_menu WHERE permission = 'tour.group') t); + +-- 给系统管理员默认授权,其他角色可在角色管理中按需分配。 +INSERT INTO sys_role_menu (roleId, menuId) +SELECT r.id, m.id +FROM sys_role r +JOIN sys_menu m ON m.permission IN ( + 'tour', + 'tour.setting', + 'tour.travelAgency', + 'tour.route', + 'tour.matter', + 'tour.signup', + 'tour.group', + 'tour.mysignup', + 'tour.unionledger', + 'tour.unionApproval', + 'tour.schoolUnionApproval', + 'tour.ledger' +) +LEFT JOIN sys_role_menu rm ON rm.roleId = r.id AND rm.menuId = m.id +WHERE r.code = 'SYSADMIN' + AND rm.roleId IS NULL; 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_dict_init.sql b/src/main/resources/db/tour_dict_init.sql new file mode 100644 index 00000000..d81d6ef1 --- /dev/null +++ b/src/main/resources/db/tour_dict_init.sql @@ -0,0 +1,253 @@ +-- 普惠疗休养系统字典初始化。 +-- 执行后可在 /platform/sys/dict 页面看到:疗休养(Tour) -> 组织形式/疗休养类型/线路类型/床型。 + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict001', '', '9500', '疗休养', '普惠疗休养平台字典', 'Tour', 0, 9500, 1, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'Tour') t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict002', p.id, CONCAT(p.path, '0001'), '组织形式', '疗休养组织形式', 'organizationType', 0, 1, 1, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'Tour' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'organizationType' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict003', p.id, CONCAT(p.path, '0001'), '校工会组织', '疗休养组织形式', 'schoolUnion', 0, 1, 0, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'organizationType' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'schoolUnion' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict004', p.id, CONCAT(p.path, '0002'), '分工会组织', '疗休养组织形式', 'branchUnion', 0, 2, 0, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'organizationType' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'branchUnion' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict005', p.id, CONCAT(p.path, '0003'), '个人组织', '疗休养组织形式', 'personal', 0, 3, 0, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'organizationType' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'personal' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict006', p.id, CONCAT(p.path, '0002'), '疗休养类型', '疗休养类型', 'tourType', 0, 2, 1, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'Tour' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'tourType' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict007', p.id, CONCAT(p.path, '0001'), '普惠性疗休养', '疗休养类型', 'inclusiveTour', 0, 1, 0, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'tourType' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'inclusiveTour' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict008', p.id, CONCAT(p.path, '0002'), '优秀职工疗休养', '疗休养类型', 'excellentWorkerTour', 0, 2, 0, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'tourType' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'excellentWorkerTour' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict009', p.id, CONCAT(p.path, '0003'), '线路类型', '线路类型', 'lineType', 0, 3, 1, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'Tour' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'lineType' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict010', p.id, CONCAT(p.path, '0001'), '省内线路', '线路类型', 'inProvinceLine', 0, 1, 0, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'lineType' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'inProvinceLine' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict011', p.id, CONCAT(p.path, '0002'), '省外线路', '线路类型', 'outProvinceLine', 0, 2, 0, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'lineType' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'outProvinceLine' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict020', p.id, CONCAT(p.path, '0004'), '床型', '床型', 'bedType', 0, 4, 1, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'Tour' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'bedType' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict021', p.id, CONCAT(p.path, '0001'), '双人床', '床型', 'doubleBed', 0, 1, 0, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'bedType' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'doubleBed' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict022', p.id, CONCAT(p.path, '0002'), '单人床', '床型', 'singleBed', 0, 2, 0, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'bedType' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'singleBed' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict023', p.id, CONCAT(p.path, '0003'), '大床', '床型', 'kingBed', 0, 3, 0, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'bedType' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'kingBed' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict012', '', '9600', '亲属关系', '亲属关系', 'familyRelationship', 0, 9600, 1, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +WHERE NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'familyRelationship') t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict013', p.id, CONCAT(p.path, '0001'), '直系亲属', '亲属关系', 'directRelative', 0, 1, 1, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'familyRelationship' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'directRelative' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict014', p.id, CONCAT(p.path, '0001'), '父亲', '直系亲属', 'father', 0, 1, 0, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'directRelative' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'father' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict015', p.id, CONCAT(p.path, '0002'), '母亲', '直系亲属', 'mother', 0, 2, 0, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'directRelative' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'mother' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict016', p.id, CONCAT(p.path, '0003'), '丈夫', '直系亲属', 'husband', 0, 3, 0, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'directRelative' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'husband' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict017', p.id, CONCAT(p.path, '0004'), '妻子', '直系亲属', 'wife', 0, 4, 0, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'directRelative' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'wife' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict018', p.id, CONCAT(p.path, '0005'), '儿子', '直系亲属', 'son', 0, 5, 0, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'directRelative' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'son' AND `parentId` = p.id) t); + +INSERT INTO `sys_dict` ( + `id`, `parentId`, `path`, `name`, `remark`, `code`, `disabled`, `location`, `hasChildren`, + `createdBy`, `createdAt`, `updatedBy`, `updatedAt`, `delFlag` +) +SELECT + 'b7c63f0c73a44a9a8c4f3d3bdict019', p.id, CONCAT(p.path, '0006'), '女儿', '直系亲属', 'daughter', 0, 6, 0, + '', UNIX_TIMESTAMP(NOW()) * 1000, '', UNIX_TIMESTAMP(NOW()) * 1000, 0 +FROM `sys_dict` p +WHERE p.`code` = 'directRelative' + AND NOT EXISTS (SELECT 1 FROM (SELECT id FROM `sys_dict` WHERE `code` = 'daughter' AND `parentId` = p.id) t); + +UPDATE `sys_dict` SET `hasChildren` = 1 WHERE `code` IN ('Tour', 'organizationType', 'tourType', 'lineType', 'bedType', 'familyRelationship', 'directRelative'); 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_ledger_add_matter.sql b/src/main/resources/db/tour_ledger_add_matter.sql new file mode 100644 index 00000000..1b782a03 --- /dev/null +++ b/src/main/resources/db/tour_ledger_add_matter.sql @@ -0,0 +1,7 @@ +-- Add matter reference so signup records are unique per travel period/matter. +ALTER TABLE `tour_ledger` + ADD COLUMN `matterId` varchar(32) DEFAULT NULL COMMENT '报名事项ID' AFTER `signupTime`; + +ALTER TABLE `tour_ledger` + ADD KEY `idx_tour_ledger_matter` (`matterId`), + ADD KEY `idx_tour_ledger_matter_job` (`matterId`, `jobNo`); diff --git a/src/main/resources/db/tour_ledger_add_over_cost_reimbursed.sql b/src/main/resources/db/tour_ledger_add_over_cost_reimbursed.sql new file mode 100644 index 00000000..8705341a --- /dev/null +++ b/src/main/resources/db/tour_ledger_add_over_cost_reimbursed.sql @@ -0,0 +1,2 @@ +ALTER TABLE `tour_ledger` + ADD COLUMN `overCostReimbursed` tinyint(1) DEFAULT 0 COMMENT '报销超出费用' AFTER `reimbursed`; diff --git a/src/main/resources/db/tour_ledger_add_query_indexes.sql b/src/main/resources/db/tour_ledger_add_query_indexes.sql new file mode 100644 index 00000000..f7ad5eae --- /dev/null +++ b/src/main/resources/db/tour_ledger_add_query_indexes.sql @@ -0,0 +1,9 @@ +-- 疗休养台账查询加速索引。 +ALTER TABLE `tour_ledger` + ADD KEY `idx_tour_ledger_year_signup` (`year`, `delFlag`, `signupTime`), + ADD KEY `idx_tour_ledger_year_line` (`year`, `lineId`, `delFlag`), + ADD KEY `idx_tour_ledger_year_union_type` (`year`, `unionId`, `lineType`, `delFlag`), + ADD KEY `idx_tour_ledger_year_job` (`year`, `jobNo`, `delFlag`); + +ALTER TABLE `wf_process_instance` + ADD KEY `idx_wf_process_instance_business_state` (`businessNo`, `state`); diff --git a/src/main/resources/db/tour_ledger_direct_relative_create.sql b/src/main/resources/db/tour_ledger_direct_relative_create.sql new file mode 100644 index 00000000..1d0d1cc5 --- /dev/null +++ b/src/main/resources/db/tour_ledger_direct_relative_create.sql @@ -0,0 +1,21 @@ +-- 直系亲属线路报名信息表,独立于普通携带亲属信息表。 +CREATE TABLE IF NOT EXISTS `tour_ledger_direct_relative` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `ledgerId` varchar(32) DEFAULT NULL COMMENT '台账ID', + `relativeName` varchar(100) DEFAULT NULL COMMENT '亲属姓名', + `unitName` varchar(100) DEFAULT NULL COMMENT '所在单位', + `relationshipCode` varchar(50) DEFAULT NULL COMMENT '亲属关系编码', + `relationshipName` varchar(50) DEFAULT NULL COMMENT '亲属关系', + `lineId` varchar(32) DEFAULT NULL COMMENT '线路ID', + `lineName` varchar(100) DEFAULT NULL COMMENT '线路名称', + `travelStartTime` varchar(20) DEFAULT NULL COMMENT '出行开始日期', + `travelEndTime` varchar(20) 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`), + KEY `idx_tour_ledger_direct_relative_ledger` (`ledgerId`), + KEY `idx_tour_ledger_direct_relative_line` (`lineId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养直系亲属线路报名信息'; diff --git a/src/main/resources/db/tour_line_add_creator_unit.sql b/src/main/resources/db/tour_line_add_creator_unit.sql new file mode 100644 index 00000000..8b06c219 --- /dev/null +++ b/src/main/resources/db/tour_line_add_creator_unit.sql @@ -0,0 +1,10 @@ +-- 线路管理增加创建人和所在单位业务字段。 +ALTER TABLE `tour_line` + ADD COLUMN `creatorUserId` varchar(32) DEFAULT NULL COMMENT '创建人ID' AFTER `travelAgencyId`, + ADD COLUMN `creatorName` varchar(100) DEFAULT NULL COMMENT '创建人' AFTER `creatorUserId`, + ADD COLUMN `unitId` varchar(32) DEFAULT NULL COMMENT '所在单位ID' AFTER `creatorName`, + ADD COLUMN `unitName` varchar(100) DEFAULT NULL COMMENT '所在单位' AFTER `unitId`; + +ALTER TABLE `tour_line` + ADD KEY `idx_tour_line_creator` (`creatorUserId`), + ADD KEY `idx_tour_line_unit` (`unitId`); diff --git a/src/main/resources/db/tour_line_add_direct_family_unit_line.sql b/src/main/resources/db/tour_line_add_direct_family_unit_line.sql new file mode 100644 index 00000000..5abcb498 --- /dev/null +++ b/src/main/resources/db/tour_line_add_direct_family_unit_line.sql @@ -0,0 +1,3 @@ +-- 线路管理增加是否直系亲属线路字段,默认否。 +ALTER TABLE `tour_line` + ADD COLUMN `directFamilyUnitLine` tinyint(1) DEFAULT 0 COMMENT '是否直系亲属线路' AFTER `openFlag`; diff --git a/src/main/resources/db/tour_line_add_open_flag.sql b/src/main/resources/db/tour_line_add_open_flag.sql new file mode 100644 index 00000000..eb9dd2d0 --- /dev/null +++ b/src/main/resources/db/tour_line_add_open_flag.sql @@ -0,0 +1,3 @@ +-- 线路管理增加是否对外开放字段,默认是。 +ALTER TABLE `tour_line` + ADD COLUMN `openFlag` tinyint(1) DEFAULT 1 COMMENT '是否对外开放' AFTER `mobileThumb`; diff --git a/src/main/resources/db/tour_matter_add_creator.sql b/src/main/resources/db/tour_matter_add_creator.sql new file mode 100644 index 00000000..7f48198a --- /dev/null +++ b/src/main/resources/db/tour_matter_add_creator.sql @@ -0,0 +1,7 @@ +-- 疗休养事项增加创建人字段。 +ALTER TABLE `tour_matter` + ADD COLUMN `creatorUserId` varchar(32) DEFAULT NULL COMMENT '创建人ID' AFTER `settingId`, + ADD COLUMN `creatorName` varchar(100) DEFAULT NULL COMMENT '创建人' AFTER `creatorUserId`; + +ALTER TABLE `tour_matter` + ADD KEY `idx_tour_matter_creator` (`creatorUserId`); diff --git a/src/main/resources/db/tour_matter_batch_create.sql b/src/main/resources/db/tour_matter_batch_create.sql new file mode 100644 index 00000000..4a02d0f5 --- /dev/null +++ b/src/main/resources/db/tour_matter_batch_create.sql @@ -0,0 +1,26 @@ +-- 疗休养事项批次表。 +CREATE TABLE IF NOT EXISTS `tour_matter_batch` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `matterId` varchar(32) DEFAULT NULL COMMENT '事项ID', + `lineId` varchar(32) DEFAULT NULL COMMENT '线路ID', + `batchName` 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 '出行结束时间', + `changeDeadline` 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 '创建时间', + `updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人', + `updatedAt` bigint DEFAULT NULL COMMENT '修改时间', + `delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记', + PRIMARY KEY (`id`), + KEY `idx_tour_matter_batch_matter` (`matterId`), + KEY `idx_tour_matter_batch_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 new file mode 100644 index 00000000..ae202a18 --- /dev/null +++ b/src/main/resources/db/tour_matter_create.sql @@ -0,0 +1,37 @@ +-- 疗休养事项表。 +CREATE TABLE IF NOT EXISTS `tour_matter` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `year` int DEFAULT NULL COMMENT '年度', + `matterName` varchar(100) DEFAULT NULL COMMENT '事项名称', + `settingId` varchar(32) DEFAULT NULL COMMENT '疗休养配置ID', + `creatorUserId` varchar(32) DEFAULT NULL COMMENT '创建人ID', + `creatorName` varchar(100) DEFAULT NULL COMMENT '创建人', + `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 '创建时间', + `updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人', + `updatedAt` bigint DEFAULT NULL COMMENT '修改时间', + `delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_tour_matter_year_name` (`year`, `matterName`), + KEY `idx_tour_matter_year` (`year`), + 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_line` (`lineId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项'; diff --git a/src/main/resources/db/tour_setting_add_activity_group.sql b/src/main/resources/db/tour_setting_add_activity_group.sql new file mode 100644 index 00000000..188e4fd6 --- /dev/null +++ b/src/main/resources/db/tour_setting_add_activity_group.sql @@ -0,0 +1,6 @@ +-- Add activity user scope reference for tour settings. +ALTER TABLE `tour_setting` + ADD COLUMN `activityGroupId` varchar(32) DEFAULT NULL COMMENT '可参加人员范围ID' AFTER `tourType`; + +ALTER TABLE `tour_setting` + ADD KEY `idx_tour_setting_activity_group` (`activityGroupId`); 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_cycle_allowed_times.sql b/src/main/resources/db/tour_setting_add_cycle_allowed_times.sql new file mode 100644 index 00000000..103af786 --- /dev/null +++ b/src/main/resources/db/tour_setting_add_cycle_allowed_times.sql @@ -0,0 +1,3 @@ +-- 疗休养配置增加周期允许次数字段。 +ALTER TABLE `tour_setting` + ADD COLUMN `cycleAllowedTimes` int DEFAULT NULL COMMENT '周期允许次数' AFTER `cycleTotalCost`; diff --git a/src/main/resources/db/tour_setting_add_cycle_total_cost.sql b/src/main/resources/db/tour_setting_add_cycle_total_cost.sql new file mode 100644 index 00000000..954ac7cf --- /dev/null +++ b/src/main/resources/db/tour_setting_add_cycle_total_cost.sql @@ -0,0 +1,2 @@ +ALTER TABLE `tour_setting` + ADD COLUMN `cycleTotalCost` int DEFAULT NULL COMMENT '周期内总费用' AFTER `cycleEndYear`; diff --git a/src/main/resources/db/tour_setting_add_cycle_year.sql b/src/main/resources/db/tour_setting_add_cycle_year.sql new file mode 100644 index 00000000..d26ec641 --- /dev/null +++ b/src/main/resources/db/tour_setting_add_cycle_year.sql @@ -0,0 +1,4 @@ +-- Add optional cycle year range for tour settings. +ALTER TABLE `tour_setting` + ADD COLUMN `cycleStartYear` int DEFAULT NULL COMMENT '周期开始年度' AFTER `outProvinceRatio`, + ADD COLUMN `cycleEndYear` int DEFAULT NULL COMMENT '周期结束年度' AFTER `cycleStartYear`; diff --git a/src/main/resources/db/tour_setting_add_fill_bed_info.sql b/src/main/resources/db/tour_setting_add_fill_bed_info.sql new file mode 100644 index 00000000..c4c68d29 --- /dev/null +++ b/src/main/resources/db/tour_setting_add_fill_bed_info.sql @@ -0,0 +1,3 @@ +-- 疗休养配置增加是否填报床位信息字段,默认开启以兼容历史配置。 +ALTER TABLE `tour_setting` + ADD COLUMN `fillBedInfo` tinyint(1) DEFAULT 1 COMMENT '是否填报床位信息' AFTER `allowFamily`; diff --git a/src/main/resources/db/tour_setting_add_out_province_ratio_type.sql b/src/main/resources/db/tour_setting_add_out_province_ratio_type.sql new file mode 100644 index 00000000..6e9567be --- /dev/null +++ b/src/main/resources/db/tour_setting_add_out_province_ratio_type.sql @@ -0,0 +1,3 @@ +ALTER TABLE `tour_setting` + ADD COLUMN `outProvinceRatioType` varchar(50) DEFAULT '当年报名人数' COMMENT '省外人数占比类型' AFTER `outProvinceRatio`, + ADD COLUMN `outProvinceFixedPeople` int DEFAULT 0 COMMENT '省外固定人数' AFTER `outProvinceRatioType`; 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 new file mode 100644 index 00000000..05caa494 --- /dev/null +++ b/src/main/resources/db/tour_setting_create.sql @@ -0,0 +1,257 @@ +-- 普惠疗休养基础配置表。 +-- 若生产环境未开启 Nutz 自动建表,请先执行本脚本再使用“疗休养设置”菜单。 +CREATE TABLE IF NOT EXISTS `tour_setting` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `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 '最多成团人数', + `outProvinceYears` int DEFAULT NULL COMMENT '省外几年去一次', + `outProvinceRatio` decimal(10,2) DEFAULT NULL COMMENT '省外人数占比', + `outProvinceRatioType` varchar(50) DEFAULT '当年报名人数' COMMENT '省外人数占比类型', + `outProvinceFixedPeople` int DEFAULT 0 COMMENT '省外固定人数', + `cycleStartYear` int DEFAULT NULL COMMENT '周期开始年度', + `cycleEndYear` int DEFAULT NULL COMMENT '周期结束年度', + `cycleTotalCost` int DEFAULT NULL COMMENT '周期内总费用', + `cycleAllowedTimes` int DEFAULT NULL COMMENT '周期允许次数', + `allowFamily` tinyint(1) DEFAULT 0 COMMENT '是否允许携带家属', + `fillBedInfo` tinyint(1) DEFAULT 1 COMMENT '是否填报床位信息', + `enabled` tinyint(1) DEFAULT 1 COMMENT '是否启用', + `serviceNotice` 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_setting_year` (`year`), + KEY `idx_tour_setting_activity_group` (`activityGroupId`), + KEY `idx_tour_setting_sort` (`sortNo`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养配置'; + +-- 疗休养配置标段表。 +-- 标段先挂在配置上维护,后续线路、旅行社、报名等模块可继续通过 lotId 做业务关联。 +CREATE TABLE IF NOT EXISTS `tour_setting_lot` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `settingId` varchar(32) DEFAULT NULL COMMENT '所属配置ID', + `lotName` varchar(50) DEFAULT NULL COMMENT '标段名称', + `lotValue` varchar(50) DEFAULT NULL COMMENT '标段值', + `activityCost` int DEFAULT NULL COMMENT '标段费用', + `allowOverReimbursement` tinyint(1) 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_lot_setting` (`settingId`), + 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', + `year` int DEFAULT NULL COMMENT '年度', + `agencyCode` varchar(50) DEFAULT NULL COMMENT '旅行社编号', + `agencyName` varchar(100) DEFAULT NULL COMMENT '旅行社名称', + `contactName` varchar(30) DEFAULT NULL COMMENT '联系人', + `contactPhone` varchar(30) DEFAULT NULL COMMENT '联系人手机', + `email` varchar(100) DEFAULT NULL COMMENT '邮箱', + `remark` text COMMENT '备注', + `mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图', + `enabled` tinyint(1) DEFAULT 1 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_travel_agency_year_code` (`year`, `agencyCode`), + KEY `idx_tour_travel_agency_name` (`agencyName`), + KEY `idx_tour_travel_agency_contact` (`contactName`, `contactPhone`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养旅行社'; + +-- 线路管理表。 +CREATE TABLE IF NOT EXISTS `tour_line` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `year` int DEFAULT NULL COMMENT '创建年度', + `lineCode` varchar(50) DEFAULT NULL COMMENT '线路编号', + `lineName` varchar(100) DEFAULT NULL COMMENT '线路名称', + `travelAgencyId` varchar(32) DEFAULT NULL COMMENT '旅行社ID', + `creatorUserId` varchar(32) DEFAULT NULL COMMENT '创建人ID', + `creatorName` varchar(100) DEFAULT NULL COMMENT '创建人', + `unitId` varchar(32) DEFAULT NULL COMMENT '所在单位ID', + `unitName` varchar(100) DEFAULT NULL COMMENT '所在单位', + `lineType` varchar(50) DEFAULT NULL COMMENT '线路类型', + `lotId` varchar(32) DEFAULT NULL COMMENT '时间标段ID', + `lineContent` text COMMENT '线路内容', + `mobileThumb` varchar(500) DEFAULT NULL COMMENT '移动端缩略图', + `openFlag` tinyint(1) DEFAULT 1 COMMENT '是否对外开放', + `directFamilyUnitLine` tinyint(1) DEFAULT 0 COMMENT '是否直系亲属线路', + `enabled` tinyint(1) DEFAULT 1 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_line_year_code` (`year`, `lineCode`), + KEY `idx_tour_line_year` (`year`), + KEY `idx_tour_line_name` (`lineName`), + KEY `idx_tour_line_agency` (`travelAgencyId`), + KEY `idx_tour_line_creator` (`creatorUserId`), + KEY `idx_tour_line_unit` (`unitId`), + KEY `idx_tour_line_lot` (`lotId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养线路'; + +-- 疗休养事项表。 +CREATE TABLE IF NOT EXISTS `tour_matter` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `year` int DEFAULT NULL COMMENT '年度', + `matterName` varchar(100) DEFAULT NULL COMMENT '事项名称', + `settingId` varchar(32) DEFAULT NULL COMMENT '疗休养配置ID', + `creatorUserId` varchar(32) DEFAULT NULL COMMENT '创建人ID', + `creatorName` varchar(100) DEFAULT NULL COMMENT '创建人', + `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 '创建时间', + `updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人', + `updatedAt` bigint DEFAULT NULL COMMENT '修改时间', + `delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_tour_matter_year_name` (`year`, `matterName`), + KEY `idx_tour_matter_year` (`year`), + 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_line` (`lineId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项'; + +-- 疗休养事项批次表。 +CREATE TABLE IF NOT EXISTS `tour_matter_batch` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `matterId` varchar(32) DEFAULT NULL COMMENT '事项ID', + `lineId` varchar(32) DEFAULT NULL COMMENT '线路ID', + `batchName` 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 '出行结束时间', + `changeDeadline` 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 '创建时间', + `updatedBy` varchar(32) DEFAULT NULL COMMENT '修改人', + `updatedAt` bigint DEFAULT NULL COMMENT '修改时间', + `delFlag` tinyint(1) DEFAULT 0 COMMENT '删除标记', + PRIMARY KEY (`id`), + KEY `idx_tour_matter_batch_matter` (`matterId`), + KEY `idx_tour_matter_batch_line` (`lineId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养事项批次'; + +-- 直系亲属线路报名信息表。 +CREATE TABLE IF NOT EXISTS `tour_ledger_direct_relative` ( + `id` varchar(32) NOT NULL COMMENT 'ID', + `ledgerId` varchar(32) DEFAULT NULL COMMENT '台账ID', + `relativeName` varchar(100) DEFAULT NULL COMMENT '亲属姓名', + `unitName` varchar(100) DEFAULT NULL COMMENT '所在单位', + `relationshipCode` varchar(50) DEFAULT NULL COMMENT '亲属关系编码', + `relationshipName` varchar(50) DEFAULT NULL COMMENT '亲属关系', + `lineId` varchar(32) DEFAULT NULL COMMENT '线路ID', + `lineName` varchar(100) DEFAULT NULL COMMENT '线路名称', + `travelStartTime` varchar(20) DEFAULT NULL COMMENT '出行开始日期', + `travelEndTime` varchar(20) 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`), + KEY `idx_tour_ledger_direct_relative_ledger` (`ledgerId`), + KEY `idx_tour_ledger_direct_relative_line` (`lineId`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='普惠疗休养直系亲属线路报名信息'; diff --git a/src/main/resources/db/tour_setting_lot_add_allow_over_reimbursement.sql b/src/main/resources/db/tour_setting_lot_add_allow_over_reimbursement.sql new file mode 100644 index 00000000..54447377 --- /dev/null +++ b/src/main/resources/db/tour_setting_lot_add_allow_over_reimbursement.sql @@ -0,0 +1,2 @@ +ALTER TABLE `tour_setting_lot` + ADD COLUMN `allowOverReimbursement` tinyint(1) DEFAULT 0 COMMENT '允许超出报销' AFTER `activityCost`; 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/assets/platform/images/tour/tour-direct-family-icon.png b/src/main/resources/static/assets/platform/images/tour/tour-direct-family-icon.png new file mode 100644 index 00000000..4c97f1fa Binary files /dev/null and b/src/main/resources/static/assets/platform/images/tour/tour-direct-family-icon.png differ diff --git a/src/main/resources/static/assets/platform/images/tour/tour-direct-family.jpg b/src/main/resources/static/assets/platform/images/tour/tour-direct-family.jpg new file mode 100644 index 00000000..530371e6 Binary files /dev/null and b/src/main/resources/static/assets/platform/images/tour/tour-direct-family.jpg differ diff --git a/src/main/resources/static/assets/platform/images/tour/tour-h5-banner-1.jpg b/src/main/resources/static/assets/platform/images/tour/tour-h5-banner-1.jpg new file mode 100644 index 00000000..4d01bd6e Binary files /dev/null and b/src/main/resources/static/assets/platform/images/tour/tour-h5-banner-1.jpg differ diff --git a/src/main/resources/static/assets/platform/images/tour/tour-h5-banner-2.jpg b/src/main/resources/static/assets/platform/images/tour/tour-h5-banner-2.jpg new file mode 100644 index 00000000..f304104d Binary files /dev/null and b/src/main/resources/static/assets/platform/images/tour/tour-h5-banner-2.jpg differ diff --git a/src/main/resources/static/assets/platform/images/tour/tour-in-province.png b/src/main/resources/static/assets/platform/images/tour/tour-in-province.png new file mode 100644 index 00000000..82be9792 Binary files /dev/null and b/src/main/resources/static/assets/platform/images/tour/tour-in-province.png differ diff --git a/src/main/resources/static/assets/platform/images/tour/tour-out-province.png b/src/main/resources/static/assets/platform/images/tour/tour-out-province.png new file mode 100644 index 00000000..c67e5b9e Binary files /dev/null and b/src/main/resources/static/assets/platform/images/tour/tour-out-province.png differ 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 @@