增加7777

This commit is contained in:
zhx 2026-07-23 20:58:48 +08:00
parent d3f29ae57e
commit c169dfb5e7
8 changed files with 289 additions and 1 deletions

View File

@ -0,0 +1,110 @@
package appconfig
import (
"encoding/json"
"errors"
"fmt"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/model"
"gorm.io/gorm"
)
const (
giftConfigGroup = "room-gift"
giftQuantityPresetsKey = "quantity-presets"
giftQuantityPresetMaxCount = 8
giftQuantityPresetMaxValue = 9_999
giftQuantityPresetsDescription = "语音房送礼数量档位,严格递增且每档为 1-9999"
)
var defaultGiftQuantityPresets = []int32{1, 9, 99, 999}
type giftConfigRequest struct {
QuantityPresets []int32 `json:"quantityPresets"`
}
// GiftConfig 是当前 App 的语音房送礼配置;缺少持久化行时仍返回兼容旧客户端的默认档位。
type GiftConfig struct {
AppCode string `json:"appCode"`
QuantityPresets []int32 `json:"quantityPresets"`
UpdatedAtMS int64 `json:"updatedAtMs"`
}
func (s *AppConfigService) GetGiftConfig(appCode string) (GiftConfig, error) {
appCode = appctx.Normalize(appCode)
item, err := s.store.GetOwnedAppConfig(appCode, giftConfigGroup, giftQuantityPresetsKey)
if errors.Is(err, gorm.ErrRecordNotFound) {
return GiftConfig{
AppCode: appCode,
QuantityPresets: cloneGiftQuantityPresets(defaultGiftQuantityPresets),
}, nil
}
if err != nil {
return GiftConfig{}, err
}
return giftConfigFromModel(item)
}
func (s *AppConfigService) UpdateGiftConfig(appCode string, request giftConfigRequest) (GiftConfig, error) {
appCode = appctx.Normalize(appCode)
presets, err := normalizeGiftQuantityPresets(request.QuantityPresets)
if err != nil {
return GiftConfig{}, err
}
raw, err := json.Marshal(presets)
if err != nil {
return GiftConfig{}, err
}
// App 作用域只取鉴权上下文;请求体没有 app_code保存 Lalu 时不会覆盖 Fami 或其他 App。
if err := s.store.UpsertScopedAppConfigs(appCode, []model.AppConfig{{
AppCode: appCode,
Group: giftConfigGroup,
Key: giftQuantityPresetsKey,
Value: string(raw),
Description: giftQuantityPresetsDescription,
}}); err != nil {
return GiftConfig{}, err
}
return s.GetGiftConfig(appCode)
}
func giftConfigFromModel(item model.AppConfig) (GiftConfig, error) {
var presets []int32
if err := json.Unmarshal([]byte(item.Value), &presets); err != nil {
return GiftConfig{}, fmt.Errorf("送礼数量档位配置格式不正确: %w", err)
}
presets, err := normalizeGiftQuantityPresets(presets)
if err != nil {
return GiftConfig{}, err
}
return GiftConfig{
AppCode: appctx.Normalize(item.AppCode),
QuantityPresets: presets,
UpdatedAtMS: item.UpdatedAtMS,
}, nil
}
func normalizeGiftQuantityPresets(values []int32) ([]int32, error) {
if len(values) == 0 || len(values) > giftQuantityPresetMaxCount {
return nil, fmt.Errorf("送礼数量档位必须配置 1-%d 个", giftQuantityPresetMaxCount)
}
normalized := make([]int32, len(values))
var previous int32
for index, value := range values {
if value < 1 || value > giftQuantityPresetMaxValue {
return nil, fmt.Errorf("送礼数量档位必须在 1-%d 之间", giftQuantityPresetMaxValue)
}
if index > 0 && value <= previous {
return nil, errors.New("送礼数量档位必须严格递增且不能重复")
}
normalized[index] = value
previous = value
}
return normalized, nil
}
func cloneGiftQuantityPresets(values []int32) []int32 {
return append([]int32(nil), values...)
}

View File

