自动化测试中关键字驱动的例子优化

之前有写了一篇关于简单的关键字驱动的测试框架,但是仍然觉得比较冗余,今天又在网上看了几篇关于关键字驱动的例子,在这里顺便记录总结一下

关键字驱动,我们把这个过程大致分为12个步骤
1、自动化实现一个端对端的流程
2、区分和实现动作的关键字
3、设置数据引擎-Excel表
4、实现java反射类
5、设置java常量,一些固定的数据
6、设置元素对象仓库文件
7、设置测试套件执行引擎
8、设置日志模块输出模块,例如log4j
9、设置框架中的异常处理
10、设置测试结果报告输出
11、设置数据驱动
12、完成框架,交付给手工测试人员

以上内容是大致的关键字测试框架的设计步骤。
上面所说的端对端,可以根据实际的例子加以理解,就是我们完成一个有效的测试功能完整的流程
最简单的例如打开指定的网页,稍微复杂点的可能在加上登录等等操作。

动作关键字就是,利用一个简短的单词(词语)来描述这个动作场景。
来一个图片就一目了然了:一个最简单的打开指定浏览器登录的页面
在这里插入图片描述
所以我们首先可以对上面的关键字进行封装,这个也是关键字驱动的精髓,后面的各种操作也都是围绕这个关键字来进行的

public class ActionsKeywords {
	public static void openBrowser(){
		System.out.println("方法openBrowser已经调用");
	}
	
	public static void openUrl(){
		System.out.println("方法openUrl已经调用");
	}
	
	public static void click_Login_link(){
		System.out.println("方法click_Login_link已经调用");
	}
	
	public static void input_Username() {
		System.out.println("方法input_Username已经调用");
	}
	
	public static void input_Password(){
		System.out.println("方法input_Password已经调用");
	}
	
	public static void click_Submit(){
		System.out.println("方法click_Submit已经调用");
	}
	
	public static void closeBrowser(){
		System.out.println("方法closeBrowser已经调用");
	}
}

假设上面是我们已经封装好的关键字,然后我们需要去调研具体的关键字按照特定的顺序进行执行,我们需要在程序跑起来的时候自动的去判断到底执行哪些关键字,我们的案例绝大部分都是放在Excel里面,所以需要把Excel案例中我们想要执行的顺序和我们定义的关键字结合起来。
这就需要搭建Data Engine—利用Apache POI

这已经是一个比较简单的Excel读取了

public class ExcelUtils {
	
	private static XSSFSheet ExcelWSheet;
	private static XSSFWorkbook ExcelWBook;
	private static XSSFCell Cell;
	
	//这里也可以定义一个构造函数来对表格进行初始化
	public static void setExcelFile(String filePath, String sheetName) throws IOException{
		FileInputStream ExcelFile = new FileInputStream(filePath);
		ExcelWBook = new XSSFWorkbook(ExcelFile);
		ExcelWSheet = ExcelWBook.getSheet(sheetName);
	}
	
	public static String getCellData(int RowNum, int ColNum){
		Cell = ExcelWSheet.getRow(RowNum).getCell(ColNum);
		String CellData = Cell.getStringCellValue();
		return CellData;
	}
		public static void main(String[] args) throws IOException {
		String filepath = "D:" + File.separator + "ExcelTest" + File.separator + "KeywordTestCase.xlsx";
		String sheetname = "Test Steps";
		ExcelUtils.setExcelFile(filepath, sheetname);
		System.out.println(ExcelUtils.getCellData(1, 3));
	}
}

读取成功之后我们可以根据读取到的关键字进行内容的判断,然后调用对应的关键字方法,这里我们可以通过最简单的if else判断,符合那个关键字就调用对应关键字的方法,但是这样有一个问题,就是如果关键字有很多怎么办?我们需要写那么多if else进行判断吗?这里我们就需要用到java反射来优化我们的代码了,假设DriverScript是执行的类,这个类里面是我们实现调用的各种方法
反射前面的文章里面有写,这里我们用起来!

public class DriverScript {
	
	//声明一个public static的类对象,所以我们可以在main方法之外去使用
	public static ActionsKeywords actionsKeywords;
	public static String sActionKeyword;
	//下面的返回类型是方法,这里用到了反射类
	public static Method method[];
	
