dalongxia/remote_sync/company_db_tool.py
2026-03-04 13:25:48 +08:00

379 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
import argparse
import datetime as dt
import json
import os
import pathlib
import sys
from typing import Dict, List, Tuple
import pytds
BUSINESS_TABLES = [
"jst_goods_profit_kpi_items_afs",
"jst_sku",
"fys_shopInfo",
"jst_order_express_details",
"fys_SkuDataLibrary_SpuConfig",
"op_operManage",
"op_TargetManagement",
"fys_Ticketing_install",
"fys_Ticketing_install_item",
"fys_jstFreightCost",
"fys_jstFreightCostDetail",
"pc_kfChatLog",
]
ENV_FILE = "/root/.openclaw/credentials/company-sql.env"
def load_env_file(path: str) -> None:
p = pathlib.Path(path)
if not p.exists():
return
for raw in p.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, val = line.split("=", 1)
key = key.strip()
val = val.strip().strip('"').strip("'")
if key and key not in os.environ:
os.environ[key] = val
def conn_params() -> Dict[str, object]:
load_env_file(ENV_FILE)
return {
"server": os.environ.get("COMPANY_DB_HOST", "218.93.191.16"),
"port": int(os.environ.get("COMPANY_DB_PORT", "1433")),
"database": os.environ.get("COMPANY_DB_NAME", "JSDataMiddlePlatform"),
"user": os.environ.get("COMPANY_DB_USER", "nanting"),
"password": os.environ.get("COMPANY_DB_PASSWORD", ""),
"login_timeout": 15,
"timeout": 300,
}
def get_conn():
params = conn_params()
if not params["password"]:
raise RuntimeError("COMPANY_DB_PASSWORD is empty")
return pytds.connect(**params)
def _fmt(value):
if isinstance(value, dt.datetime):
return value.isoformat(sep=" ")
if isinstance(value, (dt.date, dt.time)):
return value.isoformat()
return value
def print_rows(columns: List[str], rows: List[Tuple[object, ...]], truncated: bool = False) -> None:
print("COLUMNS:", ", ".join(columns))
if not rows:
print("ROWS: 0")
return
if truncated:
print(f"ROWS_SHOWN: {len(rows)} (truncated)")
else:
print(f"ROWS: {len(rows)}")
for row in rows:
obj = {columns[i]: _fmt(row[i]) for i in range(len(columns))}
print(json.dumps(obj, ensure_ascii=False, default=str))
def split_go_batches(sql_text: str) -> List[str]:
batches: List[str] = []
current: List[str] = []
for line in sql_text.splitlines():
if line.strip().upper() == "GO":
block = "\n".join(current).strip()
if block:
batches.append(block)
current = []
else:
current.append(line)
block = "\n".join(current).strip()
if block:
batches.append(block)
return batches
def fetch_with_limit(cursor, row_limit: int) -> Tuple[List[Tuple[object, ...]], bool]:
if row_limit <= 0:
return cursor.fetchall(), False
rows: List[Tuple[object, ...]] = []
truncated = False
while len(rows) <= row_limit:
batch = cursor.fetchmany(256)
if not batch:
break
rows.extend(batch)
if len(rows) > row_limit:
rows = rows[:row_limit]
truncated = True
break
return rows, truncated
def run_sql(sql_text: str, row_limit: int) -> int:
batches = split_go_batches(sql_text)
with get_conn() as conn:
cur = conn.cursor()
set_index = 0
for batch in batches:
cur.execute(batch)
while True:
if cur.description:
set_index += 1
cols = [c[0] for c in cur.description]
rows, truncated = fetch_with_limit(cur, row_limit)
print(f"RESULT_SET {set_index}")
print_rows(cols, rows, truncated=truncated)
if not cur.nextset():
break
cur.close()
return set_index
def cmd_health(_args: argparse.Namespace) -> int:
with get_conn() as conn:
cur = conn.cursor()
cur.execute("SELECT @@VERSION AS version, DB_NAME() AS db_name, GETDATE() AS server_time")
cols = [c[0] for c in cur.description]
row = cur.fetchone()
data = {cols[i]: _fmt(row[i]) for i in range(len(cols))}
print(json.dumps(data, ensure_ascii=False, default=str, indent=2))
cur.close()
return 0
def cmd_tables(_args: argparse.Namespace) -> int:
names = ", ".join([f"'{t}'" for t in BUSINESS_TABLES])
sql = f"""
SELECT c.TABLE_NAME, COUNT(*) AS column_count
FROM INFORMATION_SCHEMA.COLUMNS c
WHERE c.TABLE_SCHEMA='dbo'
AND c.TABLE_NAME IN ({names})
GROUP BY c.TABLE_NAME
ORDER BY c.TABLE_NAME
"""
with get_conn() as conn:
cur = conn.cursor()
cur.execute(sql)
rows = cur.fetchall()
cols = [c[0] for c in cur.description]
print_rows(cols, rows)
cur.close()
return 0
def cmd_columns(args: argparse.Namespace) -> int:
table = args.table
sql = f"""
SELECT
COLUMN_NAME,
DATA_TYPE,
CHARACTER_MAXIMUM_LENGTH,
NUMERIC_PRECISION,
NUMERIC_SCALE,
IS_NULLABLE,
ORDINAL_POSITION
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA='dbo' AND TABLE_NAME='{table}'
ORDER BY ORDINAL_POSITION
"""
with get_conn() as conn:
cur = conn.cursor()
cur.execute(sql)
rows = cur.fetchall()
cols = [c[0] for c in cur.description]
print_rows(cols, rows)
cur.close()
return 0
def _collect_schema() -> Dict[str, List[Tuple[object, ...]]]:
names = ", ".join([f"'{t}'" for t in BUSINESS_TABLES])
sql = f"""
SELECT
TABLE_NAME,
COLUMN_NAME,
DATA_TYPE,
CHARACTER_MAXIMUM_LENGTH,
NUMERIC_PRECISION,
NUMERIC_SCALE,
IS_NULLABLE,
ORDINAL_POSITION
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA='dbo'
AND TABLE_NAME IN ({names})
ORDER BY TABLE_NAME, ORDINAL_POSITION
"""
with get_conn() as conn:
cur = conn.cursor()
cur.execute(sql)
rows = cur.fetchall()
cur.close()
by_table: Dict[str, List[Tuple[object, ...]]] = {t: [] for t in BUSINESS_TABLES}
for row in rows:
by_table[str(row[0])].append(row)
return by_table
def cmd_catalog(args: argparse.Namespace) -> int:
by_table = _collect_schema()
now = dt.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
out = pathlib.Path(args.out)
out.parent.mkdir(parents=True, exist_ok=True)
lines: List[str] = []
lines.append("# 公司销售/售后数据库字段与维度目录")
lines.append("")
lines.append(f"> 自动生成时间: {now}")
lines.append("> 数据源: SQLServer / JSDataMiddlePlatform / dbo")
lines.append("> 适用范围: 公司销售数据、售后数据、店铺运营、物流运费、安装工单、客服问题")
lines.append("")
lines.append("## 一、强制路由规则(给AI)")
lines.append("")
lines.append("- 凡是涉及 `销售/业绩/售后/退款/费比/店铺业绩` 的需求,必须走数据库查询,禁止走旧业绩网页。")
lines.append("- 先查字段目录再写 SQL:`/root/.openclaw/workspace/outputs/company-sales-db-catalog.md`")
lines.append("- 优先复用周报模板:`/root/.openclaw/workspace/sqlserver/last_week_afs_report.sql`")
lines.append("- 输出结论必须标注:`时间范围 + 使用表 + 关键字段 + 过滤条件`。")
lines.append("")
lines.append("## 二、可查询业务维度(语义层)")
lines.append("")
dimension_rows = [
("订单维度", "订单号、子订单号、内部订单ID、订单状态、售后状态", "jst_order_express_details, jst_goods_profit_kpi_items_afs"),
("商品维度", "SKU、SPU、货号、商品名称、规格、类目、品牌", "jst_sku, op_TargetManagement, op_operManage, fys_SkuDataLibrary_SpuConfig"),
("店铺维度", "店铺名称、平台、店铺编码、负责人、渠道类型", "fys_shopInfo, jst_order_express_details, jst_goods_profit_kpi_items_afs"),
("销售金额维度", "商品金额、付款金额、实发金额、成本、毛利相关费用", "jst_goods_profit_kpi_items_afs, jst_sku"),
("售后维度", "售后类型、售后原因、退款/退货金额、售后标签", "jst_goods_profit_kpi_items_afs, pc_kfChatLog"),
("物流维度", "快递公司、快递单号、物流状态、运费、重量、收货地区", "jst_order_express_details, fys_jstFreightCost, fys_jstFreightCostDetail"),
("安装工单维度", "工单状态、安装费用、结算金额、服务商", "fys_Ticketing_install, fys_Ticketing_install_item"),
("人员维度", "运营、设计师、采购、美工、店长、客服", "op_TargetManagement, op_operManage, fys_shopInfo, fys_SkuDataLibrary_SpuConfig"),
("时间维度", "创建时间、支付时间、发货时间、签收时间、订单完成时间", "全表通用时间字段(createdAt/createTime/pay_time/send_time 等)"),
("客服问题维度", "聊天主次原因、问题内容、订单关联", "pc_kfChatLog"),
]
lines.append("| 维度 | 可查询内容 | 主要来源表 |")
lines.append("|---|---|---|")
for row in dimension_rows:
lines.append(f"| {row[0]} | {row[1]} | {row[2]} |")
lines.append("")
lines.append("## 三、核心关联键(Join Key)")
lines.append("")
lines.append("- `order_id`:`so_id / undefined2 / orderNo / OrderNum / Sourceno`")
lines.append("- `inner_order_id`:`inner_order_id / undefined1 / o_id`")
lines.append("- `sku_id`:`sku_id / undefined10 / barCode`")
lines.append("- `spu`:`spu / other_1 / undefined96`")
lines.append("- `shop_name`:`shop_name / shopName / undefined8 / shopname`")
lines.append("")
lines.append("## 四、全表字段清单(真实表头)")
lines.append("")
total_cols = 0
for table in BUSINESS_TABLES:
rows = by_table.get(table, [])
total_cols += len(rows)
lines.append(f"### {table}({len(rows)} 列)")
lines.append("")
lines.append("| 序号 | 字段名 | 类型 | 长度/精度 | 可空 |")
lines.append("|---:|---|---|---|---|")
for r in rows:
_, col, dtype, clen, prec, scale, nullable, ord_pos = r
if clen is not None:
length = str(clen)
elif prec is not None:
if scale is not None:
length = f"{prec},{scale}"
else:
length = str(prec)
else:
length = "-"
lines.append(f"| {ord_pos} | {col} | {dtype} | {length} | {nullable} |")
lines.append("")
lines.append("## 五、统计")
lines.append("")
lines.append(f"- 业务表数量:{len(BUSINESS_TABLES)}")
lines.append(f"- 字段总数:{total_cols}")
lines.append("- 建议:高频分析优先在订单售后主表 `jst_goods_profit_kpi_items_afs` 先聚合,再按需关联其他表,避免超时。")
out.write_text("\n".join(lines), encoding="utf-8")
print(str(out))
return 0
def cmd_query(args: argparse.Namespace) -> int:
sql_text = None
if args.file:
sql_text = pathlib.Path(args.file).read_text(encoding="utf-8")
elif args.sql:
sql_text = args.sql
elif args.sql_positional:
sql_text = " ".join(args.sql_positional).strip()
if not sql_text:
raise SystemExit("query requires --sql / --file / or inline SQL text")
set_count = run_sql(sql_text, args.limit)
print(f"TOTAL_RESULT_SETS: {set_count}")
return 0
def build_parser() -> argparse.ArgumentParser:
p = argparse.ArgumentParser(prog="company-db", description="Company SQLServer helper")
sub = p.add_subparsers(dest="cmd", required=True)
sub.add_parser("health")
sub.add_parser("tables")
p_cols = sub.add_parser("columns")
p_cols.add_argument("table")
p_query = sub.add_parser("query")
p_query.add_argument("--sql", help="SQL text")
p_query.add_argument("--file", help="SQL file path")
p_query.add_argument("--limit", type=int, default=200, help="row limit via SET ROWCOUNT")
p_query.add_argument("sql_positional", nargs="*", help="inline SQL text, e.g. company-db query SELECT 1")
p_run = sub.add_parser("run-file")
p_run.add_argument("file", help="SQL file path")
p_run.add_argument("--limit", type=int, default=0, help="max rows shown per result set (0 means no limit)")
p_cat = sub.add_parser("catalog")
p_cat.add_argument("--out", default="/root/.openclaw/workspace/outputs/company-sales-db-catalog.md")
return p
def main() -> int:
parser = build_parser()
args = parser.parse_args()
if args.cmd == "health":
return cmd_health(args)
if args.cmd == "tables":
return cmd_tables(args)
if args.cmd == "columns":
return cmd_columns(args)
if args.cmd == "query":
return cmd_query(args)
if args.cmd == "run-file":
args.sql = None
args.file = args.file
return cmd_query(args)
if args.cmd == "catalog":
return cmd_catalog(args)
parser.print_help()
return 1
if __name__ == "__main__":
sys.exit(main())