update
This commit is contained in:
278
HYBRID_ROLE_ARCHITECTURE.md
Normal file
278
HYBRID_ROLE_ARCHITECTURE.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user