已完成用户APP首期功能开发

交付用户端首期页面、配套接口、后台资源及测试文档。用户APP构建、静态分析和三个管理后台构建通过;完整测试仍有2项失败,后端模型注释检查未通过,详见交付记录。
This commit is contained in:
czl231
2026-09-13 00:57:32 +08:00
parent a0a4bb1218
commit 3ef33b531d
793 changed files with 40959 additions and 1204 deletions

View File

@@ -0,0 +1,141 @@
// 功能描述远程事务内验证充值幂等、本人查询、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("创建幂等、金额渠道冲突、本人查询恢复、跨主体拒绝、一次入账与回滚通过")
}