Files
platforms/backend/api/internal/logic/common/recharge_remote_test.go
czl231 3ef33b531d 已完成用户APP首期功能开发
交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
2026-09-13 00:57:32 +08:00

142 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 功能描述远程事务内验证充值幂等、本人查询、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("创建幂等、金额渠道冲突、本人查询恢复、跨主体拒绝、一次入账与回滚通过")
}