fix(db): 補全 metadata model import 與 realtime sales ORM

ADR-017 Phase 3f-0
This commit is contained in:
OoO
2026-04-29 21:00:46 +08:00
parent 8be332728e
commit f4149d4c05
12 changed files with 314 additions and 27 deletions

View File

@@ -69,6 +69,7 @@ ssh wooo@192.168.0.110 "ssh ollama@192.168.0.188 \"\
* **ADR-014: Autonomous Code Repair**(結合 Elephant Alpha 進行系統錯誤日誌Traceback掃描自動觸發 110 主機上 Aider 自動化執行程式碼修復,並依賴 Git + Gitea CI/CD 回滾防線進行安全發布)
* **ADR-015: Telegram Bot Menu 復活**OpenClawAwoooI_Bot 三專案共用、cmd:/menu:/await: callback prefix
* **ADR-016: daily_sales cache fingerprint**gunicorn 多 worker cache 失效改 DB MAX/COUNT 指紋;棄用 N-POST 9.4% 命中 hack3971fd4 已落地) |
* **ADR-017: Phase 3f 模組化收尾**DB metadata、路由雙註冊、cache、scheduler、模板與死碼清理收斂路線圖 |
| **PPT 簡報系統 V2ADR-014** | [docs/adr/ADR-014-ppt-report-system-v2.md](docs/adr/ADR-014-ppt-report-system-v2.md) |
## PPT 簡報系統9 種V2

24
app.py
View File

@@ -56,7 +56,7 @@ try:
try:
from scheduler import run_momo_task, run_edm_task, run_festival_task, run_auto_import_task, run_whitepage_check, run_competitor_price_feeder_task
from database.manager import DatabaseManager
from database.models import Product, PriceRecord, MonthlySummaryAnalysis
from database.models import Base, Product, PriceRecord, MonthlySummaryAnalysis
from database.edm_models import PromoProduct
except ImportError as e:
print(f"❌ 專案內部檔案缺失: {e}\n請檢查 database/ 或 services/ 目錄下的 .py 檔案是否存在。")
@@ -396,6 +396,28 @@ public_url = "服務啟動中..."
# 🚩 時區設定:台北時間 (UTC+8)
TAIPEI_TZ = timezone(timedelta(hours=8))
EXPECTED_METADATA_TABLES = {
'categories', 'products', 'price_records', 'monthly_summary_analysis',
'users', 'login_history', 'permissions', 'user_permissions',
'promo_products', 'trend_records', 'trend_keywords', 'trend_analysis',
'web_search_cache', 'telegram_users',
'ai_generation_history', 'ai_prompt_templates', 'ai_usage_tracking', 'ai_insights',
'agent_context', 'action_plans', 'action_outcomes', 'agent_strategy_weights',
'incidents', 'playbooks', 'heal_logs',
'import_jobs', 'import_config', 'notification_templates', 'ppt_reports',
'vendor_stockout', 'vendor_list', 'vendor_emails', 'email_send_log',
'realtime_sales_monthly',
}
def verify_metadata_tables():
missing = EXPECTED_METADATA_TABLES - set(Base.metadata.tables.keys())
if missing:
raise SystemExit(f"Base.metadata 漏表: {sorted(missing)}")
verify_metadata_tables()
# ==========================================
# 🔧 全域模板變數注入 (Context Processor)
# ==========================================

View File

@@ -1,14 +1,3 @@
# database/ai_models.py
# ⚠️ 這四個 class 的原始定義已移至 autoheal_models.pyADR-013 統一管理)。
# 此檔僅作向後相容 re-export shim不再重複定義 SQLAlchemy Table
# 以避免 "Table already defined for this MetaData instance" 衝突。
from .autoheal_models import ( # noqa: F401
AgentContext,
ActionPlan,
ActionOutcome,
AgentStrategyWeights,
)
# AI history and template models
from sqlalchemy import Column, Integer, String, DateTime, Text, Boolean, Float
from database.models import Base
@@ -163,6 +152,5 @@ class AIInsight(Base):
__all__ = [
"AgentContext", "ActionPlan", "ActionOutcome", "AgentStrategyWeights",
"AIGenerationHistory", "AIPromptTemplate", "AIUsageTracking", "AIInsight",
]

