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

18 KiB
Raw Blame History

文件存储服务(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)实现 MqConsumertype 字段过滤处理。

Tech Stack: Java 21, Spring Boot 4.x, MyBatis Plus, Fastjson2, Spring Security OAuth2, Spring Data Redis (Redisson), 阿里云 OSS SDK, 腾讯 COS SDK

前置依赖:


文件映射

新增

路径 操作 说明
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

    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 枚举 改为 final class 工具类(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.xmlparent 指向 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

    @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.ymlport=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 activeDataSize defaultMaxSize(或 long)、Map<String, BizTypeConfig> bizTypesAliyun aliyunTencent tencentLocal 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)
    • uploadossClient.putObject(bucket, storageKey, inputStream)
    • deleteossClient.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)) )
    • uploadcosClient.putObject(bucket, storageKey, inputStream)
    • deletecosClient.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
    • deleteFiles.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
      • 算 sha256DigestUtils.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.ymlrui.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.ymlrui-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 文件返回标准 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 — 当前会话连续执行,编译错误时停下确认