1、统一返回格式

在前后端接口交互中,前端按照约定请求URL路径,并传入相关参数,后端服务器接收请求,进行业务处理,返回数据给前端。在前后端分离的项目中,我们往往会将后端的结果封装为JSON数据返回,统一的数据格式会使前端对数据的操作更一致、轻松。

一般情况下,统一返回数据格式没有固定的格式,只要能描述清楚返回的数据状态以及要返回的具体数据就可以。但是一般会包含状态码返回消息数据这几部分内容

例如,我们的系统要求返回的基本数据格式如下:

  • 列表:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"success": true,
"code": 20000,
"message": "成功",
"data": {
"items": [
{
"id": "1",
"name": "刘德华",
"intro": "毕业于师范大学数学系,热爱教育事业,执教数学思维6年有余"
}
]
}
}
  • 分页:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"success": true,
"code": 20000,
"message": "成功",
"data": {
"total": 17,
"rows": [
{
"id": "1",
"name": "刘德华",
"intro": "毕业于师范大学数学系,热爱教育事业,执教数学思维6年有余"
}
]
}
}
  • 没有返回数据:
1
2
3
4
5
6
{
"success": true,
"code": 20000,
"message": "成功",
"data": {}
}
  • 失败:
1
2
3
4
5
6
{
"success": false,
"code": 20001,
"message": "失败",
"data": {}
}

我们定义统一返回格式为:

1
2
3
4
5
6
{
"success": 布尔, //响应是否成功
"code": 数字, //响应码
"message": 字符串, //返回消息
"data": HashMap //返回数据,放在键值对中
}

2、定义统一返回结果

2.1、创建返回码定义枚举类

状态码定义枚举类对于特殊响应情况做了定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Getter
@ToString
public enum ResultCodeEnum {

SUCCESS(true, 20000,"成功"),
UNKNOWN_REASON(false, 20001, "未知错误"),

BAD_SQL_GRAMMAR(false, 21001, "sql语法错误"),
JSON_PARSE_ERROR(false, 21002, "json解析异常"),
PARAM_ERROR(false, 21003, "参数不正确"),

FILE_UPLOAD_ERROR(false, 21004, "文件上传错误"),
FILE_DELETE_ERROR(false, 21005, "文件刪除错误"),
EXCEL_DATA_IMPORT_ERROR(false, 21006, "Excel数据导入错误"),

VIDEO_UPLOAD_ALIYUN_ERROR(false, 22001, "视频上传至阿里云失败"),
VIDEO_UPLOAD_TOMCAT_ERROR(false, 22002, "视频上传至业务服务器失败"),
VIDEO_DELETE_ALIYUN_ERROR(false, 22003, "阿里云视频文件删除失败"),
FETCH_VIDEO_UPLOADAUTH_ERROR(false, 22004, "获取上传地址和凭证失败"),
REFRESH_VIDEO_UPLOADAUTH_ERROR(false, 22005, "刷新上传地址和凭证失败"),
FETCH_PLAYAUTH_ERROR(false, 22006, "获取播放凭证失败"),

URL_ENCODE_ERROR(false, 23001, "URL编码失败"),
ILLEGAL_CALLBACK_REQUEST_ERROR(false, 23002, "非法回调请求"),
FETCH_ACCESSTOKEN_FAILD(false, 23003, "获取accessToken失败"),
FETCH_USERINFO_ERROR(false, 23004, "获取用户信息失败"),
LOGIN_ERROR(false, 23005, "登录失败"),

COMMENT_EMPTY(false, 24006, "评论内容必须填写"),

PAY_RUN(false, 25000, "支付中"),
PAY_UNIFIEDORDER_ERROR(false, 25001, "统一下单错误"),
PAY_ORDERQUERY_ERROR(false, 25002, "查询支付结果错误"),

ORDER_EXIST_ERROR(false, 25003, "课程已购买"),

GATEWAY_ERROR(false, 26000, "服务不能访问"),

CODE_ERROR(false, 28000, "验证码错误"),

LOGIN_PHONE_ERROR(false, 28009, "手机号码不正确"),
LOGIN_MOBILE_ERROR(false, 28001, "账号不正确"),
LOGIN_PASSWORD_ERROR(false, 28008, "密码不正确"),
LOGIN_DISABLED_ERROR(false, 28002, "该用户已被禁用"),
REGISTER_MOBLE_ERROR(false, 28003, "手机号已被注册"),
LOGIN_AUTH(false, 28004, "需要登录"),
LOGIN_ACL(false, 28005, "没有权限"),
SMS_SEND_ERROR(false, 28006, "短信发送失败"),
SMS_SEND_ERROR_BUSINESS_LIMIT_CONTROL(false, 28007, "短信发送过于频繁");


private Boolean success;

private Integer code;

private String message;

ResultCodeEnum(Boolean success, Integer code, String message) {
this.success = success;
this.code = code;
this.message = message;
}
}

