feat(Talk): 群支持最多 10 个管理员,改群名收紧到群主/管理员
成员角色新增 2=管理员(MAX_GROUP_ADMINS=10);新增 POST API/Talk/Conversation/Member/Role 设置/取消管理员(仅群主或 Talk 管理员,不能变更群主);会话列表返回 IsGroupAdmin/MyRole/AdminCount/CanManage 与成员 MemberRole;RenameConversation 增加 IsTalkAdmin 参数并把群聊改名权限收紧到群主/群管理员;Entry 下发 GroupAdminLimit。
This commit is contained in:
1 parent
aa56281493
commit
90b20f3c94
4 files changed
+145
-17
No files matched your search
@@ -30,6 +30,10 @@ STATUS_DISSOLVED = 0
|
||||
# 成员角色
|
||||
ROLE_MEMBER = 0
|
||||
ROLE_OWNER = 1
|
||||
ROLE_ADMIN = 2
|
||||
|
||||
# 单群管理员上限(群主不计入)
|
||||
MAX_GROUP_ADMINS = 10
|
||||
|
||||
# 消息类型
|
||||
MSG_TEXT = 0
|
||||
@@ -363,11 +367,11 @@ class TalkManager:
|
||||
convs = list(TalkConversation.objects.filter(
|
||||
ConversationUUID__in=conv_ids, Status=STATUS_NORMAL,
|
||||
).order_by('-LastMessageTime'))
|
||||
# 批量取成员与用户名,避免 N+1
|
||||
# 批量取成员与用户名,避免 N+1(成员同时带上角色,供前端展示群主/管理员与权限判定)
|
||||
members_map = defaultdict(list)
|
||||
all_uuids = set()
|
||||
for m in TalkMember.objects.filter(ConversationUUID__in=[c.ConversationUUID for c in convs]):
|
||||
members_map[_ToStr(m.ConversationUUID)].append(m.UserUUID)
|
||||
members_map[_ToStr(m.ConversationUUID)].append((m.UserUUID, m.MemberRole))
|
||||
all_uuids.add(_ToBytes(m.UserUUID))
|
||||
name_map = TalkManager._UserNames(all_uuids)
|
||||
records = [
|
||||
@@ -539,18 +543,82 @@ class TalkManager:
|
||||
return True, '', {'ConversationUUID': _ToStr(conv.ConversationUUID)}
|
||||
|
||||
@staticmethod
|
||||
def RenameConversation(ConversationUUID, OperatorUUID, Title):
|
||||
"""重命名会话(仅成员可操作)"""
|
||||
def RenameConversation(ConversationUUID, OperatorUUID, Title, IsTalkAdmin=False):
|
||||
"""重命名会话
|
||||
|
||||
群聊仅「群主 / 群管理员 / Talk 管理员」可改;单聊保持仅成员可改(无群主概念)。
|
||||
"""
|
||||
conv = TalkManager.GetConversation(ConversationUUID)
|
||||
operator = _ToBytes(OperatorUUID)
|
||||
if conv is None or conv.Status != STATUS_NORMAL:
|
||||
return False, '会话不存在或已解散', None
|
||||
if not TalkManager.IsMember(conv.ConversationUUID, operator):
|
||||
member = TalkManager.GetMember(conv.ConversationUUID, operator)
|
||||
if member is None:
|
||||
return False, '您不是该会话成员', None
|
||||
if conv.ConversationType == TYPE_GROUP and not IsTalkAdmin:
|
||||
if not TalkManager._IsGroupManager(conv, member, operator):
|
||||
return False, '仅群主或管理员可修改群名', None
|
||||
conv.Title = (Title or '').strip()[:128]
|
||||
conv.save(update_fields=['Title'])
|
||||
return True, '', {'ConversationUUID': _ToStr(conv.ConversationUUID), 'Title': conv.Title}
|
||||
|
||||
@staticmethod
|
||||
def SetMemberRole(ConversationUUID, OperatorUUID, TargetUUID, Role, IsTalkAdmin=False):
|
||||
"""设置群成员角色(0=成员,2=管理员)
|
||||
|
||||
仅「群主 / Talk 管理员」可操作;群主与自身角色不可通过本接口变更;
|
||||
管理员数量上限 MAX_GROUP_ADMINS(群主不计入)。
|
||||
|
||||
Returns:
|
||||
(ok: bool, msg: str, data: dict|None)
|
||||
"""
|
||||
conv = TalkManager.GetConversation(ConversationUUID)
|
||||
operator = _ToBytes(OperatorUUID)
|
||||
target = _ToBytes(TargetUUID)
|
||||
if conv is None or conv.Status != STATUS_NORMAL:
|
||||
return False, '会话不存在或已解散', None
|
||||
if conv.ConversationType != TYPE_GROUP:
|
||||
return False, '仅群聊可设置管理员', None
|
||||
operator_member = TalkManager.GetMember(conv.ConversationUUID, operator)
|
||||
if operator_member is None:
|
||||
return False, '您不是该会话成员', None
|
||||
is_owner = operator_member.MemberRole == ROLE_OWNER \
|
||||
or _ToBytes(conv.CreatorUUID) == operator
|
||||
if not is_owner and not IsTalkAdmin:
|
||||
return False, '仅群主或 Talk 管理员可设置管理员', None
|
||||
if target is None:
|
||||
return False, '缺少目标成员', None
|
||||
target_member = TalkManager.GetMember(conv.ConversationUUID, target)
|
||||
if target_member is None:
|
||||
return False, '该用户不是本群成员', None
|
||||
if target_member.MemberRole == ROLE_OWNER:
|
||||
return False, '不能变更群主角色', None
|
||||
try:
|
||||
role = int(Role)
|
||||
except (TypeError, ValueError):
|
||||
return False, '角色参数无效', None
|
||||
if role not in (ROLE_MEMBER, ROLE_ADMIN):
|
||||
return False, '角色参数无效', None
|
||||
if role == ROLE_ADMIN and target_member.MemberRole != ROLE_ADMIN:
|
||||
admin_count = TalkMember.objects.filter(
|
||||
ConversationUUID=conv.ConversationUUID, MemberRole=ROLE_ADMIN).count()
|
||||
if admin_count >= MAX_GROUP_ADMINS:
|
||||
return False, f'每个群最多设置 {MAX_GROUP_ADMINS} 个管理员', None
|
||||
if target_member.MemberRole != role:
|
||||
target_member.MemberRole = role
|
||||
target_member.save(update_fields=['MemberRole'])
|
||||
admin_count = TalkMember.objects.filter(
|
||||
ConversationUUID=conv.ConversationUUID, MemberRole=ROLE_ADMIN).count()
|
||||
name_map = TalkManager._UserNames({target})
|
||||
return True, '', {
|
||||
'ConversationUUID': _ToStr(conv.ConversationUUID),
|
||||
'UserUUID': _ToStr(target),
|
||||
'UserName': name_map.get(target, ''),
|
||||
'MemberRole': role,
|
||||
'AdminCount': admin_count,
|
||||
'MaxAdmins': MAX_GROUP_ADMINS,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def DissolveConversation(ConversationUUID, OperatorUUID, IsTalkAdmin=False):
|
||||
"""解散会话(仅群主或 Talk 管理员)"""
|
||||
@@ -751,6 +819,15 @@ class TalkManager:
|
||||
existing = {_ToBytes(e) for e in existing}
|
||||
return sum(1 for u in uuids if u not in existing)
|
||||
|
||||
@staticmethod
|
||||
def _IsGroupManager(Conversation, Member, OperatorBytes):
|
||||
"""群主或群管理员判定(创建人视为群主,兼容历史数据的角色缺失)"""
|
||||
if Member is None:
|
||||
return False
|
||||
if Member.MemberRole in (ROLE_OWNER, ROLE_ADMIN):
|
||||
return True
|
||||
return _ToBytes(Conversation.CreatorUUID) == OperatorBytes
|
||||
|
||||
@staticmethod
|
||||
def _IsCompanyUser(CompanyUUID, UserUUIDBytes):
|
||||
"""判断用户是否属于该公司"""
|
||||
@@ -803,26 +880,43 @@ class TalkManager:
|
||||
|
||||
@staticmethod
|
||||
def _ConversationDict(Conversation, CurrentUserBytes, MembersMap, NameMap):
|
||||
"""会话 → 视图 dict(单聊标题空则用对方展示名)"""
|
||||
"""会话 → 视图 dict(单聊标题空则用对方展示名)
|
||||
|
||||
MembersMap 的值为 [(UserUUID, MemberRole), ...],据此回填成员角色与本人在群内的
|
||||
角色判定(IsOwner / IsGroupAdmin / CanManage),前端据此显示群主/管理员标识与入口。
|
||||
"""
|
||||
conv_key = _ToStr(Conversation.ConversationUUID)
|
||||
member_uuids = MembersMap.get(conv_key, [])
|
||||
member_rows = list(MembersMap.get(conv_key, []))
|
||||
members = []
|
||||
for uid in member_uuids:
|
||||
for uid, role in member_rows:
|
||||
ub = _ToBytes(uid)
|
||||
name = NameMap.get(ub, '')
|
||||
members.append({
|
||||
'UserUUID': _ToStr(ub),
|
||||
'UserName': name,
|
||||
'DisplayName': name,
|
||||
'MemberRole': role,
|
||||
})
|
||||
title = Conversation.Title or ''
|
||||
if not title and Conversation.ConversationType == TYPE_SINGLE:
|
||||
for uid in member_uuids:
|
||||
for uid, _role in member_rows:
|
||||
ub = _ToBytes(uid)
|
||||
if ub != CurrentUserBytes:
|
||||
title = NameMap.get(ub, '')
|
||||
break
|
||||
# 本人成员行(已读位点与角色一并取自该行)
|
||||
member = TalkManager.GetMember(Conversation.ConversationUUID, CurrentUserBytes)
|
||||
my_role = next(
|
||||
(role for uid, role in member_rows if _ToBytes(uid) == CurrentUserBytes),
|
||||
member.MemberRole if member is not None else ROLE_MEMBER,
|
||||
)
|
||||
is_owner = my_role == ROLE_OWNER
|
||||
is_group_admin = my_role == ROLE_ADMIN
|
||||
# 群主以成员表为准(历史数据 Role 缺失时回退创建人)
|
||||
owner_bytes = next(
|
||||
(_ToBytes(uid) for uid, role in member_rows if role == ROLE_OWNER), None)
|
||||
if owner_bytes is None and Conversation.ConversationType == TYPE_GROUP:
|
||||
owner_bytes = _ToBytes(Conversation.CreatorUUID)
|
||||
unread_qs = TalkMessage.objects.filter(ConversationUUID=Conversation.ConversationUUID)
|
||||
if member is not None and member.LastReadTime is not None:
|
||||
unread_qs = unread_qs.filter(CreateTime__gt=member.LastReadTime)
|
||||
@@ -838,8 +932,14 @@ class TalkManager:
|
||||
'UnreadCount': unread_qs.count(),
|
||||
'Members': members,
|
||||
# 群主标记:前端据此对「非管理员群主」显示解散入口(管理员另有 Entry.IsAdmin)
|
||||
'IsOwner': bool(member is not None and member.MemberRole == ROLE_OWNER),
|
||||
'OwnerUUID': _ToStr(Conversation.CreatorUUID),
|
||||
'IsOwner': bool(is_owner),
|
||||
'OwnerUUID': _ToStr(owner_bytes),
|
||||
# 群内角色:本人是否为该群管理员、本人角色值、管理员数量(用于 x/10 展示)
|
||||
'IsGroupAdmin': bool(is_group_admin),
|
||||
'MyRole': my_role,
|
||||
'AdminCount': sum(1 for _uid, role in member_rows if role == ROLE_ADMIN),
|
||||
# 可管理群信息(改群名等):群主或群管理员
|
||||
'CanManage': bool(is_owner or is_group_admin),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -52,8 +52,8 @@ class TalkMember(QModel):
|
||||
ConversationUUID = UUIDField(db_index=True, db_column='ConversationUUID', verbose_name='会话UUID')
|
||||
CompanyUUID = UUIDField(db_index=True, db_column='CompanyUUID', verbose_name='公司UUID')
|
||||
UserUUID = UUIDField(db_index=True, db_column='UserUUID', verbose_name='用户UUID')
|
||||
# 成员角色:0=成员 1=群主
|
||||
MemberRole = models.SmallIntegerField(default=0, db_column='MemberRole', verbose_name='成员角色(0成员/1群主)')
|
||||
# 成员角色:0=成员 1=群主 2=管理员(管理员上限见 manager.MAX_GROUP_ADMINS)
|
||||
MemberRole = models.SmallIntegerField(default=0, db_column='MemberRole', verbose_name='成员角色(0成员/1群主/2管理员)')
|
||||
JoinTime = models.DateTimeField(default=datetime.datetime.utcnow, db_column='JoinTime', verbose_name='加入时间')
|
||||
LastReadTime = models.DateTimeField(null=True, blank=True, db_column='LastReadTime', verbose_name='最后已读时间')
|
||||
LastReadMessageUUID = UUIDField(null=True, blank=True, db_column='LastReadMessageUUID', verbose_name='最后已读消息UUID')
|
||||
|
||||
@@ -9,6 +9,7 @@ urlpatterns = [
|
||||
path('Conversation/Invite', views.ConversationInviteView.as_view()),
|
||||
path('Conversation/Leave', views.ConversationLeaveView.as_view()),
|
||||
path('Conversation/Rename', views.ConversationRenameView.as_view()),
|
||||
path('Conversation/Member/Role', views.ConversationMemberRoleView.as_view()),
|
||||
path('Conversation/Dissolve', views.ConversationDissolveView.as_view()),
|
||||
path('Messages', views.MessageListView.as_view()),
|
||||
path('Send', views.SendView.as_view()),
|
||||
|
||||
@@ -6,7 +6,8 @@ REST 接口(统一挂在 API/Talk/ 前缀下):
|
||||
POST Conversation/Create 创建单聊/群聊
|
||||
POST Conversation/Invite 邀请成员
|
||||
POST Conversation/Leave 退出会话
|
||||
POST Conversation/Rename 重命名会话
|
||||
POST Conversation/Rename 重命名会话(群聊仅群主/管理员)
|
||||
POST Conversation/Member/Role 设置/取消群管理员(仅群主或 Talk 管理员,单群上限 10)
|
||||
POST Conversation/Dissolve 解散会话
|
||||
GET Messages 拉取消息(升序,BeforeTime 向上翻页)
|
||||
POST Send 发送消息(落库后广播)
|
||||
@@ -32,7 +33,7 @@ from rest_framework.views import APIView
|
||||
from gvsdsdk.auth.admin_service import IsAdmin, IsCompanyAdmin
|
||||
from gvsdsdk.model_utils import UUIDObj
|
||||
|
||||
from .manager import CheckTalkAccess, TalkManager
|
||||
from .manager import CheckTalkAccess, TalkManager, MAX_GROUP_ADMINS
|
||||
from .permissions import IsTalkUser
|
||||
|
||||
# 附件上传大小上限:10MB
|
||||
@@ -136,6 +137,7 @@ class EntryView(TalkAPIView):
|
||||
return Response({'code': 0, 'data': {
|
||||
'CanUse': CheckTalkAccess(request),
|
||||
'IsAdmin': is_admin,
|
||||
'GroupAdminLimit': MAX_GROUP_ADMINS,
|
||||
'CompanyUUID': _ToStr(company),
|
||||
'Quotas': {
|
||||
'GroupCompanyLimit': limits['TalkGroupCompanyLimit'],
|
||||
@@ -212,7 +214,7 @@ class ConversationLeaveView(TalkAPIView):
|
||||
|
||||
|
||||
class ConversationRenameView(TalkAPIView):
|
||||
"""POST API/Talk/Conversation/Rename —— 重命名会话"""
|
||||
"""POST API/Talk/Conversation/Rename —— 重命名会话(群聊仅群主/管理员)"""
|
||||
|
||||
def post(self, request):
|
||||
user = _UserBytes(request)
|
||||
@@ -220,7 +222,32 @@ class ConversationRenameView(TalkAPIView):
|
||||
title = request.data.get('Title', '')
|
||||
if not conv_uuid:
|
||||
return Response({'code': 1, 'msg': '缺少 ConversationUUID'}, status=400)
|
||||
ok, msg, data = TalkManager.RenameConversation(conv_uuid, user, title)
|
||||
is_admin = IsAdmin(request=request) or IsCompanyAdmin(request=request)
|
||||
ok, msg, data = TalkManager.RenameConversation(conv_uuid, user, title, IsTalkAdmin=is_admin)
|
||||
if not ok:
|
||||
return Response({'code': 1, 'msg': msg}, status=400)
|
||||
return Response({'code': 0, 'data': data})
|
||||
|
||||
|
||||
class ConversationMemberRoleView(TalkAPIView):
|
||||
"""POST API/Talk/Conversation/Member/Role —— 设置/取消群管理员
|
||||
|
||||
请求体:{ConversationUUID, UserUUID, Role}(Role:0=成员 2=管理员)
|
||||
仅群主或 Talk 管理员可操作;每个群最多 manager.MAX_GROUP_ADMINS 个管理员。
|
||||
"""
|
||||
|
||||
def post(self, request):
|
||||
user = _UserBytes(request)
|
||||
conv_uuid = request.data.get('ConversationUUID')
|
||||
target_uuid = request.data.get('UserUUID')
|
||||
role = request.data.get('Role')
|
||||
if not conv_uuid:
|
||||
return Response({'code': 1, 'msg': '缺少 ConversationUUID'}, status=400)
|
||||
if not target_uuid:
|
||||
return Response({'code': 1, 'msg': '缺少 UserUUID'}, status=400)
|
||||
is_admin = IsAdmin(request=request) or IsCompanyAdmin(request=request)
|
||||
ok, msg, data = TalkManager.SetMemberRole(
|
||||
conv_uuid, user, target_uuid, role, IsTalkAdmin=is_admin)
|
||||
if not ok:
|
||||
return Response({'code': 1, 'msg': msg}, status=400)
|
||||
return Response({'code': 0, 'data': data})
|
||||
|
||||
Reference in new issue
Block a user