苍穹外卖[操作步骤+讲解]

Day01 【苍穹外卖】环境搭建和前后端联调 项目采用前后端分离的方式开发,包括前端界面(Web端管理程序、小程序客户端界面)前端vue技术开发使用nginx部署启动,后端springboot技术开发IDEA进行启动。 阅读详情

资料下载:

https://gitee.com/a-fish-v/sky-take-out

day1:

1. 初始环境导入

先创建一个主文件夹xxx,放到非中文目录下, 

从提供的资料中,找到管理端的前端工程文件夹,将这个文件夹导入到你创建的主文件夹中

双击nginx.exe启动前端工程,再将后端的初始工程文件夹导入到你创建的这个主文件夹中,

2. 运行项目

双击nginx.exe启动前端工程,在idea中打开后端的初始工程文件夹,jdk版本可以设置成17,防止出现jdk版本过低的警告。

在sky-take-out主模块下会有三个子模块

分别是sky-common(存放一些工具类) ; sky-pojo(实体类, DTO, VO) ; sky-server(配置类, Controller, Mapper, Service),  在里面找到项目的启动类SkyApplication, 点击运行,然后访问

http://localhost:80, 初始密码是123456,然后就点击登录进入到管理端

3. 创建数据库

然后在资料中找到sky.sql文件,执行sql的话,你可选择sqlyang或者DataGrip,也可以idea内置的(前提是你装了mysql),然后执行一次就可以,

一共有11张表,像employee【员工表】category【分类表】dish【菜品表】dish_flavor【菜品口味表】setmeal【套餐表】... 都是管理端的数据库表

​​​​​​

管理端登录模块

就是管理员登录一次后生成JWT令牌,返回给前端 ,然后每次登录时, 携带这个码跟后端进行校验,来判断登录成功或失败  

login方法中 生成令牌, 返回给前端

生成Jwt令牌:jwt - > 签名算法+密钥+自定义内容+有效期 

解析 - > 密钥

jwt 的 自定义内容【clamis对象, 必须是Map类型】中包含了当前登录的员工信息 

表现层(Controller)

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

        Employee employee = employeeService.login(employeeLoginDTO);

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

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

        return Result.success(employeeLoginVO);
    }

JwtUtil工具类

createJWT方法中指定了   密钥+有效期+自定义内容

parseJWT 方法中指定了 密钥 和 生成的token , 都是固定写法

public class JwtUtil {
    /**
     * 生成jwt
     * 使用Hs256算法, 私匙使用固定秘钥
     *
     * @param secretKey jwt秘钥
     * @param ttlMillis jwt过期时间(毫秒)
     * @param claims    设置的信息
     * @return
     */
    public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
        // 指定签名的时候使用的签名算法,也就是header那部分
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的时间
        long expMillis = System.currentTimeMillis() + ttlMillis;
        Date exp = new Date(expMillis);

        // 设置jwt的body
        JwtBuilder builder = Jwts.builder()
                // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                // 设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置过期时间
                .setExpiration(exp);

        return builder.compact();
    }

    /**
     * Token解密
     *
     * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
     * @param token     加密后的token
     * @return
     */
    public static Claims parseJWT(String secretKey, String token) {
        // 得到DefaultJwtParser
        Claims claims = Jwts.parser()
                // 设置签名的秘钥
                .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
                // 设置需要解析的jwt, .getBody()方法可以得到jwt中的第二个部分[自定义部分]
                .parseClaimsJws(token).getBody();
        return claims;
    }

}
@Component
//指定从配置文件中加载以sky.jwt开头的属性
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {

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

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

}

yml文件中密钥,有效期.....

sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: itcast
    # 设置jwt过期时间
    admin-ttl: 7200000
    # 设置前端传递过来的令牌名称
    admin-token-name: token
    user-secret-key: itheima
    user-ttl: 7200000
    user-token-name: authentication

表现层调用   Employee employee = employeeService.login(employeeLoginDTO);

    public Employee login(EmployeeLoginDTO employeeLoginDTO) {
        String username = employeeLoginDTO.getUsername();
        String password = employeeLoginDTO.getPassword();

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

        //2、处理各种异常情况(用户名不存在、密码不对、账号被锁定)
        if (employee == null) {
            //账号不存在, 下面所有的异常都继承自  BaseException 方法, 由全局异常处理器 
            //GlobalExceptionHandler 统一进行处理
            throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);
        }

        //密码比对
        //对前端传过来的明文密码进行md5加密处理
        password = DigestUtils.md5DigestAsHex(password.getBytes());
        if (!password.equals(employee.getPassword())) {
            //密码错误
            throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
        }
        //  StatusConstant.DISABLE = 0
        if (employee.getStatus() == StatusConstant.DISABLE) {
            //账号被锁定
            throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);
        }

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

调用Mapper 层的接口Employee employee = employeeMapper.getByUsername(username);

查询员工表

nginx反向代理

进到nginx-1.20.2\conf,打开nginx配置

# 反向代理,处理管理端发送的请求
location /api/ {
	proxy_pass   http://localhost:8080/admin/;
    #proxy_pass   http://webservers/admin/;
}

当在访问http://localhost/api/employee/login,nginx接收到请求后转到http://localhost:8080/admin/,故最终的请求地址为http://localhost:8080/admin/employee/login,和后台服务的访问地址一致。

对登录密码进行MD5加密

将MD5加密后的密码更新到数据库employee表中

password = DigestUtils.md5DigestAsHex(password.getBytes());

使用DigestUtils这个工具类

代码逻辑就是将password加密后的和数据库employee表中的进行对比

Swagger【后端【表现层】接口测试, 类似于potman】

1. 导入 knife4j 的maven坐标, 在pom.xml中添加依赖

<dependency>
   <groupId>com.github.xiaoymin</groupId>
   <artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>

2. 在配置类WebMvcConfiguration中加入 knife4j 相关配置

/**
     * 通过knife4j生成接口文档
     * @return
*/
    @Bean
    public Docket docket() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                // 指定文档的标题, 版本, 描述
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo)
                .select()
                // 可自定义:扫描包路径 
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
                // 所有路径[路径过滤]
                .paths(PathSelectors.any())
                .build();
        return docket;
    }

3. 静态资源过滤【ssm里面提到的addResourceHandlers】

在配置类WebMvcConfiguration中添加

/**
     * 设置静态资源映射
     * @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
// 添加静态资源的访问路径 /webjars/** 中的两个星号 ** 表示匹配 /webjars/ 目录下的所有文件和子目录

使用 http://localhost:8080/doc.html 访问

上面很多都是资料中提供好的

day2:

新增员工

员工分页查询

启用禁用员工账号

编辑员工

导入分类模块功能代码

1. 新增员工

根据产品原型分析/资料中的接口文档, 员工id , 账号, 姓名, 手机号, 性别, 身份证号, 将这些字段封装成一个DTO类由管理端提交到服务端

@Data
public class EmployeeDTO implements Serializable {

    private Long id;

    private String username;

    private String name;

    private String phone;

    private String sex;

    private String idNumber;

}

进入到sky-server模块中,在com.sky.controller.admin包下,在EmployeeController中创建新增员工方法,接收前端提交的参数

    @PostMapping
    @ApiOperation(value = "新增员工")
    public Result save(@RequestBody EmployeeDTO employeeDTO){
        log.info("新增员工:{}", employeeDTO);
        employeeService.save(employeeDTO);
        return Result.success();
    }

Result<T> 类 是返回给管理端的json数据,其中包含code, msg, data

Result.success(); 操作成功, 返回状态码:1成功

/**
 * 后端统一返回结果
 * @param <T>
 */
@Data
public class Result<T> implements Serializable {

    private Integer code; //编码:1成功,0和其它数字为失败
    private String msg; //错误信息
    private T data; //数据

    // 用于操作成功但不需要返回数据的场景
    public static <T> Result<T> success() {
        Result<T> result = new Result<T>();
        result.code = 1;
        return result;
    }
    // 用于操作成功且需要返回数据的场景。
    public static <T> Result<T> success(T object) {
        Result<T> result = new Result<T>();
        result.data = object;
        result.code = 1;
        return result;
    }
    // 用于操作失败的场景,设置错误信息。
    public static <T> Result<T> error(String msg) {
        Result result = new Result();
        result.msg = msg;
        result.code = 0;
        return result;
    }

}

进入到sky-server模块中,com.sky.server.EmployeeService

在service业务层定义接口, 然后在serviceImp写具体的业务代码

public interface EmployeeService {

    /**
     * 员工登录
     * @param employeeLoginDTO
     * @return
     */
    Employee login(EmployeeLoginDTO employeeLoginDTO);

    void save(EmployeeDTO employeeDTO);
}

在serviceImp 包中实现刚定义的save接口

你可以对比employee实体类和employeeDTO,完善DTO中缺失的属性,调用Mapper层接口中的方法【记得要在当前类的上面声明employeeMapper对象】,

    //新增员工
    public void save(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();
        // 将EmployeeDTO(属性少)对象转换为Employee对象(属性多)
        // 对象属性拷贝
        BeanUtils.copyProperties(employeeDTO, employee);
        // 设置账号的状态, 默认正常状态 1表示正常 0 表示禁用
        employee.setStatus(StatusConstant.ENABLE);
        // 设置密码, 默认为123456
        employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
        // 设置当前记录的创建时间和修改时间
        // employee.setCreateTime(LocalDateTime.now());
        // employee.setUpdateTime(LocalDateTime.now());
        // 设置当前记录创建人id和修改人id
        // TODO 后期需要修改为登录用户的id
        // employee.setCreateUser(BaseContext.getCurrentId());
        // employee.setUpdateUser(BaseContext.getCurrentId());
        employeeMapper.insert(employee);
    }

employeeMapper.insert(employee);

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

employee实体类中的属性和数据库字段不同的情况

在application.yml中已开启驼峰命名,故id_number和idNumber可对应

注:在初始学mybatis时, 使用的是.xml文件进行配置, 但是spring-boot中做出了改变

mybatis:
  configuration:
    #开启驼峰命名
    map-underscore-to-camel-case: true

在swagger中测试这个接口

调用员工登录接口获得一个合法的JWT令牌,【初次登录会返回给管理端的令牌】

添加令牌:

将合法的JWT令牌添加到全局参数中

文档管理-->全局参数设置-->添加参数

否则会报错

2. 员工分页查询

系统中的员工很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。而在我们的分页查询页面中, 除了分页条件以外,还有一个查询条件 "员工姓名"。

根据接口文档/原型图,请求参数有name, page【页码】pageSize 【每页记录数】

请求参数类型为Query,不是json格式提交,在路径后直接拼接。/admin/employee/page?name=zhangsan

返回数据中records数组中使用Employee实体类对属性进行封装。

@Data
public class EmployeePageQueryDTO implements Serializable {

    //员工姓名
    private String name;

    //页码
    private int page;

    //每页显示记录数
    private int pageSize;

}

后面所有的分页查询,统一都封装为PageResult对象【返回给管理端中的 data】

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {

    private long total; //总记录数

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

}

员工信息分页查询后端返回的对象类型为: Result<PageResult>

查询成功:

data: PageResult

code : 1

在sky-server模块中,com.sky.controller.admin.EmployeeController中添加分页查询方法。

    //员工分页查询
    @GetMapping("/page")
    @ApiOperation(value = "员工分页查询")
    public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
        log.info("员工分页查询:{}", employeePageQueryDTO);
        PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
        return Result.success(pageResult);
    }

在EmployeeService接口中声明pageQuery方法:

public interface EmployeeService {

    /**
     * 员工登录
     * @param employeeLoginDTO
     * @return
     */
    Employee login(EmployeeLoginDTO employeeLoginDTO);

    void save(EmployeeDTO employeeDTO);

    PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
}

在EmployeeServiceImpl中实现pageQuery方法:

注:此处使用 mybatis 的分页插件 PageHelper 来简化分页代码的开发。底层基于 mybatis 的拦截器实现。

故在pom.xml文中添加依赖(初始工程已添加)

    // 员工分页查询
    public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO){
          PageHelper.startPage(employeePageQueryDTO.getPage(),employeePageQueryDTO.getPageSize());
/*
    PageHelper:MyBatis分页插件
    startPage():设置分页参数,后续的第一个MyBatis查询会自动分页
    getPage():当前页码
    getPageSize():每页记录数

*/
        Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);
/*
    调用Mapper层的分页查询方法
    返回Page<Employee>对象(PageHelper提供的分页对象)
    这个查询会自动应用前面设置的分页参数
*/
        long total = page.getTotal();  // 总记录数
        List<Employee> result = page.getResult();  // 当前页的数据列表
        return new PageResult(total,result); // 将数据封装到自定义的PageResult对象中
    }

在 EmployeeMapper 中声明 pageQuery 方法:

    // 分页查询 -> 不再使用注解, 而采用动态的sql
    Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);

xml: 

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.EmployeeMapper">
    <select id="pageQuery" resultType="com.sky.entity.Employee">
        select * from employee
        <where>
            <if test="name!= null and name!= ''">
                and name like concat('%', #{name}, '%')
            </if>
        </where>
        order by create_time desc
    </select>
</mapper>
// spring-boot, xml文件中的几个常见属性 namespace: 和你mapper层的对应接口进行绑定
// id:指定接口中对象的方法名 resultType : 你查询的数据的类型

时间格式问题:可以在每个时间属性上添加@JsonFormat指定日期格式

也可以 

在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理

就是配置类WebMvcConfiguration 继承 WebMvcConfigurationSupport类,然后重写WebMvcConfigurationSupport类中的extendMessageConverters方法, 方法形参都是固定的

	/**
     * 扩展Spring MVC框架的消息转化器
     * @param converters
     */
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器...");
        //创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
        // 用自定义的JacksonObjectMapper替换默认的ObjectMapper
        converter.setObjectMapper(new JacksonObjectMapper());
        //将自己的消息转化器加入容器中
        // add(0, converter):插入到列表的第一个位置(最高优先级)
        //Spring会按顺序使用第一个能够处理当前类型的转换器
        converters.add(0,converter);
    }

时间格式定义,sky-common模块中

package com.sky.json;

public class JacksonObjectMapper extends ObjectMapper {

	//.......
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
    //.......

    }
}

3. 启用禁用员工账号

在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。如果某个员工账号状态为正常,则按钮显示为 "禁用",如果员工账号状态为已禁用,则按钮显示为"启用"。

1). 路径参数携带状态值。

2). 同时,把id传递过去,明确对哪个用户进行操作。

3). 返回数据code状态是必须,其它是非必须。

在url中进行拼接status和员工id,分别填充到EmployeeController的形参中

在sky-server模块中,根据接口设计中的请求参数形式对应的在 EmployeeController 中创建启用禁用员工账号的方法:

    @PostMapping("/status/{status}")
    @ApiOperation(value = "启用禁用员工账号")
    public Result startOrStop(@PathVariable Integer status, Long id){
        log.info("启用禁用员工账号:{},{}",status,id);
        employeeService.startOrStop(status, id);
        return Result.success();
    }

在 EmployeeService 接口中声明启用禁用员工账号的业务方法:

public interface EmployeeService {

    /**
     * 员工登录
     * @param employeeLoginDTO
     * @return
     */
    Employee login(EmployeeLoginDTO employeeLoginDTO);

