【狂人小白】轻量级验证框架 Validation.FO 的入门与使用

简介:

Validation.FO 使用指南

框架说明

  1. 这是一个验证框架,并且是一个 独立的验证框架 ,不依赖与其他已有的框架;

  2. 可以自由的嵌入到其他框架,比如Spring、Struts等流行框架,但实质来说他是独立的,所以无所谓嵌入到哪里,如果需要在GUI桌面应用中,也是完美的;

  3. 配置简单,可自由扩展验证器,实际只要实现IValidator接口,以及在rules.fo.xml中添加相关的配置即可;

  4. 支持Spring接口

  5. 使用过程中,你会感觉好像只用了 IValidateService.validate() 一个方法,这会让人感觉良好

优点

  1. 不与任何对象做绑定,最大限度解耦

  2. 只要规则配置写的合理,规则可以复用到多个对象

  3. 扩展验证器很简单

  4. 以Map存储验证结果,非常简单的导出JSON,只要你愿意

相关文档

如何下载

1
2
3
4
5
< dependency >
     < groupId >cc.fozone.validation</ groupId >
     < artifactId >fo-validation</ artifactId >
     < version >0.9.1.RELEASE</ version >
</ dependency >

快速开始

使用Validation.FO的步骤如下:

  1. 配置验证规则 rules.fo.xml

  2. 实例化 IValidateService 对象

  3. 调用 IValidateService.validate 方法

基本的使用方法

源代码地址:Basic Usage

1. 创建POJO对象 User.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package  validationfo.basic;
 
import  java.sql.Timestamp;
 
/**
  * 用户对象
  * @author Jimmy Song
  *
  */
public  class  User {
     // 用户名
     private  String username;
     
     // 密码
     private  String password;
     // 再次输入密码
     private  String passwordOne;
     
     // 邮箱
     private  String email;
     
     // 开始与结束时间
     private  Timestamp starttime;
     private  Timestamp endtime;
     
     /**
     * Setter & Getter
     */
     ...
}

2. 配置验证规则 rules.fo.xml

  • <include file=""/>
    导入其他规则文件

  • <group name=""></group>
    验证分组,name应该为全局唯一的

  • <field name=""></field>
    验证的字段,name是被验证对象的属性字段名

  • <rule name="" message=""></rule>
    验证规则,name是验证器名称,message是错误后返回的消息

  • <param name="" value=""></param>
    是验证规则时可能需要传入的参数,name是参数名,value是参数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<? xml  version = "1.0"  encoding = "UTF-8" ?>
< fozone-validation >
     <!-- include 标签导入其他配置 -->
     < include  file = "validationinfo/basic/another-rules.fo.xml" />
      
     <!-- 验证组ID,全局唯一 -->
     < group  name = "user.validate" >
         <!-- 验证字段 -->
         < field  name = "email" >
             <!-- 
                 规则列表
             -->
             < rule  name = "required"  message = "邮件必须填写" />
         < rule  name = "between"  message = "邮件长度应该3-100之间" >
             < param  name = "min"  value = "3" />
             < param  name = "max"  value = "100" />
         </ rule >
             < rule  name = "match"  message = "邮件格式不正确" >
                 < param  name = "regex"  value = "^[A-Za-z]+[\.\-_A-Za-z0-9]*@[A-Za-z0-9]+[\.\-_A-Za-z0-9]*$" />
             </ rule >
         </ field >
 
         ...
 
     </ group >
</ fozone-validation >

3. 实例化测试 BasicTest.java

主要步骤:

  1. 创建配置读取对象IValidateConfig

  2. 创建验证服务对象IValidateService

  3. 执行验证方法IValidateService.validate(object, groupId)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package  validationfo.basic;
 
...
 
/**
  * 最基本的测试
  * @author Jimmy Song
  *
  */
public  class  BasicTest {
     public  static  void  main(String[] args) {
         /**
          * Validation.FO的配置资源
          */
         // 验证器配置,系统默认配置
         String validatorsXML =  "validationfo/basic/validators.fo.xml" ;
         // 规则配置
         String rulesXML =  "validationfo/basic/rules.fo.xml" ;
         
         /**
          * 实例化配置对象
          */
         IValidateConfig config = new  BasicValidateConfig(validatorsXML, rulesXML);
         /**
          * 实例化验证服务层
          */
         IValidateService validateService =  new  BasicValidateService(config);
         
         // 实例化用户
         User user = createUser();
         
         /**
          * 执行验证
          */
         Map<String,String> map = validateService.validate(user,  "user.validate" );
         // 输出结果
         if (map ==  null  || map.size() ==  0 ) {
             System.out.println( "验证成功" );
         else  {
             System.out.println( "验证失败,结果如下" );
             System.out.println(map);
         }
     }
}

4. 结果输出

1
2
3
12 : 31 : 41 , 084   INFO BasicValidateConfig: 44  - read validation main file , validationfo/basic/rules.fo.xml
验证失败,结果如下
{email=邮件格式不正确, password=两次密码输入不正确, starttime=开始时间不能大于结束时间, endtime=结束时间不能小于开始时间}

如何在Spring中使用

