|
|
@@ -0,0 +1,403 @@
|
|
|
+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.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) {
|
|
|
+ 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> seen = 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 (seen.add(rule.getId())) {
|
|
|
+ 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", brand.getDescription() != null ? brand.getDescription() : "");
|
|
|
+ m.put("function", brand.getFunctionIndication() != null ? brand.getFunctionIndication() : "");
|
|
|
+ m.put("usage_dosage", brand.getUsageDosage() != null ? brand.getUsageDosage() : "");
|
|
|
+ m.put("contraindication", brand.getContraindication() != null ? brand.getContraindication() : "");
|
|
|
+ m.put("ingredients", brand.getIngredients() != null ? brand.getIngredients() : "");
|
|
|
+ m.put("properties", brand.getProperties() != null ? brand.getProperties() : "");
|
|
|
+ m.put("specification", brand.getSpecification() != null ? brand.getSpecification() : "");
|
|
|
+ m.put("adverse_reactions", brand.getAdverseReactions() != null ? brand.getAdverseReactions() : "");
|
|
|
+ m.put("precautions", brand.getPrecautions() != null ? brand.getPrecautions() : "");
|
|
|
+ m.put("execution_standard", brand.getExecutionStandard() != null ? brand.getExecutionStandard() : "");
|
|
|
+ m.put("storage", brand.getStorage() != null ? brand.getStorage() : "");
|
|
|
+ m.put("jump_url", brand.getJumpUrl() != null ? 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()) {
|
|
|
+ return Map.of("ok", false, "message", "品牌名称不能为空");
|
|
|
+ }
|
|
|
+ if (brandRepository.existsByNameAndIdNot(name.trim(), null)) {
|
|
|
+ return Map.of("ok", false, "message", "该品牌名称已存在");
|
|
|
+ }
|
|
|
+ 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()) return Map.of("ok", false, "message", "未找到该品牌");
|
|
|
+ Brand b = opt.get();
|
|
|
+ String name = (String) body.get("name");
|
|
|
+ if (name != null && !name.isBlank()) {
|
|
|
+ if (brandRepository.existsByNameAndIdNot(name.trim(), id)) {
|
|
|
+ return Map.of("ok", false, "message", "该品牌名称已存在");
|
|
|
+ }
|
|
|
+ b.setName(name.trim());
|
|
|
+ }
|
|
|
+ if (body.containsKey("function_indication")) b.setFunctionIndication(str(body, "function_indication"));
|
|
|
+ if (body.containsKey("usage_dosage")) b.setUsageDosage(str(body, "usage_dosage"));
|
|
|
+ if (body.containsKey("contraindication")) b.setContraindication(str(body, "contraindication"));
|
|
|
+ if (body.containsKey("ingredients")) b.setIngredients(str(body, "ingredients"));
|
|
|
+ if (body.containsKey("properties")) b.setProperties(str(body, "properties"));
|
|
|
+ if (body.containsKey("specification")) b.setSpecification(str(body, "specification"));
|
|
|
+ if (body.containsKey("adverse_reactions")) b.setAdverseReactions(str(body, "adverse_reactions"));
|
|
|
+ if (body.containsKey("precautions")) b.setPrecautions(str(body, "precautions"));
|
|
|
+ if (body.containsKey("execution_standard")) b.setExecutionStandard(str(body, "execution_standard"));
|
|
|
+ if (body.containsKey("storage")) b.setStorage(str(body, "storage"));
|
|
|
+ if (body.containsKey("jump_url")) b.setJumpUrl(str(body, "jump_url"));
|
|
|
+ if (body.containsKey("description")) b.setDescription(str(body, "description"));
|
|
|
+ if (body.get("sort_order") instanceof Number n) b.setSortOrder(n.intValue());
|
|
|
+ if (body.get("is_active") instanceof Boolean a) b.setIsActive(a);
|
|
|
+ brandRepository.save(b);
|
|
|
+ return Map.of("ok", true, "id", id, "message", "已更新");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ public Map<String, Object> deleteBrand(int id) {
|
|
|
+ if (!brandRepository.existsById(id)) return Map.of("ok", false, "message", "未找到该品牌");
|
|
|
+ brandRepository.deleteById(id);
|
|
|
+ return Map.of("ok", true, "message", "已删除");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 匹配规则 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()) {
|
|
|
+ return Map.of("ok", false, "message", "关键词不能为空");
|
|
|
+ }
|
|
|
+ if (!brandRepository.existsById(brandId)) {
|
|
|
+ return Map.of("ok", false, "message", "品牌不存在");
|
|
|
+ }
|
|
|
+ if (ruleRepository.existsByKeywordAndIdNot(keyword.trim(), null)) {
|
|
|
+ return Map.of("ok", false, "message", "该关键词已存在");
|
|
|
+ }
|
|
|
+ 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()) return Map.of("ok", false, "message", "未找到该规则");
|
|
|
+ BrandRecommendRule rule = opt.get();
|
|
|
+ if (keyword != null && !keyword.isBlank()) {
|
|
|
+ if (ruleRepository.existsByKeywordAndIdNot(keyword.trim(), id)) {
|
|
|
+ return Map.of("ok", false, "message", "该关键词已存在");
|
|
|
+ }
|
|
|
+ rule.setKeyword(keyword.trim());
|
|
|
+ }
|
|
|
+ if (brandId != null) {
|
|
|
+ if (!brandRepository.existsById(brandId)) {
|
|
|
+ return Map.of("ok", false, "message", "品牌不存在");
|
|
|
+ }
|
|
|
+ rule.setBrandId(brandId);
|
|
|
+ }
|
|
|
+ if (tier != null) rule.setTier(tier);
|
|
|
+ if (isActive != null) rule.setIsActive(isActive);
|
|
|
+ ruleRepository.save(rule);
|
|
|
+ return Map.of("ok", true, "id", id, "message", "已更新");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ public Map<String, Object> deleteRule(int id) {
|
|
|
+ if (!ruleRepository.existsById(id)) return Map.of("ok", false, "message", "未找到该规则");
|
|
|
+ ruleRepository.deleteById(id);
|
|
|
+ return Map.of("ok", true, "message", "已删除");
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 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 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("\"", "\"\"") + "\"";
|
|
|
+ }
|
|
|
+}
|