springboot 入门

本文介绍了SpringBoot的基本概念,展示了如何创建SpringBoot项目。接着详细讲解了配置文件的使用,包括读取配置项的多种方式和多环境配置。此外,还涉及了参数获取、MyBatis集成、静态资源处理、文件上传、统一异常处理、拦截器、AOP和事务管理等内容,是SpringBoot初学者的实用指南。

springboot是什么

传统的开发模式下,无论是基于xml注解,都要做许多配置,如果项目中集成越多的其他框架,配置内容也会越多。为了让开发人员以最少的配置去开发应用,springboot诞生了。springboot的原则是约定大于配置能不配就不配)。

springboot还未spring项目提供了很多非功能特性,比如嵌入式tomcat。
创建springboot项目

点击File->New->Project,然后左侧选择Spring Initializr,输入相关项目信息,点击next,然后选择相关依赖,选择Spring Web,点击Create

在这里插入图片描述

springboot配置文件

实际开发中,经常需要用到自定义配置下面是以微信公众号开发时为例)。

/src/main/resources/application.yml

wechat:
 appid: wx123456789asdsad
 token: ca
 appSecret: a123b456c789d147e258f369
读取配置项的方式(一)

/src/main/java/com/asd/config/WeChat.java

@ConfigurationProperties("wechat") // 参数输入前缀。@ConfigurationProperties和@Value注解用于获取配置文件中的属性定义并绑定到Java Bean或属性中
@Component
public class WeChat {
	private String appId;
	private String token;
	private String appSecret;

	...getter setter
}

/src/main/java/com/asd/controller/HellowController.java

@RestController // 等同于@Controller + @ResponseBody
public class HellowController {
	
	// 注入WeChat实体类
	@Autowired
	private WeChat weChat;

	...
	@GetMapping("/getWeChat")
	public WeChat getWeChat() {
		return weChat;
	}
}

重启服务后,访问 localhost:8080/getWeChat :

{"appId":"wx123456789asdsad", "token":"ca", "appSecret":"a123b456c789d147e258f369"}
读取配置项的方式(二)

/src/main/java/com/asd/config/WeChatConfig.java

@Configuration // 标识这是一个配置类
public class WeChatConfig {

	@Bean // 这样的话springboot就自动帮我们注入weChat对象
	@ConfigurationProperties("wechat")
	public WeChat weChat() {
		return new WeChat();
	}
}

读取配置项的方式(三)

/src/main/java/com/asd/controller/HellowController.java

@RestController
public class HellowController {

	@Value("${wechat.appId}")
	private String appId;

	@Value("${wechat.token}")
	private String token;

	@Value("${wechat.appSecret}")
	private String appSecret;

	...
	@GetMapping("/getWeChat")
	public WeChat getWeChat() {
		WeChat weChat = new WeChat();
		weChat.setAppId(appId);
		weChat.setAppSecret(appSecret);
		weChat.setToken(token);
		return weChat;
	}
}
如果属性较少,可以用@Value。如果属性较多,建议用第一种或第二种。

在实际项目开发中,有时需要读取自定义的配置文件。新建自定义配置文件:

/src/main/resources/my.yml

wechat1:
 appid: wx123456789asdsad

/src/main/java/com/asd/config/MyWeChat.java

@PropertySource("my.yml")
@ConfigurationProperties("wechat1")
@Component
public class MyWeChat {
	private String appId;

	...getter setter
}

/src/main/java/com/asd/controller/HellowController.java

@RestController
public class HellowController {

	@Resource
	private MyWeChat myWeChat;

	...
	@GetMapping("/getWeChat2")
	public MyWeChat getWeChat2() {
		return myWeChat;
	}
}
springboot多环境配置

开发项目时,通常需要经历几个阶段。

  • 本地开发接口(本地开发环境,local)
  • 开发完后与前端做接口联调(前后端联调环境,dev)
  • 联调完后提交测试(测试环境,test)
  • 测试完后有些公司会预发布(预发布环境,pre)
  • 部署到线上(生产环境,prod)

