查询绑定患者健康数据设计.md 9.0 KB

查询绑定患者健康数据设计

概述

当患者与医生/家属建立绑定关系后,允许被绑定的医生或家属查询该患者的健康数据(血糖、血压、心率、体征等)。本设计描述目标、接口、权限校验、实现要点、测试与文档规范,并参照项目已经存在的经验文档(如 ID 精度、Swagger 泛型文档化)。

背景和目标

  • 背景:已有用户绑定(患者 ↔ 医生/家属)功能,且患者在系统内可记录多类健康数据。为了满足医生/家属随访与监护需求,需要允许绑定方访问患者数据。
  • 目标:在保证安全、最小权限原则、并兼顾 API 统一性的前提下实现“绑定方查询患者健康数据”接口。

需求与约束

  1. 认证:接口需走已有的Token认证(AuthInterceptor),并依赖 request attribute 中的 currentUserId
  2. 绑定检查:接口必须校验当前请求用户(boundUser)与目标患者(patientUserId)之间是否存在有效绑定关系(UserBindingService.checkUserBinding)。
  3. 权限粒度:区分绑定类型(DOCTOR / FAMILY),医生和家属的可见字段或时间范围可不同(设计建议见下)。
  4. 日志审计:所有第三方查询患者数据的调用要记录访问日志,便于审计。
  5. API 兼容:使用与现有分页查询一致的参数和返回格式(R<T> + Page<T> VO)。
  6. 文档与前端:参照 docs/Swagger泛型返回类型字段信息不显示问题解决方案.md,在 Controller 层使用 @ApiResponse@Schema(implementation = ...) 标注返回类型;参照 docs/前端ID精度丢失问题解决方案.md 将 id 字段返回为 string。

设计要点(高层)

  1. 在每类健康数据 Controller(BloodGlucoseDataController, BloodPressureDataController, HeartRateDataController, PhysicalDataController 等)增加接口 list-by-bound-user

    • HTTP 方法:POST(与现有 list 同形式),路径示例:/blood-glucose/list-by-bound-user
    • 参数:patientUserId(Long)、bindingType(String, 可选)、BaseQueryRequest(分页、时间范围)
    • 身份鉴权:不允许在 body 内传入 boundUserId,使用 AuthInterceptor 提供的 currentUserId 来判断请求人的身份。
    • 核心逻辑:先 userBindingService.checkUserBinding(patientUserId, currentUserId),若不存在返回 R.fail(ErrorCode.DATA_ACCESS_DENIED);否则继续查询并返回数据。
  2. Service 层提供 listDataByPatient(patientUserId, BaseQueryRequest) 等方法,返回 Page<TResponse> 供 Controller 组装对应 PageResponse VO。

  3. 权限策略(建议):

    • DOCTOR:默认具有查询所有字段的权限;可以查看所有历史数据(受分页限制)。
    • FAMILY:只允许查询近 N 天(例如 365 天内)或限制敏感字段(如“医生诊断备注”等),并在返回字段中做适当脱敏/限制。绑定关系可在 CreateUserBindingRequest 中声明。
  4. 审计日志:

    • 在 Controller 的 list-by-bound-user 接口中记录 boundUserId, patientUserId, queryType, startTime, endTime,写入到 regular logs 或独立审计表(参考 docs/DevRule/06-日志和错误处理规范.md)。

API 设计示例

示例 - 血糖数据:

Controller 片段(示例,仅说明用法)

