安卓编写区块链的尝试(失败)(1)

*/

private String currentBlockHash;

/**

  • 构造函数

  • @param currentBlockHash

*/

public BlockchainIterator(String currentBlockHash) {

this.currentBlockHash = currentBlockHash;

}

/**

  • 判断是否有下一个区块

  • @return

*/

public boolean hashNext() {

if (ByteUtils.ZERO_HASH.equals(currentBlockHash)) {

return false;

}

Block lastBlock = RocksDBUtils.getInstance().getBlock(currentBlockHash);

if (lastBlock == null) {

return false;

}

// 如果是创世区块

if (ByteUtils.ZERO_HASH.equals(lastBlock.getPrevBlockHash())) {

return true;

}

return RocksDBUtils.getInstance().getBlock(lastBlock.getPrevBlockHash()) != null;

}

/**

  • 迭代获取区块

  • @return

*/

public Block next() {

Block currentBlock = RocksDBUtils.getInstance().getBlock(currentBlockHash);

if (currentBlock != null) {

this.currentBlockHash = currentBlock.getPrevBlockHash();

return currentBlock;

}

return null;

}

}

/**

  • 添加方法,用于获取迭代器实例

  • @return

*/

public BlockchainIterator getBlockchainIterator() {

return new BlockchainIterator(lastBlockHash);

}

/**

  • 打包交易,进行挖矿

  • @param transactions

*/

public void mineBlock(List transactions) throws Exception {

String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash();

Block lastBlock = RocksDBUtils.getInstance().getBlock(lastBlockHash);

if (lastBlockHash == null) {

throw new Exception("ERROR: Fail to get last block hash ! ");

}

Block block = Block.newBlock(lastBlockHash, transactions,lastBlock.getHeight()+1);

this.addBlock(block);

}

/**

  • 从交易输入中查询区块链中所有已被花费了的交易输出

  • @param address 钱包地址

  • @return 交易ID以及对应的交易输出下标地址

  • @throws Exception

*/

private Map<String, int[]> getAllSpentTXOs(String address) {

// 定义TxId ——> spentOutIndex[],存储交易ID与已被花费的交易输出数组索引值

Map<String, int[]> spentTXOs = new HashMap<>();

for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) {

Block block = blockchainIterator.next();

for (Transaction transaction : block.getTransactions()) {

// 如果是 coinbase 交易,直接跳过,因为它不存在引用前一个区块的交易输出

if (transaction.isCoinbase()) {

continue;

}

for (TXInput txInput : transaction.getInputs()) {

if (txInput.canUnlockOutputWith(address)) {

String inTxId = Hex.encodeHexString(txInput.getTxId());

int[] spentOutIndexArray = spentTXOs.get(inTxId);

if (spentOutIndexArray == null) {

spentTXOs.put(inTxId, new int[]{txInput.getTxOutputIndex()});

} else {

spentOutIndexArray = ArrayUtils.add(spentOutIndexArray, txInput.getTxOutputIndex());

spentTXOs.put(inTxId, spentOutIndexArray);

}

}

}

}

}

return spentTXOs;

}

/**

  • 查找钱包地址对应的所有未花费的交易

  • @param address 钱包地址

  • @return

*/

private Transaction[] findUnspentTransactions(String address) throws Exception {

Map<String, int[]> allSpentTXOs = this.getAllSpentTXOs(address);

Transaction[] unspentTxs = {};

// 再次遍历所有区块中的交易输出

for (BlockchainIterator blockchainIterator = this.getBlockchainIterator(); blockchainIterator.hashNext(); ) {

Block block = blockchainIterator.next();

for (Transaction transaction : block.getTransactions()) {

String txId = Hex.encodeHexString(transaction.getTxId());

int[] spentOutIndexArray = allSpentTXOs.get(txId);

for (int outIndex = 0; outIndex < transaction.getOutputs().length; outIndex++) {

if (spentOutIndexArray != null && ArrayUtils.contains(spentOutIndexArray, outIndex)) {

continue;

}

// 保存不存在 allSpentTXOs 中的交易

if (transaction.getOutputs()[outIndex].canBeUnlockedWith(address)) {

unspentTxs = ArrayUtils.add(unspentTxs, transaction);

}

}

}

}

return unspentTxs;

}

/**

  • 查找钱包地址对应的所有UTXO

  • @param address 钱包地址

  • @return

*/

