From 1ca5584d7e3200a9af65a099bd26d3593e2ba702 Mon Sep 17 00:00:00 2001
From: liyong <18434998025@163.com>
Date: 星期四, 07 五月 2026 14:36:08 +0800
Subject: [PATCH] 迁移pro

---
 src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java |  996 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 996 insertions(+), 0 deletions(-)

diff --git a/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java b/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
new file mode 100644
index 0000000..cd3e933
--- /dev/null
+++ b/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銆乸ending銆乸rocessing銆乤pproved銆乺ejected銆乺esubmitted", 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銆乤pplicant銆乤pprover锛況elated 琛ㄧず褰撳墠鐢ㄦ埛鐩稿叧锛宎pplicant 琛ㄧず鎴戝彂璧风殑锛宎pprover 琛ㄧず寰呮垜澶勭悊鐨�", 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 = "鎵ц瀹℃壒鍔ㄤ綔锛宎ction 浠呮敮鎸� approve 鎴� reject锛屼笖鍙兘澶勭悊褰撳墠鐧诲綍浜鸿嚜宸辩殑寰呭鑺傜偣銆�")
+    public String reviewTodo(@ToolMemoryId String memoryId,
+                             @P("娴佺▼缂栧彿 approveId") String approveId,
+                             @P("鍔ㄤ綔锛宎pprove=閫氳繃锛宺eject=椹冲洖") 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) {
+    }
+}

--
Gitblit v1.9.3