wlzboy
2026-03-31 61c4c3f45e4257e2e7662f033e2719e62366c632
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/HospDataController.java
@@ -2,16 +2,27 @@
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.HospitalTokenizerUtil;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.HospData;
import com.ruoyi.system.domain.HospitalTokenizerTask;
import com.ruoyi.system.domain.TbHospData;
import com.ruoyi.system.mapper.HospDataMapper;
import com.ruoyi.system.service.HospitalTokenizerAsyncService;
import com.ruoyi.system.service.ISQLHospDataService;
import com.ruoyi.system.service.ITbHospDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
/**
 * 医院数据Controller
@@ -25,11 +36,25 @@
@RequestMapping("/system/hospital")
public class HospDataController extends BaseController {
    
    /**
     * 医院搜索最低匹配分数阈值(低于此分数的结果将被过滤)
     */
    private static final int MIN_MATCH_SCORE_THRESHOLD = 1;
    @Autowired
    private HospDataMapper hospDataMapper;
    @Autowired
    private ISQLHospDataService sqlHospDataService;
    @Autowired
    private ITbHospDataService tbHospDataService;
    @Autowired
    private com.ruoyi.system.mapper.TbHospDataMapper tbHospDataMapper;
    @Autowired
    private HospitalTokenizerAsyncService asyncService;
    
    /**
     * 搜索医院(从MySQL tb_hosp_data表查询)
@@ -181,4 +206,258 @@
        
        return success(hospitals);
    }
    /**
     * 批量生成所有医院的分词(异步)
     * 管理员接口,用于初始化或重新生成医院分词
     *
     * @return 任务ID
     */
    @GetMapping("/generateKeywords")
    public AjaxResult generateAllHospitalKeywords() {
        logger.info("开始批量生成医院分词(异步)...");
        try {
            // 生成任务ID
            String taskId = UUID.randomUUID().toString().replace("-", "");
            // 异步执行任务
            asyncService.executeTokenizerTask(taskId);
            logger.info("医院分词任务已启动: taskId={}", taskId);
            // 立即返回任务ID
            return success()
                .put("taskId", taskId)
                .put("message", "分词任务已启动,请查询任务进度");
        } catch (Exception e) {
            logger.error("启动医院分词任务失败", e);
            return error("启动失败:" + e.getMessage());
        }
    }
    /**
     * 查询医院分词任务进度
     *
     * @param taskId 任务ID
     * @return 任务进度信息
     */
    @GetMapping("/getTaskProgress")
    public AjaxResult getTaskProgress(@RequestParam("taskId") String taskId) {
        try {
            HospitalTokenizerTask task = asyncService.getTaskStatus(taskId);
            if (task == null) {
                return error("任务不存在或已过期");
            }
            return success(task);
        } catch (Exception e) {
            logger.error("查询任务进度失败: taskId={}", taskId, e);
            return error("查询失败:" + e.getMessage());
        }
    }
    /**
     * 基于分词匹配搜索医院
     * 前端传入医院信息,进行分词后与数据库中的分词匹配
     * 根据匹配的分词数量进行权重排序,匹配越多排名越靠前
     *
     * @param searchText 搜索文本(医院名称、地址等)
     * @param pageSize 返回结果数量限制(默认50)
     * @return 匹配的医院列表(按匹配度排序)
     */
    @GetMapping("/searchByKeywords")
    public AjaxResult searchHospitalsByKeywords(
            @RequestParam("searchText") String searchText,
            @RequestParam(value = "pageSize", required = false, defaultValue = "50") Integer pageSize) {
        logger.info("基于分词匹配搜索医院:searchText={}, pageSize={}", searchText, pageSize);
        if (searchText == null || searchText.trim().isEmpty()) {
            return error("搜索文本不能为空");
        }
        try {
            long startTime = System.currentTimeMillis();
            // 1. 对前端传入的搜索文本进行分词
            String searchKeywords = HospitalTokenizerUtil.tokenizeSearchText(searchText);
            logger.info("搜索文本分词结果:{}", searchKeywords);
            if (searchKeywords.isEmpty()) {
                return success(new ArrayList<>());
            }
            // 2. 将分词结果拆分为关键词列表,用于数据库预过滤
            String[] keywordArray = searchKeywords.split(",");
            List<String> keywordList = new ArrayList<>();
            for (String keyword : keywordArray) {
                String trimmed = keyword.trim();
                if (!trimmed.isEmpty() && trimmed.length() >= 2) { // 只使用2个字以上的关键词
                    keywordList.add(trimmed);
                }
            }
            if (keywordList.isEmpty()) {
                logger.warn("没有有效的关键词用于搜索");
                return success(new ArrayList<>());
            }
            logger.info("使用关键词进行数据库预过滤: {}", keywordList);
            // 3. 通过数据库层面预过滤,只查询可能匹配的医院(而不是所有医院)
            List<TbHospData> candidateHospitals = tbHospDataMapper.selectTbHospDataByKeywords(keywordList, "0");
            long queryTime = System.currentTimeMillis();
            logger.info("数据库预过滤完成,候选医院数量: {}, 耗时: {}ms", candidateHospitals.size(), queryTime - startTime);
            // 4. 提取候选医院的地区名称(从 hopsArea 字段)
            Set<String> districtNames = new HashSet<>();
            for (TbHospData hospital : candidateHospitals) {
                if (StringUtils.isNotBlank(hospital.getHopsArea())) {
                    // 提取地区名,移除常见后缀
                    String area = hospital.getHopsArea()
                        .replace("区", "")
                        .replace("市", "")
                        .replace("县", "")
                        .trim();
                    if (area.length() > 0) {
                        districtNames.add(area);
                    }
                }
            }
            logger.info("提取到 {} 个独特地区名称", districtNames.size());
            // 5. 对候选医院计算匹配分数,并过滤出有匹配的医院
            List<HospitalMatchResult> matchResults = new ArrayList<>();
            long matchStartTime = System.currentTimeMillis();
            for (TbHospData hospital : candidateHospitals) {
                if (hospital.getHospKeywords() == null || hospital.getHospKeywords().isEmpty()) {
                    continue;
                }
                // 计算匹配分数(传入医院名称和地区名称集合)
                int matchScore = HospitalTokenizerUtil.calculateMatchScore(
                    searchKeywords,
                    hospital.getHospKeywords(),
                    hospital.getHospName(),
                    districtNames
                );
                // 只保留匹配分数达到阈值的医院
                if (matchScore >= MIN_MATCH_SCORE_THRESHOLD) {
                    matchResults.add(new HospitalMatchResult(hospital, matchScore));
                }
            }
            long matchTime = System.currentTimeMillis();
            logger.info("匹配计算完成,找到 {} 个匹配的医院(分数>={}}),耗时: {}ms",
                matchResults.size(), MIN_MATCH_SCORE_THRESHOLD, matchTime - matchStartTime);
            // 4. 按匹配分数降序排序,分数相同时按医院名称长度升序排序(名称越短越靠前)
            matchResults.sort(Comparator
                .comparingInt(HospitalMatchResult::getMatchScore).reversed()
                .thenComparingInt(result -> result.getHospital().getHospName().length()));
            // 5. 限制返回数量
            if (pageSize != null && pageSize > 0 && matchResults.size() > pageSize) {
                matchResults = matchResults.subList(0, pageSize);
            }
            // 6. 转换为HospData对象返回(包含匹配分数)
            List<HospDataWithScore> result = new ArrayList<>();
            for (HospitalMatchResult matchResult : matchResults) {
                TbHospData tbHospData = matchResult.getHospital();
                HospData hospData = convertToHospData(tbHospData);
                result.add(new HospDataWithScore(hospData, matchResult.getMatchScore()));
                logger.debug("医院: {}, 匹配分数: {}",
                    hospData.getHospName(), matchResult.getMatchScore());
            }
            logger.info("返回 {} 个医院结果", result.size());
            long totalTime = System.currentTimeMillis() - startTime;
            logger.info("搜索完成 - 总耗时: {}ms, 数据库查询: {}ms, 匹配计算: {}ms",
                totalTime, queryTime - startTime, matchTime - matchStartTime);
            return success(result);
        } catch (Exception e) {
            logger.error("分词匹配搜索失败", e);
            return error("搜索失败:" + e.getMessage());
        }
    }
    /**
     * 将TbHospData转换为HospData
     */
    private HospData convertToHospData(TbHospData tbHospData) {
        HospData hospData = new HospData();
        hospData.setHospId(tbHospData.getLegacyHospId());
        hospData.setHospName(tbHospData.getHospName());
        hospData.setHospCityId(tbHospData.getHospCityId());
        hospData.setHospShort(tbHospData.getHospShort());
        hospData.setHopsProvince(tbHospData.getHopsProvince());
        hospData.setHopsCity(tbHospData.getHopsCity());
        hospData.setHopsArea(tbHospData.getHopsArea());
        hospData.setHospAddress(tbHospData.getHospAddress());
        hospData.setHospTel(tbHospData.getHospTel());
        hospData.setHospUnitId(tbHospData.getHospUnitId());
        hospData.setHospState(tbHospData.getHospState());
        hospData.setHospOaId(tbHospData.getHospOaId());
        hospData.setHospIntroducerId(tbHospData.getHospIntroducerId());
        if (tbHospData.getHospIntroducerDate() != null) {
            hospData.setHospIntroducerDate(tbHospData.getHospIntroducerDate().toString());
        }
        hospData.setHospLevel(tbHospData.getHospLevel());
        return hospData;
    }
    /**
     * 医院匹配结果内部类
     */
    private static class HospitalMatchResult {
        private TbHospData hospital;
        private int matchScore;
        public HospitalMatchResult(TbHospData hospital, int matchScore) {
            this.hospital = hospital;
            this.matchScore = matchScore;
        }
        public TbHospData getHospital() {
            return hospital;
        }
        public int getMatchScore() {
            return matchScore;
        }
    }
    /**
     * 医院数据与匹配分数包装类
     */
    private static class HospDataWithScore {
        private HospData hospital;
        private int matchScore;
        public HospDataWithScore(HospData hospital, int matchScore) {
            this.hospital = hospital;
            this.matchScore = matchScore;
        }
        public HospData getHospital() {
            return hospital;
        }
        public int getMatchScore() {
            return matchScore;
        }
    }
}