Skip to content

Latest commit

 

History

History
653 lines (439 loc) · 20.4 KB

逻辑梳理.MD

File metadata and controls

653 lines (439 loc) · 20.4 KB

后端框架

springboot+mybatis+mysql

架构示意图

image

用户管理

注册-密码,MD5加密存储

  String newPwd = DigestUtils.md5DigestAsHex((UserBaseStr.md5Hex_sign + TestUser.getUserName() + TestUser.getPassword()).getBytes());
  TestUser.setPassword(newPwd);

登录

1,将用户录入的用户名密码,M5加密

2,根据用户名+加密后的密码,去数据库查询是否存在

3,不存在,给出提示

4,存在,生成token,并将token存入TokenDto,再将TokenDto存入tokenDb

5,接口返回 token

    String tokenStr = DigestUtils.md5DigestAsHex((System.currentTimeMillis() + userDto.getName() + userDto.getPwd()).getBytes());
    TokenDto tokenDto = new TokenDto();
    tokenDto.setUserId(resultTestUser.getId());
    tokenDto.setUserName(resultTestUser.getUserName());
    tokenDto.setDefaultJenkinsId(resultTestUser.getDefaultJenkinsId());
    tokenDto.setToken(tokenStr);

    tokenDb.addUserInfo(tokenStr,tokenDto);

    AitestToken aitestToken = new AitestToken();
    aitestToken.setToken(tokenStr);

    return ResultDto.success("成功",aitestToken);

判断是否登录

1,从请求的Header获取客户端附加token

2,从tokenDb中取出tokenDto

3,返回tokenDto

    String tokenStr = request.getHeader(UserBaseStr.LOGIN_TOKEN);
    TokenDto tokenDto = tokenDb.getUserInfo(tokenStr);
    return ResultDto.success("成功", tokenDto);

登出

1,判断是否登录,未登录不用登出

2,已登录,则从tokenDb删除tokenDto

全局鉴权

1,从请求的Header中获取客户端附加的token

2,判断如果是登录或注册接口,直接放行

3,如果请求中没有token,抛出异常“客户端没有传token”

4,如果请求中有token,根据token去tokenDb中查询是否已登录

5,查询成功,则放行;查询失败,则抛出异常用户未登录

@Component
@Slf4j
public class DemoInterceptor implements HandlerInterceptor {
   
@Autowired
private TokenDb tokenDb;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    log.info("=== preHandle ===");
    log.info("=== request.getRequestURI() ===" + request.getRequestURI());

    //1、从请求的Header中获取客户端附加的token
    String token = request.getHeader("token");
    String uri=request.getRequestURI();

    if ("/TestUser/#".equals(uri)||"/TestUser/register".equals(uri) ){
        return true;
    }

    //2、如果请求中无token,响应码设401,抛出业务异常:客户端没有传token
    if (Objects.isNull(token)||token==""){
        ServiceException.throwEx("客户端未传token");
    }

    //3、从token中根据token查询TokenDto
    boolean loginFlag = tokenDb.isLogin(token);

    //如果为空,则响应码设置401,抛出业务异常
    //用户未登录
    if (!loginFlag){
        ServiceException.throwEx("用户未登录");
    }


    //否则,则允许通过


    return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
    log.info("=== postHandle ===");
    log.info("=== request.getRequestURI() ===" + request.getRequestURI());
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    log.info("=== afterCompletion ===");
    log.info("=== request.getRequestURI() ===" + request.getRequestURI());
 }
}

拦截器设置

@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

@Autowired
private DemoInterceptor demoInterceptor;

@Override
protected void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(demoInterceptor).addPathPatterns("/**");
 }
}

用例管理

添加用例

不同类型用例处理

1,文本类型用例,调用/text接口,传入参数@RequestBody为 用例名称(必填)、测试数据(必填)、备注

2,文件类型用例,调用/file接口,传入参数@RequestParam为 测试文件(必填),用例名称备注,测试数据不传值

存储用例

1,将传入的参数 addTestCaseDto,复制到TestCase entity类;

2,header中取出token,从 tokenDb 中查出 userId

3,传入userId

4,setCreateTime、setUpdateTime

5,设置删除标志 setDelFlag 为 1(未删除)

注:其他对象存储过程到数据库的过程,也类似

删除用例

1,调用/{caseId}接口,传入参数 @PathVariable 为 用例id

2,header中取出token,从 tokenDb 中查出 userId

3,TestCase entity 中设置 caseId、createUserId,设置删除标志位1

4,利用TestCaseMapper类查找数据库,是否存在 ”caseId、createUserId,删除标志位1“ 都相符的数据

