永輝資料中央廚房資料表-查帳系統
Data Central Kitchen(資料中央廚房)概念說明
「Data Central Kitchen」是一個企業級 AI 數據中台 + 文件自動化處理中心的架構概念。
就像餐飲業的中央廚房負責統一備料、加工、分送到各分店,Data Central Kitchen 則負責:
接收各種來源文件 → AI辨識/清洗/轉換 → 數據標準化 → 自動路由至 ERP/CRM/稅務/薪資系統
這種架構非常適合像永輝這種 跨國、多ERP、多法規、多語言的會計師事務所集團。
Table Design 命名原則
ndividual Table 的命名不要用”_” underline,每個Table 名字只 用一個英文或2個英文字連在一起,只有Junction Table 才用”_” underline
✅ Individual Table:不用 _
✅ 只用 1 個英文或 2 個英文連在一起
✅ Junction Table:才使用 _
1. Standard 標準版
可規模化:可反覆跑、可跨案複用、可自動檢錯
目標: 讓 Data Central Kitchen Hub 不只是上傳入口,而是「資料取得管線」:定義格式、欄位 mapping、驗證明細、DRP/ETL 任務、例外處理、狀態歷程都可追溯。
只列 9 個核心(Engagement / Entity / User / Document 假設已有)。
1.1 Request
| 欄位 | 型態 |
|---|---|
| id (PK) | uuid |
| engagementId | uuid FK → Engagement |
| entityId | uuid FK → Entity |
| requestCode | text UNIQUE (per engagement) |
| datasetType | text |
| periodStart | date |
| periodEnd | date |
| instruction | text |
| requiredFields | jsonb |
| preferredFormat | text |
| deliveryMethod | text |
| priority | smallint |
| status | text |
| dueAt | timestamptz |
| ownerUserId | uuid FK → User |
| createdAt | timestamptz |
| createdBy | uuid |
| updatedAt | timestamptz |
| updatedBy | uuid |
1.2 RequestItem
| 欄位 | 型態 |
|---|---|
| id (PK) | uuid |
| requestId | uuid FK → Request |
| itemCode | text |
| itemName | text |
| datasetSubtype | text |
| schemaVersion | text |
| requiredFields | jsonb |
| optionalFields | jsonb |
| sampleDocumentId | uuid FK → Document |
| expectedRowCount | int |
| status | text |
| dueAt | timestamptz |
| notes | text |
| createdAt | timestamptz |
| createdBy | uuid |
| updatedAt | timestamptz |
| updatedBy | uuid |
1.3 Submission
| 欄位 | 型態 |
|---|---|
| id (PK) | uuid |
| requestId | uuid FK → Request |
| submissionNo | int |
| submittedByUserId | uuid FK → User |
| submittedByType | text |
| submittedAt | timestamptz |
| channel | text |
| sourceSystem | text |
| status | text |
| notes | text |
| acceptedAt | timestamptz |
| acceptedByUserId | uuid |
| rejectionReason | text |
| createdAt | timestamptz |
| updatedAt | timestamptz |
UNIQUE(requestId, submissionNo)
1.4 Mapping
| 欄位 | 型態 |
|---|---|
| id (PK) | uuid |
| engagementId | uuid |
| entityId | uuid |
| datasetType | text |
| mappingVersion | text |
| isActive | boolean |
| effectiveFrom | date |
| effectiveTo | date |
| approvedByUserId | uuid |
| approvedAt | timestamptz |
| createdAt | timestamptz |
| createdBy | uuid |
| updatedAt | timestamptz |
| updatedBy | uuid |
1.5 ValidationResult
| 欄位 | 型態 |
|---|---|
| id (PK) | uuid |
| submissionItemId | uuid FK → Submission_Item |
| validationProfile | text |
| passed | boolean |
| severityMax | text |
| issueCountTotal | int |
| issueCountError | int |
| issueCountWarn | int |
| qualityScore | smallint |
| summary | jsonb |
| validatedAt | timestamptz |
| validatedByType | text |
| validatedByUserId | uuid |
| createdAt | timestamptz |
1.6 ValidationIssue
| 欄位 | 型態 |
|---|---|
| id (PK) | uuid |
| validationResultId | uuid FK → ValidationResult |
| issueType | text |
| severity | text |
| fieldName | text |
| rowRef | text |
| expectedValue | text |
| actualValue | text |
| message | text |
| fixSuggestion | text |
| status | text |
| handledByUserId | uuid |
| handledAt | timestamptz |
| resolutionNote | text |
| createdAt | timestamptz |
1.7 Batch
| 欄位 | 型態 |
|---|---|
| id (PK) | uuid |
| engagementId | uuid |
| submissionItemId | uuid FK → Submission_Item |
| datasetType | text |
| importMode | text |
| isLatest | boolean |
| importedAt | timestamptz |
| importedByUserId | uuid |
| rowInserted | int |
| rowUpdated | int |
| rowRejected | int |
| notes | text |
| createdAt | timestamptz |
1.8 JobRun
| 欄位 | 型態 |
|---|---|
| id (PK) | uuid |
| engagementId | uuid |
| jobCode | text |
| submissionItemId | uuid |
| batchId | uuid |
| triggeredByUserId | uuid |
| triggeredAt | timestamptz |
| status | text |
| parameters | jsonb |
| startedAt | timestamptz |
| finishedAt | timestamptz |
| runtimeSeconds | int |
| errorCode | text |
| errorMessage | text |
| retryCount | int |
| createdAt | timestamptz |
1.9 Submission_Item(Junction Table)
| 欄位 |
|---|
| id (PK) |
| submissionId FK → Submission |
| requestItemId FK → RequestItem |
| status |
| validatedAt |
| acceptedAt |
| rowCount |
| currencyCode |
| checksum |
| notes |
| createdAt |
| updatedAt |
UNIQUE(submissionId, requestItemId)
1.10 Submission_Document(Junction Table)
| submissionId |
| requestItemId |
| documentId |
| fileRole |
| linkedAt |
| linkedByUserId |
| versionNote |
1.11 JobRun_Document(Junction Table)
| jobRunId |
| documentId |
| docRole |
| linkedAt |
| linkedByUserId |
| notes |
1.12 Mapping_Line(Junction Table)
| id |
| mappingId |
| sourceField |
| targetField |
| transformRule |
| requiredFlag |
| defaultValue |
| validationRule |
| sortOrder |
| createdAt |
| createdBy |
2.Enterprise 版本:
新增 tables 清單(+10 → 合計 22)
新增 Individual tables(不含 _)
- Tenant(併購後多事務所共用 / 多租戶隔離)
- ExternalUser(客戶/外部上傳者帳號)
- ExternalRole(外部角色)
- ExternalPerm(外部權限粒度)
- AccessPolicy(可見性/授權規則,支援 Row-level)
- AuditEvent(不可竄改稽核事件)
- RetentionPolicy(保存/刪除策略:國別/客戶等級)
- KeyRef(客戶級加密/金鑰參照)
- KpiSnapshot(KPI 快照:週期、補件率、失敗率等)
- NotifyOutbox(通知 Outbox:Email/SMS/Webhook)
新增 Junction tables(允許 _)
- ExternalUser_Role(ExternalUser ↔ ExternalRole)
- ExternalRole_Perm(ExternalRole ↔ ExternalPerm)
你原本列的 external_user_permission 若一定要有,也可以改成 junction:ExternalUser_Perm(同規則用
_),但我建議用「Role 為主」,User 例外授權再加這張即可(可選)。
2.1 Tenant(多租戶 / 併購後共用的根)
PK: id
| 欄位 | 型態 | 必填 | 說明 |
|---|---|---|---|
| id | uuid | Y | PK |
| tenantCode | text | Y | 例 EVTW / EVJP / PartnerMX |
| tenantName | text | Y | 顯示名稱 |
| status | text | Y | active/suspended/closed |
| region | text | N | APAC/EU/US… |
| defaultTz | text | Y | 例 Asia/Taipei |
| createdAt | timestamptz | Y | |
| createdBy | uuid | Y |
Unique: (tenantCode)
Index: (status)
設計建議(很重要):Standard 的表全部加
tenantId,並做(tenantId, id)的查詢索引;外部登入也要綁 tenant。
2.2 ExternalUser(外部使用者)
PK: id
FK: tenantId → Tenant(id)
| 欄位 | 型態 | 必填 | 說明 |
|---|---|---|---|
| id | uuid | Y | PK |
| tenantId | uuid | Y | 多租戶 |
| text | Y | 登入帳號(建議全小寫) | |
| displayName | text | Y | |
| status | text | Y | invited/active/locked/disabled |
| mfaEnabled | bool | Y | |
| lastLoginAt | timestamptz | N | |
| failedLoginCount | int | Y | |
| lockedUntil | timestamptz | N | |
| passwordHash | text | N | 若用 SSO 可為 null |
| ssoProvider | text | N | oidc/saml |
| ssoSubject | text | N | provider subject |
| createdAt | timestamptz | Y | |
| createdBy | uuid | Y | internal User |
Unique: (tenantId, email)
Index: (tenantId, status), (tenantId, lastLoginAt desc)
2.3 ExternalRole(外部角色)
PK: id
FK: tenantId → Tenant(id)
| 欄位 | 型態 | 必填 |
|---|---|---|
| id | uuid | Y |
| tenantId | uuid | Y |
| roleCode | text | Y |
| roleName | text | Y |
| isSystem | bool | Y |
| createdAt | timestamptz | Y |
| createdBy | uuid | Y |
Unique: (tenantId, roleCode)
2.4 ExternalPerm(外部權限)
PK: id
FK: tenantId → Tenant(id)
| 欄位 | 型態 | 必填 | 說明 |
|---|---|---|---|
| id | uuid | Y | |
| tenantId | uuid | Y | |
| permCode | text | Y | REQUEST_VIEW / FILE_UPLOAD / FILE_DOWNLOAD / STATUS_CHANGE… |
| permName | text | Y | |
| category | text | Y | request/file/job/audit/admin |
| createdAt | timestamptz | Y |
Unique: (tenantId, permCode)
2.5 ExternalUser_Role(Junction Table:外部使用者 ↔ 外部角色)
PK(建議複合): (externalUserId, externalRoleId)
FK: externalUserId → ExternalUser(id), externalRoleId → ExternalRole(id)
| 欄位 | 型態 | 必填 |
|---|---|---|
| externalUserId | uuid | Y |
| externalRoleId | uuid | Y |
| assignedAt | timestamptz | Y |
| assignedBy | uuid | Y |
Index: (externalUserId), (externalRoleId)
2.6 ExternalRole_Perm(Junction Table:角色 ↔ 權限)
PK(建議複合): (externalRoleId, externalPermId)
FK: externalRoleId → ExternalRole(id), externalPermId → ExternalPerm(id)
| 欄位 | 型態 | 必填 |
|---|---|---|
| externalRoleId | uuid | Y |
| externalPermId | uuid | Y |
| grantedAt | timestamptz | Y |
| grantedBy | uuid | Y |
Index: (externalRoleId), (externalPermId)
2.7 AccessPolicy(可見性規則 / Row-level 授權)
這張表是 Enterprise 的核心:外部使用者只能看到哪些 Request / Submission / Document
我建議支援「角色授權」與「個人授權」兩種。
PK: id
FK: tenantId → Tenant(id)
(可選 FK)externalUserId → ExternalUser(id) / externalRoleId → ExternalRole(id)
| 欄位 | 型態 | 必填 | 說明 |
|---|---|---|---|
| id | uuid | Y | |
| tenantId | uuid | Y | |
| subjectType | text | Y | user/role |
| externalUserId | uuid | N | subjectType=user 才填 |
| externalRoleId | uuid | N | subjectType=role 才填 |
| objectType | text | Y | Request/RequestItem/Submission/Document |
| objectId | uuid | Y | 目標資料列 id |
| accessLevel | text | Y | view/upload/download/manage |
| condition | jsonb | N | 進階條件(例:只能看某期間/某 entity) |
| expiresAt | timestamptz | N | 臨時授權 |
| createdAt | timestamptz | Y | |
| createdBy | uuid | Y | internal User |
| revokedAt | timestamptz | N | |
| revokedBy | uuid | N |
Index(非常重要)
- (tenantId, subjectType, externalUserId)
- (tenantId, subjectType, externalRoleId)
- (tenantId, objectType, objectId)
你要做 Row-Level Security (RLS) 或應用層權限,都會靠它跑得快。
2.8 AuditEvent(不可竄改稽核事件)
這張不能只是一般 log,要做到「不可竄改可驗證」:
建議做 hash chain(prevHash + eventHash),或用 WORM/Append-only。
PK: id
FK: tenantId → Tenant(id)
| 欄位 | 型態 | 必填 | 說明 |
|---|---|---|---|
| id | uuid | Y | |
| tenantId | uuid | Y | |
| actorType | text | Y | internal/external/system |
| actorUserId | uuid | N | internal User |
| actorExtId | uuid | N | ExternalUser |
| action | text | Y | view/download/upload/delete/status_change/login_failed… |
| objectType | text | Y | Request/Submission/Document… |
| objectId | uuid | Y | |
| ipAddr | inet | N | |
| userAgent | text | N | |
| requestId | text | N | trace id |
| payload | jsonb | N | 差異內容(不要塞敏感原文) |
| prevHash | text | N | hash chain |
| eventHash | text | Y | hash(payload+prevHash+…) |
| createdAt | timestamptz | Y |
Index: (tenantId, createdAt desc), (tenantId, actorType), (tenantId, objectType, objectId)
2.9 RetentionPolicy(保存/刪除策略)
PK: id
FK: tenantId → Tenant(id)
| 欄位 | 型態 | 必填 |
|---|---|---|
| id | uuid | Y |
| tenantId | uuid | Y |
| policyCode | text | Y |
| country | text | N |
| clientTier | text | N |
| datasetType | text | N |
| keepDays | int | Y |
| deleteMode | text | Y |
| legalHold | bool | Y |
| encryptRequired | bool | Y |
| effectiveFrom | date | Y |
| effectiveTo | date | N |
| createdAt | timestamptz | Y |
Unique: (tenantId, policyCode)
Index: (tenantId, country, clientTier), (tenantId, datasetType)
2.10 KeyRef(加密金鑰參照)
若你要做到「客戶級加密(每個客戶/租戶不同 key)」或「依國別/等級用不同 key」,這張表讓資料層能追溯與輪替。
PK: id
FK: tenantId → Tenant(id)
| 欄位 | 型態 | 必填 |
|---|---|---|
| id | uuid | Y |
| tenantId | uuid | Y |
| keyScope | text | Y |
| scopeId | uuid | N |
| kmsProvider | text | Y |
| keyAlias | text | Y |
| keyVersion | text | N |
| status | text | Y |
| rotatedAt | timestamptz | N |
| createdAt | timestamptz | Y |
Index: (tenantId, keyScope, status)
2.11 KpiSnapshot(KPI 快照)
PK: id
FK: tenantId → Tenant(id), engagementId → Engagement(id)
| 欄位 | 型態 | 必填 |
|---|---|---|
| id | uuid | Y |
| tenantId | uuid | Y |
| engagementId | uuid | N |
| periodType | text | Y |
| periodStart | date | Y |
| periodEnd | date | Y |
| requestCount | int | Y |
| avgCycleHours | int | Y |
| resubmitRate | numeric(6,3) | Y |
| validateFailRate | numeric(6,3) | Y |
| hoursSaved | numeric(12,2) | N |
| snapshot | jsonb | Y |
| createdAt | timestamptz | Y |
Unique: (tenantId, engagementId, periodType, periodStart, periodEnd)
2.12 NotifyOutbox(通知 Outbox)
Outbox pattern:資料庫先落一筆待送通知,由 worker 發送,成功再標記 sent,失敗可重試,並可稽核。
PK: id
FK: tenantId → Tenant(id)
| 欄位 | 型態 | 必填 |
|---|---|---|
| id | uuid | Y |
| tenantId | uuid | Y |
| channel | text | Y |
| templateCode | text | Y |
| subject | text | N |
| payload | jsonb | Y |
| recipientType | text | Y |
| recipientUserId | uuid | N |
| recipientExtId | uuid | N |
| relatedType | text | N |
| relatedId | uuid | N |
| status | text | Y |
| attemptCount | int | Y |
| nextAttemptAt | timestamptz | N |
| lastError | text | N |
| createdAt | timestamptz | Y |
| sentAt | timestamptz | N |
Index: (tenantId, status, nextAttemptAt), (tenantId, createdAt desc)
2.13 Enterprise 關鍵「內控/資安」設計點
A. 多租戶隔離(併購後共用的核心)
- Tenant 必須是第一級隔離鍵
- 幾乎每張表都應該有
tenantId(包含 Document / Request / Submission / AuditEvent / AccessPolicy) - 索引優先用
(tenantId, status)、(tenantId, createdAt),避免跨租戶掃描
B. 外部 RBAC + Row-level Access
- RBAC 只解決「能不能做某類事」
- AccessPolicy 解決「能看哪一筆資料」(例如 client A 只能看自己的 Request/Document)
- 建議 subjectType=role 為主,例外才 subjectType=user(可控也好維運)
C. 稽核不可竄改(AuditEvent)
- 不能只是一般 log:要做到「事後可驗證」
- hash chain(prevHash + eventHash) 是成本低、效果強的做法
- 下載/檢視/狀態變更/刪除(含 soft delete)一定要打點
D. 保存/刪除與法律保全(RetentionPolicy)
- 你跨國後必會遇到「不同國別、不同客戶等級、不同保存年限」
- RetentionPolicy + legalHold 能避免「被迫刪」「不該刪」的內控事故
E. 客戶級加密(KeyRef)
- 大客戶常要求「每客戶獨立 key / key rotation」
- KeyRef 讓你可以把 Document 的加密策略(keyScope/scopeId)關聯起來,稽核也交代得清楚





