Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
11 KiB
Design Spec: AspNetUsers CRUD & Member Management
日期: 2026-05-27
作者: Chris Chen
狀態: 已核准,待實作
1. 範疇
Phase 1 的兩個管理頁面:
- Member Management — 教友資料 CRUD(含軟刪除)
- User Management — AspNetUsers 帳號 CRUD(透過 ASP.NET Identity
UserManager)
業務規則:
- 每個
AppUser必須連結一個Member(MemberId不可為 null) - 一個
Member可以不有對應的AppUser(教友可無登入帳號)
2. 資料模型
2.1 基礎類別
public abstract class AuditableEntity
{
public DateTime CreatedAt { get; set; }
public string CreatedBy { get; set; } = null!; // FK → AspNetUsers.Id
public DateTime UpdatedAt { get; set; }
public string UpdatedBy { get; set; } = null!; // FK → AspNetUsers.Id
}
public abstract class SoftDeleteEntity : AuditableEntity
{
public bool IsDeleted { get; set; } = false;
public DateTime? DeletedAt { get; set; }
public string? DeletedBy { get; set; } // FK → AspNetUsers.Id
}
2.2 Member(繼承 SoftDeleteEntity)
完整欄位見 docs/DB_SCHEMA.md §4。重點欄位:
| 欄位 | 型別 | 說明 |
|---|---|---|
| Id | int PK | auto-increment |
| FirstName_en | varchar(100) NOT NULL | 英文名(法定名) |
| LastName_en | varchar(100) NOT NULL | 英文姓 |
| NickName | varchar(100)? | 常用名/英文小名(如 Chris) |
| FirstName_zh | varchar(100)? | 中文名 |
| LastName_zh | varchar(100)? | 中文姓 |
| Gender | varchar(10)? | 'M' | 'F' | 'Other' |
| DateOfBirth | date? | |
| BaptismDate | date? | |
| BaptismChurch | varchar(200)? | |
| varchar(200)? | ||
| PhoneCell | varchar(30)? | |
| PhoneHome | varchar(30)? | |
| Address / City / State / ZipCode | varchar | |
| Country | varchar(100) DEFAULT 'USA' | |
| PhotoBlobPath | varchar(500)? | Azure Blob |
| Status | varchar(20) DEFAULT 'Member' | 'Member' | 'Visitor' | 'Inactive' | 'Former' |
| LanguagePreference | varchar(10) DEFAULT 'en' | |
| JoinDate | date? | |
| Notes | text? | |
| FamilyUnitId | int? | FK → FamilyUnits.Id(Phase 1 建 FK,不做 UI) |
2.3 FamilyUnit(繼承 AuditableEntity)
Phase 1:建 Entity + Migration,不實作 CRUD UI。
欄位:Id, FamilyName_en, FamilyName_zh, Notes + audit fields。
2.4 AppUser 更新
AppUser.MemberId(已存在,nullable int)在建立 User 時強制填入。
3. 後端架構
3.1 分層結構
API/ROLAC.API/
├── Entities/
│ ├── Base/
│ │ ├── AuditableEntity.cs
│ │ └── SoftDeleteEntity.cs
│ ├── Member.cs
│ └── FamilyUnit.cs
├── Data/
│ ├── AppDbContext.cs (新增 DbSets + QueryFilter)
│ └── Interceptors/
│ └── AuditSaveChangesInterceptor.cs
├── DTOs/
│ ├── Members/
│ │ ├── MemberDto.cs
│ │ ├── MemberListItemDto.cs
│ │ ├── CreateMemberRequest.cs
│ │ └── UpdateMemberRequest.cs
│ ├── Users/
│ │ ├── UserDto.cs
│ │ ├── UserListItemDto.cs
│ │ ├── CreateUserRequest.cs
│ │ └── UpdateUserRequest.cs
│ └── Shared/
│ └── PagedResult.cs
├── Services/
│ ├── IMemberService.cs / MemberService.cs
│ └── IUserManagementService.cs / UserManagementService.cs
└── Controllers/
├── MembersController.cs
└── UsersController.cs
3.2 AuditSaveChangesInterceptor
繼承 SaveChangesInterceptor,在 SavingChangesAsync 前:
EntityState.Added+ 繼承AuditableEntity→ 填CreatedAt = UtcNow,CreatedBy = currentUserId,UpdatedAt = UtcNow,UpdatedBy = currentUserIdEntityState.Modified+ 繼承AuditableEntity→ 填UpdatedAt = UtcNow,UpdatedBy = currentUserIdEntityState.Deleted+ 繼承SoftDeleteEntity→ 攔截,改成Modified,設IsDeleted=true,DeletedAt=UtcNow,DeletedBy=currentUserIdcurrentUserId透過IHttpContextAccessor取 JWT claimsub
3.3 MemberService
public interface IMemberService
{
Task<PagedResult<MemberListItemDto>> GetPagedAsync(int page, int pageSize, string? search, string? status, bool? hasUser);
Task<MemberDto?> GetByIdAsync(int id);
Task<int> CreateAsync(CreateMemberRequest request, string createdBy);
Task UpdateAsync(int id, UpdateMemberRequest request);
Task DeleteAsync(int id); // soft delete via interceptor
}
直接注入 AppDbContext。GetPagedAsync 支援:
search:比對FirstName_en,LastName_en,NickName,FirstName_zh,LastName_zh,Emailstatus:篩選Member.StatushasUser:LEFT JOINAspNetUsersONMemberId = Member.Id
3.4 UserManagementService
public interface IUserManagementService
{
Task<PagedResult<UserListItemDto>> GetPagedAsync(int page, int pageSize, string? search);
Task<UserDto?> GetByIdAsync(string id);
Task<CreateUserResult> CreateAsync(CreateUserRequest request);
// CreateUserResult: { UserId, TempPassword }
Task UpdateAsync(string id, UpdateUserRequest request);
Task DeactivateAsync(string id); // IsActive=false + LockoutEnd=MaxValue
Task<string> ResetPasswordAsync(string id); // 回傳新臨時密碼
}
注入 UserManager<AppUser>, RoleManager<AppRole>, AppDbContext。
CreateAsync 邏輯:
- 驗證
MemberId存在且未刪除 - 驗證
MemberId未被其他 User 使用(UNIQUE 約束) - 驗證 email 未重複
- 生成 12 字臨時密碼(大小寫+數字+特殊符號)
userManager.CreateAsync(user, tempPassword)userManager.AddToRolesAsync(user, request.Roles)- 回傳
{ UserId, TempPassword }
3.5 API Endpoints
Members
| Method | Route | 說明 | 角色 |
|---|---|---|---|
| GET | /api/members |
分頁列表 | super_admin, secretary, pastor |
| GET | /api/members/{id} |
單筆詳情 | super_admin, secretary, pastor |
| POST | /api/members |
新增 | super_admin, secretary |
| PUT | /api/members/{id} |
更新 | super_admin, secretary |
| DELETE | /api/members/{id} |
軟刪除 | super_admin, secretary |
GET 查詢參數:?page=1&pageSize=20&search=Chen&status=Member&hasUser=true
回傳:PagedResult<MemberListItemDto>
Users
| Method | Route | 說明 | 角色 |
|---|---|---|---|
| GET | /api/users |
分頁列表 | super_admin |
| GET | /api/users/{id} |
單筆詳情 | super_admin |
| POST | /api/users |
建立帳號(需 MemberId) | super_admin |
| PUT | /api/users/{id} |
更新(email/roles/IsActive) | super_admin |
| DELETE | /api/users/{id} |
停用帳號 | super_admin |
| POST | /api/users/{id}/reset-password |
重設密碼 | super_admin |
POST /api/users request:
{ "memberId": 42, "email": "john@example.com", "roles": ["member"], "languagePreference": "en" }
POST /api/users response(只回傳一次):
{ "userId": "guid...", "tempPassword": "Xk9#mP2qLv8!" }
4. 前端架構
4.1 路由
/user-portal/admin/members → MembersPageComponent
/user-portal/admin/users → UsersPageComponent
4.2 新增 Angular Services
APP/src/app/
└── features/
├── members/
│ ├── models/
│ │ └── member.model.ts
│ ├── services/
│ │ └── member-api.service.ts (extends CrudBaseApiService<MemberDto>)
│ ├── components/
│ │ ├── member-form-dialog/ (新增/編輯 Member 表單)
│ │ └── create-user-dialog/ (從 Member 建立帳號)
│ └── pages/
│ └── members-page/
└── users/
├── models/
│ └── user.model.ts
├── services/
│ └── user-api.service.ts (custom,非標準 CRUD)
├── components/
│ └── edit-user-dialog/ (編輯角色/狀態)
└── pages/
└── users-page/
4.3 Members 頁面
Kendo Grid 欄位:
- 姓名(顯示優先順序:
NickName→FirstName_en,搭配LastName_en;中文名若有則顯示在括號內) - Status badge(顏色碼:Member=綠、Visitor=藍、Inactive=灰、Former=橙)
- PhoneCell
- JoinDate
- 帳號狀態(有帳號 → 綠色 ✓ badge;無 → 灰色 —)
- 操作:編輯 / 刪除 / 建立帳號(只在無帳號時顯示)
新增/編輯 Dialog: 全欄位,分為 3 個 tab:
- 基本資料:法定英文姓名(FirstName_en / LastName_en)、NickName、中文姓名、性別、生日、Status、語言
- 聯絡資訊:Email、電話、地址
- 教會資訊:JoinDate、BaptismDate、BaptismChurch、Notes
建立帳號 Dialog(CreateUserDialogComponent):
- 欄位:Email、角色(multi-select Kendo DropDownList)、語言偏好
- 送出後顯示臨時密碼畫面(加「複製」按鈕),關閉前提醒「此密碼不會再次顯示」
4.4 Users 頁面
Kendo Grid 欄位:
- 連結 Member 姓名(可點擊跳到 Member 頁面)
- 角色 badges
- IsActive toggle
- 最後登入時間
- 操作:編輯 / 停用 / 重設密碼
重設密碼: confirm dialog → 成功後顯示新臨時密碼(同 Member 頁建立帳號的結果畫面)
4.5 側邊欄更新
user-navbar.component.ts 新增 Administration 分區:
adminNavItems: NavItem[] = [
{ text: 'Members', icon: peopleIcon, path: '/user-portal/admin/members' },
{ text: 'Users', icon: userIcon, path: '/user-portal/admin/users' },
]
只有 super_admin / secretary 角色才顯示此分區(透過 AuthService.currentUser$ 的 roles 判斷)。
5. 安全性考量
- 所有
/api/members和/api/users端點均需[Authorize] - User 管理 API 僅限
super_admin;Member 管理 API 允許secretary和pastor讀取 - 臨時密碼僅在 API response 中回傳一次,不存入資料庫明文
MemberId建立 UNIQUE INDEX(避免同一 Member 對應多個 User)
6. EF Migration
新增實體後需執行一次 Migration:
Add-Migration AddMemberAndFamilyUnit
Update-Database
Migration 需包含:
Members表(含所有欄位 + soft delete + audit fields)FamilyUnits表AspNetUsers.MemberId加UNIQUE INDEX(nullable unique — 允許多個 null,但非 null 值唯一)idx_members_status,idx_members_emailindexes(參見 DB_SCHEMA.md §17)
7. 不在本次範疇
- FamilyUnit CRUD UI(Phase 1 建 Entity,UI 暫緩)
- MemberTag / MemberMinistry 管理 UI
- 大頭照上傳(PhotoBlobPath)
- Email 邀請連結
- Member 頁面的 Pagination(後端已支援,前端先實作)