mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-05-05 05:30:44 +08:00
fix: channel cache fail-close, group conflict check across pages, status toggle stale data
- GetGroupPlatforms failure now stores error-TTL cache and returns error (fail-close) - Frontend group-to-channel conflict map loads all channels instead of current page only - Toggle channel status reloads list when active filter would hide the changed item
This commit is contained in:
@@ -188,7 +188,7 @@ func (r *channelRepository) List(ctx context.Context, params pagination.Paginati
|
|||||||
// 查询 channel 列表
|
// 查询 channel 列表
|
||||||
dataQuery := fmt.Sprintf(
|
dataQuery := fmt.Sprintf(
|
||||||
`SELECT c.id, c.name, c.description, c.status, c.model_mapping, c.billing_model_source, c.restrict_models, c.created_at, c.updated_at
|
`SELECT c.id, c.name, c.description, c.status, c.model_mapping, c.billing_model_source, c.restrict_models, c.created_at, c.updated_at
|
||||||
FROM channels c WHERE %s ORDER BY c.id DESC LIMIT $%d OFFSET $%d`,
|
FROM channels c WHERE %s ORDER BY c.id ASC LIMIT $%d OFFSET $%d`,
|
||||||
whereClause, argIdx, argIdx+1,
|
whereClause, argIdx, argIdx+1,
|
||||||
)
|
)
|
||||||
args = append(args, pageSize, offset)
|
args = append(args, pageSize, offset)
|
||||||
|
|||||||
@@ -278,7 +278,10 @@ func (s *ChannelService) buildCache(ctx context.Context) (*channelCache, error)
|
|||||||
groupPlatforms, err = s.repo.GetGroupPlatforms(dbCtx, allGroupIDs)
|
groupPlatforms, err = s.repo.GetGroupPlatforms(dbCtx, allGroupIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("failed to load group platforms for channel cache", "error", err)
|
slog.Warn("failed to load group platforms for channel cache", "error", err)
|
||||||
// 降级:继续构建缓存但无法按平台过滤
|
errorCache := newEmptyChannelCache()
|
||||||
|
errorCache.loadedAt = time.Now().Add(-(channelCacheTTL - channelErrorTTL))
|
||||||
|
s.cache.Store(errorCache)
|
||||||
|
return nil, fmt.Errorf("get group platforms: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1182,12 +1182,15 @@ func TestBuildCache_GroupPlatformError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
svc := newTestChannelService(repo)
|
svc := newTestChannelService(repo)
|
||||||
|
|
||||||
// Should degrade gracefully: channel is found, but without platform info
|
// Should fail-close: error propagated when group platforms cannot be loaded
|
||||||
// pricing won't match because platform will be "" and pricing platform is "anthropic"
|
|
||||||
result, err := svc.GetChannelForGroup(context.Background(), 10)
|
result, err := svc.GetChannelForGroup(context.Background(), 10)
|
||||||
require.NoError(t, err)
|
require.Error(t, err)
|
||||||
require.NotNil(t, result) // channel still found
|
require.Nil(t, result)
|
||||||
require.Equal(t, int64(1), result.ID)
|
|
||||||
|
// Within error-TTL, second call should hit cache (empty) and return nil, nil
|
||||||
|
result2, err2 := svc.GetChannelForGroup(context.Background(), 10)
|
||||||
|
require.NoError(t, err2)
|
||||||
|
require.Nil(t, result2)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuildCache_MultipleGroupsSameChannel(t *testing.T) {
|
func TestBuildCache_MultipleGroupsSameChannel(t *testing.T) {
|
||||||
|
|||||||
@@ -499,6 +499,9 @@ const activeTab = ref<string>('basic')
|
|||||||
const allGroups = ref<AdminGroup[]>([])
|
const allGroups = ref<AdminGroup[]>([])
|
||||||
const groupsLoading = ref(false)
|
const groupsLoading = ref(false)
|
||||||
|
|
||||||
|
// All channels for group-conflict detection (independent of current page)
|
||||||
|
const allChannelsForConflict = ref<Channel[]>([])
|
||||||
|
|
||||||
// Form data
|
// Form data
|
||||||
const form = reactive({
|
const form = reactive({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -575,7 +578,7 @@ function getGroupsForPlatform(platform: GroupPlatform): AdminGroup[] {
|
|||||||
// ── Group helpers ──
|
// ── Group helpers ──
|
||||||
const groupToChannelMap = computed(() => {
|
const groupToChannelMap = computed(() => {
|
||||||
const map = new Map<number, Channel>()
|
const map = new Map<number, Channel>()
|
||||||
for (const ch of channels.value) {
|
for (const ch of allChannelsForConflict.value) {
|
||||||
if (editingChannel.value && ch.id === editingChannel.value.id) continue
|
if (editingChannel.value && ch.id === editingChannel.value.id) continue
|
||||||
for (const gid of ch.group_ids || []) {
|
for (const gid of ch.group_ids || []) {
|
||||||
map.set(gid, ch)
|
map.set(gid, ch)
|
||||||
@@ -794,6 +797,16 @@ async function loadGroups() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadAllChannelsForConflict() {
|
||||||
|
try {
|
||||||
|
const response = await adminAPI.channels.list(1, 1000)
|
||||||
|
allChannelsForConflict.value = response.items || []
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback to current page data
|
||||||
|
allChannelsForConflict.value = channels.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let searchTimeout: ReturnType<typeof setTimeout>
|
let searchTimeout: ReturnType<typeof setTimeout>
|
||||||
function handleSearch() {
|
function handleSearch() {
|
||||||
clearTimeout(searchTimeout)
|
clearTimeout(searchTimeout)
|
||||||
@@ -828,7 +841,7 @@ function resetForm() {
|
|||||||
async function openCreateDialog() {
|
async function openCreateDialog() {
|
||||||
editingChannel.value = null
|
editingChannel.value = null
|
||||||
resetForm()
|
resetForm()
|
||||||
await loadGroups()
|
await Promise.all([loadGroups(), loadAllChannelsForConflict()])
|
||||||
showDialog.value = true
|
showDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -840,7 +853,7 @@ async function openEditDialog(channel: Channel) {
|
|||||||
form.restrict_models = channel.restrict_models || false
|
form.restrict_models = channel.restrict_models || false
|
||||||
form.billing_model_source = channel.billing_model_source || 'channel_mapped'
|
form.billing_model_source = channel.billing_model_source || 'channel_mapped'
|
||||||
// Must load groups first so apiToForm can map groupID → platform
|
// Must load groups first so apiToForm can map groupID → platform
|
||||||
await loadGroups()
|
await Promise.all([loadGroups(), loadAllChannelsForConflict()])
|
||||||
form.platforms = apiToForm(channel)
|
form.platforms = apiToForm(channel)
|
||||||
showDialog.value = true
|
showDialog.value = true
|
||||||
}
|
}
|
||||||
@@ -985,7 +998,12 @@ async function toggleChannelStatus(channel: Channel) {
|
|||||||
const newStatus = channel.status === 'active' ? 'disabled' : 'active'
|
const newStatus = channel.status === 'active' ? 'disabled' : 'active'
|
||||||
try {
|
try {
|
||||||
await adminAPI.channels.update(channel.id, { status: newStatus })
|
await adminAPI.channels.update(channel.id, { status: newStatus })
|
||||||
|
if (filters.status && filters.status !== newStatus) {
|
||||||
|
// Item no longer matches the active filter — reload list
|
||||||
|
await loadChannels()
|
||||||
|
} else {
|
||||||
channel.status = newStatus
|
channel.status = newStatus
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
appStore.showError(t('admin.channels.updateError', 'Failed to update channel'))
|
appStore.showError(t('admin.channels.updateError', 'Failed to update channel'))
|
||||||
console.error('Error toggling channel status:', error)
|
console.error('Error toggling channel status:', error)
|
||||||
|
|||||||
Reference in New Issue
Block a user