3. 内容模块管理 - 异常处理与校验

news2024/9/25 19:17:59

文章目录

  • 内容模块管理
  • 一、自定义异常
    • 1.1 全局异常处理器
    • 1.2 自定义异常
    • 1.3 异常统一响应类
    • 1.4 封装通用异常信息
  • 二、JSR303校验
    • 2.1 Maven坐标
    • 2.2 校验规则
    • 2.3 代码示例
    • 2.4 捕捉校验异常
    • 2.5 分组校验
    • 2.6 备注
  • 三、全局异常处理2
    • 3.1 全局异常处理器
    • 3.2 结果集
    • 3.3 常用注解
      • 3.3.1 @ControllerAdvice
      • 3.3.2 @ExceptionHandle

内容模块管理

一、自定义异常

之前也学过对异常的统一管理,既然现在又看到了,就再学学springboot——全局异常处理器及封装结果集

如果一致对异常进行try…catch…,代码也会很冗余,我们直接throw就行,然后可以对异常进行统一处理

不论是那一层,都会有异常的出现,我们遇到异常都是向上抛出,最后让框架对异常统一处理

假如dao出异常就会抛给service,service有异常就会抛给controller,controller会抛给spring框架

image-20231115212256176

1.1 全局异常处理器

  • @ExceptionHandler

    提供的标识在方法上或类上的注解,用来表明方法的处理异常类型

  • @ControllerAdvice

    控制器增强,在项目中来增强SpringMVC中的Controller。通常和@ExceptionHandler结合使用,来处理SpringMVC的异常信息。

  • @ResponseStatus

    提供的标识在方法上或类上的注解,用状态代码和应返回的原因标记方法或异常类。

    调用处理程序方法时,状态代码将应用于HTTP响应

/**
 * 统一异常处理类
 */
@Slf4j
@ControllerAdvice
@ResponseBody//返回JSON数据
//@RestControllerAdvice = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {

    /**
     * 针对自定义异常进行处理的
     */
    @ResponseBody
    @ExceptionHandler(XueChengPlusException.class)//捕捉这个异常
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)//状态码500
    public RestErrorResponse customException(XueChengPlusException e) {
//      捕捉到异常的信息后解析异常的信息
        log.error("【系统异常】{}", e.getErrMessage(), e);
        return new RestErrorResponse(e.getErrMessage());
    }

    /**
     * 捕捉除了自定义异常以外的其他异常
     */
    @ResponseBody
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)//状态码500
    public RestErrorResponse exception(Exception e) {
        log.error("【系统异常】{}", e.getMessage(), e);
        return new RestErrorResponse(CommonError.UNKOWN_ERROR.getErrMessage());//执行过程异常,请重试

    }

}

1.2 自定义异常

@EqualsAndHashCode(callSuper = true)
@Data
public class XueChengPlusException extends RuntimeException {

   private static final long serialVersionUID = -378480393877627738L;

   private String errMessage;

    public XueChengPlusException() {
        super();
    }

    public XueChengPlusException(String errMessage) {
        super(errMessage);
        this.errMessage = errMessage;
    }


    public static void cast(CommonError commonError) {
        throw new XueChengPlusException(commonError.getErrMessage());
    }

    public static void cast(String errMessage) {
        throw new XueChengPlusException(errMessage);
    }

}

1.3 异常统一响应类

/**
 * 和前端约定返回的异常信息模型
 * 错误响应参数包装
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RestErrorResponse implements Serializable {

    private static final long serialVersionUID = 9026504397012666687L;

    private String errMessage;

}

1.4 封装通用异常信息

/**
 * 通用异常信息
 */
public enum CommonError {

   UNKOWN_ERROR("执行过程异常,请重试。"),
   PARAMS_ERROR("非法参数"),
   OBJECT_NULL("对象为空"),
   QUERY_NULL("查询结果为空"),
   REQUEST_NULL("请求参数为空");

   private String errMessage;

   public String getErrMessage() {
      return errMessage;
   }

   private CommonError( String errMessage) {
      this.errMessage = errMessage;
   }

}

二、JSR303校验

