苍穹外卖
导入接口
Swagger
Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务(https://swagger.io/)。 它的主要作用是:
使得前后端分离开发更加方便,有利于团队协作
接口的文档在线自动生成,降低后端开发人员编写接口文档的负担
功能测试
Spring已经将Swagger纳入自身的标准,建立了Spring-swagger项目,现在叫Springfox。通过在项目中引入Springfox ,即可非常简单快捷的使用Swagger。
knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger-bootstrap-ui,取名kni4j是希望它能像一把匕首一样小巧,轻量,并且功能强悍!
目前,一般都使用knife4j框架。
使用步骤
导入knife4j框架
<dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> </dependency>
在配置类 WebMvcConfiguration 中加入 knife4j 相关配置
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; /** * 配置类,注册web层相关组件 */ @Configuration @Slf4j public class WebMvcConfiguration extends WebMvcConfigurationSupport { @Autowired private JwtTokenAdminInterceptor jwtTokenAdminInterceptor; /** * 注册自定义拦截器 * * @param registry */ protected void addInterceptors(InterceptorRegistry registry) { log.info("开始注册自定义拦截器..."); registry.addInterceptor(jwtTokenAdminInterceptor) .addPathPatterns("/admin/**") .excludePathPatterns("/admin/employee/login"); } /** * 通过knife4j生成接口文档 * @return */ @Bean public Docket docket() { ApiInfo apiInfo = new ApiInfoBuilder() .title("苍穹外卖项目接口文档") .version("2.0") .description("苍穹外卖项目接口文档") .build(); Docket docket = new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo) .select() .apis(RequestHandlerSelectors.basePackage("com.sky.controller")) .paths(PathSelectors.any()) .build(); return docket; } /** * 设置静态资源映射 * @param registry */ protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); } }
设置静态资源映射,否则接口文档页面无法访问
WebMvcConfiguration.java
/** * 设置静态资源映射 * @param registry */ protected void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/"); }
访问测试
接口文档访问路径为 http://ip:port/doc.html ---> http://localhost:8080/doc.html
image-20240504165812455
常用注解
通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:
注解 | 说明 |
---|---|
@Api | 用在类上,例如Controller,表示对类的说明 |
@ApiModel | 用在类上,例如entity、DTO、VO |
@ApiModelProperty | 用在属性上,描述属性信息 |
@ApiOperation | 用在方法上,例如Controller的方法,说明方法的用途、作用 |



MD5 加密🔐
单向
password = DigestUtils.md5DigestAsHex(password.getBytes());
新增员工
- 前端传DTO
- 后端用BeanUtil拷贝
- EmployeeController
@ApiOperation(value = "添加员工")
@PostMapping()
public Result<String> addEmployee(@RequestBody EmployeeDTO employeeDTO) {
log.info("新增员工: {}", employeeDTO);
return employeeService.addEmployee(employeeDTO);
}
实现类
@Override public Result<String> addEmployee(EmployeeDTO employeeDTO) { // 用户名检查 String employeeUserName = employeeDTO.getUsername(); if (employeeUserName == null || employeeUserName.isEmpty()) { return Result.error("用户名不能为空!"); } Employee employee = employeeMapper.getByUsername(employeeUserName); if (employee != null) { return Result.error("此用户名已被使用!"); } // 将DTO转换 并添加缺失的属性 Employee newEmployee = new Employee(); BeanUtils.copyProperties(employeeDTO, newEmployee); newEmployee.setCreateTime(LocalDateTime.now()); newEmployee.setUpdateTime(LocalDateTime.now()); newEmployee.setStatus(StatusConstant.ENABLE); //设置当前记录创建人id和修改人id newEmployee.setCreateUser(10L); newEmployee.setUpdateUser(10L); //设置密码,默认密码123456 newEmployee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes())); // 向数据库中插入用户 employeeMapper.insert(newEmployee); return Result.success(); }
使用全局异常处理用户名重复问题
package com.sky.handler; import com.sky.constant.MessageConstant; 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; import java.sql.SQLIntegrityConstraintViolationException; /** * 全局异常处理器,处理项目中抛出的业务异常 */ @RestControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 捕获业务异常 * @param ex * @return */ @ExceptionHandler public Result exceptionHandler(BaseException ex){ log.error("异常信息:{}", ex.getMessage()); return Result.error(ex.getMessage()); } /** * 处理SQL异常 * @param ex * @return */ @ExceptionHandler public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){ String message = ex.getMessage(); if(message.contains("Duplicate entry")){ String[] split = message.split(" "); String username = split[2]; String msg = username + MessageConstant.ALREADY_EXISTS; return Result.error(msg); }else{ return Result.error(MessageConstant.UNKNOWN_ERROR); } } }
ThreadLocal
每个请求过程都是同一个线程执行的
- ThreadLocal 并不是一个Thread,而是Thread的局部变量。
- ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
常用方法:
- public void set(T value) 设置当前线程的线程局部变量的值
- public T get() 返回当前线程所对应的线程局部变量的值
- public void remove() 移除当前线程的线程局部变量

