package com.pharmacopoeia.service; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.pharmacopoeia.entity.Drug; import com.pharmacopoeia.entity.KnowledgePoint; import com.pharmacopoeia.entity.Question; import com.pharmacopoeia.entity.QuickAsk; import com.pharmacopoeia.entity.QuickAskAuditLog; import com.pharmacopoeia.repository.DrugRepository; import com.pharmacopoeia.repository.KnowledgePointRepository; import com.pharmacopoeia.repository.QuestionRepository; import com.pharmacopoeia.repository.QuickAskAuditLogRepository; import com.pharmacopoeia.repository.QuickAskRepository; import com.pharmacopoeia.util.IpUtils; import org.springframework.data.domain.PageRequest; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import java.util.*; import java.time.Instant; import java.time.LocalDate; import java.time.ZoneOffset; import java.util.function.Consumer; @Service public class AdminKnowledgeService { private final DrugRepository drugRepository; private final KnowledgePointRepository knowledgePointRepository; private final QuestionRepository questionRepository; private final JdbcTemplate jdbc; private final ObjectMapper mapper = new ObjectMapper(); private final QuickAskRepository quickAskRepository; private final QuickAskAuditLogRepository auditLogRepository; public AdminKnowledgeService(DrugRepository drugRepository, KnowledgePointRepository knowledgePointRepository, QuestionRepository questionRepository, QuickAskRepository quickAskRepository, QuickAskAuditLogRepository auditLogRepository, JdbcTemplate jdbc) { this.drugRepository = drugRepository; this.knowledgePointRepository = knowledgePointRepository; this.questionRepository = questionRepository; this.quickAskRepository = quickAskRepository; this.auditLogRepository = auditLogRepository; this.jdbc = jdbc; } // ==================== Drug CRUD ==================== public Map listDrugs(String keyword, String category, int page, int pageSize) { var pageable = PageRequest.of(page - 1, pageSize); var result = (keyword != null && !keyword.isBlank()) ? drugRepository.searchByKeyword(keyword, pageable) : (category != null && !category.isBlank()) ? drugRepository.findByCategoryAndIsActiveTrue(category, pageable) : drugRepository.findByIsActiveTrue(pageable); var items = result.getContent().stream() .map(this::drugToMap) .collect(java.util.stream.Collectors.toList()); Map response = new LinkedHashMap<>(); response.put("items", items); response.put("total", result.getTotalElements()); response.put("page", page); response.put("page_size", pageSize); return response; } public Optional> getDrug(String drugId) { return drugRepository.findByDrugId(drugId).map(this::drugToMap); } @Transactional public Map createDrug(Map req) { Drug drug = Drug.builder() .drugId((String) req.get("drugId")) .name((String) req.get("name")) .nameEn((String) req.get("nameEn")) .pinyin((String) req.get("pinyin")) .category((String) req.get("category")) .subcategory((String) req.get("subcategory")) .approvalNumber((String) req.get("approvalNumber")) .sourceVersion((String) req.get("sourceVersion")) .sourceVolume((String) req.get("sourceVolume")) .sourcePage((String) req.get("sourcePage")) .isActive(true) .build(); drug.setSections(castToMap(req.get("sections"))); drugRepository.save(drug); return toMap("drug_id", drug.getDrugId(), "已创建"); } @Transactional public Map updateDrug(String drugId, Map req) { return drugRepository.findByDrugId(drugId).map(d -> { applyStrIfPresent(req, "name", d::setName); applyStrIfPresent(req, "nameEn", d::setNameEn); applyStrIfPresent(req, "pinyin", d::setPinyin); applyStrIfPresent(req, "category", d::setCategory); applyStrIfPresent(req, "subcategory", d::setSubcategory); applyStrIfPresent(req, "approvalNumber", d::setApprovalNumber); applyStrIfPresent(req, "sourceVersion", d::setSourceVersion); applyStrIfPresent(req, "sourceVolume", d::setSourceVolume); applyStrIfPresent(req, "sourcePage", d::setSourcePage); applyIfPresent(req, "sections", v -> d.setSections(castToMap(v))); applyIfPresent(req, "isActive", v -> d.setIsActive((Boolean) v)); drugRepository.save(d); return toMap("drug_id", drugId, "已更新"); }).orElseGet(() -> toMap("drug_id", drugId, "药品不存在")); } @Transactional public Map deleteDrug(String drugId) { return drugRepository.findByDrugId(drugId).map(d -> { d.setIsActive(false); drugRepository.save(d); return toMap("drug_id", drugId, "已标记删除"); }).orElseGet(() -> toMap("drug_id", drugId, "药品不存在")); } // ==================== KnowledgePoint CRUD ==================== public Map listKnowledgePoints(String subject, String chapterId, Integer difficulty, String frequency, int page, int pageSize) { StringBuilder sql = new StringBuilder("SELECT * FROM knowledge_points WHERE 1=1"); List params = new ArrayList<>(); appendFilters(sql, params, new SqlFilter("subject", subject), new SqlFilter("chapter_id", chapterId), new SqlFilter("difficulty", difficulty), new SqlFilter("frequency", frequency)); sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?"); params.add(pageSize); params.add((page - 1) * pageSize); List> items = jdbc.queryForList(sql.toString(), params.toArray()); Map response = new LinkedHashMap<>(); response.put("items", items); response.put("page", page); response.put("page_size", pageSize); return response; } public Optional getKnowledgePoint(String pointId) { return knowledgePointRepository.findByPointId(pointId); } @Transactional public Map createKnowledgePoint(Map req) { KnowledgePoint kp = KnowledgePoint.builder() .pointId((String) req.get("pointId")) .subject((String) req.get("subject")) .chapterId((String) req.get("chapterId")) .chapterName((String) req.get("chapterName")) .title((String) req.get("title")) .content((String) req.get("content")) .difficulty(req.get("difficulty") != null ? (Integer) req.get("difficulty") : 1) .frequency((String) req.get("frequency")) .source((String) req.get("source")) .build(); writeJsonIfPresent(req, "relatedDrugs", kp::setRelatedDrugs); knowledgePointRepository.save(kp); return toMap("point_id", kp.getPointId(), "已创建"); } @Transactional public Map updateKnowledgePoint(String pointId, Map req) { return knowledgePointRepository.findByPointId(pointId).map(kp -> { applyStrIfPresent(req, "chapterName", kp::setChapterName); applyStrIfPresent(req, "title", kp::setTitle); applyStrIfPresent(req, "content", kp::setContent); applyIfPresent(req, "difficulty", v -> kp.setDifficulty((Integer) v)); applyStrIfPresent(req, "frequency", kp::setFrequency); applyStrIfPresent(req, "source", kp::setSource); writeJsonIfPresent(req, "relatedDrugs", kp::setRelatedDrugs); knowledgePointRepository.save(kp); return toMap("point_id", pointId, "已更新"); }).orElseGet(() -> toMap("point_id", pointId, "知识点不存在")); } @Transactional public Map deleteKnowledgePoint(String pointId) { knowledgePointRepository.findByPointId(pointId).ifPresent(knowledgePointRepository::delete); return toMap("point_id", pointId, "已删除"); } // ==================== Question CRUD ==================== public Map listQuestions(String subject, String chapterId, String questionType, Integer difficulty, Boolean audited, int page, int pageSize) { StringBuilder sql = new StringBuilder("SELECT * FROM questions WHERE 1=1"); List params = new ArrayList<>(); appendFilters(sql, params, new SqlFilter("subject", subject), new SqlFilter("chapter_id", chapterId), new SqlFilter("question_type", questionType), new SqlFilter("difficulty", difficulty), new SqlFilter("audited", audited)); sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?"); params.add(pageSize); params.add((page - 1) * pageSize); List> items = jdbc.queryForList(sql.toString(), params.toArray()); Map response = new LinkedHashMap<>(); response.put("items", items); response.put("page", page); response.put("page_size", pageSize); return response; } public Optional getQuestion(String questionId) { return questionRepository.findByQuestionId(questionId); } @Transactional public Map createQuestion(Map req) { Question q = Question.builder() .questionId((String) req.get("questionId")) .questionType((String) req.get("questionType")) .subject((String) req.get("subject")) .chapterId((String) req.get("chapterId")) .difficulty(req.get("difficulty") != null ? (Integer) req.get("difficulty") : 1) .content((String) req.get("content")) .answer((String) req.get("answer")) .explanation((String) req.get("explanation")) .frequency((String) req.get("frequency")) .source((String) req.get("source")) .audited(false) .correctCount(0) .attemptCount(0) .build(); writeJsonIfPresent(req, "options", q::setOptions); writeJsonIfPresent(req, "knowledgePointIds", q::setKnowledgePointIds); questionRepository.save(q); return toMap("question_id", q.getQuestionId(), "已创建"); } @Transactional public Map updateQuestion(String questionId, Map req) { return questionRepository.findByQuestionId(questionId).map(q -> { applyStrIfPresent(req, "questionType", q::setQuestionType); applyStrIfPresent(req, "subject", q::setSubject); applyStrIfPresent(req, "chapterId", q::setChapterId); applyIfPresent(req, "difficulty", v -> q.setDifficulty((Integer) v)); applyStrIfPresent(req, "content", q::setContent); applyStrIfPresent(req, "answer", q::setAnswer); applyStrIfPresent(req, "explanation", q::setExplanation); applyStrIfPresent(req, "frequency", q::setFrequency); applyStrIfPresent(req, "source", q::setSource); applyIfPresent(req, "audited", v -> q.setAudited((Boolean) v)); writeJsonIfPresent(req, "options", q::setOptions); writeJsonIfPresent(req, "knowledgePointIds", q::setKnowledgePointIds); questionRepository.save(q); return toMap("question_id", questionId, "已更新"); }).orElseGet(() -> toMap("question_id", questionId, "题目不存在")); } @Transactional public Map deleteQuestion(String questionId) { questionRepository.findByQuestionId(questionId).ifPresent(questionRepository::delete); return toMap("question_id", questionId, "已删除"); } @Transactional public Map batchAudit(List questionIds, boolean audited) { for (String qid : questionIds) { questionRepository.findByQuestionId(qid).ifPresent(q -> { q.setAudited(audited); questionRepository.save(q); }); } Map r = new LinkedHashMap<>(); r.put("message", "批量审核完成"); r.put("question_ids", questionIds); r.put("audited", audited); return r; } // ==================== 快捷提问标签管理 ==================== /** 获取所有激活的标签,按 sort_order 排序(公开接口,无需鉴权),最多返回6个 */ public List listQuickAsks() { return quickAskRepository.findByIsActiveTrueOrderBySortOrderAscIdAsc( org.springframework.data.domain.PageRequest.of(0, MAX_ACTIVE_QUICK_ASKS)); } /** 管理端:分页获取所有标签(含禁用) */ public Map listAllQuickAsks(int page, int pageSize) { var pageable = PageRequest.of(page - 1, pageSize); var result = quickAskRepository.findAllByOrderBySortOrderAscIdAsc(pageable); return Map.of( "items", result.getContent(), "page", page, "page_size", pageSize, "total", result.getTotalElements(), "total_pages", result.getTotalPages() ); } private static final int MAX_ACTIVE_QUICK_ASKS = 6; /** 新增标签 */ @Transactional public Map createQuickAsk(String label, String question, int sortOrder) { String trimmedLabel = label.trim(); if (quickAskRepository.countByLabel(trimmedLabel) > 0) { throw new IllegalArgumentException("推荐位文字不能重复"); } if (quickAskRepository.count() >= MAX_ACTIVE_QUICK_ASKS) { throw new IllegalArgumentException("最多只能添加" + MAX_ACTIVE_QUICK_ASKS + "个推荐位"); } String operator = getCurrentOperator(); QuickAsk qa = QuickAsk.builder() .label(trimmedLabel) .question(question != null ? question.trim() : trimmedLabel) .sortOrder(sortOrder) .isActive(true) .createdBy(operator) .build(); qa = quickAskRepository.save(qa); writeAuditLog(qa.getId(), "CREATE", operator, qa.getLabel(), qa.getQuestion(), qa.getSortOrder(), qa.getIsActive()); return Map.of("ok", true, "id", qa.getId(), "message", "已创建"); } /** 更新标签 */ @Transactional public Map updateQuickAsk(int id, String label, String question, Integer sortOrder, Boolean isActive) { var opt = quickAskRepository.findById(id); if (opt.isEmpty()) { throw new IllegalArgumentException("未找到"); } QuickAsk qa = opt.get(); if (label != null && !label.isBlank()) { String trimmedLabel = label.trim(); if (quickAskRepository.countByLabelAndIdNot(trimmedLabel, id) > 0) { throw new IllegalArgumentException("推荐位文字不能重复"); } qa.setLabel(trimmedLabel); } Optional.ofNullable(question).ifPresent(v -> qa.setQuestion(v.trim())); Optional.ofNullable(sortOrder).ifPresent(qa::setSortOrder); Optional.ofNullable(isActive).ifPresent(qa::setIsActive); String operator = getCurrentOperator(); qa.setUpdatedBy(operator); quickAskRepository.save(qa); writeAuditLog(id, "UPDATE", operator, qa.getLabel(), qa.getQuestion(), qa.getSortOrder(), qa.getIsActive()); long activeCount = quickAskRepository.countByIsActiveTrue(); String message = "已更新"; if (Boolean.TRUE.equals(qa.getIsActive()) && activeCount > MAX_ACTIVE_QUICK_ASKS) { message = "已更新(当前上架推荐位" + activeCount + "个,超过" + MAX_ACTIVE_QUICK_ASKS + "个上限)"; } return Map.of("ok", true, "id", id, "message", message); } /** 删除标签 */ @Transactional public Map deleteQuickAsk(int id) { var opt = quickAskRepository.findById(id); if (opt.isEmpty()) { throw new IllegalArgumentException("未找到"); } QuickAsk qa = opt.get(); String operator = getCurrentOperator(); writeAuditLog(id, "DELETE", operator, qa.getLabel(), qa.getQuestion(), qa.getSortOrder(), qa.getIsActive()); quickAskRepository.delete(qa); return Map.of("ok", true, "message", "已删除"); } // ==================== 排序管理 ==================== public Map reorderQuickAsks(List> items) { String operator = getCurrentOperator(); int updated = 0; for (Map item : items) { int id = ((Number) item.get("id")).intValue(); int sortOrder = ((Number) item.get("sortOrder")).intValue(); updated += quickAskRepository.updateSortOrder(id, sortOrder, operator); } return Map.of("ok", updated > 0, "message", "已更新 " + updated + " 条排序"); } private void writeAuditLog(int quickAskId, String action, String operator, String label, String question, Integer sortOrder, Boolean isActive) { auditLogRepository.save(QuickAskAuditLog.builder() .quickAskId(quickAskId).action(action) .label(label).question(question) .sortOrder(sortOrder != null ? sortOrder : 0) .isActive(isActive != null ? isActive : true) .operator(operator) .ip(getClientIp()) .build()); } /** 从当前请求中获取客户端 IP */ private static String getClientIp() { var attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attrs != null) { return IpUtils.getClientIp(attrs.getRequest()); } return "unknown"; } public Map listQuickAskAuditLog(int page, int pageSize, String action, String startDate, String endDate) { var pageable = PageRequest.of(page - 1, pageSize); Instant startTime = startDate != null && !startDate.isBlank() ? LocalDate.parse(startDate).atStartOfDay(ZoneOffset.UTC).toInstant() : null; Instant endTime = endDate != null && !endDate.isBlank() ? LocalDate.parse(endDate).plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant() : null; var result = auditLogRepository.findByFilters( action != null && !action.isBlank() ? action : null, startTime, endTime, pageable); return Map.of( "items", result.getContent(), "page", page, "page_size", pageSize, "total", result.getTotalElements(), "total_pages", result.getTotalPages() ); } // ==================== Helpers ==================== /** 从 SecurityContext 获取当前操作员标识 */ private String getCurrentOperator() { var auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.isAuthenticated() && !"anonymousUser".equals(auth.getPrincipal())) { return auth.getName(); } return "anonymous"; } @SuppressWarnings("unchecked") private Map castToMap(Object obj) { if (obj instanceof Map) { return (Map) obj; } return new HashMap<>(); } // ---- 通用 if-reduction helpers ---- /** map 中存在 key 且非 null 时执行 setter */ private void applyIfPresent(Map req, String key, Consumer setter) { Optional.ofNullable(req.get(key)).ifPresent(setter); } /** map 中存在 key 且为 非空字符串 时执行 setter */ private void applyStrIfPresent(Map req, String key, Consumer setter) { Optional.ofNullable(req.get(key)) .map(Object::toString) .filter(s -> !s.isBlank()) .ifPresent(setter); } /** 将 map 中的对象序列化为 JSON,非 null 时执行 setter */ private void writeJsonIfPresent(Map req, String key, Consumer setter) { Optional.ofNullable(req.get(key)).ifPresent(val -> { try { setter.accept(mapper.writeValueAsString(val)); } catch (JsonProcessingException ignored) {} }); } /** SQL 动态筛选字段 */ private record SqlFilter(String column, Object value) {} private void appendFilters(StringBuilder sql, List params, SqlFilter... filters) { for (var f : filters) { if (f.value == null || f.value instanceof String s && s.isBlank()) { continue; } sql.append(" AND ").append(f.column).append(" = ?"); params.add(f.value); } } private Map drugToMap(Drug d) { Map m = new LinkedHashMap<>(); m.put("drug_id", d.getDrugId()); m.put("name", d.getName()); m.put("name_en", d.getNameEn()); m.put("pinyin", d.getPinyin()); m.put("category", d.getCategory()); m.put("subcategory", d.getSubcategory()); m.put("approval_number", d.getApprovalNumber()); m.put("sections", d.getSections()); m.put("source_version", d.getSourceVersion()); m.put("source_volume", d.getSourceVolume()); m.put("source_page", d.getSourcePage()); m.put("is_active", d.getIsActive()); m.put("created_at", d.getCreatedAt()); m.put("updated_at", d.getUpdatedAt()); return m; } private Map toMap(String k1, Object v1, Object v2) { Map m = new LinkedHashMap<>(); m.put(k1, v1); m.put("message", v2); return m; } }