liuchengsen 4 недель назад
Родитель
Сommit
643a3c5a40

+ 3 - 0
backend-java/src/main/java/com/pharmacopoeia/repository/DrugChunkRepository.java

@@ -12,4 +12,7 @@ public interface DrugChunkRepository extends JpaRepository<DrugChunk, Long> {
 
     @Query("SELECT COUNT(c) FROM DrugChunk c WHERE c.vec IS NOT NULL")
     long countWithVector();
+
+    @Query("SELECT COUNT(DISTINCT c.drugId) FROM DrugChunk c WHERE c.vec IS NOT NULL")
+    long countDistinctDrugsWithVector();
 }

+ 4 - 0
backend-java/src/main/java/com/pharmacopoeia/repository/KnowledgePointRepository.java

@@ -2,6 +2,7 @@ package com.pharmacopoeia.repository;
 
 import com.pharmacopoeia.entity.KnowledgePoint;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
 
 import java.util.List;
 import java.util.Optional;
@@ -10,4 +11,7 @@ public interface KnowledgePointRepository extends JpaRepository<KnowledgePoint,
     Optional<KnowledgePoint> findByPointId(String pointId);
     List<KnowledgePoint> findBySubject(String subject);
     List<KnowledgePoint> findBySubjectAndChapterId(String subject, String chapterId);
+
+    @Query("SELECT kp.subject, COUNT(kp) FROM KnowledgePoint kp GROUP BY kp.subject ORDER BY COUNT(kp) DESC")
+    List<Object[]> countBySubjectGrouped();
 }

+ 5 - 4
backend-java/src/main/java/com/pharmacopoeia/repository/QuestionRepository.java

@@ -4,13 +4,14 @@ import com.pharmacopoeia.entity.Question;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.Pageable;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
 
 import java.util.List;
 import java.util.Optional;
 