不同的开发环境,属性配置一般都不一样。如果不做多环境配置,就得去频繁修改配置文件,这样有一定的安全隐患。比如在本地开发时,不小心连上线上数据库,这样会对线上数据库造成一定的数据污染。

在resources目录下创建各环境的配置文件,springboot在启动的过程中,首先会加载application.yml,其次去加载这N个不同环境配置文件中的某一个。

在application.yml中的属性名可以当做变量,即${}来进行引用。

application-local.yml

server:
 port: 8001

application-dev.yml

server:
 port: 8002

application.yml

wechat:
 appid: wx123456789asdsad
 token: ca
 appSecret: a123b456c789d147e258f369
 port: ${server.port}# 引用属性值,如果是引入local配置文件,其值就会是8001
spring:
 profiles:
  active: local #写你要启动的配置文件的后缀就行

获取参数

比如有实体类Student,他有name、age属性。
1)通过request对象获取参数

2)@RequestParam(针对请求头方式为x-www-form-urlencoded,比如form表单)

@GetMapping("/get")
public Student getById(@RequestParam Integer id,@RequestParam String name) {
	return id;
}
如果请求参数的name是id1,就得写成@RequestParam("id1")

3)@RequestBody(针对请求头方式为application/json)

@PostMapping("/save")
public Student save(@RequestBody Student student) {
	return student;
}

4)@PathVariable(接收url路径参数)

@GetMapping("/get/{id}")
public Student getById(@PathVariable Integer id) {
	return id;
}
若是{id1},与参数名id不一样,得写为@PathVariable("id1")

springboot集成mybatis

pom.xml

<dependency>
	<groupId>org.mybatis.spring.boot</groupId>
	<artifactId>mybatis-spring-boot-starter</artifactId>
	<version>2.1.3</version>
</dependency>

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>8.0.13</version>
</dependency>

application-local.yml

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/test?
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: wen
    hikari:
      connection-timeout: 30000
      maximum-pool-size: 30
      minimum-idle: 10
      max-lifetime: 6000
mybatis:
  configuration:
    #配置打印sql日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  #设置xml文件扫描路径。把所有配置文件放在resources/mapper下面
  mapper-locations: mapper/**/*.xml
在这个例子中application.yml目前引用的是application-local.yml。下同

最后,启动类要加注解@MapperScan("com.ca.mapper"),即扫描dao包里的接口。或在dao上加@Mapper注解。

springboot访问静态资源

resources/static里可存放静态资源文件,比如放一张图片后,localhost:8080/asd.jpg就能访问。

这是springboot默认存放静态资源的目录

若想访问自定义目录下的静态资源,比如新建resources/images目录,里面放图片。此时,需要对该文件夹做配置。

WebAppConfig.java

@Component
public class WebAppConfig implements WebMvcConfigurer {
	
	@Value("${upload.path}")
	private String uploadPath;// 下面文件上传时用

	@Resource
	private TokenInterceptor tokenInterceptor;// 下面拦截器时用

	@Override
	public void addResourceHandlers(ResourceHandlerRegistry registry) {
		registry.addResourceHandler("/static/**").addResourceLocations("classpath:/images/");

		// 下面文件上传时用,让匹配upload开头的URL,让他去找文件路径。
		registry.addResourceHandler("/upload/**").addResourceLocations("file:" + uploadPath);
	}

	// 配置拦截器,下面拦截器时用
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		// 默认拦截所有url
		// registry.addInterceptor(tokenInterceptor);// 注册拦截器

		// 但实际开发中,要对部分url放行。所以可以针对一些url进行匹配,匹配需要拦截的,即addPathPatterns("/**"),"/**"是所有url 
		// 也可以配置不需要拦截的url,比如对/student/...放行。
		registry.addInterceptor(tokenInterceptor).addPathPatterns("/**").excludePathPatterns("/student/*");
		
	}
}

WebMvcConfigurer配置类其实是Spring内部的一种配置方式,采用JavaBean的形式来代替传统的xml配置文件形式进行针对框架个性化定制,可以自定义一些Handler,Interceptor,ViewResolver,MessageConverter。基于java-based方式的spring mvc配置,需要创建一个配置类并实现WebMvcConfigurer 接口。

访问localhost:8080/static/test.jpg就行。

springboot上传文件

pom.xml

<!-- hutool -->
<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.7.13</version>
</dependency>

application-local.yml

upload:
  path: d:\\uploads\\

UploadController.java

@RestController
public class UploadController {

	// 文件上传的根路径
	@Value("${upload.path}")
	private String uploadPath;

	@PostMapping("/upload")
	public Result upload(MultipartFile file) throws IOException{

		// 获取文件名称
		String fileName = file.getOriginalFilename();
		// 获取文件后缀(通过hutool的FileUtil工具类,需要注意的是,这里没有“.”,比如只有jpg)
		String suffix = FileUtil.getSuffix(fileName);
		// 对文件名进行重命名(文件的子路径)
		// 可以指定文件名策略,比如:时间戳-UUID.文件后缀
		String url = shijianchuo + uuid + "." + suffix;
		// 文件上传的真实路径
		String filePath = uploadPath + url;
		
		// 获取文件的字节流
		InputStream inputStream = file.getInputStream();
		// 把这个字节流写入到保存路径
		FileUtil.writeFromStream(inputStream, filePath);

		// 把文件的子路径返回到前端
		return Result.success(url);
	}	
}

访问刚才上传的文件:localhost:8080/upload/上传的文件路径.jpg

统一异常处理

调用接口过程中,可能会发生各种异常。此时,springboot默认给前端响应500,但即便发生异常,也应该给前端做一个正常的响应,以及告诉前端响应错误的原因。

有人会说用try catch,但如果项目越来越大,每个方法都用try catch,代码就会越来越臃肿。springboot中可以对项目的异常做一个统一的拦截处理。

/exception/BusinessException.java

public class BusinessException extends RuntimeException { // 这是自定义异常类,要继承RuntimeException
	
	// 写一个构造方法,传递Msg
	public BusinessException(String msg) {
		super(msg);
	}
}

/exception/SystemExceptionHandler.java

// 对系统异常做一个统一的处理(全局异常处理器)。
@RestControllerAdvice // 等同于 @ControllerAdvice + @ResponseBody。此注解通过对异常的拦截实现了统一异常返回处理
public class SystemExceptionHandler {
	
	@ExceptionHandler(BusinessException.class) // 指定要拦截的异常类
	public Result handlerException(BusinessException e) {// 也可以当做参数使用
		// 当我们拦截到异常后,直接给前端返回
		return Result.fail(e.getMessage());
	}

	// 也可以拦截Exception
	@ExceptionHandler(Exception.class)
	public Result exception() {
		return Result.fail("系统异常");
	}

	// 在参数校验时使用
	// 在这里拦截MethodArgumentNotValidException异常
	@ExceptionHandler(MethodArgumentNotValidException.class)
	public Result methodArgumentNotValidException(MethodArgumentNotValidException e) {
		// 拿到所有的参数校验失败的提示
		List<String> errorMessage = e.getBindingResult()
			.getAllErrors() //获取所有的错误提示,返回的是集合对象
			.stream() // 对集合对象进行遍历
			.map(DefaultMessageSourceResolvable::getDefaultMessage) // 获取message
			.collect(Collectors.toList()) // 然后返回集合对象
		
		// 可以使用hutool的工具包,判断集合是否为空
		if (CollUtil.isNotEmpty(errorMessage)) {
			// 如果不为空,直接取他的第一个元素,得到错误提示
			String errorMsg = errorMessage.get(0);
			// 把错误提示信息返回到前端
			return Result.fail(errorMsg);
		}
		return Result.fail("系统异常");
	}

}

配置后,出现异常时,就会返回Result这个统一的restful返回信息了。

如果想拦截自定义的BusinessException类,在代码中写throw new BusinessException("错误");即可。

拦截器

