软件构造Lab2心得体会

1实验目标概述
本次实验训练抽象数据类型(ADT)的设计、规约、测试,并使用面向对象
编程(OOP)技术实现 ADT。具体来说:
⚫ 针对给定的应用问题,从问题描述中识别所需的 ADT;
⚫ 设计 ADT 规约(pre-condition、post-condition)并评估规约的质量;
⚫ 根据 ADT 的规约设计测试用例;
⚫ ADT 的泛型化;
⚫ 根据规约设计 ADT 的多种不同的实现;针对每种实现,设计其表示
(representation)、表示不变性(rep invariant)、抽象过程(abstraction
function)
⚫ 使用 OOP 实现 ADT,并判定表示不变性是否违反、各实现是否存在表
示泄露(rep exposure);
⚫ 测试 ADT 的实现并评估测试的覆盖度;
⚫ 使用 ADT 及其实现,为应用问题开发程序;
⚫ 在测试代码中,能够写出 testing strategy
2实验环境配置
EclEmma已经集成到eclipse中,无需另外配置环境。

3.1Poetic Walks
用两种不同的方法去实现Graph接口,基于点和边分别构造图结构,并应用于作诗当中。
3.1.1Get the code and prepare Git repository
1.获取代码:从所给的github网址clone或者直接下载代码
2.本地创建git仓库:新建一个文件夹作为本地仓库,使用git-bash
切换到该文件夹,输入git init指令,即可创建本地仓库。当前目录会多出一个.git文件,即创建成功
3.使用git管理:(只介绍一些常用的命令)
git add ‘filename’ 添加文件到暂存区
git commit -m “message” 提交暂存区文件到本地仓库
git log 查看日志,包括提交信息等
git status 查看当前工作区状态
git remote add origin https:…… 创建远程链接
git push -u origin master 将本地仓库提交到远程仓库
git pull 拉取整合远程仓库到本地仓库(相当于git fetch加git merge) 等等
3.1.2Problem 1: Test Graph
为Graph中的实例方法编写测试,即测试其中的add,remove,vertices,set,sources,targets方法即可。具体的测试策略请移步代码处。
由于GraphInstanceTest是一个抽象类,无法直接测试。

3.1.3Problem 2: Implement Graph
以下各部分,请按照MIT页面上相应部分的要求,逐项列出你的设计和实现思路/过程/结果。
3.1.3.1Implement ConcreteEdgesGraph
1.checkRep(): 检查顶点和边是否为空,边邻接的顶点是否都加入vertices中,检验是否有相同边,均使用assert断言测试即可
2.add(L vertex):加入顶点,先判断是否已包含,若已存在该顶点则返回false,否则加入到vertices中并返回true。
3.set(L source,L target,int weight):首先判断weight是否非负,若为负数抛出异常;然后添加两个顶点,判断是否两顶点间是否已经存在这条边,若有则删除该边(因为Edge是不可变类型),再判断weight是否大于0,大于则添加新边,否则直接返回preweight
4.remove(L vertex):先判断该点是否存在,不存在返回false,存在则从vertices中删除该点,并在edges中查找以该点为src或tag的边并删除即可,返回true
5.vertices():将vertices包装成不可变类型unmodifiableset再返回
6.sources(L target):遍历edges,查找target的source并将其加入新建的map,最后用不可变类型unmodifiablemap包装即可
7.targets(L source):和sources类似,查找source的target并加入map,用不可变类型包装返回即可
8.toString():将图以文字化的形式显示即可,这里我将各个边的toString()连接即可
Edge类:
1.构造器:Edge(L src,L tag,int w):输入source,target和weight即可创建新边
2.checkRep():检查src,tag不为空,weight大于等于0即可
3.getSrc():获取src
4.getTag():获取tag
5.getWeight():获取weight
6.toString():形式为src->tag[weight]

