diff --git a/config/urls.py b/config/urls.py index 4e4129d..dc4a914 100644 --- a/config/urls.py +++ b/config/urls.py @@ -3,8 +3,8 @@ from django.urls import path from .views import ShangpinGonggaoView, AdminUpdateConfigView, AdminUploadImageView, AdminConfigQueryView, \ DashouPaihangView,GetAdminZhanghaoView, GetXiugaiJiluView,ShangjiaTianjiaMobanView, ShangjiaMobanListView, \ ShangjiaGengxinMobanView, ShangjiaShanchuMobanView, ShangjiaGenerateLinkView,KehuGetDingdanLianjieView, \ - KehuTianxieDingdanView,GuanZhuAListView, ZuzhangHaibaoView,GuanshiQRCodeView,\ - GetDynamicConfigView, PopupConfigView, GetWithdrawModeView,CheckPhoneAuthView,\ + KehuTianxieDingdanView, GuanZhuAListView, ZuzhangHaibaoView,GuanshiQRCodeView,\ + GetDynamicConfigView, HaibaoPeizhiView, PopupConfigView, GetWithdrawModeView,CheckPhoneAuthView,\ ShangjiaLianjieListView @@ -32,6 +32,7 @@ urlpatterns = [ path('guanshiewm', GuanshiQRCodeView.as_view(), name='管事生成二维码'), path('zuzhanghb', ZuzhangHaibaoView.as_view(), name='组长二维码'), path('qdpzhq', GetDynamicConfigView.as_view(), name='小程序获取配置'), + path('haibaopz', HaibaoPeizhiView.as_view(), name='推广海报配置'), path('tanchuang', PopupConfigView.as_view(), name='微信小程序获取弹窗数据'), path('hqtxzsym', GetWithdrawModeView.as_view(), name='小程序获取提现方式'), diff --git a/config/views.py b/config/views.py index da97a7e..03d5cd5 100644 --- a/config/views.py +++ b/config/views.py @@ -115,6 +115,31 @@ class GetDynamicConfigView(APIView): return Response({"code": 500, "msg": "服务器内部错误"}, status=500) +class HaibaoPeizhiView(APIView): + """小程序推广海报背景配置(按俱乐部)。""" + permission_classes = [AllowAny] + parser_classes = [JSONParser] + + def post(self, request): + try: + from jituan.services.display_config import _display_club_id + from jituan.services.miniapp_assets import get_poster_payload + + club_id = _display_club_id(request) + poster = get_poster_payload(club_id) + return Response({ + 'code': 0, + 'msg': 'success', + 'data': { + 'club_id': club_id, + 'poster': poster, + }, + }) + except Exception as e: + logger.error('获取海报配置失败: %s', e, exc_info=True) + return Response({'code': 500, 'msg': '服务器内部错误'}, status=500) + + class ShangpinGonggaoView(APIView): @@ -2782,6 +2807,10 @@ class GuanshiQRCodeView(APIView): 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}") @@ -2815,15 +2844,15 @@ class GuanshiQRCodeView(APIView): else: logger.info(f"使用已有邀请码: {invite_code}") - # 直接获取新的 access_token,不缓存 - access_token = self.get_fresh_access_token() + # 按俱乐部使用对应小程序 appid 的 access_token + access_token = get_club_mini_access_token(club_id) if not access_token: - logger.error("获取 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]}...") + logger.info(f"获取到 access_token (前10位): {access_token[:10]}... club={club_id}") encoded_invite = urllib.parse.quote(invite_code, safe='') page_path = f'pages/dashouduan/dashouduan?inviteCode={encoded_invite}' @@ -2836,16 +2865,35 @@ class GuanshiQRCodeView(APIView): } logger.info(f"调用微信接口生成小程序码,path: {page_path}") - try: - wx_resp = requests.post(wx_url, json=post_data, timeout=10) - logger.info(f"微信接口响应状态码: {wx_resp.status_code}") - except Exception as e: - logger.error(f"请求微信接口异常: {str(e)}", exc_info=True) + 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': @@ -2881,7 +2929,7 @@ class GuanshiQRCodeView(APIView): timestamp = int(time.time()) filename = f"{current_user.UserUID}_{timestamp}.png" - oss_path = f"erweima/guanshi/{filename}" + oss_path = f"erweima/guanshi/{club_id}/{filename}" file_obj = io.BytesIO(wx_resp.content) logger.info(f"开始上传图片到COS: {oss_path}") @@ -2994,6 +3042,10 @@ class ZuzhangHaibaoView(APIView): 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: @@ -3027,8 +3079,8 @@ class ZuzhangHaibaoView(APIView): zuzhang.yaoqingma = invite_code zuzhang.save(update_fields=['yaoqingma']) - # 4. 获取微信 access_token(优先 stable_token,token 失效时自动刷新重试) - access_token = get_weixin_mini_access_token(force_refresh=False) + # 4. 获取微信 access_token(按俱乐部对应小程序) + access_token = get_club_mini_access_token(club_id, force_refresh=False) if not access_token: return Response({ 'code': 500, @@ -3042,8 +3094,8 @@ class ZuzhangHaibaoView(APIView): 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 失效,强制刷新后重试') - access_token = get_weixin_mini_access_token(force_refresh=True) + 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, @@ -3071,7 +3123,7 @@ class ZuzhangHaibaoView(APIView): # 5. 上传到腾讯云 COS timestamp = int(time.time()) filename = f"{current_user.UserUID}_{timestamp}.png" - oss_path = f"erweima/zuzhang/{filename}" + oss_path = f"erweima/zuzhang/{club_id}/{filename}" file_obj = io.BytesIO(wx_resp.content) try: diff --git a/jituan/services/miniapp_assets.py b/jituan/services/miniapp_assets.py index 74c2cdc..9097fa8 100644 --- a/jituan/services/miniapp_assets.py +++ b/jituan/services/miniapp_assets.py @@ -19,6 +19,8 @@ MINIAPP_ICON_META = [ ('fighter_recharge_deposit', '打手中心-保证金充值横幅(右侧)'), ('mine_pindao', '频道入口'), ('icon_refresh', '通用刷新'), + ('guanshi_poster_bg', '管事推广海报背景'), + ('zuzhang_poster_bg', '组长推广海报背景'), ] MINIAPP_ICON_KEYS = [k for k, _ in MINIAPP_ICON_META] @@ -35,6 +37,19 @@ MINIAPP_ICON_DEFAULTS['merchant_gold_banner'] = 'beijing/tubiao/chengxutubiao/me MINIAPP_ICON_DEFAULTS['fighter_recharge_member'] = f'{ICON_FOLDER}/fighter_recharge_member.png' MINIAPP_ICON_DEFAULTS['fighter_recharge_deposit'] = f'{ICON_FOLDER}/fighter_recharge_deposit.png' MINIAPP_ICON_DEFAULTS['mine_pindao'] = 'beijing/tubiao/grzx_guanzhualong.jpg' +MINIAPP_ICON_DEFAULTS['guanshi_poster_bg'] = 'beijing/haibaobeijing.jpg' +MINIAPP_ICON_DEFAULTS['zuzhang_poster_bg'] = 'beijing/zuzhangbeijing.jpg' + +POSTER_BG_FIELDS = { + 'guanshi_bg': 'guanshi_poster_bg', + 'zuzhang_bg': 'zuzhang_poster_bg', +} +POSTER_BG_DEFAULTS = { + 'guanshi_bg': 'beijing/haibaobeijing.jpg', + 'zuzhang_bg': 'beijing/zuzhangbeijing.jpg', +} +POSTER_FOLDER = 'beijing/haibao' +POSTER_ICON_KEYS = frozenset(POSTER_BG_FIELDS.values()) def default_icon_path(icon_key: str) -> str: @@ -70,11 +85,31 @@ def get_pindao_payload(club_id: str) -> dict: } +def get_poster_payload(club_id: str) -> dict: + """各俱乐部推广海报背景相对路径(后台上传后覆盖默认)。""" + from config.models import ClubMiniappIcon + + rows = { + r.icon_key: r + for r in ClubMiniappIcon.query.filter(club_id=club_id, is_active=True) + } + result = {} + for field, icon_key in POSTER_BG_FIELDS.items(): + default = POSTER_BG_DEFAULTS[field] + row = rows.get(icon_key) + if row and row.is_uploaded and (row.image_path or '').strip(): + result[field] = row.image_path.strip() + else: + result[field] = default + return result + + def build_miniapp_assets_payload(request) -> dict: club_id = _display_club_id(request) return { 'icons': get_miniapp_icons_dict(club_id), 'pindao': get_pindao_payload(club_id), + 'poster': get_poster_payload(club_id), } diff --git a/jituan/views_miniapp_assets.py b/jituan/views_miniapp_assets.py index f508abd..2b9ef12 100644 --- a/jituan/views_miniapp_assets.py +++ b/jituan/views_miniapp_assets.py @@ -14,9 +14,12 @@ from jituan.services.miniapp_assets import ( ICON_FOLDER, MINIAPP_ICON_LABELS, MINIAPP_ICON_META, + POSTER_FOLDER, + POSTER_ICON_KEYS, default_icon_path, get_miniapp_icons_dict, get_pindao_payload, + get_poster_payload, ) from products.views import delete_from_oss, upload_to_oss, validate_image @@ -115,7 +118,10 @@ class ClubMiniappIconManageView(APIView): ext = os.path.splitext(file_obj.name)[1].lower() or '.png' if ext not in ('.png', '.jpg', '.jpeg', '.webp'): ext = '.png' - oss_path = f'{ICON_FOLDER}/{icon_key}{ext}' + if icon_key in POSTER_ICON_KEYS: + oss_path = f'{POSTER_FOLDER}/{club_id}_{icon_key}{ext}' + else: + oss_path = f'{ICON_FOLDER}/{icon_key}{ext}' row = ClubMiniappIcon.query.filter(club_id=club_id, icon_key=icon_key).first() if row and row.image_path and row.image_path != oss_path: