package com.ruoyi.web.controller.system; 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 * 从MySQL的tb_hosp_data表查询医院数据 * hospId对应legacy_hosp_id字段(旧系统的医院ID) * * @author ruoyi * @date 2024-01-16 */ @RestController @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表查询) * 支持根据医院名称、地址、地域进行模糊搜索 * @param keyword 搜索关键词(医院名称、地址、简称、省市区) * @param deptId 部门ID(用于根据部门区域配置过滤医院) */ @GetMapping("/search") public AjaxResult searchHospitals( @RequestParam(value = "keyword", required = false) String keyword, @RequestParam(value = "deptId", required = false) Long deptId, @RequestParam(value = "pageSize", required = false, defaultValue = "50") Integer pageSize) { List list; // 如果keyword为空,使用部门区域过滤查询 if (keyword == null || keyword=="") { if (deptId != null) { list = hospDataMapper.searchHospitalsByDeptRegion("", deptId); } else { list = hospDataMapper.searchHospitals("", ""); } } else { list = hospDataMapper.searchHospitals(keyword, ""); } // 限制返回数量 if (pageSize != null && pageSize > 0 && list.size() > pageSize) { list = list.subList(0, pageSize); } // 确保"家中"在结果中 Integer homeHospId = hospDataMapper.getHomeHospId(); if(homeHospId > 0 && !list.isEmpty() && list.stream().noneMatch(hospData -> hospData.getHospId().equals(homeHospId))) { HospData hospData = hospDataMapper.selectHospDataById(homeHospId); if (hospData != null) { list.add(0, hospData); } } return success(list); } /** * 根据ID获取医院详情(从MySQL tb_hosp_data表查询) * @param hospId 医院ID(对应legacy_hosp_id) */ @GetMapping("/detail") public AjaxResult getHospitalDetail(@RequestParam("hospId") Integer hospId) { HospData hospital = hospDataMapper.selectHospDataById(hospId); return success(hospital); } /** * 获取常用转出医院列表(从MySQL tb_hosp_data表查询) * @param serviceOrdClass 分公司编码(service_order_class) * @param region 地域关键词(可选) * @param pageSize 返回结果数量限制(默认50) */ @GetMapping("/frequent/out") public AjaxResult getFrequentOutHospitals( @RequestParam("serviceOrdClass") String serviceOrdClass, @RequestParam(value = "region", required = false) String region, @RequestParam(value = "pageSize", required = false, defaultValue = "50") Integer pageSize) { // 查询常用转出医院ID列表 logger.info("getFrequentOutHospitals 传入的 serviceOrdClass :{}",serviceOrdClass); List hospIds = sqlHospDataService.selectFrequentOutHospitalIds(serviceOrdClass); logger.info(" getFrequentOutHospitals 查询出来的 hospIds :{}",hospIds.toArray().length); if (hospIds.isEmpty()) { return success(); } // 根据ID列表查询医院详情 List hospitals = hospDataMapper.selectHospDataByIds(hospIds, region); // 限制返回数量 if (pageSize != null && pageSize > 0 && hospitals.size() > pageSize) { hospitals = hospitals.subList(0, pageSize); } return success(hospitals); } /** * 获取常用转入医院列表(从MySQL tb_hosp_data表查询) * @param serviceOrdClass 分公司编码(service_order_class) * @param region 地域关键词(可选) * @param pageSize 返回结果数量限制(默认50) */ @GetMapping("/frequent/in") public AjaxResult getFrequentInHospitals( @RequestParam("serviceOrdClass") String serviceOrdClass, @RequestParam(value = "region", required = false) String region, @RequestParam(value = "pageSize", required = false, defaultValue = "50") Integer pageSize) { // 查询常用转入医院ID列表 logger.info("getFrequentInHospitals 传入的 serviceOrdClass {}",serviceOrdClass); List hospIds = sqlHospDataService.selectFrequentInHospitalIds(serviceOrdClass); logger.info("getFrequentInHospitals 查询出来的 hospIds {}",hospIds.toArray().length); if (hospIds.isEmpty()) { return success(); } Integer homeHospId=hospDataMapper.getHomeHospId(); // 根据ID列表查询医院详情 List hospitals = hospDataMapper.selectHospDataByIds(hospIds, region); // 限制返回数量 if (pageSize != null && pageSize > 0 && hospitals.size() > pageSize) { hospitals = hospitals.subList(0, pageSize); } if(homeHospId>0) { HospData hospData = hospDataMapper.selectHospDataById(homeHospId); hospitals.add(0,hospData); } return success(hospitals); } /** * 根据部门区域配置搜索医院(从MySQLtb_hosp_data表查询,支持省、市、县/区等多级区域) * @param keyword 搜索关键词 * @param deptId 部门ID * @param pageSize 返回结果数量限制(默认50) */ @GetMapping("/search/by-dept-region") public AjaxResult searchHospitalsByDeptRegion( @RequestParam(value = "keyword", required = false) String keyword, @RequestParam("deptId") Long deptId, @RequestParam(value = "pageSize", required = false, defaultValue = "50") Integer pageSize) { logger.info("根据部门区域配置搜索医院:deptId={}, keyword={}, pageSize={}", deptId, keyword, pageSize); // 调用Mapper查询,自动根据部门的区域配置过滤医院 List hospitals = hospDataMapper.searchHospitalsByDeptRegion(keyword, deptId); logger.info("查询到医院数量:{}", hospitals.size()); // 限制返回数量 if (pageSize != null && pageSize > 0 && hospitals.size() > pageSize) { hospitals = hospitals.subList(0, pageSize); } // 确保"家中"在结果中 Integer homeHospId = hospDataMapper.getHomeHospId(); if (homeHospId > 0 && hospitals.stream().noneMatch(h -> h.getHospId().equals(homeHospId))) { HospData homeHosp = hospDataMapper.selectHospDataById(homeHospId); if (homeHosp != null) { hospitals.add(0, homeHosp); } } 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 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 candidateHospitals = tbHospDataMapper.selectTbHospDataByKeywords(keywordList, "0"); long queryTime = System.currentTimeMillis(); logger.info("数据库预过滤完成,候选医院数量: {}, 耗时: {}ms", candidateHospitals.size(), queryTime - startTime); // 4. 提取候选医院的地区名称(从 hopsArea 字段) Set 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 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 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; } } }