Files
along_django/houtai/tixian_profit_views.py
2026-07-16 19:54:13 +08:00

330 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
提现抽成利润统计(真实抽成 = 申请扣款额 实际到账额 = 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=FalseMySQL 禁止 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,
},
})