100字范文,内容丰富有趣,生活中的好帮手!
100字范文 > Day400401402403404405406.商品服务 -谷粒商城

Day400401402403404405406.商品服务 -谷粒商城

时间:2020-11-15 02:19:47

相关推荐

Day400401402403404405406.商品服务 -谷粒商城

商品服务

一、品牌管理

1、效果优化与快速显示开关

将逆向工程product得到的resources\src\views\modules\product文件拷贝到achangmall/renren-fast-vue/src/views/modules/product目录下,也就是下面的两个文件

brand.vue : 显示的表单

brand-add-or-update.vue:添加和更改功能

但是显示的页面没有新增和删除功能,这是因为权限控制的原因

<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>

查看“isAuth”的定义位置:

它是在“index.js”中定义,暂时将它设置为返回值为true,即可显示添加和删除功能。 再次刷新页面能够看到,按钮已经出现了:

进行添加 测试成功

进行修改 也会自动回显 build/webpack.base.conf.js 中注释掉createLintingRule()函数体,不进行lint语法检brand.vue

<template><div class="mod-config"><el-form:inline="true":model="dataForm"@keyup.enter.native="getDataList()"><el-form-item><el-inputv-model="dataForm.key"placeholder="参数名"clearable></el-input></el-form-item><el-form-item><el-button @click="getDataList()">查询</el-button><el-buttonv-if="isAuth('product:brand:save')"type="primary"@click="addOrUpdateHandle()">新增</el-button><el-buttonv-if="isAuth('product:brand:delete')"type="danger"@click="deleteHandle()":disabled="dataListSelections.length <= 0">批量删除</el-button></el-form-item></el-form><el-table:data="dataList"borderv-loading="dataListLoading"@selection-change="selectionChangeHandle"style="width: 100%"><el-table-columntype="selection"header-align="center"align="center"width="50"></el-table-column><el-table-columnprop="brandId"header-align="center"align="center"label="品牌id"></el-table-column><el-table-columnprop="name"header-align="center"align="center"label="品牌名"></el-table-column><el-table-columnprop="logo"header-align="center"align="center"label="品牌logo地址"></el-table-column><el-table-columnprop="descript"header-align="center"align="center"label="介绍"></el-table-column><el-table-columnprop="showStatus"header-align="center"align="center"label="显示状态"><template slot-scope="scope"><el-switchv-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><el-table-columnprop="firstLetter"header-align="center"align="center"label="检索首字母"></el-table-column><el-table-columnprop="sort"header-align="center"align="center"label="排序"></el-table-column><el-table-columnfixed="right"header-align="center"align="center"width="150"label="操作"><template slot-scope="scope"><el-buttontype="text"size="small"@click="addOrUpdateHandle(scope.row.brandId)">修改</el-button><el-buttontype="text"size="small"@click="deleteHandle(scope.row.brandId)">删除</el-button></template></el-table-column></el-table><el-pagination@size-change="sizeChangeHandle"@current-change="currentChangeHandle":current-page="pageIndex":page-sizes="[10, 20, 50, 100]":page-size="pageSize":total="totalPage"layout="total, sizes, prev, pager, next, jumper"></el-pagination><!-- 弹窗, 新增 / 修改 --><add-or-updatev-if="addOrUpdateVisible"ref="addOrUpdate"@refreshDataList="getDataList"></add-or-update></div></template><script>import AddOrUpdate from "./brand-add-or-update";export default {data() {return {dataForm: {key: "",},dataList: [],pageIndex: 1,pageSize: 10,totalPage: 0,dataListLoading: false,dataListSelections: [],addOrUpdateVisible: false,};},components: {AddOrUpdate,},activated() {this.getDataList();},methods: {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: showStatus }, false),}).then(({data }) => {this.$message({type: "success",message: "状态更新成功",});});},// 获取数据列表getDataList() {this.dataListLoading = true;this.$http({url: this.$http.adornUrl("/product/brand/list"),method: "get",params: this.$http.adornParams({page: this.pageIndex,limit: this.pageSize,key: this.dataForm.key,}),}).then(({data }) => {if (data && data.code === 0) {this.dataList = data.page.list;this.totalPage = data.page.totalCount;} else {this.dataList = [];this.totalPage = 0;}this.dataListLoading = false;});},// 每页数sizeChangeHandle(val) {this.pageSize = val;this.pageIndex = 1;this.getDataList();},// 当前页currentChangeHandle(val) {this.pageIndex = val;this.getDataList();},// 多选selectionChangeHandle(val) {this.dataListSelections = val;},// 新增 / 修改addOrUpdateHandle(id) {this.addOrUpdateVisible = true;this.$nextTick(() => {this.$refs.addOrUpdate.init(id);});},// 删除deleteHandle(id) {var ids = id? [id]: this.dataListSelections.map((item) => {return item.brandId;});this.$confirm(`确定对[id=${ids.join(",")}]进行[${id ? "删除" : "批量删除"}]操作?`,"提示",{confirmButtonText: "确定",cancelButtonText: "取消",type: "warning",}).then(() => {this.$http({url: this.$http.adornUrl("/product/brand/delete"),method: "post",data: this.$http.adornData(ids, false),}).then(({data }) => {if (data && data.code === 0) {this.$message({message: "操作成功",type: "success",duration: 1500,onClose: () => {this.getDataList();},});} else {this.$message.error(data.msg);}});});},},};</script>

brand-add-or-update.vue

