fix(businessdate): 修 dayend_balance/ledger 错误 import accounting.businessdate(实际在 appbase)——导入即 ModuleNotFoundError,切日/日结链路从未可用;dayend_balance 支持注入 sor 复用调用方事务上下文+返回快照日;新增 wwwroot/api/dayend.dspy 切日端点(localhost 门禁+当日幂等守卫,三步:日结余额快照→总账→business_date+1)供生产 cron 调用(2026-09-09 生产增量部署前置)

This commit is contained in:
yumoqing 2026-09-09 13:45:08 +08:00
parent 8cc85098a9
commit c40f7f665a
4 changed files with 76 additions and 8 deletions

View File

@ -53,6 +53,14 @@ account_config + accounting_config 记账配置 + currency + exchange_rate 初
- `currency_create/update/delete.dspy` — 币种维护
- `exchange_rate_create/update/delete.dspy` — 汇率维护
- `fetch_forex_rates.dspy` — 拉取外部外汇牌价
- `dayend.dspy` — 日终切日localhost 门禁 + 当日幂等守卫)。三步一个事务:
`dayend_balance`(上一营业日余额快照进 acc_balance`accounting_ledger`(总账)
`new_business_date`params.business_date +1 天)。供宿主应用 crontab 每日调用
pipeline-app 生产:`5 0 * * * curl -s http://127.0.0.1:9090/accounting/api/dayend.dspy`)。
⚠️ 依赖 params 表已有 business_date 行,缺失会抛 BusinessDateParamsError——生产播种
见 pipeline-app 迁移 m0017。历史 bug2026-09-09 修复):`dayend_balance.py`/`ledger.py`
曾写 `from accounting.businessdate import ...`(实际模块在 appbase导入即
ModuleNotFoundError日结/总账从未可运行。
**credit_limit/(信用额度管理)**

View File

@ -1,18 +1,30 @@
from datetime import datetime
from sqlor.dbpools import DBPools
from appPublic.uniqueID import getID
from accounting.businessdate import previous_business_date
from appbase.businessdate import previous_business_date
from accounting.const import *
async def dayend_balance():
dat = await previous_business_date()
ts = datetime.now()
sql = """select a.* from (select accountid, max(acc_date) as acc_date, balance from acc_balance where accountid is not null group by accountid) a where acc_date < ${acc_date}$"""
db = DBPools()
async with db.sqlorContext(DBNAME()) as sor:
async def dayend_balance(sor=None):
"""日结:把「上一营业日」之前没有余额行的账户快照成 acc_balance 行。
返回快照的营业日dat可注入 sor切日端点在同一事务上下文里串
new_business_date dayend_balance accounting_ledger 三步
历史 bug 修复2026-09-09 import accounting.businessdate 是错的
businessdate appbase 模块导入即 ModuleNotFoundError本函数
从未被任何入口调用过切日链路整体缺位
"""
async def _f(sor):
dat = await previous_business_date(sor=sor)
sql = """select a.* from (select accountid, max(acc_date) as acc_date, balance from acc_balance where accountid is not null group by accountid) a where acc_date < ${acc_date}$"""
recs = await sor.sqlExe(sql, {'acc_date':dat})
for r in recs:
r['id'] = getID()
r['acc_date'] = dat
await sor.C('acc_balance', r)
return dat
if sor:
return await _f(sor)
db = DBPools()
async with db.sqlorContext(DBNAME()) as sor:
return await _f(sor)

View File

@ -1,7 +1,7 @@
from datetime import datetime
from appPublic.uniqueID import getID
from appPublic.timeUtils import strdate_add
from accounting.businessdate import get_business_date
from appbase.businessdate import get_business_date
async def accounting_ledger(sor):
rd = await get_business_date(sor)

48
wwwroot/api/dayend.dspy Normal file
View File

@ -0,0 +1,48 @@
# 日终切日任务端点:日结余额快照 + 日结总账 + 营业日期推进
# GET /accounting/api/dayend.dspy
#
# 安全:本端点是定时任务入口(写库),只允许本机调用——与 fetch_forex_rates.dspy 同模式。
# 判定依据 client_ipnginx 用 $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
# 切日三步(同一 sor 上下文,全成才提交):
# 1. dayend_balance —— 上一营业日余额快照acc_balance 补齐无当日行的账户)
# 2. accounting_ledger —— 上一营业日总账delete+insert 幂等重建 ledger 当日行)
# 3. new_business_date —— params.business_date 推进 +1 天
# 顺序不可颠倒:前两步都读「上一营业日」,推进日期必须在最后。
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
old_bd = await get_business_date(sor)
# 幂等守卫business_date 已到今天则跳过cron 重跑/手工重放不会多跳日期)
today = date.today().isoformat()
if old_bd >= today:
return json.dumps({
'success': True, 'skipped': True,
'business_date': old_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)
await sor.sqlExe("COMMIT", {})
return json.dumps({
'success': True,
'business_date': '%s -> %s' % (old_bd, new_bd),
'snapshot_date': snap_date,
}, ensure_ascii=False)
except Exception as e:
return json.dumps({'success': False, 'message': 'dayend failed: %s' % str(e)[:200]},
ensure_ascii=False)