Files
Django/backend/views/exams.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

755 lines
30 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.
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']
# 审核记录状态映射(原位于 zxkf.py按功能组归类到考核模块
SHENHE_ZHUANGTAI_MAP = {
0: '考核中',
1: '已通过',
2: '未通过',
3: '待审核',
}
class KhgglView(APIView):
"""
考核官管理数据接口(列表、筛选、分页)
POST /houtai/khggl
Body: {
"phone": "管理员手机号",
"page": 1, // 可选默认1
"page_size": 15, // 可选默认15
"yonghuid": "1234567", // 按用户ID筛选可选
"zhuangtai": 1, // 账号状态 0=禁用,1=正常,可选
"shenhe_zhuangtai": 0, // 审核官状态 0=空闲,1=审核中,可选
"bankuai_name": "王者" // 板块名称模糊筛选,可选
}
"""
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 'kaohepeizhi' not in permissions:
return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
# 分页参数
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 15))
if page < 1: page = 1
if page_size < 1: page_size = 15
# 筛选条件
filter_yonghuid = request.data.get('yonghuid', '').strip()
filter_zhuangtai = request.data.get('zhuangtai')
filter_shenhe_zhuangtai = request.data.get('shenhe_zhuangtai')
filter_bankuai_name = request.data.get('bankuai_name', '').strip()
# 基础查询:所有审核官,预加载用户及打手扩展表
queryset = filter_user_related_by_club(
UserShenheguan.query.select_related('user__DashouProfile'),
request,
)
if filter_yonghuid:
queryset = queryset.filter(user__UserUID=filter_yonghuid)
if filter_zhuangtai is not None:
queryset = queryset.filter(zhuangtai=filter_zhuangtai)
if filter_shenhe_zhuangtai is not None:
queryset = queryset.filter(shenhe_zhuangtai=filter_shenhe_zhuangtai)
# 如果填写了板块名称,筛选出关联该板块的审核官
if filter_bankuai_name:
bankuai_ids = Bankuai.query.filter(
mingcheng__icontains=filter_bankuai_name
).values_list('bankuai_id', flat=True)
shenheguan_ids = KaoheguanBankuai.query.filter(
bankuai_id__in=bankuai_ids
).values_list('kaoheguan_id', flat=True)
queryset = queryset.filter(user__UserUID__in=shenheguan_ids)
total = queryset.count()
start = (page - 1) * page_size
end = start + page_size
shenheguan_list = queryset.order_by('-CreateTime')[start:end]
from collections import defaultdict
from jituan.services.shenhe_club import yonghu_chenghao_qs_for_request
# ---------- 优化板块打手数量1 次 annotate 替代 2N 次循环查询)----------
# 原逻辑:每板块查 dashou 标签 ID再统计关联的不同用户数要求有 DashouProfile
bankuai_dashou_qs = yonghu_chenghao_qs_for_request(
YonghuChenghao.query.filter(
chenghao__leixing='dashou',
yonghu__DashouProfile__isnull=False,
),
request,
)
bankuai_dashou_count_map = {
item['chenghao__bankuai_id']: item['user_count']
for item in bankuai_dashou_qs.values('chenghao__bankuai_id').annotate(
user_count=Count('yonghu', distinct=True)
)
}
# ---------- 优化板块标签统计1 次 annotate 替代 N*M 次循环查询)----------
# 原逻辑:每板块每标签分别统计关联的不同用户数(不要求 DashouProfile
tag_counts_qs = yonghu_chenghao_qs_for_request(
YonghuChenghao.query.filter(chenghao__leixing='dashou'),
request,
).values(
'chenghao_id', 'chenghao__bankuai_id', 'chenghao__mingcheng'
).annotate(user_count=Count('yonghu', distinct=True))
tags_by_bankuai = defaultdict(list)
for item in tag_counts_qs:
tags_by_bankuai[item['chenghao__bankuai_id']].append({
'id': item['chenghao_id'],
'name': item['chenghao__mingcheng'],
'user_count': item['user_count'],
})
all_banquai = Bankuai.query.all()
bankuai_tags_data = [
{
'bankuai_id': bk.bankuai_id,
'mingcheng': bk.mingcheng,
'tags': tags_by_bankuai.get(bk.bankuai_id, []),
}
for bk in all_banquai
]
# ---------- 优化审核官板块关联1 次批量查询替代 P 次循环查询)----------
shenheguan_user_uids = [sg.user.UserUID for sg in shenheguan_list]
all_kgs = KaoheguanBankuai.query.filter(
kaoheguan_id__in=shenheguan_user_uids
).select_related('bankuai')
kg_map = defaultdict(list)
for kg in all_kgs:
kg_map[kg.kaoheguan_id].append(kg)
# 构造返回数据(审核官列表)
data_list = []
for sg in shenheguan_list:
user = sg.user
bankuai_info = [
{
'bankuai_id': kg.bankuai.bankuai_id,
'mingcheng': kg.bankuai.mingcheng,
'dashou_count': bankuai_dashou_count_map.get(kg.bankuai.bankuai_id, 0),
}
for kg in kg_map.get(user.UserUID, [])
]
dashou_nick = user.DashouProfile.nicheng if hasattr(user, 'DashouProfile') and user.DashouProfile else ''
data_list.append({
'yonghuid': user.UserUID,
'avatar': user.Avatar or '',
'phone': user.Phone or '',
'zhuangtai': sg.zhuangtai,
'shenhe_zhuangtai': sg.shenhe_zhuangtai,
'shenhe_zongshu': sg.shenhe_zongshu,
'tongguo_zongshu': sg.tongguo_zongshu,
'yue': float(sg.yue),
'zonge': float(sg.zonge),
'CreateTime': sg.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if sg.CreateTime else '',
'UpdateTime': sg.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if sg.UpdateTime else '',
'bankuai': bankuai_info,
'nicheng': dashou_nick,
'is_renzheng': sg.is_renzheng,
})
all_bankuai = list(Bankuai.query.values('bankuai_id', 'mingcheng'))
return Response({
'code': 0,
'msg': 'ok',
'data': {
'total': total,
'page': page,
'page_size': page_size,
'list': data_list,
'all_bankuai': all_bankuai,
'bankuai_tags': bankuai_tags_data, # 新增板块标签统计
}
})
except Exception as e:
logger.exception("KhgglView 接口错误")
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
class ShgxgsjView(APIView):
"""
审核官信息修改(余额、状态、板块关联)
POST /houtai/shgxgsj
"""
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 'kaohepeizhi' not in permissions:
return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
yonghuid = request.data.get('yonghuid')
if not yonghuid:
return Response({'code': 1, 'msg': '缺少用户ID'}, status=400)
try:
shenheguan = UserShenheguan.query.select_related('user').get(user__UserUID=yonghuid)
except UserShenheguan.DoesNotExist:
return Response({'code': 1, 'msg': '审核官不存在'}, status=404)
from jituan.services.shenhe_club import user_belongs_to_request_club
if not user_belongs_to_request_club(shenheguan.user, request):
return Response({'code': 403, 'msg': '该考核官不属于当前俱乐部'}, status=403)
action = request.data.get('action')
if action == 'update_info':
if 'yue' in request.data:
shenheguan.yue = float(request.data.get('yue', 0))
if 'zhuangtai' in request.data:
zt = int(request.data.get('zhuangtai'))
if zt not in [0, 1]:
return Response({'code': 1, 'msg': '无效的账号状态'}, status=400)
shenheguan.zhuangtai = zt
if 'shenhe_zhuangtai' in request.data:
szt = int(request.data.get('shenhe_zhuangtai'))
if szt not in [0, 1]:
return Response({'code': 1, 'msg': '无效的审核官状态'}, status=400)
shenheguan.shenhe_zhuangtai = szt
# 新增认证状态编辑
if 'is_renzheng' in request.data:
shenheguan.is_renzheng = bool(request.data.get('is_renzheng'))
shenheguan.save()
return Response({'code': 0, 'msg': '修改成功'})
elif action == 'add_bankuai':
bankuai_id = request.data.get('bankuai_id')
if not bankuai_id:
return Response({'code': 1, 'msg': '缺少板块ID'}, status=400)
if not Bankuai.query.filter(bankuai_id=bankuai_id).exists():
return Response({'code': 1, 'msg': '板块不存在'}, status=400)
if KaoheguanBankuai.query.filter(kaoheguan_id=yonghuid, bankuai_id=bankuai_id).exists():
return Response({'code': 1, 'msg': '已关联该板块'}, status=400)
KaoheguanBankuai.query.create(kaoheguan_id=yonghuid, bankuai_id=bankuai_id)
return Response({'code': 0, 'msg': '板块关联成功'})
elif action == 'remove_bankuai':
bankuai_id = request.data.get('bankuai_id')
if not bankuai_id:
return Response({'code': 1, 'msg': '缺少板块ID'}, status=400)
deleted, _ = KaoheguanBankuai.query.filter(
kaoheguan_id=yonghuid, bankuai_id=bankuai_id
).delete()
if not deleted:
return Response({'code': 1, 'msg': '未找到该板块关联'}, status=400)
return Response({'code': 0, 'msg': '板块移除成功'})
else:
return Response({'code': 1, 'msg': '未知操作'}, status=400)
except Exception as e:
logger.exception("ShgxgsjView 接口错误")
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
def _batch_user_profile_map(user_uids):
"""批量获取用户头像、昵称、手机号"""
uids = list({u for u in user_uids if u})
if not uids:
return {}
users = User.query.filter(UserUID__in=uids)
dashou_nicks = {
d.user.UserUID: d.nicheng
for d in UserDashou.query.filter(user__UserUID__in=uids).select_related('user')
}
result = {}
for u in users:
result[u.UserUID] = {
'yonghuid': u.UserUID,
'avatar': u.Avatar or '',
'phone': u.Phone or '',
'nicheng': dashou_nicks.get(u.UserUID) or u.UserName or u.Phone or u.UserUID,
}
return result
def _build_shenhe_jilu_filters(request_data):
"""构造 ShenheJilu 查询条件(列表与统计共用)"""
qs = ShenheJilu.query.select_related('bankuai', 'chenghao')
jilu_id = (request_data.get('jilu_id') or '').strip()
if jilu_id:
qs = qs.filter(jilu_id=jilu_id)
zhuangtai = request_data.get('zhuangtai')
if zhuangtai is not None and zhuangtai != '':
qs = qs.filter(zhuangtai=int(zhuangtai))
assessed = request_data.get('assessed')
if assessed == 'done':
qs = qs.filter(zhuangtai__in=[1, 2])
elif assessed == 'undone':
qs = qs.filter(zhuangtai__in=[0, 3])
bankuai_id = request_data.get('bankuai_id')
if bankuai_id not in (None, ''):
qs = qs.filter(bankuai_id=int(bankuai_id))
chenghao_id = request_data.get('chenghao_id')
if chenghao_id not in (None, ''):
qs = qs.filter(chenghao_id=int(chenghao_id))
cishu = request_data.get('cishu')
if cishu not in (None, ''):
qs = qs.filter(cishu=int(cishu))
dashou_kw = (request_data.get('dashou_keyword') or '').strip()
if dashou_kw:
dashou_uids = list(
User.query.filter(
Q(UserUID=dashou_kw) |
Q(UserName__icontains=dashou_kw) |
Q(Phone__icontains=dashou_kw) |
Q(DashouProfile__nicheng__icontains=dashou_kw)
).values_list('UserUID', flat=True)
)
if dashou_uids:
qs = qs.filter(shenqingren_id__in=dashou_uids)
else:
qs = qs.none()
kg_kw = (request_data.get('kaoheguan_keyword') or '').strip()
if kg_kw:
kg_uids = list(
User.query.filter(
Q(UserUID=kg_kw) |
Q(UserName__icontains=kg_kw) |
Q(Phone__icontains=kg_kw) |
Q(DashouProfile__nicheng__icontains=kg_kw)
).values_list('UserUID', flat=True)
)
if kg_uids:
qs = qs.filter(shenheguan_id__in=kg_uids)
else:
qs = qs.none()
start_time = (request_data.get('start_time') or '').strip()
end_time = (request_data.get('end_time') or '').strip()
if start_time:
qs = qs.filter(CreateTime__gte=start_time)
if end_time:
qs = qs.filter(CreateTime__lte=end_time)
has_kaoheguan = request_data.get('has_kaoheguan')
if has_kaoheguan in (True, 'true', '1', 1):
qs = qs.exclude(shenheguan_id__isnull=True).exclude(shenheguan_id='')
elif has_kaoheguan in (False, 'false', '0', 0):
qs = qs.filter(Q(shenheguan_id__isnull=True) | Q(shenheguan_id=''))
return qs
def _serialize_shenhe_jilu_row(jl, profile_map, fee_map=None, reject_count_map=None):
dashou = profile_map.get(jl.shenqingren_id, {
'yonghuid': jl.shenqingren_id,
'avatar': '',
'phone': '',
'nicheng': jl.shenqingren_id,
})
kaoheguan = None
if jl.shenheguan_id:
kaoheguan = profile_map.get(jl.shenheguan_id, {
'yonghuid': jl.shenheguan_id,
'avatar': '',
'phone': '',
'nicheng': jl.shenheguan_id,
})
kaoheguan['game_id'] = jl.shenheguan_youxi_id or ''
current_fee = 0.0
if fee_map is not None:
current_fee = fee_map.get((jl.jilu_id, jl.cishu), 0.0)
else:
fd = KaoheJiluFeiyong.query.filter(jilu_id=jl.jilu_id, cishu=jl.cishu).first()
if fd:
current_fee = float(fd.jine)
reject_count = 0
if reject_count_map is not None:
reject_count = reject_count_map.get(jl.jilu_id, 0)
return {
'jilu_id': jl.jilu_id,
'zhuangtai': jl.zhuangtai,
'zhuangtai_text': SHENHE_ZHUANGTAI_MAP.get(jl.zhuangtai, str(jl.zhuangtai)),
'cishu': jl.cishu,
'jiaofei_jine': float(jl.jiaofei_jine or 0),
'current_fee': current_fee,
'bankuai_id': jl.bankuai.bankuai_id if jl.bankuai else None,
'bankuai_name': jl.bankuai.mingcheng if jl.bankuai else '',
'chenghao_id': jl.chenghao.id if jl.chenghao else None,
'chenghao_name': jl.chenghao.mingcheng if jl.chenghao else '',
'chenghao_icon': jl.chenghao.tubiao_url if jl.chenghao else '',
'guize_neirong': jl.guize_neirong or '',
'dashou': {
**dashou,
'game_id': jl.dashou_youxi_id or '',
'beizhu': jl.dashou_beizhu or '',
},
'kaoheguan': kaoheguan,
'reject_count': reject_count,
'CreateTime': jl.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.CreateTime else '',
'UpdateTime': jl.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if jl.UpdateTime else '',
}
class KhjlglView(APIView):
"""
考核记录列表 + 统计
POST /houtai/khjlgl
"""
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 'kaohepeizhi' not in permissions:
return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 20))
if page < 1:
page = 1
if page_size < 1:
page_size = 20
if page_size > 100:
page_size = 100
from jituan.services.shenhe_club import filter_shenhe_jilu_by_request
filter_data = request.data
qs = filter_shenhe_jilu_by_request(
_build_shenhe_jilu_filters(filter_data),
request,
)
# FluentQuery.filter() 会原地修改同一对象;统计与列表必须各自独立查询
stats_base = filter_shenhe_jilu_by_request(
_build_shenhe_jilu_filters(filter_data),
request,
)
agg = stats_base.aggregate(total_fee=Sum('jiaofei_jine'))
tag_rows = filter_shenhe_jilu_by_request(
_build_shenhe_jilu_filters(filter_data),
request,
).values(
'chenghao_id', 'chenghao__mingcheng'
).annotate(
total=Count('jilu_id'),
passed=Count('jilu_id', filter=Q(zhuangtai=1)),
failed=Count('jilu_id', filter=Q(zhuangtai=2)),
in_progress=Count('jilu_id', filter=Q(zhuangtai=0)),
pending=Count('jilu_id', filter=Q(zhuangtai=3)),
).order_by('-total')
_stats_agg = stats_base.aggregate(
total=Count('id'),
passed=Count('id', filter=Q(zhuangtai=1)),
failed=Count('id', filter=Q(zhuangtai=2)),
in_progress=Count('id', filter=Q(zhuangtai=0)),
pending=Count('id', filter=Q(zhuangtai=3)),
)
stats = {
'total': _stats_agg['total'],
'passed': _stats_agg['passed'],
'failed': _stats_agg['failed'],
'in_progress': _stats_agg['in_progress'],
'pending': _stats_agg['pending'],
'total_fee': float(agg['total_fee'] or 0),
'tag_stats': [
{
'chenghao_id': r['chenghao_id'],
'chenghao_name': r['chenghao__mingcheng'] or '未知标签',
'total': r['total'],
'passed': r['passed'],
'failed': r['failed'],
'in_progress': r['in_progress'],
'pending': r['pending'],
}
for r in tag_rows if r['chenghao_id']
],
}
total = qs.count()
start = (page - 1) * page_size
rows = qs.order_by('-CreateTime')[start:start + page_size].to_list()
uids = []
jilu_ids = []
for jl in rows:
uids.append(jl.shenqingren_id)
if jl.shenheguan_id:
uids.append(jl.shenheguan_id)
jilu_ids.append(jl.jilu_id)
profile_map = _batch_user_profile_map(uids)
fee_pairs = [(jl.jilu_id, jl.cishu) for jl in rows]
fee_map = {}
if fee_pairs:
jids = [p[0] for p in fee_pairs]
for fd in KaoheJiluFeiyong.query.filter(jilu_id__in=jids):
fee_map[(fd.jilu_id, fd.cishu)] = float(fd.jine)
reject_counts = {
r['jilu_id']: r['cnt']
for r in KaoheJujueJilu.query.filter(jilu_id__in=jilu_ids)
.values('jilu_id')
.annotate(cnt=Count('id'))
.all()
} if jilu_ids else {}
data_list = [
_serialize_shenhe_jilu_row(jl, profile_map, fee_map, reject_counts)
for jl in rows
]
all_bankuai = list(Bankuai.query.values('bankuai_id', 'mingcheng'))
all_tags = list(
Chenghao.query.filter(leixing='dashou')
.values('id', 'mingcheng', 'bankuai_id')
.order_by('mingcheng')
)
return Response({
'code': 0,
'msg': 'ok',
'data': {
'total': total,
'page': page,
'page_size': page_size,
'list': data_list,
'stats': stats,
'all_bankuai': all_bankuai,
'all_tags': all_tags,
},
})
except Exception as e:
logger.exception('KhjlglView 接口错误')
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
class KhjlczView(APIView):
"""
考核记录操作 / 详情
POST /houtai/khjlcz
action:
- get_detail: 获取单条详情(含缴费明细、拒绝历史)
- to_pending: 考核中 → 待审核(取消指定考核官,与小程序 transfer 一致)
"""
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 'kaohepeizhi' not in permissions:
return Response({'code': 403, 'msg': '无考核管理权限'}, status=403)
action = request.data.get('action')
jilu_id = (request.data.get('jilu_id') or '').strip()
if not jilu_id:
return Response({'code': 1, 'msg': '缺少记录ID'}, status=400)
if action == 'get_detail':
try:
jl = ShenheJilu.query.select_related('bankuai', 'chenghao').get(jilu_id=jilu_id)
except ShenheJilu.DoesNotExist:
return Response({'code': 1, 'msg': '记录不存在'}, status=404)
uids = [jl.shenqingren_id]
if jl.shenheguan_id:
uids.append(jl.shenheguan_id)
profile_map = _batch_user_profile_map(uids)
fee_list = [
{
'cishu': f.cishu,
'jine': float(f.jine),
}
for f in KaoheJiluFeiyong.query.filter(jilu_id=jilu_id).order_by('cishu')
]
reject_list = []
_reject_records = list(KaoheJujueJilu.query.filter(jilu_id=jilu_id).order_by('-CreateTime'))
_kg_ids = [r.shenheguan_id for r in _reject_records if r.shenheguan_id]
_kg_prof_map = _batch_user_profile_map(_kg_ids) if _kg_ids else {}
for r in _reject_records:
kg_prof = _kg_prof_map.get(r.shenheguan_id, {})
reject_list.append({
'cishu': r.cishu,
'shenheguan_id': r.shenheguan_id,
'shenheguan_nicheng': kg_prof.get('nicheng', r.shenheguan_id),
'yuanyin': r.yuanyin,
'CreateTime': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
})
record = _serialize_shenhe_jilu_row(jl, profile_map)
record['fee_list'] = fee_list
record['reject_list'] = reject_list
return Response({'code': 0, 'msg': 'ok', 'data': record})
if action == 'to_pending':
with transaction.atomic():
jl = ShenheJilu.objects.select_for_update().filter(jilu_id=jilu_id).first()
if not jl:
return Response({'code': 1, 'msg': '记录不存在'}, status=404)
if jl.zhuangtai != 0:
return Response({'code': 1, 'msg': '仅「考核中」的记录可退回待审核'}, status=400)
old_kg_id = jl.shenheguan_id
jl.shenheguan_id = None
jl.shenheguan_youxi_id = None
jl.zhuangtai = 3
jl.save(update_fields=['shenheguan_id', 'shenheguan_youxi_id', 'zhuangtai', 'UpdateTime'])
if old_kg_id:
UserShenheguan.query.filter(user__UserUID=old_kg_id).update(shenhe_zhuangtai=0)
return Response({'code': 0, 'msg': '已退回待审核,考核官指定已取消'})
return Response({'code': 1, 'msg': '无效的操作类型'}, status=400)
except Exception as e:
logger.exception('KhjlczView 接口错误')
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)