# 日终切日任务端点:日结余额快照 + 日结总账 + 营业日期推进 # GET /accounting/api/dayend.dspy # # 安全:本端点是定时任务入口(写库),只允许本机调用——与 fetch_forex_rates.dspy 同模式。 # 判定依据 client_ip:nginx 用 $proxy_add_x_forwarded_for 追加模式、中间件取链尾值, # 外部伪造 X-Forwarded-For 会被追加真实 IP 到链尾,伪造不成立;且应用端口不对外开放。 _ip = request.get('client_ip') or '' if _ip not in ('127.0.0.1', '::1', 'localhost'): return json.dumps({'success': False, 'message': '仅允许本机调用(定时任务入口)'}, ensure_ascii=False) from sqlor.dbpools import get_sor_context # 切日(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 from accounting.dayend_balance import dayend_balance from accounting.ledger import accounting_ledger from appbase.businessdate import get_business_date, new_business_date today = date.today().isoformat() 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': bd, 'message': '已是当日营业日期,跳过切日(幂等)', }, ensure_ascii=False) 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' % (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]}, ensure_ascii=False)