package com.ruoyi.ai.tools; import com.alibaba.fastjson2.JSON; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.ruoyi.ai.context.AiSessionUserContext; import com.ruoyi.approve.mapper.ApproveLogMapper; import com.ruoyi.approve.mapper.ApproveNodeMapper; import com.ruoyi.approve.mapper.ApproveProcessMapper; import com.ruoyi.approve.pojo.ApproveLog; import com.ruoyi.approve.pojo.ApproveNode; import com.ruoyi.approve.pojo.ApproveProcess; import com.ruoyi.approve.service.IApproveNodeService; import com.ruoyi.approve.service.impl.ApproveProcessServiceImpl; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.framework.security.LoginUser; import dev.langchain4j.agent.tool.P; import dev.langchain4j.agent.tool.Tool; import dev.langchain4j.agent.tool.ToolMemoryId; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.io.IOException; import java.math.BigDecimal; import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.YearMonth; import java.time.format.DateTimeFormatter; import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.StringJoiner; import java.util.regex.Matcher; import java.util.stream.Collectors; @Component public class ApproveTodoTools { private static final int DEFAULT_LIMIT = 10; private static final int MAX_LIMIT = 20; private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); private final ApproveProcessMapper approveProcessMapper; private final ApproveNodeMapper approveNodeMapper; private final ApproveLogMapper approveLogMapper; private final IApproveNodeService approveNodeService; private final ApproveProcessServiceImpl approveProcessService; private final AiSessionUserContext aiSessionUserContext; public ApproveTodoTools(ApproveProcessMapper approveProcessMapper, ApproveNodeMapper approveNodeMapper, ApproveLogMapper approveLogMapper, IApproveNodeService approveNodeService, ApproveProcessServiceImpl approveProcessService, AiSessionUserContext aiSessionUserContext) { this.approveProcessMapper = approveProcessMapper; this.approveNodeMapper = approveNodeMapper; this.approveLogMapper = approveLogMapper; this.approveNodeService = approveNodeService; this.approveProcessService = approveProcessService; this.aiSessionUserContext = aiSessionUserContext; } @Tool(name = "查询审批待办列表", value = "查询当前登录人相关的审批待办,优先返回自己待处理的审批,支持按状态、类型、关键字和范围过滤。") public String listTodos(@ToolMemoryId String memoryId, @P(value = "审批状态,可选值:all、pending、processing、approved、rejected、resubmitted", required = false) String status, @P(value = "审批类型编号,可不传", required = false) Integer approveType, @P(value = "关键字,可匹配流程编号、标题、申请人、当前审批人", required = false) String keyword, @P(value = "返回条数,默认10,最大20", required = false) Integer limit, @P(value = "查询范围,可选值:related、applicant、approver;related 表示当前用户相关,applicant 表示我发起的,approver 表示待我处理的", required = false) String scope) { LoginUser loginUser = currentLoginUser(memoryId); Long userId = loginUser.getUserId(); Integer statusCode = parseStatus(status); String normalizedScope = normalizeScope(scope); LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ApproveProcess::getApproveDelete, 0); if (statusCode == null) { wrapper.ne(ApproveProcess::getApproveStatus, 2); } if (approveType != null) { wrapper.eq(ApproveProcess::getApproveType, approveType); } if (StringUtils.hasText(keyword)) { wrapper.and(w -> w.like(ApproveProcess::getApproveId, keyword) .or().like(ApproveProcess::getApproveReason, keyword) .or().like(ApproveProcess::getApproveUserName, keyword) .or().like(ApproveProcess::getApproveUserCurrentName, keyword)); } if ("applicant".equals(normalizedScope)) { wrapper.eq(ApproveProcess::getApproveUser, userId); if (statusCode != null) { wrapper.eq(ApproveProcess::getApproveStatus, statusCode); } } else if ("approver".equals(normalizedScope)) { wrapper.eq(ApproveProcess::getApproveUserCurrentId, userId); if (statusCode != null) { wrapper.eq(ApproveProcess::getApproveStatus, statusCode); } } else if (statusCode != null && (statusCode == 0 || statusCode == 1)) { wrapper.eq(ApproveProcess::getApproveUserCurrentId, userId); wrapper.eq(ApproveProcess::getApproveStatus, statusCode); } else { wrapper.and(w -> w.eq(ApproveProcess::getApproveUser, userId) .or().eq(ApproveProcess::getApproveUserCurrentId, userId) .or().apply("FIND_IN_SET({0}, approve_user_ids)", userId)); if (statusCode != null) { wrapper.eq(ApproveProcess::getApproveStatus, statusCode); } } wrapper.orderByDesc(ApproveProcess::getCreateTime) .last("limit " + normalizeLimit(limit)); List processes = defaultList(approveProcessMapper.selectList(wrapper)); if (processes.isEmpty()) { return jsonResponse(true, "todo_list", "未查询到当前用户符合条件的审批待办。", Map.of("count", 0), Map.of("columns", todoColumns(), "items", List.of()), Map.of()); } List> items = processes.stream() .filter(process -> canView(process, userId)) .sorted(Comparator .comparing((ApproveProcess process) -> !Objects.equals(process.getApproveUserCurrentId(), userId)) .thenComparing(ApproveProcess::getCreateTime, Comparator.nullsLast(Comparator.reverseOrder()))) .map(process -> { Map item = new LinkedHashMap<>(); item.put("approveId", process.getApproveId()); item.put("approveType", approveTypeName(process.getApproveType())); item.put("approveUserName", safe(process.getApproveUserName())); item.put("approveUserCurrentName", safe(process.getApproveUserCurrentName())); item.put("approveReason", safe(process.getApproveReason())); item.put("approveStatus", approveStatusName(process.getApproveStatus())); item.put("createTime", formatDateTime(process.getCreateTime())); item.put("relation", relationName(process, userId)); return item; }) .collect(Collectors.toList()); return jsonResponse(true, "todo_list", "已返回当前用户相关审批列表。", Map.of( "count", items.size(), "statusFilter", StringUtils.hasText(status) ? status : "all", "approveType", approveType == null ? "" : approveType, "keyword", keyword == null ? "" : keyword, "scope", normalizedScope ), Map.of("columns", todoColumns(), "items", items), Map.of()); } @Tool(name = "查询审批待办详情", value = "根据流程编号查询当前登录人可见的审批详情。") public String getTodoDetail(@ToolMemoryId String memoryId, @P("流程编号 approveId") String approveId) { ApproveProcess process = getAccessibleProcess(memoryId, approveId); if (process == null) { return "未找到对应审批,或当前用户无权查看该流程。"; } StringJoiner detail = new StringJoiner("\n"); detail.add("审批详情"); detail.add("流程编号: " + safe(process.getApproveId())); detail.add("审批类型: " + approveTypeName(process.getApproveType())); detail.add("申请人: " + safe(process.getApproveUserName())); detail.add("申请部门: " + safe(process.getApproveDeptName())); detail.add("当前审批人: " + safe(process.getApproveUserCurrentName())); detail.add("标题: " + safe(process.getApproveReason())); detail.add("状态: " + approveStatusName(process.getApproveStatus())); detail.add("申请日期: " + formatDate(process.getApproveTime())); detail.add("开始日期: " + formatDate(process.getStartDate())); detail.add("结束日期: " + formatDate(process.getEndDate())); detail.add("地点: " + safe(process.getLocation())); detail.add("金额: " + (process.getPrice() == null ? "" : process.getPrice().toPlainString())); detail.add("备注: " + safe(process.getApproveRemark())); detail.add("创建时间: " + formatDateTime(process.getCreateTime())); detail.add("与当前用户关系: " + relationName(process, currentUserId(memoryId))); return detail.toString(); } @Tool(name = "查询审批流转记录", value = "根据流程编号查询审批节点和审批日志,用于回答进度、当前卡点和历史处理记录。") public String getTodoProgress(@ToolMemoryId String memoryId, @P("流程编号 approveId") String approveId) { ApproveProcess process = getAccessibleProcess(memoryId, approveId); if (process == null) { return jsonResponse(false, "todo_progress", "未找到对应审批,或当前用户无权查看该流程。", Map.of("approveId", safe(approveId)), Map.of(), Map.of()); } List nodes = listNodes(process); List logs = listLogs(process.getId()); ApproveNode currentNode = findCurrentNode(nodes); List> nodeItems = nodes.stream().map(node -> { Map item = new LinkedHashMap<>(); item.put("approveNodeOrder", node.getApproveNodeOrder()); item.put("approveNodeUser", safe(node.getApproveNodeUser())); item.put("approveNodeUserId", node.getApproveNodeUserId()); item.put("approveNodeStatus", approveNodeStatusName(node.getApproveNodeStatus())); item.put("approveNodeTime", formatDate(node.getApproveNodeTime())); item.put("approveNodeReason", safe(node.getApproveNodeReason())); item.put("approveNodeRemark", safe(node.getApproveNodeRemark())); item.put("isCurrent", currentNode != null && Objects.equals(currentNode.getId(), node.getId())); return item; }).collect(Collectors.toList()); List> logItems = logs.stream().map(log -> { Map item = new LinkedHashMap<>(); item.put("approveNodeOrder", log.getApproveNodeOrder()); item.put("approveUser", log.getApproveUser()); item.put("approveStatus", approveStatusName(log.getApproveStatus())); item.put("approveTime", formatDate(log.getApproveTime())); item.put("approveRemark", safe(log.getApproveRemark())); return item; }).collect(Collectors.toList()); return jsonResponse(true, "todo_progress", "已返回审批流转记录。", Map.of( "approveId", safe(process.getApproveId()), "currentStatus", approveStatusName(process.getApproveStatus()), "currentApprover", safe(process.getApproveUserCurrentName()), "currentNodeOrder", currentNode == null ? "" : currentNode.getApproveNodeOrder(), "nodeCount", nodeItems.size(), "logCount", logItems.size() ), Map.of("nodes", nodeItems, "logs", logItems), Map.of()); } @Tool(name = "统计审批待办数据", value = "按用户指定的时间范围统计当前登录人相关审批的状态分布、类型分布和趋势;未指定时默认近7天。") public String getTodoStats(@ToolMemoryId String memoryId, @P(value = "开始日期 yyyy-MM-dd,可不传", required = false) String startDate, @P(value = "结束日期 yyyy-MM-dd,可不传", required = false) String endDate, @P(value = "时间范围描述,例如 今天、本月、近30天、2026-04-01到2026-04-27", required = false) String timeRange) { Long userId = currentUserId(memoryId); List processes = defaultList(approveProcessMapper.selectList(new LambdaQueryWrapper() .eq(ApproveProcess::getApproveDelete, 0) .and(w -> w.eq(ApproveProcess::getApproveUser, userId) .or().eq(ApproveProcess::getApproveUserCurrentId, userId) .or().apply("FIND_IN_SET({0}, approve_user_ids)", userId)))); DateRange dateRange = resolveDateRange(startDate, endDate, timeRange); List filteredProcesses = processes.stream() .filter(process -> withinDateRange(process.getCreateTime(), dateRange)) .collect(Collectors.toList()); if (filteredProcesses.isEmpty()) { return jsonResponse(true, "todo_stats", "当前用户没有相关审批数据。", Map.of( "total", 0, "startDate", dateRange.start().toString(), "endDate", dateRange.end().toString(), "timeRange", dateRange.label() ), Map.of( "statusDistribution", Map.of(), "typeDistribution", Map.of(), "trend", List.of() ), Map.of()); } Map statusStats = filteredProcesses.stream() .collect(Collectors.groupingBy(p -> approveStatusName(p.getApproveStatus()), LinkedHashMap::new, Collectors.counting())); Map typeStats = filteredProcesses.stream() .collect(Collectors.groupingBy(p -> approveTypeName(p.getApproveType()), LinkedHashMap::new, Collectors.counting())); long pendingCount = countByStatus(filteredProcesses, 0); long processingCount = countByStatus(filteredProcesses, 1); long approvedCount = countByStatus(filteredProcesses, 2); long rejectedCount = countByStatus(filteredProcesses, 3); long resubmittedCount = countByStatus(filteredProcesses, 4); TrendRange trendRange = buildTrendRange(dateRange.start(), dateRange.end(), filteredProcesses); Map summary = new LinkedHashMap<>(); summary.put("total", filteredProcesses.size()); summary.put("pending", pendingCount); summary.put("processing", processingCount); summary.put("approved", approvedCount); summary.put("rejected", rejectedCount); summary.put("resubmitted", resubmittedCount); summary.put("approvalCompletionRate", calculateRate(approvedCount, filteredProcesses.size())); summary.put("rejectionRate", calculateRate(rejectedCount, filteredProcesses.size())); summary.put("startDate", dateRange.start().toString()); summary.put("endDate", dateRange.end().toString()); summary.put("timeRange", dateRange.label()); summary.put("trendGranularity", trendRange.granularity()); Map charts = new LinkedHashMap<>(); charts.put("statusBarOption", buildStatusBarOption(statusStats)); charts.put("typePieOption", buildTypePieOption(typeStats)); charts.put("trendLineOption", buildTrendLineOption(trendRange.labels(), trendRange.values(), trendRange.label())); return jsonResponse(true, "todo_stats", "已返回当前用户相关审批统计。", summary, Map.of( "statusDistribution", statusStats, "typeDistribution", typeStats, "trend", toTrendItems(trendRange.labels(), trendRange.values()) ), charts); } @Transactional(rollbackFor = Exception.class) @Tool(name = "审批待办", value = "执行审批动作,action 仅支持 approve 或 reject,且只能处理当前登录人自己的待审节点。") public String reviewTodo(@ToolMemoryId String memoryId, @P("流程编号 approveId") String approveId, @P("动作,approve=通过,reject=驳回") String action, @P(value = "审批备注,可不传", required = false) String remark) { ApproveProcess process = getAccessibleProcess(memoryId, approveId); if (process == null) { return actionResult(false, "review_action", "未找到对应审批,或当前用户无权访问该流程。", approveId, null); } if (!canOperate(process, currentUserId(memoryId))) { return actionResult(false, "review_action", "当前登录人不是该审批的当前处理人。", approveId, null); } if (process.getApproveStatus() != null && (process.getApproveStatus() == 2 || process.getApproveStatus() == 3)) { return actionResult(false, "review_action", "该审批已结束,不能重复处理。", approveId, null); } List nodes = listNodes(process); ApproveNode currentNode = findCurrentNode(nodes); if (currentNode == null || !Objects.equals(currentNode.getApproveNodeUserId(), currentUserId(memoryId))) { return actionResult(false, "review_action", "未找到当前用户可处理的审批节点。", approveId, null); } String normalizedAction = action == null ? "" : action.trim().toLowerCase(); currentNode.setApproveNodeRemark(remark); currentNode.setApproveNodeReason("reject".equals(normalizedAction) ? remark : null); currentNode.setUpdateUser(currentUserId(memoryId)); currentNode.setUpdateTime(LocalDateTime.now()); currentNode.setIsLast(isLastNode(nodes, currentNode)); try { switch (normalizedAction) { case "approve" -> currentNode.setApproveNodeStatus(1); case "reject" -> currentNode.setApproveNodeStatus(2); default -> { return actionResult(false, "review_action", "action 只支持 approve 或 reject。", approveId, null); } } approveNodeService.updateApproveNode(currentNode); } catch (IOException e) { throw new RuntimeException("审批处理失败", e); } ApproveProcess refreshed = getProcessByApproveId(approveId); writeApproveLog(memoryId, refreshed, currentNode, remark); ApproveNode nextNode = refreshed == null ? null : findCurrentNode(listNodes(refreshed)); return actionResult(true, "review_action", "approve".equals(normalizedAction) ? "审批已通过。" : "审批已驳回。", approveId, Map.of( "action", normalizedAction, "currentStatus", refreshed == null ? "" : approveStatusName(refreshed.getApproveStatus()), "nextApprover", nextNode == null ? "" : safe(nextNode.getApproveNodeUser()), "remark", safe(remark) )); } @Transactional(rollbackFor = Exception.class) @Tool(name = "取消审批待办审核", value = "撤销最近一次审核结果,仅允许最近一次审核人或申请人操作。") public String cancelReviewTodo(@ToolMemoryId String memoryId, @P("流程编号 approveId") String approveId, @P(value = "取消原因,可不传", required = false) String reason) { ApproveProcess process = getAccessibleProcess(memoryId, approveId); if (process == null) { return actionResult(false, "cancel_review_action", "未找到对应审批,或当前用户无权访问该流程。", approveId, null); } List nodes = listNodes(process); ApproveNode lastReviewedNode = nodes.stream() .filter(node -> node.getApproveNodeStatus() != null && node.getApproveNodeStatus() != 0) .max(Comparator.comparing(ApproveNode::getApproveNodeOrder)) .orElse(null); if (lastReviewedNode == null) { return actionResult(false, "cancel_review_action", "当前流程没有可撤销的审核记录。", approveId, null); } Long userId = currentUserId(memoryId); if (!isAdmin(userId) && !Objects.equals(process.getApproveUser(), userId) && !Objects.equals(lastReviewedNode.getApproveNodeUserId(), userId)) { return actionResult(false, "cancel_review_action", "只有申请人、最近一次审核人或管理员可以撤销。", approveId, null); } lastReviewedNode.setApproveNodeStatus(0); lastReviewedNode.setApproveNodeTime(null); lastReviewedNode.setApproveNodeReason(null); lastReviewedNode.setApproveNodeRemark(reason); lastReviewedNode.setUpdateUser(userId); lastReviewedNode.setUpdateTime(LocalDateTime.now()); approveNodeMapper.updateById(lastReviewedNode); ApproveLog latestLog = listLogs(process.getId()).stream() .max(Comparator.comparing(ApproveLog::getApproveNodeOrder) .thenComparing(ApproveLog::getApproveTime, Comparator.nullsLast(Date::compareTo))) .orElse(null); if (latestLog != null) { approveLogMapper.deleteById(latestLog.getId()); } process.setApproveOverTime(null); process.setApproveRemark(reason); process.setApproveStatus(lastReviewedNode.getApproveNodeOrder() == null || lastReviewedNode.getApproveNodeOrder() <= 1 ? 0 : 1); process.setApproveUserCurrentId(lastReviewedNode.getApproveNodeUserId()); process.setApproveUserCurrentName(lastReviewedNode.getApproveNodeUser()); approveProcessMapper.updateById(process); return actionResult(true, "cancel_review_action", "最近一次审核已撤销。", approveId, Map.of( "rollbackNodeOrder", lastReviewedNode.getApproveNodeOrder(), "currentStatus", approveStatusName(process.getApproveStatus()), "currentApprover", safe(process.getApproveUserCurrentName()), "reason", safe(reason) )); } @Transactional(rollbackFor = Exception.class) @Tool(name = "修改审批待办", value = "修改审批单基础信息,仅允许申请人修改;不支持通过 AI 变更审批类型。") public String updateTodo(@ToolMemoryId String memoryId, @P("流程编号 approveId") String approveId, @P(value = "新的标题,可不传", required = false) String approveReason, @P(value = "新的开始日期 yyyy-MM-dd,可不传", required = false) String startDate, @P(value = "新的结束日期 yyyy-MM-dd,可不传", required = false) String endDate, @P(value = "新的金额,可不传", required = false) BigDecimal price, @P(value = "新的地点,可不传", required = false) String location, @P(value = "新的审批类型,可不传", required = false) Integer approveType, @P(value = "新的备注,可不传", required = false) String approveRemark) { ApproveProcess process = getAccessibleProcess(memoryId, approveId); if (process == null) { return actionResult(false, "update_action", "未找到对应审批,或当前用户无权访问该流程。", approveId, null); } if (!Objects.equals(process.getApproveUser(), currentUserId(memoryId)) && !isAdmin(currentUserId(memoryId))) { return actionResult(false, "update_action", "只有申请人或管理员可以修改审批单。", approveId, null); } if (process.getApproveStatus() != null && (process.getApproveStatus() == 1 || process.getApproveStatus() == 2)) { return actionResult(false, "update_action", "审批处理中或已完成时,不允许通过 AI 修改。", approveId, null); } if (approveType != null && !Objects.equals(approveType, process.getApproveType())) { return actionResult(false, "update_action", "AI 助手暂不支持直接变更审批类型,避免节点配置失真。", approveId, null); } if (!StringUtils.hasText(approveReason) && !StringUtils.hasText(startDate) && !StringUtils.hasText(endDate) && price == null && !StringUtils.hasText(location) && !StringUtils.hasText(approveRemark)) { return actionResult(false, "update_action", "没有检测到可更新的字段。", approveId, null); } if (StringUtils.hasText(approveReason)) { process.setApproveReason(approveReason); } if (StringUtils.hasText(startDate)) { process.setStartDate(parseDate(startDate)); } if (StringUtils.hasText(endDate)) { process.setEndDate(parseDate(endDate)); } if (price != null) { process.setPrice(price); } if (StringUtils.hasText(location)) { process.setLocation(location); } if (StringUtils.hasText(approveRemark)) { process.setApproveRemark(approveRemark); } approveProcessMapper.updateById(process); return actionResult(true, "update_action", "审批单已更新。", approveId, Map.of( "approveReason", safe(process.getApproveReason()), "startDate", formatDate(process.getStartDate()), "endDate", formatDate(process.getEndDate()), "price", process.getPrice() == null ? "" : process.getPrice(), "location", safe(process.getLocation()), "approveType", approveTypeName(process.getApproveType()), "approveRemark", safe(process.getApproveRemark()) )); } @Transactional(rollbackFor = Exception.class) @Tool(name = "删除审批待办", value = "删除审批流程,仅允许申请人删除未完成的流程。") public String deleteTodo(@ToolMemoryId String memoryId, @P("流程编号 approveId") String approveId) { ApproveProcess process = getAccessibleProcess(memoryId, approveId); if (process == null) { return actionResult(false, "delete_action", "未找到对应审批,或当前用户无权访问该流程。", approveId, null); } if (!Objects.equals(process.getApproveUser(), currentUserId(memoryId)) && !isAdmin(currentUserId(memoryId))) { return actionResult(false, "delete_action", "只有申请人或管理员可以删除审批单。", approveId, null); } if (process.getApproveStatus() != null && (process.getApproveStatus() == 1 || process.getApproveStatus() == 2)) { return actionResult(false, "delete_action", "审批处理中或已完成的流程不允许通过 AI 删除。", approveId, null); } approveProcessService.delByIds(Collections.singletonList(process.getId())); return actionResult(true, "delete_action", "审批流程已删除。", approveId, Map.of( "deletedProcessId", process.getId(), "approveStatus", approveStatusName(process.getApproveStatus()) )); } private ApproveProcess getAccessibleProcess(String memoryId, String approveId) { ApproveProcess process = getProcessByApproveId(approveId); if (process == null) { return null; } return canView(process, currentUserId(memoryId)) ? process : null; } private ApproveProcess getProcessByApproveId(String approveId) { if (!StringUtils.hasText(approveId)) { return null; } return approveProcessMapper.selectOne(new LambdaQueryWrapper() .eq(ApproveProcess::getApproveId, approveId) .eq(ApproveProcess::getApproveDelete, 0) .last("limit 1")); } private List listNodes(ApproveProcess process) { if (process == null) { return List.of(); } List nodes = defaultList(approveNodeMapper.selectList(new LambdaQueryWrapper() .eq(ApproveNode::getDeleteFlag, 0) .eq(ApproveNode::getApproveProcessId, process.getApproveId()) .orderByAsc(ApproveNode::getApproveNodeOrder))); if (!nodes.isEmpty()) { return nodes; } return defaultList(approveNodeMapper.selectList(new LambdaQueryWrapper() .eq(ApproveNode::getDeleteFlag, 0) .eq(ApproveNode::getApproveProcessId, String.valueOf(process.getId())) .orderByAsc(ApproveNode::getApproveNodeOrder))); } private List listLogs(Long processId) { return defaultList(approveLogMapper.selectList(new LambdaQueryWrapper() .eq(ApproveLog::getApproveId, processId) .orderByAsc(ApproveLog::getApproveNodeOrder, ApproveLog::getApproveTime))); } private ApproveNode findCurrentNode(List nodes) { return nodes.stream() .filter(node -> node.getApproveNodeStatus() != null && node.getApproveNodeStatus() == 0) .min(Comparator.comparing(ApproveNode::getApproveNodeOrder)) .orElse(null); } private boolean isLastNode(List nodes, ApproveNode currentNode) { Integer maxOrder = nodes.stream() .map(ApproveNode::getApproveNodeOrder) .filter(Objects::nonNull) .max(Integer::compareTo) .orElse(null); return maxOrder != null && Objects.equals(maxOrder, currentNode.getApproveNodeOrder()); } private void writeApproveLog(String memoryId, ApproveProcess process, ApproveNode currentNode, String remark) { if (process == null || currentNode == null) { return; } ApproveLog log = new ApproveLog(); log.setApproveId(process.getId()); log.setApproveNodeOrder(currentNode.getApproveNodeOrder()); log.setApproveUser(currentUserId(memoryId)); log.setApproveTime(new Date()); log.setApproveStatus(process.getApproveStatus()); log.setApproveRemark(remark); approveLogMapper.insert(log); } private boolean canView(ApproveProcess process, Long userId) { if (process == null || userId == null) { return false; } return isAdmin(userId) || Objects.equals(process.getApproveUser(), userId) || Objects.equals(process.getApproveUserCurrentId(), userId) || containsUserId(process.getApproveUserIds(), userId); } private boolean canOperate(ApproveProcess process, Long userId) { return process != null && userId != null && Objects.equals(process.getApproveUserCurrentId(), userId); } private boolean containsUserId(String csv, Long userId) { if (!StringUtils.hasText(csv) || userId == null) { return false; } String target = String.valueOf(userId); for (String item : csv.split(",")) { if (target.equals(item.trim())) { return true; } } return false; } private String relationName(ApproveProcess process, Long userId) { if (Objects.equals(process.getApproveUserCurrentId(), userId)) { return "当前审批人"; } if (Objects.equals(process.getApproveUser(), userId)) { return "申请人"; } if (containsUserId(process.getApproveUserIds(), userId)) { return "审批链成员"; } return "可见"; } private List todoColumns() { return List.of("approveId", "approveType", "approveUserName", "approveUserCurrentName", "approveReason", "approveStatus", "createTime", "relation"); } private int normalizeLimit(Integer limit) { if (limit == null || limit <= 0) { return DEFAULT_LIMIT; } return Math.min(limit, MAX_LIMIT); } private Integer parseStatus(String status) { if (!StringUtils.hasText(status) || "all".equalsIgnoreCase(status)) { return null; } return switch (status.trim().toLowerCase()) { case "pending" -> 0; case "processing" -> 1; case "approved" -> 2; case "rejected" -> 3; case "resubmitted" -> 4; default -> null; }; } private String normalizeScope(String scope) { if (!StringUtils.hasText(scope)) { return "related"; } return switch (scope.trim().toLowerCase()) { case "applicant", "mine", "created", "initiated" -> "applicant"; case "approver", "handler", "todo", "pending" -> "approver"; default -> "related"; }; } private String approveStatusName(Integer status) { if (status == null) { return "未知"; } return switch (status) { case 0 -> "待审核"; case 1 -> "审核中"; case 2 -> "审核完成"; case 3 -> "审核未通过"; case 4 -> "已重新提交"; default -> "未知"; }; } private String approveNodeStatusName(Integer status) { if (status == null) { return "未知"; } return switch (status) { case 0 -> "未审核"; case 1 -> "同意"; case 2 -> "拒绝"; default -> "未知"; }; } private String approveTypeName(Integer type) { if (type == null) { return "未知"; } return switch (type) { case 1 -> "公出管理"; case 2 -> "请假管理"; case 3 -> "出差管理"; case 4 -> "报销管理"; case 5 -> "采购审批"; case 6 -> "报价审批"; case 7 -> "发货审批"; case 8 -> "危险作业审批"; case 9 -> "办公用品审批"; default -> "类型" + type; }; } private String safe(Object value) { return value == null ? "" : String.valueOf(value).replace('\n', ' ').replace('\r', ' '); } private String formatDateTime(Object value) { if (value == null) { return ""; } if (value instanceof LocalDateTime localDateTime) { return localDateTime.format(DATE_TIME_FORMATTER); } return safe(value); } private String formatDate(Date value) { return value == null ? "" : DATE_FORMAT.format(value); } private long countByStatus(List processes, int status) { return processes.stream() .filter(process -> process.getApproveStatus() != null) .filter(process -> process.getApproveStatus() == status) .count(); } private String calculateRate(long part, int total) { if (total <= 0) { return "0.00%"; } return String.format("%.2f%%", part * 100.0 / total); } private List> toTrendItems(List dates, List values) { List> items = new ArrayList<>(); for (int i = 0; i < dates.size(); i++) { Map item = new LinkedHashMap<>(); item.put("date", dates.get(i)); item.put("count", values.get(i)); items.add(item); } return items; } private Map buildStatusBarOption(Map statusStats) { Map option = new LinkedHashMap<>(); option.put("title", Map.of("text", "审批状态分布", "left", "center")); option.put("tooltip", Map.of("trigger", "axis")); option.put("xAxis", Map.of("type", "category", "data", new ArrayList<>(statusStats.keySet()))); option.put("yAxis", Map.of("type", "value")); option.put("series", List.of(Map.of( "name", "数量", "type", "bar", "data", new ArrayList<>(statusStats.values()), "barWidth", "40%" ))); return option; } private Map buildTypePieOption(Map typeStats) { List> data = typeStats.entrySet().stream() .map(entry -> { Map item = new LinkedHashMap<>(); item.put("name", entry.getKey()); item.put("value", entry.getValue()); return item; }) .collect(Collectors.toList()); Map option = new LinkedHashMap<>(); option.put("title", Map.of("text", "审批类型占比", "left", "center")); option.put("tooltip", Map.of("trigger", "item")); option.put("legend", Map.of("orient", "vertical", "left", "left")); option.put("series", List.of(Map.of( "name", "审批类型", "type", "pie", "radius", List.of("35%", "65%"), "data", data ))); return option; } private Map buildTrendLineOption(List dates, List values, String label) { Map option = new LinkedHashMap<>(); option.put("title", Map.of("text", label + "审批新增趋势", "left", "center")); option.put("tooltip", Map.of("trigger", "axis")); option.put("xAxis", Map.of("type", "category", "data", dates)); option.put("yAxis", Map.of("type", "value")); option.put("series", List.of(Map.of( "name", "新增审批", "type", "line", "smooth", true, "data", values, "areaStyle", Map.of() ))); return option; } private Date parseDate(String dateText) { try { return DATE_FORMAT.parse(dateText); } catch (ParseException e) { throw new IllegalArgumentException("日期格式必须是 yyyy-MM-dd"); } } private DateRange resolveDateRange(String startDateText, String endDateText, String timeRange) { LocalDate today = LocalDate.now(); LocalDate explicitStart = parseLocalDate(startDateText); LocalDate explicitEnd = parseLocalDate(endDateText); if (explicitStart != null || explicitEnd != null) { LocalDate start = explicitStart != null ? explicitStart : explicitEnd; LocalDate end = explicitEnd != null ? explicitEnd : explicitStart; if (start.isAfter(end)) { LocalDate temp = start; start = end; end = temp; } return new DateRange(start, end, start + "至" + end); } if (!StringUtils.hasText(timeRange)) { return new DateRange(today.minusDays(6), today, "近7天"); } String text = timeRange.trim(); if (text.contains("今天")) { return new DateRange(today, today, "今天"); } if (text.contains("昨天") || text.contains("昨日")) { LocalDate day = today.minusDays(1); return new DateRange(day, day, "昨天"); } if (text.contains("本周")) { LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L); return new DateRange(start, today, "本周"); } if (text.contains("上周")) { LocalDate thisWeekStart = today.minusDays(today.getDayOfWeek().getValue() - 1L); LocalDate start = thisWeekStart.minusWeeks(1); LocalDate end = thisWeekStart.minusDays(1); return new DateRange(start, end, "上周"); } if (text.contains("本月")) { LocalDate start = today.withDayOfMonth(1); return new DateRange(start, today, "本月"); } if (text.contains("上月")) { YearMonth lastMonth = YearMonth.from(today).minusMonths(1); return new DateRange(lastMonth.atDay(1), lastMonth.atEndOfMonth(), "上月"); } if (text.contains("本年") || text.contains("今年")) { LocalDate start = today.withDayOfYear(1); return new DateRange(start, today, "本年"); } if (text.contains("去年")) { LocalDate start = today.minusYears(1).withDayOfYear(1); LocalDate end = today.minusYears(1).withMonth(12).withDayOfMonth(31); return new DateRange(start, end, "去年"); } Matcher relativeMatcher = java.util.regex.Pattern.compile("(近|最近)(\\d+)(天|周|个月|月|年)").matcher(text); if (relativeMatcher.find()) { int amount = Integer.parseInt(relativeMatcher.group(2)); String unit = relativeMatcher.group(3); LocalDate start = switch (unit) { case "天" -> today.minusDays(Math.max(amount - 1L, 0)); case "周" -> today.minusWeeks(Math.max(amount, 1)).plusDays(1); case "个月", "月" -> today.minusMonths(Math.max(amount, 1)).plusDays(1); case "年" -> today.minusYears(Math.max(amount, 1)).plusDays(1); default -> today.minusDays(6); }; return new DateRange(start, today, "近" + amount + unit); } Matcher dateMatcher = java.util.regex.Pattern.compile("(\\d{4}-\\d{2}-\\d{2})").matcher(text); if (dateMatcher.find()) { LocalDate start = LocalDate.parse(dateMatcher.group(1)); LocalDate end = dateMatcher.find() ? LocalDate.parse(dateMatcher.group(1)) : start; if (start.isAfter(end)) { LocalDate temp = start; start = end; end = temp; } return new DateRange(start, end, start + "至" + end); } return new DateRange(today.minusDays(6), today, "近7天"); } private boolean withinDateRange(LocalDateTime createTime, DateRange dateRange) { if (createTime == null) { return false; } LocalDate date = createTime.toLocalDate(); return !date.isBefore(dateRange.start()) && !date.isAfter(dateRange.end()); } private TrendRange buildTrendRange(LocalDate start, LocalDate end, List processes) { long days = ChronoUnit.DAYS.between(start, end) + 1; if (days <= 31) { List labels = new ArrayList<>(); List values = new ArrayList<>(); for (LocalDate cursor = start; !cursor.isAfter(end); cursor = cursor.plusDays(1)) { LocalDate current = cursor; labels.add(current.toString()); values.add(processes.stream() .filter(process -> process.getCreateTime() != null) .filter(process -> process.getCreateTime().toLocalDate().equals(current)) .count()); } return new TrendRange(labels, values, "day", start + "至" + end); } List labels = new ArrayList<>(); List values = new ArrayList<>(); YearMonth startMonth = YearMonth.from(start); YearMonth endMonth = YearMonth.from(end); for (YearMonth cursor = startMonth; !cursor.isAfter(endMonth); cursor = cursor.plusMonths(1)) { YearMonth current = cursor; labels.add(current.toString()); values.add(processes.stream() .filter(process -> process.getCreateTime() != null) .filter(process -> YearMonth.from(process.getCreateTime()).equals(current)) .count()); } return new TrendRange(labels, values, "month", start + "至" + end); } private LocalDate parseLocalDate(String text) { if (!StringUtils.hasText(text)) { return null; } return LocalDate.parse(text.trim()); } private String actionResult(boolean success, String type, String description, String approveId, Map data) { Map summary = new LinkedHashMap<>(); summary.put("approveId", safe(approveId)); return jsonResponse(success, type, description, summary, data == null ? Map.of() : data, Map.of()); } private String jsonResponse(boolean success, String type, String description, Map summary, Map data, Map charts) { Map result = new LinkedHashMap<>(); result.put("success", success); result.put("type", type); result.put("description", description); result.put("summary", summary == null ? Map.of() : summary); result.put("data", data == null ? Map.of() : data); result.put("charts", charts == null ? Map.of() : charts); return JSON.toJSONString(result); } private LoginUser currentLoginUser(String memoryId) { LoginUser loginUser = aiSessionUserContext.get(memoryId); if (loginUser != null) { return loginUser; } return SecurityUtils.getLoginUser(); } private Long currentUserId(String memoryId) { return currentLoginUser(memoryId).getUserId(); } private boolean isAdmin(Long userId) { return SecurityUtils.isAdmin(userId); } private List defaultList(List list) { return list == null ? List.of() : list; } private record DateRange(LocalDate start, LocalDate end, String label) { } private record TrendRange(List labels, List values, String granularity, String label) { } }