瑞吉外卖项目——瑞吉外卖

news2024/12/25 13:14:24

软件开发整体介绍

软件开发流程

  • 需求分析:产品原型、需求规格说明书

  • 设计:产品文档、UI界面设计、概要设计、详细设计、数据库设计

  • 编码:项目代码、单元测试

  • 测试:测试用例、测试报告

  • 上线运维:软件环境安装、配置

角色分工

  • 项目经理:对整个项目负责,任务分配、把控进度

  • 产品经理:进行需求调研,输出需求调研文档、产品原型等

  • UI设计师:根据产品原型输出界面效果图

  • 架构师:项目整体架构设计、技术选型等

  • 开发工程师:代码实现

  • 测试工程师:编写测试用例,输出测试报告

  • 运维工程师:软件环境搭建、项目上线

软件环境

  • 开发环境(development):开发人员在开发阶段使用的环境,一般外部用户无法访问
  • 测试环境(testing):专门给测试人员使用的环境,用于测试项目,一般外部用户无法访问
  • 生产环境(production):即线上环境,正式提供对外服务的环境

瑞吉外卖项目介绍

项目介绍

  • 本项目(瑞吉外卖)是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括系统管理后台和移动端应用两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的菜品、套餐、订单等进行管理维护。移动端应用主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单等。
  • 本项目共分为3期进行开发:
    • 第一期主要实现基本需求,其中移动端应用通过H5实现,用户可以通过手机浏览器访问。
    • 第二期主要针对移动端应用进行改进,使用微信小程序实现,用户使用起来更加方便。
    • 第三期主要针对系统进行优化升级,提高系统的访问性能。

技术选型

1

功能架构

移动端前台(H5、微信小程序)

  • 手机号登录
  • 微信登录
  • 地址管理
  • 历史订单
  • 菜品规格
  • 购物车
  • 下单
  • 菜品浏览

系统管理后台

  • 分类管理
  • 菜品管理
  • 套餐管理
  • 菜品口味管理
  • 员工登录
  • 员工退出
  • 员工管理
  • 订单管理

开发环境搭建

数据库环境搭建

  • 数据表
序号表名说明
1employee员工表
2category菜品和套餐分类表
3dish菜品表
4setmeal套餐表
5setmeal_dish套餐菜品关系表
6dish_flavor菜品口味关系表
7user用户表(C端)
8address_book地址簿表
9shopping_cart购物车表
10orders订单表
11order_detail订单明细表

Maven环境搭建

  1. 创建maven项目
  2. 导入pom文件
  3. 导入静态资源和配置文件
  4. 设置静态资源放行
package com.jihua.reggie.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

@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/");
    }
}

后台登录功能开发

  1. 创建实体类Employee,和employee表进行映射
package com.jihua.reggie.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;

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

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

    private LocalDateTime createTime;

    private LocalDateTime updateTime;

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

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

}

  1. 创建controller、service、mapper
  • controller
package com.jihua.reggie.controller;

import com.jihua.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
    @Autowired
    private EmployeeService employeeService;
}

  • service
package com.jihua.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.jihua.reggie.entity.Employee;

public interface EmployeeService extends IService<Employee> {
}

package com.jihua.reggie.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jihua.reggie.entity.Employee;
import com.jihua.reggie.mapper.EmployeeMapper;
import com.jihua.reggie.service.EmployeeService;
import org.springframework.stereotype.Service;

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

  • mapper
package com.jihua.reggie.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jihua.reggie.entity.Employee;

public interface EmployeeMapper extends BaseMapper<Employee> {
}

  1. 创建返回结果类R
  • 此类是一个通用结果类,服务端响应的所有结果最终都会包装成此种类型返回给前端页面
package com.jihua.reggie.common;

import lombok.Data;

import java.util.HashMap;
import java.util.Map;

/**
 * 通用返回结果,服务端响应的数据最终都会封装成此对象
 *
 * @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. 在Controller中创建登录方法
  • 处理逻辑如下:
    1. 将页面提交的密码password进行md5加密处理
    2. 根据页面提交的用户名username查询数据库
    3. 如果没有查询到则返回登录失败结果
    4. 密码比对,如果不一致则返回登录失败结果
    5. 查看员工状态,如果为已禁用状态,则返回员工已禁用结果
    6. 登录成功,将员工id存入Session并返回登录成功结果
Created with Raphaël 2.3.0 API请求 密码md5加密 根据用户名查询数据库 密码比对是否一致 查看员工状态是否已禁用 将员工id放入Session,返回成功结果 返回登录失败结果 yes no yes no yes no
package com.jihua.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jihua.reggie.common.R;
import com.jihua.reggie.entity.Employee;
import com.jihua.reggie.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.DigestUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
    @Autowired
    private EmployeeService employeeService;

    /**
     * 员工登录
     *
     * @param request
     * @param employee
     * @return
     */
    @PostMapping("/login")
    public R<Employee> login(HttpServletRequest request, @RequestBody 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 = employeeService.getOne(queryWrapper);
        //3. 如果没有查询到则返回登录失败结果
        if (emp == null) {
            return R.error("用户不存在");
        }
        //4. 密码比对,如果不一致则返回登录失败结果
        if (!emp.getPassword().equals(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);
    }
}

后台退出功能开发

用户点击页面中退出按钮,发送请求,请求地址为/employee/logout,请求方式为POST。我们只需要在Controller中创建对应的处理方法即可,具体的处理逻辑:

  1. 清理Session中的用户id

  2. 返回结果

/**
 * 员工退出
 	* @param request
 * @return
 */
@RequestMapping("/logout")
public R<String> logout(HttpServletRequest request) {
    //1. 清理Session中的当前登录员工id
    request.getSession().removeAttribute("employee");
    return R.success("退出成功");
}

完善登录过滤器

实现步骤:

  1. 创建自定义过滤器LoginCheckFilter
package com.jihua.reggie.filter;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 检查用户是否已经登录
 */
@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.getRequestURI());
        filterChain.doFilter(request, response);
    }
}
  1. 在启动类上加入注解@ServletComponentScan
@Slf4j//配置log
@SpringBootApplication
@ServletComponentScan//开启filter注解扫描
public class ReggieApplication {
    public static void main(String[] args) {
        SpringApplication.run(ReggieApplication.class, args);
        log.info("项目启动成功...");
    }
}
  1. 完善过滤器的处理逻辑
  • 过滤器具体的处理逻辑如下:
  1. 获取本次请求的URI
  2. 判断本次请求是否需要处理
  3. 如果不需要处理,则直接放行
  4. 判断登录状态,如果已登录,则直接放行
  5. 如果未登录则返回未登录结果
