280 lines
9.8 KiB
Go
280 lines
9.8 KiB
Go
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")
|
|
}
|
|
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
|
|
}
|