[CLAUDE] Docs: database-guide + 6 flow diagrams
docs/database/database-guide.md: - Conventions (naming, data types, audit fields, soft delete) - Schema hien tai (Identity tables sau migration Init) + seed 12 role + admin - Schema planned: Phase 1 dot 2 (Supplier/Project/Department + Permission Matrix) - Schema planned: Phase 3 (Contract + Approval + Comment + Attachment + Template + Clause + CodeSequence) - Mermaid ERD cho tung phase - Migration workflow (create/apply/revert) - Index strategy + unique indexes - Backup/restore SQL - Common pitfalls + SQL cheatsheet docs/flows/ — 6 flow documentation: - README.md: index - auth-flow.md: login/refresh/me/logout (IMPLEMENTED, sequence + edge cases + security checklist) - permission-flow.md: Phase 1 dot 2 - Role x MenuKey x CRUD resolution + FE guard + BE policy - contract-creation-flow.md: Phase 2 - Drafter flow chon template -> fill -> preview -> save draft - contract-approval-flow.md: Phase 3 - state machine 9 phase chi tiet + reject flow + timeline UI - form-render-flow.md: Phase 2 - OpenXml + ClosedXML + LibreOffice PDF convert - sla-expiry-flow.md: Phase 3 - BackgroundService auto-approve qua SLA + warning notify Update references: - CLAUDE.md (root): them 2 row Tai lieu quan trong - docs/CLAUDE.md: update project layout voi flows/ + database/ - docs/STATUS.md: log docs addition - docs/changelog/migration-todos.md: tick Phase 0 docs items Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
219
docs/flows/form-render-flow.md
Normal file
219
docs/flows/form-render-flow.md
Normal file
@ -0,0 +1,219 @@
|
||||
# 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<br/>(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<br/>(w:t nodes)
|
||||
loop Each w:t
|
||||
DOCX->>DOCX: Regex replace {{field}} → value
|
||||
end
|
||||
|
||||
DOCX->>DOCX: Process {{#loop}} blocks:<br/>1. Find row/paragraph có placeholder loop<br/>2. Clone n-1 lần cho n items<br/>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]<br/>(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<br/>--outdir /tmp in.docx]
|
||||
Run -->|exit 0| Read[Read /tmp/in.pdf bytes]
|
||||
Run -->|exit != 0| Fail[Log error<br/>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<br/>/master/contract-templates
|
||||
participant API as FormsController.UploadTemplate
|
||||
participant VAL as TemplateValidator
|
||||
participant DB
|
||||
participant FS
|
||||
|
||||
Admin->>FE: Upload file .docx + metadata<br/>(formCode, name, contractType, fieldSpec JSON)
|
||||
FE->>API: POST /api/forms/templates<br/>multipart: file + metadata
|
||||
API->>VAL: ParsePlaceholders(file)
|
||||
VAL->>VAL: Extract all {{field}} tokens<br/>→ 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<br/>(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 (`<w:br/>`) | `"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 `<w:t>` elements (Word quirk) | DocxRenderer normalize trước: merge adjacent `<w:t>` 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Đ
|
||||
Reference in New Issue
Block a user