verify_staging 原先 setattr 改写全局 Settings 单例,绕过 _ConnectionFile 严格校验与 sa 账号拒绝;profile_source 手工 拼接 ODBC 连接串且仅转义密码,database/user 含分号时可注入 连接属性。两者统一改为 model_copy 副本 + resolve_tplus_settings 完整校验 + SQLAlchemy URL 自建引擎,与 check_compatibility 同一路径,顺带消除 pyodbc connect 上下文的提交语义隐患。 真实源库连接的功能验证待本机 T+ 环境执行。 Co-Authored-By: Claude Code <noreply@anthropic.com>
73 lines
4.0 KiB
Python
73 lines
4.0 KiB
Python
"""只读统计主从关系、状态和负数行,不输出业务明细。"""
|
|
|
|
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())
|