From 67a46212e26e8ac2cf6800ce316b96dabb6b5de3 Mon Sep 17 00:00:00 2001 From: zhangrui Date: Tue, 2 Jun 2026 14:28:36 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E7=96=97=E4=BC=91=E5=85=BB1.0=E7=89=88?= =?UTF-8?q?=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../budwk/app/base/constant/RoleConstant.java | 1 + .../impl/SysDataUnitPullServiceImpl.java | 8 +- .../basic/models/ActivityUserScope.java | 13 +- .../service/ActivityBasicScopeService.java | 45 + .../impl/ActivityBasicScopeServiceImpl.java | 123 ++ .../tour/controller/TourGroupController.java | 625 ++++++ .../tour/controller/TourLedgerController.java | 959 +++++++++ .../tour/controller/TourLineController.java | 297 +++ .../tour/controller/TourMatterController.java | 441 ++++ .../controller/TourMySignupController.java | 1846 +++++++++++++++++ .../controller/TourPlaceholderController.java | 13 + .../TourSchoolUnionApprovalController.java | 581 ++++++ .../controller/TourSettingController.java | 246 +++ .../tour/controller/TourSignupController.java | 1487 +++++++++++++ .../TourTravelAgencyController.java | 160 ++ .../TourUnionApprovalController.java | 581 ++++++ .../controller/TourUnionLedgerController.java | 905 ++++++++ .../tour/mode/TourLedgerImportExcelMode.java | 34 + .../dayofficework/tour/models/TourLedger.java | 152 ++ .../tour/models/TourLedgerDirectRelative.java | 73 + .../tour/models/TourLedgerFamily.java | 83 + .../dayofficework/tour/models/TourLine.java | 106 + .../dayofficework/tour/models/TourMatter.java | 124 ++ .../tour/models/TourSetting.java | 134 ++ .../tour/models/TourSettingLot.java | 53 + .../tour/models/TourTravelAgency.java | 74 + .../TourLedgerDirectRelativeService.java | 7 + .../tour/service/TourLedgerFamilyService.java | 7 + .../tour/service/TourLedgerService.java | 7 + .../tour/service/TourLineService.java | 7 + .../tour/service/TourMatterService.java | 7 + .../tour/service/TourSettingService.java | 7 + .../tour/service/TourTravelAgencyService.java | 7 + .../TourLedgerDirectRelativeServiceImpl.java | 15 + .../impl/TourLedgerFamilyServiceImpl.java | 15 + .../service/impl/TourLedgerServiceImpl.java | 15 + .../service/impl/TourLineServiceImpl.java | 15 + .../service/impl/TourMatterServiceImpl.java | 15 + .../service/impl/TourSettingServiceImpl.java | 15 + .../impl/TourTravelAgencyServiceImpl.java | 15 + .../common/ProposalCommonController.java | 4 +- .../config/ProposalConfigUnitController.java | 19 + ...activity_user_scope_add_group_type_sql.sql | 8 + src/main/resources/db/menu/init_tour_menu.sql | 122 ++ src/main/resources/db/tour_dict_init.sql | 253 +++ .../resources/db/tour_ledger_add_matter.sql | 7 + .../tour_ledger_add_over_cost_reimbursed.sql | 2 + .../db/tour_ledger_add_query_indexes.sql | 9 + .../db/tour_ledger_direct_relative_create.sql | 21 + .../db/tour_line_add_creator_unit.sql | 10 + .../tour_line_add_direct_family_unit_line.sql | 3 + .../resources/db/tour_line_add_open_flag.sql | 3 + .../resources/db/tour_matter_add_creator.sql | 7 + .../resources/db/tour_matter_batch_create.sql | 26 + src/main/resources/db/tour_matter_create.sql | 25 + .../db/tour_setting_add_activity_group.sql | 6 + .../tour_setting_add_cycle_allowed_times.sql | 3 + .../db/tour_setting_add_cycle_total_cost.sql | 2 + .../db/tour_setting_add_cycle_year.sql | 4 + .../db/tour_setting_add_fill_bed_info.sql | 3 + ...ur_setting_add_out_province_ratio_type.sql | 3 + src/main/resources/db/tour_setting_create.sql | 183 ++ ...tting_lot_add_allow_over_reimbursement.sql | 2 + .../images/tour/tour-direct-family-icon.png | Bin 0 -> 19846 bytes .../images/tour/tour-direct-family.jpg | Bin 0 -> 93735 bytes .../platform/images/tour/tour-h5-banner-1.jpg | Bin 0 -> 68954 bytes .../platform/images/tour/tour-h5-banner-2.jpg | Bin 0 -> 57069 bytes .../platform/images/tour/tour-in-province.png | Bin 0 -> 7906 bytes .../images/tour/tour-out-province.png | Bin 0 -> 6172 bytes .../direct_relative_reimbursement_apply.docx | Bin 0 -> 18933 bytes .../tour/over_cost_reimbursement_apply.docx | Bin 0 -> 16866 bytes .../zhgh/dayofficework/Tour/group/index.html | 353 ++++ .../zhgh/dayofficework/Tour/ledger/index.html | 653 ++++++ .../zhgh/dayofficework/Tour/matter/index.html | 712 +++++++ .../dayofficework/Tour/mysignup/index.html | 1391 +++++++++++++ .../dayofficework/Tour/placeholder/index.html | 23 + .../zhgh/dayofficework/Tour/route/index.html | 432 ++++ .../Tour/schoolUnionApproval/index.html | 549 +++++ .../dayofficework/Tour/setting/index.html | 636 ++++++ .../zhgh/dayofficework/Tour/signup/index.html | 1012 +++++++++ .../Tour/travelAgency/index.html | 338 +++ .../Tour/unionApproval/index.html | 549 +++++ .../dayofficework/Tour/unionledger/index.html | 609 ++++++ .../dayofficework/tour/mysignup/index.html | 1053 ++++++++++ .../tour/schoolUnionApproval/index.html | 480 +++++ .../dayofficework/tour/signup/apply.html | 570 +++++ .../dayofficework/tour/signup/index.html | 403 ++++ .../dayofficework/tour/signup/lineInfo.html | 587 ++++++ .../dayofficework/tour/signup/list.html | 794 +++++++ .../tour/unionApproval/index.html | 455 ++++ 90 files changed, 21639 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourGroupController.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLedgerController.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLineController.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMatterController.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMySignupController.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourPlaceholderController.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSchoolUnionApprovalController.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSettingController.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSignupController.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourTravelAgencyController.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourUnionApprovalController.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourUnionLedgerController.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/mode/TourLedgerImportExcelMode.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedger.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedgerDirectRelative.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedgerFamily.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLine.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourMatter.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSetting.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSettingLot.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourTravelAgency.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerDirectRelativeService.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerFamilyService.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLedgerService.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourLineService.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourMatterService.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourSettingService.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourTravelAgencyService.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerDirectRelativeServiceImpl.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerFamilyServiceImpl.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLedgerServiceImpl.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourLineServiceImpl.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourMatterServiceImpl.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourSettingServiceImpl.java create mode 100644 src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourTravelAgencyServiceImpl.java create mode 100644 src/main/resources/db/activity_user_scope_add_group_type_sql.sql create mode 100644 src/main/resources/db/menu/init_tour_menu.sql create mode 100644 src/main/resources/db/tour_dict_init.sql create mode 100644 src/main/resources/db/tour_ledger_add_matter.sql create mode 100644 src/main/resources/db/tour_ledger_add_over_cost_reimbursed.sql create mode 100644 src/main/resources/db/tour_ledger_add_query_indexes.sql create mode 100644 src/main/resources/db/tour_ledger_direct_relative_create.sql create mode 100644 src/main/resources/db/tour_line_add_creator_unit.sql create mode 100644 src/main/resources/db/tour_line_add_direct_family_unit_line.sql create mode 100644 src/main/resources/db/tour_line_add_open_flag.sql create mode 100644 src/main/resources/db/tour_matter_add_creator.sql create mode 100644 src/main/resources/db/tour_matter_batch_create.sql create mode 100644 src/main/resources/db/tour_matter_create.sql create mode 100644 src/main/resources/db/tour_setting_add_activity_group.sql create mode 100644 src/main/resources/db/tour_setting_add_cycle_allowed_times.sql create mode 100644 src/main/resources/db/tour_setting_add_cycle_total_cost.sql create mode 100644 src/main/resources/db/tour_setting_add_cycle_year.sql create mode 100644 src/main/resources/db/tour_setting_add_fill_bed_info.sql create mode 100644 src/main/resources/db/tour_setting_add_out_province_ratio_type.sql create mode 100644 src/main/resources/db/tour_setting_create.sql create mode 100644 src/main/resources/db/tour_setting_lot_add_allow_over_reimbursement.sql create mode 100644 src/main/resources/static/assets/platform/images/tour/tour-direct-family-icon.png create mode 100644 src/main/resources/static/assets/platform/images/tour/tour-direct-family.jpg create mode 100644 src/main/resources/static/assets/platform/images/tour/tour-h5-banner-1.jpg create mode 100644 src/main/resources/static/assets/platform/images/tour/tour-h5-banner-2.jpg create mode 100644 src/main/resources/static/assets/platform/images/tour/tour-in-province.png create mode 100644 src/main/resources/static/assets/platform/images/tour/tour-out-province.png create mode 100644 src/main/resources/templates/tour/direct_relative_reimbursement_apply.docx create mode 100644 src/main/resources/templates/tour/over_cost_reimbursement_apply.docx create mode 100644 src/main/resources/views/platform/zhgh/dayofficework/Tour/group/index.html create mode 100644 src/main/resources/views/platform/zhgh/dayofficework/Tour/ledger/index.html create mode 100644 src/main/resources/views/platform/zhgh/dayofficework/Tour/matter/index.html create mode 100644 src/main/resources/views/platform/zhgh/dayofficework/Tour/mysignup/index.html create mode 100644 src/main/resources/views/platform/zhgh/dayofficework/Tour/placeholder/index.html create mode 100644 src/main/resources/views/platform/zhgh/dayofficework/Tour/route/index.html create mode 100644 src/main/resources/views/platform/zhgh/dayofficework/Tour/schoolUnionApproval/index.html create mode 100644 src/main/resources/views/platform/zhgh/dayofficework/Tour/setting/index.html create mode 100644 src/main/resources/views/platform/zhgh/dayofficework/Tour/signup/index.html create mode 100644 src/main/resources/views/platform/zhgh/dayofficework/Tour/travelAgency/index.html create mode 100644 src/main/resources/views/platform/zhgh/dayofficework/Tour/unionApproval/index.html create mode 100644 src/main/resources/views/platform/zhgh/dayofficework/Tour/unionledger/index.html create mode 100644 src/main/resources/views/platform/zhghh5/dayofficework/tour/mysignup/index.html create mode 100644 src/main/resources/views/platform/zhghh5/dayofficework/tour/schoolUnionApproval/index.html create mode 100644 src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/apply.html create mode 100644 src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/index.html create mode 100644 src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/lineInfo.html create mode 100644 src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/list.html create mode 100644 src/main/resources/views/platform/zhghh5/dayofficework/tour/unionApproval/index.html 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/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/dayofficework/tour/controller/TourGroupController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourGroupController.java new file mode 100644 index 00000000..13232d43 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourGroupController.java @@ -0,0 +1,625 @@ +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); + 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())); + } + } + + 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/TourLedgerController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLedgerController.java new file mode 100644 index 00000000..7bbbbdf4 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourLedgerController.java @@ -0,0 +1,959 @@ +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, + 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 ( + 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 + @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 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..9905aaa2 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMatterController.java @@ -0,0 +1,441 @@ +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 + @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.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..bbe52fe2 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourMySignupController.java @@ -0,0 +1,1846 @@ +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 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 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.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 + 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)); + } + + @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()); + 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); + 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())); + 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())); + return Result.success().addMsg("取消报名成功"); + } + + @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 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/TourSettingController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSettingController.java new file mode 100644 index 00000000..08dab7e9 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSettingController.java @@ -0,0 +1,246 @@ +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 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.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 { + + @Inject + private TourSettingService tourSettingService; + + @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); + 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); + return Result.success(tourSetting); + } + + @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 = "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("同年度下配置名称已存在"); + } + + // 布尔值给默认值,避免前端未传时出现空状态。 + 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.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"); + } + } else { + // 编辑时先处理页面删除的标段,再保存配置和当前标段行。 + if (Lang.isNotEmpty(lotDeleteList)) { + tourSettingService.dao().clear(TourSettingLot.class, Cnd.where(TourSettingLot::getId, "in", lotDeleteList)); + } + tourSettingService.updateIgnoreNull(tourSetting); + saveLots(tourSetting); + } + 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.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("配置名称不能为空"); + } + 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 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..fff48ec1 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/controller/TourSignupController.java @@ -0,0 +1,1487 @@ +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 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 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.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 + 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)); + } + + @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("报名事项配置不存在或已停用"); + } + Integer activityGroupId = parseInteger(setting.getActivityGroupId()); + if (activityGroupId == null || !activityBasicScopeService.isUserInGroup(activityGroupId, SecurityUtil.getUserId())) { + return Result.error("您不在本次疗休养报名范围内"); + } + 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 (!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("报名事项配置不存在或已停用"); + } + 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("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)); + } + + @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()); + 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); + } + 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())); + return Result.success().addMsg("取消报名成功"); + } + + 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("报名事项配置不存在或已停用"); + } + 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 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/TourLedger.java b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedger.java new file mode 100644 index 00000000..0e1af548 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourLedger.java @@ -0,0 +1,152 @@ +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.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..f06aec8a --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourMatter.java @@ -0,0 +1,124 @@ +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 = 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..37e212d4 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/models/TourSetting.java @@ -0,0 +1,134 @@ +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("可参加人员范围ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String activityGroupId; + + @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; + + @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/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/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..ca1cb4ef --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/TourSettingService.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.TourSetting; + +public interface TourSettingService extends BaseService { +} 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/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..68f57fcf --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/tour/service/impl/TourSettingServiceImpl.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.TourSetting; +import com.budwk.app.zhgh.dayofficework.tour.service.TourSettingService; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.IocBean; + +@IocBean(args = {"refer:dao"}) +public class TourSettingServiceImpl extends BaseServiceImpl implements TourSettingService { + + public TourSettingServiceImpl(Dao dao) { + super(dao); + } +} 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/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_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_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..7a883bfe --- /dev/null +++ b/src/main/resources/db/tour_matter_create.sql @@ -0,0 +1,25 @@ +-- 疗休养事项表。 +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 '移动端缩略图', + `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`) +) 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_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_create.sql b/src/main/resources/db/tour_setting_create.sql new file mode 100644 index 00000000..4dd97880 --- /dev/null +++ b/src/main/resources/db/tour_setting_create.sql @@ -0,0 +1,183 @@ +-- 普惠疗休养基础配置表。 +-- 若生产环境未开启 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 '疗休养类型', + `activityGroupId` varchar(32) DEFAULT NULL COMMENT '可参加人员范围ID', + `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_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 '移动端缩略图', + `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`) +) 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/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 0000000000000000000000000000000000000000..4c97f1fa2c81713c9639a857a6f7d4f64a6da22c GIT binary patch literal 19846 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..530371e66994007643a2e13ab99497a57f32bfa6 GIT binary patch literal 93735 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�r5pYJ*5?tWYBPU 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 z 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f304104d5ee2f4499ff9792ecfecebab90b37107 GIT binary patch literal 57069 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

YcIlpMX`E_cSjTs}e2a#yn+8Xy z>dFhT-a1eN5~b3>%aR(a)+xoK&#U%R#!}&HwbYF2@18#zb=M(mcLZjWL*2!Y<)TvL z#dZy8$6}ePAE#4wdc)JL&r4 z!YC<%|3;v>G!6ajX7eu5RKqB!A1vjXxTW8E6RV(l_E6KYe=Wt1bXz@2z0aopIXSlJ`_Jch!?I zS}A-=8q@NAiEwdT$E=4J)8hG9WS?5gW)dQ1+VkMPF0~h98Uz?UX*RzSxlK>xK97R# z>n?W~LQhQQ>ZV=FM;XV_1vU$qYX4`^u5_Ph3tBf^j#pY-&S;XIvw8yChIFk@6e47C z7-B|UM4vd5(@*EbB-Xg*kQ|9Mt3(x(<3?~x^Ioy23N;p2|2QrS0UJU5e$cy6&p=0@ zRVG0_f!D<&XTp9a6${j z9nO~nkRYeui;RHmp%^vbO7<8~?*djbb^gdOx|-^bwH8AvszS+yP+pL%N#a#6Huoy? zB_u7luhb)+?@XOE&*N76ugINUp;~zL5N9ooZi?i_bO|7)kvhoIRUUcFrlnguZgj

(IU8 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 D70)Fw literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..82be97923579c984ac247441be1dcec0434e538a GIT binary patch literal 7906 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 EKjEY3#{d8T literal 0 HcmV?d00001 diff --git a/src/main/resources/templates/tour/over_cost_reimbursement_apply.docx b/src/main/resources/templates/tour/over_cost_reimbursement_apply.docx new file mode 100644 index 0000000000000000000000000000000000000000..7668c7ef415a7aa21400332343d5972ae46496fc GIT binary patch literal 16866 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 YvQki>3HQ@>K>?tHHVHoS{`~a+0NmPHI{*Lx literal 0 HcmV?d00001 diff --git a/src/main/resources/views/platform/zhgh/dayofficework/Tour/group/index.html b/src/main/resources/views/platform/zhgh/dayofficework/Tour/group/index.html new file mode 100644 index 00000000..7e65acca --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/Tour/group/index.html @@ -0,0 +1,353 @@ + + +

+ + + + + + + + + + + + + + + + + + + + + +
+ 搜索 + 重置 +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + +
+ 搜索 + 重置 +
+
+
+ + +
报名人员列表
+ + + + + + + + + + + + +
+
+
+
+
+ + + + + + diff --git a/src/main/resources/views/platform/zhgh/dayofficework/Tour/ledger/index.html b/src/main/resources/views/platform/zhgh/dayofficework/Tour/ledger/index.html new file mode 100644 index 00000000..1dbbc148 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/Tour/ledger/index.html @@ -0,0 +1,653 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 搜索 + 重置 +
+
+
+ + +
+
+ +
+
+ 参加人员导入 + 设置参加人员 + 导出分工会报名压缩包 +
+
+ + 参加直系亲属单位线路人员 + + + 申请超出费用由单位承担人员 + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
温馨提醒:走审核流程的报名,审核通过后才在台账中显示
+
+
+ + + + + +
+
教职工信息
+ + {{ detail.jobNo || '' }} + {{ detail.userName || '' }} + {{ detail.gender || '' }} + {{ detail.idCard || '' }} + {{ detail.unitName || '' }} + {{ detail.unionName || '' }} + +
+ +
+
+ 报名信息 + 点击查看流程图 +
+ + {{ detail.signupTime || '' }} + {{ detail.lineName || '' }} + {{ detail.lineType || '' }} + {{ detail.hotelName || '' }} + {{ detail.travelAgencyName || '' }} + {{ detail.travelPeriod || '' }} + {{ detail.bedType || '' }} + {{ detail.bedInfo || '' }} + {{ detail.intendedRoommate || '' }} + {{ familyText() }} + {{ detail.joined ? '是' : '否' }} + {{ detail.reimbursed ? '是' : '否' }} + {{ detail.overCostReimbursed ? '是' : '否' }} + +
+ +
+
直系亲属线路
+ + {{ directRelative.relativeName || '' }} + {{ directRelative.unitName || '' }} + {{ directRelative.relationshipName || '' }} + {{ directRelative.lineName || '' }} + {{ directRelative.travelStartTime || '' }} + {{ directRelative.travelEndTime || '' }} + +
+ +
+
家属信息
+ + + + + + + + + + + +
+ + + + + + + 确定 + +
+
+ + + + + + diff --git a/src/main/resources/views/platform/zhgh/dayofficework/Tour/matter/index.html b/src/main/resources/views/platform/zhgh/dayofficework/Tour/matter/index.html new file mode 100644 index 00000000..ce7e8c96 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/Tour/matter/index.html @@ -0,0 +1,712 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + +
+ 搜索 + 重置 +
+
+
+ + + + + + 新增 + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + 启用 + 禁用 + + + + + + + + + + + + + + + + + + + + + + + + + + + + 取消 + 保存 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 取消 + 确定 + + +
+ + + + + + diff --git a/src/main/resources/views/platform/zhgh/dayofficework/Tour/mysignup/index.html b/src/main/resources/views/platform/zhgh/dayofficework/Tour/mysignup/index.html new file mode 100644 index 00000000..ed7d3569 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/Tour/mysignup/index.html @@ -0,0 +1,1391 @@ + + +
+ + + + + + + + + + + + + + + + +
+ 搜索 + 重置 +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
教职工信息
+ + {{ detail.jobNo || '' }} + {{ detail.userName || '' }} + {{ detail.gender || '' }} + {{ detail.idCard || '' }} + {{ detail.unitName || '' }} + {{ detail.unionName || '' }} + +
+ +
+
+ 报名信息 + 点击查看流程图 +
+ + {{ detail.signupTime || '' }} + {{ detail.lineName || '' }} + {{ detail.lineType || '' }} + {{ detail.hotelName || '' }} + {{ detail.travelAgencyName || '' }} + {{ detail.travelPeriod || '' }} + {{ detail.bedType || '' }} + {{ detail.bedInfo || '' }} + {{ detail.intendedRoommate || '' }} + {{ detailFamilyText() }} + {{ detail.joined ? '是' : '否' }} + {{ detail.reimbursed ? '是' : '否' }} + {{ detail.overCostReimbursed ? '是' : '否' }} + +
+ +
+
直系亲属线路
+ + {{ detailDirectRelative.relativeName || '' }} + {{ detailDirectRelative.unitName || '' }} + {{ detailDirectRelative.relationshipName || '' }} + {{ detailDirectRelative.lineName || '' }} + {{ detailDirectRelative.travelStartTime || '' }} + {{ detailDirectRelative.travelEndTime || '' }} + +
+ +
+
家属信息
+ + + + + + + + + + + +
+ + + + + + + 确定 + +
+ + + +
教职工信息
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
报名信息
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
直系亲属线路
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
亲属信息
+
+ 增加一条 +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + 返回 + 取消报名 + 提交 + +
+ + +
+
+
{{ exportType === "directRelative" ? "直系亲属疗休养申请书预览" : "费用承担申请书预览" }}
+
+ 下载PDF + 打印 + 关闭 +
+
+
+
+

