fix:修改JWTbug

This commit is contained in:
zerosaturation 2026-06-15 21:12:59 +08:00
parent 1bc86f0447
commit 060e4944fa
28 changed files with 368 additions and 170 deletions

BIN
backend/gateway-fixed Executable file

Binary file not shown.

View File

@ -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,
})
}

View File

@ -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))

View File

@ -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 表名

View File

@ -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" +

View File

@ -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;

View File

@ -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
}
]
}

View File

@ -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
}
]
}

View File

@ -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
}
]
}

View File

@ -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

1 scenario total errors five_xx p50_ms p95_ms p99_ms max_ms stages
2 S1 8 18 0 0 73.91 86.14 83.07 95.74 83.07 95.74 83.07 95.74 1 3
3 S2 8 18 8 0 0 1.55 10.48 2.90 13.52 2.90 13.52 2.90 13.52 1 3
4 S4 18 18 0 0 1.20 6.80 2.16 14.16 2.16 14.16 2.16 14.16 3

View File

@ -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 \
```

View File

@ -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

View File

@ -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 小时):

View File

@ -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)
}
}
}

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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 '更新时间,毫秒时间戳';

View File

@ -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,

View File

@ -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)

View File

@ -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
}))
}

View File

@ -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 urlroute + 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
}
};