fix: hide restored google recharge products
This commit is contained in:
parent
a13fcd61c7
commit
ce9a8c2d22
File diff suppressed because it is too large
Load Diff
@ -1441,6 +1441,8 @@ message RechargeProduct {
|
||||
string status = 21;
|
||||
// audience_type 区分 H5 普通用户档位和币商档位;旧 App 内购默认 normal。
|
||||
string audience_type = 22;
|
||||
// app_visible 只控制 App 充值列表展示;active 但不可见的 Google SKU 仍可用于服务端验单和历史订单补偿。
|
||||
bool app_visible = 23;
|
||||
}
|
||||
|
||||
message ListRechargeProductsRequest {
|
||||
@ -1518,6 +1520,8 @@ message CreateRechargeProductRequest {
|
||||
bool enabled = 9;
|
||||
int64 operator_user_id = 10;
|
||||
string audience_type = 11;
|
||||
// 不传时创建为可见,兼容旧后台;传 false 可保留 Google 验单能力但从 App 普通充值列表隐藏。
|
||||
optional bool app_visible = 12;
|
||||
}
|
||||
|
||||
message UpdateRechargeProductRequest {
|
||||
@ -1533,6 +1537,8 @@ message UpdateRechargeProductRequest {
|
||||
bool enabled = 10;
|
||||
int64 operator_user_id = 11;
|
||||
string audience_type = 12;
|
||||
// 不传时保留原可见性,避免旧后台编辑隐藏 SKU 时意外改变展示面。
|
||||
optional bool app_visible = 13;
|
||||
}
|
||||
|
||||
message DeleteRechargeProductRequest {
|
||||
|
||||
29
scripts/mysql/059_wallet_recharge_product_app_visible.sql
Normal file
29
scripts/mysql/059_wallet_recharge_product_app_visible.sql
Normal file
@ -0,0 +1,29 @@
|
||||
-- Split recharge product availability into two axes:
|
||||
-- status=active keeps a Google SKU usable for server-side purchase verification,
|
||||
-- app_visible=false hides it from the App's ordinary recharge product list.
|
||||
SET @ddl := IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_recharge_products' AND COLUMN_NAME = 'app_visible') = 0,
|
||||
'ALTER TABLE wallet_recharge_products ADD COLUMN app_visible BOOLEAN NOT NULL DEFAULT TRUE COMMENT ''是否在 App 普通充值列表展示;不影响 Google Play 验单'' AFTER status',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl := IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_recharge_products' AND INDEX_NAME = 'idx_wallet_recharge_products_scope') > 0,
|
||||
'ALTER TABLE wallet_recharge_products DROP INDEX idx_wallet_recharge_products_scope',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl := IF(
|
||||
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_recharge_products' AND INDEX_NAME = 'idx_wallet_recharge_products_scope') = 0,
|
||||
'ALTER TABLE wallet_recharge_products ADD INDEX idx_wallet_recharge_products_scope (app_code, platform, audience_type, status, app_visible, amount_micro, product_id)',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
@ -69,6 +69,7 @@ type rechargeProductDTO struct {
|
||||
ResourceAssetType string `json:"resourceAssetType"`
|
||||
Status string `json:"status"`
|
||||
Enabled bool `json:"enabled"`
|
||||
AppVisible bool `json:"appVisible"`
|
||||
SortOrder int32 `json:"sortOrder"`
|
||||
CreatedByUserID int64 `json:"createdByUserId"`
|
||||
UpdatedByUserID int64 `json:"updatedByUserId"`
|
||||
@ -260,6 +261,7 @@ func rechargeProductFromProto(item *walletv1.RechargeProduct) rechargeProductDTO
|
||||
ResourceAssetType: item.GetResourceAssetType(),
|
||||
Status: item.GetStatus(),
|
||||
Enabled: item.GetEnabled(),
|
||||
AppVisible: item.GetAppVisible(),
|
||||
SortOrder: item.GetSortOrder(),
|
||||
CreatedByUserID: item.GetCreatedByUserId(),
|
||||
UpdatedByUserID: item.GetUpdatedByUserId(),
|
||||
|
||||
@ -248,7 +248,7 @@ func (h *Handler) CreateRechargeProduct(c *gin.Context) {
|
||||
response.BadRequest(c, "金额USDT不正确")
|
||||
return
|
||||
}
|
||||
resp, err := h.wallet.CreateRechargeProduct(c.Request.Context(), &walletv1.CreateRechargeProductRequest{
|
||||
req := &walletv1.CreateRechargeProductRequest{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
AppCode: appctx.FromContext(c.Request.Context()),
|
||||
AmountMicro: amountMicro,
|
||||
@ -260,7 +260,11 @@ func (h *Handler) CreateRechargeProduct(c *gin.Context) {
|
||||
RegionIds: request.RegionIDs,
|
||||
Enabled: request.Enabled,
|
||||
OperatorUserId: actorID(c),
|
||||
})
|
||||
}
|
||||
if request.AppVisible != nil {
|
||||
req.AppVisible = request.AppVisible
|
||||
}
|
||||
resp, err := h.wallet.CreateRechargeProduct(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
writeWalletError(c, err, "创建内购配置失败")
|
||||
return
|
||||
@ -285,7 +289,7 @@ func (h *Handler) UpdateRechargeProduct(c *gin.Context) {
|
||||
response.BadRequest(c, "金额USDT不正确")
|
||||
return
|
||||
}
|
||||
resp, err := h.wallet.UpdateRechargeProduct(c.Request.Context(), &walletv1.UpdateRechargeProductRequest{
|
||||
req := &walletv1.UpdateRechargeProductRequest{
|
||||
RequestId: middleware.CurrentRequestID(c),
|
||||
AppCode: appctx.FromContext(c.Request.Context()),
|
||||
ProductId: productID,
|
||||
@ -298,7 +302,11 @@ func (h *Handler) UpdateRechargeProduct(c *gin.Context) {
|
||||
RegionIds: request.RegionIDs,
|
||||
Enabled: request.Enabled,
|
||||
OperatorUserId: actorID(c),
|
||||
})
|
||||
}
|
||||
if request.AppVisible != nil {
|
||||
req.AppVisible = request.AppVisible
|
||||
}
|
||||
resp, err := h.wallet.UpdateRechargeProduct(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
writeWalletError(c, err, "修改内购配置失败")
|
||||
return
|
||||
@ -585,6 +593,7 @@ type rechargeProductRequest struct {
|
||||
Platform string `json:"platform"`
|
||||
RegionIDs []int64 `json:"regionIds"`
|
||||
Enabled bool `json:"enabled"`
|
||||
AppVisible *bool `json:"appVisible"`
|
||||
}
|
||||
|
||||
type thirdPartyPaymentMethodStatusRequest struct {
|
||||
|
||||
@ -572,7 +572,7 @@ func TestSyncThirdPartyPaymentRatesRejectsInvalidMarkupPercent(t *testing.T) {
|
||||
func TestCreateRechargeProductForwardsWebCoinSellerAudience(t *testing.T) {
|
||||
wallet := &mockPaymentWallet{}
|
||||
router := newPaymentHandlerTestRouter(New(wallet, nil, nil, nil, nil))
|
||||
body := `{"amountUsdt":"10.000000","coinAmount":880000,"productName":"Seller 10 USD","description":"coin seller tier","audienceType":"coin_seller","platform":"web","regionIds":[7100],"enabled":true}`
|
||||
body := `{"amountUsdt":"10.000000","coinAmount":880000,"productName":"Seller 10 USD","description":"coin seller tier","audienceType":"coin_seller","platform":"web","regionIds":[7100],"enabled":true,"appVisible":false}`
|
||||
request := httptest.NewRequest(http.MethodPost, "/admin/payment/recharge-products", bytes.NewBufferString(body))
|
||||
request.Header.Set("Content-Type", "application/json")
|
||||
recorder := httptest.NewRecorder()
|
||||
@ -590,6 +590,8 @@ func TestCreateRechargeProductForwardsWebCoinSellerAudience(t *testing.T) {
|
||||
len(wallet.lastCreateProduct.GetRegionIds()) != 1 ||
|
||||
wallet.lastCreateProduct.GetRegionIds()[0] != 7100 ||
|
||||
!wallet.lastCreateProduct.GetEnabled() ||
|
||||
wallet.lastCreateProduct.AppVisible == nil ||
|
||||
wallet.lastCreateProduct.GetAppVisible() ||
|
||||
wallet.lastCreateProduct.GetOperatorUserId() != 7 {
|
||||
t.Fatalf("create recharge product request mismatch: %+v", wallet.lastCreateProduct)
|
||||
}
|
||||
@ -883,6 +885,7 @@ func (m *mockPaymentWallet) CreateRechargeProduct(_ context.Context, req *wallet
|
||||
CoinAmount: req.GetCoinAmount(),
|
||||
RegionIds: append([]int64(nil), req.GetRegionIds()...),
|
||||
Enabled: req.GetEnabled(),
|
||||
AppVisible: req.GetAppVisible(),
|
||||
Status: "active",
|
||||
}}, nil
|
||||
}
|
||||
|
||||
@ -325,12 +325,14 @@ func validateRechargeEvent(event domain.RechargeEvent) error {
|
||||
if event.EventType != "" && event.EventType != domain.RechargeEventWalletRecorded {
|
||||
return xerr.New(xerr.InvalidArgument, "first recharge event type is invalid")
|
||||
}
|
||||
if event.RechargeCoinAmount <= 0 {
|
||||
return xerr.New(xerr.InvalidArgument, "recharge_coin_amount must be greater than zero")
|
||||
}
|
||||
if event.GoogleProductID == "" {
|
||||
return xerr.New(xerr.InvalidArgument, "google_product_id is required")
|
||||
}
|
||||
// Google 首充档位按 google_product_id 命中,wallet 商品金币可为 0;
|
||||
// 这让后台能保留 Google 验单壳 SKU,同时避免重复给普通金币。
|
||||
if event.RechargeCoinAmount < 0 {
|
||||
return xerr.New(xerr.InvalidArgument, "recharge_coin_amount must not be negative")
|
||||
}
|
||||
if event.OccurredAtMS <= 0 {
|
||||
return xerr.New(xerr.InvalidArgument, "occurred_at_ms is required")
|
||||
}
|
||||
|
||||
@ -54,7 +54,7 @@ func TestFirstRechargeRewardConsumesRechargeFactWithMySQL(t *testing.T) {
|
||||
TransactionID: "wtx-recharge-1",
|
||||
CommandID: "seller-transfer-1",
|
||||
UserID: 42001,
|
||||
RechargeCoinAmount: 500,
|
||||
RechargeCoinAmount: 0,
|
||||
RechargeSequence: 2,
|
||||
RechargeType: "google",
|
||||
RechargeUSDMinor: 99,
|
||||
@ -69,7 +69,7 @@ func TestFirstRechargeRewardConsumesRechargeFactWithMySQL(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("GetFirstRechargeRewardClaimByUser failed: %v", err)
|
||||
}
|
||||
if !exists || claim.Status != domain.StatusGranted || claim.ResourceGroupID != 88001 || claim.GoogleProductID != "first_recharge_google_099" {
|
||||
if !exists || claim.Status != domain.StatusGranted || claim.ResourceGroupID != 88001 || claim.GoogleProductID != "first_recharge_google_099" || claim.RechargeCoinAmount != 0 {
|
||||
t.Fatalf("consume result mismatch: exists=%v claim=%+v", exists, claim)
|
||||
}
|
||||
if len(wallet.grants) != 1 || wallet.grants[0].GroupId != 88001 || wallet.grants[0].TargetUserId != 42001 {
|
||||
|
||||
@ -28,6 +28,7 @@ type rechargeProductData struct {
|
||||
ResourceAssetType string `json:"resource_asset_type"`
|
||||
Status string `json:"status"`
|
||||
Enabled bool `json:"enabled"`
|
||||
AppVisible bool `json:"app_visible"`
|
||||
SortOrder int32 `json:"sort_order"`
|
||||
}
|
||||
|
||||
@ -452,6 +453,7 @@ func rechargeProductFromProto(product *walletv1.RechargeProduct) rechargeProduct
|
||||
ResourceAssetType: product.GetResourceAssetType(),
|
||||
Status: product.GetStatus(),
|
||||
Enabled: product.GetEnabled(),
|
||||
AppVisible: product.GetAppVisible(),
|
||||
SortOrder: product.GetSortOrder(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -579,13 +579,14 @@ CREATE TABLE IF NOT EXISTS wallet_recharge_products (
|
||||
coin_amount BIGINT NOT NULL COMMENT '金币数量',
|
||||
resource_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '资源资产类型',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
app_visible BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否在 App 普通充值列表展示;不影响 Google Play 验单',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重,数值越小越靠前',
|
||||
created_by_user_id BIGINT NULL COMMENT '创建人用户 ID',
|
||||
updated_by_user_id BIGINT NULL COMMENT '更新人用户 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
UNIQUE KEY uk_wallet_recharge_products_code (app_code, product_code),
|
||||
KEY idx_wallet_recharge_products_scope (app_code, platform, audience_type, status, amount_micro, product_id)
|
||||
KEY idx_wallet_recharge_products_scope (app_code, platform, audience_type, status, app_visible, amount_micro, product_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包充值商品表';
|
||||
|
||||
SET @ddl := IF(
|
||||
@ -597,6 +598,15 @@ PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SET @ddl := IF(
|
||||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_recharge_products' AND COLUMN_NAME = 'app_visible') = 0,
|
||||
'ALTER TABLE wallet_recharge_products ADD COLUMN app_visible BOOLEAN NOT NULL DEFAULT TRUE COMMENT ''是否在 App 普通充值列表展示;不影响 Google Play 验单'' AFTER status',
|
||||
'SELECT 1'
|
||||
);
|
||||
PREPARE stmt FROM @ddl;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS wallet_recharge_product_regions (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||
product_id BIGINT NOT NULL COMMENT '商品 ID',
|
||||
|
||||
@ -21,6 +21,7 @@ type RechargeProduct struct {
|
||||
PolicyVersion string
|
||||
Enabled bool
|
||||
Status string
|
||||
AppVisible bool
|
||||
SortOrder int32
|
||||
RegionIDs []int64
|
||||
ResourceAssetType string
|
||||
@ -54,6 +55,7 @@ type RechargeProductCommand struct {
|
||||
Platform string
|
||||
RegionIDs []int64
|
||||
Enabled bool
|
||||
AppVisible *bool
|
||||
OperatorUserID int64
|
||||
}
|
||||
|
||||
|
||||
@ -330,7 +330,9 @@ func validateRechargeProductCommand(command ledger.RechargeProductCommand, requi
|
||||
if requireProductID && command.ProductID <= 0 {
|
||||
return xerr.New(xerr.InvalidArgument, "product_id is required")
|
||||
}
|
||||
if command.AmountMicro <= 0 || command.CoinAmount <= 0 || command.OperatorUserID <= 0 {
|
||||
// 隐藏的 Google 首充壳商品可以只承载支付金额和 SKU,不直接给普通金币;
|
||||
// 是否发首充资源组由 activity-service 根据 WalletRechargeRecorded 的 google_product_id 决定。
|
||||
if command.AmountMicro <= 0 || command.CoinAmount < 0 || command.OperatorUserID <= 0 {
|
||||
return xerr.New(xerr.InvalidArgument, "recharge product amount and operator are required")
|
||||
}
|
||||
if strings.TrimSpace(command.ProductName) == "" || len([]rune(strings.TrimSpace(command.ProductName))) > 128 {
|
||||
|
||||
@ -6446,6 +6446,9 @@ func TestRechargeProductsAreConfiguredByPlatformRegionAndStatus(t *testing.T) {
|
||||
if product.ProductID <= 0 || product.ProductCode != product.ProductName || product.Channel != ledger.RechargeChannelGoogle {
|
||||
t.Fatalf("created product identifiers mismatch: %+v", product)
|
||||
}
|
||||
if !product.AppVisible {
|
||||
t.Fatalf("new recharge product should be visible in app by default: %+v", product)
|
||||
}
|
||||
if len(product.RegionIDs) != 2 || product.RegionIDs[0] != 1001 || product.RegionIDs[1] != 1002 {
|
||||
t.Fatalf("created product regions mismatch: %+v", product.RegionIDs)
|
||||
}
|
||||
@ -6510,6 +6513,47 @@ func TestRechargeProductsAreConfiguredByPlatformRegionAndStatus(t *testing.T) {
|
||||
t.Fatalf("admin disabled product mismatch: total=%d products=%+v", total, adminProducts)
|
||||
}
|
||||
|
||||
hidden := false
|
||||
hiddenProduct, err := svc.UpdateRechargeProduct(context.Background(), ledger.RechargeProductCommand{
|
||||
AppCode: "lalu",
|
||||
ProductID: product.ProductID,
|
||||
AmountMicro: 2500000,
|
||||
CoinAmount: 0,
|
||||
ProductName: "2600 Coins",
|
||||
Description: "hidden google shell",
|
||||
Platform: ledger.RechargeProductPlatformAndroid,
|
||||
RegionIDs: []int64{1002},
|
||||
Enabled: true,
|
||||
AppVisible: &hidden,
|
||||
OperatorUserID: 90003,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateRechargeProduct hidden failed: %v", err)
|
||||
}
|
||||
if !hiddenProduct.Enabled || hiddenProduct.AppVisible || hiddenProduct.CoinAmount != 0 {
|
||||
t.Fatalf("hidden product state mismatch: %+v", hiddenProduct)
|
||||
}
|
||||
hiddenProducts, _, err := svc.ListRechargeProducts(context.Background(), 53001, 1002, ledger.RechargeProductPlatformAndroid, ledger.RechargeAudienceNormal)
|
||||
if err != nil {
|
||||
t.Fatalf("ListRechargeProducts hidden failed: %v", err)
|
||||
}
|
||||
if len(hiddenProducts) != 0 {
|
||||
t.Fatalf("hidden active product must not be exposed to app: %+v", hiddenProducts)
|
||||
}
|
||||
adminProducts, total, err = svc.ListAdminRechargeProducts(context.Background(), ledger.ListRechargeProductsQuery{
|
||||
AppCode: "lalu",
|
||||
Status: ledger.RechargeProductStatusActive,
|
||||
RegionID: 1002,
|
||||
Page: 1,
|
||||
PageSize: 10,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ListAdminRechargeProducts hidden failed: %v", err)
|
||||
}
|
||||
if total != 1 || len(adminProducts) != 1 || adminProducts[0].AppVisible || adminProducts[0].CoinAmount != 0 {
|
||||
t.Fatalf("admin hidden product mismatch: total=%d products=%+v", total, adminProducts)
|
||||
}
|
||||
|
||||
if err := svc.DeleteRechargeProduct(context.Background(), "lalu", product.ProductID); err != nil {
|
||||
t.Fatalf("DeleteRechargeProduct failed: %v", err)
|
||||
}
|
||||
@ -6669,6 +6713,74 @@ func TestConfirmGooglePaymentUsesProductNameAsGoogleProductID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfirmGooglePaymentAcceptsHiddenZeroCoinProduct(t *testing.T) {
|
||||
repository := mysqltest.NewRepository(t)
|
||||
svc := walletservice.New(repository)
|
||||
const googleProductID = "coins_hidden_first_recharge"
|
||||
hidden := false
|
||||
product, err := svc.CreateRechargeProduct(context.Background(), ledger.RechargeProductCommand{
|
||||
AppCode: "lalu",
|
||||
AmountMicro: 3990000,
|
||||
CoinAmount: 0,
|
||||
ProductName: googleProductID,
|
||||
Description: "hidden first recharge google shell",
|
||||
Platform: ledger.RechargeProductPlatformAndroid,
|
||||
RegionIDs: []int64{1001},
|
||||
Enabled: true,
|
||||
AppVisible: &hidden,
|
||||
OperatorUserID: 90001,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateRechargeProduct hidden failed: %v", err)
|
||||
}
|
||||
products, _, err := svc.ListRechargeProducts(context.Background(), 53001, 1001, ledger.RechargeProductPlatformAndroid, ledger.RechargeAudienceNormal)
|
||||
if err != nil {
|
||||
t.Fatalf("ListRechargeProducts hidden failed: %v", err)
|
||||
}
|
||||
if len(products) != 0 {
|
||||
t.Fatalf("hidden google shell must not be listed in app products: %+v", products)
|
||||
}
|
||||
svc.SetGooglePlayClient(&fakeGooglePlayClient{
|
||||
purchase: ledger.GooglePlayPurchase{
|
||||
ProductID: googleProductID,
|
||||
OrderID: "GPA.hidden",
|
||||
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-hidden-zero",
|
||||
UserID: 53001,
|
||||
RegionID: 1001,
|
||||
CountryID: 199,
|
||||
ProductCode: googleProductID,
|
||||
PackageName: "com.org.laluparty",
|
||||
PurchaseToken: "purchase-token-hidden-zero",
|
||||
OrderID: "GPA.hidden",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ConfirmGooglePayment hidden failed: %v", err)
|
||||
}
|
||||
if receipt.Status != ledger.PaymentStatusCredited || receipt.ProductID != product.ProductID || receipt.CoinAmount != 0 || receipt.Balance.AvailableAmount != 0 {
|
||||
t.Fatalf("hidden google receipt mismatch: %+v", receipt)
|
||||
}
|
||||
if got := repository.CountRows("payment_orders", "provider_order_id = ? AND product_code = ? AND coin_amount = ?", "GPA.hidden", googleProductID, int64(0)); got != 1 {
|
||||
t.Fatalf("hidden google payment should write one zero-coin payment order, got %d", got)
|
||||
}
|
||||
if got := repository.CountRows("wallet_entries", "transaction_id = ? AND available_delta = ?", receipt.TransactionID, int64(0)); got != 1 {
|
||||
t.Fatalf("hidden google payment should write one zero-delta wallet entry, got %d", got)
|
||||
}
|
||||
if got := repository.CountRows("wallet_recharge_records", "transaction_id = ? AND coin_amount = ? AND usd_minor_amount = ?", receipt.TransactionID, int64(0), int64(399)); got != 1 {
|
||||
t.Fatalf("hidden google payment should keep paid USD recharge fact, got %d", got)
|
||||
}
|
||||
if got := repository.CountRows("wallet_outbox", "transaction_id = ? AND event_type = ? AND available_delta = ? AND JSON_UNQUOTE(JSON_EXTRACT(payload, '$.google_product_id')) = ?", receipt.TransactionID, "WalletRechargeRecorded", int64(0), googleProductID); got != 1 {
|
||||
t.Fatalf("hidden google payment should publish zero-coin recharge fact for activity, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfirmGooglePaymentAcceptsLegacyProductCodeInput(t *testing.T) {
|
||||
repository := mysqltest.NewRepository(t)
|
||||
svc := walletservice.New(repository)
|
||||
|
||||
@ -24,7 +24,7 @@ func (r *Repository) ListRechargeProducts(ctx context.Context, _ int64, regionID
|
||||
platform = ledger.NormalizeRechargeProductPlatform(platform)
|
||||
audienceType = ledger.NormalizeRechargeAudienceType(audienceType)
|
||||
where := `
|
||||
WHERE p.app_code = ? AND p.status = ? AND p.audience_type = ? AND pr.region_id = ?`
|
||||
WHERE p.app_code = ? AND p.status = ? AND p.app_visible = TRUE AND p.audience_type = ? AND pr.region_id = ?`
|
||||
args := []any{app, ledger.RechargeProductStatusActive, audienceType, regionID}
|
||||
if platform != "" {
|
||||
where += ` AND p.platform = ?`
|
||||
@ -120,12 +120,12 @@ func (r *Repository) CreateRechargeProduct(ctx context.Context, command ledger.R
|
||||
result, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO wallet_recharge_products (
|
||||
app_code, audience_type, product_name, description, platform, channel, currency_code, amount_micro,
|
||||
coin_amount, resource_asset_type, status, sort_order, created_by_user_id, updated_by_user_id,
|
||||
coin_amount, resource_asset_type, status, app_visible, sort_order, created_by_user_id, updated_by_user_id,
|
||||
created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
command.AppCode, command.AudienceType, command.ProductName, command.Description, command.Platform,
|
||||
ledger.RechargeChannelForPlatform(command.Platform), ledger.PaidCurrencyUSDT, command.AmountMicro,
|
||||
command.CoinAmount, rechargeProductAssetType(command.AudienceType), productStatus(command.Enabled), 0,
|
||||
command.CoinAmount, rechargeProductAssetType(command.AudienceType), productStatus(command.Enabled), rechargeProductAppVisible(command, true), 0,
|
||||
command.OperatorUserID, command.OperatorUserID, nowMs, nowMs,
|
||||
)
|
||||
if err != nil {
|
||||
@ -173,10 +173,12 @@ func (r *Repository) UpdateRechargeProduct(ctx context.Context, command ledger.R
|
||||
UPDATE wallet_recharge_products
|
||||
SET audience_type = ?, product_code = ?, product_name = ?, description = ?, platform = ?, channel = ?, currency_code = ?,
|
||||
amount_micro = ?, coin_amount = ?, resource_asset_type = ?, status = ?,
|
||||
app_visible = CASE WHEN ? THEN ? ELSE app_visible END,
|
||||
updated_by_user_id = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND product_id = ?`,
|
||||
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.AppVisible != nil, rechargeProductAppVisible(command, true),
|
||||
command.OperatorUserID, nowMs, command.AppCode, command.ProductID,
|
||||
)
|
||||
if err != nil {
|
||||
@ -303,7 +305,7 @@ func rechargeProductSelectSQL() string {
|
||||
return `
|
||||
SELECT p.app_code, p.product_id, COALESCE(p.product_code, ''), p.product_name,
|
||||
p.description, p.platform, p.channel, p.currency_code, p.amount_micro,
|
||||
p.coin_amount, p.resource_asset_type, p.status, p.sort_order,
|
||||
p.coin_amount, p.resource_asset_type, p.status, COALESCE(p.app_visible, TRUE), p.sort_order,
|
||||
p.created_by_user_id, p.updated_by_user_id, p.created_at_ms, p.updated_at_ms,
|
||||
COALESCE(p.audience_type, 'normal')
|
||||
FROM wallet_recharge_products p
|
||||
@ -327,6 +329,7 @@ func scanRechargeProduct(scanner scanTarget) (ledger.RechargeProduct, error) {
|
||||
&product.CoinAmount,
|
||||
&product.ResourceAssetType,
|
||||
&product.Status,
|
||||
&product.AppVisible,
|
||||
&product.SortOrder,
|
||||
&createdBy,
|
||||
&updatedBy,
|
||||
@ -472,6 +475,13 @@ func normalizeRechargeProductCommand(command ledger.RechargeProductCommand) ledg
|
||||
return command
|
||||
}
|
||||
|
||||
func rechargeProductAppVisible(command ledger.RechargeProductCommand, defaultValue bool) bool {
|
||||
if command.AppVisible == nil {
|
||||
return defaultValue
|
||||
}
|
||||
return *command.AppVisible
|
||||
}
|
||||
|
||||
func normalizeRechargeProductRegions(regionIDs []int64) []int64 {
|
||||
seen := make(map[int64]struct{}, len(regionIDs))
|
||||
out := make([]int64, 0, len(regionIDs))
|
||||
|
||||
@ -71,6 +71,11 @@ func ensureExternalRechargeSchema(ctx context.Context, db *sql.DB) error {
|
||||
ADD COLUMN audience_type VARCHAR(32) NOT NULL DEFAULT 'normal' COMMENT '充值档位适用用户类型:normal/coin_seller' AFTER app_code`); err != nil && !isDuplicateColumnError(err) {
|
||||
return err
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
ALTER TABLE wallet_recharge_products
|
||||
ADD COLUMN app_visible BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否在 App 普通充值列表展示;不影响 Google Play 验单' AFTER status`); err != nil && !isDuplicateColumnError(err) {
|
||||
return err
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, `
|
||||
CREATE TABLE IF NOT EXISTS third_party_payment_channels (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||
|
||||
@ -90,6 +90,7 @@ func (s *Server) CreateRechargeProduct(ctx context.Context, req *walletv1.Create
|
||||
Platform: req.GetPlatform(),
|
||||
RegionIDs: req.GetRegionIds(),
|
||||
Enabled: req.GetEnabled(),
|
||||
AppVisible: req.AppVisible,
|
||||
OperatorUserID: req.GetOperatorUserId(),
|
||||
AudienceType: req.GetAudienceType(),
|
||||
})
|
||||
@ -112,6 +113,7 @@ func (s *Server) UpdateRechargeProduct(ctx context.Context, req *walletv1.Update
|
||||
Platform: req.GetPlatform(),
|
||||
RegionIDs: req.GetRegionIds(),
|
||||
Enabled: req.GetEnabled(),
|
||||
AppVisible: req.AppVisible,
|
||||
OperatorUserID: req.GetOperatorUserId(),
|
||||
AudienceType: req.GetAudienceType(),
|
||||
})
|
||||
@ -147,6 +149,7 @@ func rechargeProductToProto(product ledger.RechargeProduct) *walletv1.RechargePr
|
||||
PolicyVersion: product.PolicyVersion,
|
||||
Status: product.Status,
|
||||
Enabled: product.Enabled,
|
||||
AppVisible: product.AppVisible,
|
||||
SortOrder: product.SortOrder,
|
||||
RegionIds: product.RegionIDs,
|
||||
ResourceAssetType: product.ResourceAssetType,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user