项目总结-瑞吉外卖

Posted Lee_ing

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了项目总结-瑞吉外卖相关的知识,希望对你有一定的参考价值。

软件开发基础

分工:

流程:

01项目介绍

组成部分:系统管理后台、移动端

开发分期:

技术选型:

架构:

角色:

后台系统分析:

登录页面:

登录成功后,进入首页面(员工管理):

分类管理页面:

菜品管理页面:

套餐管理页面:

订单明细页面:

02开发环境搭建

数据库搭建

创建数据库-->导入资料中的表文件(db_reggie.sql)

命令行形式:

mysql> use reggie.sql;
Database changed
mysql> source D:\\db_reggie.sql;

maven项目搭建

  1. 创建新项目:检查项目的编码、maven仓库配置、jdk配置
  2. 导入pom.xml文件

父功能--springboot

<parent>
  <groupId>org.springframework.boot</groupId> 
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.4.5</version><relativePath/> 
</parent>

jdk版本

<properties>
   <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
   <maven.compiler.source>1.8</maven.compiler.source>
   <maven.compiler.target>1.8</maven.compiler.target>
</properties>

maven依赖坐标及插件

pom部分代码
 <dependencies>
    <!--spring-boot-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!--spring-boot单元测试-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
    <!--spring-boot-web应用-->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
      <scope>compile</scope>
    </dependency>

     <!--mybatis-plus-->
    <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-boot-starter</artifactId>
      <version>3.4.2</version>
    </dependency>
    <!--lombok-->
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.20</version>
    </dependency>
    <!--将对象转为json-->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>fastjson</artifactId>
      <version>1.2.76</version>
    </dependency>

    <!--通用语言包-->
    <dependency>
      <groupId>commons-lang</groupId>
      <artifactId>commons-lang</artifactId>
      <version>2.6</version>
    </dependency>

    <!--mysql驱动包-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <scope>runtime</scope>
    </dependency>
    
    <!--ali数据源-->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid-spring-boot-starter</artifactId>
      <version>1.1.23</version>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>

  </dependencies>

  <!--spring-boot-maven插件-->
  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <version>2.4.5</version>
      </plugin>
    </plugins>
  </build>
  1. 配置文件application.yml
server:
  port: 8089 #tomcat端口号
spring:
  application:
    name: reggie_take_out  #应用的名称
  datasource: #数据源相关配置
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/riggle?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: root

  redis:
    host: 127.0.0.1
    port: 6379
    password:
    database: 0

  cache:
    redis:
      time-to-live: 1800000 #设置缓存有效期

#mybatis-plus配置
mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    
    #在映射实体或者属性时,将数据库中表名和宇段名中的下划线去掉,按照驼峰命名法映射(address_book表名--->AddressBook实体类名)

    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: ASSIGN_ID


riggle:
  path: D:\\img\\
  1. 编写启动类
@Slf4j //使用日志log.(同理:lombok库中,编写实体类时,加入注解,get\\set方法可以省略,)
@SpringBootApplication(exclude=DataSourceAutoConfiguration.class)
//@ServletComponentScan
//@EnableTransactionManagement
//@EnableCaching //开启缓存注解功能
public class ReggieApplication 
    public static void main(String[] args) 
        SpringApplication.run(ReggieApplication.class,args);
        log.info("项目启动成功");
    

  1. 导入前端资源
    配置类设置静态资源的映射
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport 
    /**
     * 静态映射
     * @param registry
     */
    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) 
        log.info("开始静态资源映射");
        registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
//请求命令的格式-->映射到资源地址
        registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
    


03后台登录功能开发

需求分析

1. 需求:\\src\\main\\resources\\backend\\page\\login\\login.html

2. 响应:点击登录后,会出现404,因为还没有写响应请求的处理器


以json的格式提交到服务端

3. 后端相关类:服务端创建相关的类:


通过controller把信息接收到,最后到数据库DB中查询

4. 数据模型:employee表

前端部分:

1. 点击登录时,代码中会调用loginApi方法(封装到了js文件中)

