(四) Spring-MVC注解驱动开发

一、springMVC全注解开发步骤

1.导坐标:
spring-context
spring-webmvc
javax.servlet-api
javax.servlet.jsp-api
log4j
spring-context-support

2.配置SpringConfiguration(代替springapplication.xml)

@Configuration
@ConponentScan(value="cn.edu.szu.mvc",excludeFilters=@ConponentScan.Filter(type=FilterType.Annotation,classes=Controller.class))
public class SpringConfiguration{
}
3.配置SpringMvcConfiguration(代替springmvc.xml)
@Configuration
@ConponentScan("cn.edu.szu.mvc.controller")
public class SpringMvcConfiguration implements WebMvcConfigurer{
	
	//添加资源处理规则
	@Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/js/**")
                .addResourceLocations("/public")
                .resourceChain(true)
                .addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
    }
	
	//配置视图解析器
	@Bean
	public ViewResolver createViewResolver(){
	InternalResouceViewResolver viewResolver=new InternalResouceViewResolver();
	viewResolver.setPrefix("/WEB-INF/pages/");
	viewResolver.setSuffix(".jsp");
	return viewResolver;
	}
}

4.配置config(初始化spring和spring-ioc容器的配置类)
注意:
由于我们使用全注解开发,不再存在web.xml文件,根据Servlet3.0使用规范,在使用时都必须在对应的jar包的META-INF/services 目录创建一个名为javax.servlet.ServletContainerInitializer的文件,文件内容指定具体的ServletContainerInitializer实现类,那么,当web容器启动时就会运行这个初始化器做一些组件内的初始化工作。

spring在spring-web当中已经创建文件并指定好实现类org.springframework.web.SpringServletContainerInitializer
在这里插入图片描述
该类使用@HandlesTypes指定要加载到SpringServletContainerInitializer接口实现中的字节码,也就是说只要我们提供一个WebApplicationInitializer的实现类即可。
我们使用WebApplicationInitializer实现的一个抽象类AbstractAnnotationConfigDispatcherServletInitializer。来继承使用

public class config extends AbstractDispatcherServletInitializer{
/**
* 添加字符集过滤器
* @param servletContext
* @throws ServletException
*/
@Override
public void onStartup(ServletContext servletContext) throws ServletException
{
// servletContext.gets
super.onStartup(servletContext);
CharacterEncodingFilter characterEncodingFilter = new
CharacterEncodingFilter();
characterEncodingFilter.setEncoding("UTF-8");
FilterRegistration.Dynamic registry = servletContext.addFilter("characterEncodingFilter",characterEncodingFilter);
registry.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST),false,"/*");

}
/**
* 创建web的Ioc容器
* @return
*/
@Override
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext acw = new
AnnotationConfigWebApplicationContext();
acw.register(SpringMvcConfiguration.class);
return acw;
}
/**
* 配置servlet的映射
* @return
*/
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
/**
* 创建根容器(非web层的对象容器)
* @return
*/
@Override
protected WebApplicationContext createRootApplicationContext() {
AnnotationConfigWebApplicationContext acw = new
AnnotationConfigWebApplicationContext();
acw.register(SpringConfiguration.class);
return acw;
}
}

二、springMVC常用注解

1、基础注解

@Controller

将标注的类加入ioc容器当中,是@Conponent的衍生注解之一。

这类注解尽量遵循spring的规范。
持久层使用@Repository
业务层使用@Service
控制层使用@Controller
工具类使用@Conponent
配置类使用@Configuration

@RequesetMapping

源码:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
public @interface RequestMapping {
	//给请求URL提供一个名称。
    String name() default "";
	//指定url
    @AliasFor("path")
    String[] value() default {};
	//同上
    @AliasFor("value")
    String[] path() default {};
	
	/**
	* 用于指定当前的方法支持什么样的请求方式。它支持以下这些类型:
	* GET, POST, HEAD, OPTIONS, PUT, PATCH, DELETE, TRACE.
	* 这些值是通过RequestMethod枚举指定的。
	*/
    RequestMethod[] method() default {};
	
	/**
	* 用于指定限制请求参数的条件。它支持简单的表达式。要求请求参数key和value必须和配置的一模一样。
	* 例如:
	* params = {"accountName"},表示请求参数必须有accountName
	* params = {"moeny!100"},表示请求参数中money不能是100。
	*/
    String[] params() default {};


	/**
	* 用于指定限制请求消息头的条件。
	* 例如:
	* RequestMapping(value = "/something", headers = "content-type=text/*")
	*/
    String[] headers() default {};
    
	/**
	* 用于指定可以接收的请求正文类型(MIME类型)
	* 例如:
	* consumes = "text/plain"
	* consumes = {"text/plain", "application/*"}
	*/
    String[] consumes() default {};

	/**
	* produces:用于指定可以生成的响应正文类型。(MIME类型)
	* 例如:
	* produces = "text/plain"
	* produces = {"text/plain", "application/*"}
	* produces = MediaType.APPLICATION_JSON_UTF8_VALUE
	*/
    String[] produces() default {};
}

作用:
用于建立请求URL处理请求方法之间的对应关系。

注意:
属性只要出现2个或以上时,他们的关系是与的关系。表示必须同时满足条件。

出现位置:
写在类上:
请求URL的第一级访问目录。此处不写的话,就相当于应用的根目录。
它出现的目的是为了使我们的URL可以按照模块化管理,使我们的URL更加精细。
例如:
用户模块:
/user/add
/user/update
/user/delete
方法上:
请求URL的第二级访问目录。

衍生注解:
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
@PatchMapping

@RequestParam

源码:

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
	//指定映射参数名
    @AliasFor("name")
    String value() default "";
	//指定映射参数名
    @AliasFor("value")
    String name() default "";
	//改参数是否必须有值。为true时,参数没有值会报错。
    boolean required() default true;
	//指定参数没有值的时候的默认值
    String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
}

此注解是从请求正文中获取请求参数,给控制器方法形参赋值的。
当请求参数的名称和控制器方法形参变量名称一致时,无须使用此注解。
同时,当没有获取到请求参数时,此注解还可以给控制器方法形参提供默认值。
注意:
它只能出现在方法的参数上

示例:

@RequestMapping("useRequestParam")
public String useRequestParam(@RequestParam(value = "username",required 
	return "";
}

@InitBinder

源码:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InitBinder {
	//指定哪些参数可以进行绑定操作,若不指定则为所有参数。
    String[] value() default {};
}

作用:
用于初始化表单请求参数的数据绑定器。

使用:

//将表单提交的字符串日期转化为Date
@InitBinder("user")
public void dateBinder(WebDataBinder dataBinder){
dataBinder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}

注意:该注解声明在某个controller当中,只对该controller有效。

@DateTimeFormat

源码:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
public @interface DateTimeFormat {
    String style() default "SS";

    DateTimeFormat.ISO iso() default DateTimeFormat.ISO.NONE;
	//指定转换格式
    String pattern() default "";

    public static enum ISO {
        DATE,
        TIME,
        DATE_TIME,
        NONE;

        private ISO() {
        }
    }
}

作用:
实现表单日期字符串转为Date
使用:

首先在SpringMvcConfiguration添加@EableWebMvc。

@Configuration
@ConponentScan"..."@EableWebMvc
public class SpringMvcConfiguration{}

将该注解标注在需要转换的实体类的属性上。

public class User{
	...
	@DateTimeFormat("yyyy-MM-dd")
	privated Date birthday;
	...
}

@ControllerAdvice

源码:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
	//指定需要增强的控制器所在的包名
    @AliasFor("basePackages")
    String[] value() default {};
    
	//指定需要增强的控制器所在的包名
    @AliasFor("value")
    String[] basePackages() default {};
	
	//指定需要增强的类的字节码数组
    Class<?>[] basePackageClasses() default {};
    
	//指定特定的类型提供增强。
    Class<?>[] assignableTypes() default {};
    
	//用于指定给特定注解提供增强。
    Class<? extends Annotation>[] annotations() default {};
}

作用:
用于给控制器提供一个增强的通知。
以保证可以在多个控制器之间实现增强共享
它可以配合以下三个注解来用:
@exceptionhandler
@initbinder
@modeltattribute

