# 文章模組整套純重構（前台讀取 + 後台 CRUD 全搬新框架 Datatable）

## Context（為什麼做）
admin71 文章 create/update/destroy 目前走**舊路徑** `IndexsAPI → Article::create → createBind`（`Article.php`/`Dal\Article` 舊 Dal），前台讀取走 `DataSet('articles')` 舊 model（帶 `where compID`）。新框架（Datatable）已有讀取端（`Articles\Lists`/`Categories\Main`，無 compID）與 create 基礎（`Base\Datatable\Main::append`，自產 snowflake），但**文章 create 完全沒接 append**。

目標：把文章 6 模組（articles/products/bulletins/services/videos/tags，共用 `Articles\*` module-aware）的前後台全面搬到新框架，甩開 `IndexsAPI`/`Article.php`/`DataSet`，並完成 datamodel 終態（metadata 歸位主檔、去 compID）。先在 **jyg** 站驗收（我有後台 Playwright 自測能力），再逐站推。

決策（使用者拍板）：**一次做到底，含階段 5**（metadata 搬 `articles_bind` + DROP compID）。

## 關鍵事實（研究確認）
- `Articles\Main`(services@articles.app, gate('main')) **不覆寫** append/to_update，其 db()=內容表 `articles`。直接呼叫會寫錯表、不寫 bind/match → **create 必須從 Bind 層覆寫**。
- `articles_bind`=主檔（PK postID、snowflake、UNIQUE code、**已無 compID 欄**）。`articles`=內容表（PK (postID,lngID)、MyISAM、ON DUPLICATE 可用），目前混存「每語系欄(title/summary/cover/description/spec/content/sourceURL/author/place)」+「metadata 每篇固定卻每語複製(catID/enable/sortID/year/month/date/selected/views/compID/insTime/remark/creator/form_id)」。
- i18n 子框架（`App\Models\Datatable\I18n`）**全 codebase 從未啟用**、schema 寫死 `_i18n`/`post_id`/`lang_id`/強制 prepend `i18n_id` → 與 (postID,lngID) 複合鍵衝突。**不走它**。
- 新框架讀取端 `Articles\DB\Lists` 已 join bind+articles+match、`Articles\Lists::to_present` merge bind 欄 + categories、**無 compID**；前台只缺「模板接線」。
- DB 變更沿用 `docs/ai-agents/project-init/article-datamodel-migrate.sql` 的 per-station 分段慣例。段 A/B（postID/catID→bigint、bind 去 auto_increment）jyg 已套（snowflake create 實測通過）。

## 抉擇（採用）
- **A. i18n 寫入**：覆寫 `Articles\Bind\Main` 自己寫 `articles` 多語列（繞過未驗證的 i18n 子框架）。
- **B. metadata**：階段 1-4 維持 `articles` 複製（append 照寫）；**階段 5** 才搬進 `articles_bind`。
- **C. 順序**：前台讀取先 → 後台 create/update/destroy → 結構搬移最後。
- **D. match 串接**：集中在 `Articles\Bind\Main`（create/update 內呼叫 `module('categories.match.app')`），module-aware 一改全模組生效。
- **match 寫葉子**（對齊 migrate 段 C 方向＋新列表 boss_id 用子樹），不寫祖先列。

---

## 階段 1 — 前台讀取改用 `gate('lists')` / `gate('main')`（零 DB、逐頁灰度）
**改檔**：`stations/jygesg/apps/resources/assets/views/web/langpack/i18n/{module}/`（**只改 Twig 模板**），每模組約 3 支（index / list.selected / show），代表：
- `articles/article.index.html`：`show_children()`→`categories.app gate('main')->toForest('ESG')`；`show_item_list([],null,6,page)`→`articles.app gate('lists')->list(['boss_id'=>'ESG','is_enable'=>1,'page'=>page],6)`；`show()`→`categories.app gate('main')->info('ESG')`。
- `article.list.selected.html`：`list(['cat_id'|'boss_id'=>selected,'is_enable'=>1,'page'=>page],N)` + `breadcrumbs()`。
- `article.show.html`：`articles.app gate('main')->present(code,['lang_id'=>...])` + 上下篇 `around()`。
- 同模式套 products/bulletins/services/videos/tags 各模板。
**必檢**：① `gate('lists')` 預設**不加 is_enable** → 前台必帶 `'is_enable'=>1`（或用 `preview()`），否則露未上架。② Twig 變數名對映（舊 `item.catID[0].code`→新 `item.categories[].code`；cover 還原；分頁物件 `{list,paging}`）。逐頁 `print_r` 比對。
**DB**：無。
**驗證**：逐頁開前台 URL `browser_snapshot` 比對筆數/標題/封面/麵包屑/分頁；後台下架一篇→前台列表應消失；切語系兩邊都正確。
**上線**：逐頁/逐模組灰度，先 articles 驗收再其餘；壞了只影響該頁、立即還原。

