可配置
This commit is contained in:
parent
ce060acc6e
commit
adde820027
@ -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);
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
26
server/admin/internal/modules/giftdiamond/service_test.go
Normal file
26
server/admin/internal/modules/giftdiamond/service_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user