From 5707ab037126220ff9b6ef5cab60dd76cf4a0fa0 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Tue, 13 Oct 2026 17:20:27 +0800 Subject: [PATCH] =?UTF-8?q?feat(Talk):=20=E7=BE=A4=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E5=86=99=E7=B3=BB=E7=BB=9F=E6=B6=88=E6=81=AF=E5=B9=B6=E5=B9=BF?= =?UTF-8?q?=E6=92=AD=E4=BC=9A=E8=AF=9D=E4=BA=8B=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 AppendSystemMessage:改群名/群公告/移出成员/转让群主/设取消管理员时写入 MsgType=3 系统消息,同时回填会话最后消息摘要,使不在会话内的成员通过未读提醒感知(群公告提醒即走此路径)。 新增 BroadcastConversation,向会话广播 talk:conversation 事件(renamed/announcement/role_changed/members_removed/owner_transferred/dissolved),供前端立即同步会话列表与成员。 --- gvsds/apps/gerp/apps/talk/manager.py | 101 ++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 1 deletion(-) diff --git a/gvsds/apps/gerp/apps/talk/manager.py b/gvsds/apps/gerp/apps/talk/manager.py index 8809ad61..1634108a 100644 --- a/gvsds/apps/gerp/apps/talk/manager.py +++ b/gvsds/apps/gerp/apps/talk/manager.py @@ -4,7 +4,8 @@ - 会话/成员/消息的增删改查与序列化 - 4 项配额读取与用量统计(AppConfig 可配置,公司级 → 租户默认 → manifest 默认) - Talk 准入判定 CheckTalkAccess(供本 app 与 socket 模块共用) - - 消息广播 BroadcastMessage / BroadcastRead(调用 gvsdsdk.talk_socket) + - 消息广播 BroadcastMessage / BroadcastRead / BroadcastConversation + (调用 gvsdsdk.talk_socket;后者用于群主/管理员操作后的会话级同步) 所有方法名遵循帕斯卡命名法;UUID 一律通过 UUIDObj(x).bytes 传递。 """ @@ -204,6 +205,28 @@ def BroadcastRead(ConversationUUID, UserUUID, LastReadMessageUUID): logger.warning('Talk 已读广播失败', exc_info=True) +def BroadcastConversation(ConversationUUID, Action, Extra=None): + """广播「会话级变更」事件(事件名 talk:conversation) + + 用于群主/管理员操作后让所有在线成员的会话列表立即同步: + 改名(renamed)、群公告(announcement)、转让群主(owner_transferred)、 + 成员角色变更(role_changed)、移出成员(members_removed)、解散(dissolved)。 + 事件名不带命名空间前缀,前端订阅的是 'talk:conversation'。 + """ + try: + from gvsdsdk.talk_socket import BroadcastToConversation + except ImportError: + logger.debug('gvsdsdk.talk_socket 不存在,跳过 Talk 会话事件广播') + return + data = {'ConversationUUID': _ToStr(ConversationUUID), 'Action': Action} + if Extra: + data.update(Extra) + try: + BroadcastToConversation(ConversationUUID, 'talk:conversation', data) + except Exception: + logger.warning('Talk 会话事件广播失败', exc_info=True) + + # --------------------------------------------------------------------------- # 模块级便捷入口(gvsdsdk.talk_socket 按这些签名调用,勿改签名) # --------------------------------------------------------------------------- @@ -565,6 +588,9 @@ class TalkManager: return False, '仅群主或管理员可修改群名', None conv.Title = (Title or '').strip()[:128] conv.save(update_fields=['Title']) + TalkManager.AppendSystemMessage( + conv, f'{TalkManager._DisplayName(operator)} 修改群名为「{conv.Title}」', operator) + BroadcastConversation(conv.ConversationUUID, 'renamed', {'Title': conv.Title}) return True, '', {'ConversationUUID': _ToStr(conv.ConversationUUID), 'Title': conv.Title} @staticmethod @@ -615,6 +641,14 @@ class TalkManager: admin_count = TalkMember.objects.filter( ConversationUUID=conv.ConversationUUID, MemberRole=ROLE_ADMIN).count() name_map = TalkManager._UserNames({target}) + role_text = '管理员' if role == ROLE_ADMIN else '普通成员' + TalkManager.AppendSystemMessage( + conv, + f'{TalkManager._DisplayName(operator)} 将 {name_map.get(target, "") or "某成员"} 设为{role_text}', + operator, + ) + BroadcastConversation(conv.ConversationUUID, 'role_changed', + {'UserUUID': _ToStr(target), 'MemberRole': role}) return True, '', { 'ConversationUUID': _ToStr(conv.ConversationUUID), 'UserUUID': _ToStr(target), @@ -664,7 +698,14 @@ class TalkManager: removed.append(_ToStr(uid)) if not removed: return False, '没有可移出的成员', None + removed_bytes = [_ToBytes(u) for u in removed] + removed_names = TalkManager._UserNames(set(removed_bytes)) + names = '、'.join(removed_names.get(b, '') or '某成员' for b in removed_bytes) + TalkManager.AppendSystemMessage( + conv, f'{TalkManager._DisplayName(operator)} 将 {names} 移出群聊', operator) TalkManager._SyncMemberCount(conv) + BroadcastConversation(conv.ConversationUUID, 'members_removed', + {'Removed': removed, 'MemberCount': conv.MemberCount}) if conv.MemberCount <= 0: conv.Status = STATUS_DISSOLVED conv.save(update_fields=['Status']) @@ -705,6 +746,13 @@ class TalkManager: operator_member.MemberRole = ROLE_ADMIN operator_member.save(update_fields=['MemberRole']) name_map = TalkManager._UserNames({target}) + TalkManager.AppendSystemMessage( + conv, + f'{TalkManager._DisplayName(operator)} 将群主转让给 {name_map.get(target, "") or "某成员"}', + operator, + ) + BroadcastConversation(conv.ConversationUUID, 'owner_transferred', + {'OwnerUUID': _ToStr(target)}) return True, '', { 'ConversationUUID': _ToStr(conv.ConversationUUID), 'OwnerUUID': _ToStr(target), @@ -729,6 +777,14 @@ class TalkManager: conv.Announcement = text[:2000] conv.AnnouncementTime = TalkManager._Now() if text else None conv.save(update_fields=['Announcement', 'AnnouncementTime']) + operator_name = TalkManager._DisplayName(operator) + TalkManager.AppendSystemMessage( + conv, + f'{operator_name} 更新了群公告' if conv.Announcement else f'{operator_name} 清除了群公告', + operator, + ) + BroadcastConversation(conv.ConversationUUID, 'announcement', + {'Announcement': conv.Announcement}) return True, '', { 'ConversationUUID': _ToStr(conv.ConversationUUID), 'Announcement': conv.Announcement, @@ -747,6 +803,7 @@ class TalkManager: return False, '仅群主或 Talk 管理员可解散该会话', None conv.Status = STATUS_DISSOLVED conv.save(update_fields=['Status']) + BroadcastConversation(conv.ConversationUUID, 'dissolved') return True, '', {'ConversationUUID': _ToStr(conv.ConversationUUID)} # ==================== 消息 ==================== @@ -884,6 +941,48 @@ class TalkManager: # ==================== 内部辅助 ==================== + @staticmethod + def AppendSystemMessage(Conversation, Text, OperatorUUID=None): + """写入一条系统消息(MsgType=3)并广播 + + 群主/管理员操作留痕(改群名、群公告、移出成员、转让群主、设/取消管理员)。 + 同时回填会话最后消息摘要,使不在会话内的成员通过未读提醒感知(群公告提醒即走此路径)。 + + Returns: + dict|None:消息视图 dict(Text 为空时返回 None) + """ + text = (Text or '').strip() + if not text: + return None + sender = _ToBytes(OperatorUUID) or _ToBytes(Conversation.CreatorUUID) + if sender is None: + return None + now = TalkManager._Now() + msg = TalkMessage.objects.create( + MessageUUID=uuid.uuid4().bytes, + ConversationUUID=Conversation.ConversationUUID, + CompanyUUID=Conversation.CompanyUUID, + SenderUUID=sender, + MsgType=MSG_SYSTEM, + Content=text, + CreateTime=now, + ) + Conversation.LastMessageUUID = msg.MessageUUID + Conversation.LastMessageTime = now + Conversation.LastMessagePreview = text[:256] + Conversation.save(update_fields=[ + 'LastMessageUUID', 'LastMessageTime', 'LastMessagePreview']) + message_dict = TalkManager._MessageDict(msg, TalkManager._UserNames({sender})) + BroadcastMessage(Conversation.ConversationUUID, message_dict) + return message_dict + + @staticmethod + def _DisplayName(UserUUIDBytes): + """操作人显示名(取不到时回退「某成员」,保证系统消息可读)""" + if not UserUUIDBytes: + return '系统' + return TalkManager._UserNames({UserUUIDBytes}).get(UserUUIDBytes, '') or '某成员' + @staticmethod def _Now(): return TalkConversation._meta.get_field('CreateTime').default()