AdminKnowledgeController.java 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409
  1. package com.pharmacopoeia.controller;
  2. import com.fasterxml.jackson.databind.ObjectMapper;
  3. import org.springframework.http.MediaType;
  4. import java.nio.charset.StandardCharsets;
  5. import org.springframework.http.HttpHeaders;
  6. import com.pharmacopoeia.config.UnifiedResponse;
  7. import com.pharmacopoeia.dto.*;
  8. import com.pharmacopoeia.service.AdminKnowledgeService;
  9. import com.pharmacopoeia.service.BrandRecommendService;
  10. import jakarta.validation.Valid;
  11. import org.springframework.http.ResponseEntity;
  12. import org.springframework.web.bind.annotation.*;
  13. import org.springframework.web.multipart.MultipartFile;
  14. import java.util.*;
  15. @UnifiedResponse
  16. @RestController
  17. @RequestMapping("/api/v1/admin/knowledge")
  18. public class AdminKnowledgeController {
  19. private final AdminKnowledgeService service;
  20. private final BrandRecommendService brandRecommendService;
  21. private final ObjectMapper mapper = new ObjectMapper();
  22. public AdminKnowledgeController(AdminKnowledgeService service,
  23. BrandRecommendService brandRecommendService) {
  24. this.service = service;
  25. this.brandRecommendService = brandRecommendService;
  26. }
  27. // ==================== 药品管理 ====================
  28. @GetMapping("/drugs")
  29. public ResponseEntity<Map<String, Object>> listDrugs(
  30. @RequestParam(required = false) String keyword,
  31. @RequestParam(required = false) String category,
  32. @RequestParam(defaultValue = "1") int page,
  33. @RequestParam(defaultValue = "20") int pageSize) {
  34. return ResponseEntity.ok(service.listDrugs(keyword, category, page, pageSize));
  35. }
  36. @GetMapping("/drugs/{drugId}")
  37. public ResponseEntity<Map<String, Object>> getDrug(@PathVariable String drugId) {
  38. return ResponseEntity.ok(service.getDrug(drugId)
  39. .map(d -> Map.of("data", (Object) d))
  40. .orElse(Map.of("message", "数据入库后可用", "drug_id", drugId)));
  41. }
  42. @PostMapping("/drugs")
  43. public ResponseEntity<Map<String, Object>> createDrug(@RequestBody DrugCreateRequest req) {
  44. return ResponseEntity.ok(service.createDrug(objectToMap(req)));
  45. }
  46. @PutMapping("/drugs/{drugId}")
  47. public ResponseEntity<Map<String, Object>> updateDrug(@PathVariable String drugId,
  48. @RequestBody DrugUpdateRequest req) {
  49. return ResponseEntity.ok(service.updateDrug(drugId, objectToMap(req)));
  50. }
  51. @DeleteMapping("/drugs/{drugId}")
  52. public ResponseEntity<Map<String, Object>> deleteDrug(@PathVariable String drugId) {
  53. return ResponseEntity.ok(service.deleteDrug(drugId));
  54. }
  55. @PostMapping("/drugs/import")
  56. public ResponseEntity<Map<String, Object>> importDrugs(@RequestBody ImportRequest req) {
  57. int count = parseCount(req);
  58. return ResponseEntity.ok(Map.of(
  59. "message", "导入任务已提交",
  60. "total", count,
  61. "success", 0,
  62. "failed", 0,
  63. "note", "Phase 2 实现异步批量写入 + 向量化"
  64. ));
  65. }
  66. @PostMapping("/drugs/reindex")
  67. public ResponseEntity<Map<String, Object>> reindexDrugs(@RequestBody ReindexRequest req) {
  68. return ResponseEntity.ok(Map.of(
  69. "message", "重建索引任务已提交",
  70. "collection", req.getCollection(),
  71. "drug_ids", req.getDrugIds(),
  72. "status", "pending",
  73. "note", "Phase 2 实现:重新 chunk → 向量化 → 写入 pgvector"
  74. ));
  75. }
  76. // ==================== 知识点管理 ====================
  77. @GetMapping("/knowledge-points")
  78. public ResponseEntity<Map<String, Object>> listKnowledgePoints(
  79. @RequestParam(required = false) String subject,
  80. @RequestParam(required = false) String chapterId,
  81. @RequestParam(required = false) Integer difficulty,
  82. @RequestParam(required = false) String frequency,
  83. @RequestParam(defaultValue = "1") int page,
  84. @RequestParam(defaultValue = "20") int pageSize) {
  85. return ResponseEntity.ok(service.listKnowledgePoints(subject, chapterId, difficulty, frequency, page, pageSize));
  86. }
  87. @GetMapping("/knowledge-points/{pointId}")
  88. public ResponseEntity<Map<String, Object>> getKnowledgePoint(@PathVariable String pointId) {
  89. return ResponseEntity.ok(service.getKnowledgePoint(pointId)
  90. .<Map<String, Object>>map(kp -> Map.of(
  91. "point_id", kp.getPointId(),
  92. "subject", kp.getSubject(),
  93. "title", kp.getTitle(),
  94. "content", kp.getContent()
  95. ))
  96. .orElse(Map.of("message", "数据入库后可用", "point_id", pointId)));
  97. }
  98. @PostMapping("/knowledge-points")
  99. public ResponseEntity<Map<String, Object>> createKnowledgePoint(@RequestBody KnowledgePointCreateRequest req) {
  100. return ResponseEntity.ok(service.createKnowledgePoint(objectToMap(req)));
  101. }
  102. @PutMapping("/knowledge-points/{pointId}")
  103. public ResponseEntity<Map<String, Object>> updateKnowledgePoint(@PathVariable String pointId,
  104. @RequestBody KnowledgePointUpdateRequest req) {
  105. return ResponseEntity.ok(service.updateKnowledgePoint(pointId, objectToMap(req)));
  106. }
  107. @DeleteMapping("/knowledge-points/{pointId}")
  108. public ResponseEntity<Map<String, Object>> deleteKnowledgePoint(@PathVariable String pointId) {
  109. return ResponseEntity.ok(service.deleteKnowledgePoint(pointId));
  110. }
  111. @PostMapping("/knowledge-points/import")
  112. public ResponseEntity<Map<String, Object>> importKnowledgePoints(@RequestBody ImportRequest req) {
  113. int count = parseCount(req);
  114. return ResponseEntity.ok(Map.of(
  115. "message", "导入任务已提交",
  116. "total", count,
  117. "success", 0,
  118. "failed", 0
  119. ));
  120. }
  121. // ==================== 题库管理 ====================
  122. @GetMapping("/questions")
  123. public ResponseEntity<Map<String, Object>> listQuestions(
  124. @RequestParam(required = false) String subject,
  125. @RequestParam(required = false) String chapterId,
  126. @RequestParam(required = false) String questionType,
  127. @RequestParam(required = false) Integer difficulty,
  128. @RequestParam(required = false) Boolean audited,
  129. @RequestParam(defaultValue = "1") int page,
  130. @RequestParam(defaultValue = "20") int pageSize) {
  131. return ResponseEntity.ok(service.listQuestions(subject, chapterId, questionType, difficulty, audited, page, pageSize));
  132. }
  133. @GetMapping("/questions/{questionId}")
  134. public ResponseEntity<Map<String, Object>> getQuestion(@PathVariable String questionId) {
  135. return ResponseEntity.ok(service.getQuestion(questionId)
  136. .<Map<String, Object>>map(q -> Map.of(
  137. "question_id", q.getQuestionId(),
  138. "content", q.getContent(),
  139. "answer", q.getAnswer()
  140. ))
  141. .orElse(Map.of("message", "数据入库后可用", "question_id", questionId)));
  142. }
  143. @PostMapping("/questions")
  144. public ResponseEntity<Map<String, Object>> createQuestion(@RequestBody QuestionCreateRequest req) {
  145. return ResponseEntity.ok(service.createQuestion(objectToMap(req)));
  146. }
  147. @PutMapping("/questions/{questionId}")
  148. public ResponseEntity<Map<String, Object>> updateQuestion(@PathVariable String questionId,
  149. @RequestBody QuestionUpdateRequest req) {
  150. return ResponseEntity.ok(service.updateQuestion(questionId, objectToMap(req)));
  151. }
  152. @DeleteMapping("/questions/{questionId}")
  153. public ResponseEntity<Map<String, Object>> deleteQuestion(@PathVariable String questionId) {
  154. return ResponseEntity.ok(service.deleteQuestion(questionId));
  155. }
  156. @PostMapping("/questions/batch-audit")
  157. public ResponseEntity<Map<String, Object>> batchAudit(@RequestBody BatchAuditRequest req) {
  158. return ResponseEntity.ok(service.batchAudit(req.getQuestionIds(), req.getAudited()));
  159. }
  160. @PostMapping("/questions/import")
  161. public ResponseEntity<Map<String, Object>> importQuestions(@RequestBody ImportRequest req) {
  162. int count = parseCount(req);
  163. return ResponseEntity.ok(Map.of(
  164. "message", "导入任务已提交",
  165. "total", count,
  166. "success", 0,
  167. "failed", 0
  168. ));
  169. }
  170. @PostMapping("/questions/generate")
  171. public ResponseEntity<Map<String, Object>> generateQuestions(
  172. @RequestParam String pointId,
  173. @RequestParam(defaultValue = "10") int count,
  174. @RequestParam(defaultValue = "A") String questionType) {
  175. return ResponseEntity.ok(Map.of(
  176. "message", "AI 出题任务已提交",
  177. "point_id", pointId,
  178. "count", count,
  179. "question_type", questionType,
  180. "status", "pending",
  181. "note", "Phase 2 实现:LLM 根据知识点生成题目"
  182. ));
  183. }
  184. // ==================== 快捷提问标签管理(管理端,需要 JWT) ====================
  185. @GetMapping("/quick-asks")
  186. public ResponseEntity<Map<String, Object>> listAllQuickAsks(
  187. @RequestParam(defaultValue = "1") int page,
  188. @RequestParam(defaultValue = "20") int pageSize) {
  189. return ResponseEntity.ok(service.listAllQuickAsks(page, pageSize));
  190. }
  191. @PostMapping("/quick-asks")
  192. public ResponseEntity<Map<String, Object>> createQuickAsk(@Valid @RequestBody QuickAskCreateRequest body) {
  193. String question = body.getQuestion() != null ? body.getQuestion() : body.getLabel();
  194. return ResponseEntity.ok(service.createQuickAsk(body.getLabel().trim(), question, body.getSortOrder()));
  195. }
  196. @PutMapping("/quick-asks/{id}")
  197. public ResponseEntity<Map<String, Object>> updateQuickAsk(
  198. @PathVariable int id, @Valid @RequestBody QuickAskUpdateRequest body) {
  199. return ResponseEntity.ok(service.updateQuickAsk(id, body.getLabel(),
  200. body.getQuestion(), body.getSortOrder(), body.getIsActive()));
  201. }
  202. @DeleteMapping("/quick-asks/{id}")
  203. public ResponseEntity<Map<String, Object>> deleteQuickAsk(@PathVariable int id) {
  204. return ResponseEntity.ok(service.deleteQuickAsk(id));
  205. }
  206. @PutMapping("/quick-asks/reorder")
  207. public ResponseEntity<Map<String, Object>> reorderQuickAsks(@RequestBody List<Map<String, Object>> items) {
  208. return ResponseEntity.ok(service.reorderQuickAsks(items));
  209. }
  210. @GetMapping("/quick-asks/audit-log")
  211. public ResponseEntity<Map<String, Object>> listQuickAskAuditLog(
  212. @RequestParam(defaultValue = "1") int page,
  213. @RequestParam(defaultValue = "20") int pageSize,
  214. @RequestParam(required = false) String action,
  215. @RequestParam(required = false) String startDate,
  216. @RequestParam(required = false) String endDate) {
  217. return ResponseEntity.ok(service.listQuickAskAuditLog(page, pageSize, action, startDate, endDate));
  218. }
  219. // ==================== 品牌管理 ====================
  220. @GetMapping("/brands")
  221. public ResponseEntity<Map<String, Object>> listBrands(
  222. @RequestParam(required = false) String keyword,
  223. @RequestParam(defaultValue = "1") int page,
  224. @RequestParam(defaultValue = "20") int pageSize) {
  225. return ResponseEntity.ok(brandRecommendService.listBrands(keyword, page, pageSize));
  226. }
  227. @GetMapping("/brands/{id}")
  228. public ResponseEntity<Map<String, Object>> getBrand(@PathVariable int id) {
  229. return ResponseEntity.ok(brandRecommendService.getBrand(id));
  230. }
  231. @PostMapping("/brands")
  232. public ResponseEntity<Map<String, Object>> createBrand(@RequestBody Map<String, Object> body) {
  233. return ResponseEntity.ok(brandRecommendService.createBrand(body));
  234. }
  235. @PutMapping("/brands/{id}")
  236. public ResponseEntity<Map<String, Object>> updateBrand(
  237. @PathVariable int id, @RequestBody Map<String, Object> body) {
  238. return ResponseEntity.ok(brandRecommendService.updateBrand(id, body));
  239. }
  240. @DeleteMapping("/brands/{id}")
  241. public ResponseEntity<Map<String, Object>> deleteBrand(@PathVariable int id) {
  242. return ResponseEntity.ok(brandRecommendService.deleteBrand(id));
  243. }
  244. @PostMapping("/brands/import")
  245. public ResponseEntity<Map<String, Object>> importBrands(@RequestParam("file") MultipartFile file) {
  246. try {
  247. String csv = new String(file.getBytes(), StandardCharsets.UTF_8);
  248. return ResponseEntity.ok(brandRecommendService.importBrandsCsv(csv));
  249. } catch (Exception e) {
  250. return ResponseEntity.ok(Map.of("ok", false, "message", e.getMessage()));
  251. }
  252. }
  253. @GetMapping("/brands/export")
  254. public ResponseEntity<?> exportBrands(@RequestParam(required = false) String ids) {
  255. Set<Integer> idSet = parseIds(ids);
  256. if (idSet.isEmpty()) {
  257. return ResponseEntity.badRequest().body(Map.of("ok", false, "message", "请选择要导出的品牌"));
  258. }
  259. byte[] bytes = brandRecommendService.exportBrandsCsvBytes(idSet);
  260. return ResponseEntity.ok()
  261. .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=brands.csv")
  262. .contentType(MediaType.parseMediaType("text/csv; charset=UTF-8"))
  263. .body(bytes);
  264. }
  265. // ==================== 品牌推荐规则管理 ====================
  266. @GetMapping("/brand-recommend-rules")
  267. public ResponseEntity<Map<String, Object>> listBrandRecommendRules(
  268. @RequestParam(required = false) String keyword,
  269. @RequestParam(defaultValue = "1") int page,
  270. @RequestParam(defaultValue = "20") int pageSize) {
  271. return ResponseEntity.ok(brandRecommendService.listRules(keyword, page, pageSize));
  272. }
  273. @PostMapping("/brand-recommend-rules")
  274. public ResponseEntity<Map<String, Object>> createBrandRecommendRule(
  275. @RequestBody Map<String, Object> body) {
  276. String keyword = (String) body.get("keyword");
  277. int brandId = body.get("brand_id") instanceof Number n ? n.intValue() : 0;
  278. int tier = body.get("tier") instanceof Number n ? n.intValue() : 1;
  279. return ResponseEntity.ok(brandRecommendService.createRule(keyword, brandId, tier));
  280. }
  281. @PutMapping("/brand-recommend-rules/{id}")
  282. public ResponseEntity<Map<String, Object>> updateBrandRecommendRule(
  283. @PathVariable int id,
  284. @RequestBody Map<String, Object> body) {
  285. String keyword = (String) body.get("keyword");
  286. Integer brandId = body.get("brand_id") instanceof Number n ? n.intValue() : null;
  287. Integer tier = body.get("tier") instanceof Number n ? n.intValue() : null;
  288. Boolean isActive = body.get("is_active") instanceof Boolean b ? b : null;
  289. return ResponseEntity.ok(brandRecommendService.updateRule(id, keyword, brandId, tier, isActive));
  290. }
  291. @DeleteMapping("/brand-recommend-rules/{id}")
  292. public ResponseEntity<Map<String, Object>> deleteBrandRecommendRule(@PathVariable int id) {
  293. return ResponseEntity.ok(brandRecommendService.deleteRule(id));
  294. }
  295. @PatchMapping("/brand-recommend-rules/{id}/toggle")
  296. public ResponseEntity<Map<String, Object>> toggleBrandRecommendRule(@PathVariable int id,
  297. @RequestBody(required = false) Map<String, Boolean> body) {
  298. Boolean isActive = body != null ? body.get("is_active") : null;
  299. return ResponseEntity.ok(brandRecommendService.toggleRule(id, isActive));
  300. }
  301. @PostMapping("/brand-recommend-rules/import")
  302. public ResponseEntity<Map<String, Object>> importBrandRecommendRules(
  303. @RequestParam("file") MultipartFile file) {
  304. try {
  305. String csv = new String(file.getBytes(), StandardCharsets.UTF_8);
  306. return ResponseEntity.ok(brandRecommendService.importRulesCsv(csv));
  307. } catch (Exception e) {
  308. return ResponseEntity.ok(Map.of("ok", false, "message", e.getMessage()));
  309. }
  310. }
  311. private static Set<Integer> parseIds(String ids) {
  312. if (ids == null || ids.isBlank()) {
  313. return Set.of();
  314. }
  315. return java.util.Arrays.stream(ids.split(","))
  316. .map(String::trim)
  317. .filter(s -> !s.isEmpty())
  318. .map(Integer::parseInt)
  319. .collect(java.util.stream.Collectors.toSet());
  320. }
  321. @GetMapping("/brand-recommend-rules/export")
  322. public ResponseEntity<?> exportBrandRecommendRules(
  323. @RequestParam(required = false) String ids) {
  324. Set<Integer> idSet = parseIds(ids);
  325. if (idSet.isEmpty()) {
  326. return ResponseEntity.badRequest().body(Map.of("ok", false, "message", "请选择要导出的推荐规则"));
  327. }
  328. byte[] bytes = brandRecommendService.exportRulesCsvBytes(idSet);
  329. return ResponseEntity.ok()
  330. .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=brand_recommend_rules.csv")
  331. .contentType(MediaType.parseMediaType("text/csv; charset=UTF-8"))
  332. .body(bytes);
  333. }
  334. // ==================== Helpers ====================
  335. private int parseCount(ImportRequest req) {
  336. try {
  337. if ("json".equals(req.getFormat()) && req.getData() != null) {
  338. var items = mapper.readTree(req.getData());
  339. return items.isArray() ? items.size() : 0;
  340. }
  341. if ("csv".equals(req.getFormat()) && req.getData() != null) {
  342. return req.getData().split("\n").length - 1; // rough count
  343. }
  344. } catch (Exception ignored) {}
  345. return 0;
  346. }
  347. private Map<String, Object> objectToMap(Object obj) {
  348. try {
  349. @SuppressWarnings("unchecked")
  350. Map<String, Object> map = mapper.convertValue(obj, Map.class);
  351. return map;
  352. } catch (Exception e) {
  353. return Map.of();
  354. }
  355. }
  356. }