package com.pharmacopoeia.service; import com.pharmacopoeia.entity.Brand; import com.pharmacopoeia.entity.BrandRecommendRule; import com.pharmacopoeia.repository.BrandRecommendRuleRepository; import com.pharmacopoeia.repository.BrandRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; import java.util.function.Consumer; import java.util.stream.Collectors; @Service public class BrandRecommendService { private final BrandRecommendRuleRepository ruleRepository; private final BrandRepository brandRepository; public BrandRecommendService(BrandRecommendRuleRepository ruleRepository, BrandRepository brandRepository) { this.ruleRepository = ruleRepository; this.brandRepository = brandRepository; } // ==================== 层级匹配 ==================== public List> match(List> sources, String fullAnswer) { // 仅加载 is_active=true 的规则,已停用的不参与匹配 List rules = ruleRepository.findByIsActiveTrueOrderByTierAscKeywordAsc(); if (rules.isEmpty()) { return List.of(); } Map> byTier = rules.stream() .collect(Collectors.groupingBy(BrandRecommendRule::getTier, LinkedHashMap::new, Collectors.toList())); Set sourceNames = sources.stream() .map(s -> (String) s.getOrDefault("name", "")) .filter(n -> !n.isEmpty()) .collect(Collectors.toSet()); for (Map.Entry> entry : byTier.entrySet()) { List> matches = findMatches(entry.getValue(), sourceNames); if (!matches.isEmpty()) { return matches; } } // 全文回退也按 tier 逐层尝试 if (fullAnswer != null && !fullAnswer.isEmpty()) { for (Map.Entry> entry : byTier.entrySet()) { List> matches = findMatches(entry.getValue(), Set.of(fullAnswer)); if (!matches.isEmpty()) { return matches; } } } return List.of(); } private List> findMatches(List rules, Set targets) { // 一次性加载所有用到的 Brand Set brandIds = rules.stream().map(BrandRecommendRule::getBrandId).collect(Collectors.toSet()); Map brandMap = brandRepository.findAllById(brandIds).stream() .collect(Collectors.toMap(Brand::getId, b -> b)); Set seenRuleIds = new HashSet<>(); // 规则去重(同规则只命中一次) Set seenBrandIds = new HashSet<>(); // 品牌去重(同品牌只返回一次) List> result = new ArrayList<>(); for (BrandRecommendRule rule : rules) { for (String target : targets) { if (target.toLowerCase().contains(rule.getKeyword().toLowerCase())) { if (seenRuleIds.add(rule.getId()) && seenBrandIds.add(rule.getBrandId())) { Brand brand = brandMap.get(rule.getBrandId()); result.add(buildBrandResult(rule, brand)); } break; } } } return result; } private Map buildBrandResult(BrandRecommendRule rule, Brand brand) { Map m = new LinkedHashMap<>(); m.put("brand_id", rule.getBrandId()); m.put("matched_keyword", rule.getKeyword()); m.put("tier", rule.getTier()); if (brand != null) { m.put("brand_name", brand.getName()); m.put("description", nullToEmpty(brand.getDescription())); m.put("function", nullToEmpty(brand.getFunctionIndication())); m.put("usage_dosage", nullToEmpty(brand.getUsageDosage())); m.put("contraindication", nullToEmpty(brand.getContraindication())); m.put("ingredients", nullToEmpty(brand.getIngredients())); m.put("properties", nullToEmpty(brand.getProperties())); m.put("specification", nullToEmpty(brand.getSpecification())); m.put("adverse_reactions", nullToEmpty(brand.getAdverseReactions())); m.put("precautions", nullToEmpty(brand.getPrecautions())); m.put("execution_standard", nullToEmpty(brand.getExecutionStandard())); m.put("storage", nullToEmpty(brand.getStorage())); m.put("jump_url", nullToEmpty(brand.getJumpUrl())); } return m; } // ==================== 品牌 CRUD ==================== public Map listBrands(String keyword, int page, int pageSize) { List all = brandRepository.findAllByOrderBySortOrderAsc(); if (keyword != null && !keyword.isBlank()) { String kw = keyword.toLowerCase(); all = all.stream() .filter(b -> b.getName().toLowerCase().contains(kw)) .collect(Collectors.toList()); } int total = all.size(); int from = (page - 1) * pageSize; int to = Math.min(from + pageSize, total); List pageItems = from < total ? all.subList(from, to) : List.of(); return Map.of( "items", (Object) pageItems, "page", page, "page_size", pageSize, "total", total, "total_pages", (int) Math.ceil((double) total / pageSize) ); } @Transactional public Map createBrand(Map body) { String name = (String) body.get("name"); if (name == null || name.isBlank()) { throw new IllegalArgumentException("品牌名称不能为空"); } if (brandRepository.existsByNameAndIdNot(name.trim(), null)) { throw new IllegalArgumentException("该品牌名称已存在"); } Brand brand = Brand.builder() .name(name.trim()) .functionIndication(str(body, "function_indication")) .usageDosage(str(body, "usage_dosage")) .contraindication(str(body, "contraindication")) .ingredients(str(body, "ingredients")) .properties(str(body, "properties")) .specification(str(body, "specification")) .adverseReactions(str(body, "adverse_reactions")) .precautions(str(body, "precautions")) .executionStandard(str(body, "execution_standard")) .storage(str(body, "storage")) .jumpUrl(str(body, "jump_url")) .description(str(body, "description")) .sortOrder(body.get("sort_order") instanceof Number n ? n.intValue() : 0) .isActive(true) .build(); brand = brandRepository.save(brand); return Map.of("ok", true, "id", brand.getId(), "message", "已创建"); } @Transactional public Map updateBrand(int id, Map body) { var opt = brandRepository.findById(id); if (opt.isEmpty()) throw new IllegalArgumentException("未找到该品牌"); Brand b = opt.get(); String name = (String) body.get("name"); if (name != null && !name.isBlank()) { if (brandRepository.existsByNameAndIdNot(name.trim(), id)) { throw new IllegalArgumentException("该品牌名称已存在"); } b.setName(name.trim()); } applyStr(body, "function_indication", b::setFunctionIndication); applyStr(body, "usage_dosage", b::setUsageDosage); applyStr(body, "contraindication", b::setContraindication); applyStr(body, "ingredients", b::setIngredients); applyStr(body, "properties", b::setProperties); applyStr(body, "specification", b::setSpecification); applyStr(body, "adverse_reactions", b::setAdverseReactions); applyStr(body, "precautions", b::setPrecautions); applyStr(body, "execution_standard", b::setExecutionStandard); applyStr(body, "storage", b::setStorage); applyStr(body, "jump_url", b::setJumpUrl); applyStr(body, "description", b::setDescription); Optional.ofNullable((Number) body.get("sort_order")).ifPresent(n -> b.setSortOrder(n.intValue())); Optional.ofNullable((Boolean) body.get("is_active")).ifPresent(b::setIsActive); brandRepository.save(b); return Map.of("ok", true, "id", id, "message", "已更新"); } @Transactional public Map deleteBrand(int id) { if (!brandRepository.existsById(id)) throw new IllegalArgumentException("未找到该品牌"); brandRepository.deleteById(id); return Map.of("ok", true, "message", "已删除"); } public Map getBrand(int id) { var opt = brandRepository.findById(id); if (opt.isEmpty()) throw new IllegalArgumentException("未找到该品牌"); Brand b = opt.get(); Map m = new LinkedHashMap<>(); m.put("id", b.getId()); m.put("name", b.getName()); m.put("function_indication", b.getFunctionIndication()); m.put("usage_dosage", b.getUsageDosage()); m.put("contraindication", b.getContraindication()); m.put("ingredients", b.getIngredients()); m.put("properties", b.getProperties()); m.put("specification", b.getSpecification()); m.put("adverse_reactions", b.getAdverseReactions()); m.put("precautions", b.getPrecautions()); m.put("execution_standard", b.getExecutionStandard()); m.put("storage", b.getStorage()); m.put("jump_url", b.getJumpUrl()); m.put("description", b.getDescription()); m.put("sort_order", b.getSortOrder()); m.put("is_active", b.getIsActive()); m.put("created_at", b.getCreatedAt()); m.put("updated_at", b.getUpdatedAt()); return m; } // ==================== 匹配规则 CRUD ==================== public Map listRules(String keyword, int page, int pageSize) { List all = ruleRepository.findAllByOrderByTierAscKeywordAsc(); if (keyword != null && !keyword.isBlank()) { all = all.stream() .filter(r -> r.getKeyword().contains(keyword)) .collect(Collectors.toList()); } // 加载关联品牌名 Set brandIds = all.stream().map(BrandRecommendRule::getBrandId).collect(Collectors.toSet()); Map brandNames = brandRepository.findAllById(brandIds).stream() .collect(Collectors.toMap(Brand::getId, Brand::getName)); int total = all.size(); int from = (page - 1) * pageSize; int to = Math.min(from + pageSize, total); List pageItems = from < total ? all.subList(from, to) : List.of(); List> items = new ArrayList<>(); for (BrandRecommendRule r : pageItems) { Map m = new LinkedHashMap<>(); m.put("id", r.getId()); m.put("keyword", r.getKeyword()); m.put("brand_id", r.getBrandId()); m.put("brand_name", brandNames.getOrDefault(r.getBrandId(), "")); m.put("tier", r.getTier()); m.put("is_active", r.getIsActive()); m.put("created_at", r.getCreatedAt()); m.put("updated_at", r.getUpdatedAt()); items.add(m); } return Map.of( "items", (Object) items, "page", page, "page_size", pageSize, "total", total, "total_pages", (int) Math.ceil((double) total / pageSize) ); } @Transactional public Map createRule(String keyword, int brandId, int tier) { if (keyword == null || keyword.isBlank()) { throw new IllegalArgumentException("关键词不能为空"); } if (!brandRepository.existsById(brandId)) { throw new IllegalArgumentException("品牌不存在"); } if (ruleRepository.existsByKeywordAndIdNot(keyword.trim(), null)) { throw new IllegalArgumentException("该关键词已存在"); } BrandRecommendRule rule = BrandRecommendRule.builder() .keyword(keyword.trim()) .brandId(brandId) .tier(tier) .isActive(true) .build(); rule = ruleRepository.save(rule); return Map.of("ok", true, "id", rule.getId(), "message", "已创建"); } @Transactional public Map updateRule(int id, String keyword, Integer brandId, Integer tier, Boolean isActive) { var opt = ruleRepository.findById(id); if (opt.isEmpty()) throw new IllegalArgumentException("未找到该规则"); BrandRecommendRule rule = opt.get(); if (keyword != null && !keyword.isBlank()) { if (ruleRepository.existsByKeywordAndIdNot(keyword.trim(), id)) { throw new IllegalArgumentException("该关键词已存在"); } rule.setKeyword(keyword.trim()); } if (brandId != null) { if (!brandRepository.existsById(brandId)) { throw new IllegalArgumentException("品牌不存在"); } rule.setBrandId(brandId); } Optional.ofNullable(tier).ifPresent(rule::setTier); Optional.ofNullable(isActive).ifPresent(rule::setIsActive); ruleRepository.save(rule); return Map.of("ok", true, "id", id, "message", "已更新"); } @Transactional public Map deleteRule(int id) { if (!ruleRepository.existsById(id)) throw new IllegalArgumentException("未找到该规则"); ruleRepository.deleteById(id); return Map.of("ok", true, "message", "已删除"); } @Transactional public Map toggleRule(int id, Boolean isActive) { var opt = ruleRepository.findById(id); if (opt.isEmpty()) throw new IllegalArgumentException("未找到该规则"); BrandRecommendRule rule = opt.get(); boolean newState = isActive != null ? isActive : !Boolean.TRUE.equals(rule.getIsActive()); rule.setIsActive(newState); ruleRepository.save(rule); return Map.of("ok", true, "id", id, "is_active", newState, "message", Boolean.TRUE.equals(newState) ? "已启用" : "已停用"); } // ==================== CSV 导入导出 ==================== public Map importBrandsCsv(String csv) { int success = 0, failed = 0; for (String line : csv.split("\\n")) { line = line.trim(); if (line.isEmpty() || line.startsWith("#")) continue; String[] cols = line.split(",", 14); if (cols.length < 1) { failed++; continue; } try { String name = cols[0].trim(); if (name.isEmpty() || brandRepository.existsByNameAndIdNot(name, null)) { failed++; continue; } Brand b = Brand.builder() .name(name) .functionIndication(col(cols, 1)) .usageDosage(col(cols, 2)) .contraindication(col(cols, 3)) .ingredients(col(cols, 4)) .properties(col(cols, 5)) .specification(col(cols, 6)) .adverseReactions(col(cols, 7)) .precautions(col(cols, 8)) .executionStandard(col(cols, 9)) .storage(col(cols, 10)) .jumpUrl(col(cols, 11)) .description(col(cols, 12)) .sortOrder(cols.length > 13 ? Integer.parseInt(cols[13].trim()) : 0) .isActive(true) .build(); brandRepository.save(b); success++; } catch (Exception e) { failed++; } } return Map.of("ok", true, "success", success, "failed", failed); } public String exportBrandsCsv() { List brands = brandRepository.findAllByOrderBySortOrderAsc(); StringBuilder sb = new StringBuilder( "name,function_indication,usage_dosage,contraindication,ingredients,properties,specification,adverse_reactions,precautions,execution_standard,storage,jump_url,description,sort_order\\n"); for (Brand b : brands) { sb.append(esc(b.getName())).append(",") .append(esc(b.getFunctionIndication())).append(",") .append(esc(b.getUsageDosage())).append(",") .append(esc(b.getContraindication())).append(",") .append(esc(b.getIngredients())).append(",") .append(esc(b.getProperties())).append(",") .append(esc(b.getSpecification())).append(",") .append(esc(b.getAdverseReactions())).append(",") .append(esc(b.getPrecautions())).append(",") .append(esc(b.getExecutionStandard())).append(",") .append(esc(b.getStorage())).append(",") .append(esc(b.getJumpUrl())).append(",") .append(esc(b.getDescription())).append(",") .append(b.getSortOrder()).append("\\n"); } return sb.toString(); } public Map importRulesCsv(String csv) { int success = 0, failed = 0; for (String line : csv.split("\\n")) { line = line.trim(); if (line.isEmpty() || line.startsWith("#")) continue; String[] cols = line.split(",", 3); if (cols.length < 2) { failed++; continue; } try { String kw = cols[0].trim(); int brandId = Integer.parseInt(cols[1].trim()); int tier = cols.length > 2 ? Integer.parseInt(cols[2].trim()) : 1; if (!brandRepository.existsById(brandId)) { failed++; continue; } if (ruleRepository.existsByKeywordAndIdNot(kw, null)) { failed++; continue; } ruleRepository.save(BrandRecommendRule.builder() .keyword(kw).brandId(brandId).tier(tier).isActive(true).build()); success++; } catch (Exception e) { failed++; } } return Map.of("ok", true, "success", success, "failed", failed); } public String exportRulesCsv() { List rules = ruleRepository.findAllByOrderByTierAscKeywordAsc(); Set brandIds = rules.stream().map(BrandRecommendRule::getBrandId).collect(Collectors.toSet()); Map brandNames = brandRepository.findAllById(brandIds).stream() .collect(Collectors.toMap(Brand::getId, Brand::getName)); StringBuilder sb = new StringBuilder("keyword,brand_id,brand_name,tier,is_active\\n"); for (BrandRecommendRule r : rules) { sb.append(r.getKeyword()).append(",") .append(r.getBrandId()).append(",") .append(brandNames.getOrDefault(r.getBrandId(), "")).append(",") .append(r.getTier()).append(",") .append(r.getIsActive()).append("\\n"); } return sb.toString(); } // ==================== Helpers ==================== private static String str(Map body, String key) { Object v = body.get(key); if (v == null) return null; String s = v.toString().trim(); return s.isEmpty() ? null : s; } private static String nullToEmpty(String s) { return s != null ? s : ""; } /** map 中存在 key 时应用字符串值 */ private static void applyStr(Map body, String key, Consumer setter) { Optional.ofNullable(str(body, key)).ifPresent(setter); } private static String col(String[] cols, int idx) { if (idx >= cols.length) return null; String s = cols[idx].trim(); return s.isEmpty() ? null : s; } private static String esc(String s) { if (s == null || s.isEmpty()) return ""; return "\"" + s.replace("\"", "\"\"") + "\""; } }