	//构造函数,我们初始化ActionsKeywords类的一个对象
	public DriverScript() throws ClassNotFoundException{
		Class actionKeywords = Class.forName("com.keywordtwo.test.ActionsKeywords");
		method = actionKeywords.getMethods();
//		for (int i = 0; i < method.length; i++) {
//			System.out.println(method[i]);
//		}
	}
	public static void main(String[] args) throws ClassNotFoundException, IOException, Exception {
		DriverScript ds = new DriverScript();
		
		String filepath = "D:" + File.separator + "ExcelTest" + File.separator + "KeywordTestCase.xlsx";
		String sheetname = "Test Steps";
		ExcelUtils.setExcelFile(filepath, sheetname);
		
		for (int iRow = 1; iRow <= 6; iRow++) {
			sActionKeyword = ExcelUtils.getCellData(iRow, 3);
			excute_Actions();
		}
	}
	
	private static void excute_Actions() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException{
		for (int i = 0; i < method.length; i++) {
			if (method[i].getName().equals(sActionKeyword)) {
				method[i].invoke(actionsKeywords);
				break;
			}
		}
	}
	
}

与此同时,我们可以将我们代码中的变量构建成一个常量类,这样即使需要进行修改或者对表格进行调整的时候,也只用修改一处代码

public class Constants {
	
	public static final String PATH_TESTDATA = "D:" + File.separator + "ExcelTest" + File.separator + "KeywordTestCase.xlsx";
	public static final String File_TestData = "KeywordTestCase.xlsx";
	
	//设置表格里面一些单元格的索引值
	public static final int Col_TestCaseID = 0;
	public static final int Col_TestScenarioID = 1;
	public static final int Col_ActionKeyword = 3;
	
	//表格中的sheet名字
	public static final String Sheet_TestSteps = "Test Steps";

}

到这里我们已经差不多完成了60%的工作量了,现在已经可以按照Excel里面的顺序去调用我们封装的方法了,但是现在仍然有一些问题,我每个元素操作都写了一个动作关键字的静态方法,这样做并不合理,因为一旦一个页面或者流程操作需要很多个click操作,而我们知道页面中不同的元素定位的Xpath可能是不一样的,所以我们需要对元素对象操作进行分类。
说的在直白一些
ActionsKeywords.java文件中我们定义了两个click方法,我们把这个进行拆分,1、具体页面具体元素的xpath提取出来放到单独的文件中2、click方法单独封装起来。

	public static void click_Login_link(){
		System.out.println("方法click_Login_link已经调用");
	}
	public static void click_Submit(){
		System.out.println("方法click_Submit已经调用");
	}

我们把Excel调整成这个样子采用page object + action keyword
在这里插入图片描述
Page Object只是一个代号,我们仍然需要通过读取的Page Object值去在配置文件中去查找真正的元素xpath

public class PropertiesUtil {
	
	public static String getValue(String filePath, String keyWord) {
		Properties prop = new Properties();
		String value = null;
		try {
			// 通过输入缓冲流进行读取配置文件
			InputStream InputStream = new BufferedInputStream(
					new FileInputStream(new File(filePath)));
			// 加载输入流
			prop.load(InputStream);
			// 根据关键字获取value值
			value = prop.getProperty(keyWord);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return value;
	}
}

在DriverScript.java文件中修改

//只是新增了一个保存pageobject的对象
			sActionKeyword = ExcelUtils.getCellData(iRow, Constants.Col_ActionKeyword);
			sPageObject = ExcelUtils.getCellData(iRow, Constants.Col_PageObject);

在invoke方法中

private static void execute_Actions() throws Exception {
        //循环遍历每一个关键字驱动方法(在actionskeywords.java中)
        //method variable contain all the method and method.length returns the total number of methods
        // 下面methid.length表示方法个数,method变量表示任何一个关键字方法,例如openBrowser()
        for(int i = 0;i < method.length;i++){
            //开始对比代码中关键字方法名称和excel中关键字这列值是否匹配
            if(method[i].getName().equals(sActionKeyword)){
                //一点匹配到关键字,并传递页面对象参数和动作关键字参数
                method[i].invoke(actionsKeywords,sPageObject);
                //一旦任何关键字被执行,利用break语句去跳出for循环。
                break;
            }
        }
    }

这里面修改之后要在keyword定义的方法中加上参数