    void save(EmployeeDTO employeeDTO);

    PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);

    void startOrStop(Integer status, Long id);
}

在 EmployeeServiceImpl 中实现启用禁用员工账号的业务方法:

    //启用禁用员工账号
    public void startOrStop(Integer status, Long id) {
        // 还是类似于写一个sql : update employee set status = ? where id = ?
        // 扩展这个方法
        Employee employee = Employee.builder().status(status).id(id).build();
        employeeMapper.update(employee);
    }

在 EmployeeMapper 接口中声明 update 方法:

    @AutoFill(value = OperationType.UPDATE)
    void update(Employee employee);

在 EmployeeMapper.xml 中编写SQL:

<update id="update" parameterType="Employee">
        update employee
        <set>
            <if test="name != null">name = #{name},</if>
            <if test="username != null">username = #{username},</if>
            <if test="password != null">password = #{password},</if>
            <if test="phone != null">phone = #{phone},</if>
            <if test="sex != null">sex = #{sex},</if>
            <if test="idNumber != null">id_Number = #{idNumber},</if>
            <if test="updateTime != null">update_Time = #{updateTime},</if>
            <if test="updateUser != null">update_User = #{updateUser},</if>
            <if test="status != null">status = #{status},</if>
        </set>
        where id = #{id}
    </update>

4. 编辑员工

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

设计两个接口:

1. 根据id就进行回显员工信息 ----> 路径参数,请求方式对应rest风格中的GET

2. 对回显的员工信息进行修改    ----> 提交json数据到后台 ,请求方式对应rest风格中的PUT

1. 根据id就进行回显员工信息

在 EmployeeController 中创建 getById 方法:

    @GetMapping("/{id}")
    @ApiOperation(value = "查询员工信息")
    public Result<Employee> getById(@PathVariable Long id){
        Employee employee = employeeService.getById(id);
        return Result.success(employee);
    }

在 EmployeeService 接口中声明 getById 方法:

    Employee getById(Long id);

在 EmployeeServiceImpl 中实现 getById 方法:

    //根据id查询员工信息

    @Override
    public Employee getById(Long id) {
        Employee employee = employeeMapper.getById(id);
        employee.setPassword("******");
        return employee;
    }

在 EmployeeMapper 接口中声明 getById 方法:

    // 根据id查询员工信息
    @Select("select * from employee where id = #{id}")
    Employee getById(Long id);

2. 对回显的员工信息进行修改

在 EmployeeController 中创建 update 方法:

    //更新/修改员工信息
    @PutMapping
    @ApiOperation(value = "更新员工信息")
    public Result update(@RequestBody EmployeeDTO employeeDTO){
        log.info("更新员工信息:{}", employeeDTO);
        employeeService.update(employeeDTO);
        return Result.success();
    }

在 EmployeeService 接口中声明 update 方法:

public interface EmployeeService {

    /**
     * 员工登录
     * @param employeeLoginDTO
     * @return
     */
    Employee login(EmployeeLoginDTO employeeLoginDTO);

    void save(EmployeeDTO employeeDTO);

    PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);

    void startOrStop(Integer status, Long id);

    Employee getById(Long id);

    void update(EmployeeDTO employeeDTO);
}

在 EmployeeServiceImpl 中实现 update 方法:

    // 编辑员工信息
    @Override
    public void update(EmployeeDTO employeeDTO) {
       // 实体属性不等 -> 对象的属性拷贝
       Employee employee = new Employee();
       BeanUtils.copyProperties(employeeDTO, employee);


       // 时间和id
        // employee.setUpdateTime(LocalDateTime.now());
        //employee.setUpdateUser(BaseContext.getCurrentId());

        employeeMapper.update(employee);
    }

在实现启用禁用员工账号功能时,已实现employeeMapper.update(employee),在此不需写Mapper层代码,你业务代码写的再多,最后也肯定是落脚到操作数据库中对应的表。

接口测试和前后端联调

5. 导入分类模块功能代码

后台系统中可以管理分类信息,分类包括两种类型,分别是 菜品分类 和 套餐分类 。

先来分析菜品分类相关功能。

新增菜品分类:当我们在后台系统中添加菜品时需要选择一个菜品分类,在移动端也会按照菜品分类来展示对应的菜品。

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

根据id删除菜品分类:在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。

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

启用禁用菜品分类:在分类管理列表页面,可以对某个分类进行启用或者禁用操作。

分类类型查询:当点击分类类型下拉框时,从数据库中查询所有的菜品分类数据进行展示。

和上面员工的接口类似,可能多修改菜品/套餐这个接口

这个模块资料里有可以导入的现成代码

接口文档中提供了下面这6个接口

新增分类

分类分页查询

根据id删除分类

修改分类

启用禁用分类

根据类型查询分类

1.  新增分类

菜品分类和套餐分类共用一个接口, 新增操作携带的参数有name, 排序所需数字, type: 区分共用接口,提供的category表【数据库表】字段比你前端提交过来的DTO字段多,应该还是要用工具类进行拷贝

就是使用Rest风格对表现层进行开发的时候,删除和修改都应该在路径参数中指定id,但是下面代码中写法直接就 @DeleteMapping @PutMapping 接口文档里面把id定义成了query, 这样也可以

2. 分类分页查询

请求方式为GET, query参数为page【页码】,pageSize【每页记录数】前面写过【新增员工】,使用PageHelper插件,具体参数看资料中的接口文档

3. 根据id删除分类

请求方式为DELETE, query参数为id, 返回的VO还是Result封装的

4. 修改分类

请求方式为PUT, query参数为id, name 排序数字,type区分分类类型

5.启用禁用分类

还是路径参数中携带status: 1【启用】,0【禁用】,query参数为id

6.  根据类型查询分类

请求方式为GET, 查询所有相同类型的数据

可按照mapper-->service-->controller依次导入,这样代码不会显示相应的报错。

进入到sky-server模块

DishMapper.java

package com.sky.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface DishMapper {

    /**
     * 根据分类id查询菜品数量
     * @param categoryId
     * @return
     */
    @Select("select count(id) from dish where category_id = #{categoryId}")
    Integer countByCategoryId(Long categoryId);

}

SetmealMapper.java

package com.sky.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface SetmealMapper {

    /**
     * 根据分类id查询套餐的数量
     * @param id
     * @return
     */
    @Select("select count(id) from setmeal where category_id = #{categoryId}")
    Integer countByCategoryId(Long id);

}

CategoryMapper.java

package com.sky.mapper;

import com.github.pagehelper.Page;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface CategoryMapper {

    /**
     * 插入数据
     * @param category
     */
    @Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
            " VALUES" +
            " (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
    void insert(Category category);

    /**
     * 分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    Page<Category> pageQuery(CategoryPageQueryDTO categoryPageQueryDTO);

    /**
     * 根据id删除分类
     * @param id
     */
    @Delete("delete from category where id = #{id}")
    void deleteById(Long id);

    /**
     * 根据id修改分类
     * @param category
     */
    void update(Category category);

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    List<Category> list(Integer type);
}

CategoryMapper.xml,进入到resources/mapper目录下

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.CategoryMapper">

    <select id="pageQuery" resultType="com.sky.entity.Category">
        select * from category
        <where>
            <if test="name != null and name != ''">
                and name like concat('%',#{name},'%')
            </if>
            <if test="type != null">
                and type = #{type}
            </if>
        </where>
        order by sort asc , create_time desc
    </select>

    <update id="update" parameterType="Category">
        update category
        <set>
            <if test="type != null">
                type = #{type},
            </if>
            <if test="name != null">
                name = #{name},
            </if>
            <if test="sort != null">
                sort = #{sort},
            </if>
            <if test="status != null">
                status = #{status},
            </if>
            <if test="updateTime != null">
                update_time = #{updateTime},
            </if>
            <if test="updateUser != null">
                update_user = #{updateUser}
            </if>
        </set>
        where id = #{id}
    </update>

    <select id="list" resultType="Category">
        select * from category
        where status = 1
        <if test="type != null">
            and type = #{type}
        </if>
        order by sort asc,create_time desc
    </select>
</mapper>

CategoryService.java

package com.sky.service;

import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.result.PageResult;
import java.util.List;

public interface CategoryService {

    /**
     * 新增分类
     * @param categoryDTO
     */
    void save(CategoryDTO categoryDTO);

    /**
     * 分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO);

    /**
     * 根据id删除分类
     * @param id
     */
    void deleteById(Long id);

    /**
     * 修改分类
     * @param categoryDTO
     */
    void update(CategoryDTO categoryDTO);

    /**
     * 启用、禁用分类
     * @param status
     * @param id
     */
    void startOrStop(Integer status, Long id);

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    List<Category> list(Integer type);
}

EmployeeServiceImpl.java

package com.sky.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.PasswordConstant;
import com.sky.constant.StatusConstant;
import com.sky.context.BaseContext;
import com.sky.dto.EmployeeDTO;
import com.sky.dto.EmployeeLoginDTO;
import com.sky.dto.EmployeePageQueryDTO;
import com.sky.entity.Employee;
import com.sky.exception.AccountLockedException;
import com.sky.exception.AccountNotFoundException;
import com.sky.exception.PasswordErrorException;
import com.sky.mapper.EmployeeMapper;
import com.sky.result.PageResult;
import com.sky.service.EmployeeService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;

import java.time.LocalDateTime;
import java.util.List;

@Service
public class EmployeeServiceImpl implements EmployeeService {

    @Autowired
    private EmployeeMapper employeeMapper;

    /**
     * 员工登录
     *
     * @param employeeLoginDTO
     * @return
     */
    public Employee login(EmployeeLoginDTO employeeLoginDTO) {
        String username = employeeLoginDTO.getUsername();
        String password = employeeLoginDTO.getPassword();

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

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

        //密码比对
        // TODO 后期需要进行md5加密,然后再进行比对
        password = DigestUtils.md5DigestAsHex(password.getBytes());
        if (!password.equals(employee.getPassword())) {
            //密码错误
            throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
        }

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

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

    /**
     * 新增员工
     *
     * @param employeeDTO
     */
    public void save(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();

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

        //设置账号的状态,默认正常状态 1表示正常 0表示锁定
        employee.setStatus(StatusConstant.ENABLE);

        //设置密码,默认密码123456
        employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));

        //设置当前记录的创建时间和修改时间
        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());

        //设置当前记录创建人id和修改人id
        employee.setCreateUser(BaseContext.getCurrentId());//目前写个假数据,后期修改
        employee.setUpdateUser(BaseContext.getCurrentId());

        employeeMapper.insert(employee);
    }

    /**
     * 分页查询
     *
     * @param employeePageQueryDTO
     * @return
     */
    public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
        // select * from employee limit 0,10
        //开始分页查询
        PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());

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

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

        return new PageResult(total, records);
    }

    /**
     * 启用禁用员工账号
     *
     * @param status
     * @param id
     */
    public void startOrStop(Integer status, Long id) {
        Employee employee = Employee.builder()
                .status(status)
                .id(id)
                .build();

        employeeMapper.update(employee);
    }

    /**
     * 根据id查询员工
     *
     * @param id
     * @return
     */
    public Employee getById(Long id) {
        Employee employee = employeeMapper.getById(id);
        employee.setPassword("****");
        return employee;
    }

    /**
     * 编辑员工信息
     *
     * @param employeeDTO
     */
    public void update(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();
        BeanUtils.copyProperties(employeeDTO, employee);

        employee.setUpdateTime(LocalDateTime.now());
        employee.setUpdateUser(BaseContext.getCurrentId());

        employeeMapper.update(employee);
    }

}

CategoryController.java 

package com.sky.controller.admin;

import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.CategoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

/**
 * 分类管理
 */
@RestController
@RequestMapping("/admin/category")
@Api(tags = "分类相关接口")
@Slf4j
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

    /**
     * 新增分类
     * @param categoryDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增分类")
    public Result<String> save(@RequestBody CategoryDTO categoryDTO){
        log.info("新增分类:{}", categoryDTO);
        categoryService.save(categoryDTO);
        return Result.success();
    }

    /**
     * 分类分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("分类分页查询")
    public Result<PageResult> page(CategoryPageQueryDTO categoryPageQueryDTO){
        log.info("分页查询:{}", categoryPageQueryDTO);
        PageResult pageResult = categoryService.pageQuery(categoryPageQueryDTO);
        return Result.success(pageResult);
    }

    /**
     * 删除分类
     * @param id
     * @return
     */
    @DeleteMapping
    @ApiOperation("删除分类")
    public Result<String> deleteById(Long id){
        log.info("删除分类:{}", id);
        categoryService.deleteById(id);
        return Result.success();
    }

    /**
     * 修改分类
     * @param categoryDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改分类")
    public Result<String> update(@RequestBody CategoryDTO categoryDTO){
        categoryService.update(categoryDTO);
        return Result.success();
    }

    /**
     * 启用、禁用分类
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("启用禁用分类")
    public Result<String> startOrStop(@PathVariable("status") Integer status, Long id){
        categoryService.startOrStop(status,id);
        return Result.success();
    }

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据类型查询分类")
    public Result<List<Category>> list(Integer type){
        List<Category> list = categoryService.list(type);
        return Result.success(list);
    }
}

前后端联调测试一下就行,功能都能用即可

day3:

完成菜品管理模块中的  菜品起售停售 功能,要求:

1. 根据产品原型进行需求分析,分析出业务规则
2. 设计 菜品起售停售 功能的接口
3. 根据接口设计进行代码实现
4. 分别通过swagger接口文档和前后端联调进行功能测试

这部分视频里面应该是没有的

根据菜品名称,菜品分类,售卖状态进行分页查询,批量删除功能,新增菜品功能,起售停售功能....

1.  新增菜品

前端提交过来的json数据封装成一个DishDTO类

@Data
public class DishDTO implements Serializable {

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

}

对比dish类

/**
 * 菜品
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
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 image;

    //描述信息
    private String description;

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

    private LocalDateTime createTime;

    private LocalDateTime updateTime;

    private Long createUser;

    private Long updateUser;

}

CategoryId为菜品的分类id, 区分某个dish是属于哪个菜品类型的 dish_1 ,dish_2, dish_3 类型的,每次新增菜品的时候,都要清除缓存中的数据,强制去数据库查询数据,这样缓存中始终保持新的数据了,如果不这样做,就会查到旧的dish_1集合,因为当我们进行进行查找操作的时候,我们会先从缓存里面找dish_1, 如果没有才会去查询数据库,然后再将查到的数据存储到缓存中。删除的时候是根据key进行删除的,【相同类型的菜品,有相同的CategoryId】

注:dish_分类id

    @Autowired
    private RedisTemplate redisTemplate;
    @PostMapping
    @ApiOperation("新增菜品")
    public Result save(@RequestBody DishDTO dishDTO) {
        log.info("新增菜品:{}", dishDTO);
        dishService.saveWithFlavor(dishDTO);
        //清理缓存数据
        String key = "dish_" + dishDTO.getCategoryId();
        clearCache(key);
        return Result.success();
    }
    /**
     * 清理缓存数据
     * @param pattern
     */
    private void clearCache(String pattern) {
        Set keys = redisTemplate.keys(pattern);
        // 支持删除集合
        redisTemplate.delete(keys);
    }

