"""管事自助更换组长:配额、校验、换绑。""" 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, }