SpringBoot提供了JSR-303的支持,它就是spring-boot-starter-validation,它的底层使用Hibernate Validator,Hibernate Validator是Bean Validation 的参考实现。

所以,我们准备在Controller层使用spring-boot-starter-validation完成对请求参数的基本合法性进行校验。

  • 前端请求后端接口传输参数,是在controller中校验还是在Service中校验

    答案是都需要校验,只是分工不同。因为某些Service不仅仅被一个Controller调用,如果Service中不写校验再被其他Controller或其他Service调用时也会发生问题

  • Controller里一般校验什么

    Contoller中校验请求参数的合法性,包括:必填项校验,数据格式校验,比如:是否是符合一定的日期格式,等。

  • Service里一般校验什么

    Service中要校验的是业务规则相关的内容,比如:课程已经审核通过所以提交失败。

    Service中根据业务规则去校验不方便写成通用代码,Controller中则可以将校验的代码写成通用代码

2.1 Maven坐标

首先在Base工程添加spring-boot-starter-validation的依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

在javax.validation.constraints包下有很多这样的校验注解,直接使用注解定义校验规则即可

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2.2 校验规则

image-20231115224419263

2.3 代码示例

现在准备对内容管理模块添加课程接口进行参数校验,如下接口

定义好校验规则还需要开启校验,在controller方法中添加@Validated注解,否则校验不会生效

    @ApiOperation("新增课程")
    @PostMapping("/course")
    public CourseBaseInfoDto createCourseBase(@Validated @RequestBody AddCourseDto addCourseDto) {
//      将来会集成SpringSecurity框架,用户登录之后就可以获取到用户所属机构的ID
//      先把机构ID写死
        return courseBaseInfoService.createCourseBase(10086L,addCourseDto);
    }

进入AddCourseDto类,在属性上添加校验规则

@NotEmpty表示属性不能为空

@Size表示限制属性内容的长短

/**
 * @description 添加课程dto
 */
@Data
@ApiModel(value = "AddCourseDto", description = "新增课程基本信息")
public class AddCourseDto {

    @NotEmpty(message = "课程名称不能为空")
    @ApiModelProperty(value = "课程名称", required = true)
    private String name;

    @NotEmpty(message = "适用人群不能为空")
    @Size(message = "适用人群内容过少", min = 10)
    @ApiModelProperty(value = "适用人群", required = true)
    private String users;

    @ApiModelProperty(value = "课程标签")
    private String tags;

    @NotEmpty(message = "课程分类不能为空")
    @ApiModelProperty(value = "大分类", required = true)
    private String mt;

    @NotEmpty(message = "课程分类不能为空")
    @ApiModelProperty(value = "小分类", required = true)
    private String st;

    @NotEmpty(message = "课程等级不能为空")
    @ApiModelProperty(value = "课程等级", required = true)
    private String grade;

    @ApiModelProperty(value = "教学模式(普通,录播,直播等)", required = true)
    private String teachmode;

    @ApiModelProperty(value = "课程介绍")
    private String description;

    @ApiModelProperty(value = "课程图片", required = true)
    private String pic;

    @NotEmpty(message = "收费规则不能为空")
    @ApiModelProperty(value = "收费规则,对应数据字典", required = true)
    private String charge;

    @ApiModelProperty(value = "价格")
    private Float price;
    @ApiModelProperty(value = "原价")
    private Float originalPrice;


    @ApiModelProperty(value = "qq")
    private String qq;

    @ApiModelProperty(value = "微信")
    private String wechat;
    @ApiModelProperty(value = "电话")
    private String phone;

    @ApiModelProperty(value = "有效期")
    private Integer validDays;
}

2.4 捕捉校验异常

该异常表示在方法参数验证失败时抛出,其中验证失败的详细信息可以通过 BindingResult 对象获取