创建新增菜品的接口,观察原型图中,一个菜品会有多种口味,List<DishFlavor> flavors,一对多

public interface DishService{
    // 新增菜品和对应的口味
    public void saveWithFlavor(DishDTO dishDTO);
}

还是创建个空的dish对象, 属性拷贝 dishDTO -> dish 中

    @Transactional // 开启事务注解
    public void saveWithFlavor(DishDTO dishDTO) {
        Dish dish = new Dish();
        // 属性拷贝
        BeanUtils.copyProperties(dishDTO, dish);
        // 向菜品中查询一条数据
        dishMapper.insert(dish);
        Long dishId = dish.getId();
        // 向口味表重插入n条数据
        List<DishFlavor> flavors = dishDTO.getFlavors();
        if(flavors!=null && flavors.size()>0){
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishId);
            });
            dishFlavorMapper.insertBatch(flavors);
        }
    }

dishMapper

@AutoFill 自定义注解,用于实现公共字段的自动填充功能,  在insertDish 方法执行时,此时dish对象已经包含了完整的公共字段值,AOP编程那个before函数【ssm那个笔记详细写了】

涉及到的相关类:
1. 切面类

sky-server/src/main/java/com/sky/aspect/AutoFillAspect.java

2. AutoFillConstant常量类/BaseContext 线程局部变量工具类

sky-common/src/main/java/com/sky/context/BaseContext.java

sky-common/src/main/java/com/sky/constant/AutoFillConstant.java

    @AutoFill(value = OperationType.INSERT)
    void insert(Dish dish);

xml: 

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper">
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into dish (name,category,price,image,description,create_time,update_time,create_user,update_user,status)
        values (#{name},#{category},#{price},#{image},#{description},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})
    </insert>
</mapper>

dishFlavorMapper,菜品口味集合

@Mapper
public interface DishFlavorMapper {
    void insertBatch(List<DishFlavor> flavors);
}

xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishFlavorMapper">
    <insert id="insertBatch">
        insert into dish_flavor (dish_id,name,value) VALUES
        <foreach collection="flavors" item="df" separator=",">
            (#{df.dishId},#{df.name},#{df.value})
        </foreach>
    </insert>
</mapper>

2.  菜品分页查询

根据菜品名称,菜品分类,售卖状态进行分页查询

@Data
public class DishPageQueryDTO implements Serializable {

    private int page;

    private int pageSize;

    private String name;

    //分类id
    private Integer categoryId;

    //状态 0表示禁用 1表示启用
    private Integer status;

}

调用业务层代码

    @GetMapping("/page")
    @ApiOperation("菜品分页查询")
    public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
        log.info("菜品分页查询:{}",dishPageQueryDTO);
        PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
        return Result.success(pageResult);
    }

业务层定义抽象接口

public interface DishService{
    // 新增菜品和对应的口味
    public void saveWithFlavor(DishDTO dishDTO);

    PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
}

业务层实现类,还是使用PageHelper插件,将数量和菜品集合返回给管理端

    public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO){
        PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
        Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
        return new PageResult(page.getTotal(), page.getResult());
    }

mapper层接口

    // 菜品分页查询
    Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);

xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.DishMapper">
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into dish (name,category,price,image,description,create_time,update_time,create_user,update_user,status)
        values (#{name},#{category},#{price},#{image},#{description},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})
    </insert>
    <select id="pageQuery" resultType="com.sky.vo.DishVO">
        select d.*,c.name as categoryName
        from dish d left join category c on d.category_id = c.id
        <where>
            <if test="name!=null">
                and d.name like concat('%',#{name},'%')
            </if>
            <if test="categoryId!=null">
                and d.categoryId like concat('%',#{categoryId},'%')
            </if>
            <if test="status!=null">
                and d.status like concat('%',#{status},'%')
            </if>
        </where>
        order by d.create_time desc
    </select>
</mapper>

3.菜品批量删除

根据id进行批量删除

    @DeleteMapping
    @ApiOperation("菜品批量删除")
    public Result delete(@RequestParam List<Long> ids) {
        log.info("菜品批量删除:{}", ids);
        dishService.deleteBatch(ids);
        //将所有的菜品缓存数据清理掉,所有以dish_开头的key
        clearCache("dish_*");
        return Result.success();
    }

业务层定义接口

public interface DishService{
    // 新增菜品和对应的口味
    public void saveWithFlavor(DishDTO dishDTO);

    PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);

    void deleteBatch(List<Long> ids);
}

业务层实现类

1.遍历每个要删除的菜品ID

2.查询菜品详细信息

3.如果菜品状态是"启用/在售"状态,抛出异常

4.防止删除正在销售的菜品

5.查询这些菜品ID是否被任何套餐关联

6.如果有关联的套餐ID,抛出异常

7.防止删除已被套餐使用的菜品(保证数据完整性)

8. 删除菜品表和其对应的口味表中的数据

关联有StatusConstant和setmealDishMapper两个常量类

业务场景模拟
假设要删除菜品ID为 [1, 2, 3]:

// 第一步:状态检查
菜品1:状态=停售 → 通过
菜品2:状态=在售 → 抛出异常"菜品在售,不能删除"
菜品3:状态=停售 → 通过

// 第二步:关联检查
查询菜品[1, 3]是否被套餐关联
结果:菜品1被"豪华套餐"关联 → 抛出异常"菜品已被套餐关联,不能删除"

// 第三步:删除操作(如果前两步都通过)
删除菜品3的基本信息
删除菜品3的所有口味数据

    // 保证事务操作的一致性
    @Transactional
    //菜品批量删除
    public void deleteBatch(List<Long> ids){
        // 根据status判断是否可以删除
        for (Long id : ids) {
           Dish dish =  dishMapper.getById(id);
           if(dish.getStatus() == StatusConstant.ENABLE){
               throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
           }
        }

        // 套餐被关联的菜品不能删除
        List<Long> setmealIdsByDishIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
        if(setmealIdsByDishIds != null && !setmealIdsByDishIds.isEmpty()){
            throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
        }

        // 删除菜品
        for (Long id : ids) {
            Dish byId = dishMapper.deleteById(id);

            // 删除菜品相关的口味数据
            dishFlavorMapper.deleteByDishId(id);
        }
    }

根据id进行删除数据

    @Delete("delete * from dish where id = #{id}")
    Dish deleteById(Long id);
    @Delete("delete from dish_flavor where dish_id = #{dishId}")
    void deleteByDishId(Long dishId);

day4: 

完成套餐管理模块所有业务功能,包括:

- 新增套餐
- 套餐分页查询
- 删除套餐
- 修改套餐
- 起售停售套餐

1.  新增套餐

业务规则:

- 套餐名称唯一
- 套餐必须属于某个分类
- 套餐必须包含菜品
- 名称、分类、价格、图片为必填项
- 添加菜品窗口需要根据分类类型来展示菜品
- 新增的套餐默认为停售状态

接口设计(共涉及到4个接口):

- 根据类型查询分类(已完成)
- 根据分类id查询菜品
- 图片上传(已完成)
- 新增套餐

跟菜品管理基本类似,特殊的是在新建套餐的时候,要关联多个菜品,查询时,也可以根据分类进行查询相同类型的套餐,新增套餐中,提交的DTO要包含【套餐名称,套餐分类【id】
,套餐价格:,套餐菜品:,添加菜品,套餐图片:,套餐描述】等字段,可以直接看资料中的接口文档:

setmeal表为套餐表,用于存储套餐的信息,

 setmeal_dish表为套餐菜品关系表,用于存储套餐【id】和菜品【id】的关联关系, 

构造套餐和菜品多对多关系:

1.一个套餐可以包含多个菜品

2.一个菜品可以被多个套餐使用

/**
 * 套餐管理
 */
@RestController
@RequestMapping("/admin/setmeal")
@Api(tags = "套餐相关接口")
@Slf4j
public class SetmealController {

    @Autowired
    private SetmealService setmealService;

    /**
     * 新增套餐
     * @param setmealDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增套餐")
    public Result save(@RequestBody SetmealDTO setmealDTO) {
        setmealService.saveWithDish(setmealDTO);
        return Result.success();
    }
}

调用业务层【接口】代码  setmealService.saveWithDish(setmealDTO); 

public interface SetmealService {

    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     * @param setmealDTO
     */
    void saveWithDish(SetmealDTO setmealDTO);
}

调用业务层【实现类】

遍历套餐中的所有菜品,为每个菜品设置它们所属的套餐ID,建立套餐与菜品之间的关联关系。就是为中间表【有两个外键的表】先添加套餐id, 资料里面的那个有点问题,setmealDTO 中包含套餐id,但是setmealDishes集合里面是不包含的, 执行完代码后setmealDishes集合中的每个菜品都同时包含了套餐id和菜品id

/**
 * 套餐业务实现
 */
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {

    @Autowired
    private SetmealMapper setmealMapper;
    @Autowired
    private SetmealDishMapper setmealDishMapper;
    @Autowired
    private DishMapper dishMapper;

    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     * @param setmealDTO
     */
    @Transactional
    public void saveWithDish(SetmealDTO setmealDTO) {
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO, setmeal);

        //向套餐表插入数据
        setmealMapper.insert(setmeal);

        //获取生成的套餐id
        Long setmealId = setmeal.getId();

        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
        setmealDishes.forEach(setmealDish -> {
            setmealDish.setSetmealId(setmealId);
        });

        //保存套餐和菜品的关联关系
        setmealDishMapper.insertBatch(setmealDishes);
    }
}

SetmealMapper层, 往套餐表中插入数据

/**
     * 新增套餐
     * @param setmeal
*/
@AutoFill(OperationType.INSERT)
void insert(Setmeal setmeal);

.xml

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

SetmealDishMapper【往套餐/菜品关联表中插入数据】

/**
     * 批量保存套餐和菜品的关联关系
     * @param setmealDishes
*/
void insertBatch(List<SetmealDish> setmealDishes);

.xml

<insert id="insertBatch" parameterType="list">
    insert into setmeal_dish
    (setmeal_id,dish_id,name,price,copies)
    values
    <foreach collection="setmealDishes" item="sd" separator=",">
        (#{sd.setmealId},#{sd.dishId},#{sd.name},#{sd.price},#{sd.copies})
    </foreach>
</insert>

2. 套餐分页查询

- 根据页码进行分页展示
- 每页展示10条数据
- 可以根据需要,按照套餐名称、分类、售卖状态进行查询

返回给管理端的VO-data封装了查询出的套餐集合

@Data
public class SetmealPageQueryDTO implements Serializable {

    private int page;

    private int pageSize;

    private String name;

    //分类id
    private Integer categoryId;

    //状态 0表示禁用 1表示启用
    private Integer status;

}

  SetmealServiceImpl:

public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
    int pageNum = setmealPageQueryDTO.getPage();
    int pageSize = setmealPageQueryDTO.getPageSize();
    PageHelper.startPage(pageNum,pageSize);
    Page<SetmealVO> page = stemealMapper.pageQuery(setmealPageQueryDTO);
    return new PageResult(page.getTotal,page.getResult);
    // 总记录数 + 当前页集合
}

 SetmealMapper:

public interface SetmealMapper{
    Page<SetmealVO> pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);
}

SetmealVO:

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

    private Long id;

    //分类id
    private Long categoryId;

    //套餐名称
    private String name;

    //套餐价格
    private BigDecimal price;

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

    //描述信息
    private String description;

    //图片
    private String image;

    //更新时间
    private LocalDateTime updateTime;

    //分类名称
    private String categoryName;

    //套餐和菜品的关联关系
    private List<SetmealDish> setmealDishes = new ArrayList<>();
}

.xml  [查询套餐信息,并关联查询出对应的分类名称]

<mapper>
    <select id="pageQuery" resultType="com.sky.vo.SetmealVO">
    select
    	s.*,c.name categoryName
    from
    	setmeal s
    left join
    	category c
    on
    	s.category_id = c.id
    <where>
        <if test="name != null">
            and s.name like concat('%',#{name},'%')
        </if>
        <if test="status != null">
            and s.status = #{status}
        </if>
        <if test="categoryId != null">
            and s.category_id = #{categoryId}
        </if>
    </where>
    order by s.create_time desc    // 按创建时间降序排列(最新的在前)
    </select>
</mapper>

3. 删除套餐

可以一次删除一个套餐,也可以批量删除套餐

起售中的套餐不能删除

参数类型query, 根据id进行删除

SetmealController

把url中的请求参数变为list集合中的数据,添加@RequestParam注解

/**
     * 批量删除套餐
     * @param ids
     * @return
*/
@DeleteMapping
@ApiOperation("批量删除套餐")
public Result delete(@RequestParam List<Long> ids){
    setmealService.deleteBatch(ids);
    return Result.success();
}

SetmealService

/**
     * 批量删除套餐
     * @param ids
*/
void deleteBatch(List<Long> ids);

SetmealServiceImpl

/**
     * 批量删除套餐
     * @param ids
*/
@Transactional
public void deleteBatch(List<Long> ids) {
    ids.forEach(id -> {
        Setmeal setmeal = setmealMapper.getById(id);
        if(StatusConstant.ENABLE == setmeal.getStatus()){
            //起售中的套餐不能删除
            throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
        }
    });

    ids.forEach(setmealId -> {
        //删除套餐表中的数据
        setmealMapper.deleteById(setmealId);
        //删除套餐菜品关系表中的数据
        setmealDishMapper.deleteBySetmealId(setmealId);
    });
}

SetmealMapper

/**
     * 根据id查询套餐
     * @param id
     * @return
*/
@Select("select * from setmeal where id = #{id}")
Setmeal getById(Long id);

/**
     * 根据id删除套餐
     * @param setmealId
*/
@Delete("delete from setmeal where id = #{id}")
void deleteById(Long setmealId);

SetmealDishMapper 【两个外键:分别是套餐id 和 菜品id】

/**
     * 根据套餐id删除套餐和菜品的关联关系
     * @param setmealId
*/
@Delete("delete from setmeal_dish where setmeal_id = #{setmealId}")
void deleteBySetmealId(Long setmealId);

4. 修改套餐

- 根据id查询套餐
- 根据类型查询分类
- 根据分类id查询菜品

进行数据的回显  【SetmealVO】

然后修改完数据, PUT一下,返回给管理端code【修改成功】

SetmealController

/**
     * 根据id查询套餐,用于修改页面回显数据
     *
     * @param id
     * @return
*/
@GetMapping("/{id}")
@ApiOperation("根据id查询套餐")
public Result<SetmealVO> getById(@PathVariable Long id) {
    SetmealVO setmealVO = setmealService.getByIdWithDish(id);
    return Result.success(setmealVO);
}

/**
     * 修改套餐
     *
     * @param setmealDTO
     * @return
*/
@PutMapping
@ApiOperation("修改套餐")
public Result update(@RequestBody SetmealDTO setmealDTO) {
    setmealService.update(setmealDTO);
    return Result.success();
}

SetmealService

/**
     * 根据id查询套餐和关联的菜品数据
     * @param id
     * @return
*/
SetmealVO getByIdWithDish(Long id);

/**
     * 修改套餐
     * @param setmealDTO
*/
void update(SetmealDTO setmealDTO);

SetmealServiceImpl

/**
     * 根据id查询套餐和套餐菜品关系
     *
     * @param id
     * @return
*/
public SetmealVO getByIdWithDish(Long id) {
    Setmeal setmeal = setmealMapper.getById(id);
    List<SetmealDish> setmealDishes = setmealDishMapper.getBySetmealId(id);

    SetmealVO setmealVO = new SetmealVO();
    BeanUtils.copyProperties(setmeal, setmealVO);
    setmealVO.setSetmealDishes(setmealDishes);
    
    return setmealVO;
}

/**
     * 修改套餐
     *
     * @param setmealDTO
*/
@Transactional
public void update(SetmealDTO setmealDTO) {
    Setmeal setmeal = new Setmeal();
    BeanUtils.copyProperties(setmealDTO, setmeal);

    //1、修改套餐表,执行update
    setmealMapper.update(setmeal);

    //套餐id
    Long setmealId = setmealDTO.getId();

    //2、删除套餐和菜品的关联关系,操作setmeal_dish表,执行delete
    setmealDishMapper.deleteBySetmealId(setmealId);

    List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
    setmealDishes.forEach(setmealDish -> {
        setmealDish.setSetmealId(setmealId);
    });
    //3、重新插入套餐和菜品的关联关系,操作setmeal_dish表,执行insert
    setmealDishMapper.insertBatch(setmealDishes);
}

SetmealDishMapper

	/**
     * 根据套餐id查询套餐和菜品的关联关系
     * @param setmealId
     * @return
     */
    @Select("select * from setmeal_dish where setmeal_id = #{setmealId}")
    List<SetmealDish> getBySetmealId(Long setmealId);

5. 起售停售套餐

- 可以对状态为起售的套餐进行停售操作,可以对状态为停售的套餐进行起售操作
- 起售的套餐可以展示在用户端,停售的套餐不能展示在用户端
- 起售套餐时,如果套餐内包含停售的菜品,则不能起售

路径参数为status, query参数为id, 返回管理端 code【1:操作成功】

SetmealController

/**
     * 套餐起售停售
     * @param status
     * @param id
     * @return
*/
@PostMapping("/status/{status}")
@ApiOperation("套餐起售停售")
public Result startOrStop(@PathVariable Integer status, Long id) {
    setmealService.startOrStop(status, id);
    return Result.success();
}

SetmealService

/**
     * 套餐起售、停售
     * @param status
     * @param id
*/
void startOrStop(Integer status, Long id);

SetmealServiceImpl

/**
     * 套餐起售、停售
     * @param status
     * @param id
*/
public void startOrStop(Integer status, Long id) {
    //起售套餐时,判断套餐内是否有停售菜品,有停售菜品提示"套餐内包含未启售菜品,无法启售"
    if(status == StatusConstant.ENABLE){
        //select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = ?
        List<Dish> dishList = dishMapper.getBySetmealId(id);
        if(dishList != null && dishList.size() > 0){
            dishList.forEach(dish -> {
                if(StatusConstant.DISABLE == dish.getStatus()){
                    throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);
                }
            });
        }
    }

    Setmeal setmeal = Setmeal.builder()
        .id(id)
        .status(status)
        .build();
    setmealMapper.update(setmeal);
}

