Hyperledger Fabric网络环境手动配置及其链码自动化部署

Hyperledger Fabric网络环境手动配置及其链码自动化部署,第1张

目录

5.1 网络环境的搭建

5.1.1 生成组织结构与身份z书

5.1.2 生成创世区块和通道

5.1.3 启动Fabric网络

5.1.4  创建Fabric-SDK-GO对象并建立通道

5.1.5  Fabric-SDK-Go实现链码的自动部署

5.2 链码实现


5.1 网络环境的搭建 5.1.1 生成组织结构与身份z书

Hyperledger Fabric框架通过采用大量的证书确保系统交易(签名块、TSL、身份验证等)期间所有数据的安全性。证书主要由框架工具cryptogen生成,并且,保存至项目文件的crypto-config目录下

Hyperledger Fabric网络搭建的第一步就是生成指定的成员,用来参与交易。组织中的成员提供相应的节点服务,而身份z书则代表节点的真实性,通过身份z书在Fabric网络各个节点之间进行交易以及交易时进行的签名与验证,相关的生成主要依赖项目目录下的crypto-config.yaml配置文件。

      crypto-config.yaml文件内容如下:

OrdererOrgs:

  - Name: Orderer

    Domain: aaron.com

    Specs:

      - Hostname: orderer



PeerOrgs:

  - Name: Org1

    Domain: org1.aaron.com

    EnableNodeOUs: true

    Template:

      Count: 2

    Users:

      Count: 1



  - Name: Org2

    Domain: org2.aaron.com

    EnableNodeOUs: true

    Template:

      Count: 2

    Users:

      Count: 1

该配置文件定义了OrdererOrgs和PeerOrgs两种组织相关信息。PeerOrgs组织下所属Org1一个组织。根据配置信息Template.Count定义每个组织下peer节点个数为2,而根据配置信息Users.Count定义每个Peer节点中仅创建一个用户。Peer节点的完整域名主要由peer+peer编号+组织域名组成,该配置文件中的Peer节点为peer0.org1.aaron.com和peer1.org1.aaron.com。

保存crypto-config.yaml配置文件后,使用Hyperledger Fabric提供的工具cryptogen,来根据特定的配置文件自动化生成标准的组织结构和身份z书。

cryptogen工具提供的常用字命令与参数信息如下:

①子命令:

A. generate:生成组织结构和身份z书信息。

B. version:显示版本信息。

C.showtemplate:输出默认配置信息模板。

②参数

A. --config:指定所使用crypto-config.yaml配置文件

B. --output:指定输出内容的目录

步骤完整的 *** 作如下图:

 图5.1 完整 *** 作图

通过以上命令生成的MSP材料(密钥和证书)将被输出至项目目录的crypto-config文件夹中,并且,根据crypto-config.yaml配置文件的配置信息生成两个子目录,分别是ordererOrganizations和peerOrganizations,前者包括Orderer组织的身份信息,后者包括网络中所有的Peer节点的身份信息。MSP目录代表了该实体的身份信息,是目录中最重要的部分。

通过tree命令可以查看crypto-config目录结构如下:

crypto-config

├── ordererOrganizations

│   └── aaron.com

│       ├── ca

│       │   ├── b60224b47c7c76b29000a5138b6dbafc61e6e5b7bd018ed98c81df9a60900e39_sk

│       │   └── ca.aaron.com-cert.pem

│       ├── msp

│       │   ├── admincerts

│       │   │   └── Admin@aaron.com-cert.pem

│       │   ├── cacerts

│       │   │   └── ca.aaron.com-cert.pem

│       │   └── tlscacerts

│       │       └── tlsca.aaron.com-cert.pem

│       ├── orderers

│       │   └── orderer.aaron.com

│       │       ├── msp

│       │       │   ├── admincerts

│       │       │   │   └── Admin@aaron.com-cert.pem

│       │       │   ├── cacerts

│       │       │   │   └── ca.aaron.com-cert.pem

│       │       │   ├── keystore

│       │       │   │   └── 068c2b914f7e8f9e85df1977d9039f4b67bf97bf4b60c3f54c3ceef4991ecfb2_sk

│       │       │   ├── signcerts

│       │       │   │   └── orderer.aaron.com-cert.pem

│       │       │   └── tlscacerts

│       │       │       └── tlsca.aaron.com-cert.pem

│       │       └── tls

│       │           ├── ca.crt

│       │           ├── server.crt

│       │           └── server.key

│       ├── tlsca

│       │   ├── 17b93561bb6d56227e85b11450723d4a095fce2f63401b19342359a3bc334817_sk

│       │   └── tlsca.aaron.com-cert.pem

│       └── users

│           └── Admin@aaron.com

│               ├── msp

│               │   ├── admincerts

│               │   │   └── Admin@aaron.com-cert.pem

│               │   ├── cacerts

│               │   │   └── ca.aaron.com-cert.pem

│               │   ├── keystore

│               │   │   └── 388e3c80fbf06d272ef848335773c7acc807c30db4c193632fe3f46ca66d74c6_sk

│               │   ├── signcerts

│               │   │   └── Admin@aaron.com-cert.pem

│               │   └── tlscacerts

│               │       └── tlsca.aaron.com-cert.pem

│               └── tls

│                   ├── ca.crt

│                   ├── client.crt

│                   └── client.key

