FR-062 · v1.14.0 部署順序書 · 2026-08-10

v1.14.0 全景部署手冊

這一版部署橫跨 三個 repo(主產品 BE/FE + License Center),且金鑰體系有跨系統的順序依賴——LC 產鑰 → 公鑰進 BE 程式碼 → 該環境 BE 重新部署,漏一步整條照全數拒收。本文件是全景與順序手冊:告訴你先做什麼、後做什麼、哪一步錯了會出現什麼症狀。LC 單機安裝細節不在此重寫,一律指向 License Center repo 的部署手冊。

三 repo × 七階段 🔴 公鑰回填是最容易漏的一步 STG/POC 已依此順序實戰完成 Troubleshooting 為 2026-08-10 實戰全記錄

適用版本:v1.14.0(BE/FE 同版號對齊)|撰寫日期:2026-08-10 上游:design.md §4.2/§4.7/§4.9、LC 部署手冊 license_center/docs/deployment-guide.html(LC repo 內) 本文件與 existing-tenant-license-migration-sop.md 的關係:後者是 T-7.2 腳本的 DEV 演練 SOP,STG/POC 一律不走該腳本(見 §10)。


1. 為什麼這一版部署比較複雜

過去幾版的部署都是「拉 BE、拉 FE、套 migration、重啟」四步就完事。v1.14.0 不是,成因有三個,而且三個會互相纏在一起

① 跨三個 repo。 主產品 BE(compliance-manager-be)、主產品 FE(compliance-manager-fe)、License Center(license_center)。前兩者是既有部署流程的延伸,第三者是這一版才第一次出現在部署清單上的新系統

② License Center 是獨立系統、獨立 DB,不是主產品的一個模組。 它有自己的 repo、自己的 .env、自己的 DB(license_center_{env})、自己的 systemd 服務、自己的後台登入帳密。主產品的 poetry install 不會裝到它,主產品的 migration 不會建它的表。這是刻意的隔離設計——私鑰、客戶名單、訂單、抽佣資料不進主產品;兩邊只透過「簽好的照」與內部簽發 API 交互,無 DB 層共用。部署時的代價就是:它得從零裝一次

③ 金鑰體系有跨系統的順序依賴——這是真正的坑。 每個環境有自己獨立的一把簽發鑰(決策者要求 STG/POC 不得與 DEV 同鑰),私鑰永不離開該機。流程是:

LC keygen(該環境產鑰)
   ↓  公鑰 + kid
BE common/license/public_keys.py 的 PUBLIC_KEYS 陣列 append 一項
   ↓  commit
該環境的 BE 部署必須帶到這個 commit

🔴 漏掉中間任何一步的症狀:LC 簽發成功,產品端回「未知的 kid」,該環境所有照全數拒收。

這不是「部分功能壞掉」——是授權系統整條死。而且症狀出現的位置(產品端拒收)離根因(BE 沒帶到公鑰 commit)很遠,第一次遇到很容易往「照是不是簽壞了」的方向查。2026-08-10 STG/POC 實戰時就在這裡卡了一次。

因此:Phase 3「公鑰回填」被獨立成一個階段,而不是併進 BE 部署裡順手做。


2. 部署全景圖

2.1 拓撲:誰跟誰講話

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
graph LR
  subgraph 主產品["主產品 Guidant AI"]
    FE["FE<br/>nginx :80"]
    BE["BE :8000<br/>PUBLIC_KEYS 編譯在程式碼內"]
    PDB[("guidant_ai_&lt;env&gt;<br/>config.tenant_licenses<br/>config.tenant_license_events")]
  end
  subgraph LCsys["License Center(獨立系統)"]
    LC["LC :5062<br/>Flask + Tabler 後台"]
    KEYS["keys/<br/>私鑰(passphrase 加密)<br/>永不離開該機"]
    LDB[("license_center_&lt;env&gt;<br/>plans / license_issuance<br/>api_tokens")]
  end

  FE -->|"REST /api/1.0"| BE
  BE --> PDB
  BE -->|"POST /api/internal/issue|extend<br/>Bearer LICENSE_CENTER_API_TOKEN"| LC
  LC --> LDB
  LC --- KEYS
  KEYS -.->|"keygen 產出公鑰 + kid<br/>人工回填進 BE 原始碼"| BE
