尝试将 backend 做了视图拆分,来保证可维护性,若此版本存在生产环境问题应该回退

This commit is contained in:
2026-07-04 18:52:00 +08:00
parent 0afeff8227
commit 63cb8e9ee9
17 changed files with 10923 additions and 9176 deletions

File diff suppressed because it is too large Load Diff

View File

720
backend/views/exams.py Normal file
View File

@@ -0,0 +1,720 @@
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 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 jituan.services.shenhe_club import yonghu_chenghao_qs_for_request
# ---------- 预计算每个板块的打手数量 ----------
bankuai_dashou_count_map = {}
all_banquai = Bankuai.query.all()
for bk in all_banquai:
tag_ids = Chenghao.query.filter(bankuai=bk, leixing='dashou').values_list('id', flat=True)
count = yonghu_chenghao_qs_for_request(
YonghuChenghao.query.filter(
chenghao_id__in=tag_ids,
yonghu__DashouProfile__isnull=False,
),
request,
).values('yonghu').distinct().count()
bankuai_dashou_count_map[bk.bankuai_id] = count
# ---------- 新增:板块标签统计 ----------
bankuai_tags_data = []
for bk in all_banquai:
tags = Chenghao.query.filter(bankuai=bk, leixing='dashou')
tag_list = []
for tag in tags:
user_count = yonghu_chenghao_qs_for_request(
YonghuChenghao.query.filter(chenghao=tag),
request,
).values('yonghu').distinct().count()
tag_list.append({
'id': tag.id,
'name': tag.mingcheng,
'user_count': user_count,
})
bankuai_tags_data.append({
'bankuai_id': bk.bankuai_id,
'mingcheng': bk.mingcheng,
'tags': tag_list,
})
# 构造返回数据(审核官列表)
data_list = []
for sg in shenheguan_list:
user = sg.user
# 获取该审核官关联的板块
bk_qs = KaoheguanBankuai.query.filter(kaoheguan_id=user.UserUID).select_related('bankuai')
bankuai_info = []
for bk in bk_qs:
bid = bk.bankuai.bankuai_id
bankuai_info.append({
'bankuai_id': bid,
'mingcheng': bk.bankuai.mingcheng,
'dashou_count': bankuai_dashou_count_map.get(bid, 0)
})
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 = {
'total': stats_base.count(),
'passed': stats_base.filter(zhuangtai=1).count(),
'failed': stats_base.filter(zhuangtai=2).count(),
'in_progress': stats_base.filter(zhuangtai=0).count(),
'pending': stats_base.filter(zhuangtai=3).count(),
'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 = []
for r in KaoheJujueJilu.query.filter(jilu_id=jilu_id).order_by('-CreateTime'):
kg_prof = _batch_user_profile_map([r.shenheguan_id]).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)

968
backend/views/finance.py Normal file
View File

@@ -0,0 +1,968 @@
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, # 商品类型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',
)
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 (添加以下两个视图类)

817
backend/views/guanli.py Normal file
View File

