感谢黑马各位老师的馈赠

瑞吉外卖

项目前置部分

  1. 创建maven项目(创建结束后注意查看下项目对应的maven以及jre,jdk部分)
  2. 导入pom坐标,首先parent部分,继承springboot,之后是一个项目对应的依赖
  3. sql文件导入,图形化命令行随意
  4. 对应前端资源映射配置

    前端资源配置

    目的:修改资源映射路径
    步骤:
  • 创建WebMvcConfig配置类
  • 继承WebMvcConfigurationSupport类并实现对应方法设置映射路径
1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 设置静态资源映射
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
log.info("静态资源映射start...");
}
}

前端对应login.html
js中的方法,这是需要提前约定好的,后端传入前端的数据:(分别是三个数据code,data,msg)

后台登录功能开发

首先创建大体的框架(三层,基于MP的规范,entity,service,mapper,controller)

注意: 在启动类中需要对mapper进行扫描,使用MapperScan注解写入mapper包的路径,之后对接口可以实现自动装配
结构大体如下:

细节部分 Mapper层添加Mapper注解,在实现类Impl层上添加Service注解且继承Service类,泛型为Mapper以及实体类,且实现接口,对应Controller层需要添加对应RestController注解以及类上的url路径,且需要对service实现自动装配

导入通用返回结果类

这里是服务端相应的数据返回信息,均封装成此对象

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
/**
* 通用返回结果类
* @param <T>
*/
@Data
public class R<T> {

private Integer code; //编码:1成功,0和其它数字为失败

private String msg; //错误信息

private T data; //数据

private Map map = new HashMap(); //动态数据

public static <T> R<T> success(T object) {
R<T> r = new R<T>();
r.data = object;
r.code = 1;
return r;
}

public static <T> R<T> error(String msg) {
R r = new R();
r.msg = msg;
r.code = 0;
return r;
}

public R<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}

}

后台登录功能开发

开发流程
处理逻辑如下:

  1. 将页面提交的密码password进行md5加密处理
  2. 根据页面提交的用户名username查询数据库
  3. 如果没有查询到则返回登录失败结果
  4. 密码比对,如果不一致则返回登录失败结果
  5. 查看员工状态,如果为已禁用状态,则返回员工已禁用结果
  6. 登录成功,将员工id存入Session并返回登录成功结果
    流程图:

一个小插曲

条件构造器

补充一个在学习MP中丢失的知识点:Wrapper
条件构造器中链式结构默认使用and,如果是或者就需要手动添加or()
Wrapper框架:

  • 用于查询,删除:queryWrapper
  • 用于更改数据以及数据条件封装:updateWrapper
    queryWrapper
  1. 条件构造器组件排序条件:
  2. 删除条件:
  3. 修改功能:

    这里update中两个参数,前者是设置修改的字段以及修改的值,后者是设置修改的对象
  4. 条件优先级的设定
    consumer消费者接口中是一定要有参数的,而其中的方法就是对参数的操作

    这里的param就是lambda中的条件构造器

    在and方法和or方法中均有
  5. 组装select字句(查询其中某些字段)
    updateWrapper
    实现修改功能:(较queryWrapper较简单,无需创建实体对象)

补充结束,此时已经明白了什么是Wrapper条件构造,我们继续往下实现项目

后台登录功能实现:

  • 登录功能
    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
    @Service
    public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
    @Override
    public R<Employee> login(HttpServletRequest request, Employee employee) {
    // 1. 将页面提交的密码password进行md5加密处理
    String password = employee.getPassword();
    password = DigestUtils.md5DigestAsHex(password.getBytes());
    // 2. 根据页面提交的用户名username查询数据库
    LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(Employee::getUsername,employee.getUsername());
    Employee emp = getOne(queryWrapper);
    //3. 如果没有查询到则返回登录失败结果
    if(emp==null){
    return R.error("不存在该账户!");
    }
    //4. 密码比对,如果不一致则返回登录失败结果
    if(emp.getPassword() != password){
    return R.error("密码错误,请重新输入!");
    }
    //5. 查看员工状态,如果为已禁用状态,则返回员工已禁用结果
    if(emp.getStatus() == 0){
    return R.error("账户已被禁用!");
    }
    //6. 登录成功,将员工id存入Session并返回登录成功结果
    request.getSession().setAttribute("employee",emp.getId());
    return R.success(emp);
    }
    }
  • 退出功能
    1
    2
    3
    4
    5
    @PostMapping("/logout")
    public R<String> logout(HttpServletRequest request){
    request.getSession().removeAttribute("employee");
    return R.success("退出成功!");
    }
    退出的业务逻辑比较简单为了方便,我直接写在了controller层,建议还是放在service层

后台首页构成以及展示方式



这里设置对应的是首页右侧的菜单
为什么此处点击即可切换呢?

这个方法每次调用会切换item元素对应的url并展示在页面

完善登录功能

产生问题:这里使用index.html可以直接进入内置页面,不用登录,这是不合理的