圖 1 — 單一環境內的拓撲。私鑰只在 LC 那台機器上,公鑰編譯進 BE 程式碼;兩系統各自獨立 DB,無共用。

要點三條:

  • 私鑰只在 LC 那台機器的 keys/ 目錄,不入版控、不進主產品、不跨環境複製。
  • 公鑰編譯在 BE 程式碼common/license/public_keys.py),不放設定檔、不放 DB——理由見 §7。
  • BE → LC 是唯一的系統間呼叫,帶 API token;LC 從不主動打產品端。

2.2 順序:先做什麼後做什麼

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#E2F0F1','primaryTextColor':'#14201F','primaryBorderColor':'#0E7C86','secondaryColor':'#EEF2F3','secondaryTextColor':'#14201F','tertiaryColor':'#FBFCFC','tertiaryTextColor':'#14201F','lineColor':'#4A5A5C','textColor':'#14201F','mainBkg':'#E2F0F1','nodeBorder':'#0E7C86','nodeTextColor':'#14201F','edgeLabelBackground':'#FBFCFC','titleColor':'#14201F','clusterBkg':'#FBFCFC','clusterBorder':'#E4EAEB','actorBkg':'#E2F0F1','actorTextColor':'#14201F','actorBorder':'#0E7C86','actorLineColor':'#C7D1D2','signalColor':'#4A5A5C','signalTextColor':'#14201F','labelBoxBkgColor':'#EEF2F3','labelBoxBorderColor':'#C7D1D2','labelTextColor':'#14201F','loopTextColor':'#14201F','noteBkgColor':'#F6EBD5','noteTextColor':'#14201F','noteBorderColor':'#9C6B12','activationBkgColor':'#EEF2F3','activationBorderColor':'#C7D1D2','sequenceNumberColor':'#FFFFFF'}}}%%
sequenceDiagram
    participant OP as 部署人員
    participant DB as DB server
    participant LC as License Center(目標機)
    participant SRC as BE 原始碼(本機 repo)
    participant BE as 產品 BE(目標機)
    participant FE as 產品 FE(目標機)

    Note over OP,DB: Phase 0 — 前置盤點(唯讀,不改任何東西)
    OP->>DB: 比對 schema_migrations(DEV vs 目標環境)
    DB-->>OP: 落差清單 → 決定這次套哪幾支

    Note over OP,DB: Phase 1 — DB migration
    OP->>DB: 主產品 7 支 FR-062 migration
    OP->>DB: 建 license_center_&lt;env&gt; + LC 001/002 + seed-plans

    Note over OP,LC: Phase 2 — LC 部署
    OP->>LC: 拉 code/.env/poetry install
    OP->>LC: keygen(該環境專屬一把)
    LC-->>OP: kid + 公鑰(keys/public_keys.json)

    rect rgb(246, 235, 213)
    Note over OP,BE: 🔴 Phase 3 — 公鑰回填(跨系統,最容易漏)
    OP->>SRC: PUBLIC_KEYS append 新 kid + 公鑰
    SRC-->>OP: commit
    end

    Note over OP,BE: Phase 4 — BE 部署
    OP->>BE: 拉 code(必須含 Phase 3 那個 commit)
    OP->>BE: .env 三項 + 重啟

    Note over OP,FE: Phase 5 — FE 部署
    OP->>FE: 拉 code → build → 佈署

    Note over OP,LC: Phase 6 — API token 發放
    OP->>LC: 後台新增 token(明文只顯示一次)
    OP->>BE: 貼進 .env → 再重啟一次 BE

    Note over OP,BE: Phase 7 — 既有租戶發照
    OP->>BE: 產品後台「向 LC 請照」
    BE->>LC: POST /api/internal/issue
    LC-->>BE: 簽好的照 → BE 驗章落地
