day01 前端环境搭建 后端环境搭建 熟悉项目结构 导入后端资料到IDEA中
序号 名称 说明 1 sky-take-out maven父工程,统一管理依赖版本,聚合其他子模块 2 sky-common 子模块,存放公共类,例如:工具类、常量类、异常类等 3 sky-pojo 子模块,存放实体类、VO、DTO等 4 sky-server 子模块,后端服务,存放配置文件、Controller、Service、Mapper等
sky-common: 模块中存放的是公共类,供其他模块使用
sky-common模块的每个包的作用: | 名称 | 说明 | | —————- | ——————————————— | | constant | 存放相关常量类 | | context | 存放上下文类 | | enumeration | 项目的枚举类存储 | | exception | 存放自定义异常类 | | json | 处理json转换的类 | | properties | 存放SpringBoot相关的配置属性类 | | result | 返回结果类的封装 | | utils | 常用工具类 |
sky-pojo: 模块中存放的是一些 entity、DTO、VO
sky-pojo模块的每个包的作用:
名称 说明 Entity 实体,通常和数据库中的表对应 DTO 数据传输对象,通常用于程序中各层之间传递数据 VO 视图对象,为前端展示数据提供的对象 POJO 普通Java对象,只有属性和对应的getter和setter
sky-server: 模块中存放的是 配置文件、配置类、拦截器、controller、service、mapper、启动类等sky-server模块的每个包的作用:
名称 说明 config 存放配置类 controller 存放controller类 interceptor 存放拦截器类 mapper 存放mapper接口 service 存放service类 SkyApplication 启动类
数据库环境搭建 前后端联调 后端的初始工程中已经实现了登录 功能,直接进行前后端联调测试即可(别忘了到yml文件中配置数据库连接信息,否则会登录失败)
实现思路:
nginx反向代理和负载均衡 nginx反向代理 nginx反向代理就是将前端发送的动态请求通过nginx转发到后端服务器
nginx反向代理的好处:
提高访问速度 因为nginx本身可以缓存,如果访问同一接口并做了数据缓存,此时则不需要访问服务端,nginx就可以直接返回数据,进而提高了访问速度 进行负载均衡 负载均衡就是将大量请求按照我们指定的方式均衡的分配给集群中的每台服务器 保证后端服务安全 不暴露后端服务地址,将nginx作为请求访问的入口,到达nginx后转发到具体的后端服务器,从而保证后端服务器的安全 nginx反向代理的配置方式:
在nginx目录下,修改nginx.config文件1 2 3 4 5 6 7 8 9 server{ listen 80; server_name localhost; # 当访问 http://localhost:80/api/../.. 这样的接口的时候,它会通过 location /api/ {} 这样的反向代理到 http://localhost:8080/admin/上来。 location /api/{ proxy_pass http://localhost:8080/admin/; #反向代理 } }
proxy_pass: 该指令是用来设置代理服务器的地址,可以是主机名称,IP地址加端口号等形式。 nginx负载均衡 当服务以集群的方式部署时,nginx在转发请求到服务器时就需要做相应的负载均衡。负载均衡从本质上来说也是基于反向代理来实现的,最终效果都是转发请求
nginx负载均衡配置方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 upstream webservers{ # 声明了两组服务器 server 192.168.100.128:8080; server 192.168.100.129:8080; } server{ listen 80; server_name localhost; location /api/{ proxy_pass http://webservers/admin; #负载均衡 } }
upstream: 如果代理服务器是一组服务器的话,我们可以使用upstream指令配置后端服务器组。注: upstream后面的名称可自定义,但要上下保持一致。
以上配置的含义是:监听80端口号,当访问http://localhost:80/api/../..这样的接口时,便会通过 location /api/ {} 这样的反向代理到 http://webservers/admin,根据webservers名称找到一组服务器,根据设置的负载均衡策略(默认是轮询)转发到具体的服务器。
负载均衡的策略:
名称 说明 轮询 默认方式 weight 权重方式,默认为1,权重越高,被分配的客户端请求就越多 ip_hash 依据ip分配方式,这样每个访客可以固定访问一个后端服务 least_conn 依据最少连接方式,把请求优先分配给连接数少的后端服务 url_hash 依据url分配方式,这样相同的url会被分配到同一个后端服务 fair 依据响应时间方式,响应时间短的服务将会被优先分配
具体配置方式:
轮询 weight ip_hash least_conn url_hash fair 1 2 3 4 upstream webservers{ server 192.168.100.128:8080; server 192.168.100.129:8080; }
1 2 3 4 upstream webservers{ server 192.168.100.128:8080 weight=90; server 192.168.100.129:8080 weight=10; }
1 2 3 4 5 upstream webservers{ ip_hash; server 192.168.100.128:8080; server 192.168.100.129:8080; }
1 2 3 4 5 upstream webservers{ least_conn; server 192.168.100.128:8080; server 192.168.100.129:8080; }
1 2 3 4 5 upstream webservers{ hash &request_uri; server 192.168.100.128:8080; server 192.168.100.129:8080; }
1 2 3 4 5 upstream webservers{ server 192.168.100.128:8080; server 192.168.100.129:8080; fair; }
完善登录功能 需求: 员工表的密码字段直接明文存储在数据库中,存在安全风险,需要对密码进行加密后,再进行存储。
实现步骤:
打开数据库中的employee表,修改明文密码为加密后的123456:e10abc3949ba59abbe56e057f20f883e 修改Java代码,前端提交的密码进行MD5加密后再跟数据库中保存的密码进行比对 打开EmployeeServiceImpl类,修改代码1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Override public Employee login (EmployeeLoginDTO employeeLoginDTO) { String username = employeeLoginDTO.getUsername(); String password = employeeLoginDTO.getPassword(); Employee employee = employeeMapper.getByUsername(username); if (employee == null ) { throw new AccountNotFoundException (MessageConstant.ACCOUNT_NOT_FOUND); } password = DigestUtils.md5DigestAsHex(password.getBytes()); if (!password.equals(employee.getPassword())) { throw new PasswordErrorException (MessageConstant.PASSWORD_ERROR); } if (employee.getStatus().equals(StatusConstant.DISABLE) ) { throw new AccountLockedException (MessageConstant.ACCOUNT_LOCKED); } return employee; }
导入接口文档 该项目使用的开发方式是当前企业主流的前后端分离开发,该方式要求在开发前先将接口定义好,这样前后端才能并行开发。
前后端分离开发流程 开发流程:
定义接口并确定接口的路径、请求方式、传入参数、返回参数。 前端开发人员和后端开发人员并行开发,开发过程中也可以自测 前后端人员进行联调测试 提交给测试人员进行最终测试 操作步骤 ApiFox -> 新建项目 -> 项目管理 -> 导入数据 -> 选择Yapi -> 将资料中的json文件导入
导入效果:
Swagger 介绍 Swagger是一个规范和完整的框架,用于生成、描述、调用和可视化RESTful风格的Web服务(https://swagger.io/ )作用:
使前后端分离开发更加方便,利于团队协作 接口文档自动生成,后端开发只需要关注业务逻辑,无需手动编写接口文档 Spring将Swagger纳入了自身标准。可以通过在项目中引入Swaggerfox,来简单快捷的使用Swagger 使用步骤 knife4j是为JavaMVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger-bootstrap-ui,取名kni4j是希望它能像一把匕首一样小巧,轻量,并且功能强悍!
导入knife4j的maven坐标1 2 3 4 <dependency > <groupId > com.github.xiaoymin</groupId > <artifactId > knife4j-spring-boot-starter</artifactId > </dependency >
在WebMvcConfiguration配置类中加入knife4j的配置1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Bean public Docket docket () { ApiInfo apiInfo = new ApiInfoBuilder () .title("苍穹外卖项目接口文档" ) .version("2.0" ) .description("苍穹外卖项目接口文档" ) .build(); Docket docket = new Docket (DocumentationType.SWAGGER_2) .apiInfo(apiInfo) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller" )) .paths(PathSelectors.any()) .build(); return docket; }
继续在该配置类下设置静态资源映射,否则无法访问knife4j生成的接口文档1 2 3 4 5 6 7 8 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 还可以在这里直接测试接口
思考: 通过Swagger就可以生成接口文档了,还需要ApiFox吗?
ApiFox是设计阶段使用的工具,管理和维护接口
Swagger是在开发阶段使用的框架,帮助后端开发人员做后端的接口测试
常用注解 通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性
注解 说明 @Api 用在类上,例如Controller,表示对类的说明 @ApiModel 用在类上,例如entity、DTO、VO @ApiModelProperty 用在属性上,描述属性信息 @ApiOperation 用在方法上,例如Controller的方法,说明方法的用途、作用
通过在各种类上添加上述注解,生成更清晰,可读性更好的接口文档(下面添加的注解,在提供的资料中已经默认添加)
sky-pojo模块中:
EmployeeLoginDTO EmployeeLoginVO 1 2 3 4 5 6 7 8 9 10 11 @Data @ApiModel(description = "员工登录时传递的数据模型") public class EmployeeLoginDTO implements Serializable { @ApiModelProperty("用户名") private String username; @ApiModelProperty("密码") private String password; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Data @Builder @NoArgsConstructor @AllArgsConstructor @ApiModel(description = "员工登录返回的数据格式") public class EmployeeLoginVO implements Serializable { @ApiModelProperty("主键值") private Long id; @ApiModelProperty("用户名") private String userName; @ApiModelProperty("姓名") private String name; @ApiModelProperty("jwt令牌") private String token; }
sky-server模块中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @RestController @RequestMapping("/admin/employee") @Slf4j @Api(tags = "员工相关接口") public class EmployeeController { @Autowired private EmployeeService employeeService; @Autowired private JwtProperties jwtProperties; @PostMapping("/login") @ApiOperation(value = "员工登录") public Result<EmployeeLoginVO> login (@RequestBody EmployeeLoginDTO employeeLoginDTO) { } @PostMapping("/logout") @ApiOperation("员工退出") public Result<String> logout () { return Result.success(); } }
day02 课程内容 新增员工 员工分页查询 启用禁用员工账号 编辑员工 导入分类模块功能代码 功能实现: 员工管理、菜品分类管理。
新增员工 需求分析和设计 点击新增员工按钮后,弹出新增员工的弹窗
当填写完表单信息,点击保存按钮后,会将表单的数据提交到服务端,在服务端中接收数据,并调用相关方法保存到数据库
注意:
账号必须唯一 手机号必须是合法的十一位数字(1开头,且第二位数字不为2) 身份证号为合法的18位数字(前17位为数字,最后一位为数字或X) 密码默认为123456 在资料中找到项目接口文档 -> 苍穹外卖-管理端接口.html
明确新增员工接口的请求路径、请求方式、请求参数、返回数据
本项目约定:
管理端 发出的请求,统一使用 /admin 作为前缀。用户端 发出的请求,统一使用 /user 作为前缀。代码开发 设置DTO类 根据接口来设计对应的DTO 前端传递参数列表:
思考: 为什么不能使用对应实体类,还要专门封装DTO类?
当前端提交数据和实体类中对应属性差别较大时,建议使用DTO来封装数据 在sky-pojo模块中已经定义了EmployeeDTO类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Data public class EmployeeDTO implements Serializable { private Long id; private String username; private String name; private String phone; private String sex; private String idNumber; }
创建接口和实现类 Controller层
在sky-server模块的EmployeeController类中创建新增员工方法,接收前端提交的参数
1 2 3 4 5 6 7 8 9 10 11 12 @PostMapping @ApiOperation("新增员工") public Result save (@RequestBody EmployeeDTO employeeDTO) { log.info("新增员工:{}" ,employeeDTO); employeeService.save(employeeDTO); return Result.success(); }
在sky-common模块的com.sky.result包中定义了Result类用来统一返回结果格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Data public class Result <T> implements Serializable { private Integer code; 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; } }
Service层接口
在sky-server模块的com.sky.server.EmployeeService中声明新增员工方法
1 2 3 4 5 void save (EmployeeDTO employeeDTO) ;
Service层实现类
在sky-server模块的com.sky.server.impl.EmployeeServiceImpl中实现新增员工方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Override public void save (EmployeeDTO employeeDTO) { Employee employee = new Employee (); BeanUtils.copyProperties(employeeDTO, employee); employee.setStatus(StatusConstant.ENABLE); employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes())); employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now()); employee.setCreateUser(10L ); employee.setUpdateUser(10L ); employeeMapper.insert(employee); }
Mapper层 在EmployeeMapper接口中添加新增员工方法,并通过注解的方式来映射SQL语句
1 2 3 4 5 6 7 8 @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})") void insert (Employee employee) ;
在application.yml中已开启驼峰命名,故id_number和idNumber可对应。
功能测试 功能测试实现方式:
接口文档测试 启动服务: 访问[http://localhost:8080/doc.html][http://localhost:8080/doc.html],进入新增员工接口
Json数据:
1 2 3 4 5 6 7 8 { "id" : 0 , "idNumber" : "111222333444555666" , "name" : "xiaozhi" , "phone" : "13812344321" , "sex" : "1" , "username" : "小智" }
发送请求后,返回的响应码是401报错 通过控制台打印的信息可以知道是JwtTokenAdminInterceptor拦截器出现的问题报错原因: JWT令牌失效导致EmployeeController的save方法没有被调用解决方法: 调用员工登录接口获得一个合法的JWT令牌
获得令牌: 使用admin用户登录获取令牌
添加令牌:
将合法的JWT令牌添加到全局参数中 文档管理—>全局参数设置—>添加参数
接口测试:
其中,请求头部含有JWT令牌
前后端联调测试 启动服务后,访问localhost ,就能访问到登录界面 登录 -> 员工管理 -> 添加员工
期间可能会出现一些错误不必处理,因为那是分页查询没有完成的问题
填写信息并保存后就可以在数据库中看到对应的信息了
由于开发阶段前端和后端是并行开发的,后端完成某个功能后,此时前端对应的功能可能还没有开发完成,导致无法进行前后端联调测试。所以在开发阶段,后端测试主要以接口文档测试为主。
代码完善 目前存在的问题主要有两个:
如果录入的用户名已存在,报出的异常没有进行处理,导致程序异常退出 新增员工时,现在是将创建人和修改人的id设置为固定值,后期需要根据登录用户来动态设置 问题一 问题描述: 数据库中username字段设置了唯一索引,当用户录入的用户名已存在时会直接抛出异常而没有进行处理
解决方法: 添加全局异常处理器
在sky-server模块的com.sky.hander包下有一个GlobalExceptionHandler类,用于处理全局异常,在其中添加以下代码用于处理SQL异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @ExceptionHandler public Result exceptionHandler (SQLIntegrityConstraintViolationException ex) { String message = ex.getMessage(); if (message.contains("Duplicate entry" )){ String[] split = message.split(" " ); String username = split[2 ]; String msg = username + MessageConstant.ALREADY_EXISTS; return Result.error(msg); }else { return Result.error(MessageConstant.UNKNOWN_ERROR); } }
在sky-common模块的MessageConstant类中添加以下常量
1 public static final String ALREADY_EXISTS = "已存在" ;
此时重启服务器,再次添加一个已经存在的用户名,就会返回已存在的错误信息了
问题二 问题描述: 新增员工时,现在是将创建人和修改人的id设置为固定值,后期需要根据登录用户来动态设置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Override public void save (EmployeeDTO employeeDTO) { Employee employee = new Employee (); BeanUtils.copyProperties(employeeDTO, employee); employee.setStatus(StatusConstant.ENABLE); employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes())); employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now()); employee.setCreateUser(10L ); employee.setUpdateUser(10L ); employeeMapper.insert(employee); }
解决方法: 通过某种方式动态获取当前员工的id
员工登录成功后就会生成JWT令牌并响应给前端
在EmployeeController的login方法中已经添加过以下生成JWT令牌的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 @RestController @RequestMapping("/admin/employee") @Slf4j @Api(tags = "员工相关接口") public class EmployeeController { @Autowired private EmployeeService employeeService; @Autowired private JwtProperties jwtProperties; @PostMapping("/login") @ApiOperation(value = "员工登录") public Result<EmployeeLoginVO> login (@RequestBody EmployeeLoginDTO employeeLoginDTO) { Map<String, Object> claims = new HashMap <>(); claims.put(JwtClaimsConstant.EMP_ID, employee.getId()); String token = JwtUtil.createJWT( jwtProperties.getAdminSecretKey(), jwtProperties.getAdminTtl(), claims); return Result.success(employeeLoginVO); } }
在后续请求中,前端都会携带JWT令牌,通过JWT令牌可以解析出当前员工的id(该方法写在sky-server模块中的com.sky.interceptor包下)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Component @Slf4j public class JwtTokenAdminInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader(jwtProperties.getAdminTokenName()); try { log.info("jwt校验:{}" , token); Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token); Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString()); log.info("当前员工id:" , empId); return true ; } catch (Exception ex) { response.setStatus(401 ); return false ; } } }
思考: 光是解析出员工id,如何传递给Service的save方法呢? 通过ThreadLocal来传递
ThreadLocal 简介: ThreadLocal并不是一个线程,而是一个Thread的局部变量 ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问,而每一次用户的请求都是一个独立的线程,所以可以通过ThreadLocal来传递当前员工的id
常用方法:
public void set(T value) 设置当前线程的线程局部变量的值public T get() 返回当前线程所对应的线程局部变量的值public void remove() 移除当前线程的线程局部变量解决问题:
在初始工程中的sky-common模块已经封装了ThreadLocal操作的工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class BaseContext { public static ThreadLocal<Long> threadLocal = new ThreadLocal <>(); public static void setCurrentId (Long id) { threadLocal.set(id); } public static Long getCurrentId () { return threadLocal.get(); } public static void removeCurrentId () { threadLocal.remove(); } }
在拦截器中解析出当前登录员工的id,并放入线程局部变量中(setCurrentId方法)1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Component @Slf4j public class JwtTokenAdminInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { try { Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token); Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString()); log.info("当前员工id:" , empId); BaseContext.setCurrentId(empId); return true ; } catch (Exception ex) { } } }
在Service中直接调用BaseContext.getCurrentId()方法获取当前登录员工的id1 2 employee.setCreateUser(BaseContext.getCurrentId()); employee.setUpdateUser(BaseContext.getCurrentId());
测试:使用admin用户来添加一条记录,查看是否能保留创建者的id
员工分页查询 需求分析和设计 需求分析: 当系统之员工很多时,将数据以分页的方式来展示更加清晰明确。在分页查询页面,除了分页条件以外,还有一个查询条件”员工姓名”
业务规则:
根据页码展示员工信息 每页展示10条数据 分页查询时可以根据需要,输入员工姓名进行查询 接口设计: 资料 -> 项目接口文档 -> 苍穹外卖-管理端接口.html
注意:
请求参数类型为Query,不是json格式提交,在路径后直接拼接。/admin/employee/page?name=zhangsan 返回数据中records数组中使用Employee实体类对属性进行封装。 代码开发 设置DTO类 根据请求参数进行封装,在sky-pojo模块中创建EmployeePageQueryDTO类
1 2 3 4 5 6 7 8 9 10 11 12 13 @Data public class EmployeePageQueryDTO implements Serializable { private String name; private int page; private int pageSize; }
封装分页结果类 后面所有的分页查询,统一都封装为PageResult对象。
放在sky-common模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Data @AllArgsConstructor @NoArgsConstructor public class PageResult implements Serializable { private long total; private List records; }
员工信息分页查询后端返回的对象类型为: Result<PageResult>1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Data public class Result <T> implements Serializable { private Integer code; 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; } }
创建接口和实现类 Controller层 在员工控制层添加分页查询方法
1 2 3 4 5 6 7 8 9 10 11 12 @GetMapping("/page") @ApiOperation("员工分页查询") public Result<PageResult> page (EmployeePageQueryDTO employeePageQueryDTO) { log.info("员工分页查询,参数为:{}" , employeePageQueryDTO); PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO); return Result.success(pageResult); }
Service层接口 声明pageQuery方法
1 2 3 4 5 6 PageResult pageQuery (EmployeePageQueryDTO employeePageQueryDTO) ;
Service层实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public PageResult pageQuery (EmployeePageQueryDTO employeePageQueryDTO) { 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); }
注意: 该功能是使用mybatis提供的分页插件PageHelper来简化分页代码的开发,底层基于mybatis的拦截器实现,该功能需要在pom.xml文中添加依赖(初始工程已添加)
1 2 3 4 5 <dependency > <groupId > com.github.pagehelper</groupId > <artifactId > pagehelper-spring-boot-starter</artifactId > <version > ${pagehelper}</version > </dependency >
Mapper层
在EmployeeMapper中声明pageQuery方法
1 2 3 4 5 6 Page<Employee> pageQuery (EmployeePageQueryDTO employeePageQueryDTO) ;
因为该分页操作涉及动态条件查询,所以通过配置文件来写sql语句,在 src/main/resources/mapper/EmployeeMapper.xml 中编写SQL1 2 3 4 5 6 7 8 9 <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 >
功能测试 接口文档测试 重启服务: 访问http://localhost:8080/doc.html,进入员工分页查询
如果一直报401错误,可能是token失效的原因,此时需要重新登录获取token
响应结果:
前后端联调测试 点击员工管理并输入员工姓名
此时员工的分页查询功能已经顺利完成了,但是仔细观察会发现,显示的 最后操作时间 格式很怪,这个问题在 代码完善 中解决
代码完善 问题描述: 操作时间字段显示有问题。
解决方式:
方式一: 在属性上加上注解@JsonFormat,指定日期格式方式二: 添加消息转换器,统一日期类型的格式处理这里推荐使用方式二,因为方式一的处理方式,只对单一属性进行处理,需要每个时间类型都加上@JsonFormat注解,效率较低,而方式二的处理方式,是对所有日期类型进行统一处理,效率较高
解决方法 在sky-server模块下的WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Override protected void extendMessageConverters (List<HttpMessageConverter<?>> converters) { log.info("扩展消息转换器..." ); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter (); converter.setObjectMapper(new JacksonObjectMapper ()); converters.add(0 ,converter); }
消息转化器配置中的对象转换器,是基础工程中已经配置好的JacksonObjectMapper,它已经对日期类型进行了格式处理,我们只需要在WebMvcConfiguration中扩展消息转换器,将其加入容器中即可
此时再次测试,就能看到正确的显示时间格式了
启用/禁用员工账号 需求分析和设计 需求分析: 在员工管理列表页面,可以对某个员工账号进行启用或者禁用操作。账号禁用的员工不能登录系统,启用后的员工可以正常登录。如果某个员工账号状态为正常,则按钮显示为 “禁用”,如果员工账号状态为已禁用,则按钮显示为”启用”,关于按钮的显示切换在前端已经完成,后端只需要改变员工账号的状态即可。
业务规则:
可以对状态为“启用” 的员工账号进行“禁用”操作 可以对状态为“禁用”的员工账号进行“启用”操作 状态为“禁用”的员工账号不能登录系统 接口设计
代码开发 Controller层 根据接口设计中的请求参数形式对应的在EmployeeController中创建启用/禁用员工账号的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 @PostMapping("/status/{status}") @ApiOperation("启用禁用员工账号") public Result startOrStop (@PathVariable Integer status,Long id) { log.info("启用禁用员工账号:{},{}" ,status,id); employeeService.startOrStop(status,id); return Result.success(); }
Service层接口 在EmployeeService接口中声明启用/禁用员工账号的业务方法:
1 2 3 4 5 6 void startOrStop (Integer status, Long id) ;
Service层实现类 在EmployeeServiceImpl中实现启用/禁用员工账号的业务方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void startOrStop (Integer status, Long id) { Employee employee = Employee.builder() .status(status) .id(id) .build(); employeeMapper.update(employee); }
Mapper层 在EmployeeMapper接口中声明 update 方法:
1 2 3 4 5 void update (Employee employee) ;
在EmployeeMapper.xml中编写SQL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 < 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 >
功能测试 接口文档测试 在测试前,查询员工id和账号状态,在接口文档中作为相应的参数进行测试,测试完毕后,再次查询员工账号状态,查看是否成功启用/禁用员工账号
前后端联调测试 点击相应的按钮,会对员工账号进行启用/禁用,并提示账号状态是否更改成功
编辑员工 需求分析和设计 需求分析: 员工管理界面点击编辑按钮即可跳转到员工编辑页面,页面中显示当前员工信息,用户可以修改员工信息并保存。
员工编辑页面应当回显当前员工信息,用户在当前员工信息的基础上进行修改
接口设计: 编辑员工共涉及到两个接口:
根据id查询员工信息
编辑员工信息
代码开发 回显员工信息 Controller层 在 EmployeeController 中创建 getById 方法:1 2 3 4 5 6 7 8 9 10 11 @GetMapping("/{id}") @ApiOperation("根据id查询员工信息") public Result<Employee> getById (@PathVariable Long id) { Employee employee = employeeService.getById(id); return Result.success(employee); }
Service层接口 在 EmployeeService 接口中声明 getById 方法:1 2 3 4 5 6 Employee getById (Long id) ;
Service层实现类 在 EmployeeServiceImpl 中实现 getById 方法:1 2 3 4 5 6 7 8 9 10 public Employee getById (Long id) { Employee employee = employeeMapper.getById(id); employee.setPassword("****" ); return employee; }
Mapper层 在 EmployeeMapper 接口中声明 getById 方法:1 2 3 4 5 6 7 @Select("select * from employee where id = #{id}") Employee getById (Long id) ;
修改员工信息功能 Controller层 在 EmployeeController 中创建 update 方法:
1 2 3 4 5 6 7 8 9 10 11 12 @PutMapping @ApiOperation("编辑员工信息") public Result update (@RequestBody EmployeeDTO employeeDTO) { log.info("编辑员工信息:{}" , employeeDTO); employeeService.update(employeeDTO); return Result.success(); }
Service层接口 在 EmployeeService 接口中声明 update 方法:
1 2 3 4 5 void update (EmployeeDTO employeeDTO) ;
Service层实现类 在 EmployeeServiceImpl 中实现 update 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public void update (EmployeeDTO employeeDTO) { Employee employee = new Employee (); BeanUtils.copyProperties(employeeDTO, employee); employee.setUpdateTime(LocalDateTime.now()); employee.setUpdateUser(BaseContext.getCurrentId()); employeeMapper.update(employee); }
在实现启用禁用员工账号 功能时,已实现employeeMapper.update(employee),在此不需写Mapper层代码,直接调用即可
功能测试 接口文档测试 分别测试根据id查询员工信息和编辑员工信息接口
根据id查询员工信息 获得了对应id的相关员工信息
编辑员工信息 修改id=4的员工信息,name 由zhangsan 改为张三丰 ,username 由张三 改为zhangsanfeng 。
查看employee表数据
前后端联调测试 进入到员工列表查询
对员工姓名为杰克的员工数据修改,点击修改,数据已回显 修改后,点击保存
导入分类模块功能代码 需求分析和设计 需求分析 后台系统中可以管理分类信息,分类包括两种类型,分别是 菜品分类 和 套餐分类 。
分析菜品分类相关功能:
新增菜品分类: 当我们在后台系统中添加菜品时需要选择一个菜品分类,在移动端也会按照菜品分类来展示对应的菜品。
菜品分类分页查询: 系统中的分类很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
根据id删除菜品分类: 在分类管理列表页面,可以对某个分类进行删除操作。需要注意的是当分类关联了菜品或者套餐时,此分类不允许删除。
修改菜品分类: 在分类管理列表页面点击修改按钮,弹出修改窗口,在修改窗口回显分类信息并进行修改,最后点击确定按钮完成修改操作。
启用禁用菜品分类: 在分类管理列表页面,可以对某个分类进行启用或者禁用操作。
分类类型查询: 当点击分类类型下拉框时,从数据库中查询所有的菜品分类数据进行展示。
分类管理原型:
业务规则:
分类名称必须是唯一的 分类按照类型可以分为菜品分类和套餐分类 新添加的分类状态默认为“禁用” 接口设计 根据上述分析,菜品分类模块共涉及6个接口:
新增分类 分类分页查询 根据id删除分类 修改分类 启用禁用分类 根据类型查询分类 新增分类 分类分页查询 根据id删除分类 修改分类 启用禁用分类 根据类型查询分类 表设计 category表结构: 字段名 数据类型 说明 备注 id bigint 主键 自增 name varchar(32) 分类名称 唯一 type int 分类类型 1菜品分类 2套餐分类 sort int 排序字段 用于分类数据的排序 status int 状态 1启用 0禁用 create_time datetime 创建时间 update_time datetime 最后修改时间 create_user bigint 创建人id update_user bigint 最后修改人id
代码导入 Mapper层 CategoryMapper.java DishMapper.java SetmealMapper.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Mapper public interface CategoryMapper { @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) ; Page<Category> pageQuery (CategoryPageQueryDTO categoryPageQueryDTO) ; @Delete("delete from category where id = #{id}") void deleteById (Long id) ; void update (Category category) ; List<Category> list (Integer type) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 @Mapper public interface DishMapper { @Select("select count(id) from dish where category_id = #{categoryId}") Integer countByCategoryId (Long categoryId) ; }
1 2 3 4 5 6 7 8 9 10 11 12 @Mapper public interface SetmealMapper { @Select("select count(id) from setmeal where category_id = #{categoryId}") Integer countByCategoryId (Long id) ; }
Service层 接口类
CategoryService.java EmployeeService.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public interface CategoryService { void save (CategoryDTO categoryDTO) ; PageResult pageQuery (CategoryPageQueryDTO categoryPageQueryDTO) ; void deleteById (Long id) ; void update (CategoryDTO categoryDTO) ; void startOrStop (Integer status, Long id) ; List<Category> list (Integer type) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public interface EmployeeService { Employee login (EmployeeLoginDTO employeeLoginDTO) ; void save (EmployeeDTO employeeDTO) ; PageResult pageQuery (EmployeePageQueryDTO employeePageQueryDTO) ; void startOrStop (Integer status, Long id) ; void update (EmployeeDTO employeeDTO) ; Employee getById (Long id) ; }
实现类 CategoryServiceImpl.java EmployeeServiceImpl.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 @Service @Slf4j public class CategoryServiceImpl implements CategoryService { @Autowired private CategoryMapper categoryMapper; @Autowired private DishMapper dishMapper; @Autowired private SetmealMapper setmealMapper; @Override public void save (CategoryDTO categoryDTO) { Category category = new Category (); BeanUtils.copyProperties(categoryDTO, category); category.setStatus(StatusConstant.DISABLE); category.setCreateTime(LocalDateTime.now()); category.setUpdateTime(LocalDateTime.now()); category.setCreateUser(BaseContext.getCurrentId()); category.setUpdateUser(BaseContext.getCurrentId()); categoryMapper.insert(category); } @Override public PageResult pageQuery (CategoryPageQueryDTO categoryPageQueryDTO) { PageHelper.startPage(categoryPageQueryDTO.getPage(),categoryPageQueryDTO.getPageSize()); Page<Category> page = categoryMapper.pageQuery(categoryPageQueryDTO); return new PageResult (page.getTotal(), page.getResult()); } @Override public void deleteById (Long id) { Integer count = dishMapper.countByCategoryId(id); if (count > 0 ){ throw new DeletionNotAllowedException (MessageConstant.CATEGORY_BE_RELATED_BY_DISH); } count = setmealMapper.countByCategoryId(id); if (count > 0 ){ throw new DeletionNotAllowedException (MessageConstant.CATEGORY_BE_RELATED_BY_SETMEAL); } categoryMapper.deleteById(id); } @Override public void update (CategoryDTO categoryDTO) { Category category = new Category (); BeanUtils.copyProperties(categoryDTO,category); category.setUpdateTime(LocalDateTime.now()); category.setUpdateUser(BaseContext.getCurrentId()); categoryMapper.update(category); } @Override public void startOrStop (Integer status, Long id) { Category category = Category.builder() .id(id) .status(status) .updateTime(LocalDateTime.now()) .updateUser(BaseContext.getCurrentId()) .build(); categoryMapper.update(category); } @Override public List<Category> list (Integer type) { return categoryMapper.list(type); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 @Service public class EmployeeServiceImpl implements EmployeeService { @Autowired private EmployeeMapper employeeMapper; @Override public Employee login (EmployeeLoginDTO employeeLoginDTO) { String username = employeeLoginDTO.getUsername(); String password = employeeLoginDTO.getPassword(); Employee employee = employeeMapper.getByUsername(username); if (employee == null ) { throw new AccountNotFoundException (MessageConstant.ACCOUNT_NOT_FOUND); } password = DigestUtils.md5DigestAsHex(password.getBytes()); if (!password.equals(employee.getPassword())) { throw new PasswordErrorException (MessageConstant.PASSWORD_ERROR); } if (employee.getStatus().equals(StatusConstant.DISABLE)) { throw new AccountLockedException (MessageConstant.ACCOUNT_LOCKED); } return employee; } @Override public void save (EmployeeDTO employeeDTO) { Employee employee = new Employee (); BeanUtils.copyProperties(employeeDTO, employee); employee.setStatus(StatusConstant.ENABLE); employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes())); employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now()); employee.setCreateUser(BaseContext.getCurrentId()); employee.setUpdateUser(BaseContext.getCurrentId()); employeeMapper.insert(employee); } @Override public PageResult pageQuery (EmployeePageQueryDTO employeePageQueryDTO) { 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); } @Override public void startOrStop (Integer status, Long id) { Employee employee = Employee.builder() .status(status) .id(id) .build(); employeeMapper.update(employee); } @Override public Employee getById (Long id) { Employee employee = employeeMapper.getById(id); employee.setPassword("****" ); return employee; } @Override public void update (EmployeeDTO employeeDTO) { Employee employee = new Employee (); BeanUtils.copyProperties(employeeDTO, employee); employee.setUpdateTime(LocalDateTime.now()); employee.setUpdateUser(BaseContext.getCurrentId()); employeeMapper.update(employee); } }
Controller层 CategoryController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 @RestController @RequestMapping("/admin/category") @Api(tags = "分类相关接口") @Slf4j public class CategoryController { @Autowired private CategoryService categoryService; @PostMapping @ApiOperation("新增分类") public Result<String> save (@RequestBody CategoryDTO categoryDTO) { log.info("新增分类:{}" , categoryDTO); categoryService.save(categoryDTO); return Result.success(); } @GetMapping("/page") @ApiOperation("分类分页查询") public Result<PageResult> page (CategoryPageQueryDTO categoryPageQueryDTO) { log.info("分页查询:{}" , categoryPageQueryDTO); PageResult pageResult = categoryService.pageQuery(categoryPageQueryDTO); return Result.success(pageResult); } @DeleteMapping @ApiOperation("删除分类") public Result<String> deleteById (Long id) { log.info("删除分类:{}" , id); categoryService.deleteById(id); return Result.success(); } @PutMapping @ApiOperation("修改分类") public Result<String> update (@RequestBody CategoryDTO categoryDTO) { categoryService.update(categoryDTO); return Result.success(); } @PostMapping("/status/{status}") @ApiOperation("启用禁用分类") public Result<String> startOrStop (@PathVariable("status") Integer status, Long id) { categoryService.startOrStop(status,id); return Result.success(); } @GetMapping("/list") @ApiOperation("根据类型查询分类") public Result<List<Category>> list (Integer type) { List<Category> list = categoryService.list(type); return Result.success(list); } }
功能测试 分页查询和分类类型
启用禁用
修改 回显
修改后
新增
删除
day03 课程内容 公共字段自动填充 新增菜品 菜品分页查询 删除菜品 修改菜品 功能实现: 菜品管理
菜品管理效果图:
公共字段自动填充 需求分析 在前面学习的员工管理功能和菜品分类功能,在 添加员工 或者 添加菜品分类 时都需要设置创建时间、创建人、修改时间、修改人等字段,在 编辑员工 或 编辑菜品 分类时需要设置修改时间、修改人等字段。这些字段属于公共字段也就是在系统中很多表中都会有这些字段,如下:
序号 字段名 含义 数据类型 1 create_time 创建时间 datetime 2 create_user 创建人id bigint 3 update_time 修改时间 datetime 4 update_user 修改人id bigint
针对这些字段的赋值方式通常为:
新增数据时, 将createTime、updateTime 设置为当前时间, createUser、updateUser设置为当前登录用户ID。 更新数据时, 将updateTime设置为当前时间, updateUser设置为当前登录用户ID。 目前处理这些字段都是通过在对应的添加、修改方法中手动设置这些字段的值来实现,但是这样存在问题,比如新增、修改方法中如果忘记设置这些字段的值,那么这些字段的值就会丢失,且处理方法相对冗余、繁琐。因此,我们可以通过AOP切面编程来实现这些公共字段的自动填充。
实现思路 公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码,前面提到的共有四个公共字段需要进行自动填充,分别是:createTime、updateTime、createUser、updateUser。
序号 字段名 含义 数据类型 操作类型 1 create_time 创建时间 datetime insert 2 create_user 创建人id bigint insert 3 update_time 修改时间 datetime insert、update 4 update_user 修改人id bigint insert、update
实现步骤:
自定义注解AutoFill,用于标识需要进行公共字段自动填充的方法。 自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值。 在Mapper的方法上加入AutoFill注解 代码开发 自定义注解 进入sky-server模块,创建com.sky.annotation包
1 2 3 4 5 6 7 8 9 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AutoFill { OperationType value () ; }
其中OperationType已在sky-common模块中定义1 2 3 4 5 6 7 8 9 10 11 12 13 14 public enum OperationType { UPDATE, INSERT }
自定义切面 在sky-server模块,创建com.sky.aspect包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Aspect @Component @Slf4j public class AutoFillAspect { @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)") public void autoFillPointCut () {} @Before("autoFillPointCut()") public void autoFill (JoinPoint joinPoint) { log.info("开始进行公共字段自动填充..." ); } }
完善自定义切面 AutoFillAspect 的 autoFill 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 @Aspect @Component @Slf4j public class AutoFillAspect { @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)") public void autoFillPointCut () {} @Before("autoFillPointCut()") public void autoFill (JoinPoint joinPoint) { log.info("开始进行公共字段自动填充..." ); MethodSignature signature = (MethodSignature) joinPoint.getSignature(); AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); OperationType operationType = autoFill.value(); Object[] args = joinPoint.getArgs(); if (args == null || args.length == 0 ){ return ; } Object entity = args[0 ]; LocalDateTime now = LocalDateTime.now(); Long currentId = BaseContext.getCurrentId(); if (operationType == OperationType.INSERT){ try { Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class); Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class); Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); setCreateTime.invoke(entity,now); setCreateUser.invoke(entity,currentId); setUpdateTime.invoke(entity,now); setUpdateUser.invoke(entity,currentId); } catch (Exception e) { e.printStackTrace(); } }else if (operationType == OperationType.UPDATE){ try { Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); setUpdateTime.invoke(entity,now); setUpdateUser.invoke(entity,currentId); } catch (Exception e) { e.printStackTrace(); } } } }
添加注解 以CategoryMapper 为例,分别在新增和修改方法添加@AutoFill()注解,也需要EmployeeMapper 做相同操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Mapper public interface CategoryMapper { @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})") @AutoFill(value = OperationType.INSERT) void insert (Category category) ; @AutoFill(value = OperationType.UPDATE) void update (Category category) ; }
执行完这三步操作以后,就可以将业务层中为公共字段赋值的代码注释掉了
功能测试 重启项目并新增一个菜品分类
新增之后,可以去数据库查看category表,新增的分类数据已经包含了公共字段的赋值:
新增菜品 需求分析和设计 需求分析 在后台系统可以管理菜品的信息,通过新增功能来添加新的菜品,在添加时需要选择菜品所属分类并且上传菜品图片
业务规则:
菜品名称必须是唯一的 菜品必须属于某个分类下,不能单独存在 新增菜品时可以根据情况选择菜品的口味 每个菜品必须对应一张图片 接口设计 表设计 新增菜品,其实就是将新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。所以在新增菜品时,涉及到两个表:
表名 说明 dish 菜品表 dish_flavor 菜品口味表
字段名 数据类型 说明 备注 id bigint 主键 自增 name varchar(32) 菜品名称 唯一 category_id bigint 分类id 逻辑外键 price decimal(10,2) 菜品价格 image varchar(255) 图片路径 description varchar(255) 菜品描述 status int 售卖状态 1起售 0停售 create_time datetime 创建时间 update_time datetime 最后修改时间 create_user bigint 创建人id update_user bigint 最后修改人id
字段名 数据类型 说明 备注 id bigint 主键 自增 dish_id bigint 菜品id 逻辑外键 name varchar(32) 口味名称 value varchar(255) 口味值
代码开发 文件上传功能 新增菜品时,需要上传菜品的图片,因此需要使用到文件上传功能。 文件上传是将本地图片、视频、音频等文件上传到服务器,来供其他用户浏览或下载的过程。实现文件上传服务分为对文件进行存储和取出存储的文件两个部分,对于文件的存储解决方案通常有以下几种:
直接将图片保存到服务的硬盘(springmvc中的文件上传) 使用分布式文件系统进行存储优点:容易实现扩容 缺点:开发复杂度稍大(有成熟的产品可以使用,比如:FastDFS,MinIO) 使用第三方的存储服务(例如OSS)优点:开发简单,拥有强大功能,免维护 缺点:付费(个人使用的话费用很低) 此处使用的是阿里云的OSS服务进行文件存储,具体的阿里云OSS内容在最新版的Web课程中有过介绍,这里就不重复介绍了。Web在线笔记
实现步骤:
定义OSS相关配置 在sky-server模块的application-dev.yml中添加以下配置,其中access-key-id和access-key-secret需要从阿里云OSS控制台获取1 2 3 4 5 6 sky: alioss: endpoint: oss-cn-beijing.aliyuncs.com access-key-id: id access-key-secret: secret bucket-name: sky-take-out-silvan
此处的access-key-id和access-key-secret需要从阿里云OSS控制台获取,请勿泄露 endpoint的区域也需要根据bucket所在地区进行选择,如在杭州,则endpoint为oss-cn-hangzhou.aliyuncs.com 读取OSS配置 在sky-common模块中,基础工程已定义1 2 3 4 5 6 7 8 9 10 11 @Component @ConfigurationProperties(prefix = "sky.alioss") @Data public class AliOssProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; }
生成OSS工具类对象 在sky-server模块的config包下新建OSS配置类1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Configuration @Slf4j public class OssConfiguration { @Bean @ConditionalOnMissingBean public AliOssUtil aliOssUtil (AliOssProperties aliOssProperties) { log.info("开始创建阿里云文件上传工具类对象:{}" ,aliOssProperties); return new AliOssUtil (aliOssProperties.getEndpoint(), aliOssProperties.getAccessKeyId(), aliOssProperties.getAccessKeySecret(), aliOssProperties.getBucketName()); } }
其中的AliOssUtil.java已经在sky-common模块中定义了1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 @Data @AllArgsConstructor @Slf4j public class AliOssUtil { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; public String upload (byte [] bytes, String objectName) { OSS ossClient = new OSSClientBuilder ().build(endpoint, accessKeyId, accessKeySecret); try { ossClient.putObject(bucketName, objectName, new ByteArrayInputStream (bytes)); } catch (OSSException oe) { System.out.println("Caught an OSSException, which means your request made it to OSS, " + "but was rejected with an error response for some reason." ); System.out.println("Error Message:" + oe.getErrorMessage()); System.out.println("Error Code:" + oe.getErrorCode()); System.out.println("Request ID:" + oe.getRequestId()); System.out.println("Host ID:" + oe.getHostId()); } catch (ClientException ce) { System.out.println("Caught an ClientException, which means the client encountered " + "a serious internal problem while trying to communicate with OSS, " + "such as not being able to access the network." ); System.out.println("Error Message:" + ce.getMessage()); } finally { if (ossClient != null ) { ossClient.shutdown(); } } StringBuilder stringBuilder = new StringBuilder ("https://" ); stringBuilder .append(bucketName) .append("." ) .append(endpoint) .append("/" ) .append(objectName); log.info("文件上传到:{}" , stringBuilder.toString()); return stringBuilder.toString(); } }
定义文件上传接口 在Controller层中定义接口1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @RestController @RequestMapping("/admin/common") @Api(tags = "通用接口") @Slf4j public class CommonController { @Autowired private AliOssUtil aliOssUtil; @PostMapping("/upload") @ApiOperation("文件上传") public Result<String> upload (MultipartFile file) { log.info("文件上传:{}" ,file); try { String originalFilename = file.getOriginalFilename(); String extension = originalFilename.substring(originalFilename.lastIndexOf("." )); String objectName = UUID.randomUUID().toString() + extension; String filePath = aliOssUtil.upload(file.getBytes(), objectName); return Result.success(filePath); } catch (IOException e) { log.error("文件上传失败:{}" , e); } return Result.error(MessageConstant.UPLOAD_FAILED); } }
新增菜品实现 设计DTO类1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Data public class DishDTO implements Serializable { private Long id; private String name; private Long categoryId; private BigDecimal price; private String image; private String description; private Integer status; private List<DishFlavor> flavors = new ArrayList <>(); }
Controller层1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @RestController @RequestMapping("/admin/dish") @Api(tags = "菜品相关接口") @Slf4j public class DishController { @Autowired private DishService dishService; @PostMapping @ApiOperation("新增菜品") public Result save (@RequestBody DishDTO dishDTO) { log.info("新增菜品:{}" , dishDTO); dishService.saveWithFlavor(dishDTO); return Result.success(); } }
Service层1 2 3 4 5 6 7 8 9 10 public interface DishService { public void saveWithFlavor (DishDTO dishDTO) ; }
ServiceImpl层1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Service @Slf4j public class DishServiceImpl implements DishService { @Autowired private DishMapper dishMapper; @Autowired private DishFlavorMapper dishFlavorMapper; @Transactional public void saveWithFlavor (DishDTO dishDTO) { Dish dish = new Dish (); BeanUtils.copyProperties(dishDTO, dish); dishMapper.insert(dish); Long dishId = dish.getId(); List<DishFlavor> flavors = dishDTO.getFlavors(); if (flavors != null && flavors.size() > 0 ) { flavors.forEach(dishFlavor -> { dishFlavor.setDishId(dishId); }); dishFlavorMapper.insertBatch(flavors); } } }
Mapper层 DishMapper.java DishMapper.xml 在DishMapper中添加
1 2 3 4 5 6 7 @AutoFill(value = OperationType.INSERT) void insert (Dish dish) ;
在/resources/mapper中新建DishMapper.xml
1 2 3 4 5 6 7 8 9 10 <?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_id, price, image, description, create_time, update_time, create_user,update_user, status) values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status}) </insert > </mapper >
DishFlavorMapperDishFlavorMapper.java DishFlavorMapper.xml 在DishFlavorMapper中添加
1 2 3 4 5 6 7 8 @Mapper public interface DishFlavorMapper { void insertBatch (List<DishFlavor> flavors) ; }
在/resources/mapper中新建DishFlavorMapper.xml
1 2 3 4 5 6 7 8 9 10 11 <?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 >
功能测试 登录 -> 菜品管理 -> 新增菜品 -> 输入菜品信息 -> 保存
暂时还没有实现菜品查询功能,所以保存后通过数据库查看添加的数据
菜品分页查询 需求分析和设计 需求分析 系统中的菜品数据很多的时候,如果在一个页面中全部展示出来会显得比较乱,不便于查看,所以一般的系统中都会以分页的方式来展示列表数据。
菜品信息展示时,菜品的图片字段和菜品分类字段较特殊
图片字段在数据库中查询到的仅仅是图片名称,想要回显到页面则需要下载该图片 分类名称存储形式是分类id,需要根据id来查询分类名称并展示
接口设计
代码开发 设计DTO类 根据菜品分页查询接口定义设计对应的DTO:
在sky-pojo模块中提供了DishPageQueryDTO类用于接收菜品分页查询的请求参数
1 2 3 4 5 6 7 8 9 10 11 @Data public class DishPageQueryDTO implements Serializable { private int page; private int pageSize; private String name; private Integer categoryId; private Integer status; }
设计VO类 根据菜品分页查询接口定义设计对应的VO:
在sky-pojo模块中提供了DishVO类用于返回菜品分页查询的结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 @Data @Builder @NoArgsConstructor @AllArgsConstructor public class DishVO implements Serializable { private Long id; private String name; private Long categoryId; private BigDecimal price; private String image; private String description; private Integer status; private LocalDateTime updateTime; private String categoryName; private List<DishFlavor> flavors = new ArrayList <>(); }
Controller层 根据接口定义添加DishController的page分页查询方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 @GetMapping("/page") @ApiOperation("菜品分页查询") public Result<PageResult> page (DishPageQueryDTO dishPageQueryDTO) { log.info("菜品分页查询:{}" , dishPageQueryDTO); PageResult pageResult = dishService.pageQuery(dishPageQueryDTO); return Result.success(pageResult); }
Service层接口 在 DishService中添加分页查询方法:
1 2 3 4 5 6 7 PageResult pageQuery (DishPageQueryDTO dishPageQueryDTO) ;
Service层实现类 在 DishServiceImpl 中实现分页查询方法:
1 2 3 4 5 6 7 8 9 10 11 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层 在 DishMapper 接口中声明 pageQuery 方法:
1 2 3 4 5 6 7 Page<DishVO> pageQuery (DishPageQueryDTO dishPageQueryDTO) ;
在DishMapper.xml中编写SQL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <select id ="pageQuery" resultType ="com.sky.vo.DishVO" > select d.* , c.name as categoryName from dish d left outer 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.category_id = #{categoryId} </if > <if test ="status != null" > and d.status = #{status} </if > </where > order by d.create_time desc </select >
功能测试 删除菜品 需求分析和设计 菜品列表页面中每个菜品后面对应的操作分别为修改 、删除 、停售 ,可通过删除功能完成对菜品及相关的数据进行删除。
业务规则:
可以一次删除一个菜品,也可以批量删除菜品 起售中的菜品不能删除 被套餐关联的菜品不能删除 删除菜品后,关联的口味数据也需要删除掉 接口设计 根据原型图,设计出相应的接口。
注意: 删除一个菜品和批量删除菜品共用一个接口,区别只是参数个数不同。
表设计 删除菜品操作时,会涉及到以下三张表。
注意事项:
在dish表中删除菜品基本数据时,同时,也要把关联在dish_flavor表中的数据一块删除。 setmeal_dish表为菜品和套餐关联的中间表。 若删除的菜品数据关联着某个套餐,此时,删除失败。 若要删除套餐关联的菜品数据,先解除两者关联,再对菜品进行删除。 代码开发 Controller层 在DishController中添加批量删除菜品的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 @DeleteMapping @ApiOperation("菜品批量删除") public Result delete (@RequestParam List<Long> ids) { log.info("菜品批量删除:{}" , ids); dishService.deleteBatch(ids); return Result.success(); }
Service层接口 在DishService接口中声明deleteBatch方法:
1 2 3 4 5 6 void deleteBatch (List<Long> ids) ;
4.2.3 Service层实现类 在DishServiceImpl中实现deleteBatch方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 @Autowired private SetmealDishMapper setmealDishMapper; @Transactional public void deleteBatch (List<Long> ids) { for (Long id : ids) { Dish dish = dishMapper.getById(id); if (dish.getStatus() == StatusConstant.ENABLE) { throw new DeletionNotAllowedException (MessageConstant.DISH_ON_SALE); } } List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids); if (setmealIds != null && setmealIds.size() > 0 ) { throw new DeletionNotAllowedException (MessageConstant.DISH_BE_RELATED_BY_SETMEAL); } for (Long id : ids) { dishMapper.deleteById(id); dishFlavorMapper.deleteByDishId(id); } }
Mapper层 在DishMapper中添加getById方法
1 2 3 4 5 6 7 8 @Select("select * from dish where id = #{id}") Dish getById (Long id) ;
创建SetmealDishMapper,声明getSetmealIdsByDishIds方法,并在xml文件中编写SQL:
SetmealDishMapper.java SetmealDishMapper.xml 1 2 3 4 5 6 7 8 9 10 11 @Mapper public interface SetmealDishMapper { List<Long> getSetmealIdsByDishIds (List<Long> dishIds) ; }
1 2 3 4 5 6 7 8 9 10 11 <?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.SetmealDishMapper" > <select id ="getSetmealIdsByDishIds" resultType ="java.lang.Long" > select setmeal_id from setmeal_dish where dish_id in <foreach collection ="dishIds" item ="dishId" separator ="," open ="(" close =")" > #{dishId} </foreach > </select > </mapper >
在DishMapper.java中声明deleteById方法并配置SQL:
1 2 3 4 5 6 7 @Delete("delete from dish where id = #{id}") void deleteById (Long id) ;
在DishFlavorMapper中声明deleteByDishId方法并配置SQL:
1 2 3 4 5 6 @Delete("delete from dish_flavor where dish_id = #{dishId}") void deleteByDishId (Long dishId) ;
修改菜品 需求分析和设计 点击修改按钮后跳转到菜品修改页面,修改页面会回显菜品相关信息,点击保存则完成修改操作
接口设计 分析可得该页面涉及四个接口:
根据id查询菜品 根据类型查询分类(已实现) 文件上传(已实现) 修改菜品 代码开发 根据id查询菜品实现 Controller层 Service层接口 Service层实现类 Mapper层 在DishController中创建方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 @GetMapping("/{id}") @ApiOperation("根据id查询菜品") public Result<DishVO> getById (@PathVariable Long id) { log.info("根据id查询菜品:{}" , id); DishVO dishVO = dishService.getByIdWithFlavor(id); return Result.success(dishVO); }
在DishService接口中声明getByIdWithFlavor方法:
1 2 3 4 5 6 7 DishVO getByIdWithFlavor (Long id) ;
在DishServiceImpl中实现getByIdWithFlavor方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public DishVO getByIdWithFlavor (Long id) { Dish dish = dishMapper.getById(id); List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id); DishVO dishVO = new DishVO (); BeanUtils.copyProperties(dish, dishVO); dishVO.setFlavors(dishFlavors); return dishVO; }
在DishFlavorMapper中声明getByDishId方法,并配置SQL:
1 2 3 4 5 6 7 @Select("select * from dish_flavor where dish_id = #{dishId}") List<DishFlavor> getByDishId (Long dishId) ;
修改菜品实现 Controller层 Service层接口 Service层实现类 Mapper层 根据修改菜品的接口定义在DishController中创建方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 @PutMapping @ApiOperation("修改菜品") public Result update (@RequestBody DishDTO dishDTO) { log.info("修改菜品:{}" , dishDTO); dishService.updateWithFlavor(dishDTO); return Result.success(); }
在DishService接口中声明updateWithFlavor方法:
1 2 3 4 5 6 void updateWithFlavor (DishDTO dishDTO) ;
在DishServiceImpl中实现updateWithFlavor方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public void updateWithFlavor (DishDTO dishDTO) { Dish dish = new Dish (); BeanUtils.copyProperties(dishDTO, dish); dishMapper.update(dish); dishFlavorMapper.deleteByDishId(dishDTO.getId()); List<DishFlavor> flavors = dishDTO.getFlavors(); if (flavors != null && flavors.size() > 0 ) { flavors.forEach(dishFlavor -> { dishFlavor.setDishId(dishDTO.getId()); }); dishFlavorMapper.insertBatch(flavors); } }
在DishMapper中,声明update方法:
1 2 3 4 5 6 7 @AutoFill(value = OperationType.UPDATE) void update (Dish dish) ;
并在DishMapper.xml文件中编写SQL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <update id ="update" > update dish <set > <if test ="name != null" > name = #{name},</if > <if test ="categoryId != null" > category_id = #{categoryId},</if > <if test ="price != null" > price = #{price},</if > <if test ="image != null" > image = #{image},</if > <if test ="description != null" > description = #{description},</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 >
菜品的起售/停售 day04 完成内容 完成套餐管理模块的相关业务功能,包括:
新增套餐 套餐分页查询 删除套餐 修改套餐 套餐的起售/停售 新增套餐 需求分析和设计 业务规则:
套餐名称唯一 套餐必须属于某个分类 套餐必须包含菜品 名称、分类、价格、图片为必填项 添加菜品窗口需要根据分类类型来展示菜品 新增的套餐默认为停售状态 接口设计 涉及四个接口:
根据类型查询分类(已完成) 根据分类id查询菜品 图片上传(已完成) 新增套餐 数据库设计
setmeal表为套餐表,用于存储套餐的信息。
字段名 数据类型 说明 备注 id bigint 主键 自增 name varchar(32) 套餐名称 唯一 category_id bigint 分类id 逻辑外键 price decimal(10,2) 套餐价格 image varchar(255) 图片路径 description varchar(255) 套餐描述 status int 售卖状态 1起售 0停售 create_time datetime 创建时间 update_time datetime 最后修改时间 create_user bigint 创建人id update_user bigint 最后修改人id
setmeal_dish表为套餐菜品关系表,用于存储套餐和菜品的关联关系
字段名 数据类型 说明 备注 id bigint 主键 自增 setmeal_id bigint 套餐id 逻辑外键 dish_id bigint 菜品id 逻辑外键 name varchar(32) 菜品名称 冗余字段 price decimal(10,2) 菜品单价 冗余字段 copies int 菜品份数
代码实现 Controller层 DishController SetmealController 在DishController中添加根据分类id查询菜品的方法:
1 2 3 4 5 6 7 8 9 10 11 @GetMapping("/list") @ApiOperation("根据分类id查询菜品") public Result<List<Dish>> list (Long categoryId) { List<Dish> list = dishService.list(categoryId); return Result.success(list); }
在SetmealController中添加新增套餐的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @RestController @RequestMapping("/admin/setmeal") @Api(tags = "套餐相关接口") @Slf4j public class SetmealController { @Autowired private SetmealService setmealService; @PostMapping @ApiOperation("新增套餐") public Result save (@RequestBody SetmealDTO setmealDTO) { setmealService.saveWithDish(setmealDTO); return Result.success(); } }
Service层 DishService DishServiceImpl SetmealService SetmealServiceImpl 在DishService中添加根据分类id查询菜品的方法:
1 2 3 4 5 6 List<Dish> list (Long categoryId) ;
在DishServiceImpl中实现该方法:
1 2 3 4 5 6 7 8 9 10 11 12 public List<Dish> list (Long categoryId) { Dish dish = Dish.builder() .categoryId(categoryId) .status(StatusConstant.ENABLE) .build(); return dishMapper.list(dish); }
在SetmealService中添加新增套餐的方法
1 2 3 4 5 6 7 8 public interface SetmealService { void saveWithDish (SetmealDTO setmealDTO) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @Service @Slf4j public class SetmealServiceImpl implements SetmealService { @Autowired private SetmealMapper setmealMapper; @Autowired private SetmealDishMapper setmealDishMapper; @Autowired private DishMapper dishMapper; @Transactional public void saveWithDish (SetmealDTO setmealDTO) { Setmeal setmeal = new Setmeal (); BeanUtils.copyProperties(setmealDTO, setmeal); setmealMapper.insert(setmeal); Long setmealId = setmeal.getId(); List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes(); setmealDishes.forEach(setmealDish -> { setmealDish.setSetmealId(setmealId); }); setmealDishMapper.insertBatch(setmealDishes); } }
Mapper层 DishMapper SetmealMapper SetmealDishMapper 在DishMapper中添加list方法:
1 2 3 4 5 6 List<Dish> list (Dish dish) ;
在DishMapper.xml文件中编写SQL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <select id ="list" resultType ="Dish" parameterType ="Dish" > select * from dish <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 > order by create_time desc </select >
在SetmealMapper中添加insert方法:
1 2 3 4 5 6 @AutoFill(OperationType.INSERT) void insert (Setmeal setmeal) ;
在SetmealMapper.xml文件中编写SQL:
1 2 3 4 5 6 <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中添加insertBatch方法:
1 2 3 4 5 void insertBatch (List<SetmealDish> setmealDishes) ;
在SetmealDishMapper.xml文件中编写SQL:
1 2 3 4 5 6 7 8 <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 >
功能测试 点击新增套餐按钮,并填写相关套餐信息
点击保存按钮后,去数据库的setmeal表中查看数据是否添加成功
套餐分页查询 需求分析和设计 业务规则:
根据页码进行分页展示 每页展示10条数据 可以根据需要,按照套餐名称、分类、售卖状态进行查询 接口设计
代码实现 Controller层 在SetmealController中添加分页查询方法:
1 2 3 4 5 6 7 8 9 10 11 @GetMapping("/page") @ApiOperation("分页查询") public Result<PageResult> page (SetmealPageQueryDTO setmealPageQueryDTO) { PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO); return Result.success(pageResult); }
Service层 在SetmealService中添加分页查询方法:
1 2 3 4 5 6 PageResult pageQuery (SetmealPageQueryDTO setmealPageQueryDTO) ;
在SetmealServiceImpl中实现该方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 public PageResult pageQuery (SetmealPageQueryDTO setmealPageQueryDTO) { int pageNum = setmealPageQueryDTO.getPage(); int pageSize = setmealPageQueryDTO.getPageSize(); PageHelper.startPage(pageNum, pageSize); Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO); return new PageResult (page.getTotal(), page.getResult()); }
Mapper层 在SetmealMapper中添加分页查询方法:
1 2 3 4 5 6 Page<SetmealVO> pageQuery (SetmealPageQueryDTO setmealPageQueryDTO) ;
在SetmealMapper.xml文件中编写SQL:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <select id ="pageQuery" resultType ="com.sky.vo.SetmealVO" > select s.*,c.name categoryName from setmeal s left join category c on s.category_id = c.id <where > <if test ="name != null" > and s.name like concat('%',#{name},'%') </if > <if test ="status != null" > and s.status = #{status} </if > <if test ="categoryId != null" > and s.category_id = #{categoryId} </if > </where > order by s.create_time desc </select >
功能测试 重启服务器,访问http://localhost ,点击套餐页面的数据已经能够正常显示在浏览器中了
删除套餐 需求分析和设计 点击删除套餐按钮即可在数据库中删除该套餐,套餐可以单个删除,也可以批量删除,起售中的套餐不能删除
接口设计
代码实现 Controller层 在SetmealController中添加批量删除方法
1 2 3 4 5 6 7 8 9 10 11 @DeleteMapping @ApiOperation("批量删除套餐") public Result delete (@RequestParam List<Long> ids) { setmealService.deleteBatch(ids); return Result.success(); }
Service层 在SetmealService中添加批量删除方法
1 2 3 4 5 void deleteBatch (List<Long> ids) ;
在SetmealServiceImpl中实现该方法1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @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); }); }
Mapper层 SetmealMapper SetmealDishMapper 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Select("select * from setmeal where id = #{id}") Setmeal getById (Long id) ; @Delete("delete from setmeal where id = #{id}") void deleteById (Long setmealId) ;
1 2 3 4 5 6 @Delete("delete from setmeal_dish where setmeal_id = #{setmealId}") void deleteBySetmealId (Long setmealId) ;
功能测试 可以通过前后端联调,也可以通过接口文档测试,此处通过接口文档简单进行测试:
再次刷新数据库,该id对应的套餐就被删除了
修改套餐 需求分析和设计 和之前一样,修改套餐需要有数据的回显和对数据的更新
接口设计 共涉及5个接口
根据id查询套餐 根据类型查询分类(已完成) 根据分类id查询菜品(已完成) 图片上传(已完成) 修改套餐 代码实现 Controller层 在SetmealController中添加数据回显和修改套餐的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @GetMapping("/{id}") @ApiOperation("根据id查询套餐") public Result<SetmealVO> getById (@PathVariable Long id) { SetmealVO setmealVO = setmealService.getByIdWithDish(id); return Result.success(setmealVO); } @PutMapping @ApiOperation("修改套餐") public Result update (@RequestBody SetmealDTO setmealDTO) { setmealService.update(setmealDTO); return Result.success(); }
Service层 在SetmealService中添加数据回显和修改套餐的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 SetmealVO getByIdWithDish (Long id) ; void update (SetmealDTO setmealDTO) ;
在SetmealServiceImpl中实现这两个方法1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 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; } @Transactional public void update (SetmealDTO setmealDTO) { Setmeal setmeal = new Setmeal (); BeanUtils.copyProperties(setmealDTO, setmeal); setmealMapper.update(setmeal); Long setmealId = setmealDTO.getId(); setmealDishMapper.deleteBySetmealId(setmealId); List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes(); setmealDishes.forEach(setmealDish -> { setmealDish.setSetmealId(setmealId); }); setmealDishMapper.insertBatch(setmealDishes); }
Mapper层 在SetmealDishMapper中添加查询套餐和套餐菜品关系的方法
1 2 3 4 5 6 7 @Select("select * from setmeal_dish where setmeal_id = #{setmealId}") List<SetmealDish> getBySetmealId (Long setmealId) ;
功能测试 直接通过前后端联调进行简单测试即可,首先新增一个套餐 对该套餐各项数据都进行修改,修改完后点击保存 起售/停售套餐 需求分析和设计 和对菜品的起售/停售操作一样
业务规则:
对状态为起售的套餐进行停售操作,对状态为停售的套餐进行起售操作 起售的套餐可以展示在用户端,停售的套餐不能展示在用户端 起售套餐时,如果套餐内包含停售的菜品,则不能起售 接口设计
代码实现 Controller层 在SetmealController中添加起售/停售套餐的方法
1 2 3 4 5 6 7 8 9 10 11 12 @PostMapping("/status/{status}") @ApiOperation("套餐起售停售") public Result startOrStop (@PathVariable Integer status, Long id) { setmealService.startOrStop(status, id); return Result.success(); }
Service层 在SetmealService中添加起售/停售套餐的方法
1 2 3 4 5 6 void startOrStop (Integer status, Long id) ;
在SetmealServiceImpl中实现该方法1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public void startOrStop (Integer status, Long id) { if (status == StatusConstant.ENABLE){ 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); }
Mapper层 在DishMapper中添加查询菜品的方法
1 2 3 4 5 6 7 @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) ;
功能测试 通过接口文档,测试套餐起售停售功能
day05 课程入门 Redis入门 Redis数据类型 Redis常用命令 在Java中操作Redis 店铺营业状态设置 Redis相关的内容之前学习过并做了相关笔记,此处不再详细记录
功能实现:营业状态设置
效果图:
商家可以自己选择在线状态,状态为营业时客户可以在小程序下单,状态为打样时,客户无法在小程序端下单
Java中操作Redis Redis的Java客户端 在java程序中操作Redis就需要使用Redis的Java客户端,就如同我们使用JDBC操作MySQL数据库一样。 Redis 的 Java 客户端很多,常用的几种:
Jedis Lettuce Spring Data Redis Spring 对 Redis 客户端进行了整合,提供了 Spring Data Redis,在Spring Boot项目中还提供了对应的Starter,即 spring-boot-starter-data-redis。
Spring Data Redis使用方式 介绍 Spring Data Redis 是 Spring 的一部分,提供了在 Spring 应用中通过简单的配置就可以访问 Redis 服务,对 Redis 底层开发包进行了高度封装。 网址:https://spring.io/projects/spring-data-redis
Spring Boot提供了对应的Starter,maven坐标:
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency >
Spring Data Redis中提供了一个高度封装的类:RedisTemplate ,对相关api进行了归类封装,将同一类型操作封装为operation接口,具体分类如下:
ValueOperations:string数据操作 SetOperations:set类型数据操作 ZSetOperations:zset类型数据操作 HashOperations:hash类型的数据操作 ListOperations:list类型的数据操作 环境搭建 进入到sky-server模块
导入Spring Data Redis的maven坐标(已完成)1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency >
配置Redis数据源 在application-dev.yml中添加1 2 3 4 5 6 sky: redis: host: localhost port: 6379 password: password database: 10
说明: database:指定使用Redis的哪个数据库,Redis服务启动后默认有16个数据库,编号分别是从0到15。 可以通过修改Redis配置文件来指定数据库的数量。
在application.yml中添加读取application-dev.yml中的相关Redis配置
1 2 3 4 5 6 7 8 spring: profiles: active: dev redis: host: ${sky.redis.host} port: ${sky.redis.port} password: ${sky.redis.password} database: ${sky.redis.database}
编写配置类,创建RedisTemplate对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Configuration @Slf4j public class RedisConfiguration { @Bean public RedisTemplate redisTemplate (RedisConnectionFactory redisConnectionFactory) { log.info("开始创建redis模板对象..." ); RedisTemplate redisTemplate = new RedisTemplate (); redisTemplate.setConnectionFactory(redisConnectionFactory); redisTemplate.setKeySerializer(new StringRedisSerializer ()); return redisTemplate; } }
注意 当前配置类不是必须的,因为 Spring Boot 框架会自动装配 RedisTemplate 对象,但是默认的key序列化器为JdkSerializationRedisSerializer,导致我们存到Redis中后的数据和原始数据有差别,故设置为StringRedisSerializer序列化器。
通过RedisTemplate对象操作Redis 在test下新建测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @SpringBootTest public class SpringDataRedisTest { @Autowired private RedisTemplate redisTemplate; @Test public void testRedisTemplate () { System.out.println(redisTemplate); ValueOperations valueOperations = redisTemplate.opsForValue(); HashOperations hashOperations = redisTemplate.opsForHash(); ListOperations listOperations = redisTemplate.opsForList(); SetOperations setOperations = redisTemplate.opsForSet(); ZSetOperations zSetOperations = redisTemplate.opsForZSet(); } }
测试:
说明RedisTemplate对象注入成功,并且通过该RedisTemplate对象获取操作5种数据类型相关对象。
常见数据类型 字符串类型 哈希类型 列表类型 集合类型 有序集合类型 通用命令操作 1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void testString () { redisTemplate.opsForValue().set("name" ,"小明" ); String city = (String) redisTemplate.opsForValue().get("name" ); System.out.println(city); redisTemplate.opsForValue().set("code" ,"1234" ,3 , TimeUnit.MINUTES); redisTemplate.opsForValue().setIfAbsent("lock" ,"1" ); redisTemplate.opsForValue().setIfAbsent("lock" ,"2" ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test public void testHash () { HashOperations hashOperations = redisTemplate.opsForHash(); hashOperations.put("100" ,"name" ,"tom" ); hashOperations.put("100" ,"age" ,"20" ); String name = (String) hashOperations.get("100" , "name" ); System.out.println(name); Set keys = hashOperations.keys("100" ); System.out.println(keys); List values = hashOperations.values("100" ); System.out.println(values); hashOperations.delete("100" ,"age" ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test public void testList () { ListOperations listOperations = redisTemplate.opsForList(); listOperations.leftPushAll("mylist" ,"a" ,"b" ,"c" ); listOperations.leftPush("mylist" ,"d" ); List mylist = listOperations.range("mylist" , 0 , -1 ); System.out.println(mylist); listOperations.rightPop("mylist" ); Long size = listOperations.size("mylist" ); System.out.println(size); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Test public void testSet () { SetOperations setOperations = redisTemplate.opsForSet(); setOperations.add("set1" ,"a" ,"b" ,"c" ,"d" ); setOperations.add("set2" ,"a" ,"b" ,"x" ,"y" ); Set members = setOperations.members("set1" ); System.out.println(members); Long size = setOperations.size("set1" ); System.out.println(size); Set intersect = setOperations.intersect("set1" , "set2" ); System.out.println(intersect); Set union = setOperations.union("set1" , "set2" ); System.out.println(union); setOperations.remove("set1" ,"a" ,"b" ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test public void testZset () { ZSetOperations zSetOperations = redisTemplate.opsForZSet(); zSetOperations.add("zset1" ,"a" ,10 ); zSetOperations.add("zset1" ,"b" ,12 ); zSetOperations.add("zset1" ,"c" ,9 ); Set zset1 = zSetOperations.range("zset1" , 0 , -1 ); System.out.println(zset1); zSetOperations.incrementScore("zset1" ,"c" ,10 ); zSetOperations.remove("zset1" ,"a" ,"b" ); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Test public void testCommon () { Set keys = redisTemplate.keys("*" ); System.out.println(keys); Boolean name = redisTemplate.hasKey("name" ); Boolean set1 = redisTemplate.hasKey("set1" ); for (Object key : keys) { DataType type = redisTemplate.type(key); System.out.println(type.name()); } redisTemplate.delete("mylist" ); }
店铺营业状态设置 需求分析和设计 进到苍穹外卖后台,显示餐厅的营业状态,营业状态分为营业中 和打烊中 ,若当前餐厅处于营业状态,自动接收任何订单,客户可在小程序进行下单操作;若当前餐厅处于打烊状态,不接受任何订单,客户便无法在小程序进行下单操作。
接口设计:
注: 从技术层面分析,其实管理端和用户端查询营业状态时,可通过一个接口去实现即可。因为营业状态是一致的。但是,本项目约定:
管理端 发出的请求,统一使用/admin作为前缀。用户端 发出的请求,统一使用/user作为前缀。可以通过一张表来存储营业状态数据,但整个表就一个字段的意义不大,所以营业状态存储方式:基于Redis的字符串来存储约定: 1表示营业 0表示打烊
代码开发 设置营业状态 在sky-server模块中,创建ShopController.java
根据接口定义创建ShopController的setStatus设置营业状态方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @RestController("adminShopController") @RequestMapping("/admin/shop") @Api(tags = "店铺相关接口") @Slf4j public class ShopController { public static final String KEY = "SHOP_STATUS" ; @Autowired private RedisTemplate redisTemplate; @PutMapping("/{status}") @ApiOperation("设置店铺的营业状态") public Result setStatus (@PathVariable Integer status) { log.info("设置店铺的营业状态为:{}" ,status == 1 ? "营业中" : "打烊中" ); redisTemplate.opsForValue().set(KEY,status); return Result.success(); } }
管理端查询营业状态 根据接口定义创建ShopController的getStatus查询营业状态方法:
1 2 3 4 5 6 7 8 9 10 11 @GetMapping("/status") @ApiOperation("获取店铺的营业状态") public Result<Integer> getStatus () { Integer status = (Integer) redisTemplate.opsForValue().get(KEY); log.info("获取到店铺的营业状态为:{}" ,status == 1 ? "营业中" : "打烊中" ); return Result.success(status); }
用户端查询营业状态 创建com.sky.controller.user包,在该包下创建ShopController.java
根据接口定义创建ShopController的getStatus查询营业状态方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @RestController("userShopController") @RequestMapping("/user/shop") @Api(tags = "店铺相关接口") @Slf4j public class ShopController { public static final String KEY = "SHOP_STATUS" ; @Autowired private RedisTemplate redisTemplate; @GetMapping("/status") @ApiOperation("获取店铺的营业状态") public Result<Integer> getStatus () { Integer status = (Integer) redisTemplate.opsForValue().get(KEY); log.info("获取到店铺的营业状态为:{}" ,status == 1 ? "营业中" : "打烊中" ); return Result.success(status); } }
day06 课程内容 HttpClient 微信小程序开发 微信登录 导入商品浏览功能代码 微信小程序开发 介绍 小程序是一种新的开放能力,开发者可以快速地开发一个小程序。可以在微信内被便捷地获取和传播,同时具有出色的使用体验。官方网址: https://mp.weixin.qq.com/cgi-bin/wx?token=&lang=zh_CN
进行小程序开发时,需要先才注册一个小程序,可以以个人的身份来注册,也可以以企业或者其他组织的方式来注册,不同的主题注册小程序,最终的开放权限也不同,比如以个人身份来注册,则无法开通支付权限。 微信官方提供了一系列工具来帮助开发者快速完成小程序的开发,提供了完善的开发文档和开发者工具,还提供了相应的设计指南,同时也提供了一些小程序体验DEMO,可以快速的体验小程序实现的功能。 开发完的小程序想要上线,也提供了详细的接入教程 准备工作 开发微信小程序之前需要做如下准备工作:
注册小程序,注册地址:https://mp.weixin.qq.com/wxopen/waregister?action=step1
完善小程序信息 登录小程序后台:https://mp.weixin.qq.com/ 完善小程序信息、小程序类目
查看小程序的 AppID
下载开发者工具,下载地址:https://developers.weixin.qq.com/miniprogram/dev/devtools/stable.html
创建小程序项目
熟悉开发者工具布局
设置不校验合法域名 注: 开发阶段,小程序发出请求到后端的Tomcat服务器,若不勾选,请求发送失败。
入门案例 小程序开发本质上属于前端开发,主要使用JavaScript进行开发,简单了解即可,后面会直接给出小程序的代码
小程序目录结构 小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。一个小程序主体部分由三个文件组成,必须放在项目的根目录,如下:
文件说明:
app.js: 必须存在,主要存放小程序的逻辑代码
app.json: 必须存在,小程序配置文件,主要存放小程序的公共配置
app.wxss: 非必须存在,主要存放小程序公共样式表,类似于前端的CSS样式
对小程序主体三个文件了解后,其实一个小程序又有多个页面。比如说,有商品浏览页面、购物车的页面、订单支付的页面、商品的详情页面等等。那这些页面会放在哪呢? 会存放在pages目录。
一个小程序会有许多个页面,比如商品浏览页面、购物车的页面、订单支付的页面、商品的详情页面等等,这些页面会存放在pages目录下,每个小程序页面主要由四个文件组成
文件说明:
js文件: 必须存在,存放页面业务逻辑代码,编写的js代码。
wxml文件: 必须存在,存放页面结构,主要是做页面布局,页面效果展示的,类似于HTML页面。
json文件: 非必须,存放页面相关的配置。
wxss文件: 非必须,存放页面样式表,相当于CSS文件。
编写和编译小程序 编写 进入到index.wxml,编写页面布局1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <view class ="container" > <view > {{msg}}</view > <view > <button type ="default" bindtap ="getUserInfo" > 获取用户信息</button > <image style ="width: 100px;height: 100px;" src ="{{avatarUrl}}" > </image > {{nickName}} </view > <view > <button type ="primary" bindtap ="wxlogin" > 微信登录</button > 授权码:{{code}} </view > <view > <button type ="warn" bindtap ="sendRequest" > 发送请求</button > 响应结果:{{result}} </view > </view >
进入到index.js,编写业务逻辑代码1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 Page ({ data :{ msg :'hello world' , avatarUrl :'' , nickName :'' , code :'' , result :'' }, getUserInfo :function ( ){ wx.getUserProfile ({ desc : '获取用户信息' , success :(res ) => { console .log (res) this .setData ({ avatarUrl :res.userInfo .avatarUrl , nickName :res.userInfo .nickName }) } }) }, wxlogin :function ( ){ wx.login ({ success : (res ) => { console .log ("授权码:" +res.code ) this .setData ({ code :res.code }) } }) }, sendRequest :function ( ){ wx.request ({ url : 'http://localhost:8080/user/shop/status' , method :'GET' , success :(res ) => { console .log ("响应结果:" + res.data .data ) this .setData ({ result :res.data .data }) } }) }})
编译小程序 点击开发者工具的编译按钮,编译小程序,就能看到运行效果了 发布小程序 想要将小程序发布上线,让所有用户都用到该小程序 点击上传按钮:
上传成功:
微信登录 导入小程序代码 在微信小程序开发者工具中导入小程序代码 输入自己的AppID 查看项目结构 修改配置 将小程序请求的后端服务url修改为自己后端服务的ip地址和端口号(默认不需要修改) 微信登录流程 官方给出了小程序微信登录的详细流程:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
流程图:
步骤分析:
小程序端,调用wx.login()获取code,就是授权码。 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。 开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。 小程序端,收到自定义登录态,存储storage。 小程序端,后绪通过wx.request()发起业务请求时,携带token。 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。 下面使用Postman来进行测试:
调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID (若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key 。 之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
实现步骤
获取授权码 每次登录小程序时,小程序会弹出提示框,点击确定按钮,即可获取授权码,每个授权码只对应一次登录,因此每次测试都需要重新获取授权码明确请求接口 请求方式、请求路径、请求参数发送请求 获取session_key和openid如果出现code been used错误提示,说明授权码已被使用过,需要重新获取需求分析和设计 产品原型 用户进入小程序后,点击授权后才能点餐,需要获取当前用户的相关信息,比如昵称、头像等。该方式基于微信登录来实现小程序的登录,若用户第一次使用小程序进行点餐,则说明是新用户,需要将新用户的信息保存到数据库中完成自动注册
接口设计 通过微信登录就需要获得微信用户的openid,在小程序端获取授权码后,向后端服务发送请求并携带授权码,这样后端服务在收到授权码后就可以去请求微信你接口服务,最终后端向小程序返回openid和token等数据
说明: 请求路径/user/user/login,第一个user代表用户端,第二个user代表用户模块。
表设计 用户第一次使用小程序时会自动注册账号,并将用户信息存储到User表。
字段名 数据类型 说明 备注 id bigint 主键 自增 openid varchar(45) 微信用户的唯一标识 name varchar(32) 用户姓名 phone varchar(11) 手机号 sex varchar(2) 性别 id_number varchar(18) 身份证号 avatar varchar(500) 微信用户头像路径 create_time datetime 注册时间
说明: 手机号字段比较特殊,个人身份注册的小程序没有权限获取到微信用户的手机号。如果是以企业的资质 注册的小程序就能够拿到微信用户的手机号。
代码开发 添加相关配置 添加微信登录所需的配置项application-dev.yml application.yml 1 2 3 4 sky: wechat: appid: wxffb3637a228223b8 secret: 84311df9199ecacdf4f12d27b6b9522d
1 2 3 4 sky: wechat: appid: ${sky.wechat.appid} secret: ${sky.wechat.secret}
在application.yml中配置为用户生成jwt令牌时需要的配置项:
1 2 3 4 5 6 7 8 9 10 11 sky: jwt: admin-secret-key: itcast admin-ttl: 7200000 admin-token-name: token user-secret-key: itheima user-ttl: 7200000 user-token-name: authentication
DTO设计 根据传入参数设计DTO类:
在sky-pojo模块中已经给出了UserLoginDTO类
1 2 3 4 5 6 7 8 9 @Data public class UserLoginDTO implements Serializable { private String code; }
VO设计 根据返回数据设计VO类:
在sky-pojo模块中已经给出了UserLoginVO类
1 2 3 4 5 6 7 8 9 10 11 @Data @Builder @NoArgsConstructor @AllArgsConstructor public class UserLoginVO implements Serializable { private Long id; private String openid; private String token; }
Controller层 根据接口定义创建UserController的login方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @RestController @RequestMapping("/user/user") @Api(tags = "C端用户相关接口") @Slf4j public class UserController { @Autowired private UserService userService; @Autowired private JwtProperties jwtProperties; @PostMapping("/login") @ApiOperation("微信登录") public Result<UserLoginVO> login (@RequestBody UserLoginDTO userLoginDTO) { log.info("微信用户登录:{}" ,userLoginDTO.getCode()); User user = userService.wxLogin(userLoginDTO); 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); } }
Service层 创建UserService接口:
1 2 3 4 5 6 7 8 9 public interface UserService { User wxLogin (UserLoginDTO userLoginDTO) ; }
创建UserServiceImpl实现类: 实现获取微信用户的openid和微信登录功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 @Service @Slf4j public class UserServiceImpl implements UserService { public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session" ; @Autowired private WeChatProperties weChatProperties; @Autowired private UserMapper userMapper; public User wxLogin (UserLoginDTO userLoginDTO) { String openid = getOpenid(userLoginDTO.getCode()); if (openid == null ){ throw new LoginFailedException (MessageConstant.LOGIN_FAILED); } User user = userMapper.getByOpenid(openid); if (user == null ){ user = User.builder() .openid(openid) .createTime(LocalDateTime.now()) .build(); userMapper.insert(user); } return user; } private String getOpenid (String code) { Map<String, String> map = new HashMap <>(); map.put("appid" ,weChatProperties.getAppid()); map.put("secret" ,weChatProperties.getSecret()); map.put("js_code" ,code); map.put("grant_type" ,"authorization_code" ); String json = HttpClientUtil.doGet(WX_LOGIN, map); JSONObject jsonObject = JSON.parseObject(json); String openid = jsonObject.getString("openid" ); return openid; } }
Mapper层 创建UserMapper接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Mapper public interface UserMapper { @Select("select * from user where openid = #{openid}") User getByOpenid (String openid) ; void insert (User user) ; }
创建UserMapper.xml映射文件:
1 2 3 4 5 6 7 8 9 10 11 <?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 >
编写拦截器 编写拦截器JwtTokenUserInterceptor: 统一拦截用户端发送的请求并进行jwt校验
注意此处handler instanceof HandlerMethod中的HandlerMethod导入的是org.springframework.web.method包下的,之前引入的org.springframework.Message.handler包下的HandlerMethod类,导致此处判断条件不成立,从而导致拦截器无法生效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Component @Slf4j public class JwtTokenUserInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; public boolean preHandle (HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true ; } String token = request.getHeader(jwtProperties.getUserTokenName()); 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); return true ; } catch (Exception ex) { response.setStatus(401 ); return false ; } } }
在WebMvcConfiguration配置类中注册拦截器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Autowired private JwtTokenUserInterceptor jwtTokenUserInterceptor; protected void addInterceptors (InterceptorRegistry registry) { log.info("开始注册自定义拦截器..." ); registry.addInterceptor(jwtTokenUserInterceptor) .addPathPatterns("/user/**" ) .excludePathPatterns("/user/user/login" ) .excludePathPatterns("/user/shop/status" ); }
功能测试 重新编译并登录,能成功获取到openid和token即可
此时查看数据库的user表,就能看到成功将第一次登录的用户信息注册到表里了
导入商品浏览功能代码 需求分析和设计 用户登录成功后跳转到系统首页,在首页需要根据分类来展示菜品和套餐。如果菜品设置了口味信息,需要展示选择规格按钮,否则显示+按钮。
菜品列表效果图 菜品口味效果图 套餐列表效果图 套餐详情效果图
接口设计
查询分类 根据分类id查询菜品 根据分类id查询套餐 根据套餐id查询包含的菜品 明确每个接口的请求方式、请求路径、传入参数和返回值。
查询分类 根据分类id查询菜品 根据分类id查询套餐 根据套餐id查询包含的菜品 代码导入 导入资料中的商品浏览功能代码即可
Mapper层 在SetmealMapper.java中添加list和getDishItemBySetmealId两个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 List<Setmeal> list (Setmeal setmeal) ; @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文件 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?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 >
Service层 SetmealService SetmealServiceImpl DishService DishServiceImpl 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public interface SetmealService { List<Setmeal> list (Setmeal setmeal) ; List<DishItemVO> getDishItemById (Long id) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Service @Slf4j public class SetmealServiceImpl implements SetmealService { @Autowired private SetmealMapper setmealMapper; @Autowired private SetmealDishMapper setmealDishMapper; @Autowired private DishMapper dishMapper; public List<Setmeal> list (Setmeal setmeal) { List<Setmeal> list = setmealMapper.list(setmeal); return list; } public List<DishItemVO> getDishItemById (Long id) { return setmealMapper.getDishItemBySetmealId(id); } }
1 2 3 4 5 6 List<DishVO> listWithFlavor (Dish dish) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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); List<DishFlavor> flavors = dishFlavorMapper.getByDishId(d.getId()); dishVO.setFlavors(flavors); dishVOList.add(dishVO); } return dishVOList; }
Controller层 DishController CategoryController SetmealController 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @RestController("userDishController") @RequestMapping("/user/dish") @Slf4j @Api(tags = "C端-菜品浏览接口") public class DishController { @Autowired private DishService dishService; @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); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @RestController("userCategoryController") @RequestMapping("/user/category") @Api(tags = "C端-分类接口") public class CategoryController { @Autowired private CategoryService categoryService; @GetMapping("/list") @ApiOperation("查询分类") public Result<List<Category>> list (Integer type) { List<Category> list = categoryService.list(type); return Result.success(list); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @RestController("userSetmealController") @RequestMapping("/user/setmeal") @Api(tags = "C端-套餐浏览接口") public class SetmealController { @Autowired private SetmealService setmealService; @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); } @GetMapping("/dish/{id}") @ApiOperation("根据套餐id查询包含的菜品列表") public Result<List<DishItemVO>> dishList (@PathVariable("id") Long id) { List<DishItemVO> list = setmealService.getDishItemById(id); return Result.success(list); } }
day07 课程内容 功能实现:缓存商品 、购物车 效果图:
缓存菜品 问题说明 用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大。
优化思路 通过Redis来缓存菜品数据,减少数据库查询操作。
缓存逻辑:
每个分类下的菜品保存一份缓存数据 数据库中菜品数据有变更时清理缓存数据
代码开发 修改用户端的DishCOntroller的list方法,在展示时加入缓存处理逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Autowired private RedisTemplate redisTemplate; @GetMapping("/list") @ApiOperation("根据分类id查询菜品") public Result<List<DishVO>> list (Long categoryId) { String key = "dish_" + categoryId; List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key); 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 中的数据保持一致,修改管理端接口DishController 的相关方法,加入清理缓存逻辑,在更新或删除菜品时同时清除缓存。
需要改造的方法:
新增菜品 修改菜品 批量删除菜品 起售、停售菜品 在管理端DishController中添加1 2 3 4 5 6 7 8 9 10 @Autowired private RedisTemplate redisTemplate; private void cleanCache (String pattern) { Set keys = redisTemplate.keys(pattern); redisTemplate.delete(keys); }
优化DishController中的相关方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @PostMapping @ApiOperation("新增菜品") public Result save (@RequestBody DishDTO dishDTO) { log.info("新增菜品:{}" , dishDTO); dishService.saveWithFlavor(dishDTO); String key = "dish_" + dishDTO.getCategoryId(); cleanCache(key); return Result.success(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @DeleteMapping @ApiOperation("菜品批量删除") public Result delete (@RequestParam List<Long> ids) { log.info("菜品批量删除:{}" , ids); dishService.deleteBatch(ids); cleanCache("dish_*" ); return Result.success(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @PutMapping @ApiOperation("修改菜品") public Result update (@RequestBody DishDTO dishDTO) { log.info("修改菜品:{}" , dishDTO); dishService.updateWithFlavor(dishDTO); cleanCache("dish_*" ); return Result.success(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @PostMapping("/status/{status}") @ApiOperation("菜品起售停售") public Result<String> startOrStop (@PathVariable Integer status, Long id) { dishService.startOrStop(status, id); cleanCache("dish_*" ); return Result.success(); }
缓存套餐 Spring Cache 介绍 Spring Cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
Spring Cache提供了一层抽象,底层可以切换不同的缓存实现,例如:
起步依赖:
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-cache</artifactId > </dependency >
常用注解 在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:
注解 说明 @EnableCaching开启缓存注解功能,通常加在启动类上 @Cacheable在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中 @CachePut将方法的返回值放到缓存中 @CacheEvict将一条或多条数据从缓存中删除
在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。
例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。
入门案例 环境准备 导入基础工程: 底层已使用Redis缓存实现
资料中提供了入门案例的基本环境代码,直接导入进来即可。导入进来的工程结构如下:
数据库准备:
创建名为spring_cache_demo数据库,将springcachedemo.sql脚本直接导入数据库中。
引导类上加@EnableCaching:
1 2 3 4 5 6 7 8 9 @Slf4j @SpringBootApplication @EnableCaching public class CacheDemoApplication { public static void main (String[] args) { SpringApplication.run(CacheDemoApplication.class,args); log.info("项目启动成功..." ); } }
@CachePut注解 @CachePut 说明:
作用: 将方法返回值,放入缓存 value: 缓存的名称, 每个缓存名称下面可以有很多key key: 缓存的key -> 支持Spring的表达式语言SPEL语法 在save方法上添加@CachePut注解
当前UserController的save方法是用来保存用户信息的,我们希望在该用户信息保存到数据库的同时,也往缓存中缓存一份数据,我们可以在save方法上加上注解 @CachePut,用法如下:
1 2 3 4 5 6 7 8 9 10 11 @PostMapping @CachePut(value = "userCache", key = "#user.id") public User save (@RequestBody User user) { userMapper.insert(user); return user; }
说明: key的写法如下
user.id : #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key ; result.id : #result代表方法返回值,该表达式 代表以返回对象的id属性作为key ; p0.id:#p0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ; a0.id:#a0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ; root.args[0].id:#root.args[0]指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数 的id属性作为key ;
启动服务,通过swagger接口文档测试,访问UserController的save()方法
因为id是自增,所以不需要设置id属性
查看user表中的数据
查看Redis中的数据
3. @Cacheable注解
@Cacheable 说明:
作用: 在方法执行前,spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
value: 缓存的名称,每个缓存名称下面可以有多个key
key: 缓存的key —————> 支持Spring的表达式语言SPEL语法
在getById上加注解@Cacheable
1 2 3 4 5 6 7 8 9 10 11 @GetMapping @Cacheable(cacheNames = "userCache",key="#id") public User getById (Long id) { User user = userMapper.getById(id); return user; }
重启服务,通过swagger接口文档测试,访问UserController的getById()方法
第一次访问,会请求我们controller的方法,查询数据库。后面再查询相同的id,就直接从Redis中查询数据,不用再查询数据库了,就说明缓存生效了。
提前在redis中手动删除掉id=1的用户数据
查看控制台sql语句: 说明从数据库查询的用户数据
查看Redis中的缓存数据: 说明已成功缓存
再次查询相同id的数据时,直接从redis中直接获取,不再查询数据库。
4). @CacheEvict注解
@CacheEvict 说明:
作用: 清理指定缓存
value: 缓存的名称,每个缓存名称下面可以有多个key
key: 缓存的key —————> 支持Spring的表达式语言SPEL语法
在 delete 方法上加注解@CacheEvict
1 2 3 4 5 6 7 8 9 10 11 @DeleteMapping @CacheEvict(cacheNames = "userCache",key = "#id") public void deleteById (Long id) { userMapper.deleteById(id); } @DeleteMapping("/delAll") @CacheEvict(cacheNames = "userCache",allEntries = true) public void deleteAll () { userMapper.deleteAll(); }
重启服务,通过swagger接口文档测试,访问UserController的deleteAll()方法
查看user表: 数据清空
查询Redis缓存数据
实现思路 实现步骤:
1). 导入Spring Cache和Redis相关maven坐标
2). 在启动类上加入@EnableCaching注解,开启缓存注解功能
3). 在用户端接口SetmealController的 list 方法上加入@Cacheable注解
4). 在管理端接口SetmealController的 save、delete、update、startOrStop等方法上加入CacheEvict注解
代码开发 按照上述实现步骤:
1). 导入Spring Cache和Redis相关maven坐标(已实现)
1 2 3 4 5 6 7 8 9 <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 >
2). 在启动类上加入@EnableCaching注解,开启缓存注解功能
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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" ); } }
3). 在用户端接口SetmealController的 list 方法上加入@Cacheable注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @GetMapping("/list") @ApiOperation("根据分类id查询套餐") @Cacheable(cacheNames = "setmealCache",key = "#categoryId") 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); }
4). 在管理端接口SetmealController的 save、delete、update、startOrStop等方法上加入CacheEvict注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 @PostMapping @ApiOperation("新增套餐") @CacheEvict(cacheNames = "setmealCache",key = "#setmealDTO.categoryId") public Result save (@RequestBody SetmealDTO setmealDTO) { setmealService.saveWithDish(setmealDTO); return Result.success(); } @DeleteMapping @ApiOperation("批量删除套餐") @CacheEvict(cacheNames = "setmealCache",allEntries = true) public Result delete (@RequestParam List<Long> ids) { setmealService.deleteBatch(ids); return Result.success(); } @PutMapping @ApiOperation("修改套餐") @CacheEvict(cacheNames = "setmealCache",allEntries = true) public Result update (@RequestBody SetmealDTO setmealDTO) { setmealService.update(setmealDTO); return Result.success(); } @PostMapping("/status/{status}") @ApiOperation("套餐起售停售") @CacheEvict(cacheNames = "setmealCache",allEntries = true) public Result startOrStop (@PathVariable Integer status, Long id) { setmealService.startOrStop(status, id); return Result.success(); }
2.4 功能测试 通过前后端联调方式来进行测试,同时观察redis中缓存的套餐数据。和缓存菜品 功能测试基本一致,不再赘述。
2.5 代码提交
后续步骤和其它功能代码提交一致,不再赘述。
添加购物车 需求分析和设计 产品原型 用户可以将菜品或者套餐添加到购物车。对于菜品来说,如果设置了口味信息,则需要选择规格后才能加入购物车;对于套餐来说,可以直接点击+将当前套餐加入购物车。在购物车中可以修改菜品和套餐的数量,也可以清空购物车。
接口设计 设计出对应的添加购物车接口。
说明: 添加购物车时,有可能添加菜品,也有可能添加套餐。故传入参数要么是菜品id,要么是套餐id。
表设计 购物车对应的数据表为shopping_cart表,具体表结构如下:
字段名 数据类型 说明 备注 id bigint 主键 自增 name varchar(32) 商品名称 冗余字段 image varchar(255) 商品图片路径 冗余字段 user_id bigint 用户id 逻辑外键 dish_id bigint 菜品id 逻辑外键 setmeal_id bigint 套餐id 逻辑外键 dish_flavor varchar(50) 菜品口味 number int 商品数量 amount decimal(10,2) 商品单价 冗余字段 create_time datetime 创建时间
说明:
购物车数据是关联用户的,在表结构中,我们需要记录,每一个用户的购物车数据是哪些 菜品列表展示出来的既有套餐,又有菜品,如果用户选择的是套餐,就保存套餐ID(setmeal_id),如果用户选择的是菜品,就保存菜品ID(dish_id) 对同一个菜品/套餐,如果选择多份不需要添加多条记录,增加数量number即可 代码开发 DTO设计 根据添加购物车接口的参数设计DTO:
在sky-pojo模块中已经提供了ShoppingCartDTO
1 2 3 4 5 6 7 8 @Data public class ShoppingCartDTO implements Serializable { private Long dishId; private Long setmealId; private String dishFlavor; }
Controller层 根据添加购物车接口创建ShoppingCartController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @RestController @RequestMapping("/user/shoppingCart") @Slf4j @Api(tags = "C端-购物车接口") public class ShoppingCartController { @Autowired private ShoppingCartService shoppingCartService; @PostMapping("/add") @ApiOperation("添加购物车") public Result<String> add (@RequestBody ShoppingCartDTO shoppingCartDTO) { log.info("添加购物车:{}" , shoppingCartDTO); shoppingCartService.addShoppingCart(shoppingCartDTO); return Result.success(); } }
Service层 创建ShoppingCartService接口:
1 2 3 4 5 6 7 8 public interface ShoppingCartService { void addShoppingCart (ShoppingCartDTO shoppingCartDTO) ; }
创建ShoppingCartServiceImpl实现类,并实现add方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 @Service public class ShoppingCartServiceImpl implements ShoppingCartService { @Autowired private ShoppingCartMapper shoppingCartMapper; @Autowired private DishMapper dishMapper; @Autowired private SetmealMapper setmealMapper; 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 ) { shoppingCart = shoppingCartList.get(0 ); shoppingCart.setNumber(shoppingCart.getNumber() + 1 ); shoppingCartMapper.updateNumberById(shoppingCart); } else { 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); } } }
Mapper层 创建ShoppingCartMapper接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Mapper public interface ShoppingCartMapper { List<ShoppingCart> list (ShoppingCart shoppingCart) ; @Update("update shopping_cart set number = #{number} where id = #{id}") void updateNumberById (ShoppingCart 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <?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 >
功能测试 在小程序端点击添加菜品,添加菜品后,因为当前还没有实现查看购物车功能,因此可以去数据库的购物车表查看新填的数据
如果java程序显示插入一条语句,但是数据库表中没有数据,可能是因为前面微信登录处拦截器包导入错误,导致拦截器不生效而没有解析出user_id,插入字段为null导致的插入失败
查看购物车 需求分析和设计 产品原型 当用户添加完菜品和套餐后,可进入到购物车中,查看购物中的菜品和套餐。
接口设计 代码开发 Controller层 在ShoppingCartController中创建查看购物车的方法:
1 2 3 4 5 6 7 8 9 @GetMapping("/list") @ApiOperation("查看购物车") public Result<List<ShoppingCart>> list () { return Result.success(shoppingCartService.showShoppingCart()); }
Service层 在ShoppingCartService接口中声明查看购物车的方法:
1 2 3 4 5 List<ShoppingCart> showShoppingCart () ;
在ShoppingCartServiceImpl中实现查看购物车的方法:
1 2 3 4 5 6 7 8 9 10 public List<ShoppingCart> showShoppingCart () { return shoppingCartMapper.list(ShoppingCart. builder(). userId(BaseContext.getCurrentId()). build()); }
清空购物车 需求分析和设计 点击清空按钮时,会把购物车中的数据全部清空。
接口设计
代码开发 Controller层 在ShoppingCartController中创建清空购物车的方法:
1 2 3 4 5 6 7 8 9 10 @DeleteMapping("/clean") @ApiOperation("清空购物车商品") public Result<String> clean () { shoppingCartService.cleanShoppingCart(); return Result.success(); }
Service层 在ShoppingCartService接口中声明清空购物车的方法:
1 2 3 4 void cleanShoppingCart () ;
在ShoppingCartServiceImpl中实现清空购物车的方法:
1 2 3 4 5 6 public void cleanShoppingCart () { shoppingCartMapper.deleteByUserId(BaseContext.getCurrentId()); }
Mapper层 在ShoppingCartMapper接口中创建删除购物车数据的方法:
1 2 3 4 5 6 7 @Delete("delete from shopping_cart where user_id = #{userId}") void deleteByUserId (Long userId) ;
功能测试 点击清空
查看数据库中的数据
说明当前用户的购物车数据已全部删除。
day08 课程内容 功能实现:用户下单 、订单支付 用户下单效果图:
导入地址簿功能代码 需求分析和设计 产品原型 地址簿,指的是消费者用户的地址信息,用户登录成功后可以维护自己的地址信息。同一个用户可以有多个地址信息,但是只能有一个默认地址 。
效果图:
对于地址簿管理,需要实现以下几个功能:
查询地址列表 新增地址 修改地址 删除地址 设置默认地址 查询默认地址 接口设计 根据上述原型图先 粗粒度 设计接口,共包含7个接口。
接口设计:
新增地址 查询登录用户所有地址 查询默认地址 根据id修改地址 根据id删除地址 根据id查询地址 设置默认地址 接下来细粒度 分析每个接口,明确每个接口的请求方式、请求路径、传入参数和返回值。
新增地址 查询登录用户所有地址 查询默认地址 修改地址 根据id删除地址 根据id查询地址 设置默认地址 表设计 用户的地址信息会存储在address_book表,即地址簿表中。具体表结构如下:
字段名 数据类型 说明 备注 id bigint 主键 自增 user_id bigint 用户id 逻辑外键 consignee varchar(50) 收货人 sex varchar(2) 性别 phone varchar(11) 手机号 province_code varchar(12) 省份编码 province_name varchar(32) 省份名称 city_code varchar(12) 城市编码 city_name varchar(32) 城市名称 district_code varchar(12) 区县编码 district_name varchar(32) 区县名称 detail varchar(200) 详细地址信息 具体到门牌号 label varchar(100) 标签 公司、家、学校 is_default tinyint(1) 是否默认地址 1是 0否
这里面有一个字段is_default,实际上在设置默认地址时,只需要更新这个字段就可以了。
代码导入 对于这一类的单表增删改查,基本的开发思路都是一样的,本小节的用户地址簿管理的增删改查功能就不再一一实现了,直接导入资料中提供的代码即可
Mapper层 创建AddressBookMapper.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 @Mapper public interface AddressBookMapper { List<AddressBook> list (AddressBook 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) ; @Select("select * from address_book where id = #{id}") AddressBook getById (Long id) ; void update (AddressBook addressBook) ; @Update("update address_book set is_default = #{isDefault} where user_id = #{userId}") void updateIsDefaultByUserId (AddressBook addressBook) ; @Delete("delete from address_book where id = #{id}") void deleteById (Long id) ; }
创建AddressBookMapper.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 <?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 >
Service层 创建AddressBookService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 @Service @Slf4j public class AddressBookServiceImpl implements AddressBookService { @Autowired private AddressBookMapper addressBookMapper; public List<AddressBook> list (AddressBook addressBook) { return addressBookMapper.list(addressBook); } public void save (AddressBook addressBook) { addressBook.setUserId(BaseContext.getCurrentId()); addressBook.setIsDefault(0 ); addressBookMapper.insert(addressBook); } public AddressBook getById (Long id) { AddressBook addressBook = addressBookMapper.getById(id); return addressBook; } public void update (AddressBook addressBook) { addressBookMapper.update(addressBook); } @Transactional public void setDefault (AddressBook addressBook) { addressBook.setIsDefault(0 ); addressBook.setUserId(BaseContext.getCurrentId()); addressBookMapper.updateIsDefaultByUserId(addressBook); addressBook.setIsDefault(1 ); addressBookMapper.update(addressBook); } public void deleteById (Long id) { addressBookMapper.deleteById(id); } }
Controller层 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 @RestController @RequestMapping("/user/addressBook") @Api(tags = "C端地址簿接口") public class AddressBookController { @Autowired private AddressBookService addressBookService; @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); } @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); } @PutMapping @ApiOperation("根据id修改地址") public Result update (@RequestBody AddressBook addressBook) { addressBookService.update(addressBook); return Result.success(); } @PutMapping("/default") @ApiOperation("设置默认地址") public Result setDefault (@RequestBody AddressBook addressBook) { addressBookService.setDefault(addressBook); return Result.success(); } @DeleteMapping @ApiOperation("根据id删除地址") public Result deleteById (Long id) { addressBookService.deleteById(id); return Result.success(); } @GetMapping("default") @ApiOperation("查询默认地址") public Result<AddressBook> getDefault () { AddressBook addressBook = new AddressBook (); addressBook.setIsDefault(1 ); addressBook.setUserId(BaseContext.getCurrentId()); List<AddressBook> list = addressBookService.list(addressBook); if (list != null && list.size() == 1 ) { return Result.success(list.get(0 )); } return Result.error("没有查询到默认地址" ); } }
功能测试 可以通过如下方式进行测试:
查看控制台sql和数据库中的数据变化 Swagger接口文档测试 前后端联调 这里直接使用前后端联调 测试:
启动后台服务,编译小程序
登录进入首页—>进入个人中心—>进入地址管理
用户下单 需求分析和设计 产品原型 用户下单业务说明: 在电商系统中,用户通过下单的方式通知商家,用户已经购买了商品,需要商家进行备货和发货。 用户下单后会产生订单相关数据,订单数据需要能够体现如下信息:
用户将菜品或者套餐加入购物车后,可以点击购物车中的 “去结算” 按钮,页面跳转到订单确认页面,点击 “去支付” 按钮则完成下单操作。
用户点餐业务流程(效果图):
接口设计 接口分析:
接口设计:
表设计 用户下单业务对应的数据表为orders表和order_detail表(一对多关系,一个订单关联多个订单明细):
表名 含义 说明 orders 订单表 主要存储订单的基本信息(如: 订单号、状态、金额、支付方式、下单用户、收件地址等) order_detail 订单明细表 主要存储订单详情信息(如: 该订单关联的套餐及菜品的信息)
具体的表结构如下:
说明: 用户提交订单时,需要往订单表orders中插入一条记录,需要往order_detail中插入一条或多条记录。
代码开发 DTO设计 根据用户下单接口的参数设计DTO:
在sky-pojo模块,OrdersSubmitDTO.java已定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Data public class OrdersSubmitDTO implements Serializable { private Long addressBookId; private int payMethod; private String remark; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime estimatedDeliveryTime; private Integer deliveryStatus; private Integer tablewareNumber; private Integer tablewareStatus; private Integer packAmount; private BigDecimal amount; }
VO设计 根据用户下单接口的返回结果设计VO:
在sky-pojo模块,OrderSubmitVO.java已定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Data @Builder @NoArgsConstructor @AllArgsConstructor public class OrderSubmitVO implements Serializable { private Long id; private String orderNumber; private BigDecimal orderAmount; private LocalDateTime orderTime; }
Controller层 创建OrderController并提供用户下单方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @RestController("userOrderController") @RequestMapping("/user/order") @Slf4j @Api(tags = "C端-订单接口") public class OrderController { @Autowired private OrderService orderService; @PostMapping("/submit") @ApiOperation("用户下单") public Result<OrderSubmitVO> submit (@RequestBody OrdersSubmitDTO ordersSubmitDTO) { log.info("用户下单:{}" , ordersSubmitDTO); OrderSubmitVO orderSubmitVO = orderService.submitOrder(ordersSubmitDTO); return Result.success(orderSubmitVO); } }
Service层 创建OrderService接口,并声明用户下单方法:
1 2 3 4 5 6 7 8 9 public interface OrderService { OrderSubmitVO submitOrder (OrdersSubmitDTO ordersSubmitDTO) ; }
创建OrderServiceImpl实现OrderService接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 @Service @Slf4j public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private OrderDetailMapper orderDetailMapper; @Autowired private ShoppingCartMapper shoppingCartMapper; @Autowired private AddressBookMapper addressBookMapper; @Transactional public OrderSubmitVO submitOrder (OrdersSubmitDTO ordersSubmitDTO) { AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId()); if (addressBook == null ) { throw new AddressBookBusinessException (MessageConstant.ADDRESS_BOOK_IS_NULL); } Long userId = BaseContext.getCurrentId(); ShoppingCart shoppingCart = new ShoppingCart (); shoppingCart.setUserId(userId); List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart); if (shoppingCartList == null || shoppingCartList.size() == 0 ) { throw new ShoppingCartBusinessException (MessageConstant.SHOPPING_CART_IS_NULL); } Orders order = new Orders (); BeanUtils.copyProperties(ordersSubmitDTO,order); order.setPhone(addressBook.getPhone()); order.setAddress(addressBook.getDetail()); order.setConsignee(addressBook.getConsignee()); order.setNumber(String.valueOf(System.currentTimeMillis())); order.setUserId(userId); order.setStatus(Orders.PENDING_PAYMENT); order.setPayStatus(Orders.UN_PAID); order.setOrderTime(LocalDateTime.now()); 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); } 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; } }
Mapper层 创建OrderMapper接口和对应的xml映射文件:
1 2 3 4 5 6 7 8 @Mapper public interface OrderMapper { void insert (Orders order) ; }
创建OrderDetailMapper接口和对应的xml映射文件:
OrderDetailMapper.java OrderDetailMapper.xml 1 2 3 4 5 6 7 8 9 10 @Mapper public interface OrderDetailMapper { void insertBatch (List<OrderDetail> orderDetails) ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?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 >
功能测试 登录小程序,完成下单操作
下单操作时,同时会删除购物车中的数据
去结算—>去支付
orders表 order_detail表 shopping_cart表 订单支付 微信支付介绍 前面已经实现了用户下单,接下来就需要对订单进行支付。在现实生活中经常购买商品并且使用支付功能来付款,在付款的时候可能使用比较多的就是微信支付和支付宝支付了。在苍穹外卖项目中,选择的就是微信支付 这种支付方式。
要实现微信支付就需要注册微信支付的一个商户号,这个商户号是必须要有一家企业并且有正规的营业执照 。只有具备了这些资质之后,才可以去注册商户号,才能开通支付权限。
微信支付产品:
本项目选择小程序支付
参考:https://pay.weixin.qq.com/static/product/product_index.shtml
微信支付接入流程:
微信小程序支付时序图:
微信支付相关接口:
JSAPI下单: 商户系统调用该接口在微信支付服务后台生成预支付交易单(对应时序图的第5步)
微信小程序调起支付: 通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法调起小程序支付(对应时序图的第10步)
3.2 微信支付准备工作 3.2.1 如何保证数据安全? 完成微信支付有两个关键的步骤:
第一个 就是需要在商户系统当中调用微信后台的一个下单接口,就是生成预支付交易单。
第二个 就是支付成功之后微信后台会给推送消息。
这两个接口数据的安全性,要求其实是非常高的。
解决: 微信提供的方式就是对数据进行加密、解密、签名多种方式。要完成数据加密解密,需要提前准备相应的一些文件,其实就是一些证书。
获取微信支付平台证书、商户私钥文件:
在后绪程序开发过程中,就会使用到这两个文件,需要提前把这两个文件准备好。
3.2.2 如何调用到商户系统? 微信后台会调用到商户系统给推送支付的结果,在这里我们就会遇到一个问题,就是微信后台怎么就能调用到我们这个商户系统呢?因为这个调用过程,其实本质上也是一个HTTP请求。
目前,商户系统它的ip地址就是当前自己电脑的ip地址,只是一个局域网内的ip地址,微信后台无法调用到。
解决: 内网穿透。通过cpolar软件 可以获得一个临时域名,而这个临时域名是一个公网ip,这样,微信后台就可以请求到商户系统了。
cpolar软件的使用:
1). 下载与安装
下载地址:https://dashboard.cpolar.com/get-started
在资料中已提供,可无需下载。
安装过程中,一直下一步即可,不再演示。
2). cpolar指定authtoken
复制authtoken:
执行命令:
3). 获取临时域名
执行命令:
获取域名:
4). 验证临时域名有效性
访问接口文档
使用localhost:8080访问
使用临时域名访问
证明临时域名生效。
3.3 代码导入 导入资料中的微信支付功能代码即可
3.3.1 微信支付相关配置 application-dev.yml
1 2 3 4 5 6 7 8 9 10 11 sky: wechat: appid: wxcd2e39f677fd30ba secret: 84fbfdf5ea288f0c432d829599083637 mchid : 1561414331 mchSerialNo: 4B3B3DC35414AD50B1B755BAF8DE9CC7CF407606 privateKeyFilePath: D:\apiclient_key.pem apiV3Key: CZBK51236435wxpay435434323FFDuv3 weChatPayCertFilePath: D:\wechatpay_166D96F876F45C7D07CE98952A96EC980368ACFC.pem notifyUrl: https://www.weixin.qq.com/wxpay/pay.php refundNotifyUrl: https://www.weixin.qq.com/wxpay/pay.php
application.yml
1 2 3 4 5 6 7 8 9 10 11 12 sky: wechat: appid: ${sky.wechat.appid} secret: ${sky.wechat.secret} mchid : ${sky.wechat.mchid} mchSerialNo: ${sky.wechat.mchSerialNo} privateKeyFilePath: ${sky.wechat.privateKeyFilePath} apiV3Key: ${sky.wechat.apiV3Key} weChatPayCertFilePath: ${sky.wechat.weChatPayCertFilePath} notifyUrl: ${sky.wechat.notifyUrl} refundNotifyUrl: ${sky.wechat.refundNotifyUrl}
WeChatProperties.java:读取配置(已定义)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.sky.properties;import lombok.Data;import org.springframework.beans.factory.annotation.Value;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.stereotype.Component;@Component @ConfigurationProperties(prefix = "sky.wechat") @Data public class WeChatProperties { private String appid; private String secret; private String mchid; private String mchSerialNo; private String privateKeyFilePath; private String apiV3Key; private String weChatPayCertFilePath; private String notifyUrl; private String refundNotifyUrl; }
3.3.2 Mapper层 在OrderMapper.java中添加getByNumberAndUserId和update两个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 @Select("select * from orders where number = #{orderNumber} and user_id= #{userId}") Orders getByNumberAndUserId (String orderNumber, Long userId) ; void update (Orders orders) ;
在OrderMapper.xml中添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <update id ="update" parameterType ="com.sky.entity.Orders" > update orders <set > <if test ="cancelReason != null and cancelReason!='' " > cancel_reason=#{cancelReason}, </if > <if test ="rejectionReason != null and rejectionReason!='' " > rejection_reason=#{rejectionReason}, </if > <if test ="cancelTime != null" > cancel_time=#{cancelTime}, </if > <if test ="payStatus != null" > pay_status=#{payStatus}, </if > <if test ="payMethod != null" > pay_method=#{payMethod}, </if > <if test ="checkoutTime != null" > checkout_time=#{checkoutTime}, </if > <if test ="status != null" > status = #{status}, </if > <if test ="deliveryTime != null" > delivery_time = #{deliveryTime} </if > </set > where id = #{id} </update >
3.3.3 Service层 在OrderService.java中添加payment和paySuccess两个方法定义
1 2 3 4 5 6 7 8 9 10 11 12 OrderPaymentVO payment (OrdersPaymentDTO ordersPaymentDTO) throws Exception; void paySuccess (String outTradeNo) ;
在OrderServiceImpl.java中实现payment和paySuccess两个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 @Autowired private UserMapper userMapper; @Autowired private WeChatPayUtil weChatPayUtil; public OrderPaymentVO payment (OrdersPaymentDTO ordersPaymentDTO) throws Exception { Long userId = BaseContext.getCurrentId(); User user = userMapper.getById(userId); JSONObject jsonObject = weChatPayUtil.pay( ordersPaymentDTO.getOrderNumber(), new BigDecimal (0.01 ), "苍穹外卖订单" , user.getOpenid() ); if (jsonObject.getString("code" ) != null && jsonObject.getString("code" ).equals("ORDERPAID" )) { throw new OrderBusinessException ("该订单已支付" ); } OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class); vo.setPackageStr(jsonObject.getString("package" )); return vo; } public void paySuccess (String outTradeNo) { Long userId = BaseContext.getCurrentId(); Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId); Orders orders = Orders.builder() .id(ordersDB.getId()) .status(Orders.TO_BE_CONFIRMED) .payStatus(Orders.PAID) .checkoutTime(LocalDateTime.now()) .build(); orderMapper.update(orders); }
3.3.4 Controller层 在OrderController.java中添加payment方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @PutMapping("/payment") @ApiOperation("订单支付") public Result<OrderPaymentVO> payment (@RequestBody OrdersPaymentDTO ordersPaymentDTO) throws Exception { log.info("订单支付:{}" , ordersPaymentDTO); OrderPaymentVO orderPaymentVO = orderService.payment(ordersPaymentDTO); log.info("生成预支付交易单:{}" , orderPaymentVO); return Result.success(orderPaymentVO); }
PayNotifyController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 package com.sky.controller.notify;import com.alibaba.druid.support.json.JSONUtils;import com.alibaba.fastjson.JSON;import com.alibaba.fastjson.JSONObject;import com.sky.annotation.IgnoreToken;import com.sky.properties.WeChatProperties;import com.sky.service.OrderService;import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;import lombok.extern.slf4j.Slf4j;import org.apache.http.entity.ContentType;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.BufferedReader;import java.nio.charset.StandardCharsets;import java.util.HashMap;@RestController @RequestMapping("/notify") @Slf4j public class PayNotifyController { @Autowired private OrderService orderService; @Autowired private WeChatProperties weChatProperties; @RequestMapping("/paySuccess") public void paySuccessNotify (HttpServletRequest request, HttpServletResponse response) throws Exception { String body = readData(request); log.info("支付成功回调:{}" , body); String plainText = decryptData(body); log.info("解密后的文本:{}" , plainText); JSONObject jsonObject = JSON.parseObject(plainText); String outTradeNo = jsonObject.getString("out_trade_no" ); String transactionId = jsonObject.getString("transaction_id" ); log.info("商户平台订单号:{}" , outTradeNo); log.info("微信支付交易号:{}" , transactionId); orderService.paySuccess(outTradeNo); responseToWeixin(response); } private String readData (HttpServletRequest request) throws Exception { BufferedReader reader = request.getReader(); StringBuilder result = new StringBuilder (); String line = null ; while ((line = reader.readLine()) != null ) { if (result.length() > 0 ) { result.append("\n" ); } result.append(line); } return result.toString(); } private String decryptData (String body) throws Exception { JSONObject resultObject = JSON.parseObject(body); JSONObject resource = resultObject.getJSONObject("resource" ); String ciphertext = resource.getString("ciphertext" ); String nonce = resource.getString("nonce" ); String associatedData = resource.getString("associated_data" ); AesUtil aesUtil = new AesUtil (weChatProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8)); String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8), nonce.getBytes(StandardCharsets.UTF_8), ciphertext); return plainText; } private void responseToWeixin (HttpServletResponse response) throws Exception{ response.setStatus(200 ); HashMap<Object, Object> map = new HashMap <>(); map.put("code" , "SUCCESS" ); map.put("message" , "SUCCESS" ); response.setHeader("Content-type" , ContentType.APPLICATION_JSON.toString()); response.getOutputStream().write(JSONUtils.toJSONString(map).getBytes(StandardCharsets.UTF_8)); response.flushBuffer(); } }
3.4 功能测试 测试过程中,可通过断点方式查看后台每一步执行情况。
下单:
去支付:
确认支付:
进行扫码支付即可。
3.5 代码提交
后续步骤和其它功能代码提交一致,不再赘述。
day09 用户端历史订单模块 查询历史订单 需求分析和设计 业务规则
分页查询历史订单 可以根据订单状态查询 展示订单数据时,需要展示的数据包括:下单时间、订单状态、订单金额、订单明细(商品名称、图片) 接口设计:详见接口文档
代码实现 Controller层 在OrderController中添加page方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @GetMapping("/historyOrders") @ApiOperation("历史订单查询") public Result<PageResult> page (int page, int pageSize, Integer status) { PageResult pageResult = orderService.pageQuery4User(page, pageSize, status); return Result.success(pageResult); }
Service层 在OrderService中添加以下方法:1 2 3 4 5 6 7 8 PageResult pageQuery4User (int page, int pageSize, Integer status) ;
在OrderServiceImpl中实现该方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 public PageResult pageQuery4User (int pageNum, int pageSize, Integer status) { PageHelper.startPage(pageNum, pageSize); OrdersPageQueryDTO ordersPageQueryDTO = new OrdersPageQueryDTO (); ordersPageQueryDTO.setUserId(BaseContext.getCurrentId()); ordersPageQueryDTO.setStatus(status); Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO); List<OrderVO> list = new ArrayList (); if (page != null && page.getTotal() > 0 ) { for (Orders orders : page) { Long orderId = orders.getId(); List<OrderDetail> orderDetails = orderDetailMapper.getByOrderId(orderId); OrderVO orderVO = new OrderVO (); BeanUtils.copyProperties(orders, orderVO); orderVO.setOrderDetailList(orderDetails); list.add(orderVO); } } return new PageResult (page.getTotal(), list); }
Mapper层
在OrderMapper中添加方法
1 2 3 4 5 Page<Orders> pageQuery (OrdersPageQueryDTO ordersPageQueryDTO) ;
在映射文件中添加SQL语句
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <select id ="pageQuery" resultType ="Orders" > select * from orders <where > <if test ="number != null and number!=''" > and number like concat('%',#{number},'%') </if > <if test ="phone != null and phone!=''" > and phone like concat('%',#{phone},'%') </if > <if test ="userId != null" > and user_id = #{userId} </if > <if test ="status != null" > and status = #{status} </if > <if test ="beginTime != null" > and order_time > = #{beginTime} </if > <if test ="endTime != null" > and order_time < = #{endTime} </if > </where > order by order_time desc </select >
在OrderDetailMapper中添加方法:
1 2 3 4 5 6 7 @Select("select * from order_detail where order_id = #{orderId}") List<OrderDetail> getByOrderId (Long orderId) ;
查询订单详情 需求分析和设计 接口设计:参见接口文档
代码实现 controller层 在OrderController中添加方法
1 2 3 4 5 6 7 8 9 10 11 12 @GetMapping("/orderDetail/{id}") @ApiOperation("查询订单详情") public Result<OrderVO> details (@PathVariable("id") Long id) { OrderVO orderVO = orderService.details(id); return Result.success(orderVO); }
service层 在OrderService中添加方法1 2 3 4 5 6 OrderVO details (Long id) ;
在OrderServiceImpl中实现该方法1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public OrderVO details (Long id) { Orders orders = orderMapper.getById(id); List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId()); OrderVO orderVO = new OrderVO (); BeanUtils.copyProperties(orders, orderVO); orderVO.setOrderDetailList(orderDetailList); return orderVO; }
Mapper层 在OrderMapper中添加方法1 2 3 4 5 6 @Select("select * from orders where id=#{id}") Orders getById (Long id) ;
取消订单 需求分析和设计 业务规则:
待支付和待接单状态下,用户可直接取消订单 商家已接单状态下,用户取消订单需电话沟通商家 派送中状态下,用户取消订单需电话沟通商家 如果在待接单状态下取消订单,需要给用户退款 取消订单后需要将订单状态修改为“已取消” 接口设计:参见接口文档
代码实现 Controller层 在OrderController中添加方法
1 2 3 4 5 6 7 8 9 10 11 @PutMapping("/cancel/{id}") @ApiOperation("取消订单") public Result cancel (@PathVariable("id") Long id) throws Exception { orderService.userCancelById(id); return Result.success(); }
Service层
在OrderService中添加方法
1 2 3 4 5 void userCancelById (Long id) throws Exception;
在OrderServiceImpl中实现该方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 public void userCancelById (Long id) throws Exception { Orders ordersDB = orderMapper.getById(id); if (ordersDB == null ) { throw new OrderBusinessException (MessageConstant.ORDER_NOT_FOUND); } if (ordersDB.getStatus() > 2 ) { throw new OrderBusinessException (MessageConstant.ORDER_STATUS_ERROR); } Orders orders = new Orders (); orders.setId(ordersDB.getId()); if (ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) { orders.setPayStatus(Orders.REFUND); } orders.setStatus(Orders.CANCELLED); orders.setCancelReason("用户取消" ); orders.setCancelTime(LocalDateTime.now()); orderMapper.update(orders); }
再来一单 需求分析和设计 在历史订单详情页面,点击再来一单后会将订单数据添加到购物车中,并跳转到购物车页面。
接口设计:参见接口文档
代码实现 Controller层 在OrderController中添加方法
1 2 3 4 5 6 7 8 9 10 11 12 @PostMapping("/repetition/{id}") @ApiOperation("再来一单") public Result repetition (@PathVariable Long id) { orderService.repetition(id); return Result.success(); }
Service层 在OrderService中添加方法1 2 3 4 5 6 void repetition (Long id) ;
在OrderServiceImpl中实现方法1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 public void repetition (Long id) { Long userId = BaseContext.getCurrentId(); List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id); List<ShoppingCart> shoppingCartList = orderDetailList.stream().map(x -> { ShoppingCart shoppingCart = new ShoppingCart (); BeanUtils.copyProperties(x, shoppingCart, "id" ); shoppingCart.setUserId(userId); shoppingCart.setCreateTime(LocalDateTime.now()); return shoppingCart; }).collect(Collectors.toList()); shoppingCartMapper.insertBatch(shoppingCartList); }
Mapper层 在ShoppingCartMapper中添加方法1 2 3 4 5 6 void insertBatch (List<ShoppingCart> shoppingCartList) ;
在ShoppingCartMapper的映射文件中添加对应的SQL语句1 2 3 4 5 6 7 8 <insert id ="insertBatch" parameterType ="list" > insert into shopping_cart (name, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time) values <foreach collection ="shoppingCartList" item ="sc" separator ="," > (#{sc.name},#{sc.image},#{sc.userId},#{sc.dishId},#{sc.setmealId},#{sc.dishFlavor},#{sc.number},#{sc.amount},#{sc.createTime}) </foreach > </insert >
商家端订单管理模块 订单搜索 需求分析和设计 业务规则:
输入订单号/手机号进行搜索,支持模糊搜索 根据订单状态进行筛选 下单时间进行时间筛选 搜索内容为空,提示未找到相关订单 搜索结果页,展示包含搜索关键词的内容 分页展示搜索到的订单数据 接口设计:参见接口文档
代码实现 Controller层 在admin包下创建OrderController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @RestController("adminOrderController") @RequestMapping("/admin/order") @Slf4j @Api(tags = "订单管理接口") public class OrderController { @Autowired private OrderService orderService; @GetMapping("/conditionSearch") @ApiOperation("订单搜索") public Result<PageResult> conditionSearch (OrdersPageQueryDTO ordersPageQueryDTO) { PageResult pageResult = orderService.conditionSearch(ordersPageQueryDTO); return Result.success(pageResult); } }
Service层 在OrderService中添加以下方法1 2 3 4 5 6 PageResult conditionSearch (OrdersPageQueryDTO ordersPageQueryDTO) ;
在OrderServiceImpl中实现方法1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 public PageResult conditionSearch (OrdersPageQueryDTO ordersPageQueryDTO) { PageHelper.startPage(ordersPageQueryDTO.getPage(), ordersPageQueryDTO.getPageSize()); Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO); List<OrderVO> orderVOList = getOrderVOList(page); return new PageResult (page.getTotal(), orderVOList); } private List<OrderVO> getOrderVOList (Page<Orders> page) { List<OrderVO> orderVOList = new ArrayList <>(); List<Orders> ordersList = page.getResult(); if (!CollectionUtils.isEmpty(ordersList)) { for (Orders orders : ordersList) { OrderVO orderVO = new OrderVO (); BeanUtils.copyProperties(orders, orderVO); String orderDishes = getOrderDishesStr(orders); orderVO.setOrderDishes(orderDishes); orderVOList.add(orderVO); } } return orderVOList; } private String getOrderDishesStr (Orders orders) { List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId()); List<String> orderDishList = orderDetailList.stream().map(x -> { String orderDish = x.getName() + "*" + x.getNumber() + ";" ; return orderDish; }).collect(Collectors.toList()); return String.join("" , orderDishList); }
各个状态的订单数量统计 需求分析和设计 接口设计:参见接口文档
代码实现 Controller层 在admin包下的OrderController类中添加方法
1 2 3 4 5 6 7 8 9 10 11 @GetMapping("/statistics") @ApiOperation("各个状态的订单数量统计") public Result<OrderStatisticsVO> statistics () { OrderStatisticsVO orderStatisticsVO = orderService.statistics(); return Result.success(orderStatisticsVO); }
Service层
在OrderService中添加以下方法
1 2 3 4 5 OrderStatisticsVO statistics () ;
在OrderServiceImpl中实现方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public OrderStatisticsVO statistics () { Integer toBeConfirmed = orderMapper.countStatus(Orders.TO_BE_CONFIRMED); Integer confirmed = orderMapper.countStatus(Orders.CONFIRMED); Integer deliveryInProgress = orderMapper.countStatus(Orders.DELIVERY_IN_PROGRESS); OrderStatisticsVO orderStatisticsVO = new OrderStatisticsVO (); orderStatisticsVO.setToBeConfirmed(toBeConfirmed); orderStatisticsVO.setConfirmed(confirmed); orderStatisticsVO.setDeliveryInProgress(deliveryInProgress); return orderStatisticsVO; }
Mapper层
在OrderMapper中添加以下方法
1 2 3 4 5 6 @Select("select count(id) from orders where status = #{status}") Integer countStatus (Integer status) ;
查询订单详情 需求分析和设计 产品原型:
业务规则:
订单详情页面需要展示订单基本信息(状态、订单号、下单时间、收货人、电话、收货地址、金额等) 订单详情页面需要展示订单明细数据(商品名称、数量、单价) 接口设计:参见接口文档
代码实现 在OrderController中添加方法
1 2 3 4 5 6 7 8 9 10 11 12 @GetMapping("/details/{id}") @ApiOperation("查询订单详情") public Result<OrderVO> details (@PathVariable("id") Long id) { OrderVO orderVO = orderService.details(id); return Result.success(orderVO); }
Service层 在OrderService中添加方法
1 2 3 4 5 6 OrderVO details (Long id) ;
在OrderServiceImpl中实现方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public OrderVO details (Long id) { Orders orders = orderMapper.getById(id); List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId()); OrderVO orderVO = new OrderVO (); BeanUtils.copyProperties(orders, orderVO); orderVO.setOrderDetailList(orderDetailList); return orderVO; }
Mapper层 在OrderMapper中添加方法1 2 3 4 5 6 @Select("select * from orders where id=#{id}") Orders getById (Long id) ;
接单 需求分析和设计 接口设计:参见接口文档
代码实现 Controller层 在OrderController中添加方法
1 2 3 4 5 6 7 8 9 10 11 @PutMapping("/confirm") @ApiOperation("接单") public Result confirm (@RequestBody OrdersConfirmDTO ordersConfirmDTO) { orderService.confirm(ordersConfirmDTO); return Result.success(); }
Service层 在OrderService中添加方法
1 2 3 4 5 6 void confirm (OrdersConfirmDTO ordersConfirmDTO) ;
在OrderServiceImpl中实现方法
1 2 3 4 5 6 7 8 9 10 11 12 13 public void confirm (OrdersConfirmDTO ordersConfirmDTO) { Orders orders = Orders.builder() .id(ordersConfirmDTO.getId()) .status(Orders.CONFIRMED) .build(); orderMapper.update(orders); }
拒单 需求分析和设计 业务规则:
商家拒单其实就是将订单状态修改为“已取消” 只有订单处于“待接单”状态时可以执行拒单操作 商家拒单时需要指定拒单原因 商家拒单时,如果用户已经完成了支付,需要为用户退款 接口设计:参见接口文档
代码实现 Controller层 在DishController中创建方法
1 2 3 4 5 6 7 8 9 10 11 @PutMapping("/rejection") @ApiOperation("拒单") public Result rejection (@RequestBody OrdersRejectionDTO ordersRejectionDTO) throws Exception { orderService.rejection(ordersRejectionDTO); return Result.success(); }
Service层
1 2 3 4 5 6 void rejection (OrdersRejectionDTO ordersRejectionDTO) throws Exception;
在OrderServiceImpl中实现方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public void rejection (OrdersRejectionDTO ordersRejectionDTO) throws Exception { Orders ordersDB = orderMapper.getById(ordersRejectionDTO.getId()); if (ordersDB == null || !ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) { throw new OrderBusinessException (MessageConstant.ORDER_STATUS_ERROR); } Integer payStatus = ordersDB.getPayStatus(); Orders orders = new Orders (); if (payStatus == Orders.PAID) { orders.setId(ordersDB.getId()); orders.setStatus(Orders.CANCELLED); orders.setRejectionReason(ordersRejectionDTO.getRejectionReason()); orders.setCancelTime(LocalDateTime.now()); } orderMapper.update(orders); }
取消订单 需求分析和设计 产品原型:
业务规则:
取消订单其实就是将订单状态修改为“已取消” 商家取消订单时需要指定取消原因 商家取消订单时,如果用户已经完成了支付,需要为用户退款 接口设计:参见接口文档
代码实现 Controller层
1 2 3 4 5 6 7 8 9 10 11 @PutMapping("/cancel") @ApiOperation("取消订单") public Result cancel (@RequestBody OrdersCancelDTO ordersCancelDTO) throws Exception { orderService.cancel(ordersCancelDTO); return Result.success(); }
Service层 在OrderService中添加方法
1 2 3 4 5 6 void cancel (OrdersCancelDTO ordersCancelDTO) throws Exception;
在OrderServiceImpl中实现方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 public void cancel (OrdersCancelDTO ordersCancelDTO) throws Exception { Orders ordersDB = orderMapper.getById(ordersCancelDTO.getId()); Orders orders = new Orders (); Integer payStatus = ordersDB.getPayStatus(); if (payStatus == 1 ) { orders.setId(ordersCancelDTO.getId()); orders.setStatus(Orders.CANCELLED); orders.setCancelReason(ordersCancelDTO.getCancelReason()); orders.setCancelTime(LocalDateTime.now()); } orderMapper.update(orders); }
派送订单 需求分析和设计 产品原型:
业务规则:
派送订单其实就是将订单状态修改为“派送中” 只有状态为“待派送”的订单可以执行派送订单操作 接口设计:参见接口文档
代码实现 Controller层 在OrderController中添加方法
1 2 3 4 5 6 7 8 9 10 11 @PutMapping("/delivery/{id}") @ApiOperation("派送订单") public Result delivery (@PathVariable("id") Long id) { orderService.delivery(id); return Result.success(); }
Service层 在OrderService中添加方法
1 2 3 4 5 6 void delivery (Long id) ;
在OrderServiceImpl中实现方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void delivery (Long id) { Orders ordersDB = orderMapper.getById(id); if (ordersDB == null || !ordersDB.getStatus().equals(Orders.CONFIRMED)) { throw new OrderBusinessException (MessageConstant.ORDER_STATUS_ERROR); } Orders orders = new Orders (); orders.setId(ordersDB.getId()); orders.setStatus(Orders.DELIVERY_IN_PROGRESS); orderMapper.update(orders); }
完成订单 需求分析和设计 业务规则:
完成订单其实就是将订单状态修改为“已完成” 只有状态为“派送中”的订单可以执行订单完成操作 接口设计:参见接口文档
代码实现 Controller层 在OrderController中添加方法
1 2 3 4 5 6 7 8 9 10 11 @PutMapping("/complete/{id}") @ApiOperation("完成订单") public Result complete (@PathVariable("id") Long id) { orderService.complete(id); return Result.success(); }
Service层 在OrderService中添加方法1 2 3 4 5 6 void complete (Long id) ;
在OrderServiceImpl中实现方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public void complete (Long id) { Orders ordersDB = orderMapper.getById(id); if (ordersDB == null || !ordersDB.getStatus().equals(Orders.DELIVERY_IN_PROGRESS)) { throw new OrderBusinessException (MessageConstant.ORDER_STATUS_ERROR); } Orders orders = new Orders (); orders.setId(ordersDB.getId()); orders.setStatus(Orders.COMPLETED); orders.setDeliveryTime(LocalDateTime.now()); orderMapper.update(orders); }
校验收货地址是否超出配送范围 1. 环境准备 注册账号:https://passport.baidu.com/v2/?reg&tt=1671699340600&overseas=&gid=CF954C2-A3D2-417F-9FE6-B0F249ED7E33&tpl=pp&u=https%3A%2F%2Flbsyun.baidu.com%2Findex.php%3Ftitle%3D%E9%A6%96%E9%A1%B5
登录百度地图开放平台:https://lbsyun.baidu.com/
进入控制台,创建应用,获取AK:
相关接口:
https://lbsyun.baidu.com/index.php?title=webapi/guide/webservice-geocoding
https://lbsyun.baidu.com/index.php?title=webapi/directionlite-v1
2. 代码开发 2.1 application.yml 配置外卖商家店铺地址和百度地图的AK:
2.2 OrderServiceImpl 改造OrderServiceImpl,注入上面的配置项:
1 2 3 4 5 @Value("${sky.shop.address}") private String shopAddress;@Value("${sky.baidu.ak}") private String ak;
在OrderServiceImpl中提供校验方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 private void checkOutOfRange (String address) { Map map = new HashMap (); map.put("address" ,shopAddress); map.put("output" ,"json" ); map.put("ak" ,ak); String shopCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3" , map); JSONObject jsonObject = JSON.parseObject(shopCoordinate); if (!jsonObject.getString("status" ).equals("0" )){ throw new OrderBusinessException ("店铺地址解析失败" ); } JSONObject location = jsonObject.getJSONObject("result" ).getJSONObject("location" ); String lat = location.getString("lat" ); String lng = location.getString("lng" ); String shopLngLat = lat + "," + lng; map.put("address" ,address); String userCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3" , map); jsonObject = JSON.parseObject(userCoordinate); if (!jsonObject.getString("status" ).equals("0" )){ throw new OrderBusinessException ("收货地址解析失败" ); } location = jsonObject.getJSONObject("result" ).getJSONObject("location" ); lat = location.getString("lat" ); lng = location.getString("lng" ); String userLngLat = lat + "," + lng; map.put("origin" ,shopLngLat); map.put("destination" ,userLngLat); map.put("steps_info" ,"0" ); String json = HttpClientUtil.doGet("https://api.map.baidu.com/directionlite/v1/driving" , map); jsonObject = JSON.parseObject(json); if (!jsonObject.getString("status" ).equals("0" )){ throw new OrderBusinessException ("配送路线规划失败" ); } JSONObject result = jsonObject.getJSONObject("result" ); JSONArray jsonArray = (JSONArray) result.get("routes" ); Integer distance = (Integer) ((JSONObject) jsonArray.get(0 )).get("distance" ); if (distance > 5000 ){ throw new OrderBusinessException ("超出配送范围" ); } }
在OrderServiceImpl的submitOrder方法中调用上面的校验方法:
day10 课程内容 Spring Task 订单状态定时处理 WebSocket 来单提醒 客户催单 功能实现:订单状态定时处理 、来单提醒 和客户催单
订单状态定时处理:
来单提醒
客户催单:
Spring Task 介绍 Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。
定位: 定时任务框架作用: 定时自动执行某段Java代码
Java程序中使用SpringTask的几个应用场景:
信用卡每月还款提醒 银行贷款每月还款提醒 火车票售票系统处理未支付订单 入职纪念日为用户发送通知
只要是任何需要定时处理的场景都可以使用SpringTask
cron表达式 cron表达式 其实就是一个字符串,通过cron表达式可以定义任务触发的时间
构成规则: 分为6或7个域,由空格分隔开,每个域代表一个含义
每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)
举例:
2022年10月12日上午9点整 对应的cron表达式为:0 0 9 12 10 ? 2022
说明: 一般日 和周 的值不同时设置,其中一个设置,另一个用?表示。
比如: 描述2月份的最后一天,最后一天具体是几号呢?可能是28号,也有可能是29号,此时就不能写具体数字。可以通过一些特殊字符来描述这些信息。这些具体的细节,我们就不用自己去手写,因为cron表达式有许多在线生成器。
cron表达式在线生成器:https://cron.qqe2.com/
直接输入自己的需求就能够生成相应的cron表达式,因此一般不用自己去写cron表达式,简单了解即可。
通配符:
\* 表示所有值;
? 表示未说明的值,即不关心它为何值;
- 表示一个指定的范围;
, 表示附加一个可能值;
/ 符号前表示开始时间,符号后表示每次递增的值;
cron表达式案例:
/5 * ? 每隔5秒执行一次
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使用步骤 导入maven坐标 spring-context(已存在) 启动类添加注解 @EnableScheduling 开启任务调度
自定义定时任务类
代码开发 编写定时任务类:
进入sky-server模块中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Component @Slf4j public class MyTask { @Scheduled(cron = "0/5 * * * * ?") public void executeTask () { log.info("定时任务开始执行:{}" ,new Date ()); } }
开启任务调度:
在启动类上添加@EnableScheduling注解
1 2 3 4 5 6 7 8 9 10 11 @SpringBootApplication @EnableTransactionManagement @Slf4j @EnableCaching @EnableScheduling public class SkyApplication { public static void main (String[] args) { SpringApplication.run(SkyApplication.class, args); log.info("server started" ); } }
功能测试 启动服务,查看日志
任务每隔5秒执行一次。
订单状态定时处理 需求分析 用户下单后可能存在的情况:
下单后未支付,订单一直处于待支付状态 用户收货后管理端未点击完成按钮,订单一直处于派送中状态 上面两种情况都需要通过定时任务 来修改订单状态,具体逻辑为:
通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为已取消 通过定时任务每天凌晨1点检查一次是否存在派送中的订单,如果存在则修改订单状态为已完成 代码开发 自定义定时任务类OrderTask(待完善): 新建Task包,并创建定时任务类OrderTask1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Component @Slf4j public class OrderTask { @Autowired private OrderMapper orderMapper; @Scheduled(cron = "0 * * * * ?") public void processTimeoutOrder () { log.info("处理支付超时订单:{}" , new Date ()); } @Scheduled(cron = "0 0 1 * * ?") public void processDeliveryOrder () { log.info("处理派送中订单:{}" , new Date ()); } }
在OrderMapper接口中扩展方法: 1 2 3 4 5 6 7 @Select("select * from orders where status = #{status} and order_time < #{orderTime}") List<Orders> getByStatusAndOrdertimeLT (Integer status, LocalDateTime orderTime) ;
完善定时任务类的processTimeoutOrder方法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Scheduled(cron = "0 * * * * ?") public void processTimeoutOrder () { log.info("处理支付超时订单:{}" , new Date ()); LocalDateTime time = LocalDateTime.now().plusMinutes(-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); }); } }
完善定时任务类的processDeliveryOrder方法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Scheduled(cron = "0 0 1 * * ?") public void processDeliveryOrder () { log.info("处理派送中订单:{}" , new Date ()); 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); }); } }
功能测试 可以通过如下方式进行测试:
支付超时的订单测试:
1. 查看订单表
有一条订单,状态为1。订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
2. 开启定时任务
启动服务,观察控制台日志。处理支付超时订单任务每隔1分钟执行一次。
3. 再次查看订单表
状态已更改为6,已取消。
证明定时任务已生效。
处理“派送中”状态的订单任务 的测试步骤和上述一致。可适当修改cron表达式,改变任务执行频率,方便测试。
WebSocket 介绍 WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性 的连接, 并进行双向 数据传输。
HTTP协议和WebSocket协议对比:
HTTP是短连接 WebSocket是长连接 HTTP通信是单向 的,基于请求响应模式 WebSocket支持双向 通信 HTTP和WebSocket底层都是TCP连接 思考: 既然WebSocket支持双向通信,功能看似比HTTP强大,那么我们是不是可以基于WebSocket开发所有的业务功能?
WebSocket缺点:
服务器长期维护长连接需要一定的成本 各个浏览器支持程度不一 WebSocket 是长连接,受网络限制比较大,需要处理好重连 结论: WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用
WebSocket应用场景:
入门案例 案例分析 需求: 实现浏览器与服务器全双工通信。浏览器既可以向服务器发送消息,服务器也可主动向浏览器推送消息。
效果展示:
实现步骤:
直接使用websocket.html页面作为WebSocket客户端
导入WebSocket的maven坐标
导入WebSocket服务端组件WebSocketServer,用于和客户端通信
导入配置类WebSocketConfiguration,注册WebSocket的服务端组件
导入定时任务类WebSocketTask,定时向客户端推送数据
代码开发 定义websocket.html页面(资料中已提供) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 <!DOCTYPE HTML > <html > <head > <meta charset ="UTF-8" > <title > WebSocket Demo</title > </head > <body > <input id ="text" type ="text" /> <button onclick ="send()" > 发送消息</button > <button onclick ="closeWebSocket()" > 关闭连接</button > <div id ="message" > </div > </body > <script type ="text/javascript" > var websocket = null ; var clientId = Math .random ().toString (36 ).substr (2 ); if ('WebSocket' in window ){ websocket = new WebSocket ("ws://localhost:8080/ws/" +clientId); } else { alert ('Not support websocket' ) } websocket.onerror = function ( ){ setMessageInnerHTML ("error" ); }; websocket.onopen = function ( ){ setMessageInnerHTML ("连接成功" ); } websocket.onmessage = function (event ){ setMessageInnerHTML (event.data ); } websocket.onclose = function ( ){ setMessageInnerHTML ("close" ); } window .onbeforeunload = function ( ){ websocket.close (); } function setMessageInnerHTML (innerHTML ){ document .getElementById ('message' ).innerHTML += innerHTML + '<br/>' ; } function send ( ){ var message = document .getElementById ('text' ).value ; websocket.send (message); } function closeWebSocket ( ) { websocket.close (); } </script > </html >
导入maven坐标 在sky-server模块pom.xml中已定义
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-websocket</artifactId > </dependency >
定义WebSocket服务端组件(资料中已提供) 直接导入到sky-server模块即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 @Component @ServerEndpoint("/ws/{sid}") public class WebSocketServer { private static Map<String, Session> sessionMap = new HashMap (); @OnOpen public void onOpen (Session session, @PathParam("sid") String sid) { System.out.println("客户端:" + sid + "建立连接" ); sessionMap.put(sid, session); } @OnMessage public void onMessage (String message, @PathParam("sid") String sid) { System.out.println("收到来自客户端:" + sid + "的信息:" + message); } @OnClose public void onClose (@PathParam("sid") String sid) { System.out.println("连接断开:" + sid); sessionMap.remove(sid); } public void sendToAllClient (String message) { Collection<Session> sessions = sessionMap.values(); for (Session session : sessions) { try { session.getBasicRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } } }
定义配置类,注册WebSocket的服务端组件(从资料中直接导入即可) 1 2 3 4 5 6 7 8 9 10 11 12 @Configuration public class WebSocketConfiguration { @Bean public ServerEndpointExporter serverEndpointExporter () { return new ServerEndpointExporter (); } }
定义定时任务类,定时向客户端推送数据(从资料中直接导入即可) 1 2 3 4 5 6 7 8 9 10 11 12 13 @Component public class WebSocketTask { @Autowired private WebSocketServer webSocketServer; @Scheduled(cron = "0/5 * * * * ?") public void sendMessageToClient () { webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss" ).format(LocalDateTime.now())); } }
功能测试 启动服务,打开websocket.html页面
浏览器向服务器发送数据:
服务器向浏览器间隔5秒推送数据:
来单提醒 需求分析和设计 用户下单并且支付成功后,需要第一时间通知外卖商家。通知的形式有如下两种:
设计思路:
通过WebSocket实现管理端页面和服务端保持长连接状态 当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,contenttype 为消息类型,1为来单提醒 2为客户催单 orderId 为订单id content 为消息内容 代码开发 在OrderServiceImpl中注入WebSocketServer对象,修改paySuccess方法,加入如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Autowired private WebSocketServer webSocketServer; public void paySuccess (String outTradeNo) { Long userId = BaseContext.getCurrentId(); Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId); 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 ); map.put("orderId" , orders.getId()); map.put("content" , "订单号:" + outTradeNo); webSocketServer.sendToAllClient(JSON.toJSONString(map)); }
功能测试 可以通过如下方式进行测试:
1. 登录管理端后台
登录成功后,浏览器与服务器建立长连接
查看控制台日志
2. 小程序端下单支付
修改回调地址,利用内网穿透获取域名
下单支付
3. 查看来单提醒
支付成功后,后台收到来单提醒,并有语音播报
客户催单 需求分析和设计 用户在小程序中点击催单按钮后,需要第一时间通知外卖商家。通知的形式有如下两种:
设计思路:
通过WebSocket实现管理端页面和服务端保持长连接状态 当用户点击催单按钮后,调用WebSocket的相关API实现服务端向客户端推送消息 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,contenttype 为消息类型,1为来单提醒 2为客户催单 orderId 为订单id content 为消息内容 当用户点击催单按钮时,向服务端发送请求。
接口设计(催单):
代码开发 Controller层 根据用户催单的接口定义,在user包下的OrderController中创建催单方法:
1 2 3 4 5 6 7 8 9 10 11 12 @GetMapping("/reminder/{id}") @ApiOperation("用户催单") public Result reminder (@PathVariable("id") Long id) { orderService.reminder(id); return Result.success(); }
Service层 在OrderService接口中声明reminder方法:
1 2 3 4 5 void reminder (Long id) ;
在OrderServiceImpl中实现reminder方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void reminder (Long id) { Orders orders = orderMapper.getById(id); if (orders == null ) { throw new OrderBusinessException (MessageConstant.ORDER_NOT_FOUND); } Map map = new HashMap (); map.put("type" , 2 ); map.put("orderId" , id); map.put("content" , "订单号:" + orders.getNumber()); webSocketServer.sendToAllClient(JSON.toJSONString(map)); }
Mapper层 在OrderMapper中添加getById:
1 2 3 4 5 6 @Select("select * from orders where id=#{id}") Orders getById (Long id) ;
功能测试 可以通过如下方式进行测试:
1. 登录管理端后台
登录成功后,浏览器与服务器建立长连接
查看控制台日志
2. 用户进行催单
用户可在订单列表或者订单详情,进行催单
3. 查看催单提醒
既有催单弹窗,同时语音播报
day11 课程内容 Apache ECharts 营业额统计 用户统计 订单统计 销量排名Top10 功能实现:数据统计
数据统计效果图:
Apache ECharts 介绍 Apache ECharts 是一款基于 Javascript 的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。 官网地址:https://echarts.apache.org/zh/index.html
常见效果展示:
总结: 不管是哪种形式的图形,最本质的东西实际上是数据,这只是对数据的一种可视化展示。
入门案例 Apache Echarts官方提供的快速入门:https://echarts.apache.org/handbook/zh/get-started/
效果展示:
实现步骤:
引入echarts.js 文件(当天资料已提供)
为 ECharts 准备一个设置宽高的 DOM
初始化echarts实例
指定图表的配置项和数据
使用指定的配置项和数据显示图表
代码开发:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <!DOCTYPE html > <html > <head > <meta charset ="utf-8" /> <title > ECharts</title > <script src ="echarts.js" > </script > </head > <body > <div id ="main" style ="width: 600px;height:400px;" > </div > <script type ="text/javascript" > var myChart = echarts.init (document .getElementById ('main' )); var option = { title : { text : 'ECharts 入门示例' }, tooltip : {}, legend : { data : ['销量' ] }, xAxis : { data : ['衬衫' , '羊毛衫' , '雪纺衫' , '裤子' , '高跟鞋' , '袜子' ] }, yAxis : {}, series : [ { name : '销量' , type : 'bar' , data : [5 , 20 , 36 , 10 , 10 , 20 ] } ] }; myChart.setOption (option); </script > </body > </html >
测试: (当天资料中已提供)
使用浏览器方式打开即可。
总结: 使用Echarts,重点在于研究当前图表所需的数据格式。通常是需要后端提供符合格式要求的动态数据,然后响应给前端来展示图表。
营业额统计 需求分析和设计 产品原型 营业额统计是基于折现图来展现,并且按照天来展示的。实际上,就是某一个时间范围之内的每一天的营业额。同时,不管光标放在哪个点上,那么它就会把具体的数值展示出来。并且还需要注意日期并不是固定写死的,是由上边时间选择器来决定。比如选择是近7天、或者是近30日,或者是本周,就会把相应这个时间段之内的每一天日期通过横坐标展示。
原型图:
业务规则:
营业额指订单状态为已完成的订单金额合计 基于可视化报表的折线图展示营业额数据,X轴为日期,Y轴为营业额 根据时间选择区间,展示每天的营业额数据 接口设计 通过上述原型图,设计出对应的接口。
注意: 具体返回数据一般由前端来决定,前端展示图表,具体折现图对应数据是什么格式,是有固定的要求的。所以说,后端需要去适应前端,它需要什么格式的数据,我们就给它返回什么格式的数据。
代码开发 VO设计 根据接口定义设计对应的VO:
在sky-pojo模块,TurnoverReportVO.java已定义
1 2 3 4 5 6 7 8 9 10 11 12 13 @Data @Builder @NoArgsConstructor @AllArgsConstructor public class TurnoverReportVO implements Serializable { private String dateList; private String turnoverList; }
Controller层 根据接口定义创建ReportController:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @RestController @RequestMapping("/admin/report") @Slf4j @Api(tags = "统计报表相关接口") public class ReportController { @Autowired private ReportService reportService; @GetMapping("/turnoverStatistics") @ApiOperation("营业额数据统计") public Result<TurnoverReportVO> turnoverStatistics ( @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) { return Result.success(reportService.getTurnover(begin, end)); } }
Service层 创建ReportService接口,声明getTurnover方法:
1 2 3 4 5 6 7 8 9 10 public interface ReportService { TurnoverReportVO getTurnover (LocalDate beginTime, LocalDate endTime) ; }
创建ReportServiceImpl实现类,实现getTurnover方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @Service @Slf4j public class ReportServiceImpl implements ReportService { @Autowired private OrderMapper orderMapper; public TurnoverReportVO getTurnover (LocalDate begin, LocalDate end) { List<LocalDate> dateList = new ArrayList <>(); dateList.add(begin); while (!begin.equals(end)){ begin = begin.plusDays(1 ); dateList.add(begin); } List<Double> turnoverList = new ArrayList <>(); for (LocalDate date : dateList) { LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX); Map map = new HashMap (); map.put("status" , Orders.COMPLETED); map.put("begin" ,beginTime); map.put("end" , endTime); Double turnover = orderMapper.sumByMap(map); turnover = turnover == null ? 0.0 : turnover; turnoverList.add(turnover); } return TurnoverReportVO.builder() .dateList(StringUtils.join(dateList,"," )) .turnoverList(StringUtils.join(turnoverList,"," )) .build(); } }
Mapper层 在OrderMapper接口声明sumByMap方法:
1 2 3 4 5 Double sumByMap (Map map) ;
在OrderMapper.xml文件中编写动态SQL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <select id ="sumByMap" resultType ="java.lang.Double" > select sum(amount) from orders <where > <if test ="status != null" > and status = #{status} </if > <if test ="begin != null" > and order_time > = #{begin} </if > <if test ="end != null" > and order_time < = #{end} </if > </where > </select >
功能测试 可以通过如下方式进行测试:
启动服务器,启动nginx,直接采用前后端联调测试。
进入数据统计模块
1. 查看近7日营业额统计
进入开发者模式,查看返回数据
2. 查看近30日营业额统计
进入开发者模式,查看返回数据
也可通过断点方式启动,查看每步执行情况。
用户统计 需求分析和设计 产品原型 所谓用户统计,实际上统计的是用户的数量。通过折线图来展示,上面这根蓝色线代表的是用户总量,下边这根绿色线代表的是新增用户数量,是具体到每一天。所以说用户统计主要统计两个数据 ,一个是总的用户数量 ,另外一个是新增用户数量 。
原型图:
业务规则:
基于可视化报表的折线图展示用户数据,X轴为日期,Y轴为用户数 根据时间选择区间,展示每天的用户总量和新增用户量数据 接口设计 根据上述原型图设计接口。
代码开发 VO设计 根据用户统计接口的返回结果设计VO:
在sky-pojo模块,UserReportVO.java已定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Data @Builder @NoArgsConstructor @AllArgsConstructor public class UserReportVO implements Serializable { private String dateList; private String totalUserList; private String newUserList; }
Controller层 根据接口定义,在ReportController中创建userStatistics方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @GetMapping("/userStatistics") @ApiOperation("用户数据统计") public Result<UserReportVO> userStatistics ( @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) { return Result.success(reportService.getUserStatistics(begin,end)); }
Service层 在ReportService接口中声明getUserStatistics方法:
1 2 3 4 5 6 7 UserReportVO getUserStatistics (LocalDate begin, LocalDate end) ;
在ReportServiceImpl实现类中实现getUserStatistics方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 @Override public UserReportVO getUserStatistics (LocalDate begin, LocalDate end) { List<LocalDate> dateList = new ArrayList <>(); dateList.add(begin); while (!begin.equals(end)){ begin = begin.plusDays(1 ); dateList.add(begin); } List<Integer> newUserList = new ArrayList <>(); List<Integer> totalUserList = new ArrayList <>(); for (LocalDate date : dateList) { LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX); Integer newUser = getUserCount(beginTime, endTime); Integer totalUser = getUserCount(null , endTime); newUserList.add(newUser); totalUserList.add(totalUser); } return UserReportVO.builder() .dateList(StringUtils.join(dateList,"," )) .newUserList(StringUtils.join(newUserList,"," )) .totalUserList(StringUtils.join(totalUserList,"," )) .build(); }
在ReportServiceImpl实现类中创建私有方法getUserCount:
1 2 3 4 5 6 7 8 9 10 11 12 private Integer getUserCount (LocalDateTime beginTime, LocalDateTime endTime) { Map map = new HashMap (); map.put("begin" ,beginTime); map.put("end" , endTime); return userMapper.countByMap(map); }
Mapper层 在UserMapper接口中声明countByMap方法:
1 2 3 4 5 6 Integer countByMap (Map map) ;
在UserMapper.xml文件中编写动态SQL:
1 2 3 4 5 6 7 8 9 10 11 <select id="countByMap" resultType="java.lang.Integer" > select count (id) from user <where> <if test="begin != null" > and create_time >= #{begin} </if > <if test="end != null" > and create_time <= #{end} </if > </where> </select>
功能测试 可以通过如下方式进行测试:
进入数据统计模块
1. 查看近7日用户统计
进入开发者模式,查看返回数据
2. 查看近30日用户统计
进入开发者模式,查看返回数据
也可通过断点方式启动,查看每步执行情况。
订单统计 需求分析和设计 产品原型 订单统计通过一个折现图来展现,折线图上有两根线,这根蓝色的线代表的是订单总数,而下边这根绿色的线代表的是有效订单数,指的就是状态是已完成的订单就属于有效订单,分别反映的是每一天的数据。上面还有3个数字,分别是订单总数、有效订单、订单完成率,它指的是整个时间区间之内总的数据。
原型图:
业务规则:
有效订单指状态为 “已完成” 的订单 基于可视化报表的折线图展示订单数据,X轴为日期,Y轴为订单数量 根据时间选择区间,展示每天的订单总数和有效订单数 展示所选时间区间内的有效订单数、总订单数、订单完成率,订单完成率 = 有效订单数 / 总订单数 * 100% 接口设计 根据上述原型图设计接口。
代码开发 VO设计 根据订单统计接口的返回结果设计VO:
在sky-pojo模块,OrderReportVO.java已定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Data @Builder @NoArgsConstructor @AllArgsConstructor public class OrderReportVO implements Serializable { private String dateList; private String orderCountList; private String validOrderCountList; private Integer totalOrderCount; private Integer validOrderCount; private Double orderCompletionRate; }
Controller层 在ReportController中根据订单统计接口创建orderStatistics方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @GetMapping("/ordersStatistics") @ApiOperation("用户数据统计") public Result<OrderReportVO> orderStatistics ( @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) { return Result.success(reportService.getOrderStatistics(begin,end)); }
Service层 在ReportService接口中声明getOrderStatistics方法:
1 2 3 4 5 6 7 OrderReportVO getOrderStatistics (LocalDate begin, LocalDate end) ;
在ReportServiceImpl实现类中实现getOrderStatistics方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public OrderReportVO getOrderStatistics (LocalDate begin, LocalDate end) { List<LocalDate> dateList = new ArrayList <>(); dateList.add(begin); while (!begin.equals(end)){ begin = begin.plusDays(1 ); dateList.add(begin); } List<Integer> orderCountList = new ArrayList <>(); List<Integer> validOrderCountList = new ArrayList <>(); for (LocalDate date : dateList) { LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX); Integer orderCount = getOrderCount(beginTime, endTime, null ); Integer validOrderCount = getOrderCount(beginTime, endTime, Orders.COMPLETED); orderCountList.add(orderCount); validOrderCountList.add(validOrderCount); } Integer totalOrderCount = orderCountList.stream().reduce(Integer::sum).get(); Integer validOrderCount = validOrderCountList.stream().reduce(Integer::sum).get(); Double orderCompletionRate = 0.0 ; if (totalOrderCount != 0 ){ orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount; } return OrderReportVO.builder() .dateList(StringUtils.join(dateList, "," )) .orderCountList(StringUtils.join(orderCountList, "," )) .validOrderCountList(StringUtils.join(validOrderCountList, "," )) .totalOrderCount(totalOrderCount) .validOrderCount(validOrderCount) .orderCompletionRate(orderCompletionRate) .build(); }
在ReportServiceImpl实现类中提供私有方法getOrderCount:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private Integer getOrderCount (LocalDateTime beginTime, LocalDateTime endTime, Integer status) { Map map = new HashMap (); map.put("status" , status); map.put("begin" ,beginTime); map.put("end" , endTime); return orderMapper.countByMap(map); }
Mapper层 在OrderMapper接口中声明countByMap方法:
1 2 3 4 5 Integer countByMap (Map map) ;
在OrderMapper.xml文件中编写动态SQL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <select id="countByMap" resultType="java.lang.Integer" > select count (id) from orders <where> <if test="status != null" > and status = #{status} </if > <if test="begin != null" > and order_time >= #{begin} </if > <if test="end != null" > and order_time <= #{end} </if > </where> </select>
功能测试 可以通过如下方式进行测试:
重启服务,直接采用前后端联调测试。
进入数据统计模块
1). 查看近7日订单统计
进入开发者模式,查看返回数据
2). 查看近30日订单统计
进入开发者模式,查看返回数据
也可通过断点方式启动,查看每步执行情况。
销量排名Top10 需求分析和设计 产品原型 所谓销量排名,销量指的是商品销售的数量。项目当中的商品主要包含两类:一个是套餐 ,一个是菜品 ,所以销量排名其实指的就是菜品和套餐销售的数量排名。通过柱形图来展示销量排名,这些销量是按照降序来排列,并且只需要统计销量排名前十的商品。
原型图:
业务规则:
根据时间选择区间,展示销量前10的商品(包括菜品和套餐) 基于可视化报表的柱状图降序展示商品销量 此处的销量为商品销售的份数 接口设计 根据上述原型图设计接口。
代码开发 VO设计 根据销量排名接口的返回结果设计VO:
在sky-pojo模块,SalesTop10ReportVO.java已定义
1 2 3 4 5 6 7 8 9 10 11 12 13 @Data @Builder @NoArgsConstructor @AllArgsConstructor public class SalesTop10ReportVO implements Serializable { private String nameList; private String numberList; }
Controller层 在ReportController中根据销量排名接口创建top10方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 @GetMapping("/top10") @ApiOperation("销量排名统计") public Result<SalesTop10ReportVO> top10 ( @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) { return Result.success(reportService.getSalesTop10(begin,end)); }
5.2.3 Service层接口 在ReportService接口中声明getSalesTop10方法:
1 2 3 4 5 6 7 SalesTop10ReportVO getSalesTop10 (LocalDate begin, LocalDate end) ;
Service层实现类 在ReportServiceImpl实现类中实现getSalesTop10方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public SalesTop10ReportVO getSalesTop10 (LocalDate begin, LocalDate end) { LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN); LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX); List<GoodsSalesDTO> goodsSalesDTOList = orderMapper.getSalesTop10(beginTime, endTime); String nameList = StringUtils.join(goodsSalesDTOList.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList()),"," ); String numberList = StringUtils.join(goodsSalesDTOList.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList()),"," ); return SalesTop10ReportVO.builder() .nameList(nameList) .numberList(numberList) .build(); }
Mapper层 在OrderMapper接口中声明getSalesTop10方法:
1 2 3 4 5 6 List<GoodsSalesDTO> getSalesTop10 (LocalDateTime begin, LocalDateTime end) ;
在OrderMapper.xml文件中编写动态SQL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <select id ="getSalesTop10" resultType ="com.sky.dto.GoodsSalesDTO" > select od.name name,sum(od.number) number from order_detail od ,orders o where od.order_id = o.id and o.status = 5 <if test ="begin != null" > and order_time > = #{begin} </if > <if test ="end != null" > and order_time < = #{end} </if > group by name order by number desc limit 0, 10 </select >
功能测试 可以通过如下方式进行测试:
重启服务,直接采用前后端联调测试。
查看近30日销量排名Top10统计
若查询的某一段时间没有销量数据,则显示不出效果。
进入开发者模式,查看返回数据
也可通过断点方式启动,查看每步执行情况。
day12 课程内容 工作台 Apache POI 导出运营数据Excel报表 功能实现:工作台 、数据导出
工作台效果图:
数据导出效果图:
在数据统计页面点击数据导出 :生成Excel报表
工作台 需求分析和设计 产品原型 工作台是系统运营的数据看板,并提供快捷操作入口,可以有效提高商家的工作效率。
工作台展示的数据:
原型图:
名词解释:
营业额:已完成订单的总金额 有效订单:已完成订单的数量 订单完成率:有效订单数 / 总订单数 * 100% 平均客单价:营业额 / 有效订单数 新增用户:新增用户的数量 接口设计 通过上述原型图分析,共包含6个接口。
接口设计:
今日数据接口 订单管理接口 菜品总览接口 套餐总览接口 订单搜索(已完成) 各个状态的订单数量统计(已完成) 代码导入 直接导入课程资料中的工作台模块功能代码即可:
Controller层 添加WorkSpaceController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 @RestController @RequestMapping("/admin/workspace") @Slf4j @Api(tags = "工作台相关接口") public class WorkSpaceController { @Autowired private WorkspaceService workspaceService; @GetMapping("/businessData") @ApiOperation("工作台今日数据查询") public Result<BusinessDataVO> businessData () { LocalDateTime begin = LocalDateTime.now().with(LocalTime.MIN); LocalDateTime end = LocalDateTime.now().with(LocalTime.MAX); BusinessDataVO businessDataVO = workspaceService.getBusinessData(begin, end); return Result.success(businessDataVO); } @GetMapping("/overviewOrders") @ApiOperation("查询订单管理数据") public Result<OrderOverViewVO> orderOverView () { return Result.success(workspaceService.getOrderOverView()); } @GetMapping("/overviewDishes") @ApiOperation("查询菜品总览") public Result<DishOverViewVO> dishOverView () { return Result.success(workspaceService.getDishOverView()); } @GetMapping("/overviewSetmeals") @ApiOperation("查询套餐总览") public Result<SetmealOverViewVO> setmealOverView () { return Result.success(workspaceService.getSetmealOverView()); } }
Service层 添加WorkspaceService.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 public interface WorkspaceService { BusinessDataVO getBusinessData (LocalDateTime begin, LocalDateTime end) ; OrderOverViewVO getOrderOverView () ; DishOverViewVO getDishOverView () ; SetmealOverViewVO getSetmealOverView () ; }
添加WorkspaceServiceImpl.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 @Service @Slf4j public class WorkspaceServiceImpl implements WorkspaceService { @Autowired private OrderMapper orderMapper; @Autowired private UserMapper userMapper; @Autowired private DishMapper dishMapper; @Autowired private SetmealMapper setmealMapper; public BusinessDataVO getBusinessData (LocalDateTime begin, LocalDateTime end) { Map map = new HashMap (); map.put("begin" ,begin); map.put("end" ,end); Integer totalOrderCount = orderMapper.countByMap(map); map.put("status" , Orders.COMPLETED); Double turnover = orderMapper.sumByMap(map); turnover = turnover == null ? 0.0 : turnover; Integer validOrderCount = orderMapper.countByMap(map); Double unitPrice = 0.0 ; Double orderCompletionRate = 0.0 ; if (totalOrderCount != 0 && validOrderCount != 0 ){ orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount; unitPrice = turnover / validOrderCount; } Integer newUsers = userMapper.countByMap(map); return BusinessDataVO.builder() .turnover(turnover) .validOrderCount(validOrderCount) .orderCompletionRate(orderCompletionRate) .unitPrice(unitPrice) .newUsers(newUsers) .build(); } public OrderOverViewVO getOrderOverView () { Map map = new HashMap (); map.put("begin" , LocalDateTime.now().with(LocalTime.MIN)); map.put("status" , Orders.TO_BE_CONFIRMED); Integer waitingOrders = orderMapper.countByMap(map); map.put("status" , Orders.CONFIRMED); Integer deliveredOrders = orderMapper.countByMap(map); map.put("status" , Orders.COMPLETED); Integer completedOrders = orderMapper.countByMap(map); map.put("status" , Orders.CANCELLED); Integer cancelledOrders = orderMapper.countByMap(map); map.put("status" , null ); Integer allOrders = orderMapper.countByMap(map); return OrderOverViewVO.builder() .waitingOrders(waitingOrders) .deliveredOrders(deliveredOrders) .completedOrders(completedOrders) .cancelledOrders(cancelledOrders) .allOrders(allOrders) .build(); } public DishOverViewVO getDishOverView () { Map map = new HashMap (); map.put("status" , StatusConstant.ENABLE); Integer sold = dishMapper.countByMap(map); map.put("status" , StatusConstant.DISABLE); Integer discontinued = dishMapper.countByMap(map); return DishOverViewVO.builder() .sold(sold) .discontinued(discontinued) .build(); } public SetmealOverViewVO getSetmealOverView () { Map map = new HashMap (); map.put("status" , StatusConstant.ENABLE); Integer sold = setmealMapper.countByMap(map); map.put("status" , StatusConstant.DISABLE); Integer discontinued = setmealMapper.countByMap(map); return SetmealOverViewVO.builder() .sold(sold) .discontinued(discontinued) .build(); } }
Mapper层 在SetmealMapper中添加countByMap方法定义
1 2 3 4 5 6 Integer countByMap (Map map) ;
在SetmealMapper.xml中添加对应SQL实现
1 2 3 4 5 6 7 8 9 10 11 <select id ="countByMap" resultType ="java.lang.Integer" > select count(id) from setmeal <where > <if test ="status != null" > and status = #{status} </if > <if test ="categoryId != null" > and category_id = #{categoryId} </if > </where > </select >
在DishMapper中添加countByMap方法定义
1 2 3 4 5 6 Integer countByMap (Map map) ;
在DishMapper.xml中添加对应SQL实现
1 2 3 4 5 6 7 8 9 10 11 <select id ="countByMap" resultType ="java.lang.Integer" > select count(id) from dish <where > <if test ="status != null" > and status = #{status} </if > <if test ="categoryId != null" > and category_id = #{categoryId} </if > </where > </select >
功能测试 可以通过如下方式进行测试:
接口文档测试 启动服务 ,访问http://localhost:8080/doc.html ,进入工作台相关接口
今日数据查询 菜品总览查询 订单管理数据查询 套餐总览查询 前后端联调测试 启动nginx ,访问 http://localhost ,进入工作台
进入开发者模式,分别查看今日数据、订单管理、菜品总览、套餐总览
今日数据查询 菜品总览查询 订单管理数据查询 套餐总览查询 Apache POI 介绍 Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,可以使用 POI 在 Java 程序中对Miscrosoft Office各种文件进行读写操作。 一般情况下,POI 都是用于操作 Excel 文件。
Apache POI 的应用场景:
银行网银系统导出交易明细 各种业务系统导出Excel报表 批量导入业务数据 入门案例 Apache POI既可以将数据写入Excel文件,也可以读取Excel文件中的数据,接下来分别进行实现。
Apache POI的maven坐标: (项目中已导入)
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.apache.poi</groupId > <artifactId > poi</artifactId > <version > 3.16</version > </dependency > <dependency > <groupId > org.apache.poi</groupId > <artifactId > poi-ooxml</artifactId > <version > 3.16</version > </dependency >
将数据写入Excel文件 1. 代码开发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class POITest { public static void write () throws Exception{ XSSFWorkbook excel = new XSSFWorkbook (); XSSFSheet sheet = excel.createSheet("itcast" ); XSSFRow row1 = sheet.createRow(0 ); row1.createCell(1 ).setCellValue("姓名" ); row1.createCell(2 ).setCellValue("城市" ); XSSFRow row2 = sheet.createRow(1 ); row2.createCell(1 ).setCellValue("张三" ); row2.createCell(2 ).setCellValue("北京" ); XSSFRow row3 = sheet.createRow(2 ); row3.createCell(1 ).setCellValue("李四" ); row3.createCell(2 ).setCellValue("上海" ); FileOutputStream out = new FileOutputStream (new File ("D:\\itcast.xlsx" )); excel.write(out); out.flush(); out.close(); excel.close(); } public static void main (String[] args) throws Exception { write(); } }
2. 实现效果
在D盘中生成itcast.xlsx文件,创建名称为itcast的Sheet页,同时将内容成功写入。
读取Excel文件中的数据 1. 代码开发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public class POITest { public static void read () throws Exception{ FileInputStream in = new FileInputStream (new File ("D:\\itcast.xlsx" )); XSSFWorkbook excel = new XSSFWorkbook (in); XSSFSheet sheet = excel.getSheetAt(0 ); int lastRowNum = sheet.getLastRowNum(); for (int i = 0 ; i <= lastRowNum; i++) { XSSFRow titleRow = sheet.getRow(i); XSSFCell cell1 = titleRow.getCell(1 ); String cellValue1 = cell1.getStringCellValue(); XSSFCell cell2 = titleRow.getCell(2 ); String cellValue2 = cell2.getStringCellValue(); System.out.println(cellValue1 + " " +cellValue2); } in.close(); excel.close(); } public static void main (String[] args) throws Exception { read(); } }
2. 实现效果
将itcast.xlsx文件中的数据进行读取
导出运营数据Excel报表 需求分析和设计 产品原型 在数据统计页面,有一个数据导出的按钮,点击该按钮时,其实就会下载一个文件。这个文件实际上是一个Excel形式的文件,文件中主要包含最近30日运营相关的数据。表格的形式已经固定,主要由概览数据和明细数据两部分组成。真正导出这个报表之后,相对应的数字就会填充在表格中,就可以进行存档。
原型图:
导出的Excel报表格式:
业务规则:
导出Excel形式的报表文件 导出最近30天的运营数据 接口设计 通过上述原型图设计对应的接口。
注意:
当前接口没有传递参数,因为导出的是最近30天的运营数据,后端计算即可,所以不需要任何参数 当前接口没有返回数据,因为报表导出功能本质上是文件下载,服务端会通过输出流将Excel文件下载到客户端浏览器 代码开发 实现步骤 设计Excel模板文件
查询近30天的运营数据
将查询到的运营数据写入模板文件
通过输出流将Excel文件下载到客户端浏览器
Controller层 根据接口定义,在ReportController中创建export方法:
1 2 3 4 5 6 7 8 9 @GetMapping("/export") @ApiOperation("导出运营数据报表") public void export (HttpServletResponse response) { reportService.exportBusinessData(response); }
Service层 在ReportService接口中声明导出运营数据报表的方法:
1 2 3 4 5 void exportBusinessData (HttpServletResponse response) ;
在ReportServiceImpl实现类中实现导出运营数据报表的方法:
提前将资料中的运营数据报表模板.xlsx 拷贝到项目的resources/template目录中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public void exportBusinessData (HttpServletResponse response) { LocalDate begin = LocalDate.now().minusDays(30 ); LocalDate end = LocalDate.now().minusDays(1 ); BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(begin,LocalTime.MIN), LocalDateTime.of(end, LocalTime.MAX)); InputStream inputStream = this .getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx" ); try { XSSFWorkbook excel = new XSSFWorkbook (inputStream); XSSFSheet sheet = excel.getSheet("Sheet1" ); sheet.getRow(1 ).getCell(1 ).setCellValue(begin + "至" + end); XSSFRow row = sheet.getRow(3 ); row.getCell(2 ).setCellValue(businessData.getTurnover()); row.getCell(4 ).setCellValue(businessData.getOrderCompletionRate()); row.getCell(6 ).setCellValue(businessData.getNewUsers()); row = sheet.getRow(4 ); row.getCell(2 ).setCellValue(businessData.getValidOrderCount()); row.getCell(4 ).setCellValue(businessData.getUnitPrice()); for (int i = 0 ; i < 30 ; i++) { LocalDate date = begin.plusDays(i); businessData = workspaceService.getBusinessData(LocalDateTime.of(date,LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX)); row = sheet.getRow(7 + i); row.getCell(1 ).setCellValue(date.toString()); row.getCell(2 ).setCellValue(businessData.getTurnover()); row.getCell(3 ).setCellValue(businessData.getValidOrderCount()); row.getCell(4 ).setCellValue(businessData.getOrderCompletionRate()); row.getCell(5 ).setCellValue(businessData.getUnitPrice()); row.getCell(6 ).setCellValue(businessData.getNewUsers()); } ServletOutputStream out = response.getOutputStream(); excel.write(out); out.flush(); out.close(); excel.close(); }catch (IOException e){ e.printStackTrace(); } }
功能测试 直接使用前后端联调测试。
进入数据统计
点击数据导出 :Excel报表下载成功