"""只读统计主从关系、状态和负数行,不输出业务明细。""" import argparse import json import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "backend")) from sqlalchemy import Engine, create_engine from app.core.config import Settings, get_settings from app.core.tplus_db import resolve_tplus_settings QUERIES = { "personnel_dates": "SELECT COUNT_BIG(*) AS total, SUM(CASE WHEN updated IS NULL THEN 1 ELSE 0 END) AS null_updated FROM AA_Person", "product_dates": "SELECT COUNT_BIG(*) AS total, SUM(CASE WHEN updated IS NULL THEN 1 ELSE 0 END) AS null_updated FROM AA_Inventory", "order_detail_parent": "SELECT COUNT_BIG(*) AS total, SUM(CASE WHEN h.ID IS NULL THEN 1 ELSE 0 END) AS orphan_rows FROM SA_SaleOrder_b b LEFT JOIN SA_SaleOrder h ON h.ID=b.idSaleOrderDTO", "delivery_detail_parent": "SELECT COUNT_BIG(*) AS total, SUM(CASE WHEN h.ID IS NULL THEN 1 ELSE 0 END) AS orphan_rows FROM SA_SaleDelivery_b b LEFT JOIN SA_SaleDelivery h ON h.ID=b.idSaleDeliveryDTO", "receipt_detail_parent": "SELECT COUNT_BIG(*) AS total, SUM(CASE WHEN h.ID IS NULL THEN 1 ELSE 0 END) AS orphan_rows FROM ARAP_ReceivePayment_b b LEFT JOIN ARAP_ReceivePayment h ON h.ID=b.idArapReceivePaymentDTO", "delivery_order_link": "SELECT COUNT_BIG(*) AS total, SUM(CASE WHEN b.saleOrderId IS NULL OR b.saleOrderId=0 THEN 1 ELSE 0 END) AS missing_order_id, SUM(CASE WHEN b.saleOrderId>0 AND h.ID IS NULL THEN 1 ELSE 0 END) AS invalid_order_id FROM SA_SaleDelivery_b b LEFT JOIN SA_SaleOrder h ON h.ID=b.saleOrderId", "receipt_order_link": "SELECT COUNT_BIG(*) AS total, SUM(CASE WHEN p.SaleOrderID IS NULL OR p.SaleOrderID=0 THEN 1 ELSE 0 END) AS missing_order_id, SUM(CASE WHEN p.SaleOrderID>0 AND h.ID IS NULL THEN 1 ELSE 0 END) AS invalid_order_id FROM ARAP_ReceivePayment p LEFT JOIN SA_SaleOrder h ON h.ID=p.SaleOrderID", "receipt_direction": "SELECT isReceiveFlag, COUNT_BIG(*) AS total FROM ARAP_ReceivePayment GROUP BY isReceiveFlag", "receipt_status": "SELECT voucherstate, cancelState, COUNT_BIG(*) AS total FROM ARAP_ReceivePayment GROUP BY voucherstate,cancelState", "negative_deliveries": "SELECT SUM(CASE WHEN quantity<0 THEN 1 ELSE 0 END) AS negative_quantity, SUM(CASE WHEN costAmount<0 THEN 1 ELSE 0 END) AS negative_cost FROM SA_SaleDelivery_b", } def build_source_engine(resolved: Settings) -> Engine: """复用应用的 URL 转义和 TLS 配置自建引擎,不修改全局设置。""" return create_engine( resolved.t_plus_database_url, connect_args={"timeout": resolved.t_plus_connect_timeout}, hide_parameters=True, ) def main() -> int: """执行固定白名单统计,并保存无凭据、无明细的 JSON。""" parser = argparse.ArgumentParser(description="T+ 只读数据质量统计") parser.add_argument("--connection-file", required=True, type=Path) parser.add_argument("--report", required=True, type=Path) args = parser.parse_args() resolved = resolve_tplus_settings( get_settings().model_copy( update={"t_plus_enabled": True, "t_plus_connection_file": str(args.connection_file)} ) ) engine = build_source_engine(resolved) result = {} try: connection = engine.raw_connection() try: connection.driver_connection.timeout = resolved.t_plus_query_timeout cursor = connection.cursor() for name, sql in QUERIES.items(): cursor.execute(sql) names = [column[0] for column in cursor.description] result[name] = [dict(zip(names, row, strict=True)) for row in cursor.fetchall()] print(name, json.dumps(result[name]), flush=True) cursor.close() finally: connection.close() finally: engine.dispose() args.report.parent.mkdir(parents=True, exist_ok=True) args.report.write_text(json.dumps(result, indent=2), encoding="utf-8") return 0 if __name__ == "__main__": raise SystemExit(main())