Thuộc Thiết Kế CSDL — Nhóm Tổ chức & User
Chi Tiết Entity — Tổ Chức & User¶
Bảng:
organization_unit,app_user,user_role
organization_unit¶
CREATE TABLE organization_unit (
id BIGSERIAL PRIMARY KEY,
public_id UUID NOT NULL DEFAULT uuidv7(), -- ID public cho API/external integration
code TEXT UNIQUE NOT NULL, -- IDxx.xx.xx (VD: ID01.01.01). Bất biến
name TEXT NOT NULL, -- Tên đơn vị (VD: Phòng CS QLHC về TTXH)
level TEXT NOT NULL
CHECK (level IN ('TINH', 'SO', 'BAN', 'PHONG')),
parent_id BIGINT REFERENCES organization_unit(id), -- Cây phân cấp (self-reference)
successor_id BIGINT REFERENCES organization_unit(id), -- Đơn vị kế thừa (khi sáp nhập/chia tách)
contact_info TEXT, -- Địa chỉ, điện thoại
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_ou_public_id ON organization_unit (public_id);
CREATE INDEX idx_ou_parent ON organization_unit (parent_id);
CREATE INDEX idx_ou_level ON organization_unit (level);
CREATE INDEX idx_ou_active ON organization_unit (is_active) WHERE is_active = TRUE;
Business Rules:
- Cây phân cấp: TINH → SO → BAN → PHONG
- parent_id = NULL khi cấp TINH (gốc cây)
- code bất biến sau khi tạo
- Không xóa đơn vị nếu có đơn vị con, user, hoặc đang là chủ quản DE → dùng is_active = false
- DE chỉ gán chủ quản ở cấp PHONG trở xuống — không gán cấp TINH hay SO
app_user¶
CREATE TABLE app_user (
id BIGSERIAL PRIMARY KEY,
public_id UUID NOT NULL DEFAULT uuidv7(), -- ID public cho API/external integration
email TEXT UNIQUE NOT NULL, -- SSO mapping key
full_name TEXT NOT NULL,
organization_unit_id BIGINT NOT NULL REFERENCES organization_unit(id),
status TEXT NOT NULL DEFAULT 'ACTIVE'
CHECK (status IN ('ACTIVE', 'INACTIVE', 'PENDING')),
sso_linked BOOLEAN NOT NULL DEFAULT FALSE, -- Đã liên kết SSO chưa
keycloak_subject_id TEXT, -- Keycloak 'sub' claim (sau SSO link)
last_login_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE UNIQUE INDEX idx_user_public_id ON app_user (public_id);
CREATE INDEX idx_user_org ON app_user (organization_unit_id);
CREATE INDEX idx_user_status ON app_user (status);
CREATE UNIQUE INDEX idx_user_keycloak_unique ON app_user (keycloak_subject_id) WHERE keycloak_subject_id IS NOT NULL;
Business Rules:
- Email là key mapping duy nhất với SSO identity
- Pre-provisioning: Admin tạo user trước → user đăng nhập SSO lần đầu → hệ thống mapping bằng email
- status = PENDING cho user chưa được gán vai trò (tạo tự động khi SSO login lần đầu mà chưa có user)
- Admin không thể xóa/deactivate chính mình
- Hệ thống không lưu mật khẩu — Keycloak quản lý
user_role¶
CREATE TABLE user_role (
id BIGSERIAL PRIMARY KEY,
public_id UUID NOT NULL DEFAULT uuidv7(), -- ID public cho API/external integration
user_id BIGINT NOT NULL REFERENCES app_user(id) ON DELETE CASCADE,
role TEXT NOT NULL
CHECK (role IN ('MANAGER', 'DATA_OWNER', 'APPROVER', 'STAFF', 'ADMIN')),
assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
assigned_by BIGINT REFERENCES app_user(id),
UNIQUE (user_id, role) -- Mỗi user chỉ có 1 instance mỗi vai trò
);
CREATE UNIQUE INDEX idx_ur_public_id ON user_role (public_id);
CREATE INDEX idx_ur_user ON user_role (user_id);
CREATE INDEX idx_ur_role ON user_role (role);
Business Rules: - Một user có thể có nhiều vai trò (multi-role) - User chuyển đổi vai trò active qua sidebar (không cần đăng xuất) - Ít nhất 1 vai trò phải được gán - Thay đổi vai trò ghi audit log
delegation¶
Bảng quản lý Ủy quyền (Delegation) tạm thời các vai trò giữa các User. Bảng sử dụng UUID để chống lại Insecure Direct Object Reference (IDOR).
CREATE TABLE delegation (
id UUID PRIMARY KEY DEFAULT uuidv7(),
public_id UUID NOT NULL DEFAULT uuidv7(), -- ID public cho API/external integration
delegator_id BIGINT NOT NULL REFERENCES app_user(id), -- Người ủy quyền
delegatee_id BIGINT NOT NULL REFERENCES app_user(id), -- Người được ủy quyền
role TEXT NOT NULL
CHECK (role IN ('MANAGER', 'DATA_OWNER', 'APPROVER')),
start_time TIMESTAMPTZ NOT NULL,
end_time TIMESTAMPTZ NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT chk_time_range CHECK (end_time > start_time),
-- Cần extension btree_gist để EXCLUDE hoạt động với BIGINT/TEXT + range
CONSTRAINT ex_del_no_overlap EXCLUDE USING GIST (
delegator_id WITH =,
delegatee_id WITH =,
role WITH =,
tstzrange(start_time, end_time, '[)') WITH &&
) WHERE (is_active = TRUE)
);
CREATE UNIQUE INDEX idx_del_public_id ON delegation (public_id);
CREATE INDEX idx_del_delegator ON delegation (delegator_id);
CREATE INDEX idx_del_delegatee ON delegation (delegatee_id);
CREATE INDEX idx_del_time ON delegation (start_time, end_time);
Business Rules: - Delegator chỉ uỷ quyền được những Role mà họ đang sở hữu tại thời điểm tạo. - Cùng lúc không thể có 2 bản record uỷ quyền TRÙNG role và GIAO NHAU về mặt thời gian giữa cùng 1 cặp user. - UUIDv7 hoặc Random UUID đảm bảo link duyệt uỷ quyền (nếu có) gửi qua email không bị đoán.
Cập nhật lần cuối: 2026-04-11