一文解决 web3 合约 ethers 交互基础

  • 1_bit
  • 更新于 2022-12-21 13:44
  • 阅读 6139

一文解决 web3 合约 ethers 交互基础

文章参考:https://wtf.academy/ether-start/ReadContract/

一、查询以太坊主网某地址的余额

安装好 erthers 后,通过 import 对其引入:

import { ethers } from "ethers";

引入完毕后,可以 get 一个 Provider 对以太坊网络进行只读访问;通过 ethers 获取 Provider 对象:

 const provider = ethers.getDefaultProvider();

获得了对应的 provider 之后,接下来可以使用 async await 语法糖创建一个异步函数:

const main=async()=>{

}

接着可以在函数中通过 provider 调用 getBalance 方法去获取某个地址的余额:

//查询某个地址的余额
const balance = await provider.getBalance(`0xeB869f9835006A829120cC62d995570997e6322F`);

由于这个 balance 是一个 bigNumber 对象,我们需要通过 ethers 提供的 formatEther 方法将其转化为 eth 单位才可以获得准确的 eth 单位的数值,此时若你直接输出将会显示如下:

在这里插入图片描述 此时代码应为:

console.log(`0xcEb1C2fB4198E865B33d9cEaDc7d3bbEcD759aB3: ` + ethers.utils.formatEther(balance));

完整代码如下:

import { ethers } from "ethers";
const provider = ethers.getDefaultProvider();
const main = async () => {
    const balance = await provider.getBalance("0xcEb1C2fB4198E865B33d9cEaDc7d3bbEcD759aB3");
    console.log(`0xcEb1C2fB4198E865B33d9cEaDc7d3bbEcD759aB3: ` + ethers.utils.formatEther(balance));
}
main()

输出结果为:

在这里插入图片描述

二、指定测试网络的查询余额

在正式发布主网合约前我们一般是在测试网络上部署对应的合约,有时候需要查询测试网络的数据,那么就需要使用 providers 下的 JsonRpcProvider 指定网络,代码如下:

const providerLocal = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`)

这个地址是我本地的测试网络,我是使用 Ganache 搭建的本地网络:

在这里插入图片描述

此时的地址是 HTTP://127.0.0.1:7545,我们指定后即可链接到本地测试网。

此时我们复制其中一个地址对其进行查询余额: 在这里插入图片描述 修改第一点中的代码中查询的地址以及 provider 对象:

const balance = await providerLocal.getBalance("0x4c63Fd8500Dc73dd72cB999c5Cf305F2c846b2f8");

此时完整代码如下:

import { ethers } from "ethers";
const providerLocal = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`)
const main = async () => {
    const balance = await providerLocal.getBalance("0x4c63Fd8500Dc73dd72cB999c5Cf305F2c846b2f8");
    console.log(`0x4c63Fd8500Dc73dd72cB999c5Cf305F2c846b2f8: ` + ethers.utils.formatEther(balance));
}
main()

结果如下:

在这里插入图片描述

三、查询链接到的网络地址

我们可以通过 provider 提供的 getNetwork 方法查询对应的provider 所链接的网络,在此我们在一个示例中同时链接本地网络以及主网,直接通过对应的 provider 调用 getNetwork 方法可以得到网络标识:

import { ethers } from "ethers";
const providerLocal = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`)
const provider = ethers.getDefaultProvider();
const main = async () => {
    const balance = await providerLocal.getBalance("0x4c63Fd8500Dc73dd72cB999c5Cf305F2c846b2f8");
    console.log(`0x4c63Fd8500Dc73dd72cB999c5Cf305F2c846b2f8: ` + ethers.utils.formatEther(balance));

    const networkLocal = await providerLocal.getNetwork();//getNetwork
    const networkMain = await provider.getNetwork();//getNetwork
    console.log("network:", networkLocal);
    console.log("networkMain:", networkMain);
}
main()

此时结果如下:

在这里插入图片描述

此时可以看到对应的网络。

四、查询区块高度、gas、建议gas、区块、字节码

这些查询都是通过 provider 查询的,provider 可查询在区块网络中的只读信息,查询标题所述的内容只需要调用以下方法:

  • getBlockNumber 区块高度
  • getGasPrice gas价
  • getFeeData 建议gas等信息
  • getBlock 某个区块信息
  • getCode 某个合约的字节码

具体使用如下:

const blockNumber = await provider.getBlockNumber();
const gasPrice = await provider.getGasPrice();
const feeData = await provider.getFeeData();
const block = await provider.getBlock([某个区块号]);
const code = await provider.getCode([合约地址]]);

在这里需要注意 gasPrice 是一个 bigNumber 需要使用 erthers 中的 format 对其格式化:

console.log("gas 价:", ethers.utils.formatUnits(gasPrice));

而 feeData 是一个对象数组:

在这里插入图片描述 可以使用不同的值,例如:

console.log("建议 gas 价:", ethers.utils.formatUnits(feeData.gasPrice));

五、合约操作

在使用合约中的方法时,有不消耗 gas 的只读方法,也有修改状态变量需要 gas 的方法,那么在 ethers 中由于两者的特性不同,分为了只读合约和可读写合约,只读合约不需要钱包对其进行操作,可读写合约需要钱包进行操作。

5.1 只读合约

只读合约是创建一个对合约操作的对象后,只能对合约的只读方法进行调用。

首先指定链接的节点:

import { ethers } from "ethers";
const providerLocal = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`);

接着需要我们获取合约的 abi:

const ContractAbi = '[{"inputs": [{"internalType": "address","name": "_owner","type": "address"}],"name": "setOwner","outputs": [],"stateMutability": "nonpayable","type": "function"},{"inputs": [],"stateMutability": "nonpayable","type": "constructor"},{"inputs": [],"name": "getOwner","outputs": [{"internalType": "address","name": "","type": "address"}],"stateMutability": "view","type": "function"}]';

合约的 abi 若你是 remix 进行部署的,可以在这里找到: 在这里插入图片描述 有了 abi 后我们还需要你部署后的合约的地址:

const ContractAddress = '0x4562645a0d04a536000d5E10a494C6d07Be1e314';

最后我们使用 ethers 的 Contract 创建合约对象,需要传入合约地址、abi 以及 provider:

const _Contract = new ethers.Contract(ContractAddress, ContractAbi, providerLocal);

此时我们就拥有了一个 Contract 对象,可以使用这个 Contract 对象对里面的方法进行调用了:

const ownerVal = await _Contract.getOwner();
console.log("ownerVal:", ownerVal);

完整代码如下:

import { ethers } from "ethers";
const providerLocal = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`);
//创建合约对象
const ContractAbi = '[{"inputs": [{"internalType": "address","name": "_owner","type": "address"}],"name": "setOwner","outputs": [],"stateMutability": "nonpayable","type": "function"},{"inputs": [],"stateMutability": "nonpayable","type": "constructor"},{"inputs": [],"name": "getOwner","outputs": [{"internalType": "address","name": "","type": "address"}],"stateMutability": "view","type": "function"}]';
const ContractAddress = '0x4562645a0d04a536000d5E10a494C6d07Be1e314';
const _Contract = new ethers.Contract(ContractAddress, ContractAbi, providerLocal);

const main = async () => {
    const ownerVal = await _Contract.getOwner();
    console.log("ownerVal:", ownerVal);
}
main()

5.2 友好的 abi

在上一点中,我们使用的 abi 并不是非常友好,咱们编写 abi 还有一种比较简单的 函数签名的方式 编写abi:

const ContractAbi = [
    "function getOwner()view public returns(address)",
];

其实简单点来说你就把函数签名进行复制过来就ok了,由于我们只是一个只读合约,复制一个只读方法即可,这样也可以完成上一点的内容。

此时运行代码后: 在这里插入图片描述

5.3 可写合约

进行可写合约时,我们需要某一个账户去支付消耗的 gas,那么此时需要创建一个账户。

创建一个账户可以使用私钥进行创建,此时我们使用 ganache 打开后随便选择一个账户的私钥信息复制: 在这里插入图片描述 随后在接着上一点只读合约代码中进行更改,添加以下代码记录私钥内容:

//私钥
const privateKey = 'bde959799bd8bd8e3d27686087a53b121276f2ea8edff5f213ef799adff9ffee'

随后使用 eth 的 Wallet 方法创建钱包对象,需要传入一个私钥以及一个对应的 provider:

const wallet = new ethers.Wallet(privateKey, providerLocal)

接着我们把原来创建只读合约时指定的 provider 替换成 钱包对象: 在这里插入图片描述

此时即可创建一个读写合约,不过此时我们还需要更改 ABI 内容,毕竟现在的 ABI 只有一个只读方法:

const ContractAbi = [
    "function getOwner()view public returns(address)",
    "function setOwner(address _owner) public"
];

接着我们可以调用 setOwner 设置 owner 的值:

await _Contract.setOwner("0x503063dD8f114059B09FD5bC953E71fc14a1d672");

完整代码如下:

import { ethers } from "ethers";
const providerLocal = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`);
//私钥
const privateKey = 'bde959799bd8bd8e3d27686087a53b121276f2ea8edff5f213ef799adff9ffee'
const wallet = new ethers.Wallet(privateKey, providerLocal)
//读写合约
const ContractAddress = '0x4562645a0d04a536000d5E10a494C6d07Be1e314';
const ContractAbi = [
    "function getOwner()view public returns(address)",
    "function setOwner(address _owner) public"
];
const _Contract = new ethers.Contract(ContractAddress, ContractAbi, wallet);

const main = async () => {
    //显示原本的 owner
    let ownerVal = await _Contract.getOwner();
    console.log("原本的 owner:", ownerVal);
    //调用 setOwner 更改 owner
    await _Contract.setOwner("0x503063dD8f114059B09FD5bC953E71fc14a1d672");
    //更改后的 owner
    ownerVal = await _Contract.getOwner();
    console.log("更改后的 owner:", ownerVal);
}
main()

最后结果如下: 在这里插入图片描述

5.4 合约部署

合约部署主要是通过合约工厂方法 ContractFactory 对合约进行部署,需要传入部署合约的 ABI、字节码、以及一个钱包对象(毕竟需要花钱)。

既然已经知道了合约部署的主要方法,那么接下来操作就简单了,首先指定 provider:

import { ethers } from "ethers";
const providerLocal = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`);

接着创建钱包:

//创建钱包
const privateKey = 'bde959799bd8bd8e3d27686087a53b121276f2ea8edff5f213ef799adff9ffee';
const wallet = new ethers.Wallet(privateKey, providerLocal);

得到 ABI 和字节码:

//若构造函数有参数那么需要在 abi 中加上构造函数
const abi = [
    "function getOwner()view public returns(address)",
    "function setOwner(address _owner) public"
];
//字节码
const bytecode = '608060405234801561001057600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506101d1806100606000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c806313af40351461003b578063893d20e814610057575b600080fd5b61005560048036038101906100509190610144565b610075565b005b61005f6100b8565b60405161006c9190610180565b60405180910390f35b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610111826100e6565b9050919050565b61012181610106565b811461012c57600080fd5b50565b60008135905061013e81610118565b92915050565b60006020828403121561015a576101596100e1565b5b60006101688482850161012f565b91505092915050565b61017a81610106565b82525050565b60006020820190506101956000830184610171565b9291505056fea2646970667358221220486a69de337d6a3ede1bd3652b7f28e79df06d6e07dfc307e656a54ff44c42f464736f6c63430008110033';

字节码在这里找: 在这里插入图片描述 接着直接调用工厂合约:

//工厂合约部署
const _Contract = await new ethers.ContractFactory(abi, bytecode, wallet).deploy();

deploy() 方法表示部署,之后就等链上确认,部署完毕:

await _Contract.deployed();//等待合约部署完毕

可以查看当前合约的地址和一些部署详情:

console.log("合约地址:", _Contract.address);
console.log("部署合约后的交易详情:", _Contract.deployTransaction);

最后还可以调用以下方法,完整代码如下:

import { ethers } from "ethers";
const providerLocal = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`);