-public interface QuestionRepository extends JpaRepository<Question, Long> {
+public interface QuestionRepository extends JpaRepository<Question, Long>,
+        JpaSpecificationExecutor<Question> {
     Optional<Question> findByQuestionId(String questionId);
-    Page<Question> findBySubjectAndChapterId(String subject, String chapterId, Pageable pageable);
-    Page<Question> findByAuditedTrue(Pageable pageable);
-    List<Question> findBySubjectAndChapterIdAndAuditedTrue(String subject, String chapterId);
+    long countByAuditedTrue();
+    long countByAuditedFalse();
 }

+ 136 - 134
backend-java/src/main/java/com/pharmacopoeia/service/AdminKnowledgeService.java

@@ -6,6 +6,7 @@ import com.pharmacopoeia.entity.Drug;
 import com.pharmacopoeia.entity.KnowledgePoint;
 import com.pharmacopoeia.entity.Question;
 import com.pharmacopoeia.repository.DrugRepository;
+import com.pharmacopoeia.repository.DrugChunkRepository;
 import com.pharmacopoeia.repository.KnowledgePointRepository;
 import com.pharmacopoeia.repository.QuestionRepository;
 import com.pharmacopoeia.repository.QuickAskRepository;
@@ -21,11 +22,14 @@ import org.springframework.transaction.annotation.Transactional;
 
 
 import java.util.*;
+import java.util.function.Consumer;
+import java.time.Instant;
 
 @Service
 public class AdminKnowledgeService {
 
     private final DrugRepository drugRepository;
+    private final DrugChunkRepository drugChunkRepository;
     private final KnowledgePointRepository knowledgePointRepository;
     private final QuestionRepository questionRepository;
     private final JdbcTemplate jdbc;
@@ -35,12 +39,14 @@ public class AdminKnowledgeService {
     private final QuickAskAuditLogRepository auditLogRepository;
 
     public AdminKnowledgeService(DrugRepository drugRepository,
+                                  DrugChunkRepository drugChunkRepository,
                                   KnowledgePointRepository knowledgePointRepository,
                                   QuestionRepository questionRepository,
                                   QuickAskRepository quickAskRepository,
                                   QuickAskAuditLogRepository auditLogRepository,
                                   JdbcTemplate jdbc) {
         this.drugRepository = drugRepository;
+        this.drugChunkRepository = drugChunkRepository;
         this.knowledgePointRepository = knowledgePointRepository;
         this.questionRepository = questionRepository;
         this.quickAskRepository = quickAskRepository;
@@ -91,26 +97,26 @@ public class AdminKnowledgeService {
                 .build();
         drug.setSections(castToMap(req.get("sections")));
         drugRepository.save(drug);
-        return toMap("drug_id", drug.getDrugId(), "message", "已创建");
+        return toMap("drug_id", drug.getDrugId(), "已创建");
     }
 
     @Transactional
     public Map<String, Object> updateDrug(String drugId, Map<String, Object> req) {
         return drugRepository.findByDrugId(drugId).map(d -> {
-            if (req.containsKey("name")) d.setName((String) req.get("name"));
-            if (req.containsKey("nameEn")) d.setNameEn((String) req.get("nameEn"));
-            if (req.containsKey("pinyin")) d.setPinyin((String) req.get("pinyin"));
-            if (req.containsKey("category")) d.setCategory((String) req.get("category"));
-            if (req.containsKey("subcategory")) d.setSubcategory((String) req.get("subcategory"));
-            if (req.containsKey("approvalNumber")) d.setApprovalNumber((String) req.get("approvalNumber"));
-            if (req.containsKey("sections")) d.setSections(castToMap(req.get("sections")));
-            if (req.containsKey("sourceVersion")) d.setSourceVersion((String) req.get("sourceVersion"));
-            if (req.containsKey("sourceVolume")) d.setSourceVolume((String) req.get("sourceVolume"));
-            if (req.containsKey("sourcePage")) d.setSourcePage((String) req.get("sourcePage"));
-            if (req.containsKey("isActive")) d.setIsActive((Boolean) req.get("isActive"));
+            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, "message", "已更新");
-        }).orElseGet(() -> toMap("drug_id", drugId, "message", "药品不存在"));
+            return toMap("drug_id", drugId, "已更新");
+        }).orElseGet(() -> toMap("drug_id", drugId, "药品不存在"));
     }
 
     @Transactional
@@ -118,8 +124,8 @@ public class AdminKnowledgeService {
         return drugRepository.findByDrugId(drugId).map(d -> {
             d.setIsActive(false);
             drugRepository.save(d);
-            return toMap("drug_id", drugId, "message", "已标记删除");
-        }).orElseGet(() -> toMap("drug_id", drugId, "message", "药品不存在"));
+            return toMap("drug_id", drugId, "已标记删除");
+        }).orElseGet(() -> toMap("drug_id", drugId, "药品不存在"));
     }
 
     // ==================== KnowledgePoint CRUD ====================