└── peerOrganizations

    ├── org1.aaron.com

    │   ├── ca

    │   │   ├── 859e0970b0cb1078ade8b2f9220a60d74f655b2523f2d32acf072bc54a313c3b_sk

    │   │   └── ca.org1.aaron.com-cert.pem

    │   ├── msp

    │   │   ├── admincerts

    │   │   │   └── Admin@org1.aaron.com-cert.pem

    │   │   ├── cacerts

    │   │   │   └── ca.org1.aaron.com-cert.pem

    │   │   ├── config.yaml

    │   │   └── tlscacerts

    │   │       └── tlsca.org1.aaron.com-cert.pem

    │   ├── peers

    │   │   ├── peer0.org1.aaron.com

    │   │   │   ├── msp

    │   │   │   │   ├── admincerts

    │   │   │   │   │   └── Admin@org1.aaron.com-cert.pem

    │   │   │   │   ├── cacerts

    │   │   │   │   │   └── ca.org1.aaron.com-cert.pem

    │   │   │   │   ├── config.yaml

    │   │   │   │   ├── keystore

    │   │   │   │   │   └── d6f30d9214aac125822a3c28de7a2652dbd8fd9dd4e6971735d98e4e090af8f4_sk

    │   │   │   │   ├── signcerts

    │   │   │   │   │   └── peer0.org1.aaron.com-cert.pem

    │   │   │   │   └── tlscacerts

    │   │   │   │       └── tlsca.org1.aaron.com-cert.pem

    │   │   │   └── tls

    │   │   │       ├── ca.crt

    │   │   │       ├── server.crt

    │   │   │       └── server.key

    │   │   └── peer1.org1.aaron.com

    │   │       ├── msp

    │   │       │   ├── admincerts

    │   │       │   │   └── Admin@org1.aaron.com-cert.pem

    │   │       │   ├── cacerts

    │   │       │   │   └── ca.org1.aaron.com-cert.pem

    │   │       │   ├── config.yaml

    │   │       │   ├── keystore

    │   │       │   │   └── cf88ad4a2eb4cc70aaa3f7f567f10d2dccc8bc1d049d4673ec34f0fbd764e28d_sk

    │   │       │   ├── signcerts

    │   │       │   │   └── peer1.org1.aaron.com-cert.pem

    │   │       │   └── tlscacerts

    │   │       │       └── tlsca.org1.aaron.com-cert.pem

    │   │       └── tls

    │   │           ├── ca.crt

    │   │           ├── server.crt

    │   │           └── server.key

    │   ├── tlsca

    │   │   ├── cc9efd80166e62a30b1eccba106d3d44c521f162907bf7f67841b3d4e5e7d763_sk

    │   │   └── tlsca.org1.aaron.com-cert.pem

    │   └── users

    │       ├── Admin@org1.aaron.com

    │       │   ├── msp

    │       │   │   ├── admincerts

    │       │   │   │   └── Admin@org1.aaron.com-cert.pem

    │       │   │   ├── cacerts

    │       │   │   │   └── ca.org1.aaron.com-cert.pem

    │       │   │   ├── keystore

    │       │   │   │   └── 9c0e9ba10e1ad0410cc1e67b36f7e1ef923a68802972e57427376a485fd830cf_sk

    │       │   │   ├── signcerts

    │       │   │   │   └── Admin@org1.aaron.com-cert.pem

    │       │   │   └── tlscacerts

    │       │   │       └── tlsca.org1.aaron.com-cert.pem

    │       │   └── tls

    │       │       ├── ca.crt

    │       │       ├── client.crt

    │       │       └── client.key

    │       └── User1@org1.aaron.com

    │           ├── msp

    │           │   ├── admincerts

    │           │   │   └── User1@org1.aaron.com-cert.pem

    │           │   ├── cacerts

    │           │   │   └── ca.org1.aaron.com-cert.pem

    │           │   ├── keystore

    │           │   │   └── 911f2c101599dd25d1717bf55ca16012a5d20d04e3dcdba5402c4a2e7cc8665e_sk

    │           │   ├── signcerts

    │           │   │   └── User1@org1.aaron.com-cert.pem

    │           │   └── tlscacerts

    │           │       └── tlsca.org1.aaron.com-cert.pem

    │           └── tls

    │               ├── ca.crt

    │               ├── client.crt

    │               └── client.key

    └── org2.aaron.com

        ├── ca

        │   ├── 72a7572fe3d7d024580c4abebd79bb87b42fbfcf497973b424d87e88227fd204_sk

        │   └── ca.org2.aaron.com-cert.pem

        ├── msp

        │   ├── admincerts

        │   │   └── Admin@org2.aaron.com-cert.pem

        │   ├── cacerts

        │   │   └── ca.org2.aaron.com-cert.pem

        │   ├── config.yaml

        │   └── tlscacerts

        │       └── tlsca.org2.aaron.com-cert.pem

        ├── peers

        │   ├── peer0.org2.aaron.com

        │   │   ├── msp

        │   │   │   ├── admincerts

        │   │   │   │   └── Admin@org2.aaron.com-cert.pem

        │   │   │   ├── cacerts

        │   │   │   │   └── ca.org2.aaron.com-cert.pem

        │   │   │   ├── config.yaml

        │   │   │   ├── keystore

        │   │   │   │   └── 966a5bcd21a0280c53917c380fdd0133ba7ce0ca5b8c27f82fa299b5bb94dafa_sk

        │   │   │   ├── signcerts

        │   │   │   │   └── peer0.org2.aaron.com-cert.pem

        │   │   │   └── tlscacerts

        │   │   │       └── tlsca.org2.aaron.com-cert.pem

        │   │   └── tls

        │   │       ├── ca.crt

        │   │       ├── server.crt

        │   │       └── server.key

        │   └── peer1.org2.aaron.com

        │       ├── msp

        │       │   ├── admincerts

        │       │   │   └── Admin@org2.aaron.com-cert.pem

        │       │   ├── cacerts

        │       │   │   └── ca.org2.aaron.com-cert.pem

        │       │   ├── config.yaml

        │       │   ├── keystore

        │       │   │   └── f6d47950fcc967a7e641a3c596fe69017b685c01a7d0de28e5f95c3d10b39ecd_sk

        │       │   ├── signcerts

        │       │   │   └── peer1.org2.aaron.com-cert.pem

        │       │   └── tlscacerts

        │       │       └── tlsca.org2.aaron.com-cert.pem

        │       └── tls

        │           ├── ca.crt

        │           ├── server.crt

        │           └── server.key

        ├── tlsca

        │   ├── e5cca08947c712744d49d9e91dfd6f9c844ebc3c548e2a348719e80e8ad9932f_sk

        │   └── tlsca.org2.aaron.com-cert.pem

        └── users

            ├── Admin@org2.aaron.com

            │   ├── msp

            │   │   ├── admincerts

            │   │   │   └── Admin@org2.aaron.com-cert.pem

            │   │   ├── cacerts

            │   │   │   └── ca.org2.aaron.com-cert.pem

            │   │   ├── keystore

            │   │   │   └── 1d6f2d3c325252daa57b5cb6d7f94edc9c0694e415cfaf7618f8ec61965bcb62_sk

            │   │   ├── signcerts

            │   │   │   └── Admin@org2.aaron.com-cert.pem

            │   │   └── tlscacerts

            │   │       └── tlsca.org2.aaron.com-cert.pem

            │   └── tls

            │       ├── ca.crt

            │       ├── client.crt

            │       └── client.key

            └── User1@org2.aaron.com

                ├── msp

                │   ├── admincerts

                │   │   └── User1@org2.aaron.com-cert.pem

                │   ├── cacerts

                │   │   └── ca.org2.aaron.com-cert.pem

                │   ├── keystore

                │   │   └── 61037cd0124692d99aff5a9da85193807298fdea8f3e414fa27381ef51c2f3d5_sk

                │   ├── signcerts

                │   │   └── User1@org2.aaron.com-cert.pem

                │   └── tlscacerts

                │       └── tlsca.org2.aaron.com-cert.pem

                └── tls

                    ├── ca.crt

                    ├── client.crt

                    └── client.key