View File

@@ -1,5 +1,6 @@
import os
import re
import threading
from sqlalchemy import create_engine, desc, select, text, literal
from sqlalchemy.orm import sessionmaker
from datetime import datetime
@@ -7,12 +8,22 @@ from .models import Base, Category, Product, PriceRecord, MonthlySummaryAnalysis
from .user_models import User, LoginHistory # noqa: F401 - 必須在 trend_models 之前導入,解決 ForeignKey 依賴問題
from .edm_models import PromoProduct # V-Fix: 確保 EDM 模型被註冊,以便自動建表
from .trend_models import TrendRecord, TrendKeyword, TrendAnalysis, WebSearchCache, TelegramUser # noqa: F401 - 趨勢資料表
from .ai_models import AgentContext, ActionPlan, ActionOutcome, AgentStrategyWeights # noqa: F401 - AI agent 模型
from .autoheal_models import Incident, Playbook, HealLog # noqa: F401 - ADR-013 AIOps 自動修復
from .import_models import ImportJob # noqa: F401 - 確保 import_jobs 表被 Base.metadata 管理
from .permission_models import Permission, UserPermission # noqa: F401 - 確保權限表被 Base.metadata 管理
from .ai_models import AIGenerationHistory, AIPromptTemplate, AIUsageTracking, AIInsight # noqa: F401 - AI history/template
from .autoheal_models import ( # noqa: F401 - ADR-013 AIOps 自動修復表
AgentContext,
ActionPlan,
ActionOutcome,
AgentStrategyWeights,
Incident,
Playbook,
HealLog,
)
from .import_models import ImportJob, ImportConfig # noqa: F401 - 確保 import_jobs/import_config 被 Base.metadata 管理
from .notification_models import NotificationTemplate # noqa: F401 - 確保 notification_templates 表被 Base.metadata 管理
from .ppt_reports import PPTReport # noqa: F401 - 確保 ppt_reports 表被 Base.metadata 管理
from .vendor_models import VendorStockout # noqa: F401 - 確保 vendor_stockout 表被 Base.metadata 管理
from .vendor_models import VendorStockout, VendorList, VendorEmail, EmailSendLog # noqa: F401 - 確保 vendor 表被 Base.metadata 管理
from .realtime_sales_models import RealtimeSalesMonthly # noqa: F401 - 確保 realtime_sales_monthly 被 Base.metadata 管理
# 🚩 導入優化後的日誌管理模組
from utils.logger_manager import SystemLogger
@@ -20,6 +31,33 @@ from utils.logger_manager import SystemLogger
# 初始化資料庫模組專用 Logger
sys_log = SystemLogger("Database").get_logger()
_metadata_init_lock = threading.Lock()
_metadata_initialized = False
_POSTGRES_METADATA_LOCK_ID = 170017
def ensure_metadata_initialized(engine, use_postgres_lock=False):
"""冪等初始化 SQLAlchemy metadata避免一般流程重複碰 DDL。"""
global _metadata_initialized
if _metadata_initialized:
return
with _metadata_init_lock:
if _metadata_initialized:
return
if use_postgres_lock:
with engine.begin() as conn:
conn.execute(text("SELECT pg_advisory_lock(:lock_id)"), {"lock_id": _POSTGRES_METADATA_LOCK_ID})
try:
Base.metadata.create_all(conn)
finally:
conn.execute(text("SELECT pg_advisory_unlock(:lock_id)"), {"lock_id": _POSTGRES_METADATA_LOCK_ID})
else:
Base.metadata.create_all(engine)
_metadata_initialized = True
def sanitize_timestamp(timestamp_str):
"""
驗證並清理時間戳字串,防止 SQL Injection
@@ -63,6 +101,7 @@ class DatabaseManager:
'options': '-c statement_timeout=60000' # SQL 超時 60 秒
}
)
ensure_metadata_initialized(self.engine, use_postgres_lock=True)
self.Session = sessionmaker(bind=self.engine)
sys_log.info(f"[Database] ✅ 使用 PostgreSQL 資料庫 (連線池已優化)")
# ADR-013: 確保 AIOps 自動修復表存在並植入種子 PlayBook
@@ -422,4 +461,4 @@ def get_session():
finally:
session.close()
"""
return get_db_manager().get_session()
return get_db_manager().get_session()

