技术选型

Pasted image 20240304191612

配置属性类

Pasted image 20240306184952|850
将application.yml中的属性配置封装成一个对象

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
package com.sky.properties;  

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {

/**
* 管理端员工生成jwt令牌相关配置
*/
private String adminSecretKey;
private long adminTtl;
private String adminTokenName;

/**
* 用户端微信用户生成jwt令牌相关配置
*/
private String userSecretKey;
private long userTtl;
private String userTokenName;

}

然后再将这个对象注入到controller对象中
Pasted image 20240306185159|500

Builder

在EmployeeLoginV0类中添加注解@Builder
Pasted image 20240306211723|700在EmployeeLoginV0类中添加注解@Builder

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
package com.sky.vo;  

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "员工登录返回的数据格式")
public class EmployeeLoginVO implements Serializable {

@ApiModelProperty("主键值")
private Long id;

@ApiModelProperty("用户名")
private String userName;

@ApiModelProperty("姓名")
private String name;

@ApiModelProperty("jwt令牌")
private String token;

}

前端发送的请求是如何请求到后端服务的

Pasted image 20240306213026|800Pasted image 20240306215046 Pasted image 20240306215141
使用nginx方向代理的好处

  • 提高访问速度
  • 进行负载均衡(将请求均衡的分配给集群中的服务器)
  • 保证后端服务安全
  • Pasted image 20240306215341

nginx反向代理配置方式

Pasted image 20240306215430
Pasted image 20240306215828

nginx负载均衡配置

Pasted image 20240306215945

使用MD5加密

DigestUtils.md5DigestAsHex

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
public Employee login(EmployeeLoginDTO employeeLoginDTO) {  
String username = employeeLoginDTO.getUsername();
String password = employeeLoginDTO.getPassword();

//1、根据用户名查询数据库中的数据
Employee employee = employeeMapper.getByUsername(username);

//2、处理各种异常情况(用户名不存在、密码不对、账号被锁定)
if (employee == null) {
//账号不存在
throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
}

//密码比对
//对前端传过来的明文密码进行md5加密处理
password = DigestUtils.md5DigestAsHex(password.getBytes());
if (!password.equals(employee.getPassword())) {
//密码错误
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}

if (employee.getStatus() == StatusConstant.DISABLE) {
//账号被锁定
throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
}

//3、返回实体对象
return employee;
}

前后端开发流程

Pasted image 20240307145432

Swagger

**Pasted image 20240307151255
在配置类中加入Knife4j的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Bean  
public Docket docket1(){
log.info("准备生成接口文档...");
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();

Docket docket = new Docket(DocumentationType.SWAGGER_2)
.groupName("管理端接口")
.apiInfo(apiInfo)
.select()
//指定生成接口需要扫描的包
.apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
.paths(PathSelectors.any())
.build();

return docket;
}

设置静态资源映射,否则接口文档页面无法访问

1
2
3
4
5
6
7
8
9
/**  
* 设置静态资源映射,主要是访问接口文档(html、js、css)
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始设置静态资源映射...");
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}

Yapi的区别

Pasted image 20240307154711

Swagger注解方式

Pasted image 20240307154813

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
@RestController  
@RequestMapping("/admin/employee")
@Slf4j
@Api(tags = "员工相关接口")
public class EmployeeController {

@Autowired
private EmployeeService employeeService;
@Autowired
private JwtProperties jwtProperties;

/**
* 登录
*
* @param employeeLoginDTO
* @return
*/
@PostMapping("/login")
@ApiOperation(value = "员工登录")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
log.info("员工登录:{}", employeeLoginDTO);

Employee employee = employeeService.login(employeeLoginDTO);

//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);

EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token)
.build();

return Result.success(employeeLoginVO);
}
1
2
3
4
5
6
7
8
9
10
11
@Data  
@ApiModel(description = "员工登录时传递的数据模型")
public class EmployeeLoginDTO implements Serializable {

@ApiModelProperty("用户名")
private String username;

@ApiModelProperty("密码")
private String password;

}

用户属性拷贝

Employee

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
package com.sky.entity;  

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;
import java.time.LocalDateTime;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {

private static final long serialVersionUID = 1L;

private Long id;

private String username;

private String name;

private String password;

private String phone;

private String sex;

private String idNumber;

private Integer status;

//@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;

//@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;

private Long createUser;

private Long updateUser;

}

前端传来的数据封装EmployeeDTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.sky.dto;  

import lombok.Data;

import java.io.Serializable;

@Data
public class EmployeeDTO implements Serializable {

private Long id;

private String username;

private String name;

private String phone;

private String sex;

private String idNumber;

}

属性拷贝

1
2
//对象属性拷贝  
BeanUtils.copyProperties(employeeDTO, employee);

设置新增用户修改人ID

流程图
Pasted image 20240308151935

设置修改人id

Pasted image 20240308153231
解析出登录员工id后,如何传递给Service的save方法?
答:
使用TreadLocal
#Pasted image 20240308154002

员工分页查询

使用pagehelper插件
Pasted image 20240308161728

Controller层

最后返回Result的一个泛型

1
2
3
4
5
6
public static <T> Result<T> success(T object) {  
Result<T> result = new Result<T>();
result.data = object;
result.code = 1;
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
/**  
* 员工分页查询
* @param employeePageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("员工分页查询")
public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
log.info("员工分页查询,参数为:{}", employeePageQueryDTO);
PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
return Result.success(pageResult);
}

PageResult
@AllArgsConstructor有参构造方法
@NoArgsConstructor无参构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
/**  
* 封装分页查询结果
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {

private long total; //总记录数

private List records; //当前页数据集合

}

Service 层

pageSize就是那个插件,startPage指从哪一页开始,查询多少条数据,然后动态添加到sql语句中,limit x,y
mapper层最后返回的也是Employee

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**  
* 分页查询
*
* @param employeePageQueryDTO
* @return
*/
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
// select * from employee limit 0,10
//开始分页查询
PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());

Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);

long total = page.getTotal();
List<Employee> records = page.getResult();

return new PageResult(total, records);
}

Mapper层

没有使用注解方式,而是使用的映射文件

1
2
3
4
5
6
/**  
* 分页查询
* @param employeePageQueryDTO
* @return
*/
Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);

Pasted image 20240308163715

查询得到的操作时间格式修正

  • 在属性上加入注解,对日期进行格式化
    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
    package com.sky.entity;  

    import com.fasterxml.jackson.annotation.JsonFormat;
    import lombok.AllArgsConstructor;
    import lombok.Builder;
    import lombok.Data;
    import lombok.NoArgsConstructor;

    import java.io.Serializable;
    import java.time.LocalDateTime;

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class Employee implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    private String username;

    private String name;

    private String password;

    private String phone;

    private String sex;

    private String idNumber;

    private Integer status;

    //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;

    //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;

    private Long createUser;

    private Long updateUser;

    }
  • 在WebMvcConfiguration中扩展Spring MVC的消息转换器,统一对日期类型进行格式化处理
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**  
    * 扩展Spring MVC框架的消息转化器
    * @param converters
    */
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    log.info("扩展消息转换器...");
    //创建一个消息转换器对象
    MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
    //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
    converter.setObjectMapper(new JacksonObjectMapper());
    //将自己的消息转化器加入容器中
    converters.add(0,converter);
    }

    启用禁用员工账号

Controller层

status在路径上传参,添加注解@PathVariable
只需要返回code即可,data没有数据,Result不用写泛型

1
2
3
4
5
6
7
8
9
10
11
12
13
/**  
* 启用禁用员工账号
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("启用禁用员工账号")
public Result startOrStop(@PathVariable Integer status,Long id){
log.info("启用禁用员工账号:{},{}",status,id);
employeeService.startOrStop(status,id);
return Result.success();
}

Service层

重新构造一个Employee实体,Employee实体中添加了@Builder注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**  
* 启用禁用员工账号
*
* @param status
* @param id
*/
public void startOrStop(Integer status, Long id) {
// update employee set status = ? where id = ?

/*Employee employee = new Employee(); employee.setStatus(status); employee.setId(id);*/
Employee employee = Employee.builder()
.status(status)
.id(id)
.build();

employeeMapper.update(employee);
}

Mapper层

1
2
3
4
5
6
/**  
* 根据主键动态修改属性
* @param employee
*/
@AutoFill(value = OperationType.UPDATE)
void update(Employee employee);

使用mapper层映射文件,动态修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<update id="update" parameterType="Employee">  
update employee
<set>
<if test="name != null">name = #{name},</if>
<if test="username != null">username = #{username},</if>
<if test="password != null">password = #{password},</if>
<if test="phone != null">phone = #{phone},</if>
<if test="sex != null">sex = #{sex},</if>
<if test="idNumber != null">id_Number = #{idNumber},</if>
<if test="updateTime != null">update_Time = #{updateTime},</if>
<if test="updateUser != null">update_User = #{updateUser},</if>
<if test="status != null">status = #{status},</if>
</set>
where id = #{id}
</update>

公共字段填充