    public static void click(String object) {
        driver.findElement(By.xpath(OR.getProperty(object))).click();
    }

通过object == key来获取配置文件中 key value
这里面一定要加上参数,全部都要加,这是反射的格式

到现在这个样子,我们可以说我们跑一个案例下来已经没有问题了,但是在实际自动化测试项目中,很可能有几百条测试用例或者很多个测试组件,有时候我们需要全部执行,有时候我们只需要部分执行,一个好的测试框架,需要执行类似的需求。
我们新加入一个sheet来管理案例集,并标注那些案例集需要执行
在这里插入图片描述
这样就修改ExcelUtils类
1.修改方法setExcelFile,之前这个方法有两个参数,一个是path一个是sheetname,由于我们新增了一个TestCases的sheet,所以这个方法只保留path这个参数。

	//设置Excel文件路径,方便读取到文件,这里只读取就好了,sheet切换和读取留给其他方法做
	public static void setExcelFile(String Path) throws IOException{
		FileInputStream ExcelFile = new FileInputStream(Path);
		ExcelWBook = new XSSFWorkbook(ExcelFile);
	}

2.修改方法getCellData(),增加sheetName参数

	public static String getCellData(int RowNum, int ColNum, String SheetName){
		ExcelWSheet = ExcelWBook.getSheet(SheetName);
		Cell = ExcelWSheet.getRow(RowNum).getCell(ColNum);
		String CellData = Cell.getStringCellValue();
		return CellData;
	}

3.新增一个getRowContains()方法,这个方法有三个参数,第一个是Test Case Name,第二个是列的索引号,第三个是sheet名称,该方法的返回值是当前测试用例的编号。

	//得到一共有多少行
	public static int getRowCount(String SheetName){
		ExcelWSheet = ExcelWBook.getSheet(SheetName);
		int number = ExcelWSheet.getLastRowNum();
		return number;
	}
	
