feat(Talk): 群操作写系统消息并广播会话事件

新增 AppendSystemMessage:改群名/群公告/移出成员/转让群主/设取消管理员时写入 MsgType=3 系统消息,同时回填会话最后消息摘要,使不在会话内的成员通过未读提醒感知(群公告提醒即走此路径)。

新增 BroadcastConversation,向会话广播 talk:conversation 事件(renamed/announcement/role_changed/members_removed/owner_transferred/dissolved),供前端立即同步会话列表与成员。
This commit is contained in:
TermiNexus committed 2026-10-13 17:20:27 +08:00
1 parent f608a6f9c0
commit 5707ab0371
1 file changed
+100 -1
+100 -1
View File
@@ -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()