public TXOutput[] findUTXO(String address) throws Exception {

Transaction[] unspentTxs = this.findUnspentTransactions(address);

TXOutput[] utxos = {};

if (unspentTxs == null || unspentTxs.length == 0) {

return utxos;

}

for (Transaction tx : unspentTxs) {

for (TXOutput txOutput : tx.getOutputs()) {

if (txOutput.canBeUnlockedWith(address)) {

utxos = ArrayUtils.add(utxos, txOutput);

}

}

}

return utxos;

}

/**

  • 寻找能够花费的交易

  • @param address 钱包地址

  • @param amount 花费金额

*/

public SpendableOutputResult findSpendableOutputs(String address, int amount) throws Exception {

Transaction[] unspentTXs = this.findUnspentTransactions(address);

int accumulated = 0;

Map<String, int[]> unspentOuts = new HashMap<>();

for (Transaction tx : unspentTXs) {

String txId = Hex.encodeHexString(tx.getTxId());

for (int outId = 0; outId < tx.getOutputs().length; outId++) {

TXOutput txOutput = tx.getOutputs()[outId];

if (txOutput.canBeUnlockedWith(address) && accumulated < amount) {

accumulated += txOutput.getValue();

int[] outIds = unspentOuts.get(txId);

if (outIds == null) {

outIds = new int[]{outId};

} else {

outIds = ArrayUtils.add(outIds, outId);

}

unspentOuts.put(txId, outIds);

if (accumulated >= amount) {

break;

}

}

}

}

return new SpendableOutputResult(accumulated, unspentOuts);

}

/**

  • 从 DB 从恢复区块链数据

  • @return

  • @throws Exception

*/

public static Blockchain initBlockchainFromDB() throws Exception {

String lastBlockHash = RocksDBUtils.getInstance().getLastBlockHash();

if (lastBlockHash == null) {

throw new Exception("ERROR: Fail to init blockchain from db. ");

}

return new Blockchain(lastBlockHash);

}

}

创建ProofOfWork.java(工作量证明)

工作量证明是经过困难的工作,将数据放入区块链中,这样别人就不太可能取修改区块链中的区块,想要修改其中一个块,必须经过大量计算,还要计算这个块之后的块

这里将计算难度设置为16,就是根据nonce计算出来的hash前16位必须为0,这样才能比目标值小,这样添加的区块才能被认可

package com.example.blockchain;

import org.apache.commons.codec.digest.DigestUtils;

import org.apache.commons.lang3.StringUtils;

import java.math.BigInteger;

import lombok.AllArgsConstructor;

import lombok.Data;

/**

  • 工作量证明

  • @author hanru

*/

@Data

@AllArgsConstructor

public class ProofOfWork {

/**

  • 难度目标位

  • 0000 0000 0000 0000 1001 0001 0000 … 0001

  • 256位Hash里面前面至少有16个零

*/

public static final int TARGET_BITS = 16;

/**

  • 要验证的区块

*/

private Block block;

/**

  • 难度目标值

*/

private BigInteger target;

/**

  • 创建新的工作量证明对象

  • 对1进行移位运算,将1向左移动 (256 - TARGET_BITS) 位,得到我们的难度目标值

  • @param block

  • @return

*/

public static ProofOfWork newProofOfWork(Block block) {

/*

1.创建一个BigInteger的数值1.

0000000…00001

2.左移256-bits位

以8 bit为例

0000 0001

0010 0000

8-6

*/

BigInteger targetValue = BigInteger.ONE.shiftLeft((256 - TARGET_BITS));

return new ProofOfWork(block, targetValue);

}

/**

  • 运行工作量证明,开始挖矿,找到小于难度目标值的Hash

  • @return

*/

public PowResult run() {

long nonce = 0;

String shaHex = “”;

// System.out.printf(“开始进行挖矿:%s \n”, this.getBlock().getData());

System.out.printf(“开始进行挖矿: \n”);

long startTime = System.currentTimeMillis();

while (nonce < Long.MAX_VALUE) {

byte[] data = this.prepareData(nonce);

shaHex = DigestUtils.sha256Hex(data);

System.out.printf(“\r%d: %s”,nonce,shaHex);

if (new BigInteger(shaHex, 16).compareTo(this.target) == -1) {

System.out.println();

System.out.printf(“耗时 Time: %s seconds \n”, (float) (System.currentTimeMillis() - startTime) / 1000);

System.out.printf(“当前区块Hash: %s \n\n”, shaHex);

break;

} else {

nonce++;

}

}

return new PowResult(nonce, shaHex);

}

/**

  • 根据block的数据,以及nonce,生成一个byte数组

  • 注意:在准备区块数据时,一定要从原始数据类型转化为byte[],不能直接从字符串进行转换

  • @param nonce

  • @return

*/

private byte[] prepareData(long nonce) {

byte[] prevBlockHashBytes = {};

if (StringUtils.isNoneBlank(this.getBlock().getPrevBlockHash())) {

prevBlockHashBytes = new BigInteger(this.getBlock().getPrevBlockHash(), 16).toByteArray();

}

return ByteUtils.merge(

prevBlockHashBytes,

// this.getBlock().getData().getBytes(),

this.getBlock().hashTransaction(),

ByteUtils.toBytes(this.getBlock().getTimeStamp()),

ByteUtils.toBytes(TARGET_BITS),

ByteUtils.toBytes(nonce)

);

}

/**

  • 验证区块是否有效

  • @return

*/

public boolean validate() {

byte[] data = this.prepareData(this.getBlock().getNonce());

return new BigInteger(DigestUtils.sha256Hex(data), 16).compareTo(this.target) == -1;

}

}

