| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454 |
- 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<Map<String, Object>> match(List<Map<String, Object>> sources, String fullAnswer) {
- // 仅加载 is_active=true 的规则,已停用的不参与匹配
- List<BrandRecommendRule> rules = ruleRepository.findByIsActiveTrueOrderByTierAscKeywordAsc();
- if (rules.isEmpty()) {
- return List.of();
- }
- Map<Integer, List<BrandRecommendRule>> byTier = rules.stream()
- .collect(Collectors.groupingBy(BrandRecommendRule::getTier, LinkedHashMap::new, Collectors.toList()));
- Set<String> sourceNames = sources.stream()
- .map(s -> (String) s.getOrDefault("name", ""))
- .filter(n -> !n.isEmpty())
- .collect(Collectors.toSet());
- for (Map.Entry<Integer, List<BrandRecommendRule>> entry : byTier.entrySet()) {
- List<Map<String, Object>> matches = findMatches(entry.getValue(), sourceNames);
- if (!matches.isEmpty()) {
- return matches;
- }
- }
- // 全文回退也按 tier 逐层尝试
- if (fullAnswer != null && !fullAnswer.isEmpty()) {
- for (Map.Entry<Integer, List<BrandRecommendRule>> entry : byTier.entrySet()) {
- List<Map<String, Object>> matches = findMatches(entry.getValue(), Set.of(fullAnswer));
- if (!matches.isEmpty()) {
- return matches;
- }
- }
- }
- return List.of();
- }
- private List<Map<String, Object>> findMatches(List<BrandRecommendRule> rules, Set<String> targets) {
- // 一次性加载所有用到的 Brand
- Set<Integer> brandIds = rules.stream().map(BrandRecommendRule::getBrandId).collect(Collectors.toSet());
- Map<Integer, Brand> brandMap = brandRepository.findAllById(brandIds).stream()
- .collect(Collectors.toMap(Brand::getId, b -> b));
- Set<Integer> seenRuleIds = new HashSet<>(); // 规则去重(同规则只命中一次)
- Set<Integer> seenBrandIds = new HashSet<>(); // 品牌去重(同品牌只返回一次)
- List<Map<String, Object>> 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<String, Object> buildBrandResult(BrandRecommendRule rule, Brand brand) {
- Map<String, Object> 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<String, Object> listBrands(String keyword, int page, int pageSize) {
- List<Brand> 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<Brand> 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<String, Object> createBrand(Map<String, Object> 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<String, Object> updateBrand(int id, Map<String, Object> 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<String, Object> deleteBrand(int id) {
- if (!brandRepository.existsById(id)) throw new IllegalArgumentException("未找到该品牌");
- brandRepository.deleteById(id);
- return Map.of("ok", true, "message", "已删除");
- }
- public Map<String, Object> getBrand(int id) {
- var opt = brandRepository.findById(id);
- if (opt.isEmpty()) throw new IllegalArgumentException("未找到该品牌");
- Brand b = opt.get();
- Map<String, Object> 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<String, Object> listRules(String keyword, int page, int pageSize) {
- List<BrandRecommendRule> all = ruleRepository.findAllByOrderByTierAscKeywordAsc();
- if (keyword != null && !keyword.isBlank()) {
- all = all.stream()
- .filter(r -> r.getKeyword().contains(keyword))
- .collect(Collectors.toList());
- }
- // 加载关联品牌名
- Set<Integer> brandIds = all.stream().map(BrandRecommendRule::getBrandId).collect(Collectors.toSet());
- Map<Integer, String> 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<BrandRecommendRule> pageItems = from < total ? all.subList(from, to) : List.of();
- List<Map<String, Object>> items = new ArrayList<>();
- for (BrandRecommendRule r : pageItems) {
- Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> deleteRule(int id) {
- if (!ruleRepository.existsById(id)) throw new IllegalArgumentException("未找到该规则");
- ruleRepository.deleteById(id);
- return Map.of("ok", true, "message", "已删除");
- }
- @Transactional
- public Map<String, Object> 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<String, Object> 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<Brand> 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<String, Object> 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<BrandRecommendRule> rules = ruleRepository.findAllByOrderByTierAscKeywordAsc();
- Set<Integer> brandIds = rules.stream().map(BrandRecommendRule::getBrandId).collect(Collectors.toSet());
- Map<Integer, String> 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<String, Object> 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<String, Object> body, String key, Consumer<String> 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("\"", "\"\"") + "\"";
- }
- }
|