拦截器的拦截对象是controller里的方法。
例子:判断客户端发送的请求头中,是否包含token,并且是否值为asd。

/interceptor/TokenInterceptor.java

@Component
public class TokenInterceptor implements HandlerInterceptor{

	// 当返回true时执行controller里面的代码,但返回false时不执行下面的postHandler afterCompletion方法。
	@Override
	public boolean preHandler(HttpServletRequest request, HttpServletResponse response, Object handler) {
		// 所以在这里面可以对token进行拦截
		String token = request.getHeader("token");// 获取请求头里面的Token 
		if (StrUtil.isBlank(token)) {
			throw new BusinessException("请求头未包含token");
		}
		if (!"asd".equals(token)) {
			throw new BusinessException("请求头参数错误");
		}
		return true;
	}

	// controller方法执行之后,并且视图未渲染之前进行调用。(在前后端分离开发时,没有试图这个概念,所以了解即可)
	@Override
	public void postHandler(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception{
		HandlerInterceptor.super.postHandler(request, response, handler, modelAndView);
	}
	
	// 在整个请求处理完毕之后进行回调,所以在这个方法里可以做一些线程资源的释放
	@Override
	public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
		HandlerInterceptor.super.afterCompletion(request, response, handler, ex)
	}
}

执行顺序是preHandler、controller的方法、postHandler、aferCompletion。

用注解方式

/anotation/NeadToken.java

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)// 设置此注解只能用于方法
public @interface NeedToken {
	
}

@Retention作用是定义被它所注解的注解保留多久,一共有三种策略,定义在RetentionPolicy枚举中.
source:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;被编译器忽略
class:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期
runtime:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在。

修改preHandler方法(当controller的方法加上这个注解时,我们才去判断他的请求头中是否有token。):

@Override
public boolean preHandler(HttpServletRequest request, HttpServletResponse response, Object handler) {

	// 判断handler是不是HandlerMethod	
	if (handler instanceof HandlerMethod) {
		// 转成HandlerMethod对象
		HandlerMethod handlerMethod = (HandlerMethod) handler;
		// 通过handlerMethod可以去获得方法上面的注解
		NeedToken needToken = handlerMethod.getMethod().getAnnotation(NeedToken.class);
		if (needToken != null) {
			// 不为空,说明方法用了这个注解,就去校验token
			String token = request.getHeader("token");// 获取请求头里面的Token 
			if (StrUtil.isBlank(token)) {
				throw new BusinessException("请求头未包含token");
			}
			if (!"asd".equals(token)) {
				throw new BusinessException("请求头参数错误");
			}

		}
	}
	return true;
}

测试时,为了方便,应该去把url匹配部分注释。

registry.addInterceptor(tokenInterceptor)
	//.addPathPatterns("/**")
	//.excludePathPatterns("/student/*");

springboot aop

aop五大通知:

  • 前置通知 before advice:在目标方法执行之前执行
  • 后置通知 after returning advice:在目标方法执行之后执行
  • 异常通知 after throwing advice:目标方法抛出异常后执行
  • 最终通知 after finally advice:在目标方法执行之后都会执行(发生异常时,不执行后置通知,但执行最终通知,这是这两的区别)
  • 环绕通知 around advice:可以在目标方法执行之前执行,也可以在目标方法执行之后执行。

pom.xml

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

/aop/LogAspect.java

@Aspect // 标识这是一个切面类
@Component
public class LogAspect {
	
	// @Pointcut用来标识切点
	// RestfulStudentController.*(),指RestfulStudentController里面的所有方法
	@Pointcut("execution(* com.ca.controller.RestfulStudentController.*())") 
	public void pointcut() {
		
	}	

	// 前置通知
	@Before("pointcut()") // "pointcut":加切点,加上pointcut这个方法
	public void before() {
		logger.info("执行before方法");
	}

	// 最终通知
	@After("pointcut()")
	public void after() {
		logger.info("执行after方法");
	}

	// 后置通知
	@AfterReturning("pointcut()")
	public void afterReturn() {
		logger.info("执行afterReturn方法");
	}

