【苍穹外卖-管理端部分-学习笔记】
目录
- <<回到导览
- 1. 环境搭建
- 1.1.项目结构介绍
- 1.2.数据库环境搭建
- 1.3.Nginx简介
- 1.4.完善登录功能
- 1.5.导入接口文档
- 1.6.Swagger
- 2.员工模块
- 2.1.新增员工
- 2.2.员工分页查询
- 2.3.启用禁用账号
- 2.4.编辑员工信息
- 2.5.公共字段填充
- 3.菜品分类模块
- 3.1.前言
- 3.2.分类分页查询
- 3.3.修改菜品分类
- 3.4.启用禁用分类
- 3.5.新增分类
- 3.6.根据id删除分类
- 3.7.根据类型查询分类
- 4.菜品模块
- 4.1.菜品分页查询
- 4.2.根据id查询菜品
- 4.3.修改菜品
- 4.4.菜品起售停售
- 4.5.文件上传
- 4.6.新增菜品
- 4.7.批量删除菜品
- 5.Redis
- 5.1.数据类型
- 5.2.常用命令
- 5.3.java中操作Redis
- 5.4.店铺模块
- 6.HttpClient
<<回到导览
1. 环境搭建
1.1.项目结构介绍
项目整体结构介绍
名称 | 说明 |
---|---|
sky-take-out | maven父工程,统一管理依赖版本,聚合其他子模块 |
sky-common | 子模块,存放公共类,例如:工具类、常量类、异常类等 |
sky-pojo | 子模块,存放实体类、VO、DTO等 |
sky-server | 子模块,后端服务,存放配置文件、Controller、Service、Mapper等 |
sky-pojo模块介绍
说明 | |
---|---|
Entity | 实体,通常和数据库中的表对应 |
DTO | 数据传输对象,通常用于程序中各层之间传递数据 |
VO | 视图对象,为前端展示数据提供的对象 |
POJO | 普通Java对象,只有属性和对应的getter和setter |
1.2.数据库环境搭建
-
sql文件
表名 含义 employee 员工表 category 分类表 dish 菜品表 dish flavor 菜品口味表 setmeal 套餐表 setmeal_dish 套餐菜品关系表 user 用户表 address book 地址表 shopping_cart 购物车表 orders 订单表 order_detail 订单明细表
1.3.Nginx简介
-
反向代理:将前端发送的动态请求由nginx转发到后端服务器
优点:
- 提高访问数据(缓存)
- 进行负载均衡(大量请求按指定方式分配给集群中的每台服务器,如轮询)
- 保证后端服务安全(后端服务不能直接通过前端访问)
-
反向代理配置方式
-
负载均衡配置方式
1.4.完善登录功能
要求:将密码通过MD5加密方式对明文密码加密后储存,提高安全性
注意:MD5只能单向加密,即正常情况下,只能加密,不能解密
小技巧:发现要做的模块,现在不着急做(及生成代办注释),可以用TODO注释如:
// TODO 后期需要进行md5加密,然后再进行比对
这样我们可以通过窗口看到代办注释
对密码进行MD5加密(EmployeeServiceImpl.java)
// 对明文密码进行MD5加密
password = DigestUtils.md5DigestAsHex(password.getBytes());
我们将数据库中的密码改写成MD5加密后的,例如密码为123456,MD5加密后为e10adc3949ba59abbe56e057f20f883e
1.5.导入接口文档
-
前后端分离开发流程
接下来我们用apifox导入api
-
创建项目
-
导入
1.6.Swagger
-
使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,就可以做到生成接口文档,以及在线接口调试页面。
官网:https://swagger.io/
Knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案。
-
使用方式
-
导入Knife4j坐标
<!-- Knife4j坐标 --> <dependency><groupld>com.github.xiaoymin</groupld><artifactld>knife4j-spring-boot-starter</artifactld><version>3.0.2</version> </dependency>
-
在配置类中加入knife4j相关配置(sky-server/src/main/java/com/sky/config)
// 通过knife4j生成接口文档 @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; }
-
启动服务后,可以在浏览器输入http://localhost:8080/doc.html,查看生成的接口文档,并进行测试
-
-
Swagger常用注解
注解 说明 @Api 用在类上,例如Controller,表示对类的说明 @ApiModel 用在类上,例如entity、DTO、VO @ApiModelProperty 用在属性上,描述属性信息 @ApiOperation 用在方法上,例如Controller的方法,说明方法的用途、作用 @RestController @RequestMapping("/admin/employee") @Slf4j // 1. @Api,对类的说明 @Api(tags = "员工相关接口") public class EmployeeController {// ... }
@PostMapping("/login") // 4. @ApiOperation,用在方法上,说明方法的用途、作用 @ApiOperation(value = "员工登录") public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {// ... }
@Data // 2. @ApiModel,用在entity、DTO、VO类上 @ApiModel(description = "员工登录时传递的数据模型") public class EmployeeLoginDTO implements Serializable {// 3. @ApiModelProperty, 用在属性上,描述属性信息@ApiModelProperty("用户名")private String username;// ... }
添加完毕后,重启程序,文档会发生相应变化
2.员工模块
2.1.新增员工
代码:
-
Controller
@ApiOperation("新增员工") @PostMapping public Result save(@RequestBody EmployeeDTO employeeDTO) {log.info("新增员工:{}", employeeDTO);employeeService.save(employeeDTO);return Result.success(); }
-
Service
public interface EmployeeService {// 新增员工void save(EmployeeDTO employeeDTO); }
// impl // 新增员工 @Override public void save(EmployeeDTO employeeDTO) {Employee employee = new Employee();// 对象属性拷贝BeanUtils.copyProperties(employeeDTO, employee);// 设置账号状态,默认正常状态employee.setStatus(StatusConstant.ENABLE);// 设置密码,默认密码123456employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));// 设置当前记录的创建、修改时间employee.setCreateTime(LocalDateTime.now());employee.setUpdateTime(LocalDateTime.now());// 设置当前记录创建人id和修改人id// TODO 后期需要改为当前登录用户的idemployee.setCreateUser(10L);employee.setUpdateUser(10L);employeeMapper.insert(employee); }
-
Mapper
// 插入员工数据 @Insert( "insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user)" +"values" +"(#{name}, #{username}, #{password}, #{phone}, #{sex}, #{idNumber}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})") void insert(Employee employee);
调试:
-
发送登录请求,获取token
-
配置全局参数
删掉员工登录选修卡,可以看见token被成功添加到请求头
-
测试新增员工接口
数据库成功添加员工信息
前后端联调成功
功能完善:
-
录入的用户名已经存在,抛出异常后未处理
我们可以看到的是,报错类型是SQLIntegrityConstraintViolationException,
捕获这个sql异常并处理
/* sql异常 */ @ExceptionHandler public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){String message = ex.getMessage();if(message.contains("Duplicate entry")){String[] split = message.split(" ");String username = split[2];return Result.error(username + MessageConstant.ALREADY_EXIT);}else{return Result.error(MessageConstant.UNKNOWN_ERROR);} }
-
创建人id和修改人id设置为固定值
员工登录成功后,生成JWT令牌并相应给前端
// controller/admin/EmployeeController.java //登录成功后,生成jwt令牌 Map<String, Object> claims = new HashMap<>(); claims.put(JwtClaimsConstant.EMP_ID, employee.getId()); String token = JwtUtil.createJWT(jwtProperties.getAdminSecretKey(),jwtProperties.getAdminTtl(),claims);
在校验令牌时,拦截器进行拦截并将令牌中的id解析出来
// interceptor/JwtTokenAdminInterceptor.java //2、校验令牌 try {log.info("jwt校验:{}", token);Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);// 从JWT令牌中解析出idLong empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());log.info("当前员工id:", empId);//3、通过,放行return true; } catch (Exception ex) {//4、不通过,响应401状态码response.setStatus(401);return false; }
我们可以利用ThreadLocal将id传输到我们需要使用id的service实现类中
ThreadLocal并不是Thread而是Thread的局部变量
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
客户端发送的每一次请求都是一个单独的线程
ThreadLocal常用方法 说明 public void set(T value) 设置当前线程的线程局部变量的值 public T get() 返回当前线程的线程局部变量的值 public void remove() 移除当前线程的线程局部变量的值 在外面使用ThreadLocal时,常常会封装成一个工具类
// sky-common/src/main/java/com/sky/context/BaseContext.java public class BaseContext {public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();public static void setCurrentId(Long id) {threadLocal.set(id);}public static Long getCurrentId() {return threadLocal.get();}public static void removeCurrentId() {threadLocal.remove();} }
调用ThreadLocal完善功能
//2、校验令牌 try {Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());// 存入BaseContext.setCurrentId(empId); } catch (Exception ex) {// ... }
// 设置当前记录创建人id和修改人id employee.setCreateUser(BaseContext.getCurrentId()); employee.setUpdateUser(BaseContext.getCurrentId());
2.2.员工分页查询
业务规则:
- 根据页码显示员工信息
- 每页展示10条数据
- 分页查询时可以根据需要,输入员工姓名进行查询
代码:
-
Controller
// 员工分页查询 @ApiOperation("员工分页查询") @GetMapping("/page") public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO) {log.info("员工分页查询,参数为{}", employeePageQueryDTO);PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);return Result.success(pageResult); }
-
Service
// 员工分页查询 PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
// 员工分页查询 @Override public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {// 开始分页查询PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);long total = page.getTotal();List<Employee> records = page.getResult();return new PageResult(total, records); }
-
Mapper
// 分页查询 Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
<select id="pageQuery" resultType="com.sky.entity.Employee">select * from employee<where><if test="name != null and name != ''">and name like concat('%', #{name}, '%')</if></where>order by create_time desc </select>
2.3.启用禁用账号
业务规则:
- 启用/禁用的相互切换
- 状态为禁用的员工账号不能登录系统
代码:
-
Controller
// 启用禁用员工账号 @ApiOperation("启用/禁用员工账号") @PostMapping("/status/{status}") public Result startOrStop(@PathVariable Integer status, long id) {log.info("启用/禁用员工账号:{},{}", status, id);employeeService.startOrStop(status, id);return Result.success(); }
-
Service
// 禁用或启用员工 void startOrStop(Integer status, long id);
// 禁用或启用员工 @Override public void startOrStop(Integer status, long id) {// Employee employee = new Employee();// employee.setId(id);// employee.setStatus(status);Employee employee = Employee.builder().status(status).id(id).build();// 设置更新时间employee.setUpdateTime(LocalDateTime.now());// 设置当前记录修改人idemployee.setUpdateUser(BaseContext.getCurrentId());employeeMapper.update(employee); }
-
Mapper
// 根据主键动态修改属性 void update(Employee employee);
<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>
2.4.编辑员工信息
业务规则:
- 根据id查询员工信息
- 编辑员工信息
代码:
-
Controller
// 根据id查询员工 @ApiOperation("根据id查询员工") @GetMapping("/{id}") public Result<Employee> getById(@PathVariable Long id) {log.info("根据id={}, 查询员工",id);Employee employee = employeeService.getById(id);return Result.success(employee); }// 编辑员工信息 @ApiOperation("编辑员工信息") @PutMapping public Result update(@RequestBody EmployeeDTO employeeDTO) {log.info("编辑员工信息:{}",employeeDTO);employeeService.update(employeeDTO);return Result.success(); }
-
Service
// 根据id查询员工 Employee getById(long id); // 更新员工信息 void update(EmployeeDTO employeeDTO);
// 根据id查询员工 @Override public Employee getById(long id) {Employee employee = employeeMapper.getById(id);employee.setPassword("****");return employee; }@Override public void update(EmployeeDTO employeeDTO) {Employee employee = new Employee();// 对象属性拷贝BeanUtils.copyProperties(employeeDTO, employee);// 设置更新时间employee.setUpdateTime(LocalDateTime.now());// 设置当前记录修改人idemployee.setUpdateUser(BaseContext.getCurrentId());employeeMapper.update(employee); }
-
Mapper
// 根据id查询员工 @Select("select * from employee where id = #{id}") Employee getById(Long id);
2.5.公共字段填充
在上面业务表中存在一些公共字段,在更新、插入数据库时需要手动填充,造成代码冗余,我们可以通过自动填充公共字段来解决这个问题
字段名 | 含义 | 数据类型 | 操作类型 |
---|---|---|---|
create_time | 创建时间 | datetime | insert |
create_user | 创建人id | bigint | insert |
update_time | 修改时间 | datetime | insert、update |
update_user | 修改人id | bigint | insert、update |
实现思路:
-
自定义注解 AutoFill,用于标识需要进行公共字段填充的方法
// sky-server/src/main/java/com/sky/annotation/AutoFill.java // 自定义注解,用于标识某个方法需要进行功能字段自动填充 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AutoFill {// 数据库操作类型:update insertOperationType value(); }
-
自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
// 自定义切面,实现公共字段自动填充处理逻辑 @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("开始进行公共字段自动填充...");// 1.获取到当前被拦截的方法上的数据库操作类型MethodSignature signature = (MethodSignature) joinPoint.getSignature(); // 方法签名对象AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); // 获得方法上的注解OperationType operationType = autoFill.value(); // 获得数据库操作类型// 2.获取到当前被拦截的方法的参数--实体对象Object[] args = joinPoint.getArgs();if(args == null || args.length == 0){return;}Object entity = args[0];// 3.准备赋值的数据LocalDateTime now = LocalDateTime.now();Long currentId = BaseContext.getCurrentId();// 4.根据当前不同的操作类型,为对应的属性通过反射赋值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) {throw new RuntimeException(e);}}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) {throw new RuntimeException(e);}}} }
-
在Mapper 的方法上加入 AutoFl 注解
// 插入员工数据 @AutoFill(value = OperationType.INSERT) @Insert( "insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user)" +"values" +"(#{name}, #{username}, #{password}, #{phone}, #{sex}, #{idNumber}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})") void insert(Employee employee);// 根据主键动态修改属性 @AutoFill(value = OperationType.UPDATE) void update(Employee employee);
我们再删除Server实现类中手动填充字段的代码,重启程序,更新时间会随着操作变化,则自动填充设置成功
3.菜品分类模块
3.1.前言
虽然这个模块视频中的老师是导入已经写好的代码,但是我还是按照文档手写了一遍,也是收获满满。由于时间原因,前面的代码都是跟着老师敲的,而我又资质愚钝,效果并不是很好。在独立地写完这个模块时,也是花了很多时间去试错(比如分页查询部分,代码一切正常,但是就是没有返回结果,控制台也不报错,排错了很久才发现是Controller中没有返回,也是被自己蠢笑了QwQ)。
在写代码中我也总结出了自己的一些小经验:
-
在选择将查询语句是否写入xml文件中,一般来说,插入和删除一般不会写入xml文件,而更新和查询会。
因为插入和查询语句很简单,一般是根据id来进行操作,而不会判断值是否为空。 -
在最开始的时候我并不理解很什么是DTO和VO的作用,后来也渐渐明白,DTO就是把前端在发送请求所携带的数据时封装的对象,VO是后端给前端发送的对象,因为前端并不会使用数据库的全部数据,Entity是和数据库中的表的字段对应的对象,POJO则是普通的java对象,DTO、VO、Entity都属于POJO
3.2.分类分页查询
-
Controller
@RestController @RequestMapping("/admin/category") @Slf4j @Api(tags = "菜品分类相关接口") public class CategoryController {@Autowiredprivate CategoryService categoryService;// 分类分页查询@GetMapping("/page")@ApiOperation("菜品分类分页查询")public Result<PageResult> page(CategoryPageQueryDTO categoryPageQueryDTO) {log.info("菜品类分页查询,参数为{}", categoryPageQueryDTO);PageResult pageResult = categoryService.pageQuery(categoryPageQueryDTO);return Result.success(pageResult);} }
-
Service
public interface CategoryService {// 分类分页查询PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO); }
// impl @Service public class CategoryServiceImpl implements CategoryService {@Autowiredprivate CategoryMapper categoryMapper;// 分类分页查询@Overridepublic PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO) {PageHelper.startPage(categoryPageQueryDTO.getPage(), categoryPageQueryDTO.getPageSize());Page<Category> page = categoryMapper.pageQuery(categoryPageQueryDTO);long total = page.getTotal();List<Category> records = page.getResult();return new PageResult(total, records);} }
-
Mapper
@Mapper public interface CategoryMapper {// 分类分页查询Page<Category> pageQuery(CategoryPageQueryDTO categoryPageQueryDTO); }
<!-- 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"><!-- 菜品分页查询 --><select id="pageQuery" resultType="com.sky.entity.Category">select * from category<where><if test="name != null and name != ''">and name like concat('%', #{name}, '%')</if><if test="type != null and type != ''">and type = #{type}</if></where>order by create_time desc</select> </mapper>
3.3.修改菜品分类
-
Controller
// 修改分类 @PutMapping @ApiOperation("修改分类") public Result update(@RequestBody CategoryDTO categoryDTO) {log.info("修改分类:{}", categoryDTO);categoryService.update(categoryDTO);return Result.success(); }
-
Service
// 修改分类 void update(CategoryDTO categoryDTO);
// 修改分类 @Override public void update(CategoryDTO categoryDTO) {Category category = new Category();// 对象属性拷贝BeanUtils.copyProperties(categoryDTO, category);categoryMapper.update(category); }
-
Mapper
// 修改分类 @AutoFill(value = OperationType.UPDATE) void update(Category category);
<!-- xml --> <!-- 修改分类 --> <update id="update">update category<set><if test="type != null">type = #{type},</if><if test="name != null">name = #{name},</if><if test="sort != null">sort = #{sort},</if><if test="status != null">status = #{status},</if><if test="updateTime != null">update_time = #{updateTime},</if><if test="updateUser != null">update_user = #{updateUser}</if></set>where id = #{id} </update>
3.4.启用禁用分类
-
Controller
// 启用、禁用分类 @PostMapping("/status/{status}") @ApiOperation("启用/禁用分类") public Result startOrStop(@PathVariable Integer status, long id) {log.info("禁用/启用菜品分类:{},{}", id, status);categoryService.startOrStop(id, status);return Result.success(); }
-
Service
// 启用、禁用分类 void startOrStop(long id, Integer status);
// 启用、禁用分类 @Override public void startOrStop(long id, Integer status) {Category category = Category.builder().id(id).status(status).build();categoryMapper.update(category); }
3.5.新增分类
-
Controller
// 新增分类 @PostMapping @ApiOperation("新增分类") public Result save(@RequestBody CategoryDTO categoryDTO) {log.info("新增分类:{}", categoryDTO);categoryService.save(categoryDTO);return Result.success(); }
-
Service
// 新增分类 void save(CategoryDTO categoryDTO);
// 新增分类 @Override public void save(CategoryDTO categoryDTO) {Category category = new Category();BeanUtils.copyProperties(categoryDTO, category);categoryMapper.save(category); }
-
Mapper
// 新增分类 @AutoFill(value = OperationType.INSERT) @Insert("insert into category (type, name, sort, status, create_time, create_user, update_time, update_user) " +"value (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{createUser}, #{updateTime}, #{updateUser})") void save(Category category);
3.6.根据id删除分类
-
Controller
// 根据id删除分类 @DeleteMapping @ApiOperation("根据id删除分类") public Result delete(long id){log.info("根据id={}删除菜品", id);categoryService.deleteById(id);return Result.success(); }
-
Service
// 根据id删除分类 void deleteById(long id);
// 新增分类 @Override public void save(CategoryDTO categoryDTO) {Category category = new Category();BeanUtils.copyProperties(categoryDTO, category);categoryMapper.save(category); }
-
Mapper
// 根据id删除分类 @Delete("delete from category where id = #{id}") void delete(long id);
3.7.根据类型查询分类
-
Controller
// 根据类型查询分类 @GetMapping("/list") @ApiOperation("根据类型查询分类") public Result<List<Category>> list(String type) {log.info("根据类型type={}查询分类", type);List<Category> list = categoryService.list(type);return Result.success(list); }
-
Service
// 根据类型查询 List<Category> list(String type);
// impl // 根据类型查询 @Override public List<Category> list(String type) {return categoryMapper.list(type); }
-
Mapper
// 根据类型查询 List<Category> list(String type);
<!-- 根据类型查询 --> <select id="list" resultType="Category">select * from categorywhere status = 1<if test="type != null">and type = #{type}</if>order by sort asc,create_time desc </select>
4.菜品模块
4.1.菜品分页查询
-
Controller
// 菜品分页查询 @GetMapping("page") @ApiOperation("菜品分页查询") public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){log.info("菜品分页查询:{}",dishPageQueryDTO);PageResult page = dishService.page(dishPageQueryDTO);return Result.success(page); }
-
Service
// 菜品分页查询 PageResult page(DishPageQueryDTO dishPageQueryDTO);
// impl // 菜品分页查询 @Override public PageResult page(DishPageQueryDTO dishPageQueryDTO) {PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());Page<Dish> page = dishMapper.page(dishPageQueryDTO);long total = page.getTotal();List<Dish> records = page.getResult();return new PageResult(total, records); }
-
Mappper
// 分类分页查询 Page<Dish> page(DishPageQueryDTO dishPageQueryDTO);
<!-- xml --> <select id="page" resultType="com.sky.entity.Dish">select * from dish<where><if test="name != null"> name like concat('%', #{name}, '%' );</if><if test="categoryId != null">category_id = #{categoryId};</if><if test="status != null">status = #{status};</if></where> </select>
4.2.根据id查询菜品
-
Controller
// 根据id查询菜品 @GetMapping("/{id}") @ApiOperation("根据id查询菜品") public Result<DishVO> getById(@PathVariable Long id){log.info("根据id查询菜品:{}",id);DishVO list = dishService.getById(id);return Result.success(list); }
-
Service
// 根据id查询菜品 DishVO getById(Long id);
// impl // 根据id查询菜品 @Override public DishVO getById(Long id) {return dishMapper.getById(id); }
-
Mappper
// 根据id查询菜品 @Select("select * from dish where id = #{id}") DishVO getById(Long categoryId);
4.3.修改菜品
-
Controller
// 修改菜品 @PutMapping @ApiOperation("修改菜品") public Result update(@RequestBody DishDTO dishDTO){log.info("修改菜品:{}",dishDTO);dishService.update(dishDTO);return Result.success(); }
-
Service
// 修改菜品 void update(DishDTO dishDTO);
// impl // 修改菜品 @Override public void update(DishDTO dishDTO) {Dish dish = Dish.builder().id(dishDTO.getId()).name(dishDTO.getName()).price(dishDTO.getPrice()).image(dishDTO.getImage()).description(dishDTO.getDescription()).status(dishDTO.getStatus()).categoryId(dishDTO.getCategoryId()).build();dishMapper.update(dish); }
-
Mappper
// 修改菜品 @AutoFill(OperationType.UPDATE) void update(Dish dish);
<!-- xml --> <!-- 修改菜品 --> <update id="update">update dish<set><if test="name != null">name = #{name},</if><if test="categoryId != null">category_id = #{categoryId},</if><if test="price != null">price = #{price},</if><if test="image != null">image = #{image},</if><if test="description != null">description = #{description},</if><if test="status != null">status = #{status},</if><if test="updateTime != null">update_time = #{updateTime},</if><if test="updateUser != null">update_user = #{updateUser}</if></set> </update>
4.4.菜品起售停售
-
Controller
// 菜品起售停售 @PostMapping("status/{status}") @ApiOperation("菜品起售停售") public Result startOrStop(@PathVariable Integer status, Long id){log.info("菜品起售停售:{}, {}", status, id);dishService.startOrStop(id, status);return Result.success(); }
-
Service
// 菜品起售停售 void startOrStop(Long id, Integer status);
// impl // 菜品起售停售 @Override public void startOrStop(Long id, Integer status) {Dish dish = Dish.builder().id(id).status(status).build();dishMapper.update(dish);if(status == StatusConstant.DISABLE){// 如果是停售操作,还需要将包含当前菜品的套餐也停售List<Long> dishIds = new ArrayList<>();dishIds.add(id);List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(dishIds);if(setmealIds != null && setmealIds.size() > 0){for(Long setmealId : setmealIds){Setmeal setmeal = Setmeal.builder().id(setmealId).status(StatusConstant.DISABLE).build();setmealDishMapper.update(setmeal);}}} }
-
Mapper
// sky-server/src/main/java/com/sky/mapper/SetmealDishMapper.java // 根据id修改套餐 @AutoFill(OperationType.UPDATE) void update(Setmeal setmeal);
<!-- sky-server/src/main/resources/mapper/SetmealDishMapper.xml --> <!-- 根据id修改套餐 --> <update id="update">update setmeal<set><if test="name != null">name = #{name}</if><if test="name != null">category_id = #{categoryId}</if><if test="name != null">price = #{price}</if><if test="name != null">status = #{status}</if><if test="name != null">description = #{description}</if><if test="name != null">image = #{image}</if><if test="name != null">update_time = #{updateTime}</if><if test="name != null">update_user = #{updateUser}</if></set> </update>
4.5.文件上传
-
配置oss
sky:alioss:endpoint: ${sky.alioss.endpoint}access-key-id: ${sky.alioss.access-key-id}access-key-secret: ${sky.alioss.access-key-secret}bucket-name: ${sky.alioss.bucket-name}
配置属性类
// sky-common/src/main/java/com/sky/properties/AliOssProperties.java @Component @ConfigurationProperties(prefix = "sky.alioss") @Data public class AliOssProperties {private String endpoint;private String accessKeyId;private String accessKeySecret;private String bucketName; }
# sky-server/src/main/resources/application-dev.yml sky:alioss:endpoint: oss-cn-beijing.aliyuncs.comaccess-key-id: LTAI5tKr2ZB866rBJea9ucuJaccess-key-secret: gLx3lSngXHXv5SUyBW2c29XhHL5XQqbucket-name: sky-jiaqi
-
Controller
在项目的sky-common/src/main/java/com/sky/utils/AliOssUtil.java中,已经写好了请求oss的工具类,我们在yml文件中配置好bucket仓库,然后调用公里类中的upload方法上传文件即可
// sky-server/src/main/java/com/sky/controller/admin/CommonController.java // 通用接口 @RestController @Slf4j @RequestMapping("/admin/common") @Api(tags = "通用接口") public class CommonController {@Autowiredprivate AliOssUtil aliOssUtil;@PostMapping("/upload")@ApiOperation("文件上传")public Result<String> upload(MultipartFile file){log.info("文件上传:{}", file);try {// 原始文件名String originalFilename = file.getOriginalFilename();// 截取原始文件名的后缀String extension = originalFilename.substring(originalFilename.lastIndexOf("."));// 构造新的文件名String objectName = UUID.randomUUID().toString() + extension;// 文件请求路径String filePath = aliOssUtil.upload(file.getBytes(), objectName);return Result.success(filePath);} catch (IOException e) {log.error("文件上传失败:{}", e);}return Result.error(MessageConstant.UPLOAD_FAILED);} }
4.6.新增菜品
业务规则:
- 菜品名称必须统一
- 菜品必须属于某个分类下,不能单独存在
- 新增菜品时可以根据1情况选择菜品的口味
- 每个菜品必须对应一张图片
-
Controller
// 新增菜品 @PostMapping() @ApiOperation("新增菜品") public Result save(@RequestBody DishDTO dishDTO) {log.info("新增菜品{}", dishDTO);dishService.saveWithFlavor(dishDTO);return Result.success(); }
-
Service
// 修改菜品 void update(DishDTO dishDTO);
// impl // 新增菜品 @Override @Transactional public void saveWithFlavor(DishDTO dishDTO) {Dish dish = new Dish();BeanUtils.copyProperties(dishDTO, dish);// 向菜品表插入一条数据dishMapper.insert(dish);// 获取insert语句生成的主键值Long dishId = dish.getId();List<DishFlavor> flavors = dishDTO.getFlavors();if (flavors != null && flavors.size() > 0) {flavors.forEach(flavor -> {flavor.setDishId(dishId);});// 向口味表插入n条数据dishFlavorMapper.insertBatch(flavors);} }
-
Mappper
// 向菜品表插入一条数据 @AutoFill(OperationType.INSERT) void insert(Dish dish);
<!-- xml --> <!-- 向菜品表中插入一条数据 --> <insert id="insert" useGeneratedKeys="true" keyProperty="id">insert into dish(name, category_id, price, image, description, create_time, update_time, create_user, update_user, status)values(#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser}, #{status}); </insert>
@Mapper public interface DishFlavorMapper {// 向口味表插入n条数据void insertBatch(List<DishFlavor> flavors); }
<!-- xml --> <mapper namespace="com.sky.mapper.DishFlavorMapper"><insert id="insertBatch">insert into dish_flavor(dish_id, name, value)values<foreach collection="flavors" item="df" separator=",">(#{df.dishId}, #{df.name}, #{df.value})</foreach></insert> </mapper>
4.7.批量删除菜品
业务规则:
- 可以一次删除一个菜品,也可以批量删除菜品
- 起售中的菜品不能删除
- 被套餐关联的菜品不能删除
- 删除菜品后,关联的口味数据也需要删除掉
代码
-
Controller
// 菜品批量删除 @DeleteMapping @ApiOperation("菜品批量删除") public Result delete(@RequestParam List<Long> ids) {log.info("菜品批量删除:{}", ids);dishService.deleteBatch(ids);return Result.success(); }
-
Service
// 菜品批量删除 void deleteBatch(List<Long> ids);
// impl // 菜品批量删除 @Override @Transactional public void deleteBatch(List<Long> ids) {// 是否存在起售中的菜品for (Long id : ids) {Dish dish = dishMapper.getById(id);if(Objects.equals(dish.getStatus(), StatusConstant.ENABLE)){// 当前菜品处于起售中,不能删除throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);}}// 判断菜品是否关联套餐List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);if(setmealIds != null && setmealIds.size() > 0 ){// 当前菜品被套餐关联throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_DISH);}// 删除菜品表中的菜品数据for (Long id : ids) {dishMapper.deleteById(id);// 删除菜品关联的口味数据dishFlavorMapper.deleteByDishId(id);} }
-
Mappper
-
根据菜品id查询套餐id
// sky-server/src/main/java/com/sky/mapper/SetmealDishMapper.java // 根据菜品id查询套餐id List<Long> getSetmealIdsByDishIds(List<Long> dishIds);
<!-- sky-server/src/main/resources/mapper/SetmealDishMapper.xml --> <!-- xml --> <!-- 根据菜品id查询套餐id --> <select id="getSetmealIdsByDishIds" resultType="java.lang.Long">select setmeal_id from setmeal_dish where dish_id in<foreach collection="dishIds" item="dishId" separator="," open="(" close=")">#{dishId}</foreach> </select>
-
删除
// 根据id删除菜品表数据 @Delete("delete from dish where id = #{id}") void deleteById(Long id);
// 删除菜品关联的口味数据 @Delete("delete from dish_flavor where dish_id = #{dish_id}") void deleteByDishId(Long dishId);
-
5.Redis
Redis是一个基于内存的 key-value 结构数据库。
- 基于内存存储,读写性能高
- 适合存储热点数据 (热点商品、资讯 新闻)
- 企业应用广泛
下载地址:
- Windows版下载地址:https://github.com/microsoftarchive/redis/releases
- Linux版下载地址:https://download.redis.io/releasesl
-
解压后目录
-
启动redis服务(默认端口号为6379)
在安装的目录下运行
redis-server.exe redis.windows.conf
可以修改密码(也可以不设置)
在后面对Redis的操作,我们大多采用软件Another Redis Desktop Manager来完成
5.1.数据类型
- 字符串(string):普通字符串,Redis中最简单的数据类型
- 哈希(hash):也叫散列,类似于Java中的HashMap结构
- 列表(list):按照插入顺序排序,可以有重复元素,类似于Java中的LinkedList
- 集合(set):无序集合,没有重复元素,类似于Java中的HashSet
- 有序集合(sorted set / zset):集合中每个元素关联一个分数(score),根据分数升序排序,没有重复元素
5.2.常用命令
-
字符串
命令 说明 SET key value 设置指定key的值 GET key 获取指定key的值 SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒 SETNX key value 只有在 key 不存在时设置 key 的值 -
哈希
Redis hash 是一个string类型的 field 和 value 的映射表,hash特别适合用于存储对象
key相对于对象名,field相当于属性名,value相当于属性值
命令 说明 HSET key field value 将哈希表 key 中的字段field 的值设为 value HGET key field 获取存储在哈希表中指定字段的值 HDEL key field 删除存储在哈希表中的指定字段 HKEYS key 获取哈希表中所有字段 HVALS key 获取哈希表中所有值 -
列表
Redis 列表是简单的字符串列表,按照插入顺序排序
命令 说明 LPUSH key value1 [value2] 将一个或多个值插入到列表头部 LRANGE key start stop 获取列表指定范围内的元素(从0开始) RPOP key 移除并获取列表最后一个元素 LLEN key 获取列表长度 -
集合
Redis set 是string类型的无序集合。集合成员是唯一的,集合中不能出现重复的数据
命令 说明 SADD key member1 [member2] 向集合添加一个或多个成员 SMEMBERS key 返回集合中的所有成员 SCARD key 获取集合的成员数 SINTER key1 [key2] 返回给定所有集合的交集 SUNION key1 [key2] 返回所有给定集合的并集 SREM key member1 [member2] 删除集合中一个或多个成员 -
有序集合
Redis有序集合是string类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数。
命令 说明 ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员 ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员 ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment ZREM key member [member …] 移除有序集合中的一个或多个成员 -
通用命令
命令 说明 KEYS pattern 查找所有符合给定模式( pattern)的 key,通配符为 *
EXISTS key 检查给定 key 是否存在 TYPE key 返回 key 所储存的值的类型 DEL key 该命令用于在 key 存在时删除 key
5.3.java中操作Redis
常用的redis的java客户端:
- Jedis
- Lettuce
- Spring Data Redis(推荐)
操作步骤:
-
导入Spring Data Redis 的maven坐标(初始项目已经导入)
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
配置Redis数据源()
database配置项,在启动redis服务后,默认创建16个数据库(0~15),默认为0、
# application.yml # 该配置项与datasource平级 redis:host: ${sky.redis.host}port: ${sky.redis.port}# password: 这里我没有设置密码,所有没有写database: ${sky.redis.database}
# application-dev.yml # 该配置项与datasource平级 redis:host: localhostport: 6379# password: 这里我没有设置密码,所有没有写database: 0
-
编写配置类,创建RedisTemplate对象
// sky-server/src/main/java/com/sky/config/RedisConfiguration.java @Configuration @Slf4j public class RedisConfiguration {@Beanpublic RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {log.info("开始创建redis模版对象");RedisTemplate redisTemplate = new RedisTemplate();// 设置redis的连接工程对象redisTemplate.setConnectionFactory(redisConnectionFactory);// 设置redis key的序列化器redisTemplate.setKeySerializer(new StringRedisSerializer());return redisTemplate;} }
-
通过RedisTemplate对象操作Redis
在对数据进行操作时,可以将对象先复制,减少代码冗余
@Test public void test() {// 操作字符串的对象ValueOperations valueOperations = redisTemplate.opsForValue();// 操作哈希的对象HashOperations hashOperations = redisTemplate.opsForHash();// 操作列表的对象ListOperations listOperations = redisTemplate.opsForList();// 操作集合的对象SetOperations setOperations = redisTemplate.opsForSet();// 操作有序集合的对象ZSetOperations zSetOperations = redisTemplate.opsForZSet(); }
对字符串类型操作
@Test public void testString() {// set(添加)redisTemplate.opsForValue().set("key1", "value1");// get(获取)String city = (String) redisTemplate.opsForValue().get("key1");System.out.println(city);// setex(限时字符串)redisTemplate.opsForValue().set("key2", "111", 60, TimeUnit.SECONDS);// setnx(没有就添加字符串)redisTemplate.opsForValue().setIfAbsent("lock", "1");// 因为上面已经设置了lock,所有这里不会设置成功redisTemplate.opsForValue().setIfAbsent("lock", "2"); }
对hash类型操作
// 操作hash类型的数据 @Test public void testHsh() {HashOperations hashOperations = redisTemplate.opsForHash();// hset(添加)hashOperations.put("ob", "name", "Tom");hashOperations.put("ob", "age", "18");// hget(获取)String name = (String)hashOperations.get("ob", "name");System.out.println(name);// hvals(获取该键的所有values)List values = hashOperations.values("ob");System.out.println(values);// hdel(删除)hashOperations.delete("ob", "name"); }
对列表类型操作
@Test public void testList() {ListOperations listOperations = redisTemplate.opsForList();// lpush(添加元素)listOperations.leftPush("lst", "1");listOperations.leftPushAll("lst", "2", "3", "4");// lrange(范围获取)List list = listOperations.range("lst", 0, -1);System.out.println(list); //[4, 3, 2, 1]// rpop(右弹出一个元素)Object i1 = listOperations.rightPop("lst");System.out.println(i1); // 1// llen(列表元素个数)Long len = listOperations.size("lst");System.out.println(len); // 3 }
对集合进行操作
@Test public void testSet() {SetOperations setOperations = redisTemplate.opsForSet();// sadd(添加)setOperations.add("set1", "a", "b", "c");setOperations.add("set2", "d", "e", "f");// smembers(获取集合元素)Set members = setOperations.members("set1");System.out.println(members); // [b, c, a]// scard(元素个数)Long size = setOperations.size("set1");System.out.println(size); // 3// sinter(求交集)Set intersect = setOperations.intersect("set1", "set2");System.out.println(intersect); // []// sunion(求并集)Set union = setOperations.union("set1", "set2");System.out.println(union); // [b, a, c, d, f, e]// srem(移除元素)setOperations.remove("set1", "a"); }
对有序集合进行操作
@Test public void testZSet() {ZSetOperations zSetOperations = redisTemplate.opsForZSet();// zadd(添加)zSetOperations.add("zset1", "a", 1);zSetOperations.add("zset1", "b", 2);zSetOperations.add("zset1", "c", 3);// zincrby(赋权重)zSetOperations.incrementScore("zset1", "a", 3);zSetOperations.incrementScore("zset1", "b", 2);zSetOperations.incrementScore("zset1", "c", 1);// zrange(范围获取)Set orderset = zSetOperations.range("zset1", 0, -1);// 最先打印有序列表权重最大的元素System.out.println(orderset); // [a, b, c]// zrem(删除)zSetOperations.remove("zset1", "a"); }
通用命令操作
@Test public void testCommon() {// keys(获取key)Set keys = redisTemplate.keys("*");System.out.println(keys); // [ob, name, set1, obj, lock, set2, key1, zset1, lst]// exists(查询key是否存在)redisTemplate.hasKey("name");redisTemplate.hasKey("set1");// type(查看key的类型)for (Object key : keys) {DataType type = redisTemplate.type(key);System.out.println(type); // HASH STRING...}// del(删除)redisTemplate.delete("name"); }
5.4.店铺模块
业务规则:
-
管理端设置、查询营业状态
@RestController("adminShopController") @RequestMapping("/admin/shop") @Api(tags = "店铺相关接口") @Slf4j public class ShopController {public static final String KEY = "shop_status";@Autowiredprivate RedisTemplate redisTemplate;// 设置店铺营业状态@PutMapping("/{status}")@ApiOperation("设置店铺营业状态")public Result setStatus(@PathVariable Integer status) {log.info("设置店铺营业状态:{}", status == 1?"营业中":"打烊中");redisTemplate.opsForValue().set(KEY, status);return Result.success();}// 查询店铺状态@GetMapping("/status")@ApiOperation("获取店铺营业状态")public Result<Integer> getStatus() {Integer shopStatus = (Integer) redisTemplate.opsForValue().get(KEY);log.info("获取到店铺的营业状态为:{}", shopStatus == 1? "营业中":"打烊中");return Result.success(shopStatus);} }
-
用户端查询营业状态
@RestController("userShopController") @RequestMapping("/user/shop") @Api(tags = "店铺相关接口") @Slf4j public class ShopController {public static final String KEY = "shop_status";@Autowiredprivate RedisTemplate redisTemplate;// 查询店铺状态@GetMapping("/status")@ApiOperation("获取店铺营业状态")public Result<Integer> getStatus() {Integer shopStatus = (Integer) redisTemplate.opsForValue().get(KEY);log.info("获取到店铺的营业状态为:{}", shopStatus == 1? "营业中":"打烊中");return Result.success(shopStatus);} }
-
我们可以通过修改WebMvcConfiguration.config配置文件,将接口分为管理端和用户端
// 将docket拆分为docket1、docket2两个文档,并进行分组 @Bean public Docket docket1() {log.info("准备生成接口文档...");ApiInfo apiInfo = new ApiInfoBuilder()// 省略...Docket docket = new Docket(DocumentationType.SWAGGER_2).groupName("管理端接口")// 省略...return docket; }@Bean public Docket docket2() {log.info("准备生成接口文档...");ApiInfo apiInfo = new ApiInfoBuilder()// 省略...Docket docket = new Docket(DocumentationType.SWAGGER_2).groupName("用户端接口")// 省略...return docket; }
分组完成
6.HttpClient
HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP协议最新的版本和建议。
maven坐标
<dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId><version>4.5.13</version>
</dependency>
但是在我们的项目组不用导入,因为我吗之前导入了阿里OSS的maven坐标,这个坐标依赖了httpclientjar包
-
发送Get方式请求
先启动主程序和Redis服务
// 发送Get方式请求 @Test public void testGet() throws IOException {// 1.创建httpclient对象CloseableHttpClient httpclient = HttpClients.createDefault();// 2.创建请求对象HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");// 3.发送请求,接受响应结果CloseableHttpResponse response = httpclient.execute(httpGet);// 获取服务端返回的状态码int statusCode = response.getStatusLine().getStatusCode();System.out.println("服务端返回的状态码" + statusCode); // 服务端返回的状态码200// 获取服务端返回的数据HttpEntity entity = response.getEntity();String body = EntityUtils.toString(entity);System.out.println("服务端返回的数据" + body); // 服务端返回的数据{"code":1,"msg":null,"data":1}// 关闭资源response.close(); }
-
发送Post方式请求
@Test public void testPost() throws IOException {// 1.创建httpclient对象CloseableHttpClient httpclient = HttpClients.createDefault();// 2.创建请求对象HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");// ---设置请求参数JSONObject jsonObject = new JSONObject();jsonObject.put("username", "admin");jsonObject.put("password", "123456");StringEntity requestEntity = new StringEntity(jsonObject.toString());// 指定请求编码方式requestEntity.setContentEncoding("UTF-8");// 数据格式requestEntity.setContentType("application/json");httpPost.setEntity(requestEntity);// 3.发送请求,接受响应结果CloseableHttpResponse response = httpclient.execute(httpPost);// 获取服务端返回的状态码int statusCode = response.getStatusLine().getStatusCode();System.out.println("服务端返回的状态码" + statusCode); // 服务端返回的状态码200// 获取服务端返回的数据HttpEntity responseEntity = response.getEntity();String body = EntityUtils.toString(responseEntity);System.out.println("服务端返回的数据" + body); // // 服务端返回的数据{"code":1,"msg":null,"data":{"id":1,"userName":"admin","name":"管理员","token":"..."}}// 关闭资源response.close(); }
但是,但是?但是!在初始项目中,已经封装好了工具类(sky-common/src/main/java/com/sky/utils/HttpClientUtil.java)