站内消息功能设计文档.md 16 KB

站内消息功能设计文档

本文档遵循《03-API设计规范》《06-日志和错误处理规范》《07-数据库规范》《08-安全规范》《Swagger泛型返回类型字段信息不显示问题解决方案》《前端ID精度丢失问题解决方案》等项目规范。

概述

为提升用户互动体验,系统需支持站内消息功能,包括医生向患者/家属发送提醒、系统自动检测异常数据并发送通知。消息采用文本格式,支持已读/未读状态管理,集成推送通知,仅依赖MySQL+Spring Boot。

消息类型通过MessageType枚举定义,包括SYSTEM_DAILY(系统日常消息)、DOCTOR(医生下发的消息)、SYSTEM_ANOMALY(异常数据通知),并通过MessageTypeHandler实现与数据库的自动映射转换。

背景和目标

  • 背景:当前APP缺乏实时消息通知功能,用户无法及时收到医生提醒或异常数据警告。
  • 目标:实现消息的发送、管理和浏览功能,支持推送通知、状态管理,接口风格与现有API统一。

需求与约束

  1. 认证:接口需走Token认证(AuthInterceptor),依赖currentUserId,未认证返回401。
  2. 存储:消息内容存储于MySQL,无附件支持。
  3. 数据库:新增消息相关表,存储发送者、接收者、内容、状态等。
  4. 访问控制:医生可发送消息,患者和家属可接收和管理消息。
  5. API兼容:接口风格与现有API一致,所有响应均为R<T>格式,状态码、错误码、消息体与规范一致。
  6. 日志审计:所有操作需用SLF4J记录info级日志,异常需error级,内容含userId、操作等。
  7. Swagger文档:所有接口需用@ApiResponse+@Content+@Schema标注,确保泛型返回类型文档化。
  8. ID精度:所有响应中的ID字段在VO层转为String,避免前端精度丢失。

设计要点(高层)

  1. Controller层新增接口:
    • 医生发送消息:POST /messages/send,参数包含接收者、内容等。
    • 系统发送日常消息:POST /messages/system-daily-send
    • 系统发送异常通知:POST /messages/system-anomaly-send
    • 获取消息列表:GET /messages/list,支持分页、状态筛选。
    • 获取消息详情:GET /messages/{id}
    • 标记已读:PUT /messages/{id}/read
    • 删除消息:DELETE /messages/{id}
    • 获取未读数量:GET /messages/unread-count
    • 医生获取患者及其家属消息列表:GET /messages/doctor/patient/{patientId},返回结构化的患者和家属消息数据。
  2. Service层实现消息CRUD、状态管理、推送逻辑、数据库操作、异常处理、日志记录等逻辑。异常检测集成到健康数据上传流程。
  3. 消息类型管理:使用MessageType枚举定义消息类型,通过MessageTypeHandler实现MyBatis枚举与数据库VARCHAR字段的自动转换。
  4. 推送集成参考现有微信集成,使用订阅消息。

订阅消息发送前授权检查

在发送订阅推送(notify_subscribe=1)前,服务端应读取 t_patient_reminder 中的提醒设置,并同时判断:

  • is_notification_enabled 为 1(消息总开关开启)
  • is_subscription_available 为 1(订阅授权可用)

若任一条件不满足,应跳过调用订阅推送接口。服务端代码中示例实现:

PatientReminder pr = patientReminderMapper.selectByPatientUserId(receiverId);
if (pr != null) {
  if (pr.getNotificationEnabled() == null || pr.getNotificationEnabled() != 1) {
    canSend = false;
  }
  if (pr.getSubscriptionAvailable() == null || pr.getSubscriptionAvailable() != 1) {
    canSend = false;
  }
}
  • 家属推送同样适用该检查。
  • 注意:消息表的 notify_subscribe 字段表示发送意图,实际发送仍以 t_patient_reminder 的授权与总开关为准。

接口清单

API设计示例

医生发送消息接口

