溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Hyperledger Fabric啟用CouchDB為狀態(tài)數(shù)據(jù)庫(kù)

發(fā)布時(shí)間:2020-06-02 18:52:26 來(lái)源:網(wǎng)絡(luò) 閱讀:47759 作者:暗黑魔君 欄目:數(shù)據(jù)庫(kù)

Hyperledger Fabric 啟用CouchDB作為狀態(tài)數(shù)據(jù)庫(kù)

一.概述

1. 數(shù)據(jù)請(qǐng)求流

超級(jí)賬本采用背書(shū)/共識(shí)模型,模擬執(zhí)行和區(qū)塊驗(yàn)證是在不同角色的節(jié)點(diǎn)中分開(kāi)執(zhí)行的。模擬執(zhí)行是并發(fā)的,這樣可以提高擴(kuò)展性和吞吐量:

  • 背書(shū)節(jié)點(diǎn):模擬執(zhí)行鏈碼
  • Peer節(jié)點(diǎn):驗(yàn)證交易并提交

Hyperledger Fabric啟用CouchDB為狀態(tài)數(shù)據(jù)庫(kù)

2.超級(jí)賬本存儲(chǔ)元素

超級(jí)賬本包含以下元素:

  • 賬本編號(hào):快速查詢存在哪些賬本
  • 賬本數(shù)據(jù): 實(shí)際的區(qū)塊數(shù)據(jù)存儲(chǔ)
  • 區(qū)塊索引: 快速查詢區(qū)塊/交易
  • 狀態(tài)數(shù)據(jù): 最新的世界狀態(tài)數(shù)據(jù)
  • 歷史數(shù)據(jù): 跟蹤鍵的歷史

每個(gè)Peer節(jié)點(diǎn)會(huì)維護(hù)四個(gè)DB,分別為:

  • 賬本索引庫(kù)(IdStore):存儲(chǔ)ChainID
  • 狀態(tài)數(shù)據(jù)庫(kù)(StateDB): 存儲(chǔ)world state
  • 歷史數(shù)據(jù)庫(kù)(HistoryDB): 存儲(chǔ)Key的版本變化
  • 區(qū)塊索引庫(kù)(BlockIndex):存儲(chǔ)Block索引

Hyperledger Fabric啟用CouchDB為狀態(tài)數(shù)據(jù)庫(kù)

3.狀態(tài)數(shù)據(jù)庫(kù)

狀態(tài)數(shù)據(jù)庫(kù)可選類型包括LevelDB和CouchDB。LevelDB是嵌入在peer進(jìn)程中的默認(rèn)鍵/值狀態(tài)數(shù)據(jù)庫(kù),CouchDB是一個(gè)可選的外部狀態(tài)數(shù)據(jù)庫(kù)。與LevelDB鍵/值存儲(chǔ)一樣,CouchDB可以存儲(chǔ)任何以chaincode建模的二進(jìn)制數(shù)據(jù)(CouchDB附件函數(shù)在內(nèi)部用于非json二進(jìn)制數(shù)據(jù))。但是,當(dāng)chaincode值(例如,資產(chǎn))被建模為JSON數(shù)據(jù)時(shí),作為JSON文檔存儲(chǔ),CouchDB支持對(duì)chaincode數(shù)據(jù)進(jìn)行豐富的查詢。

LevelDB和CouchDB都支持核心chaincode操作,例如獲取和設(shè)置一個(gè)鍵(資產(chǎn)),并根據(jù)鍵進(jìn)行查詢。鍵可以通過(guò)范圍查詢,可以對(duì)組合鍵進(jìn)行建模,以支持針對(duì)多個(gè)參數(shù)的等價(jià)查詢。例如,作為所有者的組合鍵,資產(chǎn)id可以用于查詢某個(gè)實(shí)體擁有的所有資產(chǎn)。這些基于key的查詢可以用于針對(duì)賬本的只讀查詢,以及更新總賬的事務(wù)。

