Co-Authored-By: Claude Code <noreply@anthropic.com>
39 KiB
图片直播项目技术实施方案
版本:V1.3
日期:2026-09-17
关联文档:需求说明书 · 项目计划 · 优化方案
本方案用于指导第一版开发、联调和部署。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 框架,但应保持下面的数据与状态约束。版本号在工程初始化时选择并锁定,不在当前文档写死未经验证的最新版。
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 工程结构
建议使用单仓库,便于接口契约和多语言资源共同维护;此处只是规划,不自动创建业务工程。
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 下创建独立前缀,实际前缀在部署时确认:
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 授权与完成核验
- App 提交相册、分类、文件大小、类型和 clientTaskId。
- API 校验身份、权限和限额,预创建上传会话及照片 ID。
- 返回 staging 对象范围的临时 STS 凭证或签名上传请求;客户端不得写入 originals 和 variants 路径,长期密钥只在服务端使用。
- App 通过平台后台任务传输。普通照片可以单文件上传,失败重新传输该文件;分片恢复作为大文件方案验证,不把不同文件的队列恢复误称为字节级断点续传。
- App 完成通知或 OSS 通知到达服务端,两种通知均可重复,处理必须幂等。
- API 核验对象、字节数和可用的完整性信息,不单独依赖客户端“成功”字段;分片 ETag 不能直接当作文件 MD5。
- 固定经核验的源版本或使用条件复制,将对象转入客户端不可写的 originals 路径,再以事务写入处理任务与事件源记录。失败重试复用相同正式目标,核验已经存在的文件,不覆盖。
授权过期先更新授权再重试,禁止把失败标为成功。iOS 长时间后台下如何获取新授权是技术验证重点,不假设 Dart 进程永远能运行。
5.3 原图保护与重试
staging 上传请求采用可验证的禁止覆盖约束;签名请求固定必要参数,或使用可验证的权限策略,不能仅依赖客户端自觉添加请求头。普通上传与分片完成分别验证约束。遇到对象已存在时比较会话期望大小和完整性信息,再确认已有结果,不直接重写。
正式原图只允许服务端受控创建。复制过程必须固定源版本,或使用源条件校验并在目标侧复核,避免核验与复制之间源文件变化。服务端写入成功而数据库事务失败时,补偿核验同一正式对象并重试入库,不生成多张照片。
5.4 图片处理
处理进程验证真实图片类型、解码大小、方向和格式,生成不同尺寸浏览图。JPEG、PNG 为基础输入;HEIC 解码转码须用真实 iPhone 样本验证。原图长期保留,衍生版本不覆盖原图。
浏览图建议去除定位等不必要 EXIF 信息。原图下载仍可能包含原始元数据,下载策略在产品配置中明确。
处理成功后以事务切换公开状态并产生 photo.published 事件。重试处理使用确定的版本键,避免产生多套无记录资源。失败采用有限自动重试、错误归类和人工重试入口。
5.5 状态分离
本地任务
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 后台传输说明、后台会话配置。
6.3 Android
按目标 Android 版本采用适用的用户发起数据传输任务;兼容路径结合系统任务调度和符合要求的前台服务通知。具体选择以目标 SDK、设备版本和原生验证结果为准。普通可延迟工作与用户正在等待的批量上传应区别处理。
需验证主流厂商后台管理、通知权限、网络切换和用户停止任务的行为。WorkManager 等调度机制本身不构成无限连续上传保证。Android 数据传输选型、用户发起的数据传输任务。
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 事件契约
{
"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 悬浮组件
下面的域名为占位示例,部署后由后台生成真实地址,不可直接作为可用代码交付。
<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 发布步骤
- 数据库备份,执行兼容旧版本的新增迁移。
- 部署 API 与处理进程,检查健康、授权和测试照片完整链路。
- 部署观众页、后台、组件及实时入口,核对静态资源版本。
- 对测试相册验证登录、后台上传、自动公开、连续点赞、分享下载、组件和双语言。
- 按确定方式分发 App;服务端保持必要的旧客户端兼容。
- 观察错误率、队列积压、上传失败、计数差异和资源用量,达到发布门槛后扩大使用。
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 后台文件传输
- Apple 后台会话配置与强制退出边界
- Android 数据传输任务选择
- Android 用户发起的数据传输
- 阿里云 OSS STS 临时授权
- 阿里云上传临时授权方式
- 用户接口文档:待接口提供方交付,第一期不引用飞书接口。