测试:
ConcreteEdgesGraphTest():
除继承GraphInstanceTest的测试方法外,添加了对toString()的测试以及对Edge类的测试,主要测试toString(),策略见代码处。
3.1.3.2Implement ConcreteVerticesGraph
1.checkRep():检查顶点不能为空且不允许重名,使用assert断言即可
2.add(L vertex):加入顶点,先判断是否已包含,若已存在该顶点则返回false,否则加入到vertices中并返回true。
3.set(L source,L target,int weight):首先判断weight是否非负,若为负数抛出异常;然后添加两个顶点,判断weight是否等于0,若等于0则返回preweight,不为0则加入边
4.remove(L vertex):先判断该点是否存在,不存在返回false,存在则从vertices中删除该点,并查找以该点为src或tag的边并删除即可,返回true
5.vertices():将vertices包装成不可变类型unmodifiableset再返回
6.sources(L target):找到target后调用vertex的getSrc()即可
7.targets(L source):和sources类似,找到source后调用vertex的getTag()即可
8.toString():将图以文字化的形式显示即可,这里我将各个点的toString()连接即可

Vertex类:
1.构造器:Vertex(L name):输入标签即可创建顶点
2.checkRep():判断名字,sources,targets不为空即可
3.getname():获取name
4.getSrc():将sources包装成不可变的unmodifiableMap再返回即可
5.getTag():将targets包装成不可变的unmodifiableMap再返回即可
6.setSrc(L src, int weight):若weight为0,则移除该边,否则加入边或者修改边,使用Map.put即可
7.setTag():同setSrc()
8.toString():形式为{[src:weight]->[tag:weight]}

测试 :
ConcreteVertexGraphTest():
除继承GraphInstanceTest的测试方法外,添加了对toString()的测试以及对Vertex类的测试,主要测试toString(),策略见代码处。
3.1.4Problem 3: Implement generic Graph
3.1.4.1Make the implementations generic
泛型化Graph,将所有的String改成L,Graph,Vertex,Edge等加个<>即可。
这样就可以创建任何类型标签的图。

3.1.4.2Implement Graph.empty()
具体返回一个Graph实例,new一个ConcreteEdgesGraph()或者ConcreteVertexGraph()实例都可以。
并编写其他类型标签的测试。
3.1.5Problem 4: Poetic walks
3.1.5.1Test GraphPoet
编写测试用例:测试通过poem()得到的诗句和我们期待的是否相同即可
3.1.5.2Implement GraphPoet
1.checkRep():检验顶点集是否为空,使用assert断言即可
2.构造器:GraphPoet(File corpus)读取文件中的语料,并将其按空格分隔,存成一个图的形式。
3.poem(String input):将输入按空格分隔,并在每两个分隔中都去已经构造好的图里寻找有无两个词的桥边,即存在一个词,既是第一个词的target,又是第二个词的source,选择权重最大的桥词加入,构成output,即作诗完成
3.1.5.3Graph poetry slam
自己给出输入和语料库进行作诗即可。

3.2Re-implement the Social Network in Lab1
利用P1实现的Graph重新构造人际关系图,考察对ADT的灵活使用
3.2.1FriendshipGraph类
1.FriendshipGraph类继承了ConcreteEdgesGraph类
2.addVertex(Person person):调用父类中的add()方法即可
3.addEdge(Person a, Person b):调用父类中的set(a, b, 1)即可
4.getdistance(Person a, Person b):
该方法用于计算a和b之间的距离。借助bfs算法的思想。
与a距离为1的朋友成为第一层朋友,以此类推。我定义了一个队列queue和一个Set类型的layer。思路如下:
1).初始化所有Person中的flag均为false(意为没有访问过),distance初始化为0
2).如果a与b名字相同,则返回distance(两人为同一人)
3).把a添加进queue中
4).while(queue不为空时){
将queue中元素拷贝至layer中并将queue清空;
遍历layer中的元素,并把layer中元素的未被访问过的朋友(即targets)添加进queue中(即a的下一层朋友);
遍历并清空layer中元素(a的该层朋友),看b是否存在于该层中;若存在,则返回distance;
若不存在,则distance++,继续循环
}
5).若queue为空时仍不存在,则返回-1

