目录
一、商品服务-API-品牌管理1、使用逆向工程的前后端代码2、效果优化及显示开关3、云存储开通与使用(1)阿里云对象存储oss(2)oss整合测试(3)SpringCloud Alibaba4、创建第三方模块(并完成添加上传功能)(1)创建模块`gulimall-third-party`(2)改进:服务端签名后直传(3) oss前端联调测试上传5、表单校验&自定义校验器(前端校验)6、JSR303数据校验(后端校验)(1)JSR303数据校验(2)统一异常处理(3)JSR303 分组检验(4)JSR303 自定义校验注解7、品牌分类关联与级联更新(1)引入mybatis分页插件(2)品牌模糊查询(3)关联分类商品服务-品牌管理
一、商品服务-API-品牌管理
1、使用逆向工程的前后端代码
新增品牌管理菜单把逆向生成的前端代码/produt/main/resources/src/views/modules/product
下的brand.vue
和brand-add-or-update.vue
复制到前端项目的gulimall-renren\gulimall-fast-vue\src\views\modules\product
目录中
显示的页面没有新增和删除功能,这是因为权限控制的原因
解决权限问题v-if="isAuth('product:brand:save')"
判断是否有权限,更改为true
<el-button v-if="isAuth('product:brand:save')" type="primary" @click="addOrUpdateHandle()">新增</el-button><el-button v-if="isAuth('product:brand:delete')" type="danger" @click="deleteHandle()" :disabled="dataListSelections.length <= 0">批量删除</el-button>/*** 是否有权限* @param {*} key*/export function isAuth (key) {// return JSON.parse(sessionStorage.getItem('permissions') || '[]').indexOf(key) !== -1 || falsereturn true}
2、效果优化及显示开关
解决控制台语法检查报错把build/webpack.base.conf.js
中的语法检查注释掉
const createLintingRule = () => ({// test: /\.(js|vue)$/,// loader: 'eslint-loader',// enforce: 'pre',// include: [resolve('src'), resolve('test')],// options: {// formatter: require('eslint-friendly-formatter'),// emitWarning: !config.dev.showEslintErrorsInOverlay// }})
快速显示开关
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" :active-value="1" :inactive-value="0" @change="updateBrandStatus(scope.row)"></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" :active-value="1" :inactive-value="0" @change="updateBrandStatus(scope.row)"></el-switch></el-form-item>
完成状态修改开关
// 1. 显示转态修改的方法updateBrandStatus(data) {console.log("最新信息", data)let {brandId, showStatus } = data;// 1. 发送请求修改状态this.$http({url: this.$http.adornUrl("/product/brand/update"),method: "post",data: this.$http.adornData({brandId, showStatus: showStatus }, false)}).then(({data }) => {this.$message({type: "success",message: "状态更新成功"})});},
3、云存储开通与使用
(1)阿里云对象存储oss
创建Bucket(作为项目)
这种方式是手动上传图片,实际上我们可以在程序中设置自动上传图片到阿里云
对象存储。
服务端签名后直传
上传的账号信息存储在应用服务器
上传先找应用服务器要一个policy上传策略,生成防伪签名
(2)oss整合测试
导入依赖<dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>3.8.0</version></dependency>
测试上传功能
开通子账户
添加访问控制
@Testpublic void testUpload() throws FileNotFoundException {String endpoint = "oss-cn-";// 云账号AccessKey有所有API访问权限String accessKeyId = "xxxxxx";String accessKeySecret = "xxxxxxx";// 创建OSSClient实例。com.aliyun.oss.OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);// 上传文件流。InputStream inputStream = new FileInputStream("C:\\Users\\文\\Desktop\\1.jpg");ossClient.putObject("gulimall-ljn", "1.jpg", inputStream);// 关闭OSSClient。ossClient.shutdown();System.out.println("上传成功.");}
(3)SpringCloud Alibaba
导入依赖<!-- 对象存储oss--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alicloud-oss</artifactId></dependency>
application.yml
中配置key
,secret
和endpoint
相关信息
spring:cloud:alicloud:access-key: xxxxsecret-key: xxxxxoss:endpoint: oss-cn-
测试
@AutowiredOSSClient ossClient;@Testpublic void testUpload() throws FileNotFoundException {// 上传文件流。InputStream inputStream = new FileInputStream("C:\\Users\\文\\Desktop\\1.jpg");ossClient.putObject("gulimall-ljn", "2.jpg", inputStream);// 关闭OSSClient。ossClient.shutdown();System.out.println("上传成功.");}
4、创建第三方模块(并完成添加上传功能)
(1)创建模块gulimall-third-party
导入依赖引入common
依赖管理 中添加spring-cloud-alibaba-dependencies
<dependencyManagement><!-- 对象存储oss--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alicloud-oss</artifactId></dependency><!-- 依赖common--><dependency><groupId>com.ljn.gulimall</groupId><artifactId>gulimall-commom</artifactId><version>0.0.1-SNAPSHOT</version></dependency><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></dependencies></dependencyManagement>
nacos相关配置 创建gulimall-third-party
命名空间添加oss.yml(对象存储配置文件)
spring:cloud:alicloud:access-key: xxxxxsecret-key: grbcDMCvxz0IR4r30DIrKU3ZGrfoZfoss:endpoint: oss-cn-
application.yml
server:port: 30000spring:application:name: gulimall-third-partycloud:nacos:discovery:server-addr: 127.0.0.1:8848logging:level:com.yxj.gulimall.product: debug
bootstrap.peoperties
spring.cloud.nacos.config.server-addr=127.0.0.1:8848spring.cloud.nacos.config.namespace=d9ce505a-86f2-41e4-afbe-c4d62785b3ea#加载nacos中的配置文件spring.cloud.nacos.config.ext-config[0].data-id=oss.ymlspring.cloud.nacos.config.ext-config[0].group=DEFAULT_GROUPspring.cloud.nacos.config.ext-config[0].refresh=true
oss测试
@AutowiredOSSClient ossClient;@Testpublic void testUpload() throws FileNotFoundException {// 上传文件流。InputStream inputStream = new FileInputStream("C:\\Users\\文\\Desktop\\1.jpg");ossClient.putObject("gulimall-ljn", "4.jpg", inputStream);// 关闭OSSClient。ossClient.shutdown();System.out.println("上传成功.");}
(2)改进:服务端签名后直传
采用JavaScript客户端直接签名(参见JavaScript客户端签名直传)时,AccessKeyID和AcessKeySecret会暴露在前端页面,因此存在严重的安全隐患。
因此,OSS提供了服务端签名后直传的方案。
服务端签名后直传的原理如下:
编写com.ljn.gulimall.thirdparty.controller.OssController
类:获取服务端签名
@RestControllerpublic class OssController {@AutowiredOSS ossClient;@Value("${spring.cloud.alicloud.oss.endpoint}")String endpoint;@Value("${spring.cloud.alicloud.oss.bucket}")String bucket;@Value("${spring.cloud.alicloud.access-key}")String accessId;@Value("${spring.cloud.alicloud.secret-key}")String accessKey;@RequestMapping("/oss/policy")public Map<String, String> policy() {// host的格式为 bucketname.endpointString host = "https://" + bucket + "." + endpoint;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);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));} catch (Exception e) {// Assert.fail(e.getMessage());System.out.println(e.getMessage());} finally {ossClient.shutdown();}return R.ok().put("data",respMap);}}
测试:http://localhost:30000/oss/policy 返回签名数据
{"accessid":"xxxxxx","policy":"eyJleHBpcmF0aW9uIjoiMjAyMS0wMi0xNFQxMDoyOToxMS43ODhaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCIyMDIxLTAyLTE0Il1dfQ==","signature":"0OXDXrQ1vRNl61N5IaZXRFckCKM=","dir":"-02-14","host":"https://gulimall-fermhan.oss-cn-","expire":"1613298551"}}
gate网关中配置路由规则
# third-party 服务路由- id: third_party_routeuri: lb://gulimall-gatewaypredicates:- Path=/api/thirdparty/**filters:- RewritePath=/api/thirdparty/(?<segment>/?.*),/$\{segment}
以后在上传文件时的访问路径为http://localhost:88/api/thirdparty/oss/policy
(3) oss前端联调测试上传
上传组件uploadpolicy.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-uploadaction="http://gulimall-ljn.oss-cn-":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) {console.log("123124214124")this.dialogVisible = true;},beforeUpload(file) {let _self = this;return new Promise((resolve, reject) => {policy().then(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;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>
multiUpload.vue
<template><div><el-upload action="http://gulimall-ljn.oss-cn-" :data="dataObj" list-type="picture-card" :file-list="fileList" :before-upload="beforeUpload" :on-remove="handleRemove" :on-success="handleUploadSuccess" :on-preview="handlePreview" :limit="maxCount" :on-exceed="handleExceed"><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}},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>
使用组件:brand-add-or-update.vue
中
<el-form-item label="品牌logo地址" prop="logo"><!-- 使用上传组件 --><single-upload v-model="dataForm.logo"></single-upload></el-form-item>// 导入上传组件import singleUpload from '../../../components/upload/singleUpload.vue';export default {components: {singleUpload },...
<template><div><el-upload action="http://gulimall-ljn.oss-cn-" :data="dataObj" list-type="picture-card" :file-list="fileList" :before-upload="beforeUpload" :on-remove="handleRemove" :on-success="handleUploadSuccess" :on-preview="handlePreview" :limit="maxCount" :on-exceed="handleExceed"><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}},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>
开始上传时的跨域问题:在oss中基础设置中设置即可
测试
5、表单校验&自定义校验器(前端校验)
修改brand.vue
,自定义显示logo<el-table-column prop="logo" header-align="center" align="center" label="品牌logo地址"><template slot-scope="scope"><img :src="scope.row.logo" style="width:100px ; height:80pxx" /></template></el-table-column>
在brand-add-or-update.vue
中自定义检验规则
// v-model.number<el-form-item label="排序" prop="sort"><el-input v-model.number="dataForm.sort" placeholder="排序"></el-input></el-form-item></el-form>firstLetter: [{validator: (rule, value, callback) => {if (value == "" && value != 0) {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) => {if (value == "") {callback(new Error("排序字段必须填写"));} else if (!Number.isInteger(parseInt(value)) || parseInt(value) < 0) {callback(new Error("排序字段必须是一个大于等于0的整数"));} else {callback();}}, trigger: "blur"}]
6、JSR303数据校验(后端校验)
(1)JSR303数据校验
product
服务的BrandEntity
中添加校验,注解并定义自己的message提示 参考import javax.validation.constraints
包下,如:@NotBlank(message = "品牌名不能为空")private String name;@NotEmpty@URL(message = "logo地址必须合法")private String logo;@Pattern(regexp = "/^[a-zA-Z]$/", message = "检索首字母必须是一个字母")private String firstLetter;@Min(value = 0,message = "排序必须大于等于0")private Integer sort;
controller 的方法中添加@Valid注解
开启校验 新版本springboot需要添加validation
启动器
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId><version>2.3.7.RELEASE</version></dependency>
如:
@RequestMapping("/save")public R save(@Valid @RequestBody BrandEntity brand){brandService.save(brand);return R.ok();}
自定义封装
给校验的Bean后,紧跟一个BindResult
,就可以获取到校验的结果。拿到校验的结果,就可以自定义的封装。
@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) -> {//获取发生错误时的messageString 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();}
这种是针对于该请求设置了一个内容校验,如果针对于每个请求都单独进行配置,显然不是太合适,实际上可以统一的对于异常进行处理。
(2)统一异常处理
统一异常处理@ControllerAdvice
可以使用SpringMvc所提供的@ControllerAdvice
,通过basePackages
能够说明处理哪些路径下的异常。
抽取一个异常处理类:com.ljn.gulimall.product.exception.GuliMallExceptionControllerAdvice
@Slf4j@RestControllerAdvice(basePackages = "com.ljn.gulimall.product.controller")public class GuliMallExceptionControllerAdvice {// 数据校验异常@ExceptionHandler(value = Exception.class)public R handleValidException(MethodArgumentNotValidException exception) {Map<String, String> map = new HashMap<>();// 1. 获取数据校验的错误结果BindingResult bindingResult = exception.getBindingResult();// 2. 遍历获取结果bindingResult.getFieldErrors().forEach(fieldError -> {String message = fieldError.getDefaultMessage();String field = fieldError.getField();map.put(field, message);});log.error("数据校验出现问题{},异常类型{}", exception.getMessage(), exception.getClass());return R.error(400, "数据校验出现问题").put("data", map);}//默认异常@ExceptionHandler(value = Throwable.class)public R handleException(Throwable throwable) {log.error("未知异常{},异常类型{}", throwable.getMessage(), throwable.getClass());return R.error(400, "数据校验出现问题");}}
错误状态码设置 为了定义这些错误状态码,我们可以单独定义一个常量类,用来存储这些错误状态码
public enum BizCodeEnume {UNKNOW_EXEPTION(10000, "系统未知异常"),VALID_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;}}
测试:
(3)JSR303 分组检验
@Validated
注解指定分组标注上groups
,指定什么情况下才需要进行校验默认情况下,在分组校验情况下,没有指定指定分组的校验注解,将不会生效,它只会在不分组的情况下生效。如:指定在更新和添加的时候,都需要进行校验。新增时不需要带id,修改时必须带id
@NotNull(message = "修改必须定制品牌id", groups = {UpdateGroup.class})@Null(message = "新增不能指定id", groups = {AddGroup.class})@TableIdprivate Long brandId;@RequestMapping("/save")public R save(@Validated(AddGroup.class) @RequestBody BrandEntity brand) {brandService.save(brand);return R.ok();}
(4)JSR303 自定义校验注解
编写自定义检验注解@Documented@Constraint(validatedBy = {})@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})@Retention(RetentionPolicy.RUNTIME)public @interface ListValue {// 使用该属性去Validation.properties中取String message() default "{mon.valid.ListValue.message}";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};int[] value() default {};}
common
创建文件ValidationMessages.properties
配置文件
mon.valid.ListValue.message=必须提交指定的值
编写自定义校验器ConstraintValidator
public class ListValueConstraintValidator implements ConstraintValidator<ListValue, Integer> {private Set<Integer> set = new HashSet<>();// 初始化@Overridepublic void initialize(ListValue constraintAnnotation) {int[] value = constraintAnnotation.value();for (int i : value) {set.add(i);}}// 判断是否校验成功@Overridepublic boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {return set.contains(value);}}
关联校验器和检验注解
@Constraint(validatedBy = {ListValueConstraintValidator.class})一个校验注解可以匹配多个校验器
7、品牌分类关联与级联更新
(1)引入mybatis分页插件
mp自带的分页是内存分页,性能低,所以需要手动写分页配置,使用物理分页
在prduct
服务下,新建config
文件夹,MyBatisConfig
类
@Configuration@EnableTransactionManagement@MapperScan("com.ljn.gulimall.product.dao")public class MyBatisConfig {// 引入分页插件@Beanpublic PaginationInterceptor paginationInterceptor() {PaginationInterceptor paginationInterceptor = new PaginationInterceptor();//设置请求的页面大于最大页后操作,true调回到首页,false继续请求paginationInterceptor.setOverflow(true);// 设置最大单页限制paginationInterceptor.setLimit(1000);return paginationInterceptor;}}
(2)品牌模糊查询
修改com.ljn.gulimall.product.service.impl.BrandServiceImpl
@Overridepublic PageUtils queryPage(Map<String, Object> params) {// 1、获取keyString key = (String) params.get("key");QueryWrapper<BrandEntity> queryWrapper = new QueryWrapper<>();if (!StringUtils.isEmpty(key)) {queryWrapper.eq("brand_id", key).or().like("name", key);}IPage<BrandEntity> page = this.page(new Query<BrandEntity>().getPage(params),queryWrapper);return new PageUtils(page);}
(3)关联分类
新增的华为、小米、oppo都应该是手机下的品牌,但是品牌对分类
可能是一对多
的,比如小米对应手机和电视多对多
的关系应该有relation
表 ,如pms_category_brand_relation
品牌关联分类表 获取品牌关联分类,修改com.ljn.gulimall.product.controller.CategoryBrandRelationController
/*** 获取当前品牌关联的所有分类列表*/@GetMapping("/catelog/list")public R catelogList(@RequestParam("brandId") Long brandId) {List<CategoryBrandRelationEntity> data = categoryBrandRelationService.list(new QueryWrapper<CategoryBrandRelationEntity>().eq("brandId", brandId));return R.ok().put("data", data);}
新增品牌与分类关联
分类名本可以在brand表中,但因为关联查询对数据库性能有影响,在电商中大表数据从不做关联,哪怕分步查也不用关联.
所以像name这种冗余字段可以保存,优化save,保存时用关联表存好,但select时不用关联
/*** 保存*/@RequestMapping("/save")public R save(@RequestBody CategoryBrandRelationEntity categoryBrandRelation) {// 自定义savacategoryBrandRelationService.saveDetail(categoryBrandRelation);return R.ok();}
com.ljn.gulimall.product.service.impl.CategoryBrandRelationServiceImpl
@AutowiredBrandDao brandDao;@AutowiredCategoryDao categoryDao;// 保存详细信息@Overridepublic void saveDetail(CategoryBrandRelationEntity categoryBrandRelation) {// 1.获取idLong brandId = categoryBrandRelation.getBrandId();Long catelogId = categoryBrandRelation.getCatelogId();// 2.根据id获取详情BrandEntity brandEntity = brandDao.selectById(brandId);CategoryEntity categoryEntity = categoryDao.selectById(catelogId);// 3.将名字设置到关联关系categoryBrandRelation.setBrandName(brandEntity.getName());categoryBrandRelation.setCatelogName(categoryEntity.getName());// 4.保存完整信息this.save(categoryBrandRelation);}
优化:保持冗余字段的数据一致
如果分类表里的name发送变化,那么品牌表里的分类name字段应该同步变化。
所以应该修改brandController
,使之update时检测分类表里的name进行同步
/*** 修改*/@RequestMapping("/update")public R update(@Validated(UpdateGruop.class) @RequestBody BrandEntity brand) {// 修改为更新细节brandService.updateDetail(brand);return R.ok();}
BrandServiceImpl
@AutowiredCategoryBrandRelationService categoryBrandRelationService;@Overridepublic void updateDetail(BrandEntity brand) {// 保证冗余字段的数据一致// 1. 更新自己表中的数据this.updateById(brand);// 2.同步更新其他关联表中的数据if(!StringUtils.isEmpty(brand.getName())){categoryBrandRelationService.updateBrand(brand.getBrandId(),brand.getName());//TODO 更新其他关联}}
CategoryBrandRelationServiceImpl
@Overridepublic void updateBrand(Long brandId, String name) {CategoryBrandRelationEntity relationEntity = new CategoryBrandRelationEntity();relationEntity.setBrandId(brandId);relationEntity.setBrandName(name);this.update(relationEntity, new QueryWrapper<CategoryBrandRelationEntity>().eq("brand_id", brandId));}
同步分类(用第二种方式)
CategoryBrandRelationServiceImpl
@Overridepublic void updateCategory(Long catId, String name) {// 这里使用baseMapperthis.baseMapper.updateCategory(catId, name);}
CategoryBrandRelationDao
@Mapperpublic interface CategoryBrandRelationDao extends BaseMapper<CategoryBrandRelationEntity> {void updateCategory(@Param("catId") Long catId, @Param("name") String name);}
CategoryBrandRelationDao.xml
<update id="updateCategory">update pms_category_brand_relation set catelog_name=#{name} where catelog_id=#{catId}</update>