软件信息网 企业信息化 以太猫开发源码分析--以太猫CryptoKitties系统源码分析

以太猫开发源码分析--以太猫CryptoKitties系统源码分析

CryptoKitties 做了很棒的工作,他展现除了简略的金融生意之外还能够运用区块链做什么。

我希望将来咱们会看到更多立异的区块链用法,所以我想快速阅览CryptoKitties背面的代码,以展现它背面是怎么完成的。

本文是为开发人员编写的,虽然这不是一个绝对的初学者对Solidity的介绍,可是我试图包括文档的链接,以便尽或许合适一切开发者。

让咱们开端...

以太猫开发源码分析--以太猫CryptoKitties系统源码分析插图CryptoKitties源码

几乎一切的CryptoKitties代码都是开源的,因此找出它的工作原理的最好办法是阅览源代码。

总共大约有2000行,所以在这篇文章中,我只会解说我以为最重要的部分。 可是,假如您想独自阅览,请参阅EthFiddle上的完好合约代码副本:

CryptoKitties Source Code

以太猫开发源码分析--以太猫CryptoKitties系统源码分析插图1总概:

假如你不了解CryptoKitties是什么,它基本上是一个购买,销售和繁衍数字猫的游戏。 每只猫都有一个共同的外观,由它的基因所界说,当你经过两只猫繁衍时,它们的基因以一种共同的办法结合在一同产生一个子孙,然后你能够繁衍或出售它。

CryptoKitties 的代码分为许多相关的较小的合约, 而不是一个单一的包括一切东西的巨大文件

子合约像下面这样承继主kitty合约:

"font-family:Menlo, Courier, monospace, monospace, sans-serif;font-size:13.6px;margin:0px;border:none;background-color:transparent;white-space:pre-wrap;" class="hljs">contract KittyAccessControl
contract KittyBase is KittyAccessControl
contract KittyOwnership is KittyBase, ERC721
contract KittyBreeding is KittyOwnership
contract KittyAuction is KittyBreeding
contract KittyMinting is KittyAuction
contract KittyCore is KittyMinting

所以KittyCore是终究应用程序指向的合约地址,他承继了前面合约的一切的特点和办法

让咱们一个一个的看看这些合约:

以太猫开发源码分析--以太猫CryptoKitties系统源码分析插图21. KittyAccessControl:谁操控合约?

这个合约办理只能由特定人物履行操作的各种地址和束缚。这些人物叫CEO, CFO and COO.

这个合约是为了办理合约,根本不涉及到游戏的机制。他为CEO, COO 和CFO提供有“setter”办法, 他们(CEO, COO, CFO)是对合约具有特殊一切权和操控权的以太坊地址。

KittyAccessControl 界说一些modifier函数例如 onlyCEO(只要CEO才干履行),还有暂停/恢复合约的办法或许提现办法

"font-family:Menlo, Courier, monospace, monospace, sans-serif;font-size:13.6px;margin:0px;border:none;background-color:transparent;white-space:pre-wrap;" class="hljs">modifier onlyCLevel() { require(
        msg.sender == cooAddress ||
        msg.sender == ceoAddress ||
        msg.sender == cfoAddress
    );
    _;
} //...some other stuff // Only the CEO, COO, and CFO can execute this function: function pause() external onlyCLevel whenNotPaused {
    paused = true;
}

pause() 函数或许被增加,以便开发人员能够更新一个新的版别,以防有任何不可预见的错误... 但正如我的同事Luke指出,这实践上将答应开发人员彻底冻住合约,使其没有人能够转让,出售或繁衍他们的小猫! 并不是说他们会这么做 - 可是有趣的是,由于大多数人以为DApp彻底是去中心化的,仅仅由于它在以太坊上。

继续。。。

以太猫开发源码分析--以太猫CryptoKitties系统源码分析插图32. KittyBase: Kitty是什么?

这是咱们界说在整个中心功用中共享的最基本代码的当地。 这包括咱们的首要数据存储,常量和数据类型,以及用于办理这些数据的内部函数。

KittyBase 界说了应用程序的很多中心数据。首要它将Kitty界说为一个结构体

<code style="font-family:Menlo, Courier, monospace, monospace, sans-serif;font-size:13.6px;margin:0px;border:none;background-color:transparent;white-space:pre-wrap;" class="hljs">struct Kitty {
    uint256 genes;
    uint64 birthTime;
    uint64 cooldownEndBlock;
    uint32 matronId;
    uint32 sireId;
    uint32 siringWithId;
    uint16 cooldownIndex;
    uint16 generation;
}code>

