commit 34e481f0a58f4ce29a33d469d718888ea9f17254 Author: skyyemperor Date: Sat Apr 9 21:44:03 2022 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0213d44 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +src/main/resources/application.yml + +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d97ce60 --- /dev/null +++ b/pom.xml @@ -0,0 +1,135 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.5.2 + + + com.weilab + biology + 0.0.1-SNAPSHOT + biology + Demo project for Spring Boot + + + 1.8 + 1.8 + 1.8 + UTF-8 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + mysql + mysql-connector-java + runtime + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-mail + + + + com.alibaba + fastjson + 1.2.76 + + + + com.baomidou + mybatis-plus-boot-starter + 3.4.1 + + + + org.python + jython-standalone + 2.7.2 + + + + javax.validation + validation-api + 2.0.1.Final + + + + cn.hutool + hutool-all + 5.7.19 + + + + + biology + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + + + public + aliyun nexus + https://maven.aliyun.com/nexus/content/groups/public/ + + true + + + + + + + public + aliyun nexus + https://maven.aliyun.com/nexus/content/groups/public/ + + true + + + false + + + + + diff --git a/src/main/java/com/weilab/biology/BiologyApplication.java b/src/main/java/com/weilab/biology/BiologyApplication.java new file mode 100644 index 0000000..98807e8 --- /dev/null +++ b/src/main/java/com/weilab/biology/BiologyApplication.java @@ -0,0 +1,13 @@ +package com.weilab.biology; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class BiologyApplication { + + public static void main(String[] args) { + SpringApplication.run(BiologyApplication.class, args); + } + +} diff --git a/src/main/java/com/weilab/biology/config/FilterConfig.java b/src/main/java/com/weilab/biology/config/FilterConfig.java new file mode 100644 index 0000000..e7ffe93 --- /dev/null +++ b/src/main/java/com/weilab/biology/config/FilterConfig.java @@ -0,0 +1,33 @@ +package com.weilab.biology.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.DelegatingFilterProxy; + +import javax.servlet.DispatcherType; +import java.util.HashMap; +import java.util.Map; + +/** + * Created by skyyemperor on 2020-12-27 10:54 + * Description : + */ +@Configuration +public class FilterConfig { + + @Autowired + private TokenFilter tokenFilter; + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Bean + public FilterRegistrationBean tokenFilterRegistration() { + FilterRegistrationBean registration = new FilterRegistrationBean(); + registration.setFilter(tokenFilter); + registration.addUrlPatterns("/*"); + registration.setName("tokenFilter"); + registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE); + return registration; + } +} diff --git a/src/main/java/com/weilab/biology/config/TokenFilter.java b/src/main/java/com/weilab/biology/config/TokenFilter.java new file mode 100644 index 0000000..778a6a9 --- /dev/null +++ b/src/main/java/com/weilab/biology/config/TokenFilter.java @@ -0,0 +1,33 @@ +package com.weilab.biology.config; + +import org.springframework.stereotype.Component; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + + +@Component +public class TokenFilter implements Filter { + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + response.setCharacterEncoding("UTF-8"); + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS"); + response.setHeader("Access-Control-Allow-Headers", "*"); + response.setHeader("Access-Control-Expose-Headers", "*"); + response.setHeader("Access-Control-Max-Age", "36000"); + + if ("OPTIONS".equals(request.getMethod())) { + response.setStatus(200); + return; + } + + filterChain.doFilter(servletRequest, servletResponse); + } +} \ No newline at end of file diff --git a/src/main/java/com/weilab/biology/controller/JobController.java b/src/main/java/com/weilab/biology/controller/JobController.java new file mode 100644 index 0000000..e779e6f --- /dev/null +++ b/src/main/java/com/weilab/biology/controller/JobController.java @@ -0,0 +1,124 @@ +package com.weilab.biology.controller; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.util.IdUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.weilab.biology.core.data.enums.JobStatusEnum; +import com.weilab.biology.core.data.vo.result.CommonError; +import com.weilab.biology.core.data.vo.result.Result; +import com.weilab.biology.core.data.vo.result.error.JobError; +import com.weilab.biology.core.validation.EnumValidation; +import com.weilab.biology.service.JobService; +import com.weilab.biology.util.FileUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.List; + + +@RestController +@RequestMapping("job") +public class JobController { + + @Autowired + private JobService jobService; + + @Value("${biology.request-path}") + private String requestPath; + + @PostMapping("/submit") + public Result submit(@RequestParam(required = false) String dataStr, + @RequestParam(required = false) MultipartFile dataFile, + @RequestParam String param, + @RequestParam String mail, + @RequestParam(defaultValue = "0") Integer type, + @RequestParam(required = false) MultipartFile file1, + @RequestParam(required = false) MultipartFile file2, + @RequestParam(required = false) MultipartFile file3, + @RequestParam(required = false) MultipartFile file4, + @RequestParam(required = false) MultipartFile file5, + @RequestParam(required = false) MultipartFile file6, + @RequestParam(required = false) MultipartFile file7, + @RequestParam(required = false) MultipartFile file8, + @RequestParam(required = false) MultipartFile file9, + @RequestParam(required = false) MultipartFile file10, + @RequestParam(required = false) MultipartFile file11, + @RequestParam(required = false) MultipartFile file12, + @RequestParam(required = false) MultipartFile file13, + @RequestParam(required = false) MultipartFile file14, + @RequestParam(required = false) MultipartFile file15, + @RequestParam(required = false) MultipartFile file16, + @RequestParam(required = false) MultipartFile file17, + @RequestParam(required = false) MultipartFile file18, + @RequestParam(required = false) MultipartFile file19, + @RequestParam(required = false) MultipartFile file20) { + if (dataFile == null && StringUtils.isBlank(dataStr)) + return Result.getResult(JobError.PARAM_CAN_NOT_BE_EMPTY); + + JSONObject obj = null; + try { + obj = JSON.parseObject(param); + } catch (Exception e) { + return Result.getResult(CommonError.PARAM_WRONG); + } + + BufferedInputStream dataStream = null; + if (dataFile != null) { + dataStream = FileUtil.getInputStream(FileUtils.multipartToFile(dataFile)); + } + + try { + List files = Arrays.asList(file1, file2, file3, file4, file5, + file6, file7, file8, file9, file10, file11, file12, file13, file14, file15, + file16, file17, file18, file19, file20); + for (MultipartFile file : files) { + if (file != null) { + String filePath = requestPath + FileUtil.FILE_SEPARATOR + "file" + FileUtil.FILE_SEPARATOR + IdUtil.fastUUID() + "." + FileUtil.extName(file.getOriginalFilename()); + FileUtil.writeFromStream(file.getInputStream(), filePath); + obj.put(file.getName(), filePath); + } + } + } catch (IOException e) { + e.printStackTrace(); + return Result.getResult(JobError.FILE_READ_FAIL); + } + + return jobService.submit(dataStr, dataStream, obj, mail, type); + } + + @PostMapping("/status/update") + public Result updateJobStatus(@RequestParam Integer jobId, + @EnumValidation(clazz = JobStatusEnum.class, message = "没有此状态") + @RequestParam Integer status, + @RequestParam(required = false) String result) { + if (status.equals(JobStatusEnum.WAIT.getKey())) + return Result.getResult(CommonError.PARAM_WRONG); + if (status.equals(JobStatusEnum.SUCCESS.getKey())) { + if (StringUtils.isBlank(result)) + return Result.getResult(CommonError.PARAM_WRONG); + try { + JSON.parseObject(result); + } catch (Exception e) { + return Result.getResult(CommonError.PARAM_WRONG); + } + } + return jobService.updateJobStatus(jobId, JobStatusEnum.getEnumByKey(status), result); + } + + @GetMapping("/info/{jobId}") + public Result getJobInfo(@PathVariable Integer jobId) { + return jobService.getJobInfo(jobId); + } + + @GetMapping("/list") + public Result getJobList(@RequestParam(required = false) Integer type) { + return jobService.getJobList(type); + } +} diff --git a/src/main/java/com/weilab/biology/core/data/dto/JobDto.java b/src/main/java/com/weilab/biology/core/data/dto/JobDto.java new file mode 100644 index 0000000..971f14c --- /dev/null +++ b/src/main/java/com/weilab/biology/core/data/dto/JobDto.java @@ -0,0 +1,78 @@ +package com.weilab.biology.core.data.dto; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.weilab.biology.core.data.enums.JobStatusEnum; +import com.weilab.biology.core.data.po.Job; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class JobDto { + + /** + * jobId + */ + private Integer jobId; + + /** + * 任务状态 + */ + private String status; + + /** + * 请求参数 + */ + private JSONObject param; + + /** + * 运行结果 + */ + private JSONObject result; + + /** + * 请求时间 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime requestTime; + + /** + * 创建时间 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + /** + * 完成时间 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime completeTime; + + /** + * 请求类型 + */ + private Integer type; + + public static JobDto parseJob(Job job) { + return new JobDto( + job.getJobId(), + JobStatusEnum.getRemark(job.getStatus()), + JSON.parseObject(job.getParam()), + job.getResult() == null ? null : JSON.parseObject(job.getResult()), + job.getRequestTime(), + job.getCreateTime(), + job.getCompleteTime(), + job.getType() + ); + } + +} diff --git a/src/main/java/com/weilab/biology/core/data/dto/JobLessDto.java b/src/main/java/com/weilab/biology/core/data/dto/JobLessDto.java new file mode 100644 index 0000000..7c4b8f9 --- /dev/null +++ b/src/main/java/com/weilab/biology/core/data/dto/JobLessDto.java @@ -0,0 +1,64 @@ +package com.weilab.biology.core.data.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.weilab.biology.core.data.enums.JobStatusEnum; +import com.weilab.biology.core.data.po.Job; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * Created by skyyemperor on 2021-09-22 + * Description : + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class JobLessDto { + + /** + * jobId + */ + private Integer jobId; + + /** + * 任务状态 + */ + private String status; + + /** + * 请求时间 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime requestTime; + + /** + * 创建时间 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime createTime; + + /** + * 完成时间 + */ + @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime completeTime; + + /** + * 请求类型 + */ + private Integer type; + + public static JobLessDto parseJob(Job job) { + return new JobLessDto( + job.getJobId(), + JobStatusEnum.getRemark(job.getStatus()), + job.getRequestTime(), + job.getCreateTime(), + job.getCompleteTime(), + job.getType() + ); + } +} diff --git a/src/main/java/com/weilab/biology/core/data/enums/JobStatusEnum.java b/src/main/java/com/weilab/biology/core/data/enums/JobStatusEnum.java new file mode 100644 index 0000000..63c9dc2 --- /dev/null +++ b/src/main/java/com/weilab/biology/core/data/enums/JobStatusEnum.java @@ -0,0 +1,42 @@ +package com.weilab.biology.core.data.enums; + +import lombok.Getter; + +/** + * 校区的枚举类 + */ +@Getter +public enum JobStatusEnum { + WAIT(0, "waiting"), + REQED(3, "requested"), + RUNNING(1, "running"), + SUCCESS(2, "success"), + FAIL(-1, "failed"), + TIMEOUT(-2, "timeout"), + ; + + private final Integer key; + private final String remark; + + private JobStatusEnum(Integer key, String remark) { + this.key = key; + this.remark = remark; + } + + public static String getRemark(Integer key) { + for (JobStatusEnum enums : JobStatusEnum.values()) { + if (enums.key.equals(key)) + return enums.getRemark(); + } + return null; + } + + public static JobStatusEnum getEnumByKey(Integer key) { + for (JobStatusEnum enums : JobStatusEnum.values()) { + if (enums.key.equals(key)) + return enums; + } + return null; + } + +} diff --git a/src/main/java/com/weilab/biology/core/data/po/Job.java b/src/main/java/com/weilab/biology/core/data/po/Job.java new file mode 100644 index 0000000..9a52746 --- /dev/null +++ b/src/main/java/com/weilab/biology/core/data/po/Job.java @@ -0,0 +1,97 @@ +package com.weilab.biology.core.data.po; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; + +import java.io.Serializable; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Date; + +import jnr.ffi.annotations.In; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Created by skyyemperor on 2021-09-19 + * Description : + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@TableName(value = "job") +public class Job implements Serializable { + /** + * jobId + */ + @TableId(value = "job_id", type = IdType.AUTO) + private Integer jobId; + + /** + * 基因序列数据 + */ + @TableField(value = "`data`") + private String data; + + /** + * 请求参数 + */ + @TableField(value = "param") + private String param; + + /** + * 联系邮箱 + */ + @TableField(value = "mail") + private String mail; + + /** + * 运行结果 + */ + @TableField(value = "result") + private String result; + + /** + * 任务状态。0为待运行,1为正在运行,2为运行成功,-1为运行失败 + */ + @TableField(value = "`status`") + private Integer status; + + /** + * 请求时间 + */ + @TableField(value = "request_time") + private LocalDateTime requestTime; + + /** + * 创建时间 + */ + @TableField(value = "create_time") + private LocalDateTime createTime; + + /** + * 完成时间 + */ + @TableField(value = "complete_time") + private LocalDateTime completeTime; + + /** + * 请求类型 + */ + @TableField(value = "type") + private Integer type; + + public Job(String data, String param, String mail, Integer status, LocalDateTime requestTime, Integer type) { + this.data = data; + this.param = param; + this.mail = mail; + this.status = status; + this.requestTime = requestTime; + this.type = type; + } + + private static final long serialVersionUID = 1L; +} \ No newline at end of file diff --git a/src/main/java/com/weilab/biology/core/data/vo/result/CommonError.java b/src/main/java/com/weilab/biology/core/data/vo/result/CommonError.java new file mode 100644 index 0000000..f1c098c --- /dev/null +++ b/src/main/java/com/weilab/biology/core/data/vo/result/CommonError.java @@ -0,0 +1,43 @@ +package com.weilab.biology.core.data.vo.result; + +/** + * Created by skyyemperor on 2021-01-30 + * Description : 通用异常返回 + */ +public enum CommonError implements ResultError { + PARAM_WRONG(40000, "参数范围或格式错误"), + NETWORK_WRONG(40001, "网络错误"), + REQUEST_NOT_ALLOW(40002, "当前条件或时间不允许〒▽〒"), + REQUEST_FREQUENTLY(40003, "请求繁忙,请稍后再试"), + CONTENT_NOT_FOUND(40004, "你要找的东西好像走丢啦X﹏X"), + METHOD_NOT_ALLOW(40005, "方法不允许"), + THIS_IS_LAST_PAGE(40006, "这是最后一页,再怎么找也没有啦"), + THIS_IS_FIRST_PAGE(40007, "没有上一页啦"), + PIC_FORMAT_ERROR(40008, "图片格式只能为jpg, jpeg, png, gif, bmp, webp"), + ; + + private int code; + + private String message; + + private CommonError(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/weilab/biology/core/data/vo/result/Result.java b/src/main/java/com/weilab/biology/core/data/vo/result/Result.java new file mode 100644 index 0000000..d6ca97e --- /dev/null +++ b/src/main/java/com/weilab/biology/core/data/vo/result/Result.java @@ -0,0 +1,96 @@ +package com.weilab.biology.core.data.vo.result; + +/** + * web层统一返回类型 + */ +public class Result { + private int code = 0; + + private String message; + + private Object data; + + public Result() { + } + + public Result(int code, String message, Object data) { + this.code = code; + this.message = message; + this.data = data; + } + + public Result(ResultError resultError) { + this.code = resultError.getCode(); + this.message = resultError.getMessage(); + } + + public static Result getResult(int code, String message) { + return getResult(code, message, null); + } + + public static Result getResult(int code, String message, Object data) { + return new Result(code, message, data); + } + + public static Result success() { + return success(null); + } + + public static Result success(Object data) { + return success("success", data); + } + + public static Result success(String message, Object data) { + return new Result(0, message, data); + } + + public static Result fail() { + return fail(null); + } + + public static Result fail(Object data) { + return fail("请求失败", data); + } + + public static Result fail(String message, Object data) { + return new Result(-1, message, data); + } + + public static Result getResult(ResultError resultError) { + return getResult(resultError.getCode(), resultError.getMessage()); + } + + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } + + @Override + public String toString() { + return "Info{" + + "code=" + code + + ", message='" + message + '\'' + + ", data=" + data + + '}'; + } +} diff --git a/src/main/java/com/weilab/biology/core/data/vo/result/ResultError.java b/src/main/java/com/weilab/biology/core/data/vo/result/ResultError.java new file mode 100644 index 0000000..132c18c --- /dev/null +++ b/src/main/java/com/weilab/biology/core/data/vo/result/ResultError.java @@ -0,0 +1,13 @@ +package com.weilab.biology.core.data.vo.result; + +/** + * Created by skyyemperor on 2021-01-30 + * Description : 通用异常返回的父接口 + */ +public interface ResultError { + + int getCode(); + + String getMessage(); + +} diff --git a/src/main/java/com/weilab/biology/core/data/vo/result/error/JobError.java b/src/main/java/com/weilab/biology/core/data/vo/result/error/JobError.java new file mode 100644 index 0000000..44e7f01 --- /dev/null +++ b/src/main/java/com/weilab/biology/core/data/vo/result/error/JobError.java @@ -0,0 +1,40 @@ +package com.weilab.biology.core.data.vo.result.error; + +import com.weilab.biology.core.data.vo.result.ResultError; + +/** + * Created by skyyemperor on 2021-09-19 + * Description : + */ +public enum JobError implements ResultError { + PARAM_CAN_NOT_BE_EMPTY(40100, "文本框和文件不能同时为空"), + FILE_READ_FAIL(40101, "文件读取出错"), + STATUS_UPDATE_FAIL(40102,"当前状态下不允许更新为指定状态"), + ; + + private int code; + + private String message; + + + private JobError(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/src/main/java/com/weilab/biology/core/exception/BaseException.java b/src/main/java/com/weilab/biology/core/exception/BaseException.java new file mode 100644 index 0000000..6614c51 --- /dev/null +++ b/src/main/java/com/weilab/biology/core/exception/BaseException.java @@ -0,0 +1,52 @@ +package com.weilab.biology.core.exception; + +import com.weilab.biology.core.data.vo.result.ResultError; + +public class BaseException extends RuntimeException { + private static final long serialVersionUID = 1L; + + private int code; + + private String message; + + private Object data; + + public BaseException(int code, String message, Object data) { + this.code = code; + this.message = message; + this.data = data; + } + + public BaseException(int code, String message) { + this(code, message, null); + } + + public BaseException(ResultError error) { + this(error.getCode(), error.getMessage(), null); + } + + public int getCode() { + return code; + } + + public void setCode(int code) { + this.code = code; + } + + @Override + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Object getData() { + return data; + } + + public void setData(Object data) { + this.data = data; + } +} \ No newline at end of file diff --git a/src/main/java/com/weilab/biology/core/exception/GlobalExceptionHandler.java b/src/main/java/com/weilab/biology/core/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..511ed4a --- /dev/null +++ b/src/main/java/com/weilab/biology/core/exception/GlobalExceptionHandler.java @@ -0,0 +1,119 @@ +package com.weilab.biology.core.exception; + +import com.weilab.biology.core.data.vo.result.CommonError; +import com.weilab.biology.core.data.vo.result.Result; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; +import org.springframework.validation.ObjectError; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.springframework.web.multipart.support.MissingServletRequestPartException; + +import javax.validation.ConstraintViolationException; +import java.util.List; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + /** + * 自定义异常捕获 + */ + @ExceptionHandler(BaseException.class) + public Result handleBaseException(BaseException e) { + return Result.getResult(e.getCode(), e.getMessage(), e.getData()); + } + + /** + * 服务器错误。常见有数据库错误 + */ + @ExceptionHandler(Exception.class) + public Result handleException(Exception e) { + e.printStackTrace(); + return Result.fail(); + } + + /** + * JSON解析异常,最常见NullPointerException,这里均将其过滤 + */ + @ExceptionHandler({NullPointerException.class}) + public Result handleJSONException(Exception e){ + return Result.getResult(CommonError.REQUEST_NOT_ALLOW); + } + + /** + * 参数校验错误异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public Result validationBodyException(MethodArgumentNotValidException e) { + BindingResult result = e.getBindingResult(); + String message = CommonError.PARAM_WRONG.getMessage(); + if (result.hasErrors()) { + List errors = result.getAllErrors(); + if (errors.size() > 0) { + FieldError fieldError = (FieldError) errors.get(0); + message = fieldError.getDefaultMessage(); + } + } + return Result.getResult(CommonError.PARAM_WRONG.getCode(), message); + } + + + /** + * 参数类型转换错误 + */ + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public Result methodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) { + return Result.getResult(CommonError.PARAM_WRONG.getCode(), + e.getName() + "参数类型转换错误"); + } + + + /** + * 参数转换JSON出错 + */ + @ExceptionHandler(HttpMessageNotReadableException.class) + public Result httpMessageNotReadableException(HttpMessageNotReadableException e) { + return Result.getResult(CommonError.PARAM_WRONG); + } + + /** + * 请求方法不允许 + */ + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + public Result methodNotSupportedException(HttpRequestMethodNotSupportedException e) { + return Result.getResult(CommonError.METHOD_NOT_ALLOW.getCode(), + e.getMethod() + "方法不允许"); + } + + /** + * 缺少请求参数 + */ + @ExceptionHandler({MissingServletRequestParameterException.class, MissingServletRequestPartException.class}) + public Result missingParameterException(Exception e) { + String message = CommonError.PARAM_WRONG.getMessage(); + if (e instanceof MissingServletRequestParameterException) { + message = ((MissingServletRequestParameterException) e).getParameterName() + "不能为空"; + } else if (e instanceof MissingServletRequestPartException) { + message = ((MissingServletRequestPartException) e).getRequestPartName() + "不能为空"; + } + return Result.getResult(CommonError.PARAM_WRONG.getCode(), message); + } + + /** + * 请求参数格式错误 + */ + @ExceptionHandler(ConstraintViolationException.class) + public Result ConstraintViolationException(ConstraintViolationException e) { + if (e.getConstraintViolations().size() > 0) { + return Result.getResult(CommonError.PARAM_WRONG.getCode(), + e.getConstraintViolations().iterator().next().getMessageTemplate()); + } + return Result.getResult(CommonError.PARAM_WRONG); + } + +} \ No newline at end of file diff --git a/src/main/java/com/weilab/biology/core/exception/NetworkException.java b/src/main/java/com/weilab/biology/core/exception/NetworkException.java new file mode 100644 index 0000000..109660d --- /dev/null +++ b/src/main/java/com/weilab/biology/core/exception/NetworkException.java @@ -0,0 +1,13 @@ +package com.weilab.biology.core.exception; + +import com.weilab.biology.core.data.vo.result.CommonError; + +/** + * Created by skyyemperor on 2021-04-30 + * Description : 网络异常 + */ +public class NetworkException extends BaseException { + public NetworkException() { + super(CommonError.NETWORK_WRONG); + } +} diff --git a/src/main/java/com/weilab/biology/core/validation/EnumValidation.java b/src/main/java/com/weilab/biology/core/validation/EnumValidation.java new file mode 100644 index 0000000..f58e572 --- /dev/null +++ b/src/main/java/com/weilab/biology/core/validation/EnumValidation.java @@ -0,0 +1,60 @@ +package com.weilab.biology.core.validation; + + +import javax.validation.Constraint; +import javax.validation.Payload; +import java.lang.annotation.Documented; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + + +@Documented +@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) +@Retention(RUNTIME) +@Repeatable(EnumValidation.List.class) +@Constraint(validatedBy = {EnumValidator.class}) +public @interface EnumValidation { + String message() default "{*.validation.constraint.Enum.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + /** + * the enum's class-type + * + * @return Class + */ + Class clazz(); + + /** + * the method's name ,which used to validate the enum's value + * + * @return method's name + */ + String method() default "getKey"; + + /** + * 是否允许为空 + * + * @return true or false + */ + boolean allowNull() default true; + + /** + * Defines several {@link EnumValidation} annotations on the same element. + * + * @see EnumValidation + */ + @Documented + @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) + @Retention(RUNTIME) + @interface List { + EnumValidation[] value(); + } +} + diff --git a/src/main/java/com/weilab/biology/core/validation/EnumValidator.java b/src/main/java/com/weilab/biology/core/validation/EnumValidator.java new file mode 100644 index 0000000..15541e7 --- /dev/null +++ b/src/main/java/com/weilab/biology/core/validation/EnumValidator.java @@ -0,0 +1,63 @@ +package com.weilab.biology.core.validation; + + +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import java.lang.reflect.Method; + +/** + * Controller入参对象中属性枚举项校验 + */ +public class EnumValidator implements ConstraintValidator { + + private EnumValidation annotation; + + /** + * Initializes the validator in preparation for + * {@link #isValid(Object, ConstraintValidatorContext)} calls. + * The constraint annotation for a given constraint declaration + * is passed. + *

