fix(db): 補全 metadata model import 與 realtime sales ORM
ADR-017 Phase 3f-0
This commit is contained in:
@@ -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% 命中 hack;3971fd4 已落地) |
|
||||
* **ADR-017: Phase 3f 模組化收尾**(DB metadata、路由雙註冊、cache、scheduler、模板與死碼清理收斂路線圖) |
|
||||
| **PPT 簡報系統 V2(ADR-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
24
app.py
@@ -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)
|
||||
# ==========================================
|
||||
|
||||
@@ -1,14 +1,3 @@
|
||||
# database/ai_models.py
|
||||
# ⚠️ 這四個 class 的原始定義已移至 autoheal_models.py(ADR-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",
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
|
||||
41
database/realtime_sales_models.py
Normal file
41
database/realtime_sales_models.py
Normal 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)
|
||||
@@ -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 (
|
||||
|
||||
114
docs/adr/ADR-017-modularization-cleanup-roadmap.md
Normal file
114
docs/adr/ADR-017-modularization-cleanup-roadmap.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# ADR-017: 模組化收尾路線圖(Phase 3f)
|
||||
|
||||
- **狀態**: Accepted
|
||||
- **日期**: 2026-04-29
|
||||
- **觸發**: 12-Agent 全景盤點(debugger / refactor-specialist / critic / db-expert / Explore)
|
||||
- **相關 ADR**: ADR-008(188 拓撲)、ADR-011(資源隔離)、ADR-016(cache fingerprint)
|
||||
- **相關 Memory**: feedback_flask_blueprint_shadow、project_phase3e_refactor_progress、project_phase3f_cleanup_roadmap、feedback_db_metadata_import
|
||||
|
||||
## 背景
|
||||
|
||||
Phase 3e(4/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 個孤兒 service(0 引用)|
|
||||
| 模板統一 | ~50% | 三目錄並存 + 1 空檔 + 3 死檔 + 2 TemplateNotFound 風險 |
|
||||
| DB schema vs Model | ~60% | manager.py import 漏 3 模組;6 張表有 SQL 無 ORM;realtime_sales_monthly 孤兒 |
|
||||
|
||||
## 決策
|
||||
|
||||
執行 **Phase 3f 五階段收尾**,總工期估 12-15 小時(不含驗證),每階段獨立 commit、每階段 critic 審查、每階段先 SSH 驗證 production。
|
||||
|
||||
### Phase 3f-0:DB 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` 的 import,metrics 不依賴此 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 個 table;create_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 處 wrapper(174-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-check:duplicate detect 直接 raise SystemExit
|
||||
- 全 endpoint smoke test(critic 監督)
|
||||
|
||||
### Phase 3f-2:Cache 統一(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 條死路徑 mount(vendor_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 等)
|
||||
|
||||
## 風險與回滾
|
||||
|
||||
每階段獨立 commit,CI/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.py(5,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 全清
|
||||
@@ -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 fingerprint(gunicorn 多 worker 一致性) | Accepted | 2026-04-29 |
|
||||
| [017](ADR-017-modularization-cleanup-roadmap.md) | 模組化收尾路線圖(Phase 3f) | Accepted | 2026-04-29 |
|
||||
|
||||
## 規範
|
||||
|
||||
|
||||
29
docs/memory/feedback_db_metadata_import.md
Normal file
29
docs/memory/feedback_db_metadata_import.md
Normal 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 已存在的窄表。
|
||||
@@ -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-29:ADR-017 Phase 3f 模組化收尾啟動
|
||||
- **DB metadata 救急**: `database/manager.py` 改為顯式載入 permission / AI / autoheal / import / vendor / realtime_sales ORM,PostgreSQL 初始化透過 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~29:Phase 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)。
|
||||
|
||||
28
docs/memory/project_phase3f_cleanup_roadmap.md
Normal file
28
docs/memory/project_phase3f_cleanup_roadmap.md
Normal 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 manager,gunicorn 加 `--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 與根目錄模板。
|
||||
@@ -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__)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user