feat(emergency): searchExpert接口返回ConDoctor专家数据 + 修复IM UserSig缓存key未区分secretKey的问题

1. searchExpert新增ConDoctor专家字段:
   - 新增ConDoctorMapper直接查询qh_con_doctor表(同库,避免跨模块依赖)
   - EmergencyProfessionUser新增17个@TableField(exist=false)字段承载专家数据
   - searchExpert方法中批量查询ConDoctor并逐字段填充
   - 用户非专家时ConDoctor字段为null
   - 更新API文档-移动端接口.md

2. IM签名缓存优化:
   - TencentCloudImUtil新增secretKey注入
   - Redis key从 silence:im_user_sig:{sdkAppId}:{id}
     改为 silence:im_user_sig:{hash(sdkAppId+secretKey)}:{id}
   - 解决secretKey变更时旧缓存签名仍被命中导致IM请求失败的问题

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-04 11:08:03 +08:00
co-authored by Claude
parent 44e8ac7da5
commit e9360d1ad9
5 changed files with 171 additions and 2 deletions
+27 -1
View File
@@ -50,7 +50,15 @@
"userId": "xxx", "realname": "张医生", "avatar": "https://...",
"type": "6", "centerId": "xxx",
"post": "主任医师", "school": "北京大学", "edu": "博士",
"goodAt": "心血管内科", "deptName": "急诊科", "phone": "13800000001"
"goodAt": "心血管内科", "deptName": "急诊科", "phone": "13800000001",
"doctorName": "张医生", "doctorNo": "XJ-001", "photo": "https://...",
"resourceName": "新疆人民医院", "departmentId": "xxx",
"departmentName": "急诊科", "doctorTitle": "主任医师",
"doctorJob": "科长", "doctorStatus": "1", "isAccept": "1",
"doctorType": "1", "audioStatus": "1", "tfJumpHoliday": "0",
"experience": "从事心血管内科临床工作20年...",
"sort": 100, "tfRecommend": "1",
"introduction": "擅长心血管疾病的诊治..."
}
],
"total": 50, "size": 10, "current": 1
@@ -70,6 +78,24 @@
| goodAt | 擅长领域 |
| deptName | 科室 |
| phone | 手机号 |
| **↓ 以下字段来源于咨询服务专家表(qh_con_doctor),非专家时为 null** |
| doctorName | 医生姓名 |
| doctorNo | 专家编号 |
| photo | 医生头像 |
| resourceName | 所属医院名称 |
| departmentId | 科室ID |
| departmentName | 科室名称 |
| doctorTitle | 职务(z_doct_lev 字典) |
| doctorJob | 职称(z_doct_job 字典) |
| doctorStatus | 专家状态(doc_status 字典) |
| isAccept | 是否开启咨询(z_doct_sta 字典: 1开启/3关闭) |
| doctorType | 专家类型(z_doct_typ 字典: 1收费/2免费/3其他/4义诊) |
| audioStatus | 是否提供音视频 |
| tfJumpHoliday | 是否跳过节假日 |
| experience | 执业经历 |
| sort | 排序 |
| tfRecommend | 是否推荐(0否/1是) |
| introduction | 简介 |
### 1.2 搜索员工 `GET /order/searchEmployee`
@@ -134,4 +134,62 @@ public class EmergencyProfessionUser implements Serializable {
// 手机号(非持久字段,查询时填充)
@TableField(exist = false)
private String phone;
// ===== 以下字段来源于 qh_con_doctor(咨询服务医生表),查询时批量填充 =====
// 医生姓名
@TableField(exist = false)
private String doctorName;
// 专家编号
@TableField(exist = false)
private String doctorNo;
// 医生头像
@TableField(exist = false)
private String photo;
// 医院名称
@TableField(exist = false)
private String resourceName;
// 科室id
@TableField(exist = false)
private String departmentId;
// 科室名称
@TableField(exist = false)
private String departmentName;
// 职务
@TableField(exist = false)
@Dict(dicCode = "z_doct_lev")
private String doctorTitle;
// 职称
@TableField(exist = false)
@Dict(dicCode = "z_doct_job")
private String doctorJob;
// 专家状态
@TableField(exist = false)
@Dict(dicCode = "doc_status")
private String doctorStatus;
// 是否开启咨询(1开启 3关闭)
@TableField(exist = false)
@Dict(dicCode = "z_doct_sta")
private String isAccept;
// 专家类型(z_doct_typ) —— 避免与人员type冲突,命名为doctorType
@TableField(exist = false)
@Dict(dicCode = "z_doct_typ")
private String doctorType;
// 是否提供音视频
@TableField(exist = false)
private String audioStatus;
// 是否跳过节假日
@TableField(exist = false)
private String tfJumpHoliday;
// 执业经历
@TableField(exist = false)
private String experience;
// 排序
@TableField(exist = false)
private Integer sort;
// 是否推荐(0否1是)
@TableField(exist = false)
private String tfRecommend;
// 简介
@TableField(exist = false)
private String introduction;
}
@@ -80,6 +80,8 @@ public class EmergencyOrderApiServiceImpl implements EmergencyOrderApiService {
@Autowired
private EmergencyProfessionUserMapper emergencyProfessionUserMapper;
@Autowired
private ConDoctorMapper conDoctorMapper;
@Autowired
private ISysBaseAPI sysBaseAPI;
@Autowired
private EmergencyCloudManager emergencyCloudManager;
@@ -960,6 +962,41 @@ public class EmergencyOrderApiServiceImpl implements EmergencyOrderApiService {
}
});
}
// 批量查询 con_doctor 专家资料,补充专家字段
if (!userIds.isEmpty()) {
List<EmergencyProfessionUser> doctorList = conDoctorMapper.selectDoctorByIds(userIds);
if (CollectionUtils.isNotEmpty(doctorList)) {
Map<String, EmergencyProfessionUser> doctorMap = doctorList.stream()
.collect(Collectors.toMap(EmergencyProfessionUser::getUserId, e -> e, (a, b) -> a));
result.getRecords().forEach(prof -> {
EmergencyProfessionUser doc = doctorMap.get(prof.getUserId());
if (doc == null) {
return;
}
prof.setDoctorName(doc.getDoctorName());
prof.setDoctorNo(doc.getDoctorNo());
prof.setPhoto(doc.getPhoto());
prof.setResourceName(doc.getResourceName());
prof.setDepartmentId(doc.getDepartmentId());
prof.setDepartmentName(doc.getDepartmentName());
prof.setDoctorTitle(doc.getDoctorTitle());
prof.setDoctorJob(doc.getDoctorJob());
prof.setDoctorStatus(doc.getDoctorStatus());
prof.setIsAccept(doc.getIsAccept());
prof.setDoctorType(doc.getDoctorType());
prof.setAudioStatus(doc.getAudioStatus());
prof.setTfJumpHoliday(doc.getTfJumpHoliday());
prof.setExperience(doc.getExperience());
prof.setSort(doc.getSort());
prof.setTfRecommend(doc.getTfRecommend());
prof.setIntroduction(doc.getIntroduction());
// 已有字段,con_doctor 有值时覆盖为更权威的专家资料
if (StrUtil.isNotBlank(doc.getGoodAt())) prof.setGoodAt(doc.getGoodAt());
if (StrUtil.isNotBlank(doc.getSchool())) prof.setSchool(doc.getSchool());
if (StrUtil.isNotBlank(doc.getEdu())) prof.setEdu(doc.getEdu());
});
}
}
return result;
}
@@ -0,0 +1,38 @@
package com.renkang.emergency.mapper;
import com.renkang.emergency.entity.EmergencyProfessionUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 咨询服务医生表(qh_con_doctor) 查询
* 说明:health-emergency 与 health-consultation 共用同一数据库,
* 但模块间无依赖,故在应急模块内直接查询 qh_con_doctor。
*/
@Mapper
public interface ConDoctorMapper {
/**
* 根据医生id(=应急用户userId)批量查询专家资料
* 仅返回有效医生:del_flag='0' 且 status='1'
*
* @param doctorIds 医生id集合
* @return 以 EmergencyProfessionUser 承载 con_doctor 字段(仅填充新增的 exist=false 字段与 goodAt/school/edu
*/
@Select("<script>" +
"SELECT id AS userId, " +
"doctor_name AS doctorName, doctor_no AS doctorNo, photo, " +
"resource_name AS resourceName, department_id AS departmentId, department_name AS departmentName, " +
"doctor_title AS doctorTitle, doctor_job AS doctorJob, doctor_status AS doctorStatus, " +
"is_accept AS isAccept, type AS doctorType, audio_status AS audioStatus, tf_jump_holiday AS tfJumpHoliday, " +
"experience, good_at AS goodAt, school, e_q AS edu, sort, " +
"tf_recommend AS tfRecommend, introduction " +
"FROM qh_con_doctor " +
"WHERE del_flag = '0' AND status = '1' AND id IN " +
"<foreach collection='doctorIds' item='id' open='(' separator=',' close=')'>#{id}</foreach>" +
"</script>")
List<EmergencyProfessionUser> selectDoctorByIds(@Param("doctorIds") List<String> doctorIds);
}
@@ -52,6 +52,8 @@ public class TencentCloudImUtil {
@Value("${IMConfig.sdkAppId}")
private long sdkAppId;
@Value("${IMConfig.secretKey}")
private String secretKey;
@Autowired
private TximUtil tximUtil;
@@ -73,8 +75,16 @@ public class TencentCloudImUtil {
return System.currentTimeMillis() + "" + StrUtil.subSuf(Math.random() + "", 2);
}
/**
* 获取IM配置指纹,用于区分不同配置的缓存
* 当 sdkAppId 或 secretKey 变更时指纹变化,旧缓存自动失效
*/
private String getConfigFingerprint() {
return Integer.toHexString(Objects.hash(sdkAppId, secretKey));
}
private String buildUserSigKey(String identifier) {
return REDIS_IM_USER_SIG_PREFIX + sdkAppId + ":" + identifier;
return REDIS_IM_USER_SIG_PREFIX + getConfigFingerprint() + ":" + identifier;
}
/**