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

@@ -145,6 +145,45 @@ func (s *groupService) publishGroupNotice(groupID string, notice groupNoticeMess
}
}
// invalidateConversationCachesAfterSystemMessage 系统消息写入后失效相关缓存
func (s *groupService) invalidateConversationCachesAfterSystemMessage(conversationID string) {
if conversationID == "" || s.messageRepo == nil {
return
}
// 新系统消息会影响消息分页列表
cache.InvalidateMessagePages(s.cache, conversationID)
// 参与者列表可能发生变化(加群/退群)后,这里统一清理一次
s.cache.Delete(cache.ParticipantListKey(conversationID))
participants, err := s.messageRepo.GetConversationParticipants(conversationID)
if err != nil {
return
}
for _, p := range participants {
if p == nil || p.UserID == "" {
continue
}
// 会话最后消息、未读数会变化,清理用户维度缓存
cache.InvalidateConversationList(s.cache, p.UserID)
cache.InvalidateUnreadConversation(s.cache, p.UserID)
cache.InvalidateUnreadDetail(s.cache, p.UserID, conversationID)
}
}
// invalidateConversationCachesAfterMembershipChange 成员变更后失效相关缓存
func (s *groupService) invalidateConversationCachesAfterMembershipChange(conversationID, userID string) {
if conversationID == "" {
return
}
s.cache.Delete(cache.ParticipantListKey(conversationID))
if userID != "" {
s.cache.Delete(cache.ParticipantKey(conversationID, userID))
cache.InvalidateConversationList(s.cache, userID)
cache.InvalidateUnreadConversation(s.cache, userID)
cache.InvalidateUnreadDetail(s.cache, userID, conversationID)
}
}
// ==================== 群组管理 ====================
// CreateGroup 创建群组
@@ -444,6 +483,7 @@ func (s *groupService) broadcastMemberJoinNotice(groupID string, targetUserID st
log.Printf("[broadcastMemberJoinNotice] 保存入群提示消息失败: groupID=%s, userID=%s, err=%v", groupID, targetUserID, err)
} else {
savedMessage = msg
s.invalidateConversationCachesAfterSystemMessage(conv.ID)
}
} else {
log.Printf("[broadcastMemberJoinNotice] 获取群组会话失败: groupID=%s, err=%v", groupID, err)
@@ -502,6 +542,7 @@ func (s *groupService) addMemberToGroupAndConversation(group *model.Group, userI
if err := s.messageRepo.AddParticipant(conv.ID, userID); err != nil {
log.Printf("[addMemberToGroupAndConversation] 添加会话参与者失败: groupID=%s, userID=%s, err=%v", group.ID, userID, err)
}
s.invalidateConversationCachesAfterMembershipChange(conv.ID, userID)
}
}
cache.InvalidateGroupMembers(s.cache, group.ID)
@@ -1036,6 +1077,7 @@ func (s *groupService) LeaveGroup(userID string, groupID string) error {
// 如果移除参与者失败,记录日志但不阻塞退出群流程
fmt.Printf("[WARN] LeaveGroup: failed to remove participant %s from conversation %s, error: %v\n", userID, conv.ID, err)
}
s.invalidateConversationCachesAfterMembershipChange(conv.ID, userID)
}
// 失效群组成员缓存
@@ -1092,6 +1134,7 @@ func (s *groupService) RemoveMember(userID string, groupID string, targetUserID
if err := s.messageRepo.RemoveParticipant(conv.ID, targetUserID); err != nil {
log.Printf("[RemoveMember] 移除会话参与者失败: groupID=%s, userID=%s, err=%v", groupID, targetUserID, err)
}
s.invalidateConversationCachesAfterMembershipChange(conv.ID, targetUserID)
}
}
@@ -1290,6 +1333,7 @@ func (s *groupService) MuteMember(userID string, groupID string, targetUserID st
} else {
savedMessage = msg
log.Printf("[MuteMember] 禁言消息已保存, ID=%s, Seq=%d", msg.ID, msg.Seq)
s.invalidateConversationCachesAfterSystemMessage(conv.ID)
}
} else {
log.Printf("[MuteMember] 获取群组会话失败: %v", err)