品牌活动调整
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
# 登山活动 1300 人并发报名压测
|
||||
|
||||
本目录用于本地或已明确授权的测试环境。JMeter 会真实调用登录和报名接口,并向报名表写入数据,禁止直接对生产环境执行。
|
||||
|
||||
## 文件说明
|
||||
|
||||
- `prepare-brand-activity-users.ps1`:从当前项目数据库导出活动范围内、未禁用且尚未报名的人员。
|
||||
- `brand-activity-mountain-signup-1300.jmx`:每线程独立登录,所有线程到达报名步骤后同时提交。
|
||||
- `data/brand-activity-users.csv`:运行准备脚本后生成,不包含密码、手机号、身份证号等敏感数据。
|
||||
|
||||
当前登山活动参数:
|
||||
|
||||
- 活动 ID:`11619eee67574256ad8ccc8a16630023`
|
||||
- 子活动 ID:`986bf4d48a5948e1810857483f9e5af2`
|
||||
- 容量:1500 人
|
||||
|
||||
## 1. 准备人员数据
|
||||
|
||||
在项目根目录执行:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\jmeter\prepare-brand-activity-users.ps1 -UserCount 1300 -PermissionMode AllScope
|
||||
```
|
||||
|
||||
`AllScope` 会导出1300名活动范围人员。根据当前数据库,约290名人员没有报名菜单权限,其报名请求会被权限校验拒绝。
|
||||
|
||||
如果只测试具备权限的人员:
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File .\jmeter\prepare-brand-activity-users.ps1 -UserCount 1010 -PermissionMode Authorized
|
||||
```
|
||||
|
||||
当前数据库只有1010名“有报名权限且尚未报名”的人员,因此 `Authorized` 模式不能生成1300名不同人员。
|
||||
|
||||
## 2. 先进行10人试跑
|
||||
|
||||
启动本地项目,确认80端口和 Redis 6378 可访问,然后执行:
|
||||
|
||||
```powershell
|
||||
jmeter -n -t .\jmeter\brand-activity-mountain-signup-1300.jmx `
|
||||
-Jthreads=10 `
|
||||
-JloginRampSeconds=10 `
|
||||
-Jhost=127.0.0.1 `
|
||||
-Jport=80 `
|
||||
-JusersFile=jmeter/data/brand-activity-users.csv `
|
||||
-l .\jmeter\result-smoke.jtl `
|
||||
-j .\jmeter\jmeter-smoke.log
|
||||
```
|
||||
|
||||
试跑会真实产生报名记录。再次运行前必须重新生成 CSV,以排除已经报名的人员。
|
||||
|
||||
## 3. 执行1300人并发
|
||||
|
||||
确认测试数据库已备份、应用和 Redis 正常、活动仍在报名时间内后执行:
|
||||
|
||||
```powershell
|
||||
jmeter -n -t .\jmeter\brand-activity-mountain-signup-1300.jmx `
|
||||
-Jthreads=1300 `
|
||||
-JloginRampSeconds=60 `
|
||||
-JsyncTimeoutMs=300000 `
|
||||
-Jhost=127.0.0.1 `
|
||||
-Jport=80 `
|
||||
-JusersFile=jmeter/data/brand-activity-users.csv `
|
||||
-l .\jmeter\result-1300.jtl `
|
||||
-j .\jmeter\jmeter-1300.log
|
||||
```
|
||||
|
||||
登录在60秒内逐步完成;所有仍在运行的线程会在“07-集合后并发提交登山活动报名”前等待,然后同时发送报名请求。
|
||||
|
||||
## 4. 生成结果报告
|
||||
|
||||
```powershell
|
||||
jmeter -g .\jmeter\result-1300.jtl -o .\jmeter\report-1300
|
||||
```
|
||||
|
||||
重点查看:
|
||||
|
||||
- `05-人员账号登录`:独立账号登录是否成功。
|
||||
- `06-报名资格预校验`:活动范围、时间、重复报名和名额检查。
|
||||
- `07-集合后并发提交登山活动报名`:真正的并发报名结果。
|
||||
|
||||
报名结果只有返回 `code=0` 且 `msg=报名成功` 才计为成功,避免后端异常分支返回成功状态码但消息为“报名失败”时被误判。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 登录验证码由应用写入本机 Redis,JMX 仅在授权测试环境中从 Redis 读取对应验证码答案。
|
||||
- 当前 `cas.enable=false`。现有登录实现只在 CAS 模式开启时拒绝密码不匹配,因此 JMX 使用测试占位密码;该行为不应复制到生产环境。
|
||||
- Controller 使用单 JVM `ReentrantLock`,单节点下报名请求会排队;多节点下该锁无法跨节点保证一致性。
|
||||
- JMeter 1300线程会消耗较多内存。若出现堆内存不足,可在启动前提高 `HEAP`,例如 `-Xms2g -Xmx4g`。
|
||||
- 不提供自动删除报名数据的脚本,避免误删真实报名记录。
|
||||
@@ -0,0 +1,291 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<jmeterTestPlan version="1.2" properties="5.0" jmeter="5.6.3">
|
||||
<hashTree>
|
||||
<TestPlan guiclass="TestPlanGui" testclass="TestPlan" testname="品牌活动-登山活动1300人并发报名" enabled="true">
|
||||
<stringProp name="TestPlan.comments">每个线程使用独立用户和 Cookie 登录,登录完成后在报名接口前集合并发。仅限本地或经授权的测试环境。</stringProp>
|
||||
<boolProp name="TestPlan.functional_mode">false</boolProp>
|
||||
<boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>
|
||||
<boolProp name="TestPlan.serialize_threadgroups">false</boolProp>
|
||||
<elementProp name="TestPlan.user_defined_variables" elementType="Arguments" guiclass="ArgumentsPanel" testclass="Arguments" testname="业务参数" enabled="true">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="activityId" elementType="Argument">
|
||||
<stringProp name="Argument.name">activityId</stringProp>
|
||||
<stringProp name="Argument.value">${__P(activityId,11619eee67574256ad8ccc8a16630023)}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="courseId" elementType="Argument">
|
||||
<stringProp name="Argument.name">courseId</stringProp>
|
||||
<stringProp name="Argument.value">${__P(courseId,986bf4d48a5948e1810857483f9e5af2)}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="redisHost" elementType="Argument">
|
||||
<stringProp name="Argument.name">redisHost</stringProp>
|
||||
<stringProp name="Argument.value">${__P(redisHost,127.0.0.1)}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="redisPort" elementType="Argument">
|
||||
<stringProp name="Argument.name">redisPort</stringProp>
|
||||
<stringProp name="Argument.value">${__P(redisPort,6378)}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="testPassword" elementType="Argument">
|
||||
<stringProp name="Argument.name">testPassword</stringProp>
|
||||
<stringProp name="Argument.value">${__P(testPassword,LoadTestOnly@123)}</stringProp>
|
||||
<stringProp name="Argument.metadata">=</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
</TestPlan>
|
||||
<hashTree>
|
||||
<CSVDataSet guiclass="TestBeanGUI" testclass="CSVDataSet" testname="读取数据库人员CSV" enabled="true">
|
||||
<stringProp name="delimiter">,</stringProp>
|
||||
<stringProp name="fileEncoding">UTF-8</stringProp>
|
||||
<stringProp name="filename">${__P(usersFile,jmeter/data/brand-activity-users.csv)}</stringProp>
|
||||
<boolProp name="ignoreFirstLine">false</boolProp>
|
||||
<boolProp name="quotedData">true</boolProp>
|
||||
<boolProp name="recycle">false</boolProp>
|
||||
<stringProp name="shareMode">shareMode.all</stringProp>
|
||||
<boolProp name="stopThread">true</boolProp>
|
||||
<stringProp name="variableNames"></stringProp>
|
||||
</CSVDataSet>
|
||||
<hashTree/>
|
||||
<ThreadGroup guiclass="ThreadGroupGui" testclass="ThreadGroup" testname="独立用户登录并并发报名" enabled="true">
|
||||
<stringProp name="ThreadGroup.on_sample_error">continue</stringProp>
|
||||
<elementProp name="ThreadGroup.main_controller" elementType="LoopController" guiclass="LoopControlPanel" testclass="LoopController" testname="循环控制器" enabled="true">
|
||||
<boolProp name="LoopController.continue_forever">false</boolProp>
|
||||
<stringProp name="LoopController.loops">1</stringProp>
|
||||
</elementProp>
|
||||
<stringProp name="ThreadGroup.num_threads">${__P(threads,10)}</stringProp>
|
||||
<stringProp name="ThreadGroup.ramp_time">${__P(loginRampSeconds,30)}</stringProp>
|
||||
<boolProp name="ThreadGroup.scheduler">false</boolProp>
|
||||
<stringProp name="ThreadGroup.duration"></stringProp>
|
||||
<stringProp name="ThreadGroup.delay"></stringProp>
|
||||
<boolProp name="ThreadGroup.same_user_on_next_iteration">true</boolProp>
|
||||
</ThreadGroup>
|
||||
<hashTree>
|
||||
<ConfigTestElement guiclass="HttpDefaultsGui" testclass="ConfigTestElement" testname="HTTP默认配置" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="用户定义变量" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.domain">${__P(host,127.0.0.1)}</stringProp>
|
||||
<stringProp name="HTTPSampler.port">${__P(port,80)}</stringProp>
|
||||
<stringProp name="HTTPSampler.protocol">${__P(protocol,http)}</stringProp>
|
||||
<stringProp name="HTTPSampler.connect_timeout">${__P(connectTimeout,10000)}</stringProp>
|
||||
<stringProp name="HTTPSampler.response_timeout">${__P(responseTimeout,120000)}</stringProp>
|
||||
<stringProp name="HTTPSampler.contentEncoding">UTF-8</stringProp>
|
||||
</ConfigTestElement>
|
||||
<hashTree/>
|
||||
<CookieManager guiclass="CookiePanel" testclass="CookieManager" testname="每线程独立Cookie" enabled="true">
|
||||
<collectionProp name="CookieManager.cookies"/>
|
||||
<boolProp name="CookieManager.clearEachIteration">true</boolProp>
|
||||
<boolProp name="CookieManager.controlledByThreadGroup">false</boolProp>
|
||||
</CookieManager>
|
||||
<hashTree/>
|
||||
<HeaderManager guiclass="HeaderPanel" testclass="HeaderManager" testname="请求头" enabled="true">
|
||||
<collectionProp name="HeaderManager.headers">
|
||||
<elementProp name="Content-Type" elementType="Header">
|
||||
<stringProp name="Header.name">Content-Type</stringProp>
|
||||
<stringProp name="Header.value">application/x-www-form-urlencoded;charset=UTF-8</stringProp>
|
||||
</elementProp>
|
||||
<elementProp name="Accept" elementType="Header">
|
||||
<stringProp name="Header.name">Accept</stringProp>
|
||||
<stringProp name="Header.value">application/json, text/plain, */*</stringProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</HeaderManager>
|
||||
<hashTree/>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="01-获取图形验证码" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="请求参数" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.path">/platform/login/captcha</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.postBodyRaw">false</boolProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="提取验证码Key" enabled="true">
|
||||
<stringProp name="JSONPostProcessor.referenceNames">captchaKey</stringProp>
|
||||
<stringProp name="JSONPostProcessor.jsonPathExprs">$.data.key</stringProp>
|
||||
<stringProp name="JSONPostProcessor.match_numbers">1</stringProp>
|
||||
<stringProp name="JSONPostProcessor.defaultValues">NOT_FOUND</stringProp>
|
||||
</JSONPostProcessor>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<JSR223Sampler guiclass="TestBeanGUI" testclass="JSR223Sampler" testname="02-从本地Redis读取压测验证码" enabled="true">
|
||||
<stringProp name="cacheKey">true</stringProp>
|
||||
<stringProp name="filename"></stringProp>
|
||||
<stringProp name="parameters"></stringProp>
|
||||
<stringProp name="scriptLanguage">groovy</stringProp>
|
||||
<stringProp name="script"><![CDATA[
|
||||
def captchaKey = vars.get('captchaKey')
|
||||
if (!captchaKey || captchaKey == 'NOT_FOUND') {
|
||||
throw new IllegalStateException('获取验证码Key失败')
|
||||
}
|
||||
def redisKey = 'budwk5mini:ucenter:captcha:' + captchaKey
|
||||
def socket = new Socket(vars.get('redisHost'), vars.get('redisPort') as int)
|
||||
socket.soTimeout = 3000
|
||||
try {
|
||||
def keyBytes = redisKey.getBytes('UTF-8')
|
||||
def command = '*2\r\n$3\r\nGET\r\n$' + keyBytes.length + '\r\n' + redisKey + '\r\n'
|
||||
socket.outputStream.write(command.getBytes('UTF-8'))
|
||||
socket.outputStream.flush()
|
||||
def reader = new BufferedReader(new InputStreamReader(socket.inputStream, 'UTF-8'))
|
||||
def header = reader.readLine()
|
||||
if (!header || !header.startsWith('$') || header == '$-1') {
|
||||
throw new IllegalStateException('Redis中未找到验证码:' + redisKey)
|
||||
}
|
||||
def captchaCode = reader.readLine()
|
||||
if (!captchaCode) {
|
||||
throw new IllegalStateException('验证码内容为空')
|
||||
}
|
||||
vars.put('captchaCode', captchaCode)
|
||||
} finally {
|
||||
socket.close()
|
||||
}
|
||||
]]></stringProp>
|
||||
</JSR223Sampler>
|
||||
<hashTree/>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="03-获取登录RSA公钥" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="请求参数" enabled="true">
|
||||
<collectionProp name="Arguments.arguments"/>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.path">/platform/login/publicKey</stringProp>
|
||||
<stringProp name="HTTPSampler.method">GET</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<JSONPostProcessor guiclass="JSONPostProcessorGui" testclass="JSONPostProcessor" testname="提取公钥和KeyId" enabled="true">
|
||||
<stringProp name="JSONPostProcessor.referenceNames">publicKey;keyId</stringProp>
|
||||
<stringProp name="JSONPostProcessor.jsonPathExprs">$.data.publicKey;$.data.keyId</stringProp>
|
||||
<stringProp name="JSONPostProcessor.match_numbers">1;1</stringProp>
|
||||
<stringProp name="JSONPostProcessor.defaultValues">NOT_FOUND;NOT_FOUND</stringProp>
|
||||
</JSONPostProcessor>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<JSR223Sampler guiclass="TestBeanGUI" testclass="JSR223Sampler" testname="04-RSA加密测试密码" enabled="true">
|
||||
<stringProp name="cacheKey">true</stringProp>
|
||||
<stringProp name="filename"></stringProp>
|
||||
<stringProp name="parameters"></stringProp>
|
||||
<stringProp name="scriptLanguage">groovy</stringProp>
|
||||
<stringProp name="script"><![CDATA[
|
||||
import java.security.KeyFactory
|
||||
import java.security.spec.X509EncodedKeySpec
|
||||
import javax.crypto.Cipher
|
||||
|
||||
def publicKeyText = vars.get('publicKey')
|
||||
if (!publicKeyText || publicKeyText == 'NOT_FOUND') {
|
||||
throw new IllegalStateException('获取RSA公钥失败')
|
||||
}
|
||||
def keyBytes = Base64.decoder.decode(publicKeyText)
|
||||
def publicKey = KeyFactory.getInstance('RSA').generatePublic(new X509EncodedKeySpec(keyBytes))
|
||||
def cipher = Cipher.getInstance('RSA/ECB/PKCS1Padding')
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey)
|
||||
def encrypted = cipher.doFinal(vars.get('testPassword').getBytes('UTF-8'))
|
||||
vars.put('encryptedPassword', Base64.encoder.encodeToString(encrypted))
|
||||
]]></stringProp>
|
||||
</JSR223Sampler>
|
||||
<hashTree/>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="05-人员账号登录" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="登录参数" enabled="true">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="username" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">username</stringProp><stringProp name="Argument.value">${loginname}</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||
</elementProp>
|
||||
<elementProp name="password" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">password</stringProp><stringProp name="Argument.value">${encryptedPassword}</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||
</elementProp>
|
||||
<elementProp name="platformKey" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">platformKey</stringProp><stringProp name="Argument.value">${captchaKey}</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||
</elementProp>
|
||||
<elementProp name="platformCaptcha" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">platformCaptcha</stringProp><stringProp name="Argument.value">${captchaCode}</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||
</elementProp>
|
||||
<elementProp name="keyId" elementType="HTTPArgument">
|
||||
<boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">keyId</stringProp><stringProp name="Argument.value">${keyId}</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp>
|
||||
</elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.path">/platform/login/doLogin</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.postBodyRaw">false</boolProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="断言登录成功" enabled="true">
|
||||
<stringProp name="scriptLanguage">groovy</stringProp>
|
||||
<stringProp name="script"><![CDATA[
|
||||
import groovy.json.JsonSlurper
|
||||
try {
|
||||
def json = new JsonSlurper().parseText(prev.getResponseDataAsString())
|
||||
if (json.code != 0) {
|
||||
prev.setSuccessful(false)
|
||||
prev.setResponseMessage('登录失败:' + json.msg)
|
||||
}
|
||||
} catch (Exception e) {
|
||||
prev.setSuccessful(false)
|
||||
prev.setResponseMessage('登录响应不是有效JSON:' + e.message)
|
||||
}
|
||||
]]></stringProp>
|
||||
</JSR223PostProcessor>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="06-报名资格预校验" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="校验参数" enabled="true">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="courseId" elementType="HTTPArgument"><boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">courseId</stringProp><stringProp name="Argument.value">${courseId}</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp></elementProp>
|
||||
<elementProp name="currentFamilyNumber" elementType="HTTPArgument"><boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">currentFamilyNumber</stringProp><stringProp name="Argument.value">0</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp></elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.path">/platform/trainSignUp/apply/validateSignUp</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.postBodyRaw">false</boolProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree/>
|
||||
<HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="07-集合后并发提交登山活动报名" enabled="true">
|
||||
<elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="报名参数" enabled="true">
|
||||
<collectionProp name="Arguments.arguments">
|
||||
<elementProp name="activityId" elementType="HTTPArgument"><boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">activityId</stringProp><stringProp name="Argument.value">${activityId}</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp></elementProp>
|
||||
<elementProp name="courseId" elementType="HTTPArgument"><boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">courseId</stringProp><stringProp name="Argument.value">${courseId}</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp></elementProp>
|
||||
<elementProp name="mobileColumnsValue" elementType="HTTPArgument"><boolProp name="HTTPArgument.always_encode">true</boolProp><stringProp name="Argument.name">mobileColumnsValue</stringProp><stringProp name="Argument.value">[]</stringProp><stringProp name="Argument.metadata">=</stringProp><boolProp name="HTTPArgument.use_equals">true</boolProp></elementProp>
|
||||
</collectionProp>
|
||||
</elementProp>
|
||||
<stringProp name="HTTPSampler.path">/platform/trainSignUp/apply/doSignUp</stringProp>
|
||||
<stringProp name="HTTPSampler.method">POST</stringProp>
|
||||
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
|
||||
<boolProp name="HTTPSampler.use_keepalive">true</boolProp>
|
||||
<boolProp name="HTTPSampler.postBodyRaw">false</boolProp>
|
||||
</HTTPSamplerProxy>
|
||||
<hashTree>
|
||||
<SyncTimer guiclass="TestBeanGUI" testclass="SyncTimer" testname="等待本线程组全部人员后同时报名" enabled="true">
|
||||
<intProp name="groupSize">0</intProp>
|
||||
<stringProp name="timeoutInMs">${__P(syncTimeoutMs,180000)}</stringProp>
|
||||
</SyncTimer>
|
||||
<hashTree/>
|
||||
<JSR223PostProcessor guiclass="TestBeanGUI" testclass="JSR223PostProcessor" testname="断言报名成功" enabled="true">
|
||||
<stringProp name="scriptLanguage">groovy</stringProp>
|
||||
<stringProp name="script"><![CDATA[
|
||||
import groovy.json.JsonSlurper
|
||||
try {
|
||||
def json = new JsonSlurper().parseText(prev.getResponseDataAsString())
|
||||
if (json.code != 0 || json.msg != '报名成功') {
|
||||
prev.setSuccessful(false)
|
||||
prev.setResponseMessage('报名未成功;expectedAuthorized=' + vars.get('expectedAuthorized') + ';返回:' + json.msg)
|
||||
}
|
||||
} catch (Exception e) {
|
||||
prev.setSuccessful(false)
|
||||
prev.setResponseMessage('报名响应不是有效JSON;expectedAuthorized=' + vars.get('expectedAuthorized') + ';' + e.message)
|
||||
}
|
||||
]]></stringProp>
|
||||
</JSR223PostProcessor>
|
||||
<hashTree/>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</hashTree>
|
||||
</jmeterTestPlan>
|
||||
@@ -0,0 +1,132 @@
|
||||
param(
|
||||
[ValidateRange(1, 1300)]
|
||||
[int]$UserCount = 1300,
|
||||
|
||||
[ValidateSet("AllScope", "Authorized")]
|
||||
[string]$PermissionMode = "AllScope",
|
||||
|
||||
[ValidatePattern("^[0-9a-fA-F]{32}$")]
|
||||
[string]$ActivityId = "11619eee67574256ad8ccc8a16630023",
|
||||
|
||||
[string]$OutputFile = (Join-Path $PSScriptRoot "data\brand-activity-users.csv"),
|
||||
|
||||
[string]$ApplicationConfig = (Join-Path $PSScriptRoot "..\src\main\resources\application.yaml")
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Get-YamlScalar {
|
||||
param(
|
||||
[string[]]$Lines,
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
$matchedLine = $Lines | Where-Object { $_ -match ("^\s*" + [regex]::Escape($Name) + "\s*:\s*(.+?)\s*$") } | Select-Object -First 1
|
||||
if (-not $matchedLine) {
|
||||
throw "未在 application.yaml 中找到配置项:$Name"
|
||||
}
|
||||
return ([regex]::Match($matchedLine, "^\s*[^:]+:\s*(.+?)\s*$")).Groups[1].Value.Trim('"', "'")
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ApplicationConfig)) {
|
||||
throw "找不到应用配置文件:$ApplicationConfig"
|
||||
}
|
||||
|
||||
$mysqlCommand = Get-Command mysql -ErrorAction SilentlyContinue
|
||||
if (-not $mysqlCommand) {
|
||||
throw "未找到 mysql 命令,请先把 MySQL 客户端 bin 目录加入 PATH。"
|
||||
}
|
||||
|
||||
$configLines = Get-Content -LiteralPath $ApplicationConfig -Encoding UTF8
|
||||
$jdbcUrl = Get-YamlScalar -Lines $configLines -Name "url"
|
||||
$dbUser = Get-YamlScalar -Lines $configLines -Name "username"
|
||||
$dbPassword = Get-YamlScalar -Lines $configLines -Name "password"
|
||||
|
||||
$jdbcMatch = [regex]::Match($jdbcUrl, "jdbc:mysql://(?<host>[^:/?]+)(:(?<port>\d+))?/(?<database>[^?]+)")
|
||||
if (-not $jdbcMatch.Success) {
|
||||
throw "无法解析 JDBC 地址:$jdbcUrl"
|
||||
}
|
||||
|
||||
$dbHost = $jdbcMatch.Groups["host"].Value
|
||||
$dbPort = if ($jdbcMatch.Groups["port"].Success) { $jdbcMatch.Groups["port"].Value } else { "3306" }
|
||||
$database = $jdbcMatch.Groups["database"].Value
|
||||
|
||||
# 仅导出活动范围内、账号未禁用且尚未报名的人员;不读取密码、身份证号、手机号等敏感字段。
|
||||
$permissionCondition = if ($PermissionMode -eq "Authorized") {
|
||||
@"
|
||||
AND EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_user_role ur
|
||||
JOIN sys_role_menu rm ON rm.roleId = ur.roleId
|
||||
JOIN sys_menu m ON m.id = rm.menuId
|
||||
WHERE ur.userId = u.id
|
||||
AND m.permission IN ('trainSignUp.apply', 'h5.trainSignUp.apply')
|
||||
)
|
||||
"@
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
$query = @"
|
||||
SELECT
|
||||
u.loginname,
|
||||
CASE WHEN EXISTS (
|
||||
SELECT 1
|
||||
FROM sys_user_role ur
|
||||
JOIN sys_role_menu rm ON rm.roleId = ur.roleId
|
||||
JOIN sys_menu m ON m.id = rm.menuId
|
||||
WHERE ur.userId = u.id
|
||||
AND m.permission IN ('trainSignUp.apply', 'h5.trainSignUp.apply')
|
||||
) THEN 1 ELSE 0 END AS expectedAuthorized
|
||||
FROM train_sign_up_activity a
|
||||
JOIN activity_user_scope aus ON aus.groupId = a.activityGroupId
|
||||
JOIN sys_user u ON u.id = aus.userId
|
||||
LEFT JOIN train_sign_up_user su ON su.activityId = a.id AND su.userId = u.id
|
||||
WHERE a.id = '$ActivityId'
|
||||
AND u.disabled = 0
|
||||
AND u.loginname IS NOT NULL
|
||||
AND u.loginname <> ''
|
||||
AND su.id IS NULL
|
||||
$permissionCondition
|
||||
GROUP BY u.id, u.loginname
|
||||
ORDER BY expectedAuthorized DESC, u.loginname
|
||||
LIMIT $UserCount;
|
||||
"@
|
||||
|
||||
$previousMysqlPassword = $env:MYSQL_PWD
|
||||
try {
|
||||
$env:MYSQL_PWD = $dbPassword
|
||||
$rows = @(& $mysqlCommand.Source --batch --raw --skip-column-names -h $dbHost -P $dbPort -u $dbUser $database -e $query)
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "查询压测人员失败,mysql 退出码:$LASTEXITCODE"
|
||||
}
|
||||
} finally {
|
||||
$env:MYSQL_PWD = $previousMysqlPassword
|
||||
}
|
||||
|
||||
$users = @($rows | Where-Object { $_ -and $_.Trim() } | ForEach-Object {
|
||||
$columns = $_ -split "`t", 2
|
||||
[pscustomobject]@{
|
||||
loginname = $columns[0]
|
||||
expectedAuthorized = $columns[1]
|
||||
}
|
||||
})
|
||||
|
||||
if ($users.Count -lt $UserCount) {
|
||||
throw "符合条件的人员只有 $($users.Count) 人,少于要求的 $UserCount 人。PermissionMode=$PermissionMode"
|
||||
}
|
||||
|
||||
$outputDirectory = Split-Path -Parent $OutputFile
|
||||
if (-not (Test-Path -LiteralPath $outputDirectory)) {
|
||||
New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null
|
||||
}
|
||||
|
||||
$users | Export-Csv -LiteralPath $OutputFile -NoTypeInformation -Encoding UTF8 -UseQuotes Always
|
||||
|
||||
$authorizedCount = @($users | Where-Object { $_.expectedAuthorized -eq "1" }).Count
|
||||
$unauthorizedCount = $users.Count - $authorizedCount
|
||||
Write-Host "已生成 UTF-8 用户文件:$OutputFile"
|
||||
Write-Host "总人数:$($users.Count),具备报名权限:$authorizedCount,无报名权限:$unauthorizedCount"
|
||||
if ($unauthorizedCount -gt 0) {
|
||||
Write-Warning "无报名权限的线程仍会访问报名接口,但会被权限校验拒绝。"
|
||||
}
|
||||
Reference in New Issue
Block a user