Files
Chris Chen ad276c01f3 docs(1099): document Payee1099s/Form1099Boxes schema and seed Form1099 permissions
- DB_SCHEMA.md §8: add Form1099Box catalog table, Payee1099 recipient master
  (with TIN at-rest encryption note), and new FK columns on Expenses /
  ExpenseSubCategories / ExpenseCategoryGroups; update TOC and Seed Data section
- DbSeeder.cs: grant Modules.Form1099 to finance (R/W/D), pastor (R), and
  board_member (R), mirroring the Form990Report + Disbursements pattern;
  idempotent (only inserts if row absent, never clobbers admin edits)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 17:56:09 -07:00

47 KiB
Raw Permalink Blame History

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 欄位)


目錄

  1. 設計原則
  2. BaseEntity 模式
  3. Phase 1 — Authentication & Identity
  4. Phase 1 — Member Management
  5. Phase 1 — Ministry(事工部門)
  6. Phase 1 — CMS
  7. Phase 1 — Giving & Donations(奉獻)
  8. Phase 1 — Expense Tracking(支出)
  9. Phase 1 — Prayer Requests(代禱)
  10. Phase 1 — Audit Log
  11. Phase 1 — Notifications
  12. Phase 2 — Service Roster(服事表)
  13. Phase 2 — Sunday Attendance(主日出席)
  14. Phase 2 — Cell Groups(小組)
  15. Phase 3 — Ministry Budget(事工預算)
  16. Seed Data
  17. Indexes
  18. 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 實現):

// 帶 Audit 的實體(可建立/修改)
public abstract class AuditableEntity
{
    public DateTime CreatedAt { get; set; }
    public string CreatedBy { get; set; } = null!;  // FK → AppUser.Id
    public DateTime UpdatedAt { get; set; }
    public string UpdatedBy { get; set; } = null!;  // FK → AppUser.Id
}

// 帶軟刪除的實體
public abstract class SoftDeleteEntity : AuditableEntity
{
    public bool IsDeleted { get; set; } = false;
    public DateTime? DeletedAt { get; set; }
    public string? DeletedBy { get; set; }  // FK → AppUser.Id
}

3. Authentication & Identity

AppUser(繼承 IdentityUser

Table: AspNetUsers (ASP.NET Identity 預設名稱)
欄位 型別 說明
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

Table: AspNetRoles
欄位 型別 說明
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 訪客 僅限公開頁面

UserMinistryMinistry Scope — 用戶管理哪個事工)

Table: UserMinistries
欄位 型別 說明
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) 一個用戶不重複指派同一事工

UserDeviceFCM 推播 Token

Table: UserDevices
欄位 型別 說明
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(教友資料)

Table: Members
欄位 型別 說明
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(家庭單元)

Table: FamilyUnits
欄位 型別 說明
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 可有多個 MembersOne-to-Many via Member.FamilyUnitId

MemberMinistry(教友服事事工關聯 — M:N)

Table: MemberMinistries
欄位 型別 說明
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(教友標籤)

Table: MemberTags
欄位 型別 說明
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(事工部門)

Table: Ministries
欄位 型別 說明
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'。SeedAdministration → 'ManagementGeneral',其餘 → 'Program'

6. CMS

CmsPage(靜態頁面:關於我們、異象等)

Table: CmsPages
欄位 型別 說明
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(消息公告)

Table: Announcements
欄位 型別 說明
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 嵌入)

Table: SermonVideos
欄位 型別 說明
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(聯絡表單)

Table: ContactInquiries
欄位 型別 說明
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(奉獻類型)

Table: GivingCategories
欄位 型別 說明
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(主日奉獻袋批次作業)

Table: OfferingSessions
欄位 型別 說明
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(奉獻記錄)

Table: Givings
欄位 型別 說明
Id int PK
MemberId int? FK → Members.Idnull = 匿名
GivingCategoryId int NOT NULL FK → GivingCategories.Id
OfferingSessionId int? FK → OfferingSessions.Idnull = 非批次單筆
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 IDPhase 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 = trueMemberId = null)不計入個人年度收據。
PayPal Amount = GrossAmount - FeeAmountNet Amount 才用於 IRS)。

GivingReceipt(年度 IRS 奉獻收據)

Table: GivingReceipts
欄位 型別 說明
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)

