Fabric node sdk交互測試

使用 Fabric 完成基礎(chǔ)的網(wǎng)絡(luò)配置和鏈碼的編寫和部署后袜茧,還需要我們進(jìn)一步通過上層應(yīng)用服務(wù)吃靠,完成對(duì)底層Fabric搭建的聯(lián)盟鏈的調(diào)用。本文提供使用 nodejs 編寫的上層 sdk 完成對(duì)鏈碼的調(diào)用操作鱼鸠。當(dāng)然墓塌,也可以使用 go 語言,異曲同工香缺。enjoy手销!

1 基礎(chǔ) chaincode 編寫

  1. 在 chaincode 文件夾中創(chuàng)建nodejs環(huán)境
# 1.進(jìn)入 chaincode 目錄
cd $GOPATH/src/github.com/hyperledger/fabric/scripts/fabric-samples/chaincode
# 2.創(chuàng)建我們自己的chaincode目錄
mkdir helloworldcc
# 3.初始化npm
npm init
# 4.下載fabric 鏈碼的必要依賴
npm install fabric-shim
# 5.編寫鏈碼,并保存至 helloworld.js 文件中

# 6.修改 package.json 文件图张,指定容器啟動(dòng)時(shí)候執(zhí)行的js程序
vim package.json
"scripts": {
    "start": "node helloworld.js"
},
  1. 編寫鏈碼 helloworld.js
//導(dǎo)入環(huán)境依賴
const shim = require('fabric-shim');
const Chaincode = class{
    //鏈碼初始化操作
    async Init(stub){
        //獲取當(dāng)前方法的名字和參數(shù)
        var ret = stub.getFunctionAndParameters();
        var args  = ret.params;
        var a = args[0];
        var aValue = args[1];
        var b = args[2];
        var bValue = args[3];
        await  stub.putState(a,Buffer.from(aValue));
        await  stub.putState(b,Buffer.from(bValue));
        return shim.success(Buffer.from('chaincod init successs'));
    }

    async Invoke(stub){
        let ret = stub.getFunctionAndParameters();
        let fcn = this[ret.fcn];
        return fcn(stub,ret.params);
    }
    //查詢操作
    async query(stub,args){
        let a = args[0];
        let balance = await stub.getState(a);
        return shim.success(balance);
    }

};
shim.start(new Chaincode());

2 基礎(chǔ)網(wǎng)絡(luò)環(huán)境搭建與鏈碼安裝

  1. 基礎(chǔ)網(wǎng)絡(luò)配置
# 根據(jù)fabric提供的基礎(chǔ)網(wǎng)絡(luò)環(huán)境配置網(wǎng)絡(luò)
cd $GOPATH/src/github.com/hyperledger/fabric/scripts/fabric-samples/
cp -r basic-network helloworld-network
  1. node sdk環(huán)境創(chuàng)建
# 創(chuàng)建webapp的工作目錄
cd $GOPATH/src/github.com/hyperledger/fabric/scripts/fabric-samples/
mkdir helloworld-webapp
cd helloworld-webapp
# 初始化環(huán)境锋拖,安裝依賴
npm init
npm install fabric-ca-client fabric-client
  1. 編寫cli安裝鏈碼腳本 startFabric.sh
#!/bin/bash

set -e

export MSYS_NO_PATHCONV=1

starttime=$(date +%s)

# 啟動(dòng) helloworld 的網(wǎng)絡(luò)環(huán)境,創(chuàng)建 channel 并將 peer 節(jié)點(diǎn)加入網(wǎng)絡(luò)
cd ../helloworld-network
./start.sh

# 啟動(dòng) cli 容器祸轮,用于安裝兽埃,實(shí)例化鏈碼
docker-compose -f ./docker-compose.yml up -d cli

# 安裝鏈碼
docker exec -e "CORE_PEER_LOCALMSPID=Org1MSP" -e "CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp" cli peer chaincode install -l node -n helloworld -v v1.0 -p /opt/gopath/src/github.com/helloworldcc/

# 實(shí)例化鏈碼
docker exec -e "CORE_PEER_LOCALMSPID=Org1MSP" -e "CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp" cli peer chaincode instantiate -o orderer.example.com:7050 -C mychannel -n helloworld -v v1.0 -c '{"args":["init","yorick","200","tom","100"]}' -P "OR ('Org1MSP.member','Org2MSP.member')"