3.2.2Person类
1.私有字段name存储名字
2.构造器Person(String name): 输入名字即可构造一个Person类
3.getname():获取Person的名字
4.重写equals方法,因为Person是放入set中的,set判断一个对象是否在其中使用的contain方法调用了equals方法
3.2.3客户端main()

		FriendshipGraph graph = new FriendshipGraph();
		Person rachel = new Person("Rachel");
		Person ross = new Person("Ross");
		Person ben = new Person("Ben");
		Person kramer = new Person("Kramer");
		graph.addVertex(rachel);
		graph.addVertex(ross);
		graph.addVertex(ben);
		graph.addVertex(kramer);
		graph.addEdge(rachel, ross);
		graph.addEdge(ross, rachel);
		graph.addEdge(ross, ben);
		graph.addEdge(ben, ross);
		System.out.println(graph.getDistance(rachel, ross)); 
		//should print 1
		System.out.println(graph.getDistance(rachel, ben)); 
		//should print 2
		System.out.println(graph.getDistance(rachel, rachel)); 
		//should print 0
		System.out.println(graph.getDistance(rachel, kramer)); 
		//should print -1

3.2.4测试用例
1.public void testAddVertex() throws Exception
addVertex的测试,主要测试加入的Person类是否存入了FriendshipGraph类中的vertex字段中。

2.public void testAddEdge() throws Exception
addedge的测试,主要测试加入的类是否存在于graph的targets中。

3.public void testGetDistance() throws Exception
测试求出的距离与实际距离是否相等即可

3.3Playing Chess
3.3.1ADT设计/实现方案
设计了哪些ADT(接口、类),各自的rep和实现,各自的mutability/ immutability说明、AF、RI、safety from rep exposure。
必要时请使用UML class diagram(请自学)描述你设计的各ADT间的关系。
1.Position类(位置)
// Abstraction function:
// Position表示棋子位置,x表示横坐标,y表示纵坐标
// Representation invariant:
// position不能为空
// Safety from rep exposure:
// 所有fields都是 private和final的,使用immutable数据类型
rep:
private int x;
private int y;
构造器:
public Position(int x, int y) ;
实现:

/**
 * 获取棋子纵坐标
 * 
 * @return 纵坐标
 */
public int getY() ;

/**
 * 获取棋子横坐标
 * 
 * @return 横坐标
 */
public int getX();

/**
 * 重写equals方法
 */
@Override
public boolean equals(Object obj) ;

2.Piece类(棋子)
// Abstraction function:
// Piece代表棋子,belonging表示属于哪个玩家,type表示棋子的类型,piecestatus表示棋子的状态,piecePosition表示棋子位置
// Representation invariant:
// piece不能为空
// Safety from rep exposure:
// 所有fields都是 private和final的,使用immutable数据类型
rep:private final String belonging;
private final String type;
//棋子状态,0-未放置,1-已放置,2-已移除
private int pieceStatus;
private Position piecePosition;
构造器:输入归属,类型以及状态即可
public Piece(String belonging, String type, int status) ;
具体实现:

 * 设置棋子位置
 * 
 * @param x 指定位置横坐标
 * @param y 指定位置纵坐标
 */
public void setPosition(int x, int y) ;

/**
 * 设置棋子状态
 * 
 * @param status 0-未放置;1-已放置;2-已移除
 */
public void setStatus(int status) ;

/**
 * 获取棋子的所属
 * 
 * @return 棋子所属玩家的姓名
 */
public String getBelonging() ;

/**
 * 获取棋子类型
 * 
 * @return 棋子类型
 */
public String getType() ;

/**
 * 获取棋子状态
 * 
 * @return 棋子状态
 */
public int getStatus() ;

/**
 * 获取棋子横坐标
 * 
 * @return 横坐标
 */
public int getPositionX() ;

/**
 * 获取棋子纵坐标
 * 
 * @return 纵坐标
 */
public int getPositonY() ;

3.Board类(棋盘)
// Abstraction function:
// type为棋盘类型,size为棋盘大小,pieces存储棋盘上的棋子,isplaced判断棋盘上是否有棋子
// Representation invariant:
// Board不为空
//
// Safety from rep exposure:
// 所有fields都是 private and final,使用immutable数据类型
rep:private final int type;
private final int size;
private Piece[][] pieces;
private boolean[][] isplaced;
构造器:public Board(int type, int size) ;

具体实现:

 * 获取棋盘类型
 * 
 * @return type
 */
public int getType() ;

/**
 * 获取棋盘大小
 * 
 * @return size
 */
public int getSize() ;