Created with Raphaël 2.3.0 API请求 判断本次请求是否需要处理 判断登录状态 放行 返回未登录结果 yes no yes no
/**
 * 检查用户是否已经登录
 */
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
    //路径匹配器,支持通配符
    public static final AntPathMatcher ANT_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 = new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**"
        };
        
        //2. 判断本次请求是否需要处理
        boolean check = check(urls, requestURI);

        //3. 如果不需要处理,则直接放行
        if (check) {
            filterChain.doFilter(request, response);
            return;
        }

        //4. 判断登录状态,如果已登录,则直接放行
        if (request.getSession().getAttribute("employee") != null) {
            filterChain.doFilter(request, response);
            return;
        }
        //5. 如果未登录则返回未登录结果,通过输出流方式向客户端页面响应数据
        response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
        log.info("拦截到请求:{}", request.getRequestURI());
    }

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

}

员工管理

新增员工

  • 新增员工,其实就是将我们新增页面录入的员工数据插入到employee表。

  • 需要注意,employee表中对username字段加入了唯一约束,因为username是员工的登录账号,必须是唯一的。

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

  1. 页面发送ajax请求,将新增员工页面中输入的数据以json的形式提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service将数据进行保存
  3. Service调用Mapper操作数据库,保存数据
/**
 * 新增员工
 *
 * @param request
 * @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("新增员工成功");
}
  • 当用户名重复时会报错,使用AOP错误捕获,进行全局错误处理
    1. 创建全局错误处理类GlobalExceptionHandler
    2. 对用户名重复错误进行前端消息返回
/**
 * 全局异常处理
 */
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
    /**
     * 异常处理方法
     *
     * @return
     */
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
        //用户名重复异常
        if (ex.getMessage().contains("Duplicate")) {
            String[] s = ex.getMessage().split(" ");
            String msg = "用户" + s[2] + "已存在";
            return R.error(msg);
        }

        log.error(ex.getMessage());
        return R.error("未知错误");
    }
}

员工信息分页查询

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

  1. 页面发送ajax请求,将分页查询参数(page、pageSize、name)提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service查询数据
  3. Service调用Mapper操作数据库,查询分页数据
  4. Controller将查询到的分页数据响应给页面
  5. 页面接收到分页数据并通过ElementUI的Table组件展示到页面上
/**
 * 分页查询(包括搜索)
 *
 * @param page     当前页面
 * @param pageSize 每页大小
 * @param name     搜索参数
 * @return 附带Page的成功消息
 */
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name) {
    log.info("page={},pageSize={},name={}", page, pageSize, name);
    //制作分页查询构造器
    Page<Employee> pageInfo = new Page<Employee>(page, pageSize);
    //制作条件查询构造器
    LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<Employee>();
    //添加过滤条件
    queryWrapper.like(StringUtils.isNotEmpty(name), Employee::getName, name);
    //添加排序条件
    queryWrapper.orderByDesc(Employee::getUpdateTime);
    //执行查询
    employeeService.page(pageInfo, queryWrapper);
    return R.success(pageInfo);
}

启用/禁用员工账号

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

  1. 页面发送ajax请求,将参数(id、status)提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service更新数据
  3. Service调用Mapper操作数据库

启用、禁用员工账号,本质上就是一个更新操作,也就是对status状态字段进行操作

在Controller中创建update方法,此方法是一个通用的修改员工信息的方法

/**
 * 根据id修改员工信息
 * @param request
 * @param employee
 * @return
 */
@PutMapping
public R<String> update(HttpServletRequest request, @RequestBody Employee employee) {
    log.info(employee.toString());
    employee.setUpdateTime(LocalDateTime.now());
    employee.setUpdateUser((Long) request.getSession().getAttribute("employee"));
    employeeService.updateById(employee);
    return R.success("员工信息修改成功");
}
  • 为避免js处理Long型id出现精度丢失,应该在后端传id数据时将Long转换为String

具体实现步骤:

  1. 提供对象转换器JacksonObjectMapper,基于Jackson进行Java对象到JSON数据的转换(同时也修改了时间的返回格式)
package com.jihua.reggie.common;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

/**
 * 对象映射器:基于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);
    }
}

  1. 在WebMvcConfig配置类中扩展Spring mvc的消息转换器,在此消息转换器中使用提供的对象转换器进行Java对象到json数据的转换
/**
 * 扩展mvc的消息转换器
 *
 * @param converters
 */
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    log.info("扩展消息转换器");
    //创建对象转换器
    MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
    //设置对象转换器,底层使用Jackson将Java对象转为json
    messageConverter.setObjectMapper(new JacksonObjectMapper());
    //将上面的消息转换器对象追加到mvc框架的转换器集合中(并设置优先级)
    converters.add(0, messageConverter);
}

编辑员工信息

在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:

  1. 点击编辑按钮时,页面跳转到add.html,并在url中携带参数[员工id]
  2. 在add.html页面获取url中的参数[员工id]
  3. 发送ajax请求,请求服务端,同时提交员工id参数
  4. 服务端接收请求,根据员工id查询员工信息,将员工信息以json形式响应给页面
  5. 页面接收服务端响应的json数据,通过VUE的数据绑定进行员工信息回显
  6. 点击保存按钮,发送ajax请求,将页面中的员工信息以json方式提交给服务端
  7. 服务端接收员工信息,并进行处理,完成后给页面响应
  8. 页面接收到服务端响应信息后进行相应处理
  • 注意: add.html页面为公共页面,新增员工和编辑员工都是在此页面操作
/**
 * 根据id查询员工信息
 *
 * @param id
 * @return
 */
@GetMapping("/{id}")
public R<Employee> getById(@PathVariable Long id) {
    log.info("根据id查询员工信息 ...");
    Employee employee = employeeService.getById(id);
    if (employee != null) {
        return R.success(employee);
    }
    return R.error("没有查询到对应员工信息");
}
  • 点击修改后会调用前面的update方法

公共字段自动填充

  • 问题分析

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

employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser((Long) request.getSession().getAttribute("employee"));

能不能对于这些公共字段在某个地方统一处理,来简化开发呢?

答案就是使用Mybatis Plus提供的公共字段自动填充功能。

  • 代码实现

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

实现步骤:

  1. 在实体类的属性上加入@TableField注解,指定自动填充的策略
@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;
  1. 按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口
