# Form Render Flow — Template Engine > **Status:** 📝 Planned (Phase 2) > **Mục tiêu:** render .docx/.xlsx giống 100% mẫu gốc với field động đã điền > **Spec 8 form:** [`../forms-spec.md`](../forms-spec.md) ## 1. Stack lựa chọn | Format | Library | Status | Lý do | |---|---|---|---| | `.docx` render | **DocumentFormat.OpenXml** 3.x | Chọn mặc định | Free, maintained by MS, dùng đủ cho template placeholder | | `.docx` render (alternative) | Aspose.Words | Option phí | Dễ dùng hơn, render PDF in-process, nhưng cần license | | `.xlsx` render | **ClosedXML** 0.105+ | Chọn mặc định | Free, LGPL, API dễ; support formula, merged cells, style | | `.xlsx` render (alternative) | EPPlus 7+ | Không chọn | License commercial sau v5; non-commercial license risky | | PDF convert | LibreOffice headless (`soffice --convert-to pdf`) | Production path | Free, chất lượng OK | | PDF convert (dev only) | Aspose.Words `SaveAs PDF` | Option nếu mua Aspose | In-process, không cần external binary | **Quyết định Phase 2:** OpenXml + ClosedXML + LibreOffice. Nếu render DOCX phức tạp quá (table lồng, heading numbering) → đánh giá lại Aspose. ## 2. Template placeholder syntax Sử dụng `{{fieldName}}` cho text đơn giản và `{{#loop}}...{{/loop}}` cho bảng lặp. Ví dụ trong file .docx (FO-002.05 Giao khoán): ``` Hợp đồng số: {{maHopDong}} Ngày: {{ngayKy}} Bên A: {{benA_tenCongTy}} Địa chỉ: {{benA_diaChi}} MST: {{benA_maSoThue}} Bên B: {{benB_tenNCC}} ... Danh mục công việc: | STT | Hạng mục | Đơn giá | Khối lượng | Thành tiền | | --- | -------- | ------- | ---------- | ---------- | {{#hangMuc}} | {{stt}} | {{tenHangMuc}} | {{donGia}} | {{khoiLuong}} | {{thanhTien}} | {{/hangMuc}} Tổng giá trị: {{giaTri}} VND ``` Field không có trong data → replace bằng rỗng hoặc `—` (config). ## 3. Flow render .docx ```mermaid sequenceDiagram participant Caller as API / Workflow participant REN as IFormRenderer participant DOCX as DocxRenderer participant OX as OpenXml participant FS as File Storage Caller->>REN: Render(templateId, dataDict) REN->>REN: Load ContractTemplate from DB
(get TemplatePath + FieldSpec) REN->>DOCX: RenderDocx(templatePath, dataDict) DOCX->>FS: Copy template → temp/{guid}.docx DOCX->>OX: Open WordprocessingDocument DOCX->>DOCX: Find all Text elements
(w:t nodes) loop Each w:t DOCX->>DOCX: Regex replace {{field}} → value end DOCX->>DOCX: Process {{#loop}} blocks:
1. Find row/paragraph có placeholder loop
2. Clone n-1 lần cho n items
3. Replace field trong từng clone DOCX->>OX: Save + Close OX-->>DOCX: byte[] hoặc path DOCX-->>REN: file bytes + metadata REN-->>Caller: RenderResult{bytes, fileName, contentType} ``` ## 4. Flow render .xlsx (FO-002.07 PO) ```mermaid sequenceDiagram participant Caller participant REN as XlsxRenderer participant CX as ClosedXML participant FS Caller->>REN: RenderXlsx(templateId, dataDict) REN->>FS: Copy template xlsx → temp REN->>CX: Open XLWorkbook loop Each sheet REN->>CX: Iterate cells alt Cell value contains {{field}} REN->>CX: Replace value với data[field]
(giữ style, format) end end loop Each named range "LoopHangMuc" REN->>REN: Insert n-1 rows, copy style, fill data REN->>REN: Adjust formula references (=SUM(...)) end REN->>CX: Recalculate formulas REN->>CX: SaveAs byte[] CX-->>REN: xlsx bytes REN-->>Caller: RenderResult ``` **ClosedXML edge:** phải gọi `workbook.CalculateMode = XLCalculateMode.Auto` + `workbook.FormulaParser.Calculate()` trước khi save, nếu không formula tổng vẫn giữ giá trị cũ. ## 5. PDF convert flow ```mermaid flowchart TD Start([Input: .docx bytes]) --> Save[Lưu temp file .docx] Save --> Run[Run: soffice --headless --convert-to pdf
--outdir /tmp in.docx] Run -->|exit 0| Read[Read /tmp/in.pdf bytes] Run -->|exit != 0| Fail[Log error
return docx thay vì pdf] Read --> Cleanup[Cleanup temp files] Cleanup --> Return([Return PDF bytes]) Fail --> Return ``` **Deploy prod:** - Windows IIS app pool cần LibreOffice portable ở `C:\Apps\LibreOffice` + `PATH` env var - Subprocess timeout 30s (nếu > 30s → fail, user download docx thay) - Pool soffice instance (1 instance / 10 requests) — tránh spawn process mỗi request ## 6. API endpoints ### Preview (không lưu) ``` POST /api/forms/render Body: { templateId, data, outputFormat: "docx" | "pdf" } Response: binary stream, Content-Disposition: inline (nếu pdf), attachment (nếu docx) ``` ### Finalize (lưu vào ContractAttachments) ``` POST /api/contracts/{id}/render-final Body: { outputFormat: "docx" } Response: { attachmentId, url } Side effect: INSERT ContractAttachments (Purpose='Export') ``` ## 7. Template management (admin) ```mermaid sequenceDiagram actor Admin participant FE as fe-admin
/master/contract-templates participant API as FormsController.UploadTemplate participant VAL as TemplateValidator participant DB participant FS Admin->>FE: Upload file .docx + metadata
(formCode, name, contractType, fieldSpec JSON) FE->>API: POST /api/forms/templates
multipart: file + metadata API->>VAL: ParsePlaceholders(file) VAL->>VAL: Extract all {{field}} tokens
→ compare với fieldSpec alt Token không match spec VAL-->>API: warning "Field X trong template không có trong spec" end API->>FS: Save file to wwwroot/templates/{guid}.docx API->>DB: INSERT ContractTemplate
(FormCode, Name, TemplatePath, FieldSpec, IsActive=true) API-->>FE: 201 ``` ## 8. Field types supported | Type | Render behavior | Example | |---|---|---| | `string` | Replace direct | `"Công ty ABC"` | | `text` | Replace + preserve line breaks (``) | `"Dòng 1\nDòng 2"` | | `number` | Format theo culture vi-VN | `150000000` → `"150.000.000"` | | `decimal` | Giá tiền: thêm `VND` suffix | `150000000` → `"150.000.000 VND"` | | `date` | Format `dd/MM/yyyy` | `2026-04-21` → `"21/04/2026"` | | `boolean` | Checkbox symbol | `true` → `"☑"`, `false` → `"☐"` | | `reference` | Lookup entity name | `supplierId → "Công ty NCC PVL"` | | `array` | Với `{{#loop}}` | danh mục hạng mục | ## 9. Edge cases | Case | Xử lý | |---|---| | Template bị rename/xóa file vật lý | Render throw `FileNotFoundException` → API 500, log error | | Template có macro (`.docm`) | Reject ở upload — chỉ accept `.docx` | | Placeholder bị split giữa 2 `` elements (Word quirk) | DocxRenderer normalize trước: merge adjacent `` trong cùng run | | Field có ký tự đặc biệt (`&`, `<`, `>`) | OpenXml auto XML-escape khi set Text value | | Loop template có 0 items | Remove template row hoàn toàn (không để lại placeholder row rỗng) | | File size > 10MB | Chunk upload hoặc reject (Phase 4 optimize) | | Render concurrent (2 user render cùng template) | Mỗi request lock copy riêng vào temp file → không conflict | ## 10. Performance | Operation | Budget | Note | |---|---|---| | Load template from DB + FS | < 100ms | Cache FileBytes in IMemoryCache 1h | | Render .docx (5 pages, 1 table 20 rows) | < 500ms | Stream-based, không full DOM | | Convert PDF via LibreOffice | 1-3s | Bottleneck, consider pool | | Full request (render + PDF + save attachment) | < 5s | Show spinner FE | ## 11. Testing checklist (Phase 2) - [ ] FO-002.05 Giao khoán: render với 10 hạng mục → output giống mẫu 100% - [ ] FO-002.07 PO xlsx: formula `=SUM(E5:E30)` recalculate đúng - [ ] Template với ký tự Unicode (đ, ư, ă) → không lỗi encoding - [ ] Template với 0 items loop → không còn dòng trống - [ ] PDF convert với 50-page docx → timeout setting OK - [ ] Concurrent 10 request cùng 1 template → mỗi file output độc lập ## 12. Liên quan - [`../forms-spec.md`](../forms-spec.md) — spec 8 form + field types - [`contract-creation-flow.md`](contract-creation-flow.md) — nơi gọi render - [`contract-approval-flow.md`](contract-approval-flow.md) — render lại khi có mã HĐ