Files
Django/config/views/qrcode_poster.py
XingQue 435422e716 fix: 点单端推荐官按UID绑定,二维码进首页且去掉邀请码
扫码参数改为 yaoqingren=UserUID 落地点单首页;海报默认不再复用管事背景;不影响打手端与支付抢单分红。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 02:48:16 +08:00

682 lines
28 KiB
Python
Raw Permalink 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.
import io
import os
import re
import time
import random
import secrets
import hashlib
import threading
import urllib.parse
from decimal import Decimal, InvalidOperation
from django.conf import settings
from django.db import transaction, connection
from django.db.models import Q, F, Max, Prefetch
from gvsdsdk.fluent import db, func, FQ
from django.core.cache import cache
from django.core.paginator import Paginator
from django.utils import timezone
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, permissions
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.parsers import JSONParser, MultiPartParser, FormParser
from rest_framework.throttling import AnonRateThrottle, SimpleRateThrottle
from utils.oss_utils import upload_to_oss, delete_from_oss, validate_image
from utils.weixin_broadcast import WeixinBroadcastSender
from utils.weixin_token import get_weixin_mini_access_token, is_weixin_token_invalid
# from utils.ip_security import *
from utils.invitationcode_utils import CreateInvitationCode, VerifyInvitationCode
from backend.utils import update_shangjia_daily
from utils.chat_utils import subscribe_merchant_link_chat
from utils.pdd_order_validator import validate_pdd_order_id, validate_cn_mobile
from ..models import (
Gonggao, Lunbo, Tupianpeizhi, Qunpeizhi,
ShangjiaMoban, ShangjiaLianjie, PopupPage, PopupConfig, WithdrawConfig,
MiniappScriptScene, MiniappScriptAutoReply,
)
from users.models import (
AdminProfile, UserDashou, UserBoss,
UserShangjia, UserGuanshi, UserZuzhang
)
from users.business_models import User
from orders.models import CommissionRate, Order, MerchantOrderExt
from orders.notice_tasks import dingdan_guangbo
from rank.models import Chenghao, DingdanBiaoqian
from products.models import ShangpinLeixing, Huiyuangoumai
from ..serializers import PopupConfigSerializer
import traceback
import requests
import logging
logger = logging.getLogger(__name__)
class GuanZhuAListView(APIView):
"""
获取关注阿龙页面图片列表
POST /peizhi/gzal
需要JWT认证
返回:
{
"code": 0,
"msg": "success",
"data": {
"images": ["相对URL1", "相对URL2", ...],
"last_update": "2025-03-16 12:34:56",
"visit_count": 123
}
}
"""
permission_classes = [IsAuthenticated]
def post(self, request):
try:
from jituan.services.club_context import resolve_effective_club_id
club_id = resolve_effective_club_id(request, request.user)
qs = Tupianpeizhi.query.filter(ImageType=3, club_id=club_id)
if not qs.exists():
return Response({
'code': 0,
'msg': 'success',
'data': {
'images': [],
'last_update': None,
'visit_count': 0
}
}, status=status.HTTP_200_OK)
# 获取所有图片的相对URL
images = list(qs.values_list('ImageURL', flat=True))
# 获取最新更新时间取所有记录的UpdateTime最大值
last_update = qs.aggregate(UpdateTime__max=Max('UpdateTime'))['UpdateTime__max']
last_update_str = last_update.strftime('%Y-%m-%d %H:%M:%S') if last_update else None
# 原子性增加访问次数
# 先更新所有记录,再获取更新后的最大次数
qs.update(AccessCount=F('AccessCount') + 1)
visit_count = qs.aggregate(access_count__max=Max('AccessCount'))['access_count__max'] or 0
return Response({
'code': 0,
'msg': 'success',
'data': {
'images': images,
'last_update': last_update_str,
'visit_count': visit_count
}
}, status=status.HTTP_200_OK)
except Exception as e:
logger.error(f"获取关注阿龙图片列表失败: {e}", exc_info=True)
return Response({
'code': 1,
'msg': '服务器内部错误',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
#不用缓存一天2000次
class GuanshiQRCodeView(APIView):
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
logger.info("===== 开始生成管事二维码 =====")
current_user = request.user
logger.info(f"当前用户ID: {current_user.UserUID}")
try:
from jituan.services.club_context import resolve_effective_club_id
from utils.weixin_token import get_club_mini_access_token, is_weixin_token_invalid
club_id = resolve_effective_club_id(request, current_user)
try:
guanshi = current_user.GuanshiProfile
logger.info(f"管事扩展表查询成功,状态: {guanshi.zhuangtai}")
except UserGuanshi.DoesNotExist:
logger.warning(f"用户 {current_user.UserUID} 不是管事身份")
return Response(
{'code': 403, 'message': '当前用户不是管事身份', 'data': None},
status=status.HTTP_403_FORBIDDEN
)
if guanshi.zhuangtai != 1:
logger.warning(f"用户 {current_user.UserUID} 管事状态异常: {guanshi.zhuangtai}")
return Response(
{'code': 403, 'message': '管事账号状态异常', 'data': None},
status=status.HTTP_403_FORBIDDEN
)
invite_code = guanshi.yaoqingma
if not invite_code:
logger.info(f"用户 {current_user.UserUID} 无邀请码,开始生成")
invite_code = CreateInvitationCode(str(current_user.UserUID))
if not VerifyInvitationCode(invite_code):
logger.error(f"邀请码唯一性校验失败,用户: {current_user.UserUID}")
return Response(
{'code': 500, 'message': '邀请码生成失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
guanshi.yaoqingma = invite_code
guanshi.save(update_fields=['yaoqingma'])
logger.info(f"邀请码生成成功: {invite_code}")
else:
logger.info(f"使用已有邀请码: {invite_code}")
# 按俱乐部使用对应小程序 appid 的 access_token
access_token = get_club_mini_access_token(club_id)
if not access_token:
logger.error("获取 access_token 失败 club_id=%s", club_id)
return Response(
{'code': 500, 'message': '获取微信access_token失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
logger.info(f"获取到 access_token (前10位): {access_token[:10]}... club={club_id}")
encoded_invite = urllib.parse.quote(invite_code, safe='')
# 新小程序:打手端 Tab「我的」旧码 dashouduan 由小程序兼容页转发
page_path = f'pages/fighter/fighter?inviteCode={encoded_invite}'
wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}'
post_data = {
'path': page_path,
'width': 430,
'auto_color': False,
'line_color': {'r': 0, 'g': 0, 'b': 0}
}
logger.info(f"调用微信接口生成小程序码path: {page_path}")
def _request_qr(token):
try:
return requests.post(
f'https://api.weixin.qq.com/wxa/getwxacode?access_token={token}',
json=post_data, timeout=10,
)
except Exception as e:
logger.error(f"请求微信接口异常: {str(e)}", exc_info=True)
return None
wx_resp = _request_qr(access_token)
if wx_resp is None:
return Response(
{'code': 500, 'message': '调用微信服务失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
if wx_resp.headers.get('content-type', '').startswith('application/json'):
try:
err = wx_resp.json()
if is_weixin_token_invalid(err.get('errcode'), err.get('errmsg', '')):
access_token = get_club_mini_access_token(club_id, force_refresh=True)
if access_token:
wx_resp = _request_qr(access_token)
except Exception:
pass
logger.info(f"微信接口响应状态码: {wx_resp.status_code}")
if wx_resp.status_code != 200:
error_info = {}
if wx_resp.headers.get('content-type') == 'application/json':
try:
error_info = wx_resp.json()
except:
pass
errmsg = error_info.get('errmsg', '未知错误')
logger.error(f"微信接口返回错误 (HTTP {wx_resp.status_code}): {errmsg}")
if 'errcode' in error_info:
logger.error(f"微信错误码: {error_info.get('errcode')}")
return Response(
{'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
content_type = wx_resp.headers.get('content-type', '')
if 'image' not in content_type:
try:
error_info = wx_resp.json()
errmsg = error_info.get('errmsg', '未知错误')
logger.error(f"微信接口返回非图片内容: {errmsg}")
return Response(
{'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
except:
logger.error("微信接口返回未知内容无法解析为JSON")
return Response(
{'code': 500, 'message': '生成二维码失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
timestamp = int(time.time())
filename = f"{current_user.UserUID}_{timestamp}.png"
oss_path = f"erweima/guanshi/{club_id}/{filename}"
file_obj = io.BytesIO(wx_resp.content)
logger.info(f"开始上传图片到COS: {oss_path}")
try:
full_url = upload_to_oss(file_obj, oss_path)
logger.info(f"上传成功完整URL: {full_url}")
except Exception as e:
logger.error(f"上传到COS异常: {str(e)}", exc_info=True)
return Response(
{'code': 500, 'message': '图片上传失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
if not full_url:
logger.error("upload_to_oss 返回了空 URL")
return Response(
{'code': 500, 'message': '图片上传失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
cos_domain = getattr(settings, 'COS_DOMAIN', '')
if cos_domain and full_url.startswith(cos_domain):
relative_path = full_url[len(cos_domain):].lstrip('/')
else:
relative_path = oss_path
logger.info(f"相对路径: {relative_path}")
old_url = guanshi.invite_qrcode_url
if old_url:
logger.info(f"删除旧二维码: {old_url}")
try:
delete_from_oss(old_url)
except Exception as e:
logger.warning(f"删除旧二维码失败(不影响主流程): {str(e)}")
with transaction.atomic():
guanshi.invite_qrcode_url = relative_path
guanshi.save(update_fields=['invite_qrcode_url'])
logger.info("数据库更新成功")
return Response({
'code': 0,
'message': 'success',
'data': {'url': relative_path}
})
except Exception as e:
logger.error(f"生成二维码过程中发生未捕获异常: {str(e)}", exc_info=True)
return Response(
{'code': 500, 'message': '服务器内部错误', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
def get_fresh_access_token(self):
"""
直接请求微信接口获取新的 access_token不缓存保证最新。
"""
appid = getattr(settings, 'WEIXIN_APPID', '')
secret = getattr(settings, 'WEIXIN_SECRET', '')
if not appid or not secret:
logger.error("微信小程序配置缺失: WEIXIN_APPID 或 WEIXIN_SECRET 未设置")
return None
url = 'https://api.weixin.qq.com/cgi-bin/token'
params = {
'grant_type': 'client_credential',
'appid': appid,
'secret': secret
}
try:
logger.info("正在请求微信接口获取最新 access_token...")
resp = requests.get(url, params=params, timeout=5)
data = resp.json()
new_token = data.get('access_token')
if new_token:
logger.info("成功获取最新 access_token")
return new_token
else:
errcode = data.get('errcode', '未知')
errmsg = data.get('errmsg', '未知错误')
logger.error(f"获取 access_token 失败errcode={errcode}, errmsg={errmsg}")
return None
except Exception as e:
logger.error(f"请求微信 access_token 接口异常: {str(e)}", exc_info=True)
return None
class CYaoqingQRCodeView(APIView):
"""
点单端老板推荐官小程序码(落地点单首页,按 UserUID 绑定,无邀请码)。
POST /peizhi/cyaoqingewm
"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
current_user = request.user
try:
from jituan.services.club_context import resolve_effective_club_id
from utils.weixin_token import get_club_mini_access_token, is_weixin_token_invalid
from users.services.c_invite import get_or_create_c_yaoqing
from users.models import YonghuCYaoqing
if not getattr(current_user, 'UserUID', None):
return Response(
{'code': 500, 'message': '用户身份异常', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
club_id = resolve_effective_club_id(request, current_user)
profile = get_or_create_c_yaoqing(current_user)
inviter_uid = str(current_user.UserUID)
access_token = get_club_mini_access_token(club_id)
if not access_token:
return Response(
{'code': 500, 'message': '获取微信access_token失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# 必须进点单首页,禁止进打手/管事页
page_path = f'pages/index/index?yaoqingren={urllib.parse.quote(inviter_uid, safe="")}'
post_data = {
'path': page_path,
'width': 430,
'auto_color': False,
'line_color': {'r': 0, 'g': 0, 'b': 0},
}
def _request_qr(token):
try:
return requests.post(
f'https://api.weixin.qq.com/wxa/getwxacode?access_token={token}',
json=post_data,
timeout=10,
)
except Exception as e:
logger.error('请求微信接口异常: %s', e, exc_info=True)
return None
wx_resp = _request_qr(access_token)
if wx_resp is None:
return Response(
{'code': 500, 'message': '调用微信服务失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
if wx_resp.headers.get('content-type', '').startswith('application/json'):
try:
err = wx_resp.json()
if is_weixin_token_invalid(err.get('errcode'), err.get('errmsg', '')):
access_token = get_club_mini_access_token(club_id, force_refresh=True)
if access_token:
wx_resp = _request_qr(access_token)
except Exception:
pass
if wx_resp.status_code != 200:
errmsg = '未知错误'
try:
errmsg = wx_resp.json().get('errmsg', errmsg)
except Exception:
pass
return Response(
{'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
content_type = wx_resp.headers.get('content-type', '')
if 'image' not in content_type:
try:
errmsg = wx_resp.json().get('errmsg', '未知错误')
except Exception:
errmsg = '生成二维码失败'
return Response(
{'code': 500, 'message': f'生成二维码失败: {errmsg}', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
timestamp = int(time.time())
filename = f'{current_user.UserUID}_{timestamp}.png'
oss_path = f'erweima/cuser/{club_id}/{filename}'
file_obj = io.BytesIO(wx_resp.content)
full_url = upload_to_oss(file_obj, oss_path)
if not full_url:
return Response(
{'code': 500, 'message': '图片上传失败', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
cos_domain = getattr(settings, 'COS_DOMAIN', '')
if cos_domain and full_url.startswith(cos_domain):
relative_path = full_url[len(cos_domain):].lstrip('/')
else:
relative_path = oss_path
old_url = profile.invite_qrcode_url
if old_url:
try:
delete_from_oss(old_url)
except Exception as e:
logger.warning('删除旧点单端推荐二维码失败: %s', e)
with transaction.atomic():
locked = YonghuCYaoqing.objects.select_for_update().get(pk=profile.pk)
locked.invite_qrcode_url = relative_path
locked.save(update_fields=['invite_qrcode_url', 'UpdateTime'])
return Response({
'code': 0,
'message': 'success',
'data': {'url': relative_path, 'uid': inviter_uid},
})
except Exception as e:
logger.error('生成点单端推荐二维码失败: %s', e, exc_info=True)
return Response(
{'code': 500, 'message': '服务器内部错误', 'data': None},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
class ZuzhangHaibaoView(APIView):
"""
组长专属海报二维码生成接口
路径:/peizhi/zuzhanghb
方法POST
权限JWT认证仅限组长身份
返回:
{
"code": 0,
"message": "success",
"data": {
"url": "erweima/zuzhang/1000001_1234567890.png",
"yaoqingma": "ABC123XYZ"
}
}
"""
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
current_user = request.user
from jituan.services.club_context import resolve_effective_club_id
from utils.weixin_token import get_club_mini_access_token
club_id = resolve_effective_club_id(request, current_user)
# 1. 验证组长身份
try:
zuzhang = current_user.ZuzhangProfile
except UserZuzhang.DoesNotExist:
return Response({
'code': 403,
'message': '当前用户不是组长身份',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 2. 验证组长账号状态
if zuzhang.zhuangtai != 1:
return Response({
'code': 403,
'message': '组长账号状态异常,无法生成海报',
'data': None
}, status=status.HTTP_403_FORBIDDEN)
# 3. 获取或生成邀请码
invite_code = zuzhang.yaoqingma
if not invite_code:
invite_code = CreateInvitationCode(str(current_user.UserUID))
if not VerifyInvitationCode(invite_code):
logger.error(f"组长邀请码生成失败,用户: {current_user.UserUID}")
return Response({
'code': 500,
'message': '邀请码生成失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
zuzhang.yaoqingma = invite_code
zuzhang.save(update_fields=['yaoqingma'])
# 4. 获取微信 access_token按俱乐部对应小程序
access_token = get_club_mini_access_token(club_id, force_refresh=False)
if not access_token:
return Response({
'code': 500,
'message': '获取微信access_token失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# ===== 修改点:使用 A 接口getwxacode =====
encoded_invite = urllib.parse.quote(invite_code, safe='')
# 新小程序:打手端 Tab「我的」管事注册旧码 guanshiduan 由小程序兼容页转发
page_path = f'pages/fighter/fighter?registerType=guanshi&inviteCode={encoded_invite}'
wx_resp, wx_error = self._request_wx_qrcode(page_path, access_token)
if wx_error and is_weixin_token_invalid(wx_error.get('errcode'), wx_error.get('errmsg', '')):
logger.warning('组长海报二维码 token 失效,强制刷新后重试 club=%s', club_id)
access_token = get_club_mini_access_token(club_id, force_refresh=True)
if not access_token:
return Response({
'code': 500,
'message': '获取微信access_token失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
wx_resp, wx_error = self._request_wx_qrcode(page_path, access_token)
if wx_error:
errmsg = wx_error.get('errmsg', '未知错误')
logger.error(f"微信接口返回错误: {errmsg}")
return Response({
'code': 500,
'message': f'生成二维码失败: {errmsg}',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
if wx_resp is None:
return Response({
'code': 500,
'message': '调用微信服务失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 5. 上传到腾讯云 COS
timestamp = int(time.time())
filename = f"{current_user.UserUID}_{timestamp}.png"
oss_path = f"erweima/zuzhang/{club_id}/{filename}"
file_obj = io.BytesIO(wx_resp.content)
try:
full_url = upload_to_oss(file_obj, oss_path)
except Exception as e:
logger.error(f"上传到COS失败: {e}")
return Response({
'code': 500,
'message': '图片上传失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
if not full_url:
return Response({
'code': 500,
'message': '图片上传失败',
'data': None
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# 6. 获取相对路径
cos_domain = getattr(settings, 'COS_DOMAIN', '')
if cos_domain and full_url.startswith(cos_domain):
relative_path = full_url[len(cos_domain):].lstrip('/')
else:
relative_path = oss_path
# 7. 删除旧二维码文件
old_url = zuzhang.haibao_url
if old_url:
delete_from_oss(old_url)
# 8. 更新数据库中的 haibao_url 字段
with transaction.atomic():
zuzhang.haibao_url = relative_path
zuzhang.save(update_fields=['haibao_url'])
# 9. 返回邀请码和二维码相对路径
return Response({
'code': 0,
'message': 'success',
'data': {
'url': relative_path,
'yaoqingma': invite_code
}
})
def _request_wx_qrcode(self, page_path, access_token):
"""
调用 getwxacode 生成小程序码。
返回 (response, error_dict);成功时 error_dict 为 None。
"""
wx_url = f'https://api.weixin.qq.com/wxa/getwxacode?access_token={access_token}'
post_data = {
'path': page_path,
'width': 430,
'auto_color': False,
'line_color': {'r': 0, 'g': 0, 'b': 0},
}
try:
wx_resp = requests.post(wx_url, json=post_data, timeout=10)
except Exception as e:
logger.error(f'请求微信接口失败: {e}')
return None, {'errmsg': '调用微信服务失败'}
if wx_resp.status_code != 200:
error_info = {}
if wx_resp.headers.get('content-type', '').startswith('application/json'):
try:
error_info = wx_resp.json()
except Exception:
pass
return wx_resp, {
'errcode': error_info.get('errcode'),
'errmsg': error_info.get('errmsg', f'HTTP {wx_resp.status_code}'),
}
content_type = wx_resp.headers.get('content-type', '')
if 'image' not in content_type:
try:
error_info = wx_resp.json()
return wx_resp, {
'errcode': error_info.get('errcode'),
'errmsg': error_info.get('errmsg', '未知错误'),
}
except Exception:
return wx_resp, {'errmsg': '微信接口返回未知内容'}
return wx_resp, None