package com.jihua.reggie.common;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
 * 自定义元数据对象处理器
 */
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
    /**
     * 插入操作,自动填充
     *
     * @param metaObject
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("公共字段自动填充[insert] ...");
        metaObject.setValue("createTime", LocalDateTime.now());
        metaObject.setValue("updateTime", LocalDateTime.now());
        metaObject.setValue("createUser", 1L);
        metaObject.setValue("updateUser", 1L);
        log.info(metaObject.toString());
    }

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

    }
}

注意,我们在MyMetaObjectHander类中是不能获得HttpSession对象的,所以我们需要通过其他方式来获取登录用户id。
可以使用ThreadLocal来解决此问题,它是JDK中提供的一个类。

在使用ThreadLocal之前,我们需要先确认一个事情,就是客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,在处理过程中涉及到下面类中的方法都属于相同的一个线程:

  1. LogincheckFilter的doFilter方法
  2. EmployeeController的update方法
  3. MyMetaobjectHandler的updateFill方法

可以在上面的三个方法中分别加入下面代码(获取当前线程id)

long id = Thread.currentThread().getId() ;
log.info("线程id: {0}" ,id);

执行编辑员工功能进行验证,通过观察控制台输出可以发现,一次请求对应的线程id是相同的

2

  • 什么是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封装的工具类

      package com.jihua.reggie.common;
      
      /**
       * 基于ThreadLocal封装工具类,用于保存和获取当前登录用户id
       */
      public class BaseContext {
          private static ThreadLocal<Long> threadLocal = new ThreadLocal<Long>();
      
          /**
           * 设置id
           *
           * @param id 存放当前登录用户的id
           */
          public static void setCurrentId(Long id) {
              threadLocal.set(id);
          }
      
          /**
           * 获取id
           *
           * @return 返回当前登录用户的id
           */
          public static Long getCurrentId() {
              return threadLocal.get();
          }
      }
      
      
    2. 在LoginCheckFilter的doFilter方法中调用BaseContext来设置当前登录用户的id

      //获取当前登录用户的id
      Long userId = (Long) request.getSession().getAttribute("employee");
      BaseContext.setCurrentId(userId);
      
    3. 在MyMetaobjectHandler的方法中调用BaseContext获取登录用户的id

      metaObject.setValue("updateUser", BaseContext.getCurrentId());
      

分类管理

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

  • 需求分析

可以在后台系统的分类管理页面分别添加菜品分类和套餐分类,如下:

3.1

3.2

需要注意,category表中对name字段加入了唯一约束,保证分类的名称是唯一的

  • 代码开发

在开发业务功能前,先将需要用到的类和接口基本结构创建好

  • 实体类Category

    package com.jihua.reggie.entity;
    
    import com.baomidou.mybatisplus.annotation.FieldFill;
    import com.baomidou.mybatisplus.annotation.TableField;
    import lombok.Data;
    
    import java.io.Serializable;
    import java.time.LocalDateTime;
    
    /**
     * 分类
     */
    @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;
    
    
        //是否删除
        private Integer isDeleted;
    
    }
    
  • Mapper接口CategoryMapper

    package com.jihua.reggie.mapper;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.jihua.reggie.entity.Category;
    import org.apache.ibatis.annotations.Mapper;
    
    @Mapper
    public interface CategoryMapper extends BaseMapper<Category> {
    }
    
  • 业务层接口CategoryService

    package com.jihua.reggie.service;
    
    import com.baomidou.mybatisplus.extension.service.IService;
    import com.jihua.reggie.entity.Category;
    
    public interface CategoryService extends IService<Category> {
    }
    
  • 业务层实现类CategoryServicelmpl

    package com.jihua.reggie.service.impl;
    
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.jihua.reggie.entity.Category;
    import com.jihua.reggie.mapper.CategoryMapper;
    import com.jihua.reggie.service.CategoryService;
    import org.springframework.stereotype.Service;
    
    @Service
    public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
    }
    
  • 控制层CategoryController

    package com.jihua.reggie.controller;
    
    import com.jihua.reggie.service.CategoryService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * 分类管理
     */
    @RestController
    @RequestMapping("/Category")
    public class CategoryController {
        @Autowired
        private CategoryService categoryService;
    }
    

新增分类

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

  1. 页面(backend/page/category/list.html)发送ajax请求,将新增分类窗口输入的数据以json形式提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service将数据进行保存
  3. Service调用Mapper操作数据库,保存数据

新增菜品分类和新增套餐分类请求的服务端地址和提交的json数据结构相同,使用type字段区分,所以服务端只需要提供一个方法统—处理即可

分类信息分页查询

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

  1. 页面发送ajax请求,将分页查询参数(page、pageSize)提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service查询数据
  3. Service调用Mapper操作数据库,查询分页数据
  4. Controller将查询到的分页数据响应给页面
  5. 页面接收到分页数据并通过Elementul的Table组件展示到页面上
/**
 * 分页查询
 *
 * @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. 页面发送ajax请求,将参数(id)提交到服务端
  2. 服务端Controller接收页面提交的数据并调用Service删除数据
  3. Service调用Mapper操作数据库
/**
 * 根据id删除分类
 *
 * @param ids
 * @return
 */
@DeleteMapping
public R<String> delete(Long ids) {
    log.info("删除分类,id:{}", ids);
    categoryService.removeById(ids);
    return R.success("分类信息删除成功");
}

前面我们已经实现了根据id删除分类的功能,但是并没有检查删除的分类是否关联了菜品或者套餐,所以我们需要进行功能完善。

要完善分类删除功能,需要先准备基础的类和接口:

  1. 实体类Dish和Setmeal

    package com.jihua.reggie.entity;
    
    import com.baomidou.mybatisplus.annotation.FieldFill;
    import com.baomidou.mybatisplus.annotation.TableField;
    import lombok.Data;
    
    import java.io.Serializable;
    import java.math.BigDecimal;
    import java.time.LocalDateTime;
    
    /**
     * 菜品
     */
    @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;
    
    
        //是否删除
        private Integer isDeleted;
    
    }
    
    package com.jihua.reggie.entity;
    
    import com.baomidou.mybatisplus.annotation.FieldFill;
    import com.baomidou.mybatisplus.annotation.TableField;
    import lombok.Data;
    
    import java.io.Serializable;
    import java.math.BigDecimal;
    import java.time.LocalDateTime;
    
    /**
     * 套餐
     */
    @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;
    
    
        //是否删除
        private Integer isDeleted;
    }
    
  2. Mapper接口DishMapper和SetmealMapper

    package com.jihua.reggie.mapper;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.jihua.reggie.entity.Dish;
    import org.apache.ibatis.annotations.Mapper;
    
    @Mapper
    public interface DishMapper extends BaseMapper<Dish> {
    }
    
    package com.jihua.reggie.mapper;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.jihua.reggie.entity.Setmeal;
    import org.apache.ibatis.annotations.Mapper;
    
    @Mapper
    public interface SetmealMapper extends BaseMapper<Setmeal> {
    }
    
  3. Service接口DishService和SetmealService

    package com.jihua.reggie.service;
    
    import com.baomidou.mybatisplus.extension.service.IService;
    import com.jihua.reggie.entity.Dish;
    
    public interface DishService extends IService<Dish> {
    }
    
    package com.jihua.reggie.service;
    
    import com.baomidou.mybatisplus.extension.service.IService;
    import com.jihua.reggie.entity.Setmeal;
    
    public interface SetmealService extends IService<Setmeal> {
    }
    
  4. Service实现类DishServicelmpl和SetmealServicelmpl

    package com.jihua.reggie.service.impl;
    
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.jihua.reggie.entity.Dish;
    import com.jihua.reggie.mapper.DishMapper;
    import com.jihua.reggie.service.DishService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
    
    @Service
    @Slf4j
    public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
    }
    
    package com.jihua.reggie.service.impl;
    
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.jihua.reggie.entity.Setmeal;
    import com.jihua.reggie.mapper.SetmealMapper;
    import com.jihua.reggie.service.SetmealService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
    
    @Service
    @Slf4j
    public class SetmealServiceImpl extends ServiceImpl<SetmealMapper, Setmeal> implements SetmealService {
    }
    
  • 带判断的删除方法实现
  1. 自定义业务异常类
