项目:X-Pan | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
本文是"照着做就能通过 CI、不破坏既有约定"的操作手册:六个高频扩展场景各给一条 SOP(步骤化操作流程),全部以当前源码为基准。开工前的公共前提只有两条:通读 DEV_GUIDELINES.md 对应端的红线(每条都对应一次真实事故);理解"模块化单体"的边界纪律(模块间只走接口、Controller 必须在 modules/<模块>/controller/ 子包——这是登录切面包路径切点的要求,见《01-系统架构》2.2)。提交前跑一遍自检清单(《09-测试与质量保障》第 5 节)。
以"给回收站加一个'一键清空'"接口为例(该能力当前不存在,RecycleController 只有列表/统计/还原/彻底删除四端点):
DELETE /recycle 已是批量删除,清空可设计为 POST /recycle/clear)。写清入参(无需入参则省)、响应 R<Void>、错误码语义(《03-接口设计》第 5 节的 code 表)。modules/recycle/po/,@Validated + JSR-303 注解;服务层入参用 context/ 下的 Context 类聚合(对照 RestorePO → RestoreContext 的既有链)。modules/recycle/service/IRecycleService.java,实现加在 service/impl/RecycleServiceImpl.java。业务校验(归属、状态)→ @Transactional(rollbackFor = Exception.class) → Mapper 操作。红线检查:批量 IN () 前判空(红线 11);状态机变更用条件 UPDATE(红线 5);删除类操作要触发级联与 FilePhysicalDeleteEvent(见《05-核心流程》流程五,漏一条就是数据泄漏)。modules/recycle/controller/RecycleController.java——不要新建包。只做四件事:解密 ID(IdUtil.decrypt)、组装 Context、调 Service、包 R 返回。鉴权自动生效(切面覆盖),无需任何注解;如需免登录才加 @LoginIgnore。若涉敏感操作(登录/注册/创建类),在 Service 里接 RedisRateLimiter.checkRateLimit(scope, ...) 并在 application.yaml pan.ratelimit.* 加阈值(对照 share-create 的接线,common/ratelimit/RateLimitProperties.java)。server/src/test/java/com/xiaoye/pan/server/modules/recycle/RecycleTest.java 补 @SpringBootTest + @Transactional 用例(成功 + 主要失败分支),沿用回滚范式;涉独立线程参考 uploadWithChunkTest 的事务可见性处理(《09-测试与质量保障》第 2 节)。x-pan-fronted/src/api/recycle/index.ts 加方法(走 utils/http.ts 实例,自动获得鉴权/续期/code=10 处理);移动端在 x-pan-mobile/lib/services/recycle_service.dart 加方法。若响应含新 VO 字段,VO 加到出参对象即被 IdEncryptSerializer 自动加密 ID,三端无需额外处理。反模式提醒:不要在 Controller 写业务、不要让 Controller 逃出 controller/ 子包(OfflineTaskController 事故)、不要直接操作别的模块的 Mapper(走它的 Service 接口)。
以"为 AI 文本提取结果增加一个缓存桶"为例:
server/src/main/resources/application.yaml(及打包模板 distribution/conf/application.yaml——两者需同步,见《14-配置与环境详解》第 2 节)的 pan.storage.minio.* 下加 text-bucket: x-pan-text。禁止在这里写密钥默认值(红线)。framework/storage-engine/storage-engine-minio/src/main/java/com/xiaoye/pan/storage/engine/minio/config/MinIOStorageEngineProperties.java 加字段(对照既有 fileBucket/chunkBucket/thumbnailBucket 三字段的 @ConfigurationProperties 绑定方式)。MinIOStorageEngine 启动即确保三桶存在(ensureBucket 逻辑),在同类位置补第四桶的初始化。StorageEngine 接口操作(模板方法 AbstractStorageEngine 的 check→doXxx 骨架)。若五操作(store/delete/storeChunk/mergeFile/realFile)语义不覆盖新用途,先扩接口与抽象模板,再落 MinIO 实现与本地实现——两个引擎实现都要落,否则 pan.storage.minio.enabled=false 的本地模式直接坏死。本地引擎的对应物是 ${xpan.root} 下的子目录(对照缩略图缓存 xpan.thumbnail.dir 的做法)。{identifier}/{chunkNumber} 的规则化命名);登记可清理性——若对象有生命周期,补一张记录表 + 一个 framework/schedule 的 ScheduleTask 实现(对照 CleanExpireChunkFileTask:批 500 滚动、先删对象再删记录、bucket 维度删对桶——红线 10 的教训),并在 common/schedule/launcher/ 注册。application-ci.yaml pan.storage.minio.enabled: false),新桶逻辑的集成测试只能覆盖本地引擎分支;MinIO 分支按《09-测试与质量保障》第 6 节的缺口建议补容器化测试或列入手动冒烟清单。以"新增'最近删除统计'页面 /purge-stats"为例:
src/api/ 下对应域的 index.ts 加调用(或新建域目录,对照 src/api/dedup/ 的最简结构)。请求走统一 axios 实例;blob 类响应自动被拦截器短路(红线 2,无需特殊处理)。src/router/index.ts 注册路由。主布局内页面挂到 layout/index.vue 的子路由(对照 /recycles);独立页(如全屏预览)挂顶层(对照 /preview/*)。需要登录就不要进守卫白名单;同时把导航项加进 AppNavbar 与 stores/navbar.ts。src/views/list-page/<name>/index.vue(列表类)或独立目录。UI 优先复用 src/components/base/ 的 26 个 Base 组件(BaseTable/BaseModal/BaseDrawer……),图标用 @lucide/vue;确认弹窗必须走 useToast 的假封装语义——const ok = await ElMessageBox.confirm(...); if (!ok) return(前端红线 1,禁止 try/catch 等 reject)。requestSeq 的方法(红线 4,防过期响应覆盖——对照 stores/file.ts)。var(--color-xxx) 完整包装;提交前跑 grep -rn "vardanger\|varsuccess\|varwarning" src | grep -v "var(" 应零结果(自检清单)。npx vue-tsc -p tsconfig.app.json --noEmit 零输出才能过 CI;重型第三方库(预览器类)加进 vite.config.ts manualChunks(对照 7 组 vendor 拆分,见《10-性能设计与优化》第 5 节)。以"新增'标签管理'页"为例:
lib/services/ 加 file_tag_service.dart(对照 favorite_service.dart 的最简结构),Dio 实例从 core/network/http_client.dart 取单例——鉴权/续期/code=10 广播自动生效,不要自建 Dio(红线 7:临时 Dio 必须显式超时,能不用就不用)。lib/providers/,StateNotifier + copyWith 不可变更新(红线 6,禁止原地改对象);列表加载用自增 seq 防竞态(对照 file_provider.dart)。lib/pages/ 下建页面文件,通过 Navigator.push(MaterialPageRoute(...)) 压栈进入(本项目 29 个页面全部如此,go_router 只管认证门卫与三个顶层路由——见《02-页面设计》2.1);传参用构造函数。ref/context 前查 mounted(红线 7);轮询必须有总次数/时长上限(对照 _pollExtract 的 60 次上限);后端 progress 是 0-100 整数别再 ×100(红线 4);排序参数必须经 provider 映射成 snake_case 白名单(filename/file_size/update_time/create_time/delete_time)。flutter analyze 零问题 + flutter test 全绿(61 个既有用例不许退化);页面级交互建议补 testWidgets 用例(对照 login_page_test.dart)。OSS/FastDFS 两个引擎代码已完整但依赖被注释(x-pan-parent/server/pom.xml 中 oss/fastdfs 段),接入它们是"接线"而非"新建";自研新介质才走全流程:
接线 OSS(以阿里云 OSS 为例):
server/pom.xml 对 x-pan-storage-engine-oss 的依赖注释(它依赖 storage-engine-core,传递依赖自动带上 OSS SDK 3.18.5)。application.yaml 尾部已留注释模板(com.xiaoye.pan.storage.engine.oss.bucket-name/endpoint/access-key-id/access-key-secret/auto-create-bucket),解除注释并把密钥换成 ${OSS_*:} 空兜底环境变量注入。红线:切换前必须完成存量对象迁移——存量 x_pan_file.real_path 是 MinIO 对象名,新引擎无法解读(《06-部署运行》第 7 节;common/migration/StorageMigrationRunner.java 即为此准备的迁移启动器,⚠️ 待确认其当前完成度后使用)。OSSBucketInitializer 建桶、上传/秒传/下载 302(OSS 实现同样应提供预签名,对照 MinIO 的 generatePresignedGetUrl 在 FileController.download 的接线方式)、minioStorage 熔断实例语义(新引擎应注册等价熔断,对照 MinIOStorageEngine.doStore 上的 @CircuitBreaker + @Retry)。自研新介质(如 WebDAV 网关、SFTP、Azure Blob):
framework/storage-engine/storage-engine-<name>,依赖 storage-engine-core;StorageEngine 五操作(store/delete/storeChunk/mergeFile/realFile),继承 AbstractStorageEngine 复用参数校验骨架,Context 对象在 storage-engine-core/.../context/ 已备好;@ConditionalOnProperty("pan.storage.<name>.enabled") 装配(对照 MinIOStorageEngine),配置属性类对照 MinIOStorageEngineProperties;FileController 的可选注入方式(minioStorageEngine != null 判断)接入直链路径,否则大文件自动回落应用流式代理(50MB 阈值逻辑不变)。以"通知中心 90 天归档清理"为例(当前 x_pan_notification 无清理任务,见《13-技术债务与演进建议》):
framework/schedule 的 ScheduleTask 接口(对照 common/schedule/task/CleanExpireRecycleFileTask.java);单一 cron 型任务也可用原生 @Scheduled(对照 common/gc/OrphanFileGc.java,cron 走配置 xpan.gc.orphan-cron)。@EnableScheduling 已在启动类,勿删(红线 7)。common/schedule/launcher/ 下加 Launcher(对照 CleanExpireRecycleFileTaskLauncher)把任务挂进 ScheduleTaskHolder;cron 参数化到 application.yaml。StartedListener 启动日志后正常注册(框架层有 ScheduleTaskTest 可参照写单测)。六个 SOP 的落点虽然分散在三端,但改动扩散路径是同构的——先定契约、后落服务端、再同步两端调用方,最后过门禁:
动代码前问五问:① Controller 是否在 modules/<模块>/controller/ 子包(鉴权自动生效的前提)?② 新路径是否会把客户端输入拼进存储/文件路径(是则先加 @Pattern 白名单 + 工具层清洗,红线 3)?③ 是否新增了"复用 realFileId"的路径(是则必须进 SEC_UPLOAD_IDENTIFIER_LOCKS 同一临界区,红线 8)?④ 失败计数/防爆破是否用了 Redis 原生 INCR + 仅首次 EXPIRE(红线 6)?⑤ 三端契约是否同步(Web src/api/、移动端 lib/services/、code=10/New-Access-Token 语义不动)?这五问全部来自真实事故,比任何风格指南都优先。