获得当前登录用户的ID
interceptor.JwtTokenAdminInterceptor
package com.sky.interceptor; import com.sky.constant.JwtClaimsConstant; import com.sky.context.BaseContext; import com.sky.properties.JwtProperties; import com.sky.utils.JwtUtil; import io.jsonwebtoken.Claims; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * jwt令牌校验的拦截器 */ @Component @Slf4j public class JwtTokenAdminInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; /** * 校验jwt * * @param request * @param response * @param handler * @return * @throws Exception */ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判断当前拦截到的是Controller的方法还是其他资源 if (!(handler instanceof HandlerMethod)) { //当前拦截到的不是动态方法,直接放行 return true; } //1、从请求头中获取令牌 String token = request.getHeader(jwtProperties.getAdminTokenName()); //2、校验令牌 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); BaseContext.setCurrentId(empId); //3、通过,放行 return true; } catch (Exception ex) { //4、不通过,响应401状态码 response.setStatus(401); return false; } } public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { BaseContext.removeCurrentId(); } }
修改实现类
package com.sky.service.impl; import com.sky.constant.MessageConstant; import com.sky.constant.PasswordConstant; import com.sky.constant.StatusConstant; import com.sky.context.BaseContext; import com.sky.dto.EmployeeDTO; import com.sky.dto.EmployeeLoginDTO; import com.sky.entity.Employee; import com.sky.exception.AccountLockedException; import com.sky.exception.AccountNotFoundException; import com.sky.exception.PasswordErrorException; import com.sky.mapper.EmployeeMapper; import com.sky.result.Result; import com.sky.service.EmployeeService; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.util.DigestUtils; import java.time.LocalDateTime; @Service public class EmployeeServiceImpl implements EmployeeService { @Autowired private EmployeeMapper employeeMapper; /** * 员工登录 * * @param employeeLoginDTO * @return */ public Employee login(EmployeeLoginDTO employeeLoginDTO) { String username = employeeLoginDTO.getUsername(); String password = employeeLoginDTO.getPassword(); //1、根据用户名查询数据库中的数据 Employee employee = employeeMapper.getByUsername(username); //2、处理各种异常情况(用户名不存在、密码不对、账号被锁定) if (employee == null) { //账号不存在 throw new AccountNotFoundException(MessageConstant.ACCOUNT_NOT_FOUND); } // 密码比对 // 进行md5加密,然后再进行比对 password = DigestUtils.md5DigestAsHex(password.getBytes()); if (!password.equals(employee.getPassword())) { //密码错误 throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR); } if (employee.getStatus() == StatusConstant.DISABLE) { //账号被锁定 throw new AccountLockedException(MessageConstant.ACCOUNT_LOCKED); } //3、返回实体对象 return employee; } @Override public Result<String> addEmployee(EmployeeDTO employeeDTO) { // 用户名检查 // String employeeUserName = employeeDTO.getUsername(); // if (employeeUserName == null || employeeUserName.isEmpty()) { // return Result.error("用户名不能为空!"); // } // // Employee employee = employeeMapper.getByUsername(employeeUserName); // // if (employee != null) { // return Result.error("此用户名已被使用!"); // } // 将DTO转换 并添加缺失的属性 Employee newEmployee = new Employee(); BeanUtils.copyProperties(employeeDTO, newEmployee); newEmployee.setCreateTime(LocalDateTime.now()); newEmployee.setUpdateTime(LocalDateTime.now()); newEmployee.setStatus(StatusConstant.ENABLE); //设置当前记录创建人id和修改人id Long currentId = BaseContext.getCurrentId(); newEmployee.setCreateUser(currentId); newEmployee.setUpdateUser(currentId); //设置密码,默认密码123456 newEmployee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes())); // 向数据库中插入用户 employeeMapper.insert(newEmployee); return Result.success(); } }
员工分页查询
封装前端传来的参数到DTO
根据请求参数进行封装,在sky-pojo模块中
package com.sky.dto; import lombok.Data; import java.io.Serializable; @Data public class EmployeePageQueryDTO implements Serializable { //员工姓名 private String name; //页码 private int page; //每页显示记录数 private int pageSize; }
后面所有的分页查询,统一都封装为PageResult对象。
在sky-common模块
package com.sky.result; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.io.Serializable; import java.util.List; /** * 封装分页查询结果 */ @Data @AllArgsConstructor @NoArgsConstructor public class PageResult implements Serializable { private long total; //总记录数 private List records; //当前页数据集合 }
使用mybatis分页插件pagehelper实现分页
底层基于 mybatis 的拦截器实现
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>${pagehelper}</version> </dependency>
controller定义
@ApiOperation(value = "员工分页查询") @GetMapping("/page") public Result<PageResult> listEmployee(EmployeePageQueryDTO employeePageQueryDTO) { log.info("员工分页查询: {}", employeePageQueryDTO); return Result.success(employeeService.queryByPage(employeePageQueryDTO)); }
实现类
@Override public PageResult queryByPage(EmployeePageQueryDTO employeePageQueryDTO) { // 开始分页查询 // 底层使用的是ThreadLocal把页码和总数存下来,然后生成动态的SQL PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize()); Page<Employee> page = employeeMapper.queryByPage(employeePageQueryDTO); long total = page.getTotal(); List<Employee> records = page.getResult(); return new PageResult(total, records); }
EmployeeMapper
package com.sky.mapper; import com.github.pagehelper.Page; import com.sky.dto.EmployeePageQueryDTO; import com.sky.entity.Employee; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; @Mapper public interface EmployeeMapper { /** * 根据用户名查询员工 * * @param username * @return */ @Select("select * from employee where username = #{username}") Employee getByUsername(String username); @Insert("insert into employee (name, username, password, " + "phone, sex, id_number, create_time, " + "update_time, create_user, update_user,status) " + "values " + "(#{name},#{username},#{password},#{phone}," + "#{sex},#{idNumber},#{createTime},#{updateTime}," + "#{createUser},#{updateUser},#{status})") void insert(Employee employee); Page<Employee> queryByPage(EmployeePageQueryDTO employeePageQueryDTO); }
{ "code": 1, "msg": null, "data": { "total": 2, "records": [ { "id": 2, "username": "小智", "name": "xiaozhi", "password": "e10adc3949ba59abbe56e057f20f883e", "phone": "13812344321", "sex": "1", "idNumber": "111222333444555666", "status": 1, "createTime": [ 2024, 5, 6, 19, 59, 7 ], "updateTime": [ 2024, 5, 6, 19, 59, 7 ], "createUser": 1, "updateUser": 1 }, { "id": 1, "username": "admin", "name": "管理员", "password": "e10adc3949ba59abbe56e057f20f883e", "phone": "13812312312", "sex": "1", "idNumber": "110101199001010047", "status": 1, "createTime": [ 2022, 2, 15, 15, 51, 20 ], "updateTime": [ 2022, 2, 17, 9, 16, 20 ], "createUser": 10, "updateUser": 1 } ] } }
EmployeeMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.sky.mapper.EmployeeMapper"> <select id="queryByPage" resultType="com.sky.entity.Employee"> select * from employee <where> <if test="name != null and name != ''"> and name like concat('%', #{name}, '%') </if> </where> order by create_time desc </select> </mapper>
image-20240506200030332
完善时间显示
LocalDataTime 转 json 会变成数组
方法一
在属性上加上注解,对日期进行格式化

方法二(推荐 )
在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理
package com.sky.config;
import com.sky.interceptor.JwtTokenAdminInterceptor;
import com.sky.json.JacksonObjectMapper;
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.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
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;
import java.util.List;
/**
* 配置类,注册web层相关组件
*/
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/employee/login");
}
/**
* 通过knife4j生成接口文档
*
* @return
*/
@Bean
public Docket docket() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
/**
* 设置静态资源映射
*
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
/**
* 扩展SpringMVC框架的消息转换器
*
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转化器加入容器中,0设置排序,不然默认排在后面,使用不到
converters.add(0, converter);
}
}
{ "code": 1, "msg": null, "data": { "total": 2, "records": [ { "id": 2, "username": "小智", "name": "xiaozhi", "password": "e10adc3949ba59abbe56e057f20f883e", "phone": "13812344321", "sex": "1", "idNumber": "111222333444555666", "status": 1, "createTime": "2024-05-06 19:59", "updateTime": "2024-05-06 19:59", "createUser": 1, "updateUser": 1 }, { "id": 1, "username": "admin", "name": "管理员", "password": "e10adc3949ba59abbe56e057f20f883e", "phone": "13812312312", "sex": "1", "idNumber": "110101199001010047", "status": 1, "createTime": "2022-02-15 15:51", "updateTime": "2022-02-17 09:16", "createUser": 10, "updateUser": 1 } ] } }

启用/禁用员工账号
controller
@ApiOperation(value = "更改员工状态") @PostMapping("/status/{status}") public Result changeStatus(Long id, @PathVariable Integer status) { log.info("更改员工状态: {}, {}", id, status); return employeeService.changeStatus(id, status); }
实现类
@Override public Result changeStatus(Long id, Integer status) { // Employee employee = new Employee(); // employee.setId(id); // employee.setStatus(status); Employee employee = Employee.builder().id(id).status(status).build(); employeeMapper.update(employee); return Result.success(); }
mapper
void update(Employee employee);
mapper.xml
动态sql,使update更通用
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.sky.mapper.EmployeeMapper"> <update id="update" parameterType="Employee"> update employee <set> <if test="name != null">name = #{name},</if> <if test="username != null">username = #{username},</if> <if test="password != null">password = #{password},</if> <if test="phone != null">phone = #{phone},</if> <if test="sex != null">sex = #{sex},</if> <if test="idNumber != null">id_Number = #{idNumber},</if> <if test="updateTime != null">update_Time = #{updateTime},</if> <if test="updateUser != null">update_User = #{updateUser},</if> <if test="status != null">status = #{status},</if> </set> where id = #{id} </update> <select id="queryByPage" resultType="com.sky.entity.Employee"> select * from employee <where> <if test="name != null and name != ''"> and name like concat('%', #{name}, '%') </if> </where> order by create_time desc </select> </mapper>
编辑员工
通过id查询员工
@ApiOperation(value = "员工id查询")
@GetMapping("/{id}")
public Result<Employee> getById(@PathVariable Long id) {
log.info("查询员工: {}", id);
return employeeService.getById(id);
}
@Override
public Result<Employee> getById(Long id) {
Employee employee = employeeMapper.getById(id);
if (employee == null) {
return Result.error("员工不存在!");
}
employee.setPassword("🙅♀️🫣");
return Result.success(employee);
}
修改
@ApiOperation(value = "修改员工信息")
@PutMapping()
public Result updateEmployee(@RequestBody EmployeeDTO employeeDTO) {
log.info("修改员工信息: {}", employeeDTO);
return employeeService.updateEmployee(employeeDTO);
}
@Override
public Result updateEmployee(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTO, employee);
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.update(employee);
return Result.success();
}
分类管理功能代码
Controller
package com.sky.controller.admin; import com.sky.dto.CategoryDTO; import com.sky.dto.CategoryPageQueryDTO; import com.sky.entity.Category; import com.sky.result.PageResult; import com.sky.result.Result; import com.sky.service.CategoryService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; /** * @Author:CharmingDaiDai * @Project:sky-take-out * @Date:2024/5/7 上午9:14 */ @RestController @RequestMapping("/admin/category") @Slf4j @Api(tags = "分类管理") public class CategoryController { @Resource private CategoryService categoryService; @ApiOperation(value = "新增分类") @PostMapping() public Result addCategory(@RequestBody CategoryDTO categoryDTO) { log.info("新增分类:{}", categoryDTO); return categoryService.addCategory(categoryDTO); } @ApiOperation(value = "删除分类") @DeleteMapping() public Result deleteById(Integer id) { log.info("删除分类:{}", id); return categoryService.deleteById(id); } @ApiOperation(value = "查询分类") @GetMapping("/list") public Result<List<Category>> getByType(Integer type) { log.info("根据类型查询分类:{}", type); return categoryService.getByType(type); } @ApiOperation(value = "修改分类状态") @PostMapping("status/{status}") public Result setStatus(Long id, @PathVariable Integer status) { log.info("修改分类状态:{}, {}", id, status); return categoryService.setStatus(id, status); } @ApiOperation(value = "修改分类") @PutMapping public Result update(@RequestBody CategoryDTO categoryDTO) { log.info("修改分类:{}", categoryDTO); return categoryService.update(categoryDTO); } @ApiOperation(value = "分页查询") @GetMapping("/page") public Result<PageResult> getByPage(CategoryPageQueryDTO categoryPageQueryDTO) { log.info("分页查询:{}", categoryPageQueryDTO); PageResult result = categoryService.getByPage(categoryPageQueryDTO); return Result.success(result); } }
service
package com.sky.service; import com.sky.dto.CategoryDTO; import com.sky.dto.CategoryPageQueryDTO; import com.sky.entity.Category; import com.sky.result.PageResult; import com.sky.result.Result; import java.util.List; /** * @Author:CharmingDaiDai * @Project:sky-take-out * @Date:2024/5/7 上午9:19 */ public interface CategoryService { Result addCategory(CategoryDTO categoryDTO); Result deleteById(Integer id); Result<List<Category>> getByType(Integer type); Result setStatus(Long id, Integer status); Result update(CategoryDTO categoryDTO); PageResult getByPage(CategoryPageQueryDTO categoryService); }
serviceImpl
package com.sky.service.impl; import com.github.pagehelper.Page; import com.github.pagehelper.PageHelper; import com.sky.context.BaseContext; import com.sky.dto.CategoryDTO; import com.sky.dto.CategoryPageQueryDTO; import com.sky.entity.Category; import com.sky.entity.Employee; import com.sky.mapper.CategoryMapper; import com.sky.result.PageResult; import com.sky.result.Result; import com.sky.service.CategoryService; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.time.LocalDateTime; import java.util.List; /** * @Author:CharmingDaiDai * @Project:sky-take-out * @Date:2024/5/7 上午9:23 */ @Service public class CategoryServiceImpl implements CategoryService { @Resource private CategoryMapper categoryMapper; @Override public Result addCategory(CategoryDTO categoryDTO) { Category category = new Category(); BeanUtils.copyProperties(categoryDTO, category); category.setStatus(0); category.setCreateTime(LocalDateTime.now()); category.setUpdateTime(LocalDateTime.now()); category.setCreateUser(BaseContext.getCurrentId()); category.setUpdateUser(BaseContext.getCurrentId()); categoryMapper.insert(category); return Result.success(); } @Override public Result deleteById(Integer id) { categoryMapper.deleteById(id); return Result.success(); } @Override public Result<List<Category>> getByType(Integer type) { List<Category> categories = categoryMapper.getByType(type); return Result.success(categories); } @Override public Result setStatus(Long id, Integer status) { Category category = Category.builder().id(id).status(status) .updateTime(LocalDateTime.now()).updateUser(BaseContext.getCurrentId()).build(); categoryMapper.update(category); return Result.success(); } @Override public Result update(CategoryDTO categoryDTO) { Category category = new Category(); BeanUtils.copyProperties(categoryDTO, category); category.setUpdateTime(LocalDateTime.now()); category.setUpdateUser(BaseContext.getCurrentId()); categoryMapper.update(category); return Result.success(); } @Override public PageResult getByPage(CategoryPageQueryDTO categoryPageQueryDTO) { // 开始分页查询 // 底层使用的是ThreadLocal把页码和总数存下来,然后生成动态的SQL PageHelper.startPage(categoryPageQueryDTO.getPage(), categoryPageQueryDTO.getPageSize()); Page<Category> page = categoryMapper.queryByPage(categoryPageQueryDTO); long total = page.getTotal(); List<Category> records = page.getResult(); return new PageResult(total, records); } }
mapper
package com.sky.mapper; import com.github.pagehelper.Page; import com.sky.dto.CategoryPageQueryDTO; import com.sky.dto.EmployeePageQueryDTO; import com.sky.entity.Category; import com.sky.entity.Employee; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import java.util.List; /** * @Author:CharmingDaiDai * @Project:sky-take-out * @Date:2024/5/7 上午9:30 */ @Mapper public interface CategoryMapper { @Insert("insert into category (type, name, sort, " + "status, create_time, update_time, create_user, update_user) " + "values (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}," + "#{createUser}, #{updateUser});") int insert(Category category); @Delete("delete from category where id = #{id}") void deleteById(Integer id); @Select("select * from category where type = #{type}") List<Category> getByType(Integer type); void update(Category category); Page<Category> queryByPage(CategoryPageQueryDTO categoryPageQueryDTO); }
mapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.sky.mapper.CategoryMapper"> <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="updateTime != null">update_Time = #{updateTime},</if> <if test="updateUser != null">update_User = #{updateUser},</if> <if test="createTime != null">create_Time = #{createTime},</if> <if test="createUser != null">create_User = #{createUser},</if> <if test="status != null">status = #{status},</if> </set> where id = #{id} </update> <select id="queryByPage" resultType="com.sky.entity.Category"> select * from category <where> <if test="name != null and name != ''"> and name like concat('%', #{name}, '%') </if> <if test="type != null and type != ''"> and type = #{type} </if> </where> order by create_time desc </select> </mapper>
公共字段自动填充
重复的代码:添加创建人、创建时间、修改人、
使用AOP切面编程,实现功能增强,来完成公共字段自动填充功能
自定义注解
package com.sky.annotation;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
//数据库操作类型:UPDATE INSERT
OperationType value();
}
ℹ️自定义注解
- 注解声明 (
@Target(ElementType.METHOD)
): 这个注解指定了AutoFill
注解可以应用于方法上。ElementType.METHOD
表示这个注解的目标是方法。 - 注解的保留策略 (
@Retention(RetentionPolicy.RUNTIME)
): 这个注解指定了AutoFill
注解的保留策略。RetentionPolicy.RUNTIME
意味着这个注解不仅在编译时保留,而且在运行时也保留,可以通过反射获取到。 - 定义注解 (
public @interface AutoFill
): 这行代码定义了注解的开始,AutoFill
是注解的名称。 - 注解的属性 (
OperationType value()
): 这是注解中定义的一个属性,名为value
。它使用了OperationType
枚举类型作为属性的类型。这意味着任何使用了AutoFill
注解的方法都需要提供一个OperationType
枚举值作为参数。
自定义切面类
package com.sky.aspect;
import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
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;
/**
* @Author:CharmingDaiDai
* @Project:sky-take-out
* @Date:2024/5/7 上午10:40
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点
*/
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint){
log.info("进行公共字段填充");
//获取到当前被拦截的方法上的数据库操作类型
MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);//获得方法上的注解对象
OperationType operationType = autoFill.value();//获得数据库操作类型
//获取到当前被拦截的方法的参数--实体对象
Object[] args = joinPoint.getArgs();
if(args == null || args.length == 0){
return;
}
Object entity = args[0];
//准备赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
//根据当前不同的操作类型,为对应的属性通过反射来赋值
if(operationType == OperationType.INSERT){
//为4个公共字段赋值
try {
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象属性赋值
setCreateTime.invoke(entity,now);
setCreateUser.invoke(entity,currentId);
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
}else if(operationType == OperationType.UPDATE){
//为2个公共字段赋值
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
//通过反射为对象属性赋值
setUpdateTime.invoke(entity,now);
setUpdateUser.invoke(entity,currentId);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
ℹ️自定义切面类
这个Java类AutoFillAspect
是一个使用Spring AOP和AspectJ框架的切面类示例。下面是对类中每个部分的详细解释:
类注解:
@Aspect
: 这个注解声明了这个类是一个切面(aspect)。它允许你定义切点(pointcut)和通知(advice)。@Component
: 这个Spring框架的注解表明这个类是一个Spring组件,因此可以被Spring容器管理,并且可以自动注入到其他组件中。@Slf4j
: Lombok提供的注解,它在类中生成了一个日志对象log
,简化了日志记录。
类定义 (
public class AutoFillAspect { ... }
): 定义了切面类AutoFillAspect
。切入点注解:
@Pointcut
: 这个注解定义了一个切入点,它是匹配特定方法执行的模式。在这个例子中,它匹配com.sky.mapper
包下所有类的所有方法执行,并且这些方法被com.sky.annotation.AutoFill
注解标记。public void autoFillPointCut() {}
: 这是切入点的方法定义,实际的方法体为空,因为它仅用于定义切入点。
前置通知注解:
@Before
: 这个注解定义了一个前置通知(before advice),它会在切入点定义的方法执行之前执行。这里它引用了之前定义的autoFillPointCut
切入点。public void autoFill(JoinPoint joinPoint) { ... }
: 这是前置通知的方法定义。JoinPoint
参数提供了关于被拦截方法的连接点的信息,可以用来获取方法签名、参数等信息。
前置通知的实现:
log.info("进行公共字段填充");
: 在前置通知中,使用SLF4J记录了一条日志信息,表明公共字段填充即将开始。
如何自定义切面类
自定义切面类通常涉及以下步骤:
定义切面类: 创建一个新的Java类,并使用
@Aspect
注解声明它是一个切面。定义切入点: 使用
@Pointcut
注解定义一个切入点。切入点是一组匹配特定方法调用的规则。定义通知: 创建通知(advice)方法,这些方法将在与切入点匹配的方法执行前后或周围执行。Spring AOP支持不同类型的通知,如前置(before)、后置(after)、返回(after returning)、异常(after throwing)和环绕(around)通知。
将切入点与通知关联: 使用通知注解(如
@Before
、@After
等)将通知方法与切入点关联起来。配置切面: 使用
@AspectJ
注解或XML配置来配置切面,指定切入点和通知。注入到Spring容器: 使用
@Component
注解将切面类注册为Spring容器的组件,使其能够被Spring自动管理。启用AOP: 确保在Spring配置中启用了AOP的自动代理。
测试切面: 编写单元测试或集成测试来验证切面是否按预期工作。
通过以上步骤,您可以自定义切面类来在Spring应用程序中实现特定的横切关注点,如日志记录、事务管理、安全性等。
mapper
package com.sky.mapper; import com.github.pagehelper.Page; import com.sky.annotation.AutoFill; import com.sky.dto.EmployeePageQueryDTO; import com.sky.entity.Employee; import com.sky.enumeration.OperationType; import org.apache.ibatis.annotations.*; @Mapper public interface EmployeeMapper { /** * 根据用户名查询员工 * * @param username * @return */ @Select("select * from employee where username = #{username}") Employee getByUsername(String username); @Insert("insert into employee (name, username, password, " + "phone, sex, id_number, create_time, " + "update_time, create_user, update_user,status) " + "values " + "(#{name},#{username},#{password},#{phone}," + "#{sex},#{idNumber},#{createTime},#{updateTime}," + "#{createUser},#{updateUser},#{status})") @AutoFill(value = OperationType.INSERT) void insert(Employee employee); Page<Employee> queryByPage(EmployeePageQueryDTO employeePageQueryDTO); @Select("select * from employee where id = #{id}") Employee getById(Long id); @AutoFill(value = OperationType.UPDATE) void update(Employee employee); }
菜品相关
新增菜品
TODO 阿里云OSS
分页查询
DishController
@ApiOperation(value = "分页查询菜品") @GetMapping("/page") public Result<PageResult> getByPage(DishPageQueryDTO dishPageQueryDTO){ log.info("分类查询菜品:{}", dishPageQueryDTO); PageResult result = dishService.getByPage(dishPageQueryDTO); return Result.success(result); }
实现类
@Override public PageResult getByPage(DishPageQueryDTO dishPageQueryDTO) { // select d.*, c.name as categoryName from dish d // left outer join category c on d.category_id = c.id // where ... // 开启分页 PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize()); Page<DishVO> page = dishMapper.queryByPage(dishPageQueryDTO); long total = page.getTotal(); List<DishVO> records = page.getResult(); return new PageResult(total, records); }
DishMapper
package com.sky.mapper; import com.github.pagehelper.Page; import com.sky.annotation.AutoFill; import com.sky.dto.DishPageQueryDTO; import com.sky.entity.Dish; import com.sky.enumeration.OperationType; import com.sky.vo.DishVO; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import java.util.List; /** * @Author:CharmingDaiDai * @Project:sky-take-out * @Date:2024/5/7 下午3:19 */ @Mapper public interface DishMapper { @AutoFill(value = OperationType.INSERT) @Insert("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});") void insert(Dish dish); @Select("select * from dish where id = #{id}") Dish getById(Long id); @Select("select * from dish where category_id = #{categoryId}") List<Dish> getByType(Long categoryId); Page<DishVO> queryByPage(DishPageQueryDTO dishPageQueryDTO); @AutoFill(value = OperationType.UPDATE) void update(Dish dish); @Delete("delete from dish where id in #{ids}") void delete(Long[] ids); }
DishMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.sky.mapper.DishMapper"> <update id="update" parameterType="Dish"> update dish <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="image != null">image = #{image},</if> <if test="description != null">description = #{description},</if> <if test="updateTime != null">update_Time = #{updateTime},</if> <if test="updateUser != null">update_User = #{updateUser},</if> <if test="createTime != null">create_Time = #{createTime},</if> <if test="createUser != null">create_User = #{createUser},</if> <if test="status != null">status = #{status},</if> </set> where id = #{id} </update> <select id="queryByPage" resultType="com.sky.vo.DishVO"> select d.* , c.name as categoryName from dish d left outer join category c on d.category_id = c.id <where> <if test="name != null"> and d.name like concat('%',#{name},'%') </if> <if test="categoryId != null"> and d.category_id = #{categoryId} </if> <if test="status != null"> and d.status = #{status} </if> </where> order by d.create_time desc </select> </mapper>
批量删除菜品
业务规则:
- 可以一次删除一个菜品,也可以批量删除菜品
- 起售中的菜品不能删除
- 被套餐关联的菜品不能删除
- 删除菜品后,关联的口味数据也需要删除掉
在进行删除菜品操作时,会涉及到以下三张表

注意事项:
在dish表中删除菜品基本数据时,同时,也要把关联在dish_flavor表中的数据一块删除。
setmeal_dish表为菜品和套餐关联的中间表。
若删除的菜品数据关联着某个套餐,此时,删除失败。
若要删除套餐关联的菜品数据,先解除两者关联,再对菜品进行删除。
controller
@ApiOperation(value = "删除菜品") @DeleteMapping() // public Result delete(@RequestParam List<Long> ids){ public Result delete(String ids){ log.info("删除菜品:{}", ids); return dishService.delete(ids); }
实现类
@Override public Result delete(String ids) { // 使用 split 方法根据逗号分隔符来分割字符串 String[] idStrings = ids.split(","); for (String idStr : idStrings) { Long id = Long.valueOf(idStr); Dish dish = dishMapper.getById(id); // 查询起售状态 Integer status = dish.getStatus(); if (status != 1) { return Result.error("菜品起售中"); } // 查询套餐 SetmealDish setmealDish = dishMapper.selectMeal(id); if (setmealDish != null) { return Result.error("菜品被套餐关联"); } dishMapper.deleteFlavor(id); dishMapper.delete(id); } return Result.success(); }
DishMapper
package com.sky.mapper; import com.github.pagehelper.Page; import com.sky.annotation.AutoFill; import com.sky.dto.DishPageQueryDTO; import com.sky.entity.Dish; import com.sky.entity.SetmealDish; import com.sky.enumeration.OperationType; import com.sky.vo.DishVO; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import java.util.List; /** * @Author:CharmingDaiDai * @Project:sky-take-out * @Date:2024/5/7 下午3:19 */ @Mapper public interface DishMapper { @AutoFill(value = OperationType.INSERT) @Insert("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});") void insert(Dish dish); @Select("select * from dish where id = #{id}") Dish getById(Long id); @Select("select * from dish where category_id = #{categoryId}") List<Dish> getByType(Long categoryId); Page<DishVO> queryByPage(DishPageQueryDTO dishPageQueryDTO); @AutoFill(value = OperationType.UPDATE) void update(Dish dish); @Delete("delete from dish where id = #{id}") void delete(Long id); }
DishMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.sky.mapper.DishMapper"> <update id="update" parameterType="Dish"> update dish <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="image != null">image = #{image},</if> <if test="description != null">description = #{description},</if> <if test="updateTime != null">update_Time = #{updateTime},</if> <if test="updateUser != null">update_User = #{updateUser},</if> <if test="createTime != null">create_Time = #{createTime},</if> <if test="createUser != null">create_User = #{createUser},</if> <if test="status != null">status = #{status},</if> </set> where id = #{id} </update> <select id="queryByPage" resultType="com.sky.vo.DishVO"> select d.* , c.name as categoryName from dish d left outer join category c on d.category_id = c.id <where> <if test="name != null"> and d.name like concat('%',#{name},'%') </if> <if test="categoryId != null"> and d.category_id = #{categoryId} </if> <if test="status != null"> and d.status = #{status} </if> </where> order by d.create_time desc </select> </mapper>
SetmealDishMapper
package com.sky.mapper; import com.sky.entity.SetmealDish; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; /** * @Author:CharmingDaiDai * @Project:sky-take-out * @Date:2024/5/7 下午7:57 */ @Mapper public interface SetmealDishMapper { @Select("select * from setmeal_dish where dish_id = #{dishId}") SetmealDish selectMeal(Long dishId); }
DishFlavorMapper
package com.sky.mapper; import com.sky.entity.SetmealDish; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; /** * @Author:CharmingDaiDai * @Project:sky-take-out * @Date:2024/5/7 下午7:57 */ @Mapper public interface DishFlavorMapper { @Delete("delete from dish_flavor where dish_id = #{dishId}") void deleteFlavor(Long dishId); }
修改菜品
通过对上述原型图进行分析,该页面共涉及4个接口。
接口:
- 根据id查询菜品
- 根据类型查询分类(已实现)
- 文件上传(已实现)
- 修改菜品
我们只需要实现根据id查询菜品和修改菜品两个接口
根据id查询菜品的同时需要把口味查出来
controller
@ApiOperation(value = "修改菜品") @PutMapping public Result update(@RequestBody DishDTO dishDTO){ log.info("修改菜品:{}", dishDTO); return dishService.update(dishDTO); }
实现类
@Override public Result<DishVO> getById(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 Result.success(dishVO); }
修改菜品需要也修改口味
controller
@ApiOperation(value = "修改菜品") @PutMapping public Result update(@RequestBody DishDTO dishDTO){ log.info("修改菜品:{}", dishDTO); return dishService.update(dishDTO); }
实现类
@Override public Result update(DishDTO dishDTO) { Dish dish = new Dish(); BeanUtils.copyProperties(dishDTO, dish); List<DishFlavor> dishFlavors = dishDTO.getFlavors(); Long dishId = dish.getId(); // 删除原来的口味 dishFlavorMapper.deleteFlavor(dishId); // 添加新的口味 for (DishFlavor df : dishFlavors) { df.setDishId(dishId); dishFlavorMapper.add(df); } // 更新基本信息 dishMapper.update(dish); return Result.success(); }
DishFlavorMapper
package com.sky.mapper; import com.sky.annotation.AutoFill; import com.sky.entity.DishFlavor; import com.sky.entity.SetmealDish; import com.sky.enumeration.OperationType; import org.apache.ibatis.annotations.Delete; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; import java.util.List; /** * @Author:CharmingDaiDai * @Project:sky-take-out * @Date:2024/5/7 下午7:57 */ @Mapper public interface DishFlavorMapper { @Delete("delete from dish_flavor where dish_id = #{dishId}") void deleteFlavor(Long dishId); @Select("select * from dish_flavor where dish_id = #{id}") List<DishFlavor> getByDishId(Long id); @Insert("insert into dish_flavor (dish_id, name, value) " + "values (#{dishId}, #{name}, #{value});") @AutoFill(OperationType.INSERT) void add(DishFlavor dishFlavor); }
SetmealDishMapper
package com.sky.mapper; import com.sky.entity.SetmealDish; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; /** * @Author:CharmingDaiDai * @Project:sky-take-out * @Date:2024/5/7 下午7:57 */ @Mapper public interface SetmealDishMapper { @Select("select * from setmeal_dish where dish_id = #{dishId}") SetmealDish selectMeal(Long dishId); }
店铺营业状态设置
使用Redis
导入Spring Data Redis的maven坐标
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
配置Redis数据源
在application-dev.yml中添加
redis: host: localhost port: 6379 password: 123456 database: 10 lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 max-wait: 100ms
编写配置类,创建RedisTemplate对象
package com.sky.config; /** * @Author:CharmingDaiDai * @Project:sky-take-out * @Date:2024/5/7 下午8:42 */ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; @Configuration public class RedisConfig { @Bean public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { // 创建RedisTemplate对象 RedisTemplate template = new RedisTemplate(); // 设置连接工厂 template.setConnectionFactory(connectionFactory); // 创建JSON序列化工具 GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); // 设置Key的序列化 template.setKeySerializer(RedisSerializer.string()); template.setHashKeySerializer(RedisSerializer.string()); // 返回 return template; } }
解释说明:
当前配置类不是必须的,因为 Spring Boot 框架会自动装配 RedisTemplate 对象,但是默认的key序列化器为
JdkSerializationRedisSerializer,导致我们存到Redis中后的数据和原始数据有差别,故设置为
StringRedisSerializer序列化器。
- 在application.yml中添加读取application-dev.yml中的相关Redis配置
spring:
profiles:
active: dev
redis:
host: ${sky.redis.host}
port: ${sky.redis.port}
password: ${sky.redis.password}
database: ${sky.redis.database}
admin.ShopController
package com.sky.controller.admin; import com.sky.annotation.AutoFill; import com.sky.result.Result; import com.sky.service.ShopService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; /** * @Author:CharmingDaiDai * @Project:sky-take-out * @Date:2024/5/7 下午8:43 */ @Slf4j @Api(tags = "管理端店铺操作") @RestController("adminShopController") @RequestMapping("admin/shop") public class ShopController { @Resource private ShopService shopService; @ApiOperation(value = "设置店铺营业状态") @PutMapping("/{status}") public Result updateStatus(@PathVariable("status") String status) { return shopService.updateStatus(status); } @ApiOperation(value = "获取店铺营业状态") @GetMapping("/status") public Result getStatus() { return shopService.getStatus(); } }
user.ShopController
package com.sky.controller.admin.user; import com.sky.result.Result; import com.sky.service.ShopService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; /** * @Author:CharmingDaiDai * @Project:sky-take-out * @Date:2024/5/7 下午9:23 */ @Service @Api(tags = "用户端店铺操作") @RestController("userShopController") @RequestMapping("/user/shop") public class ShopController { @Resource private ShopService shopService; @ApiOperation(value = "获取店铺营业状态") @GetMapping("/status") public Result getStatus() { return shopService.getStatus(); } }
ShopServiceImpl
package com.sky.service; import com.sky.result.Result; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import javax.annotation.Resource; /** * @Author:CharmingDaiDai * @Project:sky-take-out * @Date:2024/5/7 下午8:43 */ @Service public class ShopServiceImpl implements ShopService { @Resource StringRedisTemplate stringRedisTemplate; @Override public Result updateStatus(String status) { stringRedisTemplate.opsForValue().set("SHOP_STATUS", status); return Result.success(); } @Override public Result getStatus() { String shopStatus = stringRedisTemplate.opsForValue().get("SHOP_STATUS"); return Result.success(Integer.valueOf(shopStatus)); } }
HTTPClient
HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。

HttpClient作用:
- 发送HTTP请求
- 接收响应数据
HttpClient应用场景:
当我们在使用扫描支付、查看地图、获取验证码、查看天气等功能时
其实,应用程序本身并未实现这些功能,都是在应用程序里访问提供这些功能的服务,访问这些服务需要发送HTTP请求,并且接收响应数据,可通过HttpClient来实现。

导入坐标
<dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <version>4.5.13</version> </dependency>
HttpClient的核心API:
- HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。
- HttpClients:可认为是构建器,可创建HttpClient对象。
- CloseableHttpClient:实现类,实现了HttpClient接口。
- HttpGet:Get方式请求类型。
- HttpPost:Post方式请求类型。
HttpClient发送请求步骤:
- 创建HttpClient对象
- 创建Http请求对象
- 调用HttpClient的execute方法发送请求
Get 单元测试
package com.sky.test; import org.apache.http.HttpEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.util.EntityUtils; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import java.io.IOException; /** * @Author:CharmingDaiDai * @Project:sky-take-out * @Date:2024/5/8 上午9:40 */ @SpringBootTest public class HttpClientTest { @Test public void testGet() throws IOException { // 创建httpclient对象 CloseableHttpClient httpClient = HttpClients.createDefault(); // 创建请求对象 HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status"); // 发送请求 CloseableHttpResponse response = httpClient.execute(httpGet); int statusCode = response.getStatusLine().getStatusCode(); System.out.println("statusCode = " + statusCode); HttpEntity entity = response.getEntity(); String body = EntityUtils.toString(entity); System.out.println("body = " + body); // 关闭资源 response.close(); httpClient.close(); } }
Post 单元测试
@Test public void testPost() throws Exception { // 创建httpclient对象 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 stringEntity = new StringEntity(jsonObject.toJSONString()); // 编码方式 stringEntity.setContentEncoding("UTF-8"); // 数据格式 stringEntity.setContentType("application/json"); httpPost.setEntity(stringEntity); // 发送请求 CloseableHttpResponse response = httpClient.execute(httpPost); int statusCode = response.getStatusLine().getStatusCode(); System.out.println("statusCode = " + statusCode); // 解析返回结果 HttpEntity entity = response.getEntity(); String body = EntityUtils.toString(entity); System.out.println("body = " + body); // 关闭资源 response.close(); httpClient.close(); }
ℹ️包名问题
在Spring Boot应用程序中,组件扫描(@ComponentScan
)是Spring框架用来查找、注册和管理bean的一个特性。默认情况下,Spring Boot会扫描主应用程序类(通常使用@SpringBootApplication
注解的类)所在的包以及其子包中的所有组件。
@SpringBootTest
是一个JUnit注解,它用于指示Spring Boot测试环境应该加载完整的应用程序上下文。当你使用@SpringBootTest
时,Spring Boot会启动一个Spring容器,这与应用程序的主运行时环境非常相似。
为什么要保持包名一致?
组件扫描: 如果你的测试类和被测试的组件位于相同的包或子包中,Spring Boot能够自动扫描并加载这些组件作为bean,无需额外配置。
路径一致性: 保持测试类的包名与
src/main/java
下的包名一致,确保了测试环境和应用程序运行时环境的一致性,这有助于减少环境差异导致的测试问题。依赖注入: 测试类中可能需要注入其他Spring管理的bean。如果测试类和被测试的组件在相同的包或子包中,Spring容器能够更容易地解析和注入这些依赖。
配置类识别: 如果你的应用程序使用配置类(带有
@Configuration
注解的类)来定义beans,这些配置类通常位于主应用程序包或其子包中。保持测试类在同一包结构中可以确保这些配置被正确加载。简化配置: Spring Boot的设计哲学之一是“约定优于配置”。通过遵循一定的包结构约定,你可以减少显式配置的需求,使应用程序更易于维护。
如果包名不一致会怎样?
如果测试类的包名与src/main/java
下的包名不一致,可能会导致以下问题:
组件未被扫描: 测试类和被测试的组件可能不会被Spring容器自动扫描到,因此不会被注册为bean。
依赖注入失败: 测试类可能无法自动注入所需的依赖,导致测试失败。
额外配置: 你可能需要在测试配置中显式指定要扫描的包,这增加了配置的复杂性。
环境不一致: 测试环境和应用程序运行时环境的不一致可能导致一些只在特定环境下出现的bug。
解决方案:
如果你希望测试类位于不同的包结构中,你可以通过以下方式解决:
使用
@DirtiesContext
注解: 这个注解可以确保每次测试后Spring上下文都被重新创建,有助于隔离测试。指定扫描路径: 在测试配置中使用
@ComponentScan
注解指定额外的包路径,以确保所有需要的组件都能被扫描。使用
@Import
注解: 在测试配置类中使用@Import
注解来导入需要的配置类或组件类。
封装好的工具类
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;
/**
* Http工具类
*/
public class HttpClientUtil {
static final int TIMEOUT_MSEC = 5 * 1000;
/**
* 发送GET方式请求
* @param url
* @param paramMap
* @return
*/
public static String doGet(String url,Map<String,String> paramMap){
// 创建Httpclient对象
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();
//创建GET请求
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;
}
/**
* 发送POST方式请求
* @param url
* @param paramMap
* @return
* @throws IOException
*/
public static String doPost(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
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());
// 执行http请求
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;
}
/**
* 发送POST方式请求
* @param url
* @param paramMap
* @return
* @throws IOException
*/
public static String doPost4Json(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
if (paramMap != null) {
//构造json格式数据
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());
// 执行http请求
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();
}
}
微信小程序开发
微信登录

步骤分析:
- 小程序端,调用wx.login()获取code,就是授权码。
- 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。
- 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。
- 开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。
- 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。
- 小程序端,收到自定义登录态,存储storage。
- 小程序端,后绪通过wx.request()发起业务请求时,携带token。
- 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。
- 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。
说明:
- 调用 wx.login() 获取 临时登录凭证code ,并回传到开发者服务器。
- 调用 auth.code2Session 接口,换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key。
之后开发者服务器可以根据用户标识来生成自定义登录态,用于后续业务逻辑中前后端交互时识别用户身份。
配置微信小程序appid
和密钥
application-dev.yml
sky: datasource: driver-class-name: com.mysql.cj.jdbc.Driver host: localhost port: 3306 database: sky_take_out username: cdd password: 1 redis: host: localhost port: 6379 password: 1 database: 10 lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 max-wait: 100ms wechat: appid: secret:
application.yml
... sky: ... wechat: appid: ${sky.wechat.appid} secret: ${sky.wechat.secret}
给用户设置jwt令牌
application.yml
server: port: 8080 spring: profiles: active: dev redis: host: ${sky.redis.host} port: ${sky.redis.port} password: ${sky.redis.password} database: ${sky.redis.database} main: allow-circular-references: true datasource: druid: driver-class-name: ${sky.datasource.driver-class-name} url: jdbc:mysql://${sky.datasource.host}:${sky.datasource.port}/${sky.datasource.database}?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true username: ${sky.datasource.username} password: ${sky.datasource.password} mybatis: #mapper配置文件 mapper-locations: classpath:mapper/*.xml type-aliases-package: com.sky.entity configuration: #开启驼峰命名 map-underscore-to-camel-case: true logging: level: com: sky: mapper: debug service: info controller: info sky: jwt: # 设置jwt签名加密时使用的秘钥 admin-secret-key: itcast # 设置jwt过期时间 admin-ttl: 720000000 # 设置前端传递过来的令牌名称 admin-token-name: token user-secret-key: itheima user-ttl: 7200000 user-token-name: authentication wechat: appid: ${sky.wechat.appid} secret: ${sky.wechat.secret}
UserController
package com.sky.controller.admin.user; import com.sky.constant.JwtClaimsConstant; import com.sky.dto.UserLoginDTO; import com.sky.entity.User; import com.sky.properties.JwtProperties; import com.sky.result.Result; import com.sky.service.UserService; import com.sky.utils.JwtUtil; import com.sky.vo.UserLoginVO; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.annotation.Resource; import java.util.HashMap; import java.util.Map; /** * @Author:CharmingDaiDai * @Project:sky-take-out * @Date:2024/5/8 下午12:47 */ @Slf4j @RestController @RequestMapping("/user/user") @Api(tags = "用户端用户管理") public class UserController { @Resource private UserService userService; @Resource private JwtProperties jwtProperties; @PostMapping("/login") @ApiOperation(value = "微信登录") public Result wxLogin(@RequestBody UserLoginDTO userLoginDTO) { log.info("微信用户登录:{}",userLoginDTO.getCode()); // 微信登录 User user = userService.wxLogin(userLoginDTO); // 为微信用户生成jwt令牌 Map<String, Object> claims = new HashMap<>(); claims.put(JwtClaimsConstant.USER_ID,user.getId()); String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(), jwtProperties.getUserTtl(), claims); UserLoginVO userLoginVO = UserLoginVO.builder() .id(user.getId()) .openid(user.getOpenid()) .token(token) .build(); return Result.success(userLoginVO); } }
实现类
package com.sky.service.impl; import com.alibaba.fastjson.JSONObject; import com.sky.constant.MessageConstant; import com.sky.dto.UserLoginDTO; import com.sky.entity.User; import com.sky.exception.LoginFailedException; import com.sky.mapper.UserMapper; import com.sky.properties.WeChatProperties; import com.sky.service.UserService; import com.sky.utils.HttpClientUtil; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.time.LocalDateTime; import java.util.HashMap; import java.util.Map; /** * @Author:CharmingDaiDai * @Project:sky-take-out * @Date:2024/5/8 下午3:54 */ @Service public class UserServiceImpl implements UserService { @Resource private UserMapper userMapper; // 微信服务接口地址 public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session"; @Resource private WeChatProperties weChatProperties; @Override public User wxLogin(UserLoginDTO userLoginDTO) { // 调用微信接口服务,获得当前微信用户的openid Map<String, String> map = new HashMap<>(); map.put("appid", weChatProperties.getAppid()); map.put("secret", weChatProperties.getSecret()); map.put("grant_type", "authorization_code"); map.put("js_code", userLoginDTO.getCode()); String json = HttpClientUtil.doGet(WX_LOGIN, map); JSONObject jsonObject = JSONObject.parseObject(json); String openId = jsonObject.getString("openid"); // 判空 if (openId == null || openId.equals("")) { 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; } }
UserMapper
package com.sky.mapper; import com.sky.entity.User; import org.apache.ibatis.annotations.Insert; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Select; /** * @Author:CharmingDaiDai * @Project:sky-take-out * @Date:2024/5/8 下午4:15 */ @Mapper public interface UserMapper { @Select("select * from user where openid = #{openid}") User getByOpenid(String openid); void insert(User user); }
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.sky.mapper.UserMapper"> <insert id="insert" useGeneratedKeys="true" keyProperty="id"> insert into user (openid, name, phone, sex, id_number, avatar, create_time) values (#{openid}, #{name}, #{phone}, #{sex}, #{idNumber}, #{avatar}, #{createTime}) </insert> </mapper>
用户拦截器
package com.sky.interceptor; import com.sky.constant.JwtClaimsConstant; import com.sky.context.BaseContext; import com.sky.properties.JwtProperties; import com.sky.utils.JwtUtil; import io.jsonwebtoken.Claims; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * jwt令牌校验的拦截器 */ @Component @Slf4j public class JwtTokenUserInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; /** * 校验jwt * * @param request * @param response * @param handler * @return * @throws Exception */ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判断当前拦截到的是Controller的方法还是其他资源 if (!(handler instanceof HandlerMethod)) { //当前拦截到的不是动态方法,直接放行 return true; } //1、从请求头中获取令牌 String token = request.getHeader(jwtProperties.getUserTokenName()); //2、校验令牌 try { log.info("jwt校验:{}", token); Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token); Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString()); log.info("当前用户id:", userId); BaseContext.setCurrentId(userId); //3、通过,放行 return true; } catch (Exception ex) { //4、不通过,响应401状态码 response.setStatus(401); return false; } } public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { log.info("移除ThreadLocal"); BaseContext.removeCurrentId(); } }
添加拦截器
WebMvcConfiguration
/** * 注册自定义拦截器 * * @param registry */ 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"); }
缓存商品

缓存逻辑分析:
- 每个分类下的菜品保存一份缓存数据
- 数据库中菜品数据有变更时清理缓存数据

DishController
package com.sky.controller.admin.user; import com.sky.constant.StatusConstant; import com.sky.entity.Dish; import com.sky.result.Result; import com.sky.service.DishService; import com.sky.vo.DishVO; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController("userDishController") @RequestMapping("/user/dish") @Slf4j @Api(tags = "C端-菜品浏览接口") public class DishController { @Autowired private DishService dishService; /** * 根据分类id查询菜品 * * @param categoryId * @return */ @GetMapping("/list") @ApiOperation("根据分类id查询菜品") public Result<List<DishVO>> list(Long categoryId) { return dishService.listWithFlavor(categoryId); } }
实现类
@Override /** * 条件查询菜品和口味 * @param categoryId * @return */ public Result<List<DishVO>> listWithFlavor(Long categoryId) { String key = "dish:" + categoryId; // 从redis中查询 List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key); if (list != null && !list.isEmpty()) { return Result.success(list); } Dish dish = new Dish(); dish.setCategoryId(categoryId); //查询起售中的菜品 dish.setStatus(StatusConstant.ENABLE); // 从数据库中查询 List<Dish> dishList = dishMapper.list(dish); redisTemplate.opsForValue().set(key, dishList); List<DishVO> dishVOList = new ArrayList<>(); for (Dish d : dishList) { DishVO dishVO = new DishVO(); BeanUtils.copyProperties(d, dishVO); //根据菜品id查询对应的口味 List<DishFlavor> flavors = dishFlavorMapper.getByDishId(d.getId()); dishVO.setFlavors(flavors); dishVOList.add(dishVO); } return Result.success(dishVOList); }
缓存一致性
为了保证数据库和Redis中的数据保持一致,修改管理端接口 DishController 的相关方法,加入清理缓存逻辑。
封装函数
@Resource private RedisTemplate redisTemplate; /** * 清理缓存数据 * @param pattern */ private void cleanCache(String pattern){ Set keys = redisTemplate.keys(pattern); redisTemplate.delete(keys); }
需要改造的方法:
新增菜品
@PostMapping() @ApiOperation(value = "新增菜品") public Result add(@RequestBody DishDTO dishDTO){ log.info("新增菜品:{}", dishDTO); //清理缓存数据 String key = "dish_" + dishDTO.getCategoryId(); cleanCache(key); return dishService.add(dishDTO); }
修改菜品
@ApiOperation(value = "修改菜品") @PutMapping public Result update(@RequestBody DishDTO dishDTO){ log.info("修改菜品:{}", dishDTO); //将所有的菜品缓存数据清理掉,所有以dish_开头的key cleanCache("dish_*"); return dishService.update(dishDTO); }
批量删除菜品
@ApiOperation(value = "删除菜品") @DeleteMapping() // public Result delete(@RequestParam List<Long> ids){ public Result delete(String ids){ log.info("删除菜品:{}", ids); //将所有的菜品缓存数据清理掉,所有以dish_开头的key cleanCache("dish_*"); return dishService.delete(ids); }
起售、停售菜品
@ApiOperation(value = "起售/停售") @PostMapping("/status/{status}") public Result updateStatus(@PathVariable Integer status, Long id){ log.info("起售/停售菜品:{}, {}", status, id); //将所有的菜品缓存数据清理掉,所有以dish_开头的key cleanCache("dish_*"); return dishService.updateStatus(status, id); }
Spring Cache
Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:
- EHCache
- Caffeine
- Redis(常用)
可以轻松更换底层实现,不用修改代码
起步依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId> <version>2.7.3</version>
</dependency>
常用注解
在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:
注解 | 说明 |
---|---|
@EnableCaching | 开启缓存注解功能,通常加在启动类上 |
@Cacheable | 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中 |
@CachePut | 将方法的返回值放到缓存中 |
@CacheEvict | 将一条或多条数据从缓存中删除 |
在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。
例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。
引导类上加@EnableCaching:
@Slf4j
@SpringBootApplication
@EnableCaching//开启缓存注解功能
public class CacheDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CacheDemoApplication.class,args);
log.info("项目启动成功...");
}
}
@CachePut
将方法的返回值写入缓存
@PostMapping
@CachePut(value = "userCache", key = "#user.id")//key的生成:userCache::1
public User save(@RequestBody User user){
userMapper.insert(user);
return user;
}
**说明:**key的写法如下
#user.id : #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key ;
#result.id : #result代表方法返回值,该表达式 代表以返回对象的id属性作为key ;
#p0.id:#p0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;
#a0.id:#a0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key ;
#root.args[0].id:#root.args[0]指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数
的id属性作为key ;
底层使用代理
@Cacheable
/**
* Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据, *调用方法并将方法返回值放到缓存中
* value:缓存的名称,每个缓存名称下面可以有多个key
* key:缓存的key
*/
@GetMapping
@Cacheable(cacheNames = "userCache",key="#id")
public User getById(Long id){
User user = userMapper.getById(id);
return user;
}
作用: 在方法执行前,spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
value: 缓存的名称,每个缓存名称下面可以有多个key
key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法
@CacheEvict
@DeleteMapping
@CacheEvict(cacheNames = "userCache",key = "#id")//删除某个key对应的缓存数据
public void deleteById(Long id){
userMapper.deleteById(id);
}
@DeleteMapping("/delAll")
@CacheEvict(cacheNames = "userCache",allEntries = true)//删除userCache下所有的缓存数据
public void deleteAll(){
userMapper.deleteAll();
}
缓存套餐
实现步骤:
1). 导入Spring Cache和Redis相关maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>🚩
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>🚩
</dependency>
2). 在启动类上加入@EnableCaching注解,开启缓存注解功能
package com.sky;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
🚩🚩🚩@EnableCaching
public class SkyApplication {
public static void main(String[] args) {
SpringApplication.run(SkyApplication.class, args);
log.info("server started");
}
}
3). 在用户端接口SetmealController的 list 方法上加入@Cacheable注解
@GetMapping("/list")
@ApiOperation("根据分类id查询套餐")
@Cacheable(cacheNames = "setmealCache", key = "#categoryId")
public Result<List<Setmeal>> list(Long categoryId) {
Setmeal setmeal = new Setmeal();
setmeal.setCategoryId(categoryId);
setmeal.setStatus(StatusConstant.ENABLE);
List<Setmeal> list = setmealService.list(setmeal);
return Result.success(list);
}
4). 在管理端接口SetmealController的 save、delete、update、startOrStop等方法上加入CacheEvict注解
/**
* 新增套餐
*
* @param setmealDTO
* @return
*/
@PostMapping
@ApiOperation("新增套餐")
@CacheEvict(cacheNames = "setmealCache", key = "#setmealDTO.categoryId")
public Result save(@RequestBody SetmealDTO setmealDTO) {
setmealService.saveWithDish(setmealDTO);
return Result.success();
}
/**
* 批量删除套餐
*
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("批量删除套餐")
@CacheEvict(cacheNames = "setmealCache", allEntries = true)
public Result delete(@RequestParam List<Long> ids) {
setmealService.deleteBatch(ids);
return Result.success();
}
/**
* 修改套餐
*
* @param setmealDTO
* @return
*/
@PutMapping
@ApiOperation("修改套餐")
@CacheEvict(cacheNames = "setmealCache", allEntries = true)
public Result update(@RequestBody SetmealDTO setmealDTO) {
setmealService.update(setmealDTO);
return Result.success();
}
/**
* 套餐起售停售
*
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("套餐起售停售")
@CacheEvict(cacheNames = "setmealCache", allEntries = true)
public Result startOrStop(@PathVariable Integer status, Long id) {
setmealService.startOrStop(status, id);
return Result.success();
}
购物车
ShoppingCartController
package com.sky.controller.user;
import com.sky.dto.ShoppingCartDTO;
import com.sky.entity.ShoppingCart;
import com.sky.result.Result;
import com.sky.service.ShoppingCartService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* @Author:CharmingDaiDai
* @Project:sky-take-out
* @Date:2024/5/9 下午1:22
*/
@RestController
@RequestMapping("/user/shoppingCart")
@Slf4j
@Api(tags = "C端-购物车")
public class ShoppingCartController {
@Resource
private ShoppingCartService shoppingCartService;
@PostMapping("/add")
@ApiOperation(value = "添加商品到🛒")
public Result addShoppingCart(@RequestBody ShoppingCartDTO shoppingCartDTO) {
log.info("添加购物车");
return shoppingCartService.add(shoppingCartDTO);
}
@GetMapping("/list")
@ApiOperation(value = "查询🛒")
public Result<List<ShoppingCart>> list() {
return shoppingCartService.show();
}
@DeleteMapping("/clean")
@ApiOperation(value = "清空🛒")
public Result deleteShoppingCart() {
return shoppingCartService.clean();
}
/**
* 删除购物车中一个商品
* @param shoppingCartDTO
* @return
*/
@PostMapping("/sub")
@ApiOperation("删除购物车中一个商品")
public Result sub(@RequestBody ShoppingCartDTO shoppingCartDTO){
log.info("删除购物车中一个商品,商品:{}", shoppingCartDTO);
shoppingCartService.subShoppingCart(shoppingCartDTO);
return Result.success();
}
}
ShoppingCartService
package com.sky.service;
import com.sky.dto.ShoppingCartDTO;
import com.sky.entity.ShoppingCart;
import com.sky.result.Result;
import java.util.List;
/**
* @Author:CharmingDaiDai
* @Project:sky-take-out
* @Date:2024/5/9 下午1:50
*/
public interface ShoppingCartService {
Result add(ShoppingCartDTO shoppingCartDTO);
Result<List<ShoppingCart>> show();
Result clean();
/**
* 删除购物车中一个商品
* @param shoppingCartDTO
*/
void subShoppingCart(ShoppingCartDTO shoppingCartDTO);
}
实现类
package com.sky.service.impl;
import com.sky.context.BaseContext;
import com.sky.dto.ShoppingCartDTO;
import com.sky.entity.Dish;
import com.sky.entity.Setmeal;
import com.sky.entity.ShoppingCart;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.mapper.ShoppingCartMapper;
import com.sky.result.Result;
import com.sky.service.ShoppingCartService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
/**
* @Author:CharmingDaiDai
* @Project:sky-take-out
* @Date:2024/5/9 下午1:57
*/
@Service
@Slf4j
public class ShoppingCartServiceImpl implements ShoppingCartService {
@Resource
private ShoppingCartMapper shoppingCartMapper;
@Resource
private DishMapper dishMapper;
@Resource
private SetmealMapper setmealMapper;
@Override
public Result add(ShoppingCartDTO shoppingCartDTO) {
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
shoppingCart.setUserId(BaseContext.getCurrentId());
List<ShoppingCart> shoppingCarts = shoppingCartMapper.list(shoppingCart);
if (shoppingCarts != null && !shoppingCarts.isEmpty()) {
ShoppingCart cart = shoppingCarts.get(0);
// update shopping_cart set number = ? where id = ?
cart.setNumber(cart.getNumber() + 1);
shoppingCartMapper.updateNumberById(shoppingCart);
}
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(shoppingCartDTO.getSetmealId());
shoppingCart.setName(setmeal.getName());
shoppingCart.setImage(setmeal.getImage());
shoppingCart.setAmount(setmeal.getPrice());
}
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingCartMapper.insert(shoppingCart);
}
return Result.success(shoppingCart);
}
@Override
public Result<List<ShoppingCart>> show() {
Long currentId = BaseContext.getCurrentId();
List<ShoppingCart> shoppingCarts = shoppingCartMapper.list(ShoppingCart.builder().userId(currentId).build());
return Result.success(shoppingCarts);
}
@Override
public Result clean() {
Long currentId = BaseContext.getCurrentId();
shoppingCartMapper.delete(currentId);
return Result.success();
}
/**
* 删除购物车中一个商品
* @param shoppingCartDTO
*/
public void subShoppingCart(ShoppingCartDTO shoppingCartDTO) {
ShoppingCart shoppingCart = new ShoppingCart();
BeanUtils.copyProperties(shoppingCartDTO,shoppingCart);
//设置查询条件,查询当前登录用户的购物车数据
shoppingCart.setUserId(BaseContext.getCurrentId());
List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
if(list != null && list.size() > 0){
shoppingCart = list.get(0);
Integer number = shoppingCart.getNumber();
if(number == 1){
//当前商品在购物车中的份数为1,直接删除当前记录
shoppingCartMapper.deleteById(shoppingCart.getId());
}else {
//当前商品在购物车中的份数不为1,修改份数即可
shoppingCart.setNumber(shoppingCart.getNumber() - 1);
shoppingCartMapper.updateNumberById(shoppingCart);
}
}
}
}
ShoppingCartMapper
package com.sky.mapper;
import com.sky.entity.ShoppingCart;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;
import java.util.List;
/**
* @Author:CharmingDaiDai
* @Project:sky-take-out
* @Date:2024/5/9 下午1:59
*/
@Mapper
public interface ShoppingCartMapper {
/**
* 条件查询
*
* @param shoppingCart
* @return
*/
List<ShoppingCart> list(ShoppingCart shoppingCart);
/**
* 更新商品数量
*
* @param shoppingCart
*/
@Update("update shopping_cart set number = #{number} where id = #{id}")
void updateNumberById(ShoppingCart shoppingCart);
/**
* 插入购物车数据
*
* @param shoppingCart
*/
@Insert("insert into shopping_cart (name, user_id, dish_id, setmeal_id, dish_flavor, number, amount, image, create_time) " +
" values (#{name},#{userId},#{dishId},#{setmealId},#{dishFlavor},#{number},#{amount},#{image},#{createTime})")
void insert(ShoppingCart shoppingCart);
@Delete("delete from shopping_cart where user_id = #{currentId}")
void delete(Long currentId);
/**
* 根据id删除购物车数据
* @param id
*/
@Delete("delete from shopping_cart where id = #{id}")
void deleteById(Long id);
}
ShoppingCartMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.ShoppingCartMapper">
<select id="list" parameterType="ShoppingCart" resultType="ShoppingCart">
select * from shopping_cart
<where>
<if test="userId != null">
and user_id = #{userId}
</if>
<if test="dishId != null">
and dish_id = #{dishId}
</if>
<if test="setmealId != null">
and setmeal_id = #{setmealId}
</if>
<if test="dishFlavor != null">
and dish_flavor = #{dishFlavor}
</if>
</where>
order by create_time desc
</select>
</mapper>
地址簿
AddressBookController
package com.sky.controller.user;
import com.sky.context.BaseContext;
import com.sky.entity.AddressBook;
import com.sky.result.Result;
import com.sky.service.AddressBookService;
import io.swagger.annotapackage com.sky.service;
import com.sky.entity.AddressBook;
import java.util.List;
public interface AddressBookService {
List<AddressBook> list(AddressBook addressBook);
void save(AddressBook addressBook);
AddressBook getById(Long id);
void update(AddressBook addressBook);
void setDefault(AddressBook addressBook);
void deleteById(Long id);
}
tions.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/user/addressBook")
@Api(tags = "C端-地址簿接口")
public class AddressBookController {
@Autowired
private AddressBookService addressBookService;
/**
* 查询当前登录用户的所有地址信息
*
* @return
*/
@GetMapping("/list")
@ApiOperation("查询当前登录用户的所有地址信息")
public Result<List<AddressBook>> list() {
AddressBook addressBook = new AddressBook();
addressBook.setUserId(BaseContext.getCurrentId());
List<AddressBook> list = addressBookService.list(addressBook);
return Result.success(list);
}
/**
* 新增地址
*
* @param addressBook
* @return
*/
@PostMapping
@ApiOperation("新增地址")
public Result save(@RequestBody AddressBook addressBook) {
addressBookService.save(addressBook);
return Result.success();
}
@GetMapping("/{id}")
@ApiOperation("根据id查询地址")
public Result<AddressBook> getById(@PathVariable Long id) {
AddressBook addressBook = addressBookService.getById(id);
return Result.success(addressBook);
}
/**
* 根据id修改地址
*
* @param addressBook
* @return
*/
@PutMapping
@ApiOperation("根据id修改地址")
public Result update(@RequestBody AddressBook addressBook) {
addressBookService.update(addressBook);
return Result.success();
}
/**
* 设置默认地址
*
* @param addressBook
* @return
*/
@PutMapping("/default")
@ApiOperation("设置默认地址")
public Result setDefault(@RequestBody AddressBook addressBook) {
addressBookService.setDefault(addressBook);
return Result.success();
}
/**
* 根据id删除地址
*
* @param id
* @return
*/
@DeleteMapping
@ApiOperation("根据id删除地址")
public Result deleteById(Long id) {
addressBookService.deleteById(id);
return Result.success();
}
/**
* 查询默认地址
*/
@GetMapping("default")
@ApiOperation("查询默认地址")
public Result<AddressBook> getDefault() {
//SQL:select * from address_book where user_id = ? and is_default = 1
AddressBook addressBook = new AddressBook();
addressBook.setIsDefault(1);
addressBook.setUserId(BaseContext.getCurrentId());
List<AddressBook> list = addressBookService.list(addressBook);
if (list != null && list.size() == 1) {
return Result.success(list.get(0));
}
return Result.error("没有查询到默认地址");
}
}
AddressBookService
package com.sky.service;
import com.sky.entity.AddressBook;
import java.util.List;
public interface AddressBookService {
List<AddressBook> list(AddressBook addressBook);
void save(AddressBook addressBook);
AddressBook getById(Long id);
void update(AddressBook addressBook);
void setDefault(AddressBook addressBook);
void deleteById(Long id);
}
AddressBookServiceImpl
package com.sky.service.impl;
import com.sky.context.BaseContext;
import com.sky.entity.AddressBook;
import com.sky.mapper.AddressBookMapper;
import com.sky.service.AddressBookService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Slf4j
public class AddressBookServiceImpl implements AddressBookService {
@Autowired
private AddressBookMapper addressBookMapper;
/**
* 条件查询
*
* @param addressBook
* @return
*/
public List<AddressBook> list(AddressBook addressBook) {
return addressBookMapper.list(addressBook);
}
/**
* 新增地址
*
* @param addressBook
*/
public void save(AddressBook addressBook) {
addressBook.setUserId(BaseContext.getCurrentId());
addressBook.setIsDefault(0);
addressBookMapper.insert(addressBook);
}
/**
* 根据id查询
*
* @param id
* @return
*/
public AddressBook getById(Long id) {
AddressBook addressBook = addressBookMapper.getById(id);
return addressBook;
}
/**
* 根据id修改地址
*
* @param addressBook
*/
public void update(AddressBook addressBook) {
addressBookMapper.update(addressBook);
}
/**
* 设置默认地址
*
* @param addressBook
*/
@Transactional
public void setDefault(AddressBook addressBook) {
//1、将当前用户的所有地址修改为非默认地址 update address_book set is_default = ? where user_id = ?
addressBook.setIsDefault(0);
addressBook.setUserId(BaseContext.getCurrentId());
addressBookMapper.updateIsDefaultByUserId(addressBook);
//2、将当前地址改为默认地址 update address_book set is_default = ? where id = ?
addressBook.setIsDefault(1);
addressBookMapper.update(addressBook);
}
/**
* 根据id删除地址
*
* @param id
*/
public void deleteById(Long id) {
addressBookMapper.deleteById(id);
}
}
AddressBookMapper
package com.sky.mapper;
import com.sky.entity.AddressBook;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface AddressBookMapper {
/**
* 条件查询
* @param addressBook
* @return
*/
List<AddressBook> list(AddressBook addressBook);
/**
* 新增
* @param addressBook
*/
@Insert("insert into address_book" +
" (user_id, consignee, phone, sex, province_code, province_name, city_code, city_name, district_code," +
" district_name, detail, label, is_default)" +
" values (#{userId}, #{consignee}, #{phone}, #{sex}, #{provinceCode}, #{provinceName}, #{cityCode}, #{cityName}," +
" #{districtCode}, #{districtName}, #{detail}, #{label}, #{isDefault})")
void insert(AddressBook addressBook);
/**
* 根据id查询
* @param id
* @return
*/
@Select("select * from address_book where id = #{id}")
AddressBook getById(Long id);
/**
* 根据id修改
* @param addressBook
*/
void update(AddressBook addressBook);
/**
* 根据 用户id修改 是否默认地址
* @param addressBook
*/
@Update("update address_book set is_default = #{isDefault} where user_id = #{userId}")
void updateIsDefaultByUserId(AddressBook addressBook);
/**
* 根据id删除地址
* @param id
*/
@Delete("delete from address_book where id = #{id}")
void deleteById(Long id);
}
AddressBookMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.AddressBookMapper">
<select id="list" parameterType="AddressBook" resultType="AddressBook">
select * from address_book
<where>
<if test="userId != null">
and user_id = #{userId}
</if>
<if test="phone != null">
and phone = #{phone}
</if>
<if test="isDefault != null">
and is_default = #{isDefault}
</if>
</where>
</select>
<update id="update" parameterType="addressBook">
update address_book
<set>
<if test="consignee != null">
consignee = #{consignee},
</if>
<if test="sex != null">
sex = #{sex},
</if>
<if test="phone != null">
phone = #{phone},
</if>
<if test="detail != null">
detail = #{detail},
</if>
<if test="label != null">
label = #{label},
</if>
<if test="isDefault != null">
is_default = #{isDefault},
</if>
</set>
where id = #{id}
</update>
</mapper>
用户下单
OrderController
package com.sky.controller.user;
import com.sky.dto.OrdersSubmitDTO;
import com.sky.result.Result;
import com.sky.service.OrderService;
import com.sky.vo.OrderSubmitVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @Author:CharmingDaiDai
* @Project:sky-take-out
* @Date:2024/5/9 下午8:49
*/
@RestController("UserOrderController")
@RequestMapping("/user/order")
@Api(tags = "订单管理")
@Slf4j
public class OrderController {
@Resource
private OrderService orderService;
@PostMapping("/submit")
@ApiOperation("用户下单")
public Result<OrderSubmitVO> submit(@RequestBody OrdersSubmitDTO ordersSubmitDTO) {
log.info("用户下单:{}", ordersSubmitDTO);
return orderService.submit(ordersSubmitDTO);
}
}
OrderService
package com.sky.service;
import com.sky.dto.OrdersSubmitDTO;
import com.sky.result.Result;
import com.sky.vo.OrderSubmitVO;
/**
* @Author:CharmingDaiDai
* @Project:sky-take-out
* @Date:2024/5/9 下午8:50
*/
public interface OrderService {
Result<OrderSubmitVO> submit(OrdersSubmitDTO ordersSubmitDTO);
}
OrderServiceImpl
package com.sky.service.impl;
import com.sky.constant.MessageConstant;
import com.sky.context.BaseContext;
import com.sky.dto.OrdersSubmitDTO;
import com.sky.entity.AddressBook;
import com.sky.entity.OrderDetail;
import com.sky.entity.Orders;
import com.sky.entity.ShoppingCart;
import com.sky.exception.AddressBookBusinessException;
import com.sky.exception.ShoppingCartBusinessException;
import com.sky.mapper.AddressBookMapper;
import com.sky.mapper.OrderDetailMapper;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.ShoppingCartMapper;
import com.sky.result.Result;
import com.sky.service.OrderService;
import com.sky.vo.OrderSubmitVO;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
/**
* @Author:CharmingDaiDai
* @Project:sky-take-out
* @Date:2024/5/9 下午8:50
*/
@Service
public class OrderServiceImpl implements OrderService {
@Resource
private AddressBookMapper addressBookMapper;
@Resource
private ShoppingCartMapper shoppingCartMapper;
@Resource
private OrderMapper orderMapper;
@Resource
private OrderDetailMapper orderDetailMapper;
@Override
public Result<OrderSubmitVO> submit(OrdersSubmitDTO ordersSubmitDTO) {
// 处理各种业务异常(地址簿为空,购物车数据为空)
AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
if (addressBook == null) {
throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
}
// 查询当前用户的购物车数据
Long currentId = BaseContext.getCurrentId();
ShoppingCart shoppingCart = new ShoppingCart();
shoppingCart.setId(currentId);
List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);
if (shoppingCartList == null || shoppingCartList.isEmpty()) {
throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
}
//构造订单数据
Orders order = new Orders();
BeanUtils.copyProperties(ordersSubmitDTO,order);
order.setPhone(addressBook.getPhone());
order.setAddress(addressBook.getDetail());
order.setConsignee(addressBook.getConsignee());
order.setNumber(String.valueOf(System.currentTimeMillis()));
order.setUserId(currentId);
order.setStatus(Orders.PENDING_PAYMENT);
order.setPayStatus(Orders.UN_PAID);
order.setOrderTime(LocalDateTime.now());
// 向订单表插入1条数据
orderMapper.insert(order);
List<OrderDetail> orderDetails = new ArrayList<>();
// 向订单明细表插入n条数据
for (ShoppingCart cart : shoppingCartList) {
// 订单明细
OrderDetail orderDetail = new OrderDetail();
BeanUtils.copyProperties(cart,orderDetail);
// 设置订单明细关联的订单🆔
orderDetail.setOrderId(order.getId());
}
orderDetailMapper.insertBatch(orderDetails);
// 清空当前用户的购物车数据
shoppingCartMapper.deleteById(currentId);
// 封装VO返回结果
OrderSubmitVO orderSubmitVO = OrderSubmitVO.builder()
.id(order.getId())
.orderNumber(order.getNumber())
.orderAmount(order.getAmount())
.orderTime(order.getOrderTime())
.build();
return Result.success(orderSubmitVO);
}
}
OrderMapper
package com.sky.mapper;
import com.sky.entity.Orders;
import org.apache.ibatis.annotations.Mapper;
/**
* @Author:CharmingDaiDai
* @Project:sky-take-out
* @Date:2024/5/9 下午8:51
*/
@Mapper
public interface OrderMapper {
void insert(Orders order);
}
OrderMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderMapper">
<insert id="insert" parameterType="Orders" useGeneratedKeys="true" keyProperty="id">
insert into orders
(number, status, user_id, address_book_id, order_time, checkout_time, pay_method, pay_status, amount, remark,
phone, address, consignee, estimated_delivery_time, delivery_status, pack_amount, tableware_number,
tableware_status)
values (#{number}, #{status}, #{userId}, #{addressBookId}, #{orderTime}, #{checkoutTime}, #{payMethod},
#{payStatus}, #{amount}, #{remark}, #{phone}, #{address}, #{consignee},
#{estimatedDeliveryTime}, #{deliveryStatus}, #{packAmount}, #{tablewareNumber}, #{tablewareStatus})
</insert>
</mapper>
OrderDetailMapper
package com.sky.mapper;
import com.sky.entity.OrderDetail;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* @Author:CharmingDaiDai
* @Project:sky-take-out
* @Date:2024/5/9 下午8:51
*/
@Mapper
public interface OrderDetailMapper {
void insertBatch(List<OrderDetail> orderDetails);
}
OrderDetailMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderDetailMapper">
<insert id="insertBatch" parameterType="list">
insert into order_detail
(name, order_id, dish_id, setmeal_id, dish_flavor, number, amount, image)
values
<foreach collection="orderDetails" item="od" separator=",">
(#{od.name},#{od.orderId},#{od.dishId},#{od.setmealId},#{od.dishFlavor},
#{od.number},#{od.amount},#{od.image})
</foreach>
</insert>
</mapper>
订单支付

Spring Task
Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。
**定位:**定时任务框架
**作用:**定时自动执行某段Java代码
cron表达式
cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间
**构成规则:**分为6或7个域,由空格分隔开,每个域代表一个含义
每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)
通配符:
* 表示所有值;
? 表示未说明的值,即不关心它为何值;
- 表示一个指定的范围;
, 表示附加一个可能值;
/ 符号前表示开始时间,符号后表示每次递增的值;
cron表达式案例:
*/5 * * * * ? 每隔5秒执行一次
0 */1 * * * ? 每隔1分钟执行一次
0 0 5-15 * * ? 每天5-15点整点触发
0 0/3 * * * ? 每三分钟触发一次
0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
案例
使用步骤
导入maven坐标spring-context
启动类添加注解 @EnableScheduling 开启任务调度
package com.sky;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
@EnableCaching
@EnableScheduling
public class SkyApplication {
public static void main(String[] args) {
SpringApplication.run(SkyApplication.class, args);
log.info("server started");
}
}
自定义定时任务类
package com.sky.task;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* @Author:CharmingDaiDai
* @Project:sky-take-out
* @Date:2024/5/10 下午3:37
*/
@Slf4j
@Component
public class MyTask {
@Scheduled(cron = "0/5 * * * * ?")
public void executeTask(){
log.info("MyTask execute task");
}
}
2024-05-10 15:38:35.012 INFO 5736 --- [ scheduling-1] com.sky.task.MyTask : MyTask execute task
2024-05-10 15:38:40.012 INFO 5736 --- [ scheduling-1] com.sky.task.MyTask : MyTask execute task
2024-05-10 15:38:45.003 INFO 5736 --- [ scheduling-1] com.sky.task.MyTask : MyTask execute task
订单状态定时处理
task.OrderTask
package com.sky.task;
import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
/**
* @Author:CharmingDaiDai
* @Project:sky-take-out
* @Date:2024/5/10 下午3:55
*/
@Component
@Slf4j
public class OrderTask {
@Resource
private OrderMapper orderMapper;
// 每分钟
@Scheduled(cron = "0 * * * * ?")
public void processTimeoutOrder(){
log.info("取消超时订单");
LocalDateTime localDateTime = LocalDateTime.now().plusMinutes(-15);
List<Orders> list = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, localDateTime);
list.forEach(order -> {
order.setStatus(Orders.CANCELLED);
order.setCancelReason("订单超时");
orderMapper.update(order);
});
}
}
OrderMapper
@Select("select * from orders where status = #{status} and order_time < #{orderTime}")
List<Orders> getByStatusAndOrderTimeLT(Integer status, LocalDateTime orderTime);
WebSocket
WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。
HTTP协议和WebSocket协议对比:
- HTTP是短连接
- WebSocket是长连接
- HTTP通信是单向的,基于请求响应模式
- WebSocket支持双向通信
- HTTP和WebSocket底层都是TCP连接
WebSocket缺点:
服务器长期维护长连接需要一定的成本
各个浏览器支持程度不一
WebSocket 是长连接,受网络限制比较大,需要处理好重连
**结论:**WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用
WebSocket应用场景:
不需要发送请求,服务器主动推送的数据
1). 视频弹幕
2). 网页聊天
3). 体育实况更新
4). 股票基金报价实时更新
<!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);
//判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
//连接WebSocket节点
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");
}
//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function(){
websocket.close();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//发送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}
//关闭连接
function closeWebSocket() {
websocket.close();
}
</script>
</html>
导入maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
定义WebSocket服务端组件
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;
/**
* WebSocket服务
*/
@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);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}
/**
* 连接关闭调用的方法
*
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);
}
/**
* 群发
*
* @param message
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
//服务器向客户端发送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
定义配置类,注册WebSocket的服务端组件
package com.sky.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebSocket配置类,用于注册WebSocket的Bean
*/
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
定义定时任务类,定时向客户端推送数据
package com.sky.task;
import com.sky.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Component
public class WebSocketTask {
@Autowired
private WebSocketServer webSocketServer;
/**
* 通过WebSocket每隔5秒向客户端发送消息
*/
@Scheduled(cron = "0/5 * * * * ?")
public void sendMessageToClient() {
webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
}
}
来单提醒
用户下单并且支付成功后,需要第一时间通知外卖商家。通知的形式有如下两种:
- 语音播报
- 弹出提示框
设计思路:
- 通过WebSocket实现管理端页面和服务端保持长连接状态
- 当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息
- 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
- 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content
- type 为消息类型,1为来单提醒 2为客户催单
- orderId 为订单id
- content 为消息内容
Apache ECharts
快速上手 - 使用手册 - Apache ECharts
echarts CDN by jsDelivr - A CDN for npm and GitHub
cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ECharts</title>
<!-- 引入刚刚下载的 ECharts 文件 -->
<script src="echarts.js"></script>
</head>
<body>
<!-- 为 ECharts 准备一个定义了宽高的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
var option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data: ['销量']
},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
</script>
</body>
</html>
营业额统计
ReportController
package com.sky.controller.admin;
import com.sky.result.Result;
import com.sky.service.ReportService;
import com.sky.vo.TurnoverReportVO;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.time.LocalDate;
/**
* @Author:CharmingDaiDai
* @Project:sky-take-out
* @Date:2024/5/10 下午9:34
*/
@RestController
@RequestMapping("/admin/report")
@Slf4j
@Api(tags = "数据统计")
public class ReportController {
@Resource
private ReportService reportService;
@GetMapping("/turnoverStatistics")
public Result<TurnoverReportVO> turnoverStatistics(
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end
){
log.info("begin:{},end:{}", begin, end);
return reportService.getTurnover(begin, end);
}
}
实现类
package com.sky.service.impl;
import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import com.sky.result.Result;
import com.sky.service.ReportService;
import com.sky.vo.TurnoverReportVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.apache.poi.util.StringUtil;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @Author:CharmingDaiDai
* @Project:sky-take-out
* @Date:2024/5/10 下午9:35
*/
@Service
@Slf4j
public class RepostServiceImpl implements ReportService {
@Resource
private OrderMapper orderMapper;
@Override
public Result<TurnoverReportVO> getTurnover(LocalDate begin, LocalDate end) {
List<LocalDate> dateList = new ArrayList<>();
dateList.add(begin);
while (!begin.equals(end)){
begin = begin.plusDays(1);//日期计算,获得指定日期后1天的日期
dateList.add(begin);
}
List<Double> turnoverList = new ArrayList<>();
for (LocalDate date : dateList) {
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
Map map = new HashMap();
map.put("status", Orders.COMPLETED);
map.put("begin",beginTime);
map.put("end", endTime);
Double turnover = orderMapper.sumByMap(map);
turnover = turnover == null ? 0.0 : turnover;
turnoverList.add(turnover);
}
//数据封装
TurnoverReportVO turnoverReportVO = TurnoverReportVO.builder()
.dateList(StringUtils.join(dateList, ","))
.turnoverList(StringUtils.join(turnoverList, ","))
.build();
return Result.success(turnoverReportVO);
}
}
OrderMapper
package com.sky.mapper;
import com.sky.entity.Orders;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* @Author:CharmingDaiDai
* @Project:sky-take-out
* @Date:2024/5/9 下午8:51
*/
@Mapper
public interface OrderMapper {
/**
* 插入订单数据
* @param order
*/
void insert(Orders order);
/**
* 根据订单号查询订单
* @param orderNumber
*/
@Select("select * from orders where number = #{orderNumber}")
Orders getByNumber(String orderNumber);
/**
* 修改订单信息
* @param orders
*/
void update(Orders orders);
@Select("select * from orders where status = #{status} and order_time < #{orderTime}")
List<Orders> getByStatusAndOrderTimeLT(Integer status, LocalDateTime orderTime);
Double sumByMap(Map map);
}
OrderMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderMapper">
<insert id="insert" parameterType="Orders" useGeneratedKeys="true" keyProperty="id">
insert into orders
(number, status, user_id, address_book_id, order_time, checkout_time, pay_method, pay_status, amount, remark,
phone, address, consignee, estimated_delivery_time, delivery_status, pack_amount, tableware_number,
tableware_status)
values (#{number}, #{status}, #{userId}, #{addressBookId}, #{orderTime}, #{checkoutTime}, #{payMethod},
#{payStatus}, #{amount}, #{remark}, #{phone}, #{address}, #{consignee},
#{estimatedDeliveryTime}, #{deliveryStatus}, #{packAmount}, #{tablewareNumber}, #{tablewareStatus})
</insert>
<update id="update" parameterType="com.sky.entity.Orders">
update orders
<set>
<if test="cancelReason != null and cancelReason!='' ">
cancel_reason=#{cancelReason},
</if>
<if test="rejectionReason != null and rejectionReason!='' ">
rejection_reason=#{rejectionReason},
</if>
<if test="cancelTime != null">
cancel_time=#{cancelTime},
</if>
<if test="payStatus != null">
pay_status=#{payStatus},
</if>
<if test="payMethod != null">
pay_method=#{payMethod},
</if>
<if test="checkoutTime != null">
checkout_time=#{checkoutTime},
</if>
<if test="status != null">
status = #{status},
</if>
<if test="deliveryTime != null">
delivery_time = #{deliveryTime}
</if>
</set>
where id = #{id}
</update>
<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>
</mapper>
用户统计
ReportController
@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:{}", begin, end);
return reportService.getUserStatistics(begin, end);
}
ReportServiceImpl
@Override
public Result<UserReportVO> getUserStatistics(LocalDate begin, LocalDate end) {
List<LocalDate> dateList = new ArrayList<>();
dateList.add(begin);
while (!begin.equals(end)){
begin = begin.plusDays(1);//日期计算,获得指定日期后1天的日期
dateList.add(begin);
}
List<Integer> newUserList = new ArrayList<>();
List<Integer> totalUserList = new ArrayList<>();
for (LocalDate date : dateList) {
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
//新增用户数量 select count(id) from user where create_time > ? and create_time < ?
Integer newUser = userMapper.getUserCount(beginTime, endTime);
//总用户数量 select count(id) from user where create_time < ?
Integer totalUser = userMapper.getUserCount(null, endTime);
newUserList.add(newUser);
totalUserList.add(totalUser);
}
log.info(newUserList.toString());
log.info(totalUserList.toString());
UserReportVO userReportVO = UserReportVO.builder()
.dateList(StringUtils.join(dateList, ","))
.newUserList(StringUtils.join(newUserList, ","))
.totalUserList(StringUtils.join(totalUserList, ","))
.build();
return Result.success(userReportVO);
}
UserMapper
package com.sky.mapper;
import com.sky.entity.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime;
import java.util.Map;
/**
* @Author:CharmingDaiDai
* @Project:sky-take-out
* @Date:2024/5/8 下午4:15
*/
@Mapper
public interface UserMapper {
@Select("select * from user where openid = #{openid}")
User getByOpenid(String openid);
void insert(User user);
@Select("select * from user where id = #{userId}")
User getById(Long userId);
Integer getUserCount(LocalDateTime beginTime, LocalDateTime endTime);
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.UserMapper">
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into user (openid, name, phone, sex, id_number, avatar, create_time)
values (#{openid}, #{name}, #{phone}, #{sex}, #{idNumber}, #{avatar}, #{createTime})
</insert>
<select id="getUserCount" resultType="java.lang.Integer">
select count(id) from user
<where>
<if test="beginTime != null">
and create_time >= #{beginTime}
</if>
<if test="endTime != null">
and create_time <= #{endTime}
</if>
</where>
</select>
</mapper>

订单统计
ReportController
@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:{}", begin, end);
return reportService.getOrderStatistics(begin, end);
}
ReportServiceImpl
@Override
public Result<OrderReportVO> getOrderStatistics(LocalDate begin, LocalDate end) {
List<LocalDate> dateList = new ArrayList<>();
dateList.add(begin);
while (!begin.equals(end)){
begin = begin.plusDays(1);//日期计算,获得指定日期后1天的日期
dateList.add(begin);
}
//每天订单总数集合
List<Integer> orderCountList = new ArrayList<>();
//每天有效订单数集合
List<Integer> validOrderCountList = new ArrayList<>();
for (LocalDate date : dateList) {
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
//查询每天的总订单数 select count(id) from orders where order_time > ? and order_time < ?
Integer orderCount = getOrderCount(beginTime, endTime, null);
//查询每天的有效订单数 select count(id) from orders where order_time > ? and order_time < ? and status = ?
Integer validOrderCount = getOrderCount(beginTime, endTime, Orders.COMPLETED);
orderCountList.add(orderCount);
validOrderCountList.add(validOrderCount);
}
//时间区间内的总订单数
Integer totalOrderCount = orderCountList.stream().reduce(Integer::sum).get();
//时间区间内的总有效订单数
Integer validOrderCount = validOrderCountList.stream().reduce(Integer::sum).get();
//订单完成率
Double orderCompletionRate = 0.0;
if(totalOrderCount != 0){
orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
}
OrderReportVO orderReportVO = OrderReportVO.builder()
.dateList(StringUtils.join(dateList, ","))
.orderCountList(StringUtils.join(orderCountList, ","))
.validOrderCountList(StringUtils.join(validOrderCountList, ","))
.totalOrderCount(totalOrderCount)
.validOrderCount(validOrderCount)
.orderCompletionRate(orderCompletionRate)
.build();
return Result.success(orderReportVO);
}
/**
* 根据时间区间统计指定状态的订单数量
* @param beginTime
* @param endTime
* @param status
* @return
*/
private Integer getOrderCount(LocalDateTime beginTime, LocalDateTime endTime, Integer status) {
Map map = new HashMap();
map.put("status", status);
map.put("begin",beginTime);
map.put("end", endTime);
return orderMapper.countByMap(map);
}
OrderMapper
Integer countByMap(Map map);
OrderMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderMapper">
<insert id="insert" parameterType="Orders" useGeneratedKeys="true" keyProperty="id">
insert into orders
(number, status, user_id, address_book_id, order_time, checkout_time, pay_method, pay_status, amount, remark,
phone, address, consignee, estimated_delivery_time, delivery_status, pack_amount, tableware_number,
tableware_status)
values (#{number}, #{status}, #{userId}, #{addressBookId}, #{orderTime}, #{checkoutTime}, #{payMethod},
#{payStatus}, #{amount}, #{remark}, #{phone}, #{address}, #{consignee},
#{estimatedDeliveryTime}, #{deliveryStatus}, #{packAmount}, #{tablewareNumber}, #{tablewareStatus})
</insert>
<update id="update" parameterType="com.sky.entity.Orders">
update orders
<set>
<if test="cancelReason != null and cancelReason!='' ">
cancel_reason=#{cancelReason},
</if>
<if test="rejectionReason != null and rejectionReason!='' ">
rejection_reason=#{rejectionReason},
</if>
<if test="cancelTime != null">
cancel_time=#{cancelTime},
</if>
<if test="payStatus != null">
pay_status=#{payStatus},
</if>
<if test="payMethod != null">
pay_method=#{payMethod},
</if>
<if test="checkoutTime != null">
checkout_time=#{checkoutTime},
</if>
<if test="status != null">
status = #{status},
</if>
<if test="deliveryTime != null">
delivery_time = #{deliveryTime}
</if>
</set>
where id = #{id}
</update>
<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>
<select id="countByMap" resultType="java.lang.Integer">
select count(id) from orders
<where>
<if test="status != null">
and status = #{status}
</if>
<if test="begin != null">
and order_time >= #{begin}
</if>
<if test="end != null">
and order_time <= #{end}
</if>
</where>
</select>
</mapper>
销量排名统计
ReportController
@GetMapping("/top10")
@ApiOperation("Top10统计")
public Result<SalesTop10ReportVO> getSalesTop10(
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end
){
log.info("Top10统计 begin:{},end:{}", begin, end);
return reportService.getsalesTop10(begin, end);
}
ReportServiceImpl
@Override
public Result<SalesTop10ReportVO> getsalesTop10(LocalDate begin, LocalDate end) {
/*
select od.name, sum(od.number) number from order_detail od,
orders o where od.order id o.id and o.status 5
and o.order_time > and o.order_time <
group by od.name
order by number desc
limit 0,10
*/
LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX);
List<GoodsSalesDTO> goodsSalesDTOList = orderMapper.getSalesTop10(beginTime, endTime);
String nameList = StringUtils.join(goodsSalesDTOList.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList()),",");
String numberList = StringUtils.join(goodsSalesDTOList.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList()),",");
SalesTop10ReportVO salesTop10ReportVO = SalesTop10ReportVO.builder()
.nameList(nameList)
.numberList(numberList)
.build();
return Result.success(salesTop10ReportVO);
}
OrderMapper
List<GoodsSalesDTO> getSalesTop10(LocalDateTime begin, LocalDateTime end);
OrderMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderMapper">
<insert id="insert" parameterType="Orders" useGeneratedKeys="true" keyProperty="id">
insert into orders
(number, status, user_id, address_book_id, order_time, checkout_time, pay_method, pay_status, amount, remark,
phone, address, consignee, estimated_delivery_time, delivery_status, pack_amount, tableware_number,
tableware_status)
values (#{number}, #{status}, #{userId}, #{addressBookId}, #{orderTime}, #{checkoutTime}, #{payMethod},
#{payStatus}, #{amount}, #{remark}, #{phone}, #{address}, #{consignee},
#{estimatedDeliveryTime}, #{deliveryStatus}, #{packAmount}, #{tablewareNumber}, #{tablewareStatus})
</insert>
<update id="update" parameterType="com.sky.entity.Orders">
update orders
<set>
<if test="cancelReason != null and cancelReason!='' ">
cancel_reason=#{cancelReason},
</if>
<if test="rejectionReason != null and rejectionReason!='' ">
rejection_reason=#{rejectionReason},
</if>
<if test="cancelTime != null">
cancel_time=#{cancelTime},
</if>
<if test="payStatus != null">
pay_status=#{payStatus},
</if>
<if test="payMethod != null">
pay_method=#{payMethod},
</if>
<if test="checkoutTime != null">
checkout_time=#{checkoutTime},
</if>
<if test="status != null">
status = #{status},
</if>
<if test="deliveryTime != null">
delivery_time = #{deliveryTime}
</if>
</set>
where id = #{id}
</update>
<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>
<select id="countByMap" resultType="java.lang.Integer">
select count(id) from orders
<where>
<if test="status != null">
and status = #{status}
</if>
<if test="begin != null">
and order_time >= #{begin}
</if>
<if test="end != null">
and order_time <= #{end}
</if>
</where>
</select>
<select id="getSalesTop10" resultType="com.sky.dto.GoodsSalesDTO">
select od.name name,sum(od.number) number from order_detail od ,orders o
where od.order_id = o.id
and o.status = 5
<if test="begin != null">
and order_time >= #{begin}
</if>
<if test="end != null">
and order_time <= #{end}
</if>
group by name
order by number desc
limit 0, 10
</select>
</mapper>
工作台
WorkSpaceController
package com.sky.controller.admin;
import com.sky.result.Result;
import com.sky.service.WorkspaceService;
import com.sky.vo.BusinessDataVO;
import com.sky.vo.DishOverViewVO;
import com.sky.vo.OrderOverViewVO;
import com.sky.vo.SetmealOverViewVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.LocalTime;
/**
* 工作台
*/
@RestController
@RequestMapping("/admin/workspace")
@Slf4j
@Api(tags = "工作台相关接口")
public class WorkSpaceController {
@Autowired
private WorkspaceService workspaceService;
/**
* 工作台今日数据查询
* @return
*/
@GetMapping("/businessData")
@ApiOperation("工作台今日数据查询")
public Result<BusinessDataVO> businessData(){
//获得当天的开始时间
LocalDateTime begin = LocalDateTime.now().with(LocalTime.MIN);
//获得当天的结束时间
LocalDateTime end = LocalDateTime.now().with(LocalTime.MAX);
BusinessDataVO businessDataVO = workspaceService.getBusinessData(begin, end);
return Result.success(businessDataVO);
}
/**
* 查询订单管理数据
* @return
*/
@GetMapping("/overviewOrders")
@ApiOperation("查询订单管理数据")
public Result<OrderOverViewVO> orderOverView(){
return Result.success(workspaceService.getOrderOverView());
}
/**
* 查询菜品总览
* @return
*/
@GetMapping("/overviewDishes")
@ApiOperation("查询菜品总览")
public Result<DishOverViewVO> dishOverView(){
return Result.success(workspaceService.getDishOverView());
}
/**
* 查询套餐总览
* @return
*/
@GetMapping("/overviewSetmeals")
@ApiOperation("查询套餐总览")
public Result<SetmealOverViewVO> setmealOverView(){
return Result.success(workspaceService.getSetmealOverView());
}
}
WorkSpaceService
package com.sky.service;
import com.sky.vo.BusinessDataVO;
import com.sky.vo.DishOverViewVO;
import com.sky.vo.OrderOverViewVO;
import com.sky.vo.SetmealOverViewVO;
import java.time.LocalDateTime;
public interface WorkspaceService {
/**
* 根据时间段统计营业数据
* @param begin
* @param end
* @return
*/
BusinessDataVO getBusinessData(LocalDateTime begin, LocalDateTime end);
/**
* 查询订单管理数据
* @return
*/
OrderOverViewVO getOrderOverView();
/**
* 查询菜品总览
* @return
*/
DishOverViewVO getDishOverView();
/**
* 查询套餐总览
* @return
*/
SetmealOverViewVO getSetmealOverView();
}
WorkSpaceServiceImpl
package com.sky.service.impl;
import com.sky.constant.StatusConstant;
import com.sky.entity.Orders;
import com.sky.mapper.DishMapper;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.mapper.UserMapper;
import com.sky.service.WorkspaceService;
import com.sky.vo.BusinessDataVO;
import com.sky.vo.DishOverViewVO;
import com.sky.vo.OrderOverViewVO;
import com.sky.vo.SetmealOverViewVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class WorkspaceServiceImpl implements WorkspaceService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private DishMapper dishMapper;
@Autowired
private SetmealMapper setmealMapper;
/**
* 根据时间段统计营业数据
* @param begin
* @param end
* @return
*/
public BusinessDataVO getBusinessData(LocalDateTime begin, LocalDateTime end) {
/**
* 营业额:当日已完成订单的总金额
* 有效订单:当日已完成订单的数量
* 订单完成率:有效订单数 / 总订单数
* 平均客单价:营业额 / 有效订单数
* 新增用户:当日新增用户的数量
*/
Map map = new HashMap();
map.put("begin",begin);
map.put("end",end);
//查询总订单数
Integer totalOrderCount = orderMapper.countByMap(map);
map.put("status", Orders.COMPLETED);
//营业额
Double turnover = orderMapper.sumByMap(map);
turnover = turnover == null? 0.0 : turnover;
//有效订单数
Integer validOrderCount = orderMapper.countByMap(map);
Double unitPrice = 0.0;
Double orderCompletionRate = 0.0;
if(totalOrderCount != 0 && validOrderCount != 0){
//订单完成率
orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
//平均客单价
unitPrice = turnover / validOrderCount;
}
//新增用户数
Integer newUsers = userMapper.countByMap(map);
return BusinessDataVO.builder()
.turnover(turnover)
.validOrderCount(validOrderCount)
.orderCompletionRate(orderCompletionRate)
.unitPrice(unitPrice)
.newUsers(newUsers)
.build();
}
/**
* 查询订单管理数据
*
* @return
*/
public OrderOverViewVO getOrderOverView() {
Map map = new HashMap();
map.put("begin", LocalDateTime.now().with(LocalTime.MIN));
map.put("status", Orders.TO_BE_CONFIRMED);
//待接单
Integer waitingOrders = orderMapper.countByMap(map);
//待派送
map.put("status", Orders.CONFIRMED);
Integer deliveredOrders = orderMapper.countByMap(map);
//已完成
map.put("status", Orders.COMPLETED);
Integer completedOrders = orderMapper.countByMap(map);
//已取消
map.put("status", Orders.CANCELLED);
Integer cancelledOrders = orderMapper.countByMap(map);
//全部订单
map.put("status", null);
Integer allOrders = orderMapper.countByMap(map);
return OrderOverViewVO.builder()
.waitingOrders(waitingOrders)
.deliveredOrders(deliveredOrders)
.completedOrders(completedOrders)
.cancelledOrders(cancelledOrders)
.allOrders(allOrders)
.build();
}
/**
* 查询菜品总览
*
* @return
*/
public DishOverViewVO getDishOverView() {
Map map = new HashMap();
map.put("status", StatusConstant.ENABLE);
Integer sold = dishMapper.countByMap(map);
map.put("status", StatusConstant.DISABLE);
Integer discontinued = dishMapper.countByMap(map);
return DishOverViewVO.builder()
.sold(sold)
.discontinued(discontinued)
.build();
}
/**
* 查询套餐总览
*
* @return
*/
public SetmealOverViewVO getSetmealOverView() {
Map map = new HashMap();
map.put("status", StatusConstant.ENABLE);
Integer sold = setmealMapper.countByMap(map);
map.put("status", StatusConstant.DISABLE);
Integer discontinued = setmealMapper.countByMap(map);
return SetmealOverViewVO.builder()
.sold(sold)
.discontinued(discontinued)
.build();
}
}
+DishMapper
/**
* 根据条件统计菜品数量
* @param map
* @return
*/
Integer countByMap(Map map);
+DishMapper.xml
<select id="countByMap" resultType="java.lang.Integer">
select count(id) from dish
<where>
<if test="status != null">
and status = #{status}
</if>
<if test="categoryId != null">
and category_id = #{categoryId}
</if>
</where>
</select>
+SetmealMapper
/**
* 根据条件统计套餐数量
* @param map
* @return
*/
Integer countByMap(Map map);
+SetmealMapper.xml
<select id="countByMap" resultType="java.lang.Integer">
select count(id) from setmeal
<where>
<if test="status != null">
and status = #{status}
</if>
<if test="categoryId != null">
and category_id = #{categoryId}
</if>
</where>
</select>
Apache POI
导入坐标
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.16</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.16</version>
</dependency>
Excel写入内容
package com.sky.test;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class POITest {
/**
* 基于POI向Excel文件写入数据
* @throws Exception
*/
public static void write() throws Exception{
//在内存中创建一个Excel文件对象
XSSFWorkbook excel = new XSSFWorkbook();
//创建Sheet页
XSSFSheet sheet = excel.createSheet("itcast");
//在Sheet页中创建行,0表示第1行
XSSFRow row1 = sheet.createRow(0);
//创建单元格并在单元格中设置值,单元格编号也是从0开始,1表示第2个单元格
row1.createCell(1).setCellValue("姓名");
row1.createCell(2).setCellValue("城市");
XSSFRow row2 = sheet.createRow(1);
row2.createCell(1).setCellValue("张三");
row2.createCell(2).setCellValue("北京");
XSSFRow row3 = sheet.createRow(2);
row3.createCell(1).setCellValue("李四");
row3.createCell(2).setCellValue("上海");
FileOutputStream out = new FileOutputStream(new File("D:\\itcast.xlsx"));
//通过输出流将内存中的Excel文件写入到磁盘上
excel.write(out);
//关闭资源
out.flush();
out.close();
excel.close();
}
public static void main(String[] args) throws Exception {
write();
}
}
Excel读取内容
package com.sky.test;
import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
public class POITest {
/**
* 基于POI读取Excel文件
* @throws Exception
*/
public static void read() throws Exception{
FileInputStream in = new FileInputStream(new File("D:\\itcast.xlsx"));
//通过输入流读取指定的Excel文件
XSSFWorkbook excel = new XSSFWorkbook(in);
//获取Excel文件的第1个Sheet页
XSSFSheet sheet = excel.getSheetAt(0);
//获取Sheet页中的最后一行的行号
int lastRowNum = sheet.getLastRowNum();
for (int i = 0; i <= lastRowNum; i++) {
//获取Sheet页中的行
XSSFRow titleRow = sheet.getRow(i);
//获取行的第2个单元格
XSSFCell cell1 = titleRow.getCell(1);
//获取单元格中的文本内容
String cellValue1 = cell1.getStringCellValue();
//获取行的第3个单元格
XSSFCell cell2 = titleRow.getCell(2);
//获取单元格中的文本内容
String cellValue2 = cell2.getStringCellValue();
System.out.println(cellValue1 + " " +cellValue2);
}
//关闭资源
in.close();
excel.close();
}
public static void main(String[] args) throws Exception {
read();
}
}
导出运营数据
实现步骤
1). 设计Excel模板文件
2). 查询近30天的运营数据
3). 将查询到的运营数据写入模板文件
4). 通过输出流将Excel文件下载到客户端浏览器

