雲端整合(Cloud Integrations)
功能群:證據管理|儲存後端與證據來源共用概念先讀 功能群總覽
事實基準:2026-07-05 從 FE / BE code 掃出(FR-016 Google Drive sync 三階段功能;API schema 逐條對照 marshmallow serializer 與 service 組裝碼)
1. 功能描述
雲端整合頁是租戶層級(非單一專案)的 Google Drive 連線管理頁:tenant admin 在此完成 OAuth 授權、檢視連線狀態、查看背景同步 job 歷史,以及在資料夾結構損壞時觸發全租戶重建。連線建立後,Drive 成為專案資料夾結構與證據檔案的雲端來源(見 _overview §4);per-project 的資料夾初始化 / 修復操作則分散在專案清單頁與專案規劃頁 Tab5(見 §1.1 註記),不在本頁。
主要使用者:tenant admin(is_admin
角色)。一般使用者可讀取連線狀態(頁面可進入),但寫入操作(連接/中斷/重建)皆鎖
admin。
角色速覽(完整定義見 _overview §5;本頁的角色基調與其餘系統/租戶層頁面一致):
| 角色 | 一句話 |
|---|---|
tenant admin(is_admin) |
租戶管理者 — 本頁全部寫入操作(連接/中斷/重建)的唯一授權角色 |
| super admin | 系統層級管理者 — disconnect 額外放行的第二種身分(見
§3) |
| 一般使用者 | 可進頁、可讀連線狀態,無任何寫入按鈕會出現作用(BE 仍會擋) |
1.1 功能總覽(本頁全部功能)
| # | 功能 | 說明 | 位置 | 詳述 |
|---|---|---|---|---|
| 1 | 連線狀態卡片 | 顯示 DISCONNECTED/CONNECTED/REVOKED/EXPIRED 四態,對應不同卡片內容 | GoogleDriveIntegrationCard | UC-CI-01 |
| 2 | 連接 Google Drive | OAuth popup 授權流程,完成後 postMessage 回主視窗 | 未連線態按鈕 | UC-CI-01 |
| 3 | 帳號與連線資訊顯示 | Google 帳號 email、連線時間+操作人、根資料夾連結(有才顯示)、最後同步時間(有才顯示) | 已連線態 dl 列表 | §5 |
| 4 | 中斷連線 | 確認對話框 → 停 webhook → HTTP revoke token → 清空 DB 欄位(三階段,皆 best-effort 容錯) | 已連線態右下角按鈕 | UC-CI-02 |
| 5 | 重新授權 | 連線異常(REVOKED/EXPIRED)時的醒目按鈕;已連線時收進「⋯」選單,走同一支 connect 流程 | 異常態 Message/「⋯」選單 | UC-CI-01(同流程) |
| 6 | 查看同步紀錄 | 開 dialog 顯示 drive_sync_jobs
分頁列表,可依狀態篩選、對 FAILED job 手動重試 |
「⋯」選單 → SyncJobHistoryDialog | UC-CI-04(列表本身為分頁顯示,見 §5/§6.3;重試邏輯 UC-CI-04) |
| 7 | 重建整個租戶資料夾結構 | Destructive 操作:清空全租戶
drive_folder_mappings+為每個進行中專案重新 enqueue
初始化;type-to-confirm(需輸入 REBUILD) |
「⋯」選單(限 admin) → 確認 dialog | UC-CI-03 |
| 8 | Per-project 資料夾初始化 | 對單一(尚未同步)專案手動觸發建立 Drive 資料夾 | 不在本頁——ProjectListView.vue「同步至
Drive」按鈕 |
§12 坑 1 |
| 9 | Per-project 驗證與修復 | 檢查單一專案下所有資料夾對應是否仍存在,標記失聯並重建 | 不在本頁——ProjectCloudIntegrationsPanel.vue(專案規劃頁
Tab5,見 專案雲端整合),manager-only |
§12 坑 1 |
| 10 | Webhook 手動註冊 | 補註冊 Drive Push channel+為未同步專案 backfill init(用於 Phase 3 前連線的舊租戶) | 無 FE 入口,僅 API 存在(維運操作) | UC-CI-05 |
UC(§2)展開本頁所有具實質後端邏輯的操作路徑:1/5(連接/重新授權,共用同一流程 UC-CI-01)、4(中斷 UC-CI-02)、7(重建租戶 UC-CI-03)、6(同步 job 重試 UC-CI-04)、10(webhook 手動註冊 backfill UC-CI-05)。純顯示類(1 連線狀態卡片、3 帳號資訊顯示)見 §5;同步 job 列表本身為分頁顯示見 §6.3。per-project 操作(8 init-folders、9 verify-and-repair)FE 入口不在本頁(分屬專案清單頁 / 專案規劃頁 Tab5),其 endpoint 於 §6.1 交叉列出、坑點見 §12 坑 1,本頁不重複展開以免維護 drift。
2. Use Case
角色速覽(完整定義見 _overview §5;本頁的角色基調與其餘系統/租戶層頁面一致):
| 角色 | 一句話 |
|---|---|
tenant admin(is_admin) |
租戶管理者 — 本頁全部寫入操作(連接/中斷/重建)的唯一授權角色 |
| super admin | 系統層級管理者 — disconnect 額外放行的第二種身分(見
§3) |
| 一般使用者 | 可進頁、可讀連線狀態,無任何寫入按鈕會出現作用(BE 仍會擋) |
流程圖視覺慣例:菱形 = 判斷、橘底 = 例外/擋下、紅框 = 錯誤(標 error code)、綠框 = 成功終點、虛線 = 可選路徑。
UC-CI-01 連接/重新授權 Google Drive
| 項目 | 內容 |
|---|---|
| 角色 | tenant admin |
| 前置條件 | 已登入;is_admin=true |
| 產出 / 後置條件 | tenant_drive_integrations
新增或更新一列(status=CONNECTED,token 加密存放);同 transaction
內註冊 Drive Push channel;best-effort
為斷線期間新建的專案補建資料夾 |
UC-CI-02 中斷連線
| 項目 | 內容 |
|---|---|
| 角色 | tenant admin 或 super admin |
| 前置條件 | 已登入;tenant_drive_integrations
存在該租戶的連線列 |
| 產出 / 後置條件 | Drive Push channel 停止;OAuth token 於 Google 端撤銷(best-effort);DB 欄位清空回 DISCONNECTED |
三階段皆各自容錯(stage 0/1 失敗只記 log,不擋 stage 2 執行):①
webhook_channel_manager.stop_for_tenant(best-effort)②
HTTP revoke refresh token(best-effort)③
_domain.disconnect(tenant_id) 清空 DB
欄位(實際生效的一步)。三步分屬三個獨立 @transaction
方法(google_drive_integration_service.py:187-221),中間穿插的
HTTP 呼叫不佔用 DB session。
UC-CI-03 重建整個租戶 Drive 資料夾結構(destructive)
| 項目 | 內容 |
|---|---|
| 角色 | tenant admin |
| 前置條件 | 已登入;is_admin=true;FE 要求輸入關鍵字
REBUILD 才能按下確認鈕 |
| 產出 / 後置條件 | 該租戶全部 drive_folder_mappings
被清空;tenant_drive_integrations.root_folder_id 重置為
null;每個現存專案重新 enqueue INIT_PROJECT_FOLDERS
job |
上圖沿用版面骨架圖示意「⋯」選單 → 重建 dialog 的觸發路徑;重建本身無分支例外(僅
is_admin一道門),故不另畫獨立流程圖。
UC-CI-04 查看同步紀錄並手動重試 FAILED job
| 項目 | 內容 |
|---|---|
| 角色 | tenant admin(is_admin) |
| 前置條件 | 已登入;is_admin=true;欲重試者須為 FAILED
狀態的 job |
| 產出 / 後置條件 | 該 FAILED job 重置為 PENDING(等下一輪背景
worker 輪詢重跑,非即時);不改動其他 job |
UC-CI-05 手動補註冊 Webhook 並 backfill(維運操作,無 FE 入口)
| 項目 | 內容 |
|---|---|
| 角色 | tenant admin(is_admin);route 層直接檢查 |
| 前置條件 | 已登入;is_admin=true;典型情境為 Phase 3
上線前就已連線、或斷線期間新建專案的舊租戶 |
| 產出 / 後置條件 | 註冊一個新 Drive Push channel(idempotent,已有 live channel
也不報錯);為未同步專案 backfill 補 enqueue init(略過已有
PROJECT scope 對應的專案) |
3. 權限矩陣
| 操作 | FE 判定 | BE 強制 | BE 檢查位置 |
|---|---|---|---|
| 讀連線狀態(進頁/卡片顯示) | 無(requiresAuth: true 僅要求登入) |
僅 @jwt_required(),無角色檢查 |
api/cloud_integration/routes/google_drive_integration_route.py:26-36(GET) |
| 產生 OAuth 授權 URL | 無額外判定(按鈕永遠顯示於未連線態) | is_admin → 否則 GRC_NOT_ADMIN(403) |
app/cloud_integration/service/google_drive_integration_service.py:64-67 |
| 中斷連線 | 無額外判定(按鈕永遠顯示於已連線態) | is_admin 或
is_super_admin(兩者其一即可) |
同檔 :187-190 |
| 查看同步 job 歷史 | 無額外判定 | is_admin → 否則 GRC_NOT_ADMIN |
app/cloud_integration/service/drive_sync_admin_service.py:98-99 |
| 手動重試 sync job | 無額外判定 | is_admin → 否則 GRC_NOT_ADMIN |
同檔 :110-111 |
| 重建整個租戶資料夾 | 「⋯」選單項目 v-if="isAdmin"
才出現(userInfo.is_admin) |
is_admin → 否則 GRC_NOT_ADMIN |
同檔 :157-158 |
| Per-project 初始化資料夾 | 無角色判定(ProjectListView.vue) |
無角色檢查(僅
@jwt_required(),刻意設計避免誤擋非 admin 的專案
manager) |
app/cloud_integration/service/drive_sync_admin_service.py:126-129(docstring
明載設計理由) |
| Per-project 驗證與修復 | canEdit(manager 才可點) |
專案 manager(project_participant.role == "manager")→
否則 GRC_NOT_MANAGER |
app/cloud_integration/service/drive_project_verify_service.py:122-130 |
| Webhook 手動註冊+backfill | 無 FE 入口 | is_admin → 否則 GRC_NOT_ADMIN |
api/cloud_integration/routes/google_drive_sync_route.py:205-206(route
層直接檢查,非 service 層) |
| OAuth callback(Google 導回) | — | 無角色檢查(state 一次性 Redis token 即身分綁定,見 §12
坑 2) |
api/cloud_integration/routes/google_drive_integration_route.py:69-96 |
| Webhook 事件接收 | — | 無 JWT;以 X-Goog-Channel-Token 比對取代 |
api/cloud_integration/routes/google_drive_webhook_route.py |
權限模型與其他系統層頁面一致:本頁是「租戶層級」而非「專案層級」,因此用的是
UserContextDTO.is_admin(tenant admin JWT claim)而非專案project_participants.role;唯一例外是 per-project 驗證與修復(§1.1 #9)沿用專案層 manager 檢查,因為那是掛在專案規劃頁 Tab5 下的操作。
4. 狀態機與前置條件
tenant_drive_integrations.status
四態(非稽核輪次狀態機,是本頁專屬的連線狀態):
| 狀態 | 意義 | 觸發來源 |
|---|---|---|
DISCONNECTED |
尚未連線,或已中斷 | 初始值;中斷連線後的終態 |
CONNECTED |
正常連線中 | OAuth callback 成功寫入 |
REVOKED |
Token 已被 Google 端撤銷(使用者在 Google 帳號設定移除授權) | 背景同步偵測到 401 時回寫(不在本頁鏈路,見 §11) |
EXPIRED |
Access token 過期且 refresh 失敗 | 同上 |
| 條件 | 效果 | 出處 |
|---|---|---|
status === 'DISCONNECTED' |
顯示「連接」按鈕與警語 | GoogleDriveIntegrationCard.vue:286-295 |
status IN ('REVOKED','EXPIRED') |
紅色 Message + 「重新授權」按鈕(走同一支
connect()) |
同檔 :190-191, 297-308 |
status === 'CONNECTED' |
顯示帳號資訊 dl 列表 + 「⋯」選單 + 「中斷連線」按鈕 | 同檔 :311-359 |
root_folder_id 為 null |
不顯示「根資料夾」欄位(Phase 2 才會有資料夾樹) | 同檔 :332 |
last_sync_at 為 null |
不顯示「最後同步」欄位(Phase 3 才會有同步紀錄) | 同檔 :344 |
userInfo.is_admin 為 false |
「⋯」選單不出現「重建整個租戶資料夾」項目 | 同檔 :208-214 |
| BE 寫入操作 | 無任何 phase / 狀態前置檢查——僅角色檢查(見 §3),無論目前 status 為何皆可呼叫 connect/disconnect/rebuild | app service 層程式碼確認 |
5. UI 設計
📸 實際畫面截圖待補(一律亮色模式拍攝):以 playwright 對 STG 擷取,預計補以下四張到
assets/img/: ① 未連線態卡片cloud-integrations-disconnected.png② 已連線態卡片(含帳號資訊、根資料夾連結、⋯選單展開)cloud-integrations-connected.png③ 同步 job 歷史 dialog(含 FAILED 列的重試按鈕)cloud-integrations-sync-jobs.png④ 重建租戶資料夾確認 dialog(type-to-confirm 輸入框)cloud-integrations-rebuild-dialog.png
版面骨架與區塊職責(單卡片頁,無左樹/無 Tab/無分頁列表;彩色區塊可點跳本頁小節錨點,API 僅標代表性):
狀態呈現:讀取中用 ProgressBar indeterminate
模式(非全頁 spinner);連接/中斷/重建動作各自獨立 loading
flag(connecting / disconnecting /
rebuildLoading),按鈕本身顯示 spinner 並 disable
防雙擊;無 socket,狀態更新靠 OAuth popup 完成後的
postMessage 觸發 refresh() 重打狀態
API,其餘操作靠呼叫後手動 refresh() /
fetchJobs()。多租戶切換時(watch(tenantId),位於
ProjectListView.vue,非本頁)會觸發連線狀態重查,避免殘留前一租戶的
badge。
6. API 規格
Envelope:成功 {"status": true, "data": …}(部分
endpoint 直接回傳物件不包 data 鍵,見各段落)、失敗
{"error_code": "…", "msg": "…"}(HTTP 4xx/5xx)。
6.1 總清單(本模組全部 endpoint;本頁 UI 只用其中 6 支,其餘見備註)
| 分類 | Method + Path | 說明 | 完整規格 |
|---|---|---|---|
| 連線 | GET /integrations/google-drive |
連線狀態(本頁載入用) | §6.2 |
| 連線 | POST /integrations/google-drive/auth-url |
產生 OAuth 授權 URL(admin only) | §6.2 |
| 連線 | GET /integrations/google-drive/callback |
OAuth callback(Google 導回,回 HTML 非 JSON) | §6.2 |
| 連線 | DELETE /integrations/google-drive |
中斷連線(admin/super admin only) | §6.2 |
| 同步 job | GET /integrations/google-drive/sync-jobs |
Job 歷史分頁列表(admin only) | §6.3 |
| 同步 job | POST
/integrations/google-drive/sync-jobs/{uid}/retry |
手動重試 FAILED job(admin only) | §6.3 |
| 租戶維運 | POST /integrations/google-drive/tenant/rebuild-all |
重建整個租戶資料夾(admin only,destructive) | §6.3 |
| per-project(不在本頁) | POST
/integrations/google-drive/projects/{project_uid}/init-folders |
手動初始化單一專案資料夾;FE 呼叫方在
ProjectListView.vue |
§12 坑 1 |
| per-project(不在本頁) | POST
/integrations/google-drive/projects/{project_uid}/verify-and-repair-folders |
單一專案驗證與修復;FE 呼叫方在
ProjectCloudIntegrationsPanel.vue(manager-only) |
§12 坑 1 |
| 維運(無 FE 入口) | POST /integrations/google-drive/webhook/register |
手動補註冊 webhook + backfill 未同步專案(admin only) | §6.3 |
| 系統(非本頁呼叫) | POST /webhooks/google-drive/{tenant_id} |
Google Drive 變更通知接收端;無 JWT,靠 channel token 驗證 | §11 |
6.2 連線類
[GET] /integrations/google-drive
Response data:
| 欄位 | 型別 | 說明 |
|---|---|---|
| uid | string|null | 連線記錄 uid;未連線(無 row)時整包欄位皆為
null,status 固定回 DISCONNECTED |
| status | string | CONNECTED / EXPIRED / REVOKED
/ DISCONNECTED |
| google_account_email | string|null | 已連線帳號 |
| root_folder_id | string|null | Drive 根資料夾 ID(Phase 2 才有值) |
| connected_by_user_id / connected_by_user_name | int|null / string|null | 操作人 id / nickname(service 層查 UserDomainService
轉換) |
| connected_at | datetime|null | ISO 8601 |
| last_sync_at | datetime|null | 最後一次背景同步時間(Phase 3 才有值) |
| webhook_expires_at | datetime|null | Push channel 到期時間 |
| last_sync_error | string|null | 最近一次同步錯誤訊息 |
出處:api/cloud_integration/serializers/google_drive_integration.py:9-19(GoogleDriveStatusResponse);組裝
app/cloud_integration/service/google_drive_integration_service.py:170-176(get_status)+
app/cloud_integration/dto/google_drive_integration_dto.py:24-54(GoogleDriveIntegrationStatusDTO.from_entity,無連線列時回傳全
null 的 DISCONNECTED DTO)。
[POST] /integrations/google-drive/auth-url
無 request
body。Response:{auth_url: string, state: string}。state
為一次性隨機 token,寫入 Redis(key
drive_oauth_state:<state>,TTL 600 秒,value 為
<tenant_id>:<user_id>)供 callback
驗證身分;auth_url 帶
REQUIRED_SCOPES(drive +
userinfo.email)。出處:app/cloud_integration/service/google_drive_integration_service.py:64-72。
[GET] /integrations/google-drive/callback
Request(query string):
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
| code | string | 否 | Google 授權碼 |
| state | string | 否 | 比對 Redis |
| error | string | 否 | 使用者拒絕授權時 Google 帶入 |
回傳 HTML 而非 JSON——內嵌
<script> 對 window.opener 執行
postMessage({source:'guidant-drive-oauth', status, error})
後 800ms 自動關閉視窗;error 值一律經
json.dumps 逸出避免 reflected
XSS。出處:api/cloud_integration/routes/google_drive_integration_route.py:69-123。
[DELETE] /integrations/google-drive
無 request
body。Response:{disconnected: bool}(該租戶原本無連線列時回
false)。出處:app/cloud_integration/service/google_drive_integration_service.py:187-221。
6.3 同步 job 與租戶維運類
[GET] /integrations/google-drive/sync-jobs
Request(query string):
| 欄位 | 型別 | 必填 | 說明 |
|---|---|---|---|
| status | string|null | 否 | PENDING/IN_PROGRESS/SUCCESS/FAILED/SKIPPED;省略
= 不篩選 |
| page | int | 否 | 預設 1 |
| page_size | int | 否 | 預設 20(FE dialog 固定帶 50) |
出處:api/cloud_integration/serializers/drive_sync_job.py:19-22(DriveSyncJobListQuerySchema)。
Response(頂層即含 data /
meta,不再包一層 status——與 §1
envelope 描述的分頁慣例一致):data 為陣列,每項欄位:
| 欄位 | 型別 | 說明 |
|---|---|---|
| uid | uuid | Job 識別 |
| job_type | string | INIT_PROJECT_FOLDERS / CREATE_FOLDER /
RENAME_FOLDER / PROCESS_DRIVE_CHANGES /
IMPORT_DRIVE_FILE / SOFT_DELETE_EVIDENCE /
RECONCILE_TASK_FOLDER /
ARCHIVE_DRIVE_FILE |
| status | string | 見上表 5 態 |
| priority / retry_count / max_retries | int | 預設 100 / 0 / 5 |
| next_run_at | datetime|null | Worker 排程輪詢依據 |
| last_error | string|null | 最近一次失敗訊息 |
| started_at / finished_at / created_at | datetime|null | — |
| payload | object | Job 執行上下文(依 job_type 內容不同,如 project_uid / scope_uid) |
meta 欄位:
| 欄位 | 型別 | 說明 |
|---|---|---|
| paging | bool | 固定 true |
| page | int | 目前頁碼 |
| page_size | int | 每頁筆數 |
| total | int | 總筆數 |
| total_pages | int | 總頁數 |
| has_next | bool | 是否有下一頁 |
| has_prev | bool | 是否有上一頁 |
出處:api/cloud_integration/serializers/drive_sync_job.py:4-16(DriveSyncJobResponse);組裝
api/cloud_integration/routes/google_drive_sync_route.py:38-75、app/cloud_integration/service/drive_sync_admin_service.py:82-105(list_jobs)。
[POST] /integrations/google-drive/sync-jobs/{uid}/retry
Path 參數:uid(job uid)。無 request
body。Response:{retried: bool}。行為:僅重置
FAILED job 為 PENDING(等下一輪 worker
輪詢);不做即時重跑。出處:app/cloud_integration/service/drive_sync_admin_service.py:108-112。
[POST] /integrations/google-drive/tenant/rebuild-all
無 request
body。Response:{wiped: int, enqueued_projects: int}(wiped
= 被清空的資料夾對應筆數;enqueued_projects = 重新 enqueue
初始化 job 的專案數)。Destructive:清空後舊 Drive
帳號裡的資料夾不會被刪除(變成孤兒),只是系統端不再追蹤,重建後長出全新資料夾樹。出處:app/cloud_integration/service/drive_sync_admin_service.py:139-178。
[POST] /integrations/google-drive/webhook/register
無 request
body。Response:{registered: bool, backfilled_projects: int}。Idempotent:對已有
live channel 的租戶呼叫會註冊一個新 channel(不會報錯),並略過已有
PROJECT scope
資料夾對應的專案。出處:api/cloud_integration/routes/google_drive_sync_route.py:178-216。
7. 前端檔案地圖(compliance-manager-fe/)
| 檔案 | 角色 |
|---|---|
src/views/integrations/CloudIntegrationsView.vue |
頁面殼(19 行):僅標題 +
<GoogleDriveIntegrationCard />,無自身邏輯 |
src/components/integrations/GoogleDriveIntegrationCard.vue |
本頁核心元件(403 行):四態卡片、OAuth popup 流程、中斷連線、「⋯」選單、重建 dialog 皆在此 |
src/components/integrations/SyncJobHistoryDialog.vue |
同步 job 歷史 dialog:狀態篩選 Dropdown、DataTable、FAILED 列重試按鈕 |
src/config/router/index.js:356-364 |
route cloud-integrations,path
/settings/cloud-integrations,僅
requiresAuth: true,無 capability meta |
src/config/api/api.js:379-387 |
INTEGRATION_GDRIVE* 常數群 |
src/service/CloudIntegrationService.js |
API 包裝:getGoogleDriveStatus /
getGoogleDriveAuthUrl / disconnectGoogleDrive
/ listSyncJobs / retrySyncJob /
initProjectFolders /
verifyAndRepairProjectFolders /
rebuildAllDriveFolders /
driveFolderUrl(純字串拼接,無 API 呼叫) |
src/components/grc/project/ProjectCloudIntegrationsPanel.vue |
另一頁面(專案規劃頁 Tab5,見 專案雲端整合):per-project
驗證與修復,manager-only,非本頁但共用
CloudIntegrationService |
src/views/project/ProjectListView.vue:113-124 |
per-project「同步至
Drive」按鈕(handleSyncToDrive),呼叫
initProjectFolders;非本頁但共用 service |
src/config/locales/i18n/{zh-tw,en}/cloud-integration.json |
頁面 i18n(namespace lang.cloud_integration.*) |
src/config/locales/i18n/{zh-tw,en}/menu.json:128,247 |
選單顯示名稱與說明 |
8. 後端檔案地圖(本模組核心鏈路)
| 鏈路 | Route | App Service | 底層 |
|---|---|---|---|
| 連線狀態/OAuth | api/cloud_integration/routes/google_drive_integration_route.py(GoogleDriveIntegrationRoute
GET/DELETE、GoogleDriveAuthUrlRoute、GoogleDriveCallbackRoute) |
app/cloud_integration/service/google_drive_integration_service.py::GoogleDriveIntegrationService(get_status
/ build_auth_url / handle_callback /
disconnect) |
TenantDriveIntegrationDomainService →
TenantDriveIntegrationRepoImpl(infra/cloud_integration/repository/);OAuth
交換走
infra/cloud_integration/google_drive/google_oauth_client.py;token
加密走 infra/cloud_integration/crypto/fernet_crypto.py |
| 同步 job 管理/租戶重建 | api/cloud_integration/routes/google_drive_sync_route.py(DriveSyncJobsRoute
/ DriveSyncJobRetryRoute /
DriveRebuildAllRoute /
DriveWebhookRegisterRoute) |
app/cloud_integration/service/drive_sync_admin_service.py::DriveSyncAdminService |
DriveSyncJobDomainService /
DriveFolderMappingDomainService /
TenantDriveIntegrationDomainService → 對應
infra/cloud_integration/repository/*_repo_impl.py |
| Per-project 初始化(不在本頁 UI) | 同 sync_route(DriveSyncProjectInitFoldersRoute) |
DriveSyncAdminService::trigger_init_project_folders(僅轉呼叫
orchestration,無角色檢查——見 §3) |
DriveSyncOrchestrationService::try_enqueue_init_project_folders |
| Per-project 驗證修復(不在本頁 UI) | 同 sync_route(DriveProjectVerifyAndRepairRoute) |
app/cloud_integration/service/drive_project_verify_service.py::DriveProjectVerifyService(manager
檢查、Drive API 呼叫刻意移出 @transaction 避免長時間佔用
session,見檔頭 docstring) |
ProjectTreeLoader(走 jedi_oscal_v2 SSP
控制樹)+ GoogleDriveApiClient |
| Webhook 接收(背景,非本頁) | api/cloud_integration/routes/google_drive_webhook_route.py::GoogleDriveWebhookRoute |
app/cloud_integration/service/google_drive_webhook_service.py::GoogleDriveWebhookService |
驗證 channel token → 寫入
drive_sync_jobs(PROCESS_DRIVE_CHANGES) |
DDD 提醒:本模組層級劃分清楚(route 不碰 DB,app
service 皆 @transaction,domain service 抽象 repository
interface),新增本模組 API
可直接比照現有寫法。唯一值得留意的既有簡化:trigger_init_project_folders
刻意不加角色檢查(docstring 說明是為了避免誤擋非 tenant-admin 的專案
manager),新增類似「轉呼叫
orchestration」的方法時,若涉及寫入需重新評估是否也要這樣簡化,不要無條件比照。
9. DB
9.1 資料表總清單(本模組讀寫的全部表)
| 表 | 讀/寫 | 說明 | 欄位詳述 |
|---|---|---|---|
| public.tenant_drive_integrations | 寫 | 租戶 OAuth 連線主檔(1 租戶 1 列,tenant_id
UNIQUE) |
§9.3 |
| public.drive_folder_mappings | 寫 | Drive 資料夾 ↔︎ 系統實體對應(ROOT/PROJECT/AP/CONTROL_GROUP/CONTROL/AO/TASK/ARCHIVE/EVIDENCES 九種 scope) | §9.3 |
| public.drive_sync_jobs | 寫 | 背景同步任務佇列(本頁只讀列表 + 重試,寫入多由 orchestration / webhook 觸發) | §9.3 |
| compliance.projects | 讀 | Per-project 操作定位專案(init-folders /
verify-and-repair,不在本頁 UI) |
見 project-planning §9.3 |
| compliance.project_participants | 讀 | Per-project 驗證修復的 manager 角色檢查來源 | 見 project-planning §9.3 |
9.2 ER 圖
9.3
核心表欄位(取自
scripts/deliverables/out/db_schema.json;DB 未設
comment,欄位語意依程式碼實際用途註記)
public.tenant_drive_integrations — 租戶 Drive 連線主檔
| 欄位 | 型別 | Nullable | 說明 |
|---|---|---|---|
| id | integer | 否 | PK,nextval 序列 |
| uid | uuid | 否 | 對外識別碼,gen_random_uuid() |
| tenant_id | integer | 否 | UNIQUE
索引(tenant_drive_integrations_tenant_id_key)——確保
1 租戶最多 1 列 |
| provider | varchar(20) | 否 | 預設
'google_drive'(欄位存在為未來多雲端來源預留,目前僅此一種) |
| google_account_email | varchar(255) | 是 | 已連線 Google 帳號 |
| refresh_token_encrypted | text | 是 | Fernet
加密(domain/cloud_integration/service/token_crypto_service.py),絕不明文存放 |
| access_token_cache | text | 是 | Fernet 加密的短期 access token 快取 |
| access_token_expires_at | timestamptz | 是 | access token 到期時間 |
| root_folder_id | varchar(100) | 是 | Drive 上 GuidantAI/ 根資料夾 ID |
| drive_change_cursor | varchar(255) | 是 | Drive Changes API 游標(背景同步用,見 §11) |
| webhook_channel_id / webhook_resource_id / webhook_token / webhook_expires_at | varchar(100) / varchar(100) / varchar(100) / timestamptz | 是 | Drive Push Notification channel 資訊;webhook_token 是
webhook 接收端驗證用的隨機密鑰 |
| status | varchar(20) | 否 | 預設 'DISCONNECTED';四態見 §4 |
| connected_by_user_id / connected_at | integer / timestamptz | 是 | 操作人與連線時間 |
| last_sync_at / last_sync_error | timestamptz / text | 是 | 最近一次背景同步結果 |
| created_user / updated_user | varchar(100) | 是 | 審計欄(login_name) |
索引:idx_tenant_drive_integrations_tenant_id、idx_tenant_drive_integrations_status。
public.drive_folder_mappings — 資料夾對應
| 欄位 | 型別 | Nullable | 說明 |
|---|---|---|---|
| id / uid | integer / uuid | 否 | PK / 對外識別 |
| tenant_id | integer | 否 | soft ref(無 FK,避免跨 schema CASCADE) |
| scope_type | varchar(20) | 否 | ROOT/PROJECT/AP/CONTROL_GROUP/CONTROL/AO/TASK/ARCHIVE/EVIDENCES(domain/cloud_integration/enums/scope_type.py) |
| scope_uid | uuid | 是 | 依 scope_type 指向對應實體的 uid(ROOT 無
scope_uid) |
| parent_drive_folder_id | varchar(100) | 是 | 父資料夾 Drive ID(重建父子關係用) |
| drive_folder_id | varchar(100) | 否 | UNIQUE(drive_folder_mappings_drive_folder_id_key) |
| display_name_snapshot | varchar(255) | 否 | 建立當下的資料夾顯示名稱快照 |
| is_unlinked | boolean | 否 | 預設 false;verify-and-repair 發現 Drive
端已刪除/無權限時標記為 true |
索引:uq_drive_folder_mappings_root(tenant_id, scope_type
WHERE scope_type='ROOT',確保每租戶僅 1
個根節點)、uq_drive_folder_mappings_scope(tenant_id, scope_type, scope_uid
WHERE scope_uid IS NOT NULL,同一實體同一 scope 僅 1
個對應)、idx_drive_folder_mappings_parent。
public.drive_sync_jobs — 背景同步任務佇列
| 欄位 | 型別 | Nullable | 說明 |
|---|---|---|---|
| id / uid | bigint / uuid | 否 | PK / 對外識別 |
| tenant_id | integer | 否 | soft ref |
| job_type | varchar(40) | 否 | 8 種(見 §6.3 sync-jobs 回應欄位表) |
| payload | jsonb | 否 | 預設 '{}';handler 執行所需上下文(project_uid /
scope_uid 等,依 job_type 而異) |
| status | varchar(20) | 否 | 預設 'PENDING';5 態見 §6.3 |
| priority | integer | 否 | 預設 100,數字愈小優先權愈高(worker 排程依據) |
| retry_count / max_retries | integer | 否 | 預設 0 / 5 |
| next_run_at | timestamptz | 否 | 預設 now();worker 輪詢的排程時間點 |
| last_error | text | 是 | 最近一次失敗訊息 |
| started_at / finished_at | timestamptz | 是 | 執行區間 |
索引:idx_drive_sync_jobs_pending(tenant_id, status, next_run_at
WHERE status='PENDING',worker
輪詢用)、idx_drive_sync_jobs_status(status, created_at DESC,本頁列表查詢用)。
10. 頁面邏輯與資料對應
載入時序:頁面掛載 → onMounted(refresh)
→ GET /integrations/google-drive → 依 status
切換卡片內容;「⋯」選單的同步紀錄 dialog 用
watch(dialogVisible) 觸發,開啟才打
GET /sync-jobs(非預先載入)。
關鍵欄位對應(API ↔︎ 畫面):
| 畫面元素 | FE state / computed | API 欄位 |
|---|---|---|
| 卡片顯示分支 | isConnected /
isError(status.status) |
status |
| 帳號 email | 直接綁定 | google_account_email |
| 連線時間+操作人 | new Date(...).toLocaleString() +
by {name} |
connected_at / connected_by_user_name |
| 根資料夾連結 | driveRootUrl(computed,root_folder_id 為
null 時不顯示整列) |
root_folder_id |
| 最後同步時間 | 條件渲染(last_sync_at 為 null 不顯示) |
last_sync_at |
| 「⋯」選單「重建」項目可見性 | isAdmin(userInfo.value.is_admin,非本頁
API 回傳,來自登入時的使用者資訊) |
— |
| 同步 job 表格列 | jobs(fetchJobs() 容錯兩種 shape:陣列 or
{data:[...]}) |
sync-jobs response data[] |
| Job 重試按鈕可見性 | data.status === 'FAILED' |
status |
| 重建 dialog 確認鈕啟用 | rebuildConfirmEnabled(輸入框內容 trim 後等於
'REBUILD') |
— |
儲存流程:連接/中斷/重建皆是「呼叫 API → 等回應 →
toast → refresh()
重打狀態」的一次性動作,無表單暫存或草稿概念。OAuth 連接特殊之處在於用
popup + postMessage
而非直接導頁,避免使用者離開設定頁上下文。
錯誤對應:中斷/重建/重試失敗走 toast 顯示
e?.response?.data?.msg ?? e?.message;狀態讀取失敗(401/404,未連線的正常情形)刻意只
console.debug,不顯示 toast
干擾(GoogleDriveIntegrationCard.vue:58-63
註解說明)。常見碼:GRC_NOT_ADMIN(403,非 admin
操作寫入)、GRC_DRIVE_OAUTH_STATE_INVALID(400,OAuth state
過期或重放)、GRC_DRIVE_OAUTH_EXCHANGE_FAILED /
GRC_DRIVE_USERINFO_FETCH_FAILED(400,OAuth
交換失敗)、GRC_DRIVE_OAUTH_NO_REFRESH_TOKEN(400,Google
未回 refresh token,常見於未強制 prompt=consent
的重複授權)。
11. 背景行為與外部依賴
| 類型 | 內容 |
|---|---|
| 背景 worker | app/cloud_integration/service/drive_sync_worker.py::DriveSyncWorker——由
APScheduler 週期呼叫 run_once(),消費
drive_sync_jobs 佇列(PENDING → 依
job_type 分派給對應 handler,如
create_folder_handler /
process_drive_changes_handler 等 8 種)。完整排程與
handler 細節屬背景服務 spec 範疇(本專案目前尚無獨立
background-services spec,暫以此檔案路徑為權威出處)本頁只負責顯示 job
記錄與手動重試 |
| Webhook | Google Drive Push Notification →
POST /webhooks/google-drive/{tenant_id}(無
JWT,X-Goog-Channel-Token 驗證)→ enqueue
PROCESS_DRIVE_CHANGES job;channel
由「連接」流程或「Webhook 手動註冊」API 建立,7 天到期需重新註冊 |
| jedi-* 套件 | jedi_common(@transaction、get_user_context);per-project
驗證修復另依賴 jedi_oscal_v2(透過
ProjectTreeLoader)與
jedi_project(ProjectDomainService) |
| 加密 | infra/cloud_integration/crypto/fernet_crypto.py(FernetCrypto)——refresh
token / access token 存 DB 前一律加密,key 來自
config.DRIVE_TOKEN_ENCRYPTION_KEY(見 .env /
部署文件) |
| Redis | OAuth state 一次性
token(drive_oauth_state:<state>,TTL 600
秒)——非本頁常駐依賴,僅連接流程短暫使用 |
| 系統參數 | GOOGLE_DRIVE_OAUTH_CLIENT_ID /
_CLIENT_SECRET /
_REDIRECT_URI、DRIVE_WEBHOOK_PUBLIC_BASE_URL、DRIVE_TOKEN_ENCRYPTION_KEY、DRIVE_FILE_SIZE_LIMIT_MB(皆見
di_containers/cloud_integration/cloud_integration_containers.py:99-151,實值見部署文件) |
| Socket | 無 |
12. 邊界情況與已知坑
- 本頁 UI 只涵蓋租戶層操作,per-project
操作分散在別處:
init-folders(ProjectListView.vue「同步至 Drive」按鈕)與verify-and-repair-folders(ProjectCloudIntegrationsPanel.vue,掛在專案規劃頁 Tab5)都是同一api/cloud_integration模組的 endpoint,但 FE 入口不在本頁——查文件或抓 bug 時注意「Drive 相關功能」不會全部集中在/settings/cloud-integrations - OAuth callback
無角色檢查:
GET /callback沒有@jwt_required()(Google 重定向瀏覽器過來時沒有帶 JWT),身分驗證完全依賴 Redis 一次性statetoken(TTL 600 秒,用畢即刪);state內編碼了tenant_id:user_id,若 state 外洩理論上可被重放但視窗僅 10 分鐘且一次性 disconnect允許 super admin 繞過 tenant admin 檢查:is_admin為 false 時還會額外查user.is_super_admin(系統層級),這是本模組唯一有「雙重身分」判定的操作,其餘寫入 API(connect / rebuild / retry)僅認is_admintrigger_init_project_folders刻意不做角色檢查:docstring 明載是為了避免用is_admin誤擋「非 tenant-admin 但是專案 manager」的正常操作者;純靠@jwt_required()+ FE 隱藏按鈕。新增類似「觸發 orchestration」的方法不要無條件比照,需重新評估- 重建租戶資料夾是不可逆操作:清空
drive_folder_mappings後,舊 Drive 帳號裡實際的資料夾檔案不會被刪除,只是系統端拿掉追蹤關係,變成孤兒資料夾;FE 用 type-to-confirm(需打字輸入REBUILD)降低誤觸風險,但沒有復原機制 - 帳號切換(reconnect 不同 Google
帳號)會清空全租戶資料夾對應:
handle_callback偵測到google_account_email與既有連線不同時,直接wipe_all_for_tenant(因為 Drive 權限是 per-account,舊資料夾新帳號存取不到);同帳號重新授權(token 過期後 re-auth)則保留root_folder_id,不會誤清空 GET /sync-jobsresponse 沒有外層status鍵,只有data/meta:與本頁其他 endpoint(皆為{"status": true, "data": ...})不同,是沿用return_response對分頁 meta 的特殊處理(同docs/specs/v1.8.0/project-management/_overview.md提到的頂層 meta key 慣例);FEfetchJobs()為此寫了雙 shape 容錯(陣列 or{data:[...]})- Webhook register 無 FE
入口:
POST /webhook/register只存在於 API 層,目前僅供維運人員手動呼叫(例如舊租戶在 Phase 3 上線前就已連線、或啟動一個 Drive 斷線期間建立的專案);文件與 code 都沒有 FE 按鈕,未來若要曝光給 UI 需另外設計操作入口與風險提示
13. 開發與驗證
- 跑起來:BE
python main_socketio.py(port 8000,log 在log/app.log);FE 在compliance-manager-fe/起 Vite dev server。BE 改 service code 後必須重啟(無 hot reload) - 測試帳號:dev 環境 tenant admin 角色帳號(密碼見
.env/ 部署文件);Google OAuth 需設定GOOGLE_DRIVE_OAUTH_CLIENT_ID等環境變數才能實際跑通連接流程 - 導航路徑:登入 →
側邊選單「雲端空間整合」(
cloud-integrations) - 前置資料:租戶需有 admin 角色使用者;若要測試 per-project 相關功能需另外進專案規劃頁 Tab5 或專案清單頁
- E2E:
compliance-manager-test/repo(Cucumber + Playwright);本頁相關 feature 檔以雲端整合/cloud-integration/google-drive搜尋 - 相關文件:FR-016(
docs/features/FR-016-2604-google-drive-sync/,含三個 phase 的 implementation plan:OAuth → 資料夾 → 同步);背景 worker 排程細節目前僅見程式碼(drive_sync_worker.py),尚無獨立 spec;DB / API 全量見 GAI-SD-02/03