flow
build / build-push (push) Successful in 1m1s

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