package mysql import ( "context" "database/sql" "errors" "strings" "hyapp/pkg/appcode" "hyapp/pkg/xerr" "hyapp/services/wallet-service/internal/domain/ledger" resourcedomain "hyapp/services/wallet-service/internal/domain/resource" ) type giftPrice struct { GiftID string PriceVersion string ChargeAssetType string CoinPrice int64 // GiftPointAmount 是 wallet_gift_prices 的历史列;送礼结算只按 CoinPrice 和比例计算,不再读取它做收益。 GiftPointAmount int64 HeatValue int64 } type rechargePolicy struct { PolicyID int64 RegionID int64 PolicyVersion string CurrencyCode string CoinAmount int64 USDMinorAmount int64 EffectiveFromMs int64 } type giftDiamondRatioSnapshot struct { Percent string PPM int64 } func (r *Repository) resolveGiftPrice(ctx context.Context, tx *sql.Tx, giftID string, priceVersion string, nowMs int64) (giftPrice, error) { var row *sql.Row if strings.TrimSpace(priceVersion) != "" { row = tx.QueryRowContext(ctx, `SELECT gift_id, price_version, COALESCE(charge_asset_type, 'COIN'), coin_price, gift_point_amount, heat_value FROM wallet_gift_prices WHERE app_code = ? AND gift_id = ? AND price_version = ? AND status = 'active' AND effective_at_ms <= ?`, appcode.FromContext(ctx), giftID, priceVersion, nowMs, ) } else { row = tx.QueryRowContext(ctx, `SELECT gift_id, price_version, COALESCE(charge_asset_type, 'COIN'), coin_price, gift_point_amount, heat_value FROM wallet_gift_prices WHERE app_code = ? AND gift_id = ? AND status = 'active' AND effective_at_ms <= ? ORDER BY effective_at_ms DESC, price_version DESC LIMIT 1`, appcode.FromContext(ctx), giftID, nowMs, ) } var price giftPrice if err := row.Scan(&price.GiftID, &price.PriceVersion, &price.ChargeAssetType, &price.CoinPrice, &price.GiftPointAmount, &price.HeatValue); err != nil { if errors.Is(err, sql.ErrNoRows) { return giftPrice{}, xerr.New(xerr.NotFound, "gift price is not active") } return giftPrice{}, err } if price.CoinPrice < 0 || price.HeatValue < 0 { return giftPrice{}, xerr.New(xerr.Internal, "gift price is invalid") } price.ChargeAssetType = ledger.NormalizeGiftChargeAssetType(price.ChargeAssetType) return price, nil } func (r *Repository) resolveGiftDiamondRatio(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.getGiftDiamondRatio(ctx, tx, appCode, regionID, giftTypeCode) if err != nil { return giftDiamondRatioSnapshot{}, 0, err } if exists { return ratio, regionID, nil } } return defaultGiftDiamondRatio(giftTypeCode), 0, nil } func (r *Repository) resolveGlobalGiftDiamondRatio(ctx context.Context, tx *sql.Tx, appCode string, giftTypeCode string) (giftDiamondRatioSnapshot, int64, error) { return r.resolveGiftDiamondRatio(ctx, tx, appCode, 0, giftTypeCode) } func (r *Repository) getGiftDiamondRatio(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(ratio_percent AS CHAR), CAST(ROUND(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 diamond ratio is invalid") } percent = strings.TrimSpace(percent) if percent == "" { percent = "100.00" } 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") } if chargeAmount == 0 || ratioPPM == 0 { // 0 金额或 0% 比例都是合法业务结果;这里只跳过乘法溢出工具的正数乘数约束,不放宽负数和数量校验。 return 0, nil } product, err := checkedMul(chargeAmount, ratioPPM) if err != nil { return 0, err } return product / 1_000_000, nil } func defaultGiftDiamondRatio(giftTypeCode string) giftDiamondRatioSnapshot { switch strings.TrimSpace(giftTypeCode) { case "lucky": return giftDiamondRatioSnapshot{Percent: "10.00", PPM: 100_000} case "super_lucky": return giftDiamondRatioSnapshot{Percent: "1.00", PPM: 10_000} default: return giftDiamondRatioSnapshot{Percent: "100.00", PPM: 1_000_000} } } 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 FROM wallet_recharge_policies WHERE app_code = ? AND region_id = ? AND status = 'active' AND effective_from_ms <= ? AND (effective_to_ms IS NULL OR effective_to_ms > ?) ORDER BY effective_from_ms DESC, policy_id DESC LIMIT 1`, appcode.FromContext(ctx), regionID, nowMs, nowMs, ) var policy rechargePolicy if err := row.Scan(&policy.PolicyID, &policy.RegionID, &policy.PolicyVersion, &policy.CurrencyCode, &policy.CoinAmount, &policy.USDMinorAmount, &policy.EffectiveFromMs); err != nil { if errors.Is(err, sql.ErrNoRows) { return rechargePolicy{}, xerr.New(xerr.NotFound, "recharge policy not found") } return rechargePolicy{}, err } if policy.CoinAmount <= 0 || policy.USDMinorAmount <= 0 || strings.TrimSpace(policy.CurrencyCode) == "" { return rechargePolicy{}, xerr.New(xerr.Internal, "recharge policy is invalid") } return policy, nil } func calculateRechargeUSDMinor(coinAmount int64, policy rechargePolicy) (int64, error) { numerator, err := checkedMul(coinAmount, policy.USDMinorAmount) if err != nil { return 0, err } return numerator / policy.CoinAmount, nil } func (r *Repository) resolveCoinSellerSalaryExchangeRateTier(ctx context.Context, tx *sql.Tx, regionID int64, salaryUSDMinor int64) (ledger.CoinSellerSalaryExchangeRateTier, error) { row := tx.QueryRowContext(ctx, `SELECT region_id, min_usd_minor, max_usd_minor, coin_per_usd, status, sort_order, updated_at_ms FROM coin_seller_salary_exchange_rate_tiers WHERE app_code = ? AND region_id = ? AND status = 'active' AND min_usd_minor <= ? AND max_usd_minor >= ? ORDER BY sort_order ASC, min_usd_minor ASC, tier_id ASC LIMIT 1 FOR UPDATE`, appcode.FromContext(ctx), regionID, salaryUSDMinor, salaryUSDMinor, ) var tier ledger.CoinSellerSalaryExchangeRateTier if err := row.Scan(&tier.RegionID, &tier.MinUSDMinor, &tier.MaxUSDMinor, &tier.CoinPerUSD, &tier.Status, &tier.SortOrder, &tier.UpdatedAtMS); err != nil { if errors.Is(err, sql.ErrNoRows) { return ledger.CoinSellerSalaryExchangeRateTier{}, xerr.New(xerr.NotFound, "exchange rate not configured") } return ledger.CoinSellerSalaryExchangeRateTier{}, err } if tier.MinUSDMinor <= 0 || tier.MaxUSDMinor < tier.MinUSDMinor || tier.CoinPerUSD <= 0 || tier.CoinPerUSD%100 != 0 { return ledger.CoinSellerSalaryExchangeRateTier{}, xerr.New(xerr.Internal, "coin seller salary exchange rate is invalid") } return tier, nil } func calculateSalaryCoinAmount(salaryUSDMinor int64, coinPerUSD int64) (int64, error) { numerator, err := checkedMul(salaryUSDMinor, coinPerUSD) if err != nil { return 0, err } if numerator%100 != 0 { return 0, xerr.New(xerr.InvalidArgument, "salary amount does not match exchange rate") } return numerator / 100, nil }