商品服务
一、品牌管理
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;