[CLAUDE] Infra: SeedDemoUsersAsync robust reconcile pattern + 16 demo users
All checks were successful
Deploy SOLUTION_ERP / build-deploy (push) Successful in 2m48s
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:
@ -757,9 +757,13 @@ public static class DbInitializer
|
||||
}
|
||||
}
|
||||
|
||||
// Seed ~13 demo users covering full org chart (1+ user per role, distributed
|
||||
// across 9 departments). Idempotent: skip nếu user đã tồn tại theo email.
|
||||
// Default password: User@123456 (warn ở log để rotate).
|
||||
// 16 demo users covering full org chart — RECONCILE PATTERN (idempotent +
|
||||
// self-healing). Mỗi run:
|
||||
// - 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 static async Task SeedDemoUsersAsync(
|
||||
@ -769,7 +773,7 @@ public static class DbInitializer
|
||||
var depts = await db.Departments.ToDictionaryAsync(d => d.Code, d => d.Id);
|
||||
if (depts.Count == 0)
|
||||
{
|
||||
logger.LogWarning("Skipping SeedDemoUsersAsync — no departments seeded yet.");
|
||||
logger.LogWarning("SeedDemoUsersAsync: skip — no departments seeded yet (run SeedDepartmentsAsync first).");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -778,26 +782,39 @@ public static class DbInitializer
|
||||
var demoUsers = new[]
|
||||
{
|
||||
// (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.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.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 }),
|
||||
("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 }),
|
||||
("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 }),
|
||||
("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.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.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)
|
||||
{
|
||||
if (await userManager.FindByEmailAsync(email) is not null) continue;
|
||||
|
||||
try
|
||||
{
|
||||
var existing = await userManager.FindByEmailAsync(email);
|
||||
if (existing is null)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
UserName = email,
|
||||
@ -812,19 +829,72 @@ public static class DbInitializer
|
||||
var result = await userManager.CreateAsync(user, DemoUserPassword);
|
||||
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)));
|
||||
failed++;
|
||||
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++;
|
||||
}
|
||||
if (created > 0)
|
||||
else
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Seed {Count} demo users (password={Pwd} — ⚠ rotate in prod).",
|
||||
created, DemoUserPassword);
|
||||
// Reconcile — verify dept + position + roles, fix nếu drift
|
||||
bool changed = false;
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user