feat(channels): gate available channels behind feature switch (backend)

Add a DB-backed soft switch "available_channels_enabled" controlling
the user-facing /channels/available endpoint and sidebar entry. Default
to false (opt-in) — the feature stays invisible until an admin enables
it under Admin Settings > Features.

- domain_constants: SettingKeyAvailableChannelsEnabled
- settings_view: AllSettings/PublicSettings + AvailableChannelsEnabled
- setting_service: public+all read/write, seed default "false",
  GetAvailableChannelsRuntime helper (fail-closed on read error)
- admin setting_handler: UpdateSettingsRequest *bool + update branch
  + audit diff entry
- public setting_handler: expose via GET /api/v1/settings
- available_channel_handler: featureEnabled() guard — returns empty
  list after auth when disabled (401 precedes the feature check to
  preserve existing behavior)
This commit is contained in:
erio
2026-04-21 17:23:20 +08:00
parent 59290e39f9
commit 9ba42aa556
8 changed files with 84 additions and 1 deletions

View File

@@ -21,19 +21,30 @@ import (
type AvailableChannelHandler struct {
channelService *service.ChannelService
apiKeyService *service.APIKeyService
settingService *service.SettingService
}
// NewAvailableChannelHandler 创建用户侧可用渠道 handler。
func NewAvailableChannelHandler(
channelService *service.ChannelService,
apiKeyService *service.APIKeyService,
settingService *service.SettingService,
) *AvailableChannelHandler {
return &AvailableChannelHandler{
channelService: channelService,
apiKeyService: apiKeyService,
settingService: settingService,
}
}
// featureEnabled 返回 available-channels 开关是否启用。默认关闭opt-in
func (h *AvailableChannelHandler) featureEnabled(c *gin.Context) bool {
if h.settingService == nil {
return false
}
return h.settingService.GetAvailableChannelsRuntime(c.Request.Context()).Enabled
}
// userAvailableGroup 用户可见的分组概要(白名单字段)。
type userAvailableGroup struct {
ID int64 `json:"id"`
@@ -89,6 +100,13 @@ func (h *AvailableChannelHandler) List(c *gin.Context) {
return
}
// Feature 未启用时返回空数组(不暴露渠道信息)。检查放在认证之后,
// 保持与未开关前的 401 行为一致:未登录先 401登录后再按开关决定。
if !h.featureEnabled(c) {
response.Success(c, []userAvailableChannel{})
return
}
userGroups, err := h.apiKeyService.GetAvailableGroups(c.Request.Context(), subject.UserID)
if err != nil {
response.ErrorFrom(c, err)