# Schema dữ liệu — Giai đoạn 1 (trên nền OpenGate)

> Cập nhật: 06/7/2026 · Thuộc [Kế hoạch giai đoạn 1](README.md) · Bản trực quan: [schema-du-lieu.html](schema-du-lieu.html)
>
> **Chưa đưa lên roadmap** — bản kỹ thuật nội bộ. **Đã chỉnh lại theo nền tảng OpenGate** (repo `controlplane` + `ws_suns`).
>
> ⚠️ Bản trước (SQL Server + `tenant_id`) đã bị thay. Nền tảng thực tế: **PostgreSQL, mỗi workspace một DB riêng, RBAC/SSO/users do Control Plane lo, domain là các module `.ogapp`.**

---

## 0. Nền tảng quyết định schema

| Vấn đề | Cách OpenGate xử lý | Hệ quả cho schema |
|---|---|---|
| Multi-tenant | **Mỗi trung tâm = 1 workspace = 1 DB riêng** (Level 2, cô lập) | **Bỏ `tenant_id`** khỏi mọi bảng nghiệp vụ |
| Users / đăng nhập / SSO | Control Plane (`opengate_global`): User, Membership, SSO OAuth2 | Không tự làm bảng user/session |
| Phân quyền (RBAC) | CP: Role/RoleGroup + Permission catalog; module khai `permissions[]` trong `manifest.json` | Không tự làm bảng role/permission (xem [WS0](WS0-co-cau-to-chuc-va-phan-quyen-2026-07-06.md)) |
| Domain nghiệp vụ | Mỗi tính năng = **module** (`student`, `course`, `teacher`, `vehicle`, `dat`, `rfcard`, `catalog`…) | Bảng **prefix theo key**, migrations SQL riêng từng module |
| Danh mục quy định | Module **`catalog`** (hạng GPLX, hạng đào tạo, trình độ, môn dạy) — đã seed theo quy định mới | Regulation-as-config **đã có sẵn** |

**Chuẩn kỹ thuật (theo module đang có):** PostgreSQL · `BIGSERIAL/SERIAL/BIGINT` PK · `TIMESTAMPTZ` · `BOOLEAN` · tên cột **snake_case tiếng Việt theo legacy** (`nguoi_lxid`, `ma_dk`, `so_cmt`, `ma_csdt`…) · migration idempotent `CREATE TABLE IF NOT EXISTS` · `tableName` prefix `<key>_`.

> **`ma_csdt`** (mã cơ sở đào tạo) giữ lại trong nhiều bảng dù đã cô lập theo workspace — vì là **thuộc tính nghiệp vụ** để báo cáo Sở & tương thích legacy, không phải cơ chế tenant.

---

## 1. Module đã có (port từ QLHS V2 / `dat_web_db`)

Đây là hiện trạng trong `ws_suns/modules/` — **không phải thiết kế mới**, mà là schema thật đã port.

### `student` — Học viên · bảng `student_nguoi_lx` (BIGSERIAL `nguoi_lxid`)
Record phẳng theo legacy: `ma_dk, ho_va_ten, ngay_sinh, gioi_tinh, so_cmt, ngay_cap_cmt, so_dien_thoai, noi_ct, noi_tt, hang_dao_tao, hang_gplx, khoa_hoc_id, ma_kh, ma_csdt, giao_vien_id, ma_xe_tap, rfid, is_face_id, is_finger, is_rfid, is_dao_tao, ket_qua_hoc_ly_thuyet, so_gio_hoc_ly_thuyet, anh_chup, is_active`. UNIQUE `(ma_dk, ma_kh, ma_csdt)`.

### `course` — Khóa học · `course_khoa_hoc` (`khoa_hoc_id`)
`ma_kh, ten_kh, hang_dao_tao, ma_csdt, ngay_bg, ngay_kg, ngay_dao_tao, ngay_sat_hach, dao_tao_noi_bo, is_active`.

### `teacher` — Giáo viên · `teacher_giao_vien` (`giao_vien_id`)
`ma_gv, ten_day_du, ngay_sinh, so_cmt, so_dien_thoai, dia_chi, hang_gplx, ma_cndt, ma_csdt, ma_the_rfid, is_rfid, anh_chup, is_active`. UNIQUE `(ma_gv, ma_csdt)`.

### `vehicle` — Xe tập lái · `vehicle_xe_tap_lai` (`xe_tap_lai_id`)
`ma_xe, bien_so, chu_so_huu, hang_gplx, ma_csdt, so_giay_phep, ngay_cap_gp, ngay_het_han_gp, dat_device_id, seri_dat, is_active`. UNIQUE `(ma_xe, ma_csdt)`.

### `dat` — Thiết bị DAT · `dat_device` + `dat_log`
`dat_device`: `seri_number, sim_number, device_type, handover_date, expiry_date, status`. · `dat_log`: `payload, server_message, status, sub_status, log_type, created_at`.

### `rfcard` — Thẻ RFID · `rfcard_the_rf` (`rfid`)
`ma_the, so_the, ghi_chu, ma_csdt, ngay_tao, is_active`.

### `catalog` — Danh mục dùng chung (regulation-config)
`catalog_hang_gplx` (đã seed B/B1/B2/C/C1/D/D1/A1/A2… theo quy định — có `han_su_dung`, `mo_ta_vn`), `catalog_trinh_do_hoc_van`, `catalog_hang_dao_tao`, `catalog_teaching-subject`. Hầu hết module `depends: ["catalog@>=1.0"]`.

---

## 2. Thiết kế đề xuất cho quy trình hồ sơ (WS1) — hướng LAI

> Đề xuất của tôi (bạn điều chỉnh sau). Nguyên tắc: **giữ nguyên `student_nguoi_lx` để migrate 1:1**, bổ sung lớp **trạng thái + giấy tờ + audit** mà không phá cấu trúc legacy. Bổ sung nằm trong **module `student`** (bản `1.2.0`), tên cột tiếng Việt cho đồng bộ.

### 2.1 Thêm cột vào `student_nguoi_lx`
```sql
ALTER TABLE student_nguoi_lx
  ADD COLUMN IF NOT EXISTS trang_thai_ho_so   VARCHAR(20) NOT NULL DEFAULT 'tiep_nhan',
      -- 'tu_van'|'tiep_nhan'|'cho_bo_sung'|'hop_le'|'da_thu_dot1'
      -- |'xep_khoa'|'da_bao_so'|'dang_hoc'|'huy'|'chuyen_khoa'
  ADD COLUMN IF NOT EXISTS loai_ho_so         VARCHAR(10) NOT NULL DEFAULT 'moi', -- 'moi'|'nang_hang'
  ADD COLUMN IF NOT EXISTS phong_phu_trach_id INTEGER,       -- phòng đang giữ hồ sơ (nếu mô hình phòng ban)
  ADD COLUMN IF NOT EXISTS hoc_phi_thoa_thuan NUMERIC(15,2);
```

### 2.2 `student_ho_so_lichsu` — lịch sử trạng thái & bàn giao (feeds audit)
```sql
CREATE TABLE IF NOT EXISTS student_ho_so_lichsu (
  id            BIGSERIAL   PRIMARY KEY,
  nguoi_lxid    BIGINT      NOT NULL REFERENCES student_nguoi_lx(nguoi_lxid),
  trang_thai_cu  VARCHAR(20),
  trang_thai_moi VARCHAR(20) NOT NULL,
  phong_tu_id    INTEGER,
  phong_den_id   INTEGER,
  nguoi_thao_tac VARCHAR(100),     -- user id/username từ ctx.user (CP)
  ly_do          VARCHAR(500),
  thoi_gian      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_hoso_lichsu_nlx ON student_ho_so_lichsu (nguoi_lxid);
```

### 2.3 `student_ho_so_giayto` — giấy tờ nộp (đối chiếu checklist)
```sql
CREATE TABLE IF NOT EXISTS student_ho_so_giayto (
  id             BIGSERIAL   PRIMARY KEY,
  nguoi_lxid     BIGINT      NOT NULL REFERENCES student_nguoi_lx(nguoi_lxid),
  loai_giay_to   VARCHAR(50) NOT NULL,   -- ref catalog_loai_giay_to.ma
  file_url       VARCHAR(510),
  trang_thai     VARCHAR(20) NOT NULL DEFAULT 'da_nop', -- 'da_nop'|'da_duyet'|'thieu'
  ngay_het_han   TIMESTAMPTZ,            -- giấy khám SK có hạn
  nguoi_duyet    VARCHAR(100),
  thoi_gian_duyet TIMESTAMPTZ,
  ghi_chu        VARCHAR(500)
);
CREATE INDEX IF NOT EXISTS idx_hoso_giayto_nlx ON student_ho_so_giayto (nguoi_lxid);
```

### 2.4 `student_hoc_phi_dot` — công nợ theo đợt (tạm ở student; sẽ chuyển module kế toán ở v1.5)
```sql
CREATE TABLE IF NOT EXISTS student_hoc_phi_dot (
  id           BIGSERIAL   PRIMARY KEY,
  nguoi_lxid   BIGINT      NOT NULL REFERENCES student_nguoi_lx(nguoi_lxid),
  dot          INTEGER     NOT NULL,    -- đợt 1,2,3
  so_tien      NUMERIC(15,2) NOT NULL,
  han_dong     TIMESTAMPTZ,
  da_dong      NUMERIC(15,2) NOT NULL DEFAULT 0,
  ngay_dong    TIMESTAMPTZ,
  trang_thai   VARCHAR(20) NOT NULL DEFAULT 'chua_dong', -- 'chua_dong'|'dong_mot_phan'|'da_dong'
  so_hoa_don   VARCHAR(50),
  UNIQUE (nguoi_lxid, dot)
);
```