@@ -130,22 +136,11 @@ public class AdminKnowledgeService {
         StringBuilder sql = new StringBuilder("SELECT * FROM knowledge_points WHERE 1=1");
         List<Object> params = new ArrayList<>();
 
-        if (subject != null && !subject.isBlank()) {
-            sql.append(" AND subject = ?");
-            params.add(subject);
-        }
-        if (chapterId != null && !chapterId.isBlank()) {
-            sql.append(" AND chapter_id = ?");
-            params.add(chapterId);
-        }
-        if (difficulty != null) {
-            sql.append(" AND difficulty = ?");
-            params.add(difficulty);
-        }
-        if (frequency != null && !frequency.isBlank()) {
-            sql.append(" AND frequency = ?");
-            params.add(frequency);
-        }
+        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);
@@ -177,34 +172,30 @@ public class AdminKnowledgeService {
                 .frequency((String) req.get("frequency"))
                 .source((String) req.get("source"))
                 .build();
-        if (req.get("relatedDrugs") != null) {
-            try { kp.setRelatedDrugs(mapper.writeValueAsString(req.get("relatedDrugs"))); } catch (JsonProcessingException ignored) {}
-        }
+        writeJsonIfPresent(req, "relatedDrugs", kp::setRelatedDrugs);
         knowledgePointRepository.save(kp);
-        return toMap("point_id", kp.getPointId(), "message", "已创建");
+        return toMap("point_id", kp.getPointId(), "已创建");
     }
 
     @Transactional
     public Map<String, Object> updateKnowledgePoint(String pointId, Map<String, Object> req) {
         return knowledgePointRepository.findByPointId(pointId).map(kp -> {
-            if (req.containsKey("chapterName")) kp.setChapterName((String) req.get("chapterName"));
-            if (req.containsKey("title")) kp.setTitle((String) req.get("title"));
-            if (req.containsKey("content")) kp.setContent((String) req.get("content"));
-            if (req.containsKey("difficulty")) kp.setDifficulty((Integer) req.get("difficulty"));
-            if (req.containsKey("frequency")) kp.setFrequency((String) req.get("frequency"));
-            if (req.containsKey("source")) kp.setSource((String) req.get("source"));
-            if (req.containsKey("relatedDrugs")) {
-                try { kp.setRelatedDrugs(mapper.writeValueAsString(req.get("relatedDrugs"))); } catch (JsonProcessingException ignored) {}
-            }
+            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, "message", "已更新");
-        }).orElseGet(() -> toMap("point_id", pointId, "message", "知识点不存在"));
+            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, "message", "已删除");
+        return toMap("point_id", pointId, "已删除");
     }
 
     // ==================== Question CRUD ====================
@@ -215,26 +206,12 @@ public class AdminKnowledgeService {
         StringBuilder sql = new StringBuilder("SELECT * FROM questions WHERE 1=1");
         List<Object> params = new ArrayList<>();
 
-        if (subject != null && !subject.isBlank()) {
-            sql.append(" AND subject = ?");
-            params.add(subject);
-        }
-        if (chapterId != null && !chapterId.isBlank()) {
-            sql.append(" AND chapter_id = ?");
-            params.add(chapterId);
-        }
-        if (questionType != null && !questionType.isBlank()) {
-            sql.append(" AND question_type = ?");
-            params.add(questionType);
-        }
-        if (difficulty != null) {
-            sql.append(" AND difficulty = ?");
-            params.add(difficulty);
-        }
-        if (audited != null) {
-            sql.append(" AND audited = ?");
-            params.add(audited);
-        }
+        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);
@@ -270,40 +247,36 @@ public class AdminKnowledgeService {
                 .correctCount(0)
                 .attemptCount(0)
                 .build();
-        try {
-            if (req.get("options") != null) q.setOptions(mapper.writeValueAsString(req.get("options")));
-            if (req.get("knowledgePointIds") != null) q.setKnowledgePointIds(mapper.writeValueAsString(req.get("knowledgePointIds")));
-        } catch (JsonProcessingException ignored) {}
+        writeJsonIfPresent(req, "options", q::setOptions);
+        writeJsonIfPresent(req, "knowledgePointIds", q::setKnowledgePointIds);
         questionRepository.save(q);
-        return toMap("question_id", q.getQuestionId(), "message", "已创建");
+        return toMap("question_id", q.getQuestionId(), "已创建");
     }
 
     @Transactional
     public Map<String, Object> updateQuestion(String questionId, Map<String, Object> req) {
         return questionRepository.findByQuestionId(questionId).map(q -> {
-            if (req.containsKey("questionType")) q.setQuestionType((String) req.get("questionType"));
-            if (req.containsKey("subject")) q.setSubject((String) req.get("subject"));
-            if (req.containsKey("chapterId")) q.setChapterId((String) req.get("chapterId"));
-            if (req.containsKey("difficulty")) q.setDifficulty((Integer) req.get("difficulty"));
-            if (req.containsKey("content")) q.setContent((String) req.get("content"));
-            if (req.containsKey("answer")) q.setAnswer((String) req.get("answer"));
-            if (req.containsKey("explanation")) q.setExplanation((String) req.get("explanation"));
-            if (req.containsKey("frequency")) q.setFrequency((String) req.get("frequency"));
-            if (req.containsKey("source")) q.setSource((String) req.get("source"));
-            if (req.containsKey("audited")) q.setAudited((Boolean) req.get("audited"));
-            try {
-                if (req.get("options") != null) q.setOptions(mapper.writeValueAsString(req.get("options")));
-                if (req.get("knowledgePointIds") != null) q.setKnowledgePointIds(mapper.writeValueAsString(req.get("knowledgePointIds")));
-            } catch (JsonProcessingException ignored) {}
+            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, "message", "已更新");
-        }).orElseGet(() -> toMap("question_id", questionId, "message", "题目不存在"));
+            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, "message", "已删除");
+        return toMap("question_id", questionId, "已删除");
     }
 
     @Transactional
