整体平台上传功能大改造
This commit is contained in:
@@ -0,0 +1,403 @@
|
||||
# 文件存储分层改造计划
|
||||
|
||||
> 创建日期:2026-09-09
|
||||
> 当前状态:**进行中**(改造中断时,从「进度总表」里第一个未勾选的条目继续)
|
||||
> 适用范围:`go/`(服务端)+ `backend/`(租户后台前端)+ `platform/`(平台端前端)
|
||||
|
||||
---
|
||||
|
||||
## 0. 进度总表
|
||||
|
||||
> 每完成一项,把 `- [ ]` 改成 `- [x]` 并更新日期。中断后从第一个 `- [ ]` 继续。
|
||||
|
||||
| # | 阶段 | 内容 | 状态 |
|
||||
|---|---|---|---|
|
||||
| S1 | 数据库 | `yz_system_files` 新增 `source`/`scope`/`storage`/`object_key` + 索引 + `EnsureSystemFileStorageColumns()` | ✅ 2026-09-09 |
|
||||
| S2 | services | `storage_service.go`:新增 `UploadContext`/`BuildObjectKey()`/`Stage()`/`Commit()`/`Move()` | ✅ 2026-09-09 |
|
||||
| S3 | services | 新增 `services/file_service.go`:MD5 精确查重、入库、路径解析、文件类型推断 | ✅ 2026-09-09 |
|
||||
| S4 | controllers | `backend_file.go`:上传走新目录规则 + 先算 MD5 后落盘 + 列表/删除 scope 隔离 | ✅ 2026-09-09 |
|
||||
| S5 | controllers | `platform_file.go`:固定 `platform/日期`,不做租户/用户分层 | ✅ 2026-09-09 |
|
||||
| S6 | controllers | `qiniu_upload.go`:token 的 `keyPrefix` 带 `platform/` 前缀,入库写 `source` | ✅ 2026-09-09 |
|
||||
| S7 | 前端 | backend 端上传点接入新参数(含修复 5 处 404 的 `/admin/uploadfiles`) | ✅ 2026-09-09 |
|
||||
| S8 | 前端 | platform 端上传点接入新参数(含修复 `/platform/upload`、资质上传) | ✅ 2026-09-09 |
|
||||
| S9 | 迁移 | `cmd/migrate_storage`(默认 dry-run,`--apply` 执行,七牛走 Move)— **脚本已就绪,尚未执行** | ✅ 2026-09-09(待执行) |
|
||||
| S10 | 验收 | 上传/去重/隔离/迁移全链路验证 | ☐ |
|
||||
|
||||
---
|
||||
|
||||
## 1. 目标与非目标
|
||||
|
||||
### 目标
|
||||
1. 两个端的文件物理隔离:`backend/` 与 `platform/` 分目录。
|
||||
2. backend 端按租户隔离:`backend/{tid}/`。
|
||||
3. 租户内区分**租户共享文件**与**用户个人文件**:个人文件落到 `backend/{tid}/{tuid}/`。
|
||||
4. 常规按 `年/月/日` 分目录。
|
||||
5. MD5 去重**精确到归属**:
|
||||
- 租户共享文件 → 同 `tid` 内 MD5 相同才算重复;
|
||||
- 用户个人文件 → 同 `tid` + 同 `tuid` 内 MD5 相同才算重复;
|
||||
- 「租户已有 a 文件」与「用户 c 上传同样文件」**不冲突**,可并存。
|
||||
6. 去重改为**先算 MD5 再落盘**,重复文件不产生物理垃圾。
|
||||
|
||||
### 非目标(本次不做)
|
||||
- 不做文件访问鉴权(`/uploads` 仍为公开静态目录),仅做目录隔离。
|
||||
- 不迁移 `themes/` 下的官网模板(`POST /platform/template/upload` 保持原样,不进文件表)。
|
||||
- 不改组织架构 CSV 导入(`importOrganization` 是临时解析文件,不入库 `yz_system_files`)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 现状分析
|
||||
|
||||
### 2.1 服务端链路
|
||||
|
||||
| 入口 | 代码位置 | 说明 |
|
||||
|---|---|---|
|
||||
| `POST /backend/uploadfile`、`/backend/uploadfiles` | `controllers/backend_file.go:463` | backend 端,服务端中转 |
|
||||
| `POST /platform/uploadfile`、`/platform/uploadfiles` | `controllers/platform_file.go:463` | platform 端,服务端中转 |
|
||||
| `GET /platform/qiniu/token` + `POST /platform/qiniu/save` | `controllers/qiniu_upload.go` | 七牛前端直传,**仅 platform 端有路由** |
|
||||
| `POST /platform/template/upload` | `controllers/platform_template.go` | 模板 zip → `themes/`,不进文件表 |
|
||||
|
||||
- 存储实现:`services/storage_service.go`,`LocalStorage`(`BaseDir=uploads`、`BaseURL=/`)与 `QiniuStorage`。
|
||||
- 当前路径规则:`2006/01/02/{UnixNano}{ext}`,**无任何端/租户/用户维度**。
|
||||
- 静态映射:`beego.SetStaticPath("/uploads","uploads")`(`main.go:23`)。
|
||||
- `backend_file.go` 与 `platform_file.go` 是**复制粘贴的两份**(约 900 行几乎全同),改规则必须同步两处。
|
||||
|
||||
### 2.2 数据库
|
||||
|
||||
`yz_system_files`(`models/system_file.go`)现有字段:`tid / uid / tuid / name / type / cate / size / src / uploader / md5 / create_time / update_time / delete_time`。
|
||||
|
||||
语义现状(重要):
|
||||
- `uid` = 上传者 ID(controller 里直接写 `claims.UserID`)。
|
||||
- `tuid` = 表单可选参数,**前端从未传过,库中全为 NULL**。
|
||||
- 没有「来源端」「共享/个人」「存储 key」字段 → 无法做目录归属与迁移。
|
||||
|
||||
ID 语义(已确认):
|
||||
- **backend 端登录**:`services/platform_auth.go:122` 用 `tenantUser.Uid` 签发 JWT → `claims.UserID` 就是**租户用户 uid(8 位,如 67091493)**,`claims.TenantId` = 租户 ID(如 234573)。
|
||||
- **platform 端登录**:`services/platform_auth.go:79` 用 `AdminUser.ID` 签发 → `claims.UserID` 是平台管理员 ID,`claims.TenantId = 0`。
|
||||
|
||||
**结论:backend 端 JWT 里天然就带着「租户 ID + 用户 uid」,个人文件目录名可直接用 `claims.UserID`;前端传 `tuid` 只在"代别人上传"场景才需要。**
|
||||
|
||||
### 2.3 前端上传点全量清单
|
||||
|
||||
#### backend 端(`backend/src`)—— 21 处代码点,19 处真实发请求
|
||||
|
||||
| # | 文件(相对 `backend/src`) | 功能 | 接口 | 现状参数 |
|
||||
|---|---|---|---|---|
|
||||
| B1 | `views/system/fileManager/components/uploadFile.vue` | 系统文件库上传 | `/backend/uploadfile` | `cate`(重复 append 两次) |
|
||||
| B2 | `views/components/UmoEditor.vue` | 富文本图片/视频 | `/backend/uploadfile` | 无 |
|
||||
| B3 | `views/apps/cms/article/index/components/edit.vue` | 文章封面 | `/backend/uploadfile` | `cate=article` |
|
||||
| B4 | `views/apps/cms/article/type/components/edit.vue` | 文章分类默认图 | `/backend/uploadfile` | 无 |
|
||||
| B5 | `views/moduleshop/center/index.vue` | 模块中心缩略图 | `/backend/uploadfile` | `cate=module` |
|
||||
| B6 | `views/basicSettings/siteSettings/components/normalSettings.vue` | 站点 Logo | `/backend/uploadfile` | `cate=site` |
|
||||
| B7 | 同上 | 站点白色 Logo | `/backend/uploadfile` | `cate=site` |
|
||||
| B8 | 同上 | 站点 ico 图标 | `/backend/uploadfile` | `cate=site` |
|
||||
| B9 | `views/apps/oa/schedule/components/detail.vue` | 日程相关图片/粘贴截图 | `/backend/uploadfile` | 无 |
|
||||
| B10 | `views/apps/oa/reimburse/components/detail.vue` | 报销发票/票据 | `/backend/uploadfile` | `cate=reimbursement-invoice` |
|
||||
| B11 | `views/apps/oa/employeefile/components/recordEditDialog.vue` | 员工档案记录附件 | `/backend/uploadfile` | 无 |
|
||||
| B12 | `views/apps/oa/employeefile/components/fileDetailDrawer.vue` | 员工证照-学历照片 | `/backend/uploadfile` | 无 |
|
||||
| B13 | `views/apps/cms/banner/components/edit.vue` | Banner 图片 | **`{BASE}/admin/uploadfiles`** ⚠️ | 无 |
|
||||
| B14 | `views/apps/cms/solution/index/components/edit.vue` | 方案图片 | **`{BASE}/admin/uploadfiles`** ⚠️ | 无 |
|
||||
| B15 | `views/apps/cms/product/index/components/edit.vue` | 产品图片 | **`{BASE}/admin/uploadfiles`** ⚠️ | 无 |
|
||||
| B16 | `views/apps/cms/frontMenu/components/edit.vue` | 前端菜单图片 | **`{BASE}/admin/uploadfiles`** ⚠️ | 无 |
|
||||
| B17 | `views/apps/cms/friendlink/components/edit.vue` | 友情链接 Logo | **`{BASE}/admin/uploadfiles`** ⚠️ | 无 |
|
||||
| B18 | `views/basicSettings/tenants/components/qualification.vue` | 租户资质文件 | `/api/platform/common/upload` ⚠️ | 无,提交为 mock |
|
||||
| B19 | `views/apps/organization/components/ImportExportDialog.vue` | 组织架构 CSV 导入 | `/backend/.../importOrganization` | CSV,不入库文件表 |
|
||||
| B20 | `views/moduleshop/publish/index.vue` | 模块 zip | 无(TODO 死代码) | — |
|
||||
| B21 | `views/moduleshop/components/createModules.vue` | 模块 zip | 无(TODO 死代码) | — |
|
||||
|
||||
#### platform 端(`platform/src`)—— 14 处代码点,9 处真实发请求
|
||||
|
||||
| # | 文件(相对 `platform/src`) | 功能 | 接口 | 通道 |
|
||||
|---|---|---|---|---|
|
||||
| P1 | `views/system/fileManager/components/uploadFile.vue` | 平台文件管理 | `/platform/uploadfile` | 服务端中转 |
|
||||
| P2 | `views/components/UmoEditor.vue` | 富文本(笔记本) | `/platform/uploadfile` | 服务端中转 |
|
||||
| P3 | `views/basicSettings/siteSettings/components/normalSettings.vue` | 站点 Logo | `/platform/uploadfile` | 服务端中转 |
|
||||
| P4 | 同上 | 站点白色 Logo | `/platform/uploadfile` | 服务端中转 |
|
||||
| P5 | 同上 | 站点 ico 图标 | `/platform/uploadfile` | 服务端中转 |
|
||||
| P6 | `views/moduleshop/center/index.vue` | 模块中心缩略图 | `/platform/uploadfile` | 服务端中转 |
|
||||
| P7 | `views/platform/softwareupgrade/components/edit.vue` | 软件升级包(4 平台) | `smartUpload()` | **七牛直传 / 本地自适应** |
|
||||
| P8 | `views/template/index.vue` | 官网模板 zip | `/platform/template/upload` | 服务端中转(不入库) |
|
||||
| P9 | `views/apps/babyhealth/users/components/userEdit.vue` | 用户头像 | **`{BASE}/platform/upload`** ⚠️ | 服务端中转 |
|
||||
| P10 | `views/apps/babyhealth/users/components/userEdit.vue` | 头像(裁剪后) | `uploadAvatar()` | ❌ 死代码(函数未定义) |
|
||||
| P11 | `views/basicSettings/tenants/components/qualification.vue` | 租户资质图片 | `/api/platform/common/upload` ⚠️ | 悬空(无代理/无 token) |
|
||||
| P12 | `views/apps/babyhealth/babys/components/edit.vue` | 宝贝头像 | `uploadAvatar()` | ❌ 死代码 |
|
||||
| P13 | `views/moduleshop/components/createModules.vue` | 创建模块包 | 无(调用被注释) | ❌ 不发请求 |
|
||||
| P14 | `views/moduleshop/publish/index.vue` | 发布模块包 | 无(TODO) | ❌ 不发请求 |
|
||||
|
||||
### 2.4 已发现的问题(本次一并处理)
|
||||
|
||||
| 级别 | 问题 | 说明 |
|
||||
|---|---|---|
|
||||
| 🔴 | **`/admin/uploadfiles` 路由不存在** | `go/routers` 全量搜索无 `/admin` 前缀路由;B13~B17 五处 CMS 上传实际会 404(`VITE_API_BASE=https://api.yunzer.cn`)。需收敛到 `/backend/uploadfile`。 |
|
||||
| 🔴 | **`/platform/upload` 路由不存在** | P9 头像上传会 404。需收敛到 `/platform/uploadfile`。 |
|
||||
| 🟡 | **`/api/platform/common/upload` 路由不存在** | B18 / P11 悬空,且提交逻辑是 mock。 |
|
||||
| 🔴 | **去重先落盘后判断** | 命中重复时物理文件已写入磁盘/七牛且未删除 → 产生孤儿垃圾。 |
|
||||
| 🔴 | **去重维度只有 `tid`** | 不区分端、不区分用户,与"精确到用户"要求不符。 |
|
||||
| 🟡 | **七牛直传的 `md5` 存的是 etag** | `qiniu_upload.go:168` 把 `hash`(etag)当 md5 存,与本地真 MD5 不同源,跨存储去重会失准。 |
|
||||
| 🟡 | **物理删除路径脆弱** | `removePhysicalBySrc` 直接 `os.Remove(TrimPrefix(src,"/"))`,依赖进程 CWD。 |
|
||||
| 🟡 | **删除语义不一致** | 单条 `DeleteFile` 只软删,`BatchDeleteFiles` 却真删物理文件。 |
|
||||
| 🟢 | `cate` 被重复 append | `api/file.js` 的 `options.cate` 与调用处手动 append 各一次 → multipart 里两个 `cate`。 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 目标目录规范
|
||||
|
||||
```
|
||||
{存储根}/ 本地: uploads/ 七牛: bucket 根
|
||||
├── backend/
|
||||
│ └── {tid}/ 例: 234573
|
||||
│ ├── 2026/09/09/{ts}_{rand}.{ext} ← scope=tenant(租户共享)
|
||||
│ └── {tuid}/ 例: 67091493
|
||||
│ └── 2026/09/09/{ts}_{rand}.{ext} ← scope=user(用户个人)
|
||||
└── platform/
|
||||
└── 2026/09/09/{ts}_{rand}.{ext} ← 平台端,不分层
|
||||
```
|
||||
|
||||
- `{ts}_{rand}`:`UnixNano` + 6 位随机(避免同纳秒并发冲突)。
|
||||
- 七牛用同样的 key 字符串(`/` 即逻辑目录)。
|
||||
- `tid=0`(platform 端或缺失租户上下文)时 backend 端路径退化为 `backend/0/...`,并在日志告警。
|
||||
|
||||
### 访问 URL
|
||||
- 本地:`/uploads/backend/234573/2026/09/09/xxx.png`
|
||||
- 七牛:`{domain}/backend/234573/2026/09/09/xxx.png`
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据库改造(S1)
|
||||
|
||||
### 4.1 新增字段
|
||||
|
||||
```sql
|
||||
ALTER TABLE `yz_system_files`
|
||||
ADD COLUMN `source` varchar(16) NOT NULL DEFAULT 'backend' COMMENT '来源端: backend-租户后台 platform-平台端',
|
||||
ADD COLUMN `scope` varchar(16) NOT NULL DEFAULT 'tenant' COMMENT '归属: tenant-租户共享 user-用户个人',
|
||||
ADD COLUMN `storage` varchar(16) NOT NULL DEFAULT '' COMMENT '存储类型: local/qiniu(冗余,便于迁移与排查)',
|
||||
ADD COLUMN `object_key` varchar(512) NOT NULL DEFAULT '' COMMENT '存储相对路径(不含域名),用于迁移与精确删除';
|
||||
|
||||
ALTER TABLE `yz_system_files`
|
||||
ADD KEY `idx_file_dedup` (`source`, `scope`, `tid`, `tuid`, `md5`),
|
||||
ADD KEY `idx_file_owner` (`source`, `tid`, `scope`, `tuid`, `delete_time`);
|
||||
```
|
||||
|
||||
> 不建议加 UNIQUE:`delete_time` 软删 + 并发上传下唯一索引会直接报错,改用普通索引 + 代码层查重。
|
||||
|
||||
### 4.2 字段语义(改造后明确)
|
||||
|
||||
| 字段 | 语义 |
|
||||
|---|---|
|
||||
| `tid` | 租户 ID |
|
||||
| `uid` | **上传者** ID(保持不变) |
|
||||
| `tuid` | **归属用户** ID(个人文件必填,共享文件为 NULL) |
|
||||
| `source` | `backend` / `platform` |
|
||||
| `scope` | `tenant` / `user` |
|
||||
| `object_key` | 存储相对路径,如 `backend/234573/67091493/2026/09/09/xxx.png` |
|
||||
|
||||
### 4.3 落地方式
|
||||
|
||||
在 `models/system_file.go` 增加 `EnsureSystemFileStorageColumns()`(参照 `EnsureTenantUserGroupColumn` 的既有模式,`ALTER` 报错忽略),并在 `BackendFileController.Prepare()` / `PlatformFileController.Prepare()` 中调用。SQL 脚本同步落到 `go/docs/sql/`。
|
||||
|
||||
---
|
||||
|
||||
## 5. 服务端改造
|
||||
|
||||
### S2 `services/storage_service.go`
|
||||
|
||||
```go
|
||||
// UploadContext 上传上下文,决定最终落盘路径
|
||||
type UploadContext struct {
|
||||
Source string // backend / platform
|
||||
Tid uint64 // 租户 ID
|
||||
Tuid uint64 // 归属用户 ID,0 = 租户共享
|
||||
Ext string // 扩展名
|
||||
}
|
||||
|
||||
// BuildObjectKey 生成存储相对路径(不含域名、不含 BaseDir)
|
||||
// backend 共享: backend/234573/2026/09/09/xxx.png
|
||||
// backend 个人: backend/234573/67091493/2026/09/09/xxx.png
|
||||
// platform : platform/2026/09/09/xxx.png
|
||||
func BuildObjectKey(ctx UploadContext) (key, datePath string)
|
||||
```
|
||||
|
||||
新增方法(保留原 `Upload` 内部复用):
|
||||
- `StageToTemp(file, header) (tmpPath string, md5 string, size int64, err error)` — 流式算 MD5 并写入**同磁盘**临时目录 `uploads/.tmp/`(保证后续 `os.Rename` 不跨盘)。
|
||||
- `CommitTemp(tmpPath, objectKey) (*UploadResult, error)` — `MkdirAll` + `Rename`(跨盘失败则回退 `io.Copy`)。
|
||||
- `UploadWithContext(file, header, ctx)` — 组合上面两步,供迁移脚本等简单场景使用。
|
||||
|
||||
> 七牛同样"先算 MD5":先 `StageToTemp` 得到 md5 + 临时文件 → 查重 → 命中则删临时文件返回已存在,未命中才 `Put` 到新 key。
|
||||
|
||||
### S3 新增 `services/file_service.go`
|
||||
|
||||
- `FindDuplicate(source, scope string, tid, tuid uint64, md5 string) (*models.SystemFile, error)`
|
||||
- `CreateFileRecord(...) (uint64, error)` — 统一写入 `source/scope/object_key/storage`
|
||||
- `ListFiles(source, scope string, tid, tuid uint64, ...) ` — 列表的 scope 隔离
|
||||
- `RemovePhysical(storageType, objectKey, src string) error` — 用 `object_key` 精确删除,替代脆弱的 `removePhysicalBySrc`
|
||||
|
||||
### S4 `controllers/backend_file.go`
|
||||
|
||||
1. `Prepare()` 里调 `models.EnsureSystemFileStorageColumns()`。
|
||||
2. `UploadFile` 流程改为:
|
||||
```
|
||||
鉴权 → effectiveTid → 解析 tuid(form > X-Tenant-User-Id 头 > claims.UserID)
|
||||
→ StageToTemp(拿到 md5/size)
|
||||
→ FindDuplicate(source=backend, scope, tid, tuid, md5)
|
||||
命中 → 删临时文件 → 返回 code 201(文件已存在)
|
||||
未命中 → CommitTemp(objectKey) → 入库 → 返回 code 200
|
||||
```
|
||||
3. `scope` 判定:`tuid > 0` → `user`,否则 `tenant`。
|
||||
4. 列表接口 `GetAllFiles` / `GetCateFiles` / `GetUserCate` 增加 `scope` 过滤:
|
||||
- 默认只返回 `scope=tenant`;
|
||||
- `?scope=user` 时按当前 `tuid` 过滤,只返回本人文件。
|
||||
5. 删除:统一用 `object_key` 删除物理文件;统一软删与批量删除的语义(批量删除不再误删物理文件,彻底删除才删)。
|
||||
|
||||
### S5 `controllers/platform_file.go`
|
||||
|
||||
- `source=platform`,路径固定 `platform/日期`,**忽略 `tuid` 与租户分层**。
|
||||
- 其余(先算 MD5 再落盘、`object_key` 入库、删除修复)与 S4 保持一致。
|
||||
|
||||
### S6 `controllers/qiniu_upload.go`
|
||||
|
||||
- `GetUploadToken` 返回的 `keyPrefix` 改为 `platform/2026/09/09/{ts}`(原来是 `2026/09/09/{ts}`)。
|
||||
- `SaveFileRecord` 入库时写 `source=platform`、`scope=tenant`、`object_key=req.Key`。
|
||||
- 明确 `md5` 字段:直传场景无法拿到真 MD5,**保留 etag 但写入时打标**(在 `md5` 为空时用 etag,并在注释中说明;后续如需精确去重,此通道需改为服务端中转)。
|
||||
|
||||
### S7 / S8 前端改造
|
||||
|
||||
统一封装:
|
||||
- `backend/src/api/file.js` 与 `platform/src/api/file.js` 的 `uploadFile(formData, options)` 增加 `options.tuid`,并**移除调用处重复的 `cate` append**(保留 options 里那次)。
|
||||
- 个人文件场景传 `tuid`,公共场景不传。
|
||||
|
||||
---
|
||||
|
||||
## 6. 前端上传点归属判定表
|
||||
|
||||
> **`tenant` = 租户共享(`backend/{tid}/日期/`);`user` = 用户个人(`backend/{tid}/{tuid}/日期/`)**
|
||||
|
||||
### backend 端
|
||||
|
||||
| # | 功能 | 判定 | 依据 |
|
||||
|---|---|---|---|
|
||||
| B1 | 系统文件库上传 | `tenant` | ✅ 已确认:素材库不分「共享/我的」,全部走租户共享;个人文件只来自 B9/B10 |
|
||||
| B2 | 富文本(文章/方案/产品正文) | `tenant` | 业务内容,全租户可见 |
|
||||
| B3 | CMS 文章封面 | `tenant` | 业务数据 |
|
||||
| B4 | CMS 文章分类默认图 | `tenant` | 业务数据 |
|
||||
| B5 | 模块中心缩略图 | `tenant` | 模块市场资源 |
|
||||
| B6/B7/B8 | 站点 Logo / 白色 Logo / ico | `tenant` | 租户级配置 |
|
||||
| B9 | OA 日程图片 | `user` | ✅ 已确认:按日程创建人归属 |
|
||||
| B10 | OA 报销发票 | `user` | ✅ 已确认:挂在员工个人报销单下 |
|
||||
| B11 | 员工档案记录附件 | `tenant` | ✅ 已确认:HR 需跨员工查看 |
|
||||
| B12 | 员工证照(学历照片) | `tenant` | ✅ 已确认:HR 需跨员工查看 |
|
||||
| B13~B17 | CMS Banner/方案/产品/菜单/友链 | `tenant` | 业务数据 + **需修 404**(✅ 已确认要修) |
|
||||
| B18 | 租户资质文件 | `tenant` | **需修 404**(✅ 已确认要修,接口需重新设计) |
|
||||
| B19 | 组织架构 CSV 导入 | 不涉及 | 临时解析,不入库 |
|
||||
|
||||
### platform 端(全部 `platform/日期/`,不做租户/用户分层)
|
||||
|
||||
| # | 功能 | 处理 |
|
||||
|---|---|---|
|
||||
| P1~P6 | 文件管理 / 富文本 / 站点 Logo ×3 / 模块缩略图 | 保持调用 `/platform/uploadfile`,无需传 `tuid` |
|
||||
| P7 | 软件升级包 | `smartUpload` → 七牛直传时 key 也要带 `platform/` 前缀(S6 已覆盖) |
|
||||
| P8 | 模板 zip | 不动(走 `/platform/template/upload`,不入库) |
|
||||
| P9 | 用户头像 | **需修**:`{BASE}/platform/upload` → `/platform/uploadfile` |
|
||||
| P10 / P12 | `uploadAvatar()` 死代码 | 本次不启用(或删除) |
|
||||
| P11 | 租户资质图片 | ⚠️ **待确认**,见 §8-Q3 |
|
||||
| P13 / P14 | 模块包(未发请求) | 不动 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 存量数据迁移方案(S9)
|
||||
|
||||
### 7.1 当前存储类型
|
||||
|
||||
✅ **已确认:`storage_type = qiniu`(七牛云)** → 迁移走 `BucketManager.Move`(服务端改名,不走流量、与文件大小无关)。
|
||||
|
||||
> 若后续切回本地存储,脚本自动改为 `os.Rename` 分支。
|
||||
|
||||
### 7.2 迁移原理
|
||||
|
||||
- **七牛云**:`BucketManager.Move(srcBucket, srcKey, destBucket, destKey)` — 同 bucket 内**服务端原子改名**,不走流量、秒级完成,只计 API 调用次数。与文件大小无关。
|
||||
- **本地**:`os.MkdirAll` + `os.Rename`(同盘,不搬数据)。
|
||||
|
||||
> 注意:现有 `services/storage_migration.go` 的 `MigrateFile` 是"下载再上传"的旧实现,**本次要替换为 Move/Rename**。
|
||||
|
||||
### 7.3 迁移步骤
|
||||
|
||||
1. 遍历 `yz_system_files WHERE delete_time IS NULL`。
|
||||
2. 从 `src` 解析出老 key:
|
||||
- 本地:`/uploads/2026/09/09/xxx.png` → `2026/09/09/xxx.png`
|
||||
- 七牛:`{domain}/2026/09/09/xxx.png` → `2026/09/09/xxx.png`
|
||||
3. 按新规则生成目标 key:**老数据一律按 `scope=tenant`(租户共享)迁移**(原因见 7.4)。
|
||||
4. Move / Rename。
|
||||
5. `UPDATE yz_system_files SET src=新URL, object_key=新key, source=..., scope='tenant', storage=... WHERE id=?`。
|
||||
|
||||
### 7.4 老数据的两个硬限制(必须知悉)
|
||||
|
||||
1. **还原不出"个人/共享"归属**:老数据 `tuid` 全为 NULL,`uid` 是上传者(管理员)ID,没有任何字段能说明"这是谁的个人文件"。因此老数据**统一按租户共享迁移**;个人目录只对改造后新增的文件生效。
|
||||
2. **`md5` 可能混了七牛 etag**:判断方法
|
||||
```sql
|
||||
SELECT COUNT(*) FROM yz_system_files WHERE LENGTH(md5) <> 32;
|
||||
```
|
||||
非 32 位的记录是 etag,不是真 MD5,这部分去重会失准(本次不修复,仅记录)。
|
||||
|
||||
### 7.5 硬编码 URL 风险
|
||||
|
||||
Move 后旧 URL 会 404。需排查是否有业务把上传 URL 写死在 `yz_system_files.src` 之外的地方(CMS 正文 `content`、官网模板配置 `yz_tenant_site_setting` 等)。**执行迁移前先做全库扫描**,这部分在 S9 里做。
|
||||
|
||||
### 7.6 脚本形态
|
||||
|
||||
`go/cmd/migrate_storage/main.go`:
|
||||
- 默认 `--dry-run`:只打印 `老key → 新key` 计划与统计,不改动任何数据。
|
||||
- `--apply`:真正执行。
|
||||
- `--tid=234573`:可选,只迁移指定租户。
|
||||
- 幂等:目标 key 已存在则跳过,可重复执行。
|
||||
|
||||
---
|
||||
|
||||
## 8. 待确认事项(**未答复前这些模块不改动**)
|
||||
|
||||
### 已确认
|
||||
|
||||
| 编号 | 结论 | 影响范围 |
|
||||
|---|---|---|
|
||||
| Q2 | **日程图片、报销发票 → `user`(个人);员工档案附件、员工证照 → `tenant`(共享)** | B9~B12 |
|
||||
| Q3 | **三组坏链全部修**:①`/admin/uploadfiles`(B13~B17)→ `/backend/uploadfile`;②`/platform/upload`(P9)→ `/platform/uploadfile`;③`/api/platform/common/upload`(B18、P11 资质)→ 重新设计 | B13~B18、P9、P11 |
|
||||
| Q4 | **存储类型 = 七牛云 qiniu** → 迁移走 `BucketManager.Move` | S9 |
|
||||
|
||||
| Q1 | **素材库不分 Tab,全部走租户共享。** 个人文件只来自 B9 日程图片、B10 报销发票。后端已备好 `scope=user` 能力,以后要加「我的文件」Tab 只需前端加 Tab + 传参。 | B1 |
|
||||
|
||||
### 待确认
|
||||
|
||||
无(Q1~Q4 全部已确认)。
|
||||
|
||||
### 后续单列任务(不在本次改造范围)
|
||||
|
||||
| 编号 | 事项 | 说明 |
|
||||
|---|---|---|
|
||||
| T1 | **租户资质业务落库** | B18/P11 的上传通道已修(改为走通用上传接口、带上 token),但 `submitForm` 仍是前端 mock,后端**没有资质表也没有保存接口**。需要新表(tid/type/file_url/expire_time/remark)+ 保存/详情接口,属新功能,另开任务。 |
|
||||
| T2 | 七牛直传通道的 MD5 是 etag | 软件升级包走七牛直传,`md5` 存的是 etag 与服务端中转算出的真 MD5 不同源,该通道查重只在通道内有效。如需全局精确去重,需把直传改为服务端中转(大文件代价高)。 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 风险与回滚
|
||||
|
||||
| 风险 | 应对 |
|
||||
|---|---|
|
||||
| 新目录规则上线后老 URL 失效 | 迁移前先全库扫描硬编码引用;迁移脚本先 dry-run |
|
||||
| `effectiveTid()` 拿到 0 | backend 端 tid=0 时落 `backend/0/` 并打 WARN 日志,不阻断 |
|
||||
| 并发上传同文件 | 临时文件名带随机后缀;查重与入库之间的极短窗口允许少量重复(后续可加分布式锁) |
|
||||
| `os.Rename` 跨盘失败 | 回退 `io.Copy` |
|
||||
| 改造中断 | 按 §0 进度总表从第一个未勾选项继续 |
|
||||
| 回滚 | 服务端改动集中在 `storage_service.go` + 两个 file controller,回滚即恢复这 3 个文件的旧版本;数据库新增列可保留(不影响旧逻辑) |
|
||||
|
||||
---
|
||||
|
||||
## 10. 验收清单
|
||||
|
||||
- [ ] backend 上传图片 → 落盘到 `uploads/backend/{tid}/2026/09/09/`
|
||||
- [ ] backend 传 `tuid` 上传 → 落盘到 `uploads/backend/{tid}/{tuid}/2026/09/09/`
|
||||
- [ ] platform 上传 → 落盘到 `uploads/platform/2026/09/09/`
|
||||
- [ ] 同一租户重复上传同一文件 → 返回 201,且**磁盘上没有新增文件**
|
||||
- [ ] 租户已有 a 文件,用户 c 上传同样文件 → 正常入库,不冲突(两处物理文件并存)
|
||||
- [ ] 用户 c 再传同一文件 → 返回 201
|
||||
- [ ] 文件列表:`scope=user` 只看到自己的;默认只看到租户共享
|
||||
- [ ] 删除文件 → 物理文件按 `object_key` 精确删除
|
||||
- [ ] 七牛模式:`keyPrefix` 带 `platform/` 前缀
|
||||
- [ ] 迁移 dry-run 输出正确,apply 后旧 URL 全部更新且可访问
|
||||
Reference in New Issue
Block a user