16、以太坊DApp开发利器Truffle全解析

以太坊DApp开发利器Truffle全解析

1. Truffle简介

Truffle是一个用于构建基于以太坊的去中心化应用(DApps)的强大工具,它集开发环境、框架和资产管道于一体。作为开发环境,它提供了命令行工具,可用于编译、部署、测试和构建项目;作为框架,它提供了各种包,让编写测试、部署代码以及构建客户端等工作变得轻松;作为资产管道,它支持发布包并使用他人发布的包。

安装Truffle

在开始使用Truffle之前,你需要先进行安装。可以使用npm全局安装Truffle,命令如下:

npm install -g truffle

在继续之前,请确保你正在运行网络ID为10的testrpc,原因与之前讨论的一致。

2. 初始化Truffle项目

2.1 创建项目目录

首先,你需要为你的应用创建一个目录,例如命名为 altcoin

mkdir altcoin
cd altcoin

2.2 初始化项目

altcoin 目录下,运行以下命令来初始化你的项目:

truffle init

初始化完成后,你将得到一个具有以下结构的项目:
- contracts :Truffle期望在此目录中找到Solidity合约文件。
- migrations :用于存放包含合约部署代码的文件。
- test :存放用于测试智能合约的测试文件。
- truffle.js :Truffle的主要配置文件。

默认情况下, truffle init 会为你提供一组示例合约( MetaCoin ConvertLib ),它们类似于基于以太坊构建的简单替代币。以下是 MetaCoin 智能合约的源代码:

pragma Solidity ^0.4.4; 
import "./ConvertLib.sol"; 
contract MetaCoin { 
   mapping (address => uint) balances; 
   event Transfer(address indexed _from, address indexed _to, uint256 _value); 
   function MetaCoin() { 
         balances[tx.origin] = 10000; 
   } 
   function sendCoin(address receiver, uint amount) returns(bool sufficient) { 
         if (balances[msg.sender] < amount) return false; 
         balances[msg.sender] -= amount; 
         balances[receiver] += amount; 
         Transfer(msg.sender, receiver, amount); 
         return true; 
   } 
   function getBalanceInEth(address addr) returns(uint){ 
         return ConvertLib.convert(getBalance(addr),2); 
   } 
   function getBalance(address addr) returns(uint) { 
         return balances[addr]; 
   } 
}

MetaCoin 会将10000个MetaCoin分配给部署合约的账户地址。用户可以使用 sendCoin() 函数将这些MetaCoin发送给其他人,使用 getBalance() 函数随时查询账户余额。假设一个MetaCoin等于两个以太币,你可以使用 getBalanceInEth() 函数查询以太币余额。 ConvertLib 库用于计算MetaCoin的以太币价值,它提供了 convert() 方法。

3. 编译合约

3.1 编译命令

在Truffle中编译合约会生成包含ABI(应用二进制接口)和未链接二进制代码的工件对象。编译命令如下:

truffle compile

Truffle只会编译自上次编译以来发生更改的合约,以避免不必要的编译。如果你想覆盖此行为,可以使用 --all 选项:

truffle compile --all

编译后的工件可以在 build/contracts 目录中找到,你可以根据需要编辑这些文件。这些文件在运行编译和迁移命令时会被修改。

3.2 编译前注意事项

在编译之前,需要注意以下几点:
- 合约文件名必须与合约定义的名称完全匹配。例如,如果文件名为 MyContract.sol ,则合约文件中应存在 contract MyContract{} library myContract{}
- 文件名匹配是区分大小写的,即如果文件名没有大写,合约名也不应大写。
- 可以使用Solidity的 import 命令声明合约依赖关系。Truffle会按正确的顺序编译合约,并在必要时自动链接库。依赖关系必须相对于当前的Solidity文件指定,以 ./ ../ 开头。
- Truffle版本3.1.2使用编译器版本0.4.8,目前不支持更改编译器版本。

4. 配置文件

4.1 默认配置文件

truffle.js 是一个JavaScript文件,用于配置项目。它可以执行任何必要的代码来创建项目配置,并且必须导出一个表示项目配置的对象。以下是默认的配置文件内容:

module.exports = { 
  networks: { 
    development: { 
      host: "localhost", 
      port: 8545, 
      network_id: "*" // Match any network id 
    } 
  } 
};