如果將資產(chǎn)建模為JSON并使用CouchDB,那么就可以使用chaincode中的CouchDB JSON查詢語(yǔ)言對(duì)chaincode數(shù)據(jù)值執(zhí)行復(fù)雜的富查詢,這些類型的查詢對(duì)于理解賬本上的內(nèi)容很有幫助。對(duì)于這些類型的查詢,事務(wù)協(xié)議響應(yīng)通常對(duì)客戶端應(yīng)用程序有用,但通常不會(huì)作為事務(wù)提交到排序服務(wù)。事實(shí)上,也無(wú)法保證結(jié)果集在chaincode執(zhí)行與富查詢提交時(shí)間之間的穩(wěn)定性,因此使用富查詢的結(jié)果去執(zhí)行最終的事務(wù)更新操作是不合適的,除非可以保證結(jié)果集在chaincode執(zhí)行時(shí)間與提交時(shí)間之間的穩(wěn)定性,或者可以處理在后續(xù)交易中的潛在變化。例如,如果對(duì)Alice所擁有的所有資產(chǎn)執(zhí)行一個(gè)富查詢并將其傳輸給Bob,那么一個(gè)新的資產(chǎn)可能會(huì)被另一個(gè)事務(wù)分配給Alice,這是在chaincode執(zhí)行時(shí)間和提交時(shí)間之間的另一個(gè)事務(wù),可能此過(guò)程中會(huì)錯(cuò)過(guò)這個(gè)“虛值”。

CouchDB作為一個(gè)獨(dú)立的數(shù)據(jù)庫(kù)進(jìn)程與peer一起運(yùn)行,因此在設(shè)置、管理和操作方面有額外的考慮。我們可以考慮從默認(rèn)的嵌入式LevelDB開(kāi)始,如果需要額外的復(fù)雜的富查詢,可以轉(zhuǎn)移到CouchDB。將chaincode資產(chǎn)數(shù)據(jù)建模為JSON是一種很好的做法,這樣我們就可以在將來(lái)執(zhí)行需要的復(fù)雜的富查詢。

二. 啟用CouchDB

本文均采用Hyperledger Fabric1.2中fabric-samples中相關(guān)組件與資源,在測(cè)試環(huán)境(fabric-samples/chaincode-docker-devmode)通過(guò)Docker啟動(dòng)CouchDB服務(wù)

1.配置CouchDB啟動(dòng)信息

參考:fabric-samples/first-network/docker-compose-couch.yaml

  couchdb0:
    container_name: couchdb0
    image: hyperledger/fabric-couchdb
    # Populate the COUCHDB_USER and COUCHDB_PASSWORD to set an admin user and password
    # for CouchDB.  This will prevent CouchDB from operating in an "Admin Party" mode.
    environment:
      - COUCHDB_USER=
      - COUCHDB_PASSWORD=
    # Comment/Uncomment the port mapping if you want to hide/expose the CouchDB service,
    # for example map it to utilize Fauxton User Interface in dev environments.
    ports:
      - "5984:5984"
    networks:
      - byfn

修改:fabric-samples/chaincode-docker-devmode/docker-compose-simple.yaml 末尾添加并修改

  couchdb:
    container_name: couchdb
    image: hyperledger/fabric-couchdb
    # Populate the COUCHDB_USER and COUCHDB_PASSWORD to set an admin user and password
    # for CouchDB.  This will prevent CouchDB from operating in an "Admin Party" mode.
    environment:
      - COUCHDB_USER=
      - COUCHDB_PASSWORD=
    # Comment/Uncomment the port mapping if you want to hide/expose the CouchDB service,
    # for example map it to utilize Fauxton User Interface in dev environments.
    ports:
      - "5984:5984"
2.配置CouchDB連接信息

參考fabric-samples/first-network/docker-compose-couch.yaml

  peer0.org1.example.com:
    environment:
      - CORE_LEDGER_STATE_STATEDATABASE=CouchDB
      - CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb0:5984
      # The CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME and CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD
      # provide the credentials for ledger to connect to CouchDB.  The username and password must
      # match the username and password set for the associated CouchDB.
      - CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=
      - CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=
    depends_on:
      - couchdb0