getBindingResult() 方法是 MethodArgumentNotValidException 类的一个方法,用于获取包含验证错误详细信息的 BindingResult 对象。BindingResult 包含了验证结果,包括错误对象、错误代码、默认错误消息等。

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public RestErrorResponse methodArgumentNotValidException(MethodArgumentNotValidException e) {
//      这个异常对象有一个方法getBindingResult
        BindingResult bindingResult = e.getBindingResult();
        List<String> msgList = new ArrayList<>();
        //将错误信息放在msgList
        //因为校验的时候可能多个字段都会出现问题,所以使用了一个list集合存放有问题的消息,我们后面会将他们一块拼接起来的
        bindingResult.getFieldErrors().stream().forEach(item -> msgList.add(item.getDefaultMessage()));
        //拼接错误信息
        String msg = StringUtils.join(msgList, ",");
        log.error("【系统异常】{}", msg);
        return new RestErrorResponse(msg);
    }

比如下面的这一次请求中就是两个字段不合格,都会放到List集合中,最后由StringUtils.join将其拼接起来

image-20231115231654132

2.5 分组校验

假如修改课程的Dto和增加课程的Dto是一个样子的,但是校验规则不一样,怎么办呢

方法1:定义两个Dto来分别校验

方法2:分组校验

分组校验:将Dto中的校验规则分组,新增课程使用一个检验规则组,修改课程使用一个校验规则组,但是都还是同一个Dto

定义分组

/**
 * 用于分组校验,定义一些常用的组
 */
public class ValidationGroups {

 public interface Insert{};
 public interface Update{};
 public interface Delete{};

}

划分组别

//    @NotEmpty(message = "课程名称不能为空")
//  这个地方就说明了下面@NotEmpty校验属于ValidationGroups.Insert.class组别
    @NotEmpty(message = "新增课程名称不能为空",groups = {ValidationGroups.Insert.class})
//  这个地方就说明了下面@NotEmpty校验属于ValidationGroups.Update.class组别
    @NotEmpty(message = "修改课程名称不能为空",groups = {ValidationGroups.Update.class})
    @ApiModelProperty(value = "课程名称", required = true)
    private String name;

此时Controller中也需要修改

    @ApiOperation("新增课程")
    @PostMapping("/course")
    public CourseBaseInfoDto createCourseBase(@Validated(ValidationGroups.Insert.class) @RequestBody AddCourseDto addCourseDto) {
//      将来会集成SpringSecurity框架,用户登录之后就可以获取到用户所属机构的ID
//      先把机构ID写死
        return courseBaseInfoService.createCourseBase(10086L,addCourseDto);
    }

image-20231115232957202

假如说是修改的话,如下所示

 public CourseBaseInfoDto  updateCourseBase(@Validated(ValidationGroups.Update.class) @RequestBody AddCourseDto addCourseDto) {
    ...
}

2.6 备注

如果javax.validation.constraints包下的校验规则满足不了需求怎么办?

1、手写校验代码 。

2、自定义校验规则注解。

三、全局异常处理2

上面的异常处理是在看学成在线的时候做的笔记,但是怎么看怎么别扭,特别是封装的响应结果集,很别扭,然后就再整理一份

系统如何处理异常

  1. 我们自定义一个统一的异常处理器去捕获并处理异常

  2. 使用控制器增加注解@ControllerAdvice和异常处理注解@ExceptionHandler来实现

  3. 处理自定义异常

    程序在编写代码时根据校验结果主动抛出自定义异常类对象,抛出异常时指定详细的异常信息,异常处理器捕获异常信息记录异常日志并响应给用户

  4. 处理自定义异常

    接口执行过程中的一些运行时异常也会由异常处理器统一捕获,记录异常日志,统一响应给用户500错误(状态码由前后端协定)

  5. 在异常处理中还可以针对某个异常类型进行单独处理

3.1 全局异常处理器

/**
 * 全局异常处理
 * 底层是通过代理,代理controller,通过AOP把我们的一些方法拦截到,如果有异常,就在这个类统一进行处理
 * 下面就是只要带有RestController.class, Controller.class,Service.class的注解的类或方法出现异常,我们都会进行统一处理
 */
@ControllerAdvice(annotations = {RestController.class, Controller.class, Service.class})  // 通知
@ResponseBody  //我们需要返回JSON数据
@Slf4j
public class GlobalExceptionHandler {
 
//  表示处理SQL异常
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
//      打印日志信息
        log.error(ex.getMessage());//Duplicate entry 'zhangjingqi' for key 'employee.idx_username'
 
//      在这里也可以判断异常的具体信息,比如:
        if(ex.getMessage().contains("Duplicate entry")){
            String[] s = ex.getMessage().split(" ");
            String username = s[2];
            String msg =s[2]+"已经存在";
           return   R.error(msg);
        }
//      其他情况下可以直接输出
        return R.error("未知错误:"+ex.getMessage());
    }
}
 

