设计调整原因:上传服务是统一基础设施,强制枚举会要求「加新模块 = 改框架代码」。 改为 final class + normalize() 格式校验,业务模块自定 bizType 字符串即可。 - spec §4.2: 整个 FileBizType 章节重写(工具类 + 格式约束 + 设计意图) - spec flow 5/11: 校验从 values() 改为 normalize() - spec API 表 / 代码结构表 / 订阅方示例 同步更新 - plan Step 1.2/1.4 / 状态表 / 流程校验 / 发布器签名 同步更新
18 KiB
文件存储服务(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 (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 class,bizType 不维护中央清单) -
Reference style:
CacheConstants.java(沿用 private ctor + Javadoc 写明写入方/使用方) -
Step 1.1: 创建
MqTopicConstantspublic 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:
创建改为 final class 工具类(FileBizType枚举normalize()/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-webcom.aliyun.oss:aliyun-sdk-oss(版本从rui-dependenciesBOM 取)com.qcloud:cos_api(版本从 BOM 取)spring-cloud-starter-alibaba-nacos-discovery/nacos-configspring-boot-starter-actuatorlombok/fastjson2
-
Step 3.2: 创建
StorageApplication.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的<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接口: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.createDirectoriesupload写文件到basePath + storageKey,返回url = urlPrefix + storageKeydelete调Files.deleteIfExiststype()返回"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(extendsBaseEntity) -
Create:
SysFileMapper.java(extendsBaseMapper<SysFile>) -
Create:
ISysFileService.java(extendsIService<SysFile>) -
Create:
SysFileServiceImpl.java(extendsServiceImpl<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:
UploadEventPayloadPOJO 字段与设计文档 §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.yamlNacos 配置 -
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
执行选项:
- Subagent-Driven(推荐) — 每个任务分派独立子代理
- Inline Execution — 当前会话连续执行,编译错误时停下确认