jim800121chen 4d0b870480 feat(visionA-backend): DB 接入 — 6 store 接 PostgreSQL/Redis 持久化(塊 0-5)
把 visionA-backend 6 個 in-memory store 接到資料庫持久化,範圍=完整
(PG 全接 + session 接 Redis + 交易韌性)。interface / handler 不動,
只加 DB 實作 + 換 wiring,config 未設 DB 時保留 in-memory fallback。

- 塊 0 基礎建設:pgx/v5 連線池 + DatabaseConfig/RedisConfig + golang-migrate
  runner(embed)+ cmd/migrate + testcontainers 測試基礎建設
- 塊 1 model → Postgres:array 映射、upsert 保留 CreatedAt、faa_object_key、
  三維 filter(owner/chip/source)、soft-delete partial index
- 塊 2 device → Postgres:partial unique(已刪 serial 可重註冊)、雙狀態欄位
- 塊 3 token → Postgres:pairing_tokens + session_tokens 分表、token_hash 當 PK
- 塊 4 userSession → Redis:idle + absolute 雙 TTL 取代 cleanup goroutine
  (tunnel session 維持 in-memory,yamux handle 不可序列化)
- 塊 5 交易/韌性:WithTx helper + 刪 device cascade 撤銷 token(同 tx 原子)
  + /healthz ping PG/Redis(fail-fast 503)+ pgx error 統一映射(不洩漏 raw error)

降級策略(fail-fast):PG 掉 → 持久資料 API 回 503;Redis 掉 → session 失敗
不自動 fallback in-memory(避免多機 session 不同步)。

DB:PostgreSQL 14.23(gen_random_uuid 內建、無 citext → email 用 lower() unique
index)。每塊經 Reviewer 審查 + 真 PG/Redis testcontainers 全量 dbtest 綠燈,
in-memory fallback 未受影響。

docs: 同步更新 database.md(schema/config/migration 清單)+ api-spec.md
(409/503 錯誤碼、/healthz 新行為、device unpair cascade)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 18:28:04 +08:00

