From a10b712919626a07055d7a78885c3a1abde5ee50 Mon Sep 17 00:00:00 2001 From: pigeon Date: Sun, 7 Jun 2026 21:31:59 +0800 Subject: [PATCH] =?UTF-8?q?docs(plan):=20=E6=96=87=E4=BB=B6=E5=AD=98?= =?UTF-8?q?=E5=82=A8=E6=9C=8D=E5=8A=A1=EF=BC=88rui-service-storage?= =?UTF-8?q?=EF=BC=89=E5=AE=9E=E6=96=BD=E8=AE=A1=E5=88=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../2026-06-07-file-storage-service-plan.md | 406 ++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 superpowers/plans/2026-06-07-file-storage-service-plan.md diff --git a/superpowers/plans/2026-06-07-file-storage-service-plan.md b/superpowers/plans/2026-06-07-file-storage-service-plan.md new file mode 100644 index 0000000..265a7d4 --- /dev/null +++ b/superpowers/plans/2026-06-07-file-storage-service-plan.md @@ -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`。订阅方(如 `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` 已就绪 +- 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 | `` 加 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`(`` 加 `rui-service-storage`) + +- [ ] **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=9400,servlet.multipart 兜底) + +- [ ] **Step 3.4:** `rui-service/pom.xml` 的 `` 末尾加 `rui-service-storage` + +- [ ] **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 bizTypes`、`Aliyun aliyun`、`Tencent tencent`、`Local local` + - 嵌套类 `BizTypeConfig { DataSize maxSize; List 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`(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`) +- Create: `ISysFileService.java` (extends `IService`) +- Create: `SysFileServiceImpl.java` (extends `ServiceImpl`) + +- [ ] **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` 加 ` rui-service-storage ` + +- [ ] **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 返回 | Task 11 | ☐ | +| MQ pub/sub 事件推送 | Task 12 | ☐ | +| 集成聚合启动器 | Task 13 | ☐ | +| 配置分层 (Nacos 规则) | Task 14 | ☐ | +| 最终编译通过 | Task 15 | ☐ | +| Gitea #4 关闭 | Task 16 | ☐ | + +### 验收点 +- [ ] 上传 .pem 文件返回标准 Result,data.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** — 当前会话连续执行,编译错误时停下确认