liyong
16 小时以前 1ca5584d7e3200a9af65a099bd26d3593e2ba702
src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,996 @@
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<ApproveProcess> 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<ApproveProcess> 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<Map<String, Object>> 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<String, Object> 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<ApproveNode> nodes = listNodes(process);
        List<ApproveLog> logs = listLogs(process.getId());
        ApproveNode currentNode = findCurrentNode(nodes);
        List<Map<String, Object>> nodeItems = nodes.stream().map(node -> {
            Map<String, Object> 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<Map<String, Object>> logItems = logs.stream().map(log -> {
            Map<String, Object> 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<ApproveProcess> processes = defaultList(approveProcessMapper.selectList(new LambdaQueryWrapper<ApproveProcess>()
                .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<ApproveProcess> 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<String, Long> statusStats = filteredProcesses.stream()
                .collect(Collectors.groupingBy(p -> approveStatusName(p.getApproveStatus()), LinkedHashMap::new, Collectors.counting()));
        Map<String, Long> 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<String, Object> 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<String, Object> 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<ApproveNode> 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<ApproveNode> 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<ApproveProcess>()
                .eq(ApproveProcess::getApproveId, approveId)
                .eq(ApproveProcess::getApproveDelete, 0)
                .last("limit 1"));
    }
    private List<ApproveNode> listNodes(ApproveProcess process) {
        if (process == null) {
            return List.of();
        }
        List<ApproveNode> nodes = defaultList(approveNodeMapper.selectList(new LambdaQueryWrapper<ApproveNode>()
                .eq(ApproveNode::getDeleteFlag, 0)
                .eq(ApproveNode::getApproveProcessId, process.getApproveId())
                .orderByAsc(ApproveNode::getApproveNodeOrder)));
        if (!nodes.isEmpty()) {
            return nodes;
        }
        return defaultList(approveNodeMapper.selectList(new LambdaQueryWrapper<ApproveNode>()
                .eq(ApproveNode::getDeleteFlag, 0)
                .eq(ApproveNode::getApproveProcessId, String.valueOf(process.getId()))
                .orderByAsc(ApproveNode::getApproveNodeOrder)));
    }
    private List<ApproveLog> listLogs(Long processId) {
        return defaultList(approveLogMapper.selectList(new LambdaQueryWrapper<ApproveLog>()
                .eq(ApproveLog::getApproveId, processId)
                .orderByAsc(ApproveLog::getApproveNodeOrder, ApproveLog::getApproveTime)));
    }
    private ApproveNode findCurrentNode(List<ApproveNode> nodes) {
        return nodes.stream()
                .filter(node -> node.getApproveNodeStatus() != null && node.getApproveNodeStatus() == 0)
                .min(Comparator.comparing(ApproveNode::getApproveNodeOrder))
                .orElse(null);
    }
    private boolean isLastNode(List<ApproveNode> 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<String> 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<ApproveProcess> 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<Map<String, Object>> toTrendItems(List<String> dates, List<Long> values) {
        List<Map<String, Object>> items = new ArrayList<>();
        for (int i = 0; i < dates.size(); i++) {
            Map<String, Object> item = new LinkedHashMap<>();
            item.put("date", dates.get(i));
            item.put("count", values.get(i));
            items.add(item);
        }
        return items;
    }
    private Map<String, Object> buildStatusBarOption(Map<String, Long> statusStats) {
        Map<String, Object> 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<String, Object> buildTypePieOption(Map<String, Long> typeStats) {
        List<Map<String, Object>> data = typeStats.entrySet().stream()
                .map(entry -> {
                    Map<String, Object> item = new LinkedHashMap<>();
                    item.put("name", entry.getKey());
                    item.put("value", entry.getValue());
                    return item;
                })
                .collect(Collectors.toList());
        Map<String, Object> 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<String, Object> buildTrendLineOption(List<String> dates, List<Long> values, String label) {
        Map<String, Object> 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<ApproveProcess> processes) {
        long days = ChronoUnit.DAYS.between(start, end) + 1;
        if (days <= 31) {
            List<String> labels = new ArrayList<>();
            List<Long> 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<String> labels = new ArrayList<>();
        List<Long> 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<String, Object> data) {
        Map<String, Object> 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<String, Object> summary,
                                Map<String, Object> data,
                                Map<String, Object> charts) {
        Map<String, Object> 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 <T> List<T> defaultList(List<T> list) {
        return list == null ? List.of() : list;
    }
    private record DateRange(LocalDate start, LocalDate end, String label) {
    }
    private record TrendRange(List<String> labels, List<Long> values, String granularity, String label) {
    }
}