"vscode:/vscode.git/clone" did not exist on "5c203ce6c6b969af2503934d022d964cc99caf7f"
Commit feb6999d authored by erio's avatar erio
Browse files

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
parent 71f61bbc
...@@ -188,7 +188,7 @@ func (r *channelRepository) List(ctx context.Context, params pagination.Paginati ...@@ -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) ...@@ -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) { ...@@ -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') ...@@ -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[] { ...@@ -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() { ...@@ -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() { ...@@ -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) { ...@@ -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) { ...@@ -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 })
channel.status = newStatus if (filters.status && filters.status !== newStatus) {
// Item no longer matches the active filter — reload list
await loadChannels()
} else {
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)
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment