package com.pharmacopoeia.controller; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.http.MediaType; import java.nio.charset.StandardCharsets; import org.springframework.http.HttpHeaders; import com.pharmacopoeia.config.UnifiedResponse; import com.pharmacopoeia.dto.*; import com.pharmacopoeia.service.AdminKnowledgeService; import com.pharmacopoeia.service.BrandRecommendService; import jakarta.validation.Valid; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.util.*; @UnifiedResponse @RestController @RequestMapping("/api/v1/admin/knowledge") public class AdminKnowledgeController { private final AdminKnowledgeService service; private final BrandRecommendService brandRecommendService; private final ObjectMapper mapper = new ObjectMapper(); public AdminKnowledgeController(AdminKnowledgeService service, BrandRecommendService brandRecommendService) { this.service = service; this.brandRecommendService = brandRecommendService; } // ==================== 药品管理 ==================== @GetMapping("/drugs") public ResponseEntity> listDrugs( @RequestParam(required = false) String keyword, @RequestParam(required = false) String category, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int pageSize) { return ResponseEntity.ok(service.listDrugs(keyword, category, page, pageSize)); } @GetMapping("/drugs/{drugId}") public ResponseEntity> getDrug(@PathVariable String drugId) { return ResponseEntity.ok(service.getDrug(drugId) .map(d -> Map.of("data", (Object) d)) .orElse(Map.of("message", "数据入库后可用", "drug_id", drugId))); } @PostMapping("/drugs") public ResponseEntity> createDrug(@RequestBody DrugCreateRequest req) { return ResponseEntity.ok(service.createDrug(objectToMap(req))); } @PutMapping("/drugs/{drugId}") public ResponseEntity> updateDrug(@PathVariable String drugId, @RequestBody DrugUpdateRequest req) { return ResponseEntity.ok(service.updateDrug(drugId, objectToMap(req))); } @DeleteMapping("/drugs/{drugId}") public ResponseEntity> deleteDrug(@PathVariable String drugId) { return ResponseEntity.ok(service.deleteDrug(drugId)); } @PostMapping("/drugs/import") public ResponseEntity> importDrugs(@RequestBody ImportRequest req) { int count = parseCount(req); return ResponseEntity.ok(Map.of( "message", "导入任务已提交", "total", count, "success", 0, "failed", 0, "note", "Phase 2 实现异步批量写入 + 向量化" )); } @PostMapping("/drugs/reindex") public ResponseEntity> reindexDrugs(@RequestBody ReindexRequest req) { return ResponseEntity.ok(Map.of( "message", "重建索引任务已提交", "collection", req.getCollection(), "drug_ids", req.getDrugIds(), "status", "pending", "note", "Phase 2 实现:重新 chunk → 向量化 → 写入 pgvector" )); } // ==================== 知识点管理 ==================== @GetMapping("/knowledge-points") public ResponseEntity> listKnowledgePoints( @RequestParam(required = false) String subject, @RequestParam(required = false) String chapterId, @RequestParam(required = false) Integer difficulty, @RequestParam(required = false) String frequency, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int pageSize) { return ResponseEntity.ok(service.listKnowledgePoints(subject, chapterId, difficulty, frequency, page, pageSize)); } @GetMapping("/knowledge-points/{pointId}") public ResponseEntity> getKnowledgePoint(@PathVariable String pointId) { return ResponseEntity.ok(service.getKnowledgePoint(pointId) .>map(kp -> Map.of( "point_id", kp.getPointId(), "subject", kp.getSubject(), "title", kp.getTitle(), "content", kp.getContent() )) .orElse(Map.of("message", "数据入库后可用", "point_id", pointId))); } @PostMapping("/knowledge-points") public ResponseEntity> createKnowledgePoint(@RequestBody KnowledgePointCreateRequest req) { return ResponseEntity.ok(service.createKnowledgePoint(objectToMap(req))); } @PutMapping("/knowledge-points/{pointId}") public ResponseEntity> updateKnowledgePoint(@PathVariable String pointId, @RequestBody KnowledgePointUpdateRequest req) { return ResponseEntity.ok(service.updateKnowledgePoint(pointId, objectToMap(req))); } @DeleteMapping("/knowledge-points/{pointId}") public ResponseEntity> deleteKnowledgePoint(@PathVariable String pointId) { return ResponseEntity.ok(service.deleteKnowledgePoint(pointId)); } @PostMapping("/knowledge-points/import") public ResponseEntity> importKnowledgePoints(@RequestBody ImportRequest req) { int count = parseCount(req); return ResponseEntity.ok(Map.of( "message", "导入任务已提交", "total", count, "success", 0, "failed", 0 )); } // ==================== 题库管理 ==================== @GetMapping("/questions") public ResponseEntity> listQuestions( @RequestParam(required = false) String subject, @RequestParam(required = false) String chapterId, @RequestParam(required = false) String questionType, @RequestParam(required = false) Integer difficulty, @RequestParam(required = false) Boolean audited, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int pageSize) { return ResponseEntity.ok(service.listQuestions(subject, chapterId, questionType, difficulty, audited, page, pageSize)); } @GetMapping("/questions/{questionId}") public ResponseEntity> getQuestion(@PathVariable String questionId) { return ResponseEntity.ok(service.getQuestion(questionId) .>map(q -> Map.of( "question_id", q.getQuestionId(), "content", q.getContent(), "answer", q.getAnswer() )) .orElse(Map.of("message", "数据入库后可用", "question_id", questionId))); } @PostMapping("/questions") public ResponseEntity> createQuestion(@RequestBody QuestionCreateRequest req) { return ResponseEntity.ok(service.createQuestion(objectToMap(req))); } @PutMapping("/questions/{questionId}") public ResponseEntity> updateQuestion(@PathVariable String questionId, @RequestBody QuestionUpdateRequest req) { return ResponseEntity.ok(service.updateQuestion(questionId, objectToMap(req))); } @DeleteMapping("/questions/{questionId}") public ResponseEntity> deleteQuestion(@PathVariable String questionId) { return ResponseEntity.ok(service.deleteQuestion(questionId)); } @PostMapping("/questions/batch-audit") public ResponseEntity> batchAudit(@RequestBody BatchAuditRequest req) { return ResponseEntity.ok(service.batchAudit(req.getQuestionIds(), req.getAudited())); } @PostMapping("/questions/import") public ResponseEntity> importQuestions(@RequestBody ImportRequest req) { int count = parseCount(req); return ResponseEntity.ok(Map.of( "message", "导入任务已提交", "total", count, "success", 0, "failed", 0 )); } @PostMapping("/questions/generate") public ResponseEntity> generateQuestions( @RequestParam String pointId, @RequestParam(defaultValue = "10") int count, @RequestParam(defaultValue = "A") String questionType) { return ResponseEntity.ok(Map.of( "message", "AI 出题任务已提交", "point_id", pointId, "count", count, "question_type", questionType, "status", "pending", "note", "Phase 2 实现:LLM 根据知识点生成题目" )); } // ==================== 快捷提问标签管理(管理端,需要 JWT) ==================== @GetMapping("/quick-asks") public ResponseEntity> listAllQuickAsks( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int pageSize) { return ResponseEntity.ok(service.listAllQuickAsks(page, pageSize)); } @PostMapping("/quick-asks") public ResponseEntity> createQuickAsk(@Valid @RequestBody QuickAskCreateRequest body) { String question = body.getQuestion() != null ? body.getQuestion() : body.getLabel(); return ResponseEntity.ok(service.createQuickAsk(body.getLabel().trim(), question, body.getSortOrder())); } @PutMapping("/quick-asks/{id}") public ResponseEntity> updateQuickAsk( @PathVariable int id, @Valid @RequestBody QuickAskUpdateRequest body) { return ResponseEntity.ok(service.updateQuickAsk(id, body.getLabel(), body.getQuestion(), body.getSortOrder(), body.getIsActive())); } @DeleteMapping("/quick-asks/{id}") public ResponseEntity> deleteQuickAsk(@PathVariable int id) { return ResponseEntity.ok(service.deleteQuickAsk(id)); } @PutMapping("/quick-asks/reorder") public ResponseEntity> reorderQuickAsks(@RequestBody List> items) { return ResponseEntity.ok(service.reorderQuickAsks(items)); } @GetMapping("/quick-asks/audit-log") public ResponseEntity> listQuickAskAuditLog( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int pageSize, @RequestParam(required = false) String action, @RequestParam(required = false) String startDate, @RequestParam(required = false) String endDate) { return ResponseEntity.ok(service.listQuickAskAuditLog(page, pageSize, action, startDate, endDate)); } // ==================== 品牌管理 ==================== @GetMapping("/brands") public ResponseEntity> listBrands( @RequestParam(required = false) String keyword, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int pageSize) { return ResponseEntity.ok(brandRecommendService.listBrands(keyword, page, pageSize)); } @GetMapping("/brands/{id}") public ResponseEntity> getBrand(@PathVariable int id) { return ResponseEntity.ok(brandRecommendService.getBrand(id)); } @PostMapping("/brands") public ResponseEntity> createBrand(@RequestBody Map body) { return ResponseEntity.ok(brandRecommendService.createBrand(body)); } @PutMapping("/brands/{id}") public ResponseEntity> updateBrand( @PathVariable int id, @RequestBody Map body) { return ResponseEntity.ok(brandRecommendService.updateBrand(id, body)); } @DeleteMapping("/brands/{id}") public ResponseEntity> deleteBrand(@PathVariable int id) { return ResponseEntity.ok(brandRecommendService.deleteBrand(id)); } @PostMapping("/brands/import") public ResponseEntity> importBrands(@RequestParam("file") MultipartFile file) { try { String csv = new String(file.getBytes(), StandardCharsets.UTF_8); return ResponseEntity.ok(brandRecommendService.importBrandsCsv(csv)); } catch (Exception e) { return ResponseEntity.ok(Map.of("ok", false, "message", e.getMessage())); } } @GetMapping("/brands/export") public ResponseEntity exportBrands(@RequestParam(required = false) String ids) { Set idSet = parseIds(ids); if (idSet.isEmpty()) { return ResponseEntity.badRequest().body(Map.of("ok", false, "message", "请选择要导出的品牌")); } byte[] bytes = brandRecommendService.exportBrandsCsvBytes(idSet); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=brands.csv") .contentType(MediaType.parseMediaType("text/csv; charset=UTF-8")) .body(bytes); } // ==================== 品牌推荐规则管理 ==================== @GetMapping("/brand-recommend-rules") public ResponseEntity> listBrandRecommendRules( @RequestParam(required = false) String keyword, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int pageSize) { return ResponseEntity.ok(brandRecommendService.listRules(keyword, page, pageSize)); } @PostMapping("/brand-recommend-rules") public ResponseEntity> createBrandRecommendRule( @RequestBody Map body) { String keyword = (String) body.get("keyword"); int brandId = body.get("brand_id") instanceof Number n ? n.intValue() : 0; int tier = body.get("tier") instanceof Number n ? n.intValue() : 1; return ResponseEntity.ok(brandRecommendService.createRule(keyword, brandId, tier)); } @PutMapping("/brand-recommend-rules/{id}") public ResponseEntity> updateBrandRecommendRule( @PathVariable int id, @RequestBody Map body) { String keyword = (String) body.get("keyword"); Integer brandId = body.get("brand_id") instanceof Number n ? n.intValue() : null; Integer tier = body.get("tier") instanceof Number n ? n.intValue() : null; Boolean isActive = body.get("is_active") instanceof Boolean b ? b : null; return ResponseEntity.ok(brandRecommendService.updateRule(id, keyword, brandId, tier, isActive)); } @DeleteMapping("/brand-recommend-rules/{id}") public ResponseEntity> deleteBrandRecommendRule(@PathVariable int id) { return ResponseEntity.ok(brandRecommendService.deleteRule(id)); } @PatchMapping("/brand-recommend-rules/{id}/toggle") public ResponseEntity> toggleBrandRecommendRule(@PathVariable int id, @RequestBody(required = false) Map body) { Boolean isActive = body != null ? body.get("is_active") : null; return ResponseEntity.ok(brandRecommendService.toggleRule(id, isActive)); } @PostMapping("/brand-recommend-rules/import") public ResponseEntity> importBrandRecommendRules( @RequestParam("file") MultipartFile file) { try { String csv = new String(file.getBytes(), StandardCharsets.UTF_8); return ResponseEntity.ok(brandRecommendService.importRulesCsv(csv)); } catch (Exception e) { return ResponseEntity.ok(Map.of("ok", false, "message", e.getMessage())); } } private static Set parseIds(String ids) { if (ids == null || ids.isBlank()) { return Set.of(); } return java.util.Arrays.stream(ids.split(",")) .map(String::trim) .filter(s -> !s.isEmpty()) .map(Integer::parseInt) .collect(java.util.stream.Collectors.toSet()); } @GetMapping("/brand-recommend-rules/export") public ResponseEntity exportBrandRecommendRules( @RequestParam(required = false) String ids) { Set idSet = parseIds(ids); if (idSet.isEmpty()) { return ResponseEntity.badRequest().body(Map.of("ok", false, "message", "请选择要导出的推荐规则")); } byte[] bytes = brandRecommendService.exportRulesCsvBytes(idSet); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=brand_recommend_rules.csv") .contentType(MediaType.parseMediaType("text/csv; charset=UTF-8")) .body(bytes); } // ==================== Helpers ==================== private int parseCount(ImportRequest req) { try { if ("json".equals(req.getFormat()) && req.getData() != null) { var items = mapper.readTree(req.getData()); return items.isArray() ? items.size() : 0; } if ("csv".equals(req.getFormat()) && req.getData() != null) { return req.getData().split("\n").length - 1; // rough count } } catch (Exception ignored) {} return 0; } private Map objectToMap(Object obj) { try { @SuppressWarnings("unchecked") Map map = mapper.convertValue(obj, Map.class); return map; } catch (Exception e) { return Map.of(); } } }