业务修改:(必须登录才能进入,未登录将跳转到登录页面)
那么,具体应该怎么实现呢?
答案就是使用过滤器或者拦截器,在过滤器或者拦截器中判断用户是否已经完成登录,如果没有登录则跳转到登录页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 用户访问页面的拦截器,查看是否登录成功
*/
@WebFilter(filterName = "LoginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
log.info("拦截到的信息:{}",request.getRequestURL());
filterChain.doFilter(request,response);
}
}

这里是检查业务,还未实现真正的拦截业务逻辑
还有一处需要注意,就是在启动类上需要添加一个注解@ServletComponentScan,才能使过滤器被扫描到
代码开发
具体处理逻辑:

  1. 获取本次请求的URI
  2. 判断本次请求是否需要处理
  3. 如果不需要处理,则直接放行
  4. 判断登录状态,如果已登录,则直接放行
  5. 如果未登录则返回未登录结果,通过输出流方式先客户端(前端)页面响应数据,前端有做拦截器,其中返回的msg需要是NOTLOGIN

    流程图:

代码实现:

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
/**
* 用户访问页面的拦截器,查看是否登录成功
*/
@WebFilter(filterName = "LoginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
// 1. 获取本次请求的URI
String requestURI = request.getRequestURI();
String[] urls = {
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
log.info("本次拦截{}请求",requestURI);
// 2. 判断本次请求是否需要处理
boolean check = checkUrl(urls, requestURI);
// 3. 如果不需要处理,则直接放行
if(check){
log.info("本次请求为{}无需被拦截",requestURI);
filterChain.doFilter(request,response);
return;
}
// 4. 判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee") != null){
log.info("用户已登录,用户id为{}",request.getSession().getAttribute("employee"));
filterChain.doFilter(request,response);
return;
}
// 5. 如果未登录则返回未登录结果,通过输出流方式先客户端(前端)页面响应数据,前端有单独做拦截器,其中返回的msg需要是NOTLOGIN
log.info("用户未登录!");
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}

/**
* 判断页面是否可以通过过滤器
* @param urls
* @param requestURI
* @return
*/
private boolean checkUrl(String[] urls, String requestURI) {
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI);
if (match) {
return true;
}
}
return false;
}
}

新增员工功能

小插曲,在学习前在此处复习一下之前学过的注解知识

  1. get和post

GET可以拥有请求体,RFC 文档中从来就没有说过 GET 没有请求体,RFC 只是说GET 意味着通过 URI 来识别资源。所以GET请求体中的数据一般都是不做处理的,有些 http 的 lib 里不让甚至直接不提供 GET 方法追加请求体的操作。

POST请求拥有请求体,并且请求数据一般都是放在请求体当中的。所以在处理POST请求时,通常都是从请求体中获取数据。

  1. RequestBody注解
  • 用于接收前端发送的json数据(处理json格式数据)前端POST过来的数据会通过反序列数据到实体类中,并且在反序列的过程中会进行类型的转换
  • RequestBody接收的是json格式的数据,只有请求体中能保存json,所以使用@RequestBody接收数据的时候必须是POST方式等方式。
  • 可以和其它注解同时使用,但是只能使用一次
  1. RequestParam注解
    可以用于接收URL中的参数并捆绑到方法的参数中,也可以接受post请求体中的Content-Type 为 application/x-www-form-urlencoded的数据。(post比较常用的是json格式数据)
    用句简单的话说就是使用了这个注解就是添加了一些条件,前端要进来的时候必须要有这个条件规定的东西,如果没有就进不来
    二者可以混合使用

业务开发:(后台这里只需要给对应数据附上初始值即可)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public R<String> save(Employee employee, HttpServletRequest request) {
// 给员工设置初始密码,同时进行md5加密
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));

// 给员工添加创建时间
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());

// 获得当前用户的id
// session中的数据取出来一般都是object类型,需要做强转
Long userId = (Long) request.getSession().getAttribute("employee");

employee.setCreateUser(userId);
employee.setUpdateUser(userId);

// 保存员工
save(employee);

return R.success("员工添加成功!");
}


看到这里有条sql语句insert执行成功即可。

但是前面的程序还存在一个问题,就是当我们在新增员工时输入的账号已经存在,由于employee表中对该字段加入了唯一约束,此时程序会抛出异常:

此时需要我们的程序进行异常捕获,通常有两种处理方式:

  1. 在Controller方法中加入try、catch进行异常捕获
  2. 使用异常处理器进行全局异常捕获

第一种不建议,如果使用try,catch的方式,同样的代码要写很多遍,冗余成分过多,所以此处使用第二种方式

