项目 苍穹外卖 苍穹外卖 定西 2023-08-30 2024-06-08 2023年8月25日 16点04分
这应该是写在简历上的项目了,待好好对待啊
1 项目概述&环境搭建 技术选型
前端环境搭建 ?好像也没搭建啥,直接打开资料,双击nginx就ok了
后端环境搭建 这后端代码已经写好一大部分了啊
sky-take-out
:maven父工程,统一管理依赖版本,聚合其他子模块
sky-common
:子模块,存放公共类,例如:工具类,常量类,异常类等
sky-pojo
:子模块,存放实体类,VO,DTO等
sky-server
:子模块,后端服务,存放配置文件,Controller,Service,Mapper等
sky-pojo
模块介绍
Entity
:实体,通常和数据库中的表对应
DTO
:数据传输对象,通常用于程序中各层之间传递数据
VO
:视图对象,为前端展示数据提供的对象
pojo
:普通Java对象,只有属性和对应的getter和etter
使用Git进行版本控制
害,这比网,推了半天都推不到github上
推上去了,仓库地址
数据库环境搭建 这个比较好哎,有一个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 25 26 27 package com.sky.handler;import com.sky.exception.BaseException;import com.sky.result.Result;import lombok.extern.slf4j.Slf4j;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.RestControllerAdvice;@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { @ExceptionHandler public Result exceptionHandler (BaseException ex) { log.error("异常信息:{}" , ex.getMessage()); return Result.error(ex.getMessage()); } }
在用户登录接口中,会抛出一个异常:
1 2 3 4 if (employee == null ) { throw new AccountNotFoundException (MessageConstant.ACCOUNT_NOT_FOUND); }
这里如果出现异常,就会被 GlobalExceptionHandler
捕获到,抛出的是AccountNotFoundException,能被Global捕获是因为该异常继承自BaseException
1 2 public class AccountNotFoundException extends BaseException {}
下面用到了Builder,可以用来创建对象,特点是方法名即为属性名,使用前提是实体类上加上 @Builder
注解
1 2 3 4 5 6 EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder() .id(employee.getId()) .userName(employee.getUsername()) .name(employee.getName()) .token(token) .build();
Nginx反向代理与负载均衡 反向代理
刚看了Nginx的配置文件,原来是用了反向代理
前端请求地址:http://localhost/api/employee/login
后端接口地址:http://localhost:8080/admin/employee/login
所谓反向代理,就是将前端发送的动态请求由nginx转发到后端服务器
使用nginx的好处:
提高访问速度
nginx会对请求地址进行缓存,相同的请求不必再发送至后端那后端数据变了怎么办?
进行负载均衡
所谓负载均衡,就是把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器
保证后端服务安全
该项目中的反向代理:
1 2 3 4 5 6 7 8 9 server { listen 80; server_name localhost; # 反向代理,处理管理端发送的请求 location /api/ { proxy_pass http://localhost:8080/admin/; } }
就相当于,前端请求的 http://localhost/api
,会被转发到 http://localhost:8080/admin/
,后面的 employee/login
是不变的
负载均衡
1 2 3 4 5 6 7 8 9 10 11 12 13 14 upstream webservers{ server 127.0.0.1:8080 weight=90 ; #server 127.0.0.1:8088 weight=10 ; } server{ listen 80; server_name localhost; location /api/{ proxy_pass http://webservers/amdin/; # 负载均衡 } }
请求过来时,localhost:80/api/xxxx
会被转发到 http://webservers/amdin/xxx
,而这个 webservers
就是配置中的多个 server
,nginx会对这些server进行分配,server
后面加上 weight
表示权重,这是一种负载均衡的策略
完善登录功能 现在的问题:密码明文存储
将密码加密后存储,提高安全性
使用MD5加密方式对明文密码加密
123456加密后为:e10adc3949ba59abbe56e057f20f883e
依稀记得之前还自己写md5加密的代码,原来spring自带啊,一行代码就完成了
1 2 password = DigestUtils.md5DigestAsHex(password.getBytes());
md5DigestAsHex()函数就是将字符串(的bytes)进行md5加密
Swagger 在这之前还引入了yapi,不过暂时用不到
这一小节可以放到我的开发工具箱里
使用Swagger只需要按照它的规范去定义接口及接口相关的信息,就可以做到生成接口文档,以及在线接口调试 页面,Knife4j
是为Java MVC框架集成Swagger生成Api文档的增强解决方案
使用方式
导入依赖
1 2 3 4 5 <dependency > <groupId > com.github.xiaoymin</groupId > <artifactId > knife4j-spring-boot-starter</artifactId > <version > 3.0.2</version > </dependency >
在配置类中加入 knife4j
相关配置(某个带有 @Configuration
的配置类中即可)
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; }
设置静态资源映射,否则接口文档页面无法访问(要放在某个继承自 WebMvcConfigurationSupport
类中),这个方法其实就是重写的父类的方法
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/" ); }
顺便把这个配置类也放着:
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 package com.sky.config;import com.sky.interceptor.JwtTokenAdminInterceptor;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.servlet.config.annotation.InterceptorRegistry;import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;import springfox.documentation.builders.ApiInfoBuilder;import springfox.documentation.builders.PathSelectors;import springfox.documentation.builders.RequestHandlerSelectors;import springfox.documentation.service.ApiInfo;import springfox.documentation.spi.DocumentationType;import springfox.documentation.spring.web.plugins.Docket;@Configuration @Slf4j public class WebMvcConfiguration extends WebMvcConfigurationSupport { @Autowired private JwtTokenAdminInterceptor jwtTokenAdminInterceptor; protected void addInterceptors (InterceptorRegistry registry) { log.info("开始注册自定义拦截器..." ); registry.addInterceptor(jwtTokenAdminInterceptor) .addPathPatterns("/admin/**" ) .excludePathPatterns("/admin/employee/login" ); } @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; } protected void addResourceHandlers (ResourceHandlerRegistry registry) { registry.addResourceHandler("/doc.html" ).addResourceLocations("classpath:/META-INF/resources/" ); registry.addResourceHandler("/webjars/**" ).addResourceLocations("classpath:/META-INF/resources/webjars/" ); } }
这就配置好了,启动项目进入浏览器访问 localhost:8080/doc.html
,即可进入接口文档页面
Swagger和yapi的区别
yapi是设计阶段 使用的工具,管理和维护接口
Swagger是在开发阶段 使用的框架,帮助后端开发人员做后端的接口测试
Swagger常用注解
具体使用案例
@Api(tags = "员工相关接口")
,给Controller上加,在页面上表示一级目录,**tags=**不能省略,否则无效,
1 2 3 @Api(tags = "员工相关接口") public class EmployeeController {}
@ApiOperation(value = "员工登录接口")
,给接口上加,表示具体接口的名称
1 2 3 @ApiOperation(value = "员工登录接口") public Result<EmployeeLoginVO> login (@RequestBody EmployeeLoginDTO employeeLoginDTO) {}
@ApiModel
和 @ApiModelProperty()
,给实体类和属性上加
1 2 3 4 5 6 7 8 9 10 @ApiModel(description = "员工登录时传递的数据模型") public class EmployeeLoginDTO implements Serializable { @ApiModelProperty("用户名") private String username; @ApiModelProperty("密码") private String password; }
2 员工管理 新增员工 产品原型:
这里又说了一下 DTO
的用处,我们的实体类是跟数据库进行映射的,而 DTO
,是用来封装前端接收的信息的,这个 EmployeeDTO
,就是用来封装新增员工信息的,我们的实体类中的属性要比 DTO
多一些(上面那个登录的DTO,也是和登录信息有关的)
代码开发
Controller,调用service的save方法,然后返回成功
1 2 3 4 5 6 7 8 @PostMapping @ApiOperation("新增员工") public Result save (@RequestBody EmployeeDTO employeeDTO) { log.info("新增员工信息:{}" , employeeDTO); employeeService.save(employeeDTO); return Result.success(); }
Service
就像下面说的,虽然接收前端的数据的是DTO,但是和数据库进行交互还是用实体类,所以这里涉及到类型转换,用 BeanUtils.copyProperties(source,target)
能够很方便的copy对象,前提是属性名一致
密码是默认的,为 123456
,存入数据库的要进行md5加密
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 @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.setCreateUser(10L ); employeeMapper.save(employee); }
Mapper,这个就没什么说的了.
1 2 3 4 5 6 7 8 9 10 @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 save (Employee employee) ;
明天见
代码完善
目前存在的问题
录入的用户名存在,抛出异常后没有处理
新增员工时,创建人id和修改人id设置为了固定值
解决第一个问题,使用异常处理
1 2 3 4 5 6 7 8 9 10 11 12 13 @ExceptionHandler public Result exceptionHandler (SQLIntegrityConstraintViolationException ex) { String message = ex.getMessage(); if (message.contains("Duplicate entry" )) { String[] messageArr = message.split(" " ); String repeatName = messageArr[2 ]; return Result.error(repeatName + MessageConstant.ALREADY_EXISTS); } else { return Result.error(MessageConstant.UNKNOWN_ERROR); } }
由于 用户名存在
导致sql执行时的唯一性的问题,会报错:Duplicate entry 'admin' for key 'employee.idx_username
,所以我们可以根据这个异常信息来获取需要的内容,最终封装返回结果
解决第二个问题
这里就涉及到Jwt Token了,之前用过,但是说实话,我并不是很清楚怎么用
可以通过解析请求头中的token,来得到是哪个用户发起的请求 但是获取到之后,如何传递给service?
所以引入 ThreadLocal
,ThreadLocal
并不是一个 Thread
,而是 Thread
的局部变量ThreadLocal
为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问
每次请求,都是同一个线程
ThreadLocal 常用方法
public void set(T value)
设置当前线程的线程局部变量的值
public T get()
返回当前线程对应的线程局部变量的值
public void remove()
移除当前线程的线程局部变量
这里使用了一个工具类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package com.sky.context;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(); } }
一共两处修改:
JwtTokenAdminInterceptor拦截器,在拿到token中的empId时,将该empId存入ThreadLocal中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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); BaseContextByMe.setCurrentId(empId); return true ; } catch (Exception ex) { response.setStatus(401 ); return false ; }
Service中,在设置登录用户的id处,取出ThreadLocal中的empId
1 2 3 4 5 6 Long empId = BaseContextByMe.getCurrentId();employee.setCreateUser(empId); employee.setUpdateUser(empId);
员工分页查询
业务规则
根据页码展示员工信息
每页展示10条数据
分页查询时可以根据需要,输入员工姓名进行查询
这里封装了一个PageResult对象,后面所有的分页查询,统一都封装成PageResult对象 ,然后最后返回给前端的还是Result,为 Result<PageResult>
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; }
使用到的分页插件
1 2 3 4 <dependency > <groupId > com.github.pagehelper</groupId > <artifactId > pagehelper-spring-boot-starter</artifactId > </dependency >
感觉这个分页插件和mp的分页区别挺大的,但是说实话,我现在并不太想用mp,这玩意弱化sql的能力哎.
代码开发
Controller,这里是直接拿请求参数的,不是请求体,所以不用 @RequestBody
注解,用到的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; }
1 2 3 4 5 6 7 @GetMapping("/page") @ApiOperation("员工分页查询") public Result<PageResult> page (EmployeePageQueryDTO employeePageQueryDTO) { log.info("员工分页查询,参数为:{}" , employeePageQueryDTO); PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO); return Result.success(pageResult); }
Service中,使用到了 PageHelper
,重点在于 PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
,第一个参数是当前页,第二个参数是每页个数,这两个参数都是前端传来的.最后调用mapper查询数据库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @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); }
Mapper,因为使用了分页插件,这个分页插件会动态的拼接limit,所以这里不用再写limit了,只需要拼接可选参数姓名即可
1 2 3 4 5 6 7 8 9 10 <select id ="pageQuery" resultType ="com.sky.entity.Employee" > select * from employee <where > <if test ="name!=''and name!=null" > name like concat('%',#{name},'%') </if > </where > order by create_time DESC </select >
测试整体没什么问题,但是返回的create_time,也就是日期格式有问题
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 { "code" : 1 , "msg" : null , "data" : { "total" : 5 , "records" : [ { "id" : 3 , "username" : "lisi" , "name" : "李四" , "password" : "e10adc3949ba59abbe56e057f20f883e" , "phone" : "17513571211" , "sex" : "0" , "idNumber" : "412827200104013984" , "status" : 1 , "createTime" : [ 2023 , 8 , 26 , 23 , 46 , 12 ] , "updateTime" : [ 2023 , 8 , 26 , 23 , 46 , 12 ] , "createUser" : 10 , "updateUser" : null } , { "id" : 2 , "username" : "zhuozhuo" , "name" : "灼灼" , "password" : "e10adc3949ba59abbe56e057f20f883e" , "phone" : "17513741212" , "sex" : "1" , "idNumber" : "111111112313213" , "status" : 1 , "createTime" : [ 2023 , 8 , 26 , 23 , 38 , 16 ] , "updateTime" : [ 2023 , 8 , 26 , 23 , 38 , 16 ] , "createUser" : 10 , "updateUser" : null } ] } }
前端展示的是一个字符串:
代码完善
解决方式有以下两种
在属性上加入注解,对日期进行格式化
1 2 @JsonFormt(pattern = "yyyy-MM-dd HH:mm:ss") private LocalDateTime updateTime;
在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式化处理(推荐使用,一劳永逸 )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Override protected void extendMessageConverters (List<HttpMessageConverter<?>> converters) { log.info("扩展消息转换器-用于日期格式化" ); MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter (); converter.setObjectMapper(new JacksonObjectMapper ()); converters.add(0 , converter); }
ok,员工分类查询结束
启用/禁用员工账号 这个整体还是简单许多的,就是接受前端发来的状态,已经要修改的员工id
Controller,路径参数要用注解来获取,而地址栏传参不需要加注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @PostMapping("/status/{status}") @ApiOperation("启用/禁用员工账号") public Result startOrStop (@PathVariable("status") Integer status, Long id) { log.info("启用/禁用员工账号: status:{}, id:{}" , status, id); employeeService.startOrStop(status,id); return Result.success(); }
Service,这里也用到了Builder,update方法可以根据传入对象的id,修改任意的字段-动态sql
1 2 3 4 5 6 7 8 9 10 11 12 13 @Override public void startOrStop (Integer status, Long id) { Employee employee = Employee.builder().id(id).status(status).build(); employeeMapper.update(employee); }
Mapper,这里还不能写成 test="username != null and username != ''"
,还必须只能写成这样的..
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 <update id ="update" > update employee <set > <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 > <if test ="name!=null" > name = #{name}, </if > </set > where id = #{id} </update >
编辑员工 编辑员工功能涉及到两个接口
根据id查询员工信息
Controller 这个还是很简单的
1 2 3 4 5 6 7 8 9 10 11 12 @GetMapping("/{id}") @ApiOperation("根据id查询员工信息") public Result<Employee> getById (@PathVariable Long id) { Employee employee = employeeService.getById(id); return Result.success(employee); }
Service,主要就是注意密码要重写一遍,不能直接发过去,就算是加密过的也不能发
1 2 3 4 5 6 7 8 9 10 11 12 13 @Override public Employee getById (Long id) { Employee employee = employeeMapper.getById(id); employee.setPassword("****" ); return employee; }
编辑员工信息
Controller,还是使用的 EmployeeDTO
1 2 3 4 5 6 @PutMapping @ApiOperation("编辑员工信息") public Result update (@RequestBody EmployeeDTO employeeDTO) { employeeService.update(employeeDTO); return Result.success(); }
Service,首先创建一个employee对象,因为给数据库存数据都是用的实体类,这里又用到了 BeanUtils
来copy对象,最后再把 ThreadLocal
中的当前用户id,也就是 谁更新的
设置进来,再用上面更新员工状态的 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(BaseContextByMe.getCurrentId()); employeeMapper.update(employee); }
3 分类管理 设计
分类名称必须唯一
分类按照类型可以分为菜品分类 和套餐分类
新添加的分类状态默认为禁用
接口
新增分类
分类分页查询
根据id删除分类
修改分类
启用禁用分类
根据类型查询分类
老师的意思是直接cv了,因为整体逻辑和员工管理差不多,我自己写一遍吧
新增分类
Controller,直接用categoryDTO接收前端发来的信息
1 2 3 4 5 6 7 8 9 10 11 12 @PostMapping @ApiOperation("新增分类信息") public Result save (@RequestBody CategoryDTO categoryDTO) { categoryService.save(categoryDTO); return Result.success(); }
Service,既然是新增,加到数据库的依然是 Category
而不是 CategoryDTO
,所以这里要进行类型转换,注意新增的类型是默认禁止的,最后取出修改创建人的id一起封装
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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(BaseContextByMe.getCurrentId()); category.setUpdateUser(BaseContextByMe.getCurrentId()); categoryMapper.insert(category); }
分类分页查询
Controller,遇到分页查询,既需要用 PageResult
1 2 3 4 5 6 7 8 9 10 11 @GetMapping("/page") @ApiOperation("分类分页查询") public Result<PageResult> page (CategoryPageQueryDTO categoryPageQueryDTO) { PageResult pageResult = categoryService.pageQuery(categoryPageQueryDTO); return Result.success(pageResult); }
Service,这个接口依然不算难,照着之前写的那个就能写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override public PageResult pageQuery (CategoryPageQueryDTO categoryPageQueryDTO) { PageHelper.startPage(categoryPageQueryDTO.getPage(), categoryPageQueryDTO.getPageSize()); Page<Category> page = categoryMapper.pageQuery(categoryPageQueryDTO); long total = page.getTotal(); List<Category> records = page.getResult(); return new PageResult (total, records); }
Mapper,也是常规的内容,注意分类名要模糊查询,而分类类型是直接判断
1 2 3 4 5 6 7 8 9 10 11 12 <select id ="pageQuery" resultType ="com.sky.entity.Category" > select * from category <where > <if test ="name != null and name != ''" > name like concat('%',#{name},'%') </if > <if test ="type != null and type != ''" > and type = #{type} </if > </where > order by sort asc , create_time desc </select >
根据id删除分类
Controller,那就直接调用service进行删除
1 2 3 4 5 6 7 8 9 10 11 12 @DeleteMapping @ApiOperation("删除分类") public Result<String> deleteById (Long id) { categoryService.deleteById(id); return Result.success(); }
Service,主要就是注意分类是否包含菜品或套餐 ,引入另外的两个mapper,来根据分类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 @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); }
修改分类
Controller,直接传,没什么好说的
1 2 3 4 5 6 7 8 9 10 11 @PutMapping @ApiOperation("修改分类") public Result update (@RequestBody CategoryDTO categoryDTO) { categoryService.update(categoryDTO); return Result.success(); }
Service,主要是将 categoryDTO
封装为 category
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override public void update (CategoryDTO categoryDTO) { Category category = new Category (); BeanUtils.copyProperties(categoryDTO, category); category.setUpdateUser(BaseContextByMe.getCurrentId()); category.setUpdateTime(LocalDateTime.now()); categoryMapper.update(category); }
Mapper
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <update id ="update" parameterType ="Category" > update category <set > <if test ="type != null" > type = #{type}, </if > <if test ="name != null" > name = #{name}, </if > <if test ="sort != null" > sort = #{sort}, </if > <if test ="status != null" > status = #{status}, </if > <if test ="updateTime != null" > update_time = #{updateTime}, </if > <if test ="updateUser != null" > update_user = #{updateUser} </if > </set > where id = #{id} </update >
启用禁用分类
Controller,也是和之前一样
1 2 3 4 5 6 7 8 9 10 11 @PostMapping("/status/{status}") public Result startOrStop (@PathVariable Integer status, Long id) { categoryService.startOrStop(status, id); return Result.success(); }
Service
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Override public void startOrStop (Integer status, Long id) { Category category = Category.builder().id(id).status(status) .updateTime(LocalDateTime.now()).updateUser(BaseContextByMe.getCurrentId()). build(); categoryMapper.update(category); }
根据类型查询分类
Controller
1 2 3 4 5 6 7 8 9 10 11 @GetMapping("/list") @ApiOperation("根据类型查询分类") public Result<List<Category>> list (Integer type) { List<Category> list = categoryService.list(type); return Result.success(list); }
Service
1 2 3 4 5 6 7 8 9 10 11 @Override public List<Category> list (Integer type) { List<Category> list = categoryMapper.list(type); return list; }
就到这了.分类接口写完,也全部测试通过.
4 菜品管理 公共字段自动填充 什么意思呢,就是之前写的员工管理接口,以及分类管理,这些业务表中都有公共字段:
序号
字段名
含义
数据类型
操作类型
1
create_time
创建时间
datetime
insert
2
create_user
创建人id
bigint
insert
3
update_time
修改时间
datatime
insert,update
4
update_user
修改人id
bigint
insert,update
对应的代码就是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 category.setCreateTime(LocalDateTime.now()); category.setUpdateTime(LocalDateTime.now()); category.setCreateUser(BaseContextByMe.getCurrentId()); category.setUpdateUser(BaseContextByMe.getCurrentId()); employee.setCreateTime(LocalDateTime.now()); employee.setUpdateTime(LocalDateTime.now()); Long empId = BaseContextByMe.getCurrentId();employee.setCreateUser(empId); employee.setUpdateUser(empId);
问题就是,代码冗余,不便于后期维护
果然是用到切面
自定义注解AutoFill
,用于标识需要进行公共字段自动填充的方法
自定义切面类AutoFillAspect
,统一拦截加入了AutoFill
注解的方法,通过反射为公共字段赋值
在Mapper中需要使用的方法上加入AutoFill
注解
这里还闹了个乌龙,当时在token中取数据并存入ThreadLocal时,是用的我自己写的,而刚才写切面时,却用的是之前的,两个不同的类具有不同的ThreadLocal,所以获取不到数据 ,下面是ChatGPT给出的答案
1 2 3 由于每个线程都有独立的线程上下文,如果你在不同的线程中使用了不同的 ThreadLocal 实例,那么它们存储的内容就是隔离的。在你的代码中,BaseContext 和 BaseContextByMe 都使用了不同的 ThreadLocal 实例,因此在切面方法中,使用不同的 ThreadLocal 实例就会导致获取到不同的线程上下文。 如果你在使用 BaseContext 时无法获取到 currentId,很可能是因为你的切面所在的线程没有在 BaseContext 中设置过 currentId。而在使用 BaseContextByMe 时能够获取到 currentId,可能是因为切面所在的线程在某个地方已经设置过了 currentId,例如在某个请求的上下文中。
自定义注解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 package com.sky.annotation;import com.sky.enumeration.OperationType;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AutoFill { OperationType value () ; }
里面用到的枚举类型OperationType
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.sky.enumeration;public enum OperationType { UPDATE, INSERT }
切面类AutoFillAspect
,之前学的用到了一点,之前用的是Signature,但是这个签名是拿不到具体方法上的注解的,所以要用这个MethodSignature
,.getMethod().getAnnotation(AutoFill.class);
,即可拿到指定注解的对象,然后通过autoFill.value();
就可取到注解的参数,究竟是update
还是insert
,entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME,LocalDateTime.class);
这个就是通过反射拿到对象的方法,固定写法,然后再通过invoke
来设置方法的参数setCreateTime.invoke(entity, now);
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 package com.sky.aspect;import com.sky.annotation.AutoFill;import com.sky.constant.AutoFillConstant;import com.sky.context.BaseContext;import com.sky.context.BaseContextByMe;import com.sky.enumeration.OperationType;import lombok.extern.slf4j.Slf4j;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.Signature;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;import org.aspectj.lang.annotation.Pointcut;import org.aspectj.lang.reflect.MethodSignature;import org.springframework.stereotype.Component;import java.lang.reflect.Method;import java.time.LocalDateTime;@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 = BaseContextByMe.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 setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.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 setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class); Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class); setUpdateTime.invoke(entity, now); setUpdateUser.invoke(entity, currentId); } catch (Exception e) { throw new RuntimeException (e); } } } }
Mapper改写,既然要使用切面,那肯定就要给想要使用切面的方法上加上注解,并指定类型
1 2 3 4 5 6 7 8 9 10 11 @AutoFill(value = OperationType.INSERT) @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 save (Employee employee) ;
Service,已经使用了切面了,那就不需要再在service中写了
新增菜品 这个就是一个表单,重要的是有一个文件上传,这个待好好学
业务规则
菜品名称必须是唯一的
菜品必须属于某个分类下,不能单独存在
新增菜品时可以根据情况选择菜品的口味
每个菜品必须对应一张图片
接口设计
根据类型查询分类(已完成)
文件上传
新增菜品
文件上传这个接口详情:
而下面的dish表-菜品表中,有一个category_id
,它是逻辑外键-数据库中并没有设置外键,这个外键
是通过程序实现的 在dish_flavor口味表中,也是有一个逻辑外键dish_id
,一个菜品-多个口味
文件上传接口 看来要买OSS了
整体流程:
别流程了,先去把OSS的教程看了再说,今天不早了,就到这吧,课程链接
好了,大概知道oss怎么用了,新版教程中已经将access Key和key secret都给封装到环境变量里了,这里我先把这两个配置注释掉,看看一会怎么写
配置文件中新增,注意,这个不是系统自带的配置属性,而是根据一个自定义的配置类写的,而且真正的配置信息是在dev配置文件中配置的,这个主配置文件是引用,因为以后可能会存在生产模式,方便更换配置信息
1 2 3 4 alioss: endpoint: oss-cn-beijing.aliyuncs.com bucket-name: zzmr-sky-take-out
1 2 3 4 alioss: endpoint: ${sky.alioss.endpoint} bucket-name: ${sky.alioss.bucket-name}
这个就是配置类,注解@ConfigurationProperties
可以自动获取配置文件中的信息,然后加入到容器中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package com.sky.properties;import lombok.Data;import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.stereotype.Component;@Component @ConfigurationProperties(prefix = "sky.alioss") @Data public class AliOssProperties { private String endpoint; private String bucketName; }
除了上面的配置文件以及配置类,还有一个创建工具类的配置类,这个配置类可以获取到上面的properties类中的信息,是系统在运行时自动掉用的
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 package com.sky.config;import com.sky.properties.AliOssProperties;import com.sky.utils.AliOssUtil;import lombok.extern.slf4j.Slf4j;import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;@Configuration @Slf4j public class OssConfiguration { @Bean @ConditionalOnMissingBean public AliOssUtil getAliOssUtil (AliOssProperties aliOssProperties) { log.info("开始创建阿里云文件上传工具类对象" ); log.info("参数为:{}" , aliOssProperties); AliOssUtil aliOssUtil = new AliOssUtil (aliOssProperties.getEndpoint(), aliOssProperties.getBucketName()); return aliOssUtil; } }
最后就是工具类,这个工具类也是被我改写过,之前都是将key信息配置到配置文件中,现在是将key从环境变量中取出,所以这里只需要用到剩下的两个参数,上面的配置类直接通过一个构造方法,构造一个工具类.
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 package com.sky.utils;import com.aliyun.oss.ClientException;import com.aliyun.oss.OSS;import com.aliyun.oss.OSSClientBuilder;import com.aliyun.oss.OSSException;import com.aliyun.oss.common.auth.CredentialsProviderFactory;import com.aliyun.oss.common.auth.EnvironmentVariableCredentialsProvider;import lombok.AllArgsConstructor;import lombok.Data;import lombok.extern.slf4j.Slf4j;import java.io.ByteArrayInputStream;@Data @AllArgsConstructor @Slf4j public class AliOssUtil { private String endpoint; private String bucketName; public String upload (byte [] bytes, String objectName) { EnvironmentVariableCredentialsProvider credentialsProvider = null ; try { credentialsProvider = CredentialsProviderFactory.newEnvironmentVariableCredentialsProvider(); } catch (com.aliyuncs.exceptions.ClientException e) { e.printStackTrace(); } OSS ossClient = new OSSClientBuilder ().build(endpoint, credentialsProvider); 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(); } }
到此文件上传的配置已经搞完了,但是接口还没开始写呢
文件上传接口开发
这个就是上传的接口,流程就是,先通过MultipartFile
来获取到前端发来的文件,然后获取到文件的原文件名,截取后缀,通过UUID来获取一个新的文件名来防止文件名重复的问题,最后拼在一起,存储后返回url给前端
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 package com.sky.controller.admin;import com.sky.constant.MessageConstant;import com.sky.result.Result;import com.sky.utils.AliOssUtil;import io.swagger.annotations.Api;import io.swagger.annotations.ApiOperation;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.PostMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.multipart.MultipartFile;import java.io.IOException;import java.util.UUID;@RestController @Slf4j @RequestMapping("/admin/common") @Api(tags = "通用接口") 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); e.printStackTrace(); } return Result.error(MessageConstant.UPLOAD_FAILED); } }
新增菜品接口
这里要用到事务@Transactional
,想要使用注解的事务,要在启动类上加上@EnableTransactionManagement
注解
前端传来的DTO清晰明了
Controller ,应该controller算是最简单的了,只负责将dishDTO
往service上传就ok
1 2 3 4 5 6 7 @PostMapping @ApiOperation("新增菜品") public Result save (@RequestBody DishDTO dishDTO) { log.info("新增菜品" ); dishService.saveWithFlavor(dishDTO); return Result.success(); }
Service ,在这个Service中,用到了两个Mapper,其中一个就是菜品Mapper,因为要新增菜品,所以肯定要用菜品的Mapper,每个菜品对应的有口味,要将口味也存入数据库就要用到口味的Mapper
首先将dto中的数据赋值给dish实体,以便操作数据库:BeanUtils.copyProperties
这里用到了Mapper中的插入回显Id,具体配置见Mapper,所以这里可以直接getId()
来获取菜品Id,为什么要用菜品Id?因为口味是和菜品绑定的,一个菜品对应多个口味,在口味表中,每条数据都有一个菜品Id:dish_id
,所以要获取到该条菜品的id才行
取出集合,将口味这个集合插入数据库,可以在代码中直接进行遍历,也可以在mapper中使用动态sql来实现批量插入
因为前端传来的flavors中肯定是没有dish_id
的,因为这个dish_id
只有插入了数据库之后才能得到,此时就要给集合中的每一项都赋值一个dish_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 34 35 36 @Service public class DishServiceImpl implements DishService { @Autowired private DishMapper dishMapper; @Autowired private DishFlavorMapper dishFlavorMapper; @Override @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菜品,在菜品Mapper中,要加上属性useGeneratedKeys
和keyProperty
来得到插入数据库得到的id1 2 3 4 5 6 7 <insert id ="insert" useGeneratedKeys ="true" keyProperty ="id" > insert into dish(name, category_id, price, image, description, status, create_time, update_time, create_user, update_user) values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}) </insert >
DishFlavorMapper,这个Mapper就是通过动态Sql来实现的批量插入,注意forEach的语法
1 2 3 4 5 6 <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 >
这个接口终于写完了,奶奶滴,因为字段写错了搞了老半天.好困啊,打会游戏
菜品分页查询 业务规则
根据页码展示菜品信息
每页展示10条数据
分页查询时可以根据需要输入菜品名称,菜品分类,菜品状态进行查询
接口详情:
当使用动态sql时,即使用<if>
如果这个<if>
是写在<where>
中,那就不必在每行的最后写逗号,而如果在<set>
里面,那么就要注意逗号的使用了,除了最后一个以外,都需要加逗号,这也就是查询和插入的区别吧,简单记:查询不加逗号,插入要加逗号
这个分页写的也真是曲折啊
Controller,controller还是这些活
1 2 3 4 5 6 7 8 9 10 11 12 13 @ApiOperation("分页查询菜品") @GetMapping("/page") public Result<PageResult> page (DishPageQueryDTO dishPageQueryDTO) { log.info("菜品分页查询开始" ); PageResult pageResult = dishService.pageQuery(dishPageQueryDTO); return Result.success(pageResult); }
Service,由于前端需要用到字段categoryName
,所以不能使用Dish,而要用DishVO,这个VO就刚好和前端要求的数据一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Override public PageResult pageQuery (DishPageQueryDTO dishPageQueryDTO) { PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize()); Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO); log.info("查询到的分页信息:{}" , page); long total = page.getTotal(); List<DishVO> records = page.getResult(); return new PageResult (total, records); }
Mapper,还是没想明白为什么有的地方不能写status != null and status != ''
,写成这样条件就不成立,就很麻烦,那以后先全用status != null
这种形式吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <select id ="pageQuery" resultType ="com.sky.vo.DishVO" > select dish.*, category.`name` category_name from dish left join category on dish.category_id = category.id <where > <if test ="name != null" > and dish.name like concat('%',#{name},'%') </if > <if test ="categoryId != null" > and dish.category_id = #{categoryId} </if > <if test ="status != null" > and dish.status = #{status} </if > </where > order by dish.create_time DESC </select >
删除菜品
这个我自己写了,功能好像也没问题
?但是一看视频,就知道自己写的大有问题了
业务规则:
可以一次删除一个菜品,也可以批量删除菜品
起售中的菜品不能删除
被套餐关联的菜品不能删除
删除菜品后,关联的口味数据也需要删除掉
好家伙,就实现了第一条
Controller,之前写的是用一个数组来接收,也是能实现删除的,但是按照规范,还是要用List集合,而且这里要用@RequestParam
注解才能封装上进去
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @DeleteMapping @ApiOperation("批量删除菜品") public Result deleteBatchDish (@RequestParam List<Long> ids) { log.info("传入的ids: {}" , ids.size()); dishService.deleteBatchDish(ids); return Result.success(); }
Service,就像上面说的,在删除之前,要先进行判断,
在判断菜品是否在某个套餐中时,使用了setmealDishMapper
,这个是一个新的Mapper,具体内容如下,根据dishId来查询是否有套餐关联,只要有一个菜品中关联了套餐,那就不能删除1 2 3 4 5 6 <select id ="getSetmealIdsByDishIds" resultType ="java.lang.Long" > select setmeal_id from setmeal_dish where dish_id in <foreach collection ="dishIds" separator ="," item ="dishId" open ="(" close =")" > #{dishId} </foreach > </select >
在删除了菜品后,也要删除菜品对应的口味,这里就要根据dishId来删除1 2 @Delete("delete from dish_flavor where dish_id = #{dishId};") void deleteByDishId (Long dishId) ;
最终的代码: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 @Override public void deleteBatchDish (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.CATEGORY_BE_RELATED_BY_SETMEAL); } for (Long id : ids) { dishMapper.deleteById(id); dishFlavorMapper.deleteByDishId(id); } }
修改菜品
修改菜品需要的接口
根据id查询菜品(同时也把菜品关联的口味也查出来)
根据类型(菜品类型/套餐类型)查询分类
文件上传
修改菜品
根据id查询菜品 这个还是很简单的
Controller,只是起名字起的有点特别,因为要查询菜品和菜品对应的口味,所以这里用的还是DishVO
1 2 3 4 5 6 7 8 9 10 11 12 13 @ApiOperation("根据id查询菜品,和对应的口味") @GetMapping("/{id}") public Result<DishVO> getById (@PathVariable Long id) { log.info("根据id查询菜品:{}" , id); DishVO dishVO = dishService.getByIdWithFlavor(id); return Result.success(dishVO); }
Service,这里分两步,第一步先查出菜品信息,第二步根据菜品信息的菜品dish_id再查询对应的口味,最后封装到一个VO中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Override public DishVO getByIdWithFlavor (Long id) { Dish dish = dishMapper.getById(id); List<DishFlavor> flavors = dishFlavorMapper.getByDishId(id); DishVO dishVO = new DishVO (); BeanUtils.copyProperties(dish, dishVO); dishVO.setFlavors(flavors); return dishVO; }
Mapper中就是很简单的单表查询了
修改菜品
Controller,没什么说的,因为这里更新数据传来的json是可以封装为一个DTO的,所以用不到VO了(VO只是比DTO多一个分类名称)
1 2 3 4 5 6 7 8 9 10 11 12 @PutMapping @ApiOperation("修改菜品") public Result update (@RequestBody DishDTO dishDTO) { log.info("修改菜品:{} " , dishDTO); dishService.updateWithFlavor(dishDTO); return Result.success(); }
Service,update菜品还是老办法,动态Sql,但是更改口味就换个方法了,先删再加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Override 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(flavor -> flavor.setDishId(dishDTO.getId())); dishFlavorMapper.insertBatch(flavors); } }
Mapper,再写最后一次Mapper,主要是这动态sql的格式太容易弄错了,到底加不加逗号,加不加and,现在我确定了set中不加and,加逗号,查询时用的where,要加and,不加逗号(原本就是通过and链接起来的) ,
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" > update dish <set > <if test ="name != null" > name = #{name}, </if > <if test ="categoryId != null" > category_id = #{categoryId}, </if > <if test ="status != null" > status = #{status}, </if > <if test ="price != null" > price = #{price}, </if > <if test ="description != null" > description = #{description}, </if > <if test ="image != null" > image = #{image}, </if > <if test ="updateTime != null" > update_time = #{updateTime}, </if > <if test ="updateUser != null" > update_user = #{updateUser} </if > </set > where id = #{id} </update >
5 套餐管理 这个就要自己写了,虽然给了答案,但是还是自己敲一遍吧,这个套餐管理应该和分类管理有点像?或者就是菜品管理
?费了半天劲写出了的分页,发现没有套餐的数据,明天继续吧
算是写的差不多?
暂时完成了分页和新增,显示的也没什么问题
写完咯!
对对答案
新增套餐 在新增套餐里面,会用到根据分类id查询类型对应的菜品的接口: 所以这里要先写一个根据分类id查询对应菜品的接口
Controller,前端只传来一个id,然后根据id查询
1 2 3 4 5 6 7 8 9 10 11 12 @GetMapping("/list") @ApiOperation("根据分类id查询菜品") public Result<List<Dish>> getByCategoryId (Long categoryId) { log.info("根据分类id查询菜品,参数为{}" , categoryId); List<Dish> list = dishService.getByCategoryId(categoryId); return Result.success(list); }
Service,这里我刚开始写的时候出问题了,没考虑到菜品如果下架的情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override public List<Dish> getByCategoryId (Long categoryId) { Dish dish = Dish.builder().categoryId(categoryId).status(StatusConstant.ENABLE).build(); return dishMapper.getByCategoryId(dish); }
Mapper,因为有两个值要传,所以这里直接使用dish对象来传递参数
1 2 3 4 5 6 7 8 9 @Select("select * from dish where category_id = #{categoryId} and status = #{status}") List<Dish> getByCategoryId (Dish dish) ;
注意,我这里写的和文档里面是不一样的
下面才是新增套餐的接口
Controller,前端传来的数据是DTO,因为里面不但包含套餐的基本信息,还包含套餐中对应的菜品信息
1 2 3 4 5 6 7 8 9 10 11 12 @PostMapping @ApiOperation("新增套餐") public Result save (@RequestBody SetmealDTO setmealDTO) { log.info("新增套餐数据:{}" , setmealDTO); setmealService.saveWithDish(setmealDTO); return Result.success(); }
Service,这里就稍微复杂点了
首先将dto转化为setmeal对象,然后直接进行插入,这里有一个全局异常处理,如果出现唯一键问题,会自动进行异常处理
通过mybatis中的插入后返回主键的功能,获取到插入套餐后对应的主键值,并得到每个setmealDish,对每个setmealDish的setmealId进行赋值,这样这个setmealDish中就有了菜品id(自带,因为菜品传过来肯定是要有id的),还有setmealId,最后执行插入1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Override 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.insert(setmealDishes); }
Mapper
Setmeal.mapper,常规的插入语句1 2 3 4 5 6 <insert id ="insert" 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 >
SetmealDish.mapper,使用了foreach,因为会有很多条数据,是一个集合1 2 3 4 5 6 <insert id ="insert" parameterType ="java.util.List" > insert into setmeal_dish (setmeal_id,dish_id,name,price,copies) values <foreach collection ="setmealDishes" item ="item" separator ="," > (#{item.setmealId},#{item.dishId},#{item.name},#{item.price},#{item.copies}) </foreach > </insert >
这样就完成了这个新增套餐的接口
套餐分页查询 这个也是犯了一个很蠢的错误
分页查询时根本用不到套餐对应的菜品信息
Controller,正常写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @GetMapping("/page") @ApiOperation("分页查询套餐") public Result<PageResult> page (SetmealPageQueryDTO setmealPageQueryDTO) { log.info("收到了分页请求,参数为: {}" , setmealPageQueryDTO); PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO); return Result.success(pageResult); }
Service,这个是真的傻了,当时看到什么情况,就改了这一堆代码?
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 @Override public PageResult pageQuery (SetmealPageQueryDTO setmealPageQueryDTO) { PageHelper.startPage(setmealPageQueryDTO.getPage(), setmealPageQueryDTO.getPageSize()); Page<SetmealVO> page = setmealMapper.pageQueryBySql(setmealPageQueryDTO); return new PageResult (page.getTotal(), page.getResult()); }
Mapper,一个很简单的关联查询,只是为了将分类名查出来,因为只是需要分类名,其他的并不需要…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 <select id ="pageQueryBySql" 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 >
删除套餐 业务规则:
可以一次删除一个套餐,也可以批量删除套餐
起售中的套餐不能删除
这一看果然是正常逻辑,只有起售中的套餐不能删除,那我写的应该是没问题吧
还是这个简单些
Controller,前端传来一个数组,用@RequestParam
来讲数组封装为一个Long型的集合
1 2 3 4 5 6 7 8 9 10 11 12 13 @DeleteMapping @ApiOperation("批量删除菜品") public Result deleteBitch (@RequestParam List<Long> ids) { log.info("批量删除菜品,参数:{}" , ids); setmealService.deleteBitch(ids); return Result.success(); }
Service,这里既然涉及到多表,所以用到了@Transactional
事务处理
遍历这个ids,得到每一项
对每一项进行判断状态,如果有一个状态是起售的,就直接抛出不能删除的异常
最后在删除就行了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 @Override @Transactional public void deleteBitch (List<Long> ids) { for (Long id : ids) { Setmeal setmeal = setmealMapper.getById(id); if (setmeal.getStatus() == StatusConstant.ENABLE) { throw new DeletionNotAllowedException (MessageConstant.SETMEAL_ON_SALE); } } setmealMapper.deleteBitch(ids); for (Long id : ids) { setmealDishMapper.deleteBySetmealId(id); } }
Mapper
SetmealMapper,这里因为是批量删除,所以使用了foreach(老师的是单个删除,然后外面套了一层遍历ids的循环)1 2 3 4 5 6 7 8 <delete id ="deleteBitch" parameterType ="java.util.List" > delete from setmeal where id in <foreach collection ="ids" item ="id" separator ="," open ="(" close =")" > #{id} </foreach > </delete >
SetmealDishMapper,就是普通的删除方法了,根据套餐id删1 2 3 4 5 6 @Delete("delete from setmeal_dish where setmeal_id = #{setmealId}") void deleteBySetmealId (Long setmealId) ;
修改套餐
Controller,先查,进行回显,再改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @GetMapping("/{id}") @ApiOperation("根据id查询套餐以及套餐对应的菜品列表") public Result<SetmealVO> getWithSetmealDishesById (@PathVariable Long id) { log.info("根据id查询套餐以及套餐对应的菜品列表,参数为:{}" , id); SetmealVO setmealVO = setmealService.getWithSetmealDishById(id); return Result.success(setmealVO); } @PutMapping @ApiOperation("修改套餐") public Result update (@RequestBody SetmealDTO setmealDTO) { log.info("修改的参数:{}" , setmealDTO); setmealService.update(setmealDTO); return Result.success(); }
Service,看好了啊,这里不需要查询分类名…我说控制台怎么多了这么多查询语句
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 @Override public SetmealVO getWithSetmealDishById (Long id) { Setmeal setmeal = setmealMapper.getById(id); log.info("获取到的套餐信息:{}" , setmeal); SetmealVO setmealVO = new SetmealVO (); BeanUtils.copyProperties(setmeal, setmealVO); List<SetmealDish> list = setmealDishMapper.getBySetmealId(id); setmealVO.setSetmealDishes(list); return setmealVO; } @Override @Transactional public void update (SetmealDTO setmealDTO) { Setmeal setmeal = new Setmeal (); BeanUtils.copyProperties(setmealDTO, setmeal); setmealMapper.update(setmeal); List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes(); Long setmealId = setmealDTO.getId(); setmealDishMapper.deleteBySetmealId(setmealId); for (SetmealDish setmealDish : setmealDishes) { setmealDish.setSetmealId(setmealId); } log.info("修改后的套餐菜品对象为:{}" , setmealDishes); setmealDishMapper.insert(setmealDishes); }
Mapper,在SetmealMapper中用到的这个动态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 25 26 27 28 29 30 <update id ="update" parameterType ="com.sky.entity.Setmeal" > update setmeal <set > <if test ="categoryId != null" > category_id = #{categoryId}, </if > <if test ="name != null" > name = #{name}, </if > <if test ="price != null" > price = #{price}, </if > <if test ="status != null" > status = #{status}, </if > <if test ="description != null" > description = #{description}, </if > <if test ="image" > image = #{image}, </if > <if test ="updateTime != null" > update_time = #{updateTime}, </if > <if test ="updateUser != null" > update_user = #{updateUser} </if > </set > where id = #{id} </update >
修改套餐状态
Controller,这个不难吧
1 2 3 4 5 6 @PostMapping("status/{status}") @ApiOperation("套餐起售/停售") public Result startOrStop (@PathVariable Integer status, Long id) { setmealService.startOrStop(status, id); return Result.success(); }
???原来这个也这么麻烦 实属没想到
可以对状态为起售的套餐进行停售操作,可以对状态为停售的套餐进行起售操作
起售的套餐可以展示在用户端,停售的套餐不能展示在用户端
起售套餐时,如果套餐内包含停售的菜品,则不能起售
第三条,根本么考虑到
Service
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 @Override public void startOrStop (Integer status, Long id) { if (status == StatusConstant.ENABLE){ List<SetmealDish> setmealDishes = setmealDishMapper.getBySetmealId(id); for (SetmealDish setmealDish : setmealDishes) { Dish dish = dishMapper.getById(setmealDish.getDishId()); if (dish.getStatus() == StatusConstant.DISABLE) { throw new SetmealEnableFailedException (MessageConstant.SETMEAL_ENABLE_FAILED); } } } Setmeal setmeal = Setmeal.builder().id(id).status(status).build(); setmealMapper.update(setmeal); }
Mapper,我写的那个主要就是简单的sql,老师写的那个只是多个根据setmealId在dish表中查,是一个关联查询,就是他一条Sql解决了,而我两条
1 2 3 4 5 6 7 8 @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) ;
6 店铺营业状态设置 Redis Redis复习?
这一部分就不看了,直接跳到在Java中操作Redis
Java操作Redis 一共两种操作方式:
Redis的Java客户端
Spring Data Redis使用方式
重点放在Spring Data Redis
上面
操作步骤
导入Spring Data Redis
的maven坐标
配置Redis数据源 这里还是分开配置的1 2 3 4 5 6 7 8 9 10 11 12 redis: host: ${sky.redis.host} port: ${sky.redis.port} password: ${sky.redis.password} database: ${sky.redis.database} redis: host: 1.14 .102 .xx port: 6379 password: "010203" database: 2
编写配置类,创建RedisTemplate对象1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @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; } }
通过RedisTemplate对象操作Redis,直接打印对象1 2 3 4 5 6 7 @Autowired private RedisTemplate redisTemplate;@Test public void testRedisTemplate () { System.out.println(redisTemplate); }
基本操作
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 package com.sky.test;import org.junit.jupiter.api.DisplayName;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import org.springframework.data.redis.connection.DataType;import org.springframework.data.redis.core.*;import java.util.List;import java.util.Set;import java.util.concurrent.TimeUnit;@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(); } @DisplayName("操作字符串类型的数据") @Test public void testString () { redisTemplate.opsForValue().set("city" , "汉中" ); String city = (String) redisTemplate.opsForValue().get("city" ); System.out.println(city); String county = (String) redisTemplate.opsForValue().get("county" ); System.out.println(county); redisTemplate.opsForValue().setIfAbsent("city" , "成都" ); } @Test public void testHash () { HashOperations hashOperations = redisTemplate.opsForHash(); hashOperations.put("101" , "name" , "tom" ); hashOperations.put("101" , "age" , "20" ); String name = (String) hashOperations.get("101" , "name" ); System.out.println(name); hashOperations.keys("101" ); List values = hashOperations.values("101" ); System.out.println(values); hashOperations.delete("101" , "name" ); } @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); } @Test public void testSet () { SetOperations setOperations = redisTemplate.opsForSet(); setOperations.add("set02" , "a" , "b" , "c" , "d" ); setOperations.add("set03" , "e" , "f" , "g" , "h" ); Set members = setOperations.members("set02" ); System.out.println(members); Long size = setOperations.size("set02" ); System.out.println(size); Set intersect = setOperations.intersect("set02" , "set03" ); System.out.println(intersect); Set union = setOperations.union("set02" , "set03" ); System.out.println(union); setOperations.remove("set02" , "a" , "b" ); } @Test public void testZSet () { ZSetOperations zSetOperations = redisTemplate.opsForZSet(); zSetOperations.add("zset01" , "a" , 10 ); zSetOperations.add("zset01" , "b" , 20 ); zSetOperations.add("zset01" , "c" , 30 ); Set zset01 = zSetOperations.range("zset01" , 0 , -1 ); System.out.println(zset01); zSetOperations.incrementScore("zset01" , "a" , 30 ); zSetOperations.remove("zset01" , "a" , "b" ); } @Test public void testCommon () { Set keys = redisTemplate.keys("*" ); System.out.println(keys); Boolean myList = redisTemplate.hasKey("myList" ); Boolean xx = redisTemplate.hasKey("xx" ); System.out.println(myList); System.out.println(xx); for (Object key : keys) { DataType type = redisTemplate.type(key); System.out.println(type); } redisTemplate.delete("li" ); } }
店铺营业状态设置
查询状态(由于路径规范,管理端和用户端是不同的接口路径,所以需要两个查询接口)
修改状态
营业状态数据存储方式,基于Redis的字符串来进行存储
Controller,这里指定了bean的别名,因为会有两个ShopController
,一个是admin下的,一个是user下的,所以需要指定,不然就会报错,除了Controller,其他也就不需要了,因为这里的数据是直接存储到redis中的,因为就只有一条数据,放到redis中非常快速
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 @RestController("adminShopController") @Slf4j @RequestMapping("/admin/shop") @Api(tags = "店铺相关接口") 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(); } @GetMapping("/status") @ApiOperation("查询店铺状态") public Result<Integer> getStatus () { Integer shopStatus = (Integer) redisTemplate.opsForValue().get(KEY); log.info("获取到店铺的营业状态位:{}" , shopStatus == 1 ? "营业" : "打烊中" ); return Result.success(shopStatus); } }
user下的Controller,两个逻辑其实是一样的,只是有请求路径与点区别,这里的bean别名也设置了
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("userShopController") @Slf4j @RequestMapping("/user/shop") @Api(tags = "店铺相关接口") public class ShopController { public static final String KEY = "SHOP_STATUS" ; @Autowired private RedisTemplate redisTemplate; @GetMapping("/status") @ApiOperation("查询店铺状态") public Result<Integer> getStatus () { Integer shopStatus = (Integer) redisTemplate.opsForValue().get(KEY); log.info("获取到店铺的营业状态位:{}" , shopStatus == 1 ? "营业" : "打烊中" ); return Result.success(shopStatus); } }
7 微信登录-商品预览 HttpClient HttpClient是Apache Jakarta Common
下的子项目,可以用来提供高效的,最新的,功能丰富的支持HTTP协议的客户端编程工具包,并且它支持HTTP协议最新的版本和建议
依赖,这个依赖在OSS中已经引入了
1 2 3 4 5 <dependency > <groupId > org.apache.httpcomponents</groupId > <artifactId > httpclient</artifactId > <version > 4.4.1</version > </dependency >
发送请求步骤
创建HttpClient对象
创建Http请求对象(get/post)
调用HttpClient的execute方法发送请求
发送GET请求 就是上面那三步,看起来也没啥难得,就是要记几个API
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 @Test public void testGet () { CloseableHttpClient httpClient = HttpClients.createDefault(); HttpGet httpGet = new HttpGet ("http://localhost:8080/user/shop/status" ); try { CloseableHttpResponse response = httpClient.execute(httpGet); int statusCode = response.getStatusLine().getStatusCode(); System.out.println("服务端返回的状态码:" + statusCode); HttpEntity entity = response.getEntity(); String body = EntityUtils.toString(entity); System.out.println("服务端返回的数据:" + body); response.close(); httpClient.close(); } catch (IOException e) { e.printStackTrace(); } }
发送POST请求 没啥难的,只是记得封装请求体要使用到fastJson
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 @Test public void testPost () throws IOException { CloseableHttpClient httpClient = HttpClients.createDefault(); HttpPost httpPost = new HttpPost ("http://localhost:8080/admin/employee/login" ); JSONObject jsonObject = new JSONObject (); jsonObject.put("username" , "admin" ); jsonObject.put("password" , "123456" ); StringEntity entity = new StringEntity (jsonObject.toString()); entity.setContentEncoding("utf-8" ); entity.setContentType("application/json" ); httpPost.setEntity(entity); CloseableHttpResponse response = httpClient.execute(httpPost); int statusCode = response.getStatusLine().getStatusCode(); System.out.println("响应状态码:" + statusCode); HttpEntity responseEntity = response.getEntity(); String body = EntityUtils.toString(responseEntity); System.out.println("响应体:" + body); response.close(); httpClient.close(); }
封装的工具类 但是项目中使用还是用的封装好的工具类,不然一次又一次创建HttpClient多麻烦
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 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 package com.sky.utils;import com.alibaba.fastjson.JSONObject;import org.apache.http.NameValuePair;import org.apache.http.client.config.RequestConfig;import org.apache.http.client.entity.UrlEncodedFormEntity;import org.apache.http.client.methods.CloseableHttpResponse;import org.apache.http.client.methods.HttpGet;import org.apache.http.client.methods.HttpPost;import org.apache.http.client.utils.URIBuilder;import org.apache.http.entity.StringEntity;import org.apache.http.impl.client.CloseableHttpClient;import org.apache.http.impl.client.HttpClients;import org.apache.http.message.BasicNameValuePair;import org.apache.http.util.EntityUtils;import java.io.IOException;import java.net.URI;import java.util.ArrayList;import java.util.List;import java.util.Map;public class HttpClientUtil { static final int TIMEOUT_MSEC = 5 * 1000 ; public static String doGet (String url,Map<String,String> paramMap) { CloseableHttpClient httpClient = HttpClients.createDefault(); String result = "" ; CloseableHttpResponse response = null ; try { URIBuilder builder = new URIBuilder (url); if (paramMap != null ){ for (String key : paramMap.keySet()) { builder.addParameter(key,paramMap.get(key)); } } URI uri = builder.build(); HttpGet httpGet = new HttpGet (uri); response = httpClient.execute(httpGet); if (response.getStatusLine().getStatusCode() == 200 ){ result = EntityUtils.toString(response.getEntity(),"UTF-8" ); } }catch (Exception e){ e.printStackTrace(); }finally { try { response.close(); httpClient.close(); } catch (IOException e) { e.printStackTrace(); } } return result; } public static String doPost (String url, Map<String, String> paramMap) throws IOException { CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null ; String resultString = "" ; try { HttpPost httpPost = new HttpPost (url); if (paramMap != null ) { List<NameValuePair> paramList = new ArrayList (); for (Map.Entry<String, String> param : paramMap.entrySet()) { paramList.add(new BasicNameValuePair (param.getKey(), param.getValue())); } UrlEncodedFormEntity entity = new UrlEncodedFormEntity (paramList); httpPost.setEntity(entity); } httpPost.setConfig(builderRequestConfig()); response = httpClient.execute(httpPost); resultString = EntityUtils.toString(response.getEntity(), "UTF-8" ); } catch (Exception e) { throw e; } finally { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } return resultString; } public static String doPost4Json (String url, Map<String, String> paramMap) throws IOException { CloseableHttpClient httpClient = HttpClients.createDefault(); CloseableHttpResponse response = null ; String resultString = "" ; try { HttpPost httpPost = new HttpPost (url); if (paramMap != null ) { JSONObject jsonObject = new JSONObject (); for (Map.Entry<String, String> param : paramMap.entrySet()) { jsonObject.put(param.getKey(),param.getValue()); } StringEntity entity = new StringEntity (jsonObject.toString(),"utf-8" ); entity.setContentEncoding("utf-8" ); entity.setContentType("application/json" ); httpPost.setEntity(entity); } httpPost.setConfig(builderRequestConfig()); response = httpClient.execute(httpPost); resultString = EntityUtils.toString(response.getEntity(), "UTF-8" ); } catch (Exception e) { throw e; } finally { try { response.close(); } catch (IOException e) { e.printStackTrace(); } } return resultString; } private static RequestConfig builderRequestConfig () { return RequestConfig.custom() .setConnectTimeout(TIMEOUT_MSEC) .setConnectionRequestTimeout(TIMEOUT_MSEC) .setSocketTimeout(TIMEOUT_MSEC).build(); } }
微信小程序开发 入门案例 看看这个课和培训老师讲的哪个好
获取用户的头像信息,要把基础库版本调到2.10左右才能看到弹窗,不然只能得到默认的用户名和头像,应该是接口换了
1 2 3 4 5 6 7 8 9 10 11 12 13 getUserInfo ( ) { wx.getUserProfile ({ desc : '获取用户信息' , success : (res => { console .log (res.userInfo ) this .setData ({ nickName : res.userInfo .nickName , url : res.userInfo .avatarUrl }) }) }) }
用户登录,会返回一个授权码,是随机的,且一个授权码只能使用一次
1 2 3 4 5 6 7 8 wxlogin ( ) { wx.login ({ success : (res ) => { console .log (res.code ) }, }) }
发送异步请求
1 2 3 4 5 6 7 8 9 10 11 12 13 sendRequest ( ) { wx.request ({ url : 'http://localhost:8080/user/shop/status' , method : 'GET' , success : (res ) => { console .log (res.data ) this .setData ({ status : res.data .data }) } }) }
导入小程序代码 真就是直接导入,一点都不用改
微信登录流程
就是
小程序端调wx.login
得到code(用户授权码)
小程序端发送请求,携带code
服务端接收到code和请求,然后调用微信接口服务(appId
,appsecret
,code
)
微信接口返回session_key
和openId
服务端自定义登陆状态,与openId
和session_key
关联,返回自定义状态
小程序存入自定义登录状态,每次发送请求时,携带登录状态
wx26b00f9454de88a6 6c4ad5db6efb880425969b7afe4099c7
用postman发送请求试一下,是没问题的
接口设计 业务规则
基于微信登陆实现小程序的的登录功能
如果是新用户需要自动完成注册
代码开发 jwt令牌-用户和管理端要分开配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 sky: jwt: admin-secret-key: itcast admin-ttl: 7200000 admin-token-name: token user-secret-key: itheima user-ttl: 7200000 user-token-name: authentication
wx的id
1 2 3 wechat: appid: ${sky.wechat.appid} secret: ${sky.wechat.secret}
当时以为这个HttpClient是用来分模块开发的,没想到只是用来请求微信接口的
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 @RestController @Slf4j @RequestMapping("/user/user") @Api(tags = "用户相关接口") public class UserController { @Autowired private UserService userService; @Autowired private JwtProperties jwtProperties; @ApiOperation("微信登录") @PostMapping("/login") 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(); log.info("登陆成功:{}" , userLoginVO); return Result.success(userLoginVO); } }
Service
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 @Override public User wxLogin (UserLoginDTO userLoginDTO) { String openId = getString(userLoginDTO); 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; }
现在小程序端发出的请求都会携带请求头token
我终于知道为什么有些会要求有请求头的token,有些不用了,是这个拦截器在起作用,也就是从请求头中获取到token
1 2 String token = request.getHeader(jwtProperties.getUserTokenName());
然后就是将拦截器注册进去
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 @Configuration @Slf4j public class WebMvcConfiguration extends WebMvcConfigurationSupport { @Autowired private JwtTokenAdminInterceptor jwtTokenAdminInterceptor; @Autowired private JwtTokenUserInterceptor jwtTokenUserInterceptor; protected void addInterceptors (InterceptorRegistry registry) { log.info("开始注册自定义拦截器..." ); registry.addInterceptor(jwtTokenAdminInterceptor) .addPathPatterns("/admin/**" ) .excludePathPatterns("/admin/employee/login" ); registry.addInterceptor(jwtTokenUserInterceptor) .addPathPatterns("/user/**" ) .excludePathPatterns("/user/user/login" ) .excludePathPatterns("/user/shop/status" ); } ......
商品预览 这里还涉及到种类?,就是有口味,就会有规格,没有口味,就是选个数
查询分类
根据分类id查询菜品
根据分类id查询套餐
根据套餐id查询包含的菜品
查询所有分类-CategoryController,进入小程序后自动发起请求,请求所有的分类和套餐,这里可以根据分类类型进行查询-分类/套餐
1 2 3 4 5 6 7 8 9 10 @GetMapping("/list") public Result<List<Category>> list (Integer type) { List<Category> list = categoryService.list(type); return Result.success(list); }
根据分类id查询菜品,就是点击某个分类,进行查询该分类的菜品,规则就是要求是起售的
DishController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @GetMapping("/list") @ApiOperation("根据分类id查询菜品") public Result<List<DishVO>> list (Long categoryId) { Dish dish = Dish.builder().status(StatusConstant.ENABLE).categoryId(categoryId).build(); List<DishVO> list = dishService.listWithFlavor(dish); return Result.success(list); }
DishServiceImpl
,首选根据分类的id查询所有的菜品,用一个List<Dish>
封装,然后就是遍历这个集合,创建一个DishVO对象,然后将每一个Dish对象复制给DishVO,再根据dishId查询所有的口味,最后将口味封装到DishVO中,最后返回.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Override public List<DishVO> listWithFlavor (Dish dish) { List<Dish> dishList = dishMapper.getByCategoryId(dish); List<DishVO> dishVOList = new ArrayList <>(); for (Dish d : dishList) { DishVO dishVO = new DishVO (); BeanUtils.copyProperties(d, dishVO); List<DishFlavor> flavors = dishFlavorMapper.getByDishId(dish.getId()); dishVO.setFlavors(flavors); dishVOList.add(dishVO); } return dishVOList; }
根据分类id查询套餐
1 2 3 4 5 6 7 @GetMapping("/list") @ApiOperation("根据分类id查询套餐") public Result<List<Setmeal>> list (Long categoryId) { Setmeal setmeal = Setmeal.builder().status(StatusConstant.ENABLE).categoryId(categoryId).build(); List<Setmeal> list = setmealService.list(setmeal); return Result.success(list); }
根据套餐id查询包含的菜品
1 2 3 4 5 6 @GetMapping("/dish/{id}") @ApiOperation("根据套餐id查询包含的菜品") public Result<List<DishItemVO>> dishList (@PathVariable Long id) { List<DishItemVO> list = setmealService.getDishItemById(id); return Result.success(list); }
还有Service中
1 2 3 4 5 6 7 8 9 10 11 @Override public List<DishItemVO> getDishItemById (Long id) { List<DishItemVO> list = setmealMapper.getDishItemById(id); return list; }
这里用到了多表联查
1 2 3 @Select("SELECT setmeal_dish.copies,dish.description,dish.image,dish.`name`" + "FROM setmeal_dish LEFT JOIN dish ON setmeal_dish.dish_id = dish.id WHERE setmeal_id = #{id} ") List<DishItemVO> getDishItemById (Long id) ;
8 缓存-购物车 缓存菜品
问题分析 用户端小程序展示的菜品数据都是通过查询数据库获得
,如果用户端访问量较大,数据库访问压力随之增大
结果就是,系统响应慢,用户体验差
实现思路 通过Redis来缓存菜品数据,减少数据库查询操作
就是优先读取缓存数据,有的话就用缓存的,没有再去数据库中查,然后将查到的数据写入缓存中
缓存逻辑分析
小程序端是按照分类展示的菜品,所以我们可以根据分类来进行缓存,一个分类是一份缓存数据
而缓存的key就可以用分类id来表示,value可以用菜品数据的string字符串来保存
还有一点很重要的就是:数据库中的菜品数据有变更时,清理缓存数据
改造上面的userDishController
,这个主要是查询时加入redis,当接口接收到请求,先去redis中获取,如果有就直接返回,没有再去数据库中查询,最后返回
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 @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 = Dish.builder().status(StatusConstant.ENABLE).categoryId(categoryId).build(); list = dishService.listWithFlavor(dish); redisTemplate.opsForValue().set(key, list); return Result.success(list); }
但是数据一致性还没有保证,如果现在更改了数据库中的数据,缓存的数据是不会变的
所以要在更新完数据中的数据之后,马上删除缓存中的数据,或者更新缓存中的数据?也有可能这项数据并不是立即使用的,所以可以不用立即更新,只是删除掉
好像已经猜到了该怎么做了,在修改和添加,起售停售以及删除的地方(好像是,除了查询,都要清空缓存),都要进行清空缓存,应该是用到切面类的
但是呢,目前缓存的地方只有菜品相关的,也就是说,其他接口不用进行缓存相关的操作,所以只需要操作管理端的DishController
,那涉及到的方法就不多了,完全不需要用切面
根据通配符来查找
好多修改的地方,如果不能直接拿到分类id,那就直接全部清空,因为如果没有分类id,还要查询数据库来获取分类id,本来加入缓存就是为了减少数据库的IO操作,这样就有点得不偿失了
更改后的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 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 @RestController @Api(tags = "菜品相关接口") @RequestMapping("/admin/dish") @Slf4j public class DishController { @Autowired private DishService dishService; @Autowired private RedisTemplate redisTemplate; @PostMapping @ApiOperation("新增菜品") public Result save (@RequestBody DishDTO dishDTO) { log.info("新增菜品" ); dishService.saveWithFlavor(dishDTO); cleanCache("dish_" + dishDTO.getCategoryId()); return Result.success(); } @ApiOperation("分页查询菜品") @GetMapping("/page") public Result<PageResult> page (DishPageQueryDTO dishPageQueryDTO) { log.info("菜品分页查询开始" ); PageResult pageResult = dishService.pageQuery(dishPageQueryDTO); return Result.success(pageResult); } @DeleteMapping @ApiOperation("批量删除菜品") public Result deleteBatchDish (@RequestParam List<Long> ids) { log.info("传入的ids: {}" , ids.size()); dishService.deleteBatchDish(ids); cleanCache("dish_*" ); return Result.success(); } private void cleanCache (String pattern) { Set keys = redisTemplate.keys(pattern); redisTemplate.delete(keys); } @PostMapping("status/{status}") @ApiOperation("菜品的起售/停售") public Result startOrStop (@PathVariable Integer status, Long id) { dishService.startOrStop(status, id); cleanCache("dish_*" ); return Result.success(); } @ApiOperation("根据id查询菜品,和对应的口味") @GetMapping("/{id}") public Result<DishVO> getById (@PathVariable Long id) { log.info("根据id查询菜品:{}" , id); DishVO dishVO = dishService.getByIdWithFlavor(id); return Result.success(dishVO); } @PutMapping @ApiOperation("修改菜品") public Result update (@RequestBody DishDTO dishDTO) { log.info("修改菜品:{} " , dishDTO); dishService.updateWithFlavor(dishDTO); cleanCache("dish_*" ); return Result.success(); } @GetMapping("/list") @ApiOperation("根据分类id查询菜品") public Result<List<Dish>> getByCategoryId (Long categoryId) { log.info("根据分类id查询菜品,参数为{}" , categoryId); List<Dish> list = dishService.getByCategoryId(categoryId); return Result.success(list); } }
Spring Cache 可以替代redis?
Spring Cache是一个框架,实现了基于注解
的缓存功能,只需要简单地加一个注解,就能实现缓存功能
Spring Cache提供了一层抽象,底层可以切换不同的缓存实现
导入依赖
1 2 3 4 5 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-cache</artifactId > <version > 2.7.3</version > </dependency >
然后完全不需要配置,这里Spring Cache就使用的是redis作为缓存的底层了,因为前面已经导入过spring-boot-starter-data-redis
常用注解
注解
说明
@EnableCaching
开启缓存注解功能,通常加在启动类上
@Cacheable
在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据,如果没有,调用方法,并将方法的返回值放到缓存中
@CachePut
将方法的返回值放到缓存中
@CacheEvict
将一条或多条数据从缓存中删除
@Cacheable
可以用在小程序端获取数据时,先查有没有缓存数据,然后在进行下一步操作
@CachePut
可以用在管理端,当修改了某些数据时,将修改后的数据放到缓存中
案例 这里是一个很简单的新demo
给启动类上加@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
注解的使用,就是注意key的拼接,写法啥的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @PostMapping @CachePut(cacheNames = "userCache", key = "#user.id") public User save (@RequestBody User user) { userMapper.insert(user); return user; }
@Cacheable
注解,先是根据cacheNames和key进行拼接,得到缓存数据的key,然后在redis中查找是否有该数据,有的话,会直接返回,跳过该接口的执行 ,发现一个小问题,如果不存在这条数据,那么缓存中也会存一个数据,不过为空,好像也没啥问题
1 2 3 4 5 6 7 8 9 10 11 @Cacheable(cacheNames = "userCache",key = "#id") @GetMapping public User getById (Long id) { User user = userMapper.getById(id); return user; }
@CacheEvict
清除缓存,注意清除全部缓存,要用allEntries = true
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @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(); }
缓存套餐
在启动类上加入@EnableCaching
在用户端接口SetmealController
的list方法上加入@Cacheable
注解
这样看也太简单了1 2 3 4 5 6 7 8 9 @GetMapping("/list") @ApiOperation("根据分类id查询套餐") @Cacheable(cacheNames = "setmealCache",key = "#categoryId") public Result<List<Setmeal>> list (Long categoryId) { Setmeal setmeal = Setmeal.builder().status(StatusConstant.ENABLE).categoryId(categoryId).build(); List<Setmeal> list = setmealService.list(setmeal); return Result.success(list); }
在服务端接口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 @PostMapping @ApiOperation("新增套餐") @CacheEvict(cacheNames = "setmealCache", key = "#setmealDTO.categoryId") public Result save (@RequestBody SetmealDTO setmealDTO) {} @PostMapping("status/{status}") @ApiOperation("套餐起售/停售") @CacheEvict(cacheNames = "setmealCache", allEntries = true) public Result startOrStop (@PathVariable Integer status, Long id) {} @PutMapping @ApiOperation("修改套餐") @CacheEvict(cacheNames = "setmealCache", allEntries = true) public Result update (@RequestBody SetmealDTO setmealDTO) {} @DeleteMapping @ApiOperation("批量删除菜品") @CacheEvict(cacheNames = "setmealCache", allEntries = true) public Result deleteBitch (@RequestParam List<Long> ids) {}
缓存翻篇
购物车 添加购物车 购物车表:
添加购物车的两种情况
添加一条菜品的数据,这时要先查询该用户的购物车中是否有该菜品,且口味相同,如果有则直接数量加1
相关的sql:select * from shopping_cart where user_id = ? and dish_id = ? and dish_flavor
添加一条套餐的数据,这时要先查询该用户的购物车中是否有该套餐,如果有则直接数量加1
相关的sql:select * from shopping_cart where user_id = ? and setmeal_id = ?
所以要用动态sql来解决这个问题
ShoppingCartController,前端只需要传来一个有setmeal_id,dish_id,以及dish_flavor
的对象即可,这个dish_flavor
只是一个字符串,包括选中的口味,如微辣
1 2 3 4 5 6 7 @PostMapping("/add") @ApiOperation("添加购物车") public Result add (@RequestBody ShoppingCartDTO shoppingCartDTO) { log.info("添加购物车:{}" , shoppingCartDTO); shoppingCartService.addShoppingCard(shoppingCartDTO); return Result.success(); }
Service,虽然代码长,但是逻辑不是很难,就是先判断这条新加的数据是否在购物车中出现过,如果有就加数量,没有就是新增一条数据,而封装这条购物车数据需要用到name和image等,这些字段需要查dish或者setmeal表,所以要判断这条数据是菜品还是套餐,再进一步封装数据
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 @Override public void addShoppingCard (ShoppingCartDTO shoppingCartDTO) { ShoppingCart shoppingCart = new ShoppingCart (); BeanUtils.copyProperties(shoppingCartDTO, shoppingCart); Long userId = BaseContextByMe.getCurrentId(); shoppingCart.setUserId(userId); List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart); if (list != null && list.size() > 0 ) { ShoppingCart cart = list.get(0 ); cart.setNumber(cart.getNumber() + 1 ); shoppingCartMapper.updateNumberById(cart); } else { Long dishId = shoppingCart.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(shoppingCart.getSetmealId()); shoppingCart.setName(setmeal.getName()); shoppingCart.setImage(setmeal.getImage()); shoppingCart.setAmount(setmeal.getPrice()); } shoppingCart.setCreateTime(LocalDateTime.now()); shoppingCart.setNumber(1 ); shoppingCartMapper.insert(shoppingCart); } }
Mapper
1 2 3 4 5 6 7 8 @Insert("insert into shopping_cart(name, image, user_id, dish_id, setmeal_id, dish_flavor, amount, create_time) " + "values (#{name},#{image},#{userId},#{dishId},#{setmealId},#{dishFlavor},#{amount},#{createTime})") void insert (ShoppingCart shoppingCart) ;
动态sql
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <select id ="list" resultType ="com.sky.entity.ShoppingCart" > select * from shopping_cart <where > <if test ="userId != null" > 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" > and dish_flavor = #{dishFlavor} </if > </where > </select >
查询购物车 这个接口也没啥难的
Controller,直接调用service
1 2 3 4 5 6 7 8 9 10 11 @ApiOperation("查询购物车") @GetMapping("/list") public Result<List<ShoppingCart>> list () { List<ShoppingCart> list = shoppingCartService.getByUserId(); return Result.success(list); }
Service,Service中调用上一个接口用到的list方法,就是动态sql查询购物车,但是由于只传入了一个userId,所以可以查询到该用户的所有购物车数据
1 2 3 4 5 6 7 8 9 10 11 12 13 @Override public List<ShoppingCart> getByUserId () { Long userId = BaseContextByMe.getCurrentId(); ShoppingCart shoppingCart = ShoppingCart.builder().userId(userId).build(); List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart); return list; }
清空购物车
Controller,这个就更简单了,甚至不需要前端传来任何的数据,只需要从线程中取出userId即可
1 2 3 4 5 6 @ApiOperation("清空购物车") @DeleteMapping("/clean") public Result cleanCart () { shoppingCartService.cleanCart(); return Result.success(); }
Service
1 2 3 4 5 6 @Override public void cleanCart () { Long userId = BaseContextByMe.getCurrentId(); shoppingCartMapper.deleteByUserId(userId); }
删除一项
Controller,依然是使用shoppingCartDTO来实现传参
1 2 3 4 5 6 7 8 9 10 11 12 13 @ApiOperation("删除购物车的一个商品") @PostMapping("/sub") public Result subOne (@RequestBody ShoppingCartDTO shoppingCartDTO) { log.info("删除购物车的一个商品:{}" , shoppingCartDTO); shoppingCartService.subShoppingCart(shoppingCartDTO); return Result.success(); }
Service,也没啥难得,就是要判断这条数据的number,如果大于1则是–,如果为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 @Override public void subShoppingCart (ShoppingCartDTO shoppingCartDTO) { ShoppingCart shoppingCart = new ShoppingCart (); BeanUtils.copyProperties(shoppingCartDTO, shoppingCart); Long userId = BaseContextByMe.getCurrentId(); shoppingCart.setUserId(userId); List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart); if (list != null && list.size() > 0 ) { ShoppingCart cart = list.get(0 ); if (cart.getNumber() == 1 ) { shoppingCartMapper.deleteByCartId(cart); } else { cart.setNumber(cart.getNumber() - 1 ); shoppingCartMapper.updateNumberById(cart); } } }
测试也没问题啊没问题
9 用户下单-订单支付 导入地址簿 ?
看看接口敲一遍得了
保存地址簿 新增地址
Controller
1 2 3 4 5 6 7 @ApiOperation("新增地址") @PostMapping public Result add (@RequestBody AddressBook addressBook) { log.info("新增地址:{}" , addressBook); addressBookService.add(addressBook); return Result.success(); }
Service,主要就是注意这里的业务逻辑-设置是否默认-0,还有设置用户id
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Override public void add (AddressBook addressBook) { Long userId = BaseContextByMe.getCurrentId(); addressBook.setIsDefault(0 ); addressBook.setUserId(userId); addressBookMapper.insert(addressBook); }
Mapper
1 2 3 4 5 6 @Insert("insert into address_book(user_id, consignee, sex, phone, province_code, province_name, city_code, " + "city_name, district_code, district_name, detail, label,is_default) VALUES (#{userId},#{consignee}," + "#{sex},#{phone}," + "#{provinceCode},#{provinceName},#{cityCode},#{cityName},#{districtCode},#{districtName},#{detail}," + "#{label},#{isDefault})") void insert (AddressBook addressBook) ;
地址簿列表 查询当前登录用户的所有地址信息
Controller
1 2 3 4 5 6 @ApiOperation("查询登录用户的所有地址") @GetMapping("/list") public Result<List<AddressBook>> getByUserId () { List<AddressBook> list = addressBookService.list(); return Result.success(list); }
Service
1 2 3 4 5 6 7 8 9 10 11 @Override public List<AddressBook> list () { Long userId = BaseContextByMe.getCurrentId(); List<AddressBook> list = addressBookMapper.list(userId); return list; }
Mapper
1 2 @Select("select * from address_book where user_id = #{userId}") List<AddressBook> list (Long userId) ;
修改地址簿
根据id查询地址
Controller
1 2 3 4 5 6 @ApiOperation("根据id查询地址") @GetMapping("/{id}") public Result<AddressBook> getById (@PathVariable Long id) { AddressBook addressBook = addressBookService.getById(id); return Result.success(addressBook); }
Service
1 2 3 4 5 6 7 8 9 10 11 @Override public AddressBook getById (Long id) { AddressBook addressBook = addressBookMapper.getById(id); return addressBook; }
Mapper
1 2 @Select("select * from address_book where id = #{id}") AddressBook getById (Long id) ;
修改
Controller
1 2 3 4 5 6 7 @ApiOperation("修改地址") @PutMapping public Result update (@RequestBody AddressBook addressBook) { log.info("修改地址:{}" , addressBook); addressBookService.update(addressBook); return Result.success(); }
Service
1 2 3 4 5 6 7 8 9 @Override public void update (AddressBook addressBook) { addressBookMapper.update(addressBook); }
Mapper,这个跟老师写的不太一样,但效果差不多
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 <update id ="update" > 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 ="provinceCode != null" > province_code = #{provinceCode}, </if > <if test ="provinceName != null" > province_name = #{provinceName}, </if > <if test ="cityCode != null" > city_code = #{cityCode}, </if > <if test ="cityName != null" > city_name = #{cityName}, </if > <if test ="districtCode != null" > district_code = #{districtCode}, </if > <if test ="districtName != null" > district_name = #{districtName}, </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 >
设置默认地址
Controller
1 2 3 4 5 6 7 @ApiOperation("设置默认地址") @PutMapping("/default") public Result setDefault (@RequestBody AddressBook addressBook) { addressBookService.setDefault(addressBook); return Result.success(); }
Service,这个是真的离谱,正着写要好多行,反着写就两行..,正着写就是先将目标地址置为默认,然后将其余的地址置为非默认,反着写就是先把所有的地址都设置为非默认,然后直接设置目标地址为默认即可
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 setDefault (AddressBook addressBook) { addressBookMapper.updateAllAddressByUserId(BaseContextByMe.getCurrentId()); addressBookMapper.setDefault(addressBook.getId()); }
查询默认地址
Controller
1 2 3 4 5 6 @ApiOperation("查询默认地址") @GetMapping("/default") public Result<AddressBook> getDefault () { AddressBook addressBook = addressBookService.getDefault(); return Result.success(addressBook); }
Service,这里也是和老师写的不太一样的地方,老师是查不到直接返回查不到的信息,我这里是查不到就默认第一个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Override public AddressBook getDefault () { Long userId = BaseContextByMe.getCurrentId(); AddressBook addressBook = addressBookMapper.getDefault(userId); if (addressBook == null ) { List<AddressBook> list = addressBookMapper.list(userId); addressBook = list.get(0 ); } return addressBook; }
删除地址簿 根据id删除地址
这个就没什么说的了,直接删就ok
1 2 3 4 5 6 @ApiOperation("根据id删除地址") @DeleteMapping public Result deleteById (Long id) { addressBookService.deleteById(id); return Result.success(); }
用户下单 用户下单业务说明 在电商系统中,用户是通过下单的方式通知商家,用户已经购买了商品,需要商家进行备货和发货
用户下单后会产生订单相关数据,订单数据需要能够体现如下信息
买的商品和数量
收货地址
订单总金额
哪个用户下的单
手机号多少
用户下单流程:
订单相关的数据库设计
Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 @PostMapping("/submit") @ApiOperation("下单") public Result<OrderSubmitVO> submit (@RequestBody OrdersSubmitDTO ordersSubmitDTO) { log.info("下单的数据:{}" , ordersSubmitDTO); OrderSubmitVO orderSubmitVO = orderService.submitOrder(ordersSubmitDTO); return Result.success(orderSubmitVO); }
Service,虽然代码多,但是并不难
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 @Override @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 = BaseContextByMe.getCurrentId(); List<ShoppingCart> shoppingCartList = shoppingCartMapper.getByUserId(userId); if (shoppingCartList == null || shoppingCartList.size() == 0 ) { throw new ShoppingCartBusinessException (MessageConstant.SHOPPING_CART_IS_NULL); } Orders orders = new Orders (); BeanUtils.copyProperties(ordersSubmitDTO, orders); orders.setOrderTime(LocalDateTime.now()); orders.setPayStatus(Orders.UN_PAID); orders.setStatus(Orders.PENDING_PAYMENT); orders.setNumber(String.valueOf(System.currentTimeMillis())); orders.setPhone(addressBook.getPhone()); orders.setConsignee(addressBook.getConsignee()); orders.setUserId(userId); orderMapper.insert(orders); List<OrderDetail> orderDetailList = new ArrayList <>(); for (ShoppingCart shoppingCart : shoppingCartList) { OrderDetail orderDetail = new OrderDetail (); BeanUtils.copyProperties(shoppingCart, orderDetail); orderDetail.setOrderId(orders.getId()); orderDetailList.add(orderDetail); } orderDetailMapper.insert(orderDetailList); shoppingCartMapper.deleteByUserId(userId); OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder().id(orders.getId()).orderTime(orders.getOrderTime()).orderNumber(orders.getNumber()).orderAmount(orders.getAmount()).build(); return orderSubmitVO; }
Mapper,整体都不难
1 2 3 4 5 6 7 8 9 10 11 <insert id ="insert" useGeneratedKeys ="true" keyProperty ="id" > insert into orders(number, status, user_id, address_book_id, order_time, checkout_time, pay_method, pay_status, amount, remark, phone, address, user_name, consignee, cancel_reason, rejection_reason, cancel_time, estimated_delivery_time, delivery_status, delivery_time, pack_amount, tableware_number, tableware_status) VALUES (#{number}, #{status}, #{userId}, #{addressBookId}, #{orderTime}, #{checkoutTime}, #{payMethod}, #{payStatus}, #{amount}, #{remark}, #{phone}, #{address}, #{userName}, #{consignee}, #{cancelReason}, #{rejectionReason}, #{cancelTime}, #{estimatedDeliveryTime}, #{deliveryStatus}, #{deliveryTime}, #{packAmount}, #{tablewareNumber}, #{tablewareStatus}) </insert >
订单支付 这个支付是实现不了的,但是可以了解流程,阅读文档
微信支付介绍 整体的流程还是挺复杂的.
JSPI 下单:商户系统调用该接口在微信支付服务后台生成预支付交易单
这块是真做不了,直接点击支付就支付成功得了
内网穿透 不管别的,先把内网穿透啥的搞好再说,之前用的是linux版的,这次试试win版
打开软件安装目录
没啥了,直接执行cpolar.exe http 8080
,就能给本机的8080端口一个公网的域名
能够直接进行访问接口
代码还导入吗? 不用了吧,导入了别到时候全G了
前端发请求就直接返回success,然后改变支付状态得了
看一下代码,然后了解一下流程即可
哈哈哈哈哈哈,以前的感觉又回来了…
?
还是有问题,现在前端需要数据,但是那些数据根本获取不到
要不就是直接更改订单状态?
试试
好了,反正现在能改变订单的状态了,其他就不管了
10 订单管理 这个模块又是自己写的
要完成用户端历史订单模块,商家端订单管理模块
还要对之前的功能进行优化
用户端历史订单模块
查询历史订单
Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 @GetMapping("/historyOrders") @ApiOperation("历史订单分页查询") public Result<PageResult> getHistoryOrders (OrdersPageQueryDTO ordersPageQueryDTO) { log.info("订单分页查询" ); PageResult pageResult = orderService.pageQuery(ordersPageQueryDTO); return Result.success(pageResult); }
Service,这里就比较奇怪了,当时没看明白OrderVO是干嘛的,后来才发现是继承的Orders,可以简单理解为OrderVO是order和orderDetail的结合体!
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 PageResult pageQuery (OrdersPageQueryDTO ordersPageQueryDTO) { PageHelper.startPage(ordersPageQueryDTO.getPage(), ordersPageQueryDTO.getPageSize()); ordersPageQueryDTO.setUserId(BaseContextByMe.getCurrentId()); Page<Orders> page = orderMapper.list(ordersPageQueryDTO); List<OrderVO> list = new ArrayList <>(); if (page != null && page.size() > 0 ) { for (Orders orders : page) { Long ordersId = orders.getId(); List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(ordersId); OrderVO orderVO = new OrderVO (); BeanUtils.copyProperties(orders, orderVO); orderVO.setOrderDetailList(orderDetailList); list.add(orderVO); } } return new PageResult (page.getTotal(), list); }
查询订单详情
Controller
1 2 3 4 5 6 7 8 9 @GetMapping("/orderDetail/{id}") @ApiOperation("订单详情") public Result<OrderVO> getOrderDetail (@PathVariable Long id) { OrderVO orderVO = orderService.getOrderDetail(id); return Result.success(orderVO); }
Service,这里更能看出,orderVO是继承的orders
1 2 3 4 5 6 7 8 9 10 11 12 @Override public OrderVO getOrderDetail (Long id) { Orders orders = orderMapper.getById(id); OrderVO orderVO = new OrderVO (); BeanUtils.copyProperties(orders, orderVO); List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id); orderVO.setOrderDetailList(orderDetailList); return orderVO; }
取消订单
Controller
1 2 3 4 5 6 7 @ApiOperation("取消订单") @PutMapping("/cancel/{id}") public Result cancel (@PathVariable Long id) { log.info("取消订单id为:{}" , id); orderService.userCancelById(id); return Result.success(); }
Service,原本是要取消后退款来着,但是现在很明显完成不了.
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 @Override public void userCancelById (Long id) { Orders orders = orderMapper.getById(id); if (orders == null ) { throw new OrderBusinessException (MessageConstant.ORDER_NOT_FOUND); } if (orders.getStatus() > 2 ) { throw new OrderBusinessException (MessageConstant.ORDER_STATUS_ERROR); } Orders cancelOrder = Orders.builder().id(id).status(Orders.CANCELLED) .cancelReason("用户取消" ).cancelTime(LocalDateTime.now()).build(); orderMapper.update(cancelOrder); }
再来一单
Controller
1 2 3 4 5 6 7 8 9 10 11 12 @PostMapping("/repetition/{id}") @ApiOperation("再来一单") public Result repetitionOrder (@PathVariable Long id) { log.info("要重新下单的订单id:{}" , id); orderService.repetitionOrder(id); return Result.success(); }
Service,这个跟老师写的也不太一样,但是区别不大
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 repetitionOrder (Long id) { List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id); for (OrderDetail orderDetail : orderDetailList) { ShoppingCart shoppingCart = new ShoppingCart (); BeanUtils.copyProperties(orderDetail, shoppingCart); shoppingCart.setUserId(BaseContextByMe.getCurrentId()); shoppingCart.setCreateTime(LocalDateTime.now()); shoppingCartMapper.insert(shoppingCart); } }
用户端结束
商家端订单管理模块
订单搜索
或者说是管理端
业务规则
输入订单号/手机号进行搜索,支持模糊搜索
根据订单状态搜索
下单时间进行时间筛选
搜索内容为空,提示未找到相关订单
搜索结果页,展示包含搜索关键词的内容
分页展示搜索到的订单数据
Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @GetMapping("/conditionSearch") @ApiOperation("订单搜索") public Result<PageResult> conditionSearchOrder (OrdersPageQueryDTO ordersPageQueryDTO) { PageResult pageResult = orderService.conditionSearch(ordersPageQueryDTO); return Result.success(pageResult); }
Service,这个Service主要还是封装OrderVO信息,以及拼接菜品的信息,比如娃娃菜*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 41 42 43 @Override public PageResult conditionSearch (OrdersPageQueryDTO ordersPageQueryDTO) { PageHelper.startPage(ordersPageQueryDTO.getPage(), ordersPageQueryDTO.getPageSize()); Page<Orders> page = orderMapper.list(ordersPageQueryDTO); List<OrderVO> list = new ArrayList <>(); if (page != null && page.size() > 0 ) { for (Orders orders : page) { OrderVO orderVO = new OrderVO (); BeanUtils.copyProperties(orders, orderVO); orderVO.setOrderDishes(getOrderDishes(orders)); list.add(orderVO); } } return new PageResult (page.getTotal(), list); } private String getOrderDishes (Orders orders) { List<OrderDetail> orderDetails = orderDetailMapper.getByOrderId(orders.getId()); StringBuffer stringBuffer = new StringBuffer (); for (OrderDetail orderDetail : orderDetails) { String orderDish = orderDetail.getName() + "*" + orderDetail.getNumber() + ";" ; stringBuffer.append(orderDish); } return stringBuffer.toString(); }
各个状态的订单数量统计
Controller
1 2 3 4 5 6 7 8 9 10 11 12 @GetMapping("/statistics") @ApiOperation("各个状态的订单数量统计") public Result<OrderStatisticsVO> statistics () { OrderStatisticsVO orderStatisticsVO = orderService.statistics(); return Result.success(orderStatisticsVO); }
Service,直接查数量然后赋值返回,比我自己写的那样简单多了
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 @Override 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
1 2 3 4 5 6 7 @Select("select count(id) from orders where status = #{status} ") Integer countStatus (Integer toBeConfirmed) ;
查询订单详情
业务规则 - 订单详情页面需要展示订单基本信息(状态,订单号,下单时间,收货人,电话,收货地址,金额等) - 订单详情页面需要展示订单明细数据(商品名称,数量,单价)
Controller,这个是之前用户端的业务,直接调用对应的service即可
1 2 3 4 5 6 7 8 9 10 11 12 @GetMapping("/details/{id}") @ApiOperation("查询订单详情") public Result<OrderVO> details (@PathVariable Long id) { OrderVO orderVO = orderService.getOrderDetail(id); return Result.success(orderVO); }
接单
我是没想到这个id竟然还用一个DTO来封装
Controller
1 2 3 4 5 6 @PutMapping("/confirm") @ApiOperation("接单") public Result confirm (@RequestBody OrdersConfirmDTO ordersConfirmDTO) { orderService.confirm(ordersConfirmDTO); return Result.success(); }
Service,从这一步就能看出来,根本不需要封装这个DTO,直接用id就可以了.
1 2 3 4 5 6 7 8 9 10 @Override public void confirm (OrdersConfirmDTO ordersConfirmDTO) { Orders orders = Orders.builder().id(ordersConfirmDTO.getId()).status(Orders.CONFIRMED).build(); orderMapper.update(orders); }
拒单
这个又忘了所谓的业务规则了 - 商家拒单就是将订单状态设置为已取消 - 只有当订单处于待接单的状态是可以执行拒单操作 - 需要指定原因 - 如果已完成支付,要退款?
Controller
1 2 3 4 5 6 7 @PutMapping("/rejection") @ApiOperation("拒单") public Result rejection (@RequestBody OrdersRejectionDTO ordersRejectionDTO) { orderService.rejection(ordersRejectionDTO); return Result.success(); }
Service 这个跟老师写的也不一样,老师写的还要判断状态什么的,然后还要进行退款..
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override public void rejection (OrdersRejectionDTO ordersRejectionDTO) { Integer status = orderMapper.getById(ordersRejectionDTO.getId()).getStatus(); log.info("当前状态为:{}" , status); Orders orders = Orders.builder().id(ordersRejectionDTO.getId()).status(Orders.CANCELLED).cancelTime(LocalDateTime.now()) .rejectionReason(ordersRejectionDTO.getRejectionReason()).build(); orderMapper.update(orders); }
取消订单
取消订单,就是设置订单状态为已取消
商家取消订单需要指定取消原因 如果用户已经付款,则要进行退款
Controller
1 2 3 4 5 6 @PutMapping("/cancel") @ApiOperation("取消订单") public Result cancel (@RequestBody OrdersCancelDTO ordersCancelDTO) { orderService.adminCancel(ordersCancelDTO); return Result.success(); }
Service,忘了还有一个payStatus字段了,可以根据这个字段来进行判断是否已经支付
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override public void adminCancel (OrdersCancelDTO ordersCancelDTO) { Orders ordersDB = orderMapper.getById(ordersCancelDTO.getId()); if (ordersDB.getPayStatus() == 1 ) { log.info("退款中...." ); } Orders orders = Orders.builder().id(ordersCancelDTO.getId()).cancelTime(LocalDateTime.now()) .cancelReason(ordersCancelDTO.getCancelReason()).status(Orders.CANCELLED).build(); orderMapper.update(orders); }
派送订单
业务规则 - 派送订单其实就是将订单状态修改为“派送中” - 只有状态为“待派送”的订单可以执行派送订单操作
Controller
1 2 3 4 5 6 7 @PutMapping("/delivery/{id}") @ApiOperation("派送订单") public Result delivery (@PathVariable Long id) { log.info("派送订单的id:{}" , id); orderService.delivery(id); return Result.success(); }
Service,主要就是注意状态
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Override public void delivery (Long id) { if (orderMapper.getById(id).getStatus() != Orders.CONFIRMED) { throw new OrderBusinessException ("订单状态错误" ); } Orders orders = Orders.builder().id(id).status(Orders.DELIVERY_IN_PROGRESS).build(); orderMapper.update(orders); }
完成订单
Controller
1 2 3 4 5 6 7 @PutMapping("/complete/{id}") @ApiOperation("完成订单") public Result complete (@PathVariable Long id) { orderService.complete(id); return Result.success(); }
Service,注意状态即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Override public void complete (Long id) { Orders orderDB = orderMapper.getById(id); if (orderDB == null || orderDB.getStatus() != Orders.DELIVERY_IN_PROGRESS) { throw new OrderBusinessException ("订单状态错误" ); } Orders orders = Orders.builder().id(id).status(Orders.COMPLETED).build(); orderMapper.update(orders); }
接口这块是完成了…
还有优化啥的,看看怎么做
百度地图AK:cXa9RUd5ZOkF7zcvBxAXFGm12PRLVGvP
配置文件:
1 2 3 4 shop: address: 北京市海淀区上地十街10号 baidu: ak: cXa9RUd5ZOkF7zcvBxAXFGm12PRLVGvP
在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 64 65 66 67 68 69 70 @Value("${sky.shop.address}") private String shopAddress; @Value("${sky.baidu.ak}") private String ak; 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 ("超出配送范围" ); } }
在用户下单的地方加入
1 2 checkOutOfRange(addressBook.getCityName() + addressBook.getDistrictName() + addressBook.getDetail());
那么这段代码,需不需要记下来呢?
嗯,先到这吧
11 订单状态定时处理-来单提醒和客户催单 两种提示
支付超时的订单如何处理
派送中的订单一直不点击完成如何处理
Spring Task 消息?
Spring Task
是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑
定时任务框架
作用:定时自动执行某段代码
cron表达式 cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间
构成规则:分成6或7个域,由空格分隔开,每个域代表一个含义
每个域的含义分别为:秒,分钟,小时,日,月,周,年(可选)
描述一个时间:2022年10月12日上午9点整
,对应的cron表达式为日和周只能写一个
但是有些表达式是不能直接描述的,需要一些符号
所以可以使用corn在线生成器
入门案例
导入maven坐标:spring-context
(已存在)
启动类上添加注解:@EnableScheduling
开启任务调度
自定义定时任务类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @Component @Slf4j public class MyTask { @Scheduled(cron = "0/5 * * * * ?") public void executeTask () { log.info("定时任务开始执行:{}" , new Date ()); } }
就会每隔5秒自动调用一次
订单状态定时处理 用户下单后可能存在的情况
下单后未支付,订单一直处于待支付
状态
用户收货后管理端未点完成按钮,订单一直处于派送中
状态
对于上面两种情况需要通过定时任务
来修改订单状态,具体逻辑为
通过定时任务每分钟检查一次
是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为已取消
通过定时任务每天凌晨1点检查一次
是否存在派送中
的订单,如果存在则修改订单状态为已完成
OrderTask.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 package com.sky.task;import com.sky.entity.Orders;import com.sky.mapper.OrderMapper;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.stereotype.Component;import java.time.LocalDateTime;import java.util.List;@Component @Slf4j public class OrderTask { @Autowired private OrderMapper orderMapper; @Scheduled(cron = "0 * * * * ? ") public void processTimeoutOrder () { log.info("定时处理超时订单:{}" , LocalDateTime.now()); List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, LocalDateTime.now().minusMinutes(15 )); if (ordersList != null && ordersList.size() > 0 ) { for (Orders orders : ordersList) { orders.setStatus(Orders.CANCELLED); orders.setCancelReason("订单超时,自动取消" ); orders.setCancelTime(LocalDateTime.now()); orderMapper.update(orders); } } } @Scheduled(cron = "0 0 1 * * ?") public void processDeliveryOrder () { log.info("定时处理一直处于派送中的订单:{}" , LocalDateTime.now()); List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, LocalDateTime.now().minusHours(1 )); if (ordersList != null && ordersList.size() > 0 ) { for (Orders orders : ordersList) { orders.setStatus(Orders.COMPLETED); orderMapper.update(orders); } } } }
Mapper
1 2 3 4 5 6 7 8 9 10 @Select("select * from orders where status = #{status} and order_time < #{orderTime}") List<Orders> getByStatusAndOrderTimeLT (Integer status, LocalDateTime orderTime) ;
测试也没问题啊没问题
WebSocket WebSocket是基于TCP的一种新的网络协议,它实现了浏览器与服务器全双工通信-浏览器和服务器只需要完成一次握手
,两者之间就可以创建持久性连接
,并进行双向
数据传输
那这个WebSocket和Http协议有什么区别呢?
哦,原来是长连接?握一次手,然后可以双向通讯,也就是说服务器可以主动请求浏览器?
底层二者都是TCP连接
案例 实现步骤
maven依赖1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-websocket</artifactId > </dependency >
直接使用websocket.html页面作为WebSocket客户端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 >
导入WebSocket服务端组件WebSocketServer,用于和客户端通讯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 package com.sky.websocket;import org.springframework.stereotype.Component;import javax.websocket.OnClose;import javax.websocket.OnMessage;import javax.websocket.OnOpen;import javax.websocket.Session;import javax.websocket.server.PathParam;import javax.websocket.server.ServerEndpoint;import java.util.Collection;import java.util.HashMap;import java.util.Map;@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(); } } } }
导入配置类WebSocketConfiguration,注册WebSocket的服务端组件1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.sky.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.web.socket.server.standard.ServerEndpointExporter;@Configuration public class WebSocketConfiguration { @Bean public ServerEndpointExporter serverEndpointExporter () { return new ServerEndpointExporter (); } }
导入定时任务类WebSocketTask,定时向客户端推送数据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实现管理端页面和服务端保持长连接状态
当客户支付后,调用WebSocket相关的API实现服务端向客户端推送消息
客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户端催单,进行相应的消息提示和语音播报
约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,contetn
type
为消息类型,1为来单提醒,2为客户催单
orderId
为订单id
content
为消息内容
在导入上面的WebSocketServer
后,这时运行前端就已经可以进行WebSocket连接了
但是有没有发现,前端请求是ws://localhost/ws/nigh7mb27z
,也就是80端口,而后端相当于是ws://localhost:8080/ws/{sid}
这里就是Nginx的反向代理:
1 2 3 4 5 6 7 8 # WebSocket location /ws/ { proxy_pass http://webservers/ws/; proxy_http_version 1.1; proxy_read_timeout 3600s; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "$connection_upgrade"; }
当前端服务请求到nginx上的ws://localhost/ws/xxx
就会转发到后端服务器上的ws://localhost:8080/ws/xxx
这里又要和老师写的不太一样了,因为这个老师是将这个提醒放在了支付成功的回调中
,而这个回调是做不了的,所以我直接放到自己写的模拟支付的接口中就行了
更改OrderServiceImpl
的paymentWithNoMoney
,就是在下面加上几行代码就行了
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 @Override public void paymentWithNoMoney (OrdersPaymentDTO ordersPaymentDTO) { String orderNumber = ordersPaymentDTO.getOrderNumber(); LocalDateTime checkoutTime = LocalDateTime.now(); orderMapper.changeStatusAndCheckoutTime(orderNumber, checkoutTime); Orders orders = orderMapper.getByNumber(ordersPaymentDTO.getOrderNumber()); Map map = new HashMap <>(); map.put("type" , 1 ); map.put("orderId" , orders.getId()); map.put("content" , "订单号:" + orderNumber); String json = JSON.toJSONString(map); webSocketServer.sendToAllClient(json); }
效果没问题
如果不点击提示,就会一直响!
客户催单
Controller
1 2 3 4 5 6 7 @GetMapping("/reminder/{id}") @ApiOperation("用户催单") public Result reminder (@PathVariable Long id) { orderService.reminder(id); return Result.success(); }
Service
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 @Override public void reminder (Long id) { Orders orderDB = orderMapper.getById(id); if (orderDB == null ) { throw new OrderBusinessException (MessageConstant.ORDER_STATUS_ERROR); } String number = orderDB.getNumber(); Map map = new HashMap (); map.put("type" , 2 ); map.put("orderId" , id); map.put("content" , "用户催单:" + number); webSocketServer.sendToAllClient(JSON.toJSONString(map)); }
总体来看也没啥难的.
没问题
12 数据统计 Apache ECharts Apache ECharts是一款基于JS的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表
官网
直接看快速开始就知道怎么简单的使用了
重点在于研究某个图表所需要的数据格式
,通常是需要后端提供符合格式要求的动态数据,然后相应给前端来展示图标
嗯..好像可以用这个echarts改进一下那个脱裤子放屁的东西
营业额统计 产品原型:一个折线图
所以就要获取每天的营业额 ,而日期要根据时间选择器来决定
业务规则
营业额指订单状态为已完成的订单金额合计
基于可视化报表的折线图展示营业额数据,x轴为日期,y轴为营业额
根据时间选择区间,展示每天的营业额数据
前端提交开始日期和结束日期
返回的数据应包括x轴的日期和y轴的营业额数据
Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @GetMapping("/turnoverStatistics") @ApiOperation("营业额统计") public Result<TurnoverReportVO> turnoverStatistics (@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) { log.info("营业额数据统计:{},{}" , begin, end); TurnoverReportVO reportVO = reportService.turnoverStatistics(begin, end); return Result.success(reportVO); }
Service,果然重头戏都在service中,这里涉及到日期的叠加,有LocalDateTime.of(LocalDate)
这种类型转换,使用LocalTime.MAX
来获取当天的最后一时刻,还有StringUtils.join(dateList, ",")
来获取集合的每一项,然后用逗号分隔,拼成一个字符串
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 @Override public TurnoverReportVO turnoverStatistics (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 localDate : dateList) { LocalDateTime beginTime = LocalDateTime.of(localDate, LocalTime.MIN); LocalDateTime endTime = LocalDateTime.of(localDate, LocalTime.MAX); Map map = new HashMap (); map.put("begin" , beginTime); map.put("end" , endTime); map.put("status" , Orders.COMPLETED); 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
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 >
响应结果:
就是数据有点少
用户统计 业务规则
基于可视化报表的折线图展示营业额数据,x轴为日期,y轴为用户数量
根据时间选择区间,展示每天的用户总量和新增用户量数据
这里的日期选择器是全局的,会同时改变营业额统计,用户统计,订单统计,销量排名Top10
的数据
Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @GetMapping("/userStatistics") @ApiOperation("用户统计") public Result<UserReportVO> userStatistics (@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) { log.info("用户统计:{},{}" , begin, end); UserReportVO userReportVO = reportService.userStatistics(begin, end); return Result.success(userReportVO); }
Service,跟上面的唯一区别就是多了一个集合,然后封装集合的方式不太一样,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 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 @Override public UserReportVO userStatistics (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 localDate : dateList) { LocalDateTime beginTime = LocalDateTime.of(localDate, LocalTime.MIN); LocalDateTime endTime = LocalDateTime.of(localDate, LocalTime.MAX); Map map = new HashMap (); map.put("end" , endTime); totalUserList.add(userMapper.countByMap(map)); map.put("begin" , beginTime); newUserList.add(userMapper.countByMap(map)); } return UserReportVO.builder() .dateList(StringUtils.join(dateList, "," )) .newUserList(StringUtils.join(newUserList, "," )) .totalUserList(StringUtils.join(totalUserList, "," )) .build(); }
Mapper
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 ="end != null" > and create_time < #{end} </if > <if test ="begin != null" > and create_time > #{begin} </if > </where > </select >
订单统计 这几个接口说实话都不难.
产品原型:
业务规则
状态为已完成的是有效订单
x轴依然是日期
根据时间选择区间,展示每天的订单总数和有效订单数
展示所选时间区间内的有效订单数,总订单数,订单完成率,订单完成率=有效订单数/总订单数*100%
提取了一个公用的方法,用来根据前端传来的日期区间来获取日期列表
1 2 3 4 5 6 7 8 9 10 private static List<LocalDate> getLocalDateList (LocalDate begin, LocalDate end) { List<LocalDate> dateList = new ArrayList <>(); dateList.add(begin); while (!begin.equals(end)) { begin = begin.plusDays(1 ); dateList.add(begin); } return dateList; }
Controller
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @GetMapping("/ordersStatistics") @ApiOperation("订单统计") public Result<OrderReportVO> orderStatistics (@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) { log.info("订单统计:{},{}" , begin, end); OrderReportVO orderReportVO = reportService.orderStatistics(begin, end); return Result.success(orderReportVO); }
Service,刚开始写的是没有提取出一个方法来查询数据库的,所以是自己封装map来实现的条件,而新的是定义一个方法,传入三个参数(begin,end,status),然后在方法中定义map,最后查询时能根据传入的参数进行动态查询,这样能少些好多行代码哎 ,而且总数量那两个是不要用查数据库的.
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 @Override public OrderReportVO orderStatistics (LocalDate begin, LocalDate end) { List<LocalDate> dateList = getLocalDateList(begin, end); List<Integer> orderCountList = new ArrayList <>(); List<Integer> validOrderCountList = new ArrayList <>(); for (LocalDate localDate : dateList) { LocalDateTime beginTime = LocalDateTime.of(localDate, LocalTime.MIN); LocalDateTime endTime = LocalDateTime.of(localDate, LocalTime.MAX); orderCountList.add(getOrderCount(beginTime, endTime, null )); validOrderCountList.add(getOrderCount(beginTime, endTime, Orders.COMPLETED)); } 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 / totalOrderCount.doubleValue()); } return OrderReportVO.builder() .dateList(StringUtils.join(dateList, "," )) .totalOrderCount(totalOrderCount) .validOrderCount(validOrderCount) .orderCountList(StringUtils.join(orderCountList, "," )) .validOrderCountList(StringUtils.join(validOrderCountList, "," )) .orderCompletionRate(orderCompletionRate) .build(); } private Integer getOrderCount (LocalDateTime beginTime, LocalDateTime endTime, Integer status) { Map timeMap = new HashMap (); timeMap.put("begin" , beginTime); timeMap.put("end" , endTime); timeMap.put("status" , status); return orderMapper.countOrderByMap(timeMap); }
销量排名Top10 销量排名,这个东西
业务规则
根据时间选择区间,展示销量前10的商品(包括菜品和套餐),这个和之前的时间选择也差不多
基于可视化报表的柱状图降序展示商品销量
此处的销量为商品销售的份数
销量怎么算?
在order_detail
表中根据dish_id
和setmeal_id
来算
这样的sql?select count(id) from order_detail where dish_id = ?
的确能查,但是这么多菜品呢,总不能全部查一遍吧?,而且要的是菜品的名字和数量
而且还不对,因为order_detail
表中还有一个字段是number
,表示数量
!而且,还有就是订单如果是未完成的,那么这个数据也需要进行筛选
最终的实现方式:通过一个多表联查,同时查order和order_detail两个表,查询名称和对应的数量,od.name,sum(od.number) number
,然后通过group by
进行分组,得到的是一个泛型为GoodsSalesDTO
的集合,这个GoodsSalesDTO有两个字段,分别是String类型的name和Integer类型的number,刚好可以封装查出来的数据,又因为有很多的条,所以是一个集合,最后取出集合中的name和number,进行拼接字符串
Controller
1 2 3 4 5 6 7 8 @GetMapping("/top10") @ApiOperation("销量排名") public Result<SalesTop10ReportVO> getTop10 (@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) { log.info("销量排名:{},{}" , begin, end); SalesTop10ReportVO salesTop10ReportVO = reportService.getTop10(begin, end); return Result.success(salesTop10ReportVO); }
Service,这个Stream流看起来就是很好用啊.
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 @Override public SalesTop10ReportVO getTop10 (LocalDate begin, LocalDate end) { LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN); LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX); List<GoodsSalesDTO> top10List = orderMapper.getTop10(beginTime, endTime); List<String> names = top10List.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList()); String nameList = StringUtils.join(names, "," ); List<Integer> numbers = top10List.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList()); String numberList = StringUtils.join(numbers, "," ); return SalesTop10ReportVO.builder() .nameList(nameList) .numberList(numberList) .build(); }
总算是搞完了,刚才测试才发现之前写的插入订单明细有问题,少插入了一个number,所以所有的菜品数量都是1..
13 数据统计-Excel报表 工作台接口 工作台,就是进入后台管理后的第一个页面
看着接口文档写一遍吧
没啥难的,写完了已经.
直接全放着:
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 @RestController @RequestMapping("/admin/workspace") @Slf4j @Api(tags = "工作台接口") public class WorkSpaceController { @Autowired private WorkSpaceService workSpaceService; @GetMapping("/businessData") @ApiOperation("查询今日运营数据") public Result<BusinessDataVO> businessData () { log.info("查询今日运营数据" ); BusinessDataVO businessDataVO = workSpaceService.businessData(); return Result.success(businessDataVO); } @GetMapping("/overviewSetmeals") @ApiOperation("查询套餐总览") public Result<SetmealOverViewVO> overviewSetmeals () { log.info("查询套餐总览" ); SetmealOverViewVO setmealOverViewVO = workSpaceService.overviewSetmeals(); return Result.success(setmealOverViewVO); } @GetMapping("/overviewDishes") @ApiOperation("查询菜品总览") public Result<DishOverViewVO> overviewDishes () { log.info("查询套餐总览" ); DishOverViewVO dishOverViewVO = workSpaceService.overviewDishes(); return Result.success(dishOverViewVO); } @GetMapping("/overviewOrders") @ApiOperation("订单管理") public Result<OrderOverViewVO> overviewOrder () { OrderOverViewVO orderOverViewVO = workSpaceService.overviewOrder(); return Result.success(orderOverViewVO); } }
Service
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 @Service @Slf4j public class WorkSpaceServiceImpl implements WorkSpaceService { @Autowired private UserMapper userMapper; @Autowired private OrderMapper orderMapper; @Autowired private SetmealMapper setmealMapper; @Autowired private DishMapper dishMapper; @Override public BusinessDataVO businessData () { LocalDateTime begin = LocalDateTime.of(LocalDate.now(), LocalTime.MIN); LocalDateTime end = LocalDateTime.of(LocalDate.now(), LocalTime.MAX); Map map = new HashMap (); map.put("begin" , begin); map.put("end" , end); Integer newUser = userMapper.countByMap(map); Integer orderCount = orderMapper.countOrderByMap(map); map.put("status" , Orders.COMPLETED); Integer validOrderCount = orderMapper.countOrderByMap(map); Double orderCompletionRate = 0.0 ; if (orderCount != 0 ) { orderCompletionRate = validOrderCount / orderCount.doubleValue(); } Double turnover = orderMapper.sumByMap(map); turnover = turnover == null ? 0.0 : turnover; Integer totalOrderUser = orderMapper.getDistinctUser(map); Double unitPrice = 0.0 ; if (totalOrderUser != 0 ) { unitPrice = turnover / totalOrderUser.doubleValue(); } return BusinessDataVO.builder() .validOrderCount(validOrderCount) .orderCompletionRate(orderCompletionRate) .newUsers(newUser) .turnover(turnover) .unitPrice(unitPrice) .build(); } @Override public SetmealOverViewVO overviewSetmeals () { Integer startCount = setmealMapper.getCountByStatus(StatusConstant.ENABLE); Integer stopCount = setmealMapper.getCountByStatus(StatusConstant.DISABLE); return SetmealOverViewVO.builder().sold(startCount).discontinued(stopCount).build(); } @Override public DishOverViewVO overviewDishes () { Integer startCount = dishMapper.getCountByStatus(StatusConstant.ENABLE); Integer stopCount = dishMapper.getCountByStatus(StatusConstant.DISABLE); return DishOverViewVO.builder().sold(startCount).discontinued(stopCount).build(); } @Override public OrderOverViewVO overviewOrder () { Map map = new HashMap (); Integer allOrders = orderMapper.countOrderByMap(map); map.put("status" , Orders.TO_BE_CONFIRMED); Integer waitingOrders = orderMapper.countOrderByMap(map); map.put("status" , Orders.CONFIRMED); Integer deliveredOrders = orderMapper.countOrderByMap(map); map.put("status" , Orders.COMPLETED); Integer completedOrders = orderMapper.countOrderByMap(map); map.put("status" , Orders.CANCELLED); Integer cancelledOrders = orderMapper.countOrderByMap(map); return OrderOverViewVO.builder() .allOrders(allOrders) .cancelledOrders(cancelledOrders) .completedOrders(completedOrders) .deliveredOrders(deliveredOrders) .waitingOrders(waitingOrders) .build(); } }
总体来说还是很简单的.
就是封装数据查询封装数据查询封装数据查询封装数据查询封装数据查询封装数据查询封装数据查询
Apache POI 介绍 Apache POI是一个处理Miscrosoft Office各种文件格式的开源项目,我们可以用POI在java程序中对Miscrosoft Office各种文件进行读写操作
一般情况下,POI都是用于操作Excel文件
应用场景:
银行网银系统导出交易明细
各种业务系统导出Excel报表
批量导入业务数据
基本使用 导入依赖
1 2 3 4 5 6 7 8 9 <dependency > <groupId > org.apache.poi</groupId > <artifactId > poi</artifactId > </dependency > <dependency > <groupId > org.apache.poi</groupId > <artifactId > poi-ooxml</artifactId > </dependency >
写操作测试类,基本语法就是
创建Excel对象new XSSFWorkbook
根据excel对象创建页sheet
根据sheet创建行
根据row创建单元格,并给单元格置入数据
注意,行和单元格的下标都是从0开始的.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 class POITest { public static void write () throws IOException { XSSFWorkbook excel = new XSSFWorkbook (); XSSFSheet sheet = excel.createSheet("info" ); XSSFRow row = sheet.createRow(1 ); row.createCell(1 ).setCellValue("姓名" ); row.createCell(2 ).setCellValue("城市" ); row = sheet.createRow(2 ); row.createCell(1 ).setCellValue("杨某" ); row.createCell(2 ).setCellValue("汉中" ); row = sheet.createRow(3 ); row.createCell(1 ).setCellValue("周某" ); row.createCell(2 ).setCellValue("苏州" ); FileOutputStream out = new FileOutputStream (new File ("E:\\info.xlsx" )); excel.write(out); excel.close(); out.close(); } public static void main (String[] args) throws IOException { write(); } }
读操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public static void read () throws IOException { XSSFWorkbook excel = new XSSFWorkbook (new FileInputStream ("E:\\info.xlsx" )); XSSFSheet sheet = excel.getSheetAt(0 ); int lastRowNum = sheet.getLastRowNum(); for (int i = 1 ; i <= lastRowNum; i++) { XSSFRow row = sheet.getRow(i); String cellValue1 = row.getCell(1 ).getStringCellValue(); String cellValue2 = row.getCell(2 ).getStringCellValue(); System.out.println(cellValue1 + " " + cellValue2); } }
也是非常易懂
导出运营数据Excel报表 报表形式:
业务规则
导出Excel形式的报表文件
到处最近30天的运营数据
当前接口是没有返回数据的,因为报表的本质是文件下载,服务端会通过输出流将Excel文件下载到客户端浏览器
先创建这个Excel文件,那么问题就来了,这个表格有背景,有字号的区别,还有合并单元格,用POI是可以实现的,但是非常麻烦,在真正的项目中,是先把表格的样式定好,然后读取这个表格,最后修改指定位置的数据即可
然后就是查询近30天的数据,通过POI写入数据,再通过输出流将Excel文件下载到客户端浏览器
Controller
1 2 3 4 5 @GetMapping("/export") @ApiOperation("导出excel") public void export (HttpServletResponse response) { reportService.exportBusiness(response); }
Service,也就是分成三步
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 @Override public void exportBusiness (HttpServletResponse response) { LocalDate dateBegin = LocalDate.now().minusDays(30 ); LocalDate dateEnd = LocalDate.now().minusDays(1 ); LocalDateTime begin = LocalDateTime.of(dateBegin, LocalTime.MIN); LocalDateTime end = LocalDateTime.of(dateEnd, LocalTime.MAX); BusinessDataVO businessDataVO = workSpaceService.businessData(begin, end); InputStream in = this .getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx" ); try { XSSFWorkbook excel = new XSSFWorkbook (in); XSSFSheet sheet = excel.getSheetAt(0 ); sheet.getRow(1 ).getCell(1 ).setCellValue("时间: " + dateBegin + "至" + dateEnd); sheet.getRow(3 ).getCell(2 ).setCellValue(businessDataVO.getTurnover()); sheet.getRow(3 ).getCell(4 ).setCellValue(businessDataVO.getOrderCompletionRate()); sheet.getRow(3 ).getCell(6 ).setCellValue(businessDataVO.getNewUsers()); sheet.getRow(4 ).getCell(2 ).setCellValue(businessDataVO.getValidOrderCount()); sheet.getRow(4 ).getCell(4 ).setCellValue(businessDataVO.getUnitPrice()); for (int i = 0 ; i < 30 ; i++) { LocalDate date = dateBegin.plusDays(i); begin = LocalDateTime.of(date, LocalTime.MIN); end = LocalDateTime.of(date, LocalTime.MAX); BusinessDataVO businessData = workSpaceService.businessData(begin, end); XSSFRow row = sheet.getRow(i + 7 ); 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 outputStream = response.getOutputStream(); excel.write(outputStream); outputStream.close(); excel.close(); } catch (IOException e) { e.printStackTrace(); } }
结束 2023年9月13日 21点53分