109 directories, 107 files

5.1.2 生成创世区块和通道

5.1.1节生成了组织结构和身份z书,此外,需要为区块链生成一个创世区块GenesisBlock和通道Channel,相关的配置信息由configtx.yaml文件定义。该文件的配置内容主要包括Organizations、Capabilities、Application、Orderer、Profiles等部分。

configtx.yaml文件内容如下:

# Copyright IBM Corp. All Rights Reserved.

#

# SPDX-License-Identifier: Apache-2.0

#



---

################################################################################

#

#   Section: Organizations

#

#   - This section defines the different organizational identities which will

#   be referenced later in the configuration.

#

################################################################################

Organizations:



    # SampleOrg defines an MSP using the sampleconfig.  It should never be used

    # in production but may be used as a template for other definitions

    - &OrdererOrg

        # DefaultOrg defines the organization which is used in the sampleconfig

        # of the fabric.git development environment

        Name: OrdererOrg



        # ID to load the MSP definition as

        ID: OrdererMSP



        # MSPDir is the filesystem path which contains the MSP configuration

        MSPDir: crypto-config/ordererOrganizations/aaron.com/msp



    - &Org1

        # DefaultOrg defines the organization which is used in the sampleconfig

        # of the fabric.git development environment

        Name: Org1MSP



        # ID to load the MSP definition as

        ID: Org1MSP



        MSPDir: crypto-config/peerOrganizations/org1.aaron.com/msp



        AnchorPeers:

            - Host: peer0.org1.aaron.com

              Port: 7051



################################################################################

#

#   SECTION: Application

#

#   - This section defines the values to encode into a config transaction or

#   genesis block for application related parameters

#

################################################################################

Application: &ApplicationDefaults



    # Organizations is the list of orgs which are defined as participants on

    # the application side of the network

    Organizations:



################################################################################

#

#   SECTION: Orderer

#

#   - This section defines the values to encode into a config transaction or

#   genesis block for orderer related parameters

#

################################################################################

Orderer: &OrdererDefaults



    # Orderer Type: The orderer implementation to start

    # Available types are "solo" and "kafka"

    OrdererType: solo



    Addresses:

        - orderer.aaron.com:7050



    # Batch Timeout: The amount of time to wait before creating a batch

    BatchTimeout: 2s



    # Batch Size: Controls the number of messages batched into a block

    BatchSize:



        MaxMessageCount: 10



        AbsoluteMaxBytes: 99 MB



        PreferredMaxBytes: 512 KB



    Kafka:

        # Brokers: A list of Kafka brokers to which the orderer connects

        # NOTE: Use IP:port notation

        Brokers:

            - 127.0.0.1:9092

    # Organizations is the list of orgs which are defined as participants on

    # the orderer side of the network

    Organizations:



################################################################################

#

#   Profile

#

#   - Different configuration profiles may be encoded here to be specified

#   as parameters to the configtxgen tool

#

################################################################################

Profiles:



    OneOrgOrdererGenesis:

        Orderer:

            <<: *OrdererDefaults

            Organizations:

                - *OrdererOrg

        Consortiums:

            SampleConsortium:

                Organizations:

                    - *Org1

    OneOrgChannel:

        Consortium: SampleConsortium

        Application:

            <<: *ApplicationDefaults

            Organizations:

                - *Org1

在该配置文件中,Organizations部分定义了两个组织成员OrdererOrg和Org1,并且,分别为每个组织定义了组织ID、组织Name、MSP目录,MSP目录为指定组织的根证书在Orderer Genesis块中的存储位置,任何网络实体都可以与Orderer服务通信并验证每个实体的数字签名;AnchorPeers部分定义了组织的锚节点为

peer0.org1.aaron.com。

创建完configtx.yaml配置文件后,使用Hyperledger Fabric工具configtxgen来创建Orderer服务启动初始化以及通道。

configtxgen工具提供的常用参数信息如下:

①-asOrg:指定特定的组织,执行配置的生成,主要包括组织有权设置的写集中的值。

②-channelID:在服务中指定的通道ID,默认情况下为“testchainid”。

③-inspectBlock:在指定路径下输出区块的配置信息。

④-inspectChannelCreateTx:在指定路径下输出交易中包含的配置信息。

⑤-outputBlock:创世区块文件保存的位置信息

⑥-outputCreateChannelTx:通道配置信息文件输出的位置信息

⑦-profile:根据configtx.yaml配置文件中的Profiles配置,生成指定的创世区块或通道文件。

⑧-version:显示版本信息。

掌握了configtxgen的相关指令后,进入network目录,使用configtx.yaml文件中定义的OneOrgOrdererGenesis模板,生成Orderer服务系统通道的创世区块文件,该步骤的实际 *** 作如下图:


5.2  完整 *** 作图 

方便对项目文件的管理和查看,将所有生成的文件都保存在artifacts目录下。

生成创世区块后,开始进行通道的创建环节。通道主要是为了更好的保护Hyperledger Fabric网络中的隐私数据,是一种通过数据隔离来实现类似于“子网“的机制,只有已经加入的组织成员才有权访问该通道。首先,将通道名称写入环境变量,方便后续进行通道的相关 *** 作;其次,根据configtx.yaml配置文件中的OneOrgChannel模板信息生成通道的交易配置文件;最后,根据configtx.yaml配置文件中的AnchorPeers部分指定生成组织的锚节点,用来实现同一通道中不同组织之间的通信。