代码实现类:GlobalExceptionHandler 在common包中

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
/**
* 全局异常捕获器
*/
@ControllerAdvice(annotations = {RestController.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {

// 关键是此处的注解
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandle(SQLIntegrityConstraintViolationException exception){

String message = exception.getMessage();

log.info(message);

if(message.contains("Duplicate entry")){
String[] strs = message.split(" ");
String user = strs[2];
String msg = "用户"+ user + "已存在!";
return R.error("用户{}已存在!");
}

return R.error("错误类型未定义");
}
}

小知识点:
@ControllerAdvice注解:扫描所有 annotations 中所设置的含有这个注解的类
只有设置annotations参数后才会生效。
@ExceptionHandler注解:括号内参数为异常的类型

小结

一般业务的实现基本都是这个流程,只不过是数据格式,请求地址等不同,基本上都是请求响应这样的模式。

员工信息分页查询

对需求进行分析

系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。

右上角可以设置过滤条件,也是需要实现需求之一。

业务开发

在开发代码之前,需要梳理一下整个程序的执行过程:

  1. 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service查询数据
  3. Service调用Mapper操作数据库,查询分页数据
  4. Controller将查询到的分页数据响应给页面
  5. 页面接收到分页数据并通过ElementUl的Table组件展示到页面上

补充 :MP的分页插件配置
这里可以把Mapper的包扫描放置在此处
为什么是Interceptor?
因为这个配置器是做出条件限制拦截之后才会出现

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 配置MP分页插件
*/
@Configuration
public class MybatisPlusConfig {

@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}

业务流程:

  1. 因为使用MP分页插件,首先创建分页构造器
  2. 因为需要查询,所以这里再创建一个条件构造器
  3. 执行查询并返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public R<Page> page(int page, int pageSize, String name) {
log.info("page={},pageSize={},name={}",page,pageSize,name);
// 1. 因为使用MP分页插件,首先创建分页构造器
Page<Employee> employeePage = new Page<>(); // 之间可以传入参数,一个current当前页码,一个是每页条数

// 2. 因为需要查询,所以这里再创建一个条件构造器,这里使用模糊查询
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(StringUtils.isNotEmpty(name),Employee::getName,name);

// 添加一个查询到之后的排序条件
queryWrapper.orderByDesc(Employee::getUpdateTime);

// 3. 执行查询并返回
page(employeePage,queryWrapper); // 执行的条件映射
return R.success(employeePage); // 返回类型是一个分页构造器
}

实现员工禁用/启用处理

需求分析

在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。
需要注意,只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用、禁用按钮不显示。
禁用启用只有管理员才能看到。

开发

这里前端已经做了对应的校验,只有admin用户登录才可以显示禁用启用按钮。
那么这个是如何在前端实现的呢?
首先是此处方法获得当前用户的模型数据

上面在显示之前对当前模型进行了判断

业务开发:
controller层

1
2
3
4
5
6
7
8
9
10
/**
* 根据员工id修改员工状态
* @param request
* @param employee
* @return
*/
@PostMapping
public R<String> update(HttpServletRequest request,@RequestBody Employee employee){
return employeeService.update(request,employee);
}

service层
1
2
3
4
5
6
7
8
9
10
@Override
public R<String> update(HttpServletRequest request, Employee employee) {
// 获取当前用户
Long userId = (Long) request.getSession().getAttribute("employee");
// 设置对应修改时间以及用户
employee.setUpdateUser(userId);
employee.setUpdateTime(LocalDateTime.now());
updateById(employee);
return R.success("修改用户状态成功!");
}

功能测试中出现的问题:

原因:js获取Long型数据处理时丢失了精度,超过16位的一般会对剩余位数据进行四舍五入

如何解决?
我们可以在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串,效果如下:

如何转换? 创建一个消息转换器并修改mvc框架中的首个转换器为自己设定的

JacksonObjectMapper类

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
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {

public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);


SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))

.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}

同时在mvc配置类中修改
ps. 注意类不要写错了
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 扩展添加自己设置创建的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
messageConverter.setObjectMapper(new JacksonObjectMapper());
// 将上面设置的消息转换器添加在mvc转换器狂框架中的前列
converters.add(0,messageConverter);
}

最后测试

发现禁用成功变成启用,通过

编辑员工信息

需求分析:

在员工管理列表页面点击编辑按钮,跳转到编辑页面,在编辑页面回显员工信息并进行修改,最后点击保存按钮完成编辑操作

代码开发:

在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:
1、点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
2、在add.html页面获取urt中的参数[员工id]
3、发送ajax请求,请求服务端,同时提交员工id参数
4、服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
5、页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
6、点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
7、服务端接收员工信息,并进行处理,完成后给页面响应
8、页面接收到服务端响应信息后进行相应处理

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 根据id查找员工信息
* @param id
* @return
*/
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id ){
Employee employee = employeeService.getById(id);
if(employee!=null){
return R.success(employee);
}
return R.error("员工不存在!");
}

分类管理菜单

相关业务目录如下:

字段自动填充

需求分析

前面我们已经完成了后台系统的员工管理功能开发,在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,在编辑员工时需要设置修改时间和修改人等字段。这些字段属于公共字段,也就是很多表中都有这些字段,如下:

能不能对于这些公共字段在某个地方统一处理,来简化开发呢?
Mybatis Plus就提供了公共字段自动填充功能

代码开发

如何实现?
Mybatis Plus公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。
实现步骤:

  1. 在实体类的属性上加入@FableField注解,指定自动填充的策略
  2. 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口
    实现如下:
    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
    /**
    * 自定义元数据对象处理器
    */
    @Slf4j
    @Component
    public class MyMetaObjectHandler implements MetaObjectHandler {
    /**
    * 插入时对公共字段进行填充
    * @param metaObject
    */
    @Override
    public void insertFill(MetaObject metaObject) {
    log.info("在插入时填写公共字段...");
    log.info(metaObject.toString());
    metaObject.setValue("createTime", LocalDateTime.now());
    metaObject.setValue("updateTime",LocalDateTime.now());
    metaObject.setValue("createUser",new Long(1));
    metaObject.setValue("updateUser",new Long(1));
    }

    /**
    * 更新时对公共字段进行填充
    * @param metaObject
    */
    @Override
    public void updateFill(MetaObject metaObject) {
    log.info("在更新时填写更新字段...");
    log.info(metaObject.toString());
    metaObject.setValue("updateTime",LocalDateTime.now());
    metaObject.setValue("updateUser",new Long(1));
    }
    }
    这里是存在一个相应的问题,就是对应的userId无法中动态写入只能写死,这里是很不合理的,那么这个问题该如何去解决呢?

答案是:使用threadlocal.

在学习ThreadLocal之前,我们需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:
1、LoginCheckFilter的doFilter方法
2、EmployeeController的update方法
3、MyMetaObjectHandler的updateFill
方法可以在上面的三个方法中分别加入下面代码(获取当前线程id):
test

什么是ThreadLocal?
ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
ThreadLocal常用方法:

  • public void set(T value)设置当前线程的线程局部变量的值
  • public T get()返回当前线程所对应的线程局部变量的值
    我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id,并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id),然后在MyMetaObjectHandler的updateFill方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值(用户id)。

实现步骤:

  1. 编写BaseContext工具类,基于ThreadLocal封装的工具类 工具类注意方法设置静态
  2. LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
  3. 在MyMetaObjectHandler的方法中调用BaseContext获取登录用户的id

BaseContext工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 基于ThreadLocal封装工具类,用户保存和获取当前登录用户id
*/
public class BaseContext {

static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

/**
* 设置线程id为用户id
* @param id
*/
public static void setCurrentId(Long id){
threadLocal.set(id);
}

/**
* 获取线程id(用户id)
* @return
*/
public static Long getCurrentId(){
Long userId = threadLocal.get();
return userId;
}
}

MyMetaObjectHandler类
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
/**
* 自定义元数据对象处理器
*/
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* 插入时对公共字段进行填充
* @param metaObject
*/
@Override
public void insertFill(MetaObject metaObject) {
log.info("在插入时填写公共字段...");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("createUser",BaseContext.getCurrentId());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}

/**
* 更新时对公共字段进行填充
* @param metaObject
*/
@Override
public void updateFill(MetaObject metaObject) {
log.info("在更新时填写更新字段...");
log.info(metaObject.toString());
metaObject.setValue("updateTime",BaseContext.getCurrentId());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}
}

功能完善完毕

新增分类

需求分析

后台系统中可以管理分类信息,分类包括两种类型,分别是菜品分类和套餐分类。当我们在后台系统中添加菜品时需要选择一个菜品分类,当我们在后台系统中添加一个套餐时需要选择一个套餐分类,在移动端也会按照菜品分类和套餐分类来展示对应的菜品和套餐。

主要就是完善这两个接口的添加功能

数据模型

开发阶段需求方分析完毕,此时应该去关注数据库中创建的数据模型,了解数据之间的区别以及设定方式。

代码开发

  1. 添加分类功能
    在开发业务功能前,先将需要用到的类和接口基本结构创建好:
  • 实体类Category(直接从课程资料中导入即可)
  • Mapper接口CategoryMapper
  • 业务层接口CategoryService
  • 业务层实现类CategoryServicelmpl
  • 控制层CategoryController

这里是接口测试:
可以看到

由于请求数据是json传入后端,所以接口必须使用Requestody注解,这是由前端设定,后端按照约定开发

添加菜品功能,直接调用service中的save即可

1
2
3
4
5
@PostMapping
public R<String> save(@RequestBody Category category){
categoryService.save(category);
return R.success("添加成功!");
}

