969 lines
41 KiB
Python
969 lines
41 KiB
Python
import hmac
|
||
import threading
|
||
import traceback
|
||
import uuid
|
||
import hashlib
|
||
import xmltodict
|
||
import time
|
||
import logging
|
||
import requests
|
||
import os
|
||
import secrets
|
||
import random
|
||
import string
|
||
from calendar import monthrange
|
||
from decimal import Decimal
|
||
from datetime import date, datetime, timedelta
|
||
from django.utils import timezone
|
||
from django.conf import settings
|
||
from django.core.exceptions import ObjectDoesNotExist
|
||
from django.core.paginator import Paginator
|
||
from django.db import models, transaction, IntegrityError
|
||
from django.db.models import Q, Count, Sum, F, Exists, OuterRef
|
||
from gvsdsdk.fluent import db, func, FQ
|
||
from django.db.models.functions import TruncDate, TruncMonth, TruncYear
|
||
from django.contrib.auth.hashers import make_password
|
||
from django.views.decorators.csrf import csrf_exempt
|
||
from django.utils.decorators import method_decorator
|
||
|
||
from rest_framework.views import APIView
|
||
from rest_framework.response import Response
|
||
from rest_framework import status
|
||
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
|
||
|
||
# 工具类导入
|
||
from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss
|
||
from backend.utils import (
|
||
update_dashou_daily_by_action,
|
||
update_shangjia_daily
|
||
)
|
||
from jituan.services.club_context import filter_club_char_field, filter_queryset_by_club, filter_user_related_by_club, filter_user_qs_by_club, orders_for_request
|
||
from jituan.services.catalog_scope import block_non_group_catalog_scope
|
||
from jituan.services.club_penalty import (
|
||
filter_penalty_qs, resolve_penalty_club_id, filter_gsfenhong_qs,
|
||
forbid_penalty_out_of_scope, resolve_penalty_record_club_id,
|
||
)
|
||
from jituan.services.club_user_access import forbid_if_user_out_of_scope, list_response_meta
|
||
from shop.utils import update_dianpu_daily_stat
|
||
from products.utils import update_shangpin_daily_stat
|
||
from orders.notice_tasks import dingdan_guangbo
|
||
from ..utils import (
|
||
verify_kefu_permission,
|
||
PERMISSION_TO_SHENFEN,
|
||
SHENFEN_PROFILE_MAP,
|
||
has_fadan_view_permission,
|
||
has_merchant_order_permission,
|
||
check_fadan_permission,
|
||
pick_order_id,
|
||
pick_penalty_create_params,
|
||
write_xiugai_log,
|
||
XIUGAI_LEIXING_DASHOU,
|
||
XIUGAI_LEIXING_GUANSHI,
|
||
XIUGAI_LEIXING_SHANGJIA,
|
||
XIUGAI_LEIXING_ZUZHANG,
|
||
XIUGAI_LEIXING_LABEL,
|
||
)
|
||
|
||
# models 集中导入
|
||
## gvsdsdk RBAC 模型(使用 PascalCase 字段名,匹配实际数据库表结构)
|
||
from gvsdsdk.models import Role, Permission, RolePermission, UserRole
|
||
## backend
|
||
from backend.models import WithdrawalDailyStats
|
||
|
||
## users
|
||
from users.models import (
|
||
UserGuanshi, UserBoss, UserZuzhang,
|
||
UserKefu, UserDashou, Xiugaijilu, UserShangjia, UserShenheguan
|
||
)
|
||
from users.business_models import User
|
||
|
||
## orders
|
||
from orders.models import (
|
||
CommissionRate, PlayerDeliveryImage, PenaltyRecord,
|
||
OrderPlayerHistory, Order, RefundRecord,
|
||
MerchantOrderExt, Penalty, PenaltyAppealImage, PenaltyBonus
|
||
)
|
||
|
||
## shop
|
||
from shop.models import Dianpu, DianpuMorenPeizhi, YonghuPingzheng, ShangpinLeixingDianpu, DianpuShangpinShenheShezhi
|
||
|
||
## products
|
||
from products.models import (
|
||
Shangpin, ShangpinLeixing, ShangpinZhuanqu, Huiyuan,
|
||
DuociFenhong, Czjilu, Huiyuangoumai, Gsfenhong
|
||
)
|
||
|
||
## config
|
||
from config.models import (
|
||
WithdrawConfig, ShangjiaLianjie, AccountPermission,
|
||
TixianQuotaDefault,
|
||
DailyIncomeStat, DailyPayoutStat, PopupPage, PopupConfig, PopupImage
|
||
)
|
||
|
||
## rank
|
||
from rank.models import (
|
||
KaoheguanBankuai, Bankuai, Chenghao, KaoheCishuFeiyong,
|
||
YonghuChenghao, ShenheJilu, KaoheJiluFeiyong, KaoheJujueJilu
|
||
)
|
||
|
||
# 序列化器
|
||
from ..serializers import PopupPageSerializer
|
||
|
||
# 全局常量、日志对象
|
||
logger = logging.getLogger('houtai')
|
||
SHOP_PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd']
|
||
|
||
class CaiwuView(APIView):
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
|
||
if kefu_obj is None:
|
||
return permissions
|
||
|
||
if 'caiwu' not in permissions:
|
||
return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
|
||
|
||
from jituan.services.caiwu_stats import build_caiwu_payload
|
||
|
||
try:
|
||
data = build_caiwu_payload(request)
|
||
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||
except Exception:
|
||
logger.exception('CaiwuView 异常')
|
||
return Response({'code': 99, 'msg': '统计失败'}, status=500)
|
||
|
||
|
||
class CwhybkhqView(APIView):
|
||
"""
|
||
获取所有板块及对应会员列表
|
||
POST /houtai/cwhybkhq
|
||
Body: { "phone": "管理员手机号" }
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
|
||
if 'huiyuancaiwu' not in permissions:
|
||
return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
|
||
|
||
bankuai_list = []
|
||
for bk in Bankuai.query.all():
|
||
members = Huiyuan.query.filter(bankuai=bk).values('huiyuan_id', 'jieshao', 'bankuai_id')
|
||
bankuai_list.append({
|
||
'bankuai_id': bk.bankuai_id,
|
||
'mingcheng': bk.mingcheng,
|
||
'members': list(members)
|
||
})
|
||
|
||
return Response({'code': 0, 'msg': 'ok', 'data': bankuai_list})
|
||
except Exception as e:
|
||
logger.exception("CwhybkhqView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
|
||
class HybkjtsjView(APIView):
|
||
"""
|
||
会员详细统计数据
|
||
POST /houtai/hybkjtsj
|
||
Body: {
|
||
"phone": "管理员手机号",
|
||
"huiyuan_id": "会员ID",
|
||
"granularity": "day" | "month" | "year",
|
||
"year": 2026,
|
||
"month": 5,
|
||
"include_guanshi": true,
|
||
"include_zuzhang": true,
|
||
"summary_date": "2026-05-16"
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
|
||
if 'huiyuancaiwu' not in permissions:
|
||
return Response({'code': 403, 'msg': '无会员财务查看权限'}, status=403)
|
||
|
||
huiyuan_id = request.data.get('huiyuan_id')
|
||
granularity = request.data.get('granularity', 'day')
|
||
year = request.data.get('year')
|
||
month = request.data.get('month')
|
||
include_guanshi = request.data.get('include_guanshi', False)
|
||
include_zuzhang = request.data.get('include_zuzhang', False)
|
||
summary_date = request.data.get('summary_date')
|
||
|
||
if not huiyuan_id:
|
||
return Response({'code': 1, 'msg': '缺少会员ID'}, status=400)
|
||
|
||
# 🔧 直接使用 date.today() 避免模块名冲突
|
||
now = date.today()
|
||
|
||
# ---------- 构造时间范围 ----------
|
||
if granularity == 'day':
|
||
if not year or not month:
|
||
return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
|
||
start_date = date(year, month, 1)
|
||
if month == 12:
|
||
end_date = date(year + 1, 1, 1)
|
||
else:
|
||
end_date = date(year, month + 1, 1)
|
||
trunc_func = TruncDate('CreateTime')
|
||
if not summary_date:
|
||
summary_date = now.strftime('%Y-%m-%d')
|
||
summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
|
||
elif granularity == 'month':
|
||
if not year:
|
||
return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
|
||
start_date = date(year, 1, 1)
|
||
end_date = date(year + 1, 1, 1)
|
||
trunc_func = TruncMonth('CreateTime')
|
||
if not summary_date:
|
||
summary_date = now.strftime('%Y-%m')
|
||
summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
|
||
elif granularity == 'year':
|
||
year = year or now.year
|
||
start_date = date(year, 1, 1)
|
||
end_date = date(year + 1, 1, 1)
|
||
trunc_func = TruncYear('CreateTime')
|
||
if not summary_date:
|
||
summary_date = str(year)
|
||
summary_q = date(year, 1, 1)
|
||
else:
|
||
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
|
||
|
||
# 生成 naive datetime(避免 USE_TZ=False 报错)
|
||
start_dt = datetime.combine(start_date, datetime.min.time())
|
||
end_dt = datetime.combine(end_date, datetime.min.time())
|
||
|
||
# ---------- 充值过滤 ----------
|
||
chongzhi_filter = {
|
||
'huiyuan_id': huiyuan_id, # 会员ID筛选
|
||
'leixing': 1,
|
||
'zhuangtai': 3,
|
||
'CreateTime__gte': start_dt,
|
||
'CreateTime__lt': end_dt
|
||
}
|
||
|
||
# ---------- 分红过滤 ----------
|
||
fenhong_filter = {
|
||
'huiyuan_id': huiyuan_id, # 分红表会员ID筛选
|
||
'CreateTime__gte': start_dt,
|
||
'CreateTime__lt': end_dt
|
||
}
|
||
|
||
# 充值分组查询
|
||
chongzhi_qs = filter_queryset_by_club(
|
||
Czjilu.query.filter(**chongzhi_filter), request,
|
||
) \
|
||
.annotate(period=trunc_func) \
|
||
.values('period') \
|
||
.annotate(
|
||
chongzhi_count=Count('id'),
|
||
chongzhi_amount=Sum('jine')
|
||
).order_by('period')
|
||
|
||
# 分红分组查询
|
||
fenhong_qs = None
|
||
if include_guanshi or include_zuzhang:
|
||
fenhong_agg = {}
|
||
if include_guanshi:
|
||
fenhong_agg['guanshi_amount'] = Sum('fenhong')
|
||
if include_zuzhang:
|
||
fenhong_agg['zuzhang_amount'] = Sum('zuzhang_fenhong')
|
||
fenhong_qs = filter_gsfenhong_qs(
|
||
Gsfenhong.query.filter(**fenhong_filter), request,
|
||
) \
|
||
.annotate(period=trunc_func) \
|
||
.values('period') \
|
||
.annotate(**fenhong_agg) \
|
||
.order_by('period')
|
||
|
||
# 合并数据
|
||
data_map = {}
|
||
for item in chongzhi_qs:
|
||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||
item['period'].strftime('%Y')
|
||
data_map[period_str] = {
|
||
'chongzhi_count': item['chongzhi_count'],
|
||
'chongzhi_amount': float(item['chongzhi_amount'] or 0)
|
||
}
|
||
|
||
if fenhong_qs:
|
||
for item in fenhong_qs:
|
||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||
item['period'].strftime('%Y')
|
||
if period_str not in data_map:
|
||
data_map[period_str] = {'chongzhi_count': 0, 'chongzhi_amount': 0.0}
|
||
if include_guanshi:
|
||
data_map[period_str]['guanshi_amount'] = float(item.get('guanshi_amount') or 0)
|
||
if include_zuzhang:
|
||
data_map[period_str]['zuzhang_amount'] = float(item.get('zuzhang_amount') or 0)
|
||
|
||
time_series = []
|
||
for period_str, vals in data_map.items():
|
||
guanshi = vals.get('guanshi_amount', 0) if include_guanshi else 0
|
||
zuzhang = vals.get('zuzhang_amount', 0) if include_zuzhang else 0
|
||
vals['shouyi'] = round(vals['chongzhi_amount'] - guanshi - zuzhang, 2)
|
||
vals['date'] = period_str
|
||
time_series.append(vals)
|
||
time_series.sort(key=lambda x: x['date'])
|
||
|
||
# ---------- 汇总数据 ----------
|
||
summary_chongzhi_filter = chongzhi_filter.copy()
|
||
if granularity == 'day':
|
||
summary_chongzhi_filter['CreateTime__date'] = summary_q
|
||
elif granularity == 'month':
|
||
summary_chongzhi_filter['CreateTime__year'] = summary_q.year
|
||
summary_chongzhi_filter['CreateTime__month'] = summary_q.month
|
||
else:
|
||
summary_chongzhi_filter['CreateTime__year'] = summary_q.year
|
||
|
||
summary_chongzhi = Czjilu.query.filter(**summary_chongzhi_filter).aggregate(
|
||
total_count=Count('id'),
|
||
total_amount=Sum('jine')
|
||
)
|
||
total_chongzhi_count = summary_chongzhi['total_count'] or 0
|
||
total_chongzhi_amount = float(summary_chongzhi['total_amount'] or 0)
|
||
|
||
total_guanshi = 0.0
|
||
total_zuzhang = 0.0
|
||
if include_guanshi or include_zuzhang:
|
||
summary_fenhong_filter = fenhong_filter.copy()
|
||
if granularity == 'day':
|
||
summary_fenhong_filter['CreateTime__date'] = summary_q
|
||
elif granularity == 'month':
|
||
summary_fenhong_filter['CreateTime__year'] = summary_q.year
|
||
summary_fenhong_filter['CreateTime__month'] = summary_q.month
|
||
else:
|
||
summary_fenhong_filter['CreateTime__year'] = summary_q.year
|
||
|
||
fenhong_agg = {}
|
||
if include_guanshi:
|
||
fenhong_agg['guanshi_sum'] = Sum('fenhong')
|
||
if include_zuzhang:
|
||
fenhong_agg['zuzhang_sum'] = Sum('zuzhang_fenhong')
|
||
summary_fenhong = Gsfenhong.query.filter(**summary_fenhong_filter).aggregate(**fenhong_agg)
|
||
total_guanshi = float(summary_fenhong.get('guanshi_sum') or 0)
|
||
total_zuzhang = float(summary_fenhong.get('zuzhang_sum') or 0)
|
||
|
||
total_shouyi = round(total_chongzhi_amount - total_guanshi - total_zuzhang, 2)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': {
|
||
'time_series': time_series,
|
||
'summary': {
|
||
'total_chongzhi_count': total_chongzhi_count,
|
||
'total_chongzhi_amount': total_chongzhi_amount,
|
||
'total_guanshi_fenhong': total_guanshi,
|
||
'total_zuzhang_fenhong': total_zuzhang,
|
||
'total_shouyi': total_shouyi
|
||
}
|
||
}
|
||
})
|
||
except Exception as e:
|
||
logger.exception("HybkjtsjView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
|
||
|
||
|
||
|
||
class SzxxView(APIView):
|
||
"""
|
||
每日收支详细统计数据
|
||
POST /houtai/szxx
|
||
只读 daily_income_stat / daily_payout_stat,按 date 字段汇总。
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
from jituan.services.szxx_stats import build_szxx_payload
|
||
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
|
||
if 'caiwu' not in permissions:
|
||
return Response({'code': 403, 'msg': '无财务查看权限'}, status=403)
|
||
|
||
granularity = request.data.get('granularity', 'day')
|
||
year = request.data.get('year')
|
||
month = request.data.get('month')
|
||
summary_date = request.data.get('summary_date')
|
||
|
||
data, err = build_szxx_payload(request, granularity, year, month, summary_date)
|
||
if err:
|
||
return Response({'code': 1, 'msg': err}, status=400)
|
||
|
||
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||
|
||
except Exception as e:
|
||
logger.exception("SzxxView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class CwddhqlxView(APIView):
|
||
"""
|
||
获取订单类型(商品类型)用于筛选
|
||
POST /houtai/cwddhqlx
|
||
Body: { "phone": "管理员手机号" }
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
|
||
if 'dingdancaiwu' not in permissions:
|
||
return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
|
||
|
||
# 获取非审核状态(shenhezhuangtai=1)的商品类型,只返回ID和名称
|
||
types = ShangpinLeixing.query.filter(shenhezhuangtai=1).values('id', 'jieshao')
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': list(types)
|
||
})
|
||
except Exception as e:
|
||
logger.exception("CwddhqlxView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
|
||
|
||
|
||
class CwhqjtddsjView(APIView):
|
||
"""
|
||
订单详细统计数据(修正版)
|
||
POST /houtai/cwhqjtddsj
|
||
Body: {
|
||
"phone": "管理员手机号",
|
||
"granularity": "day" | "month" | "year",
|
||
"year": 2026,
|
||
"month": 5,
|
||
"type_id": 1, # 商品类型ID,0表示全部
|
||
"order_source": 1, # 1=平台单,2=商家单,0=全部
|
||
"summary_date": "2026-05-16"
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
if 'dingdancaiwu' not in permissions:
|
||
return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
|
||
|
||
granularity = request.data.get('granularity', 'day')
|
||
year = request.data.get('year')
|
||
month = request.data.get('month')
|
||
type_id = request.data.get('type_id', 0)
|
||
order_source = request.data.get('order_source', 0)
|
||
summary_date = request.data.get('summary_date')
|
||
|
||
now = date.today()
|
||
|
||
# ---------- 时间范围 ----------
|
||
if granularity == 'day':
|
||
if not year or not month:
|
||
return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
|
||
start_date = date(year, month, 1)
|
||
end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
|
||
trunc_func = TruncDate('CreateTime')
|
||
if not summary_date:
|
||
summary_date = now.strftime('%Y-%m-%d')
|
||
summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
|
||
elif granularity == 'month':
|
||
if not year:
|
||
return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
|
||
start_date = date(year, 1, 1)
|
||
end_date = date(year + 1, 1, 1)
|
||
trunc_func = TruncMonth('CreateTime')
|
||
if not summary_date:
|
||
summary_date = now.strftime('%Y-%m')
|
||
summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
|
||
elif granularity == 'year':
|
||
year = year or now.year
|
||
start_date = date(year, 1, 1)
|
||
end_date = date(year + 1, 1, 1)
|
||
trunc_func = TruncYear('CreateTime')
|
||
if not summary_date:
|
||
summary_date = str(year)
|
||
summary_q = date(year, 1, 1)
|
||
else:
|
||
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
|
||
|
||
start_dt = datetime.combine(start_date, datetime.min.time())
|
||
end_dt = datetime.combine(end_date, datetime.min.time())
|
||
|
||
# ---------- 基础过滤 ----------
|
||
filters = {
|
||
'CreateTime__gte': start_dt,
|
||
'CreateTime__lt': end_dt,
|
||
'Status__in': [1, 2, 3, 4, 5, 6, 7, 8] # 只统计有效订单状态
|
||
}
|
||
if int(type_id) != 0:
|
||
filters['ProductTypeID'] = type_id
|
||
if int(order_source) in [1, 2]:
|
||
filters['Platform'] = order_source
|
||
|
||
# ---------- 时间序列分组查询 ----------
|
||
order_base = filter_queryset_by_club(
|
||
Order.query.filter(**filters), request, club_field='ClubID',
|
||
)
|
||
qs = order_base \
|
||
.annotate(period=trunc_func) \
|
||
.values('period') \
|
||
.annotate(
|
||
# 订单总量(所有符合条件的状态)
|
||
order_count=Count('id'),
|
||
# 订单总额
|
||
order_amount=Sum('Amount'),
|
||
# 成交单量 (Status=3)
|
||
success_count=Count('id', filter=Q(Status=3)),
|
||
# 成交订单总额
|
||
success_amount=Sum('Amount', filter=Q(Status=3)),
|
||
# 退款单量 (Status=5)
|
||
refund_count=Count('id', filter=Q(Status=5)),
|
||
# 打手分成总额(仅成交订单)
|
||
PlayerCommission_sum=Sum('PlayerCommission', filter=Q(Status=3)),
|
||
# 平台成交订单的店铺分红
|
||
dianpu_fenhong_sum=Sum('pingtai_kuozhan__ShopIncome',
|
||
filter=Q(Status=3, Platform=1)),
|
||
# 成交订单中:平台单金额、商家单金额、各自打手分成
|
||
platform_success_amount=Sum('Amount', filter=Q(Status=3, Platform=1)),
|
||
merchant_success_amount=Sum('Amount', filter=Q(Status=3, Platform=2)),
|
||
platform_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)),
|
||
merchant_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)),
|
||
).order_by('period')
|
||
|
||
time_series = []
|
||
for item in qs:
|
||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||
item['period'].strftime('%Y')
|
||
|
||
# 平台收益计算
|
||
platform_profit = (item['platform_success_amount'] or 0) \
|
||
- (item['platform_dashou'] or 0) \
|
||
- (item['dianpu_fenhong_sum'] or 0)
|
||
merchant_profit = (item['merchant_success_amount'] or 0) \
|
||
- (item['merchant_dashou'] or 0)
|
||
total_profit = platform_profit + merchant_profit
|
||
|
||
time_series.append({
|
||
'date': period_str,
|
||
'order_count': item['order_count'],
|
||
'order_amount': float(item['order_amount'] or 0),
|
||
'success_count': item['success_count'],
|
||
'success_amount': float(item['success_amount'] or 0),
|
||
'refund_count': item['refund_count'],
|
||
'dashou_fencheng': float(item['PlayerCommission_sum'] or 0),
|
||
'dianpu_fenhong': float(item['dianpu_fenhong_sum'] or 0),
|
||
'profit': round(total_profit, 2)
|
||
})
|
||
|
||
# ---------- 汇总数据 ----------
|
||
summary_qs = filter_queryset_by_club(
|
||
Order.query.filter(**filters), request, club_field='ClubID',
|
||
)
|
||
total_order_count = summary_qs.count()
|
||
total_order_amount = summary_qs.aggregate(s=Sum('Amount'))['s'] or 0
|
||
total_success_count = summary_qs.filter(Status=3).count()
|
||
total_success_amount = summary_qs.filter(Status=3).aggregate(s=Sum('Amount'))['s'] or 0
|
||
total_refund_count = summary_qs.filter(Status=5).count()
|
||
total_dashou = summary_qs.filter(Status=3).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
||
total_dianpu_fenhong = summary_qs.filter(Status=3, Platform=1).aggregate(
|
||
s=Sum('pingtai_kuozhan__ShopIncome'))['s'] or 0
|
||
|
||
platform_amount = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('Amount'))['s'] or 0
|
||
merchant_amount = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('Amount'))['s'] or 0
|
||
platform_dashou_sum = \
|
||
summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
||
merchant_dashou_sum = \
|
||
summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
||
|
||
profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong
|
||
profit_merchant = merchant_amount - merchant_dashou_sum
|
||
total_profit = profit_platform + profit_merchant
|
||
|
||
summary = {
|
||
'total_order_count': total_order_count,
|
||
'total_order_amount': round(float(total_order_amount), 2),
|
||
'total_success_count': total_success_count,
|
||
'total_success_amount': round(float(total_success_amount), 2),
|
||
'total_refund_count': total_refund_count,
|
||
'total_PlayerCommission': round(float(total_dashou), 2),
|
||
'total_dianpu_fenhong': round(float(total_dianpu_fenhong), 2),
|
||
'total_profit': round(total_profit, 2)
|
||
}
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': {
|
||
'time_series': time_series,
|
||
'summary': summary
|
||
}
|
||
})
|
||
except Exception as e:
|
||
logger.exception("CwhqjtddsjView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
class CwqtczhqView(APIView):
|
||
"""
|
||
其他充值/罚款数据统计(非会员充值)
|
||
POST /houtai/cwqtczhq
|
||
Body: {
|
||
"phone": "管理员手机号",
|
||
"granularity": "day" | "month" | "year",
|
||
"year": 2026,
|
||
"month": 5, // day时必填
|
||
"types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款
|
||
"summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY)
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
if 'qtczcaiwu' not in permissions:
|
||
return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403)
|
||
|
||
from jituan.services.finance_detail_stats import build_chongzhi_finance_payload
|
||
|
||
granularity = request.data.get('granularity', 'day')
|
||
year = request.data.get('year')
|
||
month = request.data.get('month')
|
||
types = request.data.get('types', [2, 3, 4, 5, 6])
|
||
summary_date = request.data.get('summary_date')
|
||
|
||
if isinstance(types, list) and len(types) == 0:
|
||
types = [2, 3, 4, 5, 6]
|
||
|
||
data, err = build_chongzhi_finance_payload(
|
||
request, granularity, year, month, types, summary_date,
|
||
)
|
||
if err:
|
||
return Response({'code': 1, 'msg': err}, status=400)
|
||
|
||
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||
except Exception as e:
|
||
logger.exception("CwqtczhqView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||
|
||
'''class CwqtczhqView_LEGACY(APIView):
|
||
"""
|
||
其他充值/罚款数据统计(非会员充值)
|
||
POST /houtai/cwqtczhq
|
||
Body: {
|
||
"phone": "管理员手机号",
|
||
"granularity": "day" | "month" | "year",
|
||
"year": 2026,
|
||
"month": 5, // day时必填
|
||
"types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款
|
||
"summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY)
|
||
}
|
||
"""
|
||
permission_classes = [IsAuthenticated]
|
||
|
||
def post(self, request):
|
||
try:
|
||
username_from_frontend = request.data.get('phone', None)
|
||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||
if kefu_obj is None:
|
||
return permissions
|
||
if 'qtczcaiwu' not in permissions:
|
||
return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403)
|
||
|
||
granularity = request.data.get('granularity', 'day')
|
||
year = request.data.get('year')
|
||
month = request.data.get('month')
|
||
types = request.data.get('types', [2, 3, 4, 5, 6]) # 默认全部
|
||
summary_date = request.data.get('summary_date')
|
||
|
||
if isinstance(types, list) and len(types) == 0:
|
||
types = [2, 3, 4, 5, 6]
|
||
|
||
now = date.today()
|
||
|
||
# ---------- 时间范围(用于曲线图) ----------
|
||
if granularity == 'day':
|
||
if not year or not month:
|
||
return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
|
||
start_date = date(year, month, 1)
|
||
end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
|
||
trunc_func = TruncDate('CreateTime')
|
||
# 默认汇总日期
|
||
if not summary_date:
|
||
summary_date = now.strftime('%Y-%m-%d')
|
||
summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
|
||
elif granularity == 'month':
|
||
if not year:
|
||
return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
|
||
start_date = date(year, 1, 1)
|
||
end_date = date(year + 1, 1, 1)
|
||
trunc_func = TruncMonth('CreateTime')
|
||
if not summary_date:
|
||
summary_date = now.strftime('%Y-%m')
|
||
summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
|
||
elif granularity == 'year':
|
||
year = year or now.year
|
||
start_date = date(year, 1, 1)
|
||
end_date = date(year + 1, 1, 1)
|
||
trunc_func = TruncYear('CreateTime')
|
||
if not summary_date:
|
||
summary_date = str(year)
|
||
summary_q = date(year, 1, 1)
|
||
else:
|
||
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
|
||
|
||
start_dt = datetime.combine(start_date, datetime.min.time())
|
||
end_dt = datetime.combine(end_date, datetime.min.time())
|
||
|
||
# ---------- 构建时间序列 ----------
|
||
time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} }
|
||
|
||
def ensure_date(ds):
|
||
if ds not in time_series_map:
|
||
time_series_map[ds] = {}
|
||
return time_series_map[ds]
|
||
|
||
# 1. 处理充值记录(Czjilu)
|
||
cz_types = [t for t in types if t in [2, 3, 4, 5]]
|
||
if cz_types:
|
||
cz_qs = Czjilu.query.filter(
|
||
CreateTime__gte=start_dt,
|
||
CreateTime__lt=end_dt,
|
||
leixing__in=cz_types,
|
||
zhuangtai=3 # 只统计支付成功的
|
||
).annotate(
|
||
period=trunc_func
|
||
).values('period', 'leixing').annotate(
|
||
total_count=Count('id'),
|
||
total_amount=Sum('jine')
|
||
).order_by('period', 'leixing')
|
||
|
||
for item in cz_qs:
|
||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||
item['period'].strftime('%Y')
|
||
lx = item['leixing']
|
||
amount = float(item['total_amount'] or 0)
|
||
count = item['total_count']
|
||
# 收益:积分(3)收益为金额本身,其他为0
|
||
profit = amount if lx == 3 else 0.0
|
||
|
||
day_data = ensure_date(period_str)
|
||
day_data[str(lx)] = {
|
||
'count': count,
|
||
'amount': amount,
|
||
'profit': profit,
|
||
'fenhong_count': 0, # 充值类型无分红
|
||
'fenhong_amount': 0.0
|
||
}
|
||
|
||
# 2. 处理罚款(Penalty)—— 包含分红
|
||
if 6 in types:
|
||
# 罚款记录
|
||
penalty_qs = Penalty.query.filter(
|
||
CreateTime__gte=start_dt,
|
||
CreateTime__lt=end_dt,
|
||
Status=2 # 已缴纳
|
||
).annotate(
|
||
period=trunc_func
|
||
).values('period').annotate(
|
||
penalty_count=Count('id'),
|
||
penalty_amount=Sum('FineAmount')
|
||
).order_by('period')
|
||
|
||
# 分红记录
|
||
fenhong_qs = PenaltyBonus.query.filter(
|
||
CreateTime__gte=start_dt,
|
||
CreateTime__lt=end_dt
|
||
).annotate(
|
||
period=trunc_func
|
||
).values('period').annotate(
|
||
fh_count=Count('id'),
|
||
fh_amount=Sum('WithdrawableAmount')
|
||
).order_by('period')
|
||
|
||
# 合并
|
||
for item in penalty_qs:
|
||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||
item['period'].strftime('%Y')
|
||
day_data = ensure_date(period_str)
|
||
day_data['6'] = {
|
||
'count': item['penalty_count'],
|
||
'amount': float(item['penalty_amount'] or 0),
|
||
'profit': 0.0, # 先设为0,后续计算
|
||
'fenhong_count': 0,
|
||
'fenhong_amount': 0.0
|
||
}
|
||
|
||
for item in fenhong_qs:
|
||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||
item['period'].strftime('%Y')
|
||
day_data = ensure_date(period_str)
|
||
if '6' not in day_data:
|
||
day_data['6'] = {'count': 0, 'amount': 0.0, 'profit': 0.0, 'fenhong_count': 0, 'fenhong_amount': 0.0}
|
||
day_data['6']['fenhong_count'] += item['fh_count']
|
||
day_data['6']['fenhong_amount'] += float(item['fh_amount'] or 0)
|
||
|
||
# 计算罚款收益 = 罚款金额 - 分红金额
|
||
for period_str, day_data in time_series_map.items():
|
||
if '6' in day_data:
|
||
penalty = day_data['6']
|
||
penalty['profit'] = round(penalty['amount'] - penalty['fenhong_amount'], 2)
|
||
|
||
# 构造有序时间序列列表
|
||
time_series = []
|
||
for period_str in sorted(time_series_map.keys()):
|
||
entry = {'date': period_str, 'types': {}}
|
||
for type_code in sorted(time_series_map[period_str].keys()):
|
||
entry['types'][type_code] = time_series_map[period_str][type_code]
|
||
time_series.append(entry)
|
||
|
||
# ---------- 汇总指定日期 ----------
|
||
summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0}
|
||
|
||
# 汇总充值类型
|
||
for lx in cz_types:
|
||
filter_kwargs = {
|
||
'leixing': lx,
|
||
'zhuangtai': 3,
|
||
}
|
||
if granularity == 'day':
|
||
filter_kwargs['CreateTime__date'] = summary_q
|
||
elif granularity == 'month':
|
||
filter_kwargs['CreateTime__year'] = summary_q.year
|
||
filter_kwargs['CreateTime__month'] = summary_q.month
|
||
else:
|
||
filter_kwargs['CreateTime__year'] = summary_q.year
|
||
agg = filter_queryset_by_club(
|
||
Czjilu.query.filter(**filter_kwargs), request,
|
||
).aggregate(count=Count('id'), amount=Sum('jine'))
|
||
count = agg['count'] or 0
|
||
amount = float(agg['amount'] or 0)
|
||
profit = amount if lx == 3 else 0.0
|
||
summary_result['by_type'][str(lx)] = {
|
||
'count': count,
|
||
'amount': round(amount, 2),
|
||
'profit': round(profit, 2),
|
||
'fenhong_count': 0,
|
||
'fenhong_amount': 0.0
|
||
}
|
||
summary_result['total_count'] += count
|
||
summary_result['total_amount'] += amount
|
||
summary_result['total_profit'] += profit
|
||
|
||
# 汇总罚款
|
||
if 6 in types:
|
||
penalty_filter = {
|
||
'Status': 2,
|
||
}
|
||
fenhong_filter = {}
|
||
if granularity == 'day':
|
||
penalty_filter['CreateTime__date'] = summary_q
|
||
fenhong_filter['CreateTime__date'] = summary_q
|
||
elif granularity == 'month':
|
||
penalty_filter['CreateTime__year'] = summary_q.year
|
||
penalty_filter['CreateTime__month'] = summary_q.month
|
||
fenhong_filter['CreateTime__year'] = summary_q.year
|
||
fenhong_filter['CreateTime__month'] = summary_q.month
|
||
else:
|
||
penalty_filter['CreateTime__year'] = summary_q.year
|
||
fenhong_filter['CreateTime__year'] = summary_q.year
|
||
|
||
penalty_agg = Penalty.query.filter(**penalty_filter).aggregate(
|
||
count=Count('id'), amount=Sum('FineAmount'))
|
||
penalty_count = penalty_agg['count'] or 0
|
||
penalty_amount = float(penalty_agg['amount'] or 0)
|
||
|
||
fenhong_agg = PenaltyBonus.query.filter(**fenhong_filter).aggregate(
|
||
fh_count=Count('id'), fh_amount=Sum('WithdrawableAmount'))
|
||
fh_count = fenhong_agg['fh_count'] or 0
|
||
fh_amount = float(fenhong_agg['fh_amount'] or 0)
|
||
|
||
profit = round(penalty_amount - fh_amount, 2)
|
||
summary_result['by_type']['6'] = {
|
||
'count': penalty_count,
|
||
'amount': round(penalty_amount, 2),
|
||
'profit': profit,
|
||
'fenhong_count': fh_count,
|
||
'fenhong_amount': round(fh_amount, 2)
|
||
}
|
||
summary_result['total_count'] += penalty_count
|
||
summary_result['total_amount'] += penalty_amount
|
||
summary_result['total_profit'] += profit
|
||
|
||
summary_result['total_amount'] = round(summary_result['total_amount'], 2)
|
||
summary_result['total_profit'] = round(summary_result['total_profit'], 2)
|
||
|
||
return Response({
|
||
'code': 0,
|
||
'msg': 'ok',
|
||
'data': {
|
||
'time_series': time_series,
|
||
'summary': summary_result
|
||
}
|
||
})
|
||
except Exception as e:
|
||
logger.exception("CwqtczhqView 接口错误")
|
||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)'''
|
||
|
||
|
||
|
||
|
||
|
||
# ==================== 后端接口 ====================
|
||
# 文件:views.py (添加以下两个视图类)
|
||
|
||
|
||
|