圖 2 — 七階段部署順序。虛線框住的 Phase 2→3→4 是那條跨系統的金鑰依賴鏈,順序不可交換。

Phase 2 → 3 → 4 這條鏈不可交換順序。

Phase 6(token)之所以排在 BE 部署之後,是因為 token 明文只在 LC 後台產出的當下顯示一次——必須等 LC 起來、後台能登入了才發得出來。代價是 BE 要重啟兩次(Phase 4 一次、Phase 6 貼完 token 再一次)。想省一次的話可以把 Phase 6 提前到 Phase 4 之前做(LC 在 Phase 2 就活了),但 Phase 4 的 .env 就得等 token 到手才寫完。兩種都可以,不可以做的是 Phase 3 跟 Phase 4 對調


3. Phase 0:前置盤點

這一階段全部是唯讀操作——SELECTpg_dump、看服務狀態、讀設定檔。對任何環境(含 POC)都可以直接做、不需請示。會改變環境狀態的動作從 Phase 1 才開始。

3.1 主產品 migration 落差比對

# 兩邊各撈一次已套清單(密碼查 .env DB_SECRET)
psql -h 192.168.50.188 -p 25432 -U cmmgr -d guidant_ai_dev \
  -Atc "SELECT filename FROM public.schema_migrations ORDER BY 1" > /tmp/mig-dev.txt

psql -h <目標 DB_HOST> -p 25432 -U cmmgr -d <目標 DB_NAME> \
  -Atc "SELECT filename FROM public.schema_migrations ORDER BY 1" > /tmp/mig-target.txt

# DEV 有、目標環境沒有的(左欄獨有)
comm -23 /tmp/mig-dev.txt /tmp/mig-target.txt

3.2 目標環境現有服務/DB 狀態

ssh jedi@<目標主機> 'systemctl is-active license-center; \
  curl -s -o /dev/null -w "LC:%{http_code}\n" http://127.0.0.1:5062/; \
  curl -s -o /dev/null -w "BE:%{http_code}\n" http://127.0.0.1:8000/swagger-ui/'

# LC DB 是否已存在
psql -h <目標 DB_HOST> -p 25432 -U cmmgr -d postgres \
  -Atc "SELECT datname FROM pg_database WHERE datname LIKE 'license_center%'"

3.3 決策點:哪些 migration 這次要套

比對出來的落差清單不等於待辦清單。落差裡會混進「不屬於本版範圍」的東西,這些要明確決定不套,而不是順手一起套掉。

v1.14.0 的實際決策(2026-08-10,決策者裁示):

migration 決策 理由
七支 *-fr062-*(見 §4.1) 本版範圍
2026-07-20-ssp-shared-metadata-DEV-cleanup.sql 不動 DEV 專用清理腳本,STG/POC 有各自對應的 *-STG-cleanup / *-POC-cleanup
2026-08-01-fr058-22-openscap-content-path-advanced.sql 不動 FR-058 範圍,非本版
2026-08-03-fr060-1-detection-profile-split.sql 不動 FR-060 範圍,非本版

「三環境 schema 零落差」是上版前的驗收條件,不是每次部署的目標。

開發期 DEV 領先屬正常狀態。把不屬於本版的 migration 一起帶上去,等於偷渡了未經驗收的變更——FR-058 就發生過「11 支 migration 全同步進三環境,其中 4 支 seed 讓 POC 顯示四個 available 但 agent 無對應 connector」的事故。


4. Phase 1:DB migration

🔴 環境異動鐵律:開發期間只套 DEV。STG/POC 必須有決策者「當次」明確放行才可執行。