+ReportController
/**
* 导出运营数据报表
* @param response
*/
@GetMapping("/export")
@ApiOperation("导出运营数据报表")
public void export(HttpServletResponse response){
reportService.exportBusinessData(response);
}
+ReportServiceImpl
/**导出近30天的运营数据报表
* @param response
**/
public void exportBusinessData(HttpServletResponse response) {
LocalDate begin = LocalDate.now().minusDays(30);
LocalDate end = LocalDate.now().minusDays(1);
//查询概览运营数据,提供给Excel模板文件
BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(begin,LocalTime.MIN), LocalDateTime.of(end, LocalTime.MAX));
InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
try {
//基于提供好的模板文件创建一个新的Excel表格对象
XSSFWorkbook excel = new XSSFWorkbook(inputStream);
//获得Excel文件中的一个Sheet页
XSSFSheet sheet = excel.getSheet("Sheet1");
sheet.getRow(1).getCell(1).setCellValue(begin + "至" + end);
//获得第4行
XSSFRow row = sheet.getRow(3);
//获取单元格
row.getCell(2).setCellValue(businessData.getTurnover());
row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
row.getCell(6).setCellValue(businessData.getNewUsers());
row = sheet.getRow(4);
row.getCell(2).setCellValue(businessData.getValidOrderCount());
row.getCell(4).setCellValue(businessData.getUnitPrice());
for (int i = 0; i < 30; i++) {
LocalDate date = begin.plusDays(i);
//准备明细数据
businessData = workspaceService.getBusinessData(LocalDateTime.of(date,LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX));
row = sheet.getRow(7 + i);
row.getCell(1).setCellValue(date.toString());
row.getCell(2).setCellValue(businessData.getTurnover());
row.getCell(3).setCellValue(businessData.getValidOrderCount());
row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
row.getCell(5).setCellValue(businessData.getUnitPrice());
row.getCell(6).setCellValue(businessData.getNewUsers());
}
//通过输出流将文件下载到客户端浏览器中
ServletOutputStream out = response.getOutputStream();
excel.write(out);
//关闭资源
out.flush();
out.close();
excel.close();
}catch (IOException e){
e.printStackTrace();
}
}

