BrandRecommendService.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. package com.pharmacopoeia.service;
  2. import com.pharmacopoeia.entity.Brand;
  3. import com.pharmacopoeia.entity.BrandRecommendRule;
  4. import com.pharmacopoeia.repository.BrandRecommendRuleRepository;
  5. import com.pharmacopoeia.repository.BrandRepository;
  6. import org.springframework.stereotype.Service;
  7. import org.springframework.transaction.annotation.Transactional;
  8. import java.util.*;
  9. import java.util.function.Consumer;
  10. import java.util.stream.Collectors;
  11. @Service
  12. public class BrandRecommendService {
  13. private final BrandRecommendRuleRepository ruleRepository;
  14. private final BrandRepository brandRepository;
  15. public BrandRecommendService(BrandRecommendRuleRepository ruleRepository,
  16. BrandRepository brandRepository) {
  17. this.ruleRepository = ruleRepository;
  18. this.brandRepository = brandRepository;
  19. }
  20. // ==================== 层级匹配 ====================
  21. public List<Map<String, Object>> match(List<Map<String, Object>> sources, String fullAnswer) {
  22. // 仅加载 is_active=true 的规则,已停用的不参与匹配
  23. List<BrandRecommendRule> rules = ruleRepository.findByIsActiveTrueOrderByTierAscKeywordAsc();
  24. if (rules.isEmpty()) {
  25. return List.of();
  26. }
  27. Map<Integer, List<BrandRecommendRule>> byTier = rules.stream()
  28. .collect(Collectors.groupingBy(BrandRecommendRule::getTier, LinkedHashMap::new, Collectors.toList()));
  29. Set<String> sourceNames = sources.stream()
  30. .map(s -> (String) s.getOrDefault("name", ""))
  31. .filter(n -> !n.isEmpty())
  32. .collect(Collectors.toSet());
  33. for (Map.Entry<Integer, List<BrandRecommendRule>> entry : byTier.entrySet()) {
  34. List<Map<String, Object>> matches = findMatches(entry.getValue(), sourceNames);
  35. if (!matches.isEmpty()) {
  36. return matches;
  37. }
  38. }
  39. // 全文回退也按 tier 逐层尝试
  40. if (fullAnswer != null && !fullAnswer.isEmpty()) {
  41. for (Map.Entry<Integer, List<BrandRecommendRule>> entry : byTier.entrySet()) {
  42. List<Map<String, Object>> matches = findMatches(entry.getValue(), Set.of(fullAnswer));
  43. if (!matches.isEmpty()) {
  44. return matches;
  45. }
  46. }
  47. }
  48. return List.of();
  49. }
  50. private List<Map<String, Object>> findMatches(List<BrandRecommendRule> rules, Set<String> targets) {
  51. // 一次性加载所有用到的 Brand
  52. Set<Integer> brandIds = rules.stream().map(BrandRecommendRule::getBrandId).collect(Collectors.toSet());
  53. Map<Integer, Brand> brandMap = brandRepository.findAllById(brandIds).stream()
  54. .collect(Collectors.toMap(Brand::getId, b -> b));
  55. Set<Integer> seenRuleIds = new HashSet<>(); // 规则去重(同规则只命中一次)
  56. Set<Integer> seenBrandIds = new HashSet<>(); // 品牌去重(同品牌只返回一次)
  57. List<Map<String, Object>> result = new ArrayList<>();
  58. for (BrandRecommendRule rule : rules) {
  59. for (String target : targets) {
  60. if (target.toLowerCase().contains(rule.getKeyword().toLowerCase())) {
  61. if (seenRuleIds.add(rule.getId()) && seenBrandIds.add(rule.getBrandId())) {
  62. Brand brand = brandMap.get(rule.getBrandId());
  63. result.add(buildBrandResult(rule, brand));
  64. }
  65. break;
  66. }
  67. }
  68. }
  69. return result;
  70. }
  71. private Map<String, Object> buildBrandResult(BrandRecommendRule rule, Brand brand) {
  72. Map<String, Object> m = new LinkedHashMap<>();
  73. m.put("brand_id", rule.getBrandId());
  74. m.put("matched_keyword", rule.getKeyword());
  75. m.put("tier", rule.getTier());
  76. if (brand != null) {
  77. m.put("brand_name", brand.getName());
  78. m.put("description", nullToEmpty(brand.getDescription()));
  79. m.put("function", nullToEmpty(brand.getFunctionIndication()));
  80. m.put("usage_dosage", nullToEmpty(brand.getUsageDosage()));
  81. m.put("contraindication", nullToEmpty(brand.getContraindication()));
  82. m.put("ingredients", nullToEmpty(brand.getIngredients()));
  83. m.put("properties", nullToEmpty(brand.getProperties()));
  84. m.put("specification", nullToEmpty(brand.getSpecification()));
  85. m.put("adverse_reactions", nullToEmpty(brand.getAdverseReactions()));
  86. m.put("precautions", nullToEmpty(brand.getPrecautions()));
  87. m.put("execution_standard", nullToEmpty(brand.getExecutionStandard()));
  88. m.put("storage", nullToEmpty(brand.getStorage()));
  89. m.put("jump_url", nullToEmpty(brand.getJumpUrl()));
  90. }
  91. return m;
  92. }
  93. // ==================== 品牌 CRUD ====================
  94. public Map<String, Object> listBrands(String keyword, int page, int pageSize) {
  95. List<Brand> all = brandRepository.findAllByOrderBySortOrderAsc();
  96. if (keyword != null && !keyword.isBlank()) {
  97. String kw = keyword.toLowerCase();
  98. all = all.stream()
  99. .filter(b -> b.getName().toLowerCase().contains(kw))
  100. .collect(Collectors.toList());
  101. }
  102. int total = all.size();
  103. int from = (page - 1) * pageSize;
  104. int to = Math.min(from + pageSize, total);
  105. List<Brand> pageItems = from < total ? all.subList(from, to) : List.of();
  106. return Map.of(
  107. "items", (Object) pageItems,
  108. "page", page,
  109. "page_size", pageSize,
  110. "total", total,
  111. "total_pages", (int) Math.ceil((double) total / pageSize)
  112. );
  113. }
  114. @Transactional
  115. public Map<String, Object> createBrand(Map<String, Object> body) {
  116. String name = (String) body.get("name");
  117. if (name == null || name.isBlank()) {
  118. throw new IllegalArgumentException("品牌名称不能为空");
  119. }
  120. if (brandRepository.existsByNameAndIdNot(name.trim(), null)) {
  121. throw new IllegalArgumentException("该品牌名称已存在");
  122. }
  123. Brand brand = Brand.builder()
  124. .name(name.trim())
  125. .functionIndication(str(body, "function_indication"))
  126. .usageDosage(str(body, "usage_dosage"))
  127. .contraindication(str(body, "contraindication"))
  128. .ingredients(str(body, "ingredients"))
  129. .properties(str(body, "properties"))
  130. .specification(str(body, "specification"))
  131. .adverseReactions(str(body, "adverse_reactions"))
  132. .precautions(str(body, "precautions"))
  133. .executionStandard(str(body, "execution_standard"))
  134. .storage(str(body, "storage"))
  135. .jumpUrl(str(body, "jump_url"))
  136. .description(str(body, "description"))
  137. .sortOrder(body.get("sort_order") instanceof Number n ? n.intValue() : 0)
  138. .isActive(true)
  139. .build();
  140. brand = brandRepository.save(brand);
  141. return Map.of("ok", true, "id", brand.getId(), "message", "已创建");
  142. }
  143. @Transactional
  144. public Map<String, Object> updateBrand(int id, Map<String, Object> body) {
  145. var opt = brandRepository.findById(id);
  146. if (opt.isEmpty()) throw new IllegalArgumentException("未找到该品牌");
  147. Brand b = opt.get();
  148. String name = (String) body.get("name");
  149. if (name != null && !name.isBlank()) {
  150. if (brandRepository.existsByNameAndIdNot(name.trim(), id)) {
  151. throw new IllegalArgumentException("该品牌名称已存在");
  152. }
  153. b.setName(name.trim());
  154. }
  155. applyStr(body, "function_indication", b::setFunctionIndication);
  156. applyStr(body, "usage_dosage", b::setUsageDosage);
  157. applyStr(body, "contraindication", b::setContraindication);
  158. applyStr(body, "ingredients", b::setIngredients);
  159. applyStr(body, "properties", b::setProperties);
  160. applyStr(body, "specification", b::setSpecification);
  161. applyStr(body, "adverse_reactions", b::setAdverseReactions);
  162. applyStr(body, "precautions", b::setPrecautions);
  163. applyStr(body, "execution_standard", b::setExecutionStandard);
  164. applyStr(body, "storage", b::setStorage);
  165. applyStr(body, "jump_url", b::setJumpUrl);
  166. applyStr(body, "description", b::setDescription);
  167. Optional.ofNullable((Number) body.get("sort_order")).ifPresent(n -> b.setSortOrder(n.intValue()));
  168. Optional.ofNullable((Boolean) body.get("is_active")).ifPresent(b::setIsActive);
  169. brandRepository.save(b);
  170. return Map.of("ok", true, "id", id, "message", "已更新");
  171. }
  172. @Transactional
  173. public Map<String, Object> deleteBrand(int id) {
  174. if (!brandRepository.existsById(id)) throw new IllegalArgumentException("未找到该品牌");
  175. brandRepository.deleteById(id);
  176. return Map.of("ok", true, "message", "已删除");
  177. }
  178. public Map<String, Object> getBrand(int id) {
  179. var opt = brandRepository.findById(id);
  180. if (opt.isEmpty()) throw new IllegalArgumentException("未找到该品牌");
  181. Brand b = opt.get();
  182. Map<String, Object> m = new LinkedHashMap<>();
  183. m.put("id", b.getId());
  184. m.put("name", b.getName());
  185. m.put("function_indication", b.getFunctionIndication());
  186. m.put("usage_dosage", b.getUsageDosage());
  187. m.put("contraindication", b.getContraindication());
  188. m.put("ingredients", b.getIngredients());
  189. m.put("properties", b.getProperties());
  190. m.put("specification", b.getSpecification());
  191. m.put("adverse_reactions", b.getAdverseReactions());
  192. m.put("precautions", b.getPrecautions());
  193. m.put("execution_standard", b.getExecutionStandard());
  194. m.put("storage", b.getStorage());
  195. m.put("jump_url", b.getJumpUrl());
  196. m.put("description", b.getDescription());
  197. m.put("sort_order", b.getSortOrder());
  198. m.put("is_active", b.getIsActive());
  199. m.put("created_at", b.getCreatedAt());
  200. m.put("updated_at", b.getUpdatedAt());
  201. return m;
  202. }
  203. // ==================== 匹配规则 CRUD ====================
  204. public Map<String, Object> listRules(String keyword, int page, int pageSize) {
  205. List<BrandRecommendRule> all = ruleRepository.findAllByOrderByTierAscKeywordAsc();
  206. if (keyword != null && !keyword.isBlank()) {
  207. all = all.stream()
  208. .filter(r -> r.getKeyword().contains(keyword))
  209. .collect(Collectors.toList());
  210. }
  211. // 加载关联品牌名
  212. Set<Integer> brandIds = all.stream().map(BrandRecommendRule::getBrandId).collect(Collectors.toSet());
  213. Map<Integer, String> brandNames = brandRepository.findAllById(brandIds).stream()
  214. .collect(Collectors.toMap(Brand::getId, Brand::getName));
  215. int total = all.size();
  216. int from = (page - 1) * pageSize;
  217. int to = Math.min(from + pageSize, total);
  218. List<BrandRecommendRule> pageItems = from < total ? all.subList(from, to) : List.of();
  219. List<Map<String, Object>> items = new ArrayList<>();
  220. for (BrandRecommendRule r : pageItems) {
  221. Map<String, Object> m = new LinkedHashMap<>();
  222. m.put("id", r.getId());
  223. m.put("keyword", r.getKeyword());
  224. m.put("brand_id", r.getBrandId());
  225. m.put("brand_name", brandNames.getOrDefault(r.getBrandId(), ""));
  226. m.put("tier", r.getTier());
  227. m.put("is_active", r.getIsActive());
  228. m.put("created_at", r.getCreatedAt());
  229. m.put("updated_at", r.getUpdatedAt());
  230. items.add(m);
  231. }
  232. return Map.of(
  233. "items", (Object) items,
  234. "page", page,
  235. "page_size", pageSize,
  236. "total", total,
  237. "total_pages", (int) Math.ceil((double) total / pageSize)
  238. );
  239. }
  240. @Transactional
  241. public Map<String, Object> createRule(String keyword, int brandId, int tier) {
  242. if (keyword == null || keyword.isBlank()) {
  243. throw new IllegalArgumentException("关键词不能为空");
  244. }
  245. if (!brandRepository.existsById(brandId)) {
  246. throw new IllegalArgumentException("品牌不存在");
  247. }
  248. if (ruleRepository.existsByKeywordAndIdNot(keyword.trim(), null)) {
  249. throw new IllegalArgumentException("该关键词已存在");
  250. }
  251. BrandRecommendRule rule = BrandRecommendRule.builder()
  252. .keyword(keyword.trim())
  253. .brandId(brandId)
  254. .tier(tier)
  255. .isActive(true)
  256. .build();
  257. rule = ruleRepository.save(rule);
  258. return Map.of("ok", true, "id", rule.getId(), "message", "已创建");
  259. }
  260. @Transactional
  261. public Map<String, Object> updateRule(int id, String keyword, Integer brandId, Integer tier, Boolean isActive) {
  262. var opt = ruleRepository.findById(id);
  263. if (opt.isEmpty()) throw new IllegalArgumentException("未找到该规则");
  264. BrandRecommendRule rule = opt.get();
  265. if (keyword != null && !keyword.isBlank()) {
  266. if (ruleRepository.existsByKeywordAndIdNot(keyword.trim(), id)) {
  267. throw new IllegalArgumentException("该关键词已存在");
  268. }
  269. rule.setKeyword(keyword.trim());
  270. }
  271. if (brandId != null) {
  272. if (!brandRepository.existsById(brandId)) {
  273. throw new IllegalArgumentException("品牌不存在");
  274. }
  275. rule.setBrandId(brandId);
  276. }
  277. Optional.ofNullable(tier).ifPresent(rule::setTier);
  278. Optional.ofNullable(isActive).ifPresent(rule::setIsActive);
  279. ruleRepository.save(rule);
  280. return Map.of("ok", true, "id", id, "message", "已更新");
  281. }
  282. @Transactional
  283. public Map<String, Object> deleteRule(int id) {
  284. if (!ruleRepository.existsById(id)) throw new IllegalArgumentException("未找到该规则");
  285. ruleRepository.deleteById(id);
  286. return Map.of("ok", true, "message", "已删除");
  287. }
  288. @Transactional
  289. public Map<String, Object> toggleRule(int id, Boolean isActive) {
  290. var opt = ruleRepository.findById(id);
  291. if (opt.isEmpty()) throw new IllegalArgumentException("未找到该规则");
  292. BrandRecommendRule rule = opt.get();
  293. boolean newState = isActive != null ? isActive : !Boolean.TRUE.equals(rule.getIsActive());
  294. rule.setIsActive(newState);
  295. ruleRepository.save(rule);
  296. return Map.of("ok", true, "id", id, "is_active", newState,
  297. "message", Boolean.TRUE.equals(newState) ? "已启用" : "已停用");
  298. }
  299. // ==================== CSV 导入导出 ====================
  300. public Map<String, Object> importBrandsCsv(String csv) {
  301. int success = 0, failed = 0;
  302. for (String line : csv.split("\\n")) {
  303. line = line.trim();
  304. if (line.isEmpty() || line.startsWith("#")) continue;
  305. String[] cols = line.split(",", 14);
  306. if (cols.length < 1) { failed++; continue; }
  307. try {
  308. String name = cols[0].trim();
  309. if (name.isEmpty() || brandRepository.existsByNameAndIdNot(name, null)) {
  310. failed++; continue;
  311. }
  312. Brand b = Brand.builder()
  313. .name(name)
  314. .functionIndication(col(cols, 1))
  315. .usageDosage(col(cols, 2))
  316. .contraindication(col(cols, 3))
  317. .ingredients(col(cols, 4))
  318. .properties(col(cols, 5))
  319. .specification(col(cols, 6))
  320. .adverseReactions(col(cols, 7))
  321. .precautions(col(cols, 8))
  322. .executionStandard(col(cols, 9))
  323. .storage(col(cols, 10))
  324. .jumpUrl(col(cols, 11))
  325. .description(col(cols, 12))
  326. .sortOrder(cols.length > 13 ? Integer.parseInt(cols[13].trim()) : 0)
  327. .isActive(true)
  328. .build();
  329. brandRepository.save(b);
  330. success++;
  331. } catch (Exception e) { failed++; }
  332. }
  333. return Map.of("ok", true, "success", success, "failed", failed);
  334. }
  335. public String exportBrandsCsv() {
  336. List<Brand> brands = brandRepository.findAllByOrderBySortOrderAsc();
  337. StringBuilder sb = new StringBuilder(
  338. "name,function_indication,usage_dosage,contraindication,ingredients,properties,specification,adverse_reactions,precautions,execution_standard,storage,jump_url,description,sort_order\\n");
  339. for (Brand b : brands) {
  340. sb.append(esc(b.getName())).append(",")
  341. .append(esc(b.getFunctionIndication())).append(",")
  342. .append(esc(b.getUsageDosage())).append(",")
  343. .append(esc(b.getContraindication())).append(",")
  344. .append(esc(b.getIngredients())).append(",")
  345. .append(esc(b.getProperties())).append(",")
  346. .append(esc(b.getSpecification())).append(",")
  347. .append(esc(b.getAdverseReactions())).append(",")
  348. .append(esc(b.getPrecautions())).append(",")
  349. .append(esc(b.getExecutionStandard())).append(",")
  350. .append(esc(b.getStorage())).append(",")
  351. .append(esc(b.getJumpUrl())).append(",")
  352. .append(esc(b.getDescription())).append(",")
  353. .append(b.getSortOrder()).append("\\n");
  354. }
  355. return sb.toString();
  356. }
  357. public Map<String, Object> importRulesCsv(String csv) {
  358. int success = 0, failed = 0;
  359. for (String line : csv.split("\\n")) {
  360. line = line.trim();
  361. if (line.isEmpty() || line.startsWith("#")) continue;
  362. String[] cols = line.split(",", 3);
  363. if (cols.length < 2) { failed++; continue; }
  364. try {
  365. String kw = cols[0].trim();
  366. int brandId = Integer.parseInt(cols[1].trim());
  367. int tier = cols.length > 2 ? Integer.parseInt(cols[2].trim()) : 1;
  368. if (!brandRepository.existsById(brandId)) { failed++; continue; }
  369. if (ruleRepository.existsByKeywordAndIdNot(kw, null)) { failed++; continue; }
  370. ruleRepository.save(BrandRecommendRule.builder()
  371. .keyword(kw).brandId(brandId).tier(tier).isActive(true).build());
  372. success++;
  373. } catch (Exception e) { failed++; }
  374. }
  375. return Map.of("ok", true, "success", success, "failed", failed);
  376. }
  377. public String exportRulesCsv() {
  378. List<BrandRecommendRule> rules = ruleRepository.findAllByOrderByTierAscKeywordAsc();
  379. Set<Integer> brandIds = rules.stream().map(BrandRecommendRule::getBrandId).collect(Collectors.toSet());
  380. Map<Integer, String> brandNames = brandRepository.findAllById(brandIds).stream()
  381. .collect(Collectors.toMap(Brand::getId, Brand::getName));
  382. StringBuilder sb = new StringBuilder("keyword,brand_id,brand_name,tier,is_active\\n");
  383. for (BrandRecommendRule r : rules) {
  384. sb.append(r.getKeyword()).append(",")
  385. .append(r.getBrandId()).append(",")
  386. .append(brandNames.getOrDefault(r.getBrandId(), "")).append(",")
  387. .append(r.getTier()).append(",")
  388. .append(r.getIsActive()).append("\\n");
  389. }
  390. return sb.toString();
  391. }
  392. // ==================== Helpers ====================
  393. private static String str(Map<String, Object> body, String key) {
  394. Object v = body.get(key);
  395. if (v == null) return null;
  396. String s = v.toString().trim();
  397. return s.isEmpty() ? null : s;
  398. }
  399. private static String nullToEmpty(String s) {
  400. return s != null ? s : "";
  401. }
  402. /** map 中存在 key 时应用字符串值 */
  403. private static void applyStr(Map<String, Object> body, String key, Consumer<String> setter) {
  404. Optional.ofNullable(str(body, key)).ifPresent(setter);
  405. }
  406. private static String col(String[] cols, int idx) {
  407. if (idx >= cols.length) return null;
  408. String s = cols[idx].trim();
  409. return s.isEmpty() ? null : s;
  410. }
  411. private static String esc(String s) {
  412. if (s == null || s.isEmpty()) return "";
  413. return "\"" + s.replace("\"", "\"\"") + "\"";
  414. }
  415. }