创建TXInput.java(交易中的输入)

package com.example.blockchain;

import lombok.AllArgsConstructor;

import lombok.Data;

import lombok.NoArgsConstructor;

@AllArgsConstructor

@NoArgsConstructor

@Data

public class TXInput {

/**

  • txId是前一次交易的ID

*/

private byte[] txId;

/**

  • 交易输出索引

*/

private int txOutputIndex;

/**

  • 解锁脚本

*/

private String scriptSig;

/**

  • 判断解锁数据是否能够解锁交易输出

  • @param unlockingData

  • @return

*/

public boolean canUnlockOutputWith(String unlockingData) {

return this.getScriptSig().endsWith(unlockingData);

}

}

创建TXOutput.java(交易中的输出)

package com.example.blockchain;

import lombok.AllArgsConstructor;

import lombok.Data;

import lombok.NoArgsConstructor;

/**

  • @author hanru

*/

@Data

@AllArgsConstructor

@NoArgsConstructor

public class TXOutput {

/**

  • 数值金额

*/

private int value;

/**

  • 锁定脚本

*/

private String scriptPubKey;

/**

  • 判断解锁数据是否能够解锁交易输出

  • @param unlockingData

  • @return

*/

public boolean canBeUnlockedWith(String unlockingData) {

return this.getScriptPubKey().endsWith(unlockingData);

}

}

创建 Transaction.java

package com.example.blockchain;

import org.apache.commons.codec.binary.Hex;

import org.apache.commons.codec.digest.DigestUtils;

import org.apache.commons.lang3.ArrayUtils;

import org.apache.commons.lang3.StringUtils;

import java.util.Iterator;

import java.util.Map;

import lombok.AllArgsConstructor;

import lombok.Data;

import lombok.NoArgsConstructor;

@Data

@AllArgsConstructor

@NoArgsConstructor

public class Transaction {

private static final int SUBSIDY = 10;

/**

  • 交易的Hash

*/

private byte[] txId;

/**

  • 交易输入

*/

private TXInput[] inputs;

/**

  • 交易输出

*/

private TXOutput[] outputs;

/**

  • 设置交易ID

*/

private void setTxId() {

this.setTxId(DigestUtils.sha256(SerializeUtils.serialize(this)));

}

/**

  • 创建CoinBase交易

  • @param to 收账的钱包地址

  • @param data 解锁脚本数据

  • @return

*/

public static Transaction newCoinbaseTX(String to, String data) {

if (StringUtils.isBlank(data)) {

data = String.format(“Reward to ‘%s’”, to);

}

// 创建交易输入

TXInput txInput = new TXInput(new byte[]{}, -1, data);

// 创建交易输出

TXOutput txOutput = new TXOutput(SUBSIDY, to);

// 创建交易

Transaction tx = new Transaction(null, new TXInput[]{txInput}, new TXOutput[]{txOutput});

// 设置交易ID

tx.setTxId();

return tx;

}

/**

  • 从 from 向 to 支付一定的 amount 的金额

  • @param from 支付钱包地址

  • @param to 收款钱包地址

  • @param amount 交易金额

  • @param blockchain 区块链

  • @return

*/

public static Transaction newUTXOTransaction(String from, String to, int amount, Blockchain blockchain) throws Exception {

SpendableOutputResult result = blockchain.findSpendableOutputs(from, amount);

int accumulated = result.getAccumulated();

Map<String, int[]> unspentOuts = result.getUnspentOuts();

if (accumulated < amount) {

throw new Exception(“ERROR: Not enough funds”);

}

Iterator<Map.Entry<String, int[]>> iterator = unspentOuts.entrySet().iterator();

TXInput[] txInputs = {};

while (iterator.hasNext()) {

Map.Entry<String, int[]> entry = iterator.next();

String txIdStr = entry.getKey();

int[] outIdxs = entry.getValue();

byte[] txId = Hex.decodeHex(txIdStr.toCharArray());

for (int outIndex : outIdxs) {

txInputs = ArrayUtils.add(txInputs, new TXInput(txId, outIndex, from));

}

}

TXOutput[] txOutput = {};

txOutput = ArrayUtils.add(txOutput, new TXOutput(amount, to));

if (accumulated > amount) {

txOutput = ArrayUtils.add(txOutput, new TXOutput((accumulated - amount), from));

}

Transaction newTx = new Transaction(null, txInputs, txOutput);

newTx.setTxId();

return newTx;

}

/**

  • 是否为 Coinbase 交易

  • @return

*/

public boolean isCoinbase() {

return this.getInputs().length == 1

&& this.getInputs()[0].getTxId().length == 0

&& this.getInputs()[0].getTxOutputIndex() == -1;

}

}

