文獻收斂為第三個實體聚合:TEXT_CODES+TEXT_INSTANCE_DATA 版本層級 - #1267
Conversation
frankslin
commented
Aug 20, 2026
- 新增聚合根 TextImportService:把批次匯入書稿的存儲過程(c_textid 配號、書名正規化與字形標準化、拼音派生、稽核蓋章)抽成單一真源, AdminBatchLoadBookTitlesController 改為委派、只留批次語義
- 文獻的兩重層級分開處理:TEXT_INSTANCE_DATA 版本列屬聚合內部, update 以 (edition_id, instance_id) 為列鍵做集合對賬、delete 先子後父; c_source 自引用(著錄來源樹)為跨實體引用,update 有成環護欄 (422 source_cycle)、delete 引用計數含子文獻與其他文獻版本列
- 實體級 mutation API:resource=text-entity(別名 book/books;不用 text——那是人物著述子資源 BIOG_TEXT_DATA 的既有別名),經通用 EntityAggregate*Handler 分派,direct 與 proposal 皆可用
- 專屬前端頁 /app/text(Index/Create/Edit+版本列編輯器),列表走 EntityTableBrowser、計算欄 instance_count/child_count;側欄 「文獻代碼表」改指實體頁;新增 text_entity i18n(zh-TW/en)
- 裸表寫入收斂拆兩半:text-codes 裸表 create/delete 下架(改回 501, 聚合 API 完整取代);codes UI 封寫暫緩待 parity(作者列表面板、 instance 專屬欄位),補齊後加入 closed_code_tables 即自動封寫
- 同步 API.md §4/§13、ENTITY_AGGREGATE_ARCHITECTURE.md、CHANGELOG
- 已執行 ./vendor/bin/phpunit 全套(既有 SecurityAuditLogTest 環境性 失敗除外全綠)、npm run build、php-cs-fixer
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ba37022ca0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if ($editionId === null || $editionId === '' || !ctype_digit((string) $editionId) | ||
| || $instanceId === null || $instanceId === '' || !ctype_digit((string) $instanceId)) { |
There was a problem hiding this comment.
Reject zero-valued version keys
When an API caller or the new form sends edition_id or instance_id as 0, ctype_digit() accepts it even though these keys are documented here as positive integers. The normalized input then inserts or reconciles TEXT_INSTANCE_DATA rows with c_text_edition_id/c_text_instance_id = 0, creating invalid aggregate keys; parse the values and require them to be greater than zero before accepting the row.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
已修,但沒有照建議無條件拒絕 0——先查了生產庫,那樣會造成比原問題更嚴重的退化。
TEXT_INSTANCE_DATA 共 9,817 列,其中確實存在一列 0 值鍵:
| c_textid | c_text_edition_id | c_text_instance_id | c_instance_title_chn |
|---|---|---|---|
| 40354 | 0 | 0 | 順義縣志 |
其餘 9,816 列的兩個鍵都 ≥ 1。若在 validate() 裡無條件擋下 0,/app/text/40354/edit 會完全無法儲存:編輯頁載入聚合時會把既有版本列原樣帶進表單、送出時原樣回送,於是使用者連一個字都沒碰那列也會吃 422。那是把「防止產生新的無效鍵」換成「讓一筆既有文獻永久唯讀」。
所以改成分兩層,只擋真正會新產生無效鍵的情況:
- create(
ResolvesTextAggregateInput):一律拒絕,新建文獻不可能有既有列 → 422instances.N.key: positive_integer_required。 - update(
TextAggregateDefinition::guardWrite()):比對$existing['instances'],只放行資料庫裡本來就是該 0 值鍵的那一列;任何資料庫中不存在的 0 值鍵一樣 422。
放在 guardWrite() 而不是 validate(),是因為 validate(string $operation, array $changes) 契約上拿不到 target.pk 與既有聚合,而 guardWrite($operation, $id, $input, $existing) 兩者都有。
回歸測試(ApiV2MutateTextEntityTest):
testCreateRejectsZeroVersionKeys:edition_id=0與instance_id=0兩種各自 422,且不落庫。testUpdateRejectsNewZeroVersionKeyButKeepsExistingLegacyRow:既有 0/0 列原樣回送 → 200;新增一個庫中不存在的 0 值鍵 → 422。
| {instances.map((r, i) => ( | ||
| <div key={i} className="rounded-md border border-border p-3"> |
There was a problem hiding this comment.
Use stable keys for version rows
For the editable instances list, using the array index means that removing a row causes React to reuse the DOM for a different (edition_id, instance_id) row, so browser state such as focus or validation can be associated with the wrong version entry. These rows already have a composite key, so key them by that stable identity (with a client-side temporary id for unsaved/blank rows).
AGENTS.md reference: AGENTS.md:L57-L57
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
已修,感謝——這條確實違反了 AGENTS.md §4「React 列表不要使用 index 當 key」。
實作上採「純前端穩定 id」,而不是建議的複合鍵:
interface InstanceRowState extends InstanceRow {
_uid: string;
}
let instanceUidSeq = 0;
const nextInstanceUid = () => `inst-${++instanceUidSeq}`;初始化時替既有列各配一個 uid,addInst() 產生新 uid;setInst()/removeInst() 的定位也一併從索引改成 uid(不只 key 屬性——用索引定位的更新在刪列後同樣會寫到錯的列上)。
沒有用 (edition_id, instance_id) 當 key 的原因:這兩個欄位在這張表單裡是使用者可編輯的輸入框。拿它們當 key 的話,使用者在「版本組號」欄打字時每按一鍵都會換掉 key → 整列 unmount/remount → 焦點與游標位置立刻丟失,反而製造一個比原問題更明顯的體感 bug。純前端 uid 在列的整個生命週期內恆定,既有列與未存檔的新列都適用,不需要另外區分 temp id。
送出時不受影響:payload 是逐欄明列組出來的,_uid 不會外洩到 API。
|
"This branch has conflicts that must be resolved" 这个 issue 拜托 Frank 啦 |
- 新增聚合根 TextImportService:把批次匯入書稿的存儲過程(c_textid 配號、書名正規化與字形標準化、拼音派生、稽核蓋章)抽成單一真源, AdminBatchLoadBookTitlesController 改為委派、只留批次語義 - 文獻的兩重層級分開處理:TEXT_INSTANCE_DATA 版本列屬聚合內部, update 以 (edition_id, instance_id) 為列鍵做集合對賬、delete 先子後父; c_source 自引用(著錄來源樹)為跨實體引用,update 有成環護欄 (422 source_cycle)、delete 引用計數含子文獻與其他文獻版本列 - 實體級 mutation API:resource=text-entity(別名 book/books;不用 text——那是人物著述子資源 BIOG_TEXT_DATA 的既有別名),經通用 EntityAggregate*Handler 分派,direct 與 proposal 皆可用 - 專屬前端頁 /app/text(Index/Create/Edit+版本列編輯器),列表走 EntityTableBrowser、計算欄 instance_count/child_count;側欄 「文獻代碼表」改指實體頁;新增 text_entity i18n(zh-TW/en) - 異體字落地替換(AGENTS.md §1.3)掛在聚合根三個落點:書名(早於拼音 派生)、主列其餘文本欄、TEXT_INSTANCE_DATA 版本列整列(c_publisher 是不帶 _chn 的中文欄);模式與範圍一律由 VariantReplaceScope 決定, 結果經 __variant_replaced 帶進回應 notices,累積器每次寫入前重置。 VariantReplaceHookCoverageTest 清冊同步(掛鉤由批次控制器移入聚合根) - step 4(封閉下層直寫)整體暫緩:codes UI 待 parity(作者列表面板、 instance 專屬欄位);text-codes 裸表 create 因異體字 S5 剛接上落地 替換而維持在役,與聚合並存(比照 OFFICE_CODES 拼音欄與 office 聚合) - 版本鍵 0 值:ctype_digit() 會放行 "0",但生產庫有一列歷史資料 (40354, 0, 0),無條件拒絕會讓該文獻無法編輯 ⇒ create 一律擋、 update 只擋資料庫裡不存在的 0 值鍵(codex review) - TextForm 版本列改用前端穩定 uid 當 key(AGENTS §4;不用複合鍵本身, 那兩欄使用者可編輯,改動會整列重新掛載而丟焦點)(codex review) - 同步 API.md §4/§13、ENTITY_AGGREGATE_ARCHITECTURE.md、CHANGELOG - 已執行 ./vendor/bin/phpunit 全套(2977 tests,既有 SecurityAuditLogTest 環境性失敗除外全綠)、npm run build、npm test、php-cs-fixer
|
衝突已解,已 rebase 到 因為這期間上游把異體字計畫從 S0 推到 S9,衝突不只是文字層面,有兩處語義相撞,其中一個我改了原本的做法,請 review 時特別看一下: 1. 我這個 PR 原先把 解衝突時我回退了自己的下架,取上游版本,理由:
連帶把 2. 聚合根補上異體字落地替換掛鉤(S8 的機械化把關) S8 把「新增文本寫入路徑必須掛替換」訂成常規並加了
模式與範圍一律交給 順帶修掉 codex 的兩則 P2(詳見各自的回覆串)。 驗證:全套 |