配置对象可以包含各种属性,最基本的是 networks 属性,它指定了可用于部署的网络以及与每个网络交互时的特定交易参数(如 gasPrice from gas 等)。默认的 gasPrice 是100,000,000,000, gas 是4712388, from 是以太坊客户端中第一个可用的合约。

4.2 自定义配置

你可以根据需要指定任意数量的网络。例如,将配置文件修改为以下内容:

module.exports = { 
  networks: { 
    development: { 
      host: "localhost", 
      port: 8545, 
      network_id: "10" 
    }, 
    live: { 
         host: "localhost", 
      port: 8545, 
      network_id: "1" 
    } 
  } 
};

在上述代码中,我们定义了两个网络: development live

4.3 Windows系统注意事项

在Windows上使用命令提示符时,默认的配置文件名可能会与Truffle可执行文件发生冲突。如果出现这种情况,建议使用Windows PowerShell或Git BASH,因为这些shell不会有此冲突。或者,你可以将配置文件重命名为 truffle-config.js 以避免冲突。

5. 部署合约

5.1 合约部署的网络环境

即使是最小的项目也至少会与两个区块链进行交互:一个在开发者的机器上,如EthereumJS TestRPC;另一个是开发者最终将应用部署的网络(例如以太坊主网或私有联盟网络)。

由于合约抽象在运行时会自动检测网络,这意味着你只需要部署一次应用或前端。当应用运行时,运行中的以太坊客户端将决定使用哪些工件,这使得你的应用非常灵活。

5.2 迁移文件

包含将合约部署到以太坊网络的代码的JavaScript文件称为迁移文件。这些文件负责安排部署任务,并且是基于你的部署需求会随时间变化的假设编写的。随着项目的发展,你将创建新的迁移脚本来推动区块链上的项目演进。

迁移文件的文件名前缀为数字,例如 1_initial_migration.js 2_deploy_contracts.js 。编号前缀用于记录迁移是否成功运行。 Migrations 合约会在 last_completed_migration 中存储一个数字,该数字对应于 migrations 文件夹中最后应用的迁移脚本。 Migrations 合约总是首先部署,编号约定为 x_script_name.js ,其中 x 从1开始。你的应用合约通常从编号2的脚本开始。

5.3 编写迁移脚本

在迁移文件的开头,需要使用 artifacts.require() 方法告诉Truffle你想要与之交互的合约。这个方法类似于Node的 require ,但在我们的场景中,它专门返回一个合约抽象,可在部署脚本的其余部分使用。

所有迁移都必须通过 module.exports 语法导出一个函数。每个迁移导出的函数应接受一个 deployer 对象作为其第一个参数。这个对象通过提供清晰的API来部署智能合约,并执行一些部署的常规任务,如将部署的工件保存到工件文件中以供后续使用、链接库等,协助进行部署。

deployer 对象的方法如下:
- deployer.deploy(contractAbstraction, args..., options) :部署由合约抽象对象指定的特定合约,可选择提供构造函数参数。这对于单例合约很有用,确保你的DApp中只有一个该合约的实例。部署后会设置合约的地址(即工件文件中的 address 属性将等于新部署的地址),并覆盖之前存储的任何地址。你可以选择传递一个合约数组或数组的数组,以加快多个合约的部署。此外,最后一个参数是一个可选对象,可以包含一个键 overwrite 。如果 overwrite 设置为 false ,如果该合约已经部署过, deployer 将不会再次部署。该方法返回一个Promise。
- deployer.link(library, destinations) :将已部署的库链接到一个或多个合约。 destinations 参数可以是单个合约抽象或多个合约抽象的数组。如果目标中的任何合约不依赖于要链接的库, deployer 将忽略该合约。该方法返回一个Promise。
- deployer.then(function(){}) :用于运行任意部署步骤。在迁移过程中使用它来调用特定的合约函数,以添加、编辑和重新组织合约数据。在回调函数中,你可以使用合约抽象API来部署和链接合约。

以下是一个根据网络条件进行部署的示例:

module.exports = function(deployer, network) { 
  if (network != "live") { 
   // Perform a different step otherwise. 
  } else { 
    // Do something specific to the network named "live". 
  } 
}

