钱包
This commit is contained in:
parent
93e5cd0218
commit
09d5747cbd
@ -915,7 +915,7 @@ func (s *Service) ConfirmGooglePayment(ctx context.Context, command ledger.Googl
|
|||||||
if product.Platform != ledger.RechargeProductPlatformAndroid || product.Channel != ledger.RechargeChannelGoogle {
|
if product.Platform != ledger.RechargeProductPlatformAndroid || product.Channel != ledger.RechargeChannelGoogle {
|
||||||
return ledger.GooglePaymentReceipt{}, xerr.New(xerr.InvalidArgument, "recharge product is not google play product")
|
return ledger.GooglePaymentReceipt{}, xerr.New(xerr.InvalidArgument, "recharge product is not google play product")
|
||||||
}
|
}
|
||||||
if command.ProductCode != "" && command.ProductCode != product.ProductName {
|
if command.ProductCode != "" && command.ProductCode != product.ProductName && command.ProductCode != product.ProductCode {
|
||||||
return ledger.GooglePaymentReceipt{}, xerr.New(xerr.Conflict, "product_code does not match")
|
return ledger.GooglePaymentReceipt{}, xerr.New(xerr.Conflict, "product_code does not match")
|
||||||
}
|
}
|
||||||
if !rechargeProductSupportsRegion(product, command.RegionID) {
|
if !rechargeProductSupportsRegion(product, command.RegionID) {
|
||||||
@ -935,6 +935,8 @@ func (s *Service) ConfirmGooglePayment(ctx context.Context, command ledger.Googl
|
|||||||
if purchase.PurchaseState != ledger.GooglePurchaseStatePurchased {
|
if purchase.PurchaseState != ledger.GooglePurchaseStatePurchased {
|
||||||
return ledger.GooglePaymentReceipt{}, xerr.New(xerr.Conflict, "google purchase is not purchased")
|
return ledger.GooglePaymentReceipt{}, xerr.New(xerr.Conflict, "google purchase is not purchased")
|
||||||
}
|
}
|
||||||
|
// 旧 App 首充曾把钱包内部 product_code 当成确认参数;上面的兼容只负责放行历史入参,
|
||||||
|
// 真正的 Google 商品事实仍以 purchase.ProductID 对齐 product_name,避免内部编码绕过支付商品校验。
|
||||||
if purchase.ProductID != "" && purchase.ProductID != product.ProductName {
|
if purchase.ProductID != "" && purchase.ProductID != product.ProductName {
|
||||||
return ledger.GooglePaymentReceipt{}, xerr.New(xerr.Conflict, "google product_id does not match recharge product")
|
return ledger.GooglePaymentReceipt{}, xerr.New(xerr.Conflict, "google product_id does not match recharge product")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4158,7 +4158,7 @@ func TestRechargeProductsAreConfiguredByPlatformRegionAndStatus(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("CreateRechargeProduct failed: %v", err)
|
t.Fatalf("CreateRechargeProduct failed: %v", err)
|
||||||
}
|
}
|
||||||
if product.ProductID <= 0 || product.ProductCode == "" || product.Channel != ledger.RechargeChannelGoogle {
|
if product.ProductID <= 0 || product.ProductCode != product.ProductName || product.Channel != ledger.RechargeChannelGoogle {
|
||||||
t.Fatalf("created product identifiers mismatch: %+v", product)
|
t.Fatalf("created product identifiers mismatch: %+v", product)
|
||||||
}
|
}
|
||||||
if len(product.RegionIDs) != 2 || product.RegionIDs[0] != 1001 || product.RegionIDs[1] != 1002 {
|
if len(product.RegionIDs) != 2 || product.RegionIDs[0] != 1001 || product.RegionIDs[1] != 1002 {
|
||||||
@ -4184,7 +4184,7 @@ func TestRechargeProductsAreConfiguredByPlatformRegionAndStatus(t *testing.T) {
|
|||||||
t.Fatalf("ios list must not include android product: %+v", iosProducts)
|
t.Fatalf("ios list must not include android product: %+v", iosProducts)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = svc.UpdateRechargeProduct(context.Background(), ledger.RechargeProductCommand{
|
updatedProduct, err := svc.UpdateRechargeProduct(context.Background(), ledger.RechargeProductCommand{
|
||||||
AppCode: "lalu",
|
AppCode: "lalu",
|
||||||
ProductID: product.ProductID,
|
ProductID: product.ProductID,
|
||||||
AmountMicro: 2500000,
|
AmountMicro: 2500000,
|
||||||
@ -4199,6 +4199,9 @@ func TestRechargeProductsAreConfiguredByPlatformRegionAndStatus(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("UpdateRechargeProduct failed: %v", err)
|
t.Fatalf("UpdateRechargeProduct failed: %v", err)
|
||||||
}
|
}
|
||||||
|
if updatedProduct.ProductCode != updatedProduct.ProductName {
|
||||||
|
t.Fatalf("updated product_code should follow product_name: %+v", updatedProduct)
|
||||||
|
}
|
||||||
|
|
||||||
inactiveProducts, _, err := svc.ListRechargeProducts(context.Background(), 53001, 1002, ledger.RechargeProductPlatformAndroid, ledger.RechargeAudienceNormal)
|
inactiveProducts, _, err := svc.ListRechargeProducts(context.Background(), 53001, 1002, ledger.RechargeProductPlatformAndroid, ledger.RechargeAudienceNormal)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -4333,8 +4336,8 @@ func TestConfirmGooglePaymentUsesProductNameAsGoogleProductID(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("CreateRechargeProduct failed: %v", err)
|
t.Fatalf("CreateRechargeProduct failed: %v", err)
|
||||||
}
|
}
|
||||||
if product.ProductCode == googleProductID {
|
if product.ProductCode != googleProductID {
|
||||||
t.Fatalf("test requires internal product_code to differ from Google product ID: %+v", product)
|
t.Fatalf("android product_code should match Google product ID: %+v", product)
|
||||||
}
|
}
|
||||||
svc.SetGooglePlayClient(&fakeGooglePlayClient{
|
svc.SetGooglePlayClient(&fakeGooglePlayClient{
|
||||||
purchase: ledger.GooglePlayPurchase{
|
purchase: ledger.GooglePlayPurchase{
|
||||||
@ -4377,6 +4380,58 @@ func TestConfirmGooglePaymentUsesProductNameAsGoogleProductID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfirmGooglePaymentAcceptsLegacyProductCodeInput(t *testing.T) {
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
svc := walletservice.New(repository)
|
||||||
|
const googleProductID = "coins_legacy_first_recharge"
|
||||||
|
const legacyProductCode = "iap_android_legacy"
|
||||||
|
product, err := svc.CreateRechargeProduct(context.Background(), ledger.RechargeProductCommand{
|
||||||
|
AppCode: "lalu",
|
||||||
|
AmountMicro: 1990000,
|
||||||
|
CoinAmount: 160000,
|
||||||
|
ProductName: googleProductID,
|
||||||
|
Description: "legacy first recharge pack",
|
||||||
|
Platform: ledger.RechargeProductPlatformAndroid,
|
||||||
|
RegionIDs: []int64{1001},
|
||||||
|
Enabled: true,
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateRechargeProduct failed: %v", err)
|
||||||
|
}
|
||||||
|
repository.SetRechargeProductCode(product.ProductID, legacyProductCode)
|
||||||
|
svc.SetGooglePlayClient(&fakeGooglePlayClient{
|
||||||
|
purchase: ledger.GooglePlayPurchase{
|
||||||
|
ProductID: googleProductID,
|
||||||
|
OrderID: "GPA.legacy",
|
||||||
|
PurchaseState: ledger.GooglePurchaseStatePurchased,
|
||||||
|
ConsumptionState: "CONSUMPTION_STATE_YET_TO_BE_CONSUMED",
|
||||||
|
AcknowledgementState: "ACKNOWLEDGEMENT_STATE_ACKNOWLEDGED",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
receipt, err := svc.ConfirmGooglePayment(context.Background(), ledger.GooglePaymentCommand{
|
||||||
|
AppCode: "lalu",
|
||||||
|
CommandID: "google-pay-legacy-product-code",
|
||||||
|
UserID: 53002,
|
||||||
|
RegionID: 1001,
|
||||||
|
ProductID: product.ProductID,
|
||||||
|
ProductCode: legacyProductCode,
|
||||||
|
PackageName: "com.org.laluparty",
|
||||||
|
PurchaseToken: "purchase-token-legacy-product-code",
|
||||||
|
OrderID: "GPA.legacy",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ConfirmGooglePayment should accept legacy product_code input: %v", err)
|
||||||
|
}
|
||||||
|
if receipt.Status != ledger.PaymentStatusCredited || receipt.ProductCode != legacyProductCode || receipt.CoinAmount != 160000 {
|
||||||
|
t.Fatalf("legacy google payment receipt mismatch: %+v", receipt)
|
||||||
|
}
|
||||||
|
if got := repository.CountRows("payment_orders", "product_id = ? AND product_code = ? AND product_name = ?", product.ProductID, legacyProductCode, googleProductID); got != 1 {
|
||||||
|
t.Fatalf("google payment should keep legacy product_code snapshot while validating product_name, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestApplyGameCoinChangeDebitCreditAndIdempotency(t *testing.T) {
|
func TestApplyGameCoinChangeDebitCreditAndIdempotency(t *testing.T) {
|
||||||
repository := mysqltest.NewRepository(t)
|
repository := mysqltest.NewRepository(t)
|
||||||
repository.SetBalance(88001, 500)
|
repository.SetBalance(88001, 500)
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -102,7 +101,7 @@ func (r *Repository) ListAdminRechargeProducts(ctx context.Context, query ledger
|
|||||||
return products, total, nil
|
return products, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRechargeProduct 写入内购商品和区域映射,product_code 由商品 ID 派生保持稳定。
|
// CreateRechargeProduct 写入内购商品和区域映射,product_code 和 Google 商品 ID 保持一致。
|
||||||
func (r *Repository) CreateRechargeProduct(ctx context.Context, command ledger.RechargeProductCommand) (ledger.RechargeProduct, error) {
|
func (r *Repository) CreateRechargeProduct(ctx context.Context, command ledger.RechargeProductCommand) (ledger.RechargeProduct, error) {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return ledger.RechargeProduct{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
return ledger.RechargeProduct{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
@ -136,7 +135,7 @@ func (r *Repository) CreateRechargeProduct(ctx context.Context, command ledger.R
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ledger.RechargeProduct{}, err
|
return ledger.RechargeProduct{}, err
|
||||||
}
|
}
|
||||||
productCode := fmt.Sprintf("iap_%s_%d", command.Platform, productID)
|
productCode := command.ProductName
|
||||||
if _, err := tx.ExecContext(ctx, `
|
if _, err := tx.ExecContext(ctx, `
|
||||||
UPDATE wallet_recharge_products
|
UPDATE wallet_recharge_products
|
||||||
SET product_code = ?
|
SET product_code = ?
|
||||||
@ -154,7 +153,7 @@ func (r *Repository) CreateRechargeProduct(ctx context.Context, command ledger.R
|
|||||||
return r.GetRechargeProduct(ctx, command.AppCode, productID)
|
return r.GetRechargeProduct(ctx, command.AppCode, productID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateRechargeProduct 覆盖商品展示、价格、平台、上架状态和区域映射。
|
// UpdateRechargeProduct 覆盖商品展示、价格、平台、上架状态和区域映射,并同步商品编码。
|
||||||
func (r *Repository) UpdateRechargeProduct(ctx context.Context, command ledger.RechargeProductCommand) (ledger.RechargeProduct, error) {
|
func (r *Repository) UpdateRechargeProduct(ctx context.Context, command ledger.RechargeProductCommand) (ledger.RechargeProduct, error) {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return ledger.RechargeProduct{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
return ledger.RechargeProduct{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
@ -172,11 +171,11 @@ func (r *Repository) UpdateRechargeProduct(ctx context.Context, command ledger.R
|
|||||||
nowMs := time.Now().UnixMilli()
|
nowMs := time.Now().UnixMilli()
|
||||||
result, err := tx.ExecContext(ctx, `
|
result, err := tx.ExecContext(ctx, `
|
||||||
UPDATE wallet_recharge_products
|
UPDATE wallet_recharge_products
|
||||||
SET audience_type = ?, product_name = ?, description = ?, platform = ?, channel = ?, currency_code = ?,
|
SET audience_type = ?, product_code = ?, product_name = ?, description = ?, platform = ?, channel = ?, currency_code = ?,
|
||||||
amount_micro = ?, coin_amount = ?, resource_asset_type = ?, status = ?,
|
amount_micro = ?, coin_amount = ?, resource_asset_type = ?, status = ?,
|
||||||
updated_by_user_id = ?, updated_at_ms = ?
|
updated_by_user_id = ?, updated_at_ms = ?
|
||||||
WHERE app_code = ? AND product_id = ?`,
|
WHERE app_code = ? AND product_id = ?`,
|
||||||
command.AudienceType, command.ProductName, command.Description, command.Platform, ledger.RechargeChannelForPlatform(command.Platform),
|
command.AudienceType, command.ProductName, command.ProductName, command.Description, command.Platform, ledger.RechargeChannelForPlatform(command.Platform),
|
||||||
ledger.PaidCurrencyUSDT, command.AmountMicro, command.CoinAmount, rechargeProductAssetType(command.AudienceType), productStatus(command.Enabled),
|
ledger.PaidCurrencyUSDT, command.AmountMicro, command.CoinAmount, rechargeProductAssetType(command.AudienceType), productStatus(command.Enabled),
|
||||||
command.OperatorUserID, nowMs, command.AppCode, command.ProductID,
|
command.OperatorUserID, nowMs, command.AppCode, command.ProductID,
|
||||||
)
|
)
|
||||||
@ -301,8 +300,8 @@ func scanRechargeProduct(scanner scanTarget) (ledger.RechargeProduct, error) {
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return ledger.RechargeProduct{}, err
|
return ledger.RechargeProduct{}, err
|
||||||
}
|
}
|
||||||
if product.ProductCode == "" && product.ProductID > 0 {
|
if product.ProductCode == "" {
|
||||||
product.ProductCode = fmt.Sprintf("iap_%s_%d", product.Platform, product.ProductID)
|
product.ProductCode = product.ProductName
|
||||||
}
|
}
|
||||||
if product.Channel == "" {
|
if product.Channel == "" {
|
||||||
product.Channel = ledger.RechargeChannelForPlatform(product.Platform)
|
product.Channel = ledger.RechargeChannelForPlatform(product.Platform)
|
||||||
|
|||||||
@ -45,6 +45,19 @@ func (r *Repository) CountRows(table string, where string, args ...any) int {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetRechargeProductCode overwrites product_code to model legacy rows created before code matched product_name.
|
||||||
|
func (r *Repository) SetRechargeProductCode(productID int64, productCode string) {
|
||||||
|
r.t.Helper()
|
||||||
|
if _, err := r.schema.DB.ExecContext(context.Background(), `
|
||||||
|
UPDATE wallet_recharge_products
|
||||||
|
SET product_code = ?
|
||||||
|
WHERE product_id = ?`,
|
||||||
|
productCode, productID,
|
||||||
|
); err != nil {
|
||||||
|
r.t.Fatalf("set recharge product code failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) ActiveResourceEntitlement(userID int64, resourceID int64) (expiresAtMS int64, quantity int64, remainingQuantity int64) {
|
func (r *Repository) ActiveResourceEntitlement(userID int64, resourceID int64) (expiresAtMS int64, quantity int64, remainingQuantity int64) {
|
||||||
r.t.Helper()
|
r.t.Helper()
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user