diff --git a/docs/IM-前后端对接文档.md b/docs/IM-前后端对接文档.md index da9c411..751c507 100644 --- a/docs/IM-前后端对接文档.md +++ b/docs/IM-前后端对接文档.md @@ -1354,6 +1354,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key> **状态:新增。** - 调用 4.19 接口,`mode:"bundle"`;服务端生成一条 `forward_bundle` 目标消息,内容含 `bundle_id,title,item_count,preview[],items[],source_refs[]`。首版 Bundle 明细以内联快照随消息下发,后续如消息量增大再扩展 `GET /v1/im/forward-bundles/{bundle_id}?cursor=` 分页详情。 +- `items[]` 和 `preview[]` 保留 `from` 原发送人 ID,并新增 `from_user` 用户摘要:`{"user_id":"usr...","display_name":"张三","avatar_file_id":"file_avatar"}`。头像为空时可缺省;若资料查询失败,服务端至少返回 `user_id/display_name` 兜底。 - 快照保留显示名、时间和内容摘要,对撤回/敏感字段按策略脱敏;不得允许接收端利用 Bundle 获取原会话额外权限。 - 多目标共享不可变 Bundle 内容但分别生成目标消息与授权记录。 diff --git a/server/im_http.go b/server/im_http.go index 28bd1ef..baeb314 100644 --- a/server/im_http.go +++ b/server/im_http.go @@ -9380,10 +9380,10 @@ func buildForwardBundle(principal *imPrincipal, sources []struct { Topic string `json:"topic"` Seq imForwardSeq `json:"seq"` }, target string, sourceReadable map[string]error, comment string) (map[string]any, []map[string]any) { - items := make([]map[string]any, 0, len(sources)) - preview := make([]map[string]any, 0, min(len(sources), 3)) - sourceRefs := make([]map[string]any, 0, len(sources)) failures := make([]map[string]any, 0) + sourceMessages := make([]*types.Message, 0, len(sources)) + sourceTopics := make([]string, 0, len(sources)) + sourceSeqs := make([]int64, 0, len(sources)) for _, source := range sources { sourceTopic, sourceOK := normalizeIMForwardTopic(principal.Uid, source.Topic) if sourceTopic == "" || int64(source.Seq) <= 0 || !sourceOK { @@ -9412,22 +9412,38 @@ func buildForwardBundle(principal *imPrincipal, sources []struct { failures = append(failures, forwardFailurePayload(sourceTopic, int64(source.Seq), target, "MESSAGE_UNAVAILABLE")) continue } + sourceMessages = append(sourceMessages, msg) + sourceTopics = append(sourceTopics, sourceTopic) + sourceSeqs = append(sourceSeqs, int64(source.Seq)) + } + if len(failures) > 0 { + return nil, failures + } + userSummaries := forwardMessageUserSummaries(principal.TenantID, sourceMessages) + items := make([]map[string]any, 0, len(sourceMessages)) + preview := make([]map[string]any, 0, min(len(sourceMessages), 3)) + sourceRefs := make([]map[string]any, 0, len(sourceMessages)) + for i, msg := range sourceMessages { snapshot := favoriteSnapshot(msg) + if fromUser := forwardMessageUserSummary(msg.From, userSummaries); fromUser != nil { + snapshot["from_user"] = fromUser + } items = append(items, snapshot) - ref := map[string]any{"topic": sourceTopic, "seq": int64(source.Seq)} + ref := map[string]any{"topic": sourceTopics[i], "seq": sourceSeqs[i]} sourceRefs = append(sourceRefs, ref) if len(preview) < 3 { - preview = append(preview, map[string]any{ + previewItem := map[string]any{ "from": msg.From, "type": imMessageType(msg), "summary": textSnippet(messageText(*msg), ""), "ts": msg.CreatedAt.UTC().Format(time.RFC3339Nano), - }) + } + if fromUser := forwardMessageUserSummary(msg.From, userSummaries); fromUser != nil { + previewItem["from_user"] = fromUser + } + preview = append(preview, previewItem) } } - if len(failures) > 0 { - return nil, failures - } bundleID := "fbundle_" + store.Store.GetUidString() return map[string]any{ "bundle_id": bundleID, @@ -9441,6 +9457,58 @@ func buildForwardBundle(principal *imPrincipal, sources []struct { }, nil } +func forwardMessageUserSummaries(tenantID types.TenantID, messages []*types.Message) map[types.Uid]map[string]any { + if store.Users == nil || len(messages) == 0 { + return nil + } + seen := make(map[types.Uid]bool) + uids := make([]types.Uid, 0, len(messages)) + for _, msg := range messages { + if msg == nil { + continue + } + uid := types.ParseUid(msg.From) + if uid.IsZero() || seen[uid] { + continue + } + seen[uid] = true + uids = append(uids, uid) + } + if len(uids) == 0 { + return nil + } + users, err := store.Users.GetAll(tenantID, uids...) + if err != nil { + logs.Warn.Printf("im http: failed to load forward user summaries: %v", err) + return nil + } + summaries := make(map[types.Uid]map[string]any, len(users)) + for i := range users { + uid := users[i].Uid() + if uid.IsZero() { + uid = types.ParseUid(users[i].Id) + } + if !uid.IsZero() { + summaries[uid] = userSummaryPayload(&users[i], "") + } + } + return summaries +} + +func forwardMessageUserSummary(from string, summaries map[types.Uid]map[string]any) map[string]any { + uid := types.ParseUid(from) + if uid.IsZero() { + return nil + } + if summary, ok := summaries[uid]; ok && summary != nil { + return summary + } + return map[string]any{ + "user_id": uid.UserId(), + "display_name": uid.UserId(), + } +} + func prepareHTTPForwardMessage(tenantID types.TenantID, msg *types.Message) error { if msg == nil || msg.TenantID != tenantID || strings.TrimSpace(msg.Topic) == "" { return types.ErrMalformed diff --git a/server/im_http_test.go b/server/im_http_test.go index eee88d1..4f811ff 100644 --- a/server/im_http_test.go +++ b/server/im_http_test.go @@ -5362,13 +5362,17 @@ func TestServeIMMessageForwardBundleSavesForwardBundle(t *testing.T) { oldSubs := store.Subs oldMessages := store.Messages + oldUsers := store.Users subs := mock_store.NewMockSubsPersistenceInterface(ctrl) messages := mock_store.NewMockMessagesPersistenceInterface(ctrl) + users := mock_store.NewMockUsersPersistenceInterface(ctrl) store.Subs = subs store.Messages = messages + store.Users = users t.Cleanup(func() { store.Subs = oldSubs store.Messages = oldMessages + store.Users = oldUsers }) subs.EXPECT().Get(types.TenantID(7), "grpTarget", types.Uid(42), false).Return(&types.Subscription{ @@ -5396,6 +5400,11 @@ func TestServeIMMessageForwardBundleSavesForwardBundle(t *testing.T) { CreatedAt: time.Date(2026, 8, 13, 9, 0, 0, 0, time.UTC), }, }, nil) + users.EXPECT().GetAll(types.TenantID(7), types.Uid(99)).Return([]types.User{{ + ObjHeader: types.ObjHeader{Id: types.Uid(99).String()}, + TenantID: 7, + Public: types.KVMap{"display_name": "后端-potter", "avatar_file_id": "avatar-99"}, + }}, nil) messages.EXPECT().Save(types.TenantID(7), gomock.Any(), gomock.Nil(), true).DoAndReturn( func(_ types.TenantID, msg *types.Message, _ []string, _ bool) (*types.MessageSaveResult, error) { if msg.Topic != "grpTarget" || msg.Head["x-im-type"] != "forward_bundle" { @@ -5405,6 +5414,16 @@ func TestServeIMMessageForwardBundleSavesForwardBundle(t *testing.T) { if content["bundle_id"] != "fbundle_bundle-1" || content["item_count"] != 1 { t.Fatalf("unexpected bundle content: %#v", content) } + bundleItems := content["items"].([]map[string]any) + itemUser := bundleItems[0]["from_user"].(map[string]any) + if itemUser["user_id"] != types.Uid(99).UserId() || itemUser["display_name"] != "后端-potter" || itemUser["avatar_file_id"] != "avatar-99" { + t.Fatalf("unexpected item sender summary: %#v", itemUser) + } + previewItems := content["preview"].([]map[string]any) + previewUser := previewItems[0]["from_user"].(map[string]any) + if previewUser["user_id"] != types.Uid(99).UserId() || previewUser["display_name"] != "后端-potter" || previewUser["avatar_file_id"] != "avatar-99" { + t.Fatalf("unexpected preview sender summary: %#v", previewUser) + } return &types.MessageSaveResult{SeqId: 23, MarkedReadBySender: true}, nil })