Table: GivingRecurringSchedules
欄位 型別 說明
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(支出)

Form990ExpenseLinesIRS Form 990 Part IX 自然費用科目)

Table: Form990ExpenseLines
欄位 型別 說明
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(支出大類)

Table: ExpenseCategoryGroups
欄位 型別 說明
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.IdON 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(支出子項目)

Table: ExpenseSubCategories
欄位 型別 說明
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.IdON 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(支出記錄)

Table: Expenses
欄位 型別 說明
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 工作流程:

VendorPayment:
  財務直接建立 → Status = 'Paid'(無需審核)

StaffReimbursement:
  同工提交 → 'Draft'
  提交審核 → 'PendingApproval'
  財務審核通過 → 'Approved'
  標記已還款 → 'Paid'
  財務拒絕 → 'Rejected'

MonthlyStatement(月底對帳表)

Table: MonthlyStatements
欄位 型別 說明
Id int PK
Year int NOT NULL
Month int NOT NULL 112
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) 每個月只有一份月結報表

Form1099BoxIRS 1099 報告欄位目錄)

Table: Form1099Boxes
欄位 型別 說明
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-1Nonemployee compensation — 非員工報酬,1099-NEC 第 1 欄)與 MISC-1Rents — 租金,1099-MISC 第 1 欄)。此表為唯讀參考資料,僅透過 seed 管理;新增欄位須更新 seed 並重新執行 migration。

Payee10991099 申報收款人主檔)

Table: Payee1099s
欄位 型別 說明
Id int PK
LegalName varchar(200) NOT NULL IRS 法定全名(個人或公司)
DisplayName varchar(200)? 顯示用簡稱(選填)
MemberId int? FK → Members.IdON 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 APIIDataProtector)加密後的密文;TinLast4 僅儲存末四碼明文供前端遮罩顯示(***-**-XXXX)。明文 TIN 永遠不寫入資料庫,也不出現在 Audit Log 快照中。

現有表新增欄位(1099 歸屬)

以下欄位由 1099 功能新增至現有表,透過 EF Core Migration 套用:

Expenses(新增欄位)

欄位 型別 說明
PayeeId int? FK → Payee1099s.IdON DELETE SET NULL。費用標題層級 1099 收款人歸屬;null = 不申報 1099

ExpenseSubCategories(新增欄位)

欄位 型別 說明
Form1099BoxId int? FK → Form1099Boxes.IdON DELETE SET NULL。子項目層級 1099 申報欄位映射(優先於大類值)

ExpenseCategoryGroups(新增欄位)

欄位 型別 說明
Form1099BoxId int? FK → Form1099Boxes.IdON 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(代禱事項)

Table: PrayerRequests
欄位 型別 說明
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(代禱跟進記錄)

Table: PrayerFollows
欄位 型別 說明
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(稽核記錄 — 不可修改/刪除)

Table: AuditLogs
欄位 型別 說明
Id bigint PK 使用 bigint(記錄量大)
UserId varchar(450)? FK → AspNetUsers.Idnull = 系統自動操作
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(通知發送記錄)

Table: NotificationLogs
欄位 型別 說明
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(服事項目定義)

Table: ServiceSlots
欄位 型別 說明
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(服事排班記錄)

Table: ServiceAssignments
欄位 型別 說明
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

Table: SundayAttendances
欄位 型別 說明
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(小組)

Table: CellGroups
欄位 型別 說明
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

Table: CellGroupMemberships
欄位 型別 說明
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(小組聚會記錄)

Table: CellGroupMeetings
欄位 型別 說明
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(小組出席 — 個人層級)

Table: CellGroupAttendances
欄位 型別 說明
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

Table: MinistryBudgets
欄位 型別 說明
Id int PK
MinistryId int NOT NULL FK → Ministries.Id
CategoryGroupId int NOT NULL FK → ExpenseCategoryGroups.Id
SubCategoryId int? FK → ExpenseSubCategories.Idnull = 整個大類的預算
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 才建立,但 ExpenseCategoryGroupsExpenseSubCategoriesMinistries 表已在 Phase 1 建好,無需 Schema 改動。


16. Seed Data

以下資料在 DbInitializer(或 HasData Migration)中植入:

Roles(角色)