### 2.5 Checklist giấy tờ động theo hạng — bổ sung vào `catalog`
```sql
CREATE TABLE IF NOT EXISTS catalog_loai_giay_to (
  id     SERIAL PRIMARY KEY,
  ma     VARCHAR(50) NOT NULL UNIQUE,  -- 'don_dk','cccd','giay_kham_sk','anh_the','gplx_cu'
  ten    VARCHAR(255) NOT NULL,
  co_han BOOLEAN NOT NULL DEFAULT FALSE
);
CREATE TABLE IF NOT EXISTS catalog_hang_giay_to (   -- checklist theo hạng
  id            SERIAL PRIMARY KEY,
  hang_gplx_id  INTEGER NOT NULL REFERENCES catalog_hang_gplx(id),
  loai_giay_to_id INTEGER NOT NULL REFERENCES catalog_loai_giay_to(id),
  ap_dung       VARCHAR(10) NOT NULL DEFAULT 'ca_hai', -- 'moi'|'nang_hang'|'ca_hai'
  bat_buoc      BOOLEAN NOT NULL DEFAULT TRUE,
  UNIQUE (hang_gplx_id, loai_giay_to_id, ap_dung)
);
```

**Ánh xạ trạng thái ↔ quy trình WS1:** `tiep_nhan`→B2 · `cho_bo_sung`→thiếu giấy · `hop_le`→B2 xong · `da_thu_dot1`→B4 · `xep_khoa`→B6 · `da_bao_so`→B7 · `dang_hoc`→B8.

---

## 3. Audit log — điểm cần chốt

Các module hiện **chưa có bảng audit riêng**. Dữ liệu học tập là căn cứ pháp lý → cần audit bất biến. Ba phương án:

1. **Runtime/base lo tập trung** — nếu `runtime-core`/`opengate-app-base` đã có cơ chế audit chung thì dùng lại (⚠️ **cần verify** — chưa kiểm tra code base/runtime-core).
2. **Bảng audit trong từng module** — vd `student_audit_log` append-only, chặn UPDATE/DELETE ở tầng quyền DB.
3. **Kết hợp:** dùng `student_ho_so_lichsu` (mục 2.2) cho lịch sử trạng thái hồ sơ (đủ cho compliance WS1) + audit chung ở nền tảng cho các thao tác khác.

→ **Đề xuất:** dùng (3) cho MVP — `student_ho_so_lichsu` đáp ứng yêu cầu truy vết hồ sơ ngay; kiểm tra base/runtime-core để quyết audit toàn cục sau.

---

## 4. Quan hệ (trong 1 workspace-DB)

```
[catalog_hang_gplx] ──ref── student_nguoi_lx.hang_gplx / .hang_dao_tao / course_khoa_hoc
[catalog_loai_giay_to] ─* catalog_hang_giay_to *─ [catalog_hang_gplx]     (checklist động)

student_nguoi_lx (hồ sơ = record)
   ├─ khoa_hoc_id  ─→ course_khoa_hoc
   ├─ giao_vien_id ─→ teacher_giao_vien
   ├─ ma_xe_tap    ─→ vehicle_xe_tap_lai
   ├─ rfid         ─→ rfcard_the_rf
   ├─* student_ho_so_lichsu      (trạng thái/bàn giao)
   ├─* student_ho_so_giayto      (giấy tờ)  *─ catalog_loai_giay_to
   └─* student_hoc_phi_dot       (công nợ đợt)

vehicle_xe_tap_lai.dat_device_id ─→ dat_device      dat_device ─* dat_log
```
> Liên kết chéo module dùng **khóa nghiệp vụ** (`ma_kh`, `giao_vien_id`, `ma_xe_tap`, `rfid`) chứ không FK cứng xuyên module — vì mỗi module cài/gỡ độc lập.

---

## 5. Migrate từ QLHS V2 (SQL Server) → module Postgres

- Nguồn: DB SQL Server **QLHS V2** của Vĩnh An (việc #2). Schema đích **đã bám tên cột legacy** → ánh xạ gần **1:1** cho `student_nguoi_lx`, `course_khoa_hoc`, `teacher_giao_vien`, `vehicle_xe_tap_lai`, `rfcard_the_rf`, danh mục.
- Việc chính: (1) chuyển kiểu SQL Server → Postgres, (2) đối chiếu `ma_csdt`/`ma_kh`/`ma_dk`, (3) đổ dữ liệu vào DB workspace của trung tâm, (4) sinh trạng thái hồ sơ (`trang_thai_ho_so`) từ cờ legacy (`is_dao_tao`, `ket_qua_hoc_ly_thuyet`, `is_active`).
- Chạy song song đối chiếu 1–2 khóa (WS3) trước khi cắt hệ cũ.

## 6. Cần chốt với Vĩnh An
1. Quy tắc `ma_dk` / `ma_kh` / `ma_csdt` hiện dùng → giữ tương thích khi migrate.
2. Học phí chia mấy đợt, mốc nào → `student_hoc_phi_dot`.
3. Bộ giấy tờ thực tế theo hạng → seed `catalog_loai_giay_to` + `catalog_hang_giay_to`.
4. `runtime-core`/`base` đã có audit chung chưa → chốt phương án mục 3.
</content>
