[CLAUDE] Infra: SeedDemoUsersAsync robust reconcile pattern + 16 demo users
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m48s

User feedback: prod chỉ thấy admin user, 13 demo seed cũ chưa apply →
fix robust + add thêm.

## Vấn đề trước

- Skip-if-exists pattern: nếu user nào đó tạo fail trước đó → never retry
- Không log error chi tiết khi CreateAsync hoặc AddToRoleAsync fail
- Không tự sửa khi user exists nhưng thiếu dept/position/roles (drift)
- 1 exception abort cả batch

## Fix: RECONCILE PATTERN

Mỗi run, per-user:
- **Nếu chưa có** → CreateAsync + AddToRoleAsync với detailed error log
- **Nếu đã có** → verify + fix nếu drift:
  * DepartmentId mismatch → update
  * Position mismatch → update
  * FullName mismatch → update
  * !IsActive → reactivate
  * Roles missing → AddToRoleAsync supplement
- **Try-catch per-user** → 1 fail không abort 15 còn lại
- **Detailed logging** — counts created/fixed/failed cuối cùng

## 16 demo users (3 thêm)

Trước 13 → giờ 16:
- BOD (3) — bod.huynh + bod.le + **bod.tran** (Phó GĐ Kỹ thuật NĐUQ thứ 2)
- PM (2) — pm.nguyen + **pm.le** (GĐ Vinhomes Ocean Park)
- TPB (6) — CCM/PRO/FIN/ACT/EQU/HRA — 1 mỗi (giữ nguyên)
- Drafter (5) — qs.hoang + qs.ngo + nv.cao + nv.dinh + **nv.truong**
  (NV CCM, làm cho ccm.tran)

Bổ sung: NĐUQ thứ 2 (test multi-AuthorizedSigner), PM thứ 2 (test
multi-project), Drafter trong CCM (test cùng dept với CCM TPB).

## Build

dotnet build BE pass (0 error).

## Note deploy

Khi deploy lên prod:
- HĐ user đã có → reconcile (sửa dept/position/role nếu drift)
- 13 user cũ chưa được tạo → CreateAsync với password User@123456
- 3 user mới (bod.tran, pm.le, nv.truong) → tạo mới
- Log: "Demo users reconcile xong: X created, Y fixed, Z failed.
  (Total demo: 16, password=User@123456 — ⚠ rotate prod)"

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
pqhuy1987
2026-04-24 10:20:43 +07:00
parent b93dacff44
commit a6676658a4

View File

