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'] # 与前端约定的聊天权限码(原位于 shops.py,按功能组归类到客服模块) CHAT_PERM_CODES = ['abca1', 'baac2', 'cabc3', 'cb3a2', 'bcaa4'] class KefuGetDashouListView(APIView): """ 客服获取打手列表接口(仅打手类型) 需要拥有以下任一权限:001aa,001bb,001cc,001dd,001ee,001ff,001gg 请求:POST /yonghu/kefuhqdslb 参数: username: 客服手机号(用于权限校验) keyword: 搜索关键词(可选,搜索用户ID或昵称) page: 页码(从1开始) page_size: 每页数量 返回: code: 0 data: { list: [打手对象], has_more: bool, total: int, // 总打手数 valid_dashou_count: int // 有效打手数(有有效会员) } """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # 1. 获取参数(前端字段为 username) username = request.data.get('username', '').strip() keyword = request.data.get('keyword', '').strip() try: page = int(request.data.get('page', 1)) page_size = int(request.data.get('page_size', 20)) except ValueError: return Response({'code': 400, 'msg': '分页参数错误'}) if not username: return Response({'code': 401, 'msg': '认证失败'}) # 2. 公共权限校验(验证客服身份,获取权限列表) kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions # 直接返回错误响应 # 3. 检查是否拥有打手管理相关权限(至少一个) required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'} if not any(p in permissions for p in required_perms): return Response({'code': 403, 'msg': '您没有权限查看打手列表'}) # 4. 构建打手查询集(仅打手类型) dashou_qs = filter_user_related_by_club( UserDashou.query.select_related('user').all(), request, ) # 关键词筛选(用户ID或昵称) if keyword: dashou_qs = dashou_qs.filter( Q(user__UserUID__icontains=keyword) | Q(nicheng__icontains=keyword) ) # 5. 统计打手总数 total_dashou = dashou_qs.count() # 6. 统计有效打手数(有会员且未过期) dashou_user_ids = dashou_qs.values_list('user__UserUID', flat=True) now = timezone.now() hy_qs = filter_queryset_by_club(Huiyuangoumai.query.all(), request) valid_dashou_count = hy_qs.filter( yonghu_id__in=dashou_user_ids, huiyuan_zhuangtai=1, daoqi_time__gt=now, ).values('yonghu_id').distinct().count() # 7. 分页 offset = (page - 1) * page_size dashou_list = dashou_qs.order_by('-CreateTime')[offset:offset + page_size] # 8. 构建返回数据 result = [] for dashou in dashou_list: user = dashou.user result.append({ 'yonghuid': user.UserUID, 'avatar': user.Avatar or '', 'zaixianzhuangtai': dashou.zaixianzhuangtai, 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, 'nicheng': dashou.nicheng or '', 'zhuangtai': dashou.zhuangtai, }) has_more = (offset + len(dashou_list)) < total_dashou meta = list_response_meta(request) return Response({ 'code': 0, 'club_id': meta['club_id'], 'scope': meta['scope'], 'data': { 'list': result, 'has_more': has_more, 'total': total_dashou, 'valid_dashou_count': valid_dashou_count, } }) class KefuGetDashouDetailView(APIView): """ 客服获取打手详情接口 请求:POST /yonghu/kefuhqdsxq 参数:{ "username": "客服手机号", "uid": "打手ID (yonghuid)" } 认证:JWT 返回:code=0 + 打手信息 + 会员列表 + 所有会员类型 + 客服权限列表 """ permission_classes = [IsAuthenticated] parser_classes = [JSONParser] def post(self, request): # 1. 获取参数 username = request.data.get('phone', '').strip() uid = request.data.get('uid', '').strip() if not username or not uid: return Response({'code': 400, 'msg': '缺少参数'}) # 2. 公共权限校验(验证客服身份,获取权限列表) kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions # 3. 检查是否拥有打手管理相关权限(至少一个) required_perms = {'001aa', '001bb', '001cc', '001dd', '001ee', '001ff', '001gg'} if not any(p in permissions for p in required_perms): return Response({'code': 403, 'msg': '您没有权限查看打手详情'}) # 4. 查询打手主表和扩展表 try: user_main = User.query.select_related('DashouProfile').get( UserUID=uid ) except User.DoesNotExist: return Response({'code': 404, 'msg': '打手不存在'}) denied = forbid_if_user_out_of_scope(request, user_main) if denied: return denied try: dashou = user_main.DashouProfile except ObjectDoesNotExist: # 用户存在但未注册打手身份,返回基本信息 user_info = { 'yonghuid': user_main.UserUID, 'avatar': user_main.Avatar or '', 'zaixianzhuangtai': 0, 'zhanghaozhuangtai': 0, 'zhuangtai': 0, 'nicheng': '', 'chenghao': '', 'dianhua': '', 'wechat': '', 'phone': user_main.Phone or '', 'CreateTime': user_main.UserCreateTime, 'create_time': user_main.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user_main, 'UserCreateTime', None) else '', 'ip': user_main.IP or '', 'jiedanzongliang': 0, 'chengjiaozongliang': 0, 'tuikuanliang': 0, 'jinrijiedan': 0, 'yue': 0.00, 'zonge': 0.00, 'jifen': 0, 'yajin': 0.00, 'jinrishouyi': 0.00, 'jinyueshouyi': 0.00, 'yaoqingren': '', 'jieshao': '', } member_list = [] # 仍然需要返回 all_members 和 permissions all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao')) return Response({ 'code': 0, 'data': { 'user_info': user_info, 'member_list': member_list, 'all_members': all_members, 'permissions': permissions, } }) # 5. 构建打手信息 user_info = { 'yonghuid': user_main.UserUID, 'avatar': user_main.Avatar or '', 'zaixianzhuangtai': dashou.zaixianzhuangtai, 'zhanghaozhuangtai': dashou.zhanghaozhuangtai, 'zhuangtai': dashou.zhuangtai, 'nicheng': dashou.nicheng or '', 'chenghao': dashou.chenghao or '', 'dianhua': dashou.dianhua or '', 'wechat': dashou.wechat or '', 'phone': user_main.Phone or '', 'CreateTime': user_main.UserCreateTime, 'create_time': user_main.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user_main, 'UserCreateTime', None) else '', 'ip': user_main.IP or '', 'jiedanzongliang': dashou.jiedanzongliang, 'chengjiaozongliang': dashou.chengjiaozongliang, 'tuikuanliang': dashou.tuikuanliang, 'jinrijiedan': dashou.jinrijiedan, 'yue': float(dashou.yue) if dashou.yue else 0.00, 'zonge': float(dashou.zonge) if dashou.zonge else 0.00, 'jifen': dashou.jifen, 'yajin': float(dashou.yajin) if dashou.yajin else 0.00, 'jinrishouyi': float(dashou.jinrishouyi) if dashou.jinrishouyi else 0.00, 'jinyueshouyi': float(dashou.jinyueshouyi) if dashou.jinyueshouyi else 0.00, 'yaoqingren': dashou.yaoqingren or '', 'jieshao': dashou.jieshao or '', } # 6. 查询打手已拥有的会员(未过期) member_records = Huiyuangoumai.query.filter( yonghu_id=uid ).order_by('-CreateTime') # 批量预取会员信息 _huiyuan_ids = [r.huiyuan_id for r in member_records] _huiyuan_map = { h.huiyuan_id: h.jieshao for h in Huiyuan.query.filter(huiyuan_id__in=_huiyuan_ids).only('huiyuan_id', 'jieshao') } member_list = [] for record in member_records: # 检查是否过期 if hasattr(record, 'jiance_shifou_daoqi'): is_expired = record.jiance_shifou_daoqi() else: is_expired = record.daoqi_time < timezone.now() if record.daoqi_time else True if not is_expired: huiyuan_name = _huiyuan_map.get(record.huiyuan_id, '未知会员') member_list.append({ 'id': record.id, 'huiyuan_name': huiyuan_name, 'daoqi_time': record.daoqi_time, 'is_active': record.huiyuan_zhuangtai == 1, 'huiyuan_id': record.huiyuan_id, # 用于删除操作 }) # 7. 查询所有会员类型(用于前端添加会员) all_members = list(Huiyuan.query.all().values('huiyuan_id', 'jieshao')) return Response({ 'code': 0, 'data': { 'user_info': user_info, 'member_list': member_list, 'all_members': all_members, 'permissions': permissions, # 返回当前客服的权限列表 } }) class KefuUpdateDashouView(APIView): """ 客服修改打手信息接口 支持操作: - jian_yue / jia_yue : 减/加打手余额 (权限001aa/001bb) - jian_yajin / jia_yajin : 减/加打手押金 (权限001aa/001bb) - jian_jifen / jia_jifen : 减/加打手积分 (权限001cc/001dd) - gai_zhanghaozhuangtai : 修改打手账号状态(封禁/解封) (权限001ee) - gai_zaixianzhuangtai : 修改打手在线状态 (权限001ee) - gai_zhuangtai : 修改打手接单状态(空闲/忙碌) (权限001ee) - jia_huiyuan / jian_huiyuan : 加/减会员 (权限001ff/001gg) - gai_chenghao : 修改打手称号(升级/降级金牌) (权限001hh) """ permission_classes = [IsAuthenticated] def post(self, request): # 1. 获取前端参数 username = request.data.get('username', '').strip() dashou_id = request.data.get('dashou_id', '').strip() caozuo = request.data.get('caozuo', '').strip() value = request.data.get('value') # 金额、积分、天数、称号等 huiyuan_id = request.data.get('huiyuan_id', '').strip() if not username or not dashou_id or not caozuo: return Response({'code': 400, 'msg': '参数不完整'}) # 2. 权限校验 kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions # 3. 查询打手 try: dashou_user = User.query.get(UserUID=dashou_id) dashou_profile = dashou_user.DashouProfile except User.DoesNotExist: return Response({'code': 404, 'msg': '打手不存在'}) except UserDashou.DoesNotExist: return Response({'code': 404, 'msg': '打手扩展信息缺失'}) with transaction.atomic(): try: before = { 'yue': dashou_profile.yue, 'yajin': dashou_profile.yajin, 'jifen': dashou_profile.jifen, 'zhanghaozhuangtai': dashou_profile.zhanghaozhuangtai, 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, 'zhuangtai': dashou_profile.zhuangtai, 'chenghao': dashou_profile.chenghao, } # ---------- 余额操作 ---------- if caozuo == 'jian_yue': if '001aa' not in permissions: return Response({'code': 403, 'msg': '无权限减打手余额'}) amount = Decimal(str(value)) if dashou_profile.yue < amount: return Response({'code': 400, 'msg': '余额不足'}) dashou_profile.yue -= amount dashou_profile.save() after = dashou_profile.yue write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=XIUGAI_LEIXING_DASHOU, xiugaitijiao=after, xiugaitijiaoq=before['yue'], qitashuoming=( f'【后台】减少打手可提现余额:{before["yue"]}元 → {after}元,' f'减少{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}' ), ) return Response({'code': 0, 'msg': '减余额成功', 'data': {'new_yue': str(after)}}) elif caozuo == 'jia_yue': if '001bb' not in permissions: return Response({'code': 403, 'msg': '无权限加打手余额'}) from jituan.services.group_finance_gate import deny_group_finance_exec deny = deny_group_finance_exec(request.user, request) if deny: return deny amount = Decimal(str(value)) dashou_profile.yue += amount dashou_profile.save() after = dashou_profile.yue write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=XIUGAI_LEIXING_DASHOU, xiugaitijiao=after, xiugaitijiaoq=before['yue'], qitashuoming=( f'【后台】增加打手可提现余额:{before["yue"]}元 → {after}元,' f'增加{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}' ), ) return Response({'code': 0, 'msg': '加余额成功', 'data': {'new_yue': str(after)}}) # ---------- 押金操作 ---------- elif caozuo == 'jian_yajin': if '001aa' not in permissions: return Response({'code': 403, 'msg': '无权限减打手押金'}) amount = Decimal(str(value)) if dashou_profile.yajin < amount: return Response({'code': 400, 'msg': '押金不足'}) dashou_profile.yajin -= amount dashou_profile.save() after = dashou_profile.yajin write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=XIUGAI_LEIXING_DASHOU, yajin=after, yyajin=before['yajin'], qitashuoming=( f'【后台】减少打手押金:{before["yajin"]}元 → {after}元,' f'减少{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}' ), ) return Response({'code': 0, 'msg': '减押金成功', 'data': {'new_yajin': str(after)}}) elif caozuo == 'jia_yajin': if '001bb' not in permissions: return Response({'code': 403, 'msg': '无权限加打手押金'}) from jituan.services.group_finance_gate import deny_group_finance_exec deny = deny_group_finance_exec(request.user, request) if deny: return deny amount = Decimal(str(value)) dashou_profile.yajin += amount dashou_profile.save() after = dashou_profile.yajin write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=XIUGAI_LEIXING_DASHOU, yajin=after, yyajin=before['yajin'], qitashuoming=( f'【后台】增加打手押金:{before["yajin"]}元 → {after}元,' f'增加{amount}元,打手ID={dashou_id},操作人={kefu.user.Phone}' ), ) return Response({'code': 0, 'msg': '加押金成功', 'data': {'new_yajin': str(after)}}) # ---------- 积分操作 ---------- elif caozuo == 'jian_jifen': if '001cc' not in permissions: return Response({'code': 403, 'msg': '无权限减打手积分'}) jifen = int(value) if dashou_profile.jifen < jifen: return Response({'code': 400, 'msg': '积分不足'}) dashou_profile.jifen -= jifen dashou_profile.save() after = dashou_profile.jifen write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=XIUGAI_LEIXING_DASHOU, jifen=after, yjifen=before['jifen'], qitashuoming=( f'【后台】减少打手积分:{before["jifen"]} → {after},' f'减少{jifen}分,打手ID={dashou_id},操作人={kefu.user.Phone}' ), ) return Response({'code': 0, 'msg': '减积分成功', 'data': {'new_jifen': after}}) elif caozuo == 'jia_jifen': if '001dd' not in permissions: return Response({'code': 403, 'msg': '无权限加打手积分'}) jifen = int(value) dashou_profile.jifen += jifen dashou_profile.save() after = dashou_profile.jifen write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=XIUGAI_LEIXING_DASHOU, jifen=after, yjifen=before['jifen'], qitashuoming=( f'【后台】增加打手积分:{before["jifen"]} → {after},' f'增加{jifen}分,打手ID={dashou_id},操作人={kefu.user.Phone}' ), ) return Response({'code': 0, 'msg': '加积分成功', 'data': {'new_jifen': after}}) # ---------- 状态修改 ---------- elif caozuo == 'gai_zhanghaozhuangtai': if '001ee' not in permissions: return Response({'code': 403, 'msg': '无权限修改打手账号状态'}) new_status = int(value) if new_status not in [0, 1]: return Response({'code': 400, 'msg': '状态值无效(0=封禁,1=正常)'}) dashou_profile.zhanghaozhuangtai = new_status dashou_profile.save() status_label = '正常' if new_status == 1 else '封禁' old_label = '正常' if before['zhanghaozhuangtai'] == 1 else '封禁' write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=XIUGAI_LEIXING_DASHOU, qitashuoming=( f'【后台】修改打手账号状态:{old_label} → {status_label},' f'打手ID={dashou_id},操作人={kefu.user.Phone}' ), ) return Response({'code': 0, 'msg': '修改账号状态成功', 'data': {'new_zhanghaozhuangtai': new_status}}) elif caozuo == 'gai_zaixianzhuangtai': if '001ee' not in permissions: return Response({'code': 403, 'msg': '无权限修改打手在线状态'}) new_status = int(value) if new_status not in [0, 1]: return Response({'code': 400, 'msg': '状态值无效(0=离线,1=在线)'}) dashou_profile.zaixianzhuangtai = new_status dashou_profile.save() status_label = '在线' if new_status == 1 else '离线' old_label = '在线' if before['zaixianzhuangtai'] == 1 else '离线' write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=XIUGAI_LEIXING_DASHOU, qitashuoming=( f'【后台】修改打手在线状态:{old_label} → {status_label},' f'打手ID={dashou_id},操作人={kefu.user.Phone}' ), ) return Response({'code': 0, 'msg': '修改在线状态成功', 'data': {'new_zaixianzhuangtai': new_status}}) elif caozuo == 'gai_zhuangtai': if '001ee' not in permissions: return Response({'code': 403, 'msg': '无权限修改打手接单状态'}) new_status = int(value) if new_status not in [0, 1]: return Response({'code': 400, 'msg': '状态值无效(0=忙碌,1=空闲)'}) dashou_profile.zhuangtai = new_status dashou_profile.save() status_label = '空闲' if new_status == 1 else '忙碌' old_label = '空闲' if before['zhuangtai'] == 1 else '忙碌' write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=XIUGAI_LEIXING_DASHOU, qitashuoming=( f'【后台】修改打手接单状态:{old_label} → {status_label},' f'打手ID={dashou_id},操作人={kefu.user.Phone}' ), ) return Response({'code': 0, 'msg': '修改接单状态成功', 'data': {'new_zhuangtai': new_status}}) # ---------- 会员操作 ---------- elif caozuo == 'jia_huiyuan': if '001ff' not in permissions and '000001' not in permissions: return Response({'code': 403, 'msg': '无权限给打手加会员'}) if not huiyuan_id: return Response({'code': 400, 'msg': '缺少会员ID'}) try: huiyuan = Huiyuan.query.get(huiyuan_id=huiyuan_id) except Huiyuan.DoesNotExist: return Response({'code': 404, 'msg': '会员类型不存在'}) from jituan.services.club_user import get_user_club_id from jituan.services.member_recharge import extend_member_entitlement_admin, get_club_huiyuan_row player_club = get_user_club_id(dashou_user) row = get_club_huiyuan_row(player_club, huiyuan_id, require_enabled=False) days = int(value) if value else int(row.formal_days or 30) if row else 30 record, err = extend_member_entitlement_admin( dashou_id, huiyuan_id, days, player_club, ) if err: return Response({'code': 400, 'msg': err}) write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=XIUGAI_LEIXING_DASHOU, huiyuants=days, huiyuan_id=huiyuan_id, qitashuoming=( f'【后台】给打手添加正式会员:会员ID={huiyuan_id}({huiyuan.jieshao}),' f'增加{days}天,到期时间={record.daoqi_time.strftime("%Y-%m-%d %H:%M:%S")},' f'打手ID={dashou_id},操作人={kefu.user.Phone}(无支付单、无分红)' ), ) return Response({'code': 0, 'msg': '加会员成功', 'data': {'new_daoqi': record.daoqi_time.strftime('%Y-%m-%d %H:%M:%S')}}) elif caozuo == 'jian_huiyuan': if '001gg' not in permissions: return Response({'code': 403, 'msg': '无权限给打手减会员'}) if not huiyuan_id: return Response({'code': 400, 'msg': '缺少会员ID'}) from jituan.services.member_recharge import revoke_member_entitlement_admin removed, err = revoke_member_entitlement_admin(dashou_id, huiyuan_id) if err: return Response({'code': 404, 'msg': err}) write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=XIUGAI_LEIXING_DASHOU, huiyuan_id=huiyuan_id, qitashuoming=( f'【后台】移除打手会员:会员ID={huiyuan_id},' f'打手ID={dashou_id},操作人={kefu.user.Phone}' ), ) return Response({'code': 0, 'msg': '减会员成功'}) # ---------- 称号修改(金牌打手) ---------- elif caozuo == 'gai_chenghao': if '001hh' not in permissions: return Response({'code': 403, 'msg': '无权限修改打手称号'}) new_chenghao = str(value).strip() if not new_chenghao: return Response({'code': 400, 'msg': '称号不能为空'}) # 可限制合法值,这里只记录日志 dashou_profile.chenghao = new_chenghao dashou_profile.save() write_xiugai_log( yonghuid=dashou_id, xiugaiid=kefu.user.Phone, leixing=XIUGAI_LEIXING_DASHOU, qitashuoming=( f'【后台】修改打手称号:{before["chenghao"] or "无"} → {new_chenghao},' f'打手ID={dashou_id},操作人={kefu.user.Phone}' ), ) return Response({'code': 0, 'msg': '修改称号成功', 'data': {'new_chenghao': new_chenghao}}) else: return Response({'code': 400, 'msg': '未知操作类型'}) except Exception as e: logger.error(f"修改打手信息异常: {str(e)}", exc_info=True) return Response({'code': 500, 'msg': '服务器内部错误'}) class KefuGetShangjiaListView(APIView): """ 客服/管理员获取商家列表接口 需要拥有以下任一权限:1100a, 1100b 请求:POST /houtai/hqsjgllb 参数: username: 客服手机号(用于权限校验,必填) keyword: 搜索关键词(可选,搜索商家用户ID或昵称) balance_op: 余额比较符,gte(>=) / lte(<=)(可选) balance_value: 余额数值(可选,需与balance_op同时存在) order_count_op: 派单量比较符,gte(>=) / lte(<=)(可选) order_count_value: 派单量数值(可选,需与order_count_op同时存在) status: 商家状态,1=正常,2=禁用(可选) page: 页码,从1开始,默认1 page_size: 每页数量,默认20 返回: code: 0 成功,其他失败 data: { list: [商家对象], total: int, // 总商家数 } """ permission_classes = [] # 不依赖全局认证,手动校验 parser_classes = [JSONParser] def post(self, request): # 1. 获取参数 username = request.data.get('username', '').strip() keyword = request.data.get('keyword', '').strip() balance_op = request.data.get('balance_op', '').strip().lower() balance_value_str = request.data.get('balance_value') order_count_op = request.data.get('order_count_op', '').strip().lower() order_count_value_str = request.data.get('order_count_value') status_str = request.data.get('status') 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 ValueError: return Response({'code': 400, 'msg': '分页参数错误'}) if not username: return Response({'code': 401, 'msg': '认证失败,缺少username'}) # 2. 公共权限校验(复用现有函数 verify_kefu_permission) # 假设 verify_kefu_permission 定义在某个公共模块,这里引入 kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions # 直接返回错误响应 # 3. 检查商家管理权限(1100a 或 1100b) required_perms = {'1100a', '1100b'} if not any(p in permissions for p in required_perms): return Response({'code': 403, 'msg': '您没有权限查看商家列表'}) # 4. 构建商家查询集(商家扩展表 + 关联用户主表) shangjia_qs = filter_user_related_by_club( UserShangjia.query.select_related('user').all(), request, ) # 关键词筛选(用户ID或昵称) if keyword: shangjia_qs = shangjia_qs.filter( Q(user__UserUID__icontains=keyword) | Q(nicheng__icontains=keyword) ) # 余额筛选 if balance_op and balance_value_str is not None and balance_value_str != '': try: balance_value = Decimal(str(balance_value_str)) if balance_op == 'gte': shangjia_qs = shangjia_qs.filter(yue__gte=balance_value) elif balance_op == 'lte': shangjia_qs = shangjia_qs.filter(yue__lte=balance_value) else: return Response({'code': 400, 'msg': '余额比较符只支持gte或lte'}) except Exception: return Response({'code': 400, 'msg': '余额数值格式错误'}) # 派单总量筛选(fabu字段) if order_count_op and order_count_value_str is not None and order_count_value_str != '': try: order_count_value = int(order_count_value_str) if order_count_op == 'gte': shangjia_qs = shangjia_qs.filter(fabu__gte=order_count_value) elif order_count_op == 'lte': shangjia_qs = shangjia_qs.filter(fabu__lte=order_count_value) else: return Response({'code': 400, 'msg': '派单量比较符只支持gte或lte'}) except Exception: return Response({'code': 400, 'msg': '派单量数值必须为整数'}) # 商家状态筛选 if status_str is not None and status_str != '': try: status_val = int(status_str) if status_val not in [1, 2]: return Response({'code': 400, 'msg': '商家状态值无效,应为1或2'}) shangjia_qs = shangjia_qs.filter(zhuangtai=status_val) except ValueError: return Response({'code': 400, 'msg': '商家状态格式错误'}) # 5. 统计总商家数(应用筛选后) _agg = shangjia_qs.aggregate( total=Count('id'), valid=Count('id', filter=Q(zhuangtai=1)), ) total = _agg['total'] valid_merchant_count = _agg['valid'] # 6. 分页 offset = (page - 1) * page_size shangjia_list = shangjia_qs.order_by('-CreateTime')[offset:offset + page_size] # 7. 构建返回数据 result = [] for shangjia in shangjia_list: user = shangjia.user result.append({ 'yonghuid': user.UserUID, 'avatar': user.Avatar or '', 'nicheng': shangjia.nicheng or '', 'yue': str(shangjia.yue), # 转换为字符串,避免前端精度问题 'fabu': shangjia.fabu, 'chengjiao': shangjia.chengjiao, 'zhuangtai': shangjia.zhuangtai, # 1=正常,2=禁用 }) meta = list_response_meta(request) return Response({ 'code': 0, 'club_id': meta['club_id'], 'scope': meta['scope'], 'data': { 'list': result, 'total': total, 'valid_merchant_count': valid_merchant_count, } }) # yonghu/views.py 中添加 class KefuGetShangjiaDetailView(APIView): """ 客服/管理员获取商家详情接口 需要权限:1100a 或 1100b 请求:POST /houtai/hthqsjxq 参数: username: 客服手机号(用于权限校验,必填) yonghuid: 商家用户ID(必填) 返回: code: 0 成功 data: 商家所有字段(含主表头像等) """ 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 = {'1100a', '1100b'} if not any(p in permissions for p in required_perms): return Response({'code': 403, 'msg': '您没有权限查看商家详情'}) # 查询商家 try: shangjia = UserShangjia.query.select_related('user').get(user__UserUID=yonghuid) except UserShangjia.DoesNotExist: return Response({'code': 404, 'msg': '商家不存在'}) denied = forbid_if_user_out_of_scope(request, shangjia.user) if denied: return denied user = shangjia.user data = { 'yonghuid': user.UserUID, 'avatar': user.Avatar or '', 'nicheng': shangjia.nicheng, 'zhuangtai': shangjia.zhuangtai, # 1正常,2禁用 'dianhua': shangjia.dianhua, 'wechat': shangjia.wechat, 'yue': str(shangjia.yue), 'fabu': shangjia.fabu, 'chengjiao': shangjia.chengjiao, 'tuikuan': shangjia.tuikuan, 'jinridingdan': shangjia.jinridingdan, 'jinriliushui': str(shangjia.jinriliushui), 'jinyuedingdan': shangjia.jinyuedingdan, 'jinyueliushui': str(shangjia.jinyueliushui), 'is_youzhi_shangjia': bool(shangjia.is_youzhi_shangjia), 'CreateTime': shangjia.CreateTime.isoformat() if shangjia.CreateTime else None, 'create_time': user.UserCreateTime.strftime('%Y-%m-%d %H:%M:%S') if getattr(user, 'UserCreateTime', None) else '', } return Response({'code': 0, 'data': data}) class KefuUpdateShangjiaView(APIView): """ 客服/管理员修改商家信息(余额、状态) POST /houtai/xgsjxx 参数: username, yonghuid, action action可选:add_balance, sub_balance, ban, unban amount(add_balance/sub_balance时必填) """ permission_classes = [] parser_classes = [JSONParser] def post(self, request): username = request.data.get('username', '').strip() yonghuid = request.data.get('yonghuid', '').strip() action = request.data.get('action', '').strip().lower() if not username: return Response({'code': 401, 'msg': '缺少username'}) if not yonghuid: return Response({'code': 400, 'msg': '缺少商家ID'}) if action not in ('add_balance', 'sub_balance', 'ban', 'unban', 'set_youzhi'): return Response({'code': 400, 'msg': 'action参数无效'}) # 权限校验 kefu, permissions = verify_kefu_permission(request, username) if kefu is None: return permissions # 操作权限校验 if action in ('add_balance', 'ban', 'unban', 'set_youzhi') and '1100a' not in permissions: return Response({'code': 403, 'msg': '无权限执行此操作(需要1100a)'}) if action == 'sub_balance' and '1100b' not in permissions: return Response({'code': 403, 'msg': '无权限减少商家余额(需要1100b)'}) # 使用事务,select_for_update 必须在事务内 with transaction.atomic(): try: shangjia = UserShangjia.objects.select_for_update().get(user__UserUID=yonghuid) except UserShangjia.DoesNotExist: return Response({'code': 404, 'msg': '商家不存在'}) # 增加余额 if action == 'add_balance': from jituan.services.group_finance_gate import deny_group_finance_exec deny = deny_group_finance_exec(request.user, request) if deny: return deny amount_str = request.data.get('amount') try: amount = Decimal(str(amount_str)) if amount <= 0 or amount > 1000000: return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'}) except: return Response({'code': 400, 'msg': '金额格式错误'}) old_yue = shangjia.yue new_yue = shangjia.yue + amount if new_yue > 999999999.99: return Response({'code': 400, 'msg': '增加后余额超出上限'}) shangjia.yue = new_yue shangjia.save(update_fields=['yue']) write_xiugai_log( yonghuid=yonghuid, xiugaiid=kefu.user.Phone, leixing=XIUGAI_LEIXING_SHANGJIA, shangjiayue=new_yue, shangjiayueq=old_yue, qitashuoming=( f'【后台】增加商家余额:{old_yue}元 → {new_yue}元,增加{amount}元,' f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}' ), ) return Response({'code': 0, 'msg': '余额增加成功', 'data': {'new_balance': str(shangjia.yue)}}) # 减少余额 if action == 'sub_balance': amount_str = request.data.get('amount') try: amount = Decimal(str(amount_str)) if amount <= 0 or amount > 1000000: return Response({'code': 400, 'msg': '金额需在0.01~1000000之间'}) except: return Response({'code': 400, 'msg': '金额格式错误'}) if shangjia.yue < amount: return Response({'code': 400, 'msg': '余额不足'}) old_yue = shangjia.yue shangjia.yue = shangjia.yue - amount shangjia.save(update_fields=['yue']) write_xiugai_log( yonghuid=yonghuid, xiugaiid=kefu.user.Phone, leixing=XIUGAI_LEIXING_SHANGJIA, shangjiayue=shangjia.yue, shangjiayueq=old_yue, qitashuoming=( f'【后台】减少商家余额:{old_yue}元 → {shangjia.yue}元,减少{amount}元,' f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}' ), ) return Response({'code': 0, 'msg': '余额减少成功', 'data': {'new_balance': str(shangjia.yue)}}) # 封禁 if action == 'ban': if shangjia.zhuangtai == 2: return Response({'code': 400, 'msg': '商家已是封禁状态'}) shangjia.zhuangtai = 2 shangjia.save(update_fields=['zhuangtai']) write_xiugai_log( yonghuid=yonghuid, xiugaiid=kefu.user.Phone, leixing=XIUGAI_LEIXING_SHANGJIA, qitashuoming=( f'【后台】封禁商家账号:正常 → 禁用,' f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}' ), ) return Response({'code': 0, 'msg': '商家已封禁', 'data': {'zhuangtai': 2}}) # 解封 if action == 'unban': if shangjia.zhuangtai == 1: return Response({'code': 400, 'msg': '商家已是正常状态'}) shangjia.zhuangtai = 1 shangjia.save(update_fields=['zhuangtai']) write_xiugai_log( yonghuid=yonghuid, xiugaiid=kefu.user.Phone, leixing=XIUGAI_LEIXING_SHANGJIA, qitashuoming=( f'【后台】解封商家账号:禁用 → 正常,' f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}' ), ) return Response({'code': 0, 'msg': '商家已解封', 'data': {'zhuangtai': 1}}) if action == 'set_youzhi': if '1100a' not in permissions: return Response({'code': 403, 'msg': '无权限设置优质商家(需要1100a)'}) is_youzhi = bool(request.data.get('is_youzhi_shangjia', False)) shangjia.is_youzhi_shangjia = is_youzhi shangjia.save(update_fields=['is_youzhi_shangjia']) write_xiugai_log( yonghuid=yonghuid, xiugaiid=kefu.user.Phone, leixing=XIUGAI_LEIXING_SHANGJIA, qitashuoming=( f'【后台】设置优质商家:{is_youzhi},' f'商家ID={yonghuid},昵称={shangjia.nicheng},操作人={kefu.user.Phone}' ), ) return Response({ 'code': 0, 'msg': '优质商家设置成功', 'data': {'is_youzhi_shangjia': is_youzhi}, }) return Response({'code': 400, 'msg': '未知操作'}) class KefuChatPermissionsView(APIView): """ 获取客服聊天权限及GoEasy配置 POST /houtai/kfhqltqx Body: { "phone": "客服手机号" } """ permission_classes = [IsAuthenticated] def post(self, request): # 1. 从前端获取传递的账号,用于防越权校验 username_from_frontend = request.data.get('phone', None) # 2. 调用公共验证方法。 # 返回 (kefu_obj, permissions) 或 (None, Response) kefu_obj, permissions = verify_kefu_permission(request, username_from_frontend) # 3. 如果第一个返回值为 None,说明验证失败,直接返回第二个值(错误Response) if kefu_obj is None: return permissions # ---------- 验证通过,permissions 已经是权限码列表 ---------- # 4. 检查是否拥有至少一个聊天权限 has_chat_perm = any(p in CHAT_PERM_CODES for p in permissions) # 5. 若有聊天权限,获取 GoEasy AppKey goeasy_appkey = '' if has_chat_perm: goeasy_appkey = getattr(settings, 'GOEASY_APPKEY', '') # 6. 返回数据 return Response({ 'code': 0, 'msg': 'ok', 'data': { 'permissions': permissions, 'goeasy_appkey': goeasy_appkey } })