修复钱包错误
This commit is contained in:
parent
2254097ea4
commit
beab5f57c8
@ -22,6 +22,11 @@ func firstRechargeEventFromWalletMessage(body []byte) (firstrechargedomain.Recha
|
||||
return firstrechargedomain.RechargeEvent{}, false, nil
|
||||
}
|
||||
fields := firstRechargeFactFromWalletPayload(message.PayloadJSON)
|
||||
// 首充奖励按 Google 商品 ID 精确匹配运营档位;币商转账、H5 或其他充值事实没有 Google 商品维度,
|
||||
// 这里必须在进入业务校验前确认并跳过,避免合法钱包事实被首充 consumer 当成 poison message 反复重投。
|
||||
if !isCumulativeGoogleRecharge(fields.rechargeType) {
|
||||
return firstrechargedomain.RechargeEvent{}, false, nil
|
||||
}
|
||||
return firstrechargedomain.RechargeEvent{
|
||||
AppCode: appcode.Normalize(message.AppCode),
|
||||
EventID: message.EventID,
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"hyapp/pkg/walletmq"
|
||||
firstrechargedomain "hyapp/services/activity-service/internal/domain/firstrecharge"
|
||||
)
|
||||
|
||||
func TestFirstRechargeEventFromWalletMessageSkipsNonGoogleRecharge(t *testing.T) {
|
||||
// 币商充值是合法钱包事实,但它没有 Google 商品 ID;首充 consumer 必须确认后跳过,避免 MQ 持续重投。
|
||||
body, err := walletmq.EncodeWalletOutboxMessage(walletmq.WalletOutboxMessage{
|
||||
AppCode: "lalu",
|
||||
EventID: "wev-coin-seller",
|
||||
EventType: firstrechargedomain.RechargeEventWalletRecorded,
|
||||
TransactionID: "wtx-coin-seller",
|
||||
CommandID: "coin-seller-transfer-1",
|
||||
UserID: 42001,
|
||||
AssetType: "COIN",
|
||||
AvailableDelta: 10000,
|
||||
PayloadJSON: `{"recharge_type":"coin_seller_transfer","amount":10000,"recharge_sequence":1}`,
|
||||
OccurredAtMS: 1710000000000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeWalletOutboxMessage failed: %v", err)
|
||||
}
|
||||
|
||||
event, ok, err := firstRechargeEventFromWalletMessage(body)
|
||||
if err != nil {
|
||||
t.Fatalf("firstRechargeEventFromWalletMessage returned error for non-google recharge: %v", err)
|
||||
}
|
||||
if ok || event.EventID != "" {
|
||||
t.Fatalf("non-google recharge should be acknowledged without first-recharge handling, ok=%v event=%+v", ok, event)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstRechargeEventFromWalletMessageKeepsGoogleRecharge(t *testing.T) {
|
||||
// Google 充值仍按商品 ID 进入首充奖励链路,缺商品字段应由 wallet outbox 事实修正,而不是泛化吞掉。
|
||||
body, err := walletmq.EncodeWalletOutboxMessage(walletmq.WalletOutboxMessage{
|
||||
AppCode: "lalu",
|
||||
EventID: "wev-google",
|
||||
EventType: firstrechargedomain.RechargeEventWalletRecorded,
|
||||
TransactionID: "wtx-google",
|
||||
CommandID: "google-pay-1",
|
||||
UserID: 42001,
|
||||
AssetType: "COIN",
|
||||
AvailableDelta: 160000,
|
||||
PayloadJSON: `{"recharge_type":"google","google_product_id":"coins_160000","recharge_usd_minor":199,"recharge_sequence":1}`,
|
||||
OccurredAtMS: 1710000000000,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("EncodeWalletOutboxMessage failed: %v", err)
|
||||
}
|
||||
|
||||
event, ok, err := firstRechargeEventFromWalletMessage(body)
|
||||
if err != nil {
|
||||
t.Fatalf("firstRechargeEventFromWalletMessage returned error: %v", err)
|
||||
}
|
||||
if !ok || event.RechargeType != "google" || event.GoogleProductID != "coins_160000" {
|
||||
t.Fatalf("google recharge should be routed to first-recharge handling, ok=%v event=%+v", ok, event)
|
||||
}
|
||||
}
|
||||
@ -4098,6 +4098,29 @@ func TestGrantResourceGroupExpandsWalletAssetAndEntitlement(t *testing.T) {
|
||||
if got := repository.CountRows("wallet_entries", "user_id = ? AND asset_type = ?", int64(42001), ledger.AssetCoin); got != 1 {
|
||||
t.Fatalf("group grant should write one coin wallet entry, got %d", got)
|
||||
}
|
||||
if got := repository.CountRows("wallet_transactions", "biz_type = ? AND command_id LIKE 'rgg:%' AND CHAR_LENGTH(command_id) <= 128", "resource_grant"); got != 1 {
|
||||
t.Fatalf("group wallet asset grant should use one short internal wallet command, got %d", got)
|
||||
}
|
||||
longCommand := resourcedomain.GrantResourceGroupCommand{
|
||||
CommandID: "cp_weekly_rank:1781481600000:1:cp_rel:327091463391285248:327104359357747200:327091463391285248",
|
||||
TargetUserID: 42002,
|
||||
GroupID: group.GroupID,
|
||||
Reason: "test grant long command",
|
||||
OperatorUserID: 90001,
|
||||
GrantSource: resourcedomain.GrantSourceAdmin,
|
||||
}
|
||||
if _, err := svc.GrantResourceGroup(ctx, longCommand); err != nil {
|
||||
t.Fatalf("GrantResourceGroup should accept long upstream command by shortening internal item commands: %v", err)
|
||||
}
|
||||
if got := repository.CountRows("resource_grants", "command_id = ?", longCommand.CommandID); got != 1 {
|
||||
t.Fatalf("long upstream command should still be preserved on resource grant, got %d", got)
|
||||
}
|
||||
if got := repository.CountRows("wallet_transactions", "biz_type = ? AND command_id LIKE 'rgg:%' AND CHAR_LENGTH(command_id) <= 128", "resource_grant"); got != 2 {
|
||||
t.Fatalf("long upstream group wallet asset grant should use a second short internal wallet command, got %d", got)
|
||||
}
|
||||
if got := repository.CountRows("wallet_transactions", "biz_type = ? AND CHAR_LENGTH(command_id) > 128", "resource_grant"); got != 0 {
|
||||
t.Fatalf("resource grant wallet transactions must not exceed command_id storage length, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRevokeResourceGroupGrantReversesWalletCreditAndEntitlement 锁定撤销只按原发放明细反向处理,不受资源组后续配置变化影响。
|
||||
@ -5769,8 +5792,8 @@ func TestConfirmGooglePaymentUsesProductNameAsGoogleProductID(t *testing.T) {
|
||||
if got := repository.CountRows("wallet_user_recharge_stats", "user_id = ? AND total_coin_amount = ? AND total_usd_minor_amount = ?", int64(53001), int64(1500), int64(150)); got != 1 {
|
||||
t.Fatalf("google payment should aggregate recharge USD minor from micro amount, got %d", got)
|
||||
}
|
||||
if got := repository.CountRows("wallet_outbox", "transaction_id = ? AND event_type = ? AND JSON_EXTRACT(payload, '$.target_country_id') = ? AND JSON_EXTRACT(payload, '$.country_id') = ? AND JSON_EXTRACT(payload, '$.target_region_id') = ? AND JSON_UNQUOTE(JSON_EXTRACT(payload, '$.recharge_type')) = ?", receipt.TransactionID, "WalletRechargeRecorded", int64(199), int64(199), int64(1001), ledger.RechargeChannelGoogle); got != 1 {
|
||||
t.Fatalf("google recharge fact should carry target country/region for BI, got %d", got)
|
||||
if got := repository.CountRows("wallet_outbox", "transaction_id = ? AND event_type = ? AND JSON_EXTRACT(payload, '$.target_country_id') = ? AND JSON_EXTRACT(payload, '$.country_id') = ? AND JSON_EXTRACT(payload, '$.target_region_id') = ? AND JSON_UNQUOTE(JSON_EXTRACT(payload, '$.recharge_type')) = ? AND JSON_UNQUOTE(JSON_EXTRACT(payload, '$.google_product_id')) = ? AND JSON_UNQUOTE(JSON_EXTRACT(payload, '$.product_name')) = ? AND JSON_UNQUOTE(JSON_EXTRACT(payload, '$.product_code')) = ?", receipt.TransactionID, "WalletRechargeRecorded", int64(199), int64(199), int64(1001), ledger.RechargeChannelGoogle, googleProductID, googleProductID, googleProductID); got != 1 {
|
||||
t.Fatalf("google recharge fact should carry target country/region and product identity for BI/activity, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -98,6 +98,9 @@ func (r *Repository) ConfirmGooglePayment(ctx context.Context, command ledger.Go
|
||||
Amount: product.CoinAmount,
|
||||
TargetAssetType: ledger.AssetCoin,
|
||||
TargetBalanceAfter: balanceAfter,
|
||||
GoogleProductID: product.ProductName,
|
||||
ProductCode: product.ProductCode,
|
||||
ProductName: product.ProductName,
|
||||
RechargeSequence: rechargeSequence,
|
||||
RechargeUSDMinor: rechargeUSDMinor,
|
||||
RechargeCurrencyCode: product.CurrencyCode,
|
||||
|
||||
@ -198,6 +198,9 @@ type coinSellerTransferMetadata struct {
|
||||
SellerUserID int64 `json:"seller_user_id"`
|
||||
TargetUserID int64 `json:"target_user_id"`
|
||||
TargetCountryID int64 `json:"target_country_id"`
|
||||
GoogleProductID string `json:"google_product_id,omitempty"`
|
||||
ProductCode string `json:"product_code,omitempty"`
|
||||
ProductName string `json:"product_name,omitempty"`
|
||||
SellerRegionID int64 `json:"seller_region_id"`
|
||||
TargetRegionID int64 `json:"target_region_id"`
|
||||
Amount int64 `json:"amount"`
|
||||
|
||||
@ -407,6 +407,9 @@ func rechargeRecordedEvent(transactionID string, commandID string, targetUserID
|
||||
"target_user_id": metadata.TargetUserID,
|
||||
"target_country_id": metadata.TargetCountryID,
|
||||
"country_id": metadata.TargetCountryID,
|
||||
"google_product_id": metadata.GoogleProductID,
|
||||
"product_code": metadata.ProductCode,
|
||||
"product_name": metadata.ProductName,
|
||||
"seller_region_id": metadata.SellerRegionID,
|
||||
"target_region_id": metadata.TargetRegionID,
|
||||
"region_id": metadata.TargetRegionID,
|
||||
|
||||
@ -409,19 +409,20 @@ func (r *Repository) applyWalletAssetGrantItem(ctx context.Context, tx *sql.Tx,
|
||||
if err != nil {
|
||||
return resourcedomain.ResourceGrantItem{}, err
|
||||
}
|
||||
walletCommandID := fmt.Sprintf("resource_group_grant:%s:item:%d:asset:%s", commandID, groupItem.GroupItemID, assetType)
|
||||
walletCommandID := resourceGroupWalletAssetCommandID(commandID, groupItem.GroupItemID, assetType)
|
||||
transactionID := transactionID(appcode.FromContext(ctx), walletCommandID)
|
||||
metadata := map[string]any{
|
||||
"app_code": appcode.FromContext(ctx),
|
||||
"grant_id": grantID,
|
||||
"command_id": commandID,
|
||||
"target_user_id": targetUserID,
|
||||
"group_id": groupItem.GroupID,
|
||||
"group_item_id": groupItem.GroupItemID,
|
||||
"item_type": resourcedomain.GroupItemTypeWalletAsset,
|
||||
"wallet_amount": groupItem.WalletAssetAmount,
|
||||
"wallet_asset": assetType,
|
||||
"operator_reason": "resource group grant",
|
||||
"app_code": appcode.FromContext(ctx),
|
||||
"grant_id": grantID,
|
||||
"command_id": commandID,
|
||||
"wallet_command_id": walletCommandID,
|
||||
"target_user_id": targetUserID,
|
||||
"group_id": groupItem.GroupID,
|
||||
"group_item_id": groupItem.GroupItemID,
|
||||
"item_type": resourcedomain.GroupItemTypeWalletAsset,
|
||||
"wallet_amount": groupItem.WalletAssetAmount,
|
||||
"wallet_asset": assetType,
|
||||
"operator_reason": "resource group grant",
|
||||
}
|
||||
if err := r.insertTransaction(ctx, tx, transactionID, walletCommandID, bizTypeResourceGrant, groupWalletAssetCreditHash(grantID, groupItem.GroupItemID, assetType, groupItem.WalletAssetAmount), grantID, metadata, nowMs); err != nil {
|
||||
return resourcedomain.ResourceGrantItem{}, err
|
||||
@ -835,6 +836,13 @@ func groupWalletAssetCreditHash(grantID string, groupItemID int64, assetType str
|
||||
return stableHash(fmt.Sprintf("resource_group_wallet_asset_credit|%s|%d|%s|%d", grantID, groupItemID, resourcedomain.NormalizeWalletAssetType(assetType), amount))
|
||||
}
|
||||
|
||||
func resourceGroupWalletAssetCommandID(commandID string, groupItemID int64, assetType string) string {
|
||||
// wallet_transactions.command_id 是对外幂等键字段,长度固定为 128;CP 周榜等上游命令本身可能很长,
|
||||
// 组内钱包资产 item 使用稳定短 hash 生成内部交易命令,同时在 metadata.command_id 保留完整上游命令用于审计。
|
||||
hash := stableHash(fmt.Sprintf("resource_group_grant_item|%s|%d|%s", strings.TrimSpace(commandID), groupItemID, resourcedomain.NormalizeWalletAssetType(assetType)))
|
||||
return fmt.Sprintf("rgg:%d:%s:%s", groupItemID, resourcedomain.NormalizeWalletAssetType(assetType), hash[:24])
|
||||
}
|
||||
|
||||
func resourceGrantWalletRevokeHash(grantID string, grantItemID int64, assetType string, amount int64) string {
|
||||
return stableHash(fmt.Sprintf("resource_grant_wallet_revoke|%s|%d|%s|%d", grantID, grantItemID, resourcedomain.NormalizeWalletAssetType(assetType), amount))
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user