二. 品牌管理
1. 前期工作
可以直接将代码生成器中生成的前端代码拷贝到前端页面, 从而实现简单的功能,
只需根据业务需求做相应的修改即可
1. 步骤
-
使用代码生成器生成相关代码
注意代码生成器所配置时制定的数据库
-
将代码生成器中生成的
brand.vue
和brand-and-or-update.vue
两个页面拷贝到项目中
2. 测试
-
由于生成的代码中有权限功能, 只有有权限的时候才会生成显示添加和删除等按钮, 所以我们现将权限获取全部设置为true, 后期再来验证权限
-
重新启动前端项目
-
简单验证
3. 页面优化
1. 快速显示开关
-
快速显示开关: 显示状态栏是通过0,1来展示是否显示, 我们需要通过按钮来显示
<el-table-column prop="showStatus" header-align="center" align="center" label="显示状态"> <template slot-scope="scope"> <!-- @change: 按钮发生变化时调用的方法 :active-value="1": 开关激活时的值 :inactive-value="0: 开关关闭时的值 --> <el-switch v-model="scope.row.showStatus" active-color="#13ce66" @change="updateBrandStatus(scope.row)" :active-value="1" :inactive-value="0" inactive-color="#ff4949"> </el-switch> </template> </el-table-column> <script> // 修改显示状态 updateBrandStatus(data){ console.log("最新信息: ", 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: '状态更新成功' }) }); } </script>
-
添加修改页面也需要将是否显示设置为按钮, 同时需要将品牌logo地址设置为文件上传
<el-form-item label="品牌logo地址" prop="logo"> <el-upload class="upload-demo" action="" > <el-button size="small" type="primary">点击上传</el-button> <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div> </el-upload> </el-form-item> ... <el-form-item label="显示状态" prop="showStatus"> <el-switch v-model="dataForm.showStatus" active-color="#13ce66" inactive-color="#ff4949"> </el-switch> </el-form-item>
2. 文件上传(OSS)
1. 说明
-
采用阿里云的OSS来存储文件
-
方式: 用户向应用服务器请求上传policy; 应用服务器返回上传policy; 用户直接上传数据到OSS
而不是采用用户将图片传给应用服务器, 然后再由应用服务器上传到OSS, 因为会增加应用服务器负担
2. 测试
-
引入依赖
<dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.10.2</version> </dependency>
-
测试代码
@Test public void ossTest() throws FileNotFoundException { // 配置自己的accessKey信息 String endpoint = "****************"; String accessKeyId = "****************"; String accessKeySecret = "****************"; // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); // 上传文件流 F:InputStream inputStream = new FileInputStream("F:\\测试.jpg"); // 填写Bucket名称和Object完整路径。Object完整路径中不能包含Bucket名称。 ossClient.putObject("gulimall-warehouse", "测试.jpg", inputStream); // 关闭OSSClient。 ossClient.shutdown(); System.out.println("上传成功"); }
-
说明: 引入上面的依赖需要直接创建操作类, 所以引入集成好的依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alicloud-oss</artifactId> </dependency>
-
在配置文件中配置相关信息
-
实现
@Autowired OSSClient ossClient; @Test public void ossTest2() throws FileNotFoundException { // 上传文件流 F:InputStream inputStream = new FileInputStream("F:\\测试.jpg"); ossClient.putObject("gulimall-warehouse", "测试.jpg", inputStream); // 关闭OSSClient。 ossClient.shutdown(); System.out.println("上传成功"); }
3. 第三方工具模块
创建一个第三方工具模块
gulimall-third-party
1. maven
-
相关依赖
<dependencies> <dependency> <groupId>com.hjf.gulimall</groupId> <artifactId>gulimall-common</artifactId> <version>0.0.1-SNAPSHOT</version> <!-- 不需要引入mybatis-plus相关的依赖 --> <exclusions> <exclusion> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- oss --> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alicloud-oss</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.1.0.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
2. 配置信息
-
bootstrap.properties
spring.application.name=gulimall-third-party spring.cloud.nacos.config.server-addr=127.0.0.1:8848 spring.cloud.nacos.config.namespace=4c9b6122-5d5d-4700-ade2-eaaf5dae222d spring.cloud.nacos.config.ext-config[0].data-id=oss.yml spring.cloud.nacos.config.ext-config[0].group=dev spring.cloud.nacos.config.ext-config[0].refresh=true
-
application.yml
spring: cloud: nacos: discovery: server-addr: 127.0.0.1:8848 alicloud: access-key: LTAI5t6TkqFChXhH9WaDeP2e secret-key: Zsf5Ud8zd7vn3LQs0D7k36UFdVaidp oss: endpoint: oss-cn-hangzhou.aliyuncs.com bucket: gulimall-warehouse application: name: gulimall-third-party server: port: 15000
3. controller层
-
代码
@RestController public class OssController { @Autowired OSS ossClient; @Value("${spring.cloud.alicloud.access-key}") private String accessId; @Value("${spring.cloud.alicloud.oss.endpoint}") private String endpoint; @Value("${spring.cloud.alicloud.oss.bucket}") private String bucket; @RequestMapping("/oss/policy") public Map<String, String> policy(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 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("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; } }
4. gateway模块
配置网关路由
-
配置信息
spring: cloud: gateway: routes: ... - id: third_party_route uri: lb://gulimall-third-party predicates: - Path=/api/thirdparty/** filters: - RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment} ...
5. 测试
4. 前后端联调
1. 组件导入
-
将文件夹中的
upload
目录拷贝到src/components
-
修改两个上传文件中的
action
地址
2. 跨域
-
添加和修改页面中引入组件
<el-form-item label="品牌logo地址" prop="logo"> <!-- <el-input v-model="dataForm.logo" placeholder="品牌logo地址"></el-input> --> <!--single-upload名称要和components中申明组件的名称相对于--> <single-upload v-model="dataForm.logo"></single-upload> </el-form-item> <script> // 导入组件 import SingleUpload from "@/components/upload/singleUpload" export default { // 申明有哪些组件 components: {SingleUpload}, </script>
-
跨域问题
此时上传文件时会出现跨域问题, 因为我们是通过客户端直接上传的
-
解决: 在阿里云oss中修改权限管理, 允许oss可以跨域访问
3. 表单校验
1. 步骤
- 给bean添加校验注解
- 开启校验功能:
@Valid
- 给校验的bean后紧跟一个BindingResult, 就可以获取到校验结果
- 分组校验
2. 简单校验
1. 前端校验
主要校验首字母和排序两个字段
-
代码
<el-form-item label="检索首字母" prop="firstLetter"> <el-input v-model="dataForm.firstLetter" placeholder="检索首字母"></el-input> </el-form-item> <el-form-item label="排序" prop="sort"> <el-input v-model.number="dataForm.sort" placeholder="排序"></el-input> </el-form-item> <script> // 校验规则 dataRule: { firstLetter: [ // rule: 校验规则, value: 输入的值, callback: 不满足规则时返回的方法 { validator: (rule, value, callback) => { if(value == "") { // 如果为空 callback(new Error("首字母必须填写")) } else if(!/^[a-zA-Z]$/.test(value)) { // 如果不是字母 callback(new Error("请输入正确字母")) } else { callback() } }, trigger: 'blur' } ], sort: [ { validator: (rule, value, callback) => { if(value == "") { // 如果为空 callback(new Error("排序字段必须填写")) } else if(!Number.isInteger(value) || value < 0) { // 如果不是整数 callback(new Error("排序字段必须是大于等于0的整数")) } else { callback() } }, trigger: 'blur' } ] } </script>
2. 后端校验
采用JSR303校验
-
实体内中添加注解: javax.validation.constraints, 并定义自己的message提示
-
controller 方法中开启校验功能, 如果不启用, 则不会生效
-
给校验的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) -> { // 获取到错误提示 String message = item.getDefaultMessage(); // 获取到错误的属性名称 String field = item.getField(); map.put(field, message); }); return R.error(400, "提交的数据不合法").put("data", map); } else { brandService.save(brand); } return R.ok(); }
3. 分组校验
-
创建分组接口, 里面不用填写任何内容, 只是起到区分的作用
-
修改实体类, 给校验注解标注什么情况下才需要验证; 对于没有指定分组的字段, 校验规则不会生效, 所以需要为每个字段添加分组
@Data @TableName("pms_brand") public class BrandEntity implements Serializable { private static final long serialVersionUID = 1L; // 品牌id @Null(message = "新增不能指定id", groups = {AddGroup.class}) @NotNull(message = "修改必须指定品牌id", groups = {UpdateGroup.class}) @TableId private Long brandId; // 品牌名 @NotBlank(message = "品牌名不能为空", groups = {AddGroup.class, UpdateGroup.class}) private String name; // 品牌logo地址 @NotBlank(groups = {AddGroup.class}) // 添加时不能为空, 但修改时可以, 相对于不改变 @URL(message = "logo必须是一个合法的url", groups = {AddGroup.class, UpdateGroup.class}) private String logo; // 介绍 @NotBlank(message = "描述不能为空", groups = {AddGroup.class, UpdateGroup.class}) private String descript; // 显示状态[0-不显示;1-显示] private Integer showStatus; // 检索首字母 @NotEmpty(groups = {AddGroup.class}) @Pattern(regexp = "^[a-zA-Z]$", message = "检索首字母必须是一个字母", groups = {AddGroup.class, UpdateGroup.class}) private String firstLetter; // 排序 @NotNull(message = "排序不能为空", groups = {AddGroup.class}) @Min(value = 0, message = "排序必须大于等于0") private Integer sort; }
-
在controller中指定方法的校验分组
4. 自定义校验
1. 步骤
- 编写一个自定义的校验注解
- 编写一个自定义的校验器
- 关联自定义的校验器和自定义的校验注解
2. 实现
-
引入依赖
<dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency>
-
编写自定义校验器
public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> { private Set<Integer> set = new HashSet<>(); // 初始化方法 @Override public void initialize(ListValue constraintAnnotation) { // 从注解上获取所有的值, 并将所获取的值放到set集合中 int[] vals = constraintAnnotation.vals(); for (int val: vals){ set.add(val); } } /** * 是否校验成功 * @param value 需要校验的值 * @param context 校验的上下文环境信息 * @return */ @Override public boolean isValid(Integer value, ConstraintValidatorContext context) { return set.contains(value); } }
-
编写自定义校验注解, 并将自定义校验器和自定义的校验注解关联
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE }) @Retention(RUNTIME) @Documented @Constraint(validatedBy = {ListValueConstraintValidator.class}) public @interface ListValue { // 可以创建配置文件ValidationMessages.properties来指定提示信息 // 但是我的idea读取配置文件中的中文信息会乱码, 所以就在注解中添加了 // com.hjf.gulimall.common.valid.ListValue.message=必须提交指定的值 (配置文件中的内容) // String message() default "{com.hjf.gulimall.common.valid.ListValue.message}"; String message() default ""; Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; int[] vals() default {}; }
-
在实体类中使用对于的注解
/** * 显示状态[0-不显示;1-显示] */ @ListValue(vals = {0, 1}, message = "必须提交指定的值", groups = {AddGroup.class}) private Integer showStatus;
5. bug修复
1. 原因分析
-
之前是将状态修改规则和表单修改方法合并在一起的, 修改状态时, 并不会传递表单中的必填项, 所以在表单校验时就会抛出异常
2. 解决:
将状态修改方法拆分出来, 然后再对状态修改方法重新写校验方法
-
新建一个检验分组
-
指定属性的验证分组
-
创建状态修改的方法
/** * 状态修改 */ @RequestMapping("/update/status") public R updateStatus(@Validated({UpdateStatusGroup.class}) @RequestBody BrandEntity brand){ brandService.updateById(brand); return R.ok(); }
-
修改前端状态修改时调用的方法
4. 统一的异常处理
1. 说明
为每个方法编写异常处理方法, 太过麻烦, 同时会使controller层泰国复杂, 所以我们使用统一异常处理
-
去掉方法中的异常处理方法, 并抛出异常
@RequestMapping("/save") // 不使用BindingResult参数时, 就会将异常抛出 public R save(@Valid @RequestBody BrandEntity brand /*, BindingResult result */){ brandService.save(brand); return R.ok(); }
2. 编写异常处理类
-
创建异常处理类, 集中处理各种异常
// 日志记录 @Slf4j //@ResponseBody 指定包下出现的异常都会处理 //@ControllerAdvice(basePackages = "com.hjf.gulimall.product.controller") @RestControllerAdvice(basePackages = "com.hjf.gulimall.product.controller") public class GulimallExceptionControllerAdvice { @ExceptionHandler(value = MethodArgumentNotValidException.class) public R handleValidException(MethodArgumentNotValidException e){ log.error("数据校验出现问题{}, 异常类型: {}", e.getMessage(), e.getClass()); BindingResult bindingResult = e.getBindingResult(); Map<String, String> errorMap = new HashMap<>(); bindingResult.getFieldErrors().forEach((item) -> { errorMap.put(item.getField(), item.getDefaultMessage()); }); return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(), BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data", errorMap); } @ExceptionHandler(value = Throwable.class) public R handleException(Throwable throwable){ return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(), BizCodeEnume.UNKNOW_EXCEPTION.getMsg()); } }
-
创建错误码枚举类
public enum BizCodeEnume { UNKNOW_EXCEPTION(10000, "系统未知异常"), VAILD_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; } }