# 文件存储服务(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** — 当前会话连续执行,编译错误时停下确认