可配置

This commit is contained in:
zhx 2026-06-22 20:59:47 +08:00
parent ce060acc6e
commit adde820027
9 changed files with 322 additions and 53 deletions

View File

@ -4,15 +4,33 @@ USE hyapp_wallet;
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED); SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
SET @ddl := IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'gift_diamond_ratio_configs' AND COLUMN_NAME = 'coin_return_ratio_percent') = 0,
'ALTER TABLE gift_diamond_ratio_configs ADD COLUMN coin_return_ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT ''收礼人金币返还比例,百分比'' AFTER ratio_percent',
'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
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
WHERE coin_return_ratio_percent = 0.00;
INSERT INTO gift_diamond_ratio_configs ( INSERT INTO gift_diamond_ratio_configs (
app_code, region_id, gift_type_code, status, ratio_percent, 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 created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
) VALUES ) VALUES
('lalu', 0, 'normal', 'active', 100.00, 0, 0, @now_ms, @now_ms), ('lalu', 0, 'normal', 'active', 100.00, 30.00, 0, 0, @now_ms, @now_ms),
('lalu', 0, 'lucky', 'active', 10.00, 0, 0, @now_ms, @now_ms), ('lalu', 0, 'lucky', 'active', 10.00, 10.00, 0, 0, @now_ms, @now_ms),
('lalu', 0, 'super_lucky', 'active', 1.00, 0, 0, @now_ms, @now_ms) ('lalu', 0, 'super_lucky', 'active', 1.00, 1.00, 0, 0, @now_ms, @now_ms)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
status = VALUES(status), status = VALUES(status),
ratio_percent = VALUES(ratio_percent), ratio_percent = VALUES(ratio_percent),
coin_return_ratio_percent = VALUES(coin_return_ratio_percent),
updated_by_admin_id = VALUES(updated_by_admin_id), updated_by_admin_id = VALUES(updated_by_admin_id),
updated_at_ms = VALUES(updated_at_ms); updated_at_ms = VALUES(updated_at_ms);

View File