/**
 * 获取棋盘上的棋子
 * 
 * @param x 棋盘横坐标
 * @param y 棋盘纵坐标
 * @return null如果没有棋子,否则返回棋盘上的piece
 */
public Piece getPiece(int x, int y) ;

/**
 * 在棋盘上放置棋子
 * 
 * @param piece 待放置的棋子
 * @param x 放置位置横坐标
 * @param y 放置位置纵坐标
 * @return true如果放置成功,false如果放置失败
 */
public boolean setPiece(Piece piece, int x, int y) ;
/**
 * 更改棋盘指定位置是否有棋子的状态
 * 
 * @param x 指定位置横坐标
 * @param y 指定位置纵坐标
 * @param b 指定状态
 */
public void setIsPlaced(int x, int y, boolean b) ;

/**
 * 显示棋盘
 */
public void show() ;

4.Player类(玩家)
// Abstraction function:
// Player代表玩家,由姓名唯一确定
// Representation invariant:
// player不为空,name不为null
// Safety from rep exposure:
// 所有fields都是 private或者final的,使用immutable数据类型,使用防御性拷贝,使用不可变的unmodifiableset作为返回值
rep:private final String name;
//玩家棋子
private Set pieces;
//玩家走棋历史
private List history;
构造器:public Player(String name) ;
具体实现:/**
* 获取玩家姓名
*
* @return 姓名
*/
public String getPlayerName() {
return this.name;
}

/**
 * 获取玩家棋子,使用unmodifiableSet作为返回值
 * 
 * @return 玩家棋子
 */
public Set<Piece> getPlayerPieces();

/**
 * 获取玩家走棋历史
 * 
 * @return unmodifiableList,玩家走棋历史
 */
public List<String> getPlayerHistory();

/**
 * 添加棋子
 * 
 * @param piece 
 * @return true如果添加成功,false如果添加失败
 */
public boolean addPiece(Piece piece) ;

/**
 * 移除棋子
 * 
 * @param piece
 * @return true如果移除成功,false如果移除失败
 */
public boolean removePiece(Piece piece) ;

/**
 * 获取玩家在棋盘上的棋子数
 * 
 * @return 该玩家的棋子数
 */
public int getNumberOfPieces() ;

/**
 * 添加走棋历史
 * 
 * @param step 走棋历史
 */
public void addStep(String step) ;

5.Game类(游戏)
// Abstraction function:
// 模拟一整局游戏,gameAction模拟棋手动作,board为棋盘,player1和player2为玩家
// Representation invariant:
// gameAction, board, player1,player2不为空
// Safety from rep exposure:
// 字段为private的,采用防御性拷贝,无表示泄露
rep:private Action gameAction;
private Player player1;
private Player player2;
private Board board;
构造器:public Game(String gameType, String player1_name, String player2_name) ;
具体实现:

 * 获取玩家1的姓名
 * 
 * @return player1's name
 */
public String getPlayer1name() ;
/**
 * 获取玩家2的姓名
 * 
 * @return player2's name
 */
public String getPlayer2name() ;

/**
 * 落子(针对围棋),由指定玩家将棋子落在指定位置,并添加走棋历史
 * 
 * @param name 指定玩家姓名
 * @param x 指定位置横坐标
 * @param y 指定位置纵坐标
 * @return true如果落子成功,false如果落子失败
 */
public boolean placePiece(String name, int x, int y) ;

/**
 * 移子(针对国际象棋),由指定玩家将指定位置的棋子移动到指定位置
 * 
 * @param name 指定玩家姓名
 * @param sx 原位置横坐标
 * @param sy 原位置纵坐标
 * @param tx 目的位置横坐标
 * @param ty 目的位置纵坐标
 * @return true如果移子成功,false如果移子失败
 */
public boolean movePiece(String name, int sx, int sy, int tx, int ty) ;

/**
 * 提子(针对围棋),由指定玩家将指定位置的棋子移出棋盘
 * 
 * @param name 指定玩家姓名
 * @param x 指定位置横坐标
 * @param y 指定位置纵坐标
 * @return true如果提子成功,false如果提子失败
 */
public boolean removePiece(String name, int x, int y) ;
	
