品牌数据表对应数据库gulimall-pms
中的pms-brand
数据表结构
字段名称 | 字段类型 | 字段长度 | 不是null | 主键 | 注释 |
---|---|---|---|---|---|
brand_id | bigint | 20 | √ | √ | 品牌id |
name | char | 50 | 品牌名 | ||
logo | varchar | 2000 | 品牌logo地址 | ||
descript | longtext | 介绍 | |||
show_status | tinyint | 4 | 显示状态[0-不显示;1-显示] | ||
first_letter | char | 1 | 检索首字母 | ||
sort | int | 11 | 排序 |
之前的三级分类中,自己设计界面进行增删改查。其实在逆向工程renren-generator
生成的代码中,会生成对应数据表的增删改查界面。
1. 新增品牌管理菜单
2. 搭建基础增删改查界面
使用逆向生成的代码
将两个vue界面复制到modules/product中
重启项目,打开品牌维护界面,基础增删改查已经就绪
由于系统默认会做权限控制,有一些按钮会做权限判断是否显示
将权限控制设置为永远返回true
重新查看页面效果
3. 优化细节—显示状态[0-不显示;1-显示]
-
删除
[0-不显示;1-显示]
-
列表
brand.vue
显示状态 -
新增更新
brand-add-or-update.vue
显示状态
-
-
将显示状态改为用
switch
组件控制-
列表
brand.vue
显示状态<el-table-column prop="showStatus" header-align="center" align="center" label="显示状态" > <template slot-scope="scope"> <el-switch v-model="scope.row.showStatus" active-color="#13ce66" inactive-color="#ff4949" > </el-switch> </template> </el-table-column>
-
新增更新
brand-add-or-update.vue
显示状态<el-form-item label="显示状态" prop="showStatus"> <el-switch v-model="dataForm.showStatus" active-color="#13ce66" inactive-color="#ff4949" > </el-switch> </el-form-item>
-
效果
-
-
监听
Switch
组件点击事件-
列表
brand.vue
显示状态<el-switch v-model="scope.row.showStatus" active-color="#13ce66" inactive-color="#ff4949" @change="updateBrandStatus(scope.row)" :active-value="1" :inactive-value="0" >
直接使用逆向生成代码中的
product/brand/update
接口来更新数据updateBrandStatus(data) { let { brandId, showStatus } = data; this.$http({ url: this.$http.adornUrl("/product/brand/update"), method: "post", data: this.$http.adornData({ brandId, showStatus }, false), }).then(({ data }) => { this.$message({ type: "success", message: "状态更新成功", }); }); },
-
4. 文件上传
分布式文件上传将所有的文件存储服务在统一位置处理。
4.1 阿里云对象存储OSS
4.1.1. 简介
对象存储服务(Object Storage Service,OSS)是一种海量、安全、低成本、高可靠的云存储服务,适合存放任意类型的文件。容量和处理能力弹性扩展,多种存储类型供选择,全面优化存储成本。
4.1.2. 使用步骤
-
开通阿里云对象存储服务https://www.aliyun.com/product/oss
进入控制台,可以查看API文档
资源术语
中文 英文 说明 存储空间 Bucket 存储空间是您用于存储对象(Object)的容器,所有的对象都必须隶属于某个存储空间。 对象/文件 Object 对象是 OSS 存储数据的基本单元,也被称为OSS的文件。对象由元信息(Object Meta)、用户数据(Data)和文件名(Key)组成。对象由存储空间内部唯一的Key来标识。 地域 Region 地域表示 OSS 的数据中心所在物理位置。您可以根据费用、请求来源等综合选择数据存储的地域。详情请查看OSS已经开通的Region。 访问域名 Endpoint Endpoint 表示OSS对外服务的访问域名。OSS以HTTP RESTful API的形式对外提供服务,当访问不同地域的时候,需要不同的域名。通过内网和外网访问同一个地域所需要的域名也是不同的。具体的内容请参见各个Region对应的Endpoint。 访问密钥 AccessKey AccessKey,简称 AK,指的是访问身份验证中用到的AccessKeyId 和AccessKeySecret。OSS通过使用AccessKeyId 和AccessKeySecret对称加密的方法来验证某个请求的发送者身份。AccessKeyId用于标识用户,AccessKeySecret是用户用于加密签名字符串和OSS用来验证签名字符串的密钥,其中AccessKeySecret 必须保密。 推荐:一个项目创建一个
Bucket
。 -
创建Bucket
4.2. 图片上传方式
4.2.1. 方式一:普通上传方式
这种方式用户上传还要经过自己的应用服务器,额外操作。
4.2.2. 方式一:服务端签名后直传
4.2.3. 项目采用上传方式
- 阿里云存储对象账号密码存储在自己的应用服务器中
- 前端向阿里云发送数据的时候,首先向服务器请求Policy上传策略,服务器根据账号密码生成防伪签名(防伪策略,令牌,地址等)
- 前端携带防伪签名访问OSS,如果正确接收上传请求。
4.3. 文件上传实现
4.3.1. 安装SDK,引入依赖
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
4.3.2. 测试文件上传
-
复制文件上传代码
// yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-hangzhou.aliyuncs.com。 String endpoint = "yourEndpoint"; // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。 String accessKeyId = "yourAccessKeyId"; String accessKeySecret = "yourAccessKeySecret"; // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); // 创建PutObjectRequest对象。 // 填写Bucket名称、Object完整路径和本地文件的完整路径。Object完整路径中不能包含Bucket名称。 // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。 PutObjectRequest putObjectRequest = new PutObjectRequest("examplebucket", "exampleobject.txt", new File("D:\\localpath\\examplefile.txt")); // 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。 // ObjectMetadata metadata = new ObjectMetadata(); // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString()); // metadata.setObjectAcl(CannedAccessControlList.Private); // putObjectRequest.setMetadata(metadata); // 上传文件。 ossClient.putObject(putObjectRequest); // 关闭OSSClient。 ossClient.shutdown();
-
参数
endpoint:地域节点
accessKeyId、accessKeySecret:
-
管理AccessKey
-
使用子用户AccessKey
-
创建用户
-
设置账号
-
开通后生成accessKeyId、accessKeySecret
-
新建账户没有任何权限,添加权限
-
-
完整代码
@Test public void testUpload() { String endpoint = "xxx"; String accessKeyId = "xxx"; String accessKeySecret = "xxx"; OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); PutObjectRequest putObjectRequest = new PutObjectRequest("gulimall-kaisarh", "login.png", new File("C:\\Users\\Administrator\\Pictures\\login.png")); ossClient.putObject(putObjectRequest); ossClient.shutdown(); System.out.println("上传完成"); }
-
测试
4.4. SpringCloud Alibaba-OSS实现对象存储
4.4.1. 引入SpringCloud Alibaba-OSS
由于很多服务都可能使用文件上传,因此直接在gulimall-common
中引入
--引入spring-alibaba-oss-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>
4.4.2. 配置阿里云oss 相关的账号信息
spring:
cloud:
alicloud:
oss:
endpoint: oss-cn-shanghai.aliyuncs.com
access-key: xxxxxx
secret-key: xxxxxx
注意:必须申请 RAM 账号信息,并且分配 OSS 操作权限
4.4.3 测试使用OssClient 上传
@Autowired
OSSClient ossClient;
@Test
public void testUpload() throws FileNotFoundException {
InputStream inputStream = new FileInputStream("C:\\Users\\Administrator\\Pictures\\removeAll.png");
ossClient.putObject("gulimall-kaisarh", "removeAll.png", inputStream);
ossClient.shutdown();
System.out.println("上传完成");
}
5. 创建微服务,整合第三方功能
5.1. 创建微服务gulimall-third-party
5.2. 修改依赖
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
将对象存储依赖放到gulimall-third-party
中
5.3. 将gulimall-third-parthy
添加到nacos中
5.3.1. 新建命名空间
新建配置oss.yml
完善配置
5.3.2. 配置gulimall-third-party
配置中心
spring.application.name=gulimall-third-party
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=e5b7c2f9-afd4-4750-94c7-f0be48b97fb8
spring.cloud.nacos.config.ext-config[0].data-id=oss.yml
spring.cloud.nacos.config.ext-config[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.ext-config[0].refresh=true
5.3.3. 配置gulimall-third-party
注册中心
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: gulimall-third-party
server:
port: 30000
开启服务注册发现
去除数据源mybatis依赖
<exclusions>
<exclusion>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</exclusion>
</exclusions>
5.4. 启动项目
启动项目后可以在Nacos
的服务列表查询
测试上传通过
6. 服务端签名后直传
6.1. 文档
6.2. 流程介绍
Web端向服务端请求签名,然后直接上传,不会对服务端产生压力,而且安全可靠。
6.3. 代码
-
在
gulimall-third-part
微服务中创建controller并添加@RestController
注解 -
编写获取签名接口
package com.atguigu.gulimall.thirdparty.controller; import com.aliyun.oss.OSS; import com.aliyun.oss.common.utils.BinaryUtil; import com.aliyun.oss.model.MatchMode; import com.aliyun.oss.model.PolicyConditions; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.Date; import java.util.LinkedHashMap; import java.util.Map; @RestController public class OssController { @Autowired OSS ossClient; @Value("${spring.cloud.alicloud.oss.endpoint}") private String endpoint; @Value("${spring.cloud.alicloud.oss.bucket}") private String bucket; @Value("${spring.cloud.alicloud.access-key}") private String accessId; @RequestMapping("/oss/policy") public Map<String, String> policy() { String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint // callbackUrl为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。 // String callbackUrl = "http://88.88.88.88:8888"; String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date()); String dir = format + "/"; // 用户上传文件时指定的前缀。 Map<String, String> respMap = null; try { long expireTime = 30; long expireEndTime = System.currentTimeMillis() + expireTime * 1000; Date expiration = new Date(expireEndTime); // PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。 PolicyConditions policyConds = new PolicyConditions(); policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000); policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir); String postPolicy = ossClient.generatePostPolicy(expiration, policyConds); byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8); String encodedPolicy = BinaryUtil.toBase64String(binaryData); String postSignature = ossClient.calculatePostSignature(postPolicy); respMap = new LinkedHashMap<String, String>(); respMap.put("accessid", accessId); respMap.put("policy", encodedPolicy); respMap.put("signature", postSignature); respMap.put("dir", dir); respMap.put("host", host); respMap.put("expire", String.valueOf(expireEndTime / 1000)); // respMap.put("expire", formatISO8601Date(expiration)); } catch (Exception e) { // Assert.fail(e.getMessage()); System.out.println(e.getMessage()); } finally { ossClient.shutdown(); } return respMap; } }
-
启动测试测试接口
- expire:过期时间
- dir:上传文件名
- host:上传文件地址
- policy:签名
6.4. 配置网关
- id: third_part_route
uri: lb://gulimall-third-party
predicates:
## 前端项目发送请求,带有/api前缀
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment}
测试:测试接口
7. OOS前后端联调上传功能
-
文件上传组件
el-upload
的action
属性对应Bucket
域名 -
更新后端接口,返回
R
对象R policy() { ··· return R.ok().put("data", respMap); }
-
配置OSS跨域
-
multiUpload.vue
<template> <div> <el-upload action="http://gulimall-kaisarh.oss-cn-beijing.aliyuncs.com" :data="dataObj" :list-type="listType" :file-list="fileList" :before-upload="beforeUpload" :on-remove="handleRemove" :on-success="handleUploadSuccess" :on-preview="handlePreview" :limit="maxCount" :on-exceed="handleExceed" :show-file-list="showFile" > <i class="el-icon-plus"></i> </el-upload> <el-dialog :visible.sync="dialogVisible"> <img width="100%" :src="dialogImageUrl" alt /> </el-dialog> </div> </template> <script> import { policy } from "./policy"; import { getUUID } from "@/utils"; export default { name: "multiUpload", props: { //图片属性数组 value: Array, //最大上传图片数量 maxCount: { type: Number, default: 30, }, listType: { type: String, default: "picture-card", }, showFile: { type: Boolean, default: true, }, }, data() { return { dataObj: { policy: "", signature: "", key: "", ossaccessKeyId: "", dir: "", host: "", uuid: "", }, dialogVisible: false, dialogImageUrl: null, }; }, computed: { fileList() { let fileList = []; for (let i = 0; i < this.value.length; i++) { fileList.push({ url: this.value[i] }); } return fileList; }, }, mounted() {}, methods: { emitInput(fileList) { let value = []; for (let i = 0; i < fileList.length; i++) { value.push(fileList[i].url); } this.$emit("input", value); }, handleRemove(file, fileList) { this.emitInput(fileList); }, handlePreview(file) { this.dialogVisible = true; this.dialogImageUrl = file.url; }, beforeUpload(file) { let _self = this; return new Promise((resolve, reject) => { policy() .then((response) => { console.log("这是什么${filename}"); _self.dataObj.policy = response.data.policy; _self.dataObj.signature = response.data.signature; _self.dataObj.ossaccessKeyId = response.data.accessid; _self.dataObj.key = response.data.dir + getUUID() + "_${filename}"; _self.dataObj.dir = response.data.dir; _self.dataObj.host = response.data.host; resolve(true); }) .catch((err) => { console.log("出错了...", err); reject(false); }); }); }, handleUploadSuccess(res, file) { this.fileList.push({ name: file.name, // url: this.dataObj.host + "/" + this.dataObj.dir + "/" + file.name; 替换${filename}为真正的文件名 url: this.dataObj.host + "/" + this.dataObj.key.replace("${filename}", file.name), }); this.emitInput(this.fileList); }, handleExceed(files, fileList) { this.$message({ message: "最多只能上传" + this.maxCount + "张图片", type: "warning", duration: 1000, }); }, }, }; </script> <style> </style>
-
policy.js
import http from '@/utils/httpRequest.js' export function policy() { return new Promise((resolve, reject) => { http({ url: http.adornUrl("/thirdparty/oss/policy"), method: "get", params: http.adornParams({}) }).then(({ data }) => { resolve(data); }) }); }
-
singleUpload.vue
<template> <div> <el-upload action="http://gulimall-kaisarh.oss-cn-beijing.aliyuncs.com" :data="dataObj" list-type="picture" :multiple="false" :show-file-list="showFileList" :file-list="fileList" :before-upload="beforeUpload" :on-remove="handleRemove" :on-success="handleUploadSuccess" :on-preview="handlePreview" > <el-button size="small" type="primary">点击上传</el-button> <div slot="tip" class="el-upload__tip"> 只能上传jpg/png文件,且不超过10MB </div> </el-upload> <el-dialog :visible.sync="dialogVisible"> <img width="100%" :src="fileList[0].url" alt="" /> </el-dialog> </div> </template> <script> import { policy } from "./policy"; import { getUUID } from "@/utils"; export default { name: "singleUpload", props: { value: String, }, computed: { imageUrl() { return this.value; }, imageName() { if (this.value != null && this.value !== "") { return this.value.substr(this.value.lastIndexOf("/") + 1); } else { return null; } }, fileList() { return [ { name: this.imageName, url: this.imageUrl, }, ]; }, showFileList: { get: function () { return ( this.value !== null && this.value !== "" && this.value !== undefined ); }, set: function (newValue) {}, }, }, data() { return { dataObj: { policy: "", signature: "", key: "", ossaccessKeyId: "", dir: "", host: "", // callback:'', }, dialogVisible: false, }; }, methods: { emitInput(val) { this.$emit("input", val); }, handleRemove(file, fileList) { this.emitInput(""); }, handlePreview(file) { this.dialogVisible = true; }, beforeUpload(file) { let _self = this; return new Promise((resolve, reject) => { policy() .then((response) => { console.log("响应的数据", response); _self.dataObj.policy = response.data.policy; _self.dataObj.signature = response.data.signature; _self.dataObj.ossaccessKeyId = response.data.accessid; _self.dataObj.key = response.data.dir + getUUID() + "_${filename}"; _self.dataObj.dir = response.data.dir; _self.dataObj.host = response.data.host; console.log("响应的数据222。。。", _self.dataObj); resolve(true); }) .catch((err) => { reject(false); }); }); }, handleUploadSuccess(res, file) { console.log("上传成功..."); this.showFileList = true; this.fileList.pop(); this.fileList.push({ name: file.name, url: this.dataObj.host + "/" + this.dataObj.key.replace("${filename}", file.name), }); this.emitInput(this.fileList[0].url); }, }, }; </script> <style> </style>
-
brand-add-or-update.vue
<template> ··· <el-form-item label="品牌logo地址" prop="logo"> <single-upload v-model="dataForm.logo"></single-upload> </el-form-item> ··· </template> <script> import singleUpload from "../../../components/upload/singleUpload.vue"; export default { components: { singleUpload }, ··· }; </script>
8. 品牌管理功能完善
8.1. 新增品牌界面
-
设置
switch
显示状态开关,激活为0,不激活为1<el-form-item label="显示状态" prop="showStatus"> <el-switch v-model="dataForm.showStatus" active-color="#13ce66" inactive-color="#ff4949" :active-value="1" :inactive-value="0" > </el-switch> </el-form-item>
-
新增数据的时候做校验
dataRule: { ··· firstLetter: [ { validator: (rule, value, callback) => { if (value === "") { callback(new Error("首字母必须填写!")); } else if (!^[a-zA-Z]$.test(value)) { callback(new Error("首字母必须a-z或A-Z!")); } else { callback(); } }, trigger: "blur", }, ], sort: [ { validator: (rule, value, callback) => { console.log(value); console.log(typeof value); if (value === "") { callback(new Error("排序字段必须填写!")); } else if (!Number.isInteger(value) || value < 0) { callback(new Error("排序字段必须是大于等于0的整数")); } else { callback(); } }, trigger: "blur", }, ], },
8.2. 品牌列表界面
-
设置品牌logo为图片
<template> ··· <el-table-column prop="logo" header-align="center" align="center" label="品牌logo" > <template slot-scope="scope"> <img :src="scope.row.logo" alt="logo" style="width: 100px; height: 80px" /> </template> </el-table-column> ··· </template>
9. JSR303服务端数据校验
-
给Bean添加校验注解
注解可以参考
javax\validation\constraints
-
使用注解
@Valid
告知SpringMVC进行校验,开启校验功能 -
Postman测试
效果:校验错误后会有默认响应
-
错误信息可以通过注解自定义
-
给校验的bean后紧跟一个
BindingResult
就可以获取到校验结果,可以自定义错误返回信息/** * 保存 */ @RequestMapping("/save") public R save(@Valid @RequestBody BrandEntity brand, BindingResult result) { if (result.hasErrors()) { Map<String, String> map = new HashMap<>(); // 1. 获取校验的错误结果 result.getFieldErrors().forEach(item -> { // FieldError 获取错误提示 String defaultMessage = item.getDefaultMessage(); // 获取错误属性名称 String field = item.getField(); map.put(field, defaultMessage); }); return R.error(400, "提交的数据不合法").put("data", map); } else { brandService.save(brand); return R.ok(); } }
-
给其他字段增加校验注解
package com.atguigu.gulimall.product.entity; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import java.io.Serializable; import java.util.Date; import lombok.Data; import org.hibernate.validator.constraints.URL; import javax.validation.constraints.*; /** * 品牌 * * @author KaiSarH * @email huankai7@163.com * @date 2021-04-15 16:20:25 */ @Data @TableName("pms_brand") public class BrandEntity implements Serializable { private static final long serialVersionUID = 1L; /** * 品牌id */ @TableId private Long brandId; /** * 品牌名 */ @NotBlank(message = "品牌名必须提交") private String name; /** * 品牌logo地址 */ @URL(message = "logo必须是一个合法的url地址") @NotEmpty private String logo; /** * 介绍 */ private String descript; /** * 显示状态[0-不显示;1-显示] */ private Integer showStatus; /** * 检索首字母 */ @Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母") private String firstLetter; /** * 排序 */ @Min(value = 0, message = "排序必须大于等于0") private Integer sort; }
10. 统一异常处理
很多业务都需要进行数据验证,代码大部分都是重复的,可以做统一异常处理
使用SpringMVC提供的@ControllerAdvice
使用步骤:
-
抽取异常处理类
使用
@ControllerAdvice
注解标识使用
basePackages
标识哪个位置出现异常进行处理 -
接口使用
BindingResult
会接收错误,感应异常。删除掉后就不再对异常进行处理,而是直接抛出异常。
GulimallExceptionControllerAdvice
的作用就是感应异常,集中处理。 -
错误信息以
JSON
格式返回,需要给类添加@ResponseBody
注解@ResponseBody
注解和@ControllerAdvice(basePackages
注解可以合并为@RestControllerAdvice
注解//@ResponseBody //@ControllerAdvice(basePackages = "com/atguigu/gulimall/product/controller") @RestControllerAdvice(basePackages = "com/atguigu/gulimall/product/controller")
-
统一处理
MethodArgumentNotValidException
异常package com.atguigu.gulimall.product.exception; import com.atguigu.common.utils.R; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.BindingResult; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.HashMap; import java.util.Map; /** * 集中处理所有异常 */ @Slf4j //@ResponseBody //@ControllerAdvice(basePackages = "com/atguigu/gulimall/product/controller") @RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller") public class GulimallExceptionControllerAdvice { // 准确匹配某种异常 @ExceptionHandler(value = MethodArgumentNotValidException.class) public R handleVaildException(MethodArgumentNotValidException e) { log.error("数据校验出现问题:{},异常类型:{}", e.getMessage(), e.getClass()); BindingResult bindingResult = e.getBindingResult(); Map<String, String> errorMap = new HashMap<>(); bindingResult.getFieldErrors().forEach(fieldError -> { errorMap.put(fieldError.getField(), fieldError.getDefaultMessage()); }); return R.error(400, "数据校验出现问题").put("data", errorMap); } // 无法准确匹配后处理 @ExceptionHandler(value = Throwable.class) public R handleException(Throwable throwable) { return R.error(); } }
11. 全局状态码枚举类
错误码和错误信息定义类
- 错误码定义规则为 5 为数字
- 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
- 维护错误码后需要维护错误描述,将他们定义为枚举形式
错误码列表:
-
10: 通用
001:参数格式校验
-
11: 商品
-
12: 订单
-
13: 购物车
-
14: 物流
-
状态码在很多地方都需要,因此在
gulimall-common
中定义枚举类BizCodeEnume
package com.atguigu.common.exception; /*** * 错误码和错误信息定义类 * 1. 错误码定义规则为 5 为数字 * 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常 * 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式 * 错误码列表: * 10: 通用 * 001:参数格式校验 * 11: 商品 * 12: 订单 * 13: 购物车 * 14: 物流 */ public enum BizCodeEnume { UNKNOW_EXCEPTION(10000, "系统未知异常"), VAILE_EXCEPTION(10001, "参数格式校验失败"); private int code; private String msg; BizCodeEnume(int code, String msg) { this.code = code; this.msg = msg; } public int getCode() { return code; } public String getMsg() { return msg; } }
-
定义完成后,在接口中不需要再额外写状态码
原来返回状态码方式:
return R.error(400, "数据校验出现问题").put("data", errorMap);
定义全局状态码枚举类后返回状态码方式:
return R.error(BizCodeEnume.VAILE_EXCEPTION.getCode(), BizCodeEnume.VAILE_EXCEPTION.getMsg()).put("data", errorMap);
12. JSR303分组校验
JSR303
分组校验可以完成多场景复杂校验。
在新增数据和修改数据的时候,校验的字段可能不同。
例如:
-
新增品牌的时候,由于ID是自动生成的自增长ID,所以新增的时候不携带ID
-
修改品牌的时候,需要根据ID进行修改
-
无论是新增还是修改,品牌名都不能为空
-
某些字段如logo在新增的时候需要录入必须提交,修改的时候可以不用必须提交
解决:使用JSR303分组校验功能
-
在
gulimall-common
中定义不同分组,例如新增分组和修改分组。 -
使用
groups
属性给校验注解标注什么情况下需要校验。groups
为情况数组,可以添加一个或多个。/** * * 品牌id */ @NotNull(message = "修改必须指定品牌id", groups = {UpdateGroup.class}) @Null(message = "新增不能指定id", groups = {AddGroup.class}) @TableId private Long brandId; /** * 品牌名 */ @NotBlank(message = "品牌名必须提交", groups = {UpdateGroup.class, AddGroup.class}) private String name;
-
在
Controller
中,使用@Validated
注解标识接口进行哪一组的校验/** * 保存 */ @RequestMapping("/save") public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand/*, BindingResult result*/) { brandService.save(brand); return R.ok(); } /** * 修改 */ @RequestMapping("/update") public R update(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brand) { brandService.updateById(brand); return R.ok(); }
-
注意
默认
没有指定分组
的校验注解,例如下图中@NotEmpty
在分组校验
@Validated
情况下不会生效,只会在不分组的情况下生效。
13. JSR303自定义校验
在一些特殊情况下,例如显示状态[0-不显示 1-显示],没有内置的校验注解使用,需要自己写校验方法。
- 使用
@Pattern
注解正则表达式 - 自定义校验
使用过程:
-
编写一个自定义的校验注解
-
编写一个自定义的校验器
-
关联自定义的校验器和自定义的校验注解
让校验器校验校验注解标识的字段
13.1. 编写一个自定义的校验注解
希望有一个注解@ListValue(values = {0, 1})
,用来规定字段可以使用的值(0和1).
-
创建
ListValue
校验注解 -
注解必须拥有三个属性
- message:当校验出错后,错误信息去哪取
- groups:支持分组校验
- payload:自定义负载信息
-
注解必须有以下原信息数据
@Documented @Constraint(validatedBy = {}) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME)
- Target:注解可以标注在哪些位置
- Retention:时机,可以在运行时获取到
- Constraint:注解使用哪个校验器进行校验,可以指定校验器
-
导入相关包
Payload、Constraint
依赖validation-api
,在pom.xml
导入依赖 -
基础注解
ListValue.java
package com.atguigu.common.valid; import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.*; @Documented @Constraint(validatedBy = {}) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) public @interface ListValue { String message() default "{javax.validation.constraints.NotEmpty.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
定义完成后,可以在
BrandEntity.java0
中引入使用 -
在
ListValue.java
中指定value
数组int[] values() default {};
-
指定错误信息
-
将
message
的默认值改为ListValue全类名.message
String message() default "{com.atguigu.common.valid.ListValue.message}";
-
创建配置文件
ValidationMessages.properties
-
在配置文件中配置错误信息
-
13.2. 编写一个自定义的校验器
-
创建校验器类文件
ListValueConstraintValidator.java
-
ListValueConstraintValidator
实现接口ConstraintValidator
ConstraintValidator
接口包含两个泛型,第一个为对应注解,第二个为校验数据类型 -
实现
ConstraintValidator
接口两个方法initialize
初始化方法 参数constraintAnnotation
包含默认合法的值isValid
校验方法 参数integer
为提交过来需要检验的值 -
校验器代码
package com.atguigu.common.valid; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; import java.util.HashSet; import java.util.Set; public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> { private Set<Integer> set = new HashSet<>(); // 初始化方法 @Override public void initialize(ListValue constraintAnnotation) { // 合法的值 int[] values = constraintAnnotation.values(); // 将合法值全部放到set中,便于查找是否存在 for (int value : values) { set.add(value); } } // 判断是否校验成功 @Override /* * * @params value 提交过来需要检验的值 * @params context 校验的上下文环境信息 * @return */ public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) { return set.contains(value); } }
-
注解与代码数据对应关系
注解使用的时候,会使用
values = {0, 1}
指定值@ListValue(values = {0, 1})
这些值对应的就是
initialize
初始化方法中的合法值。而
isValid
校验方法中的值,指的是客户端传递过来需要校验的值。
13.3. 关联校验器和校验注解
在校验注解位置,使用@Constraint
注解指定校验器
@Constraint(validatedBy = {ListValueConstraintValidator.class})
可以指定多个校验器,适配不同类型的校验
@Constraint(validatedBy = {A.class,B.class,C.class})
13.4. 完整校验注解代码与校验器
13.4.1. 校验注解
package com.atguigu.common.valid;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = {ListValueConstraintValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ListValue {
String message() default "{com.atguigu.common.valid.ListValue.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int[] values() default {};
}
13.4.2. 校验器
package com.atguigu.common.valid;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {
private Set<Integer> set = new HashSet<>();
// 初始化方法
@Override
public void initialize(ListValue constraintAnnotation) {
// 合法的值
int[] values = constraintAnnotation.values();
// 将合法值全部放到set中,便于查找是否存在
for (int value : values) {
set.add(value);
}
}
// 判断是否校验成功
@Override
/*
*
* @params value 提交过来需要检验的值
* @params context 校验的上下文环境信息
* @return
*/
public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
return set.contains(value);
}
}
13.4.3. 注解使用
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 显示状态[0-不显示;1-显示]
*/
@ListValue(values = {0, 1})
private Integer showStatus;
}
13.5. 测试
-
指定在添加的时候必须携带
/** * 显示状态[0-不显示;1-显示] */ @ListValue(values = {0, 1}, groups = {AddGroup.class}) private Integer showStatus;
-
save
测试-
错误测试
-
正确测试
-
14. 将修改品牌状态单独抽取一个方法
-
添加修改状态分组
-
新建接口,指定使用
UpdateStatusGroup
分组/** * 修改品牌显示状态 */ @RequestMapping("/update/status") public R updateStatus(@Validated({UpdateStatusGroup.class}) @RequestBody BrandEntity brand) { brandService.updateById(brand); return R.ok(); }
-
在
BrandEntity
中进行配置,在UpdateStatusGroup
组只判断showStatus
/** * 显示状态[0-不显示;1-显示] */ @NotNull(groups = {AddGroup.class, UpdateStatusGroup.class}) @ListValue(values = {0, 1}, groups = {AddGroup.class, UpdateStatusGroup.class}) private Integer showStatus;
-
修改前端项目更新状态请求
-
测试成功