2.2、创建结果类

对于返回数据有两种赋值方法,一是传入一个Map对象,二是传入一个Key和一个Value。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@Data
@ApiModel(value = "全局统一返回结果")
public class R {

@ApiModelProperty(value = "是否成功")
private Boolean success;

@ApiModelProperty(value = "返回码")
private Integer code;

@ApiModelProperty(value = "返回消息")
private String message;

@ApiModelProperty(value = "返回数据")
private Map<String, Object> data = new HashMap<String, Object>();

public R(){}

public static R ok(){
R r = new R();
r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess());
r.setCode(ResultCodeEnum.SUCCESS.getCode());
r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
return r;
}

public static R error(){
R r = new R();
r.setSuccess(ResultCodeEnum.UNKNOWN_REASON.getSuccess());
r.setCode(ResultCodeEnum.UNKNOWN_REASON.getCode());
r.setMessage(ResultCodeEnum.UNKNOWN_REASON.getMessage());
return r;
}

public static R setResult(ResultCodeEnum resultCodeEnum){
R r = new R();
r.setSuccess(resultCodeEnum.getSuccess());
r.setCode(resultCodeEnum.getCode());
r.setMessage(resultCodeEnum.getMessage());
return r;
}

public R success(Boolean success){
this.setSuccess(success);
return this;
}

public R message(String message){
this.setMessage(message);
return this;
}

public R code(Integer code){
this.setCode(code);
return this;
}

public R data(String key, Object value){
this.data.put(key, value);
return this;
}

public R data(Map<String, Object> map){
this.setData(map);
return this;
}
}

3、分页条件查询

3.1、需求说明

在讲师分页列表中,我们需要根据讲师名进行模糊查询,根据讲师头衔、讲师入驻时间进行查询。

image-20210212210909529

3.2、创建查询对象

创建讲师条件查询类TeacherQueryVo,该类定义了3.1中的四个条件属性。

1
2
3
4
5
6
7
8
9
@Data
public class TeacherQueryVo implements Serializable {
private static final long serialVersionUID = 1L;

private String name;
private Integer level;
private String joinDateBegin;
private String joinDateEnd;
}

3.3、在TeacherController中编写一个分页条件查询接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/***
* 实现条件查询带分页查询的方法
* @param page 当前页
* @param limit 每页记录数
* @param queryVo 条件封装成的查询对象
* @return 总记录数及讲师列表
*/
@GetMapping("list/{page}/{limit}")
@ApiOperation("实现条件查询带分页")
public R listPage(@ApiParam(value = "当前页",required = true) @PathVariable("page") Long page,
@ApiParam(value = "每页记录数",required = true) @PathVariable("limit") Long limit,
TeacherQueryVo queryVo) {
Page<Teacher> pageParam = new Page<>(page, limit);
IPage<Teacher> pageModel = teacherService.selectPage(pageParam, queryVo);
List<Teacher> records = pageModel.getRecords();
long total = pageModel.getTotal();
return R.ok().data("total", total).data("rows", records);
}

3.4、在Service层实现selectPage方法

这个方法接收传入的分页对象和条件查询对象,首先对传入的条件对象进行判断,如果条件对象为空,那么直接进行一次普通的分页查询;

