diff --git a/src/main/java/com/yunhai/workleave/controller/api/LeaveController.java b/src/main/java/com/yunhai/workleave/controller/api/LeaveController.java index f9c7b26..540a783 100644 --- a/src/main/java/com/yunhai/workleave/controller/api/LeaveController.java +++ b/src/main/java/com/yunhai/workleave/controller/api/LeaveController.java @@ -25,6 +25,7 @@ import org.springframework.web.bind.annotation.RestController; import javax.validation.Valid; import java.util.List; +import java.util.Map; /** * 请假申请域接口(leave/):申请 / 列表 / 详情 / 撤回 / 销假 / 待办 / 审核 / 统计。 @@ -46,6 +47,12 @@ public class LeaveController { return leaveApplyService.list(schId(), userId(), query); } + /** 我的请假页签数量(与列表的关键词、日期筛选口径一致) */ + @GetMapping("StatusCounts") + public Map statusCounts(LeaveQuery query) { + return leaveApplyService.statusCounts(schId(), userId(), query); + } + /** 请假详情(消息跳转落地页/详情弹窗) */ @GetMapping("Detail") public LeaveDetailVo detail(@RequestParam("leaveId") Integer leaveId) { @@ -116,10 +123,10 @@ public class LeaveController { // ============================ 余额 / 统计 ============================ - /** 表单页余额提示(可用 - 在途) */ + /** 表单页余额提示(可用 - 在途);编辑表单传 leaveId 排除自身在途占用 */ @GetMapping("MyTime") - public UserTimeSumVo myTime() { - return leaveApplyService.myTime(schId(), userId()); + public UserTimeSumVo myTime(@RequestParam(value = "leaveId", required = false) Integer leaveId) { + return leaveApplyService.myTime(schId(), userId(), leaveId); } /** 统计分析(区间按申请时间,口径:已通过) */ diff --git a/src/main/java/com/yunhai/workleave/flow/ILeaveFlowService.java b/src/main/java/com/yunhai/workleave/flow/ILeaveFlowService.java index ffdbb37..4a715b2 100644 --- a/src/main/java/com/yunhai/workleave/flow/ILeaveFlowService.java +++ b/src/main/java/com/yunhai/workleave/flow/ILeaveFlowService.java @@ -38,7 +38,7 @@ public interface ILeaveFlowService { /** 撤回(仅提交人本人 + 尚无任务被办理) */ void withdraw(Integer schId, Long instanceId, Integer submitterId); - /** 我的待办(bizType 固定 leave) */ + /** 我的待办(bizType 固定 work-leave) */ FlowTodoTaskPage todoTasks(Integer schId, Integer assigneeId, Integer page, Integer size, String bizType); /** 同意任务 */ diff --git a/src/main/java/com/yunhai/workleave/flow/LeaveFlowService.java b/src/main/java/com/yunhai/workleave/flow/LeaveFlowService.java index 29ee1a5..34aa0e5 100644 --- a/src/main/java/com/yunhai/workleave/flow/LeaveFlowService.java +++ b/src/main/java/com/yunhai/workleave/flow/LeaveFlowService.java @@ -40,7 +40,7 @@ public class LeaveFlowService implements ILeaveFlowService { private static final Logger log = LoggerFactory.getLogger(LeaveFlowService.class); /** 本应用全局唯一业务类型(手册 §3.2 红线:= app_code,不可带点分档) */ - public static final String BIZ_TYPE = "leave"; + public static final String BIZ_TYPE = "work-leave"; /** 流程流转中单据标题截断长度(快照/标题只放摘要) */ private static final int TITLE_MAX = 60; diff --git a/src/main/java/com/yunhai/workleave/repository/LeaveApplyRepository.java b/src/main/java/com/yunhai/workleave/repository/LeaveApplyRepository.java index 0bb738b..07a9feb 100644 --- a/src/main/java/com/yunhai/workleave/repository/LeaveApplyRepository.java +++ b/src/main/java/com/yunhai/workleave/repository/LeaveApplyRepository.java @@ -3,6 +3,8 @@ package com.yunhai.workleave.repository; import com.yunhai.workleave.entity.LeaveApply; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import java.util.List; import java.util.Optional; @@ -25,4 +27,20 @@ public interface LeaveApplyRepository extends JpaRepository /** 某类型被引用过(字典删除前置校验) */ boolean existsBySchIdAndLeaveType(Integer schId, String leaveType); + + /** + * 统计当前用户各请假状态的数量。 + * 日期参数与列表保持“区间重叠”口径,keyword 与列表搜索字段一致。 + */ + @Query("select l.status, count(l.id) from LeaveApply l " + + "where l.schId = :schId and l.userId = :userId " + + "and (:keyword is null or l.userName like :keyword or l.reason like :keyword or l.leaveTypeName like :keyword) " + + "and (:startTime is null or l.endTime >= :startTime) " + + "and (:endTime is null or l.startTime <= :endTime) " + + "group by l.status") + List countStatusByUser(@Param("schId") Integer schId, + @Param("userId") Integer userId, + @Param("keyword") String keyword, + @Param("startTime") String startTime, + @Param("endTime") String endTime); } diff --git a/src/main/java/com/yunhai/workleave/service/ILeaveApplyService.java b/src/main/java/com/yunhai/workleave/service/ILeaveApplyService.java index a106f22..768cee0 100644 --- a/src/main/java/com/yunhai/workleave/service/ILeaveApplyService.java +++ b/src/main/java/com/yunhai/workleave/service/ILeaveApplyService.java @@ -12,6 +12,7 @@ import com.yunhai.workleave.model.leave.TodoVo; import com.yunhai.workleave.model.leave.UserTimeSumVo; import java.util.List; +import java.util.Map; /** * 请假申请域服务(申请 / 列表 / 详情 / 销假 / 撤回 / 审核 / 统计)。 @@ -21,6 +22,9 @@ public interface ILeaveApplyService { /** 列表:scope=user 我的 / all 全部 / cc 抄送我的 */ BasePageResult list(Integer schId, Integer currentUserId, LeaveQuery query); + /** 我的请假:按状态统计当前筛选条件下的单据数 */ + Map statusCounts(Integer schId, Integer currentUserId, LeaveQuery query); + /** 详情 */ LeaveDetailVo detail(Integer schId, Integer currentUserId, Integer leaveId); @@ -50,8 +54,8 @@ public interface ILeaveApplyService { /** 审核动作(同意/拒绝,提交流程中心,本地状态待回调) */ void audit(Integer schId, Integer currentUserId, String userName, LeaveAuditRequest request); - /** 表单页余额提示 */ - UserTimeSumVo myTime(Integer schId, Integer currentUserId); + /** 表单页余额提示(可用 - 在途);编辑表单传 leaveId 排除自身在途占用 */ + UserTimeSumVo myTime(Integer schId, Integer currentUserId, Integer excludeLeaveId); /** 统计(区间按申请时间,口径:已通过) */ LeaveStatsVo statistics(Integer schId, String startTime, String endTime); diff --git a/src/main/java/com/yunhai/workleave/service/ILeaveUserTimeService.java b/src/main/java/com/yunhai/workleave/service/ILeaveUserTimeService.java index 058bbdf..af8872f 100644 --- a/src/main/java/com/yunhai/workleave/service/ILeaveUserTimeService.java +++ b/src/main/java/com/yunhai/workleave/service/ILeaveUserTimeService.java @@ -56,6 +56,9 @@ public interface ILeaveUserTimeService { /** 某教师审批中的在途占用时长(小时) */ double inTransitHoursOf(Integer schId, Integer userId); + /** 在途占用时长(小时),排除指定单据——编辑重提时排除自身,否则自身占用会顶掉自己 */ + double inTransitHoursOf(Integer schId, Integer userId, Integer excludeLeaveId); + /** 时长管控类型集合(dictKey);未启用管控返回空集 */ Set managedTypes(Integer schId); @@ -65,6 +68,9 @@ public interface ILeaveUserTimeService { /** 余额校验:在途 + 本次 <= 可用,否则抛业务异常并附可申请时长 */ void checkEnough(Integer schId, Integer userId, String leaveType, double hours); + /** 余额校验(编辑重提):在途占用先排除指定单据自身,再判断 本次 <= 可用 - 其他在途 */ + void checkEnough(Integer schId, Integer userId, String leaveType, double hours, Integer excludeLeaveId); + /** 扣减(审批通过):写流水"扣除";非管控类型静默跳过 */ void deduct(Integer schId, Integer userId, String userName, Integer leaveId, String leaveType, double hours, Integer operatorId); diff --git a/src/main/java/com/yunhai/workleave/service/impl/LeaveApplyService.java b/src/main/java/com/yunhai/workleave/service/impl/LeaveApplyService.java index d1ce2f9..a1b5c84 100644 --- a/src/main/java/com/yunhai/workleave/service/impl/LeaveApplyService.java +++ b/src/main/java/com/yunhai/workleave/service/impl/LeaveApplyService.java @@ -14,6 +14,7 @@ import com.yunhai.workleave.entity.LeaveTimeSet; import com.yunhai.workleave.entity.UpFile; import com.yunhai.workleave.enums.LeaveStatusEnum; import com.yunhai.workleave.flow.ILeaveFlowService; +import com.yunhai.workleave.flow.LeaveFlowService; import com.yunhai.workleave.service.ILeaveFlowBindService; import com.yunhai.workleave.model.BasePageResult; import com.yunhai.workleave.model.leave.FlowProgressVo; @@ -188,6 +189,34 @@ public class LeaveApplyService implements ILeaveApplyService { page.getSize(), page.getTotalPages(), page.hasNext()); } + @Override + public Map statusCounts(Integer schId, Integer currentUserId, LeaveQuery query) { + LeaveQuery q = query == null ? new LeaveQuery() : query; + String keyword = StringUtils.hasText(q.getKeyword()) ? "%" + q.getKeyword().trim() + "%" : null; + String startTime = StringUtils.hasText(q.getStartTime()) ? dayStart(q.getStartTime()) : null; + String endTime = StringUtils.hasText(q.getEndTime()) ? dayEnd(q.getEndTime()) : null; + + Map counts = new LinkedHashMap<>(); + counts.put("all", 0); + for (LeaveStatusEnum status : LeaveStatusEnum.values()) { + counts.put(String.valueOf(status.getId()), 0); + } + + int total = 0; + for (Object[] row : leaveApplyRepository.countStatusByUser( + schId, currentUserId, keyword, startTime, endTime)) { + if (row[0] == null || row[1] == null) { + continue; + } + String status = String.valueOf(((Number) row[0]).intValue()); + int count = ((Number) row[1]).intValue(); + counts.put(status, count); + total += count; + } + counts.put("all", total); + return counts; + } + @Override public LeaveDetailVo detail(Integer schId, Integer currentUserId, Integer leaveId) { LeaveApply leave = requireLeave(schId, leaveId); @@ -408,7 +437,8 @@ public class LeaveApplyService implements ILeaveApplyService { @Override public BasePageResult todoList(Integer schId, Integer currentUserId, String keyword, String startTime, String endTime, int pageIndex, int pageSize) { - FlowTodoTaskPage page = flowService.todoTasks(schId, currentUserId, pageIndex, pageSize, "leave"); + FlowTodoTaskPage page = flowService.todoTasks( + schId, currentUserId, pageIndex, pageSize, LeaveFlowService.BIZ_TYPE); List tasks = flowService.contentOf(page); if (tasks.isEmpty()) { int total = page == null ? 0 : page.getTotal(); @@ -549,10 +579,10 @@ public class LeaveApplyService implements ILeaveApplyService { // ============================ 余额 / 统计 ============================ @Override - public UserTimeSumVo myTime(Integer schId, Integer currentUserId) { + public UserTimeSumVo myTime(Integer schId, Integer currentUserId, Integer excludeLeaveId) { UserTimeSumVo vo = new UserTimeSumVo(); vo.setRemainHours(round2(userTimeService.usableOf(schId, currentUserId) - - userTimeService.inTransitHoursOf(schId, currentUserId))); + - userTimeService.inTransitHoursOf(schId, currentUserId, excludeLeaveId))); vo.setAddHours(round2(userTimeService.usableOf(schId, currentUserId))); return vo; } @@ -671,9 +701,9 @@ public class LeaveApplyService implements ILeaveApplyService { } } - // 时长余额(管控类型) + // 时长余额(管控类型);编辑重提时排除单据自身(原单在途占用未实际扣减,应只校验可用 - 其他在途) userTimeService.checkEnough(leave.getSchId(), leave.getUserId(), leave.getLeaveType(), - leave.getTotalHours() == null ? 0D : leave.getTotalHours()); + leave.getTotalHours() == null ? 0D : leave.getTotalHours(), leave.getId()); } /** 发起流程并回写实例信息 */ @@ -855,7 +885,8 @@ public class LeaveApplyService implements ILeaveApplyService { private Integer resolveLeaveIdByTask(Integer schId, Integer operatorId, Long taskId) { // 审核后按 taskId 反查单据:从该操作人的待办缓存里命中(任务刚办结仍可能在返回列表中) try { - FlowTodoTaskPage page = flowService.todoTasks(schId, operatorId, 1, 200, "leave"); + FlowTodoTaskPage page = flowService.todoTasks( + schId, operatorId, 1, 200, LeaveFlowService.BIZ_TYPE); for (FlowTodoTask task : flowService.contentOf(page)) { if (taskId.equals(task.getTaskId())) { Integer leaveId = parseLeaveId(task.getBizId()); diff --git a/src/main/java/com/yunhai/workleave/service/impl/LeaveUserTimeService.java b/src/main/java/com/yunhai/workleave/service/impl/LeaveUserTimeService.java index 8a737a9..b0e04cf 100644 --- a/src/main/java/com/yunhai/workleave/service/impl/LeaveUserTimeService.java +++ b/src/main/java/com/yunhai/workleave/service/impl/LeaveUserTimeService.java @@ -419,11 +419,17 @@ public class LeaveUserTimeService implements ILeaveUserTimeService { @Override public double inTransitHoursOf(Integer schId, Integer userId) { + return inTransitHoursOf(schId, userId, null); + } + + @Override + public double inTransitHoursOf(Integer schId, Integer userId, Integer excludeLeaveId) { List approving = leaveApplyRepository.findBySchIdAndUserIdAndStatusIn(schId, userId, Collections.singletonList(LeaveStatusEnum.APPROVING.getId())); Set managed = managedTypes(schId); return approving.stream() .filter(l -> managed.contains(l.getLeaveType())) + .filter(l -> !l.getId().equals(excludeLeaveId)) .mapToDouble(l -> l.getTotalHours() == null ? 0D : l.getTotalHours()) .sum(); } @@ -447,11 +453,16 @@ public class LeaveUserTimeService implements ILeaveUserTimeService { @Override public void checkEnough(Integer schId, Integer userId, String leaveType, double hours) { + checkEnough(schId, userId, leaveType, hours, null); + } + + @Override + public void checkEnough(Integer schId, Integer userId, String leaveType, double hours, Integer excludeLeaveId) { if (!isManaged(schId, leaveType)) { return; } double usable = usableOf(schId, userId); - double inTransit = inTransitHoursOf(schId, userId); + double inTransit = inTransitHoursOf(schId, userId, excludeLeaveId); double remain = round2(usable - inTransit); if (round2(hours) > remain + 0.0001) { throw new BizException(ErrorCode.FORBIDDEN, diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c887b3c..c4f6fc9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -45,7 +45,7 @@ spring: # 短信模块配置 sms: enabled: true - module-id: workleave + module-id: work-leave # 单点登录 / 应用间签名鉴权(基础平台 yh-auth / yh-app-auth) yh: @@ -61,6 +61,7 @@ yh: # 本应用自身身份(客户端签名用) app-key: 100020 app-secret: 99b976315ec84dd1b7e4eb761584a2eb + trust-registry: true # 提供方强制验签(两级模型,对齐 noticeservice):/oapi/** 无签名一律 401(RequireAppSignFilter), # 有签名由 AppAuthFilter 走中央密钥链校验(flow-center 100019)。保护 # /oapi/flow/dept-judge(条件路由判断)与 /oapi/flow/callback(流程回调)。 @@ -71,7 +72,6 @@ yh: enabled: true # 开放网关(服务间 oapi 正门,手册 §5.0):flow-center / usermsg 的 oapi 只在 open.* # 上有路由,指到 base.*(业务门户)会返回 404 → 流程列表/发起/待办全部失败。 - base-host: http://open.yunhaixingtong.com # 出站地址三级解析的第二级:yh.call.targets → yh.open.services → 枚举兜底 # (key 取 PlatformService.key():flow-center / usermsg / userbase / userclass) services: @@ -81,29 +81,21 @@ yh: user-center-url: http://base.yunhaixingtong.com/userbase # 班级管理(bschclass)服务基址 bschclass: http://base.yunhaixingtong.com/userclass - # 流程中心回调接收端(yh-flow-callback-spring-boot-starter) - # 默认关闭:验签密钥链依赖 infra Redis(容器网内)。本地联调在 application-local.yml - # 打开并指定局域网回调地址;线上由 Nacos workleave.yaml 覆盖本段。 + # 流程中心回调接收端(yh-flow-callback-spring-boot-starter)。 + # 生产由平台公共配置提供 open-host,starter 按应用 context-path 自动推导回调地址。 flow-callback: - enabled: false - biz-types: leave - # 回调地址:不配 callback-url 时由 open-host + path 拼(/oapi/flow/callback) - open-host: ${yh.open.base-host} + biz-types: [work-leave] + reconcile-on-register: true management: health: redis: enabled: false # 请假模块自身配置 -leave: - flow: - # 兜底默认流程:流程中心「流程管理-流程列表」中 code=work-leave 且 status=1(已发布) 的流程。 - # 仅当 leave_flow_bind 未配置任何绑定时兜底;真正生效优先走绑定表(全校默认 / 按请假类型)。 - # d2237a5b2b95492e845da9caeaa35590 = 「请假申请(小于3天或大于等于3天)」v4(2026-10-09 17:45 发布) - # 另一条已发布的 work-leave 流程:28bc3ca8acc24611809d7dd1b6b9a597 = 「产假(10天)」v5 - guid: d2237a5b2b95492e845da9caeaa35590 - # 销假时效(天):末次通过超过该天数不允许发起销假 - cancel-limit-days: 30 +work-leave: + flow: + # 销假时效(天):末次通过超过该天数不允许发起销假 + cancel-limit-days: 30 FILE_PATH: D:/data/upload -FILE_WEB_PATH: http://base.yunhaixingtong.com/userbase/upload/ \ No newline at end of file +FILE_WEB_PATH: http://base.yunhaixingtong.com/userbase/upload/ diff --git a/src/test/java/com/yunhai/workleave/controller/oapi/FlowJudgeControllerTest.java b/src/test/java/com/yunhai/workleave/controller/oapi/FlowJudgeControllerTest.java index b5c6c0a..5b57a32 100644 --- a/src/test/java/com/yunhai/workleave/controller/oapi/FlowJudgeControllerTest.java +++ b/src/test/java/com/yunhai/workleave/controller/oapi/FlowJudgeControllerTest.java @@ -142,7 +142,7 @@ class FlowJudgeControllerTest { JudgeBody body = new JudgeBody(); body.setSchId(9L); body.setInstanceId(5001L); - body.setBizType("leave"); + body.setBizType("work-leave"); body.setBizId("100-1"); body.setRouterKey("work-leave.dept-judge"); body.setOutlets(outlets); diff --git a/src/test/java/com/yunhai/workleave/flow/LeaveFlowCallbackHandlerTest.java b/src/test/java/com/yunhai/workleave/flow/LeaveFlowCallbackHandlerTest.java index 950726b..5c5f656 100644 --- a/src/test/java/com/yunhai/workleave/flow/LeaveFlowCallbackHandlerTest.java +++ b/src/test/java/com/yunhai/workleave/flow/LeaveFlowCallbackHandlerTest.java @@ -237,7 +237,7 @@ class LeaveFlowCallbackHandlerTest { event.setBizId("100-1"); event.setInstanceId(5001L); event.setInstanceVersion(version); - event.setBizType("leave"); + event.setBizType("work-leave"); event.setSchId(9); event.setOperatorUserId(operatorId); event.setOperatorName("审核人"); diff --git a/src/test/java/com/yunhai/workleave/flow/LeaveFlowServiceTest.java b/src/test/java/com/yunhai/workleave/flow/LeaveFlowServiceTest.java index 1514f95..e09351d 100644 --- a/src/test/java/com/yunhai/workleave/flow/LeaveFlowServiceTest.java +++ b/src/test/java/com/yunhai/workleave/flow/LeaveFlowServiceTest.java @@ -34,7 +34,7 @@ class LeaveFlowServiceTest { FlowApplyBody body = service.buildApplyBody(leave, "100-1", "张三的事假申请"); assertEquals("leave-apply-v1", body.getFlowGuid()); - assertEquals("leave", body.getBizType()); + assertEquals("work-leave", body.getBizType()); assertEquals("100-1", body.getBizId()); assertEquals("张三的事假申请", body.getBizTitle()); assertEquals("/detail?leaveId=100", body.getBizUrl());