在项目中,你会找到两个迁移文件: 1_initial_migration.js 2_deploy_contracts.js 。除非你知道自己在做什么,否则不要编辑第一个文件。以下是 2_deploy_contracts.js 文件的代码:

var ConvertLib = artifacts.require("./ConvertLib.sol"); 
var MetaCoin = artifacts.require("./MetaCoin.sol"); 
module.exports = function(deployer) { 
  deployer.deploy(ConvertLib); 
  deployer.link(ConvertLib, MetaCoin); 
  deployer.deploy(MetaCoin); 
};

在上述代码中,我们首先创建了 ConvertLib 库和 MetaCoin 合约的抽象。无论使用哪个网络,我们都会部署 ConvertLib 库,然后将其链接到 MetaCoin 合约,最后部署 MetaCoin 合约。

5.4 运行迁移

要运行迁移(即部署合约),可以使用以下命令:

truffle migrate --network development

这里我们告诉Truffle在 development 网络上运行迁移。如果不提供 --network 选项,Truffle将默认使用名为 development 的网络。

运行上述命令后,Truffle会自动更新 ConvertLib 库和 MetaCoin 合约在工件文件中的地址,并更新链接。

migrate 子命令的其他重要选项如下:
- --reset :从开始运行所有迁移,而不是从最后完成的迁移继续。
- -f number :从特定的迁移开始运行合约。

你可以随时使用 truffle networks 命令查找项目中合约和库在各个网络中的地址。

5.5 迁移流程

graph LR
    A[开始] --> B[创建迁移文件]
    B --> C[编写迁移脚本]
    C --> D[运行迁移命令]
    D --> E[部署合约]
    E --> F[更新工件文件]
    F --> G[结束]

6. 单元测试合约

6.1 单元测试概述

单元测试是一种测试应用的方法,它对应用中最小的可测试部分(称为单元)进行单独和独立的检查,以确保其正常运行。单元测试可以手动进行,但通常是自动化的。

Truffle默认提供了一个单元测试框架,用于自动化测试你的合约。在运行测试文件时,它会提供一个干净的环境,即Truffle会在每个测试文件开始时重新运行所有迁移,以确保你有一组全新的合约进行测试。

6.2 测试方式

Truffle允许你以两种不同的方式编写简单且易于管理的测试:
- JavaScript :从应用客户端测试你的合约。
- Solidity :从其他合约测试你的合约。

两种测试风格各有优缺点,我们将学习这两种编写测试的方法。

6.3 测试文件要求

所有测试文件应位于 ./test 目录中。Truffle只会运行具有以下文件扩展名的测试文件: .js .es .es6 .jsx .sol ,其他文件将被忽略。

6.4 测试客户端选择

在运行自动化测试时, ethereumjs-testrpc 比其他客户端快得多。此外, testrpc 包含一些特殊功能,Truffle可以利用这些功能将测试运行时间缩短近90%。作为一般的工作流程,建议在正常开发和测试期间使用 testrpc ,然后在准备部署到实时或生产网络时,针对 go-ethereum 或其他官方以太坊客户端运行一次测试。

6.5 编写JavaScript测试

Truffle的JavaScript测试框架基于 mocha 构建, mocha 是一个用于编写测试的JavaScript框架,而 chai 是一个断言库。

测试框架用于组织和执行测试,断言库提供了验证事物是否正确的工具。断言库使测试代码变得更加容易,你无需编写数千个 if 语句。大多数测试框架不包含断言库,允许用户选择自己想要使用的断言库。

在继续之前,你需要学习如何使用 mocha chai 编写测试。可以访问 https://mochajs.org/ 学习 mocha ,访问 http://chaijs.com/ 学习 chai

你的测试文件应位于 ./test 目录中,并且以 .js 扩展名结尾。

合约抽象是从JavaScript进行合约交互的基础。由于Truffle无法检测你在测试中需要与哪些合约进行交互,因此你需要使用 artifacts.require() 方法显式请求这些合约。所以在测试文件中首先要做的是为你想要测试的合约创建抽象。

以下是Truffle生成的用于测试 MetaCoin 合约的默认测试代码:

// Specifically request an abstraction for MetaCoin.sol 
var MetaCoin = artifacts.require("./MetaCoin.sol"); 
contract('MetaCoin', function(accounts) { 
  it("should put 10000 MetaCoin in the first account", function() { 
    return MetaCoin.deployed().then(function(instance) { 
      return instance.getBalance.call(accounts[0]); 
    }).then(function(balance) { 
      assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account"); 
    }); 
  }); 
  it("should send coin correctly", function() { 
    var meta; 

    // Get initial balances of first and second account. 
    var account_one = accounts[0]; 
    var account_two = accounts[1]; 
    var account_one_starting_balance; 
    var account_two_starting_balance; 
    var account_one_ending_balance; 
    var account_two_ending_balance; 
    var amount = 10; 
    return MetaCoin.deployed().then(function(instance) { 
      meta = instance; 
      return meta.getBalance.call(account_one); 
    }).then(function(balance) { 
      account_one_starting_balance = balance.toNumber(); 
      return meta.getBalance.call(account_two); 
    }).then(function(balance) { 
      account_two_starting_balance = balance.toNumber(); 
      return meta.sendCoin(account_two, amount, {from: account_one}); 
    }).then(function() { 
      return meta.getBalance.call(account_one); 
    }).then(function(balance) { 
      account_one_ending_balance = balance.toNumber(); 
      return meta.getBalance.call(account_two); 
    }).then(function(balance) { 
      account_two_ending_balance = balance.toNumber(); 
      assert.equal(account_one_ending_balance, account_one_starting_balance - amount, "Amount wasn't correctly taken"); 
      assert.equal(account_two_ending_balance, account_two_starting_balance + amount, "Amount wasn't correctly sent to"); 
    }); 
  }); 
});

在上述代码中,所有合约的交互代码都是使用 truffle-contract 库编写的,代码具有自解释性。

最后,Truffle允许你访问 mocha 的配置,以便更改 mocha 的行为。 mocha 的配置位于 truffle.js 文件导出对象的 mocha 属性下。例如:

mocha: { 
  useColors: true 
}

6.6 编写Solidity测试

以下是一个使用Solidity编写的测试合约示例:

pragma Solidity ^0.4.2; 
import "truffle/Assert.sol"; 
import "truffle/DeployedAddresses.sol"; 
import "../contracts/MetaCoin.sol"; 

contract TestMetacoin { 

  function testInitialBalanceUsingDeployedContract() { 
    MetaCoin meta = MetaCoin(DeployedAddresses.MetaCoin()); 

    uint expected = 10000; 

    Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially"); 
  } 

  function testInitialBalanceWithNewMetaCoin() { 
    MetaCoin meta = new MetaCoin(); 

    uint expected = 10000; 

    Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially"); 
  } 
}

另一个示例:

import "truffle/Assert.sol"; 

contract TestHooks { 
  uint someValue; 

  function beforeEach() { 
    someValue = 5; 
  } 

  function beforeEachAgain() { 
    someValue += 1; 
  } 

  function testSomeValueIsSix() { 
    uint expected = 6; 

    Assert.equal(someValue, expected, "someValue should have been 6"); 
  } 
}

这个测试合约表明,你的测试函数和钩子函数共享相同的合约状态。你可以在测试前设置合约数据,在测试期间使用该数据,并在测试后重置数据以准备下一次测试。需要注意的是,就像JavaScript测试一样,下一个测试函数将从上一个运行的测试函数的状态继续。

Truffle没有提供直接的方法来测试合约是否应该抛出异常(即对于使用 throw 表示预期错误的合约),但可以在 http://truffleframework.com/tutorials/testing-for-throws-in-Solidity-tests 找到一个变通的解决方案。

6.7 测试流程

graph LR
    A[开始] --> B[创建测试文件]
    B --> C[编写测试代码]
    C --> D[运行测试命令]
    D --> E[执行测试]
    E --> F[生成测试报告]
    F --> G[结束]

通过以上步骤,你可以全面地使用Truffle进行以太坊DApp的开发、部署和测试,确保你的项目的质量和灵活性。

7. 总结与最佳实践

7.1 关键步骤总结