使用:

@ControllerAdvice
public class InitBinderAdvice{
	@InitBinder("user")
	public void dateBinder(WebDataBinder dataBinder){
	dataBinder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
	}
}

@RequestHeader

源码:

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestHeader {
	// 用于指定请求消息头的名称。
    @AliasFor("name")
    String value() default "";
	// 用于指定请求消息头的名称。
    @AliasFor("value")
    String name() default "";
	//用于指定是否必须有此消息头。当取默认值时,没有此消息头会报错。
    boolean required() default true;
	//用于指定消息头的默认值。
    String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
}

作用:
类似@RequestParam,作用在方法参数上面。

@CookieValue

源码:

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CookieValue {
    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    boolean required() default true;

    String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
}

作用:
获取Cookie的值,作用在方法参数上面,类似@RequestParam。

@ModelAttribute

源码:

@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModelAttribute {
    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    boolean binding() default true;
}

作用:
它可以用于修饰方法,或者是参数。
当修饰方法时,表示执行控制器方法之前,被此注解修饰的方法都会执行。
当修饰参数时,用于获取指定的数据给参数赋值

使用:
由于被改注解修饰的方法会在控制器方法之前执行,故可以使用该注解修饰的方法对获取的参数进行预处理,处理得到的新参数放到model当中,再使用被@ModelAttribute修饰的参数获取到被处理过后的参数。

预处理参数有两种写法:
第一种:

@ModelAttribute
public void showModel(String oldName,Model model){
	//TODO--预处理
	model.addAttribute("newName",oldName);
}

第二种:

@ModelAttribute("newName")
public String showModel(String oldName){
	//TODO--预处理
	return oldName;
}

获取预处理后的参数:

@RequestMapping("/useModelAttribute")
public String useModelAttribute(@ModelAttribute("newName") String username) {
return "success";
}

@SessionAttribute和@SessionAttributes

源码:

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SessionAttribute {
    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    boolean required() default true;
}

作用:
从session域当中获取指定名称的session值。类似@RequestParam

源码:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface SessionAttributes {
	//指定存入session域当中的名称
    @AliasFor("names")
    String[] value() default {};

    @AliasFor("value")
    String[] names() default {};
	//指定类型
    Class<?>[] types() default {};
}

此注解是用于让开发者和ServletAPI进行解耦。
通过此注解即可实现把数据存入会话域,而无需在使用HttpSession的setAttribute方法。
注意:
当我们在控制器方法形参中加入Model或者ModelMap类型参数时,往model或者ModelMap当中存数据的操作是默认存入请求域的。而当控制器类上使用了此注解,以上操作在加入请求域当中的同时也会往session域中添加数据。

@ExceptionHandler

源码:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
	//指定需要捕获的异常类型
    Class<? extends Throwable>[] value() default {};
}

作用:
表明当前方法是控制器执行产生异常后的处理方法

用法:
异常捕获控制器当中添加类似方法

@ExceptionHandler(Exception.class)
public String handleException(Exception e){
System.err.println(e.getMessage());
return "error";
}

2、JSON数据交互相关注解

@RequestBody

源码:

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestBody {
    boolean required() default true;
}

作用:
获取全部的请求体

使用:

@RequestMapping("/useRequestBody")
public String useRequestBody(@RequestBody(required=false) String body){
System.out.println(body);
return "success";
}

一个应用场景:
对于以下的方法:

@RequestMapping("/add")
public String useRequestBody(User user){
	//TODO--
}

当请求的参数是json格式的数据时(如{“id”:1,“username”:“szu”,“password”:“123”})
spring无法将数据封装到user当中,此时可以使用@RequestBody获取请求体(jason数据的字符串格式),再结合jackson(jackson-core)工具类可以实现参数的封装。

@ResponseBody

源码:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseBody {
}

作用:
用该注解标注的方法返回值是用流输出的,也就是说在下面的这个例子当中就不是跳转到success的页面,而是返回一个字符串"success",但使用ajax异步请求的时候,得到的数据就是"success"这字符串。
使用:

 @RequestMapping("useResponseBody")
 //@ResponseBody
public @ResponseBody String useResponseBody(String name){
return "success";
}

