苍穹外卖[操作步骤+讲解]
资料下载:
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);
感谢大家的点赞和关注,你们的支持是我创作的动力!
其他细节/没有写到的部分,后续再添加