所以一只kitty实践上仅仅一串无符号的整数...

展开每个特点:

  • genes—代表猫的遗传暗码的256位整数。 这是决议猫的长相的中心数据。
  • birthTime—猫出生时的时刻戳
  • cooldownEndBlock—之后这只猫能够再次繁衍的最小时刻戳
  • matronId&sireId—分别是猫的母亲和父亲的ID
  • siringWithId—假如猫当时怀孕,则设置为父亲的ID,不然为零
  • cooldownIndex—目前这只猫的冷却时刻(猫需要等候多久才干繁衍)
  • generation—这只猫的“世代号”。 榜首只猫被合约发明是0代,新一代的猫是他们的爸爸妈妈一代中较大的一个,再加上1.

请注意,在Crypto Kitties中,猫是无性的,任何2只猫都能够一同繁衍 - 因此猫没有性别。

KittyBase 合约界说了一个kitty 数据结构的数据

<code style="font-family:Menlo, Courier, monospace, monospace, sans-serif;font-size:13.6px;margin:0px;border:none;background-color:transparent;white-space:pre-wrap;" class="hljs">Kitty[] kitties;code>

这个数组包括了一切Kitty的数据,所以它就像一个Kitty的数据库相同。 无论何时创立一个新的猫,它都会被增加到这个数组中,数组的索引成为猫的ID,就像这个 ID为'1'的创世喵

索引为 “1”!

该合约还包括从猫的ID到其具有者地址的映射,以盯梢具有猫的人:

"font-family:Menlo, Courier, monospace, monospace, sans-serif;font-size:13.6px;margin:0px;border:none;background-color:transparent;white-space:pre-wrap;" class="hljs">mapping (uint256 => address) public kittyIndexToOwner;

还有一些其他的映射也被界说,但为了坚持这篇文章的合理长度,我不会仔细研讨每一个细节。

每逢小猫从一个人搬运到下一个时,这个kittyIndexToOwner映射就会被更新以反映新的一切者:

"font-family:Menlo, Courier, monospace, monospace, sans-serif;font-size:13.6px;margin:0px;border:none;background-color:transparent;white-space:pre-wrap;" class="hljs"> /// @dev Assigns ownership of a specific Kitty to an address. function _transfer(address _from, address _to, uint256 _tokenId) internal { // Since the number of kittens is capped to 2^32 we can't overflow this ownershipTokenCount[_to]++; // transfer ownership kittyIndexToOwner[_tokenId] = _to; // When creating new kittens _from is 0x0, but we can't account that address. if (_from != address(0)) {
        ownershipTokenCount[_from]--; // once the kitten is transferred also clear sire allowances delete sireAllowedToAddress[_tokenId]; // clear any previously approved ownership exchange delete kittyIndexToApproved[_tokenId];
    } // Emit the transfer event. Transfer(_from, _to, _tokenId);
}

搬运一切权 设置Kitty的ID指向接收人_to的地址。
现在咱们来看看在创立一个新的kitty时会产生什么:

"font-family:Menlo, Courier, monospace, monospace, sans-serif;font-size:13.6px;margin:0px;border:none;background-color:transparent;white-space:pre-wrap;" class="hljs">function _createKitty( uint256 _matronId,
    uint256 _sireId,
    uint256 _generation,
    uint256 _genes,
    address _owner )
    internal
    returns (uint)
{ // These requires are not strictly necessary, our calling code should make // sure that these conditions are never broken. However! _createKitty() is already // an expensive call (for storage), and it doesn't hurt to be especially careful // to ensure our data structures are always valid. require(_matronId == uint256(uint32(_matronId))); require(_sireId == uint256(uint32(_sireId))); require(_generation == uint256(uint16(_generation))); // New kitty starts with the same cooldown as parent gen/2 uint16 cooldownIndex = uint16(_generation / 2); if (cooldownIndex > 13) {
        cooldownIndex = 13;
    } Kitty memory _kitty = Kitty({ genes: _genes, birthTime: uint64(now), cooldownEndBlock: 0, matronId: uint32(_matronId), sireId: uint32(_sireId), siringWithId: 0, cooldownIndex: cooldownIndex, generation: uint16(_generation)
    });
    uint256 newKittenId = kitties.push(_kitty) - 1; // It's probably never going to happen, 4 billion cats is A LOT, but // let's just be 100% sure we never let this happen. require(newKittenId == uint256(uint32(newKittenId))); // emit the birth event Birth(
        _owner,
        newKittenId, uint256(_kitty.matronId), uint256(_kitty.sireId),
        _kitty.genes ); // This will assign ownership, and also emit the Transfer event as // per ERC721 draft _transfer(0, _owner, newKittenId); return newKittenId;
}