如果条件对象不为空,那么分别取出其中的四个属性,根据属性类型使用QueryWrapper拼接不为空的条件属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
public IPage<Teacher> selectPage(Page<Teacher> pageParam, TeacherQueryVo teacherQueryVo) {

//显示分页查询列表
// 1、排序:按照sort字段排序
QueryWrapper<Teacher> queryWrapper = new QueryWrapper<>();
queryWrapper.orderByAsc("sort");

// 2、分页查询
if(teacherQueryVo == null){
return baseMapper.selectPage(pageParam, queryWrapper);
}

// 3、条件查询
String name = teacherQueryVo.getName();
Integer level = teacherQueryVo.getLevel();
String joinDateBegin = teacherQueryVo.getJoinDateBegin();
String joinDateEnd = teacherQueryVo.getJoinDateEnd();

if(!StringUtils.isEmpty(name)){
queryWrapper.likeRight("name", name);
}

if(level != null){
queryWrapper.eq("level", level);
}

if(!StringUtils.isEmpty(joinDateBegin)){
queryWrapper.ge("join_date", joinDateBegin);
}

if(!StringUtils.isEmpty(joinDateEnd)){
queryWrapper.le("join_date", joinDateEnd);
}

return baseMapper.selectPage(pageParam, queryWrapper);
}

3.5、测试

打开swagger,进行测试

image-20210212213413862

结果

image-20210212213429863

4、订单操作

由于只有登录后的用户才可以下订单,而且订单除了必需的商品号外,还必须有用户号,在合法用户登录后,我们会下发给用户一个token,在之后的订单操作中,用户需要在发起请求的时候在请求头中带上这个token,服务器解析请求头中的token后即可获取用户id。

4.1、创建JWTUtils

这个工具类封装了一系列有关token的方法,包括根据密钥生成token,鉴定token是否合法,从请求中的token解析出用户信息等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class JwtUtils {

public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";

private static Key getKeyInstance(){
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
byte[] bytes = DatatypeConverter.parseBase64Binary(APP_SECRET);
return new SecretKeySpec(bytes,signatureAlgorithm.getJcaName());
}

public static String getJwtToken(JwtInfo jwtInfo, int expire){

String JwtToken = Jwts.builder()
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
.setSubject("guli-user")//主题
.setIssuedAt(new Date())//颁发时间
.setExpiration(DateTime.now().plusSeconds(expire).toDate())//过期时间
.claim("id", jwtInfo.getId())//用户id
.claim("nickname", jwtInfo.getNickname())//用户昵称
.claim("avatar", jwtInfo.getAvatar())//用户头像
.signWith(SignatureAlgorithm.HS256, getKeyInstance())
.compact();

return JwtToken;
}

/**
* 判断token是否存在与有效
* @param jwtToken
* @return
*/
public static boolean checkJwtTToken(String jwtToken) {
if(StringUtils.isEmpty(jwtToken)) {
return false;
}
try {
Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}

/**
* 判断token是否存在与有效
* @param request
* @return
*/
public static boolean checkJwtTToken(HttpServletRequest request) {
try {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) {
return false;
}
Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}

/**
* 根据token获取会员id
* @param request
* @return
*/
public static JwtInfo getMemberIdByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) {
return null;
}
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(getKeyInstance()).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return new JwtInfo(claims.get("id").toString(), claims.get("nickname").toString(), claims.get("avatar").toString());
}
}

4.2、新增订单–Controller层

这个方法除了接收课程Id外,还需要接收一个HttpServletRequest对象,我们需要从这个request对象中获取用户信息(用户id)

1
2
3
4
5
6
7
8
9
10
11
@ApiOperation("新增订单")
@PostMapping("auth/save/{courseId}")
public R save(
@ApiParam(value = "课程Id",required = true)
@PathVariable String courseId, HttpServletRequest request) {
JwtInfo jwtInfo = JwtUtils.getMemberIdByJwtToken(request);
// 调用service层添加订单的方法,需要传入课程id和用户id
// 这个方法将返回新增的订单id
String orderId = orderService.saveOrder(courseId,jwtInfo.getId());
return R.ok().data("orderId",orderId);
}

