摘要:时间年月日星期三说明使用规范校验接口请求参数源码第一章理论简介背景介绍如今互联网项目都采用接口形式进行开发。该规范定义了一个元数据模型,默认的元数据来源是注解。
时间:2017年11月08日星期三
说明:使用JSR303规范校验http接口请求参数
源码:https://github.com/zccodere/s...
如今互联网项目都采用HTTP接口形式进行开发。无论是Web调用还是智能设备APP调用,只要约定好参数形式和规则就能够协同开发。返回值用得最多的就是JSON形式。服务端除了保证正常的业务功能,还要经常对传进来的参数进行验证,例如某些参数不能为空,字符串必须含有可见字符,数值必须大于0等这样的要求。
1-2 基础理论什么是JSR303规范
首先JSR 303是Java的标准规范。 根据官方文档的描述:在一个应用的不同层面(例如呈现层到持久层), 验证数据是一个是反复共同的任务。 许多时候相同的验证要在每一个独立的验证框架中出现很多次。 为了提升开发效率,阻止重复造轮子,于是形成了这样一套规范。 该规范定义了一个元数据模型,默认的元数据来源是注解(annotation)。
什么是AOP
是一种编程范式,不是编程语言 解决特定问题,不能解决所有问题 是OOP的补充,不是替代
JSR303定义的校验类型
空检查
@Null 验证对象是否为null @NotNull 验证对象是否不为null, 无法查检长度为0的字符串 @NotBlank 检查约束字符串是不是Null还有被Trim的长度是否大于0 @NotEmpty 检查约束元素是否为NULL或者是EMPTY.
Booelan检查
@AssertTrue 验证 Boolean 对象是否为 true @AssertFalse 验证 Boolean 对象是否为 false
长度检查
@Size(min=, max=) 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内 @Length(min=, max=) Validates that the annotated string is between min and max included.
日期检查
@Past 验证 Date 和 Calendar 对象是否在当前时间之前 @Future 验证 Date 和 Calendar 对象是否在当前时间之后 @Pattern 验证 String 对象是否符合正则表达式的规则
数值检查,建议使用在Stirng,Integer类型,不建议使用在int类型上,因为表单值为“”时无法转换为int,但可以转换为Stirng为"",Integer为null
@Min 验证 Number 和 String 对象是否大等于指定的值 @Max 验证 Number 和 String 对象是否小等于指定的值 @DecimalMax 被标注的值必须不大于约束中指定的最大值. 这个约束的参数是一个通过BigDecimal定义的最大值的字符串表示.小数存在精度 @DecimalMin 被标注的值必须不小于约束中指定的最小值. 这个约束的参数是一个通过BigDecimal定义的最小值的字符串表示.小数存在精度 @Digits 验证 Number 和 String 的构成是否合法 @Digits(integer=,fraction=) 验证字符串是否是符合指定格式的数字,interger指定整数精度,fraction指定小数精度。 @Range(min=, max=) 检查数字是否介于min和max之间. @Range(min=10000,max=50000,message="range.bean.wage")
private BigDecimal wage;
@Valid 递归的对关联对象进行校验, 如果关联对象是个集合或者数组,那么对其中的元素进行递归校验,如果是一个map,则对其中的值部分进行校验.(是否进行递归验证) @CreditCardNumber信用卡验证 @Email 验证是否是邮件地址,如果为null,不进行验证,算通过验证。 @ScriptAssert(lang= ,script=, alias=) @URL(protocol=,host=, port=,regexp=, flags=)
相关jar包
validation-api-1.0.0.GA.jar:接口规范 hibernate-validator-4.2.0.Final.jar是对上述接口的实现
Gradle坐标
compile ("javax.validation:validation-api:1.1.0.Final") compile ("org.hibernate:hibernate-validator:5.4.1.Final")第二章:简单实战 2-1 基于SpringMVC
工程创建
创建名为valid-mvc的gradle工程build.gradle如下
apply plugin: "war" apply plugin: "java" apply plugin: "eclipse" [compileJava, javadoc, compileTestJava]*.options*.encoding = "UTF-8" ext { springVersion = "4.3.8.RELEASE" } repositories { mavenLocal() maven{ url "http://maven.aliyun.com/nexus/content/groups/public" } mavenCentral() } dependencies { // Spring框架 compile ("org.springframework:spring-core:${springVersion}") compile ("org.springframework:spring-beans:${springVersion}") compile ("org.springframework:spring-context:${springVersion}") compile ("org.springframework:spring-web:${springVersion}") compile ("org.springframework:spring-webmvc:${springVersion}") compile ("org.springframework:spring-aop:${springVersion}") compile ("org.springframework:spring-aspects:${springVersion}") compile ("javax.servlet:javax.servlet-api:3.0.1") compile ("com.alibaba:fastjson:1.2.20") // JSR303数据校验 compile ("javax.validation:validation-api:1.1.0.Final") compile ("org.hibernate:hibernate-validator:5.4.1.Final") }
代码编写
1.编写WebInitializer类
package com.zccoder.valid.mvc.config; import java.util.EnumSet; import javax.servlet.DispatcherType; import javax.servlet.FilterRegistration; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRegistration.Dynamic; import org.springframework.web.WebApplicationInitializer; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.filter.CharacterEncodingFilter; import org.springframework.web.servlet.DispatcherServlet; /** * @Title apc-rest程序启动类 * @Description 当web容器启动项目的时候执行 * @author zc * @version 1.0 2017-11-08 */ public class WebInitializer implements WebApplicationInitializer { @Override public void onStartup(ServletContext servletContext) throws ServletException { // 新建WebApplication,注册配置类,并将其和当前servletContext关联。 AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); context.register(SpringConfig.class); context.setServletContext(servletContext); // 注册SpringMvc的DispatcherServlet。 Dynamic servlet = servletContext.addServlet("dispatcher", new DispatcherServlet(context)); servlet.addMapping("/"); servlet.setLoadOnStartup(1); // 注册SpringMVC的字符过滤器 FilterRegistration.Dynamic encodingFilter = servletContext.addFilter("encoding", new CharacterEncodingFilter()); EnumSetdispatcherTypes = EnumSet.allOf(DispatcherType.class); dispatcherTypes.add(DispatcherType.REQUEST); dispatcherTypes.add(DispatcherType.FORWARD); encodingFilter.addMappingForUrlPatterns(dispatcherTypes, true, "/*"); encodingFilter.setInitParameter("encoding", "utf-8"); } }
2.编写SpringConfig类
package com.zccoder.valid.mvc.config; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; import com.alibaba.fastjson.serializer.SerializerFeature; import com.alibaba.fastjson.support.config.FastJsonConfig; import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter; /** * @Title spring配置文件类 * @Description 配置spring的自动扫描 * @author zc * @version 1.0 2017-11-08 */ @Configuration @EnableWebMvc @EnableAspectJAutoProxy @ComponentScan("com.zccoder.valid.mvc") public class SpringConfig extends WebMvcConfigurerAdapter{ /** * 配置FASTJSON */ @Bean public FastJsonHttpMessageConverter fastJsonHttpMessageConverters() { FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter(); FastJsonConfig fastJsonConfig = new FastJsonConfig(); fastJsonConfig.setSerializerFeatures(SerializerFeature.QuoteFieldNames); fastJsonConfig.setCharset(Charset.forName("UTF-8")); fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss"); ListsupportedMediaTypes = new ArrayList (); supportedMediaTypes.add(MediaType.APPLICATION_JSON_UTF8); fastConverter.setSupportedMediaTypes(supportedMediaTypes); fastConverter.setFastJsonConfig(fastJsonConfig); return fastConverter; } /** * 配置JSON解析器 */ @Override public void configureMessageConverters(List > converters) { super.configureMessageConverters(converters); converters.add(this.fastJsonHttpMessageConverters()); } }
3.编写ValidAdvisor类
package com.zccoder.valid.mvc.config; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import javax.validation.Valid; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import com.zccoder.valid.mvc.constants.EnRespStatus; import com.zccoder.valid.mvc.vo.RspBaseVO; /** * @Title JSR303数据校验切面 * @Description 当校验不通过时,自动处理校验结果,停止当前请求,并响应错误提示 * @author zc * @version 1.0 2017-11-08 */ @Aspect @Component public class ValidAdvisor { // 通过@Pointcut注解声明切点。 @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)") public void annotationPointCut() { }; @Around("annotationPointCut()") public Object doTest(ProceedingJoinPoint pjp) throws Throwable{ // 获取方法签名 MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); Object[] args = pjp.getArgs(); // 实例化方法返回值类型 @SuppressWarnings("rawtypes") Class retuenClazz = signature.getReturnType(); Object retuenObject = retuenClazz.newInstance(); if(!(retuenObject instanceof RspBaseVO)){ throw new RuntimeException("方法"+method.getName()+"返回值类型非法"); } RspBaseVO rspVO = (RspBaseVO)retuenObject; // 遍历被拦截的方法上所有注解 Annotation[][] annotations = method.getParameterAnnotations(); for(int i = 0; i < annotations.length; i++){ if(!hasValidAnnotation(annotations[i])){ continue; } if(!(i < annotations.length-1 && args[i+1] instanceof BindingResult)){ //验证对象后面没有跟bindingResult,事实上如果没有应该到不了这一步 continue; } BindingResult result = (BindingResult) args[i+1]; if(result.hasErrors()){ FieldError fieldError = result.getFieldError(); rspVO.setEnRespStatus(EnRespStatus.PARAM_INVALID); rspVO.setRespDesc(fieldError.getField() + fieldError.getDefaultMessage()); return rspVO; } } return pjp.proceed(); } /** * 校验是否有@Valid注解 */ private boolean hasValidAnnotation(Annotation[] annotations){ if(annotations == null){ return false; } for(Annotation annotation : annotations){ if(annotation instanceof Valid){ return true; } } return false; } }
4.编写EnRespStatus类
package com.zccoder.valid.mvc.constants; public enum EnRespStatus { SUCCESS("0000","成功"), PARAM_INVALID("1000","缺少必传参数") ; private String respCode; private String respDesc; private EnRespStatus(String respCode,String respDesc){ this.respCode = respCode; this.respDesc = respDesc; } public String getRespCode() { return respCode; } public String getRespDesc() { return respDesc; } }
5.编写ReqBaseVO类
package com.zccoder.valid.mvc.vo; import java.io.Serializable; public class ReqBaseVO implements Serializable{ private static final long serialVersionUID = 7023512707419434863L; private String transId; private String systemCall; @Override public String toString() { return "ReqBaseVO [transId=" + transId + ", systemCall=" + systemCall + "]"; } public String getTransId() { return transId; } public void setTransId(String transId) { this.transId = transId; } public String getSystemCall() { return systemCall; } public void setSystemCall(String systemCall) { this.systemCall = systemCall; } }
6.编写RspBaseVO类
package com.zccoder.valid.mvc.vo; import java.io.Serializable; import com.zccoder.valid.mvc.constants.EnRespStatus; public class RspBaseVO implements Serializable{ private static final long serialVersionUID = 7023512707419434863L; private String respCode; private String respDesc; @Override public String toString() { return "RspBaseVO [respCode=" + respCode + ", respDesc=" + respDesc + "]"; } public void setEnRespStatus(EnRespStatus enRespStatus){ this.respCode = enRespStatus.getRespCode(); this.respDesc = enRespStatus.getRespDesc(); } public String getRespCode() { return respCode; } public void setRespCode(String respCode) { this.respCode = respCode; } public String getRespDesc() { return respDesc; } public void setRespDesc(String respDesc) { this.respDesc = respDesc; } }
7.编写StoreReqVO类
package com.zccoder.valid.mvc.vo; import javax.validation.constraints.Size; import org.hibernate.validator.constraints.NotBlank; public class StoreReqVO extends ReqBaseVO{ private static final long serialVersionUID = -342969591273353836L; @Size(min=2,max=5) @NotBlank private String name; @NotBlank private String desc; @NotBlank private String code; @Override public String toString() { return "StoreVO [name=" + name + ", desc=" + desc + ", code=" + code + "]"; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } }
8.编写StoreRspVO类
package com.zccoder.valid.mvc.vo; public class StoreRspVO extends RspBaseVO{ private static final long serialVersionUID = -342969591273353836L; private String id; @Override public String toString() { return "StoreRspVO [id=" + id + "]"; } public String getId() { return id; } public void setId(String id) { this.id = id; } }
9.编写StoreRest类
package com.zccoder.valid.mvc.rest; import javax.validation.Valid; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.zccoder.valid.mvc.constants.EnRespStatus; import com.zccoder.valid.mvc.vo.StoreReqVO; import com.zccoder.valid.mvc.vo.StoreRspVO; @RestController @RequestMapping("/store") public class StoreRest { @PostMapping("/create") public StoreRspVO create(@Valid StoreReqVO reqVO,BindingResult result){ StoreRspVO rspVO = new StoreRspVO(); System.out.println("注册成功:"+String.valueOf(reqVO.toString())); rspVO.setId(String.valueOf(System.currentTimeMillis())); rspVO.setEnRespStatus(EnRespStatus.SUCCESS); return rspVO; } }
进行验证
启动项目,并使用Postman测试效果如下
2-2 基于SpringBoot工程创建
创建名为valid-boot的gradle工程build.gradle如下
// 使用SpringBoot插件 buildscript { ext { springBootVersion = "1.5.7.RELEASE" } repositories { mavenCentral() } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") } } apply plugin: "java" apply plugin: "eclipse" apply plugin: "org.springframework.boot" repositories { mavenLocal() maven{ url "http://maven.aliyun.com/nexus/content/groups/public" } mavenCentral() } dependencies { compile("org.springframework.boot:spring-boot-starter-web") compile("org.springframework.boot:spring-boot-starter-aop") testCompile("org.springframework.boot:spring-boot-starter-test") }
代码编写
1.编写ValidBootStart类
package com.zccoder.valid.boot; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ValidBootStart { public static void main(String[] args) { SpringApplication.run(ValidBootStart.class, args); } }
2.编写ValidAdvisor类
package com.zccoder.valid.boot.aspect; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import javax.validation.Valid; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import com.zccoder.valid.boot.vo.EnRespStatus; import com.zccoder.valid.boot.vo.RspBaseVO; /** * @Title JSR303数据校验切面 * @Description 当校验不通过时,自动处理校验结果,停止当前请求,并响应错误提示 * @author zc * @version 1.0 2017-11-08 */ @Aspect @Component public class ValidAdvisor { // 通过@Pointcut注解声明切点。 @Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)") public void annotationPointCut() { }; @Around("annotationPointCut()") public Object doTest(ProceedingJoinPoint pjp) throws Throwable{ MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); @SuppressWarnings("rawtypes") Class retuenClazz = signature.getReturnType(); Object retuenObject = retuenClazz.newInstance(); if(!(retuenObject instanceof RspBaseVO)){ throw new RuntimeException("方法"+method.getName()+"返回值类型非法"); } RspBaseVO rspVO = (RspBaseVO)retuenObject; Object[] args = pjp.getArgs(); Annotation[][] annotations = method.getParameterAnnotations(); for(int i = 0; i < annotations.length; i++){ if(!hasValidAnnotation(annotations[i])){ continue; } if(!(i < annotations.length-1 && args[i+1] instanceof BindingResult)){ //验证对象后面没有跟bindingResult,事实上如果没有应该到不了这一步 continue; } BindingResult result = (BindingResult) args[i+1]; if(result.hasErrors()){ FieldError fieldError = result.getFieldError(); rspVO.setEnRespStatus(EnRespStatus.PARAM_INVALID); rspVO.setRespDesc(fieldError.getField() + fieldError.getDefaultMessage()); return rspVO; } } return pjp.proceed(); } /** * 校验是否有@Valid注解 */ private boolean hasValidAnnotation(Annotation[] annotations){ if(annotations == null){ return false; } for(Annotation annotation : annotations){ if(annotation instanceof Valid){ return true; } } return false; } }
3.编写EnRespStatus类
package com.zccoder.valid.boot.vo; public enum EnRespStatus { SUCCESS("0000","成功"), PARAM_INVALID("1000","缺少必传参数") ; private String respCode; private String respDesc; private EnRespStatus(String respCode,String respDesc){ this.respCode = respCode; this.respDesc = respDesc; } public String getRespCode() { return respCode; } public String getRespDesc() { return respDesc; } }
4.编写ReqBaseVO类
package com.zccoder.valid.boot.vo; import java.io.Serializable; public class ReqBaseVO implements Serializable{ private static final long serialVersionUID = 7023512707419434863L; private String transId; private String systemCall; @Override public String toString() { return "ReqBaseVO [transId=" + transId + ", systemCall=" + systemCall + "]"; } public String getTransId() { return transId; } public void setTransId(String transId) { this.transId = transId; } public String getSystemCall() { return systemCall; } public void setSystemCall(String systemCall) { this.systemCall = systemCall; } }
5.编写RspBaseVO类
package com.zccoder.valid.boot.vo; import java.io.Serializable; public class RspBaseVO implements Serializable{ private static final long serialVersionUID = 7023512707419434863L; private String respCode; private String respDesc; @Override public String toString() { return "RspBaseVO [respCode=" + respCode + ", respDesc=" + respDesc + "]"; } public void setEnRespStatus(EnRespStatus enRespStatus){ this.respCode = enRespStatus.getRespCode(); this.respDesc = enRespStatus.getRespDesc(); } public String getRespCode() { return respCode; } public void setRespCode(String respCode) { this.respCode = respCode; } public String getRespDesc() { return respDesc; } public void setRespDesc(String respDesc) { this.respDesc = respDesc; } }
6.编写StoreReqVO类
package com.zccoder.valid.boot.vo; import javax.validation.constraints.Size; import org.hibernate.validator.constraints.NotBlank; public class StoreReqVO extends ReqBaseVO{ private static final long serialVersionUID = -342969591273353836L; @Size(min=2,max=5) @NotBlank private String name; @NotBlank private String desc; @NotBlank private String code; @Override public String toString() { return "StoreVO [name=" + name + ", desc=" + desc + ", code=" + code + "]"; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getDesc() { return desc; } public void setDesc(String desc) { this.desc = desc; } }
7.编写StoreRspVO类
package com.zccoder.valid.boot.vo; public class StoreRspVO extends RspBaseVO{ private static final long serialVersionUID = -342969591273353836L; private String id; @Override public String toString() { return "StoreRspVO [id=" + id + "]"; } public String getId() { return id; } public void setId(String id) { this.id = id; } }
8.编写StoreRest类
package com.zccoder.valid.boot.rest; import javax.validation.Valid; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.zccoder.valid.boot.vo.EnRespStatus; import com.zccoder.valid.boot.vo.StoreReqVO; import com.zccoder.valid.boot.vo.StoreRspVO; @RestController @RequestMapping("/store") public class StoreRest { @PostMapping("/create") public StoreRspVO create(@Valid StoreReqVO reqVO,BindingResult result){ StoreRspVO rspVO = new StoreRspVO(); System.out.println("注册成功:"+String.valueOf(reqVO.toString())); rspVO.setId(String.valueOf(System.currentTimeMillis())); rspVO.setEnRespStatus(EnRespStatus.SUCCESS); return rspVO; } }
进行验证
启动项目,并使用Postman测试效果如下
参考资料
http://blog.csdn.net/chaijunk...
http://www.cnblogs.com/yangzh...
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/67982.html
摘要:我们有时需要对前端传过来的数据做校验,就可以使用。他可以使我们不用在每个编写校验代码,可以达到解耦的功能。本文环境为,框架使用。 我们有时需要对前端传过来的数据做校验,就可以使用spring validation。他可以使我们不用在每个Controller编写校验代码,可以达到解耦的功能。本文环境为jdk8,框架使用springboot 2.1.0.RELEASE。1.添加依赖 ...
摘要:本文主要介绍在中自动校验的机制。引入依赖我们使用构建应用来进行演示。在中校验数据值得注意的地方参数前需要加上注解,表明需要对其进行校验,而校验的信息会存放到其后的中。层改写方法限定需要进行校验,而方法则不做限制。 简介 JSR303/JSR-349,hibernate validation,spring validation之间的关系。JSR303是一项标准,JSR-349是其的升级版...
摘要:前言估计很多朋友都认为参数校验是客户端的职责,不关服务端的事。轻则导致服务器宕机,重则泄露数据。所以,这时就需要设置第二道关卡,服务端验证了。老项目的服务端校验不能为空不能为空看以上代码,就一个的校验就如此麻烦。 前言 估计很多朋友都认为参数校验是客户端的职责,不关服务端的事。其实这是错误的,学过 Web 安全的都知道,客户端的验证只是第一道关卡。它的参数验证并不是安全的,一旦被有心人...
摘要:参数校验代码见常规使用请求参数加上符合校验注解包括基本类型和自定义类。校验注解是在方法入参上,则需要在该方法所在的类上添加注解,在入参前或是在方法上添加启用校验注解都不生效。校验注解使用在方法入参上时,抛出的是异常。 JSR-303 参数校验 代码见validator-demo 1、常规使用 1.1、请求参数加上符合JSR-303校验注解(包括基本类型和自定义类)。如果请求参数是自定...
摘要:初步使用主要使用注解的方式对进行校验,第一个例子在需要校验的字段上指定约束条件然后在中可以这样调用,加上注解即可。如果校验失败,默认会返回框架的出错信息。指定到的分组名会全部进行校验,不指定的不校验。 Spring Boot - 表单校验(JSR303&Hibernate Validator) 回顾 Spring Boot - 初识 Hello World Spring Boot -...
阅读 2907·2021-11-23 09:51
阅读 3082·2021-11-15 11:39
阅读 2957·2021-11-09 09:47
阅读 2508·2019-08-30 13:49
阅读 2099·2019-08-30 13:09
阅读 3075·2019-08-29 16:10
阅读 3485·2019-08-26 17:04
阅读 939·2019-08-26 13:57