	//得到测试用例的行号,如第一个案例集在第一行,第二个案例集在第五行
	public static int getRowContains(String sTestCaseName, int colNum, String SheetName){
		int i;
		ExcelWSheet = ExcelWBook.getSheet(SheetName);
		int rowCount = ExcelUtils.getRowCount(SheetName);
		for (i = 0; i < rowCount; i++) {
			if (ExcelUtils.getCellData(i, colNum, SheetName).equalsIgnoreCase(sTestCaseName)) {
				break;
			}
		}
		return i;	
	}

4.新增一个方法getTestStepsCount(),同样有三个参数,分别是TestCaseID,TestCase第一个步骤和sheet名称,返回的值是测试用例的步骤数

public static int getTestStepsCount(String SheetName, String sTestCaseID, int iTestCaseStart){
		for (int i = iTestCaseStart; i < ExcelUtils.getRowCount(SheetName); i++) {
			//如果不相等
			if (!sTestCaseID.equals(ExcelUtils.getCellData(i, Constants.Col_TestCaseID, SheetName))) {
				int number = i;
				return number;
			}
		}
		//全部相等之后,也就是说sheet中只有一个testcaseid,那么执行步骤就是ExcelWSheet.getLastRowNum() + 1
		ExcelWSheet = ExcelWBook.getSheet(SheetName);
		int number = ExcelWSheet.getLastRowNum() + 1;
		return number;
	}

ExcelUtils类修改完毕之后我们需要对Driver Script类进行修改了
1.外层for循环,得到Test Cases工作簿中的Test CaseID,控制从第一个测试用例开始执行到最后一个测试用例
2.得到RunModel的值来控制当前测试用例是否被执行。
3.写另外一个内层for循环,控制Test Step表中的Test Case ID列。作用就是在当前测试用例,从第一个测试步骤执行到最后一个测试步骤

public void excute_TestCase() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException{
		//一共有多少条测试案例
		int iTotalTestCases = ExcelUtils.getRowCount(Constants.Sheet_TestCases);
		System.out.println("一共有"+iTotalTestCases+"条测试案例");
		for (int iTestCase = 1; iTestCase <= iTotalTestCases; iTestCase++) {//从1开始,通过循环可以拿到每一个测试用例的String类型的sTestCaseID和sRunMode
			sTestCaseID = ExcelUtils.getCellData(iTestCase, Constants.Col_TestCaseID, Constants.Sheet_TestCases);
			sRunMode = ExcelUtils.getCellData(iTestCase, Constants.Col_RunMode, Constants.Sheet_TestCases);
			//如果RunMode是yes
			if (sRunMode.equalsIgnoreCase("Yes")) {
				//开始的行号
				iTestStep = ExcelUtils.getRowContains(sTestCaseID, Constants.Col_TestCaseID, Constants.Sheet_TestSteps);
				//最后一个行号
				iTestLaseStep = ExcelUtils.getTestStepsCount(Constants.Sheet_TestSteps, sTestCaseID, iTestStep);
				System.out.println("iTestStep:"+iTestStep);
				System.out.println("iTestLaseStep:"+iTestLaseStep);
				//下面这个for循环的次数就等于测试用例的步骤数
				for (; iTestStep < iTestLaseStep; iTestStep++) {
					sActionKeyword = ExcelUtils.getCellData(iTestStep, Constants.Col_ActionKeyword, Constants.Sheet_TestSteps);
					sPageObject = ExcelUtils.getCellData(iTestStep, Constants.Col_PageObject, Constants.Sheet_TestSteps);
					excute_Actions();
				}
			}	
		}
	}

到这里,我们实现了案例的分类读取与执行,如果添加了log4j组件,可以直接在日志中查看案例的执行情况,但是我们需要将案例的执行结果同步到excel表格中,我们现在没有办法直接获取案例执行的结果,所以我们在每个关键字中添加try-catch去捕获异常。
在执行脚本中我们定义一个boolean型的变量,当代码跑到异常也就是catch中,我们将bResult置为false。简单一句话说,如果bResult的值变成了false,我们就在Excel对应用例位置标识failed的标记,如果是true,我们就标记pass

public static boolean bResult;

由于是关键字驱动矿建,这里无法引入TestNG生成更美观的html格式的报告。那么如何做到呐?
1、首先我们需要在我们的关键字驱动表中去新建一列Results
在这里插入图片描述
在这里插入图片描述
在常量文件中添加对应的变量

	//第一个是测试用例结果标记列的索引,第二个是测试步骤里面的结果索引
	public static final int Col_Result = 3;
	public static final int Col_TestStepResult = 5;

同时添加两个变量,记录结果状态pass和fail

	//结果状态标记
	public static final String KEYWORD_FAIL = "FAIL";
	public static final String KEYWORD_PASS ="PASS";

然后就需要继续修改我们的ExcelUtilt.java