# 等待
sleep 10

# 測試鏈碼調(diào)用
docker exec -e "CORE_PEER_LOCALMSPID=Org1MSP" -e "CORE_PEER_MSPCONFIGPATH=/opt/gopath/src/github.com/hyperledger/fabric/peer/crypto/peerOrganizations/org1.example.com/users/Admin@org1.example.com/msp" cli peer chaincode invoke -o orderer.example.com:7050 -C mychannel -n helloworld -c '{"args":["query","yorick"]}'

printf "\nTotal setup execution time : $(($(date +%s) - starttime)) secs ...\n\n\n"
chmod +x startFabric.sh
  1. 執(zhí)行腳本,完成網(wǎng)絡(luò)部署 and 鏈碼安裝并調(diào)用測試
./startFabric.sh
  • result
# 創(chuàng)建 channel 完畢
2022-12-06 11:44:03.909 UTC [channelCmd] InitCmdFactory -> INFO 001 Endorser and orderer connections initialized
2022-12-06 11:44:03.922 UTC [cli.common] readBlock -> INFO 002 Received block: 0
# peer加入 channel 完畢
2022-12-06 11:44:04.014 UTC [channelCmd] InitCmdFactory -> INFO 001 Endorser and orderer connections initialized
2022-12-06 11:44:04.090 UTC [channelCmd] executeJoin -> INFO 002 Successfully submitted proposal to join channel
# cli docker容器創(chuàng)建完畢
Creating cli ... done
# 鏈碼安裝倔撞,實(shí)例化,調(diào)用
2022-12-06 11:44:05.289 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 001 Using default escc
2022-12-06 11:44:05.289 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 002 Using default vscc
2022-12-06 11:44:05.319 UTC [chaincodeCmd] install -> INFO 003 Installed remotely response:<status:200 payload:"OK" >
2022-12-06 11:44:05.425 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 001 Using default escc
2022-12-06 11:44:05.425 UTC [chaincodeCmd] checkChaincodeCmdParams -> INFO 002 Using default vscc
# 成功獲取到 yorick 的 200 數(shù)據(jù)
2022-12-06 11:44:16.778 UTC [chaincodeCmd] chaincodeInvokeOrQuery -> INFO 001 Chaincode invoke successful. result: status:200 payload:"200"

Total setup execution time : 38 secs ...

3 CA 創(chuàng)建用戶

  1. 編寫 node 程序enrollAdmin.js慕趴,使用 ca 創(chuàng)建admin賬戶
'use strict';
var Fabric_Client = require('fabric-client');
var Fabric_CA_Client = require('fabric-ca-client');

var path = require('path');
var util = require('util');
var os = require('os');


var fabric_client = new Fabric_Client();
var fabric_ca_client = null;
var admin_user = null;
var member_user = null;
var store_path = path.join(__dirname, 'hfc-key-store');
console.log(' Store path:'+store_path);

