| | |
| | | |
| | | 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 |
| | |
| | | @Autowired |
| | | private ISQLHospDataService sqlHospDataService; |
| | | |
| | | @Autowired |
| | | private ITbHospDataService tbHospDataService; |
| | | |
| | | @Autowired |
| | | private com.ruoyi.system.mapper.TbHospDataMapper tbHospDataMapper; |
| | | |
| | | @Autowired |
| | | private HospitalTokenizerAsyncService asyncService; |
| | | |
| | | /** |
| | | * 搜索医院(从MySQL tb_hosp_data表查询) |
| | | * 支持根据医院名称、地址、地域进行模糊搜索 |
| | |
| | | @GetMapping("/search") |
| | | public AjaxResult searchHospitals( |
| | | @RequestParam(value = "keyword", required = false) String keyword, |
| | | @RequestParam(value = "deptId", required = false) Long deptId) { |
| | | @RequestParam(value = "deptId", required = false) Long deptId, |
| | | @RequestParam(value = "pageSize", required = false, defaultValue = "50") Integer pageSize) { |
| | | List<HospData> list; |
| | | |
| | | // 如果keyword为空,使用部门区域过滤查询 |
| | |
| | | |
| | | list = hospDataMapper.searchHospitals(keyword, ""); |
| | | |
| | | } |
| | | |
| | | // 限制返回数量 |
| | | if (pageSize != null && pageSize > 0 && list.size() > pageSize) { |
| | | list = list.subList(0, pageSize); |
| | | } |
| | | |
| | | // 确保"家中"在结果中 |
| | |
| | | * 获取常用转出医院列表(从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 = "region", required = false) String region, |
| | | @RequestParam(value = "pageSize", required = false, defaultValue = "50") Integer pageSize) { |
| | | // 查询常用转出医院ID列表 |
| | | logger.info("getFrequentOutHospitals 传入的 serviceOrdClass :{}",serviceOrdClass); |
| | | List<Integer> hospIds = sqlHospDataService.selectFrequentOutHospitalIds(serviceOrdClass); |
| | |
| | | |
| | | // 根据ID列表查询医院详情 |
| | | List<HospData> 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 = "region", required = false) String region, |
| | | @RequestParam(value = "pageSize", required = false, defaultValue = "50") Integer pageSize) { |
| | | // 查询常用转入医院ID列表 |
| | | logger.info("getFrequentInHospitals 传入的 serviceOrdClass {}",serviceOrdClass); |
| | | List<Integer> hospIds = sqlHospDataService.selectFrequentInHospitalIds(serviceOrdClass); |
| | |
| | | |
| | | // 根据ID列表查询医院详情 |
| | | List<HospData> 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); |
| | | HospData hospData = hospDataMapper.selectHospDataById(homeHospId); |
| | | hospitals.add(0,hospData); |
| | | } |
| | | return success(hospitals); |
| | | } |
| | | |
| | | /** |
| | | * 根据部门区域配置搜索医院(从MySQL tb_hosp_data表查询,支持省、市、县/区等多级区域) |
| | | * 根据部门区域配置搜索医院(从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) { |
| | | logger.info("根据部门区域配置搜索医院:deptId={}, keyword={}", deptId, keyword); |
| | | @RequestParam("deptId") Long deptId, |
| | | @RequestParam(value = "pageSize", required = false, defaultValue = "50") Integer pageSize) { |
| | | logger.info("根据部门区域配置搜索医院:deptId={}, keyword={}, pageSize={}", deptId, keyword, pageSize); |
| | | |
| | | // 调用Mapper查询,自动根据部门的区域配置过滤医院 |
| | | List<HospData> 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(); |
| | |
| | | |
| | | 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 > 0) { |
| | | matchResults.add(new HospitalMatchResult(hospital, matchScore)); |
| | | } |
| | | } |
| | | |
| | | long matchTime = System.currentTimeMillis(); |
| | | logger.info("匹配计算完成,找到 {} 个匹配的医院,耗时: {}ms", matchResults.size(), matchTime - matchStartTime); |
| | | |
| | | // 4. 按匹配分数降序排序(分数越高排名越靠前) |
| | | matchResults.sort(Comparator.comparingInt(HospitalMatchResult::getMatchScore).reversed()); |
| | | |
| | | // 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; |
| | | } |
| | | } |
| | | } |