@ -757,9 +757,13 @@ public static class DbInitializer
} }
} }
// Seed ~13 demo users covering full org chart (1+ user per role, distributed // 16 demo users covering full org chart — RECONCILE PATTERN (idempotent +
// across 9 departments). Idempotent: skip nếu user đã tồn tại theo email. // self-healing). Mỗi run:
// Default password: User@123456 (warn ở log để rotate). // - Nếu user chưa có → create + assign roles + dept + position
// - Nếu user đã có → verify dept + position + roles, fix nếu drift
// - Try-catch per-user → 1 fail không abort 12 còn lại
// - Log chi tiết success/fixed/created/failed counts
// Default password: User@123456 (warn log để rotate prod).
private const string DemoUserPassword = "User@123456"; private const string DemoUserPassword = "User@123456";
private static async Task SeedDemoUsersAsync( private static async Task SeedDemoUsersAsync(
@ -769,7 +773,7 @@ public static class DbInitializer
var depts = await db.Departments.ToDictionaryAsync(d => d.Code, d => d.Id); var depts = await db.Departments.ToDictionaryAsync(d => d.Code, d => d.Id);
if (depts.Count == 0) if (depts.Count == 0)
{ {
logger.LogWarning("Skipping SeedDemoUsersAsync — no departments seeded yet."); logger.LogWarning("SeedDemoUsersAsync: skip — no departments seeded yet (run SeedDepartmentsAsync first).");
return; return;
} }
@ -778,26 +782,39 @@ public static class DbInitializer
var demoUsers = new[] var demoUsers = new[]
{ {
// (Email, FullName, DeptCode, Position, RoleNames[]) // (Email, FullName, DeptCode, Position, RoleNames[])
// BOD (3) — director + signer + admin assistant
("bod.huynh@solutionerp.local", "Huỳnh Văn Hùng", "BOD", "Tổng Giám đốc", new[] { AppRoles.Director }), ("bod.huynh@solutionerp.local", "Huỳnh Văn Hùng", "BOD", "Tổng Giám đốc", new[] { AppRoles.Director }),
("bod.le@solutionerp.local", "Lê Thị Mai", "BOD", "Phó Giám đốc (NĐUQ)", new[] { AppRoles.AuthorizedSigner }), ("bod.le@solutionerp.local", "Lê Thị Mai", "BOD", "Phó Giám đốc (NĐUQ)", new[] { AppRoles.AuthorizedSigner }),
("bod.tran@solutionerp.local", "Trần Quốc Bảo", "BOD", "Phó Giám đốc Kỹ thuật (NĐUQ)", new[] { AppRoles.AuthorizedSigner }),
// PM (2) — giám đốc dự án
("pm.nguyen@solutionerp.local", "Nguyễn Quốc Cường", "PM", "Giám đốc Dự án FLOCK 01", new[] { AppRoles.ProjectManager }), ("pm.nguyen@solutionerp.local", "Nguyễn Quốc Cường", "PM", "Giám đốc Dự án FLOCK 01", new[] { AppRoles.ProjectManager }),
("pm.le@solutionerp.local", "Lê Thanh Tùng", "PM", "Giám đốc Dự án Vinhomes Ocean Park", new[] { AppRoles.ProjectManager }),
// CCM/PRO/FIN/ACT/EQU/HRA — Trưởng phòng các phòng ban (1 mỗi)
("ccm.tran@solutionerp.local", "Trần Văn Bình", "CCM", "Trưởng phòng Kiểm soát chi phí", new[] { AppRoles.CostControl, AppRoles.DeptManager }), ("ccm.tran@solutionerp.local", "Trần Văn Bình", "CCM", "Trưởng phòng Kiểm soát chi phí", new[] { AppRoles.CostControl, AppRoles.DeptManager }),
("pro.pham@solutionerp.local", "Phạm Thị Hồng", "PRO", "Trưởng phòng Cung ứng", new[] { AppRoles.Procurement, AppRoles.DeptManager }), ("pro.pham@solutionerp.local", "Phạm Thị Hồng", "PRO", "Trưởng phòng Cung ứng", new[] { AppRoles.Procurement, AppRoles.DeptManager }),
("fin.do@solutionerp.local", "Đỗ Minh Tuấn", "FIN", "Trưởng phòng Tài chính", new[] { AppRoles.Finance, AppRoles.DeptManager }), ("fin.do@solutionerp.local", "Đỗ Minh Tuấn", "FIN", "Trưởng phòng Tài chính", new[] { AppRoles.Finance, AppRoles.DeptManager }),
("act.vu@solutionerp.local", "Vũ Thị Lan", "ACT", "Kế toán trưởng", new[] { AppRoles.Accounting, AppRoles.DeptManager }), ("act.vu@solutionerp.local", "Vũ Thị Lan", "ACT", "Kế toán trưởng", new[] { AppRoles.Accounting, AppRoles.DeptManager }),
("equ.bui@solutionerp.local", "Bùi Văn Khánh", "EQU", "Trưởng phòng Thiết bị", new[] { AppRoles.Equipment, AppRoles.DeptManager }), ("equ.bui@solutionerp.local", "Bùi Văn Khánh", "EQU", "Trưởng phòng Thiết bị", new[] { AppRoles.Equipment, AppRoles.DeptManager }),
("hra.dang@solutionerp.local", "Đặng Thị Thanh", "HRA", "Trưởng phòng Nhân sự - Hành chính", new[] { AppRoles.HrAdmin, AppRoles.DeptManager }), ("hra.dang@solutionerp.local", "Đặng Thị Thanh", "HRA", "Trưởng phòng Nhân sự - Hành chính", new[] { AppRoles.HrAdmin, AppRoles.DeptManager }),
// Drafter (5) — soạn thảo HĐ ở các phòng khác nhau
("qs.hoang@solutionerp.local", "Hoàng Văn Đức", "QS", "QS công trường — soạn thảo HĐ", new[] { AppRoles.Drafter }), ("qs.hoang@solutionerp.local", "Hoàng Văn Đức", "QS", "QS công trường — soạn thảo HĐ", new[] { AppRoles.Drafter }),
("qs.ngo@solutionerp.local", "Ngô Thị Hà", "QS", "QS dự án FLOCK 01", new[] { AppRoles.Drafter }), ("qs.ngo@solutionerp.local", "Ngô Thị Hà", "QS", "QS dự án FLOCK 01", new[] { AppRoles.Drafter }),
("nv.cao@solutionerp.local", "Cao Văn Long", "PRO", "Nhân viên Cung ứng", new[] { AppRoles.Drafter }), ("nv.cao@solutionerp.local", "Cao Văn Long", "PRO", "Nhân viên Cung ứng", new[] { AppRoles.Drafter }),
("nv.dinh@solutionerp.local", "Đinh Thị Yến", "FIN", "Nhân viên Tài chính", new[] { AppRoles.Drafter }), ("nv.dinh@solutionerp.local", "Đinh Thị Yến", "FIN", "Nhân viên Tài chính", new[] { AppRoles.Drafter }),
("nv.truong@solutionerp.local", "Trương Minh Quân", "CCM", "Nhân viên Kiểm soát chi phí", new[] { AppRoles.Drafter }),
}; };
int created = 0; int created = 0, fixedExisting = 0, failed = 0;
foreach (var (email, fullName, deptCode, position, roles) in demoUsers) foreach (var (email, fullName, deptCode, position, roles) in demoUsers)
{ {
if (await userManager.FindByEmailAsync(email) is not null) continue; try
{
var existing = await userManager.FindByEmailAsync(email);
if (existing is null)
{
var user = new User var user = new User
{ {
UserName = email, UserName = email,
@ -812,19 +829,72 @@ public static class DbInitializer
var result = await userManager.CreateAsync(user, DemoUserPassword); var result = await userManager.CreateAsync(user, DemoUserPassword);
if (!result.Succeeded) if (!result.Succeeded)
{ {
logger.LogWarning("Demo user {Email} create fail: {Err}", logger.LogWarning("Demo user {Email} CREATE fail: {Err}",
email, string.Join("; ", result.Errors.Select(e => e.Description))); email, string.Join("; ", result.Errors.Select(e => e.Description)));
failed++;
continue; continue;
} }
foreach (var role in roles) await userManager.AddToRoleAsync(user, role); foreach (var role in roles)
{
var roleResult = await userManager.AddToRoleAsync(user, role);
if (!roleResult.Succeeded)
logger.LogWarning("Demo user {Email} AddRole {Role} fail: {Err}",
email, role, string.Join("; ", roleResult.Errors.Select(e => e.Description)));
}
created++; created++;
} }
if (created > 0) else
{ {
logger.LogInformation( // Reconcile — verify dept + position + roles, fix nếu drift
"Seed {Count} demo users (password={Pwd} — ⚠ rotate in prod).", bool changed = false;
created, DemoUserPassword); var targetDeptId = deptId(deptCode);
if (existing.DepartmentId != targetDeptId)
{
existing.DepartmentId = targetDeptId; changed = true;
} }
if (existing.Position != position)
{
existing.Position = position; changed = true;
}
if (existing.FullName != fullName)
{
existing.FullName = fullName; changed = true;
}
if (!existing.IsActive)
{
existing.IsActive = true; changed = true;
}
if (changed)
{
existing.UpdatedAt = DateTime.UtcNow;
await userManager.UpdateAsync(existing);
}
// Reconcile roles: ensure target roles có trong assigned set
var currentRoles = await userManager.GetRolesAsync(existing);
var missingRoles = roles.Where(r => !currentRoles.Contains(r)).ToList();
foreach (var role in missingRoles)
{
var addResult = await userManager.AddToRoleAsync(existing, role);
if (!addResult.Succeeded)
logger.LogWarning("Demo user {Email} AddRole {Role} fail: {Err}",
email, role, string.Join("; ", addResult.Errors.Select(e => e.Description)));
changed = true;
}
if (changed) fixedExisting++;
}
}
catch (Exception ex)
{
logger.LogError(ex, "SeedDemoUsersAsync: unhandled exception cho user {Email}", email);
failed++;
}
}
logger.LogInformation(
"Demo users reconcile xong: {Created} created, {Fixed} fixed (drift), {Failed} failed. (Total demo: {Total}, password={Pwd} — ⚠ rotate prod)",
created, fixedExisting, failed, demoUsers.Length, DemoUserPassword);
} }
private static async Task SeedMenuTreeAsync(ApplicationDbContext db, ILogger logger) private static async Task SeedMenuTreeAsync(ApplicationDbContext db, ILogger logger)