ADR-008: Word 文件產生模組(隔離式、handle-based)
ADR 編號: 008 狀態: 已接受 (Accepted) 決策日期: 2026-06-23 決策者: Framework Team + AI Assistant
摘要
於 appfuse-server 新增 io.leandev.appfuse.document 模組,提供以 .docx 範本為起點的 Word 文件產生能力(封裝 Apache POI XWPF,不洩漏 POI/OOXML 型別);採 handle/locator 模型(捨棄前一代的有狀態游標)、單一 Body 共用容器面,並支援把 appfuse-web RichTextEditor 的 HTML 子集渲染進 Word。
背景 (Context)
問題陳述
多個既有系統(前一代框架的消費端 csp-server、asp-server)以「載入 .docx 範本 → 在其上填值與編排 → 輸出」的方式產生證書、檢驗報告、通知書等公文。前一代框架的 appfuse-core 有一套 io.leandev.appfuse.document(XWPF*Editor),但全面把 POI 與原始 OOXML schema 型別(XWPFParagraph、XWPFTable、CTSimpleField…)暴露在公開 API,違反現行框架「依賴隔離層」原則(見 csv / workbook 模組與 30-public-api.md)。
限制條件
- 公開 API 不得洩漏 POI(
org.apache.poi.*)或 OOXML schema(org.openxmlformats.*)型別。 - 尺寸需與既有第一方型別
io.leandev.appfuse.measure(Length/Paper)一致。 - 富文本輸入格式為 appfuse-web
RichTextEditor(Tiptap/ProseMirror)輸出的 HTML 字串,非前一代的rtxAST。
假設前提
- 前一代
document是探索性設計,不視為最佳解;逐一重新評估其風格選擇。 - 真實消費端(
csp-server9 個 service、asp-server5 個 service)作為實務需求證據,不作為移植目標。
考量的方案 (Options Considered)
方案 A: 直接移植前一代 XWPF*Editor
說明: 把 appfuse-core 的 document 套件原樣搬入。
優點:
- ✅ 功能即時齊備、零設計成本
缺點:
- ❌ POI + OOXML schema(
CTSimpleField等)全洩漏進公開 API,違反隔離不變量 - ❌ 沿用有狀態游標(
seek/moveTo+ 隱藏 cursor)與三套重複的 Editor
評分: 2/5
方案 B: 隔離式重設計、handle-based、單一 Body 共用面
說明: 以 csp/asp 的真實使用為能力清單,重新設計一組隔離 wrapper(Document/Body/Paragraph/Run/Table/Row/Cell),POI 藏在套件內部;定位改用 locator(findParagraphByText/Bookmark → Optional),body/header/footer/cell 共用單一 Body 容器面。
優點:
- ✅ 公開 API 零 POI/OOXML 洩漏,與
csv/workbook一致 - ✅ 無隱藏游標狀態,定位即取得 handle、可組合、易推理
- ✅ 收斂前一代三套重複 Editor 為一個
Body
缺點:
- ❌ 設計與實作成本較高(跨 run 取代、列/表複製、HTML 渲染需自行實作)
評分: 5/5
方案 C: 不做,續用前一代 jar
說明: 維持消費端各自依賴舊 appfuse-core。
優點:
- ✅ 零工作量
缺點:
- ❌ 新專案無法用一致的隔離 API 產生 Word;技術債延續
評分: 1/5
決策 (Decision)
選擇方案: B
核心理由:
- 隔離不變量不可破——公開 API 洩漏 POI/OOXML 是現行框架明令禁止(
30-public-api.md),方案 A 直接違反。 - 前一代是探索、非最佳解——游標模型與 fluent
of()多是「POI handle 薄封裝」的副產物;隔離後既無相容義務、又無 POI handle 可包,正好整治。 - 真實證據支撐——csp/asp 的
seek→就地編輯idiom 每一處都能 1:1 對映成find→編輯(locator),無功能缺口(見下節)。
權衡分析 (Trade-offs)
我們獲得什麼 (Gains)
- ✅ 與
csv/workbook同調的隔離 API;底層 POI 可升級/替換不影響應用層 - ✅ handle 模型消除隱藏狀態;單一
Body面消除三套重複 - ✅ 順手把前一代的笨拙改掉(如
copyTo兩步式 →appendCopyOf一步回傳副本)
我們放棄什麼 (Losses)
- ❌ 不與前一代 API 相容(但消費端不移植,無實際損失)
風險與緩解措施 (Risks & Mitigations)
| 風險 | 嚴重性 | 機率 | 緩解措施 |
|---|---|---|---|
| 跨 run 文字取代、列/表深拷貝實作易出錯 | 中 | 中 | 複用前一代驗證過的 POI 食譜(內部實作,不外露);單元測試覆蓋 |
| HTML 子集渲染範圍蔓延 | 中 | 中 | v1 鎖定 marks + 區塊 + 清單 + 對齊 + 圖片;HTML 內嵌 <table> 列為 follow-up |
| jsoup 新依賴 | 低 | 低 | 以 implementation 引入、不外露;僅 HTML 渲染路徑使用 |
影響 (Consequences)
正面影響
- ➕ 新專案可用隔離 API 產生 Word 公文;與 Excel(
workbook)能力對稱 - ➕ 與
workbook的 HTML 富文本需求(FU-14)共用 HTML 子集解析
負面影響
- ➖ 模組較大(手寫範本套印的多種編排),分多個 commit 落地
中性影響
- 🔸 富文本輸入正名為 HTML 子集(非
rtxAST);rtx子系統不在本模組落點
實作指南 (Implementation Guidelines)
必須遵守的規則
- 公開簽章僅可出現自家型別、
io.leandev.appfuse.measure.*、java.awt.Color、JDK 標準型別;禁止org.apache.poi.*/org.openxmlformats.*。 - 定位一律 locator(回
Optional<handle>),不得引入有狀態游標。 - body/header/footer/cell 共用單一
Body容器面(TableCell extends Body)。 - 尺寸用
Length/Paper、顏色用java.awt.Color、字級用double points。 - 不內建 token 語法(範本作者自選
[x]/{{x}}),提供findParagraphByText/replaceText原語。
v1 範圍邊界
| 收 | 緩(follow-up) |
|---|---|
範本載入/輸出、段落/run/表格/列編排、頁面章節、頁首頁尾內容、圖片、頁碼欄位、appendCopyOf、HTML 富文本(文字流 + 圖片) | 從零自由編排體驗、HTML 內嵌 <table> 渲染、list numbering 定義、文件合併、PDF 匯出、樣式定義建立 |
前一代 vs v1 對照(取捨依 csp/asp 證據)
| 議題 | 前一代 | v1 | 證據 |
|---|---|---|---|
| 定位 | 有狀態游標 seek/moveTo | locator findParagraphByText/Bookmark | csp/asp 皆「seek→就地編輯」,1:1 對映 find→編輯 |
| 容器 | 三套重複 Editor | 單一 Body | body/header/footer/cell 操作相同(POI IBody) |
| 複製 | addTable()+copyTo(empty) 兩步 | appendCopyOf() 一步回傳副本 | asp 樣板表格複製 N 份的笨拙改善 |
| 顏色 | String hex | java.awt.Color | 與 workbook 一致 |
| 頁碼欄位 | 回 CTSimpleField(OOXML 洩漏) | addPageNumber() 隱藏 | csp 用但不碰回傳型別 |
相關文檔 (References)
內部文檔
- 使用指南:
../guides/core/document.md - 隔離策略對照模組:
../guides/core/workbook.md、../guides/core/csv.md
相關 ADR
- 本模組與既有
csv/workbook同採「依賴隔離層」哲學
變更歷史 (Change Log)
| 日期 | 變更內容 | 變更者 |
|---|---|---|
| 2026-06-23 | 初版(v1 設計定稿) | Framework Team + AI |
文檔維護者: Development Team + AI Assistant 最後審閱: 2026-06-23