diff --git a/scripts/mysql/037_gift_diamond_global_defaults.sql b/scripts/mysql/037_gift_diamond_global_defaults.sql index 4219de83..b8735697 100644 --- a/scripts/mysql/037_gift_diamond_global_defaults.sql +++ b/scripts/mysql/037_gift_diamond_global_defaults.sql @@ -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); diff --git a/server/admin/internal/modules/giftdiamond/dto.go b/server/admin/internal/modules/giftdiamond/dto.go index e45d58f1..6e80c581 100644 --- a/server/admin/internal/modules/giftdiamond/dto.go +++ b/server/admin/internal/modules/giftdiamond/dto.go @@ -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"` } diff --git a/server/admin/internal/modules/giftdiamond/handler.go b/server/admin/internal/modules/giftdiamond/handler.go index 641206f5..adb42a73 100644 --- a/server/admin/internal/modules/giftdiamond/handler.go +++ b/server/admin/internal/modules/giftdiamond/handler.go @@ -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 } diff --git a/server/admin/internal/modules/giftdiamond/service.go b/server/admin/internal/modules/giftdiamond/service.go index a68000cf..6fa26c8e 100644 --- a/server/admin/internal/modules/giftdiamond/service.go +++ b/server/admin/internal/modules/giftdiamond/service.go @@ -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 +} diff --git a/server/admin/internal/modules/giftdiamond/service_test.go b/server/admin/internal/modules/giftdiamond/service_test.go new file mode 100644 index 00000000..0d668028 --- /dev/null +++ b/server/admin/internal/modules/giftdiamond/service_test.go @@ -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) + } +} diff --git a/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql b/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql index 50d0d56a..46c5aca5 100644 --- a/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql +++ b/services/wallet-service/deploy/mysql/initdb/001_wallet_service.sql @@ -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); diff --git a/services/wallet-service/internal/service/wallet/service_test.go b/services/wallet-service/internal/service/wallet/service_test.go index 9833bfec..5a6c2e9b 100644 --- a/services/wallet-service/internal/service/wallet/service_test.go +++ b/services/wallet-service/internal/service/wallet/service_test.go @@ -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) diff --git a/services/wallet-service/internal/storage/mysql/repository.go b/services/wallet-service/internal/storage/mysql/repository.go index 8405e7ce..750d6dfb 100644 --- a/services/wallet-service/internal/storage/mysql/repository.go +++ b/services/wallet-service/internal/storage/mysql/repository.go @@ -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 diff --git a/services/wallet-service/internal/testutil/mysqltest/mysqltest.go b/services/wallet-service/internal/testutil/mysqltest/mysqltest.go index 31035df5..0d19b2fe 100644 --- a/services/wallet-service/internal/testutil/mysqltest/mysqltest.go +++ b/services/wallet-service/internal/testutil/mysqltest/mysqltest.go @@ -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()