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

@@ -5,7 +5,10 @@ import (
"encoding/json"
"fmt"
"log"
"math"
"math/rand"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
@@ -34,6 +37,38 @@ type Cache interface {
Increment(key string) int64
// IncrementBy 增加指定值
IncrementBy(key string, value int64) int64
// ==================== Hash 操作 ====================
// HSet 设置 Hash 字段
HSet(ctx context.Context, key string, field string, value interface{}) error
// HMSet 批量设置 Hash 字段
HMSet(ctx context.Context, key string, values map[string]interface{}) error
// HGet 获取 Hash 字段值
HGet(ctx context.Context, key string, field string) (string, error)
// HMGet 批量获取 Hash 字段值
HMGet(ctx context.Context, key string, fields ...string) ([]interface{}, error)
// HGetAll 获取 Hash 所有字段
HGetAll(ctx context.Context, key string) (map[string]string, error)
// HDel 删除 Hash 字段
HDel(ctx context.Context, key string, fields ...string) error
// ==================== Sorted Set 操作 ====================
// ZAdd 添加 Sorted Set 成员
ZAdd(ctx context.Context, key string, score float64, member string) error
// ZRangeByScore 按分数范围获取成员(升序)
ZRangeByScore(ctx context.Context, key string, min, max string, offset, count int64) ([]string, error)
// ZRevRangeByScore 按分数范围获取成员(降序)
ZRevRangeByScore(ctx context.Context, key string, max, min string, offset, count int64) ([]string, error)
// ZRem 删除 Sorted Set 成员
ZRem(ctx context.Context, key string, members ...interface{}) error
// ZCard 获取 Sorted Set 成员数量
ZCard(ctx context.Context, key string) (int64, error)
// ==================== 计数器操作 ====================
// Incr 原子递增(返回新值)
Incr(ctx context.Context, key string) (int64, error)
// Expire 设置过期时间
Expire(ctx context.Context, key string, ttl time.Duration) error
}
// cacheItem 缓存项(用于内存缓存降级)
@@ -64,16 +99,16 @@ type MetricsSnapshot struct {
}
type Settings struct {
Enabled bool
KeyPrefix string
DefaultTTL time.Duration
NullTTL time.Duration
JitterRatio float64
PostListTTL time.Duration
ConversationTTL time.Duration
UnreadCountTTL time.Duration
GroupMembersTTL time.Duration
DisableFlushDB bool
Enabled bool
KeyPrefix string
DefaultTTL time.Duration
NullTTL time.Duration
JitterRatio float64
PostListTTL time.Duration
ConversationTTL time.Duration
UnreadCountTTL time.Duration
GroupMembersTTL time.Duration
DisableFlushDB bool
}
var settings = Settings{
@@ -327,6 +362,378 @@ func (c *MemoryCache) Stop() {
close(c.stopCleanup)
}
// ==================== MemoryCache Hash 操作 ====================
// hashItem Hash 存储项
type hashItem struct {
fields sync.Map
}
// HSet 设置 Hash 字段
func (c *MemoryCache) HSet(ctx context.Context, key string, field string, value interface{}) error {
key = normalizeKey(key)
item, _ := c.items.Load(key)
var h *hashItem
if item == nil {
h = &hashItem{}
c.items.Store(key, &cacheItem{value: h, expiration: 0})
} else {
ci := item.(*cacheItem)
if ci.isExpired() {
h = &hashItem{}
c.items.Store(key, &cacheItem{value: h, expiration: 0})
} else {
h = ci.value.(*hashItem)
}
}
h.fields.Store(field, value)
return nil
}
// HMSet 批量设置 Hash 字段
func (c *MemoryCache) HMSet(ctx context.Context, key string, values map[string]interface{}) error {
for field, value := range values {
if err := c.HSet(ctx, key, field, value); err != nil {
return err
}
}
return nil
}
// HGet 获取 Hash 字段值
func (c *MemoryCache) HGet(ctx context.Context, key string, field string) (string, error) {
key = normalizeKey(key)
item, ok := c.items.Load(key)
if !ok {
return "", fmt.Errorf("key not found")
}
ci := item.(*cacheItem)
if ci.isExpired() {
c.items.Delete(key)
return "", fmt.Errorf("key not found")
}
h, ok := ci.value.(*hashItem)
if !ok {
return "", fmt.Errorf("key is not a hash")
}
val, ok := h.fields.Load(field)
if !ok {
return "", fmt.Errorf("field not found")
}
switch v := val.(type) {
case string:
return v, nil
case []byte:
return string(v), nil
default:
data, _ := json.Marshal(v)
return string(data), nil
}
}
// HMGet 批量获取 Hash 字段值
func (c *MemoryCache) HMGet(ctx context.Context, key string, fields ...string) ([]interface{}, error) {
result := make([]interface{}, len(fields))
for i, field := range fields {
val, err := c.HGet(ctx, key, field)
if err != nil {
result[i] = nil
} else {
result[i] = val
}
}
return result, nil
}
// HGetAll 获取 Hash 所有字段
func (c *MemoryCache) HGetAll(ctx context.Context, key string) (map[string]string, error) {
key = normalizeKey(key)
item, ok := c.items.Load(key)
if !ok {
return nil, fmt.Errorf("key not found")
}
ci := item.(*cacheItem)
if ci.isExpired() {
c.items.Delete(key)
return nil, fmt.Errorf("key not found")
}
h, ok := ci.value.(*hashItem)
if !ok {
return nil, fmt.Errorf("key is not a hash")
}
result := make(map[string]string)
h.fields.Range(func(k, v interface{}) bool {
keyStr := k.(string)
switch val := v.(type) {
case string:
result[keyStr] = val
case []byte:
result[keyStr] = string(val)
default:
data, _ := json.Marshal(val)
result[keyStr] = string(data)
}
return true
})
return result, nil
}
// HDel 删除 Hash 字段
func (c *MemoryCache) HDel(ctx context.Context, key string, fields ...string) error {
key = normalizeKey(key)
item, ok := c.items.Load(key)
if !ok {
return nil
}
ci := item.(*cacheItem)
if ci.isExpired() {
c.items.Delete(key)
return nil
}
h, ok := ci.value.(*hashItem)
if !ok {
return nil
}
for _, field := range fields {
h.fields.Delete(field)
}
return nil
}
// ==================== MemoryCache Sorted Set 操作 ====================
// zItem Sorted Set 成员
type zItem struct {
score float64
member string
}
// zsetItem Sorted Set 存储项
type zsetItem struct {
members sync.Map // member -> *zItem
byScore *sortedSlice // 按分数排序的切片
}
// sortedSlice 简单的排序切片实现
type sortedSlice struct {
items []*zItem
mu sync.RWMutex
}
// ZAdd 添加 Sorted Set 成员
func (c *MemoryCache) ZAdd(ctx context.Context, key string, score float64, member string) error {
key = normalizeKey(key)
item, _ := c.items.Load(key)
var z *zsetItem
if item == nil {
z = &zsetItem{byScore: &sortedSlice{}}
c.items.Store(key, &cacheItem{value: z, expiration: 0})
} else {
ci := item.(*cacheItem)
if ci.isExpired() {
z = &zsetItem{byScore: &sortedSlice{}}
c.items.Store(key, &cacheItem{value: z, expiration: 0})
} else {
z = ci.value.(*zsetItem)
}
}
z.members.Store(member, &zItem{score: score, member: member})
z.byScore.mu.Lock()
// 简单实现:重新构建排序切片
z.byScore.items = nil
z.members.Range(func(k, v interface{}) bool {
z.byScore.items = append(z.byScore.items, v.(*zItem))
return true
})
// 按分数排序
sort.Slice(z.byScore.items, func(i, j int) bool {
return z.byScore.items[i].score < z.byScore.items[j].score
})
z.byScore.mu.Unlock()
return nil
}
// ZRangeByScore 按分数范围获取成员(升序)
func (c *MemoryCache) ZRangeByScore(ctx context.Context, key string, min, max string, offset, count int64) ([]string, error) {
key = normalizeKey(key)
item, ok := c.items.Load(key)
if !ok {
return nil, nil
}
ci := item.(*cacheItem)
if ci.isExpired() {
c.items.Delete(key)
return nil, nil
}
z, ok := ci.value.(*zsetItem)
if !ok {
return nil, fmt.Errorf("key is not a sorted set")
}
minScore, _ := strconv.ParseFloat(min, 64)
maxScore, _ := strconv.ParseFloat(max, 64)
if min == "-inf" {
minScore = math.Inf(-1)
}
if max == "+inf" {
maxScore = math.Inf(1)
}
z.byScore.mu.RLock()
defer z.byScore.mu.RUnlock()
var result []string
var skipped int64 = 0
for _, item := range z.byScore.items {
if item.score < minScore || item.score > maxScore {
continue
}
if skipped < offset {
skipped++
continue
}
if count > 0 && int64(len(result)) >= count {
break
}
result = append(result, item.member)
}
return result, nil
}
// ZRevRangeByScore 按分数范围获取成员(降序)
func (c *MemoryCache) ZRevRangeByScore(ctx context.Context, key string, max, min string, offset, count int64) ([]string, error) {
key = normalizeKey(key)
item, ok := c.items.Load(key)
if !ok {
return nil, nil
}
ci := item.(*cacheItem)
if ci.isExpired() {
c.items.Delete(key)
return nil, nil
}
z, ok := ci.value.(*zsetItem)
if !ok {
return nil, fmt.Errorf("key is not a sorted set")
}
minScore, _ := strconv.ParseFloat(min, 64)
maxScore, _ := strconv.ParseFloat(max, 64)
if min == "-inf" {
minScore = math.Inf(-1)
}
if max == "+inf" {
maxScore = math.Inf(1)
}
z.byScore.mu.RLock()
defer z.byScore.mu.RUnlock()
var result []string
var skipped int64 = 0
// 从后往前遍历
for i := len(z.byScore.items) - 1; i >= 0; i-- {
item := z.byScore.items[i]
if item.score < minScore || item.score > maxScore {
continue
}
if skipped < offset {
skipped++
continue
}
if count > 0 && int64(len(result)) >= count {
break
}
result = append(result, item.member)
}
return result, nil
}
// ZRem 删除 Sorted Set 成员
func (c *MemoryCache) ZRem(ctx context.Context, key string, members ...interface{}) error {
key = normalizeKey(key)
item, ok := c.items.Load(key)
if !ok {
return nil
}
ci := item.(*cacheItem)
if ci.isExpired() {
c.items.Delete(key)
return nil
}
z, ok := ci.value.(*zsetItem)
if !ok {
return nil
}
for _, m := range members {
if member, ok := m.(string); ok {
z.members.Delete(member)
}
}
// 重建排序切片
z.byScore.mu.Lock()
z.byScore.items = nil
z.members.Range(func(k, v interface{}) bool {
z.byScore.items = append(z.byScore.items, v.(*zItem))
return true
})
sort.Slice(z.byScore.items, func(i, j int) bool {
return z.byScore.items[i].score < z.byScore.items[j].score
})
z.byScore.mu.Unlock()
return nil
}
// ZCard 获取 Sorted Set 成员数量
func (c *MemoryCache) ZCard(ctx context.Context, key string) (int64, error) {
key = normalizeKey(key)
item, ok := c.items.Load(key)
if !ok {
return 0, nil
}
ci := item.(*cacheItem)
if ci.isExpired() {
c.items.Delete(key)
return 0, nil
}
z, ok := ci.value.(*zsetItem)
if !ok {
return 0, fmt.Errorf("key is not a sorted set")
}
var count int64 = 0
z.members.Range(func(k, v interface{}) bool {
count++
return true
})
return count, nil
}
// ==================== MemoryCache 计数器操作 ====================
// Incr 原子递增(返回新值)
func (c *MemoryCache) Incr(ctx context.Context, key string) (int64, error) {
return c.IncrementBy(key, 1), nil
}
// Expire 设置过期时间
func (c *MemoryCache) Expire(ctx context.Context, key string, ttl time.Duration) error {
key = normalizeKey(key)
item, ok := c.items.Load(key)
if !ok {
return fmt.Errorf("key not found")
}
ci := item.(*cacheItem)
var expiration int64
if ttl > 0 {
expiration = time.Now().Add(ttl).UnixNano()
}
c.items.Store(key, &cacheItem{
value: ci.value,
expiration: expiration,
})
return nil
}
// RedisCache Redis缓存实现
type RedisCache struct {
client *redisPkg.Client
@@ -451,6 +858,91 @@ func (c *RedisCache) IncrementBy(key string, value int64) int64 {
return result
}
// ==================== RedisCache Hash 操作 ====================
// HSet 设置 Hash 字段
func (c *RedisCache) HSet(ctx context.Context, key string, field string, value interface{}) error {
key = normalizeKey(key)
return c.client.HSet(ctx, key, field, value)
}
// HMSet 批量设置 Hash 字段
func (c *RedisCache) HMSet(ctx context.Context, key string, values map[string]interface{}) error {
key = normalizeKey(key)
return c.client.HMSet(ctx, key, values)
}
// HGet 获取 Hash 字段值
func (c *RedisCache) HGet(ctx context.Context, key string, field string) (string, error) {
key = normalizeKey(key)
return c.client.HGet(ctx, key, field)
}
// HMGet 批量获取 Hash 字段值
func (c *RedisCache) HMGet(ctx context.Context, key string, fields ...string) ([]interface{}, error) {
key = normalizeKey(key)
return c.client.HMGet(ctx, key, fields...)
}
// HGetAll 获取 Hash 所有字段
func (c *RedisCache) HGetAll(ctx context.Context, key string) (map[string]string, error) {
key = normalizeKey(key)
return c.client.HGetAll(ctx, key)
}
// HDel 删除 Hash 字段
func (c *RedisCache) HDel(ctx context.Context, key string, fields ...string) error {
key = normalizeKey(key)
return c.client.HDel(ctx, key, fields...)
}
// ==================== RedisCache Sorted Set 操作 ====================
// ZAdd 添加 Sorted Set 成员
func (c *RedisCache) ZAdd(ctx context.Context, key string, score float64, member string) error {
key = normalizeKey(key)
return c.client.ZAdd(ctx, key, score, member)
}
// ZRangeByScore 按分数范围获取成员(升序)
func (c *RedisCache) ZRangeByScore(ctx context.Context, key string, min, max string, offset, count int64) ([]string, error) {
key = normalizeKey(key)
return c.client.ZRangeByScore(ctx, key, min, max, offset, count)
}
// ZRevRangeByScore 按分数范围获取成员(降序)
func (c *RedisCache) ZRevRangeByScore(ctx context.Context, key string, max, min string, offset, count int64) ([]string, error) {
key = normalizeKey(key)
return c.client.ZRevRangeByScore(ctx, key, max, min, offset, count)
}
// ZRem 删除 Sorted Set 成员
func (c *RedisCache) ZRem(ctx context.Context, key string, members ...interface{}) error {
key = normalizeKey(key)
return c.client.ZRem(ctx, key, members...)
}
// ZCard 获取 Sorted Set 成员数量
func (c *RedisCache) ZCard(ctx context.Context, key string) (int64, error) {
key = normalizeKey(key)
return c.client.ZCard(ctx, key)
}
// ==================== RedisCache 计数器操作 ====================
// Incr 原子递增(返回新值)
func (c *RedisCache) Incr(ctx context.Context, key string) (int64, error) {
key = normalizeKey(key)
return c.client.Incr(ctx, key)
}
// Expire 设置过期时间
func (c *RedisCache) Expire(ctx context.Context, key string, ttl time.Duration) error {
key = normalizeKey(key)
_, err := c.client.Expire(ctx, key, ttl)
return err
}
// 全局缓存实例
var globalCache Cache
var once sync.Once