/**
 * 吃子(针对国际象棋),由指定玩家移动棋子吃掉目标位置的棋子
 * 
 * @param name 指定玩家姓名
 * @param sx 原位置横坐标
 * @param sy 原位置纵坐标
 * @param tx 目的位置横坐标
 * @param ty 目的位置纵坐标
 * @return true如果吃子成功,false如果吃子失败
 */
public boolean eatPiece(String name, int sx, int sy, int tx, int ty) ;	

/**
 * 获取棋盘大小
 * 
 * @return 棋盘大小
 */
public int getBoardsize() ;

/**
 * 显示棋盘
 */
public void showBoard() ;

/**
 * 检查棋盘指定位置的占用情况,并打印出来
 * 
 * @param x 指定位置横坐标
 * @param y 指定位置纵坐标
 */
public void checkBoard(int x, int y) ;

/**
 * 获取玩家在棋盘上的棋子数
 * 
 * @param name 玩家姓名
 * @return 棋盘上的棋子数
 */
public int getNumberOfPiece(String name) ;

/**
 * 获取玩家的走棋历史并打印出来
 * 
 * @param name 玩家姓名
 */
public void getHistory(String name) ;

6.Action接口(玩家行为)
只定义了方法,并未实现。

 * create a new player
 * 
 * @param playername
 * @return a new player
 */
public Player createPlayer(String playername);

/**
 * create a new board
 * 
 * @return a chessboard or goboard
 */
public Board createboard();

/**
 * initialize the game, create players, board and pieces
 * 
 * @param player1_name
 * @param player2_name
 */
public void init(String player1_name, String player2_name);

/**
 * 落子(针对围棋),由指定玩家将棋子落在指定位置,并添加走棋历史
 * 
 * @param player 指定玩家
 * @param piece 指定棋子
 * @param x 指定位置横坐标
 * @param y 指定位置纵坐标
 * @return true如果落子成功,false如果落子失败
 */
public boolean placePiece(Player player, Piece piece, int x, int y);

/**
 * 移子(针对国际象棋),由指定玩家将指定位置的棋子移动到指定位置
 * 
 * @param player 指定玩家
 * @param sx 原位置横坐标
 * @param sy 原位置纵坐标
 * @param tx 目的位置横坐标
 * @param ty 目的位置纵坐标
 * @return true如果移子成功,false如果移子失败
 */
public boolean movePiece(Player player, int sx, int sy, int tx, int ty);

/**
 * 提子(针对围棋),由指定玩家将指定位置的棋子移出棋盘
 * 
 * @param player 指定玩家
 * @param x 指定位置横坐标
 * @param y 指定位置纵坐标
 * @return true如果提子成功,false如果提子失败
 */
public boolean removePiece(Player player, int x, int y);

/**
 * 吃子(针对国际象棋),由指定玩家移动棋子吃掉目标位置的棋子
 * 
 * @param player 指定玩家
 * @param sx 原位置横坐标
 * @param sy 原位置纵坐标
 * @param tx 目的位置横坐标
 * @param ty 目的位置纵坐标
 * @return true如果吃子成功,false如果吃子失败
 */
public boolean eatPiece(Player player, int sx, int sy, int tx, int ty);

/**
 * 返回player1
 * 
 * @return player1
 */
public Player getPlayer1();

/**
 * 返回player2
 * 
 * @return player2
 */
public Player getPlayer2();

/**
 * 返回棋盘
 * 
 * @return board
 */
public Board getBoard();

7.ChessAction类(Action的国际象棋实现)
// Abstraction function:
// 模拟棋手动作,chessBoard为棋盘,player1,player2为玩家
// Representation invariant:
// Action和chessBoard,player1,player2均不为空
// Safety from rep exposure:
// 字段为protected和final的,采用防御性拷贝,无表示泄露

8.GoAction(Action的围棋实现)
// Abstraction function:
// 模拟棋手动作,goBoard为棋盘,player1,player2为玩家
// Representation invariant:
// Action和goBoard,player1,player2均不为空
// Safety from rep exposure:
// 字段为protected和final的,采用防御性拷贝,无表示泄露

3.3.2主程序MyChessAndGoGame设计/实现方案
辅之以执行过程的截图,介绍主程序的设计和实现方案,特别是如何将用户在命令行输入的指令映射到各ADT的具体方法的执行。
1.启动游戏后进入欢迎界面,输入chess or go之后可进入相应的游戏