@@ -324,20 +297,26 @@ public class AdminKnowledgeService {
     // ==================== Statistics ====================
 
     public Map<String, Object> knowledgeStats() {
-        int totalDrugs = jdbc.queryForObject("SELECT COUNT(*) FROM drugs WHERE is_active = TRUE", Integer.class);
-        int drugsWithChunks = jdbc.queryForObject(
-                "SELECT COUNT(DISTINCT drug_id) FROM drug_chunks WHERE vec IS NOT NULL", Integer.class);
-
-        int totalKPs = jdbc.queryForObject("SELECT COUNT(*) FROM knowledge_points", Integer.class);
-        List<Map<String, Object>> kpBySubject = jdbc.queryForList(
-                "SELECT subject, COUNT(*) as cnt FROM knowledge_points GROUP BY subject ORDER BY cnt DESC");
+        int totalDrugs = (int) drugRepository.countByIsActiveTrue();
+        int drugsWithChunks = (int) drugChunkRepository.countDistinctDrugsWithVector();
+
+        int totalKPs = (int) knowledgePointRepository.count();
+        List<Map<String, Object>> kpBySubject = knowledgePointRepository.countBySubjectGrouped()
+                .stream()
+                .map(row -> {
+                    Map<String, Object> m = new LinkedHashMap<>();
+                    m.put("subject", row[0]);
+                    m.put("cnt", row[1]);
+                    return m;
+                })
+                .collect(java.util.stream.Collectors.toList());
 
-        int totalQs = jdbc.queryForObject("SELECT COUNT(*) FROM questions", Integer.class);
-        int auditedQs = jdbc.queryForObject("SELECT COUNT(*) FROM questions WHERE audited = TRUE", Integer.class);
-        int pendingQs = jdbc.queryForObject("SELECT COUNT(*) FROM questions WHERE audited = FALSE", Integer.class);
+        int totalQs = (int) questionRepository.count();
+        int auditedQs = (int) questionRepository.countByAuditedTrue();
+        int pendingQs = (int) questionRepository.countByAuditedFalse();
 
-        String lastUpdate = jdbc.queryForObject(
-                "SELECT COALESCE(MAX(updated_at)::text, '') FROM drugs WHERE is_active = TRUE", String.class);
+        Instant lastUpdate = drugRepository.findMaxUpdatedAt();
+        String lastUpdateStr = lastUpdate != null ? lastUpdate.toString() : "";
 
         Map<String, Object> drugsStats = new LinkedHashMap<>();
         drugsStats.put("total", totalDrugs);
@@ -356,19 +335,17 @@ public class AdminKnowledgeService {
         Map<String, Object> viStats = new LinkedHashMap<>();
         viStats.put("status", "active");
         viStats.put("collection", "drug_chunks");
-        viStats.put("last_reindex", lastUpdate);
+        viStats.put("last_reindex", lastUpdateStr);
 
         Map<String, Object> result = new LinkedHashMap<>();
         result.put("drugs", drugsStats);
         result.put("knowledge_points", kpStats);
         result.put("questions", qStats);
         result.put("vector_index", viStats);
-        result.put("last_data_update", lastUpdate);
+        result.put("last_data_update", lastUpdateStr);
         return result;
     }
 
-    // ==================== Helpers ====================
-
     // ==================== 快捷提问标签管理 ====================
 
     /** 获取所有激活的标签,按 sort_order 排序(公开接口,无需鉴权) */
@@ -406,23 +383,17 @@ public class AdminKnowledgeService {
                 .createdBy(operator)
                 .build();
         qa = quickAskRepository.save(qa);
-        auditLogRepository.save(QuickAskAuditLog.builder()
-                .quickAskId(qa.getId()).action("CREATE")
-                .label(qa.getLabel()).question(qa.getQuestion())
-                .sortOrder(qa.getSortOrder()).isActive(qa.getIsActive())
-                .operator(operator).build());
+        writeAuditLog(qa.getId(), "CREATE", operator, qa.getLabel(), qa.getQuestion(), qa.getSortOrder(), qa.getIsActive());
         return Map.of("ok", true, "id", qa.getId(), "message", "已创建");
     }
 
-    private String getCurrentOperator() {
-        return "admin";
-    }
-
     /** 更新标签 */
     @Transactional
     public Map<String, Object> updateQuickAsk(int id, String label, String question, Integer sortOrder, Boolean isActive) {
         var opt = quickAskRepository.findById(id);
-        if (opt.isEmpty()) return Map.of("ok", false, "message", "未找到");
+        if (opt.isEmpty()) {
+            return Map.of("ok", false, "message", "未找到");
+        }
         QuickAsk qa = opt.get();
         if (label != null && !label.isBlank()) {
             String trimmedLabel = label.trim();
@@ -431,17 +402,13 @@ public class AdminKnowledgeService {
             }
             qa.setLabel(trimmedLabel);
         }
-        if (question != null) qa.setQuestion(question.trim());
-        if (sortOrder != null) qa.setSortOrder(sortOrder);
-        if (isActive != null) qa.setIsActive(isActive);
+        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);
-        auditLogRepository.save(QuickAskAuditLog.builder()
-                .quickAskId(id).action("UPDATE")
-                .label(qa.getLabel()).question(qa.getQuestion())
-                .sortOrder(qa.getSortOrder()).isActive(qa.getIsActive())
-                .operator(operator).build());
+        writeAuditLog(id, "UPDATE", operator, qa.getLabel(), qa.getQuestion(), qa.getSortOrder(), qa.getIsActive());
         return Map.of("ok", true, "id", id, "message", "已更新");
     }
 
@@ -449,19 +416,17 @@ public class AdminKnowledgeService {
     @Transactional
     public Map<String, Object> deleteQuickAsk(int id) {
         var opt = quickAskRepository.findById(id);
-        if (opt.isEmpty()) return Map.of("ok", false, "message", "未找到");
+        if (opt.isEmpty()) {
+            return Map.of("ok", false, "message", "未找到");
+        }
         QuickAsk qa = opt.get();
         String operator = getCurrentOperator();
-        auditLogRepository.save(QuickAskAuditLog.builder()
-                .quickAskId(id).action("DELETE")
-                .label(qa.getLabel()).question(qa.getQuestion())
-                .sortOrder(qa.getSortOrder()).isActive(qa.getIsActive())
-                .operator(operator).build());
+        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();
@@ -509,10 +474,47 @@ public class AdminKnowledgeService {
 
     @SuppressWarnings("unchecked")
     private Map<String, Object> castToMap(Object obj) {
-        if (obj instanceof Map) return (Map<String, 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());
@@ -532,10 +534,10 @@ public class AdminKnowledgeService {
         return m;
     }
 
-    private Map<String, Object> toMap(String k1, Object v1, String k2, Object v2) {
+    private Map<String, Object> toMap(String k1, Object v1, Object v2) {
         Map<String, Object> m = new LinkedHashMap<>();
         m.put(k1, v1);
-        m.put(k2, v2);
+        m.put("message", v2);
         return m;
     }
 }