Files
Django/merchant_ops/services/stats.py
XingQue d68a1da2c2 第二轮:客服额度资金闭环与收回额度
- 划额度扣商家余额,客服派单/链接只扣个人额度
- 新增 wallet/revoke 收回未使用额度,权限 wallet_revoke
- 客服额度单撤销/退款只释放额度,不再误退商家余额
- 链接待填单(13)可撤销;总管/财务可划收额度
2026-06-20 04:59:29 +08:00

183 lines
6.4 KiB
Python

"""子客服日统计 + 商家日统计双写。"""
import logging
from datetime import date
from decimal import Decimal
from django.db import transaction
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__)
def _action_fields(action, amount):
amount = Decimal(str(amount))
if action == STAT_ACTION_DISPATCH:
return {'dispatch_count': 1, 'dispatch_amount': amount}
if action == STAT_ACTION_SETTLE:
return {'settle_count': 1, 'settle_amount': amount}
if action == STAT_ACTION_REFUND:
return {'refund_count': 1, 'refund_amount': amount}
return {}
def update_staff_daily(merchant_id, staff_user_id, member_id, amount, action):
"""子客服日统计;老板操作不写此表。"""
amount = Decimal(str(amount)) if amount else Decimal('0.00')
fields = _action_fields(action, amount)
if not fields:
return
today = date.today()
with transaction.atomic():
stat, _ = MerchantStaffDailyStats.objects.select_for_update().get_or_create(
merchant_id=merchant_id,
staff_user_id=staff_user_id,
stat_date=today,
defaults={
'club_id': 'xq',
'member_id': member_id,
'year': today.year,
'month': today.month,
'day': today.day,
},
)
update_kwargs = {}
for k, v in fields.items():
if k.endswith('_count'):
update_kwargs[k] = F(k) + int(v)
else:
update_kwargs[k] = F(k) + v
MerchantStaffDailyStats.query.filter(id=stat.id).update(**update_kwargs)
def sync_staff_stats_by_order_id(order_id, amount, action):
"""按订单派单归属同步子客服日统计(商家侧由 update_shangjia_daily 负责)。"""
if not order_id:
return
amount = Decimal(str(amount)) if amount else Decimal('0.00')
if amount <= 0:
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
update_staff_daily(
disp.merchant_id,
member.staff_user_id,
member.id,
amount,
action,
)
if action == STAT_ACTION_DISPATCH:
MerchantStaffMember.objects.filter(id=member.id).update(
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):
"""商家日统计必写;有 order_id 时按派单归属写客服统计,否则写操作人。"""
update_shangjia_daily(merchant_id, amount, action, order_id=order_id)
if order_id:
return
if staff_member:
update_staff_daily(
merchant_id,
staff_member.staff_user_id,
staff_member.id,
amount,
action,
)
def get_staff_stats_payload(member):
"""子客服个人统计(今日 + 累计),供 /me 与首页展示。"""
from django.db.models import Sum
today = date.today()
today_stat = MerchantStaffDailyStats.objects.filter(
member_id=member.id, stat_date=today,
).first()
total = MerchantStaffDailyStats.objects.filter(member_id=member.id).aggregate(
dispatch_count=Sum('dispatch_count'),
dispatch_amount=Sum('dispatch_amount'),
settle_count=Sum('settle_count'),
settle_amount=Sum('settle_amount'),
refund_count=Sum('refund_count'),
refund_amount=Sum('refund_amount'),
)
return {
'today_dispatch_count': (
today_stat.dispatch_count if today_stat else int(member.today_dispatch_count or 0)
),
'today_dispatch_amount': str(
today_stat.dispatch_amount if today_stat else (member.today_dispatch_amount or '0.00')
),
'today_refund_count': today_stat.refund_count if today_stat else 0,
'today_settle_count': today_stat.settle_count if today_stat else 0,
'dispatch_count': total['dispatch_count'] or 0,
'dispatch_amount': str(total['dispatch_amount'] or '0.00'),
'settle_count': total['settle_count'] or 0,
'settle_amount': str(total['settle_amount'] or '0.00'),
'refund_count': total['refund_count'] or 0,
'refund_amount': str(total['refund_amount'] or '0.00'),
}
def bump_cancel_count(merchant_id, staff_member):
if not staff_member:
return
today = date.today()
with transaction.atomic():
stat, _ = MerchantStaffDailyStats.objects.select_for_update().get_or_create(
merchant_id=merchant_id,
staff_user_id=staff_member.staff_user_id,
stat_date=today,
defaults={
'club_id': 'xq',
'member_id': staff_member.id,
'year': today.year,
'month': today.month,
'day': today.day,
},
)
MerchantStaffDailyStats.query.filter(id=stat.id).update(
cancel_count=F('cancel_count') + 1,
)
def bump_penalty_count(merchant_id, staff_member):
if not staff_member:
return
today = date.today()
with transaction.atomic():
stat, _ = MerchantStaffDailyStats.objects.select_for_update().get_or_create(
merchant_id=merchant_id,
staff_user_id=staff_member.staff_user_id,
stat_date=today,
defaults={
'club_id': 'xq',
'member_id': staff_member.id,
'year': today.year,
'month': today.month,
'day': today.day,
},
)
MerchantStaffDailyStats.query.filter(id=stat.id).update(
penalty_count=F('penalty_count') + 1,
)