提交 68ad0c54 编写于 作者: C chenjianxing

Merge branch 'dev' of https://github.com/fit2cloudrd/metersphere-server into dev

......@@ -8,12 +8,8 @@ import io.metersphere.commons.utils.PageUtils;
import io.metersphere.commons.utils.Pager;
import io.metersphere.controller.request.ReportRequest;
import io.metersphere.dto.ReportDTO;
import io.metersphere.report.base.ChartsData;
import io.metersphere.report.base.Errors;
import io.metersphere.report.base.ReportTimeInfo;
import io.metersphere.report.base.TestOverview;
import io.metersphere.report.base.*;
import io.metersphere.report.dto.ErrorsTop5DTO;
import io.metersphere.report.dto.RequestStatisticsDTO;
import io.metersphere.service.ReportService;
import io.metersphere.user.SessionUtils;
import org.apache.shiro.authz.annotation.Logical;
......@@ -59,7 +55,7 @@ public class PerformanceReportController {
}
@GetMapping("/content/{reportId}")
public RequestStatisticsDTO getReportContent(@PathVariable String reportId) {
public List<Statistics> getReportContent(@PathVariable String reportId) {
return reportService.getReport(reportId);
}
......
......@@ -21,6 +21,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping(value = "performance")
......@@ -88,6 +89,11 @@ public class PerformanceTestController {
performanceTestService.run(request);
}
@GetMapping("/log/{testId}")
public Map<String, String> stop(@PathVariable String testId) {
return performanceTestService.log(testId);
}
@GetMapping("/file/metadata/{testId}")
public List<FileMetadata> getFileMetadata(@PathVariable String testId) {
return fileService.getFileMetadataByTestId(testId);
......
......@@ -10,7 +10,6 @@ import io.metersphere.dto.NodeDTO;
import io.metersphere.engine.AbstractEngine;
import io.metersphere.engine.EngineContext;
import io.metersphere.engine.EngineFactory;
import io.metersphere.engine.docker.request.BaseRequest;
import io.metersphere.engine.docker.request.TestRequest;
import io.metersphere.i18n.Translator;
import org.springframework.web.client.RestTemplate;
......@@ -91,14 +90,13 @@ public class DockerTestEngine extends AbstractEngine {
public void stop() {
// TODO 停止运行测试
String testId = loadTest.getId();
BaseRequest request = new BaseRequest();
this.resourceList.forEach(r -> {
NodeDTO node = JSON.parseObject(r.getConfiguration(), NodeDTO.class);
String ip = node.getIp();
Integer port = node.getPort();
String uri = String.format(BASE_URL + "/jmeter/container/stop/" + testId, ip, port);
restTemplate.postForObject(uri, request, String.class);
restTemplate.getForObject(uri, String.class);
});
}
......@@ -106,14 +104,13 @@ public class DockerTestEngine extends AbstractEngine {
public Map<String, String> log() {
String testId = loadTest.getId();
Map<String, String> logs = new HashMap<>();
BaseRequest request = new BaseRequest();
this.resourceList.forEach(r -> {
NodeDTO node = JSON.parseObject(r.getConfiguration(), NodeDTO.class);
String ip = node.getIp();
Integer port = node.getPort();
String uri = String.format(BASE_URL + "/jmeter/container/log/" + testId, ip, port);
String log = restTemplate.postForObject(uri, request, String.class);
String log = restTemplate.getForObject(uri, String.class);
logs.put(node.getIp(), log);
});
return logs;
......
......@@ -5,9 +5,9 @@ import com.opencsv.bean.CsvToBeanBuilder;
import com.opencsv.bean.HeaderColumnNameMappingStrategy;
import io.metersphere.report.base.*;
import io.metersphere.report.dto.ErrorsTop5DTO;
import io.metersphere.report.dto.RequestStatisticsDTO;
import io.metersphere.report.parse.ResultDataParse;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.report.processor.StatisticsSummaryConsumer;
import org.apache.jmeter.report.processor.graph.impl.ActiveThreadsGraphConsumer;
import org.apache.jmeter.report.processor.graph.impl.HitsPerSecondGraphConsumer;
import org.apache.jmeter.report.processor.graph.impl.ResponseTimeOverTimeGraphConsumer;
......@@ -47,132 +47,6 @@ public class GenerateReport {
return null;
}
public static RequestStatisticsDTO getRequestStatistics(String jtlString) {
List<Integer> allElapseTimeList = new ArrayList<>();
List<RequestStatistics> requestStatisticsList = new ArrayList<>();
DecimalFormat decimalFormat = new DecimalFormat("0.00");
List<Metric> totalMetricList = resolver(jtlString);
Map<String, List<Metric>> jtlLabelMap = totalMetricList.stream().collect(Collectors.groupingBy(Metric::getLabel));
Iterator<Map.Entry<String, List<Metric>>> iterator = jtlLabelMap.entrySet().iterator();
int totalElapsedTime = 0;
float totalBytes = 0f;
while (iterator.hasNext()) {
Map.Entry<String, List<Metric>> entry = iterator.next();
String label = entry.getKey();
List<Metric> metricList = entry.getValue();
List<Integer> elapsedList = new ArrayList<>();
int jtlSamplesSize = 0, oneLineElapsedTime = 0, failSize = 0;
float oneLineBytes = 0f;
for (int i = 0; i < metricList.size(); i++) {
try {
Metric row = metricList.get(i);
String elapsed = row.getElapsed();
oneLineElapsedTime += Integer.parseInt(elapsed);
totalElapsedTime += Integer.parseInt(elapsed);
elapsedList.add(Integer.valueOf(elapsed));
allElapseTimeList.add(Integer.valueOf(elapsed));
String isSuccess = row.getSuccess();
if (!"true".equals(isSuccess)) {
failSize++;
}
String bytes = row.getBytes();
oneLineBytes += Float.parseFloat(bytes);
totalBytes += Float.parseFloat(bytes);
jtlSamplesSize++;
} catch (Exception e) {
System.out.println("exception i:" + i);
}
}
Collections.sort(elapsedList);
int tp90 = elapsedList.size() * 90 / 100;
int tp95 = elapsedList.size() * 95 / 100;
int tp99 = elapsedList.size() * 99 / 100;
metricList.sort(Comparator.comparing(t0 -> Long.valueOf(t0.getTimestamp())));
long time = Long.parseLong(metricList.get(metricList.size() - 1).getTimestamp()) - Long.parseLong(metricList.get(0).getTimestamp())
+ Long.parseLong(metricList.get(metricList.size() - 1).getElapsed());
RequestStatistics requestStatistics = new RequestStatistics();
requestStatistics.setRequestLabel(label);
requestStatistics.setSamples(jtlSamplesSize);
String average = decimalFormat.format((float) oneLineElapsedTime / jtlSamplesSize);
requestStatistics.setAverage(average);
/*
* TP90的计算
* 1,把一段时间内全部的请求的响应时间,从小到大排序,获得序列A
* 2,总的请求数量,乘以90%,获得90%对应的请求个数C
* 3,从序列A中找到第C个请求,它的响应时间,即为TP90的值
* 其余相似的指标还有TP95, TP99
*/
// todo tp90
requestStatistics.setTp90(elapsedList.get(tp90) + "");
requestStatistics.setTp95(elapsedList.get(tp95) + "");
requestStatistics.setTp99(elapsedList.get(tp99) + "");
double avgHits = (double) metricList.size() / (time * 1.0 / 1000);
requestStatistics.setAvgHits(decimalFormat.format(avgHits));
requestStatistics.setMin(elapsedList.get(0) + "");
requestStatistics.setMax(elapsedList.get(jtlSamplesSize - 1) + "");
requestStatistics.setErrors(decimalFormat.format(failSize * 100.0 / jtlSamplesSize) + "%");
requestStatistics.setKo(failSize);
/*
* 所有的相同请求的bytes总和 / 1024 / 请求持续运行的时间=sum(bytes)/1024/total time
* total time = 最大时间戳 - 最小时间戳 + 最后请求的响应时间
*/
requestStatistics.setKbPerSec(decimalFormat.format(oneLineBytes * 1.0 / 1024 / (time * 1.0 / 1000)));
requestStatisticsList.add(requestStatistics);
}
Collections.sort(allElapseTimeList);
int totalTP90 = allElapseTimeList.size() * 90 / 100;
int totalTP95 = allElapseTimeList.size() * 95 / 100;
int totalTP99 = allElapseTimeList.size() * 99 / 100;
Integer min = allElapseTimeList.get(0);
Integer max = allElapseTimeList.get(allElapseTimeList.size() - 1);
int allSamples = requestStatisticsList.stream().mapToInt(RequestStatistics::getSamples).sum();
int failSize = requestStatisticsList.stream().mapToInt(RequestStatistics::getKo).sum();
double errors = (double) failSize / allSamples * 100;
String totalErrors = decimalFormat.format(errors);
double average = (double) totalElapsedTime / allSamples;
String totalAverage = decimalFormat.format(average);
RequestStatisticsDTO statisticsDTO = new RequestStatisticsDTO();
statisticsDTO.setRequestStatisticsList(requestStatisticsList);
statisticsDTO.setTotalLabel("Total");
statisticsDTO.setTotalSamples(String.valueOf(allSamples));
statisticsDTO.setTotalErrors(totalErrors + "%");
statisticsDTO.setTotalAverage(totalAverage);
statisticsDTO.setTotalMin(String.valueOf(min));
statisticsDTO.setTotalMax(String.valueOf(max));
statisticsDTO.setTotalTP90(String.valueOf(allElapseTimeList.get(totalTP90)));
statisticsDTO.setTotalTP95(String.valueOf(allElapseTimeList.get(totalTP95)));
statisticsDTO.setTotalTP99(String.valueOf(allElapseTimeList.get(totalTP99)));
totalMetricList.sort(Comparator.comparing(t0 -> Long.valueOf(t0.getTimestamp())));
long ms = Long.parseLong(totalMetricList.get(totalMetricList.size() - 1).getTimestamp()) - Long.parseLong(totalMetricList.get(0).getTimestamp())
+ Long.parseLong(totalMetricList.get(totalMetricList.size() - 1).getElapsed());
double avgThroughput = (double) totalMetricList.size() / (ms * 1.0 / 1000);
statisticsDTO.setTotalAvgHits(decimalFormat.format(avgThroughput));
statisticsDTO.setTotalAvgBandwidth(decimalFormat.format(totalBytes * 1.0 / 1024 / (ms * 1.0 / 1000)));
return statisticsDTO;
}
public static List<Errors> getErrorsList(String jtlString) {
List<Metric> totalMetricList = resolver(jtlString);
......@@ -205,6 +79,11 @@ public class GenerateReport {
return errorsList;
}
public static List<Statistics> getRequestStatistics(String jtlString) {
Map<String, Object> statisticsDataMap = ResultDataParse.getSummryDataMap(jtlString, new StatisticsSummaryConsumer());
return ResultDataParse.summaryMapParsing(statisticsDataMap);
}
private static String getResponseCodeAndFailureMessage(Metric metric) {
return metric.getResponseCode() + "/" + metric.getResponseMessage();
}
......
package io.metersphere.report.base;
public class Statistics {
private String label;
private String samples;
private String ko;
private String error;
private String average;
private String min;
private String max;
private String tp90;
private String tp95;
private String tp99;
private String transactions;
private String received;
private String sent;
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getSamples() {
return samples;
}
public void setSamples(String samples) {
this.samples = samples;
}
public String getKo() {
return ko;
}
public void setKo(String ko) {
this.ko = ko;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public String getAverage() {
return average;
}
public void setAverage(String average) {
this.average = average;
}
public String getMin() {
return min;
}
public void setMin(String min) {
this.min = min;
}
public String getMax() {
return max;
}
public void setMax(String max) {
this.max = max;
}
public String getTp90() {
return tp90;
}
public void setTp90(String tp90) {
this.tp90 = tp90;
}
public String getTp95() {
return tp95;
}
public void setTp95(String tp95) {
this.tp95 = tp95;
}
public String getTp99() {
return tp99;
}
public void setTp99(String tp99) {
this.tp99 = tp99;
}
public String getTransactions() {
return transactions;
}
public void setTransactions(String transactions) {
this.transactions = transactions;
}
public String getSent() {
return sent;
}
public void setSent(String sent) {
this.sent = sent;
}
public String getReceived() {
return received;
}
public void setReceived(String received) {
this.received = received;
}
}
package io.metersphere.report.base;
import java.util.List;
public class SummaryData {
private List<Object> result;
public List<Object> getResult() {
return result;
}
public void setResult(List<Object> result) {
this.result = result;
}
}
package io.metersphere.report.dto;
import io.metersphere.report.base.RequestStatistics;
import java.util.List;
public class RequestStatisticsDTO extends RequestStatistics {
private List<RequestStatistics> requestStatisticsList;
private String totalLabel;
private String totalSamples;
private String totalErrors;
private String totalAverage;
private String totalMin;
private String totalMax;
private String totalTP90;
private String totalTP95;
private String totalTP99;
private String totalAvgBandwidth;
private String totalAvgHits;
public List<RequestStatistics> getRequestStatisticsList() {
return requestStatisticsList;
}
public void setRequestStatisticsList(List<RequestStatistics> requestStatisticsList) {
this.requestStatisticsList = requestStatisticsList;
}
public String getTotalLabel() {
return totalLabel;
}
public void setTotalLabel(String totalLabel) {
this.totalLabel = totalLabel;
}
public String getTotalSamples() {
return totalSamples;
}
public void setTotalSamples(String totalSamples) {
this.totalSamples = totalSamples;
}
public String getTotalErrors() {
return totalErrors;
}
public void setTotalErrors(String totalErrors) {
this.totalErrors = totalErrors;
}
public String getTotalAverage() {
return totalAverage;
}
public void setTotalAverage(String totalAverage) {
this.totalAverage = totalAverage;
}
public String getTotalMin() {
return totalMin;
}
public void setTotalMin(String totalMin) {
this.totalMin = totalMin;
}
public String getTotalMax() {
return totalMax;
}
public void setTotalMax(String totalMax) {
this.totalMax = totalMax;
}
public String getTotalTP90() {
return totalTP90;
}
public void setTotalTP90(String totalTP90) {
this.totalTP90 = totalTP90;
}
public String getTotalTP95() {
return totalTP95;
}
public void setTotalTP95(String totalTP95) {
this.totalTP95 = totalTP95;
}
public String getTotalTP99() {
return totalTP99;
}
public void setTotalTP99(String totalTP99) {
this.totalTP99 = totalTP99;
}
public String getTotalAvgBandwidth() {
return totalAvgBandwidth;
}
public void setTotalAvgBandwidth(String totalAvgBandwidth) {
this.totalAvgBandwidth = totalAvgBandwidth;
}
public String getTotalAvgHits() {
return totalAvgHits;
}
public void setTotalAvgHits(String totalAvgHits) {
this.totalAvgHits = totalAvgHits;
}
}
package io.metersphere.report.parse;
import io.metersphere.commons.utils.BeanUtils;
import io.metersphere.commons.utils.MsJMeterUtils;
import io.metersphere.report.base.ChartsData;
import io.metersphere.report.base.Statistics;
import io.metersphere.report.base.SummaryData;
import org.apache.jmeter.report.core.Sample;
import org.apache.jmeter.report.core.SampleMetadata;
import org.apache.jmeter.report.dashboard.JsonizerVisitor;
import org.apache.jmeter.report.processor.*;
import org.apache.jmeter.report.processor.graph.AbstractOverTimeGraphConsumer;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
......@@ -14,16 +19,43 @@ import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.*;
public class ResultDataParse {
private static final String DATE_TIME_PATTERN = "yyyy/MM/dd HH:mm:ss";
private static final String TIME_PATTERN = "HH:mm:ss";
public static List<Statistics> summaryMapParsing(Map<String, Object> map) {
List<Statistics> statisticsList = new ArrayList<>();
for (String key : map.keySet()) {
MapResultData mapResultData = (MapResultData) map.get(key);
ListResultData items = (ListResultData) mapResultData.getResult("items");
if (items.getSize() > 0) {
for (int i = 0; i < items.getSize(); i++) {
MapResultData resultData = (MapResultData) items.get(i);
ListResultData data = (ListResultData) resultData.getResult("data");
int size = data.getSize();
String[] strArray = new String[size];
for (int j = 0; j < size; j++) {
ValueResultData valueResultData = (ValueResultData) data.get(j);
String accept = valueResultData.accept(new JsonizerVisitor());
strArray[j] = accept.replace("\\", "");
}
Statistics statistics = null;
try {
statistics = setParam(Statistics.class, strArray);
} catch (Exception e) {
e.printStackTrace();
}
statisticsList.add(statistics);
}
}
}
return statisticsList;
}
public static List<ChartsData> graphMapParsing(Map<String, Object> map, String seriesName) {
List<ChartsData> list = new ArrayList<>();
// ThreadGroup
......@@ -136,4 +168,21 @@ public class ResultDataParse {
SimpleDateFormat after = new SimpleDateFormat(TIME_PATTERN);
return after.format(before.parse(dateString));
}
private static <T> T setParam(Class<T> clazz, Object[] args)
throws Exception {
if (clazz == null || args == null) {
throw new IllegalArgumentException();
}
T t = clazz.newInstance();
Field[] fields = clazz.getDeclaredFields();
if (fields == null || fields.length > args.length) {
throw new IndexOutOfBoundsException();
}
for (int i = 0; i < fields.length; i++) {
fields[i].setAccessible(true);
fields[i].set(t, args[i]);
}
return t;
}
}
......@@ -24,6 +24,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
......@@ -229,6 +230,23 @@ public class PerformanceTestService {
}
}
public Map<String, String> log(String testId) {
final LoadTestWithBLOBs loadTest = loadTestMapper.selectByPrimaryKey(testId);
if (loadTest == null) {
MSException.throwException(Translator.get("test_not_found") + testId);
}
if (!StringUtils.equals(loadTest.getStatus(), PerformanceTestStatus.Running.name())) {
MSException.throwException(Translator.get("test_not_running"));
}
Engine engine = EngineFactory.createEngine(loadTest);
if (engine == null) {
MSException.throwException(String.format("Engine is null,test ID:%s", testId));
}
return engine.log();
}
public List<LoadTestDTO> recentTestPlans(QueryTestPlanRequest request) {
// 查询最近的测试计划
request.setRecent(true);
......@@ -260,4 +278,5 @@ public class PerformanceTestService {
example.createCriteria().andTestResourcePoolIdEqualTo(resourcePoolId);
return loadTestMapper.selectByExampleWithBLOBs(example);
}
}
......@@ -12,16 +12,11 @@ import io.metersphere.dto.ReportDTO;
import io.metersphere.engine.Engine;
import io.metersphere.engine.EngineFactory;
import io.metersphere.report.GenerateReport;
import io.metersphere.report.base.ChartsData;
import io.metersphere.report.base.Errors;
import io.metersphere.report.base.ReportTimeInfo;
import io.metersphere.report.base.TestOverview;
import io.metersphere.report.base.*;
import io.metersphere.report.dto.ErrorsTop5DTO;
import io.metersphere.report.dto.RequestStatisticsDTO;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.List;
......@@ -86,12 +81,11 @@ public class ReportService {
return extLoadTestReportMapper.getReportTestAndProInfo(reportId);
}
public RequestStatisticsDTO getReport(String id) {
public List<Statistics> getReport(String id) {
checkReportStatus(id);
LoadTestReportWithBLOBs loadTestReport = loadTestReportMapper.selectByPrimaryKey(id);
String content = loadTestReport.getContent();
RequestStatisticsDTO requestStatistics = GenerateReport.getRequestStatistics(content);
return requestStatistics;
return GenerateReport.getRequestStatistics(content);
}
public List<Errors> getReportErrors(String id) {
......
......@@ -20,3 +20,5 @@ only_one_k8s=Only one K8s can be added
organization_id_is_null=Organization ID cannot be null
max_thread_insufficient=The number of concurrent users exceeds
cannot_edit_load_test_running=Cannot modify the running test
test_not_found=Test cannot be found:
test_not_running=Test is not running
\ No newline at end of file
......@@ -20,3 +20,5 @@ only_one_k8s=只能添加一个 K8s
organization_id_is_null=组织 ID 不能为空
max_thread_insufficient=并发用户数超额
cannot_edit_load_test_running=不能修改正在运行的测试
test_not_found=测试不存在:
test_not_running=测试未运行
\ No newline at end of file
package io.metersphere;
import io.metersphere.report.base.Statistics;
import org.junit.Test;
import java.lang.reflect.Field;
public class ResultDataParseTest {
String[] s = {"1","2","3","4","5","6","7","8","9","10","11","12","13"};
public static <T> T setParam(Class<T> clazz, Object[] args)
throws Exception {
if (clazz == null || args == null) {
throw new IllegalArgumentException();
}
T t = clazz.newInstance();
Field[] fields = clazz.getDeclaredFields();
if (fields == null || fields.length > args.length) {
throw new IndexOutOfBoundsException();
}
for (int i = 0; i < fields.length; i++) {
fields[i].setAccessible(true);
fields[i].set(t, args[i]);
}
return t;
}
@Test
public void test() throws Exception {
Statistics statistics = setParam(Statistics.class, s);
System.out.println(statistics.toString());
}
}
......@@ -6,12 +6,11 @@
border
style="width: 100%"
show-summary
:summary-method="getSummaries"
:default-sort = "{prop: 'samples', order: 'descending'}"
>
<el-table-column label="Requests" fixed width="450" align="center">
<el-table-column
prop="requestLabel"
prop="label"
label="Label"
width="450"/>
</el-table-column>
......@@ -25,7 +24,13 @@
/>
<el-table-column
prop="errors"
prop="ko"
label="KO%"
align="center"
/>
<el-table-column
prop="error"
label="Error%"
align="center"
/>
......@@ -58,18 +63,29 @@
/>
</el-table-column>
<el-table-column label="Throughput">
<el-table-column
prop="avgHits"
label="Avg Hits/s"
prop="transactions"
label="Transactions"
width="100"
/>
</el-table-column>
<el-table-column label="NetWork(KB/sec)" align="center">
<el-table-column
prop="kbPerSec"
label="Avg Bandwidth(KBytes/s)"
prop="received"
label="Received"
align="center"
width="200"
/>
<el-table-column
prop="sent"
label="Sent"
align="center"
width="200"
/>
</el-table-column>
</el-table>
</div>
</template>
......@@ -79,50 +95,20 @@
name: "RequestStatistics",
data() {
return {
tableData: [{},{},{},{},{}],
totalInfo: {
totalLabel: '',
totalSamples: '',
totalErrors: '',
totalAverage: '',
totalMin: '',
totalMax: '',
totalTP90: '',
totalTP95: '',
totalTP99: '',
totalAvgHits: '',
totalAvgBandwidth: ''
}
tableData: [{},{},{},{},{}]
}
},
methods: {
initTableData() {
this.$get("/performance/report/content/" + this.id, res => {
this.tableData = res.data.requestStatisticsList;
this.totalInfo = res.data;
this.tableData = res.data;
})
},
getSummaries () {
const sums = []
sums[0] = this.totalInfo.totalLabel;
sums[1] = this.totalInfo.totalSamples;
sums[2] = this.totalInfo.totalErrors;
sums[3] = this.totalInfo.totalAverage;
sums[4] = this.totalInfo.totalMin;
sums[5] = this.totalInfo.totalMax;
sums[6] = this.totalInfo.totalTP90;
sums[7] = this.totalInfo.totalTP95;
sums[8] = this.totalInfo.totalTP99;
sums[9] = this.totalInfo.totalAvgHits;
sums[10] = this.totalInfo.totalAvgBandwidth;
return sums;
}
},
watch: {
status() {
if ("Completed" === this.status) {
this.initTableData()
this.getSummaries()
}
}
},
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册