这里的公共字段填充是填充到比如下面的Employee实体的属性中,不是sql语句时候临时添加

  • 自定义注解AutoFill,用于标识需要进行公共字段自动填充的方法
  • 自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值
  • 在Mapper的方法上加入AutoFill注解

注解AutoFill,@Target用于只对方法起作用
用于指定被该注解修饰的注解要保留多久。在这种情况下,RetentionPolicy.RUNTIME 意味着被该注解修饰的注解在运行时仍然可用,可以通过反射等机制访问

1
2
3
4
5
6
7
8
9
/**  
* 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型:UPDATE INSERT
OperationType value();
}

这里的OperationType是一个操作枚举类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
  
/**
* 数据库操作类型
*/
public enum OperationType {

/**
* 更新操作
*/
UPDATE,

/**
* 插入操作
*/
INSERT

}

定义切入点以及通知,这个切入类也是容器内的一个组件

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
  
/**
* 自定义切面,实现公共字段自动填充处理逻辑
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {

/**
* 切入点
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}

/**
* 前置通知,在通知中进行公共字段的赋值
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
log.info("开始进行公共字段自动填充...");

//获取到当前被拦截的方法上的数据库操作类型
MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象
OperationType operationType = autoFill.value();//获得数据库操作类型

//获取到当前被拦截的方法的参数--实体对象
Object[] args = joinPoint.getArgs();
if(args == null || args.length == 0){
return;
}

Object entity = args[0];

//准备赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();

//根据当前不同的操作类型,为对应的属性通过反射来赋值
if(operationType == OperationType.INSERT){
//为4个公共字段赋值
try {
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

//通过反射为对象属性赋值
setCreateTime.invoke(entity,now);
setCreateUser.invoke(entity,currentId);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
}else if(operationType == OperationType.UPDATE){
//为2个公共字段赋值
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

//通过反射为对象属性赋值
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
}
}

}

在对应的mapper文件中所有的insert、update方法上添加@AutoFill注解

1
2
3
4
5
@Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user,status) " +  
"values " +
"(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
@AutoFill(value = OperationType.INSERT)
void insert(Employee employee);

例子

1
2
3
4
5
6
7
8
9
/**  
* 插入员工数据
* @param employee
*/
@Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user,status) " +
"values " +
"(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
@AutoFill(value = OperationType.INSERT)
void insert(Employee employee);

上传文件

使用阿里云Bucket工具桶存储,ali属性配置类

1
2
3
4
5
6
7
8
9
10
11
@Component  
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {

private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;

}

属性值都是yml文件提供,这里的值则是dev开发环境中的yml文件提供,这样便可解耦调整环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:  
profiles:
active: dev
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
# 设置jwt过期时间
admin-ttl: 7200000
# 设置前端传递过来的令牌名称
admin-token-name: token
user-secret-key: itheima
user-ttl: 7200000
user-token-name: authentication
alioss:
endpoint: ${sky.alioss.endpoint}
access-key-id: ${sky.alioss.access-key-id}
access-key-secret: ${sky.alioss.access-key-secret}
bucket-name: ${sky.alioss.bucket-name}

dev.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
sky:  
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
host: localhost
port: 3306
database: sky_take_out
username: root
password: root
alioss:
endpoint: oss-cn-beijing.aliyuncs.com
access-key-id: your-access-key-id
access-key-secret: your-access-key-secret
bucket-name: your-bucket-name

alie有专门提供的util类,初始化实例并请求

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

@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {

private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;

/**
* 文件上传
*
* @param bytes
* @param objectName
* @return
*/
public String upload(byte[] bytes, String objectName) {

// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

try {
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}

//文件访问路径规则 https://BucketName.Endpoint/ObjectName StringBuilder stringBuilder = new StringBuilder("https://");
stringBuilder
.append(bucketName)
.append(".")
.append(endpoint)
.append("/")
.append(objectName);

log.info("文件上传到:{}", stringBuilder.toString());

return stringBuilder.toString();
}
}

为了创建Util类,需要添加一个配置文件,项目启动时便可以创建类
@ConditionalOnMissingBean是指只有当缺失此类bean的时候创建,也就是spring容器中只会有一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**  
* 配置类,用于创建AliOssUtil对象
*/
@Configuration
@Slf4j
public class OssConfiguration {

@Bean
@ConditionalOnMissingBean public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties){
log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}