修改:fabric-samples/chaincode-docker-devmode/docker-compose-simple.yaml 中peer模塊

修改前

  peer:
    container_name: peer
    image: hyperledger/fabric-peer
    environment:
      - CORE_PEER_ID=peer
      - CORE_PEER_ADDRESS=peer:7051
      - CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer:7051
      - CORE_PEER_LOCALMSPID=DEFAULT
      - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
      - CORE_LOGGING_LEVEL=DEBUG
      - CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/msp
    volumes:
        - /var/run/:/host/var/run/
        - ./msp:/etc/hyperledger/msp
    working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer
    command: peer node start --peer-chaincodedev=true -o orderer:7050
    ports:
      - 7051:7051
      - 7053:7053
    depends_on:
      - orderer

修改后

  peer:
    container_name: peer
    image: hyperledger/fabric-peer
    environment:
      - CORE_PEER_ID=peer
      - CORE_PEER_ADDRESS=peer:7051
      - CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer:7051
      - CORE_PEER_LOCALMSPID=DEFAULT
      - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
      - CORE_LOGGING_LEVEL=DEBUG
      - CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/msp
      - CORE_LEDGER_STATE_STATEDATABASE=CouchDB
      - CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb:5984
      - CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=
      - CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=

    volumes:
        - /var/run/:/host/var/run/
        - ./msp:/etc/hyperledger/msp
    working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer
    command: peer node start --peer-chaincodedev=true -o orderer:7050
    ports:
      - 7051:7051
      - 7053:7053
    depends_on:
      - orderer
      - couchdb

注意JSON文件的格式以及配置信息的一致性,如couchdb名稱等

3.啟動(dòng)測(cè)試環(huán)境
# docker-compose  -f docker-compose-simple.yaml  up -d
# docker container ls

Hyperledger Fabric啟用CouchDB為狀態(tài)數(shù)據(jù)庫(kù)

三.編寫鏈碼

1.代碼結(jié)構(gòu)

代碼包:testdb

代碼文件

  • domain.go //數(shù)據(jù)結(jié)構(gòu)代碼
  • main.go //業(yè)務(wù)測(cè)試代碼
2.數(shù)據(jù)結(jié)構(gòu)
package main

type BillStruct struct {
    ObjectType   string `json:"DocType"`      //對(duì)象類型定義
    BillInfoID   string `json:"BillInfoID"`   //票據(jù)ID
    BillInfoAmt  string `json:"BillInfoAmt"`  //票據(jù)金額
    BillInfoType string `json:"BillInfoType"` //票據(jù)類型
    BillIsseData string `json:"BillIsseData"` //出票日期
    BillDueDate  string `json:"BillDueDate"`  //到期日期

    HoldrAcct       string `json:"HoldrAcct"`       //持票人名稱
    HoldrCmID       string `json:"HoldrCmID"`       //持票人ID
    WaitEndroseAcct string `json:"WaitEndroseAcct"` //待背書(shū)人名稱
    WaitEndorseCmID string `json:"WaitEndorseCmID"` //待背書(shū)人ID
}
3.測(cè)試代碼

請(qǐng)仔細(xì)閱讀注釋信息,此處不做代碼分割描述

package main

import (
    "github.com/hyperledger/fabric/core/chaincode/shim"
    "fmt"
    "github.com/hyperledger/fabric/protos/peer"
    "encoding/json"
    "bytes"
)

//定義結(jié)構(gòu)體CouchDBChaincode,作為shim.ChaincodeStubInterface實(shí)現(xiàn)類對(duì)象
type CouchDBChaincode struct {
}

//重寫shim.ChaincodeStubInterface接口的Init方法
func (t *CouchDBChaincode) Init(stub shim.ChaincodeStubInterface) peer.Response {
    return shim.Success(nil)
}