@ -1,12 +1,13 @@
package giftdiamond package giftdiamond
type ratioDTO struct { type ratioDTO struct {
GiftTypeCode string `json:"giftTypeCode"` GiftTypeCode string `json:"giftTypeCode"`
RatioPercent string `json:"ratioPercent"` RatioPercent string `json:"ratioPercent"`
RegionID int64 `json:"regionId"` ReturnCoinRatioPercent string `json:"returnCoinRatioPercent"`
EffectiveRegionID int64 `json:"effectiveRegionId"` RegionID int64 `json:"regionId"`
Status string `json:"status"` EffectiveRegionID int64 `json:"effectiveRegionId"`
UpdatedAtMS int64 `json:"updatedAtMs"` Status string `json:"status"`
UpdatedAtMS int64 `json:"updatedAtMs"`
} }
type ratioListDTO struct { type ratioListDTO struct {
@ -15,6 +16,7 @@ type ratioListDTO struct {
} }
type updateRequest struct { type updateRequest struct {
RegionID int64 `json:"regionId"` RegionID int64 `json:"regionId"`
Ratios map[string]interface{} `json:"ratios"` Ratios map[string]interface{} `json:"ratios"`
ReturnCoinRatios map[string]interface{} `json:"returnCoinRatios"`
} }

View File

@ -70,7 +70,7 @@ func (h *Handler) Update(c *gin.Context) {
func ratioSummary(items []ratioDTO) []string { func ratioSummary(items []ratioDTO) []string {
result := make([]string, 0, len(items)) result := make([]string, 0, len(items))
for _, item := range items { for _, item := range items {
result = append(result, item.GiftTypeCode+"="+item.RatioPercent) result = append(result, item.GiftTypeCode+"="+item.RatioPercent+"/return="+item.ReturnCoinRatioPercent)
} }
return result return result
} }

View File

@ -54,23 +54,32 @@ func (s *Service) Update(ctx context.Context, appCode string, req updateRequest,
for _, giftType := range giftTypes { for _, giftType := range giftTypes {
raw, exists := req.Ratios[giftType] raw, exists := req.Ratios[giftType]
if !exists { if !exists {
return ratioListDTO{}, fmt.Errorf("%s 比例不能为空", giftType) return ratioListDTO{}, fmt.Errorf("%s 主播钻石比例不能为空", giftType)
} }
ratio, err := normalizePercent(raw) ratio, err := normalizePercent(raw)
if err != nil { if err != nil {
return ratioListDTO{}, fmt.Errorf("%s 比例不正确", giftType) 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, ` if _, err := tx.ExecContext(ctx, `
INSERT INTO gift_diamond_ratio_configs ( INSERT INTO gift_diamond_ratio_configs (
app_code, region_id, gift_type_code, status, ratio_percent, 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 created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?) ) VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
status = VALUES(status), status = VALUES(status),
ratio_percent = VALUES(ratio_percent), ratio_percent = VALUES(ratio_percent),
coin_return_ratio_percent = VALUES(coin_return_ratio_percent),
updated_by_admin_id = VALUES(updated_by_admin_id), updated_by_admin_id = VALUES(updated_by_admin_id),
updated_at_ms = VALUES(updated_at_ms)`, updated_at_ms = VALUES(updated_at_ms)`,
appCode, req.RegionID, giftType, ratio, adminID, adminID, nowMs, nowMs, appCode, req.RegionID, giftType, ratio, returnCoinRatio, adminID, adminID, nowMs, nowMs,
); err != nil { ); err != nil {
return ratioListDTO{}, err return ratioListDTO{}, err
} }
@ -91,17 +100,18 @@ func (s *Service) resolveRatio(ctx context.Context, appCode string, regionID int
return item, err return item, err
} }
} }
return ratioDTO{GiftTypeCode: giftType, RatioPercent: defaultRatioPercent(giftType), RegionID: regionID, EffectiveRegionID: 0, Status: statusActive}, nil 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) { func (s *Service) getRatio(ctx context.Context, appCode string, regionID int64, giftType string) (ratioDTO, bool, error) {
var item ratioDTO var item ratioDTO
var percent string var percent string
var returnCoinPercent string
err := s.db.QueryRowContext(ctx, ` err := s.db.QueryRowContext(ctx, `
SELECT region_id, gift_type_code, CAST(ratio_percent AS CHAR), status, updated_at_ms 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 FROM gift_diamond_ratio_configs
WHERE app_code = ? AND region_id = ? AND gift_type_code = ? WHERE app_code = ? AND region_id = ? AND gift_type_code = ?
LIMIT 1`, appCode, regionID, giftType).Scan(&item.EffectiveRegionID, &item.GiftTypeCode, &percent, &item.Status, &item.UpdatedAtMS) LIMIT 1`, appCode, regionID, giftType).Scan(&item.EffectiveRegionID, &item.GiftTypeCode, &percent, &returnCoinPercent, &item.Status, &item.UpdatedAtMS)
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return ratioDTO{}, false, nil return ratioDTO{}, false, nil
} }
@ -110,6 +120,7 @@ func (s *Service) getRatio(ctx context.Context, appCode string, regionID int64,
} }
item.RegionID = regionID item.RegionID = regionID
item.RatioPercent = formatPercentString(percent) item.RatioPercent = formatPercentString(percent)
item.ReturnCoinRatioPercent = formatPercentStringWithDefault(returnCoinPercent, defaultReturnCoinRatioPercent(giftType))
return item, true, nil return item, true, nil
} }
@ -126,9 +137,13 @@ func normalizePercent(raw interface{}) (string, error) {
} }
func formatPercentString(raw string) string { 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) value, err := strconv.ParseFloat(strings.TrimSpace(raw), 64)
if err != nil { if err != nil {
return "100.00" return fallback
} }
return fmt.Sprintf("%.2f", value) return fmt.Sprintf("%.2f", value)
} }
@ -144,6 +159,17 @@ func defaultRatioPercent(giftType string) string {
} }
} }
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 { func (s *Service) ensureSchema(ctx context.Context) error {
if s == nil || s.db == nil { if s == nil || s.db == nil {
return errors.New("wallet db is not configured") return errors.New("wallet db is not configured")
@ -155,6 +181,7 @@ func (s *Service) ensureSchema(ctx context.Context) error {
gift_type_code VARCHAR(32) NOT NULL COMMENT '礼物类型编码', gift_type_code VARCHAR(32) NOT NULL COMMENT '礼物类型编码',
status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '业务状态', status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '业务状态',
ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 100.00 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', created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建后台用户 ID',
updated_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', created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
@ -164,14 +191,45 @@ func (s *Service) ensureSchema(ctx context.Context) error {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='礼物钻石和房间贡献比例配置表'`); err != nil { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='礼物钻石和房间贡献比例配置表'`); err != nil {
return err 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, ` _, err := s.db.ExecContext(ctx, `
INSERT IGNORE INTO gift_diamond_ratio_configs ( INSERT IGNORE INTO gift_diamond_ratio_configs (
app_code, region_id, gift_type_code, status, ratio_percent, 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 created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
) VALUES ) VALUES
(?, 0, 'normal', 'active', 100.00, 0, 0, 0, 0), (?, 0, 'normal', 'active', 100.00, 30.00, 0, 0, 0, 0),
(?, 0, 'lucky', 'active', 10.00, 0, 0, 0, 0), (?, 0, 'lucky', 'active', 10.00, 10.00, 0, 0, 0, 0),
(?, 0, 'super_lucky', 'active', 1.00, 0, 0, 0, 0)`, (?, 0, 'super_lucky', 'active', 1.00, 1.00, 0, 0, 0, 0)`,
"lalu", "lalu", "lalu") "lalu", "lalu", "lalu")
return err 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
}

View File

@ -0,0 +1,26 @@
package giftdiamond
import "testing"
func TestDefaultReturnCoinRatioPercentByGiftType(t *testing.T) {
cases := map[string]string{
"normal": "30.00",
"lucky": "10.00",
"super_lucky": "1.00",
"": "30.00",
}
for giftType, want := range cases {
if got := defaultReturnCoinRatioPercent(giftType); got != want {
t.Fatalf("defaultReturnCoinRatioPercent(%q) = %s, want %s", giftType, got, want)
}
}
}
func TestFormatReturnCoinRatioUsesGiftTypeDefault(t *testing.T) {
if got := formatPercentStringWithDefault("", defaultReturnCoinRatioPercent("normal")); got != "30.00" {
t.Fatalf("normal return coin fallback = %s, want 30.00", got)
}
if got := formatPercentStringWithDefault("12.345", defaultReturnCoinRatioPercent("lucky")); got != "12.35" {
t.Fatalf("return coin rounding = %s, want 12.35", got)
}
}

View File

@ -1213,6 +1213,7 @@ CREATE TABLE IF NOT EXISTS gift_diamond_ratio_configs (
gift_type_code VARCHAR(32) NOT NULL COMMENT '礼物类型编码', gift_type_code VARCHAR(32) NOT NULL COMMENT '礼物类型编码',
status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '业务状态', status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '业务状态',
ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 100.00 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', created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建后台用户 ID',
updated_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', created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
@ -1452,9 +1453,9 @@ INSERT IGNORE INTO gift_type_configs (
('lalu', 'custom', '定制礼物', 'Custom', 'active', 100, 0, 0, 0, 0); ('lalu', 'custom', '定制礼物', 'Custom', 'active', 100, 0, 0, 0, 0);
INSERT IGNORE INTO gift_diamond_ratio_configs ( INSERT IGNORE INTO gift_diamond_ratio_configs (
app_code, region_id, gift_type_code, status, ratio_percent, 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 created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
) VALUES ) VALUES
('lalu', 0, 'normal', 'active', 100.00, 0, 0, 0, 0), ('lalu', 0, 'normal', 'active', 100.00, 30.00, 0, 0, 0, 0),
('lalu', 0, 'lucky', 'active', 10.00, 0, 0, 0, 0), ('lalu', 0, 'lucky', 'active', 10.00, 10.00, 0, 0, 0, 0),
('lalu', 0, 'super_lucky', 'active', 1.00, 0, 0, 0, 0); ('lalu', 0, 'super_lucky', 'active', 1.00, 1.00, 0, 0, 0, 0);

View File

@ -345,6 +345,43 @@ func TestDebitGiftAppliesGlobalDefaultGiftDiamondRatioToHeatValueByGiftType(t *t
} }
} }
func TestDebitGiftUsesRegionalReturnCoinRatioWithoutChangingHostDiamonds(t *testing.T) {
repository := mysqltest.NewRepository(t)
svc := walletservice.New(repository)
repository.SetBalance(14501, 500)
repository.SetGiftPrice("regional-return-gift", "v1", 100, 100, 100)
repository.SetGiftType("regional-return-gift", resourcedomain.GiftTypeNormal)
repository.SetGiftDiamondRatio(7701, resourcedomain.GiftTypeNormal, "80.00")
repository.SetGiftReturnCoinRatio(7701, resourcedomain.GiftTypeNormal, "45.00")
receipt, err := svc.DebitGift(context.Background(), ledger.DebitGiftCommand{
CommandID: "cmd-regional-return-coin",
RoomID: "room-regional-return",
SenderUserID: 14501,
SenderRegionID: 1001,
RegionID: 7701,
TargetUserID: 14502,
GiftID: "regional-return-gift",
GiftCount: 1,
PriceVersion: "v1",
TargetIsHost: true,
TargetHostRegionID: 7701,
})
if err != nil {
t.Fatalf("DebitGift regional return failed: %v", err)
}
if receipt.HeatValue != 100 || receipt.HostPeriodDiamondAdded != 100 {
t.Fatalf("return coin ratio must not change heat or host diamonds: %+v", receipt)
}
balances, err := svc.GetBalances(context.Background(), 14502, []string{ledger.AssetCoin})
if err != nil {
t.Fatalf("GetBalances target income failed: %v", err)
}
if balanceAmount(balances, ledger.AssetCoin) != 45 {
t.Fatalf("regional return coin income mismatch: %+v", balances)
}
}
func TestBatchDebitGiftAppliesGlobalDefaultGiftDiamondRatioToEachTargetHeatValue(t *testing.T) { func TestBatchDebitGiftAppliesGlobalDefaultGiftDiamondRatioToEachTargetHeatValue(t *testing.T) {
repository := mysqltest.NewRepository(t) repository := mysqltest.NewRepository(t)
repository.SetBalance(15001, 1000) repository.SetBalance(15001, 1000)

View File

@ -56,8 +56,6 @@ const (
giftWallChargeAssetMixed = "MIXED" giftWallChargeAssetMixed = "MIXED"
giftChargeSourceCoin = "coin" giftChargeSourceCoin = "coin"
giftChargeSourceBag = "bag" giftChargeSourceBag = "bag"
normalGiftIncomeRatioPercent = "30.00"
normalGiftIncomeRatioPPM = 300_000
) )
// Repository 是 wallet-service 的 MySQL v2 账本入口。 // Repository 是 wallet-service 的 MySQL v2 账本入口。
@ -267,6 +265,7 @@ func ensureGiftDiamondRatioSchema(ctx context.Context, db *sql.DB) error {
gift_type_code VARCHAR(32) NOT NULL COMMENT '礼物类型编码', gift_type_code VARCHAR(32) NOT NULL COMMENT '礼物类型编码',
status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '业务状态', status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '业务状态',
ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 100.00 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', created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建后台用户 ID',
updated_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', created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
@ -276,14 +275,47 @@ func ensureGiftDiamondRatioSchema(ctx context.Context, db *sql.DB) error {
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='礼物钻石和房间贡献比例配置表'`); err != nil { ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='礼物钻石和房间贡献比例配置表'`); err != nil {
return err return err
} }
if added, err := ensureGiftReturnCoinRatioColumn(ctx, db); err != nil {
return err
} else if added {
// 老库新增列后按礼物类型补业务默认值;只在列首次创建时执行,避免覆盖后台后续配置。
if err := seedGiftReturnCoinRatioDefaults(ctx, db); err != nil {
return err
}
}
_, err := db.ExecContext(ctx, ` _, err := db.ExecContext(ctx, `
INSERT IGNORE INTO gift_diamond_ratio_configs ( INSERT IGNORE INTO gift_diamond_ratio_configs (
app_code, region_id, gift_type_code, status, ratio_percent, 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 created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
) VALUES ) VALUES
('lalu', 0, 'normal', 'active', 100.00, 0, 0, 0, 0), ('lalu', 0, 'normal', 'active', 100.00, 30.00, 0, 0, 0, 0),
('lalu', 0, 'lucky', 'active', 10.00, 0, 0, 0, 0), ('lalu', 0, 'lucky', 'active', 10.00, 10.00, 0, 0, 0, 0),
('lalu', 0, 'super_lucky', 'active', 1.00, 0, 0, 0, 0)`) ('lalu', 0, 'super_lucky', 'active', 1.00, 1.00, 0, 0, 0, 0)`)
return err
}
func ensureGiftReturnCoinRatioColumn(ctx context.Context, db *sql.DB) (bool, error) {
// 收礼金币返还和主播周期钻石是两套业务口径,同表按礼物类型和区域管理,但不能复用同一个比例列。
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 isDuplicateColumnError(err) {
return false, nil
}
return false, err
}
return true, nil
}
func seedGiftReturnCoinRatioDefaults(ctx context.Context, db *sql.DB) error {
// 新列上线时历史区域行也要拿到对应礼物类型的默认返还比例,否则已有区域配置会把收礼收入算成 0。
_, 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 return err
} }
@ -449,10 +481,13 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
if err != nil { if err != nil {
return ledger.Receipt{}, err return ledger.Receipt{}, err
} }
giftIncomeRatio, giftIncomeRatioRegionID := giftIncomeRatio(giftConfig.GiftTypeCode, roomContributionRatio, roomContributionRatioRegionID) giftIncomeRatio, giftIncomeRatioRegionID, err := r.resolveGiftReturnCoinRatio(ctx, tx, command.AppCode, command.RegionID, giftConfig.GiftTypeCode)
if err != nil {
return ledger.Receipt{}, err
}
giftIncomeCoinAmount := int64(0) giftIncomeCoinAmount := int64(0)
if !command.RobotGift { if !command.RobotGift {
// 收礼金币收入只属于真人送礼经济账;背包礼物也按礼物服务端价值发放,机器人礼物继续只服务房间展示。 // 收礼金币收入只属于真人送礼经济账;比例优先使用房间可见区域配置,背包礼物也按礼物服务端价值发放。
giftIncomeCoinAmount, err = giftDiamondAmount(chargeAmount, giftIncomeRatio.PPM) giftIncomeCoinAmount, err = giftDiamondAmount(chargeAmount, giftIncomeRatio.PPM)
if err != nil { if err != nil {
return ledger.Receipt{}, err return ledger.Receipt{}, err
@ -760,7 +795,10 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb
if err != nil { if err != nil {
return ledger.BatchGiftReceipt{}, err return ledger.BatchGiftReceipt{}, err
} }
giftIncomeRatio, giftIncomeRatioRegionID := giftIncomeRatio(giftConfig.GiftTypeCode, roomContributionRatio, roomContributionRatioRegionID) giftIncomeRatio, giftIncomeRatioRegionID, err := r.resolveGiftReturnCoinRatio(ctx, tx, command.AppCode, command.RegionID, giftConfig.GiftTypeCode)
if err != nil {
return ledger.BatchGiftReceipt{}, err
}
giftIncomeCoinAmount, err := giftDiamondAmount(chargeAmount, giftIncomeRatio.PPM) giftIncomeCoinAmount, err := giftDiamondAmount(chargeAmount, giftIncomeRatio.PPM)
if err != nil { if err != nil {
return ledger.BatchGiftReceipt{}, err return ledger.BatchGiftReceipt{}, err
@ -2776,6 +2814,53 @@ func (r *Repository) getGiftDiamondRatio(ctx context.Context, tx *sql.Tx, appCod
return giftDiamondRatioSnapshot{Percent: percent, PPM: ppm}, true, nil return giftDiamondRatioSnapshot{Percent: percent, PPM: ppm}, true, nil
} }
func (r *Repository) resolveGiftReturnCoinRatio(ctx context.Context, tx *sql.Tx, appCode string, regionID int64, giftTypeCode string) (giftDiamondRatioSnapshot, int64, error) {
giftTypeCode = strings.TrimSpace(giftTypeCode)
if giftTypeCode == "" {
giftTypeCode = "normal"
}
regions := []int64{regionID}
if regionID != 0 {
regions = append(regions, 0)
}
for _, regionID := range regions {
ratio, exists, err := r.getGiftReturnCoinRatio(ctx, tx, appCode, regionID, giftTypeCode)
if err != nil {
return giftDiamondRatioSnapshot{}, 0, err
}
if exists {
return ratio, regionID, nil
}
}
return defaultGiftReturnCoinRatio(giftTypeCode), 0, nil
}
func (r *Repository) getGiftReturnCoinRatio(ctx context.Context, tx *sql.Tx, appCode string, regionID int64, giftTypeCode string) (giftDiamondRatioSnapshot, bool, error) {
var percent string
var ppm int64
err := tx.QueryRowContext(ctx, `
SELECT CAST(coin_return_ratio_percent AS CHAR), CAST(ROUND(coin_return_ratio_percent * 10000) AS SIGNED)
FROM gift_diamond_ratio_configs
WHERE app_code = ? AND region_id = ? AND gift_type_code = ? AND status = 'active'
LIMIT 1`,
appcode.Normalize(appCode), regionID, giftTypeCode,
).Scan(&percent, &ppm)
if errors.Is(err, sql.ErrNoRows) {
return giftDiamondRatioSnapshot{}, false, nil
}
if err != nil {
return giftDiamondRatioSnapshot{}, false, err
}
if ppm < 0 || ppm > 1_000_000 {
return giftDiamondRatioSnapshot{}, false, xerr.New(xerr.InvalidArgument, "gift return coin ratio is invalid")
}
percent = strings.TrimSpace(percent)
if percent == "" {
percent = defaultGiftReturnCoinRatio(giftTypeCode).Percent
}
return giftDiamondRatioSnapshot{Percent: percent, PPM: ppm}, true, nil
}
func giftDiamondAmount(chargeAmount int64, ratioPPM int64) (int64, error) { func giftDiamondAmount(chargeAmount int64, ratioPPM int64) (int64, error) {
if chargeAmount < 0 || ratioPPM < 0 { if chargeAmount < 0 || ratioPPM < 0 {
return 0, xerr.New(xerr.InvalidArgument, "gift diamond amount is invalid") return 0, xerr.New(xerr.InvalidArgument, "gift diamond amount is invalid")
@ -2787,17 +2872,6 @@ func giftDiamondAmount(chargeAmount int64, ratioPPM int64) (int64, error) {
return product / 1_000_000, nil return product / 1_000_000, nil
} }
func giftIncomeRatio(giftTypeCode string, configuredRatio giftDiamondRatioSnapshot, configuredRegionID int64) (giftDiamondRatioSnapshot, int64) {
switch strings.TrimSpace(giftTypeCode) {
case resourcedomain.GiftTypeLucky, resourcedomain.GiftTypeSuperLucky:
// 幸运和超级幸运礼物继续使用后台全局比例,默认分别是 10% 和 1%;这里不复用普通礼物新增的 30% 收礼收入。
return configuredRatio, configuredRegionID
default:
// 普通礼物收礼金币收入是本次新增固定口径;它不影响房间热度和主播周期钻石的后台配置比例。
return giftDiamondRatioSnapshot{Percent: normalGiftIncomeRatioPercent, PPM: normalGiftIncomeRatioPPM}, 0
}
}
func defaultGiftDiamondRatio(giftTypeCode string) giftDiamondRatioSnapshot { func defaultGiftDiamondRatio(giftTypeCode string) giftDiamondRatioSnapshot {
switch strings.TrimSpace(giftTypeCode) { switch strings.TrimSpace(giftTypeCode) {
case "lucky": case "lucky":
@ -2809,6 +2883,17 @@ func defaultGiftDiamondRatio(giftTypeCode string) giftDiamondRatioSnapshot {
} }
} }
func defaultGiftReturnCoinRatio(giftTypeCode string) giftDiamondRatioSnapshot {
switch strings.TrimSpace(giftTypeCode) {
case resourcedomain.GiftTypeLucky:
return giftDiamondRatioSnapshot{Percent: "10.00", PPM: 100_000}
case resourcedomain.GiftTypeSuperLucky:
return giftDiamondRatioSnapshot{Percent: "1.00", PPM: 10_000}
default:
return giftDiamondRatioSnapshot{Percent: "30.00", PPM: 300_000}
}
}
func (r *Repository) resolveRechargePolicy(ctx context.Context, tx *sql.Tx, regionID int64, nowMs int64) (rechargePolicy, error) { func (r *Repository) resolveRechargePolicy(ctx context.Context, tx *sql.Tx, regionID int64, nowMs int64) (rechargePolicy, error) {
row := tx.QueryRowContext(ctx, row := tx.QueryRowContext(ctx,
`SELECT policy_id, region_id, policy_version, currency_code, coin_amount, usd_minor_amount, effective_from_ms `SELECT policy_id, region_id, policy_version, currency_code, coin_amount, usd_minor_amount, effective_from_ms

View File

@ -5,6 +5,7 @@ import (
"context" "context"
"database/sql" "database/sql"
"fmt" "fmt"
"strings"
"testing" "testing"
"time" "time"
@ -494,19 +495,60 @@ func (r *Repository) SetGiftDiamondRatio(regionID int64, giftTypeCode string, ra
nowMs := time.Now().UnixMilli() nowMs := time.Now().UnixMilli()
_, err := r.schema.DB.ExecContext(context.Background(), ` _, err := r.schema.DB.ExecContext(context.Background(), `
INSERT INTO gift_diamond_ratio_configs ( INSERT INTO gift_diamond_ratio_configs (
region_id, gift_type_code, status, ratio_percent, 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 created_by_admin_id, updated_by_admin_id, created_at_ms, updated_at_ms
) VALUES (?, ?, 'active', ?, 0, 0, ?, ?) ) VALUES (?, ?, 'active', ?, ?, 0, 0, ?, ?)
ON DUPLICATE KEY UPDATE ON DUPLICATE KEY UPDATE
status = VALUES(status), status = VALUES(status),
ratio_percent = VALUES(ratio_percent), ratio_percent = VALUES(ratio_percent),
updated_at_ms = VALUES(updated_at_ms)`, updated_at_ms = VALUES(updated_at_ms)`,
regionID, giftTypeCode, ratioPercent, nowMs, nowMs) regionID, giftTypeCode, ratioPercent, defaultGiftReturnCoinRatioPercent(giftTypeCode), nowMs, nowMs)
if err != nil { if err != nil {
r.t.Fatalf("set gift diamond ratio failed: %v", err) r.t.Fatalf("set gift diamond ratio failed: %v", err)
} }
} }
// SetGiftReturnCoinRatio configures the receiver COIN return ratio without changing host-period diamond rules.
func (r *Repository) SetGiftReturnCoinRatio(regionID int64, giftTypeCode string, ratioPercent string) {
r.t.Helper()
nowMs := time.Now().UnixMilli()
_, err := r.schema.DB.ExecContext(context.Background(), `
INSERT INTO gift_diamond_ratio_configs (
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', ?, ?, 0, 0, ?, ?)
ON DUPLICATE KEY UPDATE
status = VALUES(status),
coin_return_ratio_percent = VALUES(coin_return_ratio_percent),
updated_at_ms = VALUES(updated_at_ms)`,
regionID, giftTypeCode, defaultGiftDiamondRatioPercent(giftTypeCode), ratioPercent, nowMs, nowMs)
if err != nil {
r.t.Fatalf("set gift return coin ratio failed: %v", err)
}
}
func defaultGiftDiamondRatioPercent(giftTypeCode string) string {
switch strings.TrimSpace(giftTypeCode) {
case "lucky":
return "10.00"
case "super_lucky":
return "1.00"
default:
return "100.00"
}
}
func defaultGiftReturnCoinRatioPercent(giftTypeCode string) string {
switch strings.TrimSpace(giftTypeCode) {
case "lucky":
return "10.00"
case "super_lucky":
return "1.00"
default:
return "30.00"
}
}
// SetRechargePolicy 配置区域充值定价,用于外部充值类事实测试。 // SetRechargePolicy 配置区域充值定价,用于外部充值类事实测试。
func (r *Repository) SetRechargePolicy(regionID int64, policyVersion string, coinAmount int64, usdMinorAmount int64) { func (r *Repository) SetRechargePolicy(regionID int64, policyVersion string, coinAmount int64, usdMinorAmount int64) {
r.t.Helper() r.t.Helper()