「只是加個欄位應該無害」正是出事的起點。派工單或交接文件若寫「三環境都套」,該指令本身可能就是錯的,停下問決策者。誤套了就停下回報,不要自行補救——回滾也是異動。

4.1 主產品:七支 FR-062 migration

依日期順序套(同日的照下表順序,後三支對前面建立的物件做調整,不可提前):

# 檔名 做什麼
1 2026-08-08-fr062-2-tenant-licenses.sql config.tenant_licenses(租戶級 RLS)+ 全域時鐘回撥浮水印
2 2026-08-08-fr062-2-license-menu-route.sql ui_routes 登記 License 管理頁+授權狀態頁
3 2026-08-09-fr062-tenant-license-events.sql config.tenant_license_events(append-only 生命週期事件表)
4 2026-08-10-fr062-8-license-menu-restructure.sql 選單結構調整:license.read 能力點(is_platform=true)+ group-license 群組 + 關閉舊儀表板入口
5 2026-08-10-fr062-9-license-manual-suspend.sql tenant_licensessuspended_at / suspend_reason,人為停權與到期管線拆成兩個正交軸
6 2026-08-10-fr062-10-license-lc-events.sql tenant_license_events.event_type 值域加 lc_issue / lc_extend
7 2026-08-10-fr062-license-group-sort-last.sql group-license 選單群組沉底(sort 45→90)
cd /path/to/compliance-manager-be
for f in \
  scripts/sql/2026-08-08-fr062-2-tenant-licenses.sql \
  scripts/sql/2026-08-08-fr062-2-license-menu-route.sql \
  scripts/sql/2026-08-09-fr062-tenant-license-events.sql \
  scripts/sql/2026-08-10-fr062-8-license-menu-restructure.sql \
  scripts/sql/2026-08-10-fr062-9-license-manual-suspend.sql \
  scripts/sql/2026-08-10-fr062-10-license-lc-events.sql \
  scripts/sql/2026-08-10-fr062-license-group-sort-last.sql ; do
  echo "=== $f"
  psql -h <DB_HOST> -p 25432 -U cmmgr -d <DB_NAME> \
       --single-transaction -v ON_ERROR_STOP=1 -f "$f" || break
done

帳號一律 cmmgr(系統管理員繞 RLS;cm_app 受 RLS 會 silent fail)。密碼請查 .envDB_SECRET,任何情況下都不要寫進指令稿或文件。port 必帶 -p 25432(非預設 5432,漏了症狀長得像 connection refused)。

每支腳本收尾都會 INSERT public.schema_migrationsON CONFLICT DO NOTHING),所以重跑是安全的,但整支腳本本身不見得冪等——出錯時看 --single-transaction 已自動回滾該支,修完再跑。

4.2 License Center:建 DB + 兩支 migration + seed

# 1) 建 DB(命名新制:license_center_{dev|stg};prod 不帶後綴)
psql -h <DB_HOST> -p 25432 -U cmmgr -d postgres -c 'CREATE DATABASE license_center_stg'

# 2) 套 LC repo 的兩支 migration(依序)
cd /opt/license_center
psql -h <DB_HOST> -p 25432 -U cmmgr -d license_center_stg \
  --single-transaction -v ON_ERROR_STOP=1 -f scripts/sql/001_create_plans_and_license_issuance.sql
psql -h <DB_HOST> -p 25432 -U cmmgr -d license_center_stg \
  --single-transaction -v ON_ERROR_STOP=1 -f scripts/sql/002_create_api_tokens.sql

# 3) seed 預設方案(basic / professional / enterprise)
#    ⚠️ 需 .env 先就緒(Phase 2 §5.2),CLI 走同一組 DB 連線設定
PYTHONPATH=src poetry run python -m license_center.cli.main seed-plans

DB user 目前沿用 cmmgr(上 PRD 前要換成 LC 專屬帳號,見 LC 手冊安全備忘)。


5. Phase 2:License Center 部署