2.然后输入玩家一和玩家二的名字,即可初始化游戏(即创建一个Game实例)
Game game = new Game(gameType, player1_name, player2_name);

3.进入游戏后,会打印棋盘的大小以防止用户输入的坐标超出范围,并会提示可进行的操作。这里我写了一个ChessMenu方法来打印chess可进行的操作.还有一个GoMenu。

4.选择操作后,系统会提示是否需要显示棋盘,这里调用了game的showBoard()方法
下面是根据choice来进行相应操作的方法。
public void ChoiceOfChess(Game game, String name, String choice, Scanner in) ;
//根据选项进行不同的行为

围棋:
下面是根据choice来进行相应操作的方法。
public void ChoiceOfGo(Game game, String name, String choice, Scanner in) ;
//根据选项进行不同的行为

3.3.3ADT和主程序的测试方案
介绍针对各ADT的各方法的测试方案和testing strategy。
介绍你如何对该应用进行测试用例的设计,以及具体的测试过程。
1.对ChessAction的测试,主要测试init,movePiece,eatPiece方法
// Testing strategy
// 测试init方法():
// 是否创建了board,player1和player2,是否加入棋子
// 根据player的name和board的size,棋子的size进行判断
//
// 测试movePiece方法():
// 1.目标位置不在棋盘内 2.目的地有其他棋子 3.初始位置无棋子 4.两个位置相同 5.初始位置并非该棋手所有 6.移子成功
// 根据返回值来判断是否移子成功
//
// 测试eatPiece()方法:
// 1.目标位置不在棋盘内 2.初始位置并非该棋手所有 3.初始位置无棋子 4.目标位置无棋子 5.目标位置棋子属于该玩家 6.吃子成功
// 根据返回值来判断是否吃子成功
对各种异常情况都进行测试即可。测试覆盖了ChessAction的所有代码,并且通过。

2.对GoAction的测试,主要测试init,placePiece,removePiece即可
// Testing strategy
// 测试init方法():
// 是否创建了board,player1和player2
// 根据player的name和board的size进行判断
//
// 测试placePiece方法():
// 1.目标位置不在棋盘内 2.目的地有其他棋 3.棋子并非该棋手所有 4.指定棋子已经在棋盘上 5.落子成功
// 根据返回值来判断是否落子成功
//
// 测试removePiece()方法:
// 1.目标位置不在棋盘内 2.目标位置并非对方棋手所有 3.目标位置无棋子 4.提子成功
// 根据返回值来判断是否提子成功
对各种落子,提子可能出现的异常进行测试即可。覆盖了GoAction的所有代码,并且通过。

3.对Game的测试,测试getPlayername,movePiece,placePiece,removePiece,eatPiece,getNumberOfPieces即可
// Testing strategy
// 测试getPlayername()方法:
// init后返回player1name,player2name并与预期的比较即可
//
// 测试movePiece()方法:
// 1.目标位置不在棋盘内 2.目的地有其他棋子 3.初始位置无棋子 4.两个位置相同 5.初始位置并非该棋手所有 6.无该棋手 7.移子成功
// 根据返回值进行判断即可
//
// 测试eatPiece()方法:
// 1.目标位置不在棋盘内 2.初始位置并非该棋手所有 3.初始位置无棋子 4.目标位置无棋子 5.目标位置棋子属于该玩家 6.无该玩家 7.吃子成功
// 根据返回值进行判断即可
//
// 测试placePiece()方法:
// 1.目标位置不在棋盘内 2.目的地有其他棋 3.棋子并非该棋手所有 4.指定棋子已经在棋盘上 5.无该棋手 6.落子成功
// 根据返回值来判断是否落子成功
//
// 测试removePiece()方法:
// 1.目标位置不在棋盘内 2.目标位置并非对方棋手所有 3.目标位置无棋子 4.无该玩家 5.提子成功
// 根据返回值来判断是否提子成功
//
// 测试getNumberOfPieces()方法:
// 以chess为例,吃子后分别计算两个玩家的棋子数即可,测试没有的玩家
// 根据实际棋子数与预期棋子数是否相等即可
考虑各种异常情况并进行测试即可。覆盖了Game的大部分代码。且全部通过。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值