5,没有查询到则抛出异常”未查到测试用例信息“

6,查询到了就 setDelFlag 标志位置为0,并删除该条记录

任务管理

添加任务

1,传入参数任务名称任务备注运行测试的Jenkins服务器id创建者id(客户端传值无效,以token数据为准)、测试用例id(List)

2,校验传参:

(1)测试用例 id list 非空 (2)若传入的任务名称为空,则设置默认名称

4,从tokenDto中取出 UserId、DefaultJenkinsId

3,根据 DefaultJenkinsId 查询 Jenkisn 信息,并做非空校验

4,根据用户选择的测试用例id列表,查询测试用例信息列表

5,生成测试命令: 根据 Jenkins 信息、测试用例信息(可能不止一个)生成测试命令

  • 从 TestJenkins(entity)对象中,取出commandRunCaseType
  • 从 TestJenkins(entity)对象中,取出测试命令
  • 从 TestCase(entity)对象中,取出测试数据

(1)若commandRunCaseType为1,则为文本类型,拼接后形如

mvn test Class1
mvn test Class2
mvn test Class3

(2)若commandRunCaseType为1,则为文件类型

  • 先拼接下载文件的curl命令
  • 再拼接命令+用例名称

拼接后形如

curl -o xxx.yml ${BaseUrl}/testCase/data/${id} -H \"token: ${token}\" || true\n\n
hrun --alluredir=${WORKSPACE}/target/allure-results xxx.yml || true \n\n

curl -o xxx.yml ${BaseUrl}/testCase/data/${id} -H \"token: ${token}\" || true\n\n
hrun --alluredir=${WORKSPACE}/target/allure-results xxx.yml || true \n\n

curl -o xxx.yml ${BaseUrl}/testCase/data/${id} -H \"token: ${token}\" || true\n\n
hrun --alluredir=${WORKSPACE}/target/allure-results xxx.yml || true \n\n

6,落库测试任务表

7,落库任务-用例关联表

执行测试任务

1,传入参数 任务id命令脚本

3,header 中获取 token ,利用tokenDto getUserId、getDefaultJenkinsId、getTestCommand、getTaskId ,可以拿到任务信息、测试命令等数据。

4,根据请求request,拿到 url

6,更新任务状态为执行中

7,以从数据库查询的数据为基础数据,组装调用Jenkins的参数

    OperateJenkinsJobDto operateJenkinsJobDto = new OperateJenkinsJobDto();
    operateJenkinsJobDto.setTokenDto(tokenDto);
    operateJenkinsJobDto.setTestJenkins(resultTestJenkins);
    operateJenkinsJobDto.setParams(map);
    operateJenkinsJobDto.setTestUser(resultTestUser);

    ResultDto<TestUser> resultDto = JenkinsUtil.build2(operateJenkinsJobDto);

8,获取Jenkins Job对应的配置文件

    ClassPathResource classPathResource = new ClassPathResource("JenkinsConfigDir/test_mini_test_start.xml");
    InputStream inputStream = classPathResource.getInputStream();
    String jobConfigXml = IOUtils.toString(inputStream, StandardCharsets.UTF_8);

9,通过Jenkins客户端连接Jenkins

    String baseUrl = testJenkins.getUrl();
    String userName = testJenkins.getUserName();
    String password = testJenkins.getPassword();

    String jobName = "test_mini_test_start_" + testUser.getId();

    JenkinsHttpClient jenkinsHttpClient = new JenkinsHttpClient(new URI(baseUrl), userName, password);

10, 通过Jenkins客户端获取JenkinsServer对象

    JenkinsServer jenkinsServer = new JenkinsServer(jenkinsHttpClient);

11, 通过JenkinsServer对象创建或者更新Jenkins Job

    if (StringUtils.isEmpty(testUser.getStartTestJobName())) {
        jenkinsServer.createJob(jobName, jobConfigXml, true);
        testUser.setStartTestJobName(jobName);
    } else {
        jenkinsServer.updateJob(jobName, jobConfigXml, true);
    }

12, 根据Job名称获取对应的Job

    Map<String, Job> jobMap = jenkinsServer.getJobs();
    Job job = jobMap.get(jobName);

13,通过Jenkins Job对象构建job

job.build(params, true);

14,数据返回给前端

执行完成回传

注意,上面的执行过程,Jenkins 完成 job 构建后,还有1个重要的功能点:

  • 需要设置完成状态为已完成

    在Jenkins job中调用更改status接口

     curl -X PUT "http://localhost:8081/task/status " -H "Content-Type: application/json " -H "token: acab4db17fe7f898c9f5dd2f726a7f3c" -d '{"buildUrl":"${BUILD_URL}","taskId":80,"status":3}'
    

    image

    image