该步骤完整的 *** 作如下。

① Orderer服务启动创世区块的创建:

 图5.3  完整 *** 作图

② 生成应用通道的交易配置文件:

5.4  完整 *** 作图

③ 生成锚节点更新配置文件:

5.5  完整 *** 作图

以上三步执行完毕后,在artifacts目录下会得到3个生成的文件,分别是channel.tx、genesis.block、Org1MSPanchors.tx。

如需查看生成文件的详细内容,可以通过inspectBlock、inspectChannelCreateTx两个相关参数查看相应的配置内容。

5.1.3 启动Fabric网络

Hyperledger Fabric网络启动之前所需的所有配置信息均已配置完成,并且生成创世区块和通道文件,该节需要对网络服务进行相应的配置,配置各个网络服务的节点,采用Hyperledger Fabric提供的容器技术,通过docker-compose工具进行节点容器管理。需要区块链开发者编写docker-compose.yml配置文件

Compose 文件是一个 YML 文件,用于定义 Docker 应用程序的服务、网络和卷。

该配置文件的完整内容如下:

version: '2'



networks:

  default:



services:



  orderer.aaron.com:

    image: hyperledger/fabric-orderer

    container_name: orderer.aaron.com

    environment:

      - ORDERER_GENERAL_LOGLEVEL=debug

      - ORDERER_GENERAL_LISTENADDRESS=0.0.0.0

      - ORDERER_GENERAL_LISTENPORT=7050

      - ORDERER_GENERAL_GENESISPROFILE=aaron

      - ORDERER_GENERAL_GENESISMETHOD=file

      - ORDERER_GENERAL_GENESISFILE=/var/hyperledger/orderer/genesis.block

      - ORDERER_GENERAL_LOCALMSPID=aaron.com

      - ORDERER_GENERAL_LOCALMSPDIR=/var/hyperledger/orderer/msp

      - ORDERER_GENERAL_TLS_ENABLED=true

      - ORDERER_GENERAL_TLS_PRIVATEKEY=/var/hyperledger/orderer/tls/server.key

      - ORDERER_GENERAL_TLS_CERTIFICATE=/var/hyperledger/orderer/tls/server.crt

      - ORDERER_GENERAL_TLS_ROOTCAS=[/var/hyperledger/orderer/tls/ca.crt]

      - GODEBUG=netdns=go

    working_dir: /opt/gopath/src/github.com/hyperledger/fabric

    command: orderer

    volumes:

      - ./artifacts/genesis.block:/var/hyperledger/orderer/genesis.block

      - ./crypto-config/ordererOrganizations/aaron.com/orderers/orderer.aaron.com/msp:/var/hyperledger/orderer/msp

      - ./crypto-config/ordererOrganizations/aaron.com/orderers/orderer.aaron.com/tls:/var/hyperledger/orderer/tls

    ports:

      - 7050:7050

    networks:

      default:

        aliases:

          - orderer.aaron.com



  ca.org1.aaron.com:

    image: hyperledger/fabric-ca

    container_name: ca.org1.aaron.com

    environment:

      - FABRIC_CA_HOME=/etc/hyperledger/fabric-ca-server

      - FABRIC_CA_SERVER_CA_NAME=ca.org1.aaron.com

      - FABRIC_CA_SERVER_CA_CERTFILE=/etc/hyperledger/fabric-ca-server-config/ca.org1.aaron.com-cert.pem

      - FABRIC_CA_SERVER_CA_KEYFILE=/etc/hyperledger/fabric-ca-server-config/b94191a52feb6e2f99fef0480b419a6c8a88fdc2385409cff808f0721c17b348_sk

      - FABRIC_CA_SERVER_TLS_ENABLED=true

      - FABRIC_CA_SERVER_TLS_CERTFILE=/etc/hyperledger/fabric-ca-server-config/ca.org1.aaron.com-cert.pem

      - FABRIC_CA_SERVER_TLS_KEYFILE=/etc/hyperledger/fabric-ca-server-config/b94191a52feb6e2f99fef0480b419a6c8a88fdc2385409cff808f0721c17b348_sk

      - GODEBUG=netdns=go

    ports:

      - 7054:7054

    command: sh -c 'fabric-ca-server start -b admin:adminpw -d'

    volumes:

      - ./crypto-config/peerOrganizations/org1.aaron.com/ca/:/etc/hyperledger/fabric-ca-server-config

    networks:

      default:

        aliases:

          - ca.org1.aaron.com



  peer0.org1.aaron.com:

    image: hyperledger/fabric-peer

    container_name: peer0.org1.aaron.com

    environment:

      - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock

      - CORE_VM_DOCKER_ATTACHSTDOUT=true

      - CORE_LOGGING_LEVEL=DEBUG

      - CORE_PEER_NETWORKID=aaron

      - CORE_PEER_PROFILE_ENABLED=true

      - CORE_PEER_TLS_ENABLED=true

      - CORE_PEER_TLS_CERT_FILE=/var/hyperledger/tls/server.crt

      - CORE_PEER_TLS_KEY_FILE=/var/hyperledger/tls/server.key

      - CORE_PEER_TLS_ROOTCERT_FILE=/var/hyperledger/tls/ca.crt

      - CORE_PEER_ID=peer0.org1.aaron.com

      - CORE_PEER_ADDRESSAUTODETECT=true

      - CORE_PEER_ADDRESS=peer0.org1.aaron.com:7051

      - CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer0.org1.aaron.com:7051

      - CORE_PEER_GOSSIP_USELEADERELECTION=true

      - CORE_PEER_GOSSIP_ORGLEADER=false

      - CORE_PEER_GOSSIP_SKIPHANDSHAKE=true

      - CORE_PEER_LOCALMSPID=org1.aaron.com

      - CORE_PEER_MSPCONFIGPATH=/var/hyperledger/msp

      - CORE_PEER_TLS_SERVERHOSTOVERRIDE=peer0.org1.aaron.com

        #      - CORE_LEDGER_STATE_STATEDATABASE=CouchDB

        # - CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb:5984

        # - CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=

        #- CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=

      - GODEBUG=netdns=go

    working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer

    command: peer node start

    volumes:

      - /var/run/:/host/var/run/

      - ./crypto-config/peerOrganizations/org1.aaron.com/peers/peer0.org1.aaron.com/msp:/var/hyperledger/msp

      - ./crypto-config/peerOrganizations/org1.aaron.com/peers/peer0.org1.aaron.com/tls:/var/hyperledger/tls

    ports:

      - 7051:7051

      - 7053:7053

    depends_on:

      - orderer.aaron.com

    networks:

      default:

        aliases:

          - peer0.org1.aaron.com



  peer1.org1.aaron.com:

    image: hyperledger/fabric-peer

    container_name: peer1.org1.aaron.com

    environment:

      - CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock

      - CORE_VM_DOCKER_ATTACHSTDOUT=true

      - CORE_LOGGING_LEVEL=DEBUG

      - CORE_PEER_NETWORKID=aaron

      - CORE_PEER_PROFILE_ENABLED=true

      - CORE_PEER_TLS_ENABLED=true

      - CORE_PEER_TLS_CERT_FILE=/var/hyperledger/tls/server.crt

      - CORE_PEER_TLS_KEY_FILE=/var/hyperledger/tls/server.key

      - CORE_PEER_TLS_ROOTCERT_FILE=/var/hyperledger/tls/ca.crt

      - CORE_PEER_ID=peer1.org1.aaron.com

      - CORE_PEER_ADDRESSAUTODETECT=true

      - CORE_PEER_ADDRESS=peer1.org1.aaron.com:7051

      - CORE_PEER_GOSSIP_EXTERNALENDPOINT=peer1.org1.aaron.com:7051

      - CORE_PEER_GOSSIP_USELEADERELECTION=true

      - CORE_PEER_GOSSIP_ORGLEADER=false

      - CORE_PEER_GOSSIP_SKIPHANDSHAKE=true

      - CORE_PEER_LOCALMSPID=org1.aaron.com

      - CORE_PEER_MSPCONFIGPATH=/var/hyperledger/msp

      - CORE_PEER_TLS_SERVERHOSTOVERRIDE=peer1.org1.aaron.com

        # - CORE_LEDGER_STATE_STATEDATABASE=CouchDB

        #  - CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb:5984

        # - CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=

        # - CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=

      - GODEBUG=netdns=go

    working_dir: /opt/gopath/src/github.com/hyperledger/fabric/peer

    command: peer node start

    volumes:

      - /var/run/:/host/var/run/

      - ./crypto-config/peerOrganizations/org1.aaron.com/peers/peer1.org1.aaron.com/msp:/var/hyperledger/msp

      - ./crypto-config/peerOrganizations/org1.aaron.com/peers/peer1.org1.aaron.com/tls:/var/hyperledger/tls

    ports:

      - 7151:7051

      - 7153:7053

    depends_on:

      - orderer.aaron.com

    networks:

      default:

        aliases:

          - peer1.org1.aaron.com