login.html核心代码
      methods: 
        async handleLogin() 
          this.$refs.loginForm.validate(async (valid) => 
            if (valid) 
              this.loading = true
              let res = await loginApi(this.loginForm)
              if (String(res.code) === \'1\') //1表示登录成功
                localStorage.setItem(\'userInfo\',JSON.stringify(res.data))
                window.location.href= \'/backend/index.html\'
               else 
                this.$message.error(res.msg)
                this.loading = false
              
            
          )
        
      

loginForm为提交的json数据
响应返回值res,有code、data、msg等属性;(所以后端处理最后的返回值需要有这些)
数据交互:页面response响应回的数据是json数据,后端将R对象转变为json
把数据存储在localStorage【F12中application里可以查看】

2. login.js文件中,通过ajax服务来发送请求;(对应上面的404错误)

js文件code
function loginApi(data) 
  return $axios(
    \'url\': \'/employee/login\',
    \'method\': \'post\',
    data
  )

后端开发

通用结果类:导入返回结果类R(响应前端)【common包】

R类
/**
 * 通用返回结果,服务端响应的数据最终都会封装成此对象
 *
 * @param <T>
 */
@Data
public class R<T> implements Serializable 

    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. 实体类:创建实体类Employee,和表employee进行映射(entity包中)

Employee
/**
 * 员工实体
 */
@Data
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;


    //这些都是公共字段,加入TableField注解,FieldFill.INSERT表示插入时填充字段

    //创建时间
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    //更新时间
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;


2. mapper接口(mapper包)
基于mybatis-plus,提供了相应的基础父类or接口

@Mapper
public interface EmployeeMapper extends BaseMapper<Employee>

3. service接口以及impl实现类(service包)

service接口:

public interface EmployeeService extends IService<Employee> 

实现类:

@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService 


4. controller类(controller包)

@Slf4j
@RestController
@RequestMapping("/employee")//根据请求url
public class EmployeeController 

    @Autowired //注入service接口
    private EmployeeService employeeService;

登录方法:(controller类的方法)

1. 逻辑:

2. 代码

  • 前端传入了一个json数据,接收数据时,需要加注解@RequestBody
  • HttpServletRequest request:如果登录成功后,把对象id存到session一份,想获取当前登录用户的话,可以随时获取(request.getSession)
  • 查询数据库:employeeService.getOne(wrapper);【索引中username是unique类型的,所以唯一,使用getOne】
    /**
     * 员工登录
     *
     * @param request
     * @param employee
     * @return
     */
    @PostMapping("login")
    public R<Employee> login(HttpServletRequest request, @RequestBody Employee employee) //传入需要和Employee类中的名称相对应
       //获取用户名和密码
        String username = employee.getUsername();
        String password = employee.getPassword();
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) 
            log.info("登录失败");
            return R.error("登录失败");
        
        password = DigestUtils.md5DigestAsHex(password.getBytes());
       
       //查询数据库
        QueryWrapper<Employee> wrapper = new QueryWrapper<>();
        wrapper.eq("username", username);
        //LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>();
        //wrapper.eq(Employee::getUsername, employee.getUsername());
        Employee result = employeeService.getOne(wrapper);

        //如果没有查到
        if (result == null) 
            log.info("登录失败没有查询结果");
            return R.error("登录失败");
        
        //查到了,对比密码
        if (!result.getPassword().equals(password)) 
            log.info("登录失败密码不对");
            return R.error("登录失败");
        
        //查到了,对比状态
        if (result.getStatus() != 1) 
            log.info("登录失败禁用");
            return R.error("账号不可用");
        

        //登录成功;将员工id存入session中
        request.getSession().setAttribute("employee", result.getId());

        return R.success(result);

    

退出功能:

1. 前端分析:

index.html
     methods: 
          logout() 
            logoutApi().then((res)=>
              if(res.code === 1)
                localStorage.removeItem(\'userInfo\')
                window.location.href = \'/backend/page/login/login.html\'
              
            )
          
    

logout()方法:logoutApi()

api/login.js
function logoutApi()
  return $axios(
    \'url\': \'/employee/logout\',
    \'method\': \'post\',
  )

logoutApi()中有请求方式

2. 退出方法:(controller类的方法)接收前端发送的请求

清理session中的用户id:操作session,需要HttpServletRequest request
返回结果:R

    /**
     * 退出登录,移除session
     *
     * @param request
     * @return
     */
    @PostMapping("/logout")
    public R<String> logout(HttpServletRequest request) 
        request.getSession().removeAttribute("employee");
        return R.success("退出成功");
    

完善功能:(过滤器/拦截器)

必须登录成功后才能进入系统首页面中;如果没有登录,需要跳转到登录页面

实现:

1、创建自定义过滤器LoginCheckFilter
2、在启动类上加入注解@ServletComponentScan
3、完善过滤器的处理逻辑

代码:

  1. 创建过滤器:(filter包)

注解:@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/")
"/
":所有的请求都拦截

/**
 * 检查用户是否已经完成登录
 */
@Slf4j
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
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;

        //获取本次请求的URI
        String requestURI = request.getRequestURI();// /backend/index.html
        log.info("拦截到请求:",requestURI);
        filterChain.doFilter(request,response);//放行
    
  1. 在启动类上加入注解@ServletComponentScan

  2. 处理拦截到的请求

LoginCheckFilter类
/**
 * 检查用户是否已经完成登录
 */
@Slf4j
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
public class LoginCheckFilter implements Filter
    //路径匹配器,支持通配符
    public 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();// /backend/index.html

        log.info("拦截到请求:",requestURI);

        //定义不需要处理的请求路径
        String[] urls = new String[]
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**",
                "/common/**",
                "/user/sendMsg",
                "/user/login",
        ;


        //2、判断本次请求是否需要处理
        boolean check = check(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;
        

        log.info("用户未登录");
        //5、如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        return;

    

    /**
     * 路径匹配,检查本次请求是否需要放行
     * @param urls
     * @param requestURI
     * @return
     */
    public boolean check(String[] urls,String requestURI)
        for (String url : urls) 
            boolean match = PATH_MATCHER.match(url, requestURI);
            if(match)
                return true;
            
        
        return false;
    


路径匹配器:
public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

用户没有登录,并不是直接跳页面。结合前端js代码,前端也有拦截器:输出流的方式往回写数据,前端接收到会自动页面跳转:

resources\\backend\\js\\request.js
// 响应拦截器(前端拦截器)
  service.interceptors.response.use(res => 
      console.log(\'---响应拦截器---\',res)
      // 未设置状态码则默认成功状态
      const code = res.data.code;
      // 获取错误信息
      const msg = res.data.msg
      console.log(\'---code---\',code)
      if (res.data.code === 0 && res.data.msg === "NOTLOGIN") // 返回登录页面
        // MessageBox.confirm(\'登录状态已过期,您可以继续留在该页面,或者重新登录\', \'系统提示\', 
        //     confirmButtonText: \'重新登录\',
        //     cancelButtonText: \'取消\',
        //     type: \'warning\'
        //   
        // ).then(() => 
        // )
        console.log(\'---/backend/page/login/login.html---\',code)
        localStorage.removeItem(\'userInfo\')
        window.top.location.href = \'/backend/page/login/login.html\'
       else 
        return res.data
      
    ,
.......

04员工管理业务开发

新增员工

1. 数据模型:是将新增页面录入的员工数据插入到employee表。

需要注意,employee表中对username字段加入了唯一约束,因为username是员工的登录账号,必须是唯一的
状态值默认为1

2. 开发逻辑

1、页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据

3. 代码实现

json格式数据需要@RequestBody Employee employee

/**
     * 新增员工
     *
     * @param employee
     * @return
     */
    @PostMapping
    public R<String> save(HttpServletRequest request, @RequestBody Employee employee) 
        log.info("新增员工,员工信息:", employee.toString());

        //设置初始密码123456,需要进行md5加密处理
         employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));

         employee.setCreateTime(LocalDateTime.now());
         employee.setUpdateTime(LocalDateTime.now());

        //获得当前登录用户的id
        Long empId = (Long) request.getSession().getAttribute("employee");//强转

        employee.setCreateUser(empId);
        employee.setUpdateUser(empId);

        employeeService.save(employee);
        return R.success("新增员工成功");
    

  1. 完善
    ①解决异常:(提交重复unique字段会报异常)

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

全局异常处理类(common包)

GlobalExceptionHandler
@Slf4j
@ControllerAdvice(annotations = RestController.class, Controller.class)//不管哪个类,只要加了这两个注解,就会被异常处理器处理
@ResponseBody//需要返回json数据
/**
 * 全局异常捕获
 */
public class GlobalExceptionHandler 

    /**
     * 异常处理方法
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) 
        log.error(ex.getMessage());
        //Duplicate entry \'zhangsan\' for key \'idx_username\'

        /**
         * 对于添加员工已存在的名字
         */
        if (ex.getMessage().contains("Duplicate entry")) 
            String[] split = ex.getMessage().split(" ");//数组对象
            String msg = split[2] + "已存在";
            return R.error(msg);
        
        return R.error("未知错误");
    


总结:请求-响应式模式

员工信息分页

1. 需求:

分页的方式来展示列表数据;
根据过滤条件进行查询

2. 代码逻辑

1、页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端

2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
5、页面接收到分页数据并通过ElementUl的Table组件展示到页面上

前端分析:list.html

前端的request.js在拦截器:拦截get请求的处理:把json数据解析出来,动态的追加到url地址后面【
Request URL: http://localhost:8080/employce/page?page=1&pagesize=10】

Vue中钩子函数:
......
        created() 
          this.init()
          this.user = JSON.parse(localStorage.getItem(\'userInfo\')).username
        ,
        mounted() 
        ,
        methods: 
          async init () #自定义init()
            #构造数据json
            const params = 
              page: this.page,
              pageSize: this.pageSize,
              name: this.input ? this.input : undefined
            
            #getMemberList封装到了member.js文件中
            await getMemberList(params).then(res => 
              if (String(res.code) === \'1\') 
                #前端需要这样的数据
                this.tableData = res.data.records || []
                this.counts = res.data.total
              
            ).catch(err => 
              this.$message.error(\'请求出错了:\' + err)
            )
          ,
..........

3. 代码实现

使用mybatis-plus提供的分页插件

配置分页插件:(config包下存放配置类)

/**
 * 配置MP的分页插件
 */
@Configuration//配置类的注解
public class MybatisPlusConfig 

    @Bean //表示需要spring来管理它
    public MybatisPlusInterceptor mybatisPlusInterceptor() //拦截器
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        //加入一个拦截器插件
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    

分页方法:(EmployeeController类)

使用Page泛型:根据前端,响应字段需要有records、total等字段
发送请求:刷新、查询、跳转到下一页【都会重新发送请求】

/**
     * 员工信息分页查询
     *
     * @param page     当前查询页码
     * @param pageSize 每页展示记录数
     * @param name     员工姓名 - 可选参数
     * @return
     */
    @GetMapping("/page")//get方式请求
    public R<Page> page(int page, int pageSize, String name) 
        //Page类是mybatis-plus封装好的
        log.info("page = ,pageSize = ,name = ", page, pageSize, name);
        //构造分页构造器
        Page pageInfo = new Page(page, pageSize);
        //构造条件构造器
        LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper();
        //添加过滤条件
        queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);
        //添加排序条件
        queryWrapper.orderByDesc(Employee::getUpdateTime);
        //执行查询
        employeeService.page(pageInfo, queryWrapper);

        return R.success(pageInfo);

    

启用or禁用员工账号

1. 需求

对某个员工账号进行启用或者禁用操作。

需要注意,只有管理员(admin用户)可以对其他普通用户进行启用、禁用操作,所以普通用户登录系统后启用禁用按钮不显示。

2.代码逻辑

1、页面发送ajax请求,将参数(id、status)提交到服务端

2、服务端Controller接收页面提交的数据并调用Service更新数据
3、Service调用Mapper操作数据库

前端分析:

点击启用/禁用按钮,如何发送请求

member/list.html
         //状态修改
          statusHandle (row) 
            this.id = row.id
            this.status = row.status
            this.$confirm(\'确认调整该账号的状态?\', \'提示\', 
              \'confirmButtonText\': \'确定\',
              \'cancelButtonText\': \'取消\',
              \'type\': \'warning\'
              ).then(() => 
              
             //enableOrDisableEmployee封装到了member.js中
              enableOrDisableEmployee( \'id\': this.id, \'status\': !this.status ? 1 : 0 ).then(res => 
                console.log(\'enableOrDisableEmployee\',res)
                if (String(res.code) === \'1\') 
                  this.$message.success(\'账号状态更改成功!\')
                  this.handleQuery()
                
              ).catch(err => 
                this.$message.error(\'请求出错了:\' + err)
              )
            )
          ,
member.js
// 修改---启用禁用接口
//  与  修改---添加员工 使用的是一个方法:所以路径是一样的,
function enableOrDisableEmployee (params) 
  return $axios(
    url: \'/employee\',
    method: \'put\',
    data:  ...params 
  )

已经实现了只有管理员才能看到 启用/禁用 按钮

            <el-button
              type="text"
              size="small"
              class="delBut non"
              @click="statusHandle(scope.row)"
              v-if="user === \'admin\'"
            >
               scope.row.status == \'1\' ? \'禁用\' : \'启用\' 
            </el-button>

3. 代码实现

本质上就是一个更新操作,也就是对status状态字段进行操作在Controller中创建update方法,此方法是一个通用的修改员工信息的方法【该方法可以与编辑员工信息通用,都是更新操作】

    /**
     * 根据id修改员工信息,如禁用,启用
     * 这是一个通用的方法,在修改员工信息的时候,可以直接用,
     * @param employee
     * @return
     */
    @PutMapping
    public R<String> update(HttpServletRequest request, @RequestBody Employee employee) 
    //因为返回值只要一个res.code,所以R<String>

        log.info(employee.toString());

        Long id = (Long) request.getSession().getAttribute("employee");
        employee.setUpdateTime(LocalDateTime.now());
        employee.setUpdateUser(id);//对当前登录用户进行修改
        employeeService.updateById(employee);

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

4. 代码修复

数据丢失问题

id从分页列表中取出来,页面返回数据没有问题;
但是点禁用按钮的时候,发送给我们的id就变化了【js对数据处理的时候会丢失精度,只能保证前16位,使得提交的id与数据库中的id不一致】

解决:在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串

1)提供对象转换器]acksonObjectMapper,基于Jackson进行Java对象到json数据的转换 (资料中已经提供,直接复制到项目中使用)
2)在WebMvcConfia配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行lava对象到
json数据的转换

对象转换器:

common/JacksonObjectMapper.java
/**
 * 对象映射器:基于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)
              //Long序列化器
                .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);
    

配置类中扩展消息转换器:

config/WebMvcConfig.java
........
    /**
     * 扩展mvc框架的消息转换器
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) 
        log.info("扩展消息转换器...");
        //创建消息转换器对象,webmvc包里提供的【将controller返回结果转为相应的json数据,输出流的方式响应给页面】
        MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
        //设置对象转换器,底层使用Jackson将Java对象转为json
        messageConverter.setObjectMapper(new JacksonObjectMapper());
        //将上面的消息转换器对象追加到mvc框架的转换器集合中
        converters.add(0,messageConverter);
    
.......

编辑员工信息

1. 需求

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

2. 代码逻辑

1、点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
【注意: add.html页面为公共页面,新增员工和编辑员工都是在此页面操作】
2、在add.html页面获取url中的参数[员工id]
3、发送ajax请求【一次请求】,请求服务端,同时提交员工id参数
4、服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
5、页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
6、点击保存按钮,发送ajax请求【两次请求】,将页面中的员工信息以json方式提交给服务端
7、服务端接收员工信息,并进行处理,完成后给页面响应【R.success】
8、页面接收到服务端响应信息后进行相应处理【提示修改成功】

前端分析:【页面跳转到add.html,并在url中携带参数[员工id]】

member/add.html
      created() 
          this.id = requestUrlParam(\'id\')//requestUrlParam封装到了index.js中
          this.actionType = this.id ? \'edit\' : \'add\'
          if (this.id) 
            this.init()
          
        ,
        mounted() 
        ,
        methods: 
          async init () 
            queryEmployeeById(this.id).then(res => 
              console.log(res)
              if (String(res.code) === \'1\') 
                console.log(res.data)
                this.ruleForm = res.data
                this.ruleForm.sex = res.data.sex === \'0\' ? \'女\' : \'男\'
                // this.ruleForm.password = \'\'
               else 
                this.$message.error(res.msg || \'操作失败\')
              
            )
          ,

获取url中id信息的方法【this.id = requestUrlParam(\'id\')】

js/index.js
//获取url地址上面的参数
function requestUrlParam(argname)
  var url = location.href //获取完整的请求url路径
  var arrStr = url.substring(url.indexOf("?")+1).split("&")
  for(var i =0;i<arrStr.length;i++)
  
      var loc = arrStr[i].indexOf(argname+"=")
      if(loc!=-1)
          return arrStr[i].replace(argname+"=","").replace("?","")
      
  
  return ""

发送ajax请求:queryEmployeeById(this.id)

api/member.js
// 修改页面反查详情接口
function queryEmployeeById (id) 
  return $axios(
    url: `/employee/$id`,
    method: \'get\'
  )

3. 代码实现【创建方法处理请求】

使用路径变量@PathVariable("id") Long id
url地址栏方式的请求:@GetMapping("/id")

回显数据:【第一次请求】

    /**
     * 根据id查询员工信息
     *
     * @param id
     * @return
     */
    @GetMapping("/id")
    public R<Employee> getById(@PathVariable("id") Long id) //@PathVariable路径变量
        log.info("根据id查询员工信息...");
        Employee employee = employeeService.getById(id);
        if (employee != null) 
            return R.success(employee);
        
        return R.error("没有查询到对应员工信息");
    

保存数据:【第二次请求】
与启用/禁用使用的是同一个update方法;@PutMapping

问题完善:公共字段自动填充

1. 问题

在新增员工时需要设置创建时间、创建人、修改时间、修改人等字段,
在编辑员工时需要设置修改时间和修改人等字段。
这些字段属于公共字段,也就是很多表中都有这些字段,

2.解决方法:Mybatis plus提供的公共字段自动填充功能

在插入或者更新的时候为指定字段赋予指定的值,
好处:统一对这些字段进行处理,避免了重复代码

实现步骤:

1、在实体类的属性上加入@TableField注解,指定自动填充的策略
【默认不处理DEFAULT、插入时填充字段INSERI、更新时填充字段UPDATE、插入和更新时填充字段INSERT_UPDATE】
2、按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetabiectHandler接口

  • Employee类:【在公共属性上 加入@TableField注解】
//这些都是公共字段,加入TableField注解,FieldFill.INSERT表示插入时填充字段

    //创建时间
    @TableField(fill = FieldFill.INSERT)//填充策略:插入时填充字段
    private LocalDateTime createTime;

    //更新时间
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
  • 元数据对象处理器:【common包】

@Component让spring框架管理它
因为没有request对象,所以使用线程工具类获取当前id【ThreadLocal类】

MyMetaObjecthandler
/**
 * 自定义元数据对象处理器
 */
@Component
@Slf4j
public class MyMetaObjecthandler implements MetaObjectHandler 
    /**
     * 插入操作,自动填充
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) 
        log.info("公共字段自动填充[insert]...");
        log.info(metaObject.toString());
        
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime",LocalDateTime.now());
        //该类中不能获得Session中的对象【因为没有request对象】。所以使用线程工具类
        metaObject.setValue("createUser",BaseContext.getCurrentId());
        metaObject.setValue("updateUser",BaseContext.getCurrentId());
    

    /**
     * 更新操作,自动填充
     * @param metaObject
     */
    @Override
    public void updateFill(MetaObject metaObject) 
        log.info("公共字段自动填充[update]...");
        log.info(metaObject.toString());

        metaObject.setValue("updateTime",LocalDateTime.now());
        metaObject.setValue("updateUser",BaseContext.getCurrentId());
    

  • 修改之前方法:把新增员工、更新方法中的字段注释掉:
//        //这些都是公共字段,因为许多表中都有这些字段
         //设置了公共字段填充,所以就不需要再写了,
//        employee.setCreateTime(LocalDateTime.now());
//        employee.setUpdateTime(LocalDateTime.now());
//
//        //获得当前登录用户的id
//        Long empId = (Long) request.getSession().getAttribute("employee");
//
//        employee.setCreateUser(empId);
//        employee.setUpdateUser(empId);

ThreadLocal类:

  • ThreadLocal类:获取当前线程id

我们可以在LoginCheckFilter的doFilter方法中获取当前登录用户id
并调用ThreadLocal的set方法来设置当前线程的线程局部变量的值(用户id),
然后在MyMetaobjectHandler的updateFil方法中调用ThreadLocal的get方法来获得当前线程所对应的线程局部变量的值 (用户id)

由于线程相同:

客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:
1、LoginCheckFilter的doFilter方法
2、EmployeeController的update方法
3、MyMetaObjectHandler的updateFill方法
可以在上面的三个方法中分别加入下面代码 (获取当前线程id)来证明相同:
long id = Thread. currentThread().getId();
log. info("线程id:",id);

正是由于线程相同,所以可以使用ThreadLocal类:【线程的局部变量】

ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
ThreadLocal常用方法:
public void set(T value)设置当前线程的线程局部变量的值
public T get()返回当前线程所对应的线程局部变量的值

  • 实现步骤

1、编写BaseContext工具类,基于ThreadLoca[封装的工具类
2、在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id
3、在MyMetaobiectHandler的方法中调用BaseContext获取登录用户的id

BaseContext工具类:【common包】

作用范围:某个线程之内;【每次请求都是一个新的线程】

common/BaseContext.java
/**
 * 基于ThreadLocal封装的工具类,用户保存和获取当前登录用户id
 */
public class BaseContext 

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

    public static void setCurrentId(Long id) 
        threadLocal.set(id);
    

    public static Long getCurrentId() 
        return threadLocal.get();
    

Set调用:LoginCheckFilter的doFilter方法中调用BaseContext

filter\\LoginCheckFilter.java
        //4、判断登录状态,如果已登录,则直接放行
        if(request.getSession().getAttribute("employee") != null)
            log.info("用户已登录,用户id为:",request.getSession().getAttribute("employee"));


            //程序走到这里,表示已经登陆了,可以把用户id存到BaseContext,基于ThreadLocal封装的工具类
            //这样就可以在公共字段自动填充的方法中得到用户id了。
            Long empId = (Long) request.getSession().getAttribute("employee");
            BaseContext.setCurrentId(empId);//自己写的BaseContext类

            filterChain.doFilter(request,response);
            return;
        

Get调用:MyMetaobiectHandler的方法中调用BaseContext

common\\MyMetaObjecthandler.java
        //该类中不能获得Session中的对象。所以使用线程工具类
        metaObject.setValue("createUser",BaseContext.getCurrentId());
        metaObject.setValue("updateUser",BaseContext.getCurrentId());

05分类管理业务


新增分类

1. 需求分析

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

2. 数据模型

category表:name[unique]

3. 代码逻辑

需要用到的类和接口基本结构创建好

实体类Category、Mapper接口CategoryMapper、业务层接口CategoryService、业务层实现类CategoryServicelmpl、控制层CategoryController

entity\\Category.java
/**
 * 分类
 */
@Data
public class Category implements Serializable 

    private static final long serialVersionUID = 1L;

    private Long id;

    //类型 1 菜品分类 2 套餐分类
    private Integer type;

    //分类名称
    private String name;

    //顺序
    private Integer sort;

    //创建时间
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    //更新时间
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;
    
    //创建人
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    
    //修改人
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;

mapper\\CategoryMapper.java
@Mapper
public interface CategoryMapper extends BaseMapper<Category> 

Service\\CategoryService.java
public interface CategoryService extends IService<Category> 


Service\\impl\\CategoryServiceImpl.java
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService 


controller\\javaCategoryController.java
/**
 * 分类管理
 */
@RestController
@RequestMapping("/category")
@Slf4j
public class CategoryController 
    @Autowired
    private CategoryService categoryService;

实现步骤:

1、页面(backend/page/category/list.html)发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端

2、服务端Controller接收页面提交的数据并调用Service将数据进行保存
3、Service调用Mapper操作数据库,保存数据

前端分析:

点击确定按钮的时候,执行submitForm方法

category\\list.html
       //数据提交
          submitForm(st) 
              const classData = this.classData
              const valid = (classData.name === 0 ||classData.name)  && (classData.sort === 0 || classData.sort)
              if (this.action === \'add\') 
                if (valid) 
                  const reg = /^\\d+$/
                  if (reg.test(classData.sort)) 
                    addCategory(\'name\': classData.name,\'type\':this.type, sort: classData.sort).then(res => 
                      console.log(res)
                      if (res.code === 1) 
                        this.$message.success(\'分类添加成功!\')
                        if (!st) 
                          this.classData.dialogVisible = false
                         else 
                          this.classData.name = \'\'
                          this.classData.sort = \'\'
                        
                        this.handleQuery()
                       else 
                        ............

其中,addCategory方法来发送请求。

api\\category.js
// 新增接口
const addCategory = (params) => 
  return $axios(
    url: \'/category\',
    method: \'post\',
    data:  ...params 
  )

4. 代码实现【controller包CategoryController.java】

返回值类型R<String>:根据前端代码可见,只用到了一个code【res.code】
@RequestBody Category category:json形式的数据
如果unique字段重复,会进入全局异常处理器中

    /**
     * 新增分类
     *
     * @param category
     * @return
     */
    @PostMapping
    public R<String> save(@RequestBody Category category) 
        log.info("category:", category);
        categoryService.save(category);
        return R.success("新增分类成功");
    

分类信息分页查询

同员工管理的分页一样,只是操作的表不一样。

1. 代码逻辑

1、页面发送ajax请求,将分页查询参数(page、pageSize)提交到服务端

2、服务端Controller接收页面提交的数据并调用Service查询数据
3、Service调用Mapper操作数据库,查询分页数据
4、Controller将查询到的分页数据响应给页面
5、页面接收到分页数据并通过ElementUl的Table组件展示到页面上

前端分析:

list.html的钩子函数内的方法(getCategoryPage方法)【该方法封装在了category.js里】

2. 代码实现

    /**
     * 分页查询
     *
     * @param page
     * @param pageSize
     * @return
     */
    @GetMapping("/page")
    public R<Page> page(int page, int pageSize) 
        //分页构造器
        Page<Category> pageInfo = new Page<>(page, pageSize);
        //条件构造器
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        //添加排序条件,根据sort进行排序
        queryWrapper.orderByAsc(Category::getSort);

        //分页查询
        categoryService.page(pageInfo, queryWrapper);
        return R.success(pageInfo);
    

删除分类

1. 需求分析

对某个分类进行删除操作。【需要判断是否关联菜品】

需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除

2. 代码逻辑

1、页面发送ajax请求,将参数(id)提交到服务端

2、服务端Controller接收页面提交的数据并调用Service删除数据
3、Service调用Mapper操作数据库

前端分析:

list.html删除按钮绑定了deleteHandle事件,并把id动态的传过去。

category\\list.html
.........
        //删除
          deleteHandle(id) 
            this.$confirm(\'此操作将永久删除该文件, 是否继续?\', \'提示\', 
              \'confirmButtonText\': \'确定\',
              \'cancelButtonText\': \'取消\',
              \'type\': \'warning\'
            ).then(() => 
              deleCategory(id).then(res => 
                if (res.code === 1) 
                  this.$message.success(\'删除成功!\')
                  this.handleQuery()
                 else 
                  this.$message.error(res.msg || \'操作失败\')
                
              ).catch(err => 
                this.$message.error(\'请求出错了:\' + err)
              )
            )
          ,
.........

执行deleCategory方法发送ajax请求

api\\category.js
// 删除当前列的接口
const deleCategory = (ids) => 
  return $axios(
    url: \'/category\',
    method: \'delete\',
    params:  ids 
  )

3. 代码实现

R<String>:只要返回code就可以【if (res.code === 1)】
Long ids:参数通过url地址?的形式传过来,不用RequestBody注解。

     /**
     * 根据id删除分类
     * @param ids
     * @return
     */
    @DeleteMapping
    public R<String> delete(Long ids) //只要返回code就可以,所以类型String
        log.info("删除分类,id为:", ids);

        //根据id删除
        categoryService.removeById(id);

        return R.success("分类信息删除成功");
    

4. 代码完善

需要检查 要删除的分类 是否 关联了菜品或者套餐【所以需要菜品和套餐的相关类:需要使用两个类中的categoryId属性】

要完善分类删除功能,需要先准备基础的类和接口:
1、实体类Dish和Setmeal
2、Mapper接口DishMapper和SetmealMapper
3、Service接口DishService和SetmealService
4、Service实现类DishServicelmpl和SetmealServicelmpl

Dish基础的类和接口:

entity/Dish.java
/**
 菜品
 */
@Data
public class Dish implements Serializable 
    private static final long serialVersionUID = 1L;

    private Long id;

    //菜品名称
    private String name;

    //菜品分类id
    private Long categoryId;

    //菜品价格
    private BigDecimal price;

    //商品码
    private String code;

    //图片
    private String image;

    //描述信息
    private String description;

    //0 停售 1 起售
    private Integer status;

    //顺序
    private Integer sort;

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;

mapper/DishMapper.java
@Mapper
public interface DishMapper extends BaseMapper<Dish> 

service/DishService.java
public interface DishService extends IService<Dish> 

service/impl/DishServiceImpl.java
@Service
@Slf4j
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService 

controller/DishController.java
/**
 * 菜品管理
 */
@RestController
@RequestMapping("/dish")
@Slf4j
public class DishController 
    @Autowired
    private DishService dishService;

Setmeal基础的类和接口:

entity/Setmeal.java
/**
 * 套餐
 */
@Data
public class Setmeal implements Serializable 
    private static final long serialVersionUID = 1L;

    private Long id;

    //分类id
    private Long categoryId;

    //套餐名称
    private String name;

    //套餐价格
    private BigDecimal price;

    //状态 0:停用 1:启用
    private Integer status;

    //编码
    private String code;

    //描述信息
    private String description;

    //图片
    private String image;

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;

mapper/SetmealMapper.java
@Mapper
public interface SetmealMapper extends BaseMapper<Setmeal> 

service/SetmealService.java
public interface SetmealService extends IService<Setmeal> 

service/impl/SetmealServiceImpl.java
@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService 

controller/SetmealController.java
/**
 * 套餐管理
 */
@RestController
@RequestMapping("/setmeal")
@Slf4j
public class SetmealController 
    @Autowired
    private SetmealService setmealService;
    @Autowired
    private SetmealDishService setmealDishService;

    @Autowired
    private CategoryService categoryService;

完善代码:【加入判断】

  • 在CategoryService中扩展方法
    //根据ID删除分类
    public void remove(Long ids);
  • 在CategoryServiceImpl中实现 扩展的方法

查Dish这张表[category_id]:
mysgl> select count(*) from dish where category id=?
查Setmeal这张表[category_id]:
mysgl> select count(*) from Setmeal where category id=?
查不到的话,需要报异常:自定义相关异常

    @Autowired
    private DishService dishService;
    @Autowired
    private SetmealService setmealService;

    /**
     * 根据id删除分类

以上是关于项目总结-瑞吉外卖的主要内容,如果未能解决你的问题,请参考以下文章

瑞吉外卖知识点总结

基于Springboot和MybatisPlus的外卖项目 瑞吉外卖Day4

Java项目瑞吉外卖保姆级学习笔记(改项目名称+改邮件验证码登录+功能补充)

基于Springboot和mybatis的外卖项目瑞吉外卖Day5

基于Springboot+MybatisPlus的外卖项目瑞吉外卖Day3

瑞吉外卖项目剩余功能补充