+ * This method is guaranteed to be called before any use of this instance for + * validation. + *

+ * The default implementation is a no-op. + * + * @param constraintAnnotation annotation instance for a given constraint declaration + */ + @Override + public void initialize(EnumValidation constraintAnnotation) { + this.annotation = constraintAnnotation; + } + + /** + * Implements the validation logic. + * The state of {@code value} must not be altered. + *

+ * This method can be accessed concurrently, thread-safety must be ensured + * by the implementation. + * + * @param value object to validate + * @param context context in which the constraint is evaluated + * @return {@code false} if {@code value} does not pass the constraint + */ + @Override + public boolean isValid(Object value, ConstraintValidatorContext context) { + if (value == null) { + return annotation.allowNull(); + } + + Object[] objects = annotation.clazz().getEnumConstants(); + try { + Method method = annotation.clazz().getMethod(annotation.method()); + for (Object o : objects) { + if (value.equals(method.invoke(o))) { + return true; + } + } + } catch (Exception e) { + throw new RuntimeException(e); + } + return false; + } +} diff --git a/src/main/java/com/weilab/biology/mapper/JobMapper.java b/src/main/java/com/weilab/biology/mapper/JobMapper.java new file mode 100644 index 0000000..7885ee2 --- /dev/null +++ b/src/main/java/com/weilab/biology/mapper/JobMapper.java @@ -0,0 +1,26 @@ +package com.weilab.biology.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.weilab.biology.core.data.dto.JobLessDto; +import com.weilab.biology.core.data.po.Job; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.python.modules.itertools.count; + +import java.util.List; + +/** + * Created by skyyemperor on 2021-09-19 + * Description : + */ +@Mapper +public interface JobMapper extends BaseMapper { + + List selectRunningJobs(); + + Job selectNextWaitingJob(); + + List selectJobList(@Param("type") Integer type, + @Param("count") Integer count); + +} \ No newline at end of file diff --git a/src/main/java/com/weilab/biology/service/JobService.java b/src/main/java/com/weilab/biology/service/JobService.java new file mode 100644 index 0000000..5af802a --- /dev/null +++ b/src/main/java/com/weilab/biology/service/JobService.java @@ -0,0 +1,242 @@ +package com.weilab.biology.service; + +import cn.hutool.core.io.FileUtil; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.weilab.biology.core.data.dto.JobDto; +import com.weilab.biology.core.data.dto.JobLessDto; +import com.weilab.biology.core.data.enums.JobStatusEnum; +import com.weilab.biology.core.data.po.Job; +import com.weilab.biology.core.data.vo.result.CommonError; +import com.weilab.biology.core.data.vo.result.Result; +import com.weilab.biology.core.data.vo.result.error.JobError; +import com.weilab.biology.mapper.JobMapper; +import com.weilab.biology.util.FileUtils; +import com.weilab.biology.util.MailUtil; +import com.weilab.biology.util.TaskExecutorUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.BufferedInputStream; +import java.io.File; +import java.time.LocalDateTime; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +/** + * Created by skyyemperor on 2021-09-19 + * Description : + */ +@Service +@Slf4j +public class JobService { + + @Value("${biology.python-cmd}") + private String pythonCmd; + + @Value("${biology.request-path}") + private String requestPath; + + @Value("${biology.result-path}") + private String resultDataPath; + + @Value("${biology.log-path}") + private String logPath; + + @Value("${biology.concurrent-num}") + private Integer concurrentNum; + + @Autowired + private JobMapper jobMapper; + + @Autowired + private MailUtil mailUtil; + + @Autowired + private TaskExecutorUtil taskExecutorUtil; + + private static final String SUBJECT = "【DeepBIO Result Notice】"; + private static final String SUCCESS_EMAIL_CONTENT = "Your request has been completed, click http://server.wei-group.net/front/biology/#/resultMail?jobId=%s to check the detail information"; + private static final String FAIL_EMAIL_CONTENT = "We are very sorry, but some errors occurred in the task you submitted, click http://server.wei-group.net/front/biology/#/resultMail?jobId=%s to check the detail information"; + private static final String TIMEOUT_EMAIL_CONTENT = "We are very sorry, but the task you submitted was overtime, click http://server.wei-group.net/front/biology/#/resultMail?jobId=%s to check the detail information"; + private static final String RECEIVED_EMAIL_CONTENT = "Your request has been received, click http://server.wei-group.net/front/biology/#/resultMail?jobId=%s to check the detail information"; + private static final String START_RUNNING_EMAIL_CONTENT = "Your task has started running, click http://server.wei-group.net/front/biology/#/resultMail?jobId=%s to check the detail information"; + + /** + * 提交job + * + * @param dataStr 文本内容 + * @param param 其他参数(json格式) + * @param mail 邮箱 + * @return + * @throws Exception + */ + @Transactional + public Result submit(String dataStr, BufferedInputStream dataStream, JSONObject param, String mail, Integer type) { + Job job = new Job("", JSON.toJSONString(param), mail, JobStatusEnum.WAIT.getKey(), LocalDateTime.now(), type); + jobMapper.insert(job); + sendEmail(job.getJobId(), JobStatusEnum.WAIT, mail); + + try { + //将请求数据写入本地文件,之后向python传递文件路径参数 + String dataPath = String.format(requestPath + File.separator + "job-%d-dataStr.txt", job.getJobId()); + if (dataStream != null) { + FileUtil.writeFromStream(dataStream, dataPath); + } else { + FileUtils.writeStringToFile(dataPath, dataStr); + } + + //将jobId和dataPath参数添加至param中 + param.put("jobId", job.getJobId()); + param.put("requestDataPath", dataPath); + param.put("resultDataPath", resultDataPath); + + //更新数据库param字段 + job.setParam(JSON.toJSONString(param)); + jobMapper.updateById(job); + + runNextJob(); + } catch (Exception e) { + e.printStackTrace(); + updateJobStatus(job.getJobId(), JobStatusEnum.FAIL); + } + + return getJobInfo(job.getJobId()); + } + + public Result getJobInfo(Integer jobId) { + Job job = jobMapper.selectById(jobId); + if (job == null) + return Result.getResult(CommonError.CONTENT_NOT_FOUND); + return Result.success(JobDto.parseJob(job)); + } + + public Result updateJobStatus(Integer jobId, JobStatusEnum status) { + return updateJobStatus(jobId, status, null); + } + + public Result updateJobStatus(Integer jobId, JobStatusEnum status, String result) { + System.out.println(status.getRemark()); + Job job = jobMapper.selectById(jobId); + if (job == null) + return Result.getResult(CommonError.CONTENT_NOT_FOUND); + + switch (status) { + case WAIT: + break; + case SUCCESS: + job.setCompleteTime(LocalDateTime.now()); + job.setResult(result); + break; + case FAIL: + case TIMEOUT: + if (!job.getStatus().equals(JobStatusEnum.RUNNING.getKey()) + && !job.getStatus().equals(JobStatusEnum.REQED.getKey())) + return Result.getResult(JobError.STATUS_UPDATE_FAIL); + job.setCompleteTime(LocalDateTime.now()); + break; + case RUNNING: + if (!job.getStatus().equals(JobStatusEnum.REQED.getKey())) + return Result.getResult(JobError.STATUS_UPDATE_FAIL); + job.setCreateTime(LocalDateTime.now()); + break; + } + + job.setStatus(status.getKey()); + jobMapper.updateById(job); + + sendEmail(jobId, status, job.getMail()); + runNextJob(); + + return getJobInfo(jobId); + } + + public Result getJobList(Integer type) { + List jobs = jobMapper.selectJobList(type, 200); + return Result.success(jobs.stream().map(JobLessDto::parseJob).collect(Collectors.toList())); + } + + /** + * 运行下一个job + */ + private synchronized void runNextJob() { + Job nextJob = null; + try { + //并发数小于concurrentNum,运行该job + if (jobMapper.selectRunningJobs().size() < concurrentNum) { + if ((nextJob = jobMapper.selectNextWaitingJob()) != null) { + //更新job状态 + nextJob.setStatus(JobStatusEnum.REQED.getKey()); + jobMapper.updateById(nextJob); + + waitRunning(nextJob.getJobId()); + + String logFilePath = String.format(logPath + File.separator + "task-log-%s.txt", nextJob.getJobId()); + String cmd = String.format("%s -setting '%s' >> %s 2>&1", pythonCmd, nextJob.getParam(), logFilePath); + + log.info("执行命令: " + cmd); + + String[] cmds = new String[]{"/bin/sh", "-c", cmd}; + + Runtime.getRuntime().exec(cmds); + } + } + } catch (Exception e) { + e.printStackTrace(); + if (nextJob != null) { + nextJob.setStatus(JobStatusEnum.FAIL.getKey()); + jobMapper.updateById(nextJob); + } + } + } + + /** + * 等待运行,超时时间60秒 + */ + private void waitRunning(Integer jobId) { + //等待60秒,检查是否已运行 + taskExecutorUtil.schedule(() -> { + Job job = jobMapper.selectById(jobId); + if (job.getStatus().equals(JobStatusEnum.REQED.getKey())) { + updateJobStatus(jobId, JobStatusEnum.FAIL); + } + }, 60, TimeUnit.SECONDS); + + //等待4小时,查看是否执行完成 + taskExecutorUtil.schedule(() -> { + Job job = jobMapper.selectById(jobId); + if (job.getStatus().equals(JobStatusEnum.RUNNING.getKey())) { + updateJobStatus(jobId, JobStatusEnum.TIMEOUT); + } + }, 4, TimeUnit.HOURS); + } + + private void sendEmail(Integer jobId, JobStatusEnum status, String mail) { + String content = null; + switch (status) { + case WAIT: + content = String.format(RECEIVED_EMAIL_CONTENT, jobId); + break; + case SUCCESS: + content = String.format(SUCCESS_EMAIL_CONTENT, jobId); + break; + case FAIL: + content = String.format(FAIL_EMAIL_CONTENT, jobId); + break; + case TIMEOUT: + content = String.format(TIMEOUT_EMAIL_CONTENT, jobId); + break; + case RUNNING: + content = String.format(START_RUNNING_EMAIL_CONTENT, jobId); + break; + } + if (content != null) + mailUtil.send(mail, SUBJECT, content); + } + +} + diff --git a/src/main/java/com/weilab/biology/util/FileUtils.java b/src/main/java/com/weilab/biology/util/FileUtils.java new file mode 100644 index 0000000..4169282 --- /dev/null +++ b/src/main/java/com/weilab/biology/util/FileUtils.java @@ -0,0 +1,118 @@ +package com.weilab.biology.util; + +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; + +/** + * 文件读取工具类 + */ +public class FileUtils { + + /** + * MultipartFile 转换成File + * + * @param multfile 原文件类型 + * @return File + */ + public static File multipartToFile(MultipartFile multfile) { + File file = null; + try { + file = File.createTempFile("prefix", "_" + multfile.getOriginalFilename()); + multfile.transferTo(file); + } catch (IOException e) { + e.printStackTrace(); + } + return file; + } + + /** + * 读取文件内容,作为字符串返回 + */ + public static String readFileAsString(String filePath) throws IOException { + File file = new File(filePath); + if (!file.exists()) { + throw new FileNotFoundException(filePath); + } + + return readFileAsString(file); + } + + /** + * 读取文件内容,作为字符串返回 + */ + public static String readFileAsString(File file) throws IOException { + StringBuilder sb = new StringBuilder((int) (file.length())); + // 创建字节输入流 + FileInputStream fis = new FileInputStream(file); + // 创建一个长度为10240的Buffer + byte[] bbuf = new byte[10240]; + // 用于保存实际读取的字节数 + int hasRead = 0; + while ((hasRead = fis.read(bbuf)) > 0) { + sb.append(new String(bbuf, 0, hasRead)); + } + fis.close(); + return sb.toString(); + } + + /** + * 根据文件路径读取byte[] 数组 + */ + public static byte[] readFileAsBytes(String filePath) throws IOException { + File file = new File(filePath); + if (!file.exists()) { + throw new FileNotFoundException(filePath); + } else { + return readFileAsBytes(file); + } + } + + /** + * 根据文件读取byte[]数组 + */ + public static byte[] readFileAsBytes(File file) throws IOException { + if (file == null) { + throw new FileNotFoundException(); + } else { + ByteArrayOutputStream bos = new ByteArrayOutputStream((int) file.length()); + BufferedInputStream in = null; + + try { + in = new BufferedInputStream(new FileInputStream(file)); + short bufSize = 1024; + byte[] buffer = new byte[bufSize]; + int len1; + while (-1 != (len1 = in.read(buffer, 0, bufSize))) { + bos.write(buffer, 0, len1); + } + + return bos.toByteArray(); + } finally { + try { + if (in != null) { + in.close(); + } + } catch (IOException var14) { + var14.printStackTrace(); + } + bos.close(); + } + } + } + + + public static void writeStringToFile(String filePath, String content) throws IOException { + BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(filePath)); + bufferedOutputStream.write(content.getBytes()); + bufferedOutputStream.flush(); + bufferedOutputStream.close(); + } + + public static void writeByteToFile(String filePath, byte[] content) throws IOException { + BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(filePath)); + bufferedOutputStream.write(content); + bufferedOutputStream.flush(); + bufferedOutputStream.close(); + } +} diff --git a/src/main/java/com/weilab/biology/util/MailUtil.java b/src/main/java/com/weilab/biology/util/MailUtil.java new file mode 100644 index 0000000..5cd0324 --- /dev/null +++ b/src/main/java/com/weilab/biology/util/MailUtil.java @@ -0,0 +1,39 @@ +package com.weilab.biology.util; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSenderImpl; +import org.springframework.stereotype.Component; + +import java.util.HashMap; +import java.util.Map; + +@Component +@Slf4j +public class MailUtil { + @Autowired + private JavaMailSenderImpl mailSender; + + @Value("${spring.mail.username}") + private String sender; + + @Autowired + private TaskExecutorUtil taskExecutorUtil; + + public void send(String receiver, String subject, String content) { + taskExecutorUtil.run(() -> { + SimpleMailMessage message = new SimpleMailMessage(); + message.setSubject(subject);//设置标题 + message.setText(content);//设置内容 + + message.setTo(receiver); + message.setFrom(sender); + + mailSender.send(message); + + log.info("Mail已发送: " + message); + }); + } +} diff --git a/src/main/java/com/weilab/biology/util/TaskExecutorUtil.java b/src/main/java/com/weilab/biology/util/TaskExecutorUtil.java new file mode 100644 index 0000000..af0ebae --- /dev/null +++ b/src/main/java/com/weilab/biology/util/TaskExecutorUtil.java @@ -0,0 +1,43 @@ +package com.weilab.biology.util; + +import org.springframework.stereotype.Component; + +import java.util.concurrent.*; + +@Component +public class TaskExecutorUtil { + + public TaskExecutorUtil() { + + } + + private static final ThreadFactory factory = new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + return new Thread(r); + } + }; + + private static final ThreadPoolExecutor cachePool = new ThreadPoolExecutor( + 2, 40, + 60L, TimeUnit.SECONDS, + new SynchronousQueue(), + factory + ); + + private static final ScheduledExecutorService scheduledThreadPool = + Executors.newScheduledThreadPool(5); + + public void run(Runnable r) { + cachePool.execute(r); + } + + public Future submit(Callable c) { + return cachePool.submit(c); + } + + public void schedule(Runnable runnable, long delay, TimeUnit timeUnit) { + scheduledThreadPool.schedule(runnable, delay, timeUnit); + } + +} diff --git a/src/main/resources/mapper/JobMapper.xml b/src/main/resources/mapper/JobMapper.xml new file mode 100644 index 0000000..a2603f3 --- /dev/null +++ b/src/main/resources/mapper/JobMapper.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + SELECT job_id, + `data`, + param, + mail, + result, + `status`, + request_time, + create_time, + complete_time, + type + FROM job + + + SELECT job_id, + `status`, + request_time, + create_time, + complete_time, + type + FROM job + + + + + \ No newline at end of file diff --git a/src/test/java/com/weilab/biology/BiologyApplicationTests.java b/src/test/java/com/weilab/biology/BiologyApplicationTests.java new file mode 100644 index 0000000..81ee4d5 --- /dev/null +++ b/src/test/java/com/weilab/biology/BiologyApplicationTests.java @@ -0,0 +1,13 @@ +package com.weilab.biology; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class BiologyApplicationTests { + + @Test + void contextLoads() { + } + +}