完成压力测试工具优化和文档整理
主要改进: - 优化压测工具:支持真实认证、智能商品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>
This commit is contained in:
@@ -1,10 +1,27 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func OpenMySQL(dsn string) (*gorm.DB, error) {
|
||||
return gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 限制连接池,避免本地压测瞬间打满 MySQL max_connections。
|
||||
sqlDB.SetMaxOpenConns(50)
|
||||
sqlDB.SetMaxIdleConns(10)
|
||||
sqlDB.SetConnMaxLifetime(30 * time.Minute)
|
||||
sqlDB.SetConnMaxIdleTime(5 * time.Minute)
|
||||
|
||||
return db, nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/auditlog"
|
||||
@@ -22,13 +23,22 @@ import (
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
db *gorm.DB
|
||||
db *gorm.DB
|
||||
publicZoneCountsMu sync.Mutex
|
||||
publicZoneCounts publicZoneCountCache
|
||||
}
|
||||
|
||||
func NewRepository(db *gorm.DB) *Repository {
|
||||
return &Repository{db: db}
|
||||
}
|
||||
|
||||
type publicZoneCountCache struct {
|
||||
Counts map[string]int64
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
const publicZoneCountCacheTTL = 5 * time.Second
|
||||
|
||||
func initialPublishState(reviewRequired bool) (string, string, *time.Time) {
|
||||
if reviewRequired {
|
||||
return "draft", "pending", nil
|
||||
@@ -621,6 +631,11 @@ func (r *Repository) Offline(ownerID uint64, listingID uint64) error {
|
||||
}
|
||||
|
||||
func (r *Repository) ListPublic(query PublicListQuery) (*PublicListResult, error) {
|
||||
page, pageSize := normalizedPublicPage(query)
|
||||
if canListPublicWithSQL(query) {
|
||||
return r.listPublicPage(query, page, pageSize)
|
||||
}
|
||||
|
||||
var rows []listingRow
|
||||
err := r.baseQuery().
|
||||
Where("l.status = ? AND l.review_status = ? AND l.in_transaction = ?", "published", "approved", false).
|
||||
@@ -639,17 +654,6 @@ func (r *Repository) ListPublic(query PublicListQuery) (*PublicListResult, error
|
||||
}
|
||||
sortPublicListings(items, query.Sort)
|
||||
total := int64(len(items))
|
||||
page := query.Page
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
pageSize := query.PageSize
|
||||
if pageSize <= 0 {
|
||||
pageSize = 20
|
||||
}
|
||||
if pageSize > 50 {
|
||||
pageSize = 50
|
||||
}
|
||||
start := (page - 1) * pageSize
|
||||
if start < 0 {
|
||||
start = 0
|
||||
@@ -672,6 +676,156 @@ func (r *Repository) ListPublic(query PublicListQuery) (*PublicListResult, error
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) listPublicPage(query PublicListQuery, page int, pageSize int) (*PublicListResult, error) {
|
||||
var total int64
|
||||
if err := r.db.Table("rental_listings AS l").
|
||||
Where("l.status = ? AND l.review_status = ? AND l.in_transaction = ?", "published", "approved", false).
|
||||
Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rows []listingRow
|
||||
offset := (page - 1) * pageSize
|
||||
err := applyPublicSQLSort(r.baseQuery(), query.Sort).
|
||||
Where("l.status = ? AND l.review_status = ? AND l.in_transaction = ?", "published", "approved", false).
|
||||
Limit(pageSize).
|
||||
Offset(offset).
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
zoneCounts, err := r.publicZoneCountsCached()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PublicListResult{
|
||||
Items: publicListings(rowsToDTO(rows)),
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
ZoneCounts: zoneCounts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizedPublicPage(query PublicListQuery) (int, int) {
|
||||
page := query.Page
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
pageSize := query.PageSize
|
||||
if pageSize <= 0 {
|
||||
pageSize = 20
|
||||
}
|
||||
if pageSize > 50 {
|
||||
pageSize = 50
|
||||
}
|
||||
return page, pageSize
|
||||
}
|
||||
|
||||
func canListPublicWithSQL(query PublicListQuery) bool {
|
||||
if query.Keyword != "" {
|
||||
return false
|
||||
}
|
||||
if query.Zone != "" && query.Zone != "all" {
|
||||
return false
|
||||
}
|
||||
if len(query.Server) > 0 || len(query.Region) > 0 || len(query.LoginMethod) > 0 || len(query.Rank) > 0 {
|
||||
return false
|
||||
}
|
||||
if len(query.Insurance) > 0 || len(query.Stamina) > 0 || len(query.Load) > 0 {
|
||||
return false
|
||||
}
|
||||
if len(query.SkinGroup) > 0 || len(query.SkinName) > 0 || len(query.ResourceRanges) > 0 {
|
||||
return false
|
||||
}
|
||||
if query.MinCoin != nil || query.MaxCoin != nil || query.MinPrice != nil || query.MaxPrice != nil {
|
||||
return false
|
||||
}
|
||||
if query.MinDeposit != nil || query.MaxDeposit != nil || query.MinTotal != nil || query.MaxTotal != nil {
|
||||
return false
|
||||
}
|
||||
if query.MinFireLevel != nil || query.MaxFireLevel != nil || query.MinSecretKD != nil || query.MaxSecretKD != nil {
|
||||
return false
|
||||
}
|
||||
switch query.Sort {
|
||||
case "", "published", "recommended", "comprehensive", "priceAsc", "priceDesc", "coinDesc":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func applyPublicSQLSort(db *gorm.DB, sortKey string) *gorm.DB {
|
||||
switch sortKey {
|
||||
case "priceAsc":
|
||||
return db.Order("l.price ASC, l.published_at DESC, l.id DESC")
|
||||
case "priceDesc":
|
||||
return db.Order("l.price DESC, l.published_at DESC, l.id DESC")
|
||||
case "coinDesc":
|
||||
return db.Order("a.haf_coin_amount DESC, l.published_at DESC, l.id DESC")
|
||||
default:
|
||||
return db.Order("l.published_at DESC, l.id DESC")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Repository) publicZoneCountsCached() (map[string]int64, error) {
|
||||
now := time.Now()
|
||||
r.publicZoneCountsMu.Lock()
|
||||
defer r.publicZoneCountsMu.Unlock()
|
||||
if r.publicZoneCounts.Counts != nil && now.Before(r.publicZoneCounts.ExpiresAt) {
|
||||
return copyPublicZoneCounts(r.publicZoneCounts.Counts), nil
|
||||
}
|
||||
|
||||
var rows []publicZoneRow
|
||||
err := r.db.Table("rental_listings AS l").
|
||||
Select("a.login_platform, a.haf_coin_amount, a.asset_summary").
|
||||
Joins("JOIN game_accounts AS a ON a.id = l.account_id").
|
||||
Where("l.status = ? AND l.review_status = ? AND l.in_transaction = ?", "published", "approved", false).
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
counts := map[string]int64{
|
||||
"all": int64(len(rows)),
|
||||
"sale": 0,
|
||||
"gift": 0,
|
||||
"night": 0,
|
||||
"password": 0,
|
||||
"highCoin": 0,
|
||||
}
|
||||
for _, row := range rows {
|
||||
summary := decodeAssetSummary(row.AssetSummary)
|
||||
if isAcceleratedSale(summary) {
|
||||
counts["sale"]++
|
||||
}
|
||||
if hasGiftResourcesSummary(summary) {
|
||||
counts["gift"]++
|
||||
}
|
||||
if isNightAvailableSummary(summary) {
|
||||
counts["night"]++
|
||||
}
|
||||
if strings.Contains(row.LoginPlatform, "账密") || strings.Contains(row.LoginPlatform, "账号密码") {
|
||||
counts["password"]++
|
||||
}
|
||||
if float64(row.HafCoinAmount)/1000000 >= 100 {
|
||||
counts["highCoin"]++
|
||||
}
|
||||
}
|
||||
r.publicZoneCounts = publicZoneCountCache{
|
||||
Counts: counts,
|
||||
ExpiresAt: now.Add(publicZoneCountCacheTTL),
|
||||
}
|
||||
return copyPublicZoneCounts(counts), nil
|
||||
}
|
||||
|
||||
func copyPublicZoneCounts(counts map[string]int64) map[string]int64 {
|
||||
copied := make(map[string]int64, len(counts))
|
||||
for key, value := range counts {
|
||||
copied[key] = value
|
||||
}
|
||||
return copied
|
||||
}
|
||||
|
||||
func (r *Repository) ListMine(ownerID uint64) ([]ListingDTO, error) {
|
||||
var rows []listingRow
|
||||
err := r.baseQuery().
|
||||
@@ -1155,6 +1309,12 @@ type listingRow struct {
|
||||
ScreenshotURLS datatypes.JSON `gorm:"column:screenshot_urls"`
|
||||
}
|
||||
|
||||
type publicZoneRow struct {
|
||||
LoginPlatform string
|
||||
HafCoinAmount int64
|
||||
AssetSummary datatypes.JSON `gorm:"column:asset_summary"`
|
||||
}
|
||||
|
||||
func rowsToDTO(rows []listingRow) []ListingDTO {
|
||||
items := make([]ListingDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
|
||||
Reference in New Issue
Block a user