这个函数传递了母亲和父亲的ID,小猫的世代号码,256位遗传暗码和一切者的地址。 然后创立小猫,将其加入到Kitty数组,然后调用_transfer() 将其分配给它的新一切者。

Cool - 现在咱们能够看到CryptoKitties怎么将一只猫咪界说为一种数据类型,它怎么将一切小猫都存储在区块链中,以及怎么盯梢谁具有哪些小猫。

以太猫开发源码分析--以太猫CryptoKitties系统源码分析插图53. KittyOwnership: Kitties代币化

这提供了遵从ERC-721规范草案的基本不可交换令牌生意所需的办法。

CryptoKitties契合ERC721代币规范,这是一种不可替换的代币类型,它十分合适在MMORPG中盯梢数字收集游戏(如数字扑克牌或稀有物品)的一切权。

关于Fungibility的说明:Ether是可交换的,由于任何5个ETH都与其他5个ETH相同好。 可是像CryptoKitties这样的是非可交换代币,并不是每只猫都是平等的,所以它们不能相互交换。

您能够从合约界说中看出,KittyOwnership承继了ERC721合约:

<code style="font-family:Menlo, Courier, monospace, monospace, sans-serif;font-size:13.6px;margin:0px;border:none;background-color:transparent;white-space:pre-wrap;" class="hljs">contract KittyOwnership is KittyBase, ERC721 {code>

而一切ERC721令牌都遵从相同的规范,所以KittyOwnership合约完成了以下功用:

"font-family:Menlo, Courier, monospace, monospace, sans-serif;font-size:13.6px;margin:0px;border:none;background-color:transparent;white-space:pre-wrap;" class="hljs">/// @title Interface for contracts conforming to ERC-721: Non-Fungible Tokens /// @author Dieter Shirley <dete@axiomzen.co></dete@axiomzen.co> (https://github.com/dete) contract ERC721 { // Required methods function totalSupply() public view returns (uint256 total); function balanceOf(address _owner) public view returns (uint256 balance); function ownerOf(uint256 _tokenId) external view returns (address owner); function approve(address _to, uint256 _tokenId) external; function transfer(address _to, uint256 _tokenId) external; function transferFrom(address _from, address _to, uint256 _tokenId) external; // Events event Transfer(address from, address to, uint256 tokenId); event Approval(address owner, address approved, uint256 tokenId); // Optional // function name() public view returns (string name); // function symbol() public view returns (string symbol); // function tokensOfOwner(address _owner) external view returns (uint256[] tokenIds); // function tokenMetadata(uint256 _tokenId, string _preferredTransport) public view returns (string infoUrl); // ERC-165 Compatibility (https://github.com/ethereum/EIPs/issues/165) function supportsInterface(bytes4 _interfaceID) external view returns (bool);
}

由于这些办法是公开的,这就为用户提供了一个规范的办法来与CryptoKitties令牌进行交互,就像他们与任何其他ERC721令牌进行交相互同。 您能够经过直接与以太坊区块链上的CryptoKitties合约进行交互,而不必经过他们的Web界面来将您的代币转让给其他人,所以从这个意义上说,您真的具有自己的小猫。 (除非CEO暂停合约)。

我不会解读一切这些办法的完成,可是你能够在EthFiddle上查看它们(搜索“KittyOwnership”)。

以太猫开发源码分析--以太猫CryptoKitties系统源码分析插图64. KittyBreeding:猫的繁衍

这个文件包括了将猫一同繁衍所必需的办法,包括盯梢繁衍提供者,并依托外部基因组合合约。

“外部基因组合合约”(geneScience)存储在一个不是开源的独自合约中。

KittyBreeding 合约包括一个办法,让CEO设置这个外部基因组合约地址:

"font-family:Menlo, Courier, monospace, monospace, sans-serif;font-size:13.6px;margin:0px;border:none;background-color:transparent;white-space:pre-wrap;" class="hljs"> /// @dev Update the address of the genetic contract, can only be called by the CEO. /// @param _address An address of a GeneScience contract instance to be used from this point forward. function setGeneScienceAddress(address _address) external onlyCEO { GeneScienceInterface candidateContract = GeneScienceInterface(_address); // NOTE: verify that a contract is what we expect - https://github.com/Lunyr/crowdsale-contracts/blob/cfadd15986c30521d8ba7d5b6f57b4fefcc7ac38/contracts/LunyrToken.sol#L117 require(candidateContract.isGeneScience()); // Set the new contract address geneScience = candidateContract;
}

他们这样做是为了让游戏变得不那么简单 - 假如你能够读懂一只小猫的DNA是怎么确认的,那么就知道为了得到一只“奇特的猫”而跟哪只猫繁衍会简单得多。

这个外部 geneScience合约之后会在theGiveBirth() 函数(咱们稍后会看到)中运用,以确认新猫的DNA。

现在让咱们看看当两只猫在一同时会产生什么:

"font-family:Menlo, Courier, monospace, monospace, sans-serif;font-size:13.6px;margin:0px;border:none;background-color:transparent;white-space:pre-wrap;" class="hljs">/// @dev Internal utility function to initiate breeding, assumes that all breeding ///  requirements have been checked. function _breedWith(uint256 _matronId, uint256 _sireId) internal { // Grab a reference to the Kitties from storage. Kitty storage sire = kitties[_sireId]; Kitty storage matron = kitties[_matronId]; // Mark the matron as pregnant, keeping track of who the sire is. matron.siringWithId = uint32(_sireId); // Trigger the cooldown for both parents. _triggerCooldown(sire); _triggerCooldown(matron); // Clear siring permission for both parents. This may not be strictly necessary // but it's likely to avoid confusion! delete sireAllowedToAddress[_matronId]; delete sireAllowedToAddress[_sireId]; // Every time a kitty gets pregnant, counter is incremented. pregnantKitties++; // Emit the pregnancy event. Pregnant(kittyIndexToOwner[_matronId], _matronId, _sireId, matron.cooldownEndBlock);
}

这个函数需要母亲和父亲的ID,在kitties数组中查找它们,并将母亲上的siringWithId设置为父亲的ID。 (当siringWithId不为零时,表示母亲怀孕)。

它也履行爸爸妈妈两边的triggerCooldown函数,这会使他们在一段时刻内不能再一次繁衍。

接下来,有一个公开的 giveBirth() 函数 创立一个新的猫:

"font-family:Menlo, Courier, monospace, monospace, sans-serif;font-size:13.6px;margin:0px;border:none;background-color:transparent;white-space:pre-wrap;" class="hljs">/// @notice Have a pregnant Kitty give birth! /// @param _matronId A Kitty ready to give birth. /// @return The Kitty ID of the new kitten. /// @dev Looks at a given Kitty and, if pregnant and if the gestation period has passed, ///  combines the genes of the two parents to create a new kitten. The new Kitty is assigned ///  to the current owner of the matron. Upon successful completion, both the matron and the ///  new kitten will be ready to breed again. Note that anyone can call this function (if they ///  are willing to pay the gas!), but the new kitten always goes to the mother's owner. function giveBirth(uint256 _matronId)
    external
    whenNotPaused returns(uint256)
{ // Grab a reference to the matron in storage. Kitty storage matron = kitties[_matronId]; // Check that the matron is a valid cat. require(matron.birthTime != 0); // Check that the matron is pregnant, and that its time has come! require(_isReadyToGiveBirth(matron)); // Grab a reference to the sire in storage. uint256 sireId = matron.siringWithId; Kitty storage sire = kitties[sireId]; // Determine the higher generation number of the two parents uint16 parentGen = matron.generation; if (sire.generation > matron.generation) {
        parentGen = sire.generation;
    } // Call the sooper-sekret gene mixing operation. uint256 childGenes = geneScience.mixGenes(matron.genes, sire.genes, matron.cooldownEndBlock - 1); // Make the new kitten! address owner = kittyIndexToOwner[_matronId];
    uint256 kittenId = _createKitty(_matronId, matron.siringWithId, parentGen + 1, childGenes, owner); // Clear the reference to sire from the matron (REQUIRED! Having siringWithId // set is what marks a matron as being pregnant.) delete matron.siringWithId; // Every time a kitty gives birth counter is decremented. pregnantKitties--; // Send the balance fee to the person who made birth happen. msg.sender.send(autoBirthFee); // return the new kitten's ID return kittenId;
}