	//构造一个往单元格写数据的方法,主要是用来写结果pass还是fail
	public static void setCellData(String Result, int RowNum, int ColNum, String SheetName){
		try {
			ExcelWSheet = ExcelWBook.getSheet(SheetName);
			Row = ExcelWSheet.getRow(RowNum);
			Cell = Row.getCell(ColNum);
			if (Cell == null) {
				Cell = Row.createCell(ColNum);
				Cell.setCellValue(Result);
			}else {
				Cell.setCellValue(Result);
			}
			FileOutputStream fileOut = new FileOutputStream(Constants.PATH_TESTDATA);
			ExcelWBook.write(fileOut);
			fileOut.close();
            ExcelWBook = new XSSFWorkbook(new FileInputStream(Constants.PATH_TESTDATA));
		} catch (Exception e) {
			DriverScript.bResult = false;
			e.printStackTrace();
		}
	}

修改DriverSprict

package com.keywordtwo.test;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class DriverScript {
	
	//声明一个public static的类对象,所以我们可以在main方法之外去使用
	public static ActionsKeywords actionsKeywords;
	public static String sActionKeyword;
	
	public static String sPageObject;
	//下面的返回类型是方法,这里用到了反射类
	public static Method method[];
	
	public static int iTestStep;
	public static int iTestLaseStep;
	public static String sTestCaseID;
	public static String sRunMode;
	//执行结果成功标记
	public static boolean bResult;
	
	
	
	//构造函数,我们初始化ActionsKeywords类的一个对象
	public DriverScript() throws ClassNotFoundException{
		try {
			Class actionKeywords = Class.forName("com.keywordtwo.test.ActionsKeywords");
			method = actionKeywords.getMethods();
		} catch (SecurityException e) {
			
			e.printStackTrace();
		}
//		for (int i = 0; i < method.length; i++) {
//			System.out.println(method[i]);
//		}
	}
	public static void main(String[] args) throws ClassNotFoundException, IOException, Exception {
		
		DriverScript ds = new DriverScript();
		
		String filepath = Constants.PATH_TESTDATA;
		String sheetname = Constants.Sheet_TestSteps;
		ExcelUtils.setExcelFile(filepath);
		
		/*for (int iRow = 1; iRow <= 6; iRow++) {
			sActionKeyword = ExcelUtils.getCellData(iRow, Constants.Col_ActionKeyword);
			sPageObject = ExcelUtils.getCellData(iRow, Constants.Col_PageObject);

//			String keyvalue = PropertiesUtil.getValue(Constants.OR_Path, sPageObject);
//			System.out.println(keyvalue);
			excute_Actions();
		}*/
		
		DriverScript startEngine = new DriverScript();
		startEngine.excute_TestCase();
	}
	
	public void excute_TestCase() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException{
		//一共有多少条测试案例
		int iTotalTestCases = ExcelUtils.getRowCount(Constants.Sheet_TestCases);
		System.out.println("一共有"+iTotalTestCases+"条测试案例");
		for (int iTestCase = 1; iTestCase <= iTotalTestCases; iTestCase++) {//从1开始,通过循环可以拿到每一个测试用例的String类型的sTestCaseID和sRunMode
			bResult = true;
			sTestCaseID = ExcelUtils.getCellData(iTestCase, Constants.Col_TestCaseID, Constants.Sheet_TestCases);
			sRunMode = ExcelUtils.getCellData(iTestCase, Constants.Col_RunMode, Constants.Sheet_TestCases);
			//如果RunMode是yes
			System.out.println("sTestCaseID:"+sTestCaseID);
			if (sRunMode.equalsIgnoreCase("yes")) {
				//开始的行号
				iTestStep = ExcelUtils.getRowContains(sTestCaseID, Constants.Col_TestCaseID, Constants.Sheet_TestSteps);
				//最后一个行号
				iTestLaseStep = ExcelUtils.getTestStepsCount(Constants.Sheet_TestSteps, sTestCaseID, iTestStep);
				System.out.println("iTestStep:"+iTestStep);
				System.out.println("iTestLaseStep:"+iTestLaseStep);
				bResult = true;
				//下面这个for循环的次数就等于测试用例的步骤数
				for (; iTestStep < iTestLaseStep; iTestStep++) {
					sActionKeyword = ExcelUtils.getCellData(iTestStep, Constants.Col_ActionKeyword, Constants.Sheet_TestSteps);
					sPageObject = ExcelUtils.getCellData(iTestStep, Constants.Col_PageObject, Constants.Sheet_TestSteps);
					excute_Actions();
					if (bResult == false) {
						ExcelUtils.setCellData(Constants.KEYWORD_FAIL, iTestCase, Constants.Col_Result, Constants.Sheet_TestCases);
						break;
					}
				}
				//成功执行完一个案例之后在判断
				if (bResult == true) {
					ExcelUtils.setCellData(Constants.KEYWORD_PASS, iTestCase, Constants.Col_Result, Constants.Sheet_TestCases);
				}
			}	
		}
	}
	
	
	private static void excute_Actions() throws IllegalAccessException, IllegalArgumentException, InvocationTargetException{
		for (int i = 0; i < method.length; i++) {
			if (method[i].getName().equals(sActionKeyword)) {
				//method[i].invoke(actionsKeywords);
				method[i].invoke(actionsKeywords,sPageObject);
				if (bResult == true) {
					ExcelUtils.setCellData(Constants.KEYWORD_PASS, iTestStep, Constants.Col_TestStepResult, Constants.Sheet_TestSteps);
					break;
				}else {
					ExcelUtils.setCellData(Constants.KEYWORD_FAIL, iTestStep, Constants.Col_TestStepResult, Constants.Sheet_TestSteps);
					break;
				}
			}
		}
	}
}

这里面仍然有一些不足,就是我们所做的操作不光光只有点击这种,我们仍然需要输入,那么输入的数据我们怎么读取进来呐?
添加一列数据列,我们修改一下之前的result常量
在这里插入图片描述
修改ActionsKeywords.java类
主要是构造一个input()的方法,我们修改每个关键字方法,参数都改成两个,一个参数是object,另外一个参数就是本文的data set。
修改DriverScript.java类
1.创建一个新的变量public static String sData;
2.添加代码语句块,从excel中data set列复制给sData变量
3.在execute_Action()代码块,把sData参数传递进去。

 sActionKeyword=ExcelUtils.getCellData(iTestStep,Constants.Col_ActionKeyword,Constants.Sheet_TestStep);
 sPageObject = ExcelUtils.getCellData(iTestStep, Constants.Col_PageObject, Constants.Sheet_TestSteps);
 sData = ExcelUtils.getCellData(iTestStep, Constants.Col_DataSet, Constants.Sheet_TestSteps);
 execute_Actions();

这里是真正的读取参数然后将参数通过反射传递给真正的方法

这样就可以了,回过头来总结,当有人问你是否熟悉关键字驱动的时候你是否可以说的清楚?
注意到我们设置了一个常量类,这里主要是帮我们简化excel的操作,其实我们可以完全不用这个常量类,只需要在excel工具类中加上判断第一行的值等于多少在记录对应的列数就可以了,这样无论案例表格怎么改,都是可以执行的,这里至于怎么考虑就看个人了,一般公司的案例格式都是会固定的,统一原则嘛!

首先在我个人看来,
1、关键字驱动的核心是数据引擎,也就是我们的Excel表,我们后续的设计都是围绕着这个表格进行的。最基本的就是要有案例列表和案例执行步骤列表,一个测试过程会有很多的案例,而且每个案例又会包含很多个执行步骤,每个步骤又会有不同的操作
2、有了驱动表之后我们为了涉及方便设置了常量类,其中包含了我们的文件路径,一些默认值、索引值以及工作簿的名称,以UI自动化为例,不同页面都会有相应的点击操作,我们不能把每一个点击操作都写成一个关键字,这样关键字的数量就会非常庞大,我们将这些操作抽象的部分提取出来,一个点击的btn有对应的xpath路径,而点击的操作click都是相同的,这里仍然是设计驱动表所需要关注的,也就是对象仓库,把不同页面的不同对象提取出来,也就是我们的案例操作涉及到哪些元素提出出来放到一个配置文件里面,加入页面元素的路径有修改,我们直接修改配置文件里面的内容就可以了,而代码如click关键字是不需要修改的
4、其次才是设计我们的excel工具类,由于我们之前提取了一部分常量出来,所以excel工具类就变得没有那么复杂注意row和cell和提取写入方式,工具类基本就完成了基本操作每个小的方法基本10行左右的代码就可以搞定了
5、封装关键字,将每一个关键字封装成一个一个的方法,打开浏览器是一个关键字,输入URL是一个关键字,点击是一个关键字,输入帐号是一个关键字。
6、有了excel工具类,我们就可以读取我们想要的数据,写入我们想要的结果,我们读取到excel关键字需要和我们定义的关键字关联起来才能有对应的操作,这里就涉及到java的反射机制,这里我们可不可以不用java的反射,那么不用反射怎么做?我们需要读取到一个关键字然后进行if-else判断看读取到的关键字和哪个方法匹配,那么如果一个测试步骤有很多个关键字呐?我们也这么以此去if-else判断吗?显然这样代码的维护性是很差的。反射正好帮我们解决了这个问题,将所有的关键字的方法都存储在一个数组中,然后在数组中去查找对应的关键字7
7、这些都完成之后我们才会设计我们执行的代码Driver Script类通过内外层循环加是否执行关键字来判断具体的案例执行以及通过异常处理来跟踪具体测试步骤的执行结果,当然大家还可以通过log4j组件来跟踪完善我们的测试过程。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值