From 2624f9189bca24cb6d9f14bf75424788db2b0356 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Wed, 9 Sep 2026 14:43:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(dayend):=20=E5=88=87=E6=97=A5=E7=AB=AF?= =?UTF-8?q?=E7=82=B9=E6=94=B9=20catch-up=20=E8=AF=AD=E4=B9=89=E2=80=94?= =?UTF-8?q?=E2=80=94=E5=BE=AA=E7=8E=AF=E4=B8=89=E6=AD=A5(=E6=97=A5?= =?UTF-8?q?=E7=BB=93=E4=BD=99=E9=A2=9D=E5=BF=AB=E7=85=A7=E2=86=92=E6=80=BB?= =?UTF-8?q?=E8=B4=A6=E2=86=92=E6=97=A5=E6=9C=9F+1)=E7=9B=B4=E8=87=B3?= =?UTF-8?q?=E8=BF=BD=E5=B9=B3=E4=BB=8A=E5=A4=A9,=E5=81=9C=E6=9C=BA?= =?UTF-8?q?=E5=A4=9A=E6=97=A5/cron=E6=BC=8F=E8=B7=91=E4=B8=80=E6=AC=A1?= =?UTF-8?q?=E8=B0=83=E7=94=A8=E8=A1=A5=E9=BD=90=E4=B8=8D=E6=BC=8F=E6=97=A5?= =?UTF-8?q?;=E5=90=8C=E6=97=A5=E9=87=8D=E6=94=BE0=E6=AD=A5=E8=B7=B3?= =?UTF-8?q?=E8=BF=87;business=5Fdate=E8=B6=85=E5=89=8D=E4=BB=8A=E5=A4=A9?= =?UTF-8?q?=E6=8B=92=E7=BB=9D;=5FMAX=5FCATCHUP=3D400=E9=98=B2=E5=BE=A1?= =?UTF-8?q?=E5=BC=82=E5=B8=B8=E9=85=8D=E7=BD=AE(=E8=BE=BE=E4=B8=8A?= =?UTF-8?q?=E9=99=90=E6=97=B6=E5=B7=B2=E6=8E=A8=E8=BF=9B=E5=A4=A9=E6=95=B0?= =?UTF-8?q?=E5=AE=8C=E6=95=B4=E7=94=9F=E6=95=88,=E6=8A=A5=E9=94=99?= =?UTF-8?q?=E8=A6=81=E6=B1=82=E4=BA=BA=E5=B7=A5=E6=A0=B8=E6=9F=A5=E7=BB=AD?= =?UTF-8?q?=E8=B7=91)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- wwwroot/api/dayend.dspy | 57 +++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/wwwroot/api/dayend.dspy b/wwwroot/api/dayend.dspy index 550719d..e314ce9 100644 --- a/wwwroot/api/dayend.dspy +++ b/wwwroot/api/dayend.dspy @@ -11,11 +11,14 @@ if _ip not in ('127.0.0.1', '::1', 'localhost'): from sqlor.dbpools import get_sor_context -# 切日三步(同一 sor 上下文,全成才提交): -# 1. dayend_balance —— 上一营业日余额快照(acc_balance 补齐无当日行的账户) -# 2. accounting_ledger —— 上一营业日总账(delete+insert 幂等重建 ledger 当日行) -# 3. new_business_date —— params.business_date 推进 +1 天 -# 顺序不可颠倒:前两步都读「上一营业日」,推进日期必须在最后。 +# 切日(catch-up 语义,同一 sor 上下文,全成才提交): +# 循环执行三步直至 business_date 追平今天: +# 1. dayend_balance —— 上一营业日余额快照(acc_balance 补齐无当日行的账户) +# 2. accounting_ledger —— 上一营业日总账(delete+insert 幂等重建 ledger 当日行) +# 3. new_business_date —— params.business_date 推进 +1 天 +# 停机多日/cron 漏跑后一次调用自动追平,不会漏日;同日重放 0 步跳过(幂等)。 +# _MAX_CATCHUP 防御异常配置(如 business_date 误写远古值)导致超长循环。 +_MAX_CATCHUP = 400 async with get_sor_context(request._run_ns, 'accounting') as sor: try: from datetime import date @@ -23,25 +26,47 @@ async with get_sor_context(request._run_ns, 'accounting') as sor: from accounting.ledger import accounting_ledger from appbase.businessdate import get_business_date, new_business_date - old_bd = await get_business_date(sor) - # 幂等守卫:business_date 已到今天则跳过(cron 重跑/手工重放不会多跳日期) today = date.today().isoformat() - if old_bd >= today: + bd = await get_business_date(sor) + start_bd = bd + if bd > today: + return json.dumps({ + 'success': False, + 'business_date': bd, + 'message': 'business_date 超前于今天(配置异常),拒绝切日', + }, ensure_ascii=False) + steps = 0 + snapshots = [] + while bd < today and steps < _MAX_CATCHUP: + snap_date = await dayend_balance(sor) + await accounting_ledger(sor) + await new_business_date(sor) + bd = await get_business_date(sor) + snapshots.append(snap_date) + steps += 1 + if steps == 0: return json.dumps({ 'success': True, 'skipped': True, - 'business_date': old_bd, + 'business_date': bd, 'message': '已是当日营业日期,跳过切日(幂等)', }, ensure_ascii=False) - - snap_date = await dayend_balance(sor) - await accounting_ledger(sor) - await new_business_date(sor) - new_bd = await get_business_date(sor) + if bd < today: + # 触达防御上限仍未追平:已推进的天数是完整日结单元、照常生效(可续跑), + # 但 business_date 疑似配置异常(如误写远古值),必须人工核查后再续。 + return json.dumps({ + 'success': False, + 'business_date': '%s -> %s' % (start_bd, bd), + 'steps': steps, + 'message': 'catch-up 达 %d 天上限仍未追平今天,business_date 疑似配置异常,' + '请人工核查后重跑(已推进的 %d 天日结完整生效,重跑自动续推)' + % (_MAX_CATCHUP, steps), + }, ensure_ascii=False) await sor.sqlExe("COMMIT", {}) return json.dumps({ 'success': True, - 'business_date': '%s -> %s' % (old_bd, new_bd), - 'snapshot_date': snap_date, + 'business_date': '%s -> %s' % (start_bd, bd), + 'steps': steps, + 'snapshots': snapshots[-5:], }, ensure_ascii=False) except Exception as e: return json.dumps({'success': False, 'message': 'dayend failed: %s' % str(e)[:200]},