@ -0,0 +1,43 @@
package appconfig
import (
"fmt"
"strconv"
"strings"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/modules/shared"
"hyapp-admin-server/internal/response"
"github.com/gin-gonic/gin"
)
func (h *Handler) GetGiftConfig(c *gin.Context) {
item, err := h.service.GetGiftConfig(appctx.FromContext(c.Request.Context()))
if err != nil {
response.ServerError(c, "获取送礼配置失败")
return
}
response.OK(c, item)
}
func (h *Handler) UpdateGiftConfig(c *gin.Context) {
var request giftConfigRequest
if err := c.ShouldBindJSON(&request); err != nil {
response.BadRequest(c, "参数不正确")
return
}
appCode := appctx.FromContext(c.Request.Context())
item, err := h.service.UpdateGiftConfig(appCode, request)
if err != nil {
response.BadRequest(c, err.Error())
return
}
// 审计记录最终规范化档位,避免仅有“保存成功”而无法还原当时对客户端下发的数量列表。
parts := make([]string, 0, len(item.QuantityPresets))
for _, value := range item.QuantityPresets {
parts = append(parts, strconv.FormatInt(int64(value), 10))
}
shared.OperationLog(c, h.audit, "update-room-gift-config", "admin_app_configs", "success", fmt.Sprintf("app_code=%s quantity_presets=%s", item.AppCode, strings.Join(parts, ",")))
response.OK(c, item)
}

View File

@ -16,6 +16,8 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
protected.PUT("/admin/app-config/h5-links", middleware.RequirePermission("app-config:update"), h.UpdateH5Links) protected.PUT("/admin/app-config/h5-links", middleware.RequirePermission("app-config:update"), h.UpdateH5Links)
protected.PUT("/admin/app-config/h5-links/:key", middleware.RequirePermission("app-config:update"), h.UpdateH5Link) protected.PUT("/admin/app-config/h5-links/:key", middleware.RequirePermission("app-config:update"), h.UpdateH5Link)
protected.DELETE("/admin/app-config/h5-links/:key", middleware.RequirePermission("app-config:update"), h.DeleteH5Link) protected.DELETE("/admin/app-config/h5-links/:key", middleware.RequirePermission("app-config:update"), h.DeleteH5Link)
protected.GET("/admin/app-config/gift", middleware.RequirePermission("app-config:view"), h.GetGiftConfig)
protected.PUT("/admin/app-config/gift", middleware.RequirePermission("app-config:update"), h.UpdateGiftConfig)
protected.GET("/admin/app-config/gift-combo", middleware.RequirePermission("app-config:view"), h.GetGiftComboConfig) protected.GET("/admin/app-config/gift-combo", middleware.RequirePermission("app-config:view"), h.GetGiftComboConfig)
protected.PUT("/admin/app-config/gift-combo", middleware.RequirePermission("app-config:update"), h.UpdateGiftComboConfig) protected.PUT("/admin/app-config/gift-combo", middleware.RequirePermission("app-config:update"), h.UpdateGiftComboConfig)
protected.GET("/admin/app-config/floating-screen", middleware.RequirePermission("app-config:view"), h.GetFloatingScreenConfig) protected.GET("/admin/app-config/floating-screen", middleware.RequirePermission("app-config:view"), h.GetFloatingScreenConfig)

View File

@ -0,0 +1,25 @@
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
-- 查询与写入都命中 uk_admin_app_config_app_group_key(app_code, group, key),只处理两个常量行,
-- 不扫描礼物、账务或配置事实表。重复执行时保留管理员已经保存的值,避免回滚线上档位。
INSERT INTO admin_app_configs (app_code, `group`, `key`, value, description, is_deleted, created_at_ms, updated_at_ms)
VALUES
('lalu', 'room-gift', 'quantity-presets', '[1,7,77,777,7777]', '语音房送礼数量档位,严格递增且每档为 1-9999', FALSE, @now_ms, @now_ms),
('fami', 'room-gift', 'quantity-presets', '[1,7,77,777,7777]', '语音房送礼数量档位,严格递增且每档为 1-9999', FALSE, @now_ms, @now_ms)
ON DUPLICATE KEY UPDATE id = id;
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms)
SELECT parent.id, '送礼配置', 'app-config-gift', '/app-config/gift', 'gift', 'app-config:view', 74, TRUE, @now_ms, @now_ms
FROM admin_menus parent
WHERE parent.code = 'app-config'
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
title = VALUES(title),
path = VALUES(path),
icon = VALUES(icon),
permission_code = VALUES(permission_code),
sort = VALUES(sort),
visible = VALUES(visible),
updated_at_ms = VALUES(updated_at_ms);

