优化一键脚本
This commit is contained in:
+31
-1
@@ -64,6 +64,7 @@ show_help() {
|
|||||||
--reset-db 重置数据库(删除并重建)
|
--reset-db 重置数据库(删除并重建)
|
||||||
--logs 部署完成后跟随查看后端日志
|
--logs 部署完成后跟随查看后端日志
|
||||||
--skip-check 跳过部署前代码门禁(不推荐)
|
--skip-check 跳过部署前代码门禁(不推荐)
|
||||||
|
--no-clean 跳过部署后的 Docker 垃圾清理
|
||||||
-h, --help 显示帮助信息
|
-h, --help 显示帮助信息
|
||||||
|
|
||||||
环境变量:
|
环境变量:
|
||||||
@@ -87,6 +88,7 @@ NO_MIGRATE=0
|
|||||||
RESET_DB=0
|
RESET_DB=0
|
||||||
FOLLOW_LOGS=0
|
FOLLOW_LOGS=0
|
||||||
SKIP_CHECK=0
|
SKIP_CHECK=0
|
||||||
|
NO_CLEAN=0
|
||||||
|
|
||||||
while [[ $# -gt 0 ]]; do
|
while [[ $# -gt 0 ]]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
@@ -110,6 +112,10 @@ while [[ $# -gt 0 ]]; do
|
|||||||
SKIP_CHECK=1
|
SKIP_CHECK=1
|
||||||
shift
|
shift
|
||||||
;;
|
;;
|
||||||
|
--no-clean)
|
||||||
|
NO_CLEAN=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
-h|--help)
|
-h|--help)
|
||||||
show_help
|
show_help
|
||||||
exit 0
|
exit 0
|
||||||
@@ -319,6 +325,23 @@ wait_http_ready() {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 清理部署过程中产生的 Docker 垃圾(旧镜像、构建缓存)
|
||||||
|
# 每次 docker compose build 会生成新镜像,旧镜像变为 <none>:<none>
|
||||||
|
# 构建缓存(BuildKit)也会持续增长,不清理会轻松堆积几十 GB
|
||||||
|
docker_cleanup() {
|
||||||
|
log "清理 Docker 垃圾..."
|
||||||
|
local before after
|
||||||
|
|
||||||
|
before=$(docker system df --format '{{.TotalCount}}' 2>/dev/null || echo "?")
|
||||||
|
|
||||||
|
docker image prune -f 2>/dev/null || true
|
||||||
|
docker builder prune -f 2>/dev/null || true
|
||||||
|
|
||||||
|
after=$(docker system df --format '{{.TotalCount}}' 2>/dev/null || echo "?")
|
||||||
|
|
||||||
|
log_success "Docker 清理完成(对象数: ${before} -> ${after})"
|
||||||
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "${ROOT_DIR}"
|
cd "${ROOT_DIR}"
|
||||||
need_cmd docker
|
need_cmd docker
|
||||||
@@ -348,6 +371,9 @@ main() {
|
|||||||
log "启动生产服务..."
|
log "启动生产服务..."
|
||||||
compose up -d
|
compose up -d
|
||||||
else
|
else
|
||||||
|
log "清理旧镜像以释放空间..."
|
||||||
|
docker image prune -f 2>/dev/null || true
|
||||||
|
|
||||||
log "构建后端镜像..."
|
log "构建后端镜像..."
|
||||||
compose build backend
|
compose build backend
|
||||||
|
|
||||||
@@ -355,7 +381,7 @@ main() {
|
|||||||
compose build caddy
|
compose build caddy
|
||||||
|
|
||||||
log "启动生产服务..."
|
log "启动生产服务..."
|
||||||
compose up -d --no-build
|
compose up -d --no-build --remove-orphans
|
||||||
fi
|
fi
|
||||||
|
|
||||||
wait_service_healthy mysql "MySQL"
|
wait_service_healthy mysql "MySQL"
|
||||||
@@ -372,6 +398,10 @@ main() {
|
|||||||
wait_service_healthy caddy "Caddy"
|
wait_service_healthy caddy "Caddy"
|
||||||
wait_http_ready
|
wait_http_ready
|
||||||
|
|
||||||
|
if [[ "${NO_CLEAN}" != "1" ]]; then
|
||||||
|
docker_cleanup
|
||||||
|
fi
|
||||||
|
|
||||||
log_success "部署完成:$(public_url)"
|
log_success "部署完成:$(public_url)"
|
||||||
log "常用命令:$(compose_user_cmd) logs -f caddy backend"
|
log "常用命令:$(compose_user_cmd) logs -f caddy backend"
|
||||||
|
|
||||||
|
|||||||
@@ -1,840 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
|
|
||||||
adminUsername := os.Getenv("LOAD_STRESS_ADMIN_USERNAME")
|
|
||||||
adminPassword := os.Getenv("LOAD_STRESS_ADMIN_PASSWORD")
|
|
||||||
if adminUsername == "" || adminPassword == "" {
|
|
||||||
fmt.Println(" 跳过后台登录:请设置 LOAD_STRESS_ADMIN_USERNAME 和 LOAD_STRESS_ADMIN_PASSWORD")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 登录后台账号
|
|
||||||
loginPayload := map[string]string{
|
|
||||||
"username": adminUsername,
|
|
||||||
"password": adminPassword,
|
|
||||||
"captcha_id": captchaResp.Data.CaptchaID,
|
|
||||||
"captcha_code": "1234",
|
|
||||||
}
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
@@ -1,495 +0,0 @@
|
|||||||
-- ============================================
|
|
||||||
-- 压力测试数据生成脚本
|
|
||||||
-- 用于生成大量测试数据以进行性能测试
|
|
||||||
-- ============================================
|
|
||||||
|
|
||||||
SET NAMES utf8mb4;
|
|
||||||
SET FOREIGN_KEY_CHECKS = 0;
|
|
||||||
|
|
||||||
-- 清理测试数据(可选,谨慎使用)
|
|
||||||
-- DELETE FROM wallet_ledger WHERE user_id > 1000;
|
|
||||||
-- DELETE FROM rental_orders WHERE id > 100;
|
|
||||||
-- DELETE FROM rental_listings WHERE id > 100;
|
|
||||||
-- DELETE FROM game_accounts WHERE id > 100;
|
|
||||||
-- DELETE FROM users WHERE id > 1000;
|
|
||||||
|
|
||||||
-- ============================================
|
|
||||||
-- 1. 批量生成用户数据(10000个用户)
|
|
||||||
-- ============================================
|
|
||||||
DROP PROCEDURE IF EXISTS generate_users;
|
|
||||||
DELIMITER $$
|
|
||||||
CREATE PROCEDURE generate_users(IN batch_size INT)
|
|
||||||
BEGIN
|
|
||||||
DECLARE i INT DEFAULT 1;
|
|
||||||
DECLARE phone_num VARCHAR(32);
|
|
||||||
DECLARE nick VARCHAR(64);
|
|
||||||
|
|
||||||
WHILE i <= batch_size DO
|
|
||||||
SET phone_num = CONCAT('138', LPAD(i, 8, '0'));
|
|
||||||
SET nick = CONCAT('测试用户', i);
|
|
||||||
|
|
||||||
INSERT INTO users (
|
|
||||||
phone, nickname, realname_status, risk_status,
|
|
||||||
credit_score, status, created_at
|
|
||||||
) VALUES (
|
|
||||||
phone_num, nick,
|
|
||||||
CASE WHEN i % 10 = 0 THEN 'unverified' ELSE 'verified' END,
|
|
||||||
CASE WHEN i % 100 = 0 THEN 'frozen' ELSE 'normal' END,
|
|
||||||
80 + (i % 20),
|
|
||||||
'active',
|
|
||||||
DATE_SUB(NOW(), INTERVAL (i % 365) DAY)
|
|
||||||
) ON DUPLICATE KEY UPDATE id=id;
|
|
||||||
|
|
||||||
-- 为已实名用户创建实名记录
|
|
||||||
IF i % 10 != 0 THEN
|
|
||||||
INSERT INTO user_realname (
|
|
||||||
user_id, provider, status, masked_name, masked_id_no, verified_at
|
|
||||||
) VALUES (
|
|
||||||
LAST_INSERT_ID(), 'mock', 'success',
|
|
||||||
CONCAT('张*', CHAR(65 + (i % 26))),
|
|
||||||
CONCAT('3301**********', LPAD(i % 10000, 4, '0')),
|
|
||||||
DATE_SUB(NOW(), INTERVAL (i % 300) DAY)
|
|
||||||
) ON DUPLICATE KEY UPDATE user_id=user_id;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- 为用户创建钱包
|
|
||||||
INSERT INTO wallet_accounts (user_id, available_balance_cent, frozen_balance_cent, status)
|
|
||||||
VALUES (
|
|
||||||
LAST_INSERT_ID(),
|
|
||||||
(i % 10) * 10000,
|
|
||||||
(i % 5) * 5000,
|
|
||||||
'active'
|
|
||||||
) ON DUPLICATE KEY UPDATE user_id=user_id;
|
|
||||||
|
|
||||||
SET i = i + 1;
|
|
||||||
|
|
||||||
-- 每1000条提交一次
|
|
||||||
IF i % 1000 = 0 THEN
|
|
||||||
COMMIT;
|
|
||||||
END IF;
|
|
||||||
END WHILE;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
END$$
|
|
||||||
DELIMITER ;
|
|
||||||
|
|
||||||
-- ============================================
|
|
||||||
-- 2. 批量生成游戏账号和商品(50000个商品)
|
|
||||||
-- ============================================
|
|
||||||
DROP PROCEDURE IF EXISTS generate_listings;
|
|
||||||
DELIMITER $$
|
|
||||||
CREATE PROCEDURE generate_listings(IN batch_size INT)
|
|
||||||
BEGIN
|
|
||||||
DECLARE i INT DEFAULT 1;
|
|
||||||
DECLARE user_id_val BIGINT;
|
|
||||||
DECLARE account_id_val BIGINT;
|
|
||||||
DECLARE price_val BIGINT;
|
|
||||||
DECLARE deposit_val BIGINT;
|
|
||||||
DECLARE verified_user_min BIGINT;
|
|
||||||
DECLARE verified_user_max BIGINT;
|
|
||||||
DECLARE user_pick BIGINT;
|
|
||||||
|
|
||||||
SELECT MIN(id), MAX(id)
|
|
||||||
INTO verified_user_min, verified_user_max
|
|
||||||
FROM users
|
|
||||||
WHERE realname_status = 'verified';
|
|
||||||
|
|
||||||
IF verified_user_min IS NULL THEN
|
|
||||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的已实名用户';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
WHILE i <= batch_size DO
|
|
||||||
-- 近似随机选择一个用户作为号主,避免 ORDER BY RAND() 全表排序。
|
|
||||||
SET user_id_val = NULL;
|
|
||||||
SET user_pick = verified_user_min + FLOOR(RAND() * (verified_user_max - verified_user_min + 1));
|
|
||||||
SELECT id INTO user_id_val FROM users
|
|
||||||
WHERE id >= user_pick AND realname_status = 'verified'
|
|
||||||
ORDER BY id LIMIT 1;
|
|
||||||
|
|
||||||
IF user_id_val IS NULL THEN
|
|
||||||
SELECT id INTO user_id_val FROM users
|
|
||||||
WHERE realname_status = 'verified'
|
|
||||||
ORDER BY id LIMIT 1;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- 创建游戏账号
|
|
||||||
INSERT INTO game_accounts (
|
|
||||||
owner_id, game_name, server_region, login_platform,
|
|
||||||
title, description, rank_level, haf_coin_amount, status
|
|
||||||
) VALUES (
|
|
||||||
user_id_val,
|
|
||||||
'delta_force',
|
|
||||||
CASE (i % 4)
|
|
||||||
WHEN 0 THEN '亚服'
|
|
||||||
WHEN 1 THEN '美服'
|
|
||||||
WHEN 2 THEN '欧服'
|
|
||||||
ELSE '国服'
|
|
||||||
END,
|
|
||||||
CASE (i % 3)
|
|
||||||
WHEN 0 THEN 'Steam'
|
|
||||||
WHEN 1 THEN 'Epic'
|
|
||||||
ELSE 'WeGame'
|
|
||||||
END,
|
|
||||||
CONCAT('账号', i, ' 高分段'),
|
|
||||||
CONCAT('这是一个测试账号,编号', i),
|
|
||||||
CASE (i % 5)
|
|
||||||
WHEN 0 THEN '青铜'
|
|
||||||
WHEN 1 THEN '白银'
|
|
||||||
WHEN 2 THEN '黄金'
|
|
||||||
WHEN 3 THEN '铂金'
|
|
||||||
ELSE '钻石'
|
|
||||||
END,
|
|
||||||
(i % 100) * 10000,
|
|
||||||
'published'
|
|
||||||
);
|
|
||||||
|
|
||||||
SET account_id_val = LAST_INSERT_ID();
|
|
||||||
|
|
||||||
-- 创建租号商品
|
|
||||||
SET price_val = 500 + (i % 20) * 50;
|
|
||||||
SET deposit_val = 10000 + (i % 10) * 5000;
|
|
||||||
|
|
||||||
INSERT INTO rental_listings (
|
|
||||||
listing_no, account_id, owner_id, price_cent, deposit_amount_cent,
|
|
||||||
in_transaction, status, review_status, published_at
|
|
||||||
) VALUES (
|
|
||||||
CONCAT(
|
|
||||||
DATE_FORMAT(DATE_SUB(NOW(), INTERVAL (i % 90) DAY), '%Y%m%d'),
|
|
||||||
LPAD(FLOOR((i - 1) / 90) + 1, 4, '0')
|
|
||||||
),
|
|
||||||
account_id_val,
|
|
||||||
user_id_val,
|
|
||||||
price_val,
|
|
||||||
deposit_val,
|
|
||||||
CASE WHEN i % 10 = 0 THEN 1 ELSE 0 END,
|
|
||||||
CASE
|
|
||||||
WHEN i % 20 = 0 THEN 'offline'
|
|
||||||
WHEN i % 15 = 0 THEN 'draft'
|
|
||||||
ELSE 'published'
|
|
||||||
END,
|
|
||||||
CASE
|
|
||||||
WHEN i % 15 = 0 THEN 'pending'
|
|
||||||
WHEN i % 30 = 0 THEN 'rejected'
|
|
||||||
ELSE 'approved'
|
|
||||||
END,
|
|
||||||
DATE_SUB(NOW(), INTERVAL (i % 90) DAY)
|
|
||||||
);
|
|
||||||
|
|
||||||
SET i = i + 1;
|
|
||||||
|
|
||||||
IF i % 1000 = 0 THEN
|
|
||||||
COMMIT;
|
|
||||||
END IF;
|
|
||||||
END WHILE;
|
|
||||||
|
|
||||||
INSERT INTO listing_no_sequences (biz_date, next_seq)
|
|
||||||
SELECT LEFT(listing_no, 8), MAX(CAST(SUBSTRING(listing_no, 9) AS UNSIGNED))
|
|
||||||
FROM rental_listings
|
|
||||||
GROUP BY LEFT(listing_no, 8)
|
|
||||||
ON DUPLICATE KEY UPDATE next_seq = GREATEST(next_seq, VALUES(next_seq));
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
END$$
|
|
||||||
DELIMITER ;
|
|
||||||
|
|
||||||
-- ============================================
|
|
||||||
-- 3. 批量生成订单数据(30000个订单)
|
|
||||||
-- ============================================
|
|
||||||
DROP PROCEDURE IF EXISTS generate_orders;
|
|
||||||
DELIMITER $$
|
|
||||||
CREATE PROCEDURE generate_orders(IN batch_size INT)
|
|
||||||
BEGIN
|
|
||||||
DECLARE i INT DEFAULT 1;
|
|
||||||
DECLARE listing_id_val BIGINT;
|
|
||||||
DECLARE account_id_val BIGINT;
|
|
||||||
DECLARE owner_id_val BIGINT;
|
|
||||||
DECLARE renter_id_val BIGINT;
|
|
||||||
DECLARE order_no_val VARCHAR(64);
|
|
||||||
DECLARE rent_amount_val BIGINT;
|
|
||||||
DECLARE deposit_val BIGINT;
|
|
||||||
DECLARE listing_min BIGINT;
|
|
||||||
DECLARE listing_max BIGINT;
|
|
||||||
DECLARE listing_pick BIGINT;
|
|
||||||
DECLARE verified_user_min BIGINT;
|
|
||||||
DECLARE verified_user_max BIGINT;
|
|
||||||
DECLARE user_pick BIGINT;
|
|
||||||
|
|
||||||
SELECT MIN(id), MAX(id)
|
|
||||||
INTO listing_min, listing_max
|
|
||||||
FROM rental_listings
|
|
||||||
WHERE status IN ('published', 'active') AND review_status = 'approved';
|
|
||||||
|
|
||||||
SELECT MIN(id), MAX(id)
|
|
||||||
INTO verified_user_min, verified_user_max
|
|
||||||
FROM users
|
|
||||||
WHERE realname_status = 'verified';
|
|
||||||
|
|
||||||
IF listing_min IS NULL THEN
|
|
||||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的已上架商品';
|
|
||||||
END IF;
|
|
||||||
IF verified_user_min IS NULL THEN
|
|
||||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的已实名用户';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
WHILE i <= batch_size DO
|
|
||||||
-- 近似随机选择一个上架商品,避免 ORDER BY RAND() 全表排序。
|
|
||||||
SET listing_id_val = NULL;
|
|
||||||
SET listing_pick = listing_min + FLOOR(RAND() * (listing_max - listing_min + 1));
|
|
||||||
SELECT rl.id, rl.account_id, rl.owner_id, rl.price_cent, rl.deposit_amount_cent
|
|
||||||
INTO listing_id_val, account_id_val, owner_id_val, rent_amount_val, deposit_val
|
|
||||||
FROM rental_listings rl
|
|
||||||
WHERE rl.id >= listing_pick
|
|
||||||
AND rl.status IN ('published', 'active')
|
|
||||||
AND rl.review_status = 'approved'
|
|
||||||
ORDER BY rl.id LIMIT 1;
|
|
||||||
|
|
||||||
IF listing_id_val IS NULL THEN
|
|
||||||
SELECT rl.id, rl.account_id, rl.owner_id, rl.price_cent, rl.deposit_amount_cent
|
|
||||||
INTO listing_id_val, account_id_val, owner_id_val, rent_amount_val, deposit_val
|
|
||||||
FROM rental_listings rl
|
|
||||||
WHERE rl.status IN ('published', 'active')
|
|
||||||
AND rl.review_status = 'approved'
|
|
||||||
ORDER BY rl.id LIMIT 1;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- 近似随机选择一个租客(不能是号主本人)。
|
|
||||||
SET renter_id_val = NULL;
|
|
||||||
SET user_pick = verified_user_min + FLOOR(RAND() * (verified_user_max - verified_user_min + 1));
|
|
||||||
SELECT id INTO renter_id_val FROM users
|
|
||||||
WHERE id >= user_pick AND id != owner_id_val AND realname_status = 'verified'
|
|
||||||
ORDER BY id LIMIT 1;
|
|
||||||
|
|
||||||
IF renter_id_val IS NULL THEN
|
|
||||||
SELECT id INTO renter_id_val FROM users
|
|
||||||
WHERE id != owner_id_val AND realname_status = 'verified'
|
|
||||||
ORDER BY id LIMIT 1;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF renter_id_val IS NULL THEN
|
|
||||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的租客用户';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
SET order_no_val = CONCAT('ORD', DATE_FORMAT(NOW(), '%Y%m%d'), LPAD(i, 8, '0'));
|
|
||||||
SET rent_amount_val = rent_amount_val * 24; -- 24小时租金
|
|
||||||
|
|
||||||
INSERT INTO rental_orders (
|
|
||||||
order_no, listing_id, account_id, owner_id, renter_id,
|
|
||||||
estimated_duration_hours, rent_amount_cent, owner_rent_amount_cent,
|
|
||||||
deposit_amount_cent, deposit_original_amount_cent, platform_fee_cent, status, handoff_status,
|
|
||||||
settlement_status, rented_at, created_at
|
|
||||||
) VALUES (
|
|
||||||
order_no_val,
|
|
||||||
listing_id_val,
|
|
||||||
account_id_val,
|
|
||||||
owner_id_val,
|
|
||||||
renter_id_val,
|
|
||||||
24,
|
|
||||||
rent_amount_val,
|
|
||||||
ROUND(rent_amount_val * 0.95), -- 号主实得95%
|
|
||||||
deposit_val,
|
|
||||||
deposit_val,
|
|
||||||
ROUND(rent_amount_val * 0.05), -- 平台5%手续费
|
|
||||||
CASE (i % 10)
|
|
||||||
WHEN 0 THEN 'pending_payment'
|
|
||||||
WHEN 1 THEN 'cancelled'
|
|
||||||
WHEN 2 THEN 'closed'
|
|
||||||
ELSE 'completed'
|
|
||||||
END,
|
|
||||||
CASE (i % 10)
|
|
||||||
WHEN 0 THEN 'none'
|
|
||||||
WHEN 1 THEN 'none'
|
|
||||||
WHEN 2 THEN 'owner_delivered'
|
|
||||||
ELSE 'owner_received'
|
|
||||||
END,
|
|
||||||
CASE (i % 10)
|
|
||||||
WHEN 0 THEN 'unsettled'
|
|
||||||
WHEN 1 THEN 'unsettled'
|
|
||||||
ELSE 'settled'
|
|
||||||
END,
|
|
||||||
DATE_SUB(NOW(), INTERVAL (i % 60) DAY),
|
|
||||||
DATE_SUB(NOW(), INTERVAL (i % 60) DAY)
|
|
||||||
);
|
|
||||||
|
|
||||||
SET i = i + 1;
|
|
||||||
|
|
||||||
IF i % 1000 = 0 THEN
|
|
||||||
COMMIT;
|
|
||||||
END IF;
|
|
||||||
END WHILE;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
END$$
|
|
||||||
DELIMITER ;
|
|
||||||
|
|
||||||
-- ============================================
|
|
||||||
-- 4. 批量生成钱包流水(100000条流水)
|
|
||||||
-- ============================================
|
|
||||||
DROP PROCEDURE IF EXISTS generate_wallet_ledger;
|
|
||||||
DELIMITER $$
|
|
||||||
CREATE PROCEDURE generate_wallet_ledger(IN batch_size INT)
|
|
||||||
BEGIN
|
|
||||||
DECLARE i INT DEFAULT 1;
|
|
||||||
DECLARE user_id_val BIGINT;
|
|
||||||
DECLARE order_id_val BIGINT;
|
|
||||||
DECLARE ledger_no_val VARCHAR(64);
|
|
||||||
DECLARE amount_val BIGINT;
|
|
||||||
DECLARE user_min BIGINT;
|
|
||||||
DECLARE user_max BIGINT;
|
|
||||||
DECLARE order_min BIGINT;
|
|
||||||
DECLARE order_max BIGINT;
|
|
||||||
DECLARE user_pick BIGINT;
|
|
||||||
DECLARE order_pick BIGINT;
|
|
||||||
|
|
||||||
SELECT MIN(id), MAX(id) INTO user_min, user_max FROM users;
|
|
||||||
SELECT MIN(id), MAX(id) INTO order_min, order_max FROM rental_orders;
|
|
||||||
|
|
||||||
IF user_min IS NULL THEN
|
|
||||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的用户';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
WHILE i <= batch_size DO
|
|
||||||
-- 近似随机选择用户,避免 ORDER BY RAND() 全表排序。
|
|
||||||
SET user_id_val = NULL;
|
|
||||||
SET user_pick = user_min + FLOOR(RAND() * (user_max - user_min + 1));
|
|
||||||
SELECT id INTO user_id_val FROM users
|
|
||||||
WHERE id >= user_pick
|
|
||||||
ORDER BY id LIMIT 1;
|
|
||||||
|
|
||||||
IF user_id_val IS NULL THEN
|
|
||||||
SELECT id INTO user_id_val FROM users ORDER BY id LIMIT 1;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
-- 随机关联订单(50%概率)
|
|
||||||
IF RAND() > 0.5 AND order_min IS NOT NULL THEN
|
|
||||||
SET order_id_val = NULL;
|
|
||||||
SET order_pick = order_min + FLOOR(RAND() * (order_max - order_min + 1));
|
|
||||||
SELECT id INTO order_id_val FROM rental_orders
|
|
||||||
WHERE id >= order_pick
|
|
||||||
ORDER BY id LIMIT 1;
|
|
||||||
|
|
||||||
IF order_id_val IS NULL THEN
|
|
||||||
SELECT id INTO order_id_val FROM rental_orders ORDER BY id LIMIT 1;
|
|
||||||
END IF;
|
|
||||||
ELSE
|
|
||||||
SET order_id_val = NULL;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
SET ledger_no_val = CONCAT('LDG', DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'), LPAD(i, 6, '0'));
|
|
||||||
SET amount_val = ((i % 500) * 100) + FLOOR(RAND() * 10000);
|
|
||||||
|
|
||||||
INSERT INTO wallet_ledger (
|
|
||||||
ledger_no, user_id, order_id, direction, amount_cent,
|
|
||||||
balance_after_cent, balance_type, biz_type, biz_no, remark, created_at
|
|
||||||
) VALUES (
|
|
||||||
ledger_no_val,
|
|
||||||
user_id_val,
|
|
||||||
order_id_val,
|
|
||||||
CASE WHEN i % 2 = 0 THEN 'in' ELSE 'out' END,
|
|
||||||
amount_val,
|
|
||||||
100000 + (i % 1000) * 100,
|
|
||||||
CASE WHEN i % 5 = 0 THEN 'frozen' ELSE 'available' END,
|
|
||||||
CASE (i % 6)
|
|
||||||
WHEN 0 THEN 'rent_payment'
|
|
||||||
WHEN 1 THEN 'deposit_freeze'
|
|
||||||
WHEN 2 THEN 'settlement'
|
|
||||||
WHEN 3 THEN 'refund'
|
|
||||||
WHEN 4 THEN 'recharge'
|
|
||||||
ELSE 'withdraw'
|
|
||||||
END,
|
|
||||||
CONCAT('BIZ', LPAD(i, 10, '0')),
|
|
||||||
CONCAT('测试流水', i),
|
|
||||||
DATE_SUB(NOW(), INTERVAL (i % 180) DAY)
|
|
||||||
);
|
|
||||||
|
|
||||||
SET i = i + 1;
|
|
||||||
|
|
||||||
IF i % 1000 = 0 THEN
|
|
||||||
COMMIT;
|
|
||||||
END IF;
|
|
||||||
END WHILE;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
END$$
|
|
||||||
DELIMITER ;
|
|
||||||
|
|
||||||
-- ============================================
|
|
||||||
-- 5. 批量生成聊天消息(50000条消息)
|
|
||||||
-- ============================================
|
|
||||||
DROP PROCEDURE IF EXISTS generate_chat_messages;
|
|
||||||
DELIMITER $$
|
|
||||||
CREATE PROCEDURE generate_chat_messages(IN batch_size INT)
|
|
||||||
BEGIN
|
|
||||||
DECLARE i INT DEFAULT 1;
|
|
||||||
DECLARE conv_id BIGINT;
|
|
||||||
DECLARE user_id_val BIGINT;
|
|
||||||
DECLARE conv_min BIGINT;
|
|
||||||
DECLARE conv_max BIGINT;
|
|
||||||
DECLARE user_min BIGINT;
|
|
||||||
DECLARE user_max BIGINT;
|
|
||||||
DECLARE conv_pick BIGINT;
|
|
||||||
DECLARE user_pick BIGINT;
|
|
||||||
|
|
||||||
SELECT MIN(id), MAX(id) INTO conv_min, conv_max FROM chat_conversations;
|
|
||||||
SELECT MIN(id), MAX(id) INTO user_min, user_max FROM users;
|
|
||||||
|
|
||||||
IF conv_min IS NULL THEN
|
|
||||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的聊天会话';
|
|
||||||
END IF;
|
|
||||||
IF user_min IS NULL THEN
|
|
||||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '没有可用的用户';
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
WHILE i <= batch_size DO
|
|
||||||
-- 近似随机选择一个会话和发送者,避免 ORDER BY RAND() 全表排序。
|
|
||||||
SET conv_id = NULL;
|
|
||||||
SET conv_pick = conv_min + FLOOR(RAND() * (conv_max - conv_min + 1));
|
|
||||||
SELECT id INTO conv_id FROM chat_conversations
|
|
||||||
WHERE id >= conv_pick
|
|
||||||
ORDER BY id LIMIT 1;
|
|
||||||
|
|
||||||
IF conv_id IS NULL THEN
|
|
||||||
SELECT id INTO conv_id FROM chat_conversations ORDER BY id LIMIT 1;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
IF conv_id IS NOT NULL THEN
|
|
||||||
SET user_id_val = NULL;
|
|
||||||
SET user_pick = user_min + FLOOR(RAND() * (user_max - user_min + 1));
|
|
||||||
SELECT id INTO user_id_val FROM users
|
|
||||||
WHERE id >= user_pick
|
|
||||||
ORDER BY id LIMIT 1;
|
|
||||||
|
|
||||||
IF user_id_val IS NULL THEN
|
|
||||||
SELECT id INTO user_id_val FROM users ORDER BY id LIMIT 1;
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
INSERT INTO chat_messages (
|
|
||||||
conversation_id, sender_type, sender_id, sender_role,
|
|
||||||
content_type, content, created_at
|
|
||||||
) VALUES (
|
|
||||||
conv_id,
|
|
||||||
'user',
|
|
||||||
user_id_val,
|
|
||||||
CASE WHEN i % 2 = 0 THEN 'owner' ELSE 'renter' END,
|
|
||||||
'text',
|
|
||||||
CONCAT('这是测试消息', i, ',内容随机生成用于压力测试'),
|
|
||||||
DATE_SUB(NOW(), INTERVAL (i % 30) DAY)
|
|
||||||
);
|
|
||||||
END IF;
|
|
||||||
|
|
||||||
SET i = i + 1;
|
|
||||||
|
|
||||||
IF i % 1000 = 0 THEN
|
|
||||||
COMMIT;
|
|
||||||
END IF;
|
|
||||||
END WHILE;
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
END$$
|
|
||||||
DELIMITER ;
|
|
||||||
|
|
||||||
-- ============================================
|
|
||||||
-- 本文件只创建存储过程,不直接生成数据。
|
|
||||||
-- 请使用 scripts/stress_test.sh data 按参数生成,避免意外重复造数。
|
|
||||||
-- ============================================
|
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
|
||||||
@@ -1,510 +0,0 @@
|
|||||||
-- ============================================
|
|
||||||
-- 优化的压力测试数据生成脚本
|
|
||||||
-- 使用批量生成 + 临时表,避免循环中的随机查询
|
|
||||||
-- ============================================
|
|
||||||
|
|
||||||
SET NAMES utf8mb4;
|
|
||||||
SET FOREIGN_KEY_CHECKS = 0;
|
|
||||||
|
|
||||||
-- ============================================
|
|
||||||
-- 1. 批量生成用户数据
|
|
||||||
-- ============================================
|
|
||||||
DROP PROCEDURE IF EXISTS generate_users_batch;
|
|
||||||
DELIMITER $$
|
|
||||||
CREATE PROCEDURE generate_users_batch(IN batch_size INT)
|
|
||||||
BEGIN
|
|
||||||
DECLARE batch_limit INT DEFAULT 1000;
|
|
||||||
DECLARE batches INT;
|
|
||||||
DECLARE current_batch INT DEFAULT 0;
|
|
||||||
DECLARE batch_start INT;
|
|
||||||
DECLARE batch_end INT;
|
|
||||||
DECLARE current_limit INT;
|
|
||||||
|
|
||||||
SET batches = CEIL(batch_size / batch_limit);
|
|
||||||
|
|
||||||
WHILE current_batch < batches DO
|
|
||||||
SET batch_start = current_batch * batch_limit + 1;
|
|
||||||
SET batch_end = LEAST((current_batch + 1) * batch_limit, batch_size);
|
|
||||||
SET current_limit = batch_end - batch_start + 1;
|
|
||||||
|
|
||||||
-- 使用 INSERT ... SELECT 批量生成
|
|
||||||
INSERT INTO users (phone, nickname, realname_status, risk_status, credit_score, status, created_at)
|
|
||||||
SELECT
|
|
||||||
CONCAT('138', LPAD(seq, 8, '0')) as phone,
|
|
||||||
CONCAT('测试用户', seq) as nickname,
|
|
||||||
CASE WHEN seq % 10 = 0 THEN 'unverified' ELSE 'verified' END as realname_status,
|
|
||||||
CASE WHEN seq % 100 = 0 THEN 'frozen' ELSE 'normal' END as risk_status,
|
|
||||||
80 + (seq % 20) as credit_score,
|
|
||||||
'active' as status,
|
|
||||||
DATE_SUB(NOW(), INTERVAL (seq % 365) DAY) as created_at
|
|
||||||
FROM (
|
|
||||||
SELECT @row := @row + 1 AS seq
|
|
||||||
FROM
|
|
||||||
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
|
||||||
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1,
|
|
||||||
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
|
||||||
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2,
|
|
||||||
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
|
||||||
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3,
|
|
||||||
(SELECT @row := batch_start - 1) r
|
|
||||||
LIMIT current_limit
|
|
||||||
) seqs
|
|
||||||
ON DUPLICATE KEY UPDATE id=id;
|
|
||||||
|
|
||||||
SET current_batch = current_batch + 1;
|
|
||||||
COMMIT;
|
|
||||||
END WHILE;
|
|
||||||
|
|
||||||
-- 批量生成实名记录(为已实名用户)
|
|
||||||
INSERT INTO user_realname (user_id, provider, status, masked_name, masked_id_no, verified_at)
|
|
||||||
SELECT
|
|
||||||
u.id,
|
|
||||||
'mock',
|
|
||||||
'success',
|
|
||||||
CONCAT('张*', CHAR(65 + (u.id % 26))),
|
|
||||||
CONCAT('3301**********', LPAD(u.id % 10000, 4, '0')),
|
|
||||||
DATE_SUB(NOW(), INTERVAL (u.id % 300) DAY)
|
|
||||||
FROM users u
|
|
||||||
WHERE u.realname_status = 'verified'
|
|
||||||
AND NOT EXISTS (SELECT 1 FROM user_realname WHERE user_id = u.id);
|
|
||||||
|
|
||||||
-- 批量生成钱包账户
|
|
||||||
INSERT INTO wallet_accounts (user_id, available_balance_cent, frozen_balance_cent, status)
|
|
||||||
SELECT
|
|
||||||
u.id,
|
|
||||||
(u.id % 10) * 10000,
|
|
||||||
(u.id % 5) * 5000,
|
|
||||||
'active'
|
|
||||||
FROM users u
|
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM wallet_accounts WHERE user_id = u.id);
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
END$$
|
|
||||||
DELIMITER ;
|
|
||||||
|
|
||||||
-- ============================================
|
|
||||||
-- 2. 批量生成游戏账号和商品
|
|
||||||
-- ============================================
|
|
||||||
DROP PROCEDURE IF EXISTS generate_listings_batch;
|
|
||||||
DELIMITER $$
|
|
||||||
CREATE PROCEDURE generate_listings_batch(IN batch_size INT)
|
|
||||||
BEGIN
|
|
||||||
DECLARE batch_limit INT DEFAULT 1000;
|
|
||||||
DECLARE batches INT;
|
|
||||||
DECLARE current_batch INT DEFAULT 0;
|
|
||||||
DECLARE verified_user_count INT DEFAULT 0;
|
|
||||||
|
|
||||||
-- 创建临时表存储已实名用户ID
|
|
||||||
DROP TEMPORARY TABLE IF EXISTS tmp_verified_users;
|
|
||||||
CREATE TEMPORARY TABLE tmp_verified_users (
|
|
||||||
id BIGINT PRIMARY KEY,
|
|
||||||
row_num INT
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO tmp_verified_users (id, row_num)
|
|
||||||
SELECT id, (@rn := @rn + 1) as row_num
|
|
||||||
FROM users, (SELECT @rn := 0) init
|
|
||||||
WHERE realname_status = 'verified'
|
|
||||||
ORDER BY id;
|
|
||||||
|
|
||||||
SELECT COUNT(*) INTO verified_user_count FROM tmp_verified_users;
|
|
||||||
|
|
||||||
SET batches = CEIL(batch_size / batch_limit);
|
|
||||||
|
|
||||||
WHILE current_batch < batches DO
|
|
||||||
-- 批量生成游戏账号
|
|
||||||
INSERT INTO game_accounts (
|
|
||||||
owner_id, game_name, server_region, login_platform,
|
|
||||||
title, description, rank_level, haf_coin_amount, status
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
u.id as owner_id,
|
|
||||||
'delta_force',
|
|
||||||
CASE (seq % 4)
|
|
||||||
WHEN 0 THEN '亚服'
|
|
||||||
WHEN 1 THEN '美服'
|
|
||||||
WHEN 2 THEN '欧服'
|
|
||||||
ELSE '国服'
|
|
||||||
END,
|
|
||||||
CASE (seq % 3)
|
|
||||||
WHEN 0 THEN 'Steam'
|
|
||||||
WHEN 1 THEN 'Epic'
|
|
||||||
ELSE 'WeGame'
|
|
||||||
END,
|
|
||||||
CONCAT('账号', seq, ' 高分段'),
|
|
||||||
CONCAT('这是一个测试账号,编号', seq),
|
|
||||||
CASE (seq % 5)
|
|
||||||
WHEN 0 THEN '青铜'
|
|
||||||
WHEN 1 THEN '白银'
|
|
||||||
WHEN 2 THEN '黄金'
|
|
||||||
WHEN 3 THEN '铂金'
|
|
||||||
ELSE '钻石'
|
|
||||||
END,
|
|
||||||
(seq % 100) * 10000,
|
|
||||||
'published'
|
|
||||||
FROM (
|
|
||||||
SELECT @row2 := @row2 + 1 AS seq
|
|
||||||
FROM
|
|
||||||
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
|
||||||
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1,
|
|
||||||
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
|
||||||
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2,
|
|
||||||
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
|
||||||
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3,
|
|
||||||
(SELECT @row2 := current_batch * batch_limit) r
|
|
||||||
LIMIT batch_limit
|
|
||||||
) seqs
|
|
||||||
INNER JOIN tmp_verified_users u ON u.row_num = (seq % verified_user_count) + 1
|
|
||||||
WHERE seq <= batch_size;
|
|
||||||
|
|
||||||
-- 批量生成租号商品
|
|
||||||
INSERT INTO rental_listings (
|
|
||||||
listing_no, account_id, owner_id, price_cent, deposit_amount_cent,
|
|
||||||
in_transaction, status, review_status, published_at
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
CONCAT(
|
|
||||||
DATE_FORMAT(DATE_SUB(NOW(), INTERVAL (ga.id % 90) DAY), '%Y%m%d'),
|
|
||||||
LPAD(FLOOR((ga.id - 1) / 90) + 1, 4, '0')
|
|
||||||
) as listing_no,
|
|
||||||
ga.id as account_id,
|
|
||||||
ga.owner_id,
|
|
||||||
500 + ((ga.id % 20) * 50) as price_cent,
|
|
||||||
10000 + ((ga.id % 10) * 5000) as deposit_amount_cent,
|
|
||||||
CASE WHEN ga.id % 10 = 0 THEN 1 ELSE 0 END as in_transaction,
|
|
||||||
CASE
|
|
||||||
WHEN ga.id % 20 = 0 THEN 'offline'
|
|
||||||
WHEN ga.id % 15 = 0 THEN 'draft'
|
|
||||||
ELSE 'published'
|
|
||||||
END as status,
|
|
||||||
CASE
|
|
||||||
WHEN ga.id % 15 = 0 THEN 'pending'
|
|
||||||
WHEN ga.id % 30 = 0 THEN 'rejected'
|
|
||||||
ELSE 'approved'
|
|
||||||
END as review_status,
|
|
||||||
DATE_SUB(NOW(), INTERVAL (ga.id % 90) DAY) as published_at
|
|
||||||
FROM game_accounts ga
|
|
||||||
WHERE ga.id > (SELECT COALESCE(MAX(account_id), 0) FROM rental_listings)
|
|
||||||
LIMIT batch_limit;
|
|
||||||
|
|
||||||
SET current_batch = current_batch + 1;
|
|
||||||
COMMIT;
|
|
||||||
END WHILE;
|
|
||||||
|
|
||||||
INSERT INTO listing_no_sequences (biz_date, next_seq)
|
|
||||||
SELECT LEFT(listing_no, 8), MAX(CAST(SUBSTRING(listing_no, 9) AS UNSIGNED))
|
|
||||||
FROM rental_listings
|
|
||||||
GROUP BY LEFT(listing_no, 8)
|
|
||||||
ON DUPLICATE KEY UPDATE next_seq = GREATEST(next_seq, VALUES(next_seq));
|
|
||||||
|
|
||||||
COMMIT;
|
|
||||||
|
|
||||||
DROP TEMPORARY TABLE IF EXISTS tmp_verified_users;
|
|
||||||
END$$
|
|
||||||
DELIMITER ;
|
|
||||||
|
|
||||||
-- ============================================
|
|
||||||
-- 3. 批量生成订单数据(优化版)
|
|
||||||
-- ============================================
|
|
||||||
DROP PROCEDURE IF EXISTS generate_orders_batch;
|
|
||||||
DELIMITER $$
|
|
||||||
CREATE PROCEDURE generate_orders_batch(IN batch_size INT)
|
|
||||||
BEGIN
|
|
||||||
DECLARE batch_limit INT DEFAULT 1000;
|
|
||||||
DECLARE batches INT;
|
|
||||||
DECLARE current_batch INT DEFAULT 0;
|
|
||||||
DECLARE available_listing_count INT DEFAULT 0;
|
|
||||||
DECLARE verified_user_count INT DEFAULT 0;
|
|
||||||
|
|
||||||
-- 创建临时表:可用商品
|
|
||||||
DROP TEMPORARY TABLE IF EXISTS tmp_available_listings;
|
|
||||||
CREATE TEMPORARY TABLE tmp_available_listings (
|
|
||||||
id BIGINT PRIMARY KEY,
|
|
||||||
account_id BIGINT,
|
|
||||||
owner_id BIGINT,
|
|
||||||
price_cent BIGINT,
|
|
||||||
deposit_amount_cent BIGINT,
|
|
||||||
row_num INT
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO tmp_available_listings (id, account_id, owner_id, price_cent, deposit_amount_cent, row_num)
|
|
||||||
SELECT id, account_id, owner_id, price_cent, deposit_amount_cent, (@rn := @rn + 1)
|
|
||||||
FROM rental_listings, (SELECT @rn := 0) init
|
|
||||||
WHERE status IN ('published', 'active') AND review_status = 'approved'
|
|
||||||
ORDER BY id;
|
|
||||||
|
|
||||||
SELECT COUNT(*) INTO available_listing_count FROM tmp_available_listings;
|
|
||||||
|
|
||||||
-- 创建临时表:已实名用户
|
|
||||||
DROP TEMPORARY TABLE IF EXISTS tmp_verified_users;
|
|
||||||
CREATE TEMPORARY TABLE tmp_verified_users (
|
|
||||||
id BIGINT PRIMARY KEY,
|
|
||||||
row_num INT
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO tmp_verified_users (id, row_num)
|
|
||||||
SELECT id, (@rn2 := @rn2 + 1)
|
|
||||||
FROM users, (SELECT @rn2 := 0) init
|
|
||||||
WHERE realname_status = 'verified'
|
|
||||||
ORDER BY id;
|
|
||||||
|
|
||||||
SELECT COUNT(*) INTO verified_user_count FROM tmp_verified_users;
|
|
||||||
|
|
||||||
SET batches = CEIL(batch_size / batch_limit);
|
|
||||||
|
|
||||||
WHILE current_batch < batches DO
|
|
||||||
INSERT INTO rental_orders (
|
|
||||||
order_no, listing_id, account_id, owner_id, renter_id,
|
|
||||||
estimated_duration_hours, rent_amount_cent, owner_rent_amount_cent,
|
|
||||||
deposit_amount_cent, deposit_original_amount_cent, platform_fee_cent, status, handoff_status,
|
|
||||||
settlement_status, rented_at, created_at
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
CONCAT('ORD', DATE_FORMAT(NOW(), '%Y%m%d'), LPAD(seq, 8, '0')) as order_no,
|
|
||||||
l.id as listing_id,
|
|
||||||
l.account_id,
|
|
||||||
l.owner_id,
|
|
||||||
r.id as renter_id,
|
|
||||||
24 as estimated_duration_hours,
|
|
||||||
l.price_cent * 24 as rent_amount_cent,
|
|
||||||
ROUND(l.price_cent * 24 * 0.95) as owner_rent_amount_cent,
|
|
||||||
l.deposit_amount_cent,
|
|
||||||
l.deposit_amount_cent,
|
|
||||||
ROUND(l.price_cent * 24 * 0.05) as platform_fee_cent,
|
|
||||||
CASE (seq % 10)
|
|
||||||
WHEN 0 THEN 'pending_payment'
|
|
||||||
WHEN 1 THEN 'cancelled'
|
|
||||||
WHEN 2 THEN 'closed'
|
|
||||||
ELSE 'completed'
|
|
||||||
END as status,
|
|
||||||
CASE (seq % 10)
|
|
||||||
WHEN 0 THEN 'none'
|
|
||||||
WHEN 1 THEN 'none'
|
|
||||||
WHEN 2 THEN 'owner_delivered'
|
|
||||||
ELSE 'owner_received'
|
|
||||||
END as handoff_status,
|
|
||||||
CASE (seq % 10)
|
|
||||||
WHEN 0 THEN 'unsettled'
|
|
||||||
WHEN 1 THEN 'unsettled'
|
|
||||||
ELSE 'settled'
|
|
||||||
END as settlement_status,
|
|
||||||
DATE_SUB(NOW(), INTERVAL (seq % 60) DAY) as rented_at,
|
|
||||||
DATE_SUB(NOW(), INTERVAL (seq % 60) DAY) as created_at
|
|
||||||
FROM (
|
|
||||||
SELECT @row3 := @row3 + 1 AS seq
|
|
||||||
FROM
|
|
||||||
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
|
||||||
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1,
|
|
||||||
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
|
||||||
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2,
|
|
||||||
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
|
||||||
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3,
|
|
||||||
(SELECT @row3 := current_batch * batch_limit) r
|
|
||||||
LIMIT batch_limit
|
|
||||||
) seqs
|
|
||||||
INNER JOIN tmp_available_listings l ON l.row_num = (seq % available_listing_count) + 1
|
|
||||||
INNER JOIN tmp_verified_users r ON r.row_num = (seq % verified_user_count) + 1
|
|
||||||
WHERE seq <= batch_size AND r.id != l.owner_id
|
|
||||||
LIMIT batch_limit;
|
|
||||||
|
|
||||||
SET current_batch = current_batch + 1;
|
|
||||||
COMMIT;
|
|
||||||
END WHILE;
|
|
||||||
|
|
||||||
DROP TEMPORARY TABLE IF EXISTS tmp_available_listings;
|
|
||||||
DROP TEMPORARY TABLE IF EXISTS tmp_verified_users;
|
|
||||||
END$$
|
|
||||||
DELIMITER ;
|
|
||||||
|
|
||||||
-- ============================================
|
|
||||||
-- 4. 批量生成钱包流水
|
|
||||||
-- ============================================
|
|
||||||
DROP PROCEDURE IF EXISTS generate_wallet_ledger_batch;
|
|
||||||
DELIMITER $$
|
|
||||||
CREATE PROCEDURE generate_wallet_ledger_batch(IN batch_size INT)
|
|
||||||
BEGIN
|
|
||||||
DECLARE batch_limit INT DEFAULT 1000;
|
|
||||||
DECLARE batches INT;
|
|
||||||
DECLARE current_batch INT DEFAULT 0;
|
|
||||||
DECLARE user_count INT DEFAULT 0;
|
|
||||||
DECLARE order_count INT DEFAULT 0;
|
|
||||||
|
|
||||||
-- 创建临时表:用户列表
|
|
||||||
DROP TEMPORARY TABLE IF EXISTS tmp_users;
|
|
||||||
CREATE TEMPORARY TABLE tmp_users (
|
|
||||||
id BIGINT PRIMARY KEY,
|
|
||||||
row_num INT
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO tmp_users (id, row_num)
|
|
||||||
SELECT id, (@rn := @rn + 1)
|
|
||||||
FROM users, (SELECT @rn := 0) init
|
|
||||||
ORDER BY id;
|
|
||||||
|
|
||||||
SELECT COUNT(*) INTO user_count FROM tmp_users;
|
|
||||||
|
|
||||||
-- 创建临时表:订单列表
|
|
||||||
DROP TEMPORARY TABLE IF EXISTS tmp_orders;
|
|
||||||
CREATE TEMPORARY TABLE tmp_orders (
|
|
||||||
id BIGINT PRIMARY KEY,
|
|
||||||
row_num INT
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO tmp_orders (id, row_num)
|
|
||||||
SELECT id, (@rn2 := @rn2 + 1)
|
|
||||||
FROM rental_orders, (SELECT @rn2 := 0) init
|
|
||||||
ORDER BY id;
|
|
||||||
|
|
||||||
SELECT COUNT(*) INTO order_count FROM tmp_orders;
|
|
||||||
|
|
||||||
SET batches = CEIL(batch_size / batch_limit);
|
|
||||||
|
|
||||||
WHILE current_batch < batches DO
|
|
||||||
INSERT INTO wallet_ledger (
|
|
||||||
ledger_no, user_id, order_id, direction, amount_cent,
|
|
||||||
balance_after_cent, balance_type, biz_type, biz_no, remark, created_at
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
CONCAT('LDG', DATE_FORMAT(NOW(), '%Y%m%d%H%i%s'), LPAD(seq, 6, '0')) as ledger_no,
|
|
||||||
u.id as user_id,
|
|
||||||
IF(seq % 2 = 0, o.id, NULL) as order_id,
|
|
||||||
CASE WHEN seq % 2 = 0 THEN 'in' ELSE 'out' END as direction,
|
|
||||||
((seq % 500) * 100) + seq as amount_cent,
|
|
||||||
100000 + ((seq % 1000) * 100) as balance_after_cent,
|
|
||||||
CASE WHEN seq % 5 = 0 THEN 'frozen' ELSE 'available' END as balance_type,
|
|
||||||
CASE (seq % 6)
|
|
||||||
WHEN 0 THEN 'rent_payment'
|
|
||||||
WHEN 1 THEN 'deposit_freeze'
|
|
||||||
WHEN 2 THEN 'settlement'
|
|
||||||
WHEN 3 THEN 'refund'
|
|
||||||
WHEN 4 THEN 'recharge'
|
|
||||||
ELSE 'withdraw'
|
|
||||||
END as biz_type,
|
|
||||||
CONCAT('BIZ', LPAD(seq, 10, '0')) as biz_no,
|
|
||||||
CONCAT('测试流水', seq) as remark,
|
|
||||||
DATE_SUB(NOW(), INTERVAL (seq % 180) DAY) as created_at
|
|
||||||
FROM (
|
|
||||||
SELECT @row4 := @row4 + 1 AS seq
|
|
||||||
FROM
|
|
||||||
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
|
||||||
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1,
|
|
||||||
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
|
||||||
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2,
|
|
||||||
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
|
||||||
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3,
|
|
||||||
(SELECT @row4 := current_batch * batch_limit) r
|
|
||||||
LIMIT batch_limit
|
|
||||||
) seqs
|
|
||||||
INNER JOIN tmp_users u ON u.row_num = (seq % user_count) + 1
|
|
||||||
LEFT JOIN tmp_orders o ON o.row_num = (seq % order_count) + 1
|
|
||||||
WHERE seq <= batch_size
|
|
||||||
LIMIT batch_limit;
|
|
||||||
|
|
||||||
SET current_batch = current_batch + 1;
|
|
||||||
COMMIT;
|
|
||||||
END WHILE;
|
|
||||||
|
|
||||||
DROP TEMPORARY TABLE IF EXISTS tmp_users;
|
|
||||||
DROP TEMPORARY TABLE IF EXISTS tmp_orders;
|
|
||||||
END$$
|
|
||||||
DELIMITER ;
|
|
||||||
|
|
||||||
-- ============================================
|
|
||||||
-- 5. 批量生成聊天会话和消息
|
|
||||||
-- ============================================
|
|
||||||
DROP PROCEDURE IF EXISTS generate_chat_data_batch;
|
|
||||||
DELIMITER $$
|
|
||||||
CREATE PROCEDURE generate_chat_data_batch(IN message_count INT)
|
|
||||||
BEGIN
|
|
||||||
DECLARE batch_limit INT DEFAULT 1000;
|
|
||||||
DECLARE batches INT;
|
|
||||||
DECLARE current_batch INT DEFAULT 0;
|
|
||||||
DECLARE conversation_count INT DEFAULT 0;
|
|
||||||
DECLARE user_count INT DEFAULT 0;
|
|
||||||
|
|
||||||
-- 先生成聊天会话(基于订单)
|
|
||||||
INSERT INTO chat_conversations (order_id, type, title, status, last_message_at)
|
|
||||||
SELECT
|
|
||||||
ro.id,
|
|
||||||
'order_group',
|
|
||||||
CONCAT('订单', ro.order_no, '群聊'),
|
|
||||||
'active',
|
|
||||||
ro.created_at
|
|
||||||
FROM rental_orders ro
|
|
||||||
WHERE NOT EXISTS (SELECT 1 FROM chat_conversations WHERE order_id = ro.id)
|
|
||||||
LIMIT 1000
|
|
||||||
ON DUPLICATE KEY UPDATE order_id=order_id;
|
|
||||||
|
|
||||||
DROP TEMPORARY TABLE IF EXISTS tmp_conversations;
|
|
||||||
CREATE TEMPORARY TABLE tmp_conversations (
|
|
||||||
id BIGINT PRIMARY KEY,
|
|
||||||
row_num INT
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO tmp_conversations (id, row_num)
|
|
||||||
SELECT id, (@rn := @rn + 1)
|
|
||||||
FROM chat_conversations, (SELECT @rn := 0) init
|
|
||||||
ORDER BY id;
|
|
||||||
|
|
||||||
SELECT COUNT(*) INTO conversation_count FROM tmp_conversations;
|
|
||||||
|
|
||||||
DROP TEMPORARY TABLE IF EXISTS tmp_users;
|
|
||||||
CREATE TEMPORARY TABLE tmp_users (
|
|
||||||
id BIGINT PRIMARY KEY,
|
|
||||||
row_num INT
|
|
||||||
);
|
|
||||||
|
|
||||||
INSERT INTO tmp_users (id, row_num)
|
|
||||||
SELECT id, (@rn2 := @rn2 + 1)
|
|
||||||
FROM users, (SELECT @rn2 := 0) init
|
|
||||||
ORDER BY id;
|
|
||||||
|
|
||||||
SELECT COUNT(*) INTO user_count FROM tmp_users;
|
|
||||||
|
|
||||||
SET batches = CEIL(message_count / batch_limit);
|
|
||||||
|
|
||||||
WHILE current_batch < batches DO
|
|
||||||
INSERT INTO chat_messages (
|
|
||||||
conversation_id, sender_type, sender_id, sender_role,
|
|
||||||
content_type, content, created_at
|
|
||||||
)
|
|
||||||
SELECT
|
|
||||||
c.id as conversation_id,
|
|
||||||
'user' as sender_type,
|
|
||||||
u.id as sender_id,
|
|
||||||
CASE WHEN seq % 2 = 0 THEN 'owner' ELSE 'renter' END as sender_role,
|
|
||||||
'text' as content_type,
|
|
||||||
CONCAT('这是测试消息', seq, ',内容随机生成用于压力测试') as content,
|
|
||||||
DATE_SUB(NOW(), INTERVAL (seq % 30) DAY) as created_at
|
|
||||||
FROM (
|
|
||||||
SELECT @row5 := @row5 + 1 AS seq
|
|
||||||
FROM
|
|
||||||
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
|
||||||
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t1,
|
|
||||||
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
|
||||||
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t2,
|
|
||||||
(SELECT 0 UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4
|
|
||||||
UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) t3,
|
|
||||||
(SELECT @row5 := current_batch * batch_limit) r
|
|
||||||
LIMIT batch_limit
|
|
||||||
) seqs
|
|
||||||
INNER JOIN tmp_conversations c ON c.row_num = (seq % conversation_count) + 1
|
|
||||||
INNER JOIN tmp_users u ON u.row_num = (seq % user_count) + 1
|
|
||||||
WHERE seq <= message_count
|
|
||||||
LIMIT batch_limit;
|
|
||||||
|
|
||||||
SET current_batch = current_batch + 1;
|
|
||||||
COMMIT;
|
|
||||||
END WHILE;
|
|
||||||
|
|
||||||
DROP TEMPORARY TABLE IF EXISTS tmp_conversations;
|
|
||||||
DROP TEMPORARY TABLE IF EXISTS tmp_users;
|
|
||||||
END$$
|
|
||||||
DELIMITER ;
|
|
||||||
|
|
||||||
-- ============================================
|
|
||||||
-- 本文件只创建存储过程,不直接生成数据。
|
|
||||||
-- 请使用 scripts/stress_test.sh data 按参数生成
|
|
||||||
-- ============================================
|
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
|
||||||
@@ -1,482 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# 压力测试快速启动脚本
|
|
||||||
|
|
||||||
set -e
|
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
||||||
|
|
||||||
# 颜色输出
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
function log_info() {
|
|
||||||
echo -e "${GREEN}[INFO]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
function log_warn() {
|
|
||||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
function log_error() {
|
|
||||||
echo -e "${RED}[ERROR]${NC} $1"
|
|
||||||
}
|
|
||||||
|
|
||||||
function show_usage() {
|
|
||||||
cat << EOF
|
|
||||||
压力测试脚本(优化版)
|
|
||||||
|
|
||||||
用法:
|
|
||||||
$0 [command] [options]
|
|
||||||
|
|
||||||
命令:
|
|
||||||
data 生成测试数据(使用优化的批量生成)
|
|
||||||
test 执行压力测试(支持真实认证)
|
|
||||||
monitor 监控系统性能
|
|
||||||
clean 清理测试数据
|
|
||||||
report 生成压测报告
|
|
||||||
all 执行完整流程(生成数据 + 压测 + 报告)
|
|
||||||
|
|
||||||
选项:
|
|
||||||
数据生成:
|
|
||||||
-u, --users NUM 生成用户数量(默认: 1000)
|
|
||||||
-l, --listings NUM 生成商品数量(默认: 5000)
|
|
||||||
-o, --orders NUM 生成订单数量(默认: 3000)
|
|
||||||
--ledger NUM 生成钱包流水数量(默认: 10000)
|
|
||||||
--chat-messages NUM 生成聊天消息数量(默认: 5000)
|
|
||||||
--use-optimized 使用优化的数据生成脚本(推荐)
|
|
||||||
|
|
||||||
压力测试:
|
|
||||||
-c, --concurrency NUM 并发数(默认: 50)
|
|
||||||
-d, --duration SEC 测试时长/秒(默认: 60)
|
|
||||||
-s, --scenario NAME 测试场景:
|
|
||||||
- realistic: 真实业务场景(默认)
|
|
||||||
- admin: 管理后台场景
|
|
||||||
- listing_only: 仅商品查询
|
|
||||||
--gradual 启用梯度压测
|
|
||||||
--warmup NUM 预热用户数(默认: 100)
|
|
||||||
--use-improved 使用改进的压测工具(支持真实认证)
|
|
||||||
--url URL 后端地址(默认: http://localhost:8080)
|
|
||||||
|
|
||||||
通用:
|
|
||||||
-h, --help 显示帮助信息
|
|
||||||
|
|
||||||
示例:
|
|
||||||
# 小规模数据生成(使用优化脚本)
|
|
||||||
$0 data --use-optimized
|
|
||||||
|
|
||||||
# 真实场景压测(100个用户token,50并发,持续60秒)
|
|
||||||
$0 test --use-improved -c 50 -d 60 -s realistic --warmup 100
|
|
||||||
|
|
||||||
# 管理后台压测
|
|
||||||
$0 test --use-improved -c 30 -d 120 -s admin
|
|
||||||
|
|
||||||
# 梯度压测(逐步增加负载)
|
|
||||||
$0 test --use-improved --gradual -d 60
|
|
||||||
|
|
||||||
# 执行完整流程(优化版)
|
|
||||||
$0 all --use-optimized --use-improved
|
|
||||||
|
|
||||||
# 清理测试数据
|
|
||||||
$0 clean
|
|
||||||
|
|
||||||
EOF
|
|
||||||
}
|
|
||||||
|
|
||||||
# 默认参数
|
|
||||||
USERS=1000
|
|
||||||
LISTINGS=5000
|
|
||||||
ORDERS=3000
|
|
||||||
LEDGER=10000
|
|
||||||
CHAT_MESSAGES=5000
|
|
||||||
CONCURRENCY=50
|
|
||||||
DURATION=60
|
|
||||||
SCENARIO="realistic"
|
|
||||||
BASE_URL="http://localhost:8080"
|
|
||||||
USE_OPTIMIZED=false
|
|
||||||
USE_IMPROVED=true
|
|
||||||
GRADUAL=false
|
|
||||||
WARMUP=100
|
|
||||||
|
|
||||||
# 解析命令行参数
|
|
||||||
COMMAND=""
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
data|test|monitor|clean|report|all)
|
|
||||||
COMMAND="$1"
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-u|--users)
|
|
||||||
USERS="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-l|--listings)
|
|
||||||
LISTINGS="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-o|--orders)
|
|
||||||
ORDERS="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--ledger)
|
|
||||||
LEDGER="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--chat-messages)
|
|
||||||
CHAT_MESSAGES="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-c|--concurrency)
|
|
||||||
CONCURRENCY="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-d|--duration)
|
|
||||||
DURATION="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-s|--scenario)
|
|
||||||
SCENARIO="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--url)
|
|
||||||
BASE_URL="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
--use-optimized)
|
|
||||||
USE_OPTIMIZED=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--use-improved)
|
|
||||||
USE_IMPROVED=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--gradual)
|
|
||||||
GRADUAL=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--warmup)
|
|
||||||
WARMUP="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
show_usage
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
log_error "未知参数: $1"
|
|
||||||
show_usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ -z "$COMMAND" ]; then
|
|
||||||
log_error "请指定命令"
|
|
||||||
show_usage
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 检查数据库连接
|
|
||||||
function check_database() {
|
|
||||||
log_info "检查数据库连接..."
|
|
||||||
if docker exec hfb-mysql mysql -uhfb -psecret -e "SELECT 1" &>/dev/null; then
|
|
||||||
log_info "数据库连接正常"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
log_error "数据库连接失败,请确保 Docker 容器正在运行"
|
|
||||||
log_info "提示: 执行 'docker-compose -f deploy/docker-compose.dev.yml up -d' 启动服务"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 检查后端服务
|
|
||||||
function check_backend() {
|
|
||||||
log_info "检查后端服务..."
|
|
||||||
if curl -s "$BASE_URL/health" &>/dev/null; then
|
|
||||||
log_info "后端服务正常"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
log_warn "后端服务未响应: $BASE_URL"
|
|
||||||
log_info "提示: 确保后端服务已启动"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# 生成测试数据
|
|
||||||
function generate_data() {
|
|
||||||
log_info "开始生成测试数据..."
|
|
||||||
log_info "配置: 用户=$USERS, 商品=$LISTINGS, 订单=$ORDERS, 钱包流水=$LEDGER, 聊天消息=$CHAT_MESSAGES"
|
|
||||||
|
|
||||||
check_database || exit 1
|
|
||||||
|
|
||||||
# 选择使用的SQL脚本
|
|
||||||
SQL_FILE="$SCRIPT_DIR/load_test_data.sql"
|
|
||||||
if [ "$USE_OPTIMIZED" = true ]; then
|
|
||||||
SQL_FILE="$SCRIPT_DIR/load_test_data_optimized.sql"
|
|
||||||
log_info "使用优化的数据生成脚本"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 创建临时SQL文件
|
|
||||||
TMP_SQL="/tmp/load_test_data_$(date +%s).sql"
|
|
||||||
trap 'rm -f "$TMP_SQL"' RETURN
|
|
||||||
|
|
||||||
if [ "$USE_OPTIMIZED" = true ]; then
|
|
||||||
cat > "$TMP_SQL" << EOF
|
|
||||||
-- 优化版数据生成
|
|
||||||
USE hfb_sys;
|
|
||||||
|
|
||||||
-- 调用批量生成存储过程
|
|
||||||
CALL generate_users_batch($USERS);
|
|
||||||
CALL generate_listings_batch($LISTINGS);
|
|
||||||
CALL generate_orders_batch($ORDERS);
|
|
||||||
CALL generate_wallet_ledger_batch($LEDGER);
|
|
||||||
CALL generate_chat_data_batch($CHAT_MESSAGES);
|
|
||||||
|
|
||||||
-- 显示统计
|
|
||||||
SELECT '用户数' as item, COUNT(*) as count FROM users
|
|
||||||
UNION ALL SELECT '游戏账号数', COUNT(*) FROM game_accounts
|
|
||||||
UNION ALL SELECT '商品数', COUNT(*) FROM rental_listings
|
|
||||||
UNION ALL SELECT '订单数', COUNT(*) FROM rental_orders
|
|
||||||
UNION ALL SELECT '钱包流水数', COUNT(*) FROM wallet_ledger
|
|
||||||
UNION ALL SELECT '聊天会话数', COUNT(*) FROM chat_conversations
|
|
||||||
UNION ALL SELECT '聊天消息数', COUNT(*) FROM chat_messages;
|
|
||||||
EOF
|
|
||||||
else
|
|
||||||
cat > "$TMP_SQL" << EOF
|
|
||||||
-- 原始版数据生成
|
|
||||||
USE hfb_sys;
|
|
||||||
|
|
||||||
-- 调用存储过程生成数据
|
|
||||||
CALL generate_users($USERS);
|
|
||||||
CALL generate_listings($LISTINGS);
|
|
||||||
CALL generate_orders($ORDERS);
|
|
||||||
CALL generate_wallet_ledger($LEDGER);
|
|
||||||
|
|
||||||
-- 生成聊天会话
|
|
||||||
INSERT INTO chat_conversations (order_id, type, title, status, last_message_at)
|
|
||||||
SELECT id, 'order_group', CONCAT('订单', order_no, '群聊'), 'active', created_at
|
|
||||||
FROM rental_orders
|
|
||||||
WHERE id <= 1000
|
|
||||||
ON DUPLICATE KEY UPDATE order_id=order_id;
|
|
||||||
|
|
||||||
CALL generate_chat_messages($CHAT_MESSAGES);
|
|
||||||
|
|
||||||
-- 显示统计
|
|
||||||
SELECT '用户数' as item, COUNT(*) as count FROM users
|
|
||||||
UNION ALL SELECT '游戏账号数', COUNT(*) FROM game_accounts
|
|
||||||
UNION ALL SELECT '商品数', COUNT(*) FROM rental_listings
|
|
||||||
UNION ALL SELECT '订单数', COUNT(*) FROM rental_orders
|
|
||||||
UNION ALL SELECT '钱包流水数', COUNT(*) FROM wallet_ledger
|
|
||||||
UNION ALL SELECT '聊天会话数', COUNT(*) FROM chat_conversations
|
|
||||||
UNION ALL SELECT '聊天消息数', COUNT(*) FROM chat_messages;
|
|
||||||
EOF
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "执行数据生成..."
|
|
||||||
|
|
||||||
# 先执行基础SQL创建存储过程
|
|
||||||
docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys < "$SQL_FILE"
|
|
||||||
|
|
||||||
# 执行数据生成
|
|
||||||
docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys < "$TMP_SQL"
|
|
||||||
|
|
||||||
rm -f "$TMP_SQL"
|
|
||||||
trap - RETURN
|
|
||||||
|
|
||||||
log_info "测试数据生成完成!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 执行压力测试
|
|
||||||
function run_stress_test() {
|
|
||||||
log_info "开始压力测试..."
|
|
||||||
log_info "配置: 场景=$SCENARIO, 并发=$CONCURRENCY, 时长=${DURATION}秒, 目标=$BASE_URL"
|
|
||||||
|
|
||||||
check_backend || log_warn "后端服务未响应,测试可能失败"
|
|
||||||
|
|
||||||
# 编译压测工具
|
|
||||||
log_info "编译压测工具..."
|
|
||||||
cd "$SCRIPT_DIR"
|
|
||||||
|
|
||||||
go build -o stress_test load_stress.go
|
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
log_error "压测工具编译失败"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 执行压测
|
|
||||||
log_info "开始执行压力测试..."
|
|
||||||
|
|
||||||
if [ "$GRADUAL" = true ]; then
|
|
||||||
./stress_test -url "$BASE_URL" -c "$CONCURRENCY" -d "$DURATION" -s "$SCENARIO" -warmup "$WARMUP" -gradual
|
|
||||||
else
|
|
||||||
./stress_test -url "$BASE_URL" -c "$CONCURRENCY" -d "$DURATION" -s "$SCENARIO" -warmup "$WARMUP"
|
|
||||||
fi
|
|
||||||
|
|
||||||
log_info "压力测试完成!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 监控系统性能
|
|
||||||
function monitor_system() {
|
|
||||||
log_info "开始监控系统性能(按 Ctrl+C 停止)..."
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Docker 容器资源使用 ==="
|
|
||||||
docker stats --no-stream hfb-backend hfb-mysql hfb-redis 2>/dev/null || log_warn "部分容器未运行"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== MySQL 连接数 ==="
|
|
||||||
docker exec hfb-mysql mysql -uhfb -psecret -e "SHOW STATUS LIKE 'Threads_connected';" 2>/dev/null
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== MySQL 慢查询统计 ==="
|
|
||||||
docker exec hfb-mysql mysql -uhfb -psecret -e "SHOW STATUS LIKE 'Slow_queries';" 2>/dev/null
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Redis 信息 ==="
|
|
||||||
docker exec hfb-redis redis-cli INFO stats | grep -E "total_connections_received|total_commands_processed|instantaneous_ops_per_sec" 2>/dev/null
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
log_info "持续监控请使用: docker stats"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 清理测试数据
|
|
||||||
function clean_data() {
|
|
||||||
log_warn "即将清理所有测试数据,此操作不可恢复!"
|
|
||||||
read -p "确认继续?(yes/no): " confirm
|
|
||||||
|
|
||||||
if [ "$confirm" != "yes" ]; then
|
|
||||||
log_info "已取消清理操作"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
check_database || exit 1
|
|
||||||
|
|
||||||
log_info "开始清理测试数据..."
|
|
||||||
|
|
||||||
docker exec -i hfb-mysql mysql -uhfb -psecret hfb_sys << 'EOF'
|
|
||||||
SET FOREIGN_KEY_CHECKS = 0;
|
|
||||||
|
|
||||||
-- 清理测试数据(保留ID < 1000的数据)
|
|
||||||
DELETE FROM chat_messages WHERE id > 100;
|
|
||||||
DELETE FROM chat_conversations WHERE id > 100;
|
|
||||||
DELETE FROM wallet_ledger WHERE id > 100;
|
|
||||||
DELETE FROM rental_orders WHERE id > 100;
|
|
||||||
DELETE FROM rental_listings WHERE id > 100;
|
|
||||||
DELETE FROM game_accounts WHERE id > 100;
|
|
||||||
DELETE FROM users WHERE id > 1000;
|
|
||||||
|
|
||||||
-- 重置自增ID
|
|
||||||
ALTER TABLE users AUTO_INCREMENT = 1001;
|
|
||||||
ALTER TABLE game_accounts AUTO_INCREMENT = 101;
|
|
||||||
ALTER TABLE rental_listings AUTO_INCREMENT = 101;
|
|
||||||
ALTER TABLE rental_orders AUTO_INCREMENT = 101;
|
|
||||||
ALTER TABLE wallet_ledger AUTO_INCREMENT = 101;
|
|
||||||
ALTER TABLE chat_conversations AUTO_INCREMENT = 101;
|
|
||||||
ALTER TABLE chat_messages AUTO_INCREMENT = 101;
|
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
|
||||||
|
|
||||||
-- 显示清理后的统计
|
|
||||||
SELECT '用户数' as item, COUNT(*) as count FROM users
|
|
||||||
UNION ALL SELECT '商品数', COUNT(*) FROM rental_listings
|
|
||||||
UNION ALL SELECT '订单数', COUNT(*) FROM rental_orders
|
|
||||||
UNION ALL SELECT '钱包流水数', COUNT(*) FROM wallet_ledger;
|
|
||||||
EOF
|
|
||||||
|
|
||||||
log_info "测试数据清理完成!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 生成压测报告
|
|
||||||
function generate_report() {
|
|
||||||
log_info "生成压测报告..."
|
|
||||||
|
|
||||||
REPORT_FILE="$PROJECT_ROOT/stress_test_report_$(date +%Y%m%d_%H%M%S).txt"
|
|
||||||
|
|
||||||
{
|
|
||||||
echo "==================================="
|
|
||||||
echo "压力测试报告"
|
|
||||||
echo "生成时间: $(date '+%Y-%m-%d %H:%M:%S')"
|
|
||||||
echo "==================================="
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "--- 数据库统计 ---"
|
|
||||||
docker exec hfb-mysql mysql -uhfb -psecret hfb_sys -e "
|
|
||||||
SELECT '用户数' as item, COUNT(*) as count FROM users
|
|
||||||
UNION ALL SELECT '商品数', COUNT(*) FROM rental_listings
|
|
||||||
UNION ALL SELECT '订单数', COUNT(*) FROM rental_orders
|
|
||||||
UNION ALL SELECT '钱包流水', COUNT(*) FROM wallet_ledger;
|
|
||||||
" 2>/dev/null
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "--- MySQL 性能指标 ---"
|
|
||||||
docker exec hfb-mysql mysql -uhfb -psecret -e "
|
|
||||||
SHOW STATUS LIKE 'Threads_connected';
|
|
||||||
SHOW STATUS LIKE 'Slow_queries';
|
|
||||||
SHOW STATUS LIKE 'Questions';
|
|
||||||
" 2>/dev/null
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "--- 表大小统计 ---"
|
|
||||||
docker exec hfb-mysql mysql -uhfb -psecret hfb_sys -e "
|
|
||||||
SELECT
|
|
||||||
table_name AS '表名',
|
|
||||||
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS '大小(MB)',
|
|
||||||
table_rows AS '行数'
|
|
||||||
FROM information_schema.TABLES
|
|
||||||
WHERE table_schema = 'hfb_sys'
|
|
||||||
ORDER BY (data_length + index_length) DESC
|
|
||||||
LIMIT 10;
|
|
||||||
" 2>/dev/null
|
|
||||||
|
|
||||||
} > "$REPORT_FILE"
|
|
||||||
|
|
||||||
cat "$REPORT_FILE"
|
|
||||||
|
|
||||||
log_info "报告已保存到: $REPORT_FILE"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 执行完整流程
|
|
||||||
function run_all() {
|
|
||||||
log_info "执行完整压测流程..."
|
|
||||||
|
|
||||||
generate_data
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
run_stress_test
|
|
||||||
sleep 2
|
|
||||||
|
|
||||||
generate_report
|
|
||||||
|
|
||||||
log_info "完整流程执行完成!"
|
|
||||||
}
|
|
||||||
|
|
||||||
# 执行命令
|
|
||||||
case $COMMAND in
|
|
||||||
data)
|
|
||||||
generate_data
|
|
||||||
;;
|
|
||||||
test)
|
|
||||||
run_stress_test
|
|
||||||
;;
|
|
||||||
monitor)
|
|
||||||
monitor_system
|
|
||||||
;;
|
|
||||||
clean)
|
|
||||||
clean_data
|
|
||||||
;;
|
|
||||||
report)
|
|
||||||
generate_report
|
|
||||||
;;
|
|
||||||
all)
|
|
||||||
run_all
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
log_error "未知命令: $COMMAND"
|
|
||||||
show_usage
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
Reference in New Issue
Block a user