@Operation(summary = "医生发送消息", description = "医生向患者/家属发送站内消息")
@ApiResponses(value = {
  @ApiResponse(responseCode = "200", description = "发送成功",
    content = @Content(mediaType = "application/json",
      schema = @Schema(implementation = R.class))),
  @ApiResponse(responseCode = "400", description = "参数错误",
    content = @Content(mediaType = "application/json",
      schema = @Schema(implementation = R.class))),
  @ApiResponse(responseCode = "401", description = "未认证",
    content = @Content(mediaType = "application/json",
      schema = @Schema(implementation = R.class)))
})
@PostMapping("/messages/send")
public R<String> sendMessage(@RequestBody SendMessageRequest request) {
  // 校验参数、保存消息、触发推送、返回ID(String格式)
}

获取消息列表接口

@Operation(summary = "获取消息列表", description = "分页获取用户消息列表")
@ApiResponses(value = {
  @ApiResponse(responseCode = "200", description = "获取成功",
    content = @Content(mediaType = "application/json",
      schema = @Schema(implementation = R.class)))
})
@GetMapping("/messages/list")
public R<Page<MessageVO>> getMessageList(@RequestParam(defaultValue = "1") int page,
                                         @RequestParam(defaultValue = "10") int size,
                                         @RequestParam(required = false) Integer status) {
  // 返回分页列表,ID转为String
}

医生获取患者及其家属消息列表接口

@Operation(summary = "医生获取患者及其家属消息列表", description = "医生查看指定患者及其所有家属的消息记录")
@ApiResponses(value = {
  @ApiResponse(responseCode = "200", description = "获取成功",
    content = @Content(mediaType = "application/json",
      schema = @Schema(implementation = R.class))),
  @ApiResponse(responseCode = "403", description = "权限不足",
    content = @Content(mediaType = "application/json",
      schema = @Schema(implementation = R.class))),
  @ApiResponse(responseCode = "404", description = "患者不存在",
    content = @Content(mediaType = "application/json",
      schema = @Schema(implementation = R.class)))
})
@GetMapping("/messages/doctor/patient/{patientId}")
public R<DoctorPatientMessagesVO> getPatientAndFamilyMessages(@PathVariable String patientId) {
  // 校验医生权限、查询患者及其家属消息、返回结构化数据
}

配置示例(application.yml)

# MyBatis配置 - 注册MessageTypeHandler
mybatis:
  type-handlers-package: work.baiyun.chronicdiseaseapp.handler

# 其他配置复用现有推送和数据库配置

数据库字段设计与兼容性

消息表 (t_messages)

  • id: BIGINT PRIMARY KEY AUTO_INCREMENT(雪花算法生成,响应时转为String)
  • sender_id: BIGINT NOT NULL, 发送者ID(系统消息为0)
  • receiver_id: BIGINT NOT NULL, 接收者ID
  • content: TEXT NOT NULL, 消息内容
  • content_format: VARCHAR(20) DEFAULT 'PLAIN', 内容格式
  • type: VARCHAR(50) NOT NULL, 消息类型(使用MessageType枚举:SYSTEM_DAILY-系统日常消息,DOCTOR-医生下发的消息,SYSTEM_ANOMALY-异常数据通知)
  • notify_popup: TINYINT DEFAULT 0, 是否弹窗
  • notify_subscribe: TINYINT DEFAULT 0, 是否订阅推送
  • status: TINYINT DEFAULT 0, 状态(0未读1已读2已删除)
  • read_time: DATETIME, 阅读时间
  • is_deleted: TINYINT DEFAULT 0, 软删除标志
  • create_time: DATETIME DEFAULT CURRENT_TIMESTAMP(BaseEntity字段)
  • update_time: DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(BaseEntity字段)
  • create_user: BIGINT(BaseEntity字段,创建者ID,转为String)
  • update_user: BIGINT(BaseEntity字段,更新者ID,转为String)
  • version: INT DEFAULT 0(BaseEntity字段,乐观锁版本号)
  • remark: VARCHAR(255)(BaseEntity字段,可选备注)