区块持久化存储
创建SerializeUtils.java

该类将原来的类型转换成byte[]类型,这样可以将其存入数据库中,当我们需要从数据库读取数据,再将其反序列化获得指定object

package com.example.blockchain;

import com.esotericsoftware.kryo.Kryo;

import com.esotericsoftware.kryo.io.Input;

import com.esotericsoftware.kryo.io.Output;

/**

  • 序列化工具类

*/

public class SerializeUtils {

/**

  • 序列化

  • @param object 需要序列化的对象

  • @return

*/

public static byte[] serialize(Object object) {

Output output = new Output(4096, -1);

new Kryo().writeClassAndObject(output, object);

byte[] bytes = output.toBytes();

output.close();

return bytes;

}

/**

  • 反序列化

  • @param bytes 对象对应的字节数组

  • @return

*/

public static Object deserialize(byte[] bytes) {

Input input = new Input(bytes);

Object obj = new Kryo().readClassAndObject(input);

input.close();

return obj;

}

}

原文是使用RocksDB来存储的

package com.example.blockchain;

import org.rocksdb.RocksDB;

import org.rocksdb.RocksDBException;

import java.util.HashMap;

import java.util.Map;

/**

  • 数据库存储的工具类

*/

public class RocksDBUtils {

/**

  • 区块链数据文件

*/

private static final String DB_FILE = “blockchain.db”;

/**

  • 区块桶前缀

*/

private static final String BLOCKS_BUCKET_KEY = “blocks”;

/**

  • 最新一个区块的hash

*/

private static final String LAST_BLOCK_KEY = “l”;

private volatile static RocksDBUtils instance;

/**

  • 获取RocksDBUtils的单例

  • @return

*/

public static RocksDBUtils getInstance() {

if (instance == null) {

synchronized (RocksDBUtils.class) {

if (instance == null) {

instance = new RocksDBUtils();

}

}

}

return instance;

}

private RocksDBUtils() {

openDB();

initBlockBucket();

}

private RocksDB db;

/**

  • block buckets

*/

private Map<String, byte[]> blocksBucket;

/**

  • 打开数据库

*/

private void openDB() {

try {

db = RocksDB.open(DB_FILE);

} catch (RocksDBException e) {

throw new RuntimeException("打开数据库失败。。 ! ", e);

}

}

/**

  • 初始化 blocks 数据桶

*/

private void initBlockBucket() {

try {

//

byte[] blockBucketKey = SerializeUtils.serialize(BLOCKS_BUCKET_KEY);

byte[] blockBucketBytes = db.get(blockBucketKey);

if (blockBucketBytes != null) {

blocksBucket = (Map) SerializeUtils.deserialize(blockBucketBytes);

} else {

blocksBucket = new HashMap<>();

db.put(blockBucketKey, SerializeUtils.serialize(blocksBucket));

}

} catch (RocksDBException e) {

throw new RuntimeException("初始化block的bucket失败。。! ", e);

}

}

/**

  • 保存区块

  • @param block

*/

public void putBlock(Block block) {

try {

blocksBucket.put(block.getHash(), SerializeUtils.serialize(block));

db.put(SerializeUtils.serialize(BLOCKS_BUCKET_KEY), SerializeUtils.serialize(blocksBucket));

} catch (RocksDBException e) {

throw new RuntimeException("存储区块失败。。 ", e);

}

}

/**

  • 查询区块

  • @param blockHash

  • @return

*/

public Block getBlock(String blockHash) {

return (Block) SerializeUtils.deserialize(blocksBucket.get(blockHash));

}

/**

  • 保存最新一个区块的Hash值

  • @param tipBlockHash

*/

public void putLastBlockHash(String tipBlockHash) {

try {

blocksBucket.put(LAST_BLOCK_KEY, SerializeUtils.serialize(tipBlockHash));

db.put(SerializeUtils.serialize(BLOCKS_BUCKET_KEY), SerializeUtils.serialize(blocksBucket));

} catch (RocksDBException e) {

throw new RuntimeException("数据库存储最新区块hash失败。。 ", e);

}

}

/**

  • 查询最新一个区块的Hash值

  • @return

*/

public String getLastBlockHash() {

byte[] lastBlockHashBytes = blocksBucket.get(LAST_BLOCK_KEY);

if (lastBlockHashBytes != null) {

return (String) SerializeUtils.deserialize(lastBlockHashBytes);

}

return “”;

}

/**

  • 关闭数据库

*/

public void closeDB() {

try {

db.close();

} catch (Exception e) {

throw new RuntimeException("关闭数据库失败。。 ", e);

}

}

}

