spark-server/internal/module/message/logic/read.go
DESKTOP-L8OUL9F\gfl0034 d293de34f5 实现 TCP READ(命令 8),已读水位前进时清未读并通知其他设备。
补上会员价格按平台筛选,并修正广场机器人测试的 Feed 调用,使 go test 能通过。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-16 18:22:08 +08:00

119 lines
3.3 KiB
Go

package logic
import (
"context"
"errors"
conventity "spark-server/internal/module/conversation/entity"
deliverentity "spark-server/internal/module/deliver/entity"
"spark-server/internal/pkg/errs"
)
const cmdRead uint16 = 8
// ReadInput is TCP command 8. msg_id is the message the member has read.
type ReadInput struct {
ConversationID int64 `json:"conversation_id"`
MsgID int64 `json:"msg_id"`
}
// ReadResult is the stored cursor. msg_id is last_read_msg_id and never moves backward.
type ReadResult struct {
ConversationID int64 `json:"conversation_id"`
MsgID int64 `json:"msg_id"`
}
func (s *Service) MarkRead(ctx context.Context, userID int64, deviceID uint64, in *ReadInput) (*ReadResult, error) {
if !s.ready() || userID <= 0 {
return nil, errs.ErrUnauthorized
}
if in == nil || in.ConversationID <= 0 || in.MsgID <= 0 {
return nil, errs.ErrInvalidArgument
}
mem, err := s.readMember(ctx, in.ConversationID, userID)
if err != nil {
return nil, err
}
if err := s.readTarget(ctx, in); err != nil {
return nil, err
}
cursor, advanced, err := s.advanceRead(ctx, mem, in)
if err != nil {
return nil, err
}
if err := s.clearDialogUnread(ctx, userID, in.ConversationID); err != nil {
return nil, err
}
if advanced {
if err := s.enqueueRead(ctx, userID, deviceID, in.ConversationID, cursor); err != nil {
return nil, err
}
}
return &ReadResult{ConversationID: in.ConversationID, MsgID: cursor}, nil
}
func (s *Service) readMember(ctx context.Context, conversationID, userID int64) (*conventity.Member, error) {
mem, err := s.chats.GetMember(ctx, conversationID, userID)
if err != nil {
if errors.Is(err, errs.ErrNotFound) {
return nil, errs.ErrForbidden
}
return nil, err
}
return mem, nil
}
func (s *Service) readTarget(ctx context.Context, in *ReadInput) error {
msg, err := s.repo.GetByID(ctx, in.MsgID)
if err != nil {
return err
}
if msg.ConversationID != in.ConversationID {
return errs.ErrInvalidArgument
}
return nil
}
func (s *Service) advanceRead(ctx context.Context, mem *conventity.Member, in *ReadInput) (int64, bool, error) {
if mem.LastReadMsgID >= in.MsgID {
return mem.LastReadMsgID, false, nil
}
if err := s.chats.UpdateLastRead(ctx, in.ConversationID, mem.UserID, in.MsgID); err != nil {
if errors.Is(err, errs.ErrNotFound) {
return 0, false, errs.ErrForbidden
}
return 0, false, err
}
return in.MsgID, true, nil
}
func (s *Service) clearDialogUnread(ctx context.Context, userID, conversationID int64) error {
if s.inbox == nil {
return nil
}
var score int64
if conv, err := s.chats.Get(ctx, conversationID); err == nil && conv != nil {
score = conv.LastMsgAt
}
if err := s.inbox.ClearUnread(ctx, userID, conversationID, score); err != nil {
return errs.ErrInner.WithError("inbox", err)
}
return nil
}
func (s *Service) enqueueRead(ctx context.Context, userID int64, deviceID uint64, conversationID, cursor int64) error {
if s.queue == nil {
return nil
}
jobID, err := s.ids.NextID()
if err != nil {
return err
}
return s.queue.Push(ctx, &deliverentity.Job{
MsgID: jobID, Cmd: cmdRead, ConversationID: conversationID,
FromUserID: userID, FromDeviceID: deviceID, ExcludeDevice: deviceID,
ToUserIDs: []int64{userID}, NotPush: true, NotMailbox: true,
Payload: controlBytes(cursor, conversationID),
})
}