277 lines
9.0 KiB
Python
277 lines
9.0 KiB
Python
"""管事自助更换组长:配额、校验、换绑。"""
|
||
import logging
|
||
|
||
from django.core.exceptions import ObjectDoesNotExist
|
||
from django.db import transaction
|
||
from django.db.models import F
|
||
|
||
from jituan.services.club_context import resolve_effective_club_id
|
||
from jituan.services.invite_guard import assert_invite_same_club
|
||
from users.models import GuanshiChangeLeaderLog, GuanshiChangeLeaderQuota, UserGuanshi, UserZuzhang
|
||
from users.business_models import User
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
DEFAULT_QUOTA = 1
|
||
|
||
|
||
def _guanshi_display_name(user):
|
||
try:
|
||
return (user.BossProfile.nickname or '').strip() or user.UserUID
|
||
except ObjectDoesNotExist:
|
||
return user.UserUID
|
||
|
||
|
||
def _zuzhang_brief(uid):
|
||
if not uid:
|
||
return None
|
||
try:
|
||
u = User.query.get(UserUID=uid)
|
||
except User.DoesNotExist:
|
||
return {'uid': uid, 'nicheng': '未知', 'touxiang': ''}
|
||
try:
|
||
nicheng = u.BossProfile.nickname or '组长'
|
||
except ObjectDoesNotExist:
|
||
nicheng = '组长'
|
||
return {
|
||
'uid': u.UserUID,
|
||
'nicheng': nicheng,
|
||
'touxiang': u.Avatar or '',
|
||
}
|
||
|
||
|
||
def get_or_create_quota(club_id, guanshi_uid):
|
||
club_id = (club_id or '').strip()
|
||
guanshi_uid = (guanshi_uid or '').strip()
|
||
obj, _ = GuanshiChangeLeaderQuota.objects.get_or_create(
|
||
club_id=club_id,
|
||
guanshi_uid=guanshi_uid,
|
||
defaults={'total_quota': DEFAULT_QUOTA, 'used_count': 0},
|
||
)
|
||
return obj
|
||
|
||
|
||
def quota_payload(quota):
|
||
return {
|
||
'total_quota': int(quota.total_quota or 0),
|
||
'used_count': int(quota.used_count or 0),
|
||
'remain_quota': quota.remain_quota,
|
||
}
|
||
|
||
|
||
def build_change_leader_info(request, user):
|
||
"""小程序 info 接口数据。"""
|
||
try:
|
||
guanshi = user.GuanshiProfile
|
||
except ObjectDoesNotExist:
|
||
return {'ok': False, 'code': 403, 'msg': '您还不是管事'}
|
||
|
||
if guanshi.zhuangtai != 1:
|
||
return {'ok': False, 'code': 403, 'msg': '管事账号已禁用,无法更换组长'}
|
||
|
||
club_id = resolve_effective_club_id(request, user)
|
||
quota = get_or_create_quota(club_id, user.UserUID)
|
||
return {
|
||
'ok': True,
|
||
'code': 200,
|
||
'msg': 'ok',
|
||
'data': {
|
||
'club_id': club_id,
|
||
'current_zuzhang': _zuzhang_brief(guanshi.yaoqingren),
|
||
'quota': quota_payload(quota),
|
||
},
|
||
}
|
||
|
||
|
||
def change_leader_by_invite(request, user, invite_code, source='miniapp', operator=None):
|
||
"""
|
||
用组长邀请码换绑。成功返回 data;失败返回 ok=False。
|
||
仅更新管事 yaoqingren,不改名下打手。
|
||
"""
|
||
invite_code = (invite_code or '').strip()
|
||
if not invite_code:
|
||
return {'ok': False, 'code': 400, 'msg': '请输入组长邀请码'}
|
||
if len(invite_code) > 100:
|
||
return {'ok': False, 'code': 400, 'msg': '邀请码长度超过限制'}
|
||
|
||
try:
|
||
guanshi = user.GuanshiProfile
|
||
except ObjectDoesNotExist:
|
||
return {'ok': False, 'code': 403, 'msg': '您还不是管事'}
|
||
|
||
if guanshi.zhuangtai != 1:
|
||
return {'ok': False, 'code': 403, 'msg': '管事账号已禁用,无法更换组长'}
|
||
|
||
try:
|
||
zuzhang = UserZuzhang.query.select_related('user').get(yaoqingma=invite_code)
|
||
except UserZuzhang.DoesNotExist:
|
||
return {'ok': False, 'code': 404, 'msg': '邀请码无效或不存在'}
|
||
|
||
if zuzhang.zhuangtai != 1:
|
||
return {'ok': False, 'code': 403, 'msg': '该邀请码对应的组长账号已被禁用'}
|
||
|
||
new_uid = zuzhang.user.UserUID
|
||
if new_uid == user.UserUID:
|
||
return {'ok': False, 'code': 400, 'msg': '不能将自己设为组长'}
|
||
|
||
old_uid = (guanshi.yaoqingren or '').strip()
|
||
if old_uid and old_uid == new_uid:
|
||
return {'ok': False, 'code': 400, 'msg': '已是该组长,无需更换'}
|
||
|
||
ok, invite_msg = assert_invite_same_club(request, zuzhang.user, user)
|
||
if not ok:
|
||
return {'ok': False, 'code': 403, 'msg': invite_msg}
|
||
|
||
club_id = resolve_effective_club_id(request, user)
|
||
op = (operator or user.UserUID or '').strip()
|
||
|
||
with transaction.atomic():
|
||
quota = (
|
||
GuanshiChangeLeaderQuota.objects
|
||
.select_for_update()
|
||
.filter(club_id=club_id, guanshi_uid=user.UserUID)
|
||
.first()
|
||
)
|
||
if not quota:
|
||
quota = GuanshiChangeLeaderQuota.objects.create(
|
||
club_id=club_id,
|
||
guanshi_uid=user.UserUID,
|
||
total_quota=DEFAULT_QUOTA,
|
||
used_count=0,
|
||
)
|
||
quota = (
|
||
GuanshiChangeLeaderQuota.objects
|
||
.select_for_update()
|
||
.get(pk=quota.pk)
|
||
)
|
||
|
||
if quota.remain_quota <= 0:
|
||
return {'ok': False, 'code': 403, 'msg': '更换次数已用完,请联系客服增加次数'}
|
||
|
||
# 再读一次管事行,防并发
|
||
guanshi = (
|
||
UserGuanshi.query
|
||
.select_for_update()
|
||
.select_related('user')
|
||
.get(pk=guanshi.pk)
|
||
)
|
||
old_uid = (guanshi.yaoqingren or '').strip()
|
||
if old_uid == new_uid:
|
||
return {'ok': False, 'code': 400, 'msg': '已是该组长,无需更换'}
|
||
|
||
guanshi.yaoqingren = new_uid
|
||
guanshi.save(update_fields=['yaoqingren'])
|
||
|
||
if old_uid:
|
||
try:
|
||
old_zz = UserZuzhang.query.select_related('user').get(user__UserUID=old_uid)
|
||
if (old_zz.yaoqing_zongshu or 0) > 0:
|
||
UserZuzhang.query.filter(pk=old_zz.pk).update(
|
||
yaoqing_zongshu=F('yaoqing_zongshu') - 1
|
||
)
|
||
except UserZuzhang.DoesNotExist:
|
||
pass
|
||
UserZuzhang.query.filter(pk=zuzhang.pk).update(
|
||
yaoqing_zongshu=F('yaoqing_zongshu') + 1
|
||
)
|
||
|
||
quota.used_count = int(quota.used_count or 0) + 1
|
||
quota.save(update_fields=['used_count', 'UpdateTime'])
|
||
|
||
GuanshiChangeLeaderLog.objects.create(
|
||
club_id=club_id,
|
||
guanshi_uid=user.UserUID,
|
||
guanshi_name=_guanshi_display_name(user)[:50],
|
||
old_zuzhang_uid=old_uid or '',
|
||
new_zuzhang_uid=new_uid,
|
||
new_invite_code=invite_code[:40],
|
||
source=source or 'miniapp',
|
||
operator=op[:64],
|
||
)
|
||
|
||
logger.info(
|
||
'guanshi_change_leader ok club=%s gs=%s %s -> %s source=%s',
|
||
club_id, user.UserUID, old_uid, new_uid, source,
|
||
)
|
||
return {
|
||
'ok': True,
|
||
'code': 200,
|
||
'msg': '更换组长成功',
|
||
'data': {
|
||
'current_zuzhang': _zuzhang_brief(new_uid),
|
||
'quota': quota_payload(quota),
|
||
'old_zuzhang_uid': old_uid or '',
|
||
'new_zuzhang_uid': new_uid,
|
||
},
|
||
}
|
||
|
||
|
||
def set_total_quota(club_id, guanshi_uid, total_quota):
|
||
"""后台设置总次数(不得小于已用次数)。"""
|
||
try:
|
||
total = int(total_quota)
|
||
except (TypeError, ValueError):
|
||
return {'ok': False, 'code': 400, 'msg': '总次数须为整数'}
|
||
if total < 0:
|
||
return {'ok': False, 'code': 400, 'msg': '总次数不能为负数'}
|
||
|
||
quota = get_or_create_quota(club_id, guanshi_uid)
|
||
if total < int(quota.used_count or 0):
|
||
return {
|
||
'ok': False,
|
||
'code': 400,
|
||
'msg': f'总次数不能小于已用次数(已用 {quota.used_count})',
|
||
}
|
||
quota.total_quota = total
|
||
quota.save(update_fields=['total_quota', 'UpdateTime'])
|
||
return {'ok': True, 'code': 0, 'msg': '已更新', 'data': quota_payload(quota)}
|
||
|
||
|
||
def add_quota(club_id, guanshi_uid, add_n):
|
||
"""后台追加次数(增加 total_quota)。"""
|
||
try:
|
||
n = int(add_n)
|
||
except (TypeError, ValueError):
|
||
return {'ok': False, 'code': 400, 'msg': '追加次数须为整数'}
|
||
if n <= 0:
|
||
return {'ok': False, 'code': 400, 'msg': '追加次数须大于 0'}
|
||
quota = get_or_create_quota(club_id, guanshi_uid)
|
||
quota.total_quota = int(quota.total_quota or 0) + n
|
||
quota.save(update_fields=['total_quota', 'UpdateTime'])
|
||
return {'ok': True, 'code': 0, 'msg': '已追加', 'data': quota_payload(quota)}
|
||
|
||
|
||
def list_change_logs(club_id, guanshi_uid, page=1, page_size=20):
|
||
qs = (
|
||
GuanshiChangeLeaderLog.objects
|
||
.filter(club_id=club_id, guanshi_uid=guanshi_uid)
|
||
.order_by('-CreateTime', '-id')
|
||
)
|
||
total = qs.count()
|
||
try:
|
||
page = max(1, int(page))
|
||
except (TypeError, ValueError):
|
||
page = 1
|
||
try:
|
||
page_size = min(100, max(1, int(page_size)))
|
||
except (TypeError, ValueError):
|
||
page_size = 20
|
||
start = (page - 1) * page_size
|
||
rows = []
|
||
for row in qs[start:start + page_size]:
|
||
rows.append({
|
||
'id': row.id,
|
||
'old_zuzhang_uid': row.old_zuzhang_uid,
|
||
'new_zuzhang_uid': row.new_zuzhang_uid,
|
||
'new_invite_code': row.new_invite_code,
|
||
'source': row.source,
|
||
'operator': row.operator,
|
||
'created_at': row.CreateTime.isoformat() if row.CreateTime else None,
|
||
})
|
||
return {
|
||
'list': rows,
|
||
'total': total,
|
||
'page': page,
|
||
'page_size': page_size,
|
||
}
|