固定版面表單 PDF
狀態:實驗性 API;排版警示政策由不可變版本
appfuse-server 4.0.0-alpha.34提供,前端警示交付由@appfuse/appfuse-web 19.0.0-alpha.134提供。2026-09-30 使用者確認將固定表單及其需求規格納入框架;下游提供的樣張只作能力核對,不在框架實作其業務報表。
與流式報表的邊界
ADR-029 的模型分離仍有效。
io.leandev.appfuse.pdf.FixedDocument 明確建立每張頁面及元素座標;FixedPdfRenderer 輸出 PDF。
它不使用 ReportDocument、ReportArtifact 或 flow nodes 作公開固定表單模型;可交付 caller OutputStream 或中立的 PdfArtifact。
共用 ReportFonts 字型登錄、PDFBox runtime、內部字型量測與影像嵌入,避免維護兩套中文字型行為。
內部 renderer 暫置於既有 report.internal.pdf 以共用 package-private 基礎設施,該路徑不是消費端 API。
需求以 RPT 報表規格 描述,layout_mode: fixed。
規格可獨立實作與驗證;只有產品操作、資料授權與交付流程才由 US 承接。
公開契約
| API | 契約 |
|---|---|
FixedDocument(title, pages) | 不可變文件,至少一頁;資料綁定與續頁已由應用範本完成 |
Page(widthMm, heightMm, elements) | 每頁可有不同尺寸;元素清單順序即疊放順序,同頁 ID 唯一 |
Frame(x, y, width, height) | mm,左上原點、x 向右、y 向下;有限數,寬高大於零 |
Text | 字型家族、pt 字級與基線行距、粗體、RGB、水平對齊、單行或框內換行、pt 字距(letterSpacingPt,預設 0) |
Rectangle/Ellipse/Line | 框、橢圓、任意方向線;填色、線寬;Line 可自訂 pt 虛線(dashPatternPt/dashPhasePt),舊 dashed=true 為 3pt/3pt |
Image | PNG/JPEG 等比置中,防禦複製資料;20MB/25M pixels 上限 |
PdfBackground | 指定 1-based PDF 頁面作 STATIC 底稿;保留頁面外觀,不匯入註解、AcroForm、動作 |
Barcode | Code 39、Code 128、QR 黑白向量;實體模組寬、條高、靜區;不自動縮放 |
FixedTextMeasurer | 以同一 ReportFonts 與 renderer 規則量測單行寬度、ascent/descent 及實際使用字型;供應用自行定位 |
Options | 全表單/只印 VARIABLE 層、mm 平移校準、Clock、取消回呼、完成頁數回呼 onPage、layoutPolicy(預設 OUTPUT_WITH_WARNINGS) |
Result | 完成頁數、實際寫出 bytes、diagnostics |
FixedLayoutException | 可定位的 code/location;嚴格模式的幾何異常及所有模式的非排版錯誤不產生可交付 PDF |
STATIC 層用於底圖、印刷框線與標籤;VARIABLE 層用於資料。overlayOnly=true 完全略過 STATIC 元素。
嚴格模式要求框線筆畫及校準後的幾何落在紙張內;座標允許正負 5000 mm,尺寸最大 5000 mm,校準限正負 100 mm。
空資料是否仍出單、留幾列及如何呈現由 RPT 定義;renderer 不代替業務判斷。
文字框以字型 ascent 對齊頂部、以 descent 核對底部,行距是基線間距。
可選嚴格模式的 SINGLE_LINE 遇換行或超寬拒絕;WRAP 按 Unicode code point 換行,超高、單字元超寬或行距造成字型高度重疊時拒絕。
不自動截斷、縮字、改框高或移動後續欄位;缺字及未知字型也拒絕。換行目前不提供英文保詞、禁則排版、雙向文字、複雜 shaping 或拉丁字距微調(kerning)。
虛線、字距與文字量測
Line 的虛線以 pt 指定交替的線段與空白長度,整條線以單一路徑繪製,不需拆成多段:
import io.leandev.appfuse.pdf.FixedDocument.Layer;
import io.leandev.appfuse.pdf.FixedDocument.Line;
import java.util.List;
var dotted = new Line("cut", Layer.STATIC, 15, 150, 195, 150, 0.4, 0x000000, false)
.withDash(List.of(1.0, 2.0), 0); // 1pt 線段、2pt 空白
空清單為實線;奇數個元素由 PDF 重複形成週期,phase 可大於週期。長度須為有限非負值、不可全為零,實線的 phase 必須為 0。
保留的 dashed 建構子等同 Line.DEFAULT_DASH(3pt/3pt),既有文件的繪圖指令不變。
Text.letterSpacingPt 是同一行相鄰 code point 之間額外加的距離,行尾不加;置中、靠右、換行與超框檢查都使用含字距的寬度,換行後新行首字仍貼齊框緣。
字距 0 時不寫出 PDF 字元間距指令。中文標籤的「姓 名」式拉開可直接以字距表達,不需逐字建立元素:
var label = new Text("name-label", Layer.STATIC, frame, "姓名", "sans", 11, 17, false, 0,
Align.LEFT, TextMode.SINGLE_LINE).withLetterSpacing(11);
應用需要自行計算座標時使用 FixedTextMeasurer,不要直接以 PDFBox 量測——後者不會套用框架的 fallback 鏈、字距與缺字拒絕規則:
import io.leandev.appfuse.pdf.FixedTextMeasurer;
try (var measurer = FixedTextMeasurer.create(fonts)) {
var m = measurer.measure("合計 NT$", "sans", 11, false, 0.5);
double widthMm = m.widthPt() * 25.4 / 72; // 與 renderer 對齊、超框檢查使用的寬度相同
}
量測結果含行寬、所用字型的最大 ascent/descent,以及依實際字型切分的片段(Run.font() 為 fallback 鏈中實際使用的 FontSource)。
只量單行:Tab 與 Text 相同展開為四個空格,換行或其他控制字元拒絕。未知家族、缺粗體字面及任何字型都不含的字元,與 renderer 一樣以 pdf.fixed.font 拒絕。
實例持有已解析字型,非 thread-safe;同一次產生內重用同一實例,用畢 close()。
不提供 kerning:PDFBox 不對內嵌 TrueType 套用 GPOS 字距調整,量測與繪製都以各字 advance 加總,兩者保持一致。
底稿限 20MB,須未加密、無旋轉、CropBox 原點為 0、UserUnit 為 1,尺寸與目標頁相同(0.01pt 比較容差);不偷偷縮放。 嚴格模式的全頁底稿配非零校準會越頁而拒絕;警示模式則照校準平移並回報可能裁切;預印紙套印模式略過底稿後可平移可變欄位。 只有底稿頁面內容會匯入;若原表單顯示內容來自註解/AcroForm appearance,須先用正式模板製作流程整理成純頁面內容。
排版異常仍輸出(預設)
此政策只改變固定版面的幾何異常處理;流式報表維持原契約。舊建構子與 Options.defaults() 都使用 OUTPUT_WITH_WARNINGS。幾何異常不再使預設生成失敗;需要拒絕的建立者須明確選用 STRICT。
import io.leandev.appfuse.pdf.FixedPdfRenderer;
var options = FixedPdfRenderer.Options.defaults()
.layoutPolicy(FixedPdfRenderer.LayoutPolicy.OUTPUT_WITH_WARNINGS); // 可省略:這是預設政策。
try (var artifact = renderer.render(document, options, "form.pdf")) {
var warnings = artifact.layoutDiagnostics();
artifact.transferTo(out);
}
OUTPUT_WITH_WARNINGS 保留字型、字級、基線行距、對齊、座標、內容及元素繪製順序,允許重疊;不縮字、不刪資料、不增頁或移動後續欄位。
WRAP 仍按既定框寬換行,全部行都繪製;SINGLE_LINE 遇原始換行時逐行繪製,保留原基線間距。
框內有效高度及單字元寬度超限以 pdf.fixed.text-overflow 回報。
框、線、圖片、條碼、底稿的筆畫及校準越頁以 pdf.fixed.out-of-page 回報;文字實際繪製範圍即使框在頁內,超出紙張也另回報此代碼。
完整內容會寫入 PDF,但紙張之外的部分可能無法顯示或列印;warning 明確說明可能裁切,不將文字存在於 PDF 當成全部可見的保證。
建立者將排版判斷加入 FixedDocument(title, pages, layoutIssues),例如:
import io.leandev.appfuse.pdf.FixedDocument;
var issue = new FixedDocument.LayoutIssue("app.form.block-overlap", 2,
"customer-name", "details", "Customer name overlaps the details block");
var document = new FixedDocument("Form", pages, java.util.List.of(issue));
此接點只供幾何排版問題;頁碼須存在,指定的元素 ID 須位於該頁,也可只提供區域識別。建立者自行判定業務區塊重疊,renderer 不將底圖與文字等合法疊放自動視為異常。
Options.defaults().layoutPolicy(LayoutPolicy.STRICT) 的嚴格政策拒絕建立者宣告的問題,警示政策將其保留為 WARNING。應用自訂穩定 code,不把權限、資料缺漏或版本衝突塞入此清單;這些檢查仍在建立文件之前依原應用政策拒絕。
| 診斷欄位 | 契約 |
|---|---|
severity | 成功產物為 WARNING;PdfDiagnostic.Severity 另提供 ERROR 表達拒絕診斷 |
code | 內建 pdf.fixed.text-overflow/pdf.fixed.out-of-page;建立者自訂應用命名空間 |
pageNumber | 原始 PDF 的 1-based 實體頁碼,不因選頁列印重新編號 |
elementId/regionId | 至少一項;內建診斷提供元素 ID,應用可提供區域 |
message | 英文可顯示訊息,不含原始欄位資料 |
串流輸出的 Result.diagnostics()、PdfArtifact.layoutDiagnostics() 與保存後的 StoredPdf.diagnostics() 都提供不可變清單。
PdfArtifact 的中性接點刻意名為 layoutDiagnostics(),以保留既有流式 ReportArtifact.diagnostics() 的型別相容性。
StoredPdfs.save 保留診斷;應用將保存結果的 diagnostics 或串流結果映射至前端 ReportArtifact.diagnostics,PDF bytes 與診斷分開交付。
兩個參考 server 的固定 job 同樣預設警示政策;ReportJobWork.Fixed 可明確指定 STRICT。
保存時將完整清單與 READY 原子寫入 ReportJob.layoutDiagnostics,GET /api/v1/reports/{id} 及列表的 ReportJobResponse.diagnostics 會回傳 warning;PDF 的 content/print 端點仍可使用。
狀態回應的 severity 依應用 enum wire 規則為小寫 warning;ReportViewer 同時接受中立 Java serializer 的 WARNING。
舊 DB 列的 null 視為空診斷;已有 report_job 表的環境需在部署新來源前加入可空的 layout_diagnostics 大型文字欄,沿既有人工 Entity Diff/migration 流程執行,這次不改已接受 snapshot 或執行環境 migration。
非 READY 的狀態不投影成功產物診斷;既有 owner/tenant 限制同時保護 PDF 與 warnings。
只有明確的幾何檢查使用此政策,沒有攔截整個 renderer 的例外。缺字、未知字型、無效底稿或圖片、文字/資產容量超限、無法序列化或寫出有效 PDF 仍失敗;取消也仍取消。 完整 PDF 生成成功後才交付;遇非排版錯誤不交付前面已完成的頁面。
前端採用方式見 appfuse-web/lib/components/data-display/report-viewer/README.md:保持 ready,持續顯示「已產生,但有排版問題」及原始位置;預覽、下載、列印仍可使用。
共用列印 adapter 在直接列印前顯示問題,使用者選擇列印才繼續;有 warnings 時 viewer 的選頁對話框不自動關閉。自訂 adapter 也須在直接列印入口提供可見警示,onPrintComplete 不應自動卸載唯一的警示介面。
FixedLayoutPolicyTest 以實際 PDF 逐字與座標驗證嚴格拒絕/警示完整輸出、重疊、超框與越頁位置,並核對缺字、無效資產、容量與 I/O 仍失敗;前端單元及 Storybook browser 測試另核對交付操作與警示可見性。
相依與輸出
應用加入 org.apache.pdfbox:pdfbox 3.0.8 以上、4.0 以前;只有使用條碼才需另加入 com.google.zxing:core:3.5.3。
兩者皆為框架 compileOnly,未用固定 PDF 的應用不必引入;缺依賴時給出明確指引。
ZXing 3.5.3 是本次驗證的條碼編碼器版本。
renderer 可重用,每次產生各自的 PDF 文件、字型與暫存檔。 完整 PDF 先序列化至暫存,再寫入 caller OutputStream;模型/排版失敗或產生階段取消不寫出任何 bytes。 輸出串流由 caller 關閉;寫出階段 I/O 失敗可能已送出部分 bytes,caller 必須捨棄部分輸出。 預設紙張輸出沒有自動安全邊界:不可列印區由 RPT 的元件配置約束,實紙另驗證。
條碼責任
Code 39 使用基本字元集,不接受手動 start/stop 星號;Code 128 限 printable ASCII。
QR 使用 UTF-8、M 錯誤更正;byte mode 可能包含 UTF-8 ECI 標頭,正式掃描器必須以實際設備驗證,不保證代收相容。QR 高度由模組數決定,heightMm 只控制一維碼條高。
一維碼左右靜區至少 10 modules,QR 四邊至少 4;編碼後若超紙張,嚴格模式拒絕,警示模式完整繪製並回報可能裁切。
payload 不在例外中回顯。可讀文字以獨立 Text 放置,應用須保證與 payload 對應。
銀行、郵局、超商等帳號組合、補零、業務檢查碼及代收規範由下游 RPT/adapter 定義; 通用條碼可以解碼,不等於正式繳款單已經代收機構認證。
合成驗證規格與證據
測試來源為 appfuse-server/src/test/java/io/leandev/appfuse/report/FixedPdfTest.java,不帶入下游樣張的個資。
樣張使用 A4 210×297mm、sans(Noto Sans/Noto Sans TC fallback)、11pt、17pt 行距、固定產生時間 2026-09-30T04:00:00Z。
| 情境 | 明確預期 | 證據 |
|---|---|---|
| 訂購單 7 筆 | 每頁 5 個固定槽,兩頁;7 個品項各出現一次;第 2 頁整份總計 700.00;空槽不產生假資料 | orderWithExplicitContinuation、order-fixed.pdf |
| 簽收單 | 指定簽名區留白,標題/框線為 STATIC、單號/收件資料為 VARIABLE | receiptFullAndCalibratedOverlay、receipt-fixed.pdf |
| 同簽收單套印 | 只有 VARIABLE;x +1mm、y +2mm;紙張仍為 A4 | receipt-overlay.pdf;獨立座標測試驗證 dx/dy |
| 三種條碼 | DEMO payload 在 PDF raster 300dpi 解碼後逐字相同 | barcodeRasterRoundTripAndPaymentLikeSample、barcode-fixed.pdf |
| 長中文 | 框內完整換行;後續 END 欄位不移動 | PDF 文字與位置斷言 |
| 超容量 | 明確選用 STRICT 時,文字超框或線寬/校準越頁明確失敗;即使前頁成功也不交付半份 PDF | atomic failure 測試 |
| 底稿/圖形 | PDF 底稿與新文字可共存;套印省略底稿;尺寸/頁碼錯誤拒絕;橢圓外觀 | 底稿文字及 raster 斷言 |
| 圖片 | bytes 不可被外部改寫,等比置中、不拉伸;損壞圖片拒絕 | raster 幾何斷言 |
| 執行期 | 取消、thread interrupt、輸出失敗、caller 串流所有權、並行重用 | lifecycle 測試 |
| 選用相依 | 缺 PDFBox 時模型可建、factory 有指引;缺 ZXing 時一般 PDF 可輸出、Barcode 有指引且零 bytes | 隔離 classpath 測試 |
明細的 5 筆容量與續頁是合成範本規則,不是 renderer 內建業務上限;各下游須在 RPT 定義自身 N/N+1、總計與聯別。
重跑:在 appfuse-server/ 執行 ./gradlew test --tests 'io.leandev.appfuse.report.FixedPdfTest' --console=plain。
產物在 build/reports/fixed-forms/;供檢視副本與摘要在 workspace output/pdf/fixed-forms/。
固定訂購單樣張的資料與定位契約
此表把合成測試的規格寫成可核對的定位資料,示範下游 RPT 應有的精度;不是正式訂購單的業務需求。 文件 ID 固定 DEMO-ORDER-001,客戶與地址為合成固定文字;明細序號 1–7,名稱依序「合成品項-01」至「合成品項-07」, 每筆數量 1、保存金額 100.00 TWD,整份總計 700.00。模板不得因續頁重複加總。 本樣張不讀 DB;真實資料的查詢、快照、稅額與授權不在此合成案例驗證範圍。
共同樣式為 11pt/17pt 行距、左對齊、框內換行,金額另採單行靠右;此合成範本測試明確選用 STRICT,所有超框皆拒絕,不移動其他欄位。 幾何單位 mm、定位容差 0.01pt(PDF 數值量測);這不是實紙套印容差。
| 元件/欄位 | 圖層 | x | y | w | h | 資料/規則 |
|---|---|---|---|---|---|---|
| 標題 | STATIC | 15 | 15 | 180 | 12 | 訂購單 — 框架驗證樣張 |
| 單號及頁次 | VARIABLE | 15 | 30 | 180 | 10 | 單號、實體頁次/2 |
| 客戶及地址 | VARIABLE | 15 | 44 | 180 | 16 | 兩行固定合成文字 |
| 表格外框 | STATIC | 15 | 62 | 180 | 90 | 0.6pt 黑線;直線 x=35、115、145 |
| 表頭四欄 | STATIC | 18/40/120/158 | 65 | 14/65/20/32 | 8 | 項次、商品名稱、數量、金額 |
| 每頁序號 | VARIABLE | 18 | 79 + 15r | 14 | 12 | r=0..4;續頁接續第 6 筆 |
| 每頁品名 | VARIABLE | 40 | 79 + 15r | 65 | 12 | 綁定當頁第 r 筆名稱 |
| 每頁數量 | VARIABLE | 120 | 79 + 15r | 20 | 12 | 綁定數量 |
| 每頁金額 | VARIABLE | 150 | 79 + 15r | 40 | 12 | 兩位小數,靠右,單行 |
| 總計區 | VARIABLE | 120 | 160 | 75 | 12 | 第 1 頁「尚有續頁」,第 2 頁整份總計 |
| 簽名區 | STATIC | 15 | 205 | 180 | 12 | 訂購人及審核留白 |
| 樣張提示 | STATIC | 15 | 265 | 180 | 10 | 非正式單據、100% 列印 |
表格橫線 y=77、92、107、122、137,0.4pt 黑線;頁 2 未使用的三個槽保留空白,不造出零金額品項。 只印 VARIABLE 模式會省略框線及提示,應與已印好的同版次表單配對;不得拿空白紙套印輸出當成完整單據。
下游能力核對與發布界線
| 參考版面 | 判定與仍需驗證 |
|---|---|
| 月結對帳單 | 適合既有 flow;下游補來源、分組與金額規格 |
| 收到檢體登記表 | 適合 flow 表格;下游定義計數、日期格式與空值 |
| 初檢基本資料紀錄表 | flow 多段文字與表格;需合成密集多行及跨頁組合驗證 |
| 固定繳款單 | 本 API 提供座標、底稿、圖層、橢圓、條碼;還需下游正式欄位座標、payload 規則與實紙掃讀/套印驗收 |
工作區合成驗證不計為 REP-09 的兩個實際 consuming reports;未發布前下游不能宣稱已可從 registry 使用此 API。 人工版面接受、實紙套印、設備掃讀、真實資料與產品授權均另列證據,不能沿用 RPT-001 流式樣張的接受結果。
目前不含 AcroForm 編輯、數位簽章、自動表格分頁、縮字/截斷、自動印表機校準、silent print、任意 HTML 模板或 JasperReports 引擎。 兩個 server 參考實作已將固定表單接入 ReportJob;下游仍須採用該參考實作並接上自己的業務資料與權限。固定產物不包裝成流式 ReportArtifact。
本次驗證結果
2026-09-30:固定表單 16 項、缺 ZXing 2 項、缺 PDFBox 5 項測試通過;既有流式 corpus/diagnostics 回歸及記憶體 gates 通過。
Javadoc、文檔站建置與 llms 索引檢查通過。四份樣張共五頁完成 AI 視覺檢視;人工版面、實紙套印與設備掃讀尚未驗收。
結果與來源/PDF 摘要保存在 workspace output/pdf/fixed-forms/verification.json。仍為未發布的工作區成果。
共用交付管線(2026-10-01)
FixedPdfRenderer.render(document, options, filename) 回傳 PdfArtifact,呼叫端負責關閉。
它提供檔名、頁數、大小、產生時間、layoutDiagnostics 及可重複開啟的串流;關閉產物後禁止開新串流,既有串流關閉後才刪除暫存檔。
流式 ReportArtifact 繼承同一中立介面,另外保留 flow metadata 與 diagnostics。
StoredPdfs.save(storage, partition, artifact, clock) 接管產物並在成功或失敗後關閉,回傳 StoredPdf。
兩個 server 的 ReportJobRequest.fixed(...) 接受 FixedReportJobTask,由 worker 在原有唯讀交易與身分脈絡中建立文件。
worker 統一管理 Clock、取消與頁數進度,再走相同保存、owner 授權下載、頁面選取、刪除及到期清理管線。
校準或套印可透過 ReportJobWork.Fixed 明確指定;舊 flow request 的五參數建構子保留。
FixedLayoutException 對外映射為不含 payload 的 report.invalid,取消映射為 report.cancelled。
這些 API 尚未發布;參考實作目前以 -PuseLocalFramework=true 驗證,正式採用須先發布框架並更新相依版本。
前節 2026-09-30 的視覺證據是當時快照,來源修改後不能當成目前版本的驗收證據;人工接受狀態不自動繼承。
審閱補充契約(2026-10-01)
中立 PdfArtifact/StoredPdf/StoredPdfs 遵循一般 API 相容政策;只有 Fixed* 為實驗性。 使用新版 worker 即須註冊 FixedPdfRenderer bean,純 flow 也相同;沿用既有 PDFBox/字型相依,不要求 ZXing,除非輸出條碼。 ZXing 支援範圍為 3.5.3 以上、3.6.0 以前;runtime 不作版本探測,下游須依此範圍鎖版,本次測試使用 3.5.3。
CRLF/CR 正規化為 LF,Tab 展開為四個空格(不提供 tab stops);其他控制字元及不合法條碼 payload 丟 FixedLayoutException, 對外映射 report.invalid。幾何/樣式程式設定錯誤仍為 IllegalArgumentException,不把所有程式錯誤當資料錯誤。 文字行高使用整個文字框各字型的最大 ascent 加最大 descent,並非逐字形可見輪廓。多行有效下限是該值(pt),隨字型與 fallback 改變; 不能保證 1.2 倍字級可用,本次 sans 合成樣張採 11pt/17pt,模板換字型須重驗。 WRAP 以 code point 分行,可能拆開 combining marks/ZWJ 序列;行尾空白計入量測而影響 CENTER/RIGHT 的視覺對齊。 本次接受此限制,應用輸入需符合 RPT 的文字範圍與空白規則;框架不擅自 trim 業務文字。 下載檔名替換 Unicode 控制/格式字元與路徑分隔字元,最多保留 180 code points,再補 .pdf。
覆蓋界線:job 取消測試驗 save 階段,invalid 文件實際經 renderer 驗證溢位及失敗分類,故障注入仍為 flow;FixedPdfTest 另驗 renderer 的取消與排版拒絕。
內部 TempStorage 測試接點可直接斷言序列化暫存檔於排版失敗、頁面完成後取消及產物關閉後刪除;不涵蓋 PDFBox scratch 或每一種 I/O 故障。新補粗體/對齊/緊行距、旋轉/CropBox 底稿、超長 Code128、UTF-8 QR raster 掃讀測試。
原 output/pdf/fixed-forms/verification.json 是舊來源快照,已過期;本輪不重新宣稱該批樣張的 AI 視覺檢視有效。
圖像資料與診斷邊界
目前 Image/PdfBackground 的空 bytes、超過 20MB、mediaType 不符等建構期契約檢查仍丟 IllegalArgumentException, job 會映射 report.failed;通過建構後的解碼/底稿內容錯誤才丟 FixedLayoutException,映射 report.invalid。 此分類尚未統一,呼叫端應在 adapter 先檢查素材契約;不能假設所有資料錯誤都會回 report.invalid。 建構期尚未組入頁面,例外 location 只有元件 ID;render 期才含 page/N/element。 直接呼叫 renderer/範本的程式可讀取例外的 code/location,ReportJob 對外狀態刻意只傳通用錯誤碼以免洩露內部識別; 失敗的逐欄錯誤資訊仍需另定授權與脫敏契約;成功固定產物的排版 warnings 已由新 job API 的 diagnostics 提供,與失敗訊息分開。