支持管理员配置上游错误如何返回给客户端: - 新增 ErrorPassthroughRule 数据模型和 Ent Schema - 实现规则的 CRUD API(/admin/error-passthrough-rules) - 支持按错误码、关键词匹配,支持 any/all 匹配模式 - 支持按平台过滤(anthropic/openai/gemini/antigravity) - 支持透传或自定义响应状态码和错误消息 - 实现两级缓存(Redis + 本地内存)和多实例同步 - 集成到 gateway_handler 的错误处理流程 - 新增前端管理界面组件 - 新增单元测试覆盖核心匹配逻辑 优化: - 移除 refreshLocalCache 中的冗余排序(数据库已排序) - 后端 Validate() 增加匹配条件非空校验
129 lines
2.9 KiB
Go
129 lines
2.9 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"log"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/model"
|
|
"github.com/Wei-Shaw/sub2api/internal/service"
|
|
"github.com/redis/go-redis/v9"
|
|
)
|
|
|
|
const (
|
|
errorPassthroughCacheKey = "error_passthrough_rules"
|
|
errorPassthroughPubSubKey = "error_passthrough_rules_updated"
|
|
errorPassthroughCacheTTL = 24 * time.Hour
|
|
)
|
|
|
|
type errorPassthroughCache struct {
|
|
rdb *redis.Client
|
|
localCache []*model.ErrorPassthroughRule
|
|
localMu sync.RWMutex
|
|
}
|
|
|
|
// NewErrorPassthroughCache 创建错误透传规则缓存
|
|
func NewErrorPassthroughCache(rdb *redis.Client) service.ErrorPassthroughCache {
|
|
return &errorPassthroughCache{
|
|
rdb: rdb,
|
|
}
|
|
}
|
|
|
|
// Get 从缓存获取规则列表
|
|
func (c *errorPassthroughCache) Get(ctx context.Context) ([]*model.ErrorPassthroughRule, bool) {
|
|
// 先检查本地缓存
|
|
c.localMu.RLock()
|
|
if c.localCache != nil {
|
|
rules := c.localCache
|
|
c.localMu.RUnlock()
|
|
return rules, true
|
|
}
|
|
c.localMu.RUnlock()
|
|
|
|
// 从 Redis 获取
|
|
data, err := c.rdb.Get(ctx, errorPassthroughCacheKey).Bytes()
|
|
if err != nil {
|
|
if err != redis.Nil {
|
|
log.Printf("[ErrorPassthroughCache] Failed to get from Redis: %v", err)
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
var rules []*model.ErrorPassthroughRule
|
|
if err := json.Unmarshal(data, &rules); err != nil {
|
|
log.Printf("[ErrorPassthroughCache] Failed to unmarshal rules: %v", err)
|
|
return nil, false
|
|
}
|
|
|
|
// 更新本地缓存
|
|
c.localMu.Lock()
|
|
c.localCache = rules
|
|
c.localMu.Unlock()
|
|
|
|
return rules, true
|
|
}
|
|
|
|
// Set 设置缓存
|
|
func (c *errorPassthroughCache) Set(ctx context.Context, rules []*model.ErrorPassthroughRule) error {
|
|
data, err := json.Marshal(rules)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := c.rdb.Set(ctx, errorPassthroughCacheKey, data, errorPassthroughCacheTTL).Err(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// 更新本地缓存
|
|
c.localMu.Lock()
|
|
c.localCache = rules
|
|
c.localMu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Invalidate 使缓存失效
|
|
func (c *errorPassthroughCache) Invalidate(ctx context.Context) error {
|
|
// 清除本地缓存
|
|
c.localMu.Lock()
|
|
c.localCache = nil
|
|
c.localMu.Unlock()
|
|
|
|
// 清除 Redis 缓存
|
|
return c.rdb.Del(ctx, errorPassthroughCacheKey).Err()
|
|
}
|
|
|
|
// NotifyUpdate 通知其他实例刷新缓存
|
|
func (c *errorPassthroughCache) NotifyUpdate(ctx context.Context) error {
|
|
return c.rdb.Publish(ctx, errorPassthroughPubSubKey, "refresh").Err()
|
|
}
|
|
|
|
// SubscribeUpdates 订阅缓存更新通知
|
|
func (c *errorPassthroughCache) SubscribeUpdates(ctx context.Context, handler func()) {
|
|
go func() {
|
|
sub := c.rdb.Subscribe(ctx, errorPassthroughPubSubKey)
|
|
defer func() { _ = sub.Close() }()
|
|
|
|
ch := sub.Channel()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case msg := <-ch:
|
|
if msg == nil {
|
|
return
|
|
}
|
|
// 清除本地缓存,下次访问时会从 Redis 或数据库重新加载
|
|
c.localMu.Lock()
|
|
c.localCache = nil
|
|
c.localMu.Unlock()
|
|
|
|
// 调用处理函数
|
|
handler()
|
|
}
|
|
}
|
|
}()
|
|
}
|