View File

@@ -0,0 +1,41 @@
from sqlalchemy import Column, Date, DateTime, Integer, Numeric, String, Text
from database.models import Base
class RealtimeSalesMonthly(Base):
"""
即時業績月報 ORM。
這張表先由 PostgreSQL init.sql 建出,之後又被程式碼與匯入流程持續擴充。
這裡先把目前程式碼直接依賴的核心欄位納入 metadata讓 create_all、
metrics 與啟動自檢有一致的表定義。
"""
__tablename__ = "realtime_sales_monthly"
__table_args__ = {"extend_existing": True}
id = Column(Integer, primary_key=True)
日期 = Column(Date, index=True)
訂單編號 = Column(String(50), index=True)
商品ID = Column(String(100), index=True)
商品編號 = Column(String(100), index=True)
商品名稱 = Column(Text)
數量 = Column(Integer)
總業績 = Column(Numeric(15, 2))
總成本 = Column(Numeric(15, 2))
毛利 = Column(Numeric(15, 2))
退貨數量 = Column(Integer)
商品單位售價 = Column(Numeric(15, 2))
廠商名稱 = Column(String(255), index=True)
分類名稱 = Column(String(255), index=True)
商品館 = Column(String(255), index=True)
品牌名稱 = Column(String(255), index=True)
時間 = Column(String(50))
付款方式 = Column(String(100))
折扣活動名稱 = Column(String(255))
折價券折扣金額 = Column(Numeric(15, 2))
折扣金額 = Column(Numeric(15, 2))
滿額再折扣金額 = Column(Numeric(15, 2))
分期手續費 = Column(Numeric(15, 2))
created_at = Column(DateTime)

View File

