# -*- 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