2026-06-22 20:59:47 +08:00

236 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}