Controller接口
使用UUID格式,将原始文件名取后缀和UUID拼接

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
@RestController  
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {

@Autowired
private AliOssUtil aliOssUtil;

/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file){
log.info("文件上传:{}",file);

try {
//原始文件名
String originalFilename = file.getOriginalFilename();
//截取原始文件名的后缀 dfdfdf.png
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
//构造新文件名称
String objectName = UUID.randomUUID().toString() + extension;

//文件的请求路径
String filePath = aliOssUtil.upload(file.getBytes(), objectName);
return Result.success(filePath);
} catch (IOException e) {
log.error("文件上传失败:{}", e);
}

return Result.error(MessageConstant.UPLOAD_FAILED);
}
}

插入口味

service层
使用@Transactional 添加事务注解,那么启动项需要添加@EnableTransactionManagement

1
2
3
4
5
6
7
8
9
10
11
@SpringBootApplication  
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
@EnableCaching//开发缓存注解功能
@EnableScheduling //开启任务调度
public class SkyApplication {
public static void main(String[] args) {
SpringApplication.run(SkyApplication.class, args);
log.info("server started");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Transactional  
public void saveWithFlavor(DishDTO dishDTO) {

Dish dish = new Dish();

BeanUtils.copyProperties(dishDTO, dish);

//向菜品表插入1条数据
dishMapper.insert(dish);

//获取insert语句生成的主键值
Long dishId = dish.getId();

List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0) {
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishId);
});
//向口味表插入n条数据
dishFlavorMapper.insertBatch(flavors);
}
}

上面的代码中,由于前端没有传来dishid的值,所以我们要自己获取,insert插入后就会产生dishid,在mapper层的映射文件中,使用useGeneratedKeys=”true” keyProperty=”id”,其中的id是要赋值给dish类中的id属性;然后使用forEach逐一set id

1
2
3
<insert id="insert" useGeneratedKeys="true" keyProperty="id">  
insert into dish (name, category_id, price, image, description, create_time, update_time, create_user,
update_user, status) values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status})</insert>

saveBatch逻辑映射文件,collection=”flavors”中的flavors是指调用mapper层时传来的集合flavors