  1. 在Spring中需要使用 SpringValidateConfg 配置对象

  2. 需要额外配置 SpringValidator Bean对象

  3. Spring DEMO 源代码下载

1. 配置Spring配置 context.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<? xml  version = "1.0"  encoding = "UTF-8" ?>
< beans  xmlns = "http://www.springframework.org/schema/beans"
     xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance" >
 
     <!-- 基于 Spring 配置读取 -->
     < bean  id = "springValidateConfig"  class = "cc.fozone.validation.config.SpringValidateConfig" >
         < property  name = "validators" >
             < value >validationfo/spring/validators.fo.xml</ value >
         </ property >
         < property  name = "rules" >
             < value >validationfo/spring/rules.fo.xml</ value >
         </ property >
     </ bean >
     
     <!-- 配置验证服务 -->
     < bean  id = "basicValidateService"  class = "cc.fozone.validation.BasicValidateService" >
         < constructor-arg  index = "0"  ref = "springValidateConfig" />
     </ bean >
     
     <!-- 配置基于Spring的验证器 -->
     < bean  class = "cc.fozone.validation.validators.SpringValidator" />
</ beans >

2. 如何通过Spring进行验证,SpringTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public  class  SpringTest {
     public  static  void  main(String[] args) {
         ClassPathXmlApplicationContext context =  new  ClassPathXmlApplicationContext(
                 "validationfo/spring/context.xml" );
 
         // 获取验证服务
         IValidateService service = context.getBean(IValidateService. class );
         // 创建用户对象
         User user = createUser();
 
         // 执行验证
         Map<String, String> map = service.validate(user,  "user.validate" );
         // 输出结果
         if  (map ==  null  || map.size() ==  0 ) {
             System.out.println( "验证成功" );
         else  {
             System.out.println( "验证失败,结果如下" );
             System.out.println(map);
         }
     }
}

3. 执行结果

1
2
3
4
5
6
13 : 26 : 00 , 150   INFO ClassPathXmlApplicationContext: 510  - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext @283b4947 : startup date [Tue Oct  07  13 : 26 : 00  CST  2014 ]; root of context hierarchy
13 : 26 : 00 , 194   INFO XmlBeanDefinitionReader: 315  - Loading XML bean definitions from  class  path resource [validationfo/spring/context.xml]
13 : 26 : 00 , 373   INFO DefaultListableBeanFactory: 598  - Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory @212b0f8a : defining beans [springValidateConfig,basicValidateService,cc.fozone.validation.validators.SpringValidator# 0 ]; root of factory hierarchy
13 : 26 : 00 , 442   INFO BasicValidateConfig: 44  - read validation main file , validationfo/spring/rules.fo.xml
验证失败,结果如下
{email=邮件格式不正确, password=两次密码输入不正确, starttime=开始时间不能大于结束时间, endtime=结束时间不能小于开始时间}

Validator验证器与规则

系统默认的验证器文件 validators.fo.xml

1
2
3
4
5
6
7
8
9
10
11
12
<? xml  version = "1.0"  encoding = "UTF-8" ?>
< fozone-validators >
     < validator  name = "required"  class = "cc.fozone.validation.validators.RequiredValidator" />
     < validator  name = "match"  class = "cc.fozone.validation.validators.MatchValidator" />
     < validator  name = "between"  class = "cc.fozone.validation.validators.BetweenValidator" />
     < validator  name = "min"  class = "cc.fozone.validation.validators.MinValidator" />
     < validator  name = "max"  class = "cc.fozone.validation.validators.MaxValidator" />
     < validator  name = "equals"  class = "cc.fozone.validation.validators.EqualsValidator" />
     < validator  name = "timestampLessEqual"  class = "cc.fozone.validation.validators.TimestampLessEqualValidator" />
     < validator  name = "timestampCreaterEqual"  class = "cc.fozone.validation.validators.TimestampCreaterEqualValidator" />
     < validator  name = "spring"   useSpring = "true"  class = "cc.fozone.validation.validators.SpringValidator" />
</ fozone-validators >

默认验证规则

required - 必填字段

消息message: 必填

参数param: 无

案例:

1
2
3
4
5
< group  name = "user.validate" >
     < field  name = "username" >
         < rule  name = "required"  message = "姓名必须填写。" />
     </ field >
</ group >

match - 正则匹配

消息message: 必填

参数param: 

Name Must Description
regex Yes 正则表达式,如果为空,验证直接返回true

案例:

1
2
3
4
5
6
7
< group  name = "user.validate" >
     < field  name = "email" >
         < rule  name = "match"  message = "邮件格式不正确" >
             < param  name = "regex"  value = "^[A-Za-z]+[\.\-_A-Za-z0-9]*@[A-Za-z0-9]+[\.\-_A-Za-z0-9]*$" />
         </ rule >
     </ field >
</ group >

between - 判断字符串或数组非空长度是否介于两者之间,min <= length <= max

消息message: 必填

参数param: 

Name Must Description
min Yes 最小长度(包含)
max Yes 最大长度(包含)

案例:

1
2
3
4
5
6
7
8
< group  name = "user.validate" >
     < field  name = "email" >
         < rule  name = "between"  message = "邮件长度应该3-100之间" >
             < param  name = "min"  value = "3" />
             < param  name = "max"  value = "100" />
         </ rule >
     </ field >
</ group >

min - 判断字符串或数组非空长度是否大于等于最小长度,length >= min

消息message: 必填

参数param: 

Name Must Description
value Yes 最小长度(包含)

案例:

1
2
3
4
5
6
7
< group  name = "user.validate" >
     < field  name = "password" >
         < rule  name = "min"  message = "密码至少5个字符" >
             < param  name = "value"  value = "5" />
         </ rule >
     </ field >
</ group >

max - 判断字符串或数组非空长度是否小于等于最大长度,length <= max

消息message: 必填

参数param: 

Name Must Description
value Yes 最大长度(包含)

案例:

1
2
3
4
5
6
7
< group  name = "user.validate" >
     < field  name = "password" >
         < rule  name = "max"  message = "密码最多20个字符" >
             < param  name = "value"  value = "20" />
         </ rule >
     </ field >
</ group >

equals - 判断字段是否与指定的字段值是否相同

消息message: 必填

参数param: 

Name Must Description
target Yes 指定的字段名称,并非确定的值

案例:

POJO对象部分字段

1
2
3
4
public  class  User {
     private  String password;
     private  String passwordOne;
}
1
2
3
4
5
6
7
< group  name = "user.validate" >
     < field  name = "password" >
         < rule  name = "equals"  message = "两次输入的密码不相同" >
             < param  name = "target"  value = "passwordOne" />
         </ rule >
     </ field >
</ group >

注:这里会将password与目标passwordOne字段的值进行equals比较
有些时候你可能需要重写equals方法


timestampLessEqual - 时间戳是否小于等于指定的目标时间字段

消息message: 必填

参数param: 

Name Must Description
target Yes 指定的时间字段名称,并非确定的值

案例:

POJO对象部分字段

1
2
3
4
public  class  User {
     private  Timestamp starttime;
     private  Timestamp endtime;
}
1
2
3
4
5
6
7
< group  name = "user.validate" >
     < field  name = "starttime" >
         < rule  name = "timestampLessEqual"  message = "开始时间不能大于结束时间" >
             < param  name = "target"  value = "endtime" />
         </ rule >
     </ field >
</ group >

注:

  1. 这里会将starttime与目标endtime字段的值进行大小比较

  2. 这里的字段必须是java.sql.Timestamp类型


timestampCreaterEqual - 时间戳是否大于等于指定的目标时间字段

消息message: 必填

参数param: 

Name Must Description
target Yes 指定的时间字段名称,并非确定的值

案例:

POJO对象部分字段

1
2
3
4
public  class  User {
     private  Timestamp starttime;
     private  Timestamp endtime;
}
1
2
3
4
5
6
7
< group  name = "user.validate" >
     < field  name = "endtime" >
         < rule  name = "timestampCreaterEqual"  message = "结束时间不能小于开始时间" >
             < param  name = "target"  value = "starttime" />
         </ rule >
     </ field >
</ group >

注:

  1. 这里会将endtime与目标starttime字段的值进行大小比较

  2. 这里的字段必须是java.sql.Timestamp类型


spring - 通过Spring调用其他对象的指定方法进行判断

DEMO: 点击查看

消息message: 必填

参数param: 

Name Must Description
beanName Yes Spring Context中的Bean标识
methodName Yes Bean对象中的指定执行方法,该方法必须返回 boolean 类型
parameterName No 执行方法中传递的参数,**默认** 传递被验证字段的值,如果使用**this** 表示传递被验证的对象, 其他 表示传递对象指定的字段

案例:

POJO对象部分字段

1
2
3
4
public  class  User {
     private  String username;
     private  int  usernameLength;
}

验证配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
< group  name = "user.validate" >
     < field  name = "username" >
         < rule  name = "spring"  message = "该用户已存在" >
             < param  name = "beanName"  value = "userService" />
             < param  name = "methodName"  value = "exist" />
         </ rule >
     </ field >
     
     < field  name = "usernameLength" >
         <!-- 使用 this 关键字 -->
         < rule  name = "spring"  message = "用户名长度验证错误" >
             < param  name = "beanName"  value = "userService" />
             < param  name = "methodName"  value = "length" />
             < param  name = "parameter"  value = "this" />
         </ rule >
         
         <!-- 指定字段 -->
         < rule  name = "spring"  message = "用户名长度不能小于5个字符" >
             < param  name = "beanName"  value = "userService" />
             < param  name = "methodName"  value = "lengthByName" />
             < param  name = "parameter"  value = "username" />
         </ rule >
     </ field >
</ group >

Spring中的配置

1
2
3
4
< beans >
     <!-- 通过Spring进行验证 -->
     < bean  id = "userService"  class = "validationfo.springadvance.UserService" />
</ beans >

通过Spring验证的接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package  validationfo.springadvance;
 
public  class  UserService {
     public  boolean  exist(String value) {
         return  ! "superman" .equals(value);
     }
     
     public  boolean  length(User user){
         return  user.getUsername().length() == user.getUsernameLength();
     }
     
     public  boolean  lengthByName(String username){
         return  username.length() >=  5 ;
     }
}

注:

  1. 要启用spring配置,需要参照上述的 如何在Spring使用 进行Spring与Validation.FO的整合配置

  2. 指定的对象方法必须返回 boolean 类型

高级部分:如何自定义验证器 IValidator

  1. 实现 IValidator 接口

  2. 在 validators.fo.xml 验证器配置中添加已实现的验证器

  3. 点击这里查看Demo源代码

一个简单的例子

POJO对象

1
2
3
4
5
6
7
8
9
package  validationfo.custom;
 
public  class  Money {
     private  int  number;
     private  String text;
     
     ...
     
}

CustomValidator实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package  validationfo.custom;
/**
  * 构造一个判断 参数 是否能够被 指定的值 整出的验证器
  * 例如:对象值为4 ,验证参数为2,那么 4可以被2整除,表示成功
 
  * @author Jimmy Song
  *
  */
public  class  CustomValidator  implements  IValidator {
     private  static  final  Logger logger = Logger.getLogger(CustomValidator. class );
     
     /**
      * 要处理的对象
      * @param context 上下文
      * @param type 对象类型
      * @param value 对象值
      * @param rule 对象参数
      * @return 是否成功
      */
     public  boolean  execute(Object context, Class type, Object value, Rule rule) {
         // TODO Auto-generated method stub
         /**
          * 这里只是演示一下context的用处
          * 可以通过context以及反射获取对象的属性值
          * 举个例子
          */
         try  {
             System.out.println( "TEXT = " +PropertyUtils.getProperty(context,  "text" ));
         catch  (Exception e1) {
             // TODO Auto-generated catch block
         }
         
         /**
          * 通过 value 实际验证的值
          */
         if (value ==  null return  true ;
         
         /**
          * 通过 type 获取值的类型
          */
         if (!(type == Integer. class  || type ==  int . class )) {
             return  false ;
         }
         int  objectValue = ((Integer)value).intValue();
         
         
         /**
          * 通过 rule 获取规则参数
          */
         String toValue = rule.getParameter( "value" );
         if (StringUtils.isBlank(toValue))  return  false ;
         
         int  intValue = - 1 ;
         try  {
             intValue = Integer.parseInt(toValue);
         catch (NumberFormatException e){
             logger.error( "倍数值转换错误!" );
             return  false ;
         }
         
         if (intValue <=  0 ) {
             logger.warn( "倍数值不能小于等于0" );
             return  false ;
         }
         
         return  objectValue % intValue ==  0 ;
     }
 
}

配置 validators.fo.xml 中添加自定义验证器

关于validators.fo.xml配置的参数说明

语法 : <validator name="" useSpring="true|false"  class=""  beanId=""/>

参数名 必填 含义
name 验证器名称
useSpring 是否通过 Spring 实例化验证器,这样做可以为验证器注入 ApplicationContext
class 当 useSpring=false 时,按照正常的方式进行实例化;当 useSpring=true 时,且使用了 beanId ,则 class 无效,如果没有使用 beanId ,那么 Spring 通过 class 进行实例化
beanId Spring中的bean id
1
2
3
4
5
<? xml  version = "1.0"  encoding = "UTF-8" ?>
< fozone-validators >
     ...
     < validator  name = "custom"  class = "validationfo.custom.CustomValidator" />
</ fozone-validators >

配置验证规则

1
2
3
4
5
6
7
8
9
10
11
12
<? xml  version = "1.0"  encoding = "UTF-8" ?>
< fozone-validation >
     <!-- 验证组ID,全局唯一 -->
     < group  name = "money.validate" >
         <!-- 验证字段 -->
         < field  name = "number" >
             < rule  name = "custom"  message = "验证失败,不能被指定的数整除" >
                 < param  name = "value"  value = "3" />
             </ rule >
         </ field >
     </ group >
</ fozone-validation >

执行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package  validationfo.custom;
 
public  class  CustomTest {
     public  static  void  main(String[] args) {
         /**
          * Validation.FO的配置资源
          */
         // 验证器配置,系统默认配置
         String validatorsXML =  "validationfo/custom/validators.fo.xml" ;
         // 规则配置
         String rulesXML =  "validationfo/custom/rules.fo.xml" ;
         
         /**
          * 实例化配置对象
          */
         IValidateConfig config = new  BasicValidateConfig(validatorsXML, rulesXML);
         /**
          * 实例化验证服务层
          */
         IValidateService validateService =  new  BasicValidateService(config);
         
         // 实例化数据
         Money money =  new  Money();
         money.setNumber( 7 );
         money.setText( "演示Context使用的Text" );
         
         /**
          * 执行验证
          */
         Map<String,String> map = validateService.validate(money,  "money.validate" );
         // 输出结果
         if (map ==  null  || map.size() ==  0 ) {
             System.out.println( "验证成功" );
         else  {
             System.out.println( "验证失败,结果如下" );
             System.out.println(map);
         }
     }  
}

结果输出

1
2
3
4
16 : 44 : 17 , 178   INFO BasicValidateConfig: 44  - read validation main file , validationfo/custom/rules.fo.xml
TEXT = 演示Context使用的Text
验证失败,结果如下
{number=验证失败,不能被指定的数整除}









本文转自 sundunjam 51CTO博客,原文链接:http://blog.51cto.com/sunspot/1561016,如需转载请自行联系原作者
目录
相关文章
|
5月前
|
Java API
从零开始学习 Java:简单易懂的入门指南之File类(二十九)
从零开始学习 Java:简单易懂的入门指南之File类(二十九)
|
4月前
|
XML 测试技术 数据格式
《手把手教你》系列基础篇(七十五)-java+ selenium自动化测试-框架设计基础-TestNG实现DDT - 中篇(详解教程)
【6月更文挑战第16天】本文介绍了TestNG中`@DataProvider`的两种使用方法。本文通过实例展示了TestNG如何利用`@DataProvider`结合方法名和`ITestContext`来灵活地为测试方法传递参数。
32 1
|
4月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的用于日语词汇学习的微信小程的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的用于日语词汇学习的微信小程的详细设计和实现(源码+lw+部署文档+讲解等)
|
4月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的四六级词汇的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的四六级词汇的详细设计和实现(源码+lw+部署文档+讲解等)
|
4月前
|
小程序 Java 关系型数据库
基于SpringBoot+Vue大学生网络教学平台设计和实现(源码+LW+调试文档+讲解等)
基于SpringBoot+Vue大学生网络教学平台设计和实现(源码+LW+调试文档+讲解等)
|
4月前
|
存储 测试技术 数据安全/隐私保护
《手把手教你》系列基础篇(七十六)-java+ selenium自动化测试-框架设计基础-TestNG实现DDT - 下篇(详解教程)
【6月更文挑战第17天】本文是一篇关于使用Selenium和TestNG进行数据驱动测试的教程。作者宏哥通过实例展示了如何处理多个用户登录场景。
56 0
|
5月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的经典诗文学习爱好者学习交流平台的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的经典诗文学习爱好者学习交流平台的详细设计和实现(源码+lw+部署文档+讲解等)
|
5月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的中学课内小说阅读与学习系统的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的中学课内小说阅读与学习系统的详细设计和实现(源码+lw+部署文档+讲解等)
|
5月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的小学生课外知识学习网站的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的小学生课外知识学习网站的详细设计和实现(源码+lw+部署文档+讲解等)
|
5月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的大学生竞赛管理系统的设计与实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的大学生竞赛管理系统的设计与实现(源码+lw+部署文档+讲解等)
下一篇
无影云桌面