package com.jihua.reggie.common;

/**
 * 自定义业务异常类
 */
public class CustomException extends RuntimeException {
    public CustomException(String message) {
        super(message);
    }
}
  1. 修改CategoryService和CategoryServiceImpl,自定义带判断的remove方法
package com.jihua.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.jihua.reggie.entity.Category;

public interface CategoryService extends IService<Category> {

    /**
     * 根据id删除分类,并进行删除之前的判断
     *
     * @param id
     */
    public void remove(Long id);
}
package com.jihua.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.jihua.reggie.common.CustomException;
import com.jihua.reggie.entity.Category;
import com.jihua.reggie.entity.Dish;
import com.jihua.reggie.entity.Setmeal;
import com.jihua.reggie.mapper.CategoryMapper;
import com.jihua.reggie.service.CategoryService;
import com.jihua.reggie.service.DishService;
import com.jihua.reggie.service.SetmealService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {

    @Autowired
    private DishService dishService;

    @Autowired
    private SetmealService setmealService;

    /**
     * 根据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(setmealLambdaQueryWrapper);
        if (count2 > 0) {
            //抛出业务异常
            throw new CustomException("当前分类下关联了套餐,不能删除");
        }
        //正常删除分类
        super.removeById(id);
    }
}
  1. 在全局异常处理类中添加CustomException的处理方法
/**
 * 异常处理方法
 *
 * @return
 */
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex) {
    log.error(ex.getMessage());
    return R.error(ex.getMessage());
}

修改分类

  • 需求分析

在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作

/**
 * 根据id修改分类信息
 * @param category
 * @return
 */
@PutMapping
public R<String> update(@RequestBody Category category) {
    log.info("修改分类信息:{}", category);
    categoryService.updateById(category);
    return R.success("修改分类信息成功");
}

菜品管理

文件上传下载

  • 文件上传介绍

文件上传,也称为upload,是指将本地图片、视频、音频等文件上传到服务器上,可以供其他用户浏览或下载的过程。文件上传在项目中应用非常广泛,我们经常发微博、发微信朋友圈都用到了文件上传功能。

文件上传时,对页面的form表单有如下要求:

  • method=“post” 采用post方式提交数据

  • enctype=“multipart/form-data” 采用multipart格式上传文件

  • type=“file” 使用input的file控件上传

举例:

<form method="post" action="/common/upload" enctype="multipart/form-data">
    <input name="myFile" type="file" />
	<input type="submit" value="提交"/>
</form>

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

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

  • commons-fileupload
  • commons-io

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

/**
 * 文件上传
 * @param file
 * @return
 */
@PostMapping(value = "upload")
public R<String> upload(MultipartFile file) {
    System.out.println(file);
    return R.success("文件上传成功");
}
  • 文件下载介绍

文件下载,也称为download,是指将文件从服务器传输到本地计算机的过程。

通过浏览器进行文件下载,通常有两种表现形式:

  • 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
  • 直接在浏览器中打开

通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程。

文件上传代码实现

文件上传,页面端可以使用ElementUI提供的上传组件。

<el-upload class="avatar-uploader"
           action="/common/upload"
           :show-file-list="false"
           :on-success="handleAvatarSuccess"
           :before-upload="beforeUpload"
           ref="upload">
    <img v-if="imageUrl" :src="imageUrl" class="avatar"></img>
	<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>

后端代码实现

/**
 * 文件上传
 *
 * @param file
 * @return
 */
@PostMapping(value = "/upload")
public R<String> upload(MultipartFile file) {
    //file是一个临时文件,需要转存到指定位置,否则本次请求完成后临时文件会删陈
    log.info(file.toString());
    //原始文件名
    String originalFilename = file.getOriginalFilename();//abc.jpg
    //获得文件后缀名称
    String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
    //使用UUID重新生成文件名,防止文件名称重复造成文件覆盖
    String fileName = UUID.randomUUID().toString() + suffix;//xxxxxxxx.jpg

    //创建一个目录对象
    File dir = new File(basePath);
    //判断当前目录是否存在
    if (!dir.exists()) {
        //目录不存在,需要创建
        dir.mkdirs();
    }
    //将临时文件转存到指定位置
    try {
        file.transferTo(new File(basePath + fileName));
    } catch (IOException e) {
        e.printStackTrace();
    }
    return R.success(fileName);
}

文件下载代码实现

文件下载,页面端可以使用标签展示下载的图片

<img v-if="imageUrl" : src="imageUrl" class="avatar"></img>
handleAvatarSuccess (response,file,fileList){
	this.imageUrl ='/common/download?name=${response.data}'
}
  • 后端代码实现
/**
 * 文件下载
 *
 * @param name
 * @param response
 */
@GetMapping("/download")
public void download(String name, HttpServletResponse response) {
    try {
        //输入流,通过输入流读取文件内容
        FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
        //输出流,通过输出流将文件写回浏览器,在浏览器展示图片
        ServletOutputStream outputStream = response.getOutputStream();

        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();
        }
        //关闭资源
        outputStream.close();
        fileInputStream.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

新增菜品

后台系统中可以管理菜品信息,通过新增功能来添加一个新的菜品,在添加菜品时需要选择当前菜品所属的菜品分类,并日需要卜传菜品图片,在移动端会按照菜品分类来展示对应的菜品信息。

新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。所以在新增菜品时,涉及到两个表:

  • dish 菜品表
  • dish_flavor 菜品口味表

在开发业务功能前,先将需要用到的类和接口基本结构创建好:

  • 实体类DishFlavor(Dish实体前面已经导入过了)

    package com.jihua.reggie.entity;
    
    import com.baomidou.mybatisplus.annotation.FieldFill;
    import com.baomidou.mybatisplus.annotation.TableField;
    import lombok.Data;
    
    import java.io.Serializable;
    import java.time.LocalDateTime;
    
    /**
     * 菜品口味
     */
    @Data
    public class DishFlavor implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        private Long id;
    
    
        //菜品id
        private Long dishId;
    
    
        //口味名称
        private String name;
    
    
        //口味数据list
        private String value;
    
    
        @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;
    
    
        //是否删除
        private Integer isDeleted;
    
    }
    
  • Mapper接口DishFlavorMapper

    package com.jihua.reggie.mapper;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.jihua.reggie.entity.DishFlavor;
    import org.apache.ibatis.annotations.Mapper;
    
    @Mapper
    public interface DishFlavorMapper extends BaseMapper<DishFlavor> {
    }
    
  • 业务层接口DishFlavorService

    package com.jihua.reggie.service;
    
    import com.baomidou.mybatisplus.extension.service.IService;
    import com.jihua.reggie.entity.DishFlavor;
    
    public interface DishFlavorService extends IService<DishFlavor> {
    }
    
  • 业务层实现类DishFlavorServicelmpl

    package com.jihua.reggie.service.impl;
    
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.jihua.reggie.entity.DishFlavor;
    import com.jihua.reggie.mapper.DishFlavorMapper;
    import com.jihua.reggie.service.DishFlavorService;
    import org.springframework.stereotype.Service;
    
    @Service
    public class DishFlavorServiceImpl extends ServiceImpl<DishFlavorMapper, DishFlavor> implements DishFlavorService {
    }
    
  • 控制层DishController

    package com.jihua.reggie.controller;
    
    import com.jihua.reggie.service.DishFlavorService;
    import com.jihua.reggie.service.DishService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * 菜品管理
     */
    @RestController
    @RequestMapping("/dish")
    @Slf4j
    public class DishController {
        @Autowired
        private DishService dishService;
    
        @Autowired
        private DishFlavorService dishFlavorService;
    
    }
    

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

  1. 页面(backend/page/food/add.html)发送ajax请求,请求服务端获取菜品分类数据并展示到下拉框中

    //在CategoryController中书写
    /**
     * 根据条件查询分类数据
     *
     * @param type 区分菜品分类(1)和套餐(2)分类
     * @return
     */
    @GetMapping("/list")
    public R<List<Category>> list(String type) {
        //条件构造器
        LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
        //添加条件
        queryWrapper.eq(type != null, Category::getType, type);
        //添加排序条件
        queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
    
        List<Category> list = categoryService.list(queryWrapper);
        return R.success(list);
    }
    
  2. 页面发送请求进行图片上传,请求服务端将图片保存到服务器

  3. 页面发送请求进行图片下载,将上传的图片进行回显

  4. 点击保存按钮,发送ajax请求,将菜品相关数据以json形式提交到服务端

