第二轮:客服额度资金闭环与收回额度

- 划额度扣商家余额,客服派单/链接只扣个人额度
- 新增 wallet/revoke 收回未使用额度,权限 wallet_revoke
- 客服额度单撤销/退款只释放额度,不再误退商家余额
- 链接待填单(13)可撤销;总管/财务可划收额度
This commit is contained in:
XingQue
2026-06-20 04:59:29 +08:00
parent b2520726dc
commit d68a1da2c2
10 changed files with 234 additions and 50 deletions

View File

@@ -98,8 +98,10 @@ def staff_dispatch_order(member, staff_user, data):
if shangjia.zhuangtai != 1:
return False, {'code': 403, 'msg': '商家状态异常', 'data': None}
if jiage > float(shangjia.yue):
return False, {'code': 400, 'msg': f'商家余额不足,当前{shangjia.yue}', 'data': None}
try:
check_wallet_available(member, jiage)
except ValueError as e:
return False, {'code': 400, 'msg': str(e), 'data': None}
zhiding_dashou = None
if zhiding_uid:
@@ -152,7 +154,6 @@ def staff_dispatch_order(member, staff_user, data):
UserShangjia.query.filter(id=shangjia.id).update(
fabu=F('fabu') + 1,
yue=F('yue') - Decimal(str(jiage)),
jinridingdan=F('jinridingdan') + 1,
jinriliushui=F('jinriliushui') + Decimal(str(jiage)),
jinyuedingdan=F('jinyuedingdan') + 1,

View File

@@ -97,8 +97,10 @@ def staff_generate_link(member, staff_user, data):
return False, {'code': 400, 'msg': '模板不存在', 'data': {}}
jiage = moban.Price
if jiage > shangjia.yue:
return False, {'code': 400, 'msg': f'商家余额不足,当前{shangjia.yue}', 'data': {}}
try:
check_wallet_available(member, jiage)
except ValueError as e:
return False, {'code': 400, 'msg': str(e), 'data': {}}
try:
lilu = float(CommissionRate.query.get(Platform='3').Rate) or 1.0
@@ -160,7 +162,6 @@ def staff_generate_link(member, staff_user, data):
UserShangjia.query.filter(id=shangjia.id).update(
fabu=F('fabu') + 1,
yue=F('yue') - jiage,
jinridingdan=F('jinridingdan') + 1,
jinriliushui=F('jinriliushui') + jiage,
jinyuedingdan=F('jinyuedingdan') + 1,

View File

@@ -9,6 +9,7 @@ from django.db.models import F
from backend.utils import update_shangjia_daily
from merchant_ops.constants import STAT_ACTION_DISPATCH, STAT_ACTION_REFUND, STAT_ACTION_SETTLE
from merchant_ops.models import MerchantOrderDispatch, MerchantStaffDailyStats, MerchantStaffMember
from merchant_ops.services.wallet import release_dispatch_quota
logger = logging.getLogger(__name__)
@@ -81,6 +82,11 @@ def sync_staff_stats_by_order_id(order_id, amount, action):
today_dispatch_count=F('today_dispatch_count') + 1,
today_dispatch_amount=F('today_dispatch_amount') + amount,
)
elif action == STAT_ACTION_REFUND:
try:
release_dispatch_quota(order_id, amount, member.staff_user_id)
except Exception:
logger.warning('释放客服派单额度失败 order_id=%s', order_id, exc_info=True)
def update_merchant_and_staff_stats(merchant_id, staff_member, amount, action, order_id=None):

View File

@@ -1,11 +1,14 @@
"""资金池:划额度、派单扣减。"""
"""资金池:划额度、派单扣减、退款释放"""
from decimal import Decimal
from django.db import transaction
from django.db.models import F
from merchant_ops.constants import WALLET_ALLOCATE, WALLET_DISPATCH_CONFIRM
from merchant_ops.constants import (
WALLET_ALLOCATE, WALLET_DISPATCH_CONFIRM, WALLET_REFUND_RELEASE,
)
from merchant_ops.models import MerchantStaffWallet, MerchantStaffWalletLedger
from users.models import UserShangjia
def get_or_create_wallet(member):
@@ -18,15 +21,23 @@ def get_or_create_wallet(member):
def allocate_quota(member, amount, operator_id, remark=''):
"""
老板划额度:从商家余额转入客服可用额度(商家余额减少,客服 quota_total 增加)。
"""
amount = Decimal(str(amount))
if amount <= 0:
raise ValueError('划额度必须大于0')
with transaction.atomic():
shangjia = UserShangjia.objects.select_for_update().get(user__UserUID=member.merchant_id)
if amount > shangjia.yue:
raise ValueError(f'商家余额不足,当前{shangjia.yue}')
wallet = MerchantStaffWallet.objects.select_for_update().get(
merchant_id=member.merchant_id,
staff_user_id=member.staff_user_id,
)
UserShangjia.objects.filter(id=shangjia.id).update(yue=F('yue') - amount)
wallet.quota_total = F('quota_total') + amount
wallet.save(update_fields=['quota_total'])
wallet.refresh_from_db()
@@ -38,7 +49,7 @@ def allocate_quota(member, amount, operator_id, remark=''):
amount=amount,
balance_after=wallet.quota_available,
operator_id=operator_id,
remark=remark,
remark=remark or '老板划额度',
)
return wallet
@@ -47,11 +58,11 @@ def check_wallet_available(member, amount):
amount = Decimal(str(amount))
wallet = get_or_create_wallet(member)
if amount > wallet.quota_available:
raise ValueError('客服可用额度不足')
raise ValueError(f'客服可用额度不足,当前{wallet.quota_available}')
def confirm_dispatch_deduct(member, amount, order_id, operator_id):
"""派单成功:扣客服额度。"""
"""派单成功:扣客服额度(商家余额已在划额度时扣除)"""
amount = Decimal(str(amount))
with transaction.atomic():
wallet = MerchantStaffWallet.objects.select_for_update().get(
@@ -75,3 +86,97 @@ def confirm_dispatch_deduct(member, amount, order_id, operator_id):
operator_id=operator_id,
remark='派单扣额度',
)
def release_dispatch_quota(order_id, amount, operator_id, remark='退款释放额度'):
"""订单撤销/退款:释放子客服已扣额度。"""
from merchant_ops.models import MerchantOrderDispatch, MerchantStaffMember
amount = Decimal(str(amount))
if amount <= 0 or not order_id:
return
disp = MerchantOrderDispatch.objects.filter(order_id=order_id).first()
if not disp or not disp.staff_member_id:
return
member = MerchantStaffMember.objects.filter(id=disp.staff_member_id).first()
if not member:
return
with transaction.atomic():
wallet = MerchantStaffWallet.objects.select_for_update().filter(
merchant_id=member.merchant_id,
staff_user_id=member.staff_user_id,
).first()
if not wallet:
return
release = min(amount, wallet.quota_used)
if release <= 0:
return
wallet.quota_used = F('quota_used') - release
wallet.save(update_fields=['quota_used'])
wallet.refresh_from_db()
MerchantStaffWalletLedger.objects.create(
wallet=wallet,
merchant_id=member.merchant_id,
staff_user_id=member.staff_user_id,
change_type=WALLET_REFUND_RELEASE,
amount=release,
balance_after=wallet.quota_available,
related_order_id=order_id,
operator_id=operator_id or member.staff_user_id,
remark=remark,
)
def is_staff_quota_order(order_id):
"""订单是否由子客服额度出资(划额度池 + 派单扣额度,非老板余额直扣)。"""
from merchant_ops.models import MerchantOrderDispatch
if not order_id:
return False
return MerchantOrderDispatch.objects.filter(
order_id=order_id,
staff_member_id__isnull=False,
).exists()
def revoke_quota(member, amount, operator_id, remark=''):
"""
收回未使用额度:客服 quota_total 减少,商家余额增加。
已派单占用quota_used部分不可收回防止资金悬空或重复退回。
"""
amount = Decimal(str(amount))
if amount <= 0:
raise ValueError('收回额度必须大于0')
with transaction.atomic():
wallet = MerchantStaffWallet.objects.select_for_update().get(
merchant_id=member.merchant_id,
staff_user_id=member.staff_user_id,
)
available = wallet.quota_total - wallet.quota_used - wallet.quota_frozen
if amount > available:
raise ValueError(
f'最多可收回{available}元(未使用额度);'
f'已派单占用{wallet.quota_used}元需先撤销/退款订单后再释放'
)
shangjia = UserShangjia.objects.select_for_update().get(user__UserUID=member.merchant_id)
wallet.quota_total = F('quota_total') - amount
wallet.save(update_fields=['quota_total'])
wallet.refresh_from_db()
UserShangjia.objects.filter(id=shangjia.id).update(yue=F('yue') + amount)
MerchantStaffWalletLedger.objects.create(
wallet=wallet,
merchant_id=member.merchant_id,
staff_user_id=member.staff_user_id,
change_type=WALLET_REVOKE,
amount=-amount,
balance_after=wallet.quota_available,
operator_id=operator_id,
remark=remark or '收回未使用额度',
)
return wallet