docker-compose.yml配置文件定义了Orderer节点的端口号、MSP信息、监听地址、初始区块和主机与Docker的映射信息;并且定义了CA目录、服务器的名称、密钥名称以及路径、证书名称以及路径等信息;最后,定义了Peer节点的端口号、监听地址、MSP信息、证书名称及其路径和和主机与Docker的映射信息。

保存该配置文件后,进入network文件夹,通过docker-compose up命令读取network目录下docker-commpose.yml配置文件,根据配置信息启动网络,如不需要输出日志信息,可以通过添加-d参数。在新的终端窗口输入docker ps命令查看容器,可以看到2个peer节点、一个CA节点和一个Orderer节点,并且可以看到相应的端口信息,代表成功启动了网络。当不需要网络时,通过docker-compose down命令结束网络。至此,网络环境搭建成功。

该步骤完整的 *** 作如下:

 5.6  完整 *** 作图

5.1.4  创建Fabric-SDK-GO对象并建立通道

       Hyperledger Fabric支持很多语言的SDK,如Java、Node、Python等,本系统采用Golang语言来设计并实现应用程序,包含智能合约部分,也就是Fabric框架内的链码。由于篇幅限制,本文不再讲解其他语言的SDK实现,具体的学习步骤可以去Fabric官方提供的在线文档进行学习。

       上一节,成功搭建了Hyperledger Fabric的基础网络环境,接下来,创建一个config.yaml配置文件,配置Fabric-SDK-Go的相关参数,指定Fabric各个组件的通信地址。

在项目的根目录aaron下,创建config.yaml配置文件,该文件的详细内容如下:

# Copyright IBM Corp. All Rights Reserved.

#

# SPDX-License-Identifier: Apache-2.0

#



---

################################################################################

#

#   Section: Organizations

#

#   - This section defines the different organizational identities which will

#   be referenced later in the configuration.

#

################################################################################

Organizations:



    # SampleOrg defines an MSP using the sampleconfig.  It should never be used

    # in production but may be used as a template for other definitions

    - &OrdererOrg

        # DefaultOrg defines the organization which is used in the sampleconfig

        # of the fabric.git development environment

        Name: OrdererOrg



        # ID to load the MSP definition as

        ID: aaron.com



        # MSPDir is the filesystem path which contains the MSP configuration

        MSPDir: crypto-config/ordererOrganizations/aaron.com/msp



    - &Org1

        # DefaultOrg defines the organization which is used in the sampleconfig

        # of the fabric.git development environment

        Name: Org1MSP



        # ID to load the MSP definition as

        ID: org1.aaron.com



        MSPDir: crypto-config/peerOrganizations/org1.aaron.com/msp



        AnchorPeers:

            # AnchorPeers defines the location of peers which can be used

            # for cross org gossip communication.  Note, this value is only

            # encoded in the genesis block in the Application section context

            - Host: peer0.org1.aaron.com

              Port: 7051



################################################################################

#

#   SECTION: Application

#

#   - This section defines the values to encode into a config transaction or

#   genesis block for application related parameters

#

################################################################################

Application: &ApplicationDefaults



    # Organizations is the list of orgs which are defined as participants on

    # the application side of the network

    Organizations:



################################################################################

#

#   SECTION: Orderer

#

#   - This section defines the values to encode into a config transaction or

#   genesis block for orderer related parameters

#

################################################################################