//重寫shim.ChaincodeStubInterface接口的Invoke方法
func (t *CouchDBChaincode) Invoke(stub shim.ChaincodeStubInterface) peer.Response {
    //獲取用戶意圖與參數(shù)
    fun, args := stub.GetFunctionAndParameters()
    //根據(jù)用戶意圖判斷使用何種實(shí)現(xiàn)函數(shù)
    if fun == "billInit" {
        return billInit(stub)
    } else if fun == "queryBills" {
        return queryBills(stub, args)
    } else if fun == "queryWaitBills" {
        return queryWaitBills(stub, args)
    }
    //如果用戶意圖不符合如上,進(jìn)行錯(cuò)誤提示
    return shim.Error("非法操作,指定的函數(shù)名無(wú)效")
}

//billInit函數(shù):初始化票據(jù)數(shù)據(jù)
func billInit(stub shim.ChaincodeStubInterface) peer.Response {

    /*
定義第一個(gè)票據(jù):
持票人名稱:AAA
持票人ID:AID
待背書(shū)人名稱:無(wú)
待背書(shū)人ID:無(wú)
     */

    billA := BillStruct{
        ObjectType:      "billObj",
        BillInfoID:      "POC001",
        BillInfoAmt:     "1000",
        BillInfoType:    "111",
        BillIsseData:    "20180501",
        BillDueDate:     "20180508",
        HoldrAcct:       "AAA",
        HoldrCmID:       "AID",
        WaitEndroseAcct: "",
        WaitEndorseCmID: "",
    }
    //通過(guò)json.Marshal方法對(duì)票據(jù)進(jìn)行序列化操作
    billAByte, _ := json.Marshal(billA)
    //通過(guò)stub.PutState方法存儲(chǔ)序列化后的字節(jié)數(shù)組
    err := stub.PutState(billA.BillInfoID, billAByte)
    if err != nil {
        return shim.Error("初始化第一個(gè)票據(jù)失敗:" + err.Error())
    }

    billB := BillStruct{
        ObjectType:      "billObj",
        BillInfoID:      "POC002",
        BillInfoAmt:     "1000",
        BillInfoType:    "111",
        BillIsseData:    "20180501",
        BillDueDate:     "20180508",
        HoldrAcct:       "AAA",
        HoldrCmID:       "AID",
        WaitEndroseAcct: "BBB",
        WaitEndorseCmID: "BID",
    }
    billBByte, _ := json.Marshal(billB)
    err = stub.PutState(billB.BillInfoID, billBByte)
    if err != nil {
        return shim.Error("初始化第二個(gè)票據(jù)失敗:" + err.Error())
    }

    billC := BillStruct{
        ObjectType:      "billObj",
        BillInfoID:      "POC003",
        BillInfoAmt:     "1000",
        BillInfoType:    "111",
        BillIsseData:    "20180501",
        BillDueDate:     "20180508",
        HoldrAcct:       "BBB",
        HoldrCmID:       "BID",
        WaitEndroseAcct: "CCC",
        WaitEndorseCmID: "CID",
    }

    billCByte, _ := json.Marshal(billC)
    err = stub.PutState(billC.BillInfoID, billCByte)
    if err != nil {
        return shim.Error("初始化第三個(gè)票據(jù)失敗:" + err.Error())
    }

    billD := BillStruct{
        ObjectType:      "billObj",
        BillInfoID:      "POC004",
        BillInfoAmt:     "1000",
        BillInfoType:    "111",
        BillIsseData:    "20180501",
        BillDueDate:     "20180508",
        HoldrAcct:       "CCC",
        HoldrCmID:       "CID",
        WaitEndroseAcct: "BBB",
        WaitEndorseCmID: "BID",
    }

    billDByte, _ := json.Marshal(billD)
    err = stub.PutState(billD.BillInfoID, billDByte)
    if err != nil {
        return shim.Error("初始化第四個(gè)票據(jù)失敗:" + err.Error())
    }

    return shim.Success([]byte("所有票據(jù)初始化成功"))

}

