fix(content): V26 修章节/Block 软删后 order_no uk 碰撞(partial index,待人类应用 flyway)+ 软删 uk 全仓审计

审计(用户授权):Explore 扫 177 uk,22 高危/137 中危/18 安全(yudao 软删 + ~99% uk 漏 WHERE deleted=false)。
精炼剔假阳(幂等/递增键不复用),真可复用业务键 uk = content order_no×2(确证)+ meta/ai/knowledge *_key×13。
全仓 ON CONFLICT 仅引用 command_id/id/asset/kb/validation/source,22 高危均不被引用→改 partial 安全。

V26 修 chapter+block order_no 改 partial index WHERE deleted=false(block create L294 确证同 selectCount+1 bug)。
应用需重启 48080 跑 flyway(DDL 改真 PG,auto-mode 归人类执行,未绕过)。chapter-create-delete e2e 保持
test.fixme、注释指向 V26,人类应用后转正。其余 13 业务键 uk 建议 V27 按模块逐步修(账本有清单)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lili 2026-06-21 19:09:59 -07:00
parent a30a9686ff
commit 172261f540
3 changed files with 32 additions and 1 deletions

View File

@ -162,6 +162,7 @@
- ✅ **2026-06-21:补权益配额/用量归属真后端 e2e + 个人中心 account 面 e2e 全闭环**。`UsageStats`(GET /account/entitlements 套餐/配额/发布能力 + GET /account/usage Token 用量/按归属分布)此前无专门 e2e(live-read 仅断言 profile nickname、未覆盖 UsageStats);curl 证两端 code:0 真后端就绪(非 facade unavailable)。补 `account-usage.spec.ts`(验 200/code:0 + 「权益与配额」/「归属分布」区块渲染,不依赖数值,commit `1dbbfc7`)。**至此个人中心 account 面真后端 e2e 全闭环**:profile 读(live-read)+ 写(account-profile-update)、权益配额、用量归属、购买/授权/发布三件套(account-market-records + account-publish-records)、安全事件 ack(account-security-ack)。**全量 e2e 41/0**(本会话 35/1→41/0:净增 6 真后端 e2e、修 market-publish 慢一拍 + profile update 500 两真因、清 agent 测试污染)。
- ✅/⚠️ **2026-06-21:修新建章节缺契约字段(前端真 bug)+ 挖出后端 order_no 软删 schema bug(反假绿,后端待人类定 DDL)**。延 #30 盘 content/editor 旅程发现:前端 `useChapterCreate` 只传 `{title}`,而后端 `ChapterCreateReqVO` 强制 `commandId`(幂等)+ `expectedWorkRevision`(作品乐观锁),缺失被 @Valid 拦成 400——dev mock 不校验长期掩盖、真后端建章必败。修:补两字段 + 经 WorkspacePage→ChapterPanel(props)→useChapterCreate 透传 work.revision、建章后失效 workDetail 缓存(commit `ca33c8e`);curl 证干净作品 create code:0。**修前端后请求合法到达后端,又暴露独立后端 bug**:`createChapter` 用 `orderNo=selectCountByWorkId(active)+1`,章节 `deleteById` 是软删(行仍在表),而 `uk_muse_content_chapter_work_order UNIQUE(tenant_id,work_id,order_no)` **不含 deleted**(同文件 `uk_muse_content_chapter_command` 却是 `WHERE command_id IS NOT NULL` 的 partial index——本该 partial 却遗漏)→ 删章节后再建 order_no 与软删行冲突 → **500**(影响 chapter create/reorder;block 等表 uk 同形、潜在)。属 schema/软删语义系统性问题,按规范归人类定 DDL 修法(**推荐**:uk 改 partial index `WHERE deleted=false`,对齐 command uk 惯例;备选 物理删 / order_no 取含软删 max+1)。e2e `chapter-create-delete.spec.ts` 暂 `test.fixme` 标注待修(tracked 不掩盖)。注:测试在 work1 留了 1 个软删章节行(order=2,active 列表不显示),随后端修复一并清理。**全量 e2e 41/0(+1 fixme)**。
- ✅ **2026-06-21:修删除作品缺契约字段致真后端 400(同 content 写路系统性 gap,纯前端修复)**。续盘发现前端 `useWorkDelete` 调 `api.delete` 不传 body,而后端 `deleteWork` 同样要 `@Valid RevisionCommandReqVO`(commandId + expectedRevision)→ 真后端删作品必 400(dev mock 掩盖);叠加列表 VO `GET /works` **不含 revision**(仅 detail 有),拿不到乐观锁版本。修(纯前端):useWorkDelete 删除前先读 work 详情拿 revision、再带 commandId 提交(commit `59d1396`);WorkListPage 无需改(净零)。curl 实证后端 work create→delete code:0(无 chapter 那类 order_no schema bug,后端 work delete 干净)。补 `work-create-delete.spec.ts`(配对自清理,建→删验 200/code:0 + 列表无残留,**通过**)。**全量 e2e 42/0(+1 chapter fixme)**。**小结(content 写路系统性 gap)**:写命令前端普遍漏后端必填的 commandId/expectedRevision(dev mock 不校验长期掩盖),且部分列表 VO 不暴露 revision——本会话已修 work create(早前)/chapter create/work delete;chapter delete 的 expectedRevision 硬编码=1 对 revision>1 章节仍是隐患(待评估),建议后端列表 VO 统一暴露 revision。
- ⚠️ **2026-06-21:全仓软删表 uk 系统审计(用户授权)+ V26 修 content order_no(待人类应用 flyway)**。Explore 扫 25 个 flyway migration / 177 唯一约束,按「软删表 + uk 不含 deleted + 含可复用业务键」筛:**22 高危 / 137 中危 / 18 安全**(yudao 框架级:业务表继承 BaseDO 自动软删,但 ~99% uk 未加 `WHERE deleted=false` → 软删行仍占唯一键名额、删后重建相同键碰撞 500)。批判精炼(剔 Explore 假阳):22 高危中 `idempotency_key`×2/`version_no`×3/`sequence_no`×2 是幂等/递增键、实际不复用、当前不触发;真可复用业务键 uk = content `order_no`×2(chapter/block,**已确证**)+ meta/ai/knowledge 的 `*_key`/`normalized_name`×13(schema_key/agent_key/prompt_key/policy_key/grant_key/node_key/chain_key/section_key/field_key 等)。**关键安全前提**:全仓 ON CONFLICT 仅引用 command_id/id/asset/kb/validation/source 组合,22 高危业务键 uk **均不被 ON CONFLICT 引用 → 都可安全改 partial**(active 唯一性不变,区别于 V22 须保留完整索引的 command 场景)。已写 `V26__fix_content_softdelete_order_uk_partial.sql`(chapter+block order_no 改 partial;block create L294 确证同 selectCount+1 bug),但**应用需重启 48080 跑 flyway——DDL 改真 PG,auto-mode 归人类执行**(未绕过)。chapter-create-delete e2e 保持 fixme、注释指向 V26,应用后转正。**待人类**:(1)应用 V26(重启 48080);(2)review 其余 13 业务键 uk 是否纳入 V27 按模块逐步修(改 partial 安全但跨模块无 e2e 覆盖、需确认各表删后重建路径)。
- 现状:其余旅程仍 ~14% 真连、dev 默认 MSW;但 MVP#1 已打通"关 MSW→直连活体→playwright e2e"模式(见下),可复用到后续旅程。
- ✅ **AI 候选采纳(rendered UI 活体证,2026-06-14)**:`main.tsx` 加 `VITE_API_MOCK=false` 开关关 MSW、前端直连活体单体;`accept-suggestion.spec.ts` 在 chromium 实跑——**happy-path**(生成经 page.route stub 发真实 suggestionId→点"采纳替换"→真合并)隔离跑绿 + DB 证(block rev1→2、正文=候选);**negative-path**(陈旧 revision)绿(乐观锁 1041000002)。连带**真实修复 client.ts 注入 tenant-id**(活体集成缺口,dev MSW 不校验故没暴露)。**遗留**:happy-path 受种子状态依赖(合并后 block rev 变 + RQ 缓存,需 fresh 种子复跑);`workspace.spec.ts` 已改活体壳冒烟(2026-06-15,MSW 互斥红消除、整套 8/8 全绿);suggestion 合并后留 pending=merge 不在同事务回写 AI 域状态(靠 outbox→AI 异步翻,本机 dispatcher 节流故 DB 仍 pending,读 `ContentSourceServiceImpl` 确认,非产品 bug)。
- ✅ **content/market/account/ai-agent 读旅程(rendered UI 活体证,2026-06-14)**:`live-read.spec.ts` 4/4 绿(MSW off,chromium:作品列表/市场浏览/个人中心/智能体列表均渲染真实后端数据)——证 tenant-id 修复后 studio 直连活体单体跨 BC 取数、真实数据形态与页面渲染契约对齐。