View File

@ -0,0 +1,74 @@
package appconfig
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"strings"
)
const (
giftConfigGroup = "room-gift"
giftQuantityPresetsKey = "quantity-presets"
giftQuantityPresetMaxCount = 8
giftQuantityPresetMaxValue = 9_999
)
const getGiftQuantityPresetsSQL = `
SELECT COALESCE(value, '')
FROM admin_app_configs
WHERE app_code = ? AND ` + "`group`" + ` = ? AND ` + "`key`" + ` = ? AND is_deleted = FALSE
LIMIT 1`
// DefaultGiftQuantityPresets preserves the existing product behavior for Apps that have not saved
// their own room-gift configuration. Callers receive a copy so request-local fallback cannot mutate it.
func DefaultGiftQuantityPresets() []int32 {
return []int32{1, 9, 99, 999}
}
// GiftQuantityPresets resolves only the selected App's row. The three-column unique index makes this
// a bounded point lookup; a missing row is a normal compatibility fallback, not a cross-App baseline.
func (r *MySQLReader) GiftQuantityPresets(ctx context.Context, appCode string) ([]int32, error) {
if r == nil || r.db == nil {
return nil, errors.New("app config reader is not configured")
}
appCode = normalizeAppCode(appCode)
var raw string
err := r.db.QueryRowContext(ctx, getGiftQuantityPresetsSQL, appCode, giftConfigGroup, giftQuantityPresetsKey).Scan(&raw)
if errors.Is(err, sql.ErrNoRows) {
return DefaultGiftQuantityPresets(), nil
}
if err != nil {
return nil, err
}
var presets []int32
if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &presets); err != nil {
return nil, fmt.Errorf("parse gift quantity presets for app %s: %w", appCode, err)
}
return normalizeGiftQuantityPresets(presets)
}
func normalizeGiftQuantityPresets(values []int32) ([]int32, error) {
if len(values) == 0 || len(values) > giftQuantityPresetMaxCount {
return nil, fmt.Errorf("gift quantity presets must contain between 1 and %d values", giftQuantityPresetMaxCount)
}
normalized := make([]int32, len(values))
var previous int32
for index, value := range values {
if value < 1 || value > giftQuantityPresetMaxValue {
return nil, fmt.Errorf("gift quantity preset must be between 1 and %d", giftQuantityPresetMaxValue)
}
if index > 0 && value <= previous {
return nil, errors.New("gift quantity presets must be strictly increasing and unique")
}
normalized[index] = value
previous = value
}
return normalized, nil
}
var _ interface {
GiftQuantityPresets(context.Context, string) ([]int32, error)
} = (*MySQLReader)(nil)

View File

@ -0,0 +1,18 @@
package roomapi
import (
"context"
"hyapp/services/gateway-service/internal/appconfig"
)
type giftQuantityPresetReader interface {
GiftQuantityPresets(ctx context.Context, appCode string) ([]int32, error)
}
func (h *Handler) roomGiftQuantityPresets(ctx context.Context, appCode string) ([]int32, error) {
if h.giftQuantityReader == nil {
return appconfig.DefaultGiftQuantityPresets(), nil
}
return h.giftQuantityReader.GiftQuantityPresets(ctx, appCode)
}

View File