5天外超出部分疗休养费用由单位承担申请书

+
学校工会:
+

+ 本人为{{ exportForm.unionName }}{{ exportForm.userName }},拟参加{{ exportForm.unionName }}于 + {{ exportForm.travelStartYear }}{{ exportForm.travelStartMonth }}{{ exportForm.travelStartDay }}日至{{ exportForm.travelEndYear }}{{ exportForm.travelEndMonth }}{{ exportForm.travelEndDay }}日组织的{{ exportForm.lineName }}线路疗休养,预计总时长{{ exportForm.totalDays }}天(含在途时间)、总费用预计{{ exportForm.estimatedCost }}元,本人申请5天以外(不含5天)超出部分费用由学校承担。 +

+

特此申请,请批准。

+ +
+
申请人:{{ exportForm.userName }}
+
时间:{{ formatDate(exportForm.applyDate) }}
+
+
+
分工会主席签字:{{ exportForm.unionAuditorName }}
+
时间:{{ formatDate(exportForm.unionAuditDate) }}
+
+
+
学校工会:盖章
+
时间:{{ formatDate(exportForm.schoolUnionAuditDate) }}
+
+
+
+

本人随同参加直系亲属单位疗休养活动暨疗休养费用由我校按规定标准给予报销申请书

+
学校工会:
+

+ 我本人是{{ directRelativeExportForm.unionName }}{{ directRelativeExportForm.userName }},我申请随同参加本人直系亲属(亲属关系为{{ directRelativeExportForm.relationshipName }})姓名{{ directRelativeExportForm.relativeName }}所在单位{{ directRelativeExportForm.relativeUnitName }}于 + {{ directRelativeExportForm.travelStartYear }}{{ directRelativeExportForm.travelStartMonth }}{{ directRelativeExportForm.travelStartDay }}日至{{ directRelativeExportForm.travelEndYear }}{{ directRelativeExportForm.travelEndMonth }}{{ directRelativeExportForm.travelEndDay }}日期间所开展的{{ directRelativeExportForm.lineName }}线路疗休养活动;同时,申请本人此次疗休养费用由我校按规定标准给予报销。 +

+

特此申请,请批准。

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