fix:修改JWTbug
This commit is contained in:
parent
1bc86f0447
commit
060e4944fa
BIN
backend/gateway-fixed
Executable file
BIN
backend/gateway-fixed
Executable file
Binary file not shown.
@ -741,9 +741,12 @@ func convertMintingActivitiesResponse(resp *pbActivity.GetMintingActivitiesRespo
|
||||
"cover_image": activity.CoverImage,
|
||||
"star_id": activity.StarId,
|
||||
"route": activity.Route,
|
||||
"is_active": activity.IsActive,
|
||||
"created_at": activity.CreatedAt,
|
||||
"updated_at": activity.UpdatedAt,
|
||||
// 路由参数 JSON 字符串,与 route 配合使用。注意:始终返回字段(包括空字符串),
|
||||
// 便于前端稳定判断是否有路由参数;DB 里 NULL/空时为 ""。
|
||||
"params": activity.Params,
|
||||
"is_active": activity.IsActive,
|
||||
"created_at": activity.CreatedAt,
|
||||
"updated_at": activity.UpdatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ import (
|
||||
"github.com/topfans/backend/gateway/config"
|
||||
"github.com/topfans/backend/gateway/router"
|
||||
"github.com/topfans/backend/pkg/database"
|
||||
"github.com/topfans/backend/pkg/jwt"
|
||||
"github.com/topfans/backend/pkg/logger"
|
||||
"go.uber.org/zap"
|
||||
|
||||
@ -46,7 +47,15 @@ func main() {
|
||||
|
||||
logger.Logger.Info("Starting Top-Fans Gateway...")
|
||||
|
||||
// 2. 加载配置
|
||||
// 2. 初始化 JWT secret (必须在任何 JWT 操作前, 否则用 default)
|
||||
if cfg := os.Getenv("JWT_SECRET"); cfg != "" {
|
||||
jwt.SetSecret(cfg)
|
||||
logger.Logger.Info("JWT secret loaded", zap.Int("bytes", len(cfg)))
|
||||
} else {
|
||||
logger.Logger.Warn("⚠️ JWT_SECRET is empty, using insecure default (DO NOT use in prod)")
|
||||
}
|
||||
|
||||
// 2.5 加载配置
|
||||
cfg := config.Load()
|
||||
if err := cfg.Validate(); err != nil {
|
||||
logger.Logger.Fatal("Invalid configuration", zap.Error(err))
|
||||
|
||||
@ -8,9 +8,12 @@ type MintingActivity struct {
|
||||
CoverImage string `json:"cover_image" gorm:"size:500"`
|
||||
StarID int64 `json:"star_id" gorm:"not null"`
|
||||
Route string `json:"route" gorm:"size:200"`
|
||||
IsActive bool `json:"is_active" gorm:"default:true"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
// Params 路由参数 JSON 字符串,与 Route 配合使用(前端 uni.navigateTo url 拼接)。
|
||||
// 例:{"star_id": 123, "tab": "rank"} -> "/pages/foo?star_id=123&tab=rank"
|
||||
Params string `json:"params" gorm:"type:jsonb;column:params"`
|
||||
IsActive bool `json:"is_active" gorm:"default:true"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
UpdatedAt int64 `json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName 表名
|
||||
|
||||
@ -1397,6 +1397,7 @@ type MintingActivity struct {
|
||||
CoverImage string `protobuf:"bytes,4,opt,name=cover_image,json=coverImage,proto3" json:"cover_image,omitempty"`
|
||||
StarId int64 `protobuf:"varint,5,opt,name=star_id,json=starId,proto3" json:"star_id,omitempty"`
|
||||
Route string `protobuf:"bytes,6,opt,name=route,proto3" json:"route,omitempty"`
|
||||
Params string `protobuf:"bytes,10,opt,name=params,proto3" json:"params,omitempty"` // 路由参数 JSON 字符串,与 route 配合使用(前端 uni.navigateTo url 拼接)
|
||||
IsActive bool `protobuf:"varint,7,opt,name=is_active,json=isActive,proto3" json:"is_active,omitempty"`
|
||||
CreatedAt int64 `protobuf:"varint,8,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
|
||||
UpdatedAt int64 `protobuf:"varint,9,opt,name=updated_at,json=updatedAt,proto3" json:"updated_at,omitempty"`
|
||||
@ -1476,6 +1477,13 @@ func (x *MintingActivity) GetRoute() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *MintingActivity) GetParams() string {
|
||||
if x != nil {
|
||||
return x.Params
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *MintingActivity) GetIsActive() bool {
|
||||
if x != nil {
|
||||
return x.IsActive
|
||||
@ -2016,7 +2024,7 @@ const file_activity_proto_rawDesc = "" +
|
||||
"\x0ftarget_progress\x18\x04 \x01(\x03R\x0etargetProgress\x12#\n" +
|
||||
"\rcurrent_stage\x18\x05 \x01(\tR\fcurrentStage\x12\x19\n" +
|
||||
"\bend_time\x18\x06 \x01(\x03R\aendTime\x12\x16\n" +
|
||||
"\x06status\x18\a \x01(\tR\x06status\"\x84\x02\n" +
|
||||
"\x06status\x18\a \x01(\tR\x06status\"\x9c\x02\n" +
|
||||
"\x0fMintingActivity\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\x03R\x02id\x12\x14\n" +
|
||||
"\x05title\x18\x02 \x01(\tR\x05title\x12 \n" +
|
||||
@ -2024,7 +2032,9 @@ const file_activity_proto_rawDesc = "" +
|
||||
"\vcover_image\x18\x04 \x01(\tR\n" +
|
||||
"coverImage\x12\x17\n" +
|
||||
"\astar_id\x18\x05 \x01(\x03R\x06starId\x12\x14\n" +
|
||||
"\x05route\x18\x06 \x01(\tR\x05route\x12\x1b\n" +
|
||||
"\x05route\x18\x06 \x01(\tR\x05route\x12\x16\n" +
|
||||
"\x06params\x18\n" +
|
||||
" \x01(\tR\x06params\x12\x1b\n" +
|
||||
"\tis_active\x18\a \x01(\bR\bisActive\x12\x1d\n" +
|
||||
"\n" +
|
||||
"created_at\x18\b \x01(\x03R\tcreatedAt\x12\x1d\n" +
|
||||
|
||||
@ -179,6 +179,7 @@ message MintingActivity {
|
||||
string cover_image = 4;
|
||||
int64 star_id = 5;
|
||||
string route = 6;
|
||||
string params = 10; // 路由参数 JSON 字符串,与 route 配合使用(前端 uni.navigateTo url 拼接)
|
||||
bool is_active = 7;
|
||||
int64 created_at = 8;
|
||||
int64 updated_at = 9;
|
||||
|
||||
@ -1,23 +1,45 @@
|
||||
{
|
||||
"scenario": "S1",
|
||||
"total_requests": 8,
|
||||
"total_requests": 18,
|
||||
"errors": 0,
|
||||
"five_xx": 0,
|
||||
"p50_us": 73919,
|
||||
"p95_us": 83071,
|
||||
"p99_us": 83071,
|
||||
"max_us": 83071,
|
||||
"p50_us": 86143,
|
||||
"p95_us": 95743,
|
||||
"p99_us": 95743,
|
||||
"max_us": 95743,
|
||||
"stages": [
|
||||
{
|
||||
"stage_idx": 1,
|
||||
"target_rps": 1,
|
||||
"total_requests": 8,
|
||||
"total_requests": 3,
|
||||
"errors": 0,
|
||||
"five_xx": 0,
|
||||
"p50_us": 73919,
|
||||
"p95_us": 83071,
|
||||
"p99_us": 83071,
|
||||
"max_us": 83071
|
||||
"p50_us": 93951,
|
||||
"p95_us": 98495,
|
||||
"p99_us": 98495,
|
||||
"max_us": 98495
|
||||
},
|
||||
{
|
||||
"stage_idx": 2,
|
||||
"target_rps": 2,
|
||||
"total_requests": 6,
|
||||
"errors": 0,
|
||||
"five_xx": 0,
|
||||
"p50_us": 87295,
|
||||
"p95_us": 89215,
|
||||
"p99_us": 89215,
|
||||
"max_us": 89215
|
||||
},
|
||||
{
|
||||
"stage_idx": 3,
|
||||
"target_rps": 3,
|
||||
"total_requests": 9,
|
||||
"errors": 0,
|
||||
"five_xx": 0,
|
||||
"p50_us": 86143,
|
||||
"p95_us": 95743,
|
||||
"p99_us": 95743,
|
||||
"max_us": 95743
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,23 +1,45 @@
|
||||
{
|
||||
"scenario": "S2",
|
||||
"total_requests": 8,
|
||||
"errors": 8,
|
||||
"total_requests": 18,
|
||||
"errors": 0,
|
||||
"five_xx": 0,
|
||||
"p50_us": 1552,
|
||||
"p95_us": 2909,
|
||||
"p99_us": 2909,
|
||||
"max_us": 2909,
|
||||
"p50_us": 10487,
|
||||
"p95_us": 13527,
|
||||
"p99_us": 13527,
|
||||
"max_us": 13527,
|
||||
"stages": [
|
||||
{
|
||||
"stage_idx": 1,
|
||||
"target_rps": 1,
|
||||
"total_requests": 8,
|
||||
"errors": 8,
|
||||
"total_requests": 3,
|
||||
"errors": 0,
|
||||
"five_xx": 0,
|
||||
"p50_us": 1552,
|
||||
"p95_us": 2909,
|
||||
"p99_us": 2909,
|
||||
"max_us": 2909
|
||||
"p50_us": 13751,
|
||||
"p95_us": 30815,
|
||||
"p99_us": 30815,
|
||||
"max_us": 30815
|
||||
},
|
||||
{
|
||||
"stage_idx": 2,
|
||||
"target_rps": 2,
|
||||
"total_requests": 6,
|
||||
"errors": 0,
|
||||
"five_xx": 0,
|
||||
"p50_us": 9463,
|
||||
"p95_us": 11999,
|
||||
"p99_us": 11999,
|
||||
"max_us": 11999
|
||||
},
|
||||
{
|
||||
"stage_idx": 3,
|
||||
"target_rps": 3,
|
||||
"total_requests": 9,
|
||||
"errors": 0,
|
||||
"five_xx": 0,
|
||||
"p50_us": 10487,
|
||||
"p95_us": 13527,
|
||||
"p99_us": 13527,
|
||||
"max_us": 13527
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,45 +1,45 @@
|
||||
{
|
||||
"scenario": "S4",
|
||||
"total_requests": 18,
|
||||
"errors": 18,
|
||||
"errors": 0,
|
||||
"five_xx": 0,
|
||||
"p50_us": 1210,
|
||||
"p95_us": 2161,
|
||||
"p99_us": 2161,
|
||||
"max_us": 2161,
|
||||
"p50_us": 6803,
|
||||
"p95_us": 14167,
|
||||
"p99_us": 14167,
|
||||
"max_us": 14167,
|
||||
"stages": [
|
||||
{
|
||||
"stage_idx": 1,
|
||||
"target_rps": 1,
|
||||
"total_requests": 3,
|
||||
"errors": 3,
|
||||
"errors": 0,
|
||||
"five_xx": 0,
|
||||
"p50_us": 4143,
|
||||
"p95_us": 8943,
|
||||
"p99_us": 8943,
|
||||
"max_us": 8943
|
||||
"p50_us": 11647,
|
||||
"p95_us": 15183,
|
||||
"p99_us": 15183,
|
||||
"max_us": 15183
|
||||
},
|
||||
{
|
||||
"stage_idx": 2,
|
||||
"target_rps": 2,
|
||||
"total_requests": 6,
|
||||
"errors": 6,
|
||||
"errors": 0,
|
||||
"five_xx": 0,
|
||||
"p50_us": 1314,
|
||||
"p95_us": 2044,
|
||||
"p99_us": 2044,
|
||||
"max_us": 2044
|
||||
"p50_us": 6651,
|
||||
"p95_us": 12479,
|
||||
"p99_us": 12479,
|
||||
"max_us": 12479
|
||||
},
|
||||
{
|
||||
"stage_idx": 3,
|
||||
"target_rps": 3,
|
||||
"total_requests": 9,
|
||||
"errors": 9,
|
||||
"errors": 0,
|
||||
"five_xx": 0,
|
||||
"p50_us": 1210,
|
||||
"p95_us": 2161,
|
||||
"p99_us": 2161,
|
||||
"max_us": 2161
|
||||
"p50_us": 6803,
|
||||
"p95_us": 14167,
|
||||
"p99_us": 14167,
|
||||
"max_us": 14167
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
scenario,total,errors,five_xx,p50_ms,p95_ms,p99_ms,max_ms,stages
|
||||
S1,8,0,0,73.91,83.07,83.07,83.07,1
|
||||
S2,8,8,0,1.55,2.90,2.90,2.90,1
|
||||
S4,18,18,0,1.20,2.16,2.16,2.16,3
|
||||
S1,18,0,0,86.14,95.74,95.74,95.74,3
|
||||
S2,18,0,0,10.48,13.52,13.52,13.52,3
|
||||
S4,18,0,0,6.80,14.16,14.16,14.16,3
|
||||
|
||||
|
@ -4,35 +4,32 @@
|
||||
|
||||
| 项 | 值 |
|
||||
|---|---|
|
||||
| **生成时间** | 2026-06-15 20:05:56 CST |
|
||||
| **压测开始** | 2026-06-15 20:05:47 CST |
|
||||
| **压测结束** | 2026-06-15 20:05:56 CST |
|
||||
| **总耗时** | 9s |
|
||||
| **生成时间** | 2026-06-15 21:06:41 CST |
|
||||
| **压测开始** | 2026-06-15 21:05:10 CST |
|
||||
| **压测结束** | 2026-06-15 21:05:38 CST |
|
||||
| **总耗时** | 27s |
|
||||
| **目标地址** | `http://localhost:8080` |
|
||||
| **测试场景** | S4 |
|
||||
| **测试场景** | S1, S2, S4 |
|
||||
| **阶梯模式** | step (`1,2,3`) |
|
||||
| **JWT 签名密钥** | `topfans-***` (前 8 位) |
|
||||
| **监控模式** | off |
|
||||
| **总请求数** | 34 |
|
||||
| **总错误数** | 26 (76.47%) |
|
||||
| **总请求数** | 54 |
|
||||
| **总错误数** | 0 (0.00%) |
|
||||
| **5xx 数** | 0 (0.00%) |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 执行摘要
|
||||
|
||||
**总览**: ✅ 1 健康 / ⚠️ 0 警告 / 🚨 2 严重 (共 3)
|
||||
**总览**: ✅ 3 健康 / ⚠️ 0 警告 / 🚨 0 严重 (共 3)
|
||||
|
||||
🚨 **关键问题** (2 个):
|
||||
|
||||
- **S2 (浏览资产详情)**: 错误率 100.00%
|
||||
- **S4 (资产铸造 (mint))**: 错误率 100.00%
|
||||
🎉 **所有场景通过健康阈值,系统可承载预期负载。**
|
||||
|
||||
**场景速览**:
|
||||
|
||||
- ✅ **S1 用户登录** — p99=83ms, err 0.00%
|
||||
- 🚨 **S2 浏览资产详情** — p99=3ms, err 100.00%
|
||||
- 🚨 **S4 资产铸造 (mint)** — p99=2ms, err 100.00%
|
||||
- ✅ **S1 用户登录** — p99=96ms, err 0.00%
|
||||
- ✅ **S2 浏览资产详情** — p99=14ms, err 0.00%
|
||||
- ✅ **S4 资产铸造 (mint)** — p99=14ms, err 0.00%
|
||||
|
||||
---
|
||||
|
||||
@ -40,9 +37,9 @@
|
||||
|
||||
| 场景 | 描述 | Total | Err | 5xx | P50ms | P95ms | P99ms | Maxms | 拐点 RPS | 状态 |
|
||||
|------|------|-------|-----|-----|-------|-------|-------|-------|---------|------|
|
||||
| **S1** | 用户登录 | 8 | 0 (0.00%) | 0 (0.00%) | 74 | 83 | 83 | 83 | — | ✅ |
|
||||
| **S2** | 浏览资产详情 | 8 | 8 (100.00%) | 0 (0.00%) | 2 | 3 | 3 | 3 | — | 🚨 |
|
||||
| **S4** | 资产铸造 (mint) | 18 | 18 (100.00%) | 0 (0.00%) | 1 | 2 | 2 | 2 | — | 🚨 |
|
||||
| **S1** | 用户登录 | 18 | 0 (0.00%) | 0 (0.00%) | 86 | 96 | 96 | 96 | — | ✅ |
|
||||
| **S2** | 浏览资产详情 | 18 | 0 (0.00%) | 0 (0.00%) | 10 | 14 | 14 | 14 | — | ✅ |
|
||||
| **S4** | 资产铸造 (mint) | 18 | 0 (0.00%) | 0 (0.00%) | 7 | 14 | 14 | 14 | — | ✅ |
|
||||
|
||||
> 说明: Err 包含 4xx + 5xx,5xx 是子集。错误率 = Err / Total。
|
||||
|
||||
@ -52,9 +49,9 @@
|
||||
|
||||
**P99 / 阈值 比率** (从高到低):
|
||||
|
||||
- S1: 0.08x (83ms)
|
||||
- S2: 0.01x (3ms)
|
||||
- S4: 0.00x (2ms)
|
||||
- S1: 0.10x (96ms)
|
||||
- S2: 0.03x (14ms)
|
||||
- S4: 0.01x (14ms)
|
||||
|
||||
---
|
||||
|
||||
@ -73,22 +70,24 @@
|
||||
|
||||
| 指标 | 实测 | 阈值 | 判定 |
|
||||
|------|------|------|------|
|
||||
| P50ms | 74 | ≤100 | ✅ |
|
||||
| P95ms | 83 | ≤300 | ✅ |
|
||||
| P99ms | 83 | ≤1000 | ✅ |
|
||||
| Maxms | 83 | — | ℹ️ 参考 |
|
||||
| P50ms | 86 | ≤100 | ✅ |
|
||||
| P95ms | 96 | ≤300 | ✅ |
|
||||
| P99ms | 96 | ≤1000 | ✅ |
|
||||
| Maxms | 96 | — | ℹ️ 参考 |
|
||||
| 错误率 | 0.00% | ≤1.00% | ✅ |
|
||||
| 5xx 率 | 0.00% | ≤0.10% | ✅ |
|
||||
|
||||
### 📍 拐点分析
|
||||
|
||||
ℹ️ 仅 1 个 stage,未做阶梯测试,无法判断拐点。
|
||||
✅ **拐点未触发** — 全程 3 个 stage 健康运行,最高 3 RPS p99=96ms。
|
||||
|
||||
### 🔢 阶梯结果
|
||||
|
||||
| Stage | TargetRPS | Total | Err | 5xx | P50ms | P95ms | P99ms | Maxms | 涨幅 |
|
||||
|-------|-----------|-------|-----|-----|-------|-------|-------|-------|------|
|
||||
| 1 | 1 | 8 | 0 | 0 | 74 | 83 | 83 | 83 | |
|
||||
| 1 | 1 | 3 | 0 | 0 | 94 | 98 | 98 | 98 | |
|
||||
| 2 | 2 | 6 | 0 | 0 | 87 | 89 | 89 | 89 | -9% |
|
||||
| 3 | 3 | 9 | 0 | 0 | 86 | 96 | 96 | 96 | +7% |
|
||||
|
||||
### 🎯 行动项
|
||||
|
||||
@ -100,7 +99,7 @@
|
||||
|
||||
---
|
||||
|
||||
## 🚨 S2 浏览资产详情
|
||||
## ✅ S2 浏览资产详情
|
||||
|
||||
### 📌 测试说明
|
||||
|
||||
@ -115,26 +114,28 @@
|
||||
|
||||
| 指标 | 实测 | 阈值 | 判定 |
|
||||
|------|------|------|------|
|
||||
| P50ms | 2 | ≤50 | ✅ |
|
||||
| P95ms | 3 | ≤150 | ✅ |
|
||||
| P99ms | 3 | ≤500 | ✅ |
|
||||
| Maxms | 3 | — | ℹ️ 参考 |
|
||||
| 错误率 | 100.00% | ≤1.00% | 🚨 |
|
||||
| P50ms | 10 | ≤50 | ✅ |
|
||||
| P95ms | 14 | ≤150 | ✅ |
|
||||
| P99ms | 14 | ≤500 | ✅ |
|
||||
| Maxms | 14 | — | ℹ️ 参考 |
|
||||
| 错误率 | 0.00% | ≤1.00% | ✅ |
|
||||
| 5xx 率 | 0.00% | ≤0.10% | ✅ |
|
||||
|
||||
### 📍 拐点分析
|
||||
|
||||
ℹ️ 仅 1 个 stage,未做阶梯测试,无法判断拐点。
|
||||
✅ **拐点未触发** — 全程 3 个 stage 健康运行,最高 3 RPS p99=14ms。
|
||||
|
||||
### 🔢 阶梯结果
|
||||
|
||||
| Stage | TargetRPS | Total | Err | 5xx | P50ms | P95ms | P99ms | Maxms | 涨幅 |
|
||||
|-------|-----------|-------|-----|-----|-------|-------|-------|-------|------|
|
||||
| 1 | 1 | 8 | 8 | 0 | 2 | 3 | 3 | 3 | |
|
||||
| 1 | 1 | 3 | 0 | 0 | 14 | 31 | 31 | 31 | |
|
||||
| 2 | 2 | 6 | 0 | 0 | 9 | 12 | 12 | 12 | -61% |
|
||||
| 3 | 3 | 9 | 0 | 0 | 10 | 14 | 14 | 14 | +13% |
|
||||
|
||||
### 🎯 行动项
|
||||
|
||||
- [ ] **🟡 P1**: 错误率 100.00% — 检查 4xx 错误码,看是否 JWT 过期 / 数据缺失
|
||||
✅ 无需行动项 — 所有指标在阈值内。
|
||||
|
||||
### 📉 图表
|
||||
|
||||
@ -142,7 +143,7 @@
|
||||
|
||||
---
|
||||
|
||||
## 🚨 S4 资产铸造 (mint)
|
||||
## ✅ S4 资产铸造 (mint)
|
||||
|
||||
### 📌 测试说明
|
||||
|
||||
@ -157,28 +158,28 @@
|
||||
|
||||
| 指标 | 实测 | 阈值 | 判定 |
|
||||
|------|------|------|------|
|
||||
| P50ms | 1 | ≤300 | ✅ |
|
||||
| P95ms | 2 | ≤800 | ✅ |
|
||||
| P99ms | 2 | ≤2000 | ✅ |
|
||||
| Maxms | 2 | — | ℹ️ 参考 |
|
||||
| 错误率 | 100.00% | ≤1.00% | 🚨 |
|
||||
| P50ms | 7 | ≤300 | ✅ |
|
||||
| P95ms | 14 | ≤800 | ✅ |
|
||||
| P99ms | 14 | ≤2000 | ✅ |
|
||||
| Maxms | 14 | — | ℹ️ 参考 |
|
||||
| 错误率 | 0.00% | ≤1.00% | ✅ |
|
||||
| 5xx 率 | 0.00% | ≤0.10% | ✅ |
|
||||
|
||||
### 📍 拐点分析
|
||||
|
||||
✅ **拐点未触发** — 全程 3 个 stage 健康运行,最高 3 RPS p99=2ms。
|
||||
✅ **拐点未触发** — 全程 3 个 stage 健康运行,最高 3 RPS p99=14ms。
|
||||
|
||||
### 🔢 阶梯结果
|
||||
|
||||
| Stage | TargetRPS | Total | Err | 5xx | P50ms | P95ms | P99ms | Maxms | 涨幅 |
|
||||
|-------|-----------|-------|-----|-----|-------|-------|-------|-------|------|
|
||||
| 1 | 1 | 3 | 3 | 0 | 4 | 9 | 9 | 9 | |
|
||||
| 2 | 2 | 6 | 6 | 0 | 1 | 2 | 2 | 2 | -77% |
|
||||
| 3 | 3 | 9 | 9 | 0 | 1 | 2 | 2 | 2 | +6% |
|
||||
| 1 | 1 | 3 | 0 | 0 | 12 | 15 | 15 | 15 | |
|
||||
| 2 | 2 | 6 | 0 | 0 | 7 | 12 | 12 | 12 | -18% |
|
||||
| 3 | 3 | 9 | 0 | 0 | 7 | 14 | 14 | 14 | +14% |
|
||||
|
||||
### 🎯 行动项
|
||||
|
||||
- [ ] **🟡 P1**: 错误率 100.00% — 检查 4xx 错误码,看是否 JWT 过期 / 数据缺失
|
||||
✅ 无需行动项 — 所有指标在阈值内。
|
||||
|
||||
### 📉 图表
|
||||
|
||||
@ -221,7 +222,7 @@ reports/
|
||||
|
||||
```bash
|
||||
cd /opt/topfans/loadtest
|
||||
./loadgen --cmd=run --scenarios=S4 --stage=step --step-schedule='1,2,3' \
|
||||
./loadgen --cmd=run --scenarios=S1,S2,S4 --stage=step --step-schedule='1,2,3' \
|
||||
--target=http://localhost:8080 \
|
||||
--monitor=off \
|
||||
```
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
{
|
||||
"start_time": "2026-06-15T20:05:47.357522+08:00",
|
||||
"end_time": "2026-06-15T20:05:56.380495+08:00",
|
||||
"start_time": "2026-06-15T21:05:10.831978+08:00",
|
||||
"end_time": "2026-06-15T21:05:38.174693+08:00",
|
||||
"target": "http://localhost:8080",
|
||||
"scenarios": [
|
||||
"S1",
|
||||
"S2",
|
||||
"S4"
|
||||
],
|
||||
"step_schedule": "1,2,3",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 20 KiB |
@ -175,6 +175,10 @@ JWT_SECRET=$(grep '^JWT_SECRET=' /opt/topfans/docker/.env.prod | cut -d= -f2) \
|
||||
|
||||
## 5. 正式压测 (T0+3min)
|
||||
|
||||
> ⚠️ **重要 flag**: `--inter-scenario-pause` (默认 `0s`)
|
||||
> - prod 凌晨窗口直接连跑,**不加这个 flag 或显式写 `=0s`**
|
||||
> - 旧版本默认是 15 分钟,如果你升级前用过请确认
|
||||
|
||||
### 5.1 选择策略
|
||||
|
||||
**Plan B 推荐** (S1 + S2 + S4,~1.5 小时):
|
||||
|
||||
@ -27,7 +27,7 @@ func main() {
|
||||
rps = flag.Int("rps", 0, "single-RPS mode (overrides stage)")
|
||||
vus = flag.Int("vus", 0, "max concurrent virtual users (default: auto)")
|
||||
duration = flag.Duration("duration", 0, "single stage duration (default per §5.3)")
|
||||
interPause = flag.Duration("inter-scenario-pause", 15*time.Minute, "pause between scenarios")
|
||||
interPause = flag.Duration("inter-scenario-pause", 0, "pause between scenarios (default 0 = back-to-back; 推荐 0 跑 prod, 5m 调试用)")
|
||||
monitor = flag.String("monitor", "lite", "off|lite|full")
|
||||
prodSSH = flag.String("prod-ssh", "", "user@host for ssh metrics")
|
||||
target = flag.String("target", "http://101.132.250.62:8080", "target gateway URL")
|
||||
@ -203,8 +203,10 @@ func runLoadgen(target, scenarioIDs, stage, stepSchedule string, rps, vus int, d
|
||||
break
|
||||
}
|
||||
if idx < len(ids)-1 {
|
||||
log.Printf("inter-scenario pause %v", interPause)
|
||||
time.Sleep(interPause)
|
||||
if interPause > 0 {
|
||||
log.Printf("⏸️ inter-scenario pause %v (用 --inter-scenario-pause=0 跳过)", interPause)
|
||||
time.Sleep(interPause)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -31,20 +31,38 @@ func newS1(c *http.Client, u []lib.TestUser, e, t, f *atomic.Int64, r *lib.Laten
|
||||
}
|
||||
|
||||
func (s *s1Login) Run(ctx context.Context, rpsOverride int, durationOverride time.Duration, dash *lib.Dashboard, breaker *lib.CircuitBreaker, stages []int) error {
|
||||
targetRPS := rpsOverride
|
||||
if targetRPS == 0 {
|
||||
targetRPS = 15
|
||||
// 决定 stage 列表:
|
||||
// - 给了 --step-schedule → 走阶梯模式
|
||||
// - 给了 --rps → 单点 (包成 stage 1)
|
||||
// - 都没给 → 默认 [15]
|
||||
schedule := stages
|
||||
if len(schedule) == 0 {
|
||||
rps := rpsOverride
|
||||
if rps == 0 {
|
||||
rps = 15
|
||||
}
|
||||
schedule = []int{rps}
|
||||
}
|
||||
duration := durationOverride
|
||||
if duration == 0 {
|
||||
duration = 2 * time.Minute
|
||||
stageDuration := durationOverride
|
||||
if stageDuration == 0 {
|
||||
stageDuration = 2 * time.Minute
|
||||
}
|
||||
|
||||
// S1 doesn't internally iterate stages, so wrap entire run as stage 1
|
||||
s.rec.BeginStage(1, targetRPS)
|
||||
defer s.rec.EndStage()
|
||||
for stageIdx, stageRPS := range schedule {
|
||||
logf("S1 stage %d/%d: %d RPS × %v", stageIdx+1, len(schedule), stageRPS, stageDuration)
|
||||
s.rec.BeginStage(stageIdx+1, stageRPS)
|
||||
if err := s.runStage(ctx, stageRPS, stageDuration); err != nil {
|
||||
s.rec.EndStage()
|
||||
return err
|
||||
}
|
||||
s.rec.EndStage()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Second / time.Duration(targetRPS))
|
||||
// runStage 在指定 RPS 下跑 stageDuration 时长。
|
||||
func (s *s1Login) runStage(ctx context.Context, rps int, duration time.Duration) error {
|
||||
ticker := time.NewTicker(time.Second / time.Duration(rps))
|
||||
defer ticker.Stop()
|
||||
timeout := time.NewTimer(duration)
|
||||
defer timeout.Stop()
|
||||
|
||||
@ -29,20 +29,33 @@ func newS2(c *http.Client, u []lib.TestUser, e, t, f *atomic.Int64, r *lib.Laten
|
||||
}
|
||||
|
||||
func (s *s2Read) Run(ctx context.Context, rpsOverride int, durationOverride time.Duration, dash *lib.Dashboard, breaker *lib.CircuitBreaker, stages []int) error {
|
||||
targetRPS := rpsOverride
|
||||
if targetRPS == 0 {
|
||||
targetRPS = 250
|
||||
schedule := stages
|
||||
if len(schedule) == 0 {
|
||||
rps := rpsOverride
|
||||
if rps == 0 {
|
||||
rps = 250
|
||||
}
|
||||
schedule = []int{rps}
|
||||
}
|
||||
duration := durationOverride
|
||||
if duration == 0 {
|
||||
duration = 2 * time.Minute
|
||||
stageDuration := durationOverride
|
||||
if stageDuration == 0 {
|
||||
stageDuration = 2 * time.Minute
|
||||
}
|
||||
|
||||
// S2 doesn't internally iterate stages, wrap entire run as stage 1
|
||||
s.rec.BeginStage(1, targetRPS)
|
||||
defer s.rec.EndStage()
|
||||
for stageIdx, stageRPS := range schedule {
|
||||
logf("S2 stage %d/%d: %d RPS × %v", stageIdx+1, len(schedule), stageRPS, stageDuration)
|
||||
s.rec.BeginStage(stageIdx+1, stageRPS)
|
||||
if err := s.runStage(ctx, stageRPS, stageDuration); err != nil {
|
||||
s.rec.EndStage()
|
||||
return err
|
||||
}
|
||||
s.rec.EndStage()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Second / time.Duration(targetRPS))
|
||||
func (s *s2Read) runStage(ctx context.Context, rps int, duration time.Duration) error {
|
||||
ticker := time.NewTicker(time.Second / time.Duration(rps))
|
||||
defer ticker.Stop()
|
||||
timeout := time.NewTimer(duration)
|
||||
defer timeout.Stop()
|
||||
|
||||
@ -30,20 +30,33 @@ func newS3(c *http.Client, u []lib.TestUser, e, t, f *atomic.Int64, r *lib.Laten
|
||||
}
|
||||
|
||||
func (s *s3Like) Run(ctx context.Context, rpsOverride int, durationOverride time.Duration, dash *lib.Dashboard, breaker *lib.CircuitBreaker, stages []int) error {
|
||||
targetRPS := rpsOverride
|
||||
if targetRPS == 0 {
|
||||
targetRPS = 50
|
||||
schedule := stages
|
||||
if len(schedule) == 0 {
|
||||
rps := rpsOverride
|
||||
if rps == 0 {
|
||||
rps = 50
|
||||
}
|
||||
schedule = []int{rps}
|
||||
}
|
||||
duration := durationOverride
|
||||
if duration == 0 {
|
||||
duration = 2 * time.Minute
|
||||
stageDuration := durationOverride
|
||||
if stageDuration == 0 {
|
||||
stageDuration = 2 * time.Minute
|
||||
}
|
||||
|
||||
// S3 doesn't internally iterate stages, wrap entire run as stage 1
|
||||
s.rec.BeginStage(1, targetRPS)
|
||||
defer s.rec.EndStage()
|
||||
for stageIdx, stageRPS := range schedule {
|
||||
logf("S3 stage %d/%d: %d RPS × %v", stageIdx+1, len(schedule), stageRPS, stageDuration)
|
||||
s.rec.BeginStage(stageIdx+1, stageRPS)
|
||||
if err := s.runStage(ctx, stageRPS, stageDuration); err != nil {
|
||||
s.rec.EndStage()
|
||||
return err
|
||||
}
|
||||
s.rec.EndStage()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Second / time.Duration(targetRPS))
|
||||
func (s *s3Like) runStage(ctx context.Context, rps int, duration time.Duration) error {
|
||||
ticker := time.NewTicker(time.Second / time.Duration(rps))
|
||||
defer ticker.Stop()
|
||||
timeout := time.NewTimer(duration)
|
||||
defer timeout.Stop()
|
||||
@ -63,7 +76,7 @@ func (s *s3Like) Run(ctx context.Context, rpsOverride int, durationOverride time
|
||||
if rand.Float64() < 0.5 {
|
||||
exID := u.ExhibitionIDs[rand.Intn(len(u.ExhibitionIDs))]
|
||||
body := fmt.Sprintf(`{"exhibition_id":%d}`, exID)
|
||||
assetID := u.AssetIDs[rand.Intn(2)] // asset 1, 2 (上<EFBFBD>架的)
|
||||
assetID := u.AssetIDs[rand.Intn(2)] // asset 1, 2 (上架的)
|
||||
req, _ := http.NewRequest("POST", fmt.Sprintf("%s/api/v1/social/assets/%d/like", s.baseURL, assetID), strings.NewReader(body))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+u.JWTToken)
|
||||
|
||||
@ -37,16 +37,33 @@ func newS5(c *http.Client, u []lib.TestUser, e, t, f *atomic.Int64, r *lib.Laten
|
||||
}
|
||||
|
||||
func (s *s5Dashboard) Run(ctx context.Context, rpsOverride int, durationOverride time.Duration, dash *lib.Dashboard, breaker *lib.CircuitBreaker, stages []int) error {
|
||||
targetRPS := rpsOverride
|
||||
if targetRPS == 0 {
|
||||
targetRPS = 20 // 用户会话/秒 × 7 = 140 backend QPS
|
||||
schedule := stages
|
||||
if len(schedule) == 0 {
|
||||
rps := rpsOverride
|
||||
if rps == 0 {
|
||||
rps = 20 // 用户会话/秒 × 7 = 140 backend QPS
|
||||
}
|
||||
schedule = []int{rps}
|
||||
}
|
||||
duration := durationOverride
|
||||
if duration == 0 {
|
||||
duration = 2 * time.Minute
|
||||
stageDuration := durationOverride
|
||||
if stageDuration == 0 {
|
||||
stageDuration = 2 * time.Minute
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Second / time.Duration(targetRPS))
|
||||
for stageIdx, stageRPS := range schedule {
|
||||
logf("S5 stage %d/%d: %d RPS × %v", stageIdx+1, len(schedule), stageRPS, stageDuration)
|
||||
s.rec.BeginStage(stageIdx+1, stageRPS)
|
||||
if err := s.runStage(ctx, stageRPS, stageDuration); err != nil {
|
||||
s.rec.EndStage()
|
||||
return err
|
||||
}
|
||||
s.rec.EndStage()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *s5Dashboard) runStage(ctx context.Context, rps int, duration time.Duration) error {
|
||||
ticker := time.NewTicker(time.Second / time.Duration(rps))
|
||||
defer ticker.Stop()
|
||||
timeout := time.NewTimer(duration)
|
||||
defer timeout.Stop()
|
||||
|
||||
@ -34,16 +34,33 @@ func newS6(c *http.Client, u []lib.TestUser, e, t, f *atomic.Int64, r *lib.Laten
|
||||
}
|
||||
|
||||
func (s *s6Ranking) Run(ctx context.Context, rpsOverride int, durationOverride time.Duration, dash *lib.Dashboard, breaker *lib.CircuitBreaker, stages []int) error {
|
||||
targetRPS := rpsOverride
|
||||
if targetRPS == 0 {
|
||||
targetRPS = 300
|
||||
schedule := stages
|
||||
if len(schedule) == 0 {
|
||||
rps := rpsOverride
|
||||
if rps == 0 {
|
||||
rps = 300
|
||||
}
|
||||
schedule = []int{rps}
|
||||
}
|
||||
duration := durationOverride
|
||||
if duration == 0 {
|
||||
duration = 2 * time.Minute
|
||||
stageDuration := durationOverride
|
||||
if stageDuration == 0 {
|
||||
stageDuration = 2 * time.Minute
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Second / time.Duration(targetRPS))
|
||||
for stageIdx, stageRPS := range schedule {
|
||||
logf("S6 stage %d/%d: %d RPS × %v", stageIdx+1, len(schedule), stageRPS, stageDuration)
|
||||
s.rec.BeginStage(stageIdx+1, stageRPS)
|
||||
if err := s.runStage(ctx, stageRPS, stageDuration); err != nil {
|
||||
s.rec.EndStage()
|
||||
return err
|
||||
}
|
||||
s.rec.EndStage()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *s6Ranking) runStage(ctx context.Context, rps int, duration time.Duration) error {
|
||||
ticker := time.NewTicker(time.Second / time.Duration(rps))
|
||||
defer ticker.Stop()
|
||||
timeout := time.NewTimer(duration)
|
||||
defer timeout.Stop()
|
||||
|
||||
@ -31,16 +31,33 @@ func newS7(c *http.Client, u []lib.TestUser, e, t, f *atomic.Int64, r *lib.Laten
|
||||
}
|
||||
|
||||
func (s *s7Place) Run(ctx context.Context, rpsOverride int, durationOverride time.Duration, dash *lib.Dashboard, breaker *lib.CircuitBreaker, stages []int) error {
|
||||
targetRPS := rpsOverride
|
||||
if targetRPS == 0 {
|
||||
targetRPS = 50
|
||||
schedule := stages
|
||||
if len(schedule) == 0 {
|
||||
rps := rpsOverride
|
||||
if rps == 0 {
|
||||
rps = 50
|
||||
}
|
||||
schedule = []int{rps}
|
||||
}
|
||||
duration := durationOverride
|
||||
if duration == 0 {
|
||||
duration = 2 * time.Minute
|
||||
stageDuration := durationOverride
|
||||
if stageDuration == 0 {
|
||||
stageDuration = 2 * time.Minute
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(time.Second / time.Duration(targetRPS))
|
||||
for stageIdx, stageRPS := range schedule {
|
||||
logf("S7 stage %d/%d: %d RPS × %v", stageIdx+1, len(schedule), stageRPS, stageDuration)
|
||||
s.rec.BeginStage(stageIdx+1, stageRPS)
|
||||
if err := s.runStage(ctx, stageRPS, stageDuration); err != nil {
|
||||
s.rec.EndStage()
|
||||
return err
|
||||
}
|
||||
s.rec.EndStage()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *s7Place) runStage(ctx context.Context, rps int, duration time.Duration) error {
|
||||
ticker := time.NewTicker(time.Second / time.Duration(rps))
|
||||
defer ticker.Stop()
|
||||
timeout := time.NewTimer(duration)
|
||||
defer timeout.Stop()
|
||||
|
||||
@ -382,6 +382,7 @@ COMMENT ON COLUMN public.minting_activities.description IS '活动描述';
|
||||
COMMENT ON COLUMN public.minting_activities.cover_image IS '活动封面图 URL';
|
||||
COMMENT ON COLUMN public.minting_activities.star_id IS '所属明星/星球 ID';
|
||||
COMMENT ON COLUMN public.minting_activities.route IS '活动页面路由路径';
|
||||
COMMENT ON COLUMN public.minting_activities.params IS '路由参数 JSON 字符串,与 route 配合使用(前端 uni.navigateTo url 拼接)';
|
||||
COMMENT ON COLUMN public.minting_activities.is_active IS '是否启用';
|
||||
COMMENT ON COLUMN public.minting_activities.created_at IS '创建时间,毫秒时间戳';
|
||||
COMMENT ON COLUMN public.minting_activities.updated_at IS '更新时间,毫秒时间戳';
|
||||
|
||||
@ -903,6 +903,7 @@ func (s *activityService) GetMintingActivities(ctx context.Context, req *pb.GetM
|
||||
CoverImage: activity.CoverImage,
|
||||
StarId: activity.StarID,
|
||||
Route: activity.Route,
|
||||
Params: activity.Params,
|
||||
IsActive: activity.IsActive,
|
||||
CreatedAt: activity.CreatedAt,
|
||||
UpdatedAt: activity.UpdatedAt,
|
||||
|
||||
@ -14,6 +14,7 @@ import (
|
||||
|
||||
"github.com/topfans/backend/pkg/database"
|
||||
"github.com/topfans/backend/pkg/health"
|
||||
"github.com/topfans/backend/pkg/jwt"
|
||||
"github.com/topfans/backend/pkg/logger"
|
||||
"github.com/topfans/backend/pkg/models"
|
||||
pb "github.com/topfans/backend/pkg/proto/user"
|
||||
@ -72,6 +73,16 @@ func main() {
|
||||
|
||||
logger.Sugar.Info("Starting User Service...")
|
||||
|
||||
// 初始化 JWT secret (必须在任何 JWT 操作前)
|
||||
jwtSecret := os.Getenv("JWT_SECRET")
|
||||
if jwtSecret == "" {
|
||||
logger.Sugar.Warn("⚠️ JWT_SECRET is empty, using insecure default (DO NOT use in prod)")
|
||||
jwtSecret = "your-secret-key-change-in-production"
|
||||
} else {
|
||||
logger.Sugar.Infof("JWT secret loaded (%d bytes)", len(jwtSecret))
|
||||
}
|
||||
jwt.SetSecret(jwtSecret)
|
||||
|
||||
// 初始化数据库
|
||||
if err := initDatabase(); err != nil {
|
||||
logger.Sugar.Fatalf("Failed to initialize database: %v", err)
|
||||
|
||||
@ -47,7 +47,9 @@ export function useBanner() {
|
||||
link_type: 'activity',
|
||||
link_value: String(activity.id),
|
||||
description: activity.description,
|
||||
route: activity.route
|
||||
route: activity.route,
|
||||
// 后端路由参数 JSON 字符串,前端在 handleBannerClick 中拼到 url 上
|
||||
params: activity.params
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@ -191,17 +191,26 @@ const handleBannerClick = (banner) => {
|
||||
console.log("[square] banner click", banner);
|
||||
// 优先使用自定义 route
|
||||
if (banner.route) {
|
||||
return uni.navigateTo({ url: banner.route });
|
||||
return uni.navigateTo({ url: buildBannerUrl(banner) });
|
||||
}
|
||||
if (banner.link_type === "activity") {
|
||||
return uni.navigateTo({
|
||||
url: `/pages/castlove/detail?id=${banner.link_value}`,
|
||||
});
|
||||
}
|
||||
if (banner.link_type === "topic") {
|
||||
return uni.navigateTo({
|
||||
url: `/pages/topic/detail?id=${banner.link_value}`,
|
||||
});
|
||||
};
|
||||
|
||||
// 拼接 banner 跳转 url:route + params(JSON 字符串解析后作为 query)
|
||||
const buildBannerUrl = (banner) => {
|
||||
const base = banner.route || ""
|
||||
if (!banner.params) return base
|
||||
try {
|
||||
const obj = JSON.parse(banner.params)
|
||||
const entries = Object.entries(obj)
|
||||
.filter(([, v]) => v !== undefined && v !== null)
|
||||
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
|
||||
if (entries.length === 0) return base
|
||||
const sep = base.includes("?") ? "&" : "?"
|
||||
return `${base}${sep}${entries.join("&")}`
|
||||
} catch (e) {
|
||||
// params 解析失败时只打印日志,不影响 route 跳转
|
||||
console.warn("[square] banner.params 解析失败, 忽略", banner.params, e)
|
||||
return base
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user