前言:
长话短说,继上一篇 接口自动化 -- 基于JMeter的实战攻略(2) ,已完成了 jmeter 脚本模板的搭建,这篇我将继续补充模板中的内容要怎么编写。
补充 jemter 脚本:
1、封装get、delete请求处理
①、http取样器
②、BeanShell后置处理器(处理关联数据)
用于处理接口关联数据,如:登录接口获取 token ,后续接口拿这个 token 发起请求,token 就属于关联数据。
import org.json.JSONObject;
private static String getJsonObjectValue(JSONObject jsonObject,String key) {
String string = jsonObject.toString();
int i = string.indexOf("\""+key+"\":");
String substring = "";
if (i != -1){
//从匹配到的下标往后找:
int i1 = string.indexOf(":", i);
//找到:再往后+2找到value开始的下标,再往后找分号(value为字符串,但不处于结尾)
int i2 = string.indexOf("\",", i1 + 2);
if (i2 == -1){
//value为字符串且是结尾
i2 = string.indexOf("\"}",i1+2);
if (i2 == -1){
//value为数字,且不是结尾
i2 = string.indexOf(",",i1+1);
if (i2 == -1){
//value为数字,且是结尾
i2 = string.indexOf("}",i1+1);
}
substring = string.substring(i1+1, i2);
System.out.println(substring);
return substring;
}
}
substring = string.substring(i1+2, i2);
System.out.println(substring);
return substring;
}
return "";
}
//判断是否有关联数据,有的话需要存储起来,没有则忽略
String associatedData = vars.get("associatedData");
log.info("BeanShell 处理关联数据:开始处理关联数据,associatedData="+associatedData);
if(!"".equals(associatedData) && associatedData != null){
JSONObject associatedObj= new JSONObject(associatedData);
Iterator it = associatedObj.keys();
//关联数据用json格式编写,具体分为两种,1、明确value的,直接填值;2、不明确value的,用#{}标明从响应结果中拿。
while(it.hasNext()){
//获得key
String key = it.next();
String value = String.valueOf(associatedObj.get(key));
//判断是否有#{}占位符,有的话要从响应结果中拿值,没有的话直接存jmeter变量
if(value.length()>2 && "#{".equals(value.substring(0, 2)) && "}".equals(value.substring(value.length() - 1, value.length()))){
log.info("BeanShell 处理关联数据:!!!!!!!符合预期,需要从接口响应结果中取值,并存储为jmeter变量,供后续接口使用!"+value.substring(2,value.length() - 1));
valueKey = value.substring(2,value.length() - 1);
//响应数据
JSONObject associatedObj= new JSONObject(prev.getResponseDataAsString());
value = getJsonObjectValue(associatedObj,valueKey);
log.info("BeanShell 处理关联数据:!!! key="+key+";valueKey="+valueKey+";value="+value+" !!!");
//将key、value存jemter变量
props.put(key,value);
}
}
}
需要注意的是,org.json.JSONObject 这个类需要我们自己找资源jar,放入 jmeter 安装目录的 ext 目录下,否则上述代码会报错缺少依赖。
资源路径:
https://cdn.yiduoyun.space/JMeter%E5%AE%9E%E6%88%98%E7%B3%BB%E5%88%97/ext/json-20220320.jar
③、BeanShell后置处理器(校验响应结果)
import org.json.JSONObject;
import java.util.*;
//比较响应结果的key是否存在于预期结果的key中,若不存在则直接报错,若存在则比较value是否相同,不同也报错。
public static boolean comparisonResults(String expectedResult , String responseResult) {
JSONObject responseObj = new JSONObject(responseResult);
JSONObject expectedResultObj = new JSONObject(expectedResult);
Iterator expectedResultIt = expectedResultObj.keys();
while (expectedResultIt.hasNext()){
String expectedResultKey = (String) expectedResultIt.next();
if (responseObj.has(expectedResultKey)){
String responseValue = String.valueOf(responseObj.get(expectedResultKey));
String expectedValue = String.valueOf(expectedResultObj.get(expectedResultKey));
if (!responseValue.equals(expectedValue)){
return false;
}
}else {
return false;
}
}
return true;
}
String expectedResult = vars.get("expectedResults");
String responseResult = prev.getResponseDataAsString();
props.put("responseResult",responseResult);
log.info("------------------------ get校验 start "+ vars.get("interfaceNo") + "_" + vars.get("caseNo") +" ------------------------");
log.info("BeanShell 校验responseBody:expectedResult: "+expectedResult);
log.info("BeanShell 校验responseBody:responseResult: "+responseResult);
boolean flag = comparisonResults(expectedResult,responseResult);
log.info("BeanShell 校验responseBody:第一步的测试结果:"+flag);
props.put("flag",flag);
//如果预期结果与响应结果对比不符,用例执行完毕
if(props.get("flag") == false)
{
log.info("BeanShell 校验responseBody:第一步:用例执行完毕,测试不通过,testCaseStatus为end!");
//修改testCaseStatus状态为end就表示结束
vars.put("testCaseStatus","end");
}
//如果不需要校验sql,并且预期结果与响应结果的内容匹配上了,也表示用例执行完毕
if("${ifVerifySql}" == "FALSE" && props.get("flag") == true)
{
log.info("BeanShell 校验responseBody:第一步:用例执行完毕,测试通过,不需要校验sql,testCaseStatus为end!");
//修改testCaseStatus状态为end就表示结束
vars.put("testCaseStatus","end");
}
④、BeanShell 断言(断言处理)
flag == flase 时,会标红察看结果树中的用例
//如果预期结果与响应结果对比判定为失败,就断言标明一下。
if(props.get("flag") == false){
Failure=true; // 表示断言失败
FailureMessage="响应结果与预期结果不符!";
}
2、封装post、put请求处理
(1)、HTTP信息头管理器
此处新建 HTTP 信息头管理器的目的,是为了后续在BeanShell中获取HeaderManager类,并通过他来设置头部请求参数。
(2)、处理json-data请求
${__groovy(vars.get("headers").contains("application/json"),)}
①、BeanShell预处理程序(添加头部参数)
import org.apache.jmeter.protocol.http.control.HeaderManager;
import org.apache.jmeter.testelement.property.CollectionProperty;
import org.apache.jmeter.protocol.http.sampler;
import org.apache.jmeter.protocol.http.control.Header;
import org.json.JSONObject;
log.info("-------------------------- POST json-data start " + vars.get("interfaceNo") + "_" + vars.get("caseNo") + " ----------------------------");
log.info("-------------------------- 添加头部参数:开始设置头部参数 --------------------------------");
//sampler 里面有个getHeaderManager 可以获得请求头
HeaderManager hm =sampler.getHeaderManager();
//打印出header信息
CollectionProperty cp=hm.getHeaders();
//将header 转为jsonobject
//String headers = "{\"Content-Type\": \"application/json\"}";
String headers = vars.get("headers");
log.info("添加头部参数:headers=="+headers);
if(!"".equals(headers) && headers != null){
JSONObject headerObj= new JSONObject(headers);
Iterator it = headerObj.keys();
//遍历出json对象中的key、value,并设置进hm中
while(it.hasNext()){
//获得key
String key = it.next();
String value = headerObj.getString(key);
Header hd = new Header();
hd.setName(key);
hd.setValue(value);
//如果value字符串开头是#{ 结尾是} ,那么就认为他是个变量,要取jmeter变量
if(value.length()>2 && "#{".equals(value.substring(0, 2)) && "}".equals(value.substring(value.length() - 1, value.length()))){
log.info("添加头部参数:!!!!!!!!!!!!!!符合预期,需要获取jmeter变量!");
String param = value.substring(2,value.length() - 1);
value = props.get(param);
}
hm.add(hd);
log.info("添加头部参数:key=="+key+" "+"value=="+value);
}
}
②、BeanShell预处理程序(设置body请求参数)
import org.json.JSONObject;
log.info("body请求参数:json-data进来了");
String body = vars.get("testData");
log.info("body请求参数:body="+body);
//判断body中开头是#{ 结尾是} ,那么就认为他是个变量,要取jmeter变量
if(body != null && !"".equals(body)){
JSONObject associatedObj= new JSONObject(body);
Iterator it = associatedObj.keys();
//关联数据用json格式编写,具体分为两种,1、明确value的,直接填值;2、不明确value的,用#{}标明从响应结果中拿。
while(it.hasNext()){
//获得key
String key = it.next();
String value = String.valueOf(associatedObj.get(key));
log.info("body请求参数:key = "+key+";value = "+value);
//判断是否有#{}占位符,有的话要从响应结果中拿值,没有的话直接存jmeter变量
if(value.length()>2 && "#{".equals(value.substring(0, 2)) && "}".equals(value.substring(value.length() - 1, value.length()))){
String valueKey = value.substring(2,value.length() - 1);
String value2 = props.get(valueKey);
//替换掉字符串的#{param}为真实值
body = body.replace(value, value2);
log.info("body请求参数:修改后的body=="+body);
}
}
}
log.info("body请求参数:最终修改后的body=="+body);
vars.put("body",body);
③、http取样器
④、BeanShell 处理关联数据(处理关联数据)
import org.json.JSONObject;
private static String getJsonObjectValue(JSONObject jsonObject,String key) {
String string = jsonObject.toString();
int i = string.indexOf("\""+key+"\":");
String substring = "";
if (i != -1){
//从匹配到的下标往后找:
int i1 = string.indexOf(":", i);
//找到:再往后+2找到value开始的下标,再往后找分号(value为字符串,但不处于结尾)
int i2 = string.indexOf("\",", i1 + 2);
if (i2 == -1){
//value为字符串且是结尾
i2 = string.indexOf("\"}",i1+2);
if (i2 == -1){
//value为数字,且不是结尾
i2 = string.indexOf(",",i1+1);
if (i2 == -1){
//value为数字,且是结尾
i2 = string.indexOf("}",i1+1);
}
substring = string.substring(i1+1, i2);
System.out.println(substring);
return substring;
}
}
substring = string.substring(i1+2, i2);
System.out.println(substring);
return substring;
}
return "";
}
//判断是否有关联数据,有的话需要存储起来,没有则忽略
String associatedData = vars.get("associatedData");
log.info("BeanShell 处理关联数据:开始处理关联数据,associatedData="+associatedData);
if(!"".equals(associatedData) && associatedData != null){
JSONObject associatedObj= new JSONObject(associatedData);
Iterator it = associatedObj.keys();
//关联数据用json格式编写,具体分为两种,1、明确value的,直接填值;2、不明确value的,用#{}标明从响应结果中拿。
while(it.hasNext()){
//获得key
String key = it.next();
String value = String.valueOf(associatedObj.get(key));
log.info("BeanShell 处理关联数据:key = "+key+";value = "+value);
//判断是否有#{}占位符,有的话要从响应结果中拿值,没有的话直接存jmeter变量
if(value.length()>2 && "#{".equals(value.substring(0, 2)) && "}".equals(value.substring(value.length() - 1, value.length()))){
String valueKey = value.substring(2,value.length() - 1);
//响应数据转换为jsonObject对象,再通过key从中获取value
JSONObject associatedObj= new JSONObject(prev.getResponseDataAsString());
value = getJsonObjectValue(associatedObj,valueKey);
log.info("BeanShell 处理关联数据:!!! key="+key+";valueKey="+valueKey+";value="+value);
//将key、value存jemter变量,需要跨线程,所以使用props
props.put(key,value);
}else{
//没有#{}表示,value明确,直接存jmeter变量即可
props.put(key,value);
}
}
}
⑤、BeanShell 后置处理器(校验响应结果)
import org.json.JSONObject;
import java.util.*;
//比较响应结果的key是否存在于预期结果的key中,若不存在则直接报错,若存在则比较value是否相同,不同也报错。
public static boolean comparisonResults(String expectedResult , String responseResult) {
JSONObject responseObj = new JSONObject(responseResult);
JSONObject expectedResultObj = new JSONObject(expectedResult);
Iterator expectedResultIt = expectedResultObj.keys();
while (expectedResultIt.hasNext()){
String expectedResultKey = (String) expectedResultIt.next();
if (responseObj.has(expectedResultKey)){
String responseValue = String.valueOf(responseObj.get(expectedResultKey));
String expectedValue = String.valueOf(expectedResultObj.get(expectedResultKey));
if (!responseValue.equals(expectedValue)){
return false;
}
}else {
return false;
}
}
return true;
}
String expectedResult = vars.get("expectedResults");
String responseResult = prev.getResponseDataAsString();
props.put("responseResult",responseResult);
log.info("------------------------ post校验 start "+ vars.get("interfaceNo") + "_" + vars.get("caseNo") +" ------------------------");
log.info("ponseBody:expectedResult: "+expectedResult);
log.info("ponseBody:responseResult: "+responseResult);
boolean flag = comparisonResults(expectedResult,responseResult);
log.info("BeanShell 校验responseBody:第一步的测试结果:"+flag);
props.put("flag",flag);
//如果响应结果对比不符 或 不需要校验sql,表示用例执行完毕
if(props.get("flag") == false)
{
log.info("BeanShell 校验responseBody:第一步:用例执行完毕,testCaseStatus为end!");
//修改testCaseStatus状态为end就表示结束
vars.put("testCaseStatus","end");
}
//如果不需要校验sql,并且预期结果与响应结果的内容匹配上了,也表示用例执行完毕
if("${ifVerifySql}" == "FALSE" && props.get("flag") == true)
{
log.info("BeanShell 校验responseBody:第一步:用例执行完毕,testCaseStatus为end!");
//修改testCaseStatus状态为end就表示结束
vars.put("testCaseStatus","end");
}
⑥、BeanShell断言(断言处理)
flag == flase 时,会标红察看结果树中的用例
//如果预期结果与响应结果对比判定为失败,就断言标明一下。
if(props.get("flag") == false){
Failure=true; // 表示断言失败
FailureMessage="响应结果与预期结果不符!";
}
(3)、处理from-data请求
与json-data请求处理起来类似,区别可以说仅在body请求体。
①、BeanShell预处理程序(添加头部参数)
import org.apache.jmeter.protocol.http.control.HeaderManager;
import org.apache.jmeter.testelement.property.CollectionProperty;
import org.apache.jmeter.protocol.http.sampler;
import org.apache.jmeter.protocol.http.control.Header;
import org.json.JSONObject;
log.info("-------------------------- POST from-data start " + vars.get("interfaceNo") + "_" + vars.get("caseNo") + " ----------------------------");
log.info("-------------------------- 添加头部参数:开始设置头部参数 --------------------------------");
//sampler 里面有个getHeaderManager 可以获得请求头
HeaderManager hm =sampler.getHeaderManager();
//打印出header信息
CollectionProperty cp=hm.getHeaders();
//将header 转为jsonobject
//String headers = "{\"Content-Type\": \"application/x-www-form-urlencoded\"}";
String headers = vars.get("headers");
log.info("添加头部参数:headers=="+headers);
if(!"".equals(headers) && headers != null){
JSONObject headerObj= new JSONObject(headers);
Iterator it = headerObj.keys();
//遍历出json对象中的key、value,并设置进hm中
while(it.hasNext()){
//获得key
String key = it.next();
String value = headerObj.getString(key);
log.info("添加头部参数:=========== "+key+" "+value);
Header hd = new Header();
hd.setName(key);
//如果value字符串开头是${ 结尾是} ,那么就认为他是个变量,要取jmeter变量
if(value.length()>2 && "#{".equals(value.substring(0, 2)) && "}".equals(value.substring(value.length() - 1, value.length()))){
log.info("添加头部参数:!!!!!!!!!!!!!!符合预期,需要获取jmeter变量!");
String param = value.substring(2,value.length() - 1);
value = props.get(param);
}
hd.setValue(value);
hm.add(hd);
log.info("添加头部参数:key=="+key+" "+"value=="+value);
}
}
②、BeanShell预处理程序(body请求参数)
其中 args.addArgument(new HTTPArgument(requestObjKey, requestObjValue)); 就是关键,将读取到的参数存入body请求体中。
import org.json.JSONObject;
import java.util.*;
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.protocol.http.util.HTTPArgument;
log.info("body请求参数:form-data进来了");
// 获取请求参数对象,然后从json字符串中提取参数发送form-data。
Arguments args = sampler.getArguments();
// 清空之前所有的请求参数
args.clear();
JSONObject requestObj = new JSONObject(vars.get("testData"));
Iterator requestObjIt = requestObj.keys();
while (requestObjIt.hasNext()){
String requestObjKey = (String) requestObjIt.next();
String requestObjValue = String.valueOf(requestObj.get(requestObjKey));
//如果requestObjValue中开头是#{ 结尾是} ,那么就认为他是个变量,要取jmeter变量
if(requestObjValue.length()>2 && "#{".equals(requestObjValue.substring(0, 2)) && "}".equals(requestObjValue.substring(requestObjValue.length() - 1, requestObjValue.length()))){
String param = requestObjValue.substring(2,requestObjValue.length() - 1);
requestObjValue = props.get(param);
}
//**将请求参数放进body
args.addArgument(new HTTPArgument(requestObjKey, requestObjValue));
}
log.info("body请求参数:body:"+args.toString());
③、http取样器
④、BeanShell 处理关联数据(处理关联数据)
import org.json.JSONObject;
private static String getJsonObjectValue(JSONObject jsonObject,String key) {
String string = jsonObject.toString();
int i = string.indexOf("\""+key+"\":");
String substring = "";
if (i != -1){
//从匹配到的下标往后找:
int i1 = string.indexOf(":", i);
//找到:再往后+2找到value开始的下标,再往后找分号(value为字符串,但不处于结尾)
int i2 = string.indexOf("\",", i1 + 2);
if (i2 == -1){
//value为字符串且是结尾
i2 = string.indexOf("\"}",i1+2);
if (i2 == -1){
//value为数字,且不是结尾
i2 = string.indexOf(",",i1+1);
if (i2 == -1){
//value为数字,且是结尾
i2 = string.indexOf("}",i1+1);
}
substring = string.substring(i1+1, i2);
System.out.println(substring);
return substring;
}
}
substring = string.substring(i1+2, i2);
System.out.println(substring);
return substring;
}
return "";
}
//判断是否有关联数据,有的话需要存储起来,没有则忽略
String associatedData = vars.get("associatedData");
log.info("BeanShell 处理关联数据:开始处理关联数据,associatedData="+associatedData);
if(!"".equals(associatedData) && associatedData != null && associatedData.length()>2){
JSONObject associatedObj= new JSONObject(associatedData);
Iterator it = associatedObj.keys();
//关联数据用json格式编写,具体分为两种,1、明确value的,直接填值;2、不明确value的,用#{}标明从响应结果中拿。
while(it.hasNext()){
//获得key
String key = it.next();
String value = String.valueOf(associatedObj.get(key));
//判断是否有#{}占位符,有的话要从响应结果中拿值,没有的话直接存jmeter变量
if(value.length()>2 && "#{".equals(value.substring(0, 2)) && "}".equals(value.substring(value.length() - 1, value.length()))){
log.info("BeanShell 处理关联数据:!!!!!!!符合预期,需要从接口响应结果中取值,并存储为jmeter变量,供后续接口使用!"+value.substring(2,value.length() - 1));
valueKey = value.substring(2,value.length() - 1);
//响应数据
JSONObject associatedObj= new JSONObject(prev.getResponseDataAsString());
value = getJsonObjectValue(associatedObj,valueKey);
log.info("BeanShell 处理关联数据:!!! key="+key+";valueKey="+valueKey+";value="+value+" !!!");
//将key、value存jemter变量
props.put(key,value);
}
}
}
⑤、BeanShell 后置处理器(校验响应结果)
import org.json.JSONObject;
import java.util.*;
//比较响应结果与预期结果
public static boolean comparisonResults(String expectedResult , String responseResult) {
JSONObject responseObj = new JSONObject(responseResult);
JSONObject expectedResultObj = new JSONObject(expectedResult);
Iterator expectedResultIt = expectedResultObj.keys();
while (expectedResultIt.hasNext()){
String expectedResultKey = (String) expectedResultIt.next();
//比较响应结果的key是否存在于预期结果的key中,若不存在则直接报错,若存在则比较value是否相同,不同也报错。
if (responseObj.has(expectedResultKey)){
String responseValue = String.valueOf(responseObj.get(expectedResultKey));
String expectedValue = String.valueOf(expectedResultObj.get(expectedResultKey));
if (!responseValue.equals(expectedValue)){
return false;
}
}else {
return false;
}
}
return true;
}
String expectedResult = vars.get("expectedResults");
String responseResult = prev.getResponseDataAsString();
props.put("responseResult",responseResult);
log.info("------------------------ post校验 start "+ vars.get("interfaceNo") + "_" + vars.get("caseNo") +" ------------------------");
log.info("BeanShell 校验responeseBody:expectedResult: "+expectedResult);
log.info("BeanShell 校验responeseBody:responseResult: "+responseResult);
boolean flag = comparisonResults(expectedResult,responseResult);
log.info("BeanShell 校验responeseBody:第一步的测试结果:"+flag);
props.put("flag",flag);
//如果响应结果对比不符 或 不需要校验sql,表示用例执行完毕
if(props.get("flag") == false)
{
log.info("BeanShell 校验responeseBody:第一步:用例执行完毕,testCaseStatus为end!");
//修改testCaseStatus状态为end就表示结束
vars.put("testCaseStatus","end");
}
//如果不需要校验sql,并且预期结果与响应结果的内容匹配上了,也表示用例执行完毕
if("${ifVerifySql}" == "FALSE" && props.get("flag") == true)
{
log.info("BeanShell 校验responseBody:第一步:用例执行完毕,testCaseStatus为end!");
//修改testCaseStatus状态为end就表示结束
vars.put("testCaseStatus","end");
}
⑥、BeanShell断言(断言处理)
flag == flase 时,会标红察看结果树中的用例
//如果预期结果与响应结果对比判定为失败,就断言标明一下。
if(props.get("flag") == false){
Failure=true; // 表示断言失败
FailureMessage="响应结果与预期结果不符!";
}
3、SQL校验
大部分接口测试,严谨的说,都不能仅从响应结果来判断接口是否正常,大多数情况我们都需要去数据库再做一次验证,所以诞生了这个模块。
${__jexl3(props.get("flag") == true && "${ifVerifySql}" == "TRUE",)}
(1)JDBC 请求
(2)BeanShell 后置处理程序 (校验SQL结果)
因为我们再接口测试用例文档中的预期结果写的是表达式,无法直接套用到java代码中使用,所以我们要引入 org.mvel2.MVEL 依赖来处理。
资源通过下方链接下载后,放入 jmeter 安装路径下的 lib/ext 目录下即可。
mvel2 jar包资源:
https://cdn.yiduoyun.space/JMeter%E5%AE%9E%E6%88%98%E7%B3%BB%E5%88%97/ext/mvel2-2.4.14.Final.jar
import org.mvel2.MVEL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.HashMap;
import java.util.Map;
log.info("BeanShell 校验SQL结果:SQL断言执行,dbData =====" + vars.get("dbData"));
log.info("BeanShell 校验SQL结果:vars.get(\"expectedSqlResults\")==" + vars.get("expectedSqlResults"));
String parent = vars.get("expectedSqlResults");
//正则匹配#{}
String child = "(#\\{)(.*?)(\\})";
Pattern p = Pattern.compile(child);
Matcher m = p.matcher(parent);
Map maps = new HashMap();
while (m.find()) {
String paramName = m.group(2);
String paramValue = String.valueOf(vars.getObject("dbData").get(0).get(paramName));
log.info("BeanShell 校验SQL结果:paramName="+paramName+" paramValue="+paramValue);
maps.put(paramName,paramValue);
//将条件表达式中的#{}去掉
parent = parent.replace(m.group(), paramName);
}
log.info("BeanShell 校验SQL结果:parent表达式最终效果为: "+parent);
log.info("BeanShell 校验SQL结果:"+maps.toString());
//判断表达式
boolean flag = (Boolean) MVEL.eval(parent,maps);
if(flag){
log.info("表达式成立!!!");
}else{
log.info("表达式不成立~~~");
}
props.put("flag",flag);
log.info("BeanShell 校验SQL结果:第二步:用例执行完毕,testCaseStatus为end!");
vars.put("testCaseStatus","end");
(3)、BeanShell断言(断言处理)
flag == flase 时,会标红察看结果树中的用例
//如果预期结果与响应结果对比判定为失败,就断言标明一下。
if(props.get("flag") == false){
Failure=true; // 表示断言失败
FailureMessage="响应结果与预期结果不符!";
}
总结
至此,读取文档数据并执行的脚本就已经算是完成了。我想说,其实这还不算完善,像缺少jar包依赖,某段语句报错,很可能会直接导致后续代码逻辑不执行,这种情况是不会显示在“察看结果树”里的,我们只能从执行日志中排查,这很不人性化,或许这就是beanshell的毛病吧。
评论区