异常在全局捕获器中被捕获,不需要做额外判断

  1. 分页查询功能

    这是对应接口的信息,上面做过一次应该比较熟悉
    类似的操作,再来一次
    建议写在service层
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Override
    public R<Page> page(int page, int pageSize) {
    // 1. 分页构造器
    Page<Category> pageInfo = new Page<>();
    // 2. 条件构造器
    LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();

    queryWrapper.orderByDesc(Category::getUpdateTime);

    // 直接返回构造器即可
    page(pageInfo,queryWrapper); // 执行条件映射

    return R.success(pageInfo);
    }
  2. 删除分类
  • 需求分析
    在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。
  • 业务开发
    在开发代码之前,需要梳理一下整个程序的执行过程:
    1、页面发送ajax请求,将参数(id)提交到服务端
    2、服务端Controller接收页面提交的数据并调用Service删除数据
    3、Service调用Mapper操作数据库
    接口状态:

    通过问号形式传参就不需要处理,直接写上即可,因为这里几乎明确了告诉mapping就用这个接收这个参数,所以可以不用添加对应注解,而无❓在前的就需要添加@PathVariable注解
    Controller层
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
        /**
    * 根据id删除分类
    * @param ids
    * @return
    */
    @DeleteMapping
    public R<String> delete(Long ids){
    log.info("删除分类,id为:{}",ids);

    categoryService.remove(ids);
    // categoryService.removeById(ids);
    return R.success("分类信息删除成功");
    }
    Service层
    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
    @Autowired
    private DishService dishService;
    @Autowired
    private SetmealService setmealService;
    @Override
    public R<Page> page(int page, int pageSize) {
    // 1. 分页构造器
    Page<Category> pageInfo = new Page<>();
    // 2. 条件构造器
    LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();

    queryWrapper.orderByDesc(Category::getUpdateTime);

    // 直接返回构造器即可
    page(pageInfo,queryWrapper); // 执行条件映射

    return R.success(pageInfo);
    }

    @Override
    public void remove(Long id) {
    // 1. 查询分类下是否有菜品关联
    LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
    dishLambdaQueryWrapper.eq(Dish::getCategoryId, id);
    int count1 = dishService.count(dishLambdaQueryWrapper);
    if (count1 > 0) {
    // 抛出异常,表明分类下有菜品关联
    throw new CustomException("当前分类下关联了菜品,不能删除");
    }
    // 2. 查询分类是否与套餐关联
    LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
    setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId, id);
    int count2 = setmealService.count(setmealLambdaQueryWrapper);
    if (count2 > 0) {
    // 抛出异常,表明分类下有套餐关联
    throw new CustomException("当前分类下关联了套餐,不能删除");
    }

    super.removeById(id);
    }
    这个业务重点是检查删除的分类是否关联了菜品或者套餐,这里需要反馈异常到页面提供给使用人员看,那么此时就需要抛出异常,使用到全局异常捕获器去反馈,为了可控,异常类型需要手动抛出,那么此时需要写个自定义异常类
    1
    2
    3
    4
    5
    6
    7
    8
    /**
    * 自定义业务异常类
    */
    public class CustomException extends RuntimeException{
    public CustomException(String msg){
    super(msg);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    * 自定义异常捕获
    * @param exception
    * @return
    */
    @ExceptionHandler(CustomException.class)
    public R<String> exceptionHandle(CustomException exception){
    log.error(exception.getMessage());

    return R.error(exception.getMessage());
    }
  1. 修改分类
    直接看接口

  • 业务开发:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
    * 修改分类信息
    * @param category
    * @return
    */
    @PutMapping
    public R<String> update(@RequestBody Category category){
    categoryService.updateById(category); // ById并非是id,而是实体类对象
    return R.success("更新成功!");
    }

    菜品部分修改

    业务目录:
  1. 文件上传下载
  2. 新增菜品
  3. 菜品信息分页查询
  4. 修改菜品心

文件交互

前置学习

上传介绍:

目前一些前端组件库也提供了相应的上传组件,但是底层原理还是基于form表单的文件上例如ElementUl中提供的upload上传组件:

服务端要接收客户端页面上传的文件,通常都会使用Apache的两个组件:

  • commons-fileupload
  • commons-io

Spring框架在spring-web包中对文件上传进行了封装,大大简化了服务端代码,我们只需要在Controller的方法中声明一个MultipartFile类型的参数即可接收上传的文件,例如:

但是底层还是借用了apache的组件

下载介绍:
文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程。
通过浏览器进行文件下载,通常有两种表现形式:

  • 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
  • 直接在浏览器中打开
    通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。

代码开发

首先我们来看接口可以提供的信息

url路径,以及请求方式post

  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
    // 前置:获取在配置文件中设置的路劲
    @Value("${Reggie.path}")
    private String basePath;

    @PostMapping("/upload")
    public R<String> upload(MultipartFile file){

    // 1. 获取对应文件的文件名称
    String originalFilename = file.getOriginalFilename();
    // 2. 对文件名进行后缀截取
    String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
    // 3. 使用UUID生成新文件名称并进行拼接
    String s = UUID.randomUUID().toString();
    String fileName = s + suffix;
    // 4. 对配置文件设定的文件夹是否存在进行判断
    File path = new File(basePath);
    if(!path.exists()){
    // 不存在
    path.mkdir();
    }
    // 5. 对上传的文件进行转存
    try {
    file.transferTo(new File(basePath + fileName));
    } catch (IOException e) {
    throw new RuntimeException(e);
    }
    return R.success(fileName);
    }
  2. 文件下载实现
    文件下载需要获取文件输出流,重定向对浏览器页面进行输出需要使用到 HttpServletResponse response 获取

    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
    /**
    * 文件下载功能
    * @param name
    * @param response
    */
    @GetMapping("/download")
    public void download(String name , HttpServletResponse response){

    try {
    // 1. 文件流输入 读取文件内容
    FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));

    // 2. 文件流输出 将文件内容写回浏览器
    ServletOutputStream outputStream = response.getOutputStream();

    // 作用是使客户端浏览器区分不同种类的数据,并根据不同的MIME调用浏览器内不同的程序嵌入模块来处理相应的数据。
    response.setContentType("image/jpeg");

    int len = 0;
    byte[] bytes = new byte[1024];

    while((len = fileInputStream.read(bytes)) != -1){
    outputStream.write(bytes,0,len);
    outputStream.flush();
    }
    // 3. 关闭资源
    fileInputStream.close();
    outputStream.close();

    } catch (Exception e) {
    throw new RuntimeException(e);
    }
    }