1
dishFlavorMapper.insertBatch(flavors);
1
2
3
4
5
6
<insert id="insertBatch">  
insert into dish_flavor (dish_id, name, value) VALUES
<foreach collection="flavors" item="df" separator=",">
(#{df.dishId},#{df.name},#{df.value})
</foreach>
</insert>

菜品分页查询

mapper层映射文件
dish用d,left outer join左外连接,category用c,on指条件 如果直接查询c.name,那么查询结果会有两个name字段,返回给DishVo的时候,就会报错;
所以使用c.name as categoryName用别名

1
Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
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
@Data  
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DishVO implements Serializable {

private Long id;
//菜品名称
private String name;
//菜品分类id
private Long categoryId;
//菜品价格
private BigDecimal price;
//图片
private String image;
//描述信息
private String description;
//0 停售 1 起售
private Integer status;
//更新时间
private LocalDateTime updateTime;
//分类名称
private String categoryName;
//菜品关联的口味
private List<DishFlavor> flavors = new ArrayList<>();

//private Integer copies;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<select id="pageQuery" resultType="com.sky.vo.DishVO">  
select d.* , c.name as categoryName from dish d left outer join category c on d.category_id = c.id
<where>
<if test="name != null">
and d.name like concat('%',#{name},'%')
</if>
<if test="categoryId != null">
and d.category_id = #{categoryId}
</if>
<if test="status != null">
and d.status = #{status}
</if>
</where>
order by d.create_time desc
</select>

Controller层

方法请求参数没有加注解@RequestBody,是因为前端不是json格式,而是请求路径传参

1
2
3
4
5
6
7
8
```java
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO) {
log.info("菜品分页查询:{}", dishPageQueryDTO);
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}

前端ids参数传递添加注解

@RequestParam是让mvc可以将前端传来的string id参数封装到list集合中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**  
* 菜品批量删除
*
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids) {
log.info("菜品批量删除:{}", ids);
dishService.deleteBatch(ids);

//将所有的菜品缓存数据清理掉,所有以dish_开头的key
cleanCache("dish_*");

return Result.success();
}
xml映射文件中,使用forEach
1
2
3
4
5
6
<select id="getSetmealIdsByDishIds" resultType="java.lang.Long">  
select setmeal_id from setmeal_dish where dish_id in
<foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
#{dishId}
</foreach>
</select>

一些参数说明

1、这里的 @CacheEvict 注解用于清除指定缓存中的所有条目。下面是各个参数的含义:

  • cacheNames = "setmealCache":指定要清除的缓存名称为 “setmealCache”,即清除名为 “setmealCache” 的缓存中的数据。
  • allEntries = true:表示清除整个缓存,而不是只清除部分条目。如果为 false(默认),则只清除与方法参数匹配的缓存项
1
2
3
4
5
6
7
8
9
10
11
12
13
/**  
* 批量删除套餐
*
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("批量删除套餐")
@CacheEvict(cacheNames = "setmealCache",allEntries = true)
public Result delete(@RequestParam List<Long> ids) {
setmealService.deleteBatch(ids);
return Result.success();
}

2、这段代码是 MyBatis 中的一个 XML 映射语句,用于向数据库中插入一个 Setmeal 对象,并返回插入记录的主键值。下面是各个参数的含义:

  • insert:是该 SQL 语句的唯一标识符,供 MyBatis 使用。
  • parameterType="Setmeal":指定了这个 SQL 语句的参数类型为 Setmeal 类型,即插入操作的参数是一个 Setmeal 对象。
  • useGeneratedKeys="true":表示要使用数据库的自动生成主键功能。
  • keyProperty="id":指定了自动生成的主键值要设置到 Setmeal 对象的 id 属性中。
1
2
3
<insert id="insert" parameterType="Setmeal" useGeneratedKeys="true" keyProperty="id">  
insert into setmeal
(category_id, name, price, status, description, image, create_time, update_time, create_user, update_user) values (#{categoryId}, #{name}, #{price}, #{status}, #{description}, #{image}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})</insert>

3、这段代码是一个 MyBatis 的 XML 映射语句,用于根据ID查询套餐(Setmeal)信息,并包含关联的菜品(Dish)信息。下面是各个参数的含义:

  • id:是该 SQL 语句的唯一标识符,供 MyBatis 使用。
  • parameterType="long":指定了这个 SQL 语句的参数类型为 long,即查询操作的参数是一个长整型的ID。
  • resultMap="setmealAndDishMap":指定了查询结果的映射规则,即如何将查询结果映射为对象。setmealAndDishMap 是一个自定义的结果映射规则,定义了如何将查询结果映射为包含套餐和菜品信息的对象。
    1
    2
    3
    <select id="getByIdWithDish" parameterType="long" resultMap="setmealAndDishMap">  
    select a.*,
    b.id sd_id, b.setmeal_id, b.dish_id, b.name sd_name, b.price sd_price, b.copies from setmeal a left join setmeal_dish b on a.id = b.setmeal_id where a.id = #{id}</select>

这段代码定义了一个结果映射(resultMap),用于将查询结果映射为 SetmealVO 类型的对象,其中包含了套餐(Setmeal)和菜品(Dish)的信息。具体含义如下:

  • <resultMap id="setmealAndDishMap" type="com.sky.vo.SetmealVO" autoMapping="true">:定义了一个名为 setmealAndDishMap 的结果映射,将查询结果映射为 SetmealVO 类型的对象。autoMapping="true" 表示启用自动映射,可以将数据库列名自动映射到对象属性,前提是列名和属性名相同。
  • <result column="id" property="id"/>:将查询结果中名为 id 的列映射到 SetmealVO 对象的 id 属性。
  • <collection property="setmealDishes" ofType="SetmealDish">:定义了一个集合属性 setmealDishes,用于存储套餐和菜品的关联信息,其中 ofType="SetmealDish" 表示集合中元素的类型为 SetmealDish 类型。
  • <result column="sd_id" property="id"/>:将查询结果中名为 sd_id 的列映射到 SetmealDish 对象的 id 属性。
  • <result column="setmeal_id" property="setmealId"/>:将查询结果中名为 setmeal_id 的列映射到 SetmealDish 对象的 setmealId 属性。
  • <result column="dish_id" property="dishId"/>:将查询结果中名为 dish_id 的列映射到 SetmealDish 对象的 dishId 属性。
  • <result column="sd_name" property="name"/>:将查询结果中名为 sd_name 的列映射到 SetmealDish 对象的 name 属性。
  • <result column="sd_price" property="price"/>:将查询结果中名为 sd_price 的列映射到 SetmealDish 对象的 price 属性。
  • <result column="copies" property="copies"/>:将查询结果中名为 copies 的列映射到 SetmealDish 对象的 copies 属性。
1
2
3
4
5
6
7
8
9
10
<resultMap id="setmealAndDishMap" type="com.sky.vo.SetmealVO" autoMapping="true">  
<result column="id" property="id"/>
<collection property="setmealDishes" ofType="SetmealDish">
<result column="sd_id" property="id"/>
<result column="setmeal_id" property="setmealId"/>
<result column="dish_id" property="dishId"/>
<result column="sd_name" property="name"/>
<result column="sd_price" property="price"/>
<result column="copies" property="copies"/>
</collection></resultMap>