前端(Vue2)
基于脚手架创建前端工程
node.js 前端项目的运行环境
npm JavaScript的包管理工具
Vue CLI 基于Vue进行快速开发的完整系统,实现交互式的项目脚手架
npm i @vue/cli -g
i:install -g:全局安装
使用Vue CLI创建前端工程:
**方式一:**vue create 项目名称
vue create vue_demo
**方式二:**vue ui
vue ui





vue2只能有一个div
基本使用方式
文本插值
作用:用来绑定data方法返回的对象属性
用法:

属性绑定
作用:为标签的属性绑定data方法中返回的值
用法:v-bind:xxx 简写为::xxx

事件绑定
作用:为元素绑定对应的事件
用法:v-on:xxx 简写为:@xxx

双向绑定
作用:表单输入项和data方法中的属性进行绑定,任意一方改变都会同步给另一方
用法:v-model

条件渲染
作用:根据表达式的值来动态渲染页面元素
用法:v-if、v-else、v-else-if

axios
Axios是一个基于promise的网络请求库,作用于浏览器和node.js中
安装命令:npm install axios
导入命令:import axios from 'axios'
API列表:
- axios.get(url[,config])
- axios.delete(url[,config])
- axios.head(url[,config])
- axios.options(url[,config])
- axios.post(url[,data[,config]])
- axios.put(url[,data[,config]])
- axios.patch(url[,data[,config]])
参数说明
- ur:请求路径
- data:请求体数据,最常见的是SON格式数据
- config:配置对象,可以设置查询参数、请求头信息
为了解决跨域问题,可以在vue.config,js文件中配置代理:

- post、get示例

- 统一使用方式:axios(config)

Axios中文文档 | Axios中文网 (axios-http.cn)
请求配置 | Axios中文文档 | Axios中文网 (axios-http.cn)
{
// `url` 是用于请求的服务器 URL
url: '/user',
// `method` 是创建请求时使用的方法
method: 'get', // 默认值
// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
// 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
baseURL: 'https://some-domain.com/api/',
// `transformRequest` 允许在向服务器发送前,修改请求数据
// 它只能用于 'PUT', 'POST' 和 'PATCH' 这几个请求方法
// 数组中最后一个函数必须返回一个字符串, 一个Buffer实例,ArrayBuffer,FormData,或 Stream
// 你可以修改请求头。
transformRequest: [function (data, headers) {
// 对发送的 data 进行任意转换处理
return data;
}],
// `transformResponse` 在传递给 then/catch 前,允许修改响应数据
transformResponse: [function (data) {
// 对接收的 data 进行任意转换处理
return data;
}],
// 自定义请求头
headers: {'X-Requested-With': 'XMLHttpRequest'},
// `params` 是与请求一起发送的 URL 参数
// 必须是一个简单对象或 URLSearchParams 对象
params: {
ID: 12345
},
// `paramsSerializer`是可选方法,主要用于序列化`params`
// (e.g. https://www.npmjs.com/package/qs, http://api.jquery.com/jquery.param/)
paramsSerializer: function (params) {
return Qs.stringify(params, {arrayFormat: 'brackets'})
},
// `data` 是作为请求体被发送的数据
// 仅适用 'PUT', 'POST', 'DELETE 和 'PATCH' 请求方法
// 在没有设置 `transformRequest` 时,则必须是以下类型之一:
// - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
// - 浏览器专属: FormData, File, Blob
// - Node 专属: Stream, Buffer
data: {
firstName: 'Fred'
},
// 发送请求体数据的可选语法
// 请求方式 post
// 只有 value 会被发送,key 则不会
data: 'Country=Brasil&City=Belo Horizonte',
// `timeout` 指定请求超时的毫秒数。
// 如果请求时间超过 `timeout` 的值,则请求会被中断
timeout: 1000, // 默认值是 `0` (永不超时)
// `withCredentials` 表示跨域请求时是否需要使用凭证
withCredentials: false, // default
// `adapter` 允许自定义处理请求,这使测试更加容易。
// 返回一个 promise 并提供一个有效的响应 (参见 lib/adapters/README.md)。
adapter: function (config) {
/* ... */
},
// `auth` HTTP Basic Auth
auth: {
username: 'janedoe',
password: 's00pers3cret'
},
// `responseType` 表示浏览器将要响应的数据类型
// 选项包括: 'arraybuffer', 'document', 'json', 'text', 'stream'
// 浏览器专属:'blob'
responseType: 'json', // 默认值
// `responseEncoding` 表示用于解码响应的编码 (Node.js 专属)
// 注意:忽略 `responseType` 的值为 'stream',或者是客户端请求
// Note: Ignored for `responseType` of 'stream' or client-side requests
responseEncoding: 'utf8', // 默认值
// `xsrfCookieName` 是 xsrf token 的值,被用作 cookie 的名称
xsrfCookieName: 'XSRF-TOKEN', // 默认值
// `xsrfHeaderName` 是带有 xsrf token 值的http 请求头名称
xsrfHeaderName: 'X-XSRF-TOKEN', // 默认值
// `onUploadProgress` 允许为上传处理进度事件
// 浏览器专属
onUploadProgress: function (progressEvent) {
// 处理原生进度事件
},
// `onDownloadProgress` 允许为下载处理进度事件
// 浏览器专属
onDownloadProgress: function (progressEvent) {
// 处理原生进度事件
},
// `maxContentLength` 定义了node.js中允许的HTTP响应内容的最大字节数
maxContentLength: 2000,
// `maxBodyLength`(仅Node)定义允许的http请求内容的最大字节数
maxBodyLength: 2000,
// `validateStatus` 定义了对于给定的 HTTP状态码是 resolve 还是 reject promise。
// 如果 `validateStatus` 返回 `true` (或者设置为 `null` 或 `undefined`),
// 则promise 将会 resolved,否则是 rejected。
validateStatus: function (status) {
return status >= 200 && status < 300; // 默认值
},
// `maxRedirects` 定义了在node.js中要遵循的最大重定向数。
// 如果设置为0,则不会进行重定向
maxRedirects: 5, // 默认值
// `socketPath` 定义了在node.js中使用的UNIX套接字。
// e.g. '/var/run/docker.sock' 发送请求到 docker 守护进程。
// 只能指定 `socketPath` 或 `proxy` 。
// 若都指定,这使用 `socketPath` 。
socketPath: null, // default
// `httpAgent` and `httpsAgent` define a custom agent to be used when performing http
// and https requests, respectively, in node.js. This allows options to be added like
// `keepAlive` that are not enabled by default.
httpAgent: new http.Agent({ keepAlive: true }),
httpsAgent: new https.Agent({ keepAlive: true }),
// `proxy` 定义了代理服务器的主机名,端口和协议。
// 您可以使用常规的`http_proxy` 和 `https_proxy` 环境变量。
// 使用 `false` 可以禁用代理功能,同时环境变量也会被忽略。
// `auth`表示应使用HTTP Basic auth连接到代理,并且提供凭据。
// 这将设置一个 `Proxy-Authorization` 请求头,它会覆盖 `headers` 中已存在的自定义 `Proxy-Authorization` 请求头。
// 如果代理服务器使用 HTTPS,则必须设置 protocol 为`https`
proxy: {
protocol: 'https',
host: '127.0.0.1',
port: 9000,
auth: {
username: 'mikeymike',
password: 'rapunz3l'
}
},
// see https://axios-http.com/zh/docs/cancellation
cancelToken: new CancelToken(function (cancel) {
}),
// `decompress` indicates whether or not the response body should be decompressed
// automatically. If set to `true` will also remove the 'content-encoding' header
// from the responses objects of all decompressed responses
// - Node only (XHR cannot turn off decompression)
decompress: true // 默认值
}
路由 Vue-Router
vue属于单页面应用,所谓的路由,就是根据浏览器路径不同,用不同的视图组件替换这个页面内容
用vue ui创建项目时,选择手动,并打开Router

