feat: 更新玩法配置管理,简化字段并增强功能
- 将玩法相关的显示名称字段统一为 `display_name`,移除多语言字段。 - 在 `PlayTypePatchController` 中新增即时切换玩法开关的功能,并推送大厅更新。 - 优化多个控制器和服务中的权限检查与数据处理逻辑,提升代码可读性与维护性。
This commit is contained in:
26
scripts/perf/README.md
Normal file
26
scripts/perf/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Performance scripts (PRD §17.2)
|
||||
|
||||
Requires [k6](https://grafana.com/docs/k6/latest/set-up/install-k6/).
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `BASE_URL` | yes | API origin, e.g. `http://127.0.0.1:8000` |
|
||||
| `PLAYER_ID` | bet-qps, seal | `players.id` for `Bearer dev:{id}` |
|
||||
| `DRAW_NO` | yes | Open draw `draw_no` (`YYYYMMDD-NNN`) |
|
||||
| `PLAYER_IDS` | oversell | Comma-separated player ids |
|
||||
| `HOT_NUMBER` | oversell | Shared number, default `8888` |
|
||||
| `LINE_AMOUNT` | oversell | Per-request line amount, default `100` |
|
||||
|
||||
Staging must use `LOTTERY_RISK_POOL_USE_REDIS_LUA=true` and sufficient wallet balance.
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
k6 run bet-qps.js
|
||||
k6 run oversell-race.js
|
||||
k6 run seal-after-close.js
|
||||
```
|
||||
|
||||
Thresholds are defined in each script (`thresholds` block). Non-zero exit means failed acceptance.
|
||||
75
scripts/perf/bet-qps.js
Normal file
75
scripts/perf/bet-qps.js
Normal file
@@ -0,0 +1,75 @@
|
||||
import http from 'k6/http';
|
||||
import { check, sleep } from 'k6';
|
||||
import { Counter, Trend } from 'k6/metrics';
|
||||
|
||||
const okLatency = new Trend('place_ok_latency', true);
|
||||
const okCount = new Counter('place_ok_total');
|
||||
|
||||
const baseUrl = __ENV.BASE_URL;
|
||||
const playerId = __ENV.PLAYER_ID;
|
||||
const drawNo = __ENV.DRAW_NO;
|
||||
|
||||
if (!baseUrl || !playerId || !drawNo) {
|
||||
throw new Error('Set BASE_URL, PLAYER_ID, DRAW_NO');
|
||||
}
|
||||
|
||||
export const options = {
|
||||
scenarios: {
|
||||
bet_ramp: {
|
||||
executor: 'ramping-vus',
|
||||
startVUs: 0,
|
||||
stages: [
|
||||
{ duration: '30s', target: 200 },
|
||||
{ duration: '60s', target: 1000 },
|
||||
{ duration: '30s', target: 1000 },
|
||||
{ duration: '15s', target: 0 },
|
||||
],
|
||||
gracefulRampDown: '10s',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
http_req_failed: ['rate<0.05'],
|
||||
http_req_duration: ['p(99)<200'],
|
||||
place_ok_latency: ['p(99)<200'],
|
||||
place_ok_total: ['count>30000'],
|
||||
},
|
||||
};
|
||||
|
||||
function payload(vu, iter) {
|
||||
const n = String(1000 + ((vu + iter) % 9000)).padStart(4, '0');
|
||||
return JSON.stringify({
|
||||
draw_id: drawNo,
|
||||
currency_code: 'NPR',
|
||||
client_trace_id: `k6-${__VU}-${__ITER}-${Date.now()}`,
|
||||
lines: [{ number: n, play_code: 'big', amount: 10 }],
|
||||
});
|
||||
}
|
||||
|
||||
export default function () {
|
||||
const res = http.post(`${baseUrl}/api/v1/ticket/place`, payload(__VU, __ITER), {
|
||||
headers: {
|
||||
Authorization: `Bearer dev:${playerId}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
tags: { name: 'ticket_place' },
|
||||
});
|
||||
|
||||
const ok = check(res, {
|
||||
'status 200': (r) => r.status === 200,
|
||||
'code success': (r) => {
|
||||
try {
|
||||
return r.json('code') === 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (ok) {
|
||||
okLatency.add(res.timings.duration);
|
||||
okCount.add(1);
|
||||
}
|
||||
|
||||
sleep(0.01);
|
||||
}
|
||||
72
scripts/perf/oversell-race.js
Normal file
72
scripts/perf/oversell-race.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import http from 'k6/http';
|
||||
import { check } from 'k6';
|
||||
import { Counter } from 'k6/metrics';
|
||||
|
||||
const baseUrl = __ENV.BASE_URL;
|
||||
const drawNo = __ENV.DRAW_NO;
|
||||
const hotNumber = __ENV.HOT_NUMBER || '8888';
|
||||
const lineAmount = Number(__ENV.LINE_AMOUNT || '100');
|
||||
const playerIds = (__ENV.PLAYER_IDS || '')
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
if (!baseUrl || !drawNo || playerIds.length === 0) {
|
||||
throw new Error('Set BASE_URL, DRAW_NO, PLAYER_IDS (comma-separated)');
|
||||
}
|
||||
|
||||
export const options = {
|
||||
scenarios: {
|
||||
oversell: {
|
||||
executor: 'constant-vus',
|
||||
vus: Math.min(playerIds.length, 200),
|
||||
duration: '45s',
|
||||
},
|
||||
},
|
||||
thresholds: {
|
||||
http_req_failed: ['rate<0.15'],
|
||||
oversell_seen: ['count>0'],
|
||||
success_seen: ['count>0'],
|
||||
},
|
||||
};
|
||||
|
||||
const oversellSeen = new Counter('oversell_seen');
|
||||
const successSeen = new Counter('success_seen');
|
||||
|
||||
function body(playerId, iter) {
|
||||
return JSON.stringify({
|
||||
draw_id: drawNo,
|
||||
currency_code: 'NPR',
|
||||
client_trace_id: `race-${playerId}-${iter}-${Date.now()}`,
|
||||
lines: [{ number: hotNumber, play_code: 'big', amount: lineAmount }],
|
||||
});
|
||||
}
|
||||
|
||||
export default function () {
|
||||
const playerId = playerIds[(__VU - 1) % playerIds.length];
|
||||
const res = http.post(`${baseUrl}/api/v1/ticket/place`, body(playerId, __ITER), {
|
||||
headers: {
|
||||
Authorization: `Bearer dev:${playerId}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
let code = -1;
|
||||
try {
|
||||
code = res.json('code');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (code === 0) {
|
||||
successSeen.add(1);
|
||||
}
|
||||
if (code === 4001) {
|
||||
oversellSeen.add(1);
|
||||
}
|
||||
|
||||
check(res, {
|
||||
'structured response': (r) => r.status === 200 || r.status === 400,
|
||||
});
|
||||
}
|
||||
57
scripts/perf/seal-after-close.js
Normal file
57
scripts/perf/seal-after-close.js
Normal file
@@ -0,0 +1,57 @@
|
||||
import http from 'k6/http';
|
||||
import { check } from 'k6';
|
||||
import { Counter } from 'k6/metrics';
|
||||
|
||||
const baseUrl = __ENV.BASE_URL;
|
||||
const playerId = __ENV.PLAYER_ID;
|
||||
const drawNo = __ENV.DRAW_NO;
|
||||
|
||||
if (!baseUrl || !playerId || !drawNo) {
|
||||
throw new Error('Set BASE_URL, PLAYER_ID, DRAW_NO (must already be sealed / past close_time)');
|
||||
}
|
||||
|
||||
const sealedReject = new Counter('sealed_reject_2001');
|
||||
|
||||
export const options = {
|
||||
vus: 50,
|
||||
duration: '20s',
|
||||
thresholds: {
|
||||
sealed_reject_2001: ['count>400'],
|
||||
http_req_failed: ['rate<0.01'],
|
||||
},
|
||||
};
|
||||
|
||||
export default function () {
|
||||
const res = http.post(
|
||||
`${baseUrl}/api/v1/ticket/place`,
|
||||
JSON.stringify({
|
||||
draw_id: drawNo,
|
||||
currency_code: 'NPR',
|
||||
client_trace_id: `seal-${__VU}-${__ITER}-${Date.now()}`,
|
||||
lines: [{ number: '1234', play_code: 'big', amount: 10 }],
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer dev:${playerId}`,
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let code = null;
|
||||
try {
|
||||
code = res.json('code');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
const rejected = check(res, {
|
||||
'http 400': (r) => r.status === 400,
|
||||
'code 2001': () => code === 2001,
|
||||
});
|
||||
|
||||
if (rejected) {
|
||||
sealedReject.add(1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user