@@ -14,22 +14,38 @@ CREATE TABLE IF NOT EXISTS realtime_sales_monthly (
id SERIAL PRIMARY KEY,
DATE,
VARCHAR(50),
ID VARCHAR(100),
VARCHAR(100),
TEXT,
VARCHAR(50),
INTEGER,
DECIMAL(15, 2),
DECIMAL(15, 2),
VARCHAR(200),
VARCHAR(200),
VARCHAR(200),
DECIMAL(15, 2),
退 INTEGER,
DECIMAL(15, 2),
VARCHAR(255),
VARCHAR(255),
VARCHAR(255),
VARCHAR(255),
VARCHAR(50),
VARCHAR(100),
VARCHAR(255),
DECIMAL(15, 2),
DECIMAL(15, 2),
滿 DECIMAL(15, 2),
DECIMAL(15, 2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 建立索引以加速查詢
CREATE INDEX idx_sales_date ON realtime_sales_monthly();
CREATE INDEX idx_sales_vendor ON realtime_sales_monthly();
CREATE INDEX idx_sales_category ON realtime_sales_monthly();
CREATE INDEX idx_sales_brand ON realtime_sales_monthly();
CREATE INDEX IF NOT EXISTS idx_sales_date ON realtime_sales_monthly();
CREATE INDEX IF NOT EXISTS idx_sales_order ON realtime_sales_monthly();
CREATE INDEX IF NOT EXISTS idx_sales_product_id ON realtime_sales_monthly(ID);
CREATE INDEX IF NOT EXISTS idx_sales_product_code ON realtime_sales_monthly();
CREATE INDEX IF NOT EXISTS idx_sales_vendor ON realtime_sales_monthly();
CREATE INDEX IF NOT EXISTS idx_sales_category ON realtime_sales_monthly();
CREATE INDEX IF NOT EXISTS idx_sales_hall ON realtime_sales_monthly();
CREATE INDEX IF NOT EXISTS idx_sales_brand ON realtime_sales_monthly();
-- EDM 資料表
CREATE TABLE IF NOT EXISTS edm_data (

View File

@@ -0,0 +1,114 @@
# ADR-017: 模組化收尾路線圖Phase 3f
- **狀態**: Accepted
- **日期**: 2026-04-29
- **觸發**: 12-Agent 全景盤點debugger / refactor-specialist / critic / db-expert / Explore
- **相關 ADR**: ADR-008188 拓撲、ADR-011資源隔離、ADR-016cache fingerprint
- **相關 Memory**: feedback_flask_blueprint_shadow、project_phase3e_refactor_progress、project_phase3f_cleanup_roadmap、feedback_db_metadata_import
## 背景
Phase 3e4/28-29完成 app.py 7,386→6,590 行(-10.8%),但**僅完成「搬檔」,未完成「拆乾淨」**。12-Agent 盤點揭露 6 個面向的殘留問題,且發現新的 critical 風險DB metadata import 漏洞、孤兒表)。
## 完成度真相2026-04-29 盤點基線)
| 維度 | 完成度 | 真相 |
|------|--------|------|
| 路由搬檔41 條 → 28 BP| 68% | 13 條 app.py 獨有未遷移 |
| 路由「拆乾淨」 | **0%** | app.py 41 條一條未刪28 條與 BP 同 URL 雙寫,由 first-registered-wins 決定行為 |
| `USE_MODULAR_ROUTES` 開關 | **0%** | `register_blueprints()` 從未被 app.py 呼叫,整套設計死碼 |
| services/ 模組化 | ~95% | 唯一乾淨3 個孤兒 service0 引用)|
| 模板統一 | ~50% | 三目錄並存 + 1 空檔 + 3 死檔 + 2 TemplateNotFound 風險 |
| DB schema vs Model | ~60% | manager.py import 漏 3 模組6 張表有 SQL 無 ORMrealtime_sales_monthly 孤兒 |
## 決策
執行 **Phase 3f 五階段收尾**,總工期估 12-15 小時(不含驗證),每階段獨立 commit、每階段 critic 審查、每階段先 SSH 驗證 production。
### Phase 3f-0DB metadata 救急30 分鐘,最高優先)
1. `database/manager.py` 補完 import
- `permission_models`Permission, UserPermission
- `vendor_models` 補 VendorList / VendorEmail / EmailSendLog
- `ai_models` 顯式 import 4 個 AI history/template class
- `autoheal_models` 顯式 import 7 個 AIOps class移除 `ai_models.py` re-export shim
2. 處置 `realtime_sales_monthly` 孤兒表:
- 選項 A`database/realtime_sales_models.py`(推薦,補 ORM 一致性)
- 選項 B移除 `app.py:693` 的 importmetrics 不依賴此 model
3. PostgreSQL 與 SQLite 初始化都執行全域 `Base.metadata.create_all()`PostgreSQL 路徑以 process-local guard + advisory lock 保護,避免一般流量重複碰 DDL
4. `app.py` 啟動加 metadata self-check缺表直接 `SystemExit`
5. `docker/postgres/init/01-init.sql``realtime_sales_monthly` 欄位同步 ORM避免 fresh volume 先由 init.sql 建出窄表後 create_all 無法補欄位
**驗收**Base.metadata 含全 34 個 tablecreate_all 在新環境零漏。
### Phase 3f-1路由雙註冊徹底解4-6 小時P9 切分)
**策略**:保留 Blueprint刪 app.py 對應 `@app.route`28 條13 條 app.py 獨有遷至 BP。實作順序要先處理 API shadow、次要頁面與 `/brand_assets`,首頁 `/` 最後動,因為多處 `url_for('index')` 仍依賴 app endpoint name。
子任務(按 BP 分組獨立 commit
| Sprint | 範圍 | 動作 |
|--------|------|------|
| 3f-1-a | dashboard_bp | 刪 app.py:722 `/`;確認 BP 版可用 |
| 3f-1-b | edm_bp | 刪 app.py:1029, 1280 兩條 |
| 3f-1-c | export_bp | 刪 app.py:1431-1905 共 9 條;補 `/api/export/excel/seasonality_detail` 進 BP |
| 3f-1-d | api_bp | 刪 app.py:2219-2451 共 6 條 trigger/run |
| 3f-1-e | import_bp | 刪 app.py:2691, 2980 兩條 |
| 3f-1-f | monthly_bp | 刪 app.py:3076, 3083 兩條 |
| 3f-1-g | sales_bp | 刪 app.py:3592-5809 共 7 條;**同時刪 routes/sales_routes.py 6 處 wrapper174-348**,把實作搬進 BP |
| 3f-1-h | system routes 補強 | 現有 `system_bp``/api/system` prefix公開 URL`/health` `/metrics` `/logs` `/settings` 等)需新建無 prefix 的 `system_public_bp` 或拆成 public/internal 兩個 BP不能直接塞進現有 `system_bp``/abc_analysis/detail``sales_bp`。 |
| 3f-1-i | 死碼清除 | 刪 `routes/__init__.py:32-177` 整套register_blueprints / MODULAR_ENDPOINTS / is_endpoint_modular / cleanup_duplicate_routes+ `config.py:244-254` USE_MODULAR_ROUTES |
**驗收**
- `app.url_map.iter_rules()` 任一 (rule, methods) 皆唯一
- 啟動加 self-checkduplicate detect 直接 raise SystemExit
- 全 endpoint smoke testcritic 監督)
### Phase 3f-2Cache 統一2-3 小時)
1. Dockerfile + docker-compose.yml gunicorn 加 `--preload`,只作為 COW 記憶體優化,不作為一致性保證
2. 新建 `services/cache_manager.py` 套用 ADR-016 fingerprint 模式
3. 移除三處重複 `_SALES_*_CACHE` 定義app.py:82, routes/sales_routes.py:32, services/cache_service.py:16→ 統一 import `services.cache_manager`
4. `clear_cache` endpoint 從「N-POST 廣播」改「DB fingerprint pull」已在 daily_sales bp 完成,擴及 sales
### Phase 3f-3穩定性補強1-2 小時)
1. 先在 `services/event_router.py` 補同步安全 facade`notify_failure()` / `dispatch_sync()`),因現有 `EventRouter` 只有 async `dispatch()`scheduler 直接呼叫會掉告警
2. scheduler.py 7 處裸 `except: pass`line 243, 254, 582, 587, 653, 890, 1222改為 `except Exception as e: logger.exception(...)`P1/P2 經 EventRouter 同步 facade 強制告警
3. docker-compose.yml 刪 7 條死路徑 mountvendor_routes.py / vendor_stockout_*.html
4. routes/vendor_routes.py:28-30 template_folder 改用絕對路徑,移除靠 Flask fallback 的脆弱依賴
### Phase 3f-4模板統一2-3 小時)
1. 補 2 個 TemplateNotFound`trends.html`trend_routes.py:33`login_history.html`user_routes.py:40→ 找回或停用對應 endpoint
2.`web/templates/sales_analysis.html`0-byte 空檔)
3. 刪 3 個雙寫死檔:`web/templates/brand_assets.html``web/templates/growth_analysis.html`、根目錄 `logs.html`
4. 根目錄 11 個 *.html 全搬 `templates/`,確認 docker-compose 舊 mount 已清理後,再移除 `app.py:185-188` ChoiceLoader 的 BASE_DIR fallback
5. `web/templates/` 僅留 `vendor_stockout/` 子目錄vendor_bp 自帶 template_folder
### Phase 3f-5死碼清除30 分鐘)
1. 確認並刪除 3 個孤兒 service
- `services/elephant_alpha_decision_router.py`
- `services/telegram_ai_integration.py`
- `services/watcher_agent.py`
2. `.env.example` 補齊 15+ 個程式碼實際讀但 example 缺的變數AIDER_*, ELEPHANT_ALPHA_*, HEAL_SSH_*, NVIDIA_API_KEY 等)
## 風險與回滾
每階段獨立 commitCI/CD 失敗自動回滾(依賴 ADR-014 防線。Phase 3f-1 為高風險區,必須:
1. 先在本機 `python app.py` 啟動驗證 url_map 無重複
2. critic 審 diff 過關
3. 部署後 SSH 健康檢查 `/health` + 抽測 5 條核心 endpoint
## 不做的事
- 不做模組化開關設計回填USE_MODULAR_ROUTES 已死,直接刪)
- 不做 ORM 全面遷移25 張無 SQL migration 軌跡的 table 留待 Phase 4
- 不在 3f-0 修 `docker/postgres/init/01-init.sql` 與 ORM 的 `products` 中文欄位 schema drift這是 Phase 4 migration 題
- 不做 openclaw_bot_routes.py5,543 行)拆解(留 Phase 4
- 不撤換 NGROK Token統帥 2026-04-29 明示先忽略)
## 完成定義
- app.py < 5,000 行
- url_map 零重複
- create_all 新環境零漏表
- 模板僅存 `templates/` + `web/templates/vendor_stockout/`
- gunicorn `--preload` 啟用、cache 跨 worker 一致
- scheduler 7 處 except 全改、docker-compose mount 全清