Orderer: &OrdererDefaults



    # Orderer Type: The orderer implementation to start

    # Available types are "solo" and "kafka"

    OrdererType: solo



    Addresses:

        - orderer.aaron.com:7050



    # Batch Timeout: The amount of time to wait before creating a batch

    BatchTimeout: 2s



    # Batch Size: Controls the number of messages batched into a block

    BatchSize:



        # Max Message Count: The maximum number of messages to permit in a batch

        MaxMessageCount: 10



        # Absolute Max Bytes: The absolute maximum number of bytes allowed for

        # the serialized messages in a batch.

        AbsoluteMaxBytes: 99 MB



        # Preferred Max Bytes: The preferred maximum number of bytes allowed for

        # the serialized messages in a batch. A message larger than the preferred

        # max bytes will result in a batch larger than preferred max bytes.

        PreferredMaxBytes: 512 KB



    Kafka:

        # Brokers: A list of Kafka brokers to which the orderer connects

        # NOTE: Use IP:port notation

        Brokers:

            - 127.0.0.1:9092



    # Organizations is the list of orgs which are defined as participants on

    # the orderer side of the network

    Organizations:



################################################################################

#

#   Profile

#

#   - Different configuration profiles may be encoded here to be specified

#   as parameters to the configtxgen tool

#

################################################################################

Profiles:



    OneOrgOrdererGenesis:

        Orderer:

            <<: *OrdererDefaults

            Organizations:

                - *OrdererOrg

        Consortiums:

            SampleConsortium:

                Organizations:

                    - *Org1

    OneOrgChannel:

        Consortium: SampleConsortium

        Application:

            <<: *ApplicationDefaults

            Organizations:

                - *Org1

保存config.yaml配置文件后,在项目目录aaron下新建sdkInit目录,用来创建SDK和应用通道,并且,方便对后续链码的自动部署,减少了复杂的链码配置步骤。进入sdkInit目录,在目录下创建fabricInitInfo.go文件,配置Fabric-SDK的相关信息,定义一个InitInfo结构体,通过实例化该结构体,可以采用Fabric-SDK-GO提供的相关API,创建通道,将定义的Peer节点加入应用通道中,安装并实例化链码,创建客户端实例等相关 *** 作。

fabricInitInfo.go的完整内容如下:

/**

  author: aaron

 */



package sdkInit



import (

      "github.com/hyperledger/fabric-sdk-go/pkg/client/resmgmt"

)



type InitInfo struct {

      ChannelID     string

      ChannelConfig string

      OrgAdmin      string

      OrgName       string

      OrdererOrgName       string

      OrgResMgmt *resmgmt.Client



      ChaincodeID       string

      ChaincodeGoPath      string

      ChaincodePath    string

      UserName    string

}

该结构体的各个成员定义如下:

① ChannelID:通道名称。

② ChannelConfig:通道配置文件的路径。

③ OrgAdmin:组织管理员的名称。

④ OrgName:组织名称。

⑤ OrdererOrgName:组织的Orderer节点名称。

⑥ OrgResMgmt:客户端资源管理器实例。

⑦ ChaincodeID:链码名称。

⑧ ChaincodeGoPath: *** 作系统GOPATH路径。

⑨ ChaincodePath:链码文件的路径。

⑩ UserName:组织的用户名称。

保存fabricInitInfo.go文件后,继续在sdkInit目录下,创建start.go文件,用于创建SDK以及通道,并且将Peer节点加入到通道中。

start.go的完整内容如下:

/**

  author: aaron

 */



package sdkInit



import (

      "github.com/hyperledger/fabric-sdk-go/pkg/fabsdk"

      "github.com/hyperledger/fabric-sdk-go/pkg/core/config"

      "fmt"

      "github.com/hyperledger/fabric-sdk-go/pkg/client/resmgmt"

      mspclient "github.com/hyperledger/fabric-sdk-go/pkg/client/msp"

      "github.com/hyperledger/fabric-sdk-go/pkg/common/providers/msp"

      "github.com/hyperledger/fabric-sdk-go/pkg/common/errors/retry"

   

      "github.com/hyperledger/fabric-sdk-go/pkg/fab/ccpackager/gopackager"

      "github.com/hyperledger/fabric-sdk-go/third_party/github.com/hyperledger/fabric/common/cauthdsl"

      "github.com/hyperledger/fabric-sdk-go/pkg/client/channel"



)





const ChaincodeVersion  = "1.0"





func SetupSDK(ConfigFile string, initialized bool) (*fabsdk.FabricSDK, error) {



      if initialized {

             return nil, fmt.Errorf("Fabric SDK已被实例化")

      }



      sdk, err := fabsdk.New(config.FromFile(ConfigFile))

      if err != nil {

             return nil, fmt.Errorf("实例化Fabric SDK失败: %v", err)

      }



      fmt.Println("Fabric SDK初始化成功")

      return sdk, nil

}



// 创建通道并将指定的Peers加入

func CreateChannel(sdk *fabsdk.FabricSDK, info *InitInfo) error {



      clientContext := sdk.Context(fabsdk.WithUser(info.OrgAdmin), fabsdk.WithOrg(info.OrgName))

      if clientContext == nil {

             return fmt.Errorf("根据指定的组织名称与管理员创建资源管理客户端Context失败")

      }



      // New returns a resource management client instance.

      resMgmtClient, err := resmgmt.New(clientContext)

      if err != nil {

             return fmt.Errorf("根据指定的资源管理客户端Context创建通道管理客户端失败: %v", err)

      }



      // New creates a new Client instance

      mspClient, err := mspclient.New(sdk.Context(), mspclient.WithOrg(info.OrgName))

      if err != nil {

             return fmt.Errorf("根据指定的 OrgName 创建 Org MSP 客户端实例失败: %v", err)

      }



      //  Returns: signing identity

      adminIdentity, err := mspClient.GetSigningIdentity(info.OrgAdmin)

      if err != nil {

             return fmt.Errorf("获取指定id的签名标识失败: %v", err)

      }



      // SaveChannelRequest holds parameters for save channel request

      channelReq := resmgmt.SaveChannelRequest{ChannelID:info.ChannelID, ChannelConfigPath:info.ChannelConfig, SigningIdentities:[]msp.SigningIdentity{adminIdentity}}

      // save channel response with transaction ID

       _, err = resMgmtClient.SaveChannel(channelReq, resmgmt.WithRetry(retry.DefaultResMgmtOpts), resmgmt.WithOrdererEndpoint(info.OrdererOrgName))

      if err != nil {

             return fmt.Errorf("创建应用通道失败: %v", err)

      }



      fmt.Println("通道已成功创建,")



      info.OrgResMgmt = resMgmtClient



      // allows for peers to join existing channel with optional custom options (specific peers, filtered peers). If peer(s) are not specified in options it will default to all peers that belong to client's MSP.

      err = info.OrgResMgmt.JoinChannel(info.ChannelID, resmgmt.WithRetry(retry.DefaultResMgmtOpts), resmgmt.WithOrdererEndpoint(info.OrdererOrgName))

      if err != nil {

             return fmt.Errorf("Peers加入通道失败: %v", err)

      }



      fmt.Println("peers 已成功加入通道.")

      return nil

}



