accounting/wwwroot/proxy_recharge_submit.dspy

224 lines
6.9 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

async def _audit_recharge(sor, operator_id, operator_name, customerid, customer_name, orgname, amount, client_ip):
"""代客充值审计:检查审计模块,有则写入,失败不阻断主流程。"""
try:
from app_audit import audit_log
except ImportError:
return
try:
await audit_log(
sor, operator_id, operator_name, 'customer_recharge',
target=customerid,
detail='代客充值 客户=' + str(customer_name) + ' (' + str(orgname or '') + ') 金额=' + str(amount),
result='ok', client_ip=client_ip)
except Exception:
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):
"""客户是否归属该 reseller2026-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()
amount_raw = params_kw.get('amount', 0)
debug(f'{params_kw=},{username=}, {amount_raw=}')
action = params_kw.get('action', 'submit')
# ---- Lookup mode: find customer by username, return info ----
if action == 'lookup':
username = params_kw.get('username', '').strip()
if not username:
return json.dumps({'status': 'error', 'message': '用户名不能为空'}, ensure_ascii=False, default=str)
db = DBPools()
dbname = get_module_dbname('accounting')
uid = await get_user()
userorgid = await get_userorgid()
async with db.sqlorContext(dbname) as sor:
sql = """
select
u.username,
u.orgid as customerid,
o.orgname,
a.id as accountid,
a.balance
from users u
left join organization o on u.orgid = o.id COLLATE utf8mb4_unicode_ci
left join account a on a.orgid = u.orgid COLLATE utf8mb4_unicode_ci
where u.username = ${username}$
limit 1
"""
recs = await sor.sqlExe(sql, {'username': username})
if not recs or len(recs) == 0:
return json.dumps({'status': 'error', 'message': f'用户 {username} 不存在'}, ensure_ascii=False, default=str)
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({
'status': 'ok',
'data': {
'username': rec.username,
'customerid': rec.customerid,
'orgname': rec.orgname or '',
'accountid': rec.accountid or '',
'balance': float(rec.balance) if rec.balance else 0.0
}
}, ensure_ascii=False, default=str)
# ---- Submit mode: process the proxy recharge ----
if not username:
return {
"widgettype": "Text",
"options": {"text": "❌ 用户名不能为空", "color": "#EF4444"}
}
try:
amount = float(amount_raw)
except (ValueError, TypeError):
return {
"widgettype": "Text",
"options": {"text": "❌ 充值金额格式错误", "color": "#EF4444"}
}
if amount != amount or amount <= 0:
return {
"widgettype": "Text",
"options": {"text": "❌ 充值金额必须大于0", "color": "#EF4444"}
}
userid = await get_user()
userorgid = await get_userorgid()
db = DBPools()
# Look up the target customer by username
dbname = get_module_dbname('accounting')
async with db.sqlorContext(dbname) as sor:
sql = """
select
u.username,
u.orgid as customerid,
o.orgname,
a.id as accountid
from users u
left join organization o on u.orgid = o.id COLLATE utf8mb4_unicode_ci
left join account a on a.orgid = u.orgid COLLATE utf8mb4_unicode_ci
where u.username = ${username}$
limit 1
"""
recs = await sor.sqlExe(sql, {'username': username})
if not recs or len(recs) == 0:
return {
"widgettype": "Text",
"options": {"text": f"❌ 找不到用户名: {username}", "color": "#EF4444"}
}
customer = recs[0]
customerid = customer.customerid
if customerid == userorgid:
return {
"widgettype": "Text",
"options": {"text": "❌ 不能给自己进行代客充值", "color": "#EF4444"}
}
# 归属校验2026-09-08reseller 财务角色只能给自己名下客户充值
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
unipay_dbname = get_module_dbname('unipay')
async with db.sqlorContext(unipay_dbname) as unipay_sor:
plog_id = uuid()
biz_date = await get_business_date(sor)
now_str = timestampstr()
plog_data = {
"id": plog_id,
"customerid": customerid,
"channelid": "proxy",
"payment_name": "充值",
"payer_client_ip": "admin_proxy",
"amount_total": amount,
"pay_feerate": 0.0,
"pay_fee": 0.0,
"currency": "CNY",
"payment_status": "1",
"init_timestamp": now_str,
"payed_timestamp": now_str,
"cancel_timestamp": "2000-01-01 00:00:00.001",
"userid": userid
}
await unipay_sor.C('payment_log', plog_data.copy())
# Perform recharge accounting
await recharge_accounting(
sor,
customerid,
'RECHARGE',
plog_id,
biz_date,
amount,
0.0
)
# 审计:代客充值(旁路,失败不阻断主流程)
_op_username = userid
try:
_op_recs = await sor.sqlExe("SELECT username FROM users WHERE id=${u}$", {'u': userid})
if _op_recs:
_op_username = _op_recs[0].username or userid
except Exception:
pass
_client_ip = ''
try:
_client_ip = request.get('client_ip', '') or ''
except Exception:
pass
await _audit_recharge(sor, userid, _op_username, customerid, username, customer.orgname or '', amount, _client_ip)
debug(f'Proxy recharge: user={username}, customerid={customerid}, amount={amount}, operator={userid}')
orgname = customer.orgname or ''
return {
"widgettype": "Text",
"options": {
"text": f"✅ 代客充值成功 — 已为用户 {username} ({orgname}) 充值 ¥{amount:.2f}",
"color": "#22C55E",
"fontSize": "14px",
"fontWeight": "500"
}
}