From 72e1fb920d6c122e5625d124013e4e21d04a45de Mon Sep 17 00:00:00 2001 From: zhx Date: Wed, 17 Jun 2026 11:13:25 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=89=E6=96=B9=E6=94=AF=E4=BB=98=E7=9A=84?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../internal/modules/coinledger/service.go | 10 +++- .../modules/coinledger/service_test.go | 9 +-- .../internal/service/wallet/service_test.go | 55 ++++++++++++++++++ .../mysql/third_party_payment_repository.go | 58 ++++++++++--------- 4 files changed, 99 insertions(+), 33 deletions(-) diff --git a/server/admin/internal/modules/coinledger/service.go b/server/admin/internal/modules/coinledger/service.go index 50db06a6..13a99ad5 100644 --- a/server/admin/internal/modules/coinledger/service.go +++ b/server/admin/internal/modules/coinledger/service.go @@ -22,6 +22,7 @@ const ( coinSellerAssetType = "COIN_SELLER_COIN" coinManualCreditBizType = "manual_credit" coinSellerTransferBizType = "coin_seller_transfer" + coinSellerRechargeBizType = "coin_seller_recharge" coinSellerStockPurchaseBizType = "coin_seller_stock_purchase" coinSellerCoinCompensationBizType = "coin_seller_coin_compensation" salaryTransferToCoinSellerBizType = "salary_transfer_to_coin_seller" @@ -781,14 +782,15 @@ func coinSellerLedgerBizTypes(ledgerType string) ([]string, error) { // 空类型表示“全部币商流水”,但仍只允许当前产品定义的公开币商库存口径,不能把其他内部账带出来。 return []string{ coinSellerStockPurchaseBizType, + coinSellerRechargeBizType, coinSellerCoinCompensationBizType, coinSellerTransferBizType, salaryTransferToCoinSellerBizType, coinManualCreditBizType, }, nil case coinSellerLedgerTypeAdminStockCredit: - // 后台操作是运营口径,底层包含 USDT 进货、金币补偿和后台扣除;资产条件仍限定在 COIN_SELLER_COIN。 - return []string{coinSellerStockPurchaseBizType, coinSellerCoinCompensationBizType, coinManualCreditBizType}, nil + // 进货操作是运营库存口径,底层包含后台 USDT 进货、H5 三方币商充值、金币补偿和后台扣除;资产条件仍限定在 COIN_SELLER_COIN。 + return []string{coinSellerStockPurchaseBizType, coinSellerRechargeBizType, coinSellerCoinCompensationBizType, coinManualCreditBizType}, nil case coinSellerLedgerTypeSellerTransfer: // 币商转用户只展示币商侧出账分录,收款用户资料在展示投影里补齐。 return []string{coinSellerTransferBizType}, nil @@ -802,7 +804,7 @@ func coinSellerLedgerBizTypes(ledgerType string) ([]string, error) { func coinSellerLedgerTypeForBizType(bizType string) string { switch bizType { - case coinSellerStockPurchaseBizType, coinSellerCoinCompensationBizType: + case coinSellerStockPurchaseBizType, coinSellerRechargeBizType, coinSellerCoinCompensationBizType: return coinSellerLedgerTypeAdminStockCredit case coinManualCreditBizType: return coinSellerLedgerTypeAdminStockCredit @@ -819,6 +821,8 @@ func coinSellerLedgerLabel(item coinSellerLedgerDTO) string { switch item.BizType { case coinSellerStockPurchaseBizType: return "USDT进货" + case coinSellerRechargeBizType: + return "三方充值" case coinSellerCoinCompensationBizType: return "金币补偿" case coinManualCreditBizType: diff --git a/server/admin/internal/modules/coinledger/service_test.go b/server/admin/internal/modules/coinledger/service_test.go index 7d3750c6..42e764fd 100644 --- a/server/admin/internal/modules/coinledger/service_test.go +++ b/server/admin/internal/modules/coinledger/service_test.go @@ -51,10 +51,10 @@ func TestCoinSellerLedgerWhereMapsAdminStockCredit(t *testing.T) { if err != nil { t.Fatalf("coin seller ledger where failed: %v", err) } - if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type IN (?,?,?) AND e.user_id IN (?,?)"; where != want { + if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type IN (?,?,?,?) AND e.user_id IN (?,?)"; where != want { t.Fatalf("where mismatch:\nwant %s\n got %s", want, where) } - if len(args) != 7 || args[0] != "lalu" || args[1] != coinSellerAssetType || args[2] != coinSellerStockPurchaseBizType || args[3] != coinSellerCoinCompensationBizType || args[4] != coinManualCreditBizType || args[5] != int64(3001) || args[6] != int64(3002) { + if len(args) != 8 || args[0] != "lalu" || args[1] != coinSellerAssetType || args[2] != coinSellerStockPurchaseBizType || args[3] != coinSellerRechargeBizType || args[4] != coinSellerCoinCompensationBizType || args[5] != coinManualCreditBizType || args[6] != int64(3001) || args[7] != int64(3002) { t.Fatalf("args mismatch: %#v", args) } } @@ -64,10 +64,10 @@ func TestCoinSellerLedgerWhereEmptyTypeUsesAllPublicTypes(t *testing.T) { if err != nil { t.Fatalf("coin seller ledger where failed: %v", err) } - if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type IN (?,?,?,?,?)"; where != want { + if want := "WHERE e.app_code = ? AND e.asset_type = ? AND wt.biz_type IN (?,?,?,?,?,?)"; where != want { t.Fatalf("where mismatch:\nwant %s\n got %s", want, where) } - if len(args) != 7 || args[0] != "lalu" || args[1] != coinSellerAssetType || args[2] != coinSellerStockPurchaseBizType || args[3] != coinSellerCoinCompensationBizType || args[4] != coinSellerTransferBizType || args[5] != salaryTransferToCoinSellerBizType || args[6] != coinManualCreditBizType { + if len(args) != 8 || args[0] != "lalu" || args[1] != coinSellerAssetType || args[2] != coinSellerStockPurchaseBizType || args[3] != coinSellerRechargeBizType || args[4] != coinSellerCoinCompensationBizType || args[5] != coinSellerTransferBizType || args[6] != salaryTransferToCoinSellerBizType || args[7] != coinManualCreditBizType { t.Fatalf("args mismatch: %#v", args) } } @@ -117,6 +117,7 @@ func TestCoinSellerLedgerLabelUsesConcreteStockAndDebitType(t *testing.T) { want string }{ {name: "usdt purchase", item: coinSellerLedgerDTO{BizType: coinSellerStockPurchaseBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit}, want: "USDT进货"}, + {name: "third party recharge", item: coinSellerLedgerDTO{BizType: coinSellerRechargeBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit}, want: "三方充值"}, {name: "compensation", item: coinSellerLedgerDTO{BizType: coinSellerCoinCompensationBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit}, want: "金币补偿"}, {name: "debit", item: coinSellerLedgerDTO{BizType: coinManualCreditBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit, AvailableDelta: -100}, want: "金币扣除"}, {name: "manual increase", item: coinSellerLedgerDTO{BizType: coinManualCreditBizType, LedgerType: coinSellerLedgerTypeAdminStockCredit, AvailableDelta: 100}, want: "金币增加"}, diff --git a/services/wallet-service/internal/service/wallet/service_test.go b/services/wallet-service/internal/service/wallet/service_test.go index 0d75b7aa..c873b1d6 100644 --- a/services/wallet-service/internal/service/wallet/service_test.go +++ b/services/wallet-service/internal/service/wallet/service_test.go @@ -2312,6 +2312,61 @@ func TestH5MifaPayStatusQueryCreditsOrder(t *testing.T) { } } +func TestH5MifaPayCoinSellerCreditsSellerStockUSDTEquivalent(t *testing.T) { + repository := mysqltest.NewRepository(t) + svc := walletservice.New(repository) + mifaPay := &fakeMifaPayClient{} + svc.SetMifaPayClient(mifaPay) + svc.SetExternalRechargeConfig(walletservice.ExternalRechargeConfig{MifaPayNotifyURL: "https://api.example.com/api/v1/payment/mifapay/notify"}) + product := repository.CreateH5RechargeProduct(ledger.RechargeAudienceCoinSeller, 7265, 10_000_000, 880000) + saCardMethodID := repository.ThirdPartyPaymentMethodID("SA", "Card", "MADA") + order, err := svc.CreateH5RechargeOrder(context.Background(), ledger.CreateExternalRechargeOrderCommand{ + AppCode: appcode.Default, + CommandID: "cmd-h5-mifapay-seller-query-success", + TargetUserID: 42605, + TargetRegionID: 7265, + TargetCountryCode: "SA", + AudienceType: ledger.RechargeAudienceCoinSeller, + ProductID: product.ProductID, + ProviderCode: ledger.PaymentProviderMifaPay, + PaymentMethodID: saCardMethodID, + PayerName: "seller55", + PayerAccount: "163055", + PayerEmail: "seller55@lalu.com", + }) + if err != nil { + t.Fatalf("CreateH5RechargeOrder seller mifapay failed: %v", err) + } + mifaPay.queryResp = walletservice.MifaPayQueryOrderResponse{ + OrderID: order.OrderID, + ProviderOrderID: order.ProviderOrderID, + Status: "1", + Amount: "1000", + Currency: order.CurrencyCode, + PayType: order.PayType, + RawJSON: `{"status":"1"}`, + } + + credited, err := svc.GetH5RechargeOrder(context.Background(), appcode.Default, order.OrderID, order.TargetUserID) + if err != nil { + t.Fatalf("GetH5RechargeOrder seller query success failed: %v", err) + } + if credited.Status != ledger.ExternalRechargeStatusCredited || credited.TransactionID == "" { + t.Fatalf("mifapay seller query should credit order: %+v", credited) + } + assertBalance(t, svc, order.TargetUserID, ledger.AssetCoinSellerCoin, 880000) + assertBalance(t, svc, order.TargetUserID, ledger.AssetCoin, 0) + if got := repository.CountRows("wallet_recharge_records", "transaction_id = ?", credited.TransactionID); got != 0 { + t.Fatalf("seller mifapay recharge must not write user recharge record, got %d", got) + } + if got := repository.CountRows("coin_seller_stock_records", "transaction_id = ? AND counts_as_seller_recharge = TRUE AND coin_amount = ? AND paid_currency_code = ? AND paid_amount_micro = ?", credited.TransactionID, int64(880000), ledger.PaidCurrencyUSDT, int64(10_000_000)); got != 1 { + t.Fatalf("seller mifapay recharge should write one usdt-equivalent seller stock record, got %d", got) + } + if got := repository.CountRows("wallet_transactions", "transaction_id = ? AND biz_type = ? AND JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.paid_currency_code')) = ? AND JSON_EXTRACT(metadata_json, '$.paid_amount_micro') = 10000000", credited.TransactionID, "coin_seller_recharge", ledger.PaidCurrencyUSDT); got != 1 { + t.Fatalf("seller mifapay transaction metadata should use usdt-equivalent cumulative amount, got %d", got) + } +} + func TestH5MifaPayStatusQueryMarksFailedOrder(t *testing.T) { repository := mysqltest.NewRepository(t) svc := walletservice.New(repository) diff --git a/services/wallet-service/internal/storage/mysql/third_party_payment_repository.go b/services/wallet-service/internal/storage/mysql/third_party_payment_repository.go index 79c8014e..6a74d585 100644 --- a/services/wallet-service/internal/storage/mysql/third_party_payment_repository.go +++ b/services/wallet-service/internal/storage/mysql/third_party_payment_repository.go @@ -506,14 +506,43 @@ func (r *Repository) CreditExternalRechargeOrder(ctx context.Context, appCode st RechargePolicyUSDMinorAmount: order.USDMinorAmount, RechargeType: rechargeType, } - if !isCoinSellerRecharge { + var transactionMetadata any = metadata + var stockMetadata coinSellerStockMetadata + if isCoinSellerRecharge { + // 币商列表的“累充USDT”是统一经营口径,不能直接使用 MiFaPay 本地支付币种金额。 + // 这里按商品 USD 金额折成 USDT 微单位,确保 SA/SAR、ID/IDR 等三方通道都进入同一个币商累充口径。 + paidAmountMicro, err := checkedMul(order.USDMinorAmount, 10_000) + if err != nil { + return ledger.ExternalRechargeOrder{}, err + } + stockMetadata = coinSellerStockMetadata{ + AppCode: order.AppCode, + PaymentOrderID: order.OrderID, + ProviderCode: order.ProviderCode, + PaymentMethodID: order.PaymentMethodID, + SellerUserID: order.TargetUserID, + SellerRegionID: order.TargetRegionID, + StockType: ledger.StockTypeUSDTPurchase, + CoinAmount: order.CoinAmount, + PaidCurrencyCode: ledger.PaidCurrencyUSDT, + PaidAmountMicro: paidAmountMicro, + PaymentRef: externalRechargePaymentRef(order), + EvidenceRef: order.OrderID, + Reason: "h5 external recharge", + AssetType: ledger.AssetCoinSellerCoin, + CountsAsSellerRecharge: true, + BalanceAfter: balanceAfter, + CreatedAtMS: nowMS, + } + transactionMetadata = stockMetadata + } else { rechargeSequence, err := r.reserveUserRechargeSequence(ctx, tx, order.AppCode, order.TargetUserID, transactionID, order.CoinAmount, order.USDMinorAmount, nowMS) if err != nil { return ledger.ExternalRechargeOrder{}, err } metadata.RechargeSequence = rechargeSequence } - if err := r.insertTransaction(ctx, tx, transactionID, order.CommandID, bizType, externalRechargeRequestHash(order, assetType), order.OrderID, metadata, nowMS); err != nil { + if err := r.insertTransaction(ctx, tx, transactionID, order.CommandID, bizType, externalRechargeRequestHash(order, assetType), order.OrderID, transactionMetadata, nowMS); err != nil { return ledger.ExternalRechargeOrder{}, err } if err := r.applyAccountDelta(ctx, tx, account, order.CoinAmount, 0, nowMS); err != nil { @@ -533,32 +562,9 @@ func (r *Repository) CreditExternalRechargeOrder(ctx context.Context, appCode st // 普通用户 H5 充值进入普通 COIN 钱包,并写普通充值记录/累计统计,供首充、累充、VIP 等用户侧口径消费。 // 币商 H5 充值只进入 COIN_SELLER_COIN 库存,并写币商库存记录;不能写 wallet_recharge_records,避免被误算成普通用户充值。 outboxEvents := []walletOutboxEvent{ - balanceChangedEvent(transactionID, order.CommandID, order.TargetUserID, assetType, order.CoinAmount, 0, balanceAfter, account.FrozenAmount, account.Version+1, metadata, nowMS), + balanceChangedEvent(transactionID, order.CommandID, order.TargetUserID, assetType, order.CoinAmount, 0, balanceAfter, account.FrozenAmount, account.Version+1, transactionMetadata, nowMS), } if isCoinSellerRecharge { - paidAmountMicro, err := checkedMul(order.ProviderAmountMinor, 10_000) - if err != nil { - return ledger.ExternalRechargeOrder{}, err - } - stockMetadata := coinSellerStockMetadata{ - AppCode: order.AppCode, - PaymentOrderID: order.OrderID, - ProviderCode: order.ProviderCode, - PaymentMethodID: order.PaymentMethodID, - SellerUserID: order.TargetUserID, - SellerRegionID: order.TargetRegionID, - StockType: ledger.StockTypeUSDTPurchase, - CoinAmount: order.CoinAmount, - PaidCurrencyCode: order.CurrencyCode, - PaidAmountMicro: paidAmountMicro, - PaymentRef: externalRechargePaymentRef(order), - EvidenceRef: order.OrderID, - Reason: "h5 external recharge", - AssetType: ledger.AssetCoinSellerCoin, - CountsAsSellerRecharge: true, - BalanceAfter: balanceAfter, - CreatedAtMS: nowMS, - } if err := r.insertCoinSellerStockRecord(ctx, tx, transactionID, order.CommandID, stockMetadata, nowMS); err != nil { return ledger.ExternalRechargeOrder{}, err }