This commit is contained in:
zhx 2026-06-13 14:34:49 +08:00
parent 93e5cd0218
commit 09d5747cbd
4 changed files with 82 additions and 13 deletions

View File

@ -915,7 +915,7 @@ func (s *Service) ConfirmGooglePayment(ctx context.Context, command ledger.Googl
if product.Platform != ledger.RechargeProductPlatformAndroid || product.Channel != ledger.RechargeChannelGoogle {
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")
}
if !rechargeProductSupportsRegion(product, command.RegionID) {
@ -935,6 +935,8 @@ func (s *Service) ConfirmGooglePayment(ctx context.Context, command ledger.Googl
if purchase.PurchaseState != ledger.GooglePurchaseStatePurchased {
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 {
return ledger.GooglePaymentReceipt{}, xerr.New(xerr.Conflict, "google product_id does not match recharge product")
}

View File

@ -4158,7 +4158,7 @@ func TestRechargeProductsAreConfiguredByPlatformRegionAndStatus(t *testing.T) {
if err != nil {
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)
}
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)
}
_, err = svc.UpdateRechargeProduct(context.Background(), ledger.RechargeProductCommand{
updatedProduct, err := svc.UpdateRechargeProduct(context.Background(), ledger.RechargeProductCommand{
AppCode: "lalu",
ProductID: product.ProductID,
AmountMicro: 2500000,
@ -4199,6 +4199,9 @@ func TestRechargeProductsAreConfiguredByPlatformRegionAndStatus(t *testing.T) {
if err != nil {
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)
if err != nil {
@ -4333,8 +4336,8 @@ func TestConfirmGooglePaymentUsesProductNameAsGoogleProductID(t *testing.T) {
if err != nil {
t.Fatalf("CreateRechargeProduct failed: %v", err)
}
if product.ProductCode == googleProductID {
t.Fatalf("test requires internal product_code to differ from Google product ID: %+v", product)
if product.ProductCode != googleProductID {
t.Fatalf("android product_code should match Google product ID: %+v", product)
}
svc.SetGooglePlayClient(&fakeGooglePlayClient{
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) {
repository := mysqltest.NewRepository(t)
repository.SetBalance(88001, 500)

View File

@ -4,7 +4,6 @@ import (
"context"
"database/sql"
"errors"
"fmt"
"sort"
"strings"
"time"
@ -102,7 +101,7 @@ func (r *Repository) ListAdminRechargeProducts(ctx context.Context, query ledger
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) {
if r == nil || r.db == nil {
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 {
return ledger.RechargeProduct{}, err
}
productCode := fmt.Sprintf("iap_%s_%d", command.Platform, productID)
productCode := command.ProductName
if _, err := tx.ExecContext(ctx, `
UPDATE wallet_recharge_products
SET product_code = ?
@ -154,7 +153,7 @@ func (r *Repository) CreateRechargeProduct(ctx context.Context, command ledger.R
return r.GetRechargeProduct(ctx, command.AppCode, productID)
}
// UpdateRechargeProduct 覆盖商品展示、价格、平台、上架状态和区域映射
// UpdateRechargeProduct 覆盖商品展示、价格、平台、上架状态和区域映射,并同步商品编码
func (r *Repository) UpdateRechargeProduct(ctx context.Context, command ledger.RechargeProductCommand) (ledger.RechargeProduct, error) {
if r == nil || r.db == nil {
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()
result, err := tx.ExecContext(ctx, `
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 = ?,
updated_by_user_id = ?, updated_at_ms = ?
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),
command.OperatorUserID, nowMs, command.AppCode, command.ProductID,
)
@ -301,8 +300,8 @@ func scanRechargeProduct(scanner scanTarget) (ledger.RechargeProduct, error) {
); err != nil {
return ledger.RechargeProduct{}, err
}
if product.ProductCode == "" && product.ProductID > 0 {
product.ProductCode = fmt.Sprintf("iap_%s_%d", product.Platform, product.ProductID)
if product.ProductCode == "" {
product.ProductCode = product.ProductName
}
if product.Channel == "" {
product.Channel = ledger.RechargeChannelForPlatform(product.Platform)

View File

@ -45,6 +45,19 @@ func (r *Repository) CountRows(table string, where string, args ...any) int {
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) {
r.t.Helper()