代码是十分显着的。 基本上,代码首要履行一些检查,看看母亲是否准备好生孩子。 然后运用geneScience.mixGenes()确认孩子的基因,将新基因的一切权分配给母亲,然后调用咱们在KittyBase中的函数_createKitty()。

请注意,geneScience.mixGenes()函数是一个黑匣子,由于该合约是闭源的。 所以咱们实践上并不知道孩子的基因是怎么决议的,但咱们知道这是母亲基因和父亲基因的功用,还有母亲的cooldownEndBlock。

以太猫开发源码分析--以太猫CryptoKitties系统源码分析插图75. KittyAuctions: 生意和繁衍服务(出台)

在这里,咱们有公开的办法来拍卖猫或招标猫或繁衍猫。 实践的拍卖功用是在两个兄弟合约(一个用于生意,一个用于繁衍)中处理的,而拍卖的创立和投标首要是经过中心合约。

根据开发者的说法,他们将这个拍卖功用分为“兄弟”合约,是由于“他们的逻辑有点复杂,总是存在微妙的bug风险。 经过保留它们自己的合约,咱们能够升级它们而不会中断追踪小猫一切权的主合约。“
因此,这个KittyAuctions合约包括函数setSaleAuctionAddress() 和setSiringAuctionAddress(),像 setGeneScienceAddress() 只能由CEO调用,并设置处理这些函数的外部合约的地址。

注意:“Siring”指的是把你的猫拉出来 - 把它拍卖,在那里另一个用户能够付钱给你以太,让你的猫与他们一同繁衍。哈哈。

这意味着,即便CryptoKitties合约本身是不可变的,首席履行官也能够灵活地改变这些拍卖合约的地址,然后改变拍卖规则。 相同,不一定是坏事,由于有时候开发人员需要批改bug,可是这是要注意的事情。

我不打算具体讨论怎么处理拍卖和出价逻辑,以防止这篇文章过长(已经够长了!),可是您能够在EthFiddle中查看代码(搜索KittyAuctions)。

以太猫开发源码分析--以太猫CryptoKitties系统源码分析插图86. KittyMinting: 创世猫工厂

最终一个方面包括咱们用来创立新的gen0猫的功用。 咱们最多能够制造5000只能够赠送的“营销”猫(在社区初期的时候尤为重要),其他一切的猫只能经过算法确认的开始价格创立,然后立即投入拍卖。 不管它们是怎么发明的,都有50k gen0猫的硬性极限。 之后,社群就要繁衍,繁衍,繁衍!

合约能够创立的promo cats和gen0 cat的数量在这里是硬编码的:

"font-family:Menlo, Courier, monospace, monospace, sans-serif;font-size:13.6px;margin:0px;border:none;background-color:transparent;white-space:pre-wrap;" class="hljs">uint256 public constant PROMO_CREATION_LIMIT = 5000;
uint256 public constant GEN0_CREATION_LIMIT = 45000;

这里是“COO”能够创立营销小猫和gen0小猫的代码:

"font-family:Menlo, Courier, monospace, monospace, sans-serif;font-size:13.6px;margin:0px;border:none;background-color:transparent;white-space:pre-wrap;" class="hljs">/// @dev we can create promo kittens, up to a limit. Only callable by COO /// @param _genes the encoded genes of the kitten to be created, any value is accepted /// @param _owner the future owner of the created kittens. Default to contract COO function createPromoKitty(uint256 _genes, address _owner) external onlyCOO {
    address kittyOwner = _owner; if (kittyOwner == address(0)) {
         kittyOwner = cooAddress;
    } require(promoCreatedCount < PROMO_CREATION_LIMIT);

    promoCreatedCount++; _createKitty(0, 0, 0, _genes, kittyOwner);
} /// @dev Creates a new gen0 kitty with the given genes and ///  creates an auction for it. function createGen0Auction(uint256 _genes) external onlyCOO { require(gen0CreatedCount < GEN0_CREATION_LIMIT);

    uint256 kittyId = _createKitty(0, 0, 0, _genes, address(this)); _approve(kittyId, saleAuction);

    saleAuction.createAuction(
        kittyId, _computeNextGen0Price(), 0, GEN0_AUCTION_DURATION, address(this)
    );

    gen0CreatedCount++;
}

