2026-04-09 10:01:23 +08:00
package services
import (
2026-05-31 18:43:48 +08:00
"agent-desk/internal/models"
"agent-desk/internal/pkg/dto"
"agent-desk/internal/pkg/enums"
"agent-desk/internal/pkg/errorsx"
"agent-desk/internal/pkg/openidentity"
"agent-desk/internal/repositories"
2026-04-09 10:01:23 +08:00
"strings"
"time"
2026-05-31 18:43:48 +08:00
"agent-desk/internal/pkg/httpx/params"
2026-04-09 10:01:23 +08:00
"github.com/mlogclub/simple/common/strs"
"github.com/mlogclub/simple/sqls"
"gorm.io/gorm"
)
var ConversationReadStateService = newConversationReadStateService ()
func newConversationReadStateService () * conversationReadStateService {
return & conversationReadStateService {}
}
type conversationReadStateService struct {
}
// readerCursor 已读游标行的身份键(包内私有,供客服 / 客户两条路径共用)。
type readerCursor struct {
readerType enums . IMSenderType
readerID int64
externalReaderID string
auditUserID int64
auditUserName string
}
func agentReaderCursor ( operator * dto . AuthPrincipal ) ( readerCursor , error ) {
if operator == nil {
2026-06-02 20:51:13 +08:00
return readerCursor {}, errorsx . UnauthorizedI18n ( "error.auth.expired" )
2026-04-09 10:01:23 +08:00
}
return readerCursor {
readerType : enums . IMSenderTypeAgent ,
readerID : operator . UserID ,
externalReaderID : "" ,
auditUserID : operator . UserID ,
auditUserName : operator . Username ,
}, nil
}
2026-04-28 11:10:47 +08:00
func customerReaderCursor ( external * openidentity . ExternalUser ) ( readerCursor , error ) {
2026-04-09 10:01:23 +08:00
if external == nil || strings . TrimSpace ( external . ExternalID ) == "" {
2026-06-02 20:51:13 +08:00
return readerCursor {}, errorsx . UnauthorizedI18n ( "error.e0149" )
2026-04-09 10:01:23 +08:00
}
extID := strings . TrimSpace ( external . ExternalID )
name := strings . TrimSpace ( external . ExternalName )
if name == "" {
name = extID
}
return readerCursor {
readerType : enums . IMSenderTypeCustomer ,
readerID : 0 ,
externalReaderID : extID ,
auditUserID : 0 ,
auditUserName : name ,
}, nil
}
func ( s * conversationReadStateService ) Get ( id int64 ) * models . ConversationReadState {
return repositories . ConversationReadStateRepository . Get ( sqls . DB (), id )
}
func ( s * conversationReadStateService ) Take ( where ... any ) * models . ConversationReadState {
return repositories . ConversationReadStateRepository . Take ( sqls . DB (), where ... )
}
func ( s * conversationReadStateService ) Find ( cnd * sqls . Cnd ) [] models . ConversationReadState {
return repositories . ConversationReadStateRepository . Find ( sqls . DB (), cnd )
}
func ( s * conversationReadStateService ) FindOne ( cnd * sqls . Cnd ) * models . ConversationReadState {
return repositories . ConversationReadStateRepository . FindOne ( sqls . DB (), cnd )
}
func ( s * conversationReadStateService ) FindPageByParams ( queryParams * params . QueryParams ) ( list [] models . ConversationReadState , paging * sqls . Paging ) {
return repositories . ConversationReadStateRepository . FindPageByParams ( sqls . DB (), queryParams )
}
func ( s * conversationReadStateService ) FindPageByCnd ( cnd * sqls . Cnd ) ( list [] models . ConversationReadState , paging * sqls . Paging ) {
return repositories . ConversationReadStateRepository . FindPageByCnd ( sqls . DB (), cnd )
}
func ( s * conversationReadStateService ) Count ( cnd * sqls . Cnd ) int64 {
return repositories . ConversationReadStateRepository . Count ( sqls . DB (), cnd )
}
func ( s * conversationReadStateService ) Create ( item * models . ConversationReadState ) error {
return repositories . ConversationReadStateRepository . Create ( sqls . DB (), item )
}
func ( s * conversationReadStateService ) Update ( item * models . ConversationReadState ) error {
return repositories . ConversationReadStateRepository . Update ( sqls . DB (), item )
}
func ( s * conversationReadStateService ) Updates ( id int64 , columns map [ string ] any ) error {
return repositories . ConversationReadStateRepository . Updates ( sqls . DB (), id , columns )
}
func ( s * conversationReadStateService ) UpdateColumn ( id int64 , name string , value any ) error {
return repositories . ConversationReadStateRepository . UpdateColumn ( sqls . DB (), id , name , value )
}
func ( s * conversationReadStateService ) Delete ( id int64 ) {
repositories . ConversationReadStateRepository . Delete ( sqls . DB (), id )
}
// GetByAgentReader 查询客服侧已读游标。
func ( s * conversationReadStateService ) GetByAgentReader ( conversationID int64 , operator * dto . AuthPrincipal ) * models . ConversationReadState {
if operator == nil {
return nil
}
return s . getByCursor ( conversationID , readerCursor {
readerType : enums . IMSenderTypeAgent ,
readerID : operator . UserID ,
externalReaderID : "" ,
})
}
// GetByCustomerReader 查询 IM 客户侧已读游标(按 ExternalID)。
2026-04-28 11:10:47 +08:00
func ( s * conversationReadStateService ) GetByCustomerReader ( conversationID int64 , external * openidentity . ExternalUser ) * models . ConversationReadState {
2026-04-09 10:01:23 +08:00
if external == nil || strings . TrimSpace ( external . ExternalID ) == "" {
return nil
}
return s . getByCursor ( conversationID , readerCursor {
readerType : enums . IMSenderTypeCustomer ,
readerID : 0 ,
externalReaderID : strings . TrimSpace ( external . ExternalID ),
})
}
func ( s * conversationReadStateService ) getByCursor ( conversationID int64 , c readerCursor ) * models . ConversationReadState {
return s . FindOne ( sqls . NewCnd ().
Eq ( "conversation_id" , conversationID ).
Eq ( "reader_type" , c . readerType ).
Eq ( "reader_id" , c . readerID ).
Eq ( "external_reader_id" , c . externalReaderID ))
}
func ( s * conversationReadStateService ) GetConversationReadStates ( conversationID int64 ) ( agentState , customerState * models . ConversationReadState ) {
return s . getConversationReadStates ( sqls . DB (), conversationID )
}
func ( s * conversationReadStateService ) getConversationReadStates ( db * gorm . DB , conversationID int64 ) ( agentState , customerState * models . ConversationReadState ) {
list := repositories . ConversationReadStateRepository . Find ( db , sqls . NewCnd (). Eq ( "conversation_id" , conversationID ))
return s . pickConversationReadStates ( list )
}
func ( s * conversationReadStateService ) pickConversationReadStates ( list [] models . ConversationReadState ) ( agentState , customerState * models . ConversationReadState ) {
for i := range list {
item := & list [ i ]
switch item . ReaderType {
case enums . IMSenderTypeAgent :
2026-06-24 22:53:17 +08:00
if agentState == nil || item . LastReadMessageID > agentState . LastReadMessageID {
2026-04-09 10:01:23 +08:00
agentState = item
}
case enums . IMSenderTypeCustomer :
2026-06-24 22:53:17 +08:00
if customerState == nil || item . LastReadMessageID > customerState . LastReadMessageID {
2026-04-09 10:01:23 +08:00
customerState = item
}
}
}
return agentState , customerState
}
// MarkAgentRead 在事务内更新/创建客服已读游标。
2026-05-09 22:05:34 +08:00
func ( s * conversationReadStateService ) MarkAgentRead ( ctx * sqls . TxContext , conversation * models . Conversation , operator * dto . AuthPrincipal , message * models . Message ) ( * models . ConversationReadState , error ) {
2026-04-09 10:01:23 +08:00
c , err := agentReaderCursor ( operator )
if err != nil {
return nil , err
}
2026-05-09 22:05:34 +08:00
return s . markReadTxWithCursor ( ctx , conversation , c , message )
2026-04-09 10:01:23 +08:00
}
// MarkCustomerRead 在事务内更新/创建 IM 客户已读游标。
2026-05-09 22:05:34 +08:00
func ( s * conversationReadStateService ) MarkCustomerRead ( ctx * sqls . TxContext , conversation * models . Conversation , external * openidentity . ExternalUser , message * models . Message ) ( * models . ConversationReadState , error ) {
2026-04-09 10:01:23 +08:00
c , err := customerReaderCursor ( external )
if err != nil {
return nil , err
}
2026-05-09 22:05:34 +08:00
return s . markReadTxWithCursor ( ctx , conversation , c , message )
2026-04-09 10:01:23 +08:00
}
2026-05-09 22:05:34 +08:00
func ( s * conversationReadStateService ) markReadTxWithCursor ( ctx * sqls . TxContext , conversation * models . Conversation , c readerCursor , message * models . Message ) ( * models . ConversationReadState , error ) {
2026-04-09 10:01:23 +08:00
if ctx == nil || conversation == nil || message == nil {
return nil , nil
}
if c . readerType != enums . IMSenderTypeAgent && c . readerType != enums . IMSenderTypeCustomer {
2026-06-02 20:51:13 +08:00
return nil , errorsx . InvalidParamI18n ( "error.e0081" )
2026-04-09 10:01:23 +08:00
}
2026-05-09 22:05:34 +08:00
now := time . Now ()
2026-04-09 10:01:23 +08:00
item := & models . ConversationReadState {}
err := ctx . Tx . Where ( "conversation_id = ? AND reader_type = ? AND reader_id = ? AND external_reader_id = ?" ,
conversation . ID , c . readerType , c . readerID , c . externalReaderID ,
). First ( item ). Error
if err != nil {
if err != gorm . ErrRecordNotFound {
return nil , err
}
item = & models . ConversationReadState {
ConversationID : conversation . ID ,
ReaderType : c . readerType ,
ReaderID : c . readerID ,
ExternalReaderID : c . externalReaderID ,
LastReadMessageID : message . ID ,
LastReadAt : & now ,
AuditFields : models . AuditFields {
CreatedAt : now ,
CreateUserID : c . auditUserID ,
CreateUserName : c . auditUserName ,
UpdatedAt : now ,
UpdateUserID : c . auditUserID ,
UpdateUserName : c . auditUserName ,
},
}
if err := ctx . Tx . Create ( item ). Error ; err != nil {
return nil , err
}
return item , nil
}
2026-06-24 22:53:17 +08:00
if item . LastReadMessageID >= message . ID {
2026-04-09 10:01:23 +08:00
return item , nil
}
item . LastReadMessageID = message . ID
item . LastReadAt = & now
item . UpdatedAt = now
item . UpdateUserID = c . auditUserID
item . UpdateUserName = c . auditUserName
if err := repositories . ConversationReadStateRepository . Updates ( ctx . Tx , item . ID , map [ string ] any {
"last_read_message_id" : item . LastReadMessageID ,
"last_read_at" : item . LastReadAt ,
"updated_at" : item . UpdatedAt ,
"update_user_id" : item . UpdateUserID ,
"update_user_name" : item . UpdateUserName ,
}); err != nil {
return nil , err
}
return item , nil
}
2026-06-24 22:53:17 +08:00
func ( s * conversationReadStateService ) CountUnreadMessages ( ctx * sqls . TxContext , conversationID , lastReadMessageID int64 , senderTypes ... enums . IMSenderType ) ( int64 , error ) {
2026-04-09 10:01:23 +08:00
normalizedSenderTypes := make ([] enums . IMSenderType , 0 , len ( senderTypes ))
for _ , senderType := range senderTypes {
if strs . IsBlank ( string ( senderType )) {
continue
}
normalizedSenderTypes = append ( normalizedSenderTypes , senderType )
}
if len ( normalizedSenderTypes ) == 0 {
return 0 , nil
}
var count int64
query := ctx . Tx . Model ( & models . Message {}).
2026-06-24 22:53:17 +08:00
Where ( "conversation_id = ? AND id > ? AND recalled_at IS NULL AND send_status <> ?" , conversationID , lastReadMessageID , int ( enums . IMMessageStatusRecalled ))
2026-04-09 10:01:23 +08:00
if len ( normalizedSenderTypes ) == 1 {
query = query . Where ( "sender_type = ?" , normalizedSenderTypes [ 0 ])
} else {
query = query . Where ( "sender_type IN ?" , normalizedSenderTypes )
}
err := query . Count ( & count ). Error
return count , err
}