浅念
64e1be09d3
test(api): pin earliest-uploadedAt + uploadEnabled-agnostic pre-check; fix comment
...
评审硬化三项(checkUploadDuplicates 后续,不 amend):
- e2e 补「同 bytes 多条可见命中取最早 uploadedAt」:两条同 bytes 不同
uploadedAt 的种子行,断言回显更早值。
- e2e 补「uploadEnabled=false 预检仍 200 且照常命中」:钉住只读预览刻意
不校验开关的偏差——防止未来一致性重构把检查加回来,破坏 App 关册时的
降级体验(init 入队时仍有 409 兜底)。
- 修正 uploads.service checkDuplicates 的 null-uploadedAt 注释:原「谓词
命中与否不受影响」不准确,改为「仅直改库等理论形状(bytes>0 且
uploadedAt=null)下预检可能漏报而 init 仍会 409」。
uploads e2e 30/30 ✅ · api typecheck ✅
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-28 19:06:47 +08:00
浅念
805f77be76
feat(api): upload duplicate pre-check endpoint (checkUploadDuplicates)
...
「上传查重前置到入队前」Task 1:POST /v1/uploads/duplicates/check 只读批量
查重预检——用户点上传(确认入队)时刻预判服务端历史重复(跨设备/重装/改名
均可查到),不再等任务在上传中心以 PHOTO_DUPLICATE 失败才暴露。
- 契约纯增量:/uploads/duplicates/check + UploadDuplicateCheck(Item|Request|
Duplicate|Response) schemas;regen openapi.d.ts。
- 判定谓词与 init ③.5 服务端查重逐字一致:同 albumId + uploaderId=当前用户
+ bytes 相等 + visibility != 'deleted'(filename 刻意不参与),保证「预检
即真相」;命中条目按请求下标逐项返回(同 bytes 多请求条目各一条)。
- 权限链同 init(canView 404 防枚举 / canUpload 403 / 相册空值兜底 404),
刻意不校验 uploadEnabled:只读预览,init 真正入队时仍按开关 409 兜底。
- 查询走 idx_photos_dedup:请求条目去重字节集一次 findMany,内存建
bytes→最早 uploadedAt 映射后按请求下标回射。
- items 1..500(minItems/maxItems + 嵌套逐项校验,越界 400)。
- e2e 追加 describe 7 用例:命中含双条目同 bytes/未命中/uploaderId 隔离/
deleted 不命中/非成员 404/items 边界 400/未认证 401。
契约 lint ✅ · contracts+api typecheck ✅ · api 单测 56/56 ✅ ·
uploads e2e 28/28 ✅
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-28 18:56:07 +08:00
浅念
4b5940c0d8
test(api): 修复 uploads e2e 与服务端查重( 90dcc58)的字节数冲突
...
90dcc58 引入 init ③.5 服务端查重(同 albumId+uploaderId+bytes 非 deleted
→ 409 PHOTO_DUPLICATE)时未同步更新本 spec:renew/complete describe 内多个
init 沿用 bytes=1000,C-bytes complete 落定 1000 字节照片后,C-type/C-happy/
DEV-1/DEV-2 的 init 即被查重 409 拦截(该提交仅跑了 app 397 测试,api e2e
漏跑)。为各冲突 init 改用互异字节数(1002/1003/1004/1005),并同步
putStaging 与 bytes/expectedAt 断言;各用例语义不变。
本地库补apply 20260928120000_photos_dedup_index(idx_photos_dedup)。
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-28 18:38:36 +08:00
浅念
90dcc58c33
feat(uploads): P0-1 batch 4 — server-side duplicate photo detection (409 + force + index)
...
契约 UploadInitRequest 加 force(bool,仅 owner/admin 跳过查重)。
服务端 init 事务前置查重:同 albumId + uploaderId + bytes 的非 deleted
照片已存在 → 409 PHOTO_DUPLICATE。photos 表加 idx_photos_dedup 索引
(迁移 20260928120000)。
App UploadInitRequest 加 force 字段 + toJson 序列化。
本地查重(层 1)+ 服务端查重(层 2)双层防线到位。
API typecheck ✅ · app analyze ✅ · 397/397 ✅ · contract lint ✅
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-28 11:29:29 +08:00
浅念
903962a46b
feat(embed-slots): soft-delete slots — admin entry, ov shutdown via enabled=false snapshot
...
- 契约 deleteEmbedSlot(DELETE /admin/embed-slots/{id} → 204 幂等;admin 专属)
- cn schema:embed_slots.deletedAt(迁移 20260923120000,本地已应用)
- softDelete:乐观锁 claim + outbox slot.updated(payload enabled 强制 false
——ov 副本无墓碑,enabled=false 快照即公开端 404 下线;priority=high)+ audit
- 列表/加载/更新过滤 deletedAt → 同码 404
- Admin 列表操作「删除」(确认:已接入网站将停止展示、不可恢复)
- API 56/56、admin 87/87 + build 全绿
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-23 13:37:39 +08:00
浅念
2618ad3163
feat(assets): album asset status — show current banner/cover/logo in admin branding tab
...
- 契约 listAlbumAssetStatus(GET /albums/{albumId}/assets):每 (kind,locale)
最新 effective 行 + 现签 URL;权限同详情(可见即读);deleted → 404
- 服务端 AssetsService.status(createdAt,id DESC 组内取首;presignGet 现签)
- Admin 品牌资产页签改造:进入即加载现状——三种 kind 卡片(预览图/未设置
空态/设置时间),banner 分全局/zh/en 槽位;上传成功自动刷新(替换语义)
- API 56/56、admin 87/87 + build 全绿
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-23 10:51:53 +08:00
浅念
8e674a902e
feat(albums): soft-delete albums (admin+owner, hidden & irreversible, both app entries)
...
契约 deleteAlbum(DELETE /albums/{albumId} → 204 幂等)+ status enum 'deleted':
- 服务端:softDelete(canView 404→canManage 403 防枚举;乐观锁 claim +
outbox album.updated priority=high + audit album.delete);列表/详情/更新/
成员/资产/分类/水印/照片/展示位绑定全部 deleted→404;公开端 cn
(status!=='active')与 ov(isAlbumVisibleOv)天然屏蔽;ov 副本经
album.updated 载荷 status 直投自动下线
- Admin:列表表格操作「删除」+ 详情页头「删除相册」(确认含不可恢复说明)
- App:操作面板「删除相册」(红字、二次确认、成功退回列表并刷新)
- 验证:API 56/56、App 397/397(analyze 0)、admin typecheck/lint/test/build 绿
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-23 10:13:08 +08:00
浅念
37d88da4bc
docs(ui05,ui06): admin analysis+HTML preview, app-preview evidence, UI06 unified visual plan
...
- admin-preview-2026-09-22/ 20页新版后台HTML原型+截图+validation(UI05 交付)
- app-preview-2026-09-22/ 截图/validation/check 脚本入库及小改
- admin-redesign-concept 概念图
- UI05 管理后台分析与新版设计说明、用户列表分页计划文档
- UI06 全端视觉统一实施方案(以两个 HTML 预览为唯一视觉基准)
- services/api g4-sync-once 手动同步脚本
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-22 22:42:00 +08:00
浅念
2f6b6afc92
feat(contracts,api): page-mode pagination + synced profile on admin users list
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-22 18:06:12 +08:00
浅念
f6ff1d44f2
feat(api,db-cn): persist synced source profile snapshot on mappings
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-22 17:33:27 +08:00
浅念
8c255eb747
feat(api): USER_SYNC_SOURCE switch wires mock/http user source (G4)
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-22 16:28:55 +08:00
浅念
d732caa739
feat(api): HttpUserSource adapter for uniservice open/v1/users (G4)
...
全量拉取+phone 全局去重(一人多档跨页)、水位游标不当页码回归守护、
分块续翻缓存;对齐 UserSourcePort 类型。
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-22 16:27:43 +08:00
浅念
52cd31679a
refactor(api): per-source sourceSystem on user source port, retire pending-activation dead code
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-22 16:24:37 +08:00
浅念
b7754d52d1
test(api): user-sync e2e aligned to V1.1 initial credentials
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-22 16:22:38 +08:00
浅念
66d1eb3e9f
feat(embed,ui04): E01 v2 contract skeleton — displayMode/scrollSpeed/reactionsEnabled, v2 loader entry and fixed 195x280 shell
...
- contracts: EmbedSlotSettings optional v2 fields (carousel default) + SlotSettingsPayload; gen + redocly green
- api: DTO validation + normalization full-set (cn manage + ov public projection share normalizeStoredSettings)
- embed-loader: params v2 parse/merge/url family, loader-v2.js fixed shell (border/radius/#10161E/safe-area, resize ignored, stack gap 12), dual-artifact build+sync; v1 source frozen
- viewer: EmbedPage v=2 branch, EmbedStream (viewport/track/real anchors/collapse/empty), stream.ts pure fns, i18n zh/en
- admin: types + utils (create default vertical-stream, clampScrollSpeed, v1/v2 snippet selection)
- tests: loader 36 (+9 v2), viewer 222, admin 82, api unit 51, v1 embed e2e 20, v2 E01 gate 2 (195x280 desktop+mobile cross-origin, no footer)
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-22 12:50:28 +08:00
浅念
8f4ad11bd5
feat(contracts,api): UI-01 cover, preview, scope=own and embed config extensions
...
契约扩展(全部可选/可空,旧客户端零感知)+ cn/ov 实现 + e2e 权限矩阵:
- AlbumSummary += cover(kind=cover state=effective 最新行的短期签名 URL,
列表/详情按页批量组装非 N+1;未配置为 null)
- AdminPhoto += preview{thumb,browse}(变体齐备且未删除才签发;隐藏仅
admin/owner;协作者仅自己上传,scope=own 服务端过滤 uploaderId,
成员资格逐请求裁决)
- listAlbumPhotos += scope=all|own(缺省 all 行为不变:仅 owner/admin,
协作者 403;own 任何有效成员)
- EmbedSlotSettings += imageFit/offsetX/offsetBottom/zIndex(cn PATCH 校验
→ slot-state 投影 → ov 公开回读;settings 为 Json 列免迁移,读侧
normalizeStoredSettings 兜旧形状默认值)
- Flutter 模型同步(SignedUrl/PhotoPreview/AlbumPhotoScope,可空容错)
e2e:ui01-contracts 9 例(封面选取/字节取回/N+1 计数断言、preview 权限
矩阵、scope 过滤与 403/404/400)+ embed ⑨ 配置链路;全量 e2e 257 通过。
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-21 14:09:35 +08:00
浅念
92e64b7f10
fix(api): seed-admin works in production containers (no dotenv/workspace)
...
Production image installs prod deps only — dotenv is a devDep, and the
pnpm-workspace.yaml root lookup throws in containers. Both now degrade
gracefully: env comes from container env vars. Hot-patched into the running
test deployment during 2026-09-21 deploy; committed for the next image build.
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-21 11:32:07 +08:00
浅念
8f2c21d25a
feat(viewer): embed carousel iframe with visibility pause
...
P4b Task 4:宿主 loader 以 iframe 载入的轮播页(/embed/:slotToken,裸布局)。
- src/embed/feed.ts:纯函数集——A22 旧响应裁决(dispatch seq + configVersion
双闸:过期/回看丢弃、等值差分合并、升值全量重置)、照片队列差分(新照片
追加队尾、缺席淘汰、thumb 签名刷新)、interval/高度夹取、轮询退避;
- src/embed/useEmbedFeed.ts:取数编排(If-None-Match → 304 零处理)、轮询
(查询 interval 覆盖 → EMBED_POLL_INTERVAL_MS,连续 5 失败 ×5 退避,成功
归零;404/410 静默 collapse 隐藏自身)、A23 不可见暂停(document 隐藏 +
IntersectionObserver 滚出视口 + 本地收起;恢复立即校准)、轮播 intervalMs
切换;__embedPollMs e2e 注入点;
- src/embed/postMessage.ts:上行 resize/collapse(referrer origin 收敛,缺失
退 '*'——载荷无敏感数据,宿主侧 loader source+origin 双校验)、下行 expand
(event.source === window.parent 白名单);
- EmbedPage.vue:交叉淡入淡出(仅 thumb 小图省流量)、点击 window.open 单图
深链(noopener)、计数圆点、「照片即将上线」空态卡、×收起;lang 查询
(auto→相册 defaultLocale)驱动嵌入词条子集;
- api client:fetchPublicEmbedSlot(304/ETag 分支,不走 apiRequest);
- e2e:宿主夹具页 public/__test__/embed-host.html(仅测试资产)+ embed.spec 7
用例(轮换/新标签深链/收起记忆/窄屏 sm/轮询增量/换绑重置/空态/幽灵槽);
helpers 增展示位 admin API 种子 + flushOutbox;
- vitest 18 用例(裁决矩阵/队列差分/夹取/退避/304/可见性状态机)。
配套修正:api CORS exposedHeaders ETag(跨源浏览器可读,304 语义成立);
contracts 新增浏览器安全子路径 ./constants(根出口 re-export internal-sync
的 node:crypto 会打进浏览器包);eslint ignore embed-loader 产物拷贝
(Task 3 起 viewer lint 红,先修)。
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-20 23:02:07 +08:00
浅念
aee4b7ac46
feat(api): embed slots with ov replica and public feed
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-20 22:22:19 +08:00
浅念
3681999667
feat(api): scheduled user sync with mock source (T30 semantics)
...
P4a Task 7 —— cn 侧用户定期同步(mock 源先行,G4 真实接口到位后仅替换
UserSourcePort provider,引擎/契约/表不变)。
数据模型(db-cn 迁移 20260920114149,utf8mb4_0900_bin 逐表校对):
- user_sync_runs:批次账本(trigger cron|manual、state running|success|
partial|failed、cursor 水位、counts JSON、起止时间与错误摘要);
- user_sync_errors:单批次坏行明细(runId 索引);
- external_user_mappings:本地↔外部 ID 映射(@@unique([sourceSystem,
externalUserId]),sourceVersion 逐行高水位);
- user_sync_state:单行状态(lastRunAt/lastSuccessAt/cursor/runCount)。
同步引擎(UserSyncEngine):
- Redis SETNX 分布式互斥(TTL 10min,锁值=runId,Lua CAS 释放);
定时 tick 与手动触发共用,运行中批次复用 runId(202);
- 游标增量 + 周期全量对账(RECONCILE_EVERY_N_RUNS 默认 10);
- 逐页单事务提交(run counts+cursor / state cursor),分页中断→failed
且水位不前进、会话不动;
- 红线遵守:sync 不写 password_hash/role/album_members/localLocked;
空/部分页不误伤停用;上游恢复不解除本地锁定;
- 新用户按 pending 建(随机不可用哨兵哈希,激活+重置后方可登录);
- 停用即撤销该用户全部会话;本地预建同名账号走关联而非重建。
Admin API(/admin/user-sync):GET status|runs|errors?runId=、POST runs
(202 {runId});runs 为 startedAt+ID 倒序游标分页。
auth.service:登录对哨兵/损坏哈希加固(等代价 dummy verify,
401 而非 500,时序平坦)。
测试:user-sync.e2e-spec 12 用例(首轮全量/改名幂等/停用撤会话/
恢复不解锁/新增/空页/分页中断/部分无效/手动互斥/增量+对账/API 形状
与权限/pending 登录门);api e2e 22 套件 239 用例全绿。
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-20 20:01:17 +08:00
浅念
f4a1ba7a14
feat(api): admin account governance endpoints
...
P4a Task 6: GET /admin/users 双模式收口(无 accountPrefix 进入管理列表
模式:全量用户、query/status 过滤、createdAt+ID 倒序键集游标、仅 admin;
前缀模式 P1b 语义保留)+ PATCH / reset-password / activate / deactivate
治理端点(sourceStatus 只读拒绝、自我保护、会话撤销、审计、生成初始
密码)。附带修复 full-chain e2e 的 job-done 断言竞态(轮询条件补齐
photo_jobs 终态)。api e2e 214 → 227 全绿。
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-20 19:31:00 +08:00
浅念
4937eeecff
feat(api): usage summary with threshold alerts
...
P4a Task 5(A23 后台侧):用量统计与阈值告警后端全链。
- cn 迁移(校对手决 utf8mb4_0900_bin):usage_thresholds 单行配置表
(迁移内种子行,storageBytes/perAlbumBytes BigInt?,null=维度停用)+
usage_alerts 告警账本(kind storage|perAlbum、albumId?、resolvedAt?
索引)。「同 kind+albumId 活跃告警唯一」为应用级约束(MySQL 无部分
唯一索引):评估事务以单行配置 FOR UPDATE 作串行化锚点闭合竞态。
- 共享核心 packages/db-cn/src/usage.ts:computeUsageAggregates(非
deleted 口径实时 SUM:photos groupBy + 变体 JOIN raw)+ evaluateUsage
(storage/perAlbum 两维超阈 ensure、回落/停用 resolve、去重)+
evaluateUsageSafe(自开事务、失败仅记日志不拖垮业务主流程)。api 与
worker 共用(放置原因见文件头)。
- api:GET /admin/usage/summary(相册聚合 + totals + 阈值 + 告警:活跃
在前、近期解除补位,合计 100)与 PUT /admin/usage/thresholds(缺省
保持/显式 null 清除 + 契约「即刻重新评估」);仅 admin。BigInt 运输
决策:库内 BIGINT、JSON 出入口 Number() 收敛(2^53≈9PB 上限,DTO 以
MAX_SAFE_INTEGER 拒绝超界输入杜绝静默失真)。
- 检查点(提交后评估):worker 发布事务后(仅真实 published)、worker
水印任务完成后、api photos 可见性迁移(PATCH/软删除)与批量隐藏后、
阈值 PUT 后。
- tests:usage e2e +6(汇总数学/降序/标题、403/401、400 校验矩阵、
perAlbum 告警产生-去重-上调解除、storage 告警-null 清除解除、hidden
计入+deleted 回落且穿过 api 检查点);两侧 reset 增补 usage 表截断 +
单行重种。全量回归:api e2e 214/214、api 单测 51/51、worker 单测
14/14 + e2e 13/13。
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-20 19:01:41 +08:00
浅念
2bb6f291d3
feat(ov,viewer): two-tier download with wechat guidance
...
P4a Task 4(T12 收口):ov 域公开照片下载端点 + viewer 下载 UI。
- interactions 增 downloads:GET /public/albums/{token}/photos/{photoId}
/download?variant=browse|original。两级开关(A18):browse 需
downloadEnabled;original 需叠加 originalDownloadEnabled(级联,browse 关
则两者均 403 DOWNLOAD_DISABLED)。水印相册 browse 现签 watermark pattern
key(browse-key 解析抽取 shared/ov-browse-key 与 public-ov 复用);原图
现签 originalObjectKey(SIGNED_URL_TTL)。来源 IP 固定窗限速
(DOWNLOAD_WINDOW_REQUESTS/DOWNLOAD_WINDOW_SECONDS,env 可调);零浏览
计数写入(排除规则)。STORAGE_PORT_OV 经新 StorageOvModule 与 public-ov
共享同一实例。
- 契约微调登记:PublicAlbum 增 readOnly 必返 downloadEnabled/
originalDownloadEnabled(cn+ov 同形返回;public e2e 精确键断言同步)。
- viewer:lightbox 下载入口随开关渲染(无/单按钮/分体菜单两选项)、过期
凭证静默重取一次、微信内置浏览器(MicroMessenger UA)出保存图片指引
浮层(复制深链 + 反馈)替代直接下载;i18n zh/en 词条。
- tests:api e2e +7(开关矩阵 + URL/字节级断言、水印键一致、隐藏 404、
限速 429、零计数行);viewer vitest +9(UA/矩阵/菜单/重取/失败/指引/
复制);viewer Playwright +3(入口矩阵、微信 UA 浮层、真实端点 200 +
签名 URL 模式)。
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-20 18:32:34 +08:00
浅念
243b2f8721
feat(worker): watermark versioning with atomic history regeneration
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-20 17:53:44 +08:00
浅念
a125fcab1a
feat(api): batch hide with per-item results and owner transfer
...
P4a Task 2(A16/A17):
- POST /albums/{albumId}/photos/hide-batch:逐项结果(部分成功)——
不存在/他册 → ok:false PHOTO_NOT_FOUND;非 published(unpublished/
deleted)→ ok:false PHOTO_NOT_PUBLIC;已 hidden → ok:true 幂等 no-op
(同值不产生第二次事件/审计/版本,P1 语义);published → 逐照片事务
(条件更新 + outbox photo.hidden 高优先级 + audit photo.hide)。
results 与请求同序;权限同管理照片列表(仅创建者或管理员,404 防枚举);
单批 1..200(DTO 层 400)。
- POST /admin/albums/{albumId}/transfer:admin-only owner 移交——事务内
条件更新 ownerId+entityVersion+1(乐观重试)+ 成员互换(目标 upsert 为
owner,原 owner 按 keepAsCollaborator 降级 collaborator 或删行)+
outbox album.updated(标准载荷,版本推进)+ audit album.transfer
{from,to,kept}。目标校验同成员邀请资格谓词(active/未锁定/未停用);
自交接 400;photos.uploaderId 不动(A17 历史上传人不变)。
- e2e(batch-ops.e2e-spec,双入口 harness):8 用例覆盖逐项结果/幂等/
入参权限矩阵/ov 传播(真实发布链→隐藏→photos_ov=hidden+公开列表剔除)/
keep 两分支/目标校验/交接后权限(新 owner 管理、旧 owner 保留协作者
可上传)。
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-20 16:36:34 +08:00
浅念
8e3e19a7d3
test(api): fix key-order expectation in users-lookup e2e
...
Object.keys().sort() 是码点序(大小写敏感):'localLocked'(大写 L)排
在 'locale' 之前;原断言字面量按不区分大小写的字母序书写导致确定性失败
(P4a Task 2 全量回归时暴露,与 HEAD 行为无关的既有笔误)。
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-20 16:36:19 +08:00
浅念
8a8ad8a18d
feat(contracts): p4a admin governance, watermark, download and usage endpoints
...
P4a Task 1 契约扩展批次(36 → 51 operations,gen/redocly/枚举同步守护绿):
- photos:POST /albums/{albumId}/photos/hide-batch(photoIds 1..200,逐项
BatchHideResult {photoId, ok, code?},部分成功语义,A16);
- admin:POST /admin/albums/{albumId}/transfer(owner 交接,历史上传人不动,
keepAsCollaborator,A17);
- albums:POST/GET /albums/{albumId}/watermark-jobs(WatermarkConfig 版本化
+ 历史重生成任务 state/progress/total/failed,原子切换、原图永不覆盖,
A18/A20);
- interactions:GET /public/albums/{token}/photos/{photoId}/download
(variant=browse|original 两级开关校验;**新增错误码 DOWNLOAD_DISABLED**,
errors.ts 与 Error enum 两侧同步,A18);
- admin 账号治理:GET /admin/users 统一为 listAdminUsers 双模式 + PATCH
/admin/users/{userId} 与 reset-password / activate / deactivate;
- admin 用户同步:GET /admin/user-sync/{status,runs,errors} + POST runs
手动补拉(互斥复用进行中批次,A29);
- admin 用量:GET /admin/usage/summary + PUT thresholds(null 清除、
即刻重评估,A23)。
偏差登记①——/admin/users 路径冲突(P1b lookupUsersByAccount):P4a 决策
统一为双模式端点(前缀模式沿用 P1b 语义与任何已认证工作人员可用;管理
列表模式 query/status 过滤 + 游标 + 仅 admin),响应统一 AdminUserList
(AdminUser 字段超集,游标元信息随行)。消费方同步:admin 邀请搜索
(fetchLookupUsers/类型镜像,行为不变)与 api 过渡实现(仍仅前缀模式、
limit 对齐 1..100 缺省 20;管理列表模式与仅 admin 判定随 Task 6 落地,
users-lookup e2e 已按统一形状修订)。
偏差登记②——apps/mobile 手写镜像未随本次更新:其邀请流程仍按旧 3 字段
投影消费(AdminUserList 为超集,运行时兼容,mobile IT 不经该路径);列
为 P4a 跟进项,仅当其 e2e 破坏时处理。
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-20 15:56:38 +08:00
浅念
fb3c278e50
test(mobile): integration suite against real backend on mumu
...
P3 Task 11 (WRITE & COMPILE phase; device run pending MuMu availability).
- integration_test/p3_flow_test.dart: login -> album (zh+en) -> category
-> invite -> enqueue 3 real JPEGs -> queue completed -> server history
(sourceDeviceId) -> ov public API sees 3 -> hide via API + UI sync ->
DIO fault-gate offline (waitingNetwork -> retry -> 4 completed) ->
password change (session kept) -> relogin -> in-app locale smoke.
- integration_test/p3_background_test.dart: IT_PHASE=prepare/verify spec
for HOME-background continuation and force-stop kill-restart recovery
(2 x ~14MB noise JPEGs give a deterministic mid-flight window).
- helpers: dart-define config, fault-injecting DIO adapter, app harness
(picker/notification/locale provider overrides), UI step library with
live-binding polling (no pumpAndSettle).
- scripts/run-mobile-it.sh: device detect (exit 2), backend ensure
(compose + api/ov/worker/sync, exit 3), idempotent per-run user seed
(no DB reset), APK build/install + adb push of testdata, phase 1 /
--background phase 2 orchestration with server-side confirmed-count
truth via curl.
- services/api seed:it script (create-if-absent staff accounts).
- debug-only network security config: cleartext HTTP permitted to
10.0.2.2/127.0.0.1/localhost only (release unaffected).
- testdata: 3 x ~120KB + 2 x ~14MB generated JPEGs.
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-19 05:35:16 +08:00
浅念
31ba03de11
feat(api): password change endpoint and source device id on uploads
...
P3 Task 3 契约增补批次(单一事实来源 openapi.yaml + cn 实现 + e2e):
- POST /v1/auth/password-change(operationId changePassword,需求 4.1 的
P1 契约缺口补齐):Bearer + {oldPassword 1-128, newPassword 8-128} → 204;
旧密码错误 401 AUTH_INVALID_CREDENTIALS;成功后事务内更新 argon2 哈希、
撤销该用户除当前会话外的全部其他会话(当前会话保留,本人不下线)、写
审计 auth.password_change。
- sourceDeviceId(A19 设备来源语义):UploadInitRequest 可选 8-64 位设备
标识;upload_sessions 落列(VARCHAR(64) utf8mb4_0900_bin,可空,存量
行 NULL 兼容);GET /uploads 条目回显;幂等重放不比对(first-writer,
首次落库值不随重放漂移)。
- e2e:auth 增 4 例(未认证 401 / 新密码过短 400 / 旧密码错误 401 且会话
无副作用 / 成功后他端会话撤销当前保留 + 新旧密码登录裁决 + 审计);
uploads 增 1 例(携带落库回显、换设备重放保留首值、缺省 null、长度
越界 400)。api e2e 179 → 184;contracts gen/redocly/单测、worker、
sync 门全绿。
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-19 00:59:39 +08:00
浅念
452ec12bbd
test(sync): cross-region simulation integration suite
...
P2 Task 11: 六幕串行场景套件(cn+ov 双入口进程内起真服务,真实双
MySQL/MinIO/HMAC/WS)——①端到端复制+互动+统计回传(A25 观众路径只打
ov、A27 快照替换)②隐藏传播(high 优先+WS photo.hidden+恢复发布复原)
③发送器停机积压→排空追平→对账稳态④墓碑不复活(旧 published 重放拒绝
+bootstrap 重跑仍隐藏,A26)⑤紧急停用(ov 404/WS error、cn 无感、enable
复原含新照片)⑥50 并发点赞/30 并发浏览幂等小样(终值==Σdelta、恰+15)。
services/sync devDeps 增 ws/@types/ws/ioredis(WS 场景客户端 + 限速
滑窗键清场,同 api spec 约定;版本与 api 对齐)。
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 20:05:25 +08:00
浅念
955bea49b5
feat(sync): stat snapshot return path with version replacement
...
P2 Task 9 统计回传(ov→cn):
- cn 迁移:albums 增 viewCountReplica/statsVersion/statsSyncedAt、
photos 增 statsVersion(like_count 已有副本位;无 VARCHAR 新列不涉校对手决)
- cn 接收端 POST /v1/internal/sync/stat-snapshots(InternalSyncGuard):
整批单事务,逐快照 updateMany WHERE statsVersion < statVersion —— 仅更高
statVersion 以权威 total 整体替换副本(绝不相加),重复/乱序幂等忽略;
accepted 只数实际应用条数;views 应用同时推进 statsSyncedAt(A27 展示)
- ov 回传发送器 services/sync/stat-return.ts:聚合 outbox_stat_events 未发送
行按(kind, photoId|albumId)取 MAX(statVersion) 折叠突发 → HMAC 推送 →
200 后按计数器标记截至该版本全部源行 sentAt;失败留待下轮重发;main.ts
第三循环 STAT_RETURN_INTERVAL_MS(默认 60s,env 可调)
- 契约 AlbumSummary + statsSyncedAt(string|null readOnly,+gen);listAlbums/
getAlbum 经 toSummaryResponse 暴露(管理 UI 列留 P4a)
- e2e(services/sync stat-return.e2e-spec,7 例):逐次点赞回传替换式收敛
(2→5→10 非累加)、浏览窗口去重回传 + statsSyncedAt、重复回传 accepted=0
值不动(A27 不重复累计)、乱序高版本替换(total 可下调)+ 低版本忽略、
发送端折叠只发最高版本且被折叠行随批确认、管理列表 likeCount/statsSyncedAt
展示、新端点 401/400 守卫
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 18:45:43 +08:00
浅念
1f2a0c658f
feat(ov): windowed dedup visit counting with retention cleanup
...
Task 8 (P2): POST /public/albums/{token}/visits with visit_sessions
window dedup (VISIT_WINDOW_MINUTES), requestId idempotent replay
(echoes stored outcome; expired beyond VISIT_REQUEST_TTL_MINUTES
rejected), album_stats viewCount/statVersion authority + outbox_stat_events
views snapshots, WS stats.updated broadcast, visitorId+IP fixed-window
rate limit (VISIT_WINDOW_REQUESTS), and ov CleanupModule scheduler
(setInterval, CLEANUP_INTERVAL_MINUTES) deleting like_batches /
visit_requests / visit_sessions past their windows — totals never
touched. Contract prose aligned to replay semantics; constants
VISIT_WINDOW_REQUESTS / CLEANUP_INTERVAL_MINUTES added.
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 18:14:01 +08:00
浅念
85b2da3db5
feat(ov): idempotent batched likes and realtime broadcast
...
P2 Task 7(点赞批次端点 + ov 实时网关):
- interactions 模块(仅 ov 装配):POST /public/albums/{token}/photos/
{photoId}/likes——相册不可见/照片未发布 404、delta 1..20 与
sessionId/batchId 1..64 校验 400;单事务 like_batches 唯一插入
(P2002=幂等重试,batchAccepted=true 不重加)→ photos_ov.likeTotal/
countVersion 原子自增回读 → outbox_stat_events(kind=likes) 兜底;
Redis ov(REDIS_URL_OV)INCRBY+TTL 会话/IP 双窗口限流(默认
500/2000,超限 429 RATE_LIMITED),幂等重试先于限流不耗额度
- 过期批次重放边界(24h 清理后的 batchId 重插会再次计入):接受该
边界——无永久账本可查(保留全量批次行违背清理策略),会话/IP 窗口
上限即重放天然封顶(同一 sessionId 重放挤占自身额度),偏差已在
likes.service 头注释登记,Task 8 统计侧按清理后语义处理
- realtime:ContractWsAdapter(契约 {type,...} 帧分发,ws 与 HTTP 同
端口挂 /v1/realtime)+ RealtimeGateway 相册房间模型——subscribe 逐
相册校验 isAlbumVisibleOv,失败回 error 帧且连接保持;应用层 ping→
pong 回显 ts;传输层 HEARTBEAT_MS 周期 ping 踢死连接;点赞提交经
EventEmitter2(单进程,多实例 Redis pub/sub 登记 P5)触发
likes.burst 500ms 合并窗(delta 聚合、originBatchIds 供客户端过滤
自身批次);sync-inbox 应用事务提交后发 OV_PHOTO_APPLIED →
photo.published/hidden/deleted 按房间广播(stale/duplicate/暂存/
缺席即撤下不转发)
- 契约:REALTIME_WS_PATH、ServerMessage 增 error 帧(穷尽锁 7 成员
+ 类型测试)、LIKE_SESSION_WINDOW_DELTA/LIKE_IP_WINDOW_DELTA/
LIKE_BURST_MERGE_MS 常量与 .env.example
- e2e(likes-realtime.e2e-spec.ts,6 用例):A09 三批累加/幂等重试/
outbox 行、小额度 429、ping/pong+幽灵 token error 帧连接保持、
A10 双房间隔离(先证非空调订)、合并窗单 burst(delta 求和+双
originBatchIds)、cn 隐藏→房间收 photo.hidden+隐藏后点赞 404;
凡进程内起 AppOvModule 的 spec(sync×2/public-ov/emergency)补装
ContractWsAdapter 与生产 main-ov 一致——api e2e 168(162+6)
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 17:51:55 +08:00
浅念
0fcf18cc5c
feat(ov): public read api on replicas with hot sorting
...
P2 Task 6(ov 公开读 API + viewer 切换):
- ov 装配 PublicOvModule(仅 app-ov.module):/public/albums/{token}、
/photos、/photos/{id} 三端点同契约读 ov 副本——translations/categories
自 JSON 列解析(防御性过滤),banner 经 ov 侧 STORAGE_PORT_OV 预签名
(同本地 MinIO;只读属约定,真实 OSS 只读凭证 P5),viewCount 取
album_stats 权威累计(无行默认 0;计数端点 Task 8 接线)
- 排序:latest=发布时间 keyset(shared/cursor.ts 同 cn);hot=likeTotal
desc + photoId desc 决胜,顺序按 `${albumId}|${category}` 短期快照
(内存 TTL 默认 30s,HOT_SORT_TTL_MS 惰性读取)——计数逐请求现读、
顺序冻结窗口内一致,偏移游标(base64url 整数)在快照内严格稳定;
非法 sort 400 VALIDATION_FAILED(cn 不感知该参数)
- 契约:viewCount 放宽为 integer|null(cn null、ov 权威值);
listPublicPhotos 增 sort 枚举(latest|hot,缺省 latest);
PhotoPublishedPayload 增 categoryId——补上 Tasks 3/4 管线缺口
(payloads/publish.service/sync-inbox 投影 + 关联测试随改)
- e2e(services/api/test/public-ov.e2e-spec.ts,7 用例):双入口同进程
起 cn+ov,cn API 种子→真实发送管线投递;断言相册/列表/单图与 cn
深等(仅签名 URL 剔除)、banner 真字节、参数矩阵 400/404、hot 排序
TTL 内快照(计数新/顺序旧)+TTL 过期翻转+偏移翻页、隐藏/下线/紧急
停用逐态撤下
- viewer 切 ov:API 基址 VITE_API_BASE(缺省 http://127.0.0.1:8081 ,
空串=同源网关;Vite 只读 apps/viewer/.env*,根 .env 对前端无效),
移除 /v1 dev 代理(CORS_ORIGINS 增 127.0.0.1:5175 直连两形);
playwright webServer 增 dev:ov;e2e 夹具 flushSync(albumId)=cn
bootstrap 快照 + deliverPendingOnce(sender 新导出)经真实管线投影
种子,deleteSeededPhoto 改软删让墓碑可同步;14 个 Playwright 全绿
(journey 管理/staffB 腿仍走 cn 8080)
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 17:05:56 +08:00
浅念
eac8da6d6c
feat(ov): emergency disable channel independent of sync
...
P2 Task 5(海外紧急停用入口):
- ov 新增 POST /v1/internal/emergency/disable|enable(InternalSyncGuard
HMAC,albumToken/albumId 恰好其一、都缺省 400;副本不存在 404
ALBUM_NOT_FOUND):直写 albums_ov.emergencyDisabled(唯一 ov 本地可写
位),独立于同步版本链——更高版本 album.updated 重放不解除(e2e 断言
entityVersion 推进而标志保持),恢复仅经显式 enable;幂等语义
(alreadyDisabled/wasDisabled 入审计行)
- cn 侧不感知:无对应 cn 状态位、无反向同步(契约描述文档化)
- 审计:pino child(audit=emergency 字段常驻每行)结构化日志——偏差
登记:计划原文「写 ov 侧日志文件」落地为字段标记而非独立文件通道
(免新增文件 transport 依赖,P5 运维按字段路由聚合);注入令牌
EMERGENCY_AUDIT_LOGGER,测试覆写为内存目的地断言字段
- isAlbumVisibleOv 共享判定(status=active && publicEnabled &&
!emergencyDisabled——镜像 cn 公开语义叠加本地停用位):Task 6 公开
API / Task 7 WS subscribe 的 404 ALBUM_NOT_PUBLIC 语义单一实现,
判定矩阵单测锁定
- 契约 [internal] 增 emergency disable/enable 操作与 schema(gen 已跑、
redocly 通过);e2e:token/id 定位、幂等、enable 恢复、未签名 401、
均缺省 400、未知相册 404、同步事件不解除
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 16:19:02 +08:00
浅念
36082c5a74
feat(sync): reconciliation scheduler with digest and bootstrap healing
...
P2 Task 4(inbox 对账):
- services/sync 新增 SyncReconciler(runOnce 可测;main 双循环,间隔
SYNC_RECONCILE_INTERVAL_MS env 可调,CN_API_URL 指向 cn 入口):每轮
先按 album/category/photo/asset 逐类分页拉取 cn reconcile「当前状态
事件」并直接签名推送到既有 content-batches 管道。sinceVersion 取 0
(全量当前态)而非 per-type 已应用最大版本——cn reconcile 以类型级
全局版本阈值过滤,类型内单实体滞后会被 max 掩蔽;ov 的 eventId 幂等
+ 版本单调把已追平实体全部判重(recon 确定性 eventId 重扫零成本),
偏差登记:计划原文对账返回「缺失清单」,契约已定为可重放 SyncEvent
数组(免建第二条拉取通道),openapi 描述同步更新
- cn 新增 POST /internal/sync/digest:每相册权威计数 {categoryCount,
publishedPhotoCount, photoCount, assetCount};ov 比对副本计数(照片
取 [publishedPhotoCount, photoCount] 区间口径——hidden/deleted 照片
若从未以 published 态同步过 ov 合法无行,缺席即撤下),不一致相册
自动触发 cn bootstrap 重建;ov-only 相册(cn 行已删,bootstrap 无法
表达删除)告警登记不自动处置
- 收敛语义两处落位:① sync-inbox 版本比较改「严格更旧才拒绝」——
同版本新 eventId(bootstrap/reconcile 快照重发)为当前态幂等重断言
应用,修复 ov 缺失副本行;A26 不受影响(更旧版本仍拒绝,cn 侧
entityVersion 与状态一一对应)② album.updated 快照载荷(bootstrap/
reconcile)携带分类全量清单,ov 存在即整体替换 albums_ov.categories
——修复错过的 category 墓碑(Task 3 登记边界:cn 行已删,无现存实体
事件可重放,唯全量替换可收敛)
- e2e(reconcile.e2e-spec):漏事件追赶(ov v1/cn v5 事件投后即失)→
清扫追平;digest 修复缺失 photos_ov 行(bootstrap 重发 → 同版本
重断言重建);digest 修复丢失的分类墓碑(全量清单替换收敛);稳态
空转(applied=0、无 bootstrap);Task 3 套件保持绿
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 16:18:31 +08:00
浅念
65726702f3
feat(sync): cn event coverage, bootstrap and outbox sender
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 15:21:41 +08:00
浅念
3d6e9ce175
feat(ov): ov schema, entry skeleton and compose profile
...
P2 Task 2 ov 数据层:
- compose ov profile:mysql-ov(mysql:8.4,127.0.0.1:3309,库级
utf8mb4_0900_bin)+ redis-ov(6381),cn 侧默认 up 不受影响
(3307/3308 已被其它本机项目占用,端口决策 3309)
- @photo/db-ov:9 表 schema(albums_ov/photos_ov 副本、sync_inbox/
sync_state 同步账本(水位行同表标志位)、like_batches/album_stats/
visit_sessions/visit_requests 互动域、outbox_stat_events 回传队列);
迁移 20260918060102_init_ov 走固化校对手决流程,information_schema
复核全部业务表/列 utf8mb4_0900_bin(_prisma_migrations 同 cn 先例
保持 unicode_ci)
- generator 独立 output(包内 generated/):pnpm 将两包 @prisma/client
提升为同一 store 实例,默认 .prisma/client 输出会被双 schema 互相
覆盖,ov 客户端独立产出经 src/index.ts 转出口
- ov 入口骨架:main-ov.ts(PORT_OV 默认 8081)+ app-ov.module.ts
(仅管线骨架 + /v1/health,feature modules 后续任务装配);logger
装配选项抽取至 shared/logger-options.ts 供 cn/ov 双入口共用(行为
不变,P1 套件守护)
- test/reset-ov.ts:ov 库 truncate-if-exists 隔离工具(无 MinIO 清理)
- 根脚本:db:generate 双库齐跑,新增 db-ov:migrate/db-ov:generate/dev:ov
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 14:11:41 +08:00
浅念
8ba98ace63
feat(api): management photo list and account lookup
...
P1b Task 4: listAlbumPhotos (GET /albums/{albumId}/photos, tag
[photos]) + lookupUsersByAccount (GET /admin/users, tag [admin]).
- management list: all visibility states visible (unpublished/
published/hidden/deleted) for owner/admin; collaborator 403,
outsider/ghost album 404 (canView -> canManage ordering, same
anti-enumeration as members). Items = PhotoRecord + uploader
{userId, account, displayName} (flat-FK second query, redacted).
Keyset (uploadedAt,id) DESC via shared/cursor; visibility filter
validated against PhotoVisibility enum (400 otherwise), categoryId
must belong to the album (ghost/cross-album same-code 400).
pending_upload rows (uploadedAt NULL) excluded: the sort key is
both the contract-required field and the keyset predicate - NULL
can neither be shaped as PhotoRecord nor compared; in-flight
uploads are covered by the uploads history endpoint.
- account lookup (new src/admin module): prefix search over active
users only (credentialStatus=active, sourceStatus!=disabled,
!localLocked - same eligibility as SessionGuard/members.add),
account ASC, no cursor (small cap). $queryRaw LIKE prefix% with
explicit ESCAPE '!': !/%/_/\ escaped to literal matching (Prisma
startsWith escaping is not contractual); utf8mb4_0900_bin makes
the prefix match case-sensitive ('AL' misses 'alice'), documented
in contract + tests. Access policy per contract: any authenticated
staff (deviation from plan's admin/owner), unauthenticated 401.
- e2e: photos-admin (6) + users-lookup (6) cases; api e2e 136 -> 148.
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 08:54:44 +08:00
浅念
361a72f300
feat(api): album asset upload chain
...
P1b Task 3: initAlbumAssetUpload / completeAlbumAssetUpload (tags
[assets]) over album_assets with in-row one-shot sessions.
- migration asset_state: album_assets + state (pending|effective),
expectedBytes, contentType (utf8mb4_0900_bin); pre-existing rows
backfilled to effective. Complete head-verifies bytes AND
contentType vs the init declaration (P1a Task 11 server-side
fallback), then conditionally flips pending->effective with the
winner writing the album.asset.add audit (resourceId=albumId,
metadata assetId/kind/locale). Effective rows re-complete
idempotently (re-signed URL, no re-verify, survives object
cleanup).
- object key is the final key (albums/{albumId}/assets/{assetId}.{ext});
the pending row is the logical staging - no copy step, keys embed
assetId so re-init can never overwrite another row's object.
- permissions: owner/admin only (collaborator 403, outsider 404);
uploadEnabled does NOT gate assets (switch governs photo uploads
per 4.9 - both endpoints carry no ALBUM_UPLOAD_DISABLED).
- public banner now locale-aware (req 4.7): effective rows resolved
as display-locale row -> global (locale NULL) row -> null, latest
wins per tier (same-kind re-upload replaces, old rows retained).
- e2e: test/assets.e2e-spec.ts (14 cases, real jpegs via sharp,
MinIO round-trips); public spec seed marked effective.
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 08:40:32 +08:00
浅念
47fb81ad31
chore(api): p1a final-review hygiene items
...
- contracts: add NOT_FOUND error code (framework-level 404 for unknown
routes); Error enum + regen committed
- api: 404 default mapping in AllExceptionsFilter now NOT_FOUND (was
ALBUM_NOT_FOUND); business 404s unchanged via explicit ApiException codes
- api: extract configureApp (prefix/CORS/x-powered-by) shared by main-cn
bootstrap and e2e; disable x-powered-by, asserted in e2e
- api: move parseCursor/parseLimit (+page-size bounds) from
albums.controller to shared/pagination; members/uploads/public re-pointed
- infra: bind compose published ports to 127.0.0.1 (mysql/redis/minio);
.env.example endpoints follow suit
- e2e: unknown-route 404 NOT_FOUND + x-powered-by absent cases added
(api e2e 120 -> 122)
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 08:18:30 +08:00
浅念
96057ba207
test(api): full-chain e2e covering A01 A02 A03 A06
...
One spec chaining the whole business flow over public APIs only:
seed-admin + real password login, album/category/member invite,
same-filename double upload via presign PUT (idempotent complete),
real BullMQ worker consumption publishing both photos (outbox has
exactly two photo.published events), public feed with locale
fallback, cursor pagination and signed-URL bytes, hide/remove/
soft-delete lifecycle with immediate permission loss, and full
audit-trail assertions.
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-18 00:03:35 +08:00
浅念
6d61d117d5
feat(api): photo visibility lifecycle with audit and outbox events
...
Serializes turbo tasks (concurrency=1): api and worker e2e suites share
one MySQL/MinIO and resetDb TRUNCATEs raced when run in parallel.
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-17 23:51:29 +08:00
浅念
1f1c8ce191
feat(api): public photo feed with stable cursor pagination
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-17 23:39:50 +08:00
浅念
6863c736c3
feat(api): public album info with locale fallback
...
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-17 23:33:57 +08:00
浅念
94357e9f03
feat(worker): publish transaction with outbox event and compensating sweep
...
Task 21: PublishService.processPhoto streams the original from storage,
runs the sharp pipeline, puts thumb/browse variants under
photo-live/dev/albums/{albumId}/variants/{photoId}/1/, then commits ONE
Prisma transaction: photo_variants upserts (thumb+browse), a conditional
photos update WHERE processingState IN (uploaded, processing_failed) that
flips to ready+published, stamps publishedAt/width/height, clears
errorCode and bumps entityVersion, and the outbox_events row
(photo.published, payload = contracts PhotoPublishedPayload with the
post-increment entityVersion). A zero-row conditional update means a
concurrent winner already published -> idempotent 'already' with no
second event and no extra version bump. The BullMQ worker wrapper
(QUEUE_PHOTO_PROCESS, concurrency 2, jobId 'process-{photoId}' without
the reserved ':') claims photo_jobs pending->active (attempt++), marks
done for published/already/skipped alike, and rethrows pipeline/storage
errors for the producer-configured 3-attempt exponential backoff; the
final attempt (BullMQ increments attemptsMade only after
moveToFinished, so in-processor it is 0-based: current = attemptsMade+1)
marks photo_jobs failed + photo processing_failed with the
PipelineError code while visibility stays unpublished. sweepOnce()
claims due pending rows (nextRetryAt IS NULL OR < now) and re-enqueues
stale (>5min) active rows with jobId dedupe; startSweepInterval(30s)
boot-scans then loops, and main.ts wires pino logging, dedicated redis
connections and graceful SIGINT/SIGTERM shutdown.
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-17 23:21:57 +08:00
浅念
64a2cec788
feat(worker): sharp pipeline with orientation, variants, exif strip
...
Task 20: services/worker package (@photo/worker, commonjs) with jest
unit/e2e configs mirroring api conventions (root .env via test/load-env.ts,
moduleNameMapper to @photo/contracts/db-cn/storage TS sources, maxWorkers 1,
test/reset.ts = MySQL TRUNCATE + MinIO photo-live/dev prefix wipe) and the
sharp processing pipeline runPipeline(): metadata whitelist gate
(jpeg/png/webp/avif/heif/tiff, else PipelineError UNSUPPORTED_TYPE),
decode failures mapped to PipelineError DECODE_FAILED, no-arg .rotate()
auto-orientation (EXIF 5-8 swaps stored width/height), thumb <=480w q75
and browse <=1920 q82 JPEG variants with withoutEnlargement, and no
withMetadata so outputs carry no EXIF/GPS at all. sharp 0.35 prebuilt
loads script-free under pnpm 10; its win32 libvips lacks a HEVC encoder,
so the HEVC-HEIF unit case self-skips with a console note (G2 verifies
with real iPhone samples) while the AV1-HEIF case covers the same
libheif decode path / 'heif' format bucket.
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-17 23:16:20 +08:00
浅念
f5f1855b58
feat(api): upload verify, forbid-overwrite copy, idempotent complete
...
Task 19: POST /uploads/{id}/renew re-signs the SAME staging key for pending
sessions (uploadEnabled=false -> 409 ALBUM_UPLOAD_DISABLED; non-owner/unknown/
removed-member -> 403 AUTH_FORBIDDEN, indistinguishable per contract) and
slides the session expiry — renew is the documented recovery path for an
expired credential. POST /uploads/{id}/complete verifies the staging object
out-of-transaction first (head: missing / byte-count mismatch / contentType
mismatch — the server-side backstop for presign not signing content-type),
then copyIfAbsent's it to the finals key with forbid-overwrite semantics,
then in ONE transaction updates photo(bytes/uploadedAt/uploaded), claims the
session pending->confirmed via conditional updateMany (concurrent completes:
exactly one winner creates the photo_jobs row + upload.complete audit), and
enqueues QUEUE_PHOTO_PROCESS post-commit (jobId derived from photoId for
dedupe; failure only logs — Task 21 sweep compensates on photo_jobs). Verify
failures are request-level 400s: the session stays pending and retryable.
Repeat complete on a confirmed session re-runs copyIfAbsent (created:false,
staging already cleaned is fine) and returns the current processingState —
no second job/audit/enqueue (A03). GET /uploads lists the caller's own
sessions newest-first with (createdAt,id) keyset pagination and the joined
photo processingState/visibility. Both producer and test Queue instances now
quit their caller-owned ioredis connection (BullMQ does not take ownership),
otherwise jest hangs on open handles.
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-17 23:00:19 +08:00
浅念
a2a3ccd340
feat(api): idempotent upload session init with presigned credential
...
POST /v1/uploads (Task 18, A03): @@unique(userId, clientTaskId) idempotency
anchor — same params return the original session (201, no duplicate photo);
param drift (album/bytes/contentType/extension) -> 409 UPLOAD_SESSION_CONFLICT.
photos.objectKey is set to the FINAL originals key at init while the session
holds the staging key; IDs come from an app-side cuid-like generator so both
self-referencing keys are written in one transaction. Credential is a real
MinIO presigned PUT (SIGNED_URL_TTL_SECONDS, lazily read). Permission order:
album invisible 404 -> member 403 -> uploadEnabled 409 -> size/ext 400 ->
category ownership 400. No audit on init (audit anchors on complete).
Pre-step for Task 19: QUEUE_PHOTO_PROCESS contract constant + lazy-safe
BullMQ queue producer (Redis down at boot must not crash the app; enqueue
failure only logs — sweep compensates). e2e reset now also wipes the MinIO
photo-live/dev prefix; jest maps @photo/storage to TS source.
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-17 22:42:13 +08:00
浅念
e04e1f3bdf
feat(api): members and categories management
...
成员(/v1/albums/{albumId}/members):owner/管理员邀请已开通同事为协作者
(重复 409 MEMBER_CONFLICT、owner 目标 400、未开通/停用账号 400)、成员列表
((addedAt,id) DESC 键集分页)、移除协作者(幂等 204;成员行是唯一权限来源,
删除行即失去可见性 A02;owner 不可移除 403)。分类(/albums/{albumId}/categories
与 /categories/{categoryId}):创建/部分更新/删除,translations 全量替换,
相册详情按 sortOrder 升序带出;404 统一 ALBUM_NOT_FOUND(分类不存在与相册
不可见同码)。审计 album.member.add/remove、album.category.add/update/remove
(resourceId=albumId,metadata 记目标 id/字段)。共用错误/P2002 判定抽至
support.ts(修复 meta.target 为 DB 约束名时的误判)。
Co-Authored-By: Claude Code <noreply@anthropic.com >
2026-09-17 22:16:16 +08:00