224 lines
6.9 KiB
Plaintext
224 lines
6.9 KiB
Plaintext
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):
|
||
"""客户是否归属该 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()
|
||
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-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
|
||
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"
|
||
}
|
||
}
|