路由组成:
VueRouter
:路由器,根据路由请求在路由视图中动态渲染对应的视图组件
<router-link>
:路由链接组件,浏览器会解析成
<router-view>
:路由视图组件,用来展示与路由路径匹配的视图组件
路由表
静态导入:
静态导入意味着在构建时就已经确定了路由的映射关系。这种方式下,路由组件在应用启动时就被导入,因此它们的代码会被包含在最终的打包文件中。
// 静态导入路由组件
import Home from './components/Home.vue';
import About from './components/About.vue';
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
];
// 创建Vue路由器实例
const router = new VueRouter({
routes // 简写,等同于 routes: routes
});
动态导入:
动态导入则是一种在运行时才确定路由映射的方式。它允许我们在需要的时候才加载路由组件,这有助于代码分割和按需加载,从而可以减小应用的初始下载体积。
区别
- 打包体积:静态导入的组件会在初始包中被加载,而动态导入的组件会生成一个或多个按需加载的代码块。
- 性能:动态导入可以提高大型应用的性能,因为它允许浏览器在用户实际访问某个路由之前不加载对应的组件。
- 构建时间:静态导入的组件在构建时就已经确定,可能会使构建过程更快一些,而动态导入的组件则需要额外的处理来生成多个代码块。
router-link
<router-link>
是 Vue Router 的官方路由链接组件,它提供了一种声明式的方法来实现页面导航。使用 <router-link>
可以轻松地在不同的路由之间跳转。
基本用法
<template>
<div>
<!-- 使用 `to` 属性来指定目标路由 -->
<router-link to="/">首页</router-link>
<router-link to="/about">关于我们</router-link>
</div>
</template>
<script>
export default {
// 组件的其他选项
}
</script>
路由对象
<template>
<div>
<!-- 使用路由对象来设置 `to` 属性 -->
<router-link :to="{ name: 'user', params: { userId: 123 }}">用户页面</router-link>
</div>
</template>
使用tag属性
<template>
<div>
<!-- 使用 `tag` 属性将 `<a>` 标签替换为其他HTML标签 -->
<router-link to="/about" tag="li">关于我们</router-link>
</div>
</template>
使用replace属性
<template>
<div>
<!-- 使用 `replace` 属性可以替换当前路由,而不是向历史堆栈中添加新路由 -->
<router-link :to="{ name: 'user' }" replace>用户页面</router-link>
</div>
</template>
使用active-class属性
<template>
<div>
<!-- 使用 `active-class` 属性来指定激活链接的类名 -->
<router-link to="/about" active-class="active-link">关于我们</router-link>
</div>
</template>
使用exact属性
<template>
<div>
<!-- 使用 `exact` 属性确保只有当路由完全匹配时,链接才是激活的 -->
<router-link to="/" exact>首页</router-link>
</div>
</template>
使用append属性
<template>
<div>
<!-- 使用 `append` 属性可以在当前路由的基础上追加子路由 -->
<router-link to="/contact" append>联系我们</router-link>
</div>
</template>
组合使用
<template>
<div>
<!-- 可以组合使用多个属性 -->
<router-link
:to="{ name: 'user', params: { userId: 123 } }"
tag="li"
replace
active-class="active-link"
>
用户页面
</router-link>
</div>
</template>
router-view
<router-view>
是 Vue Router 的一个内置组件,用于渲染匹配到的路由所对应的组件。在 Vue Router 的使用中,它是实现页面路由跳转的核心组件之一。
当你使用 <router-view>
时,它会被路由系统替换为当前路由的组件。如果你有多个 router-view
,在嵌套路由的情况下,它们会按照嵌套的层级显示不同的组件。
<!-- App.vue -->
<template>
<div id="app">
<!-- 使用 router-link 组件进行导航 -->
<router-link to="/home">Home</router-link>
<router-link to="/about">About</router-link>
<!-- router-view 用于渲染路由匹配到的组件 -->
<router-view></router-view>
</div>
</template>
<script>
// 导入所需的路由组件
import Home from './components/Home.vue';
import About from './components/About.vue';
export default {
components: {
Home,
About
}
};
</script>
在这个例子中,当用户点击 "Home" 或 "About" 链接时,Vue Router 会渲染对应的 Home
或 About
组件到 <router-view>
标签的位置。
编程式路由跳转
this.$router.push()
this.$router.push('/home');
如果需要传递参数
this.$router.push({ path: '/user', query: { name: 'John' } });
使用命名视图
this.$router.push({ name: 'user', params: { userId: 123 } });
this.$router.replace()
这个方法与 push()
类似,但它会替换当前的路由记录,而不会向历史堆栈添加新记录。这意味着用户不能通过浏览器的后退按钮返回到以前的页面
this.$router.replace('/about');
this.$router.go()
使用 go()
方法可以跳转到历史堆栈中的某个位置,你可以向前或向后跳转任意个路由记录
// 前进1个记录
this.$router.go(1);
// 后退1个记录
this.$router.go(-1);
// 跳转到特定的记录
this.$router.go(-3);
this.𝑟𝑜𝑢𝑡𝑒𝑟.𝑏𝑎𝑐𝑘()和𝑡ℎ𝑖𝑠.rout**er.back()和this.router.forward()
这两个方法分别是 go(-1)
和 go(1)
的别名,用于实现后退和前进
// 后退
this.$router.back();
// 前进
this.$router.forward();
示例
假设你有一个按钮,点击这个按钮需要跳转到用户信息页面
<template>
<button @click="goToUser">Go to User Profile</button>
</template>
<script>
export default {
methods: {
goToUser() {
this.$router.push({ name: 'UserProfile', params: { userId: '123' } });
}
}
};
</script>
如果用户访问的路由地址不存在,该如何处理?
使用 notFound
路由配置
Vue Router 允许你配置一个特殊的路由,用于捕获所有其他路由都无法匹配的路径。这通常被称为“404 Not Found”页面。
// router.js
const routes = [
// ... 其他正常路由配置
{
path: '/:catchAll(.*)', // 使用正则表达式来匹配所有路由
component: NotFoundComponent // 404 Not Found 组件
}
];
const router = new VueRouter({
routes
});
在上面的代码中,NotFoundComponent
是一个自定义组件,用于显示当用户访问不存在的路由时的内容。
使用 *
作为通配符
在 Vue Router 4 中,可以使用 *
作为通配符来匹配任意路径。
// router.js
const routes = [
// ... 其他正常路由配置
{
path: '*',
component: NotFoundComponent // 404 Not Found 组件
}
];
const router = createRouter({
routes
});
使用 redirect
进行重定向
如果你希望用户在访问不存在的路由时被重定向到特定的页面,可以使用 redirect
属性。
// router.js
const routes = [
// ... 其他正常路由配置
{
path: '/not-found',
redirect: '/404' // 重定向到一个自定义的404页面
},
{
path: '/404',
component: NotFoundComponent
}
];
编程式导航到 404 页面
在编程式导航中,如果尝试导航到一个不存在的路由,可以手动将用户重定向到 404 页面。
// JavaScript 代码中
if (routeDoesNotExist) {
router.push('/404');
}
使用 router.onNotFound
(Vue Router 3)
router.onNotFound((location) => {
// 处理未找到路由的情况
router.push('/404');
});
示例组件
<!-- NotFoundComponent.vue -->
<template>
<div>
<h1>404 - 页面未找到</h1>
<p>您访问的页面不存在,请尝试以下选项:</p>
<router-link to="/">返回首页</router-link>
</div>
</template>
<script>
export default {
name: 'NotFoundComponent'
};
</script>
<style scoped>
/* 你的CSS样式 */
</style>
嵌套路由(子路由)
嵌套路由是Vue Router 功能的一部分,它允许你将一个路由放置在另一个路由的内部,形成一个路由嵌套的结构。这在构建具有多层级结构的应用程序时非常有用,比如网站导航菜单或多级菜单。
基本语法
在Vue Router中设置嵌套路由,你需要在 children
属性中定义子路由。每个子路由都与父路由相关联,并且可以访问父路由的路径参数。
// router.js
const routes = [
{
path: '/parent',
component: ParentComponent,
children: [
{
path: 'child',
component: ChildComponent
},
{
path: 'child2',
component: AnotherChildComponent
}
]
}
];
const router = new VueRouter({
routes
});
在这个例子中,ParentComponent
是父级组件,而 ChildComponent
和 AnotherChildComponent
是它的子组件。当用户导航到 /parent/child
时,ParentComponent
将被渲染,并且 ChildComponent
将作为其子路由被渲染。
传递路径参数
在嵌套路由中,子路由可以访问父路由的路径参数。
// router.js
const routes = [
{
path: '/parent/:parentId',
component: ParentComponent,
children: [
{
path: 'child/:childId',
component: ChildComponent
}
]
}
];
在这个例子中,ParentComponent
可以访问 parentId
参数,而 ChildComponent
可以访问 childId
参数。如果用户访问 /parent/123/child/456
,那么 ParentComponent
的 parentId
将是 123
,而 ChildComponent
的 childId
将是 456
。
使用 <router-link>
进行导航
在模板中,你可以使用 <router-link>
来链接到嵌套的路由。
<template>
<div>
<router-link :to="{ name: 'parent', params: { parentId: '123' } }">Go to Parent</router-link>
<router-link :to="{ path: '/parent/123/child/456' }">Go to Child</router-link>
</div>
</template>
嵌套 <router-view>
组件
在应用的模板中,你可以使用多个 <router-view>
组件来显示嵌套的组件。
<!-- App.vue -->
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
在父级组件中,也可以包含 <router-view>
来渲染子级组件:
<!-- ParentComponent.vue -->
<template>
<div>
<h1>Parent Component</h1>
<!-- 这个 <router-view> 将渲染子组件 -->
<router-view></router-view>
</div>
</template>
嵌套路由是Vue Router中非常强大的特性,它允许你构建复杂的页面结构,同时保持组件的可复用性和清晰的组织。
实现步骤
安装并导入elementui,实现页面布局(Container布局容器)---ContainerView.vue
提供子视图组件,用于效果展示---P1View.vue、P2View.vue、P3View.vue
在src/router/index.js中配置路由映射规则(嵌套路由配置)
在布局容器视图中添加<router-view>
,实现子视图组件展示
在布局容器视图中添加<router--link>
,实现路由请求
状态管理 vuex
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式和库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 非常适合用于大型单页应用程序 (SPA),其中需要在多个组件之间共享状态。
Vuex 的核心概念
- State:应用的状态,是一个对象,包含了应用中大部分的状态信息。
- Getters:类似于 Vue 的计算属性,允许你从 store 中的 state 派生出一些状态,以便于在多个组件之间复用。
- Mutations:更改 Vue.store.state 的唯一方法是提交 mutation。Vuex 中的 mutation 非常类似于事件:每个 mutation 都有一个字符串类型的 event 和一个回调函数。mutation 可以看作是同步事务。
- Actions:类似于 mutation,不同在于它们提交的是 mutation,而不是直接变更状态。Action 可以包含任意异步操作。
- Modules:当应用变得非常复杂时,store 对象就可能变得相当臃肿。为了解决这个问题,Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter,甚至是嵌套子模块。
安装 Vuex
使用 npm 或者 yarn 来安装 Vuex:
npm install vuex@next --save
# 或者
yarn add vuex@next
基本用法
- 创建一个 Vuex store 实例:
import { createStore } from 'vuex';
const store = createStore({
state: {
count: 0
},
getters: {
doubleCount: (state) => state.count * 2
},
mutations: {
increment(state) {
state.count++;
}
},
actions: {
incrementAsync({ commit }) {
setTimeout(() => {
commit('increment');
}, 1000);
}
}
});
- 在 Vue 应用中使用 store:
import { createApp } from 'vue';
import App from './App.vue';
import { store } from './store';
const app = createApp(App);
app.use(store);
app.mount('#app');
- 在 Vue 组件中访问和修改状态:
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
<button @click="incrementAsync">Increment Async</button>
</div>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex';
export default {
computed: {
...mapState(['count']),
...mapGetters(['doubleCount'])
},
methods: {
...mapMutations(['increment']),
...mapActions(['incrementAsync'])
}
};
</script>
在这个组件中,我们使用了辅助函数 mapState
, mapGetters
, mapMutations
, 和 mapActions
来将 store 中的 state、getters、mutations 和 actions 映射到组件的计算属性和方法中。
模块化
在复杂的应用中,将 store 分割成模块是一个好的实践:
const moduleA = {
state: { ... },
getters: { ... },
actions: { ... },
mutations: { ... }
};
const moduleB = {
state: { ... },
getters: { ... },
actions: { ... },
mutations: { ... }
};
const store = createStore({
modules: {
a: moduleA,
b: moduleB
}
});
模块化使得代码更加清晰和易于维护,尤其是在大型应用中。
Vuex 是 Vue 生态系统中一个非常强大的工具,它通过提供可预测的状态管理来帮助开发者构建复杂的应用。
使用commit修改共享数据
在Vuex中,使用commit
来修改共享数据(即存储在Vuex store中的state)是标准的做法。这些共享数据可以被应用程序中的多个组件访问和修改,从而实现组件间的通信。
以下是使用commit
来提交mutation并修改共享数据的步骤:
1. 定义State和Mutation
首先,你需要在Vuex store的state
中定义共享数据,并创建一个mutation
来修改它。
// store.js
export default new Vuex.Store({
state: {
sharedData: {
count: 0
}
},
mutations: {
updateSharedData(state, newData) {
state.sharedData = { ...state.sharedData, ...newData };
}
}
});
2. 在组件中提交Mutation
然后,在Vue组件中,你可以通过this.$store.commit
来提交mutation,从而修改共享数据。
<template>
<div>
<p>Shared Count: {{ sharedData.count }}</p>
<button @click="incrementCount">Increment Count</button>
</div>
</template>
<script>
export default {
computed: {
sharedData() {
return this.$store.state.sharedData;
}
},
methods: {
incrementCount() {
this.$store.commit('updateSharedData', { count: this.sharedData.count + 1 });
}
}
};
</script>
3. 使用MapMutations辅助函数
为了简化组件中的mutation提交,可以使用mapMutations
辅助函数。
import { mapMutations } from 'vuex';
export default {
computed: {
sharedData() {
return this.$store.state.sharedData;
}
},
methods: {
...mapMutations([
'updateSharedData' // 这将 `this.$store.commit('updateSharedData')` 映射为 `this.updateSharedData`
]),
incrementCount() {
this.updateSharedData({ count: this.sharedData.count + 1 });
}
}
};
4. 在Actions中提交Mutation
如果你需要执行异步操作,可以在actions中进行,并在actions中提交mutation。
// store.js
actions: {
incrementSharedDataAsync({ commit }) {
setTimeout(() => {
commit('updateSharedData', { count: this.state.sharedData.count + 1 });
}, 1000);
}
}
然后在组件中调用这个action:
<script>
import { mapActions } from 'vuex';
export default {
methods: {
...mapActions([
'incrementSharedDataAsync'
]),
incrementCountAsync() {
this.incrementSharedDataAsync();
}
}
};
</script>
请注意,由于this的指向问题,直接在action中使用this.state
或this.commit
可能不会按预期工作。通常,你需要将当前state和需要的mutation作为参数传递给action。
通过这些步骤,你可以在Vuex中使用commit
来修改多个组件间共享的数据。这有助于保持组件间的通信和状态同步,同时避免直接在组件内部管理复杂的逻辑。

