feat(账务): reseller财务角色三能力(2026-09-08用户定夺)——①修RECHARGE_REVESE拼写bug(白名单漏R致冲正/支付宝退款长期被拒,兼容旧拼写归一)②代客充值加归属校验(reseller只能操作本组织名下客户:organization.parentid或discount_customer_bind,lookup不泄露别家存在性)③新增充值错帐处理三件套(列表按归属过滤+冲正走RECHARGE_REVERSE引擎翻转借贷+orderid=REV:原billid防重复+审计留痕;授FIN_ROLES不含reseller.sale)
This commit is contained in:
parent
6c45f8e798
commit
20dfdca82a
@ -79,10 +79,18 @@ async def recharge_accounting(sor, customerid, action, orderid,
|
|||||||
"""
|
"""
|
||||||
summary:recharge channe(handly, wechat, alipay)
|
summary:recharge channe(handly, wechat, alipay)
|
||||||
"""
|
"""
|
||||||
if action not in ['RECHARGE', 'RECHARGE_REVESE']:
|
# 2026-09-08 修拼写 bug:原白名单写的是 'RECHARGE_REVESE'(漏 R),而 const.py 的
|
||||||
|
# ACTION_RECHARGE_REVERSE='RECHARGE_REVERSE'、accounting_config.py 判冲正用
|
||||||
|
# endswith('_REVERSE')、unipay 支付宝退款传 'RECHARGE_REVERSE'——正确值一直被白名单
|
||||||
|
# 拒绝,错拼值又不被引擎识别为冲正(按普通充值方向记账,错上加错)。
|
||||||
|
# 错帐冲正与支付宝退款冲正因此长期不可用。改回正确值;兼容旧错拼防存量调用方。
|
||||||
|
if action not in ['RECHARGE', ACTION_RECHARGE_REVERSE, 'RECHARGE_REVESE']:
|
||||||
e = Exception(f'get a wrong recharge action({action})')
|
e = Exception(f'get a wrong recharge action({action})')
|
||||||
exception(f'{e}')
|
exception(f'{e}')
|
||||||
raise e
|
raise e
|
||||||
|
# 旧错拼归一为正确值(引擎按 endswith('_REVERSE') 判冲正方向)
|
||||||
|
if action == 'RECHARGE_REVESE':
|
||||||
|
action = ACTION_RECHARGE_REVERSE
|
||||||
recharge_log = {
|
recharge_log = {
|
||||||
"customerid":customerid,
|
"customerid":customerid,
|
||||||
"transdate":transdate,
|
"transdate":transdate,
|
||||||
|
|||||||
@ -214,6 +214,14 @@ PATHS_PROXY = [
|
|||||||
f"/{MOD}/proxy_recharge_submit.dspy",
|
f"/{MOD}/proxy_recharge_submit.dspy",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# 充值错帐处理(冲正)— 财务角色(FIN_ROLES):owner 财务 + reseller 管理员/会计/运营。
|
||||||
|
# 敏感操作不开放给 reseller.sale(代客充值可,冲正不可)。
|
||||||
|
PATHS_FIN += [
|
||||||
|
f"/{MOD}/recharge_reverse.ui",
|
||||||
|
f"/{MOD}/recharge_reverse_list.dspy",
|
||||||
|
f"/{MOD}/recharge_reverse_submit.dspy",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# 数据库辅助(角色解析 + 旧授权清理)
|
# 数据库辅助(角色解析 + 旧授权清理)
|
||||||
|
|||||||
@ -14,6 +14,36 @@ async def _audit_recharge(sor, operator_id, operator_name, customerid, customer_
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
async def _is_owner_role(sor, uid):
|
||||||
|
"""操作者是否持 owner 组织角色(owner 财务不受归属限制)。"""
|
||||||
|
recs = await sor.sqlExe(
|
||||||
|
"SELECT r.orgtypeid FROM userrole ur JOIN role r ON ur.roleid=r.id WHERE ur.userid=${u}$",
|
||||||
|
{'u': uid})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
return any((getattr(r, 'orgtypeid', '') or '') == 'owner' for r in (recs or []))
|
||||||
|
|
||||||
|
|
||||||
|
async def _customer_belongs(sor, customerid, resellerid):
|
||||||
|
"""客户是否归属该 reseller(2026-09-08 多租户隔离:reseller 财务角色只能操作
|
||||||
|
自己名下客户)。归属来源两条,命中其一即通过:
|
||||||
|
1. organization.parentid = resellerid(注册时按 tenant_domain 绑定,register.dspy)
|
||||||
|
2. discount_customer_bind.resellerid = resellerid(客户归属表)
|
||||||
|
"""
|
||||||
|
if not customerid or not resellerid or resellerid == '0':
|
||||||
|
return False
|
||||||
|
recs = await sor.sqlExe(
|
||||||
|
"SELECT id FROM organization WHERE id=${c}$ AND parentid=${r}$",
|
||||||
|
{'c': customerid, 'r': resellerid})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
if recs:
|
||||||
|
return True
|
||||||
|
recs2 = await sor.sqlExe(
|
||||||
|
"SELECT id FROM discount_customer_bind WHERE customerid=${c}$ AND resellerid=${r}$ LIMIT 1",
|
||||||
|
{'c': customerid, 'r': resellerid})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
return bool(recs2)
|
||||||
|
|
||||||
|
|
||||||
username = params_kw.get('username', '').strip()
|
username = params_kw.get('username', '').strip()
|
||||||
amount_raw = params_kw.get('amount', 0)
|
amount_raw = params_kw.get('amount', 0)
|
||||||
debug(f'{params_kw=},{username=}, {amount_raw=}')
|
debug(f'{params_kw=},{username=}, {amount_raw=}')
|
||||||
@ -27,6 +57,8 @@ if action == 'lookup':
|
|||||||
|
|
||||||
db = DBPools()
|
db = DBPools()
|
||||||
dbname = get_module_dbname('accounting')
|
dbname = get_module_dbname('accounting')
|
||||||
|
uid = await get_user()
|
||||||
|
userorgid = await get_userorgid()
|
||||||
async with db.sqlorContext(dbname) as sor:
|
async with db.sqlorContext(dbname) as sor:
|
||||||
sql = """
|
sql = """
|
||||||
select
|
select
|
||||||
@ -46,6 +78,11 @@ if action == 'lookup':
|
|||||||
return json.dumps({'status': 'error', 'message': f'用户 {username} 不存在'}, ensure_ascii=False, default=str)
|
return json.dumps({'status': 'error', 'message': f'用户 {username} 不存在'}, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
rec = recs[0]
|
rec = recs[0]
|
||||||
|
# 归属校验(reseller 财务角色只能查自己名下客户;不归属时按「不存在」应答,
|
||||||
|
# 不泄露别家客户的存在性/余额)
|
||||||
|
if not await _is_owner_role(sor, uid):
|
||||||
|
if not await _customer_belongs(sor, rec.customerid, userorgid):
|
||||||
|
return json.dumps({'status': 'error', 'message': f'用户 {username} 不存在'}, ensure_ascii=False, default=str)
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
'status': 'ok',
|
'status': 'ok',
|
||||||
'data': {
|
'data': {
|
||||||
@ -114,6 +151,14 @@ async with db.sqlorContext(dbname) as sor:
|
|||||||
"options": {"text": "❌ 不能给自己进行代客充值", "color": "#EF4444"}
|
"options": {"text": "❌ 不能给自己进行代客充值", "color": "#EF4444"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 归属校验(2026-09-08):reseller 财务角色只能给自己名下客户充值
|
||||||
|
if not await _is_owner_role(sor, userid):
|
||||||
|
if not await _customer_belongs(sor, customerid, userorgid):
|
||||||
|
return {
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {"text": f"❌ 找不到用户名: {username}", "color": "#EF4444"}
|
||||||
|
}
|
||||||
|
|
||||||
# Create payment log in unipay for audit trail
|
# Create payment log in unipay for audit trail
|
||||||
unipay_dbname = get_module_dbname('unipay')
|
unipay_dbname = get_module_dbname('unipay')
|
||||||
async with db.sqlorContext(unipay_dbname) as unipay_sor:
|
async with db.sqlorContext(unipay_dbname) as unipay_sor:
|
||||||
|
|||||||
42
wwwroot/recharge_reverse.ui
Normal file
42
wwwroot/recharge_reverse.ui
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {
|
||||||
|
"width": "100%",
|
||||||
|
"height": "100%",
|
||||||
|
"padding": "16px",
|
||||||
|
"gap": "12px"
|
||||||
|
},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "Title3",
|
||||||
|
"options": {"text": "充值错帐处理"}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "Text",
|
||||||
|
"options": {
|
||||||
|
"text": "选中错帐的充值账单点击「冲正」:引擎自动翻转原记账借贷方向(客户余额相应扣回),冲正单以 REV:原账单ID 记账、同一笔不可重复冲正、操作记入审计。分销商财务角色仅能处理自己名下客户的充值账单。",
|
||||||
|
"color": "#94A3B8",
|
||||||
|
"cfontsize": 0.9,
|
||||||
|
"halign": "left",
|
||||||
|
"wrap": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"widgettype": "VScrollPanel",
|
||||||
|
"options": {"css": "filler"},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"id": "rev_list",
|
||||||
|
"options": {"width": "100%"},
|
||||||
|
"subwidgets": [
|
||||||
|
{
|
||||||
|
"widgettype": "urlwidget",
|
||||||
|
"options": {"url": "{{entire_url('/accounting/recharge_reverse_list.dspy')}}"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
121
wwwroot/recharge_reverse_list.dspy
Normal file
121
wwwroot/recharge_reverse_list.dspy
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
# recharge_reverse_list.dspy — 充值错帐处理:充值账单列表(2026-09-08)
|
||||||
|
# 归属过滤:reseller 财务角色只看自己名下客户;owner 财务角色看全部。
|
||||||
|
# 每条账单一个「冲正」按钮(conform 确认),已冲正的显示标记禁用。
|
||||||
|
# 返回 widget 描述(VBox 卡片列表),由 recharge_reverse.ui 的 rev_list 容器加载。
|
||||||
|
|
||||||
|
dbname = get_module_dbname('accounting')
|
||||||
|
uid = await get_user()
|
||||||
|
if not uid:
|
||||||
|
return {"widgettype": "Text", "options": {"text": "请先登录", "color": "#EF4444"}}
|
||||||
|
userorgid = await get_userorgid() or ''
|
||||||
|
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
# 操作者是否 owner 财务角色(不受归属限制)
|
||||||
|
roles_recs = await sor.sqlExe(
|
||||||
|
"SELECT r.orgtypeid FROM userrole ur JOIN role r ON ur.roleid=r.id WHERE ur.userid=${u}$",
|
||||||
|
{'u': uid})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
is_owner = any((getattr(r, 'orgtypeid', '') or '') == 'owner' for r in (roles_recs or []))
|
||||||
|
|
||||||
|
# 充值账单(RECHARGE + 冲正 RECHARGE_REVERSE 一起查,冲正行用于判「已冲正」)
|
||||||
|
if is_owner:
|
||||||
|
bills = await sor.sqlExe(
|
||||||
|
"SELECT b.id, b.customerid, b.orderid, b.business_op, b.amount, b.bill_date, "
|
||||||
|
"COALESCE(o.orgname,'') AS orgname FROM bill b "
|
||||||
|
"LEFT JOIN organization o ON o.id=b.customerid "
|
||||||
|
"WHERE b.business_op IN ('RECHARGE','RECHARGE_REVERSE') "
|
||||||
|
"ORDER BY b.bill_date DESC, b.bill_timestamp DESC LIMIT 100", {})
|
||||||
|
else:
|
||||||
|
# reseller:只看自己名下客户(organization.parentid 或 discount_customer_bind)
|
||||||
|
bills = await sor.sqlExe(
|
||||||
|
"SELECT b.id, b.customerid, b.orderid, b.business_op, b.amount, b.bill_date, "
|
||||||
|
"COALESCE(o.orgname,'') AS orgname FROM bill b "
|
||||||
|
"LEFT JOIN organization o ON o.id=b.customerid "
|
||||||
|
"WHERE b.business_op IN ('RECHARGE','RECHARGE_REVERSE') "
|
||||||
|
"AND (o.parentid=${r}$ OR EXISTS ("
|
||||||
|
" SELECT 1 FROM discount_customer_bind dcb "
|
||||||
|
" WHERE dcb.customerid=b.customerid AND dcb.resellerid=${r}$)) "
|
||||||
|
"ORDER BY b.bill_date DESC, b.bill_timestamp DESC LIMIT 100", {'r': userorgid})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
|
||||||
|
# 已冲正集合:冲正单的 orderid='REV:<原bill.id>'
|
||||||
|
reversed_ids = set()
|
||||||
|
reverses = [b for b in (bills or []) if getattr(b, 'business_op', '') == 'RECHARGE_REVERSE']
|
||||||
|
for rv in reverses:
|
||||||
|
oid = getattr(rv, 'orderid', '') or ''
|
||||||
|
if oid.startswith('REV:'):
|
||||||
|
reversed_ids.add(oid[4:])
|
||||||
|
|
||||||
|
submit_url = entire_url('/accounting/recharge_reverse_submit.dspy')
|
||||||
|
list_url = entire_url('/accounting/recharge_reverse_list.dspy')
|
||||||
|
|
||||||
|
cards = []
|
||||||
|
for b in (bills or []):
|
||||||
|
bop = getattr(b, 'business_op', '')
|
||||||
|
bid = getattr(b, 'id', '')
|
||||||
|
if bop == 'RECHARGE_REVERSE':
|
||||||
|
# 冲正单本身只展示不再冲正
|
||||||
|
cards.append({
|
||||||
|
"widgettype": "HBox",
|
||||||
|
"options": {"bgcolor": "#F0FDF4", "border": "1px solid #BBF7D0",
|
||||||
|
"borderRadius": "8px", "padding": "10px 14px", "gap": "12px",
|
||||||
|
"alignItems": "center"},
|
||||||
|
"subwidgets": [
|
||||||
|
{"widgettype": "Text", "options": {"text": "冲正单", "bgcolor": "#16A34A",
|
||||||
|
"color": "#FFFFFF", "borderRadius": "6px",
|
||||||
|
"padding": "2px 8px", "cfontsize": 0.85}},
|
||||||
|
{"widgettype": "Text", "options": {"text": str(getattr(b, 'orgname', '') or getattr(b, 'customerid', '')), "halign": "left", "css": "filler"}},
|
||||||
|
{"widgettype": "Text", "options": {"text": "¥" + str(getattr(b, 'amount', 0)), "halign": "left"}},
|
||||||
|
{"widgettype": "Text", "options": {"text": str(getattr(b, 'bill_date', '')), "color": "#94A3B8", "cfontsize": 0.85, "halign": "left"}},
|
||||||
|
]})
|
||||||
|
continue
|
||||||
|
is_rev = bid in reversed_ids
|
||||||
|
row = [
|
||||||
|
{"widgettype": "Text", "options": {"text": "充值", "bgcolor": "#2563EB" if not is_rev else "#94A3B8",
|
||||||
|
"color": "#FFFFFF", "borderRadius": "6px",
|
||||||
|
"padding": "2px 8px", "cfontsize": 0.85}},
|
||||||
|
{"widgettype": "Text", "options": {"text": str(getattr(b, 'orgname', '') or getattr(b, 'customerid', '')), "halign": "left", "css": "filler"}},
|
||||||
|
{"widgettype": "Text", "options": {"text": "¥" + str(getattr(b, 'amount', 0)), "halign": "left"}},
|
||||||
|
{"widgettype": "Text", "options": {"text": str(getattr(b, 'bill_date', '')), "color": "#94A3B8", "cfontsize": 0.85, "halign": "left"}},
|
||||||
|
]
|
||||||
|
if is_rev:
|
||||||
|
row.append({"widgettype": "Text", "options": {"text": "已冲正", "color": "#16A34A", "cfontsize": 0.85, "halign": "left"}})
|
||||||
|
else:
|
||||||
|
# 冲正按钮:conform 确认 → script fetch POST → 成功后刷新列表
|
||||||
|
script = (
|
||||||
|
"var body=new URLSearchParams();"
|
||||||
|
"body.append('bill_id'," + json.dumps(bid) + ");"
|
||||||
|
"body.append('reason',(window.prompt&&window.prompt('冲正原因(记入审计)',''))||'');"
|
||||||
|
"var resp=await fetch(" + json.dumps(submit_url) + ",{method:'POST',body:body});"
|
||||||
|
"var rj=await resp.json();"
|
||||||
|
"if(rj.success){"
|
||||||
|
" var lc=bricks.getWidgetById('rev_list',bricks.app);"
|
||||||
|
" if(lc){lc.clear_widgets();var lr=await fetch(" + json.dumps(list_url) + ");var ld=await lr.json();"
|
||||||
|
" var lw=await bricks.widgetBuild(ld,lc);if(lw)lc.add_widget(lw);}"
|
||||||
|
" new bricks.Message({title:'冲正成功',message:rj.message||'已冲正'});"
|
||||||
|
"}else{new bricks.Message({title:'冲正失败',message:rj.error||'失败'});}")
|
||||||
|
row.append({
|
||||||
|
"widgettype": "Button",
|
||||||
|
"options": {"label": "冲正", "css": "small danger"},
|
||||||
|
"binds": [{
|
||||||
|
"wid": "self", "event": "click", "actiontype": "script", "target": "self",
|
||||||
|
"conform": {"title": "冲正确认",
|
||||||
|
"message": "确认冲正该笔充值 ¥" + str(getattr(b, 'amount', 0)) + "?冲正将翻转原记账方向(客户余额相应扣回),操作记入审计且不可自动撤销。",
|
||||||
|
"conform": {"label": "确认冲正"},
|
||||||
|
"discard": {"label": "取消"}},
|
||||||
|
"script": script}]})
|
||||||
|
cards.append({
|
||||||
|
"widgettype": "HBox",
|
||||||
|
"options": {"bgcolor": "#1E293B" if not is_rev else "#26313F",
|
||||||
|
"border": "1px solid #334155", "borderRadius": "8px",
|
||||||
|
"padding": "10px 14px", "gap": "12px", "alignItems": "center"},
|
||||||
|
"subwidgets": row})
|
||||||
|
|
||||||
|
if not cards:
|
||||||
|
cards = [{"widgettype": "Text", "options": {"text": "暂无充值账单", "color": "#94A3B8", "padding": "20px"}}]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"widgettype": "VBox",
|
||||||
|
"options": {"width": "100%", "gap": "8px"},
|
||||||
|
"subwidgets": cards}
|
||||||
108
wwwroot/recharge_reverse_submit.dspy
Normal file
108
wwwroot/recharge_reverse_submit.dspy
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# recharge_reverse_submit.dspy — 充值错帐冲正提交(2026-09-08)
|
||||||
|
# 参数:bill_id(要冲正的充值账单)、reason(冲正原因,记入审计)
|
||||||
|
# 逻辑:校验归属→防重复冲正→取原单费率→recharge_accounting(RECHARGE_REVERSE)
|
||||||
|
# 引擎自动翻转借贷方向(accounting_config.py endswith('_REVERSE'))→审计留痕
|
||||||
|
# orderid='REV:<bill_id>' 既防重复冲正,又让列表能识别「已冲正」
|
||||||
|
|
||||||
|
dbname = get_module_dbname('accounting')
|
||||||
|
uid = await get_user()
|
||||||
|
if not uid:
|
||||||
|
return json.dumps({'success': False, 'error': '未登录'}, ensure_ascii=False)
|
||||||
|
userorgid = await get_userorgid() or ''
|
||||||
|
bill_id = (params_kw or {}).get('bill_id', '') or ''
|
||||||
|
reason = (params_kw or {}).get('reason', '') or ''
|
||||||
|
if not bill_id:
|
||||||
|
return json.dumps({'success': False, 'error': '缺少 bill_id'}, ensure_ascii=False)
|
||||||
|
|
||||||
|
|
||||||
|
async def _is_owner_role(sor, uid):
|
||||||
|
recs = await sor.sqlExe(
|
||||||
|
"SELECT r.orgtypeid FROM userrole ur JOIN role r ON ur.roleid=r.id WHERE ur.userid=${u}$",
|
||||||
|
{'u': uid})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
return any((getattr(r, 'orgtypeid', '') or '') == 'owner' for r in (recs or []))
|
||||||
|
|
||||||
|
|
||||||
|
async def _customer_belongs(sor, customerid, resellerid):
|
||||||
|
if not customerid or not resellerid or resellerid == '0':
|
||||||
|
return False
|
||||||
|
recs = await sor.sqlExe(
|
||||||
|
"SELECT id FROM organization WHERE id=${c}$ AND parentid=${r}$",
|
||||||
|
{'c': customerid, 'r': resellerid})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
if recs:
|
||||||
|
return True
|
||||||
|
recs2 = await sor.sqlExe(
|
||||||
|
"SELECT id FROM discount_customer_bind WHERE customerid=${c}$ AND resellerid=${r}$ LIMIT 1",
|
||||||
|
{'c': customerid, 'r': resellerid})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
return bool(recs2)
|
||||||
|
|
||||||
|
|
||||||
|
db = DBPools()
|
||||||
|
async with db.sqlorContext(dbname) as sor:
|
||||||
|
# 1. 取原充值账单
|
||||||
|
recs = await sor.sqlExe(
|
||||||
|
"SELECT id, customerid, orderid, business_op, amount FROM bill WHERE id=${b}$",
|
||||||
|
{'b': bill_id})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
if not recs:
|
||||||
|
return json.dumps({'success': False, 'error': '账单不存在'}, ensure_ascii=False)
|
||||||
|
bill = recs[0]
|
||||||
|
if getattr(bill, 'business_op', '') != 'RECHARGE':
|
||||||
|
return json.dumps({'success': False, 'error': '只能冲正充值(RECHARGE)账单'}, ensure_ascii=False)
|
||||||
|
customerid = getattr(bill, 'customerid', '') or ''
|
||||||
|
|
||||||
|
# 2. 归属校验(reseller 财务只能冲正自己名下客户的充值)
|
||||||
|
is_owner = await _is_owner_role(sor, uid)
|
||||||
|
if not is_owner and not await _customer_belongs(sor, customerid, userorgid):
|
||||||
|
return json.dumps({'success': False, 'error': '无权冲正该账单(客户不归属当前组织)'}, ensure_ascii=False)
|
||||||
|
|
||||||
|
# 3. 防重复冲正:已存在 orderid='REV:<bill_id>' 的冲正单则拒绝
|
||||||
|
rev_orderid = 'REV:' + bill_id
|
||||||
|
exist = await sor.sqlExe(
|
||||||
|
"SELECT id FROM bill WHERE orderid=${o}$ AND business_op='RECHARGE_REVERSE' LIMIT 1",
|
||||||
|
{'o': rev_orderid})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
if exist:
|
||||||
|
return json.dumps({'success': False, 'error': '该充值账单已冲正过,不可重复冲正'}, ensure_ascii=False)
|
||||||
|
|
||||||
|
# 4. 取原单充值费率(payment_log.id = bill.orderid,代客充值/在线充值都写了 payment_log)
|
||||||
|
feerate = 0.0
|
||||||
|
orig_orderid = getattr(bill, 'orderid', '') or ''
|
||||||
|
if orig_orderid:
|
||||||
|
prec = await sor.sqlExe(
|
||||||
|
"SELECT pay_feerate FROM payment_log WHERE id=${o}$ LIMIT 1", {'o': orig_orderid})
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
if prec:
|
||||||
|
try:
|
||||||
|
feerate = float(getattr(prec[0], 'pay_feerate', 0) or 0)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
feerate = 0.0
|
||||||
|
|
||||||
|
# 5. 冲正记账(引擎按 endswith('_REVERSE') 翻转借贷方向;transdate=当前营业日)
|
||||||
|
amount = float(getattr(bill, 'amount', 0) or 0)
|
||||||
|
biz_date = await get_business_date(sor)
|
||||||
|
try:
|
||||||
|
await recharge_accounting(sor, customerid, 'RECHARGE_REVERSE', rev_orderid,
|
||||||
|
biz_date, amount, feerate)
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({'success': False,
|
||||||
|
'error': '冲正记账失败:%s' % str(e)[:200]}, ensure_ascii=False)
|
||||||
|
|
||||||
|
# 6. 审计留痕(旁路,失败不阻断)
|
||||||
|
try:
|
||||||
|
from app_audit import audit_log
|
||||||
|
op_recs = await sor.sqlExe("SELECT username FROM users WHERE id=${u}$", {'u': uid})
|
||||||
|
op_name = (op_recs[0].username if op_recs else uid)
|
||||||
|
await audit_log(sor, uid, op_name, 'recharge_reverse', target=customerid,
|
||||||
|
detail='充值错帐冲正 bill=%s 金额=%s 原因=%s' % (bill_id, amount, reason or '(未填)'),
|
||||||
|
result='ok', client_ip='')
|
||||||
|
await sor.sqlExe("COMMIT", {})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return json.dumps({'success': True,
|
||||||
|
'message': '冲正成功:¥%.2f(原账单 %s)' % (amount, bill_id)},
|
||||||
|
ensure_ascii=False)
|
||||||
Loading…
x
Reference in New Issue
Block a user