package giftdiamond import ( "context" "database/sql" "errors" "fmt" "math" "strconv" "strings" "time" ) const statusActive = "active" var giftTypes = []string{"normal", "lucky", "super_lucky"} type Service struct { db *sql.DB } func NewService(db *sql.DB) *Service { return &Service{db: db} } func (s *Service) List(ctx context.Context, appCode string, regionID int64) (ratioListDTO, error) { if err := s.ensureSchema(ctx); err != nil { return ratioListDTO{}, err } items := make([]ratioDTO, 0, len(giftTypes)) for _, giftType := range giftTypes { item, err := s.resolveRatio(ctx, appCode, regionID, giftType) if err != nil { return ratioListDTO{}, err } items = append(items, item) } return ratioListDTO{RegionID: regionID, Items: items}, nil } func (s *Service) Update(ctx context.Context, appCode string, req updateRequest, adminID int64) (ratioListDTO, error) { if req.RegionID < 0 { return ratioListDTO{}, errors.New("区域不正确") } if err := s.ensureSchema(ctx); err != nil { return ratioListDTO{}, err } nowMs := time.Now().UnixMilli() tx, err := s.db.BeginTx(ctx, nil) if err != nil { return ratioListDTO{}, err } defer func() { _ = tx.Rollback() }() for _, giftType := range giftTypes { raw, exists := req.Ratios[giftType] if !exists { return ratioListDTO{}, fmt.Errorf("%s 主播钻石比例不能为空", giftType) } ratio, err := normalizePercent(raw) if err != nil { return ratioListDTO{}, fmt.Errorf("%s 主播钻石比例不正确", giftType) } rawReturnCoin, exists := req.ReturnCoinRatios[giftType] if !exists { return ratioListDTO{}, fmt.Errorf("%s 返还金币比例不能为空", giftType) } returnCoinRatio, err := normalizePercent(rawReturnCoin) if err != nil { return ratioListDTO{}, fmt.Errorf("%s 返还金币比例不正确", giftType) } if _, err := tx.ExecContext(ctx, ` INSERT INTO gift_diamond_ratio_configs ( app_code, region_id, gift_type_code, status, ratio_percent, coin_return_ratio_percent, created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms ) VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE status = VALUES(status), ratio_percent = VALUES(ratio_percent), coin_return_ratio_percent = VALUES(coin_return_ratio_percent), updated_by_admin_id = VALUES(updated_by_admin_id), updated_at_ms = VALUES(updated_at_ms)`, appCode, req.RegionID, giftType, ratio, returnCoinRatio, adminID, adminID, nowMs, nowMs, ); err != nil { return ratioListDTO{}, err } } if err := tx.Commit(); err != nil { return ratioListDTO{}, err } return s.List(ctx, appCode, req.RegionID) } func (s *Service) resolveRatio(ctx context.Context, appCode string, regionID int64, giftType string) (ratioDTO, error) { if item, exists, err := s.getRatio(ctx, appCode, regionID, giftType); err != nil || exists { return item, err } if regionID != 0 { if item, exists, err := s.getRatio(ctx, appCode, 0, giftType); err != nil || exists { item.RegionID = regionID return item, err } } return ratioDTO{GiftTypeCode: giftType, RatioPercent: defaultRatioPercent(giftType), ReturnCoinRatioPercent: defaultReturnCoinRatioPercent(giftType), RegionID: regionID, EffectiveRegionID: 0, Status: statusActive}, nil } func (s *Service) getRatio(ctx context.Context, appCode string, regionID int64, giftType string) (ratioDTO, bool, error) { var item ratioDTO var percent string var returnCoinPercent string err := s.db.QueryRowContext(ctx, ` SELECT region_id, gift_type_code, CAST(ratio_percent AS CHAR), CAST(coin_return_ratio_percent AS CHAR), status, updated_at_ms FROM gift_diamond_ratio_configs WHERE app_code = ? AND region_id = ? AND gift_type_code = ? LIMIT 1`, appCode, regionID, giftType).Scan(&item.EffectiveRegionID, &item.GiftTypeCode, &percent, &returnCoinPercent, &item.Status, &item.UpdatedAtMS) if errors.Is(err, sql.ErrNoRows) { return ratioDTO{}, false, nil } if err != nil { return ratioDTO{}, false, err } item.RegionID = regionID item.RatioPercent = formatPercentString(percent) item.ReturnCoinRatioPercent = formatPercentStringWithDefault(returnCoinPercent, defaultReturnCoinRatioPercent(giftType)) return item, true, nil } func normalizePercent(raw interface{}) (string, error) { text := strings.TrimSpace(fmt.Sprint(raw)) if text == "" { return "", errors.New("empty") } value, err := strconv.ParseFloat(text, 64) if err != nil || math.IsNaN(value) || math.IsInf(value, 0) || value < 0 || value > 100 { return "", errors.New("invalid") } return fmt.Sprintf("%.2f", value), nil } func formatPercentString(raw string) string { return formatPercentStringWithDefault(raw, "100.00") } func formatPercentStringWithDefault(raw string, fallback string) string { value, err := strconv.ParseFloat(strings.TrimSpace(raw), 64) if err != nil { return fallback } return fmt.Sprintf("%.2f", value) } func defaultRatioPercent(giftType string) string { switch strings.TrimSpace(giftType) { case "lucky": return "10.00" case "super_lucky": return "1.00" default: return "100.00" } } func defaultReturnCoinRatioPercent(giftType string) string { switch strings.TrimSpace(giftType) { case "lucky": return "10.00" case "super_lucky": return "1.00" default: return "30.00" } } func (s *Service) ensureSchema(ctx context.Context) error { if s == nil || s.db == nil { return errors.New("wallet db is not configured") } if _, err := s.db.ExecContext(ctx, ` CREATE TABLE IF NOT EXISTS gift_diamond_ratio_configs ( app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离', region_id BIGINT NOT NULL DEFAULT 0 COMMENT '房间可见区域,0 表示全局默认', gift_type_code VARCHAR(32) NOT NULL COMMENT '礼物类型编码', status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '业务状态', ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 100.00 COMMENT '房间贡献热度和主播周期钻石入账比例,百分比', coin_return_ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT '收礼人金币返还比例,百分比', created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建后台用户 ID', updated_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '更新后台用户 ID', created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms', updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms', PRIMARY KEY (app_code, region_id, gift_type_code), KEY idx_gift_diamond_ratio_region (app_code, region_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='礼物钻石和房间贡献比例配置表'`); err != nil { return err } if added, err := ensureReturnCoinRatioColumn(ctx, s.db); err != nil { return err } else if added { if err := seedReturnCoinRatioDefaults(ctx, s.db); err != nil { return err } } _, err := s.db.ExecContext(ctx, ` INSERT IGNORE INTO gift_diamond_ratio_configs ( app_code, region_id, gift_type_code, status, ratio_percent, coin_return_ratio_percent, created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms ) VALUES (?, 0, 'normal', 'active', 100.00, 30.00, 0, 0, 0, 0), (?, 0, 'lucky', 'active', 10.00, 10.00, 0, 0, 0, 0), (?, 0, 'super_lucky', 'active', 1.00, 1.00, 0, 0, 0, 0)`, "lalu", "lalu", "lalu") return err } func ensureReturnCoinRatioColumn(ctx context.Context, db *sql.DB) (bool, error) { // 该列和 ratio_percent 共用区域/礼物类型维度,但服务于收礼人金币收入,避免后台调整主播钻石时误改用户返还。 if _, err := db.ExecContext(ctx, ` ALTER TABLE gift_diamond_ratio_configs ADD COLUMN coin_return_ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT '收礼人金币返还比例,百分比' AFTER ratio_percent`); err != nil { if strings.Contains(strings.ToLower(err.Error()), "duplicate column") { return false, nil } return false, err } return true, nil } func seedReturnCoinRatioDefaults(ctx context.Context, db *sql.DB) error { _, err := db.ExecContext(ctx, ` UPDATE gift_diamond_ratio_configs SET coin_return_ratio_percent = CASE gift_type_code WHEN 'lucky' THEN 10.00 WHEN 'super_lucky' THEN 1.00 ELSE 30.00 END`) return err }