3.2 结果集

不论是成功响应还是异常响应,都使用下面这个结果集

/**
 * 通用返回结果类,服务端响应的数据都会封装成此对象
 * @param <T>  这个类会接受多种类型,可能是普通对象,可能是数组、集合等等等等,所以我们要将这个加个泛型<T>,表示可以接收任何参数
 */
//  为什么不用Object,而用<T>?  如果用object需要强转类型 而T不用
@Data
public class R<T> {
 
    private Integer code; //编码:1成功,0和其它数字为失败
 
    private String msg; //错误信息
 
    private T data; //数据
 
    private Map map = new HashMap(); //动态数据
 
//  方法的返回值及参数中的T属于泛型
    public static <T> R<T> success(T object) {
        R<T> r = new R<T>();
        r.data = object;
        r.code = 1;   //成功
        return r;
    }
 
    public static <T> R<T> error(String msg) {
        R r = new R();
        r.msg = msg;
        r.code = 0;   //失败
        return r;
    }
 
    public R<T> add(String key, Object value) {
        this.map.put(key, value);
        return this;
    }
 
}

3.3 常用注解

3.3.1 @ControllerAdvice

SpringMVC 中 @ControllerAdvice 注解的三种使用场景! - 江南一点雨 - 博客园 (cnblogs.com)

  • basePackages/basePackageClasses属性

    指定扫描哪个包

@ControllerAdvice(basePackages = "com.zhangjingqi.controller")
public class GlobalExceptionHandler {
    // ...
}

  • annotations属性

    指定注解类型,限定只有被特定注解标记的控制器才会受到 @ControllerAdvice 类的影响

@ControllerAdvice(annotations = {RestController.class, Controller.class, Service.class})  // 通知
@ResponseBody  //我们需要返回JSON数据
@Slf4j
public class GlobalExceptionHandler {
    .......
}
  • assignableTypes属性

    通过指定类类型,限定只有继承自特定类的控制器才会受到 @ControllerAdvice 类的影响

@ControllerAdvice(assignableTypes = MyController.class)
public class GlobalExceptionHandler {
    // ...
}

  • value

    basePackages 属性类似,用于指定扫描的包。在很多情况下,value 属性可以替代 basePackages

@ControllerAdvice(value = "com.zhangjingqi.controllers")
public class GlobalExceptionHandler {
    // ...
}

  • useDefaultResponseAdvice

    默认为 true。当设置为 false 时,禁用默认的 ResponseEntityExceptionHandler,这样你就可以完全掌控异常处理的行为

@ControllerAdvice(useDefaultResponseAdvice = false)
public class CustomExceptionHandler {
    // ...
}

3.3.2 @ExceptionHandle

可以捕获到controller中抛出的一些自定义异常,统一进行处理,一般用于进行一些特定的异常处理

可以根据需要定义多个 @ExceptionHandler 方法,每个方法处理一种特定类型的异常

//  表示处理SQL异常
    @ExceptionHandler(SQLIntegrityConstraintViolationException.class)
    public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex) {
        ......
    }
  • value

    指定要处理的异常类型。可以是单个异常类或一组异常类

@ExceptionHandler(value = {NullPointerException.class, IllegalArgumentException.class})
public String handleSpecificExceptions(Exception e) {
    // 处理特定类型的异常
    return "处理特定类型的异常";
}

  • exceptions

    value 属性类似,用于指定要处理的异常类型

@ExceptionHandler(exceptions = NullPointerException.class)
public String handleCustomException(Exception e) {
    // 处理自定义异常
    return "处理自定义异常";
}

  • basePackages/basePackageClasses

    限定 @ExceptionHandler 方法的扫描范围,类似于 @ControllerAdvice 的属性

