赞
踩
很高兴向您发布这个 web3.0 版本,也许您一直在寻找一个很好的例子来帮助您开始开发去中心化应用程序。
在本文中,您将逐步学习如何实现具有匿名聊天功能的去中心化自治组织 (DAO)。
您将需要安装以下工具才能成功的构建:
NodeJs 安装
确保您的机器上已经安装了 NodeJs,如果还没有,请从HERE安装它。接下来,在终端上运行代码以确认它已安装。
Yarn、Ganache-cli 和 Truffle 安装
在您的终端上运行以下代码,以全局安装这些基本软件包。
npm i -g yarn
npm i -g truffle
npm i -g ganache-cli
克隆 Web3 入门项目
使用下面的命令,克隆下面的 web 3.0 入门项目。这将确保我们都在同一个页面上并使用相同的包。
git clone https://github.com/Daltonic/dominionDAO
接下来,让我们将 package.json 文件替换为以下文件:
{ "name": "dominionDAO", "private": true, "version": "0.0.0", "scripts": { "start": "react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test", "eject": "react-scripts eject" }, "dependencies": { "@cometchat-pro/chat": "3.0.6", "moment": "^2.29.3", "react": "^17.0.2", "react-dom": "^17.0.2", "react-hooks-global-state": "^1.0.2", "react-icons": "^4.3.1", "react-identicons": "^1.2.5", "react-moment": "^1.1.2", "react-router-dom": "6", "react-scripts": "5.0.0", "react-toastify": "^9.0.1", "recharts": "^2.1.9", "web-vitals": "^2.1.4", "web3": "^1.7.1" }, "devDependencies": { "@openzeppelin/contracts": "^4.5.0", "@tailwindcss/forms": "0.4.0", "@truffle/hdwallet-provider": "^2.0.4", "assert": "^2.0.0", "autoprefixer": "10.4.2", "babel-polyfill": "^6.26.0", "babel-preset-env": "^1.7.0", "babel-preset-es2015": "^6.24.1", "babel-preset-stage-2": "^6.24.1", "babel-preset-stage-3": "^6.24.1", "babel-register": "^6.26.0", "buffer": "^6.0.3", "chai": "^4.3.6", "chai-as-promised": "^7.1.1", "crypto-browserify": "^3.12.0", "dotenv": "^16.0.0", "https-browserify": "^1.0.0", "mnemonics": "^1.1.3", "os-browserify": "^0.3.0", "postcss": "8.4.5", "process": "^0.11.10", "react-app-rewired": "^2.1.11", "stream-browserify": "^3.0.0", "stream-http": "^3.2.0", "tailwindcss": "3.0.18", "url": "^0.11.0" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } }
太好了,用上面的代码替换你的 package.json 文件,然后在你的终端上运行 yarn install。
安装完所有这些,让我们开始编写 Dominion DAO 智能合约。
要配置CometChat SDK,请按照以下步骤操作,最后,您需要将这些密钥存储为环境变量。
步骤1:
前往CometChat ,并创建一个帐户。
第2步:
在注册后登录CometChat。
第 3 步:
在面板中,添加一个名为dominionDAO 的新应用程序。
第4步:
从列表中选择您刚刚创建的应用程序。
第 5 步:
从快速入门中将 APP_ID、REGION 和 AUTH_KEY 复制到您的 .env 文件中。请参阅下图和代码。
将 REACT_COMET_CHAT 占位符替换为相应的值。
REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************
步骤1:
前往Infuria并创建一个帐户。
第2步:
创建一个新项目。
第 3 步:
将 Rinkeby 测试网络 WebSocket 端点 URL 复制到您的 .env 文件。
接下来,添加您的 Metamask 密码短语和您首选的帐户私钥。如果您正确地完成了这些操作,您的环境变量现在应该如下所示。
ENDPOINT_URL=***************************
DEPLOYER_KEY=**********************
REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************
如果您不知道如何访问您的私钥,请参阅下面的部分。
步骤1:
单击您的 Metamask 浏览器扩展,并确保选择 Rinkeby 作为测试网络。接下来,在首选帐户上,单击三个点并选择 account details 。见下图。
第2步:
在提供的输入框中输入您的密码,然后单击确认按钮,这样您能够访问您的帐户私钥。
第 3 步:
单击“export private key”查看您的私钥。千万注意永远不要在 Github 等公共页面上公开您的密钥。这就是为什么我们将其附加为环境变量。
第4步:
将您的私钥复制到您的 .env 文件中。请参阅下图和代码:
ENDPOINT_URL=***************************
SECRET_KEY=******************
DEPLOYER_KEY=**********************
REACT_APP_COMET_CHAT_REGION=**
REACT_APP_COMET_CHAT_APP_ID=**************
REACT_APP_COMET_CHAT_AUTH_KEY=******************************
至于您的 SECRET_KEY,您需要将 Metamask 密码短语粘贴到环境文件提供的对应位置上。
这是智能合约的完整代码,稍后将逐个解释所有函数和变量。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.7; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/security/ReentrancyGuard.sol"; contract DominionDAO is ReentrancyGuard, AccessControl { bytes32 private immutable CONTRIBUTOR_ROLE = keccak256("CONTRIBUTOR"); bytes32 private immutable STAKEHOLDER_ROLE = keccak256("STAKEHOLDER"); uint32 immutable MIN_VOTE_DURATION = 1 weeks; uint256 totalProposals; uint256 public daoBalance; mapping(uint256 => ProposalStruct) private raisedProposals; mapping(address => uint256[]) private stakeholderVotes; mapping(uint256 => VotedStruct[]) private votedOn; mapping(address => uint256) private contributors; mapping(address => uint256) private stakeholders; struct ProposalStruct { uint256 id; uint256 amount; uint256 duration; uint256 upvotes; uint256 downvotes; string title; string description; bool passed; bool paid; address payable beneficiary; address proposer; address executor; } struct VotedStruct { address voter; uint256 timestamp; bool choosen; } event Action( address indexed initiator, bytes32 role, string message, address indexed beneficiary, uint256 amount ); modifier stakeholderOnly(string memory message) { require(hasRole(STAKEHOLDER_ROLE, msg.sender), message); _; } modifier contributorOnly(string memory message) { require(hasRole(CONTRIBUTOR_ROLE, msg.sender), message); _; } function createProposal( string calldata title, string calldata description, address beneficiary, uint256 amount )external stakeholderOnly("Proposal Creation Allowed for Stakeholders only") { uint256 proposalId = totalProposals++; ProposalStruct storage proposal = raisedProposals[proposalId]; proposal.id = proposalId; proposal.proposer = payable(msg.sender); proposal.title = title; proposal.description = description; proposal.beneficiary = payable(beneficiary); proposal.amount = amount; proposal.duration = block.timestamp + MIN_VOTE_DURATION; emit Action( msg.sender, CONTRIBUTOR_ROLE, "PROPOSAL RAISED", beneficiary, amount ); } function performVote(uint256 proposalId, bool choosen) external stakeholderOnly("Unauthorized: Stakeholders only") { ProposalStruct storage proposal = raisedProposals[proposalId]; handleVoting(proposal); if (choosen) proposal.upvotes++; else proposal.downvotes++; stakeholderVotes[msg.sender].push(proposal.id); votedOn[proposal.id].push( VotedStruct( msg.sender, block.timestamp, choosen ) ); emit Action( msg.sender, STAKEHOLDER_ROLE, "PROPOSAL VOTE", proposal.beneficiary, proposal.amount ); } function handleVoting(ProposalStruct storage proposal) private { if ( proposal.passed || proposal.duration <= block.timestamp ) { proposal.passed = true; revert("Proposal duration expired"); } uint256[] memory tempVotes = stakeholderVotes[msg.sender]; for (uint256 votes = 0; votes < tempVotes.length; votes++) { if (proposal.id == tempVotes[votes]) revert("Double voting not allowed"); } } function payBeneficiary(uint256 proposalId) external stakeholderOnly("Unauthorized: Stakeholders only") returns (bool) { ProposalStruct storage proposal = raisedProposals[proposalId]; require(daoBalance >= proposal.amount, "Insufficient fund"); require(block.timestamp > proposal.duration, "Proposal still ongoing"); if (proposal.paid) revert("Payment sent before"); if (proposal.upvotes <= proposal.downvotes) revert("Insufficient votes"); payTo(proposal.beneficiary, proposal.amount); proposal.paid = true; proposal.executor = msg.sender; daoBalance -= proposal.amount; emit Action( msg.sender, STAKEHOLDER_ROLE, "PAYMENT TRANSFERED", proposal.beneficiary, proposal.amount ); return true; } function contribute() payable external { if (!hasRole(STAKEHOLDER_ROLE, msg.sender)) { uint256 totalContribution = contributors[msg.sender] + msg.value; if (totalContribution >= 5 ether) { stakeholders[msg.sender] = totalContribution; contributors[msg.sender] += msg.value; _setupRole(STAKEHOLDER_ROLE, msg.sender); _setupRole(CONTRIBUTOR_ROLE, msg.sender); } else { contributors[msg.sender] += msg.value; _setupRole(CONTRIBUTOR_ROLE, msg.sender); } } else { contributors[msg.sender] += msg.value; stakeholders[msg.sender] += msg.value; } daoBalance += msg.value; emit Action( msg.sender, STAKEHOLDER_ROLE, "CONTRIBUTION RECEIVED", address(this), msg.value ); } function getProposals() external view returns (ProposalStruct[] memory props) { props = new ProposalStruct[](totalProposals); for (uint256 i = 0; i < totalProposals; i++) { props[i] = raisedProposals[i]; } } function getProposal(uint256 proposalId) external view returns (ProposalStruct memory) { return raisedProposals[proposalId]; } function getVotesOf(uint256 proposalId) external view returns (VotedStruct[] memory) { return votedOn[proposalId]; } function getStakeholderVotes() external view stakeholderOnly("Unauthorized: not a stakeholder") returns (uint256[] memory) { return stakeholderVotes[msg.sender]; } function getStakeholderBalance() external view stakeholderOnly("Unauthorized: not a stakeholder") returns (uint256) { return stakeholders[msg.sender]; } function isStakeholder() external view returns (bool) { return stakeholders[msg.sender] > 0; } function getContributorBalance() external view contributorOnly("Denied: User is not a contributor") returns (uint256) { return contributors[msg.sender]; } function isContributor() external view returns (bool) { return contributors[msg.sender] > 0; } function getBalance() external view returns (uint256) { return contributors[msg.sender]; } function payTo( address to, uint256 amount ) internal returns (bool) { (bool success,) = payable(to).call{value: amount}(""); require(success, "Payment failed"); return true; } }
在您刚刚克隆的项目中,前往src >> contract目录并创建一个名为 DominionDAO.sol 的文件,然后将上述代码粘贴到其中。
解释:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
Solidity 需要一个许可证标识符来编译你的代码,否则它会产生一个警告,要求你指定一个。此外,Solidity 要求您为智能合约指定编译器的版本。这就是pragma这个词所代表的意思。
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
在上面的代码块中,我们使用了两个 openzeppelin 的智能合约来指定角色并保护我们的智能合约免受重入攻击。
bytes32 private immutable CONTRIBUTOR_ROLE = keccak256("CONTRIBUTOR");
bytes32 private immutable STAKEHOLDER_ROLE = keccak256("STAKEHOLDER");
uint32 immutable MIN_VOTE_DURATION = 1 weeks;
uint256 totalProposals;
uint256 public daoBalance;
我们为利益相关者和贡献者角色设置了一些状态变量,并指定最短投票持续时间为一周。我们还初始化了总提案计数器 totalProposals 和一个变量 daoBalance 来记录我们的可用余额。
mapping(uint256 => ProposalStruct) private raisedProposals;
mapping(address => uint256[]) private stakeholderVotes;
mapping(uint256 => VotedStruct[]) private votedOn;
mapping(address => uint256) private contributors;
mapping(address => uint256) private stakeholders;
raiseProposals 记录提交给我们智能合约的所有提案。stakeholderVotes,顾名思义,记录利益相关者的投票。votedOn 记录与提案相关的所有投票。contributors 记录向我们平台捐款的任何人,另一方面,stakeholders 记录贡献了超过 1 个以太币的人。
struct ProposalStruct { uint256 id; uint256 amount; uint256 duration; uint256 upvotes; uint256 downvotes; string title; string description; bool passed; bool paid; address payable beneficiary; address proposer; address executor; } struct VotedStruct { address voter; uint256 timestamp; bool choosen; }
proposalStruct 描述了每个提案的内容,而 votedStruct 描述了每个投票的内容。
event Action(
address indexed initiator,
bytes32 role,
string message,
address indexed beneficiary,
uint256 amount
);
这是一个名为 Action 的动态事件。这将帮助我们丰富每笔交易注销的信息。
modifier stakeholderOnly(string memory message) {
require(hasRole(STAKEHOLDER_ROLE, msg.sender), message);
_;
}
modifier contributorOnly(string memory message) {
require(hasRole(CONTRIBUTOR_ROLE, msg.sender), message);
_;
}
上述修饰符帮助我们按角色识别用户,也可以防止他们访问一些未经授权的资源。
function createProposal( string calldata title, string calldata description, address beneficiary, uint256 amount )external stakeholderOnly("Proposal Creation Allowed for Stakeholders only") { uint256 proposalId = totalProposals++; ProposalStruct storage proposal = raisedProposals[proposalId]; proposal.id = proposalId; proposal.proposer = payable(msg.sender); proposal.title = title; proposal.description = description; proposal.beneficiary = payable(beneficiary); proposal.amount = amount; proposal.duration = block.timestamp + MIN_VOTE_DURATION; emit Action( msg.sender, CONTRIBUTOR_ROLE, "PROPOSAL RAISED", beneficiary, amount ); }
上述函数获取提案的标题、描述、金额和受益人的钱包地址并创建提案。该功能仅允许利益相关者创建提案。利益相关者是至少贡献了 1 个以太币的用户。
function performVote(uint256 proposalId, bool choosen) external stakeholderOnly("Unauthorized: Stakeholders only") { ProposalStruct storage proposal = raisedProposals[proposalId]; handleVoting(proposal); if (choosen) proposal.upvotes++; else proposal.downvotes++; stakeholderVotes[msg.sender].push(proposal.id); votedOn[proposal.id].push( VotedStruct( msg.sender, block.timestamp, choosen ) ); emit Action( msg.sender, STAKEHOLDER_ROLE, "PROPOSAL VOTE", proposal.beneficiary, proposal.amount ); }
该函数接受两个参数,一个提案 ID 和一个由布尔值表示的首选选项。True 表示您接受投票,False 表示拒绝。
function handleVoting(ProposalStruct storage proposal) private {
if (
proposal.passed ||
proposal.duration <= block.timestamp
) {
proposal.passed = true;
revert("Proposal duration expired");
}
uint256[] memory tempVotes = stakeholderVotes[msg.sender];
for (uint256 votes = 0; votes < tempVotes.length; votes++) {
if (proposal.id == tempVotes[votes])
revert("Double voting not allowed");
}
}
此函数执行实际投票,包括检查用户是否是利益相关者并有资格投票。
function payBeneficiary(uint256 proposalId) external stakeholderOnly("Unauthorized: Stakeholders only") returns (bool) { ProposalStruct storage proposal = raisedProposals[proposalId]; require(daoBalance >= proposal.amount, "Insufficient fund"); require(block.timestamp > proposal.duration, "Proposal still ongoing"); if (proposal.paid) revert("Payment sent before"); if (proposal.upvotes <= proposal.downvotes) revert("Insufficient votes"); payTo(proposal.beneficiary, proposal.amount); proposal.paid = true; proposal.executor = msg.sender; daoBalance -= proposal.amount; emit Action( msg.sender, STAKEHOLDER_ROLE, "PAYMENT TRANSFERED", proposal.beneficiary, proposal.amount ); return true; }
此函数负责根据特定标准向提案所附的受益人付款。
function contribute() payable external { if (!hasRole(STAKEHOLDER_ROLE, msg.sender)) { uint256 totalContribution = contributors[msg.sender] + msg.value; if (totalContribution >= 5 ether) { stakeholders[msg.sender] = totalContribution; contributors[msg.sender] += msg.value; _setupRole(STAKEHOLDER_ROLE, msg.sender); _setupRole(CONTRIBUTOR_ROLE, msg.sender); } else { contributors[msg.sender] += msg.value; _setupRole(CONTRIBUTOR_ROLE, msg.sender); } } else { contributors[msg.sender] += msg.value; stakeholders[msg.sender] += msg.value; } daoBalance += msg.value; emit Action( msg.sender, STAKEHOLDER_ROLE, "CONTRIBUTION RECEIVED", address(this), msg.value ); }
该函数负责从捐助者和有兴趣成为利益相关者的人那里收集捐款。
function getProposals()
external
view
returns (ProposalStruct[] memory props)
{
props = new ProposalStruct[](totalProposals);
for (uint256 i = 0; i < totalProposals; i++) {
props[i] = raisedProposals[i];
}
}
此函数检索记录在此智能合约上的一组提案。
function getProposal(uint256 proposalId)
external
view
returns (ProposalStruct memory)
{
return raisedProposals[proposalId];
}
此函数按 Id 检索特定提案。
function getVotesOf(uint256 proposalId)
external
view
returns (VotedStruct[] memory)
{
return votedOn[proposalId];
}
这将返回与特定提案相关的投票列表。
function getStakeholderVotes()
external
view
stakeholderOnly("Unauthorized: not a stakeholder")
returns (uint256[] memory)
{
return stakeholderVotes[msg.sender];
}
这将返回智能合约上的利益相关者列表,并且只有利益相关者可以调用此函数。
function getStakeholderBalance()
external
view
stakeholderOnly("Unauthorized: not a stakeholder")
returns (uint256)
{
return stakeholders[msg.sender];
}
这将返回利益相关者贡献的金额。
function isStakeholder() external view returns (bool) {
return stakeholders[msg.sender] > 0;
}
如果用户是利益相关者,则返回 True 或 False。
function getContributorBalance()
external
view
contributorOnly("Denied: User is not a contributor")
returns (uint256)
{
return contributors[msg.sender];
}
这将返回贡献者的余额,并且只有贡献者可以访问。
function isContributor() external view returns (bool) {
return contributors[msg.sender] > 0;
}
这将检查用户是否是贡献者,并用 True 或 False 表示。
function getBalance() external view returns (uint256) {
return contributors[msg.sender];
}
返回调用用户的余额,无论其角色如何。
function payTo(
address to,
uint256 amount
) internal returns (bool) {
(bool success,) = payable(to).call{value: amount}("");
require(success, "Payment failed");
return true;
}
此函数执行指定金额和帐户的付款。
与智能合约有关的另一件事是配置部署脚本。
在项目头上的 migrations 文件夹中,>> **2_deploy_contracts.js,**并使用下面的代码片段对其进行更新。
const DominionDAO = artifacts.require('DominionDAO')
module.exports = async function (deployer) {
await deployer.deploy(DominionDAO)
}
到这里,我们就完成了应用程序的智能合约,是时候开始构建 Dapp 界面了。
前端包括许多组件和部件。我们将创建所有组件、视图和其余外围设备。
Header 组件
该组件捕获有关当前用户的信息,并带有一个用于明暗模式的主题切换按钮。如果您想知道我是如何做到这一点的,那是通过 Tailwind CSS,请参阅下面的代码。
import { useState, useEffect } from 'react' import { FaUserSecret } from 'react-icons/fa' import { MdLightMode } from 'react-icons/md' import { FaMoon } from 'react-icons/fa' import { Link } from 'react-router-dom' import { connectWallet } from '../Dominion' import { useGlobalState, truncate } from '../store' const Header = () => { const [theme, setTheme] = useState(localStorage.theme) const themeColor = theme === 'dark' ? 'light' : 'dark' const darken = theme === 'dark' ? true : false const [connectedAccount] = useGlobalState('connectedAccount') useEffect(() => { const root = window.document.documentElement root.classList.remove(themeColor) root.classList.add(theme) localStorage.setItem('theme', theme) }, [themeColor, theme]) const toggleLight = () => { const root = window.document.documentElement root.classList.remove(themeColor) root.classList.add(theme) localStorage.setItem('theme', theme) setTheme(themeColor) } return ( <header className="sticky top-0 z-50 dark:text-blue-500"> <nav className="navbar navbar-expand-lg shadow-md py-2 relative flex items-center w-full justify-between bg-white dark:bg-[#212936]"> <div className="px-6 w-full flex flex-wrap items-center justify-between"> <div className="navbar-collapse collapse grow flex flex-row justify-between items-center p-2"> <Link to={'/'} className="flex flex-row justify-start items-center space-x-3" > <FaUserSecret className="cursor-pointer" size={25} /> <span className="invisible md:visible dark:text-gray-300"> Dominion </span> </Link> <div className="flex flex-row justify-center items-center space-x-5"> {darken ? ( <MdLightMode className="cursor-pointer" size={25} onClick={toggleLight} /> ) : ( <FaMoon className="cursor-pointer" size={25} onClick={toggleLight} /> )} {connectedAccount ? ( <button className="px-4 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out dark:text-blue-500 dark:border dark:border-blue-500 dark:bg-transparent" > {truncate(connectedAccount, 4, 4, 11)} </button> ) : ( <button className="px-4 py-2.5 bg-blue-600 text-white font-medium text-xs leading-tight uppercase rounded-full shadow-md hover:bg-blue-700 hover:shadow-lg focus:bg-blue-700 focus:shadow-lg focus:outline-none focus:ring-0 active:bg-blue-800 active:shadow-lg transition duration-150 ease-in-out dark:text-blue-500 dark:border dark:border-blue-500 dark:bg-transparent" onClick={connectWallet} > Connect Wallet </button> )} </div> </div> </div> </nav> </header> ) } export default Header
Banner组件
该组件包含有关 DAO 当前状态的信息,例如总余额和未决提案的数量。
该组件还包括使用贡献函数生成新提案的能力。看看下面的代码。
import { useState } from 'react'
import { setGlobalState, useGlobalState } from '../store'
import { performContribute } from '../Dominion'
import { toast } from 'react-toastify'
const Banner = () => {
const [isStakeholder] = useGlobalState('isStakeholder')
const [proposals] = useGlobalState('proposals')
const [connectedAccount] = useGlobalState('connectedAccount')
const [currentUser] = useGlobalState('currentUser')
const [balance] = useGlobalState('balance')
const [mybalance] = useGlobalState('mybalance')
const [amount, setAmount] = useState('')
const onPropose = () => {
if (!isStakeholder) return
setGlobalState('createModal', 'scale-100')
}
const onContribute = () => {
if (!!!amount || amount == '') return
toast.info('Contribution in progress...')
performContribute(amount).then((bal) => {
if (!!!bal.message) {
setGlobalState('balance', Number(balance) + Number(bal))
setGlobalState('mybalance', Number(mybalance) + Number(bal))
setAmount('')
toast.success('Contribution received')
}
})
}
const opened = () =>
proposals.filter(
(proposal) => new Date().getTime() < Number(proposal.duration + '000')
).length
return (
<div className="p-8">
<h2 className="font-semibold text-3xl mb-5">
{opened()} Proposal{opened() == 1 ? '' : 's'} Currenly Opened
</h2>
<p>
Current DAO Balance: <strong>{balance} Eth</strong> <br />
Your contributions:{' '}
<span>
<strong>{mybalance} Eth</strong>
{isStakeholder ? ', and you are now a stakeholder 声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/717045
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。