//queryBills函數(shù):批量查詢指定用戶的持票列表
func queryBills(stub shim.ChaincodeStubInterface, args []string) peer.Response {
    //判斷是否有參數(shù)傳入
    if len(args) != 1 {
        return shim.Error("必須指定持票人的證件號(hào)碼")
    }
    //將第一個(gè)參數(shù)作為用戶ID
    holdrCmID := args[0]

    /*將CouchDB查詢字符串拼接成一個(gè)JSON串,格式如下:
        {
        "selector": {
            "docType": "billObj",
            "HoldrCmID": "%s"
        }
    }
    */
    queryString := fmt.Sprintf("{\"selector\":{\"DocType\":\"billObj\",\"HoldrCmID\":\"%s\"}}", holdrCmID)
    //通過(guò)自定義的getBillByQueryString函數(shù)進(jìn)行數(shù)據(jù)查詢操作
    result, err := getBillByQueryString(stub, queryString)
    if err != nil {
        return shim.Error("根據(jù)持票人的證件號(hào)碼批量查詢持票人持有票據(jù)列表時(shí)發(fā)生錯(cuò)誤" + err.Error())
    }
    return shim.Success(result)

}

//queryWaitBills函數(shù):批量查詢指定用戶的待背書(shū)票據(jù)列表
func queryWaitBills(stub shim.ChaincodeStubInterface, args []string) peer.Response {
    if len(args) != 1 {
        return shim.Error("必須指定待背書(shū)人的證件號(hào)碼")
    }
    waitEndorseCmID := args[0]

    queryString := fmt.Sprintf("{\"selector\":{\"docType\":\"billObj\",\"WaitEndorseCmID\":\"%s\"}}", waitEndorseCmID)
    result, err := getBillByQueryString(stub, queryString)

    if err != nil {
        return shim.Error("根據(jù)待背書(shū)人的證件號(hào)碼批量查詢待背書(shū)票據(jù)列表時(shí)發(fā)生錯(cuò)誤" + err.Error())
    }
    return shim.Success(result)
}

//自定義函數(shù):getBillByQueryString:根據(jù)指定的查詢字符串(CouchDB查詢語(yǔ)句)查詢數(shù)據(jù)
func getBillByQueryString(stub shim.ChaincodeStubInterface, queryString string) ([]byte, error) {
    //通過(guò)stub.GetQueryResult方法獲取迭代器iterator
    iterator, err := stub.GetQueryResult(queryString)
    if err != nil {
        return nil, err
    }
    //延遲關(guān)閉迭代器iterator
    defer iterator.Close()
    //定義字節(jié)緩沖變量
    var buffer bytes.Buffer
    //定義分割符
    var isSplit bool
    //對(duì)迭代器進(jìn)行遍歷操作
    for iterator.HasNext() {
        //通過(guò)迭代器的Next()方法獲取下一個(gè)對(duì)象的Key與Value值(*queryresult.KV)
        result, err := iterator.Next()
        if err != nil {
            return nil, err
        }

        if isSplit {
            buffer.WriteString(";")
        }
        //定義格式
        // key:result.key result.Value
        buffer.WriteString("key:")
        buffer.WriteString(result.Key)
        buffer.WriteString(",value:")
        buffer.WriteString(string(result.Value))
        //獲取到第一個(gè)值后,將isSplit設(shè)置為true,用于跟第二個(gè)值進(jìn)行分割
        isSplit = true

    }
    //返回buffer對(duì)象的字節(jié)類型
    return buffer.Bytes(), nil
}

func main() {
    //啟動(dòng)鏈碼CouchDBChaincode
    err := shim.Start(new(CouchDBChaincode))
    //如有報(bào)錯(cuò),提示報(bào)錯(cuò)信息
    if err != nil {
        fmt.Errorf(err.Error())
    }

}

四.安裝鏈碼

1.上傳鏈碼

上傳鏈碼包testdb至:fabric-samples/chaincode中

# ls /home/bruce/hyfa/fabric-samples/chaincode/testdb/
 domain.go  main.go
2.編譯鏈碼
# cd  /home/bruce/hyfa/fabric-samples/chaincode/testdb/
# go build 
# ls 
domain.go  main.go  testdb
3.啟動(dòng)鏈碼

進(jìn)入chaincode容器進(jìn)行操作

