diff --git a/gvsds/apps/gerp/apps/talk/manager.py b/gvsds/apps/gerp/apps/talk/manager.py index 1634108a..f679fe31 100644 --- a/gvsds/apps/gerp/apps/talk/manager.py +++ b/gvsds/apps/gerp/apps/talk/manager.py @@ -209,7 +209,7 @@ def BroadcastConversation(ConversationUUID, Action, Extra=None): """广播「会话级变更」事件(事件名 talk:conversation) 用于群主/管理员操作后让所有在线成员的会话列表立即同步: - 改名(renamed)、群公告(announcement)、转让群主(owner_transferred)、 + 改名(renamed)、群公告(announcement)、群头像(avatar)、转让群主(owner_transferred)、 成员角色变更(role_changed)、移出成员(members_removed)、解散(dissolved)。 事件名不带命名空间前缀,前端订阅的是 'talk:conversation'。 """ @@ -791,6 +791,46 @@ class TalkManager: 'AnnouncementTime': conv.AnnouncementTime.isoformat() if conv.AnnouncementTime else None, } + @staticmethod + def SetConversationAvatar(ConversationUUID, OperatorUUID, AvatarSHA1, IsTalkAdmin=False): + """设置群头像(群主/群管理员/Talk 管理员;传空串即恢复默认九宫格) + + AvatarSHA1 为空串表示恢复默认(成员九宫格);非空必须是 40 位十六进制 + (由 /API/Talk/Upload 上传图片得到,指向 FileAsset)。 + """ + conv = TalkManager.GetConversation(ConversationUUID) + operator = _ToBytes(OperatorUUID) + if conv is None or conv.Status != STATUS_NORMAL: + return False, '会话不存在或已解散', None + if conv.ConversationType != TYPE_GROUP: + return False, '仅群聊可设置群头像', None + member = TalkManager.GetMember(conv.ConversationUUID, operator) + if member is None: + return False, '您不是该会话成员', None + if not TalkManager._IsGroupManager(conv, member, operator) and not IsTalkAdmin: + return False, '仅群主或管理员可修改群头像', None + sha1 = (AvatarSHA1 or '').strip().lower() + if sha1 and (len(sha1) != 40 or any(c not in '0123456789abcdef' for c in sha1)): + return False, '无效的头像资源标识', None + if sha1 == (conv.AvatarSHA1 or ''): + return True, '', { + 'ConversationUUID': _ToStr(conv.ConversationUUID), + 'AvatarSHA1': conv.AvatarSHA1 or '', + } + conv.AvatarSHA1 = sha1 + conv.save(update_fields=['AvatarSHA1']) + operator_name = TalkManager._DisplayName(operator) + TalkManager.AppendSystemMessage( + conv, + f'{operator_name} 更换了群头像' if sha1 else f'{operator_name} 恢复了默认群头像', + operator, + ) + BroadcastConversation(conv.ConversationUUID, 'avatar', {'AvatarSHA1': sha1}) + return True, '', { + 'ConversationUUID': _ToStr(conv.ConversationUUID), + 'AvatarSHA1': sha1, + } + @staticmethod def DissolveConversation(ConversationUUID, OperatorUUID, IsTalkAdmin=False): """解散会话(仅群主或 Talk 管理员)""" @@ -1163,6 +1203,8 @@ class TalkManager: 'LastMessagePreview': Conversation.LastMessagePreview or '', 'Announcement': Conversation.Announcement or '', 'AnnouncementTime': Conversation.AnnouncementTime.isoformat() if Conversation.AnnouncementTime else None, + # 群头像 SHA1(空串=使用成员九宫格默认头像;前端据此拼资源 URL) + 'AvatarSHA1': Conversation.AvatarSHA1 or '', 'UnreadCount': unread_qs.count(), 'Members': members, # 群主标记:前端据此对「非管理员群主」显示解散入口(管理员另有 Entry.IsAdmin) diff --git a/gvsds/apps/gerp/apps/talk/models.py b/gvsds/apps/gerp/apps/talk/models.py index 88602698..c046bc02 100644 --- a/gvsds/apps/gerp/apps/talk/models.py +++ b/gvsds/apps/gerp/apps/talk/models.py @@ -33,6 +33,8 @@ class TalkConversation(QModel): # 群公告(仅群聊使用;群主/群管理员可编辑,成员在会话顶部可见) Announcement = models.TextField(default='', db_column='Announcement', verbose_name='群公告') AnnouncementTime = models.DateTimeField(null=True, blank=True, db_column='AnnouncementTime', verbose_name='群公告更新时间') + # 群头像(仅群聊使用;存 FileAsset 的 SHA1,空串表示使用成员九宫格默认头像) + AvatarSHA1 = models.CharField(max_length=40, default='', db_column='AvatarSHA1', verbose_name='群头像SHA1') # 会话状态:1=正常 0=已解散 Status = models.SmallIntegerField(default=1, db_column='Status', verbose_name='状态(1正常/0解散)') CreateTime = models.DateTimeField(default=datetime.datetime.utcnow, db_column='CreateTime', verbose_name='创建时间') diff --git a/gvsds/apps/gerp/apps/talk/urls.py b/gvsds/apps/gerp/apps/talk/urls.py index bf28cf46..f0081bd8 100644 --- a/gvsds/apps/gerp/apps/talk/urls.py +++ b/gvsds/apps/gerp/apps/talk/urls.py @@ -13,6 +13,7 @@ urlpatterns = [ path('Conversation/Member/Remove', views.ConversationMemberRemoveView.as_view()), path('Conversation/Transfer', views.ConversationTransferView.as_view()), path('Conversation/Announcement', views.ConversationAnnouncementView.as_view()), + path('Conversation/Avatar', views.ConversationAvatarView.as_view()), path('Conversation/Dissolve', views.ConversationDissolveView.as_view()), path('Messages', views.MessageListView.as_view()), path('Send', views.SendView.as_view()), diff --git a/gvsds/apps/gerp/apps/talk/views.py b/gvsds/apps/gerp/apps/talk/views.py index ca6bbe1e..fc2208ce 100644 --- a/gvsds/apps/gerp/apps/talk/views.py +++ b/gvsds/apps/gerp/apps/talk/views.py @@ -316,6 +316,27 @@ class ConversationAnnouncementView(TalkAPIView): return Response({'code': 0, 'data': data}) +class ConversationAvatarView(TalkAPIView): + """POST API/Talk/Conversation/Avatar —— 设置群头像(空串即恢复默认九宫格) + + 请求体:{ConversationUUID, AvatarSHA1} + AvatarSHA1 由 /API/Talk/Upload 上传图片得到(指向 FileAsset)。 + """ + + def post(self, request): + user = _UserBytes(request) + conv_uuid = request.data.get('ConversationUUID') + avatar_sha1 = request.data.get('AvatarSHA1', '') + if not conv_uuid: + return Response({'code': 1, 'msg': '缺少 ConversationUUID'}, status=400) + is_admin = IsAdmin(request=request) or IsCompanyAdmin(request=request) + ok, msg, data = TalkManager.SetConversationAvatar( + conv_uuid, user, avatar_sha1, IsTalkAdmin=is_admin) + if not ok: + return Response({'code': 1, 'msg': msg}, status=400) + return Response({'code': 0, 'data': data}) + + class ConversationDissolveView(TalkAPIView): """POST API/Talk/Conversation/Dissolve —— 解散会话(仅群主或 Talk 管理员)""" diff --git a/gvsds/apps/gerp/migrations/0057_talk_conversation_avatar.py b/gvsds/apps/gerp/migrations/0057_talk_conversation_avatar.py new file mode 100644 index 00000000..1b8b4a36 --- /dev/null +++ b/gvsds/apps/gerp/migrations/0057_talk_conversation_avatar.py @@ -0,0 +1,19 @@ +"""Talk 群头像(TalkConversation.AvatarSHA1) +""" +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('gerp', '0056_talk_conversation_announcement'), + ] + + operations = [ + migrations.AddField( + model_name='talkconversation', + name='AvatarSHA1', + field=models.CharField( + default='', max_length=40, db_column='AvatarSHA1', verbose_name='群头像SHA1'), + ), + ]