// 創(chuàng)建 key-value-store痪蝇,用于保存用戶的身份信息(公私鑰)
// promise 風(fēng)格
Fabric_Client.newDefaultKeyValueStore({ path: store_path
}).then((state_store) => {
    // 設(shè)置存儲(chǔ)區(qū)塊鏈網(wǎng)絡(luò)狀態(tài)的目錄路徑鄙陡,比如智能合約的狀態(tài)和交易的狀態(tài)
    fabric_client.setStateStore(state_store);
    var crypto_suite = Fabric_Client.newCryptoSuite();
    // 設(shè)置存儲(chǔ)組織、節(jié)點(diǎn)和用戶的密鑰的目錄路徑(兩個(gè)目前使用相同的目錄存儲(chǔ))
    var crypto_store = Fabric_Client.newCryptoKeyStore({path: store_path});
    crypto_suite.setCryptoKeyStore(crypto_store);
    fabric_client.setCryptoSuite(crypto_suite);
    var tlsOptions = {
        trustedRoots: [],
        verify: false
    };
    // be sure to change the http to https when the CA is running TLS enabled
    // 設(shè)置 ca 客戶端
    fabric_ca_client = new Fabric_CA_Client('http://localhost:7054', tlsOptions , 'ca.example.com', crypto_suite);

    // 首次獲取admin賬戶躏啰,查看是否已經(jīng)登記
    return fabric_client.getUserContext('admin', true);
}).then((user_from_store) => {
    // 已經(jīng)登記admin趁矾,無需ca再創(chuàng)建
    if (user_from_store && user_from_store.isEnrolled()) {
        console.log('Successfully loaded admin from persistence');
        admin_user = user_from_store;
        return null;
    } else {
        // 需要 ca 服務(wù)端完成admin賬戶創(chuàng)建
        // 輸入 ca 的id和password,在docker-compose中已經(jīng)指定
        return fabric_ca_client.enroll({
          enrollmentID: 'admin',
          enrollmentSecret: 'adminpw'
        }).then((enrollment) => {
          console.log('Successfully enrolled admin user "admin"');
          // 創(chuàng)建 admin 賬戶
          return fabric_client.createUser(
              {username: 'admin',
                  mspid: 'Org1MSP',
                  cryptoContent: { privateKeyPEM: enrollment.key.toBytes(), signedCertPEM: enrollment.certificate }
              });
        }).then((user) => {
          // 給 fabric 客戶端設(shè)置 admin
          admin_user = user;
          return fabric_client.setUserContext(admin_user);
        }).catch((err) => {
          console.error('Failed to enroll and persist admin. Error: ' + err.stack ? err.stack : err);
          throw new Error('Failed to enroll admin');
        });
    }
}).then(() => {
    console.log('Assigned the admin user to the fabric client ::' + admin_user.toString());
}).catch((err) => {
    console.error('Failed to enroll admin: ' + err);
});
  1. 執(zhí)行 enrollAdmin.js 程序给僵,得到ca給其辦法的證書毫捣,hfc-key-store中存有admin用戶的公私鑰信息
$ node enrollAdmin.js
 Store path:/opt/go-project/src/github.com/hyperledger/fabric/scripts/fabric-samples/helloworld-webapp/hfc-key-store
Successfully enrolled admin user "admin"
Assigned the admin user to the fabric client ::{"name":"admin","mspid":"Org1MSP","roles":null,"affiliation":"","enrollmentSecret":"","enrollment":{"signingIdentity":"bee3c03e36a357f52a80434a542e8d1e574d576c1f7ad93f95cae78b968ce980","identity":{"certificate":"-----BEGIN CERTIFICATE-----\nMIICATCCAaigAwIBAgIUSQqYMOL+H6y2I8vVFhAOfQ1G2wYwCgYIKoZIzj0EAwIw\nczELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExFjAUBgNVBAcTDVNh\nbiBGcmFuY2lzY28xGTAXBgNVBAoTEG9yZzEuZXhhbXBsZS5jb20xHDAaBgNVBAMT\nE2NhLm9yZzEuZXhhbXBsZS5jb20wHhcNMjIxMjA2MTE0ODAwWhcNMjMxMjA2MTE1\nMzAwWjAhMQ8wDQYDVQQLEwZjbGllbnQxDjAMBgNVBAMTBWFkbWluMFkwEwYHKoZI\nzj0CAQYIKoZIzj0DAQcDQgAEvkFbiPCqKdeljnWzEuBwVXa87V4l3Dak3mVbyOO6\nQ75OIpdisxulZHxgNmpNYhMyb1pW3sRtqUP/LPXebnq4yKNsMGowDgYDVR0PAQH/\nBAQDAgeAMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFB9QLZbsSi5gX4Ew0ctTPN4g\nSxAqMCsGA1UdIwQkMCKAIEI5qg3NdtruuLoM2nAYUdFFBNMarRst3dusalc2Xkl8\nMAoGCCqGSM49BAMCA0cAMEQCIELJMJjIk2Qz19XZh1VmF0tJPshSiU9+KHjAltYq\nOANdAiABN0jioRUQBHlvUNONZP3tm798/Vj7akqs0kHCz1/dzQ==\n-----END CERTIFICATE-----\n"}}}
  1. 編寫 node 程序registerUser.js,使用剛剛生成的admin用戶帝际,注冊(cè)u(píng)ser1用戶
'use strict';
/*
* Copyright IBM Corp All Rights Reserved
*
* SPDX-License-Identifier: Apache-2.0
*/
/*
 * Register and Enroll a user
 */

var Fabric_Client = require('fabric-client');
var Fabric_CA_Client = require('fabric-ca-client');

var path = require('path');
var util = require('util');
var os = require('os');

//
var fabric_client = new Fabric_Client();
var fabric_ca_client = null;
var admin_user = null;
var member_user = null;
var store_path = path.join(__dirname, 'hfc-key-store');
console.log(' Store path:'+store_path);