开发新增菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。

  • DTO

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

前面的内容前端传过来的数据可以直接用实体类来接收,如:

@PutMapping
public R<String> update(@RequestBody Category category)

当前端传送的数据不能直接接受时,就要编写DTO,例如此处还需要接收flavors数组:

@Data
public class DishDto extends Dish {
    private List<DishFlavor> flavors = new ArrayList<>();
}
  • 新增菜品代码实现
/**
 * 新增菜品
 *
 * @param dishDto
 * @return
 */
@PostMapping
public R<String> save(@RequestBody DishDto dishDto) {
    log.info("新增菜品:{}", dishDto);
    dishService.saveWithFlavor(dishDto);
    return R.success("菜品新增成功");
}

此处的saveWithFlavor是自己编写的同时保存菜品,同时保存对应的口味数据选择两个数据的功能

public interface DishService extends IService<Dish> {
    /**
     * 新增菜品,同时保存对应的口味数据
     *
     * @param dishDto
     */
    public void saveWithFlavor(DishDto dishDto);
}
/**
 * 新增菜品,同时保存对应的口味数据
 *
 * @param dishDto
 */
@Override
@Transactional//多表操作,需要开启事务
public void saveWithFlavor(DishDto dishDto) {
    //保存菜品的基本信息到菜品表dish
    this.save(dishDto);
    Long dishId = dishDto.getId();//菜品id
    //菜品口味
    List<DishFlavor> flavors = dishDto.getFlavors();
    flavors.forEach(flavor -> flavor.setDishId(dishId));
    //保存菜品口味数据到菜品口味表dish_flavor
    dishFlavorService.saveBatch(flavors);
}

因为同时操作了多张表,需要进行事务处理,在启动类上开启事务注解支持注解

@Slf4j//配置log
@SpringBootApplication
@ServletComponentScan//开启filter注解扫描
@EnableTransactionManagement//开启事务注解支持
public class ReggieApplication {
    public static void main(String[] args) {
        SpringApplication.run(ReggieApplication.class, args);
        log.info("项目启动成功...");
    }
}

菜品信息分页查询

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

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

开发菜品信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可

  • 注:直接返回Page<Dish>类型的数据没有菜品分类的具体名称,需要在后端重新封装一个Page<DishDto>,将
/**
 * 菜品信息分页查询
 *
 * @param page
 * @param pageSize
 * @param name
 * @return
 */
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name) {
    //构造分页构造器对象
    Page<Dish> pageInfo = new Page<>(page, pageSize);
    Page<DishDto> pageDto = new Page<>(page, pageSize);
    //构造条件构造器
    LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
    //添加过滤条件
    queryWrapper.like(name != null, Dish::getName, name);
    //添加排序条件
    queryWrapper.orderByDesc(Dish::getUpdateTime);
    //执行分页查询
    dishService.page(pageInfo, queryWrapper);

    //对象拷贝
    BeanUtils.copyProperties(pageInfo, pageDto, "records");//忽略records,因为records需要进行处理
    List<Dish> records = pageInfo.getRecords();
    List<DishDto> list = records.stream().map((item) -> {
        DishDto dishDto = new DishDto();

        BeanUtils.copyProperties(item, dishDto);

        Long categoryId = item.getCategoryId();//分类id
        //根据id查询分类对象
        Category category = categoryService.getById(categoryId);
        if (category != null) {
            //获取分类的名称
            String categoryName = category.getName();
            dishDto.setCategoryName(categoryName);
        }
        return dishDto;
    }).collect(Collectors.toList());


    pageDto.setRecords(list);

    return R.success(pageDto);

}

修改菜品

在开发代码之前,需要梳理一下修改菜品时前端页面(add.html)和服务端的交互过程:

  1. 页面发送ajax请求,请求服务端获取分类数据,用于菜品分类下拉框中数据展示
  2. 页面发送ajax请求,请求服务端,根据id查询当前菜品信息,用于菜品信息回显
  3. 页面发送请求,请求服务端进行图片下载,用于页图片回显
  4. 点击保存按钮,页面发送ajax请求,将修改后的菜品相关数据以json形式提交到服务端

开发修改菜品功能,其实就是在服务端编写代码去处理前端页面发送的这4次请求即可。

回显

/**
 * 根据id查询菜品信息和对应的口味信息
 *
 * @param id
 * @return
 */
@GetMapping("/{id}")
public R<DishDto> getById(@PathVariable Long id) {
    DishDto dishDto = dishService.getByIdWithFlavor(id);
    return R.success(dishDto);
}

此处的getByIdWithFlavor是自己编写的方法:

/**
 * 根据id查询菜品信息和对应的口味信息
 *
 * @param id
 */
public DishDto getByIdWithFlavor(Long id);
/**
 * 根据id查询菜品信息和对应的口味信息
 *
 * @param id
 */
@Override
public DishDto getByIdWithFlavor(Long id) {
    //查询菜品基本信息,从dish表查询
    Dish dish = this.getById(id);

    //对象拷贝
    DishDto dishDto = new DishDto();
    BeanUtils.copyProperties(dish, dishDto);


    //查询当前菜品对应的口味信息,从dish_flavor表查询
    LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(DishFlavor::getDishId, dish.getId());
    List<DishFlavor> flavors = dishFlavorService.list(queryWrapper);

    dishDto.setFlavors(flavors);
    return dishDto;
}

修改

/**
 * 根据id修改菜品信息
 *
 * @param dishDto
 * @return
 */
