Files
Django/backend/views/finance.py
TermiNexus f1c8633345 perf: P1 性能优化批量修复 + kefu 视图拆分
P1 性能优化(避免循环内 N+1 / N 次 DB 查询):
- shop_order_views._batch_images: 移除 ThreadPoolExecutor 掩盖的 N 次查询,改单次批量 + Python 侧分组
- product_query.DashouHuiyuanList: 5N 查询 -> 3 次批量预取 + 本地闭包判断
- roles.GetRolePermissionView / 用户列表: 循环内 RolePermission/Permission/UserRole/Role -> 批量 __in 预取
- guanli / zuzhang 杜次分红 4.2/4.3: 循环内 update_or_create + 单条 delete -> bulk_create + bulk_update + 批量 delete
- admin_config QQ 群配置: 循环内 exists/first/save/create -> 批量预取 + bulk_create + bulk_update
- jituan 服务层多处合并统计查询、批量预取 map
- rank 多处循环 N+1 改批量预取
- backend 多处循环内 count/create 改批量
- config/orders/merchant_ops 多处循环 N+1 改批量预取

其他改动:
- users/views/kefu.py 拆分为 kefu_base/kefu_dashou/kefu_orders/kefu_punishment/kefu_withdraw 5 个文件
- 删除遗留脚本 check_prod_uid.py / create_rbac_tables.sql
2026-07-05 23:17:38 +08:00

984 lines
42 KiB
Python
Raw Permalink 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.
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 collections import defaultdict
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)
all_bankuai = list(Bankuai.query.all())
bankuai_ids = [bk.bankuai_id for bk in all_bankuai]
all_members = Huiyuan.query.filter(bankuai_id__in=bankuai_ids).values('huiyuan_id', 'jieshao', 'bankuai_id')
members_by_bankuai = defaultdict(list)
for m in all_members:
members_by_bankuai[m['bankuai_id']].append(dict(m))
bankuai_list = []
for bk in all_bankuai:
bankuai_list.append({
'bankuai_id': bk.bankuai_id,
'mingcheng': bk.mingcheng,
'members': members_by_bankuai.get(bk.bankuai_id, [])
})
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, # 商品类型ID0表示全部
"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',
)
agg = summary_qs.aggregate(
total_order_count=Count('id'),
total_order_amount=Sum('Amount'),
total_success_count=Count('id', filter=Q(Status=3)),
total_success_amount=Sum('Amount', filter=Q(Status=3)),
total_refund_count=Count('id', filter=Q(Status=5)),
total_dashou=Sum('PlayerCommission', filter=Q(Status=3)),
total_dianpu_fenhong=Sum('pingtai_kuozhan__ShopIncome', filter=Q(Status=3, Platform=1)),
platform_amount=Sum('Amount', filter=Q(Status=3, Platform=1)),
merchant_amount=Sum('Amount', filter=Q(Status=3, Platform=2)),
platform_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)),
merchant_dashou_sum=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)),
)
total_order_count = agg['total_order_count'] or 0
total_order_amount = agg['total_order_amount'] or 0
total_success_count = agg['total_success_count'] or 0
total_success_amount = agg['total_success_amount'] or 0
total_refund_count = agg['total_refund_count'] or 0
total_dashou = agg['total_dashou'] or 0
total_dianpu_fenhong = agg['total_dianpu_fenhong'] or 0
platform_amount = agg['platform_amount'] or 0
merchant_amount = agg['merchant_amount'] or 0
platform_dashou_sum = agg['platform_dashou_sum'] or 0
merchant_dashou_sum = agg['merchant_dashou_sum'] 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 (添加以下两个视图类)