單機安裝的完整細節在 LC 部署手冊license_center/docs/deployment-guide.html),此處只給順序與本手冊視角的重點。

步驟 一句話 LC 手冊對應
5.1 拉 code git clone / git pull/opt/license_center §2 前置需求
5.2 .env 放 repo 根目錄(程式 load_dotenv() 自讀)。LICENSE_CENTER_DB_NAME 要指對環境 §4 .env 設定
5.3 poetry install 裝依賴、建 venv §2
5.4 keygen 每環境獨立一把,私鑰不離開該機 §5 金鑰對產生
5.5 systemd license-center.service 常駐 §6 systemd 常駐
5.6 啟動驗證 curl 回 302、後台可登入、passphrase 自驗通過 §9 驗收 checklist

5.2 .env:兩個最容易寫錯的欄位

LICENSE_CENTER_DB_NAME 指錯環境 = 在別的環境的 DB 上簽照。

從別台機器 scp 過來的 .env 範本最容易留著上一個環境的 DB 名。改完務必回頭 grep 一次。同理 LICENSE_CENTER_BIND_HOST——預設 127.0.0.1 只綁本機,要對外服務才設 0.0.0.0(且必須在內網防火牆內,不應直接曝露公網)。

密碼類欄位(LICENSE_CENTER_DB_PASSWORD / ADMIN_PASSWORD / SECRET_KEY / KEY_PASSPHRASE一律查密碼管理器或部署文件,不寫進任何會 commit 的檔案

5.4 keygen:passphrase 先產先存,再貼

順序很重要——先產 passphrase 存進密碼管理器,再拿去 keygen,不要 keygen 完才回頭補記:

cd /opt/license_center && export PYTHONPATH=src
python3 -c "import secrets; print(secrets.token_hex(32))"   # 先產、先存密碼管理器
poetry run python -m license_center.cli.main keygen          # 互動輸入兩次 passphrase
cat keys/public_keys.json                                    # 取得 kid + pubkey → Phase 3 要用

passphrase 遺失,且該鑰「還沒簽過任何照」時,重產的成本是零。

2026-08-10 STG 首把鑰 ef99f508c74e43fd 就是因 passphrase 對不上而作廢重產成 04b1e65f100c6c8f——因為當時一張照都還沒簽,直接重來最快。

但一旦簽過照就完全不同:私鑰遺失 = 該環境存量照無法補發重簽,只能換鑰(重 keygen + 更新 PUBLIC_KEYS + 全部照重簽)。所以「先存密碼管理器」這個順序在正式環境是硬要求。

5.5 systemd:PYTHONPATH=src 為什麼不能放 .env

unit 檔內必須寫 Environment=PYTHONPATH=src放進 .env 無效——Python 的模組解析(import license_center)發生在程式執行 load_dotenv() 之前,等 .env 被讀進來時 import 早就失敗了。症狀是 ModuleNotFoundError: license_center

同理 ExecStart 要用 venv 的絕對路徑(venv 不在專案內時用 poetry env info --path 查),WorkingDirectory 要指對(.env 靠它才找得到)。


6. Phase 3:公鑰回填 BE 🔴

🔴 這是整套部署最容易漏、漏了整條死的一步。

症狀:LC 簽發成功(後台看得到那筆 license_issuance),但產品端上傳/落地時回「未知的 kid: xxxx(公鑰列表無對應項)」,該環境所有照全數拒收。

因為 LC 那端一切正常,很容易往「照簽壞了」的方向查。判準很簡單:去 BE 目標機上 grep 那個 kid,找不到就是這一步沒做完。

6.1 三個動作

# ① 在 LC 機器上取 kid + 公鑰
cat /opt/license_center/keys/public_keys.json

② 加進 BE 的 common/license/public_keys.py——陣列 append,不是取代

PUBLIC_KEYS: list[dict] = [
    {   # DEV 簽發鑰(開發者本機 LC,2026-08-08 起)
        "kid": "2ce3bb59a6f5a2ce",
        "pubkey": "/WVn5Kvbc6EOnl2L3m7qOwlR5xL7gMWtznjlKEOYvUQ=",
    },
    # ... 既有各環境的鑰全部保留 ...
    {   # ← 新環境的鑰 append 在最後,附註解說明是哪個環境、哪天產的
        "kid": "<新 kid>",
        "pubkey": "<新 pubkey>",
    },
]

多把共存是必要的,不可「換成新的」。

PUBLIC_KEYS全域共用的一份程式碼,同一份 BE 原始碼會部署到 DEV/STG/POC 三個環境。把舊 kid 換掉,等於讓其他環境的存量照全部驗不過。金鑰輪替也是靠這個機制過渡(新舊雙鑰並存,等舊照全數換發完再移除)。

commit,然後——

該環境的 BE 部署必須帶到這個 commit。

Phase 4 拉 code 時如果拉到的是這個 commit 之前的版本,前面兩步全部白做。每次 keygen ⇒ 一個 BE commit ⇒ 該環境 BE 重新部署,這三件事是一組的

6.2 為什麼公鑰放程式碼,不放設定檔或 DB

這是刻意的設計決策(design.md §4.2/§4.9),不是圖方便:

放設定檔或 DB 的話,能改到設定檔/DB 的人就能換掉公鑰,換掉公鑰就能用自己的私鑰簽假照。 而落地部署(host 版)的整個威脅模型,就是假設客戶對自己機房裡的機器有完整存取權。公鑰編譯進程式碼,把「偽造授權」的門檻從「改一行設定」提高到「改 Python 原始碼並重啟服務」——後者留得下痕跡,也不是靠部署權限就能無聲完成的。

🔴 客戶落地版(Docker image)的連帶要求:正式環境的簽發鑰必須在 build image 之前就進 PUBLIC_KEYS

image 一旦封裝出去,公鑰列表就凍結了——事後才產的鑰簽出的照,那個 image 一張都驗不過,只能重 build 重出貨。


7. Phase 4:BE 部署

7.1 拉 code

必須包含 Phase 3 那個公鑰 commit。部署後先驗一次:

grep -c '"kid"' /opt/compliance-manager-be/common/license/public_keys.py   # 應等於已部署環境數
grep '<本環境 kid>' /opt/compliance-manager-be/common/license/public_keys.py

7.2 .env 三項

變數 說明
DEPLOYMENT_MODE saashost 部署形態語意,供 license 驗證引擎區分行為(host 才綁機器指紋)。預設 saas請照時 BE 會把這個值帶給 LC(見 §10.3)
LICENSE_ACTIVATION_SERVER_URL http://<LC 位址>:5062 指向該環境自己的 LC。LC 與 BE 同機時用 http://127.0.0.1:5062
LICENSE_CENTER_API_TOKEN Phase 6 產出後回填 打 LC 內部簽發 API 用的 Bearer token。此時可先留空

另有兩個保險絲(出廠都是開,一般不動):LICENSE_ENFORCEMENT_ENABLED(執法總開關)、LICENSE_READONLY_GATE_ENABLED(只關「過期後全域擋寫入」、保留模組守門)。

7.3 重啟

# BE 重啟必 kill -9——既有紀律,普通 kill 會留孤兒 PID 佔住 8000
pkill -9 -f main_app.py
cd /opt/compliance-manager-be && nohup python main_app.py > /dev/null 2>&1 &
tail -50 log/app.log            # 確認起得來、無 traceback

8. Phase 5:FE 部署

拉 code → npm ci && npm run build → 佈署到該環境既有位置(STG/POC 皆為 nginx /var/www/html/audit-manager)。流程與過去各版相同,本版沒有特殊步驟。

本版 FE 的 License 相關改動(compliance-manager-fe,版號同步 v1.14.0):

  • 「向 LC 請照」dialog(新簽/展延,含照型態選項——admin 可發 Trial 照)
  • 停權文案與狀態快取失效、SaaS 版到期橫幅與唯讀提示改用不提「上傳照」的文案、SaaS 版隱藏客戶側開通/續約入口
  • 登出修復:登出後不再誤打 /license/statusafterEach 排除未登入態+登出 reset store),並在送 LOGOUT 前先等 chatbot session 清完
  • 授權歷程事件標籤補 lc_issue / lc_extend 翻譯