@Operation(summary = "医生/家属分页查询患者血糖数据", description = "绑定方查询患者血糖测量记录(需有绑定关系)")
@ApiResponses(value = {
	@ApiResponse(responseCode = "200", description = "成功查询血糖数据列表",
		content = @Content(mediaType = "application/json",
			schema = @Schema(implementation = work.baiyun.chronicdiseaseapp.model.vo.BloodGlucoseDataPageResponse.class))),
	@ApiResponse(responseCode = "403", description = "无权限访问",
		content = @Content(mediaType = "application/json",
			schema = @Schema(implementation = Void.class)))
})
@PostMapping(path = "/list-by-bound-user", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public R<?> listByBoundUser(Long patientUserId, String bindingType, @RequestBody BaseQueryRequest req, HttpServletRequest request) {
	Long boundUserId = (Long) request.getAttribute("currentUserId");

	// 1. 校验参数
	if (patientUserId == null) {
		return R.fail(ErrorCode.PARAMETER_ERROR.getCode(), ErrorCode.PARAMETER_ERROR.getMessage());
	}

	// 2. 校验绑定关系
	CheckUserBindingRequest checkReq = new CheckUserBindingRequest();
	checkReq.setPatientUserId(patientUserId);
	checkReq.setBoundUserId(boundUserId);
	CheckUserBindingResponse checkResp = userBindingService.checkUserBinding(checkReq);
	if (!checkResp.getExists()) {
		return R.fail(ErrorCode.DATA_ACCESS_DENIED.getCode(), "当前用户与目标患者未建立绑定关系");
	}

	// 3. 依据权限决定查询逻辑(可在 service 层统一处理)
	Page<BloodGlucoseDataResponse> page = service.listBloodGlucoseDataByPatient(patientUserId, bindingType, req);
	// 下面同 list 接口组装 PageResponse,返回
}

Service 层新增方法签名示例:

Page<BloodGlucoseDataResponse> listBloodGlucoseDataByPatient(Long patientUserId, String bindingType, BaseQueryRequest request);

实现要点:

  • 在 mapper 查询条件中替换 eq(BloodGlucoseData::getUserId, userId) 为 patientUserId。
  • 对 FAMILY 限制时间范围(例如:request.setStartTime >= now - 365 天);或在 mapper 层追加条件。

Swagger 与 API 文档

  • 统一使用 @ApiResponse + @Content + @Schema(implementation = ...) 来避免泛型类型在文档中缺失(参考 docs/Swagger泛型返回类型...)。
  • 所有 VO 的 id 字段在响应层转换成 String(参见 docs/前端ID精度...)。

示例:

@ApiResponse(responseCode = "200", description = "查询成功",
	content = @Content(mediaType = "application/json",
		schema = @Schema(implementation = work.baiyun.chronicdiseaseapp.model.vo.BloodGlucoseDataPageResponse.class)))

权限/业务细节与差异化策略

  1. bindingType = DOCTOR
    • 默认开放全部字段及历史记录(分页受 request 限制)。
  2. bindingType = FAMILY
    • 可限定时间窗口(例如 365 天)和/或禁止返回某些敏感字段(如直接的医疗诊断、医生备注),也可在 VO 中对字段进行脱敏。
  3. bindingType 为空:由 userBindingService.checkUserBinding 返回的绑定类型决定权限。

实现小技巧:在 UserBindingService 中返回绑定类型(CheckUserBindingResponse 已有),Controller/Service 使用该返回值作为权限判断依据。

日志 & 审计

  1. 写入标准访问日志,格式包括 boundUserId, patientUserId, bindingType, operation, queryStart, queryEnd, resultCount
  2. 如需合规审计,可在数据库中建立审计表 patient_data_access_log 记录详细调用历史;字段示例:id、access_time、accessor_id、patient_id、data_type、start_time、end_time、ip_address。

测试建议

  1. 单元测试:模拟 UserBindingService 返回不同绑定关系,验证 listByBoundUser 返回或拒绝的逻辑(正常与未绑定)。
  2. 集成测试:启动应用(或测试环境),分别用 doctor / family 的 token 发起查询请求,校验返回结果字段及分页。注意验证 ID 字段为字符串。
  3. 性能测试:对长时间范围查询与频繁查询场景做压测,评估 DB 索引是否充分;若发现慢查询,建议加索引(例如测量时间 idx)或靠缓存(例如热点患者数据)优化。

兼容性与前端要求

  1. 前端应使用 token 登录并把 token 放在 Authorization: Bearer <token> 中。AuthInterceptor 将解析并注入 currentUserId
  2. 前端需要了解 id 字段为字符串,不要当作 Number 处理(参考 docs/前端ID精度...)。
  3. 文档中明确标注字段是否对家属脱敏或受时间窗口限制。

迁移/变更管理

  • 该功能应由小步提交:先实现后端接口和测试,再在 Swagger 文档中公开,最后通知前端(以便正确处理 id 字符串与权限错误)。

开发任务清单

  1. Controller:在 BloodGlucoseDataController, BloodPressureDataController, HeartRateDataController, PhysicalDataController 等实现 list-by-bound-user
  2. Service:添加 listByPatient 变体并对 bindingType 做权限过滤。
  3. Mapper:如必要,添加 AND user_id = #{patientUserId} 的查询条件。
  4. Unit / Integration Tests:覆盖权限校验、数据返回与分页。
  5. 日志 & 审计:实现访问日志或可选审计表。

参考

  • docs/前端ID精度丢失问题解决方案.md
  • docs/Swagger泛型返回类型字段信息不显示问题解决方案.md
  • docs/DevRule/03-API设计规范.md
  • UserBindingController.java / UserBindingService.java

责任人

  • 设计:后端开发团队
  • 实现:后端 API 开发者
  • 联调:后端 + 前端

以上为实现“绑定方查询患者健康数据”功能的设计文档草案,若需要我可以继续生成 Controller / Service 的代码模版以及对应的测试用例。