Jenkins 管理

TestJenkins entity 的 defaultJenkinsFlag 字段不存入数据库

是否设置为默认服务器 1 是 0 否

添加 Jenkisn 时,如果是否为默认Jenkins的标志位为1,则修改test_user表中的default_jenkins_id为当前 JenkinsId

resultTestUser.setDefaultJenkinsId(testJenkins.getId());

同时,记得修改tokenDto中的default_jenkins_id

tokenDto.setDefaultJenkinsId(testJenkins.getId());

report 管理

Jenkins Job报告回传

任务已完成状态,需要取到报告

/allureReport/{taskId} 接口获取

主要逻辑:

public static String getAllureReportUrl(String buildUrl,TestJenkins testJenkins,boolean autoLoginJenkinsFlag) {
    if (StringUtils.isEmpty(buildUrl) || !buildUrl.contains("/job")) {
        return buildUrl;
    }

    String allureReportUrl = buildUrl;
    if (autoLoginJenkinsFlag) {
        allureReportUrl = getAllureReportUrlAndLogin(buildUrl, testJenkins);
    }
    return allureReportUrl + "allure/";
}

折线图

查询语句

<select id="getCaseCountByTask" resultType="TestTask">
    select * FROM test_task where create_user_id = #{createUserId} ORDER BY id
    <if test="start != null and end != null ">
        limit #{start}, #{end}
    </if>
</select>

统一响应大对象

@ApiModel(value = "基础返回类", description = "基础返回类")
public class ResultDto<T> implements Serializable {
private static final long serialVersionUID = -7472879865481412372L;

@ApiModelProperty(value = "返回结果码 1成功 0失败", required = true, example = "1", allowableValues = "1,0")
private Integer resultCode;

@ApiModelProperty(value = "提示信息", example = "成功", allowableValues = "成功,失败")
private String message = "";

@ApiModelProperty(value = "响应结果数据")
private T data = null;

public Integer getResultCode() {
    return resultCode;
}

public static ResultDto newInstance() {
    return new ResultDto();
}

/**
 * 设置成功
 */

public void setAsSuccess() {
    this.resultCode = 1;
}

public String getMessage() {
    return message;
}

public void setMessage(String message) {
    this.message = message;
}

public static ResultDto success(String message) {
    ResultDto resultDto = new ResultDto();
    resultDto.setAsSuccess();
    resultDto.setMessage(message);
    return resultDto;
}

public static <T> ResultDto<T> success(String message, T data) {
    ResultDto<T> resultDto = new ResultDto<>();
    resultDto.setAsSuccess();
    resultDto.setMessage(message);
    resultDto.setData(data);
    return resultDto;
}

/**
 * 设置失败
 */
public void setAsFailure() {
    this.resultCode = 0;
}

public static <T> ResultDto<T> fail(String message) {
    ResultDto<T> resultDto = new ResultDto<>();
    resultDto.setAsFailure();
    resultDto.setMessage(message);
    return resultDto;
}

public static <T> ResultDto<T> fail(String message, T data) {
    ResultDto<T> resultDto = new ResultDto<>();
    resultDto.setAsFailure();
    resultDto.setMessage(message);
    resultDto.setData(data);
    return resultDto;
}

public T getData() {
    return data;
}

public void setData(T data) {
    this.data = data;
}

public static ResultDto main(String[] args) {
    return ResultDto.fail("密码不对", new HashMap<>());
 }
}

异常处理

非空校验

比如,添加用例-用例名称不能为空

image

此时,用统一大对象处理

if (StringUtils.isEmpty(addTestCaseDto.getCaseName())) {
        return ResultDto.fail("测试用例名称不能为空");
    }

业务异常

if (Objects.isNull(testJenkins)) {
        ServiceException.throwEx("组装测试命令时,Jenkins信息为空");
    }

if (Objects.isNull(testCaseList) || testCaseList.size() == 0) {
        throw new ServiceException("组装测试命令时,测试用例列表信息为空");
    }

全局异常

分为业务异常(自定义ServiceException)、非业务异常(Exception)、其他异常(Throwable,如Error)

  • 打印出日志信息
  • 业务异常返回401错误,非业务异常返回502错误
@Slf4j
@ResponseBody
@ControllerAdvice
public class GlobalExceptionHandler {

    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler({ServiceException.class})
    public ResultDto serviceExceptionHandler(ServiceException se){
        log.error(se.getMessage());
        return resultFormat(se) ;
    }

