refactor: v2.0 完全解耦 — 阿里云内闭环

- 删除 VOC_DATA_DIR / get_voc_conn(不再跨云直读 SQLite)
- 案例 DB 自带 comments 表,自包含所有数据
- 新增 POST /import-voc:通过 VOC 公网 API 导入评论
- VOC_API_BASE 环境变量控制 API 地址
- 新增 httpx 依赖
This commit is contained in:
2026-04-07 19:47:34 +08:00
parent ec8eaa0b36
commit c5e2a58258
5 changed files with 194 additions and 191 deletions
+29 -52
View File
@@ -1,9 +1,11 @@
"""
黑手党提案 — 数据库管理
黑手党提案 — 数据库管理(完全独立,阿里云内闭环)
双库设计:
1. 案例 DB(读写):每个提案案例一个 SQLite,存分析结果
2. VOC DB(只读):读取共享 VOC 数据层的原始评论
每个提案案例一个 SQLite 文件,自包含所有数据:
- case_card:案例元信息
- comments:从 VOC API 导入的评论(本地副本)
- ude_sentences / ude_clusters:UDE 分析结果
- conflicts / proposal_sections:后续阶段
"""
import os
import sqlite3
@@ -17,10 +19,11 @@ load_dotenv()
DATA_DIR = Path(__file__).parent / "data"
DATA_DIR.mkdir(exist_ok=True)
VOC_DATA_DIR = Path(os.getenv("VOC_DATA_DIR", ""))
# VOC 公网 API(腾讯云,跨云只读访问)
VOC_API_BASE = os.getenv("VOC_API_BASE", "https://brand.brainwork.club/voc/api/research")
# ═══════════ 案例 DB(读写) ═══════════
# ═══════════ Schema ═══════════
CASE_SCHEMA = """
CREATE TABLE IF NOT EXISTS case_card (
@@ -29,13 +32,25 @@ CREATE TABLE IF NOT EXISTS case_card (
focus_product TEXT,
competitors TEXT,
voc_research_id TEXT,
voc_api_base TEXT,
created_at TEXT DEFAULT (datetime('now')),
status TEXT DEFAULT 'draft'
);
CREATE TABLE IF NOT EXISTS comments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
voc_id INTEGER,
platform TEXT,
text TEXT NOT NULL,
like_count INTEGER DEFAULT 0,
published_at TEXT,
imported_at TEXT DEFAULT (datetime('now')),
UNIQUE(voc_id)
);
CREATE TABLE IF NOT EXISTS ude_sentences (
id INTEGER PRIMARY KEY AUTOINCREMENT,
voc_comment_id INTEGER,
comment_id INTEGER REFERENCES comments(id),
ude_text TEXT NOT NULL,
confidence REAL DEFAULT 0.5,
vector TEXT,
@@ -73,8 +88,10 @@ CREATE TABLE IF NOT EXISTS proposal_sections (
"""
# ═══════════ 案例 DB ═══════════
def get_case_conn(case_id: str) -> sqlite3.Connection:
"""获取案例 DB 连接(读写)"""
"""获取案例 DB 连接"""
path = DATA_DIR / f"{case_id}.db"
if not path.exists():
raise FileNotFoundError(f"案例 {case_id} 不存在")
@@ -93,8 +110,8 @@ def init_case_db(brand_name: str, category: str = "", focus_product: str = "",
conn.row_factory = sqlite3.Row
conn.executescript(CASE_SCHEMA)
conn.execute(
"INSERT INTO case_card (brand_name, category, focus_product, competitors, voc_research_id) VALUES (?,?,?,?,?)",
(brand_name, category, focus_product, competitors, voc_research_id)
"INSERT INTO case_card (brand_name, category, focus_product, competitors, voc_research_id, voc_api_base) VALUES (?,?,?,?,?,?)",
(brand_name, category, focus_product, competitors, voc_research_id, VOC_API_BASE)
)
conn.commit()
conn.close()
@@ -111,11 +128,13 @@ def list_cases() -> list[dict]:
conn.row_factory = sqlite3.Row
card = conn.execute("SELECT * FROM case_card LIMIT 1").fetchone()
if card:
comment_count = conn.execute("SELECT count(*) FROM comments").fetchone()[0]
ude_count = conn.execute("SELECT count(*) FROM ude_sentences").fetchone()[0]
cluster_count = conn.execute("SELECT count(*) FROM ude_clusters").fetchone()[0]
cases.append({
"case_id": case_id,
**dict(card),
"comment_count": comment_count,
"ude_count": ude_count,
"cluster_count": cluster_count,
})
@@ -123,45 +142,3 @@ def list_cases() -> list[dict]:
except Exception:
pass
return cases
# ═══════════ VOC DB(只读) ═══════════
def get_voc_conn(voc_research_id: str) -> sqlite3.Connection:
"""只读访问共享 VOC 数据"""
if not VOC_DATA_DIR.exists():
raise FileNotFoundError(f"VOC 数据目录不存在: {VOC_DATA_DIR}")
path = VOC_DATA_DIR / f"{voc_research_id}.db"
if not path.exists():
raise FileNotFoundError(f"VOC 研究 {voc_research_id} 不存在")
conn = sqlite3.connect(f"file:{path}?mode=ro", uri=True)
conn.row_factory = sqlite3.Row
return conn
def list_voc_researches() -> list[dict]:
"""列出共享 VOC 数据层中的所有研究"""
if not VOC_DATA_DIR.exists():
return []
researches = []
for db_file in sorted(VOC_DATA_DIR.glob("*.db")):
if db_file.name in ("global_cache.db", "agent_sessions.db"):
continue
rid = db_file.stem
try:
conn = sqlite3.connect(f"file:{db_file}?mode=ro", uri=True)
conn.row_factory = sqlite3.Row
card = conn.execute("SELECT brand_name FROM research_card LIMIT 1").fetchone()
comment_count = conn.execute(
"SELECT count(*) FROM comments WHERE length(text) > 10"
).fetchone()[0]
conn.close()
if card and comment_count > 0:
researches.append({
"research_id": rid,
"brand_name": card["brand_name"],
"comment_count": comment_count,
})
except Exception:
pass
return researches