docs(plan): 文件存储服务(rui-service-storage)实施计划

- 16 个可独立 commit 的子任务
- Task 1: 常量 + 枚举
- Task 2: sys_file DDL
- Task 3-9: 模块骨架 + Strategy 模式
- Task 10-12: 实体/Service/Controller/Event
- Task 13-14: 集成启动器 + 公共配置
- Task 15-16: 编译验证 + Gitea 关闭

关联 Gitea #4
This commit is contained in:
2026-06-07 21:31:59 +08:00
parent 66f0712486
commit a10b712919
@@ -0,0 +1,406 @@
# 文件存储服务(rui-service-storage)实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers-subagent-driven-development (recommended) or superpowers-executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 落地 `rui-service-storage` 独立微服务,提供统一上传接口(阿里云 OSS / 腾讯云 COS / 本地),并通过 Redis pub/sub 广播 `ON_UPLOAD` / `ON_FILE_DELETED` 事件。
**Architecture:** Strategy 模式 + 事件驱动。`POST /storage/upload` → 鉴权 → 校验 → Strategy 上传 → 落 `sys_file` → 推 `ON_UPLOAD` 事件 → 返回 `Result<T>`。订阅方(如 `rui-service-system`)实现 `MqConsumer``type` 字段过滤处理。
**Tech Stack:** Java 21, Spring Boot 4.x, MyBatis Plus, Fastjson2, Spring Security OAuth2, Spring Data Redis (Redisson), 阿里云 OSS SDK, 腾讯 COS SDK
**前置依赖:**
- 设计文档 [docs/superpowers/specs/2026-06-07-file-storage-service-design.md](docs/superpowers/specs/2026-06-07-file-storage-service-design.md) (commit 66f0712)
- 主仓指针 commit c467eaf
- `rui-common-mq-redis` 已就绪,`@MqTopic` 注解可用
- `rui-common-web/.../annotation/AutoPermission` 已就绪
- `rui-common-core/.../result/Result<T>` 已就绪
- Gitea #4 实施中
---
## 文件映射
### 新增
| 路径 | 操作 | 说明 |
|------|------|------|
| `rui-common/rui-common-core/.../constants/MqTopicConstants.java` | Create | MQ topic 常量 |
| `rui-common/rui-common-core/.../enums/FileBizType.java` | Create | 文件业务类型枚举 |
| `rui-service/rui-service-storage/pom.xml` | Create | 新模块 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/StorageApplication.java` | Create | 启动类 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/controller/SysFileController.java` | Create | 上传/查询/删除 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/IFileStorage.java` | Create | Strategy 接口 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/impl/AliyunOssFileStorage.java` | Create | 阿里云 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/impl/TencentCosFileStorage.java` | Create | 腾讯 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/impl/LocalFileStorage.java` | Create | 本地 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/impl/FileStorageRouter.java` | Create | 选实现 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/event/UploadEventPublisher.java` | Create | ON_UPLOAD 推送 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/event/FileDeletedEventPublisher.java` | Create | ON_FILE_DELETED 推送 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/properties/FileProperties.java` | Create | @ConfigurationProperties |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/entity/SysFile.java` | Create | 实体 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/mapper/SysFileMapper.java` | Create | Mapper |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/ISysFileService.java` | Create | Service 接口 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/impl/SysFileServiceImpl.java` | Create | Service 实现 |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/dto/SysFileUploadVO.java` | Create | 上传返回 VO |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/dto/SysFileQueryVO.java` | Create | 查询返回 VO |
| `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/dto/UploadEventPayload.java` | Create | 事件 payload POJO |
| `rui-service/rui-service-storage/src/main/resources/application.yml` | Create | port=9400 |
| `sql/init-database.sql` | Modify | 新增 sys_file DDL |
### 修改
| 路径 | 操作 | 说明 |
|------|------|------|
| `rui-service/pom.xml` | Modify | `<modules>` 加 storage |
| `rui-service/rui-service-starter/pom.xml` | Modify | 加 storage 依赖 |
| `rui-service/rui-service-starter/.../StarterApplication.java` | Modify | ComponentScan + storage |
| `rui-service/rui-service-starter/src/main/resources/application.yml` | Modify | rui.modules.available 加 storage 入口 |
| `docs/backend/config-templates/application-template.yml` | Modify | rui.file.* 公共配置示例 |
| `Gitea #4` | Reply + Close | 实施完成通知 |
---
## 任务列表
### Task 1: 公共常量与枚举(rui-common-core
**Files:**
- Create: `rui-common/rui-common-core/src/main/java/com/rui/common/core/constants/MqTopicConstants.java`
- Create: `rui-common/rui-common-core/src/main/java/com/rui/common/core/enums/FileBizType.java`
- Reference style: `CacheConstants.java`(沿用 private ctor + Javadoc 写明写入方/使用方)
- [ ] **Step 1.1:** 创建 `MqTopicConstants`
```java
public final class MqTopicConstants {
public static final String ON_UPLOAD = "ON_UPLOAD";
public static final String ON_FILE_DELETED = "ON_FILE_DELETED";
private MqTopicConstants() {}
}
```
- [ ] **Step 1.2:** 创建 `FileBizType` 枚举(4 个值 + `uploadType()` / `deletedType()` 方法)
- [ ] **Step 1.3:** 编译 `mvn -pl rui-common/rui-common-core compile` 通过
- [ ] **Step 1.4:** 提交 `feat(core): 新增 MqTopicConstants 和 FileBizType`
---
### Task 2: 数据库 DDL
**Files:**
- Modify: `sql/init-database.sql` (新增 sys_file 表 DDL)
- [ ] **Step 2.1:** 在 `sql/init-database.sql` 末尾追加 `sys_file` 表 DDL(参见设计文档 §4.1)
- [ ] **Step 2.2:** 提交 `feat(db): 新增 sys_file 表`
---
### Task 3: rui-service-storage 模块骨架
**Files:**
- Create: `rui-service/rui-service-storage/pom.xml`
- Create: `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/StorageApplication.java`
- Create: `rui-service/rui-service-storage/src/main/resources/application.yml`
- Modify: `rui-service/pom.xml``<modules>` 加 `<module>rui-service-storage</module>`
- [ ] **Step 3.1:** 创建 `pom.xml`parent 指向 `rui-service`,依赖:
- `rui-common-web` / `rui-common-mybatis` / `rui-common-redis` / `rui-common-mq` / `rui-common-mq-redis` / `rui-common-security` / `rui-common-oauth2`(可选)
- `spring-boot-starter-web`
- `com.aliyun.oss:aliyun-sdk-oss`(版本从 `rui-dependencies` BOM 取)
- `com.qcloud:cos_api`(版本从 BOM 取)
- `spring-cloud-starter-alibaba-nacos-discovery` / `nacos-config`
- `spring-boot-starter-actuator`
- `lombok` / `fastjson2`
- [ ] **Step 3.2:** 创建 `StorageApplication.java`
```java
@SpringBootApplication
@EnableDiscoveryClient
@EnableResourceServer
@ComponentScan(basePackages = {"com.rui.service.storage"})
public class StorageApplication {
public static void main(String[] args) {
SpringApplication.run(StorageApplication.class, args);
}
}
```
- [ ] **Step 3.3:** 创建 `application.yml`port=9400servlet.multipart 兜底)
- [ ] **Step 3.4:** `rui-service/pom.xml` 的 `<modules>` 末尾加 `<module>rui-service-storage</module>`
- [ ] **Step 3.5:** 编译 `mvn -pl rui-service/rui-service-storage -am compile` 通过
- [ ] **Step 3.6:** 提交 `feat(storage): 新建 rui-service-storage 模块骨架`
---
### Task 4: 配置类 FileProperties
**Files:**
- Create: `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/properties/FileProperties.java`
- [ ] **Step 4.1:** 创建 `FileProperties`
- `@ConfigurationProperties(prefix = "rui.file")` + `@Data` + `@Component`(或 `@EnableConfigurationProperties`
- 字段:`String active`、`DataSize defaultMaxSize`(或 long)、`Map<String, BizTypeConfig> bizTypes`、`Aliyun aliyun`、`Tencent tencent`、`Local local`
- 嵌套类 `BizTypeConfig { DataSize maxSize; List<String> allowedExtensions; }`
- 嵌套类 `Aliyun { boolean enabled; String endpoint, accessKey, secretKey, bucket, urlPrefix, basePath; }`
- 嵌套类 `Tencent { boolean enabled; String secretId, secretKey, region, bucket, urlPrefix, basePath; }`
- 嵌套类 `Local { String basePath, urlPrefix; }`
- [ ] **Step 4.2:** 编译通过
- [ ] **Step 4.3:** 与本任务其他提交合并到 Task 3 的 commit(避免空 commit
---
### Task 5: FileStorage Strategy 接口
**Files:**
- Create: `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/IFileStorage.java`
- Create: `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/dto/FileStorageResult.java`
- [ ] **Step 5.1:** 创建 `IFileStorage` 接口:
```java
public interface IFileStorage {
String type(); // "ALIYUN" / "TENCENT" / "LOCAL"
boolean enabled(FileProperties props);
FileStorageResult upload(MultipartFile file, String storageKey, FileProperties props) throws IOException;
void delete(String storageKey, FileProperties props);
}
```
- [ ] **Step 5.2:** 创建 `FileStorageResult { String url; String storageKey; }`
---
### Task 6: AliyunOssFileStorage 实现
**Files:**
- Create: `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/impl/AliyunOssFileStorage.java`
- [ ] **Step 6.1:** 实现:
- `@Component("ALIYUN")` + `@ConditionalOnProperty(prefix = "rui.file.aliyun", name = "enabled", havingValue = "true")`(用 `@ConditionalOnBean` 触发;或简单写死 bean,启用由 `enabled` 控制)
- 用 `OSSClientBuilder().build(endpoint, ak, sk)`
- `upload` 调 `ossClient.putObject(bucket, storageKey, inputStream)`
- `delete` 调 `ossClient.deleteObject(bucket, storageKey)`
- 构造 `url` = `urlPrefix + "/" + storageKey`
- `enabled` 返回 `aliyun.enabled`
- [ ] **Step 6.2:** 编译通过
---
### Task 7: TencentCosFileStorage 实现
**Files:**
- Create: `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/impl/TencentCosFileStorage.java`
- [ ] **Step 7.1:** 实现:
- `@Component("TENCENT")` + 同条件注解
- 用 `COSClient( new BasicCOSCredentials(sid, sk), new ClientConfig(new Region(region)) )`
- `upload` 调 `cosClient.putObject(bucket, storageKey, inputStream)`
- `delete` 调 `cosClient.deleteObject(bucket, storageKey)`
- 构造 `url` = `urlPrefix + "/" + storageKey`
- [ ] **Step 7.2:** 编译通过
---
### Task 8: LocalFileStorage 实现
**Files:**
- Create: `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/impl/LocalFileStorage.java`
- [ ] **Step 8.1:** 实现:
- `@Component("LOCAL")` (默认总是启用)
- `basePath` 启动时 `Files.createDirectories`
- `upload` 写文件到 `basePath + storageKey`,返回 `url = urlPrefix + storageKey`
- `delete` 调 `Files.deleteIfExists`
- `type()` 返回 `"LOCAL"`
- [ ] **Step 8.2:** 编译通过
---
### Task 9: FileStorageRouter
**Files:**
- Create: `rui-service/rui-service-storage/src/main/java/com/rui/service/storage/service/impl/FileStorageRouter.java`
- [ ] **Step 9.1:** 实现:
- `@Component`
- 构造注入 `Map<String, IFileStorage>`Spring 按 bean name 注入所有实现)
- `route(String storageHint)` 方法:先按 `storageHint` 找;找不到走 `props.getActive()`;都没有用 `LOCAL`
- 失败抛 `BizException("STORAGE_NOT_AVAILABLE")`
- [ ] **Step 9.2:** Task 5-9 一起提交 `feat(storage): FileStorage Strategy 模式 + 三家实现`
---
### Task 10: SysFile 实体/Mapper/Service
**Files:**
- Create: `SysFile.java` (extends `BaseEntity`)
- Create: `SysFileMapper.java` (extends `BaseMapper<SysFile>`)
- Create: `ISysFileService.java` (extends `IService<SysFile>`)
- Create: `SysFileServiceImpl.java` (extends `ServiceImpl<SysFileMapper, SysFile>`)
- [ ] **Step 10.1:** `SysFile` 字段:`id, name, originalName, url, storageType, bizType, bizId, size, contentType, sha256, uploaderId`,其他由 `BaseEntity` 提供
- [ ] **Step 10.2:** Service 增加 `appendBizId(Long fileId, String bizId)` 方便订阅方回填
- [ ] **Step 10.3:** 编译通过
- [ ] **Step 10.4:** 提交 `feat(storage): sys_file 实体/Mapper/Service`
---
### Task 11: 上传/查询/删除 Controller
**Files:**
- Create: `SysFileUploadVO.java`
- Create: `SysFileQueryVO.java`
- Create: `SysFileController.java`
- [ ] **Step 11.1:** `SysFileUploadVO` 字段:`id, name, originalName, url, size, contentType, storageType, bizType`
- [ ] **Step 11.2:** `SysFileQueryVO` 字段:`id, name, originalName, url, size, bizType, createdAt`
- [ ] **Step 11.3:** `SysFileController`
- 类级 `@AutoPermission("sys:file:upload")`
- `@PostMapping("/upload")` → `upload(file, bizType, storage?)`
- 校验 `bizType` ∈ `FileBizType.values()`
- 加载 `FileProperties.bizTypes[bizType]`,校验大小/扩展名
- 调 `FileStorageRouter` 选实现 → `upload`
- 算 sha256`DigestUtils.sha256Hex`
- 落 `sys_file`
- 调 `UploadEventPublisher.publish(...)`
- 返回 `Result.ok(vo)`
- `@GetMapping("/file/{id}")` → `@AutoPermission("sys:file:query")` 查询单条
- `@GetMapping("/file/page")` → 分页查询
- `@DeleteMapping("/file/{id}")` → `@AutoPermission("sys:file:delete")` 删除
- 使用 `BaseController<...>` 或独立 `@RestController`(推荐独立,路径 `/storage`
- [ ] **Step 11.4:** 编译通过
- [ ] **Step 11.5:** 提交 `feat(storage): 文件上传/查询/删除接口`
---
### Task 12: Event Publishers
**Files:**
- Create: `UploadEventPayload.java`
- Create: `UploadEventPublisher.java`
- Create: `FileDeletedEventPublisher.java`
- [ ] **Step 12.1:** `UploadEventPayload` POJO 字段与设计文档 §8.2 一致,标注 `@JSONField` 序列化
- [ ] **Step 12.2:** `UploadEventPublisher`
- `@Component @RequiredArgsConstructor`
- 注入 `MqClient`
- `publish(FileBizType bizType, SysFile entity, Long uploaderId, Long tenantId, JSONObject extra)`
- 内部 `mqClient.publish(MqProvider.REDIS, MqTopicConstants.ON_UPLOAD, payload)`
- 失败只 `log.error`,不抛(避免上传回滚)
- [ ] **Step 12.3:** `FileDeletedEventPublisher` 同上结构,topic 用 `ON_FILE_DELETED`
- [ ] **Step 12.4:** 编译通过
- [ ] **Step 12.5:** 提交 `feat(storage): ON_UPLOAD/ON_FILE_DELETED 事件推送`
---
### Task 13: 集成到 rui-service-starter
**Files:**
- Modify: `rui-service/rui-service-starter/pom.xml`
- Modify: `rui-service/rui-service-starter/.../StarterApplication.java`
- Modify: `rui-service/rui-service-starter/src/main/resources/application.yml`
- [ ] **Step 13.1:** `pom.xml` 加 `<dependency> rui-service-storage </dependency>`
- [ ] **Step 13.2:** `StarterApplication` 的 `@ComponentScan` 加 `"com.rui.service.storage"`
- [ ] **Step 13.3:** `application.yml` 的 `rui.modules.available` 数组加 `code: storage, name: 文件存储, icon: tabler:cloud-upload`
- [ ] **Step 13.4:** 编译 `mvn -pl rui-service/rui-service-starter -am compile` 通过
- [ ] **Step 13.5:** 提交 `feat(starter): 集成 rui-service-storage`
---
### Task 14: 公共配置示例
**Files:**
- Modify: `docs/backend/config-templates/application-template.yml` 或 `rui-common.yaml` Nacos 配置
- [ ] **Step 14.1:** 在公共 yaml 模板加 `rui.file` 配置(active / defaultMaxSize / bizTypes 字典)参照设计文档 §9.1
- [ ] **Step 14.2:** 提交 `docs(config): rui.file 公共配置示例`
---
### Task 15: 编译验证
- [ ] **Step 15.1:** `mvn clean compile -DskipTests` 全部模块通过
- [ ] **Step 15.2:** 若有编译错误,按模块修复
---
### Task 16: 推送 + 关闭 Gitea #4
- [ ] **Step 16.1:** 累计 commit 数 ≥10 时 `git push origin main`
- [ ] **Step 16.2:** 通过 `bin/gitea-helper.sh issue-comment --id 4 --body "..."` 回复实现说明
- [ ] **Step 16.3:** `bin/gitea-helper.sh issue-close --id 4` 关闭工单
---
## 实施计划检查清单
### 规范覆盖检查
| 规范要求 | 对应任务 | 状态 |
|---------|---------|------|
| 公共常量集中 (MqTopicConstants) | Task 1 | ☐ |
| 业务枚举集中 (FileBizType) | Task 1 | ☐ |
| 数据库继承 BaseEntity | Task 10 | ☐ |
| Strategy 模式可插拔 | Tasks 5-9 | ☐ |
| 内置 @AutoPermission 鉴权 | Task 11 | ☐ |
| 统一 Result<T> 返回 | Task 11 | ☐ |
| MQ pub/sub 事件推送 | Task 12 | ☐ |
| 集成聚合启动器 | Task 13 | ☐ |
| 配置分层 (Nacos 规则) | Task 14 | ☐ |
| 最终编译通过 | Task 15 | ☐ |
| Gitea #4 关闭 | Task 16 | ☐ |
### 验收点
- [ ] 上传 .pem 文件返回标准 Resultdata.url 可访问
- [ ] 超大文件/不允许扩展名/未知 bizType 均返回 400
- [ ] Redis 收到 ON_UPLOAD 消息
- [ ] 删除后 Redis 收到 ON_FILE_DELETED 消息
- [ ] 无 JWT 返回 401,无权限返回 403
- [ ] `rui-service-starter` 启动时 storage 子模块同时激活
- [ ] Gitea #4 已关闭
- [ ] 全部 commit 推送至 origin/main
### 无占位符检查
- [ ] 无 "TBD"、"TODO"、"implement later"
- [ ] 文件路径全部相对项目根目录
- [ ] 字段命名符合 MyBatis Plus 驼峰转下划线
- [ ] 每个步骤可独立 commit + 编译
---
**计划完成!**
保存路径:`docs/superpowers/plans/2026-06-07-file-storage-service-plan.md`
设计参考:`docs/superpowers/specs/2026-06-07-file-storage-service-design.md`
**执行选项:**
1. **Subagent-Driven(推荐)** — 每个任务分派独立子代理
2. **Inline Execution** — 当前会话连续执行,编译错误时停下确认