@@ -0,0 +1,817 @@
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 GetGuanliListView(APIView):
"""
获取管事列表
权限4400a,4400b,4400c,4400d,4400e,4400f 任一
POST /houtai/hthqgslb
参数:
username, keyword(可选), balance_op/balance_value, commission_op/commission_value,
invite_count_op/invite_count_value, status, page, page_size
返回:
code:0, data: { list: [...], total }
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
# 权限校验(需要任一权限)
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'}
if not any(p in permissions for p in required_perms):
return Response({'code': 403, 'msg': '您没有权限查看管事列表'})
# 分页参数
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 20))
if page < 1 or page_size < 1:
raise ValueError
except:
return Response({'code': 400, 'msg': '分页参数错误'})
# 筛选参数
keyword = request.data.get('keyword', '').strip()
balance_op = request.data.get('balance_op', '').strip().lower()
balance_value = request.data.get('balance_value')
commission_op = request.data.get('commission_op', '').strip().lower()
commission_value = request.data.get('commission_value')
invite_count_op = request.data.get('invite_count_op', '').strip().lower()
invite_count_value = request.data.get('invite_count_value')
status = request.data.get('status')
# 构建查询:从 User 开始,关联 UserGuanshi 和 UserBoss左连接获取昵称
qs = filter_user_qs_by_club(
User.query.filter(
GuanshiProfile__isnull=False # 确保是管事
).select_related('GuanshiProfile').select_related('BossProfile'),
request,
)
# 关键词搜索用户ID 或 昵称来自 boss_profile.nickname
if keyword:
qs = qs.filter(
Q(UserUID__icontains=keyword) |
Q(BossProfile__nickname__icontains=keyword)
)
# 余额筛选(使用 guanshi_profile.yue
if balance_op and balance_value is not None and balance_value != '':
try:
balance_val = Decimal(str(balance_value))
if balance_op == 'gte':
qs = qs.filter(GuanshiProfile__yue__gte=balance_val)
elif balance_op == 'lte':
qs = qs.filter(GuanshiProfile__yue__lte=balance_val)
else:
return Response({'code': 400, 'msg': '余额比较符无效'})
except:
return Response({'code': 400, 'msg': '余额数值格式错误'})
# 分佣总额筛选guanshi_profile.chongzhifenrun
if commission_op and commission_value is not None and commission_value != '':
try:
commission_val = Decimal(str(commission_value))
if commission_op == 'gte':
qs = qs.filter(GuanshiProfile__chongzhifenrun__gte=commission_val)
elif commission_op == 'lte':
qs = qs.filter(GuanshiProfile__chongzhifenrun__lte=commission_val)
else:
return Response({'code': 400, 'msg': '分佣比较符无效'})
except:
return Response({'code': 400, 'msg': '分佣数值格式错误'})
# 邀请打手总数筛选guanshi_profile.yaogingshuliang
if invite_count_op and invite_count_value is not None and invite_count_value != '':
try:
invite_val = int(invite_count_value)
if invite_count_op == 'gte':
qs = qs.filter(GuanshiProfile__yaogingshuliang__gte=invite_val)
elif invite_count_op == 'lte':
qs = qs.filter(GuanshiProfile__yaogingshuliang__lte=invite_val)
else:
return Response({'code': 400, 'msg': '邀请数量比较符无效'})
except:
return Response({'code': 400, 'msg': '邀请数量格式错误'})
# 状态筛选
if status is not None and status != '':
try:
status_val = int(status)
if status_val not in (0, 1):
raise ValueError
qs = qs.filter(GuanshiProfile__zhuangtai=status_val)
except:
return Response({'code': 400, 'msg': '状态值无效'})
# 计算总数
total = qs.count()
# 分页
offset = (page - 1) * page_size
users = qs.order_by('-UserCreateTime')[offset:offset + page_size]
# 构建返回数据
result = []
for user in users:
guanshi = user.GuanshiProfile
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
result.append({
'yonghuid': user.UserUID,
'avatar': user.Avatar or '',
'nickname': boss.nickname if boss else '',
'zhuangtai': guanshi.zhuangtai,
'yaogingshuliang': guanshi.yaogingshuliang,
'yue': str(guanshi.yue),
'chongzhifenrun': str(guanshi.chongzhifenrun),
})
return Response({
'code': 0,
**list_response_meta(request),
'data': {'list': result, 'total': total},
})
class GetGuanliDetailView(APIView):
"""
获取管事详情
权限4400a,4400b,4400c,4400d,4400e,4400f 任一
POST /houtai/hthqgsxq
参数username, yonghuid
返回:
code:0, data: {
base: {...},
balance: {...},
permanent_enabled, permanent_amount,
custom_first_dividend: {},
extra_dividend_map: {},
all_members: []
}
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
yonghuid = request.data.get('yonghuid', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
if not yonghuid:
return Response({'code': 400, 'msg': '缺少用户ID'})
# 权限校验(需要任一权限)
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
required_perms = {'4400a', '4400b', '4400c', '4400d', '4400e', '4400f'}
if not any(p in permissions for p in required_perms):
return Response({'code': 403, 'msg': '您没有权限查看管事详情'})
# 查询用户主表和管事扩展表
try:
user = User.query.select_related('GuanshiProfile', 'BossProfile').get(UserUID=yonghuid)
guanshi = user.GuanshiProfile
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
except User.DoesNotExist:
return Response({'code': 404, 'msg': '管事不存在'})
except UserGuanshi.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是管事'})
denied = forbid_if_user_out_of_scope(request, user)
if denied:
return denied
# 基础信息
is_zuzhang = UserZuzhang.query.filter(user=user).exists()
base_data = {
'yonghuid': user.UserUID,
'nickname': boss.nickname if boss else '',
'avatar': user.Avatar or '',
'dianhua': guanshi.dianhua or '',
'wechat': guanshi.wechat or '',
'zhuangtai': guanshi.zhuangtai,
'yaoqingma': guanshi.yaoqingma,
'yaoqingren': guanshi.yaoqingren,
'invite_qrcode_url': guanshi.invite_qrcode_url or '',
'yaogingshuliang': guanshi.yaogingshuliang,
'jinrichongzhi': guanshi.jinrichongzhi,
'jinyuechongzhi': guanshi.jinyuechongzhi,
'chongzhifenrun': str(guanshi.chongzhifenrun),
'create_time': guanshi.CreateTime.isoformat() if guanshi.CreateTime else None,
'CreateTime': guanshi.CreateTime.isoformat() if guanshi.CreateTime else None,
}
# 余额提现相关
balance_data = {
'yue': str(guanshi.yue),
'jinritixian_jine': str(guanshi.jinritixian_jine),
'last_tixian_date': guanshi.last_tixian_date.isoformat() if guanshi.last_tixian_date else None,
'kaioi_ewai_xiane': guanshi.kaioi_ewai_xiane,
'ewai_xiane': str(guanshi.ewai_xiane),
}
# 永久分红(二次分红)
permanent_enabled = guanshi.fenghong_erci_enabled
permanent_amount = str(guanshi.fenghong_erci) if guanshi.fenghong_erci else '0'
# 获取所有会员(用于前端选择)
all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage', 'guanshifc'))
for m in all_members:
m['jiage'] = str(m['jiage'])
m['guanshifc'] = str(m['guanshifc'])
# 获取多次分红配置(按次数和会员组织,按俱乐部隔离)
from jituan.services.club_penalty import resolve_gsfenhong_club_id
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
duoci_records = DuociFenhong.query.filter(
yonghuid=yonghuid, club_id=guanshi_club,
).order_by('cishu')
custom_first = {} # 首次分红定制cishu=1
extra_map = {} # 其他次数 { cishu: { huiyuan_id: { guanshi_fenhong, jieshao, jiage } } }
for record in duoci_records:
# 获取会员信息
member = Huiyuan.query.filter(huiyuan_id=record.huiyuan).first()
if not member:
continue
member_info = {
'jieshao': member.jieshao,
'jiage': str(member.jiage),
}
if record.cishu == 1:
custom_first[record.huiyuan] = float(record.guanshi_fenhong)
else:
times = record.cishu
if times not in extra_map:
extra_map[times] = {}
extra_map[times][record.huiyuan] = {
'guanshi_fenhong': float(record.guanshi_fenhong),
**member_info
}
# 注意:首次分红定制只存储有定制记录的会员,没有定制的会员使用默认值(前端通过会员表展示)
# 额外次数分红按次数返回
return Response({
'code': 0,
'data': {
'base': base_data,
'balance': balance_data,
'permanent_enabled': permanent_enabled,
'permanent_amount': permanent_amount,
'custom_first_dividend': custom_first,
'extra_dividend_map': extra_map,
'all_members': all_members,
'is_zuzhang': is_zuzhang,
}
})
def _generate_unique_zuzhang_yaoqingma(yonghuid):
"""生成唯一组长邀请码(与小程序组长注册逻辑一致)"""
timestamp = str(int(time.time()))[-6:]
uid_part = yonghuid[-6:] if len(yonghuid) >= 6 else yonghuid.rjust(6, '0')
rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=2))
base = f"Z{timestamp}{uid_part}{rand_str}"
for _ in range(5):
if not UserZuzhang.query.filter(yaoqingma=base).exists():
return base
base = base + random.choice(string.digits)
return base + str(int(time.time() * 1000))[-4:]
def _create_zuzhang_profile(user):
"""为已有用户创建组长扩展记录(默认字段与小程序注册一致)"""
invite_guanshi_count = UserGuanshi.query.filter(yaoqingren=user.UserUID).count()
yaoqingma = _generate_unique_zuzhang_yaoqingma(user.UserUID)
return UserZuzhang.query.create(
user=user,
yaoqingma=yaoqingma,
fenyong_zonge=Decimal('0.00'),
ketixian_jine=Decimal('0.00'),
yaoqing_zongshu=invite_guanshi_count,
jinri_fenyong=Decimal('0.00'),
jinyue_fenyong=Decimal('0.00'),
zhuangtai=1,
jinri_tixian=Decimal('0.00'),
ewai_tixian_xiane=Decimal('0.00'),
kaioi_ewai_tixian=False,
kaioi_ewai_fenhong=False,
ewai_fenhong_jine=Decimal('0.00'),
)
class PromoteGuanshiToZuzhangView(APIView):
"""
后台将管事用户升级为组长(写入 user_zuzhang 扩展表)
POST /houtai/htgssjzz
权限sjzz666
参数username, yonghuid
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
yonghuid = request.data.get('yonghuid', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
if not yonghuid:
return Response({'code': 400, 'msg': '缺少用户ID'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if 'sjzz666' not in permissions:
return Response({'code': 403, 'msg': '无权限升级组长(需要 sjzz666'})
try:
user = User.query.select_related('GuanshiProfile').get(UserUID=yonghuid)
guanshi = user.GuanshiProfile
except User.DoesNotExist:
return Response({'code': 404, 'msg': '用户不存在'})
except UserGuanshi.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是管事,无法升级组长'})
if UserZuzhang.query.filter(user=user).exists():
zuzhang = user.ZuzhangProfile
return Response({
'code': 400,
'msg': '该用户已是组长',
'data': {
'yonghuid': user.UserUID,
'yaoqingma': zuzhang.yaoqingma,
},
})
if guanshi.zhuangtai != 1:
return Response({'code': 400, 'msg': '管事账号已禁用,无法升级组长'})
try:
with transaction.atomic():
zuzhang = _create_zuzhang_profile(user)
except IntegrityError:
return Response({'code': 400, 'msg': '升级失败,请稍后重试'})
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=kefu.user.Phone,
leixing=XIUGAI_LEIXING_GUANSHI,
qitashuoming=(
f'【后台】管事升级组长用户ID={yonghuid}'
f'生成组长邀请码={zuzhang.yaoqingma},操作人={kefu.user.Phone}'
),
)
logger.info(f"管事升级组长成功: yonghuid={yonghuid}, yaoqingma={zuzhang.yaoqingma}")
return Response({
'code': 0,
'msg': '已升级为组长',
'data': {
'yonghuid': user.UserUID,
'nickname': getattr(user.BossProfile, 'nickname', '') if hasattr(user, 'BossProfile') else '',
'yaoqingma': zuzhang.yaoqingma,
'yaoqing_zongshu': zuzhang.yaoqing_zongshu,
'ketixian_jine': str(zuzhang.ketixian_jine),
},
})
class UpdateGuanliView(APIView):
"""
修改管事信息(基础信息、余额、提现限额、分红策略)
权限细分:
- 修改状态(封禁/解封)及昵称/电话/微信需要4400a
- 增加余额需要4400c
- 减少余额需要4400b
- 修改提现限额kaioi_ewai_xiane/ewai_xiane需要4400e
- 修改分红相关永久分红、定制首次、额外次数需要4400f
POST /houtai/htxggsxx
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
yonghuid = request.data.get('yonghuid', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
if not yonghuid:
return Response({'code': 400, 'msg': '缺少用户ID'})
# 权限校验(先获取权限列表)
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
# 查询管事对象(不在事务外加锁,事务内加锁)
try:
user = User.query.get(UserUID=yonghuid)
guanshi = user.GuanshiProfile
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
except User.DoesNotExist:
return Response({'code': 404, 'msg': '用户不存在'})
except UserGuanshi.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是管事'})
# 开始事务,内部使用 select_for_update
with transaction.atomic():
# 重新获取加锁对象
user = User.objects.select_for_update().get(UserUID=yonghuid)
guanshi = user.GuanshiProfile
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
operator = kefu.user.Phone
# ---------- 1. 基础信息修改需要4400a----------
nickname = request.data.get('nickname')
dianhua = request.data.get('dianhua')
wechat = request.data.get('wechat')
zhuangtai = request.data.get('zhuangtai')
avatar = request.data.get('avatar')
if any(v is not None for v in [nickname, dianhua, wechat, zhuangtai, avatar]):
if '4400a' not in permissions:
return Response({'code': 403, 'msg': '无权限修改管事基础信息需要4400a'})
base_changes = []
old_nickname = boss.nickname if boss else ''
old_dianhua = guanshi.dianhua
old_wechat = guanshi.wechat
old_zhuangtai = guanshi.zhuangtai
# 处理昵称(在老板扩展表中)
if nickname is not None:
if not boss:
boss = UserBoss.query.create(user=user, nickname=nickname)
else:
boss.nickname = nickname
boss.save()
if str(nickname) != str(old_nickname):
base_changes.append(f'昵称:{old_nickname or ""}{nickname}')
# 处理其他字段
if dianhua is not None:
guanshi.dianhua = dianhua
if str(dianhua) != str(old_dianhua):
base_changes.append(f'电话:{old_dianhua or ""}{dianhua}')
if wechat is not None:
guanshi.wechat = wechat
if str(wechat) != str(old_wechat):
base_changes.append(f'微信:{old_wechat or ""}{wechat}')
if zhuangtai is not None:
try:
zt = int(zhuangtai)
if zt in (0, 1):
guanshi.zhuangtai = zt
if zt != old_zhuangtai:
zt_map = {0: '禁用', 1: '正常'}
base_changes.append(f'状态:{zt_map.get(old_zhuangtai, old_zhuangtai)}{zt_map.get(zt, zt)}')
except:
pass
if avatar is not None:
user.Avatar = avatar
user.save()
base_changes.append('更新了头像')
guanshi.save()
if base_changes:
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_GUANSHI,
qitashuoming=(
f'【后台】修改管事基础信息:{"".join(base_changes)}'
f'管事ID={yonghuid},操作人={operator}'
),
)
# ---------- 2. 余额修改4400c4400b----------
new_yue = request.data.get('yue')
if new_yue is not None:
try:
new_yue = Decimal(str(new_yue))
if new_yue < 0:
return Response({'code': 400, 'msg': '余额不能为负数'})
except:
return Response({'code': 400, 'msg': '余额格式错误'})
old_yue = guanshi.yue
if new_yue > old_yue:
if '4400c' not in permissions:
return Response({'code': 403, 'msg': '无权限增加余额需要4400c'})
from jituan.services.group_finance_gate import deny_group_finance_exec
deny = deny_group_finance_exec(request.user, request)
if deny:
return deny
action_desc = f'增加{new_yue - old_yue}'
elif new_yue < old_yue:
if '4400b' not in permissions:
return Response({'code': 403, 'msg': '无权限减少余额需要4400b'})
action_desc = f'减少{old_yue - new_yue}'
else:
action_desc = '无变动'
if new_yue != old_yue:
guanshi.yue = new_yue
guanshi.save()
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_GUANSHI,
guanshiyue=new_yue,
guanshiyueq=old_yue,
qitashuoming=(
f'【后台】修改管事可提现余额:{old_yue}元 → {new_yue}元({action_desc}'
f'管事ID={yonghuid},操作人={operator}'
),
)
# ---------- 3. 提现限额修改需要4400e----------
kaioi_ewai = request.data.get('kaioi_ewai_xiane')
ewai_val = request.data.get('ewai_xiane')
if kaioi_ewai is not None or ewai_val is not None:
if '4400e' not in permissions:
return Response({'code': 403, 'msg': '无权限修改提现限额需要4400e'})
limit_changes = []
old_kaioi = guanshi.kaioi_ewai_xiane
old_ewai = guanshi.ewai_xiane
if kaioi_ewai is not None:
guanshi.kaioi_ewai_xiane = bool(kaioi_ewai)
if bool(kaioi_ewai) != old_kaioi:
limit_changes.append(f'额外限额开关:{"" if old_kaioi else ""}{"" if kaioi_ewai else ""}')
if ewai_val is not None:
try:
guanshi.ewai_xiane = Decimal(str(ewai_val))
if guanshi.ewai_xiane != old_ewai:
limit_changes.append(f'额外限额金额:{old_ewai}{guanshi.ewai_xiane}')
except:
return Response({'code': 400, 'msg': '额外限额金额格式错误'})
guanshi.save()
if limit_changes:
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_GUANSHI,
qitashuoming=(
f'【后台】修改管事提现限额:{"".join(limit_changes)}'
f'管事ID={yonghuid},操作人={operator}'
),
)
# ---------- 4. 分红相关修改需要4400f----------
perm_enabled = request.data.get('permanent_enabled')
perm_amount = request.data.get('permanent_amount')
custom_first = request.data.get('custom_first_dividend')
extra_map = request.data.get('extra_dividend_map')
if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]):
if '4400f' not in permissions:
return Response({'code': 403, 'msg': '无权限修改分红策略需要4400f'})
# 4.1 永久分红
if perm_enabled is not None:
guanshi.fenghong_erci_enabled = bool(perm_enabled)
if perm_amount is not None:
try:
guanshi.fenghong_erci = Decimal(str(perm_amount))
except:
return Response({'code': 400, 'msg': '永久分红金额格式错误'})
guanshi.save()
# 4.2 首次定制分红cishu=1
if custom_first is not None and isinstance(custom_first, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
# 获取当前所有首次定制记录
existing_first = DuociFenhong.query.filter(
club_id=guanshi_club, yonghuid=yonghuid, cishu=1,
)
# 前端传来的定制字典 {会员ID: 金额}
for huiyuan_id, amount in custom_first.items():
try:
amount = Decimal(str(amount))
except:
continue
if amount <= 0:
DuociFenhong.query.filter(
club_id=guanshi_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1,
).delete()
else:
DuociFenhong.query.update_or_create(
club_id=guanshi_club,
yonghuid=yonghuid,
huiyuan=huiyuan_id,
cishu=1,
defaults={
'guanshi_fenhong': amount,
'zuzhang_fenhong': Decimal('0'),
},
)
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
for record in existing_first:
if record.huiyuan not in custom_first:
record.delete()
# 4.3 额外次数分红cishu >= 2
if extra_map is not None and isinstance(extra_map, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id
guanshi_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
# 收集需要保留的记录
keep_set = set()
for times_str, members in extra_map.items():
try:
cishu = int(times_str)
if cishu < 2:
continue
except:
continue
if not isinstance(members, dict):
continue
for huiyuan_id, data in members.items():
if not isinstance(data, dict):
continue
amount = data.get('guanshi_fenhong')
if amount is None:
continue
try:
amount = Decimal(str(amount))
except:
continue
keep_set.add((cishu, huiyuan_id))
# 更新或创建
DuociFenhong.query.update_or_create(
club_id=guanshi_club,
yonghuid=yonghuid,
huiyuan=huiyuan_id,
cishu=cishu,
defaults={
'guanshi_fenhong': amount,
'zuzhang_fenhong': Decimal('0')
}
)
# 删除不在 keep_set 中的额外次数记录
existing_extra = DuociFenhong.query.filter(
club_id=guanshi_club, yonghuid=yonghuid, cishu__gte=2,
)
for record in existing_extra:
if (record.cishu, record.huiyuan) not in keep_set:
record.delete()
dividend_parts = []
if perm_enabled is not None:
dividend_parts.append(f'永久分红开关→{"" if perm_enabled else ""}')
if perm_amount is not None:
dividend_parts.append(f'永久分红金额→{perm_amount}')
if custom_first is not None:
dividend_parts.append(f'更新首次定制分红({len(custom_first)}项)')
if extra_map is not None:
dividend_parts.append(f'更新额外次数分红({len(extra_map)}组)')
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_GUANSHI,
qitashuoming=(
f'【后台】修改管事分红策略:{"".join(dividend_parts) or "配置已更新"}'
f'管事ID={yonghuid},操作人={operator}'
),
)
return Response({'code': 0, 'msg': '修改成功'})

1147
backend/views/kefu.py Normal file

File diff suppressed because it is too large Load Diff

351
backend/views/members.py Normal file
View File

@@ -0,0 +1,351 @@
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 GetMemberListView(APIView):
"""
获取会员列表
权限3300a
POST /houtai/hthqhylb
参数username
返回code:0, data: { list: [...] }
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '3300a' not in permissions:
return Response({'code': 403, 'msg': '无权限查看会员列表'})
from jituan.services.club_member_admin import build_member_list_payload
payload = build_member_list_payload(request)
return Response({
'code': 0,
'data': {'list': payload['list']},
'club_id': payload.get('club_id'),
'scope': payload.get('scope'),
'price_note': payload.get('price_note'),
})
class UpdateMemberView(APIView):
"""
修改会员信息
权限3300a
POST /houtai/htxghyxx
参数:
username, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao
返回code:0 成功
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
huiyuan_id = request.data.get('huiyuan_id', '').strip()
jieshao = request.data.get('jieshao', '').strip()
jiage = request.data.get('jiage')
guanshifc = request.data.get('guanshifc')
zuzhangfc = request.data.get('zuzhangfc')
jtjieshao = request.data.get('jtjieshao', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
if not huiyuan_id:
return Response({'code': 400, 'msg': '缺少会员ID'})
if not jieshao:
return Response({'code': 400, 'msg': '会员名称不能为空'})
try:
jiage = Decimal(str(jiage))
guanshifc = Decimal(str(guanshifc))
zuzhangfc = Decimal(str(zuzhangfc))
if jiage < 0 or guanshifc < 0 or zuzhangfc < 0:
raise ValueError
except:
return Response({'code': 400, 'msg': '金额格式错误'})
if guanshifc + zuzhangfc > jiage:
return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'})
# 权限校验
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '3300a' not in permissions:
return Response({'code': 403, 'msg': '无权限修改会员'})
from jituan.services.club_member_admin import update_member_for_club
extra = {
'formal_days': request.data.get('formal_days'),
'trial_enabled': request.data.get('trial_enabled'),
'trial_price': request.data.get('trial_price'),
'trial_days': request.data.get('trial_days'),
'trial_guanshifc': request.data.get('trial_guanshifc'),
'trial_zuzhangfc': request.data.get('trial_zuzhangfc'),
}
data, err = update_member_for_club(
request, huiyuan_id, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao, extra=extra,
)
if err:
return Response({'code': 400, 'msg': err})
return Response({'code': 0, 'msg': '修改成功'})
class AddMemberView(APIView):
"""
添加会员
权限3300a
POST /houtai/httjhy
参数:
username, jieshao, jiage, guanshifc, zuzhangfc, jtjieshao
返回code:0, data: { huiyuan_id }
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
jieshao = request.data.get('jieshao', '').strip()
jiage = request.data.get('jiage')
guanshifc = request.data.get('guanshifc')
zuzhangfc = request.data.get('zuzhangfc')
jtjieshao = request.data.get('jtjieshao', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
if not jieshao:
return Response({'code': 400, 'msg': '会员名称不能为空'})
try:
jiage = Decimal(str(jiage))
guanshifc = Decimal(str(guanshifc))
zuzhangfc = Decimal(str(zuzhangfc))
if jiage <= 0 or guanshifc < 0 or zuzhangfc < 0:
raise ValueError
except:
return Response({'code': 400, 'msg': '金额格式错误'})
if guanshifc + zuzhangfc > jiage:
return Response({'code': 400, 'msg': '管事分成+组长分成不能大于会员价格'})
# 权限校验
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '3300a' not in permissions:
return Response({'code': 403, 'msg': '无权限添加会员'})
from jituan.services.club_member_admin import add_global_member
from jituan.services.club_context import resolve_club_scope
from jituan.constants import DATA_SCOPE_ALL
if resolve_club_scope(request) != DATA_SCOPE_ALL and '000001' not in permissions:
return Response({
'code': 403,
'msg': '仅集团汇总视图可新建全局会员类型;俱乐部请从会员 catalog 上架',
})
data, err = add_global_member(jieshao, jiage, guanshifc, zuzhangfc, jtjieshao)
if err:
return Response({'code': 400, 'msg': err})
return Response({'code': 0, 'msg': '添加成功', 'data': data})
class ClubMemberCatalogView(APIView):
"""俱乐部可上架的集团会员 catalog。POST /houtai/hthycatalog"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '3300a' not in permissions:
return Response({'code': 403, 'msg': '无权限'})
from jituan.services.club_member_admin import build_member_catalog_payload
return Response({'code': 0, 'data': build_member_catalog_payload(request)})
class ClubMemberEnableView(APIView):
"""俱乐部上架会员。POST /houtai/hthysj"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
huiyuan_id = request.data.get('huiyuan_id', '').strip()
if not username or not huiyuan_id:
return Response({'code': 400, 'msg': '参数不完整'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '3300a' not in permissions:
return Response({'code': 403, 'msg': '无权限'})
try:
jiage = Decimal(str(request.data.get('jiage')))
guanshifc = Decimal(str(request.data.get('guanshifc')))
zuzhangfc = Decimal(str(request.data.get('zuzhangfc')))
except Exception:
return Response({'code': 400, 'msg': '金额格式错误'})
from jituan.services.club_member_admin import enable_member_for_club
extra = request.data
data, err = enable_member_for_club(
request, huiyuan_id, jiage, guanshifc, zuzhangfc, extra=extra,
)
if err:
return Response({'code': 400, 'msg': err})
return Response({'code': 0, 'msg': '上架成功', 'data': data})
class ClubMemberDisableView(APIView):
"""俱乐部下架会员。POST /houtai/hthyxj"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
huiyuan_id = request.data.get('huiyuan_id', '').strip()
if not username or not huiyuan_id:
return Response({'code': 400, 'msg': '参数不完整'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '3300a' not in permissions:
return Response({'code': 403, 'msg': '无权限'})
from jituan.services.club_member_admin import disable_member_for_club
data, err = disable_member_for_club(request, huiyuan_id)
if err:
return Response({'code': 400, 'msg': err})
return Response({'code': 0, 'msg': '已下架', 'data': data})

733
backend/views/penalties.py Normal file
View File

@@ -0,0 +1,733 @@
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 FaKuanTongJiView(APIView):
"""
罚款统计接口
POST /houtai/htglyhqcfsltj
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 400, 'msg': '参数不完整'})
try:
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if not has_fadan_view_permission(permissions, username):
return Response({'code': 403, 'msg': '无权限查看罚款统计'})
from utils.penalty_status import penalty_platform_audit_pending_q
pending_q = penalty_platform_audit_pending_q()
stats = filter_penalty_qs(Penalty.query.all(), request).aggregate(
total=Count('id'),
daijiaona=Count('id', filter=Q(Status=1)),
shensuzhong=Count('id', filter=Q(Status=3)),
yijiaona=Count('id', filter=Q(Status=2)),
yibohui=Count('id', filter=Q(Status=4)),
pingtai_shenhe=Count('id', filter=pending_q),
)
data = {k: v or 0 for k, v in stats.items()}
from jituan.services.club_user_access import list_response_meta
return Response({'code': 0, 'msg': '成功', 'data': {**data, **list_response_meta(request)}})
except Exception as e:
logger.error(f"罚款统计异常: {traceback.format_exc()}")
return Response({'code': 500, 'msg': '系统繁忙'})
class FaKuanLieBiaoView(APIView):
"""
罚款列表接口
POST /houtai/hthqfklb
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 400, 'msg': '参数不完整'})
try:
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if not has_fadan_view_permission(permissions, username):
return Response({'code': 403, 'msg': '无权限查看罚款列表'})
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 20))
if page_size > 100:
page_size = 100
qs = filter_penalty_qs(Penalty.query.all(), request)
# 状态筛选5=平台审核中,仅 zhuangtai=5
from utils.penalty_status import penalty_platform_audit_pending_q
zhuangtai_raw = request.data.get('zhuangtai')
if zhuangtai_raw is not None and str(zhuangtai_raw).strip() != '':
try:
zt = int(zhuangtai_raw)
if zt == 5:
qs = qs.filter(penalty_platform_audit_pending_q())
elif zt in (1, 2, 3, 4):
qs = qs.filter(Status=zt)
except (ValueError, TypeError):
pass
# 被处罚人ID
PenalizedUserID = request.data.get('PenalizedUserID', '').strip()
if PenalizedUserID:
qs = qs.filter(PenalizedUserID=PenalizedUserID)
# 被处罚人身份
beichufa_shenfen = request.data.get('beichufa_shenfen')
if beichufa_shenfen is not None and int(beichufa_shenfen) in [1, 2, 3, 4]:
qs = qs.filter(Identity=int(beichufa_shenfen))
# 模糊搜索订单ID、被罚人ID、申请人ID
sousuo = request.data.get('sousuo', '').strip()
if sousuo:
qs = qs.filter(
Q(RelatedOrderID__icontains=sousuo) |
Q(PenalizedUserID__icontains=sousuo) |
Q(ApplicantID__icontains=sousuo)
)
qs = qs.order_by('-CreateTime')
paginator = Paginator(qs, page_size)
try:
page_obj = paginator.page(page)
except Exception:
return Response({
'code': 0, 'msg': '成功',
'data': {'list': [], 'total': 0, 'page': page, 'page_size': page_size}
})
records = list(page_obj)
penalty_ids = [r.id for r in records]
# 批量获取图片并分组
zhengju_map = {}
shensu_map = {}
if penalty_ids:
tupians = PenaltyAppealImage.query.filter(
Penalty_id__in=penalty_ids
).values('Penalty_id', 'ImageURL', 'Purpose')
for t in tupians:
fid = t['Penalty_id']
url = t['ImageURL']
if t['Purpose'] == 1:
zhengju_map.setdefault(fid, []).append(url)
else:
shensu_map.setdefault(fid, []).append(url)
from utils.penalty_status import penalty_display_status, is_penalty_platform_audit_pending
# 组装列表数据
results = []
for r in records:
results.append({
'id': r.id,
'beichufa_id': r.PenalizedUserID,
'shenfen': r.Identity,
'shenfen_text': dict(Penalty._meta.get_field('Identity').choices).get(r.Identity, ''),
'shenqing_chufa': r.ApplicantID or '',
'shenqingren_shenfen': r.ApplicantIdentity,
'chufaliyou': r.Reason or '',
'fakuanjine': str(r.FineAmount),
'guanliandingdan_id': r.RelatedOrderID or '',
'zhuangtai': r.Status,
'zhuangtai_text': penalty_display_status(r),
'platform_audit_pending': is_penalty_platform_audit_pending(r),
'yingxiang_qiangdan': r.AffectsGrabbing,
'shensuliyou': r.AppealReason or '',
'bohuiliyou': r.RejectReason or '',
'chulizhe': r.ProcessorID or '',
'chulizhe_shenfen': r.ProcessorIdentity,
'zhengju_tupian': zhengju_map.get(r.id, []),
'shensu_tupian': shensu_map.get(r.id, []),
'creat_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
'create_time': r.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if r.CreateTime else '',
'UpdateTime': r.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if r.UpdateTime else '',
})
from jituan.services.club_user_access import list_response_meta
return Response({
'code': 0, 'msg': '成功',
'data': {
'list': results,
'total': paginator.count,
'page': page,
'page_size': page_size,
**list_response_meta(request),
}
})
except Exception as e:
logger.error(f"罚款列表异常: {traceback.format_exc()}")
return Response({'code': 500, 'msg': '系统繁忙'})
class FaKuanChuLiView(APIView):
"""
处理罚款申诉(同意/拒绝)
POST /houtai/glyclfk
"""
permission_classes = [IsAuthenticated]
parser_classes = [MultiPartParser, FormParser, JSONParser]
def post(self, request):
try:
username = request.data.get('username', '').strip()
penalty_id = request.data.get('penalty_id') or request.data.get('fadan_id')
action = request.data.get('action', '').strip()
chuli_liyou = request.data.get('chuli_liyou', '').strip()
if not all([username, penalty_id, action]):
return Response({'code': 400, 'msg': '参数不完整'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
with transaction.atomic():
try:
penalty = Penalty.objects.select_for_update().get(id=penalty_id)
except Penalty.DoesNotExist:
return Response({'code': 404, 'msg': '罚单不存在'})
deny = forbid_penalty_out_of_scope(request, penalty)
if deny:
return deny
if penalty.Status != 3:
return Response({'code': 400, 'msg': '当前状态不可处理'})
if not check_fadan_permission(permissions, penalty.Identity):
return Response({'code': 403, 'msg': '无权限处理该类罚单'})
# 处理上传的图片
uploaded_urls = []
files = request.FILES.getlist('tupian') or []
if not files:
single = request.FILES.get('tupian')
if single:
files = [single]
for file in files:
valid, msg = validate_image(file)
if not valid:
return Response({'code': 400, 'msg': msg})
ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
file_name = f"chufa_{penalty_id}_{int(time.time() * 1000)}.{ext}"
file_path = f"chufatupian/kechufatupian/{file_name}"
url = upload_to_oss(file, file_path)
if not url:
return Response({'code': 500, 'msg': '图片上传失败'})
uploaded_urls.append(file_path)
# 更新状态
if action == 'agree': # 同意申诉 → 驳回处罚
penalty.Status = 4
penalty.RejectReason = chuli_liyou
elif action == 'reject': # 拒绝申诉 → 回到待缴纳
penalty.Status = 1
penalty.RejectReason = chuli_liyou
else:
return Response({'code': 400, 'msg': '无效的操作类型'})
# 记录处理者(使用 request.user 的 phone
penalty.ProcessorID = request.user.Phone
penalty.ProcessorIdentity = 2 # 这里假设处理者是售后身份,可根据实际业务获取
penalty.save()
# 保存图片证据图片用途1
for relative_url in uploaded_urls:
PenaltyAppealImage.query.create(
Penalty_id=penalty_id,
PenalizedUserID=penalty.PenalizedUserID,
ImageURL=relative_url,
Purpose=1
)
logger.info(f"罚款处理成功: {penalty_id}, 操作: {action}")
return Response({'code': 0, 'msg': '处理成功'})
except Exception as e:
logger.error(f"罚款处理异常: {traceback.format_exc()}")
return Response({'code': 500, 'msg': '系统繁忙'})
class FaKuanPingTaiShenHeView(APIView):
"""
平台审核商家提交的罚款(同意/驳回)
POST /houtai/glyptshfk
action: platform_approve | platform_reject
"""
permission_classes = [IsAuthenticated]
parser_classes = [MultiPartParser, FormParser, JSONParser]
def post(self, request):
try:
from utils.penalty_status import (
PENALTY_PLATFORM_AUDIT, PENALTY_PENDING_PAY, PENALTY_APPEALING, PENALTY_REJECTED,
)
username = request.data.get('username', '').strip()
penalty_id = request.data.get('penalty_id') or request.data.get('fadan_id')
action = request.data.get('action', '').strip()
chuli_liyou = request.data.get('chuli_liyou', '').strip()
if not all([username, penalty_id, action]):
return Response({'code': 400, 'msg': '参数不完整'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
with transaction.atomic():
try:
penalty = Penalty.objects.select_for_update().get(id=penalty_id)
except Penalty.DoesNotExist:
return Response({'code': 404, 'msg': '罚单不存在'})
deny = forbid_penalty_out_of_scope(request, penalty)
if deny:
return deny
if penalty.Status != PENALTY_PLATFORM_AUDIT:
return Response({'code': 400, 'msg': '当前状态不可平台审核'})
if not check_fadan_permission(permissions, penalty.Identity):
return Response({'code': 403, 'msg': '无权限处理该类罚单'})
if action == 'platform_approve':
if penalty.AppealReason:
penalty.Status = PENALTY_APPEALING
else:
penalty.Status = PENALTY_PENDING_PAY
penalty.ApproveReason = chuli_liyou or penalty.ApproveReason or ''
elif action == 'platform_reject':
penalty.Status = PENALTY_REJECTED
penalty.RejectReason = chuli_liyou or '平台审核驳回'
else:
return Response({'code': 400, 'msg': '无效的操作类型'})
penalty.ProcessorID = request.user.Phone
penalty.ProcessorIdentity = 3
penalty.save()
logger.info(f"罚款平台审核成功: {penalty_id}, 操作: {action}")
return Response({'code': 0, 'msg': '平台审核完成'})
except Exception as e:
logger.error(f"罚款平台审核异常: {traceback.format_exc()}")
return Response({'code': 500, 'msg': '系统繁忙'})
class FaKuanChuangJianView(APIView):
"""
管理员创建罚款
POST /houtai/htfksc
"""
permission_classes = [IsAuthenticated]
parser_classes = [MultiPartParser, FormParser, JSONParser]
def post(self, request):
try:
username = request.data.get('username', '').strip()
PenalizedUserID, Reason, FineAmount, AffectsGrabbing = pick_penalty_create_params(request.data)
shenfen = request.data.get('shenfen')
if not all([username, PenalizedUserID, shenfen, Reason, FineAmount is not None]):
return Response({'code': 400, 'msg': '参数不完整'})
try:
shenfen = int(shenfen)
FineAmount = float(FineAmount)
except (ValueError, TypeError):
return Response({'code': 400, 'msg': '参数格式错误'})
if shenfen not in [1, 2, 3, 4]:
return Response({'code': 400, 'msg': '无效的被罚人身份'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if not check_fadan_permission(permissions, shenfen):
return Response({'code': 403, 'msg': '无权限创建该类罚单'})
# 验证用户是否存在对应的扩展表
try:
user = User.query.get(UserUID=PenalizedUserID)
profile_attr = SHENFEN_PROFILE_MAP.get(shenfen)
if not profile_attr or not hasattr(user, profile_attr):
return Response({'code': 400, 'msg': '该用户不是所选身份'})
except User.DoesNotExist:
return Response({'code': 404, 'msg': '用户不存在'})
# 处理图片上传
uploaded_urls = []
files = request.FILES.getlist('tupian') or []
if not files:
single = request.FILES.get('tupian')
if single:
files = [single]
for file in files:
valid, msg = validate_image(file)
if not valid:
return Response({'code': 400, 'msg': msg})
ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
file_name = f"chufa_{int(time.time() * 1000)}.{ext}"
file_path = f"chufatupian/kechufatupian/{file_name}"
url = upload_to_oss(file, file_path)
if not url:
return Response({'code': 500, 'msg': '图片上传失败'})
uploaded_urls.append(file_path)
with transaction.atomic():
penalty = Penalty.query.create(
PenalizedUserID=PenalizedUserID,
Identity=shenfen,
ApplicantID=request.user.Phone, # 申请人(客服)手机号
ApplicantIdentity=2, # 售后身份
Reason=Reason,
FineAmount=FineAmount,
AffectsGrabbing=AffectsGrabbing,
Status=1, # 待缴纳
ClubID=resolve_penalty_club_id(
penalized_user_id=PenalizedUserID,
request=request,
user=request.user,
),
)
for relative_url in uploaded_urls:
PenaltyAppealImage.query.create(
Penalty=penalty,
PenalizedUserID=PenalizedUserID,
ImageURL=relative_url,
Purpose=1
)
from users.fadan_fenhong_utils import lock_penalty_bonus
lock_penalty_bonus(penalty)
logger.info(f"罚款创建成功: {penalty.id}")
return Response({'code': 0, 'msg': '罚款已创建', 'data': {'id': penalty.id}})
except Exception as e:
logger.error(f"创建罚款异常: {traceback.format_exc()}")
return Response({'code': 500, 'msg': '系统繁忙'})
class FineApplyView(APIView):
"""
客服罚款申请接口(基于订单对打手发起罚款)
POST /houtai/kffkdssq
权限: JWT认证 + 客服权限验证
请求参数:
username (必填) : 客服用户名(手机号)
OrderID (必填) : 订单ID
Reason (必填) : 罚款原因
FineAmount (必填) : 罚款金额(元)
AffectsGrabbing (可选) : 是否影响抢单1=是0=否默认1
"""
permission_classes = [IsAuthenticated]
parser_classes = [MultiPartParser, FormParser, JSONParser]
def post(self, request):
try:
username = request.data.get('username', '').strip()
OrderID = pick_order_id(request.data)
_, Reason, FineAmount, AffectsGrabbing = pick_penalty_create_params(request.data)
if not all([username, OrderID, Reason, FineAmount is not None]):
return Response({'code': 400, 'msg': '参数不完整'})
try:
FineAmount = float(FineAmount)
except (ValueError, TypeError):
return Response({'code': 400, 'msg': '罚款金额格式错误'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
# 验证订单存在
try:
order = Order.query.get(OrderID=OrderID)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'})
# 获取打手ID
dashou_id = order.PlayerID
if not dashou_id:
return Response({'code': 400, 'msg': '该订单无接单打手,无法罚款'})
# 重复罚款检查
if Penalty.query.filter(PenalizedUserID=dashou_id, RelatedOrderID=OrderID).exists():
return Response({'code': 400, 'msg': '该打手已被罚款过'})
# 检查权限(打手身份 shenfen=1
if not check_fadan_permission(permissions, 1):
return Response({'code': 403, 'msg': '无权限处罚打手'})
# 处理图片上传
uploaded_urls = []
files = request.FILES.getlist('tupian') or []
if not files:
single = request.FILES.get('tupian')
if single:
files = [single]
for file in files:
valid, msg = validate_image(file)
if not valid:
return Response({'code': 400, 'msg': msg})
ext = file.name.split('.')[-1] if '.' in file.name else 'jpg'
file_name = f"chufa_{int(time.time() * 1000)}.{ext}"
file_path = f"chufatupian/kechufatupian/{file_name}"
url = upload_to_oss(file, file_path)
if not url:
return Response({'code': 500, 'msg': '图片上传失败'})
uploaded_urls.append(file_path)
# 创建罚单
with transaction.atomic():
penalty = Penalty.query.create(
PenalizedUserID=dashou_id,
ApplicantID=request.user.Phone,
Identity=1, # 被处罚人身份1 打手
Reason=Reason,
FineAmount=FineAmount,
RelatedOrderID=OrderID,
Status=1, # 待缴纳
AffectsGrabbing=AffectsGrabbing,
ApplicantIdentity=1, # 申请人身份1 客服
ClubID=resolve_penalty_club_id(
order=order,
request=request,
user=request.user,
applicant_id=getattr(request.user, 'UserUID', None),
),
)
for relative_url in uploaded_urls:
PenaltyAppealImage.query.create(
Penalty=penalty,
PenalizedUserID=dashou_id,
ImageURL=relative_url,
Purpose=1
)
from users.fadan_fenhong_utils import lock_penalty_bonus
lock_penalty_bonus(penalty)
logger.info(f"客服罚款申请成功: 订单 {OrderID}, 打手 {dashou_id}, 金额 {FineAmount}")
return Response({'code': 0, 'msg': '罚款已生成', 'data': {'id': penalty.id}})
except Exception as e:
logger.error(f"客服罚款申请异常: {traceback.format_exc()}")
return Response({'code': 500, 'msg': '系统繁忙'})
class PunishDashouView(APIView):
"""
客服处罚打手接口(扣除积分)
POST /houtai/kpcf
权限: JWT认证 + 客服权限验证
请求参数:
username (必填) : 客服用户名(手机号)
OrderID (必填) : 订单ID
reason (可选) : 处罚原因
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
try:
username = request.data.get('username', '').strip()
OrderID = request.data.get('OrderID', '').strip()
reason = request.data.get('reason', '').strip()
if not all([username, OrderID]):
return Response({'code': 400, 'msg': '参数不完整'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
# 检查权限(打手身份 shenfen=1
if not check_fadan_permission(permissions, 1):
return Response({'code': 403, 'msg': '无权限处罚打手'})
# 查询订单
try:
order = Order.query.get(OrderID=OrderID)
except Order.DoesNotExist:
return Response({'code': 404, 'msg': '订单不存在'})
# 获取打手ID
dashou_id = order.PlayerID
if not dashou_id:
return Response({'code': 400, 'msg': '订单未接单,无法处罚打手'})
# 查询打手
try:
dashou_user = User.query.get(UserUID=dashou_id)
dashou = dashou_user.DashouProfile
except User.DoesNotExist:
return Response({'code': 404, 'msg': '打手用户不存在'})
except ObjectDoesNotExist:
return Response({'code': 400, 'msg': '该用户不是打手'})
# 创建处罚记录扣除积分5分
jifen = 5
with transaction.atomic():
penalty_record = PenaltyRecord.query.create(
PlayerID=dashou_id,
ApplicantID=username,
PenaltyReason=reason or '',
ApplyStatus=0,
DeductedPoints=jifen,
OrderID=OrderID,
ClubID=resolve_penalty_record_club_id(
order_id=OrderID,
player_id=dashou_id,
request=request,
user=request.user,
applicant_id=getattr(request.user, 'UserUID', None) or username,
),
)
# 扣除打手积分
dashou.jifen = F('jifen') - jifen
dashou.save(update_fields=['jifen'])
logger.info(f"客服处罚打手成功: 打手 {dashou_id}, 订单 {OrderID}, 扣除积分 {jifen}")
return Response({'code': 0, 'msg': '处罚成功', 'data': {'penalty_record_id': penalty_record.id}})
except Exception as e:
logger.error(f"客服处罚打手异常: {traceback.format_exc()}")
return Response({'code': 500, 'msg': '系统繁忙'})

1140
backend/views/products.py Normal file

File diff suppressed because it is too large Load Diff

806
backend/views/roles.py Normal file
View File

@@ -0,0 +1,806 @@
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']
def _serialize_permission_row(perm_row):
"""gvsdsdk Permission → 后台 Vue 约定的小写字段"""
if isinstance(perm_row, dict):
code = perm_row.get('PermCode') or perm_row.get('perm_code') or ''
name = perm_row.get('PermName') or perm_row.get('perm_name') or code
desc = perm_row.get('PermDesc') or perm_row.get('perm_desc') or ''
else:
code = perm_row.PermCode or ''
name = perm_row.PermName or code
desc = perm_row.PermDesc or ''
return {'perm_code': code, 'perm_name': name, 'perm_desc': desc}
class GetRolePermissionView(APIView):
"""
获取所有角色、权限以及角色关联的用户数
权限要求:用户必须拥有 000001 权限
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 400, 'msg': '缺少username'})
# 1. 权限校验(验证客服身份,并获取权限列表)
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions # 直接返回错误响应
# 2. 检查是否有 000001 权限
if '000001' not in permissions:
return Response({'code': 403, 'msg': '您无权访问此页面'})
# 3. 获取所有权限
all_perms = Permission.objects.filter(PermStatus=1).values('PermCode', 'PermName', 'PermDesc')
permissions_list = [_serialize_permission_row(p) for p in all_perms]
from jituan.services.club_rbac import count_role_users_in_scope, filter_roles_for_request
kefu_user_uuids = list(
User.objects.filter(KefuProfile__isnull=False).values_list('UserUUID', flat=True)
)
roles = filter_roles_for_request(request)
roles_data = []
for role in roles:
user_count = count_role_users_in_scope(role, request, kefu_user_uuids)
role_perm_uuids = RolePermission.objects.filter(
RoleUUID=role.RoleUUID
).values_list('PermUUID', flat=True)
perms_of_role = [
_serialize_permission_row(p)
for p in Permission.objects.filter(
PermUUID__in=list(role_perm_uuids)
).values('PermCode', 'PermName', 'PermDesc')
]
roles_data.append({
'role_code': role.RoleName,
'role_name': role.RoleName,
'description': role.RoleDesc or '',
'club_id': getattr(role, 'ClubID', '') or '',
'user_count': user_count,
'permissions': perms_of_role
})
return Response({
'code': 0,
'data': {
'roles': roles_data,
'permissions': permissions_list
}
})
class ModifyRolePermissionView(APIView):
"""
通用接口:支持删除角色、为角色添加权限、移除角色权限
操作类型通过 action 字段区分
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username', '').strip()
action = request.data.get('action') # delete_role, add_permission, remove_permission
if not username or not action:
return Response({'code': 400, 'msg': '缺少必要参数'})
# 权限校验
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '000001' not in permissions:
return Response({'code': 403, 'msg': '您无权进行此操作'})
# 获取角色(需要角色名称)
role_code = request.data.get('role_code')
if not role_code:
return Response({'code': 400, 'msg': '缺少角色编码'})
try:
role = Role.objects.get(RoleName=role_code)
except Role.DoesNotExist:
return Response({'code': 404, 'msg': '角色不存在'})
# 超级管理员角色的特殊限制
if role.RoleName == '管理员':
if action in ['delete_role']:
return Response({'code': 403, 'msg': '超级管理员角色不能删除'})
# 根据 action 处理
if action == 'delete_role':
# 删除角色:同时删除角色权限关联、用户角色关联
with transaction.atomic():
RolePermission.objects.filter(RoleUUID=role.RoleUUID).delete()
UserRole.objects.filter(RoleUUID=role.RoleUUID).delete()
role.delete()
return Response({'code': 0, 'msg': '角色删除成功'})
elif action == 'add_permission':
perm_code = request.data.get('perm_code')
if not perm_code:
return Response({'code': 400, 'msg': '缺少权限编码'})
if role.RoleName == '管理员' and perm_code == '000001':
return Response({'code': 403, 'msg': '此权限已存在,不能重复添加'})
try:
perm = Permission.objects.get(PermCode=perm_code)
except Permission.DoesNotExist:
return Response({'code': 404, 'msg': '权限不存在'})
# 检查是否已存在
if RolePermission.objects.filter(RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID).exists():
return Response({'code': 400, 'msg': '该角色已拥有此权限'})
RolePermission.objects.create(
RolePermUUID=uuid.uuid4().bytes,
RoleUUID=role.RoleUUID,
PermUUID=perm.PermUUID,
CreateTime=timezone.now(),
)
return Response({'code': 0, 'msg': '权限添加成功'})
elif action == 'remove_permission':
perm_code = request.data.get('perm_code')
if not perm_code:
return Response({'code': 400, 'msg': '缺少权限编码'})
if role.RoleName == '管理员' and perm_code == '000001':
return Response({'code': 403, 'msg': '不能移除超级管理员的此权限'})
try:
perm = Permission.objects.get(PermCode=perm_code)
except Permission.DoesNotExist:
return Response({'code': 404, 'msg': '权限不存在'})
deleted, _ = RolePermission.objects.filter(RoleUUID=role.RoleUUID, PermUUID=perm.PermUUID).delete()
if deleted == 0:
return Response({'code': 400, 'msg': '该角色未拥有此权限'})
return Response({'code': 0, 'msg': '权限移除成功'})
elif action == 'update_role':
role_name = (request.data.get('role_name') or '').strip()
description = (request.data.get('description') or '').strip()
perm_codes = request.data.get('perm_codes') or []
if role.RoleName == '管理员':
role_name = role.RoleName
elif role_name and role_name != role.RoleName:
if Role.objects.filter(RoleName=role_name).exclude(RoleUUID=role.RoleUUID).exists():
return Response({'code': 400, 'msg': '角色名称已存在'})
with transaction.atomic():
if role.RoleName != '管理员':
if role_name:
role.RoleName = role_name
role.RoleDesc = description
role.save(update_fields=['RoleName', 'RoleDesc'])
RolePermission.objects.filter(RoleUUID=role.RoleUUID).delete()
for pc in perm_codes:
if pc == '000001' and role.RoleName != '管理员':
continue
try:
perm = Permission.objects.get(PermCode=pc, PermStatus=1)
except Permission.DoesNotExist:
continue
RolePermission.objects.create(
RolePermUUID=uuid.uuid4().bytes,
RoleUUID=role.RoleUUID,
PermUUID=perm.PermUUID,
CreateTime=timezone.now(),
)
if role.RoleName == '管理员':
try:
super_perm = Permission.objects.get(PermCode='000001', PermStatus=1)
if not RolePermission.objects.filter(
RoleUUID=role.RoleUUID, PermUUID=super_perm.PermUUID,
).exists():
RolePermission.objects.create(
RolePermUUID=uuid.uuid4().bytes,
RoleUUID=role.RoleUUID,
PermUUID=super_perm.PermUUID,
CreateTime=timezone.now(),
)
except Permission.DoesNotExist:
pass
return Response({'code': 0, 'msg': '角色保存成功'})
else:
return Response({'code': 400, 'msg': '无效的 action'})
class AddRoleView(APIView):
"""
添加角色:自动生成角色编码,前端提供角色名称、描述、选择的权限列表
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username', '').strip()
role_name = request.data.get('role_name', '').strip()
description = request.data.get('description', '').strip()
perm_codes = request.data.get('perm_codes', []) # 列表
if not username or not role_name:
return Response({'code': 400, 'msg': '缺少必要参数'})
# 权限校验
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '000001' not in permissions:
return Response({'code': 403, 'msg': '您无权进行此操作'})
# 检查角色名称是否已存在(同俱乐部内唯一)
from jituan.services.club_rbac import resolve_role_club_id, role_name_exists
target_club_id = resolve_role_club_id(
request, (request.data.get('target_club_id') or '').strip() or None,
)
if role_name_exists(role_name, target_club_id):
return Response({'code': 400, 'msg': '该俱乐部下角色名称已存在'})
# 生成唯一角色编码格式ROLE_ + 8位随机字母数字
role_code = f"ROLE_{''.join(random.choices(string.ascii_uppercase + string.digits, k=8))}"
# 创建角色
with transaction.atomic():
role = Role.objects.create(
RoleUUID=uuid.uuid4().bytes,
TenantUUID=b'\x00' * 16,
RoleName=role_name,
RoleType=2,
RoleStatus=1,
AssignScope=0,
RoleDesc=description,
ClubID=target_club_id,
CreateTime=timezone.now(),
)
# 绑定权限(过滤掉不存在的权限编码)
for pc in perm_codes:
# 不允许添加 000001 权限(此权限仅供超级管理员)
if pc == '000001':
continue
try:
perm = Permission.objects.get(PermCode=pc)
RolePermission.objects.create(
RolePermUUID=uuid.uuid4().bytes,
RoleUUID=role.RoleUUID,
PermUUID=perm.PermUUID,
CreateTime=timezone.now(),
)
except Permission.DoesNotExist:
logger.warning(f"添加角色时忽略不存在的权限编码: {pc}")
return Response({'code': 0, 'msg': '角色添加成功', 'data': {'role_code': role_code}})
class GetAdminUserListView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
try:
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 400, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '000001' not in permissions:
return Response({'code': 403, 'msg': '您无权访问此页面'})
phone = request.data.get('phone', '').strip()
nicheng = request.data.get('nicheng', '').strip()
role_code = request.data.get('roleCode', '').strip()
page = int(request.data.get('page', 1))
page_size = int(request.data.get('pageSize', 20))
# UserType 是 @property 非数据库字段,不能用 filter
# KefuProfile__isnull=False 已确保只查到客服用户
users = User.query.filter(
KefuProfile__isnull=False
).select_related('KefuProfile')
from jituan.services.club_rbac import filter_kefu_users_qs
users = filter_kefu_users_qs(users, request)
if phone:
users = users.filter(Phone__icontains=phone)
if nicheng:
users = users.filter(KefuProfile__nicheng__icontains=nicheng)
# 关键修正:先求值为列表,再传入 phone__in
if role_code:
target_role = Role.objects.filter(RoleName=role_code).first()
if target_role:
user_uuids = list(
UserRole.objects.filter(RoleUUID=target_role.RoleUUID)
.values_list('UserUUID', flat=True)
)
users = users.filter(UserUUID__in=user_uuids)
else:
users = users.none()
users = users.order_by('-UserCreateTime')
paginator = Paginator(users, page_size)
page_obj = paginator.get_page(page)
data_list = []
for user in page_obj:
# 通过 gvsdsdk UserRole 获取用户角色
user_role_uuids = list(
UserRole.objects.filter(UserUUID=user.UserUUID)
.values_list('RoleUUID', flat=True)
)
user_roles = Role.objects.filter(RoleUUID__in=user_role_uuids)
roles_data = [
{'role_code': r.RoleName, 'role_name': r.RoleName}
for r in user_roles
]
data_list.append({
'phone': user.Phone or '',
'yonghuid': user.UserUID or '',
'nicheng': user.KefuProfile.nicheng,
'club_id': getattr(user, 'ClubID', '') or '',
'roles': roles_data,
'status': user.KefuProfile.zhuangtai,
'create_time': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '',
'update_time': user.KefuProfile.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user.KefuProfile, 'UpdateTime', None) else '',
'CreateTime': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '',
'UpdateTime': user.KefuProfile.UpdateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user.KefuProfile, 'UpdateTime', None) else '',
})
return Response({
'code': 0,
'data': {
'list': data_list,
'total': paginator.count,
'page': page,
'pageSize': page_size
}
})
except Exception as e:
logger.error(f"GetAdminUserListView 异常: {e}\n{traceback.format_exc()}")
return Response({'code': 500, 'msg': f'服务器内部错误: {str(e)}'})
class GetAdminRolesView(APIView):
"""获取所有角色(用于筛选和分配)"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 400, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '000001' not in permissions:
return Response({'code': 403, 'msg': '您无权访问此页面'})
from jituan.services.club_rbac import filter_roles_for_request
roles = filter_roles_for_request(request).values('RoleName', 'ClubID')
return Response({
'code': 0,
'data': {
'roles': [
{
'role_code': r['RoleName'],
'role_name': r['RoleName'],
'club_id': r.get('ClubID') or '',
}
for r in roles
]
}
})
'''class GetAdminUserListView(APIView):
"""获取后台用户列表(支持分页、筛选)"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 400, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '000001' not in permissions:
return Response({'code': 403, 'msg': '您无权访问此页面'})
# 筛选条件
phone = request.data.get('phone', '').strip()
nicheng = request.data.get('nicheng', '').strip()
role_code = request.data.get('roleCode', '').strip()
page = int(request.data.get('page', 1))
page_size = int(request.data.get('pageSize', 20))
# 基础查询:存在客服扩展表
users = User.query.filter(KefuProfile__isnull=False).select_related('KefuProfile')
if phone:
users = users.filter(Phone__icontains=phone)
if nicheng:
users = users.filter(KefuProfile__nicheng__icontains=nicheng)
if role_code:
# 筛选拥有该角色的用户
user_ids_with_role = UserRole.query.filter(role__role_code=role_code).values_list('account_id', flat=True)
users = users.filter(phone__in=user_ids_with_role)
# 排序
users = users.order_by('-CreateTime')
# 分页
paginator = Paginator(users, page_size)
page_obj = paginator.get_page(page)
# 构建返回数据
data_list = []
for user in page_obj:
# 获取用户的所有角色
user_roles = UserRole.query.filter(account_id=user.Phone).select_related('role')
roles_data = [{'role_code': ur.role.role_code, 'role_name': ur.role.role_name} for ur in user_roles]
data_list.append({
'phone': user.Phone,
'nicheng': user.KefuProfile.nicheng,
'roles': roles_data,
'status': user.KefuProfile.zhuangtai,
'CreateTime': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S'),
'UpdateTime': user.UpdateTime.strftime('%Y-%m-%d %H:%M:%S'),
})
return Response({
'code': 0,
'data': {
'list': data_list,
'total': paginator.count,
'page': page,
'pageSize': page_size
}
})'''
class ModifyAdminUserView(APIView):
"""修改后台用户信息(昵称、密码、二级密码、状态、角色)"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username', '').strip()
action = request.data.get('action')
if not username or not action:
return Response({'code': 400, 'msg': '缺少参数'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '000001' not in permissions:
return Response({'code': 403, 'msg': '您无权进行此操作'})
target_yonghuid = request.data.get('yonghuid', '').strip()
target_phone = request.data.get('phone', '').strip()
if not target_yonghuid and not target_phone:
return Response({'code': 400, 'msg': '缺少目标用户yonghuid 或 phone'})
from jituan.services.club_rbac import resolve_kefu_admin_user
target_user = resolve_kefu_admin_user(yonghuid=target_yonghuid, phone=target_phone)
if not target_user:
return Response({'code': 404, 'msg': '用户不存在或非后台账号'})
try:
target_kefu = target_user.KefuProfile
except UserKefu.DoesNotExist:
return Response({'code': 404, 'msg': '用户扩展信息缺失'})
with transaction.atomic():
if action == 'update_user_info':
# 修改昵称、状态、密码、二级密码
nicheng = request.data.get('nicheng')
status = request.data.get('status')
password = request.data.get('password')
erjimima = request.data.get('erjimima')
if nicheng is not None:
target_kefu.nicheng = nicheng
if status is not None:
target_kefu.zhuangtai = int(status)
if password:
if len(password) < 6:
return Response({'code': 400, 'msg': '密码至少6位'})
target_user.SetPassword(password)
if erjimima:
if len(erjimima) < 6:
return Response({'code': 400, 'msg': '二级密码至少6位'})
import bcrypt as _bcrypt
target_kefu.erjimima = _bcrypt.hashpw(erjimima.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8')
target_user.save()
target_kefu.save()
return Response({'code': 0, 'msg': '修改成功'})
elif action == 'add_user_roles':
role_codes = request.data.get('role_codes', [])
if not role_codes:
return Response({'code': 400, 'msg': '请选择角色'})
from jituan.services.club_rbac import resolve_role_by_name
club_id = getattr(target_user, 'ClubID', '') or ''
added, missing = [], []
for rc in role_codes:
role = resolve_role_by_name(rc, club_id)
if not role:
missing.append(rc)
continue
_, created = UserRole.objects.get_or_create(
UserUUID=target_user.UserUUID,
RoleUUID=role.RoleUUID,
defaults={
'UserRoleUUID': uuid.uuid4().bytes,
'CompanyUUID': uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes,
},
)
if created:
added.append(role.RoleName)
elif role.RoleName not in added:
added.append(role.RoleName)
if not added:
return Response({
'code': 400,
'msg': f'未找到可绑定的角色:{", ".join(missing or role_codes)}。请确认顶栏俱乐部与角色所属俱乐部一致',
})
if missing:
return Response({
'code': 0,
'msg': f'已绑定:{", ".join(added)};未找到:{", ".join(missing)}',
})
return Response({'code': 0, 'msg': '角色添加成功'})
elif action == 'remove_user_role':
role_code = request.data.get('role_code')
if not role_code:
return Response({'code': 400, 'msg': '缺少角色编码'})
from jituan.services.club_rbac import resolve_role_by_name
club_id = getattr(target_user, 'ClubID', '') or ''
role = resolve_role_by_name(role_code, club_id)
if not role:
return Response({'code': 404, 'msg': '角色不存在或与用户俱乐部不匹配'})
UserRole.objects.filter(UserUUID=target_user.UserUUID, RoleUUID=role.RoleUUID).delete()
return Response({'code': 0, 'msg': '角色移除成功'})
else:
return Response({'code': 400, 'msg': '无效的action'})
class AddAdminUserView(APIView):
"""添加后台用户(客服账号)"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 400, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if '000001' not in permissions:
return Response({'code': 403, 'msg': '您无权进行此操作'})
phone = request.data.get('phone', '').strip()
password = request.data.get('password', '').strip()
erjimima = request.data.get('erjimima', '').strip()
nicheng = request.data.get('nicheng', '').strip()
role_codes = request.data.get('role_codes', [])
if not all([phone, password, erjimima, nicheng]):
return Response({'code': 400, 'msg': '请填写完整信息'})
if len(password) < 6 or len(erjimima) < 6:
return Response({'code': 400, 'msg': '密码和二级密码至少6位'})
from jituan.services.club_context import resolve_club_id_from_request
from jituan.services.club_rbac import resolve_roles_for_binding
target_club_id = resolve_club_id_from_request(request)
# 校验手机号是否已被后台客服占用(同俱乐部优先)
existing_kefu = User.query.filter(
Phone=phone, KefuProfile__isnull=False
).order_by('-UserCreateTime').first()
if existing_kefu:
return Response({'code': 400, 'msg': '该账号已存在'})
if not isinstance(role_codes, list):
return Response({'code': 400, 'msg': 'role_codes 格式错误'})
# 分配角色(默认客服 + 所选角色,去重;任一角色找不到则整单拒绝)
role_names_to_bind = []
seen_names = set()
for name in (['客服'] if '客服' not in role_codes else []) + list(role_codes):
n = (name or '').strip()
if n and n not in seen_names:
seen_names.add(n)
role_names_to_bind.append(n)
roles_to_bind, missing_roles = resolve_roles_for_binding(role_names_to_bind, target_club_id)
if missing_roles:
return Response({
'code': 400,
'msg': f'角色不存在或与当前俱乐部不匹配:{", ".join(missing_roles)}',
})
if not roles_to_bind:
return Response({'code': 400, 'msg': '请至少选择一个有效角色'})
with transaction.atomic():
# 事务内二次校验,防止并发重复创建
if User.query.filter(Phone=phone, KefuProfile__isnull=False).exists():
return Response({'code': 400, 'msg': '该账号已存在'})
# 生成唯一的 yonghuid7位数字
while True:
yonghuid = str(random.randint(1000000, 9999999))
if not User.query.filter(UserUID=yonghuid).exists():
break
# 生成唯一的 openid格式admin_{时间戳}_{随机6位}
timestamp = str(int(time.time() * 1000))
random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
openid = f"admin_{timestamp}_{random_suffix}"
while User.query.filter(OpenID=openid).exists():
random_suffix = ''.join(random.choices(string.ascii_lowercase + string.digits, k=6))
openid = f"admin_{timestamp}_{random_suffix}"
user_main = User.query.create(
UserUID=yonghuid,
UserName=openid,
OpenID=openid,
Phone=phone,
ClubID=target_club_id,
)
user_main.SetPassword(password)
user_main.save(update_fields=['UserPassword', 'ClubID'])
# 创建客服扩展表(后台用户扩展)
import bcrypt as _bcrypt
erjimima_hashed = _bcrypt.hashpw(erjimima.encode('utf-8'), _bcrypt.gensalt(rounds=12)).decode('utf-8')
UserKefu.query.create(
user=user_main,
nicheng=nicheng,
erjimima=erjimima_hashed,
zhuangtai=1
)
for role in roles_to_bind:
UserRole.objects.get_or_create(
UserUUID=user_main.UserUUID,
RoleUUID=role.RoleUUID,
defaults={
'UserRoleUUID': uuid.uuid4().bytes,
'CompanyUUID': uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes,
},
)
return Response({'code': 0, 'msg': '添加成功'})

354
backend/views/sections.py Normal file
View File

@@ -0,0 +1,354 @@
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 HqbkxxView(APIView):
"""
获取板块配置所需全部数据
POST /houtai/hqbkxx
Body: { "phone": "管理员手机号" }
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 1. 从前端获取传递的账号,用于防越权校验
username_from_frontend = request.data.get('phone', None)
# 2. 调用公共验证方法
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
# 3. 如果第一个返回值为 None说明验证失败直接返回第二个值
if kefu_obj is None:
return permissions
# ---------- 验证通过 ----------
# 4. 检查是否有板块管理权限
if 'bankuai' not in permissions:
return Response({'code': 403, 'msg': '无板块管理权限'}, status=403)
scope_err = block_non_group_catalog_scope(request)
if scope_err:
return Response({'code': 400, 'msg': scope_err})
# 5. 查询所有板块
bankuai_list = []
for bk in Bankuai.query.all():
bankuai_list.append({
'bankuai_id': bk.bankuai_id,
'mingcheng': bk.mingcheng,
'CreateTime': bk.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if bk.CreateTime else ''
})
# 6. 查询所有商品类型
shangpin_leixing_list = []
for sp in ShangpinLeixing.query.all():
shangpin_leixing_list.append({
'id': sp.id,
'jieshao': sp.jieshao,
'tupian_url': sp.tupian_url or '',
'bankuai_id': sp.bankuai_id
})
# 7. 查询会员(附带当前俱乐部售价)
from jituan.constants import CLUB_ID_DEFAULT, DATA_SCOPE_ALL
from jituan.models import ClubHuiyuanPrice
from jituan.services.club_context import resolve_club_id_from_request, resolve_club_scope
scope = resolve_club_scope(request)
club_id = resolve_club_id_from_request(request)
price_map = {}
if scope != DATA_SCOPE_ALL:
for row in ClubHuiyuanPrice.query.filter(club_id=club_id):
price_map[row.huiyuan_id] = row
huiyuan_list = []
for hy in Huiyuan.query.all():
row = price_map.get(hy.huiyuan_id)
if scope != DATA_SCOPE_ALL and club_id != CLUB_ID_DEFAULT and row is None:
continue
if row is not None and not row.is_enabled:
continue
item = {
'huiyuan_id': hy.huiyuan_id,
'jieshao': hy.jieshao,
'bankuai_id': hy.bankuai_id,
'jiage': str(row.jiage) if row else str(hy.jiage),
'guanshifc': str(row.guanshifc) if row else str(hy.guanshifc),
'zuzhangfc': str(row.zuzhangfc) if row else str(hy.zuzhangfc),
}
huiyuan_list.append(item)
return Response({
'code': 0,
'msg': 'ok',
'data': {
'bankuai': bankuai_list,
'shangpin_leixing': shangpin_leixing_list,
'huiyuan': huiyuan_list,
'club_id': club_id,
'scope': scope,
}
})
class BkxgView(APIView):
"""
板块增删改统一接口
POST /houtai/bkxg
Body: {
"phone": "管理员手机号",
"action": "create | update | delete | add_items | remove_items",
"bankuai_id": int,
"bankuai_name": str,
"item_type": "shangpin_leixing | huiyuan",
"item_ids": [1,2,3]
}
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 1. 从前端获取传递的账号
username_from_frontend = request.data.get('phone', None)
# 2. 调用公共验证方法
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
# 3. 验证失败直接返回
if kefu_obj is None:
return permissions
# 4. 检查板块管理权限
if 'bankuai' not in permissions:
return Response({'code': 403, 'msg': '无板块管理权限'}, status=403)
scope_err = block_non_group_catalog_scope(request)
if scope_err:
return Response({'code': 400, 'msg': scope_err})
# 5. 获取操作类型
action = request.data.get('action')
if not action:
return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400)
# ==================== 创建板块 ====================
if action == 'create':
bankuai_name = request.data.get('bankuai_name', '').strip()
if not bankuai_name:
return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400)
if Bankuai.query.filter(mingcheng=bankuai_name).exists():
return Response({'code': 1, 'msg': '板块名称已存在'}, status=400)
shangpin_ids = request.data.get('shangpin_ids', [])
huiyuan_ids = request.data.get('huiyuan_ids', [])
with transaction.atomic():
new_bk = Bankuai.query.create(mingcheng=bankuai_name)
if shangpin_ids:
ShangpinLeixing.query.filter(
id__in=shangpin_ids,
bankuai__isnull=True
).update(bankuai=new_bk)
if huiyuan_ids:
Huiyuan.query.filter(
huiyuan_id__in=huiyuan_ids,
bankuai__isnull=True
).update(bankuai=new_bk)
return Response({
'code': 0,
'msg': '创建成功',
'data': {'bankuai_id': new_bk.bankuai_id}
})
# ==================== 以下操作需要 bankuai_id ====================
bankuai_id = request.data.get('bankuai_id')
if not bankuai_id:
return Response({'code': 1, 'msg': '缺少 bankuai_id'}, status=400)
try:
bk = Bankuai.query.get(bankuai_id=bankuai_id)
except Bankuai.DoesNotExist:
return Response({'code': 1, 'msg': '板块不存在'}, status=404)
# ==================== 修改板块名称 ====================
if action == 'update':
new_name = request.data.get('bankuai_name', '').strip()
if not new_name:
return Response({'code': 1, 'msg': '板块名称不能为空'}, status=400)
if Bankuai.query.filter(mingcheng=new_name).exclude(bankuai_id=bankuai_id).exists():
return Response({'code': 1, 'msg': '板块名称已存在'}, status=400)
bk.mingcheng = new_name
bk.save()
return Response({'code': 0, 'msg': '修改成功'})
# ==================== 删除板块 ====================
elif action == 'delete':
with transaction.atomic():
ShangpinLeixing.query.filter(bankuai=bk).update(bankuai=None)
Huiyuan.query.filter(bankuai=bk).update(bankuai=None)
bk.delete()
return Response({'code': 0, 'msg': '删除成功'})
# ==================== 添加绑定 ====================
elif action == 'add_items':
item_type = request.data.get('item_type')
item_ids = request.data.get('item_ids', [])
if not item_type or not item_ids:
return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400)
if item_type == 'shangpin_leixing':
ShangpinLeixing.query.filter(
id__in=item_ids,
bankuai__isnull=True
).update(bankuai=bk)
elif item_type == 'huiyuan':
Huiyuan.query.filter(
huiyuan_id__in=item_ids,
bankuai__isnull=True
).update(bankuai=bk)
else:
return Response({'code': 1, 'msg': '无效的 item_type'}, status=400)
return Response({'code': 0, 'msg': '添加成功'})
# ==================== 移除绑定 ====================
elif action == 'remove_items':
item_type = request.data.get('item_type')
item_ids = request.data.get('item_ids', [])
if not item_type or not item_ids:
return Response({'code': 1, 'msg': '缺少 item_type 或 item_ids'}, status=400)
if item_type == 'shangpin_leixing':
ShangpinLeixing.query.filter(
id__in=item_ids,
bankuai=bk
).update(bankuai=None)
elif item_type == 'huiyuan':
Huiyuan.query.filter(
huiyuan_id__in=item_ids,
bankuai=bk
).update(bankuai=None)
else:
return Response({'code': 1, 'msg': '无效的 item_type'}, status=400)
return Response({'code': 0, 'msg': '移除成功'})
else:
return Response({'code': 1, 'msg': '未知操作'}, status=400)

939
backend/views/shops.py Normal file
View File

@@ -0,0 +1,939 @@
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 ShopListView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
# 1. 调用公共权限验证(与您给的示例完全一致)
username_frontend = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username_frontend)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
# 2. 检查是否拥有所需权限999a~999e 任意一个)
required_perms = {'999a', '999b', '999c', '999d', '999e'}
if not required_perms.intersection(set(permissions)):
return Response({'code': 403, 'msg': '您没有权限访问店铺管理功能'})
# 3. 解析请求参数
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 10))
dianpu_mingcheng = request.data.get('dianpu_mingcheng', '').strip()
zhuangtai = request.data.get('zhuangtai')
lianxi_dianhua = request.data.get('lianxi_dianhua', '').strip()
user_id = request.data.get('user_id', '').strip()
ketixian_yue_min = request.data.get('ketixian_yue_min')
ketixian_yue_max = request.data.get('ketixian_yue_max')
zhengqu_zonge_min = request.data.get('zhengqu_zonge_min')
zhengqu_zonge_max = request.data.get('zhengqu_zonge_max')
# 4. 构建查询集(先查店铺,预加载关联凭证)
queryset = Dianpu.query.select_related('pingzheng').all()
if dianpu_mingcheng:
queryset = queryset.filter(dianpu_mingcheng__icontains=dianpu_mingcheng)
if zhuangtai is not None:
queryset = queryset.filter(zhuangtai=zhuangtai)
if lianxi_dianhua:
queryset = queryset.filter(lianxi_dianhua__icontains=lianxi_dianhua)
if user_id:
queryset = queryset.filter(pingzheng__yonghu=user_id)
# 金额范围过滤
if ketixian_yue_min is not None:
queryset = queryset.filter(ketixian_yue__gte=ketixian_yue_min)
if ketixian_yue_max is not None:
queryset = queryset.filter(ketixian_yue__lte=ketixian_yue_max)
if zhengqu_zonge_min is not None:
queryset = queryset.filter(zhengqu_zonge__gte=zhengqu_zonge_min)
if zhengqu_zonge_max is not None:
queryset = queryset.filter(zhengqu_zonge__lte=zhengqu_zonge_max)
# 5. 分页
paginator = Paginator(queryset, page_size)
page_obj = paginator.get_page(page)
# 6. 构造返回数据
shop_list = []
for dianpu in page_obj:
pingzheng = dianpu.pingzheng
shop_list.append({
'id': dianpu.id,
'dianpu_mingcheng': dianpu.dianpu_mingcheng,
'dianpu_touxiang': dianpu.dianpu_touxiang or '',
'lianxi_dianhua': dianpu.lianxi_dianhua or '',
'weixinhao': dianpu.weixinhao or '',
'zhuangtai': dianpu.zhuangtai,
'bangding_yonghushu': dianpu.bangding_yonghushu,
'erweima_url': dianpu.erweima_url or '',
'kaiqi_shouyi_choucheng': dianpu.kaiqi_shouyi_choucheng,
'shouyi_choucheng_feilv': dianpu.shouyi_choucheng_feilv or 0,
'ketixian_yue': dianpu.ketixian_yue,
'zhengqu_zonge': dianpu.zhengqu_zonge,
'meiri_tixian_xiane': dianpu.meiri_tixian_xiane or 0,
'kaiqi_meiri_xiane': dianpu.kaiqi_meiri_xiane,
'pingzheng__yonghu': pingzheng.yonghu, # 用户ID
'pingzheng__zhanghao': pingzheng.zhanghao, # 登录账号
})
# 获取默认配置ID=1不存在则创建
default_config, _ = DianpuMorenPeizhi.query.get_or_create(id=1, defaults={
'moren_choucheng_feilv': 0.1,
'moren_tixian_xiane': 1000.00
})
return Response({
'code': 0,
'msg': 'success',
'data': {
'list': shop_list,
'total': paginator.count,
'default_config': {
'moren_choucheng_feilv': default_config.moren_choucheng_feilv,
'moren_tixian_xiane': default_config.moren_tixian_xiane,
}
}
})
class ShopModifyView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
# 1. 身份验证
username_frontend = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username_frontend)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
# 转换权限为集合方便判断
user_perms = set(permissions)
action = request.data.get('action', '')
# 2. 根据 action 分派任务
if action == 'update_dianpu':
return self._update_dianpu(request, user_perms)
elif action == 'add_dianpu':
return self._add_dianpu(request, user_perms)
elif action == 'update_default':
return self._update_default(request, user_perms)
else:
return Response({'code': 400, 'msg': '未知操作类型'})
def _update_dianpu(self, request, user_perms):
"""修改店铺信息,精确权限控制"""
dianpu_id = request.data.get('dianpu_id')
if not dianpu_id:
return Response({'code': 400, 'msg': '缺少店铺ID'})
try:
with transaction.atomic():
dianpu = Dianpu.objects.select_for_update().get(id=dianpu_id)
# 基础信息修改 (999e)
if any(k in request.data for k in ['dianpu_mingcheng', 'lianxi_dianhua', 'weixinhao']):
if '999e' not in user_perms:
return Response({'code': 403, 'msg': '无权限修改店铺基本信息(999e)'})
if 'dianpu_mingcheng' in request.data:
dianpu.dianpu_mingcheng = request.data['dianpu_mingcheng']
if 'lianxi_dianhua' in request.data:
dianpu.lianxi_dianhua = request.data['lianxi_dianhua']
if 'weixinhao' in request.data:
dianpu.weixinhao = request.data['weixinhao']
# 店铺状态 (999a)
if 'zhuangtai' in request.data:
if '999a' not in user_perms:
return Response({'code': 403, 'msg': '无权限修改店铺状态(999a)'})
new_status = int(request.data['zhuangtai'])
if new_status not in (0, 1):
return Response({'code': 400, 'msg': '店铺状态无效'})
dianpu.zhuangtai = new_status
# 可提现余额 (999b)
if 'ketixian_yue' in request.data:
if '999b' not in user_perms:
return Response({'code': 403, 'msg': '无权限修改可提现余额(999b)'})
dianpu.ketixian_yue = request.data['ketixian_yue']
# 提现限额相关 (999c)
if 'kaiqi_meiri_xiane' in request.data or 'meiri_tixian_xiane' in request.data:
if '999c' not in user_perms:
return Response({'code': 403, 'msg': '无权限修改提现限额配置(999c)'})
if 'kaiqi_meiri_xiane' in request.data:
dianpu.kaiqi_meiri_xiane = request.data['kaiqi_meiri_xiane']
if 'meiri_tixian_xiane' in request.data:
dianpu.meiri_tixian_xiane = request.data['meiri_tixian_xiane']
# 抽成相关 (999d)
if 'kaiqi_shouyi_choucheng' in request.data or 'shouyi_choucheng_feilv' in request.data:
if '999d' not in user_perms:
return Response({'code': 403, 'msg': '无权限修改抽成配置(999d)'})
if 'kaiqi_shouyi_choucheng' in request.data:
dianpu.kaiqi_shouyi_choucheng = request.data['kaiqi_shouyi_choucheng']
if 'shouyi_choucheng_feilv' in request.data:
dianpu.shouyi_choucheng_feilv = request.data['shouyi_choucheng_feilv']
dianpu.save() # Django 会自动判断更新哪些字段
return Response({'code': 0, 'msg': '店铺信息更新成功'})
except Dianpu.DoesNotExist:
return Response({'code': 404, 'msg': '店铺不存在'})
except Exception as e:
logger.exception("修改店铺失败")
return Response({'code': 500, 'msg': '修改失败,请稍后重试'})
def _add_dianpu(self, request, user_perms):
"""添加店铺,需要权限 999b"""
if '999b' not in user_perms:
return Response({'code': 403, 'msg': '无权限添加店铺(999b)'})
yonghu_id = request.data.get('yonghu_id')
zhanghao = request.data.get('zhanghao')
mima = request.data.get('mima')
dianpu_mingcheng = request.data.get('dianpu_mingcheng')
if not all([yonghu_id, zhanghao, mima, dianpu_mingcheng]):
return Response({'code': 400, 'msg': '必填字段缺失用户ID、账号、密码、店铺名称'})
# 检查用户是否存在
if not User.query.filter(UserUID=yonghu_id).exists():
return Response({'code': 404, 'msg': '用户不存在'})
# 检查凭证是否已存在
if YonghuPingzheng.query.filter(zhanghao=zhanghao).exists():
return Response({'code': 400, 'msg': '登录账号已存在'})
if YonghuPingzheng.query.filter(yonghu=yonghu_id).exists():
return Response({'code': 400, 'msg': '该用户ID已有店铺凭证'})
try:
with transaction.atomic():
pingzheng = YonghuPingzheng.query.create(
yonghu=yonghu_id,
zhanghao=zhanghao,
mima=mima,
is_active=True
)
dianpu = Dianpu.query.create(
pingzheng=pingzheng,
dianpu_mingcheng=dianpu_mingcheng,
lianxi_dianhua=request.data.get('lianxi_dianhua', ''),
weixinhao=request.data.get('weixinhao', ''),
kaiqi_shouyi_choucheng=request.data.get('kaiqi_shouyi_choucheng', False),
shouyi_choucheng_feilv=request.data.get('shouyi_choucheng_feilv', 0),
kaiqi_meiri_xiane=request.data.get('kaiqi_meiri_xiane', False),
meiri_tixian_xiane=request.data.get('meiri_tixian_xiane', 0),
)
return Response({'code': 0, 'msg': '店铺创建成功', 'dianpu_id': dianpu.id})
except IntegrityError as e:
logger.error("创建店铺违反唯一性约束: %s", e)
return Response({'code': 400, 'msg': '数据冲突请检查账号或用户ID'})
except Exception as e:
logger.exception("创建店铺失败")
return Response({'code': 500, 'msg': '创建失败,请稍后重试'})
def _update_default(self, request, user_perms):
"""修改默认配置,按修改内容校验权限"""
feilv = request.data.get('moren_choucheng_feilv')
xiane = request.data.get('moren_tixian_xiane')
modify_commission = feilv is not None
modify_withdraw = xiane is not None
if modify_commission and '999d' not in user_perms:
return Response({'code': 403, 'msg': '无权限修改默认抽成费率(999d)'})
if modify_withdraw and '999c' not in user_perms:
return Response({'code': 403, 'msg': '无权限修改默认可提现限额(999c)'})
try:
config, _ = DianpuMorenPeizhi.query.get_or_create(id=1)
if modify_commission:
config.moren_choucheng_feilv = feilv
if modify_withdraw:
config.moren_tixian_xiane = xiane
config.save(update_fields=(['moren_choucheng_feilv'] if modify_commission else []) +
(['moren_tixian_xiane'] if modify_withdraw else []))
return Response({'code': 0, 'msg': '默认配置更新成功'})
except Exception as e:
logger.exception("更新默认配置失败")
return Response({'code': 500, 'msg': '更新失败'})
# ==================== 1. 公共商品类型 + 审核模式 ====================
class ShopPublicTypeAndAuditView(APIView):
"""
返回:①正常状态的公共商品类型列表 ②全局商品审核模式开关
前端请求路径:/houtai/htdphqsplx
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# ---------- 身份与权限校验 ----------
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败,请检查登录状态'})
# 拥有 1199ab/1199abc/1199abd 任一个即可访问
if not set(permissions).intersection(SHOP_PRODUCT_PERMS):
return Response({'code': 403, 'msg': '无权访问店铺商品管理功能'})
# ---------- 查询公共商品类型(仅正常审核状态的) ----------
try:
# shenhezhuangtai = 1 表示正常 (根据模型默认值)
types_qs = ShangpinLeixing.query.filter(shenhezhuangtai=1).values(
'id', 'jieshao', 'tupian_url', 'yaoqiuleixing', 'huiyuan_id', 'yongjin'
)
type_list = []
for t in types_qs:
type_list.append({
'id': t['id'],
'jieshao': t['jieshao'] or '',
'tupian_url': t['tupian_url'] or '',
'yaoqiuleixing': t['yaoqiuleixing'],
'huiyuan_id': t['huiyuan_id'],
'yongjin': t['yongjin'],
})
except Exception as e:
logger.exception("查询公共商品类型失败")
return Response({'code': 500, 'msg': '服务器错误,请稍后重试'})
# ---------- 获取审核模式配置 ----------
audit_config = DianpuShangpinShenheShezhi.query.filter(id=1).first()
kaiqi_shenhe = audit_config.kaiqi_shenhe if audit_config else False
return Response({
'code': 0,
'data': {
'public_types': type_list,
'kaiqi_shenhe': kaiqi_shenhe
}
})
# ==================== 2. 店铺列表(支持搜索、筛选、分页) ====================
class ShopListForProductView(APIView):
"""
返回店铺列表,用于商品管理页面选择店铺。
支持关键词搜索店铺ID/名称/用户ID、店铺状态筛选、分页。
前端请求路径:/houtai/htdphqdpys
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
if not set(permissions).intersection(SHOP_PRODUCT_PERMS):
return Response({'code': 403, 'msg': '无权访问店铺列表'})
# ---------- 安全解析分页参数 ----------
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 10))
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '分页参数格式错误'})
# 搜索关键词字符串ORM 自动防注入)
keyword = str(request.data.get('keyword', '')).strip()
# 店铺状态筛选1正常0封禁不传则全部
zhuangtai = request.data.get('zhuangtai')
# ---------- 构建查询集关联凭证表获取用户ID ----------
shops = Dianpu.query.select_related('pingzheng').all()
# 状态筛选:只接受 0 或 1
if zhuangtai is not None:
try:
zhuangtai = int(zhuangtai)
if zhuangtai in [0, 1]:
shops = shops.filter(zhuangtai=zhuangtai)
else:
return Response({'code': 400, 'msg': '状态参数只能为0或1'})
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '状态参数必须为整数'})
# 关键词搜索店铺ID/名称/用户ID
if keyword:
shops = shops.filter(
models.Q(id__icontains=keyword) |
models.Q(dianpu_mingcheng__icontains=keyword) |
models.Q(pingzheng__yonghu__icontains=keyword)
)
# ---------- 分页 ----------
paginator = Paginator(shops, page_size)
page_obj = paginator.get_page(page)
shop_list = []
for shop in page_obj:
shop_list.append({
'id': shop.id,
'dianpu_mingcheng': shop.dianpu_mingcheng,
'zhuangtai': shop.zhuangtai,
'yonghu_id': shop.pingzheng.yonghu if shop.pingzheng else '',
'lianxi_dianhua': shop.lianxi_dianhua or '',
'weixinhao': shop.weixinhao or '',
})
return Response({
'code': 0,
'data': {
'list': shop_list,
'total': paginator.count
}
})
# ==================== 3. 店铺商品类型映射列表(含筛选、待审核计数) ====================
class ShopProductTypeMappingView(APIView):
"""
返回店铺商品类型映射ShangpinLeixingDianpu列表。
支持店铺ID、公共类型ID、上架/封禁/审核状态筛选,关键字搜索(介绍/ID/店铺名/公共类型名)。
每次请求同时返回“待审核总数”shenhe_zhuangtai=False 的记录数)。
前端请求路径:/houtai/htdphqyssplx
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
if not set(permissions).intersection(SHOP_PRODUCT_PERMS):
return Response({'code': 403, 'msg': '无权访问商品类型映射'})
# ---------- 安全解析参数 ----------
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 10))
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '分页参数格式错误'})
keyword = str(request.data.get('keyword', '')).strip()
shop_id = request.data.get('shop_id')
public_type_id = request.data.get('public_type_id')
# 布尔型筛选字段处理None 表示不筛选,否则转为布尔值
def parse_bool(val):
if val is None:
return None
return bool(val)
shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
# ---------- 查询(连表店铺和公共类型) ----------
mappings = ShangpinLeixingDianpu.query.select_related('dianpu', 'gonggong_leixing').all()
# 筛选条件:均为参数化,完全防注入
if shop_id is not None:
mappings = mappings.filter(dianpu_id=int(shop_id))
if public_type_id is not None:
mappings = mappings.filter(gonggong_leixing_id=int(public_type_id))
if shangjia is not None:
mappings = mappings.filter(shangjia_zhuangtai=shangjia)
if fengjin is not None:
mappings = mappings.filter(fengjin_zhuangtai=fengjin)
if shenhe is not None:
mappings = mappings.filter(shenhe_zhuangtai=shenhe)
# 关键字搜索介绍、ID、店铺名、公共类型名
if keyword:
mappings = mappings.filter(
models.Q(jieshao__icontains=keyword) |
models.Q(id__icontains=keyword) |
models.Q(dianpu__dianpu_mingcheng__icontains=keyword) |
models.Q(gonggong_leixing__jieshao__icontains=keyword)
)
# ---------- 待审核总数(不受分页影响) ----------
pending_total = mappings.filter(shenhe_zhuangtai=False).count()
# ---------- 分页 ----------
paginator = Paginator(mappings, page_size)
page_obj = paginator.get_page(page)
mapping_list = []
for m in page_obj:
mapping_list.append({
'id': m.id,
'dianpu_id': m.dianpu_id,
'dianpu_name': m.dianpu.dianpu_mingcheng if m.dianpu else '',
'gonggong_leixing_id': m.gonggong_leixing_id,
'gonggong_leixing_name': m.gonggong_leixing.jieshao if m.gonggong_leixing else '',
'jieshao': m.jieshao,
'tupian_url': m.tupian_url or '',
'paixu': m.paixu,
'shangjia_zhuangtai': m.shangjia_zhuangtai,
'fengjin_zhuangtai': m.fengjin_zhuangtai,
'shenhe_zhuangtai': m.shenhe_zhuangtai,
})
return Response({
'code': 0,
'data': {
'list': mapping_list,
'total': paginator.count,
'pending_total': pending_total
}
})
# ==================== 统一修改接口(已修正事务) ====================
class ShopProductModifyView(APIView):
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
user_perms = set(permissions)
action = request.data.get('action')
if action == 'update_audit_mode':
return self._update_audit_mode(request, user_perms)
elif action == 'update_mapping_status':
return self._update_mapping_status(request, user_perms)
else:
return Response({'code': 400, 'msg': '未知操作'})
@transaction.atomic # ✅ 事务修复点
def _update_audit_mode(self, request, user_perms):
if '1199ab' not in user_perms:
return Response({'code': 403, 'msg': '无权限修改审核模式'})
kaiqi = request.data.get('kaiqi_shenhe')
if kaiqi is None:
return Response({'code': 400, 'msg': '缺少参数 kaiqi_shenhe'})
try:
config, _ = DianpuShangpinShenheShezhi.objects.select_for_update().get_or_create(id=1)
config.kaiqi_shenhe = bool(kaiqi)
config.save(update_fields=['kaiqi_shenhe'])
return Response({'code': 0, 'msg': '审核模式已更新'})
except Exception as e:
logger.exception("更新审核模式失败")
return Response({'code': 500, 'msg': '服务器错误'})
@transaction.atomic
def _update_mapping_status(self, request, user_perms):
mapping_id = request.data.get('mapping_id')
if not mapping_id:
return Response({'code': 400, 'msg': '缺少 mapping_id'})
try:
mapping = ShangpinLeixingDianpu.objects.select_for_update().get(id=int(mapping_id))
except (ShangpinLeixingDianpu.DoesNotExist, ValueError):
return Response({'code': 404, 'msg': '映射不存在'})
fengjin = request.data.get('fengjin_zhuangtai')
shangjia = request.data.get('shangjia_zhuangtai')
shenhe = request.data.get('shenhe_zhuangtai')
# 权限判断
if fengjin is not None:
if fengjin:
if '1199abc' not in user_perms:
return Response({'code': 403, 'msg': '无权限封禁'})
else:
if '1199abd' not in user_perms:
return Response({'code': 403, 'msg': '无权限解封'})
mapping.fengjin_zhuangtai = bool(fengjin)
if shangjia is not None:
if not shangjia:
if '1199abc' not in user_perms:
return Response({'code': 403, 'msg': '无权限下架'})
else:
if '1199abd' not in user_perms:
return Response({'code': 403, 'msg': '无权限上架'})
mapping.shangjia_zhuangtai = bool(shangjia)
if shenhe is not None:
if shenhe:
if '1199abd' not in user_perms:
return Response({'code': 403, 'msg': '无权限审核通过'})
mapping.shenhe_zhuangtai = bool(shenhe)
mapping.save()
return Response({'code': 0, 'msg': '修改成功'})
# 商品管理相关权限组(拥有任意一个即可查看列表)
PRODUCT_PERMS = ['1199ab', '1199abc', '1199abd', '1199abe']
class ProductListView(APIView):
"""
商品列表接口,支持多条件筛选与分页。
请求路径: /houtai/hqhtdpsplx
权限要求: 1199ab / 1199abc / 1199abd / 1199abe 任一即可
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# ----- 身份与权限验证 -----
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
if not set(permissions).intersection(PRODUCT_PERMS):
return Response({'code': 403, 'msg': '无商品管理权限'})
# ----- 安全解析分页与筛选参数 -----
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 15))
except (TypeError, ValueError):
return Response({'code': 400, 'msg': '分页参数格式错误'})
keyword = str(request.data.get('keyword', '')).strip()
shop_id = request.data.get('shop_id') # 店铺ID (可选)
shop_product_type_id = request.data.get('shop_product_type_id') # 店铺商品类型映射ID (可选)
public_type_id = request.data.get('public_type_id') # 公共商品类型ID (可选)
# 布尔型状态筛选
def parse_bool(val):
if val is None:
return None
return bool(val)
shenhe = parse_bool(request.data.get('shenhe_zhuangtai'))
shangjia = parse_bool(request.data.get('shangjia_zhuangtai'))
fengjin = parse_bool(request.data.get('fengjin_zhuangtai'))
# ----- 构建查询(预加载店铺和店铺商品类型) -----
products = Shangpin.query.select_related('dianpu', 'dianpu_leixing').all()
# 按店铺筛选
if shop_id is not None:
try:
products = products.filter(dianpu_id=int(shop_id))
except (ValueError, TypeError):
return Response({'code': 400, 'msg': 'shop_id 格式错误'})
# 按店铺商品类型映射筛选
if shop_product_type_id is not None:
try:
products = products.filter(dianpu_leixing_id=int(shop_product_type_id))
except (ValueError, TypeError):
return Response({'code': 400, 'msg': 'shop_product_type_id 格式错误'})
# 按公共商品类型筛选leixing_id 存储的是公共类型的ID
if public_type_id is not None:
try:
products = products.filter(leixing_id=int(public_type_id))
except (ValueError, TypeError):
return Response({'code': 400, 'msg': 'public_type_id 格式错误'})
# 平台审核状态筛选 (BooleanField)
if shenhe is not None:
products = products.filter(shenhe_zhuangtai=shenhe)
# 上架状态筛选
if shangjia is not None:
products = products.filter(shangjia_zhuangtai=shangjia)
# 封禁状态筛选
if fengjin is not None:
products = products.filter(fengjin_zhuangtai=fengjin)
# 关键词搜索商品ID或商品标题搜索具有模糊性使用icontains
if keyword:
# 尝试纯数字可能是商品ID使用精确匹配与标题模糊搜索
products = products.filter(
models.Q(id__icontains=keyword) |
models.Q(biaoqian__icontains=keyword)
)
# ----- 获取待审核总数(平台审核未通过的数量) -----
pending_total = products.filter(shenhe_zhuangtai=False).count()
# ----- 分页 -----
paginator = Paginator(products, page_size)
page_obj = paginator.get_page(page)
# ----- 构造返回数据批量获取公共类型名称避免N+1 -----
# 提取所有公共商品类型ID
type_ids = {p.leixing_id for p in page_obj if p.leixing_id is not None}
public_type_map = {}
if type_ids:
public_types = ShangpinLeixing.query.filter(id__in=type_ids).values('id', 'jieshao')
public_type_map = {t['id']: t['jieshao'] for t in public_types}
product_list = []
for p in page_obj:
product_list.append({
'id': p.id,
'biaoqian': p.biaoqian or '',
'jiage': p.jiage,
'kucun': p.kucun,
'leixing_id': p.leixing_id,
'public_type_name': public_type_map.get(p.leixing_id, ''),
'zhenshi_xiaoliang': p.zhenshi_xiaoliang,
'duiwai_xiaoliang': p.duiwai_xiaoliang,
'jieshao': p.jieshao or '',
'xiadan_xuzhi': p.xiadan_xuzhi or '',
'guize_tupian': p.guize_tupian or '',
'yaoqiuleixing': p.yaoqiuleixing,
'huiyuan_id': p.huiyuan_id or '',
'yongjin': p.yongjin,
'kaioi_ewai_PlayerCommission': p.kaioi_ewai_dashou_fencheng,
'ewai_PlayerCommission': p.ewai_dashou_fencheng,
'CreateTime': p.CreateTime.isoformat() if p.CreateTime else '',
'UpdateTime': p.UpdateTime.isoformat() if p.UpdateTime else '',
'paixu': p.paixu,
'dianpu_id': p.dianpu_id,
'dianpu_name': p.dianpu.dianpu_mingcheng if p.dianpu else '',
'dianpu_leixing_id': p.dianpu_leixing_id,
'dianpu_leixing_name': p.dianpu_leixing.jieshao if p.dianpu_leixing else '',
'shangjia_zhuangtai': p.shangjia_zhuangtai,
'fengjin_zhuangtai': p.fengjin_zhuangtai,
'shenhe_zhuangtai': p.shenhe_zhuangtai,
})
return Response({
'code': 0,
'data': {
'list': product_list,
'total': paginator.count,
'pending_total': pending_total, # 待审核商品总数
}
})
class ProductModifyView(APIView):
"""
商品修改接口,根据修改内容细粒度鉴权。
请求路径: /houtai/htxgdpspsj
"""
permission_classes = [IsAuthenticated]
def post(self, request):
username = request.data.get('username')
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return Response({'code': 403, 'msg': '身份验证失败'})
user_perms = set(permissions)
action = request.data.get('action', '')
if action != 'update_product':
return Response({'code': 400, 'msg': '未知操作'})
return self._update_product(request, user_perms)
@transaction.atomic
def _update_product(self, request, user_perms):
"""执行商品修改,严格校验权限"""
product_id = request.data.get('product_id')
if not product_id:
return Response({'code': 400, 'msg': '缺少商品ID'})
try:
# 行级锁防止并发修改
product = Shangpin.objects.select_for_update().get(id=int(product_id))
except (Shangpin.DoesNotExist, ValueError):
return Response({'code': 404, 'msg': '商品不存在'})
# 需要修改的字段
fields_to_update = {}
# 将要修改的状态标志用于后续权限判断
status_changes = {
'shangjia': request.data.get('shangjia_zhuangtai'),
'fengjin': request.data.get('fengjin_zhuangtai'),
'shenhe': request.data.get('shenhe_zhuangtai'),
}
# ---------- 权限分类检查 ----------
# 1. 封禁/下架操作 (消极操作) -> 1199abc
if any(status_changes.get(k) is not None and status_changes[k] == False for k in ['shangjia', 'shenhe']):
if '1199abc' not in user_perms:
return Response({'code': 403, 'msg': '无权限执行下架/封禁/驳回审核操作(1199abc)'})
if status_changes.get('fengjin') is not None and status_changes['fengjin'] == True:
if '1199abc' not in user_perms:
return Response({'code': 403, 'msg': '无权限执行封禁操作(1199abc)'})
# 2. 解封/上架/过审 (积极操作) -> 1199abd
if any(status_changes.get(k) is not None and status_changes[k] == True for k in ['shangjia', 'shenhe']):
if '1199abd' not in user_perms:
return Response({'code': 403, 'msg': '无权限执行上架/解封/审核通过操作(1199abd)'})
if status_changes.get('fengjin') is not None and status_changes['fengjin'] == False:
if '1199abd' not in user_perms:
return Response({'code': 403, 'msg': '无权限执行解封操作(1199abd)'})
# 3. 修改其他数据(非状态字段)-> 1199abe
# 除了上架、封禁、审核之外的字段都属于“其他数据”
other_fields = [k for k in request.data.keys() if k not in ['action', 'product_id', 'username',
'shangjia_zhuangtai', 'fengjin_zhuangtai',
'shenhe_zhuangtai']]
if other_fields and '1199abe' not in user_perms:
return Response({'code': 403, 'msg': '无权限修改商品基本信息(1199abe)'})
# ---------- 应用修改 ----------
# 允许修改的字段白名单(防止注入)
# 前端参数名 → 模型字段名映射
field_name_map = {
'kaioi_ewai_PlayerCommission': 'kaioi_ewai_dashou_fencheng',
'ewai_PlayerCommission': 'ewai_dashou_fencheng',
}
allowed_fields = [
'biaoqian', 'jiage', 'kucun', 'leixing_id', 'duiwai_xiaoliang',
'jieshao', 'xiadan_xuzhi', 'yaoqiuleixing', 'yongjin',
'kaioi_ewai_PlayerCommission', 'ewai_PlayerCommission',
'shangjia_zhuangtai', 'fengjin_zhuangtai', 'shenhe_zhuangtai'
]
update_fields = []
for field in allowed_fields:
value = request.data.get(field)
if value is not None:
model_field = field_name_map.get(field, field)
# 简单类型校验(根据字段类型)
if field in ['jiage', 'kucun', 'duiwai_xiaoliang', 'yongjin', 'ewai_PlayerCommission', 'paixu']:
try:
value = float(value) if 'jiage' in field or 'yongjin' in field or 'ewai' in field else int(value)
except (ValueError, TypeError):
return Response({'code': 400, 'msg': f'字段 {field} 值格式错误'})
setattr(product, model_field, value)
update_fields.append(model_field)
if update_fields:
product.save(update_fields=update_fields)
return Response({'code': 0, 'msg': '商品修改成功'})
# 与前端约定的聊天权限码
CHAT_PERM_CODES = ['abca1', 'baac2', 'cabc3', 'cb3a2', 'bcaa4']

1159
backend/views/system.py Normal file

File diff suppressed because it is too large Load Diff

347
backend/views/titles.py Normal file
View File

@@ -0,0 +1,347 @@
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 KhpzhqView(APIView):
"""
获取所有考核配置数据(称号列表、板块、费用)
POST /houtai/khpzhq
"""
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)
bankuai_list = list(Bankuai.query.values('bankuai_id', 'mingcheng'))
chenghao_qs = Chenghao.query.prefetch_related('kaohecishufeiyong_set').all()
chenghao_list = []
for ch in chenghao_qs:
# 获取费用:按次数排序,转为 {cishu: feiyong} 字典
feiyong_dict = {
item.cishu: float(item.feiyong)
for item in ch.kaohecishufeiyong_set.order_by('cishu')
}
max_cishu = max(feiyong_dict.keys()) if feiyong_dict else 0
chenghao_list.append({
'id': ch.id,
'mingcheng': ch.mingcheng,
'leixing': ch.leixing,
'leixing_name': ch.get_leixing_display(),
'bankuai_id': ch.bankuai_id,
'tubiao_url': ch.tubiao_url or '',
'texiao_miaoshu': ch.texiao_miaoshu or '',
'CreateTime': ch.CreateTime.strftime('%Y-%m-%d %H:%M:%S') if ch.CreateTime else '',
'kaohe_guize': ch.kaohe_guize or '',
'kaioi_jinpai': ch.kaioi_jinpai,
'feiyong': feiyong_dict, # {1:100.0, 2:200.0}
'max_cishu': max_cishu
})
return Response({
'code': 0,
'msg': 'ok',
'data': {
'bankuai': bankuai_list,
'chenghao': chenghao_list
}
})
except Exception as e:
logger.exception("KhpzhqView 接口错误")
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
class ChzsgcView(APIView):
"""
称号(标签)增删改查统一接口
POST /houtai/chzsgc
"""
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')
if not action:
return Response({'code': 1, 'msg': '缺少 action 参数'}, status=400)
# ========== 创建称号 ==========
if action == 'create':
mingcheng = request.data.get('mingcheng', '').strip()
leixing = request.data.get('leixing', 'dashou')
bankuai_id = request.data.get('bankuai_id')
texiao_miaoshu = request.data.get('texiao_miaoshu', '')
kaohe_guize = request.data.get('kaohe_guize', '')
kaioi_jinpai = request.data.get('kaioi_jinpai', False)
feiyong_list = request.data.get('feiyong_list', [])
if not mingcheng:
return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400)
if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']:
return Response({'code': 1, 'msg': '无效的称号类型'}, status=400)
if Chenghao.query.filter(mingcheng=mingcheng).exists():
return Response({'code': 1, 'msg': '称号名称已存在'}, status=400)
with transaction.atomic():
ch = Chenghao.query.create(
mingcheng=mingcheng,
leixing=leixing,
bankuai_id=bankuai_id,
texiao_miaoshu=texiao_miaoshu,
kaohe_guize=kaohe_guize,
kaioi_jinpai=kaioi_jinpai
)
# 处理费用:前端传来 [{cishu:1, feiyong:100}, ...]
if feiyong_list:
for item in feiyong_list:
cishu = int(item.get('cishu', 1))
feiyong = float(item.get('feiyong', 0))
KaoheCishuFeiyong.query.update_or_create(
chenghao=ch, cishu=cishu,
defaults={'feiyong': feiyong}
)
else:
# 默认添加第一次免费
KaoheCishuFeiyong.query.create(chenghao=ch, cishu=1, feiyong=0)
return Response({'code': 0, 'msg': '创建成功', 'data': {'chenghao_id': ch.id}})
# ========== 修改称号 ==========
elif action == 'update':
ch_id = request.data.get('chenghao_id')
if not ch_id:
return Response({'code': 1, 'msg': '缺少称号ID'}, status=400)
try:
ch = Chenghao.query.get(id=ch_id)
except Chenghao.DoesNotExist:
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
mingcheng = request.data.get('mingcheng', ch.mingcheng).strip()
leixing = request.data.get('leixing', ch.leixing)
bankuai_id = request.data.get('bankuai_id', ch.bankuai_id)
texiao_miaoshu = request.data.get('texiao_miaoshu', ch.texiao_miaoshu)
kaohe_guize = request.data.get('kaohe_guize', ch.kaohe_guize)
kaioi_jinpai = request.data.get('kaioi_jinpai', ch.kaioi_jinpai)
if not mingcheng:
return Response({'code': 1, 'msg': '称号名称不能为空'}, status=400)
if leixing not in ['dashou','boss','shangjia','guanshi','zuzhang']:
return Response({'code': 1, 'msg': '无效的称号类型'}, status=400)
if Chenghao.query.filter(mingcheng=mingcheng).exclude(id=ch_id).exists():
return Response({'code': 1, 'msg': '称号名称已存在'}, status=400)
ch.mingcheng = mingcheng
ch.leixing = leixing
ch.bankuai_id = bankuai_id
ch.texiao_miaoshu = texiao_miaoshu
ch.kaohe_guize = kaohe_guize
ch.kaioi_jinpai = kaioi_jinpai
ch.save()
# 如果前端传递了 feiyong_list则全量替换费用
feiyong_list = request.data.get('feiyong_list')
if feiyong_list is not None:
with transaction.atomic():
ch.kaohecishufeiyong_set.all().delete()
for item in feiyong_list:
KaoheCishuFeiyong.query.create(
chenghao=ch,
cishu=int(item.get('cishu', 1)),
feiyong=float(item.get('feiyong', 0))
)
return Response({'code': 0, 'msg': '修改成功'})
# ========== 删除称号 ==========
elif action == 'delete':
ch_id = request.data.get('chenghao_id')
if not ch_id:
return Response({'code': 1, 'msg': '缺少称号ID'}, status=400)
try:
ch = Chenghao.query.get(id=ch_id)
except Chenghao.DoesNotExist:
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
ch.delete()
return Response({'code': 0, 'msg': '删除成功'})
# ========== 添加/修改某次费用 ==========
elif action == 'add_feiyong':
ch_id = request.data.get('chenghao_id')
cishu = request.data.get('cishu')
feiyong = request.data.get('feiyong', 0)
if not ch_id or not cishu:
return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400)
try:
ch = Chenghao.query.get(id=ch_id)
except Chenghao.DoesNotExist:
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
if int(cishu) < 1:
return Response({'code': 1, 'msg': '次数必须>=1'}, status=400)
KaoheCishuFeiyong.query.update_or_create(
chenghao=ch, cishu=cishu,
defaults={'feiyong': feiyong}
)
return Response({'code': 0, 'msg': '添加/修改费用成功'})
# ========== 删除某次费用 ==========
elif action == 'remove_feiyong':
ch_id = request.data.get('chenghao_id')
cishu = request.data.get('cishu')
if not ch_id or not cishu:
return Response({'code': 1, 'msg': '缺少称号ID或次数'}, status=400)
if int(cishu) == 1:
return Response({'code': 1, 'msg': '不能删除第1次考核费用'}, status=400)
try:
ch = Chenghao.query.get(id=ch_id)
except Chenghao.DoesNotExist:
return Response({'code': 1, 'msg': '称号不存在'}, status=404)
KaoheCishuFeiyong.query.filter(chenghao=ch, cishu=cishu).delete()
return Response({'code': 0, 'msg': '删除成功'})
# ========== 修改某次费用等价于add ==========
elif action == 'update_feiyong':
return self.add_feiyong(request) # 复用逻辑
else:
return Response({'code': 1, 'msg': '未知操作'}, status=400)
except Exception as e:
logger.exception("ChzsgcView 接口错误")
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
# ==================== 后端 views.py ====================
# 请确保已有以下导入,如果没有请在文件头部添加

View File

@@ -0,0 +1,170 @@
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 GetWithdrawSettingsView(APIView):
"""
获取提现设置(手续费利率、每日限额、当日已提现总额)
权限:需要 5500a 或 5500b 或 5500c 任一
POST /houtai/hthqtxpz
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
# 需要5500a/b/c任一
if not any(p in permissions for p in ('5500a', '5500b', '5500c')):
return Response({'code': 403, 'msg': '无权限查看提现设置'})
from jituan.services.withdraw_config import build_withdraw_settings_payload
return Response({'code': 0, 'data': build_withdraw_settings_payload(request)})
class UpdateWithdrawSettingsView(APIView):
"""
修改提现设置(权限细分):
- 修改打手相关利率、个人限额、总限额需要5500a
- 修改管事相关需要5500b
- 修改组长相关需要5500c
注意:修改每日总限额时,会联动影响“当日已提现总额”的显示(由前端实时计算),
后端不直接修改 WithdrawalDailyStats该表由提现流程自动累加。
POST /houtai/htxgtxsz
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
# 权限映射:要修改的角色需要对应的权限
# 前端传来 rates, quotas, total_limits 三个对象,每个包含 1,2,3
from jituan.services.withdraw_config import apply_withdraw_settings_update
return apply_withdraw_settings_update(request, permissions)

791
backend/views/zuzhang.py Normal file
View File

@@ -0,0 +1,791 @@
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 GetZuzhangListView(APIView):
"""
组长列表接口(分页 + 多条件筛选)
路由POST /houtai/hthqzzlb
权限:需要登录,且拥有 6600a~6600e 任一权限
"""
permission_classes = [IsAuthenticated]
@transaction.atomic # 保证查询的一致性(可选,对只读查询影响不大但无坏处)
def post(self, request):
# 1. 获取前端传递的 username用于防越权
frontend_username = request.data.get('username', '')
# 2. 调用公共校验方法,验证身份并获取权限列表
result = verify_kefu_permission(request, username_from_frontend=frontend_username)
# 重要verify_kefu_permission 返回格式为 (user_obj, permissions_list) 或 (None, Response)
# 如果第二个元素是 Response 对象,说明校验失败,直接返回该 Response
if isinstance(result, tuple) and len(result) == 2:
user_obj, second = result
if isinstance(second, Response):
return second # 校验失败,直接返回错误响应
else:
permissions = second # 正常情况permissions 是列表
else:
# 防止意外返回格式,视为失败
return Response({'code': 500, 'msg': '权限校验模块异常'})
# 3. 检查是否拥有组长管理所需权限6600a ~ 6600e 任一)
required_perms = ['6600a', '6600b', '6600c', '6600d', '6600e']
if not any(perm in permissions for perm in required_perms):
logger.warning(f"用户 {request.user.Phone} 尝试访问组长列表但缺少必要权限,已有权限: {permissions}")
return Response({'code': 403, 'msg': '您无权访问组长管理页面'})
# 4. 提取请求参数(分页 + 筛选)
try:
page = int(request.data.get('page', 1))
page_size = int(request.data.get('page_size', 20))
except ValueError:
page, page_size = 1, 20
keyword = request.data.get('keyword', '').strip()
status = request.data.get('status') # 1=正常, 0=禁用
invite_count_op = request.data.get('invite_count_op') # gte / lte
invite_count_value = request.data.get('invite_count_value')
amount_op = request.data.get('amount_op')
amount_value = request.data.get('amount_value')
commission_op = request.data.get('commission_op')
commission_value = request.data.get('commission_value')
# 5. 构建基础 QuerySet预加载关联表
queryset = filter_user_related_by_club(
UserZuzhang.query.select_related('user', 'user__BossProfile').all(),
request,
)
# 6. 应用筛选条件(使用 ORM 安全过滤)
if keyword:
queryset = queryset.filter(
Q(user__UserUID__icontains=keyword) |
Q(user__BossProfile__nickname__icontains=keyword)
)
if status is not None and status != '':
try:
queryset = queryset.filter(zhuangtai=int(status))
except ValueError:
pass
# 邀请总数筛选
if invite_count_value not in (None, ''):
try:
val = float(invite_count_value)
op = invite_count_op if invite_count_op in ('gte', 'lte') else 'gte'
queryset = queryset.filter(**{f'yaoqing_zongshu__{op}': val})
except (ValueError, TypeError):
pass
# 可提现金额筛选
if amount_value not in (None, ''):
try:
val = float(amount_value)
op = amount_op if amount_op in ('gte', 'lte') else 'gte'
queryset = queryset.filter(**{f'ketixian_jine__{op}': val})
except (ValueError, TypeError):
pass
# 分佣总额筛选
if commission_value not in (None, ''):
try:
val = float(commission_value)
op = commission_op if commission_op in ('gte', 'lte') else 'gte'
queryset = queryset.filter(**{f'fenyong_zonge__{op}': val})
except (ValueError, TypeError):
pass
# 7. 总数统计
total = queryset.count()
# 8. 分页
offset = (page - 1) * page_size
paged_queryset = queryset[offset:offset + page_size]
# 9. 构造返回数据
data_list = []
for zu in paged_queryset:
user = zu.user
nickname = ''
if hasattr(user, 'BossProfile') and user.BossProfile:
nickname = user.BossProfile.nickname or ''
data_list.append({
'yonghuid': user.UserUID,
'avatar': user.Avatar or '',
'nickname': nickname,
'fenyong_zonge': float(zu.fenyong_zonge),
'ketixian_jine': float(zu.ketixian_jine),
'yaoqing_zongshu': zu.yaoqing_zongshu,
'zhuangtai': zu.zhuangtai,
})
return Response({
'code': 0,
'msg': 'success',
**list_response_meta(request),
'data': {
'list': data_list,
'total': total,
}
})
class GetZuzhangDetailView(APIView):
"""
获取组长详情
权限6600a,6600b,6600c,6600d,6600e 任一
POST /houtai/hthqzzxq
参数username, yonghuid
返回:
code:0, data: {
base: {...},
balance: {...},
permanent_enabled, permanent_amount,
custom_first_dividend: {},
extra_dividend_map: {},
all_members: []
}
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
yonghuid = request.data.get('yonghuid', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
if not yonghuid:
return Response({'code': 400, 'msg': '缺少用户ID'})
# 权限校验(需要任一权限)
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
required_perms = {'6600a', '6600b', '6600c', '6600d', '6600e'}
if not any(p in permissions for p in required_perms):
return Response({'code': 403, 'msg': '您没有权限查看组长详情'})
# 查询组长主表、扩展表、老板扩展表(昵称)
try:
user = User.query.select_related('ZuzhangProfile', 'BossProfile').get(UserUID=yonghuid)
zuzhang = user.ZuzhangProfile
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
except User.DoesNotExist:
return Response({'code': 404, 'msg': '组长不存在'})
except UserZuzhang.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是组长'})
denied = forbid_if_user_out_of_scope(request, user)
if denied:
return denied
# 基础信息
base_data = {
'yonghuid': user.UserUID,
'nickname': boss.nickname if boss else '',
'avatar': user.Avatar or '',
'dianhua': '', # 组长表无电话字段?组长扩展表没有电话/微信但前端可能需要可从User关联实际上组长表无电话字段这里留空或从其他表获取
'wechat': '',
'zhuangtai': zuzhang.zhuangtai,
'yaoqingma': zuzhang.yaoqingma,
'yaoqingren': '', # 组长表没有邀请人字段
'haibao_url': zuzhang.haibao_url or '',
'yaoqing_zongshu': zuzhang.yaoqing_zongshu,
'jinri_fenyong': str(zuzhang.jinri_fenyong),
'jinyue_fenyong': str(zuzhang.jinyue_fenyong),
'fenyong_zonge': str(zuzhang.fenyong_zonge),
'CreateTime': zuzhang.CreateTime.isoformat() if zuzhang.CreateTime else None,
'create_time': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '',
}
# 余额提现相关
balance_data = {
'ketixian_jine': str(zuzhang.ketixian_jine),
'jinri_tixian': str(zuzhang.jinri_tixian),
'last_tixian_time': zuzhang.last_tixian_time.isoformat() if zuzhang.last_tixian_time else None,
'kaioi_ewai_tixian': zuzhang.kaioi_ewai_tixian,
'ewai_tixian_xiane': str(zuzhang.ewai_tixian_xiane),
}
# 永久分红(额外分红开关和金额)
permanent_enabled = zuzhang.kaioi_ewai_fenhong
permanent_amount = str(zuzhang.ewai_fenhong_jine) if zuzhang.ewai_fenhong_jine else '0'
# 获取所有会员(用于前端选择)
all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao', 'jiage', 'zuzhangfc'))
for m in all_members:
m['jiage'] = str(m['jiage'])
m['zuzhangfc'] = str(m['zuzhangfc'])
# 获取多次分红配置(按次数和会员组织,按俱乐部隔离)
from jituan.services.club_penalty import resolve_gsfenhong_club_id
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
duoci_records = DuociFenhong.query.filter(
yonghuid=yonghuid, club_id=zuzhang_club,
).order_by('cishu')
custom_first = {} # 首次分红定制cishu=1
extra_map = {} # 其他次数 { cishu: { huiyuan_id: { zuzhang_fenhong, jieshao, jiage } } }
for record in duoci_records:
# 获取会员信息
member = Huiyuan.query.filter(huiyuan_id=record.huiyuan).first()
if not member:
continue
member_info = {
'jieshao': member.jieshao,
'jiage': str(member.jiage),
}
if record.cishu == 1:
custom_first[record.huiyuan] = float(record.zuzhang_fenhong)
else:
times = record.cishu
if times not in extra_map:
extra_map[times] = {}
extra_map[times][record.huiyuan] = {
'zuzhang_fenhong': float(record.zuzhang_fenhong),
**member_info
}
return Response({
'code': 0,
'data': {
'base': base_data,
'balance': balance_data,
'permanent_enabled': permanent_enabled,
'permanent_amount': permanent_amount,
'custom_first_dividend': custom_first,
'extra_dividend_map': extra_map,
'all_members': all_members,
}
})
class UpdateZuzhangView(APIView):
"""
修改组长信息(基础信息、余额、提现限额、分红策略)
权限细分:
- 增加可提现金额6600a
- 减少可提现金额6600b
- 修改状态(封禁/解封6600c
- 修改提现限额kaioi_ewai_tixian/ewai_tixian_xiane6600d
- 修改分红相关永久分红、定制首次、额外次数6600e
POST /houtai/htxgzzxx
参数:
username, yonghuid,
nickname, avatar, dianhua, wechat, zhuangtai, # 基础
ketixian_jine, kaioi_ewai_tixian, ewai_tixian_xiane, # 余额提现
permanent_enabled, permanent_amount, # 永久分红
custom_first_dividend: {}, # 首次定制分红 {会员ID: 金额}
extra_dividend_map: {} # 额外次数分红 {次数: {会员ID: {zuzhang_fenhong}}}
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
yonghuid = request.data.get('yonghuid', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
if not yonghuid:
return Response({'code': 400, 'msg': '缺少用户ID'})
# 权限校验
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
# 查询组长对象
try:
user = User.query.get(UserUID=yonghuid)
zuzhang = user.ZuzhangProfile
boss = user.BossProfile if hasattr(user, 'BossProfile') else None
except User.DoesNotExist:
return Response({'code': 404, 'msg': '用户不存在'})
except UserZuzhang.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是组长'})
# 获取原始数据(用于对比金额变化)
old_ketixian_jine = zuzhang.ketixian_jine
# 开始事务
with transaction.atomic():
operator = kefu.user.Phone
old_nickname = boss.nickname if boss else ''
old_zhuangtai = zuzhang.zhuangtai
# ---------- 1. 基础信息修改需要6600c按需求基础信息修改通常用6600c但为了安全暂不校验如有需要可加----------
nickname = request.data.get('nickname', '').strip()
avatar = request.data.get('avatar', '').strip()
dianhua = request.data.get('dianhua', '').strip()
wechat = request.data.get('wechat', '').strip()
zhuangtai = request.data.get('zhuangtai')
base_changes = []
# 状态修改需要6600c
if zhuangtai is not None:
if '6600c' not in permissions:
return Response({'code': 403, 'msg': '无权限修改组长状态需要6600c'})
try:
zt = int(zhuangtai)
if zt in (0, 1):
zuzhang.zhuangtai = zt
if zt != old_zhuangtai:
zt_map = {0: '禁用', 1: '正常'}
base_changes.append(f'状态:{zt_map.get(old_zhuangtai, old_zhuangtai)}{zt_map.get(zt, zt)}')
except:
pass
if nickname:
if not boss:
boss = UserBoss.query.create(user=user, nickname=nickname)
else:
boss.nickname = nickname
boss.save()
if nickname != old_nickname:
base_changes.append(f'昵称:{old_nickname or ""}{nickname}')
if avatar:
user.Avatar = avatar
user.save()
base_changes.append('更新了头像')
if base_changes:
zuzhang.save()
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_ZUZHANG,
qitashuoming=(
f'【后台】修改组长基础信息:{"".join(base_changes)}'
f'组长ID={yonghuid},操作人={operator}'
),
)
# 组长表没有电话/微信字段如需存储可暂存到User或忽略
# 这里不处理
# ---------- 2. 可提现金额修改6600a6600b----------
new_ketixian_jine = request.data.get('ketixian_jine')
if new_ketixian_jine is not None:
try:
new_val = Decimal(str(new_ketixian_jine))
if new_val < 0:
return Response({'code': 400, 'msg': '可提现金额不能为负数'})
except:
return Response({'code': 400, 'msg': '金额格式错误'})
if new_val > old_ketixian_jine:
if '6600a' not in permissions:
return Response({'code': 403, 'msg': '无权限增加可提现金额需要6600a'})
from jituan.services.group_finance_gate import deny_group_finance_exec
deny = deny_group_finance_exec(request.user, request)
if deny:
return deny
action_desc = f'增加{new_val - old_ketixian_jine}'
elif new_val < old_ketixian_jine:
if '6600b' not in permissions:
return Response({'code': 403, 'msg': '无权限减少可提现金额需要6600b'})
action_desc = f'减少{old_ketixian_jine - new_val}'
else:
action_desc = '无变动'
if new_val != old_ketixian_jine:
zuzhang.ketixian_jine = new_val
zuzhang.save(update_fields=['ketixian_jine'])
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_ZUZHANG,
xiugaitijiao=new_val,
xiugaitijiaoq=old_ketixian_jine,
qitashuoming=(
f'【后台】修改组长可提现金额:{old_ketixian_jine}元 → {new_val}元({action_desc}'
f'组长ID={yonghuid},操作人={operator}'
),
)
# ---------- 3. 提现限额修改需要6600d----------
kaioi_ewai = request.data.get('kaioi_ewai_tixian')
ewai_val = request.data.get('ewai_tixian_xiane')
if kaioi_ewai is not None or ewai_val is not None:
if '6600d' not in permissions:
return Response({'code': 403, 'msg': '无权限修改提现限额需要6600d'})
limit_changes = []
old_kaioi = zuzhang.kaioi_ewai_tixian
old_ewai = zuzhang.ewai_tixian_xiane
if kaioi_ewai is not None:
zuzhang.kaioi_ewai_tixian = bool(kaioi_ewai)
if bool(kaioi_ewai) != old_kaioi:
limit_changes.append(f'额外限额开关:{"" if old_kaioi else ""}{"" if kaioi_ewai else ""}')
if ewai_val is not None:
try:
zuzhang.ewai_tixian_xiane = Decimal(str(ewai_val))
if zuzhang.ewai_tixian_xiane != old_ewai:
limit_changes.append(f'额外限额金额:{old_ewai}{zuzhang.ewai_tixian_xiane}')
except:
return Response({'code': 400, 'msg': '额外限额金额格式错误'})
zuzhang.save()
if limit_changes:
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_ZUZHANG,
qitashuoming=(
f'【后台】修改组长提现限额:{"".join(limit_changes)}'
f'组长ID={yonghuid},操作人={operator}'
),
)
# ---------- 4. 分红相关修改需要6600e----------
perm_enabled = request.data.get('permanent_enabled')
perm_amount = request.data.get('permanent_amount')
custom_first = request.data.get('custom_first_dividend')
extra_map = request.data.get('extra_dividend_map')
if any(v is not None for v in [perm_enabled, perm_amount, custom_first, extra_map]):
if '6600e' not in permissions:
return Response({'code': 403, 'msg': '无权限修改分红策略需要6600e'})
# 4.1 永久分红(额外分红开关和金额)
if perm_enabled is not None:
zuzhang.kaioi_ewai_fenhong = bool(perm_enabled)
if perm_amount is not None:
try:
zuzhang.ewai_fenhong_jine = Decimal(str(perm_amount))
except:
return Response({'code': 400, 'msg': '永久分红金额格式错误'})
zuzhang.save()
# 4.2 首次定制分红cishu=1
if custom_first is not None and isinstance(custom_first, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
existing_first = DuociFenhong.query.filter(
club_id=zuzhang_club, yonghuid=yonghuid, cishu=1,
)
for huiyuan_id, amount in custom_first.items():
try:
amount = Decimal(str(amount))
except:
continue
if amount <= 0:
# 删除定制(恢复默认)
DuociFenhong.query.filter(
club_id=zuzhang_club, yonghuid=yonghuid, huiyuan=huiyuan_id, cishu=1,
).delete()
else:
DuociFenhong.query.update_or_create(
club_id=zuzhang_club,
yonghuid=yonghuid,
huiyuan=huiyuan_id,
cishu=1,
defaults={
'zuzhang_fenhong': amount,
'guanshi_fenhong': Decimal('0') # 组长表只关心组长分红
}
)
# 删除那些在前端字典中不存在的首次定制记录(即已取消定制)
for record in existing_first:
if record.huiyuan not in custom_first:
record.delete()
# 4.3 额外次数分红cishu >= 2
if extra_map is not None and isinstance(extra_map, dict):
from jituan.services.club_penalty import resolve_gsfenhong_club_id
zuzhang_club = resolve_gsfenhong_club_id(dashouid=yonghuid)
keep_set = set()
for times_str, members in extra_map.items():
try:
cishu = int(times_str)
if cishu < 2:
continue
except:
continue
if not isinstance(members, dict):
continue
for huiyuan_id, data in members.items():
if not isinstance(data, dict):
continue
amount = data.get('zuzhang_fenhong')
if amount is None:
continue
try:
amount = Decimal(str(amount))
except:
continue
keep_set.add((cishu, huiyuan_id))
DuociFenhong.query.update_or_create(
club_id=zuzhang_club,
yonghuid=yonghuid,
huiyuan=huiyuan_id,
cishu=cishu,
defaults={
'zuzhang_fenhong': amount,
'guanshi_fenhong': Decimal('0')
}
)
# 删除不在 keep_set 中的额外次数记录
existing_extra = DuociFenhong.query.filter(
club_id=zuzhang_club, yonghuid=yonghuid, cishu__gte=2,
)
for record in existing_extra:
if (record.cishu, record.huiyuan) not in keep_set:
record.delete()
dividend_parts = []
if perm_enabled is not None:
dividend_parts.append(f'永久分红开关→{"" if perm_enabled else ""}')
if perm_amount is not None:
dividend_parts.append(f'永久分红金额→{perm_amount}')
if custom_first is not None:
dividend_parts.append(f'更新首次定制分红({len(custom_first)}项)')
if extra_map is not None:
dividend_parts.append(f'更新额外次数分红({len(extra_map)}组)')
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_ZUZHANG,
qitashuoming=(
f'【后台】修改组长分红策略:{"".join(dividend_parts) or "配置已更新"}'
f'组长ID={yonghuid},操作人={operator}'
),
)
# 保存组长其他可能修改的字段(例如电话、微信没有,忽略)
return Response({'code': 0, 'msg': '修改成功'})
class ChangeInviterView(APIView):
"""
后台更换邀请人/上级
POST /houtai/htghyqr
参数:
username: 操作人账号
yonghuid: 被修改用户ID
target_type: dashou打手| guanshi管事
new_yaoqingren: 新邀请人用户ID须在 User 主表存在)
权限:
dashou → 001ee
guanshi → 4400a
校验:
打手新上级须为管事;管事新上级须为组长;不可设为自己
"""
permission_classes = []
parser_classes = [JSONParser]
def post(self, request):
username = request.data.get('username', '').strip()
yonghuid = request.data.get('yonghuid', '').strip()
target_type = request.data.get('target_type', '').strip().lower()
new_yaoqingren = request.data.get('new_yaoqingren', '').strip()
if not username:
return Response({'code': 401, 'msg': '缺少username'})
if not yonghuid:
return Response({'code': 400, 'msg': '缺少用户ID'})
if target_type not in ('dashou', 'guanshi'):
return Response({'code': 400, 'msg': 'target_type 须为 dashou 或 guanshi'})
if not new_yaoqingren:
return Response({'code': 400, 'msg': '缺少新邀请人ID'})
if new_yaoqingren == yonghuid:
return Response({'code': 400, 'msg': '不能将自己设为邀请人'})
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions
if target_type == 'dashou':
if '001ee' not in permissions:
return Response({'code': 403, 'msg': '无权限更换打手邀请人需要001ee'})
else:
if '4400a' not in permissions:
return Response({'code': 403, 'msg': '无权限更换管事邀请人需要4400a'})
try:
target_user = User.query.get(UserUID=yonghuid)
except User.DoesNotExist:
return Response({'code': 404, 'msg': '被修改用户不存在'})
try:
inviter_user = User.query.get(UserUID=new_yaoqingren)
except User.DoesNotExist:
return Response({'code': 404, 'msg': '新邀请人用户ID不存在'})
operator = kefu.user.Phone
with transaction.atomic():
if target_type == 'dashou':
try:
dashou = target_user.DashouProfile
except UserDashou.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是打手'})
try:
guanshi_profile = inviter_user.GuanshiProfile
except UserGuanshi.DoesNotExist:
return Response({'code': 400, 'msg': '新邀请人不是管事,无法作为打手上级'})
if guanshi_profile.zhuangtai != 1:
return Response({'code': 400, 'msg': '新邀请人管事账号已禁用'})
old_inviter = dashou.yaoqingren or ''
dashou.yaoqingren = new_yaoqingren
dashou.save(update_fields=['yaoqingren'])
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_DASHOU,
qitashuoming=(
f'【后台】更换打手邀请人:{old_inviter}{new_yaoqingren}'
f'打手ID={yonghuid},新上级须为管事(已校验),操作人={operator}'
),
)
else:
try:
guanshi = target_user.GuanshiProfile
except UserGuanshi.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是管事'})
try:
zuzhang_profile = inviter_user.ZuzhangProfile
except UserZuzhang.DoesNotExist:
return Response({'code': 400, 'msg': '新邀请人不是组长,无法作为管事上级'})
if zuzhang_profile.zhuangtai != 1:
return Response({'code': 400, 'msg': '新邀请人组长账号已禁用'})
old_inviter = guanshi.yaoqingren or ''
guanshi.yaoqingren = new_yaoqingren
guanshi.save(update_fields=['yaoqingren'])
write_xiugai_log(
yonghuid=yonghuid,
xiugaiid=operator,
leixing=XIUGAI_LEIXING_GUANSHI,
qitashuoming=(
f'【后台】更换管事邀请人:{old_inviter}{new_yaoqingren}'
f'管事ID={yonghuid},新上级须为组长(已校验),操作人={operator}'
),
)
return Response({
'code': 0,
'msg': '邀请人更换成功',
'data': {'yonghuid': yonghuid, 'new_yaoqingren': new_yaoqingren},
})

357
backend/views/zxkf.py Normal file
View File

@@ -0,0 +1,357 @@
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 ZxkfghdsView(APIView):
"""
客服更换打手接口
路径POST /houtai/zxkfghds
功能:
1. 将当前订单状态改为已退款5释放接单打手若有
2. 如需跨平台通知,则调用对方平台退款通知接口
3. 基于原订单信息生成全新订单(状态直接为 1已替老板填写
4. 生成新的派单链接,并标记为已使用
5. 新链接记录上一个链接ID和上一个订单ID
6. 【关键】不操作商家余额、退款次数、每日统计,因新订单是替换而非新增扣款
权限002ac商家订单管理/ 002ab平台订单管理/ 003aa跨平台管理
"""
permission_classes = [IsAuthenticated]
parser_classes = [JSONParser]
def post(self, request):
# ========== 1. 参数提取 ==========
username = request.data.get('username', '').strip() or request.data.get('phone', '').strip()
OrderID = pick_order_id(request.data)
if not username or not OrderID:
logger.warning("更换打手参数不完整")
return Response({'code': 400, 'msg': '参数不完整'})
# ========== 2. 权限校验(商家链接页客服需 002ac非仅跨平台 003aa ==========
kefu, permissions = verify_kefu_permission(request, username)
if kefu is None:
return permissions # 返回 error response
if not has_merchant_order_permission(permissions):
return Response({'code': 403, 'msg': '您无权处理订单'})
current_user = request.user
# ========== 3. 查询原订单及关联信息 ==========
try:
old_order = Order.query.select_related('shangjia_kuozhan').get(OrderID=OrderID)
except Order.DoesNotExist:
logger.warning(f"订单不存在: {OrderID}")
return Response({'code': 404, 'msg': '订单不存在'})
# 仅允许状态为 2进行中或 8结算中更换打手
if old_order.Status not in [1,7,2, 8]:
return Response({'code': 400, 'msg': '当前订单状态不允许更换打手'})
# 本接口仅处理商家发单Platform=2
if old_order.Platform != 2:
return Response({'code': 400, 'msg': '仅支持商家发单的更换操作'})
# 获取原链接用于模板ID等
try:
old_lianjie = ShangjiaLianjie.query.get(OrderID=OrderID)
except ShangjiaLianjie.DoesNotExist:
logger.error(f"原链接记录不存在,订单: {OrderID}")
return Response({'code': 500, 'msg': '链接数据异常'})
# 商家信息(仅用于记录关联,不操作余额)
try:
shangjia_ext = old_order.shangjia_kuozhan
shangjia_id = shangjia_ext.MerchantID
# 获取商家昵称用于新订单扩展表
shangjia_user = User.query.get(UserUID=shangjia_id)
shangjia_nicheng = shangjia_user.ShopProfile.nicheng or f"商家{shangjia_id}"
except (ObjectDoesNotExist, UserShangjia.DoesNotExist):
logger.error(f"商家信息缺失,订单: {OrderID}")
return Response({'code': 500, 'msg': '商家数据异常'})
# ========== 4. 退款处理(仅打手侧,不碰商家) ==========
with transaction.atomic():
# 5.1 订单状态改为已退款
old_order.Status = 5
old_order.AssignedCS = current_user.Phone
old_order.save()
# 5.2 处理接单打手(若有)
dashou_id = old_order.PlayerID
if dashou_id:
try:
dashou_user = User.query.get(UserUID=dashou_id)
dashou_profile = dashou_user.DashouProfile
dashou_profile.tuikuanliang += 1 # 退款次数+1
dashou_profile.zhuangtai = 1 # 设为空闲
dashou_profile.save()
except (User.DoesNotExist, ObjectDoesNotExist):
logger.warning(f"打手 {dashou_id} 不存在,跳过更新")
# 5.3 更新退款记录表
RefundRecord.query.update_or_create(
OrderID=OrderID,
defaults={
'PlayerID': dashou_id or '',
'ApplicantID': '',
'ProcessorID': current_user.Phone,
'Reason': '客服更换打手退款',
'ApplyStatus': 1,
'Amount': old_order.Amount or 0,
'PlayerCommission': old_order.PlayerCommission or 0,
'Description': old_order.Description or '',
'Remark': '更换打手自动退款',
'Nickname': old_order.Nickname or '',
}
)
# 5.4 打手每日统计(退款)
if dashou_id:
try:
update_dashou_daily_by_action(
yonghuid=dashou_id,
amount=old_order.PlayerCommission,
action=3 # 3=退款
)
except Exception as e:
logger.error(f"打手每日统计更新失败: {e}")
# 5.5 客服处理统计(管理员无 KefuProfile跳过
if hasattr(kefu, 'jinrichuli'):
kefu.jinrichuli += 1
kefu.jinyuechuli += 1
kefu.zongchuli += 1
kefu.save()
# 至此,原订单已退款,事务提交
# ========== 6. 生成新订单和链接(不扣商家余额) ==========
try:
with transaction.atomic():
# ---- 6.1 生成新的订单ID和Token ----
def generate_OrderID():
timestamp = int(time.time() * 1000)
process_id = os.getpid() % 1000
random_num = random.randint(1000, 9999)
return f"SJ{timestamp}{process_id:03d}{random_num}"
def generate_secure_token(dd_id, uid):
timestamp = int(time.time() * 1_000_000)
salt = secrets.token_hex(8)
raw = f"{timestamp}:{dd_id}:{uid}:{salt}"
token = hashlib.sha256(raw.encode()).hexdigest()[:24]
if ShangjiaLianjie.query.filter(LinkToken=token).exists():
return generate_secure_token(dd_id, uid)
return token
def generate_link_url(token):
base = getattr(settings, 'H5_DOMAIN', 'https://h5.yourdomain.com').rstrip('/')
return f"{base}/order/{token}"
new_OrderID = generate_OrderID()
secure_token = generate_secure_token(new_OrderID, shangjia_id)
# ---- 6.2 创建新订单复制核心数据状态直接为1 ----
new_order = Order.query.create(
OrderID=new_OrderID,
Status=1, # 下单中
Platform=2,
Amount=old_order.Amount,
PlayerCommission=old_order.PlayerCommission,
ProductTypeID=old_order.ProductTypeID,
GrabRequirement=old_order.GrabRequirement,
MembershipID=old_order.MembershipID,
CommissionReq=old_order.CommissionReq,
Description=old_order.Description,
Nickname=old_order.Nickname, # 继承游戏昵称
Remark=old_order.Remark,
User1ID=old_order.User1ID, # 商家标识
ImageURL=old_order.ImageURL,
ExternalOrderID=old_order.ExternalOrderID,
ClubID=getattr(old_order, 'ClubID', None) or 'xq',
# 注意:不继承 AssignedID、PlayerID
)
# ---- 6.3 创建商家扩展表 ----
MerchantOrderExt.query.create(
Order=new_order,
MerchantID=shangjia_id,
MerchantNickname=shangjia_nicheng,
)
# ---- 6.4 生成新链接并关联旧链接信息 ----
link_url = generate_link_url(secure_token)
new_lianjie = ShangjiaLianjie.query.create(
UserID=shangjia_id,
ProductTypeID=old_lianjie.ProductTypeID,
TemplateID=old_lianjie.TemplateID,
LinkURL=link_url,
LinkToken=secure_token,
OrderID=new_OrderID,
is_used=True, # 直接标记已使用
ExpireTime=timezone.now() + timezone.timedelta(days=1),
PreviousLinkID=old_lianjie.id, # 记录旧链接ID
PreviousOrderID=old_order.OrderID, # 记录旧订单ID
)
# ---- 6.5 异步通知(广播新订单、跨平台同步) ----
try:
order_info = {
'OrderID': new_OrderID,
'game_type': self._get_game_type_name(new_order.ProductTypeID),
'amount': str(new_order.Amount),
'order_desc': (new_order.Description or '')[:50],
}
dingdan_guangbo.delay(order_info)
except Exception as e:
logger.error(f"广播通知失败: {e}")
# 注意:此处不更新商家余额、发布量、每日统计,因为这是替换订单
logger.info(f"更换打手成功:旧订单{OrderID}→退款,新订单{new_OrderID}已生成")
return Response({
'code': 0,
'msg': '更换成功',
'data': {
'new_OrderID': new_OrderID,
'new_dingdan_id': new_OrderID,
'new_lianjie': link_url,
}
})
except Exception as e:
logger.error(f"生成新订单事务失败: {str(e)}", exc_info=True)
return Response({'code': 500, 'msg': '生成新订单失败'})
def _get_game_type_name(self, ProductTypeID):
"""根据类型ID获取游戏类型名称"""
try:
gt = ShangpinLeixing.query.filter(id=ProductTypeID).first()
return gt.jieshao if gt else '游戏订单'
except Exception:
return '游戏订单'
# ==================== 考核记录管理 ====================
SHENHE_ZHUANGTAI_MAP = {
0: '考核中',
1: '已通过',
2: '未通过',
3: '待审核',
}