| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524 |
- 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<String, Object> 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<String, Object> 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<Map<String, Object>> getDrug(String drugId) {
- return drugRepository.findByDrugId(drugId).map(this::drugToMap);
- }
- @Transactional
- public Map<String, Object> createDrug(Map<String, Object> 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<String, Object> updateDrug(String drugId, Map<String, Object> 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<String, Object> 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<String, Object> 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<Object> 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<Map<String, Object>> items = jdbc.queryForList(sql.toString(), params.toArray());
- Map<String, Object> response = new LinkedHashMap<>();
- response.put("items", items);
- response.put("page", page);
- response.put("page_size", pageSize);
- return response;
- }
- public Optional<KnowledgePoint> getKnowledgePoint(String pointId) {
- return knowledgePointRepository.findByPointId(pointId);
- }
- @Transactional
- public Map<String, Object> createKnowledgePoint(Map<String, Object> 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<String, Object> updateKnowledgePoint(String pointId, Map<String, Object> 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<String, Object> deleteKnowledgePoint(String pointId) {
- knowledgePointRepository.findByPointId(pointId).ifPresent(knowledgePointRepository::delete);
- return toMap("point_id", pointId, "已删除");
- }
- // ==================== Question CRUD ====================
- public Map<String, Object> 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<Object> 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<Map<String, Object>> items = jdbc.queryForList(sql.toString(), params.toArray());
- Map<String, Object> response = new LinkedHashMap<>();
- response.put("items", items);
- response.put("page", page);
- response.put("page_size", pageSize);
- return response;
- }
- public Optional<Question> getQuestion(String questionId) {
- return questionRepository.findByQuestionId(questionId);
- }
- @Transactional
- public Map<String, Object> createQuestion(Map<String, Object> 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<String, Object> updateQuestion(String questionId, Map<String, Object> 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<String, Object> deleteQuestion(String questionId) {
- questionRepository.findByQuestionId(questionId).ifPresent(questionRepository::delete);
- return toMap("question_id", questionId, "已删除");
- }
- @Transactional
- public Map<String, Object> batchAudit(List<String> questionIds, boolean audited) {
- for (String qid : questionIds) {
- questionRepository.findByQuestionId(qid).ifPresent(q -> {
- q.setAudited(audited);
- questionRepository.save(q);
- });
- }
- Map<String, Object> r = new LinkedHashMap<>();
- r.put("message", "批量审核完成");
- r.put("question_ids", questionIds);
- r.put("audited", audited);
- return r;
- }
- // ==================== 快捷提问标签管理 ====================
- /** 获取所有激活的标签,按 sort_order 排序(公开接口,无需鉴权),最多返回6个 */
- public List<QuickAsk> listQuickAsks() {
- return quickAskRepository.findByIsActiveTrueOrderBySortOrderAscIdAsc(
- org.springframework.data.domain.PageRequest.of(0, MAX_ACTIVE_QUICK_ASKS));
- }
- /** 管理端:分页获取所有标签(含禁用) */
- public Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> reorderQuickAsks(List<Map<String, Object>> items) {
- String operator = getCurrentOperator();
- int updated = 0;
- for (Map<String, Object> 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<String, Object> 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<String, Object> castToMap(Object obj) {
- if (obj instanceof Map) {
- return (Map<String, Object>) obj;
- }
- return new HashMap<>();
- }
- // ---- 通用 if-reduction helpers ----
- /** map 中存在 key 且非 null 时执行 setter */
- private void applyIfPresent(Map<String, Object> req, String key, Consumer<Object> setter) {
- Optional.ofNullable(req.get(key)).ifPresent(setter);
- }
- /** map 中存在 key 且为 非空字符串 时执行 setter */
- private void applyStrIfPresent(Map<String, Object> req, String key, Consumer<String> setter) {
- Optional.ofNullable(req.get(key))
- .map(Object::toString)
- .filter(s -> !s.isBlank())
- .ifPresent(setter);
- }
- /** 将 map 中的对象序列化为 JSON,非 null 时执行 setter */
- private void writeJsonIfPresent(Map<String, Object> req, String key, Consumer<String> 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<Object> 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<String, Object> drugToMap(Drug d) {
- Map<String, Object> 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<String, Object> toMap(String k1, Object v1, Object v2) {
- Map<String, Object> m = new LinkedHashMap<>();
- m.put(k1, v1);
- m.put("message", v2);
- return m;
- }
- }
|