DishMapper

/**
     * 根据套餐id查询菜品
     * @param setmealId
     * @return
*/
@Select("select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = #{setmealId}")
List<Dish> getBySetmealId(Long setmealId);

day 6 

小程序端配置和代码导入

1. 小程序端配置

- 注册小程序

https://mp.weixin.qq.com/

- 完善小程序信息

登录成功后,在开发管理中的开发设置中设置AppID【当前注册小程序的唯一标识】和密钥

- 下载开发者工具

https://developers.weixin.qq.com/miniprogram/dev/devtools/stable.html

进入到开发者工具后, 依次点击:详情 -> 本地设置 -> 勾选【不校验合法域名】

2.代码导入 :

在资料中找到mp-weixin这个文件夹,按目录进行导入即可,同时指定你自己的AppID

导入成功后,在common文件夹中找到wendor.js , ctrl + F , 查找搜localhost, 修改请求后端服务的 baseUrl , localhost:8080, 一般是没啥问题的

1. 微信登录

服务端使用HttpClint发送httlp向微信接口服务, 发送请求

具体流程:

https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html

你可以在小程序端重新编译一下【相当于文档中调用的wx.login(), 获取code】, 然后在控制台复制code, 在postman 中 测试 https://api.weixin.qq.com/sns/jscode2session ,就会【json格式】返回openid

具体流程就是小程序端发送请求【携带code】到服务端, 然后服务端使用HttpClint 调用微信的接口服务

