当前位置: 首页 > news >正文

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

资料下载:

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的bodyJwtBuilder 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) {// 得到DefaultJwtParserClaims 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: tokenuser-secret-key: itheimauser-ttl: 7200000user-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) {//账号不存在throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND);}//密码比对//对前端传过来的明文密码进行md5加密处理password = DigestUtils.md5DigestAsHex(password.getBytes());if (!password.equals(employee.getPassword())) {//密码错误throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);}if (employee.getStatus() == StatusConstant.DISABLE) {//账号被锁定throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED);}//3、返回实体对象return employee;}

调用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
*/@Beanpublic 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/");
}

使用 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);// 设置密码, 默认为123456employee.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 方法:

    // 分页查询 -> 不再使用注解, 而采用动态的sqlPage<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替换默认的ObjectMapperconverter.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查询员工信息@Overridepublic 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 方法:

    // 编辑员工信息@Overridepublic 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 categorywhere 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 {@Autowiredprivate 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);//设置密码,默认密码123456employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));//设置当前记录的创建时间和修改时间employee.setCreateTime(LocalDateTime.now());employee.setUpdateTime(LocalDateTime.now());//设置当前记录创建人id和修改人idemployee.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 {@Autowiredprivate 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;//菜品分类idprivate 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;//菜品分类idprivate 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

    @Autowiredprivate 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;//分类idprivate 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 categoryNamefrom 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_开头的keyclearCache("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 {@Autowiredprivate 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 {@Autowiredprivate SetmealMapper setmealMapper;@Autowiredprivate SetmealDishMapper setmealDishMapper;@Autowiredprivate DishMapper dishMapper;/*** 新增套餐,同时需要保存套餐和菜品的关联关系* @param setmealDTO*/@Transactionalpublic void saveWithDish(SetmealDTO setmealDTO) {Setmeal setmeal = new Setmeal();BeanUtils.copyProperties(setmealDTO, setmeal);//向套餐表插入数据setmealMapper.insert(setmeal);//获取生成的套餐idLong 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;//分类idprivate 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;//分类idprivate 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">selects.*,c.name categoryNamefromsetmeal sleft joincategory cons.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、修改套餐表,执行updatesetmealMapper.update(setmeal);//套餐idLong setmealId = setmealDTO.getId();//2、删除套餐和菜品的关联关系,操作setmeal_dish表,执行deletesetmealDishMapper.deleteBySetmealId(setmealId);List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();setmealDishes.forEach(setmealDish -> {setmealDish.setSetmealId(setmealId);});//3、重新插入套餐和菜品的关联关系,操作setmeal_dish表,执行insertsetmealDishMapper.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: wxffb3637a228223b8secret: 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: tokenuser-secret-key: itheimauser-ttl: 7200000user-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 {@Autowiredprivate UserService userService;@Autowiredprivate 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";@Autowiredprivate WeChatProperties weChatProperties;@Autowiredprivate 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){//调用微信接口服务,获得当前微信用户的openidMap<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 {@Autowiredprivate 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配置类中注册拦截器:

@Autowiredprivate 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 {@Autowiredprivate SetmealMapper setmealMapper;@Autowiredprivate SetmealDishMapper setmealDishMapper;@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate 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 {@Autowiredprivate 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_分类idString 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中添加 】

	@Autowiredprivate 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_开头的keycleanCache("dish_*");return Result.success();}

3). 修改菜品优化

	/*** 修改菜品** @param dishDTO* @return*/@PutMapping@ApiOperation("修改菜品")public Result update(@RequestBody DishDTO dishDTO) {log.info("修改菜品:{}", dishDTO);dishService.updateWithFlavor(dishDTO);//将所有的菜品缓存数据清理掉,所有以dish_开头的keycleanCache("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_开头的keycleanCache("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::100public 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::100public 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 {@Autowiredprivate 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 {@Autowiredprivate ShoppingCartMapper shoppingCartMapper;@Autowiredprivate DishMapper dishMapper;@Autowiredprivate 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) {//如果已经存在,就更新数量,数量加1shoppingCart = 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;//用户idprivate 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 {@Autowiredprivate 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*/@Transactionalpublic 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 {@Autowiredprivate 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 = 1AddressBook 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 {//地址簿idprivate 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 {//订单idprivate 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 {@Autowiredprivate 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 {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate OrderDetailMapper orderDetailMapper;@Autowiredprivate ShoppingCartMapper shoppingCartMapper;@Autowiredprivate AddressBookMapper addressBookMapper;/*** 用户下单** @param ordersSubmitDTO* @return*/@Transactionalpublic 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: wx3910b10fd7db38d1secret: f274884c2a56466016ebbcf009404e63mchid: 1561414331mchSerialNo: 4B3B3DC35414AD50B1B755BAF8DE9CC7CF407606privateKeyFilePath: C:\software\apiclient_key.pemapiV3Key: CZBK51236435wxpay435434323FFDuv3weChatPayCertFilePat: C:\software\wechatpay_166D96F876F45C7D07CE98952A96EC980368ACFC.pemnotifyUrl: https://9ea0754.r19.cpolar.top/notify/paySuccessrefundNotifyUrl: 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 {// 当前登录用户idLong 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 {@Autowiredprivate 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方法,加入如下代码:

	@Autowiredprivate WebSocketServer webSocketServer;/*** 支付成功,修改订单状态** @param outTradeNo*/public void paySuccess(String outTradeNo) {// 当前登录用户idLong 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);

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

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

 

http://www.dtcms.com/a/481868.html

相关文章:

  • 用vs2008做网站教程成都旅游景点排名前十
  • 悟空 AI CRM 的回款功能:加速资金回流,保障企业财务健康
  • 奥威BI金蝶数据分析可视化方案:200+开箱即用报表驱动智能决策
  • 盲盒小程序系统开发:未来趋势与长期价值
  • 查找成绩(数组实现)
  • 桃城区网站制作公司做网站注册商标
  • RCE 漏洞全解析:从原理到实战
  • VScode无法获取扩展 Error while fetching extensions.Failed to fetch
  • 用 Docker + Squoosh 打造图片压缩 API 服务
  • 仙桃网站设计公司易拉罐手工制作大全
  • 企业级DevOps选型新思维:从“工具堆砌”到“平台赋能”
  • ThinkPHP8集成RabbitMQ的完整案例实现 原创
  • 一份关于语言模型对齐的技术论述:从基于PPO的RLHF到直接偏好优化
  • 扬州市建设厅网站网站空间在哪里
  • 开源 C++ QT QML 开发(十九)多媒体--音频录制
  • json转excel python
  • 在传输数据时,网络中会出现的问题
  • jenkins在使用中遇到的问题
  • 第8章 zynq uboot更新系统镜像并引导启动和个人心得
  • 网站系统升级建设合同汽车之家官网首页网页
  • 电销外包公司有哪些seo学习网站
  • 基于弱监督病灶增强的模型展开式快速磁共振成像|文献速递-文献分享
  • 十四、OpenCV中的形态学操作
  • 算法279. 完全平方数
  • Prometheus pushgateway学习
  • MySQL索引结构:B树与B+树
  • 进程的基本认识
  • Webpack 打包优化与骨架屏结合:双管齐下提升前端性能与用户体验
  • 鸿蒙:在沙箱目录下压缩或解压文件
  • 智能SQL客户端Chat2DB技术解析