为了更清晰地回顾使用Truffle开发以太坊DApp的整个流程,下面以表格形式总结关键步骤:
| 步骤 | 操作内容 | 命令示例 |
| ---- | ---- | ---- |
| 安装Truffle | 使用npm全局安装Truffle | npm install -g truffle |
| 初始化项目 | 创建项目目录并初始化Truffle项目 | mkdir altcoin && cd altcoin && truffle init |
| 编译合约 | 编译合约生成工件对象 | truffle compile truffle compile --all |
| 配置项目 | 编辑 truffle.js 文件配置网络等信息 | |
| 部署合约 | 编写迁移文件并运行迁移命令 | truffle migrate --network development |
| 单元测试 | 编写测试文件并运行测试 | |

7.2 最佳实践建议

  • 合约开发
    • 遵循合约文件名与合约定义名称完全匹配的规则,且注意大小写。
    • 合理使用 import 命令声明合约依赖关系,确保合约编译顺序正确。
  • 迁移文件编写
    • 不要随意修改 1_initial_migration.js 文件,除非你清楚其影响。
    • 利用 deployer 对象的方法,如 deploy link ,高效部署和链接合约。
    • 根据不同网络环境,使用条件语句编写迁移脚本,提高代码的灵活性。
  • 单元测试
    • 在正常开发和测试期间使用 ethereumjs - testrpc 提高测试效率,在部署前针对官方以太坊客户端进行测试。
    • 结合 JavaScript 和 Solidity 两种测试方式,全面测试合约功能。
    • 合理使用 mocha chai 进行断言,确保测试代码的准确性。

7.3 未来展望

随着以太坊技术的不断发展,Truffle也会不断更新和完善。未来可能会支持更多的编译器版本,提供更丰富的测试工具和功能,进一步简化DApp的开发流程。同时,随着区块链应用场景的不断拓展,Truffle在更多领域的应用也值得期待。

8. 常见问题与解决方案

8.1 安装与配置问题

  • 问题 :在Windows上使用命令提示符时,配置文件名与Truffle可执行文件冲突。
  • 解决方案 :使用Windows PowerShell或Git BASH,或者将配置文件重命名为 truffle - config.js

  • 问题 :安装Truffle时出现权限问题。

  • 解决方案 :在Linux或macOS系统上,使用 sudo npm install -g truffle 命令;在Windows系统上,以管理员身份运行命令提示符。

8.2 编译问题

  • 问题 :编译时提示合约文件名与合约定义名称不匹配。
  • 解决方案 :检查合约文件名和合约定义名称,确保它们完全一致,且注意大小写。

  • 问题 :编译时出现依赖关系错误。

  • 解决方案 :检查 import 命令的路径是否正确,依赖关系是否相对于当前的Solidity文件指定。

8.3 部署问题

  • 问题 :迁移脚本运行失败。
  • 解决方案 :检查迁移文件的语法错误,确保 deployer 对象的方法使用正确。同时,检查网络配置是否正确,确保以太坊客户端正常运行。

  • 问题 :合约部署后地址未更新。

  • 解决方案 :检查迁移脚本中 deploy 方法的使用,确保没有设置 overwrite false

8.4 测试问题

  • 问题 :测试运行时出现合约未部署错误。
  • 解决方案 :确保在测试文件开始时使用 artifacts.require() 方法正确请求合约抽象,并且迁移脚本能够正确部署合约。

  • 问题 :测试代码中断言失败。

  • 解决方案 :检查测试代码逻辑,确保合约方法调用和断言条件正确。同时,检查合约的初始状态和方法实现是否符合预期。

9. 拓展学习资源

9.1 官方文档

Truffle的官方文档是学习和使用Truffle的重要资源,它提供了详细的功能介绍、命令参考和示例代码。可以访问 Truffle官方文档 进行学习。

9.2 在线教程

网上有许多关于Truffle的在线教程,例如以太坊官方教程、一些技术博客等。这些教程通常会结合实际案例,帮助你更好地理解和掌握Truffle的使用。

9.3 开源项目

参考一些使用Truffle开发的开源以太坊DApp项目,可以学习到他人的开发经验和最佳实践。可以在GitHub等代码托管平台上搜索相关项目。

9.4 社区论坛

参与以太坊和Truffle的社区论坛,如以太坊官方论坛、Stack Overflow等。在这些论坛上,你可以与其他开发者交流经验,解决遇到的问题。