## 階段 2 — 最小「create 走 append」（核心交付，API 直測，不接 controller/前台/schema）
**改檔**：`PHP71/pubs/apps/Models/Articles/Bind/Main.php`（目前空殼）覆寫 `append($_inputs)`：
1. `$post_id = $_inputs['postID'] ?? $this->id()`（snowflake）；抽出 `$i18n`、`$cat_id`。
2. 寫主檔：`$this->db()->insertOrUpdate($post_id, $bind_inputs)` → `articles_bind`（code 唯一檢查 `is_unique`、catID JSON、enable/sortID 等 bind 欄）。
3. 多語：`foreach($i18n as $lng=>$row)` 呼叫**內容表 DB**（`neko('services@articles.app')->db('Main')->insertOrUpdate($post_id, array_merge($row,['postID'=>$post_id,'lngID'=>$lng], $metadata))`，metadata 照抄每語列＝抉擇 B）；空語系不寫。
4. match：`$this->module('categories.match.app')->gate('main')->update/append`，寫**葉子** catID（多分類迴圈）。
**DB**：無（前提段 A/B 已套；上線前確認 jyg 文章段 B 已套）。
**驗證**（直接證明「create 走 append」）：Playwright 後台登入態打 `module('bind')->gate('main')->append([...])`（暫掛 dev probe 或既有端點）→ 回 snowflake postID → 用階段1 的 `gate('lists')->list(['post_id'=>id])`＋前台確認三表（bind/articles/match）都寫對；帶兩語系驗多語。
**上線**：純新增方法、零現行路徑影響，可獨立上（內部能力，無 UI 變化）。

## 階段 3 — create controller 改打 append
**改檔**：`admin71/apps/Httpd/Controllers/Articles/API/ArticleController.php@create`(line13)：`IndexsAPI::createArticle` → `neko('services@{target}s.app')->module('bind')->gate('main')->append($inputs)`；把表單 payload（basic/i18n/catID/status/online_period）轉接成 append inputs。**upload/hashtag/seo** 先沿用舊補丁或 append 後段呼叫既有上傳模型，逐步收斂。
**DB**：無。
**驗證**：後台新增文章（標題/分類/內容/封面）→ 攔 network 看回 200+postID → 列表/前台確認；邊界：只填 basic 不填 i18n 仍寫 match；重複 code 回「編碼重複」。
**上線**：可與階段2同批；保留舊 createArticle 為 fallback 旗標，出錯只影響新增。

## 階段 4 — update / destroy 走框架
**改檔**：`Articles/Bind/Main.php` 覆寫 `update`（多語 upsert + match 重綁 + metadata 同步）、`remove`/`to_remove`（刪 bind + articles 全語列 + match）；`ArticleController@update`(line17)/`@destroy`(line22) 改打新框架。online/sticky/status/seo 端點評估一併或延後。
**DB**：無。
**驗證**：編輯改標題/分類/下架→前後台反映；刪除→三表查無＋前台消失；切分類→match 重綁（原分類消失、新出現）。

## 階段 5 — metadata 搬 `articles_bind` + 段 D 去 compID（結構終態，最後做）
**前提**：階段 1（前台舊讀取退役）＋階段 3/4（舊 Dal create 退役）完成。
**migration**（新增於 `article-datamodel-migrate.sql` 風格，per-station，6 模組）：
- `articles_bind` 補欄（catID/enable/sortID/year/month/date/views…）→ `UPDATE ... JOIN articles` 灌資料 → `articles` DROP 這些 metadata 欄。
- 段 D：DROP compID（內容表 articles / articles_list / category 表，6 模組）。
**改程式**：`Articles\DB\Lists`(join 改：metadata 取自 bind)、`Articles\DB\Main::info`、`Articles\Bind\Main`(寫入不再複製 metadata)、`Articles\Lists::to_present`(merge)。前台輸出鍵不變、理論上不動（回歸驗證）。
**驗證**：全模組前台 + 後台 CRUD 全回歸。
**風險**：高（schema+資料+讀寫雙改+per-station）。jyg 驗收後才逐站推。

---

## 跨階段
- **6 模組共用**：`Articles\*` 改動 module-aware 一次生效；**前台模板（階段1）各模組各自有、per-station**，jyg 驗收後才複製他站。
- **PHP7 vs PHP71**：本次走 PHP71；上線前確認 jyg runtime 指向哪棵樹，勿改錯。
- **回退**：每階段以「新增方法／換 controller 目標／換模板」為主，舊路徑保留到驗收後才刪，任一階段可快速 revert。
- **部署**：jyg auto-deploy（push main→自動 pull）；每階段 push 後用 Playwright 自測。

## 最關鍵檔案
- `PHP71/pubs/apps/Models/Articles/Bind/Main.php`（階段2/4 核心：append/update/remove）
- `PHP71/pubs/apps/Models/Articles/DB/Main.php`（內容表 (postID,lngID) ODKU）
- `admin71/apps/Httpd/Controllers/Articles/API/ArticleController.php`（階段3/4 改線）
- `stations/jygesg/apps/resources/assets/views/web/langpack/i18n/articles/*.html`（階段1 代表，其餘 5 模組×3 同模式）
- `docs/ai-agents/project-init/article-datamodel-migrate.sql`（階段5 段D/結構遷移 per-station 範本）

## 驗證總則
全程用 jyg 後台 Playwright（nextop/micro168）自登自測：API 回傳看 snowflake/status、前台頁 snapshot 比對、CRUD 後三表交叉確認；測試資料一律 sf-test 前綴、用 destroy API 清除。
