330 lines
14 KiB
Python
330 lines
14 KiB
Python
"""
|
||
提现抽成利润统计(真实抽成 = 申请扣款额 − 实际到账额 = 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.conf import settings
|
||
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):
|
||
"""日历日 → datetime 区间 [start, end];兼容 USE_TZ=False(MySQL 禁止 aware datetime)。"""
|
||
start = datetime.combine(d, time.min)
|
||
end = datetime.combine(d, time(23, 59, 59, 999999))
|
||
if getattr(settings, 'USE_TZ', False):
|
||
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,
|
||
},
|
||
})
|