package com.mydreamjob.messaging; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Lock; import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import jakarta.persistence.LockModeType; import java.time.LocalDateTime; import java.util.List; @Repository public interface MessageRepository extends JpaRepository { Page findByConversationIdOrderByCreatedAtDesc(Long conversationId, Pageable pageable); List findByConversationIdInOrderByConversationIdAscCreatedAtDesc(List conversationIds); /** * 条件更新未命中后的当前读,用于区分并发请求已完成撤回与真实冲突。 * PESSIMISTIC_WRITE 强制读取提交后的最新行,避免 MySQL REPEATABLE READ 使用旧快照。 */ @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("select m from Message m where m.id = :messageId") java.util.Optional findCurrentByIdForRecall(@Param("messageId") Long messageId); @Modifying(clearAutomatically = true, flushAutomatically = true) @Query(""" update Message m set m.status = 'RECALLED', m.content = :placeholder, m.recalledAt = :recalledAt, m.version = m.version + 1 where m.id = :messageId and m.conversationId = :conversationId and m.senderUserId = :senderUserId and m.status = 'SENT' and m.version = :expectedVersion """) int recallIfCurrent(@Param("messageId") Long messageId, @Param("conversationId") Long conversationId, @Param("senderUserId") Long senderUserId, @Param("expectedVersion") Long expectedVersion, @Param("placeholder") String placeholder, @Param("recalledAt") LocalDateTime recalledAt); }