// create the key value store as defined in the fabric-client/config/default.json 'key-value-store' setting
Fabric_Client.newDefaultKeyValueStore({ path: store_path
}).then((state_store) => {
    // assign the store to the fabric client
    fabric_client.setStateStore(state_store);
    var crypto_suite = Fabric_Client.newCryptoSuite();
    // use the same location for the state store (where the users' certificate are kept)
    // and the crypto store (where the users' keys are kept)
    var crypto_store = Fabric_Client.newCryptoKeyStore({path: store_path});
    crypto_suite.setCryptoKeyStore(crypto_store);
    fabric_client.setCryptoSuite(crypto_suite);
    var tlsOptions = {
        trustedRoots: [],
        verify: false
    };
    // be sure to change the http to https when the CA is running TLS enabled
    fabric_ca_client = new Fabric_CA_Client('http://localhost:7054', null , '', crypto_suite);

    // first check to see if the admin is already enrolled
    return fabric_client.getUserContext('admin', true);
}).then((user_from_store) => {
    // 創(chuàng)建 user1 普通用戶前蔓同,必須已經(jīng)有 admin 賬戶,否則拋異常
    if (user_from_store && user_from_store.isEnrolled()) {
        console.log('Successfully loaded admin from persistence');
        admin_user = user_from_store;
    } else {
        throw new Error('Failed to get admin.... run enrollAdmin.js');
    }

    // at this point we should have the admin user
    // first need to register the user with the CA server
    return fabric_ca_client.register({enrollmentID: 'user1', affiliation: 'org1.department1'}, admin_user);
}).then((secret) => {
    // next we need to enroll the user with CA server
    console.log('Successfully registered user1 - secret:'+ secret);

    return fabric_ca_client.enroll({enrollmentID: 'user1', enrollmentSecret: secret});
}).then((enrollment) => {
  console.log('Successfully enrolled member user "user1" ');
  return fabric_client.createUser(
     {username: 'user1',
     mspid: 'Org1MSP',
     cryptoContent: { privateKeyPEM: enrollment.key.toBytes(), signedCertPEM: enrollment.certificate }
     });
}).then((user) => {
     member_user = user;

     return fabric_client.setUserContext(member_user);
}).then(()=>{
     console.log('User1 was successfully registered and enrolled and is ready to intreact with the fabric network');

}).catch((err) => {
    console.error('Failed to register: ' + err);
    if(err.toString().indexOf('Authorization') > -1) {
        console.error('Authorization failures may be caused by having admin credentials from a previous CA instance.\n' +
        'Try again after deleting the contents of the store directory '+store_path);
    }
});
  1. 執(zhí)行 registerUser.js 程序蹲诀,hfc-key-store中存有user1用戶的公私鑰信息
$ node registerUser.js
 Store path:/opt/go-project/src/github.com/hyperledger/fabric/scripts/fabric-samples/helloworld-webapp/hfc-key-store
Successfully loaded admin from persistence
Successfully registered user1 - secret:PbjicGKZoMwV
Successfully enrolled member user "user1"
User1 was successfully registered and enrolled and is ready to intreact with the fabric network
  1. 編寫 node 程序 query.js 斑粱,調(diào)用helloworld鏈碼的query函數(shù),查詢“yorick”的值
'use strict';
/*
* Copyright IBM Corp All Rights Reserved
*
* SPDX-License-Identifier: Apache-2.0
*/
/*
 * Chaincode query
 */

var Fabric_Client = require('fabric-client');
var path = require('path');
var util = require('util');
var os = require('os');

//
var fabric_client = new Fabric_Client();

// setup the fabric network
var channel = fabric_client.newChannel('mychannel');
var peer = fabric_client.newPeer('grpc://localhost:7051');
channel.addPeer(peer);

//
var member_user = null;
var store_path = path.join(__dirname, 'hfc-key-store');
console.log('Store path:'+store_path);
var tx_id = null;