func InstallAndInstantiateCC(sdk *fabsdk.FabricSDK, info *InitInfo) (*channel.Client, error) {

      fmt.Println("开始安装链码......")

      // creates new go lang chaincode package

      ccPkg, err := gopackager.NewCCPackage(info.ChaincodePath, info.ChaincodeGoPath)

      if err != nil {

             return nil, fmt.Errorf("创建链码包失败: %v", err)

      }



      // contains install chaincode request parameters

      installCCReq := resmgmt.InstallCCRequest{Name: info.ChaincodeID, Path: info.ChaincodePath, Version: ChaincodeVersion, Package: ccPkg}

      // allows administrators to install chaincode onto the filesystem of a peer

      _, err = info.OrgResMgmt.InstallCC(installCCReq, resmgmt.WithRetry(retry.DefaultResMgmtOpts))

      if err != nil {

             return nil, fmt.Errorf("安装链码失败: %v", err)

      }



      fmt.Println("指定的链码安装成功")

      fmt.Println("开始实例化链码......")



      //  returns a policy that requires one valid

      ccPolicy := cauthdsl.SignedByAnyMember([]string{"org1.aaron.com"})



      instantiateCCReq := resmgmt.InstantiateCCRequest{Name: info.ChaincodeID, Path: info.ChaincodePath, Version: ChaincodeVersion, Args: [][]byte{[]byte("init")}, Policy: ccPolicy}

      // instantiates chaincode with optional custom options (specific peers, filtered peers, timeout). If peer(s) are not specified

      _, err = info.OrgResMgmt.InstantiateCC(info.ChannelID, instantiateCCReq, resmgmt.WithRetry(retry.DefaultResMgmtOpts))

      if err != nil {

             return nil, fmt.Errorf("实例化链码失败: %v", err)

      }



      fmt.Println("链码实例化成功")



      clientChannelContext := sdk.ChannelContext(info.ChannelID, fabsdk.WithUser(info.UserName), fabsdk.WithOrg(info.OrgName))

      // returns a Client instance. Channel client can query chaincode, execute chaincode and register/unregister for chaincode events on specific channel.

      channelClient, err := channel.New(clientChannelContext)

      if err != nil {

             return nil, fmt.Errorf("创建应用通道客户端失败: %v", err)

      }



      fmt.Println("通道客户端创建成功,可以利用此客户端调用链码进行查询或执行事务.")



      return channelClient, nil

}

保存start.go文件后,启动Fabric网络,在项目根目录aaron下,新建main.go文件,main.go的完整内容如下:

/**

  author: aaron

 */

package main



import (

      "os"

      "fmt"

      "github.com/kongyixueyuan.com/kongyixueyuan/sdkInit"

      "github.com/kongyixueyuan.com/kongyixueyuan/service"

)



const (

      configFile = "config.yaml"

      initialized = false

      SimpleCC = "simplecc"//chaincode目录下

)



func main() {



      initInfo := &sdkInit.InitInfo{



             ChannelID: "mychannel",

             ChannelConfig: os.Getenv("GOPATH") + "/src/github.com/aaron/network/artifacts/channel.tx",



             OrgAdmin:"Admin",

             OrgName:"Org1",

             OrdererOrgName: "orderer.aaron.com",

             ChaincodeID: SimpleCC,

             ChaincodeGoPath: os.Getenv("GOPATH"),

             ChaincodePath: "github.com/aaron/chaincode/",

             UserName:"User1",

      }



      sdk, err := sdkInit.SetupSDK(configFile, initialized)

      if err != nil {

             fmt.Printf(err.Error())

             return

      }

      defer sdk.Close()



      err = sdkInit.CreateChannel(sdk, initInfo)

      if err != nil {

             fmt.Println(err.Error())

             return

      }



}

保存main.go文件后,需要根据项目要求安装所需的依赖,使用dep工具,在项目根目录aaron下,创建Gopkg.toml配置文件,将指定SDK的版本下载至vendor目录下。

配置好项目所需的依赖后,进行Fabric-SDK-Go的测试,打开终端1,进入network目录下,通过docker-compose up命令启动Fabric网络并输出日志,打开终端2,进行项目根目录aaron,首先通过docker ps命令查看是否成功启动容器,然后通过go build命令编译代码,通过./aaron命令运行。由于上述启动及关闭Fabric网络并且清理Fabric网络环境需要执行很多复杂的命令,因此,本系统介绍采用编写Makefile文件,该文件是一个Linux系统下的Shell脚本,实现项目开发的自动化,提高开发效率,通过make工具实现简化 *** 作步骤,有关make工具的安装本文不再介绍,可以在网上查阅相关文档进行安装。在项目根目录aaron创建Makefile文件,文件中的详细内容如下:

.PHONY: all dev clean build env-up env-down run

all: clean build env-up run

dev: build run



##### BUILD

build:

      @echo "Build ..."

#    @dep ensure

      @go build

      @echo "Build done"



##### ENV

env-up:

      @echo "Start environment ..."

      @cd network && docker-compose up --force-recreate -d

      @echo "Environment up"



env-down:

      @echo "Stop environment ..."

      @cd network && docker-compose down

      @echo "Environment down"



##### RUN

run:

      @echo "Start app ..."

      @./aaron



##### CLEAN

clean: env-down

      @echo "Clean up ..."

      @rm -rf /tmp/aaron-* aaron

      @docker rm -f -v `docker ps -a --no-trunc | grep "aaron" | cut -d ' ' -f 1` 2>/dev/null || true

      @docker rmi `docker images --no-trunc | grep "aaron" | cut -d ' ' -f 1` 2>/dev/null || true

      @echo "Clean up done"

