复用 ClubMiniappIcon 增加点单端海报背景;新增 C 端邀请码/换绑店铺接口;hqsp 支持 zhi_kan_shenhe,不影响支付抢单分红。 Co-authored-by: Cursor <cursoragent@cursor.com>
675 lines
27 KiB
Python
675 lines
27 KiB
Python
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):
|
||
"""
|
||
点单端用户邀请小程序码。
|
||
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 ensure_c_invite_code
|
||
from users.models import YonghuCYaoqing
|
||
|
||
club_id = resolve_effective_club_id(request, current_user)
|
||
profile, invite_code, _ = ensure_c_invite_code(current_user)
|
||
|
||
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
|
||
)
|
||
|
||
encoded_invite = urllib.parse.quote(invite_code, safe='')
|
||
page_path = f'pages/index/index?inviteCode={encoded_invite}'
|
||
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('删除旧 C 端二维码失败: %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, 'yaoqingma': invite_code},
|
||
})
|
||
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
|
||
|
||
|
||
|
||
|