Files
photo-live/docs/plans/progress/G4-用户接口对接-验收记录.md
T
2026-09-23 17:04:45 +08:00

157 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# G4 验收记录:用户接口对接(uniservice /open/v1/users)
对应方案:[2026-09-22-G4-用户接口对接-实施计划](../2026-09-22-G4-用户接口对接-实施计划.md)
执行日期:2026-09-22
外部契约:[open-v1-users-api.md](../../open-v1-users-api.md)(服务方 uniservice,合塑/新友合集团统一后台)
## 0 基线与保护
- 基线 commit:f193e4f(fix(viewer): remove extra closing brace)。
- 工作树既有改动收编:起始时工作树存在 G4 半成品未提交改动
(docs/open-v1-users-api.md、http-user-source.ts 草稿、user-sync.service.ts 引擎
V1.1 行为变更、.env 两示例新增变量)——本任务全部收编重写/对齐后分批提交。
- 契约/schema 零改动:packages/contracts、db-cn/db-ov schema、main-ov.ts、
同步账本(outbox/sync_inbox/sync_state)均未触碰;引擎红线 1-4 逐条维持
(e2e ①-⑫ 全量断言仍守护)。
## 1 关键决策及依据
| 决策 | 依据 |
|---|---|
| V1.1 初始凭据:同步新建用户直接 `credentialStatus=active`,账号=手机号,初始密码=手机号后 6 位(不足回退 '000000'),**全局生效(含 mock 源)** | 用户拍板(2026-09-22,真实接口上线即用);P4a「待激活+哨兵哈希」闭环退役,auth.service 等代价 dummy verify 防御保留兼容历史行;activate 端点对 active 用户幂等,reset-password 仍为管理员轮换出口 |
| HTTP 源**每批全量拉取**,不做增量 | 上游 API 无增量过滤参数(仅 page/page_size/company_id/status/keyword/ids);幂等由引擎既有行级 sourceVersion 比较保证(213 用户/200 每页≈2 页,成本可忽略) |
| 游标协议:续翻块编码 `p:N`;**纯数值水位绝不解析为页码** | 半成品草稿的严重 bug:引擎增量批传入水位(epoch 秒,如 "1758520000")被 `parseInt` 当页号→请求第 17 亿页→空页"成功"空转。单测有专项回归用例 |
| 一人多档按 phone **全局**(跨上游页)去重,保留 updated_at 最新行 | 草稿仅页内去重:同 phone 两行分落两页时后应用的旧行(如离职档)会覆盖新行状态。全量先拉齐再切块,消除跨页分裂 |
| `external_user_mappings.sourceSystem` 随源:mock=`'mock'`、http=`'uniservice'` | UserSourcePort 新增只读 `sourceSystem`;映射锚点 (sourceSystem, externalUserId) 命名空间按源隔离,既有 `mock:` 映射自然成为孤儿(预期,见 §5) |
| 模块装配 env 开关 `USER_SYNC_SOURCE=mock|http`,缺省 mock | e2e/本地默认零变化(app.get(MockUserSource) 注入场景依赖);生产显式切换 |
## 2 改动清单(提交序列)
| commit | 内容 |
|---|---|
| 42878f1 | docs(g4):接口文档 + 实施计划入库 |
| b7754d5 | test(api):e2e ①⑤⑪ + 文件头按 V1.1 重写(新增"同步即可登录"断言,强度不低于旧哨兵断言) |
| 52cd316 | refactor(api):端口 sourceSystem + 引擎收编(initialPassword helper、删哨兵死代码、注释 V1.1 化、auth.service 注释历史化) |
| d732caa | feat(api):HttpUserSource 适配器(全量拉取+全局去重+分块续翻缓存+页数护栏)+ 5 单测(fetch mock) |
| 8c255eb | feat(api):USER_SYNC_SOURCE 开关装配 + .env.example / .env.production.example-cn |
| (本提交) | docs(g4):本验收记录 |
文件:user-source.port.ts、mock-user-source.ts、user-sync.service.ts、http-user-source.ts(重写)、user-sync.module.ts、auth.service.ts(仅注释)、test/user-sync.e2e-spec.ts、test/http-user-source.spec.ts(新增)、.env 两示例。
## 3 测试证据
- `pnpm --filter @photo/api-cn typecheck`:0 error。
- 单测全量:`pnpm --filter @photo/api-cn test` → **8 套件 / 56 用例全绿**(含 http-user-source.spec 5 用例:跨页去重/水位不当页码回归/分块续翻缓存/401·500·success=false·env 缺失/无手机号跳过)。
- user-sync e2e:`test:e2e -- user-sync.e2e-spec` → **12/12 全绿**(红线逐条断言维持;⑪ 新语义:初始密码登录 200 → activate 幂等 → reset-password 轮换后初始密码 401)。
- 全量 e2e:`test:e2e` → **22/24 套件通过(249/257 用例)**,2 个失败均与本分支无关(见 §5 残留,均已取证)。
### 环境事件登记(复现性说明)
首次全量 e2e 出现 8 套件失败,排查定位为**本地游离 `pnpm dev:worker` 进程(PID 56112)抢消费 e2e 照片处理队列**(测试自行直调 PublishService,worker 抢先将照片置 ready → `processPhoto` 返回 'already')。经用户确认终止该进程后复跑:uploads/watermark/batch-ops/full-chain/likes-realtime/public-ov 全部恢复绿。本地跑全量 e2e 前须确认无游离 worker。
## 4 部署注意(P5 线)
- 生产/测试服务器 cn 侧 env 三变量:`USER_SYNC_SOURCE=http` +
`OPEN_API_USERS_URL=https://uniservice.server.hnxinyouhe.com/open/v1/users` +
`OPEN_API_USERS_KEY=<服务方下发>`(key 仅服务端持有,勿入前端/仓库)。
- **本地开发 .env 切勿设 `USER_SYNC_SOURCE=http`**——模块 import 时读 env,会导致
user-sync e2e 的 `app.get(MockUserSource)` 解析失败。
- key 未配置/错误的表现:每批 sync failed(批次行 error 落"鉴权失败/HTTP 401"),
不影响任何既有用户/会话——引擎批次级故障隔离语义。
- 测试服务器(219.157.255.213)换真实 key 后建议手动触发一批
(POST /v1/admin/user-sync/runs)并核对 status/runs/errors 与 users 新增
(账号=手机号、可手机号后 6 位登录)——属 P5 待办。
## 4.5 真实联调证据(2026-09-22,用户提供正式 key 后执行)
- 连通性:`GET /open/v1/users`(X-Api-Key)200,返回真实档案(合塑/新友合)。
- 首批(临时脚本走 AppCnModule 真实装配 + `engine.tick()`,env 仅命令行注入不落盘):
**state=success,13s,added=230 / updated=0 / disabled=0 / failed=0**;
`external_user_mappings(sourceSystem='uniservice')` 230 条;水位 1789973885(epoch 秒)。
- 幂等:紧接第二批 **added=0 / updated=0 / failed=0**,水位不动——行级版本比较按设计工作。
- V1.1 凭据抽查:在职用户(手机号账号)`verifyPassword(手机号后 6 位)`=true、
错误口令=false;用户分布 110 active / 104 disabled(status=all 含离职停用,预期)。
- 格式登记:上游 `updated_at` 为 `YYYY-MM-DD HH:mm:ss`(非 ISO-T),V8 按本地时区
解析——仅作单调版本号用途,语义无影响。
- 临时脚本已删除(不入库);key 未写入任何文件/仓库。
## 4.6 测试服务器部署(2026-09-22 当日完成)
- 镜像:`photo-live-server:3a8a1cf-20260922`(git archive HEAD 干净构建,排除工作区
未提交 viewer 改动;216MB gz);cn/ov 两栈 server 容器均滚动至该 tag,web 保持
各自当前 tag(cn=ui02-20260922 / ov=viewer-fix)。无新增 DB 迁移,跳过 migrate。
- env:`.env.production-cn` 追加 `USER_SYNC_SOURCE=http` + URL + KEY(chmod 600,
key 未入库)。
- 结果:manual 批 14s **added=230 / failed=0**,水位 1790002685;随后新定时器 cron 批
added=0(服务器侧幂等验证 ✓);分布 110 active / 104 disabled,与本地一致;
`uniservice` 映射 230。内部 health 双栈 `{"status":"ok"}`。
- 运维经验登记:①api 容器重建后 **web 容器需一并 restart**(nginx 缓存旧上游容器
IP → 502);②`.env.production-cn` 的 ADMIN_PASSWORD 与库中不符(后台改过密码),
定时同步不受影响,管理端点手动触发请用后台实际密码;③服务器库名为
framevia_cn/ov(非部署文档示例的 photo_cn/ov),MySQL 8.4 `trigger`/`cursor`
为保留字,手工 SQL 需反引号。
## 4.7 用户列表分页与同步档案展示(2026-09-22 同日迭代上线)
- 范围:管理后台用户列表改标准页码分页(`page/pageSize` 偏移模式 + `total`,游标
模式兼容保留);同步档案(工号/邮箱/公司/头像)落 `external_user_mappings.profile`
(新 JSON 列迁移 20260922174000)经 `AdminUser.synced` 展示;幂等跳过路径的展示
补偿写解决**存量 230 用户回填**(版本门跳过但快照缺失 → 仅刷映射行)。提交
f6ff1d4 / 2f6b6af / eebf3b3;服务器镜像 `eebf3b3-20260922` + cn web
`admin-eebf3b3-20260922` 已部署,迁移已应用。
- **同步间隔调整(用户指令)**:生产 `SYNC_USERS_INTERVAL_MS=43200000`(每 12 小时
一批,原 5 分钟);服务器 env 已设并重建生效(日志核实)。缺省 5 分钟保留为
开发/演示节奏。**注意**:12h 间隔下容器重启后首批要等 12h——存量回填/即时同步
用后台「手动同步」按钮触发。
- **uniservice 服务端不可达事件(进行中)**:部署后定时批次 fetch failed;测试服务
器/开发机两处独立网络均超时,DNS 正常、其余站点可达 → 服务方侧故障(09:10 尚
正常)。批次隔离语义正确:失败批不动任何用户。恢复后手动触发一批即完成 230
用户档案回填(工号/邮箱/公司/头像列即有数据)。
- 既有登记沿用:UI-04 线 embed e2e/`embedSlots.spec.ts` displayMode 类型漂移为
线外残留(本迭代观察到 admin typecheck 同源 3 错,非本域文件)。
## 4.8 构建环境事件与分页档位调优(2026-09-22 晚)
- 分页档位调优(用户手改 + 单测同步):默认每页 15、档位 [15,20,50,100](80a7658)。
- **事件 A:本机 Docker Hub 不可达**(TLS 握手超时持续 20+ 分钟,同日早些时候正常)。
应对:**构建挪到测试服务器**(其 Docker 配有国内加速器 docker.1ms.run/1panel.live,
基础镜像可拉):git archive HEAD 解包 scp → 服务器 docker build → 直接 up(免镜像
传输)。此路径已验证可用,Hub 再抽风时的标准预案。
- **事件 B:服务器新拉的 `nginx:alpine`(当日最新 digest)entrypoint 卡死**——
10-listen-on-ipv6-by-default.sh 在该服务器挂起(本地构建缓存旧 digest 无此问题;
nginx 配置本身 nginx -t 通过)。**临时补丁**:构建期
`RUN rm -f /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh`(服务器侧
web-patched.Dockerfile,产物即线上 admin-80a7658-20260922,已 healthy)。
**待办**:仓库 Dockerfile 的 `FROM nginx:alpine` 钉具体版本(待本机 Hub 恢复统一
处理),并把补丁行正式化(或钉版本后确认无需补丁)。
- 中途回滚一次(admin-eebf3b3 web)恢复服务后再上补丁版——停机窗口约 4 分钟。
## 4.9 复制按钮 HTTP 上下文修复(2026-09-23)
- 现象:后台 4 处「复制」按钮(公开令牌/分享链接/嵌入代码/重置密码)点击无效。
根因:`navigator.clipboard` 仅安全上下文(HTTPS/localhost)可用,测试入口
`http://IP:32005` 下为 undefined → 抛 TypeError 且无任何提示。
- 修复(aaea76b):统一 `apps/admin/src/utils/clipboard.ts` `copyText`——
clipboard API 优先,失败/缺失降级隐藏 textarea + `execCommand('copy')`;
4 调用点替换 + 失败 toast。vitest 4 用例(API 路径/抛错降级/undefined 降级/
全失败 false)。**长期正解**:上正式域名 + HTTPS 后 clipboard API 原生可用
(降级路径无害保留)。
- 本机 Hub 已恢复,回归本地标准构建部署(web `admin-aaea76b-20260923`,
healthy;本地缓存 nginx 基础镜像无 §4.8 卡死问题)。
## 5 残留与登记偏差
- **embed.e2e-spec 失败(既有,非本分支)**:API settings 响应含 UI-04 线新增
`displayMode` 字段而该 spec 期望对象未更新——基线 f193e4f 上复现同样失败
(取证:checkout 基线单跑 exit 1)。归属 UI-04 线待修。
- **health.e2e-spec 全量长跑偶发(非本分支)**:两次全量中出现一次,单独跑恒
6/6 绿;长跑资源竞争瞬时问题,登记观察。
- admin UI `sourceVersion` 展示从 mock 自增版本(1,2,3…)变为 epoch 秒
(如 1758520000)——纯展示差异,契约仍 int64。
- 无 phone 上游行被跳过(warn 日志)——V1.1 账号=手机号下无法登录故不同步;
数量可观时人工复核。
- `RECONCILE_EVERY_N_RUNS` 周期对账对 http 源语义弱化(每批本就全量)——
引擎机制保留不动,无害。
- 飞书登录对接(union_id/open_id 议题,接口文档 §5)未做——后续可选线,
YAGNI登记。