View File

@ -0,0 +1,28 @@
-- 修复 content 章节/Block 软删后 order_no 唯一约束碰撞 schema bug。
--
-- 背景:muse_content_chapter / muse_content_block 继承 yudao 软删(deleted 列 + @TableLogic,
-- 删除是 UPDATE deleted=1、行仍在表)。但 uk_muse_content_chapter_work_order(tenant_id, work_id, order_no)
-- 与 uk_muse_content_block_chapter_order(tenant_id, chapter_id, order_no) 是完整唯一约束、不含 deleted 条件,
-- 软删行仍占 order_no 名额;而 createChapter/createBlock 用 orderNo = selectCount(active)+1(只算未删行),
-- 删章节/Block 后再建同序号 → 与软删行碰撞 → PostgreSQL duplicate key → 500 系统异常。
-- (反假绿:studio 前端补齐 chapter create 契约字段后真后端建章才暴露此 bug;见 docs/mvp/进度总账.md。)
--
-- 修法:改为 partial unique index(WHERE deleted = FALSE),软删行不占名额、active 行唯一性不变,
-- 对齐同库 uk_muse_content_chapter_command 等既有 partial index 惯例。
-- 安全性:order_no uk 不被任何 ON CONFLICT 引用(content 的 ON CONFLICT 仅在 command/outbox 表、用完整 uk),
-- 故改 partial 不影响 insert/ON CONFLICT 仲裁(区别于 V22 须保留完整索引的 command_id 场景)。
-- 幂等:DROP CONSTRAINT/INDEX IF EXISTS 后重建,对已应用 V1 的库与全新库均安全。
-- 章节:DROP 原完整唯一约束,改建 partial unique index(同名)
ALTER TABLE muse_content_chapter DROP CONSTRAINT IF EXISTS uk_muse_content_chapter_work_order;
DROP INDEX IF EXISTS uk_muse_content_chapter_work_order;
CREATE UNIQUE INDEX uk_muse_content_chapter_work_order
ON muse_content_chapter (tenant_id, work_id, order_no)
WHERE deleted = FALSE;
-- Block:同理(createBlock 同样用 selectCountByChapterId(active)+1,删后重建同序号会碰撞)
ALTER TABLE muse_content_block DROP CONSTRAINT IF EXISTS uk_muse_content_block_chapter_order;
DROP INDEX IF EXISTS uk_muse_content_block_chapter_order;
CREATE UNIQUE INDEX uk_muse_content_block_chapter_order
ON muse_content_block (tenant_id, chapter_id, order_no)
WHERE deleted = FALSE;

View File

@ -24,7 +24,9 @@ async function seedToken(page: Page): Promise<void> {
// 但后端 createChapter 用 orderNo = selectCountByWorkId(active)+1,而章节软删后行仍在表、
// uk_muse_content_chapter_work_order 不含 deleted(对比同文件 uk_muse_content_chapter_command 是 partial index)
// → 删章节后再建 order_no 与软删行冲突 → 500。系统性 schema/软删语义问题(chapter create/reorder 受影响,
// block 等表同形潜在),需人类定 DDL 修法(partial index / 物理删 / order_no 含软删 max)。后端修复后改回 test()。
// block 等表同形(已确证 block create 同 selectCount(active)+1 bug)。修法已写入
// sql/muse/V26__fix_content_softdelete_order_uk_partial.sql(chapter/block order_no uk 改 partial index
// WHERE deleted=false);待人类重启 48080 应用 flyway(DDL 改真 PG 归人类执行)后改回 test() 转正。
test.fixme('用户新建并删除章节(真实后端写命令:commandId + 作品乐观锁)', async ({ page }) => {
await seedToken(page);