ROLAC — Database Schema Design
版本: v1.0 (2026-05-24)
資料庫: PostgreSQL 15+
ORM: Entity Framework Core 8 (Code-First Migrations)
命名慣例: PascalCase 表名 / 欄位名(EF Core 預設映射到 snake_case PostgreSQL 欄位)
目錄
- 設計原則
- BaseEntity 模式
- Phase 1 — Authentication & Identity
- Phase 1 — Member Management
- Phase 1 — Ministry(事工部門)
- Phase 1 — CMS
- Phase 1 — Giving & Donations(奉獻)
- Phase 1 — Expense Tracking(支出)
- Phase 1 — Prayer Requests(代禱)
- Phase 1 — Audit Log
- Phase 1 — Notifications
- Phase 2 — Service Roster(服事表)
- Phase 2 — Sunday Attendance(主日出席)
- Phase 2 — Cell Groups(小組)
- Phase 3 — Ministry Budget(事工預算)
- Seed Data
- Indexes
- EF Core 設定摘要
1. 設計原則
| 原則 |
說明 |
| Code-First |
全部透過 EF Core Migration 建立,不手寫 DDL |
| Soft Delete |
重要實體用 IsDeleted 標記刪除,不實際刪除 DB 記錄 |
| Audit Trail |
每個可修改實體均記錄 CreatedAt / CreatedBy / UpdatedAt / UpdatedBy |
| Bilingual Fields |
需要雙語的欄位加 _en / _zh 後綴(如 Name_en / Name_zh) |
| Money |
所有金額欄位使用 decimal(18,2) |
| Phone |
varchar(30)(支援國際格式) |
| EIN |
不存入資料庫,由環境變數 CHURCH_EIN 提供 |
| Azure Blob |
圖片/PDF 只存 Blob 路徑(varchar(500)),不存 Base64 |
| jsonb |
AuditLog 的 Before/After 用 PostgreSQL jsonb 儲存 |
| IDs |
一般實體用 int (SERIAL);User 繼承 ASP.NET Identity 使用 string (Guid) |
2. BaseEntity 模式
大部分實體繼承以下基礎類別(透過 EF Core TPH 或 owned type 實現):
3. Authentication & Identity
AppUser(繼承 IdentityUser)
| 欄位 |
型別 |
說明 |
| Id |
varchar(450) PK |
Guid,由 Identity 管理 |
| UserName |
varchar(256) |
使用者名稱(通常等同 Email) |
| NormalizedUserName |
varchar(256) |
大寫版本,用於查詢 |
| Email |
varchar(256) |
電子郵件 |
| NormalizedEmail |
varchar(256) |
|
| EmailConfirmed |
bool |
|
| PasswordHash |
varchar(MAX) |
|
| SecurityStamp |
varchar(MAX) |
|
| ConcurrencyStamp |
varchar(MAX) |
|
| PhoneNumber |
varchar(256)? |
|
| PhoneNumberConfirmed |
bool |
|
| TwoFactorEnabled |
bool |
|
| LockoutEnd |
datetimeoffset? |
|
| LockoutEnabled |
bool |
|
| AccessFailedCount |
int |
|
| MemberId |
int? |
FK → Member.Id(可為 null) |
| LanguagePreference |
varchar(10) |
'en' | 'zh-TW',DEFAULT 'en' |
| IsActive |
bool |
DEFAULT true,停用帳號用 |
| LastLoginAt |
timestamp? |
最後登入時間 |
| CreatedAt |
timestamp |
帳號建立時間 |
Identity 角色表(AspNetRoles, AspNetUserRoles)由 ASP.NET Identity 自動管理。
AppRole(繼承 IdentityRole)
| 欄位 |
型別 |
說明 |
| Id |
varchar(450) PK |
Guid |
| Name |
varchar(256) |
角色名稱(見下方列表) |
| NormalizedName |
varchar(256) |
|
| ConcurrencyStamp |
varchar(MAX) |
|
| Description |
varchar(500)? |
角色說明 |
預設角色(Seed):
ROLAC 為靈糧堂體制,無長老制。
| Name |
中文 |
說明 |
| super_admin |
系統管理員 |
所有權限 |
| pastor |
牧師 |
全覽教友與財務摘要 |
| board_member |
理事 |
教會治理委員,可查看財務摘要與教友概覽 |
| coworker_chair |
同工會主席 |
統籌各事工領袖,可管理服事與小組 |
| ministry_leader |
事工領袖 |
受 Ministry Scope 限制 |
| district_leader |
區長 |
管理轄下多個小組 |
| cell_leader |
小組長 |
僅限自身小組 |
| coworker |
同工 |
參與指定事工,可申請報銷 |
| finance |
財務同工 |
管理奉獻與支出 |
| secretary |
行政秘書 |
管理教友資料與排班 |
| worship_leader |
敬拜領袖 |
管理歌曲庫與歌單(Phase 暫緩) |
| member |
一般教友 |
查看個人資料與服事表 |
| visitor |
訪客 |
僅限公開頁面 |
UserMinistry(Ministry Scope — 用戶管理哪個事工)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| UserId |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| MinistryId |
int NOT NULL |
FK → Ministries.Id |
| AssignedAt |
timestamp NOT NULL |
|
| AssignedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| UNIQUE |
(UserId, MinistryId) |
一個用戶不重複指派同一事工 |
UserDevice(FCM 推播 Token)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| UserId |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| FcmToken |
varchar(500) NOT NULL |
Firebase Cloud Messaging token |
| Platform |
varchar(20) NOT NULL |
'ios' | 'android' | 'web' |
| DeviceName |
varchar(100)? |
裝置名稱(選填) |
| LastSeenAt |
timestamp NOT NULL |
|
| IsActive |
bool NOT NULL DEFAULT true |
|
| CreatedAt |
timestamp NOT NULL |
|
4. Member Management
Member(教友資料)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| FirstName_en |
varchar(100) NOT NULL |
英文名 |
| LastName_en |
varchar(100) NOT NULL |
英文姓 |
| FirstName_zh |
varchar(100)? |
中文名 |
| LastName_zh |
varchar(100)? |
中文姓 |
| Gender |
varchar(10)? |
'M' | 'F' | 'Other' |
| DateOfBirth |
date? |
生日 |
| BaptismDate |
date? |
受洗日期 |
| BaptismChurch |
varchar(200)? |
受洗教會 |
| Email |
varchar(200)? |
|
| PhoneCell |
varchar(30)? |
手機 |
| PhoneHome |
varchar(30)? |
家電 |
| Address |
varchar(500)? |
地址 |
| City |
varchar(100)? |
|
| State |
varchar(50)? |
|
| ZipCode |
varchar(20)? |
|
| Country |
varchar(100) NOT NULL DEFAULT 'USA' |
|
| PhotoBlobPath |
varchar(500)? |
Azure Blob 路徑 |
| Status |
varchar(20) NOT NULL DEFAULT 'Member' |
'Member' | 'Visitor' | 'Inactive' | 'Former' |
| LanguagePreference |
varchar(10) NOT NULL DEFAULT 'en' |
'en' | 'zh-TW' |
| JoinDate |
date? |
加入教會日期 |
| Notes |
text? |
內部備注 |
| FamilyUnitId |
int? |
FK → FamilyUnits.Id |
| IsDeleted |
bool NOT NULL DEFAULT false |
軟刪除 |
| DeletedAt |
timestamp? |
|
| DeletedBy |
varchar(450)? |
FK → AspNetUsers.Id |
| CreatedAt |
timestamp NOT NULL |
|
| CreatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| UpdatedAt |
timestamp NOT NULL |
|
| UpdatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
FamilyUnit(家庭單元)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| FamilyName_en |
varchar(200)? |
家庭英文名(如 "Chang Family") |
| FamilyName_zh |
varchar(200)? |
家庭中文名(如「張家」) |
| Notes |
text? |
|
| CreatedAt |
timestamp NOT NULL |
|
| CreatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| UpdatedAt |
timestamp NOT NULL |
|
| UpdatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
一個 FamilyUnit 可有多個 Members(One-to-Many via Member.FamilyUnitId)
MemberMinistry(教友服事事工關聯 — M:N)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| MemberId |
int NOT NULL |
FK → Members.Id |
| MinistryId |
int NOT NULL |
FK → Ministries.Id |
| MinistryRole |
varchar(50)? |
在事工中的角色(Leader/Member/Coordinator) |
| JoinedAt |
date? |
|
| Notes |
varchar(200)? |
|
| UNIQUE |
(MemberId, MinistryId) |
|
MemberTag(教友標籤)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| MemberId |
int NOT NULL |
FK → Members.Id |
| Tag |
varchar(100) NOT NULL |
e.g., 'NewBeliever', 'Volunteer', 'Youth' |
| UNIQUE |
(MemberId, Tag) |
|
5. Ministry(事工部門)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| Name_en |
varchar(200) NOT NULL |
|
| Name_zh |
varchar(200)? |
|
| Description_en |
text? |
|
| Description_zh |
text? |
|
| SortOrder |
int NOT NULL DEFAULT 0 |
顯示排序 |
| IsActive |
bool NOT NULL DEFAULT true |
|
| DefaultFunctionalClass |
varchar(20) NOT NULL DEFAULT 'Program' |
IRS Form 990 功能性費用分類:'Program' | 'ManagementGeneral' | 'Fundraising'。Seed:Administration → 'ManagementGeneral',其餘 → 'Program' |
6. CMS
CmsPage(靜態頁面:關於我們、異象等)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| Slug |
varchar(100) UNIQUE NOT NULL |
URL 識別碼,如 'about', 'vision' |
| Title_en |
varchar(300) NOT NULL |
|
| Title_zh |
varchar(300)? |
|
| Body_en |
text? |
Markdown 或 HTML |
| Body_zh |
text? |
|
| IsPublished |
bool NOT NULL DEFAULT false |
|
| PublishedAt |
timestamp? |
|
| UpdatedAt |
timestamp NOT NULL |
|
| UpdatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
Announcement(消息公告)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| Title_en |
varchar(300) NOT NULL |
|
| Title_zh |
varchar(300)? |
|
| Body_en |
text NOT NULL |
|
| Body_zh |
text? |
|
| PinnedUntil |
date? |
null = 不置頂 |
| ScheduledAt |
timestamp? |
null = 立即發佈 |
| IsPublished |
bool NOT NULL DEFAULT false |
|
| PublishedAt |
timestamp? |
|
| ImageBlobPath |
varchar(500)? |
封面圖 Azure Blob 路徑 |
| CreatedAt |
timestamp NOT NULL |
|
| CreatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| UpdatedAt |
timestamp NOT NULL |
|
| UpdatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
SermonVideo(講道影片 — YouTube 嵌入)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| Title_en |
varchar(300) NOT NULL |
|
| Title_zh |
varchar(300)? |
|
| YouTubeVideoId |
varchar(50) NOT NULL |
YouTube 影片 ID(非完整 URL) |
| PreacherName |
varchar(200)? |
講員姓名 |
| SermonDate |
date NOT NULL |
講道日期 |
| Description_en |
text? |
|
| Description_zh |
text? |
|
| IsPublished |
bool NOT NULL DEFAULT true |
|
| SortOrder |
int NOT NULL DEFAULT 0 |
顯示排序(新到舊) |
| CreatedAt |
timestamp NOT NULL |
|
| CreatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
ContactInquiry(聯絡表單)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| Name |
varchar(200) NOT NULL |
|
| Email |
varchar(200) NOT NULL |
|
| Phone |
varchar(30)? |
|
| Subject |
varchar(300)? |
|
| Message |
text NOT NULL |
|
| Language |
varchar(10) NOT NULL DEFAULT 'en' |
表單填寫語言 |
| IsRead |
bool NOT NULL DEFAULT false |
|
| ReadAt |
timestamp? |
|
| ReadBy |
varchar(450)? |
FK → AspNetUsers.Id |
| AssignedTo |
varchar(450)? |
FK → AspNetUsers.Id |
| InternalNotes |
text? |
後台同工備注 |
| CreatedAt |
timestamp NOT NULL DEFAULT now() |
|
7. Giving & Donations(奉獻)
GivingCategory(奉獻類型)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| Name_en |
varchar(200) NOT NULL |
e.g., 'Tithe', 'General Offering', 'Building Fund' |
| Name_zh |
varchar(200)? |
e.g., '什一奉獻', '一般奉獻', '建堂基金' |
| Description_en |
varchar(500)? |
|
| Description_zh |
varchar(500)? |
|
| IsActive |
bool NOT NULL DEFAULT true |
|
| SortOrder |
int NOT NULL DEFAULT 0 |
|
OfferingSession(主日奉獻袋批次作業)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| SessionDate |
date NOT NULL |
主日日期 |
| Status |
varchar(20) NOT NULL DEFAULT 'Draft' |
'Draft' | 'Submitted' | 'Reconciled' |
| CashTotal |
decimal(18,2) NOT NULL DEFAULT 0 |
財務同工清點現金總額(手輸) |
| CheckTotal |
decimal(18,2) NOT NULL DEFAULT 0 |
支票加總總額(手輸) |
| SystemTotal |
decimal(18,2) NOT NULL DEFAULT 0 |
系統計算奉獻條目加總(觸發或計算) |
| Difference |
decimal(18,2) NOT NULL DEFAULT 0 |
= (CashTotal + CheckTotal) - SystemTotal |
| Notes |
text? |
|
| SubmittedAt |
timestamp? |
|
| SubmittedBy |
varchar(450)? |
FK → AspNetUsers.Id |
| ReconciledAt |
timestamp? |
|
| ReconciledBy |
varchar(450)? |
FK → AspNetUsers.Id |
| CreatedAt |
timestamp NOT NULL |
|
| CreatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| UpdatedAt |
timestamp NOT NULL |
|
| UpdatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| UNIQUE |
(SessionDate) |
一個主日只有一個 Session |
鍵盤優先 UI 設計: 財務同工 Tab/Enter 跳欄逐筆輸入,每筆加入後右側即時更新加總;最後輸入 CashTotal / CheckTotal 人工清點金額,系統計算 Difference,目標為零後點「提交」。
Giving(奉獻記錄)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| MemberId |
int? |
FK → Members.Id;null = 匿名 |
| GivingCategoryId |
int NOT NULL |
FK → GivingCategories.Id |
| OfferingSessionId |
int? |
FK → OfferingSessions.Id;null = 非批次單筆 |
| Amount |
decimal(18,2) NOT NULL |
用於 IRS 收據的金額(PayPal 用 NetAmount) |
| GrossAmount |
decimal(18,2)? |
PayPal 手續費前總額 |
| FeeAmount |
decimal(18,2)? |
PayPal/Stripe 手續費 |
| PaymentMethod |
varchar(20) NOT NULL |
'Cash' | 'Check' | 'Zelle' | 'PayPal' | 'Stripe' | 'Other' |
| CheckNumber |
varchar(50)? |
支票號碼 |
| ZelleReferenceCode |
varchar(100)? |
Zelle 參考碼(手動輸入) |
| PayPalTransactionId |
varchar(100)? |
PayPal 交易 ID |
| StripePaymentIntentId |
varchar(200)? |
Stripe PaymentIntent ID(Phase 4) |
| GivingDate |
date NOT NULL |
奉獻日期 |
| IsAnonymous |
bool NOT NULL DEFAULT false |
|
| Notes |
varchar(500)? |
|
| CreatedAt |
timestamp NOT NULL |
|
| CreatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| UpdatedAt |
timestamp NOT NULL |
|
| UpdatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
IRS 規則: 匿名奉獻(IsAnonymous = true 或 MemberId = null)不計入個人年度收據。
PayPal: Amount = GrossAmount - FeeAmount(Net Amount 才用於 IRS)。
GivingReceipt(年度 IRS 奉獻收據)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| MemberId |
int NOT NULL |
FK → Members.Id |
| FiscalYear |
int NOT NULL |
年份(如 2026) |
| TotalAmount |
decimal(18,2) NOT NULL |
該年度奉獻總額 |
| GivingCount |
int NOT NULL |
奉獻筆數 |
| PdfBlobPath |
varchar(500)? |
Azure Blob 路徑(receipts/{year}/{memberId}.pdf) |
| GeneratedAt |
timestamp? |
PDF 產生時間 |
| SentAt |
timestamp? |
Email 寄出時間 |
| SentToEmail |
varchar(200)? |
寄送 Email 位址 |
| IsVoided |
bool NOT NULL DEFAULT false |
作廢(保留 PDF,不再有效) |
| VoidReason |
varchar(500)? |
|
| CreatedAt |
timestamp NOT NULL |
|
| CreatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| UNIQUE |
(MemberId, FiscalYear) |
每人每年只一份正式收據 |
GivingRecurringSchedule(定期奉獻排程 — Phase 4)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| MemberId |
int NOT NULL |
FK → Members.Id |
| GivingCategoryId |
int NOT NULL |
FK → GivingCategories.Id |
| Amount |
decimal(18,2) NOT NULL |
|
| Frequency |
varchar(20) NOT NULL |
'Weekly' | 'BiWeekly' | 'Monthly' |
| StripeSubscriptionId |
varchar(200)? |
Stripe Subscription ID |
| StartDate |
date NOT NULL |
|
| EndDate |
date? |
null = 持續進行 |
| IsActive |
bool NOT NULL DEFAULT true |
|
| CreatedAt |
timestamp NOT NULL |
|
| CreatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
8. Expense Tracking(支出)
Form990ExpenseLines(IRS Form 990 Part IX 自然費用科目)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| LineCode |
varchar(10) NOT NULL UNIQUE |
Part IX 行號,如 "7"、"11b"、"16"、"24" |
| Name_en |
varchar(200) NOT NULL |
英文科目名稱 |
| Name_zh |
varchar(200)? |
中文科目名稱 |
| SortOrder |
int NOT NULL |
排序 |
| IsActive |
bool NOT NULL DEFAULT true |
|
| CreatedAt |
timestamp NOT NULL |
|
| CreatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| UpdatedAt |
timestamp NOT NULL |
|
| UpdatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
IRS Form 990 Part IX 說明: Part IX「Statement of Functional Expenses」要求將每筆支出依 IRS 自然費用科目(LineCode)分類,同時按功能性費用分類(Program / Management & General / Fundraising)橫向彙總,生成 Form 990 Schedule Part IX 報表。
ExpenseCategoryGroup(支出大類)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| Name_en |
varchar(200) NOT NULL |
|
| Name_zh |
varchar(200)? |
|
| SortOrder |
int NOT NULL DEFAULT 0 |
|
| IsActive |
bool NOT NULL DEFAULT true |
|
| Form990LineId |
int? |
FK → Form990ExpenseLines.Id,ON DELETE SET NULL。大類層級預設 990 行號;Seed 預設為行 "24"(其他費用) |
Seed 大類(14 個):
| Id |
Name_en |
Name_zh |
| 1 |
Equipment |
設備 |
| 2 |
Consumables |
消耗品 |
| 3 |
Food & Beverage |
餐飲 |
| 4 |
Training |
培訓 |
| 5 |
Materials |
教材 |
| 6 |
Facility |
場地 |
| 7 |
Printing |
印刷 |
| 8 |
Missions |
宣教 |
| 9 |
Benevolence |
關懷救助 |
| 10 |
Other |
其他 |
| 11 |
Personnel |
人事 |
| 12 |
Professional Services |
專業服務 |
| 13 |
Information Technology |
資訊科技 |
| 14 |
Finance & Banking |
財務與銀行 |
ExpenseSubCategory(支出子項目)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| GroupId |
int NOT NULL |
FK → ExpenseCategoryGroups.Id |
| Name_en |
varchar(200) NOT NULL |
|
| Name_zh |
varchar(200)? |
|
| SortOrder |
int NOT NULL DEFAULT 0 |
|
| IsActive |
bool NOT NULL DEFAULT true |
|
| Form990LineId |
int? |
FK → Form990ExpenseLines.Id,ON DELETE SET NULL。子項目層級 990 行號(優先於大類值) |
有效 990 行號解析順序: SubCategory.Form990LineId ?? Group.Form990LineId ?? "24"(先取子項目的行號;若為 null 則取大類的行號;仍為 null 則視為行 "24" — Other Expenses)。
Seed 子項目(完整種子):
| GroupId |
Name_en |
Name_zh |
| 1 Equipment |
Purchase |
購置 |
| 1 Equipment |
Rental |
租借 |
| 1 Equipment |
Maintenance & Repair |
維修 |
| 2 Consumables |
Batteries |
電池 |
| 2 Consumables |
Accessories |
配件 |
| 2 Consumables |
Cleaning Supplies |
清潔用品 |
| 2 Consumables |
Office Supplies |
文具 |
| 3 Food & Beverage |
Catering |
出餐費用 |
| 3 Food & Beverage |
Food Ingredients |
食材採購 |
| 3 Food & Beverage |
Utensils |
器具 |
| 3 Food & Beverage |
Disposable Tableware |
一次性餐具 |
| 4 Training |
Course Fees |
課程費用 |
| 4 Training |
Books |
書籍 |
| 4 Training |
Conference |
研討會 |
| 4 Training |
Travel |
差旅 |
| 5 Materials |
Curriculum Printing |
教材印刷 |
| 5 Materials |
Craft Supplies |
手工材料 |
| 5 Materials |
Copyright & Licensing |
版權購買 |
| 6 Facility |
Rent |
場地租金 |
| 6 Facility |
Utilities |
水電 |
| 6 Facility |
Property Insurance |
財產保險 |
| 6 Facility |
Decoration |
裝飾 |
| 6 Facility |
Repairs & Maintenance |
修繕維護 |
| 7 Printing |
Bulletins |
週報 |
| 7 Printing |
Order of Service |
程序單 |
| 7 Printing |
Posters |
海報 |
| 7 Printing |
Advertising & Promotion |
廣告與推廣 |
| 8 Missions |
Offering Transfer |
奉獻轉帳 |
| 8 Missions |
Missionary Support |
宣教士支援 |
| 8 Missions |
Foreign Missions Support |
海外宣教支援 |
| 8 Missions |
Travel |
差旅 |
| 9 Benevolence |
Emergency Aid |
急難救助 |
| 9 Benevolence |
Condolence Gifts |
慰問禮品 |
| 9 Benevolence |
Visit Expenses |
探訪費用 |
| 10 Other |
Miscellaneous |
雜支 |
| 10 Other |
Gifts |
禮品 |
| 11 Personnel |
Salary & Wages |
薪資 |
| 11 Personnel |
Officer / Key Employee Compensation |
主任/關鍵員工薪酬 |
| 11 Personnel |
Payroll Taxes |
薪資稅費 |
| 11 Personnel |
Employee Benefits |
員工福利 |
| 11 Personnel |
Retirement / Pension |
退休/養老金 |
| 11 Personnel |
Workers Compensation |
勞工保險 |
| 11 Personnel |
Honorarium |
酬庸 |
| 11 Personnel |
Staff Training |
同工進修 |
| 11 Personnel |
Contract Labor |
外包勞務 |
備注: Facility > 財產保險 指建築物/場地責任險;員工健保、團體保險等歸 Personnel > 員工福利。同工代墊報銷依實際購買物選大類,不歸人事。
修繕歸類: 建物日常修繕(水電、油漆等)歸 Facility > 修繕維護(990 第16行 Occupancy);設備維修歸 Equipment > Maintenance & Repair(990 第13行)。重大資本改良(整修屋頂、大規模裝修)應資本化、走折舊(第22行,目前未實作)。
子項目更名說明: Food & Beverage > Consumables(消耗品)更名為 Disposable Tableware(一次性餐具)以消除與大類同名的歧義;Materials > Printing(印刷費用)更名為 Curriculum Printing(教材印刷)以與 Printing 大類區分。
Expense(支出記錄)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| MinistryId |
int NOT NULL |
FK → Ministries.Id |
| CategoryGroupId |
int NOT NULL |
FK → ExpenseCategoryGroups.Id |
| SubCategoryId |
int NOT NULL |
FK → ExpenseSubCategories.Id |
| Type |
varchar(30) NOT NULL |
'VendorPayment' | 'StaffReimbursement' |
| Status |
varchar(30) NOT NULL DEFAULT 'Draft' |
見下方說明 |
| Amount |
decimal(18,2) NOT NULL |
|
| Description |
varchar(500) NOT NULL |
費用說明 |
| VendorName |
varchar(200)? |
廠商名稱(VendorPayment 用) |
| CheckNumber |
varchar(50)? |
付款支票號碼 |
| ExpenseDate |
date NOT NULL |
費用日期 |
| ReceiptBlobPath |
varchar(500)? |
收據照片 Azure Blob 路徑 |
| Notes |
text? |
|
| SubmittedBy |
varchar(450)? |
FK → AspNetUsers.Id(申請報銷的同工) |
| SubmittedAt |
timestamp? |
|
| ReviewedBy |
varchar(450)? |
FK → AspNetUsers.Id(財務審核人) |
| ReviewedAt |
timestamp? |
|
| ReviewNotes |
varchar(500)? |
審核備注 |
| PaidAt |
timestamp? |
標記已付款時間 |
| PaidBy |
varchar(450)? |
FK → AspNetUsers.Id |
| FunctionalClass |
varchar(20)? |
IRS Form 990 功能性費用分類個別覆寫:'Program' | 'ManagementGeneral' | 'Fundraising';null = 繼承 Ministry.DefaultFunctionalClass |
| IsDeleted |
bool NOT NULL DEFAULT false |
|
| DeletedAt |
timestamp? |
|
| DeletedBy |
varchar(450)? |
FK → AspNetUsers.Id |
| CreatedAt |
timestamp NOT NULL |
|
| CreatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| UpdatedAt |
timestamp NOT NULL |
|
| UpdatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
有效功能性費用分類解析順序: Expense.FunctionalClass ?? Ministry.DefaultFunctionalClass ?? 'Program'(先取費用記錄的個別覆寫值;若為 null 則取所屬事工的預設值;仍為 null 則視為 'Program')。
Status 工作流程:
MonthlyStatement(月底對帳表)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| Year |
int NOT NULL |
|
| Month |
int NOT NULL |
1–12 |
| OpeningBalance |
decimal(18,2) NOT NULL |
期初餘額(手輸) |
| TotalGiving |
decimal(18,2) NOT NULL |
系統計算:本月奉獻合計 |
| TotalOtherIncome |
decimal(18,2) NOT NULL DEFAULT 0 |
其他收入(手輸) |
| TotalExpenses |
decimal(18,2) NOT NULL |
系統計算:本月已付支出合計 |
| CalculatedClosingBalance |
decimal(18,2) NOT NULL |
= OpeningBalance + TotalGiving + TotalOtherIncome − TotalExpenses |
| BankStatementBalance |
decimal(18,2) NOT NULL |
銀行對帳單期末餘額(手輸) |
| Difference |
decimal(18,2) NOT NULL |
= CalculatedClosingBalance − BankStatementBalance(目標 = 0) |
| Notes |
text? |
|
| IsFinalized |
bool NOT NULL DEFAULT false |
定稿後鎖定,不允許修改 |
| FinalizedAt |
timestamp? |
|
| FinalizedBy |
varchar(450)? |
FK → AspNetUsers.Id |
| CreatedAt |
timestamp NOT NULL |
|
| CreatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| UpdatedAt |
timestamp NOT NULL |
|
| UpdatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| UNIQUE |
(Year, Month) |
每個月只有一份月結報表 |
Form1099Box(IRS 1099 報告欄位目錄)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| BoxCode |
varchar(20) NOT NULL UNIQUE |
欄位代碼,如 "NEC-1"、"MISC-1" |
| Name_en |
varchar(200) NOT NULL |
英文欄位名稱 |
| Name_zh |
varchar(200)? |
中文欄位名稱 |
| FormType |
varchar(20) NOT NULL |
'1099-NEC' | '1099-MISC' |
| SortOrder |
int NOT NULL DEFAULT 0 |
顯示排序 |
| IsActive |
bool NOT NULL DEFAULT true |
|
| CreatedAt |
timestamp NOT NULL |
|
| CreatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| UpdatedAt |
timestamp NOT NULL |
|
| UpdatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
說明: IRS 1099 申報欄位目錄(catalog)。Seed 預設兩個欄位:NEC-1(Nonemployee compensation — 非員工報酬,1099-NEC 第 1 欄)與 MISC-1(Rents — 租金,1099-MISC 第 1 欄)。此表為唯讀參考資料,僅透過 seed 管理;新增欄位須更新 seed 並重新執行 migration。
Payee1099(1099 申報收款人主檔)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| LegalName |
varchar(200) NOT NULL |
IRS 法定全名(個人或公司) |
| DisplayName |
varchar(200)? |
顯示用簡稱(選填) |
| MemberId |
int? |
FK → Members.Id,ON DELETE SET NULL。收款人同時為教友時可選填關聯 |
| TaxClassification |
varchar(50) NOT NULL |
稅務分類,如 'Individual'、'SoleProprietor'、'Corporation'、'Partnership' 等 |
| Is1099Tracked |
bool NOT NULL DEFAULT true |
是否需要申報 1099 |
| TinType |
varchar(10)? |
'SSN' | 'EIN';null = 尚未收到 W-9 |
| TinEncrypted |
varchar(MAX)? |
TIN 加密密文(使用 ASP.NET Data Protection API 加密靜態儲存,明文永不入庫) |
| TinLast4 |
varchar(4)? |
TIN 末四碼明文(僅供遮罩顯示用,如 ***-**-1234) |
| AddressLine1 |
varchar(200)? |
|
| AddressLine2 |
varchar(200)? |
|
| City |
varchar(100)? |
|
| State |
varchar(50)? |
|
| Zip |
varchar(20)? |
|
| Email |
varchar(200)? |
|
| Phone |
varchar(30)? |
|
| W9Status |
varchar(20) NOT NULL DEFAULT 'Missing' |
'Missing' | 'Requested' | 'OnFile' | 'Expired' |
| W9ReceivedDate |
date? |
W-9 文件收到日期 |
| W9BlobPath |
varchar(500)? |
上傳的 W-9 文件 Azure Blob 路徑 |
| IsActive |
bool NOT NULL DEFAULT true |
|
| Notes |
text? |
內部備注 |
| IsDeleted |
bool NOT NULL DEFAULT false |
軟刪除 |
| DeletedAt |
timestamp? |
|
| DeletedBy |
varchar(450)? |
FK → AspNetUsers.Id |
| CreatedAt |
timestamp NOT NULL |
|
| CreatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| UpdatedAt |
timestamp NOT NULL |
|
| UpdatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
TIN 靜態加密(Encryption at Rest): 納稅識別碼(SSN / EIN)屬高敏感個人資料。TinEncrypted 欄位儲存使用 ASP.NET Data Protection API(IDataProtector)加密後的密文;TinLast4 僅儲存末四碼明文供前端遮罩顯示(***-**-XXXX)。明文 TIN 永遠不寫入資料庫,也不出現在 Audit Log 快照中。
現有表新增欄位(1099 歸屬)
以下欄位由 1099 功能新增至現有表,透過 EF Core Migration 套用:
Expenses(新增欄位)
| 欄位 |
型別 |
說明 |
| PayeeId |
int? |
FK → Payee1099s.Id,ON DELETE SET NULL。費用標題層級 1099 收款人歸屬;null = 不申報 1099 |
ExpenseSubCategories(新增欄位)
| 欄位 |
型別 |
說明 |
| Form1099BoxId |
int? |
FK → Form1099Boxes.Id,ON DELETE SET NULL。子項目層級 1099 申報欄位映射(優先於大類值) |
ExpenseCategoryGroups(新增欄位)
| 欄位 |
型別 |
說明 |
| Form1099BoxId |
int? |
FK → Form1099Boxes.Id,ON DELETE SET NULL。大類層級 1099 申報欄位備援映射 |
有效 1099 欄位解析順序: SubCategory.Form1099BoxId ?? Group.Form1099BoxId ?? null(先取子項目欄位;若為 null 則取大類欄位;仍為 null = 該費用不需申報 1099)。此解析邏輯與 Form 990 行號解析(SubCategory.Form990LineId ?? Group.Form990LineId ?? "24")平行,但語意不同:1099 的 null 代表「不申報」,而 990 的 null 會回退至行 "24"(其他費用)。
9. Prayer Requests(代禱)
PrayerRequest(代禱事項)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| RequestedByMemberId |
int NOT NULL |
FK → Members.Id(提交人) |
| Title |
varchar(300) NOT NULL |
|
| Content |
text NOT NULL |
|
| Visibility |
varchar(20) NOT NULL |
'Private' | 'CellGroup' | 'AllLeaders' | 'Public' |
| IsAnswered |
bool NOT NULL DEFAULT false |
已應允 |
| AnsweredAt |
timestamp? |
|
| ExpiresAt |
date? |
可選到期日(過期不顯示) |
| IsDeleted |
bool NOT NULL DEFAULT false |
|
| DeletedAt |
timestamp? |
|
| DeletedBy |
varchar(450)? |
FK → AspNetUsers.Id |
| CreatedAt |
timestamp NOT NULL |
|
| UpdatedAt |
timestamp NOT NULL |
|
Visibility 規則:
| Visibility |
可見對象 |
| Private |
僅本人 + pastor + super_admin |
| CellGroup |
本人所在小組成員 + cell_leader + district_leader + pastor |
| AllLeaders |
ministry_leader / district_leader / coworker_chair / board_member / pastor 及以上 |
| Public |
全體教友(登入後可見) |
PrayerFollow(代禱跟進記錄)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| PrayerRequestId |
int NOT NULL |
FK → PrayerRequests.Id |
| FollowedByUserId |
varchar(450) NOT NULL |
FK → AspNetUsers.Id(牧者/組長) |
| Note |
text? |
跟進備注 |
| CreatedAt |
timestamp NOT NULL |
|
10. Audit Log
AuditLog(稽核記錄 — 不可修改/刪除)
| 欄位 |
型別 |
說明 |
| Id |
bigint PK |
使用 bigint(記錄量大) |
| UserId |
varchar(450)? |
FK → AspNetUsers.Id;null = 系統自動操作 |
| Action |
varchar(50) NOT NULL |
'Create' | 'Update' | 'Delete' | 'Login' | 'Logout' | 'Export' | 'ViewSensitive' |
| EntityType |
varchar(100) NOT NULL |
實體名稱(如 'Member', 'Giving', 'Expense') |
| EntityId |
varchar(100)? |
受影響實體的主鍵值 |
| OldValues |
jsonb? |
修改前的 JSON 快照 |
| NewValues |
jsonb? |
修改後的 JSON 快照 |
| IpAddress |
varchar(45)? |
IPv4 或 IPv6 |
| UserAgent |
varchar(500)? |
瀏覽器/App 資訊 |
| CreatedAt |
timestamp NOT NULL DEFAULT now() |
|
重要: AuditLog 永遠只有 INSERT,永遠不 UPDATE 或 DELETE。
11. Notifications
NotificationLog(通知發送記錄)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| UserId |
varchar(450)? |
FK → AspNetUsers.Id |
| MemberId |
int? |
FK → Members.Id |
| Channel |
varchar(20) NOT NULL |
'Email' | 'Push' | 'SMS' | 'InApp' |
| TemplateKey |
varchar(100) NOT NULL |
通知模板 Key(如 'giving.receipt.sent') |
| Subject |
varchar(300)? |
Email 主旨 |
| SentToAddress |
varchar(200)? |
Email / 手機號 / FCM Token |
| Status |
varchar(20) NOT NULL |
'Sent' | 'Failed' | 'Queued' |
| ErrorMessage |
varchar(500)? |
失敗原因 |
| RelatedEntityType |
varchar(100)? |
關聯實體類型 |
| RelatedEntityId |
varchar(100)? |
關聯實體 ID |
| CreatedAt |
timestamp NOT NULL DEFAULT now() |
|
12. Service Roster(服事表 — Phase 2)
ServiceSlot(服事項目定義)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| MinistryId |
int NOT NULL |
FK → Ministries.Id |
| Name_en |
varchar(200) NOT NULL |
e.g., 'Worship Leader' |
| Name_zh |
varchar(200)? |
e.g., '敬拜帶領' |
| RequiredCount |
int NOT NULL DEFAULT 1 |
每次需要幾人 |
| IsActive |
bool NOT NULL DEFAULT true |
|
| SortOrder |
int NOT NULL DEFAULT 0 |
|
Seed 服事項目(16 個主日服事槽,參見 PLANNING.md §3.4):
| MinistryId |
Name_en |
Name_zh |
| 2 Preaching |
Preacher |
講員 |
| 3 Emcee |
Emcee |
司會 |
| 4 Worship |
Worship Leader |
敬拜帶領 |
| 4 Worship |
Keyboard / Piano |
鍵盤/鋼琴 |
| 4 Worship |
Guitar |
吉他 |
| 4 Worship |
Bass |
貝斯 |
| 4 Worship |
Drums |
爵士鼓 |
| 4 Worship |
Vocalist |
詩班 |
| 5 PPT/Media |
PPT Operator |
PPT 操作 |
| 5 PPT/Media |
Livestream |
直播 |
| 6 Sound |
Sound Engineer |
音控 |
| 7 Facility |
Setup Lead |
場地佈置組長 |
| 7 Facility |
Setup Team |
場地佈置組員 |
| 8 Hospitality |
Greeter |
招待接待 |
| 9 Children |
Children's Teacher |
兒童老師 |
| 10 Catering |
Agape Meal Coord. |
愛宴負責人 |
ServiceAssignment(服事排班記錄)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| ServiceSlotId |
int NOT NULL |
FK → ServiceSlots.Id |
| MemberId |
int NOT NULL |
FK → Members.Id |
| ServiceDate |
date NOT NULL |
服事日期(通常為主日) |
| Status |
varchar(20) NOT NULL DEFAULT 'Scheduled' |
'Scheduled' | 'Confirmed' | 'Absent' | 'Replaced' |
| ReplacedByMemberId |
int? |
FK → Members.Id(替補人選) |
| Notes |
varchar(200)? |
|
| ReminderSentAt |
timestamp? |
提醒通知發送時間 |
| CreatedAt |
timestamp NOT NULL |
|
| CreatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| UpdatedAt |
timestamp NOT NULL |
|
| UpdatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
13. Sunday Attendance(主日出席 — Phase 2)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| AttendanceDate |
date NOT NULL UNIQUE |
主日日期 |
| AdultCount |
int NOT NULL DEFAULT 0 |
大人 |
| TeenagerCount |
int NOT NULL DEFAULT 0 |
青少年 |
| ChildrenCount |
int NOT NULL DEFAULT 0 |
兒童 |
| TotalCount |
int NOT NULL |
計算欄位(= Adult + Teenager + Children) |
| Notes |
varchar(500)? |
|
| RecordedByUserId |
varchar(450) NOT NULL |
FK → AspNetUsers.Id(行政秘書) |
| CreatedAt |
timestamp NOT NULL |
|
| UpdatedAt |
timestamp NOT NULL |
|
14. Cell Groups(小組 — Phase 2)
CellGroup(小組)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| Name_en |
varchar(200) NOT NULL |
|
| Name_zh |
varchar(200)? |
|
| ParentGroupId |
int? |
FK → CellGroups.Id(巢狀小組) |
| LeaderMemberId |
int? |
FK → Members.Id(組長) |
| CoLeaderMemberId |
int? |
FK → Members.Id(副組長) |
| MeetingDay |
varchar(20)? |
'Monday' | 'Tuesday' | ... |
| MeetingTime |
time? |
|
| MeetingLocation |
varchar(200)? |
|
| Description_en |
text? |
|
| Description_zh |
text? |
|
| IsActive |
bool NOT NULL DEFAULT true |
|
| CreatedAt |
timestamp NOT NULL |
|
| CreatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| UpdatedAt |
timestamp NOT NULL |
|
| UpdatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
CellGroupMembership(小組成員 — M:N)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| CellGroupId |
int NOT NULL |
FK → CellGroups.Id |
| MemberId |
int NOT NULL |
FK → Members.Id |
| JoinedAt |
date? |
|
| LeftAt |
date? |
null = 仍在小組中 |
| IsActive |
bool NOT NULL DEFAULT true |
|
| UNIQUE |
(CellGroupId, MemberId) WHERE IsActive = true |
|
CellGroupMeeting(小組聚會記錄)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| CellGroupId |
int NOT NULL |
FK → CellGroups.Id |
| MeetingDate |
date NOT NULL |
|
| Topic |
varchar(300)? |
聚會主題 |
| Notes |
text? |
|
| CreatedAt |
timestamp NOT NULL |
|
| CreatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
CellGroupAttendance(小組出席 — 個人層級)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| MeetingId |
int NOT NULL |
FK → CellGroupMeetings.Id |
| MemberId |
int NOT NULL |
FK → Members.Id |
| IsPresent |
bool NOT NULL DEFAULT true |
出席 / 缺席 |
| Notes |
varchar(200)? |
|
| UNIQUE |
(MeetingId, MemberId) |
|
15. Ministry Budget(事工預算 — Phase 3)
| 欄位 |
型別 |
說明 |
| Id |
int PK |
|
| MinistryId |
int NOT NULL |
FK → Ministries.Id |
| CategoryGroupId |
int NOT NULL |
FK → ExpenseCategoryGroups.Id |
| SubCategoryId |
int? |
FK → ExpenseSubCategories.Id;null = 整個大類的預算 |
| FiscalYear |
int NOT NULL |
財政年度(如 2027) |
| BudgetAmount |
decimal(18,2) NOT NULL |
|
| Notes |
varchar(500)? |
|
| CreatedAt |
timestamp NOT NULL |
|
| CreatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| UpdatedAt |
timestamp NOT NULL |
|
| UpdatedBy |
varchar(450) NOT NULL |
FK → AspNetUsers.Id |
| UNIQUE |
(MinistryId, CategoryGroupId, SubCategoryId, FiscalYear) |
|
備注: 此表 Phase 3 才建立,但 ExpenseCategoryGroups、ExpenseSubCategories、Ministries 表已在 Phase 1 建好,無需 Schema 改動。
16. Seed Data
以下資料在 DbInitializer(或 HasData Migration)中植入:
Roles(角色)
Ministries(12 個事工部門)
GivingCategories(奉獻類型)
ExpenseCategoryGroups(14 個大類)
Form990 權限模組
Form1099 權限模組
CmsPages(靜態頁面 Slug)
17. Indexes
18. EF Core 設定摘要
DbContext 範例結構
重要 EF Core 慣例
文件由 ROLAC 開發團隊維護。如需更新 Schema,必須同步更新此文件和 EF Core Migration。