注:实体类继承BaseEntity,自动包含上述公共字段。id、create_user、update_user在JSON响应中通过ToStringSerializer转为String。

消息类型枚举设计

MessageType枚举

package work.baiyun.chronicdiseaseapp.enums;

public enum MessageType {
    SYSTEM_DAILY("系统日常消息"),
    DOCTOR("医生下发的消息"),
    SYSTEM_ANOMALY("异常数据通知");

    private final String description;

    MessageType(String description) {
        this.description = description;
    }

    public String getDescription() {
        return description;
    }
}

MessageTypeHandler类型处理器

package work.baiyun.chronicdiseaseapp.handler;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;
import work.baiyun.chronicdiseaseapp.enums.MessageType;

@MappedTypes(MessageType.class)
public class MessageTypeHandler extends BaseTypeHandler<MessageType> {
    // 处理MessageType枚举与数据库VARCHAR字段间的转换
    // 存储时:enum.name() -> 数据库字符串
    // 查询时:数据库字符串 -> MessageType.valueOf(name)
}

注:MessageTypeHandler继承自MyBatis的BaseTypeHandler,实现枚举与数据库字段的自动转换。通过@MappedTypes注解指定处理的枚举类型,确保数据库中的VARCHAR字段能正确映射为MessageType枚举值。

数据传输对象设计

请求VO

SendMessageRequest(医生发送消息请求)

@Schema(description = "医生发送消息请求")
public class SendMessageRequest {
    @Schema(description = "接收者ID列表", required = true)
    @NotEmpty(message = "接收者不能为空")
    private List<Long> receiverIds;

    @Schema(description = "消息内容", required = true)
    @NotBlank(message = "内容不能为空")
    private String content;

    @Schema(description = "内容格式", example = "PLAIN")
    private String contentFormat = "PLAIN";

    @Schema(description = "消息类型", required = true, example = "DOCTOR")
    @NotNull(message = "类型不能为空")
    private MessageType type;

    @Schema(description = "是否通知家属")
    private Boolean notifyFamily = false;

    @Schema(description = "是否弹窗")
    private Boolean notifyPopup;

    @Schema(description = "是否订阅推送")
    private Boolean notifySubscribe;
}

SystemDailySendRequest(系统发送日常消息请求)

@Schema(description = "系统发送日常消息请求")
public class SystemDailySendRequest {
    @Schema(description = "接收者ID列表", required = true)
    @NotEmpty(message = "接收者不能为空")
    private List<Long> receiverIds;

    @Schema(description = "消息标题")
    private String title;

    @Schema(description = "消息内容", required = true)
    @NotBlank(message = "内容不能为空")
    private String content;

    @Schema(description = "消息类型", required = true, example = "SYSTEM_DAILY")
    @NotNull(message = "类型不能为空")
    private MessageType type = MessageType.SYSTEM_DAILY;
}

SystemAnomalySendRequest(系统发送异常通知请求)

@Schema(description = "系统发送异常通知请求")
public class SystemAnomalySendRequest {
    @Schema(description = "患者ID", required = true)
    @NotNull(message = "患者ID不能为空")
    private Long patientId;

    @Schema(description = "异常数据详情")
    private Object anomalyData;

    @Schema(description = "消息类型", required = true, example = "SYSTEM_ANOMALY")
    @NotNull(message = "类型不能为空")
    private MessageType type = MessageType.SYSTEM_ANOMALY;
}

响应VO

MessageVO(消息信息响应)

@Schema(description = "消息信息")
public class MessageVO {
    @Schema(description = "消息ID", example = "1234567890123456789")
    private String id;

    @Schema(description = "发送者ID", example = "1234567890123456789")
    private String senderId;

    @Schema(description = "发送者姓名")
    private String senderName;

    @Schema(description = "接收者ID", example = "1234567890123456789")
    private String receiverId;

    @Schema(description = "消息内容")
    private String content;

