fix: hide restored google recharge products

This commit is contained in:
zhx 2026-07-09 14:23:41 +08:00
parent a13fcd61c7
commit ce9a8c2d22
16 changed files with 2548 additions and 2313 deletions

File diff suppressed because it is too large Load Diff

View File

@ -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 {

View 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;

View File

@ -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(),

View File

@ -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 {

View File

@ -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
}

View File

@ -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")
}

View File

@ -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 {

View File

@ -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(),
}
}

View File

@ -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',

View File

@ -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
}

View File

@ -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 {

View File

@ -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)

View File

@ -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))

View File

@ -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 '应用编码用于多租户隔离',

View File

@ -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,