@@ -0,0 +1,76 @@
# 前端直传腾讯云 COS(视频大文件)
## Goal
让管理后台的视频上传绕开 PHP 服务器的中转通道,由浏览器直接把视频文件分片上传到腾讯云 COS。
解决两类痛点:(1) 服务器双倍带宽消耗(client→server→COS);(2) PHP `upload_max_filesize` / `post_max_size` 与 fpm 进程内存对大视频不友好。
首发场景:诊单 `CallRecordPanel.vue` 上传通话回放视频。
## What I already know
- 现有上传链路:`admin/src/components/upload/index.vue` → `POST /upload/video` ( `UploadController::video` )→ `UploadService::video()` → `storage/Driver` → `engine/Qcloud::upload()` 用 `putObject` 。
- 服务端入参依赖 `$_FILES` ,受 PHP 上传上限约束;返回 `{id, cid, type, name, uri, url}` , `uri` 经 `FileService::getFileUrl()` 拼接 OSS/CDN 域名。
- 存储驱动配置走 `dev_config.storage` (管理员可在后台「存储配置」切换 local/qiniu/aliyun/qcloud;本任务只对 qcloud 启用直传)。
- composer 已有 `qcloud/cos-sdk-v5 ^2.5` ,但 STS 临时凭证需要再装 `qcloud/cos-sts-sdk` 。
- 未提交代码:`CallRecordPanel.vue` 已加「上传视频」按钮,调用 `createManualCallRecord` + `attachLocalCallRecording` ,依赖上传组件吐出的 `response.data.uri/url` 。AuthMiddleware 已为 `tcm.diagnosis/uploadcallrecording` 等加白名单 — 直传新接口与之解耦,不需要白名单。
## Assumptions (temporary)
- 生产环境默认存储引擎已经是 `qcloud` ;切到 `local/aliyun/qiniu` 时 direct 模式需要降级回老链路。
- COS Bucket 已开启跨域 CORS(直传必须)。如未开启,需要运维在 COS 控制台配置 `AllowedOrigin = 后台域名 + 本地 dev` 、`AllowedMethod = PUT/POST/HEAD` 、`AllowedHeader = *` 、`ExposeHeader: ETag` 。
- 后台域名固定(暂无多租户独立域名)。
- 视频文件大小有合理上限(建议 2GB)。
## Decisions (locked)
- 视频上限 **2 GB ** (写进 STS policy `numeric_less_than_equal cos:content-length` + 前端 `file.size` 预检)。
- 保留 `/upload/video` 老接口;`upload/index.vue` 的 `direct` prop 默认 `false` ,仅 `CallRecordPanel.vue` 这类显式声明的视频场景启用直传。
- STS 凭证有效期 **30 分钟 ** ( 1800 秒)。
- 直传完成后**必须等 `oss-confirm` ** 返回(写完 `file` 表 + HEAD 校验通过)才 emit `success` ,保证业务侧拿到的 `uri/url` 一定有 `file_id` 。
## Requirements (evolving)
- [R1] 后端新增 `POST /adminapi/upload/oss-credentials` ,仅当 `storage.default == qcloud` 时返回 `{provider, bucket, region, credentials, expiredTime, key, host, cdn_url_base}` ;其它 driver 返回 `{provider: '<engine>', fallback: true}` 让前端自动降级到老链路。
- [R2] 后端新增 `POST /adminapi/upload/oss-confirm` , HEAD 一下 `key` 校验确实存在 + size 在阈值内,写入 `file` 表(沿用 `FileService::getFileUrl` ),返回与 `UploadService::video()` 完全对齐的 `{id, cid, type, name, uri, url}` 。
- [R3] 新装 composer 依赖 `qcloud/cos-sts-sdk` ;在 `engine/Qcloud.php` 增加 `getStsCredentials($keyPrefix, $maxSize)` 方法,复用现有 `access_key/secret_key/bucket/region` 。
- [R4] STS policy 限定 action: `name/cos:PostObject` 、`name/cos:PutObject` 、`name/cos:InitiateMultipartUpload` 、`name/cos:UploadPart` 、`name/cos:CompleteMultipartUpload` 、`name/cos:AbortMultipartUpload` 、`name/cos:ListParts` ; resource 限定 `qcs::cos:${region}:uid/${appId}:${bucket}/uploads/video/{YYYYMMDD}/*` 。
- [R5] 前端新增 npm 依赖 `cos-js-sdk-v5` ; `upload/index.vue` 增加 `direct: boolean` prop(默认 `false` ),`type==='video' && direct===true` 时走 STS + `cos.uploadFile` (含 `SliceSize=5MB` 、`onProgress` ),仍发出原 `success` 事件 `{ code, data: { uri, url, ... } }` 保持调用侧兼容。
- [R6] `CallRecordPanel.vue` 切到 `<upload type="video" direct :show-progress="true">` ,并支持上传过程中显示百分比(沿用组件内置 `el-dialog` 进度)。
- [R7] 失败/降级路径:STS 接口失败 / response 标记 `fallback=true` / `cos-js-sdk-v5` 上传报错时,自动降级到老的 `POST /upload/video` ,提示「直传失败,已切换到普通上传」。
- [R8] 安全:保留登录鉴权;STS 凭证 30 分钟有效期;前端拒绝不在 `accept` 列表的扩展名(沿用现有 `.wmv,.avi,...,.mp4,.mkv` )。
## Acceptance Criteria (evolving)
- [ ] 在 storage=qcloud 环境,从 `CallRecordPanel.vue` 上传 1.5GB mp4 文件,浏览器 Network 面板能看到流量直接打到 `*.cos.*.myqcloud.com` , `/upload/video` 没有调用。
- [ ] 上传过程中能看到分片进度条(5MB 一片,并发 ≥3)。
- [ ] 上传完成后,`call_record` 表正确写入 `recording_urls` ,前端表格 `recording_urls_list` 渲染出可播放视频。
- [ ] `file` 表新增一行,`uri` 形如 `uploads/video/20260508/xxxxx.mp4` , `url` 拼接了 CDN/COS 域名。
- [ ] 把 `dev_config.storage.default` 改成 `local` ,再次上传,自动降级到老 `/upload/video` 路径,整体功能正常。
- [ ] 凭证接口未登录调用返回 401。
- [ ] 上传一个被 STS policy resource 拒绝的 key(人为篡改 prefix),COS 返回 403,前端给出明确错误。
## Definition of Done
- 单元/集成测试:暂无 PHPUnit 工程,跳过自动化测试;保证手测覆盖 AC 全部场景。
- 前端 `npm run type-check` 通过、`npm run lint` 无新增告警。
- 后端 `php -l` 通过,新接口在 admin 路由可访问。
- 文档:在 `.trellis/spec/` (如已有上传相关 spec)补充直传规范;PRD 同步关闭 Open Questions。
- COS CORS / STS 子账号最小权限策略整理为 ops 备忘(写到 prd.md 的 Technical Notes)。
## Out of Scope (explicit)
- 不做断点续传(刷新页面不保留 UploadId)。
- 不做 PostObject 回调链路。
- 不做 aliyun / qiniu 直传。
- 不改 `upload/image` 、`upload/file` 链路(图片小文件用老链路即可)。
- 不做客户端 H5/uniapp 端直传(uniapp 本来就不走 admin)。
- 不做并发上传限流 / 全局上传队列。
## Technical Notes
- 关键文件:`server/app/common/service/storage/engine/Qcloud.php` 、`server/app/common/service/UploadService.php` 、`server/app/adminapi/controller/UploadController.php` 、`admin/src/components/upload/index.vue` 、`admin/src/views/tcm/diagnosis/components/CallRecordPanel.vue` 。
- composer 新增:`qcloud/cos-sts-sdk: ^3.0` 。
- npm 新增:`cos-js-sdk-v5` 。
- COS CORS 配置(运维):`AllowedOrigin: <admin 域名>` 、`AllowedMethod: PUT,POST,HEAD,GET` 、`AllowedHeader: *` 、`ExposeHeader: ETag,x-cos-request-id,x-cos-version-id` 。
- STS 子账号最小权限:仅赋予对 `${bucket}/uploads/video/*` 的 `cos:Get*` 、`cos:Put*` 、`cos:Post*` 、Multipart 系列动作;STS policy 在此基础上再用 condition + resource 收敛。
- Key 命名:`uploads/video/${YYYYMMDD}/${ulid}.${ext}` ,与现有 `UploadService::getUploadUrl()` 的 `Ymd` 分日目录保持一致。