super_admin, pastor, board_member, coworker_chair, ministry_leader, district_leader, cell_leader, coworker, finance, secretary, worship_leader, member, visitor

Ministries12 個事工部門)

1. Administration / 行政           DefaultFunctionalClass = 'ManagementGeneral'
2. Preaching / 講道                DefaultFunctionalClass = 'Program'
3. Emcee / 司會                    DefaultFunctionalClass = 'Program'
4. Worship / 敬拜                  DefaultFunctionalClass = 'Program'
5. PPT/Media / PPT/影音            DefaultFunctionalClass = 'Program'
6. Sound / 音控                    DefaultFunctionalClass = 'Program'
7. Facility / 場地組               DefaultFunctionalClass = 'Program'
8. Hospitality / 招待              DefaultFunctionalClass = 'Program'
9. Children / 兒牧                 DefaultFunctionalClass = 'Program'
10. Catering / 餐飲                DefaultFunctionalClass = 'Program'
11. Cell Groups / 小組牧養         DefaultFunctionalClass = 'Program'
12. Special Events / 特別活動      DefaultFunctionalClass = 'Program'

GivingCategories(奉獻類型)

1. Tithe / 什一奉獻
2. General Offering / 一般奉獻
3. Special Offering / 特別奉獻
4. Building Fund / 建堂基金
5. Mission / 宣教奉獻

ExpenseCategoryGroups14 個大類)