@PutMapping
public R<String> update(@RequestBody DishDto dishDto) {
    dishService.updateWithFlavor(dishDto);
    log.info("修改菜品信息:{}", dishDto);
    return R.success("菜品信息修改成功");
}

此处的updateWithFlavor是自己编写的方法:

/**
 * 更新菜品信息,同时更新对应的口味信息
 *
 * @param dishDto
 */
public void updateWithFlavor(DishDto dishDto);
/**
 * 更新菜品信息,同时更新对应的口味信息
 *
 * @param dishDto
 */
@Override
@Transactional//事务
public void updateWithFlavor(DishDto dishDto) {
    //更新dish表基本信息
    this.updateById(dishDto);

    //先清理当前菜品对应口味数据——dish_flavor表的delete操作

    LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper();
    queryWrapper.eq(DishFlavor::getDishId, dishDto.getId());

    dishFlavorService.remove(queryWrapper);
    //再添加当前提交过来的口味数据——dish_flavor表的insert操作
    List<DishFlavor> flavors = dishDto.getFlavors();
    flavors.forEach(flavor -> flavor.setDishId(dishDto.getId()));

    dishFlavorService.saveBatch(flavors);
}

删除菜品

删除菜品时需要同时删除菜品信息、口味信息以及菜品图片

/**
 * 根据id删除菜品
 *
 * @param arrayIDs
 * @return
 */
@DeleteMapping()
public R<String> delete(@RequestParam("ids") Long[] arrayIDs) {
    List<Long> ids = Arrays.asList(arrayIDs);
    log.info("删除菜品id:{}", ids);
    dishService.deleteWithFlavor(ids);
    return R.success("菜品信息删除成功");
}

其中的deleteWithFlavor时自己写的方法

/**
 * 删除菜品信息,同时删除对应的口味信息和图片
 *
 * @param ids
 */
public void deleteWithFlavor(List<Long> ids); 
@Value("${reggie.path}")
private String basePath;

/**
 * 删除菜品信息,同时删除对应的口味信息和图片
 *
 * @param ids
 */
@Override
@Transactional//事务
public void deleteWithFlavor(List<Long> ids) {
    //先查询图片名称,否则菜品信息删除后无法查询到图片名称
    LambdaQueryWrapper<Dish> fileQueryWrapper = new LambdaQueryWrapper<>();
    fileQueryWrapper.eq(Dish::getId, ids);
    List<Dish> dishList = this.listByIds(ids);


    //根据id删除菜品信息
    this.removeByIds(ids);

    //清理当前菜品对应口味数据——dish_flavor表的delete操作
    LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.in(DishFlavor::getDishId, ids);
    dishFlavorService.remove(queryWrapper);

    //删除菜品图片
    for (Dish dish : dishList) {
        File pictureFile = new File(basePath + dish.getImage());
        boolean delete = pictureFile.delete();
        if (delete) {
            log.info("图片{}删除成功", dish.getImage());
        } else {
            log.info("图片{}删除失败", dish.getImage());
        }
    }
}

套餐管理

新增套餐

套餐就是菜品的集合。

后台系统中可以管理套餐信息,通过新增套餐功能来添加一个新的套餐,在添加套餐时需要选择当前套餐所属的套餐分类和包含的菜品,并且需要上传套餐对应的图片,在移动端会按照套餐分类来展示对应的套餐。

新增套餐,其实就是将新增页面录入的套餐信息插入到setmeal表,还需要向setmeal_dish表插入套餐和菜品关联数据。所以在新增套餐时,涉及到两个表:

  • setmeal 套餐表
  • setmeal_dish 套餐菜品关系表

在开发业务功能前,先将需要用到的类和接口基本结构创建好︰

  • 实体类SetmealDish(Setmeal实体前面已经导入过了)

    package com.jihua.reggie.entity;
    
    import com.baomidou.mybatisplus.annotation.FieldFill;
    import com.baomidou.mybatisplus.annotation.TableField;
    import lombok.Data;
    
    import java.io.Serializable;
    import java.math.BigDecimal;
    import java.time.LocalDateTime;
    
    /**
     * 套餐菜品关系
     */
    @Data
    public class SetmealDish implements Serializable {
    
        private static final long serialVersionUID = 1L;
    
        private Long id;
    
    
        //套餐id
        private Long setmealId;
    
    
        //菜品id
        private Long dishId;
    
    
        //菜品名称 (冗余字段)
        private String name;
    
        //菜品原价
        private BigDecimal price;
    
        //份数
        private Integer copies;
    
    
        //排序
        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;
    
    
        //是否删除
        private Integer isDeleted;
    }
    
  • DTO SetmealDto

    package com.jihua.reggie.dto;
    
    import com.jihua.reggie.entity.Setmeal;
    import com.jihua.reggie.entity.SetmealDish;
    import lombok.Data;
    
    import java.util.List;
    
    @Data
    public class SetmealDto extends Setmeal {
    
        private List<SetmealDish> setmealDishes;
    
        private String categoryName;
    }
    
  • Mapper接口SetmealDishMapper

    package com.jihua.reggie.mapper;
    
    import com.baomidou.mybatisplus.core.mapper.BaseMapper;
    import com.jihua.reggie.entity.SetmealDish;
    import org.apache.ibatis.annotations.Mapper;
    
    @Mapper
    public interface SetmealDishMapper extends BaseMapper<SetmealDish> {
    }
    
  • 业务层接口SetmealDishService

    package com.jihua.reggie.service;
    
    import com.baomidou.mybatisplus.extension.service.IService;
    import com.jihua.reggie.entity.SetmealDish;
    
    public interface SetmealDishService extends IService<SetmealDish> {
    }
    
  • 业务层实现类SetmealDishServicelmpl

    package com.jihua.reggie.service.impl;
    
    import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
    import com.jihua.reggie.entity.SetmealDish;
    import com.jihua.reggie.mapper.SetmealDishMapper;
    import com.jihua.reggie.service.SetmealDishService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
    
    @Service
    @Slf4j
    public class SetmealDishServiceImpl extends ServiceImpl<SetmealDishMapper, SetmealDish> implements SetmealDishService {
    
    }
    
  • 控制层SetmealController

    package com.jihua.reggie.controller;
    
    import com.jihua.reggie.service.SetmealDishService;
    import com.jihua.reggie.service.SetmealService;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    /**
     * 套餐管理
     */
    @RestController
    @RequestMapping("/setmeal")
    @Slf4j
    public class SetmealController {
        @Autowired
        private SetmealService setmealService;
        @Autowired
        private SetmealDishService setmealDishService;
    }
    

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

  1. 页面(backend/page/combo/add.html)发送ajax请求,请求服务端获取套餐分类数据并展示到下拉框中(前面已完成,和菜品分类共用)

  2. 页面发送ajax请求,请求服务端获取菜品分类数据并展示到添加菜品窗口中(菜品分类前面已完成)

  3. 页面发送ajax请求,请求服务端,根据菜品分类查询对应的菜品数据并展示到添加菜品窗口中(其实就是根据分类条件和搜索条件进行菜品的条件查询,所以写在DishController中)

    /**
     * 根据条件查询对应的菜品数据
     *
     * @param dish
     * @return
     */
    @GetMapping("list")
    public R<List<Dish>> list(Dish dish) {
        //构造查询条件
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
        //添加条件,查询状态为1(起售状态)的菜品
        queryWrapper.eq(dish.getStatus() != null, Dish::getStatus, dish.getStatus());
        //添加name搜索条件
        queryWrapper.like(dish.getName() != null, Dish::getName, dish.getName());
        //添加排序条件
        queryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
    
        List<Dish> list = dishService.list(queryWrapper);
        return R.success(list);
    }
    
  4. 页面发送请求进行图片上传,请求服务端将图片保存到服务器

  5. 页面发送请求进行图片下载,将上传的图片进行回显

  6. 点击保存按钮,发送ajax请求,将套餐相关数据以json形式提交到服务端

    @Autowired
    private SetmealDishService setmealDishService;
    
    /**
     * 新增套餐
     *
     * @param setmealDto
     * @return
     */
    @PostMapping
    public R<String> save(@RequestBody SetmealDto setmealDto) {
        setmealService.saveWithDish(setmealDto);
        return R.success("新增套餐成功");
    }
    

    此处的saveWithDish是自己写的方法

    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     *
     * @param setmealDto
     */
    public void saveWithDish(SetmealDto setmealDto);
    
    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     *
     * @param setmealDto
     */
    @Override
    @Transactional//事务
    public void saveWithDish(SetmealDto setmealDto) {
        //保存套餐的基本信息,操作setmeal表,执行insert操作
        this.save(setmealDto);
        List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();
        //处理集合
        setmealDishes.stream().map((item) -> {
            item.setSetmealId(setmealDto.getId());
            return item;
        }).collect(Collectors.toList());
        //保存套餐和菜品的关联信息,操作setmeal_dish表,执行insert操作
        setmealDishService.saveBatch(setmealDishes);
    }
    

