diff --git a/config/views/miniapp_config.py b/config/views/miniapp_config.py index 3f08bad..b9a0ad6 100644 --- a/config/views/miniapp_config.py +++ b/config/views/miniapp_config.py @@ -179,12 +179,16 @@ class PopupConfigView(APIView): }) # 4. 查询该页面下所有启用的弹窗(且在有效时间范围内) + # start/end 为空视为不限制(避免后台「配了却永远查不到」) + from django.db.models import Q + now = timezone.now() popups = PopupConfig.query.filter( page=popup_page, is_active=True, - start_time__lte=now, - end_time__gte=now + ).filter( + Q(start_time__isnull=True) | Q(start_time__lte=now), + Q(end_time__isnull=True) | Q(end_time__gte=now), ).prefetch_related('images') # 预加载图片,避免N+1查询 # 按 sort_order 排序(model Meta 已定义) diff --git a/jituan/services/club_seed_invite.py b/jituan/services/club_seed_invite.py new file mode 100644 index 0000000..b674d38 --- /dev/null +++ b/jituan/services/club_seed_invite.py @@ -0,0 +1,201 @@ +"""俱乐部种子组长 / 管事(邀请链)。 + +规则: +- 若该俱乐部在组长表或管事表已有归属用户,则对应角色不再造种子; +- 两者都已有 → 整单跳过; +- 缺哪个补哪个;管事邀请人挂到该俱乐部现有/新建的组长 UID。 +""" +from __future__ import annotations + +import hashlib +import logging +import random +import time +from typing import Any, Dict, Optional, Tuple + +from django.db import transaction + +from jituan.models import Club +from jituan.services.default_invite import ( + get_club_default_dashou_invite, + set_club_default_dashou_invite, +) +from users.business_models import User +from users.models import UserBoss, UserGuanshi, UserZuzhang +from utils.invitationcode_utils import CreateInvitationCode + +logger = logging.getLogger(__name__) + + +def _club_zuzhang_qs(club_id: str): + return UserZuzhang.query.filter(user__ClubID=club_id) + + +def _club_guanshi_qs(club_id: str): + return UserGuanshi.query.filter(user__ClubID=club_id) + + +def club_invite_seed_status(club_id: str) -> Dict[str, Any]: + club_id = (club_id or '').strip() + zz_cnt = _club_zuzhang_qs(club_id).count() if club_id else 0 + gs_cnt = _club_guanshi_qs(club_id).count() if club_id else 0 + return { + 'club_id': club_id, + 'zuzhang_count': zz_cnt, + 'guanshi_count': gs_cnt, + 'need_seed': zz_cnt == 0 or gs_cnt == 0, + 'has_zuzhang': zz_cnt > 0, + 'has_guanshi': gs_cnt > 0, + } + + +def _alloc_user_uid() -> str: + for _ in range(30): + 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 RuntimeError('生成种子用户UID失败') + + +def _phone_for(club_id: str, role: str) -> str: + digest = hashlib.md5(f'{club_id}:{role}:seed_phone_v1'.encode('utf-8')).hexdigest() + # 199 + 8 位数字,尽量不与真实号冲突 + return '199' + ''.join(str(int(ch, 16) % 10) for ch in digest[:8]) + + +def _ensure_seed_user(club_id: str, role: str, nickname: str) -> Tuple[User, bool]: + openid = f'{club_id}_seed_openid_{role}_v1'[:64] + phone = _phone_for(club_id, role) + user = User.query.filter(OpenID=openid).first() + if not user: + user = User.query.filter(Phone=phone, ClubID=club_id).first() + if user: + changed = False + if (user.ClubID or '') != club_id: + user.ClubID = club_id + changed = True + if changed: + user.save(update_fields=['ClubID']) + return user, False + + uid = _alloc_user_uid() + user = User.query.create( + UserUID=uid, + UserName=f'{club_id}_seed_{role}', + OpenID=openid, + Phone=phone, + ClubID=club_id, + ) + UserBoss.query.create(user=user, nickname=nickname) + return user, True + + +def _ensure_zuzhang(user: User) -> Tuple[UserZuzhang, bool]: + try: + return user.ZuzhangProfile, False + except UserZuzhang.DoesNotExist: + pass + z = UserZuzhang.query.create( + user=user, + yaoqingma=CreateInvitationCode(str(user.UserUID)), + zhuangtai=1, + ) + return z, True + + +def _ensure_guanshi(user: User, zuzhang_uid: str) -> Tuple[UserGuanshi, bool]: + try: + return user.GuanshiProfile, False + except UserGuanshi.DoesNotExist: + pass + g = UserGuanshi.query.create( + user=user, + yaoqingma=CreateInvitationCode(str(user.UserUID)), + yaoqingren=zuzhang_uid, + zhuangtai=1, + ) + return g, True + + +def ensure_club_seed_invite(club_id: str, set_default_invite: bool = True) -> Dict[str, Any]: + """ + 为俱乐部补齐种子组长/管事。 + 已有对应角色数据则跳过该角色;两者都有则整单 skipped。 + """ + club_id = (club_id or '').strip() + if not club_id: + return {'ok': False, 'msg': '缺少 club_id'} + + club = Club.query.filter(club_id=club_id).first() + if not club: + return {'ok': False, 'msg': f'俱乐部 {club_id} 不存在'} + + status_before = club_invite_seed_status(club_id) + if not status_before['need_seed']: + return { + 'ok': True, + 'skipped': True, + 'msg': '该俱乐部已有组长与管事,无需种子', + 'status': status_before, + 'default_dashou_invite': get_club_default_dashou_invite(club_id), + } + + created = {'zuzhang': False, 'guanshi': False} + zz_profile: Optional[UserZuzhang] = None + gs_profile: Optional[UserGuanshi] = None + zz_user: Optional[User] = None + gs_user: Optional[User] = None + + with transaction.atomic(): + if status_before['has_zuzhang']: + zz_profile = _club_zuzhang_qs(club_id).select_related('user').first() + zz_user = zz_profile.user if zz_profile else None + else: + zz_user, _ = _ensure_seed_user(club_id, 'zuzhang', f'{club.name or club_id}种子组长') + zz_profile, zz_new = _ensure_zuzhang(zz_user) + created['zuzhang'] = zz_new + + if not zz_user or not zz_profile: + return {'ok': False, 'msg': '无法确定种子组长'} + + if status_before['has_guanshi']: + gs_profile = _club_guanshi_qs(club_id).select_related('user').first() + gs_user = gs_profile.user if gs_profile else None + else: + gs_user, _ = _ensure_seed_user(club_id, 'guanshi', f'{club.name or club_id}种子管事') + gs_profile, gs_new = _ensure_guanshi(gs_user, str(zz_user.UserUID)) + created['guanshi'] = gs_new + + invite = '' + if gs_profile: + invite = (gs_profile.yaoqingma or '').strip() + # config_json 里还没有显式默认邀请码时,写入管事邀请码 + raw_cfg_invite = '' + try: + raw_cfg_invite = ((club.config_json or {}).get('default_dashou_invite') or '').strip() + except Exception: + raw_cfg_invite = '' + if set_default_invite and invite and not raw_cfg_invite: + set_club_default_dashou_invite(club, invite) + club.save(update_fields=['config_json']) + + status_after = club_invite_seed_status(club_id) + return { + 'ok': True, + 'skipped': False, + 'msg': '种子组长/管事已就绪', + 'created': created, + 'status': status_after, + 'zuzhang': { + 'user_uid': str(zz_user.UserUID) if zz_user else '', + 'invite_code': (zz_profile.yaoqingma if zz_profile else '') or '', + }, + 'guanshi': { + 'user_uid': str(gs_user.UserUID) if gs_user else '', + 'invite_code': (gs_profile.yaoqingma if gs_profile else '') or '', + }, + 'default_dashou_invite': get_club_default_dashou_invite(club_id), + } diff --git a/jituan/views.py b/jituan/views.py index 2b1fa9d..72794be 100644 --- a/jituan/views.py +++ b/jituan/views.py @@ -765,7 +765,34 @@ class ClubManageView(APIView): 'data': self._serialize(club, full=True), }) - return Response({'code': 400, 'msg': '无效 action,支持 list/get/update/create'}) + if action in ('seed_invite_status', 'seed_invite'): + from jituan.services.club_seed_invite import ( + club_invite_seed_status, + ensure_club_seed_invite, + ) + if action == 'seed_invite_status': + return Response({ + 'code': 0, + 'data': club_invite_seed_status(club_id), + }) + result = ensure_club_seed_invite(club_id) + if not result.get('ok'): + return Response({'code': 400, 'msg': result.get('msg') or '种子失败'}) + # 刷新默认邀请码到序列化结果 + club = Club.query.filter(club_id=club_id).first() + return Response({ + 'code': 0, + 'msg': result.get('msg') or 'ok', + 'data': { + **result, + 'club': self._serialize(club, full=True) if club else None, + }, + }) + + return Response({ + 'code': 400, + 'msg': '无效 action,支持 list/get/update/create/seed_invite/seed_invite_status', + }) except Exception: logger.exception('ClubManageView 异常') return Response({'code': 99, 'msg': '俱乐部配置操作失败'}, status=500)