236 lines
8.1 KiB
Go
236 lines
8.1 KiB
Go
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
|
||
}
|