如何理解Vuex?
实现多个组件之间的数据共享
共享数据是响应式的,实时渲染到模版
可以集中管理共享数据
如何使用vuex?
在store对象的state属性中定义共享数据
在store对象的mutations属性中定义修改共享数据的函数
在store对象的actions属性中定义调用mutation的函数,可以进行异步操作
mutations中的函数不能直接调用,只能通过store对象的commit方法调用
actions中定义的函数不能直接调用,只能通过store对象的dispatch方法调用
TypeScript
TypeScript中文网 · TypeScript——JavaScript的超集 (tslang.cn)
TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,这意味着任何有效的 JavaScript 代码都是有效的 TypeScript 代码。TypeScript 增添了类型系统和对 ES6+ 特性的支持,目标是开发大型应用程序并提供JavaScript所不具备的一些特性,以帮助开发者提高开发体验和代码质量。
TypeScript 的主要特点:
静态类型检查:TypeScript 允许你为变量、函数参数和返回值指定类型。这有助于在编译时而非运行时捕捉错误。
类:TypeScript 支持基于类的面向对象编程,这在 JavaScript 中是不可用的(尽管 JavaScript 有原型继承)。
接口:TypeScript 提供了接口,这是一种强大的方式,用于定义合同(contract)或对象的结构,而不实际实现任何功能。
高级类型:包括联合类型(Union Types)、交叉类型(Intersection Types)、泛型(Generics)等。
编译时错误检查:TypeScript 在编译时进行类型检查,有助于捕捉潜在的错误。
工具支持:由于 TypeScript 的类型信息,现代编辑器和 IDE 可以提供更好的自动完成和内联文档。
编译为 JavaScript:TypeScript 代码最终会被编译为 JavaScript,这意味着它可以运行在任何支持 JavaScript 的环境中。
定义文件(.d.ts):TypeScript 允许你为现有的 JavaScript 库编写声明文件,从而允许 TypeScript 编译器理解它们的 API。
安装 TypeScript
你可以通过 npm 安装 TypeScript 的编译器:
npm install -g typescript
编译 TypeScript 代码
安装后,你可以使用 tsc
命令来编译 TypeScript 文件:
tsc hello.ts
这会生成一个 hello.js
文件,其中包含了编译后的 JavaScript 代码。
基本示例
// hello.ts
let greeting: string = "Hello, World!";
console.log(greeting);
在 TypeScript 中,你声明了一个类型为 string
的变量 greeting
,然后打印它。
TypeScript 旨在成为 JavaScript 的一个可选的、更健壮的替代品,它通过添加类型系统和其他特性来增强开发体验,同时保持与 JavaScript 的兼容性。
数据类型
下面是 TypeScript 常用数据类型以表格形式列出,包括类型名称、描述和基本示例:
数据类型 | 描述 | 示例 |
---|---|---|
number | 表示数字,包括整数和浮点数。 | let age: number = 25; |
string | 表示文本数据。 | let name: string = "Alice"; |
boolean | 表示逻辑值 true 或 false 。 | let isApproved: boolean = true; |
null | 表示空值。 | let value: null = null; |
undefined | 表示未定义值。 | let value: undefined = undefined; |
array | 表示数字数组,使用 type[] 表示法。 | let numbers: number[] = [1, 2, 3]; |
tuple | 元组类型,允许表示一个包含多个不同类型的数组。 | let person: [string, number] = ["Alice", 25]; |
enum | 枚举类型,定义一组命名的常数。 | enum Color {Red, Green, Blue} |
any | 表示任意类型。 | let value: any = true; value = "Hello"; |
unknown | 表示任何类型,更安全,需要类型检查。 | let value: unknown; if (typeof value === "string") { value = value.toUpperCase(); } |
void | 表示没有任何类型,常用于没有返回值的函数。 | function warn(): void { console.log("Warning!"); } |
union types | 联合类型,一个值可以是几种类型之一。 | `let user: string |
intersection types | 交叉类型,从多个类型中创建一个新的类型。 | interface Person { name: string; } interface Developer { code: string; } type PersonDeveloper = Person & Developer; |
type alias | 类型别名,为复杂的类型定义一个新名字。 | type Name = string; let userName: Name = "Alice"; |
readonly | 只读类型,表示一个类型的只读版本。 | let list: readonly number[] = [1, 2, 3]; |
async | 表示一个返回 Promise 的函数。 | async function fetchData(): Promise<number[]> { return [1, 2, 3]; } |
generics | 泛型,允许定义函数、接口或类时不预先确定具体的类型。 | function identity<T>(arg: T): T { return arg; } |
interface
在TypeScript中,interface
是一种强大的方式,用于定义对象的结构,它可以帮助你以一种类型安全的方式来描述和使用复杂的数据结构。interface
主要用于类型检查和确保一个对象只有特定的结构。
基本用法
你可以定义一个接口来约束一个对象,然后使用这个接口作为函数参数或返回值的类型注解。
interface Person {
firstName: string;
lastName: string;
age?: number; // 可选属性,用问号表示
}
function greeter(person: Person) {
console.log(`Hello, ${person.firstName} ${person.lastName}`);
}
let user = {
firstName: "Alice",
lastName: "Smith",
age: 25
};
greeter(user);
可选属性和只读属性
接口中的属性可以是可选的,也可以是只读的。
interface Point {
x: number;
y: number;
readonly z?: number; // 可选的只读属性
}
let point: Point = { x: 10, y: 10 };
函数类型
接口不仅可以用来描述对象的结构,还可以用来描述函数的类型。
interface SearchFunc {
(source: string, subString: string): boolean;
}
let mySearch: SearchFunc = function(source: string, subString: string) {
return source.search(subString) !== -1;
};
类的实现
接口也可以用于类的实现,确保类遵守接口定义的结构。
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
class Clock implements ClockInterface {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
}
继承和扩展
接口可以继承其他接口,从而实现接口的扩展。
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = <Square>{ color: "blue", sideLength: 4 };
索引签名
接口可以包含索引签名,用于表示对象的索引类型。
interface StringArray {
[index: number]: string;
}
let myArray: StringArray = ["Hello", "World"];
类型别名 vs. 接口
虽然类型别名(type
)和接口(interface
)在很多地方可以互换使用,但它们之间有一些细微的差别:
- 接口可以被扩展(
extends
),而类型别名不可以。 - 一个接口可以被多次声明并合并为一个接口,而类型别名不能。
- 使用接口定义对象结构更明确,而类型别名更灵活,常用于基本数据类型或交叉类型。
接口是TypeScript中定义和使用高级类型系统的核心特性之一,它们为大型应用程序的开发提供了强大的类型检查能力。