管理小程序端注册的用户数据
CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',
  `openid` varchar(45) COLLATE utf8_bin DEFAULT NULL COMMENT '微信用户唯一标识', 
  `name` varchar(32) COLLATE utf8_bin DEFAULT NULL COMMENT '姓名',
  `phone` varchar(11) COLLATE utf8_bin DEFAULT NULL COMMENT '手机号',
  `sex` varchar(2) COLLATE utf8_bin DEFAULT NULL COMMENT '性别',
  `id_number` varchar(18) COLLATE utf8_bin DEFAULT NULL COMMENT '身份证号',
  `avatar` varchar(500) COLLATE utf8_bin DEFAULT NULL COMMENT '头像',
  `create_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='用户信息';
说句题外话, 如果是企业注册的小程序, 当你点击登录后, 就能拿到你的手机号....

配置微信登录所需配置项

1. application-dev.yml

sky:
  wechat:
    appid: wxffb3637a228223b8
    secret: 84311df9199ecacdf4f12d27b6b9522d

2. application.yml

sky:
  wechat:
    appid: ${sky.wechat.appid}
    secret: ${sky.wechat.secret}

配置为微信用户生成jwt令牌时使用的配置项

application.yml

sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: itcast
    # 设置jwt过期时间
    admin-ttl: 7200000
    # 设置前端传递过来的令牌名称
    admin-token-name: token
    user-secret-key: itheima
    user-ttl: 7200000
    user-token-name: authentication

请求参数:以json方式进行提交code【微信授权码】

DTO: 

@Data
public class UserLoginDTO implements Serializable {

    private String code;

}

VO: 

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

    private Long id;
    private String openid;
    private String token;

}

根据接口定义创建UserController的login方法:

使用建造者模式创建VO对象,填充用户id, 微信openId, JWT token, 

private JwtProperties jwtProperties; 

//指定从配置文件中加载以sky.jwt开头的属性
@ConfigurationProperties(prefix = "sky.jwt") ,类似于之前ssm中的import, 配置类的【互相】导入

@RestController = @Controller 和 @ResponseBody 的组合注解
@RequestMapping("/user/user")
@Api(tags = "C端用户相关接口")
@Slf4j    // 记录日志
public class UserController {

    @Autowired
    private UserService userService;
    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 微信登录
     * @param userLoginDTO
     * @return
     */
    @PostMapping("/login")
    @ApiOperation("微信登录")
    public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO){
        log.info("微信用户登录:{}",userLoginDTO.getCode());

        //微信登录
        User user = userService.wxLogin(userLoginDTO);//后绪步骤实现

        //为微信用户生成jwt令牌
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.USER_ID,user.getId());
        String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims);

        UserLoginVO userLoginVO = UserLoginVO.builder()
                .id(user.getId())
                .openid(user.getOpenid())
                .token(token)
                .build();
        return Result.success(userLoginVO);
    }
}

创建UserService接口:

查询用户的Id和微信的openId

public interface UserService {

    /**
     * 微信登录
     * @param userLoginDTO
     * @return
     */
    User wxLogin(UserLoginDTO userLoginDTO);
}

创建UserServiceImpl实现类:

实现获取微信用户的openid和微信登录功能,用户的Id可以直接从数据库查,但是openid就要调用微信的接口了

@Service
@Slf4j
public class UserServiceImpl implements UserService {

    //微信服务接口地址
    public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";

    @Autowired
    private WeChatProperties weChatProperties;
    @Autowired
    private UserMapper userMapper;

    /**
     * 微信登录
     * @param userLoginDTO
     * @return
     */
    public User wxLogin(UserLoginDTO userLoginDTO) {
        String openid = getOpenid(userLoginDTO.getCode());

        //判断openid是否为空,如果为空表示登录失败,抛出业务异常
        
        if(openid == null){
            throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
            // 括号里面就是一个常量类, 找一下就行
        }

        //判断当前用户是否为新用户
        // // User 表中没有 当前用户,所以拿不到 对应的 openId, 根据 openid进行查询用户
        User user = userMapper.getByOpenid(openid);

        //如果是新用户,自动完成注册
        if(user == null){
            user = User.builder()
                    .openid(openid)
                    .createTime(LocalDateTime.now())
                    .build();
            userMapper.insert(user);//后绪步骤实现
        }

        //返回这个用户对象
        return user;
    }

    /**
     * 调用微信接口服务,获取微信用户的openid
     * @param code
     * @return
     */
    private String getOpenid(String code){
        //调用微信接口服务,获得当前微信用户的openid
        Map<String, String> map = new HashMap<>();
        // .yml 中的appid...等值, 被封装到了一个配置属性类weChatProperties中
        map.put("appid",weChatProperties.getAppid());
        map.put("secret",weChatProperties.getSecret());
        map.put("js_code",code);
        map.put("grant_type","authorization_code");
        // 使用HttpClient 向微信接口服务发送请求
        String json = HttpClientUtil.doGet(WX_LOGIN, map);

        JSONObject jsonObject = JSON.parseObject(json);
        String openid = jsonObject.getString("openid");   // 指定 key 进行获取
        return openid;
    }
}

创建UserMapper接口:

@Mapper
public interface UserMapper {

    /**
     * 根据openid查询用户
     * @param openid
     * @return
     */
    @Select("select * from user where openid = #{openid}")
    User getByOpenid(String openid);

    /**
     * 插入数据
     * @param user
     */
    void insert(User user);
}

创建UserMapper.xml映射文件:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.UserMapper">

    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into user (openid, name, phone, sex, id_number, avatar, create_time)
        values (#{openid}, #{name}, #{phone}, #{sex}, #{idNumber}, #{avatar}, #{createTime})
    </insert>

</mapper>
/*
useGeneratedKeys="true" keyProperty="id" 
这两个参数就可以将我们的主键值返回
useGeneratedKeys="true"    <!-- 使用数据库自增主键 , 按顺序递增 -->
keyProperty="id">          <!-- 将主键值设置到参数的id属性, 给插入数据自动添加userId【以递增的形式】 -->
*/

编写拦截器JwtTokenUserInterceptor:

统一拦截用户端发送的请求并进行jwt校验

/**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getUserTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
            Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
            log.info("当前用户的id:", userId);
            BaseContext.setCurrentId(userId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}

在WebMvcConfiguration配置类中注册拦截器:

@Autowired
    private JwtTokenUserInterceptor jwtTokenUserInterceptor;
	/**
     * 注册自定义拦截器
     * @param registry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        //.........

        registry.addInterceptor(jwtTokenUserInterceptor)
                .addPathPatterns("/user/**")
                .excludePathPatterns("/user/user/login")
                .excludePathPatterns("/user/shop/status");
    }

用户登录成功后跳转到系统首页,在首页需要根据分类来展示菜品和套餐。如果菜品设置了口味信息,需要展示选择规格按钮,否则显示 + 按钮。

对应接口

- 查询分类
- 根据分类id查询菜品
- 根据分类id查询套餐
- 根据套餐id查询包含的菜品

导入资料中的商品浏览功能代码即可

可按照mapper-->service-->controller依次导入,这样代码不会显示相应的报错。

进入到sky-server模块中

在SetmealMapper.java中添加list和getDishItemBySetmealId两个方法

	/**
     * 动态条件查询套餐
     * @param setmeal
     * @return
     */
    List<Setmeal> list(Setmeal setmeal);

    /**
     * 根据套餐id查询菜品选项
     * @param setmealId
     * @return
     */
    @Select("select sd.name, sd.copies, d.image, d.description " +
            "from setmeal_dish sd left join dish d on sd.dish_id = d.id " +
            "where sd.setmeal_id = #{setmealId}")
    List<DishItemVO> getDishItemBySetmealId(Long setmealId);

创建SetmealMapper.xml文件

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.SetmealMapper">

    <select id="list" parameterType="Setmeal" resultType="Setmeal">
        select * from setmeal
        <where>
            <if test="name != null">
                and name like concat('%',#{name},'%')
            </if>
            <if test="categoryId != null">
                and category_id = #{categoryId}
            </if>
            <if test="status != null">
                and status = #{status}
            </if>
        </where>
    </select>
</mapper>

创建SetmealService.java

package com.sky.service;

import com.sky.dto.SetmealDTO;
import com.sky.dto.SetmealPageQueryDTO;
import com.sky.entity.Setmeal;
import com.sky.result.PageResult;
import com.sky.vo.DishItemVO;
import com.sky.vo.SetmealVO;
import java.util.List;

public interface SetmealService {

    /**
     * 条件查询
     * @param setmeal
     * @return
     */
    List<Setmeal> list(Setmeal setmeal);

    /**
     * 根据id查询菜品选项
     * @param id
     * @return
     */
    List<DishItemVO> getDishItemById(Long id);

}

创建SetmealServiceImpl.java

package com.sky.service.impl;

import com.sky.entity.Setmeal;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealDishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.service.SetmealService;
import com.sky.vo.DishItemVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 套餐业务实现
 */
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {

    @Autowired
    private SetmealMapper setmealMapper;
    @Autowired
    private SetmealDishMapper setmealDishMapper;
    @Autowired
    private DishMapper dishMapper;

    /**
     * 条件查询
     * @param setmeal
     * @return
     */
    public List<Setmeal> list(Setmeal setmeal) {
        List<Setmeal> list = setmealMapper.list(setmeal);
        return list;
    }

    /**
     * 根据id查询菜品选项
     * @param id
     * @return
     */
    public List<DishItemVO> getDishItemById(Long id) {
        return setmealMapper.getDishItemBySetmealId(id);
    }
}

在DishService.java中添加listWithFlavor方法定义

	/**
     * 条件查询菜品和口味
     * @param dish
     * @return
     */
    List<DishVO> listWithFlavor(Dish dish);

在DishServiceImpl.java中实现listWithFlavor方法

	/**
     * 条件查询菜品和口味
     * @param dish
     * @return
     */
    public List<DishVO> listWithFlavor(Dish dish) {
        List<Dish> dishList = dishMapper.list(dish);

        List<DishVO> dishVOList = new ArrayList<>();

        for (Dish d : dishList) {
            DishVO dishVO = new DishVO();
            BeanUtils.copyProperties(d,dishVO);

            //根据菜品id查询对应的口味
            List<DishFlavor> flavors = dishFlavorMapper.getByDishId(d.getId());

            dishVO.setFlavors(flavors);
            dishVOList.add(dishVO);
        }

        return dishVOList;
    }

创建DishController.java

package com.sky.controller.user;

import com.sky.constant.StatusConstant;
import com.sky.entity.Dish;
import com.sky.result.Result;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController("userDishController")
@RequestMapping("/user/dish")
@Slf4j
@Api(tags = "C端-菜品浏览接口")
public class DishController {
    @Autowired
    private DishService dishService;

    /**
     * 根据分类id查询菜品
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询菜品")
    public Result<List<DishVO>> list(Long categoryId) {
        Dish dish = new Dish();
        dish.setCategoryId(categoryId);
        dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品

        List<DishVO> list = dishService.listWithFlavor(dish);

        return Result.success(list);
    }

}

创建CategoryController.java

package com.sky.controller.user;

import com.sky.entity.Category;
import com.sky.result.Result;
import com.sky.service.CategoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController("userCategoryController")
@RequestMapping("/user/category")
@Api(tags = "C端-分类接口")
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

    /**
     * 查询分类
     * @param type
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("查询分类")
    public Result<List<Category>> list(Integer type) {
        List<Category> list = categoryService.list(type);
        return Result.success(list);
    }
}

创建SetmealController.java

package com.sky.controller.user;

import com.sky.constant.StatusConstant;
import com.sky.entity.Setmeal;
import com.sky.result.Result;
import com.sky.service.SetmealService;
import com.sky.vo.DishItemVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController("userSetmealController")
@RequestMapping("/user/setmeal")
@Api(tags = "C端-套餐浏览接口")
public class SetmealController {
    @Autowired
    private SetmealService setmealService;

    /**
     * 条件查询
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询套餐")
    public Result<List<Setmeal>> list(Long categoryId) {
        Setmeal setmeal = new Setmeal();
        setmeal.setCategoryId(categoryId);
        setmeal.setStatus(StatusConstant.ENABLE);

        List<Setmeal> list = setmealService.list(setmeal);
        return Result.success(list);
    }

    /**
     * 根据套餐id查询包含的菜品列表
     *
     * @param id
     * @return
     */
    @GetMapping("/dish/{id}")
    @ApiOperation("根据套餐id查询包含的菜品列表")
    public Result<List<DishItemVO>> dishList(@PathVariable("id") Long id) {
        List<DishItemVO> list = setmealService.getDishItemById(id);
        return Result.success(list);
    }
}

day 7:

- 缓存菜品
- 缓存套餐
- 添加购物车
- 查看购物车
- 清空购物车

1. 缓存菜品 缓存套餐

修改用户端接口 DishController 的 list 方法,加入缓存处理逻辑 

@Configuration  // 标识这是一个配置类,Spring 会读取其中的Bean定义
@Slf4j         // 自动生成日志对象 log,用于输出日志信息
public class RedisConfiguration {
    
    @Bean  // 声明这是一个Spring Bean,方法返回的对象会被纳入Spring容器管理
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
        log.info("开始创建redis模版对象...");  // 记录日志,便于调试
        
        RedisTemplate redisTemplate = new RedisTemplate();
        
        // 设置redis的连接工厂对象
        // RedisConnectionFactory 由Spring Boot自动配置,包含Redis服务器连接信息
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        
        // 设置redis key的序列化方式为字符串序列化
        // 这样存储在Redis中的key都会是易读的字符串格式
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        
        return redisTemplate;  // 返回配置好的RedisTemplate实例
    }
}

@Autowired
private RedisTemplate redisTemplate;

// 存储数据
redisTemplate.opsForValue().set("user:1001", "张三");

// 读取数据
String userName = (String) redisTemplate.opsForValue().get("user:1001");

// 在Redis中实际存储的key是:"user:1001"(字符串格式)
/*
@RestController("userDishController")的括号内字符串‌用于显式指定该控制器的Bean名称
当未指定名称时,Spring默认将类名的首字母小写作为Bean名称(如UserDishController变为userDishController)。通过括号显式命名会覆盖此默认规则
在需要手动注入或通过@Qualifier指定Bean时,该名称可作为唯一标识。例如:
@Autowired  
@Qualifier("userDishController")  
private UserDishController controller;
*/

@RestController("userDishController")
@RequestMapping("/user/dish")
@Slf4j
@Api(tags = "C端-菜品浏览接口")
public class DishController {
    @Autowired
    private DishService dishService;
    @Autowired
    //1. 在pom.xml中导入依赖 2. yml中指定数据源 3.RedisConfiguation配置类
    private RedisTemplate redisTemplate;
    /**
     * 根据分类id查询菜品
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询菜品")
    public Result<List<DishVO>> list(Long categoryId) {
        // 构造redis中的key, 规则:dish_分类id
        String key = "dish_" + categoryId;    //key: 菜品分类 value: 菜品集合

        // 查询redis中是否存在菜品数据
        List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);    // 存储的 List<DishVO> 类型
        if (list != null && list.size() > 0) {
            //如果存在,直接返回,无需查询数据库
            return Result.success(list);
        }
        Dish dish = new Dish();
        dish.setCategoryId(categoryId);
        dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
    
        // 查询到的菜品数据集合
        list = dishService.listWithFlavor(dish);
        redisTemplate.opsForValue().set(key, list);
        return Result.success(list);
    }

}
// 注:最终在redis中【list集合被序列化】是以String类型存储的

- 新增菜品【新增菜品后小程序端某个分类下就多了一个菜品】
- 修改菜品
- 批量删除菜品
- 起售、停售菜品

当管理端对菜品进行了上述操作后, 由于没有及时的清理redis缓存中的数据,会出现小程序端没有发生修改的情况

抽取清理缓存的方法 【在管理端DishController中添加 】

	@Autowired
    private RedisTemplate redisTemplate;
    // 操作redis数据库, 就要新建redisTemplate对象
	/**
     * 清理缓存数据
     * @param pattern
     */
    private void cleanCache(String pattern){
        Set keys = redisTemplate.keys(pattern);
        redisTemplate.delete(keys);
    }
修改管理端的DishController

1). 新增菜品优化

	/**
     * 新增菜品
     *
     * @param dishDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增菜品")
    public Result save(@RequestBody DishDTO dishDTO) {
        log.info("新增菜品:{}", dishDTO);
        dishService.saveWithFlavor(dishDTO);

        //清理缓存数据
        String key = "dish_" + dishDTO.getCategoryId();
        cleanCache(key);
        return Result.success();
    }

2). 菜品批量删除优化

	/**
     * 菜品批量删除
     *
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("菜品批量删除")
    public Result delete(@RequestParam List<Long> ids) {
        log.info("菜品批量删除:{}", ids);
        dishService.deleteBatch(ids);

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

        return Result.success();
    }

3). 修改菜品优化

	/**
     * 修改菜品
     *
     * @param dishDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改菜品")
    public Result update(@RequestBody DishDTO dishDTO) {
        log.info("修改菜品:{}", dishDTO);
        dishService.updateWithFlavor(dishDTO);

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

        return Result.success();
    }

4). 菜品起售停售优化

	/**
     * 菜品起售停售
     *
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("菜品起售停售")
    public Result<String> startOrStop(@PathVariable Integer status, Long id) {
        dishService.startOrStop(status, id);

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

        return Result.success();
    }

Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。

Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:

- EHCache
- Caffeine
- Redis(常用)

Spring Cache 框架,注解都是通用的,如何切换缓存实现呢?

在pom.xml中更换不同的maven坐标

<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>  		            		       	 <version>2.7.3</version> 
</dependency>

Spring Cache 常用注解:

@EnableCaching : 开启缓存注解功能,通常加在启动类上

@Cacheable:  在方法执行前先查询缓存 中否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法将方法返回值放到缓存中

@CachePut: 将方法的返回值放到缓存中

@Cacheable 和 @CachePut 的区别在于:@CachePut 只能放,@Cacheable既能拿也能放

@CacheEvict : 将一条或多条数据从缓存中删除

新增用户操作举例:

private UserMapper userMapper; 
@POstMapping
@CachePut(cacheNames = "userCache", key = "#user.id")
// 如果使用Spring Cache 缓存数据,key 的生成:userCache::
// cacheNames 指定缓存名称 
// 注:注解中的key 参数 #后 .前 的这部分 是下面方法中的形参 user
// 这样每一个存到缓存【redis】的用户 就有了不同的key
public User save(@RequestBody User user){
    userMapper.insert(user);
    // 先插入到数据库, 再去操作缓存数据
    // 就是数据库里面先有数据,接着redis里面才会有数据
    return user;
}

查询用户【根据id】操作举例:

private UserMapper userMapper; 
@GetMapping
@Cacheable(cacheNames = "userCache", key = "#id")
// 如果缓存中有用户数据,就不会调用下面的方法
// 如果没有,就执行下面的查询方法,然后再将查询的数据,放入到缓存redis中
public User getById(Long id){
    User user = userMapper.getById(id);
    return user;
}

导入Spring Cache和Redis相关maven坐标(已实现) 

<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>

在启动类上加入@EnableCaching注解,开启缓存注解功能

package com.sky;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
@EnableCaching
public class SkyApplication {
    public static void main(String[] args) {
        SpringApplication.run(SkyApplication.class, args);
        log.info("server started");
    }
}

在用户端接口SetmealController的 list 方法上加入@Cacheable注解

	/**
     * 条件查询
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询套餐")
    @Cacheable(cacheNames = "setmealCache",key = "#categoryId") //key: setmealCache::100
    public Result<List<Setmeal>> list(Long categoryId) {
        Setmeal setmeal = new Setmeal();
        setmeal.setCategoryId(categoryId);
        setmeal.setStatus(StatusConstant.ENABLE);

        List<Setmeal> list = setmealService.list(setmeal);
        return Result.success(list);
    }

在管理端接口SetmealController的 save、delete、update、startOrStop等方法上加入CacheEvict注解

	/**
     * 新增套餐
     *
     * @param setmealDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增套餐")
    @CacheEvict(cacheNames = "setmealCache",key = "#setmealDTO.categoryId")//key: setmealCache::100
    public Result save(@RequestBody SetmealDTO setmealDTO) {
        setmealService.saveWithDish(setmealDTO);
        return Result.success();
    }
	/**
     * 批量删除套餐
     *
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("批量删除套餐")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result delete(@RequestParam List<Long> ids) {
        setmealService.deleteBatch(ids);
        return Result.success();
    }
	/**
     * 修改套餐
     *
     * @param setmealDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改套餐")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result update(@RequestBody SetmealDTO setmealDTO) {
        setmealService.update(setmealDTO);
        return Result.success();
    }

    /**
     * 套餐起售停售
     *
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("套餐起售停售")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result startOrStop(@PathVariable Integer status, Long id) {
        setmealService.startOrStop(status, id);
        return Result.success();
    }

添加购物车

用户可以将菜品或者套餐添加到购物车。对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车;对于套餐来说,可以直接点击

+ 将当前套餐加入购物车。在购物车中可以修改菜品和套餐的数量,也可以清空购物车。请求参数:套餐id /菜品id, 相关口味数据

购物车表的设计:

1. 选了什么商品 2. 每个商品都买了几个 3. 不同用户的购物车需要分开......

根据添加购物车接口的参数设计DTO:

在sky-pojo模块,ShoppingCartDTO.java已定义

package com.sky.dto;

import lombok.Data;
import java.io.Serializable;

@Data
public class ShoppingCartDTO implements Serializable {

    private Long dishId;
    private Long setmealId;
    private String dishFlavor;

}

根据添加购物车接口创建ShoppingCartController

package com.sky.controller.user;


import com.sky.dto.ShoppingCartDTO;
import com.sky.result.Result;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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;

/**
 * 购物车
 */
@RestController
@RequestMapping("/user/shoppingCart")
@Slf4j
@Api(tags = "C端-购物车接口")
public class ShoppingCartController {

    @Autowired
    private ShoppingCartService shoppingCartService;

    /**
     * 添加购物车
     * @param shoppingCartDTO
     * @return
     */
    @PostMapping("/add")
    @ApiOperation("添加购物车")
    public Result<String> add(@RequestBody ShoppingCartDTO shoppingCartDTO){
        log.info("添加购物车:{}", shoppingCartDTO);
        shoppingCartService.addShoppingCart(shoppingCartDTO);//后绪步骤实现
        return Result.success();
    }
}

创建ShoppingCartService接口:

package com.sky.service;

import com.sky.dto.ShoppingCartDTO;
import com.sky.entity.ShoppingCart;
import java.util.List;

public interface ShoppingCartService {

    /**
     * 添加购物车
     * @param shoppingCartDTO
     */
    void addShoppingCart(ShoppingCartDTO shoppingCartDTO);
}

创建ShoppingCartServiceImpl实现类,并实现add方法:

先判断新增的商品, 在购物车里面是否有相同的 , 如果有, 增加数量即可(update), 如果没有,新增商品(insert) 

ShoppingCart ,user_id: 用于区分当前购物车是属于哪个用户的

package com.sky.service.impl;


import com.sky.context.BaseContext;
import com.sky.dto.ShoppingCartDTO;
import com.sky.entity.Dish;
import com.sky.entity.Setmeal;
import com.sky.entity.ShoppingCart;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.service.ShoppingCartService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;

@Service
public class ShoppingCartServiceImpl implements ShoppingCartService {

    @Autowired
    private ShoppingCartMapper shoppingCartMapper;
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private SetmealMapper setmealMapper;
    
    /**
     * 添加购物车
     *
     * @param shoppingCartDTO
     */
    public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
        ShoppingCart shoppingCart = new ShoppingCart();
        BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
        //只能查询自己的购物车数据
        shoppingCart.setUserId(BaseContext.getCurrentId());

        //判断当前商品是否在购物车中
        List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);

        if (shoppingCartList != null && shoppingCartList.size() == 1) {
            //如果已经存在,就更新数量,数量加1
            shoppingCart = shoppingCartList.get(0);
            shoppingCart.setNumber(shoppingCart.getNumber() + 1);
            shoppingCartMapper.updateNumberById(shoppingCart);
        } else {
            //如果不存在,插入数据,数量就是1

            //判断当前添加到购物车的是菜品还是套餐
            Long dishId = shoppingCartDTO.getDishId();
            if (dishId != null) {
                //添加到购物车的是菜品
                Dish dish = dishMapper.getById(dishId);
                shoppingCart.setName(dish.getName());
                shoppingCart.setImage(dish.getImage());
                shoppingCart.setAmount(dish.getPrice());
            } else {
                //添加到购物车的是套餐
                Setmeal setmeal = setmealMapper.getById(shoppingCartDTO.getSetmealId());
                shoppingCart.setName(setmeal.getName());
                shoppingCart.setImage(setmeal.getImage());
                shoppingCart.setAmount(setmeal.getPrice());
            }
            shoppingCart.setNumber(1);
            shoppingCart.setCreateTime(LocalDateTime.now());
            shoppingCartMapper.insert(shoppingCart);
        }
    }
}

创建ShoppingCartMapper接口:

package com.sky.mapper;

import com.sky.entity.ShoppingCart;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;
import java.util.List;

@Mapper
public interface ShoppingCartMapper {
    /**
     * 条件查询
     *
     * @param shoppingCart
     * @return
     */
    List<ShoppingCart> list(ShoppingCart shoppingCart);

    /**
     * 更新商品数量
     *
     * @param shoppingCart
     */
    @Update("update shopping_cart set number = #{number} where id = #{id}")
    void updateNumberById(ShoppingCart shoppingCart);

    /**
     * 插入购物车数据
     *
     * @param shoppingCart
     */
    @Insert("insert into shopping_cart (name, user_id, dish_id, setmeal_id, dish_flavor, number, amount, image, create_time) " +
            " values (#{name},#{userId},#{dishId},#{setmealId},#{dishFlavor},#{number},#{amount},#{image},#{createTime})")
    void insert(ShoppingCart shoppingCart);

}

创建ShoppingCartMapper.xml:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.ShoppingCartMapper">
    <select id="list" parameterType="ShoppingCart" resultType="ShoppingCart">
        select * from shopping_cart
        <where>
            <if test="userId != null">
                and user_id = #{userId}
            </if>
            <if test="dishId != null">
                and dish_id = #{dishId}
            </if>
            <if test="setmealId != null">
                and setmeal_id = #{setmealId}
            </if>
            <if test="dishFlavor != null">
                and dish_flavor = #{dishFlavor}
            </if>
        </where>
        order by create_time desc
    </select>
</mapper>

查看购物车

当用户添加完菜品和套餐后,可进入到购物车中,查看购物中的菜品和套餐。直接点击下面的购物车按钮,进行Get请求,查询ShoppingCart 表中的数据,返回到小程序端

每条购物车记录仅保存 ‌一个菜品‌ 或 ‌一个套餐‌(通过 dishId 或 setmealId 区分)
多个菜品需要插入多条 ShoppingCart 记录

BaseContext.getCurrentId() :获取当前微信用户的id

在ShoppingCartController中创建查看购物车的方法:

	/**
     * 查看购物车
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("查看购物车")
    public Result<List<ShoppingCart>> list(){
        return Result.success(shoppingCartService.showShoppingCart());
    }

在ShoppingCartService接口中声明查看购物车的方法:

	/**
     * 查看购物车
     * @return
     */
    List<ShoppingCart> showShoppingCart();

在ShoppingCartServiceImpl中实现查看购物车的方法:

	/**
     * 查看购物车
     * @return
     */
    public List<ShoppingCart> showShoppingCart() {
        return shoppingCartMapper.list(ShoppingCart.
                                       builder().
                                       userId(BaseContext.getCurrentId()).
                                       build());
    }

// 感觉有点多余再构建一个空的ShoppingCart 对象了,你可以直接user_id, 进行查询 ShoppingCart 数据表中的数据

清空购物车

根据用户id 进行删除 ShoppingCart 表中的数据 即可

在ShoppingCartController中创建清空购物车的方法:

	/**
     * 清空购物车商品
     * @return
     */
    @DeleteMapping("/clean")
    @ApiOperation("清空购物车商品")
    public Result<String> clean(){
        shoppingCartService.cleanShoppingCart();
        return Result.success();
    }

在ShoppingCartService接口中声明清空购物车的方法:

	/**
     * 清空购物车商品
     */
    void cleanShoppingCart();

在ShoppingCartServiceImpl中实现清空购物车的方法:

	/**
     * 清空购物车商品
     */
    public void cleanShoppingCart() {
        shoppingCartMapper.deleteByUserId(BaseContext.getCurrentId());
    }

在ShoppingCartMapper接口中创建删除购物车数据的方法:

	/**
     * 根据用户id删除购物车数据
     *
     * @param userId
     */
    @Delete("delete from shopping_cart where user_id = #{userId}")
    void deleteByUserId(Long userId);

day08 :

导入地址簿功能代码
用户下单

订单支付

1. 导入地址簿功能代码

地址簿,指的是消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址。

对于地址簿管理,我们需要实现以下几个功能:

- 查询地址列表
- 新增地址
- 修改地址
- 删除地址
- 设置默认地址
- 查询默认地址

接口设计:

​​​​​ 新增地址

查询登录用户所有地址

查询默认地址

根据id修改地址

根据id删除地址

根据id查询地址

设置默认地址

​​​​​新增地址

head json body: 详细地址, 手机号 , sex

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

    private static final long serialVersionUID = 1L;

    private Long id;

    //用户id
    private Long userId;

    //收货人
    private String consignee;

    //手机号
    private String phone;

    //性别 0 女 1 男
    private String sex;

    //省级区划编号
    private String provinceCode;

    //省级名称
    private String provinceName;

    //市级区划编号
    private String cityCode;

    //市级名称
    private String cityName;

    //区级区划编号
    private String districtCode;

    //区级名称
    private String districtName;

    //详细地址
    private String detail;

    //标签
    private String label;

    //是否默认 0否 1是
    private Integer isDefault;
}
Controller 层 直接传这个

用户的地址信息会存储在address_book表,即地址簿表中

这里面有一个字段is_default,实际上我们在设置默认地址时,只需要更新这个字段就可以了。

对于这一类的单表的增删改查,我们已经写过很多了,基本的开发思路都是一样的,那么本小节的用户地址簿管理的增删改查功能,我们就不再一一实现了,基本的代码我们都已经提供了,直接导入进来,做一个测试即可。

创建AddressBookMapper.java

package com.sky.mapper;

import com.sky.entity.AddressBook;
import org.apache.ibatis.annotations.*;
import java.util.List;

@Mapper
public interface AddressBookMapper {

    /**
     * 条件查询
     * @param addressBook
     * @return
     */
    List<AddressBook> list(AddressBook addressBook);

    /**
     * 新增
     * @param addressBook
     */
    @Insert("insert into address_book" +
            "        (user_id, consignee, phone, sex, province_code, province_name, city_code, city_name, district_code," +
            "         district_name, detail, label, is_default)" +
            "        values (#{userId}, #{consignee}, #{phone}, #{sex}, #{provinceCode}, #{provinceName}, #{cityCode}, #{cityName}," +
            "                #{districtCode}, #{districtName}, #{detail}, #{label}, #{isDefault})")
    void insert(AddressBook addressBook);

    /**
     * 根据id查询
     * @param id
     * @return
     */
    @Select("select * from address_book where id = #{id}")
    AddressBook getById(Long id);

    /**
     * 根据id修改
     * @param addressBook
     */
    void update(AddressBook addressBook);

    /**
     * 根据 用户id修改 是否默认地址
     * @param addressBook
     */
    @Update("update address_book set is_default = #{isDefault} where user_id = #{userId}")
    void updateIsDefaultByUserId(AddressBook addressBook);

    /**
     * 根据id删除地址
     * @param id
     */
    @Delete("delete from address_book where id = #{id}")
    void deleteById(Long id);

}

创建AddressBookMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.AddressBookMapper">

    <select id="list" parameterType="AddressBook" resultType="AddressBook">
        select * from address_book
        <where>
            <if test="userId != null">
                and user_id = #{userId}
            </if>
            <if test="phone != null">
                and phone = #{phone}
            </if>
            <if test="isDefault != null">
                and is_default = #{isDefault}
            </if>
        </where>
    </select>

    <update id="update" parameterType="addressBook">
        update address_book
        <set>
            <if test="consignee != null">
                consignee = #{consignee},
            </if>
            <if test="sex != null">
                sex = #{sex},
            </if>
            <if test="phone != null">
                phone = #{phone},
            </if>
            <if test="detail != null">
                detail = #{detail},
            </if>
            <if test="label != null">
                label = #{label},
            </if>
            <if test="isDefault != null">
                is_default = #{isDefault},
            </if>
        </set>
        where id = #{id}
    </update>

</mapper>

创建AddressBookService.java

package com.sky.service;

import com.sky.entity.AddressBook;
import java.util.List;

public interface AddressBookService {

    List<AddressBook> list(AddressBook addressBook);

    void save(AddressBook addressBook);

    AddressBook getById(Long id);

    void update(AddressBook addressBook);

    void setDefault(AddressBook addressBook);

    void deleteById(Long id);

}

创建AddressBookServiceImpl.java

package com.sky.service.impl;

import com.sky.context.BaseContext;
import com.sky.entity.AddressBook;
import com.sky.mapper.AddressBookMapper;
import com.sky.service.AddressBookService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;

@Service
@Slf4j
public class AddressBookServiceImpl implements AddressBookService {
    @Autowired
    private AddressBookMapper addressBookMapper;

    /**
     * 条件查询
     *
     * @param addressBook
     * @return
     */
    public List<AddressBook> list(AddressBook addressBook) {
        return addressBookMapper.list(addressBook);
    }

    /**
     * 新增地址
     *
     * @param addressBook
     */
    public void save(AddressBook addressBook) {
        addressBook.setUserId(BaseContext.getCurrentId());
        addressBook.setIsDefault(0);
        addressBookMapper.insert(addressBook);
    }

    /**
     * 根据id查询
     *
     * @param id
     * @return
     */
    public AddressBook getById(Long id) {
        AddressBook addressBook = addressBookMapper.getById(id);
        return addressBook;
    }

    /**
     * 根据id修改地址
     *
     * @param addressBook
     */
    public void update(AddressBook addressBook) {
        addressBookMapper.update(addressBook);
    }

    /**
     * 设置默认地址
     *
     * @param addressBook
     */
    @Transactional
    public void setDefault(AddressBook addressBook) {
        //1、将当前用户的所有地址修改为非默认地址 update address_book set is_default = ? where user_id = ?
        addressBook.setIsDefault(0);
        addressBook.setUserId(BaseContext.getCurrentId());
        addressBookMapper.updateIsDefaultByUserId(addressBook);

        //2、将当前地址改为默认地址 update address_book set is_default = ? where id = ?
        addressBook.setIsDefault(1);
        addressBookMapper.update(addressBook);
    }

    /**
     * 根据id删除地址
     *
     * @param id
     */
    public void deleteById(Long id) {
        addressBookMapper.deleteById(id);
    }

}

Controller层

package com.sky.controller.user;

import com.sky.context.BaseContext;
import com.sky.entity.AddressBook;
import com.sky.result.Result;
import com.sky.service.AddressBookService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/user/addressBook")
@Api(tags = "C端地址簿接口")
public class AddressBookController {

    @Autowired
    private AddressBookService addressBookService;

    /**
     * 查询当前登录用户的所有地址信息
     *
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("查询当前登录用户的所有地址信息")
    public Result<List<AddressBook>> list() {
        AddressBook addressBook = new AddressBook();
        addressBook.setUserId(BaseContext.getCurrentId());
        List<AddressBook> list = addressBookService.list(addressBook);
        return Result.success(list);
    }

    /**
     * 新增地址
     *
     * @param addressBook
     * @return
     */
    @PostMapping
    @ApiOperation("新增地址")
    public Result save(@RequestBody AddressBook addressBook) {
        addressBookService.save(addressBook);
        return Result.success();
    }

    @GetMapping("/{id}")
    @ApiOperation("根据id查询地址")
    public Result<AddressBook> getById(@PathVariable Long id) {
        AddressBook addressBook = addressBookService.getById(id);
        return Result.success(addressBook);
    }

    /**
     * 根据id修改地址
     *
     * @param addressBook
     * @return
     */
    @PutMapping
    @ApiOperation("根据id修改地址")
    public Result update(@RequestBody AddressBook addressBook) {
        addressBookService.update(addressBook);
        return Result.success();
    }

    /**
     * 设置默认地址
     *
     * @param addressBook
     * @return
     */
    @PutMapping("/default")
    @ApiOperation("设置默认地址")
    public Result setDefault(@RequestBody AddressBook addressBook) {
        addressBookService.setDefault(addressBook);
        return Result.success();
    }

    /**
     * 根据id删除地址
     *
     * @param id
     * @return
     */
    @DeleteMapping
    @ApiOperation("根据id删除地址")
    public Result deleteById(Long id) {
        addressBookService.deleteById(id);
        return Result.success();
    }

    /**
     * 查询默认地址
     */
    @GetMapping("default")
    @ApiOperation("查询默认地址")
    public Result<AddressBook> getDefault() {
        //SQL:select * from address_book where user_id = ? and is_default = 1
        AddressBook addressBook = new AddressBook();
        addressBook.setIsDefault(1);
        addressBook.setUserId(BaseContext.getCurrentId());
        List<AddressBook> list = addressBookService.list(addressBook);

        if (list != null && list.size() == 1) {
            return Result.success(list.get(0));
        }

        return Result.error("没有查询到默认地址");
    }

}

用户下单

用户下单业务说明: 在电商系统中,用户是通过下单的方式通知商家,用户已经购买了商品,需要商家进行备货和发货。 用户下单后会产生订单相关数据

用户将菜品或者套餐加入购物车后,可以点击购物车中的 "去结算" 按钮,页面跳转到订单确认页面,点击 "去支付" 按钮则完成下单操作。

我们现在开发的是订单确认部分:

对于数据库表:

用户下单业务对应的数据表为orders表和order_detail表(一对多关系,一个订单关联多个订单明细):

orders -- 订单表 --- 主要存储订单的基本信息(如: 订单号、状态、金额、支付方式、下单用户、收件地址等)

order_detail -- 订单明细表 -- 主要存储订单详情信息(如: 该订单关联的套餐及菜品的信息)
说明:用户提交订单时,需要往订单表orders中插入一条记录,并且需要往order_detail中插入一条或多条记录。
@Data
public class OrdersSubmitDTO implements Serializable {
    //地址簿id
    private Long addressBookId;
    //付款方式
    private int payMethod;
    //备注
    private String remark;
    //预计送达时间
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime estimatedDeliveryTime;
    //配送状态  1立即送出  0选择具体时间
    private Integer deliveryStatus;
    //餐具数量
    private Integer tablewareNumber;
    //餐具数量状态  1按餐量提供  0选择具体数量
    private Integer tablewareStatus;
    //打包费
    private Integer packAmount;
    //总金额
    private BigDecimal amount;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderSubmitVO implements Serializable {
    //订单id
    private Long id;
    //订单号
    private String orderNumber;
    //订单金额
    private BigDecimal orderAmount;
    //下单时间
    private LocalDateTime orderTime;
}

创建OrderController并提供用户下单方法:

package com.sky.controller.user;

import com.sky.dto.OrdersPaymentDTO;
import com.sky.dto.OrdersSubmitDTO;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.OrderService;
import com.sky.vo.OrderPaymentVO;
import com.sky.vo.OrderSubmitVO;
import com.sky.vo.OrderVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

/**
 * 订单
 */
@RestController("userOrderController")
@RequestMapping("/user/order")
@Slf4j
@Api(tags = "C端-订单接口")
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 用户下单
     *
     * @param ordersSubmitDTO
     * @return
     */
    @PostMapping("/submit")
    @ApiOperation("用户下单")
    public Result<OrderSubmitVO> submit(@RequestBody OrdersSubmitDTO ordersSubmitDTO) {
        log.info("用户下单:{}", ordersSubmitDTO);
        OrderSubmitVO orderSubmitVO = orderService.submitOrder(ordersSubmitDTO);
        return Result.success(orderSubmitVO);
    } 

}

创建OrderService接口,并声明用户下单方法:

package com.sky.service;

import com.sky.dto.*;
import com.sky.vo.OrderSubmitVO;

public interface OrderService {

    /**
     * 用户下单
     * @param ordersSubmitDTO
     * @return
     */
    OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO);
}

创建OrderServiceImpl实现OrderService接口:

package com.sky.service.impl;

/**
 * 订单
 */
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderDetailMapper orderDetailMapper;
    @Autowired
    private ShoppingCartMapper shoppingCartMapper;
    @Autowired
    private AddressBookMapper addressBookMapper;


    /**
     * 用户下单
     *
     * @param ordersSubmitDTO
     * @return
     */
    @Transactional
    public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {
        //异常情况的处理(收货地址为空、超出配送范围、购物车为空)
        AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
        if (addressBook == null) {
            throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
        }

        Long userId = BaseContext.getCurrentId();
        ShoppingCart shoppingCart = new ShoppingCart();
        shoppingCart.setUserId(userId);

        //查询当前用户的购物车数据
        List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);
        if (shoppingCartList == null || shoppingCartList.size() == 0) {
            throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
        }

        //构造订单数据
        Orders order = new Orders();
        BeanUtils.copyProperties(ordersSubmitDTO,order);
        order.setPhone(addressBook.getPhone());
        order.setAddress(addressBook.getDetail());
        order.setConsignee(addressBook.getConsignee());
        order.setNumber(String.valueOf(System.currentTimeMillis()));
        order.setUserId(userId);
        order.setStatus(Orders.PENDING_PAYMENT);
        order.setPayStatus(Orders.UN_PAID);
        order.setOrderTime(LocalDateTime.now());

        //向订单表插入1条数据
        orderMapper.insert(order);

        //订单明细数据
        List<OrderDetail> orderDetailList = new ArrayList<>();
        for (ShoppingCart cart : shoppingCartList) {
            OrderDetail orderDetail = new OrderDetail();
            BeanUtils.copyProperties(cart, orderDetail);
            orderDetail.setOrderId(order.getId());
            orderDetailList.add(orderDetail);
        }

        //向明细表插入n条数据
        orderDetailMapper.insertBatch(orderDetailList);

        //清理购物车中的数据
        shoppingCartMapper.deleteByUserId(userId);

        //封装返回结果
        OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder()
                .id(order.getId())
                .orderNumber(order.getNumber())
                .orderAmount(order.getAmount())
                .orderTime(order.getOrderTime())
                .build();

        return orderSubmitVO;
    }
    
}

创建OrderMapper接口和对应的xml映射文件:

OrderMapper.java

package com.sky.mapper;

@Mapper
public interface OrderMapper {
    /**
     * 插入订单数据
     * @param order
     */
    void insert(Orders order);
}

OrderMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderMapper">

    <insert id="insert" parameterType="Orders" useGeneratedKeys="true" keyProperty="id">
        insert into orders
        (number, status, user_id, address_book_id, order_time, checkout_time, pay_method, pay_status, amount, remark,
         phone, address, consignee, estimated_delivery_time, delivery_status, pack_amount, tableware_number,
         tableware_status)
        values (#{number}, #{status}, #{userId}, #{addressBookId}, #{orderTime}, #{checkoutTime}, #{payMethod},
                #{payStatus}, #{amount}, #{remark}, #{phone}, #{address}, #{consignee},
                #{estimatedDeliveryTime}, #{deliveryStatus}, #{packAmount}, #{tablewareNumber}, #{tablewareStatus})
    </insert>
</mapper>

创建OrderDetailMapper接口和对应的xml映射文件:

OrderDetailMapper.java

package com.sky.mapper;

import com.sky.entity.OrderDetail;
import java.util.List;

@Mapper
public interface OrderDetailMapper {

    /**
     * 批量插入订单明细数据
     * @param orderDetails
     */
    void insertBatch(List<OrderDetail> orderDetails);

}

OrderDetailMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderDetailMapper">

    <insert id="insertBatch" parameterType="list">
        insert into order_detail
        (name, order_id, dish_id, setmeal_id, dish_flavor, number, amount, image)
        values
        <foreach collection="orderDetails" item="od" separator=",">
            (#{od.name},#{od.orderId},#{od.dishId},#{od.setmealId},#{od.dishFlavor},
             #{od.number},#{od.amount},#{od.image})
        </foreach>
    </insert>

</mapper>

订单支付

微信支付的时序图:

mchid是商户号,out_trade_no是订单号,appid是应用的id,notify_url是回调地址,amount是总金额,payer是支付者,openid是当前付款用户的openid。

notify_url是回调地址:  向商务系统推送支付结果的地址

在配置项application.yml中加入如下代码:

sky:
  wechat:
    appid: ${sky.wechat.appit}
    secret: ${sky.wechat.secret}
    mchid: ${sky.wechat.mchid}
    mchSerialNo: ${sky.wechat.mchid}
    privateKeyFilePath: ${sky.wechat.privateKeyFilePath}
    apiV3Key: ${sky.wechat.apiV3Key}
    weChatPayCertFilePath: ${sky.wechat.weChatPayCertFilePath}
    notifyUrl: ${sky.wechat.notifyUrl}
    refundNotifyUrl: ${sky.wechat.refundNotifyUrl}

在application-dev.yml文件中写入如下的代码(要注意notifyUrl和refundNotifyUrl,这两个url的前半部分都是cpolar临时生成的公网ip,因为是临时域名,所以每次生成的都会变化,以后要注意修改):

sky:
  wechat:
    appid: wx3910b10fd7db38d1
    secret: f274884c2a56466016ebbcf009404e63
    mchid: 1561414331
    mchSerialNo: 4B3B3DC35414AD50B1B755BAF8DE9CC7CF407606
    privateKeyFilePath: C:\software\apiclient_key.pem
    apiV3Key: CZBK51236435wxpay435434323FFDuv3
    weChatPayCertFilePat: C:\software\wechatpay_166D96F876F45C7D07CE98952A96EC980368ACFC.pem
    notifyUrl: https://9ea0754.r19.cpolar.top/notify/paySuccess
    refundNotifyUrl: https://9ea0754.r19.cpolar.top/notify/refundSuccess

 notifyUrl和refundNotifyUrl这俩地址是由微信后台请求的。

1. 将OrderController下的payment方法(订单支付)代码,复制到sky-server的controller的user下的OrderController中。

2. 将OrderService下的payment和paySuccess方法(订单支付,支付成功修改订单状态)代码,复制到sky-server的service下。

3. 将OrderServiceImpl下的payment和paySuccess方法(订单支付,支付成功修改订单状态)代码,复制到sky-server的service下的Impl中。还有注入下面的工具类。

@Autowired
private WeChatPayUtil weChatPayUtil;

4. 将OrderMapper下的getByNumber和update方法代码,复制到sky-server的Mapper下。

这个update方法一定要复制,后面章节频繁用到这个方法。

5. 直接把我下面完整版的update代码复制到sky-server的resources的mapper下的OrderMapper.xml中。

<update id="update" parameterType="Orders">
        update orders
        <set>
            <if test="number != null"> number=#{number}, </if>
            <if test="status != null"> status=#{status}, </if>
            <if test="addressBookId != null"> address_book_id=#{addressBookId}, </if>
            <if test="orderTime != null"> order_time=#{orderTime},</if>
            <if test="checkoutTime != null"> checkout_time=#{checkoutTime}, </if>
            <if test="payMethod != null"> pay_method=#{payMethod}, </if>
            <if test="payStatus != null"> pay_status=#{payStatus}, </if>
            <if test="amount != null"> amount=#{amount}, </if>
            <if test="remark != null"> remark=#{remark}, </if>
            <if test="phone != null"> phone=#{phone}, </if>
            <if test="address != null"> address=#{address}, </if>
            <if test="userName != null"> user_name=#{userName}, </if>
            <if test="consignee != null"> consignee=#{consignee} ,</if>
            <if test="cancelReason != null"> cancel_reason=#{cancelReason}, </if>
            <if test="rejectionReason != null"> rejection_reason=#{rejectionReason}, </if>
            <if test="cancelTime != null"> cancel_time=#{cancelTime}, </if>
            <if test="estimatedDeliveryTime != null"> estimated_delivery_time=#{estimatedDeliveryTime}, </if>
            <if test="deliveryStatus != null"> delivery_status=#{deliveryStatus}, </if>
            <if test="deliveryTime != null"> delivery_Time=#{deliveryTime}, </if>
            <if test="packAmount != null"> pack_amount=#{packAmount},</if>
            <if test="tablewareNumber != null"> tableware_number=#{tablewareNumber}, </if>
            <if test="tablewareStatus != null"> tableware_status=#{tablewareStatus}, </if>
        </set>
        where id=#{id}
    </update>

6.将下面代码,复制到sky-server的Mapper下的UserMapper中:

@Select("select * from user where id=#{id}")
User getById(Long userId);

7. 最后在sky-server的controller下创建notify包,将PayNotifyController类复制到notify包下。

因为没有营业执照没办法真正支付,所以下面打算直接绕过支付,直接支付成功。

需要修改2个地方:1个是微信小程序,点击支付按钮后直接跳转支付成功。另1个是后端,要求在收到前端支付操作后,不进行任何判断,直接给数据库设置已支付状态。

下面是修改过程:

首先在微信小程序里的pay包下的index.js中将如下的代码注释掉:

Ctrl + f, 搜索 res.code, 然后注释掉 if 中的代码,然后把原先注释掉的重定向解除【下面有注释】

然后在IDEA中,把service/impl下的OrderServiceImpl中pymant方法替换成下面的代码:

/**
* 订单支付
* @param ordersPaymentDTO
* @return
*/
 public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
        // 当前登录用户id
        Long userId = BaseContext.getCurrentId();
        User user = userMapper.getById(userId);
/*        //调用微信支付接口,生成预支付交易单
        JSONObject jsonObject = weChatPayUtil.pay(
                ordersPaymentDTO.getOrderNumber(), //商户订单号
                new BigDecimal(0.01), //支付金额,单位 元
                "苍穹外卖订单", //商品描述
                user.getOpenid() //微信用户的openid
        );
 
        if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) {
            throw new OrderBusinessException("该订单已支付");
        }
*/
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code","ORDERPAID");
        OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
        vo.setPackageStr(jsonObject.getString("package"));
        Integer OrderPaidStatus = Orders.PAID;//支付状态,已支付
        Integer OrderStatus = Orders.TO_BE_CONFIRMED;  //订单状态,待接单
        LocalDateTime check_out_time = LocalDateTime.now();//更新支付时间
        orderMapper.updateStatus(OrderStatus, OrderPaidStatus, check_out_time, this.orders.getId());
        return vo;
    }

 在OrderMapper中写入如下代码:

@Update("update orders set status = #{orderStatus},pay_status = #{orderPaidStatus} ,checkout_time = #{check_out_time} where id = #{id}")
void updateStatus(Integer orderStatus, Integer orderPaidStatus, LocalDateTime check_out_time, Long id);

现在还有一个问题就是orderId怎么获取?方法如下:

在OrderServiceImpl里加一个全局变量orders(如下图),在submitOrder方法(提交订单)中的给全局变量赋值

day 9

根据资料自行导入代码即可

day 10

- Spring Task
- 订单状态定时处理
- WebSocket
- 来单提醒
- 客户催单

1. Spring Task

Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。

定位:定时任务框架

作用:定时自动执行某段Java代码

cron表达式 其实就是一个字符串,通过cron表达式可以定义任务触发的时间

构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义

每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)

举例:
2022年10月12日上午9点整 对应的cron表达式为:0 0 9 12 10 ? 2022

说明:一般 日 和 周 的值不同时设置,其中一个设置,另一个用?表示。

比如:描述2月份的最后一天,最后一天具体是几号呢?可能是28号,也有可能是29号,所以就不能写具体数字。

为了描述这些信息,提供一些特殊的字符。这些具体的细节,我们就不用自己去手写,因为这个cron表达式,它其实有在线生成器。

cron表达式在线生成器:https://cron.qqe2.com/

可以直接在这个网站上面,只要根据自己的要求去生成corn表达式即可。所以一般就不用自己去编写这个表达式。

通配符:

* 表示所有值; 

? 表示未说明的值,即不关心它为何值; 

- 表示一个指定的范围; 

, 表示附加一个可能值; 

/ 符号前表示开始时间,符号后表示每次递增的值;

cron表达式案例【可以练习一下】:

*/5 * * * * ? 每隔5秒执行一次   【* 代表该字段的起始值,对于"秒"字段来说就是 0】

0 */1 * * * ? 每隔1分钟执行一次

0 0 5-15 * * ? 每天5-15点整点触发

0 0/3 * * * ? 每三分钟触发一次

0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发 

0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发

0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发

0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时

0 0 10,14,16 * * ? 每天上午10点,下午2点,4点 

Spring Task使用步骤

1). 导入maven坐标 spring-context(已存在)

2). 启动类添加注解 @EnableScheduling 开启任务调度

3). 自定义定时任务类

编写定时任务类:

进入sky-server模块中

package com.sky.task;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * 自定义定时任务类
 */
@Component
@Slf4j
public class MyTask {

    /**
     * 定时任务 每隔5秒触发一次
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void executeTask(){
        log.info("定时任务开始执行:{}",new Date());
    }
}

开启任务调度:

启动类添加注解 @EnableScheduling

package com.sky;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
@EnableCaching
@EnableScheduling
public class SkyApplication {
    public static void main(String[] args) {
        SpringApplication.run(SkyApplication.class, args);
        log.info("server started");
    }
}

订单状态定时处理

用户下单后可能存在的情况:

- 下单后未支付,订单一直处于“待支付”状态
- 用户收货后管理端未点击完成按钮,订单一直处于“派送中”状态

支付超时的订单如何处理?派送中的订单一直不点击完成如何处理?

对于上面两种情况需要通过**定时任务**来修改订单状态,具体逻辑为:

- 通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消”
- 通过定时任务每天凌晨1点检查一次是否存在“派送中”的订单,如果存在则修改订单状态为“已完成”

1). 自定义定时任务类OrderTask(待完善):

package com.sky.task;

/**
 * 自定义定时任务,实现订单状态定时处理
 */
@Component
@Slf4j
public class OrderTask {

    @Autowired
    private OrderMapper orderMapper;

    /**
     * 处理支付超时订单
     */
    @Scheduled(cron = "0 * * * * ?")
    public void processTimeoutOrder(){
        log.info("处理支付超时订单:{}", new Date());
    }

    /**
     * 处理“派送中”状态的订单
     */
    @Scheduled(cron = "0 0 1 * * ?")
    public void processDeliveryOrder(){
        log.info("处理派送中订单:{}", new Date());
    }

}

2). 在OrderMapper接口中扩展方法:

	/**
     * 根据状态和下单时间查询订单
     * @param status
     * @param orderTime
     */
    @Select("select * from orders where status = #{status} and order_time < #{orderTime}")
    List<Orders> getByStatusAndOrdertimeLT(Integer status, LocalDateTime orderTime);

3). 完善定时任务类的processTimeoutOrder方法:

	/**
     * 处理支付超时订单
     */
    @Scheduled(cron = "0 * * * * ?")
    public void processTimeoutOrder(){
        log.info("处理支付超时订单:{}", new Date());

        LocalDateTime time = LocalDateTime.now().plusMinutes(-15);

        // select * from orders where status = 1 and order_time < 当前时间-15分钟
        List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.PENDING_PAYMENT, time);
        if(ordersList != null && ordersList.size() > 0){
            ordersList.forEach(order -> {
                order.setStatus(Orders.CANCELLED);
                order.setCancelReason("支付超时,自动取消");
                order.setCancelTime(LocalDateTime.now());
                orderMapper.update(order);
            });
        }
    }

4). 完善定时任务类的processDeliveryOrder方法:

	/**
     * 处理“派送中”状态的订单
     */
    @Scheduled(cron = "0 0 1 * * ?")
    public void processDeliveryOrder(){
        log.info("处理派送中订单:{}", new Date());
        // select * from orders where status = 4 and order_time < 当前时间-1小时
        LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
        List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.DELIVERY_IN_PROGRESS, time);

        if(ordersList != null && ordersList.size() > 0){
            ordersList.forEach(order -> {
                order.setStatus(Orders.COMPLETED);
                orderMapper.update(order);
            });
        }
    }

4 .来单提醒

用户下单并且支付成功后,需要第一时间通知外卖商家。通知的形式有如下两种:

1. 语音播报 2. 弹出提示框

设计思路:

- 通过WebSocket实现管理端页面和服务端保持长连接状态
- 当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息
- 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
- 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content
  - type 为消息类型,1为来单提醒 2为客户催单
  - orderId 为订单id
  - content 为消息内容

在OrderServiceImpl中注入WebSocketServer对象,修改paySuccess方法,加入如下代码:

	@Autowired
    private WebSocketServer webSocketServer;
	/**
     * 支付成功,修改订单状态
     *
     * @param outTradeNo
     */
    public void paySuccess(String outTradeNo) {
        // 当前登录用户id
        Long userId = BaseContext.getCurrentId();

        // 根据订单号查询当前用户的订单
        Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);

        // 根据订单id更新订单的状态、支付方式、支付状态、结账时间
        Orders orders = Orders.builder()
                .id(ordersDB.getId())
                .status(Orders.TO_BE_CONFIRMED)
                .payStatus(Orders.PAID)
                .checkoutTime(LocalDateTime.now())
                .build();

        orderMapper.update(orders);
		//////////////////////////////////////////////
        Map map = new HashMap();
        map.put("type", 1);//消息类型,1表示来单提醒
        map.put("orderId", orders.getId());
        map.put("content", "订单号:" + outTradeNo);

        //通过WebSocket实现来单提醒,向客户端浏览器推送消息
        webSocketServer.sendToAllClient(JSON.toJSONString(map));
        ///////////////////////////////////////////////////
    }

5.客户催单

根据用户催单的接口定义,在user/OrderController中创建催单方法:

	/**
     * 用户催单
     *
     * @param id
     * @return
     */
    @GetMapping("/reminder/{id}")
    @ApiOperation("用户催单")
    public Result reminder(@PathVariable("id") Long id) {
        orderService.reminder(id);
        return Result.success();
    }
// 根据订单id 进行催单

在OrderService接口中声明reminder方法:

	/**
     * 用户催单
     * @param id
     */
    void reminder(Long id);

在OrderServiceImpl中实现reminder方法:

	/**
     * 用户催单
     *
     * @param id
     */
    public void reminder(Long id) {
        // 查询订单是否存在
        Orders orders = orderMapper.getById(id);
        if (orders == null) {
            throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
        }

        //基于WebSocket实现催单
        Map map = new HashMap();
        map.put("type", 2);//2代表用户催单
        map.put("orderId", id);
        map.put("content", "订单号:" + orders.getNumber());
        webSocketServer.sendToAllClient(JSON.toJSONString(map));
    }

在OrderMapper中添加getById:

	/**
     * 根据id查询订单
     * @param id
     */
    @Select("select * from orders where id=#{id}")
    Orders getById(Long id);

感谢大家的点赞和关注,你们的支持是我创作的动力!

其他细节/没有写到的部分,后续再添加

【学习笔记】java项目—苍穹外卖day01 作为一名软件开发工程师,我们需要了解在软件开发过程中的开发流程, 以及软件开发过程中涉及到的岗位角色,角色的分工、职责, 并了解软件开发中涉及到的三种软件环境。那么这一小节,我们将从 软件开发流程、角色分工、软件环境 三个方面整体介绍一下软件开发。在开发苍穹外卖这个项目之前,我们需要全方位的来介绍一下当前我们学习的这个项目。接下来,我们将从项目简介、产品原型、技术选型三个方面来介绍苍穹外卖这个项目。 阅读详情

相关推荐

苍穹外卖(一)项目概述、环境搭建

作为一名软件开发工程师,我们需要了解在软件开发过程中的开发流程, 以及软件开发过程中涉及到的岗位角色,角色的分工、职责, 并了解软件开发中涉及到的三种软件环境。那么这一小节,我们将从 软件开发流程、角色分工、软件环境 三个方面整体介绍一下软件开发。在开发苍穹外卖这个项目之前,我们需要全方位的来介绍一下当前我们学习的这个项目。接下来,我们将从项目简介、产品原型、技术选型三个方面来介绍苍穹外卖这个项目。

weixin_69912448的博客 4102

Java项目实战《苍穹外卖》 二、项目搭建

本项目(苍穹外卖)是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括 **系统管理后台** 和 **小程序端应用** 两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的分类、菜品、套餐、订单、员工等进行管理维护,对餐厅的各类数据进行统计,同时也可进行来单语音播报功能。小程序端主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单、支付、催单等。

yuan 1599

项目实战————苍穹外卖(DAY11)

Apache ECharts 是一款基于 Javascript 的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。1). 柱形图2). 饼形图3). 折线图不管是哪种形式的图形,最本质的东西实际上是数据,它其实是对数据的一种可视化展示。

qq_62006367的博客 4684

苍穹外卖启动流程

1.启动mysql服务 2.启动nginx 3.启动redis 4.启动后台,小程序,前端页面

m0_48362854的博客 1410

项目:苍穹外卖的初始化

该项目采用前后端分离开发,其中使用Swagger进行接口管理。 该项目主要采用一些比较前沿的技术:SpringBoot+Mybatis+ElementUI+Vue+微信小程序开发等。 该项目采用Git进行版本控制。

苍穹外卖前端部署指南[可运行源码]

本文详细介绍了如何通过nvm管理node版本,成功运行苍穹外卖前端项目的过程。作者分享了从卸载旧版node、安装nvm v1.1.7、配置node v12.22.12环境,到解决npm代理设置和安装@vue/cli 5.0.8的全套解决方案。重点说明了版本兼容性问题,提供了nvm安装路径建议、镜像源配置等实用技巧,并附有完整的命令行操作步骤。最后作者总结了项目运行失败的主要原因是版本控制,并邀请读者在评论区交流其他可能遇到的问题。

最适合新手的SpringBoot+SSM项目《苍穹外卖》实战—(二)开发环境搭建

黑马程序员最新Java项目实战《苍穹外卖》,最适合新手的SpringBoot+SSM的企业级Java项目实战。

赵士杰——成功并非一蹴而就,而是在持之以恒的努力中逐渐实现。 4353

labview设置工业摄像头外部触发

使用labview对海康MV-CA013-GM相机设置外部触发。 一、硬件部分 1、硬件引脚定义 2、触发模式设置 相机的触发模式分为两种,一种是内触发模式、另外一种是外触发模式。 3、接线图 信号线的地要跟给信号的设备共地。信号线给高电平时触发拍照。 二、软件部分 1、创建vision acquisition 2、选择使用的摄像头。 3、选择模式。 4、在Acquisition Control中设置这些选项。 5、设置保存图片路径 设置保存路径和保存图片的格式。点击完成即可 参考:https:/

qq_43493715的博客 5665

瑞吉外卖和苍穹外卖项目重难点、易错点和涉及到的技术的总结归纳

本项目是专门为餐饮企业定制的一款软件产品,包括管理端和用户端两部分。其中管理端主要提供给外卖商家使用,包括工作台、数据统计、员工管理、菜品管理、套餐管理、分类管理、订单管理、来电提醒等功能。移动端主要提供给消费者使用,包括微信小程序登录、浏览菜品、添加购物车、用户下单、微信支付、历史订单、地址管理、用户催单等功能。

weixin_45836130的博客 3万+

Android Framework之Pkms详解

system 用户并没有访问应用程序目录的权限,但是 Installd 服务是以 root 用户启动的,可以访问 /data/data/ 下的目录,Installd 需要完成一些创建应用数据的任务。Installer 服务是 Android 提供的用于安装的服务,Installer 持有 Installd 守护进程对应的 Binder 服务的代理对象,本质上是通过 Binder 通信调用底层的 Installd 服务真正完成 APK 文件格式的优化和转换、建立相关的数据目录、删除文件、安装应用等工作。

门徒07的博客 1841

Win10 IPv6 远程桌面连接(小米路由器)

背景 由于家里是移动网,且懒得向运营商要公网IPv4,所以想着用IPv6试一下。 家里的环境是这样的: 宽带用的是移动的,没有公网IPv4,但经过测试发现IPv6是公网的 上网通过光猫拨号,而不是通过路由器 路由器是小米AC2100 光猫更改为桥接模式 路由器必须要通过PPPoE拨号,如果是光猫拨号,那么要找对应的维护人员辅助你把光猫改成桥接模式。通过PPPoE拨号路由器就能够拿到公网的IPv6网段,也就可以给底下的其他服务分配公网IPv6了。而如果是光猫拨号,那么只有路由器可以拿到公网的IP。其他的设

timczm的博客 1万+

苍穹外卖总结(面试题)

苍穹外卖内容总结1(适用于马上面试的小伙伴)

2301_77615930的博客 1万+

1、硬件工程师笔面试真题汇总(2025版本)

答:按照使用习惯,电源指示灯用红色,信号指示灯用绿色这两种颜色的LED 灯技术最成熟,价格最便宜通孔是贯穿整个PCB 的过孔盲孔是从PCB 表层连接到内层的过孔埋孔是埋在PCB 内层的过孔。大多数PCB厂家的加工能力是这样的:大于等于8mil 的过孔可以做机械孔小于等于6mil 的过孔需要做激光孔对小于等于6mil 的微型孔,在钻孔空间不够时,允许一部分过打在PCB 焊盘上。

XU157303764的博客 2万+

苍穹外卖(一):项目介绍

作为一名软件开发工程师,我们需要了解在软件开发过程中的开发流程, 以及软件开发过程中涉及到的岗位角色,角色的分工、职责, 并了解软件开发中涉及到的三种软件环境。那么这一小节,我们将从软件开发流程、角色分工、软件环境 三个方面整体介绍一下软件开发。在开发苍穹外卖这个项目之前,我们需要全方位的来介绍一下当前我们学习的这个项目。接下来,我们将从项目简介、产品原型、技术选型三个方面来介绍苍穹外卖这个项目。

c55555556的博客 3920

Qt/C++ 音视频开发: 共享解码线程/重复利用解码/极低CPU占用/画面同步/进度同步

通过共享解码线程和重复利用解码技术,可以显著降低CPU占用,提高多媒体应用的性能。同时,画面和进度同步技术保证了视频播放过程的平滑体验。

走向CTO的路上... 985

使用Everything清理垃圾文件的实战方案(持续更新)

本文主要介绍了使用Everything清理垃圾文件的实战方案,希望能对同学们有所帮助。 文章目录 1. Everything工作原理简述 2. Everything实战清理垃圾文件 2.1 清理应用无用文件 2.2 清理无用但遗忘的文件 2.3 使用Python代码自动化删除重复文件

weixin_43178406的博客 2万+

《苍穹外卖》电商实战项目(java)知识点整理(P1~P65)【上】

史上最完整的《苍穹外卖》项目实操笔记【上】,跟视频的每一P对应,涵盖详细步骤与问题的解决方案。如果你操作到某一步卡壳,参考这篇,相信会带给你极大启发。

RuanFun的博客 9万+

PD3.0详解 第六章 协议层—数据消息之—VDM

PD3.0的VDM 很多对PD3.0协议了解不是很深入的同学可能对VDM有一些陌生,因为这个VDM是较为次要,只不过相对沟通电源的协议来讲VDM是次要,但从PD3.0协议的角度来说,这个也是非常重要的,是必须要非常明白的了解这个东西,所以今天大师匈就分享一下这部分的重要知识点。 VDM (vendor define message) VDM是什么意思呢,就是原厂定义的消息,比如PD快充芯片的原厂定义的一些自己独立的message。但是呢这部分也有规定的一些标准的字段。 另外我们要很清楚PD协议的数据分为几类

大师匈的博客 1万+

苍穹外卖day01

需求分析:需求规格说明书(word),产品原型(通过静态网页来展示项目中的业务功能)。设计:ui设计,数据库设计,接口设计。项目代码,单元测试。测试:测试用例,测试报告。软件环境安装,配置定位:专门为餐饮企业(餐厅,饭店)定制的一款软件产品管理端---外卖商家使用用户端---点餐用户使用是通过通过knife4j生成的接口文档,可以在其中调试swagger和yapi。

Salute2005的博客 1761

【笔记篇】Davinci Configurator OS模块(上)

主要讲解了AUTOSAR OS配置项和Vector MICROSAR OS的特性

weixin_47571898的博客 2271

蓝牙模块HM-10手册

蓝牙模块HM-10手册,该手册对蓝牙模块HM-10进行了详细的介绍和使用方法以及一些AT指令。

苍穹外卖之拦截器

对SpringBoot练手项目苍穹外卖的复习,希望可以帮助大家更好的理解学习拦截器

2301_80026107的博客 1217

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

苍穹外卖项目学习总结

1)定位:专门为餐饮企业定制的一款软件产品;功能架构:体现项目中的业务功能模块,分为管理端、用户端;管理端:员工管理、分类管理、菜品管理、套餐管理、订单管理、工作台、数据统计、来单提醒;用户端:微信登录、商品浏览、购物车、用户下单、微信支付、历史订单、地址管理、用户催单;产品原型:用于展示项目的业务功能,一般由产品经理进行设计;技术选型:展示项目中用到的技术框架和中间件等;2)整体结构前端工程基于nignx运行;后端工程基于maven进行项目构建,并进行分模块开发;项目结构序号名称。

qq_55504269的博客 3378

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

苍穹外卖-day01

作为一名软件开发工程师,我们需要了解在软件开发过程中的开发流程, 以及软件开发过程中涉及到的岗位角色,角色的分工、职责, 并了解软件开发中涉及到的三种软件环境。那么这一小节,我们将从 软件开发流程、角色分工、软件环境 三个方面整体介绍一下软件开发。在开发苍穹外卖这个项目之前,我们需要全方位的来介绍一下当前我们学习的这个项目。接下来,我们将从项目简介、产品原型、技术选型三个方面来介绍苍穹外卖这个项目。

qq_63763836的博客 2046

苍穹外卖技术要点

苍穹外卖个人笔记

yhzs_lgh的博客 1678

苍穹外卖-2025 完成基础配置环节(详细图解)

本文主要记录苍穹外卖的基础配置环节,属于本人的学习记录,本人在之前已经看完了黑马阿玮上下部的javase以及24年10月的AI+JavaWeb大部分的内容

2301_80558092的博客 7480
上一篇: 力扣第470场周赛
下一篇: 使用deepseek+cursor快速开发服务端
一只鱼^_
博客等级 码龄3年 1073粉丝 92原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值