文件操作部分记录

学习到的地方

  • 文件上传 学习了几个api,对文件进行存储重定向 文件后缀截取,使用字符串的subString,使用“.”来分割
    学会设置环境变量并在程序中被调用,对空文件进行判断创建
  • 文件下载 文件流输出的复习,使用write api的固定用法 bytes
  • 接收参数的方式 注解分类:@RequestParam 可省略,针对Get请求 @PathVariable 路径占位符 @RequestBody Post请求 json数据接收专属

学习到一个前端操作,对被隐藏的密码进行页面显示,只需要F12将密码对应的方式改成text即可

一个小问题:(为什么在Springboot中为什么有时候使用Autowired的时候会被警告,而使用Resource注解不会?)
Autowired是Spring提供的,它是特定IoC提供的特定注解,这就导致了应用与框架的强绑定,一旦换用了其他的IoC框架,是不能够支持注入的。而Resource是java提供的,它是Java标准,平常使用的IoC容器应当去兼容它,这样即使更换容器,也可以正常工作。

新增菜品

业务需求

在开发代码之前,需要梳理一下新增菜品时前端页面和服务端的交互过程:

  1. 页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中
  2. 页面发送请求进行图片上传,.请求服务端将图片保存到服务器
  3. 页面发送请求进行图片下载,将上传的图片进行回显
  4. 点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。

首先是第一个业务修改

对应controller层

1
2
3
4
5
6
7
8
9
/**
* 查询可供选择的分类
* @param category
* @return
*/
@GetMapping("/list")
public R<List<Category>> list(Category category){
return categoryService.list(category);
}

1
2
3
4
5
6
7
8
9
10
11
@Override
public R<List<Category>> list(Category category) {
// 创建条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
// 填充条件构造器条件
queryWrapper.eq(category.getType()!=null,Category::getType,category.getType());
// 添加查询到之后的排序条件
queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
List<Category> list = categoryService.list(queryWrapper);
return R.success(list);
}

添加菜品接口实现

首先出现一个问题:

这里是不光涉及到一个表的数据,所以单独一个Dish不足以接收,所以这里就出现了一个新的数据传输对象-DTO

DTO:全称为Data Transfer Object,即数据传输对象,一般用于展示层与服务层之间的数据传输。

这里直接使用一个DTO数据进行接收
Controller层

1
2
3
4
5
@PostMapping
public R<String> addDish(@RequestBody DishDto dishDto){
dishService.saveWithFlavor(dishDto);
return R.success("菜品添加成功!");
}

Service层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Autowired
private DishFlavorService dishFlavorService;
@Override
public void saveWithFlavor(DishDto dishDto) {
// 首先对dish的内容进行封装
this.save(dishDto);
// 对flavor中的内容进行封装 其中flavor不存在id属性,所以此处需要额外遍历添加
List<DishFlavor> flavors = dishDto.getFlavors();
Long dishId = dishDto.getId();
flavors = flavors.stream().map((flavor)->{
flavor.setDishId(dishId);
return flavor;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}

菜品信息分页功能实现

这是第三次接触分页,相当于进行一次复习,有段时间没碰项目,所以此处会写的比较详细

具体需求

  1. 页面(backend/page/food/list.html)发送ajax请求,将分页查询参数(page、pageSize、name)
    提交到服务端,获取分页数据
  2. 页面发送请求,请求服务端进行图片下载,用于页面图片展示

三个参数分别代表对应对应页码以及对应的尺码,和需要可能会进行查询的名称

对于此处返回值设置为page类型,基于MP实现分页有三个步骤 1. 分页构造器 2. 条件构造器 3. 排序条件

在条件构造器中一般是,前一个设置不为空的条件,之后设置匹配条件

最后在本身基于MP的page方法中添入这两个构造器即可

stream流:使用map方法就是将list集合中的每个元素单独的拿出来,然后进行依次赋值,最后注意还需要进行收集

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public R<Page> page(int page, int pageSize, String name) {
// 1. 创建分页构造器
Page<Dish> pageInfo = new Page<>(page,pageSize);
// 2. 创建条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(name!=null, Dish::getName,name);

// 3. 排序条件
queryWrapper.orderByDesc(Dish::getUpdateTime);

page(pageInfo,queryWrapper);

return R.success(pageInfo);
}

这样可以实现分页查询,但是出现了一个问题就是category的Name无法进行显示,那么此时需要对项目进行改进

此时改进的方法就是和stream进行拷贝
但是Page类中records属性不需要进行拷贝因为需要对其进行相应的赋值操作,所以此时需要处理一下
在基础处理之后再进行拷贝操作

代码开发:

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 R<Page> page(int page, int pageSize, String name) {
// 1. 创建分页构造器
Page<Dish> pageInfo = new Page<>(page,pageSize);
Page<DishDto> dishDtoPageInfo = new Page<>();

// 2. 创建条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(name!=null, Dish::getName,name);

// 3. 排序条件
queryWrapper.orderByDesc(Dish::getUpdateTime);

page(pageInfo,queryWrapper);

// 进行一次拷贝
BeanUtils.copyProperties(pageInfo,dishDtoPageInfo,"records");

List<Dish> records = pageInfo.getRecords();

// 对菜品分类进行特殊处理
List<DishDto>list = records.stream().map((dish) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(dish,dishDto);
Long categoryId = dish.getCategoryId();
// 给dishDto中的菜品分类名称赋值
Category category = categoryService.getById(categoryId);
if(category != null){
dishDto.setCategoryName(category.getName());
}
return dishDto;
}).collect(Collectors.toList());

dishDtoPageInfo.setRecords(list);

return R.success(dishDtoPageInfo);
}

处理结束

修改菜品

需求分析:

首先是查询单个菜品的接口实现

第二是一个对dishDto类型保存处理

难度出现在进行多表操作,往往是通过一个中间DTO子类去实现数据交互

根据菜品id查询菜品同时进行回显吗,实现对应接口即可

实现功能1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public R<DishDto> getByIdWithFlaovr(Long id) {
Dish dish = getById(id);

DishDto dishDto = new DishDto();

BeanUtils.copyProperties(dish,dishDto);
// 这里只需要对flavor进行相应处理
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,id);
List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);

dishDto.setFlavors(flavors);
return R.success(dishDto);
}

实现功能2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Override
public R<String> updateWithFlaovr(DishDto dishDto) {
// 修改dish基本信息
updateById(dishDto);

// 删除flavor中的存储
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dishDto.getId());
dishFlavorService.remove(queryWrapper);

// 重新进行信息插入
List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map(flavor -> {
flavor.setDishId(dishDto.getId());
return flavor;
}).collect(Collectors.toList());

dishFlavorService.saveBatch(flavors);

return R.success("修改成功!");
}

套餐管理

同样难点在于多表操作,与上面部分类似,主要是几个接口的构建,用到的有多表查询,其中借助了dto操作,之后是分页page操作

需求分析:
在开发代码之前,需要梳理一下新增套餐时前端页面和服务端的交互过程:

  1. 页面(backend/page/combo/add.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中
  2. 页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中
  3. 页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中
  4. 页面发送请求进行图片上传,请求服务端将图片保存到服务器
  5. 页面发送请求进行图片下载,将上传的图片进行回显
  6. 点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端

共六次交互过程,其中4,5已被实现 需要处理的接口是1,2,3,6(但实际上这里并非开发顺序)

我们按照业务逻辑的顺序进行记录:
分页查询套餐展示

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
@Override
public R<Page> page(int page, int pageSize, String name) {
// 创建分页构造器
Page<Setmeal> pageInfo = new Page<>(page,pageSize);
Page<SetmealDto> setmealDtoPage = new Page<>();
// 创建条件构造器
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(name != null,Setmeal::getName,name);
// 添加排序条件
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
// 进行赋值
page(pageInfo,queryWrapper);
// 对page中的基础信息进行拷贝
BeanUtils.copyProperties(pageInfo,setmealDtoPage,"records");
List<Setmeal> records = pageInfo.getRecords();
// 遍历赋值操作
List<SetmealDto> list = records.stream().map(record -> {
SetmealDto setmealDto = new SetmealDto();
// 基本信息进行拷贝
BeanUtils.copyProperties(record, setmealDto);
// 剩余信息赋值
Long categoryId = record.getCategoryId();
String categoryName = categoryService.getById(categoryId).getName();
setmealDto.setCategoryName(categoryName);
return setmealDto;
}).collect(Collectors.toList());

setmealDtoPage.setRecords(list);

return R.success(setmealDtoPage);
}

对于dto分页类拷贝时,需要忽略的属性是record,原因是他们的泛型不同

请求服务端获取菜品分类数据并展示到添加菜品窗口中:

1
2
3
4
5
6
7
8
9
/**
* 查询可供选择的分类
* @param category
* @return
*/
@GetMapping("/list")
public R<List<Category>> list(Category category){
return categoryService.list(category);
}

Service层
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 根据类别查询对应菜品
* @param category
* @return
*/
@Override
public R<List<Category>> list(Category category) {
// 创建条件构造器
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
// 填充条件构造器条件
queryWrapper.eq(category.getType()!=null,Category::getType,category.getType());
// 添加查询到之后的排序条件
queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
List<Category> list = categoryService.list(queryWrapper);
return R.success(list);
}

保存套餐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 添加套餐
* @param setmealDto
* @return
*/
public R<String> saveWithDish(SetmealDto setmealDto) {
// 保存其他信息 此时操作表为:setmeal
save(setmealDto);
// 对套餐表进行 setmeal_dish
List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
setmealDishes = setmealDishes.stream().map(setmealDish -> {
// 对为null的属性进行赋值
setmealDish.setSetmealId(setmealDto.getId());
return setmealDish;
}).collect(Collectors.toList());

setmealDishService.saveBatch(setmealDishes);
return R.success("设置套餐成功!");
}

requestParam注解需要注意:接收的参数来自于请求头

responseBody注解是什么?这里8

p.s. 当处理业务逻辑时,可以选择优先去写出sql语句之后再写逻辑

删除套餐

需要注意这里是同时对两张表进行操作

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
/**
* 根据id删除分类,删除之前需要进行判断
* @param id
*/
@Override
public void remove(Long id) {
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据分类id进行查询
dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
int count1 = dishService.count(dishLambdaQueryWrapper);

//查询当前分类是否关联了菜品,如果已经关联,抛出一个业务异常
if(count1 > 0){
//已经关联菜品,抛出一个业务异常
throw new CustomException("当前分类下关联了菜品,不能删除");
}

//查询当前分类是否关联了套餐,如果已经关联,抛出一个业务异常
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加查询条件,根据分类id进行查询
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
int count2 = setmealService.count();
if(count2 > 0){
//已经关联套餐,抛出一个业务异常
throw new CustomException("当前分类下关联了套餐,不能删除");
}

//正常删除分类
super.removeById(id);
}

权限认证

此处记录的是:spring sercuity

使用方式:一个配置类即可

作用:

  1. 登录认证 权限认证 两者是分开的 在配置类中分别是使用两个方法进行验证
  2. 登录认证时:可以使用方法去传入密码校验方式 ,但查询sql业务判断用户是否存在是需要自己去写的
  3. 权限认证:充当过滤器,赋予登录用户权限

需要使用可查文档

项目优化

mysql主从复制

一个主库可以有多个从库
示意:

MySQL复制过程分成三步:

  • master将改变记录到二进制日志(binary log)
  • slave将master的binary log拷贝到它的中继日志(relay log)
  • slave重做中继日志中的事件,将改变应用到自己的数据库中

主库需要创建一个用户给予从库,使其具有部分信息

读写分离

面对日益增加的系统访问量,数据库的吞吐量面临着巨大瓶颈。
对于同一时刻有大量并发读操作和较少写操作类型的应用系统来说,将数据库拆分为主库和从库,主库负责处理事务性的增删改操作,从库负责处理查询操作,能够有效的避免由数据更新导致的行锁,使得整个系统的查询性能得到极大的改善。

nginx

一种web服务器,和tomcat(服务)不同

  • 正向代理

    不直接去访问,通过代理服务器去访问,访问的结果从该服务器返回到本机
  • 反向代理

    这二者的区别就在于前者是明确知道存在这样一个代理服务存在的,而后者不知道
    作用统一入口,组成局域网,隐藏了真正的web服务器

    应用 负载均衡

    使用的原理还是反向代理,但是对应的是多台服务器

    使用轮询算法(循环)

负载均衡策略:

前后端分离项目开发

主要变化,工程拆分,后端部署再tomcat,前端静态资源部署在nginx中

开发流程:

定义接口:
使用YApi 常用的api管理工具

Swagger技术:
目的:生成接口

底层集成swagger的工具

开启knf4j需要使用的两个注解:

返回类型,文件配置

swagger常用注解(使得接口文档可读性更高)用可查

项目部署

服务部署:

nginx反向代理url重写逻辑:

首先部署前端项目,开启后端服务,繁琐的操作可以使用脚本文件统一执行

如果出现项目内容更新只需进行git推送,之后再重新执行脚本文件即可,这是一种统一化执行的思想更加快捷,学会使用脚本去简化繁琐操作

此时也更明白之前学习Linux本地操作的意义,项目上线后端都是linux系统操作,线上服务器也是Linux系统,所以此时重要性体现出了