9 天以前 5cfb55c54a7c7f6b6158f132cf6aa8bacc046816
feat(purchase): 新增企业采购智能助理功能

- 实现采购智能助理接口,集成LangChain4j AI服务
- 开发采购助理工具类,提供台账查询、统计分析等功能
- 添加采购助理配置,实现聊天记忆存储管理
- 构建采购AI控制器,支持流式对话和会话管理
- 实现意图识别执行器,自动解析采购查询需求
- 定义采购助理系统提示词,规范AI响应行为
已添加6个文件
577 ■■■■■ 文件已修改
src/main/java/com/ruoyi/ai/assistant/PurchaseAgent.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/PurchaseAgentConfig.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java 315 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/purchase-agent-prompt.txt 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/PurchaseAgent.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.ruoyi.ai.assistant;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
import reactor.core.publisher.Flux;
import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
@AiService(
        wiringMode = EXPLICIT,
        streamingChatModel = "qwenStreamingChatModel",
        chatMemoryProvider = "chatMemoryProviderPurchase",
        tools = "purchaseAgentTools"
)
public interface PurchaseAgent {
    @SystemMessage(fromResource = "purchase-agent-prompt.txt")
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage);
}
src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,110 @@
package com.ruoyi.ai.assistant;
import com.ruoyi.ai.tools.PurchaseAgentTools;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Component
public class PurchaseIntentExecutor {
    private static final Pattern ID_PATTERN = Pattern.compile("\\b\\d{1,12}\\b");
    private static final Pattern LIMIT_PATTERN = Pattern.compile("(前|最近)?(\\d{1,2})条");
    private static final Pattern DATE_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
    private final PurchaseAgentTools purchaseAgentTools;
    public PurchaseIntentExecutor(PurchaseAgentTools purchaseAgentTools) {
        this.purchaseAgentTools = purchaseAgentTools;
    }
    public String tryExecute(String memoryId, String message) {
        if (!StringUtils.hasText(message)) {
            return null;
        }
        String text = message.trim();
        if (isStatsIntent(text)) {
            return purchaseAgentTools.getPurchaseStats(
                    memoryId,
                    extractStartDate(text),
                    extractEndDate(text),
                    text
            );
        }
        if (containsAny(text, "详情", "明细") && extractId(text) != null) {
            return purchaseAgentTools.getPurchaseLedgerDetail(memoryId, extractId(text));
        }
        if (containsAny(text, "台账", "采购单", "合同", "列表", "查询")) {
            return purchaseAgentTools.listPurchaseLedgers(
                    memoryId,
                    extractKeyword(text),
                    extractStartDate(text),
                    extractEndDate(text),
                    extractLimit(text)
            );
        }
        return null;
    }
    private boolean isStatsIntent(String text) {
        if (containsAny(text, "统计", "分析", "报表", "汇总", "趋势", "数据看板")) {
            return true;
        }
        boolean queryWord = containsAny(text, "查询", "查看", "看下", "看看", "获取");
        boolean dataWord = containsAny(text, "数据", "金额", "数量", "合同额", "付款额", "发票额");
        boolean timeWord = containsAny(text, "今天", "本周", "本月", "上月", "今年", "去年", "近半年", "最近半个月", "半个月")
                || DATE_PATTERN.matcher(text).find();
        return queryWord && dataWord && timeWord;
    }
    private boolean containsAny(String text, String... keywords) {
        for (String keyword : keywords) {
            if (text.contains(keyword)) {
                return true;
            }
        }
        return false;
    }
    private Long extractId(String text) {
        Matcher matcher = ID_PATTERN.matcher(text);
        if (!matcher.find()) {
            return null;
        }
        return Long.parseLong(matcher.group());
    }
    private Integer extractLimit(String text) {
        Matcher matcher = LIMIT_PATTERN.matcher(text);
        return matcher.find() ? Integer.parseInt(matcher.group(2)) : 10;
    }
    private String extractStartDate(String text) {
        Matcher matcher = DATE_PATTERN.matcher(text);
        return matcher.find() ? matcher.group() : null;
    }
    private String extractEndDate(String text) {
        Matcher matcher = DATE_PATTERN.matcher(text);
        if (!matcher.find()) {
            return null;
        }
        return matcher.find() ? matcher.group() : null;
    }
    private String extractKeyword(String text) {
        String cleaned = text
                .replace("查询", "")
                .replace("查看", "")
                .replace("采购", "")
                .replace("台账", "")
                .replace("列表", "")
                .replace("最近10条", "")
                .replace("前10条", "")
                .trim();
        return cleaned.length() >= 2 ? cleaned : null;
    }
}
src/main/java/com/ruoyi/ai/config/PurchaseAgentConfig.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.ai.config;
import com.ruoyi.ai.store.MongoChatMemoryStore;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class PurchaseAgentConfig {
    @Bean
    ChatMemoryProvider chatMemoryProviderPurchase(MongoChatMemoryStore mongoChatMemoryStore) {
        return memoryId -> MessageWindowChatMemory.builder()
                .id(memoryId)
                .maxMessages(30)
                .chatMemoryStore(mongoChatMemoryStore)
                .build();
    }
}
src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
package com.ruoyi.ai.controller;
import com.ruoyi.ai.assistant.PurchaseAgent;
import com.ruoyi.ai.assistant.PurchaseIntentExecutor;
import com.ruoyi.ai.bean.ChatForm;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.ai.service.AiChatSessionService;
import com.ruoyi.ai.store.MongoChatMemoryStore;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.util.List;
@Tag(name = "采购智能体")
@RestController
@RequestMapping("/purchase-ai")
public class PurchaseAiController extends BaseController {
    private final PurchaseAgent purchaseAgent;
    private final PurchaseIntentExecutor purchaseIntentExecutor;
    private final AiSessionUserContext aiSessionUserContext;
    private final MongoChatMemoryStore mongoChatMemoryStore;
    private final AiChatSessionService aiChatSessionService;
    public PurchaseAiController(PurchaseAgent purchaseAgent,
                                PurchaseIntentExecutor purchaseIntentExecutor,
                                AiSessionUserContext aiSessionUserContext,
                                MongoChatMemoryStore mongoChatMemoryStore,
                                AiChatSessionService aiChatSessionService) {
        this.purchaseAgent = purchaseAgent;
        this.purchaseIntentExecutor = purchaseIntentExecutor;
        this.aiSessionUserContext = aiSessionUserContext;
        this.mongoChatMemoryStore = mongoChatMemoryStore;
        this.aiChatSessionService = aiChatSessionService;
    }
    @Operation(summary = "采购对话")
    @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
    public Flux<String> chat(@RequestBody ChatForm chatForm) {
        if (!StringUtils.hasText(chatForm.getMemoryId())) {
            return Flux.just("memoryId不能为空");
        }
        if (!StringUtils.hasText(chatForm.getMessage())) {
            return Flux.just("message不能为空");
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        String memoryId = chatForm.getMemoryId();
        String userMessage = chatForm.getMessage();
        aiSessionUserContext.bind(memoryId, loginUser);
        aiChatSessionService.touchSession(memoryId, loginUser, userMessage);
        String directResponse = purchaseIntentExecutor.tryExecute(memoryId, userMessage);
        if (StringUtils.isNotEmpty(directResponse)) {
            mongoChatMemoryStore.appendMessages(
                    memoryId,
                    List.of(UserMessage.from(userMessage), AiMessage.from(directResponse))
            );
            aiChatSessionService.refreshSessionStats(memoryId, loginUser);
            return Flux.just(directResponse);
        }
        return purchaseAgent.chat(memoryId, userMessage)
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
    }
    @Operation(summary = "采购会话列表")
    @GetMapping("/history/sessions")
    public AjaxResult listSessions() {
        return success(aiChatSessionService.listCurrentUserSessions(SecurityUtils.getLoginUser()));
    }
    @Operation(summary = "采购会话消息")
    @GetMapping("/history/messages/{memoryId}")
    public AjaxResult listMessages(@PathVariable String memoryId) {
        return success(aiChatSessionService.listCurrentUserMessages(memoryId, SecurityUtils.getLoginUser()));
    }
    @Operation(summary = "删除采购会话")
    @DeleteMapping("/history/{memoryId}")
    public AjaxResult deleteSession(@PathVariable String memoryId) {
        aiSessionUserContext.remove(memoryId);
        return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser()));
    }
}
src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,315 @@
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.common.utils.SecurityUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.purchase.mapper.InvoicePurchaseMapper;
import com.ruoyi.purchase.mapper.PaymentRegistrationMapper;
import com.ruoyi.purchase.mapper.PurchaseLedgerMapper;
import com.ruoyi.purchase.mapper.PurchaseReturnOrdersMapper;
import com.ruoyi.purchase.pojo.InvoicePurchase;
import com.ruoyi.purchase.pojo.PaymentRegistration;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.purchase.pojo.PurchaseReturnOrders;
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.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@Component
public class PurchaseAgentTools {
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final int DEFAULT_LIMIT = 10;
    private static final int MAX_LIMIT = 30;
    private final PurchaseLedgerMapper purchaseLedgerMapper;
    private final PaymentRegistrationMapper paymentRegistrationMapper;
    private final InvoicePurchaseMapper invoicePurchaseMapper;
    private final PurchaseReturnOrdersMapper purchaseReturnOrdersMapper;
    private final AiSessionUserContext aiSessionUserContext;
    public PurchaseAgentTools(PurchaseLedgerMapper purchaseLedgerMapper,
                              PaymentRegistrationMapper paymentRegistrationMapper,
                              InvoicePurchaseMapper invoicePurchaseMapper,
                              PurchaseReturnOrdersMapper purchaseReturnOrdersMapper,
                              AiSessionUserContext aiSessionUserContext) {
        this.purchaseLedgerMapper = purchaseLedgerMapper;
        this.paymentRegistrationMapper = paymentRegistrationMapper;
        this.invoicePurchaseMapper = invoicePurchaseMapper;
        this.purchaseReturnOrdersMapper = purchaseReturnOrdersMapper;
        this.aiSessionUserContext = aiSessionUserContext;
    }
    @Tool(name = "查询采购台账列表", value = "按关键字和时间范围查询采购台账,支持返回最近N条")
    public String listPurchaseLedgers(@ToolMemoryId String memoryId,
                                      @P(value = "关键字,可匹配采购合同号/供应商/项目名", required = false) String keyword,
                                      @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                      @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                      @P(value = "返回条数,默认10,最大30", required = false) Integer limit) {
        LoginUser loginUser = currentLoginUser(memoryId);
        LocalDate start = parseLocalDate(startDate);
        LocalDate end = parseLocalDate(endDate);
        int finalLimit = normalizeLimit(limit);
        LambdaQueryWrapper<PurchaseLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), PurchaseLedger::getTenantId);
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(PurchaseLedger::getPurchaseContractNumber, keyword)
                    .or().like(PurchaseLedger::getSupplierName, keyword)
                    .or().like(PurchaseLedger::getProjectName, keyword));
        }
        if (start != null) {
            wrapper.ge(PurchaseLedger::getEntryDate, toDate(start));
        }
        if (end != null) {
            wrapper.le(PurchaseLedger::getEntryDate, toDate(end));
        }
        wrapper.orderByDesc(PurchaseLedger::getEntryDate, PurchaseLedger::getId).last("limit " + finalLimit);
        List<PurchaseLedger> rows = defaultList(purchaseLedgerMapper.selectList(wrapper));
        List<Map<String, Object>> items = rows.stream().map(this::toLedgerItem).collect(Collectors.toList());
        return jsonResponse(true, "purchase_ledger_list", "已返回采购台账列表",
                Map.of("count", items.size(), "limit", finalLimit, "keyword", safe(keyword)),
                Map.of("items", items), Map.of());
    }
    @Tool(name = "查询采购台账详情", value = "按采购台账ID查询详情")
    public String getPurchaseLedgerDetail(@ToolMemoryId String memoryId, @P("采购台账ID") Long ledgerId) {
        if (ledgerId == null) {
            return jsonResponse(false, "purchase_ledger_detail", "采购台账ID不能为空", Map.of(), Map.of(), Map.of());
        }
        LoginUser loginUser = currentLoginUser(memoryId);
        PurchaseLedger ledger = purchaseLedgerMapper.selectById(ledgerId);
        if (ledger == null || !tenantMatched(ledger.getTenantId(), loginUser.getTenantId())) {
            return jsonResponse(false, "purchase_ledger_detail", "未找到该采购台账或无权限访问", Map.of("ledgerId", ledgerId), Map.of(), Map.of());
        }
        return jsonResponse(true, "purchase_ledger_detail", "已返回采购台账详情",
                Map.of("ledgerId", ledgerId),
                Map.of("detail", toLedgerItem(ledger)),
                Map.of());
    }
    @Tool(name = "统计采购数据", value = "统计时间范围内采购合同数、合同金额、付款金额、发票金额、退货金额")
    public String getPurchaseStats(@ToolMemoryId String memoryId,
                                   @P(value = "开始日期 yyyy-MM-dd", required = false) String startDate,
                                   @P(value = "结束日期 yyyy-MM-dd", required = false) String endDate,
                                   @P(value = "时间范围描述,例如今年、本月、近30天", required = false) String timeRange) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange);
        List<PurchaseLedger> ledgers = queryLedgers(loginUser, range);
        List<PaymentRegistration> payments = queryPayments(loginUser, range);
        List<InvoicePurchase> invoices = queryInvoices(loginUser, range);
        List<PurchaseReturnOrders> returns = queryReturns(loginUser, range);
        BigDecimal contractAmount = ledgers.stream()
                .map(PurchaseLedger::getContractAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal paymentAmount = payments.stream()
                .map(PaymentRegistration::getCurrentPaymentAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal invoiceAmount = invoices.stream()
                .map(InvoicePurchase::getInvoiceAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        BigDecimal returnAmount = returns.stream()
                .map(PurchaseReturnOrders::getTotalAmount)
                .filter(Objects::nonNull)
                .reduce(BigDecimal.ZERO, BigDecimal::add);
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", range.start().toString());
        summary.put("endDate", range.end().toString());
        summary.put("ledgerCount", ledgers.size());
        summary.put("paymentCount", payments.size());
        summary.put("invoiceCount", invoices.size());
        summary.put("returnCount", returns.size());
        summary.put("contractAmount", contractAmount);
        summary.put("paymentAmount", paymentAmount);
        summary.put("invoiceAmount", invoiceAmount);
        summary.put("returnAmount", returnAmount);
        return jsonResponse(true, "purchase_stats", "已返回采购统计数据", summary, Map.of(), Map.of());
    }
    private List<PurchaseLedger> queryLedgers(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<PurchaseLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), PurchaseLedger::getTenantId);
        wrapper.ge(PurchaseLedger::getEntryDate, toDate(range.start()))
                .le(PurchaseLedger::getEntryDate, toDate(range.end()));
        return defaultList(purchaseLedgerMapper.selectList(wrapper));
    }
    private List<PaymentRegistration> queryPayments(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<PaymentRegistration> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), PaymentRegistration::getTenantId);
        wrapper.ge(PaymentRegistration::getPaymentDate, toDate(range.start()))
                .le(PaymentRegistration::getPaymentDate, toDate(range.end()));
        return defaultList(paymentRegistrationMapper.selectList(wrapper));
    }
    private List<InvoicePurchase> queryInvoices(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<InvoicePurchase> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), InvoicePurchase::getTenantId);
        wrapper.ge(InvoicePurchase::getIssueDate, range.start())
                .le(InvoicePurchase::getIssueDate, range.end());
        return defaultList(invoicePurchaseMapper.selectList(wrapper));
    }
    private List<PurchaseReturnOrders> queryReturns(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<PurchaseReturnOrders> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), PurchaseReturnOrders::getDeptId);
        wrapper.ge(PurchaseReturnOrders::getPreparedAt, range.start())
                .le(PurchaseReturnOrders::getPreparedAt, range.end());
        return defaultList(purchaseReturnOrdersMapper.selectList(wrapper));
    }
    private Map<String, Object> toLedgerItem(PurchaseLedger item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("purchaseContractNumber", safe(item.getPurchaseContractNumber()));
        map.put("supplierName", safe(item.getSupplierName()));
        map.put("projectName", safe(item.getProjectName()));
        map.put("entryDate", formatDate(item.getEntryDate()));
        map.put("contractAmount", item.getContractAmount());
        map.put("approvalStatus", item.getApprovalStatus());
        map.put("paymentMethod", safe(item.getPaymentMethod()));
        return map;
    }
    private DateRange resolveDateRange(String startDate, String endDate, String timeRange) {
        LocalDate today = LocalDate.now();
        LocalDate start = parseLocalDate(startDate);
        LocalDate end = parseLocalDate(endDate);
        if (start != null || end != null) {
            LocalDate s = start != null ? start : end;
            LocalDate e = end != null ? end : start;
            if (s.isAfter(e)) {
                LocalDate temp = s;
                s = e;
                e = temp;
            }
            return new DateRange(s, e, s + "至" + e);
        }
        if (!StringUtils.hasText(timeRange)) {
            return new DateRange(today.minusDays(29), today, "近30天");
        }
        String text = timeRange.trim();
        if (text.contains("今年") || text.contains("本年")) {
            return new DateRange(today.withDayOfYear(1), today, "今年");
        }
        if (text.contains("本月")) {
            return new DateRange(today.withDayOfMonth(1), today, "本月");
        }
        if (text.contains("上月")) {
            LocalDate first = today.minusMonths(1).withDayOfMonth(1);
            LocalDate last = first.withDayOfMonth(first.lengthOfMonth());
            return new DateRange(first, last, "上月");
        }
        if (text.contains("近半年") || text.contains("最近半年")) {
            return new DateRange(today.minusMonths(6).plusDays(1), today, "近半年");
        }
        if (text.contains("近半个月") || text.contains("最近半个月") || text.contains("半个月")) {
            return new DateRange(today.minusDays(14), today, "近半个月");
        }
        return new DateRange(today.minusDays(29), today, "近30天");
    }
    private LocalDate parseLocalDate(String text) {
        if (!StringUtils.hasText(text)) {
            return null;
        }
        return LocalDate.parse(text.trim(), DATE_FMT);
    }
    private Date toDate(LocalDate localDate) {
        return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
    private String formatDate(Date date) {
        if (date == null) {
            return "";
        }
        return DATE_FMT.format(date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
    }
    private boolean tenantMatched(Long dataTenantId, Long userTenantId) {
        if (userTenantId == null) {
            return true;
        }
        return Objects.equals(dataTenantId, userTenantId);
    }
    private <T> void applyTenantFilter(LambdaQueryWrapper<T> wrapper, Long tenantId, com.baomidou.mybatisplus.core.toolkit.support.SFunction<T, Long> field) {
        if (tenantId != null) {
            wrapper.eq(field, tenantId);
        }
    }
    private <T> void applyDeptFilter(LambdaQueryWrapper<T> wrapper, Long deptId, com.baomidou.mybatisplus.core.toolkit.support.SFunction<T, Long> field) {
        if (deptId != null) {
            wrapper.eq(field, deptId);
        }
    }
    private LoginUser currentLoginUser(String memoryId) {
        LoginUser loginUser = aiSessionUserContext.get(memoryId);
        if (loginUser != null) {
            return loginUser;
        }
        return SecurityUtils.getLoginUser();
    }
    private int normalizeLimit(Integer limit) {
        if (limit == null || limit <= 0) {
            return DEFAULT_LIMIT;
        }
        return Math.min(limit, MAX_LIMIT);
    }
    private String safe(Object value) {
        return value == null ? "" : String.valueOf(value).replace('\n', ' ').replace('\r', ' ');
    }
    private <T> List<T> defaultList(List<T> list) {
        return list == null ? List.of() : list;
    }
    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 record DateRange(LocalDate start, LocalDate end, String label) {
    }
}
src/main/resources/purchase-agent-prompt.txt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
你是企业采购智能助理。
你的目标是帮助用户快速完成采购相关信息查询与解读。
工作规则:
1. ä¼˜å…ˆè°ƒç”¨å·¥å…·å‡½æ•°èŽ·å–é‡‡è´­å°è´¦ã€ä»˜æ¬¾ã€å‘ç¥¨ã€é€€è´§ç­‰ç»“æž„åŒ–æ•°æ®ã€‚
2. é‡åˆ°â€œç»Ÿè®¡/分析/报表/今年/本月/近XX天”等需求,优先给出统计结果和关键结论。
3. æ— æ³•直接得出结论时,明确说明缺少哪些字段或筛选条件。
4. ç»“果用简洁中文回答,先给结论,再给关键数据点。
5. ä¸è¦ç¼–造采购数据,所有结论必须基于工具返回。