所以经过createPromoKitty(),看起来COO能够用任何他想要的基因创立一个新的kitty,然后发送给任何他想要给的人(最多5000个kitty)。 我猜想他们是为了前期测验者,朋友和家人,为了促销意图而赠送免费的小猫咪等等。

可是这也意味着你的猫或许并不像你想象的那样绝无仅有,由于他或许会有5000个相同的副本!

对于createGen0Auction(),COO也提供新基因的遗传暗码。 但不是将其分配给特定的人的地址,而是创立一个用户能够出价购买小猫的拍卖。

以太猫开发源码分析--以太猫CryptoKitties系统源码分析插图97. KittyCore: 主合约

这是首要的CryptoKitties合约,编译和运行在以太坊区块链上。 这份合约把一切东西联系在一同。

由于承继结构,它承继了咱们之前所看到的一切合约,并增加了几个终究的办法,就像这个运用ID来获取一切的Kitty数据的函数:

"font-family:Menlo, Courier, monospace, monospace, sans-serif;font-size:13.6px;margin:0px;border:none;background-color:transparent;white-space:pre-wrap;" class="hljs"> /// @notice Returns all the relevant information about a specific kitty. /// @param _id The ID of the kitty of interest. function getKitty(uint256 _id)
    external
    view returns ( bool isGestating, bool isReady,
    uint256 cooldownIndex,
    uint256 nextActionAt,
    uint256 siringWithId,
    uint256 birthTime,
    uint256 matronId,
    uint256 sireId,
    uint256 generation,
    uint256 genes ) {
    Kitty storage kit = kitties[_id]; // if this variable is 0 then it's not gestating isGestating = (kit.siringWithId != 0);
    isReady = (kit.cooldownEndBlock <= block.number);
    cooldownIndex = uint256(kit.cooldownIndex);
    nextActionAt = uint256(kit.cooldownEndBlock);
    siringWithId = uint256(kit.siringWithId);
    birthTime = uint256(kit.birthTime);
    matronId = uint256(kit.matronId);
    sireId = uint256(kit.sireId);
    generation = uint256(kit.generation);
    genes = kit.genes;
}

这是一个公共办法,它回来区块链中特定小猫的一切数据。 我想这是他们的Web服务器在网站上显示的猫的查询。

等等...我没有看到任何图画数据。 什么决议了小猫的姿态?

从上面的代码能够看出,一个“小猫”基本上归结为一个256位的无符号整数,代表其遗传暗码。

Solidity合约代码中没有任何内容存储猫的图画或其描述,或许确认这个256位整数的实践意义。 该遗传暗码的解释产生在CryptoKitty的网络服务器上。

所以虽然这是区块链上游戏的一个十分聪明的演示,但实践上并不是100%的区块链。 假如将来他们的网站被脱机,除非有人备份了一切的图画,不然只剩下一个毫无意义的256位整数。

在合约代码中,我找到了一个名为ERC721Metadata的合约,但它永远不会被用于任何事情。 所以我的猜想是,他们最初方案将一切内容都存储在区块链中,但之后却决议不要这么做(在Ethereum中存储很多数据的价值太高),所以他们终究需要将其存储在Web服务器上。

以太猫开发源码分析--以太猫CryptoKitties系统源码分析插图10总结一下:

  • 小猫怎么表现为数据
  • 现存的一切小猫怎么存储在一个智能合约中,以及怎么盯梢谁具有什么
  • gen0小猫怎么生产
  • 小猫怎么在一同繁衍,形成新的小猫

本文来自网络,不代表软件信息网立场,转载请注明出处。软件定制开发交流:15528175269(微信同号)http://www.saasyo.com/xz/14477.html

作者: 华企网通圣西罗

圣西罗,一直致力于企业客户软件定制开发,计算机专业毕业后,一直从事于互联网产品开发到现在。系统开发,系统源码:15889726201
上一篇
下一篇
联系我们

联系我们

15889726201

在线咨询: QQ交谈

邮箱: 187395037@qq.com

工作时间:周一至周五,9:00-17:30,节假日休息

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

关注微博
返回顶部