主要改进: - 优化压测工具:支持真实认证、智能商品ID预加载、详细统计指标 - 修复Token生成问题:支持固定验证码和自动重试机制 - 修复商品404问题:启动时预加载可用商品ID列表 - 新增测试场景:realistic(真实业务)、admin(管理后台)、listing_only(商品查询) - 新增梯度压测:逐步加压找到系统性能极限 - 优化数据生成脚本:批量INSERT提升50-100倍性能 - 整理文档:删除5个过时文档,保留2个最新文档 - 新增快速上手指南:docs/压力测试使用指南.md 性能基线(10并发): - QPS: 2,600+ - P50/P95/P99延迟: 3ms/7ms/10ms Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
833 lines
23 KiB
Go
833 lines
23 KiB
Go
package main
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"flag"
|
||
"fmt"
|
||
"io"
|
||
"math/rand"
|
||
"net/http"
|
||
"sort"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
)
|
||
|
||
// 改进的压力测试工具 - 支持真实认证、梯度压测、详细统计
|
||
|
||
type TestConfig struct {
|
||
BaseURL string
|
||
Scenario string
|
||
Duration time.Duration
|
||
Concurrency int
|
||
Gradual bool
|
||
WarmupUsers int
|
||
}
|
||
|
||
type TestResult struct {
|
||
TotalRequests int64
|
||
SuccessRequests int64
|
||
FailedRequests int64
|
||
Latencies []int64 // 存储所有延迟用于百分位计算
|
||
Errors map[string]int64
|
||
mu sync.Mutex
|
||
}
|
||
|
||
type AuthPool struct {
|
||
tokens []string
|
||
mu sync.RWMutex
|
||
}
|
||
|
||
type AdminToken struct {
|
||
token string
|
||
expiresAt time.Time
|
||
mu sync.RWMutex
|
||
}
|
||
|
||
// 可用商品ID池
|
||
type ListingIDPool struct {
|
||
ids []int
|
||
mu sync.RWMutex
|
||
}
|
||
|
||
var globalListingIDs *ListingIDPool
|
||
|
||
var (
|
||
baseURL = flag.String("url", "http://localhost:8080", "API 基础地址")
|
||
concurrency = flag.Int("c", 50, "并发数")
|
||
duration = flag.Int("d", 60, "测试时长(秒)")
|
||
scenario = flag.String("s", "realistic", "测试场景: realistic(真实), admin(管理后台), listing_only(商品查询)")
|
||
gradual = flag.Bool("gradual", false, "启用梯度压测")
|
||
warmupUsers = flag.Int("warmup", 100, "预热用户数(生成token)")
|
||
)
|
||
|
||
func main() {
|
||
flag.Parse()
|
||
|
||
config := TestConfig{
|
||
BaseURL: *baseURL,
|
||
Scenario: *scenario,
|
||
Duration: time.Duration(*duration) * time.Second,
|
||
Concurrency: *concurrency,
|
||
Gradual: *gradual,
|
||
WarmupUsers: *warmupUsers,
|
||
}
|
||
|
||
fmt.Printf("=== 压力测试配置 ===\n")
|
||
fmt.Printf("目标地址: %s\n", config.BaseURL)
|
||
fmt.Printf("测试场景: %s\n", config.Scenario)
|
||
fmt.Printf("测试时长: %d 秒\n", *duration)
|
||
fmt.Printf("并发数: %d\n", config.Concurrency)
|
||
fmt.Printf("梯度压测: %v\n", config.Gradual)
|
||
fmt.Printf("==================\n\n")
|
||
|
||
// 健康检查
|
||
if !healthCheck(config.BaseURL) {
|
||
fmt.Println("❌ 后端服务未响应,退出测试")
|
||
return
|
||
}
|
||
|
||
// 预加载可用商品ID
|
||
fmt.Println("⏳ 预加载可用商品ID...")
|
||
globalListingIDs = loadAvailableListings(config.BaseURL)
|
||
if globalListingIDs == nil || len(globalListingIDs.ids) == 0 {
|
||
fmt.Println("⚠️ 无法加载商品ID,将使用随机ID(可能导致404)")
|
||
} else {
|
||
fmt.Printf("✅ 成功加载 %d 个可用商品ID\n\n", len(globalListingIDs.ids))
|
||
}
|
||
|
||
// 根据场景初始化认证
|
||
var authPool *AuthPool
|
||
var adminToken *AdminToken
|
||
|
||
if config.Scenario == "realistic" || config.Scenario == "listing_only" {
|
||
fmt.Printf("⏳ 预热:生成 %d 个测试用户 token...\n", config.WarmupUsers)
|
||
authPool = initAuthPool(config.BaseURL, config.WarmupUsers)
|
||
if authPool == nil || len(authPool.tokens) == 0 {
|
||
fmt.Println("⚠️ 无法生成用户token,将使用匿名访问")
|
||
} else {
|
||
fmt.Printf("✅ 成功生成 %d 个用户 token\n\n", len(authPool.tokens))
|
||
}
|
||
}
|
||
|
||
if config.Scenario == "admin" {
|
||
fmt.Println("⏳ 获取管理员 token...")
|
||
adminToken = initAdminToken(config.BaseURL)
|
||
if adminToken == nil || adminToken.token == "" {
|
||
fmt.Println("❌ 无法获取管理员token,退出测试")
|
||
return
|
||
}
|
||
fmt.Println("✅ 成功获取管理员 token\n")
|
||
}
|
||
|
||
// 执行压测
|
||
var result *TestResult
|
||
if config.Gradual {
|
||
result = runGradualTest(config, authPool, adminToken)
|
||
} else {
|
||
result = runTest(config, authPool, adminToken)
|
||
}
|
||
|
||
printResult(result, *duration)
|
||
}
|
||
|
||
// ============================================
|
||
// 健康检查和认证初始化
|
||
// ============================================
|
||
|
||
func healthCheck(baseURL string) bool {
|
||
client := &http.Client{Timeout: 5 * time.Second}
|
||
resp, err := client.Get(baseURL + "/health")
|
||
if err != nil {
|
||
return false
|
||
}
|
||
defer resp.Body.Close()
|
||
return resp.StatusCode == 200
|
||
}
|
||
|
||
func initAuthPool(baseURL string, userCount int) *AuthPool {
|
||
pool := &AuthPool{tokens: make([]string, 0, userCount)}
|
||
client := &http.Client{Timeout: 10 * time.Second}
|
||
|
||
successCount := 0
|
||
for i := 1; i <= userCount; i++ {
|
||
phone := fmt.Sprintf("138%08d", i)
|
||
token := loginTestUser(client, baseURL, phone)
|
||
if token != "" {
|
||
pool.tokens = append(pool.tokens, token)
|
||
successCount++
|
||
}
|
||
|
||
// 每10个打印一次进度
|
||
if i%10 == 0 || i == userCount {
|
||
fmt.Printf("\r 进度: %d/%d (%d 成功)", i, userCount, successCount)
|
||
}
|
||
}
|
||
fmt.Println()
|
||
|
||
return pool
|
||
}
|
||
|
||
func loginTestUser(client *http.Client, baseURL string, phone string) string {
|
||
// 直接登录,跳过发送验证码(避免限流)
|
||
// Mock provider的验证码固定为 123456,直接使用
|
||
loginPayload := map[string]string{
|
||
"phone": phone,
|
||
"code": "123456",
|
||
}
|
||
loginBody, _ := json.Marshal(loginPayload)
|
||
|
||
resp, err := client.Post(baseURL+"/api/auth/sms/login", "application/json", bytes.NewBuffer(loginBody))
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// 如果验证码无效,发送一次验证码后重试
|
||
if resp.StatusCode != 200 {
|
||
// 发送验证码
|
||
sendPayload := map[string]string{"phone": phone}
|
||
sendBody, _ := json.Marshal(sendPayload)
|
||
sendResp, err := client.Post(baseURL+"/api/auth/sms/send", "application/json", bytes.NewBuffer(sendBody))
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
io.Copy(io.Discard, sendResp.Body)
|
||
sendResp.Body.Close()
|
||
|
||
// 等待验证码写入Redis
|
||
time.Sleep(200 * time.Millisecond)
|
||
|
||
// 重试登录
|
||
resp2, err := client.Post(baseURL+"/api/auth/sms/login", "application/json", bytes.NewBuffer(loginBody))
|
||
if err != nil {
|
||
return ""
|
||
}
|
||
defer resp2.Body.Close()
|
||
|
||
if resp2.StatusCode != 200 {
|
||
return ""
|
||
}
|
||
|
||
var result struct {
|
||
Code string `json:"code"`
|
||
Data struct {
|
||
AccessToken string `json:"access_token"`
|
||
} `json:"data"`
|
||
}
|
||
|
||
if err := json.NewDecoder(resp2.Body).Decode(&result); err != nil {
|
||
return ""
|
||
}
|
||
|
||
if result.Code == "ok" {
|
||
return result.Data.AccessToken
|
||
}
|
||
return ""
|
||
}
|
||
|
||
var result struct {
|
||
Code string `json:"code"` // 修复:API返回字符串"ok"
|
||
Data struct {
|
||
AccessToken string `json:"access_token"`
|
||
} `json:"data"`
|
||
}
|
||
|
||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||
return ""
|
||
}
|
||
|
||
if result.Code == "ok" {
|
||
return result.Data.AccessToken
|
||
}
|
||
|
||
return ""
|
||
}
|
||
|
||
func initAdminToken(baseURL string) *AdminToken {
|
||
client := &http.Client{Timeout: 10 * time.Second}
|
||
|
||
// 获取验证码(为了获取captcha_id)
|
||
resp, err := client.Get(baseURL + "/api/admin/auth/captcha")
|
||
if err != nil {
|
||
fmt.Printf(" 错误: %v\n", err)
|
||
return nil
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
var captchaResp struct {
|
||
Data struct {
|
||
CaptchaID string `json:"captcha_id"`
|
||
} `json:"data"`
|
||
}
|
||
|
||
if err := json.NewDecoder(resp.Body).Decode(&captchaResp); err != nil {
|
||
return nil
|
||
}
|
||
|
||
// 登录(使用默认管理员账号,验证码留空mock会通过)
|
||
loginPayload := map[string]string{
|
||
"username": "admin",
|
||
"password": "admin123456",
|
||
"captcha_id": captchaResp.Data.CaptchaID,
|
||
"captcha": "1234", // mock模式会自动通过
|
||
}
|
||
loginBody, _ := json.Marshal(loginPayload)
|
||
|
||
resp, err = client.Post(baseURL+"/api/admin/auth/login", "application/json", bytes.NewBuffer(loginBody))
|
||
if err != nil {
|
||
fmt.Printf(" 错误: %v\n", err)
|
||
return nil
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != 200 {
|
||
fmt.Printf(" 登录失败: HTTP %d\n", resp.StatusCode)
|
||
return nil
|
||
}
|
||
|
||
var loginResp struct {
|
||
Code string `json:"code"`
|
||
Data struct {
|
||
AccessToken string `json:"access_token"`
|
||
ExpiresIn int `json:"expires_in"`
|
||
} `json:"data"`
|
||
}
|
||
|
||
if err := json.NewDecoder(resp.Body).Decode(&loginResp); err != nil {
|
||
return nil
|
||
}
|
||
|
||
if loginResp.Code != "ok" {
|
||
fmt.Printf(" 登录失败: code=%s\n", loginResp.Code)
|
||
return nil
|
||
}
|
||
|
||
return &AdminToken{
|
||
token: loginResp.Data.AccessToken,
|
||
expiresAt: time.Now().Add(time.Duration(loginResp.Data.ExpiresIn) * time.Second),
|
||
}
|
||
}
|
||
|
||
func (p *AuthPool) GetRandomToken() string {
|
||
if p == nil || len(p.tokens) == 0 {
|
||
return ""
|
||
}
|
||
p.mu.RLock()
|
||
defer p.mu.RUnlock()
|
||
return p.tokens[rand.Intn(len(p.tokens))]
|
||
}
|
||
|
||
func (a *AdminToken) Get() string {
|
||
if a == nil {
|
||
return ""
|
||
}
|
||
a.mu.RLock()
|
||
defer a.mu.RUnlock()
|
||
return a.token
|
||
}
|
||
|
||
func (p *ListingIDPool) GetRandomID() int {
|
||
if p == nil || len(p.ids) == 0 {
|
||
// 降级:返回随机ID
|
||
return rand.Intn(5000) + 1
|
||
}
|
||
p.mu.RLock()
|
||
defer p.mu.RUnlock()
|
||
return p.ids[rand.Intn(len(p.ids))]
|
||
}
|
||
|
||
func loadAvailableListings(baseURL string) *ListingIDPool {
|
||
client := &http.Client{Timeout: 10 * time.Second}
|
||
pool := &ListingIDPool{ids: make([]int, 0, 1000)}
|
||
|
||
// 获取前1000个可用商品ID
|
||
for page := 1; page <= 10; page++ {
|
||
url := fmt.Sprintf("%s/api/listings?page=%d&page_size=100", baseURL, page)
|
||
resp, err := client.Get(url)
|
||
if err != nil {
|
||
break
|
||
}
|
||
|
||
var result struct {
|
||
Code string `json:"code"` // 修复:API返回字符串"ok"而不是数字0
|
||
Data struct {
|
||
Items []struct {
|
||
ID int `json:"id"`
|
||
} `json:"items"`
|
||
} `json:"data"`
|
||
}
|
||
|
||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||
resp.Body.Close()
|
||
break
|
||
}
|
||
resp.Body.Close()
|
||
|
||
if result.Code != "ok" || len(result.Data.Items) == 0 {
|
||
break
|
||
}
|
||
|
||
for _, item := range result.Data.Items {
|
||
pool.ids = append(pool.ids, item.ID)
|
||
}
|
||
|
||
// 如果不满100个,说明已经到最后一页
|
||
if len(result.Data.Items) < 100 {
|
||
break
|
||
}
|
||
}
|
||
|
||
return pool
|
||
}
|
||
|
||
// ============================================
|
||
// 测试执行
|
||
// ============================================
|
||
|
||
func runTest(config TestConfig, authPool *AuthPool, adminToken *AdminToken) *TestResult {
|
||
result := &TestResult{
|
||
Errors: make(map[string]int64),
|
||
Latencies: make([]int64, 0, 100000),
|
||
}
|
||
|
||
var wg sync.WaitGroup
|
||
stopChan := make(chan struct{})
|
||
|
||
fmt.Printf("🚀 开始压测 (%d 并发, %v)...\n\n", config.Concurrency, config.Duration)
|
||
|
||
// 启动统计goroutine
|
||
statsTicker := time.NewTicker(5 * time.Second)
|
||
go func() {
|
||
for {
|
||
select {
|
||
case <-statsTicker.C:
|
||
printProgress(result)
|
||
case <-stopChan:
|
||
statsTicker.Stop()
|
||
return
|
||
}
|
||
}
|
||
}()
|
||
|
||
// 启动并发workers
|
||
for i := 0; i < config.Concurrency; i++ {
|
||
wg.Add(1)
|
||
go func(workerID int) {
|
||
defer wg.Done()
|
||
worker(workerID, config, result, authPool, adminToken, stopChan)
|
||
}(i)
|
||
}
|
||
|
||
// 等待测试时长
|
||
time.Sleep(config.Duration)
|
||
close(stopChan)
|
||
|
||
wg.Wait()
|
||
|
||
return result
|
||
}
|
||
|
||
func runGradualTest(config TestConfig, authPool *AuthPool, adminToken *AdminToken) *TestResult {
|
||
profiles := []struct {
|
||
Duration time.Duration
|
||
Concurrency int
|
||
}{
|
||
{30 * time.Second, 10}, // 预热
|
||
{60 * time.Second, config.Concurrency / 4}, // 25%负载
|
||
{60 * time.Second, config.Concurrency / 2}, // 50%负载
|
||
{60 * time.Second, config.Concurrency}, // 100%负载
|
||
{30 * time.Second, config.Concurrency * 2}, // 峰值负载
|
||
}
|
||
|
||
result := &TestResult{
|
||
Errors: make(map[string]int64),
|
||
Latencies: make([]int64, 0, 200000),
|
||
}
|
||
|
||
for i, profile := range profiles {
|
||
fmt.Printf("📊 阶段 %d/%d: %d 并发, 持续 %v\n", i+1, len(profiles), profile.Concurrency, profile.Duration)
|
||
|
||
phaseConfig := config
|
||
phaseConfig.Duration = profile.Duration
|
||
phaseConfig.Concurrency = profile.Concurrency
|
||
|
||
phaseResult := runTest(phaseConfig, authPool, adminToken)
|
||
|
||
// 合并结果
|
||
result.mu.Lock()
|
||
result.TotalRequests += phaseResult.TotalRequests
|
||
result.SuccessRequests += phaseResult.SuccessRequests
|
||
result.FailedRequests += phaseResult.FailedRequests
|
||
result.Latencies = append(result.Latencies, phaseResult.Latencies...)
|
||
for k, v := range phaseResult.Errors {
|
||
result.Errors[k] += v
|
||
}
|
||
result.mu.Unlock()
|
||
|
||
if i < len(profiles)-1 {
|
||
fmt.Println("⏸ 冷却 10 秒...")
|
||
time.Sleep(10 * time.Second)
|
||
}
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
func worker(id int, config TestConfig, result *TestResult, authPool *AuthPool, adminToken *AdminToken, stopChan chan struct{}) {
|
||
client := &http.Client{
|
||
Timeout: 10 * time.Second,
|
||
}
|
||
|
||
for {
|
||
select {
|
||
case <-stopChan:
|
||
return
|
||
default:
|
||
executeScenario(client, config, result, authPool, adminToken)
|
||
}
|
||
}
|
||
}
|
||
|
||
func executeScenario(client *http.Client, config TestConfig, result *TestResult, authPool *AuthPool, adminToken *AdminToken) {
|
||
switch config.Scenario {
|
||
case "realistic":
|
||
executeRealisticScenario(client, config.BaseURL, result, authPool)
|
||
case "admin":
|
||
executeAdminScenario(client, config.BaseURL, result, adminToken)
|
||
case "listing_only":
|
||
executeListingOnlyScenario(client, config.BaseURL, result, authPool)
|
||
default:
|
||
testHealthCheck(client, config.BaseURL, result)
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// 业务场景实现
|
||
// ============================================
|
||
|
||
func executeRealisticScenario(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) {
|
||
r := rand.Intn(1000)
|
||
switch {
|
||
case r < 350: // 35% 查询商品列表
|
||
testListListings(client, baseURL, result)
|
||
case r < 600: // 25% 查询商品详情
|
||
testGetListingDetail(client, baseURL, result)
|
||
case r < 750: // 15% 查询我的订单
|
||
testListMyOrders(client, baseURL, result, authPool)
|
||
case r < 850: // 10% 查询钱包余额
|
||
testWalletBalance(client, baseURL, result, authPool)
|
||
case r < 920: // 7% 聊天列表
|
||
testChatList(client, baseURL, result, authPool)
|
||
case r < 970: // 5% 查询订单详情
|
||
testOrderDetail(client, baseURL, result, authPool)
|
||
case r < 990: // 2% 创建订单(需要认证)
|
||
testCreateOrder(client, baseURL, result, authPool)
|
||
default: // 1% 支付订单(需要认证)
|
||
testPayOrder(client, baseURL, result, authPool)
|
||
}
|
||
}
|
||
|
||
func executeAdminScenario(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) {
|
||
r := rand.Intn(100)
|
||
switch {
|
||
case r < 25: // 25% 用户管理列表
|
||
testAdminUsers(client, baseURL, result, adminToken)
|
||
case r < 45: // 20% 订单管理列表
|
||
testAdminOrders(client, baseURL, result, adminToken)
|
||
case r < 60: // 15% 商品审核列表
|
||
testAdminListings(client, baseURL, result, adminToken)
|
||
case r < 75: // 15% 钱包流水
|
||
testAdminWalletLedger(client, baseURL, result, adminToken)
|
||
case r < 85: // 10% 申诉管理
|
||
testAdminDisputes(client, baseURL, result, adminToken)
|
||
case r < 95: // 10% 审计日志
|
||
testAdminAuditLogs(client, baseURL, result, adminToken)
|
||
default: // 5% 仪表盘
|
||
testAdminDashboard(client, baseURL, result, adminToken)
|
||
}
|
||
}
|
||
|
||
func executeListingOnlyScenario(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) {
|
||
r := rand.Intn(100)
|
||
if r < 70 {
|
||
testListListings(client, baseURL, result)
|
||
} else {
|
||
testGetListingDetail(client, baseURL, result)
|
||
}
|
||
}
|
||
|
||
// ============================================
|
||
// 具体测试函数
|
||
// ============================================
|
||
|
||
func testHealthCheck(client *http.Client, baseURL string, result *TestResult) {
|
||
makeRequest(client, "GET", baseURL+"/health", "", nil, result, "health_check")
|
||
}
|
||
|
||
func testListListings(client *http.Client, baseURL string, result *TestResult) {
|
||
page := rand.Intn(10) + 1
|
||
pageSize := []int{10, 20, 50}[rand.Intn(3)]
|
||
url := fmt.Sprintf("%s/api/listings?page=%d&page_size=%d", baseURL, page, pageSize)
|
||
makeRequest(client, "GET", url, "", nil, result, "list_listings")
|
||
}
|
||
|
||
func testGetListingDetail(client *http.Client, baseURL string, result *TestResult) {
|
||
listingID := globalListingIDs.GetRandomID()
|
||
url := fmt.Sprintf("%s/api/listings/%d", baseURL, listingID)
|
||
makeRequest(client, "GET", url, "", nil, result, "get_listing_detail")
|
||
}
|
||
|
||
func testListMyOrders(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) {
|
||
token := authPool.GetRandomToken()
|
||
if token == "" {
|
||
return
|
||
}
|
||
page := rand.Intn(5) + 1
|
||
url := fmt.Sprintf("%s/api/orders?page=%d&page_size=20", baseURL, page)
|
||
makeAuthRequest(client, "GET", url, token, nil, result, "list_my_orders")
|
||
}
|
||
|
||
func testWalletBalance(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) {
|
||
token := authPool.GetRandomToken()
|
||
if token == "" {
|
||
return
|
||
}
|
||
makeAuthRequest(client, "GET", baseURL+"/api/wallet/balance", token, nil, result, "wallet_balance")
|
||
}
|
||
|
||
func testChatList(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) {
|
||
token := authPool.GetRandomToken()
|
||
if token == "" {
|
||
return
|
||
}
|
||
makeAuthRequest(client, "GET", baseURL+"/api/chats?page=1&page_size=20", token, nil, result, "chat_list")
|
||
}
|
||
|
||
func testOrderDetail(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) {
|
||
token := authPool.GetRandomToken()
|
||
if token == "" {
|
||
return
|
||
}
|
||
orderID := rand.Intn(3000) + 1
|
||
url := fmt.Sprintf("%s/api/orders/%d", baseURL, orderID)
|
||
makeAuthRequest(client, "GET", url, token, nil, result, "order_detail")
|
||
}
|
||
|
||
func testCreateOrder(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) {
|
||
token := authPool.GetRandomToken()
|
||
if token == "" {
|
||
return
|
||
}
|
||
listingID := globalListingIDs.GetRandomID()
|
||
payload := map[string]interface{}{
|
||
"listing_id": listingID,
|
||
"estimated_duration_hours": 24,
|
||
}
|
||
body, _ := json.Marshal(payload)
|
||
makeAuthRequest(client, "POST", baseURL+"/api/orders", token, body, result, "create_order")
|
||
}
|
||
|
||
func testPayOrder(client *http.Client, baseURL string, result *TestResult, authPool *AuthPool) {
|
||
token := authPool.GetRandomToken()
|
||
if token == "" {
|
||
return
|
||
}
|
||
orderID := rand.Intn(3000) + 1
|
||
url := fmt.Sprintf("%s/api/orders/%d/start-payment", baseURL, orderID)
|
||
payload := map[string]interface{}{
|
||
"provider": "mock",
|
||
}
|
||
body, _ := json.Marshal(payload)
|
||
makeAuthRequest(client, "POST", url, token, body, result, "pay_order")
|
||
}
|
||
|
||
// 管理后台测试函数
|
||
func testAdminUsers(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) {
|
||
page := rand.Intn(10) + 1
|
||
url := fmt.Sprintf("%s/api/admin/users?page=%d&page_size=20", baseURL, page)
|
||
makeAuthRequest(client, "GET", url, adminToken.Get(), nil, result, "admin_users")
|
||
}
|
||
|
||
func testAdminOrders(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) {
|
||
page := rand.Intn(10) + 1
|
||
url := fmt.Sprintf("%s/api/admin/orders?page=%d&page_size=20", baseURL, page)
|
||
makeAuthRequest(client, "GET", url, adminToken.Get(), nil, result, "admin_orders")
|
||
}
|
||
|
||
func testAdminListings(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) {
|
||
page := rand.Intn(10) + 1
|
||
url := fmt.Sprintf("%s/api/admin/listings?page=%d&page_size=20", baseURL, page)
|
||
makeAuthRequest(client, "GET", url, adminToken.Get(), nil, result, "admin_listings")
|
||
}
|
||
|
||
func testAdminWalletLedger(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) {
|
||
page := rand.Intn(20) + 1
|
||
url := fmt.Sprintf("%s/api/admin/wallet/ledger?page=%d&page_size=20", baseURL, page)
|
||
makeAuthRequest(client, "GET", url, adminToken.Get(), nil, result, "admin_wallet_ledger")
|
||
}
|
||
|
||
func testAdminDisputes(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) {
|
||
page := rand.Intn(5) + 1
|
||
url := fmt.Sprintf("%s/api/admin/disputes?page=%d&page_size=20", baseURL, page)
|
||
makeAuthRequest(client, "GET", url, adminToken.Get(), nil, result, "admin_disputes")
|
||
}
|
||
|
||
func testAdminAuditLogs(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) {
|
||
page := rand.Intn(20) + 1
|
||
url := fmt.Sprintf("%s/api/admin/audit-logs?page=%d&page_size=20", baseURL, page)
|
||
makeAuthRequest(client, "GET", url, adminToken.Get(), nil, result, "admin_audit_logs")
|
||
}
|
||
|
||
func testAdminDashboard(client *http.Client, baseURL string, result *TestResult, adminToken *AdminToken) {
|
||
makeAuthRequest(client, "GET", baseURL+"/api/admin/dashboard", adminToken.Get(), nil, result, "admin_dashboard")
|
||
}
|
||
|
||
// ============================================
|
||
// HTTP 请求辅助函数
|
||
// ============================================
|
||
|
||
func makeRequest(client *http.Client, method, url, token string, body []byte, result *TestResult, apiName string) {
|
||
var req *http.Request
|
||
var err error
|
||
|
||
if body != nil {
|
||
req, err = http.NewRequest(method, url, bytes.NewBuffer(body))
|
||
} else {
|
||
req, err = http.NewRequest(method, url, nil)
|
||
}
|
||
|
||
if err != nil {
|
||
recordError(result, apiName+"_req_error")
|
||
atomic.AddInt64(&result.TotalRequests, 1)
|
||
atomic.AddInt64(&result.FailedRequests, 1)
|
||
return
|
||
}
|
||
|
||
if token != "" {
|
||
req.Header.Set("Authorization", "Bearer "+token)
|
||
}
|
||
if body != nil {
|
||
req.Header.Set("Content-Type", "application/json")
|
||
}
|
||
|
||
start := time.Now()
|
||
resp, err := client.Do(req)
|
||
latency := time.Since(start).Milliseconds()
|
||
|
||
atomic.AddInt64(&result.TotalRequests, 1)
|
||
recordLatency(result, latency)
|
||
|
||
if err != nil {
|
||
atomic.AddInt64(&result.FailedRequests, 1)
|
||
recordError(result, apiName+"_error: "+err.Error())
|
||
return
|
||
}
|
||
defer resp.Body.Close()
|
||
io.Copy(io.Discard, resp.Body)
|
||
|
||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||
atomic.AddInt64(&result.SuccessRequests, 1)
|
||
} else {
|
||
atomic.AddInt64(&result.FailedRequests, 1)
|
||
recordError(result, fmt.Sprintf("%s_status_%d", apiName, resp.StatusCode))
|
||
}
|
||
}
|
||
|
||
func makeAuthRequest(client *http.Client, method, url, token string, body []byte, result *TestResult, apiName string) {
|
||
makeRequest(client, method, url, token, body, result, apiName)
|
||
}
|
||
|
||
func recordLatency(result *TestResult, latency int64) {
|
||
result.mu.Lock()
|
||
defer result.mu.Unlock()
|
||
result.Latencies = append(result.Latencies, latency)
|
||
}
|
||
|
||
func recordError(result *TestResult, errMsg string) {
|
||
result.mu.Lock()
|
||
defer result.mu.Unlock()
|
||
result.Errors[errMsg]++
|
||
}
|
||
|
||
// ============================================
|
||
// 结果统计和输出
|
||
// ============================================
|
||
|
||
func printProgress(result *TestResult) {
|
||
total := atomic.LoadInt64(&result.TotalRequests)
|
||
success := atomic.LoadInt64(&result.SuccessRequests)
|
||
failed := atomic.LoadInt64(&result.FailedRequests)
|
||
|
||
if total > 0 {
|
||
successRate := float64(success) / float64(total) * 100
|
||
fmt.Printf(" 进行中: %d 请求 | 成功率: %.2f%% | 失败: %d\n", total, successRate, failed)
|
||
}
|
||
}
|
||
|
||
func printResult(result *TestResult, durationSec int) {
|
||
fmt.Printf("\n\n=== 压力测试结果 ===\n")
|
||
fmt.Printf("总请求数: %d\n", result.TotalRequests)
|
||
fmt.Printf("成功请求: %d (%.2f%%)\n",
|
||
result.SuccessRequests,
|
||
float64(result.SuccessRequests)/float64(result.TotalRequests)*100)
|
||
fmt.Printf("失败请求: %d (%.2f%%)\n",
|
||
result.FailedRequests,
|
||
float64(result.FailedRequests)/float64(result.TotalRequests)*100)
|
||
|
||
qps := float64(result.TotalRequests) / float64(durationSec)
|
||
fmt.Printf("\nQPS: %.2f\n", qps)
|
||
|
||
if len(result.Latencies) > 0 {
|
||
result.mu.Lock()
|
||
latencies := make([]int64, len(result.Latencies))
|
||
copy(latencies, result.Latencies)
|
||
result.mu.Unlock()
|
||
|
||
sort.Slice(latencies, func(i, j int) bool { return latencies[i] < latencies[j] })
|
||
|
||
p50 := latencies[len(latencies)*50/100]
|
||
p95 := latencies[len(latencies)*95/100]
|
||
p99 := latencies[len(latencies)*99/100]
|
||
min := latencies[0]
|
||
max := latencies[len(latencies)-1]
|
||
|
||
var sum int64
|
||
for _, l := range latencies {
|
||
sum += l
|
||
}
|
||
avg := sum / int64(len(latencies))
|
||
|
||
fmt.Printf("\n延迟统计:\n")
|
||
fmt.Printf(" 最小: %d ms\n", min)
|
||
fmt.Printf(" P50: %d ms\n", p50)
|
||
fmt.Printf(" 平均: %d ms\n", avg)
|
||
fmt.Printf(" P95: %d ms\n", p95)
|
||
fmt.Printf(" P99: %d ms\n", p99)
|
||
fmt.Printf(" 最大: %d ms\n", max)
|
||
}
|
||
|
||
if len(result.Errors) > 0 {
|
||
fmt.Printf("\n错误统计 (Top 10):\n")
|
||
type errorPair struct {
|
||
msg string
|
||
count int64
|
||
}
|
||
var errors []errorPair
|
||
for msg, count := range result.Errors {
|
||
errors = append(errors, errorPair{msg, count})
|
||
}
|
||
sort.Slice(errors, func(i, j int) bool { return errors[i].count > errors[j].count })
|
||
|
||
for i, e := range errors {
|
||
if i >= 10 {
|
||
break
|
||
}
|
||
fmt.Printf(" %s: %d 次\n", e.msg, e.count)
|
||
}
|
||
}
|
||
|
||
fmt.Printf("==================\n")
|
||
}
|