开发新增套餐功能,其实就是在服务端编写代码去处理前端页面发送的这6次请求即可。

套餐信息分页查询

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

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

开发套餐信息分页查询功能,其实就是在服务端编写代码去处理前端页面发送的这2次请求即可。

/**
 * 分页查询套餐信息
 * @param page
 * @param pageSize
 * @param name
 * @return
 */
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name) {
    //分页构造器对象
    Page<Setmeal> pageInfo = new Page<>(page, pageSize);
    Page<SetmealDto> dtoPage = new Page<>();

    LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
    //添加查询条件,根据name进行like模糊查询
    queryWrapper.like(name != null, Setmeal::getName, name);
    //添加排序条件,根据更新时间降序排列
    queryWrapper.orderByAsc(Setmeal::getUpdateTime);
    setmealService.page(pageInfo, queryWrapper);

    //对象拷贝
    BeanUtils.copyProperties(pageInfo, dtoPage, "records");
    List<Setmeal> records = pageInfo.getRecords();
    List<SetmealDto> list = records.stream().map((item) -> {
        SetmealDto setmealDto = new SetmealDto();
        //对象拷贝
        BeanUtils.copyProperties(item, setmealDto);
        //分类id
        Long categoryId = item.getCategoryId();
        //根据分类id查询分类对象
        Category category = categoryService.getById(categoryId);
        if (category != null) {
            //分类名称
            String categoryName = category.getName();
            setmealDto.setCategoryName(categoryName);
        }
        return setmealDto;
    }).collect(Collectors.toList());
    dtoPage.setRecords(list);
    return R.success(dtoPage);
}

删除套餐

在套餐管理列表页面点击删除按钮,可以删除对应的套餐信息。也可以通过复选框选择多个套餐,点击批量删除按钮一次删除多个套餐。

注意,对于状态为售卖中的套餐不能删除,需要先停售,然后才能删除。

/**
 * 删除套餐
 *
 * @param ids
 * @return
 */
@DeleteMapping
public R<String> delete(@RequestParam List<Long> ids) {
    setmealService.deleteWithDish(ids);
    return R.success("套餐数据删除成功");
}

此处的deleteWithDish是自己写的方法

/**
 * 删除套餐,同时需要删除套餐和菜品的关联数据,以及套餐图片
 *
 * @param ids
 */
public void deleteWithDish(List<Long> ids);
@Value("${reggie.path}")
private String basePath;

/**
 * 删除套餐,同时需要删除套餐和菜品的关联数据,以及套餐图片
 *
 * @param ids
 */
