update skill and docs
This commit is contained in:
118
.claude/skills/sa-2-architecture/templates/contract-index.md
Normal file
118
.claude/skills/sa-2-architecture/templates/contract-index.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# CTR — Contract Files Index — <PROJECT>
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Version** | 1.0 |
|
||||
| **Date** | YYYY-MM-DD |
|
||||
| **Author** | <SA> (skill sa-2-architecture) |
|
||||
| **Status** | 🟡 Draft |
|
||||
| **Approved by** | Tech Lead: — · BE Lead: — · FE Lead: — |
|
||||
| **Source** | ICD_… v1.0 · API_… của BA · SRS_… §4.1 (mã lỗi) · DOM_… §5 (event) · SEC_… §3 |
|
||||
| **Scope** | |
|
||||
| **Confidence** | 🟡 |
|
||||
|
||||
## Change Log
|
||||
|
||||
| Version | Date | Người sửa | Thay đổi | ADR |
|
||||
|---|---|---|---|---|
|
||||
| 1.0 | | | Bản đầu | — |
|
||||
|
||||
> **Thẩm quyền:** file trong `02-architecture/contracts/` **thắng** mọi bảng mô tả endpoint — ở `ICD`,
|
||||
> ở `API` của BA, ở đây. Tài liệu này là **mục lục và biên bản lint**, không chép nội dung contract.
|
||||
>
|
||||
> **Vì sao có tài liệu này:** `ICD` mô tả interface bằng bảng, `API` của BA cũng bằng bảng. Dev phải
|
||||
> tự viết lại OpenAPI từ bảng, FE và BE viết hai bản khác nhau, mock server không dựng được, contract
|
||||
> test không có gì để chạy. File máy đọc đóng đúng lỗ đó.
|
||||
|
||||
---
|
||||
|
||||
## 1. Quy ước
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **REST** | OpenAPI **3.1** · một file mỗi module/bounded context · `contracts/openapi/<module>.yaml` |
|
||||
| **Event** | AsyncAPI **3.0** · một file mỗi domain event group · `contracts/asyncapi/<domain>-events.yaml` |
|
||||
| **gRPC** *(nếu có)* | `contracts/proto/<service>.proto` · versioning theo package |
|
||||
| **Đối tác ngoài** | `contracts/partners/<ten>.yaml` — **mirror** tài liệu đối tác, ghi rõ nguồn + ngày lấy; không tự viết thay đối tác |
|
||||
| **operationId** | `<module>.<động từ><DanhTừ>` — ví dụ `cartOrder.getCart`, `cartOrder.checkout`; **duy nhất toàn dự án** |
|
||||
| **Mã lỗi** | schema `Error { code, message, details? }`; `code` là `E-<DOMAIN>-nnnn` của SRS §4.1, liệt kê trong `x-error-codes` mỗi operation |
|
||||
| **Ba quy ước chốt** (`ICD` §1) | số lớn `type: string`; thời gian `format: date-time` UTC; phân trang theo `ADR-nnn` — khai báo một lần trong `components/` và `$ref` |
|
||||
| **Ví dụ** | mỗi operation ≥ 1 `example` thành công + 1 ví dụ lỗi 4xx |
|
||||
| **Lint** | `spectral lint` với ruleset `contracts/.spectral.yaml` nếu có; không có ⇒ kiểm cấu trúc bằng script ở §4 |
|
||||
|
||||
---
|
||||
|
||||
## 2. Mục lục file — `CTR-nn`
|
||||
|
||||
| `CTR` | File | Loại | Phủ `IF-nnn` | Owner (`ICD` §2) | Version contract | Lint | Ngày lint |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `CTR-01` | `contracts/openapi/cart-order.yaml` | OpenAPI 3.1 | `IF-002` | BE Nhóm Giao dịch | `1.0.0` | ☐ | |
|
||||
| `CTR-02` | `contracts/asyncapi/order-events.yaml` | AsyncAPI 3.0 | `IF-009` | BE Nhóm Giao dịch (publish) | `1.0.0` | ☐ | |
|
||||
| `CTR-03` | `contracts/partners/vnpay.yaml` | mirror | `IF-007` | VNPay — 🔴 chưa có tài liệu thật (`ARISK-03`) | — | — | |
|
||||
|
||||
---
|
||||
|
||||
## 3. Ánh xạ `IF` → operation → BA
|
||||
|
||||
*Bảng `sa-conformance` đọc. Mỗi `IF-nnn` sync trong `ICD` §2 phải có ≥ 1 dòng; mỗi endpoint trong
|
||||
`API` của BA phải có `operationId`.*
|
||||
|
||||
| `IF-nnn` | `operationId` / channel | Method + path / topic | Endpoint `API` của BA | `AC` liên quan | Mã lỗi (`E-…`) trong schema | `ROLE` (`SEC` §3) |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `IF-002` | `cartOrder.getCart` | `GET /v1/cart` | `API_US002-003 §3.1` | `AC-US002-01..03` | `E-CART-0401`, `E-CART-0503` | `ROLE-01`, Guest |
|
||||
| `IF-002` | `cartOrder.checkout` | `POST /v1/checkout` | `API_US002-003 §3.4` | `AC-US003-01..10` | `E-CHK-0409`, `E-CHK-0422`, `E-CHK-0503` | `ROLE-01`, Guest |
|
||||
| `IF-009` | `OrderPlaced` | topic `order.placed.v1` | — | — | — | publish `CMP-04` |
|
||||
|
||||
🔴 Endpoint có trong `API` của BA mà không có `operationId` ⇒ hoặc BA đề xuất endpoint không tồn tại,
|
||||
hoặc `CTR` bỏ sót. Xử lý ngay, báo BA qua `baSyncIssues`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Kết quả lint và kiểm cấu trúc
|
||||
|
||||
*Người điều phối (có Bash) chạy, chép nguyên văn vào đây. Runner không có Bash ⇒ ghi "chưa lint".*
|
||||
|
||||
```bash
|
||||
# có spectral
|
||||
npx @stoplight/spectral-cli lint contracts/openapi/*.yaml contracts/asyncapi/*.yaml
|
||||
# không có spectral — kiểm cấu trúc tối thiểu
|
||||
node .claude/skills/sa-2-architecture/scripts/contract-check.mjs --dir sa-output/<P>/02-architecture/contracts \
|
||||
--icd sa-output/<P>/02-architecture/ICD_<P>_v1.0.md --srs ba-output/<P>/03-specification/SRS_*.md
|
||||
```
|
||||
|
||||
| Kiểm | Kết quả | Chi tiết |
|
||||
|---|---|---|
|
||||
| Mọi file parse được, `openapi: 3.1.x` / `asyncapi: 3.0.x` | ☐ | |
|
||||
| Mọi `operationId` duy nhất | ☐ | |
|
||||
| Mọi `IF-nnn` sync của `ICD` §2 có operation | ☐ | |
|
||||
| Mọi mã lỗi `E-…` trong SRS §4.1 xuất hiện trong ≥ 1 `x-error-codes` | ☐ | |
|
||||
| Số lớn là `string`, thời gian `format: date-time` | ☐ | |
|
||||
| Mỗi operation có `example` thành công + lỗi | ☐ | |
|
||||
|
||||
---
|
||||
|
||||
## 5. Sinh code và mock từ contract *(khuyến nghị cho `AGD`)*
|
||||
|
||||
| Việc | Công cụ gợi ý | Ai | Ghi ở |
|
||||
|---|---|---|---|
|
||||
| Sinh client/server stub | openapi-generator · orval · oapi-codegen | BE/FE | `AGD` §2 reference implementation |
|
||||
| Mock server cho FE | Prism · MSW từ OpenAPI | FE | `AGD` |
|
||||
| Contract test | Pact · schemathesis | QA/BE | `FIT-nn` |
|
||||
|
||||
---
|
||||
|
||||
## 6. Giả định & Ngoài phạm vi
|
||||
|
||||
**Giả định:**
|
||||
|
||||
| ID | Giả định | Cách xác minh | Nếu sai |
|
||||
|---|---|---|---|
|
||||
|
||||
**Ngoài phạm vi:**
|
||||
|
||||
- *(ví dụ: không viết contract cho interface in-process cùng module — thuộc `AGD`)*
|
||||
|
||||
## 7. Open Questions
|
||||
|
||||
| ID | Câu hỏi | Hỏi ai | Từ ngày | Chặn gì | Hệ quả nếu trả lời ngược |
|
||||
|---|---|---|---|---|---|
|
||||
@@ -0,0 +1,71 @@
|
||||
# CTR-nn · contracts/asyncapi/<domain>-events.yaml — AsyncAPI 3.0
|
||||
# Nguồn: ICD_<P> §4 (hợp đồng sự kiện) · DOM_<P> §5 (domain event) · DAT §4.1 (outbox/saga) · SEC §5 (PII)
|
||||
# Quy ước: at-least-once, người nhận khử trùng bằng eventId; breaking change ⇒ eventVersion mới + channel mới
|
||||
asyncapi: 3.0.0
|
||||
info:
|
||||
title: <Domain> Events
|
||||
version: 1.0.0
|
||||
description: |
|
||||
Sự kiện phát từ CMP-nn qua IF-nnn. File này THẮNG bảng ICD §4 khi lệch.
|
||||
Không chứa PII ngoài id trừ khi SEC §5 cho phép đích danh.
|
||||
defaultContentType: application/json
|
||||
servers:
|
||||
prod:
|
||||
host: <broker-host>
|
||||
protocol: sqs # kafka | sqs | amqp | eventbridge
|
||||
description: production
|
||||
channels:
|
||||
orderPlaced:
|
||||
address: <domain>.<event>.v1
|
||||
description: Phát khi aggregate <Root> commit xong (DOM §5); publish qua outbox (ADR-nnn)
|
||||
messages:
|
||||
OrderPlaced:
|
||||
$ref: '#/components/messages/OrderPlaced'
|
||||
operations:
|
||||
publishOrderPlaced:
|
||||
action: send
|
||||
channel: { $ref: '#/channels/orderPlaced' }
|
||||
summary: CMP-nn publish — IF-nnn
|
||||
x-if: IF-nnn
|
||||
x-producer: CMP-nn
|
||||
x-consumers: [CMP-06, CMP-09]
|
||||
x-ordering: theo partition key <aggregateId>
|
||||
x-delivery: at-least-once · DLQ giữ 14 ngày (FAIL FM-nn)
|
||||
components:
|
||||
messages:
|
||||
OrderPlaced:
|
||||
name: OrderPlaced
|
||||
title: Đơn hàng đã đặt
|
||||
contentType: application/json
|
||||
headers:
|
||||
$ref: '#/components/schemas/EventHeaders'
|
||||
payload:
|
||||
$ref: '#/components/schemas/OrderPlacedPayload'
|
||||
examples:
|
||||
- name: ok
|
||||
headers: { eventId: "018f…", eventVersion: "1.0", occurredAt: "2026-09-22T03:00:00Z", correlationId: "req-…" }
|
||||
payload: { orderId: "018f…", customerId: "018e…", sellerIds: ["018d…"], totalAmount: { amount: "150000.00", currency: "VND" } }
|
||||
schemas:
|
||||
EventHeaders:
|
||||
type: object
|
||||
required: [eventId, eventVersion, occurredAt, correlationId]
|
||||
properties:
|
||||
eventId: { type: string, description: 'Khoá khử trùng cho consumer (ICD §4)' }
|
||||
eventVersion: { type: string, pattern: '^[0-9]+\.[0-9]+$' }
|
||||
occurredAt: { type: string, format: date-time }
|
||||
correlationId: { type: string, description: 'Truyền xuyên suốt từ request gốc (ICD §1 điểm 8)' }
|
||||
Money:
|
||||
type: object
|
||||
required: [amount, currency]
|
||||
properties:
|
||||
amount: { type: string, pattern: '^-?[0-9]+(\.[0-9]{1,2})?$' }
|
||||
currency: { type: string, minLength: 3, maxLength: 3 }
|
||||
OrderPlacedPayload:
|
||||
type: object
|
||||
additionalProperties: false
|
||||
required: [orderId, sellerIds, totalAmount]
|
||||
properties:
|
||||
orderId: { type: string }
|
||||
customerId: { type: [string, 'null'], description: 'null = Guest' }
|
||||
sellerIds: { type: array, items: { type: string }, minItems: 1 }
|
||||
totalAmount: { $ref: '#/components/schemas/Money' }
|
||||
@@ -0,0 +1,155 @@
|
||||
# CTR-nn · contracts/openapi/<module>.yaml — OpenAPI 3.1
|
||||
# Nguồn: ICD_<P> §3 (IF-nnn) · API_<US> của BA · SRS §4.1 (mã lỗi) · SEC §3 (quyền)
|
||||
# Quy ước: ICD §1 — số lớn là string, thời gian date-time UTC, lỗi nghiệp vụ 4xx, phân trang theo ADR-nnn
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: <Module> API
|
||||
version: 1.0.0
|
||||
description: |
|
||||
Contract cho IF-nnn. Tài liệu này THẮNG bảng mô tả trong ICD/API khi lệch.
|
||||
Mã lỗi theo SRS §4.1; quyền theo SEC §3.
|
||||
servers:
|
||||
- url: https://api.<domain>/v1
|
||||
description: production
|
||||
- url: https://api-stg.<domain>/v1
|
||||
description: staging
|
||||
tags:
|
||||
- name: <resource>
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/<resource>:
|
||||
get:
|
||||
operationId: <module>.list<Resource>
|
||||
summary: <Mục đích một câu — khớp ICD §3.x>
|
||||
tags: [<resource>]
|
||||
x-if: IF-nnn
|
||||
x-roles: [ROLE-01, ROLE-02]
|
||||
x-error-codes: [E-<DOMAIN>-0401, E-<DOMAIN>-0403, E-<DOMAIN>-0503]
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/PageCursor'
|
||||
- $ref: '#/components/parameters/PageSize'
|
||||
responses:
|
||||
'200':
|
||||
description: Danh sách
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/<Resource>Page'
|
||||
examples:
|
||||
ok:
|
||||
value: { items: [], nextCursor: null, hasNext: false }
|
||||
'401': { $ref: '#/components/responses/Unauthorized' }
|
||||
'403': { $ref: '#/components/responses/Forbidden' }
|
||||
'503': { $ref: '#/components/responses/Unavailable' }
|
||||
post:
|
||||
operationId: <module>.create<Resource>
|
||||
summary: <Mục đích>
|
||||
tags: [<resource>]
|
||||
x-if: IF-nnn
|
||||
x-roles: [ROLE-01]
|
||||
x-idempotency: header Idempotency-Key, giữ 24h (ADR-nnn)
|
||||
x-error-codes: [E-<DOMAIN>-0400, E-<DOMAIN>-0409, E-<DOMAIN>-0503]
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/IdempotencyKey'
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/<Resource>Create'
|
||||
examples:
|
||||
ok:
|
||||
value: { name: "Cửa hàng 01" }
|
||||
responses:
|
||||
'201':
|
||||
description: Đã tạo
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/<Resource>' }
|
||||
'200':
|
||||
description: Trùng Idempotency-Key — trả bản ghi cũ, không tạo mới
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/<Resource>' }
|
||||
'400': { $ref: '#/components/responses/BadRequest' }
|
||||
'409': { $ref: '#/components/responses/Conflict' }
|
||||
'503': { $ref: '#/components/responses/Unavailable' }
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
parameters:
|
||||
IdempotencyKey:
|
||||
name: Idempotency-Key
|
||||
in: header
|
||||
required: true
|
||||
schema: { type: string, maxLength: 64 }
|
||||
PageCursor:
|
||||
name: cursor
|
||||
in: query
|
||||
schema: { type: string }
|
||||
PageSize:
|
||||
name: size
|
||||
in: query
|
||||
schema: { type: integer, minimum: 1, maximum: 100, default: 20 }
|
||||
schemas:
|
||||
Id:
|
||||
type: string
|
||||
description: Số lớn truyền dạng string (ICD §1 điểm 1)
|
||||
pattern: '^[0-9a-f-]{36}$'
|
||||
Money:
|
||||
type: object
|
||||
required: [amount, currency]
|
||||
properties:
|
||||
amount: { type: string, pattern: '^-?[0-9]+(\.[0-9]{1,2})?$', description: 'decimal dạng string' }
|
||||
currency: { type: string, minLength: 3, maxLength: 3, example: VND }
|
||||
Error:
|
||||
type: object
|
||||
required: [code, message]
|
||||
properties:
|
||||
code: { type: string, pattern: '^E-[A-Z]+-[0-9]{4}$', description: 'Mã lỗi SRS §4.1' }
|
||||
message: { type: string, description: 'Text hiển thị theo SRS §4.2 hoặc mã để client dịch (ICD §1 điểm 6)' }
|
||||
details:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
field: { type: string }
|
||||
code: { type: string }
|
||||
<Resource>:
|
||||
type: object
|
||||
required: [id, createdAt]
|
||||
properties:
|
||||
id: { $ref: '#/components/schemas/Id' }
|
||||
createdAt: { type: string, format: date-time, description: 'UTC ISO-8601' }
|
||||
<Resource>Create:
|
||||
type: object
|
||||
required: [name]
|
||||
properties:
|
||||
name: { type: string, minLength: 1, maxLength: 40, description: '40 code point — khớp FLD-<SCR>-nn' }
|
||||
<Resource>Page:
|
||||
type: object
|
||||
required: [items, hasNext]
|
||||
properties:
|
||||
items: { type: array, items: { $ref: '#/components/schemas/<Resource>' } }
|
||||
nextCursor: { type: [string, 'null'] }
|
||||
hasNext: { type: boolean }
|
||||
responses:
|
||||
BadRequest:
|
||||
description: Sai dữ liệu vào
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/Error' }, example: { code: E-<DOMAIN>-0400, message: "…" } } }
|
||||
Unauthorized:
|
||||
description: Hết phiên
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
|
||||
Forbidden:
|
||||
description: Không đủ quyền — client không xoá dữ liệu đã nhập (ICD §6)
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
|
||||
Conflict:
|
||||
description: Vi phạm ràng buộc nghiệp vụ (BR-nnn)
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
|
||||
Unavailable:
|
||||
description: Phụ thuộc hỏng/timeout (FAIL FM-nn) — client cho thử lại, giữ dữ liệu đã nhập
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
|
||||
@@ -32,8 +32,10 @@ read model.
|
||||
|
||||
## 2. Mô hình dữ liệu mức khái niệm
|
||||
|
||||
*Thực thể và quan hệ, chưa phải schema. Schema chi tiết thuộc dev.*
|
||||
*Thực thể và quan hệ, chưa phải schema. Schema vật lý đủ cột, index, DDL và migration nằm ở
|
||||
`PDM` (`--focus pdm`), sinh sau tài liệu này — mỗi thực thể ở đây phải có ≥ 1 bảng trong `PDM` §2.*
|
||||
|
||||
<!-- archify: mermaid-only -->
|
||||
```mermaid
|
||||
erDiagram
|
||||
```
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"diagram_type": "architecture",
|
||||
"meta": {
|
||||
"title": "C4 Container — Hệ thống đối soát (mẫu)",
|
||||
"quality_profile": "showcase",
|
||||
"views": [
|
||||
{
|
||||
"id": "duong-chinh",
|
||||
"label": "Đường chính nạp và đối soát",
|
||||
"focus": [
|
||||
"NV",
|
||||
"WEB",
|
||||
"API",
|
||||
"DB"
|
||||
],
|
||||
"note": "Người dùng tải file POS, API ghi và đối soát trên CSDL chính."
|
||||
},
|
||||
{
|
||||
"id": "duong-bat-dong-bo",
|
||||
"label": "Nhánh bất đồng bộ",
|
||||
"focus": [
|
||||
"API",
|
||||
"QUEUE",
|
||||
"WORKER",
|
||||
"POS"
|
||||
],
|
||||
"note": "Đối soát nặng chạy nền, gọi hệ thống POS ngoài có timeout."
|
||||
}
|
||||
]
|
||||
},
|
||||
"components": [
|
||||
{
|
||||
"id": "NV",
|
||||
"type": "external",
|
||||
"label": "Nhân viên đối soát",
|
||||
"sublabel": "ROLE-01 · trình duyệt",
|
||||
"pos": [
|
||||
40,
|
||||
300
|
||||
],
|
||||
"size": [
|
||||
150,
|
||||
60
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "IDP",
|
||||
"type": "security",
|
||||
"label": "IdP nội bộ",
|
||||
"sublabel": "OIDC · IF-001",
|
||||
"pos": [
|
||||
560,
|
||||
100
|
||||
],
|
||||
"size": [
|
||||
150,
|
||||
60
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "WEB",
|
||||
"type": "frontend",
|
||||
"label": "CMP-01 Web App",
|
||||
"sublabel": "SPA · SCR-01..05",
|
||||
"pos": [
|
||||
300,
|
||||
300
|
||||
],
|
||||
"size": [
|
||||
150,
|
||||
60
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "API",
|
||||
"type": "backend",
|
||||
"label": "CMP-02 Settlement API",
|
||||
"sublabel": "REST/JSON · /v1",
|
||||
"pos": [
|
||||
560,
|
||||
300
|
||||
],
|
||||
"size": [
|
||||
150,
|
||||
60
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "DB",
|
||||
"type": "database",
|
||||
"label": "CMP-03 CSDL chính",
|
||||
"sublabel": "PostgreSQL · SoT",
|
||||
"pos": [
|
||||
820,
|
||||
300
|
||||
],
|
||||
"size": [
|
||||
150,
|
||||
60
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "QUEUE",
|
||||
"type": "messagebus",
|
||||
"label": "CMP-04 Hàng đợi",
|
||||
"sublabel": "SQS FIFO · IF-004",
|
||||
"pos": [
|
||||
560,
|
||||
520
|
||||
],
|
||||
"size": [
|
||||
150,
|
||||
60
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "WORKER",
|
||||
"type": "backend",
|
||||
"label": "CMP-05 Worker",
|
||||
"sublabel": "job đối soát nền",
|
||||
"pos": [
|
||||
820,
|
||||
520
|
||||
],
|
||||
"size": [
|
||||
150,
|
||||
60
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "POS",
|
||||
"type": "external",
|
||||
"label": "Hệ thống POS",
|
||||
"sublabel": "đối tác · IF-005",
|
||||
"pos": [
|
||||
1080,
|
||||
520
|
||||
],
|
||||
"size": [
|
||||
150,
|
||||
60
|
||||
]
|
||||
}
|
||||
],
|
||||
"boundaries": [
|
||||
{
|
||||
"kind": "region",
|
||||
"label": "Vùng hệ thống của ta · ap-southeast-1",
|
||||
"wraps": [
|
||||
"WEB",
|
||||
"API",
|
||||
"DB",
|
||||
"QUEUE",
|
||||
"WORKER"
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "security-group",
|
||||
"label": "sg-app · chỉ nhận từ ALB",
|
||||
"wraps": [
|
||||
"API",
|
||||
"WORKER"
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": [
|
||||
{
|
||||
"id": "nv-web",
|
||||
"from": "NV",
|
||||
"to": "WEB",
|
||||
"label": "HTTPS · sync",
|
||||
"variant": "emphasis"
|
||||
},
|
||||
{
|
||||
"id": "web-api",
|
||||
"from": "WEB",
|
||||
"to": "API",
|
||||
"label": "REST · IF-002",
|
||||
"variant": "emphasis"
|
||||
},
|
||||
{
|
||||
"id": "idp-api",
|
||||
"from": "IDP",
|
||||
"to": "API",
|
||||
"label": "JWT · IF-001",
|
||||
"variant": "security",
|
||||
"labelDy": 24
|
||||
},
|
||||
{
|
||||
"id": "api-db",
|
||||
"from": "API",
|
||||
"to": "DB",
|
||||
"label": "SQL · sync",
|
||||
"variant": "emphasis"
|
||||
},
|
||||
{
|
||||
"id": "api-queue",
|
||||
"from": "API",
|
||||
"to": "QUEUE",
|
||||
"label": "publish · async",
|
||||
"variant": "dashed",
|
||||
"labelDy": 24
|
||||
},
|
||||
{
|
||||
"id": "queue-worker",
|
||||
"from": "QUEUE",
|
||||
"to": "WORKER",
|
||||
"label": "consume",
|
||||
"variant": "dashed"
|
||||
},
|
||||
{
|
||||
"id": "worker-db",
|
||||
"from": "WORKER",
|
||||
"to": "DB",
|
||||
"label": "SQL · sync",
|
||||
"labelDy": -24
|
||||
},
|
||||
{
|
||||
"id": "worker-pos",
|
||||
"from": "WORKER",
|
||||
"to": "POS",
|
||||
"label": "REST · 3s ·×3",
|
||||
"variant": "dashed"
|
||||
}
|
||||
],
|
||||
"cards": [
|
||||
{
|
||||
"dot": "emerald",
|
||||
"title": "Đường chính",
|
||||
"items": [
|
||||
"Người dùng → Web → API → CSDL chính, toàn bộ đồng bộ",
|
||||
"CSDL chính là single source of truth (DAT §1)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dot": "amber",
|
||||
"title": "Nhánh bất đồng bộ",
|
||||
"items": [
|
||||
"Đối soát nặng đẩy qua hàng đợi FIFO, worker xử lý at-least-once",
|
||||
"Gọi POS ngoài có timeout 3s, retry 3 lần theo FAIL FM-05"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dot": "rose",
|
||||
"title": "Bảo mật",
|
||||
"items": [
|
||||
"IdP nội bộ phát JWT, API kiểm ở tầng service (SEC §3)",
|
||||
"API và Worker nằm trong security group chỉ nhận từ ALB"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"diagram_type": "sequence",
|
||||
"meta": {
|
||||
"title": "Luồng chính — Lưu cửa hàng có kiểm tra POS (mẫu)",
|
||||
"quality_profile": "showcase",
|
||||
"viewBox": [1080, 600],
|
||||
"column_fit": "spread",
|
||||
"views": [
|
||||
{ "id": "thanh-cong", "label": "Nhánh thành công", "focus": ["U", "FE", "BE", "POS"], "note": "POS phản hồi trong 3s, hệ thống lưu và báo đã lưu." },
|
||||
{ "id": "timeout", "label": "Nhánh POS timeout", "focus": ["BE", "POS", "FE", "U"], "note": "Hết 3s không có phản hồi: trả 503 E-STR-0503, giữ nguyên dữ liệu đã nhập." }
|
||||
]
|
||||
},
|
||||
"participants": [
|
||||
{ "id": "U", "type": "external", "label": "Nhân viên", "sublabel": "ROLE-01" },
|
||||
{ "id": "FE", "type": "frontend", "label": "Giao diện", "sublabel": "SCR-02 Chi tiết" },
|
||||
{ "id": "BE", "type": "backend", "label": "Settlement API", "sublabel": "POST /stores" },
|
||||
{ "id": "POS", "type": "external", "label": "Hệ thống POS", "sublabel": "IF-005 · timeout 3s" }
|
||||
],
|
||||
"segments": [
|
||||
{ "from": 160, "to": 300, "label": "Gửi và kiểm tra" },
|
||||
{ "from": 310, "to": 400, "label": "Nhánh thành công" },
|
||||
{ "from": 410, "to": 520, "label": "Nhánh POS timeout" }
|
||||
],
|
||||
"messages": [
|
||||
{ "id": "m1", "from": "U", "to": "FE", "y": 175, "label": "Bấm Lưu", "variant": "default" },
|
||||
{ "id": "m2", "from": "FE", "to": "BE", "y": 210, "label": "POST /stores", "variant": "emphasis" },
|
||||
{ "id": "m3", "from": "BE", "to": "POS", "y": 250, "label": "GET /verify (timeout 3s)", "variant": "emphasis" },
|
||||
{ "id": "m4", "from": "POS", "to": "BE", "y": 320, "label": "200 OK", "variant": "return" },
|
||||
{ "id": "m5", "from": "BE", "to": "FE", "y": 350, "label": "200 + id", "variant": "return" },
|
||||
{ "id": "m6", "from": "FE", "to": "U", "y": 380, "label": "Toast \"Đã lưu\"", "variant": "return" },
|
||||
{ "id": "m7", "from": "POS", "to": "BE", "y": 430, "label": "timeout sau 3s", "variant": "dashed", "note": "FAIL FM-05" },
|
||||
{ "id": "m8", "from": "BE", "to": "FE", "y": 465, "label": "503 · E-STR-0503", "variant": "return" },
|
||||
{ "id": "m9", "from": "FE", "to": "U", "y": 500, "label": "Báo lỗi, GIỮ dữ liệu đã nhập", "variant": "return" }
|
||||
],
|
||||
"activations": [
|
||||
{ "participant": "FE", "from": 170, "to": 505, "type": "frontend" },
|
||||
{ "participant": "BE", "from": 205, "to": 470, "type": "backend" },
|
||||
{ "participant": "POS", "from": 245, "to": 435, "type": "external" }
|
||||
],
|
||||
"cards": [
|
||||
{ "dot": "emerald", "title": "Thành công", "items": ["POS trả 200 trong 3s, API lưu và trả id", "Giao diện hiện toast theo UICONV §7"] },
|
||||
{ "dot": "rose", "title": "Lỗi", "items": ["Timeout 3s là ngân sách theo FAIL §2, không tự đặt", "503 E-STR-0503 cho thử lại, dữ liệu đã nhập không mất (W4)"] }
|
||||
]
|
||||
}
|
||||
157
.claude/skills/sa-2-architecture/templates/domain-model.md
Normal file
157
.claude/skills/sa-2-architecture/templates/domain-model.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# DOM — Domain Model — <PROJECT>
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Version** | 1.0 |
|
||||
| **Date** | YYYY-MM-DD |
|
||||
| **Author** | <SA> (skill sa-2-architecture) |
|
||||
| **Status** | 🟡 Draft |
|
||||
| **Approved by** | Tech Lead: — · BE Lead: — |
|
||||
| **Source** | SAD_… v1.0 · DAT_… v1.0 · BR_… của BA · BACKLOG_… của BA |
|
||||
| **Scope** | |
|
||||
| **Confidence** | 🟡 |
|
||||
|
||||
## Change Log
|
||||
|
||||
| Version | Date | Người sửa | Thay đổi | ADR |
|
||||
|---|---|---|---|---|
|
||||
| 1.0 | | | Bản đầu | — |
|
||||
|
||||
> **Thẩm quyền khi mâu thuẫn:** sơ đồ thắng về **quan hệ và luồng**; bảng thắng về **ràng buộc và
|
||||
> con số**. `BR` của BA là **nguồn** của mọi invariant ở đây; `DOM` chỉ nói invariant đó sống ở
|
||||
> class nào. Lệch với `BR` ⇒ `OQ`, không tự sửa rule.
|
||||
|
||||
> **Vì sao có tài liệu này:** `DAT` nói *ai sở hữu dữ liệu gì*, `PDM` nói *bảng vật lý trông thế
|
||||
> nào*. Không có `DOM`, dev tự suy ra aggregate từ bảng — và mỗi người suy một kiểu, transaction
|
||||
> boundary lệch nhau, invariant bị kiểm ở ba chỗ hoặc không chỗ nào.
|
||||
|
||||
---
|
||||
|
||||
## 1. Bounded context và aggregate
|
||||
|
||||
*Một dòng mỗi aggregate. Aggregate là **ranh giới transaction**: mọi invariant trong aggregate được
|
||||
kiểm trong một lần ghi; giữa các aggregate chỉ có eventual consistency (`DAT` §4).*
|
||||
|
||||
| Bounded context | `CMP-nn` sở hữu | Aggregate root | Entity con | Value object | Invariant chính (`BR`) | Sự kiện phát ra |
|
||||
|---|---|---|---|---|---|---|
|
||||
| Cart & Order | `CMP-04` | `Order` | `OrderSeller`, `OrderItem` | `Money`, `Address` | `BR-014` tổng tiền = Σ dòng | `OrderPlaced` |
|
||||
| | | | | | | |
|
||||
|
||||
🔴 **Một entity thuộc đúng một aggregate.** Entity xuất hiện ở hai aggregate ⇒ một trong hai chỉ
|
||||
được giữ **id tham chiếu** (không giữ object), ghi rõ ở §3.
|
||||
|
||||
---
|
||||
|
||||
## 2. Class diagram theo bounded context
|
||||
|
||||
*Một sơ đồ mỗi bounded context, ≤ 12 class. Chỉ vẽ thuộc tính nghiệp vụ và phương thức đổi trạng
|
||||
thái; getter/setter, DTO, repository không vẽ ở đây (thuộc `AGD`).*
|
||||
|
||||
### 2.1 <Bounded context>
|
||||
|
||||
<!-- archify: mermaid-only -->
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Order {
|
||||
+OrderId id
|
||||
+CustomerId customerId
|
||||
+Money totalAmount
|
||||
+OrderStatus status
|
||||
+place(cart) Order
|
||||
+cancel(reason)
|
||||
}
|
||||
class OrderSeller {
|
||||
+SellerId sellerId
|
||||
+Money subtotal
|
||||
+confirm()
|
||||
}
|
||||
class OrderItem {
|
||||
+ProductVariantId variantId
|
||||
+int quantity
|
||||
+Money unitPriceSnapshot
|
||||
}
|
||||
class Money {
|
||||
<<value object>>
|
||||
+decimal amount
|
||||
+Currency currency
|
||||
}
|
||||
Order "1" *-- "1..*" OrderSeller : splits into
|
||||
OrderSeller "1" *-- "1..*" OrderItem : contains
|
||||
Order ..> Money
|
||||
OrderItem ..> Money
|
||||
```
|
||||
|
||||
**Bảng đi kèm** *(W13 — sơ đồ không nói được invariant kiểm ở đâu và ai được gọi)*:
|
||||
|
||||
| Class | Loại | Thuộc tính nghiệp vụ | Invariant (`BR`) | Kiểm ở phương thức | Ai được gọi (`ROLE`/`CMP`) | Bảng `PDM` |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `Order` | aggregate root | `totalAmount`, `status` | `BR-014`, `BR-021` | `place()`, `cancel()` | `ROLE-01` qua `IF-002` | `TBL-012 order` |
|
||||
| `Money` | value object | `amount`, `currency` | `BR-003` không âm | constructor | — | cột `*_amount` |
|
||||
|
||||
**Quan hệ:** `*--` composition (cùng transaction) · `o--` aggregation (khác vòng đời) · `..>` phụ
|
||||
thuộc · `-->` tham chiếu bằng id sang aggregate khác.
|
||||
|
||||
### 2.2 <Bounded context tiếp theo>
|
||||
|
||||
*(cùng cấu trúc)*
|
||||
|
||||
---
|
||||
|
||||
## 3. Tham chiếu xuyên aggregate
|
||||
|
||||
*Chỗ hay sai nhất: giữ object của aggregate khác thay vì id, rồi load cả cây.*
|
||||
|
||||
| Từ aggregate | Tham chiếu tới | Bằng gì | Đọc dữ liệu kia bằng | Trễ chấp nhận (`DAT` §4) |
|
||||
|---|---|---|---|---|
|
||||
| `Order` | `Customer` | `CustomerId` | `IF-0nn` / read model | eventual ≤ … |
|
||||
| `OrderItem` | `ProductVariant` | `ProductVariantId` + snapshot giá/tên | snapshot lúc đặt | không cần đồng bộ |
|
||||
|
||||
🔴 **Snapshot hay tham chiếu sống?** Giá, tên, địa chỉ lúc giao dịch phải là **snapshot** (thay đổi
|
||||
sau đó không được đổi lịch sử). Ghi rõ từng trường, đây là nguồn của nhiều bug đối soát.
|
||||
|
||||
---
|
||||
|
||||
## 4. Vòng đời trạng thái → class
|
||||
|
||||
*Bảng chuyển trạng thái nằm ở `BR` §3; đây chỉ map xuống class và phương thức.*
|
||||
|
||||
| Entity | Enum trạng thái | Thuộc `BR` | Phương thức chuyển | Guard kiểm ở đâu | Ghi vết (`SEC` §audit) |
|
||||
|---|---|---|---|---|---|
|
||||
| `Order` | `OrderStatus` | `BR-021` | `confirm()`, `cancel()` | trong aggregate | `order_status_history` |
|
||||
|
||||
---
|
||||
|
||||
## 5. Domain event
|
||||
|
||||
| Sự kiện | Phát từ aggregate | Khi nào | Payload tối thiểu | Người nhận (`CMP`) | Contract (`CTR`) |
|
||||
|---|---|---|---|---|---|
|
||||
| `OrderPlaced` | `Order` | `place()` commit xong | `orderId`, `customerId`, `sellerIds[]`, `totalAmount` | `CMP-06`, `CMP-09` | `contracts/asyncapi/order-events.yaml#OrderPlaced` |
|
||||
|
||||
🔴 Payload event **không chứa PII** ngoài id trừ khi `SEC` §5 cho phép đích danh.
|
||||
|
||||
---
|
||||
|
||||
## 6. Đối chiếu
|
||||
|
||||
| Kiểm | Kết quả | Hành động |
|
||||
|---|---|---|
|
||||
| Mọi thực thể trong `DAT` §1 có class ở §2 | n/n | |
|
||||
| Mọi `BR` loại "ràng buộc dữ liệu"/"tính toán" có class kiểm | n/n | |
|
||||
| Mọi entity có trạng thái ở `BR` §3 có enum + phương thức ở §4 | n/n | |
|
||||
| Mọi class root có bảng trong `PDM` §2 | n/n | *(điền khi `PDM` xong)* |
|
||||
|
||||
## 7. Giả định & Ngoài phạm vi
|
||||
|
||||
**Giả định:**
|
||||
|
||||
| ID | Giả định | Cách xác minh | Nếu sai |
|
||||
|---|---|---|---|
|
||||
|
||||
**Ngoài phạm vi:**
|
||||
|
||||
- *(ví dụ: không mô hình hoá module báo cáo — chỉ đọc, dùng read model của `DAT` §4)*
|
||||
|
||||
## 8. Open Questions
|
||||
|
||||
| ID | Câu hỏi | Hỏi ai | Từ ngày | Chặn gì | Hệ quả nếu trả lời ngược |
|
||||
|---|---|---|---|---|---|
|
||||
@@ -48,6 +48,7 @@ quả đo `QAS` không?** Đo hiệu năng trên máy nhỏ hơn 4 lần rồi k
|
||||
|
||||
## 3. Sơ đồ triển khai
|
||||
|
||||
<!-- archify: architecture · diagrams/INF_deployment.architecture.json -->
|
||||
```mermaid
|
||||
flowchart TB
|
||||
```
|
||||
|
||||
@@ -60,6 +60,11 @@ diện không hiện lỗi. Chốt 4xx ngay ở đây.
|
||||
🔴 **Interface không biết ai sở hữu là interface sẽ bị đổi mà không ai báo.** Cột này không
|
||||
được để trống, kể cả với hệ thống nội bộ.
|
||||
|
||||
🔴 **Cột "Contract ở đâu" phải trỏ tới file thật** trong `02-architecture/contracts/` do `CTR`
|
||||
(`--focus ctr`) sinh: `contracts/openapi/<module>.yaml#<operationId>` hoặc
|
||||
`contracts/asyncapi/<domain>.yaml#<channel>`. Ghi "dự kiến" ⇒ `OQ` + `Confidence` 🔴 và AG2 chặn.
|
||||
Interface đối tác chưa có tài liệu ⇒ ghi `ARISK`, không tự viết contract thay đối tác.
|
||||
|
||||
### 2.1 Interface với hệ thống ngoài
|
||||
|
||||
| ID | Hệ thống | Ai liên hệ được | **SLA của họ** | Giới hạn tốc độ | Cơ chế xác thực | Môi trường thử | Đã gọi thử chưa |
|
||||
@@ -87,8 +92,8 @@ diện không hiện lỗi. Chốt 4xx ngay ở đây.
|
||||
|
||||
**Contract**
|
||||
|
||||
Nguồn sự thật: `<đường dẫn file OpenAPI/AsyncAPI/proto>` — **không chép nội dung contract vào
|
||||
đây**, chỉ ghi những điểm cần chú ý:
|
||||
Nguồn sự thật: `contracts/openapi/<module>.yaml` · `operationId: <…>` (xem `CTR` §2) — **không
|
||||
chép nội dung contract vào đây**, chỉ ghi những điểm cần chú ý:
|
||||
|
||||
| Điểm cần chú ý | Quyết định |
|
||||
|---|---|
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
# PDM — Physical Data Model — <PROJECT>
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Version** | 1.0 |
|
||||
| **Date** | YYYY-MM-DD |
|
||||
| **Author** | <SA> (skill sa-2-architecture) |
|
||||
| **Status** | 🟡 Draft |
|
||||
| **Approved by** | Tech Lead: — · DBA: — · BE Lead: — |
|
||||
| **Source** | DAT_… v1.0 · DOM_… v1.0 · SRS_… §2.3.3 (bảng field) · BR_… của BA · SEC_… §5 |
|
||||
| **Scope** | *(kho nào — một PDM mỗi kho vật lý, hoặc một file nhiều §2.x)* |
|
||||
| **Confidence** | 🟡 |
|
||||
|
||||
## Change Log
|
||||
|
||||
| Version | Date | Người sửa | Thay đổi | ADR / MIG |
|
||||
|---|---|---|---|---|
|
||||
| 1.0 | | | Bản đầu | `MIG-001` |
|
||||
|
||||
> **Thẩm quyền:** file DDL trong `02-architecture/schema/` **thắng** bảng ở đây khi lệch — bảng là
|
||||
> bản đọc cho người, DDL là thứ chạy. Lệch ⇒ lỗi tài liệu, sửa bảng.
|
||||
>
|
||||
> **Vì sao có tài liệu này:** `DAT` dừng ở "thực thể, quan hệ, ai sở hữu". Không có `PDM`, mỗi dev
|
||||
> tự đặt kiểu cột, độ dài, nullable, index — và bảng field của BA (`FLD-…`) chặn ở tầng UI nhưng
|
||||
> không chặn ở tầng DB, hoặc ngược lại.
|
||||
|
||||
---
|
||||
|
||||
## 1. Kho và quy ước
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Kho** | *(tên · loại · phiên bản, ví dụ PostgreSQL 16 · RDS)* — quyết định ở `DAT` §3 / `ADR-nnn` |
|
||||
| **Schema / namespace** | *(schema-per-module theo `ADR-nnn`: `identity`, `catalog`, `cart_order`…)* |
|
||||
| **Công cụ migration** | *(Flyway / Liquibase / Prisma Migrate / Alembic / golang-migrate…)* — `ADR-nnn` |
|
||||
| **Đặt tên** | bảng `snake_case` số ít · PK `id` · FK `<bảng>_id` · index `ix_<bảng>_<cột>` · unique `ux_…` · FK constraint `fk_<bảng>_<cột>` |
|
||||
| **Kiểu id** | `uuid v7` / `bigint identity` — `ADR-nnn`; **truyền qua API dạng string** (`ICD` §1) |
|
||||
| **Thời gian** | `timestamptz`, lưu UTC; cột `created_at`, `updated_at` bắt buộc mọi bảng |
|
||||
| **Tiền** | `numeric(18,2)` + cột `currency char(3)` hoặc value object cố định VND — theo `DOM` §2 `Money` |
|
||||
| **Xoá** | soft delete (`deleted_at`) cho bảng có yêu cầu retention ở `DAT` §5; hard delete cho bảng còn lại — ghi từng bảng ở §2 |
|
||||
| **Ký tự** | `UTF-8`; độ dài `varchar(n)` tính theo **code point**, khớp bảng field của SRS (W3) |
|
||||
|
||||
---
|
||||
|
||||
## 2. Bảng — `TBL-nnn`
|
||||
|
||||
*Một mục mỗi bảng. Mỗi bảng phải truy về một thực thể `DAT` §1 và một class `DOM` §2; bảng không
|
||||
truy được là bảng thừa hoặc thiếu mô hình.*
|
||||
|
||||
### 2.1 `TBL-001` · `cart_order.order` ← `Order` (`DAT` §1.3 · `DOM` §2.1)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Chủ sở hữu** | `CMP-04` — chỉ component này được ghi (`DAT` §1) |
|
||||
| **Ước lượng** | … bản ghi/năm 1 · … năm 3 (`DAT` §2.2) · tăng …/tháng |
|
||||
| **Xoá** | soft delete · retention … năm (`DAT` §5) |
|
||||
| **Phân mảnh** | không / theo `placed_at` tháng (`DAT` §7.4 · `ADR-nnn`) |
|
||||
|
||||
**Cột**
|
||||
|
||||
| Cột | Kiểu | Null | Default | Ràng buộc | PII (`SEC` §5) | Nguồn (`FLD`/`BR`) | Ghi chú |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `id` | `uuid` | ✗ | `gen_uuid_v7()` | PK | — | — | |
|
||||
| `order_number` | `varchar(20)` | ✗ | — | `ux_order_order_number` | — | `BR-010` định dạng | khoá nghiệp vụ |
|
||||
| `customer_id` | `uuid` | ✓ | `NULL` | *(không FK vật lý — khác schema, `DAT` §1)* | 🔶 gián tiếp | `FLD-SCR04-01` | null = Guest |
|
||||
| `total_amount` | `numeric(18,2)` | ✗ | — | `CHECK (total_amount >= 0)` | — | `BR-014` | = Σ `order_seller.subtotal_amount` |
|
||||
| `status` | `varchar(32)` | ✗ | `'pending'` | `CHECK (status IN (…))` theo `BR` §3 | — | `BR-021` | enum ở tầng app, CHECK ở DB |
|
||||
| `idempotency_key` | `varchar(64)` | ✗ | — | `ux_order_idempotency_key` | — | `ADR-004` | khoá của `POST /v1/checkout` |
|
||||
| `placed_at` | `timestamptz` | ✗ | `now()` | | — | | |
|
||||
| `created_at` / `updated_at` | `timestamptz` | ✗ | `now()` | | — | quy ước §1 | |
|
||||
| `deleted_at` | `timestamptz` | ✓ | `NULL` | | — | quy ước §1 | soft delete |
|
||||
|
||||
🔴 **Bốn ô không được trống ở bất kỳ cột nào**: kiểu có độ dài/độ chính xác · Null · Default ·
|
||||
Nguồn. Trùng nguyên tắc "bảng ràng buộc cụ thể tới mức không cần hỏi lại" của SRS PART 2.
|
||||
|
||||
**Index và khoá ngoại**
|
||||
|
||||
| Tên | Loại | Cột | Vì sao (`QAS`/truy vấn `DAT` §7.1) | Kích cỡ ước lượng |
|
||||
|---|---|---|---|---|
|
||||
| `ix_order_customer_placed` | btree | `(customer_id, placed_at DESC)` | lịch sử đơn · `QAS-006` | |
|
||||
| `fk_order_seller_order` | FK | `order_seller.order_id → order.id` | `ON DELETE RESTRICT` | |
|
||||
|
||||
🔴 Index không truy về truy vấn nào ⇒ bỏ. Truy vấn trong `DAT` §7.1 không có index ⇒ thiếu.
|
||||
|
||||
### 2.2 `TBL-002` · `<schema>.<bảng>` ← `<Entity>`
|
||||
|
||||
*(cùng cấu trúc)*
|
||||
|
||||
---
|
||||
|
||||
## 3. Ánh xạ thực thể → bảng
|
||||
|
||||
*Bảng đối chiếu bắt buộc, `sa-conformance` đọc bảng này.*
|
||||
|
||||
| Thực thể (`DAT` §1) | Class (`DOM` §2) | Bảng (`TBL`) | Quan hệ 1 thực thể → n bảng? | Ghi chú |
|
||||
|---|---|---|---|---|
|
||||
| `Order` | `Order` | `TBL-001` | 1→1 | |
|
||||
| `Order` × `Seller` | `OrderSeller` | `TBL-002` | tách theo `ADR-003` | |
|
||||
| `Cart` (Redis) | `Cart` | *(không phải bảng — key schema §5)* | | |
|
||||
|
||||
Thực thể không có dòng ⇒ 🔴 chặn AG2. Bảng không có thực thể ⇒ bảng thừa hoặc thiếu mô hình.
|
||||
|
||||
---
|
||||
|
||||
## 4. DDL và migration — `MIG-nnn`
|
||||
|
||||
**Vị trí:** `sa-output/<PROJECT>/02-architecture/schema/<kho>/`
|
||||
|
||||
```
|
||||
schema/<kho>/
|
||||
├── V001__init_<schema>.sql ← MIG-001: toàn bộ bảng §2, chạy được từ CSDL rỗng
|
||||
├── V001__init_<schema>.down.sql ← rollback tương ứng (bắt buộc)
|
||||
├── V002__<thay-doi>.sql ← mỗi thay đổi sau baseline một cặp up/down
|
||||
├── V002__<thay-doi>.down.sql
|
||||
└── seed/
|
||||
└── R__reference_data.sql ← dữ liệu tham chiếu (enum bảng, cấu hình) — idempotent
|
||||
```
|
||||
|
||||
| `MIG` | File | Nội dung | Rollback | Dữ liệu phát sinh trong lúc chạy | Thời lượng ước lượng | Chạy trên |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `MIG-001` | `V001__init_cart_order.sql` | tạo `TBL-001..0nn` | `V001__….down.sql` drop ngược thứ tự FK | không (CSDL rỗng) | < 1 phút | dev/stg/prod |
|
||||
| `MIG-002` | | | | | | |
|
||||
|
||||
🔴 **Migration không có down là migration một chiều** (`DAT` §8). Với migration đổi dữ liệu
|
||||
(không chỉ DDL) phải ghi thêm: cách đối chiếu sau khi chạy, ngưỡng chênh lệch chấp nhận, ai duyệt.
|
||||
|
||||
**Quy ước file SQL:** một câu lệnh một dòng logic · comment đầu file ghi `MIG-nnn`, `TBL` liên quan,
|
||||
`ADR` · không dùng lệnh phụ thuộc quyền superuser · `CREATE … IF NOT EXISTS` chỉ trong seed.
|
||||
|
||||
**Kiểm bằng máy** *(người điều phối chạy, ghi kết quả vào đây)*:
|
||||
|
||||
| Kiểm | Lệnh | Kết quả |
|
||||
|---|---|---|
|
||||
| Up từ rỗng | `<tool> migrate` trên container CSDL rỗng | ☐ |
|
||||
| Down toàn bộ rồi up lại | `<tool> migrate down --all && <tool> migrate` | ☐ |
|
||||
| Mọi bảng §2 có trong DDL | `grep -c "CREATE TABLE" V001*.sql` = số `TBL` | ☐ |
|
||||
| Mọi cột PII có cờ ở `SEC` §5 | đối chiếu cột 🔶/✅ PII với `SEC` §5 | ☐ |
|
||||
|
||||
---
|
||||
|
||||
## 5. Kho không quan hệ *(nếu có — cache, document, search)*
|
||||
|
||||
| Kho | Key / collection | Cấu trúc value | TTL | Invalidation (`DAT` §7.4) | Kích cỡ ước lượng |
|
||||
|---|---|---|---|---|---|
|
||||
| Redis `CMP-15` | `cart:{sessionId}` | JSON `CartSnapshot` (`DOM` §2) | 7 ngày | write-through khi `PATCH`/`DELETE` | ≤ 50 dòng × … |
|
||||
|
||||
---
|
||||
|
||||
## 6. Dữ liệu tham chiếu và seed
|
||||
|
||||
| Bảng | Nguồn dữ liệu | Ai duy trì | Cách nạp | Idempotent |
|
||||
|---|---|---|---|---|
|
||||
|
||||
---
|
||||
|
||||
## 7. Đối chiếu
|
||||
|
||||
| Kiểm | Kết quả | Hành động |
|
||||
|---|---|---|
|
||||
| `DAT` thực thể → `TBL` (§3) | n/n | |
|
||||
| `FLD-*` trong SRS PART 2 → cột (kiểu/độ dài/null khớp) | n/n | lệch ⇒ `OQ`, BA cập nhật SRS hoặc PDM sửa |
|
||||
| Cột PII → `SEC` §5 có dòng | n/n | |
|
||||
| Truy vấn `DAT` §7.1 → index | n/n | |
|
||||
| Mọi `MIG` có down | n/n | |
|
||||
|
||||
## 8. Giả định & Ngoài phạm vi
|
||||
|
||||
**Giả định:**
|
||||
|
||||
| ID | Giả định | Cách xác minh | Nếu sai |
|
||||
|---|---|---|---|
|
||||
|
||||
**Ngoài phạm vi:**
|
||||
|
||||
- *(ví dụ: không thiết kế kho phân tích/BI — ngoài `CON-…`)*
|
||||
|
||||
## 9. Open Questions
|
||||
|
||||
| ID | Câu hỏi | Hỏi ai | Từ ngày | Chặn gì | Hệ quả nếu trả lời ngược |
|
||||
|---|---|---|---|---|---|
|
||||
@@ -53,6 +53,7 @@ Tách service là quyết định phải *thắng* một lập luận, không ph
|
||||
|
||||
*Ai dùng hệ thống, hệ thống nói chuyện với cái gì bên ngoài. Không có chi tiết bên trong.*
|
||||
|
||||
<!-- archify: architecture · diagrams/SAD_c4-context.architecture.json -->
|
||||
```mermaid
|
||||
flowchart LR
|
||||
```
|
||||
@@ -70,6 +71,7 @@ flowchart LR
|
||||
*Các khối chạy được và triển khai được (ứng dụng, CSDL, hàng đợi, job). Đây là sơ đồ dev dùng
|
||||
nhiều nhất.*
|
||||
|
||||
<!-- archify: architecture · diagrams/SAD_c4-container.architecture.json -->
|
||||
```mermaid
|
||||
flowchart TB
|
||||
```
|
||||
@@ -107,6 +109,7 @@ Một loại thay đổi thường xuyên mà chạm ≥ 3 container ⇒ phân r
|
||||
|
||||
*Không vẽ mức này cho mọi container. Vẽ cho cái khó nhất, để dev có mẫu.*
|
||||
|
||||
<!-- archify: architecture · diagrams/SAD_c4-component.architecture.json -->
|
||||
```mermaid
|
||||
flowchart TB
|
||||
```
|
||||
@@ -120,6 +123,7 @@ timeout ở đâu, giao dịch bắt đầu và kết thúc ở đâu.*
|
||||
|
||||
### 6.1 <tên luồng>
|
||||
|
||||
<!-- archify: sequence · diagrams/SAD_luong-chinh.sequence.json -->
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
```
|
||||
@@ -133,6 +137,7 @@ sequenceDiagram
|
||||
|
||||
*Cái gì chạy ở đâu, mấy bản, ranh giới mạng, ranh giới tin cậy.*
|
||||
|
||||
<!-- archify: architecture · diagrams/SAD_deployment.architecture.json -->
|
||||
```mermaid
|
||||
flowchart TB
|
||||
```
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
*Nơi dữ liệu đi từ vùng ít tin cậy sang vùng tin cậy hơn. Mỗi ranh giới phải có kiểm tra đầu vào.*
|
||||
|
||||
<!-- archify: architecture · diagrams/SEC_ranh-gioi-tin-cay.architecture.json -->
|
||||
```mermaid
|
||||
flowchart LR
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user