9. Phase 6:API token 發放

供產品 BE 呼叫 LC 內部簽發 API(/api/internal/issue/extend/plans)用。

  1. LC 後台登入 → API Token 管理新增:名稱用 product-{env} 命名慣例(STG 現況名為 Staging、POC 為 GuidantAI-POC),選對 environment
  2. 複製明文 token(見下方警示)
  3. 貼進產品 BE .envLICENSE_CENTER_API_TOKEN
  4. 重啟 BE 才生效(config 在啟動時讀入)

明文只顯示一次——當場複製。

關掉那個畫面就再也拿不回來(DB 只存雜湊),只能作廢重發。

撤銷 = 在 LC 後台停用該列,即時生效(不必等 BE 重啟)。


10. Phase 7:既有租戶發照

10.1 走產品後台「向 LC 請照」,不走 T-7.2 腳本

FR-062.10 上線後,發照的正解是產品後台的「向 LC 請照」按鈕。

scripts/ 下的 T-7.2 發照 migration 腳本(SOP 見 existing-tenant-license-migration-sop.md護欄只認 guidant_ai_dev,且刻意不提供 --db-name 逃生門——它是 DEV 演練用的,STG/POC 一律不用。2026-08-10 的 STG/POC 發照就是走「向 LC 請照」完成的。

操作路徑:產品 root 後台 → 授權管理(選單最下方 授權管理 群組)→ 選租戶 → 向 LC 請照 → 選方案(basic/professional/enterprise)、照型態、天數 → 送出。

底層流程:BE 帶 API token 打 LC POST /api/internal/issue → LC 簽章回照 → BE 驗章後走既有 upload_license 漏斗落地,事件記為 lc_issue(展延為 lc_extend)。

10.2 每個頂層客戶租戶各發一張

照綁在頂層租戶上(子租戶透過租戶樹 resolve 到頂層的照),所以逐一為每個頂層客戶租戶發一張即可,子租戶不必也不能各發。root tenant 無條件豁免 license 執法、永遠不持照——這是設計如此,不是漏發。

10.3 deployment_mode 會自動帶站台 config

commit 8c82e8e6 之後,請照未指定 deployment_mode 時會自動帶該站台的 DEPLOYMENT_MODE

該 commit 之前部署的環境,請照時若沒明確指定,LC 會用自己的預設值猜——POC 首發那張照就因此被簽成 saas(POC 應為 host),只能連同事件清空重發。

判準:發完照去產品後台看那張照的 deployment_mode 欄位,跟該環境的 DEPLOYMENT_MODE 對不上就是踩到了。


11. 部署驗收 checklist

逐項勾,不要跳。前四項驗 LC 自身,後四項驗端到端。


12. Troubleshooting

以下全部是 2026-08-10 STG/POC 實戰時真的踩到的,按「症狀 → 原因 → 解法」排。

症狀 原因 解法
ModuleNotFoundError: license_center 沒設 PYTHONPATH=src 手動跑要 export PYTHONPATH=src;systemd 要用 Environment=PYTHONPATH=src.env 無效,模組解析早於 dotenv 載入)
LC 簽發回 503「尚未就緒」 LICENSE_CENTER_KEY_PASSPHRASE 未設,或改了 .env 沒重啟 補設後 systemctl restart license-center
LC 簽發回 **503「私鑰解鎖失敗」 passphrase 與 keygen 當下設的不符 先跑驗證 one-liner 確認(見下方 ⚠️)。救不回且該鑰未簽過任何照**時,重 keygen 成本為零,直接重產+回填新公鑰
產品端「未知的 kid Phase 3 沒做,或 BE 沒重新部署到含公鑰的 commit 去 BE 目標機 grep 該 kid;缺就補 §6 三步驟
請照回 400 參數或值域問題 查 BE log:grep 'license.lc' log/app.log
請照回 401 API token 對不上——.env 貼錯,或貼完沒重啟 BE 重貼 token → 重啟 BE(token 在啟動時讀入)
照記載的 deployment_mode 見 §10.3 重新「新簽」一張——rebind 不會修,rebind 是同內容換綁定

⚠️ passphrase 驗證 one-liner 必須先 load_dotenv()

漏了這行,指令會讀不到 .env 裡的 passphrase 而回失敗,把「沒讀到」誤判成「填錯」——2026-08-10 就因此多繞了一圈,差點把一把其實正確的鑰判死。

cd /opt/license_center
poetry run python -c "
from dotenv import load_dotenv; load_dotenv()
from license_center.web.keys import unlock_private_key, passphrase_from_env
unlock_private_key(passphrase_from_env() or ''); print('passphrase 正確')"

log 位置:LC → journalctl -u license-center -f;產品 BE → log/app.log(repo 根目錄下)。


13. 環境現況對照表

DEV STG POC
主機 開發者本機 192.168.50.188 192.168.50.189
BE ~/Projects/.../compliance-manager-be(:8000,main_app.py /opt/compliance-manager-be(:8000) /opt/compliance-manager-be(:8000)
FE npm run dev(:5180) nginx /var/www/html/audit-manager(:80) nginx /var/www/html/audit-manager(:80)
LC 位置 ~/Projects/.../license_center(:5062) /opt/license_center(:5062) /opt/license_center(:5062)
LC 啟動方式 手動 PYTHONPATH=src poetry run python -m license_center.web.app systemd license-center.service(enabled+active,BIND_HOST=0.0.0.0 已起(:5062)
產品 DB guidant_ai_dev(188:25432) guidant_ai_stg(188:25432) guidant_ai_poc(189:25432)
LC DB license_center_dev(188:25432) license_center_stg(188:25432) license_center_poc(189:25432)
簽發鑰 kid 2ce3bb59a6f5a2ce 04b1e65f100c6c8f f3b562a6d9755527
LC API token 名稱 (本機自行發放) Staging(env=stg) GuidantAI-POC(env=production)
v1.14.0 部署狀態 全部完成(開發環境) BE/FE/LC 部署完成;兩租戶正式發照由決策者手動執行 BE/FE/LC 部署完成;現行照 1 張(102 Billows Tech,host 模式)

🔴 POC 等同 production(對外 demo/客戶試玩)。唯讀操作(SELECT/pg_dump/看容器/讀設定)可自由做、不需請示;任何寫入(含 migration、重啟、部署、改設定)一律停下問決策者。

STG 首把簽發鑰 ef99f508c74e43fd 因 passphrase 遺失重產,未簽過任何照即作廢,不保留——PUBLIC_KEYS 內只有上表三把。


14. 相關文件

文件 內容
design.md FR-062 設計定稿(§4.2 簽章與驗證/§4.7 送照時序/§4.9 金鑰生命週期)
license_center/docs/deployment-guide.html LC 單機部署細節手冊(DB/.env/keygen/systemd/token/安全備忘)——本手冊 §5 全部指向它
existing-tenant-license-migration-sop.md T-7.2 發照腳本 SOP——僅適用 DEV,STG/POC 走 §10 的「向 LC 請照」
docs/user-manual/license-guide.md 授權管理使用指南(平台管理員+租戶使用者兩視角)
handoff/fr062-STATE.md FR-062 當下狀態(living)
FR-062 · v1.14.0 全景部署手冊 · 2026-08-10 · LC 單機細節見 license_center/docs/deployment-guide.html