@Override
@Transactional//事务
public void deleteWithDish(List<Long> ids) {
    //查询套餐状态,确定是否可用删陈
    LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.in(ids != null, Setmeal::getId, ids);
    queryWrapper.eq(Setmeal::getStatus, 1);
    int count = this.count(queryWrapper);

    //如果不能删除,抛出一个业务异常
    if (count > 0) {
        throw new CustomException("套餐正在售卖中,不能删除");
    }

    //先查询图片名称,否则套餐信息删除后无法查询到图片名称
    LambdaQueryWrapper<Setmeal> fileQueryWrapper = new LambdaQueryWrapper<>();
    fileQueryWrapper.eq(Setmeal::getId, ids);
    List<Setmeal> setmealList = this.listByIds(ids);


    //如果可以删除,先删除套餐表中的数据——setmeal
    this.removeByIds(ids);
    //删除关系表中的数据——setmeal_dish
    LambdaQueryWrapper<SetmealDish> dishQueryWrapper = new LambdaQueryWrapper<>();
    dishQueryWrapper.in(SetmealDish::getSetmealId, ids);
    setmealDishService.remove(dishQueryWrapper);

    //删除套餐图片
    for (Setmeal setmea : setmealList) {
        File pictureFile = new File(basePath + setmea.getImage());
        boolean delete = pictureFile.delete();
        if (delete) {
            log.info("图片{}删除成功", setmea.getImage());
        } else {
            log.info("图片{}删除失败", setmea.getImage());
        }
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/437581.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Qt编写推流综合应用示例(文件推流/桌面推流/本地摄像头/网络摄像头/转发推流/视频分发)

一、功能特点 1.1 文件推流 指定网卡和监听端口&#xff0c;接收网络请求推送音视频等各种文件。实时统计显示每个文件对应的访问数量、总访问数量、不同IP地址访问数量。可指定多种模式&#xff0c;0-直接播放、1-下载播放。实时打印显示各种收发请求和应答数据。每个文件对…

Vivado布线和生成bit参数设置

本文主要介绍Vivado布线参数设置&#xff0c;基本设置方式和vivado综合参数设置基本一致&#xff0c;将详细说明如何设置布线参数以优化FPGA设计的性能&#xff0c;以及如何设置Vivado压缩BIT文件。 一、Vivado布线概述 Vivado布线是在FPGA设计中实现物理布局的关键步骤之一。…

C语言-字符串

sizeof和strlen 的区别&#xff1a; 区别1&#xff1a; 1.sizeof计算整个数组大小&#xff0c; 2.strlen 计算有效的数组大小 新建字符数组”hello“ char cdata[128]"hello"; printf("sizeof--cdata的长度&#xff1a;%d\n",sizeof(cdata)); pri…

Hive窗口函数全解

在SQL中有一类函数叫做聚合函数&#xff0c;例如sum()、avg()、max()等等&#xff0c;这类函数可以将多行数据按照规则聚集为一行&#xff0c;一般来讲聚集后的行数是要少于聚集前的行数的。但是有时我们想要既显示聚集前的数据&#xff0c;又要显示聚集后的数据&#xff0c;这…

零基础抽象MYSQL

既然完成了下载安装和密码登录&#xff0c;那么打开你的MYSQL MYSQL中最简单的 创建数据库、查看数据库、选择数据库、删除数据库 先从重要的创建数据库 直接输入create database data_a ; 注意了要打分号 &#xff1b; 如果查看你创建的数据库 show databases &#xff…

使用 Flask 快速构建 基于langchain 和 chatGPT的 PDF摘要总结

简介 这里不对 langchain 和 chatGPT 进行介绍&#xff0c;仅对实现过程进行整理 环境 Python >3.8 Flask2.2.3 Jinja23.1.2 langchain0.0.143 openai0.27.4 实现 总结功能 使用 langchain 和 openai 接口实现总结功能 实现逻辑&#xff1a;通过text_splitter 将pdf 分…

LeetCode 501: 二叉搜索树中的众数 | C++语言版

LeetCode 501. 二叉搜索树中的众数 | C语言版 LeetCode 501. 二叉搜索树中的众数题目描述解题思路思路一&#xff1a;使用迭代代码实现运行结果参考文章&#xff1a; 思路二&#xff1a;减少遍历节点数代码实现运行结果参考文章&#xff1a; LeetCode 501. 二叉搜索树中的众数 …

Edius抠像过程(实践笔记)

最近的工作有点烦&#xff0c;一个月左右的时间全是在帮别人做视频的过程&#xff08;在我所在的行业里&#xff0c;就是打杂&#xff09; 因为自己不专业&#xff0c;所有的操作都是现学现用&#xff0c;前几个视频还好说&#xff0c;随便剪剪&#xff0c;就是看他们本人录的…

水务行业怎么运用智能配电

摘要&#xff1a;在构建智慧水务和“双碳”时代背景下&#xff0c;智能配电系统在水务行业中发挥日益突出的重要作用。本文首先回顾了智能配电系统在水务行业的发展历程&#xff0c;并对其应用现状进行了分析&#xff0c;进而展望了智能配电系统在水务行业的发展趋势。 关键词&…

走进小程序【五】微信小程序架构之【逻辑层】详解

文章目录 &#x1f31f;前言&#x1f31f;小程序架构&#x1f31f;逻辑层 App Service&#x1f31f;注册小程序&#x1f31f;注册页面&#x1f31f;使用 Page 构造器注册页面&#x1f31f;在页面中使用 behaviors&#x1f31f;使用 Component 构造器构造页面 &#x1f31f;页面…

信创实力认证,创邻科技荣获“2023爱分析·信创产品及服务创新奖”

近日&#xff0c;数字化市场研究咨询机构爱分析正式发布“2023爱分析信创产品及服务创新奖”评选结果。经过申报、初评、调研、终评多轮角逐&#xff0c;创邻科技凭借自研产品Galaxybase国产原生高性能图平台以及国产化替代方案成功获评“2023爱分析信创产品及服务创新奖”。 据…

KDZD606绝缘服试验装置

一、产品概述 KDZD606绝缘服试验装置是按照国家电力公司关于颁发DL/T 976-2017《带电作业用工具、装置和设备预防性试验规程》的要求的基础上研制而成&#xff0c;本产品各项指标均符合国标的要求。可以按DL/T 976-2017《带电作业用工具、装置和设备预防性试验规程》要求对绝缘…

Nginx的漏洞浮现

本文参考https://vulhub.org/#/environments/nginx/nginx_parsing_vulnerability/ 环境搭建均是采用docker 拉取环境请移步到参考。 一、Nginx的配置错误案列 1. CRLF注入漏洞 配置错误文件error1.conf rootubuntu-virtual-machine:/vulhub/vulhub-master/nginx/insecure-confi…

「解析」Pytorch 自动计算 batchsize

日志是一个十分必要的操作&#xff0c;有助于后期分析实验结果&#xff0c;特别是在多台不同环境下训练&#xff0c;为了区分&#xff0c;还是十分有必要记录相关平台信息的&#xff0c;比如 hostname&#xff0c;Python版本信息&#xff0c;Pytorch版本信息等&#xff01; im…

SpringSecurity定义多个过滤器链

在Spring Security中可以定义多个过滤器链&#xff0c;一个WebSerityConfigurerAdapter的实例就可以配置一个过滤器链&#xff0c;我们只需要配置多个WebSerityConfigurerAdapter的实例即可 可以看到&#xff0c;当请求到达 FilterChainProxy 之后&#xff0c;FilterChainProx…

什么是 CDN

CDN 是一种用来分发内容的网络拓扑结构&#xff0c;在彻底搞明白它之前&#xff0c;我们需要先来理解另外两个名词。 1、节点 用户使用CDN网络前&#xff0c;CDN提供商会在全国/全球部署多个节点。这里的节点可以看做机房&#xff0c;或者服务器集群&#xff0c;专业的称呼是…

瑞吉外卖项目——前后端分离

前后端分离开发 介绍 前后端分离开发&#xff0c;就是在项目开发过程中&#xff0c;对于前端代码的开发由专门的前端开发人员负责&#xff0c;后端代码则由后端开发人员负责&#xff0c;这样可以做到分工明确、各司其职&#xff0c;提高开发效率&#xff0c;前后端代码并行开…

Compose 学习总结

ompose发布正式版已经有一段时间了。趁最近比较闲&#xff0c;抓紧学习一波。 学习过程中&#xff0c;主要以实战项目中常用技术为目标。下面是项目地址&#xff0c;会长期更新&#xff0c;希望能给正在学习Compose的小伙伴一点参考。同时您有什么好的建议&#xff0c;也可以提…

嗖的一下!3分钟用ChatGPT生成海南旅游思维导图!

大家好&#xff0c;我是菜鸟哥&#xff01; 五一长假即将来临&#xff0c;很多小伙伴都要准备出去玩了&#xff01;旅游肯定要做攻略啊&#xff0c;比如热门的景点海南三亚&#xff0c;北京&#xff0c;上海&#xff0c;成都这些都是打卡的网红景点&#xff01;小编比较喜欢去海…

IIC协议相关

一.IIC协议初识 IIC(集成电路总线)&#xff0c;半双工同步通信方式 *特点 1.简单性和有效性 由于接口直接在组件之上&#xff0c;因此IIC总线占用的空间特别小&#xff0c;减少了电路板的空间和芯片管脚的数量&#xff0c;降低了互联成本&#xff0c;总线的长度可高达25英尺…