10. 实战案例分析

10.1 案例背景

假设我们要开发一个简单的众筹DApp,允许用户向项目进行捐款。我们将使用Truffle来完成合约开发、部署和测试。

10.2 合约开发

首先,我们编写众筹合约 Crowdfunding.sol

pragma solidity ^0.4.4;

contract Crowdfunding {
    address public owner;
    mapping(address => uint) public contributions;
    uint public goal;
    uint public totalContributions;

    event ContributionReceived(address contributor, uint amount);

    function Crowdfunding(uint _goal) public {
        owner = msg.sender;
        goal = _goal;
    }

    function contribute() public payable {
        contributions[msg.sender] += msg.value;
        totalContributions += msg.value;
        ContributionReceived(msg.sender, msg.value);
    }

    function checkGoalReached() public view returns (bool) {
        return totalContributions >= goal;
    }
}

10.3 迁移文件编写

创建迁移文件 2_deploy_crowdfunding.js

var Crowdfunding = artifacts.require("./Crowdfunding.sol");

module.exports = function (deployer) {
    deployer.deploy(Crowdfunding, 1000000000000000000); // 目标金额为1以太币
};

10.4 单元测试

编写测试文件 testCrowdfunding.js

var Crowdfunding = artifacts.require("./Crowdfunding.sol");

contract('Crowdfunding', function (accounts) {
    it("should set the correct goal", function () {
        return Crowdfunding.deployed().then(function (instance) {
            return instance.goal.call();
        }).then(function (goal) {
            assert.equal(goal.toNumber(), 1000000000000000000, "Goal was not set correctly");
        });
    });

    it("should accept contributions", function () {
        var crowdfunding;
        var account_one = accounts[0];
        var amount = 100000000000000000;

        return Crowdfunding.deployed().then(function (instance) {
            crowdfunding = instance;
            return crowdfunding.contribute({ from: account_one, value: amount });
        }).then(function () {
            return crowdfunding.contributions.call(account_one);
        }).then(function (contribution) {
            assert.equal(contribution.toNumber(), amount, "Contribution was not recorded correctly");
        });
    });
});

10.5 部署与测试流程

graph LR
    A[开始] --> B[编写合约代码]
    B --> C[编写迁移文件]
    C --> D[编写测试文件]
    D --> E[编译合约]
    E --> F[运行迁移部署合约]
    F --> G[运行测试]
    G --> H[结束]

通过这个实战案例,我们可以更深入地理解如何使用Truffle进行以太坊DApp的开发、部署和测试。

综上所述,Truffle为以太坊DApp开发提供了强大的支持,通过遵循上述步骤和最佳实践,结合常见问题的解决方案,以及拓展学习资源和实战案例的学习,你可以更高效地开发出高质量的以太坊DApp。

内容概要:本文介绍了一个基于冠豪猪优化算法(CPO)的无人机三维路径规划项目,利用Python实现了在复杂三维环境中为无人机规划安、高效、低能耗飞行路径的完整解决方案。项目涵盖空间环境建模、无人机动力学约束、路径编码、多目标代价函数设计以及CPO算法的核心实现。通过体素网格建模、动态障碍物处理、路径平滑技术和多约束融合机制,系统能够在高维、密集障碍环境下快速搜索出满足飞行可行性、安性与能效最优的路径,并支持在线重规划以适应动态环境变化。文中还提供了关键模块的代码示例,包括环境建模、路径评估和CPO优化流程。; 适合人群:具备一定Python编程基础和优化算法基础知识,从事无人机、智能机器人、路径规划或智能优化算法研究的相关科研人员与工程技术人员,尤其适合研究生及有一定工作经验的研发工程师。; 使用场景及目标:①应用于复杂三维环境下的无人机自主导航与避障;②研究智能优化算法(如CPO)在路径规划中的实际部署与性能优化;③实现多目标(路径最短、能耗最低、安性最高)耦合条件下的工程化路径求解;④构建可扩展的智能无人系统决策框架。; 阅读建议:建议结合文中模型架构与代码示例进行实践运行,重点关注目标函数设计、CPO算法改进策略与约束处理机制,宜在仿真环境中测试不同场景以深入理解算法行为与系统鲁棒性。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值