refactor: migrate workflow to v2 core and archive legacy modules
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -199,14 +199,18 @@ def get_customers(limit: int = 100) -> List[Dict]:
|
||||
|
||||
|
||||
def get_conversation(customer_id: str, limit: int = 200) -> List[Dict]:
|
||||
"""返回某客户的全部对话记录(按时间升序)"""
|
||||
"""返回某客户的最近对话记录(按时间升序)"""
|
||||
with _get_conn() as conn:
|
||||
# 核心修复:先取最新的 limit 条,再按时间正序排列
|
||||
rows = conn.execute(_sql("""
|
||||
SELECT id, direction, message, msg_type, timestamp, acc_id
|
||||
FROM chat_logs
|
||||
WHERE customer_id = ?
|
||||
SELECT * FROM (
|
||||
SELECT id, direction, message, msg_type, timestamp, acc_id
|
||||
FROM chat_logs
|
||||
WHERE customer_id = ?
|
||||
ORDER BY timestamp DESC, id DESC
|
||||
LIMIT ?
|
||||
) AS recent
|
||||
ORDER BY timestamp ASC, id ASC
|
||||
LIMIT ?
|
||||
"""), (customer_id, limit)).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
Binary file not shown.
@@ -1,336 +0,0 @@
|
||||
"""客户风控数据库(MySQL 优先,SQLite 兜底)"""
|
||||
import os
|
||||
import sqlite3
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
|
||||
_DB_TYPE = os.getenv("DB_TYPE", "sqlite").lower()
|
||||
_MYSQL_HOST = os.getenv("MYSQL_HOST", "127.0.0.1")
|
||||
_MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
|
||||
_MYSQL_USER = os.getenv("MYSQL_USER", "root")
|
||||
_MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "")
|
||||
_MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "ai_cs")
|
||||
|
||||
|
||||
def _is_mysql() -> bool:
|
||||
return _DB_TYPE in ("mysql", "mariadb")
|
||||
|
||||
|
||||
class CustomerRiskDB:
|
||||
def __init__(self, sqlite_path: str = "db/customer_risk_db/risk.db"):
|
||||
self.sqlite_path = Path(sqlite_path)
|
||||
self.backend = "mysql" if _is_mysql() else "sqlite"
|
||||
self._sqlite_in_memory = False
|
||||
try:
|
||||
self._ensure_db()
|
||||
except Exception:
|
||||
# MySQL 不可用时自动回退,避免主流程被数据库连接拖垮
|
||||
self.backend = "sqlite"
|
||||
try:
|
||||
self._ensure_sqlite_db()
|
||||
except Exception:
|
||||
# 最后兜底:内存 SQLite,保证模块可导入
|
||||
self._sqlite_in_memory = True
|
||||
self._ensure_sqlite_db()
|
||||
|
||||
def _get_mysql_conn(self):
|
||||
import pymysql
|
||||
return pymysql.connect(
|
||||
host=_MYSQL_HOST,
|
||||
port=_MYSQL_PORT,
|
||||
user=_MYSQL_USER,
|
||||
password=_MYSQL_PASSWORD,
|
||||
database=_MYSQL_DATABASE,
|
||||
charset="utf8mb4",
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
autocommit=False,
|
||||
)
|
||||
|
||||
def _get_sqlite_conn(self):
|
||||
if self._sqlite_in_memory:
|
||||
conn = sqlite3.connect(":memory:")
|
||||
else:
|
||||
self.sqlite_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(str(self.sqlite_path))
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def _ensure_db(self):
|
||||
if self.backend == "mysql":
|
||||
with self._get_mysql_conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS customer_risk_profile (
|
||||
customer_id VARCHAR(128) PRIMARY KEY,
|
||||
do_not_serve TINYINT(1) NOT NULL DEFAULT 0,
|
||||
risk_level VARCHAR(16) NOT NULL DEFAULT 'low',
|
||||
risk_score INT NOT NULL DEFAULT 0,
|
||||
note TEXT,
|
||||
tags_json TEXT,
|
||||
updated_at DATETIME NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
"""
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS customer_risk_event (
|
||||
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
customer_id VARCHAR(128) NOT NULL,
|
||||
event_type VARCHAR(32) NOT NULL,
|
||||
event_count INT NOT NULL DEFAULT 1,
|
||||
note TEXT,
|
||||
created_at DATETIME NOT NULL,
|
||||
INDEX idx_customer_time (customer_id, created_at),
|
||||
INDEX idx_event_type (event_type)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
"""
|
||||
)
|
||||
conn.commit()
|
||||
return
|
||||
self._ensure_sqlite_db()
|
||||
|
||||
def _ensure_sqlite_db(self):
|
||||
with self._get_sqlite_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS customer_risk_profile (
|
||||
customer_id TEXT PRIMARY KEY,
|
||||
do_not_serve INTEGER NOT NULL DEFAULT 0,
|
||||
risk_level TEXT NOT NULL DEFAULT 'low',
|
||||
risk_score INTEGER NOT NULL DEFAULT 0,
|
||||
note TEXT,
|
||||
tags_json TEXT,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
cur.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS customer_risk_event (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
customer_id TEXT NOT NULL,
|
||||
event_type TEXT NOT NULL,
|
||||
event_count INTEGER NOT NULL DEFAULT 1,
|
||||
note TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
cur.execute("CREATE INDEX IF NOT EXISTS idx_customer_time ON customer_risk_event(customer_id, created_at)")
|
||||
cur.execute("CREATE INDEX IF NOT EXISTS idx_event_type ON customer_risk_event(event_type)")
|
||||
conn.commit()
|
||||
|
||||
def record_event(self, customer_id: str, event_type: str, event_count: int = 1, note: str = ""):
|
||||
if not customer_id or not event_type:
|
||||
return
|
||||
now = datetime.now()
|
||||
if self.backend == "mysql":
|
||||
with self._get_mysql_conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO customer_risk_event (customer_id, event_type, event_count, note, created_at)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
""",
|
||||
(customer_id, event_type, int(max(1, event_count)), note, now.strftime("%Y-%m-%d %H:%M:%S")),
|
||||
)
|
||||
conn.commit()
|
||||
return
|
||||
with self._get_sqlite_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO customer_risk_event (customer_id, event_type, event_count, note, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""",
|
||||
(customer_id, event_type, int(max(1, event_count)), note, now.isoformat()),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def set_profile(
|
||||
self,
|
||||
customer_id: str,
|
||||
*,
|
||||
do_not_serve: bool = False,
|
||||
risk_level: str = "low",
|
||||
risk_score: int = 0,
|
||||
note: str = "",
|
||||
tags: list | None = None,
|
||||
):
|
||||
if not customer_id:
|
||||
return
|
||||
tags_json = json.dumps(tags or [], ensure_ascii=False)
|
||||
now = datetime.now()
|
||||
if self.backend == "mysql":
|
||||
with self._get_mysql_conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
REPLACE INTO customer_risk_profile
|
||||
(customer_id, do_not_serve, risk_level, risk_score, note, tags_json, updated_at)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
(
|
||||
customer_id,
|
||||
1 if do_not_serve else 0,
|
||||
risk_level,
|
||||
int(max(0, risk_score)),
|
||||
note,
|
||||
tags_json,
|
||||
now.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
return
|
||||
with self._get_sqlite_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO customer_risk_profile
|
||||
(customer_id, do_not_serve, risk_level, risk_score, note, tags_json, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(customer_id) DO UPDATE SET
|
||||
do_not_serve=excluded.do_not_serve,
|
||||
risk_level=excluded.risk_level,
|
||||
risk_score=excluded.risk_score,
|
||||
note=excluded.note,
|
||||
tags_json=excluded.tags_json,
|
||||
updated_at=excluded.updated_at
|
||||
""",
|
||||
(
|
||||
customer_id,
|
||||
1 if do_not_serve else 0,
|
||||
risk_level,
|
||||
int(max(0, risk_score)),
|
||||
note,
|
||||
tags_json,
|
||||
now.isoformat(),
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
def _sum_events(self, customer_id: str, event_type: str, days: int) -> int:
|
||||
if self.backend == "mysql":
|
||||
with self._get_mysql_conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT COALESCE(SUM(event_count), 0) AS total
|
||||
FROM customer_risk_event
|
||||
WHERE customer_id=%s
|
||||
AND event_type=%s
|
||||
AND created_at >= (NOW() - INTERVAL %s DAY)
|
||||
""",
|
||||
(customer_id, event_type, int(max(1, days))),
|
||||
)
|
||||
row = cur.fetchone() or {}
|
||||
return int(row.get("total") or 0)
|
||||
with self._get_sqlite_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT COALESCE(SUM(event_count), 0) AS total
|
||||
FROM customer_risk_event
|
||||
WHERE customer_id=?
|
||||
AND event_type=?
|
||||
AND created_at >= datetime('now', ?)
|
||||
""",
|
||||
(customer_id, event_type, f"-{int(max(1, days))} day"),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
return int((row["total"] if row else 0) or 0)
|
||||
|
||||
def get_profile(self, customer_id: str) -> Dict[str, Any]:
|
||||
out = {
|
||||
"customer_id": customer_id,
|
||||
"do_not_serve": False,
|
||||
"risk_level": "low",
|
||||
"risk_score": 0,
|
||||
"note": "",
|
||||
"tags": [],
|
||||
}
|
||||
if self.backend == "mysql":
|
||||
with self._get_mysql_conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT customer_id, do_not_serve, risk_level, risk_score, note, tags_json
|
||||
FROM customer_risk_profile
|
||||
WHERE customer_id=%s
|
||||
LIMIT 1
|
||||
""",
|
||||
(customer_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
return out
|
||||
out.update(
|
||||
{
|
||||
"do_not_serve": bool(row.get("do_not_serve")),
|
||||
"risk_level": str(row.get("risk_level") or "low"),
|
||||
"risk_score": int(row.get("risk_score") or 0),
|
||||
"note": str(row.get("note") or ""),
|
||||
"tags": json.loads(row.get("tags_json") or "[]"),
|
||||
}
|
||||
)
|
||||
return out
|
||||
with self._get_sqlite_conn() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT customer_id, do_not_serve, risk_level, risk_score, note, tags_json
|
||||
FROM customer_risk_profile
|
||||
WHERE customer_id=?
|
||||
LIMIT 1
|
||||
""",
|
||||
(customer_id,),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
return out
|
||||
out.update(
|
||||
{
|
||||
"do_not_serve": bool(row["do_not_serve"]),
|
||||
"risk_level": str(row["risk_level"] or "low"),
|
||||
"risk_score": int(row["risk_score"] or 0),
|
||||
"note": str(row["note"] or ""),
|
||||
"tags": json.loads(row["tags_json"] or "[]"),
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
def evaluate_customer(self, customer_id: str) -> Dict[str, Any]:
|
||||
profile = self.get_profile(customer_id)
|
||||
refund_30d = self._sum_events(customer_id, "refund", 30)
|
||||
unpaid_7d = self._sum_events(customer_id, "unpaid_order", 7)
|
||||
bad_review_90d = self._sum_events(customer_id, "bad_review", 90)
|
||||
|
||||
score = int(profile.get("risk_score") or 0)
|
||||
score += refund_30d * 20
|
||||
score += unpaid_7d * 8
|
||||
score += bad_review_90d * 15
|
||||
|
||||
level = "low"
|
||||
if score >= 70:
|
||||
level = "high"
|
||||
elif score >= 35:
|
||||
level = "medium"
|
||||
|
||||
return {
|
||||
**profile,
|
||||
"refund_30d": refund_30d,
|
||||
"unpaid_7d": unpaid_7d,
|
||||
"bad_review_90d": bad_review_90d,
|
||||
"computed_score": score,
|
||||
"computed_level": level,
|
||||
}
|
||||
|
||||
|
||||
risk_db = CustomerRiskDB()
|
||||
@@ -1,246 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
成交/未成交记录 - 用于日报与数据分析
|
||||
"""
|
||||
import sqlite3
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import List, Dict, Optional
|
||||
|
||||
_DB_PATH = os.path.join(os.path.dirname(__file__), "deal_outcome_db", "outcomes.db")
|
||||
_DB_TYPE = os.getenv("DB_TYPE", "sqlite").lower()
|
||||
_MYSQL_HOST = os.getenv("MYSQL_HOST", "127.0.0.1")
|
||||
_MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
|
||||
_MYSQL_USER = os.getenv("MYSQL_USER", "root")
|
||||
_MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "")
|
||||
_MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "ai_cs")
|
||||
|
||||
|
||||
class _CompatResult:
|
||||
def __init__(self, rows=None, rowcount: int = 0, lastrowid: int = 0):
|
||||
self._rows = rows or []
|
||||
self.rowcount = rowcount
|
||||
self.lastrowid = lastrowid
|
||||
|
||||
def fetchall(self):
|
||||
return self._rows
|
||||
|
||||
def fetchone(self):
|
||||
return self._rows[0] if self._rows else None
|
||||
|
||||
|
||||
class _PyMySQLCompatConn:
|
||||
def __init__(self, conn):
|
||||
self._conn = conn
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
if exc_type:
|
||||
try:
|
||||
self._conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
self._conn.close()
|
||||
|
||||
def execute(self, query: str, args=None):
|
||||
cur = self._conn.cursor()
|
||||
cur.execute(query, args or ())
|
||||
rows = cur.fetchall() if cur.description else []
|
||||
res = _CompatResult(rows=rows, rowcount=cur.rowcount, lastrowid=getattr(cur, "lastrowid", 0))
|
||||
cur.close()
|
||||
return res
|
||||
|
||||
def commit(self):
|
||||
self._conn.commit()
|
||||
|
||||
def _is_mysql() -> bool:
|
||||
return _DB_TYPE in ("mysql", "mariadb")
|
||||
|
||||
def _sql(query: str) -> str:
|
||||
return query.replace("?", "%s") if _is_mysql() else query
|
||||
|
||||
|
||||
def _get_conn() -> sqlite3.Connection:
|
||||
if _is_mysql():
|
||||
import pymysql
|
||||
conn = pymysql.connect(
|
||||
host=_MYSQL_HOST,
|
||||
port=_MYSQL_PORT,
|
||||
user=_MYSQL_USER,
|
||||
password=_MYSQL_PASSWORD,
|
||||
database=_MYSQL_DATABASE,
|
||||
charset="utf8mb4",
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
autocommit=False,
|
||||
)
|
||||
return _PyMySQLCompatConn(conn)
|
||||
os.makedirs(os.path.dirname(_DB_PATH), exist_ok=True)
|
||||
conn = sqlite3.connect(_DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
def _init_db():
|
||||
with _get_conn() as conn:
|
||||
if _is_mysql():
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS deal_outcomes (
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
customer_id VARCHAR(128) NOT NULL,
|
||||
customer_name VARCHAR(255) DEFAULT '',
|
||||
acc_id VARCHAR(128) DEFAULT '',
|
||||
platform VARCHAR(64) DEFAULT '',
|
||||
date DATE NOT NULL,
|
||||
outcome VARCHAR(16) NOT NULL,
|
||||
reason TEXT,
|
||||
order_id VARCHAR(128) DEFAULT '',
|
||||
amount REAL DEFAULT 0,
|
||||
discount_given INTEGER DEFAULT 0,
|
||||
timestamp DATETIME NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""")
|
||||
idx_rows = conn.execute("SHOW INDEX FROM deal_outcomes").fetchall()
|
||||
exists = {str(r.get("Key_name", "")) for r in idx_rows}
|
||||
if "idx_deal_date" not in exists:
|
||||
conn.execute("CREATE INDEX idx_deal_date ON deal_outcomes(date)")
|
||||
if "idx_deal_customer" not in exists:
|
||||
conn.execute("CREATE INDEX idx_deal_customer ON deal_outcomes(customer_id)")
|
||||
if "idx_deal_acc" not in exists:
|
||||
conn.execute("CREATE INDEX idx_deal_acc ON deal_outcomes(acc_id)")
|
||||
if "idx_deal_outcome" not in exists:
|
||||
conn.execute("CREATE INDEX idx_deal_outcome ON deal_outcomes(outcome)")
|
||||
else:
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS deal_outcomes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
customer_id TEXT NOT NULL,
|
||||
customer_name TEXT DEFAULT '',
|
||||
acc_id TEXT DEFAULT '',
|
||||
platform TEXT DEFAULT '',
|
||||
date TEXT NOT NULL,
|
||||
outcome TEXT NOT NULL CHECK(outcome IN ('成交','未成交')),
|
||||
reason TEXT DEFAULT '',
|
||||
order_id TEXT DEFAULT '',
|
||||
amount REAL DEFAULT 0,
|
||||
discount_given INTEGER DEFAULT 0,
|
||||
timestamp TEXT NOT NULL
|
||||
)
|
||||
""")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_date ON deal_outcomes(date)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_customer ON deal_outcomes(customer_id)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_acc ON deal_outcomes(acc_id)")
|
||||
conn.execute("CREATE INDEX IF NOT EXISTS idx_deal_outcome ON deal_outcomes(outcome)")
|
||||
conn.commit()
|
||||
|
||||
|
||||
_init_db()
|
||||
|
||||
|
||||
def record_deal(
|
||||
customer_id: str,
|
||||
outcome: str,
|
||||
reason: str = "",
|
||||
customer_name: str = "",
|
||||
acc_id: str = "",
|
||||
platform: str = "",
|
||||
order_id: str = "",
|
||||
amount: float = 0,
|
||||
discount_given: bool = False,
|
||||
):
|
||||
"""记录一笔成交或未成交"""
|
||||
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
date = datetime.now().strftime("%Y-%m-%d")
|
||||
with _get_conn() as conn:
|
||||
conn.execute(
|
||||
_sql("""INSERT INTO deal_outcomes
|
||||
(customer_id, customer_name, acc_id, platform, date, outcome, reason,
|
||||
order_id, amount, discount_given, timestamp)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?)"""),
|
||||
(
|
||||
customer_id,
|
||||
customer_name or "",
|
||||
acc_id or "",
|
||||
platform or "",
|
||||
date,
|
||||
outcome,
|
||||
reason or "",
|
||||
order_id or "",
|
||||
amount,
|
||||
1 if discount_given else 0,
|
||||
ts,
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def get_daily_outcomes(date: str = "") -> List[Dict]:
|
||||
"""获取指定日期的成交/未成交记录,用于日报"""
|
||||
if not date:
|
||||
date = datetime.now().strftime("%Y-%m-%d")
|
||||
with _get_conn() as conn:
|
||||
rows = conn.execute(
|
||||
_sql("""
|
||||
SELECT customer_id, customer_name, acc_id, outcome, reason,
|
||||
order_id, amount, discount_given, timestamp
|
||||
FROM deal_outcomes
|
||||
WHERE date = ?
|
||||
ORDER BY timestamp ASC
|
||||
"""),
|
||||
(date,),
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
def get_daily_summary(date: str = "") -> Dict:
|
||||
"""获取指定日期的成交/未成交汇总统计"""
|
||||
outcomes = get_daily_outcomes(date)
|
||||
success = [o for o in outcomes if o["outcome"] == "成交"]
|
||||
fail = [o for o in outcomes if o["outcome"] == "未成交"]
|
||||
|
||||
# 按原因分组
|
||||
fail_by_reason: Dict[str, int] = {}
|
||||
for o in fail:
|
||||
r = o.get("reason") or "其他"
|
||||
fail_by_reason[r] = fail_by_reason.get(r, 0) + 1
|
||||
|
||||
return {
|
||||
"date": date or datetime.now().strftime("%Y-%m-%d"),
|
||||
"成交数": len(success),
|
||||
"未成交数": len(fail),
|
||||
"成交金额": sum(o.get("amount") or 0 for o in success),
|
||||
"成交明细": success,
|
||||
"未成交明细": fail,
|
||||
"未成交原因分布": fail_by_reason,
|
||||
}
|
||||
|
||||
|
||||
def export_for_analysis(start_date: str = "", end_date: str = "") -> List[Dict]:
|
||||
"""
|
||||
导出成交/未成交记录,供数据库分析。
|
||||
日期格式 YYYY-MM-DD,留空则查全部。
|
||||
"""
|
||||
with _get_conn() as conn:
|
||||
if start_date and end_date:
|
||||
rows = conn.execute(
|
||||
_sql("""SELECT * FROM deal_outcomes
|
||||
WHERE date BETWEEN ? AND ?
|
||||
ORDER BY date, timestamp"""),
|
||||
(start_date, end_date),
|
||||
).fetchall()
|
||||
elif start_date:
|
||||
rows = conn.execute(
|
||||
_sql("""SELECT * FROM deal_outcomes WHERE date >= ? ORDER BY date, timestamp"""),
|
||||
(start_date,),
|
||||
).fetchall()
|
||||
elif end_date:
|
||||
rows = conn.execute(
|
||||
_sql("""SELECT * FROM deal_outcomes WHERE date <= ? ORDER BY date, timestamp"""),
|
||||
(end_date,),
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"""SELECT * FROM deal_outcomes ORDER BY date, timestamp"""
|
||||
).fetchall()
|
||||
return [dict(r) for r in rows]
|
||||
@@ -1,279 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
设计师派单数据库(SQLite)
|
||||
|
||||
同一设计师在不同店铺对应不同 group_id,派单时从在线设计师中轮询。
|
||||
企微群「上线」/「下线」通过 update_online(wechat_user_id, is_online) 更新。
|
||||
"""
|
||||
import sqlite3
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
_DB_PATH = os.path.join(os.path.dirname(__file__), "designer_roster_db", "roster.db")
|
||||
_DB_TYPE = os.getenv("DB_TYPE", "sqlite").lower()
|
||||
_MYSQL_HOST = os.getenv("MYSQL_HOST", "127.0.0.1")
|
||||
_MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
|
||||
_MYSQL_USER = os.getenv("MYSQL_USER", "root")
|
||||
_MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "")
|
||||
_MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "ai_cs")
|
||||
|
||||
|
||||
class _CompatResult:
|
||||
def __init__(self, rows=None, rowcount: int = 0, lastrowid: int = 0):
|
||||
self._rows = rows or []
|
||||
self.rowcount = rowcount
|
||||
self.lastrowid = lastrowid
|
||||
|
||||
def fetchall(self):
|
||||
return self._rows
|
||||
|
||||
def fetchone(self):
|
||||
return self._rows[0] if self._rows else None
|
||||
|
||||
|
||||
class _PyMySQLCompatConn:
|
||||
def __init__(self, conn):
|
||||
self._conn = conn
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
if exc_type:
|
||||
try:
|
||||
self._conn.rollback()
|
||||
except Exception:
|
||||
pass
|
||||
self._conn.close()
|
||||
|
||||
def execute(self, query: str, args=None):
|
||||
cur = self._conn.cursor()
|
||||
cur.execute(query, args or ())
|
||||
rows = cur.fetchall() if cur.description else []
|
||||
res = _CompatResult(rows=rows, rowcount=cur.rowcount, lastrowid=getattr(cur, "lastrowid", 0))
|
||||
cur.close()
|
||||
return res
|
||||
|
||||
def commit(self):
|
||||
self._conn.commit()
|
||||
|
||||
def _is_mysql() -> bool:
|
||||
return _DB_TYPE in ("mysql", "mariadb")
|
||||
|
||||
def _sql(query: str) -> str:
|
||||
return query.replace("?", "%s") if _is_mysql() else query
|
||||
|
||||
|
||||
def _get_conn() -> sqlite3.Connection:
|
||||
if _is_mysql():
|
||||
import pymysql
|
||||
conn = pymysql.connect(
|
||||
host=_MYSQL_HOST,
|
||||
port=_MYSQL_PORT,
|
||||
user=_MYSQL_USER,
|
||||
password=_MYSQL_PASSWORD,
|
||||
database=_MYSQL_DATABASE,
|
||||
charset="utf8mb4",
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
autocommit=False,
|
||||
)
|
||||
return _PyMySQLCompatConn(conn)
|
||||
os.makedirs(os.path.dirname(_DB_PATH), exist_ok=True)
|
||||
conn = sqlite3.connect(_DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
def init_db():
|
||||
with _get_conn() as conn:
|
||||
if _is_mysql():
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS designers (
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
wechat_user_id VARCHAR(128) UNIQUE NOT NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS designer_shops (
|
||||
designer_id INTEGER NOT NULL,
|
||||
shop_id VARCHAR(128) NOT NULL,
|
||||
group_id VARCHAR(128) NOT NULL,
|
||||
PRIMARY KEY (designer_id, shop_id),
|
||||
FOREIGN KEY (designer_id) REFERENCES designers(id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS designer_online (
|
||||
wechat_user_id VARCHAR(128) PRIMARY KEY,
|
||||
is_online INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at DATETIME
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS round_robin (
|
||||
shop_id VARCHAR(128) PRIMARY KEY,
|
||||
last_index INTEGER NOT NULL DEFAULT 0
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""")
|
||||
else:
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS designers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
wechat_user_id TEXT UNIQUE NOT NULL
|
||||
)
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS designer_shops (
|
||||
designer_id INTEGER NOT NULL,
|
||||
shop_id TEXT NOT NULL,
|
||||
group_id TEXT NOT NULL,
|
||||
PRIMARY KEY (designer_id, shop_id),
|
||||
FOREIGN KEY (designer_id) REFERENCES designers(id)
|
||||
)
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS designer_online (
|
||||
wechat_user_id TEXT PRIMARY KEY,
|
||||
is_online INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at TEXT
|
||||
)
|
||||
""")
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS round_robin (
|
||||
shop_id TEXT PRIMARY KEY,
|
||||
last_index INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
""")
|
||||
conn.commit()
|
||||
|
||||
|
||||
init_db()
|
||||
|
||||
|
||||
# ========== 设计师管理 ==========
|
||||
|
||||
def add_designer(name: str, wechat_user_id: str) -> int:
|
||||
"""添加设计师,返回 id"""
|
||||
with _get_conn() as conn:
|
||||
if _is_mysql():
|
||||
conn.execute(
|
||||
"INSERT IGNORE INTO designers (name, wechat_user_id) VALUES (%s, %s)",
|
||||
(name, wechat_user_id),
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO designers (name, wechat_user_id) VALUES (?, ?)",
|
||||
(name, wechat_user_id),
|
||||
)
|
||||
conn.commit()
|
||||
row = conn.execute(_sql("SELECT id FROM designers WHERE wechat_user_id = ?"), (wechat_user_id,)).fetchone()
|
||||
return row["id"] if row else 0
|
||||
|
||||
|
||||
def set_designer_shop(designer_id: int, shop_id: str, group_id: str):
|
||||
"""设置设计师在某店铺的分组 ID(同一设计师不同店铺不同 group_id)"""
|
||||
with _get_conn() as conn:
|
||||
if _is_mysql():
|
||||
conn.execute(
|
||||
"REPLACE INTO designer_shops (designer_id, shop_id, group_id) VALUES (%s, %s, %s)",
|
||||
(designer_id, shop_id, group_id),
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO designer_shops (designer_id, shop_id, group_id) VALUES (?, ?, ?)",
|
||||
(designer_id, shop_id, group_id),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def update_online(wechat_user_id: str, is_online: bool):
|
||||
"""更新设计师在线状态(企微群「上线」/「下线」解析后调用)"""
|
||||
from datetime import datetime
|
||||
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
with _get_conn() as conn:
|
||||
if _is_mysql():
|
||||
conn.execute(
|
||||
"REPLACE INTO designer_online (wechat_user_id, is_online, updated_at) VALUES (%s, %s, %s)",
|
||||
(wechat_user_id, 1 if is_online else 0, ts),
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO designer_online (wechat_user_id, is_online, updated_at) VALUES (?, ?, ?)",
|
||||
(wechat_user_id, 1 if is_online else 0, ts),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
# ========== 派单 ==========
|
||||
|
||||
def get_transfer_group_for_shop(shop_id: str) -> Optional[str]:
|
||||
"""
|
||||
为店铺轮询派单,返回分组 ID。
|
||||
从该店铺的在线设计师中轮询选一个,返回其在该店铺的 group_id。
|
||||
无人在线则返回 None。
|
||||
"""
|
||||
with _get_conn() as conn:
|
||||
rows = conn.execute(_sql("""
|
||||
SELECT d.wechat_user_id, ds.group_id
|
||||
FROM designer_shops ds
|
||||
JOIN designers d ON d.id = ds.designer_id
|
||||
JOIN designer_online o ON o.wechat_user_id = d.wechat_user_id AND o.is_online = 1
|
||||
WHERE ds.shop_id = ?
|
||||
"""), (shop_id,)).fetchall()
|
||||
|
||||
if not rows:
|
||||
return None
|
||||
|
||||
with _get_conn() as conn:
|
||||
rr = conn.execute(_sql("SELECT last_index FROM round_robin WHERE shop_id = ?"), (shop_id,)).fetchone()
|
||||
last = rr["last_index"] if rr else 0
|
||||
idx = last % len(rows)
|
||||
chosen = rows[idx]
|
||||
if _is_mysql():
|
||||
conn.execute(
|
||||
"REPLACE INTO round_robin (shop_id, last_index) VALUES (%s, %s)",
|
||||
(shop_id, idx + 1),
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"INSERT OR REPLACE INTO round_robin (shop_id, last_index) VALUES (?, ?)",
|
||||
(shop_id, idx + 1),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
return chosen["group_id"]
|
||||
|
||||
|
||||
# ========== 查询 ==========
|
||||
|
||||
def get_all_wechat_user_ids() -> list:
|
||||
"""获取所有设计师的 wechat_user_id(用于同步在线状态)"""
|
||||
with _get_conn() as conn:
|
||||
rows = conn.execute("SELECT wechat_user_id FROM designers").fetchall()
|
||||
return [r["wechat_user_id"] for r in rows]
|
||||
|
||||
|
||||
def list_designers():
|
||||
"""列出所有设计师及其店铺分组"""
|
||||
with _get_conn() as conn:
|
||||
designers = conn.execute("SELECT id, name, wechat_user_id FROM designers").fetchall()
|
||||
result = []
|
||||
for d in designers:
|
||||
shops = conn.execute(
|
||||
_sql("SELECT shop_id, group_id FROM designer_shops WHERE designer_id = ?"),
|
||||
(d["id"],),
|
||||
).fetchall()
|
||||
online = conn.execute(
|
||||
_sql("SELECT is_online FROM designer_online WHERE wechat_user_id = ?"),
|
||||
(d["wechat_user_id"],),
|
||||
).fetchone()
|
||||
result.append({
|
||||
"id": d["id"],
|
||||
"name": d["name"],
|
||||
"wechat_user_id": d["wechat_user_id"],
|
||||
"shops": {s["shop_id"]: s["group_id"] for s in shops},
|
||||
"is_online": bool(online and online["is_online"]),
|
||||
})
|
||||
return result
|
||||
BIN
db/image_tasks.db
Normal file
BIN
db/image_tasks.db
Normal file
Binary file not shown.
@@ -1,480 +1,113 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
图片任务数据库管理
|
||||
支持客户后续增加需求细节
|
||||
"""
|
||||
import sqlite3
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
import os
|
||||
from typing import Optional, List, Dict
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_DB_TYPE = os.getenv("DB_TYPE", "sqlite").lower()
|
||||
_MYSQL_HOST = os.getenv("MYSQL_HOST", "127.0.0.1")
|
||||
_MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
|
||||
_MYSQL_USER = os.getenv("MYSQL_USER", "root")
|
||||
_MYSQL_PASSWORD = os.getenv("MYSQL_PASSWORD", "")
|
||||
_MYSQL_DATABASE = os.getenv("MYSQL_DATABASE", "ai_cs")
|
||||
|
||||
def _is_mysql() -> bool:
|
||||
return _DB_TYPE in ("mysql", "mariadb")
|
||||
|
||||
def _sql(query: str) -> str:
|
||||
return query.replace("?", "%s") if _is_mysql() else query
|
||||
|
||||
def _now_str() -> str:
|
||||
if _is_mysql():
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
return datetime.now().isoformat()
|
||||
|
||||
class TaskStatus(Enum):
|
||||
"""任务状态"""
|
||||
PENDING = "pending" # 待付款
|
||||
PAID = "paid" # 已付款,待处理
|
||||
PROCESSING = "processing" # 处理中
|
||||
AWAITING_CONFIRM = "awaiting_confirm" # 已完成,待客户确认
|
||||
COMPLETED = "completed" # 已完成
|
||||
FAILED = "failed" # 失败
|
||||
CANCELLED = "cancelled" # 已取消
|
||||
PENDING = "pending"
|
||||
PROCESSING = "processing"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
class ImageTaskManager:
|
||||
"""图片任务管理器"""
|
||||
|
||||
def __init__(self, db_path: str = None):
|
||||
if db_path is None:
|
||||
db_path = Path(__file__).parent / "image_tasks.db"
|
||||
|
||||
self.db_path = db_path
|
||||
self._init_db()
|
||||
logger.info(f"图片任务管理器初始化完成,数据库:{self.db_path}")
|
||||
|
||||
|
||||
def _init_db(self):
|
||||
"""初始化数据库"""
|
||||
if _is_mysql():
|
||||
conn = self._get_conn()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS image_tasks (
|
||||
task_id VARCHAR(128) PRIMARY KEY,
|
||||
customer_id VARCHAR(128) NOT NULL,
|
||||
customer_name VARCHAR(255),
|
||||
original_image TEXT NOT NULL,
|
||||
operation VARCHAR(64) DEFAULT 'enhance',
|
||||
requirements TEXT,
|
||||
customer_notes TEXT,
|
||||
status VARCHAR(32) DEFAULT 'pending',
|
||||
created_at DATETIME,
|
||||
paid_at DATETIME,
|
||||
started_at DATETIME,
|
||||
completed_at DATETIME,
|
||||
result_image TEXT,
|
||||
error_message TEXT,
|
||||
retry_count INT DEFAULT 0,
|
||||
acc_id VARCHAR(128),
|
||||
acc_type VARCHAR(64) DEFAULT 'AliWorkbench'
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS task_requirement_changes (
|
||||
id INTEGER PRIMARY KEY AUTO_INCREMENT,
|
||||
task_id VARCHAR(128) NOT NULL,
|
||||
change_type VARCHAR(64),
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
changed_at DATETIME,
|
||||
changed_by VARCHAR(32),
|
||||
FOREIGN KEY (task_id) REFERENCES image_tasks(task_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
''')
|
||||
cursor.execute("SHOW INDEX FROM image_tasks")
|
||||
exists = {str(r.get("Key_name", "")) for r in cursor.fetchall()}
|
||||
if "idx_customer" not in exists:
|
||||
cursor.execute('CREATE INDEX idx_customer ON image_tasks(customer_id)')
|
||||
if "idx_status" not in exists:
|
||||
cursor.execute('CREATE INDEX idx_status ON image_tasks(status)')
|
||||
if "idx_created" not in exists:
|
||||
cursor.execute('CREATE INDEX idx_created ON image_tasks(created_at)')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
else:
|
||||
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS image_tasks (
|
||||
task_id TEXT PRIMARY KEY,
|
||||
customer_id TEXT NOT NULL,
|
||||
customer_name TEXT,
|
||||
original_image TEXT NOT NULL,
|
||||
operation TEXT DEFAULT 'enhance',
|
||||
requirements TEXT,
|
||||
customer_notes TEXT,
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_at TEXT,
|
||||
paid_at TEXT,
|
||||
started_at TEXT,
|
||||
completed_at TEXT,
|
||||
result_image TEXT,
|
||||
error_message TEXT,
|
||||
retry_count INTEGER DEFAULT 0,
|
||||
acc_id TEXT,
|
||||
acc_type TEXT DEFAULT 'AliWorkbench'
|
||||
)
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS task_requirement_changes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
task_id TEXT NOT NULL,
|
||||
change_type TEXT,
|
||||
old_value TEXT,
|
||||
new_value TEXT,
|
||||
changed_at TEXT,
|
||||
changed_by TEXT,
|
||||
FOREIGN KEY (task_id) REFERENCES image_tasks(task_id)
|
||||
)
|
||||
''')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_customer ON image_tasks(customer_id)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_status ON image_tasks(status)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_created ON image_tasks(created_at)')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
logger.info("数据库表初始化完成")
|
||||
|
||||
def _get_conn(self):
|
||||
"""获取数据库连接"""
|
||||
if _is_mysql():
|
||||
import pymysql
|
||||
return pymysql.connect(
|
||||
host=_MYSQL_HOST,
|
||||
port=_MYSQL_PORT,
|
||||
user=_MYSQL_USER,
|
||||
password=_MYSQL_PASSWORD,
|
||||
database=_MYSQL_DATABASE,
|
||||
charset="utf8mb4",
|
||||
cursorclass=pymysql.cursors.DictCursor,
|
||||
autocommit=False,
|
||||
self.db_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
# 核心任务表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS image_tasks (
|
||||
task_id TEXT PRIMARY KEY,
|
||||
customer_id TEXT NOT NULL,
|
||||
platform TEXT DEFAULT 'qianniu',
|
||||
original_image TEXT NOT NULL,
|
||||
operation TEXT DEFAULT 'enhance',
|
||||
requirements TEXT,
|
||||
status TEXT DEFAULT 'pending',
|
||||
result_image TEXT,
|
||||
price REAL DEFAULT 0.0,
|
||||
outcome TEXT DEFAULT 'pending',
|
||||
created_at TEXT,
|
||||
updated_at TEXT
|
||||
)
|
||||
''')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_status ON image_tasks(status)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_cust_plat ON image_tasks(customer_id, platform)')
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
def _get_conn(self):
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
def create_task(self, task_id: str, customer_id: str, customer_name: str,
|
||||
original_image: str, operation: str = 'enhance',
|
||||
requirements: dict = None, acc_id: str = '', acc_type: str = 'AliWorkbench') -> bool:
|
||||
"""创建图片任务"""
|
||||
|
||||
def add_task(self, customer_id: str, platform: str, original_image: str, operation: str, requirements: str = "", status: str = "pending") -> str:
|
||||
task_id = str(uuid.uuid4())
|
||||
now = datetime.now().isoformat()
|
||||
try:
|
||||
conn = self._get_conn()
|
||||
cursor = conn.cursor()
|
||||
|
||||
requirements_json = json.dumps(requirements) if requirements else None
|
||||
|
||||
cursor.execute(_sql('''
|
||||
INSERT INTO image_tasks (
|
||||
task_id, customer_id, customer_name, original_image,
|
||||
operation, requirements, customer_notes, status,
|
||||
created_at, acc_id, acc_type
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
'''), (
|
||||
task_id,
|
||||
customer_id,
|
||||
customer_name,
|
||||
original_image,
|
||||
operation,
|
||||
requirements_json,
|
||||
'', # 初始备注为空
|
||||
TaskStatus.PENDING.value,
|
||||
_now_str(),
|
||||
acc_id,
|
||||
acc_type
|
||||
))
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO image_tasks (task_id, customer_id, platform, original_image, operation, requirements, status, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (task_id, customer_id, platform, original_image, operation, requirements, status, now, now))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info(f"图片任务创建成功:{task_id}")
|
||||
return True
|
||||
|
||||
return task_id
|
||||
except Exception as e:
|
||||
logger.error(f"创建图片任务失败:{e}")
|
||||
return False
|
||||
|
||||
def get_task(self, task_id: str) -> Optional[dict]:
|
||||
"""查询任务"""
|
||||
logger.error(f"Failed to add task: {e}")
|
||||
return ""
|
||||
|
||||
def update_status(self, task_id: str, status: str, result_image: str = ""):
|
||||
now = datetime.now().isoformat()
|
||||
try:
|
||||
conn = self._get_conn()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(_sql('SELECT * FROM image_tasks WHERE task_id = ?'), (task_id,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
if row:
|
||||
task = dict(row)
|
||||
# 解析 JSON 字段
|
||||
if task.get('requirements'):
|
||||
task['requirements'] = json.loads(task['requirements'])
|
||||
return task
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"查询任务失败:{e}")
|
||||
return None
|
||||
|
||||
def get_customer_tasks(self, customer_id: str, status: str = None) -> List[dict]:
|
||||
"""查询客户的任务列表"""
|
||||
try:
|
||||
conn = self._get_conn()
|
||||
cursor = conn.cursor()
|
||||
|
||||
if status:
|
||||
cursor.execute(_sql('''
|
||||
SELECT * FROM image_tasks
|
||||
WHERE customer_id = ? AND status = ?
|
||||
ORDER BY created_at DESC
|
||||
'''), (customer_id, status))
|
||||
if result_image:
|
||||
cursor.execute('UPDATE image_tasks SET status = ?, result_image = ?, updated_at = ? WHERE task_id = ?',
|
||||
(status, result_image, now, task_id))
|
||||
else:
|
||||
cursor.execute(_sql('''
|
||||
SELECT * FROM image_tasks
|
||||
WHERE customer_id = ?
|
||||
ORDER BY created_at DESC
|
||||
'''), (customer_id,))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
tasks = []
|
||||
for row in rows:
|
||||
task = dict(row)
|
||||
if task.get('requirements'):
|
||||
task['requirements'] = json.loads(task['requirements'])
|
||||
tasks.append(task)
|
||||
|
||||
return tasks
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"查询客户任务失败:{e}")
|
||||
return []
|
||||
|
||||
def update_status(self, task_id: str, status: TaskStatus):
|
||||
"""更新任务状态"""
|
||||
try:
|
||||
conn = self._get_conn()
|
||||
cursor = conn.cursor()
|
||||
|
||||
placeholder = "%s" if _is_mysql() else "?"
|
||||
updates = [f'status = {placeholder}']
|
||||
params = [status.value]
|
||||
|
||||
# 根据状态设置时间
|
||||
if status == TaskStatus.PAID:
|
||||
updates.append(f'paid_at = {placeholder}')
|
||||
params.append(_now_str())
|
||||
elif status == TaskStatus.PROCESSING:
|
||||
updates.append(f'started_at = {placeholder}')
|
||||
params.append(_now_str())
|
||||
elif status in [TaskStatus.COMPLETED, TaskStatus.FAILED]:
|
||||
updates.append(f'completed_at = {placeholder}')
|
||||
params.append(_now_str())
|
||||
|
||||
params.append(task_id)
|
||||
|
||||
cursor.execute(_sql(f'''
|
||||
UPDATE image_tasks
|
||||
SET {', '.join(updates)}
|
||||
WHERE task_id = ?
|
||||
'''), params)
|
||||
|
||||
cursor.execute('UPDATE image_tasks SET status = ?, updated_at = ? WHERE task_id = ?',
|
||||
(status, now, task_id))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info(f"任务状态更新:{task_id} -> {status.value}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"更新任务状态失败:{e}")
|
||||
|
||||
def update_result(self, task_id: str, result_image: str, error_message: str = None):
|
||||
"""更新处理结果"""
|
||||
logger.error(f"Failed to update task status: {e}")
|
||||
|
||||
def update_outcome(self, customer_id: str, platform: str, outcome: str):
|
||||
"""记录任务的最终结局(用于训练样本分类)"""
|
||||
now = datetime.now().isoformat()
|
||||
try:
|
||||
conn = self._get_conn()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(_sql('''
|
||||
cursor.execute('''
|
||||
UPDATE image_tasks
|
||||
SET result_image = ?, error_message = ?
|
||||
WHERE task_id = ?
|
||||
'''), (result_image, error_message, task_id))
|
||||
|
||||
SET outcome = ?, updated_at = ?
|
||||
WHERE task_id = (
|
||||
SELECT task_id FROM image_tasks
|
||||
WHERE customer_id = ? AND platform = ?
|
||||
ORDER BY created_at DESC LIMIT 1
|
||||
)
|
||||
''', (outcome, now, customer_id, platform))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info(f"任务结果更新:{task_id}")
|
||||
|
||||
logger.info(f"[DB] 客户 {customer_id} 任务结局更新为: {outcome}")
|
||||
except Exception as e:
|
||||
logger.error(f"更新任务结果失败:{e}")
|
||||
|
||||
def add_customer_note(self, task_id: str, note: str, changed_by: str = 'customer') -> bool:
|
||||
"""
|
||||
客户添加需求备注(支持后续增加细节)
|
||||
|
||||
Args:
|
||||
task_id: 任务 ID
|
||||
note: 备注内容
|
||||
changed_by: 修改者(customer/staff)
|
||||
|
||||
Returns:
|
||||
bool: 是否成功
|
||||
"""
|
||||
try:
|
||||
conn = self._get_conn()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 获取旧备注
|
||||
cursor.execute(_sql('SELECT customer_notes FROM image_tasks WHERE task_id = ?'), (task_id,))
|
||||
row = cursor.fetchone()
|
||||
old_note = row['customer_notes'] if row else ''
|
||||
|
||||
# 更新备注
|
||||
new_note = f"{old_note}\n[{datetime.now().strftime('%m-%d %H:%M')}] {note}" if old_note else f"[{datetime.now().strftime('%m-%d %H:%M')}] {note}"
|
||||
|
||||
cursor.execute(_sql('''
|
||||
UPDATE image_tasks
|
||||
SET customer_notes = ?
|
||||
WHERE task_id = ?
|
||||
'''), (new_note, task_id))
|
||||
|
||||
# 记录变更历史
|
||||
cursor.execute(_sql('''
|
||||
INSERT INTO task_requirement_changes (
|
||||
task_id, change_type, old_value, new_value, changed_at, changed_by
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
'''), (
|
||||
task_id,
|
||||
'add_note',
|
||||
old_note or '无',
|
||||
note,
|
||||
_now_str(),
|
||||
changed_by
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info(f"客户添加备注成功:{task_id}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"添加客户备注失败:{e}")
|
||||
return False
|
||||
|
||||
def modify_operation(self, task_id: str, new_operation: str, changed_by: str = 'customer') -> bool:
|
||||
"""
|
||||
修改操作类型(客户后续修改需求)
|
||||
|
||||
Args:
|
||||
task_id: 任务 ID
|
||||
new_operation: 新操作类型
|
||||
changed_by: 修改者
|
||||
|
||||
Returns:
|
||||
bool: 是否成功
|
||||
"""
|
||||
try:
|
||||
conn = self._get_conn()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 获取旧操作
|
||||
cursor.execute(_sql('SELECT operation FROM image_tasks WHERE task_id = ?'), (task_id,))
|
||||
row = cursor.fetchone()
|
||||
old_operation = row['operation'] if row else ''
|
||||
|
||||
# 更新操作
|
||||
cursor.execute(_sql('''
|
||||
UPDATE image_tasks
|
||||
SET operation = ?
|
||||
WHERE task_id = ?
|
||||
'''), (new_operation, task_id))
|
||||
|
||||
# 记录变更历史
|
||||
cursor.execute(_sql('''
|
||||
INSERT INTO task_requirement_changes (
|
||||
task_id, change_type, old_value, new_value, changed_at, changed_by
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
'''), (
|
||||
task_id,
|
||||
'modify_operation',
|
||||
old_operation,
|
||||
new_operation,
|
||||
_now_str(),
|
||||
changed_by
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
logger.info(f"修改操作类型成功:{task_id} -> {new_operation}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"修改操作类型失败:{e}")
|
||||
return False
|
||||
|
||||
def get_requirement_history(self, task_id: str) -> List[dict]:
|
||||
"""获取需求变更历史"""
|
||||
try:
|
||||
conn = self._get_conn()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(_sql('''
|
||||
SELECT * FROM task_requirement_changes
|
||||
WHERE task_id = ?
|
||||
ORDER BY changed_at DESC
|
||||
'''), (task_id,))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"查询需求历史失败:{e}")
|
||||
return []
|
||||
|
||||
def get_pending_tasks(self) -> List[dict]:
|
||||
"""获取所有待处理任务"""
|
||||
return self.get_customer_tasks('', 'pending')
|
||||
|
||||
def increment_retry(self, task_id: str) -> int:
|
||||
"""增加重试次数"""
|
||||
try:
|
||||
conn = self._get_conn()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(_sql('''
|
||||
UPDATE image_tasks
|
||||
SET retry_count = retry_count + 1
|
||||
WHERE task_id = ?
|
||||
'''), (task_id,))
|
||||
|
||||
cursor.execute(_sql('SELECT retry_count FROM image_tasks WHERE task_id = ?'), (task_id,))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
|
||||
return row['retry_count'] if row else 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"增加重试次数失败:{e}")
|
||||
return 999
|
||||
logger.error(f"Failed to update outcome: {e}")
|
||||
|
||||
# 单例
|
||||
_task_manager: Optional[ImageTaskManager] = None
|
||||
|
||||
def get_image_task_manager() -> ImageTaskManager:
|
||||
"""获取图片任务管理器单例"""
|
||||
global _task_manager
|
||||
if _task_manager is None:
|
||||
_task_manager = ImageTaskManager()
|
||||
return _task_manager
|
||||
db = ImageTaskManager()
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user