chore: init repository skeleton

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-17 16:13:49 +08:00
commit d973c714f0
23 changed files with 2666 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{json,ts,tsx,yaml,yml,js,jsx}]
indent_style = space
indent_size = 2
+9
View File
@@ -0,0 +1,9 @@
node_modules/
dist/
.turbo/
coverage/
.env
.env.*
!.env.example
*.log
.DS_Store
+1
View File
@@ -0,0 +1 @@
20
+6
View File
@@ -0,0 +1,6 @@
# 图片直播(photo-live)
图片直播平台:活动现场照片即拍即传、审核后实时分发的直播相册系统。
- 需求文档:[docs/01-需求说明书.md](docs/01-需求说明书.md)
- 实施总纲:[docs/plans/2026-09-17-实施总纲.md](docs/plans/2026-09-17-实施总纲.md)
+335
View File
@@ -0,0 +1,335 @@
# 图片直播项目需求说明书
版本:V1.3
日期:2026-09-17
适用阶段:第一版需求评审与开发
关联文档:[项目计划](./02-项目计划.md) · [技术实施方案](./03-技术实施方案.md) · [优化方案](./04-优化方案.md)
本项目为公司自用的活动图片直播系统,面向广交会、企业活动等场景。工作人员通过 App 创建相册并协作上传,观众通过网页实时看图和互动,公司网站通过 JS 悬浮组件同步展示照片。
本文用“已确认”表示用户明确提出或接受的要求;“建议基线”表示供第一版实施使用的设计建议;“待确认”表示仍需决定的范围。建议基线不等同于已确认需求。
V1.1 纳入 R17 至 R26 优化;V1.2 新增 R27 至 R30 国内外部署要求;V1.3 将第一期改为账号密码登录,移除飞书接入,新增 R31 定期用户接口同步。未冻结的同步周期、地域及容量参数仍按建议值标识。
## 1 产品范围
### 1.1 已确认范围
| 编号 | 模块 | 要求 |
|---|---|---|
| R01 | 工作人员 App | Flutter 开发,同时支持 Android 和 iOS |
| R02 | 工作人员身份 | 第一期仅账号密码登录,不接入飞书,不开放自行注册;App 与后台共用内部账号 |
| R03 | 相册管理 | App 内创建和配置相册,包括活动时间、地点等信息 |
| R04 | 多人协作 | 创建者管理相册并邀请同事,协作者管理自己的照片,管理员管理全部 |
| R05 | 照片上传 | 手机拍照或选择照片,多人上传,上传后自动公开,无人工审核和修图 |
| R06 | 上传任务 | 切换后台和锁屏后继续上传,提供上传记录列表 |
| R07 | 文件存储 | 对接现有阿里云 OSS Bucket,照片默认长期保存 |
| R08 | 观众访问 | 扫码或分享链接访问,不要求登录,不设访问密码 |
| R09 | 观众展示 | 顶部宣传图、活动信息、照片瀑布流 |
| R10 | 大图浏览 | 点击弹出大图,左右滑动切换,支持点赞、分享、下载 |
| R11 | 点赞 | 每张照片可连续点赞,多次累计,无需登录,不设取消 |
| R12 | 气泡 | 同一活动内所有在线观众能看到其他人的点赞气泡 |
| R13 | 多语言 | App、管理后台、观众端均支持语言切换 |
| R14 | 网站嵌入 | 引入 JS 后在网站右侧固定悬浮轮播,新照片持续更新,点击打开观众页 |
| R15 | 管理后台 | 公司统一管理账号、相册、照片和权限 |
| R16 | 相册浏览次数 | 记录每个相册的累计浏览次数,并在观众页活动信息区展示 |
| R17 | 连续拍摄 | 拍摄后自动准备并入队,常驻显示目标相册和队列状态 |
| R18 | 活动控制 | 允许上传和对外公开独立控制,支持批量隐藏及跨端同步 |
| R19 | 相册交接 | 管理员转移负责人,停用账号不删除历史照片 |
| R20 | 下载与水印 | 下载和原图下载分开控制,水印版本化,历史重生成明确发起 |
| R21 | 上传准备与历史 | 区分准备失败和上传失败,本地待传任务与服务端历史分离 |
| R22 | 原图与访问保护 | 已发布原图不可被旧上传授权覆盖,图片链接过期可恢复 |
| R23 | 浏览去重 | 同浏览器跨标签页共享匿名标识,窗口参数待定 |
| R24 | 气泡体验 | 自身广播不重复播放,后台暂停动画,支持减少或关闭动画 |
| R25 | 网站展示位 | JS 绑定展示位,后台更换相册,无需修改官网代码 |
| R26 | 用量管理 | 容量流量统计与告警,组件不可见时暂停轮询,不自动清理正式照片 |
术语约定:第一版建议一个活动对应一个直播相册;跨天活动在同一相册内分类。若后续需要一个活动下多个独立相册,再扩展活动实体。本版本中的“同一活动广播”按相册隔离。
### 1.2 建议基线
- 首批语言为简体中文和英文,其他语言可扩展;具体首发语种待确认。
- 图片处理仅包括解码、方向校正、缩略图、浏览图及可选水印,不包含美化修图。
- 相册可配置封面、宣传图、简介、分类、Logo、水印和原图下载开关。
- 新照片以提示方式更新瀑布流,避免直接插入导致正在浏览的位置跳动。
- 支持“最新”和“热门”列表,热门按累计点赞次数排序。
- 分享单张照片直接定位该照片;关闭大图回到原列表位置。
- JS 组件支持收起、关闭、尺寸和轮播间隔配置,默认优先轮播最近照片。
- 照片删除采用软删除;永久清理规则另行确定,不默认自动删除 OSS 原图。
### 1.3 第一版不包含
飞书登录与飞书绑定、相机直传、桌面传图客户端、AI 修图、人工修图工作流、审核流程、人脸找图、评论、留言墙、投票、抽奖、视频直播、付费下载、对外注册和多企业商业化租户系统。
### 1.4 国内管理与海外访问
已确认:管理后台与 App 业务请求访问国内服务器,图片直传现有国内 OSS;观众主要在国外,观众页及查询、浏览统计、点赞气泡由海外服务承接,图片使用 OSS 全球 CDN。第一版不建立第二套海外图片存储,不把图片文件经业务服务器跨境复制。
国内向海外同步公开展示所需元数据,海外向国内回传统计。采用事件触发推送与定时补漏的实施基线,同步周期和延迟指标待验证。海外直接读本地数据,不在每次浏览或点赞时请求国内。
| 编号 | 模块 | 要求 |
|---|---|---|
| R27 | 地域分工 | 国内管理和上传,海外观众服务,OSS 全球 CDN 分发图片 |
| R28 | 内容同步 | 相册分类语言、照片资源标识、展示位及隐藏删除状态可靠同步海外 |
| R29 | 互动回传 | 海外权威记录点赞浏览,国内展示回传统计及更新时间 |
| R30 | 海外验收 | 验证新图冷缓存、同步中断恢复、乱序去重和下线传播 |
| R31 | 用户同步 | 国内服务定期拉取指定用户接口,更新本地用户资料,支持日志、重试和手动补拉 |
公开状态增加海外同步中、海外可见、同步失败等管理提示。国内处理完成不代表海外立即可见;海外确认元数据完整与必要浏览资源可访问后展示。相册下线及照片隐藏优先同步;跨境中断期间不能承诺海外即时撤回,需明确告警和海外紧急停用通道。
## 2 角色与权限
工作人员资料通过指定接口定期同步到国内系统,登录凭据与项目权限在本系统管理,不把资料同步等同于密码同步。初始管理员通过受控初始化开通;其他手工账号是否保留待确认。相册角色与公司角色分开:同一工作人员可在相册 A 中是创建者,在相册 B 中是协作者。
| 操作 | 公司管理员 | 相册创建者 | 相册协作者 | 观众 |
|---|---|---|---|---|
| 激活停用与密码管理 | 全部 | 不允许 | 不允许 | 不允许 |
| 创建相册 | 允许 | 工作人员允许 | 工作人员允许 | 不允许 |
| 配置相册 | 全部 | 自己创建的相册 | 不允许 | 不允许 |
| 邀请移除协作者 | 全部 | 自己创建的相册 | 不允许 | 不允许 |
| 上传照片 | 全部 | 自己创建的相册 | 已加入的相册 | 不允许 |
| 管理照片 | 全部 | 相册全部照片 | 自己上传的照片 | 不允许 |
| 浏览点赞分享 | 允许 | 允许 | 允许 | 无需登录 |
| 下载原图 | 管理权限内 | 管理权限内 | 自己照片 | 按相册设置 |
“管理自己照片”建议包括修改分类、隐藏和软删除,不包含相册配置与他人照片操作。是否允许协作者重新公开自己隐藏的照片,按此建议在原型评审时确认。
服务端必须逐次校验权限;前端隐藏按钮不能替代权限验证。协作者被移除或账号被停用后,不得继续提交发布操作。已签发短期上传凭证可能尚未到期,服务端应拦截其发布并回收孤立上传任务。
## 3 业务流程
### 3.1 准备活动
1. 国内服务同步用户资料,管理员按确定规则激活账号、设置初始凭据并分配公司角色。
2. 工作人员登录 App,创建相册并填写活动信息。
3. 创建者邀请已开通账号的同事参加相册,不通过邀请开放注册。
4. 生成观众链接、二维码;需要时创建或选择有权限的网站展示位,绑定相册并生成嵌入代码。
5. 空相册可访问并展示“照片即将上线”等本地化提示。
### 3.2 上传与发布
1. 工作人员选择相册、分类,通过拍摄或系统相册选择图片。
2. App 将文件保存到受管理的本地任务目录,并记录上传任务。
3. 服务端验证身份与相册权限,签发限定对象范围的临时上传授权。
4. App 将文件直接上传 OSS,业务服务器不承担图片中转。
5. 服务端核验对象存在、大小和必要的完整性信息,创建处理任务。
6. 生成浏览资源后将照片设为公开,通知观众端和嵌入组件。
7. 上传和发布结果写回任务列表;失败时显示原因及恢复方式。
“上传即公开”指无需人工审批,必要图片处理完成后自动公开。上传结束到公开存在处理时间,不能把传输完成直接显示为已公开。
### 3.3 观众浏览与互动
1. 观众扫描二维码或打开链接,无需登录。
2. 查看宣传图、活动信息和瀑布流,按需切换语言、分类和排序。
3. 点击照片打开大图,左右滑动查看当前列表中的相邻照片。
4. 连续点赞时立即显示本地动画;确认后更新计数并广播活动气泡。
5. 分享相册或照片链接;按设置下载允许的图片版本。
6. 页面恢复或网络重连后重新获取计数和新增照片,不要求补播历史气泡。
### 3.4 网站悬浮轮播
1. 管理员配置网站展示位并授权,创建者或管理员将有管理权限的相册绑定展示位,生成嵌入代码。
2. 网站维护人员在页面引入 JS,设置公开的展示位标识;以后切换活动在后台修改绑定即可。
3. 组件在右侧悬浮,轮播已公开照片,自动发现新照片。
4. 点击图片打开观众页面,建议在新标签页定位该图片。
5. 隐藏或删除的照片应从后续轮播中移除;空相册不显示损坏图片。
## 4 功能明细
### 4.1 登录与账号
- 第一期管理后台仅账号密码登录,统一账号体系下 App 同样使用账号密码;飞书不在第一期开发和验收范围内。
- 支持退出、修改密码和管理员重置密码,不开放自行注册。
- 用户资料由国内定时任务调用指定接口新增或更新,不能按姓名匹配,必须使用上游稳定唯一 ID。
- 接口负责资料同步,默认不拉取明文密码、不覆盖本地密码、项目角色、相册成员关系和语言偏好。
- 新用户建议先处于待激活状态,由管理员完成初始密码与授权;初始密码交付、首次修改和是否允许自动激活待确认。
- 上游明确停用时,本地阻止登录、续签和发布并撤销会话;本地管理员主动停用不能被普通同步自动解除。重新启用策略待确认。
- 接口失败、空响应、分页未完成或增量中未出现某人,不能据此批量停用或删除用户。
- 后台展示上次成功同步时间、状态、新增更新停用及失败数量,允许管理员手动补拉和查看失败记录。
- 用户同步不删除历史相册和照片,不改变历史上传人;同步接口不可用时已有有效本地账号仍可按本地状态登录。
- 同步周期、接口鉴权、字段、分页、增量水位、离职和删除语义待接口文档确认。
### 4.2 相册字段
| 字段 | 建议规则 | 状态 |
|---|---|---|
| 标题 | 必填,支持多语言 | 标题已确认,规则建议 |
| 活动介绍 | 可选,支持多语言,第一版建议纯文本 | 建议基线 |
| 活动时间 | 开始与结束,允许跨天,结束不得早于开始 | 已确认需要,规则建议 |
| 活动时区 | 默认 Asia/Shanghai,可配置 | 建议基线 |
| 地点 | 场馆名和地址文本,支持多语言 | 已确认需要,规则建议 |
| 宣传图 | 观众页顶部展示,按原比例或规定裁切比例 | 已确认 |
| 封面 | 用于工作人员列表和分享,可复用宣传图 | 建议基线 |
| 主办承办信息 | 可选文本 | 待确认是否纳入 |
| 分类 | 名称、语言版本和显示顺序 | 建议基线 |
| Logo 和水印 | 可选;原图不覆盖 | 建议基线 |
| 下载与原图下载开关 | 原图下载以允许下载为前提;关闭全部下载不阻止截图 | V1.1 纳入 |
| 上传与公开开关 | 分开控制,活动结束不自动改变开关 | V1.1 纳入 |
| 默认语言 | 翻译缺失时用于回退 | 建议基线 |
活动结束不触发删除、禁止浏览或自动停止补图。相册采用独立上传与公开开关,下线后停止公开查询、新签发图片链接和公开频道订阅。
### 4.3 上传与记录
- 支持多选、批量上传;照片数和单文件大小上限通过技术验证后配置。
- 第一版建议支持 JPEG、PNG;iPhone HEIC 输入必须验证并完成可浏览格式转换。动态照片和 RAW 不默认支持。
- 上传记录按相册、状态和时间筛选,显示缩略图、文件名、进度、数量、错误原因。
- 准备阶段覆盖源文件获取、本地复制和空间检查,区分准备中与准备失败;准备完成才显示已入队。
- App 提供连续拍摄入口,单张自动入队;切换目标仅影响新照片。
- 服务端历史可跨设备查看,本机尚未上传的文件和队列不能在另一台手机直接恢复。
- 支持失败重试、批量重试和取消未完成任务;手动暂停与恢复为可选增强。
- 本地记录应跨 App 重启保留,禁止因页面关闭而丢失任务。
- 锁屏和后台由平台原生传输能力承接,不能仅依靠 Flutter 页面内任务。
- 强制退出、系统停止、权限变化、断网等情况下保留状态,并说明何时可恢复;不承诺无限后台执行。
- 账号退出不应继续使用旧权限创建上传;未完成队列按账号隔离,重新登录原账号再恢复。
- 已上传文件的本地缓存可按策略清理,不能提前清除未完成任务源文件。
### 4.4 观众页面
- 手机端双列瀑布流,照片保持合理比例;桌面端可增加列数。
- 顶部宣传图下展示活动信息;语言入口易于查找。
- 使用分页与按需渲染,大相册不一次加载全部照片。
- 大图支持左右滑动、缩放、关闭、当前序号和加载失败重试。
- 大图内点赞、分享、下载操作始终针对当前照片。
- 采用稳定浏览顺序,点赞改变热门排名时不突然切换当前图片。
- 相册累计浏览次数为第一版必做项,在观众页活动信息区展示,文案为“浏览次数”,不能标成“浏览人数”。
- 微信内分享与保存按浏览器能力提供引导;系统分享不可用时提供复制链接。
### 4.5 相册浏览统计
已确认:每个相册独立累计浏览次数,观众页必须展示;计数由服务端保存,不能只写浏览器本地存储。
浏览器匿名标识跨同源标签页共享并持久保存;存储不可用时降级为当前页标识,不能承诺精确去重人数。
建议口径:一次访问在同一浏览器匿名标识、同一相册的 30 分钟窗口内只计一次,窗口内刷新、返回和重新打开不重复累计;超过窗口后的新页面访问可再计一次,停留页面不按计时器自动加量。30 分钟为待确认参数。
- 相册有效、公开且页面可见并成功展示活动信息后上报,避免预加载误计。
- 单张照片分享入口归入所属相册,不单独另加相册浏览量。
- 网站 JS 自动轮播、后台预览、API 轮询、分页请求、图片资源请求和点赞不计入。
- 点击悬浮组件并实际打开观众页,按正常访问规则计入。
- 计数请求重试必须幂等,不能因网络失败重复累计。
- 这是匿名访问次数,不承诺准确去重人数;换设备、清除浏览器标识可能产生新访问。
- 观众页初次加载与计数确认后更新显示;第一版建议不单独广播浏览量。
- 后台建议展示相同累计数字用于核对,不包含独立 BI 分析系统。
### 4.6 点赞与气泡
- 点赞次数按照片累计,可匿名连续操作,不记录“已点赞不可再点”的状态。
- 同一相册内列表页和大图页均接收活动级气泡。
- 气泡建议包含当前被点赞照片的小缩略图,点击可打开对应照片。
- 气泡不抢占主要点击区域;密集互动时限制屏幕动画数量。
- 动画合并与计数分开:多次有效点击可合为一组动画,计数不能只加一次。
- 防止网络重试重复累计;基础频率控制用于稳定服务,不恢复为每人只能一次。
- 断开实时连接时允许浏览;重连后获取权威计数,不补播过期动画。
- 本地已播放批次不因自身广播重复播放,合并事件仅过滤自身部分。
- 页面不可见时暂停动画,恢复时只校准计数;支持减少或关闭气泡。
- 隐藏照片同时从待广播和待展示气泡中过滤。
### 4.7 多语言
- App、后台、观众页与组件使用统一语言代码,首批建议 zh-CN、en。
- 初次跟随设备或浏览器,手动选择优先,保存用户选择。
- 工作人员语言偏好可随账号同步,匿名观众保存在浏览器。
- 活动内容分别填写翻译,缺失则返回活动默认语言;图片文字不会自动翻译。
- 不同语言可配置宣传图;未配置时回退默认图片。
- 时间按活动时区明确展示;界面语言不改变活动实际时间。
- 第一版不包含自动机器翻译。
### 4.8 网站 JS 组件
- 网站引入一个版本化 JS 地址,通过 slotId 指定展示位,后台配置其相册绑定。
- 默认右侧固定悬浮,滚动页面时位置不变,支持图片轮播和自动更新。
- 建议提供尺寸、边距、轮播间隔、语言、收起和关闭选项。
- 不加载整个相册,只获取近期公开照片与小尺寸浏览资源。
- 第一版通过轮询更新展示位配置与照片,不依赖点赞模块;不可见时暂停,恢复后校准。
- 切换展示位相册后丢弃旧响应;管理员统一管理展示位,创建者只操作被授权展示位。
- 组件样式、事件和依赖不污染宿主站点;不能阻塞网站首屏。
- 访问异常、相册下线、无图、移动端窄屏均有可用状态。
- 公开嵌入代码不包含工作人员凭证;具体接入方式见实施文档。
### 4.9 活动控制与交接
关闭上传后拒绝新建任务和授权续签,尚未确认的对象等待重新开放或管理员处理;已经完成核验的处理任务可以继续。关闭公开不阻止内部上传处理,但所有观众入口停止返回相册内容。重新公开不会恢复隐藏或软删除照片。
批量隐藏返回逐项结果,并同步通知列表、大图及组件;已签发资源链接按确认的失效窗口处理。管理员转移负责人时明确原负责人是否保留协作者身份,历史上传人与审计记录不变。
### 4.10 下载水印与用量
允许下载且不允许原图时,只提供配置允许的浏览图或水印图。水印变更默认用于新照片;创建者或管理员可明确发起历史重生成,显示进度、失败可重试,完成前保留可用旧版本,原图不覆盖。
后台统计项目和相册容量、流量及相关资源用量,设置阈值和告警接收人。告警不自动删除照片或中断活动;日志和临时碎片与正式图片分别制定保留策略。
## 5 数据与长期保存
原图、浏览资源存放现有 OSS,账号、相册、照片归属和点赞计数存数据库。独立对象键避免同名覆盖,数据库不保存长期有效的签名 URL。
照片默认长期保存,不配置原图自动到期。需检查现有 Bucket 生命周期规则对项目路径的影响。允许清理失败上传产生的碎片和临时资源,但应与正式原图区分,并经过策略确认。
隐藏仅影响对外展示,不删除 OSS 原图。软删除、恢复和永久删除的权限及期限待确认。已有用户下载的文件无法通过后续隐藏操作收回。
## 6 非功能要求与验收
| 编号 | 验收场景 | 通过条件 |
|---|---|---|
| A01 | 密码登录 | App 和后台使用本地账号密码,无飞书入口;未激活与停用账号无法进入 |
| A02 | 协作权限 | 协作者不能修改他人照片,创建者不能越权其他相册 |
| A03 | 多人上传 | 同名图片不覆盖,重试同一任务不重复发布 |
| A04 | 后台上传 | Android 和 iOS 真机切后台、锁屏后按系统允许机制继续,受阻任务可恢复 |
| A05 | 上传恢复 | 断网、凭证过期和 App 重启后任务记录完整、可重试 |
| A06 | 自动发布 | 有效照片无需人工操作即可公开,损坏图片不进入公开列表 |
| A07 | 大相册 | 在确认的测试量下分页稳定,不随已浏览照片无限增长内存 |
| A08 | 大图操作 | 滑动顺序正确,关闭返回原位置,分享定位到正确照片 |
| A09 | 点赞计数 | 连续点击累计,重复请求不重复加计,断线后能校准 |
| A10 | 实时隔离 | 两个相册同时观看,事件仅送达所属相册 |
| A11 | 嵌入组件 | 网站滚动时悬浮、自动更新、正确跳转,不破坏宿主样式 |
| A12 | 多语言 | 所有入口可切换,主要提示无漏译,内容回退一致 |
| A13 | 长期存储 | 正式照片无自动过期规则,上传端无长期 OSS 密钥 |
| A14 | 浏览统计 | 有效访问累计且展示;跨标签页、重试和窗口内操作不重复计,轮播和后台预览不计 |
| A15 | 连续拍摄 | 拍完自动准备入队,切相册不改变已有任务归属 |
| A16 | 控制与隐藏 | 上传公开独立,批量隐藏后各端停止展示,重新公开不恢复隐藏图 |
| A17 | 负责人交接 | 转移后权限正确,历史上传人和记录不变 |
| A18 | 下载水印 | 开关组合合法,历史重生成可重试,不覆盖原图或破坏旧浏览图 |
| A19 | 准备与历史 | 云端文件和空间异常有状态,跨设备仅恢复服务端历史 |
| A20 | 资源保护 | 旧上传权限不能改变已发布原图,过期图片链接可刷新 |
| A21 | 气泡体验 | 自己事件不重复,后台无积压,减少动画不改变计数 |
| A22 | 展示位 | 后台切换相册不改官网,旧响应不串图,展示位权限正确 |
| A23 | 用量控制 | 告警可触发,组件不可见暂停请求,不自动删除照片 |
| A24 | 部署验证 | 海外观众 API 与实时同部署,国内 worker 独立,区域数据不共用跨境数据库 |
| A25 | 海外直读 | 页面、列表、浏览点赞不逐次请求国内,图片经全球 CDN |
| A26 | 内容同步 | 重复、乱序、中断和补发不复活已隐藏删除照片,不显示缺资源图片 |
| A27 | 统计回传 | 重试不重复累计,国内最终与海外权威计数一致,并展示同步时间 |
| A28 | 全球访问 | 目标地域测试冷缓存新图、原图下载与下线传播,记录实际时延 |
| A29 | 用户接口同步 | 新增更新幂等,稳定 ID 不变;失败不误停用,不覆盖本地密码权限,支持状态与补拉 |
照片量、并发人数和响应时间尚未由业务确定。项目计划给出首轮测试建议,均需通过实测确认,不能解释为已具备容量或生产服务承诺。
## 7 待确认决策
| 编号 | 决策 | 建议 | 最晚确认时点 |
|---|---|---|---|
| D01 | 首发语言 | 简体中文和英文 | 原型定稿 |
| D02 | 后端与 Web 技术栈 | 采用实施文档建议或公司标准 | 技术验证前 |
| D03 | iOS 和 Android 分发方式 | 按公司设备和账号资质决定 | 第一周启动 |
| D04 | 删除回收策略 | 先软删除,不自动永久清理 | 照片管理开发前 |
| D05 | 数量并发与网络目标 | 采用分级压测逐步校准 | 技术验证结束 |
| D06 | 下载水印与原图默认值 | 活动创建时明确设置 | 观众原型定稿 |
| D07 | 主办信息与浏览去重参数 | 浏览统计已必做;确认主办字段及建议的 30 分钟窗口 | 原型定稿 |
| D08 | 用户接口与凭据规则 | 确认稳定 ID、字段、鉴权、周期、停启用语义、初始密码及激活方式 | 用户同步开发前 |
| D09 | 海外服务地域 | 全球 CDN 已确定;确认观众分布、海外服务器地域和测试点 | 部署方案定稿 |
| D10 | 缓存与用量参数 | 资源失效窗口、轮询周期、容量和流量告警阈值 | 对应模块验收前 |
| D11 | 跨境同步参数 | 确认补漏周期、延迟告警、统计回传周期及中断下线策略 | 同步验收前 |
## 8 需求变更与参考
新增需求应记录编号、目的、验收方式及工期影响,再决定进入当前版本或后续版本。相册级公开模式、匿名连续点赞及无修图流程为当前基线,避免实现时回退到早期讨论过的登录点赞或单次点赞方案。
观众布局参考用户提供的截图:宣传图、活动信息、图片瀑布流。喔图仅作为业务流程参考,不构成对其界面素材或完整功能的复制要求。
公开参考资料:
- [喔图产品介绍](https://www.alltuu.com/cooperationPage)
- [喔图上传与照片管理说明](https://faq.alltuu.com/a538/03/f140)
- [喔图上传和发布状态说明](https://faq.alltuu.com/a538/godson/6505)
+217
View File
@@ -0,0 +1,217 @@
# 图片直播项目计划
版本:V1.3
日期:2026-09-17
关联文档:[需求说明书](./01-需求说明书.md) · [技术实施方案](./03-技术实施方案.md) · [优化方案](./04-优化方案.md)
本计划把第一版拆成可交付任务,并优先验证双平台后台上传。完成目标是交付 Flutter App、管理后台、观众网页、网站 JS 组件和支撑服务,跑通活动创建到照片传播的完整流程。
## 1 排期假设
目前没有确认开发人员和上线日期,因此采用相对周次 W1 至 W16,不设具体日历承诺。
用于估算的团队配置为:1 名 Flutter 工程师、1 名后端工程师、1 名 Web 工程师;另配兼职产品设计、测试和运维支持。测试在早期参与,不能只在最后一周介入。人员减少、缺乏原生移动端经验或外部审批延误,都需要重新排期。
增加海外服务及双向业务同步后,原 12 周基线暂增加 2 至 4 周,按 14 至 16 周估算,以 16 周安排为工作基线,W15 至 W16 用于试运行、修复与发布。以下工作量是规划估算,单位为角色人日,不含等待外部审核时间,不代表固定报价。W2 技术验证结束后重估。
## 2 交付清单
| 编号 | 交付物 | 完成标准 |
|---|---|---|
| P01 | 需求和原型 | 角色权限、三端页面、上传和互动状态明确 |
| P02 | 技术验证报告 | Android 和 iOS 真机后台上传方案可行,异常边界有记录 |
| P03 | 后端与管理后台 | 账号、相册、人员、照片及 OSS 处理闭环 |
| P04 | Flutter App | 账号密码登录、相册配置、协作、上传记录和照片管理 |
| P05 | 观众网页 | 多语言活动页、浏览次数、瀑布流、大图、下载分享、点赞气泡 |
| P06 | JS 嵌入组件 | 可复制代码接入、悬浮轮播、自动更新和跳转 |
| P07 | 验证材料 | 权限、弱网、兼容、大相册、实时互动测试报告 |
| P08 | 上线与运维交接 | 国内海外部署配置、数据库迁移、备份恢复、监控和回滚步骤 |
| P09 | 海外同步与全球分发 | 内容推送补漏、统计回传、CDN 与冷缓存地域测试报告 |
## 3 阶段安排
| 阶段 | 周次 | 主要工作 | 退出条件 |
|---|---|---|---|
| S1 需求原型 | W1 | 字段、权限、页面原型、账号分发条件确认 | 需求基线与待确认负责人明确 |
| S2 技术验证 | W1 至 W2 | 原生后台传输、OSS、用户接口、海外网络与 CDN 验证 | 核心风险有实测结果,确定实施方式 |
| S3 基础链路 | W3 至 W6 | 身份、相册、上传、处理、公开列表,基础 App 和后台 | 单张和批量上传自动发布端到端可用 |
| S4 功能建设 | W7 至 W10 | 协作管理、队列恢复、大图、语言、点赞、下载分享 | 主要需求完成并通过模块测试 |
| S5 优化与集成 | W11 至 W14 | 展示位、交接、双向同步、CDN、访问保护与跨地域测试 | 新增优化通过验收,功能冻结 |
| S6 试运行发布 | W15 至 W16 | 模拟或真实活动、故障演练、修复、分发和交接 | 业务验收通过,发布检查和回滚准备完成 |
后台、App 和 Web 可在接口契约稳定后并行。多语言、日志和权限随各模块实施,不在末期集中补做。App 分发资格与域名准备从 W1 启动,外部审核周期另计。
## 4 工作分解
表中主责为岗位而非已指定人员。估算区间涵盖实现与开发自测,联调测试单列。依赖项表示开始或完成前所需条件。
| WBS | 工作包 | 主责 | 估算人日 | 依赖 | 交付证据 |
|---|---|---|---|---|---|
| T01 | 字段权限原型与验收清单 | 产品设计 | 4 至 6 | 无 | 原型、R01 至 R31 对应清单 |
| T02 | 仓库工程环境与构建流水线 | 全端运维 | 3 至 5 | 技术选型 | 三端基础构建、环境说明 |
| T03 | 双平台后台上传与 OSS 验证 | Flutter 后端 | 8 至 12 | 测试 Bucket 路径、真机 | 锁屏弱网过期恢复报告 |
| T04 | 用户接口契约与同步验证 | 后端 测试 | 3 至 5 | 用户接口文档和测试凭据 | 稳定 ID、分页增量、异常和停用语义验证 |
| T05 | 账号会话权限与后台账号管理 | 后端 Web | 6 至 8 | T01 T04 | 登录、停用、重置和越权用例 |
| T06 | 相册分类语言与协作接口 | 后端 | 5 至 7 | T05 | API 契约及权限测试 |
| T07 | OSS 授权处理发布与失败恢复 | 后端 | 7 至 10 | T03 T06 | 幂等上传发布、后台补偿 |
| T08 | App 登录相册配置和协作页面 | Flutter | 7 至 9 | T04 T06 | 双平台可操作页面 |
| T09 | App 上传队列与记录正式实现 | Flutter | 10 至 14 | T03 T07 | 重启恢复、失败重试、账号隔离 |
| T10 | 后台相册照片和用量管理 | Web | 6 至 8 | T06 T07 | 配置、筛选、隐藏删除和用量 |
| T11 | 观众活动页瀑布流和大图 | Web | 7 至 9 | T06 T07 | 大相册分页与滑动定位 |
| T12 | 分享下载与浏览器适配 | Web 后端 | 3 至 5 | T11 | 照片链接、下载开关和微信引导 |
| T13 | 点赞计数与实时气泡 | 后端 Web | 6 至 8 | T07 T11 | 去重、广播隔离和断线恢复 |
| T14 | JS 悬浮组件与配置生成 | Web | 4 至 6 | T06 T11 | 独立网站接入样例 |
| T15 | 全端语言验收与文案补齐 | 全端产品 | 3 至 5 | 各功能 | 语言切换与缺失回退检查 |
| T16 | 全链路测试容量与缺陷修复 | 测试全端 | 10 至 15 | T08 至 T15、T19 至 T24、T26 至 T28、T30 | 测试报告、阻断问题关闭 |
| T17 | 部署备份回滚与监控 | 运维后端 | 4 至 6 | T02 T07 | 部署和恢复演练记录 |
| T18 | 试运行App分发与交接 | 全端测试 | 4 至 6 | T16 T17 T29 | 业务验收和发布记录 |
| T19 | 相册浏览计数与前台展示 | 后端 Web | 2 至 3 | T06 T11 | 幂等计数、窗口去重及展示核对 |
| T20 | 连拍活动控制交接与水印规则增量 | Flutter 后端 Web | 10 至 15 | T06 T08 T10 | A15 至 A18 验收,含历史水印任务 |
| T21 | 准备状态与跨设备记录增量 | Flutter 后端 | 6 至 9 | T03 T09 | A19 验收与锁屏长队列恢复 |
| T22 | 原图不可变与资源访问增量 | 后端 Web 运维 | 5 至 8 | T07 T12 | A20 验收与签名缓存失效测试 |
| T23 | 展示位管理与不可见暂停增量 | 后端 Web | 3 至 5 | T06 T14 | A22 A23 验收,换相册不改站点 |
| T24 | 气泡批次去重与动画偏好增量 | 后端 Web | 3 至 5 | T13 | A21 验收,自己的动画不重复 |
| T25 | 海外部署与全球 CDN 可行性验证 | 后端 运维 测试 | 3 至 5 | T02、海外测试资源 | 目标地区冷缓存和服务直读报告 |
| T26 | 国内到海外元数据同步与补漏 | 后端 Web 测试 | 8 至 12 | T06 T07 T25 | 版本去重、隐藏删除、补漏与同步状态 |
| T27 | 海外计数权威与统计回传 | 后端 测试 运维 | 4 至 6 | T13 T19 T26 | 重复回传不加计、国内最终一致 |
| T28 | 全球 CDN 接入与访问策略增量 | 运维 后端 Web | 3 至 5 | T22 T25 | 域名鉴权、缓存失效、冷缓存测量 |
| T29 | 跨地域故障与上线验收 | 测试 后端 Web 运维 | 4 至 6 | T16 T26 T27 T28 | 中断恢复、下线传播、地域时延与告警 |
| T30 | 用户定期同步与后台同步管理 | 后端 Web 运维 测试 | 5 至 8 | T04 T05 | 幂等更新、水位、定时补拉、日志与失败告警 |
合计约 156 至 231 角色人日。V1.3 用 T04 用户接口验证替代飞书验证,并增加 T30 同步实现,较 V1.2 增加 5 至 9 人日,仍按 16 周基线安排,接口契约未提供前不能视为固定工期。T04 在 W1 至 W2,T30 在 W3 至 W6 完成并纳入核心账号验收。V1.2 历史估算为 151 至 222 人日。V1.1 的 129 至 188 人日保留;T25 至 T29 是海外架构增量,增加 22 至 34 人日,不重复计算基础计数和 OSS 上传。T25 在 W1 至 W2 验证,T26 从 W3 起随核心接口推进,T27 和 T28 在功能期联调,T29 于 W13 至 W14 完成跨地域验收。T19 调整为 W7 至 W8。同步异常不得留到上线后补做。
## 4A 岗位工作量与资源检查
以下按各任务估算上限分配,单位为人日;这是估算分配,不是已落实人员。每行合计等于上方任务上限,避免多人主责被误认为每个人都要投入整项工作量。
| 任务 | Flutter | 后端 | Web | 产品设计 | 测试 | 运维 |
|---|---|---|---|---|---|---|
| T01 | 0 | 0 | 0 | 6 | 0 | 0 |
| T02 | 1 | 1 | 1 | 0 | 0 | 2 |
| T03 | 8 | 3 | 0 | 0 | 1 | 0 |
| T04 | 0 | 3 | 0 | 0 | 2 | 0 |
| T05 | 0 | 4 | 3 | 0 | 1 | 0 |
| T06 | 0 | 6 | 0 | 0 | 1 | 0 |
| T07 | 0 | 8 | 0 | 0 | 1 | 1 |
| T08 | 8 | 0 | 0 | 1 | 0 | 0 |
| T09 | 12 | 0 | 0 | 0 | 2 | 0 |
| T10 | 0 | 0 | 7 | 0 | 1 | 0 |
| T11 | 0 | 0 | 8 | 1 | 0 | 0 |
| T12 | 0 | 1 | 3 | 0 | 1 | 0 |
| T13 | 0 | 4 | 3 | 0 | 1 | 0 |
| T14 | 0 | 0 | 5 | 0 | 1 | 0 |
| T15 | 1 | 0 | 1 | 2 | 1 | 0 |
| T16 | 2 | 2 | 2 | 0 | 9 | 0 |
| T17 | 0 | 1 | 0 | 0 | 1 | 4 |
| T18 | 1 | 0 | 1 | 0 | 3 | 1 |
| T19 | 0 | 1 | 1 | 0 | 1 | 0 |
| T20 | 4 | 4 | 4 | 1 | 2 | 0 |
| T21 | 6 | 1 | 0 | 0 | 2 | 0 |
| T22 | 0 | 4 | 1 | 0 | 1 | 2 |
| T23 | 0 | 1 | 3 | 0 | 1 | 0 |
| T24 | 0 | 1 | 3 | 0 | 1 | 0 |
| T25 | 0 | 2 | 0 | 0 | 1 | 2 |
| T26 | 0 | 8 | 1 | 0 | 2 | 1 |
| T27 | 0 | 4 | 0 | 0 | 1 | 1 |
| T28 | 0 | 1 | 1 | 0 | 1 | 2 |
| T29 | 0 | 1 | 1 | 0 | 3 | 1 |
| T30 | 0 | 4 | 2 | 0 | 1 | 1 |
| 合计 | 43 | 65 | 51 | 11 | 43 | 18 |
16 周按每周 5 个工作日约 80 人日计算,三名开发各自均需保留会议、评审、外部等待和非项目工作的空间。后端总量最高,需优先保障双向同步;Web 避免同时推进后台、观众页与组件三条主线;兼职测试也需要在集成期集中投入,不能假设仅偶尔验收。
排期容量检查还需按周落实到人:W1 至 W2 优先验证,W3 至 W6 核心链路,W7 至 W10 功能,W11 至 W14 优化集成,W15 至 W16 试运行发布。总量有余量并不证明所有依赖可在 14 周内完成;W2 后根据实际速度发布细化到人的排期。
## 5 关键路径与里程碑
关键路径为:后台上传验证 → 持久化上传队列 → OSS 核验和自动发布 → 海外可见与同步恢复验证 → 双平台恢复验证 → 试运行 → 发布。若后台上传验证未通过,不应把功能标记为完成后继续承诺原上线日期。
| 里程碑 | 建议时间 | 验收内容 | 验收岗位 |
|---|---|---|---|
| M1 需求基线 | W1 结束 | 已确认范围、建议项和未决策项明确 | 业务负责人、技术负责人 |
| M2 技术可行 | W2 结束 | 后台上传、凭证过期、用户接口同步验证通过 | 技术负责人、测试 |
| M3 核心链路 | W6 结束 | 国内上传后海外观众页自动看到照片 | 业务负责人、测试 |
| M4 功能齐备 | W14 结束 | App、后台、观众、组件和多语言可用 | 产品、测试 |
| M5 试运行通过 | W15 结束 | 多人现场操作、弱网及权限行为符合要求 | 业务负责人 |
| M6 发布交接 | W16 结束 | 生产部署、App 分发、监控、回滚和交接完成 | 项目负责人、运维 |
## 6 首轮容量验证建议
业务尚未提供容量,以下仅是待评审的测试阶梯,不是生产承诺。采用固定图片大小、清晰度、网络和设备条件记录结果。
| 层级 | 相册照片数 | 同时上传人数 | 在线观众 | 目的 |
|---|---|---|---|---|
| L1 基础 | 1,000 | 3 | 100 | 验证完整流程和初步稳定性 |
| L2 目标候选 | 10,000 | 10 | 1,000 | 评估首版资源与优化需求 |
| L3 扩展探测 | 50,000 | 20 | 5,000 | 查找瓶颈,不作为默认上线门槛 |
照片数量测试可使用测试数据;实际上传吞吐和缩略图处理必须另用真实尺寸图片验证。在线数和点赞频率分别施压,避免只验证连接数而忽略消息负载。
建议候选指标:普通列表 API P95 小于 500ms;约定网络下活动页首批照片 P95 小于 3 秒;传输完成到发布 P95 小于 10 秒;点赞确认及广播 P95 小于 2 秒。W2 根据样本、部署规格和成本重新确认,上传耗时单独统计,不包含在图片处理时延内。
## 7 测试计划
### 7.1 开发阶段
核心权限、上传幂等、点赞幂等、排序分页和语言回退编写服务端测试。每个功能提交时验证对应验收编号,不编写只重复页面实现的低价值测试。
### 7.2 集成阶段
- Android 和 iOS 真机:前台、后台、锁屏、系统中止、强制退出、重启、低电量模式。
- 网络:离线入队、上传中断网、Wi-Fi 与移动网络切换、临时授权过期。
- 数据:同名不同文件、重复任务、HEIC、超限和损坏文件、处理失败、回调重复。
- 权限:移除协作者、账号停用、相册下线、直接调用他人照片 API。
- 观众:微信内浏览器、Android Chrome、iOS Safari 和桌面主流浏览器。
- 实时:同相册多窗口、不同相册隔离、密集点赞、断线重连、隐藏照片后旧链接。
- 嵌入:不同 CSS 框架、窄屏、组件多次加载、无图、宿主 CSP 限制。
- 语言:中英切换、长英文排版、时区、未填写翻译和不同宣传图。
- 优化:连拍切相册、准备失败、负责人转移、上传公开独立、历史水印失败、旧授权覆盖、跨标签浏览、自己气泡重复、展示位切换和用量告警。
用户同步专项:重复记录、改名改账号、分页中断、空响应、超时重试、并发补拉、旧版本覆盖、显式停用、本地锁定、密码权限不被覆盖及水位恢复。
### 7.3 发布门槛
需求文档 A01 至 A29 完成映射。浏览统计需验证首次访问、短时刷新、单图入口、跨窗口访问、重复请求和嵌入轮播排除。无数据丢失、越权、无法恢复上传等阻断缺陷;其他遗留问题写明影响和处理时间。完成一次数据库恢复与发布回滚演练,并形成容量实测报告。
跨境专项:断开内容同步、乱序重放、重复计数回传、重建海外副本、隐藏删除优先传递、同步积压告警;从确认的目标地域分别测新图冷缓存和缓存命中,不能只测国内网络。同步延迟与图片传输时延分开报告。
## 8 资源与外部依赖
| 资源 | 用途 | 提供时间 |
|---|---|---|
| 现有 OSS 地域与项目路径 | 临时授权、上传、资源分发 | W1 |
| 用户接口文档和测试凭据 | 字段、唯一 ID、鉴权、分页、周期、停用语义 | W1 |
| 域名和测试部署环境 | API、观众页、组件与回调 | W1 至 W2 |
| Android 和 iOS 真机 | 后台上传和浏览器验收 | W1 |
| macOS 构建环境与 Apple 开发资质 | iOS 编译签名与分发 | W1 启动 |
| 活动样例与中英文案 | 原型和真实试运行 | W2 |
| 海外服务器与测试节点 | 海外 API、数据库、同步接收和真实地域测试 | W1 |
| 全球 CDN 域名和权限 | OSS 回源、观众鉴权、缓存失效与按需预热 | W1 至 W2 |
| 开发和测试负责人 | 确认实际排期与缺陷责任 | 启动前 |
密钥通过安全配置渠道提供,不写入代码、文档或聊天记录。长期云资源费用另行测算,费用包括 OSS 容量、请求和出网流量、CDN、图片处理、数据库、服务实例与日志。
## 9 风险及处置
| 风险 | 影响 | 处置 |
|---|---|---|
| 后台传输与系统限制不匹配 | 上传中断、延误 | W1 至 W2 原生验证,保留可恢复队列 |
| 图片量和流量未知 | 性能及成本失控 | 分级压测、用量统计、容量告警 |
| OSS 凭证过期 | 长任务失败 | 验证刷新与后台恢复,不无限延长授权 |
| 多人权限变更 | 越权发布 | 完成上传时再次校验权限 |
| 匿名连续点赞被滥用 | 计数及广播负载 | 基础限速、幂等、动画聚合 |
| App 分发或用户接口联调延迟 | 功能完成但无法使用 | W1 准备分发与接口样本,外部等待单独跟踪 |
| 用户同步异常或语义不明 | 重复账号或误停用 | 稳定 ID、完整快照判断、本地权限隔离及审计 |
| 海外地区与冷缓存 | CDN 回源及 API 远距离延迟 | 确认用户地区,分别测试冷缓存与命中 |
| 跨境同步中断或乱序 | 新图延迟、旧状态覆盖 | 持久事件、版本校验、补漏及墓碑 |
| 统计重复回传 | 国内计数虚增 | 海外权威累计快照,版本替换,不重复相加 |
## 10 项目跟踪方式
每个任务记录负责人、需求编号、依赖、预计完成周、状态、验收证据和风险。状态使用待开始、进行中、待联调、待验收、完成、阻塞。
每周检查里程碑和关键路径,每阶段演示真实完整流程。新增需求需先评估涉及页面、接口、存储和测试的影响,明确替换现有范围、延长排期或进入后续版本。
项目启动先完成 D01 至 D03、D08 的决策,并落实资源负责人;W2 结束后发布修订后的工期和容量基线。
+484
View File
@@ -0,0 +1,484 @@
# 图片直播项目技术实施方案
版本:V1.3
日期:2026-09-17
关联文档:[需求说明书](./01-需求说明书.md) · [项目计划](./02-项目计划.md) · [优化方案](./04-优化方案.md)
本方案用于指导第一版开发、联调和部署。Flutter、Android 与 iOS、第一期账号密码登录与用户接口定期拉取、现有国内 OSS、全球 CDN、国内管理与海外观众服务为已确认约束;具体服务端框架、语言数量、参数和资源规格均为建议,需在技术验证后冻结。
## 1 架构与选型
第一版按地域部署国内管理 API 与海外观众 API,两地使用独立数据库与缓存,不让海外请求直接查询国内数据库。海外 API 内部署 WebSocket 模块,国内图片处理 worker 独立。代码可复用公共模块,但域名、权限、凭据和路由按角色隔离。
| 层 | 建议实现 | 选择依据 |
|---|---|---|
| App | Flutter 界面与业务,平台原生后台上传桥接 | 双平台统一业务,后台任务由系统承接 |
| 管理后台 | Vue 3 与 TypeScript | 表单、列表和管理操作 |
| 观众端 | Vue 3 与 TypeScript,独立轻量入口 | 图片浏览、路由定位和互动 |
| 嵌入组件 | 小型 TypeScript 加载器与 iframe | 隔离宿主样式,减少依赖冲突 |
| API | NestJS 与 TypeScript | 统一模块边界及接口契约 |
| 数据库 | PostgreSQL | 权限、关联数据、事务和可靠计数 |
| 缓存与任务 | Redis,持久任务队列可采用 BullMQ | 分发、限速和异步处理;数据库保留任务源记录 |
| 文件 | 现有阿里云 OSS | App 直传、原图长期存放 |
| 实时 | WebSocket,故障时降级轮询 | 新图通知、点赞气泡和计数同步 |
| 分发 | OSS 全球 CDN,已确定 | 图片直接由 CDN 分发,验证冷缓存回源 |
以上为建议选型,不表示已安装依赖或完成实现。公司已有技术栈可替换 API 和 Web 框架,但应保持下面的数据与状态约束。版本号在工程初始化时选择并锁定,不在当前文档写死未经验证的最新版。
```mermaid
flowchart TB
APP["工作人员 App"] --> CN["国内管理 API"]
ADM["国内管理后台"] --> CN
USRC["用户信息接口"] -->|国内定时拉取| USYNC["用户资料同步任务"]
USYNC --> CN
APP -->|临时授权直传| OSS["国内 OSS 原图与浏览资源"]
CN --> CDB[("国内主数据库")]
CN --> WORKER["国内图片处理 worker"]
WORKER --> OSS
WORKER --> CDB
CDB --> OUT["持久事件与定时补漏"]
OUT -->|版本化元数据| OVER["海外观众 API 与实时模块"]
OVER --> ODB[("海外展示副本与互动数据库")]
ODB -->|统计快照回传| CN
USER["海外观众"] -->|页面 查询 点赞 浏览| OVER
SLOT["官网 JS 展示位"] -->|轮询| OVER
OSS -->|回源| CDN["全球 CDN"]
USER -->|图片请求| CDN
SLOT -->|轮播图片| CDN
```
## 2 工程结构
建议使用单仓库,便于接口契约和多语言资源共同维护;此处只是规划,不自动创建业务工程。
```text
photo/
docs/ 需求、计划、实施和后续决策记录
apps/mobile/ Flutter App 与 Android iOS 原生桥接
apps/admin/ 管理后台
apps/viewer/ 观众页与嵌入轮播页面
packages/embed-loader/ 对外 JS 加载器
packages/contracts/ 接口定义与事件类型
packages/locales/ Web 公共翻译和词条约定
services/api/ 国内管理与海外观众两个部署入口
services/worker/ 图片处理与补偿任务
services/api/realtime/ 海外观众 API 内的 WebSocket 模块
services/sync/ 内容同步接收及统计回传任务
infra/ 部署配置和迁移脚本
```
环境划分为本地、测试和生产。测试与生产至少使用独立数据库、Redis 命名空间、OSS 对象前缀和凭证权限。不得让测试清理脚本操作现有业务照片。
## 2A 国内海外数据与同步
### 数据归属
| 数据 | 权威写入地点 | 另一端职责 |
|---|---|---|
| 工作人员本地账号、外部用户映射和管理权限 | 国内 | 不把密码、会话或工作人员令牌同步海外 |
| 相册、翻译、分类、照片可见性、下载水印设置、展示位 | 国内 | 海外保存公开服务需要的展示副本 |
| 原图与浏览资源 | 国内 OSS | 全球 CDN 分发,海外业务服务器不复制文件 |
| 点赞和浏览计数、批次去重 | 海外 | 国内接收版本化累计快照,仅作统计展示 |
所有观众,包括从国内打开公开链接的观众,第一版统一写海外互动服务。国内管理员不能同时对这些权威计数加值。内容同步事件不能覆盖海外点赞数和浏览量。浏览和互动去重明细默认留在海外,国内仅收所需累计统计。
### 国内到海外内容同步
国内业务事务同时写 outbox,发送器推送签名鉴权的内部同步接口;海外事务内登记 inbox eventId、检查实体版本并更新展示副本,持久成功后应答。网络重试复用 eventId,不采用跨境数据库事务。
事件包含 schemaVersion、entityType、entityId、entityVersion、eventId 和 payload。每个实体只应用更新版本,旧版本及重复消息忽略;先到的照片缺相册或分类时暂存并补取依赖,不能显示残缺关系。首次全量快照携带水位,再回放其后的增量;定时按水位及清单对账,补发缺失数据。
海外必须保存隐藏删除墓碑及版本,不能因旧新增消息或全量补漏复活图片。相册下线和照片隐藏走高优先级队列;相册公开状态与照片可见性同时校验,旧的照片事件不能重新打开下线相册。展示位切换同样使用版本,避免返回旧活动。
同步元数据为资源键、衍生版本、尺寸和 CDN 路径,不同步容易过期的签名 URL。图片在 OSS 处理完成才发可用事件;海外收到后确认必要浏览资源可访问,再设为可见。一次海外探测不等于所有 CDN 节点已经缓存。
App 和后台分别显示国内处理状态与海外同步状态,包括同步中、海外可见、失败以及最近时间,不能把国内 ready 直接当作海外可见。具体目标延迟、重试次数和补漏周期在 T25 验证后冻结。
### 海外到国内统计回传
海外点赞事务、浏览去重和权威累计全部在当地完成。回传采用按照片或相册的累计快照,带独立单调 statVersion、来源及确认时间;国内仅以更高版本替换统计副本,绝不把相同累计值反复相加。重试、乱序及国内暂时不可用不会改变海外权威数。
国内后台标明统计同步时间,内容配置更新不得清空计数。海外恢复备份后,先核对已确认水位与统计副本,修复再恢复写入;不能直接切换权威地域产生双写。精确灾难恢复指标和快照保留时间在部署评审确认。
### 同步接口与运行边界
内部接口建议 POST /internal/sync/content-batches、POST /internal/sync/stat-snapshots,使用服务身份认证、请求签名或双向 TLS、时间戳与重放约束;接口不对匿名观众开放。发送成功不等于消费成功,必须以接收端持久化应答更新水位。持久记录同步失败和可重放批次,不静默丢弃。
跨境链路中断时海外继续提供已同步照片与当地互动,国内可继续上传并排队;国内显示同步延迟。无法保证此时的下线指令立刻到达海外,需提供运维可达的海外紧急停用入口。该入口只允许更严格的屏蔽,不修改国内业务权威数据;解除需明确操作,普通同步事件不得自动解除。
国内管理、海外公开页面和 API 使用不同域名配置。观众浏览、下载签名、点赞和气泡不逐次访问国内;海外以受限配置验证公开副本并签发 CDN 访问凭证,不持有 OSS 上传或管理权限。周期性探测可访问源站,不应变成每次浏览回源国内 API。
## 3 账号与权限
### 3.1 内部身份与第一期登录
管理后台与 App 共用内部 users.id,第一期仅账号密码登录,不提供飞书入口、OAuth 回调、绑定流程或相应密钥配置。外部用户接口用于拉取资料,不代表外部认证接口;是否使用外部密码校验尚未提出,实施基线为本地密码验证。
资料同步使用 source_system 与 external_user_id 组合唯一映射到 users.id,姓名、手机号、邮箱或账号变更不新建人员;登录账号冲突进入人工处理,不根据姓名自动合并。第一期不开放自行注册。
密码采用成熟密码哈希,登录会话可撤销。同步不得覆盖 password_hash、管理员角色、相册成员、个人语言偏好或管理员主动锁定状态。初始管理员受控初始化,导入用户默认待激活,管理员完成初始凭据及授权;自动激活、初始密码交付与首次修改在接口评审时确认。
### 3.1A 用户接口定期拉取
用户接口地址、鉴权方式、字段和速率约束待提供,不能假设现有接口路径或数据格式。建议逻辑字段为 externalUserId、loginAccount、displayName、department、sourceStatus、updatedAt 或 sourceVersion;手机号与邮箱仅按实际需要映射。先冻结字段归属和停启用语义,再开发适配器。
流程:国内定时调度或管理员手动触发 → 获得任务锁 → 读取上次成功水位 → 拉取分页数据 → 校验暂存 → 按外部 ID 幂等更新 → 提交批次与水位 → 记录结果与告警。支持增量时定期全量对账;只支持全量时逐页获取完整快照再做差异处理。
计划任务与手动补拉共用互斥锁,避免并行执行覆盖水位。超时、限速和失败按退避重试,保持原成功水位;旧 sourceVersion 不覆盖新数据。没有源版本时禁止并发批次,以已完成快照序号提交。部分无效记录先保存错误及可重试原始标识,再决定是否推进分页水位,不能静默漏人。
同步任务保存成功、部分失败或失败状态,记录开始结束、上次成功时间、读取新增更新停用及失败数量。分页不完整、空响应、接口故障不执行缺失用户停用;只有明确的停用字段或接口契约保证的完整删除快照,才进入停用处理。第一期默认不因缺失自动物理删除。
源状态、本地手动锁定和凭据激活分开保存。有效状态要求源有效、无本地锁定且凭据已激活。显式源停用后撤销会话、阻止续签及发布,保留相册和上传历史;上游恢复不能解除本地锁定,重新激活策略待确认。接口中断时已有本地有效账号可继续登录,但后台显示资料可能过期及同步告警。
上游凭据仅存国内服务端,不发送 App 或海外,不写入日志。后台仅有授权管理员可触发同步和查看脱敏错误。此任务与国内到海外展示元数据同步是两条不同链路,用户密码和用户资料明细不随展示副本推送海外。
### 3.2 资源权限
每个写操作以用户、相册角色、照片上传人共同判断。照片更新应查询 photo.album_id 和 uploader_id,不接受客户端声明的归属作为授权依据。
上传初始化、续签和完成确认均检查权限。协作者被移除后,已有对象可上传但不得公开;短期权限自然过期,后台登记孤立对象供人工或策略清理。
公开 API 只返回公开相册中的已发布照片,不泄露工作人员信息、原始存储键或内部任务错误堆栈。匿名访问标识只用于限速与幂等辅助,不代表真实观众身份。
转移相册负责人通过管理员事务操作:验证目标有效账号,更新 owner_id 和成员角色,按明确选择保留或移除原负责人,记录审计;不修改 photos.uploader_id。账号停用只撤销权限,不删除历史图片。
## 4 数据模型
建议所有主键使用全局唯一 ID,时间存 UTC,活动额外保存 IANA 时区。大整数计数通过明确接口类型传输,避免前端精度隐患。
| 表 | 主要字段 | 约束与用途 |
|---|---|---|
| users | id account password_hash source_status local_locked credential_status role locale | account 唯一,源状态与本地权限隔离 |
| external_user_mappings | user_id source_system external_user_id source_version | 外部用户组合唯一,稳定映射内部 ID |
| user_sync_runs | id trigger status cursor counts started_at finished_at | 定时及手动同步批次记录 |
| user_sync_errors | run_id external_user_id error_code retry_state | 失败可见并可补偿 |
| user_sync_state | source_system success_cursor snapshot_version last_success_at | 只更新已确认成功水位 |
| sessions | user_id token_hash expires_at revoked_at | 支持注销与撤销 |
| albums | id public_token owner_id start_at end_at timezone status default_locale upload_enabled public_enabled download_enabled original_download_enabled watermark_version | public_token 唯一,上传公开下载开关独立 |
| album_translations | album_id locale title description venue address | album_id 与 locale 唯一 |
| album_members | album_id user_id role status | 成员组合唯一 |
| categories | id album_id sort_order | 归属于相册 |
| category_translations | category_id locale name | 支持名称回退 |
| album_assets | album_id kind locale object_key | 宣传图、封面和 Logo |
| photos | id album_id uploader_id category_id object_key bytes width height shot_at uploaded_at published_at processing_state visibility like_count | 使用独立图片 ID,原图不覆盖 |
| photo_variants | photo_id variant object_key bytes version | 缩略图、浏览图和水印图 |
| upload_sessions | id user_id album_id client_task_id object_key expected_bytes multipart_id state expires_at | 用户与 client_task_id 唯一 |
| photo_jobs | id photo_id type state attempt next_retry_at | 持久任务源记录 |
| like_batches | photo_id client_session_id batch_id delta created_at | 会话批次唯一,防网络重试重复累计 |
| outbox_events | id album_id type payload created_at delivered_at | 与业务事务共同提交 |
| audit_logs | actor_id action resource_id metadata created_at | 管理、权限及删除操作可追踪 |
| embed_slots | id public_token album_id enabled config_version settings | 网站绑定展示位,后台切换相册 |
| embed_slot_members | slot_id user_id permission | 管理员授予展示位管理权 |
| watermark_jobs | id album_id config_version state progress | 历史衍生图明确发起重生成 |
| album_stats | album_id view_count updated_at | 相册累计浏览次数,数据库持久化 |
| album_visit_sessions | album_id visitor_id last_counted_at | 相册与持久浏览器标识唯一,用于滚动窗口判断 |
| album_visit_requests | album_id visitor_id request_id counted created_at | 请求唯一,重试返回原结果 |
点赞批次增长快,应按时间分区或设置去重窗口。窗口长度属于部署参数;过期批次请求应拒绝或校准,不能在删除去重记录后允许旧批次重新计数。
主要索引:公开列表按相册、可见性、发布时间、ID;照片管理按相册、上传人、分类、上传时间;热门按相册、可见性、点赞数、ID;任务按状态与下次重试时间。
## 5 OSS 上传与图片发布
### 5.1 对象组织
建议在现有 Bucket 下创建独立前缀,实际前缀在部署时确认:
```text
photo-live/{env}/staging/{uploadId}/{photoId}.{ext}
photo-live/{env}/albums/{albumId}/originals/{photoId}.{ext}
photo-live/{env}/albums/{albumId}/variants/{photoId}/{version}/{variant}.{ext}
photo-live/{env}/albums/{albumId}/assets/{assetId}.{ext}
```
文件名不作为对象唯一标识。相同文件名的两张照片使用不同 photoId。重试同一任务继续使用相同上传会话,不能重新创建照片。内容哈希可用于提示重复,但不在未确认规则前静默合并不同人员或不同分类的照片。
### 5.2 授权与完成核验
1. App 提交相册、分类、文件大小、类型和 clientTaskId。
2. API 校验身份、权限和限额,预创建上传会话及照片 ID。
3. 返回 staging 对象范围的临时 STS 凭证或签名上传请求;客户端不得写入 originals 和 variants 路径,长期密钥只在服务端使用。
4. App 通过平台后台任务传输。普通照片可以单文件上传,失败重新传输该文件;分片恢复作为大文件方案验证,不把不同文件的队列恢复误称为字节级断点续传。
5. App 完成通知或 OSS 通知到达服务端,两种通知均可重复,处理必须幂等。
6. API 核验对象、字节数和可用的完整性信息,不单独依赖客户端“成功”字段;分片 ETag 不能直接当作文件 MD5。
7. 固定经核验的源版本或使用条件复制,将对象转入客户端不可写的 originals 路径,再以事务写入处理任务与事件源记录。失败重试复用相同正式目标,核验已经存在的文件,不覆盖。
授权过期先更新授权再重试,禁止把失败标为成功。iOS 长时间后台下如何获取新授权是技术验证重点,不假设 Dart 进程永远能运行。
### 5.3 原图保护与重试
staging 上传请求采用可验证的禁止覆盖约束;签名请求固定必要参数,或使用可验证的权限策略,不能仅依赖客户端自觉添加请求头。普通上传与分片完成分别验证约束。遇到对象已存在时比较会话期望大小和完整性信息,再确认已有结果,不直接重写。
正式原图只允许服务端受控创建。复制过程必须固定源版本,或使用源条件校验并在目标侧复核,避免核验与复制之间源文件变化。服务端写入成功而数据库事务失败时,补偿核验同一正式对象并重试入库,不生成多张照片。
### 5.4 图片处理
处理进程验证真实图片类型、解码大小、方向和格式,生成不同尺寸浏览图。JPEG、PNG 为基础输入;HEIC 解码转码须用真实 iPhone 样本验证。原图长期保留,衍生版本不覆盖原图。
浏览图建议去除定位等不必要 EXIF 信息。原图下载仍可能包含原始元数据,下载策略在产品配置中明确。
处理成功后以事务切换公开状态并产生 photo.published 事件。重试处理使用确定的版本键,避免产生多套无记录资源。失败采用有限自动重试、错误归类和人工重试入口。
### 5.5 状态分离
```text
本地任务
preparing → queued → uploading → uploaded → confirming → completed
└ preparation_failed → preparing
└ waiting_network / retryable_failed / cancelled
服务端处理
pending_upload → uploaded → processing → ready
└ processing_failed → processing
对外可见性
unpublished → published ↔ hidden → deleted
```
本地 completed 表示传输和确认完成;页面另外显示服务端处理中或已公开状态。deleted 是软删除状态,物理删除走独立流程。
### 5.6 补偿与长期保存
定期扫描确认超时会话、已入库但未入队任务、处理超时任务以及未发送事件。重试带退避和次数限制,最终失败进入运维可见列表。
现有 OSS 生命周期不得自动清理正式原图。分片残留、临时缓存和正式文件分路径处理;任何生命周期变更应先确认影响范围。对象存储持久性不替代误删除恢复策略,版本控制或备份及成本需要部署评审。
## 6 Flutter 后台上传
### 6.1 共同设计
Flutter 负责界面、权限交互和任务展示;原生层负责平台传输。使用本地数据库保存任务 ID、账号、相册、源文件位置、系统任务标识、重试信息及服务端会话。
连续拍摄每成功生成一张源文件就创建 preparing 任务,目标相册与分类在任务创建时固定;切换相册仅影响新任务。
每个文件先完成云端源文件获取并复制到受管理目录,确认空间和读权限后入队。准备失败单独记录,不伪装成上传失败;准备阶段不能保证在后台无限执行。账号切换时隔离队列;任务源文件仅在成功确认或明确取消后按策略清理。批量并发设为可配置小值,技术验证后确定,不能为上百张图片同时打开传输。
服务端返回已建立上传会话的历史,设备本地保留未准备和未上传队列。另一设备可查看服务端历史,但不能恢复原设备未传文件;接口返回 sourceDeviceId 或等效来源信息避免误导。
### 6.2 iOS
采用原生 background URLSession 文件上传,通过 Flutter 插件或自建平台桥接维护任务映射。后台文件传输由系统调度,不能用 Dart Timer 或常驻进程模拟。
后台上传使用实际文件;传输完成后持久记录原生回调,在系统允许的执行窗口内完成确认,必要时由服务端对象通知与补偿推动发布。重新启动时重连系统会话并校准本地任务。
用户强制退出后系统不会自动重新启动应用承接该类事件,需在下次启动恢复。低电量、网络调度等可能延迟执行,UI 应明确显示等待而非承诺固定后台完成时间。[Apple 后台传输说明](https://developer.apple.com/documentation/foundation/downloading-files-in-the-background)、[后台会话配置](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/background(withidentifier:))。
### 6.3 Android
按目标 Android 版本采用适用的用户发起数据传输任务;兼容路径结合系统任务调度和符合要求的前台服务通知。具体选择以目标 SDK、设备版本和原生验证结果为准。普通可延迟工作与用户正在等待的批量上传应区别处理。
需验证主流厂商后台管理、通知权限、网络切换和用户停止任务的行为。WorkManager 等调度机制本身不构成无限连续上传保证。[Android 数据传输选型](https://developer.android.com/develop/background-work/background-tasks/data-transfer-options)、[用户发起的数据传输任务](https://developer.android.com/develop/background-work/background-tasks/uidt)。
### 6.4 技术验证用例
批量 100 张真实照片作为起始样本,覆盖前台切后台、锁屏至少 15 分钟、网络中断与恢复、凭证过期、强制退出后重开、源文件缺失和存储不足。数字为验证建议,记录设备、系统、文件大小、成功数、恢复行为和重复记录数。
## 7 观众列表与全屏看图
相册关闭上传后拒绝新任务和续签,尚未完成核验的上传等待重新开放或管理员处理;已经核验的处理任务继续。处理 ready 与相册是否公开分离,公开查询要求 publicEnabled 且照片为 published,重新公开不恢复 hidden 或 deleted。
公开链接采用不可枚举的 publicToken,但不作为秘密身份凭证;持有链接者均可访问。所有公开接口仍检查相册在线状态和照片可见性。
使用游标分页,最新列表按 published_at 与 id 稳定排序。每页建议 30 至 60 张,最终以手机内存和网络测试调整。缩略图携带宽高以预留位置,减少加载跳动。
热门列表的点赞数会变化,建议使用带版本的短期排序快照。大图打开后保持已建立的浏览序列,补取相邻页面时沿用同一快照;主动刷新才切换新排序。
大图按需加载高清版本,只预取有限相邻图片;关闭时保留滚动锚点。单图分享路径建议使用相册 token 加 photoId,服务端验证两者归属。隐藏或删除照片的旧链接返回本地化不可用提示。
下载接口检查相册下载配置,返回短期有效的原图或允许版本链接。私有 OSS 的签名 URL 是短期访问凭证,不能写入永久分享链接。相册下线停止新授权,已签发链接在有效期内可能仍可访问,应在缓存和签名策略中记录这一边界。
### 7.1 相册浏览次数
浏览次数为已确认必做项。建议采用同一浏览器匿名标识同一相册 30 分钟内只累计一次的规则,窗口参数需业务确认,不将结果描述为去重人数。
观众页成功加载公开相册且文档可见后,提交独立访问上报请求;该接口不由照片列表读取、轮播、实时重连或图片加载自动触发。单图链接通过同一相册入口逻辑上报。后台预览使用独立预览路由,不调用计数接口。
服务端事务内先对 requestId 去重,再锁定或原子创建 album_visit_sessions 行,按服务端时间判断距离上次计数是否超过窗口。允许计数时原子增加 album_stats.view_count 并更新 last_counted_at;重复或窗口内请求仅返回最新总数。并发两个首次请求必须依靠唯一约束与事务重试保证只加一次。
返回 viewCount 和 counted 标志,观众页用权威值更新显示。GET 相册信息也返回 viewCount,便于首屏展示;失败重试复用 requestId。长停留不通过定时器增加次数。
会话与请求去重数据按确认的保留策略清理,旧请求过期后拒绝重放;累计总数长期保留。浏览标识使用同源随机持久浏览器标识,跨标签页共享;写入采用跨标签协调并读回一致值,无法持久存储时降级为当前页面标识,不要求登录,不使用高精度设备指纹。无法彻底识别机器人或清除标识后的同一人,因此仅提供基础限速并明确统计口径。
### 7.2 下载与水印版本
下载需要 downloadEnabled,原图还要求 originalDownloadEnabled。接口在签发时检查两级权限;禁止下载是产品控制,不能阻止保存已经展示的浏览图片。
水印配置每次修改生成新版本,默认只作用新照片。历史重生成由显式任务执行,绑定配置版本,逐张生成后原子切换 variants 引用,失败保留旧图。原图永远不被水印任务覆盖,旧衍生图清理独立执行。
### 7.3 全球 CDN 与访问失效
全球 CDN 为必做项,分别配置私有 OSS 回源授权与观众 URL 鉴权,并验证缓存键不会因去掉鉴权参数而泄露未授权资源。短期链接过期时客户端重新请求允许版本,禁止永久保存签名 URL。
隐藏或相册下线停止签发、关闭公开订阅并通知在线客户端清除对应内容;组件轮询下次更新时收敛。已有缓存和签发 URL 在既定窗口内失效,窗口需测试确认,不能承诺瞬时撤回所有资源。已有下载不在可撤回范围内。
## 8 点赞与实时广播
### 8.1 匿名连续点赞
客户端为浏览会话生成随机 sessionId,每一组待提交点击生成 batchId,提交 photoId、delta 和批次 ID。delta 有合理上限,服务端检查照片公开状态及基础频率限制,但不限制每人只能点赞一次。
服务端在同一事务内:插入唯一批次 → 原子增加 photos.like_count → 插入 outbox 事件。批次冲突时返回已确认结果,不重复增加。页面本地动画即时响应,最终以服务端确认的计数为准,失败时校准暂存增量。
数据库是累计计数的权威来源,不能仅将点赞数留在易失缓存。客户端提供的 delta 不能证明真实点击,只能通过上限与速率约束滥用;第一版计数是互动指标,不用于奖项或结算。
### 8.2 事件契约
```json
{
"eventId": "unique-event-id",
"type": "photo.likes.updated",
"albumId": "album-id",
"photoId": "photo-id",
"delta": 5,
"total": 128,
"countVersion": 128,
"occurredAt": "2026-09-17T08:00:00Z"
}
```
事件类型至少包含 photo.published、photo.hidden、photo.deleted、photo.likes.updated 和 album.updated。计数单调递增时可使用总数作为比较版本;如未来支持人工修正,应改为独立版本字段。客户端忽略过期版本、重复 eventId,不把同一广播再次加到本地总数。
### 8.3 广播与动画
实时订阅按相册隔离,服务端验证公开状态后加入对应频道。高频事件可按短时间窗和照片合并,广播最新 total 与累计 delta。客户端只展示有限数量的动画,动画数不等于点赞数。
发送失败可从 outbox 重试,消费者按 eventId 去重。广播可采用非持久实时通道;断线后通过 API 获取权威列表和计数,不要求重播气泡。
每个页面用 originPageId 与 batchId 关联本地已播放动画,广播仅包含随机临时标识,不暴露身份。合并广播保留源批次贡献或等效去重信息,页面只抑制自己的部分。后台页面不排队动画,前台恢复只校准计数;减少或关闭动画不改变请求与计数。广播发送和客户端展示前过滤已隐藏或删除照片。实时不可用时降级轮询,点赞请求仍经 API 确认。
## 9 JS 悬浮组件
下面的域名为占位示例,部署后由后台生成真实地址,不可直接作为可用代码交付。
```html
<script
src="https://YOUR-DOMAIN.example/embed/v1/loader.js"
data-slot="PUBLIC_SLOT_TOKEN"
data-lang="auto"
data-position="right"
data-interval="5000"
defer>
</script>
```
展示位绑定相册,管理员管理全局展示位并授权操作人员,创建者只能将有管理权限的相册绑定到获授权展示位。切换产生 configVersion,不修改官网脚本。
加载器读取当前脚本配置,防止重复初始化,创建固定位置的 iframe。轮播页面部署在项目域名内,通过只读公开 API 获取近期照片;宿主站点无需接触内部业务 API 或工作人员令牌。
组件高度、收起等通信通过 postMessage,双方验证消息来源与允许的 origin,不接受任意页面发送的尺寸或跳转命令。iframe 配置只启用所需能力。宿主需允许 script-src 与 frame-src,嵌入页需配置允许的 frame-ancestors;不能笼统移除全站安全策略。
加载器版本路径长期稳定;内部轮播页面可独立更新。默认只拉取近期有限照片,新照片加入队列、旧照片淘汰;第一版定期拉取配置和近期照片,隐藏内容在下次成功更新时移除,不依赖点赞 WebSocket。轮询周期及失效上限在验收前确定;不可见时暂停,恢复时校准。切换配置后取消旧请求,比较 configVersion 丢弃晚到响应。用户关闭后记住当前会话选择,不强制反复弹出。
点击图片使用用户手势打开新标签页,链接附 photoId 与当前语言,并设置适当的 opener 隔离。移动端缩小或收起的具体尺寸在原型阶段确认。
## 10 接口草案
以下是资源边界草案,正式开发以 OpenAPI 契约为准。公开读取接口不接受工作人员写权限。
| 方法与路径 | 权限 | 用途 |
|---|---|---|
| POST /v1/auth/password | 登录入口 | 密码登录 |
| POST /v1/auth/refresh | 有效刷新凭据 | 刷新会话 |
| POST /v1/admin/users/{id}/activate | 管理员 | 激活已同步用户并初始化本地凭据 |
| POST /v1/admin/user-sync/runs | 管理员 | 手动触发补拉,与定时任务共用锁 |
| GET /v1/admin/user-sync/runs | 管理员 | 查看同步状态、历史和脱敏错误 |
| PATCH /v1/admin/users/{id} | 管理员 | 本地停用、角色管理;上游字段只读 |
| POST /v1/albums | 工作人员 | 创建相册 |
| PATCH /v1/albums/{id} | 创建者或管理员 | 配置语言、上传公开和下载开关 |
| POST /v1/admin/albums/{id}/transfer | 管理员 | 转移负责人并明确旧负责人角色 |
| POST /v1/albums/{id}/photos/hide | 对应照片管理权限 | 批量隐藏,逐项返回结果 |
| POST /v1/albums/{id}/watermark-jobs | 创建者或管理员 | 明确发起历史衍生图重生成 |
| POST /v1/admin/embed-slots | 管理员 | 创建展示位并配置管理权限 |
| PATCH /v1/embed-slots/{id} | 展示位权限及目标相册管理权限 | 切换相册或配置 |
| GET /v1/public/embed-slots/{token} | 匿名 | 公开配置版本、相册和近期照片 |
| POST /v1/albums/{id}/members | 创建者或管理员 | 添加已开通同事 |
| DELETE /v1/albums/{id}/members/{userId} | 创建者或管理员 | 移除成员 |
| POST /v1/uploads | 相册上传权限 | 幂等初始化任务 |
| POST /v1/uploads/{id}/renew | 任务本人及有效权限 | 更新临时授权 |
| POST /v1/uploads/{id}/complete | 任务本人及有效权限 | 核验并进入图片处理 |
| GET /v1/uploads | 工作人员本人 | 获取服务端任务记录 |
| PATCH /v1/photos/{id} | 对应照片管理权限 | 分类或隐藏 |
| DELETE /v1/photos/{id} | 对应照片管理权限 | 软删除 |
| GET /v1/public/albums/{token} | 匿名 | 活动信息及语言版本 |
| POST /v1/public/albums/{token}/visits | 匿名及限速 | 幂等登记有效浏览,返回累计浏览次数 |
| GET /v1/public/albums/{token}/photos | 匿名 | 分类排序游标分页 |
| GET /v1/public/albums/{token}/photos/{id} | 匿名 | 单图定位 |
| POST /v1/public/albums/{token}/photos/{id}/likes | 匿名及限速 | 批次幂等点赞 |
| GET /v1/public/albums/{token}/photos/{id}/download | 匿名及下载设置 | 获取允许版本的短期链接 |
写操作携带幂等键或资源版本,错误返回稳定 code 和 requestId,显示文案在客户端按语言映射。日志不得记录密码、完整签名 URL、STS 密钥或用户接口凭据。
## 11 多语言实施
Flutter 使用本地化资源文件,Web 使用对应语言字典,共用词条命名约定但不要求直接共用不同运行时格式。构建时检查缺失键与插值参数。
语言优先级为手动选择、账号已存偏好、设备或浏览器、系统回退。匿名观众不要求账号偏好。活动内容按请求语言、活动默认语言顺序返回,并携带实际返回语言供界面判断。
UTC 时间与活动时区分开存储;展示时格式化,不随用户语言隐式改变活动时区。英文长文本和中文字体都纳入页面及组件尺寸验证。
## 12 部署与运维
### 12.1 部署前置
- 确认 OSS 地域、现有规则、项目前缀、RAM 最小权限和跨域需求;不直接改动其他业务配置。
- 确认域名、TLS、用户接口鉴权和网络可达性。
- 配置数据库、Redis、任务进程、实时入口及反向代理的连接超时。
- iOS 构建需要 macOS、Xcode、签名与确定的分发方式;Android 配置签名和安装分发。
- secrets 注入环境或密钥管理服务,不进入版本库。生产与测试凭证隔离。
### 12.2 发布步骤
1. 数据库备份,执行兼容旧版本的新增迁移。
2. 部署 API 与处理进程,检查健康、授权和测试照片完整链路。
3. 部署观众页、后台、组件及实时入口,核对静态资源版本。
4. 对测试相册验证登录、后台上传、自动公开、连续点赞、分享下载、组件和双语言。
5. 按确定方式分发 App;服务端保持必要的旧客户端兼容。
6. 观察错误率、队列积压、上传失败、计数差异和资源用量,达到发布门槛后扩大使用。
### 12.3 监控
配置项目与相册容量和流量告警,明确阈值、统计延迟和接收人,日志与临时碎片单独制定保留周期;告警不自动删图或停止活动。
记录 API 时延与错误率、上传完成率、处理等待时长、失败任务数、实时连接数、广播延迟、数据库计数写入失败、OSS 容量和出网流量。业务日志使用 requestId、uploadId、photoId 关联。
图片处理异常时保留原图和待处理状态,不显示坏图;实时故障时降级轮询;API 不可用时客户端保留上传队列并退避重试。避免无限重试产生费用和负载。
### 12.4 备份和回滚
数据库定期备份并演练恢复。上传原图与业务元数据共同决定可恢复性,应核对数据库引用和 OSS 对象清单。建议的恢复点与恢复时间指标在部署资源评审后确定。
发布回滚优先恢复旧应用镜像和静态资源,数据库使用向前兼容迁移,避免自动执行破坏性逆迁移。停止有缺陷的处理进程后保留任务,修复后重放。永久删除、覆盖原图或生命周期变更不应作为日常回滚步骤。
### 12.5 跨地域发布与验收
先部署海外接收接口和数据库,验证鉴权与协议版本,再做首次快照、增量追赶及对账,达到同步水位后开放观众域名。后续升级采用兼容事件版本,先升级接收端再启用发送端新字段;回滚保留 inbox、outbox、水位和墓碑,不清空同步状态。
全球 CDN 设置国内 OSS 回源、观众 URL 鉴权和缓存规则,分别测目标地区冷缓存新图、命中浏览图与原图下载。只对必要浏览资源按测试结果选择预热,不能把预热等同于所有节点瞬时就绪。来源参数、签名和缓存键一起测试,下载关闭与隐藏失效需要在海外链路验收。
新增监控:内容同步积压与延迟、统计回传水位、墓碑对账、资源探测失败、海外 API 和 CDN 地域时延。告警区分国内图片处理慢、跨境同步慢、海外 API 慢和 CDN 回源慢。
必须演练断网重放、重复乱序、首次快照与增量并发、下线优先传递、海外紧急停用、统计重复回传以及国内恢复。两地分别备份并验证恢复后数据权威不会倒置。
## 13 实施启动清单
- [ ] 确认海外地域、观众域名、全球 CDN、同步周期和海外紧急停用负责人。
- [ ] 确认建议技术栈、首发语言和目标系统版本。
- [ ] 确认用户接口字段、同步周期、停启用和初始密码规则。
- [ ] 获取 OSS 非敏感配置,建立项目隔离路径和权限。
- [ ] 准备双平台真机、macOS 构建环境及 App 分发资质。
- [ ] 完成后台上传和过期授权验证,再选择插件或自建原生桥接。
- [ ] 冻结数据表、接口契约、事件格式和状态定义。
- [ ] 对照需求 R01 至 R31 建立任务,对照 A01 至 A29 建立验收记录。
- [ ] 试运行前确认容量、下载默认值、删除策略和监控负责人。
## 14 技术参考
以下为技术方案核验入口,访问日期为 2026-09-17。实际实现前结合目标系统版本复核;本文不承诺任何插件已满足全部后台场景。
- [Apple 后台文件传输](https://developer.apple.com/documentation/foundation/downloading-files-in-the-background)
- [Apple 后台会话配置与强制退出边界](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/background(withidentifier:))
- [Android 数据传输任务选择](https://developer.android.com/develop/background-work/background-tasks/data-transfer-options)
- [Android 用户发起的数据传输](https://developer.android.com/develop/background-work/background-tasks/uidt)
- [阿里云 OSS STS 临时授权](https://www.alibabacloud.com/help/en/oss/sts-temporary-access-authorization)
- [阿里云上传临时授权方式](https://www.alibabacloud.com/help/en/oss/developer-reference/authorize-access-6)
- 用户接口文档:待接口提供方交付,第一期不引用飞书接口。
+142
View File
@@ -0,0 +1,142 @@
# 图片直播项目优化方案
版本:V1.3
日期:2026-09-17
关联文档:[需求说明书](./01-需求说明书.md) · [项目计划](./02-项目计划.md) · [技术实施方案](./03-技术实施方案.md)
本次优化补齐现场操作、异常恢复、文件保护、统计口径和运营维护,并降低第一版部署复杂度。以下优化纳入第一版文档基线;语言范围、时间窗口、容量、设备兼容和工期仍需验证,不视为已经实现或测试通过。
## 1 优化范围与优先级
P0 表示必须在核心开发前明确或验证,P1 表示第一版交付前完成。优先级用于实施排序,不代表 P1 自动延期。
| 编号 | 优化内容 | 优先级 | 需求映射 | 任务映射 | 验收映射 |
|---|---|---|---|---|---|
| O01 | 连续拍摄并自动入队,清楚显示目标相册 | P1 | R17 | T20 | A15 |
| O02 | 上传开关与公开开关独立,支持批量隐藏 | P0 | R18 | T20 | A16 |
| O03 | 管理员转移相册负责人,保留历史归属 | P1 | R19 | T20 | A17 |
| O04 | 下载权限拆分与水印版本规则 | P0 | R20 | T20 T22 | A18 |
| O05 | 照片准备阶段与本地服务端记录分离 | P0 | R21 | T21 | A19 |
| O06 | 原图防覆盖及受控图片访问 | P0 | R22 | T22 | A20 |
| O07 | 明确匿名浏览统计和跨标签页去重 | P0 | R16 R23 | T19 | A14 |
| O08 | 气泡去重、后台暂停和减少动画 | P1 | R24 | T24 | A21 |
| O09 | JS 绑定展示位,后台切换相册 | P1 | R25 | T23 | A22 |
| O10 | 容量流量告警与组件节省流量 | P1 | R26 | T17 T23 | A23 |
| O11 | API 与实时入口合并部署,任务进程独立 | P0 | 实施约束 | T02 | A24 |
| O12 | 修正任务依赖并按岗位检查工期 | P0 | 管理约束 | 项目计划 | 里程碑评审 |
## 2 现场拍摄与照片准备
### 2.1 连续拍摄
工作人员先选择相册和分类,再进入连续拍摄模式。每次拍摄成功后自动创建准备任务,不要求逐张跳回列表确认。拍摄页常驻显示相册名称、分类、待上传数量和失败提示。
切换相册只影响后续照片,已入队任务的目标不可隐式修改。每张照片都有独立任务 ID;拍摄成功但写入本地失败时必须明确提示,不能显示“已加入队列”。
### 2.2 准备阶段
新增“准备中”和“准备失败”,涵盖系统相册文件获取、云端照片下载、本地空间、格式检查和复制。准备完成后才进入可后台传输的队列。
准备任务和上传任务分别统计,例如“准备 5 张,等待上传 20 张”。准备阶段不承诺可以无限后台执行;切后台前未准备完成的照片应保留可恢复状态,必要时提示回到 App。
本地任务包含实际文件和系统传输信息;服务端记录包含上传确认、发布结果和历史信息。换手机后可查看服务端历史,不能恢复另一台手机尚未上传的文件。新手机重新选择同一文件需明确创建新任务或进行重复提示。
## 3 相册控制与人员交接
相册采用独立的 uploadEnabled 和 publicEnabled 开关。关闭上传阻止新建任务和续签,已经完成核验的处理任务可继续处理;关闭公开立即停止公开 API 与实时订阅,新图片可处理就绪但不对外展示。
重新公开时,只展示符合照片可见性要求的图片,不自动恢复已隐藏或软删除的照片。活动结束时间本身不改变两个开关。
批量隐藏操作应返回逐项结果并记录操作者。观众列表、全屏图、轮播和待发送气泡均校验照片可见性;已有下载文件无法收回,缓存访问边界见第 5 节。
公司管理员可把负责人转移给有效工作人员,原上传人信息保持不变。原负责人是否保留协作者身份必须在转移操作中明确选择。停用账号不删除其历史照片,相册由管理员接管后再安排转移。
## 4 下载与水印
采用两个字段:downloadEnabled 和 originalDownloadEnabled。原图下载必须以允许下载为前提;关闭全部下载时隐藏下载操作并拒绝下载接口,但不能阻止截图或保存已展示的浏览图片。
允许下载且关闭原图时,提供当前配置允许的浏览版或水印版。原图始终保存,不把加水印图片覆盖到原图对象上。
水印配置带版本号。修改默认仅影响后续新照片;历史照片重生成由创建者或管理员明确发起,显示数量和进度,失败可重试。新衍生版本全部就绪前保留可用旧版本,更新单张照片引用必须原子化。旧版本清理通过独立策略执行,不在生成失败后直接删除。
## 5 文件不可变与访问链路
上传会话中的临时对象与已发布原图应有清楚边界。推荐先上传 staging 路径,服务端固定源版本或完成条件校验后复制到客户端无写权限的 originals 路径。后续图片处理只读取固定的原图对象。
仅随机文件名不够:同一会话的旧授权仍可能重写同一路径。上传路径应禁止覆盖,并对“对象已存在”执行完整性核验;分片完成与普通上传分别验证。不能只靠客户端自觉添加禁止覆盖参数,服务端签名或权限机制应能约束相应行为。
私有 OSS 和 CDN 回源授权、观众侧 CDN 访问鉴权是不同环节。接入 CDN 时必须确定签名、缓存键、有效期、刷新与隐藏后的失效方式。未采用 CDN 时也要处理短期 OSS 链接过期,长时间打开的大图可重新请求授权。
下线或隐藏后停止新签发链接、撤销频道并通知在线页面。对已签发 URL 和缓存定义可接受失效窗口,参数实测后确认;不能承诺未下载图片在所有缓存中瞬时消失。
参考:[OSS 禁止覆盖示例](https://github.com/aliyun/aliyun-oss-python-sdk/blob/master/examples/object_forbid_overwrite.py)、[CDN 与 OSS 访问说明](https://www.alibabacloud.com/help/en/cdn/use-cases/cdn-acceleration-oss-faq)。
## 6 浏览统计与互动
### 6.1 浏览次数
匿名观众使用同源持久浏览器标识,同一浏览器下多个标签页共享;标识创建发生并发时由服务端去重逻辑保护。同一浏览器、同一相册在建议的 30 分钟窗口内只计一次,窗口仍是待确认参数。
仅页面实际可见且相册信息加载成功后上报。翻图、语言切换、轮询、自动轮播、后台预览和资源请求不计数。超过窗口后的新页面访问可再计一次,原页面持续停留不定时增加次数。
这不是可靠的真实人数统计;不同浏览器、隐私模式或清除记录可能重新计数。浏览器禁止持久存储时降级为当前页面标识,明确降低去重精度,不影响浏览。
### 6.2 点赞与气泡
点击者先播放本地动画。服务端确认与广播通过源批次标识关联,当前页面忽略自己已经播放的批次;其他页面照常展示。若合并事件同时含自己和他人的点击,只抑制自己的部分。
后台标签页不积压动画,恢复后只同步最新计数。照片隐藏后,待发送和待展示的气泡都要过滤。支持关闭气泡或减少动画,不改变点赞数,也不影响其他观众。
## 7 网站展示位
官网接入时绑定 slotId,后台配置展示位指向哪个相册。更换活动时修改绑定即可,无需改动网站代码。组件只暴露公开配置,不携带管理权限。
展示位包括名称、启用状态、相册、默认语言、尺寸、位置和轮播参数。管理员管理全局展示位,创建者只能绑定有管理权限的相册到被授权的展示位,不能覆盖他人官网配置。
第一版用短周期轮询获取展示位配置和近期照片,周期参数经测试确定,不依赖点赞 WebSocket。切换相册时清空旧图与请求状态,避免旧响应覆盖新配置;后台返回配置版本供客户端校验。
组件页面不可见时暂停轮询与预取,恢复可见后重新校准。无图、相册下线或展示位停用时显示约定空态或收起;具体外观在原型确定。
## 8 部署与费用
V1.2 调整为国内管理服务与海外观众服务分别部署,海外 API 和 WebSocket 在同一服务进程内按模块组织,国内图片处理 worker 独立。保留未来独立实时进程的接口,不提前部署多个小服务。生产实例数量按可用性与压测结果配置。
费用模型至少区分原图、浏览版本、旧水印版本、请求、处理、下载流量、CDN、备份和日志。按十进制举例,1 万张 8 MB 原图约 80 GB;1 万观众每人浏览 100 张 300 KB 图片,约 300 GB 图片传输。这只是用量算例,不是价格估算,也未扣除浏览器缓存或计入原图下载。
设定项目及相册用量统计、费用或容量阈值与告警接收人。告警不自动删除照片,也不默认中断活动;是否需要限制原图下载或新增上传由管理员明确操作。
## 9 排期影响
V1.1 历史估算:原计划为 102 至 146 角色人日。增加 T20 至 T24 五个增量工作包,约 27 至 42 人日,修订为 129 至 188 人日。浏览统计精化并入 T19,部署简化并入 T02,费用告警并入 T17,不重复计入新增任务。
V1.1 当时采用 12 周基线;V1.2 增加国内海外同步和全球 CDN 验收后,当时为 151 至 222 人日、14 至 16 周估算,以 16 周安排为基线。岗位分配和任务依赖见项目计划,W2 技术验证后再次确认。
## 10 优化专项验收
- 连续拍摄时切相册不会改变已经入队照片的归属。
- 云端照片未准备完、存储不足及源文件失效都有明确状态。
- 锁屏批量上传遇到临时授权过期可恢复,无重复发布。
- 停止公开后 API、频道、轮播和气泡行为一致。
- 管理员转移负责人后旧照片上传人和审计信息保留。
- 两个下载开关组合合法,历史水印重生成失败不破坏现有浏览图。
- 旧凭证不能改变已发布原图,重试已有对象不会覆盖或误报成功。
- 同浏览器多标签访问去重,自动轮播不增加浏览次数。
- 本地气泡不会因自己的广播重复出现,减少动画不改变计数。
- 展示位换相册不用修改官网,旧请求不能恢复旧相册内容。
- 组件不可见时停止无效更新,流量与容量告警可验证。
## 11 仍待验证的参数
30 分钟浏览窗口、后台最长测试时长、图片格式与大小上限、上传并发、图片尺寸与质量、缓存失效窗口、轮询周期、用量阈值、目标 OS 版本及岗位实际投入。待定参数不阻止完成原型与技术试验,但必须在对应模块验收前冻结。
## 12 V1.2 海外访问增补
国内负责管理与上传,海外负责公开页面、查询、浏览点赞和气泡,现有国内 OSS 配合全球 CDN。图片文件不经跨境业务同步复制;国内向海外发送版本化展示元数据,海外向国内回传版本化统计快照。
新增 T25 至 T29,覆盖海外可行性、持久内容同步、统计回传、CDN 链路及故障验收。隐藏删除优先传递,定时对账补漏;权威数据按类型单点写入。V1.2 是海外架构变更记录;当前范围与估算见 V1.3 计划。
## 13 V1.3 账号与用户来源调整
第一期后台与 App 统一账号密码登录,飞书接入延期,不实现飞书授权或绑定。国内服务定期调用用户接口更新本地资料,支持任务记录、失败重试、手动补拉和稳定外部 ID 映射。
本地密码、权限和手动停用不受普通资料同步覆盖。新增用户激活、初始密码、同步周期和源停用字段需确认;空响应与部分分页失败不得误停用人员。T04 改为接口验证,新增 T30 同步管理,当前估算为 156 至 231 人日,16 周工作基线待接口验证后复核。
+19
View File
@@ -0,0 +1,19 @@
# 图片直播项目文档
版本:V1.3 · 更新日期:2026-09-17
本目录包含第一版项目需求、计划、技术实施与优化方案。建议按以下顺序阅读:
1. [需求说明书](./01-需求说明书.md):范围、角色、流程、功能和验收标准。
2. [项目计划](./02-项目计划.md):工作分解、依赖、相对周次、工作量和交付门槛。
3. [技术实施方案](./03-技术实施方案.md):架构、数据模型、接口、OSS、后台上传、实时互动、嵌入组件和部署。
4. [优化方案](./04-优化方案.md):12 项优化、需求任务验收映射、异常处理和排期影响。
5. [项目思维导图与计划导图](./diagrams/README.md):项目全景、业务流程、工作分解、里程碑和任务依赖,共七张图。
已确认的核心选型为 Flutter 双平台 App 与现有国内阿里云 OSS、全球 CDN、国内管理与海外观众服务。首发语种、服务端框架、测试容量和工期属于建议基线;优化已同步到原三份文档。项目计划按三名开发及兼职支持估算,约 156 至 231 角色人日,以 16 周排期为工作基线,需结合真实人员与技术验证结果修订。
当前只完成文档编制,尚未创建业务工程、接入用户接口或 OSS,也未执行产品功能与容量测试。
V1.3:第一期后台与 App 仅账号密码登录,飞书暂不接入;用户信息由指定接口定期拉取,密码与项目权限由本地管理。
+60
View File
@@ -0,0 +1,60 @@
mindmap
root((公司图片直播项目))
工作人员 App
Flutter 双平台
Android
iOS
登录与权限
账号密码登录
接口同步用户 不开放注册
相册与协作
创建配置 时间地点宣传图
邀请同事 多人上传
自己照片管理
拍摄与上传
连续拍摄和相册选图
准备状态与上传队列
后台锁屏上传与异常恢复
本机队列与服务端历史
管理后台
定期拉取用户接口
稳定 ID 幂等更新
失败重试与手动补拉
用户同步与账号激活停用
全部相册与照片管理
上传公开独立开关
批量隐藏与负责人交接
下载权限与水印版本
网站展示位与用量告警
观众网页
扫码或分享链接免登录
宣传图与活动信息
浏览次数与跨标签去重
瀑布流与分类
全屏大图与左右滑动
分享与按权限下载
连续点赞与全相册气泡
自身动画去重
后台暂停与减少动画
网站悬浮组件
一段 JS 绑定展示位
后台切换活动相册
右侧悬浮轮播
自动获取最新公开照片
点击打开观众页
不可见时暂停更新
支撑服务
内部身份与相册权限
国内 OSS 直传和全球 CDN
原图防覆盖与衍生图处理
海外 API 与实时模块合并部署
独立图片处理进程
国内元数据推送与定时补漏
海外浏览点赞与统计回传
两地数据库 备份监控
共用规则
全端多语言
无修图 无人工审核
上传完成处理后自动公开
隐藏删除与公开状态一致
匿名计数不等于真实人数
+109
View File
@@ -0,0 +1,109 @@
# 图片直播项目全景导图
版本:V1.3 · 日期:2026-09-17
依据:[需求说明书](../01-需求说明书.md) · [优化方案](../04-优化方案.md)
相关:[业务流程](./02-业务流程.md) · [实施计划](./03-实施计划.md)
以下导图描述第一版规划范围,不表示功能已经实现。中文和英文为建议首发语种,服务端技术栈和容量参数待验证。
## 1 项目思维导图
```mermaid
mindmap
root((公司图片直播项目))
工作人员 App
Flutter 双平台
Android
iOS
登录与权限
账号密码登录
接口同步用户 不开放注册
相册与协作
创建配置 时间地点宣传图
邀请同事 多人上传
自己照片管理
拍摄与上传
连续拍摄和相册选图
准备状态与上传队列
后台锁屏上传与异常恢复
本机队列与服务端历史
管理后台
定期拉取用户接口
稳定 ID 幂等更新
失败重试与手动补拉
用户同步与账号激活停用
全部相册与照片管理
上传公开独立开关
批量隐藏与负责人交接
下载权限与水印版本
网站展示位与用量告警
观众网页
扫码或分享链接免登录
宣传图与活动信息
浏览次数与跨标签去重
瀑布流与分类
全屏大图与左右滑动
分享与按权限下载
连续点赞与全相册气泡
自身动画去重
后台暂停与减少动画
网站悬浮组件
一段 JS 绑定展示位
后台切换活动相册
右侧悬浮轮播
自动获取最新公开照片
点击打开观众页
不可见时暂停更新
支撑服务
内部身份与相册权限
国内 OSS 直传和全球 CDN
原图防覆盖与衍生图处理
海外 API 与实时模块合并部署
独立图片处理进程
国内元数据推送与定时补漏
海外浏览点赞与统计回传
两地数据库 备份监控
共用规则
全端多语言
无修图 无人工审核
上传完成处理后自动公开
隐藏删除与公开状态一致
匿名计数不等于真实人数
```
## 2 系统组成与数据关系
Flutter 和现有 OSS 已确定;其余组件为实施方案基线。国内管理与海外观众服务分区部署;海外实时模块与观众 API 同部署,国内 worker 独立;轮播读海外,图片经全球 CDN。
```mermaid
flowchart TB
APP["工作人员 App"] --> CN["国内管理 API"]
ADM["国内管理后台"] --> CN
USRC["用户信息接口"] -->|国内定时拉取| USYNC["用户资料同步任务"]
USYNC --> CN
APP -->|临时授权直传| OSS["国内 OSS 原图与浏览资源"]
CN --> CDB[("国内主数据库")]
CN --> WORKER["国内图片处理 worker"]
WORKER --> OSS
WORKER --> CDB
CDB --> OUT["持久事件与定时补漏"]
OUT -->|版本化元数据| OVER["海外观众 API 与实时模块"]
OVER --> ODB[("海外展示副本与互动数据库")]
ODB -->|统计快照回传| CN
USER["海外观众"] -->|页面 查询 点赞 浏览| OVER
SLOT["官网 JS 展示位"] -->|轮询| OVER
OSS -->|回源| CDN["全球 CDN"]
USER -->|图片请求| CDN
SLOT -->|轮播图片| CDN
```
## 3 权限速查
| 角色 | 管理范围 |
|---|---|
| 公司管理员 | 用户同步、账号激活、相册、照片、展示位和负责人转移 |
| 相册创建者 | 自己创建的相册、协作者、全部相册照片及获授权展示位 |
| 相册协作者 | 已加入相册的上传权限,管理自己上传的照片 |
| 匿名观众 | 浏览、连续点赞、分享,按相册设置下载 |
长期保存不等于永久公开。关闭公开开关不删除原图;关闭上传也不自动关闭观众页面。
+19
View File
@@ -0,0 +1,19 @@
flowchart TB
APP["工作人员 App"] --> CN["国内管理 API"]
ADM["国内管理后台"] --> CN
USRC["用户信息接口"] -->|国内定时拉取| USYNC["用户资料同步任务"]
USYNC --> CN
APP -->|临时授权直传| OSS["国内 OSS 原图与浏览资源"]
CN --> CDB[("国内主数据库")]
CN --> WORKER["国内图片处理 worker"]
WORKER --> OSS
WORKER --> CDB
CDB --> OUT["持久事件与定时补漏"]
OUT -->|版本化元数据| OVER["海外观众 API 与实时模块"]
OVER --> ODB[("海外展示副本与互动数据库")]
ODB -->|统计快照回传| CN
USER["海外观众"] -->|页面 查询 点赞 浏览| OVER
SLOT["官网 JS 展示位"] -->|轮询| OVER
OSS -->|回源| CDN["全球 CDN"]
USER -->|图片请求| CDN
SLOT -->|轮播图片| CDN
+90
View File
@@ -0,0 +1,90 @@
# 图片直播业务流程导图
版本:V1.3 · 日期:2026-09-17
依据:[需求说明书](../01-需求说明书.md) · [技术实施方案](../03-技术实施方案.md)
相关:[项目全景](./01-项目全景.md) · [实施计划](./03-实施计划.md)
## 1 从创建活动到观众互动
```mermaid
flowchart TB
A["接口同步资料并激活账号"] --> B["工作人员登录<br/>账号密码"]
B --> C["创建配置相册<br/>宣传图 时间 地点 多语言 分类"]
C --> D["邀请同事加入相册"]
D --> E["拍摄或选图<br/>准备本地文件并入队"]
E --> F["OSS 临时授权直传<br/>后台锁屏与失败恢复"]
F --> G["服务端核验并固定原图<br/>生成浏览图和可选水印"]
G --> H{"处理成功且<br/>允许对外展示?"}
H -->|否| HOLD["保留待处理或非公开状态<br/>失败重试或等待开放"]
HOLD -->|恢复后重新检查| H
H -->|是| SYNC["国内事件推送与定时补漏<br/>海外核验资源与版本"]
SYNC --> I["海外可见<br/>无需人工审核"]
C --> LINK["生成观众链接和二维码"]
LINK --> V["观众免登录打开活动页"]
I --> V
COUNT --> STATS["统计快照回传国内<br/>版本替换不重复相加"]
LIKE --> STATS
V --> COUNT["上报有效浏览<br/>海外去重累计并展示"]
V --> LIST["瀑布流 分类 最新或热门"]
LIST --> FULL["点击全屏看图<br/>缩放 左右滑动"]
FULL --> LIKE["连续点赞<br/>海外批次幂等累计"]
LIKE --> BUBBLE["同相册在线观众气泡<br/>自身去重 密集动画合并"]
FULL --> SHARE["分享定位当前照片"]
FULL --> DL["按下载权限获取图片"]
SHARE --> V
C --> SLOT["绑定官网展示位"]
SLOT --> JS["官网接入一次 JS"]
I --> ROTATE["轮询近期公开照片<br/>右侧悬浮轮播"]
JS --> ROTATE
ROTATE -->|点击照片| V
```
浏览统计只由观众页实际访问触发;自动轮播、图片请求、分页和点赞不额外累计。30 分钟去重窗口为待确认参数。
## 2 上传与异常恢复
```mermaid
flowchart TB
CAP["拍摄成功或选择照片"] --> PREP["准备中<br/>获取源文件 检查空间 本地复制"]
PREP -->|失败| PF["准备失败<br/>保留原因和恢复入口"]
PF -->|重新获取或释放空间| PREP
PREP -->|成功| QUEUE["持久化队列<br/>固定账号 相册 分类 任务 ID"]
QUEUE --> AUTH{"账号权限与<br/>上传开关有效?"}
AUTH -->|否| WAIT["等待恢复权限或开放上传<br/>也可明确取消任务"]
WAIT -->|条件恢复| AUTH
AUTH -->|是| UP["原生系统传输至 OSS 暂存区"]
UP -->|断网或授权过期| RETRY["保存任务<br/>等待网络或续签后重试"]
RETRY --> AUTH
UP -->|系统中止或强制退出| REOPEN["保留可恢复记录<br/>系统允许或下次启动后校准"]
REOPEN --> AUTH
UP -->|文件传输完成| CONFIRM["服务端核验与幂等确认<br/>固定不可覆盖的正式原图"]
CONFIRM -->|失败| CF["确认失败或待补偿<br/>核验已有对象 不盲目重传"]
CF --> CONFIRM
CONFIRM --> JOB["处理图片与生成浏览资源"]
JOB -->|失败| JF["记录处理失败<br/>有限重试或人工重试"]
JF --> JOB
JOB -->|完成| READY["处理就绪"]
READY --> CHECK{"相册公开且<br/>照片未隐藏删除?"}
CHECK -->|是| SYNC["元数据同步海外<br/>幂等 版本检查 定时补漏"]
SYNC --> LIVE["海外资源可用后可见<br/>通知观众并进入轮播"]
CHECK -->|否| INTERNAL["内部保留<br/>原图继续长期保存"]
INTERNAL -->|公开状态变化后检查| CHECK
```
“后台继续上传”依赖平台系统调度,不等于强制退出后持续执行。换手机可查看服务端历史,不能恢复另一台手机尚未上传的本地源文件。
## 3 相册控制规则
| 操作 | 上传处理 | 观众与轮播 | 文件 |
|---|---|---|---|
| 关闭上传 | 拒绝新任务和续签,已核验处理可继续 | 保持原公开设置 | 不删除 |
| 关闭公开 | 内部上传处理可继续 | 停止公开查询和订阅,组件更新后收起或空态 | 长期保留 |
| 批量隐藏 | 不影响其他照片上传 | 移除相关照片和待展示气泡 | 原图保留 |
| 重新公开 | 不自动修改上传开关 | 仅展示符合可见性条件的照片 | 不恢复已隐藏或删除照片 |
| 转移负责人 | 更新操作权限,已有任务重新校验 | 不改变相册公开地址 | 保留历史上传归属 |
已签发资源链接按约定缓存失效窗口处理,不能撤回已下载图片。
国内处理就绪与海外可见分开显示。同步中断不阻止海外访问既有内容,国内新增内容等待补发;下线优先同步,跨境中断时使用海外紧急停用通道。观众图片统一经全球 CDN,冷缓存首次访问单独验收。
+32
View File
@@ -0,0 +1,32 @@
flowchart TB
A["接口同步资料并激活账号"] --> B["工作人员登录<br/>账号密码"]
B --> C["创建配置相册<br/>宣传图 时间 地点 多语言 分类"]
C --> D["邀请同事加入相册"]
D --> E["拍摄或选图<br/>准备本地文件并入队"]
E --> F["OSS 临时授权直传<br/>后台锁屏与失败恢复"]
F --> G["服务端核验并固定原图<br/>生成浏览图和可选水印"]
G --> H{"处理成功且<br/>允许对外展示?"}
H -->|否| HOLD["保留待处理或非公开状态<br/>失败重试或等待开放"]
HOLD -->|恢复后重新检查| H
H -->|是| SYNC["国内事件推送与定时补漏<br/>海外核验资源与版本"]
SYNC --> I["海外可见<br/>无需人工审核"]
C --> LINK["生成观众链接和二维码"]
LINK --> V["观众免登录打开活动页"]
I --> V
COUNT --> STATS["统计快照回传国内<br/>版本替换不重复相加"]
LIKE --> STATS
V --> COUNT["上报有效浏览<br/>海外去重累计并展示"]
V --> LIST["瀑布流 分类 最新或热门"]
LIST --> FULL["点击全屏看图<br/>缩放 左右滑动"]
FULL --> LIKE["连续点赞<br/>海外批次幂等累计"]
LIKE --> BUBBLE["同相册在线观众气泡<br/>自身去重 密集动画合并"]
FULL --> SHARE["分享定位当前照片"]
FULL --> DL["按下载权限获取图片"]
SHARE --> V
C --> SLOT["绑定官网展示位"]
SLOT --> JS["官网接入一次 JS"]
I --> ROTATE["轮询近期公开照片<br/>右侧悬浮轮播"]
JS --> ROTATE
ROTATE -->|点击照片| V
+149
View File
@@ -0,0 +1,149 @@
# 图片直播项目实施计划导图
版本:V1.3 · 日期:2026-09-17
依据:[项目计划](../02-项目计划.md) · [优化方案](../04-优化方案.md)
相关:[项目全景](./01-项目全景.md) · [业务流程](./02-业务流程.md)
以 16 周为工作基线,约 156 至 231 角色人日。假设 1 名 Flutter、1 名后端、1 名 Web 开发,加兼职产品设计、测试及运维。周次为相对时间,并非已承诺交付日期;W2 技术验证后重估。以下箭头表示交付依赖,不表示所有任务必须逐个串行。
## 1 工作分解思维导图
每个任务编号与项目计划一致,本图覆盖 T01 至 T30。分组用于阅读,不能替代任务依赖。
```mermaid
mindmap
root((第一版实施计划))
需求与基础验证
T01 需求权限原型
T02 工程环境与流水线
T03 双平台后台上传验证
T04 用户接口同步验证
账号与核心服务
T05 账号会话和权限
T30 用户定期同步与后台管理
T06 相册分类语言协作
T07 OSS 核验处理发布
Flutter App
T08 登录相册与协作页面
T09 上传队列和记录
T21 准备状态与跨设备历史
后台与活动管理
T10 相册照片用量管理
T20 连拍开关交接水印增量
观众与传播
T11 活动页瀑布流大图
T12 分享下载与浏览器适配
T13 点赞与实时气泡
T19 浏览计数与展示
T24 气泡去重与动画偏好
官网嵌入
T14 JS 悬浮组件
T23 展示位与不可见暂停
海外服务与同步
T25 海外与全球 CDN 验证
T26 内容同步与定时补漏
T27 海外计数与统计回传
T28 全球 CDN 访问策略
T29 跨地域故障验收
质量与发布
T15 全端多语言验收
T16 联调容量与修复
T17 部署备份监控告警
T18 试运行分发交接
T22 原图保护与访问失效
```
## 2 阶段与里程碑
```mermaid
flowchart TB
START["启动准备<br/>人员 OSS 用户接口 真机 iOS 构建与分发条件"]
START --> S1["W1 需求原型<br/>范围 权限 字段 页面 验收"]
START --> S2["W1 至 W2 技术验证<br/>锁屏上传 海外与 CDN 验证 用户同步"]
S1 --> M1["M1 需求基线<br/>W1 结束"]
S2 --> G{"M2 技术可行<br/>W2 结束"}
G -->|未通过| FIX["调整实现或范围<br/>修订工作量与排期"]
FIX --> S2
M1 --> CORE["W3 至 W6 核心链路<br/>国内管理上传 元数据同步 海外可见"]
G -->|通过| CORE
CORE --> M3["M3 端到端可用<br/>W6 结束"]
M3 --> FUN["W7 至 W10 功能建设<br/>协作 队列 大图 互动 浏览统计 多语言"]
FUN --> OPT["W11 至 W14 优化与集成<br/>交接 水印 双向同步 全球 CDN 跨地域测试"]
OPT --> M4["M4 功能齐备<br/>W14 结束"]
M4 --> PILOT["W15 现场试运行<br/>多人操作 弱网 故障恢复 验收"]
PILOT --> M5["M5 试运行通过<br/>W15 结束"]
M5 --> RELEASE["W16 修复与发布<br/>App 分发 监控 备份 回滚 交接"]
RELEASE --> M6["M6 发布交接<br/>W16 结束"]
```
多语言、权限、日志和测试随开发持续进行;W7 至 W10 是完成集成的窗口,并非首次开始这些工作。分发资质从 W1 准备,外部审核等待另行跟踪。
## 3 核心任务依赖
本图突出影响工期的依赖,完整前置条件以项目计划任务表为准。网站组件不依赖点赞模块,可在公开列表可用后推进。
```mermaid
flowchart TB
T01["T01 需求原型"] --> T05["T05 账号与权限"]
T04["T04 用户接口验证"] --> T05
T04 --> T30["T30 定时拉取与后台同步管理"]
T05 --> T30
T30 --> QA
T05 --> T06["T06 相册与协作"]
T03["T03 后台上传验证"] --> T07["T07 OSS 自动发布"]
T06 --> T07
T06 --> T08["T08 App 页面"]
T04 --> T08
T07 --> T09["T09 上传队列"]
T03 --> T09
T09 --> T21["T21 准备与历史"]
T07 --> T10["T10 管理后台"]
T08 --> T20["T20 连拍 控制 交接 水印"]
T10 --> T20
T07 --> T11["T11 观众浏览"]
T11 --> T12["T12 分享下载"]
T12 --> T22["T22 原图与访问保护"]
T07 --> T22
T11 --> T13["T13 实时点赞"]
T13 --> T24["T24 动画体验"]
T11 --> T19["T19 浏览统计"]
T11 --> T14["T14 悬浮组件"]
T14 --> T23["T23 展示位"]
T20 --> QA["T15 语言验收与 T16 全链路验收<br/>覆盖全部功能与 A01 至 A29"]
T21 --> QA
T22 --> QA
T23 --> QA
T24 --> QA
T19 --> QA
T02["T02 工程与部署基础"] --> T17["T17 备份 监控 回滚"]
T07 --> T17
T25["T25 海外 CDN 验证"] --> T26["T26 内容同步"]
T07 --> T26
T26 --> T27["T27 统计回传"]
T19 --> T27
T13 --> T27
T22 --> T28["T28 全球 CDN 接入"]
T25 --> T28
T26 --> QA
T27 --> QA
T28 --> QA
QA --> T29["T29 跨地域验收"]
T29 --> T18["T18 试运行 分发 交接"]
T17 --> T18
```
## 4 验收关口与资源
| 关口 | 必须看到的证据 |
|---|---|
| 技术可行 | 双平台长队列锁屏上传、断网与凭证过期恢复,无重复发布 |
| 核心链路 | 多人从 App 上传,观众页自动显示正确照片 |
| 功能齐备 | 全端多语言、权限、浏览计数、点赞、展示位和新增优化通过 |
| 试运行 | 真实尺寸照片、现场网络、多人操作和故障恢复记录 |
| 发布交接 | 无阻断缺陷,备份恢复和回滚演练完成,明确监控负责人 |
按任务估算上限分配:Flutter 43、后端 65、Web 51、产品设计 11、测试 43、运维 18 人日,合计 231 人日。后端需承担双向同步,Web 需要兼顾三个入口,测试在集成阶段需集中投入;总人日不能直接换算为最短日历工期。
## 5 下一步决策
明确实际人员、首发语言和 App 分发方式;准备现有 OSS 配置、用户接口、真机与 iOS 构建环境;优先执行 T03 与 T04,依据结果细化到人、到周的排期。
+25
View File
@@ -0,0 +1,25 @@
flowchart TB
CAP["拍摄成功或选择照片"] --> PREP["准备中<br/>获取源文件 检查空间 本地复制"]
PREP -->|失败| PF["准备失败<br/>保留原因和恢复入口"]
PF -->|重新获取或释放空间| PREP
PREP -->|成功| QUEUE["持久化队列<br/>固定账号 相册 分类 任务 ID"]
QUEUE --> AUTH{"账号权限与<br/>上传开关有效?"}
AUTH -->|否| WAIT["等待恢复权限或开放上传<br/>也可明确取消任务"]
WAIT -->|条件恢复| AUTH
AUTH -->|是| UP["原生系统传输至 OSS 暂存区"]
UP -->|断网或授权过期| RETRY["保存任务<br/>等待网络或续签后重试"]
RETRY --> AUTH
UP -->|系统中止或强制退出| REOPEN["保留可恢复记录<br/>系统允许或下次启动后校准"]
REOPEN --> AUTH
UP -->|文件传输完成| CONFIRM["服务端核验与幂等确认<br/>固定不可覆盖的正式原图"]
CONFIRM -->|失败| CF["确认失败或待补偿<br/>核验已有对象 不盲目重传"]
CF --> CONFIRM
CONFIRM --> JOB["处理图片与生成浏览资源"]
JOB -->|失败| JF["记录处理失败<br/>有限重试或人工重试"]
JF --> JOB
JOB -->|完成| READY["处理就绪"]
READY --> CHECK{"相册公开且<br/>照片未隐藏删除?"}
CHECK -->|是| SYNC["元数据同步海外<br/>幂等 版本检查 定时补漏"]
SYNC --> LIVE["海外资源可用后可见<br/>通知观众并进入轮播"]
CHECK -->|否| INTERNAL["内部保留<br/>原图继续长期保存"]
INTERNAL -->|公开状态变化后检查| CHECK
+40
View File
@@ -0,0 +1,40 @@
mindmap
root((第一版实施计划))
需求与基础验证
T01 需求权限原型
T02 工程环境与流水线
T03 双平台后台上传验证
T04 用户接口同步验证
账号与核心服务
T05 账号会话和权限
T30 用户定期同步与后台管理
T06 相册分类语言协作
T07 OSS 核验处理发布
Flutter App
T08 登录相册与协作页面
T09 上传队列和记录
T21 准备状态与跨设备历史
后台与活动管理
T10 相册照片用量管理
T20 连拍开关交接水印增量
观众与传播
T11 活动页瀑布流大图
T12 分享下载与浏览器适配
T13 点赞与实时气泡
T19 浏览计数与展示
T24 气泡去重与动画偏好
官网嵌入
T14 JS 悬浮组件
T23 展示位与不可见暂停
海外服务与同步
T25 海外与全球 CDN 验证
T26 内容同步与定时补漏
T27 海外计数与统计回传
T28 全球 CDN 访问策略
T29 跨地域故障验收
质量与发布
T15 全端多语言验收
T16 联调容量与修复
T17 部署备份监控告警
T18 试运行分发交接
T22 原图保护与访问失效
+18
View File
@@ -0,0 +1,18 @@
flowchart TB
START["启动准备<br/>人员 OSS 用户接口 真机 iOS 构建与分发条件"]
START --> S1["W1 需求原型<br/>范围 权限 字段 页面 验收"]
START --> S2["W1 至 W2 技术验证<br/>锁屏上传 海外与 CDN 验证 用户同步"]
S1 --> M1["M1 需求基线<br/>W1 结束"]
S2 --> G{"M2 技术可行<br/>W2 结束"}
G -->|未通过| FIX["调整实现或范围<br/>修订工作量与排期"]
FIX --> S2
M1 --> CORE["W3 至 W6 核心链路<br/>国内管理上传 元数据同步 海外可见"]
G -->|通过| CORE
CORE --> M3["M3 端到端可用<br/>W6 结束"]
M3 --> FUN["W7 至 W10 功能建设<br/>协作 队列 大图 互动 浏览统计 多语言"]
FUN --> OPT["W11 至 W14 优化与集成<br/>交接 水印 双向同步 全球 CDN 跨地域测试"]
OPT --> M4["M4 功能齐备<br/>W14 结束"]
M4 --> PILOT["W15 现场试运行<br/>多人操作 弱网 故障恢复 验收"]
PILOT --> M5["M5 试运行通过<br/>W15 结束"]
M5 --> RELEASE["W16 修复与发布<br/>App 分发 监控 备份 回滚 交接"]
RELEASE --> M6["M6 发布交接<br/>W16 结束"]
+47
View File
@@ -0,0 +1,47 @@
flowchart TB
T01["T01 需求原型"] --> T05["T05 账号与权限"]
T04["T04 用户接口验证"] --> T05
T04 --> T30["T30 定时拉取与后台同步管理"]
T05 --> T30
T30 --> QA
T05 --> T06["T06 相册与协作"]
T03["T03 后台上传验证"] --> T07["T07 OSS 自动发布"]
T06 --> T07
T06 --> T08["T08 App 页面"]
T04 --> T08
T07 --> T09["T09 上传队列"]
T03 --> T09
T09 --> T21["T21 准备与历史"]
T07 --> T10["T10 管理后台"]
T08 --> T20["T20 连拍 控制 交接 水印"]
T10 --> T20
T07 --> T11["T11 观众浏览"]
T11 --> T12["T12 分享下载"]
T12 --> T22["T22 原图与访问保护"]
T07 --> T22
T11 --> T13["T13 实时点赞"]
T13 --> T24["T24 动画体验"]
T11 --> T19["T19 浏览统计"]
T11 --> T14["T14 悬浮组件"]
T14 --> T23["T23 展示位"]
T20 --> QA["T15 语言验收与 T16 全链路验收<br/>覆盖全部功能与 A01 至 A29"]
T21 --> QA
T22 --> QA
T23 --> QA
T24 --> QA
T19 --> QA
T02["T02 工程与部署基础"] --> T17["T17 备份 监控 回滚"]
T07 --> T17
T25["T25 海外 CDN 验证"] --> T26["T26 内容同步"]
T07 --> T26
T26 --> T27["T27 统计回传"]
T19 --> T27
T13 --> T27
T22 --> T28["T28 全球 CDN 接入"]
T25 --> T28
T26 --> QA
T27 --> QA
T28 --> QA
QA --> T29["T29 跨地域验收"]
T29 --> T18["T18 试运行 分发 交接"]
T17 --> T18
+25
View File
@@ -0,0 +1,25 @@
# 图片直播项目导图
版本:V1.3 · 日期:2026-09-17
与上级目录的 V1.3 文档同步,包含三份导图文档、七张图及对应 Mermaid 源文件。
| 导图文档 | 内容 |
|---|---|
| [项目全景](./01-项目全景.md) | 功能思维导图、系统组成与数据关系 |
| [业务流程](./02-业务流程.md) | 活动全流程、上传与异常恢复 |
| [实施计划](./03-实施计划.md) | 30 项工作分解、16 周阶段里程碑、关键任务依赖 |
支持 Mermaid 的 Markdown 查看器可直接渲染文档中的图;同目录的 `.mmd` 文件可在 Mermaid 编辑工具中单独打开或转换为图片。普通文本查看器会显示可编辑源码。
## 单图源文件
- [项目功能思维导图](./01-project-mindmap.mmd)
- [系统组成图](./02-system-overview.mmd)
- [业务全流程图](./03-business-flow.mmd)
- [上传与异常恢复图](./04-upload-recovery.mmd)
- [工作分解思维导图](./05-work-breakdown.mmd)
- [阶段与里程碑图](./06-milestones.mmd)
- [核心任务依赖图](./07-task-dependencies.mmd)
范围来源:[需求说明书](../01-需求说明书.md)、[项目计划](../02-项目计划.md)、[技术实施方案](../03-技术实施方案.md)、[优化方案](../04-优化方案.md)。导图不代表任务已经完成,16 周为估算基线。
@@ -0,0 +1,649 @@
# P1a 后端核心链路 实施计划
> **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:** 搭建 monorepo 工程底座并交付国内单地域后端核心链路:密码登录 → 建相册(多语言/成员/分类)→ 上传会话(幂等、临时凭证)→ 完成核验 → worker 图片处理 → 自动发布 → 公开读 API(游标分页、语言回退、短期签名 URL),全程 API 级 E2E 验证。
**Architecture:** NestJS 单代码库(P1a 仅 cn-app 部署入口)+ Prisma(MySQL) + BullMQ/Redis + sharp worker;对象存储经 StoragePort 接口访问 MinIO(生产替换 OSS 适配器);契约(OpenAPI + 事件 + 错误码)在 packages/contracts 单源生成类型。对齐《实施总纲》P1 与技术实施方案 §3-§7。
**Tech Stack:** Node 20 · pnpm 9 · Turborepo · NestJS 10 · Prisma ^5.18 (MySQL 8.4 容器) · BullMQ 5 · Redis 7 · @aws-sdk/client-s3(MinIO S3 兼容)· sharp · @node-rs/argon2 · Jest + supertest · OpenAPI 3.1 + openapi-typescript
**对应验收:** A01(API 级)、A02、A03、A06、A13(凭证设计级);A07 后端部分。P1a 完成后另册编写 P1b(viewer + admin 最小页)计划。
---
## 前置确认(用户关口,开工前登记)
- [ ] D01 首发语言:按建议值 zh-CN、en 开发(不确认则默认执行)
- [x] D12 生产 MySQL 版本:已确认 8.0+(2026-09-17);本地容器用 MySQL 8.4 对齐,迁移仅使用 8.0 兼容特性。**注意**:库级默认校对 `utf8mb4_0900_bin` 需 MySQL ≥ 8.0.30——部署前在验收记录登记两台 ECS 的精确小版本;若低于 8.0.30 则回退 `utf8mb4_bin`(同样大小写敏感)并重跑语义测试。
- [ ] 本机环境:`node -v` ≥ 20、`pnpm -v` ≥ 9、`docker -v` 与 `docker compose version` 可用(Windows + Docker Desktop)
---
## 里程碑 M0 工程底座(T02)
### Task 1: git 仓库与目录骨架
**Files:**
- Create: `.gitignore`, `.editorconfig`, `.nvmrc`, `README.md`, `docs/plans/progress/.gitkeep`
- [ ] **Step 1: 初始化仓库与目录**
```bash
cd /d/project/photo
git init -b main
mkdir -p apps packages services infra docs/plans/progress
```
`.gitignore`:
```gitignore
node_modules/
dist/
.turbo/
coverage/
.env
.env.*
!.env.example
*.log
.DS_Store
```
`.nvmrc`:`20`
- [ ] **Step 2: 首次提交**
```bash
git add -A && git commit -m "chore: init repository skeleton"
```
Expected: 首个提交生成,主干 main。
### Task 2: pnpm workspace + Turborepo
**Files:**
- Create: `package.json`, `pnpm-workspace.yaml`, `turbo.json`
- [ ] **Step 1: 写入三个根配置**
`pnpm-workspace.yaml`:
```yaml
packages:
- apps/*
- packages/*
- services/*
```
`package.json`:
```json
{
"name": "photo-live",
"private": true,
"packageManager": "pnpm@9.12.0",
"engines": { "node": ">=20" },
"scripts": {
"lint": "turbo run lint",
"typecheck": "turbo run typecheck",
"test": "turbo run test",
"build": "turbo run build",
"db:migrate": "dotenv -e .env -- pnpm --filter @photo/db-cn exec prisma migrate dev",
"db:generate": "dotenv -e .env -- pnpm --filter @photo/db-cn exec prisma generate",
"db:studio": "dotenv -e .env -- pnpm --filter @photo/db-cn exec prisma studio",
"dev:api": "dotenv -e .env -- pnpm --filter @photo/api-cn start",
"dev:worker": "dotenv -e .env -- pnpm --filter @photo/worker start",
"e2e": "turbo run test:e2e"
},
"devDependencies": {
"turbo": "^2.3.0",
"typescript": "^5.6.0",
"prettier": "^3.3.0",
"dotenv-cli": "^7.4.0",
"eslint": "^9.0.0",
"typescript-eslint": "^8.0.0"
}
}
```
`turbo.json`:
```json
{
"tasks": {
"build": { "dependsOn": ["^build"], "outputs": ["dist/**"] },
"lint": {},
"typecheck": { "dependsOn": ["^build"] },
"test": { "dependsOn": ["^build"] },
"test:e2e": { "dependsOn": ["^build"] }
}
}
```
- [ ] **Step 2: 验证 workspace**
```bash
pnpm install
```
Expected: lockfile 生成,无 workspace 报错。
- [ ] **Step 3: Commit** `chore: pnpm workspace and turbo pipeline`
### Task 3: docker-compose 基础设施(cn 侧)
**Files:**
- Create: `infra/docker-compose.yml`, `.env.example`
- [ ] **Step 1: 编写 compose(P1a 仅 cn profile)**
```yaml
# infra/docker-compose.yml
name: photo-live
services:
mysql-cn:
image: mysql:8.4
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_bin
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: photo_cn
ports: ["3306:3306"]
volumes: [mysql-cn-data:/var/lib/mysql]
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-uroot", "-proot"]
interval: 5s
retries: 12
redis-cn:
image: redis:7-alpine
ports: ["6379:6379"]
healthcheck: { test: ["CMD", "redis-cli", "ping"], interval: 5s, retries: 12 }
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: photo-dev
MINIO_ROOT_PASSWORD: photo-dev-secret
ports: ["9000:9000", "9001:9001"]
volumes: [minio-data:/data]
minio-init:
image: minio/mc:latest
depends_on: { minio: { condition: service_started } }
entrypoint: >
/bin/sh -c "
until mc alias set dev http://minio:9000 photo-dev photo-dev-secret; do sleep 1; done;
mc mb -p dev/photo-dev || true;
mc anonymous set none dev/photo-dev;"
volumes:
mysql-cn-data:
minio-data:
```
说明:collation 取 `utf8mb4_0900_bin`(大小写敏感、二进制安全)作为**库级默认**,直接化解 token 大小写语义差异(总纲 PG→MySQL 复核项);业务文本列如需不区分大小写再按列显式设定。本机端口冲突(3306/6379/9000/9001 已被占用,Windows 常见本机 MySQL 服务)时改宿主侧映射并同步 `.env`。
`.env.example`(仓库根):
```env
DATABASE_URL_CN=mysql://root:root@localhost:3306/photo_cn?connection_limit=10
REDIS_URL_CN=redis://localhost:6379
S3_ENDPOINT=http://localhost:9000
S3_REGION=us-east-1
S3_ACCESS_KEY=photo-dev
S3_SECRET_KEY=photo-dev-secret
S3_BUCKET=photo-dev
SIGNED_URL_TTL_SECONDS=900
AUTH_SESSION_TTL_SECONDS=2592000
UPLOAD_MAX_BYTES=52428800
```
- [ ] **Step 2: 启动并验证**
```bash
cd infra && docker compose up -d && docker compose ps
```
Expected: mysql-cn / redis-cn / minio / minio-init 均 healthy 或 exited(0)(init 为一次性任务)。
- [ ] **Step 3: Commit** `chore(infra): cn-side mysql/redis/minio compose`
---
## 里程碑 M1 契约(packages/contracts)
### Task 4: contracts 包骨架 + 错误码 + OpenAPI 基座
**Files:**
- Create: `packages/contracts/package.json`, `packages/contracts/tsconfig.json`, `packages/contracts/openapi.yaml`, `packages/contracts/src/errors.ts`, `packages/contracts/src/index.ts`
- [ ] **Step 1: 包配置**
`package.json`(name `@photo/contracts`,scripts: `gen`、`lint:api`、`build`、`typecheck`、`test`)依赖 `openapi-typescript@^7`、`@redocly/cli@^1` 作为 devDeps;`src/index.ts` 导出 errors 与生成类型。
- [ ] **Step 2: 错误码(稳定 code,客户端按语言映射文案)**
```ts
// packages/contracts/src/errors.ts
export const ErrorCodes = {
AUTH_INVALID_CREDENTIALS: 'AUTH_INVALID_CREDENTIALS',
AUTH_TOKEN_EXPIRED: 'AUTH_TOKEN_EXPIRED',
AUTH_FORBIDDEN: 'AUTH_FORBIDDEN',
ALBUM_NOT_FOUND: 'ALBUM_NOT_FOUND',
ALBUM_NOT_PUBLIC: 'ALBUM_NOT_PUBLIC',
MEMBER_CONFLICT: 'MEMBER_CONFLICT',
ALBUM_UPLOAD_DISABLED: 'ALBUM_UPLOAD_DISABLED',
UPLOAD_SESSION_CONFLICT: 'UPLOAD_SESSION_CONFLICT',
UPLOAD_VERIFY_FAILED: 'UPLOAD_VERIFY_FAILED',
PHOTO_NOT_FOUND: 'PHOTO_NOT_FOUND',
PHOTO_NOT_PUBLIC: 'PHOTO_NOT_PUBLIC',
VALIDATION_FAILED: 'VALIDATION_FAILED',
INTERNAL_ERROR: 'INTERNAL_ERROR',
} as const;
export type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes];
```
- [ ] **Step 3: OpenAPI 基座**(3.1;`info.title=photo-live`;servers `/v1`;components: `Error{code,message,requestId}`、`CursorPage{items,nextCursor}`;securitySchemes: bearerAuth)。路径骨架先落 auth 三条 + 错误响应复用。
- [ ] **Step 4: 类型生成脚本验证**
`pnpm --filter @photo/contracts gen && pnpm --filter @photo/contracts lint:api`
Expected: `src/generated/openapi.d.ts` 生成;redocly 0 错误。
- [ ] **Step 5: Commit** `feat(contracts): openapi base, error codes, type generation`
### Task 5: 业务端点契约(albums/members/categories/uploads/photos/public)
**Files:**
- Modify: `packages/contracts/openapi.yaml`
- [ ] **Step 1: 补全 P1a 路径与 schema**(字段以技术实施方案 §4/§10 为准):
- `POST /albums`(title 多语言必填、时间、时区默认 Asia/Shanghai、地点、开关默认值 upload/public=true、download=true、original=false、defaultLocale)
- `GET /albums`、`GET /albums/{id}`、`PATCH /albums/{id}`(仅创建者/管理员可写;开关字段独立)
- `POST/GET/DELETE /albums/{id}/members`;`POST /albums/{id}/categories` + `PATCH/DELETE /categories/{id}`
- `POST /uploads`:请求 `{albumId, categoryId?, clientTaskId, filename, bytes, contentType}`,响应 `{uploadId, photoId, objectKey, credential:{mode,url,method,headers,expiresAt}}`;`POST /uploads/{id}/renew`;`POST /uploads/{id}/complete` → `{photoId, processingState}`;`GET /uploads`
- `PATCH /photos/{id}`(`{visibility: hidden|published}`)、`DELETE /photos/{id}`(软删除)
- `GET /public/albums/{token}`( translations 按请求 locale 回退、categories、assets、`viewCount: null` 占位、timezone、startAt/endAt ISO)
- `GET /public/albums/{token}/photos`(query: category、locale、cursor、limit≤60;item 含 `photoId,width,height,thumb{url,expiresAt},browse{url,expiresAt},likeCount,publishedAt`)
- `GET /public/albums/{token}/photos/{photoId}`(单图定位;同时作为签名 URL 过期后的刷新入口)
- [ ] **Step 2: 生成 + lint 通过**(同 Task 4 命令)
- [ ] **Step 3: Commit** `feat(contracts): p1a endpoint contracts`
### Task 6: 事件契约 + 用户确认点
**Files:**
- Create: `packages/contracts/src/events.ts`
- [ ] **Step 1: 事件类型**(对齐技术实施方案 §8.2;P1a 无消费者,事件仅持久化到 outbox,实际会产生 photo.published / photo.hidden / photo.deleted 三类)
```ts
export interface OutboxEvent<T = unknown> {
eventId: string; // cuid,持久唯一
type: 'photo.published' | 'photo.hidden' | 'photo.deleted' | 'photo.likes.updated' | 'album.updated';
albumId: string;
photoId?: string;
entityVersion: number; // 单调递增,P2 inbox 消费依据
payload: T;
occurredAt: string; // ISO UTC
}
```
- [ ] **Step 2: 单测**:类型测试——合法 `OutboxEvent{type:'photo.published'}` 编译通过,非法 type 用 `@ts-expect-error` 验证编译期拒绝。
```bash
pnpm --filter @photo/contracts test
```
Expected: PASS。
- [ ] **Step 3: ⏸ 用户确认点:请用户过目 openapi.yaml(接口形态、字段命名、分页方式)后再进入 M2。**
- [ ] **Step 4: Commit** `feat(contracts): outbox event contract types`
---
## 里程碑 M2 服务骨架与数据层
### Task 7: NestJS api 骨架、测试与 Lint 工具链、env 加载约定
**Files:**
- Create: `services/api/package.json`(name `@photo/api-cn`)、`tsconfig.json`、`nest-cli.json`、`src/main-cn.ts`、`src/app-cn.module.ts`、`src/health.controller.ts`、`src/request-id.middleware.ts`、`src/all-exceptions.filter.ts`、`services/api/jest.config.ts`、`services/api/test/load-env.ts`、`services/api/test/reset.ts`、根 `eslint.config.js`
- [ ] **Step 1: env 加载约定(全仓库唯一机制,解决 pnpm --filter cwd 在包目录的问题)**:根 `.env` 为唯一 env 文件。开发/迁移脚本经根 `dotenv-cli` 注入(Task 2 scripts 已配置);Jest 经 `services/api/test/load-env.ts`(`dotenv.config({ path: join(__dirname, '../../../.env') })`)作为 `setupFiles` 载入;运行时代码只读 `process.env`。
- [ ] **Step 2: 依赖与入口**:deps `@nestjs/common @nestjs/core @nestjs/platform-express nest-pino pino-http`;devDeps `jest ts-jest @types/jest supertest @types/supertest @nestjs/testing`;scripts:`test`(`*.spec.ts` 单测)、`test:e2e`(`test/*.e2e-spec.ts`,前置 compose 依赖)、`typecheck`、`lint`、`start`。`test/reset.ts` 为 e2e 共用隔离工具:每个 e2e spec `beforeAll` 调用——truncate 全部 cn 表 + 删除 MinIO `photo-live/dev` 前缀对象(防止 spec 间唯一约束冲突与脏数据)。入口监听 `PORT`(默认 8080),`app.setGlobalPrefix('v1')`,注册 request-id 中间件(无则生成 cuid,挂 `res.setHeader('x-request-id')`)、全局异常过滤器(映射 `ErrorCodes`,响应 `{code,message,requestId}`,不泄露堆栈)、`/v1/health` 返回 `{status:'ok'}`。
- [ ] **Step 3: 根 ESLint flat config**(eslint + typescript-eslint,覆盖 packages/services/apps),各包(contracts、storage、db-cn、api、worker)`lint` 脚本接 `eslint .`,确保 turbo 不静默跳过。ts-jest 保持类型检查模式(非 isolatedModules),使 `@ts-expect-error` 负向断言生效。
- [ ] **Step 4: 启动验证**
```bash
cp .env.example .env
pnpm lint && pnpm typecheck
pnpm dev:api &
until curl -s localhost:8080/v1/health; do sleep 1; done # 等待启动 → {"status":"ok"}
```
- [ ] **Step 5: Commit** `feat(api): cn-app skeleton, toolchain, env loading convention`
### Task 8: Prisma cn schema —— 身份与审计域
**Files:**
- Create: `packages/db-cn/package.json`(name `@photo/db-cn`;deps `@prisma/client`,devDeps `prisma`;api 与 worker 共享此 client,解决 worker 访问 cn Prisma 的归属问题)、`packages/db-cn/prisma/schema.prisma`(cn schema)、`packages/db-cn/src/index.ts`(导出 PrismaClient)、`services/api/src/prisma/prisma.module.ts`(封装 @photo/db-cn 供注入)
- Test: `services/api/test/db.semantics.e2e-spec.ts`
- [ ] **Step 1: 模型**(MySQL;时间 `@db.DateTime(6)`;ID cuid;账号字段唯一;连接池上限复核项由 `DATABASE_URL_CN` 的 `?connection_limit=10` 承担,PrismaService 不另行放大):
```prisma
model users {
id String @id @default(cuid())
account String @unique @db.VarChar(64)
passwordHash String @db.VarChar(255)
displayName String @db.VarChar(128)
role String @default("staff") // staff|admin(公司角色;相册角色另表)
sourceStatus String @default("local") // local|active|disabled(P4a 同步用)
localLocked Boolean @default(false)
credentialStatus String @default("active")
locale String? @db.VarChar(16)
createdAt DateTime @default(now()) @db.DateTime(6)
sessions sessions[]
auditLogs audit_logs[]
}
model sessions {
id String @id @default(cuid())
userId String
tokenHash String @unique @db.VarChar(64)
expiresAt DateTime @db.DateTime(6) // 长效会话期限(默认 30 天,AUTH_SESSION_TTL_SECONDS 可调;不做短时效 access/refresh 双层——用户决策 2026-09-17)
revokedAt DateTime? @db.DateTime(6) // 注销/停用撤销
user users @relation(fields: [userId], references: [id])
}
model audit_logs {
id String @id @default(cuid())
actorId String?
action String @db.VarChar(64)
resourceId String? @db.VarChar(64)
metadata Json?
createdAt DateTime @default(now()) @db.DateTime(6)
actor users? @relation(fields: [actorId], references: [id])
}
```
- [ ] **Step 2: 迁移**
```bash
pnpm db:migrate -- --name init_identity
```
Expected: migration SQL 生成并应用。
- [ ] **Step 3: 写语义复核 E2E 测试**(TDD:先写测试)——UTC 往返(写入+读回同为 UTC 瞬间)、`utf8mb4_0900_bin` 下 `public_token` 大小写敏感查询、audit 插入。运行 `pnpm --filter @photo/api-cn test:e2e`,Expected: PASS。
- [ ] **Step 4: Commit** `feat(api): identity and audit schema with mysql semantics tests`
### Task 9: Prisma cn schema —— 相册域
**Files:**
- Modify: `packages/db-cn/prisma/schema.prisma`
- [ ] **Step 1: 模型**:`albums`(publicToken `@unique @db.VarChar(32)`、ownerId、startAt/endAt、timezone、status active|offline、defaultLocale、uploadEnabled/publicEnabled/downloadEnabled/originalDownloadEnabled 默认 true/true/true/false、watermarkVersion 默认 0)、`album_translations`(`@@unique([albumId, locale])`,title/description/venue/address)、`album_members`(`@@unique([albumId, userId])`,role owner|collaborator,status)、`categories`(albumId、sortOrder)+ `category_translations`(`@@unique([categoryId, locale])`)、`album_assets`(kind banner|cover|logo、locale?、objectKey)。
- [ ] **Step 2: 迁移** `--name album_domain`;Step 3: Commit `feat(api): album domain schema`
### Task 10: Prisma cn schema —— 照片与上传域 + outbox
**Files:**
- Modify: `packages/db-cn/prisma/schema.prisma`
- [ ] **Step 1: 模型**:
```prisma
model photos {
id String @id @default(cuid())
albumId String
uploaderId String
categoryId String?
objectKey String @unique @db.VarChar(255) // originals 正式键
bytes Int @default(0)
width Int?
height Int?
shotAt DateTime? @db.DateTime(6)
uploadedAt DateTime? @db.DateTime(6)
publishedAt DateTime? @db.DateTime(6)
processingState String @default("pending_upload") // pending_upload|uploaded|processing|ready|processing_failed
errorCode String? @db.VarChar(64)
visibility String @default("unpublished") // unpublished|published|hidden|deleted
likeCount Int @default(0) // P2 起为统计副本
entityVersion Int @default(1)
@@index([albumId, visibility, publishedAt, id])
@@index([albumId, uploaderId, uploadedAt])
}
model photo_variants {
photoId String
variant String // thumb|browse
objectKey String @unique @db.VarChar(255)
bytes Int
version Int @default(1)
width Int?
height Int?
@@unique([photoId, variant])
}
model upload_sessions {
id String @id @default(cuid())
userId String
albumId String
clientTaskId String @db.VarChar(128)
photoId String @unique
objectKey String @unique @db.VarChar(255) // staging 键
expectedBytes Int
contentType String @db.VarChar(128)
multipartId String?
state String @default("pending_upload") // pending_upload|uploaded|confirmed|failed
expiresAt DateTime @db.DateTime(6)
createdAt DateTime @default(now()) @db.DateTime(6)
@@unique([userId, clientTaskId]) // 幂等锚点(A03)
}
model photo_jobs {
id String @id @default(cuid())
photoId String
type String @default("process")
state String @default("pending") // pending|active|done|failed
attempt Int @default(0)
errorCode String? @db.VarChar(64)
nextRetryAt DateTime? @db.DateTime(6)
updatedAt DateTime @updatedAt @db.DateTime(6)
@@unique([photoId, type])
@@index([state, nextRetryAt])
}
model outbox_events {
id String @id @default(cuid())
type String @db.VarChar(64)
albumId String @db.VarChar(64)
photoId String? @db.VarChar(64)
entityVersion Int
payload Json
occurredAt DateTime @default(now()) @db.DateTime(6)
deliveredAt DateTime? @db.DateTime(6)
@@index([deliveredAt, occurredAt])
}
```
- [ ] **Step 2: 迁移** `--name photo_upload_domain`;Step 3: Commit `feat(api): photo, upload, outbox schema`
### Task 11: StoragePort 接口 + MinIO 适配器
**Files:**
- Create: `packages/storage/package.json`、`src/port.ts`、`src/minio.adapter.ts`、`src/index.ts`
- Test: `packages/storage/test/minio.adapter.spec.ts`(真实 MinIO 集成测试)
- [ ] **Step 1: 端口定义**(生产 OSS 适配器实现同一接口,G1 验证差异)
```ts
export interface UploadCredential { mode: 'presigned'; url: string; method: 'PUT'; headers: Record<string,string>; expiresAt: string; }
export interface StoragePort {
presignPut(objectKey: string, bytes: number, contentType: string, ttlSeconds: number): Promise<UploadCredential>;
presignGet(objectKey: string, ttlSeconds: number): Promise<string>;
head(objectKey: string): Promise<{ bytes: number; etag: string } | null>;
getStream(objectKey: string): Promise<NodeJS.ReadableStream>;
put(objectKey: string, body: Buffer, contentType: string): Promise<{ etag: string }>;
/** 复制到正式键;目标已存在时校验一致性并幂等返回(禁止覆盖语义)。 */
copyIfAbsent(src: string, dest: string, expectedBytes: number): Promise<{ created: boolean; etag: string }>;
}
```
- [ ] **Step 2: MinIO 实现**:`@aws-sdk/client-s3` + `s3-request-presigner`;`copyIfAbsent` = head(dest) 存在→比对 bytes 一致返回 `{created:false}`、不一致抛 `UPLOAD_VERIFY_FAILED`;不存在→`CopyObject` 后复核。测试覆盖:presign PUT 可写入、head 字节数、copyIfAbsent 幂等(二次调用 created:false)、目标不一致抛错。
- [ ] **Step 3: 运行** `pnpm --filter @photo/storage test`(compose 已起 MinIO)。Expected: PASS。
- [ ] **Step 4: Commit** `feat(storage): storage port and minio adapter with forbid-overwrite semantics`
---
## 里程碑 M3 身份与权限(T05 后端)
### Task 12: 密码哈希与受控初始管理员
**Files:**
- Create: `services/api/src/auth/password.ts`、`services/api/scripts/seed-admin.ts`
- Test: `services/api/test/password.spec.ts`
- [ ] **Step 1: 失败测试**:hash/verify(@node-rs/argon2,memoryCost 默认)、错误口令 verify=false。
- [ ] **Step 2: 实现 + seed 脚本**(读 env `ADMIN_ACCOUNT/ADMIN_PASSWORD`,不存在则插入 admin 角色,存在则跳过——幂等)。
- [ ] **Step 3: 测试通过;Commit** `feat(api): argon2 password and idempotent admin seed`
### Task 13: 登录 / 刷新 / 注销
**Files:**
- Create: `services/api/src/auth/auth.controller.ts`、`auth.service.ts`、`auth.module.ts`、`session.guard.ts`
- Test: `services/api/test/auth.e2e-spec.ts`
- [ ] **Step 1: 失败 E2E**:正确账号密码→200 + sessionToken(长效单 token,默认 30 天)+ expiresAt + audit `auth.login`;错误密码→401 `AUTH_INVALID_CREDENTIALS`;停用(sourceStatus=disabled 或 localLocked)→401(不区分文案);**未激活(credentialStatus=pending)→401**(A01"未激活与停用账号无法进入");refresh→滑动续期(持有有效 token 延长 expiresAt,仍为同一 token,不做旋转);logout→revokedAt 后 401 `AUTH_TOKEN_EXPIRED`。
- [ ] **Step 2: 实现**:token=32B random hex,库存 sha256;会话期限由 `AUTH_SESSION_TTL_SECONDS`(默认 30 天)控制;登录时校验 `credentialStatus='active'`;`SessionGuard` 校验 tokenHash 未撤销未过期,挂 `req.user`。
- [ ] **Step 3: E2E 通过(A01 API 级);Commit** `feat(api): password login, sliding renewal, revocable sessions`
### Task 14: 相册角色权限服务(A02 基石)
**Files:**
- Create: `services/api/src/albums/permissions.service.ts`
- Test: `services/api/test/permissions.spec.ts`
- [ ] **Step 1: 失败测试**:`canViewAlbum/canManageAlbum/canManagePhoto`——admin 全通过;创建者管本相册、不能管他人相册;协作者只能传/管自己照片;非成员拒绝;被移除成员(members 无行)拒绝(逐次查库,不信客户端声明)。
- [ ] **Step 2: 实现**(方法内部 join members+albums+photos.uploaderId 校验)。
- [ ] **Step 3: 通过;Commit** `feat(api): server-side album role permission checks`
---
## 里程碑 M4 相册与协作(T06)
### Task 15: 相册创建与多语言
**Files:**
- Create: `services/api/src/albums/albums.controller.ts`、`albums.service.ts`、`albums.module.ts`(含 translations/categories 模型引用)
- Test: `services/api/test/albums.e2e-spec.ts`
- [ ] **Step 1: 失败 E2E**:POST /albums 带 zh-CN+en translations → 201 返回 publicToken(`^[0-9a-z]{20,32}$`);title 缺失→`VALIDATION_FAILED`;endAt<startAt→400;owner 自动入 album_members(role=owner) + audit。
- [ ] **Step 2: 实现**(事务:albums + translations + members(owner) + audit)。
- [ ] **Step 3: 通过;Commit** `feat(api): album creation with translations and owner membership`
### Task 16: 相册查询 / 更新与开关
- [ ] **Step 1: 失败 E2E**:创建者 PATCH 改 title/开关→200 且 `uploadEnabled/publicEnabled` 独立变化;协作者 PATCH→403 `AUTH_FORBIDDEN`(A02);GET 列表仅返回自己参与或 admin 全量。
- [ ] **Step 2: 实现(配置变更写 audit `album.update`);通过;Commit** `feat(api): album update with independent upload/public switches`
### Task 17: 成员与分类管理
- [ ] **Step 1: 失败 E2E**:创建者邀请已存在账号→201;重复邀请→`MEMBER_CONFLICT`;移除后该成员 complete 上传→403(Task 21 回归);分类增改排序→列表按 sortOrder。
- [ ] **Step 2: 实现(成员变更写 audit);通过;Commit** `feat(api): members and categories management`
---
## 里程碑 M5 上传与自动发布(T07)
### Task 18: POST /v1/uploads 幂等初始化
**Files:**
- Create: `services/api/src/uploads/uploads.controller.ts`、`uploads.service.ts`、`uploads.module.ts`
- Test: `services/api/test/uploads.e2e-spec.ts`
- [ ] **Step 1: 失败 E2E**(A03):同 `clientTaskId` 两次调用→**同 uploadId+photoId**(upsert 语义,非 409);不同 clientTaskId 同 filename→两个会话两个 photoId;非成员→403;`uploadEnabled=false`→409 `ALBUM_UPLOAD_DISABLED`;`bytes > UPLOAD_MAX_BYTES`(默认 50MB)→`VALIDATION_FAILED`;`contentType` ∉ {image/jpeg, image/png, image/heic}(heic 解码能力待 G2 验证标注)→`VALIDATION_FAILED`;响应含有效 presign credential(用 StoragePort 实际签名)。
- [ ] **Step 2: 实现**:入参校验(bytes 上限、类型白名单、相册上传开关与成员权)→ 事务内 upsert(`@@unique([userId, clientTaskId])` 冲突→查回既有会话且校验 album/category 一致,不一致→`UPLOAD_SESSION_CONFLICT`);staging 键 = `photo-live/dev/staging/{uploadId}/{photoId}.{ext}`(会话 objectKey);photo 预创建(visibility=unpublished),其 `objectKey` 在 init 即写为最终 originals 键 `photo-live/dev/albums/{albumId}/originals/{photoId}.{ext}`(staging 与正式键分离,杜绝二义)。
- [ ] **Step 3: 通过;Commit** `feat(api): idempotent upload session init with presigned credential`
### Task 19: 续签与完成核验(含受控复制)
- [ ] **Step 1: 失败 E2E**:`renew` 返回新 presign(同 objectKey);`renew` 在 `uploadEnabled=false`→409 `ALBUM_UPLOAD_DISABLED`(§4.9 关闭上传拒绝续签);被移除成员 `renew`→403;`complete`:
- 对象未上传→404/400 `UPLOAD_VERIFY_FAILED`;
- 字节数不符→`UPLOAD_VERIFY_FAILED`;
- 成功→photo(uploadedAt、bytes) + photo_jobs(pending) + session(confirmed) 同事务;
- **重复 complete(A03)**→200 幂等,不产生第二个 photo/job/outbox;
- 被移除成员 complete→403;
- `GET /uploads` 返回本人会话历史(state 联查 photo.processingState,App 服务端历史的后端前置)。
- [ ] **Step 2: 实现**:head(staging) 校验 bytes → `copyIfAbsent(staging→originals:{albumId}/originals/{photoId}.ext)` → 事务更新三表 → enqueue BullMQ(队列名常量 `PHOTO_PROCESS` 由 `@photo/contracts` 导出,api 与 worker 共享;提交后入队,队列不可用不回滚,靠 Task 21 sweep 补偿)。
- [ ] **Step 3: 通过;Commit** `feat(api): upload verify, forbid-overwrite copy, idempotent complete`
### Task 20: worker 服务与 sharp 处理流水线
**Files:**
- Create: `services/worker/package.json`(name `@photo/worker`)、`src/main.ts`、`src/process.processor.ts`、`src/pipeline.ts`、`src/sweep.ts`
- Test: `services/worker/test/pipeline.spec.ts`(真实 jpeg/png 样本 + 一张损坏文件 + 一张 HEIC 样本记录结果)
- [ ] **Step 1: 失败测试**:pipeline(buffer)→输出 thumb(≤480w) 与 browse(≤1920w) 两 Buffer,EXIF 已清(无 GPS/ orientations 元数据)、宽高返回;损坏 buffer→`DECODE_FAILED`;HEIC 样本(真实 iPhone 样本置于 `services/worker/test/fixtures/sample.heic`,暂缺时该用例 skip 并登记验收记录,不阻塞)→若 sharp 可解码则通过否则标记 `UNSUPPORTED_TYPE` 并**登记到验收记录**(总纲风险项)。
- [ ] **Step 2: 实现**:worker package 依赖 `@photo/db-cn`(与 api 共享同一 Prisma client);`sharp(buf).rotate()` 自动方向 → `resize({width, withoutEnlargement:true}).jpeg({quality})`(thumb q75 / browse q82,不调用 withMetadata 即清 EXIF)。
- [ ] **Step 3: 通过;Commit** `feat(worker): sharp pipeline with orientation, variants, exif strip`
### Task 21: 处理消费者、发布事务与补偿扫描
**Files:**
- Create: `services/worker/src/publish.service.ts`
- Test: `services/worker/test/publish.e2e-spec.ts`(真实 MySQL/Redis/MinIO;worker package scripts 含 `test` 与 `test:e2e`,后者纳入根 `pnpm e2e` 的 turbo 任务)
- [ ] **Step 1: 失败 E2E**(A06):photo_jobs(pending) → 消费者处理:variants put 上传 → **单事务**:photo_variants upsert + photos(ready, published, publishedAt=now, entityVersion+1) + outbox_events(photo.published, entityVersion 同步) → 断言三表一致且事件 payload 符合 contracts/events.ts;重复投递(同 photoId)→ 幂等无二套 variants;损坏文件→3 次退避后 `processing_failed` 且不公开。
- [ ] **Step 2: 实现**:BullMQ `attempts:3, backoff:exponential`;`sweep.ts` 每 30s 认领 `state='pending' AND (nextRetryAt IS NULL OR nextRetryAt < now())`(SQL NULL 语义;含 complete 后入队失败的孤儿任务,总纲 §5.6 补偿)。
- [ ] **Step 3: 通过;Commit** `feat(worker): publish transaction with outbox event and compensating sweep`
---
## 里程碑 M6 公开读与可见性(T11 部分 + V4)
### Task 22: 相册公开信息与语言回退
**Files:**
- Create: `services/api/src/public/public.controller.ts`、`public.service.ts`、`public.module.ts`
- Test: `services/api/test/public.e2e-spec.ts`
- [ ] **Step 1: 失败 E2E**:`GET /public/albums/{token}` locale=en 但缺 en 翻译→回退 defaultLocale 且响应携带 `resolvedLocale`;offline 或 publicEnabled=false→404 `ALBUM_NOT_PUBLIC`;返回 categories(名称同样回退)、banner 签名 URL(fixture:直接经 Prisma/StoragePort 预置 album_assets 行 + MinIO 对象——上传链路在 P1b)、timezone、`viewCount:null`。
- [ ] **Step 2: 实现;通过;Commit** `feat(api): public album info with locale fallback`
### Task 23: 公开照片列表(游标分页 + 分类过滤 + 签名 URL)
- [ ] **Step 1: 失败 E2E**:seed 35 张已发布→limit=30 首页 30 条 + nextCursor;翻页得余 5 条且无重复(跨页稳定,A07 后端级);`category` 过滤正确;仅 `visibility=published`(hidden/deleted 不出现);item 含 thumb/browse 签名 URL 与 `expiresAt`;同秒发布的多张排序按 id 稳定。
- [ ] **Step 2: 实现**:cursor=base64(`{publishedAt,id}`),WHERE `(publishedAt,id) < (cursor)` 语义(MySQL 行值比较或等价 OR 条件,D12 复核项)。
- [ ] **Step 3: 通过;Commit** `feat(api): public photo feed with stable cursor pagination`
### Task 24: 单图定位 / 隐藏 / 软删除
- [ ] **Step 1: 失败 E2E**:单图路由命中已发布照片→200(并作为 URL 刷新入口返回新签名);隐藏后单图与列表均不可见(404 `PHOTO_NOT_PUBLIC`,本地化文案键);uploader 隐藏自己照片 OK、协作者隐藏他人照片→403(A02);软删除后不可见;隐藏/删除写 audit;outbox 追加 `photo.hidden`/`photo.deleted` 事件(P2 消费);管理员/创建者重新公开(hidden→published)→ 列表恢复可见(§5.5 状态机;协作者能否自行恢复属需求待确认项,P1a 默认拒绝并登记)。
- [ ] **Step 2: 实现;通过;Commit** `feat(api): photo visibility lifecycle with audit and outbox events`
---
## 里程碑 M7 端到端与验收
### Task 25: 全链路 API E2E(A01/A02/A03/A06 证据)
**Files:**
- Test: `services/api/test/e2e.full-chain.e2e-spec.ts`
- [ ] **Step 1: 失败 E2E**(一个 spec 串全链路):
1. seed-admin → 登录 admin → 建 album(zh+en) + 分类 + 邀请 staffB;
2. staffB 登录 → **同名文件两次上传**(不同 clientTaskId)→ 两个 presign PUT 实际写入 MinIO → 各 complete(其一 complete 调两次);
3. 运行 worker(测试内 `sweepOnce()`)→ 两照片 ready+published,outbox 各一条 photo.published,**无第三条**(A03);
4. 公开 API:列表 2 张、翻页、语言回退、签名 URL 可 GET 到图片字节;
5. 隐藏其中一张 → 列表剩 1、单图 404;
6. admin 软删除另一张 → 空列表;audit 全程可查。
- [ ] **Step 2: 通过;Commit** `test(api): full-chain e2e covering A01 A02 A03 A06`
### Task 26: 回归门与 P1a 验收记录
- [ ] **Step 1: 全量回归**:`pnpm lint && pnpm typecheck && pnpm test && pnpm e2e`(单测与 e2e 分列)。Expected: 全绿。
- [ ] **Step 2: 写验收记录** `docs/plans/progress/P1a-验收记录.md`:A01(API 级证据+App 端留 P3)、A02、A03、A06、A13(凭证设计级:无长期客户端密钥、签名 TTL 受 env 控制;OSS 生命周期核验随 G1/P5)、HEIC 实测结果、未尽事项。
- [ ] **Step 3: 打 tag** `git tag p1a`;Commit `docs: p1a acceptance record`。
---
## 执行注意
- 每任务 TDD 节奏不可跳步:先失败测试→实现→通过→提交;集成/E2E 测试统一跑在 compose 依赖上(`docker compose up -d` 前置)。
- e2e 隔离:每个 e2e spec `beforeAll` 调 `test/reset.ts`(truncate cn 全表 + 清 MinIO `photo-live/dev` 前缀);worker/storage 测试沿用同约定(各自包内复制 reset 工具或引用)。
- env:命令一律在仓库根执行;根 `.env` 为唯一 env 文件,脚本经 dotenv-cli 注入、测试经 `test/load-env.ts` 载入(Task 7 约定;`services/worker/test` 与 `packages/storage/test` 复制同约定 load-env.ts——两者到仓库根同为三级向上,路径一致);新增 env 变量必须同步 `.env.example`;密钥不入库。
- 发现契约需变更时:先改 `packages/contracts` 并重新生成类型、在任务提交信息中登记,再改消费端(契约单一事实源)。
- 与总纲冲突处以总纲为准并回写修订;HEIC、MySQL 行值比较等 D12 复核项结论写入验收记录。
- P1a 完成后:编写 P1b 计划(viewer 基础页 + admin 最小页 + **宣传图/封面/Logo 上传链路(album_assets 的 presign+确认,复用照片上传机制,R09 依赖)** + Playwright 冒烟)。
+179
View File
@@ -0,0 +1,179 @@
# 图片直播项目实施总纲
版本:V1.0
日期:2026-09-17
执行模式:Claude Code 代理开发 + 显式人工关口
关联文档:[需求说明书](../01-需求说明书.md) · [项目计划](../02-项目计划.md) · [技术实施方案](../03-技术实施方案.md) · [优化方案](../04-优化方案.md)
本总纲把 V1.3 文档基线拆解为代理可执行的实施阶段序列,定义工程结构、技术选型锁定、阶段边界、人工关口与决策挂起清单。它不替代上述四份文档:需求以 R01 至 R31 为准,验收以 A01 至 A29 为准,工作包编号 T01 至 T30 沿用项目计划。各阶段的详细可执行计划在进入该阶段时另册编写(见 docs/plans/ 目录)。
## 1 执行模式与技术栈锁定
开发主体为 Claude Code 代理,按"每阶段交付可在本地完整运行、可自动化验证的端到端增量"推进;无法由代理完成的验证(真机、真实云资源、外部接口)显式登记为人工关口,不阻塞代理开发,但在对应阶段出口必须闭环。
D02 技术栈决策本次锁定如下,后续变更需评估返工范围后修订本总纲:
| 层 | 选型 | 说明 |
|---|---|---|
| App | Flutter,原生桥接(iOS background URLSession / Android 用户发起传输任务 + 前台服务) | P3 创建,双平台 |
| 管理后台 | Art Design Pro 模板(Vue 3 + Vite + TypeScript + Element Plus) | 锁定模板版本;P1 尾部接入 |
| 观众页 | Vue 3 + Vite + TypeScript 轻量入口 | 独立于后台 |
| 嵌入组件 | TypeScript 加载器 + 项目域内 iframe 轮播页 | P4b |
| API | NestJS + TypeScript,单代码库双部署入口 cn-app / ov-app | 模块按部署角色组织 |
| ORM | Prisma(MySQL 方言),cn 与 ov 各自独立 schema | 去重窗口清理等以原生 SQL / 调度任务实现 |
| 数据库 | MySQL 8 ×2:生产复用国内与海外 ECS 上 1Panel 既有 MySQL 容器,各建独立库与独立账号;本地开发用同版本 MySQL 容器保持方言一致 | 双库结构不同、互不直连,符合 A24 |
| 队列与缓存 | BullMQ + Redis ×2 | cn 承任务队列;ov 承限速与实时辅助(独立实例与命名空间) |
| 图片处理 | sharp(libvips) | HEIC 解码为验证关口,见 G2 |
| 对象存储 | StoragePort 适配层:开发环境 MinIO,生产阿里云 OSS | 见第 3 节 |
| 实时 | NestJS WebSocket(ws 适配器),部署于 ov-app | P2 |
| 测试 | 服务端 Jest + supertest;Web 端 Vitest;E2E Playwright | 基础设施依赖 docker-compose 固定版本 |
| Monorepo | pnpm workspaces + Turborepo | Flutter 子树用原生工具链 |
## 2 工程结构
```text
photo/
docs/
plans/ 实施总纲、各阶段详细计划与验收记录
apps/
mobile/ Flutter App(P3 创建)
admin/ 管理后台(Art Design Pro)
viewer/ 观众页
packages/
contracts/ OpenAPI 契约 + 生成 TS 类型 + 事件契约 + 稳定错误码
locales/ Web 公共翻译词条
embed-loader/ JS 悬浮组件加载器(P4b)
storage/ StoragePort 接口与 MinIO / OSS 适配器
services/
api/ NestJS:cn-app 与 ov-app 两个部署入口 + 共享领域模块
worker/ 图片处理 worker(BullMQ 消费者 + sharp)
sync/ outbox 发送 / inbox 接收 / 统计回传(P2)
infra/
docker-compose.yml MySQL×2、Redis×2、MinIO(P1 仅启用 cn 侧,P2 起以 profiles 启用 ov 侧;版本对齐生产 1Panel 环境)
migrations/ 原生 SQL 与清理任务(去重窗口等)
deploy/ 部署配置(P5)
```
Schema 所有权:国内库表(users、albums、photos、upload_sessions、outbox_events 等)归 `services/api` 的 cn schema 管理;海外库表(展示副本、inbox、点赞批次、浏览去重、统计权威)归 ov schema 管理。两端共享的数据形状只经 `packages/contracts` 传递,禁止互相引用对方 schema 生成物。
契约优先:`packages/contracts` 是接口与事件的单一事实源,三端类型由其生成;契约任务完成处设用户确认点后再开发消费端。
## 3 代理执行的核心工程决策
1. 本地双地域仿真:docker-compose 启动两套独立 MySQL 与 Redis;cn-api(:8080) 与 ov-api(:8081) 双进程;sync 服务经 HTTP 互推。跨境同步、墓碑、乱序去重、统计回传全部在集成测试中自动化验证,不依赖真实海外资源(真实部署见 P5)。
2. 存储适配层:`packages/storage` 定义 StoragePort(签发临时上传凭证、head 核验、服务端受控复制、禁止覆盖语义)。开发环境用 MinIO(presigned);生产用阿里云 OSS 适配器。生产适配器优先返回签名 PUT URL(与 MinIO 形态统一,App 保持单一 PUT 上传代码路径);STS/分片作为大文件增强备选,经 G1 验证后决定。凭证形态差异在接口上归一化,适配器各自的特殊语义用独立测试覆盖;真实 OSS 差异集中在 G1/G2 关口验证。
3. 用户源接口 Mock:按技术实施方案 3.1A 假设字段实现 mock provider 与适配器接口,同步调度、水位、幂等、失败不停用等逻辑先行开发测试;真实接口契约(D08)到位后仅替换适配器。配套 fixtures 覆盖改名、停用、分页中断、空响应、增量等场景。
横切关注点自 P1 内建:结构化日志贯穿三端并以 requestId / uploadId / photoId 关联;稳定错误码 + 本地化文案映射;audit_logs 记录管理与权限操作;/health 端点;密钥经环境注入且不入库。
## 4 阶段划分与交付物
| 阶段 | 可运行交付物 | T 映射 | A 映射 | 出口门 | 人工关口 |
|---|---|---|---|---|---|
| P1 底座 + 国内核心链路 | compose 一键起环境;密码登录→建相册→上传会话→核验→处理→自动发布→观众页瀑布流/大图/短期签名 URL 刷新(读 cn 公开 API);可见性状态机与单张隐藏/软删除;seed 与脚本化 E2E;admin 最小页(后段) | T02 T05(后端部分)T06 T07,T11 部分 | A01(API 与后台级,App 端随 P3)A02 A03 A06 A13(凭证设计级,OSS 生命周期全量核验随 G1/P5);A07/A08 基础级 | 回归全绿 + 验收记录;可选 G1 | G1(可选) |
| P2 双地域仿真 + 互动 | ov-app + outbox/inbox 同步(版本、墓碑、高优先级下线、补漏对账);点赞批次幂等;WebSocket 气泡(自身批次过滤、后台暂停);浏览统计窗口去重;统计快照回传;观众页切换至 ov-api 并新增最新/热门排序 | T25(仿真部分)T26 T13 T19 T27 T24,T11 完成 | A09 A10 A14 A21 A26 A27(仿真级) | 回归全绿 + 验收记录;D07/D11 初值确认 | 无 |
| P3 Flutter App | 登录、相册配置、协作、持久化上传队列(准备阶段状态机、账号隔离、跨设备服务端历史)、连拍入队、原生后台传输桥接 | T03 T08 T09 T21 | A04 A05 A15 A19(代码 + 模拟器级) | 回归全绿 + 验收记录 | G2(强制)、G3(资源) |
| P4a 后台完备 + 账号治理 | Art Design Pro 全功能后台:相册/照片/用量管理与告警、上传公开开关、批量隐藏、负责人交接、水印版本与历史重生成、下载两级控制、后台语言切换与账号管理(激活停用/重置密码/角色)、用户定期同步任务与管理页(mock 源) | T10 T20 T22 T12 T30 T04(mock 部分) | A16 A17 A18 A20 A23 A29(mock 级) | 回归全绿 + 验收记录;D06 前置 | G4(真实接口) |
| P4b 传播组件 | embed-loader + 展示位管理 + 轮播 iframe:configVersion 校验、旧响应丢弃、不可见暂停、postMessage 白名单、data-lang 语言配置 | T14 T23 T15 | A11 A12 A22;A23 组件部分 | 回归全绿 + 验收记录 + 全端语言验收(T15/A12) | 无 |
| P5 真实环境接入 | 真 OSS/STS 适配器验证、海外部署与域名、全球 CDN 接入与失效策略、用户接口真实对接、跨地域故障演练、容量压测、试运行与分发交接 | T16 T17 T25(真实)T28 T29 T18 | A24 A25 A28 + 全量 A 回归 + 容量报告 | 发布门槛(对齐项目计划 7.3) | G5、G6 |
阶段顺序 P1 → P2 → P3 → P4a → P4b → P5。P3 与 P4a 在 P2 契约稳定后可并行;P4b 依赖 P2 的公开列表契约与同步通道(展示位事件扩展),其 T15 全端语言验收还需 P3 完成;P5 依赖全部前序。
每阶段出口执行统一回归门:lint + typecheck + 单元 + 集成 + E2E 全绿,并产出《阶段验收记录》(docs/plans/progress/,逐项对照 A 编号登记证据与未尽事项)。
## 5 决策挂起清单
| 决策 | 内容 | 最晚确认时点 | 本次状态 |
|---|---|---|---|
| D01 | 首发语言(建议 zh-CN、en) | P1 开始前 | 待确认,P1 按建议值开发词条结构 |
| D02 | 后端与 Web 技术栈 | — | 已锁定(本总纲第 1 节) |
| D03 | iOS/Android 分发方式 | P3 开始前(资源自首周准备) | 待确认 |
| D04 | 删除回收策略 | 永久清理规则 P4a 前 | 软删除基线已随 V4 纳入 P1 |
| D05 | 数量并发与网络目标 | P5 压测计划前 | 待确认,按项目计划 L1/L2 阶梯准备 |
| D06 | 下载水印与原图默认值 | P4a 开始前 | 待确认 |
| D07 | 浏览统计窗口(建议 30 分钟)与主办字段 | P2 验收前给初值 | 待确认 |
| D08 | 用户接口契约(稳定 ID、字段、鉴权、停启用语义) | P4a 开始前 | 待确认,mock 先行 |
| D09 | 海外 ECS 地域规格确认与测试点 | P5 开始前 | 海外 ECS 已就绪(1Panel + Docker + MySQL),规格与地域待登记 |
| D10 | 缓存失效窗口、轮询周期、用量阈值 | 对应模块(P4b/P5)验收前 | 待确认 |
| D11 | 跨境同步参数(补漏周期、回传周期、中断策略) | P2 验收前给初值,P5 实测冻结 | 待确认 |
| D12 | 两台 ECS 的 MySQL 版本、字符集与配置确认 | P1 开始前 | 已确认为 8.0+(2026-09-17),本地容器用 8.4 对齐 |
| D13 | 代码托管平台与 CI 接入 | P5 镜像构建链路前(建议 P2 前) | 待确认,本地 git + 脚本先行 |
## 6 人工关口与外部资源
| 关口 | 内容 | 时点 | 阻塞范围 |
|---|---|---|---|
| G1 | 真实 OSS 冒烟:presign/STS、head 核验、禁止覆盖、staging→originals 受控复制、跨域、现有 Bucket 生命周期规则对项目前缀的影响检查 | P1 出口(有凭证即执行,无凭证显式跳过并登记) | OSS 适配器定稿 |
| G2 | 真机双平台 + 真 OSS 组合验证:锁屏、切后台、厂商后台管理、凭证过期恢复、HEIC 真机样本解码(对齐技术验证用例 6.4) | P3 出口(强制) | App 上传链路发布 |
| G3 | macOS/Xcode 构建链、签名与分发资质 | P3 资源(自 P1 起准备) | iOS 构建 |
| G4 | 用户接口真实契约与测试凭据 | P4a | T30 真实对接 |
| G5 | 海外 ECS(已装 1Panel + Docker + MySQL)规格与地域确认、域名、全球 CDN 权限 | P5 | 海外部署与 CDN |
| G6 | 试运行真实活动与业务验收 | P5 末 | 发布 |
## 7 工程治理
- 版本库:项目当前未初始化 git。P1 第一个任务执行 `git init`,trunk-based 单主干,约定式提交信息,每任务至少一次提交,阶段完成打 tag(p1、p2…)。
- 分支与回退:阶段 tag 作为回退基线;数据库迁移保持向前兼容,不做破坏性逆迁移(对齐技术实施方案 12.4)。
- 密钥:全部经 .env 注入,仓库只提交 .env.example;不提交任何 OSS、接口、签名凭据。
- 验收留痕:每阶段验收记录对照 A 编号登记;代理执行的关键决策(如契约变更)记录在阶段计划文档内。
- 代码托管与 CI:本地 git 起步即满足回归门;仓库托管平台与 CI 接入为待确认项(D13),确认前以本地脚本保证质量,P5 镜像构建发布链路随之补齐。
- T01 原型的替代:纸面原型由"契约评审(用户确认点)+ P1 即可运行的观众页与后台最小页"替代,页面字段与权限以 R 编号与契约为准。
## 8 各阶段范围明细
### P1 底座 + 国内核心链路
包含:
- git 初始化、monorepo 脚手架(pnpm + Turborepo)、compose(MySQL/Redis/MinIO,cn 侧,版本对齐生产)、基础 lint/typecheck/test 脚本。
- packages/contracts:P1 范围 OpenAPI 契约(auth、albums、members、uploads、photos、public read)+ 事件契约(photo.published 等)+ 错误码;完成后设用户确认点。
- Prisma cn schema 与迁移(MySQL 方言):users、sessions、albums、album_translations、album_members、categories、category_translations、album_assets、photos、photo_variants、upload_sessions、photo_jobs、outbox_events、audit_logs。
- 数据模型 PG→MySQL 语义复核清单:唯一约束与 upsert 行为、时间精度与 UTC 存储、排序与游标稳定性、token 大小写敏感(显式 binary 校对或受限字符集)、连接池上限。
- cn-api:密码登录/刷新/注销(成熟密码哈希、会话可撤销、登录失败基础限速);相册 CRUD 与多语言字段、成员与分类;上传会话幂等初始化(clientTaskId 唯一)、临时凭证签发(StoragePort)、续签、完成核验(head + 字节数 + 完整性)、staging→originals 受控复制(禁止覆盖);照片可见性状态机与单张隐藏/软删除;公开读接口(相册信息按语言回退、游标分页稳定排序、单图定位);图片短期签名 URL 签发与过期刷新;outbox 事务写入;CORS 来源按环境配置;/health。
- worker:BullMQ 消费者 + sharp 流水线(真实类型校验、解码、方向校正、缩略图/浏览图、EXIF 清理、尺寸记录),事务切换公开状态并写 photo.published 事件;失败归类与有限重试。
- viewer 基础页:活动信息区(宣传图、多语言回退、浏览次数占位、活动时间按相册时区展示)、双列瀑布流(宽高预留、按需渲染/懒加载)、大图基础交互(左右切换、双指缩放、关闭回原位)、单图分享定位路由、空相册本地化空态、新照片提示(轮询)、分类筛选、zh-CN/en 切换;最新/热门排序切换依赖点赞数,随 P2 提供。
- admin 最小页(后段低优先级):Art Design Pro 登录对接本地会话、相册列表/创建/编辑、成员管理。
- 宣传图/封面/Logo 上传接口与 viewer 展示(album_assets 的 presign+确认,复用照片上传机制)随 P1b 交付(R09 依赖,P1a 先建表不出接口)。
- E2E:脚本上传(多文件、同名、重复完成通知)→ worker 处理 → 公开 API 可见 → Playwright 观众页冒烟。
不包含:海外同步与互动(P2)、Flutter(P3)、批量隐藏/交接/水印/下载控制/用量(P4a)、展示位与组件(P4b)、真实 OSS 常态接入(G1/P5)。
P1 内部再切 1a(后端链路,API 级 E2E 先行)与 1b(观众页与 admin 界面);admin 最小页如遇模板接入风险可顺延至 P4a,不阻塞 P1 出口。
### P2 双地域仿真 + 互动
包含:ov schema(展示副本、inbox、水位、点赞批次、浏览会话与请求去重、统计权威)与 like_batches 去重窗口方案(MySQL 表分区要求分区键进入全部唯一键、会破坏批次唯一约束,故采用索引 + 定时清理任务 + 过期批次拒绝重放替代分区,清理任务纳入 infra/migrations 与调度);sync 服务(outbox 发送器签名鉴权、inbox 幂等消费、版本与墓碑、高优先级下线队列、定时补漏对账);海外紧急停用入口(独立于同步链路的 ov 侧运维端点,跨境中断时可达:仅允许更严屏蔽、不修改国内权威数据、不因普通同步事件自动解除——技术方案 §2A);ov-app 公开 API + WebSocket(相册隔离订阅、点赞批次幂等、合并广播、自身批次过滤、断线降级轮询);浏览统计(跨标签页共享匿名标识、窗口去重、requestId 幂等、并发首访唯一、排除规则:分页请求/图片资源请求/轮播/后台预览不计入,后台预览走独立路由);统计快照回传(statVersion 单调替换);观众页切换 ov-api 并接入实时更新,新增最新/热门排序切换(热门按点赞数版本化短期快照)。D07、D11 初值在验收前确认。
### P3 Flutter App
包含:登录与账号隔离、修改密码;App 内语言切换(zh-CN/en,跟随设备与手动选择);相册配置与协作页面(含相册分享链接与二维码入口);本地任务库与状态机(preparing/preparation_failed/queued/uploading/…);准备阶段(源文件获取、空间检查);连续拍摄自动入队;原生后台上传桥接(iOS URLSession background、Android 用户发起传输任务/前台服务)与任务映射恢复;凭证过期续签(单文件 PUT 为上传基线,分片上传作为后续大文件验证项);跨设备服务端历史。出口强制 G2 真机验证:未通过不标志 P3 完成、不阻塞 P4a/P4b 推进,但阻塞 P5 试运行并触发重估。
### P4a 后台完备 + 账号治理
包含:后台全功能(相册/照片管理、筛选、批量隐藏逐项结果、上传公开独立开关、负责人转移、用量统计与阈值告警(含统计数字与同步时间后台展示)、相册分享链接与二维码展示、后台语言切换、账号管理:激活停用/重置密码/角色);水印配置版本化与历史重生成任务(进度、可重试、不覆盖原图、原子切换引用);下载两级控制与微信等浏览器引导;用户定期同步任务(调度锁、水位、幂等、失败记录、手动补拉、状态页)接 mock 源。G4 后替换真实适配器。
### P4b 传播组件
包含:embed-loader(版本化地址、防重复初始化、postMessage 白名单、data-lang 语言配置);展示位管理与权限(展示位实体与 configVersion 纳入 cn→ov 内容同步,海外公开组件 API 读取本地副本,对应 R28);轮播 iframe(configVersion、旧响应丢弃、近期照片队列、不可见暂停、空态);接入样例站点与宿主 CSP 说明;出口执行 T15 全端语言验收与文案补齐(App、后台、观众页、组件四端切换、回退与漏译检查,对应 A12)。
### P5 真实环境接入
包含:OSS 真实适配器与 STS 验证;国内与海外两台 ECS(均已装 1Panel + Docker + 既有 MySQL 容器)上的部署:镜像与 compose 编排交由 1Panel 容器管理、域名与 TLS 经 1Panel OpenResty 反代、复用既有 MySQL 容器各建独立库与账号、Redis 以容器部署、1Panel 计划备份并演练恢复、数据库迁移由发布流程执行(不手工进容器操作);海外首次全量快照与增量追赶;全球 CDN 回源、URL 鉴权、缓存失效与预热策略;用户接口真实对接;跨地域故障演练(断网重放、乱序、重复回传、紧急停用、恢复);容量压测(L1/L2)与实测报告;监控告警、回滚演练;试运行与 App 分发交接(服务端保持必要的旧客户端兼容)。
## 9 风险与应对(实施总纲层)
| 风险 | 影响 | 应对 |
|---|---|---|
| MinIO 与 OSS 语义差异(STS 形态、禁止覆盖、分片) | 适配器返工 | StoragePort 归一化 + G1 前置冒烟 + G2 真机组合验证 |
| 数据模型从 PG 假设迁移到 MySQL 的语义差异 | 去重、并发、排序行为偏差 | P1 首批任务含语义复核清单(唯一约束、upsert、UTC 时间精度、token 校对、游标排序);去重窗口替代分区,P2 用真实并发事务测试验证 |
| 复用 1Panel 既有 MySQL 容器 | 与其他业务争用资源、备份耦合 | 独立库 + 独立账号 + 连接池上限;纳入 1Panel 计划备份并演练恢复;用量告警(A23)覆盖数据库资源 |
| 生产 MySQL 实际版本低于 8.0 | 迁移与去重 SQL 兼容性问题 | D12 于 P1 开始前确认;必要时在 1Panel 新建 8.x 容器专用本项目,原容器不动 |
| sharp HEIC 解码在 Windows 预编译不可用 | iPhone 照片失败 | P1 用样本 HEIC 验证;不可用则换 libheif 路线并登记 |
| Art Design Pro 模板升级或裁剪成本 | 后台进度受阻 | 锁定模板版本;接入置于 P1 尾部不阻塞主线 |
| 本地仿真与真实跨境网络差异 | P5 问题集中暴露 | 同步语义全部版本化/幂等;P5 演练清单对齐项目计划 7.3 |
| 决策挂起未按时确认 | 阶段出口阻塞 | 第 5 节清单随阶段计划再次提醒,未确认按建议值开发并标记 |
| 代理长周期执行上下文漂移 | 实现偏离文档 | 每阶段独立计划 + 验收记录 + 契约单一事实源 |
## 10 与项目计划(S1-S6 / 岗位人日)的关系
本总纲面向代理执行重新纵切,阶段与 S1-S6 不一一对应:S1 需求基线由本总纲 + 契约评审替代;S2 技术验证拆入 G1(P1 出口可选)与 G2/G3(P3)前置;S3 对应 P1;S4 对应 P2 + P3 + P4a 主体;S5 对应 P4a/P4b + P5 前段;S6 对应 P5 末段。T01-T30 全覆盖(T01 由契约评审与早可用页面替代;T04 由 P4a 的 mock 契约先行 + G4/P5 真实接口验证替代;T15 落在 P4b 出口的全端语言验收)。原岗位人日估算(156 至 231)作为规模参照,代理执行的实际节奏以阶段验收记录为准;对外承诺的日历工期仍以项目计划 W1-W16 基线管理,两者并行不悖。
View File