编写main函数先测试

package com.example.blockchain;

import org.apache.commons.codec.binary.Hex;

import java.text.SimpleDateFormat;

import java.util.ArrayList;

import java.util.Date;

import java.util.List;

public class Main {

public static void main(String[] args) throws Exception {

Blockchain.createBlockchain(“test”);

String address = “test”;

// 查询

Blockchain blockchain = Blockchain.createBlockchain(address);

TXOutput[] txOutputs = blockchain.findUTXO(address);

int balance = 0;

if (txOutputs != null && txOutputs.length > 0) {

for (TXOutput txOutput : txOutputs) {

balance += txOutput.getValue();

}

}

System.out.printf(“Balance of ‘%s’: %d\n”, address, balance);

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

**其实上面说了这么多,钱是永远赚不完的,在这个知识付费的时代,知识技能提升才是是根本!我作为一名8年的高级工程师,知识技能已经学习的差不多。**在看这篇文章的可能有刚刚入门,刚刚开始工作,或者大佬级人物。

像刚刚开始学Android开发小白想要快速提升自己,最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以这里分享一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

这么重要的事情说三遍啦!点赞+点赞+点赞!

【Android高级架构师系统学习资料】高级架构师进阶必备——设计思想解读开源框架

第一章、热修复设计
第二章、插件化框架设计
第三章、组件化框架设计
第四章、图片加载框架
第五章、网络访问框架设计
第六章、RXJava 响应式编程框架设计
第七章、IOC 架构设计
第八章、Android 架构组件 Jetpack

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

indUTXO(address);

int balance = 0;

if (txOutputs != null && txOutputs.length > 0) {

for (TXOutput txOutput : txOutputs) {

balance += txOutput.getValue();

}

}

System.out.printf(“Balance of ‘%s’: %d\n”, address, balance);

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-wGbzLymT-1713596433020)]

[外链图片转存中…(img-SFWC9EO1-1713596433021)]

[外链图片转存中…(img-UkY0U5OG-1713596433022)]

[外链图片转存中…(img-eYh7WQi7-1713596433023)]

[外链图片转存中…(img-Dkf2ko7p-1713596433025)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

**其实上面说了这么多,钱是永远赚不完的,在这个知识付费的时代,知识技能提升才是是根本!我作为一名8年的高级工程师,知识技能已经学习的差不多。**在看这篇文章的可能有刚刚入门,刚刚开始工作,或者大佬级人物。

像刚刚开始学Android开发小白想要快速提升自己,最快捷的方式,就是有人可以带着你一起分析,这样学习起来最为高效,所以这里分享一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

这么重要的事情说三遍啦!点赞+点赞+点赞!
[外链图片转存中…(img-5b6DbZ48-1713596433026)]

【Android高级架构师系统学习资料】高级架构师进阶必备——设计思想解读开源框架

第一章、热修复设计
第二章、插件化框架设计
第三章、组件化框架设计
第四章、图片加载框架
第五章、网络访问框架设计
第六章、RXJava 响应式编程框架设计
第七章、IOC 架构设计
第八章、Android 架构组件 Jetpack

[外链图片转存中…(img-SHXZGlzh-1713596433027)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值