|
@@ -0,0 +1,265 @@
|
|
|
|
|
+# 站内消息功能设计文档
|
|
|
|
|
+
|
|
|
|
|
+> 本文档遵循《03-API设计规范》《06-日志和错误处理规范》《07-数据库规范》《08-安全规范》《Swagger泛型返回类型字段信息不显示问题解决方案》《前端ID精度丢失问题解决方案》等项目规范。
|
|
|
|
|
+
|
|
|
|
|
+## 概述
|
|
|
|
|
+为提升用户互动体验,系统需支持站内消息功能,包括医生向患者/家属发送提醒、系统自动检测异常数据并发送通知。消息采用文本格式,支持已读/未读状态管理,集成推送通知,仅依赖MySQL+Spring Boot。
|
|
|
|
|
+
|
|
|
|
|
+## 背景和目标
|
|
|
|
|
+- 背景:当前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`。
|
|
|
|
|
+2. Service层实现消息CRUD、状态管理、推送逻辑、数据库操作、异常处理、日志记录等逻辑。异常检测集成到健康数据上传流程。
|
|
|
|
|
+3. 推送集成参考现有微信集成,使用订阅消息。
|
|
|
|
|
+
|
|
|
|
|
+## 接口清单
|
|
|
|
|
+
|
|
|
|
|
+| 接口路径 | 方法 | 描述 | 权限要求 | 请求体/参数 | 响应 |
|
|
|
|
|
+|----------|------|------|----------|-------------|------|
|
|
|
|
|
+| `/messages/send` | POST | 医生发送消息 | 医生 | SendMessageRequest (receiverIds, content, etc.) | R<String> (消息ID) |
|
|
|
|
|
+| `/messages/system-daily-send` | POST | 系统发送日常消息 | 管理员 | SystemDailySendRequest (receiverIds, title, content) | R<String> (消息ID) |
|
|
|
|
|
+| `/messages/system-anomaly-send` | POST | 系统发送异常通知 | 系统/管理员 | SystemAnomalySendRequest (patientId, anomalyData) | R<String> (消息ID) |
|
|
|
|
|
+| `/messages/list` | GET | 获取消息列表(分页) | 患者/家属 | 查询参数: page, size, status | R<Page<MessageVO>> |
|
|
|
|
|
+| `/messages/{id}` | GET | 获取消息详情 | 患者/家属 | 路径参数: id | R<MessageVO> |
|
|
|
|
|
+| `/messages/{id}/read` | PUT | 标记消息已读 | 患者/家属 | 路径参数: id | R<Void> |
|
|
|
|
|
+| `/messages/{id}` | DELETE | 删除消息 | 患者/家属 | 路径参数: id | R<Void> |
|
|
|
|
|
+| `/messages/unread-count` | GET | 获取未读消息数量 | 患者/家属 | 无 | R<Integer> |
|
|
|
|
|
+
|
|
|
|
|
+## API设计示例
|
|
|
|
|
+
|
|
|
|
|
+### 医生发送消息接口
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+@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格式)
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 获取消息列表接口
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+@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
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 配置示例(application.yml)
|
|
|
|
|
+
|
|
|
|
|
+```yaml
|
|
|
|
|
+# 无特定配置,复用现有推送和数据库配置
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 数据库字段设计与兼容性
|
|
|
|
|
+
|
|
|
|
|
+### 消息表 (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, 消息类型
|
|
|
|
|
+- 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。
|
|
|
|
|
+
|
|
|
|
|
+## 数据传输对象设计
|
|
|
|
|
+
|
|
|
|
|
+### 请求VO
|
|
|
|
|
+
|
|
|
|
|
+#### SendMessageRequest(医生发送消息请求)
|
|
|
|
|
+```java
|
|
|
|
|
+@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")
|
|
|
|
|
+ @NotBlank(message = "类型不能为空")
|
|
|
|
|
+ private String type;
|
|
|
|
|
+
|
|
|
|
|
+ @Schema(description = "是否通知家属")
|
|
|
|
|
+ private Boolean notifyFamily = false;
|
|
|
|
|
+
|
|
|
|
|
+ @Schema(description = "是否弹窗")
|
|
|
|
|
+ private Boolean notifyPopup;
|
|
|
|
|
+
|
|
|
|
|
+ @Schema(description = "是否订阅推送")
|
|
|
|
|
+ private Boolean notifySubscribe;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### SystemDailySendRequest(系统发送日常消息请求)
|
|
|
|
|
+```java
|
|
|
|
|
+@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")
|
|
|
|
|
+ @NotBlank(message = "类型不能为空")
|
|
|
|
|
+ private String type = "SYSTEM_DAILY";
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### SystemAnomalySendRequest(系统发送异常通知请求)
|
|
|
|
|
+```java
|
|
|
|
|
+@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")
|
|
|
|
|
+ @NotBlank(message = "类型不能为空")
|
|
|
|
|
+ private String type = "SYSTEM_ANOMALY";
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 响应VO
|
|
|
|
|
+
|
|
|
|
|
+#### MessageVO(消息信息响应)
|
|
|
|
|
+```java
|
|
|
|
|
+@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 String 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填充。
|
|
|
|
|
+
|
|
|
|
|
+### 联动查询设计
|
|
|
|
|
+消息模块涉及跨表查询用户信息,为填充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。
|
|
|
|
|
+
|
|
|
|
|
+## 迁移/变更管理
|
|
|
|
|
+- 先实现后端接口,测试通过后前端接入。
|