手机号认证流需同时带头像与 shoujihao_code 且无昵称;普通编辑只上传头像可正常保存。 Co-authored-by: Cursor <cursoragent@cursor.com>
1179 lines
46 KiB
Python
1179 lines
46 KiB
Python
"""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()
|
||
|
||
# 手机号强制认证页:同时带头像 + shoujihao_code,且不带 nicheng(资料编辑页会带昵称)
|
||
is_phone_auth_flow = bool(av) and bool(code) 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 <token>
|
||
返回邀请人的用户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': '您暂无订单记录,消息功能暂不可用'
|
||
})
|