|
@@ -0,0 +1,238 @@
|
|
|
|
|
+# 资讯管理功能设计文档
|
|
|
|
|
+
|
|
|
|
|
+> 本文档遵循《03-API设计规范》《06-日志和错误处理规范》《07-数据库规范》《08-安全规范》《Swagger泛型返回类型字段信息不显示问题解决方案》《前端ID精度丢失问题解决方案》等项目规范。
|
|
|
|
|
+
|
|
|
|
|
+## 概述
|
|
|
|
|
+为提升用户健康知识水平,系统需支持医生发布和管理资讯,患者和家属浏览资讯。资讯采用Markdown格式,支持图片展示,图片存储于本地磁盘,路径可配置,支持文件大小和类型限制,仅依赖MySQL+Spring Boot+本地存储。
|
|
|
|
|
+
|
|
|
|
|
+## 背景和目标
|
|
|
|
|
+- 背景:当前APP缺乏健康资讯功能,用户无法获取权威的慢病相关知识。
|
|
|
|
|
+- 目标:实现资讯的发布、管理和浏览功能,支持Markdown编辑、图片上传,接口风格与现有API统一。
|
|
|
|
|
+
|
|
|
|
|
+## 需求与约束
|
|
|
|
|
+1. 认证:接口需走Token认证(AuthInterceptor),依赖`currentUserId`,未认证返回401。
|
|
|
|
|
+2. 存储:资讯内容存储于MySQL,图片文件存储于本地磁盘,根路径通过配置文件指定。
|
|
|
|
|
+3. 文件限制:资讯图片仅允许图片类型(jpg/png/jpeg/webp),最大文件大小(如5MB)可配置,超限或非法类型返回400。
|
|
|
|
|
+4. 数据库:新增资讯相关表,存储标题、内容、概要、医生ID等。
|
|
|
|
|
+5. 访问控制:医生可发布和管理资讯,患者和家属可浏览。
|
|
|
|
|
+6. API兼容:接口风格与现有API一致,所有响应均为`R<T>`格式,状态码、错误码、消息体与规范一致。
|
|
|
|
|
+7. 日志审计:所有操作需用SLF4J记录info级日志,异常需error级,内容含userId、操作等。
|
|
|
|
|
+8. Swagger文档:所有接口需用`@ApiResponse`+`@Content`+`@Schema`标注,确保泛型返回类型文档化。
|
|
|
|
|
+9. ID精度:所有响应中的ID字段在VO层转为String,避免前端精度丢失。
|
|
|
|
|
+
|
|
|
|
|
+## 设计要点(高层)
|
|
|
|
|
+1. Controller层新增接口:
|
|
|
|
|
+ - 创建资讯:`POST /news`,参数包含标题、内容、概要等。
|
|
|
|
|
+ - 更新资讯:`PUT /news/{id}`。
|
|
|
|
|
+ - 删除资讯:`DELETE /news/{id}`。
|
|
|
|
|
+ - 获取资讯列表:`GET /news/list`,支持分页、搜索。
|
|
|
|
|
+ - 获取资讯详情:`GET /news/{id}`。
|
|
|
|
|
+ - 上传资讯图片:`POST /news/upload-image`,返回图片相对路径。
|
|
|
|
|
+2. Service层实现资讯CRUD、图片上传、Markdown解析、数据库操作、异常处理、日志记录等逻辑。新建NewsImageService(类似UserAvatarService)处理图片上传,使用NewsImageProperties配置。资讯查询时需联动UserInfoMapper查询医生用户信息,填充authorName(昵称)。
|
|
|
|
|
+3. 图片存储参考头像实现,使用 `NewsImageProperties` 配置,路径为 `news-images/{医生ID}/{timestamp}.{ext}`。
|
|
|
|
|
+
|
|
|
|
|
+## 接口清单
|
|
|
|
|
+
|
|
|
|
|
+| 接口路径 | 方法 | 描述 | 权限要求 | 请求体/参数 | 响应 |
|
|
|
|
|
+|----------|------|------|----------|-------------|------|
|
|
|
|
|
+| `/news` | POST | 创建资讯 | 医生 | CreateNewsRequest (title, content, summary) | R<String> (资讯ID) |
|
|
|
|
|
+| `/news/{id}` | PUT | 更新资讯 | 医生 | UpdateNewsRequest (title, content, summary) | R<String> (资讯ID) |
|
|
|
|
|
+| `/news/{id}` | DELETE | 删除资讯 | 医生 | 路径参数: id | R<Void> |
|
|
|
|
|
+| `/news/{id}` | GET | 获取资讯详情 | 患者/家属 | 路径参数: id | R<NewsVO> |
|
|
|
|
|
+| `/news/list` | GET | 获取资讯列表(分页) | 患者/家属 | 查询参数: page, size | R<Page<NewsVO>> |
|
|
|
|
|
+| `/news/upload-image` | POST | 上传资讯图片 | 医生 | MultipartFile (file) | R<String> (相对路径) |
|
|
|
|
|
+
|
|
|
|
|
+## 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("/news")
|
|
|
|
|
+public R<String> createNews(@RequestBody CreateNewsRequest request) {
|
|
|
|
|
+ // 校验参数、保存资讯、返回ID(String格式)
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 上传资讯图片接口
|
|
|
|
|
+
|
|
|
|
|
+```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)))
|
|
|
|
|
+})
|
|
|
|
|
+@PostMapping(value = "/news/upload-image", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
|
|
|
|
+public R<String> uploadImage(@RequestPart("file") MultipartFile file) {
|
|
|
|
|
+ // 校验文件、保存图片、返回相对路径
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 获取资讯列表接口
|
|
|
|
|
+
|
|
|
|
|
+```java
|
|
|
|
|
+@Operation(summary = "获取资讯列表", description = "分页获取资讯列表")
|
|
|
|
|
+@ApiResponses(value = {
|
|
|
|
|
+ @ApiResponse(responseCode = "200", description = "获取成功",
|
|
|
|
|
+ content = @Content(mediaType = "application/json",
|
|
|
|
|
+ schema = @Schema(implementation = R.class)))
|
|
|
|
|
+})
|
|
|
|
|
+@GetMapping("/news/list")
|
|
|
|
|
+public R<Page<NewsVO>> getNewsList(@RequestParam(defaultValue = "1") int page,
|
|
|
|
|
+ @RequestParam(defaultValue = "10") int size) {
|
|
|
|
|
+ // 返回分页列表,ID转为String
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 配置示例(application.yml)
|
|
|
|
|
+
|
|
|
|
|
+```yaml
|
|
|
|
|
+news-image:
|
|
|
|
|
+ root-path: D:/news-images/
|
|
|
|
|
+ max-size: 5MB
|
|
|
|
|
+ allowed-types: jpg,png,jpeg,webp
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 数据库字段设计与兼容性
|
|
|
|
|
+
|
|
|
|
|
+### 资讯表 (t_news_info)
|
|
|
|
|
+- id: BIGINT PRIMARY KEY AUTO_INCREMENT(雪花算法生成,响应时转为String)
|
|
|
|
|
+- title: VARCHAR(30) NOT NULL, 资讯标题
|
|
|
|
|
+- content: TEXT NOT NULL, Markdown内容
|
|
|
|
|
+- summary: VARCHAR(50), 资讯概要
|
|
|
|
|
+- author_id: BIGINT NOT NULL, 医生ID
|
|
|
|
|
+- publish_time: DATETIME DEFAULT CURRENT_TIMESTAMP, 发布时间
|
|
|
|
|
+- 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
|
|
|
|
|
+
|
|
|
|
|
+#### CreateNewsRequest(创建资讯请求)
|
|
|
|
|
+```java
|
|
|
|
|
+@Schema(description = "创建资讯请求")
|
|
|
|
|
+public class CreateNewsRequest {
|
|
|
|
|
+ @Schema(description = "资讯标题", required = true, maxLength = 30)
|
|
|
|
|
+ @NotBlank(message = "标题不能为空")
|
|
|
|
|
+ @Size(max = 30, message = "标题长度不能超过30字符")
|
|
|
|
|
+ private String title;
|
|
|
|
|
+
|
|
|
|
|
+ @Schema(description = "资讯内容(Markdown格式)", required = true)
|
|
|
|
|
+ @NotBlank(message = "内容不能为空")
|
|
|
|
|
+ private String content;
|
|
|
|
|
+
|
|
|
|
|
+ @Schema(description = "资讯概要", maxLength = 50)
|
|
|
|
|
+ @Size(max = 50, message = "概要长度不能超过50字符")
|
|
|
|
|
+ private String summary;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+#### UpdateNewsRequest(更新资讯请求)
|
|
|
|
|
+```java
|
|
|
|
|
+@Schema(description = "更新资讯请求")
|
|
|
|
|
+public class UpdateNewsRequest {
|
|
|
|
|
+ @Schema(description = "资讯标题", required = true, maxLength = 30)
|
|
|
|
|
+ @NotBlank(message = "标题不能为空")
|
|
|
|
|
+ @Size(max = 30, message = "标题长度不能超过30字符")
|
|
|
|
|
+ private String title;
|
|
|
|
|
+
|
|
|
|
|
+ @Schema(description = "资讯内容(Markdown格式)", required = true)
|
|
|
|
|
+ @NotBlank(message = "内容不能为空")
|
|
|
|
|
+ private String content;
|
|
|
|
|
+
|
|
|
|
|
+ @Schema(description = "资讯概要", maxLength = 50)
|
|
|
|
|
+ @Size(max = 50, message = "概要长度不能超过50字符")
|
|
|
|
|
+ private String summary;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 响应VO
|
|
|
|
|
+
|
|
|
|
|
+#### NewsVO(资讯信息响应)
|
|
|
|
|
+```java
|
|
|
|
|
+@Schema(description = "资讯信息")
|
|
|
|
|
+public class NewsVO {
|
|
|
|
|
+ @Schema(description = "资讯ID", example = "1234567890123456789")
|
|
|
|
|
+ private String id;
|
|
|
|
|
+
|
|
|
|
|
+ @Schema(description = "资讯标题")
|
|
|
|
|
+ private String title;
|
|
|
|
|
+
|
|
|
|
|
+ @Schema(description = "资讯概要")
|
|
|
|
|
+ private String summary;
|
|
|
|
|
+
|
|
|
|
|
+ @Schema(description = "资讯内容(Markdown格式)")
|
|
|
|
|
+ private String content;
|
|
|
|
|
+
|
|
|
|
|
+ @Schema(description = "作者ID", example = "1234567890123456789")
|
|
|
|
|
+ private String authorId;
|
|
|
|
|
+
|
|
|
|
|
+ @Schema(description = "作者姓名(从UserInfo.nickname获取)")
|
|
|
|
|
+ private String authorName;
|
|
|
|
|
+
|
|
|
|
|
+ @Schema(description = "发布时间", example = "2023-12-01 10:00:00")
|
|
|
|
|
+ @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
|
|
|
|
+ private LocalDateTime publishTime;
|
|
|
|
|
+
|
|
|
|
|
+ @Schema(description = "封面图片URL(从内容中提取的第一张图片)")
|
|
|
|
|
+ private String coverImage;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+注:所有ID字段在VO中定义为String类型,避免前端精度丢失。时间字段使用@JsonFormat格式化。authorName通过注入UserInfoMapper查询UserInfo.nickname填充。
|
|
|
|
|
+
|
|
|
|
|
+### 联动查询设计
|
|
|
|
|
+资讯模块涉及跨表查询用户信息,为填充NewsVO中的authorName字段:
|
|
|
|
|
+- **注入依赖**:NewsServiceImpl注入UserInfoMapper。
|
|
|
|
|
+- **查询逻辑**:查询资讯列表或详情时,根据news.author_id调用userInfoMapper.selectById(authorId),获取UserInfo对象。
|
|
|
|
|
+- **字段映射**:将userInfo.getNickname()赋值给newsVO.setAuthorName()。
|
|
|
|
|
+- **异常处理**:若用户信息不存在,authorName设为空字符串或默认值,避免查询失败。
|
|
|
|
|
+- **性能优化**:对于列表查询,可批量查询用户信息,或使用缓存减少数据库访问。
|
|
|
|
|
+
|
|
|
|
|
+## 权限与安全
|
|
|
|
|
+- 创建/更新/删除接口仅医生角色可操作。
|
|
|
|
|
+- 浏览接口公开,但需认证。
|
|
|
|
|
+- 图片上传防路径穿越,文件名唯一。
|
|
|
|
|
+
|
|
|
|
|
+## 日志与审计
|
|
|
|
|
+- 操作日志:`[NewsOperation] userId={}, action={}, newsId={}`
|
|
|
|
|
+- 异常日志:`[NewsError] userId={}, error={}`
|
|
|
|
|
+- 使用SLF4J,日志级别INFO/ERROR。
|
|
|
|
|
+- ID在日志中以字符串记录。
|
|
|
|
|
+
|
|
|
|
|
+## 测试建议
|
|
|
|
|
+1. 单元测试:CRUD逻辑、图片上传校验。
|
|
|
|
|
+2. 集成测试:接口联调、Markdown解析。
|
|
|
|
|
+3. 性能测试:列表分页、图片访问。
|
|
|
|
|
+
|
|
|
|
|
+## 兼容性与前端要求
|
|
|
|
|
+- 前端需支持Markdown编辑器、图片上传。
|
|
|
|
|
+- 列表显示标题、封面、概要。
|
|
|
|
|
+- 详情页解析Markdown展示。
|
|
|
|
|
+- 响应中ID字段为String。
|
|
|
|
|
+
|
|
|
|
|
+## 迁移/变更管理
|
|
|
|
|
+- 先实现后端接口,测试通过后前端接入。
|
|
|
|
|
+- 图片存储路径可配置。
|