// 獲取操作鏈碼的用戶身份信息
Fabric_Client.newDefaultKeyValueStore({ path: store_path
}).then((state_store) => {
    // assign the store to the fabric client
    fabric_client.setStateStore(state_store);
    var crypto_suite = Fabric_Client.newCryptoSuite();
    // use the same location for the state store (where the users' certificate are kept)
    // and the crypto store (where the users' keys are kept)
    var crypto_store = Fabric_Client.newCryptoKeyStore({path: store_path});
    crypto_suite.setCryptoKeyStore(crypto_store);
    fabric_client.setCryptoSuite(crypto_suite);

    // get the enrolled user from persistence, this user will sign all requests
    return fabric_client.getUserContext('user1', true);
}).then((user_from_store) => {
    if (user_from_store && user_from_store.isEnrolled()) {
        console.log('Successfully loaded user1 from persistence');
        member_user = user_from_store;
    } else {
        throw new Error('Failed to get user1.... run registerUser.js');
    }

    // 查詢請(qǐng)求構(gòu)建
    const request = {
        chaincodeId: 'helloworld',
        fcn: 'query',
        args: ['yorick']
    };

    // 向peer節(jié)點(diǎn)發(fā)送請(qǐng)求提案
    return channel.queryByChaincode(request);
}).then((query_responses) => {
    console.log("Query has completed, checking results");
    // query_responses could have more than one  results if there multiple peers were used as targets
    if (query_responses && query_responses.length == 1) {
        if (query_responses[0] instanceof Error) {
            console.error("error from query = ", query_responses[0]);
        } else {
      // 打印查詢的結(jié)果
            console.log("Response is ", query_responses[0].toString());
        }
    } else {
        console.log("No payloads were returned from query");
    }
}).catch((err) => {
    console.error('Failed to query successfully :: ' + err);
});
  1. 執(zhí)行 query.js 程序脯爪,使用hfc-key-store中存有user1用戶作為身份憑證調(diào)用鏈碼则北,獲取yorick的值信息
$ node query.js
Store path:/opt/go-project/src/github.com/hyperledger/fabric/scripts/fabric-samples/helloworld-webapp/hfc-key-store
Successfully loaded user1 from persistence
Query has completed, checking results
Response is  200

至此,完成了通過 node sdk 的方式調(diào)用鏈碼痕慢。后續(xù)可以對(duì)sdk進(jìn)一步封裝尚揣,對(duì)外提供相關(guān)訪問請(qǐng)求crud接口,供上層服務(wù)調(diào)用掖举。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末快骗,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子拇泛,更是在濱河造成了極大的恐慌滨巴,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,490評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件俺叭,死亡現(xiàn)場離奇詭異恭取,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)熄守,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門蜈垮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人裕照,你說我怎么就攤上這事攒发。” “怎么了晋南?”我有些...
    開封第一講書人閱讀 165,830評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵惠猿,是天一觀的道長。 經(jīng)常有香客問我负间,道長偶妖,這世上最難降的妖魔是什么姜凄? 我笑而不...
    開封第一講書人閱讀 58,957評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮趾访,結(jié)果婚禮上态秧,老公的妹妹穿的比我還像新娘。我一直安慰自己扼鞋,他們只是感情好申鱼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,974評(píng)論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著云头,像睡著了一般捐友。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盘寡,一...
    開封第一講書人閱讀 51,754評(píng)論 1 307
  • 那天楚殿,我揣著相機(jī)與錄音,去河邊找鬼竿痰。 笑死脆粥,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的影涉。 我是一名探鬼主播变隔,決...
    沈念sama閱讀 40,464評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蟹倾!你這毒婦竟也來了匣缘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤鲜棠,失蹤者是張志新(化名)和其女友劉穎肌厨,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體豁陆,經(jīng)...
    沈念sama閱讀 45,847評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡柑爸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,995評(píng)論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了盒音。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片表鳍。...
    茶點(diǎn)故事閱讀 40,137評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖祥诽,靈堂內(nèi)的尸體忽然破棺而出譬圣,到底是詐尸還是另有隱情,我是刑警寧澤雄坪,帶...
    沈念sama閱讀 35,819評(píng)論 5 346
  • 正文 年R本政府宣布厘熟,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏绳姨。R本人自食惡果不足惜颇玷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,482評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望就缆。 院中可真熱鬧,春花似錦谒亦、人聲如沸竭宰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽切揭。三九已至,卻和暖如春锁摔,著一層夾襖步出監(jiān)牢的瞬間廓旬,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評(píng)論 1 272
  • 我被黑心中介騙來泰國打工谐腰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留孕豹,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,409評(píng)論 3 373
  • 正文 我出身青樓十气,卻偏偏與公主長得像励背,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子砸西,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,086評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容