View File

@@ -37,6 +37,8 @@
| [013](ADR-013-aiops-autoheal.md) | AIOps 自動修復閉環架構(七步閉環 + SSH Jump Executor | Accepted | 2026-04-19 |
| [014](ADR-014-ppt-report-system-v2.md) | PPT 簡報系統 V2 — 原生圖表 + 9 種報告類型 | Accepted | 2026-04-20 |
| [015](ADR-015-telegram-bot-menu-restoration.md) | Telegram Bot 完整菜單系統恢復 | Accepted | 2026-04-20 |
| [016](ADR-016-daily-sales-cache-fingerprint.md) | daily_sales cache fingerprintgunicorn 多 worker 一致性) | Accepted | 2026-04-29 |
| [017](ADR-017-modularization-cleanup-roadmap.md) | 模組化收尾路線圖Phase 3f | Accepted | 2026-04-29 |
## 規範

View File

@@ -0,0 +1,29 @@
# DB Metadata Import 鐵律
## 問題
`Base.metadata.create_all()` 只會建立已載入 metadata 的 model。若 model 檔沒有被 `database/manager.py` 或其他啟動必經路徑 import新環境就會漏表但既有環境可能因舊資料庫已存在而掩蓋問題。
## 鐵律
1. 新增 SQLAlchemy model 時,必須在 `database/manager.py` 顯式 import。
2. 禁止依賴 re-export shim 或副作用 import 讓 table 偷偷進 metadata。
3. 若 init SQL 已有表,仍要補 ORM 或在 ADR 中明確說明為外部管理表。
4. `app.py` 啟動 self-check 的 expected table 清單要同步更新。
5. 新環境驗收要跑:
```bash
python3 - <<'PY'
from database.manager import Base
print(len(Base.metadata.tables))
print(sorted(Base.metadata.tables))
PY
```
## Phase 3f-0 決策
- `database/manager.py` 顯式載入 permission、AI history、autoheal、import、notification、PPT、vendor、realtime sales models。
- `database/ai_models.py` 移除 autoheal re-export shim需要 autoheal model 的程式必須直接 import `database.autoheal_models`
- `database/realtime_sales_models.py``realtime_sales_monthly` ORM讓 metrics、匯入與啟動自檢有同一份 metadata 來源。
- PostgreSQL `create_all()` 必須經 process-local guard 與 advisory lock避免多 worker 或多次 `DatabaseManager()` 在一般流程重複做 DDL 檢查。
- ORM 若對齊 init SQL 管理的表,必須同步 `docker/postgres/init/01-init.sql`,因 `create_all()` 不會 alter 已存在的窄表。

View File

@@ -1,6 +1,7 @@
# EwoooC 專案歷史紀錄 (History Logs)
## 📌 重大里程碑
- **2026-04-29**: ADR-017 Phase 3f 模組化收尾立案,啟動 DB metadata、路由雙註冊、cache、scheduler、模板與死碼清理六線收斂。
- **2026-04-18**: 專案正式正名為 **EwoooC**AI 治理架構 Phase 4 結案V10.3)。
- **2026-02-13**: WOOO AIOps SaaS 核心模組完成。
- **2026-01-24**: 曾嘗試遷移至 K3s後於 04-18 審計確認回歸 Docker Compose
@@ -10,6 +11,12 @@
## 📅 詳細更新日誌 (考古存檔)
### 2026-04-29ADR-017 Phase 3f 模組化收尾啟動
- **DB metadata 救急**: `database/manager.py` 改為顯式載入 permission / AI / autoheal / import / vendor / realtime_sales ORMPostgreSQL 初始化透過 process-local guard + advisory lock 執行 `Base.metadata.create_all()`,避免新環境漏表與一般流量重複碰 DDL。
- **realtime_sales_monthly 補 ORM**: 新增 `database/realtime_sales_models.py`,並同步 `docker/postgres/init/01-init.sql` 欄位,避免 fresh volume 先建出窄表後造成匯入欄位靜默遺失。
- **啟動自檢**: `app.py` 啟動時檢查 34 張 expected metadata tables缺表直接 fail fast防止「看似啟動成功但 create_all 漏表」。
- **路線校正**: 12-Agent 盤點確認 3f-1 需先處理 API shadow 與 `system_bp` prefix 分裂3f-3 需先補 EventRouter 同步告警 facade再改 scheduler 裸 `except`
### 2026-04-28~29Phase 3e 重構大戰 + daily_sales cache 隱形 bug 根除
- **app.py 縮減 -10.8%**: 7,386 → 6,590 行11 commits 全綠零 502。
- **抽 Blueprint**: `/api/categories``category_routes.py` (8fce73b)`/api/test_url` + `/brand_assets``misc_routes.py` (e676840)。