    @Schema(description = "内容格式", example = "PLAIN")
    private String contentFormat;

    @Schema(description = "消息类型", example = "DOCTOR")
    private MessageType type;

    @Schema(description = "是否弹窗")
    private Boolean notifyPopup;

    @Schema(description = "是否订阅推送")
    private Boolean notifySubscribe;

    @Schema(description = "状态", example = "0")
    private Byte status;

    @Schema(description = "阅读时间", example = "2023-12-01 10:00:00")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime readTime;

    @Schema(description = "创建时间", example = "2023-12-01 10:00:00")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
}

注:所有ID字段在VO中定义为String类型,避免前端精度丢失。时间字段使用@JsonFormat格式化。senderName通过注入UserInfoMapper查询UserInfo.nickname填充。type字段使用MessageType枚举类型,在JSON序列化时会转换为枚举名称字符串。

DoctorPatientMessagesVO(医生患者消息响应)

@Schema(description = "医生患者消息响应")
public class DoctorPatientMessagesVO {
    @Schema(description = "患者消息列表")
    private List<MessageVO> patientMessages;

    @Schema(description = "家属消息列表")
    private List<MessageVO> familyMessages;
}

注:该VO用于医生查看指定患者及其家属的消息记录,将患者和家属的消息分别组织在不同的列表中,便于前端展示和管理。

联动查询设计

消息模块涉及跨表查询用户信息,为填充MessageVO中的senderName字段:

  • 注入依赖:MessageServiceImpl注入UserInfoMapper。
  • 查询逻辑:查询消息列表或详情时,根据message.sender_id调用userInfoMapper.selectById(senderId),获取UserInfo对象。
  • 字段映射:将userInfo.getNickname()赋值给messageVO.setSenderName()。
  • 异常处理:若用户信息不存在,senderName设为空字符串或默认值,避免查询失败。
  • 性能优化:对于列表查询,可批量查询用户信息,或使用缓存减少数据库访问。

权限与安全

  • 发送接口仅医生/管理员角色可操作。
  • 接收接口公开,但需认证。
  • 医生专用接口(如获取患者及其家属消息列表)仅医生角色可访问,且需验证医生与患者的绑定关系。

日志与审计

  • 操作日志:[MessageOperation] userId={}, action={}, messageId={}
  • 异常日志:[MessageError] userId={}, error={}
  • 使用SLF4J,日志级别INFO/ERROR。
  • ID在日志中以字符串记录。

测试建议

  1. 单元测试:CRUD逻辑、状态管理。
  2. 集成测试:接口联调、推送触发。
  3. 性能测试:列表分页、状态更新。

兼容性与前端要求

  • 前端需支持消息列表、详情、状态管理。
  • 响应中ID字段为String,MessageType枚举字段在JSON中以字符串形式返回(如"DOCTOR")。
  • 前端可根据消息类型字符串进行相应的UI展示和逻辑处理。

迁移/变更管理

  • 先实现后端接口,测试通过后前端接入。
接口路径 方法 描述 权限要求 请求体/参数 响应
/messages/send POST 医生发送消息 医生 SendMessageRequest (receiverIds, content, etc.) R (消息ID)
/messages/system-daily-send POST 系统发送日常消息 管理员 SystemDailySendRequest (receiverIds, title, content) R (消息ID)
/messages/system-anomaly-send POST 系统发送异常通知 系统/管理员 SystemAnomalySendRequest (patientId, anomalyData) R (消息ID)
/messages/list GET 获取消息列表(分页) 患者/家属 查询参数: page, size, status R>
/messages/{id} GET 获取消息详情 患者/家属 路径参数: id R
/messages/{id}/read PUT 标记消息已读 患者/家属 路径参数: id R
/messages/{id} DELETE 删除消息 患者/家属 路径参数: id R
/messages/unread-count GET 获取未读消息数量 患者/家属 R
/messages/doctor/patient/{patientId} GET 医生获取患者及其家属消息列表 医生 路径参数: patientId R