303 lines
13 KiB
Plaintext
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# visionA-backend 環境變數範本
#
# 使用方式:
# cp .env.example .env
# # 視情況修改 .env 內的值(尤其 VISIONA_STORAGE_SIGNING_SECRET 與 VISIONA_PAIRING_TOKEN
#
# ⚠️ 不要把 .env commit 進 git已在 .gitignore 中排除)
# 相關文件:
# - .autoflow/04-architecture/build-deploy.md §9變數對照表
# - internal/config/config.go每個欄位的定義
# ============================================================
# 共用
# ============================================================
# 日誌等級debug / info / warn / error
VISIONA_LOG_LEVEL=info
# ============================================================
# api-server
# ============================================================
# 對前端的 REST / WebSocket port對齊 local-tool 的 base URL 預設)
VISIONA_API_PORT=3721
# api-server 連 remote-proxy 的 internal HTTP base URL
# 本機 go run 時用 localhostdocker-compose 內部會被 compose 覆寫為 http://remote-proxy:3801
VISIONA_PROXY_INTERNAL_URL=http://localhost:3801
# Static user — Phase 0.7 security audit 後僅供 dev seedVISIONA_SEED_DEMO_DATA=true
# 與 unit test fixture 用;不再注入 api.Deps、stage/prod 留空無影響。
# 見 .autoflow/05-implementation/review/phase-0.7-security-audit.md C1。
VISIONA_STATIC_USER_ID=demo-user
# 啟動時 seed 示範資料device + model + pairing token方便前端 demo
VISIONA_SEED_DEMO_DATA=true
# CORS 白名單(逗號分隔)— 預設允許 frontend dev serverhttp://localhost:3000
VISIONA_CORS_ALLOWED_ORIGINS=http://localhost:3000
# ============================================================
# OIDC必填 — OB5 起 OIDC 是唯一認證路徑A1 起支援 public PKCE-only client
# ============================================================
# 必填欄位缺任何一項main.go 啟動時會 fatal log 退出。
#
# 對應 Innovedus Member Center 的 OIDC client 設定:
# - 在 Member Center 註冊一個 OAuth clientconfidential 或 public 皆可)
# - 取得 client_idpublic client 沒有 client_secret
# - 將 RedirectURL 加入 Member Center 的白名單
# Member Center 的 issuer不帶結尾斜線MC 的 issuer 末尾斜線必要時請保留)
# dev: http://localhost:5050
# stage: https://stage-9527.innovedus.com:7850/
# prod: https://members.innovedus.com
VISIONA_OIDC_ISSUER_URL=http://localhost:5050
# 在 Member Center 註冊的 OAuth client_id
VISIONA_OIDC_CLIENT_ID=visiona-cloud
# Client secretA1選填 — public PKCE-only client 留空)
# - 有值 → confidential client modeclient_secret + PKCE 雙保險)
# - 留空 → public PKCE-only client mode依靠 PKCE 防 code interception
# ⚠️ 不可 commitprod 用 Secrets Manager。Stage MC 配的 client `b8093fea...` 是 public留空。
VISIONA_OIDC_CLIENT_SECRET=
# Backend callback URL — 必須與 Member Center 註冊值完全一致
# dev: http://localhost:3721/api/auth/callback
# stage: https://stage-9527.innovedus.com:9527/api/auth/callback
# prod: https://api.visiona.cloud/api/auth/callback
VISIONA_OIDC_REDIRECT_URL=http://localhost:3721/api/auth/callback
# Frontend base URL — callback 完成後 302 redirect 的目的地
# dev: http://localhost:3000
# stage: https://stage-9527.innovedus.com:9527
# prod: https://app.visiona.cloud
VISIONA_FRONTEND_URL=http://localhost:3000
# Phase 0.8b 移除VISIONA_OIDC_SERVICE_CLIENT_ID / _SECRET
# 服務間認證從 OAuth client_credentials 改為 pre-shared API key見 ADR-015、conversion.md §3
# 兩個 service client env 不再讀取OIDCConfig.ServiceClientID/Secret struct 欄位
# 為了 backward compat 暫保留、但 conversion 模組不再依賴)。
# 取代設定見下方 Phase 0.8 / 0.8b 區塊的 VISIONA_CONVERTER_API_KEY
# Phase 0.8b v0.6 T4 起 visionA 端不再直接呼叫 FAA、原 VISIONA_FAA_API_KEY 已撤回)。
# Cookie HMAC 簽章 secret 至少 32 byte 隨機字串prod 用 openssl rand -hex 32
VISIONA_SESSION_SECRET=CHANGE_ME_TO_RANDOM_64_BYTES_in_production
# Cookie 設定dev 預設 host-only / non-secureprod 改 .visiona.cloud + Secure=true
VISIONA_SESSION_COOKIE_NAME=visiona_session
VISIONA_SESSION_COOKIE_DOMAIN=
VISIONA_SESSION_COOKIE_SECURE=false
# Session TTL — 預設 7 天 absolute / 24h idle
VISIONA_SESSION_ABSOLUTE_TTL=168h
VISIONA_SESSION_IDLE_TTL=24h
# Relay 對外可達 URLagent tunnel 用)— POST /api/pairing/exchange 會回給 agent。
# 雛形為空時會 fallback 到 wss://relay.visionA.cloudplaceholder
# 實機請設為實際可達的 WSS URLwss://relay.visionA.cloud
VISIONA_RELAY_PUBLIC_URL=
# ============================================================
# remote-proxy
# ============================================================
# 對 local agent 的 WebSocket tunnel port
VISIONA_TUNNEL_PORT=3800
# 對 api-server 的 internal HTTP port不對外暴露
VISIONA_PROXY_INTERNAL_PORT=3801
# ============================================================
# Tunnel 心跳 / 掉線判定(對齊 tunnel.md §4.2
# ============================================================
VISIONA_TUNNEL_HEARTBEAT_INTERVAL=10s
VISIONA_TUNNEL_IDLE_TIMEOUT=30s
# ============================================================
# StorageLocalFS — Phase 0 雛形Phase 1 會改 S3
# ============================================================
# 儲存根目錄容器內docker-compose 已 mount 成 volume
VISIONA_STORAGE_BACKEND=localfs
VISIONA_STORAGE_LOCALFS_ROOT=./data/storage
# 瀏覽器 / 上傳 client 看到的 presigned URL base
# 本機開發http://localhost:3721/storage
# docker-compose demo同上透過 host port mapping
VISIONA_STORAGE_BASE_URL=http://localhost:3721/storage
VISIONA_STORAGE_LOCALFS_BASE_URL=http://localhost:3721/storage
# HMAC 簽章 secret — 用於 LocalFS presigned URL 與Phase 1pairing token hash
# ⚠️ 生產環境必改openssl rand -hex 32 產生 64 字元 hex
VISIONA_STORAGE_SIGNING_SECRET=CHANGE_ME_IN_PRODUCTION_use_openssl_rand_hex_32
# ============================================================
# Model 上傳限制
# ============================================================
# 單檔上限MB— Phase 0 規範 100 MBPRD §8.4
VISIONA_MODEL_MAX_SIZE_MB=100
# ============================================================
# Pairinglocal agent ↔ remote-proxy 配對)
# ============================================================
# 格式vAc_ + 32 hex見 security.md §1.3
# 建議用vAc_$(openssl rand -hex 16)
# 留空代表雛形 InMemoryPairingStore 會動態配發(前端呼叫 POST /api/pairing/token
VISIONA_PAIRING_TOKEN=
# ============================================================
# Phase 0.8 / 0.8b — 轉檔功能整合converter pre-shared API key
# ============================================================
# 對齊 docs/autoflow/04-architecture/conversion.md §3 + ADR-015 + ADR-016。
#
# Phase 0.8b 變更:服務間認證從 OAuth client_credentials 改為 pre-shared API key。
#
# 啟用判定2 個欄位ConverterBaseURL / ConverterAPIKey**全部非空**才視為啟用;
# 任一缺 → 5 個 /api/conversion/* endpoint 不註冊 / 回 501。
#
# Phase 0.8b 移除(不再讀取):
# - VISIONA_OIDC_SERVICE_CLIENT_ID / _SECRETOAuth client_credentials 機制取消)
# - VISIONA_OIDC_TENANT_ID取消 tenant 概念converter 端的 user_id 仍由 visionA 灌入)
# - VISIONA_FAA_DELEGATED_TTL_SECONDSdelegated download token 機制取消,改 server-side stream proxy
#
# Phase 0.8b v0.6 T4 移除不再讀取ADR-016 撤回 v0.5 設計缺口):
# - VISIONA_FAA_BASE_URLvisionA 端不再直接呼叫 FAA
# - VISIONA_FAA_API_KEY同上download / promote 改走 converter.GetResult
# kneron_model_converter task-scheduler base URL
# dev/stagehttp://192.168.0.130:9501
# prodhttps://converter.visiona.cloud
VISIONA_CONVERTER_BASE_URL=
# Pre-shared API key — visionA → converter 服務間認證Phase 0.8b 新增ADR-015 §3
# 產生openssl rand -hex 3264 字元 hex
# 與 converter 端 CONVERTER_API_KEY env 對齊(雙方獨立持有,嚴格分環境 dev / stage / prod
# ⚠️ 不可 commitprod 用 Secrets Manager / Vaultlog 永遠不印此值全文
VISIONA_CONVERTER_API_KEY=
# 上傳模型檔大小上限MB— 與 converter 端 limit 對齊
VISIONA_CONVERTER_MAX_MODEL_SIZE_MB=500
# ============================================================
# Phase 0.9 — 模型庫 model 直連 FAA 下載ADR-017 (a)
# ============================================================
# 對齊 docs/autoflow/04-architecture/adr/adr-017-model-library-access.md §10stage e2e 實測藍本)。
#
# 下載鏈visionA 用 service client 打 MC /oauth/tokenscope files:download.delegate
# → 打 MC POST /file-access/download-tokensIssue簽 opaque fdt_ token
# → 回給 Client「FAA 下載 URL + fdt token」Client 帶 Authorization: Bearer fdt_...
# 直接 GET {FAA}/files/{object_key}(不經 visionA、不經 AWS
#
# 啟用判定MC_BASE_URL / SERVICE_CLIENT_ID / SERVICE_CLIENT_SECRET / TENANT_ID / FAA_BASE_URL
# 全部非空才啟用;任一缺 → GET /api/models/:id/download 回 501。
#
# ⚠️⚠️ 技術債ADR-017 §7 R1 / Q10第一階段 PoC 短期**共用 FAA 的 service client**
# stage 用 4242ba63...,實測可拿 files:download.delegate token。MC 規範明訂
# 「OAuth client 禁止混用 usage、secret 不共用」——這份 secret 同時被 FAA 與 visionA 持有,
# 任一邊洩漏會波及兩個服務。**正式上線前須請 MC 配發 visionA 專屬 usage=file_api client**
# 換掉此共用 client把 secret 邊界收回 visionA。
# Member Center API base URL不帶結尾斜線
# stagehttps://stage-9527.innovedus.com:7850
VISIONA_FILE_ACCESS_MC_BASE_URL=
# Service clientclient_credentials grant打 MC /oauth/token + Issue download token
# ⚠️ 技術債:第一階段 PoC 共用 FAA service clientstage4242ba63099d4f318dd3f143d27ef4c5
VISIONA_FILE_ACCESS_SERVICE_CLIENT_ID=
# Service client secret
# ⚠️ 不可 commitprod 用 Secrets Manager / Vaultlog 永遠不印此值全文
# ⚠️ 技術債:第一階段 PoC 共用 FAA service client secret正式上線前換 visionA 專屬 client
VISIONA_FILE_ACCESS_SERVICE_CLIENT_SECRET=
# 簽 download token 時帶給 MC 的 tenant_id須與 FAA validate 的 tenant 一致)
# stage732270c0-449c-489c-bfad-321e9bf89b3d
VISIONA_FILE_ACCESS_TENANT_ID=
# File Access Agent 對外 base URL不帶結尾斜線— 組回給 Client 的 download_url 用
# stagehttps://stage-9527.innovedus.com:5081
VISIONA_FILE_ACCESS_FAA_BASE_URL=
# download token 有效期(秒)— ADR-017 Q2 區間 60300s預設 120
VISIONA_FILE_ACCESS_DOWNLOAD_TOKEN_TTL_SECONDS=120
# ============================================================
# DB 接入塊 0 — PostgreSQL持久業務資料model / device / token
# ============================================================
# 對齊 docs/autoflow/04-architecture/database.md §5.5.1。
#
# 啟用判定HOST / USER / NAME 三者全非空才啟用;任一缺 →
# api-server 不建連線池,所有 repository 維持 in-memorylocal dev fallback雛形行為不變
# ⚠️ 塊 0 範圍:即使啟用,目前 repository 仍是 in-memory建池只為驗證基礎建設 + 讓 schema 就位);
# model/device/token 切到 Postgres 是塊 13。
#
# ⚠️ DB 由他人在 stage host(130) 另開 visionA 專用實例並提供連線資訊visionA 端不 provision、只接上。
# ⚠️ PASSWORD 不可 commitprod 走 Secrets Manager / Vaultlog 永遠不印密碼/DSN 全文。
# visionA 專用 PG hostcredential 已取得;他人在 130 provision。留空 = 不啟用(用 in-memory
VISIONA_DB_HOST=
# PG port預設 5432
VISIONA_DB_PORT=5432
# app role
VISIONA_DB_USER=
# ⚠️ 不可 commit走 secrets 機制
VISIONA_DB_PASSWORD=
# visionA 專用 database 名稱
VISIONA_DB_NAME=
# sslmodestage/prod 用 require / verify-full本機 testcontainers 用 disable
VISIONA_DB_SSLMODE=require
# pgxpool 連線數上限 / 常駐數
VISIONA_DB_MAX_CONNS=10
VISIONA_DB_MIN_CONNS=2
# 連線生命週期 / 建池+啟動 ping 逾時
VISIONA_DB_MAX_CONN_LIFETIME=1h
VISIONA_DB_CONN_TIMEOUT=5s
# 啟動時是否自動跑 migrate up預設 true。設 false 改用獨立 `go run ./cmd/migrate up`。
VISIONA_DB_AUTO_MIGRATE=true
# ============================================================
# DB 接入塊 4 鉤子 — Redis僅 userSessionbrowser cookie session
# ============================================================
# 對齊 docs/autoflow/04-architecture/database.md §5.5.2。
#
# ⚠️ 塊 0 只先把 config 鉤子留好main.go 尚未 wire等塊 4 接 RedisUserSessionStore
# 啟用判定HOST 非空即啟用;未啟用 → userSession 仍用 in-memoryprocess 重啟掉 session
# ⚠️ visionA 專用 Redis 實例由使用者在 130 另起、設密碼PASSWORD 不可 commit、log 不印全文。
# visionA 專用 Redis host。留空 = 不啟用(用 in-memory
VISIONA_REDIS_HOST=
# Redis port預設 6379
VISIONA_REDIS_PORT=6379
# ⚠️ visionA 專用實例必設密碼;不可 commit
VISIONA_REDIS_PASSWORD=
# Redis db index預設 0
VISIONA_REDIS_DB=0
# 建連 / 啟動 ping 逾時
VISIONA_REDIS_CONN_TIMEOUT=5s