1. 前言
最近在正式系统每次演示之前总是会出现BUG,然后再领导的带领下写了这么个系统
主要实现的功能如下
- 创建项目
包括项目负责人,登录方式,负责人邮箱,手机号等,方便测试结果推送
- 创建项目环境
针对正式环境,测试环境等多环境单独配置,账号密码,获取token的地址等
- 添加接口列表
给项目添加接口列表,后续创建测试计划的时候可以针对不同的接口进行测试
- 创建测试计划
针对项目创建测试计划,是否发送邮件,执行几次,配置cron表达式等。
支持动态增减定时任务,测试完成后发送测试报告
支持websocket实时查看测试结果
- 测试报告查询
2. ER 图
- 项目信息表
- 项目环境表
- 环境变量表
- 项目接口表
- 测试计划表
- 计划接口详情表
- 测试计划执行记录表
- 执行记录接口结果表
- 定时任务表
太懒了
一个项目对应多个接口,
一个项目对应多个环境。
一个项目对应多个测试计划。
每个环境对应多个环境变量。
一个测试计划对应多个接口。
一个测试计划对应多个执行结果
每个执行结果对应多条接口执行记录
多条执行记录合并成一个测试报告
3. 关键代码记录
1. 动态增减定时任务
这里用的是Quartz,本来准备用xxlJob呢后来就给放弃了
1. 数据库
稍微简化一下,总的来说就是创建任务的时候会有一个
plan_id(任务id)
,这个组别可以是项目id也可以是其他的,反正 plan_id + group_name 组合起来是唯一的就行
mysql
create table tb_task(
id int primary key auto_increment,
plan_id int comment '计划id',
group_name varchar(32) comment '组别',
cron_expression varchar(30) comment '定时任务表达式'
)
2. 启动代码自动添加定时任务
java
import net.lesscoding.testing.entity.PlanTask;
import net.lesscoding.testing.mapper.PlanTaskMapper;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import java.util.List;
/**
* @author eleven
* @date 2023/11/10 9:43
* @apiNote
*/
@Configuration
@Slf4j
public class ScheduledRunner implements ApplicationRunner {
@Autowired
private PlanTaskMapper planTaskMapper;
@Autowired
private SchedulerFactoryBean schedulerFactoryBean;
@Override
public void run(ApplicationArguments args) throws Exception {
// 查询表中没有被逻辑删除的计划
List<PlanTask> planTaskList = planTaskMapper.selectList(new QueryWrapper<PlanTask>().eq("del_flag", false));
// 获取quartz的执行器
Scheduler scheduler = schedulerFactoryBean.getScheduler();
// 批量创建定时任务
if (CollUtil.isNotEmpty(planTaskList)) {
for (PlanTask planTask : planTaskList) {
JobDetail jobDetail = JobBuilder.newJob(TestingJob.class)
.withIdentity(planTask.getPlanId(), planTask.getGroupName())
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(planTask.getPlanId(), planTask.getGroupName())
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule(planTask.getCronExpression()))
.build();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
}
}
}
}
3. 创建测试JOB
java
import net.lesscoding.testing.entity.TestingPlan;
import net.lesscoding.testing.service.TestingPlanService;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.quartz.impl.triggers.CronTriggerImpl;
import org.springframework.beans.factory.annotation.Autowired;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
/**
* @author eleven
* @date 2023/11/10 10:01
* @apiNote
*/
@Slf4j
public class TestingJob implements Job {
@Autowired
private TestingPlanService planService;
@Override
public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
JobDetail jobDetail = jobExecutionContext.getJobDetail();
JobKey key = jobDetail.getKey();
String planId = key.getName();
log.info("{}trigger {}", planId, ((CronTriggerImpl) jobExecutionContext.getTrigger()).getCronExpression());
log.info("{} jobKey {} time {}", planId, key, LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
log.info("定时任务开始执行测试计划");
TestingPlan plan = new TestingPlan();
plan.setId(planId);
try {
planService.runPlan(plan);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
4. 动态增减定时任务
这里就是变更计划的时候,要变更quartz里边的job
java
import net.lesscoding.testing.service.PlanTaskService;
import net.lesscoding.testing.common.Result;
import net.lesscoding.testing.common.ResultFactory;
import net.lesscoding.testing.entity.PlanTask;
import net.lesscoding.testing.entity.TestingPlan;
import net.lesscoding.testing.mapper.PlanTaskMapper;
import net.lesscoding.testing.runner.TestingJob;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Service;
/**
* @author eleven
* @date 2023/11/10 10:48
* @apiNote
*/
@Service
@Slf4j
public class PlanTaskServiceImpl extends ServiceImpl<PlanTaskMapper, PlanTask> implements PlanTaskService {
@Autowired
private PlanTaskMapper taskMapper;
@Autowired
private SchedulerFactoryBean schedulerFactoryBean;
// 根据测试计划编辑定时任务
@Override
public int updateTaskByPlan(TestingPlan plan) throws SchedulerException {
PlanTask planTask = taskMapper.selectOne(new QueryWrapper<PlanTask>()
.eq("del_flag", false)
.eq("plan_id", plan.getId()));
if (planTask == null) {
planTask = new PlanTask();
planTask.setPlanId(plan.getId());
planTask.setGroupName(plan.getPlanName());
planTask.setCronExpression(plan.getCron());
}
// 编辑定时任务
editPlanTask(planTask);
return 1;
}
// 创建新的定时任务
private void createNewScheduler(PlanTask task) throws SchedulerException {
log.info("开始执行创建新任务");
Scheduler scheduler = schedulerFactoryBean.getScheduler();
JobKey jobKey = jobKey(task);
JobDetail jobDetail = JobBuilder.newJob(TestingJob.class)
.withIdentity(jobKey)
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.withIdentity(task.getPlanId(), task.getGroupName())
.startNow()
.withSchedule(CronScheduleBuilder.cronSchedule(task.getCronExpression()))
.build();
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
log.info("任务创建完成");
}
/**
* 更新job 直接删除再创建一个新
*/
private void updateJob(PlanTask task) throws SchedulerException {
deleteJob(task);
createNewScheduler(task);
log.info("更新成功");
}
/**
* 删除job
* @param task
* @throws SchedulerException
*/
public boolean deleteJob(PlanTask task) throws SchedulerException {
Scheduler scheduler = schedulerFactoryBean.getScheduler();
JobKey jobKey = jobKey(task);
boolean deleteJob = scheduler.deleteJob(jobKey);
log.info("当前 jobKey {} 删除结果{}", jobKey, deleteJob);
return deleteJob;
}
private JobKey jobKey(PlanTask task) {
JobKey jobKey = new JobKey(task.getPlanId(), task.getGroupName());
log.info("当前任务 {}, jobKey{}", task, jobKey);
return jobKey;
}
}
2. 生成测试报告
有两种格式的,一种md的,一种html 的。怎么生成pdf没搞明白。这个用的是velocity模板引擎
1. 导入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-velocity</artifactId>
<version>1.4.7.RELEASE</version>
</dependency>
2. 模板
1. md模板
velocity
# 1. $result.title
```mermaid
pie title 统计结果
"成功" : ${result.successNum}
"失败" : ${result.failedNum}
```
- 统计结果
| | 接口总数 | 成功 | 失败 | 成功率 |
| ---- | -------- | ---- | ---- | ------ |
| 统计 | ${result.requestTotal} | ${result.successNum} | ${result.failedNum} | ${result.successRate} |
- 总览
| 描述 | 值 |
| -------------- |---------------------------|
| 开始时间 | ${result.startTime} |
| 结束时间 | ${result.endTime} |
| 总耗时 | ${result.timeTotal}ms |
| 总响应时间 | ${result.allTime}ms |
| 平均响应时间 | ${result.perTime}ms |
| 总响应数据大小 | ${result.responseSizeStr} |
| 未测接口 | ${result.waitNum} |
# 2. 失败列表
#foreach($item in $result.failedResultList)
<h2> $foreach.index $item.interfaceName</h2>
$item.interfaceName
- 调用详情
| 请求方式 | 请求路径 | 是否成功 | 状态码 | 响应时长 | 响应大小 |
|----------------|--------| -------- |------------|--------|-----------------------|
| `$item.method` | `$item.requestUri` | #if($item.success):white_check_mark:#else :x:#end | `$item.code` | $item.responseTime | $item.responseSizeStr |
- 请求参数
```json
$item.paramsJson
```
- 响应结果
```json
$item.responseJson
```
#end
# 3. 接口列表
#foreach($item in $result.allResultList)
<h2>$foreach.index $item.interfaceName</h2>
- 调用详情
| 请求方式 | 请求路径 | 是否成功 | 状态码 | 响应时长 | 响应大小 |
|----------------|--------| -------- |------------|--------|-----------------------|
| `$item.method` | `$item.requestUri` | #if($item.success):white_check_mark:#else :x:#end | `$item.code` | $item.responseTime | $item.responseSizeStr |
#if($item.paramsJson != '')
- 请求参数
```json
$item.paramsJson
```
#end
#end
2. html模板
ve
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>$result.title</title>
<style type="text/css">
/* 清除默认样式的代码 */
/* 去除常见标签默认的 margin 和 padding */
body,
h1,
h2,
h3,
h4,
h5,
h6,
p,
ul,
ol,
li,
dl,
dt,
dd,
input {
margin: 0;
padding: 0;
}
/* 內减模式 */
* {
box-sizing: border-box;
}
/* 设置网页统一的字体大小、行高、字体系列相关属性 */
body {
font: 16px/1.5 "Helvetica Neue", Helvetica, Arial, "Microsoft Yahei",
"Hiragino Sans GB", "Heiti SC", "WenQuanYi Micro Hei", sans-serif;
color: #333;
}
/* 去除列表默认样式 */
ul,
ol {
list-style: none;
}
/* 去除默认的倾斜效果 */
em,
i {
font-style: normal;
}
/* 去除a标签默认下划线,并设置默认文字颜色 */
a {
text-decoration: none;
color: #333;
}
/* 设置img的垂直对齐方式为居中对齐,去除img默认下间隙 */
img {
vertical-align: middle;
}
/* 去除input默认样式 */
input {
border: none;
outline: none;
color: #333;
}
/* 左浮动 */
.fl {
float: left;
}
/* 右浮动 */
.fr {
float: right;
}
/* 双伪元素清除法 */
.clearfix::before,
.clearfix::after {
content: "";
display: table;
}
.clearfix::after {
clear: both;
}
.report-box {
overflow: hidden;
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
height: 100%;
background: rgb(233, 238, 243);
}
.report-content {
overflow-y: auto;
width: 75%;
background: #fff;
padding: 30px;
}
.report-content-title {
height: 100px;
font-size: 30px;
font-weight: bold;
display: flex;
/* justify-content: center; */
align-items: center;
}
.report-content-result {
padding-top: 15px;
font-size: 18px;
border-top: 2px dashed #ccc;
border-bottom: 2px dashed #ccc;
}
.report-result-chart {
display: flex;
}
.api-title {
padding: 13px 0 0 15px;
font-size: 16px;
color: #666666;
display: flex;
flex-direction: column;
span {
margin-bottom: 10px;
}
:nth-child(1) {
font-size: 18px;
}
}
.table-box {
padding-top: 10px;
text-align: center;
}
.report-result-list > ul {
width: 100%;
display: flex;
flex-wrap: wrap;
font-size: 14px;
color: #666666;
padding-top: 15px;
}
.api-info-item {
width: 25%;
height: 30px;
}
.detial-info-title {
padding-top: 20px;
display: flex;
align-items: center;
}
.detial-info-title-state {
width: 18px;
height: 18px;
background: #f2f2f2;
display: flex;
align-items: center;
justify-content: center;
margin-right: 10px;
}
.detial-info-title-state-img {
width: 10px;
height: 5px;
}
.report-resulr-detail {
font-size: 16px;
}
.detial-info-list-api {
width: 100%;
margin-top: 10px;
height: 30px;
background: #f0eeee;
display: flex;
align-items: center;
}
.detial-info-list-api > span {
margin-right: 20px;
}
.api-info-item-var {
font-size: 14px;
color: #666666;
}
</style>
</head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/echarts/5.4.3/echarts.min.js"></script>
<body>
<div class="report-box">
<div class="report-content">
<div class="report-content-title">$result.title</div>
<div class="report-content-result">
<img src="$result.reportUrl" alt=""/>
<span style="color: rgb(246, 115, 12)">测试结果</span>
<div class="report-result-chart">
<div id="chart" style="width: 100px; height: 100px"></div>
<div class="api-title">
<span>接口</span>
<span>接口通过率:<span>$result.successRate</span></span>
<span>接口失败率:<span>$result.failedRate</span></span>
</div>
</div>
<div class="table-box">
<table
border="1"
bordercolor="#ccc"
width="100%"
height="100"
align="center"
cellspacing="0"
cellpadding="0"
>
<tr>
<td width="25%"></td>
<td width="25%">请求总数</td>
<td width="25%">通过</td>
<td width="25%">失败</td>
</tr>
<tr>
<td>接口</td>
<td>${result.requestTotal}</td>
<td>${result.successNum}</td>
<td>${result.failedNum}</td>
</tr>
</table>
</div>
<div class="report-result-list">
<ul>
<li class="api-info-item">
开始时间:<span>${result.startTime}</span>
</li>
<li class="api-info-item">
结束时间:<span>${result.endTime}</span>
</li>
<li class="api-info-item">总耗时:<span>${result.timeTotal}ms</span></li>
<li class="api-info-item">总响应时间:<span>${result.allTime}ms</span></li>
<li class="api-info-item">
平均响应时间:<span>${result.perTime}ms</span>
</li>
<li class="api-info-item">
总响应数据大小: <span>${result.responseSizeStr}</span>
</li>
<li class="api-info-item">未测接口:<span>${result.waitNum}</span></li>
</ul>
</div>
</div>
<div class="report-resulr-detail">
<img src="$result.reportUrl" alt=""/>
<span style="color: rgb(246, 115, 12); font-size: 16px"
>测试情况</span
>
<div>
<div class="detial-info-title">
<div class="detial-info-title-state">
<img
src="$result.arrowUrl"
alt=""
class="detial-info-title-state-img"
/>
</div>
<span>失败情况</span>
</div>
#foreach($item in $result.failedResultList)
<div>
<div class="detial-info-list-api">
<img
src="$result.errorUrl"
alt=""
style="width: 18px; height: 18px; margin-right: 20px"
/>
<span>$foreach.index </span>
<span style="color: #26cea4"> $item.method</span>
<span>$item.interfaceName</span>
<span style="color: blue"
>$item.requestUri</span
>
</div>
<div class="report-result-list">
<ul>
<li class="api-info-item">请求状况:<span>#if($item.success) 成功 #else 失败 #end</span>
</li>
<li class="api-info-item">状态码:<span>$item.code</span></li>
<li class="api-info-item">响应时间:<span>$item.responseTime</span></li>
<li class="api-info-item">相应数据大小:<span>$item.responseSizeStr</span></li>
</ul>
<div class="api-info-item-var">
<div>
请求参数:<span
>$item.paramsJson</span
>
</div>
<div style="margin-top: 4px">
响应数据:<span
>$item.responseJson</span
>
</div>
</div>
</div>
</div>
#end
</div>
<div>
<div class="detial-info-title">
<div class="detial-info-title-state">
<img
src="$result.arrowUrl"
alt=""
class="detial-info-title-state-img"
/>
</div>
<span>测试详情</span>
</div>
#foreach($item in $result.allResultList)
<div>
<div class="detial-info-list-api">
<img
src="#if($item.success) $result.successUrl #else $result.errorUrl #end"
alt=""
style="width: 18px; height: 18px; margin-right: 20px"
/>
<span>$foreach.index </span>
<span style="color: #26cea4"> $item.method</span>
<span>$item.interfaceName</span>
<span style="color: blue"
>$item.requestUri</span
>
</div>
<div class="report-result-list">
<ul>
<li class="api-info-item">请求状况:<span>#if($item.success) 成功 #else 失败 #end</span>
</li>
<li class="api-info-item">状态码:<span>$item.code</span></li>
<li class="api-info-item">响应时间:<span>$item.responseTime</span></li>
<li class="api-info-item">相应数据大小:<span>$item.responseSizeStr</span></li>
</ul>
<div class="api-info-item-var">
<div>
请求参数:<span
>$item.paramsJson</span
>
</div>
#if(!$item.success)
<div style="margin-top: 4px">
响应数据:<span
>$item.responseJson</span
>
</div>
#end
</div>
</div>
</div>
#end
</div>
</div>
</div>
</div>
<script type="text/javascript">
var Chart = echarts.init(document.getElementById("chart"));
var option = {
tooltip: {
trigger: "item",
},
series: [
{
color: ["#26CEA4", "#FA5A5A"],
name: "接口",
type: "pie",
radius: ["50%", "90%"],
avoidLabelOverlap: false,
label: {
show: false,
position: "center",
},
emphasis: {
label: {
show: true,
fontSize: 16,
fontWeight: "bold",
},
},
labelLine: {
show: false,
},
data: [
{value: 1048, name: "接口通过率"},
{value: 735, name: "接口失败率"},
],
},
],
};
Chart.setOption(option);
</script>
</body>
</html>
3. 导出相应的测试报告
每次测试计划执行之后都会有一个执行记录,也就是这个testingPlanRunLog。
根据这个runLog查询到相应的结果记录。
java
@Override
public void exportRunLogResult(TestingPlanRunLog runLog, HttpServletResponse response) throws IOException, DocumentException {
Integer exportType = runLog.getExportType();
if (exportType == null) {
runLog.setExportType(1);
}
String ext = "";
switch (runLog.getExportType()) {
case 0 :
ext = "md";
break;
case 2 :
ext = "pdf";
break;
default:
ext = "html";
}
String fileName = "导出结果." + ext;
ServletOutputStream outputStream = response.getOutputStream();
response.setHeader("Content-Disposition", "attachment;filename="+ URLEncoder.encode(fileName, "UTF-8"));
response.setHeader("Connection", "close");
response.setHeader("Content-Type", "application/octet-stream");
//TestingResultVo testingResultVo = testResultTemplate(runLog);
TestingResultVo testResultVO = testingResultMapper.getTestingResultById(runLog);
exportUtil.writeToOutputStream(outputStream, testResultVO, exportType);
}
- 导出工具
java
import net.lesscoding.testing.model.vo.TestingResultVo;
import com.itextpdf.text.Document;
import com.itextpdf.text.DocumentException;
import com.itextpdf.text.html.simpleparser.HTMLWorker;
import com.itextpdf.text.pdf.PdfWriter;
import lombok.extern.slf4j.Slf4j;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;
import org.springframework.stereotype.Component;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
/**
* @author eleven
* @date 2023/11/16 15:46
* @apiNote
*/
@Component
@Slf4j
public class PlanResultExportUtil {
public void writeToOutputStream(OutputStream outputStream, TestingResultVo resultVo, Integer type) throws IOException, DocumentException {
String templatePath = type == 0 ? "templates/planResult.md.vm" : "templates/planResult.html.vm";
log.info("当前类型 {}, 模板地址{}", type, templatePath);
Template template = buildTemplate(templatePath);
PrintWriter writer = new PrintWriter(outputStream);
template.merge(resultContext(resultVo), writer);
log.info("当前data {}", writer.toString());
// md 和 html 直接导出
if (type != 2) {
writer.flush();
writer.close();
} else {
// todo 暂时无法直接从 writer生成pdf
}
}
/**
* 根据结果获取velocity的模板
* @param resultVo 执行结果
* @return
*/
private VelocityContext resultContext(TestingResultVo resultVo) {
Map<String, Object> contextMap = new HashMap<>(1);
contextMap.put("result", resultVo);
return new VelocityContext(contextMap);
}
public static Template buildTemplate(String path){
Properties properties = new Properties();
// 这里当然可以配置在yml里边
properties.put("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
properties.put("directive.foreach.counter.name", "velocityCount");
properties.put("directive.foreach.counter.initial.value", "1");
Velocity.init(properties);
return Velocity.getTemplate(path,"utf-8");
}
}
3. 生成邮件通知
这个用的是hutool的东西,更是low的一笔。页面丑的我自己都不想看
1. 申请秘钥
各大邮箱都可以开通pop3 stmp等等的
2. 创建配置
在resources下创建mail.settings
文件
ini
host=smtp.qq.com
port=465
from=XXX@qq.com
user=xxx
pass=刚才获取到的授权码
sslEnable=true
3. 创建一个非常丑的html
应该也能用
velocity
, 不过我懒
html
<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
.container {
text-align: center;
}
.header {
font-size: 30px;
font-weight: bold;
font-family: 黑体;
}
.tips {
font-size: 16px;
font-family: 宋体;
color: darkgrey;
}
.bold {
font-weight: bold;
font-size: 18px;
font-family: 黑体;
text-decoration: underline;
}
.success {
color: greenyellow;
}
.failed {
color: red;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<p><span class="bold">{projectName}-{dateStr} 报告邮件</span></p>
<hr>
</div>
<div class="mid">
<div>
<p>
尊敬的用户【<span class="bold">{projectLeader}</span>】您好:
</p>
<p>
您所负责的【<span class="bold">{projectName}</span> 】项目<br>,
于 <span class="bold">{dateStr}</span>自动执行测试计划<span class="bold">{planName}</span>,环境<span class="bold">{profileName}</span><br>
本次共执行了<span class="bold">{interfaceTotal}</span>个接口<br>
成功<span class="bold success">{successNum}</span>个,失败<span class="bold failed">{failedNum}</span>个<br>
详情请点击此链接查看<a href="{detailUrl}">{projectName}{dateStr}</a>
</p>
</div>
<div class="tips">
<p>( 如不是本人操作,请联系统管理员 请勿直接回复本邮件 )</p>
<br>
<hr>
</div>
</div>
</div>
<p>
</p>
</body>
</html>
4. 发送邮件
总的来说就是执行完成之后,将结果拼接好 替换html里边的内容就行了
java
import net.lesscoding.testing.model.vo.EmailContentVo;
import net.lesscoding.testing.model.vo.TestingResultVo;
import cn.hutool.core.io.resource.ClassPathResource;
import cn.hutool.extra.mail.MailUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;
/**
* @author eleven
* @date 2023/11/16 14:17
* @apiNote
*/
@Component
@Slf4j
public class EmailUtil {
public Boolean sendResultEmail(EmailContentVo vo) throws IOException {
String template = template();
String content = replaceTemplate(template, getContextMap(vo));
log.info("当前邮件内容 {}", content);
String send = MailUtil.send(vo.getLeaderEmail(), vo.getSubject(), content, null, true, null);
return false;
}
private Map<String, String> getContextMap(EmailContentVo vo) {
Map<String,String> resultMap = new HashMap<>(7);
resultMap.put("{projectName}", vo.getProjectName());
resultMap.put("{projectLeader}", vo.getProjectLeader());
resultMap.put("{planName}", vo.getPlanName());
resultMap.put("{profileName}", vo.getProfileName());
resultMap.put("{dateStr}", vo.getDateStr());
resultMap.put("{interfaceTotal}", vo.getInterfaceTotal().toString());
resultMap.put("{successNum}", vo.getSuccessNum().toString());
resultMap.put("{failedNum}", vo.getFailedNum().toString());
resultMap.put("{detailUrl}", vo.getDetailUrl().toString());
return resultMap;
}
private String template() throws IOException {
ClassPathResource resource = new ClassPathResource("templates/planCompleted.html");
InputStream stream = resource.getStream();
// 读取文件内容为字节数组
byte[] fileContent = Files.readAllBytes(resource.getFile().toPath());
// 将字节数组转换为字符串
return new String(fileContent, StandardCharsets.UTF_8);
}
/**
* 替换模板
*
* @param template 模板内容
* @param contextMap 替换内容
*/
private String replaceTemplate(String template, Map<String, String> contextMap) {
for (String key : contextMap.keySet()) {
template = template.replace(key, contextMap.get(key));
}
return template;
}
}
4. 实时调用获取结果
需要创建一个Websocket服务,这个真是折磨了我很久啊。
1. 引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2. 创建Websocket配置
1. 消息处理
java
package net.lesscoding.testing.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* @author 24962
*/
@Component
@Slf4j
public class MyWebSocketHandler extends TextWebSocketHandler {
public static List<WebSocketSession> list = new ArrayList<>();
public List<WebSocketSession> getSessionList() {
return list;
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 当WebSocket连接建立时调用
list.add(session);
log.info("websocket链接已建立");
}
public void sendMessageToSession(WebSocketSession session, String message) throws Exception {
// 向指定会话发送消息
session.sendMessage(new TextMessage(message));
}
public void broadcastMessage(String message) throws Exception {
// 广播消息到所有会话
for(Iterator<WebSocketSession> sessionIterator = list.listIterator(); sessionIterator.hasNext();) {
WebSocketSession session = sessionIterator.next();
if (session != null && session.isOpen()) {
session.sendMessage(new TextMessage(message));
} else {
sessionIterator.remove();
}
}
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
// 当接收到WebSocket消息时调用
String receivedMessage = (String) message.getPayload();
// 处理接收到的消息
log.info("当前服务端收到消息 {}", receivedMessage);
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
// 当WebSocket传输错误时调用
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
log.info("当前链接关闭 {}, 状态 {}", session, closeStatus);
// 当WebSocket连接关闭时调用
list.remove(session);
}
@Override
public boolean supportsPartialMessages() {
return false;
}
}
2. websocket配置
超时自动断开连接, 配置websocket端口等等。
现在的端口为
ws://{host}:{port}/{context-path}/websocket
java
package net.lesscoding.testing.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
/**
* @author 24962
*/
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{
@Autowired
private WebsocketInterceptor websocketInterceptor;
// 配置连接端口
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 指定WebSocket处理程序和路径
registry.addHandler(new MyWebSocketHandler(), "/websocket")
// 特别注意下边这两个行, 否则js原生的 new WebSocket链接不上 淦!!!!
.setAllowedOrigins("*")
.addInterceptors(websocketInterceptor);
}
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(512000);
container.setMaxBinaryMessageBufferSize(512000);
// 30s 活跃
container.setMaxSessionIdleTimeout(30 * 1000L);
return container;
}
}
3. 拦截器
这个没怎么用上
java
package net.lesscoding.testing.config;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
/**
* @author eleven
* @date 2023/11/15 12:16
* @apiNote
*/
@Component
public class WebsocketInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
System.out.println("前置拦截");
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
System.out.println("后置拦截");
}
}
3. 实时调用结果
总的来说呢,就是在这个
runPlan
的方法里边每次HttpClient调用了一个请求之中,都算出来结果成功数量,失败数量,等待数量,成功率,每个接口的返回结果等
然后调用websocket的广播发送给前端就行了
单播其实也能实现,就是懒
java
/**
* 广播消息
* @param prefixUrl
* @param timer
* @param socketResult
* @param item
* @param responseStr
* @throws Exception
*/
private void broadcastSocketMessage(String prefixUrl, TimeInterval timer, SocketResultVo socketResult,
ProjectInterface item, String responseStr) throws Exception {
// 有websocket链接再进行广播
ResultVo resultVo = new ResultVo();
resultVo.setMethod(item.getMethod());
resultVo.setInterfaceName(item.getInterfaceName());
resultVo.setRequestUri(String.format("%s%s", prefixUrl, item.getInterfaceUri()));
resultVo.setSuccess(responseStr.contains(Consts.SUCCESS_CODE.toString()));
resultVo.setCode(PlanResultUtil.getResponseCode(responseStr));
resultVo.setResponseTime(timer.intervalMs(item.getId()));
resultVo.setResponseSize((long)responseStr.length());
resultVo.setResponseJson(responseStr);
socketResult.addResultVo(resultVo);
log.info("当前执行方法 {}, 当前对象{}", item.getInterfaceUri(), socketResult);
String socketMsg = gson.toJson(socketResult);
socketHandler.broadcastMessage(socketMsg);
log.info("websocket广播消息{}", socketMsg);
}