143 lines
6.4 KiB
Python
143 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
||
"""到货批次汇总修复:列出/备份/修复 实际到货数量、未到货数、到货日期
|
||
|
||
根因:2026-07-27 前的旧版 backfill_arrival_sync.py 按 order_id 汇总到货批次时未按
|
||
order_type 过滤,且只更新第一个命中的订单表(domestic → overseas → packaging 后 return),
|
||
导致 domestic/overseas 表中与其它类型订单 id 相同的行被写入别类型批次的合计。
|
||
|
||
修复口径(与详情弹窗一致):实际到货数量 = 本订单本类型批次合计,未到货数 = 数量 - 该合计。
|
||
"""
|
||
import json
|
||
import os
|
||
import sys
|
||
from datetime import datetime
|
||
|
||
sys.path.insert(0, '/home/gxd/.openclaw/workspace/dy-dashboard')
|
||
os.chdir('/home/gxd/.openclaw/workspace/dy-dashboard')
|
||
|
||
from services.db import query, get_db
|
||
|
||
TABLE_MAP = {
|
||
'domestic': ('domestic_orders', '实际到货日期', '货品编号'),
|
||
'packaging': ('packaging_orders', '到货日期', '编码'),
|
||
'overseas': ('overseas_orders', '实际到货日期', '产品编码'),
|
||
}
|
||
|
||
|
||
def batch_truth(order_type):
|
||
rows = query("""
|
||
SELECT order_id, SUM(arrival_qty) AS total, MAX(arrival_date) AS latest_date, COUNT(*) AS cnt
|
||
FROM arrival_batches WHERE order_type=%s GROUP BY order_id
|
||
""", (order_type,))
|
||
return {r['order_id']: r for r in rows}
|
||
|
||
|
||
def all_type_totals():
|
||
rows = query("""
|
||
SELECT order_id, SUM(arrival_qty) AS total FROM arrival_batches GROUP BY order_id
|
||
""")
|
||
return {r['order_id']: int(r['total'] or 0) for r in rows}
|
||
|
||
|
||
def single_batch_qtys():
|
||
"""order_id -> 该 id 下各类型批次单条数量集合(用于识别旧脚本写入的单批次值)"""
|
||
rows = query("SELECT order_id, arrival_qty FROM arrival_batches")
|
||
out = {}
|
||
for r in rows:
|
||
out.setdefault(r['order_id'], set()).add(int(r['arrival_qty'] or 0))
|
||
return out
|
||
|
||
|
||
def batch_dates():
|
||
"""order_id -> 该 id 下全部批次的到货日期集合(用于确认日期也来自别类型批次)"""
|
||
rows = query("SELECT order_id, arrival_date FROM arrival_batches WHERE arrival_date IS NOT NULL")
|
||
out = {}
|
||
for r in rows:
|
||
d = r['arrival_date']
|
||
d = d.strftime('%Y-%m-%d') if hasattr(d, 'strftime') else str(d)[:10]
|
||
out.setdefault(r['order_id'], set()).add(d)
|
||
return out
|
||
|
||
|
||
def build_plan():
|
||
plan = []
|
||
all_totals = all_type_totals()
|
||
single_qtys = single_batch_qtys()
|
||
other_dates = batch_dates()
|
||
for order_type, (table, date_col, code_col) in TABLE_MAP.items():
|
||
truth = batch_truth(order_type)
|
||
orders = query("SELECT id, `数量`, `实际到货数量`, `未到货数`, `" + date_col + "` AS arr_date FROM " + table)
|
||
for o in orders:
|
||
oid = o['id']
|
||
qty = int(o['数量'] or 0)
|
||
stored = int(o['实际到货数量'] or 0)
|
||
stored_unf = o['未到货数']
|
||
stored_unf = None if stored_unf is None else int(stored_unf)
|
||
cur_date = o['arr_date']
|
||
if cur_date and hasattr(cur_date, 'strftime'):
|
||
cur_date = cur_date.strftime('%Y-%m-%d')
|
||
elif cur_date:
|
||
cur_date = str(cur_date)[:10]
|
||
t = truth.get(oid)
|
||
if t:
|
||
new_total = int(t['total'] or 0)
|
||
nd = t['latest_date']
|
||
new_date = nd.strftime('%Y-%m-%d') if hasattr(nd, 'strftime') else (str(nd)[:10] if nd else None)
|
||
new_unf = qty - new_total
|
||
if stored != new_total or stored_unf != new_unf or cur_date != new_date:
|
||
plan.append({'type': order_type, 'reason': 'batch-sync', 'id': oid,
|
||
'before': {'实际到货数量': stored, '未到货数': stored_unf, '到货日期': cur_date},
|
||
'after': {'实际到货数量': new_total, '未到货数': new_unf, '到货日期': new_date}})
|
||
else:
|
||
# 无本类型批次:仅修复可证明来自其它类型批次的污染值
|
||
# 签名:存储值 = 该 order_id 全部批次合计,或 = 其中某一条批次的数量
|
||
if stored > 0 and (all_totals.get(oid) == stored or stored in single_qtys.get(oid, set())):
|
||
# 到货日期只在能证明来自同 id 其它类型批次时才清空
|
||
new_date = None if (cur_date and cur_date in other_dates.get(oid, set())) or not cur_date else cur_date
|
||
plan.append({'type': order_type, 'reason': 'cross-type-contamination', 'id': oid,
|
||
'before': {'实际到货数量': stored, '未到货数': stored_unf, '到货日期': cur_date},
|
||
'after': {'实际到货数量': 0, '未到货数': qty, '到货日期': new_date}})
|
||
return plan
|
||
|
||
|
||
def main():
|
||
apply = '--apply' in sys.argv
|
||
plan = build_plan()
|
||
print('待修复行数:%d' % len(plan))
|
||
for p in plan:
|
||
print(' [%s] %s id=%s %s %s -> %s' % (p['reason'], p['type'], p['id'],
|
||
'', json.dumps(p['before'], ensure_ascii=False), json.dumps(p['after'], ensure_ascii=False)))
|
||
if not plan:
|
||
return
|
||
ts = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||
outdir = 'backups/arrival_totals_repair_%s' % ts
|
||
if apply:
|
||
os.makedirs(outdir, exist_ok=True)
|
||
with open(os.path.join(outdir, 'before.json'), 'w', encoding='utf-8') as f:
|
||
json.dump(plan, f, ensure_ascii=False, indent=2, default=str)
|
||
conn = get_db()
|
||
try:
|
||
with conn.cursor() as cur:
|
||
for p in plan:
|
||
table, date_col, _ = TABLE_MAP[p['type']]
|
||
cur.execute(
|
||
"UPDATE " + table + " SET `实际到货数量`=%s, `未到货数`=%s, `" + date_col + "`=%s WHERE id=%s",
|
||
(p['after']['实际到货数量'], p['after']['未到货数'], p['after']['到货日期'], p['id'])
|
||
)
|
||
conn.commit()
|
||
finally:
|
||
conn.close()
|
||
with open(os.path.join(outdir, 'applied.json'), 'w', encoding='utf-8') as f:
|
||
json.dump(plan, f, ensure_ascii=False, indent=2, default=str)
|
||
print('已应用修复,备份目录:%s' % outdir)
|
||
try:
|
||
from services.cache import refresh_all_caches
|
||
refresh_all_caches()
|
||
print('缓存已刷新')
|
||
except Exception as e:
|
||
print('缓存刷新失败:%s' % e)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|