电商生鲜网站开发(三)——后台开发:商品分类模块-Redis/Swagger/统一身份校验/IDEA技巧
分类层级
在商品分类上需要继续做归类操作
分类设置成三级
层级太深的弊端:对用户不友好,不利于查找;后台管理部方便
分类模块主要功能
分类数据的设置
根据分类的父级目录递归查找
接口与表设计
https://blog.csdn.net/a2272062968/article/details/123385857
分类接口开发
跟用户接口类似
参数校验
注解 | 说明 |
---|---|
@Valid | 需要验证 |
@NotNull | 非空 |
@Max(Value) | 最大值 |
@Size(max,min) | 字符串长度范围限制 |
在前台传入的实体类定义中参数上增加对应规则的注解即可,可以增加多个规则
package com.imooc.mall.model.request;
import lombok.Data;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
/**
* 描述: AddCategoryReq
*/
@Data
public class AddCategoryReq {
@Size(min = 2,max = 5)
private String name;
@NotNull
@Max(3)
private Integer type;
@NotNull(message = "parentId不能为null")
private Integer parentId;
@NotNull(message = "orderNum不能为null")
private Integer orderNum;
}
增加校验后即可拦截不规范的数据,我们捕捉注解抛出的异常就可以获取到提示信息
去之前的GloalExceptionHandler拦截异常类中编写异常过滤器
package com.learn2333.mall.exception;
//拦截异常
@ControllerAdvice
@Slf4j
public class GloalExceptionHandler {
private final Logger logger = LoggerFactory.getLogger(GloalExceptionHandler.class);
@ExceptionHandler(Exception.class)
@ResponseBody
public Object handleException(Exception e) {
logger.error("Default Exception:", e);
return ApiRestResponse.error(MallExceptionEnum.SYSTEM_ERROR);
}
@ExceptionHandler(MallException.class)
@ResponseBody
public Object handleImoocMallException(MallException e) {
logger.error("MallException:", e);
return ApiRestResponse.error(e.getCode(), e.getMessage());
}
/**
* 方法参数不合规处理
* @param e
* @return
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public ApiRestResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
logger.error("MethodArgumentNotValidException:", e);
return handleBindingResult(e.getBindingResult());
}
private ApiRestResponse handleBindingResult(BindingResult result) {
//把异常处理为对外暴露的提示
List<String> list = new ArrayList<>();
if (result.hasErrors()) {
List<ObjectError> allErrors = result.getAllErrors();
// itli
for (ObjectError objectError : allErrors) {
String message = objectError.getDefaultMessage();
list.add(message);
}
}
if (list.size() == 0) {
return ApiRestResponse.error(MallExceptionEnum.REQUEST_PARAM_ERROR);
}
return ApiRestResponse.error(MallExceptionEnum.REQUEST_PARAM_ERROR.getCode(), list.toString());
}
}
异常枚举所有错误码
package com.learn2333.mall.exception;
/**
* 描述: 异常枚举
*/
public enum MallExceptionEnum {
NEED_USER_NAME(10001,"用户名不能为空"),
NEED_PASSWORD(10002,"密码不能为空"),
NEED_TOO_SHORT(10003,"密码长度不能小于8位"),
NAME_EXISTED(10004,"不允许重名"),
INSERT_FAILED(10005,"插入失败,请重试"),
WRONG_PASSWORD(10006,"登录密码错误,请重试"),
NEED_LOGIN(10007,"用户未登录,请登录"),
UPDATE_FAILD(10008,"更新失败,请重试"),
NEED_ADMIN(10009,"无管理员权限"),
PARA_NOT_NULL(10010,"参数不能为空"),
CREATE_FAILED(10011,"新增失败"),
REQUEST_PARAM_ERROR(10012,"参数错误"),
DELETE_FAILED(10013,"删除失败"),
MKDIR_FAILED(10014,"文件夹创建失败"),
UPLOAD_FAILED(10015,"图片上传失败"),
NOT_SALE(10016,"商品状态不可售"),
NOT_ENOUGH(10017,"商品库存不足"),
CART_EMPTY(10018,"购物车已勾选的商品为空"),
NO_ENUM(10019,"未找到对应的枚举"),
NO_ORDER(10020,"订单不存在"),
NO_YOUR_ORDER(10021,"订单不属于你"),
WRONG_ORDER_STATUS(10021,"订单不符"),
SYSTEM_ERROR(20000,"系统异常");
/**
* 异常码
*/
Integer code;
/**
* 异常信息
*/
String msg;
MallExceptionEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
Swagger自动生成API文档
引入依赖
pom
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
开启
入口类上增加@EnableSwagger2注解开启自动生成api文档
修改配置
config包下修改配置文件
package com.learn2333.mall.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
/**
* 配置Swagger
*/
@Configuration
public class SpringFoxConfig {
//访问http://localhost:8083/swagger-ui.html可以看到API文档
@Bean
public Docket api() {
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("生鲜网站")
.description("")
.termsOfServiceUrl("")
.build();
}
}
配置MVC Config Swagger地址映射
package com.learn2333.mall.config;
import com.learn2333.mall.common.Constant;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 描述: 配置Swagger地址映射
*/
@Configuration
public class MallWebMvcConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// registry.addResourceHandler("/images/**").addResourceLocations("file:" + Constant.FILE_UPLOAD_DIR);
registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
}
使用方法
在写好的接口上增加@ApiOperation注解
@ApiOperation("后台删除目录")
@PostMapping("admin/category/delete")
@ResponseBody
public ApiRestResponse deleteCategory(@RequestParam Integer id) {
categoryService.delete(id);
return ApiRestResponse.success();
}
访问
http://localhost:8083/swagger-ui.html
即可展示所有的接口和接口名,在此页面也可以直接简单调试接口
统一校验管理员身份
编写过滤器
package com.learn2333.mall.filter;
import com.learn2333.mall.common.Constant;
import com.learn2333.mall.model.pojo.User;
import com.learn2333.mall.service.UserService;
import java.io.IOException;
import java.io.PrintWriter;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
import javax.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
/**
* 描述: 管理员校验过滤器
*/
public class AdminFilter implements Filter {
@Autowired
UserService userService;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpSession session = request.getSession();
User currentUser = (User) session.getAttribute(Constant.MALL_USER);
if (currentUser == null) {
PrintWriter out = new HttpServletResponseWrapper(
(HttpServletResponse) servletResponse).getWriter();
out.write("{\n"
+ " \"status\": 10007,\n"
+ " \"msg\": \"NEED_LOGIN\",\n"
+ " \"data\": null\n"
+ "}");
out.flush();
out.close();
return;
}
//校验是否是管理员
boolean adminRole = userService.checkAdminRole(currentUser);
if (adminRole) {
filterChain.doFilter(servletRequest, servletResponse);
} else {
PrintWriter out = new HttpServletResponseWrapper(
(HttpServletResponse) servletResponse).getWriter();
out.write("{\n"
+ " \"status\": 10009,\n"
+ " \"msg\": \"NEED_ADMIN\",\n"
+ " \"data\": null\n"
+ "}");
out.flush();
out.close();
}
}
@Override
public void destroy() {
}
}
配置过滤器生效范围
这样,当请求路径带有以下范围时,过滤器拦截生效,进行上面的管理员身份校验
package com.learn2333.mall.config;
import com.learn2333.mall.filter.AdminFilter;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 描述: Admin过滤器的配置
*/
@Configuration
public class AdminFilterConfig {
@Bean
public AdminFilter adminFilter() {
return new AdminFilter();
}
@Bean(name = "adminFilterConf")
public FilterRegistrationBean adminFilterConfig() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
filterRegistrationBean.setFilter(adminFilter());
filterRegistrationBean.addUrlPatterns("/admin/category/*");
filterRegistrationBean.addUrlPatterns("/admin/product/*");
filterRegistrationBean.addUrlPatterns("/admin/order/*");
filterRegistrationBean.setName("adminFilterConf");
return filterRegistrationBean;
}
}
分页功能
@Override
public PageInfo listForAdmin(Integer pageNum, Integer pageSize) {
PageHelper.startPage(pageNum, pageSize, "type,order_num");
List<Category> categoryList = categoryMapper.selectList();
PageInfo pageInfo = new PageInfo(categoryList);
return pageInfo;
}
用户分类接口列表-递归查询
@Override
public List<CategoryVO> listCategoryForCustomer(Integer parentId) {
ArrayList<CategoryVO> CategoryVOList = new ArrayList<>();
recursivelyFindCategories(CategoryVOList, parentId);
return CategoryVOList;
}
private void recursivelyFindCategories(List<CategoryVO> categoryVOList, Integer parentId) {
//递归获取所有子类别,并组合成为一个"目录树"
List<Category> categoryList = categoryMapper.selectCategoriesByParentId(parentId);
if (!CollectionUtils.isEmpty(categoryList)) {
for (Category category : categoryList) {
CategoryVO categoryVO = new CategoryVO();
BeanUtils.copyProperties(category, categoryVO);
categoryVOList.add(categoryVO);
//将此目录的子目录进行递归填充
recursivelyFindCategories(categoryVO.getChildCategory(), categoryVO.getId());
}
}
}
利用SpringBoot集成Redis
引入pom
<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>
配置连接
application.properties 配置连接(默认端口号、密码为空)
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=
增加启动注解
启动类上增加@EnableCaching注解,打开缓存功能
测试redis
测试redis是否启动:终端进入redis/src目录 - ./redis-cli - ping回应PONG说明启动了
Redis内容:https://blog.csdn.net/a2272062968/article/details/119866634
在需要缓存的方法上增加注解
在目录列表实现方法上面增加注解,此方法的Cache就配置好了
@Override
@Cacheable(value = "listCategoryForCustomer")
public List<CategoryVO> listCategoryForCustomer(Integer parentId) {
ArrayList<CategoryVO> CategoryVOList = new ArrayList<>();
recursivelyFindCategories(CategoryVOList, parentId);
return CategoryVOList;
}
配置Redis
还要配置Redis,编写Redis的配置类,配置超时时间等
package com.learn2333.mall.config;
import java.time.Duration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
/**
* 描述: 缓存的配置类
*/
@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.lockingRedisCacheWriter(connectionFactory);
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
cacheConfiguration = cacheConfiguration.entryTtl(Duration.ofSeconds(30)); //超时时间30s
RedisCacheManager redisCacheManager = new RedisCacheManager(redisCacheWriter, cacheConfiguration);
return redisCacheManager;
}
}
需要缓存的实体类实现接口
要想让分类写入缓存还必须为对应的实体类实现Serializable接口
public class CategoryVO implements Serializable
验证缓存生效
**方法一:**在30秒内调用的缓存方法都会返回之前查询的缓存内容;可以在需要缓存的内容方法中打断电测试,调用方法时没有执行方法而是直接从缓存中读取,速度显著提升
**方法二:**在终端使用keys查看redis中的内容
IDEA调试技巧
统一开关
让所有断点失效
条件断点
指定条件成立时生效
单步调试
表达式求值
可以输入任意表达式查询内部的内容
总结
知识点:参数校验、Swagger、统一鉴权、Redis整合、调试功能
开发常见问题:参数手动校验、项目不用缓存、不善用调试