spark-server/internal/module/sync/logic/service.go
DESKTOP-L8OUL9F\gfl0034 e27ae8723a fix: 单聊系统消息带回当前用户视角的对方
SYNC_INBOX 补 conversation_type、peer_id、group_id 和 last_read_msg_id。历史消息在 sender 为 0 时用 peer_id 定位对方。已读清未读时保留会话 ZSET 分数,避免被毫秒时间戳冲到游标后面。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-19 15:04:03 +08:00

154 lines
4.2 KiB
Go

// Package logic is SYNC_INBOX. HTTP and TCP share it.
package logic
import (
"context"
conventity "spark-server/internal/module/conversation/entity"
inboxentity "spark-server/internal/module/inbox/entity"
inboxlogic "spark-server/internal/module/inbox/logic"
"spark-server/internal/module/sync/entity"
"spark-server/internal/pkg/errs"
)
const defaultInboxLimit = 200
type Mailbox interface {
Put(ctx context.Context, userID int64, eventType entity.EventType, conversationID int64, payload []byte) error
ListSince(ctx context.Context, userID int64, deviceID uint64, seq int64, limit int) ([]*entity.MailboxItem, error)
SaveCursor(ctx context.Context, userID int64, deviceID uint64, mailboxSeq, dialogScore int64) error
PurgeUser(ctx context.Context, userID int64) error
}
type InboxResponse struct {
Dialogs []inboxentity.Dialog `json:"dialogs"`
Mailbox []*entity.MailboxItem `json:"mailbox"`
DialogScore int64 `json:"dialog_score"`
MailboxSeq int64 `json:"mailbox_seq"`
HasMore bool `json:"has_more"`
}
// Service is SYNC_INBOX. HTTP and TCP share it.
// DialogFaces resolves viewer-relative peer / group / read cursor. Nil leaves those fields empty.
type DialogFaces interface {
DialogFaces(ctx context.Context, viewerID int64, conversationIDs []int64) (map[int64]conventity.DialogFace, error)
}
type Service struct {
inbox inboxlogic.Store
box Mailbox
faces DialogFaces
}
func NewService(inbox inboxlogic.Store, box Mailbox) *Service {
return &Service{inbox: inbox, box: box}
}
func (s *Service) BindFaces(faces DialogFaces) {
if s == nil {
return
}
s.faces = faces
}
func (s *Service) ready() bool {
return s != nil && s.inbox != nil
}
func (s *Service) SyncInbox(ctx context.Context, req entity.InboxRequest) (*InboxResponse, error) {
if !s.ready() || req.UserID <= 0 {
return nil, errs.ErrUnauthorized
}
if req.MailboxSeq < 0 || req.DialogScore < 0 {
return nil, errs.ErrInvalidArgument
}
limit := req.Limit
if limit <= 0 || limit > defaultInboxLimit {
limit = defaultInboxLimit
}
hits, err := s.inbox.ListUpdatedSince(ctx, req.UserID, req.DialogScore, limit+1)
if err != nil {
return nil, err
}
hasMore := len(hits) > limit
if hasMore {
hits = hits[:limit]
}
dialogs, err := s.inbox.LoadDialogs(ctx, req.UserID, hits)
if err != nil {
return nil, err
}
if dialogs == nil {
dialogs = []inboxentity.Dialog{}
}
if err := s.attachFaces(ctx, req.UserID, dialogs); err != nil {
return nil, err
}
nextScore := maxScore(req.DialogScore, dialogs)
mailbox, nextSeq, err := s.loadMailbox(ctx, req, limit)
if err != nil {
return nil, err
}
if s.box != nil {
_ = s.box.SaveCursor(ctx, req.UserID, req.DeviceID, nextSeq, nextScore)
}
return &InboxResponse{
Dialogs: dialogs, Mailbox: mailbox,
DialogScore: nextScore, MailboxSeq: nextSeq, HasMore: hasMore,
}, nil
}
func (s *Service) loadMailbox(ctx context.Context, req entity.InboxRequest, limit int) ([]*entity.MailboxItem, int64, error) {
nextSeq := req.MailboxSeq
if s.box == nil {
return []*entity.MailboxItem{}, nextSeq, nil
}
mailbox, err := s.box.ListSince(ctx, req.UserID, req.DeviceID, req.MailboxSeq, limit)
if err != nil {
return nil, 0, err
}
if mailbox == nil {
mailbox = []*entity.MailboxItem{}
}
for _, item := range mailbox {
if item != nil && item.Seq > nextSeq {
nextSeq = item.Seq
}
}
return mailbox, nextSeq, nil
}
func (s *Service) attachFaces(ctx context.Context, viewerID int64, dialogs []inboxentity.Dialog) error {
if s == nil || s.faces == nil || len(dialogs) == 0 {
return nil
}
ids := make([]int64, len(dialogs))
for i := range dialogs {
ids[i] = dialogs[i].ConversationID
}
faces, err := s.faces.DialogFaces(ctx, viewerID, ids)
if err != nil {
return err
}
for i := range dialogs {
face, ok := faces[dialogs[i].ConversationID]
if !ok {
continue
}
dialogs[i].ConversationType = int(face.Type)
dialogs[i].PeerID = face.PeerID
dialogs[i].GroupID = face.GroupID
dialogs[i].LastReadMsgID = face.LastReadMsgID
}
return nil
}
func maxScore(cur int64, dialogs []inboxentity.Dialog) int64 {
for i := range dialogs {
if dialogs[i].Score > cur {
cur = dialogs[i].Score
}
}
return cur
}