@ -162,6 +162,11 @@ func (h *Handler) serveRoomGiftPanel(writer http.ResponseWriter, request *http.R
// 当前 Flutter 无论是否聚合都继续走 V2不能因配置故障切回旧财务入口。 // 当前 Flutter 无论是否聚合都继续走 V2不能因配置故障切回旧财务入口。
return config, err != nil, err return config, err != nil, err
}) })
quantityPresetsCh := startRoomGiftPanelStage(&stageGroup, totalCtx, requestID, roomID, "quantity_presets", func() ([]int32, bool, error) {
presets, err := h.roomGiftQuantityPresets(totalCtx, app)
// 配置库故障不能阻断礼物面板;降级到旧档位并保留阶段错误,便于区分未配置和运行时依赖异常。
return presets, err != nil, err
})
snapshotResp, _, err := waitRoomGiftPanelStage(totalCtx, snapshotCh) snapshotResp, _, err := waitRoomGiftPanelStage(totalCtx, snapshotCh)
if err != nil { if err != nil {
@ -219,6 +224,14 @@ func (h *Handler) serveRoomGiftPanel(writer http.ResponseWriter, request *http.R
} }
comboConfig = disabledGiftComboFallback() comboConfig = disabledGiftComboFallback()
} }
quantityPresets, _, quantityPresetsErr := waitRoomGiftPanelStage(totalCtx, quantityPresetsCh)
if quantityPresetsErr != nil {
if totalCtx.Err() != nil {
h.writeRoomGiftPanelFailure(writer, request, requestID, roomID, totalStartedAt, quantityPresetsErr)
return
}
quantityPresets = appconfig.DefaultGiftQuantityPresets()
}
panelGifts := append([]giftConfigData{}, panelConfig.Gifts...) panelGifts := append([]giftConfigData{}, panelConfig.Gifts...)
panelGifts = append(panelGifts, roomGiftBagGiftsFromResources(bagResources, panelConfig.Gifts, snapshot.GetVisibleRegionId())...) panelGifts = append(panelGifts, roomGiftBagGiftsFromResources(bagResources, panelConfig.Gifts, snapshot.GetVisibleRegionId())...)
@ -227,7 +240,7 @@ func (h *Handler) serveRoomGiftPanel(writer http.ResponseWriter, request *http.R
Recipients: roomGiftRecipients(snapshot, recipientProfiles), Recipients: roomGiftRecipients(snapshot, recipientProfiles),
Tabs: roomGiftTabs(panelGifts, panelConfig.GiftTypes), Tabs: roomGiftTabs(panelGifts, panelConfig.GiftTypes),
Gifts: panelGifts, Gifts: panelGifts,
QuantityPresets: []int32{1, 9, 99, 999}, QuantityPresets: quantityPresets,
ComboConfig: comboConfig, ComboConfig: comboConfig,
} }
logRoomGiftPanelStage(totalCtx, requestID, roomID, "total", totalStartedAt, nil, false) logRoomGiftPanelStage(totalCtx, requestID, roomID, "total", totalStartedAt, nil, false)

View File

@ -43,6 +43,7 @@ type Handler struct {
userHostClient client.UserHostClient userHostClient client.UserHostClient
appConfigReader appapi.ConfigReader appConfigReader appapi.ConfigReader
giftComboReader giftComboConfigReader giftComboReader giftComboConfigReader
giftQuantityReader giftQuantityPresetReader
walletClient client.WalletClient walletClient client.WalletClient
giftPanelCache *roomGiftPanelConfigCache giftPanelCache *roomGiftPanelConfigCache
growthLevelClient client.GrowthLevelClient growthLevelClient client.GrowthLevelClient
@ -102,6 +103,7 @@ func New(config Config) *Handler {
giftMaxUnits = 5_000 giftMaxUnits = 5_000
} }
comboReader, _ := any(config.AppConfigReader).(giftComboConfigReader) comboReader, _ := any(config.AppConfigReader).(giftComboConfigReader)
giftQuantityReader, _ := any(config.AppConfigReader).(giftQuantityPresetReader)
roomVIPClient, _ := any(config.RoomClient).(client.RoomVIPFeatureClient) roomVIPClient, _ := any(config.RoomClient).(client.RoomVIPFeatureClient)
roomMediaGuard, _ := any(config.RoomGuardClient).(client.RoomMediaGuardClient) roomMediaGuard, _ := any(config.RoomGuardClient).(client.RoomMediaGuardClient)
urlVerifier, _ := any(config.ObjectUploader).(objectURLVerifier) urlVerifier, _ := any(config.ObjectUploader).(objectURLVerifier)
@ -120,6 +122,7 @@ func New(config Config) *Handler {
userHostClient: config.UserHostClient, userHostClient: config.UserHostClient,
appConfigReader: config.AppConfigReader, appConfigReader: config.AppConfigReader,
giftComboReader: comboReader, giftComboReader: comboReader,
giftQuantityReader: giftQuantityReader,
walletClient: config.WalletClient, walletClient: config.WalletClient,
giftPanelCache: newRoomGiftPanelConfigCache(45 * time.Second), giftPanelCache: newRoomGiftPanelConfigCache(45 * time.Second),
growthLevelClient: config.GrowthLevelClient, growthLevelClient: config.GrowthLevelClient,