View File

@@ -0,0 +1,28 @@
# Phase 3f 模組化收尾任務矩陣
> 來源ADR-017。此檔只保存執行矩陣與紅線完整背景與決策以 `docs/adr/ADR-017-modularization-cleanup-roadmap.md` 為準。
## 紅線
1. 每個 sub-phase 獨立 commit。
2. 進下一階段前必須完成 critic review、本機 self-check、188 SSH 健康檢查。
3. ADR-011 永遠有效:部署與修復不得使用 `--remove-orphans`,不得影響 `momo-db`
## 時序
| Phase | 目標 | 風險 | 預估 |
|---|---|---|---|
| 3f-0 | DB metadata 救急create_all 新環境零漏表 | 最高 | 30m |
| 3f-1 | 路由雙註冊徹底解除,保 Blueprint、刪 app.py duplicate | 高 | 4-6h |
| 3f-2 | Cache 統一至 fingerprint managergunicorn 加 `--preload` | 中 | 2-3h |
| 3f-3 | scheduler 裸 `except`、EventRouter 同步告警、compose mount、vendor template_folder | 中 | 1-2h |
| 3f-4 | 模板統一至 `templates/``web/templates/vendor_stockout/` | 低 | 2-3h |
| 3f-5 | 孤兒 service 與 `.env.example` 收尾 | 極低 | 30m |
## 已校正事項
- 3f-0 metadata 驗收為 34 張表,不是 33 張。
- 3f-1 的公開 `/health``/metrics``/settings` 等不能直接遷進現有 `system_bp`,因它有 `/api/system` prefix。
- 3f-2 的 `--preload` 只降低 copy-on-write 記憶體成本,一致性仍以 DB fingerprint 為準。
- 3f-3 必須先補 `EventRouter` 同步 facade再把 scheduler P1/P2 失敗導入告警。
- 3f-4 移除 ChoiceLoader fallback 前,需先清 docker-compose 舊 mount 與根目錄模板。

View File

@@ -9,7 +9,7 @@ from sqlalchemy import text
from services.hermes_analyst_service import HermesAnalystService
from services.nemoton_dispatcher_service import NemotronDispatcher
from database.manager import get_session
from database.ai_models import AgentContext, ActionPlan
from database.autoheal_models import AgentContext, ActionPlan
logger = logging.getLogger(__name__)