持续学习&持续更新中…
学习态度:守破离
【雷丰阳-谷粒商城 】【分布式基础篇-全栈开发篇】【04】
- 跨域
- 问题
- 解决
- 实现
- 逻辑删除
- 文件存储
- 普通上传
- 云存储
- 阿里云OSS
- 简介
- 术语
- 简单使用
- 使用SpringCloudAlibaba—oss
- 服务端签名后直传
- 普通上传方式:
- 服务端签名后直传:
- 实现:
- 后端校验—JSR303
- 统一异常处理
- 参考
跨域
问题
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/CORS
解决
实现
gulimall-gateway:
spring:
cloud:
gateway:
routes:
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
#需要把精确的路由放到上面【/api/product/**比/api/**更精确】
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
## 前端项目,/api
## http://localhost:88/api/captcha.jpg http://localhost:8080/renren-fast/captcha.jpg
## http://localhost:88/api/product/category/list/tree http://localhost:10000/product/category/list/tree
# - id: qq_route
# uri: https://www.qq.com
# predicates:
# - Query=url,qq
# - id: bing
# uri: https://cn.bing.com/
# predicates:
# - Query=url,bing
@Configuration
public class GulimallCorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
// 配置跨域
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOrigin("*"); // 请求来源
corsConfiguration.setAllowCredentials(true); // cookie
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsWebFilter(source);
}
}
逻辑删除
数据库:
application.yml:【全局设置】
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
id-type: auto
logic-delete-field: deleted # 全局逻辑删除的实体字段名
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
Model:【局部设置】
@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
// ...
/**
* 是否显示[0-不显示,1显示]
*/
@TableLogic(value = "1", delval = "0")
private Integer showStatus;
}
Controller:
@RestController
@RequestMapping("product/category")
public class CategoryController {
/**
* 删除
* @RequestBody:获取请求体,必须发送POST请求
* SpringMVC自动将请求体的数据(json),转为对应的对象
*/
@RequestMapping("/delete")
//@RequiresPermissions("product:category:delete")
public R delete(@RequestBody Long[] catIds){
//categoryService.removeByIds(Arrays.asList(catIds));
categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}
}
ServiceImpl:
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
@Override
public void removeMenuByIds(List<Long> asList) {
//TODO 检查当前删除的菜单,是否被别的地方引用
//逻辑删除
baseMapper.deleteBatchIds(asList);
}
}
文件存储
普通上传
云存储
阿里云OSS
https://www.aliyun.com/product/oss
https://help.aliyun.com/product/31815.html
简介
阿里云对象存储OSS(Object Storage Service)为您提供基于网络的数据存取服务。使用OSS,您可以通过网络随时存储和调用包括文本、图片、音视频在内的各类数据文件。
OSS视频简介:https://static-aliyun-doc.oss-cn-hangzhou.aliyuncs.com/file-manage-files/zh-CN/20220727/ttob/OSS%E6%96%B0%E7%89%88%E4%BA%A7%E5%93%81%E4%BB%8B%E7%BB%8D%EF%BC%88%E4%B8%AD%E6%96%87%EF%BC%89.mp4
-
阿里云对象存储OSS(Object Storage Service)是一款海量、安全、低成本、高可靠的云存储服务,可提供99.9999999999%(12个9)的数据持久性,99.995%的数据可用性。多种存储类型供选择,全面优化存储成本。
-
OSS具有与平台无关的RESTful API接口,您可以在任何应用、任何时间、任何地点存储和访问任意类型的数据。
-
您可以使用阿里云提供的API、SDK接口或者OSS迁移工具轻松地将海量数据移入或移出阿里云OSS。数据存储到阿里云OSS以后,您可以选择标准存储(Standard)作为移动应用、大型网站、图片分享或热点音视频的主要存储方式,也可以选择成本更低、存储期限更长的低频访问存储(Infrequent Access)、归档存储(Archive)、冷归档存储(Cold Archive)作为不经常访问数据的存储方式。
术语
中文 | 英文 | 说明 |
---|---|---|
存储空间 | Bucket | 存储空间是您用于存储对象(Object)的容器,所有的对象都必须隶属于某个存储空间。 |
对象/文件 | Object | 对象是 OSS 存储数据的基本单元,也被称为OSS的文件。对象由元信息(Object Meta)、用户数据(Data)和文件名(Key)组成。对象由存储空间内部唯一的Key来标识。 |
地域 | Region | 地域表示 OSS 的数据中心所在物理位置。您可以根据费用、请求来源等综合选择数据存储的地域。详情请查看OSS已经开通的Region。 |
访问域名 | Endpoint | Endpoint 表示OSS对外服务的访问域名。OSS以HTTP RESTful API的形式对外提供服务,当访问不同地域的时候,需要不同的域名。通过内网和外网访问同一个地域所需要的域名也是不同的。具体的内容请参见各个Region对应的Endpoint。 |
访问密钥 | AccessKey | AccessKey,简称 AK,指的是访问身份验证中用到的AccessKeyId 和AccessKeySecret。OSS通过使用AccessKeyId 和AccessKeySecret对称加密的方法来验证某个请求的发送者身份。AccessKeyId用于标识用户,AccessKeySecret是用户用于加密签名字符串和OSS用来验证签名字符串的密钥,其中AccessKeySecret 必须保密。 |
建议:一个项目创建一个Bucket
简单使用
-
创建一个AccessKey,只用于“
OpenAPI 调用访问
”,并分配“AliyunOSSFullAccess
”权限 -
创建一个Bucket,存储类型为“低频访问存储”,读写权限为“公共读”
-
简单的文件上传:
<dependency> <groupId>com.aliyun.oss</groupId> <artifactId>aliyun-sdk-oss</artifactId> <version>3.5.0</version> </dependency>
@Test public void testUpload() throws Exception { // Endpoint以华东1(杭州)为例,其它Region请按实际情况填写。 String endpoint = "xxx"; // 阿里云账号AccessKey拥有所有API的访问权限,风险很高。强烈建议您创建并使用RAM用户进行API访问或日常运维,请登录RAM控制台创建RAM用户。 String accessKeyId = "xxx"; String accessKeySecret = "xxx"; // 填写Bucket名称,例如examplebucket。 String bucketName = "xxx"; // 填写Object完整路径,完整路径中不能包含Bucket名称,例如exampledir/exampleobject.txt。 String objectName = "mengwa.jpg"; // 填写本地文件的完整路径,例如D:\\localpath\\examplefile.txt。 // 如果未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件流。 String filePath = "C:\\Users\\lpruoyu\\Pictures\\Saved Pictures\\mengwa.jpg"; // 创建OSSClient实例。 OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); try { InputStream inputStream = new FileInputStream(filePath); // 创建PutObject请求。 ossClient.putObject(bucketName, objectName, inputStream); } catch (OSSException oe) { System.out.println("Caught an OSSException, which means your request made it to OSS, " + "but was rejected with an error response for some reason."); System.out.println("Error Message:" + oe.getErrorMessage()); System.out.println("Error Code:" + oe.getErrorCode()); System.out.println("Request ID:" + oe.getRequestId()); System.out.println("Host ID:" + oe.getHostId()); } catch (ClientException ce) { System.out.println("Caught an ClientException, which means the client encountered " + "a serious internal problem while trying to communicate with OSS, " + "such as not being able to access the network."); System.out.println("Error Message:" + ce.getMessage()); } finally { if (ossClient != null) { ossClient.shutdown(); } } }
使用SpringCloudAlibaba—oss
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>
spring:
cloud:
alicloud:
access-key: xxx
secret-key: xxx
oss:
endpoint: xxx
@Autowired
OSSClient ossClient;
@Test
public void testUpload() throws Exception {
String bucketName = "xxx";
String objectName = "chumen.png";
String filePath = "C:\\Users\\lpruoyu\\Pictures\\Saved Pictures\\chumen.png";
try {
InputStream inputStream = new FileInputStream(filePath);
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, inputStream);
} catch (Exception e) {
System.out.println("Exception:" + e);
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
}
服务端签名后直传
普通上传方式:
服务端签名后直传:
实现:
创建跨域规则:
@RestController
public class OssController {
@Autowired
OSS ossClient;
@Value("${spring.cloud.alicloud.oss.endpoint}")
private String endpoint;
@Value("${spring.cloud.alicloud.oss.bucket}")
private String bucket;
@Value("${spring.cloud.alicloud.access-key}")
private String accessId;
@RequestMapping("/oss/policy")
public R policy() {
//https://gulimall-hellolp.oss-cn-hangzhou.aliyuncs.com/chumen.png
String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
// callbackUrl为 上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
// String callbackUrl = "http://88.88.88.88:8888";
String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
String dir = format + "/"; // 用户上传文件时指定的前缀。
Map<String, String> respMap = null;
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
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());
}
return R.ok().put("data", respMap);
}
}
后端校验—JSR303
前后端都需要校验
// 是接口就行(用于分组校验)
public interface AddGroup {
}
public interface UpdateGroup {
}
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class})
@Null(message = "新增不能指定id",groups = {AddGroup.class})
@TableId
private Long brandId;
/**
* 品牌名
*/
@NotBlank(message = "品牌名必须提交",groups = {AddGroup.class,UpdateGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotBlank(groups = {AddGroup.class})
@URL(message = "logo必须是一个合法的url地址",groups={AddGroup.class,UpdateGroup.class})
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
// @Pattern()
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
@ListValue(vals={0,1},groups = {AddGroup.class, UpdateStatusGroup.class})
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty(groups={AddGroup.class})
@Pattern(regexp="^[a-zA-Z]$",message = "检索首字母必须是一个字母",groups={AddGroup.class,UpdateGroup.class})
private String firstLetter;
/**
* 排序
*/
@NotNull(groups={AddGroup.class})
@Min(value = 0,message = "排序必须大于等于0",groups={AddGroup.class,UpdateGroup.class})
private Integer sort;
}
@RequestMapping("/save")
//@RequiresPermissions("product:brand:save")
// 给校验的bean后紧跟一个BindingResult,就可以获取到校验的结果
public R save(/*@Valid*/ @Validated({AddGroup.class}) @RequestBody BrandEntity brand/*,BindingResult result*/){
// if(result.hasErrors()){
// Map<String,String> map = new HashMap<>();
// //1、获取校验的错误结果
// result.getFieldErrors().forEach((item)->{
// //FieldError (item)
// 获取到错误提示
// String message = item.getDefaultMessage();
// //获取错误的属性的名字
// String field = item.getField();
// map.put(field,message);
// });
//
// return R.error(400,"提交的数据不合法").put("data",map);
// }else {
//
// }
brandService.save(brand);
return R.ok();
}
/**
* 修改
*/
@RequestMapping("/update")
//@RequiresPermissions("product:brand:update")
public R update(@Validated(UpdateGroup.class) @RequestBody BrandEntity brand){
brandService.updateDetail(brand);
return R.ok();
}
自定义校验:
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
@Documented
@Constraint(validatedBy = { l.p.common.valid.ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
String message() default "{l.p.common.valid.ListValue.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int[] vals() default { };
}
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private Set<Integer> set = new HashSet<>();
//初始化方法
@Override
public void initialize(ListValue constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
set.add(val);
}
}
//判断是否校验成功
/**
*
* @param value 需要校验的值
* @param context
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
ValidationMessages.properties:
l.p.common.valid.ListValue.message=必须提交指定的值
使用:
@NotNull(groups = {AddGroup.class, UpdateStatusGroup.class})
@ListValue(vals = {0, 1}, groups = {AddGroup.class, UpdateStatusGroup.class})
private Integer showStatus;
@RequestMapping("/xx")
public R xx(@Validated(AddGroup.class) @RequestBody BrandEntity brand){
// xxxx
return R.ok();
}
统一异常处理
/***
* 错误码和错误信息定义类
* 1. 错误码定义规则为5为数字
* 2. 前两位表示业务场景,最后三位表示错误码。例如:100001。10:通用 001:系统未知异常
* 3. 维护错误码后需要维护错误描述,将他们定义为枚举形式
* 错误码列表:
* 10: 通用
* 001:参数格式校验
* 11: 商品
* 12: 订单
* 13: 购物车
* 14: 物流
*/
public enum BizCodeEnume {
UNKNOW_EXCEPTION(10000,"系统未知异常"),
VAILD_EXCEPTION(10001,"参数格式校验失败");
private int code;
private String msg;
BizCodeEnume(int code,String msg){
this.code = code;
this.msg = msg;
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
}
/**
* 集中处理所有异常
*/
@Slf4j
//@ResponseBody
//@ControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
@RestControllerAdvice(basePackages = "com.atguigu.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
@ExceptionHandler(value= MethodArgumentNotValidException.class)
public R handleVaildException(MethodArgumentNotValidException e){
log.error("数据校验出现问题{},异常类型:{}",e.getMessage(),e.getClass());
BindingResult bindingResult = e.getBindingResult();
Map<String,String> errorMap = new HashMap<>();
bindingResult.getFieldErrors().forEach((fieldError)->{
errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
});
return R.error(BizCodeEnume.VAILD_EXCEPTION.getCode(),BizCodeEnume.VAILD_EXCEPTION.getMsg()).put("data",errorMap);
}
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable){
log.error("错误:",throwable);
return R.error(BizCodeEnume.UNKNOW_EXCEPTION.getCode(),BizCodeEnume.UNKNOW_EXCEPTION.getMsg());
}
}
参考
雷丰阳: Java项目《谷粒商城》Java架构师 | 微服务 | 大型电商项目).
本文完,感谢您的关注支持!