package com.wok.supportbot.service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.wok.supportbot.dao.KnowledgeFaqMapper; import com.wok.supportbot.entity.KnowledgeFaq; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; import java.util.concurrent.CompletableFuture; /** * FAQ 知识库 CRUD 服务 */ @Service @Slf4j public class FaqService { @Autowired private KnowledgeFaqMapper faqMapper; @Autowired private JdbcTemplate jdbcTemplate; @Autowired private FaqMatchEngine faqMatchEngine; private final ObjectMapper objectMapper = new ObjectMapper(); // ==================== 分页查询 ==================== /** * 分页查询 FAQ 列表 * * @param page 页码(从1开始) * @param size 每页条数 * @param keyword 关键词搜索(question LIKE,可选) * @param category 分类过滤(可选) * @param status 状态过滤(可选) * @return 分页结果 Map {total, records} */ public Map list(int page, int size, String keyword, Long categoryId, String status) { QueryWrapper wrapper = new QueryWrapper<>(); if (keyword != null && !keyword.isBlank()) { wrapper.like("question", keyword.trim()); } if (categoryId != null) { wrapper.eq("category_id", categoryId); } if (status != null && !status.isBlank()) { wrapper.eq("status", status.trim()); } Long total = faqMapper.selectCount(wrapper); wrapper.orderByDesc("priority").orderByDesc("create_time"); wrapper.last("LIMIT " + size + " OFFSET " + (page - 1) * size); List records = faqMapper.selectList(wrapper); Map result = new HashMap<>(); result.put("total", total); result.put("records", records); return result; } // ==================== 新增 ==================== /** * 新增 FAQ,新增后异步计算向量 */ @Transactional(rollbackFor = Exception.class) public KnowledgeFaq create(KnowledgeFaq faq) { // 设置默认值 if (faq.getStatus() == null || faq.getStatus().isBlank()) { faq.setStatus("ENABLED"); } if (faq.getPriority() == null) { faq.setPriority(0); } if (faq.getHitCount() == null) { faq.setHitCount(0L); } if (faq.getSource() == null || faq.getSource().isBlank()) { faq.setSource("manual"); } if (faq.getSimilarQuestions() == null) { faq.setSimilarQuestions("[]"); } faq.setCreateTime(new Date()); faq.setUpdateTime(new Date()); faqMapper.insert(faq); log.info("FAQ 已创建: id={}, question={}", faq.getId(), faq.getQuestion()); // 异步计算向量 CompletableFuture.runAsync(() -> faqMatchEngine.computeAndSaveEmbedding(faq.getId(), faq.getQuestion())); return faq; } // ==================== 修改 ==================== /** * 修改 FAQ,修改后重新计算向量 */ @Transactional(rollbackFor = Exception.class) public KnowledgeFaq update(Long id, KnowledgeFaq faq) { KnowledgeFaq existing = faqMapper.selectById(id); if (existing == null) { throw new IllegalArgumentException("FAQ 不存在: id=" + id); } // 更新非空字段 if (faq.getQuestion() != null) { existing.setQuestion(faq.getQuestion()); } if (faq.getAnswer() != null) { existing.setAnswer(faq.getAnswer()); } if (faq.getSimilarQuestions() != null) { existing.setSimilarQuestions(faq.getSimilarQuestions()); } if (faq.getCategory() != null) { existing.setCategory(faq.getCategory()); } if (faq.getCategoryId() != null) { existing.setCategoryId(faq.getCategoryId()); } if (faq.getStatus() != null) { existing.setStatus(faq.getStatus()); } if (faq.getPriority() != null) { existing.setPriority(faq.getPriority()); } existing.setUpdateTime(new Date()); faqMapper.updateById(existing); log.info("FAQ 已更新: id={}", id); // 问题文本变更时重新计算向量 CompletableFuture.runAsync(() -> faqMatchEngine.computeAndSaveEmbedding(id, existing.getQuestion())); return existing; } // ==================== 删除 ==================== /** * 逻辑删除 FAQ,同时删除 faq_embedding 中对应记录 */ @Transactional(rollbackFor = Exception.class) public void delete(Long id) { KnowledgeFaq existing = faqMapper.selectById(id); if (existing == null) { throw new IllegalArgumentException("FAQ 不存在: id=" + id); } // 逻辑删除 FAQ faqMapper.deleteById(id); // 物理删除对应的向量记录 jdbcTemplate.update("DELETE FROM faq_embedding WHERE faq_id = ?", id); log.info("FAQ 已删除: id={}", id); } // ==================== 启用/禁用 ==================== /** * 切换 FAQ 启用/禁用状态 */ @Transactional(rollbackFor = Exception.class) public void toggleStatus(Long id, String status) { KnowledgeFaq existing = faqMapper.selectById(id); if (existing == null) { throw new IllegalArgumentException("FAQ 不存在: id=" + id); } if (!"ENABLED".equals(status) && !"DISABLED".equals(status)) { throw new IllegalArgumentException("无效的状态值: " + status + ",仅支持 ENABLED/DISABLED"); } existing.setStatus(status); existing.setUpdateTime(new Date()); faqMapper.updateById(existing); log.info("FAQ 状态已切换: id={}, status={}", id, status); } // ==================== 批量导入 ==================== /** * 批量导入 FAQ(Excel 解析后的数据),批量计算向量 */ @Transactional(rollbackFor = Exception.class) public int batchImport(List faqs) { if (faqs == null || faqs.isEmpty()) { return 0; } List importedIds = new ArrayList<>(); for (KnowledgeFaq faq : faqs) { // 设置默认值 if (faq.getStatus() == null || faq.getStatus().isBlank()) { faq.setStatus("ENABLED"); } if (faq.getPriority() == null) { faq.setPriority(0); } if (faq.getHitCount() == null) { faq.setHitCount(0L); } if (faq.getSource() == null || faq.getSource().isBlank()) { faq.setSource("import"); } if (faq.getSimilarQuestions() == null) { faq.setSimilarQuestions("[]"); } faq.setCreateTime(new Date()); faq.setUpdateTime(new Date()); faqMapper.insert(faq); importedIds.add(faq.getId()); } log.info("FAQ 批量导入完成: count={}", importedIds.size()); // 异步批量计算向量 faqMatchEngine.batchComputeEmbeddings(importedIds); return importedIds.size(); } // ==================== 导出 ==================== /** * 导出所有启用的 FAQ */ public List exportAll() { QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("status", "ENABLED"); wrapper.orderByDesc("priority").orderByDesc("hit_count"); return faqMapper.selectList(wrapper); } // ==================== 统计 ==================== /** * 获取 FAQ 匹配统计信息 * * @return 统计数据 Map */ public Map getStats() { Map stats = new HashMap<>(); // 总 FAQ 数 QueryWrapper totalWrapper = new QueryWrapper<>(); Long totalCount = faqMapper.selectCount(totalWrapper); stats.put("totalCount", totalCount); // 启用数 QueryWrapper enabledWrapper = new QueryWrapper<>(); enabledWrapper.eq("status", "ENABLED"); Long enabledCount = faqMapper.selectCount(enabledWrapper); stats.put("enabledCount", enabledCount); // 总命中次数 Long totalHits = jdbcTemplate.queryForObject( "SELECT COALESCE(SUM(hit_count), 0) FROM knowledge_faq WHERE is_delete = false", Long.class ); stats.put("totalHits", totalHits); // Top10 热门 FAQ List> topFaqs = jdbcTemplate.queryForList( "SELECT id, question, hit_count, category, category_id FROM knowledge_faq " + "WHERE is_delete = false AND status = 'ENABLED' " + "ORDER BY hit_count DESC LIMIT 10" ); stats.put("topFaqs", topFaqs); return stats; } // ==================== 手动重算向量 ==================== /** * 手动重新计算某条 FAQ 的向量 */ public void recomputeEmbedding(Long id) { KnowledgeFaq existing = faqMapper.selectById(id); if (existing == null) { throw new IllegalArgumentException("FAQ 不存在: id=" + id); } faqMatchEngine.computeAndSaveEmbedding(id, existing.getQuestion()); log.info("FAQ 向量已重新计算: id={}", id); } }