本文详细介绍了如何在Polygon Mumbai测试网络上创建和部署NFT市场智能合约,涵盖了环境设置、合约编写、测试、部署及与NFT市场交互的全过程。同时,提供了相关的代码示例与配置说明,使读者能够清晰理解每一步骤的实现。适合对区块链和智能合约开发有一定了解的读者。
一个非同质化代币 (NFT) 市场是一个用户可以购买和出售称为 NFTs 的独特数字资产的平台。这些数字资产可以代表多种事物,例如可收藏的物品、数字艺术、游戏内物品等等。本指南将教你如何在 Polygon Mumbai 测试网络上使用 Hardhat 创建和部署 NFT 市场智能合约。你还将学习如何使用 Ethers.js 测试和与你已部署的市场合约进行交互。让我们开始吧!
我们将做什么
使用 Hardhat 在 Polygon Mumbai 测试网创建并部署 NFT 市场。
创建一个 NFT(使用 ERC-721 标准),我们将在 NFT 市场上使用它。
使用 Ethers.js 与 NFT 市场智能合约进行交互。
你将需要的
要为我们的智能合约设置 Hardhat 环境,请执行以下一系列终端命令:
mkdir marketplace-hardhat
cd marketplace-hardhat
npm install --save-dev hardhat
npx hardhat
系统会提示你在终端中选择项目类型。选择以下默认配置:
What do you want to do? · Create a JavaScript project
Hardhat project root: · /Users/User/*/marketplace-hardhat
Do you want to add a .gitignore? (Y/n) · y
Do you want to install this sample project's dependencies with npm (@nomicfoundation/hardhat-toolbox)? (Y/n) · y
然后,运行以下命令以安装 OpenZeppelin 库、验证 Hardhat 上智能合约的插件和 dotenv 库,以保护我们的私有数据:
npm install @openzeppelin/contracts dotenv ethers@5.7
npm install --save-dev @nomiclabs/hardhat-etherscan
有关 Hardhat 的更多背景信息,请查看此 QuickNode 指南。
要部署并与我们的 NFT 市场合约进行交互,我们需要一个连接到 Polygon Mumbai 测试网的完整节点。你可以通过查看 Polygon 文档上的 Nodes 选项卡来运行自己的节点。然而,这有时可能很难管理,可能没有我们想要的那样优化。相反,你可以轻松地在 这里 设置一个免费的 QuickNode 帐户并访问 20 多个区块链。QuickNode 的基础设施针对延迟和冗余进行了优化,使其比竞争对手快多达 8 倍。你可以使用 QuickNode 比较工具 来基准测试不同的 RPC 与 QuickNode 的端点。
单击 创建端点 按钮并选择 Polygon 链、Mumbai 测试网 网络。然后,一旦你的端点准备就绪,请保留 HTTP 提供者 URL,因为你将在设置环境变量时需要它。
你还需要一些 Mumbai 测试网上的 MATIC Token来支付交易费用。你可以在 QuickNode Faucet 或 Polygon Mumbai Faucet 获取。
在进入实际代码之前,让我们先了解我们的 NFT 市场合约应包含的功能。它应该能够完成以下操作:
存储已列出 NFT 的详细信息,例如 token ID、token 地址、NFT 类型(ERC-721 或 ERC-1155)、价格和卖家的地址。
允许用户在市场上列出待售的 NFT(通过 createListing 函数)
允许用户购买被列出待售的 NFT(通过 buyNFT 函数)
促进买卖双方之间的 NFT 转让(通过市场合约作为中介)
让用户查看自己已列出和购买的 NFT(通过公共的 getMyListedNFTs 和 getMarketItem 函数)
现在我们知道 NFT 市场合约将如何工作,接下来让我们开始创建市场合约。
导入依赖项并声明合约
进入你在 marketplace-hardhat 文件夹中的 contracts 文件夹,并运行以下命令创建所需的 Solidity 文件:
echo > marketplace.sol
echo > NFT.sol
然后,打开 marketplace.sol 文件并输入以下代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Marketplace is ReentrancyGuard, Ownable {
我们将分段回顾代码,以便全面理解每个部分。如果你不想跟着走,可以自由跳到本节末尾查看完整代码。
我们的 Solidity 文件的第一行是许可证标识符。然后,在第二行,我们定义希望编译的版本指令。^0.8.9 意味着我们可以在 Solidity 版本 0.8.9 及更高版本中编译代码。
接下来,我们导入所有要继承和使用的合约。我们的合约名称将是 Marketplace,它将继承其他合约,例如 ReentrancyGuard 和 Ownable。
创建 NFT 市场的状态
现在,将以下代码粘贴到你之前复制到 marketplace.sol 的代码下方:
using Counters for Counters.Counter;
Counters.Counter private marketplaceIds;
Counters.Counter private totalMarketplaceItemsSold;
mapping(uint => Listing) private marketplaceIdToListingItem;
struct Listing {
uint marketplaceId;
address nftAddress;
uint tokenId;
address payable seller;
address payable owner;
uint listPrice;
}
event ListingCreated(
uint indexed marketplaceId,
address indexed nftAddress,
uint indexed tokenId,
address seller,
address owner,
uint listPrice
);
在上面的代码中,我们对 Counters.Counter 使用 using 关键字来将 Counters 库分配给该变量。我们还创建了两个私有函数,marketplaceIds 和 totalMarketplaceItemsSold,它们将跟踪市场中的 ID 和 NFT 总销售量。
合约还声明了一个映射 marketplaceIdToListingItem,它将 uint 映射到一个名为 Listing 的结构体。这个结构体将保存如 marketplaceId、nftAddress、tokenId、seller、owner 和 listPrice 等数据。
每当用户在市场上列出 NFT 时,将触发事件 ListingCreated。该事件对于实时和历史列表非常有用。
将 NFT 列出到市场
我们的市场还需要逻辑来列出 NFT。将以下代码粘贴到你的 marketplace.sol 文件末尾:
function createListing(
uint tokenId,
address nftAddress,
uint price
) public nonReentrant {
require(price > 0, "List price must be 1 wei >=");
marketplaceIds.increment();
uint marketplaceItemId = marketplaceIds.current();
marketplaceIdToListingItem[marketplaceItemId] = Listing(
marketplaceItemId,
nftAddress,
tokenId,
payable(msg.sender),
payable(address(0)),
price
);
IERC721(nftAddress).transferFrom(msg.sender, address(this), tokenId);
emit ListingCreated(
marketplaceItemId,
nftAddress,
tokenId,
msg.sender,
address(0),
price
);
}
让我们回顾代码。
createListing 公共函数接受三个参数;tokenId、nftAddress 和 price。我们使用 nonReentrant 修饰符防止重入攻击,使用 require 语句确保列出价格大于 1 wei(即 ETH 的最小面额)。其余函数的逻辑包括递增 marketplaceId,将列表的详细信息添加到 Listing 结构体,使用 transferFrom 将代币转移到市场,然后触发 ListingCreated 事件。市场合约将 NFT 安全地存放在市场合约中。这与将列出的 NFT 持有在你的钱包中的方式不同,因为市场合约没有权利随时从你的钱包中获取 NFT(即与你的私钥绑定的账户)。
创建购买列表的功能
function buyListing(uint marketplaceItemId, address nftAddress)
public
payable
nonReentrant
{
uint price = marketplaceIdToListingItem[marketplaceItemId].listPrice;
require(
msg.value == price,
"Value sent does not meet list price for NFT"
);
uint tokenId = marketplaceIdToListingItem[marketplaceItemId].tokenId;
marketplaceIdToListingItem[marketplaceItemId].seller.transfer(msg.value);
IERC721(nftAddress).transferFrom(address(this), msg.sender, tokenId);
marketplaceIdToListingItem[marketplaceItemId].owner = payable(msg.sender);
totalMarketplaceItemsSold.increment();
}
让我们回顾代码。
buyListing 函数是一个公共可支付函数,接受 marketplaceItemId 和 nftAddress。它也使用 nonReentrant 修饰符防止重入。函数的逻辑包括检索列表价格并确保随函数调用发送的值满足列表价格。其余逻辑由将 NFT 转移给买家、在 marketplaceIdToListingItem 映射中更改所有者值以及递增 totalMarketplaceItemsSold 变量组成。
为 NFT 市场创建辅助函数
function getMarketItem(uint marketplaceItemId)
public
view
returns (Listing memory)
{
return marketplaceIdToListingItem[marketplaceItemId];
}
function getMyListedNFTs() public view returns (Listing[] memory) {
uint totalListingCount = marketplaceIds.current();
uint listingCount = 0;
uint index = 0;
for (uint i = 0; i < totalListingCount; i++) {
if (marketplaceIdToListingItem[i + 1].owner == msg.sender) {
listingCount += 1;
}
}
Listing[] memory items = new Listing[](listingCount);
for (uint i = 0; i < totalListingCount; i++) {
if (marketplaceIdToListingItem[i + 1].owner == msg.sender) {
uint currentId = marketplaceIdToListingItem[i + 1].marketplaceId;
Listing memory currentItem = marketplaceIdToListingItem[currentId];
items[index] = currentItem;
index += 1;
}
}
return items;
}
}
让我们回顾代码。
这两个函数是辅助函数,将返回一个市场项目并检索卖家的已列出 NFT。getMyListedNFTs 使用 for 循环 迭代并返回市场项目。
你的完整市场代码应如下所示:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract Marketplace is ReentrancyGuard, Ownable {
using Counters for Counters.Counter;
Counters.Counter private marketplaceIds;
Counters.Counter private totalMarketplaceItemsSold;
mapping(uint => Listing) private marketplaceIdToListingItem;
struct Listing {
uint marketplaceId;
address nftAddress;
uint tokenId;
address payable seller;
address payable owner;
uint listPrice;
}
event ListingCreated(
uint indexed marketplaceId,
address indexed nftAddress,
uint indexed tokenId,
address seller,
address owner,
uint listPrice
);
function createListing(
uint tokenId,
address nftAddress,
uint price
) public nonReentrant {
require(price > 0, "List price must be 1 wei >=");
marketplaceIds.increment();
uint marketplaceItemId = marketplaceIds.current();
marketplaceIdToListingItem[marketplaceItemId] = Listing(
marketplaceItemId,
nftAddress,
tokenId,
payable(msg.sender),
payable(address(0)),
price
);
IERC721(nftAddress).transferFrom(msg.sender, address(this), tokenId);
emit ListingCreated(
marketplaceItemId,
nftAddress,
tokenId,
msg.sender,
address(0),
price
);
}
function buyListing(uint marketplaceItemId, address nftAddress)
public
payable
nonReentrant
{
uint price = marketplaceIdToListingItem[marketplaceItemId].listPrice;
require(
msg.value == price,
"Value sent does not meet list price for NFT"
);
uint tokenId = marketplaceIdToListingItem[marketplaceItemId].tokenId;
marketplaceIdToListingItem[marketplaceItemId].seller.transfer(msg.value);
IERC721(nftAddress).transferFrom(address(this), msg.sender, tokenId);
marketplaceIdToListingItem[marketplaceItemId].owner = payable(msg.sender);
totalMarketplaceItemsSold.increment();
}
function getMarketItem(uint marketplaceItemId)
public
view
returns (Listing memory)
{
return marketplaceIdToListingItem[marketplaceItemId];
}
function getMyListedNFTs() public view returns (Listing[] memory) {
uint totalListingCount = marketplaceIds.current();
uint listingCount = 0;
uint index = 0;
for (uint i = 0; i < totalListingCount; i++) {
if (marketplaceIdToListingItem[i + 1].owner == msg.sender) {
listingCount += 1;
}
}
Listing[] memory items = new Listing[](listingCount);
for (uint i = 0; i < totalListingCount; i++) {
if (marketplaceIdToListingItem[i + 1].owner == msg.sender) {
uint currentId = marketplaceIdToListingItem[i + 1].marketplaceId;
Listing memory currentItem = marketplaceIdToListingItem[currentId];
items[index] = currentItem;
index += 1;
}
}
return items;
}
}
接下来,我们需要创建测试 NFTs,以便在我们的市场合约中使用。对于我们的示例,我们将创建一个 ERC-721 进行测试。
打开 NFT.sol 文件并输入以下代码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract NFT is ERC721, ERC721URIStorage, Ownable {
using Counters for Counters.Counter;
Counters.Counter private _tokenIdCounter;
constructor() ERC721("YOUR_NFTS_NAME", "YOUR_NFTS_SYMBOL") {}
function safeMint(address to, string memory uri) public onlyOwner {
uint256 tokenId = _tokenIdCounter.current();
_tokenIdCounter.increment();
_safeMint(to, tokenId);
_setTokenURI(tokenId, uri);
}
function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
super._burn(tokenId);
}
function tokenURI(uint256 tokenId)
public
view
override(ERC721, ERC721URIStorage)
returns (string memory)
{
return super.tokenURI(tokenId);
}
}
让我们回顾代码。
上面的代码是从 OpenZeppelin 获取的样板代码。对于这个 NFT,我们继承了 ERC721URIStorage 和 Ownable 用于元数据和访问控制。此外,我们设置了 safeMint 函数、burn 函数和 tokenURI 函数用于返回代币元数据。
在继续下一个部分之前,请花些时间通过替换 YOUR_NFTS_NAME 和 YOUR_NFTS_SYMBOL 占位符来重命名你的 NFT。记得保存文件!
现在我们已完成创建测试 NFT,接下来是编译所有内容并确保其按预期工作。
要编译合约,请运行以下命令: npx hardhat compile
编译后,你将注意到两个新文件夹 - artifacts 和 cache。Artifacts 是你可以找到智能合约 ABI 和字节代码的地方。你稍后需要 ABI 进行合约部署。
注意:如果你想清除缓存并删除已编译的 artifacts,可以运行 npx hardhat clean 命令。
现在,我们将使用 Hardhat 的测试功能来测试所有合约的函数,在下一个部分中进行。
在我们将合约部署到像 Mumbai 这样的测试区块链之前,我们应该在本地环境中测试合约,以确保一切按预期运行。
进入 marketplace-hardhat 目录中的 test 文件夹,并创建一个名为 marketplace-test.js 的新文件。
该测试文件将允许我们执行不同的函数,并查看市场合约是否按预期运行。例如,当一个人列出 NFT 时,我们将检查 NFT 是否从卖方转移到市场等。
将以下代码复制并粘贴到 marketplace-test.js 文件中:
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("Marketplace", function () {
let acc1, acc2;
let marketplaceAddress;
let nftAddress;
let nft;
let marketplace;
let listPrice = ethers.utils.parseEther("0.01", "ether");
beforeEach(async function () {
[acc1, acc2] = await ethers.getSigners();
const Marketplace = await ethers.getContractFactory("Marketplace");
nftMarketplace = await Marketplace.deploy();
await nftMarketplace.deployed();
marketplaceAddress = nftMarketplace.address;
const NFT = await ethers.getContractFactory("NFT");
nft = await NFT.deploy();
await nft.deployed();
nftAddress = nft.address.toString();
});
it("Should list an NFT onto the marketplace", async function () {
await nft.safeMint(acc1.address, "META_DATA_URI");
await nft.approve(marketplaceAddress, 0);
await nftMarketplace.createListing(0, nftAddress, listPrice); //0.01 MATIC
});
it("Should sell an active NFT listed on the marketplace ", async function () {
await nft.safeMint(acc1.address, "META_DATA_URI");
await nft.approve(marketplaceAddress, 0);
await nftMarketplace.createListing(0, nftAddress, listPrice); //0.01 MATIC
await expect(
await nftMarketplace
.connect(acc2)
.buyListing(1, nftAddress, { value: listPrice })
)
item = await nftMarketplace.getMarketItem(1);
expect(item.owner).to.equal(acc2.address);
});
it("Test a market sale that does not send sufficient funds", async function () {
await nft.safeMint(acc1.address, "META_DATA_URI");
await nft.approve(marketplaceAddress, 0);
await nftMarketplace.createListing(0, nftAddress, listPrice);
await expect(
nftMarketplace.connect(acc2).buyListing(1, nftAddress, { value: ethers.utils.parseEther("0.02", "ether")})
).to.be.revertedWith(
"Value sent does not meet list price for NFT"
);
item = await nftMarketplace.getMarketItem(1);
expect(item.owner).to.equal("0x0000000000000000000000000000000000000000");
});
});
上面的脚本展示了一个测试的大致轮廓。然而,请注意,此测试仅涵盖市场中的一些功能。
要运行上述测试脚本,请运行此终端命令: npx hardhat test test/marketplace-test.js。
一旦测试成功通过,你应该看到以下内容:
这意味着所有的 expect 断言均为真,因此测试成功。
在下一个部分中,我们将把合约部署到 Polygon Mumbai 测试网区块链上。
截至此时,你已成功创建并测试了你的 NFT 市场。现在是时候将你的合约部署到 Polygon 的 Mumbai 测试网。在此之前,我们需要设置一些依赖项并配置我们的环境变量。
首先,在你的 marketplace-hardhat 目录中,通过运行以下终端命令创建 .env 文件:
echo > .env
然后,打开 .env 文件并添加以下变量:
PRIVATE_KEY_ACCOUNT_1=
PRIVATE_KEY_ACCOUNT_2=
POLYGONSCAN_API_KEY=
RPC_URL=
请花些时间填写你的 私钥 和 QuickNode HTTP 提供者 URL(来自 QuickNode)。创建两个帐户的目的是为了在市场上测试列出和购买功能。另外,在 Polygonscan 创建一个帐户并获取 API 密钥。你可以通过单击 Polygonscan 上的“我的个人资料”,然后单击“API KEYS”选项卡,再单击“添加”生成API密钥。
在 .env 文件中填充所有值后,保存文件。
我们还需要配置 hardhat.config.js 文件。打开该文件并输入以下代码:
require("dotenv").config();
require("@nomicfoundation/hardhat-toolbox");
require("@nomiclabs/hardhat-etherscan");
module.exports = {
solidity: "0.8.9",
networks: {
mumbai: {
url: process.env.RPC_URL,
accounts: [process.env.PRIVATE_KEY_ACCOUNT_1],
gas: 2100000,
gasPrice: 8000000000,
},
},
etherscan: {
apiKey: {
polygonMumbai: process.env.POLYGONSCAN_API_KEY
}
},
};
注意:gas 和 gasPrice 被硬编码以防止在高网络活动期间产生不可预测的等待时间。
现在是时候部署智能合约了。我们将通过位于 scripts 文件夹中的脚本执行此操作。将 scripts/deploy.js 文件中的内容替换为以下代码:
const hre = require("hardhat");
async function main() {
const Marketplace = await hre.ethers.getContractFactory("Marketplace");
const marketplace = await Marketplace.deploy()
await marketplace.deployed();
const NFT = await hre.ethers.getContractFactory("NFT");
const nft = await NFT.deploy()
await nft.deployed();
console.log(
`NFT Marketplace deployed to ${marketplace.address} - Block explorer URL: https://mumbai.polygonscan.com/address/${marketplace.address}`);
console.log(
`NFT deployed to ${nft.address} - Block explorer URL: https://mumbai.polygonscan.com/address/${nft.address}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
然后,要部署合约,请运行以下终端命令:
npx hardhat run --network mumbai scripts/deploy.js
你应该看到类似于以下输出:
你刚刚将市场合约和 NFT Token部署到 Mumbai 测试网!花点时间通过访问终端提供的 URL 验证交易,在 Polygonscan 上进行核实。在下一个部分中,我们将使用 Javascript 和 Ethers.js 与我们的市场智能合约进行交互。
随着我们的 NFT 市场合约被部署,我们现在将使用 Ethers.js 与其进行交互。在你的 scripts 文件夹中,创建一个名为 interact.js 的文件,然后输入以下代码:
const { ethers } = require("hardhat");
const hre = require("hardhat");
require("dotenv").config();
async function main() {
[acc1] = await ethers.getSigners(); //
const acc2 = await new ethers.Wallet(process.env.PRIVATE_KEY_ACCOUNT_2, acc1.provider)
const nConfirm = 10;
const marketplaceId = 1; //this value will need to be modified according to the NFT being listed/sold
//Create instances of the marketplace contract
const Marketplace = await hre.ethers.getContractFactory("Marketplace");
const marketplace = await Marketplace.attach(
"YOUR_MARKETPLACE_CONTRACT_ADDRESS" // The marketplace contract address
);
//Create instances of the NFT contract
const NFT = await hre.ethers.getContractFactory("NFT");
const nft = await NFT.attach(
"YOUR_NFT_CONTRACT_ADDRESS" // The nft contract address
);
//Mint an NFT to list on the marketplace
const mintTxn = await nft.safeMint(acc1.address, "YOUR_META_DATA_URI");
console.log("safeMint function call Tx Hash:", mintTxn.hash);
const receipt = await mintTxn.wait([confirms = nConfirm])
let tokenId = parseInt(receipt["logs"][0].topics[3].toString())
//Approve the marketplace address as a spender
const approval = await nft.approve(marketplace.address, tokenId);
console.log("Approval function call Tx Hash:", approval.hash);
approval.wait([confirms = nConfirm]); //wait till the transaction mines
//List the NFT onto the marketplace
const createListing = await marketplace.createListing(tokenId, nft.address, ethers.utils.parseEther("0.01", "ether"));
console.log("createListing function call Tx Hash:", createListing.hash);
createListing.wait([confirms = nConfirm]); //wait till the transaction mines
//Buy the NFT from acc2
const buyNFT = await marketplace.connect(acc2).buyListing(marketplaceId, nft.address, { value: ethers.utils.parseEther("0.01", "ether")});
console.log("Buy NFT Tx Hash:", buyNFT.hash)
buyNFT.wait([confirms = nConfirm]); //wait till the transaction mines
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
重要提示:请记得在上面的代码中替换 YOUR_MARKETPLACE_CONTRACT_ADDRESS 和 YOUR_NFT_CONTRACT_ADDRESS 占位符为你自己已部署智能合约的地址。
另外,如果你想设置元数据 URL,请将 YOUR_META_DATA_URI 占位符替换为你的实际元数据 URL。要了解如何设置 NFT 元数据,请查看此 QuickNode 指南 中的“将文件添加到 IPFS”部分。
花几分钟时间查看 interact.js 代码中的代码注释。然后,运行以下终端命令来执行 interact.js 脚本:
npx hardhat run scripts/interact.js --network mumbai
你应该看到类似于以下输出:
在 Polygonscan 上验证合约源代码
让我们花一点时间在公共区块浏览器上验证我们的合约源。验证后,我们将分析市场和 NFT 合约的活动以查看 NFT 销售。
在你的 marketplace-hardhat 文件夹中,为你部署的每个合约运行以下 hardhat 命令以验证合约。记得用你实际部署的合约地址替换占位符值。
npx hardhat verify --network mumbai <contract_address>
在命令成功执行后,你将看到一条链接,指向你合约的公开验证代码。导航到你 NFT 合约的 Polygonscan URL,单击 合约 选项卡,然后单击 读取合约 选项卡。
如果你输入第二个账户(购买 NFT 的账户)的地址,你将看到余额为 1。同时,确认第一个帐户的 NFT 余额应为零。
干得好!你已学习如何在 Polygon 的 Mumbai 测试网部署 NFT 市场。尝试向市场合约添加你自己的逻辑(例如,奖励、佣金),以扩展其功能。你还可以编写自己的测试,以了解合约在不同情况下的反应。
在 Twitter 或 Discord 展示你的技能。我们很想知道你在构建什么!
如果你对本指南有任何反馈, 请告诉我们!
- 原文链接: quicknode.com/guides/oth...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!