feat: 俱乐部种子组长/管事;弹窗时间窗空值可用
This commit is contained in:
@@ -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 已定义)
|
||||
|
||||
|
||||
201
jituan/services/club_seed_invite.py
Normal file
201
jituan/services/club_seed_invite.py
Normal file
@@ -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),
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user