P0防双笔:查不清绝不新建第二笔;新增提现抽成利润统计接口。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
XingQue
2026-07-16 19:49:38 +08:00
parent 52e777c3a0
commit fc93f1c8fc
4 changed files with 374 additions and 65 deletions

View File

@@ -0,0 +1,328 @@
"""
提现抽成利润统计(真实抽成 = 申请扣款额 实际到账额 = shouxufei
口径说明(务必与分账一致):
- 仅统计「提现成功」记录Tixianjilu.zhuangtai = 2
- 利润 = COALESCE(shouxufei, max(shenqing_jine - jine, 0))
- 手动/自动一并计入dakuan_mode 可筛)
- 日期维度默认按 update_time成功落账日可按 create_time申请日
- 不含会员/押金充值/商家充值等,那些不是抽成利润
"""
import logging
from datetime import datetime, time
from decimal import Decimal
from django.db.models import (
Count, DecimalField, F, Q, Sum, Value,
)
from django.db.models.functions import Coalesce, Greatest, TruncDate, TruncMonth
from django.utils import timezone
from rest_framework.parsers import JSONParser
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from yonghu.models import Tixianjilu
from .view import verify_kefu_permission
logger = logging.getLogger(__name__)
_LEIXING_LABEL = {
1: '打手佣金',
2: '管事分红',
3: '组长分红',
4: '审核官分佣',
5: '打手押金',
6: '商家余额',
}
_FEE_EXPR = Coalesce(
F('shouxufei'),
Greatest(
Coalesce(F('shenqing_jine'), Value(Decimal('0.00')))
- Coalesce(F('jine'), Value(Decimal('0.00'))),
Value(Decimal('0.00')),
),
Value(Decimal('0.00')),
output_field=DecimalField(max_digits=12, decimal_places=2),
)
def _parse_date(s):
if not s:
return None
try:
return datetime.strptime(str(s).strip()[:10], '%Y-%m-%d').date()
except (TypeError, ValueError):
return None
def _day_bounds(d):
"""本地日历日 → aware datetime 区间 [start, end)"""
start = datetime.combine(d, time.min)
end = datetime.combine(d, time.max)
if timezone.is_naive(start):
tz = timezone.get_current_timezone()
start = timezone.make_aware(start, tz)
end = timezone.make_aware(end, tz)
return start, end
class TixianChouchengProfitView(APIView):
"""
POST /houtai/txchouchengtj
提现抽成利润统计(分账专用口径)
参数:
phone: 客服手机号
date_from / date_to: YYYY-MM-DD闭区间按成功日或申请日
date_field: update_time(默认,成功日) | create_time(申请日)
leixing: 1~6 可选;不传=全部身份
dakuan_mode: 1手动 2自动 可选;不传=全部
group_by: day | month | leixing | none默认 day
page / page_size: 明细分页(可选,仅 group_by=none 时返回 list
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
phone = (request.data.get('phone') or request.data.get('username') or '').strip()
kefu, permissions = verify_kefu_permission(request, phone)
if kefu is None:
return permissions
if 'caiwu' not in permissions and not any(
p in permissions for p in ('5500a', '5500b', '5500c')
):
return Response({'code': 403, 'msg': '无提现抽成利润查看权限'})
date_from = _parse_date(request.data.get('date_from') or request.data.get('start_date'))
date_to = _parse_date(request.data.get('date_to') or request.data.get('end_date'))
if not date_from or not date_to:
today = timezone.localdate()
date_from = date_from or today
date_to = date_to or today
if date_from > date_to:
return Response({'code': 400, 'msg': 'date_from 不能晚于 date_to'})
date_field = (request.data.get('date_field') or 'update_time').strip()
if date_field not in ('update_time', 'create_time'):
return Response({'code': 400, 'msg': 'date_field 仅支持 update_time / create_time'})
group_by = (request.data.get('group_by') or 'day').strip()
if group_by not in ('day', 'month', 'leixing', 'none'):
return Response({'code': 400, 'msg': 'group_by 仅支持 day/month/leixing/none'})
leixing_val = None
raw_leixing = request.data.get('leixing')
if raw_leixing is not None and str(raw_leixing).strip() != '':
try:
leixing_val = int(raw_leixing)
except (TypeError, ValueError):
return Response({'code': 400, 'msg': 'leixing 无效'})
if leixing_val not in (1, 2, 3, 4, 5, 6):
return Response({'code': 400, 'msg': 'leixing 仅支持 1~6'})
dakuan_mode_val = None
raw_mode = request.data.get('dakuan_mode')
if raw_mode is not None and str(raw_mode).strip() != '':
try:
dakuan_mode_val = int(raw_mode)
except (TypeError, ValueError):
return Response({'code': 400, 'msg': 'dakuan_mode 无效'})
if dakuan_mode_val not in (1, 2):
return Response({'code': 400, 'msg': 'dakuan_mode 仅支持 1/2'})
start_dt, _ = _day_bounds(date_from)
_, end_dt = _day_bounds(date_to)
# 仅提现成功;手动+自动
q = Q(zhuangtai=2)
if date_field == 'create_time':
q &= Q(create_time__gte=start_dt, create_time__lte=end_dt)
else:
q &= Q(update_time__gte=start_dt, update_time__lte=end_dt)
if leixing_val is not None:
q &= Q(leixing=leixing_val)
if dakuan_mode_val is not None:
q &= Q(dakuan_mode=dakuan_mode_val)
qs = Tixianjilu.objects.filter(q)
annotated = qs.annotate(fee=_FEE_EXPR)
total_agg = annotated.aggregate(
fee_sum=Sum('fee'),
apply_sum=Sum(Coalesce(F('shenqing_jine'), F('jine'), Value(Decimal('0.00')))),
payout_sum=Sum(Coalesce(F('jine'), Value(Decimal('0.00')))),
cnt=Count('id'),
)
fee_total = total_agg.get('fee_sum') or Decimal('0.00')
apply_total = total_agg.get('apply_sum') or Decimal('0.00')
payout_total = total_agg.get('payout_sum') or Decimal('0.00')
# 交叉校验:申请 到账 应约等于抽成(允许历史脏数据偏差)
cross_check = (apply_total - payout_total).quantize(Decimal('0.01'))
summary = {
'profit_total': str(fee_total.quantize(Decimal('0.01'))),
'apply_total': str(apply_total.quantize(Decimal('0.01'))),
'payout_total': str(payout_total.quantize(Decimal('0.01'))),
'apply_minus_payout': str(cross_check),
'success_count': int(total_agg.get('cnt') or 0),
'date_from': date_from.isoformat(),
'date_to': date_to.isoformat(),
'date_field': date_field,
'leixing': leixing_val,
'leixing_label': _LEIXING_LABEL.get(leixing_val, '全部身份'),
'dakuan_mode': dakuan_mode_val,
'formula': '利润=申请扣款额(shenqing_jine)−实际到账(jine)=手续费(shouxufei);仅统计提现成功(zhuangtai=2)',
}
rows = []
if group_by == 'day':
trunc = TruncDate(date_field)
grouped = (
annotated.annotate(period=trunc)
.values('period')
.annotate(
profit=Sum('fee'),
apply_amount=Sum(Coalesce(F('shenqing_jine'), F('jine'), Value(Decimal('0.00')))),
payout_amount=Sum(Coalesce(F('jine'), Value(Decimal('0.00')))),
count=Count('id'),
)
.order_by('period')
)
for g in grouped:
period = g['period']
rows.append({
'period': period.isoformat() if period else '',
'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))),
'apply_amount': str((g['apply_amount'] or Decimal('0')).quantize(Decimal('0.01'))),
'payout_amount': str((g['payout_amount'] or Decimal('0')).quantize(Decimal('0.01'))),
'count': g['count'] or 0,
})
elif group_by == 'month':
trunc = TruncMonth(date_field)
grouped = (
annotated.annotate(period=trunc)
.values('period')
.annotate(
profit=Sum('fee'),
apply_amount=Sum(Coalesce(F('shenqing_jine'), F('jine'), Value(Decimal('0.00')))),
payout_amount=Sum(Coalesce(F('jine'), Value(Decimal('0.00')))),
count=Count('id'),
)
.order_by('period')
)
for g in grouped:
period = g['period']
rows.append({
'period': period.strftime('%Y-%m') if period else '',
'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))),
'apply_amount': str((g['apply_amount'] or Decimal('0')).quantize(Decimal('0.01'))),
'payout_amount': str((g['payout_amount'] or Decimal('0')).quantize(Decimal('0.01'))),
'count': g['count'] or 0,
})
elif group_by == 'leixing':
grouped = (
annotated.values('leixing')
.annotate(
profit=Sum('fee'),
apply_amount=Sum(Coalesce(F('shenqing_jine'), F('jine'), Value(Decimal('0.00')))),
payout_amount=Sum(Coalesce(F('jine'), Value(Decimal('0.00')))),
count=Count('id'),
)
.order_by('leixing')
)
for g in grouped:
lx = g['leixing']
rows.append({
'leixing': lx,
'leixing_label': _LEIXING_LABEL.get(lx, f'类型{lx}'),
'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))),
'apply_amount': str((g['apply_amount'] or Decimal('0')).quantize(Decimal('0.01'))),
'payout_amount': str((g['payout_amount'] or Decimal('0')).quantize(Decimal('0.01'))),
'count': g['count'] or 0,
})
else:
# 明细
try:
page = max(1, int(request.data.get('page') or 1))
page_size = min(200, max(1, int(request.data.get('page_size') or 50)))
except (TypeError, ValueError):
page, page_size = 1, 50
total = annotated.count()
items = list(
annotated.order_by(f'-{date_field}', '-id')
[(page - 1) * page_size: page * page_size]
.values(
'id', 'yonghuid', 'leixing', 'dakuan_mode',
'shenqing_jine', 'jine', 'shouxufei', 'fee',
'create_time', 'update_time', 'shenhe_danhao',
)
)
for it in items:
fee = it.get('fee') or Decimal('0')
rows.append({
'id': it['id'],
'yonghuid': it['yonghuid'],
'leixing': it['leixing'],
'leixing_label': _LEIXING_LABEL.get(it['leixing'], ''),
'dakuan_mode': it['dakuan_mode'],
'dakuan_mode_label': '自动' if it['dakuan_mode'] == 2 else '手动',
'shenqing_jine': str(it['shenqing_jine'] or '0.00'),
'shijidaozhang': str(it['jine'] or '0.00'),
'shouxufei': str(it['shouxufei'] if it['shouxufei'] is not None else fee),
'profit': str(Decimal(fee).quantize(Decimal('0.01'))),
'shenhe_danhao': it.get('shenhe_danhao') or '',
'create_time': it['create_time'].strftime('%Y-%m-%d %H:%M:%S') if it.get('create_time') else '',
'update_time': it['update_time'].strftime('%Y-%m-%d %H:%M:%S') if it.get('update_time') else '',
})
return Response({
'code': 0,
'data': {
'summary': summary,
'group_by': group_by,
'list': rows,
'total': total,
'page': page,
'page_size': page_size,
},
})
# 按身份拆分汇总(便于分账核对,始终附带)
by_leixing = []
for g in (
annotated.values('leixing')
.annotate(profit=Sum('fee'), count=Count('id'))
.order_by('leixing')
):
by_leixing.append({
'leixing': g['leixing'],
'leixing_label': _LEIXING_LABEL.get(g['leixing'], ''),
'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))),
'count': g['count'] or 0,
})
by_mode = []
for g in (
annotated.values('dakuan_mode')
.annotate(profit=Sum('fee'), count=Count('id'))
.order_by('dakuan_mode')
):
by_mode.append({
'dakuan_mode': g['dakuan_mode'],
'dakuan_mode_label': '自动' if g['dakuan_mode'] == 2 else '手动',
'profit': str((g['profit'] or Decimal('0')).quantize(Decimal('0.01'))),
'count': g['count'] or 0,
})
return Response({
'code': 0,
'data': {
'summary': summary,
'group_by': group_by,
'rows': rows,
'by_leixing': by_leixing,
'by_dakuan_mode': by_mode,
},
})