提交 0c932748 authored 作者: 黄夏豪's avatar 黄夏豪

fix(base): 修了很多Bug

上级 811fbcd4
......@@ -157,7 +157,7 @@ CREATE TABLE `kt_execution_record` (
`test_data_id` bigint(20) DEFAULT NULL COMMENT '测试数据id',
`status` int(11) DEFAULT NULL COMMENT '执行状态,0:停止,1:执行',
`type` varchar(255) CHARACTER SET utf8 DEFAULT NULL COMMENT '类型',
`union_key` varchar(255) CHARACTER SET utf8 DEFAULT NULL COMMENT 'key用来记录执行批次',
`unique_key` varchar(255) CHARACTER SET utf8 DEFAULT NULL COMMENT 'key用来记录执行批次',
`log` text CHARACTER SET utf8 DEFAULT NULL COMMENT '日志',
`create_time` timestamp NULL DEFAULT NULL,
`update_time` timestamp NULL DEFAULT NULL,
......
......@@ -81,7 +81,7 @@ create table kt_execution_record
test_data_id bigint null comment '测试数据id',
status int null comment '执行状态,0:停止,1:执行',
type varchar(255) charset utf8 null comment '类型',
union_key varchar(255) charset utf8 null comment 'key用来记录执行批次',
unique_key varchar(255) charset utf8 null comment 'key用来记录执行批次',
log text charset utf8 null comment '日志',
create_time timestamp not null comment '数据创建时间',
update_time timestamp not null comment '数据更新时间'
......
......@@ -30,10 +30,8 @@ import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
......@@ -62,9 +60,9 @@ public class HttpClientActuator implements Actuator {
private void completeHttpRequestDetail(HttpRequestDetail httpRequestDetail,Long envId,Long projectId){
for (RequestHeader header : httpRequestDetail.getHeaders()) {
//获取动态变量,并将动态变量替换进去
header.setName(
header.setKey(
completeExpressionUtil.completeDynamicVariable(
header.getName(),
header.getKey(),
envId,
projectId)
);
......
......@@ -13,11 +13,11 @@ import org.apache.http.message.BasicHeader;
@NoArgsConstructor
public class RequestHeader {
private String name;
private String key;
private String value;
public BasicHeader getBasicHeader(){
return new BasicHeader(name,value);
return new BasicHeader(key,value);
}
}
......@@ -2,6 +2,7 @@ package org.matrix.actuators.usecase;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.apache.commons.lang3.StringUtils;
import org.matrix.actuators.Actuator;
......@@ -18,20 +19,20 @@ import org.matrix.actuators.httpclient.HttpResponseDetail;
import org.matrix.database.service.IExecutionHistoryService;
import org.matrix.enums.ExecutionHistoryStatus;
import org.matrix.exception.GlobalException;
import org.matrix.socket.ExecutionStatusMonitorSocketHandler;
import org.matrix.socket.pool.TestCaseExecuteSocketPool;
import org.matrix.socket.queue.LogQueueRuntime;
import org.matrix.socket.vo.CaseExecuteVo;
import org.matrix.socket.vo.TestExecuteLog;
import org.matrix.util.SpringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.*;
/**
* 测试用例执行器
......@@ -48,10 +49,13 @@ public class CaseActuator implements Actuator {
private final String baseJsPath = "syntaxCheck.js";
public CaseActuator(CheckPointActuator checkPointActuator, HttpClientActuator httpClientActuator, IExecutionHistoryService executionHistoryService) {
private final ExecutionStatusMonitorSocketHandler executionStatusMonitorSocketHandler;
public CaseActuator(CheckPointActuator checkPointActuator, HttpClientActuator httpClientActuator, IExecutionHistoryService executionHistoryService, ExecutionStatusMonitorSocketHandler executionStatusMonitorSocketHandler) {
this.checkPointActuator = checkPointActuator;
this.httpClientActuator = httpClientActuator;
this.executionHistoryService = executionHistoryService;
this.executionStatusMonitorSocketHandler = executionStatusMonitorSocketHandler;
}
......@@ -221,7 +225,9 @@ public class CaseActuator implements Actuator {
* @param session 如果是从socket运行并希望获取实时日志请从这里运行
* 外界如果执行测试用例的话请走这个接口,执行TestCase,并控制运行态日志池,进行日志的生成。
*/
public List<TestCaseExecuteResult> runTestCase(WebSocketSession session, CaseExecuteVo caseExecuteVo) {
public RunCaseResult runTestCase(WebSocketSession session, CaseExecuteVo caseExecuteVo) {
RunCaseResult runCaseResult = new RunCaseResult();
runCaseResult.setJobId(caseExecuteVo.getJobId());
String uniqueKey = UUID.randomUUID().toString();
Long currentThreadId = ThreadUtil.currentThreadId();
try {
......@@ -236,6 +242,7 @@ public class CaseActuator implements Actuator {
}
//建立执行历史(ExecutionHistory)
insertExecutionHistory(uniqueKey, caseExecuteVo);
Map<Long,List<TestCaseExecuteResult>> resultMap= new HashMap<>();
for (TestCaseListDataBto testCaseListDataBto : testCaseListDataBtoList) {
LogQueueRuntime
.initTestCaseLog(
......@@ -246,7 +253,11 @@ public class CaseActuator implements Actuator {
, uniqueKey
);
//执行测试用例
return executeTestCases(testCaseListDataBto, caseExecuteVo.getEnvId(), caseExecuteVo.getProjectId());
if (testCaseListDataBto.getTestDataList()!=null&&testCaseListDataBto.getTestDataList().size()>0){
List<TestCaseExecuteResult> resultList = executeTestCases(testCaseListDataBto, caseExecuteVo.getEnvId(), caseExecuteVo.getProjectId());
resultMap.put(testCaseListDataBto.getTestCase().getId(),resultList);
runCaseResult.setExecutionMap(resultMap);
}
}
} catch (GlobalException e) {
e.printStackTrace();
......@@ -344,28 +355,58 @@ public class CaseActuator implements Actuator {
.eq(ExecutionHistory::getDataId,currentTestExecute.getTestDataId())
.eq(ExecutionHistory::getUniqueKey,currentTestExecute.getUniqueKey())
.eq(ExecutionHistory::getUserId,currentTestExecute.getUserId())
);
//发送执行状态
try {
executionStatusMonitorSocketHandler.sendOverviewCurrentStatus(currentTestExecute.getUniqueKey());
executionStatusMonitorSocketHandler.sendDetailCurrentStatus(
currentTestExecute.getUserId(),
currentTestExecute.getTestJobId(),
currentTestExecute.getTestCaseId(),
currentTestExecute.getTestDataId(),
currentTestExecute.getUniqueKey(),
executionHistoryStatus);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 将所有还未完成的执行历史设置为ERROR
*/
public void endExecutionHistory() {
TestExecuteLog currentTestExecute = LogQueueRuntime.getCurrentTestExecute(ThreadUtil.currentThreadId());
if (currentTestExecute!=null){
LambdaUpdateWrapper<ExecutionHistory> wrapper = Wrappers
.lambdaUpdate(ExecutionHistory.class)
.eq(ExecutionHistory::getJobId, currentTestExecute.getTestJobId())
.eq(ExecutionHistory::getCaseId, currentTestExecute.getTestCaseId())
.eq(ExecutionHistory::getUniqueKey, currentTestExecute.getUniqueKey())
.eq(ExecutionHistory::getUserId, currentTestExecute.getUserId())
.ne(ExecutionHistory::getStatus, ExecutionHistoryStatus.FINISH);
ExecutionHistory executionHistory = new ExecutionHistory();
executionHistory.setStatus(ExecutionHistoryStatus.ERROR);
executionHistory.setEndTime(LocalDateTime.now());
executionHistoryService.update(executionHistory, Wrappers
.lambdaUpdate(ExecutionHistory.class)
.eq(ExecutionHistory::getJobId,currentTestExecute.getTestJobId())
.eq(ExecutionHistory::getCaseId,currentTestExecute.getTestCaseId())
.eq(ExecutionHistory::getUniqueKey,currentTestExecute.getUniqueKey())
.eq(ExecutionHistory::getUserId,currentTestExecute.getUserId())
.ne(ExecutionHistory::getStatus,ExecutionHistoryStatus.FINISH)
);
List<ExecutionHistory> list = executionHistoryService.list(wrapper);
executionHistoryService.update(executionHistory, wrapper);
try {
executionStatusMonitorSocketHandler.sendOverviewCurrentStatus(currentTestExecute.getUniqueKey());
for (ExecutionHistory history : list) {
executionStatusMonitorSocketHandler.sendDetailCurrentStatus(
history.getUserId(),
history.getJobId(),
history.getCaseId(),
history.getDataId(),
history.getUniqueKey(),
ExecutionHistoryStatus.ERROR);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
......
package org.matrix.actuators.usecase;
import lombok.Data;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 当用户执行了一组测试用例后,会返回这个对象
* @author huangxiahao
*/
@Data
public class RunCaseResult {
private Long jobId;
/**
* Key为caseId,List为case下的每一个执行结果
*/
private Map<Long, List<TestCaseExecuteResult>> executionMap= new HashMap<>();
}
package org.matrix.config;
import org.matrix.socket.ExecutionSocketHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.matrix.socket.ExecutionStatusMonitorSocketHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
......@@ -16,14 +16,18 @@ public class WebSocketConfig implements WebSocketConfigurer {
private final ExecutionSocketHandler executionSocketHandler;
public WebSocketConfig(ExecutionSocketHandler executionSocketHandler) {
private final ExecutionStatusMonitorSocketHandler executionStatusMonitorSocketHandler;
public WebSocketConfig(ExecutionSocketHandler executionSocketHandler,ExecutionStatusMonitorSocketHandler executionStatusMonitorSocketHandler) {
this.executionSocketHandler = executionSocketHandler;
this.executionStatusMonitorSocketHandler = executionStatusMonitorSocketHandler;
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry
.addHandler(executionSocketHandler, "ws")
.addHandler(executionStatusMonitorSocketHandler, "statusMonitor")
.setAllowedOrigins("*");
}
}
\ No newline at end of file
......@@ -27,12 +27,13 @@ public interface ExecutionHistoryMapper extends BaseMapper<ExecutionHistory> {
* @param jobId 测试任务ID
* @param caseId 用例ID
* @param userId 用户ID
* @param uniqueKey 批次号
* @return 执行记录列表
*/
@Select("<script>" +
"SELECT\n" +
"\tkeh.id,\n" +
"\tkeh.union_key uniqueKey,\n" +
"\tkeh.unique_key uniqueKey,\n" +
"\tkeh.case_id caseId,\n" +
"\tkeh.data_id dataId,\n" +
"\tkeh.job_id jobId,\n" +
......@@ -56,10 +57,13 @@ public interface ExecutionHistoryMapper extends BaseMapper<ExecutionHistory> {
"<if test=\"jobId!=null\">\n" +
"and keh.job_id= #{jobId} \n" +
" </if>" +
"<if test=\"uniqueKey!=null and uniqueKey!=''\">" +
"and keh.`unique_key` = #{uniqueKey}" +
"</if>" +
"</where>" +
"GROUP BY keh.union_key " +
"GROUP BY keh.unique_key " +
"</script>")
IPage<ExecutionHistoryVo> pageByCaseIdAndJobId(IPage<ExecutionHistoryVo> page, Long caseId, Long jobId,Long userId);
IPage<ExecutionHistoryVo> pageByCaseIdAndJobId(IPage<ExecutionHistoryVo> page, Long caseId, Long jobId,Long userId,String uniqueKey);
/**
* 查出最近一条的执行历史
......@@ -71,7 +75,7 @@ public interface ExecutionHistoryMapper extends BaseMapper<ExecutionHistory> {
@Select("<script>" +
"SELECT\n" +
"\tkeh.id,\n" +
"\tkeh.union_key uniqueKey,\n" +
"\tkeh.unique_key uniqueKey,\n" +
"\tkeh.case_id caseId,\n" +
"\tkeh.data_id dataId,\n" +
"\tkeh.job_id jobId,\n" +
......@@ -89,7 +93,7 @@ public interface ExecutionHistoryMapper extends BaseMapper<ExecutionHistory> {
"\tkt_execution_history keh\n" +
"\tLEFT JOIN kt_test_case ktc ON ktc.id = keh.case_id\n" +
"\tLEFT JOIN kt_test_data ktd ON ktd.id = keh.data_id \n" +
"\t WHERE keh.union_key = ( SELECT union_key FROM kt_execution_history WHERE id IN ( SELECT max( id ) FROM kt_execution_history " +
"\t WHERE keh.unique_key = ( SELECT unique_key FROM kt_execution_history WHERE id IN ( SELECT max( id ) FROM kt_execution_history " +
"<where>" +
"<if test=\"userId!=null\">\n" +
"and user_id= #{userId} \n" +
......@@ -103,7 +107,7 @@ public interface ExecutionHistoryMapper extends BaseMapper<ExecutionHistory> {
"</where>" +
"GROUP BY case_id ) ) \n" +
"GROUP BY\n" +
"\tkeh.union_key\n" +
"\tkeh.unique_key\n" +
"</script>")
ExecutionHistoryVo selectLastExecutionHistoryVo( Long caseId, Long jobId,Long userId);
......
......@@ -26,8 +26,8 @@ public interface TestJobMapper extends BaseMapper<TestJob> {
"\t`status` \n" +
"FROM\n" +
"\t`kt_test_job` ktj\n" +
"\tLEFT JOIN ( SELECT job_id, union_key FROM kt_execution_history WHERE id IN ( SELECT max( id ) FROM kt_execution_history WHERE job_id != - 1 GROUP BY job_id ) ) keh ON ktj.id = keh.job_id\n" +
"\tLEFT JOIN ( SELECT union_key, IF ( MIN( `status` )= 0, 0, MAX( `status` )) `status` FROM kt_execution_history GROUP BY union_key ) us ON us.union_key = keh.union_key" +
"\tLEFT JOIN ( SELECT job_id, unique_key FROM kt_execution_history WHERE id IN ( SELECT max( id ) FROM kt_execution_history WHERE job_id != - 1 GROUP BY job_id ) ) keh ON ktj.id = keh.job_id\n" +
"\tLEFT JOIN ( SELECT unique_key, IF ( MIN( `status` )= 0, 0, MAX( `status` )) `status` FROM kt_execution_history GROUP BY unique_key ) us ON us.unique_key = keh.unique_key" +
"<where>" +
"<if test=\"name!=null and name!=''\">" +
"and ktj.`name` like concat('%',#{name},'%')" +
......
......@@ -22,9 +22,10 @@ public interface IExecutionHistoryService extends IService<ExecutionHistory> {
* @param pageSize 分页条数
* @param jobId 测试任务ID
* @param caseId 用例ID
* @uniqueKey caseId 批次号
* @return 执行记录列表
*/
IPage<ExecutionHistoryVo> pageExecutionHistoryVoByCaseIdAndJobId(Long caseId, Long jobId, int pageSize, int pageNum);
IPage<ExecutionHistoryVo> pageExecutionHistoryVoByCaseIdAndJobId(Long caseId, Long jobId,String uniqueKey, int pageSize, int pageNum);
/**
* 根据CaseId和JobId 查询对应的最后一次执行记录
......
......@@ -33,10 +33,10 @@ import static java.util.Comparator.comparingLong;
public class ExecutionHistoryServiceImpl extends ServiceImpl<ExecutionHistoryMapper, ExecutionHistory> implements IExecutionHistoryService {
@Override
public IPage<ExecutionHistoryVo> pageExecutionHistoryVoByCaseIdAndJobId(Long caseId, Long jobId, int pageNum, int pageSize ) {
public IPage<ExecutionHistoryVo> pageExecutionHistoryVoByCaseIdAndJobId(Long caseId, Long jobId,String uniqueKey, int pageNum, int pageSize ) {
// todo 黄夏豪 等待接入用户ID
Page<ExecutionHistoryVo> page = new Page<>(pageNum, pageSize);
return baseMapper.pageByCaseIdAndJobId(page,caseId,jobId,1L);
return baseMapper.pageByCaseIdAndJobId(page,caseId,jobId,1L,uniqueKey);
}
@Override
......
package org.matrix.database.vo;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.matrix.database.entity.BaseEntity;
import org.matrix.database.entity.ExecutionRecord;
import org.matrix.socket.enums.TestExecuteType;
/**
* @author mry
*/
@Data
public class ExecutionRecordVo extends ExecutionRecord {
private String jobName;
private String caseName;
private String dataName;
}
package org.matrix.socket;
import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.apache.commons.beanutils.BeanUtils;
import org.matrix.actuators.datasource.IDataSourceService;
import org.matrix.actuators.sql.SqlExpActuator;
import org.matrix.actuators.usecase.CaseActuator;
import org.matrix.database.entity.TestCase;
import org.matrix.database.entity.TestCaseListDataBto;
import org.matrix.database.entity.TestData;
import org.matrix.database.entity.TestJob;
import org.matrix.database.service.ITestCaseService;
import org.matrix.database.service.ITestDataService;
import org.matrix.database.service.ITestJobService;
import org.matrix.exception.GlobalException;
import org.matrix.socket.enums.SocketType;
import org.matrix.socket.pool.MonitorSocketPool;
import org.matrix.socket.vo.CaseExecuteVo;
import org.matrix.socket.vo.ExecuteMonitorVo;
import org.matrix.socket.vo.SocketVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* webSocket处理 调用和执行的日志发送
*
......@@ -27,10 +46,20 @@ public class ExecutionSocketHandler extends TextWebSocketHandler {
final CaseActuator caseActuator;
final ITestDataService testDataService;
final ITestJobService testJobService;
final ITestCaseService testCaseService;
final SqlExpActuator sqlExpActuator;
final JdbcTemplate jdbcTemplate;
final IDataSourceService dataSourceService;
public ExecutionSocketHandler(CaseActuator caseActuator, ITestDataService testDataService) {
public ExecutionSocketHandler(CaseActuator caseActuator, ITestDataService testDataService, ITestJobService testJobService, ITestCaseService testCaseService, SqlExpActuator sqlExpActuator, JdbcTemplate jdbcTemplate, IDataSourceService dataSourceService) {
this.caseActuator = caseActuator;
this.testDataService = testDataService;
this.testJobService = testJobService;
this.testCaseService = testCaseService;
this.sqlExpActuator = sqlExpActuator;
this.jdbcTemplate = jdbcTemplate;
this.dataSourceService = dataSourceService;
}
@Override
......@@ -44,23 +73,47 @@ public class ExecutionSocketHandler extends TextWebSocketHandler {
if (SocketType.TEST_CASE_EXECUTE.equals(socketVo.getSocketType())) {
CaseExecuteVo caseExecuteVo = JSON.parseObject(payload, CaseExecuteVo.class);
caseActuator.runTestCase(session, caseExecuteVo);
} else if (SocketType.TEST_JOB_EXECUTE.equals(socketVo.getSocketType())) {
CaseExecuteVo caseExecuteVo = JSON.parseObject(payload, CaseExecuteVo.class);
TestJob byId = testJobService.getById(caseExecuteVo.getJobId());
String sqlStatement = byId.getSqlStatement();
dataSourceService.switchMainDataSource();
List<Map<String, Object>> list = jdbcTemplate.queryForList(sqlStatement);
List<TestCase> testCaseList = new ArrayList<>();
for (Map<String, Object> objectMap : list) {
TestCase testCase = BeanUtil.mapToBean(objectMap,TestCase.class,false);
testCaseList.add(testCase);
}
for (TestCase testCase : testCaseList) {
List<TestData> testDataList = testDataService.list(Wrappers.lambdaQuery(TestData.class).eq(TestData::getTestCaseId, testCase.getId()));
TestCaseListDataBto testCaseListDataBto = new TestCaseListDataBto();
testCaseListDataBto.setTestCase(testCase);
testCaseListDataBto.setTestDataList(testDataList);
caseExecuteVo.getTestCaseListDataBtoList().add(testCaseListDataBto);
}
if (testCaseList.size()>0){
caseExecuteVo.setProjectId(testCaseList.get(0).getProjectId());
}
session.sendMessage(new TextMessage(JSONObject.toJSONString(caseExecuteVo)));
caseActuator.runTestCase(session, caseExecuteVo);
} else if (SocketType.TEST_CASE_MONITOR.equals(socketVo.getSocketType())) {
ExecuteMonitorVo caseExecuteVo = JSON.parseObject(payload, ExecuteMonitorVo.class);
EXECUTE_MONITOR_SOCKET_POOL.add(caseExecuteVo.getUniqueKey(), session);
} else {
session.sendMessage(new TextMessage("入参不符合规定"));
throw new GlobalException("入参不符合规定");
}
}
} catch (JSONException e) {
session.sendMessage(new TextMessage("入参不符合规定"));
} catch (Exception e) {
e.printStackTrace();
session.sendMessage(new TextMessage(e.getMessage()));
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
EXECUTE_MONITOR_SOCKET_POOL.remove(session);
}
}
package org.matrix.socket;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.apache.commons.lang3.StringUtils;
import org.matrix.database.entity.ExecutionHistory;
import org.matrix.database.service.IExecutionHistoryService;
import org.matrix.database.vo.ExecutionHistoryVo;
import org.matrix.enums.ExecutionHistoryStatus;
import org.matrix.socket.enums.ExecuteStatusSendMessageType;
import org.matrix.socket.pool.MonitorSocketPool;
import org.matrix.socket.vo.ExecutionStatusMonitorVo;
import org.matrix.socket.vo.ExecutionStatusSendMessageVo;
import org.matrix.socket.vo.TestExecuteLog;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.List;
/**
* webSocket处理
* 订阅用例调试和测试任务执行的
*
* @author huangxiahao
*/
@Component
public class ExecutionStatusMonitorSocketHandler extends TextWebSocketHandler {
public static final MonitorSocketPool EXECUTE_MONITOR_SOCKET_POOL = new MonitorSocketPool();
private final IExecutionHistoryService executionHistoryService;
public ExecutionStatusMonitorSocketHandler(IExecutionHistoryService executionHistoryService) {
this.executionHistoryService = executionHistoryService;
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
super.handleTextMessage(session, message);
String payload = message.getPayload();
ExecutionStatusMonitorVo executionStatusMonitorVo = JSON.parseObject(payload, ExecutionStatusMonitorVo.class);
if (executionStatusMonitorVo != null && executionStatusMonitorVo.getExecuteStatusMonitorType() != null) {
switch (executionStatusMonitorVo.getExecuteStatusMonitorType()) {
case DETAIL:
if (!StringUtils.isEmpty(executionStatusMonitorVo.getUniqueKey())&&executionStatusMonitorVo.getCaseId()!=null&&executionStatusMonitorVo.getDataId()!=null){
EXECUTE_MONITOR_SOCKET_POOL.add(String.format("%s.%s.%s.%s.%s",
executionStatusMonitorVo.getUserId(),
executionStatusMonitorVo.getJobId(),
executionStatusMonitorVo.getCaseId(),
executionStatusMonitorVo.getDataId(),
executionStatusMonitorVo.getUniqueKey()
), session);
}
sendDetailCurrentStatus(
executionStatusMonitorVo.getUserId(),
executionStatusMonitorVo.getJobId(),
executionStatusMonitorVo.getCaseId(),
executionStatusMonitorVo.getDataId(),
executionStatusMonitorVo.getUniqueKey(),
null
);
break;
default:
if (!StringUtils.isEmpty(executionStatusMonitorVo.getUniqueKey())){
EXECUTE_MONITOR_SOCKET_POOL.add(executionStatusMonitorVo.getUniqueKey(), session);
sendOverviewCurrentStatus(executionStatusMonitorVo.getUniqueKey());
}
}
} else {
session.sendMessage(new TextMessage("入参不符合规定"));
}
}
public void sendOverviewCurrentStatus(String uniqueKey) throws IOException {
//获取该数据当前执行状态
IPage<ExecutionHistoryVo> page = executionHistoryService.pageExecutionHistoryVoByCaseIdAndJobId(null, null, uniqueKey, 1, 10);
if (page.getRecords() != null && page.getRecords().size() > 0) {
ExecutionHistoryStatus status = page.getRecords().get(0).getStatus();
ExecutionStatusSendMessageVo sendMessageVo = new ExecutionStatusSendMessageVo(
uniqueKey,
null,
null,
null,
status,
ExecuteStatusSendMessageType.OVERVIEW
);
List<WebSocketSession> webSocketSessions = EXECUTE_MONITOR_SOCKET_POOL.get(uniqueKey);
if (webSocketSessions!=null){
for (WebSocketSession webSocketSession : webSocketSessions) {
webSocketSession.sendMessage(new TextMessage(JSONObject.toJSONString(sendMessageVo)));
}
}
}
}
public void sendDetailCurrentStatus(Long userId,Long jobId,Long caseId,Long dataId,String uniqueKey,ExecutionHistoryStatus status) throws IOException {
//获取该数据当前执行状态
String key = String.format("%s.%s.%s.%s.%s",
userId,
jobId,
caseId,
dataId,
uniqueKey
);
if (status==null){
LambdaQueryWrapper<ExecutionHistory> eq = Wrappers.lambdaQuery(ExecutionHistory.class)
.eq(ExecutionHistory::getUserId, userId)
.eq(ExecutionHistory::getJobId, jobId)
.eq(ExecutionHistory::getCaseId, caseId)
.eq(ExecutionHistory::getDataId, dataId)
.eq(ExecutionHistory::getUniqueKey, uniqueKey);
ExecutionHistory one = executionHistoryService.getOne(eq);
if (one!=null){
status = one.getStatus();
}
}
List<WebSocketSession> webSocketSessions = EXECUTE_MONITOR_SOCKET_POOL.get(key);
ExecutionStatusSendMessageVo sendMessageVo = new ExecutionStatusSendMessageVo(
uniqueKey,
jobId,
caseId,
dataId,
status,
ExecuteStatusSendMessageType.Data
);
if (webSocketSessions!=null){
for (WebSocketSession webSocketSession : webSocketSessions) {
webSocketSession.sendMessage(new TextMessage(JSONObject.toJSONString(sendMessageVo)));
}
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
EXECUTE_MONITOR_SOCKET_POOL.remove(session);
}
}
package org.matrix.socket;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
/**
* webSocket处理
* 订阅用例调试和测试任务执行的
*
* @author huangxiahao
*/
@Component
public class ExecutionStatusMonitorSocketPool extends TextWebSocketHandler {
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
super.handleTextMessage(session, message);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
}
}
package org.matrix.socket.enums;
/**
* @author huangxiahao
*/
public enum ExecuteStatusMonitorType {
/**
* 订阅批次状态
*/
OVERVIEW,
/**
* 订阅具体的数据状态
*/
DETAIL
;
}
package org.matrix.socket.enums;
/**
* @author huangxiahao
*/
public enum ExecuteStatusSendMessageType {
/**
* 改变了批次状态
*/
OVERVIEW,
/**
* 改变了实例状态
*/
Data
;
}
......@@ -10,6 +10,7 @@ public enum SocketType {
* 测试用例类型
*/
TEST_CASE_EXECUTE,
TEST_JOB_EXECUTE,
TEST_CASE_MONITOR;
}
package org.matrix.socket.pool;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* 订阅执行日志的池子
* 订阅的池子
*
* @author huangxiahao
*/
......@@ -25,11 +27,16 @@ public class MonitorSocketPool {
*/
private ConcurrentHashMap<String, List<String>> socketKeyMap = new ConcurrentHashMap<>();
public void add(String key, WebSocketSession clientSocket) {
public void add(String key, WebSocketSession clientSocket) throws IOException {
if (clientSocket != null & key != null) {
List<WebSocketSession> webSocketSessions = socketMap.get(key);
if (webSocketSessions != null) {
webSocketSessions.add(clientSocket);
List<String> list = socketKeyMap.get(clientSocket.getId());
if (list!=null&&list.contains(key)){
clientSocket.sendMessage(new TextMessage("请勿重复订阅"));
}else {
webSocketSessions.add(clientSocket);
}
} else {
webSocketSessions = new ArrayList<>();
webSocketSessions.add(clientSocket);
......@@ -57,7 +64,11 @@ public class MonitorSocketPool {
List<String> list = socketKeyMap.get(socketId);
if (list != null) {
for (String s : list) {
socketMap.get(s);
socketMap.get(s).remove(clientSocket);
if (socketMap.get(s).size()==0){
socketMap.remove(s);
}
}
}
socketKeyMap.remove(socketId);
......
package org.matrix.socket.vo;
import lombok.Data;
import org.matrix.socket.enums.ExecuteStatusMonitorType;
import org.matrix.socket.enums.TestExecuteType;
import java.util.List;
/**
* 前端需要监听某一个测试任务或者测试用例调试时需要传入的值
......@@ -9,6 +13,16 @@ import lombok.Data;
@Data
public class ExecutionStatusMonitorVo {
private String uniqueKey;
private Long userId;
private Long jobId = -1L;
private Long caseId ;
private Long dataId;
private ExecuteStatusMonitorType executeStatusMonitorType;
}
package org.matrix.socket.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.matrix.enums.ExecutionHistoryStatus;
import org.matrix.socket.enums.ExecuteStatusSendMessageType;
/**
* 当订阅的执行任务或者用例调试的状态发生变化的时候 向外界发送状态所用到的VO
* @author huangxiahao
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ExecutionStatusSendMessageVo {
private String uniqueKey;
private Long jobId;
private Long caseId;
private Long dataId;
private ExecutionHistoryStatus status;
private ExecuteStatusSendMessageType type;
}
......@@ -90,27 +90,27 @@ public class TestPigeon extends AbstractTestNGSpringContextTests {
@Test(dataProvider = "testData")
public void test(Map<String, String> data) {
log.info("这是testData");
String id = data.get("id");
long caseId = Long.parseLong(id);
TestCase testCase = java.util.Optional.of(caseService.getById(caseId))
.orElseThrow(() -> new GlobalException(String.format("没有找到id = %d 的TestCase", caseId)));
List<TestData> testDataList = java.util.Optional.of(dataService.list(Wrappers.lambdaQuery(TestData.class)
.eq(TestData::getTestCaseId, caseId)))
.orElseThrow(() -> new GlobalException(String.format("没有找到testCaseId = %d 的TestData", caseId)));
CaseExecuteVo caseExecuteVo = new CaseExecuteVo();
caseExecuteVo.setProjectId(testCase.getProjectId());
caseExecuteVo.setEnvId(envId);
caseExecuteVo.setUserId(1L);
caseExecuteVo.getTestCaseListDataBtoList().add(
new TestCaseListDataBto(
testCase,
testDataList
)
);
List<TestCaseExecuteResult> testCaseExecuteResults = caseActuator.runTestCase(null, caseExecuteVo);
resultMap.put(caseId, testCaseExecuteResults);
userResultMap.put(1L, resultMap);
// log.info("这是testData");
// String id = data.get("id");
// long caseId = Long.parseLong(id);
// TestCase testCase = java.util.Optional.of(caseService.getById(caseId))
// .orElseThrow(() -> new GlobalException(String.format("没有找到id = %d 的TestCase", caseId)));
// List<TestData> testDataList = java.util.Optional.of(dataService.list(Wrappers.lambdaQuery(TestData.class)
// .eq(TestData::getTestCaseId, caseId)))
// .orElseThrow(() -> new GlobalException(String.format("没有找到testCaseId = %d 的TestData", caseId)));
// CaseExecuteVo caseExecuteVo = new CaseExecuteVo();
// caseExecuteVo.setProjectId(testCase.getProjectId());
// caseExecuteVo.setEnvId(envId);
// caseExecuteVo.setUserId(1L);
// caseExecuteVo.getTestCaseListDataBtoList().add(
// new TestCaseListDataBto(
// testCase,
// testDataList
// )
// );
// List<TestCaseExecuteResult> testCaseExecuteResults = caseActuator.runTestCase(null, caseExecuteVo);
// resultMap.put(caseId, testCaseExecuteResults);
// userResultMap.put(1L, resultMap);
}
......
package org.matrix.actuators.http;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.matrix.actuators.httpclient.HttpClientActuator;
import org.matrix.actuators.httpclient.HttpRequestDetail;
import org.matrix.actuators.httpclient.HttpResponseDetail;
import org.matrix.config.HttpRequestConfig;
import org.matrix.database.entity.ExecutionRecord;
import org.matrix.database.service.IExecutionRecordService;
import org.matrix.exception.GlobalException;
import org.matrix.socket.enums.TestExecuteType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static java.util.stream.Collectors.groupingBy;
@RunWith(SpringRunner.class)
@SpringBootTest
......@@ -18,6 +32,9 @@ class HttpClientActuatorTest {
@Autowired
HttpClientActuator httpClientActuator;
@Autowired
IExecutionRecordService executionRecordService;
@Test
void test(){
......@@ -38,5 +55,65 @@ class HttpClientActuatorTest {
HttpResponseDetail httpResponseDetail = httpClientActuator.sendHttpRequest(httpRequestDetail,1l,1l);
System.out.println(httpResponseDetail);
}
@Test
void test2(){
List<ExecutionRecord> results = getExecutionRecords(1L, null, null, "e9e0a9cd-df63-4fea-9992-68d31c8b2160", null);
Map<Long, Map<Long, Map<Long, List<ExecutionRecord>>>> map = results.stream().collect(
groupingBy(ExecutionRecord::getTestJobId,
groupingBy(ExecutionRecord::getTestCaseId,
groupingBy(ExecutionRecord::getTestDataId))));
System.out.println("1");
}
List<ExecutionRecord> getExecutionRecords(Long userId, Long testDataId, Long testCaseId, String uniqueKey, TestExecuteType type) {
List<ExecutionRecord> results = Optional.of(executionRecordService.list(
Wrappers.lambdaQuery(ExecutionRecord.class)
.eq(userId != null, ExecutionRecord::getUserId, userId)
.eq(testDataId != null, ExecutionRecord::getTestDataId, testDataId)
.eq(testCaseId != null, ExecutionRecord::getTestCaseId, testCaseId)
.eq(StringUtils.hasLength(uniqueKey), ExecutionRecord::getUniqueKey, uniqueKey)
.eq(type != null, ExecutionRecord::getType, type)
)).orElseThrow(() -> new GlobalException("xxx"));
return results;
}
void putValueToNext(ExecutionRecord record, JSONObject resultJson, JSONArray jsonArray, int num){
if (num==4){
jsonArray.add(record.getLog());
}else {
if (num==1){
JSONObject jsonObject = resultJson.getJSONObject(record.getTestJobId() + "");
if (jsonObject!=null){
putValueToNext(record,jsonObject,jsonArray,2);
}else {
JSONObject newJsonObject = new JSONObject();
resultJson.put(record.getTestJobId() + "",newJsonObject);
putValueToNext(record,newJsonObject,jsonArray,2);
}
}
if (num==2){
JSONObject jsonObject = resultJson.getJSONObject(record.getTestCaseId() + "");
if (jsonObject!=null){
putValueToNext(record,jsonObject,jsonArray,3);
}else {
JSONObject newJsonObject = new JSONObject();
resultJson.put(record.getTestCaseId() + "",newJsonObject);
putValueToNext(record,newJsonObject,jsonArray,3);
}
}
if (num==3){
JSONObject jsonObject = resultJson.getJSONObject(record.getTestDataId() + "");
if (jsonObject!=null){
putValueToNext(record,jsonObject,jsonArray,4);
}else {
JSONArray newJsonArray = new JSONArray();
resultJson.put(record.getTestDataId() + "",newJsonArray);
putValueToNext(record,null,jsonArray,4);
}
}
}
}
}
......@@ -51,7 +51,7 @@ public class ExecutionHistoryController {
@ApiImplicitParam(name="jobId",value="测试任务ID",paramType="query"),
@ApiImplicitParam(name="caseId",value="测试用例ID",paramType="query")
})
public ResponseEntity<CommonResultObj<IPage<ExecutionHistoryVo>>> findExecutionHistory(
public ResponseEntity<CommonResultObj<IPage<ExecutionHistoryVo>>> executionHistoryDetail(
@RequestParam(defaultValue = "10") int pageSize,
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "-1") Long jobId,
......@@ -87,15 +87,17 @@ public class ExecutionHistoryController {
@ApiImplicitParam(name="pageSize",value="页码",required=true,paramType="query"),
@ApiImplicitParam(name="pageNum",value="当前页显示调试",required=true,paramType="query"),
@ApiImplicitParam(name="jobId",value="测试任务ID",paramType="query"),
@ApiImplicitParam(name="caseId",value="测试用例ID",paramType="query")
@ApiImplicitParam(name="caseId",value="测试用例ID",paramType="query"),
@ApiImplicitParam(name="uniqueKey",value="执行历史批次号",paramType="query"),
})
public ResponseEntity<CommonResultObj<IPage<ExecutionHistoryVo>>> findExecutionHistory(
@RequestParam(defaultValue = "10") int pageSize,
@RequestParam(defaultValue = "1") int pageNum,
@RequestParam(defaultValue = "-1") Long jobId,
Long caseId
){
IPage<ExecutionHistoryVo> page = executionHistoryService.pageExecutionHistoryVoByCaseIdAndJobId(caseId, jobId, pageNum, pageSize);
Long caseId,
String uniqueKey
){
IPage<ExecutionHistoryVo> page = executionHistoryService.pageExecutionHistoryVoByCaseIdAndJobId(caseId, jobId,uniqueKey, pageNum, pageSize);
return page.getRecords().size() != 0
? CommonResult.success(page, "查询成功")
: CommonResult.failed(page, "查询失败或无数据");
......@@ -117,5 +119,4 @@ public class ExecutionHistoryController {
: CommonResult.failed(null, "查询失败或无数据");
}
}
package org.matrix.autotest.controller;
import cn.hutool.core.bean.BeanUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.matrix.autotest.utils.PageTools;
import org.matrix.database.entity.ExecutionRecord;
import org.matrix.database.entity.TestJob;
import org.matrix.database.service.IExecutionRecordService;
import org.matrix.database.service.ITestCaseService;
import org.matrix.database.service.ITestDataService;
import org.matrix.database.service.ITestJobService;
import org.matrix.database.vo.CommonResult;
import org.matrix.database.vo.CommonResultObj;
import org.matrix.database.vo.ExecutionRecordVo;
import org.matrix.enums.ExecutionRecType;
import org.matrix.exception.GlobalException;
import org.matrix.socket.enums.TestExecuteType;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.*;
/**
* @author mry
......@@ -30,8 +43,20 @@ public class ExecutionRecordController {
private final IExecutionRecordService executionRecordService;
public ExecutionRecordController(IExecutionRecordService executionRecordService) {
final
ITestJobService testJobService;
final
ITestCaseService testCaseService;
final
ITestDataService testDataService;
public ExecutionRecordController(IExecutionRecordService executionRecordService, ITestJobService testJobService, ITestCaseService testCaseService, ITestDataService testDataService) {
this.executionRecordService = executionRecordService;
this.testJobService = testJobService;
this.testCaseService = testCaseService;
this.testDataService = testDataService;
}
/**
......@@ -42,7 +67,7 @@ public class ExecutionRecordController {
* @param userId 查询条件:用户ID
* @param testDataId 查询条件:测试数据ID
* @param testCaseId 查询条件:测试用例ID
* @param uniqueKey 查询条件:唯一批次号
* @param uniqueKey 查询条件:唯一批次号
* @param status 查询条件:运行状态
* @param type 查询条件:执行类型
* @return 分页查询的结果, 执行记录
......@@ -78,20 +103,64 @@ public class ExecutionRecordController {
* @param userId 查询条件:用户ID
* @param testDataId 查询条件:测试数据ID
* @param testCaseId 查询条件:测试用例ID
* @param uniqueKey 查询条件:唯一批次号
* @param status 查询条件:运行状态
* @param uniqueKey 查询条件:唯一批次号
* @param type 查询条件:执行类型
* @return 分页查询的结果, 执行记录
*/
@ApiOperation("分页查询执行记录")
@ApiOperation("列表查询执行记录")
@GetMapping("/list")
public ResponseEntity<CommonResultObj<List<ExecutionRecord>>> findListExecutionRecord(
Long userId,
Long testDataId,
Long testCaseId,
String uniqueKey,
ExecutionRecType status,
TestExecuteType type) {
List<ExecutionRecord> results = getExecutionRecords(userId, testDataId, testCaseId, uniqueKey, type);
Map<Long, Map<Long, Map<Long, List<String>>>> resultMap = new HashMap<>();
return results.size() != 0
? CommonResult.success(results, "查询成功")
: CommonResult.failed(results, "查询失败或无数据");
}
/**
* 分页查询执行记录
*
* @param userId 查询条件:用户ID
* @param testDataId 查询条件:测试数据ID
* @param testCaseId 查询条件:测试用例ID
* @param uniqueKey 查询条件:唯一批次号
* @param type 查询条件:执行类型
* @return 分页查询的结果, 执行记录
*/
@ApiOperation("树状查询执行记录")
@GetMapping("/tree")
public ResponseEntity<CommonResultObj<Map<Long, Map<Long, Map<Long, List<ExecutionRecordVo>>>>>> tree(
Long userId,
Long testDataId,
Long testCaseId,
String uniqueKey,
TestExecuteType type) {
List<ExecutionRecord> results = getExecutionRecords(userId, testDataId, testCaseId, uniqueKey, type);
List<ExecutionRecordVo> list = new ArrayList<>();
for (ExecutionRecord result : results) {
ExecutionRecordVo recordVo = new ExecutionRecordVo();
BeanUtils.copyProperties(result,recordVo);
recordVo.setJobName(testJobService.getById(result.getTestJobId()).getName());
recordVo.setCaseName(testCaseService.getById(result.getTestCaseId()).getName());
if (recordVo.getTestDataId()!=-1L){
recordVo.setDataName(testDataService.getById(result.getTestDataId()).getName());
}
list.add(recordVo);
}
Map<Long, Map<Long, Map<Long, List<ExecutionRecordVo>>>> map = list.stream().collect(
groupingBy(ExecutionRecord::getTestJobId,
groupingBy(ExecutionRecord::getTestCaseId,
groupingBy(ExecutionRecord::getTestDataId))));
return CommonResult.success(map, "查询成功");
}
private List<ExecutionRecord> getExecutionRecords(Long userId, Long testDataId, Long testCaseId, String uniqueKey, TestExecuteType type) {
List<ExecutionRecord> results = Optional.of(executionRecordService.list(
Wrappers.lambdaQuery(ExecutionRecord.class)
.eq(userId != null, ExecutionRecord::getUserId, userId)
......@@ -99,12 +168,11 @@ public class ExecutionRecordController {
.eq(testCaseId != null, ExecutionRecord::getTestCaseId, testCaseId)
.eq(StringUtils.hasLength(uniqueKey), ExecutionRecord::getUniqueKey, uniqueKey)
.eq(type != null, ExecutionRecord::getType, type)
)).orElseThrow(()->new GlobalException("xxx"));
return results.size() != 0
? CommonResult.success(results, "查询成功")
: CommonResult.failed(results, "查询失败或无数据");
)).orElseThrow(() -> new GlobalException("xxx"));
return results;
}
/**
* 添加执行记录
*
......
package org.matrix;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.matrix.database.entity.ExecutionRecord;
import org.matrix.database.service.IExecutionRecordService;
import org.matrix.exception.GlobalException;
import org.matrix.socket.enums.TestExecuteType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Optional;
/**
* Unit test for simple App.
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class AppTest {
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论