<template><el-dialog:title="!dataForm.brandId ? '新增' : '修改'":close-on-click-modal="false":visible.sync="visible"><el-form:model="dataForm":rules="dataRule"ref="dataForm"@keyup.enter.native="dataFormSubmit()"label-width="80px"><el-form-item label="品牌名" prop="name"><el-input v-model="dataForm.name" placeholder="品牌名"></el-input></el-form-item><el-form-item label="品牌logo地址" prop="logo"><el-input v-model="dataForm.logo" placeholder="品牌logo地址"></el-input></el-form-item><el-form-item label="介绍" prop="descript"><el-input v-model="dataForm.descript" placeholder="介绍"></el-input></el-form-item><el-form-item label="显示状态" prop="showStatus"><el-switchv-model="dataForm.showStatus"active-color="#13ce66"inactive-color="#ff4949"></el-switch></el-form-item><el-form-item label="检索首字母" prop="firstLetter"><el-inputv-model="dataForm.firstLetter"placeholder="检索首字母"></el-input></el-form-item><el-form-item label="排序" prop="sort"><el-input v-model="dataForm.sort" placeholder="排序"></el-input></el-form-item></el-form><span slot="footer" class="dialog-footer"><el-button @click="visible = false">取消</el-button><el-button type="primary" @click="dataFormSubmit()">确定</el-button></span></el-dialog></template><script>export default {data() {return {visible: false,dataForm: {brandId: 0,name: "",logo: "",descript: "",showStatus: "",firstLetter: "",sort: "",},dataRule: {name: [{required: true, message: "品牌名不能为空", trigger: "blur" }],logo: [{required: true, message: "品牌logo地址不能为空", trigger: "blur" },],descript: [{required: true, message: "介绍不能为空", trigger: "blur" },],showStatus: [{required: true,message: "显示状态[0-不显示;1-显示]不能为空",trigger: "blur",},],firstLetter: [{required: true, message: "检索首字母不能为空", trigger: "blur" },],sort: [{required: true, message: "排序不能为空", trigger: "blur" }],},};},methods: {init(id) {this.dataForm.brandId = id || 0;this.visible = true;this.$nextTick(() => {this.$refs["dataForm"].resetFields();if (this.dataForm.brandId) {this.$http({url: this.$http.adornUrl(`/product/brand/info/${this.dataForm.brandId}`),method: "get",params: this.$http.adornParams(),}).then(({data }) => {if (data && data.code === 0) {this.dataForm.name = data.brand.name;this.dataForm.logo = data.brand.logo;this.dataForm.descript = data.brand.descript;this.dataForm.showStatus = data.brand.showStatus;this.dataForm.firstLetter = data.brand.firstLetter;this.dataForm.sort = data.brand.sort;}});}});},// 表单提交dataFormSubmit() {this.$refs["dataForm"].validate((valid) => {if (valid) {this.$http({url: this.$http.adornUrl(`/product/brand/${!this.dataForm.brandId ? "save" : "update"}`),method: "post",data: this.$http.adornData({brandId: this.dataForm.brandId || undefined,name: this.dataForm.name,logo: this.dataForm.logo,descript: this.dataForm.descript,showStatus: this.dataForm.showStatus,firstLetter: this.dataForm.firstLetter,sort: this.dataForm.sort,}),}).then(({data }) => {if (data && data.code === 0) {this.$message({message: "操作成功",type: "success",duration: 1500,onClose: () => {this.visible = false;this.$emit("refreshDataList");},});} else {this.$message.error(data.msg);}});}});},},};</script>

2、添加上传

这里我们选择将图片放置到阿里云上,使用对象存储。 阿里云上使使用对象存储方式:

创建Bucket(作为项目) 上传文件:上传成功后,取得图片的URL 这种方式是手动上传图片,实际上我们可以在程序中设置自动上传图片到阿里云对象存储。

上传的账号信息存储在应用服务器 上传先找应用服务器要一个policy上传策略,生成防伪签名

使用代码上传 查看阿里云关于文件上传的帮助:

/document_detail/3.html?spm=a2c4g.11186623.6.768.549d59aaWuZMGJ

achangmall-product/pom.xml中添加依赖包

<dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId><version>3.10.2</version></dependency>

上传文件流

使用文件上传,您可以将本地文件上传到OSS文件。

以下代码用于将本地文件examplefile.txt上传到目标存储空间examplebucket中exampledir目录下的exampleobject.txt文件。

// yourEndpoint填写Bucket所在地域对应的Endpoint。以华东1(杭州)为例,Endpoint填写为https://oss-cn-。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名称(例如examplebucket)、Object完整路径(例如exampledir/exampleobject.txt)和本地文件的完整路径。Object完整路径中不能包含Bucket名称。// 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。PutObjectRequest putObjectRequest = new PutObjectRequest("examplebucket", "exampledir/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的取值:

点击概览就可以看到你的endpoint信息,endpoint在这里就是上海等地区,如 oss-cn-

bucket域名:

就是签名加上bucket,如achangmall0.oss-cn-

accessKeyId和accessKeySecret需要创建一个RAM账号:

选上编程访问 创建用户完毕后,会得到一个“AccessKey ID”和“AccessKeySecret”,

然后复制这两个值到代码的“AccessKey ID”和“AccessKeySecret”。

另外还需要添加访问控制权限:

@Testvoid test0() throws FileNotFoundException {// Endpoint以杭州为例,其它Region请按实际情况填写。String endpoint = "oss-cn-";// 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram. 创建。String accessKeyId = "你的accessKeyId";String accessKeySecret = "你的accessKeySecret";// 创建OSSClient实例。OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);// 上传文件流。InputStream inputStream = new FileInputStream("C:\\Users\\PePe\\Pictures\\Camera Roll\\321.png");ossClient.putObject("achangmall0", "321.png", inputStream);// 关闭OSSClient。ossClient.shutdown();System.out.println("上传成功.");}

更为简单的使用方式,是使用SpringCloud Alibaba

/alibaba/aliyun-spring-boot/blob/master/aliyun-spring-boot-samples/aliyun-oss-spring-boot-sample/README-zh.md

achangmall-common/pom.xml引入依赖

具体的可以在maven中央仓库查找

<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alicloud-oss</artifactId><version>2.1.1.RELEASE</version></dependency>

在配置文件中配置 OSS 服务对应的 accessKey、secretKey 和 endpoint。

alicloud:access-key: xxxsecret-key: xxxoss:endpoint: oss-cn-

注入OSSClient并进行文件上传下载等操作

@RunWith(SpringRunner.class)@SpringBootTestpublic class OssTest {@Resourceprivate OSSClient ossClient;@Testvoid test1() throws FileNotFoundException {// 上传文件流。InputStream inputStream = new FileInputStream("C:\\Users\\PePe\\Pictures\\Camera Roll\\321.png");ossClient.putObject("achangmall0", "321.png", inputStream);// 关闭OSSClient。ossClient.shutdown();System.out.println("上传完成...");}}

但是这样来做还是比较麻烦,如果以后的上传任务都交给achangmall-product来完成,显然耦合度高。最好单独新建一个Module来完成文件上传任务。

创建第三方模块添加依赖,将原来achangmall-common中的“spring-cloud-starter-alicloud-oss”依赖移动到该项目中

<dependencies><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alicloud-oss</artifactId><version>2.1.1.RELEASE</version></dependency><dependency><groupId>com.achang.achangmall</groupId><artifactId>achangmall-common</artifactId><version>0.0.1-SNAPSHOT</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency></dependencies>

主启动类@EnableDiscoveryClient // 在主启动类中开启服务的注册和发现

在nacos中注册 在nacos创建命名空间“ achangmall-third-party ”

在“ achangmall-third-party”命名空间中,创建“ achangmall-third-service.yml”文件 编写配置文件 application.yml

server:port: 30000spring:application:name: achangmall-third-servicecloud:nacos:discovery:server-addr: 127.0.0.1:8848

bootstrap.properties

spring.cloud.nacos.config.name=achangmall-third-servicespring.cloud.nacos.config.server-addr=127.0.0.1:8848spring.cloud.nacos.config.namespace=ad0431b9-a77f-4220-bf61-b48c7e117250spring.cloud.nacos.config.extension-configs[0].data-id=achangmall-third-service.ymlspring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUPspring.cloud.nacos.config.extension-configs[0].refresh=true

编写测试类

@SpringBootTest@RunWith(SpringRunner.class)class AchangmallThirdServiceApplicationTests {@ResourceOSSClient ossClient;@Testvoid contextLoads() throws FileNotFoundException {//上传文件流。InputStream inputStream = new FileInputStream("C:\\Users\\PePe\\Pictures\\Camera Roll\\123.jpg");ossClient.putObject("achangmall0", "333.jpg", inputStream);// 关闭OSSClient。ossClient.shutdown();System.out.println("上传成功.");}}

改进:服务端签名后直传

采用JavaScript客户端直接签名(参见JavaScript客户端签名直传)时,AccessKeyID和AcessKeySecret会暴露在前端页面,因此存在严重的安全隐患。

因此,OSS提供了服务端签名后直传的方案。

向服务器获取到签名,再去请求oss服务器 服务端签名后直传的原理如下:

用户发送上传Policy请求到应用服务器。 应用服务器返回上传Policy和签名给用户。

用户直接上传数据到OSS。

在com.achang.achangmall.controller.OssController编写controller

/******@author 阿昌@create -09-25 14:32********/@RestController@RequestMapping("third-service/oss")public class OssController {@Resourceprivate OSSClient ossClient;@Value("${spring.cloud.alicloud.oss.endpoint}")public String endpoint;@Value("${spring.cloud.alicloud.oss.bucket}")public String bucket;@Value("${spring.cloud.alicloud.access-key}")public String accessId;private final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");@GetMapping("/policy")public Map<String, String> getPolicy(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 dir = format.format(new Date())+"/"; // 用户上传文件时指定的前缀。以日期格式存储// 创建OSSClient实例。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;}}

测试请求,http://localhost:30000/third-service/oss/policy,成功获取

然后,我们通过gateway网关代理转发,在上传文件时的访问路径为“ http://localhost:88/api/third/oss/policy”,

配置网关

- id: oss_routeuri: lb://achangmall-third-servicepredicates:- Path=/api/third-service/**filters:- RewritePath=/api/third-service/(?<segment>.*),/$\{segment}

访问http://localhost:88/api/third-service/third-service/oss/policy,测试是否可以转发到我们的接口,如下成功访问

上传组件

放置项目提供的upload文件夹到components/目录下,一个是单文件上传,另外一个是多文件上传

policy.js封装一个Promise,发送/thirdparty/oss/policy请求。vue项目会自动加上api前缀

multiUpload.vue多文件上传。要改,改方式如下

singleUpload.vue单文件上传。

要替换里面的action中的内容action=“http://achangmall0.oss-cn-”,你的阿里云指定的bucket域名

singleUpload.vue代码

<template><div><el-uploadaction="http://achangmall0.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) {this.dialogVisible = true;},beforeUpload(file) {let _self = this;return new Promise((resolve, reject) => {policy().then((response) => {console.log("响应的数据", response);_self.dataObj.policy = response.policy;_self.dataObj.signature = response.signature;_self.dataObj.ossaccessKeyId = response.accessid;_self.dataObj.key = response.dir + getUUID() + "_${filename}";_self.dataObj.dir = response.dir;_self.dataObj.host = response.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>

multiUpload.vue代码

<template><div><el-uploadaction="http://achangmall0.oss-cn-":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代码

/third-service/third-service/oss/policy为你88网关代理的oss服务路由uri地址

import http from '@/utils/httpRequest.js'export function policy() {return new Promise((resolve, reject) => {http({#修改你88网关代理的oss服务路由uri地址url: http.adornUrl("/third-service/third-service/oss/policy"),method: "get",params: http.adornParams({})}).then(({data }) => {resolve(data);})});}

我们在后端准备好了签名controller,那么前端是在哪里获取的呢

而文件上传前调用的方法::before-upload=“beforeUpload”发现该方法返回了一个new Promise,调用了policy(),该方法是policy.js中的 import { policy } from “./policy”;

在vue中看是response.data.policy,在控制台看response.policy。所以去java里面改返回值为R。return R.ok().put(“data”,respMap);也可以像上面,阿昌这样子直接修改前端代码,选择一个即可

阿里云开启跨域

开始执行上传,但是在上传过程中,出现了跨域请求问题:

这又是一个跨域的问题,解决方法就是在阿里云上开启跨域访问:

配置oss跨域

再次执行文件上传。 注意上传时他的key变成了response.dir +getUUID()+"_${filename}";

3、表单校验&自定义校验器

修改brand-add-or-update如下: :active-value=“1” :inactive-value=“0” # 激活为1,不激活为0

<el-switchv-model="dataForm.showStatus"active-color="#13ce66"inactive-color="#ff4949":active-value="1":inactive-value="0"></el-switch>

添加表单校验&自定义校验器

<script>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) => {if (value == "") {callback(new Error("排序字段必须填写"));} else if (!Number.isInteger(parseInt(value)) || parseInt(value) < 0){callback(new Error("排序字段必须是一个整数"));} else {callback();}}, trigger: "blur" }]</script>

完整brand-add-or-update修改代码

<template><el-dialog:title="!dataForm.brandId ? '新增' : '修改'":close-on-click-modal="false":visible.sync="visible"><el-form:model="dataForm":rules="dataRule"ref="dataForm"@keyup.enter.native="dataFormSubmit()"label-width="80px"><el-form-item label="品牌名" prop="name"><el-input v-model="dataForm.name" placeholder="品牌名"></el-input></el-form-item><el-form-item label="品牌logo地址" prop="logo"><!-- <el-input v-model="dataForm.logo" placeholder="品牌logo地址"></el-input> --><singleUpload v-model="dataForm.logo"></singleUpload></el-form-item><el-form-item label="介绍" prop="descript"><el-input v-model="dataForm.descript" placeholder="介绍"></el-input></el-form-item><el-form-item label="显示状态" prop="showStatus"><el-switchv-model="dataForm.showStatus"active-color="#13ce66"inactive-color="#ff4949":active-value="1":inactive-value="0"></el-switch></el-form-item><el-form-item label="检索首字母" prop="firstLetter"><el-inputv-model="dataForm.firstLetter"placeholder="检索首字母"></el-input></el-form-item><el-form-item label="排序" prop="sort"><el-input v-model="dataForm.sort" placeholder="排序"></el-input></el-form-item></el-form><span slot="footer" class="dialog-footer"><el-button @click="visible = false">取消</el-button><el-button type="primary" @click="dataFormSubmit()">确定</el-button></span></el-dialog></template><script>import singleUpload from "@/components/upload/singleUpload";export default {components: {singleUpload: singleUpload,},data() {return {visible: false,dataForm: {brandId: 0,name: "",logo: "",descript: "",showStatus: "",firstLetter: "",sort: "",},dataRule: {name: [{required: true, message: "品牌名不能为空", trigger: "blur" }],logo: [{required: true, message: "品牌logo地址不能为空", trigger: "blur" },],descript: [{required: true, message: "介绍不能为空", trigger: "blur" },],showStatus: [{required: true,message: "显示状态[0-不显示;1-显示]不能为空",trigger: "blur",},],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) => {if (value == "") {callback(new Error("排序字段必须填写"));} else if (!Number.isInteger(parseInt(value)) ||parseInt(value) < 0) {callback(new Error("排序字段必须是一个整数"));} else {callback();}},trigger: "blur",},],},};},methods: {init(id) {this.dataForm.brandId = id || 0;this.visible = true;this.$nextTick(() => {this.$refs["dataForm"].resetFields();if (this.dataForm.brandId) {this.$http({url: this.$http.adornUrl(`/product/brand/info/${this.dataForm.brandId}`),method: "get",params: this.$http.adornParams(),}).then(({data }) => {if (data && data.code === 0) {this.dataForm.name = data.brand.name;this.dataForm.logo = data.brand.logo;this.dataForm.descript = data.brand.descript;this.dataForm.showStatus = data.brand.showStatus;this.dataForm.firstLetter = data.brand.firstLetter;this.dataForm.sort = data.brand.sort;}});}});},// 表单提交dataFormSubmit() {this.$refs["dataForm"].validate((valid) => {if (valid) {this.$http({url: this.$http.adornUrl(`/product/brand/${!this.dataForm.brandId ? "save" : "update"}`),method: "post",data: this.$http.adornData({brandId: this.dataForm.brandId || undefined,name: this.dataForm.name,logo: this.dataForm.logo,descript: this.dataForm.descript,showStatus: this.dataForm.showStatus,firstLetter: this.dataForm.firstLetter,sort: this.dataForm.sort,}),}).then(({data }) => {if (data && data.code === 0) {this.$message({message: "操作成功",type: "success",duration: 1500,onClose: () => {this.visible = false;this.$emit("refreshDataList");},});} else {this.$message.error(data.msg);}});}});},},};</script>

4、JSR303数据校验

问题引入

填写form时应该有前端校验,后端也应该有校验 前端 前端的校验是element-ui表单验证 Form 组件提供了表单验证的功能,只需要通过 rules 属性传入约定的验证规则,并将 Form-Item 的 prop 属性设置为需校验的字段名即可。

如果你的springboot版本没有默认引入,就导入依赖

<!--jsr3参数校验器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency>

里面依赖了hibernate-validator 在非空处理方式上提供了@NotNull,@NotBlank和@NotEmpty

在实体类的属性上使用如上的注解等

@Data@TableName("pms_brand")public class BrandEntity implements Serializable {private static final long serialVersionUID = 1L;/*** 品牌id*/@TableIdprivate Long brandId;/*** 品牌名*/@NotBlankprivate String name;/*** 品牌logo地址*/private String logo;/*** 介绍*/private String descript;/*** 显示状态[0-不显示;1-显示]*/@NotNullprivate Integer showStatus;/*** 检索首字母*/@NotEmptyprivate String firstLetter;/*** 排序*/@NotNull@Min(0)private Integer sort;}

步骤2:controller中加校验注解@Valid,开启校验,

@RequestMapping("/save")public R save(@RequestBody @Valid BrandEntity brand){brandService.save(brand);return R.ok();}

可以在添加注解的时候,修改message

@NotBlank(message = "品牌名必须非空")private String name;

但是这种返回的错误结果并不符合我们的业务需要。

步骤3:给校验的Bean后,紧跟一个BindResult,就可以获取到校验的结果。拿到校验的结果,就可以自定义的封装。

@RequestMapping("/save")public R save(@RequestBody @Valid 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);}brandService.save(brand);return R.ok();}

这种是针对于该请求设置了一个内容校验,如果针对于每个请求都单独进行配置,显然不是太合适,实际上可以统一的对于异常进行处理

统一异常处理@ControllerAdvice步骤4:统一异常处理

可以使用SpringMvc所提供的@ControllerAdvice,通过“basePackages”能够说明处理哪些路径下的异常。

在com.achang.achangmall.product.exception.AchangExceptionControllerAdvice编写

@Slf4j@RestControllerAdvice(basePackages = "com.achang.achangmall.product")public class AchangExceptionControllerAdvice {@ExceptionHandler(value = Exception.class) // 也可以返回ModelAndViewpublic R handleValidException(MethodArgumentNotValidException exception) {Map<String, String> map = new HashMap<>();// 获取数据校验的错误结果BindingResult bindingResult = exception.getBindingResult();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);}}

测试: http://localhost:88/api/product/brand/save

如果没有用,可能是spring没有扫描到,在主函数上添加@ComponentScan("com.achang.achangmall.product")

默认异常处理

@ExceptionHandler(value = Throwable.class)public R handleException(Throwable throwable){log.error("未知异常{},异常类型{}",throwable.getMessage(),throwable.getClass());return R.error(400,"数据校验出现问题");}

错误状态码

上面代码中,针对于错误状态码,是我们进行随意定义的,然而正规开发过程中,错误状态码有着严格的定义规则,如该在项目中我们的错误状态码定义

为了定义这些错误状态码,我们可以单独定义一个常量类,用来存储这些错误状态码

mon.exception,在通用模块中

/**** 错误码和错误信息定义类* 1. 错误码定义规则为5为数字* 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常* 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式* 错误码列表:* 10: 通用*001:参数格式校验* 11: 商品* 12: 订单* 13: 购物车* 14: 物流*/public enum BizCodeEnum {UNKNOW_EXEPTION(10000,"系统未知异常"),VALID_EXCEPTION( 10001,"参数格式校验失败");private int code;private String msg;BizCodeEnum(int code, String msg) {this.code = code;this.msg = msg;}public int getCode() {return code;}public String getMsg() {return msg;}}

5、分组校验功能(完成多场景的复杂校验)

给校验注解,标注上groups,指定什么情况下才需要进行校验

groups里面的内容要以接口的形式显示出来

如:指定在更新和添加的时候,都需要进行校验。新增时不需要带id,修改时必须带id

在通用模块中创建校验用的空接口,他只是个标识

achangmall-common中的mon.vail

//更新校验public interface UpdateVail {}//新增校验public interface AddVail {}

在实例类上groups标志接口标识

在这种情况下,没有指定分组的校验注解,默认是不起作用的。想要起作用就必须要加groups。

@NotNull(message = "修改必须定制品牌id", groups = {UpdateVailGroup.class})@Null(message = "新增不能指定id", groups = {AddVailGroup.class})private Long brandId;

业务方法参数上使用@Validated注解,并用@Validated指定使用校验的接口标识

分组情况下,校验注解生效问题

默认情况下,在分组校验情况下,没有指定指定分组的校验注解,将不会生效,它只会在不分组的情况下生效。

6、自定义校验功能

场景

要校验showStatus的01状态,可以用正则,但我们可以利用其他方式解决复杂场景。比如我们想要下面的场景

添加依赖

<dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId><version>2.0.1.Final</version></dependency>

编写自定义的校验注解

/*** 自定义校验注解*/@Documented@Constraint(validatedBy = {ListValueConstraintValidator.class})@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })@Retention(RUNTIME)public @interface ListValue {// 使用该属性去Validation.properties中取String message() default "{mon.valid.ListValue.message}";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};int[] value() default {};//传入可通过校验的值[]}

自定义校验器

/*** 自定义校验器*/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);}}@Override//左侧:传入需要校验的值public boolean isValid(Integer value, ConstraintValidatorContext context) {return set.contains(value);}}

关联校验器和校验注解

一个校验注解可以匹配多个校验器

使用实例

/*** 显示状态[0-不显示;1-显示]* 标识只能接受0,1;其他值都不能通过校验*/@ListValue(value = {0,1},groups ={AddGroup.class})private Integer showStatus;

二、SPU和SKU管理

1、SPU&SKU&规格参数&销售属性

重新执行“sys_menus.sql”

SPU

standard product unit(标准化产品单元):是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。

如iphoneX是SPU

SKU

stock keeping unit(库存量单位):库存进出计量的基本单元,可以是件/盒/托盘等单位。

SKU是对于大型连锁超市DC配送中心物流管理的一个必要的方法。现在已经被引申为产品统一编号的简称,每种产品对应有唯一的SKU号。

如iphoneX 64G 黑色 是SKU

同一个SPU拥有的特性叫基本属性

如机身长度,这个是手机共用的属性。而每款手机的属性值不同能决定库存量的叫销售属性。如颜色

基本属性[规格参数]与销售属性 每个分类下的商品共享规格参数,与销售属性。

只是有些商品不一定要用这个分类下全部的属性;

属性是以三级分类组织起来的 规格参数中有些是可以提供检索的 规格参数也是基本属性,他们具有自己的分组 属性的分组也是以三级分类组织起来的 属性名确定的,但是值是每一个商品不同来决定的

数据库表

pms数据库下的attr属性表,attr-group表

attr-group-id:几号分组

catelog-id:什么类别下的,比如手机 根据商品找到spu-id,attr-id

属性关系-规格参数-销售属性-三级分类 关联关系

SPU-SKU属性表

荣耀V20有两个属性,网络和像素,但是这两个属性的spu是同一个,代表是同款手机。

sku表里保存spu是同一手机,sku可能相同可能不同,相同代表是同一款,不同代表是不同款。

属性表说明每个属性的 枚举值 分类表有所有的分类,但有父子关系

2、API-属性分组-前端组件抽取&父子组件交互

sys_menus.sql

/*SQLyog Ultimate v11.25 (64 bit)MySQL - 5.7.27 : Database - gulimall_admin**********************************************************************//*!40101 SET NAMES utf8 */;/*!40101 SET SQL_MODE=''*/;/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;CREATE DATABASE /*!32312 IF NOT EXISTS*/`gulimall_admin` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;USE `achangmall_admin`;/*Table structure for table `sys_menu` */DROP TABLE IF EXISTS `sys_menu`;CREATE TABLE `sys_menu` (`menu_id` BIGINT(20) NOT NULL AUTO_INCREMENT,`parent_id` BIGINT(20) DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',`name` VARCHAR(50) DEFAULT NULL COMMENT '菜单名称',`url` VARCHAR(200) DEFAULT NULL COMMENT '菜单URL',`perms` VARCHAR(500) DEFAULT NULL COMMENT '授权(多个用逗号分隔,如:user:list,user:create)',`type` INT(11) DEFAULT NULL COMMENT '类型 0:目录 1:菜单 2:按钮',`icon` VARCHAR(50) DEFAULT NULL COMMENT '菜单图标',`order_num` INT(11) DEFAULT NULL COMMENT '排序',PRIMARY KEY (`menu_id`)) ENGINE=INNODB AUTO_INCREMENT=76 DEFAULT CHARSET=utf8mb4 COMMENT='菜单管理';/*Data for the table `sys_menu` */INSERT INTO `sys_menu`(`menu_id`,`parent_id`,`name`,`url`,`perms`,`type`,`icon`,`order_num`) VALUES (1,0,'系统管理',NULL,NULL,0,'system',0),(2,1,'管理员列表','sys/user',NULL,1,'admin',1),(3,1,'角色管理','sys/role',NULL,1,'role',2),(4,1,'菜单管理','sys/menu',NULL,1,'menu',3),(5,1,'SQL监控','http://localhost:8080/renren-fast/druid/sql.html',NULL,1,'sql',4),(6,1,'定时任务','job/schedule',NULL,1,'job',5),(7,6,'查看',NULL,'sys:schedule:list,sys:schedule:info',2,NULL,0),(8,6,'新增',NULL,'sys:schedule:save',2,NULL,0),(9,6,'修改',NULL,'sys:schedule:update',2,NULL,0),(10,6,'删除',NULL,'sys:schedule:delete',2,NULL,0),(11,6,'暂停',NULL,'sys:schedule:pause',2,NULL,0),(12,6,'恢复',NULL,'sys:schedule:resume',2,NULL,0),(13,6,'立即执行',NULL,'sys:schedule:run',2,NULL,0),(14,6,'日志列表',NULL,'sys:schedule:log',2,NULL,0),(15,2,'查看',NULL,'sys:user:list,sys:user:info',2,NULL,0),(16,2,'新增',NULL,'sys:user:save,sys:role:select',2,NULL,0),(17,2,'修改',NULL,'sys:user:update,sys:role:select',2,NULL,0),(18,2,'删除',NULL,'sys:user:delete',2,NULL,0),(19,3,'查看',NULL,'sys:role:list,sys:role:info',2,NULL,0),(20,3,'新增',NULL,'sys:role:save,sys:menu:list',2,NULL,0),(21,3,'修改',NULL,'sys:role:update,sys:menu:list',2,NULL,0),(22,3,'删除',NULL,'sys:role:delete',2,NULL,0),(23,4,'查看',NULL,'sys:menu:list,sys:menu:info',2,NULL,0),(24,4,'新增',NULL,'sys:menu:save,sys:menu:select',2,NULL,0),(25,4,'修改',NULL,'sys:menu:update,sys:menu:select',2,NULL,0),(26,4,'删除',NULL,'sys:menu:delete',2,NULL,0),(27,1,'参数管理','sys/config','sys:config:list,sys:config:info,sys:config:save,sys:config:update,sys:config:delete',1,'config',6),(29,1,'系统日志','sys/log','sys:log:list',1,'log',7),(30,1,'文件上传','oss/oss','sys:oss:all',1,'oss',6),(31,0,'商品系统','','',0,'editor',0),(32,31,'分类维护','product/category','',1,'menu',0),(34,31,'品牌管理','product/brand','',1,'editor',0),(37,31,'平台属性','','',0,'system',0),(38,37,'属性分组','product/attrgroup','',1,'tubiao',0),(39,37,'规格参数','product/baseattr','',1,'log',0),(40,37,'销售属性','product/saleattr','',1,'zonghe',0),(41,31,'商品维护','product/spu','',0,'zonghe',0),(42,0,'优惠营销','','',0,'mudedi',0),(43,0,'库存系统','','',0,'shouye',0),(44,0,'订单系统','','',0,'config',0),(45,0,'用户系统','','',0,'admin',0),(46,0,'内容管理','','',0,'sousuo',0),(47,42,'优惠券管理','coupon/coupon','',1,'zhedie',0),(48,42,'发放记录','coupon/history','',1,'sql',0),(49,42,'专题活动','coupon/subject','',1,'tixing',0),(50,42,'秒杀活动','coupon/seckill','',1,'daohang',0),(51,42,'积分维护','coupon/bounds','',1,'geren',0),(52,42,'满减折扣','coupon/full','',1,'shoucang',0),(53,43,'仓库维护','ware/wareinfo','',1,'shouye',0),(54,43,'库存工作单','ware/task','',1,'log',0),(55,43,'商品库存','ware/sku','',1,'jiesuo',0),(56,44,'订单查询','order/order','',1,'zhedie',0),(57,44,'退货单处理','order/return','',1,'shanchu',0),(58,44,'等级规则','order/settings','',1,'system',0),(59,44,'支付流水查询','order/payment','',1,'job',0),(60,44,'退款流水查询','order/refund','',1,'mudedi',0),(61,45,'会员列表','member/member','',1,'geren',0),(62,45,'会员等级','member/level','',1,'tubiao',0),(63,45,'积分变化','member/growth','',1,'bianji',0),(64,45,'统计信息','member/statistics','',1,'sql',0),(65,46,'首页推荐','content/index','',1,'shouye',0),(66,46,'分类热门','content/category','',1,'zhedie',0),(67,46,'评论管理','content/comments','',1,'pinglun',0),(68,41,'spu管理','product/spu','',1,'config',0),(69,41,'发布商品','product/spuadd','',1,'bianji',0),(70,43,'采购单维护','','',0,'tubiao',0),(71,70,'采购需求','ware/purchaseitem','',1,'editor',0),(72,70,'采购单','ware/purchase','',1,'menu',0),(73,41,'商品管理','product/manager','',1,'zonghe',0),(74,42,'会员价格','coupon/memberprice','',1,'admin',0),(75,42,'每日秒杀','coupon/seckillsession','',1,'job',0);/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

点击子组件,父组件触发事件 执行sys_menus.sql

接口文档地址https://easydoc.xyz/s/78237135

属性分组 现在想要实现点击菜单的左边,能够实现在右边展示数据

根据请求地址http://localhost:8001/#/product-attrgroup

所以应该有product/attrgroup.vue。

我们之前写过product/cateory.vue,现在我们要抽象到common//cateory.vue

左侧内容

要在左面显示菜单,右面显示表格复制<el-row :gutter="20">,放到attrgroup.vue的<template>。20表示列间距去element-ui文档里找到布局分为2个模块,分别占6列和18列

<el-row :gutter="20"><el-col :span="6"><div class="grid-content bg-purple"></div></el-col><el-col :span="18"><div class="grid-content bg-purple"></div></el-col></el-row>

有了布局之后,要在里面放内容。接下来要抽象一个分类vue。新建common/category,生成vue模板。把之前写的el-tree放到<template>.

所以他把menus绑定到了菜单上, 所以我们应该在export default {中有menus的信息 该具体信息会随着点击等事件的发生会改变值(或比如created生命周期时), tree也就同步变化了

common/category写好后,就可以在attrgroup.vue中导入使用了

import category from "../common/category.vue";

右侧表格内容

开始填写属性分组页面右侧的表格

复制achangmall-product\src\main\resources\src\views\modules\product\attrgroup.vue中的部分内容div到attrgroup.vue,他是我们之前通过人人的逆向生成的前端代码

批量删除是弹窗add-or-update 导入data、结合components

父子组件

要实现功能:点击左侧,右侧表格对应内容显示。

父子组件传递数据:category.vue点击时,引用它的attgroup.vue能感知到, 然后通知到add-or-update

比如嵌套div,里层div有事件后冒泡到外层div(是指一次点击调用了两个div的点击函数)

子组件(category)给父组件(attrgroup)传递数据,事件机制;

去element-ui的tree部分找event事件,看node-click() 在category中绑定node-click事件

<el-tree:data="data":props="defaultProps"node-key="catId"ref="menuTree"@node-click="nodeClick()"></el-tree>

this.$emit()

子组件给父组件发送一个事件,携带上数据;

//单击事件nodeClick(data, Node, item) {//向父组件发送事件//参数1:事件名(自定义命名);//后面可写任意的参数,他们会被传递出去this.$$emit("tree-node-click", data, Node, item);},

父组件中的获取发送的事件

attr-group中写

<el-col :span="6"><category @tree-node-click="treenodeclick()"></category></el-col>

表明他的子组件可能会传递过来点击事件,用自定义的函数接收传递过来的参数

//获取到子组件发送来的事件treenodeclick(data, Node, item) {console.log("attgroup感知到的category的节点被点击",data,Node,component);console.log("刚才被点击的菜单ID",data.catId);},

3、按接口文档开发

/s/78237135/ZUqEdvA4/OXTgKobR

查询功能: GET /product/attrgroup/list/{catelogId}

按照这个url,去product项目下的attrgroup-controller里修改

controller

/*** 列表* @param catelogId 0的话查所有*/@RequestMapping("/list/{catelogId}")public R list(@RequestParam Map<String, Object> params,@PathVariable("catelogId")Integer catelogId){PageUtils page = attrGroupService.queryPage(params,catelogId);return R.ok().put("page", page);}

增加接口与实现

Query里面就有个方法getPage(),传入map,将map解析为mybatis-plus的IPage对象

自定义PageUtils类用于传入IPage对象,得到其中的分页信息

AttrGroupServiceImpl extends ServiceImpl,其中ServiceImpl的父类中有方法page(IPage, Wrapper)。对于wrapper而言,没有条件的话就是查询所有

queryPage()返回前还会return new PageUtils(page);,

把page对象解析好页码信息,就封装为了响应数据

@Overridepublic PageUtils queryPage(Map<String, Object> params, Integer catelogId) {if (catelogId == 0) {IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params),new QueryWrapper<AttrGroupEntity>());return new PageUtils(page);} else {String key = (String) params.get("key");QueryWrapper<AttrGroupEntity> wrapper = new QueryWrapper<>();wrapper.eq("catelog_id", catelogId);if (StringUtils.isNotEmpty(key)) {wrapper.and(obj -> {obj.eq("attr_group_id", key).or().like("attr_group_name", key);});}IPage<AttrGroupEntity> page = this.page(new Query<AttrGroupEntity>().getPage(params),wrapper);return new PageUtils(page);}}

前端代码

attrgroup.vue

//获取到子组件发送来的事件treenodeclick(data, node, item) {if (node.level == 3) {#判断是否是三级分类this.catId = data.catId;this.getDataList();}},// 获取数据列表getDataList() {this.dataListLoading = true;this.$http({url: this.$http.adornUrl(`/product/attrgroup/list/${this.catId}`),#调整至我们写的接口urlmethod: "get",params: this.$http.adornParams({page: this.pageIndex,limit: this.pageSize,key: this.dataForm.key,}),}).then(({data }) => {if (data && data.code === 0) {this.dataList = data.page.list;this.totalPage = data.page.totalCount;} else {this.dataList = [];this.totalPage = 0;}this.dataListLoading = false;});},

新增功能

上面演示了查询功能,下面写insert分类

但是想要下面这个效果:

下拉菜单应该是手机一级分类的,这个功能是级联选择器

级联选择器<el-cascader级联选择的下拉同样是个options数组,多级的话用children属性即可

只需为 Cascader 的options属性指定选项数组即可渲染出一个级联选择器。通过props.expandTrigger可以定义展开子级菜单的触发方式。

去vue里找src\views\modules\product\attrgroup-add-or-update.vue

修改对应的位置为<el-cascader 。。。>

<el-cascaderv-model="value":options="options"@change="handleChange"></el-cascader>

把data()里的数组categorys绑定到options上即可,更详细的设置可以用props绑定

<el-cascaderv-model="dataForm.catelogId":options="categorys":props="props"></el-cascader>

categorys: [], //三级菜单数据props: {children: "children", label: "name", value: "catId" }, //cascader的设置属性

created() {this.getCategorys();},methods: {getCategorys() {this.$http({url: this.$http.adornUrl(`/product/category/list/tree`),method: "get",}).then((resp) => {this.categorys = resp.data.list;});},

发现了一个问题,就是后台返回children默认为[]时,vue也渲染出了子选框@JsonInclude去空字段

优化:没有下级菜单时不要有下一级空菜单,在java端把children属性空值去掉,空集合时去掉字段,

@TableField(exist = false)@JsonInclude(JsonInclude.Include.NON_EMPTY)private List<CategoryEntity> children;

提交完后返回页面也刷新了,是用到了父子组件。

在 m e s s a g e 弹 窗 结 束 回 调 message弹窗结束回调 message弹窗结束回调this.emit 接下来要解决的问题是,修改了该vue后,新增是可以用,修改回显就有问题了,应该回显3级

在init方法里进行回显但是分类的id还是不对,应该是用数组封装的路径

<el-buttontype="text"size="small"@click="addOrUpdateHandle(scope.row.attrGroupId)">修改</el-button><script>// 新增 / 修改addOrUpdateHandle(id) {// 先显示弹窗this.addOrUpdateVisible = true;// .$nextTick(代表渲染结束后再接着执行this.$nextTick(() => {// this是attrgroup.vue// $refs是它里面的所有组件。在本vue里使用的时候,标签里会些ref=""// addOrUpdate这个组件// 组件的init(id);方法this.$refs.addOrUpdate.init(id);});},</script>

在init方法里进行回显 但是分类的id还是不对,应该是用数组封装的路径

init(id) {this.dataForm.attrGroupId = id || 0;this.visible = true;this.$nextTick(() => {this.$refs["dataForm"].resetFields();if (this.dataForm.attrGroupId) {this.$http({url: this.$http.adornUrl(`/product/attrgroup/info/${this.dataForm.attrGroupId}`),method: "get",params: this.$http.adornParams(),}).then(({data }) => {if (data && data.code === 0) {this.dataForm.attrGroupName = data.attrGroup.attrGroupName;this.dataForm.sort = data.attrGroup.sort;this.dataForm.descript = data.attrGroup.descript;this.dataForm.icon = data.attrGroup.icon;this.dataForm.catelogId = data.attrGroup.catelogId;//查出catelogId的完整路径this.dataForm.catelogPath = data.attrGroup.catelogPath;}});}});},

修改AttrGroupEntity

@TableField(exist = false)private Long[] catelogPath;

修改controller

/*** 信息*/@RequestMapping("/info/{attrId}")public R info(@PathVariable("attrId") Long attrId){AttrEntity attr = attrService.getById(attrId);Long catelogId = attr.getCatelogId();Long[] path = categoryService.findCatelogPath(catelogId);attr.setCatelogPath(path);return R.ok().put("attr", attr);}

添加service

//找到catelogId的完整路径:[父/子/孙]@Overridepublic Long[] findCatelogPath(Long catelogId) {ArrayList<Long> list = new ArrayList<>();List<Long> parentPath = findParentPath(catelogId, list);Collections.reverse(parentPath);return (Long[]) list.toArray(new Long[parentPath.size()]);}private List<Long> findParentPath(Long catelogId,ArrayList<Long> list){list.add(catelogId);CategoryEntity entity = this.getById(catelogId);if (entity.getParentCid()!=0){findParentPath(entity.getParentCid(),list);}return list;}

优化:会话关闭时清空内容,防止下次开启还遗留数据

添加mybaitsplus分页配置

com.achang.achangmall.product.conf.MybatisConfig

@Configuration@EnableTransactionManagement@MapperScan("com.achang.achangmall.product.dao")public class MybatisConfig {//分页插件@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));return interceptor;}}

4、PO、DO、TO、DTO

PO持久对象

PO就是对应数据库中某个表中的一条记录,多个记录可以用PO的集合。PO中应该不包含任何对数据的操作。

DO(Domain 0bject)领域对象

就是从现实世界中推象出来的有形或无形的业务实体。

TO(Transfer 0bject)

数据传输对象传输的对象 不同的应用程序之间传输的对象。微服

DTO(Data Transfer Obiect)数据传输对象

概念来源于J2EE的设汁模式,原来的目的是为了EJB的分布式应用握供粗粒度的数据实体,以减少分布式调用的次数,从而握分布式调用的性能和降低网络负载,但在这里,泛指用于示层与服务层之间的数据传输对象。

V0(value object)值对象

通常用干业务层之闾的数据传递,和PO一样也是仅仅包含数据而已。但应是抽象出的业务对象,可以和表对应,也可以不,这根据业务的需要。用new关韃字创建,由GC回收的

Viewobject:视图对象

接受页面传递来的对象,封装对象

将业务处理完成的对象,封装成页面要用的数据

BO(business object)业务对象

从业务模型的度看.见IJML元#领嵫模型的领嵫对象。封装业务逻辑的java对象,通过用DAO方法,结合PO,VO进行业务操作。businessobject:业务对象主要作用是把业务逻辑封装为一个对苤。这个对象可以包括一个或多个其它的对彖。比如一个简历,有教育经历、工怍经历、社会关系等等。我们可以把教育经历对应一个PO工作经历

POJO简单无规则java对象

DAO

重写com.achang.achangmall.product.controller.AttrController,中save方法

controller

/*** 保存*/@RequestMapping("/save")public R save(@RequestBody AttrVo vo){attrService.saveAttr(vo);return R.ok();}

service

@Overridepublic void saveAttr(AttrVo vo) {AttrEntity attrEntity = new AttrEntity();BeanUtils.copyProperties(vo,attrEntity);this.save(attrEntity);AttrAttrgroupRelationEntity relationEntity = new AttrAttrgroupRelationEntity();relationEntity.setAttrGroupId(vo.getAttrGroupId());relationEntity.setAttrId(vo.getAttrId());attrAttrgroupRelationService.save(relationEntity);}

vo(com.achang.achangmall.product.vo.AttrVo)

@Datapublic class AttrVo implements Serializable {private static final long serialVersionUID = 1L;/*** 属性id*/private Long attrId;/*** 属性名*/private String attrName;/*** 是否需要检索[0-不需要,1-需要]*/private Integer searchType;/*** 属性图标*/private String icon;/*** 可选值列表[用逗号分隔]*/private String valueSelect;/*** 属性类型[0-销售属性,1-基本属性,2-既是销售属性又是基本属性]*/private Integer attrType;/*** 启用状态[0 - 禁用,1 - 启用]*/private Long enable;/*** 所属分类*/private Long catelogId;/*** 快速展示【是否展示在介绍上;0-否 1-是】,在sku中仍然可以调整*/private Integer showDesc;private Long[] catelogPath;private Long attrGroupId;}

规格参数列表

controller

@GetMapping("/attr/base/list/{catelogId}")public R baseAttrList(@RequestParam Map<String, Object> params,@PathVariable("catelogId")Integer catelogId){PageUtils page = attrService.queryBaseAttrPage(params,catelogId);return R.ok().put("page", page);}

service

@Overridepublic PageUtils queryBaseAttrPage(Map<String, Object> params, Integer catelogId) {QueryWrapper<AttrEntity> wrapper = new QueryWrapper<>();if (catelogId !=0){wrapper.eq("catelog_id",catelogId);}String key = (String) params.get("key");if (!StringUtils.isEmpty(key)){wrapper.eq("catelog_id",key).or().like("attr_name",key);}IPage<AttrEntity> page = this.page(new Query<AttrEntity>().getPage(params),wrapper);PageUtils pageUtils = new PageUtils(page);List<AttrEntity> list = page.getRecords();List<AttrRespVo> resultList = list.stream().map(item -> {AttrRespVo attrRespVo = new AttrRespVo();BeanUtils.copyProperties(item, attrRespVo);AttrAttrgroupRelationEntity attrId = attrAttrgroupRelationService.getOne(new QueryWrapper<AttrAttrgroupRelationEntity>().eq("attr_id", item.getAttrId()));if (attrId != null) {AttrGroupEntity attrGroupEntity = attrGroupService.getById(attrId);attrRespVo.setGroupName(attrGroupEntity.getAttrGroupName());}CategoryEntity categoryEntity = categoryService.getById(item.getCatelogId());if (categoryEntity != null) {attrRespVo.setCatelogName(categoryEntity.getName());}return attrRespVo;}).collect(Collectors.toList());pageUtils.setList(resultList);return pageUtils;}

vo

@Datapublic class AttrRespVo extends AttrVo {private String catelogName;private String groupName;}

阿昌这里看到了70多p感觉老师写的都是业务代码,就没敲了,所以按大家的对crud的掌握情况进行编码书写,如果是crud老手,就看看个流程就好,如果还没写过的,就跟着老师敲敲

在后续启动achangmall-member报了No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbala错误

记住,要在application.yaml中,关闭ribbon

spring:cloud:loadbalancer:ribbon:enabled: false

在pom.xml去排除ribbon

<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId><exclusions><exclusion><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-ribbon</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId><version>2.2.1.RELEASE</version></dependency>

5、设置日期数据规则

spring:jackson:date-format: yyyy-MM-dd HH:mm:sstime-zone: GMT+8

6、debug调试技巧

debug时,mysql默认的隔离级别为读已提交,为了能够在调试过程中,获取到数据库中的数据信息,可以调整隔离级别为读未提交:

SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

7、IDEA一键启动项目设置

这样子就只需要通过这个Compound一键启动需要启动的服务了!!!

8、采购简要流程

9、bug解决

pubsub、publish报错

解决如下:

1 、npm install --save pubsub-js

2 、在src下的main.js中引用:

import PubSub from ‘pubsub-js’ Vue.prototype.PubSub = PubSub

数据库里少了value_type字段

解决如下:

在数据库的 pms_attr 表加上value_type字段,类型为tinyint就行;

在代码中,AttyEntity.java、AttrVo.java中各添加:

private Integer valueType,

在AttrDao.xml中添加:

<result property="valueType" column="value_type"/>

规格参数显示不出来页面,原因是要在每个分组属性上至少关联一个属性。控制台foreach报错null

解决如下:

在spuadd.vue的showBaseAttrs()方法中在 //先对表单的baseAttrs进行初始化加上非空判断 if (item.attrs != null)就可以了

data.data.forEach(item => {let attrArray = [];if (item.attrs != null) {item.attrs.forEach(attr => {attrArray.push({attrId: attr.attrId,attrValues: "",showDesc: attr.showDesc});});}this.dataResp.baseAttrs.push(attrArray);});

feign超时异常导致读取失败

解决如下:

在achangmall-product的application.yml添加如下即可解决(时间设置长点就行了)

ribbon:ReadTimeout: 30000ConnectTimeout: 30000

点击规格找不到页面,以及规格回显问题解决
1 点击规格找不到页面,解决如下:

INSERT INTO sys_menu (menu_id, parent_id, name, url, perms, type, icon, order_num) VALUES (76, 37, '规格维护', 'product/attrupdate', '', 2, 'log', 0);

2 规格回显问题不出来

原因:

因为那个属性的值类型是多选而pms_product_attr_value这个表里面的属性值存的单个值。前端展示将这个值用;切割成数组来展示的。切完数组里面只有一个值就转成字符串。所以在多选下拉就赋不了值

解决如下:

将页面attrupdate.vue中showBaseAttrs这个方法里面的代码

if (v.length == 1) {v = v[0] + ''}

换成下面这个

if (v.length == 1 && attr.valueType == 0) {v = v[0] + ''}

10、总结

分布式基附概念

微服务、注册中心、配置中心、远程调用、 Feign、网关

基础开发

springboot2.0、 SpringCloud、 Mybatis-Plus、Vue组件化、阿里云对象存储

环境

Vmware、 Linux、 Docker、 MYSQL、 Redis、逆向工程&人人开源

开发规范

数据校验JSR303、全局异常处理、全局统一返回、全局跨域处理

枚举状态,业务状态码、VO与TO与PO划分,逻组删除

Lombok @Data @Slf4j

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。