279 lines
6.3 KiB
Markdown
279 lines
6.3 KiB
Markdown
# Hybrid Role Architecture - Hướng dẫn sử dụng
|
|
|
|
## 📋 Tổng quan
|
|
|
|
Hệ thống sử dụng **Hybrid Role Architecture** để tối ưu hiệu năng:
|
|
|
|
- **80-90% users (học sinh, phụ huynh)**: Dùng `primary_role_info` trong `UserProfile` → **NHANH** (2 JOINs)
|
|
- **10-20% users (giáo viên, quản lý)**: Dùng `UserAssignment` → **Linh hoạt** (5 JOINs)
|
|
|
|
## 🗂️ Cấu trúc dữ liệu
|
|
|
|
### UserProfile.primary_role_info (JSON)
|
|
|
|
```json
|
|
{
|
|
"role_id": "uuid",
|
|
"role_code": "student",
|
|
"role_name": "Học sinh",
|
|
"school": {
|
|
"id": "uuid",
|
|
"name": "SENA Hà Nội"
|
|
},
|
|
"class": {
|
|
"id": "uuid",
|
|
"name": "K1A"
|
|
},
|
|
"grade": {
|
|
"id": "uuid",
|
|
"name": "Khối 1"
|
|
},
|
|
"student_code": "HS001",
|
|
"enrollment_date": "2024-09-01",
|
|
"status": "active"
|
|
}
|
|
```
|
|
|
|
## 🚀 Migration & Setup
|
|
|
|
### 1. Thêm column vào database
|
|
|
|
```bash
|
|
node scripts/add-primary-role-info.js
|
|
```
|
|
|
|
### 2. Populate dữ liệu cho học sinh hiện có
|
|
|
|
```bash
|
|
node scripts/populate-primary-role-info.js
|
|
```
|
|
|
|
### 3. Kiểm tra kết quả
|
|
|
|
```sql
|
|
SELECT
|
|
user_id,
|
|
full_name,
|
|
JSON_EXTRACT(primary_role_info, '$.role_code') as role,
|
|
JSON_EXTRACT(primary_role_info, '$.school.name') as school
|
|
FROM user_profiles
|
|
WHERE primary_role_info IS NOT NULL
|
|
LIMIT 10;
|
|
```
|
|
|
|
## 📝 API Response mới
|
|
|
|
### Login Response
|
|
|
|
```json
|
|
{
|
|
"success": true,
|
|
"message": "Đăng nhập thành công",
|
|
"data": {
|
|
"token": "eyJhbGc...",
|
|
"user": {
|
|
"id": "uuid",
|
|
"username": "student001",
|
|
"email": "student@example.com",
|
|
"profile": {
|
|
"full_name": "Nguyễn Văn A",
|
|
"avatar_url": "https://...",
|
|
"primary_role_info": {
|
|
"role_code": "student",
|
|
"role_name": "Học sinh",
|
|
"school": { "id": "uuid", "name": "SENA HN" },
|
|
"class": { "id": "uuid", "name": "K1A" }
|
|
}
|
|
},
|
|
"role": {
|
|
"role_id": "uuid",
|
|
"role_code": "student",
|
|
"role_name": "Học sinh",
|
|
"school": { "id": "uuid", "name": "SENA HN" },
|
|
"class": { "id": "uuid", "name": "K1A" }
|
|
},
|
|
"permissions": [
|
|
{
|
|
"code": "view_own_grades",
|
|
"name": "Xem điểm của mình",
|
|
"resource": "grades",
|
|
"action": "view"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## 🔧 Khi nào dùng cái nào?
|
|
|
|
### ✅ Dùng `primary_role_info` (Fast Path)
|
|
|
|
- Học sinh (`student`)
|
|
- Phụ huynh thường (`parent`)
|
|
- User có 1 role cố định
|
|
- Không thay đổi school/class thường xuyên
|
|
|
|
**Cách tạo:**
|
|
```javascript
|
|
await UserProfile.create({
|
|
user_id: userId,
|
|
full_name: "Nguyễn Văn A",
|
|
primary_role_info: {
|
|
role_id: studentRoleId,
|
|
role_code: "student",
|
|
role_name: "Học sinh",
|
|
school: { id: schoolId, name: "SENA HN" },
|
|
class: { id: classId, name: "K1A" },
|
|
}
|
|
});
|
|
```
|
|
|
|
### ✅ Dùng `UserAssignment` (Flexible Path)
|
|
|
|
- Giáo viên dạy nhiều trường
|
|
- Quản lý nhiều center
|
|
- User có nhiều role đồng thời
|
|
- Cần theo dõi thời gian hiệu lực role
|
|
|
|
**Cách tạo:**
|
|
```javascript
|
|
// Set primary_role_info = null
|
|
await UserProfile.create({
|
|
user_id: userId,
|
|
full_name: "Nguyễn Thị B",
|
|
primary_role_info: null, // ← Để null
|
|
});
|
|
|
|
// Tạo assignments
|
|
await UserAssignment.bulkCreate([
|
|
{
|
|
user_id: userId,
|
|
role_id: teacherRoleId,
|
|
school_id: school1Id,
|
|
is_primary: true,
|
|
},
|
|
{
|
|
user_id: userId,
|
|
role_id: teacherRoleId,
|
|
school_id: school2Id,
|
|
is_primary: false,
|
|
}
|
|
]);
|
|
```
|
|
|
|
## ⚡ Performance
|
|
|
|
### Before (Tất cả user dùng UserAssignment)
|
|
- Login query: **6 JOINs**
|
|
- Avg response time: **50-80ms** (without cache)
|
|
|
|
### After (Hybrid approach)
|
|
- **Student login**: **2 JOINs** → **20-30ms** ✅ (-60%)
|
|
- **Teacher login**: **5 JOINs** → **50-70ms** (tương tự)
|
|
|
|
### Với Redis Cache
|
|
- **Student**: **< 1ms** (cache hit)
|
|
- **Teacher**: **< 2ms** (cache hit)
|
|
|
|
## 🔄 Cập nhật primary_role_info
|
|
|
|
Khi học sinh chuyển lớp hoặc thay đổi thông tin:
|
|
|
|
```javascript
|
|
// Option 1: Update trực tiếp
|
|
await userProfile.update({
|
|
primary_role_info: {
|
|
...userProfile.primary_role_info,
|
|
class: { id: newClassId, name: "K2A" }
|
|
}
|
|
});
|
|
|
|
// Option 2: Rebuild từ StudentDetail
|
|
const student = await StudentDetail.findOne({
|
|
where: { user_id: userId },
|
|
include: [
|
|
{ model: Class, as: 'currentClass', include: [School] },
|
|
]
|
|
});
|
|
|
|
await userProfile.update({
|
|
primary_role_info: {
|
|
role_id: studentRole.id,
|
|
role_code: 'student',
|
|
role_name: 'Học sinh',
|
|
school: {
|
|
id: student.currentClass.school.id,
|
|
name: student.currentClass.school.school_name
|
|
},
|
|
class: {
|
|
id: student.currentClass.id,
|
|
name: student.currentClass.class_name
|
|
}
|
|
}
|
|
});
|
|
|
|
// Don't forget to invalidate cache!
|
|
await redis.del(`user:${userId}:permissions`);
|
|
```
|
|
|
|
## 🎯 Best Practices
|
|
|
|
1. **Luôn check `primary_role_info` trước** khi query `UserAssignment`
|
|
2. **Cache permissions** trong Redis với TTL 1-24h
|
|
3. **Invalidate cache** khi update role/permission
|
|
4. **Monitor slow queries** để phát hiện N+1 query
|
|
5. **Index properly**: `user_id`, `role_id`, `school_id` trong `user_assignments`
|
|
|
|
## 📊 Monitoring
|
|
|
|
```sql
|
|
-- Thống kê users theo role strategy
|
|
SELECT
|
|
CASE
|
|
WHEN primary_role_info IS NOT NULL THEN 'Fast Path'
|
|
ELSE 'Flexible Path'
|
|
END as strategy,
|
|
COUNT(*) as count,
|
|
ROUND(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM user_profiles), 2) as percentage
|
|
FROM user_profiles
|
|
GROUP BY strategy;
|
|
|
|
-- Expected result:
|
|
-- Fast Path: 80-90% (students, parents)
|
|
-- Flexible Path: 10-20% (teachers, managers)
|
|
```
|
|
|
|
## 🔐 Security
|
|
|
|
- JWT token bây giờ bao gồm `roleCode` để validate nhanh
|
|
- Permissions vẫn được check từ database (không tin JWT hoàn toàn)
|
|
- Rate limiting theo role: student (100 req/min), teacher (200 req/min)
|
|
|
|
## 🐛 Troubleshooting
|
|
|
|
### User không có role sau login?
|
|
|
|
```javascript
|
|
// Check primary_role_info
|
|
const profile = await UserProfile.findOne({ where: { user_id } });
|
|
console.log(profile.primary_role_info);
|
|
|
|
// Check UserAssignment
|
|
const assignments = await UserAssignment.findAll({
|
|
where: { user_id, is_active: true }
|
|
});
|
|
console.log(assignments);
|
|
```
|
|
|
|
### Query chậm?
|
|
|
|
1. Check indexes: `SHOW INDEX FROM user_profiles;`
|
|
2. Check Redis cache hit rate
|
|
3. Enable query logging: `SET GLOBAL general_log = 'ON';`
|
|
|
|
---
|
|
|
|
**Last updated:** January 19, 2026
|
|
**Version:** 2.0.0
|