写在方法上,表示这个方法的返回值用流输出。
写在类上表示该控制器所有的方法都用流输出。

@RestController

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
/**
* 用于指定存入ioc容器时bean的唯一标识。
*/
@AliasFor(annotation = Controller.class)
String value() default "";
}

作用:
它具备@Controller注解的全部功能,同时多了一个@ResponseBody注解的功能
该注解标注的类上所有的方法返回值都是以流输出。

@RestControllerAdvice

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
@AliasFor("basePackages")
String[] value() default {};
@AliasFor("value")
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
Class<?>[] assignableTypes() default {};
Class<? extends Annotation>[] annotations() default {};
}

作用:
它和@ControllerAdvice注解的作用一样,并且支持@ResponseBody的功能

当我们希望需要增强的方法返回值以流的方式输出时可使用该注解。

@RestControllerAdvice
public class ExceptionHandlerAdvice {
	@ExceptionHandler(Exception.class)
	public String handleException(Exception e){
	System.err.println(e.getMessage());
	return "error";
	}
}

3、Rest风格URL请求相关注解

REST即表述性状态传递(英文:Representational State Transfer,简称REST)是Roy Fielding博士在2000年他的博士论文中提出来的一种软件架构风格。它是一种针对网络应用的设计和开发方式,可以降低开发的复杂性,提高系统的可伸缩性。

传统方式:
http://localhost:8080/user/save POST
http://localhost:8080/user/update POST
http://localhost:8080/user/delete?id=1 GET
http://localhost:8080/user/find ?id=1 GET

REST
http://localhost:8080/user/ POST
http://localhost:8080/user/ PUT
http://localhost:8080/user/1 DELETE
http://localhost:8080/user/1 GET

@PathVariable

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PathVariable {

//指定url映射中占位符的名称
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
boolean required() default true;
}

作用:
它是springmvc框架支持rest风格url的标识。
它可以用于获取请求url映射中占位符对应的值。

使用:

@Controller
public class PathVariableController {
	@RequestMapping("/usePathVariable/{id}")
	public String usePathVariable(@PathVariable("id") Integer id){
	System.out.println(id);
	return "success";
	}
}

4、跨域访问

跨域访问即跨站 HTTP 请求(Cross-site HTTP request),它是指发起请求的资源所在域不同于该
请求所指向资源所在的域的 HTTP 请求。
比如说,域名A(http://www.itheima.example)的某 Web 应用程序中通过标签引入了域名
B(http://www.itheima.foo)站点的某图片资源(http://www.itheima.foo/image.jpg),域名A的
那 Web 应用就会导致浏览器发起一个跨站 HTTP 请求。在当今的 Web 开发中,使用跨站 HTTP 请求加载
各类资源(包括CSS、图片、JavaScript 脚本以及其它类资源),已经成为了一种普遍且流行的方式。

@CrossOrigin

源码:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {
    /** @deprecated */
    @Deprecated
    String[] DEFAULT_ORIGINS = new String[]{"*"};
    /** @deprecated */
    @Deprecated
    String[] DEFAULT_ALLOWED_HEADERS = new String[]{"*"};
    /** @deprecated */
    @Deprecated
    boolean DEFAULT_ALLOW_CREDENTIALS = false;
    /** @deprecated */
    @Deprecated
    long DEFAULT_MAX_AGE = 1800L;

    @AliasFor("origins")
    String[] value() default {};

    @AliasFor("value")
    String[] origins() default {};

    String[] originPatterns() default {};

    String[] allowedHeaders() default {};

    String[] exposedHeaders() default {};

    RequestMethod[] methods() default {};

    String allowCredentials() default "";

    long maxAge() default -1L;
}

作用:
指定是否支持跨域访问。

使用:

@RestController
@CrossOrigin
public class CrossOriginController {
@RequestMapping("/useCrossOrigin")
public String useCrossOrigin()throws Exception{
System.out.println("支持跨域访问");
return "success";
}
}

5、其他

纯注解开发配置拦截器步骤

1.编写拦截器类,并将类配置到ioc容器当中:

@Conponent
public class InterceptorDemo implements HandlerInterceptor {
	//控制器方法之前执行,对执行方法做前置增强。
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //TODO--
        return true;
    }
	
	//控制器方法执行之后,结果视图执行之前执行,可以对相应数据进行增强。
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(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);
    }
}

2.注册到InterceptorRegistry当中。

@Configuration
@ConponentScan()
public class SpringMvcConfiguration implements WebMvcConfigurer{
	@Autowired
    private InterceptorDemo interceptorDemo;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptorDemo);
    }
}

执行顺序:preHandle->控制器方法->postHandle->视图渲染->afterCompletion

当有多个拦截器的时候,执行顺序为在springMvc配置类当中用加入拦截器注册的顺序

 @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptorDemo1);//先执行
        registry.addInterceptor(interceptorDemo2);//后执行
    }

当有多个拦截器的时候,执行的方法顺序:
执行成功:
preHandle1 – true
preHandle2 --true
preHandle3 --true

preHandle(n) --true

控制器方法

postHandle(n)
postHandle(n-1)

postHandle1
afterCompletion(n)
afterCompletion(n-1)

afterCompletion1

执行失败:
preHandle1 – true
preHandle2 --true
preHandle3 --true

preHandle(n) --false
afterCompletion(n-1)
afterCompletion(n-2)

afterCompletion1

责任链模式

责任链模式是一种常见的行为模式。它是使多个对象都有处理请求的机会,从而避免了请求的发送者和接
收者之间的耦合关系。将这些对象串成一条链,并沿着这条链一直传递该请求,直到有对象处理它为止。
优势:
解耦了请求与处理;

请求处理者(节点对象)只需关注自己感兴趣的请求进行处理即可,对于不感兴趣的请求,直接转发给下一级节点对象;

具备链式传递处理请求功能,请求发送者无需知晓链路结构,只需等待请求处理结果;

链路结构灵活,可以通过改变链路结构动态地新增或删减责任;

易于扩展新的请求处理类(节点),符合 开闭原则;

弊端:
责任链路过长时,可能对请求传递处理效率有影响;

如果节点对象存在循环引用时,会造成死循环,导致系统崩溃;

类型转换器Converter

public interface Converter<S, T> {
/**
* 提供类型转换的逻辑
*/
@Nullable
T convert(S source);
}

定义转化器:

public class StringToDateConverter implements Converter<String,Date>{
	private String pattern;
	public void setPattern(String pattern) {
		this.pattern = pattern;
	}
	private DateFormat format;
	@Override
	public Date convert(String source) {
		try{
		//1.实例化format对象
		if(StringUtils.isEmpty(pattern)){
		pattern = "yyyy-MM-dd";
		}
		format = new SimpleDateFormat(pattern);
		//2.转换字符串
		return format.parse(source);
	}catch (Exception e){
		throw new IllegalArgumentException("给定的日期格式不对!");
	}
	}
}

注册类型转换器:

@ControllerAdvice
	public class InitBinderAdvice {
	@Autowired
	private Converter stringToDateConverter;

	@InitBinder
	public void initBinder(WebDataBinder dataBinder){
	ConversionService conversionService = dataBinder.getConversionService();
	if(conversionService instanceof GenericConversionService){
	GenericConversionService genericConversionService =(GenericConversionService)conversionService;
	genericConversionService.addConverter(stringToDateConverter);
		}
	}
}

异常处理器HandlerExceptionResovler

@Component
public class CustomeExceptionHandlerResovler implements HandlerExceptionResolver
{
	@Override
	public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
	//1.创建返回值
	ModelAndView mv = new ModelAndView();
	//2.判断当前的ex是系统异常还是业务异常
	CustomeException ce = null;
	if(ex instanceof CustomeException){
	//业务异常
	ce = (CustomeException)ex;
	//设置错误信息
	mv.addObject("errorMsg",ce.getMessage());
	}else{
	//系统异常
	//设置错误信息
	mv.addObject("errorMsg","服务器忙!"+ex.getMessage());
	//只输出系统异到控制台
	ex.printStackTrace();
	}
	//3.设置响应视图
	mv.setViewName("error");
	return mv;
}
}

SpringMVC文件上传

MultipartResolver

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值