1173 lines
42 KiB
Markdown
1173 lines
42 KiB
Markdown
# 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. [設計原則](#1-設計原則)
|
||
2. [BaseEntity 模式](#2-baseentity-模式)
|
||
3. [Phase 1 — Authentication & Identity](#3-authentication--identity)
|
||
4. [Phase 1 — Member Management](#4-member-management)
|
||
5. [Phase 1 — Ministry(事工部門)](#5-ministry-事工部門)
|
||
6. [Phase 1 — CMS](#6-cms)
|
||
7. [Phase 1 — Giving & Donations(奉獻)](#7-giving--donations-奉獻)
|
||
8. [Phase 1 — Expense Tracking(支出)](#8-expense-tracking-支出)
|
||
9. [Phase 1 — Prayer Requests(代禱)](#9-prayer-requests-代禱)
|
||
10. [Phase 1 — Audit Log](#10-audit-log)
|
||
11. [Phase 1 — Notifications](#11-notifications)
|
||
12. [Phase 2 — Service Roster(服事表)](#12-service-roster-服事表)
|
||
13. [Phase 2 — Sunday Attendance(主日出席)](#13-sunday-attendance-主日出席)
|
||
14. [Phase 2 — Cell Groups(小組)](#14-cell-groups-小組)
|
||
15. [Phase 3 — Ministry Budget(事工預算)](#15-ministry-budget-事工預算)
|
||
16. [Seed Data](#16-seed-data)
|
||
17. [Indexes](#17-indexes)
|
||
18. [EF Core 設定摘要](#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 實現):
|
||
|
||
```csharp
|
||
// 帶 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 | 訪客 | 僅限公開頁面 |
|
||
|
||
### UserMinistry(Ministry 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) | 一個用戶不重複指派同一事工 |
|
||
|
||
### UserDevice(FCM 推播 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 可有多個 Members(One-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'。Seed:Administration → '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.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 奉獻收據)
|
||
|
||
```
|
||
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(支出)
|
||
|
||
### Form990ExpenseLines(IRS 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.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(支出子項目)
|
||
|
||
```
|
||
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.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(支出記錄)
|
||
|
||
```
|
||
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 | 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) | 每個月只有一份月結報表 |
|
||
|
||
---
|
||
|
||
## 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.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(通知發送記錄)
|
||
|
||
```
|
||
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.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(角色)
|
||
```
|
||
super_admin, pastor, board_member, coworker_chair, ministry_leader, district_leader, cell_leader, coworker, finance, secretary, worship_leader, member, visitor
|
||
```
|
||
|
||
### Ministries(12 個事工部門)
|
||
```
|
||
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 / 宣教奉獻
|
||
```
|
||
|
||
### ExpenseCategoryGroups(14 個大類)
|
||
```
|
||
見 §8 Seed 大類列表(含新增 Professional Services、Information Technology、Finance & Banking)
|
||
```
|
||
|
||
### Form990 權限模組
|
||
```
|
||
Form990Report — 唯讀報表權限,授予角色:finance、pastor、board_member
|
||
```
|
||
|
||
### CmsPages(靜態頁面 Slug)
|
||
```
|
||
about, vision, service-times, contact
|
||
```
|
||
|
||
---
|
||
|
||
## 17. Indexes
|
||
|
||
```sql
|
||
-- 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 範例結構
|
||
|
||
```csharp
|
||
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 慣例
|
||
|
||
```csharp
|
||
// 軟刪除攔截 (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。*
|