"""users.views.user_info - auto-generated by split script.""" import os import sys import json import re import time import uuid import random import string import requests import hashlib import traceback import decimal import jwt import ipaddress import xmltodict from datetime import datetime, timedelta from decimal import Decimal import defusedxml.ElementTree as ET from django.conf import settings from django.db import models, transaction, IntegrityError, DatabaseError, connection from django.db.models import Q, F, Sum, Count, Case, When, IntegerField, Prefetch from django.core.cache import cache from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from django.core.exceptions import ObjectDoesNotExist from django.shortcuts import get_object_or_404 from django.utils import timezone from django.http import HttpResponse from django.views import View from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.hashers import make_password from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import permissions, status from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.parsers import MultiPartParser, FormParser, JSONParser from rest_framework.throttling import AnonRateThrottle from rest_framework_simplejwt.tokens import RefreshToken from rest_framework_simplejwt.authentication import JWTAuthentication from gvsdsdk.fluent import db, func, FQ from utils.oss_utils import validate_image, upload_to_oss, delete_from_oss from utils.money import yuan_to_fen from utils.fadan_utils import check_fadan_qiangdan_eligible from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode from users.fadan_fenhong_utils import process_fadan_fenhong from orders.utils import ( update_daily_payout, settle_shangjia_order_guanshi_fenhong ) from backend.utils import ( update_dashou_daily_by_action, update_guanshi_daily_by_action, update_shangjia_daily, update_zuzhang_daily_by_action, verify_kefu_permission ) from rank.utils import get_tag_fee, create_shenhe_jilu, validate_shenheguan from ..models import ( UserBoss, UserDashou, UserShangjia, UserGuanshi, UserZuzhang, UserKefu, AdminProfile, OfficialAccountUser, TixianAutoRecord, TixianShenheJilu, Tixianjilu, Xiugaijilu, RankingRecord ) from users.business_models import User from ..tixian_shenhe_services import ( load_audit_meta_map, refund_balance, sync_audit_and_jilu_status, mark_transfer_success, release_collect_quota_for_record, close_audit_wechat_failed, query_wechat_transfer_bill, check_shijidaozhang_within_limit, check_dashou_trial_blocks_commission_withdraw, WX_STATE_FAIL, WX_STATE_SUCCESS, WX_STATE_WAIT, WX_STATE_CANCELING, ) from ..tixian_shenhe_views import process_audit_collect from orders.models import ( Order, PlatformOrderExt, MerchantOrderExt, CommissionRate, PenaltyRecord, PenaltyEvidenceImage, RefundRecord, PlayerDeliveryImage, Penalty, PenaltyAppealImage ) from products.models import ( ShangpinLeixing, Huiyuan, Huiyuangoumai, Gsfenhong, Czjilu ) from config.models import Qunpeizhi from backend.models import MerchantDailyStats from rank.models import ( KaohePayTemp, Chenghao, ShenheJilu, KaoheCishuFeiyong, YonghuChenghao ) from users.models import UserShenheguan from products.utils import update_shangpin_daily_stat from shop.utils import update_dianpu_daily_stat from rank.utils import create_shenhe_jilu_from_temp import logging logger = logging.getLogger(__name__) def huoquYonghuDaili(request): """ 获取用户浏览器/设备信息 """ user_agent = request.META.get('HTTP_USER_AGENT', '') # 这里可以添加更复杂的用户代理解析 # 为简化,我们只返回原始字符串的前100个字符 return user_agent[:100] if user_agent else '' def huoquKehuduanIP(request): """ 强化版:安全、兼容地获取客户端真实IP地址 """ ip = None # 1. 从标准头部获取IP链 x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: # 按逗号分割,取第一个(即最原始的客户端IP) ip = x_forwarded_for.split(',')[0].strip() # 2. 如果上面没有,则用Nginx传递的X-Real-IP if not ip: ip = request.META.get('HTTP_X_REAL_IP') # 3. 最后降级到连接地址 if not ip: ip = request.META.get('REMOTE_ADDR') # ===== 核心:清洗和验证IP格式 ===== if ip: # 情况A: 处理IPv4映射的IPv6地址 (::ffff:192.168.1.1) if ip.startswith('::ffff:'): ip = ip[7:] # 情况B: 处理带端口的IPv4地址 (223.91.110.123:62417) # 这是导致您记录“三段”的最可能原因! # 如果IP字符串中有冒号,且最后一个冒号前的部分是IPv4,则分离端口。 if ':' in ip and '.' in ip: # 尝试按冒号分割,取第一部分 possible_ip = ip.rsplit(':', 1)[0] # 从右边分割一次 # 验证它是否是有效的IPv4地址 try: ipaddress.IPv4Address(possible_ip) # 如果通过验证,说明格式正确 ip = possible_ip except (ipaddress.AddressValueError, ValueError): # 如果验证失败,说明可能不是IPv4带端口,保持原样或进行其他处理 pass # 情况C: 验证最终结果是否为有效IPv4(可选,但推荐) try: # 此步骤会抛出异常,如果ip变量此时仍是“223.91.110.”这种格式 ipaddress.IPv4Address(ip) except (ipaddress.AddressValueError, ValueError): # 记录一个警告日志,并将IP标记为无效或存为原始字符串 print(f"警告:获取到无效或非标准IP地址格式: {ip}") # 您可以选择在此处 return None 或存储原始字符串以供排查 return ip class UserInfoUpdateView(APIView): permission_classes = [permissions.IsAuthenticated] parser_classes = [MultiPartParser, FormParser, JSONParser] # 进程内缓存,防止同一code被重复调用微信(单进程安全,多进程小概率重复,但用户极少可忽略) _phone_cache = {} def post(self, request): try: user = request.user boss = self._get_or_create_boss(user) result = {} nick = request.data.get('nicheng') # 诊断日志:记录入参和实例状态 logger.warning( "[USERINFO_XIUGAI] 入参 content_type=%s, data_keys=%s, files_keys=%s, " "user_pk=%s, user_UserUID=%s, boss_pk=%s, boss_nickname_before=%r", request.content_type, list(request.data.keys()) if hasattr(request, 'data') else 'N/A', list(request.FILES.keys()), getattr(user, 'pk', None), getattr(user, 'UserUID', None), getattr(boss, 'pk', None), getattr(boss, 'nickname', None), ) # 头像 av = request.FILES.get('avatar') if av: ok, msg = validate_image(av) if not ok: return Response({'code': 1, 'msg': msg, 'data': None}, status=400) url = self._upload_avatar(user, av) if url: result['touxiang'] = url user.Avatar = url else: return Response({'code': 2, 'msg': '头像上传失败', 'data': None}, status=500) # 昵称 if nick: nick = str(nick).strip() if len(nick) > 20: return Response({'code': 3, 'msg': '昵称过长', 'data': None}, status=400) boss.nickname = nick result['nicheng'] = nick # 手机号 —— multipart 表单字段兼容 request.data / request.POST code = ( request.data.get('shoujihao_code') or request.POST.get('shoujihao_code') or '' ) if isinstance(code, str): code = code.strip() else: code = str(code or '').strip() # 认证页仅上传头像+手机号,不带昵称 is_phone_auth_flow = bool(av) and not nick phone_error = None if is_phone_auth_flow and not code: return Response({ 'code': 6, 'msg': '未收到手机号授权,请重新点击获取手机号', 'data': None, }, status=200) if code: phone = self._get_phone_safe(code, request) if phone is None: phone_error = '手机号获取失败,请重新授权' else: user.Phone = phone user.PhoneVerified = True result['phone'] = phone result['phone_verified'] = True if not result and not phone_error: logger.warning("[USERINFO_XIUGAI] 无更新,result 为空") return Response({'code': 5, 'msg': '无更新', 'data': None}, status=400) # 诊断日志:save 前的状态 logger.warning( "[USERINFO_XIUGAI] 准备保存 result_keys=%s, boss_pk=%s, boss_nickname_after=%r, " "user_Avatar=%r, user_Phone=%r, phone_error=%r", list(result.keys()), getattr(boss, 'pk', None), getattr(boss, 'nickname', None), getattr(user, 'Avatar', None), getattr(user, 'Phone', None), phone_error, ) # 认证流:必须拿到手机号才算成功,避免只存头像却返回 code=0 导致反复弹窗 if is_phone_auth_flow and 'phone' not in result: msg = phone_error or '手机号获取失败,请重新授权' if result: with transaction.atomic(): user_update_fields = [] if 'touxiang' in result: user_update_fields.append('Avatar') if user_update_fields: user.save(update_fields=user_update_fields) if 'nicheng' in result: boss.save() return Response({'code': 6, 'msg': msg, 'data': result or None}, status=200) # 有字段更新时先保存(资料编辑:手机号失败不阻塞头像/昵称) if result: user_update_fields = [] if 'touxiang' in result: user_update_fields.append('Avatar') if 'phone' in result: user_update_fields.extend(['Phone', 'PhoneVerified']) with transaction.atomic(): if user_update_fields: user.save(update_fields=user_update_fields) elif result: user.save() boss.save() logger.warning("[USERINFO_XIUGAI] 保存成功 result_keys=%s", list(result.keys())) if phone_error: return Response({'code': 6, 'msg': phone_error, 'data': result}, status=200) return Response({'code': 0, 'msg': '更新成功', 'data': result}) except Exception as e: logger.error("[USERINFO_XIUGAI] 异常: %s", str(e), exc_info=True) return Response({'code': 99, 'msg': str(e), 'data': None}, status=500) # ---------- 安全手机号(内存去重) ---------- def _get_phone_safe(self, code, request): cache_key = f'{code}:{self._resolve_wx_club_id(request, request.user)}' if cache_key in self._phone_cache: cached = self._phone_cache[cache_key] return cached if cached != '__FAIL__' else None self._phone_cache[cache_key] = '__PROCESSING__' try: phone = self._call_wechat(code, request) self._phone_cache[cache_key] = phone return phone except Exception as e: logger.error(f"微信调用失败: {str(e)}") self._phone_cache[cache_key] = '__FAIL__' return None def _resolve_wx_club_id(self, request, user): """手机号 code 与小程序 appid 绑定,须用对应俱乐部的 wx 配置换 token。""" from jituan.constants import CLUB_ID_DEFAULT from jituan.services.club_context import resolve_club_id_from_request from jituan.services.club_user import get_user_club_id club_id = resolve_club_id_from_request(request) if club_id == CLUB_ID_DEFAULT: user_club = get_user_club_id(user) if user_club: return user_club return club_id # ---------- 微信接口调用(按俱乐部 appid 换 token) ---------- def _call_wechat(self, code, request): club_id = self._resolve_wx_club_id(request, request.user) token = self._get_new_token(club_id, force_refresh=True) url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}' resp = requests.post(url, json={'code': code}, timeout=10) data = resp.json() errcode = data.get('errcode') if errcode == 0: return data['phone_info']['purePhoneNumber'] if errcode == 40001: token = self._get_new_token(club_id, force_refresh=True) url = f'https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={token}' resp = requests.post(url, json={'code': code}, timeout=10) data = resp.json() if data.get('errcode') == 0: return data['phone_info']['purePhoneNumber'] raise Exception(f"微信错误(errcode={errcode}): {data.get('errmsg')}") def _get_new_token(self, club_id, force_refresh=False): from utils.weixin_token import get_club_mini_access_token token = get_club_mini_access_token(club_id, force_refresh=force_refresh) if token: return token raise Exception(f'俱乐部 {club_id} 缺少微信配置或获取 access_token 失败') # ---------- 工具 ---------- def _get_or_create_boss(self, user): try: return UserBoss.query.get(user=user) except UserBoss.DoesNotExist: return UserBoss.query.create(user=user, nickname='微信用户') def _upload_avatar(self, user, file): try: uid = user.UserUID if not uid: return None old = user.Avatar if old: delete_from_oss(old) ext = os.path.splitext(file.name)[1] or '.jpg' name = f"{uuid.uuid4().hex}{ext}" path = f"avatar/{uid}/{name}" return path if upload_to_oss(file, path) else None except: return None class DashouXinxiAPIView(APIView): """ 获取打手信息接口 请求方式:POST 认证方式:JWT Token 返回:打手基本信息、资产信息、会员信息,金牌打手额外返回专属字段 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取当前登录用户 current_user = request.user yonghuid = current_user.UserUID # 2. 查询打手扩展表 try: dashou_profile = current_user.DashouProfile except UserDashou.DoesNotExist: return Response({ 'code': 400, 'message': '您不是打手身份', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 3. 构建基础信息响应(原有字段完全保留) data = { 'dashounicheng': dashou_profile.nicheng or '', 'zhanghaostatus': dashou_profile.zhanghaozhuangtai, 'zaixianzhuangtai': dashou_profile.zaixianzhuangtai, 'dashouzhuangtai': dashou_profile.zhuangtai, 'yongjin': str(dashou_profile.yue) if dashou_profile.yue is not None else '0.00', 'zonge': str(dashou_profile.zonge) if dashou_profile.zonge is not None else '0.00', 'yajin': str(dashou_profile.yajin) if dashou_profile.yajin is not None else '0.00', 'jiedanZongliang':dashou_profile.jiedanzongliang if dashou_profile.jiedanzongliang is not None else '0', 'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00', 'chengjiaoliang': dashou_profile.chengjiaozongliang, 'jifen': dashou_profile.jifen, 'chenghao': dashou_profile.chenghao or '', } # 4. 查询会员信息(每次请求同步权益 + clumber,体验=正式) try: from jituan.services.huiyuan_bundle import refresh_grab_pool_membership_state from jituan.services.club_context import resolve_effective_club_id cid = resolve_effective_club_id(request, request.user) _, huiyuan_list = refresh_grab_pool_membership_state(yonghuid, club_id=cid) except Exception: huiyuan_list = [] data['clumber'] = huiyuan_list # 5. 如果是金牌打手,添加专属字段 if dashou_profile.chenghao == "金牌打手": data.update({ 'jiedanZongliang': dashou_profile.jiedanzongliang, # 接单总量 'jinriJiedan': str(dashou_profile.jinrishouyi) if dashou_profile.jinrishouyi is not None else '0.00', # 今日接单金额 'zuijinTixian': str(dashou_profile.jinritixian_jine) if dashou_profile.jinritixian_jine is not None else '0.00', # 最近提现金额(今日已提现) }) # 6. 返回成功响应 return Response({ 'code': 200, 'message': '获取成功', 'data': data }, status=status.HTTP_200_OK) except Exception as e: # 生产环境应使用 logging 记录错误,而不是 print # logger.error(f"获取打手信息失败: {e}", exc_info=True) return Response({ 'code': 500, 'message': f'服务器内部错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class HuoQuYaoQingRenView(APIView): """ 获取当前打手的邀请人信息 POST /yonghu/hqwdyqr Headers: Authorization: Bearer 返回邀请人的用户ID、昵称(来自老板扩展表)、头像相对URL """ permission_classes = [IsAuthenticated] def post(self, request): try: current_user = request.user # 当前登录用户 yonghuid = current_user.UserUID # 7位用户ID # 1. 校验打手身份及账号状态(只检查账号状态,不检查接单状态) try: dashou = UserDashou.query.get(user=current_user) if dashou.zhanghaozhuangtai != 1: return Response({ 'code': 403, 'message': '您的账号已被封禁,无法查看邀请人', 'data': None }) except UserDashou.DoesNotExist: return Response({ 'code': 404, 'message': '您还不是打手,无法查看邀请人', 'data': None }) # 2. 获取邀请人ID yaoqingren_id = dashou.yaoqingren if not yaoqingren_id: return Response({ 'code': 404, 'message': '您没有邀请人', 'data': None }) # 3. 查询邀请人主表(头像) try: inviter_main = User.query.get(UserUID=yaoqingren_id) touxiang = inviter_main.Avatar or '' except User.DoesNotExist: return Response({ 'code': 404, 'message': '邀请人信息不存在', 'data': None }) # 4. 查询老板扩展表(昵称),如果没有则使用空字符串 nicheng = '' try: boss = UserBoss.query.get(user=inviter_main) nicheng = boss.nickname or '' except UserBoss.DoesNotExist: pass # 邀请人可能不是老板,昵称留空 response_data = { 'uid': yaoqingren_id, 'nicheng': nicheng, 'touxiang': touxiang # 相对路径,前端自行拼接 } return Response({ 'code': 200, 'message': '获取成功', 'data': response_data }) except Exception as e: logger.error(f'获取邀请人失败: {str(e)}', exc_info=True) return Response({ 'code': 500, 'message': '系统繁忙,请稍后重试', 'data': None }) class ZaixianZhuangtaiAPIView(APIView): """ 更改打手在线状态接口 请求方式:POST 认证方式:JWT Token 请求参数:status(0=离线,1=在线) 返回:更新后的状态信息 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 1. 获取请求参数 new_status = request.data.get('status') if new_status is None: return Response({ 'code': 400, 'message': '缺少状态参数', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 转换为整数,确保是0或1 try: new_status = int(new_status) if new_status not in [0, 1]: return Response({ 'code': 400, 'message': '状态参数必须是0或1', 'data': None }, status=status.HTTP_400_BAD_REQUEST) except ValueError: return Response({ 'code': 400, 'message': '状态参数必须是整数', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 2. 获取当前登录用户 current_user = request.user yonghuid = current_user.UserUID # 3. 查询打手扩展表 try: dashou_profile = current_user.DashouProfile except UserDashou.DoesNotExist: return Response({ 'code': 400, 'message': '您不是打手身份', 'data': None }, status=status.HTTP_400_BAD_REQUEST) # 4. 检查账号状态(是否被封禁) if dashou_profile.zhanghaozhuangtai != 1: return Response({ 'code': 403, 'message': '账号状态异常,无法更改在线状态', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 5. 更新在线状态 old_status = dashou_profile.zaixianzhuangtai # 如果状态没有变化,直接返回成功 if old_status == new_status: return Response({ 'code': 200, 'message': '状态未变化', 'data': { 'zaixianzhuangtai': new_status, 'status_text': '在线' if new_status == 1 else '离线' } }, status=status.HTTP_200_OK) # 更新状态 dashou_profile.zaixianzhuangtai = new_status dashou_profile.save() # 6. 返回成功响应 return Response({ 'code': 200, 'message': '状态更新成功', 'data': { 'zaixianzhuangtai': new_status, 'status_text': '在线' if new_status == 1 else '离线' } }, status=status.HTTP_200_OK) except Exception as e: return Response({ 'code': 500, 'message': f'服务器内部错误: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class GuanshiXinxiView(APIView): """ 获取管事信息接口 路径:/yonghu/guanshixinxi 方法:POST 权限:JWT认证 功能:获取当前管事的详细信息,包括基本信息、分红信息以及已充值打手数量 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 获取当前用户(通过JWT认证) current_user = request.user # 查询管事扩展表 try: guanshi_profile = UserGuanshi.query.get(user=current_user) # 🔥【新增】查询已充值打手数量(从分红表中统计) # Gsfenhong 表中 guanshi 字段存储管事ID(yonghuid) yichongzhi_count = Gsfenhong.query.filter(guanshi=current_user.UserUID).count() # 返回管事信息 return Response({ 'code': 200, 'msg': '获取成功', 'data': { 'gszhstatus': guanshi_profile.zhuangtai, # 管事账号状态 'yaoqingzongshu': guanshi_profile.yaogingshuliang, # 邀请打手总数 'fenyongzonge': float(guanshi_profile.chongzhifenrun), # 充值分佣总额 'fenyongtixian': float(guanshi_profile.yue), # 可提现余额 'yichongzhiDashou': yichongzhi_count, # 已充值打手数量 } }) except UserGuanshi.DoesNotExist: # 用户不是管事,返回错误 return Response({ 'code': 400, 'msg': '您还不是管事,请先注册', 'data': None }, status=400) except Exception as e: # 捕获所有异常,返回错误信息并记录日志 logger.error(f"获取管事信息异常: {str(e)}", exc_info=True) return Response({ 'code': 500, 'msg': f'获取信息失败: {str(e)}', 'data': None }, status=500) class ShangJiaXinXiView(APIView): """ 获取商家信息接口 (POST /yonghu/shangjiaxinxi) 权限:需JWT认证 返回:商家基本信息 + 称号列表 + 今日/本月统计 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 获取商家扩展资料 shangjia = request.user.ShopProfile except ObjectDoesNotExist: return Response({ 'code': 4001, 'msg': '您还未注册成为商家或商家资料不存在', 'data': None }, status=status.HTTP_200_OK) user = request.user yonghuid = user.UserUID today = timezone.now().date() this_month_start = today.replace(day=1) # ------------------- 1. 从 UserShangjia 获取基础字段 ------------------- response_data = { 'nicheng': shangjia.nicheng or '', 'sjzhzhuangtai': shangjia.zhuangtai, # 商家账号状态 'fadanzong': shangjia.fabu, # 发单总量(累计) 'fabu': shangjia.fabu, 'tuikuanzong': shangjia.tuikuan, # 退款总量(累计) 'sjyue': float(shangjia.yue), # 商家余额 # 'riliushui' 和 'yueliushui' 将从统计表计算,不再使用原字段 } # ------------------- 2. 获取称号列表(仅限商家称号) ------------------- chenghao_list = [] # 查询当前用户的所有称号关联 yonghu_chenghao_qs = YonghuChenghao.query.filter(yonghu=user).select_related('chenghao') for yc in yonghu_chenghao_qs: chenghao_obj = yc.chenghao # 只保留类型为“商家”的称号 if chenghao_obj.leixing == 'shangjia': chenghao_list.append({ 'mingcheng': chenghao_obj.mingcheng, 'texiao_json': chenghao_obj.texiao_miaoshu, # 前端需要的特效描述 # 如果还需要其他字段(如图标URL)可在此添加,打手端有 texiao_json }) response_data['chenghao_list'] = chenghao_list # ------------------- 3. 从 MerchantDailyStats 获取今日统计 ------------------- try: today_stat = MerchantDailyStats.query.get(MerchantID=yonghuid, Date=today) jinri_paifa_dingdan = today_stat.AssignedOrderCount jinri_paifa_jine = float(today_stat.AssignedAmount) jinri_tuikuan_dingdan = today_stat.RefundOrderCount except MerchantDailyStats.DoesNotExist: jinri_paifa_dingdan = 0 jinri_paifa_jine = 0.00 jinri_tuikuan_dingdan = 0 response_data['jinripaidan'] = jinri_paifa_dingdan # 今日派单数(新增) response_data['jinrituikuan'] = jinri_tuikuan_dingdan # 今日退款数(新增) response_data['riliushui'] = jinri_paifa_jine # 今日流水(派发总金额) # ------------------- 4. 从 MerchantDailyStats 获取本月统计 ------------------- month_stats = MerchantDailyStats.query.filter( MerchantID=yonghuid, Date__gte=this_month_start, Date__lte=today ).aggregate( total_paifa_jine=Sum('AssignedAmount'), total_paifa_dingdan=Sum('AssignedOrderCount') ) yue_paifa_jine = float(month_stats.get('total_paifa_jine') or 0.00) # yue_paifa_dingdan = month_stats.get('total_paifa_dingdan') or 0 # 如果前端不需要可不加 response_data['yueliushui'] = yue_paifa_jine # 今月流水(派发总金额) # (可选)如果前端还需要今月派单总数,取消下面注释 # response_data['jinyuepaidan'] = yue_paifa_dingdan return Response({ 'code': 200, 'msg': '获取商家信息成功', 'data': response_data }, status=status.HTTP_200_OK) class ShangjiaNichengGengxinView(APIView): """商家老板修改昵称 POST /yonghu/shangjiagxnc""" permission_classes = [IsAuthenticated] def post(self, request): from merchant_ops.services.authz import is_merchant_owner if not is_merchant_owner(request.user): return Response({'code': 403, 'msg': '仅商家老板可修改昵称', 'data': None}, status=status.HTTP_200_OK) nicheng = (request.data.get('nicheng') or '').strip() if not nicheng: return Response({'code': 400, 'msg': '昵称不能为空', 'data': None}, status=status.HTTP_200_OK) if len(nicheng) > 20: return Response({'code': 400, 'msg': '昵称不能超过20字', 'data': None}, status=status.HTTP_200_OK) try: shangjia = request.user.ShopProfile except ObjectDoesNotExist: return Response({'code': 4001, 'msg': '商家资料不存在', 'data': None}, status=status.HTTP_200_OK) if UserShangjia.query.filter(nicheng=nicheng).exclude(user=request.user).exists(): return Response({'code': 400, 'msg': '该昵称已被使用', 'data': None}, status=status.HTTP_200_OK) shangjia.nicheng = nicheng shangjia.save(update_fields=['nicheng']) return Response({'code': 200, 'msg': '昵称已更新', 'data': {'nicheng': nicheng}}, status=status.HTTP_200_OK) class BossBiaoshiView(APIView): """ 获取用户的GoEasy标识 - 极简实现 POST /yonghu/bossbiaoshi 需要JWT Token认证 """ permission_classes = [IsAuthenticated] def post(self, request): # 1. 直接从request.user获取用户 user = request.user # 2. 获取yonghuid(确保用户已登录) if not user or not hasattr(user, 'UserUID'): return Response({ 'code': 401, 'msg': '用户未认证或用户信息不完整' }, status=401) # 3. 生成标识:Boss + yonghuid(与前端约定) boss_biaoshi = f"Boss{user.UserUID}" # 4. 返回给前端 return Response({ 'code': 200, 'msg': '获取成功', 'data': { 'boss_biaoshi': boss_biaoshi, 'yonghuid': user.UserUID, 'user_type': user.UserType } }) class YaoqingmaView(APIView): """ 获取或生成管事邀请码 访问路径: /api/yonghu/yaoqingma/ 请求方法: GET 权限要求: JWT Token认证通过的用户 """ permission_classes = [IsAuthenticated] def post(self, request): """ 获取当前用户的邀请码 逻辑: 1. 通过JWT获取当前用户 (request.user) 2. 反向查询UserGuanshi扩展表 3. 验证管事状态(zhuangtai=1) 4. 已有邀请码直接返回,没有则生成并保存 """ # 获取当前认证用户 current_user = request.user # 1. 使用反向查询获取管事扩展信息 (性能最优方式) try: guanshi_profile = current_user.GuanshiProfile except UserGuanshi.DoesNotExist: # 用户不是管事,没有扩展表记录 return Response({ 'code': 403, 'message': '当前用户不是管事身份,无法生成邀请码', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 2. 验证管事状态 (zhuangtai 必须等于 1) if guanshi_profile.zhuangtai != 1: return Response({ 'code': 403, 'message': '管事账号状态异常,无法生成邀请码', 'data': None }, status=status.HTTP_403_FORBIDDEN) # 3. 检查是否已存在邀请码 existing_yaoqingma = guanshi_profile.yaoqingma # 如果存在且非空,直接返回 if existing_yaoqingma and existing_yaoqingma.strip() != '': return Response({ 'code': 200, 'message': '成功获取现有邀请码', 'data': { 'yaoqingma': existing_yaoqingma, 'is_new': False # 标记是否为新生成的 } }) # 4. 生成新的邀请码 (使用用户ID字符串) # 注意: 这里使用yonghuid,不是主键id,如你要求的"123456"这种形式 yonghuid_str = str(current_user.UserUID) new_yaoqingma = CreateInvitationCode(yonghuid_str) # 5. 验证生成的邀请码格式 if not VerifyInvitationCode(new_yaoqingma): return Response({ 'code': 500, 'message': '邀请码生成失败,格式验证错误', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 6. 保存到数据库 (使用事务确保数据一致性) try: with transaction.atomic(): guanshi_profile.yaoqingma = new_yaoqingma guanshi_profile.save() except Exception as e: # 这里可以记录日志 return Response({ 'code': 500, 'message': f'保存邀请码到数据库失败: {str(e)}', 'data': None }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # 7. 返回新生成的邀请码 return Response({ 'code': 200, 'message': '成功生成新的邀请码', 'data': { 'yaoqingma': new_yaoqingma, 'is_new': True # 标记是新生成的 } }) class DashouZiliaoHuoquView(APIView): """ 打手资料获取接口 URL: /yonghu/dszjxxhq 方法: POST 认证: JWT Token 返回: 打手扩展表信息 """ permission_classes = [IsAuthenticated] def post(self, request): try: # 使用select_related高效查询,避免N+1问题 dashou = UserDashou.query.select_related('user').get(user=request.user) # 构建返回数据 data = { 'nicheng': dashou.nicheng if dashou.nicheng else '', 'jieshao': dashou.jieshao if dashou.jieshao else '', 'dianhua': dashou.dianhua if dashou.dianhua else '', 'wechat': dashou.wechat if dashou.wechat else '', 'uid': request.user.UserUID, # 从主表获取UID 'avatar': request.user.Avatar if request.user.Avatar else '', # 头像相对URL } return Response({ 'code': 200, 'msg': 'success', 'data': data }, status=status.HTTP_200_OK) except UserDashou.DoesNotExist: logger.warning(f'打手扩展表不存在: user_id={request.user.id}') return Response({ 'code': 404, 'msg': '打手信息不存在' }, status=status.HTTP_404_NOT_FOUND) except Exception as e: logger.error(f'获取打手资料异常: {str(e)}', exc_info=True) return Response({ 'code': 500, 'msg': '服务器内部错误' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class DashouGengxinView(APIView): """ 打手信息更新接口 URL: /yonghu/dsgxxx 方法: POST 认证: JWT Token 参数: nicheng(昵称), dianhua(电话), wechat(微信), jieshao(介绍) """ permission_classes = [IsAuthenticated] def post(self, request): try: # 获取请求数据 nicheng = request.data.get('nicheng', '').strip() dianhua = request.data.get('dianhua', '').strip() wechat = request.data.get('wechat', '').strip() jieshao = request.data.get('jieshao', '').strip() # 数据验证 validation_errors = self.validate_data(nicheng, dianhua, wechat, jieshao) if validation_errors: return Response({ 'code': 400, 'msg': validation_errors }, status=status.HTTP_400_BAD_REQUEST) # 获取打手扩展表 try: dashou = UserDashou.query.get(user=request.user) except UserDashou.DoesNotExist: return Response({ 'code': 404, 'msg': '打手信息不存在' }, status=status.HTTP_404_NOT_FOUND) # 更新数据 update_fields = [] if nicheng is not None: dashou.nicheng = nicheng if nicheng != '' else None update_fields.append('nicheng') if dianhua is not None: dashou.dianhua = dianhua if dianhua != '' else None update_fields.append('dianhua') if wechat is not None: dashou.wechat = wechat if wechat != '' else None update_fields.append('wechat') if jieshao is not None: dashou.jieshao = jieshao if jieshao != '' else None update_fields.append('jieshao') # 保存更新(只更新修改的字段) if update_fields: dashou.save(update_fields=update_fields) return Response({ 'code': 200, 'msg': '更新成功', 'data': { 'nicheng': dashou.nicheng or '', 'dianhua': dashou.dianhua or '', 'wechat': dashou.wechat or '', 'jieshao': dashou.jieshao or '' } }, status=status.HTTP_200_OK) except Exception as e: logger.error(f'更新打手信息异常: {str(e)}', exc_info=True) return Response({ 'code': 500, 'msg': '服务器内部错误' }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def validate_data(self, nicheng, dianhua, wechat, jieshao): """数据验证方法""" errors = [] # 1. 昵称验证(必填) if not nicheng: errors.append('昵称不能为空') elif len(nicheng) > 20: errors.append('昵称不能超过20个字符') # 2. 手机号验证(非必填,但填写时必须正确) if dianhua: # 手机号正则:1开头,11位数字 phone_pattern = r'^1[3-9]\d{9}$' if not re.match(phone_pattern, dianhua): errors.append('手机号格式不正确') # 3. 微信号验证(非必填) if wechat and len(wechat) > 30: errors.append('微信号不能超过30个字符') # 4. 个人介绍验证 if jieshao and len(jieshao) > 200: errors.append('个人介绍不能超过200个字符') return '; '.join(errors) if errors else None class DashouJianquanView(APIView): """打手消息权限鉴权""" permission_classes = [IsAuthenticated] def post(self, request): # ========== 1. 通过 JWT 获取当前登录用户 ========== user = request.user # User 实例 # ========== 2. 检查是否为已注册打手 ========== try: dashou = user.DashouProfile # OneToOneField 反向查询 except UserDashou.DoesNotExist: return Response({ 'code': 0, 'allow': 0, 'msg': '您尚未注册打手身份,请先注册' }) # ========== 3. 条件一:押金 >= 5 元 ========== if dashou.yajin and dashou.yajin >= 5: return Response({ 'code': 0, 'allow': 1, 'msg': '鉴权通过(押金满足条件)' }) # ========== 4. 条件二:存在未过期会员 ========== # 高效查询:利用 yonghu_id + huiyuan_zhuangtai 复合索引 huiyuan_list = Huiyuangoumai.query.filter( yonghu_id=user.UserUID, huiyuan_zhuangtai=1 # 只查状态为"可用"的,减少内存遍历 ).only('id', 'daoqi_time', 'huiyuan_zhuangtai') for hy in huiyuan_list: if not hy.jiance_shifou_daoqi(): # 未过期返回 False return Response({ 'code': 0, 'allow': 1, 'msg': '鉴权通过(会员未过期)' }) # ========== 5. 条件三:存在订单状态为 2 或 8 的订单 ========== # 利用 jiedan_dashou_id + zhuangtai 复合索引,只查 1 条 has_valid_order = Order.query.filter( PlayerID=user.UserUID, Status__in=[2, 8] ).only('id').exists() # exists() 高性能判断,只发 SELECT 1 LIMIT 1 if has_valid_order: return Response({ 'code': 0, 'allow': 1, 'msg': '鉴权通过(存在有效订单)' }) # ========== 6. 以上均不满足 → 拒绝 ========== return Response({ 'code': 0, 'allow': 0, 'msg': '消息权限不足,请充值会员或联系管理员' }) class LaobanJianquanView(APIView): """老板消息权限鉴权""" permission_classes = [IsAuthenticated] def post(self, request): # ========== 1. 通过 JWT 获取当前登录用户 ========== user = request.user # User 实例 # ========== 2. 高效查询平台订单扩展表 ========== # BossID 为模型字段名(db_column=laoban_id);订单状态用 Order.Status has_valid_order = PlatformOrderExt.query.filter( BossID=user.UserUID ).filter( Order__Status__in=[1, 2, 3, 4, 5, 6, 7, 8] ).only('id').exists() if has_valid_order: return Response({ 'code': 0, 'allow': 1, 'msg': '鉴权通过' }) return Response({ 'code': 0, 'allow': 0, 'msg': '您暂无订单记录,消息功能暂不可用' })