From 09d5747cbd1e976b08a106f0c79cac124af0f223 Mon Sep 17 00:00:00 2001 From: zhx Date: Sat, 13 Jun 2026 14:34:49 +0800 Subject: [PATCH] =?UTF-8?q?=E9=92=B1=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/service/wallet/service.go | 4 +- .../internal/service/wallet/service_test.go | 63 +++++++++++++++++-- .../mysql/recharge_product_repository.go | 15 +++-- .../internal/testutil/mysqltest/mysqltest.go | 13 ++++ 4 files changed, 82 insertions(+), 13 deletions(-) diff --git a/services/wallet-service/internal/service/wallet/service.go b/services/wallet-service/internal/service/wallet/service.go index b9536db3..695b519e 100644 --- a/services/wallet-service/internal/service/wallet/service.go +++ b/services/wallet-service/internal/service/wallet/service.go @@ -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") } diff --git a/services/wallet-service/internal/service/wallet/service_test.go b/services/wallet-service/internal/service/wallet/service_test.go index 72092f7d..e7135743 100644 --- a/services/wallet-service/internal/service/wallet/service_test.go +++ b/services/wallet-service/internal/service/wallet/service_test.go @@ -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) diff --git a/services/wallet-service/internal/storage/mysql/recharge_product_repository.go b/services/wallet-service/internal/storage/mysql/recharge_product_repository.go index 39b4f2a1..98e9b48b 100644 --- a/services/wallet-service/internal/storage/mysql/recharge_product_repository.go +++ b/services/wallet-service/internal/storage/mysql/recharge_product_repository.go @@ -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) diff --git a/services/wallet-service/internal/testutil/mysqltest/mysqltest.go b/services/wallet-service/internal/testutil/mysqltest/mysqltest.go index 6332848c..d23a69cb 100644 --- a/services/wallet-service/internal/testutil/mysqltest/mysqltest.go +++ b/services/wallet-service/internal/testutil/mysqltest/mysqltest.go @@ -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()