TL;DR(人话)
在 postgres 生产库上,Console 记录编辑弹窗每次保存都弹「保存冲突/记录已被其他人修改」,PATCH 恒 409 CONCURRENT_UPDATE——哪怕库里这条记录从建出来就没人碰过。sqlite 开发环境完全无感,所以一直没人发现。
版本与环境
- objectos-ee 镜像 4.1.1(内建 runtime 17.2.0,builtAt 2026-08-24),
@objectstack/rest / @objectstack/driver-sql 随镜像 - postgres:16-alpine,
OS_DATABASE_DRIVER=postgres,TZ=Asia/Shanghai - 单节点即可复现(与集群/副本数无关,已用 1 副本对照验证)
最小复现(不依赖任何应用包)
任意开 timestamps 的对象,postgres 驱动:
GET /api/v1/data/<object>/<id> → 响应里 updated_at: "2026-08-30T10:19:25.947Z"(ISO,带毫秒)PATCH /api/v1/data/<object>/<id>,body 带 expectedVersion: "2026-08-30T10:19:25.947Z"(即第 1 步读到的值,Console 编辑弹窗就是这么发的)- → 409,永远:
{
"error": "Record os_tianshun_ehr_project/kAE91j_gPi7CscPW was modified by another user (current version Sun Aug 30 2026 18:19:25 GMT+0800 (China Standard Time), expected 2026-08-30T10:19:25.947Z)",
"code": "CONCURRENT_UPDATE",
"currentVersion": "Sun Aug 30 2026 18:19:25 GMT+0800 (China Standard Time)"
}注意:报错里的 current 与 expected 是同一瞬间(18:19:25+08:00 == 10:19:25.947Z),却被判为冲突。库里该行 created_at == updated_at == 2026-08-30 18:19:25.947+08,审计日志只有一条 create——不存在并发写。
根因(已读 dist 定位)
packages/rest 的 OCC 门:
normaliseVersionToken(v) 对版本令牌做 String(v)。postgres 驱动返回的 updated_at 是 JS Date,String(Date) 产出 "Sun Aug 30 2026 18:19:25 GMT+0800 (China Standard Time)" —— 毫秒被丢弃、格式随进程时区漂移;assertVersionOf() 拿它与客户端回传的 ISO 字符串做严格字符串比较currentVersion !== expected → 同一瞬间恒不等 → ConcurrentUpdateError。
sqlite 驱动把 updated_at 以 ISO 字符串存取,两边字符串恰好相同,所以 dev 从不复现——这是个只在 Date 型驱动(至少 postgres)上暴露的驱动相关缺陷。
违反的契约
assertVersionOf 自己的 docblock:"When the caller passes a non-empty expectedVersion token (typically the updated_at value they read), a mismatch throws"——按此契约,客户端原样回传它读到的 updated_at 必须判为匹配。现实现对 Date 型驱动必判不匹配。
建议修法(仅供参考)
两边令牌先尝试 Date.parse,可解析则按时间值(epoch ms)比较,不可解析再退回字符串比较;或 normaliseVersionToken 对 Date 实例统一转 ISO。
我们没有做的事
应用侧没有加任何 workaround(不吞 409、不禁用 OCC)。终端用户目前只能每次在「保存冲突」弹窗点「覆盖保存」强行通过——所有 postgres 生产部署的所有对象编辑都受影响,爆炸半径是整个 Console 编辑链路。
Part of steedos-labs/os-project-titanwind-ehr (deploy-ee 集群可行性实测中发现;单节点复现)
TL;DR(人话)
在 postgres 生产库上,Console 记录编辑弹窗每次保存都弹「保存冲突/记录已被其他人修改」,PATCH 恒 409
CONCURRENT_UPDATE——哪怕库里这条记录从建出来就没人碰过。sqlite 开发环境完全无感,所以一直没人发现。版本与环境
@objectstack/rest/@objectstack/driver-sql随镜像OS_DATABASE_DRIVER=postgres,TZ=Asia/Shanghai最小复现(不依赖任何应用包)
任意开 timestamps 的对象,postgres 驱动:
GET /api/v1/data/<object>/<id>→ 响应里updated_at: "2026-08-30T10:19:25.947Z"(ISO,带毫秒)PATCH /api/v1/data/<object>/<id>,body 带expectedVersion: "2026-08-30T10:19:25.947Z"(即第 1 步读到的值,Console 编辑弹窗就是这么发的){ "error": "Record os_tianshun_ehr_project/kAE91j_gPi7CscPW was modified by another user (current version Sun Aug 30 2026 18:19:25 GMT+0800 (China Standard Time), expected 2026-08-30T10:19:25.947Z)", "code": "CONCURRENT_UPDATE", "currentVersion": "Sun Aug 30 2026 18:19:25 GMT+0800 (China Standard Time)" }注意:报错里的 current 与 expected 是同一瞬间(18:19:25+08:00 == 10:19:25.947Z),却被判为冲突。库里该行
created_at == updated_at == 2026-08-30 18:19:25.947+08,审计日志只有一条 create——不存在并发写。根因(已读 dist 定位)
packages/rest的 OCC 门:normaliseVersionToken(v)对版本令牌做String(v)。postgres 驱动返回的updated_at是 JSDate,String(Date)产出"Sun Aug 30 2026 18:19:25 GMT+0800 (China Standard Time)"—— 毫秒被丢弃、格式随进程时区漂移;assertVersionOf()拿它与客户端回传的 ISO 字符串做严格字符串比较currentVersion !== expected→ 同一瞬间恒不等 →ConcurrentUpdateError。sqlite 驱动把
updated_at以 ISO 字符串存取,两边字符串恰好相同,所以 dev 从不复现——这是个只在 Date 型驱动(至少 postgres)上暴露的驱动相关缺陷。违反的契约
assertVersionOf自己的 docblock:"When the caller passes a non-emptyexpectedVersiontoken (typically theupdated_atvalue they read), a mismatch throws"——按此契约,客户端原样回传它读到的updated_at必须判为匹配。现实现对 Date 型驱动必判不匹配。建议修法(仅供参考)
两边令牌先尝试
Date.parse,可解析则按时间值(epoch ms)比较,不可解析再退回字符串比较;或normaliseVersionToken对Date实例统一转 ISO。我们没有做的事
应用侧没有加任何 workaround(不吞 409、不禁用 OCC)。终端用户目前只能每次在「保存冲突」弹窗点「覆盖保存」强行通过——所有 postgres 生产部署的所有对象编辑都受影响,爆炸半径是整个 Console 编辑链路。
Part of steedos-labs/os-project-titanwind-ehr (deploy-ee 集群可行性实测中发现;单节点复现)