该Makefile文件中的dep ensure命令因个人网络问题,可以采用其他方式下载所需依赖包,故可以注释掉。

完整的 *** 作步骤如下: 

5.7  完整 *** 作图

5.1.5  Fabric-SDK-Go实现链码的自动部署

在5.1.4节,成功创建了start.go文件,并且实现了InstallAndInstantiateCC函数,该函数主要实现了三个功能,分别是链码的自动安装,将链码实例化,并且创建项目的客户端实例,通过SDK提供的相关API实现了对链码的自动安装以及实例化,不需要在终端不断地输入命令来编译、运行和配置链码。

继续对5.1.4节的main.go文件进行修改,调用InstallAndInstantiateCC函数,添加的部分内容如下:

      channelClient, err := sdkInit.InstallAndInstantiateCC(sdk, initInfo)



      if err != nil {

             fmt.Println(err.Error())



             return

      }



      fmt.Println(channelClient)

通过Makefile的完整 *** 作如下:

 5.8  完整 *** 作图

至此,Hyperledger Fabric的网络环境已经搭建完成,通过Fabric-SDK-Go创建了应用通道,peer节点也可以成功加入通道中,并且实现了链码的自动化部署,对接下来链码的编写,业务层的设计打好了基础。

5.2 链码实现

考虑到基于区块链的系统的后期的可拓展性及维护性,将所有客户的请求都通过业务层,调用链码执行相应的功能,从而实现对Fabric账本状态的查询和修改等 *** 作。

在项目根目录aaron下,新建service文件夹,作为存储业务层相关的代码的目录,使用Fabric-SDK-Go提供的API,实现相应的接口对象,从而调用链码实现访问,最后,对分类账本进行相应的 *** 作。

首先,在service文件夹下新建domain.go文件,该文件主要声明ServiceSetup结构体和系统所需的结构体,这里作为测试声明User、Role和Permission结构体,用于定义链码ID、客户端等配置信息。定义registerEvent和eventResult函数,前者用于注册调用链码功能的相关事件处理,后者用于根据指定的事件ID,接受相应的链码事件,从而返回事件的结果。

domain.go文件的完整内容如下:

/**

  @Author : aaron

*/

package service



import (

      "github.com/hyperledger/fabric-sdk-go/pkg/client/channel"

      "fmt"

      "time"

      "github.com/hyperledger/fabric-sdk-go/pkg/common/providers/fab"

)



/**

姓名:刘子豪,性别:男,



身份z号:13242423434353512312322



专业:软件工程



用户类别:学生



用户编号:12345678



访问设备MAC地址:B8-97-5A-75-A9-91



 */

type User struct {

       ObjectType   string     `json:"docType"`

       Name     string     `json:"Name"`            // 姓名

       Gender  string     `json:"Gender"`          // 性别

       EntityID       string     `json:"EntityID"`        // 身份z号

       Major     string     `json:"Major"`     // 专业

       UserType      string     `json:"UserType"`      // 用户类别

       UserNo  string     `json:"UserNo"`         //用户编号

       DeviceMAC string     `json:"DeviceMAC"` //访问设备MAC地址

}



type Role struct {

       RoleID string `json:"RoleID"`

       RoleName string `json:"RoleName"`

}



type Menu struct {

       MenuID string `json:"MenuID"`

       MenuName string `json:"MenuName"`

       MenuURL string `json:"MenuURL"`

       ParentMenuID string `json:"ParentMenuID"`

}



type RoleAuthorization struct {

       UserID string  `json:"UserID"`

       RoleID string `json:"RoleID"`

}



type PermissionAuthorization struct {

       RoleID string `json:"RoleID"`

        MenuID string `json:"MenuID"`

}



type Historys struct {

       HistoryID string `json:"HistoryID"`

       History HistoryItem

       SecurityLevel string `json:"SecurityLevel"`

}



type HistoryItem struct {

       ActionID string

       ActionType string

       ActionSatus string

}

type ServiceSetup struct {

      ChaincodeID       string

      Client     *channel.Client

}



func regitserEvent(client *channel.Client, chaincodeID, eventID string) (fab.Registration, <-chan *fab.CCEvent) {



      reg, notifier, err := client.RegisterChaincodeEvent(chaincodeID, eventID)

      if err != nil {

             fmt.Println("注册链码事件失败: %s", err)

      }

      return reg, notifier

}



func eventResult(notifier <-chan *fab.CCEvent, eventID string) error {

      select {

      case ccEvent := <-notifier:

             fmt.Printf("接收到链码事件: %v\n", ccEvent)

      case <-time.After(time.Second * 20):

             return fmt.Errorf("不能根据指定的事件ID接收到相应的链码事件(%s)", eventID)

      }

      return nil

}

保存domain.go文件后,就可以编写服务的调用代码。新建SimpleService.go文件,该文件主要定义一些项目对Fabric账本 *** 作的函数:

① SaveUser(user User)(string, error):实现向Fabric账本添加状态。

② FindUserInfoByEntityID(entityID string)([]byte, error):实现通过用户实体ID查询User状态信息的功能。

③FindUserByUserNameAndUserType(userName, userType string) ([]byte, error):实现通过用户姓名和用户类型查询User状态信息的功能。

④ ModifyUser(user User)(string, error):实现对已存入账本的状态进行修改的功能。

以上链码部分实现User信息的上链,对于Role、Permission和日志信息的上链,采用Hyperledger Fabric多链多通道的方式实现,不同的链将通道节点与数据进行隔离,不同的Peer节点加入不同的通道,产生多个不同的链,提高了隐私数据的安全性,并且,提高了该系统对数据的并行处理效率,提高了对系统数据存储账本空间的利用率。

在项目根目录aaron下编写main函数,通过对服务的调用,执行一些账本的 *** 作事务,最后,通过make命令查看执行的过程,可以看到成功发布信息,并且,查询到UserNo为2018215064的用户详细信息,该步骤的完整 *** 作展示如下图:

5.9  完整 *** 作图

 

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/1498363.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-25
下一篇 2022-06-25

发表评论

登录后才能评论

评论列表(0条)

保存