    @ResponseStatus(HttpStatus.BAD_GATEWAY)
    @ExceptionHandler({Exception.class})
    public ResultDto exceptionHandler(Exception e){
        log.error(e.getMessage());
        return resultFormat(e);
    }

    @ExceptionHandler({Throwable.class})
    public ResultDto throwableHandler(Throwable t) {
        log.error(t.getMessage());
        return ResultDto.fail("系统错误 系统繁忙,请稍后重试");
    }

    public ResultDto resultFormat(Throwable t){
        String tips="系统繁忙,请稍后重试";
        if(t instanceof  ServiceException){
            return ResultDto.fail( "业务异常 "+t.getMessage());
        }
            if(t instanceof  Exception){
            return ResultDto.fail("非业务异常 "+t.getMessage());
        }
        return ResultDto.fail(tips);
     }
    }

跨域设置

@Configuration
public class GlobalCorsConfig {
    @Bean
    public CorsFilter corsFilter(){
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();

        config.setAllowCredentials(true);
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");
        config.setMaxAge(18000L);
        config.addAllowedMethod("OPTIONS");
        config.addAllowedMethod("HEAD");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("DELETE");
        config.addAllowedMethod("PATCH");
        config.addAllowedMethod("GET");

        source.registerCorsConfiguration("/**",config);
        return new CorsFilter(source);
 }
}

列表查询

查询语句

<select id="count" resultType="int">
    select count(1) from test_task htt where htt.create_user_id = #{params.createUserId}

    <if test="params.name != null and params.name != '' ">
        and htt.name = #{params.name}
    </if>
</select>


<select id="list" resultType="TestTask">
    select * from test_task htt where htt.create_user_id = #{params.createUserId}

    <if test="params.name != null and params.name != '' ">
        and htt.name = #{params.name}
    </if>
    order by htt.update_time desc
    limit #{pageNum}, #{pageSize}
</select>

入参Dto

@ApiModel(value = "列表查询的分贝参数", description = "请求参数类")
@Data
public class PageTableRequest<Dto> implements Serializable {

    private static final long serialVersionUID = 7328071045193618467L;

    @ApiModelProperty(value = "页码", required = true, example = "1")
    private Integer pageNum = 1;

    @ApiModelProperty(value = "每页数据量", required = true, example = "10")
    private Integer pageSize = 10;

    @ApiModelProperty(value = "特定查询参数", required = true, example = "status=1")
    private Map params;
}

出参Dto

@Data
public class PageTableResponse<T> implements Serializable {
    private static final long serialVersionUID = -7472879865481412372L;

    //返回的数据量
    private Integer recordsTotal;

    //返回的数据列表
    private List<T> data;
}

参数解析

public class PageTableArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        Class<?> cla = parameter.getParameterType();
        return cla.isAssignableFrom(PageTableRequest.class);
}

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {

        HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);

        Map<String, String[]> param = request.getParameterMap();
        PageTableRequest tableRequest = new PageTableRequest();
        if (param.containsKey("pageNum")) {
        tableRequest.setPageNum(Integer.parseInt(request.getParameter("pageNum")));
        }
        if (param.containsKey("pageSize")) {
            tableRequest.setPageSize(Integer.parseInt(request.getParameter("pageSize")));
        }

        Map<String,Object> map = new HashMap<>();
        tableRequest.setParams(map);

        param.forEach((k, v) -> {
            if (v.length == 1) {
               map.put(k, v[0]);
             } else {
                map.put(k, Arrays.asList(v));
            }
        });
        return tableRequest;
    }
}

tk.mybaitis 常用SQL API

添加

1,insertUseGeneratedKeys:插入数据,限制为实体包含id属性并且必须为自增列,实体配置的主键策略无效

2,insert:保存一个实体,null的属性也会保存,不会使用数据库默认值

3,insertList:批量插入,支持批量插入的数据库可以使用,例如MySQL,H2等,另外该接口限制实体包含id属性并且必须为自增列

修改

1,updateByPrimaryKey:根据主键更新实体全部字段,null值会被更新

2,updateByPrimaryKeySelective:根据主键更新属性不为null的值

查询

1,selectOne:根据实体中的属性进行查询,只能有一个返回值,有多个结果是抛出异常,查询条件使用等号

2,selectByIds:根据主键字符串进行查询,类中只有存在一个带有@Id注解的字段

3,select:根据实体中的属性值进行查询,查询条件使用等号

删除

1,deleteByPrimaryKey:根据主键字段进行删除,方法参数必须包含完整的主键属性

2,delete:根据实体属性作为条件进行删除,查询条件使用等号