三方支付的显示

This commit is contained in:
zhx 2026-06-17 11:13:25 +08:00
parent d0bb8a496c
commit 72e1fb920d
4 changed files with 99 additions and 33 deletions

View File

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

View File

@ -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: "金币增加"},

View File

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

View File

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