見 §8 Seed 大類列表(含新增 Professional Services、Information Technology、Finance & Banking

Form990 權限模組

Form990Report — 唯讀報表權限,授予角色:finance、pastor、board_member

Form1099 權限模組

Form1099 — 1099 收款人管理與申報,授予角色:
  finance       — Read / Write / Delete(完整管理)
  pastor        — Read(唯讀總覽)
  board_member  — Read(唯讀總覽)

CmsPages(靜態頁面 Slug

about, vision, service-times, contact

17. Indexes

-- Members
CREATE INDEX idx_members_status ON "Members" ("Status") WHERE "IsDeleted" = false;
CREATE INDEX idx_members_family_unit ON "Members" ("FamilyUnitId");
CREATE INDEX idx_members_email ON "Members" ("Email") WHERE "Email" IS NOT NULL;

-- Giving
CREATE INDEX idx_givings_member_date ON "Givings" ("MemberId", "GivingDate");
CREATE INDEX idx_givings_session ON "Givings" ("OfferingSessionId") WHERE "OfferingSessionId" IS NOT NULL;
CREATE INDEX idx_givings_date ON "Givings" ("GivingDate");

-- Expense
CREATE INDEX idx_expenses_ministry ON "Expenses" ("MinistryId");
CREATE INDEX idx_expenses_status ON "Expenses" ("Status") WHERE "IsDeleted" = false;
CREATE INDEX idx_expenses_date ON "Expenses" ("ExpenseDate");

-- AuditLog
CREATE INDEX idx_auditlog_user ON "AuditLogs" ("UserId");
CREATE INDEX idx_auditlog_entity ON "AuditLogs" ("EntityType", "EntityId");
CREATE INDEX idx_auditlog_created ON "AuditLogs" ("CreatedAt");

-- PrayerRequest
CREATE INDEX idx_prayer_visibility ON "PrayerRequests" ("Visibility") WHERE "IsDeleted" = false;
CREATE INDEX idx_prayer_member ON "PrayerRequests" ("RequestedByMemberId");

-- Phase 2: ServiceAssignment
CREATE INDEX idx_service_assign_date ON "ServiceAssignments" ("ServiceDate");
CREATE INDEX idx_service_assign_member ON "ServiceAssignments" ("MemberId");

-- Phase 2: SundayAttendance
-- UNIQUE constraint on AttendanceDate already creates an index

18. EF Core 設定摘要

DbContext 範例結構

public class RolacDbContext : IdentityDbContext<AppUser, AppRole, string>
{
    // Phase 1
    public DbSet<Member> Members => Set<Member>();
    public DbSet<FamilyUnit> FamilyUnits => Set<FamilyUnit>();
    public DbSet<MemberMinistry> MemberMinistries => Set<MemberMinistry>();
    public DbSet<MemberTag> MemberTags => Set<MemberTag>();
    public DbSet<Ministry> Ministries => Set<Ministry>();
    public DbSet<UserMinistry> UserMinistries => Set<UserMinistry>();
    public DbSet<UserDevice> UserDevices => Set<UserDevice>();

    public DbSet<Announcement> Announcements => Set<Announcement>();
    public DbSet<SermonVideo> SermonVideos => Set<SermonVideo>();
    public DbSet<CmsPage> CmsPages => Set<CmsPage>();
    public DbSet<ContactInquiry> ContactInquiries => Set<ContactInquiry>();

    public DbSet<GivingCategory> GivingCategories => Set<GivingCategory>();
    public DbSet<OfferingSession> OfferingSessions => Set<OfferingSession>();
    public DbSet<Giving> Givings => Set<Giving>();
    public DbSet<GivingReceipt> GivingReceipts => Set<GivingReceipt>();
    public DbSet<GivingRecurringSchedule> GivingRecurringSchedules => Set<GivingRecurringSchedule>();

    public DbSet<Form990ExpenseLine> Form990ExpenseLines => Set<Form990ExpenseLine>();
    public DbSet<ExpenseCategoryGroup> ExpenseCategoryGroups => Set<ExpenseCategoryGroup>();
    public DbSet<ExpenseSubCategory> ExpenseSubCategories => Set<ExpenseSubCategory>();
    public DbSet<Expense> Expenses => Set<Expense>();
    public DbSet<MonthlyStatement> MonthlyStatements => Set<MonthlyStatement>();

    public DbSet<PrayerRequest> PrayerRequests => Set<PrayerRequest>();
    public DbSet<PrayerFollow> PrayerFollows => Set<PrayerFollow>();

    public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
    public DbSet<NotificationLog> NotificationLogs => Set<NotificationLog>();

    // Phase 2 (schema defined now, implemented later)
    public DbSet<ServiceSlot> ServiceSlots => Set<ServiceSlot>();
    public DbSet<ServiceAssignment> ServiceAssignments => Set<ServiceAssignment>();
    public DbSet<SundayAttendance> SundayAttendances => Set<SundayAttendance>();
    public DbSet<CellGroup> CellGroups => Set<CellGroup>();
    public DbSet<CellGroupMembership> CellGroupMemberships => Set<CellGroupMembership>();
    public DbSet<CellGroupMeeting> CellGroupMeetings => Set<CellGroupMeeting>();
    public DbSet<CellGroupAttendance> CellGroupAttendances => Set<CellGroupAttendance>();

    // Phase 3
    public DbSet<MinistryBudget> MinistryBudgets => Set<MinistryBudget>();

    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        // Apply all entity configurations from assembly
        builder.ApplyConfigurationsFromAssembly(typeof(RolacDbContext).Assembly);

        // Global soft-delete filter
        builder.Entity<Member>().HasQueryFilter(m => !m.IsDeleted);
        builder.Entity<Expense>().HasQueryFilter(e => !e.IsDeleted);
        builder.Entity<PrayerRequest>().HasQueryFilter(p => !p.IsDeleted);

        // Decimal precision
        foreach (var property in builder.Model.GetEntityTypes()
            .SelectMany(t => t.GetProperties())
            .Where(p => p.ClrType == typeof(decimal) || p.ClrType == typeof(decimal?)))
        {
            property.SetColumnType("decimal(18,2)");
        }
    }
}

重要 EF Core 慣例

// 軟刪除攔截 (SaveChangesInterceptor)
// 在 SaveChanges 前,如果 EntityState = Deleted 且實體繼承 SoftDeleteEntity
// 改成 Modified 並設定 IsDeleted = true, DeletedAt = now(), DeletedBy = currentUser

// Audit 自動填充 (SaveChangesInterceptor)
// 在 SaveChanges 前,自動填充 CreatedAt/CreatedBy(新增)和 UpdatedAt/UpdatedBy(修改)
// CurrentUser 透過 IHttpContextAccessor 取得

// AuditLog 攔截 (SaveChangesInterceptor)
// 記錄所有 Create/Update/Delete 動作到 AuditLogs 表
// OldValues / NewValues 使用 JsonSerializer.Serialize(entry.OriginalValues.ToObject())

文件由 ROLAC 開發團隊維護。如需更新 Schema,必須同步更新此文件和 EF Core Migration。