Files
rui-docs/superpowers/plans/2026-06-07-file-storage-service-plan.md
T
vifo 3aebe0b5a5 docs(spec/plan): FileBizType 改为工具类(非枚举),bizType 自由字符串
设计调整原因:上传服务是统一基础设施,强制枚举会要求「加新模块 = 改框架代码」。
改为 final class + normalize() 格式校验,业务模块自定 bizType 字符串即可。

- spec §4.2: 整个 FileBizType 章节重写(工具类 + 格式约束 + 设计意图)
- spec flow 5/11: 校验从 values() 改为 normalize()
- spec API 表 / 代码结构表 / 订阅方示例 同步更新
- plan Step 1.2/1.4 / 状态表 / 流程校验 / 发布器签名 同步更新
2026-06-07 22:44:12 +08:00

407 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 文件存储服务(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` | Modify | 工具类(非枚举),含 normalize / uploadType / deletedType |
| `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`
- Modify: `rui-common/rui-common-core/src/main/java/com/rui/common/core/enums/FileBizType.java`(已从 enum 改为 final classbizType 不维护中央清单)
- 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() {}
}
```
- [x] **Step 1.2:** ~~创建 `FileBizType` 枚举~~ 改为 final class 工具类(`normalize()` / `uploadType()` / `deletedType()`),**不维护业务类型清单**
- [ ] **Step 1.3:** 编译 `mvn -pl rui-common/rui-common-core compile` 通过
- [x] **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.normalize()`);不再校验「是否已注册」
- 加载 `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(String bizType, SysFile entity, String url, Long uploaderId, Long tenantId, JSONObject extra)` (bizType 是已规范化的字符串,type = FileBizType.uploadType(bizType)
- 内部 `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** — 当前会话连续执行,编译错误时停下确认