尝试将 backend 做了视图拆分,来保证可维护性,若此版本存在生产环境问题应该回退
This commit is contained in:
0
backend/views/__init__.py
Normal file
0
backend/views/__init__.py
Normal file
720
backend/views/exams.py
Normal file
720
backend/views/exams.py
Normal 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
968
backend/views/finance.py
Normal 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, # 商品类型ID,0表示全部
|
||||
"order_source": 1, # 1=平台单,2=商家单,0=全部
|
||||
"summary_date": "2026-05-16"
|
||||
}
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if 'dingdancaiwu' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无订单财务查看权限'}, status=403)
|
||||
|
||||
granularity = request.data.get('granularity', 'day')
|
||||
year = request.data.get('year')
|
||||
month = request.data.get('month')
|
||||
type_id = request.data.get('type_id', 0)
|
||||
order_source = request.data.get('order_source', 0)
|
||||
summary_date = request.data.get('summary_date')
|
||||
|
||||
now = date.today()
|
||||
|
||||
# ---------- 时间范围 ----------
|
||||
if granularity == 'day':
|
||||
if not year or not month:
|
||||
return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
|
||||
start_date = date(year, month, 1)
|
||||
end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
|
||||
trunc_func = TruncDate('CreateTime')
|
||||
if not summary_date:
|
||||
summary_date = now.strftime('%Y-%m-%d')
|
||||
summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
|
||||
elif granularity == 'month':
|
||||
if not year:
|
||||
return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
|
||||
start_date = date(year, 1, 1)
|
||||
end_date = date(year + 1, 1, 1)
|
||||
trunc_func = TruncMonth('CreateTime')
|
||||
if not summary_date:
|
||||
summary_date = now.strftime('%Y-%m')
|
||||
summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
|
||||
elif granularity == 'year':
|
||||
year = year or now.year
|
||||
start_date = date(year, 1, 1)
|
||||
end_date = date(year + 1, 1, 1)
|
||||
trunc_func = TruncYear('CreateTime')
|
||||
if not summary_date:
|
||||
summary_date = str(year)
|
||||
summary_q = date(year, 1, 1)
|
||||
else:
|
||||
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
|
||||
|
||||
start_dt = datetime.combine(start_date, datetime.min.time())
|
||||
end_dt = datetime.combine(end_date, datetime.min.time())
|
||||
|
||||
# ---------- 基础过滤 ----------
|
||||
filters = {
|
||||
'CreateTime__gte': start_dt,
|
||||
'CreateTime__lt': end_dt,
|
||||
'Status__in': [1, 2, 3, 4, 5, 6, 7, 8] # 只统计有效订单状态
|
||||
}
|
||||
if int(type_id) != 0:
|
||||
filters['ProductTypeID'] = type_id
|
||||
if int(order_source) in [1, 2]:
|
||||
filters['Platform'] = order_source
|
||||
|
||||
# ---------- 时间序列分组查询 ----------
|
||||
order_base = filter_queryset_by_club(
|
||||
Order.query.filter(**filters), request, club_field='ClubID',
|
||||
)
|
||||
qs = order_base \
|
||||
.annotate(period=trunc_func) \
|
||||
.values('period') \
|
||||
.annotate(
|
||||
# 订单总量(所有符合条件的状态)
|
||||
order_count=Count('id'),
|
||||
# 订单总额
|
||||
order_amount=Sum('Amount'),
|
||||
# 成交单量 (Status=3)
|
||||
success_count=Count('id', filter=Q(Status=3)),
|
||||
# 成交订单总额
|
||||
success_amount=Sum('Amount', filter=Q(Status=3)),
|
||||
# 退款单量 (Status=5)
|
||||
refund_count=Count('id', filter=Q(Status=5)),
|
||||
# 打手分成总额(仅成交订单)
|
||||
PlayerCommission_sum=Sum('PlayerCommission', filter=Q(Status=3)),
|
||||
# 平台成交订单的店铺分红
|
||||
dianpu_fenhong_sum=Sum('pingtai_kuozhan__ShopIncome',
|
||||
filter=Q(Status=3, Platform=1)),
|
||||
# 成交订单中:平台单金额、商家单金额、各自打手分成
|
||||
platform_success_amount=Sum('Amount', filter=Q(Status=3, Platform=1)),
|
||||
merchant_success_amount=Sum('Amount', filter=Q(Status=3, Platform=2)),
|
||||
platform_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=1)),
|
||||
merchant_dashou=Sum('PlayerCommission', filter=Q(Status=3, Platform=2)),
|
||||
).order_by('period')
|
||||
|
||||
time_series = []
|
||||
for item in qs:
|
||||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||||
item['period'].strftime('%Y')
|
||||
|
||||
# 平台收益计算
|
||||
platform_profit = (item['platform_success_amount'] or 0) \
|
||||
- (item['platform_dashou'] or 0) \
|
||||
- (item['dianpu_fenhong_sum'] or 0)
|
||||
merchant_profit = (item['merchant_success_amount'] or 0) \
|
||||
- (item['merchant_dashou'] or 0)
|
||||
total_profit = platform_profit + merchant_profit
|
||||
|
||||
time_series.append({
|
||||
'date': period_str,
|
||||
'order_count': item['order_count'],
|
||||
'order_amount': float(item['order_amount'] or 0),
|
||||
'success_count': item['success_count'],
|
||||
'success_amount': float(item['success_amount'] or 0),
|
||||
'refund_count': item['refund_count'],
|
||||
'dashou_fencheng': float(item['PlayerCommission_sum'] or 0),
|
||||
'dianpu_fenhong': float(item['dianpu_fenhong_sum'] or 0),
|
||||
'profit': round(total_profit, 2)
|
||||
})
|
||||
|
||||
# ---------- 汇总数据 ----------
|
||||
summary_qs = filter_queryset_by_club(
|
||||
Order.query.filter(**filters), request, club_field='ClubID',
|
||||
)
|
||||
total_order_count = summary_qs.count()
|
||||
total_order_amount = summary_qs.aggregate(s=Sum('Amount'))['s'] or 0
|
||||
total_success_count = summary_qs.filter(Status=3).count()
|
||||
total_success_amount = summary_qs.filter(Status=3).aggregate(s=Sum('Amount'))['s'] or 0
|
||||
total_refund_count = summary_qs.filter(Status=5).count()
|
||||
total_dashou = summary_qs.filter(Status=3).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
||||
total_dianpu_fenhong = summary_qs.filter(Status=3, Platform=1).aggregate(
|
||||
s=Sum('pingtai_kuozhan__ShopIncome'))['s'] or 0
|
||||
|
||||
platform_amount = summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('Amount'))['s'] or 0
|
||||
merchant_amount = summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('Amount'))['s'] or 0
|
||||
platform_dashou_sum = \
|
||||
summary_qs.filter(Status=3, Platform=1).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
||||
merchant_dashou_sum = \
|
||||
summary_qs.filter(Status=3, Platform=2).aggregate(s=Sum('PlayerCommission'))['s'] or 0
|
||||
|
||||
profit_platform = platform_amount - platform_dashou_sum - total_dianpu_fenhong
|
||||
profit_merchant = merchant_amount - merchant_dashou_sum
|
||||
total_profit = profit_platform + profit_merchant
|
||||
|
||||
summary = {
|
||||
'total_order_count': total_order_count,
|
||||
'total_order_amount': round(float(total_order_amount), 2),
|
||||
'total_success_count': total_success_count,
|
||||
'total_success_amount': round(float(total_success_amount), 2),
|
||||
'total_refund_count': total_refund_count,
|
||||
'total_PlayerCommission': round(float(total_dashou), 2),
|
||||
'total_dianpu_fenhong': round(float(total_dianpu_fenhong), 2),
|
||||
'total_profit': round(total_profit, 2)
|
||||
}
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': 'ok',
|
||||
'data': {
|
||||
'time_series': time_series,
|
||||
'summary': summary
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception("CwhqjtddsjView 接口错误")
|
||||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class CwqtczhqView(APIView):
|
||||
"""
|
||||
其他充值/罚款数据统计(非会员充值)
|
||||
POST /houtai/cwqtczhq
|
||||
Body: {
|
||||
"phone": "管理员手机号",
|
||||
"granularity": "day" | "month" | "year",
|
||||
"year": 2026,
|
||||
"month": 5, // day时必填
|
||||
"types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款
|
||||
"summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY)
|
||||
}
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if 'qtczcaiwu' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403)
|
||||
|
||||
from jituan.services.finance_detail_stats import build_chongzhi_finance_payload
|
||||
|
||||
granularity = request.data.get('granularity', 'day')
|
||||
year = request.data.get('year')
|
||||
month = request.data.get('month')
|
||||
types = request.data.get('types', [2, 3, 4, 5, 6])
|
||||
summary_date = request.data.get('summary_date')
|
||||
|
||||
if isinstance(types, list) and len(types) == 0:
|
||||
types = [2, 3, 4, 5, 6]
|
||||
|
||||
data, err = build_chongzhi_finance_payload(
|
||||
request, granularity, year, month, types, summary_date,
|
||||
)
|
||||
if err:
|
||||
return Response({'code': 1, 'msg': err}, status=400)
|
||||
|
||||
return Response({'code': 0, 'msg': 'ok', 'data': data})
|
||||
except Exception as e:
|
||||
logger.exception("CwqtczhqView 接口错误")
|
||||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)
|
||||
|
||||
'''class CwqtczhqView_LEGACY(APIView):
|
||||
"""
|
||||
其他充值/罚款数据统计(非会员充值)
|
||||
POST /houtai/cwqtczhq
|
||||
Body: {
|
||||
"phone": "管理员手机号",
|
||||
"granularity": "day" | "month" | "year",
|
||||
"year": 2026,
|
||||
"month": 5, // day时必填
|
||||
"types": [2,3,4,5,6], // 2=押金,3=积分,4=商家,5=考核,6=罚款
|
||||
"summary_date": "2026-05-17" // 汇总日期,格式随颗粒度(day:YYYY-MM-DD, month:YYYY-MM, year:YYYY)
|
||||
}
|
||||
"""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
try:
|
||||
username_from_frontend = request.data.get('phone', None)
|
||||
kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend)
|
||||
if kefu_obj is None:
|
||||
return permissions
|
||||
if 'qtczcaiwu' not in permissions:
|
||||
return Response({'code': 403, 'msg': '无其他充值财务查看权限'}, status=403)
|
||||
|
||||
granularity = request.data.get('granularity', 'day')
|
||||
year = request.data.get('year')
|
||||
month = request.data.get('month')
|
||||
types = request.data.get('types', [2, 3, 4, 5, 6]) # 默认全部
|
||||
summary_date = request.data.get('summary_date')
|
||||
|
||||
if isinstance(types, list) and len(types) == 0:
|
||||
types = [2, 3, 4, 5, 6]
|
||||
|
||||
now = date.today()
|
||||
|
||||
# ---------- 时间范围(用于曲线图) ----------
|
||||
if granularity == 'day':
|
||||
if not year or not month:
|
||||
return Response({'code': 1, 'msg': '按日统计需要年份和月份'}, status=400)
|
||||
start_date = date(year, month, 1)
|
||||
end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
|
||||
trunc_func = TruncDate('CreateTime')
|
||||
# 默认汇总日期
|
||||
if not summary_date:
|
||||
summary_date = now.strftime('%Y-%m-%d')
|
||||
summary_q = datetime.strptime(summary_date, '%Y-%m-%d').date()
|
||||
elif granularity == 'month':
|
||||
if not year:
|
||||
return Response({'code': 1, 'msg': '按月统计需要年份'}, status=400)
|
||||
start_date = date(year, 1, 1)
|
||||
end_date = date(year + 1, 1, 1)
|
||||
trunc_func = TruncMonth('CreateTime')
|
||||
if not summary_date:
|
||||
summary_date = now.strftime('%Y-%m')
|
||||
summary_q = datetime.strptime(summary_date + '-01', '%Y-%m-%d').date()
|
||||
elif granularity == 'year':
|
||||
year = year or now.year
|
||||
start_date = date(year, 1, 1)
|
||||
end_date = date(year + 1, 1, 1)
|
||||
trunc_func = TruncYear('CreateTime')
|
||||
if not summary_date:
|
||||
summary_date = str(year)
|
||||
summary_q = date(year, 1, 1)
|
||||
else:
|
||||
return Response({'code': 1, 'msg': '无效的颗粒度'}, status=400)
|
||||
|
||||
start_dt = datetime.combine(start_date, datetime.min.time())
|
||||
end_dt = datetime.combine(end_date, datetime.min.time())
|
||||
|
||||
# ---------- 构建时间序列 ----------
|
||||
time_series_map = {} # period_str -> { type_code: {count, amount, profit, fenhong_count, fenhong_amount} }
|
||||
|
||||
def ensure_date(ds):
|
||||
if ds not in time_series_map:
|
||||
time_series_map[ds] = {}
|
||||
return time_series_map[ds]
|
||||
|
||||
# 1. 处理充值记录(Czjilu)
|
||||
cz_types = [t for t in types if t in [2, 3, 4, 5]]
|
||||
if cz_types:
|
||||
cz_qs = Czjilu.query.filter(
|
||||
CreateTime__gte=start_dt,
|
||||
CreateTime__lt=end_dt,
|
||||
leixing__in=cz_types,
|
||||
zhuangtai=3 # 只统计支付成功的
|
||||
).annotate(
|
||||
period=trunc_func
|
||||
).values('period', 'leixing').annotate(
|
||||
total_count=Count('id'),
|
||||
total_amount=Sum('jine')
|
||||
).order_by('period', 'leixing')
|
||||
|
||||
for item in cz_qs:
|
||||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||||
item['period'].strftime('%Y')
|
||||
lx = item['leixing']
|
||||
amount = float(item['total_amount'] or 0)
|
||||
count = item['total_count']
|
||||
# 收益:积分(3)收益为金额本身,其他为0
|
||||
profit = amount if lx == 3 else 0.0
|
||||
|
||||
day_data = ensure_date(period_str)
|
||||
day_data[str(lx)] = {
|
||||
'count': count,
|
||||
'amount': amount,
|
||||
'profit': profit,
|
||||
'fenhong_count': 0, # 充值类型无分红
|
||||
'fenhong_amount': 0.0
|
||||
}
|
||||
|
||||
# 2. 处理罚款(Penalty)—— 包含分红
|
||||
if 6 in types:
|
||||
# 罚款记录
|
||||
penalty_qs = Penalty.query.filter(
|
||||
CreateTime__gte=start_dt,
|
||||
CreateTime__lt=end_dt,
|
||||
Status=2 # 已缴纳
|
||||
).annotate(
|
||||
period=trunc_func
|
||||
).values('period').annotate(
|
||||
penalty_count=Count('id'),
|
||||
penalty_amount=Sum('FineAmount')
|
||||
).order_by('period')
|
||||
|
||||
# 分红记录
|
||||
fenhong_qs = PenaltyBonus.query.filter(
|
||||
CreateTime__gte=start_dt,
|
||||
CreateTime__lt=end_dt
|
||||
).annotate(
|
||||
period=trunc_func
|
||||
).values('period').annotate(
|
||||
fh_count=Count('id'),
|
||||
fh_amount=Sum('WithdrawableAmount')
|
||||
).order_by('period')
|
||||
|
||||
# 合并
|
||||
for item in penalty_qs:
|
||||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||||
item['period'].strftime('%Y')
|
||||
day_data = ensure_date(period_str)
|
||||
day_data['6'] = {
|
||||
'count': item['penalty_count'],
|
||||
'amount': float(item['penalty_amount'] or 0),
|
||||
'profit': 0.0, # 先设为0,后续计算
|
||||
'fenhong_count': 0,
|
||||
'fenhong_amount': 0.0
|
||||
}
|
||||
|
||||
for item in fenhong_qs:
|
||||
period_str = item['period'].strftime('%Y-%m-%d') if granularity == 'day' else \
|
||||
item['period'].strftime('%Y-%m') if granularity == 'month' else \
|
||||
item['period'].strftime('%Y')
|
||||
day_data = ensure_date(period_str)
|
||||
if '6' not in day_data:
|
||||
day_data['6'] = {'count': 0, 'amount': 0.0, 'profit': 0.0, 'fenhong_count': 0, 'fenhong_amount': 0.0}
|
||||
day_data['6']['fenhong_count'] += item['fh_count']
|
||||
day_data['6']['fenhong_amount'] += float(item['fh_amount'] or 0)
|
||||
|
||||
# 计算罚款收益 = 罚款金额 - 分红金额
|
||||
for period_str, day_data in time_series_map.items():
|
||||
if '6' in day_data:
|
||||
penalty = day_data['6']
|
||||
penalty['profit'] = round(penalty['amount'] - penalty['fenhong_amount'], 2)
|
||||
|
||||
# 构造有序时间序列列表
|
||||
time_series = []
|
||||
for period_str in sorted(time_series_map.keys()):
|
||||
entry = {'date': period_str, 'types': {}}
|
||||
for type_code in sorted(time_series_map[period_str].keys()):
|
||||
entry['types'][type_code] = time_series_map[period_str][type_code]
|
||||
time_series.append(entry)
|
||||
|
||||
# ---------- 汇总指定日期 ----------
|
||||
summary_result = {'by_type': {}, 'total_count': 0, 'total_amount': 0.0, 'total_profit': 0.0}
|
||||
|
||||
# 汇总充值类型
|
||||
for lx in cz_types:
|
||||
filter_kwargs = {
|
||||
'leixing': lx,
|
||||
'zhuangtai': 3,
|
||||
}
|
||||
if granularity == 'day':
|
||||
filter_kwargs['CreateTime__date'] = summary_q
|
||||
elif granularity == 'month':
|
||||
filter_kwargs['CreateTime__year'] = summary_q.year
|
||||
filter_kwargs['CreateTime__month'] = summary_q.month
|
||||
else:
|
||||
filter_kwargs['CreateTime__year'] = summary_q.year
|
||||
agg = filter_queryset_by_club(
|
||||
Czjilu.query.filter(**filter_kwargs), request,
|
||||
).aggregate(count=Count('id'), amount=Sum('jine'))
|
||||
count = agg['count'] or 0
|
||||
amount = float(agg['amount'] or 0)
|
||||
profit = amount if lx == 3 else 0.0
|
||||
summary_result['by_type'][str(lx)] = {
|
||||
'count': count,
|
||||
'amount': round(amount, 2),
|
||||
'profit': round(profit, 2),
|
||||
'fenhong_count': 0,
|
||||
'fenhong_amount': 0.0
|
||||
}
|
||||
summary_result['total_count'] += count
|
||||
summary_result['total_amount'] += amount
|
||||
summary_result['total_profit'] += profit
|
||||
|
||||
# 汇总罚款
|
||||
if 6 in types:
|
||||
penalty_filter = {
|
||||
'Status': 2,
|
||||
}
|
||||
fenhong_filter = {}
|
||||
if granularity == 'day':
|
||||
penalty_filter['CreateTime__date'] = summary_q
|
||||
fenhong_filter['CreateTime__date'] = summary_q
|
||||
elif granularity == 'month':
|
||||
penalty_filter['CreateTime__year'] = summary_q.year
|
||||
penalty_filter['CreateTime__month'] = summary_q.month
|
||||
fenhong_filter['CreateTime__year'] = summary_q.year
|
||||
fenhong_filter['CreateTime__month'] = summary_q.month
|
||||
else:
|
||||
penalty_filter['CreateTime__year'] = summary_q.year
|
||||
fenhong_filter['CreateTime__year'] = summary_q.year
|
||||
|
||||
penalty_agg = Penalty.query.filter(**penalty_filter).aggregate(
|
||||
count=Count('id'), amount=Sum('FineAmount'))
|
||||
penalty_count = penalty_agg['count'] or 0
|
||||
penalty_amount = float(penalty_agg['amount'] or 0)
|
||||
|
||||
fenhong_agg = PenaltyBonus.query.filter(**fenhong_filter).aggregate(
|
||||
fh_count=Count('id'), fh_amount=Sum('WithdrawableAmount'))
|
||||
fh_count = fenhong_agg['fh_count'] or 0
|
||||
fh_amount = float(fenhong_agg['fh_amount'] or 0)
|
||||
|
||||
profit = round(penalty_amount - fh_amount, 2)
|
||||
summary_result['by_type']['6'] = {
|
||||
'count': penalty_count,
|
||||
'amount': round(penalty_amount, 2),
|
||||
'profit': profit,
|
||||
'fenhong_count': fh_count,
|
||||
'fenhong_amount': round(fh_amount, 2)
|
||||
}
|
||||
summary_result['total_count'] += penalty_count
|
||||
summary_result['total_amount'] += penalty_amount
|
||||
summary_result['total_profit'] += profit
|
||||
|
||||
summary_result['total_amount'] = round(summary_result['total_amount'], 2)
|
||||
summary_result['total_profit'] = round(summary_result['total_profit'], 2)
|
||||
|
||||
return Response({
|
||||
'code': 0,
|
||||
'msg': 'ok',
|
||||
'data': {
|
||||
'time_series': time_series,
|
||||
'summary': summary_result
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception("CwqtczhqView 接口错误")
|
||||
return Response({'code': 1, 'msg': f'服务器内部错误: {str(e)}'}, status=500)'''
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ==================== 后端接口 ====================
|
||||
# 文件:views.py (添加以下两个视图类)
|
||||
|
||||
|
||||
|
||||
817
backend/views/guanli.py
Normal file
817
backend/views/guanli.py
Normal 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. 余额修改(加:4400c;减:4400b)----------
|
||||
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
1147
backend/views/kefu.py
Normal file
File diff suppressed because it is too large
Load Diff
351
backend/views/members.py
Normal file
351
backend/views/members.py
Normal 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
733
backend/views/penalties.py
Normal 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
1140
backend/views/products.py
Normal file
File diff suppressed because it is too large
Load Diff
806
backend/views/roles.py
Normal file
806
backend/views/roles.py
Normal 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': '该账号已存在'})
|
||||
|
||||
# 生成唯一的 yonghuid(7位数字)
|
||||
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
354
backend/views/sections.py
Normal 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
939
backend/views/shops.py
Normal 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
1159
backend/views/system.py
Normal file
File diff suppressed because it is too large
Load Diff
347
backend/views/titles.py
Normal file
347
backend/views/titles.py
Normal 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 ====================
|
||||
# 请确保已有以下导入,如果没有请在文件头部添加
|
||||
|
||||
|
||||
|
||||
|
||||
170
backend/views/withdraw_settings.py
Normal file
170
backend/views/withdraw_settings.py
Normal 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
791
backend/views/zuzhang.py
Normal 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_xiane):6600d
|
||||
- 修改分红相关(永久分红、定制首次、额外次数):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. 可提现金额修改(加:6600a;减:6600b)----------
|
||||
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
357
backend/views/zxkf.py
Normal 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: '待审核',
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user