//创建钱包
const privateKey = 'bde959799bd8bd8e3d27686087a53b121276f2ea8edff5f213ef799adff9ffee';
const wallet = new ethers.Wallet(privateKey, providerLocal);
//若构造函数有参数那么需要在 abi 中加上构造函数
const abi = [
    "function getOwner()view public returns(address)",
    "function setOwner(address _owner) public"
];
//字节码
const bytecode = '608060405234801561001057600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506101d1806100606000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c806313af40351461003b578063893d20e814610057575b600080fd5b61005560048036038101906100509190610144565b610075565b005b61005f6100b8565b60405161006c9190610180565b60405180910390f35b806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610111826100e6565b9050919050565b61012181610106565b811461012c57600080fd5b50565b60008135905061013e81610118565b92915050565b60006020828403121561015a576101596100e1565b5b60006101688482850161012f565b91505092915050565b61017a81610106565b82525050565b60006020820190506101956000830184610171565b9291505056fea2646970667358221220486a69de337d6a3ede1bd3652b7f28e79df06d6e07dfc307e656a54ff44c42f464736f6c63430008110033';

const main = async () => {
    //工厂合约部署
    const _Contract = await new ethers.ContractFactory(abi, bytecode, wallet).deploy();
    await _Contract.deployed();//等待合约部署完毕

    console.log("合约地址:", _Contract.address);
    console.log("部署合约后的交易详情:", _Contract.deployTransaction);
    //调用方法
    //显示原本的 owner
    let ownerVal = await _Contract.getOwner();
    console.log("原本的 owner:", ownerVal);
    //调用 setOwner 更改 owner
    await _Contract.setOwner("0x7A85F346BbC42769cE13910fCF878211A767FF1F");
    //更改后的 owner
    ownerVal = await _Contract.getOwner();
    console.log("更改后的 owner:", ownerVal);
}
main()

结果如下: 在这里插入图片描述

六、账户操作

6.1 转账

上一点我们已经知道如何创建钱包,那么接下来就通过钱包对某个地址发起转账。

创建一个转账的交易只需要在对应的一个钱包基础上创建一个 tx 对象:

const tx = {
    to: "0xB464C27f8d481D3a733f91B6059A2AF26CC9e5AB",
    value: ethers.utils.parseEther("30");//此时是 eth 作为单位
}

这个 tx 对象中的 to 表示向某个地址转账,value 表示转账的金额,在此是用 utils 中的 parseEther 将转账的金额单位设置为 eth,转账 30 个eth。

完整代码如下:

import { ethers } from "ethers";
const providerLocal = new ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`);

//私钥
const privateKey = 'bde959799bd8bd8e3d27686087a53b121276f2ea8edff5f213ef799adff9ffee'
const wallet = new ethers.Wallet(privateKey, providerLocal)
const tx = {
    to: "0xB464C27f8d481D3a733f91B6059A2AF26CC9e5AB",
    value: ethers.utils.parseEther("30")//此时是 eth 作为单位
}

const main = async () => {
    //发送交易 返回一个交易详情
    const receipt = await wallet.sendTransaction(tx);
    //等待确认
    await receipt.wait();
    console.log(receipt);
}
main()

此时我设置的账户是第一个: 在这里插入图片描述 运行后交易完毕:

在这里插入图片描述

在 ganache 中查看对应的 eth 余额,发现另一个账户新增了 30,我选择的账户减少了 30:

在这里插入图片描述 你也可以打印对应的交易详情: 在这里插入图片描述

6.2 账户信息

账户还可以查看对应的信息:

> import { ethers } from "ethers"; const providerLocal = new
> ethers.providers.JsonRpcProvider(`http://127.0.0.1:7545`);
> 
> //私钥 const privateKey =
> 'bde959799bd8bd8e3d27686087a53b121276f2ea8edff5f213ef799adff9ffee'
> const wallet = new ethers.Wallet(privateKey, providerLocal) const tx =
> {
>     to: "0xB464C27f8d481D3a733f91B6059A2AF26CC9e5AB",
>     value: ethers.utils.parseEther("30")//此时是 eth 作为单位 }
> 
> console.log("私钥:" + wallet.privateKey); console.log("交易次数:" + (await
> wallet.getTransactionCount()));

在这里插入图片描述

七、alchemy

在平常的开发测试中,使用 alchemy 可以使我们在连接到各个节点,例如 goerli 测试网。

alchemy 的 官网:alchemy.com

注册之后,打开 Dashboard 点击 create App 创建应用: 在这里插入图片描述 输入对应的项目名信息,选择对应的网络即可创建: 在这里插入图片描述 在你创建的内容中,点击 view key 可以查看链接的 PRC: 在这里插入图片描述 等下我们就在 ethers 中使用 https 的链接方式: 在这里插入图片描述

八、事件

首先我们需要将一个合约部署在测试网:

// SPDX-License-Identifier:MIT
pragma solidity ^0.8.17;
contract test{
    address owner;
    constructor(){
        owner=msg.sender;
    }
    event Transfer(address indexed msg, address indexed to, uint256 indexed number);

    function getOwner()view public returns(address){
        return owner;
    }

    function setOwner(address _owner) public{
        owner=_owner;
        emit Transfer(msg.sender, _owner, 10);
    }
}

部署完毕后开始测试。

8.1 检索得到某一合约的事件

刚刚部署的合约中有一个事件:

event Transfer(address indexed msg, address indexed to, uint256 indexed number);

我们可以调用 setOwner 方法对其触发,触发完毕后,开始编写 ethers 代码。

首先引入 eth 并且指定对应的网络,这个时候你就需要拿你在 alchemy 中得到的 https 链接过来了:

import { ethers } from "ethers";
const provider = new ethers.providers.JsonRpcProvider('这里是你的alchemy 链接');

指定完毕后,需要你的这个事件的 abi:

const abi = [
    "event Transfer(address indexed msg, address indexed to, uint256 indexed number)"
];

接着是你这个合约部署的地址:

const address = '0x37F64f179e4549580a15e20c2Ee89B776003Ca30';

创建一个合约的操作对象:

const contract = new ethers.Contract(address, abi, provider);

此时我们就需要准备使用 contract 的 queryFilter 获取当前合约的事件了。queryFilter 接收 3 个参数,分别是 事件名、起始区块和结束区块。此时我们就需要得到对应的起始区块和结束区块,我们可以对这个范围内的事件信息做检索,找到我们当前合约所释放的时间。(注意,这个区块范围是你查找的这个事件的区间)

接着通过 getBlockNumber 获取当前区块高度:

const height= await provider.getBlockNumber();

随后我们可以通过 queryFilter 检索这个区块区间内的时间事件了:

const transferEvents = await contract.queryFilter('Transfer', height - 5000, height);

随后可以打印出第一个,当然你也可以遍历输出:

console.log(transferEvents[0]);

由于我是昨天晚上释放的事件,所以我就减去了 5000 的区块范围,否则就找不到了。

完整代码如下:

import { ethers } from "ethers";
const provider = new ethers.providers.JsonRpcProvider(`https://eth-goerli.g.alchemy.com/v2/1YDyu2dfaCyIDHFsVfI_0qzBDYC19qAA`)

const abi = [
    "event Transfer(address indexed msg, address indexed to, uint256 indexed number)"
];
const address = '0x37F64f179e4549580a15e20c2Ee89B776003Ca30';
const contract = new ethers.Contract(address, abi, provider);

const height = await provider.getBlockNumber();
const transferEvents = await contract.queryFilter('Transfer', height - 5000, height);
console.log('事件:');
console.log(transferEvents[0]);

8.2 监听合约释放的事件

监听合约事件我们使用 contract.on 或者 contract.once,一个是监听一个是只监听一次。

我们可以通过这个监听事件监听某一个交易所的充值提现,例如监听币安的提现充值。

咱们可以在币安的帮助文档可以找到合约地址:https://www.binance.com/zh-CN/support/faq/360040487711

在这里插入图片描述

进去查看后可以找到事件:

在这里插入图片描述

我们可以查看币安的合约事件,从而得到 abi:

在这里插入图片描述 那么此时已经得到了地址和abi,那么我们开始编写对应监听币安的 transfer 事件。首先我们可以在 alchemy.com 中创建对应的网络 RPC 地址供我们链接到以太坊网络,复制后编写代码:

import { ethers } from "ethers";
const provider = new ethers.providers.JsonRpcProvider('更改为你的 RPC 也就是那个 HTTPS(这里是主网)');

接着弄一个变量存那个合约地址:

const address = '0xdac17f958d2ee523a2206206994597c13d831ec7';

创建 abi:

const abi = [
    "event Transfer(address indexed from, address indexed to, uint value)"
];

接着创建一个对应的合约对象:

const contract = new ethers.Contract(address, abi, provider);

接着开始使用 contract.on 对合约进行监听,contract.on 第一个参数是你的事件名,接着是一个function,这个function接收的参数是对应的事件参数,可以得到该事件参数的值:

console.log("币安的合约 transfer 监听:");
contract.on('Transfer', (from, to, value) => {
    console.log(
        '从 ' + from + ' 转账到' + to + ' ' + ethers.utils.formatUnits(ethers.BigNumber.from(value))
    );
});

完整代码如下:

import { ethers } from "ethers";
const provider = new ethers.providers.JsonRpcProvider('https://eth-mainnet.g.alchemy.com/v2/OcBb0EJSh2QoQQoaAO30ndviX4r6GRd0');
const address = '0xdac17f958d2ee523a2206206994597c13d831ec7';
const abi = [
    "event Transfer(address indexed from, address indexed to, uint value)"
];

const contract = new ethers.Contract(address, abi, provider);
console.log("币安的合约 transfer 监听:");
contract.on('Transfer', (from, to, value) => {
    console.log(
        '从 ' + from + ' 转账到' + to + ' ' + ethers.utils.formatUnits(ethers.BigNumber.from(value),6)
        //引:6为token有效小数位。查到的余额除以有效小数位才是实际余额
    );
});

结果如下: 在这里插入图片描述

8.3 合约过滤

九、ethers 中的 eth 单位转化

在以太坊中 1 个 eth 等于 10^18 wei,在 ethers 中提供了相关的函数给我们互相之间进行转化。

十、查看合约是否是 ERC721 标准

查看合约是不是 ERC721 其实简单,直接通过provider 指定了 RPC 后,创建合约对象去调用 supportsInterface 接口即可。

首先指定 provider:

import { ethers } from "ethers";
const provider = new ethers.providers.JsonRpcProvider([这里改成你指定的RPC]);

接着是对应的 abi 和 address :

const abi = [
    "function supportsInterface(bytes4) public view returns(bool)",//改成自己的
];
const address = "合约地址";

接着创建合约以及对应的传入接口id:

const contract = new ethers.Contract(address, abi, provider);
const isERC721 = await contract.supportsInterface('0x80ac58cd');

最后打印即可:

console.log('isERC721?', isERC721)

在这里插入图片描述 接口 id 我们可以在以太坊改进建议 EIP 中得到,例如你要得到 721 的就去看 721的: https://eips.ethereum.org/EIPS/eip-721在这里插入图片描述

点赞 2
收藏 9
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
1_bit
1_bit
转区块链中(求职)... InfoQ签约作者 蓝桥签约作者 CSDN、51、InfoQ专家、 2020年博客之星TOP5 CSDN第二季新星评委 CSDN新星导师 2021年博客新星评委 自媒体程序员 2021Infoq社区年度社区荣誉共建奖