@ExceptionHandler(basePackages = "com.zhangjingqi.controllers")
public String handleExceptionsInControllerPackage(Exception e) {
    // 处理指定包中的异常
    return "packageError";
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1315013.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

喝葡萄酒时观酒闻香尝味究竟有什么用?

对许多人来说&#xff0c;在品尝葡萄酒时能发现大多数人闻不到的香气和尝不到的味道似乎是一种神奇的能力。其他人则认为这是学究式葡萄酒爱好者过于活跃的想象&#xff0c;或者是保持葡萄酒鉴赏精英声誉的一种方式&#xff0c;但两者都不是。 部分是艺术&#xff0c;部分是科…

【异步绘制】UIView刷新原理 与 异步绘制

快捷目录 壹、 iOS界面刷新机制贰、浅谈UIView的刷新与绘制概述一.UIView 与 CALayer1. UIView 与 CALayer的关系2. CALayer的一些常用属性contents属性contentGravity属性contentsScale属性maskToBounds属性contentsRect属性 二.View的布局与显示1.图像显示原理2.布局layoutSu…

关于Ubuntu22.04恢复误删文件的记录

挂载在Ubuntu22.04下的固态盘有文件被误删了&#xff0c;该固态盘是ntfs格式的。 在网上找了很多教程&#xff0c;最后决定用TestDisk工具进行恢复。 现记录如下&#xff1a; Ubuntu安装testdisk sudo apt-get install testdisk运行testdisk sudo testdisk得到 我选择的是…

Vue3使用了Vite和UnoCSS导致前端项目启动报错:Error:EMFILE:too many open files

一个 Vue3 的项目&#xff0c;用的是 Vite 打包&#xff0c;通过 npm run dev 运行时&#xff0c;遇到了以下错误&#xff08;尤其是引入了 Element-Plus 后&#xff09;&#xff1a; Error: EMFILE: too many open files&#xff0c;后面是具体的文件路径。。甚至到了 node_mo…

面试官:这些大学生都会

大家好&#xff0c;我是 JavaPub。 最近有些同学在后台问我&#xff0c;面试总是会遇到被问 Linux 命令的问题&#xff0c;自己就面试个后端开发岗位&#xff0c;怎么这么难呢&#xff1f; 其实 Linux 命令&#xff0c;对于一个后端开发来说&#xff0c;并不是很难&#xff0c…

【DataSophon】大数据管理平台DataSophon-1.2.1基本使用

&#x1f984; 个人主页——&#x1f390;开着拖拉机回家_Linux,大数据运维-CSDN博客 &#x1f390;✨&#x1f341; &#x1fa81;&#x1f341;&#x1fa81;&#x1f341;&#x1fa81;&#x1f341;&#x1fa81;&#x1f341; &#x1fa81;&#x1f341;&#x1fa81;&am…

git 常见错误总结(会不断更新中。。)

常见错误 1. 配置部署key后git clone还是拉不下代码 执行以下命令 先添加 SSH 密钥到 SSH 代理&#xff1a; 如果你使用 SSH 代理&#xff08;例如 ssh-agent&#xff09;&#xff0c;将生成的私钥添加到代理中。 ssh-add ~/.ssh/gstplatrontend/id_rsa如果报错以下错误信息…

wps左上角有绿色小三角的数字如何求和

1.这个状态是求和不了的&#xff0c;使用求和公式求出来的也是0 2.进行如下操作 3.转换好后 则可以求和成功了

Orange Comet利用Sui Kiosk进行游戏道具和知识产权保护

Orange Comet与AMC合作开发基于《行尸走肉》系列的NFT和游戏&#xff0c;首要关注的问题就是保护AMC的知识产权。利用Sui的Kiosk原语不仅让Orange Comet向AMC保证其资产安全&#xff0c;而且为即将推出的《行尸走肉大陆》游戏打开了无限的可能性。 Kiosk是Sui上的一个原语&…

文物数字化建模纹理贴图

在线工具推荐&#xff1a; 3D数字孪生场景编辑器 - GLTF/GLB材质纹理编辑器 - 3D模型在线转换 - Three.js AI自动纹理开发包 - YOLO 虚幻合成数据生成器 - 三维模型预览图生成器 - 3D模型语义搜索引擎 1、文物3D数字化建模的特点 文物埋在地下历经千年&#xff0c;由于时…

拷贝的艺术:深拷贝与浅拷贝的区别与应用(下)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

Apple Find My「查找」认证芯片找哪家,认准伦茨科技ST17H6x芯片

深圳市伦茨科技有限公司&#xff08;以下简称“伦茨科技”&#xff09;发布ST17H6x Soc平台。成为继Nordic之后全球第二家取得Apple Find My「查找」认证的芯片厂家&#xff0c;该平台提供可通过Apple Find My认证的Apple查找&#xff08;Find My&#xff09;功能集成解决方案。…

STM32与Freertos入门(五)任务案例

1、实现功能 通过两个按键任务分别控制不同的点灯案例 创建 4 个任务&#xff1a;在点灯任务的基础上在创建两个按键任务&#xff1a; Task_led&#xff1a;间隔 500ms 闪烁 LE1&#xff1b; Task_led2&#xff1a;间隔 1000ms 闪烁 LED2&#xff1b; Task_key&#xff1a;如…

为了吃鸡苦练狙击,避免坑队友自己造一个狙击游戏!

引言 一文教会你造一个简易的狙击游戏。 说到狙击&#xff0c;相信大家都不陌生&#xff0c;无论是影视作品还是网络游戏&#xff0c;都经常能看到狙击枪的身影&#xff0c;最深刻的是它能够从百里之外&#xff0c;一枪爆头。 本文将介绍如何在Cocos Creator中造一个简易的狙…

Jenkins Pipeline 脚本优化实践:从繁琐到简洁

引言 在持续集成的过程中&#xff0c;Jenkins Pipeline 是非常关键的一环。它定义了如何自动编译、测试和部署代码。随着项目的不断发展&#xff0c;Pipeline 的复杂性也在不断上升&#xff0c;这就需要我们持续优化 Pipeline 脚本&#xff0c;以提高代码的可读性和维护性。本…

最好的猫粮排行榜前十名有哪些牌子?盘点好的主食冻干猫粮前五名牌子

现在很多猫咪因为吃了不好的猫粮&#xff0c;出现了各种问题&#xff0c;甚至有的还发生了悲剧&#xff0c;让猫主人心疼又无奈。要解决这个问题&#xff0c;选择一款健康又安全的主食冻干猫粮是非常关键的。优质的主食冻干猫粮不仅在配方上要健康&#xff0c;营养配比也要科学…

【深度学习】注意力机制(二)

本文介绍一些注意力机制的实现&#xff0c;包括EA/MHSA/SK/DA/EPSA。 【深度学习】注意力机制&#xff08;一&#xff09; 【深度学习】注意力机制&#xff08;三&#xff09; 目录 一、EA&#xff08;External Attention&#xff09; 二、Multi Head Self Attention 三、…

26 redis 中 replication/cluster 集群中的主从复制

前言 我们这里首先来看 redis 这边实现比较复杂的 replication集群模式 我们这里主要关注的是 redis 这边的主从同步的相关实现 这边相对比较简单, 我们直接基于 cluster集群模式 进行调试 主从命令同步复制 比如这里 master 是 redis_7002, slave 是 redis_7005 然后 这…

打开软木塞,我们来谈谈葡萄酒泡泡吧

香槟是任何庆祝场合的最佳搭配。从婚礼和生日到单身派对和典型的周五晚上&#xff0c;这款气泡饮料是生活中特别聚会的受欢迎伴侣。 来自云仓酒庄品牌雷盛红酒分享你知道吗&#xff0c;你喜欢喝的那瓶香槟酒可能根本不是香槟&#xff0c;而是汽酒&#xff1f;你不是唯一一个认…

6个超好用的小众图片素材网站,高清、免费,值得收藏~

推荐几个超好用的图片素材网站&#xff0c;免费下载&#xff0c;还可以商用&#xff0c;建议收藏哦~ 1、菜鸟图库 https://www.sucai999.com/pic.html?vNTYwNDUx 我推荐过很多次的设计素材网站&#xff0c;除了设计类素材&#xff0c;还有很多自媒体可以用到的高清图片、背景…