"""客户风控数据库(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()