	// 异常通知
	@AfterThrowing("pointcut()")
	public void ex() {
		logger.info("执行ex方法");
	}
}

执行后:
没异常的话,顺序为before、控制器的接口方法、afterReturn、after。
有异常的话,顺序为before、控制器的接口方法、ex、after。

补充
@Pointcut("execution(* com.ca.controller.RestfulStudentController.*(..))") // 所有方法的所有参数.*(..)
public void pointcut() {
		
}

@Before("pointcut()")
public void before(JoinPoint joinPoint) {// 这里可以加参数 JoinPoint joinPoint(@After、@AfterReturning亦是)
	// 通过joinPoint可以获取目标方法的参数
	Object[] args = joinPoint.getArgs();

	// 还可以通过joinPoint获取目标方法
	MethodSignature methodSignature = (MethodSignature)joinPoint.getSignature();
	Method method = methodSignature.getMethod();// 获取目标方法

	logger.info("拦截目标方法:{}", method.getName());
	logger.info("目标方法参数:{}"JSONUtil.toJsonStr(args));// 转换为json后输出。这时候要取pointcut里修改为拦截有参数的方法。
	logger.info("执行before方法");
}

@AfterThrowing(value="pointcut()", throwing="exception")// 然后注解里要写上throwing="exception"
public void ex(Exception exception) {// 这里也可以传递异常对象,就能获取到是什么异常对象了
	// 之后可以打印异常
	logger.error(exception.getMessage(), exception);
	logger.info("执行ex方法");
}
环绕通知

顾名思义,就是对目标方法进行包围。使用环绕通知时,就不用使用其他通知了,可以统统注释掉。

@Around("pointcut()")
public void around(ProceedingJoinPoint proceedingJoinPoint) {// 里面可以传递 ProceedingJoinPoint proceedingJoinPoint 参数
	// 执行目标方法前执行前置通知
	System.out.println("执行前置通知");
	
	Object result;
	try {
		// 通过proceedingJoinPoint也可以参数和目标方法
		Object[] args = proceedingJoinPoint.getArgs();
		MethodSignature methodSignature = (MethodSignature)proceedingJoinPoint.getSignature();
		Method method = methodSignature.getMethod();
		logger.info("拦截目标方法:{}", method.getName());
		logger.info("目标方法参数:{}"JSONUtil.toJsonStr(args));	
	
		result = proceedingJoinPoint.proceed();// 执行目标方法
	}catch(Throwable e) {
		// 还可以捕获异常
		logger.error(e.getMessage(), e);
	}
	
	// 执行目标方法后执行后置通知
	System.out.println("执行后置通知");

	return result;
}

springboot事务

事务指的是多个操作(插入、更新、删除等)同时进行,要么同时成功或同时失败。

需要用到事务的地方,在方法上添加@Transactional

注意事项

1)比如service里有save和save1方法,本来save上有@Transactional。后来把save里面的代码和@Transactional都干到save1上,然后在save里调用save1。这时,若出现异常也不会回滚。因为@Transactional使用了aop,aop首先要拦截的方法是save,而不是save1,所以他并没有获取到事务注解,此时事务就直接交给数据库自动控制了。所以@Transactional得加到save上才行(然后干掉save1上@Transactional)。

2)假如@Transactional加到save,在save中使用try catch,此时就算有异常,也不会回滚。因为aop只看这个方法是否抛出异常,抛出了才回滚。所以在catch里throw ex;即可。

3)目前service里有save,加上了@Transactional。假如有service1,里面有save1。在service的save里调用service1.save1(),而service1的save1加了@Transactional(propagation = Propagation.REQUIRES_NEW),REQUIRES_NEW是表示每次都重新开启一个新的事务。此时,在save中做了三个操作(加管理员,加管理员角色,[刚刚追加的]加角色)能否同时成功或失败?执行后可以发现,save里有异常时前两个失败,但save1成功了,这是因为save1开启了新的事务,所以他自己就提交了。所以,要保证所有的操作都要在同一个事务里面。

