Files
Django/users/views/auth.py
XingQue 12f9100b17 fix: 适配老小程序抢单池展示,登录clumber含体验会员
老端仅认 yaoqiuleixing=0 或 clumber.huiyuanid 严格匹配;统一 build_auth_clumber 与 dddhq 同源含 czjilu 兜底。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 15:27:58 +08:00

1905 lines
74 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""users.views.auth - 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__)
class WechatMiniProgramLoginView(APIView):
"""
微信小程序登录接口
"""
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
def post(self, request):
try:
code = request.data.get('code', '').strip()
if not code:
return Response({'code': 1, 'msg': '微信授权码不能为空', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
# 获取微信openid和unionid
wechat_data = self.get_wechat_openid(code)
if not wechat_data or 'openid' not in wechat_data:
error_msg = wechat_data.get('errmsg', '微信授权失败')
return Response({'code': 2, 'msg': f'微信登录失败: {error_msg}', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
openid = wechat_data['openid']
session_key = wechat_data.get('session_key', '')
unionid = wechat_data.get('unionid', '') # 🆕【新增】获取unionid
# 获取客户端真实IP
kehuduan_ip = self.huoquKehuduanIP(request)
with transaction.atomic():
# 创建或获取用户
user_main, created = User.objects.select_for_update().get_or_create(
OpenID=openid,
defaults={'UserUID': self.shengchengYonghuID(), 'UserName': openid}
)
# 保存unionid
if unionid and unionid != user_main.UnionID:
user_main.UnionID = unionid
user_main.IP = kehuduan_ip
user_main.UserLastLoginDate = timezone.now()
user_main.save()
if created:
UserBoss.query.create(user=user_main, nickname='板板大人')
# 新用户默认分配消费者角色
from users.business_models import UserRole, Role
normal_role = Role.objects.filter(RoleName='消费者').first()
if normal_role:
UserRole.objects.create(
UserRoleUUID=uuid.uuid4().bytes,
UserUUID=user_main.UserUUID,
RoleUUID=normal_role.RoleUUID,
CompanyUUID=uuid.UUID('b0000000-0000-0000-0000-000000000001').bytes,
)
# 检查扩展表
dashou_status = 0
shangjia_status = 0
guanshi_status = 0
boss_nickname = '微信用户'
try:
boss_profile = UserBoss.query.get(user=user_main)
boss_nickname = boss_profile.nickname if boss_profile.nickname else '微信用户'
except UserBoss.DoesNotExist:
pass
try:
UserDashou.query.get(user=user_main)
dashou_status = 1
except UserDashou.DoesNotExist:
dashou_status = None
try:
UserShangjia.query.get(user=user_main)
shangjia_status = 1
except UserShangjia.DoesNotExist:
shangjia_status = None
try:
UserGuanshi.query.get(user=user_main)
guanshi_status = 1
except UserGuanshi.DoesNotExist:
guanshi_status = None
try:
UserZuzhang.query.get(user=user_main)
zuzhang_status = 1
except UserZuzhang.DoesNotExist:
zuzhang_status = None
# 在检查 zuzhang 扩展表之后,插入以下代码
try:
kg = UserShenheguan.query.get(user=user_main)
kaoheguan_status = 1 if kg.zhuangtai == 1 else 0
except UserShenheguan.DoesNotExist:
kaoheguan_status = 0
# 生成JWT token
refresh = RefreshToken.for_user(user_main)
token = str(refresh.access_token)
# 计算订单数量
order_counts = self.jisuanOrderShuliang(user_main.UserUID)
# 获取群配置
group_info = self.huoquQunPeizhi()
from merchant_ops.services.authz import staff_fields_for_login
staff_fields = staff_fields_for_login(user_main)
# 返回数据
response_data = {
'token': token,
'nicheng': boss_nickname,
'uid': user_main.UserUID,
'touxiang': user_main.Avatar or '',
'shangjiastatus': shangjia_status,
'dashoustatus': dashou_status,
'guanshistatus': guanshi_status,
'zuzhangstatus': zuzhang_status,
'kaoheguanstatus': kaoheguan_status,
'dingdantiaoshu': order_counts,
'dashouqun': group_info.get('dashouqun', ''),
'dashouqunid': group_info.get('dashouqunid', ''),
'guanshiqun': group_info.get('guanshiqun', ''),
'guanshiqunid': group_info.get('guanshiqunid', ''),
**staff_fields,
}
return Response({'code': 0, 'msg': '登录成功', 'data': response_data})
except Exception as e:
error_openid = locals().get('openid', 'N/A')
logger.error(f"微信登录异常 - openid: {error_openid}, 错误类型: {type(e).__name__}, 错误详情: {str(e)}",
exc_info=True)
return Response({'code': 99, 'msg': '系统繁忙,请稍后重试', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def get_wechat_openid(self, code):
"""
调用微信接口获取openid和unionid
"""
try:
appid = getattr(settings, 'WEIXIN_APPID', '')
secret = getattr(settings, 'WEIXIN_SECRET', '')
if not appid or not secret:
raise ValueError('微信配置未设置')
url = 'https://api.weixin.qq.com/sns/jscode2session'
params = {'appid': appid, 'secret': secret, 'js_code': code, 'grant_type': 'authorization_code'}
response = requests.get(url, params=params, timeout=10)
result = response.json()
if 'openid' in result:
return {'openid': result['openid'], 'session_key': result.get('session_key', ''),
'unionid': result.get('unionid', '')}
else:
errcode = result.get('errcode', 'unknown')
errmsg = result.get('errmsg', '未知错误')
return {'errmsg': f'[{errcode}]{errmsg}'}
except requests.exceptions.Timeout:
return {'errmsg': '请求微信接口超时'}
except Exception as e:
return {'errmsg': f'请求微信接口异常: {str(e)}'}
def shengchengYonghuID(self):
"""
生成7位数字用户ID
"""
for _ in range(10):
timestamp_part = str(int(time.time()))[-5:].zfill(5)
random_part = str(random.randint(0, 99)).zfill(2)
user_id = timestamp_part + random_part
if len(user_id) == 7 and user_id.isdigit():
if not User.query.filter(UserUID=user_id).exists():
return user_id
raise Exception('生成用户ID失败')
def huoquKehuduanIP(self, request):
"""
获取客户端真实IP地址
"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0].strip()
if ip:
return ip
ip = request.META.get('REMOTE_ADDR', '')
return ip if ip else '0.0.0.0'
def jisuanOrderShuliang(self, yonghuid):
"""
统计订单数量
"""
try:
if not PlatformOrderExt.query.filter(BossID=yonghuid).exists():
return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0}
dingdan_ids = PlatformOrderExt.query.filter(BossID=yonghuid).values_list('Order__id', flat=True)
results = Order.query.filter(id__in=dingdan_ids).aggregate(
daifuwu=Count(Case(When(Q(Status=1) | Q(Status=7), then=1), output_field=IntegerField())),
fuwuzhong=Count(Case(When(Status=2, then=1), output_field=IntegerField())),
yiwancheng=Count(Case(When(Status=3, then=1), output_field=IntegerField())),
yituikuan=Count(Case(When(Status=5, then=1), output_field=IntegerField()))
)
return {
'daifuwu': results['daifuwu'] or 0,
'fuwuzhong': results['fuwuzhong'] or 0,
'yiwancheng': results['yiwancheng'] or 0,
'yituikuan': results['yituikuan'] or 0
}
except Exception:
return {'daifuwu': 0, 'fuwuzhong': 0, 'yiwancheng': 0, 'yituikuan': 0}
def huoquQunPeizhi(self):
"""
获取群配置信息
"""
try:
qun_configs = Qunpeizhi.query.filter(id__in=[1, 2])
result = {
'dashouqun': '',
'dashouqunid': '',
'guanshiqun': '',
'guanshiqunid': ''
}
for config in qun_configs:
if config.id == 1:
result['dashouqun'] = config.GroupContent or ''
result['dashouqunid'] = config.GroupID or ''
elif config.id == 2:
result['guanshiqun'] = config.GroupContent or ''
result['guanshiqunid'] = config.GroupID or ''
return result
except Exception:
return {
'dashouqun': '',
'dashouqunid': '',
'guanshiqun': '',
'guanshiqunid': ''
}
class WeixinOfficialCallbackView(APIView):
"""
微信服务号回调接口
处理用户关注、取消关注等事件
核心有关注事件就存不管有没有UnionID
"""
throttle_classes = []
permission_classes = [AllowAny]
authentication_classes = []
def get(self, request):
"""验证服务器配置(微信官方要求)"""
try:
signature = request.GET.get('signature', '')
timestamp = request.GET.get('timestamp', '')
nonce = request.GET.get('nonce', '')
echostr = request.GET.get('echostr', '')
token = getattr(settings, 'WEIXIN_OFFICIAL_TOKEN', '')
if not token:
logger.error("服务号Token未配置")
return HttpResponse('Token not configured', status=500)
# 验证签名
tmp_list = sorted([token, timestamp, nonce])
tmp_str = ''.join(tmp_list)
hash_str = hashlib.sha1(tmp_str.encode()).hexdigest()
if hash_str == signature:
return HttpResponse(echostr)
else:
logger.error(f"签名验证失败")
return HttpResponse('Signature verification failed', status=403)
except Exception as e:
logger.error(f"验证服务器异常: {str(e)}")
return HttpResponse('Internal Server Error', status=500)
def post(self, request):
"""处理事件推送(关注、取关)"""
try:
xml_str = request.body.decode('utf-8')
logger.info(f"收到服务号事件原始数据前200字符: {xml_str[:200]}")
root = ET.fromstring(xml_str)
msg_type = root.find('MsgType').text
from_user = root.find('FromUserName').text # 服务号OpenID
if msg_type == 'event':
event = root.find('Event').text
if event == 'subscribe':
return self.handle_subscribe_event(root, from_user)
elif event == 'unsubscribe':
return self.handle_unsubscribe_event(root, from_user)
else:
# 其他事件如菜单点击直接返回success
return HttpResponse('success')
# 非事件消息也直接返回success
return HttpResponse('success')
except Exception as e:
logger.error(f"处理服务号事件异常: {str(e)}", exc_info=True)
# 即使异常也返回success避免微信服务器重试
return HttpResponse('success')
def handle_subscribe_event(self, root, from_user):
"""处理用户关注事件 - 简化版:有就存,没有拉倒"""
try:
# 尝试获取UnionID有就有没有也没关系
unionid_elem = root.find('UnionId')
unionid = unionid_elem.text if unionid_elem is not None else None
logger.info(f"用户关注OpenID: {from_user}, UnionID: {unionid}")
# 核心逻辑:更新或创建记录
obj, created = OfficialAccountUser.query.update_or_create(
official_openid=from_user,
defaults={
'unionid': unionid,
'is_subscribed': True,
'subscribe_time': timezone.now()
}
)
action = "新建" if created else "更新"
logger.info(f"{action}用户记录成功: OpenID={from_user}")
# 回复欢迎消息(可选)
return self.generate_text_response(root, "感谢关注星阙网络服务!平台有新订单时您会收到通知。")
except Exception as e:
logger.error(f"处理关注事件异常: {str(e)}", exc_info=True)
# 依然返回success不让微信重试
return HttpResponse('success')
def handle_unsubscribe_event(self, root, from_user):
"""处理用户取消关注事件 - 简化版:标记为未订阅"""
try:
updated_count = OfficialAccountUser.query.filter(
official_openid=from_user
).update(is_subscribed=False)
if updated_count > 0:
logger.info(f"用户取消关注,已标记为未订阅: {from_user}")
else:
logger.info(f"用户取消关注,但记录不存在(无需处理): {from_user}")
return HttpResponse('success')
except Exception as e:
logger.error(f"处理取消关注事件异常: {str(e)}", exc_info=True)
return HttpResponse('success')
def generate_text_response(self, root, content):
"""生成文本消息响应"""
try:
to_user = root.find('ToUserName').text
from_user = root.find('FromUserName').text
response_xml = f"""<xml>
<ToUserName><![CDATA[{from_user}]]></ToUserName>
<FromUserName><![CDATA[{to_user}]]></FromUserName>
<CreateTime>{int(time.time())}</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[{content}]]></Content>
</xml>"""
return HttpResponse(response_xml, content_type='application/xml')
except Exception as e:
logger.error(f"生成回复消息异常: {str(e)}")
# 即使生成回复失败也返回一个空的success
return HttpResponse('success')
class DashouZhuceView(APIView):
"""
打手注册接口 (通过管事邀请码)
访问路径: /api/yonghu/dashouzhuce/
请求方法: POST
权限要求: JWT Token认证通过的用户
请求体: { "inviteCode": "AbC123..." }
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 1. 获取并验证邀请码参数
yaoqingma = request.data.get('inviteCode')
if not yaoqingma:
return Response({
'code': 400,
'message': '邀请码不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
yaoqingma = yaoqingma.strip()
if len(yaoqingma) > 100:
return Response({
'code': 400,
'message': '邀请码长度超过限制',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
current_user = request.user
# 注意这里request.user是User实例
# 2. 检查用户是否已是打手 (使用反向查询,性能最优)
try:
dashou_profile = current_user.DashouProfile
# 用户已是打手,直接返回现有信息
return self.fanhuiXianyouDashouXinxi(dashou_profile, current_user)
except UserDashou.DoesNotExist:
# 用户不是打手,继续注册流程
pass
# 3. 根据邀请码查找对应的管事
try:
# 使用select_related一次性获取管事及其关联的主表用户信息性能最高
guanshi_profile = UserGuanshi.query.select_related('user').get(yaoqingma=yaoqingma)
except UserGuanshi.DoesNotExist:
return Response({
'code': 404,
'message': '邀请码无效或不存在',
'data': None
}, status=status.HTTP_404_NOT_FOUND)
from jituan.services.invite_guard import assert_invite_same_club
ok, msg = assert_invite_same_club(request, guanshi_profile.user, current_user)
if not ok:
return Response({
'code': 403,
'message': msg,
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 4. 验证管事状态
# 5. 核心:在数据库事务中创建用户的所有身份
try:
with transaction.atomic():
# 获取管事的用户ID (yonghuid)
guanshi_yonghuid = guanshi_profile.user.UserUID
# 5.1 创建打手扩展表
dashou_profile = UserDashou.query.create(
user=current_user,
nicheng='大手子', # 默认昵称
chenghao='普通大手', # 默认称号
yaoqingren=guanshi_yonghuid, # 设置邀请人ID
# 其他字段全部使用模型定义的默认值
)
# 5.2 创建商家扩展表
try:
boss_profile = current_user.BossProfile
if boss_profile.nickname:
boss_nickname = boss_profile.nickname
except:
pass
# 5.3 创建管事扩展表
# 5.4 更新主表的用户类型 (如果需要,可以设为'dashou',或保持原逻辑)
# current_user.UserType = 'dashou' # 根据你的业务逻辑决定
# current_user.save()
# 5.5 更新原管事的邀请数量 (使用行锁防止并发问题)
# 重新查询并锁定该行记录
locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk)
locked_guanshi.yaogingshuliang += 1
locked_guanshi.save()
# ========== 新增:管事每日统计(邀请打手) ==========
try:
update_guanshi_daily_by_action(
yonghuid=guanshi_yonghuid,
action=1, # 1 = 邀请打手
amount=Decimal('0.00')
)
logger.info(f"管事每日统计更新成功(邀请打手):管事{guanshi_yonghuid}")
except Exception as e:
logger.error(f"管事每日统计更新失败(邀请打手):{str(e)}")
except Exception as e:
# 事务会自动回滚
# 这里可以记录更详细的日志
return Response({
'code': 500,
'message': f'注册过程中发生系统错误: {str(e)}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 6. 注册成功,返回信息 (新注册用户没有会员记录clumber返回空列表)
fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=True)
return Response({
'code': 200,
'message': '注册成功!已开通打手、管事身份。',
'data': fanhui_data
})
def fanhuiXianyouDashouXinxi(self, dashou_profile, current_user):
"""处理已是打手的用户,返回其现有信息"""
fanhui_data = self.zhuangbeiFanhuiShuju(dashou_profile, current_user, is_new=False)
return Response({
'code': 200,
'message': '您已是打手,返回当前信息。',
'data': fanhui_data
})
def zhuangbeiFanhuiShuju(self, dashou_profile, current_user, is_new=False):
"""准备返回给前端的打手数据,字段名严格匹配前端"""
# 构建基础信息
data = {
'dashounicheng': dashou_profile.nicheng,
'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
'yongjin': str(dashou_profile.yue), # Decimal转字符串
'zonge': str(dashou_profile.zonge),
'yajin': str(dashou_profile.yajin),
'chenghao': dashou_profile.chenghao,
'jinfen': dashou_profile.jifen,
'chengjiaoliang': dashou_profile.chengjiaozongliang,
'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
'dashouzhuangtai': dashou_profile.zhuangtai,
}
# 查询并处理会员列表 (clumber)
from jituan.services.huiyuan_bundle import build_auth_clumber
if is_new:
huiyuan_goumai_list = []
else:
cid = getattr(current_user, 'ClubID', None) or None
huiyuan_goumai_list = build_auth_clumber(current_user.UserUID, club_id=cid)
data['clumber'] = huiyuan_goumai_list
return data
class GuanshiYaoqingDashouListView(APIView):
"""
管事获取已邀请打手列表(支持筛选、搜索、分页)
POST /api/yonghu/gshqyqds/
Headers: Authorization: Bearer <token>
Body:
{
"page": 1, // 页码从1开始
"pageSize": 30, // 每页条数最大100
"keyword": "", // 搜索关键词用户ID或昵称
"zhanghaozhuangtai": 1, // 账号状态1=正常2=封禁(可选)
"zaixianzhuangtai": 1 // 在线状态1=在线2=离线(可选)
}
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
current_user = request.user
guanshi_yonghuid = current_user.UserUID
# 1. 分页参数
page = int(request.data.get('page', 1))
page_size = int(request.data.get('pageSize', 30))
if page < 1 or page_size < 1:
return Response({'code': 400, 'message': '分页参数必须大于0'}, status=400)
if page_size > 100:
page_size = 100
# 2. 筛选参数
keyword = request.data.get('keyword', '').strip()
zhanghaozhuangtai = request.data.get('zhanghaozhuangtai') # 可选1=正常2=封禁
zaixianzhuangtai = request.data.get('zaixianzhuangtai') # 可选1=在线2=离线
# 3. 基础查询:当前管事邀请的所有打手
base_qs = UserDashou.query.select_related('user').filter(
yaoqingren=guanshi_yonghuid
)
# 4. 全局统计(不受筛选影响,直接基于 base_qs
total_all = base_qs.count()
zaixian_all = base_qs.filter(zaixianzhuangtai=1).count()
zhengchang_all = base_qs.filter(zhanghaozhuangtai=1).count()
fengjin_all = total_all - zhengchang_all
# 5. 应用筛选条件
if keyword:
base_qs = base_qs.filter(
Q(nicheng__icontains=keyword) | Q(user__UserUID__icontains=keyword)
)
if zhanghaozhuangtai is not None:
try:
val = int(zhanghaozhuangtai)
if val == 1:
base_qs = base_qs.filter(zhanghaozhuangtai=1)
else:
base_qs = base_qs.exclude(zhanghaozhuangtai=1)
except (ValueError, TypeError):
pass
if zaixianzhuangtai is not None:
try:
val = int(zaixianzhuangtai)
if val == 1:
base_qs = base_qs.filter(zaixianzhuangtai=1)
else:
base_qs = base_qs.exclude(zaixianzhuangtai=1)
except (ValueError, TypeError):
pass
# 6. 排序和分页
base_qs = base_qs.order_by('-CreateTime')
paginator = Paginator(base_qs, page_size)
try:
page_obj = paginator.page(page)
except EmptyPage:
page_obj = []
has_more = False
filtered_total = paginator.count if paginator.count else 0
else:
has_more = page_obj.has_next()
filtered_total = paginator.count
# 7. 构建打手列表
dashou_list = []
for dashou in page_obj:
user_main = dashou.user
dashou_list.append({
'uid': user_main.UserUID,
'nicheng': dashou.nicheng or '打手',
'zaixianzhuangtai': dashou.zaixianzhuangtai,
'zhanghaozhuangtai': dashou.zhanghaozhuangtai,
'touxiang': user_main.Avatar or '',
'jiedanzongliang': dashou.jiedanzongliang,
'chengjiaozongliang': dashou.chengjiaozongliang,
'tuikuanliang': dashou.tuikuanliang,
'yue': str(dashou.zonge),
})
# 8. 获取管事邀请总数(来自管事扩展表)
try:
yaoqingzongshu = current_user.GuanshiProfile.yaogingshuliang
except Exception:
yaoqingzongshu = total_all
response_data = {
'dashouList': dashou_list,
'yaoqingzongshu': yaoqingzongshu, # 邀请总数(全局)
'totalCount': filtered_total, # 当前筛选条件下的总数
'zaixianCount': zaixian_all, # 全局在线
'zhengchangCount': zhengchang_all, # 全局正常
'fengjinCount': fengjin_all, # 全局封禁
'currentPage': page,
'pageSize': page_size,
'hasMore': has_more,
}
return Response({'code': 200, 'message': '获取成功', 'data': response_data})
except ValueError:
return Response({'code': 400, 'message': '分页参数格式错误'}, status=400)
except Exception as e:
logger.error(f'获取打手列表失败: {str(e)}', exc_info=True)
return Response({'code': 500, 'message': '系统错误'}, status=500)
class WechatLoginAndDashouRegisterView(APIView):
"""
微信登录并注册打手一体化接口
访问路径: /api/yonghu/wdlyhdl
请求方法: POST
权限要求: 无需登录
请求体: { "code": "微信登录code", "inviteCode": "邀请码" }
功能: 用户扫码进入但未登录时,一次性完成微信登录和打手注册
"""
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
def post(self, request):
"""
处理微信登录并注册打手请求
"""
try:
# 1. 获取并验证参数
code = request.data.get('code', '').strip()
yaoqingma = request.data.get('inviteCode', '').strip()
if not code:
return Response({
'code': 1,
'msg': '微信授权码不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
if not yaoqingma:
return Response({
'code': 2,
'msg': '邀请码不能为空',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
if len(yaoqingma) > 100:
return Response({
'code': 3,
'msg': '邀请码长度超过限制',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
# 2. 获取微信openid
wechat_data = self.get_wechat_openid(code)
if not wechat_data or 'openid' not in wechat_data:
error_msg = wechat_data.get('errmsg', '微信授权失败')
return Response({
'code': 4,
'msg': f'微信登录失败: {error_msg}',
'data': None
}, status=status.HTTP_400_BAD_REQUEST)
openid = wechat_data['openid']
# 3. 获取客户端真实IP
kehuduan_ip = self.huoquKehuduanIP(request)
# 4. 开始数据库事务(确保登录和注册的原子性)
with transaction.atomic():
# 4.1 创建或获取用户(登录逻辑)
user_main, created = User.objects.select_for_update().get_or_create(
OpenID=openid,
defaults={
'UserUID': self.shengchengYonghuID(),
'UserName': openid,
}
)
# 更新用户IP和最后登录时间
cunchu_ip = kehuduan_ip
user_main.IP = cunchu_ip
user_main.UserLastLoginDate = timezone.now()
user_main.save()
if created:
# 新用户:创建老板扩展表
UserBoss.query.create(user=user_main, nickname='微信用户')
# 4.2 验证邀请码对应的管事(注册逻辑)
try:
# 使用select_related获取管事及其关联的主表用户信息
guanshi_profile = UserGuanshi.query.select_related('user').get(yaoqingma=yaoqingma)
except UserGuanshi.DoesNotExist:
# 邀请码无效,但用户已登录,可以返回登录成功但注册失败
# 这里我们继续执行,但只返回登录信息
return self.fanhuiZhihouDengluXinxi(user_main, yaoqingma_wuxiao=True)
from jituan.services.invite_guard import assert_invite_same_club
from jituan.services.club_user import ensure_user_club_id
from jituan.services.club_context import resolve_club_id_from_request
ok, invite_msg = assert_invite_same_club(
request, guanshi_profile.user, user_main,
)
if not ok:
return Response({
'code': 403,
'msg': invite_msg,
'data': None
}, status=status.HTTP_403_FORBIDDEN)
ensure_user_club_id(user_main, resolve_club_id_from_request(request))
# 验证管事状态
# 4.3 检查用户是否已是打手
dashou_exists = UserDashou.query.filter(user=user_main).exists()
if dashou_exists:
# 用户已是打手,直接返回信息
dashou_profile = UserDashou.query.get(user=user_main)
return self.fanhuiDengluZhuceshuju(user_main, dashou_profile)
# 4.4 核心:为用户创建所有身份(打手、商家、管事)
guanshi_yonghuid = guanshi_profile.user.UserUID
# 创建打手扩展表
dashou_profile = UserDashou.query.create(
user=user_main,
nicheng='大神',
chenghao='普通大神',
yaoqingren=guanshi_yonghuid,
# 其他字段使用模型默认值
)
# 创建商家扩展表
# 创建管事扩展表
# 更新原管事的邀请数量
locked_guanshi = UserGuanshi.objects.select_for_update().get(pk=guanshi_profile.pk)
locked_guanshi.yaogingshuliang += 1
locked_guanshi.save()
# ========== 新增:管事每日统计(邀请打手) ==========
try:
update_guanshi_daily_by_action(
yonghuid=guanshi_yonghuid,
action=1, # 1 = 邀请打手
amount=Decimal('0.00')
)
logger.info(f"管事每日统计更新成功(邀请打手):管事{guanshi_yonghuid}")
except Exception as e:
logger.error(f"管事每日统计更新失败(邀请打手):{str(e)}")
# 5. 生成JWT token
refresh = RefreshToken.for_user(user_main)
token = str(refresh.access_token)
# 6. 准备返回数据
response_data = self.zhuangbeiFanhuiShuju(user_main, dashou_profile, token)
return Response({
'code': 0,
'msg': '登录并注册成功!已开通打手、管事等身份。',
'data': response_data
})
except Exception as e:
# 记录错误日志
error_openid = locals().get('openid', 'N/A')
logger.error(
f"微信登录注册一体化接口异常 - openid: {error_openid}, "
f"错误类型: {type(e).__name__}, 错误详情: {str(e)}",
exc_info=True
)
# 返回友好错误信息
return Response({
'code': 99,
'msg': '系统繁忙,请稍后重试',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
def get_wechat_openid(self, code):
"""
调用微信接口获取openid
"""
try:
appid = getattr(settings, 'WEIXIN_APPID', '')
secret = getattr(settings, 'WEIXIN_SECRET', '')
if not appid or not secret:
raise ValueError('微信配置未设置')
url = 'https://api.weixin.qq.com/sns/jscode2session'
params = {
'appid': appid,
'secret': secret,
'js_code': code,
'grant_type': 'authorization_code'
}
response = requests.get(url, params=params, timeout=10)
result = response.json()
if 'openid' in result:
return result
else:
errcode = result.get('errcode', 'unknown')
errmsg = result.get('errmsg', '未知错误')
return {'errmsg': f'[{errcode}]{errmsg}'}
except requests.exceptions.Timeout:
return {'errmsg': '请求微信接口超时'}
except Exception as e:
return {'errmsg': f'请求微信接口异常: {str(e)}'}
def shengchengYonghuID(self):
"""
生成7位数字用户ID
"""
for _ in range(10):
timestamp_part = str(int(time.time()))[-5:].zfill(5)
random_part = str(random.randint(0, 99)).zfill(2)
user_id = timestamp_part + random_part
if len(user_id) == 7 and user_id.isdigit():
if not User.query.filter(UserUID=user_id).exists():
return user_id
raise Exception('生成用户ID失败')
def huoquKehuduanIP(self, request):
"""
获取客户端真实IP地址
"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0].strip()
if ip:
return ip
ip = request.META.get('REMOTE_ADDR', '')
return ip if ip else '0.0.0.0'
def huoquQunPeizhi(self):
"""
获取群配置信息
"""
try:
qun_configs = Qunpeizhi.query.filter(id__in=[1, 2])
result = {
'dashouqun': '',
'dashouqunid': '',
'guanshiqun': '',
'guanshiqunid': ''
}
for config in qun_configs:
if config.id == 1:
result['dashouqun'] = config.GroupContent or ''
result['dashouqunid'] = config.GroupID or ''
elif config.id == 2:
result['guanshiqun'] = config.GroupContent or ''
result['guanshiqunid'] = config.GroupID or ''
return result
except Exception:
return {
'dashouqun': '',
'dashouqunid': '',
'guanshiqun': '',
'guanshiqunid': ''
}
def fanhuiZhihouDengluXinxi(self, user_main, yaoqingma_wuxiao=False, guanshi_jinyong=False):
"""
返回只登录成功的信息(当邀请码无效或管事禁用时)
"""
# 获取老板昵称
boss_nickname = '微信用户'
try:
boss_profile = UserBoss.query.get(user=user_main)
if boss_profile.nickname:
boss_nickname = boss_profile.nickname
except UserBoss.DoesNotExist:
pass
# 检查各身份状态
dashou_status = 1 if UserDashou.query.filter(user=user_main).exists() else 0
shangjia_status = 1 if UserShangjia.query.filter(user=user_main).exists() else 0
guanshi_status = 1 if UserGuanshi.query.filter(user=user_main).exists() else 0
# 生成token
refresh = RefreshToken.for_user(user_main)
token = str(refresh.access_token)
# 获取群配置
group_info = self.huoquQunPeizhi()
# 构建返回数据
response_data = {
'token': token,
'nicheng': boss_nickname,
'uid': user_main.UserUID,
'touxiang': user_main.Avatar or '',
'shangjiastatus': shangjia_status,
'dashoustatus': dashou_status,
'guanshistatus': guanshi_status,
# 🟢 注意这里没有返回dingdantiaoshu订单数量与前端需求一致
'dashouqun': group_info.get('dashouqun', ''),
'dashouqunid': group_info.get('dashouqunid', ''),
'guanshiqun': group_info.get('guanshiqun', ''),
'guanshiqunid': group_info.get('guanshiqunid', '')
}
# 根据情况设置返回消息
if yaoqingma_wuxiao:
msg = '登录成功,但邀请码无效'
elif guanshi_jinyong:
msg = '登录成功,但邀请码对应的管事账号已被禁用'
else:
msg = '登录成功'
return Response({
'code': 0,
'msg': msg,
'data': response_data
})
def fanhuiDengluZhuceshuju(self, user_main, dashou_profile):
"""
返回登录并注册的数据(用户已是打手的情况)
"""
# 获取老板昵称
boss_nickname = '微信用户'
try:
boss_profile = UserBoss.query.get(user=user_main)
if boss_profile.nickname:
boss_nickname = boss_profile.nickname
except UserBoss.DoesNotExist:
pass
# 检查各身份状态这里用户已是打手所以都是1
dashou_status = 1
shangjia_status = 1 if UserShangjia.query.filter(user=user_main).exists() else 1 # 确保是1
guanshi_status = 1 if UserGuanshi.query.filter(user=user_main).exists() else 1 # 确保是1
# 生成token
refresh = RefreshToken.for_user(user_main)
token = str(refresh.access_token)
# 获取群配置
group_info = self.huoquQunPeizhi()
# 准备打手信息
dashou_data = self.zhuangbeiDashouFanhuiShuju(dashou_profile)
# 合并所有数据
response_data = {
'token': token,
'nicheng': boss_nickname,
'uid': user_main.UserUID,
'touxiang': user_main.Avatar or '',
'shangjiastatus': shangjia_status,
'dashoustatus': dashou_status,
'guanshistatus': guanshi_status,
'dashouqun': group_info.get('dashouqun', ''),
'dashouqunid': group_info.get('dashouqunid', ''),
'guanshiqun': group_info.get('guanshiqun', ''),
'guanshiqunid': group_info.get('guanshiqunid', ''),
# 合并打手信息
**dashou_data
}
return Response({
'code': 0,
'msg': '您已是打手,登录成功',
'data': response_data
})
def zhuangbeiFanhuiShuju(self, user_main, dashou_profile, token):
"""
准备完整的返回数据(登录+注册成功的情况)
"""
# 获取老板昵称
boss_nickname = '微信用户'
try:
boss_profile = UserBoss.query.get(user=user_main)
if boss_profile.nickname:
boss_nickname = boss_profile.nickname
except UserBoss.DoesNotExist:
pass
# 身份状态刚注册成功应该都是1
dashou_status = 1
shangjia_status = 1
guanshi_status = 1
# 获取群配置
group_info = self.huoquQunPeizhi()
# 准备打手信息
dashou_data = self.zhuangbeiDashouFanhuiShuju(dashou_profile)
# 合并所有数据
response_data = {
'token': token,
'nicheng': boss_nickname,
'uid': user_main.UserUID,
'touxiang': user_main.Avatar or '',
'shangjiastatus': shangjia_status,
'dashoustatus': dashou_status,
'guanshistatus': guanshi_status,
'dashouqun': group_info.get('dashouqun', ''),
'dashouqunid': group_info.get('dashouqunid', ''),
'guanshiqun': group_info.get('guanshiqun', ''),
'guanshiqunid': group_info.get('guanshiqunid', ''),
# 合并打手信息
**dashou_data
}
return response_data
def zhuangbeiDashouFanhuiShuju(self, dashou_profile):
"""
准备打手返回数据(字段名严格匹配前端)
"""
# 构建基础信息
data = {
'dashounicheng': dashou_profile.nicheng,
'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
'yongjin': str(dashou_profile.yue), # Decimal转字符串
'zonge': str(dashou_profile.zonge),
'yajin': str(dashou_profile.yajin),
'chenghao': dashou_profile.chenghao,
'jinfen': dashou_profile.jifen,
'chengjiaoliang': dashou_profile.chengjiaozongliang,
'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
'dashouzhuangtai': dashou_profile.zhuangtai,
}
# 查询并处理会员列表 (clumber)
from jituan.services.huiyuan_bundle import build_auth_clumber
cid = getattr(dashou_profile.user, 'ClubID', None) or None
huiyuan_goumai_list = build_auth_clumber(dashou_profile.user.UserUID, club_id=cid)
data['clumber'] = huiyuan_goumai_list
return data
class WechatLoginAndGuanshiRegisterView(APIView):
"""
未登录用户微信登录+管事注册一体化接口
路径: /api/yonghu/zuzhangyqmzc
方法: POST
权限: 允许所有
请求体: { "code": "微信登录code", "inviteCode": "组长邀请码" }
成功返回 code: 200数据同 GuanshiRegisterView
"""
permission_classes = [AllowAny]
def post(self, request):
code = request.data.get('code', '').strip()
yaoqingma = request.data.get('inviteCode', '').strip()
if not code:
return Response({'code': 400, 'msg': '微信授权码不能为空', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
if not yaoqingma:
return Response({'code': 400, 'msg': '邀请码不能为空', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
if len(yaoqingma) > 100:
return Response({'code': 400, 'msg': '邀请码长度超过限制', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
wechat_data = self._get_wechat_openid(code)
if not wechat_data or 'openid' not in wechat_data:
error_msg = wechat_data.get('errmsg', '微信授权失败')
return Response({'code': 400, 'msg': f'微信登录失败: {error_msg}', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
openid = wechat_data['openid']
kehuduan_ip = self._huoqu_kehuduan_ip(request)
with transaction.atomic():
user_main, created = User.objects.select_for_update().get_or_create(
OpenID=openid,
defaults={
'UserUID': self._shengcheng_yonghu_id(),
'UserName': openid,
}
)
user_main.IP = kehuduan_ip
user_main.UserLastLoginDate = timezone.now()
user_main.save()
if created:
UserBoss.query.create(user=user_main, nickname='微信用户')
if hasattr(user_main, 'GuanshiProfile'):
return self._fanhui_xianyou_guanshi_info(user_main)
try:
zuzhang = UserZuzhang.query.select_related('user').get(yaoqingma=yaoqingma)
except UserZuzhang.DoesNotExist:
return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND)
if zuzhang.zhuangtai != 1:
return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None},
status=status.HTTP_403_FORBIDDEN)
zuzhang_yonghuid = zuzhang.user.UserUID
guanshi_yaoqingma = CreateInvitationCode(str(user_main.UserUID))
guanshi_profile = UserGuanshi.query.create(
user=user_main,
yaoqingma=guanshi_yaoqingma,
yaoqingren=zuzhang_yonghuid,
)
UserZuzhang.query.filter(pk=zuzhang.pk).update(
yaoqing_zongshu=models.F('yaoqing_zongshu') + 1
)
# ========== 新增:组长每日统计(邀请管事 action=1 ==========
try:
update_zuzhang_daily_by_action(
yonghuid=zuzhang_yonghuid,
action=1 # 1 = 邀请管事
)
logger.info(f"组长每日统计更新成功(邀请管事):组长{zuzhang_yonghuid}")
except Exception as e:
logger.error(f"组长每日统计更新失败(邀请管事):{str(e)}")
if not hasattr(user_main, 'DashouProfile'):
dashou_profile = UserDashou.query.create(
user=user_main,
nicheng='大神',
chenghao='普通大神',
yaoqingren=zuzhang_yonghuid,
)
else:
dashou_profile = user_main.DashouProfile
refresh = RefreshToken.for_user(user_main)
token = str(refresh.access_token)
response_data = self._zhuangbei_fanhui_shuju(
user_main, dashou_profile, guanshi_profile, token
)
return Response({'code': 0, 'msg': '登录并注册成功!', 'data': response_data})
# ---------- 辅助方法与接口1完全相同完整列出以保证完整性 ----------
def _get_wechat_openid(self, code):
try:
appid = getattr(settings, 'WEIXIN_APPID', '')
secret = getattr(settings, 'WEIXIN_SECRET', '')
if not appid or not secret:
raise ValueError('微信配置未设置')
url = 'https://api.weixin.qq.com/sns/jscode2session'
params = {
'appid': appid,
'secret': secret,
'js_code': code,
'grant_type': 'authorization_code'
}
response = requests.get(url, params=params, timeout=10)
result = response.json()
if 'openid' in result:
return result
else:
errcode = result.get('errcode', 'unknown')
errmsg = result.get('errmsg', '未知错误')
return {'errmsg': f'[{errcode}]{errmsg}'}
except requests.exceptions.Timeout:
logger.error('请求微信接口超时')
return {'errmsg': '请求微信接口超时'}
except Exception as e:
logger.error(f'请求微信接口异常: {e}')
return {'errmsg': f'请求微信接口异常: {str(e)}'}
def _shengcheng_yonghu_id(self):
for _ in range(10):
timestamp_part = str(int(time.time()))[-5:].zfill(5)
random_part = str(random.randint(0, 99)).zfill(2)
user_id = timestamp_part + random_part
if len(user_id) == 7 and user_id.isdigit():
if not User.query.filter(UserUID=user_id).exists():
return user_id
raise Exception('生成用户ID失败')
def _huoqu_kehuduan_ip(self, request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0].strip()
if ip:
return ip
ip = request.META.get('REMOTE_ADDR', '')
return ip if ip else '0.0.0.0'
def _huoqu_qun_peizhi(self):
try:
qun_configs = Qunpeizhi.query.filter(id__in=[1, 2])
result = {
'dashouqun': '',
'dashouqunid': '',
'guanshiqun': '',
'guanshiqunid': ''
}
for config in qun_configs:
if config.id == 1:
result['dashouqun'] = config.GroupContent or ''
result['dashouqunid'] = config.GroupID or ''
elif config.id == 2:
result['guanshiqun'] = config.GroupContent or ''
result['guanshiqunid'] = config.GroupID or ''
return result
except Exception as e:
logger.error(f"获取群配置失败: {e}")
return {'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': ''}
def _zhuangbei_dashou_fanhui(self, dashou_profile):
data = {
'dashounicheng': dashou_profile.nicheng,
'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
'yongjin': str(dashou_profile.yue),
'zonge': str(dashou_profile.zonge),
'yajin': str(dashou_profile.yajin),
'chenghao': dashou_profile.chenghao,
'jinfen': dashou_profile.jifen,
'chengjiaoliang': dashou_profile.chengjiaozongliang,
'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
'dashouzhuangtai': dashou_profile.zhuangtai,
}
from jituan.services.huiyuan_bundle import build_auth_clumber
try:
cid = getattr(dashou_profile.user, 'ClubID', None) or None
huiyuan_list = build_auth_clumber(dashou_profile.user.UserUID, club_id=cid)
except Exception as e:
logger.warning(f"获取会员列表失败: {e}")
huiyuan_list = []
data['clumber'] = huiyuan_list
return data
def _fanhui_xianyou_guanshi_info(self, user_main):
guanshi_profile = user_main.GuanshiProfile
dashou_profile = getattr(user_main, 'DashouProfile', None)
group_info = self._huoqu_qun_peizhi()
dashou_status = 1 if dashou_profile else 0
shangjia_status = 1 if hasattr(user_main, 'ShopProfile') else 0
guanshi_status = 1
try:
nicheng = user_main.BossProfile.nickname or '微信用户'
except:
nicheng = '微信用户'
refresh = RefreshToken.for_user(user_main)
token = str(refresh.access_token)
data = {
'token': token,
'nicheng': nicheng,
'uid': user_main.UserUID,
'touxiang': user_main.Avatar or '',
'shangjiastatus': shangjia_status,
'dashoustatus': dashou_status,
'guanshistatus': guanshi_status,
**group_info,
}
guanshi_data = {
'gszhstatus': guanshi_profile.zhuangtai,
'yaoqingzongshu': guanshi_profile.yaogingshuliang,
'fenyongzonge': str(guanshi_profile.chongzhifenrun),
'fenyongtixian': str(guanshi_profile.yue),
'yichongzhiDashou': guanshi_profile.jinrichongzhi,
}
data.update(guanshi_data)
if dashou_profile:
dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
data.update(dashou_data)
else:
data.update({
'dashounicheng': '',
'zhanghaostatus': 0,
'yongjin': '0.00',
'zonge': '0.00',
'yajin': '0.00',
'chenghao': '',
'jinfen': 0,
'chengjiaoliang': 0,
'zaixianzhuangtai': 0,
'dashouzhuangtai': 0,
'clumber': []
})
return Response({'code': 0, 'msg': '您已是管事', 'data': data})
def _zhuangbei_fanhui_shuju(self, user_main, dashou_profile, guanshi_profile, token):
try:
nicheng = user_main.BossProfile.nickname or '微信用户'
except:
nicheng = '微信用户'
dashou_status = 1
shangjia_status = 1 if hasattr(user_main, 'ShopProfile') else 0
guanshi_status = 1
group_info = self._huoqu_qun_peizhi()
data = {
'token': token,
'nicheng': nicheng,
'uid': user_main.UserUID,
'touxiang': user_main.Avatar or '',
'shangjiastatus': shangjia_status,
'dashoustatus': dashou_status,
'guanshistatus': guanshi_status,
**group_info,
}
guanshi_data = {
'gszhstatus': guanshi_profile.zhuangtai,
'yaoqingzongshu': guanshi_profile.yaogingshuliang,
'fenyongzonge': str(guanshi_profile.chongzhifenrun),
'fenyongtixian': str(guanshi_profile.yue),
'yichongzhiDashou': guanshi_profile.jinrichongzhi,
}
data.update(guanshi_data)
dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
data.update(dashou_data)
return data
class GuanshiRegisterView(APIView):
"""
已登录用户注册管事接口
路径: /api/yonghu/guanshizhuce
方法: POST
权限: JWT认证
请求体: { "inviteCode": "组长邀请码" }
成功返回 code: 200
"""
permission_classes = [IsAuthenticated]
def post(self, request):
# 1. 获取并验证邀请码
yaoqingma = request.data.get('inviteCode', '').strip()
if not yaoqingma:
return Response({'code': 400, 'msg': '邀请码不能为空', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
if len(yaoqingma) > 100:
return Response({'code': 400, 'msg': '邀请码长度超过限制', 'data': None},
status=status.HTTP_400_BAD_REQUEST)
current_user = request.user
# 2. 检查用户是否已是管事
if hasattr(current_user, 'GuanshiProfile'):
# 用户已是管事,直接返回现有信息
return self._fanhui_xianyou_guanshi_info(current_user)
# 3. 验证邀请码对应的组长
try:
zuzhang = UserZuzhang.query.select_related('user').get(yaoqingma=yaoqingma)
except UserZuzhang.DoesNotExist:
return Response({'code': 404, 'msg': '邀请码无效或不存在', 'data': None},
status=status.HTTP_404_NOT_FOUND)
if zuzhang.zhuangtai != 1:
return Response({'code': 403, 'msg': '该邀请码对应的组长账号已被禁用', 'data': None},
status=status.HTTP_403_FORBIDDEN)
from jituan.services.invite_guard import assert_invite_same_club
ok, invite_msg = assert_invite_same_club(request, zuzhang.user, current_user)
if not ok:
return Response({'code': 403, 'msg': invite_msg, 'data': None},
status=status.HTTP_403_FORBIDDEN)
zuzhang_yonghuid = zuzhang.user.UserUID
# 4. 事务内创建管事实体和打手(如果需要)
with transaction.atomic():
# 创建管事扩展表
guanshi_yaoqingma = CreateInvitationCode(str(current_user.UserUID))
guanshi_profile = UserGuanshi.query.create(
user=current_user,
yaoqingma=guanshi_yaoqingma,
yaoqingren=zuzhang_yonghuid,
)
# 更新组长的邀请总数
UserZuzhang.query.filter(pk=zuzhang.pk).update(
yaoqing_zongshu=models.F('yaoqing_zongshu') + 1
)
# ========== 新增:组长每日统计(邀请管事 action=1 ==========
try:
update_zuzhang_daily_by_action(
yonghuid=zuzhang_yonghuid,
action=1 # 1 = 邀请管事
)
logger.info(f"组长每日统计更新成功(邀请管事):组长{zuzhang_yonghuid}")
except Exception as e:
logger.error(f"组长每日统计更新失败(邀请管事):{str(e)}")
# 检查用户是否已是打手,如果不是则创建
if not hasattr(current_user, 'DashouProfile'):
dashou_profile = UserDashou.query.create(
user=current_user,
nicheng='大手子',
chenghao='普通大手',
yaoqingren=zuzhang_yonghuid,
)
else:
dashou_profile = current_user.DashouProfile
# 5. 生成新token
refresh = RefreshToken.for_user(current_user)
token = str(refresh.access_token)
# 6. 准备返回数据(包含所有字段)
response_data = self._zhuangbei_fanhui_shuju(
current_user, dashou_profile, guanshi_profile, token
)
return Response({'code': 200, 'msg': '注册成功!', 'data': response_data})
# ---------- 辅助方法 ----------
def _huoqu_qun_peizhi(self):
"""获取群配置信息"""
try:
qun_configs = Qunpeizhi.query.filter(id__in=[1, 2])
result = {
'dashouqun': '',
'dashouqunid': '',
'guanshiqun': '',
'guanshiqunid': ''
}
for config in qun_configs:
if config.id == 1:
result['dashouqun'] = config.GroupContent or ''
result['dashouqunid'] = config.GroupID or ''
elif config.id == 2:
result['guanshiqun'] = config.GroupContent or ''
result['guanshiqunid'] = config.GroupID or ''
return result
except Exception as e:
logger.error(f"获取群配置失败: {e}")
return {'dashouqun': '', 'dashouqunid': '', 'guanshiqun': '', 'guanshiqunid': ''}
def _zhuangbei_dashou_fanhui(self, dashou_profile):
"""组装打手信息(字段名与前端完全匹配)"""
data = {
'dashounicheng': dashou_profile.nicheng,
'zhanghaostatus': dashou_profile.zhanghaozhuangtai,
'yongjin': str(dashou_profile.yue),
'zonge': str(dashou_profile.zonge),
'yajin': str(dashou_profile.yajin),
'chenghao': dashou_profile.chenghao,
'jinfen': dashou_profile.jifen,
'chengjiaoliang': dashou_profile.chengjiaozongliang,
'zaixianzhuangtai': dashou_profile.zaixianzhuangtai,
'dashouzhuangtai': dashou_profile.zhuangtai,
}
# 会员列表
from jituan.services.huiyuan_bundle import build_auth_clumber
try:
cid = getattr(dashou_profile.user, 'ClubID', None) or None
huiyuan_list = build_auth_clumber(dashou_profile.user.UserUID, club_id=cid)
except Exception as e:
logger.warning(f"获取会员列表失败: {e}")
huiyuan_list = []
data['clumber'] = huiyuan_list
return data
def _fanhui_xianyou_guanshi_info(self, user_main):
"""用户已是管事时的返回"""
guanshi_profile = user_main.GuanshiProfile
dashou_profile = getattr(user_main, 'DashouProfile', None)
group_info = self._huoqu_qun_peizhi()
dashou_status = 1 if dashou_profile else 0
shangjia_status = 1 if hasattr(user_main, 'ShopProfile') else 0
guanshi_status = 1
try:
nicheng = user_main.BossProfile.nickname or '微信用户'
except:
nicheng = '微信用户'
refresh = RefreshToken.for_user(user_main)
token = str(refresh.access_token)
data = {
'token': token,
'nicheng': nicheng,
'uid': user_main.UserUID,
'touxiang': user_main.Avatar or '',
'shangjiastatus': shangjia_status,
'dashoustatus': dashou_status,
'guanshistatus': guanshi_status,
**group_info,
}
# 管事信息
guanshi_data = {
'gszhstatus': guanshi_profile.zhuangtai,
'yaoqingzongshu': guanshi_profile.yaogingshuliang,
'fenyongzonge': str(guanshi_profile.chongzhifenrun),
'fenyongtixian': str(guanshi_profile.yue),
'yichongzhiDashou': guanshi_profile.jinrichongzhi,
}
data.update(guanshi_data)
if dashou_profile:
dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
data.update(dashou_data)
else:
data.update({
'dashounicheng': '',
'zhanghaostatus': 0,
'yongjin': '0.00',
'zonge': '0.00',
'yajin': '0.00',
'chenghao': '',
'jinfen': 0,
'chengjiaoliang': 0,
'zaixianzhuangtai': 0,
'dashouzhuangtai': 0,
'clumber': []
})
return Response({'code': 200, 'msg': '您已是管事', 'data': data})
def _zhuangbei_fanhui_shuju(self, user_main, dashou_profile, guanshi_profile, token):
"""组装完整返回数据(新注册)"""
try:
nicheng = user_main.BossProfile.nickname or '微信用户'
except:
nicheng = '微信用户'
dashou_status = 1
shangjia_status = 1 if hasattr(user_main, 'ShopProfile') else 0
guanshi_status = 1
group_info = self._huoqu_qun_peizhi()
data = {
'token': token,
'nicheng': nicheng,
'uid': user_main.UserUID,
'touxiang': user_main.Avatar or '',
'shangjiastatus': shangjia_status,
'dashoustatus': dashou_status,
'guanshistatus': guanshi_status,
**group_info,
}
# 管事信息
guanshi_data = {
'gszhstatus': guanshi_profile.zhuangtai,
'yaoqingzongshu': guanshi_profile.yaogingshuliang,
'fenyongzonge': str(guanshi_profile.chongzhifenrun),
'fenyongtixian': str(guanshi_profile.yue),
'yichongzhiDashou': guanshi_profile.jinrichongzhi,
}
data.update(guanshi_data)
dashou_data = self._zhuangbei_dashou_fanhui(dashou_profile)
data.update(dashou_data)
return data
class ZuzhangZhuceView(APIView):
"""
组长注册/激活接口
POST /yonghu/zuzhangzhuce
请求体: {"inviteCode": "xxx"}
返回:
{
"code": 200,
"msg": "success",
"data": {
"ketixian": "0.00", # 可提现金额
"guanshiCount": 0, # 邀请的管事数量
"fenhongZonge": "0.00" # 分红总额
}
}
或错误码
"""
permission_classes = [IsAuthenticated]
# 硬编码激活码(注意转义特殊字符)
ACTIVATION_CODE = "237huwehdw77e777eyw224fr"
def post(self, request):
user = request.user
invite_code = request.data.get('inviteCode', '').strip()
if not invite_code:
return Response({'code': 400, 'msg': '邀请码不能为空'}, status=400)
# 检查是否已是组长
try:
zuzhang = UserZuzhang.query.select_related('user').get(user=user)
# 已注册,直接返回数据
data = self._build_response_data(zuzhang, user)
return Response({'code':200, 'msg': '已注册', 'data': data})
except UserZuzhang.DoesNotExist:
pass
# 比对激活码
if invite_code != self.ACTIVATION_CODE:
return Response({'code': 400, 'msg': '邀请码无效'}, status=400)
from jituan.services.club_user import ensure_user_club_id
from jituan.services.club_context import resolve_club_id_from_request
ensure_user_club_id(user, resolve_club_id_from_request(request))
# 生成唯一组长邀请码:时间戳 + 用户ID后6位 + 随机字符
yaoqingma = self._generate_unique_yaoqingma(user.UserUID)
# 创建组长扩展记录
try:
with transaction.atomic():
zuzhang = UserZuzhang.query.create(
user=user,
yaoqingma=yaoqingma,
fenyong_zonge=0.00,
ketixian_jine=0.00,
yaoqing_zongshu=0,
jinri_fenyong=0.00,
jinyue_fenyong=0.00,
zhuangtai=1,
jinri_tixian=0.00,
ewai_tixian_xiane=0.00,
kaioi_ewai_tixian=False,
kaioi_ewai_fenhong=False,
ewai_fenhong_jine=0.00
)
except IntegrityError:
# 极低概率重复,重试一次
yaoqingma = self._generate_unique_yaoqingma(user.UserUID, retry=True)
zuzhang = UserZuzhang.query.create(
user=user,
yaoqingma=yaoqingma,
# 其他字段默认同上
fenyong_zonge=0.00,
ketixian_jine=0.00,
yaoqing_zongshu=0,
jinri_fenyong=0.00,
jinyue_fenyong=0.00,
zhuangtai=1,
jinri_tixian=0.00,
ewai_tixian_xiane=0.00,
kaioi_ewai_tixian=False,
kaioi_ewai_fenhong=False,
ewai_fenhong_jine=0.00
)
data = self._build_response_data(zuzhang, user)
return Response({'code':200, 'msg': '注册成功', 'data': data})
def _generate_unique_yaoqingma(self, yonghuid, retry=False):
"""生成唯一邀请码:时间戳+用户ID后6位+随机2位字符"""
timestamp = str(int(time.time()))[-6:] # 取后6位
uid_part = yonghuid[-6:] if len(yonghuid) >= 6 else yonghuid.rjust(6, '0')
rand_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=2))
base = f"Z{timestamp}{uid_part}{rand_str}"
# 确保唯一性若已存在则追加随机数最多尝试3次
for _ in range(3):
if not UserZuzhang.query.filter(yaoqingma=base).exists():
return base
base = base + random.choice(string.digits)
# 极端情况,再附加时间戳毫秒
millis = str(time.time()).replace('.', '')[-4:]
return base + millis
def _build_response_data(self, zuzhang, user):
"""组装前端所需数据"""
# 计算邀请的管事数量通过UserGuanshi表的yaoqingren字段关联到user的yonghuid
guanshi_count = UserGuanshi.query.filter(yaoqingren=user.UserUID).count()
return {
'ketixian': str(zuzhang.ketixian_jine), # 可提现余额
'guanshiCount': guanshi_count, # 邀请管事数
'fenhongZonge': str(zuzhang.fenyong_zonge) # 分红总额
}
class ZuzhangXinxiView(APIView):
"""
组长信息查询接口
POST /yonghu/zuzhangxinxi
请求体: 无
返回: 同注册接口成功返回格式
"""
permission_classes = [IsAuthenticated]
def post(self, request):
user = request.user
try:
zuzhang = UserZuzhang.query.get(user=user)
except UserZuzhang.DoesNotExist:
return Response({'code': 404, 'msg': '用户不是组长'}, status=404)
guanshi_count = UserGuanshi.query.filter(yaoqingren=user.UserUID).count()
data = {
'ketixian': str(zuzhang.ketixian_jine),
'guanshiCount': guanshi_count,
'fenhongZonge': str(zuzhang.fenyong_zonge)
}
return Response({'code': 200, 'msg': 'success', 'data': data})