Day400&401&402&403&404&405&406.商品服务 -谷粒商城
商品服务一、品牌管理1、效果优化与快速显示开关将逆向工程product得到的resources\src\views\modules\product文件拷贝到achangmall/renren-fast-vue/src/views/modules/product目录下,也就是下面的两个文件brand.vue : 显示的表单brand-add-or-update.vue:添加和更改功能但是显示的页面没
商品服务
一、品牌管理
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-input
v-model="dataForm.key"
placeholder="参数名"
clearable
></el-input>
</el-form-item>
<el-form-item>
<el-button @click="getDataList()">查询</el-button>
<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
>
</el-form-item>
</el-form>
<el-table
:data="dataList"
border
v-loading="dataListLoading"
@selection-change="selectionChangeHandle"
style="width: 100%"
>
<el-table-column
type="selection"
header-align="center"
align="center"
width="50"
>
</el-table-column>
<el-table-column
prop="brandId"
header-align="center"
align="center"
label="品牌id"
>
</el-table-column>
<el-table-column
prop="name"
header-align="center"
align="center"
label="品牌名"
>
</el-table-column>
<el-table-column
prop="logo"
header-align="center"
align="center"
label="品牌logo地址"
>
</el-table-column>
<el-table-column
prop="descript"
header-align="center"
align="center"
label="介绍"
>
</el-table-column>
<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>
<el-table-column
prop="firstLetter"
header-align="center"
align="center"
label="检索首字母"
>
</el-table-column>
<el-table-column
prop="sort"
header-align="center"
align="center"
label="排序"
>
</el-table-column>
<el-table-column
fixed="right"
header-align="center"
align="center"
width="150"
label="操作"
>
<template slot-scope="scope">
<el-button
type="text"
size="small"
@click="addOrUpdateHandle(scope.row.brandId)"
>修改</el-button
>
<el-button
type="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-update
v-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-switch
v-model="dataForm.showStatus"
active-color="#13ce66"
inactive-color="#ff4949"
>
</el-switch>
</el-form-item>
<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="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上传策略,生成防伪签名
-
使用代码上传 查看阿里云关于文件上传的帮助:
https://help.aliyun.com/document_detail/32009.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-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名称(例如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-qingdao.aliyuncs.com
-
bucket域名:
-
就是签名加上bucket,如achangmall0.oss-cn-hangzhou.aliyuncs.com
accessKeyId和accessKeySecret需要创建一个RAM账号:
-
选上编程访问 创建用户完毕后,会得到一个“AccessKey ID”和“AccessKeySecret”,
然后复制这两个值到代码的“AccessKey ID”和“AccessKeySecret”。
另外还需要添加访问控制权限:
@Test void test0() throws FileNotFoundException { // Endpoint以杭州为例,其它Region请按实际情况填写。 String endpoint = "oss-cn-hangzhou.aliyuncs.com"; // 云账号AccessKey有所有API访问权限,建议遵循阿里云安全最佳实践,创建并使用RAM子账号进行API访问或日常运维,请登录 https://ram.console.aliyun.com 创建。 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
https://github.com/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: xxx secret-key: xxx oss: endpoint: oss-cn-hangzhou.aliyuncs.com
- 注入OSSClient并进行文件上传下载等操作
@RunWith(SpringRunner.class) @SpringBootTest public class OssTest { @Resource private OSSClient ossClient; @Test void 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: 30000
spring:
application:
name: achangmall-third-service
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
- bootstrap.properties
spring.cloud.nacos.config.name=achangmall-third-service
spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.cloud.nacos.config.namespace=ad0431b9-a77f-4220-bf61-b48c7e117250
spring.cloud.nacos.config.extension-configs[0].data-id=achangmall-third-service.yml
spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.extension-configs[0].refresh=true
- 编写测试类
@SpringBootTest
@RunWith(SpringRunner.class)
class AchangmallThirdServiceApplicationTests {
@Resource
OSSClient ossClient;
@Test
void 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 2021-09-25 14:32
*******
*/
@RestController
@RequestMapping("third-service/oss")
public class OssController {
@Resource
private 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_route
uri: lb://achangmall-third-service
predicates:
- 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-hangzhou.aliyuncs.com”,
你的阿里云指定的bucket域名
- 要替换里面的action中的内容action=“http://achangmall0.oss-cn-hangzhou.aliyuncs.com”,
-
singleUpload.vue代码
<template>
<div>
<el-upload
action="http://achangmall0.oss-cn-hangzhou.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.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-upload
action="http://achangmall0.oss-cn-hangzhou.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代码
/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-switch
v-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-switch
v-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-input
v-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
*/
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank
private String name;
/**
* 品牌logo地址
*/
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
@NotNull
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty
private 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) -> {
//获取发生错误时的message
String 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) // 也可以返回ModelAndView
public 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,"数据校验出现问题");
}
- 错误状态码
上面代码中,针对于错误状态码,是我们进行随意定义的,然而正规开发过程中,错误状态码有着严格的定义规则,如该在项目中我们的错误状态码定义
- 为了定义这些错误状态码,我们可以单独定义一个常量类,用来存储这些错误状态码
com.achang.common.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中的com.achang.common.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 "{com.atguigu.common.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<>();
@Override
public 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是SPUSKU:
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、按接口文档开发
https://easydoc.net/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对象解析好页码信息,就封装为了响应数据
@Override
public 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}`),#调整至我们写的接口url
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;
});
},
- 新增功能
上面演示了查询功能,下面写insert分类
但是想要下面这个效果:
下拉菜单应该是手机一级分类的,这个功能是级联选择器
级联选择器
<el-cascader
级联选择的下拉同样是个options数组,多级的话用children属性即可只需为 Cascader 的options属性指定选项数组即可渲染出一个级联选择器。通过props.expandTrigger可以定义展开子级菜单的触发方式。
- 去vue里找src\views\modules\product\attrgroup-add-or-update.vue
修改对应的位置为<el-cascader 。。。>
<el-cascader
v-model="value"
:options="options"
@change="handleChange"
></el-cascader>
- 把data()里的数组categorys绑定到options上即可,更详细的设置可以用props绑定
<el-cascader
v-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-button
type="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的完整路径:[父/子/孙]
@Override
public 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 {
//分页插件
@Bean
public 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
@Override
public 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)
@Data
public 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
@Override
public 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
@Data
public 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:ss
time-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: 30000 ConnectTimeout: 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
更多推荐
所有评论(0)