4)创建自定义异常类TestException(继承了Exception)。然后在save里throw new TestException();,但发现不回滚。因为@Transactional默认拦截的异常需要继承RuntimeException。但可以通过@Transactional(rollbackFor = Exception.class)来解决。

参数校验

在controller里用传统的if方式校验参数时,若参数多,就会代码冗余。为了解决这个,有必要使用参数校验框架springboot validation。

他允许我们通过注解的方式来定义对象校验规则,把校验和业务逻辑分离。

springboot validation 常用注解如下:

  • @NotBlank(校验字符串不为null,并且不为空字符串)
  • @NotEmpty(校验字符串不为null,允许空字符串)
  • @NotNull(校验对象不为null)
  • @Length(校验字符串长度)
  • @Min(最小值校验)
  • @Max(最大值校验)
  • @Pattern(正则匹配校验),比如邮箱号、手机号时可以使用这个
  • @Valid(当一个对象嵌套另外一个对象时可使用)
    class Student {
    @Valid
    private Admin admin;
    }
    当我们使用学员类去接收参数时,我们也需要去校验Admin里面的参数的时候,就可以使用@Valid去进行校验。

pom.xml

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

Student.java

public class Student {
	@NotBlank(message="姓名不能为空")
	@Length(max = 5, message = "姓名不能超过5个字符")
	private String name;
	
	@NotNull
	@Max(value=100, message="年龄不能大于100岁")
	@Min(value=1, message="年龄不能小于1岁")
	private Integer age;

	@NotNull
	@Min(value = 1)
	@Max(value = 2)
	private Integer sex;
}

StudentController.java

@PostMapping
public Student save(@RequestBody @Valid Student student) {
	
}

也可以使用@Validated,这个是spring里面的注解,@Validjavax里面的注解,这两个是都可以的,但有点区别。

运行后,若参数有问题,就会校验不通过,抛出MethodArgumentNotValidException异常,但不告诉前端是什么问题。

当然,可以用全局异常拦截器来解决这个问题。
校验模式

springboot validation分两种校验模式,全校验快速校验推荐)。

全校验指的是对所有的参数都校验完毕之后进行返回,快速校验是指只要遇到接口参数校验失败就立即返回。

使用快速校验模式时,需要做一个单独的配置,只需要把下面代码复制到项目即可。

BeanConfig.java

@Configuration
public class BeanConfig {
	// 配置快速校验模式
	@Bean
	public Validator validator() {
		ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
			.configure()
			.failFast(true)// 配置校验失败就立即停止
			.buildValidatorFactory();
		Validator validator = validatorFactory.getValidator();
		return validator;
	}
}
其他

使用springboot validator时,可能会遇到一个问题,比如save和update时,若都用Student来接收参数的话,save时虽然不需要做id校验,但update时则需要做id校验。此时,可对校验做分组。

比如@NotNull(groups = Update.class)groups = 这里要设置一个类(得是接口),所以先创建两个接口。若设置为groups = Update.class就说明要在修改时才做校验。

/valid/Save.java

public interface Save {

}

/valid/Update.java

public interface Update {

}

Student.java

public class Student {
	@NotNull(groups=Update.class)
	private Integer id;

	@NotBlank(message="姓名不能为空", groups={Save.class, Update.class})// 也可以指定多个分组
	@Length(max = 5, message = "姓名不能超过5个字符", groups=Save.class)
	private String name;
	
	@NotNull
	@Max(value=100, message="年龄不能大于100岁", groups=Save.class)
	@Min(value=1, message="年龄不能小于1岁", groups=Save.class)
	private Integer age;

	@NotNull
	@Min(value = 1, groups=Save.class)
	@Max(value = 2, groups=Save.class)
	private Integer sex;
}

然后还要把controller里的@Valid改为springboot里的@Validated(Save.class),即也指定了分组。

StudentController.java

@PostMapping
public Student save(@RequestBody @Validated(Save.class) Student student) {
	
}

@PutMapping
public Student update(@RequestBody @Validated(Update.class) Student student) {

}

以上内容源于网络。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值