可配置

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 @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 (
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
) VALUES
('lalu', 0, 'normal', 'active', 100.00, 0, 0, @now_ms, @now_ms),
('lalu', 0, 'lucky', 'active', 10.00, 0, 0, @now_ms, @now_ms),
('lalu', 0, 'super_lucky', 'active', 1.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, 10.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
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);

View File

@ -1,12 +1,13 @@
package giftdiamond
type ratioDTO struct {
GiftTypeCode string `json:"giftTypeCode"`
RatioPercent string `json:"ratioPercent"`
RegionID int64 `json:"regionId"`
EffectiveRegionID int64 `json:"effectiveRegionId"`
Status string `json:"status"`
UpdatedAtMS int64 `json:"updatedAtMs"`
GiftTypeCode string `json:"giftTypeCode"`
RatioPercent string `json:"ratioPercent"`
ReturnCoinRatioPercent string `json:"returnCoinRatioPercent"`
RegionID int64 `json:"regionId"`
EffectiveRegionID int64 `json:"effectiveRegionId"`
Status string `json:"status"`
UpdatedAtMS int64 `json:"updatedAtMs"`
}
type ratioListDTO struct {
@ -15,6 +16,7 @@ type ratioListDTO struct {
}
type updateRequest struct {
RegionID int64 `json:"regionId"`
Ratios map[string]interface{} `json:"ratios"`
RegionID int64 `json:"regionId"`
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 {
result := make([]string, 0, len(items))
for _, item := range items {
result = append(result, item.GiftTypeCode+"="+item.RatioPercent)
result = append(result, item.GiftTypeCode+"="+item.RatioPercent+"/return="+item.ReturnCoinRatioPercent)
}
return result
}

View File

@ -54,23 +54,32 @@ func (s *Service) Update(ctx context.Context, appCode string, req updateRequest,
for _, giftType := range giftTypes {
raw, exists := req.Ratios[giftType]
if !exists {
return ratioListDTO{}, fmt.Errorf("%s 比例不能为空", giftType)
return ratioListDTO{}, fmt.Errorf("%s 主播钻石比例不能为空", giftType)
}
ratio, err := normalizePercent(raw)
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, `
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
) VALUES (?, ?, ?, 'active', ?, ?, ?, ?, ?)
) 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, adminID, adminID, nowMs, nowMs,
appCode, req.RegionID, giftType, ratio, returnCoinRatio, adminID, adminID, nowMs, nowMs,
); err != nil {
return ratioListDTO{}, err
}
@ -91,17 +100,18 @@ func (s *Service) resolveRatio(ctx context.Context, appCode string, regionID int
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) {
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), 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
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) {
return ratioDTO{}, false, nil
}
@ -110,6 +120,7 @@ func (s *Service) getRatio(ctx context.Context, appCode string, regionID int64,
}
item.RegionID = regionID
item.RatioPercent = formatPercentString(percent)
item.ReturnCoinRatioPercent = formatPercentStringWithDefault(returnCoinPercent, defaultReturnCoinRatioPercent(giftType))
return item, true, nil
}
@ -126,9 +137,13 @@ func normalizePercent(raw interface{}) (string, error) {
}
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 "100.00"
return fallback
}
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 {
if s == nil || s.db == nil {
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 '礼物类型编码',
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',
@ -164,14 +191,45 @@ func (s *Service) ensureSchema(ctx context.Context) error {
) 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,
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, 0, 0, 0, 0),
(?, 0, 'lucky', 'active', 10.00, 0, 0, 0, 0),
(?, 0, 'super_lucky', 'active', 1.00, 0, 0, 0, 0)`,
(?, 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
}

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 '礼物类型编码',
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',
@ -1452,9 +1453,9 @@ INSERT IGNORE INTO gift_type_configs (
('lalu', 'custom', '定制礼物', 'Custom', 'active', 100, 0, 0, 0, 0);
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
) VALUES
('lalu', 0, 'normal', 'active', 100.00, 0, 0, 0, 0),
('lalu', 0, 'lucky', 'active', 10.00, 0, 0, 0, 0),
('lalu', 0, 'super_lucky', 'active', 1.00, 0, 0, 0, 0);
('lalu', 0, 'normal', 'active', 100.00, 30.00, 0, 0, 0, 0),
('lalu', 0, 'lucky', 'active', 10.00, 10.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) {
repository := mysqltest.NewRepository(t)
repository.SetBalance(15001, 1000)

View File

@ -56,8 +56,6 @@ const (
giftWallChargeAssetMixed = "MIXED"
giftChargeSourceCoin = "coin"
giftChargeSourceBag = "bag"
normalGiftIncomeRatioPercent = "30.00"
normalGiftIncomeRatioPPM = 300_000
)
// 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 '礼物类型编码',
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',
@ -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 {
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, `
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
) VALUES
('lalu', 0, 'normal', 'active', 100.00, 0, 0, 0, 0),
('lalu', 0, 'lucky', 'active', 10.00, 0, 0, 0, 0),
('lalu', 0, 'super_lucky', 'active', 1.00, 0, 0, 0, 0)`)
('lalu', 0, 'normal', 'active', 100.00, 30.00, 0, 0, 0, 0),
('lalu', 0, 'lucky', 'active', 10.00, 10.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
}
@ -449,10 +481,13 @@ func (r *Repository) DebitGift(ctx context.Context, command ledger.DebitGiftComm
if err != nil {
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)
if !command.RobotGift {
// 收礼金币收入只属于真人送礼经济账;背包礼物也按礼物服务端价值发放,机器人礼物继续只服务房间展示。
// 收礼金币收入只属于真人送礼经济账;比例优先使用房间可见区域配置,背包礼物也按礼物服务端价值发放。
giftIncomeCoinAmount, err = giftDiamondAmount(chargeAmount, giftIncomeRatio.PPM)
if err != nil {
return ledger.Receipt{}, err
@ -760,7 +795,10 @@ func (r *Repository) BatchDebitGift(ctx context.Context, command ledger.BatchDeb
if err != nil {
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)
if err != nil {
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
}
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) {
if chargeAmount < 0 || ratioPPM < 0 {
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
}
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 {
switch strings.TrimSpace(giftTypeCode) {
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) {
row := tx.QueryRowContext(ctx,
`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"
"database/sql"
"fmt"
"strings"
"testing"
"time"
@ -494,19 +495,60 @@ func (r *Repository) SetGiftDiamondRatio(regionID int64, giftTypeCode string, ra
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,
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, ?, ?)
) VALUES (?, ?, 'active', ?, ?, 0, 0, ?, ?)
ON DUPLICATE KEY UPDATE
status = VALUES(status),
ratio_percent = VALUES(ratio_percent),
updated_at_ms = VALUES(updated_at_ms)`,
regionID, giftTypeCode, ratioPercent, nowMs, nowMs)
regionID, giftTypeCode, ratioPercent, defaultGiftReturnCoinRatioPercent(giftTypeCode), nowMs, nowMs)
if err != nil {
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 配置区域充值定价,用于外部充值类事实测试。
func (r *Repository) SetRechargePolicy(regionID int64, policyVersion string, coinAmount int64, usdMinorAmount int64) {
r.t.Helper()