4.3、新增订单–Service层

Service层中新增订单的流程如下:

  • 根据传入的memberId和courseId作为条件,使用QueryWrapper查询该用户是否已经存在当前课程的订单
    • 如果已存在,直接返回已存在的订单号即可
  • 查询课程信息和用户信息是否存在
  • 创建订单,使用一个工具类生成订单号,然后将上面根据课程号查询到的课程信息,根据用户id查询到的用户信息填入订单对象中。
  • 向数据库中插入该对象,根据Mybatis-Plus的id自动回填功能将订单对象Id返回。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Override
public String saveOrder(String courseId, String memberId) {
//1 查询当前用户是否已经存在当前课程的订单
QueryWrapper<Order> orderQueryWrapper = new QueryWrapper<>();
orderQueryWrapper.eq("course_id",courseId);
orderQueryWrapper.eq("member_id",memberId);
Order orderExist = baseMapper.selectOne(orderQueryWrapper);
//如果已经存在订单,直接返回查找到的订单id即可
if(orderExist != null) {
return orderExist.getId();
}
//2 查询课程信息
CourseDto courseDto = eduCourseService.getCourseDtoById(courseId);
if(courseDto == null) {
// 如果查询courseDto为空,直接抛出异常
throw new GrainException(ResultCodeEnum.PARAM_ERROR);
}
//3 查询用户信息
MemberDto memberDto = eduMemberService.getMemberDtoByMemberId(memberId);
if(memberDto == null) {
// 如果查询memberDto为空,直接抛出异常
throw new GrainException(ResultCodeEnum.PARAM_ERROR);
}

//4 创建订单
Order order = new Order();
order.setOrderNo(OrderNoUtils.getOrderNo());
order.setCourseId(courseId);
order.setCourseTitle(courseDto.getTitle());
order.setCourseCover(courseDto.getCover());
order.setTeacherName(courseDto.getTeacherName());
//分
order.setTotalFee(courseDto.getPrice().multiply(new BigDecimal(100)));
order.setMemberId(memberId);
order.setMobile(memberDto.getMobile());
order.setNickname(memberDto.getNickname());
//未支付
order.setStatus(0);
//微信支付
order.setPayType(1);
baseMapper.insert(order);
return order.getId();
}

5、定时任务

5.1、定时任务实现方式

Timer

使用jdk的Timer和TimerTask可以实现简单的间隔执行任务,无法实现按日历去调度执行任务

ScheduledThreadPool线程池

创建可以延迟或定时执行任务的线程,无法实现按日历去调度执行任务

quartz

使用Quartz实现 Quartz 是一个异步任务调度框架,功能丰富,可以实现按日历调度

Spring Task

Spring 3.0后提供Spring Task实现任务调度,支持按日历调度,相比Quartz功能稍简单,但是开发基本够用,支持注解编程方式

5.2、集成Spring Task

在启动类中添加注解@EnableScheduling

1
2
3
4
5
6
7
8
9
10
11
12
13
@EnableScheduling
@SpringBootApplication
@ComponentScan({"com.hzx.grain"})
@EnableDiscoveryClient
@EnableFeignClients
public class ServiceStatisticsApplication {

public static void main(String[] args) {
SpringApplication.run(ServiceStatisticsApplication.class, args);
System.out.println("http://localhost:8180/doc.html");
System.out.println("http://localhost:8180/swagger-ui.html");
}
}

5.3、创建定时任务类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@Component
public class ScheduledTask {

@Autowired
private DailyService dailyService;

/**
* 测试
*/
@Scheduled(cron="0/3 * * * * *") // 每隔3秒执行一次
public void task1() {
log.info("task1 执行");
}
}

5.4、测试

查看控制台,可以发现控制台定时输出日志

image-20210212215219245

5.5、在线生成cron表达式

http://cron.qqe2.com/

参考教程如下: