feat(schedule): add course table screens and navigation

Add complete schedule functionality including:
- Schedule screen with weekly course table view
- Course detail screen with transparent modal presentation
- New ScheduleStack navigator integrated into main tab bar
- Schedule service for API interactions
- Type definitions for course entities

Also includes bug fixes for group invite/request handlers
to include required groupId parameter.
This commit is contained in:
2026-03-12 08:38:14 +08:00
parent 21293644b8
commit 0a0cbacbcc
25 changed files with 3050 additions and 260 deletions

View File

@@ -2,11 +2,14 @@ package service
import (
"context"
"log"
"time"
"carrot_bbs/internal/cache"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
"gorm.io/gorm"
)
// 缓存TTL常量
@@ -21,15 +24,37 @@ const (
// MessageService 消息服务
type MessageService struct {
db *gorm.DB
// 基础仓储
messageRepo *repository.MessageRepository
cache cache.Cache
// 缓存相关字段
conversationCache *cache.ConversationCache
// 基础缓存(用于简单缓存操作)
baseCache cache.Cache
}
// NewMessageService 创建消息服务
func NewMessageService(messageRepo *repository.MessageRepository) *MessageService {
func NewMessageService(db *gorm.DB, messageRepo *repository.MessageRepository) *MessageService {
// 创建适配器
convRepoAdapter := cache.NewConversationRepositoryAdapter(messageRepo)
msgRepoAdapter := cache.NewMessageRepositoryAdapter(messageRepo)
// 创建会话缓存
conversationCache := cache.NewConversationCache(
cache.GetCache(),
convRepoAdapter,
msgRepoAdapter,
cache.DefaultConversationCacheSettings(),
)
return &MessageService{
messageRepo: messageRepo,
cache: cache.GetCache(),
db: db,
messageRepo: messageRepo,
conversationCache: conversationCache,
baseCache: cache.GetCache(),
}
}
@@ -61,20 +86,50 @@ func (s *MessageService) SendMessage(ctx context.Context, senderID, receiverID s
return nil, err
}
// 新消息会改变分页结果,先失效分页缓存,避免读到旧列表
if s.conversationCache != nil {
s.conversationCache.InvalidateMessagePages(conv.ID)
}
// 异步写入缓存
go func() {
if err := s.cacheMessage(context.Background(), conv.ID, msg); err != nil {
log.Printf("[MessageService] async cache message failed, convID=%s, msgID=%s, err=%v", conv.ID, msg.ID, err)
}
}()
// 失效会话列表缓存(发送者和接收者)
cache.InvalidateConversationList(s.cache, senderID)
cache.InvalidateConversationList(s.cache, receiverID)
s.conversationCache.InvalidateConversationList(senderID)
s.conversationCache.InvalidateConversationList(receiverID)
// 失效未读数缓存
cache.InvalidateUnreadConversation(s.cache, receiverID)
cache.InvalidateUnreadDetail(s.cache, receiverID, conv.ID)
cache.InvalidateUnreadConversation(s.baseCache, receiverID)
s.conversationCache.InvalidateUnreadCount(receiverID, conv.ID)
return msg, nil
}
// cacheMessage 缓存消息(内部方法)
func (s *MessageService) cacheMessage(ctx context.Context, convID string, msg *model.Message) error {
if s.conversationCache == nil {
return nil
}
asyncCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
return s.conversationCache.CacheMessage(asyncCtx, convID, msg)
}
// GetConversations 获取会话列表(带缓存)
// userID 参数为 string 类型UUID格式与JWT中user_id保持一致
func (s *MessageService) GetConversations(ctx context.Context, userID string, page, pageSize int) ([]*model.Conversation, int64, error) {
// 优先使用 ConversationCache
if s.conversationCache != nil {
return s.conversationCache.GetConversationList(ctx, userID, page, pageSize)
}
// 降级到基础缓存
cacheSettings := cache.GetSettings()
conversationTTL := cacheSettings.ConversationTTL
if conversationTTL <= 0 {
@@ -92,7 +147,7 @@ func (s *MessageService) GetConversations(ctx context.Context, userID string, pa
// 生成缓存键
cacheKey := cache.ConversationListKey(userID, page, pageSize)
result, err := cache.GetOrLoadTyped[*ConversationListResult](
s.cache,
s.baseCache,
cacheKey,
conversationTTL,
jitter,
@@ -117,8 +172,14 @@ func (s *MessageService) GetConversations(ctx context.Context, userID string, pa
return result.Conversations, result.Total, nil
}
// GetMessages 获取消息列表
// GetMessages 获取消息列表(带缓存)
func (s *MessageService) GetMessages(ctx context.Context, conversationID string, page, pageSize int) ([]*model.Message, int64, error) {
// 优先使用 ConversationCache
if s.conversationCache != nil {
return s.conversationCache.GetMessages(ctx, conversationID, page, pageSize)
}
// 降级到直接访问数据库
return s.messageRepo.GetMessages(conversationID, page, pageSize)
}
@@ -127,20 +188,25 @@ func (s *MessageService) GetMessagesAfterSeq(ctx context.Context, conversationID
return s.messageRepo.GetMessagesAfterSeq(conversationID, afterSeq, limit)
}
// MarkAsRead 标记为已读
// MarkAsRead 标记为已读(使用 Cache-Aside 模式)
// userID 参数为 string 类型UUID格式与JWT中user_id保持一致
func (s *MessageService) MarkAsRead(ctx context.Context, conversationID string, userID string, lastReadSeq int64) error {
// 1. 先写入DB保证数据一致性DB是唯一数据源
err := s.messageRepo.UpdateLastReadSeq(conversationID, userID, lastReadSeq)
if err != nil {
return err
}
// 失效未读数缓存
cache.InvalidateUnreadConversation(s.cache, userID)
cache.InvalidateUnreadDetail(s.cache, userID, conversationID)
// 失效会话列表缓存
cache.InvalidateConversationList(s.cache, userID)
// 2. DB 写入成功后失效缓存Cache-Aside 模式)
if s.conversationCache != nil {
// 失效参与者缓存,下次读取时会从 DB 加载最新数据
s.conversationCache.InvalidateParticipant(conversationID, userID)
// 失效未读数缓存
s.conversationCache.InvalidateUnreadCount(userID, conversationID)
// 失效会话列表缓存
s.conversationCache.InvalidateConversationList(userID)
}
cache.InvalidateUnreadConversation(s.baseCache, userID)
return nil
}
@@ -148,6 +214,12 @@ func (s *MessageService) MarkAsRead(ctx context.Context, conversationID string,
// GetUnreadCount 获取未读消息数(带缓存)
// userID 参数为 string 类型UUID格式与JWT中user_id保持一致
func (s *MessageService) GetUnreadCount(ctx context.Context, conversationID string, userID string) (int64, error) {
// 优先使用 ConversationCache
if s.conversationCache != nil {
return s.conversationCache.GetUnreadCount(ctx, userID, conversationID)
}
// 降级到基础缓存
cacheSettings := cache.GetSettings()
unreadTTL := cacheSettings.UnreadCountTTL
if unreadTTL <= 0 {
@@ -166,7 +238,7 @@ func (s *MessageService) GetUnreadCount(ctx context.Context, conversationID stri
cacheKey := cache.UnreadDetailKey(userID, conversationID)
return cache.GetOrLoadTyped[int64](
s.cache,
s.baseCache,
cacheKey,
unreadTTL,
jitter,
@@ -186,14 +258,18 @@ func (s *MessageService) GetOrCreateConversation(ctx context.Context, user1ID, u
}
// 失效会话列表缓存
cache.InvalidateConversationList(s.cache, user1ID)
cache.InvalidateConversationList(s.cache, user2ID)
s.conversationCache.InvalidateConversationList(user1ID)
s.conversationCache.InvalidateConversationList(user2ID)
return conv, nil
}
// GetConversationParticipants 获取会话参与者列表
func (s *MessageService) GetConversationParticipants(conversationID string) ([]*model.ConversationParticipant, error) {
// 优先使用缓存
if s.conversationCache != nil {
return s.conversationCache.GetParticipants(context.Background(), conversationID)
}
return s.messageRepo.GetConversationParticipants(conversationID)
}
@@ -204,12 +280,12 @@ func ParseConversationID(idStr string) (string, error) {
// InvalidateUserConversationCache 失效用户会话相关缓存(供外部调用)
func (s *MessageService) InvalidateUserConversationCache(userID string) {
cache.InvalidateConversationList(s.cache, userID)
cache.InvalidateUnreadConversation(s.cache, userID)
s.conversationCache.InvalidateConversationList(userID)
cache.InvalidateUnreadConversation(s.baseCache, userID)
}
// InvalidateUserUnreadCache 失效用户未读数缓存(供外部调用)
func (s *MessageService) InvalidateUserUnreadCache(userID, conversationID string) {
cache.InvalidateUnreadConversation(s.cache, userID)
cache.InvalidateUnreadDetail(s.cache, userID, conversationID)
cache.InvalidateUnreadConversation(s.baseCache, userID)
s.conversationCache.InvalidateUnreadCount(userID, conversationID)
}