# docker container exec -it chaincode bash #進(jìn)入chaincode容器進(jìn)行操作
# cd testdb/
# CORE_PEER_ADDRESS=peer:7052 CORE_CHAINCODE_ID_NAME=testCouchDB:1.0  ./testdb

2018-08-05 10:33:37.063 UTC [shim] SetupChaincodeLogging -> INFO 001 Chaincode log level not provided; defaulting to: INFO
2018-08-05 10:33:37.063 UTC [shim] SetupChaincodeLogging -> INFO 002 Chaincode (build level: ) starting up ...
4.安裝與實(shí)例化鏈碼

進(jìn)入cli容器進(jìn)行操作

# docker container exec -it cli bash
# peer chaincode install -n testCouchDB -v 1.0 -p chaincodedev/chaincode/testdb
# peer chaincode instantiate -n testCouchDB -v 1.0 -C myc -c '{"Args":["init"]}'
如有更新請(qǐng)用如下命令進(jìn)行操作
# peer chaincode install -n testCouchDB -v 1.1 -p chaincodedev/chaincode/testdb
# peer chaincode upgrade -n testCouchDB -v 1.1 -C myc -c '{"Args":["init"]}'

五.測(cè)試鏈碼

1.初始化票據(jù)
# peer chaincode  invoke  -n testCouchDB -C myc -c '{"Args":["billInit"]}'
2.查詢指定用戶所持票據(jù)
# peer chaincode  query  -n testCouchDB -C myc -c '{"Args":["queryBills","AID"]}'

Hyperledger Fabric啟用CouchDB為狀態(tài)數(shù)據(jù)庫(kù)

key: POC001, value: {
    "BillDueDate": "20180508",
    "BillInfoAmt": "1000",
    "BillInfoID": "POC001",
    "BillInfoType": "111",
    "BillIsseData": "20180501",
    "HoldrAcct": "AAA",
    "HoldrCmID": "AID",
    "WaitEndorseCmID": "",
    "WaitEndroseAcct": "",
    "docType": "billObj"
};
key: POC002, value: {
    "BillDueDate": "20180508",
    "BillInfoAmt": "1000",
    "BillInfoID": "POC002",
    "BillInfoType": "111",
    "BillIsseData": "20180501",
    "HoldrAcct": "AAA",
    "HoldrCmID": "AID",
    "WaitEndorseCmID": "BID",
    "WaitEndroseAcct": "BBB",
    "docType": "billObj"
}

查詢結(jié)果可以看到我們定義的分隔符;

3.查詢指定用戶待背書(shū)票據(jù)
# peer chaincode  query  -n testCouchDB -C myc -c '{"Args":["queryWaitBills","BID"]}'

Hyperledger Fabric啟用CouchDB為狀態(tài)數(shù)據(jù)庫(kù)

key: POC002, value: {
    "BillDueDate": "20180508",
    "BillInfoAmt": "1000",
    "BillInfoID": "POC002",
    "BillInfoType": "111",
    "BillIsseData": "20180501",
    "HoldrAcct": "AAA",
    "HoldrCmID": "AID",
    "WaitEndorseCmID": "BID",
    "WaitEndroseAcct": "BBB",
    "docType": "billObj"
};
key: POC004, value: {
    "BillDueDate": "20180508",
    "BillInfoAmt": "1000",
    "BillInfoID": "POC004",
    "BillInfoType": "111",
    "BillIsseData": "20180501",
    "HoldrAcct": "CCC",
    "HoldrCmID": "CID",
    "WaitEndorseCmID": "BID",
    "WaitEndroseAcct": "BBB",
    "docType": "billObj"
}

另外關(guān)于LevelDB,CouchDB還是MongoDB,今后可能隨著Hyperledger Fabric的版本變化而采取不同的數(shù)據(jù)庫(kù)類型,我們拭目以待,現(xiàn)在唯一能做的,就是在已有的資源下面用Hyperledger Fabric為業(yè)務(wù)場(chǎng)景創(chuàng)造最大的業(yè)務(wù)價(jià)值。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI