// 功能描述:远程事务内验证充值幂等、本人查询、Mock入账及整体回滚;版本:1.0.0。 package common import ( "encoding/json" "fmt" "net/http/httptest" "os" "testing" "time" "git.apinb.com/bsm-sdk/core/database" dbsql "git.apinb.com/bsm-sdk/core/database/sql" "git.apinb.com/bsm-sdk/core/types" "git.apinb.com/heqiapp/platforms/backend/api/internal/config" "git.apinb.com/heqiapp/platforms/backend/api/internal/impl" "git.apinb.com/heqiapp/platforms/backend/api/internal/models" "github.com/gin-gonic/gin" "gorm.io/gorm" "gorm.io/gorm/logger" ) // TestRechargeRemoteRollback 不调真实支付渠道,所有资金与账户夹具均处于最终回滚事务内。 func TestRechargeRemoteRollback(t *testing.T) { if os.Getenv("HEQI_REMOTE_RECHARGE_TEST") != "1" { t.Skip("需显式开启远程回滚测试") } config.New("heqi") db, err := database.NewDatabase(config.Spec.Databases.Driver, config.Spec.Databases.Source, dbsql.SetOptions(nil)) if err != nil { t.Fatal("远程数据库连接失败") } db = db.Session(&gorm.Session{Logger: logger.Default.LogMode(logger.Silent)}) connection, err := db.DB() if err != nil { t.Fatal(err) } defer connection.Close() tx := db.Begin() if tx.Error != nil { t.Fatal(tx.Error) } defer tx.Rollback() if err := tx.Exec(`SET LOCAL lock_timeout = '3s'`).Error; err != nil { t.Fatal(err) } oldDB, oldMock := impl.DBService, config.Spec.Global.MockPaymentEnabled impl.DBService, config.Spec.Global.MockPaymentEnabled = tx, true defer func() { impl.DBService, config.Spec.Global.MockPaymentEnabled = oldDB, oldMock }() account := models.UserAccount{Entity: NewEntity(1), Name: "充值回滚验证", Phone: fmt.Sprintf("199%08d", time.Now().UnixNano()%100000000)} account.Username = "verify-recharge-" + account.Identity if err := tx.Create(&account).Error; err != nil { t.Fatal("创建独立测试账号失败") } request := "verify-recharge-" + models.NewIdentity() call := func(handler gin.HandlerFunc, body string, params gin.Params, query string, expected int) map[string]any { ctx, response := paymentTestContext(body) ctx.Set("Auth", &types.JwtClaims{Client: "user_app", Identity: account.Identity}) ctx.Params = params if query != "" { ctx.Request = httptest.NewRequest("GET", query, nil) } handler(ctx) var result struct { Code int `json:"code"` Details json.RawMessage `json:"details"` } if err := json.Unmarshal(response.Body.Bytes(), &result); err != nil || result.Code != expected { t.Fatalf("接口结果不符,期望%d,响应%s", expected, response.Body.String()) } details := map[string]any{} if expected == 0 { if err := json.Unmarshal(result.Details, &details); err != nil { t.Fatal(err) } } return details } body := fmt.Sprintf(`{"amount":2300,"channel":"mock","request_no":%q}`, request) first := call(CreateRecharge("user_app"), body, nil, "", 0) identity, ok := first["identity"].(string) if !ok || identity == "" { t.Fatal("缺少充值标识") } second := call(CreateRecharge("user_app"), body, nil, "", 0) if second["identity"] != identity { t.Fatal("幂等重试生成了另一充值单") } call(CreateRecharge("user_app"), fmt.Sprintf(`{"amount":2400,"channel":"mock","request_no":%q}`, request), nil, "", 2411) call(CreateRecharge("user_app"), fmt.Sprintf(`{"amount":2300,"channel":"alipay","request_no":%q}`, request), nil, "", 2411) params := gin.Params{{Key: "identity", Value: identity}} pending := call(GetRecharge("user_app"), "", params, "/wallet/recharges/owned", 0) if pending["recharge_status"] != float64(10) || pending["payment_status"] != nil { t.Fatal("待确认充值被误报为已支付") } recovered := call(GetRecharge("user_app"), "", gin.Params{{Key: "request", Value: request}}, "/wallet/recharge-requests/owned", 0) if recovered["identity"] != identity || recovered["wallet_basic_id"] != nil || recovered["owner_identity"] != nil { t.Fatal("恢复查询标识不一致或暴露内部字段") } // 临时改变夹具归属类型,验证不能跨主体类型读取同一个身份值的记录。 if err := tx.Model(&models.WalletRechargeOrder{}).Where("identity = ?", identity).Update("owner_type", "staff").Error; err != nil { t.Fatal(err) } call(GetRecharge("user_app"), "", params, "/wallet/recharges/foreign", 1112) call(CreateRecharge("user_app"), body, nil, "", 2411) if err := tx.Model(&models.WalletRechargeOrder{}).Where("identity = ?", identity).Update("owner_type", "user").Error; err != nil { t.Fatal(err) } call(ConfirmMockRecharge("user_app"), "", params, "", 0) call(ConfirmMockRecharge("user_app"), "", params, "", 0) paid := call(GetRecharge("user_app"), "", params, "/wallet/recharges/owned", 0) if paid["recharge_status"] != float64(23) || paid["completed_at"] == nil { t.Fatal("入账完成状态不正确") } list := call(ListRecharges("user_app"), "", nil, "/wallet/recharges", 0) if len(list["items"].([]any)) != 1 { t.Fatal("充值记录重复或归属异常") } var wallet models.WalletBasic if err := tx.Where("owner_type = ? AND owner_identity = ?", "user", account.Identity).First(&wallet).Error; err != nil { t.Fatal(err) } if wallet.Balance != 2300 || wallet.WithdrawalBalance != 0 { t.Fatal("重复入账或可提现余额被改变") } var records int64 if err := tx.Model(&models.WalletRecord{}).Where("wallet_basic_id = ?", wallet.ID).Count(&records).Error; err != nil || records != 1 { t.Fatal("流水数量错误") } if err := tx.Rollback().Error; err != nil { t.Fatal(err) } var remaining int64 if err := db.Model(&models.WalletRechargeOrder{}).Where("request_no = ?", request).Count(&remaining).Error; err != nil || remaining != 0 { t.Fatal("充值夹具未完整回滚") } if err := db.Model(&models.WalletBasic{}).Where("owner_identity = ?", account.Identity).Count(&remaining).Error; err != nil || remaining != 0 { t.Fatal("钱包夹具未完整回滚") } t.Log("创建幂等、金额渠道冲突、本人查询恢复、跨主体拒绝、一次入账与回滚通过") }