diff --git a/src/main/java/com/budwk/app/base/sms/impl/jshvc/SmsJshvcServiceImpl.java b/src/main/java/com/budwk/app/base/sms/impl/jshvc/SmsJshvcServiceImpl.java index 1827742..9b31e3f 100644 --- a/src/main/java/com/budwk/app/base/sms/impl/jshvc/SmsJshvcServiceImpl.java +++ b/src/main/java/com/budwk/app/base/sms/impl/jshvc/SmsJshvcServiceImpl.java @@ -27,13 +27,14 @@ import java.util.Map; @IocBean public class SmsJshvcServiceImpl implements SmsService { - private static final String APPID = ""; - private static final String APP_SECRET = ""; - private static final String TOKEN_URL = ""; - private static final String MSG_URL = ""; - private static final String REDIS_KEY_MSG_ACCESS_TOKEN = ""; + private static final String APPID = "1430576717768122368"; + private static final String APP_SECRET = "19A0ACB03FBQHL4R4XQD"; + private static final String TOKEN_URL = "https://gateway.jshvc.edu.cn/token/gateway/accessToken"; + private static final String MSG_URL = "https://gateway.jshvc.edu.cn/mp/restful/v2/message/send"; + private static final String REDIS_KEY_MSG_ACCESS_TOKEN = "msg:token:"; - @Inject + + @Inject private RedisService redisService; @Override diff --git a/src/main/java/com/budwk/app/flow/service/FlowCommonService.java b/src/main/java/com/budwk/app/flow/service/FlowCommonService.java index bd6b901..622cbe8 100644 --- a/src/main/java/com/budwk/app/flow/service/FlowCommonService.java +++ b/src/main/java/com/budwk/app/flow/service/FlowCommonService.java @@ -3,19 +3,38 @@ package com.budwk.app.flow.service; import cn.hutool.core.lang.Dict; import cn.hutool.core.util.ObjectUtil; import com.budwk.app.base.event.flow.jumptofirsttask.FlowJumpToFirstTaskNodePublisher; +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.engine.event.ProcessEvent; +import com.budwk.app.flow.engine.event.ProcessPublisher; +import com.budwk.app.flow.entity.ProcessInstance; +import com.budwk.app.flow.entity.ProcessTask; +import com.budwk.app.flow.enums.ProcessEventTypeEnum; +import com.budwk.app.flow.enums.ProcessInstanceStateEnum; +import com.budwk.app.flow.enums.ProcessTaskPerformTypeEnum; +import com.budwk.app.flow.enums.ProcessTaskStateEnum; import com.budwk.app.flow.enums.ProcessSubmitTypeEnum; import com.budwk.app.web.commons.auth.utils.SecurityUtil; +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.ioc.aop.Aop; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; +import java.util.List; + @IocBean public class FlowCommonService { @Inject private FlowEngine flowEngine; + @Inject + private Dao dao; + /** * 执行任务 * @param args @@ -70,4 +89,44 @@ public class FlowCommonService { } } + /** + * 撤销已流转到下一步的任务,并重新激活当前任务和流程实例。 + * 场馆预约等业务在校工会审核撤回时复用这里的流程状态处理。 + */ + @Aop(TransAop.READ_COMMITTED) + public Result revokeTask(Long taskId) { + // 自己任务 + ProcessTask selfTask = dao.fetch(ProcessTask.class, taskId); + selfTask.setTaskState(ProcessTaskStateEnum.DOING.getCode()); + + // 撤销任务 + List taskList = dao.query(ProcessTask.class, Cnd.where(ProcessTask::getTaskParentId, "=", taskId)); + for (ProcessTask task : taskList) { + task.setTaskState(ProcessTaskStateEnum.WITHDRAW.getCode()); + dao.update(task); + } + // 会签并行任务 撤销后续任务 + if (selfTask.getPerformType().equals(ProcessTaskPerformTypeEnum.COUNTERSIGN.getCode())) { + List doingTasks = dao.query(ProcessTask.class, Cnd.where(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.DOING.getCode()) + .and(ProcessTask::getProcessInstanceId, "=", selfTask.getProcessInstanceId())); + for (ProcessTask doingTask : doingTasks) { + doingTask.setTaskState(ProcessTaskStateEnum.WITHDRAW.getCode()); + dao.update(doingTask); + } + } + + // 激活 + dao.update(selfTask); + + // 流程激活 + dao.update(ProcessInstance.class, Chain.make("state", ProcessInstanceStateEnum.DOING.getCode()), Cnd.where(ProcessInstance::getId, "=", selfTask.getProcessInstanceId())); + + // 发送任务撤回事件,保证业务监听能同步处理已撤回节点。 + for (ProcessTask task : taskList) { + ProcessPublisher.notify(ProcessEvent.builder().eventType(ProcessEventTypeEnum.PROCESS_TASK_REVOKE).sourceId(task.getId()).build()); + } + + return Result.success(); + } + } diff --git a/src/main/java/com/budwk/app/flow/service/ProcessTaskService.java b/src/main/java/com/budwk/app/flow/service/ProcessTaskService.java index 44b298a..3f51ce8 100644 --- a/src/main/java/com/budwk/app/flow/service/ProcessTaskService.java +++ b/src/main/java/com/budwk/app/flow/service/ProcessTaskService.java @@ -211,4 +211,13 @@ public interface ProcessTaskService extends BaseService { */ List getDoingTaskByBizIdTaskName(List bizIds, String taskName); + /** + * 获取已结束的任务 + * + * @param bizIds 业务ID + * @param taskName 任务名称 + * @return + */ + List getDoneTaskByBizIdTaskName(List bizIds, String taskName); + } diff --git a/src/main/java/com/budwk/app/flow/service/impl/ProcessTaskServiceImpl.java b/src/main/java/com/budwk/app/flow/service/impl/ProcessTaskServiceImpl.java index 943a009..39a4e18 100644 --- a/src/main/java/com/budwk/app/flow/service/impl/ProcessTaskServiceImpl.java +++ b/src/main/java/com/budwk/app/flow/service/impl/ProcessTaskServiceImpl.java @@ -87,6 +87,16 @@ public class ProcessTaskServiceImpl extends BaseServiceImpl impleme return vo; } + @Override + public List getDoneTaskByBizIdTaskName(List bizIds, String taskName) { + List instances = dao().query(ProcessInstance.class, Cnd.where(ProcessInstance::getBusinessNo, "in", bizIds)); + List instanceIds = instances.stream().map(ProcessInstance::getId).toList(); + List tasks = dao().query(ProcessTask.class, Cnd.where(ProcessTask::getProcessInstanceId, "in", instanceIds) + .and(ProcessTask::getTaskName, "=", taskName) + .and(ProcessTask::getTaskState, "=", ProcessTaskStateEnum.FINISHED.getCode())); + return tasks; + } + @Override @Aop(TransAop.READ_COMMITTED) public void saveProcessTask(ProcessTask processTask) { 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 e2517cd..c71ff5b 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 9e25367..e1c5121 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 9a4905b..945d879 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/siteCug/controller/SiteCugApplyController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/controller/SiteCugApplyController.java new file mode 100644 index 0000000..f95e3ee --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/controller/SiteCugApplyController.java @@ -0,0 +1,823 @@ +package com.budwk.app.zhgh.dayofficework.siteCug.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaMode; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +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.PageUtil; +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.sys.models.SysHoliday; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugApply; +import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugFunctionType; +import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo; +import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugApplyService; +import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugInfoService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.nutz.aop.interceptor.ioc.TransAop; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +import org.nutz.dao.Sqls; +import org.nutz.dao.sql.Sql; +import org.nutz.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 org.nutz.mvc.annotation.Param; + +import java.time.Year; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +@Slf4j +@IocBean +@Ok("json:full") +@Api(tags = "\u573a\u5730\u9884\u7ea6-\u573a\u9986") +@At("/platform/siteCug/apply") +public class SiteCugApplyController { + + private static final String YEARLY_BATCH_PREFIX = "YEARLY_BATCH:"; + + @Inject + private Dao dao; + @Inject + private SiteCugInfoService infoService; + @Inject + private SiteCugApplyService applyService; + @Inject + private FlowEngine flowEngine; + + @At("/") + @SaCheckPermission("siteCug.apply") + @Ok("beetl:/platform/zhgh/dayofficework/siteCug/apply/index.html") + public void index() { + } + + @At("/h5") + @SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR) + @Ok("beetl:/platform/zhghh5/dayofficework/siteCug/apply/index.html") + public void h5Index() { + } + + @At("/form/h5") + @SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR) + @Ok("beetl:/platform/zhghh5/dayofficework/siteCug/apply/form/index.html") + public void h5Form() { + } + + @At + @ApiOperation("\u5206\u9875\u67e5\u8be2\u573a\u5730") + @SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR) + public Result pageData(PageForm pageForm, @Param("type") String type) { + Cnd cnd = Cnd.NEW(); + cnd.and(SiteCugInfo::getState, "=", true); + cnd.andEX(SiteCugInfo::getTypeId, "=", type); + if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) { + SqlExpressionGroup seg = new SqlExpressionGroup(); + seg.or(SiteCugInfo::getName, "like", "%" + pageForm.getSearchKeyword() + "%"); + seg.or(SiteCugInfo::getAddress, "like", "%" + pageForm.getSearchKeyword() + "%"); + cnd.and(seg); + } + if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) { + cnd.asc("sortNum * 1"); + } else { + cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy())); + } + Pagination pagination = infoService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd); + List listMap = pagination.getList(NutMap.class); + + List typeList = dao.query(SiteCugFunctionType.class, Cnd.NEW()); + Map typeMap = typeList.stream().collect(Collectors.toMap(SiteCugFunctionType::getId, SiteCugFunctionType::getName)); + List siteIds = listMap.stream().map(item -> item.getString("id")).filter(StrUtil::isNotBlank).toList(); + String today = DateUtil.today(); + String dayStart = today + " 00:00:00"; + String dayEnd = today + " 23:59:59"; + Map> applyMap = queryTodayApplyMap(siteIds, dayStart, dayEnd); + Set holidaySet = dao.query(SysHoliday.class, Cnd.NEW()).stream() + .map(SysHoliday::getDay) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toSet()); + for (NutMap map : listMap) { + map.put("typeName", typeMap.get(map.getString("typeId"))); + map.put("reserveTimeTypeName", getReserveTimeTypeName(map.getInt("reserveTimeType"))); + map.put("timelineSegments", buildTimelineSegments(map, applyMap.getOrDefault(map.getString("id"), new ArrayList<>()), today, holidaySet)); + } + return Result.success(pagination); + } + + @At + @ApiOperation("\u67e5\u8be2\u573a\u5730\u53ef\u9884\u7ea6\u65f6\u6bb5") + @SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR) + public Result availability(@Param("siteId") String siteId, @Param("date") String date) { + if (StrUtil.isBlank(siteId)) { + return Result.error("\u573a\u5730\u4e0d\u80fd\u4e3a\u7a7a"); + } + String targetDate = StrUtil.isBlank(date) ? DateUtil.today() : date; + if (!targetDate.matches("^\\d{4}-\\d{2}-\\d{2}$")) { + return Result.error("\u9884\u7ea6\u65e5\u671f\u683c\u5f0f\u4e0d\u6b63\u786e"); + } + SiteCugInfo siteInfo = infoService.fetch(siteId); + if (siteInfo == null) { + return Result.error("\u573a\u5730\u4e0d\u5b58\u5728"); + } + Set holidaySet = queryHolidaySet(); + List applyList = queryDayApplyList(siteId, targetDate + " 00:00:00", targetDate + " 23:59:59"); + return Result.success(NutMap.NEW() + .addv("date", targetDate) + .addv("reserveTimeType", siteInfo.getReserveTimeType()) + .addv("reserveTimeTypeName", getReserveTimeTypeName(siteInfo.getReserveTimeType())) + .addv("blocks", buildAvailabilityBlocks(siteInfo, targetDate, applyList, holidaySet))); + } + + private Map> queryTodayApplyMap(List siteIds, String dayStart, String dayEnd) { + if (siteIds == null || siteIds.isEmpty()) { + return Map.of(); + } + Sql sql = Sqls.create(""" + select + sa.* + from + site_cug_apply sa + left join wf_process_instance ins on ins.businessNo = sa.id + $condition + """); + Cnd cnd = Cnd.NEW(); + cnd.and("sa.siteId", "in", siteIds); + cnd.and("ins.state", "in", List.of( + ProcessInstanceStateEnum.DOING.getCode(), + ProcessInstanceStateEnum.FINISHED.getCode(), + ProcessInstanceStateEnum.PENDING.getCode() + )); + cnd.and("sa.reserveEndTime", ">=", dayStart); + cnd.and("sa.reserveStartTime", "<=", dayEnd); + sql.setCondition(cnd); + sql.setEntity(dao.getEntity(SiteCugApply.class)); + sql.setCallback(Sqls.callback.entities()); + dao.execute(sql); + List applyList = sql.getList(SiteCugApply.class); + if (applyList == null || applyList.isEmpty()) { + return Map.of(); + } + return applyList.stream() + .filter(Objects::nonNull) + .collect(Collectors.groupingBy(SiteCugApply::getSiteId)); + } + + private List queryDayApplyList(String siteId, String dayStart, String dayEnd) { + if (StrUtil.isBlank(siteId)) { + return new ArrayList<>(); + } + Sql sql = Sqls.create(""" + select + sa.* + from + site_cug_apply sa + left join wf_process_instance ins on ins.businessNo = sa.id + $condition + """); + Cnd cnd = Cnd.NEW(); + cnd.and("sa.siteId", "=", siteId); + cnd.and("ins.state", "in", List.of( + ProcessInstanceStateEnum.DOING.getCode(), + ProcessInstanceStateEnum.FINISHED.getCode(), + ProcessInstanceStateEnum.PENDING.getCode() + )); + cnd.and("sa.reserveEndTime", ">=", dayStart); + cnd.and("sa.reserveStartTime", "<=", dayEnd); + sql.setCondition(cnd); + sql.setEntity(dao.getEntity(SiteCugApply.class)); + sql.setCallback(Sqls.callback.entities()); + dao.execute(sql); + List applyList = sql.getList(SiteCugApply.class); + return applyList == null ? new ArrayList<>() : applyList; + } + + private Set queryHolidaySet() { + return dao.query(SysHoliday.class, Cnd.NEW()).stream() + .map(SysHoliday::getDay) + .filter(StrUtil::isNotBlank) + .collect(Collectors.toSet()); + } + + private String getReserveTimeTypeName(Integer reserveTimeType) { + if (reserveTimeType != null && reserveTimeType == 2) { + return "\u5168\u5929\u5019\u9884\u7ea6"; + } + return "\u5206\u6bb5\u9884\u7ea6"; + } + + private List buildTimelineSegments(NutMap site, List applyList, String today, Set holidaySet) { + List segments = new ArrayList<>(); + boolean isWeekend = isWeekend(today); + boolean isHoliday = holidaySet.contains(today); + List openRanges = buildOpenRanges(site, isWeekend, isHoliday); + List disabledRanges = buildDisabledRanges(site, today); + List reservedRanges = buildReservedRanges(applyList, today); + int totalMinutes = 24 * 60; + int segmentMinutes = 30; + for (int start = 0; start < totalMinutes; start += segmentMinutes) { + int end = start + segmentMinutes; + String status = "closed"; + if (intersectsAny(start, end, openRanges)) { + status = "available"; + } + if (intersectsAny(start, end, disabledRanges)) { + status = "closed"; + } + if (!"closed".equals(status) && intersectsAny(start, end, reservedRanges)) { + status = "reserved"; + } + segments.add(NutMap.NEW() + .addv("status", status) + .addv("startMinute", start) + .addv("endMinute", end)); + } + return segments; + } + + private List buildOpenRanges(NutMap site, boolean isWeekend, boolean isHoliday) { + if (isWeekend) { + return new ArrayList<>(); + } + if (Boolean.TRUE.equals(site.getBoolean("filterHolidays")) && isHoliday) { + return new ArrayList<>(); + } + Integer reserveTimeType = site.getInt("reserveTimeType"); + if (reserveTimeType != null && reserveTimeType == 2) { + NutMap fullDayOpenHour = toNutMap(site.get("fullDayOpenHour")); + if (fullDayOpenHour.isEmpty()) { + List openHours = toNutMapList(site.get("openHours")); + fullDayOpenHour = openHours.isEmpty() ? NutMap.NEW() : openHours.get(0); + } + return parseTimeRanges(List.of(fullDayOpenHour)); + } + List segmentedOpenHours = toNutMapList(site.get("segmentedOpenHours")); + if (segmentedOpenHours.isEmpty()) { + segmentedOpenHours = toNutMapList(site.get("openHours")); + } + return parseTimeRanges(segmentedOpenHours); + } + + private List buildDisabledRanges(NutMap site, String today) { + List disabledList = toNutMapList(site.get("notApplyTimeList")); + List result = new ArrayList<>(); + for (NutMap item : disabledList) { + if (item == null || !StrUtil.equals(today, item.getString("date"))) { + continue; + } + Integer start = parseTimeToMinutes(item.getString("startTime")); + Integer end = parseTimeToMinutes(item.getString("endTime")); + if (start != null && end != null && end > start) { + result.add(new int[]{start, end}); + } + } + return result; + } + + private List buildReservedRanges(List applyList, String today) { + List result = new ArrayList<>(); + String dayStart = today + " 00:00:00"; + String dayEnd = today + " 23:59:59"; + DateTime startOfDay = DateUtil.parseDateTime(dayStart); + DateTime endOfDay = DateUtil.parseDateTime(dayEnd); + for (SiteCugApply item : applyList) { + if (item == null || StrUtil.hasBlank(item.getReserveStartTime(), item.getReserveEndTime())) { + continue; + } + DateTime reserveStart = DateUtil.parseDateTime(item.getReserveStartTime()); + DateTime reserveEnd = DateUtil.parseDateTime(item.getReserveEndTime()); + if (!reserveEnd.isAfter(reserveStart)) { + continue; + } + DateTime actualStart = reserveStart.isBefore(startOfDay) ? startOfDay : reserveStart; + DateTime actualEnd = reserveEnd.isAfter(endOfDay) ? endOfDay : reserveEnd; + int startMinute = actualStart.hour(true) * 60 + actualStart.minute(); + int endMinute = actualEnd.hour(true) * 60 + actualEnd.minute(); + if (actualEnd.second() > 0) { + endMinute = Math.min(24 * 60, endMinute + 1); + } + if (endMinute > startMinute) { + result.add(new int[]{startMinute, endMinute}); + } + } + return result; + } + + private List parseTimeRanges(List sourceList) { + List result = new ArrayList<>(); + for (NutMap item : sourceList) { + if (item == null) { + continue; + } + Integer start = parseTimeToMinutes(item.getString("startTime")); + Integer end = parseTimeToMinutes(item.getString("endTime")); + if (start != null && end != null && end > start) { + result.add(new int[]{start, end}); + } + } + return result; + } + + private NutMap toNutMap(Object value) { + if (value instanceof NutMap nutMap) { + return nutMap; + } + if (value instanceof Map map) { + NutMap nutMap = NutMap.NEW(); + map.forEach((key, val) -> nutMap.put(String.valueOf(key), val)); + return nutMap; + } + return NutMap.NEW(); + } + + private List toNutMapList(Object value) { + List result = new ArrayList<>(); + if (!(value instanceof List list)) { + return result; + } + for (Object item : list) { + result.add(toNutMap(item)); + } + return result; + } + + private Integer parseTimeToMinutes(String timeText) { + if (StrUtil.isBlank(timeText)) { + return null; + } + List parts = Arrays.stream(timeText.split(":")) + .filter(StrUtil::isNotBlank) + .toList(); + if (parts.size() < 2) { + return null; + } + try { + int hour = Integer.parseInt(parts.get(0)); + int minute = Integer.parseInt(parts.get(1)); + if (hour < 0 || hour > 24 || minute < 0 || minute > 59) { + return null; + } + if (hour == 24 && minute > 0) { + return null; + } + return hour * 60 + minute; + } catch (NumberFormatException e) { + return null; + } + } + + private boolean intersectsAny(int start, int end, List ranges) { + for (int[] range : ranges) { + if (range != null && start < range[1] && end > range[0]) { + return true; + } + } + return false; + } + + private boolean isWeekend(String dateText) { + DateTime date = DateUtil.parseDate(dateText); + int week = date.dayOfWeek() - 1; + return week == 0 || week == 6; + } + + private List buildAvailabilityBlocks(SiteCugInfo siteInfo, String day, List applyList, Set holidaySet) { + if (siteInfo.getReserveTimeType() != null && siteInfo.getReserveTimeType() == 2) { + return buildFullDayAvailabilityBlocks(siteInfo, day, applyList, holidaySet); + } + return buildSegmentedAvailabilityBlocks(siteInfo, day, applyList, holidaySet); + } + + private List buildSegmentedAvailabilityBlocks(SiteCugInfo siteInfo, String day, List applyList, Set holidaySet) { + List sourceList = siteInfo.getSegmentedOpenHours(); + if (sourceList == null || sourceList.isEmpty()) { + sourceList = siteInfo.getOpenHours(); + } + List result = new ArrayList<>(); + List reservedRanges = buildReservedRanges(applyList, day); + List disabledRanges = buildDisabledRanges(toSiteNutMap(siteInfo), day); + boolean siteClosed = isSiteClosed(siteInfo, day, holidaySet); + for (int i = 0; i < sourceList.size(); i++) { + NutMap item = toNutMap(sourceList.get(i)); + Integer slotStart = parseTimeToMinutes(item.getString("startTime")); + Integer slotEnd = parseTimeToMinutes(item.getString("endTime")); + int unitMinutes = parseTimeUnitMinutes(item.get("timeUnit")); + if (slotStart == null || slotEnd == null || slotEnd <= slotStart || unitMinutes <= 0) { + continue; + } + String groupLabel = "\u573a\u6b21" + (i + 1) + " " + formatMinutes(slotStart) + "-" + formatMinutes(slotEnd) + " / " + formatUnitText(unitMinutes); + for (int cursor = slotStart; cursor + unitMinutes <= slotEnd; cursor += unitMinutes) { + int blockEnd = cursor + unitMinutes; + result.add(buildAvailabilityBlock(day, cursor, blockEnd, reservedRanges, disabledRanges, siteClosed, i, groupLabel)); + } + } + return result; + } + + private List buildFullDayAvailabilityBlocks(SiteCugInfo siteInfo, String day, List applyList, Set holidaySet) { + NutMap source = getFullDayOpenHourConfig(siteInfo); + Integer slotStart = parseTimeToMinutes(source.getString("startTime")); + Integer slotEnd = parseTimeToMinutes(source.getString("endTime")); + int unitMinutes = parseTimeUnitMinutes(source.get("timeUnit")); + if (slotStart == null || slotEnd == null || slotEnd <= slotStart || unitMinutes <= 0) { + return new ArrayList<>(); + } + List result = new ArrayList<>(); + List reservedRanges = buildReservedRanges(applyList, day); + List disabledRanges = buildDisabledRanges(toSiteNutMap(siteInfo), day); + boolean siteClosed = isSiteClosed(siteInfo, day, holidaySet); + String groupLabel = "\u5168\u5929\u5019\u65f6\u6bb5 / " + formatUnitText(unitMinutes); + for (int cursor = slotStart; cursor + unitMinutes <= slotEnd; cursor += unitMinutes) { + int blockEnd = cursor + unitMinutes; + result.add(buildAvailabilityBlock(day, cursor, blockEnd, reservedRanges, disabledRanges, siteClosed, 0, groupLabel)); + } + return result; + } + + /** + * 当预约日期就是今天时,已经过了开始时间的时间块直接标记为不可预约, + * 这样全天候和分段预约在前端展示时都会统一变成禁用状态。 + */ + private NutMap buildAvailabilityBlock(String day, int startMinute, int endMinute, List reservedRanges, List disabledRanges, boolean siteClosed, int groupIndex, String groupLabel) { + String status = "available"; + if (siteClosed || isExpiredAvailabilityBlock(day, startMinute) || intersectsAny(startMinute, endMinute, disabledRanges)) { + status = "closed"; + } else if (intersectsAny(startMinute, endMinute, reservedRanges)) { + status = "reserved"; + } + return NutMap.NEW() + .addv("key", day + "_" + startMinute + "_" + endMinute) + .addv("groupIndex", groupIndex) + .addv("groupLabel", groupLabel) + .addv("status", status) + .addv("startMinute", startMinute) + .addv("endMinute", endMinute) + .addv("startTime", formatMinutes(startMinute)) + .addv("endTime", formatMinutes(endMinute)) + .addv("label", formatMinutes(startMinute) + "-" + formatMinutes(endMinute)) + .addv("startDateTime", day + " " + formatMinutes(startMinute) + ":00") + .addv("endDateTime", day + " " + formatMinutes(endMinute) + ":00"); + } + + /** + * 仅在当天按“时间块开始时间是否已经过去”判断是否失效, + * 避免用户继续选中已经开始或已经结束的预约时段。 + */ + private boolean isExpiredAvailabilityBlock(String day, int startMinute) { + String today = DateUtil.today(); + if (!StrUtil.equals(today, day)) { + return false; + } + DateTime blockStart = DateUtil.parseDateTime(day + " " + formatMinutes(startMinute) + ":00"); + return !blockStart.isAfter(DateUtil.date()); + } + + private boolean isSiteClosed(SiteCugInfo siteInfo, String day, Set holidaySet) { + if (isWeekend(day)) { + return true; + } + return Boolean.TRUE.equals(siteInfo.getFilterHolidays()) && holidaySet.contains(day); + } + + private NutMap toSiteNutMap(SiteCugInfo siteInfo) { + return NutMap.NEW() + .addv("notApplyTimeList", siteInfo.getNotApplyTimeList()) + .addv("filterHolidays", siteInfo.getFilterHolidays()) + .addv("reserveTimeType", siteInfo.getReserveTimeType()) + .addv("openHours", siteInfo.getOpenHours()) + .addv("segmentedOpenHours", siteInfo.getSegmentedOpenHours()) + .addv("fullDayOpenHour", siteInfo.getFullDayOpenHour()); + } + + private NutMap getFullDayOpenHourConfig(SiteCugInfo siteInfo) { + NutMap source = toNutMap(siteInfo.getFullDayOpenHour()); + if (!source.isEmpty()) { + return source; + } + List openHours = siteInfo.getOpenHours(); + return openHours == null || openHours.isEmpty() ? NutMap.NEW() : toNutMap(openHours.get(0)); + } + + private int parseTimeUnitMinutes(Object value) { + if (value == null) { + return 60; + } + try { + double unit = Double.parseDouble(String.valueOf(value)); + if (unit <= 0) { + return 60; + } + return (int) Math.round(unit); + } catch (NumberFormatException e) { + return 60; + } + } + + private String formatMinutes(int totalMinutes) { + int hour = totalMinutes / 60; + int minute = totalMinutes % 60; + return String.format("%02d:%02d", hour, minute); + } + + private String formatUnitText(int unitMinutes) { + return unitMinutes + "\u5206\u949f"; + } + + private Result validateApplySelectionBySchedule(SiteCugApply apply) { + if (apply == null || StrUtil.hasBlank(apply.getSiteId(), apply.getReserveStartTime(), apply.getReserveEndTime())) { + return Result.success(); + } + SiteCugInfo siteInfo = infoService.fetch(apply.getSiteId()); + if (siteInfo == null) { + return Result.error("\u573a\u5730\u4e0d\u5b58\u5728"); + } + DateTime start = DateUtil.parseDateTime(apply.getReserveStartTime()); + DateTime end = DateUtil.parseDateTime(apply.getReserveEndTime()); + String startDay = DateUtil.formatDate(start); + String endDay = DateUtil.formatDate(end); + if (!StrUtil.equals(startDay, endDay)) { + return Result.error("\u9884\u7ea6\u65f6\u95f4\u5fc5\u987b\u5728\u540c\u4e00\u5929\u5185"); + } + Set holidaySet = queryHolidaySet(); + List applyList = queryDayApplyList(apply.getSiteId(), startDay + " 00:00:00", startDay + " 23:59:59").stream() + .filter(item -> item != null && !StrUtil.equals(item.getId(), apply.getId())) + .collect(Collectors.toList()); + if (siteInfo.getReserveTimeType() != null && siteInfo.getReserveTimeType() == 2) { + if (!isWithinOpenRanges(siteInfo, startDay, start, end, holidaySet)) { + return Result.error("\u5168\u5929\u5019\u9884\u7ea6\u8bf7\u5728\u5f53\u5929\u5f00\u653e\u65f6\u95f4\u5185\u6309\u9884\u7ea6\u65f6\u95f4\u5355\u4f4d\u9009\u62e9\u4e00\u4e2a\u6216\u591a\u4e2a\u8fde\u7eed\u65f6\u6bb5"); + } + if (!isAlignedWithFullDayTimeUnit(siteInfo, start, end)) { + return Result.error("\u5168\u5929\u5019\u9884\u7ea6\u7684\u5f00\u59cb\u548c\u7ed3\u675f\u65f6\u95f4\u5fc5\u987b\u7b26\u5408\u9884\u7ea6\u65f6\u95f4\u5355\u4f4d"); + } + return Result.success(); + } + List blocks = buildSegmentedAvailabilityBlocks(siteInfo, startDay, applyList, holidaySet); + String startText = apply.getReserveStartTime(); + String endText = apply.getReserveEndTime(); + for (int i = 0; i < blocks.size(); i++) { + NutMap block = blocks.get(i); + if (!"available".equals(block.getString("status"))) { + continue; + } + if (!StrUtil.equals(startText, block.getString("startDateTime"))) { + continue; + } + String currentEnd = block.getString("endDateTime"); + if (StrUtil.equals(endText, currentEnd)) { + return Result.success(); + } + for (int j = i + 1; j < blocks.size(); j++) { + NutMap nextBlock = blocks.get(j); + NutMap prevBlock = blocks.get(j - 1); + if (!"available".equals(nextBlock.getString("status"))) { + break; + } + if (!StrUtil.equals(prevBlock.getString("endDateTime"), nextBlock.getString("startDateTime"))) { + break; + } + currentEnd = nextBlock.getString("endDateTime"); + if (StrUtil.equals(endText, currentEnd)) { + return Result.success(); + } + } + } + return Result.error("\u5206\u6bb5\u9884\u7ea6\u8bf7\u6309\u573a\u6b21\u65f6\u95f4\u5355\u4f4d\u9009\u62e9\u4e00\u4e2a\u6216\u591a\u4e2a\u8fde\u7eed\u65f6\u6bb5"); + } + + private boolean isWithinOpenRanges(SiteCugInfo siteInfo, String day, DateTime start, DateTime end, Set holidaySet) { + if (isSiteClosed(siteInfo, day, holidaySet)) { + return false; + } + List openRanges = buildOpenRanges(toSiteNutMap(siteInfo), false, false); + List disabledRanges = buildDisabledRanges(toSiteNutMap(siteInfo), day); + int startMinute = start.hour(true) * 60 + start.minute(); + int endMinute = end.hour(true) * 60 + end.minute(); + if (end.second() > 0) { + endMinute = Math.min(24 * 60, endMinute + 1); + } + final int finalEndMinute = endMinute; + boolean inOpenRange = openRanges.stream().anyMatch(range -> startMinute >= range[0] && finalEndMinute <= range[1]); + return inOpenRange && !intersectsAny(startMinute, finalEndMinute, disabledRanges); + } + + private boolean isAlignedWithFullDayTimeUnit(SiteCugInfo siteInfo, DateTime start, DateTime end) { + if (start == null || end == null || start.second() > 0 || end.second() > 0) { + return false; + } + NutMap source = getFullDayOpenHourConfig(siteInfo); + Integer slotStart = parseTimeToMinutes(source.getString("startTime")); + Integer slotEnd = parseTimeToMinutes(source.getString("endTime")); + int unitMinutes = parseTimeUnitMinutes(source.get("timeUnit")); + if (slotStart == null || slotEnd == null || slotEnd <= slotStart || unitMinutes <= 0) { + return false; + } + int startMinute = start.hour(true) * 60 + start.minute(); + int endMinute = end.hour(true) * 60 + end.minute(); + return startMinute >= slotStart + && endMinute <= slotEnd + && endMinute > startMinute + && (startMinute - slotStart) % unitMinutes == 0 + && (endMinute - slotStart) % unitMinutes == 0; + } + + @At + @ApiOperation("\u63d0\u4ea4\u9884\u7ea6") + @SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR) + @SLog(tag = "\u573a\u9986\u529f\u80fd\u7ba1\u7406-\u573a\u5730\u9884\u7ea6", msg = "\u63d0\u4ea4\u573a\u5730\u9884\u7ea6") + public Result submit(@Param("data") SiteCugApply apply) { + Result scheduleValidate = validateApplySelectionBySchedule(apply); + if (scheduleValidate.getCode() != 0) { + return scheduleValidate; + } + Map validate = applyService.validateApply(apply); + if (validate.containsKey(false)) { + return Result.error(validate.get(false)); + } + submitSingleApply(apply); + return Result.success(); + } + + @At("/submitYearly") + @ApiOperation("\u6309\u672c\u5e74\u540e\u7eed\u540c\u661f\u671f\u540c\u65f6\u95f4\u6bb5\u6279\u91cf\u63d0\u4ea4\u9884\u7ea6") + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR) + @SLog(tag = "\u573a\u9986\u529f\u80fd\u7ba1\u7406-\u573a\u5730\u9884\u7ea6", msg = "\u6279\u91cf\u63d0\u4ea4\u672c\u5e74\u573a\u5730\u9884\u7ea6") + public Result submitYearly(@Param("data") SiteCugApply apply) { + List applyList = buildYearlyApplyList(apply); + if (applyList.isEmpty()) { + return Result.error("\u672a\u751f\u6210\u53ef\u9884\u7ea6\u7684\u65e5\u671f"); + } + for (SiteCugApply item : applyList) { + Result scheduleValidate = validateApplySelectionBySchedule(item); + if (scheduleValidate.getCode() != 0) { + return scheduleValidate; + } + Map validate = applyService.validateApply(item); + if (validate.containsKey(false)) { + return Result.error(String.format("%s \u9884\u7ea6\u5931\u8d25\uff1a%s", item.getReserveStartTime(), validate.get(false))); + } + } + String yearlyBatchNo = YEARLY_BATCH_PREFIX + UUID.randomUUID(); + for (SiteCugApply item : applyList) { + item.setBackOption(yearlyBatchNo); + submitSingleApply(item); + } + return Result.success(String.format("\u5df2\u6210\u529f\u9884\u7ea6\u622a\u81f3%s\u5171%d\u4e2a\u65f6\u95f4\u6bb5", apply.getYearlyReserveEndDate(), applyList.size())); + } + + @At + @ApiOperation("\u67e5\u8be2\u9884\u7ea6\u9650\u5236\u914d\u7f6e") + @SaCheckPermission(value = {"siteCug.apply", "h5.siteCug.apply"}, mode = SaMode.OR) + public Result timeLimitConfig(@Param("siteId") String siteId) { + if (StrUtil.isBlank(siteId)) { + return Result.success(NutMap.NEW().addv("filterHolidays", false).addv("holidayList", new ArrayList<>()).addv("notApplyTimeList", new ArrayList<>())); + } + SiteCugInfo info = infoService.fetch(siteId); + if (info == null) { + return Result.success(NutMap.NEW().addv("filterHolidays", false).addv("holidayList", new ArrayList<>()).addv("notApplyTimeList", new ArrayList<>())); + } + List holidayList = new ArrayList<>(); + if (Boolean.TRUE.equals(info.getFilterHolidays())) { + int currentYear = Year.now().getValue(); + List yearPrefixList = List.of(currentYear + "-", (currentYear + 1) + "-"); + List holidays = dao.query(SysHoliday.class, Cnd.NEW()); + holidayList = holidays.stream() + .map(SysHoliday::getDay) + .filter(day -> day != null && yearPrefixList.stream().anyMatch(day::startsWith)) + .distinct() + .collect(Collectors.toList()); + } + return Result.success(NutMap.NEW() + .addv("filterHolidays", Boolean.TRUE.equals(info.getFilterHolidays())) + .addv("holidayList", holidayList) + .addv("notApplyTimeList", info.getNotApplyTimeList() == null ? new ArrayList<>() : info.getNotApplyTimeList())); + } + + private List buildYearlyApplyList(SiteCugApply apply) { + if (apply == null || StrUtil.hasBlank(apply.getReserveStartTime(), apply.getReserveEndTime(), apply.getYearlyReserveEndDate())) { + return new ArrayList<>(); + } + DateTime start = DateUtil.parseDateTime(apply.getReserveStartTime()); + DateTime end = DateUtil.parseDateTime(apply.getReserveEndTime()); + if (!end.isAfter(start)) { + return new ArrayList<>(); + } + DateTime reserveEndDate = parseReserveEndDate(apply.getYearlyReserveEndDate()); + if (reserveEndDate == null || reserveEndDate.isBefore(DateUtil.beginOfDay(start))) { + return new ArrayList<>(); + } + SiteCugInfo siteInfo = infoService.fetch(apply.getSiteId()); + if (siteInfo == null) { + return new ArrayList<>(); + } + List holidayList = new ArrayList<>(); + if (Boolean.TRUE.equals(siteInfo.getFilterHolidays())) { + int currentYear = start.year(); + List yearPrefixList = List.of(currentYear + "-", (currentYear + 1) + "-"); + List holidays = dao.query(SysHoliday.class, Cnd.NEW()); + holidayList = holidays.stream() + .map(SysHoliday::getDay) + .filter(day -> day != null && yearPrefixList.stream().anyMatch(day::startsWith)) + .distinct() + .collect(Collectors.toList()); + } + int targetWeek = start.dayOfWeek() - 1; + int startHour = start.hour(true); + int startMinute = start.minute(); + int startSecond = start.second(); + int endHour = end.hour(true); + int endMinute = end.minute(); + int endSecond = end.second(); + Date current = DateUtil.beginOfDay(start); + Date batchEndDate = DateUtil.endOfDay(reserveEndDate); + List result = new ArrayList<>(); + while (current.compareTo(batchEndDate) <= 0) { + DateTime currentDate = DateUtil.date(current); + String currentDay = DateUtil.formatDate(currentDate); + if (currentDate.dayOfWeek() - 1 == targetWeek && !holidayList.contains(currentDay)) { + SiteCugApply item = copyApply(apply); + item.setId(null); + item.setReserveStartTime(DateUtil.formatDateTime(buildDateTime(currentDate, startHour, startMinute, startSecond))); + item.setReserveEndTime(DateUtil.formatDateTime(buildDateTime(currentDate, endHour, endMinute, endSecond))); + result.add(item); + } + current = DateUtil.offsetDay(current, 1); + } + return result; + } + + private DateTime parseReserveEndDate(String endDate) { + if (StrUtil.isBlank(endDate)) { + return null; + } + try { + return DateUtil.parseDate(endDate); + } catch (Exception e) { + return null; + } + } + + private DateTime buildDateTime(DateTime date, int hour, int minute, int second) { + return DateUtil.parseDateTime(String.format("%s %02d:%02d:%02d", DateUtil.formatDate(date), hour, minute, second)); + } + + private SiteCugApply copyApply(SiteCugApply apply) { + return new SiteCugApply() + .setSiteId(apply.getSiteId()) + .setApplyUserId(apply.getApplyUserId()) + .setApplyUserName(apply.getApplyUserName()) + .setApplySex(apply.getApplySex()) + .setApplyLoginName(apply.getApplyLoginName()) + .setApplyUnitId(apply.getApplyUnitId()) + .setApplyUnitName(apply.getApplyUnitName()) + .setReserveType(apply.getReserveType()) + .setApplyUnionId(apply.getApplyUnionId()) + .setApplyUnionName(apply.getApplyUnionName()) + .setClubId(apply.getClubId()) + .setClubName(apply.getClubName()) + .setApplyMobile(apply.getApplyMobile()) + .setReserveStartTime(apply.getReserveStartTime()) + .setReserveEndTime(apply.getReserveEndTime()) + .setJoinCount(apply.getJoinCount()) + .setApplyCause(apply.getApplyCause()) + .setBackOption(apply.getBackOption()) + .setYearlyReserveEndDate(apply.getYearlyReserveEndDate()); + } + + private void submitSingleApply(SiteCugApply apply) { + dao.insertOrUpdate(apply); + + Dict args = Dict.create(); + args.set(FlowConst.SUBMIT_TYPE, ProcessSubmitTypeEnum.APPLY.getCode()); + args.set(FlowConst.FORM_DATA, apply); + ProcessInstance instance = flowEngine.startProcessInstanceByKey("CDYY", apply.getId(), SecurityUtil.getUserId(), args); + + List doingTaskList = flowEngine.processTaskService().getDoingTaskList(instance.getId(), null); + for (ProcessTask task : doingTaskList) { + flowEngine.executeProcessTask(task.getId(), SecurityUtil.getUserId(), args); + } + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/controller/SiteCugFunctionTypeController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/controller/SiteCugFunctionTypeController.java new file mode 100644 index 0000000..65c1907 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/controller/SiteCugFunctionTypeController.java @@ -0,0 +1,85 @@ +package com.budwk.app.zhgh.dayofficework.siteCug.controller; + +import cn.dev33.satoken.annotation.SaCheckLogin; +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.PageUtil; +import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugFunctionType; +import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugFunctionTypeService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.nutz.dao.Cnd; +import org.nutz.dao.util.cri.SqlExpressionGroup; +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; + +import java.util.List; + +@Slf4j +@IocBean +@Ok("json:full") +@Api(tags = "功能类型") +@At("/platform/siteCug/function/type") +public class SiteCugFunctionTypeController { + + @Inject + private SiteCugFunctionTypeService typeService; + + @At("") + @SaCheckPermission("siteCug.function.type") + @Ok("beetl:/platform/zhgh/dayofficework/siteCug/type/index.html") + public void index() {} + + @At + @ApiOperation("分页查询") + @SaCheckPermission("siteCug.function.type") + public Result pageData(PageForm pageForm) { + Cnd cnd = Cnd.NEW(); + if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) { + SqlExpressionGroup seg = new SqlExpressionGroup(); + seg.or(SiteCugFunctionType::getName, "like", "%" + pageForm.getSearchKeyword() + "%"); + seg.or(SiteCugFunctionType::getCode, "like", "%" + pageForm.getSearchKeyword() + "%"); + cnd.and(seg); + } + if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) { + cnd.asc("code"); + } else { + cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy())); + } + Pagination pagination = typeService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd); + return Result.success(pagination); + } + + @At + @ApiOperation("新增/修改功能类型") + @SaCheckPermission("siteCug.function.type") + @SLog(tag = "场馆功能管理-功能类型", msg = "新增/修改功能类型") + public Object submit(SiteCugFunctionType type) { + typeService.insertOrUpdate(type); + return Result.success(); + } + + @At + @ApiOperation("删除功能类型") + @SaCheckPermission("siteCug.function.type") + @SLog(tag = "场馆功能管理-功能类型", msg = "删除功能类型") + public Object delete(String id) { + typeService.delete(id); + return Result.success(); + } + + @At + @ApiOperation("查询功能类型") + @SaCheckLogin + public Result queryFunctionType() { + List list = typeService.query(Cnd.where(SiteCugFunctionType::getEnable, "=", true).asc(SiteCugFunctionType::getCode)); + return Result.success(list); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/controller/SiteCugManageController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/controller/SiteCugManageController.java new file mode 100644 index 0000000..d95de00 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/controller/SiteCugManageController.java @@ -0,0 +1,242 @@ +package com.budwk.app.zhgh.dayofficework.siteCug.controller; + +import cn.dev33.satoken.annotation.SaCheckLogin; +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.PageUtil; +import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugFunctionType; +import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo; +import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugInfoService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +import org.nutz.dao.Cnd; +import org.nutz.dao.Dao; +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 org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.Param; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +@Slf4j +@IocBean +@Ok("json:full") +@Api(tags = "siteCug manage") +@At("/platform/siteCug/manage") +public class SiteCugManageController { + + @Inject + private Dao dao; + @Inject + private SiteCugInfoService infoService; + + @At("") + @SaCheckPermission("siteCug.manage") + @Ok("beetl:/platform/zhgh/dayofficework/siteCug/manage/index.html") + public void index() {} + + @At + @ApiOperation("page data") + @SaCheckPermission("siteCug.manage") + public Result pageData(PageForm pageForm, @Param("type") String type) { + Cnd cnd = Cnd.NEW(); + cnd.andEX(SiteCugInfo::getTypeId, "=", type); + if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) { + SqlExpressionGroup seg = new SqlExpressionGroup(); + seg.or(SiteCugInfo::getName, "like", "%" + pageForm.getSearchKeyword() + "%"); + seg.or(SiteCugInfo::getAddress, "like", "%" + pageForm.getSearchKeyword() + "%"); + cnd.and(seg); + } + if (StrUtil.isAllBlank(pageForm.getPageOrderName(), pageForm.getPageOrderBy())) { + cnd.asc("sortNum * 1"); + } else { + cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy())); + } + Pagination pagination = infoService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), cnd); + List listMap = pagination.getList(NutMap.class); + + List typeList = dao.query(SiteCugFunctionType.class, Cnd.NEW()); + Map typeMap = typeList.stream().collect(Collectors.toMap(SiteCugFunctionType::getId, SiteCugFunctionType::getName)); + + for (NutMap map : listMap) { + map.put("typeName", typeMap.get(map.getString("typeId"))); + } + return Result.success(pagination); + } + + @At + @ApiOperation("submit") + @SaCheckPermission("siteCug.manage") + @SLog(tag = "siteCug.manage", msg = "submit site") + public Object submit(@Param("data") SiteCugInfo info) { + List notApplyTimeList = info.getNotApplyTimeList(); + if (notApplyTimeList != null) { + boolean invalid = notApplyTimeList.stream().anyMatch(item -> + item == null + || StrUtil.isBlank(item.getString("date")) + || StrUtil.isBlank(item.getString("startTime")) + || StrUtil.isBlank(item.getString("endTime")) + || item.getString("startTime").compareTo(item.getString("endTime")) >= 0); + if (invalid) { + return Result.error("Invalid disabled time settings"); + } + } + + if (info.getReserveTimeType() == null) { + info.setReserveTimeType(1); + } + List segmentedOpenHours = info.getSegmentedOpenHours(); + NutMap fullDayOpenHour = info.getFullDayOpenHour(); + if ((segmentedOpenHours == null || segmentedOpenHours.isEmpty()) && info.getReserveTimeType() == 1) { + segmentedOpenHours = info.getOpenHours(); + } + if ((fullDayOpenHour == null || fullDayOpenHour.isEmpty()) && info.getReserveTimeType() == 2 && info.getOpenHours() != null && !info.getOpenHours().isEmpty()) { + fullDayOpenHour = info.getOpenHours().get(0); + } + if (info.getReserveTimeType() == 1) { + if (segmentedOpenHours == null || segmentedOpenHours.isEmpty()) { + return Result.error("Please add at least one booking slot"); + } + boolean invalid = segmentedOpenHours.stream().anyMatch(item -> + item == null + || StrUtil.isBlank(item.getString("startTime")) + || StrUtil.isBlank(item.getString("endTime")) + || item.getString("startTime").compareTo(item.getString("endTime")) >= 0 + || !isPositiveNumber(item.get("timeUnit"))); + if (invalid) { + return Result.error("Invalid segmented booking slot settings"); + } + if (hasOverlap(segmentedOpenHours)) { + return Result.error("Segmented booking slots cannot overlap"); + } + segmentedOpenHours = segmentedOpenHours.stream() + .filter(Objects::nonNull) + .map(this::normalizeOpenHourTimeUnit) + .collect(Collectors.toList()); + info.setSegmentedOpenHours(segmentedOpenHours); + info.setOpenHours(segmentedOpenHours); + } else if (info.getReserveTimeType() == 2) { + if (fullDayOpenHour == null || fullDayOpenHour.isEmpty()) { + return Result.error("Please set the full-day booking time range"); + } + NutMap item = fullDayOpenHour; + if (item == null + || StrUtil.isBlank(item.getString("startTime")) + || StrUtil.isBlank(item.getString("endTime")) + || item.getString("startTime").compareTo(item.getString("endTime")) >= 0) { + return Result.error("Invalid full-day booking time range"); + } + if (!isPositiveNumber(item.get("timeUnit"))) { + return Result.error("Invalid full-day booking time unit"); + } + fullDayOpenHour = normalizeOpenHourTimeUnit(fullDayOpenHour); + info.setFullDayOpenHour(fullDayOpenHour); + info.setOpenHours(List.of(fullDayOpenHour)); + } + + if (info.getFilterHolidays() == null) { + info.setFilterHolidays(false); + } + infoService.insertOrUpdate(info); + SiteCugInfo savedInfo = infoService.fetch(info.getId()); + return Result.success(savedInfo == null ? info : savedInfo); + } + + @At + @ApiOperation("delete") + @SaCheckPermission("siteCug.manage") + @SLog(tag = "siteCug.manage", msg = "delete site") + public Object delete(String id) { + infoService.delete(id); + return Result.success(); + } + + @At + @ApiOperation("query sites") + @SaCheckLogin + public Result querySites() { + List list = infoService.query(Cnd.where(SiteCugInfo::getState, "=", true).desc(SiteCugInfo::getSortNum)); + return Result.success(list); + } + + @At + @ApiOperation("query site info") + @SaCheckLogin + public Result info(String id) { + SiteCugInfo info = infoService.fetch(id); + if (info == null) { + return Result.success(null); + } + NutMap map = Lang.obj2nutmap(info); + SiteCugFunctionType type = dao.fetch(SiteCugFunctionType.class, info.getTypeId()); + map.put("typeName", type == null ? "" : type.getName()); + return Result.success(map); + } + + private boolean isPositiveNumber(Object value) { + if (value == null) { + return false; + } + try { + return Double.parseDouble(String.valueOf(value)) > 0; + } catch (Exception e) { + return false; + } + } + + private NutMap normalizeOpenHourTimeUnit(NutMap item) { + if (item == null) { + return null; + } + item.put("timeUnit", normalizeTimeUnitMinutes(item.get("timeUnit"))); + return item; + } + + private int normalizeTimeUnitMinutes(Object value) { + if (value == null) { + return 60; + } + try { + double parsed = Double.parseDouble(String.valueOf(value)); + if (parsed <= 0) { + return 60; + } + return (int) Math.round(parsed); + } catch (Exception e) { + return 60; + } + } + + private boolean hasOverlap(List openHours) { + if (openHours == null || openHours.size() < 2) { + return false; + } + List validHours = openHours.stream() + .filter(item -> item != null + && StrUtil.isNotBlank(item.getString("startTime")) + && StrUtil.isNotBlank(item.getString("endTime")) + && item.getString("startTime").compareTo(item.getString("endTime")) < 0) + .sorted((a, b) -> a.getString("startTime").compareTo(b.getString("startTime"))) + .collect(Collectors.toList()); + for (int i = 1; i < validHours.size(); i++) { + NutMap prev = validHours.get(i - 1); + NutMap current = validHours.get(i); + if (current.getString("startTime").compareTo(prev.getString("endTime")) < 0) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/controller/SiteCugMineController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/controller/SiteCugMineController.java new file mode 100644 index 0000000..3f33fc8 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/controller/SiteCugMineController.java @@ -0,0 +1,184 @@ +package com.budwk.app.zhgh.dayofficework.siteCug.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaMode; +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.PageUtil; +import com.budwk.app.flow.engine.FlowEngine; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugApply; +import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugApplyService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import lombok.extern.slf4j.Slf4j; +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 org.nutz.mvc.annotation.Param; + +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@IocBean +@Ok("json:full") +@Api(tags = "场地预约-我的预约") +@At("/platform/siteCug/mine") +public class SiteCugMineController { + + private static final String YEARLY_BATCH_PREFIX = "YEARLY_BATCH:"; + + @Inject + private FlowEngine flowEngine; + @Inject + private SiteCugApplyService applyService; + + @At("/") + @Ok("beetl:/platform/zhgh/dayofficework/siteCug/mine/index.html") + @SaCheckPermission("siteCug.mine") + public void index() { + } + + @At("/h5") + @Ok("beetl:/platform/zhghh5/dayofficework/siteCug/mine/index.html") + @SaCheckPermission(value = {"siteCug.mine", "h5.siteCug.mine"}, mode = SaMode.OR) + public void h5Index() { + } + + @At + @ApiOperation("分页查询我的预约") + @SaCheckPermission(value = {"siteCug.mine", "h5.siteCug.mine"}, mode = SaMode.OR) + public Result pageData(PageForm pageForm, + @Param("siteType") String siteType, + @Param("siteId") String siteId) { + Sql sql = Sqls.create(""" + SELECT + info.*, + si.name as siteName, + ins.id AS instanceId, + ins.businessNo, + ins.state instanceState, + ins.variable instanceVariable, + ins.processDefineId instanceProcessDefineId, + t.id taskId, + t.taskName AS taskKey, + t.displayName taskName, + t.taskType, + t.performType taskPerformType, + t.taskState, + t.finishTime, + t.taskParentId, + t.variable taskVariable, + CASE WHEN info.reserveType = 'club' THEN info.clubName ELSE info.applyUnionName END AS reserveTargetName, + CASE + WHEN ins.state in (10, 20) AND (t.displayName LIKE '%校工会允许%' OR t.taskName = 'c8c03407-cf26-4e0b-82f1-b2afc9c79996') + THEN 1 + ELSE 0 + END canCancel, + IF((SELECT taskName FROM wf_process_task tt WHERE tt.id = t.taskParentId) = 'startTask', 1, 0) canRevoke, + (select MAX(id) from wf_process_task where processInstanceId = ins.id and taskName = 'startTask') AS startTaskId, + CASE WHEN info.backOption LIKE 'YEARLY_BATCH:%' THEN 1 ELSE 0 END AS yearlyBatch, + CASE WHEN info.backOption LIKE 'YEARLY_BATCH:%' THEN ( + SELECT COUNT(1) FROM site_cug_apply batch_info WHERE batch_info.backOption = info.backOption AND batch_info.createdBy = info.createdBy + ) ELSE 1 END AS batchDeleteCount + FROM + site_cug_apply info + LEFT JOIN site_cug_info si ON info.siteId = si.id + LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id + LEFT JOIN wf_process_task t ON t.processInstanceId = ins.id AND t.taskState = 10 + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id + $condition + """); + Cnd cnd = Cnd.NEW(); + if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) { + SqlExpressionGroup seg = new SqlExpressionGroup(); + seg.or(SiteCugApply::getApplyUserName, "like", "%" + pageForm.getSearchKeyword() + "%"); + seg.or(SiteCugApply::getApplyLoginName, "like", "%" + pageForm.getSearchKeyword() + "%"); + cnd.and(seg); + } + cnd.andEX("si.typeId", "=", siteType); + cnd.andEX("info.siteId", "=", siteId); + cnd.and("info.createdBy", "=", SecurityUtil.getUserId()); + + if (StrUtil.isNotBlank(pageForm.getPageOrderName()) && StrUtil.isNotBlank(pageForm.getPageOrderBy())) { + cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy())); + } else { + cnd.desc("info.createdAt"); + } + cnd.groupBy("info.id"); + sql.setCondition(cnd); + Pagination pagination = applyService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); + return Result.success(pagination); + } + + @At + @ApiOperation("取消我的预约") + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission(value = {"siteCug.mine", "h5.siteCug.mine"}, mode = SaMode.OR) + @SLog(tag = "场馆功能管理-我的预约", msg = "取消场地预约") + public Result delete(@Param("id") String id) { + SiteCugApply apply = applyService.fetch(id); + if (apply == null) { + return Result.error("预约记录不存在"); + } + if (!StrUtil.equals(apply.getCreatedBy(), SecurityUtil.getUserId())) { + return Result.error("无权取消该预约"); + } + + List deleteIds = new ArrayList<>(); + if (StrUtil.isNotBlank(apply.getBackOption()) && apply.getBackOption().startsWith(YEARLY_BATCH_PREFIX)) { + List batchList = applyService.query(Cnd.where("backOption", "=", apply.getBackOption()).and("createdBy", "=", SecurityUtil.getUserId())); + deleteIds.addAll(batchList.stream().map(SiteCugApply::getId).toList()); + } else { + deleteIds.add(id); + } + if (deleteIds.isEmpty()) { + return Result.error("未找到可取消的预约记录"); + } + + for (String deleteId : deleteIds) { + Sql sql = Sqls.create(""" + SELECT + CASE + WHEN ins.state in (10, 20) AND (t.displayName LIKE '%校工会允许%' OR t.taskName = 'c8c03407-cf26-4e0b-82f1-b2afc9c79996') + THEN 1 + ELSE 0 + END canCancel + FROM + site_cug_apply info + LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id + LEFT JOIN wf_process_task t ON t.processInstanceId = ins.id AND t.taskState = 10 + WHERE info.id = @id + """); + sql.params().set("id", deleteId); + sql.setCallback(Sqls.callback.maps()); + applyService.dao().execute(sql); + List list = sql.getList(NutMap.class); + boolean canCancel = list != null && !list.isEmpty() && list.get(0).getInt("canCancel", 0) == 1; + if (!canCancel) { + return Result.error("当前批次中存在不可取消的预约,无法整批删除"); + } + } + + for (String deleteId : deleteIds) { + applyService.delete(deleteId); + flowEngine.processInstanceService().deleteProcessInstanceByBusinessKey(deleteId); + } + if (deleteIds.size() > 1) { + return Result.success("已取消该批次下全部预约记录"); + } + return Result.success("取消成功"); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/controller/SiteCugRecordController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/controller/SiteCugRecordController.java new file mode 100644 index 0000000..6baeb45 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/controller/SiteCugRecordController.java @@ -0,0 +1,98 @@ +package com.budwk.app.zhgh.dayofficework.siteCug.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import com.budwk.app.base.page.Pagination; +import com.budwk.app.base.result.Result; +import com.budwk.app.flow.engine.FlowEngine; +import com.budwk.app.flow.enums.ProcessInstanceStateEnum; +import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugApply; +import com.budwk.app.zhgh.dayofficework.siteCug.param.SiteCugRecordPageForm; +import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugApplyService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +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.lang.util.NutMap; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.Param; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; + +@IocBean +@Ok("json:full") +@Api(tags = "场馆预约查询") +@At("/platform/siteCug/record") +public class SiteCugRecordController { + + @Inject + private FlowEngine flowEngine; + @Inject + private SiteCugApplyService applyService; + + @At("") + @Ok("beetl:/platform/zhgh/dayofficework/siteCug/record/index.html") + @SaCheckPermission("siteCug.record") + public void index() { + } + + @At + @ApiOperation("分页查询") + @SaCheckPermission("siteCug.record") + public Result pageData(SiteCugRecordPageForm pageForm) { + // 查询页只负责调用 service 公共查询方法,controller 不再维护重复 SQL/Cnd 逻辑 + Sql sql = applyService.buildRecordSql(); + Cnd cnd = applyService.buildRecordCnd(pageForm); + sql.setCondition(cnd); + Pagination pageVO = applyService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); + return Result.success(pageVO); + } + + @At + @ApiOperation("删除预约记录") + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission("siteCug.record") + public Result delete(@Param("id") String id) { + SiteCugApply apply = applyService.fetch(id); + if (apply == null) { + return Result.error("记录不存在"); + } + + Sql sql = Sqls.create(""" + SELECT + ins.state AS instanceState + FROM + site_cug_apply info + LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id + WHERE info.id = @id + """); + sql.params().set("id", id); + sql.setCallback(Sqls.callback.maps()); + applyService.dao().execute(sql); + List list = sql.getList(NutMap.class); + Integer state = list != null && !list.isEmpty() ? list.get(0).getInt("instanceState") : null; + // 预约查询页只展示审核结束数据,这里也只允许删除已完成/已拒绝的流程记录 + if (state == null || !List.of(ProcessInstanceStateEnum.FINISHED.getCode(), ProcessInstanceStateEnum.REJECT.getCode()).contains(state)) { + return Result.error("仅可删除审核结束的预约记录"); + } + + applyService.delete(id); + flowEngine.processInstanceService().deleteProcessInstanceByBusinessKey(id); + return Result.success(); + } + + @At + @Ok("void") + @ApiOperation("导出预约记录") + @SaCheckPermission("siteCug.record") + public void doExport(SiteCugRecordPageForm pageForm, HttpServletResponse response) { + // 导出逻辑统一下沉到 service,导出结果和当前筛选条件保持一致 + applyService.exportRecord(pageForm, response); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/controller/SiteCugSchoolUnionAuditController.java b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/controller/SiteCugSchoolUnionAuditController.java new file mode 100644 index 0000000..b14d135 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/controller/SiteCugSchoolUnionAuditController.java @@ -0,0 +1,245 @@ +package com.budwk.app.zhgh.dayofficework.siteCug.controller; + +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaMode; +import cn.dev33.satoken.stp.StpUtil; +import cn.hutool.core.lang.Dict; +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.PageUtil; +import com.budwk.app.flow.constant.FlowConst; +import com.budwk.app.flow.entity.ProcessTask; +import com.budwk.app.flow.enums.ProcessTaskStateEnum; +import com.budwk.app.flow.service.FlowCommonService; +import com.budwk.app.flow.service.ProcessTaskService; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugApply; +import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugApplyService; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +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.json.Json; +import org.nutz.mvc.annotation.At; +import org.nutz.mvc.annotation.Ok; +import org.nutz.mvc.annotation.Param; + +import java.util.ArrayList; +import java.util.List; + +@IocBean +@At("/platform/siteCug/schoolUnionAudit") +@Api("场馆校工会审核") +@Ok("json:full") +public class SiteCugSchoolUnionAuditController { + + private static final String YEARLY_BATCH_PREFIX = "YEARLY_BATCH:"; + + @Inject + private SiteCugApplyService applyService; + @Inject + private ProcessTaskService processTaskService; + @Inject + private FlowCommonService flowCommonService; + + @At("/") + @Ok("beetl:/platform/zhgh/dayofficework/siteCug/schoolUnionAudit/index.html") + @SaCheckPermission("siteCug.schoolUnionAudit") + public void index() { + } + + @At("/h5") + @Ok("beetl:/platform/zhghh5/dayofficework/siteCug/schoolUnionAudit/index.html") + @SaCheckPermission("h5.siteCug.schoolUnionAudit") + public void h5Index() { + } + + @At + @ApiOperation("分页查询") + @SaCheckPermission(value = {"siteCug.schoolUnionAudit", "h5.siteCug.schoolUnionAudit"}, mode = SaMode.OR) + public Result pageData(PageForm pageForm, + @Param("siteType") String siteType, + @Param("siteId") String siteId, + @Param("approval") Boolean approval) { + Sql sql = Sqls.create(""" + SELECT + info.*, + si.name as siteName, + CASE WHEN info.reserveType = 'club' THEN info.clubName ELSE info.applyUnionName END AS reserveTargetName, + ins.id AS instanceId, + ins.businessNo, + ins.state instanceState, + ins.variable instanceVariable, + ins.processDefineId instanceProcessDefineId, + t.id taskId, + t.taskName AS taskKey, + t.displayName taskName, + t.taskType, + t.performType taskPerformType, + t.taskState, + t.finishTime, + t.taskParentId, + t.variable taskVariable, + IFNULL(GROUP_CONCAT(DISTINCT nt.displayName),'结束') curTaskName, + CASE WHEN EXISTS ( + SELECT 1 FROM wf_process_task rt + WHERE rt.processInstanceId = ins.id AND rt.taskParentId = t.id + ) THEN 1 ELSE 0 END AS canRevoke, + CASE WHEN info.backOption LIKE 'YEARLY_BATCH:%' THEN 1 ELSE 0 END AS yearlyBatch, + CASE WHEN info.backOption LIKE 'YEARLY_BATCH:%' THEN ( + SELECT COUNT(1) FROM site_cug_apply batch_info WHERE batch_info.backOption = info.backOption AND batch_info.createdBy = info.createdBy + ) ELSE 1 END AS batchAuditCount + FROM + wf_process_task t + LEFT JOIN wf_process_instance ins ON ins.id = t.processInstanceId + LEFT JOIN site_cug_apply info ON info.id = ins.businessNo + LEFT JOIN wf_process_task_actor ta ON ta.processTaskId = t.id + LEFT JOIN site_cug_info si ON info.siteId = si.id + LEFT JOIN wf_process_task nt ON nt.processInstanceId = ins.id AND nt.taskState = 10 + LEFT JOIN wf_process_task rt ON rt.processInstanceId = ins.id AND rt.taskParentId = t.id + $condition + """); + Cnd cnd = Cnd.NEW(); + if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) { + SqlExpressionGroup seg = new SqlExpressionGroup(); + seg.or(SiteCugApply::getApplyUserName, "like", "%" + pageForm.getSearchKeyword() + "%"); + seg.or(SiteCugApply::getApplyLoginName, "like", "%" + pageForm.getSearchKeyword() + "%"); + cnd.and(seg); + } + cnd.andEX("si.typeId", "=", siteType); + cnd.andEX("info.siteId", "=", siteId); + cnd.and("t.taskName", "=", "c8c03407-cf26-4e0b-82f1-b2afc9c79996"); + if (!StpUtil.hasRole(RoleConstant.SYSADMIN.name())) { + cnd.and("ta.actorId", "in", List.of(SecurityUtil.getUserId())); + } + if (Boolean.TRUE.equals(approval)) { + cnd.and("t.taskState", "in", List.of(ProcessTaskStateEnum.FINISHED.getCode(), ProcessTaskStateEnum.INTERRUPT.getCode())); + } else { + cnd.and("t.taskState", "=", ProcessTaskStateEnum.DOING.getCode()); + } + + if (StrUtil.isNotBlank(pageForm.getPageOrderName()) && StrUtil.isNotBlank(pageForm.getPageOrderBy())) { + cnd.orderBy(resolveOrderColumn(pageForm.getPageOrderName()), PageUtil.getOrder(pageForm.getPageOrderBy())); + } else { + cnd.desc("info.createdAt"); + } + cnd.groupBy("t.id"); + sql.setCondition(cnd); + Pagination pageVO = applyService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); + return Result.success(pageVO); + } + + private String resolveOrderColumn(String pageOrderName) { + if (StrUtil.isBlank(pageOrderName)) { + return "info.createdAt"; + } + switch (pageOrderName) { + case "siteName": + return "si.name"; + case "applyUserName": + return "info.applyUserName"; + case "applyLoginName": + return "info.applyLoginName"; + case "yearlyBatch": + return "yearlyBatch"; + case "reserveTargetName": + return "reserveTargetName"; + case "reserveStartTime": + return "info.reserveStartTime"; + case "reserveEndTime": + return "info.reserveEndTime"; + case "applyMobile": + return "info.applyMobile"; + case "curTaskName": + return "curTaskName"; + case "instanceState": + return "ins.state"; + default: + return "info.createdAt"; + } + } + + @At("/executeTask") + @ApiOperation("执行审核任务") + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission(value = {"siteCug.schoolUnionAudit", "h5.siteCug.schoolUnionAudit"}, mode = SaMode.OR) + public Result executeTask(@Param("data") String data) { + Dict args = Json.fromJson(Dict.class, data); + Long processTaskId = args.getLong(FlowConst.PROCESS_TASK_ID_KEY); + ProcessTask currentTask = processTaskService.getById(processTaskId); + if (currentTask == null) { + return Result.error("审核任务不存在"); + } + + SiteCugApply apply = applyService.fetch(args.getStr("applyId")); + if (apply == null) { + return Result.error("预约记录不存在"); + } + + List taskList = new ArrayList<>(); + if (StrUtil.isNotBlank(apply.getBackOption()) && apply.getBackOption().startsWith(YEARLY_BATCH_PREFIX)) { + List batchList = applyService.query(Cnd.where("backOption", "=", apply.getBackOption()).and("createdBy", "=", apply.getCreatedBy())); + List bizIds = batchList.stream().map(SiteCugApply::getId).toList(); + taskList = processTaskService.getDoingTaskByBizIdTaskName(bizIds, currentTask.getTaskName()); + } + if (taskList.isEmpty()) { + taskList.add(currentTask); + } + + for (ProcessTask task : taskList) { + Dict cloneArgs = args.clone(); + cloneArgs.put(FlowConst.PROCESS_TASK_ID_KEY, task.getId()); + flowCommonService.executeTask(cloneArgs); + } + if (taskList.size() > 1) { + return Result.success("已完成该全年预约批次的审核"); + } + return Result.success("提交成功"); + } + + + @At("/revokeTask") + @ApiOperation("撤回审核任务") + @Aop(TransAop.READ_COMMITTED) + @SaCheckPermission(value = {"siteCug.schoolUnionAudit", "h5.siteCug.schoolUnionAudit"}, mode = SaMode.OR) + public Result revokeTask(@Param("taskId") Long taskId, @Param("applyId") String applyId) { + ProcessTask currentTask = processTaskService.getById(taskId); + if (currentTask == null) { + return Result.error("撤回任务不存在"); + } + + SiteCugApply apply = applyService.fetch(applyId); + if (apply == null) { + return Result.error("预约记录不存在"); + } + + List taskList = new ArrayList<>(); + if (StrUtil.isNotBlank(apply.getBackOption()) && apply.getBackOption().startsWith(YEARLY_BATCH_PREFIX)) { + List batchList = applyService.query(Cnd.where("backOption", "=", apply.getBackOption()).and("createdBy", "=", apply.getCreatedBy())); + List bizIds = batchList.stream().map(SiteCugApply::getId).toList(); + taskList = processTaskService.getDoneTaskByBizIdTaskName(bizIds, currentTask.getTaskName()); + } + if (taskList.isEmpty()) { + taskList.add(currentTask); + } + + for (ProcessTask task : taskList) { + flowCommonService.revokeTask(task.getId()); + } + if (taskList.size() > 1) { + return Result.success("已撤回该全年预约批次的审核任务"); + } + return Result.success("撤回成功"); + } + +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/model/SiteCugApply.java b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/model/SiteCugApply.java new file mode 100644 index 0000000..4857fa3 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/model/SiteCugApply.java @@ -0,0 +1,114 @@ +package com.budwk.app.zhgh.dayofficework.siteCug.model; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.nutz.dao.entity.annotation.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +@Data +@Comment("场地预约-场馆") +@Accessors(chain = true) +@Table("site_cug_apply") +@EqualsAndHashCode(callSuper = true) +public class SiteCugApply extends BaseModel { + + @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 siteId; + + @Column + @Comment("预约人") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String applyUserId; + + @Column + @Comment("预约人姓名") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String applyUserName; + + @Column + @Comment("性别") + @ColDefine(type = ColType.VARCHAR, width = 10) + private String applySex; + + @Column + @Comment("预约人工号") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String applyLoginName; + + @Column + @Comment("预约人单位id") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String applyUnitId; + + @Column + @Comment("预约人单位") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String applyUnitName; + + @Column + @Comment("预约类型") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String reserveType; + + @Column + @Comment("分工会id") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String applyUnionId; + + @Column + @Comment("分工会名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String applyUnionName; + + @Column + @Comment("协会id") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String clubId; + + @Column + @Comment("协会名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String clubName; + + @Column + @Comment("预约人联系方式") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String applyMobile; + + @Column + @Comment("预约开始时间") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String reserveStartTime; + + @Column + @Comment("预约结束时间") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String reserveEndTime; + + @Column + @Comment("预约人数") + @ColDefine(type = ColType.INT) + private Integer joinCount; + + @Column + @Comment("预约事由") + @ColDefine(type = ColType.VARCHAR, width = 1000) + private String applyCause; + + @Column + @Comment("反馈意见") + @ColDefine(type = ColType.VARCHAR, width = 200) + private String backOption; + + private String yearlyReserveEndDate; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/model/SiteCugFunctionType.java b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/model/SiteCugFunctionType.java new file mode 100644 index 0000000..60271de --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/model/SiteCugFunctionType.java @@ -0,0 +1,43 @@ +package com.budwk.app.zhgh.dayofficework.siteCug.model; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.nutz.dao.entity.annotation.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; + +@Data +@Comment("功能类型") +@Accessors(chain = true) +@Table("site_cug_function_type") +@EqualsAndHashCode(callSuper = true) +public class SiteCugFunctionType extends BaseModel { + + @Name + @Comment("ID") + @ColDefine(type = ColType.VARCHAR, width = 32) + @PrevInsert(uu32 = true) + private String id; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 30) + @Comment("类型编码") + private String code; + + @Column + @ColDefine(type = ColType.VARCHAR, width = 100) + @Comment("类型名称") + private String name; + + @Column + @ColDefine(type = ColType.BOOLEAN) + @Comment("是否启用") + @Default("1") + private Boolean enable; + + @Column + @ColDefine(type = ColType.INT) + @Comment("排序编号") + private Integer sortNum; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/model/SiteCugInfo.java b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/model/SiteCugInfo.java new file mode 100644 index 0000000..96be2a4 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/model/SiteCugInfo.java @@ -0,0 +1,123 @@ +package com.budwk.app.zhgh.dayofficework.siteCug.model; + +import com.budwk.app.base.model.BaseModel; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.experimental.Accessors; +import org.nutz.dao.entity.annotation.*; +import org.nutz.dao.interceptor.annotation.PrevInsert; +import org.nutz.lang.util.NutMap; + +import java.util.List; + +@Data +@Comment("场地信息-场馆") +@Accessors(chain = true) +@Table("site_cug_info") +@EqualsAndHashCode(callSuper = true) +public class SiteCugInfo extends BaseModel { + + @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 createUserId; + + @Column + @Comment("创建人") + @ColDefine(type = ColType.VARCHAR, width = 20) + private String createUserName; + + @Column + @Comment("排序编号") + @ColDefine(type = ColType.INT) + private Integer sortNum; + + @Column + @Comment("场地名称") + @ColDefine(type = ColType.VARCHAR, width = 100) + private String name; + + @Column + @Comment("场地地址") + @ColDefine(type = ColType.VARCHAR, width = 255) + private String address; + + @Column + @Comment("联系人") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String contactName; + + @Column + @Comment("联系电话") + @ColDefine(type = ColType.VARCHAR, width = 30) + private String contactPhone; + + @Column + @Comment("可容纳人数") + @ColDefine(type = ColType.INT) + private Integer maxNum; + + @Column + @Comment("场地类型") + @ColDefine(type = ColType.VARCHAR, width = 32) + private String typeId; + + @Column + @Comment("性别限制(0不限,1男,2女)") + @ColDefine(type = ColType.INT) + private Integer sexLimit; + + @Column + @Comment("场地的禁用时间") + @ColDefine(type = ColType.MYSQL_JSON) + private List notApplyTimeList; + + @Column + @Comment("预约时间段类型(1分段预约,2全天候预约)") + @ColDefine(type = ColType.INT) + @Default("1") + private Integer reserveTimeType; + + @Column + @Comment("场次信息") + @ColDefine(type = ColType.MYSQL_JSON) + private List openHours; + + @Column + @Comment("分段预约场次信息") + @ColDefine(type = ColType.MYSQL_JSON) + private List segmentedOpenHours; + + @Column + @Comment("全天候预约时间段") + @ColDefine(type = ColType.MYSQL_JSON) + private NutMap fullDayOpenHour; + + @Column + @Comment("开启状态") + @ColDefine(type = ColType.BOOLEAN) + @Default("1") + private Boolean state; + + @Column + @Comment("排除节假日") + @ColDefine(type = ColType.BOOLEAN) + @Default("0") + private Boolean filterHolidays; + + @Column + @Comment("场地介绍") + @ColDefine(type = ColType.TEXT) + private String introduce; + + @Column + @Comment("场地照片") + @ColDefine(type = ColType.VARCHAR, width = 255) + private String sitePhoto; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/param/SiteCugRecordPageForm.java b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/param/SiteCugRecordPageForm.java new file mode 100644 index 0000000..c0a7c4f --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/param/SiteCugRecordPageForm.java @@ -0,0 +1,31 @@ +package com.budwk.app.zhgh.dayofficework.siteCug.param; + +import com.budwk.app.base.param.PageForm; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; +import lombok.EqualsAndHashCode; + +@ApiModel("场馆预约查询参数") +@EqualsAndHashCode(callSuper = true) +@Data +public class SiteCugRecordPageForm extends PageForm { + + @ApiModelProperty("场地类型ID") + private String siteType; + + @ApiModelProperty("场地ID") + private String siteId; + + @ApiModelProperty("预约类型") + private String reserveType; + + @ApiModelProperty("预约单位关键字") + private String reserveTargetKeyword; + + @ApiModelProperty("预约时间开始") + private String reserveTimeStart; + + @ApiModelProperty("预约时间结束") + private String reserveTimeEnd; +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/service/SiteCugApplyService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/service/SiteCugApplyService.java new file mode 100644 index 0000000..1be3c7a --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/service/SiteCugApplyService.java @@ -0,0 +1,24 @@ +package com.budwk.app.zhgh.dayofficework.siteCug.service; + +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugApply; +import com.budwk.app.zhgh.dayofficework.siteCug.param.SiteCugRecordPageForm; +import org.nutz.dao.Cnd; +import org.nutz.dao.sql.Sql; + +import javax.servlet.http.HttpServletResponse; +import java.util.Map; + +public interface SiteCugApplyService extends BaseService { + + Map validateApply(SiteCugApply apply); + + // 场馆预约查询页和导出共用同一套查询字段定义,统一由 service 生成 SQL + Sql buildRecordSql(); + + // 场馆预约查询页和导出的筛选条件统一由 service 组装,避免 controller 重复维护 + Cnd buildRecordCnd(SiteCugRecordPageForm pageForm); + + // 场馆预约查询页的导出逻辑统一放在 service,controller 只负责接收请求 + void exportRecord(SiteCugRecordPageForm pageForm, HttpServletResponse response); +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/service/SiteCugFunctionTypeService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/service/SiteCugFunctionTypeService.java new file mode 100644 index 0000000..a8dd6c6 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/service/SiteCugFunctionTypeService.java @@ -0,0 +1,7 @@ +package com.budwk.app.zhgh.dayofficework.siteCug.service; + +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugFunctionType; + +public interface SiteCugFunctionTypeService extends BaseService { +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/service/SiteCugInfoService.java b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/service/SiteCugInfoService.java new file mode 100644 index 0000000..1fa2d0b --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/service/SiteCugInfoService.java @@ -0,0 +1,7 @@ +package com.budwk.app.zhgh.dayofficework.siteCug.service; + +import com.budwk.app.base.service.BaseService; +import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo; + +public interface SiteCugInfoService extends BaseService { +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/service/impl/SiteCugApplyServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/service/impl/SiteCugApplyServiceImpl.java new file mode 100644 index 0000000..adddbe9 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/service/impl/SiteCugApplyServiceImpl.java @@ -0,0 +1,323 @@ +package com.budwk.app.zhgh.dayofficework.siteCug.service.impl; + +import cn.afterturn.easypoi.excel.ExcelExportUtil; +import cn.afterturn.easypoi.excel.entity.ExportParams; +import cn.afterturn.easypoi.excel.entity.enmus.ExcelType; +import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity; +import cn.hutool.core.date.DateTime; +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.util.StrUtil; +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.base.utils.CommonDownloadUtil; +import com.budwk.app.base.utils.PageUtil; +import com.budwk.app.flow.entity.ProcessInstance; +import com.budwk.app.flow.enums.ProcessInstanceStateEnum; +import com.budwk.app.sys.models.SysHoliday; +import com.budwk.app.sys.models.Sys_union; +import com.budwk.app.web.commons.auth.utils.SecurityUtil; +import com.budwk.app.zhgh.club.model.SysClub; +import com.budwk.app.zhgh.club.service.SysClubService; +import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugApply; +import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo; +import com.budwk.app.zhgh.dayofficework.siteCug.param.SiteCugRecordPageForm; +import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugApplyService; +import lombok.extern.slf4j.Slf4j; +import org.apache.poi.ss.usermodel.Workbook; +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.IocBean; +import org.nutz.ioc.loader.annotation.Inject; +import org.nutz.lang.util.NutMap; + +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +@IocBean(args = {"refer:dao"}) +public class SiteCugApplyServiceImpl extends BaseServiceImpl implements SiteCugApplyService { + + @Inject + private SysClubService sysClubService; + + public SiteCugApplyServiceImpl(Dao dao) { + super(dao); + } + + @Override + public Map validateApply(SiteCugApply apply) { + // 预约类型校验和自动回填: + // 1. 分工会预约时,强制读取当前登录人的分工会信息 + // 2. 协会预约时,只允许选择当前登录人实际管理的协会 + Map reserveTypeValidate = fillReserveTypeData(apply); + if (reserveTypeValidate.containsKey(false)) { + return reserveTypeValidate; + } + + if (StrUtil.isBlank(apply.getReserveStartTime()) || StrUtil.isBlank(apply.getReserveEndTime())) { + return Map.of(false, "预约时间不能为空"); + } + if (!DateUtil.parseDateTime(apply.getReserveEndTime()).isAfter(DateUtil.parseDateTime(apply.getReserveStartTime()))) { + return Map.of(false, "结束时间必须晚于开始时间"); + } + if (!DateUtil.parseDateTime(apply.getReserveStartTime()).isAfter(DateUtil.date())) { + return Map.of(false, "预约开始时间必须晚于当前时间"); + } + + SiteCugInfo siteInfo = dao().fetch(SiteCugInfo.class, apply.getSiteId()); + if (siteInfo == null) { + return Map.of(false, "场地不存在"); + } + if (!Boolean.TRUE.equals(siteInfo.getState())) { + return Map.of(false, "该场地未开启预约"); + } + if (apply.getJoinCount() == null || apply.getJoinCount() <= 0) { + return Map.of(false, "预约人数必须大于0"); + } + if (apply.getJoinCount() > siteInfo.getMaxNum()) { + return Map.of(false, "预约人数最多为%s人".formatted(siteInfo.getMaxNum())); + } + // 周六、周日整天不可预约,避免只做前端限制被绕过 + if (containsWeekend(apply.getReserveStartTime(), apply.getReserveEndTime())) { + return Map.of(false, "预约时间不能落在周六或周日"); + } + // 开启排除节假日后,节假日整天不可预约 + if (Boolean.TRUE.equals(siteInfo.getFilterHolidays()) && containsHoliday(apply.getReserveStartTime(), apply.getReserveEndTime())) { + return Map.of(false, "预约时间不能落在节假日"); + } + // 只要预约时间范围和禁用时间段有交叉,就不允许预约 + if (intersectsDisabledTime(siteInfo, apply.getReserveStartTime(), apply.getReserveEndTime())) { + return Map.of(false, "预约时间落在禁用时间内"); + } + + Sql sql = Sqls.create(""" + select + sa.* + from + site_cug_apply sa + left join wf_process_instance ins on ins.businessNo = sa.id + $condition + """); + Cnd cnd = Cnd.NEW(); + cnd.and(SiteCugApply::getSiteId, "=", apply.getSiteId()); + cnd.andEX("sa.id", "!=", apply.getId()); + List stateList = List.of(ProcessInstanceStateEnum.DOING.getCode(), ProcessInstanceStateEnum.FINISHED.getCode(), ProcessInstanceStateEnum.PENDING.getCode()); + cnd.and(ProcessInstance::getState, "in", stateList); + sql.setCondition(cnd); + List applyList = listEntity(sql); + + // 只要已有预约和当前申请时间段有交叉,并且流程状态是待审核/办理中/已通过,就直接判定冲突 + for (SiteCugApply item : applyList) { + boolean overlap = DateUtil.parseDateTime(item.getReserveStartTime()).isBefore(DateUtil.parseDateTime(apply.getReserveEndTime())) + && DateUtil.parseDateTime(item.getReserveEndTime()).isAfter(DateUtil.parseDateTime(apply.getReserveStartTime())); + if (overlap) { + if (StrUtil.isBlank(apply.getId()) && StrUtil.equals(item.getApplyUserId(), SecurityUtil.getUserId())) { + return Map.of(false, "该时间段您已存在预约"); + } + return Map.of(false, String.format("预约时间冲突,%s 至 %s 已被预约,请重新选择", item.getReserveStartTime(), item.getReserveEndTime())); + } + } + return Map.of(true, ""); + } + + @Override + public Sql buildRecordSql() { + // 场馆预约查询页和导出页共用同一套返回字段,统一在 service 中维护,避免 controller 重复拼 SQL + return Sqls.create(""" + SELECT + info.*, + si.name AS siteName, + ins.id AS instanceId, + ins.processDefineId instanceProcessDefineId, + CASE WHEN info.backOption LIKE 'YEARLY_BATCH:%' THEN 1 ELSE 0 END AS yearlyBatch, + CASE WHEN info.reserveType = 'club' THEN '协会预约' ELSE '分工会预约' END AS reserveTypeName, + CASE WHEN info.reserveType = 'club' THEN info.clubName ELSE info.applyUnionName END AS reserveTargetName, + DATE_FORMAT(info.createdAt, '%Y-%m-%d %H:%i:%s') AS applyTime + FROM + site_cug_apply info + LEFT JOIN wf_process_instance ins ON ins.businessNo = info.id + LEFT JOIN site_cug_info si ON info.siteId = si.id + $condition + """); + } + + @Override + public Cnd buildRecordCnd(SiteCugRecordPageForm pageForm) { + // 场馆预约查询页和导出共用同一套筛选条件,保证列表看见什么,导出就拿到什么 + Cnd cnd = Cnd.NEW(); + if (StrUtil.isNotBlank(pageForm.getSearchKeyword())) { + SqlExpressionGroup seg = new SqlExpressionGroup(); + seg.or(SiteCugApply::getApplyUserName, "like", "%" + pageForm.getSearchKeyword() + "%"); + seg.or(SiteCugApply::getApplyLoginName, "like", "%" + pageForm.getSearchKeyword() + "%"); + cnd.and(seg); + } + if (StrUtil.isNotBlank(pageForm.getReserveTargetKeyword())) { + // 预约单位支持同时匹配分工会名称和协会名称,方便直接查某个预约主体的全部记录 + SqlExpressionGroup reserveTargetSeg = new SqlExpressionGroup(); + reserveTargetSeg.or("info.applyUnionName", "like", "%" + pageForm.getReserveTargetKeyword() + "%"); + reserveTargetSeg.or("info.clubName", "like", "%" + pageForm.getReserveTargetKeyword() + "%"); + cnd.and(reserveTargetSeg); + } + if (StrUtil.isNotBlank(pageForm.getReserveType())) { + cnd.andEX("info.reserveType", "=", pageForm.getReserveType()); + } + if (StrUtil.isNotBlank(pageForm.getReserveTimeStart())) { + // 按预约时间查询时,开始边界取“预约结束时间大于等于筛选开始”,这样跨段预约也能查出来 + cnd.andEX("info.reserveEndTime", ">=", pageForm.getReserveTimeStart()); + } + if (StrUtil.isNotBlank(pageForm.getReserveTimeEnd())) { + // 按预约时间查询时,结束边界取“预约开始时间小于等于筛选结束”,保证时间段有交叉就能命中 + cnd.andEX("info.reserveStartTime", "<=", pageForm.getReserveTimeEnd()); + } + cnd.andEX("si.typeId", "=", pageForm.getSiteType()); + cnd.andEX("info.siteId", "=", pageForm.getSiteId()); + // 预约查询页固定只看审核结束记录;导出复用同一条件,实现所见即所得 + cnd.and("ins.state", "in", List.of(ProcessInstanceStateEnum.FINISHED.getCode(), ProcessInstanceStateEnum.REJECT.getCode())); + if (StrUtil.isNotBlank(pageForm.getPageOrderName()) && StrUtil.isNotBlank(pageForm.getPageOrderBy())) { + cnd.orderBy(pageForm.getPageOrderName(), PageUtil.getOrder(pageForm.getPageOrderBy())); + } else { + cnd.desc("info.createdAt"); + } + cnd.groupBy("info.id"); + return cnd; + } + + @Override + public void exportRecord(SiteCugRecordPageForm pageForm, HttpServletResponse response) { + // 导出直接复用列表同一套查询条件和字段定义,保证导出结果与当前列表查询结果一致 + Sql sql = buildRecordSql(); + Cnd cnd = buildRecordCnd(pageForm); + sql.setCondition(cnd); + sql.setCallback(Sqls.callback.maps()); + dao().execute(sql); + List list = sql.getList(NutMap.class); + ExportParams exportParams = new ExportParams(); + exportParams.setTitle("预约查询"); + exportParams.setSheetName("预约查询"); + exportParams.setType(ExcelType.XSSF); + Workbook workbook = ExcelExportUtil.exportExcel(exportParams, buildRecordExportEntities(), list); + CommonDownloadUtil.download("场馆预约查询.xlsx", workbook, response); + } + + // 场馆预约查询导出字段统一在 service 中定义,并与当前列表展示字段保持一致 + private List buildRecordExportEntities() { + List exportEntities = new ArrayList<>(); + exportEntities.add(new ExcelExportEntity("场地名称", "siteName", 20)); + exportEntities.add(new ExcelExportEntity("预约人", "applyUserName", 20)); + exportEntities.add(new ExcelExportEntity("预约类型", "reserveTypeName", 20)); + exportEntities.add(new ExcelExportEntity("预约单位", "reserveTargetName", 20)); + exportEntities.add(new ExcelExportEntity("所属单位", "applyUnitName", 20)); + exportEntities.add(new ExcelExportEntity("预约开始时间", "reserveStartTime", 24)); + exportEntities.add(new ExcelExportEntity("预约结束时间", "reserveEndTime", 24)); + exportEntities.add(new ExcelExportEntity("联系方式", "applyMobile", 20)); + return exportEntities; + } + + // 预约时间范围内只要包含周六或周日,就视为不可预约 + private boolean containsWeekend(String reserveStartTime, String reserveEndTime) { + Date start = DateUtil.beginOfDay(DateUtil.parseDateTime(reserveStartTime)); + Date end = DateUtil.beginOfDay(DateUtil.parseDateTime(reserveEndTime)); + Date current = start; + while (current.compareTo(end) <= 0) { + DateTime currentDay = DateUtil.date(current); + int week = currentDay.dayOfWeek() - 1; + if (week == 0 || week == 6) { + return true; + } + current = DateUtil.offsetDay(current, 1); + } + return false; + } + + // 预约时间范围内只要包含节假日,就视为不可预约 + private boolean containsHoliday(String reserveStartTime, String reserveEndTime) { + Date start = DateUtil.beginOfDay(DateUtil.parseDateTime(reserveStartTime)); + Date end = DateUtil.beginOfDay(DateUtil.parseDateTime(reserveEndTime)); + List dayList = new ArrayList<>(); + Date current = start; + while (current.compareTo(end) <= 0) { + dayList.add(DateUtil.formatDate(current)); + current = DateUtil.offsetDay(current, 1); + } + if (dayList.isEmpty()) { + return false; + } + List holidays = dao().query(SysHoliday.class, Cnd.NEW()); + Set holidaySet = holidays.stream().map(SysHoliday::getDay).filter(StrUtil::isNotBlank).collect(Collectors.toSet()); + return dayList.stream().anyMatch(holidaySet::contains); + } + + // 预约时间范围只要与后台配置的禁用时间段有交叉,就直接拦截 + private boolean intersectsDisabledTime(SiteCugInfo siteInfo, String reserveStartTime, String reserveEndTime) { + List timeRanges = siteInfo.getNotApplyTimeList(); + if (timeRanges == null || timeRanges.isEmpty()) { + return false; + } + DateTime reserveStart = DateUtil.parseDateTime(reserveStartTime); + DateTime reserveEnd = DateUtil.parseDateTime(reserveEndTime); + for (NutMap range : timeRanges) { + if (range == null || StrUtil.hasBlank(range.getString("date"), range.getString("startTime"), range.getString("endTime"))) { + continue; + } + DateTime disabledStart = DateUtil.parseDateTime(range.getString("date") + " " + range.getString("startTime") + ":00"); + DateTime disabledEnd = DateUtil.parseDateTime(range.getString("date") + " " + range.getString("endTime") + ":00"); + boolean overlap = reserveStart.isBefore(disabledEnd) && reserveEnd.isAfter(disabledStart); + if (overlap) { + return true; + } + } + return false; + } + + // 按预约类型补齐最终落库数据,避免前端传错或被绕过 + private Map fillReserveTypeData(SiteCugApply apply) { + if (StrUtil.isBlank(apply.getReserveType())) { + return Map.of(false, "请选择预约类型"); + } + if (StrUtil.equals(apply.getReserveType(), "union")) { + String unionId = SecurityUtil.getUnionId(); + if (StrUtil.isBlank(unionId)) { + return Map.of(false, "当前用户未关联分工会,不能发起分工会预约"); + } + Sys_union union = dao().fetch(Sys_union.class, unionId); + if (union == null) { + return Map.of(false, "当前用户分工会信息不存在,请联系管理员核查"); + } + apply.setApplyUnionId(union.getId()); + apply.setApplyUnionName(union.getName()); + apply.setClubId(""); + apply.setClubName(""); + return Map.of(true, ""); + } + if (StrUtil.equals(apply.getReserveType(), "club")) { + if (StrUtil.isBlank(apply.getClubId())) { + return Map.of(false, "请选择协会"); + } + // 协会预约直接复用项目现成能力;getMyManageClub() 对 SYSADMIN 已放开,因此超级管理员也能代任意协会预约 + List myManageClub = sysClubService.getMyManageClub(); + SysClub club = myManageClub.stream() + .filter(item -> StrUtil.equals(item.getId(), apply.getClubId())) + .findFirst() + .orElse(null); + if (club == null) { + return Map.of(false, "所选协会不是您当前管理的协会"); + } + apply.setClubName(club.getClubName()); + apply.setApplyUnionId(""); + apply.setApplyUnionName(""); + return Map.of(true, ""); + } + return Map.of(false, "预约类型不合法"); + } +} + + + + diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/service/impl/SiteCugFunctionTypeServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/service/impl/SiteCugFunctionTypeServiceImpl.java new file mode 100644 index 0000000..3e53f6c --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/service/impl/SiteCugFunctionTypeServiceImpl.java @@ -0,0 +1,17 @@ +package com.budwk.app.zhgh.dayofficework.siteCug.service.impl; + +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugFunctionType; +import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugFunctionTypeService; +import lombok.extern.slf4j.Slf4j; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.IocBean; + +@Slf4j +@IocBean(args = {"refer:dao"}) +public class SiteCugFunctionTypeServiceImpl extends BaseServiceImpl implements SiteCugFunctionTypeService { + + public SiteCugFunctionTypeServiceImpl(Dao dao) { + super(dao); + } +} diff --git a/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/service/impl/SiteCugInfoServiceImpl.java b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/service/impl/SiteCugInfoServiceImpl.java new file mode 100644 index 0000000..0292fb6 --- /dev/null +++ b/src/main/java/com/budwk/app/zhgh/dayofficework/siteCug/service/impl/SiteCugInfoServiceImpl.java @@ -0,0 +1,17 @@ +package com.budwk.app.zhgh.dayofficework.siteCug.service.impl; + +import com.budwk.app.base.service.impl.BaseServiceImpl; +import com.budwk.app.zhgh.dayofficework.siteCug.model.SiteCugInfo; +import com.budwk.app.zhgh.dayofficework.siteCug.service.SiteCugInfoService; +import lombok.extern.slf4j.Slf4j; +import org.nutz.dao.Dao; +import org.nutz.ioc.loader.annotation.IocBean; + +@Slf4j +@IocBean(args = {"refer:dao"}) +public class SiteCugInfoServiceImpl extends BaseServiceImpl implements SiteCugInfoService { + + public SiteCugInfoServiceImpl(Dao dao) { + super(dao); + } +} 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 0000000..13232d4 --- /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 0000000..7bbbbdf --- /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 0000000..b6758ac --- /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 0000000..9905aaa --- /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 0000000..bbe52fe --- /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 0000000..1d669e7 --- /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 0000000..1c6b746 --- /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 0000000..08dab7e --- /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 0000000..52ed7f9 --- /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("tour.signup") + 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 0000000..900a571 --- /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 0000000..4a222e6 --- /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 0000000..068f047 --- /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 0000000..b19393d --- /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 0000000..0e1af54 --- /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 0000000..7021209 --- /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 0000000..87c1807 --- /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 0000000..9fbfaef --- /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 0000000..f06aec8 --- /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 0000000..37e212d --- /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 0000000..9b50236 --- /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 0000000..88f0865 --- /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 0000000..f92bb26 --- /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 0000000..f1ce741 --- /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 0000000..076a5dd --- /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 0000000..240cf45 --- /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 0000000..8171b59 --- /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 0000000..ca1cb4e --- /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 0000000..f90ba29 --- /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 0000000..8da9d20 --- /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 0000000..0e045b6 --- /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 0000000..88c2f66 --- /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 0000000..c06e6cf --- /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 0000000..c7e10db --- /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 0000000..68f57fc --- /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 0000000..5122784 --- /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/resources/db/menu/init_tour_menu.sql b/src/main/resources/db/menu/init_tour_menu.sql new file mode 100644 index 0000000..0042dd6 --- /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/site_cug_info_add_booking_mode_cache.sql b/src/main/resources/db/site_cug_info_add_booking_mode_cache.sql new file mode 100644 index 0000000..d80232f --- /dev/null +++ b/src/main/resources/db/site_cug_info_add_booking_mode_cache.sql @@ -0,0 +1,3 @@ +ALTER TABLE `site_cug_info` +ADD COLUMN `segmented_open_hours` JSON NULL COMMENT '分段预约场次信息' AFTER `open_hours`, +ADD COLUMN `full_day_open_hour` JSON NULL COMMENT '全天候预约时间段' AFTER `segmented_open_hours`; diff --git a/src/main/resources/db/site_cug_info_add_reserve_config.sql b/src/main/resources/db/site_cug_info_add_reserve_config.sql new file mode 100644 index 0000000..0802c14 --- /dev/null +++ b/src/main/resources/db/site_cug_info_add_reserve_config.sql @@ -0,0 +1,3 @@ +ALTER TABLE `site_cug_info` +ADD COLUMN `reserve_time_type` INT NULL DEFAULT 1 COMMENT '预约时间段类型(1分段预约,2全天候预约)' AFTER `not_apply_time_list`, +ADD COLUMN `open_hours` JSON NULL COMMENT '场次信息' AFTER `reserve_time_type`; diff --git a/src/main/resources/db/site_cug_info_add_site_photo.sql b/src/main/resources/db/site_cug_info_add_site_photo.sql new file mode 100644 index 0000000..a4d6297 --- /dev/null +++ b/src/main/resources/db/site_cug_info_add_site_photo.sql @@ -0,0 +1,2 @@ +ALTER TABLE `site_cug_info` +ADD COLUMN `site_photo` VARCHAR(255) NULL COMMENT '场地照片' AFTER `introduce`; 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 0000000..d81d6ef --- /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 0000000..1b782a0 --- /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 0000000..8705341 --- /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 0000000..f7ad5ea --- /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 0000000..1d0d1cc --- /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 0000000..8b06c21 --- /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 0000000..5abcb49 --- /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 0000000..eb9dd2d --- /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 0000000..7f48198 --- /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 0000000..4a02d0f --- /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 0000000..7a883bf --- /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 0000000..188e4fd --- /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 0000000..103af78 --- /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 0000000..954ac7c --- /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 0000000..d26ec64 --- /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 0000000..c4c68d2 --- /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 0000000..6e9567b --- /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 0000000..4dd9788 --- /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 0000000..5444737 --- /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/ioc/upload.json b/src/main/resources/ioc/upload.json index d1bce19..55f642a 100644 --- a/src/main/resources/ioc/upload.json +++ b/src/main/resources/ioc/upload.json @@ -32,7 +32,7 @@ var ioc={ // 单个文件最大尺寸(大约的值,单位为字节,即 209715200 为 200M) maxFileSize : 209715200, // 正则表达式匹配可以支持的文件名 - nameFilter : '^(.+[.])(gif||svg|jpg|png|jpeg|doc|docx|xls|xlsx|rar|zip|7z|txt|pdf|pptx|ppt|mp4)$' + nameFilter : '^(.+[.])(gif||svg|jpg|png|jpeg|doc|docx|xls|xlsx|rar|zip|7z|txt|pdf|pptx|ppt|flv|swf|mkv|avi|rm|rmvb|wmv|mp4|mov|mpg|mpeg|webm|m4v|3gp|3g2|ts|mts|m2ts|ogv)$' } }, fileUpload : { 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 0000000..530371e Binary files /dev/null and b/src/main/resources/static/assets/platform/images/tour/tour-direct-family.jpg differ diff --git a/src/main/resources/static/assets/platform/images/tour/tour-h5-banner-1.jpg b/src/main/resources/static/assets/platform/images/tour/tour-h5-banner-1.jpg new file mode 100644 index 0000000..4d01bd6 Binary files /dev/null and b/src/main/resources/static/assets/platform/images/tour/tour-h5-banner-1.jpg differ diff --git a/src/main/resources/static/assets/platform/images/tour/tour-h5-banner-2.jpg b/src/main/resources/static/assets/platform/images/tour/tour-h5-banner-2.jpg new file mode 100644 index 0000000..f304104 Binary files /dev/null and b/src/main/resources/static/assets/platform/images/tour/tour-h5-banner-2.jpg differ diff --git a/src/main/resources/static/assets/platform/images/tour/tour-in-province.png b/src/main/resources/static/assets/platform/images/tour/tour-in-province.png new file mode 100644 index 0000000..82be979 Binary files /dev/null and b/src/main/resources/static/assets/platform/images/tour/tour-in-province.png differ diff --git a/src/main/resources/static/assets/platform/images/tour/tour-out-province.png b/src/main/resources/static/assets/platform/images/tour/tour-out-province.png new file mode 100644 index 0000000..c67e5b9 Binary files /dev/null and b/src/main/resources/static/assets/platform/images/tour/tour-out-province.png differ diff --git a/src/main/resources/static/components/plugins/sysUpload/index.vue b/src/main/resources/static/components/plugins/sysUpload/index.vue index 370d6cf..83b916a 100644 --- a/src/main/resources/static/components/plugins/sysUpload/index.vue +++ b/src/main/resources/static/components/plugins/sysUpload/index.vue @@ -322,29 +322,51 @@ module.exports = { return true } - // 去除前后空格并转换为数组 - const acceptTypes = this.fileAccept - .trim() - .split(",") - .map((type) => type.trim().toUpperCase()) - - let fileType - const dotIndex = file.name.lastIndexOf(".") - if (dotIndex !== -1) { - fileType = file.name.substring(dotIndex).toUpperCase() - } else { - this.$message.error("文件必须包含有效的扩展名!") - return false - } - - const valid = acceptTypes.includes(fileType) + const valid = this.isAcceptFile(file) if (valid) { return true } - this.$message.error(`文件只能是 ${this.fileAccept} 格式!`) + this.$message.error("文件只能是 " + this.fileAccept + " 格式!") return false }, + // 兼容 accept="video/*" 这类 MIME 通配写法,同时保留原有扩展名白名单校验。 + isAcceptFile(file) { + if (!this.fileAccept || typeof this.fileAccept !== "string") { + return true + } + const acceptTypes = this.getAcceptTypes() + const fileName = file.name || "" + const dotIndex = fileName.lastIndexOf(".") + const fileType = dotIndex !== -1 ? fileName.substring(dotIndex).toUpperCase() : "" + const mimeType = (file.type || "").toUpperCase() + + if (!fileType) { + this.$message.error("文件必须包含有效的扩展名!") + return false + } + + return acceptTypes.some((acceptType) => { + if (acceptType.endsWith("/*")) { + return mimeType.startsWith(acceptType.substring(0, acceptType.length - 1)) || this.matchVideoWildcard(acceptType, fileType) + } + return acceptType === fileType + }) + }, + + // 部分浏览器或系统无法识别视频 MIME,此处用视频后缀兜底保证 video/* 可覆盖常见视频格式。 + matchVideoWildcard(acceptType, fileType) { + const videoExtTypes = [".FLV", ".SWF", ".MKV", ".AVI", ".RM", ".RMVB", ".WMV", ".MP4", ".MOV", ".MPG", ".MPEG", ".WEBM", ".M4V", ".3GP", ".3G2", ".TS", ".MTS", ".M2TS", ".OGV"] + return acceptType === "VIDEO/*" && videoExtTypes.includes(fileType) + }, + + getAcceptTypes() { + return this.fileAccept + .trim() + .split(",") + .map((type) => type.trim().toUpperCase()) + }, + // 预览图片 handlePreview(file) { const suffix = file.name.substring(file.name.lastIndexOf(".") + 1).toLowerCase() @@ -571,24 +593,13 @@ module.exports = { } if (this.fileAccept) { - // 去除前后空格并转换为数组 - const acceptTypes = this.fileAccept - .trim() - .split(",") - .map((type) => type.trim().toUpperCase()) - const validFiles = data.files.filter((file) => { - let fileType - const dotIndex = file.name.lastIndexOf(".") - if (dotIndex !== -1) { - fileType = file.name.substring(dotIndex).toUpperCase() - } - return acceptTypes.includes(fileType) + return this.isAcceptFile(file) }) if (validFiles.length < data.files.length) { data.files = validFiles - this.$message.error(`文件只能是 ${this.fileAccept} 格式,已自动过滤掉不符合要求文件!`) + this.$message.error("文件只能是 " + this.fileAccept + " 格式,已自动过滤掉不符合要求文件!") } } diff --git a/src/main/resources/templates/tour/direct_relative_reimbursement_apply.docx b/src/main/resources/templates/tour/direct_relative_reimbursement_apply.docx new file mode 100644 index 0000000..d4a8abc Binary files /dev/null and b/src/main/resources/templates/tour/direct_relative_reimbursement_apply.docx differ 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 0000000..7668c7e Binary files /dev/null and b/src/main/resources/templates/tour/over_cost_reimbursement_apply.docx differ 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 0000000..db6e4b1 --- /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 0000000..1dbbc14 --- /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 0000000..ce7e8c9 --- /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 0000000..535e919 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/Tour/mysignup/index.html @@ -0,0 +1,1389 @@ + + +
+ + + + + + + + + + + + + + + + +
+ 搜索 + 重置 +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
教职工信息
+ + {{ 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 0000000..69a541e --- /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 0000000..5a1aada --- /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 0000000..93009dd --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/Tour/schoolUnionApproval/index.html @@ -0,0 +1,547 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 搜索 + 重置 +
+
+
+ + + + + 已审核 + 未审核 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + 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 0000000..1da707e --- /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 0000000..c78f554 --- /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 0000000..b418a9f --- /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 0000000..c91705f --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/Tour/unionApproval/index.html @@ -0,0 +1,547 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ 搜索 + 重置 +
+
+
+ + + + + 已审核 + 未审核 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + + 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 0000000..98cfada --- /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/zhgh/dayofficework/siteCug/apply/apply.js b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/apply/apply.js new file mode 100644 index 0000000..ecd499b --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/apply/apply.js @@ -0,0 +1,1137 @@ +const apply = { + template: /*language=HTML*/ ` +
+ + +
+ 您正在预约【{{ row.name }}】活动场地 +
+
+
+ + + + + + + + + + + + + + + + + + + + + + 分工会预约 + 协会预约 + + + + {{ item.label }} + + + 当前无可用的预约权限 + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
可预约时段
+
{{ scheduleRuleText }}
+
+
+ {{ reserveModeText }} + + +
+
+ +
+ 未预约 + 已预约 + 已选择 + 不可预约 +
+ + + + + + +
+ 当前选择:{{ selectionSummary }} +
+
+
+ + + + + + + + + + + + + + + +
+ 预约本年后续每周同一时段 + + 例如先选某个周一 09:00-11:00,勾选后会自动预约本年剩余所有周一的这个时段。 + +
+
+ + +
+
+ {{ batchReserveSummary }} +
+
+
+ + + + + +
+
+ + + 取消 + 提交预约 + +
+ `, + store, + data() { + return { + row: {}, + clubOptions: [], + yearlyReserve: false, + scheduleDate: '', + availabilityLoading: false, + selectedBlockKeys: [], + timeLimitConfig: { + filterHolidays: false, + holidayList: [], + notApplyTimeList: [], + }, + availabilityData: { + date: '', + reserveTimeType: 1, + reserveTimeTypeName: '分段预约', + blocks: [], + }, + pickerRefreshKey: 0, + formData: { + reserveType: '', + applyUnionId: '', + applyUnionName: '', + clubId: '', + clubName: '', + reserveStartTime: '', + reserveEndTime: '', + yearlyReserveEndDate: '', + joinCount: 1, + applyCause: '', + }, + formRules: { + applyUserName: [{required: true, message: '必填', trigger: ['blur', 'change']}], + applySex: [{required: true, message: '必填', trigger: ['blur', 'change']}], + applyLoginName: [{required: true, message: '必填', trigger: ['blur', 'change']}], + applyUnitName: [{required: true, message: '必填', trigger: ['blur', 'change']}], + reserveType: [{required: true, message: '必填', trigger: ['blur', 'change']}], + applyMobile: [{required: true, message: '必填', trigger: ['blur', 'change']}], + reserveStartTime: [{required: true, message: '必填', trigger: ['blur', 'change']}], + reserveEndTime: [{required: true, message: '必填', trigger: ['blur', 'change']}], + joinCount: [{required: true, message: '必填', trigger: ['blur', 'change']}], + applyCause: [{required: true, message: '必填', trigger: ['blur', 'change']}], + clubId: [{ + validator: (rule, value, callback) => { + if (this.formData.reserveType === 'club' && !value) { + callback(new Error('请选择您管理的协会')) + return + } + callback() + }, + trigger: ['blur', 'change'] + }], + }, + } + }, + computed: { + canApplyUnion() { + return this.$auth.hasPermission('siteCug.apply.union') + }, + canApplyClub() { + return this.$auth.hasPermission('siteCug.apply.club') + }, + availableReserveTypes() { + const result = [] + if (this.canApplyUnion) { + result.push('union') + } + if (this.canApplyClub) { + result.push('club') + } + return result + }, + availableReserveTypeOptions() { + const result = [] + if (this.canApplyUnion) { + result.push({value: 'union', label: '分工会预约'}) + } + if (this.canApplyClub) { + result.push({value: 'club', label: '协会预约'}) + } + return result + }, + availabilityBlocks() { + return Array.isArray(this.availabilityData.blocks) ? this.availabilityData.blocks : [] + }, + isSegmentedMode() { + return Number(this.availabilityData.reserveTimeType || this.row.reserveTimeType || 1) === 1 + }, + reserveModeText() { + return this.availabilityData.reserveTimeTypeName || (this.isSegmentedMode ? '分段预约' : '全天候预约') + }, + scheduleRuleText() { + if (this.isSegmentedMode) { + return '分段预约场地需按场次时间单位选择一个或多个连续时段,不能跨间隔跳选。' + } + return '全天候预约场地需按预约时间单位选择一个或多个连续时段,开始和结束时间会自动回填。' + }, + segmentedBlockGroups() { + const groups = [] + this.availabilityBlocks.forEach(block => { + let target = groups.find(item => item.groupIndex === block.groupIndex) + if (!target) { + target = { + groupIndex: block.groupIndex, + groupLabel: block.groupLabel, + blocks: [], + } + groups.push(target) + } + target.blocks.push(block) + }) + return groups + }, + startPickerOptions() { + return { + disabledDate: (time) => this.isDisabledDate(time), + selectableRange: this.buildSelectableRange(this.formData.reserveStartTime), + } + }, + endPickerOptions() { + return { + disabledDate: (time) => this.isDisabledDate(time), + selectableRange: this.buildSelectableRange(this.formData.reserveEndTime), + } + }, + yearlyReserveEndDatePickerOptions() { + return { + disabledDate: (time) => { + if (!this.formData.reserveStartTime) { + return false + } + return this.$moment(time).format('YYYY-MM-DD') < this.formData.reserveStartTime.slice(0, 10) + }, + } + }, + yearlyReserveDates() { + if (!this.yearlyReserve || !this.formData.reserveStartTime || !this.formData.reserveEndTime || !this.formData.yearlyReserveEndDate) { + return [] + } + const start = this.$moment(this.formData.reserveStartTime) + const end = this.$moment(this.formData.reserveEndTime) + const reserveEndDate = this.$moment(this.formData.yearlyReserveEndDate, 'YYYY-MM-DD') + if (!start.isValid() || !end.isValid() || !end.isAfter(start) || !reserveEndDate.isValid() || reserveEndDate.isBefore(start, 'day')) { + return [] + } + const result = [] + const current = start.clone().startOf('day') + const reserveEndDay = reserveEndDate.clone().startOf('day') + const targetWeekDay = start.day() + const startClock = start.format('HH:mm:ss') + const endClock = end.format('HH:mm:ss') + while (current.isSameOrBefore(reserveEndDay, 'day')) { + if (current.day() === targetWeekDay) { + const day = current.format('YYYY-MM-DD') + const startTime = day + ' ' + startClock + const endTime = day + ' ' + endClock + if (this.validateTimeLimit(false, startTime, endTime) && this.validateGraphSelection(false, startTime, endTime, day)) { + result.push(day) + } + } + current.add(1, 'day') + } + return result + }, + batchReserveSummary() { + if (!this.yearlyReserve) { + return '' + } + if (!this.formData.yearlyReserveEndDate) { + return '请选择批量预约截止日期' + } + if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) { + return '请先选择开始和结束时间,再批量预约截止日期内的同星期时段' + } + if (!this.yearlyReserveDates.length) { + return '当前时间段在截止日期内无法生成可批量预约的日期' + } + return '将从 ' + this.yearlyReserveDates[0] + ' 开始,自动预约到 ' + this.formData.yearlyReserveEndDate + ',共 ' + this.yearlyReserveDates.length + ' 个时段' + }, + yearlyReserveSummary() { + if (!this.yearlyReserve) { + return '' + } + if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) { + return '请先选择开始和结束时间后,再批量预约本年同星期时段' + } + if (!this.yearlyReserveDates.length) { + return '当前时间段无法生成本年批量预约日期' + } + return '将从 ' + this.yearlyReserveDates[0] + ' 开始,自动预约到 ' + this.yearlyReserveDates[this.yearlyReserveDates.length - 1] + ',共 ' + this.yearlyReserveDates.length + ' 个时段' + }, + batchReserveSummary() { + if (!this.yearlyReserve) { + return '' + } + if (!this.formData.yearlyReserveEndDate) { + return '请选择批量预约截止日期' + } + if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) { + return '请先选择开始和结束时间,再设置批量预约截止日期' + } + if (!this.yearlyReserveDates.length) { + return '当前截止日期内没有可批量预约的同星期时段' + } + return '将从 ' + this.formData.reserveStartTime.slice(0, 10) + ' 开始,自动预约到 ' + this.formData.yearlyReserveEndDate + }, + selectionSummary() { + if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) { + return '' + } + return this.formData.reserveStartTime + ' 至 ' + this.formData.reserveEndTime + }, + }, + watch: { + yearlyReserve(val) { + if (val) { + this.syncYearlyReserveEndDate(true) + } + }, + }, + methods: { + createDefaultFormData(row) { + const reserveType = this.getDefaultReserveType() + return { + siteId: row.id, + applyUserId: this.$store.state.user.id, + applyUserName: this.$store.state.user.username, + applySex: this.$store.state.user.sex, + applyLoginName: this.$store.state.user.loginname, + applyUnitId: this.$store.state.user?.unit?.id, + applyUnitName: this.$store.state.user?.unit?.name, + reserveType, + applyUnionId: reserveType === 'union' ? (this.$store.state.user?.union?.id || '') : '', + applyUnionName: reserveType === 'union' ? (this.$store.state.user?.union?.name || '') : '', + clubId: '', + clubName: '', + applyMobile: this.$store.state.user.mobile, + reserveStartTime: '', + reserveEndTime: '', + yearlyReserveEndDate: '', + joinCount: 1, + applyCause: '', + } + }, + getDefaultReserveType() { + return this.availableReserveTypes[0] || '' + }, + async onOpen(row) { + this.row = row || {} + this.clubOptions = [] + this.yearlyReserve = false + this.selectedBlockKeys = [] + this.scheduleDate = '' + this.availabilityData = { + date: '', + reserveTimeType: Number(row.reserveTimeType || 1), + reserveTimeTypeName: row.reserveTimeTypeName || (Number(row.reserveTimeType || 1) === 2 ? '全天候预约' : '分段预约'), + blocks: [], + } + this.formData = this.createDefaultFormData(row || {}) + if (this.formData.reserveType) { + await this.reserveTypeChange(this.formData.reserveType) + } + this.syncYearlyReserveEndDate(true) + await this.queryTimeLimitConfig() + this.scheduleDate = this.getInitialScheduleDate() + await this.queryAvailability() + }, + getDefaultYearlyReserveEndDate() { + const base = this.formData.reserveStartTime + ? this.$moment(this.formData.reserveStartTime) + : (this.scheduleDate ? this.$moment(this.scheduleDate, 'YYYY-MM-DD') : this.$moment()) + if (!base.isValid()) { + return this.$moment().endOf('month').format('YYYY-MM-DD') + } + return base.clone().endOf('month').format('YYYY-MM-DD') + }, + syncYearlyReserveEndDate(force = false) { + const defaultDate = this.getDefaultYearlyReserveEndDate() + if (force || !this.formData.yearlyReserveEndDate) { + this.$set(this.formData, 'yearlyReserveEndDate', defaultDate) + return + } + if (this.formData.reserveStartTime && this.formData.yearlyReserveEndDate < this.formData.reserveStartTime.slice(0, 10)) { + this.$set(this.formData, 'yearlyReserveEndDate', defaultDate) + } + }, + getInitialScheduleDate() { + let current = this.$moment().startOf('day') + for (let i = 0; i < 90; i++) { + if (!this.isDisabledDate(current.toDate())) { + return current.format('YYYY-MM-DD') + } + current = current.add(1, 'day') + } + return this.$moment().add(1, 'day').format('YYYY-MM-DD') + }, + async queryTimeLimitConfig() { + const fallback = { + filterHolidays: !!this.row.filterHolidays, + holidayList: [], + notApplyTimeList: Array.isArray(this.row.notApplyTimeList) ? this.row.notApplyTimeList : [], + } + try { + const res = await this.$axios.post('/platform/siteCug/apply/timeLimitConfig', { + siteId: this.row.id, + }) + if (res.code === 0 && res.data) { + this.timeLimitConfig = { + filterHolidays: !!res.data.filterHolidays, + holidayList: Array.isArray(res.data.holidayList) ? res.data.holidayList : [], + notApplyTimeList: Array.isArray(res.data.notApplyTimeList) ? res.data.notApplyTimeList : [], + } + } else { + this.timeLimitConfig = fallback + } + } catch (e) { + this.timeLimitConfig = fallback + } + this.pickerRefreshKey += 1 + }, + async queryAvailability(date) { + const targetDate = date || this.scheduleDate + if (!targetDate || !this.row.id) { + this.availabilityData.blocks = [] + return + } + this.availabilityLoading = true + try { + const res = await this.$axios.post('/platform/siteCug/apply/availability', { + siteId: this.row.id, + date: targetDate, + }) + if (res.code === 0 && res.data) { + this.availabilityData = { + date: res.data.date || targetDate, + reserveTimeType: Number(res.data.reserveTimeType || this.row.reserveTimeType || 1), + reserveTimeTypeName: res.data.reserveTimeTypeName || this.reserveModeText, + blocks: Array.isArray(res.data.blocks) ? res.data.blocks : [], + } + } else { + this.availabilityData = { + date: targetDate, + reserveTimeType: Number(this.row.reserveTimeType || 1), + reserveTimeTypeName: Number(this.row.reserveTimeType || 1) === 2 ? '全天候预约' : '分段预约', + blocks: [], + } + this.$message.warning((res && res.msg) || '预约时段加载失败') + } + } catch (e) { + this.availabilityData = { + date: targetDate, + reserveTimeType: Number(this.row.reserveTimeType || 1), + reserveTimeTypeName: Number(this.row.reserveTimeType || 1) === 2 ? '全天候预约' : '分段预约', + blocks: [], + } + this.$message.warning('预约时段加载失败,请稍后重试') + } finally { + this.availabilityLoading = false + this.syncSelectionFromFields() + } + }, + async onScheduleDateChange(value) { + this.scheduleDate = value || '' + this.clearSelection(true) + await this.queryAvailability(value) + }, + async reserveTypeChange(value) { + if (!this.availableReserveTypes.includes(value)) { + this.formData.reserveType = this.getDefaultReserveType() + return + } + if (value === 'union') { + this.formData.applyUnionId = this.$store.state.user?.union?.id || '' + this.formData.applyUnionName = this.$store.state.user?.union?.name || '' + this.formData.clubId = '' + this.formData.clubName = '' + } + if (value === 'club') { + this.formData.applyUnionId = '' + this.formData.applyUnionName = '' + this.formData.clubId = '' + this.formData.clubName = '' + await this.loadManagedClubs() + } + this.$nextTick(() => { + this.$refs.formRef?.clearValidate(['applyUnionName', 'clubId']) + }) + }, + async loadManagedClubs() { + try { + const res = await this.$axios.post('/platform/club/examine/apply/getClubsByRole') + this.clubOptions = Array.isArray(res.data) ? res.data : [] + this.syncManagedClubSelection() + if (!this.clubOptions.length) { + this.$message.warning('您当前没有可预约的协会管理权限') + } + } catch (e) { + this.clubOptions = [] + this.formData.clubId = '' + this.formData.clubName = '' + this.$message.warning('协会列表加载失败,请稍后重试') + } + }, + syncManagedClubSelection() { + if (!this.clubOptions.length) { + this.formData.clubId = '' + this.formData.clubName = '' + return + } + const currentClub = this.clubOptions.find(item => item.id === this.formData.clubId) + if (currentClub) { + this.formData.clubName = currentClub.clubName || '' + return + } + if (this.clubOptions.length === 1) { + this.formData.clubId = this.clubOptions[0].id + this.formData.clubName = this.clubOptions[0].clubName || '' + return + } + this.formData.clubId = '' + this.formData.clubName = '' + }, + clubChange(clubId) { + const club = this.clubOptions.find(item => item.id === clubId) + this.formData.clubName = club ? club.clubName : '' + }, + async normalizeReserveTypeData() { + if (!this.availableReserveTypes.includes(this.formData.reserveType)) { + this.formData.reserveType = this.getDefaultReserveType() + } + if (this.formData.reserveType === 'union') { + this.formData.applyUnionId = this.$store.state.user?.union?.id || '' + this.formData.applyUnionName = this.$store.state.user?.union?.name || '' + this.formData.clubId = '' + this.formData.clubName = '' + return true + } + if (this.formData.reserveType === 'club') { + if (!this.clubOptions.length) { + await this.loadManagedClubs() + } + const club = this.clubOptions.find(item => item.id === this.formData.clubId) + this.formData.clubName = club ? club.clubName : '' + this.formData.applyUnionId = '' + this.formData.applyUnionName = '' + return true + } + return false + }, + isDisabledDate(time) { + const day = this.$moment(time).format('YYYY-MM-DD') + const today = this.$moment().startOf('day') + const currentDay = this.$moment(day) + if (currentDay.isBefore(today)) { + return true + } + if (currentDay.day() === 0 || currentDay.day() === 6) { + return true + } + return this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(day) + }, + normalizeTime(timeStr) { + if (!timeStr) { + return '00:00:00' + } + return timeStr.length === 5 ? timeStr + ':00' : timeStr + }, + getDisabledRangesByDay(dayStr) { + const list = Array.isArray(this.timeLimitConfig.notApplyTimeList) ? this.timeLimitConfig.notApplyTimeList : [] + return list + .filter(item => item && item.date && this.$moment(item.date).format('YYYY-MM-DD') === dayStr) + .sort((a, b) => this.normalizeTime(a.startTime).localeCompare(this.normalizeTime(b.startTime))) + }, + buildSelectableRange(dateTimeValue) { + if (!dateTimeValue) { + return ['00:00:00 - 23:59:59'] + } + const dayStr = this.$moment(dateTimeValue).format('YYYY-MM-DD') + const disabledRanges = this.getDisabledRangesByDay(dayStr) + if (!disabledRanges.length) { + return ['00:00:00 - 23:59:59'] + } + const result = [] + let cursor = '00:00:00' + disabledRanges.forEach(item => { + const start = this.normalizeTime(item.startTime) + const end = this.normalizeTime(item.endTime) + if (cursor < start) { + result.push(cursor + ' - ' + start) + } + if (cursor < end) { + cursor = end + } + }) + if (cursor < '23:59:59') { + result.push(cursor + ' - 23:59:59') + } + return result.length ? result : ['00:00:00 - 00:00:00'] + }, + isDateTimeBlocked(dateTimeStr) { + if (!dateTimeStr) { + return false + } + const target = this.$moment(dateTimeStr) + const dayStr = target.format('YYYY-MM-DD') + if (this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(dayStr)) { + return true + } + const disabledRanges = this.getDisabledRangesByDay(dayStr) + return disabledRanges.some(item => { + const start = this.$moment(dayStr + ' ' + this.normalizeTime(item.startTime)) + const end = this.$moment(dayStr + ' ' + this.normalizeTime(item.endTime)) + return target.isSameOrAfter(start) && target.isBefore(end) + }) + }, + hasHolidayInRange(start, end) { + const current = start.clone().startOf('day') + const endDay = end.clone().startOf('day') + while (current.isSameOrBefore(endDay)) { + if (current.day() === 0 || current.day() === 6) { + return true + } + if (this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(current.format('YYYY-MM-DD'))) { + return true + } + current.add(1, 'day') + } + return false + }, + overlapsDisabledRange(start, end) { + const list = Array.isArray(this.timeLimitConfig.notApplyTimeList) ? this.timeLimitConfig.notApplyTimeList : [] + return list.some(item => { + if (!item || !item.date || !item.startTime || !item.endTime) { + return false + } + const rangeStart = this.$moment(this.$moment(item.date).format('YYYY-MM-DD') + ' ' + this.normalizeTime(item.startTime)) + const rangeEnd = this.$moment(this.$moment(item.date).format('YYYY-MM-DD') + ' ' + this.normalizeTime(item.endTime)) + return start.isBefore(rangeEnd) && end.isAfter(rangeStart) + }) + }, + validateTimeLimit(showMessage = true, startTime = this.formData.reserveStartTime, endTime = this.formData.reserveEndTime) { + if (!startTime || !endTime) { + return true + } + const start = this.$moment(startTime) + const end = this.$moment(endTime) + if (!start.isValid() || !end.isValid()) { + return false + } + if (start.format('YYYY-MM-DD') !== end.format('YYYY-MM-DD')) { + if (showMessage) { + this.$message.warning('预约时间必须在同一天内') + } + return false + } + if (this.isDateTimeBlocked(startTime) || this.isDateTimeBlocked(endTime)) { + if (showMessage) { + this.$message.warning('预约时间不能选择周末、节假日或禁用时间') + } + return false + } + if (this.hasHolidayInRange(start, end)) { + if (showMessage) { + this.$message.warning('预约时间范围内包含周末或节假日,请重新选择') + } + return false + } + if (this.overlapsDisabledRange(start, end)) { + if (showMessage) { + this.$message.warning('预约时间范围与禁用时间冲突,请重新选择') + } + return false + } + return true + }, + clearSelection(clearTimeFields = false) { + this.selectedBlockKeys = [] + if (clearTimeFields) { + this.formData.reserveStartTime = '' + this.formData.reserveEndTime = '' + } + }, + getSelectedIndices() { + return this.availabilityBlocks + .map((block, index) => this.selectedBlockKeys.includes(block.key) ? index : -1) + .filter(index => index > -1) + }, + getBlockClass(block) { + const expired = this.isExpiredAvailabilityBlock(block) + return { + 'apply-block': true, + 'apply-block--available': block.status === 'available' && !expired && !this.selectedBlockKeys.includes(block.key), + 'apply-block--reserved': block.status === 'reserved', + 'apply-block--closed': block.status === 'closed' || expired, + 'apply-block--selected': this.selectedBlockKeys.includes(block.key), + } + }, + /** + * 前端按当天当前时间再做一层兜底判断,避免预约弹窗长时间停留后, + * 之前可选的全天候时间块在开始后仍被继续点选。 + */ + isExpiredAvailabilityBlock(block) { + if (!block || !block.startDateTime) { + return false + } + const start = this.$moment(block.startDateTime) + if (!start.isValid()) { + return false + } + return start.isSame(this.$moment(), 'day') && !start.isAfter(this.$moment()) + }, + onAvailabilityBlockClick(block) { + if (!block || !block.key) { + return + } + if (this.isExpiredAvailabilityBlock(block) && !this.selectedBlockKeys.includes(block.key)) { + this.$message.warning('该时段开始时间已过,请选择其他时段') + return + } + if (block.status !== 'available' && !this.selectedBlockKeys.includes(block.key)) { + this.$message.warning(block.status === 'reserved' ? '该时段已被预约,请选择其他时段' : '该时段当前不可预约') + return + } + const index = this.availabilityBlocks.findIndex(item => item.key === block.key) + if (index < 0) { + return + } + this.toggleContinuousSelection(index) + }, + toggleContinuousSelection(index) { + const selectedIndices = this.getSelectedIndices() + if (!selectedIndices.length) { + this.applySelectionRange(index, index) + return + } + const selectedStart = selectedIndices[0] + const selectedEnd = selectedIndices[selectedIndices.length - 1] + if (selectedIndices.includes(index)) { + if (selectedIndices.length === 1) { + this.clearSelection(true) + return + } + if (index === selectedStart) { + this.applySelectionRange(selectedStart + 1, selectedEnd) + return + } + if (index === selectedEnd) { + this.applySelectionRange(selectedStart, selectedEnd - 1) + return + } + this.applySelectionRange(index, index) + return + } + const nextStart = Math.min(index, selectedStart) + const nextEnd = Math.max(index, selectedEnd) + if (!this.canSelectRange(nextStart, nextEnd)) { + this.$message.warning(this.isSegmentedMode ? '分段预约只能连续选择一个或多个时间段,不能跨间隔跳选' : '全天候预约只能连续选择一个时间段') + return + } + this.applySelectionRange(nextStart, nextEnd) + }, + canSelectRange(startIndex, endIndex) { + if (startIndex < 0 || endIndex >= this.availabilityBlocks.length || startIndex > endIndex) { + return false + } + for (let i = startIndex; i <= endIndex; i++) { + const block = this.availabilityBlocks[i] + if (!block || block.status !== 'available' || this.isExpiredAvailabilityBlock(block)) { + return false + } + if (i > startIndex) { + const prev = this.availabilityBlocks[i - 1] + if (!prev || prev.endDateTime !== block.startDateTime) { + return false + } + } + } + return true + }, + applySelectionRange(startIndex, endIndex) { + const selected = this.availabilityBlocks.slice(startIndex, endIndex + 1) + this.selectedBlockKeys = selected.map(item => item.key) + if (selected.length) { + this.formData.reserveStartTime = selected[0].startDateTime + this.formData.reserveEndTime = selected[selected.length - 1].endDateTime + this.scheduleDate = selected[0].startDateTime.slice(0, 10) + } + }, + findRangeIndicesByFields(startTime, endTime) { + if (!startTime || !endTime) { + return null + } + for (let i = 0; i < this.availabilityBlocks.length; i++) { + const block = this.availabilityBlocks[i] + if (block.status !== 'available' || block.startDateTime !== startTime) { + continue + } + let currentEnd = block.endDateTime + if (currentEnd === endTime) { + return [i, i] + } + for (let j = i + 1; j < this.availabilityBlocks.length; j++) { + const prev = this.availabilityBlocks[j - 1] + const next = this.availabilityBlocks[j] + if (next.status !== 'available' || prev.endDateTime !== next.startDateTime) { + break + } + currentEnd = next.endDateTime + if (currentEnd === endTime) { + return [i, j] + } + } + } + return null + }, + syncSelectionFromFields() { + const startTime = this.formData.reserveStartTime + const endTime = this.formData.reserveEndTime + if (!startTime || !endTime) { + this.selectedBlockKeys = [] + return + } + if (startTime.slice(0, 10) !== this.scheduleDate || endTime.slice(0, 10) !== this.scheduleDate) { + this.selectedBlockKeys = [] + return + } + const range = this.findRangeIndicesByFields(startTime, endTime) + if (!range) { + this.selectedBlockKeys = [] + return + } + this.selectedBlockKeys = this.availabilityBlocks.slice(range[0], range[1] + 1).map(item => item.key) + }, + validateGraphSelection(showMessage = true, startTime = this.formData.reserveStartTime, endTime = this.formData.reserveEndTime, targetDay = this.scheduleDate) { + if (!startTime || !endTime) { + return true + } + if (!targetDay || startTime.slice(0, 10) !== targetDay || endTime.slice(0, 10) !== targetDay) { + if (showMessage) { + this.$message.warning('预约时间必须与当前选择的预约日期一致') + } + return false + } + const range = this.findRangeIndicesByFields(startTime, endTime) + if (range) { + return true + } + if (showMessage) { + this.$message.warning(this.isSegmentedMode + ? '分段预约请按场次时间单位选择一个或多个连续时段' + : '全天候预约请按预约时间单位选择一个或多个连续时段') + } + return false + }, + async timeFieldChange(field) { + const value = this.formData[field] + if (!value) { + if (field === 'reserveStartTime') { + this.syncYearlyReserveEndDate(true) + } + this.syncSelectionFromFields() + return + } + if (this.isDateTimeBlocked(value)) { + this.$message.warning('该时间点不可预约,请重新选择') + this.$set(this.formData, field, '') + return + } + const day = this.$moment(value).format('YYYY-MM-DD') + if (day !== this.scheduleDate) { + this.scheduleDate = day + await this.queryAvailability(day) + } + if (this.formData.reserveStartTime && this.formData.reserveEndTime && !this.validateTimeLimit()) { + this.$set(this.formData, field, '') + return + } + if (field === 'reserveStartTime') { + this.syncYearlyReserveEndDate() + } + this.syncSelectionFromFields() + }, + validateYearlyReserve() { + if (!this.yearlyReserve) { + return true + } + if (!this.formData.yearlyReserveEndDate) { + this.$message.warning('请选择批量预约截止日期') + return false + } + if (this.formData.reserveStartTime && this.formData.yearlyReserveEndDate < this.formData.reserveStartTime.slice(0, 10)) { + this.$message.warning('批量预约截止日期不能早于开始日期') + return false + } + if (!this.yearlyReserveDates.length) { + this.$message.warning('当前选择的时间段在截止日期内无法生成可批量预约的日期') + return false + } + if (!this.yearlyReserveDates.length) { + this.$message.warning('当前选择的时间段无法生成本年批量预约日期') + return false + } + return true + }, + buildSubmitApi() { + return this.yearlyReserve ? '/platform/siteCug/apply/submitYearly' : '/platform/siteCug/apply/submit' + }, + buildSubmitConfirmMessage() { + const baseText = '您已选择 ' + this.formData.reserveStartTime + ' 至 ' + this.formData.reserveEndTime + if (!this.yearlyReserve) { + return baseText + ',是否确定提交预约?' + } + return baseText + '。系统将继续预约本年后续 ' + this.yearlyReserveDates.length + ' 个同星期时段,是否确定提交?' + }, + validateYearlyReserve() { + if (!this.yearlyReserve) { + return true + } + if (!this.formData.yearlyReserveEndDate) { + this.$message.warning('请选择批量预约截止日期') + return false + } + if (this.formData.reserveStartTime && this.formData.yearlyReserveEndDate < this.formData.reserveStartTime.slice(0, 10)) { + this.$message.warning('批量预约截止日期不能早于开始日期') + return false + } + if (!this.yearlyReserveDates.length) { + this.$message.warning('当前选择的时间段在截止日期内无法生成可批量预约的日期') + return false + } + return true + }, + buildSubmitConfirmMessage() { + const baseText = '您已选择 ' + this.formData.reserveStartTime + ' 至 ' + this.formData.reserveEndTime + if (!this.yearlyReserve) { + return baseText + ',是否确定提交预约?' + } + return baseText + '。系统将继续预约到 ' + this.formData.yearlyReserveEndDate + ',共 ' + this.yearlyReserveDates.length + ' 个同星期时段,是否确定提交?' + }, + batchReserveSummary() { + if (!this.yearlyReserve) { + return '' + } + if (!this.formData.yearlyReserveEndDate) { + return '请选择批量预约截止日期' + } + if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) { + return '请先选择开始和结束时间,再设置批量预约截止日期' + } + if (!this.yearlyReserveDates.length) { + return '当前截止日期内没有可批量预约的同星期时段' + } + return '将从 ' + this.yearlyReserveDates[0] + ' 开始,自动预约到 ' + this.formData.yearlyReserveEndDate + }, + buildSubmitConfirmMessage() { + const baseText = '您已选择 ' + this.formData.reserveStartTime + ' 至 ' + this.formData.reserveEndTime + if (!this.yearlyReserve) { + return baseText + ',是否确定提交预约?' + } + return baseText + '。系统将继续预约到 ' + this.formData.yearlyReserveEndDate + ',是否确定提交?' + }, + onSubmit() { + this.$refs.formRef.validate(async (valid) => { + if (!valid) return + if (!this.formData.reserveType) { + this.$message.warning('请选择预约类型') + return + } + await this.normalizeReserveTypeData() + if (this.formData.reserveType === 'union' && !this.formData.applyUnionId) { + this.$message.warning('当前用户未关联分工会,不能发起分工会预约') + return + } + if (this.formData.reserveType === 'club' && !this.formData.clubId) { + this.$message.warning('请选择您管理的协会') + return + } + if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) { + this.$message.warning('请选择预约开始和结束时间') + return + } + const start = this.$moment(this.formData.reserveStartTime) + const end = this.$moment(this.formData.reserveEndTime) + if (!start.isAfter(this.$moment())) { + this.$message.warning('预约开始时间必须晚于当前时间') + return + } + if (!end.isAfter(start)) { + this.$message.warning('预约结束时间必须晚于开始时间') + return + } + if (!this.validateTimeLimit()) { + return + } + if (!this.validateGraphSelection()) { + return + } + if (!this.validateYearlyReserve()) { + return + } + + this.$confirm(this.buildSubmitConfirmMessage(), '提示', { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + }).then(() => { + this.$axios.post(this.buildSubmitApi(), { + data: JSON.stringify(this.formData), + }).then((resp) => { + if (resp.code === 0) { + this.$message.success(resp.msg || '提交成功') + this.$emit('refresh') + } else { + this.$message.warning(resp.msg || '提交失败') + } + }).catch((err) => { + this.$message.warning((err && err.msg) || '提交失败') + }) + }).catch(() => {}) + }) + }, + }, +}; diff --git a/src/main/resources/views/platform/zhgh/dayofficework/siteCug/apply/index.html b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/apply/index.html new file mode 100644 index 0000000..e8a05db --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/apply/index.html @@ -0,0 +1,530 @@ + + +
+ + + + + + +
+ + + + + + diff --git a/src/main/resources/views/platform/zhgh/dayofficework/siteCug/common/applyInfo.js b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/common/applyInfo.js new file mode 100644 index 0000000..c549a4f --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/common/applyInfo.js @@ -0,0 +1,80 @@ +const siteCugApplyInfo = { + template: /*language=HTML*/ ` +
+
+ 申请信息 + 点击查看流程图 +
+ + {{ viewData.siteName }} + {{ viewData.applyUserName }} + {{ viewData.applyLoginName }} + {{ viewData.applyUnitName }} + {{ reserveTypeLabel }} + {{ reserveTargetName }} + {{ viewData.reserveStartTime }} + {{ viewData.reserveEndTime }} + {{ viewData.applyMobile }} + {{ viewData.joinCount }} + {{ viewData.applyCause }} + + + + +
+ `, + store, + dicts: ["PROCESS_TASK_SUBMIT_TYPE"], + data() { + return { + viewData: {}, + doneTasks: [], + row: null + } + }, + computed: { + reserveTypeLabel() { + return this.viewData.reserveType === 'club' ? '协会预约' : '分工会预约' + }, + reserveTargetName() { + return this.viewData.reserveType === 'club' ? (this.viewData.clubName || '-') : (this.viewData.applyUnionName || '-') + }, + }, + methods: { + onOpen(row) { + this.row = row + this.viewData = row + this.getDoneTasks() + }, + getDoneTasks() { + this.$axios.post('/flow/common/doneTasks', {bizId: this.row.id}).then((res) => { + if (res.code === 0) { + this.doneTasks = res.data + } + }) + }, + openChart() { + this.$refs.snakerChartRef.onOpenFull(this.row.instanceProcessDefineId, this.row.instanceId) + } + }, +} diff --git a/src/main/resources/views/platform/zhgh/dayofficework/siteCug/manage/basicForm.js b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/manage/basicForm.js new file mode 100644 index 0000000..14bd8ab --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/manage/basicForm.js @@ -0,0 +1,740 @@ +const basicForm = { + template: /*language=HTML*/ ` +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 开启 + 禁用 + + + + + + + 不限制 + 男 + 女 + + + + + + + + + + 分段预约 + 全天候预约 + + + + + + + 是 + 否 + + + + + + 点击设置禁用时间 + 您已设置{{ formData.notApplyTimeList ? formData.notApplyTimeList.length : 0 }}个禁用时间 + + + + + 场次信息 + +
+
分段预约:可设置一个或多个场次,预约人按拆分后的场次进行预约。
+ + + + + + + + + + + + + + + + +
+ +
+
全天候预约:设置可预约的开始时间、结束时间和预约时间单位(分钟),预约人按设定单位选择连续时段。
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+
+ 暂存:保存当前填写内容并留在本页继续编辑。正式提交:保存后返回列表页。 +
+ + 取消 + 暂存并继续编辑 + 正式提交 + + + +
选择日期
+ + +
设置禁用时间
+ + + + + + 一键设置开始/结束时间 + + + + + + + + + + + + + + + + + + 取消 + 确定 + +
+
+ `, + props: { + typeList: { + type: Array, + required: false, + default: [], + } + }, + store, + data() { + return { + activeTab: 'basicInfo', + segmentedOpenHoursCache: [{ startTime: '', endTime: '', timeUnit: 30 }], + fullDayOpenHourCache: { startTime: '', endTime: '', timeUnit: 30 }, + formData: { + state: true, + sexLimit: 0, + filterHolidays: false, + reserveTimeType: 1, + openHours: [{ startTime: '', endTime: '', timeUnit: 30 }], + segmentedOpenHours: [{ startTime: '', endTime: '', timeUnit: 30 }], + fullDayOpenHour: { startTime: '', endTime: '', timeUnit: 30 }, + notApplyTimeList: [], + createUserName: this.$store.state.user.username, + createUserId: this.$store.state.user.id, + }, + formRules: { + createUserName: [{required: true, message: '必填', trigger: ['blur', 'change']}], + sortNum: [{required: true, message: '必填', trigger: ['blur', 'change']}], + name: [{required: true, message: '必填', trigger: ['blur', 'change']}], + address: [{required: true, message: '必填', trigger: ['blur', 'change']}], + contactName: [{required: true, message: '必填', trigger: ['blur', 'change']}], + contactPhone: [{required: true, message: '必填', trigger: ['blur', 'change']}], + maxNum: [{required: true, message: '必填', trigger: ['blur', 'change']}], + typeId: [{required: true, message: '必填', trigger: ['blur', 'change']}], + sexLimit: [{required: true, message: '必填', trigger: ['blur', 'change']}], + filterHolidays: [{required: true, message: '必填', trigger: ['blur', 'change']}], + state: [{required: true, message: '必填', trigger: ['blur', 'change']}], + reserveTimeType: [{required: true, message: '必填', trigger: ['blur', 'change']}], + }, + setUpTimeDialog: false, + timeOneKeySet: { + startTime: "", + endTime: "" + }, + multipleSelection: [], + } + }, + computed: { + fullDayOpenHour() { + if (!Array.isArray(this.formData.openHours)) { + this.$set(this.formData, 'openHours', []) + } + if (this.formData.openHours.length === 0) { + this.formData.openHours.push({ startTime: '', endTime: '', timeUnit: 30 }) + } + if (!this.formData.openHours[0].timeUnit || this.formData.openHours[0].timeUnit <= 0) { + this.$set(this.formData.openHours[0], 'timeUnit', 30) + } + return this.formData.openHours[0] + } + }, + methods: { + normalizeTimeUnit(value, defaultValue = 30) { + const num = Number(value) + if (!Number.isFinite(num) || num <= 0) { + return defaultValue + } + return Math.round(num) + }, + syncOpenHoursCache() { + if (this.formData.reserveTimeType === 1) { + this.segmentedOpenHoursCache = Array.isArray(this.formData.openHours) && this.formData.openHours.length > 0 + ? clone(this.formData.openHours).map(item => ({ + startTime: item.startTime || '', + endTime: item.endTime || '', + timeUnit: this.normalizeTimeUnit(item.timeUnit), + })) + : [{ startTime: '', endTime: '', timeUnit: 30 }] + this.formData.segmentedOpenHours = clone(this.segmentedOpenHoursCache) + } else { + const first = (this.formData.openHours && this.formData.openHours[0]) || {} + this.fullDayOpenHourCache = { + startTime: first.startTime || '', + endTime: first.endTime || '', + timeUnit: this.normalizeTimeUnit(first.timeUnit), + } + this.formData.fullDayOpenHour = clone(this.fullDayOpenHourCache) + } + }, + buildSubmitFormData() { + this.syncOpenHoursCache() + const submitData = clone(this.formData) + submitData.segmentedOpenHours = clone(this.segmentedOpenHoursCache || []) + submitData.fullDayOpenHour = clone(this.fullDayOpenHourCache || { startTime: '', endTime: '', timeUnit: 30 }) + if (submitData.reserveTimeType === 1) { + submitData.openHours = clone(this.segmentedOpenHoursCache || []) + } else { + submitData.openHours = [clone(this.fullDayOpenHourCache || { startTime: '', endTime: '', timeUnit: 60 })] + } + return submitData + }, + initOpenHoursCache() { + const segmentedOpenHours = Array.isArray(this.formData.segmentedOpenHours) && this.formData.segmentedOpenHours.length > 0 + ? clone(this.formData.segmentedOpenHours) + : (Array.isArray(this.formData.openHours) ? clone(this.formData.openHours) : []) + const fullDayOpenHour = this.formData.fullDayOpenHour && Object.keys(this.formData.fullDayOpenHour).length > 0 + ? clone(this.formData.fullDayOpenHour) + : ((Array.isArray(this.formData.openHours) && this.formData.openHours.length > 0) ? clone(this.formData.openHours[0]) : {}) + if (this.formData.reserveTimeType === 2) { + this.fullDayOpenHourCache = { + startTime: fullDayOpenHour.startTime || '', + endTime: fullDayOpenHour.endTime || '', + timeUnit: this.normalizeTimeUnit(fullDayOpenHour.timeUnit), + } + this.segmentedOpenHoursCache = segmentedOpenHours && segmentedOpenHours.length > 0 + ? segmentedOpenHours.map(item => ({ + startTime: item.startTime || '', + endTime: item.endTime || '', + timeUnit: this.normalizeTimeUnit(item.timeUnit), + })) + : [{ startTime: '', endTime: '', timeUnit: 30 }] + this.formData.openHours = [clone(this.fullDayOpenHourCache)] + } else { + this.segmentedOpenHoursCache = segmentedOpenHours.length > 0 + ? segmentedOpenHours.map(item => ({ + startTime: item.startTime || '', + endTime: item.endTime || '', + timeUnit: this.normalizeTimeUnit(item.timeUnit), + })) + : [{ startTime: '', endTime: '', timeUnit: 30 }] + const first = this.segmentedOpenHoursCache[0] || {} + this.fullDayOpenHourCache = { + startTime: fullDayOpenHour.startTime || first.startTime || '', + endTime: fullDayOpenHour.endTime || first.endTime || '', + timeUnit: this.normalizeTimeUnit(fullDayOpenHour.timeUnit || first.timeUnit), + } + this.formData.openHours = clone(this.segmentedOpenHoursCache) + } + this.formData.segmentedOpenHours = clone(this.segmentedOpenHoursCache) + this.formData.fullDayOpenHour = clone(this.fullDayOpenHourCache) + }, + addOpenHour() { + if (!Array.isArray(this.formData.openHours)) { + this.$set(this.formData, 'openHours', []) + } + this.formData.openHours.push({ startTime: '', endTime: '', timeUnit: 30 }) + this.segmentedOpenHoursCache = clone(this.formData.openHours) + }, + hasSegmentedOpenHoursOverlap(openHours) { + const validHours = (openHours || []) + .map((item, index) => ({ ...item, index })) + .filter(item => item.startTime && item.endTime && item.startTime < item.endTime) + .sort((a, b) => a.startTime.localeCompare(b.startTime)) + for (let i = 1; i < validHours.length; i++) { + const prev = validHours[i - 1] + const current = validHours[i] + if (current.startTime < prev.endTime) { + return { + overlap: true, + prevIndex: prev.index, + currentIndex: current.index, + } + } + } + return { overlap: false } + }, + checkSegmentedOpenHoursOverlap() { + if (this.formData.reserveTimeType !== 1) { + return false + } + const result = this.hasSegmentedOpenHoursOverlap(this.formData.openHours) + if (result.overlap) { + this.$message.warning('第' + (result.prevIndex + 1) + '条和第' + (result.currentIndex + 1) + '条场次时间有重叠,请调整后再保存') + return true + } + return false + }, + handleReserveTimeTypeChange(val) { + if (!Array.isArray(this.formData.openHours)) { + this.$set(this.formData, 'openHours', []) + } + if (val === 1) { + const currentFullDay = this.formData.openHours[0] || this.fullDayOpenHourCache || {} + this.fullDayOpenHourCache = { + startTime: currentFullDay.startTime || '', + endTime: currentFullDay.endTime || '', + timeUnit: this.normalizeTimeUnit(currentFullDay.timeUnit), + } + if (!Array.isArray(this.segmentedOpenHoursCache) || this.segmentedOpenHoursCache.length === 0) { + this.segmentedOpenHoursCache = [{ + startTime: this.fullDayOpenHourCache.startTime || '', + endTime: this.fullDayOpenHourCache.endTime || '', + timeUnit: this.normalizeTimeUnit(this.fullDayOpenHourCache.timeUnit), + }] + } + this.formData.openHours = clone(this.segmentedOpenHoursCache) + } else if (val === 2) { + this.segmentedOpenHoursCache = Array.isArray(this.formData.openHours) && this.formData.openHours.length > 0 + ? clone(this.formData.openHours) + : this.segmentedOpenHoursCache + const first = this.fullDayOpenHourCache && (this.fullDayOpenHourCache.startTime || this.fullDayOpenHourCache.endTime) + ? this.fullDayOpenHourCache + : (this.segmentedOpenHoursCache[0] || {}) + this.fullDayOpenHourCache = { + startTime: first.startTime || '', + endTime: first.endTime || '', + timeUnit: this.normalizeTimeUnit(first.timeUnit || this.fullDayOpenHourCache.timeUnit), + } + this.formData.openHours = [clone(this.fullDayOpenHourCache)] + } + }, + validateOpenHours() { + const openHours = this.formData.openHours || [] + if (this.formData.reserveTimeType === 1) { + if (openHours.length === 0) { + this.$message.warning('请至少添加一条场次信息') + return false + } + for (let i = 0; i < openHours.length; i++) { + const item = openHours[i] || {} + if (!item.startTime) { + this.$message.warning('第' + (i + 1) + '条场次信息中,开始时间必填') + return false + } + if (!item.endTime) { + this.$message.warning('第' + (i + 1) + '条场次信息中,结束时间必填') + return false + } + if (item.startTime >= item.endTime) { + this.$message.warning('第' + (i + 1) + '条场次信息中,开始时间必须早于结束时间') + return false + } + if (!item.timeUnit || item.timeUnit <= 0) { + this.$message.warning('第' + (i + 1) + '条场次信息中,预约时间单位必须大于0分钟') + return false + } + } + const overlapResult = this.hasSegmentedOpenHoursOverlap(openHours) + if (overlapResult.overlap) { + this.$message.warning('第' + (overlapResult.prevIndex + 1) + '条和第' + (overlapResult.currentIndex + 1) + '条场次时间有重叠,请调整后再保存') + return false + } + } else { + const item = openHours[0] || {} + if (!item.startTime || !item.endTime) { + this.$message.warning('请设置全天候预约的开始时间和结束时间') + return false + } + if (item.startTime >= item.endTime) { + this.$message.warning('全天候预约的开始时间必须早于结束时间') + return false + } + if (!item.timeUnit || item.timeUnit <= 0) { + this.$message.warning('全天候预约的预约时间单位必须大于0分钟') + return false + } + } + return true + }, + doConfirmSetUpCourse() { + const timeList = this.formData.notApplyTimeList + if (timeList && timeList.length > 0) { + const valid = timeList.every(v => v.date && v.startTime && v.endTime && (v.startTime < v.endTime)) + if (!valid) { + this.$message.warning("时间不完整或者有误") + return + } + } + this.setUpTimeDialog = false + }, + removeSetUpTableRow(row, index) { + this.formData.notApplyTimeList.splice(index, 1) + const dateArray = this.formData.notApplyTimeList.map(v => this.$moment(v.date).format("YYYY-MM-DD")) + const setUpDate = this.formData.setUpDate || [] + this.formData.setUpDate = setUpDate.filter(v => { + return dateArray.includes(this.$moment(v).format("YYYY-MM-DD")) + }) + }, + handleSelectionChange(val) { + this.multipleSelection = val + }, + oneKeySetStartEndTime() { + if (this.multipleSelection.length === 0) { + this.$message.warning('请在下面表格多选框中选择需要一键设置的时间') + return + } + const { startTime, endTime } = this.timeOneKeySet + this.formData.notApplyTimeList.forEach(v => { + const selected = this.multipleSelection.find(o => o.date === v.date && o.startTime === v.startTime && o.endTime === v.endTime) + if (selected) { + this.$set(v, "startTime", startTime) + this.$set(v, "endTime", endTime) + } + }) + this.$forceUpdate() + }, + setUpDateChange(val) { + if (!val) { + this.formData.notApplyTimeList = [] + return + } + if (this.formData.notApplyTimeList === undefined) { + this.$set(this.formData, "notApplyTimeList", []) + } + const dateSet = new Set(this.formData.notApplyTimeList.map(v => this.$moment(v.date).format("YYYY-MM-DD"))) + val.forEach(v => { + if (!dateSet.has(v)) { + this.formData.notApplyTimeList.push({ + date: v, startTime: "", endTime: "" + }) + } + }) + this.formData.notApplyTimeList = this.formData.notApplyTimeList.filter(v => { + return val.includes(this.$moment(v.date).format("YYYY-MM-DD")) + }) + this.formData.notApplyTimeList.sort((a, b) => { + return Date.parse(a.date) - Date.parse(b.date) + }) + }, + openSetUpTime() { + if (this.formData.notApplyTimeList) { + this.$set(this.formData, 'setUpDate', this.formData.notApplyTimeList.map(o => o.date)) + } + this.activeTab = 'siteSetting' + this.setUpTimeDialog = true + }, + onOpen(row) { + this.activeTab = 'basicInfo' + if (row && row.id) { + this.formData = clone(row) + if (!Array.isArray(this.formData.notApplyTimeList)) { + this.$set(this.formData, 'notApplyTimeList', []) + } + if (!this.formData.reserveTimeType) { + this.$set(this.formData, 'reserveTimeType', 1) + } + if (!Array.isArray(this.formData.openHours) || this.formData.openHours.length === 0) { + this.$set(this.formData, 'openHours', this.formData.reserveTimeType === 2 ? [{ startTime: '', endTime: '', timeUnit: 30 }] : [{ startTime: '', endTime: '', timeUnit: 30 }]) + } + if (!Array.isArray(this.formData.segmentedOpenHours) || this.formData.segmentedOpenHours.length === 0) { + this.$set(this.formData, 'segmentedOpenHours', [{ startTime: '', endTime: '', timeUnit: 30 }]) + } + if (!this.formData.fullDayOpenHour) { + this.$set(this.formData, 'fullDayOpenHour', { startTime: '', endTime: '', timeUnit: 30 }) + } + this.initOpenHoursCache() + } else { + this.formData = { + state: true, + sexLimit: 0, + filterHolidays: false, + reserveTimeType: 1, + openHours: [{ startTime: '', endTime: '', timeUnit: 30 }], + segmentedOpenHours: [{ startTime: '', endTime: '', timeUnit: 30 }], + fullDayOpenHour: { startTime: '', endTime: '', timeUnit: 30 }, + notApplyTimeList: [], + createUserName: this.$store.state.user.username, + createUserId: this.$store.state.user.id, + } + this.segmentedOpenHoursCache = [{ startTime: '', endTime: '', timeUnit: 30 }] + this.fullDayOpenHourCache = { startTime: '', endTime: '', timeUnit: 30 } + } + }, + async doSave(stayOnPage) { + if (!this.validateOpenHours()) { + this.activeTab = 'siteSetting' + return + } + const submitData = this.buildSubmitFormData() + const resp = await this.$axios.post("/platform/siteCug/manage/submit", {data: JSON.stringify(submitData)}) + if (resp.code === 0) { + this.$message.success(stayOnPage ? '暂存成功,您可以继续编辑当前内容' : '正式提交成功,已返回列表页') + if (resp.data) { + this.formData = clone(resp.data) + if (!Array.isArray(this.formData.notApplyTimeList)) { + this.$set(this.formData, 'notApplyTimeList', []) + } + if (!this.formData.reserveTimeType) { + this.$set(this.formData, 'reserveTimeType', 1) + } + if (!Array.isArray(this.formData.openHours) || this.formData.openHours.length === 0) { + this.$set(this.formData, 'openHours', this.formData.reserveTimeType === 2 ? [{ startTime: '', endTime: '', timeUnit: 30 }] : [{ startTime: '', endTime: '', timeUnit: 30 }]) + } + if (!Array.isArray(this.formData.segmentedOpenHours) || this.formData.segmentedOpenHours.length === 0) { + this.$set(this.formData, 'segmentedOpenHours', [{ startTime: '', endTime: '', timeUnit: 30 }]) + } + if (!this.formData.fullDayOpenHour) { + this.$set(this.formData, 'fullDayOpenHour', { startTime: '', endTime: '', timeUnit: 30 }) + } + this.initOpenHoursCache() + } + if (!stayOnPage) { + this.$emit('refresh') + } + } else { + this.$message.warning(resp.msg) + } + }, + onTempSave() { + this.$refs.formRef.validate(async (valid) => { + if (valid) { + this.$confirm("确认暂存当前内容,并继续留在本页编辑吗?", "暂存确认", { + confirmButtonText: "确认暂存", + cancelButtonText: "取消", + type: "warning" + }).then(async () => { + await this.doSave(true) + }).catch(() => {}) + } + }) + }, + onSubmit() { + this.$refs.formRef.validate((valid) => { + if (valid) { + this.$confirm("确认正式提交当前内容吗?提交成功后将返回列表页。", "正式提交确认", { + confirmButtonText: "确认提交", + cancelButtonText: "取消", + type: "primary" + }).then(async () => { + await this.doSave(false) + }) + } + }) + }, + }, + style: /*language=CSS*/ ` + + ` +}; diff --git a/src/main/resources/views/platform/zhgh/dayofficework/siteCug/manage/index.html b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/manage/index.html new file mode 100644 index 0000000..ce35356 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/manage/index.html @@ -0,0 +1,184 @@ + + + + +
+ + + + + + +
+ + diff --git a/src/main/resources/views/platform/zhgh/dayofficework/siteCug/manage/info.js b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/manage/info.js new file mode 100644 index 0000000..208f6c7 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/manage/info.js @@ -0,0 +1,107 @@ +const siteInfo = { + template: /*language=HTML*/ ` +
+ + + + {{ viewData.createUserName }} + {{ viewData.sortNum }} + {{ viewData.name }} + {{ viewData.address }} + {{ viewData.contactName }} + {{ viewData.contactPhone }} + {{ viewData.maxNum }} + {{ viewData.typeName }} + +
+
暂无场地介绍
+
+ + + +
暂无场地照片
+
+
+
+ + + + + 开启 + 禁用 + + + 不限制 + 男 + 女 + -- + + + 是 + 否 + + + 分段预约 + 全天候预约 + -- + + + + + + + + 暂无禁用时间 + + +
+ + + + + + + 暂无场次信息 +
+
+
+
开始时间:{{ viewData.openHours[0].startTime || '--' }}
+
结束时间:{{ viewData.openHours[0].endTime || '--' }}
+
预约时间单位(分钟):{{ viewData.openHours[0].timeUnit || '--' }}
+
+ 暂无场次信息 +
+ 暂无场次信息 +
+
+
+
+
+ `, + data() { + return { + activeTab: 'basicInfo', + viewData: {}, + } + }, + methods: { + onOpen(row) { + this.activeTab = 'basicInfo' + this.viewData = row + }, + }, + style: /*language=CSS*/ ` + .el-descriptions-item__label { + width: 200px; + min-width: 200px; + max-width: 200px; + } + ` +}; diff --git a/src/main/resources/views/platform/zhgh/dayofficework/siteCug/mine/index.html b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/mine/index.html new file mode 100644 index 0000000..223fc47 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/mine/index.html @@ -0,0 +1,250 @@ + + + +
+ + + + + +
+ + + + diff --git a/src/main/resources/views/platform/zhgh/dayofficework/siteCug/record/index.html b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/record/index.html new file mode 100644 index 0000000..2ca98d3 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/record/index.html @@ -0,0 +1,317 @@ + + + +
+ + + + + +
+ + + + diff --git a/src/main/resources/views/platform/zhgh/dayofficework/siteCug/schoolUnionAudit/index.html b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/schoolUnionAudit/index.html new file mode 100644 index 0000000..0e3bfa8 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/schoolUnionAudit/index.html @@ -0,0 +1,368 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + 已审核 + 未审核 + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + diff --git a/src/main/resources/views/platform/zhgh/dayofficework/siteCug/type/basicForm.js b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/type/basicForm.js new file mode 100644 index 0000000..07040d9 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/type/basicForm.js @@ -0,0 +1,68 @@ +const basicForm = { + template: /*language=HTML*/ ` +
+ + + + + + + + + + + + + + 取消 + 提交 + +
+ `, + data() { + return { + formData: { + enable: true + }, + formRules: { + name: [{required: true, message: '必填', trigger: ['blur', 'change']}], + code: [{required: true, message: '必填', trigger: ['blur', 'change']}], + enable: [{required: true, message: '必填', trigger: ['blur', 'change']}], + }, + } + }, + methods: { + onOpen(row) { + if (row && row.id) { + this.formData = clone(row) + } + }, + onSubmit() { + this.$refs.formRef.validate((valid) => { + if (valid) { + this.$confirm("您确定要提交吗?", "提示", { + confirmButtonText: "确定", + cancelButtonText: "取消", + type: "warning" + }).then(async () => { + const resp = await this.$axios.post("/platform/siteCug/function/type/submit", this.formData) + if (resp.code === 0) { + this.$message.success(resp.msg) + this.$emit('refresh') + } else { + this.$message.warning(resp.msg) + } + }) + } + }) + }, + }, + style: /*language=CSS*/ ` + + ` +} diff --git a/src/main/resources/views/platform/zhgh/dayofficework/siteCug/type/index.html b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/type/index.html new file mode 100644 index 0000000..dff17b7 --- /dev/null +++ b/src/main/resources/views/platform/zhgh/dayofficework/siteCug/type/index.html @@ -0,0 +1,139 @@ + + + + +
+ + + + + +
+ + diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/apply/form/apply.js b/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/apply/form/apply.js new file mode 100644 index 0000000..9771985 --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/apply/form/apply.js @@ -0,0 +1,979 @@ +new Vue({ + el: '#app', + store, + data() { + return { + noticeCollapsed: true, + siteId: GetQueryString('siteId'), + row: {}, + siteLoaded: false, + clubOptions: [], + yearlyReserve: false, + scheduleDate: '', + availabilityLoading: false, + activeTimeGroupKey: '', + selectedBlockKeys: [], + timeLimitConfig: { + filterHolidays: false, + holidayList: [], + notApplyTimeList: [], + }, + availabilityData: { + date: '', + reserveTimeType: 1, + reserveTimeTypeName: '分段预约', + blocks: [], + }, + formData: { + siteId: '', + applyUserId: '', + applyUserName: '', + applySex: '', + applyLoginName: '', + applyUnitId: '', + applyUnitName: '', + reserveType: '', + applyUnionId: '', + applyUnionName: '', + clubId: '', + clubName: '', + applyMobile: '', + reserveStartTime: '', + reserveEndTime: '', + yearlyReserveEndDate: '', + joinCount: 1, + applyCause: '', + }, + showReserveTypePicker: false, + showClubPicker: false, + showScheduleDatePicker: false, + showAvailabilityPopup: false, + showYearlyEndDatePicker: false, + schedulePickerDate: new Date(), + yearlyReservePickerDate: new Date(), + } + }, + computed: { + canApplyUnion() { + return this.$auth.hasPermission('siteCug.apply.union') + }, + canApplyClub() { + return this.$auth.hasPermission('siteCug.apply.club') + }, + availableReserveTypes() { + const result = [] + if (this.canApplyUnion) { + result.push('union') + } + if (this.canApplyClub) { + result.push('club') + } + return result + }, + availableReserveTypeOptions() { + const result = [] + if (this.canApplyUnion) { + result.push({ value: 'union', label: '分工会预约' }) + } + if (this.canApplyClub) { + result.push({ value: 'club', label: '协会预约' }) + } + return result + }, + reserveTypeColumns() { + return this.availableReserveTypeOptions.map(item => item.label) + }, + reserveTypeText() { + const current = this.availableReserveTypeOptions.find(item => item.value === this.formData.reserveType) + return current ? current.label : '' + }, + clubColumns() { + return this.clubOptions.map(item => item.clubName) + }, + availabilityBlocks() { + return Array.isArray(this.availabilityData.blocks) ? this.availabilityData.blocks : [] + }, + isSegmentedMode() { + return Number(this.availabilityData.reserveTimeType || this.row.reserveTimeType || 1) === 1 + }, + reserveModeText() { + return this.availabilityData.reserveTimeTypeName || (this.isSegmentedMode ? '分段预约' : '全天候预约') + }, + timeGroupTabs() { + const result = [] + const used = {} + this.availabilityBlocks.forEach((block) => { + const key = this.getBlockGroupKey(block) + if (!key || used[key]) { + return + } + used[key] = true + result.push({ + key: key, + label: this.getBlockGroupLabel(block), + }) + }) + return result + }, + displayedAvailabilityBlocks() { + if (!this.activeTimeGroupKey) { + return this.availabilityBlocks + } + return this.availabilityBlocks.filter(block => this.getBlockGroupKey(block) === this.activeTimeGroupKey) + }, + selectionSummary() { + if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) { + return '' + } + return this.formData.reserveStartTime + ' 至 ' + this.formData.reserveEndTime + }, + scheduleMinDate() { + return this.$moment().startOf('day').toDate() + }, + scheduleMaxDate() { + return this.$moment().add(365, 'day').endOf('day').toDate() + }, + yearlyReserveMinDate() { + const base = this.formData.reserveStartTime + ? this.$moment(this.formData.reserveStartTime) + : (this.scheduleDate ? this.$moment(this.scheduleDate, 'YYYY-MM-DD') : this.$moment()) + return base.startOf('day').toDate() + }, + yearlyReserveMaxDate() { + return this.$moment().add(2, 'year').endOf('year').toDate() + }, + yearlyReserveDates() { + if (!this.yearlyReserve || !this.formData.reserveStartTime || !this.formData.reserveEndTime || !this.formData.yearlyReserveEndDate) { + return [] + } + const start = this.$moment(this.formData.reserveStartTime) + const end = this.$moment(this.formData.reserveEndTime) + const reserveEndDate = this.$moment(this.formData.yearlyReserveEndDate, 'YYYY-MM-DD') + if (!start.isValid() || !end.isValid() || !end.isAfter(start) || !reserveEndDate.isValid() || reserveEndDate.isBefore(start, 'day')) { + return [] + } + const result = [] + const current = start.clone().startOf('day') + const reserveEndDay = reserveEndDate.clone().startOf('day') + const targetWeekDay = start.day() + const startClock = start.format('HH:mm:ss') + const endClock = end.format('HH:mm:ss') + while (current.isSameOrBefore(reserveEndDay, 'day')) { + if (current.day() === targetWeekDay) { + const day = current.format('YYYY-MM-DD') + const startTime = day + ' ' + startClock + const endTime = day + ' ' + endClock + if (this.validateTimeLimit(false, false, startTime, endTime)) { + result.push(day) + } + } + current.add(1, 'day') + } + return result + }, + batchReserveSummary() { + if (!this.yearlyReserve) { + return '' + } + if (!this.formData.yearlyReserveEndDate) { + return '请选择批量预约截止日期' + } + if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) { + return '请先选择预约时间段,再设置批量预约截止日期' + } + if (!this.yearlyReserveDates.length) { + return '当前截止日期内没有可批量预约的同星期时段' + } + return '将从 ' + this.yearlyReserveDates[0] + ' 开始,自动预约到 ' + this.formData.yearlyReserveEndDate + }, + }, + watch: { + yearlyReserve(val) { + if (val) { + this.syncYearlyReserveEndDate(true) + } + }, + }, + methods: { + historyBack, + getDefaultReserveType() { + return this.availableReserveTypes[0] || '' + }, + buildDefaultFormData() { + const user = this.$store.state.user || {} + const reserveType = this.getDefaultReserveType() + return { + siteId: this.row.id || '', + applyUserId: user.id || '', + applyUserName: user.username || '', + applySex: user.sex || '', + applyLoginName: user.loginname || '', + applyUnitId: user.unit ? user.unit.id : '', + applyUnitName: user.unit ? user.unit.name : '', + reserveType: reserveType, + applyUnionId: reserveType === 'union' && user.union ? user.union.id : '', + applyUnionName: reserveType === 'union' && user.union ? user.union.name : '', + clubId: '', + clubName: '', + applyMobile: user.mobile || '', + reserveStartTime: '', + reserveEndTime: '', + yearlyReserveEndDate: '', + joinCount: 1, + applyCause: '', + } + }, + isBlockedDay(momentValue) { + return this.isDisabledDate(momentValue.toDate()) + }, + getInitialScheduleDate() { + let current = this.$moment().startOf('day') + for (let i = 0; i < 90; i++) { + if (!this.isBlockedDay(current)) { + return current.format('YYYY-MM-DD') + } + current = current.add(1, 'day') + } + return this.$moment().add(1, 'day').format('YYYY-MM-DD') + }, + getDefaultYearlyReserveEndDate() { + const base = this.formData.reserveStartTime + ? this.$moment(this.formData.reserveStartTime) + : (this.scheduleDate ? this.$moment(this.scheduleDate, 'YYYY-MM-DD') : this.$moment()) + if (!base.isValid()) { + return this.$moment().endOf('month').format('YYYY-MM-DD') + } + return base.clone().endOf('month').format('YYYY-MM-DD') + }, + syncYearlyReserveEndDate(force = false) { + const defaultDate = this.getDefaultYearlyReserveEndDate() + if (force || !this.formData.yearlyReserveEndDate) { + this.$set(this.formData, 'yearlyReserveEndDate', defaultDate) + return + } + if (this.formData.reserveStartTime && this.formData.yearlyReserveEndDate < this.formData.reserveStartTime.slice(0, 10)) { + this.$set(this.formData, 'yearlyReserveEndDate', defaultDate) + } + }, + openReserveTypePicker() { + if (!this.reserveTypeColumns.length) { + this.$toast('当前没有可用的预约类型') + return + } + this.showReserveTypePicker = true + }, + async onReserveTypeConfirm(value, index) { + this.showReserveTypePicker = false + const option = this.availableReserveTypeOptions[index] + if (!option) { + return + } + this.$set(this.formData, 'reserveType', option.value) + await this.reserveTypeChange(option.value) + }, + async reserveTypeChange(value) { + const user = this.$store.state.user || {} + if (!this.availableReserveTypes.includes(value)) { + this.formData.reserveType = this.getDefaultReserveType() + return + } + if (value === 'union') { + this.formData.applyUnionId = user.union ? user.union.id : '' + this.formData.applyUnionName = user.union ? user.union.name : '' + this.formData.clubId = '' + this.formData.clubName = '' + return + } + if (value === 'club') { + this.formData.applyUnionId = '' + this.formData.applyUnionName = '' + this.formData.clubId = '' + this.formData.clubName = '' + await this.loadManagedClubs() + } + }, + async loadManagedClubs() { + try { + const res = await this.$axios.post('/platform/club/examine/apply/getClubsByRole') + this.clubOptions = Array.isArray(res.data) ? res.data : [] + this.syncManagedClubSelection() + if (!this.clubOptions.length) { + this.$toast('您当前没有可预约的协会管理权限') + } + } catch (e) { + this.clubOptions = [] + this.formData.clubId = '' + this.formData.clubName = '' + this.$toast('协会列表加载失败,请稍后重试') + } + }, + syncManagedClubSelection() { + if (!this.clubOptions.length) { + this.formData.clubId = '' + this.formData.clubName = '' + return + } + const currentClub = this.clubOptions.find(item => item.id === this.formData.clubId) + if (currentClub) { + this.formData.clubName = currentClub.clubName || '' + return + } + if (this.clubOptions.length === 1) { + this.formData.clubId = this.clubOptions[0].id + this.formData.clubName = this.clubOptions[0].clubName || '' + return + } + this.formData.clubId = '' + this.formData.clubName = '' + }, + async openClubPicker() { + if (this.formData.reserveType !== 'club') { + this.$toast('请先选择协会预约') + return + } + if (!this.clubOptions.length) { + await this.loadManagedClubs() + } + if (!this.clubOptions.length) { + return + } + if (this.clubOptions.length === 1 && this.formData.clubId) { + return + } + this.showClubPicker = true + }, + onClubConfirm(value, index) { + this.showClubPicker = false + const club = this.clubOptions[index] + this.formData.clubId = club ? club.id : '' + this.formData.clubName = club ? club.clubName : value + }, + openScheduleDatePicker() { + const target = this.scheduleDate + ? this.$moment(this.scheduleDate, 'YYYY-MM-DD') + : this.$moment(this.getInitialScheduleDate(), 'YYYY-MM-DD') + this.schedulePickerDate = target.isValid() ? target.toDate() : new Date() + this.showScheduleDatePicker = true + }, + async onScheduleDateConfirm(value) { + this.showScheduleDatePicker = false + const target = this.$moment(value) + if (!target.isValid()) { + this.$toast('请选择正确的预约日期') + return + } + if (this.isBlockedDay(target)) { + this.$toast('该日期不可预约,请重新选择') + return + } + const day = target.format('YYYY-MM-DD') + this.schedulePickerDate = target.toDate() + this.scheduleDate = day + this.clearSelection(true) + await this.queryAvailability(day) + this.showAvailabilityPopup = true + }, + async openAvailabilityPopup() { + if (!this.scheduleDate) { + this.$toast('请先选择预约日期') + this.openScheduleDatePicker() + return + } + if (!this.availabilityData.date || this.availabilityData.date !== this.scheduleDate) { + await this.queryAvailability(this.scheduleDate) + } + this.showAvailabilityPopup = true + }, + async queryAvailability(date = this.scheduleDate) { + const targetDate = date || this.scheduleDate + if (!targetDate || !this.row.id) { + this.availabilityData = { + date: targetDate || '', + reserveTimeType: Number(this.row.reserveTimeType || 1), + reserveTimeTypeName: Number(this.row.reserveTimeType || 1) === 2 ? '全天候预约' : '分段预约', + blocks: [], + } + this.resetActiveTimeGroup() + return + } + this.availabilityLoading = true + try { + const res = await this.$axios.post('/platform/siteCug/apply/availability', { + siteId: this.row.id, + date: targetDate, + }) + if (res.code === 0 && res.data) { + this.availabilityData = { + date: res.data.date || targetDate, + reserveTimeType: Number(res.data.reserveTimeType || this.row.reserveTimeType || 1), + reserveTimeTypeName: res.data.reserveTimeTypeName || (Number(res.data.reserveTimeType || this.row.reserveTimeType || 1) === 2 ? '全天候预约' : '分段预约'), + blocks: Array.isArray(res.data.blocks) ? res.data.blocks : [], + } + } else { + this.availabilityData = { + date: targetDate, + reserveTimeType: Number(this.row.reserveTimeType || 1), + reserveTimeTypeName: Number(this.row.reserveTimeType || 1) === 2 ? '全天候预约' : '分段预约', + blocks: [], + } + this.$toast((res && res.msg) || '预约时段加载失败') + } + } catch (e) { + this.availabilityData = { + date: targetDate, + reserveTimeType: Number(this.row.reserveTimeType || 1), + reserveTimeTypeName: Number(this.row.reserveTimeType || 1) === 2 ? '全天候预约' : '分段预约', + blocks: [], + } + this.$toast('预约时段加载失败,请稍后重试') + } finally { + this.availabilityLoading = false + this.resetActiveTimeGroup() + this.syncSelectionFromFields() + } + }, + resetActiveTimeGroup() { + const selectedBlock = this.availabilityBlocks.find(block => this.selectedBlockKeys.includes(block.key)) + if (selectedBlock) { + this.activeTimeGroupKey = this.getBlockGroupKey(selectedBlock) + return + } + this.activeTimeGroupKey = this.timeGroupTabs.length ? this.timeGroupTabs[0].key : '' + }, + getBlockGroupKey(block) { + if (!block) { + return '' + } + if (this.isSegmentedMode) { + const groupIndex = block.groupIndex === undefined || block.groupIndex === null ? 0 : block.groupIndex + return 'group_' + groupIndex + } + const hour = this.$moment(block.startDateTime).hour() + return hour < 12 ? 'am' : 'pm' + }, + getBlockGroupLabel(block) { + if (!block) { + return '' + } + if (this.isSegmentedMode) { + const groupLabel = block.groupLabel || '场次' + return groupLabel.split(' / ')[0] + } + const hour = this.$moment(block.startDateTime).hour() + return hour < 12 ? '上午' : '下午' + }, + getSelectedIndices() { + return this.availabilityBlocks + .map((block, index) => this.selectedBlockKeys.includes(block.key) ? index : -1) + .filter(index => index > -1) + }, + getBlockClass(block) { + const result = ['slot-block'] + if (this.selectedBlockKeys.includes(block.key)) { + result.push('slot-block--selected') + return result + } + if (block.status === 'available') { + result.push('slot-block--available') + return result + } + if (block.status === 'reserved') { + result.push('slot-block--reserved') + return result + } + result.push('slot-block--closed') + return result + }, + onAvailabilityBlockClick(block) { + if (!block || !block.key) { + return + } + if (block.status !== 'available' && !this.selectedBlockKeys.includes(block.key)) { + this.$toast(block.status === 'reserved' ? '该时段已被预约,请选择其他时段' : '该时段当前不可预约') + return + } + const index = this.availabilityBlocks.findIndex(item => item.key === block.key) + if (index < 0) { + return + } + const selectedIndices = this.getSelectedIndices() + if (selectedIndices.length) { + const currentGroupKey = this.getBlockGroupKey(this.availabilityBlocks[selectedIndices[0]]) + const targetGroupKey = this.getBlockGroupKey(block) + if (currentGroupKey && targetGroupKey && currentGroupKey !== targetGroupKey) { + this.applySelectionRange(index, index) + return + } + } + this.toggleContinuousSelection(index) + }, + toggleContinuousSelection(index) { + const selectedIndices = this.getSelectedIndices() + if (!selectedIndices.length) { + this.applySelectionRange(index, index) + return + } + const selectedStart = selectedIndices[0] + const selectedEnd = selectedIndices[selectedIndices.length - 1] + if (selectedIndices.includes(index)) { + if (selectedIndices.length === 1) { + this.clearSelection(true) + return + } + if (index === selectedStart) { + this.applySelectionRange(selectedStart + 1, selectedEnd) + return + } + if (index === selectedEnd) { + this.applySelectionRange(selectedStart, selectedEnd - 1) + return + } + this.applySelectionRange(index, index) + return + } + const nextStart = Math.min(index, selectedStart) + const nextEnd = Math.max(index, selectedEnd) + if (!this.canSelectRange(nextStart, nextEnd)) { + this.$toast(this.isSegmentedMode + ? '分段预约只能连续选择一个或多个时段,不能跨间隔选择' + : '全天候预约只能连续选择一个或多个时段,不能跨间隔选择') + return + } + this.applySelectionRange(nextStart, nextEnd) + }, + canSelectRange(startIndex, endIndex) { + if (startIndex < 0 || endIndex >= this.availabilityBlocks.length || startIndex > endIndex) { + return false + } + const firstBlock = this.availabilityBlocks[startIndex] + const groupKey = this.getBlockGroupKey(firstBlock) + for (let i = startIndex; i <= endIndex; i++) { + const block = this.availabilityBlocks[i] + if (!block || block.status !== 'available') { + return false + } + if (this.getBlockGroupKey(block) !== groupKey) { + return false + } + if (i > startIndex) { + const prev = this.availabilityBlocks[i - 1] + if (!prev || prev.endDateTime !== block.startDateTime) { + return false + } + } + } + return true + }, + applySelectionRange(startIndex, endIndex) { + const selected = this.availabilityBlocks.slice(startIndex, endIndex + 1) + this.selectedBlockKeys = selected.map(item => item.key) + if (!selected.length) { + return + } + this.formData.reserveStartTime = selected[0].startDateTime + this.formData.reserveEndTime = selected[selected.length - 1].endDateTime + this.scheduleDate = selected[0].startDateTime.slice(0, 10) + this.schedulePickerDate = this.$moment(this.scheduleDate, 'YYYY-MM-DD').toDate() + this.activeTimeGroupKey = this.getBlockGroupKey(selected[0]) + this.syncYearlyReserveEndDate() + }, + clearSelection(clearTimeFields = false) { + this.selectedBlockKeys = [] + if (clearTimeFields) { + this.formData.reserveStartTime = '' + this.formData.reserveEndTime = '' + } + }, + confirmAvailabilitySelection() { + if (!this.formData.reserveStartTime || !this.formData.reserveEndTime || !this.selectedBlockKeys.length) { + this.$toast('请选择一个或多个连续时段') + return + } + if (!this.validateGraphSelection()) { + return + } + this.showAvailabilityPopup = false + }, + findRangeIndicesByFields(startTime, endTime) { + if (!startTime || !endTime) { + return null + } + for (let i = 0; i < this.availabilityBlocks.length; i++) { + const block = this.availabilityBlocks[i] + if (block.status !== 'available' || block.startDateTime !== startTime) { + continue + } + let currentEnd = block.endDateTime + if (currentEnd === endTime) { + return [i, i] + } + for (let j = i + 1; j < this.availabilityBlocks.length; j++) { + const prev = this.availabilityBlocks[j - 1] + const next = this.availabilityBlocks[j] + if (next.status !== 'available' || prev.endDateTime !== next.startDateTime) { + break + } + if (this.getBlockGroupKey(prev) !== this.getBlockGroupKey(next)) { + break + } + currentEnd = next.endDateTime + if (currentEnd === endTime) { + return [i, j] + } + } + } + return null + }, + syncSelectionFromFields() { + const startTime = this.formData.reserveStartTime + const endTime = this.formData.reserveEndTime + if (!startTime || !endTime) { + this.selectedBlockKeys = [] + this.resetActiveTimeGroup() + return + } + if (startTime.slice(0, 10) !== this.scheduleDate || endTime.slice(0, 10) !== this.scheduleDate) { + this.selectedBlockKeys = [] + this.resetActiveTimeGroup() + return + } + const range = this.findRangeIndicesByFields(startTime, endTime) + if (!range) { + this.selectedBlockKeys = [] + this.resetActiveTimeGroup() + return + } + const selected = this.availabilityBlocks.slice(range[0], range[1] + 1) + this.selectedBlockKeys = selected.map(item => item.key) + if (selected.length) { + this.activeTimeGroupKey = this.getBlockGroupKey(selected[0]) + } + }, + validateGraphSelection(showMessage = true, startTime = this.formData.reserveStartTime, endTime = this.formData.reserveEndTime, targetDay = this.scheduleDate) { + if (!startTime || !endTime) { + return true + } + if (!targetDay || startTime.slice(0, 10) !== targetDay || endTime.slice(0, 10) !== targetDay) { + if (showMessage) { + this.$toast('预约时间必须与当前选择的预约日期一致') + } + return false + } + const range = this.findRangeIndicesByFields(startTime, endTime) + if (range) { + return true + } + if (showMessage) { + this.$toast(this.isSegmentedMode + ? '分段预约请按场次标签选择一个或多个连续时段' + : '全天候预约请按上午或下午标签选择一个或多个连续时段') + } + return false + }, + async normalizeReserveTypeData() { + const user = this.$store.state.user || {} + if (!this.availableReserveTypes.includes(this.formData.reserveType)) { + this.formData.reserveType = this.getDefaultReserveType() + } + if (this.formData.reserveType === 'union') { + this.formData.applyUnionId = user.union ? user.union.id : '' + this.formData.applyUnionName = user.union ? user.union.name : '' + this.formData.clubId = '' + this.formData.clubName = '' + return true + } + if (this.formData.reserveType === 'club') { + if (!this.clubOptions.length) { + await this.loadManagedClubs() + } + const club = this.clubOptions.find(item => item.id === this.formData.clubId) + this.formData.clubName = club ? club.clubName : '' + this.formData.applyUnionId = '' + this.formData.applyUnionName = '' + return true + } + return false + }, + normalizeTime(timeStr) { + if (!timeStr) { + return '00:00:00' + } + return timeStr.length === 5 ? timeStr + ':00' : timeStr + }, + isDisabledDate(time) { + const day = this.$moment(time).format('YYYY-MM-DD') + const today = this.$moment().startOf('day') + const currentDay = this.$moment(day) + if (currentDay.isBefore(today)) { + return true + } + if (currentDay.day() === 0 || currentDay.day() === 6) { + return true + } + return this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(day) + }, + getDisabledRangesByDay(dayStr) { + const list = Array.isArray(this.timeLimitConfig.notApplyTimeList) ? this.timeLimitConfig.notApplyTimeList : [] + return list + .filter(item => item && item.date && this.$moment(item.date).format('YYYY-MM-DD') === dayStr) + .sort((a, b) => this.normalizeTime(a.startTime).localeCompare(this.normalizeTime(b.startTime))) + }, + isDateTimeBlocked(dateTimeStr) { + if (!dateTimeStr) { + return false + } + const target = this.$moment(dateTimeStr) + const dayStr = target.format('YYYY-MM-DD') + if (target.day() === 0 || target.day() === 6) { + return true + } + if (this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(dayStr)) { + return true + } + const disabledRanges = this.getDisabledRangesByDay(dayStr) + return disabledRanges.some(item => { + const start = this.$moment(dayStr + ' ' + this.normalizeTime(item.startTime)) + const end = this.$moment(dayStr + ' ' + this.normalizeTime(item.endTime)) + return target.isSameOrAfter(start) && target.isBefore(end) + }) + }, + hasHolidayInRange(start, end) { + const current = start.clone().startOf('day') + const endDay = end.clone().startOf('day') + while (current.isSameOrBefore(endDay)) { + if (current.day() === 0 || current.day() === 6) { + return true + } + if (this.timeLimitConfig.filterHolidays && this.timeLimitConfig.holidayList.includes(current.format('YYYY-MM-DD'))) { + return true + } + current.add(1, 'day') + } + return false + }, + overlapsDisabledRange(start, end) { + const list = Array.isArray(this.timeLimitConfig.notApplyTimeList) ? this.timeLimitConfig.notApplyTimeList : [] + return list.some(item => { + if (!item || !item.date || !item.startTime || !item.endTime) { + return false + } + const rangeStart = this.$moment(this.$moment(item.date).format('YYYY-MM-DD') + ' ' + this.normalizeTime(item.startTime)) + const rangeEnd = this.$moment(this.$moment(item.date).format('YYYY-MM-DD') + ' ' + this.normalizeTime(item.endTime)) + return start.isBefore(rangeEnd) && end.isAfter(rangeStart) + }) + }, + showSubmitError(message) { + return this.$dialog.alert({ + title: '提示', + message: message || '提交失败', + }).catch(() => {}) + }, + validateTimeLimit(showMessage = true, useDialog = false, reserveStartTime = this.formData.reserveStartTime, reserveEndTime = this.formData.reserveEndTime) { + if (!reserveStartTime || !reserveEndTime) { + return true + } + const start = this.$moment(reserveStartTime) + const end = this.$moment(reserveEndTime) + const showError = (message) => { + if (!showMessage) { + return + } + if (useDialog) { + this.showSubmitError(message) + return + } + this.$toast(message) + } + if (!start.isValid() || !end.isValid()) { + showError('请选择正确的预约时间') + return false + } + if (!end.isAfter(start)) { + showError('预约结束时间必须晚于开始时间') + return false + } + if (start.format('YYYY-MM-DD') !== end.format('YYYY-MM-DD')) { + showError('预约时间必须在同一天内') + return false + } + if (this.isDateTimeBlocked(reserveStartTime) || this.isDateTimeBlocked(reserveEndTime)) { + showError('预约时间不能选择周末、节假日或禁用时间') + return false + } + if (this.hasHolidayInRange(start, end)) { + showError('预约时间范围内包含周末或节假日,请重新选择') + return false + } + if (this.overlapsDisabledRange(start, end)) { + showError('预约时间范围与禁用时间冲突,请重新选择') + return false + } + return true + }, + openYearlyEndDatePicker() { + if (!this.yearlyReserve) { + return + } + this.syncYearlyReserveEndDate() + const current = this.formData.yearlyReserveEndDate + ? this.$moment(this.formData.yearlyReserveEndDate, 'YYYY-MM-DD') + : this.$moment(this.getDefaultYearlyReserveEndDate(), 'YYYY-MM-DD') + this.yearlyReservePickerDate = current.isValid() ? current.toDate() : new Date() + this.showYearlyEndDatePicker = true + }, + onYearlyReserveEndDateConfirm(value) { + this.showYearlyEndDatePicker = false + this.yearlyReservePickerDate = value + this.$set(this.formData, 'yearlyReserveEndDate', this.$moment(value).format('YYYY-MM-DD')) + }, + validateYearlyReserve() { + if (!this.yearlyReserve) { + return true + } + if (!this.formData.yearlyReserveEndDate) { + this.$toast('请选择批量预约截止日期') + return false + } + if (this.formData.reserveStartTime && this.formData.yearlyReserveEndDate < this.formData.reserveStartTime.slice(0, 10)) { + this.$toast('批量预约截止日期不能早于开始日期') + return false + } + if (!this.yearlyReserveDates.length) { + this.$toast('当前截止日期内没有可批量预约的同星期时段') + return false + } + return true + }, + buildSubmitApi() { + return this.yearlyReserve ? '/platform/siteCug/apply/submitYearly' : '/platform/siteCug/apply/submit' + }, + buildSubmitConfirmMessage() { + const baseText = '您已选择 ' + this.formData.reserveStartTime + ' 至 ' + this.formData.reserveEndTime + if (!this.yearlyReserve) { + return baseText + ',是否确定选择并提交预约?' + } + return baseText + '。系统将继续预约到 ' + this.formData.yearlyReserveEndDate + ',是否确定选择并提交预约?' + }, + async onSubmit() { + this.$refs.formRef.validate().then(async () => { + if (!this.formData.reserveType) { + await this.showSubmitError('请选择预约类型') + return + } + await this.normalizeReserveTypeData() + if (this.formData.reserveType === 'union' && !this.formData.applyUnionId) { + await this.showSubmitError('当前用户未关联分工会,不能发起分工会预约') + return + } + if (this.formData.reserveType === 'club' && !this.formData.clubId) { + await this.showSubmitError('请选择您管理的协会') + return + } + if (!this.formData.reserveStartTime || !this.formData.reserveEndTime) { + await this.showSubmitError('请选择预约时间段') + return + } + const start = this.$moment(this.formData.reserveStartTime) + const end = this.$moment(this.formData.reserveEndTime) + if (!start.isAfter(this.$moment())) { + await this.showSubmitError('预约开始时间必须晚于当前时间') + return + } + if (!end.isAfter(start)) { + await this.showSubmitError('预约结束时间必须晚于开始时间') + return + } + if (!this.validateGraphSelection()) { + return + } + if (!this.validateTimeLimit(true, true)) { + return + } + if (!this.validateYearlyReserve()) { + return + } + this.$dialog.confirm({ + title: '提示', + message: this.buildSubmitConfirmMessage(), + }).then(() => { + const loading = this.$toast.loading({ + message: '提交中...', + forbidClick: true, + overlay: true, + duration: 0, + }) + this.$axios.post(this.buildSubmitApi(), { + data: JSON.stringify(this.formData), + }).then((res) => { + if (res.code === 0) { + this.$toast.success(res.msg || '提交成功') + this.$pjaxReplace('/platform/siteCug/apply/h5') + } else { + this.showSubmitError(res.msg || '提交失败') + } + }).catch((err) => { + this.showSubmitError((err && err.msg) || '提交失败') + }).finally(() => { + loading.close() + }) + }).catch(() => {}) + }).catch(() => {}) + }, + async queryTimeLimitConfig() { + const fallback = { + filterHolidays: !!this.row.filterHolidays, + holidayList: [], + notApplyTimeList: Array.isArray(this.row.notApplyTimeList) ? this.row.notApplyTimeList : [], + } + try { + const res = await this.$axios.post('/platform/siteCug/apply/timeLimitConfig', { + siteId: this.row.id, + }) + if (res.code === 0 && res.data) { + this.timeLimitConfig = { + filterHolidays: !!res.data.filterHolidays, + holidayList: Array.isArray(res.data.holidayList) ? res.data.holidayList : [], + notApplyTimeList: Array.isArray(res.data.notApplyTimeList) ? res.data.notApplyTimeList : [], + } + return + } + } catch (e) { + } + this.timeLimitConfig = fallback + }, + async init() { + if (!this.siteId) { + this.$toast('未获取到场地信息') + this.historyBack() + return + } + let siteRes + try { + siteRes = await this.$axios.post('/platform/siteCug/manage/info', { id: this.siteId }) + } catch (e) { + this.$toast('场地信息加载失败') + this.historyBack() + return + } + if (siteRes.code !== 0 || !siteRes.data) { + this.$toast(siteRes.msg || '场地信息加载失败') + this.historyBack() + return + } + this.row = siteRes.data + this.formData = this.buildDefaultFormData() + this.syncYearlyReserveEndDate(true) + await this.queryTimeLimitConfig() + if (this.formData.reserveType) { + await this.reserveTypeChange(this.formData.reserveType) + } + this.scheduleDate = this.getInitialScheduleDate() + this.schedulePickerDate = this.$moment(this.scheduleDate, 'YYYY-MM-DD').toDate() + await this.queryAvailability(this.scheduleDate) + this.siteLoaded = true + }, + }, + created() { + this.init() + } +}) diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/apply/form/index.html b/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/apply/form/index.html new file mode 100644 index 0000000..aed48e9 --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/apply/form/index.html @@ -0,0 +1,583 @@ + + + + +
+ + +
+
+ +
+
+
+
可预约日期
+
仅限工作日
+
+
+
节假日限制
+
{{ timeLimitConfig.filterHolidays ? '节假日不可预约' : '不排除节假日' }}
+
+
+
禁用时段
+
+
+
+ {{ item.date }} {{ item.startTime }} - {{ item.endTime }} +
+
+
暂无禁用时段
+
+
+
+
+
+ + +
+ + + + + + + + + + + + + + + +
当前已选:{{ selectionSummary }}
+ + + +
+
将自动预约截止日期内每周同一时段,默认到本月底。
+ + +
{{ batchReserveSummary }}
+
+ +
+
+
+
+ + + + + + + + + + + + + + + + + +
+
+
选择预约时段
+
{{ scheduleDate || '请选择预约日期' }} {{ reserveModeText }}
+
+ +
+
+ +
+
+ 可预约 + 已预约 + 已选择 + 不可预约 +
+
+ 时段加载中 + + +
+ +
+
+ +
+ + + + + +
+ + + + diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/apply/index.html b/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/apply/index.html new file mode 100644 index 0000000..bcd0f54 --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/apply/index.html @@ -0,0 +1,474 @@ + + + + +
+ + + +
+
+
+ +
+ + +
+
+
已选场地类型:{{ selectedTypeText }}
+ +
+
+
+ + + + + + + + +
选择场地类型
+
+ +
+
+ + +
+ + + + diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/common/applyInfo.js b/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/common/applyInfo.js new file mode 100644 index 0000000..0098bdd --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/common/applyInfo.js @@ -0,0 +1,80 @@ +const siteCugApplyInfoH5 = { + template: + /*language=HTML*/ + ` + +
+ + {{ viewData.siteName }} + {{ viewData.applyUserName }} + {{ viewData.applyLoginName }} + {{ viewData.applyUnitName }} + {{ reserveTypeLabel }} + {{ reserveTargetName }} + {{ viewData.reserveStartTime }} + {{ viewData.reserveEndTime }} + {{ viewData.applyMobile }} + {{ viewData.applyCause }} + + +
+ +
+ `, + dicts: ['PROCESS_TASK_SUBMIT_TYPE'], + data() { + return { + visible: false, + viewData: {}, + doneTasks: [], + row: null, + } + }, + computed: { + reserveTypeLabel() { + return this.viewData.reserveType === 'club' ? '协会预约' : '分工会预约' + }, + reserveTargetName() { + return this.viewData.reserveType === 'club' ? (this.viewData.clubName || '-') : (this.viewData.applyUnionName || '-') + }, + sheetTitle() { + return this.visible && this.$slots.default ? '审核预约' : '预约详情' + }, + }, + methods: { + onOpen(row) { + this.row = row + this.visible = true + this.viewData = row + this.getDoneTasks() + }, + onClose() { + this.visible = false + }, + getDoneTasks() { + this.$axios.post('/flow/common/doneTasks', { bizId: this.row.id }).then((res) => { + if (res.code === 0) { + this.doneTasks = res.data + } + }) + }, + }, +}; diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/common/siteInfo.js b/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/common/siteInfo.js new file mode 100644 index 0000000..d24c209 --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/common/siteInfo.js @@ -0,0 +1,142 @@ +const siteInfo = { + template: /*language=HTML*/ ` +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
日期开始时间结束时间
{{ item.date }}{{ item.startTime }}{{ item.endTime }}
+
+
+
+
+ `, + data() { + return { + viewData: {}, + visible: false, + } + }, + methods: { + onOpen(row) { + this.viewData = row + this.visible = true + }, + }, + style: /*language=CSS*/ ` + /deep/ .van-action-sheet { + border-top-left-radius: 24px; + border-top-right-radius: 24px; + overflow: hidden; + background: #f8fafc; + } + /deep/ .van-action-sheet__header { + font-size: 17px; + font-weight: 700; + color: #0f172a; + background: #ffffff; + } + /deep/ .detail-container { + padding: 10px 12px 18px; + background: #f8fafc; + } + /deep/ .detail-container .van-cell-group { + margin-bottom: 12px; + overflow: hidden; + border-radius: 18px; + box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06); + } + /deep/ .detail-container .van-cell-group__title { + padding: 12px 16px 8px; + color: #334155; + font-size: 14px; + font-weight: 700; + } + /deep/ .detail-container .van-cell { + min-height: 52px; + } + /deep/ .direction-column-cell .van-cell__value { + white-space: normal; + text-align: left; + } + /deep/ .table-class { + width: 100%; + border-radius: 10px; + overflow: hidden; + line-height: 1.8rem; + font-size: 13px; + table-layout: fixed; + border-collapse: collapse; + background: #ffffff; + } + /deep/ .table-class th { + padding: 10px 8px; + background-color: #f1f5f9; + border: 1px solid #dbe4ee; + color: #334155; + font-weight: 600; + } + /deep/ .table-class tr { + text-align: center; + border-bottom: 1px solid #dbe4ee; + } + /deep/ .table-class td { + padding: 10px 8px; + border: 1px solid #dbe4ee; + color: #475569; + } + ` +} diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/mine/index.html b/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/mine/index.html new file mode 100644 index 0000000..3b8e7ec --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/mine/index.html @@ -0,0 +1,148 @@ + + + + +
+ + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/schoolUnionAudit/index.html b/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/schoolUnionAudit/index.html new file mode 100644 index 0000000..7da6993 --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/dayofficework/siteCug/schoolUnionAudit/index.html @@ -0,0 +1,335 @@ + + + + +
+ + + + + + + + + + + + + + + + +
+ + + + +
+ + +
+
{{formData.taskName}}
+ + + +
+ 退回 + 不同意 + 同意 +
+
+
+
+ + + + 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 0000000..ae38113 --- /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 0000000..5a5ffd0 --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/dayofficework/tour/schoolUnionApproval/index.html @@ -0,0 +1,478 @@ + + + + +
+ + + + + + + + + + + + + +
+ + + + +
+ + +
+
{{ 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 0000000..f7ee33d --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/apply.html @@ -0,0 +1,556 @@ + + + + +
+ + +
+ 加载中... + + +
+ +
+ 提交报名 +
+ + + + + + + + + + + + +
+ + + + 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 0000000..2d46c85 --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/index.html @@ -0,0 +1,210 @@ + + + + + + + + + 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 0000000..558e69c --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/lineInfo.html @@ -0,0 +1,559 @@ + + + + +
+ + +
+ 加载中... + +
+ + +
+ + + + 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 0000000..7ab47c0 --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/dayofficework/tour/signup/list.html @@ -0,0 +1,787 @@ + + + + +
+ + +
+
+ + + + +
+
+
+ 省内线路 +
+
省内线路
+
+
+
+ 省外线路 +
+
省外线路
+
+
+
+ 直系亲属 +
+
直系亲属
+
+
+
+
+ +
+ +
+
+
+ +
暂无图片
+
+
+
+
{{ 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 0000000..9bad022 --- /dev/null +++ b/src/main/resources/views/platform/zhghh5/dayofficework/tour/unionApproval/index.html @@ -0,0 +1,453 @@ + + + + +
+ + + + + + + + + + + + +
+ + + + +
+ + +
+
{{ formData.taskName || '审核' }}
+ + + + +
+ 退回 + 拒绝 + 同意 +
+
+
+
+ + + +