区块链发票系统(后端)

背景描述

税收是国家的财政来源,制造假发票,虚开发票影响国家税收。为了提高发票透明度,市税务局请你开发一套区块链税务系统进行试点。发票的整个开具、使用、核心

流程包括:

  1. 税务局首先登记有资格认领发票的试点企业。经过登记的企业采用有资格认领区块链发票。

  2. 税务局将试点发票登记进入区块链发票池,发票池中是合法的区块链发票,没有经过登记进入发票池发票均为无效发票。

  3. 试点企业认领区块链发票,税务局登记企业认领了哪些发票。

  4. 试点企业根据实际发生的业务,将认领的发票开具给他人,企业只能开具自己认领的发票,不得使用其他企业认领的发票。

  5. 收到发票的企业可以核销抵税。

限制要求:为了提高系统可信度,由市场监督管理局代表企业运营 2 个节点,一个处于企业的上链业务,另一个节点向公众提供发票查询服务;财政局作为监督节点,不参与业务。

分析

  1. Order节点:由市税务局、市场监督管理局和财政局共同管理的区块链税务系统需要至少1个Order节点来管理交易的顺序和一致性。
  2. Peer节点:考虑到区块链系统的可扩展性和容错性,建议至少有3个Peer节点。这些节点可以分别由市税务局、市场监督管理局和财政局控制。
  3. 组织分配:
    • 市税务局:作为整个系统的发起者和主要管理者,可以担任一个Peer节点,并负责区块链发票的登记、发行和核查等操作。
    • 市场监督管理局:作为系统可信度的代表,该机构可以担任一个Peer节点,负责对企业的上链业务进行监督和审核,并向公众提供发票查询服务。
    • 财政局:作为监督节点,该机构不参与具体的业务操作。可以担任第三个Peer节点,负责监督和验证系统中的交易和数据。
  4. 部门对应组织:
    • 税务部门:属于市税务局,负责发票登记、发行和核查。
    • 市场监管部门:属于市场监督管理局,负责对企业上链业务进行监督和审核。
    • 财政部门:属于财政局,负责监督和验证系统中的交易和数据。

结论

财政局: orderer.zxy.com 一个order节点

市税务局: peer.chen.com 一个peer节点

市场监察局: peer.zhou.com 两个peer节点

需要实现功能

1、税务局将发票放入发票池

2、添加试点企业

3、将发票分配给指定的企业

4、开发票

5、查询发票

注:因实际情况,具体的实现逻辑有所不同,现在为最初版本,能实现基本功能但是逻辑不严谨,后续改进

搭建

网络环境

crypto-config.yaml,搭建好网络拓扑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 排序节点的组织定义
OrdererOrgs:
- Name: ZXY # 名称
Domain: zxy.com # 域名
Specs: # 节点域名:orderer.carunion.com
- Hostname: orderer # 主机名

# peer节点的组织定义
PeerOrgs:
# CHEN-组织
- Name: CHEN # 名称
Domain: chen.com # 域名
Template: # 使用模板定义。Count 指的是该组织下组织节点的个数
Count: 1 # 节点域名:peer0.chen.com 和 peer1.chen.com
Users: # 组织的用户信息。Count 指该组织中除了 Admin 之外的用户的个数
Count: 1 # 用户:Admin 和 User1

# ZHOU-组织
- Name: ZHOU
Domain: zhou.com
Template:
Count: 2 # 节点域名:peer0.zhou.com 和 peer1.zhou.com
Users:
Count: 1 # 用户:Admin 和 User1

configtx.yaml,网络的基本配置,用来生成创世块和channel通道

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 定义组织机构实体
Organizations:
- &ZXY
Name: ZXY # 组织的名称
ID: ZXYMSP # 组织的 MSPID
MSPDir: crypto-config/ordererOrganizations/zxy.com/msp #组织的证书相对位置(生成的crypto-config目录)

- &CHEN
Name: CHEN
ID: CHENMSP
MSPDir: crypto-config/peerOrganizations/chen.com/msp
AnchorPeers: # 组织锚节点的配置
- Host: peer0.chen.com
Port: 7051

- &ZHOU
Name: ZHOU
ID: ZHOUMSP
MSPDir: crypto-config/peerOrganizations/zhou.com/msp
AnchorPeers: # 组织锚节点的配置
- Host: peer0.zhou.com
Port: 7051

# 定义了排序服务的相关参数,这些参数将用于创建创世区块
Orderer: &OrdererDefaults
# 排序节点类型用来指定要启用的排序节点实现,不同的实现对应不同的共识算法
OrdererType: solo # 共识机制
Addresses: # Orderer 的域名(用于连接)
- orderer.zxy.com:7050
BatchTimeout: 2s # 出块时间间隔
BatchSize: # 用于控制每个block的信息量
MaxMessageCount: 10 #每个区块的消息个数
AbsoluteMaxBytes: 99 MB #每个区块最大的信息大小
PreferredMaxBytes: 512 KB #每个区块包含的一条信息最大长度
Organizations:

# 定义Peer组织如何与应用程序通道交互的策略
# 默认策略:所有Peer组织都将能够读取数据并将数据写入账本
Application: &ApplicationDefaults
Organizations:

# 用来定义用于 configtxgen 工具的配置入口
# 将 Profile 参数( TwoOrgsOrdererGenesis 或 TwoOrgsChannel )指定为 configtxgen 工具的参数
Profiles:
# TwoOrgsOrdererGenesis配置文件用于创建系统通道创世块
# 该配置文件创建一个名为SampleConsortium的联盟
# 该联盟在configtx.yaml文件中包含两个Peer组织CHEN和ZHOU
TwoOrgsOrdererGenesis:
Orderer:
<<: *OrdererDefaults
Organizations:
- *ZXY
Consortiums:
SampleConsortium:
Organizations:
- *CHEN
- *ZHOU
# 使用TwoOrgsChannel配置文件创建应用程序通道
TwoOrgsChannel:
Consortium: SampleConsortium
Application:
<<: *ApplicationDefaults
Organizations:
- *CHEN
- *ZHOU

docker-compose.yaml,使用docker容器,启动网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
version: '2.1'

volumes:
orderer.zxy.com:
peer0.chen.com:
peer1.chen.com:
peer0.zhou.com:
peer1.zhou.com:

networks:
fabric_network:
name: fabric_network

services:
# 排序服务节点
orderer.zxy.com:
container_name: orderer.zxy.com
image: hyperledger/fabric-orderer:1.4.12
environment:
- GODEBUG=netdns=go
- ORDERER_GENERAL_LISTENADDRESS=0.0.0.0
- ORDERER_GENERAL_GENESISMETHOD=file
- ORDERER_GENERAL_GENESISFILE=/etc/hyperledger/config/genesis.block # 注入创世区块
- ORDERER_GENERAL_LOCALMSPID=ZXYMSP
- ORDERER_GENERAL_LOCALMSPDIR=/etc/hyperledger/orderer/msp # 证书相关
command: orderer
ports:
- "7050:7050"
volumes: # 挂载由cryptogen和configtxgen生成的证书文件以及创世区块
- ./config/genesis.block:/etc/hyperledger/config/genesis.block
- ./crypto-config/ordererOrganizations/zxy.com/orderers/orderer.zxy.com/:/etc/hyperledger/orderer
- orderer.zxy.com:/var/hyperledger/production/orderer
networks:
- fabric_network

# CHEN 组织 peer0 节点
peer0.chen.com:
extends:
file: docker-compose-base.yaml
service: peer-base
container_name: peer0.chen.com
environment:
- CORE_PEER_ID=peer0.chen.com
- CORE_PEER_LOCALMSPID=CHENMSP
- CORE_PEER_ADDRESS=peer0.chen.com:7051
- CORE_LEDGER_STATE_STATEDATABASE=CouchDB
- CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb:5984
- CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=admin
- CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=123456
ports:
- "7051:7051" # grpc服务端口
- "7053:7053" # eventhub端口
volumes:
- ./crypto-config/peerOrganizations/chen.com/peers/peer0.chen.com:/etc/hyperledger/peer
- peer0.chen.com:/var/hyperledger/production
depends_on:
- orderer.zxy.com

# CHEN 组织 peer1 节点
peer1.chen.com:
extends:
file: docker-compose-base.yaml
service: peer-base
container_name: peer1.chen.com
environment:
- CORE_PEER_ID=peer1.chen.com
- CORE_PEER_LOCALMSPID=CHENMSP
- CORE_PEER_ADDRESS=peer1.chen.com:7051
- CORE_LEDGER_STATE_STATEDATABASE=CouchDB
- CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb:5984
- CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=admin
- CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=123456
ports:
- "17051:7051"
- "17053:7053"
volumes:
- ./crypto-config/peerOrganizations/chen.com/peers/peer1.chen.com:/etc/hyperledger/peer
- peer1.chen.com:/var/hyperledger/production
depends_on:
- orderer.zxy.com

# ZHOU 组织 peer0 节点
peer0.zhou.com:
extends:
file: docker-compose-base.yaml
service: peer-base
container_name: peer0.zhou.com
environment:
- CORE_PEER_ID=peer0.zhou.com
- CORE_PEER_LOCALMSPID=ZHOUMSP
- CORE_PEER_ADDRESS=peer0.zhou.com:7051
- CORE_LEDGER_STATE_STATEDATABASE=CouchDB
- CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb:5984
- CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=admin
- CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=123456
ports:
- "27051:7051"
- "27053:7053"
volumes:
- ./crypto-config/peerOrganizations/zhou.com/peers/peer0.zhou.com:/etc/hyperledger/peer
- peer0.zhou.com:/var/hyperledger/production
depends_on:
- orderer.zxy.com

# ZHOU 组织 peer1 节点
peer1.zhou.com:
extends:
file: docker-compose-base.yaml
service: peer-base
container_name: peer1.zhou.com
environment:
- CORE_PEER_ID=peer1.zhou.com
- CORE_PEER_LOCALMSPID=ZHOUMSP
- CORE_PEER_ADDRESS=peer1.zhou.com:7051
- CORE_LEDGER_STATE_STATEDATABASE=CouchDB
- CORE_LEDGER_STATE_COUCHDBCONFIG_COUCHDBADDRESS=couchdb:5984
- CORE_LEDGER_STATE_COUCHDBCONFIG_USERNAME=admin
- CORE_LEDGER_STATE_COUCHDBCONFIG_PASSWORD=123456
ports:
- "37051:7051"
- "37053:7053"
volumes:
- ./crypto-config/peerOrganizations/zhou.com/peers/peer1.zhou.com:/etc/hyperledger/peer
- peer1.zhou.com:/var/hyperledger/production
depends_on:
- orderer.zxy.com

# 客户端节点
cli:
container_name: cli
image: hyperledger/fabric-tools:1.4.12
tty: true
environment:
# go 环境设置
- GO111MODULE=auto
- GOPROXY=https://goproxy.cn
- CORE_PEER_ID=cli
command: /bin/bash
volumes:
- ./config:/etc/hyperledger/config
- ./crypto-config/peerOrganizations/chen.com/:/etc/hyperledger/peer/chen.com
- ./crypto-config/peerOrganizations/zhou.com/:/etc/hyperledger/peer/zhou.com
- ./../chaincode:/opt/gopath/src/chaincode # 链码路径注入
networks:
- fabric_network
depends_on:
- orderer.zxy.com
- peer0.chen.com
- peer1.chen.com
- peer0.zhou.com
- peer1.zhou.com

couchdb:
container_name: couchdb
image: hyperledger/fabric-couchdb:latest
# 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=admin
- COUCHDB_PASSWORD=123456
# 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:
- fabric_network

docker-compose-base.yaml,共同的网络配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
version: '2.1'

services:
peer-base: # peer的公共服务
image: hyperledger/fabric-peer:1.4.12
environment:
- GODEBUG=netdns=go
- CORE_VM_ENDPOINT=unix:///host/var/run/docker.sock
- CORE_LOGGING_PEER=info
- CORE_CHAINCODE_LOGGING_LEVEL=INFO
- CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/msp # msp证书(节点证书)
# - CORE_LEDGER_STATE_STATEDATABASE=CouchDB # 状态数据库的存储引擎(or CouchDB)
- CORE_VM_DOCKER_HOSTCONFIG_NETWORKMODE=fabric_network # docker 网络
volumes:
- /var/run/docker.sock:/host/var/run/docker.sock
working_dir: /opt/gopath/src/github.com/hyperledger/fabric
command: peer node start
networks:
- fabric_network

编写智能合约和应用

1、编写一个工具类,用于封装对数据的增删查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
package utils

import (
"encoding/json"
"errors"
"fmt"

"github.com/hyperledger/fabric/core/chaincode/shim"
)

// WriteLedger 写入账本
func WriteLedger(obj interface{}, stub shim.ChaincodeStubInterface, objectType string, keys []string) error {
//创建复合主键
var key string
if val, err := stub.CreateCompositeKey(objectType, keys); err != nil {
return errors.New(fmt.Sprintf("%s-创建复合主键出错 %s", objectType, err))
} else {
key = val
}
bytes, err := json.Marshal(obj)
if err != nil {
return errors.New(fmt.Sprintf("%s-序列化json数据失败出错: %s", objectType, err))
}
//写入区块链账本
if err := stub.PutState(key, bytes); err != nil {
return errors.New(fmt.Sprintf("%s-写入区块链账本出错: %s", objectType, err))
}
return nil
}

// DelLedger 删除账本
func DelLedger(stub shim.ChaincodeStubInterface, objectType string, keys []string) error {
//创建复合主键
var key string
if val, err := stub.CreateCompositeKey(objectType, keys); err != nil {
return errors.New(fmt.Sprintf("%s-创建复合主键出错 %s", objectType, err))
} else {
key = val
}
//写入区块链账本
if err := stub.DelState(key); err != nil {
return errors.New(fmt.Sprintf("%s-删除区块链账本出错: %s", objectType, err))
}
return nil
}

// GetStateByPartialCompositeKeys 根据复合主键查询数据(适合获取全部,多个,单个数据)
// 将keys拆分查询
func GetStateByPartialCompositeKeys(stub shim.ChaincodeStubInterface, objectType string, keys []string) (results [][]byte, err error) {
if len(keys) == 0 {
// 传入的keys长度为0,则查找并返回所有数据
// 通过主键从区块链查找相关的数据,相当于对主键的模糊查询
resultIterator, err := stub.GetStateByPartialCompositeKey(objectType, keys)
if err != nil {
return nil, errors.New(fmt.Sprintf("%s-获取全部数据出错: %s", objectType, err))
}
defer resultIterator.Close()

//检查返回的数据是否为空,不为空则遍历数据,否则返回空数组
for resultIterator.HasNext() {
val, err := resultIterator.Next()
if err != nil {
return nil, errors.New(fmt.Sprintf("%s-返回的数据出错: %s", objectType, err))
}

results = append(results, val.GetValue())
}
} else {
// 传入的keys长度不为0,查找相应的数据并返回
for _, v := range keys {
// 创建组合键
key, err := stub.CreateCompositeKey(objectType, []string{v})
if err != nil {
return nil, errors.New(fmt.Sprintf("%s-创建组合键出错: %s", objectType, err))
}
// 从账本中获取数据
bytes, err := stub.GetState(key)
if err != nil {
return nil, errors.New(fmt.Sprintf("%s-获取数据出错: %s", objectType, err))
}

if bytes != nil {
results = append(results, bytes)
}
}
}

return results, nil
}

// GetStateByPartialCompositeKeys2 根据复合主键查询数据(适合获取全部或指定的数据)
func GetStateByPartialCompositeKeys2(stub shim.ChaincodeStubInterface, objectType string, keys []string) (results [][]byte, err error) {
// 通过主键从区块链查找相关的数据,相当于对主键的模糊查询
resultIterator, err := stub.GetStateByPartialCompositeKey(objectType, keys)
if err != nil {
return nil, errors.New(fmt.Sprintf("%s-获取全部数据出错: %s", objectType, err))
}
defer resultIterator.Close()

//检查返回的数据是否为空,不为空则遍历数据,否则返回空数组
for resultIterator.HasNext() {
val, err := resultIterator.Next()
if err != nil {
return nil, errors.New(fmt.Sprintf("%s-返回的数据出错: %s", objectType, err))
}

results = append(results, val.GetValue())
}
return results, nil
}

2、根据需求定义好结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package model


// Invoice 发票结构体
type Invoice struct {
InvoiceId string `json:"invoiceId"`
Owner string `json:"owner"` // 客户,默认是nil
Pool string `json:"pool"` // unuse代表入未使用,use表示已使用
Value string `json:"value"` // 默认为0
Companies string `json:"companies"` // 持有发票的公司
IssuedTo string `json:"issuedTo"` // 是否已开发票,yes为开,no为未开
}

// Company 试点企业结构体
type Company struct {
// CompanyId string `json:"companyId"`
Name string `json:"name"`
}


const (
AccountKey = "account-key"
InvoiceKey = "invoice-key"
CompanyKey = "company-key"
)

3、编写合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
package api

import (
"chaincode/model"
"chaincode/pkg/utils"

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


// 查询发票
func GetInvoice(stub shim.ChaincodeStubInterface, args []string) pb.Response {
var invoiceList []model.Invoice
// 验证参数
if len(args) != 1 {
return shim.Error("Insufficient number of parameters")
}

invoiceId := args[0]
if invoiceId == "" {
return shim.Error("A null value exists for the parameter")
}

results, err := utils.GetStateByPartialCompositeKeys2(stub, model.InvoiceKey, args)
if err != nil {
return shim.Error(fmt.Sprintf("%s",err))
}
for _, v := range results {
if v != nil {
var invoice model.Invoice
err := json.Unmarshal(v, &invoice)
if err != nil {
return shim.Error(fmt.Sprintf("GetInvoice-An error occurred in deserialization: %s", err))
}
invoiceList = append(invoiceList, invoice)
}
}
invoiceListByte, err := json.Marshal(invoiceList)
if err != nil {
return shim.Error(fmt.Sprintf("GetInvoice-There was an error in serialization: %s", err))
}

return shim.Success(invoiceListByte)

}

// 添加试点企业
func RegisterCompany(stub shim.ChaincodeStubInterface, args []string) pb.Response {
// 由税务局完成,先判断操作人是否为税务局(暂时未实现)
if(len(args) != 1) {
return shim.Error("Insufficient number of parameters")
}
name := args[0]
if name == "" {
return shim.Error("A null value exists for the parameter")
}
// 查重
resultsCompany, err := utils.GetStateByPartialCompositeKeys(stub, model.CompanyKey, []string{name})
if err != nil || len(resultsCompany) != 0 {
return shim.Error(fmt.Sprintf("There was an accident:%s", err))
}
// 写入
companyInfo := &model.Company{
Name: name,
}
if err := utils.WriteLedger(companyInfo, stub, model.CompanyKey, []string{name}); err != nil {
return shim.Error(fmt.Sprintf("An error occurred in writing to the account---RegisterCompany:%s", err))
}

companyInfoByte, err := json.Marshal(companyInfo)
if err != nil {
return shim.Error(fmt.Sprintf("There was an error creating serialization---RegisterCompany: %s", err))
}
return shim.Success(companyInfoByte)
}

// 将发票放入发票池并给指定企业
func PrepareInvoice (stub shim.ChaincodeStubInterface, args []string) pb.Response {
// 首先,发票入池操作只可以由税务局完成,所以我们要先判断发起操作的是否是税务局
// 其次,将发票放入发票池,只需要指定已存在的企业即可,其他值默认
// 所以,需要传入两个值,一个是税务局身份标识,另一个是企业名字
// 注:发票肯定能入池,如果未入池就报错,所以不存在未入池的发票,所以无需判断发票是否入池
if len(args) != 2 {
return shim.Error("Insufficient number of parameters")
}
taxName := args[0]
companyName := args[1]
// 判断tax
if taxName != "Tax" {
return shim.Error("You have no authority")
}
invoiceInfo := &model.Invoice{
InvoiceId: stub.GetTxID()[:6],
Owner: "null", // 客户,默认是null
Pool: "unuse", // unuse代表入未使用,use表示已使用
Value: "0", // 默认为0
Companies: companyName, // 持有发票的公司
IssuedTo: "no", // 是否已开发票,yes为开,no为未开
}
if err := utils.WriteLedger(invoiceInfo, stub, model.InvoiceKey, []string{invoiceInfo.InvoiceId,invoiceInfo.Companies}); err != nil {
return shim.Error(fmt.Sprintf("An error occurred in writing to the account---RegisterCompany:%s", err))
}
invoiceInfoByte, err := json.Marshal(invoiceInfo)
if err != nil {
return shim.Error(fmt.Sprintf("There was an error creating serialization---RegisterCompany: %s", err))
}
return shim.Success(invoiceInfoByte)
}

// 开发票
func IssueTos (stub shim.ChaincodeStubInterface, args []string) pb.Response {
//所使用的发票必须是分配给自己的
// 需要提供公司名称以及客户标识、金额才可以开发票
if len(args) != 6 {
return shim.Error("Insufficient number of parameters")
}
// 正常情况下,每个企业只能看到自己具有的发票,不存在开出去的发票是别的企业的,所以先不做判断,默认情况正常
companyName := args[0]
value := args[1]
owner := args[2]
invoiceId := args[3]
pool := args[4]
issuedTo := args[5]

if (companyName == "" || value == "" || owner == "" || invoiceId == "" || pool == "" || issuedTo == "") {
return shim.Error("A null value exists")
}
if pool == "use" {
return shim.Error("The pool is already in use")
}
if issuedTo == "yes" {
return shim.Error("The issuedTo is already in use")
}
issuedToList := &model.Invoice{
InvoiceId: invoiceId,
Owner: owner,
Pool: "use",
Value: value,
Companies: companyName,
IssuedTo: "yes",
}
//清除原来的发票信息
if err := utils.DelLedger(stub, model.InvoiceKey, []string{issuedToList.InvoiceId, issuedToList.Companies}); err != nil {
return shim.Error(fmt.Sprintf("delete Failure:%s", err))
}
// 写入账本
if err := utils.WriteLedger(issuedToList, stub, model.InvoiceKey, []string{issuedToList.InvoiceId,issuedToList.Companies}); err != nil {
return shim.Error(fmt.Sprintf("An error occurred in writing to the account---IssueTos:%s", err))
}
issuedToListByte, err := json.Marshal(issuedToList)
if err != nil {
return shim.Error(fmt.Sprintf("There was an error creating serialization---IssueTos: %s", err))
}
return shim.Success(issuedToListByte)
}

// login
func Login(stub shim.ChaincodeStubInterface, args []string) pb.Response {
var companyList []model.Company
// 验证参数
if len(args) != 1 {
return shim.Error("Insufficient number of parameters")
}
companyName := args[0]
if (companyName == "") {
return shim.Error("A null value exists")
}
resultsCompany, err := utils.GetStateByPartialCompositeKeys(stub, model.CompanyKey, []string{companyName})
if err != nil {
return shim.Error(fmt.Sprintf("%s",err))
}
for _, v := range resultsCompany {
if v != nil {
var company model.Company
err := json.Unmarshal(v, &company)
if err != nil {
return shim.Error(fmt.Sprintf("GetInvoice-An error occurred in deserialization: %s", err))
}
companyList = append(companyList, company)
}
}
companyListByte, err := json.Marshal(companyList)
if err != nil {
return shim.Error(fmt.Sprintf("GetInvoice-There was an error in serialization: %s", err))
}
return shim.Success(companyListByte)
}

4、合约的初始化及调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package main

import (
"chaincode/api"
"fmt"
"time"
"chaincode/model"
"chaincode/pkg/utils"

"github.com/hyperledger/fabric/core/chaincode/shim"
pb "github.com/hyperledger/fabric/protos/peer"
)

type BlockChainRealEstate struct {
}

// Init 链码初始化
func (t *BlockChainRealEstate) Init(stub shim.ChaincodeStubInterface) pb.Response {
fmt.Println("链码初始化")
//初始化默认数据
var tax = [1]string {"Tax"}
company := &model.Company{
Name: tax[0],
}
// 写入账本
if err := utils.WriteLedger(company, stub, model.CompanyKey, []string{company.Name}); err != nil {
return shim.Error(fmt.Sprintf("%s", err))
}
// 为了方便测试,现在这里创建一个company
var companyName = "Tencent"
companyTencent := &model.Company{
Name: companyName,
}
if err := utils.WriteLedger(companyTencent, stub, model.CompanyKey, []string{companyTencent.Name}); err != nil {
return shim.Error(fmt.Sprintf("%s", err))
}
// 测试

return shim.Success(nil)
}

// Invoke 实现Invoke接口调用智能合约
func (t *BlockChainRealEstate) Invoke(stub shim.ChaincodeStubInterface) pb.Response {
funcName, args := stub.GetFunctionAndParameters()
switch funcName {
case "hello":
return api.Hello(stub, args);
case "getInvoice":
return api.GetInvoice(stub, args);
case "registerCompany":
return api.RegisterCompany(stub, args);
case "prepareInvoice":
return api.PrepareInvoice(stub, args);
case "issueTos":
return api.IssueTos(stub, args);
case "login":
return api.Login(stub, args);
default:
return shim.Error(fmt.Sprintf("No such feature: %s", funcName));
}
}

func main() {
timeLocal, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
panic(err)
}
time.Local = timeLocal
err = shim.Start(new(BlockChainRealEstate))
if err != nil {
fmt.Printf("Error starting Simple chaincode: %s", err)
}
}

5、统一接口应用返回参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package app

import (
"github.com/gin-gonic/gin"
)

type Gin struct {
C *gin.Context
}

type Response struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data interface{} `json:"data"`
}

func (g *Gin) Response(httpCode int, errMsg string, data interface{}) {
g.C.JSON(httpCode, Response{
Code: httpCode,
Msg: errMsg,
Data: data,
})
return
}

6、fabric-go-sdk的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package blockchain

import (
"github.com/hyperledger/fabric-sdk-go/pkg/client/channel"
"github.com/hyperledger/fabric-sdk-go/pkg/core/config"
"github.com/hyperledger/fabric-sdk-go/pkg/fabsdk"
)

// 配置信息
var (
sdk *fabsdk.FabricSDK // Fabric SDK
configPath = "config.yaml" // 配置文件路径
channelName = "appchannel" // 通道名称
user = "Admin" // 用户
chainCodeName = "fabric-realty" // 链码名称
endpoints = []string{"peer0.zhou.com", "peer0.chen.com"} // 要发送交易的节点
)

// Init 初始化
func Init() {
var err error
// 通过配置文件初始化SDK
sdk, err = fabsdk.New(config.FromFile(configPath))
if err != nil {
panic(err)
}
}

// ChannelExecute 区块链交互
func ChannelExecute(fcn string, args [][]byte) (channel.Response, error) {
// 创建客户端,表明在通道的身份
ctx := sdk.ChannelContext(channelName, fabsdk.WithUser(user))
cli, err := channel.New(ctx)
if err != nil {
return channel.Response{}, err
}
// 对区块链账本的写操作(调用了链码的invoke)
resp, err := cli.Execute(channel.Request{
ChaincodeID: chainCodeName,
Fcn: fcn,
Args: args,
}, channel.WithTargetEndpoints(endpoints...))
if err != nil {
return channel.Response{}, err
}
//返回链码执行后的结果
return resp, nil
}

// ChannelQuery 区块链查询
func ChannelQuery(fcn string, args [][]byte) (channel.Response, error) {
// 创建客户端,表明在通道的身份
ctx := sdk.ChannelContext(channelName, fabsdk.WithUser(user))
cli, err := channel.New(ctx)
if err != nil {
return channel.Response{}, err
}
// 对区块链账本查询的操作(调用了链码的invoke),只返回结果
resp, err := cli.Query(channel.Request{
ChaincodeID: chainCodeName,
Fcn: fcn,
Args: args,
}, channel.WithTargetEndpoints(endpoints...))
if err != nil {
return channel.Response{}, err
}
//返回链码执行后的结果
return resp, nil
}

7、编写应用接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
package v1

import (
"application/pkg/app"
bc "application/blockchain"
"net/http"
"bytes"
"encoding/json"
"fmt"

"github.com/gin-gonic/gin"
)

// 查找发票
type InvoiceIdBody struct {
InvoiceId string `json:"invoiceId"`
}

type InvoiceRequestBody struct {
Args []InvoiceIdBody `json:"args"`
}

// 添加试点企业
type CompanyBody struct {
Name string `json:"name"`
}

// 将发票放入发票池并给指定企业
type InvoiceBody struct {
TaxName string `json:"taxName"`
Companies string `json:"companyName"`
}

// 开发票
type IssueToBody struct {
InvoiceId string `json:"invoiceId"`
Owner string `json:"owner"`
Pool string `json:"pool"` // unuse代表入未使用,use表示已使用
Value string `json:"value"`
Companies string `json:"companies"`
IssuedTo string `json:"issuedTo"`
}
// 查找
func GetInvoice(c *gin.Context) {
appG := app.Gin{C: c}
body := new(InvoiceRequestBody)
// 解析body参数
if err := c.ShouldBind(body); err != nil {
appG.Response(http.StatusBadRequest, "Failure", fmt.Sprintf("Coarse-grained operation parameters:%s", err.Error()))
return
}
var bodyBytes [][]byte
for _, val := range body.Args {
bodyBytes = append(bodyBytes, []byte(val.InvoiceId))
}
//调用智能合约
resp, err := bc.ChannelQuery("getInvoice", bodyBytes)
if err != nil {
appG.Response(http.StatusInternalServerError, "Failure", err.Error())
return
}
// 反序列化json
var data []map[string]interface{}
if err = json.Unmarshal(bytes.NewBuffer(resp.Payload).Bytes(), &data); err != nil {
appG.Response(http.StatusInternalServerError, "Failure", err.Error())
return
}
appG.Response(http.StatusOK, "Success", data)
}

// 添加试点企业
func RegisterCompany(c *gin.Context) {
appG := app.Gin{C: c}
body := new(CompanyBody)
//解析Body参数
if err := c.ShouldBind(body); err != nil {
appG.Response(http.StatusBadRequest, "Failure", fmt.Sprintf("Coarse-grained operation parameters:%s", err.Error()))
return
}
var bodyBytes [][]byte
bodyBytes = append(bodyBytes, []byte(body.Name))

// 调用智能合约
resp, err := bc.ChannelExecute("registerCompany", bodyBytes)
if err != nil {
appG.Response(http.StatusInternalServerError, "Failure", err.Error())
return
}
var data map[string]interface{}
if err = json.Unmarshal(bytes.NewBuffer(resp.Payload).Bytes(), &data); err != nil {
appG.Response(http.StatusInternalServerError, "Failure", err.Error())
return
}
appG.Response(http.StatusOK, "Success", data)
}

// 将发票放入发票池并给指定企业
func PrepareInvoice(c *gin.Context) {
appG := app.Gin{C: c}
body := new(InvoiceBody)
//解析Body参数
if err := c.ShouldBind(body); err != nil {
appG.Response(http.StatusBadRequest, "Failure", fmt.Sprintf("Coarse-grained operation parameters:%s", err.Error()))
return
}
var bodyBytes [][]byte
bodyBytes = append(bodyBytes, []byte(body.TaxName))
bodyBytes = append(bodyBytes, []byte(body.Companies))
// 调用智能合约
resp, err := bc.ChannelExecute("prepareInvoice", bodyBytes)
if err != nil {
appG.Response(http.StatusInternalServerError, "Failure", err.Error())
return
}
var data map[string]interface{}
if err = json.Unmarshal(bytes.NewBuffer(resp.Payload).Bytes(), &data); err != nil {
appG.Response(http.StatusInternalServerError, "Failure", err.Error())
return
}
appG.Response(http.StatusOK, "Success", data)
}

// 开发票
func IssueTos(c *gin.Context) {
appG := app.Gin{C: c}
body := new(IssueToBody)
//解析Body参数
if err := c.ShouldBind(body); err != nil {
appG.Response(http.StatusBadRequest, "Failure", fmt.Sprintf("Coarse-grained operation parameters:%s", err.Error()))
return
}
var bodyBytes [][]byte
bodyBytes = append(bodyBytes, []byte(body.Companies))
bodyBytes = append(bodyBytes, []byte(body.Value))
bodyBytes = append(bodyBytes, []byte(body.Owner))
bodyBytes = append(bodyBytes, []byte(body.InvoiceId))
bodyBytes = append(bodyBytes, []byte(body.Pool))
bodyBytes = append(bodyBytes, []byte(body.IssuedTo))

// 调用智能合约
resp, err := bc.ChannelExecute("issueTos", bodyBytes)
if err != nil {
appG.Response(http.StatusInternalServerError, "Failure", err.Error())
return
}
var data map[string]interface{}
if err = json.Unmarshal(bytes.NewBuffer(resp.Payload).Bytes(), &data); err != nil {
appG.Response(http.StatusInternalServerError, "Failure", err.Error())
return
}
appG.Response(http.StatusOK, "Success", data)
}

// Login
func Login (c *gin.Context) {
appG := app.Gin{C: c}
body := new(CompanyBody)
//解析Body参数
if err := c.ShouldBind(body); err != nil {
appG.Response(http.StatusBadRequest, "Failure", fmt.Sprintf("Coarse-grained operation parameters:%s", err.Error()))
return
}
var bodyBytes [][]byte
bodyBytes = append(bodyBytes, []byte(body.Name))
resp, err := bc.ChannelQuery("login", bodyBytes)
if err != nil {
appG.Response(http.StatusInternalServerError, "Failure", err.Error())
return
}
// 反序列化json
var data []map[string]interface{}
if err = json.Unmarshal(bytes.NewBuffer(resp.Payload).Bytes(), &data); err != nil {
appG.Response(http.StatusInternalServerError, "Failure", err.Error())
return
}
if data == nil {
appG.Response(http.StatusInternalServerError, "Failure,The business does not exist", err.Error())
return
}
appG.Response(http.StatusOK, "Success", data)
}

8、定义路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package routers

import (
v1 "application/api/v1"

"github.com/gin-gonic/gin"
)

// InitRouter 初始化路由信息
func InitRouter() *gin.Engine {
r := gin.Default()

apiV1 := r.Group("/api/v1")
{
apiV1.GET("/hello", v1.Hello)
apiV1.POST("/getIncoice", v1.GetInvoice)
apiV1.POST("/registerCompany", v1.RegisterCompany)
apiV1.POST("/prepareInvoice", v1.PrepareInvoice)
apiV1.POST("/issueTos", v1.IssueTos)
apiV1.POST("/login",v1.Login)
}

return r
}

9、定义程序主入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"fmt"
"log"
"net/http"
"time"

"application/blockchain"
"application/routers"
)

func main() {
timeLocal, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
log.Printf("时区设置失败 %s", err)
}
time.Local = timeLocal

blockchain.Init()

endPoint := fmt.Sprintf("0.0.0.0:%d", 8000)
server := &http.Server{
Addr: endPoint,
Handler: routers.InitRouter(),
}
log.Printf("[info] start http server listening %s", endPoint)
if err := server.ListenAndServe(); err != nil {
log.Printf("start http server failed %s", err)
}
}

10、编写dockerfile文件,用来构建镜像

1
2
3
4
5
6
7
8
9
10
11
12
FROM golang:1.14 AS app
ENV GO111MODULE=on
ENV GOPROXY https://goproxy.cn,direct
WORKDIR /root/togettoyou
COPY server/. .
RUN CGO_ENABLED=0 go build -v -o "app" .

FROM scratch
WORKDIR /root/togettoyou/
COPY --from=app /root/togettoyou/app ./
COPY --from=app /root/togettoyou/config.yaml ./
ENTRYPOINT ["./app"]

11、docker-compose.yaml文件

1
2
3
4
5
6
7
8
9
10
11
12
FROM golang:1.14 AS app
ENV GO111MODULE=on
ENV GOPROXY https://goproxy.cn,direct
WORKDIR /root/togettoyou
COPY server/. .
RUN CGO_ENABLED=0 go build -v -o "app" .

FROM scratch
WORKDIR /root/togettoyou/
COPY --from=app /root/togettoyou/app ./
COPY --from=app /root/togettoyou/config.yaml ./
ENTRYPOINT ["./app"]

12、编写运行脚本

  1. 构建application

    1
    2
    3
    #!/bin/bash

    docker-compose build
  2. 运行application

    1
    2
    3
    #!/bin/bash

    docker-compose up -d
  3. 停止application

    1
    2
    3
    #!/bin/bash

    docker-compose down
  4. 启动网络

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    #!/bin/bash

    if [[ `uname` == 'Darwin' ]]; then
    echo "Mac OS"
    export PATH=${PWD}/hyperledger-fabric-darwin-amd64-1.4.12/bin:$PATH
    fi
    if [[ `uname` == 'Linux' ]]; then
    echo "Linux"
    export PATH=${PWD}/hyperledger-fabric-linux-amd64-1.4.12/bin:$PATH
    fi

    echo "一、清理环境"
    ./stop.sh

    echo "二、生成证书和秘钥( MSP 材料),生成结果将保存在 crypto-config 文件夹中"
    cryptogen generate --config=./crypto-config.yaml

    echo "三、创建排序通道创世区块"
    configtxgen -profile TwoOrgsOrdererGenesis -outputBlock ./config/genesis.block -channelID firstchannel

    echo "四、生成通道配置事务'appchannel.tx'"
    configtxgen -profile TwoOrgsChannel -outputCreateChannelTx ./config/appchannel.tx -channelID appchannel

    echo "五、为 CHEN 定义锚节点"
    configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./config/CHENAnchor.tx -channelID appchannel -asOrg CHEN

    echo "六、为 ZHOU 定义锚节点"
    configtxgen -profile TwoOrgsChannel -outputAnchorPeersUpdate ./config/ZHOUAnchor.tx -channelID appchannel -asOrg ZHOU

    echo "区块链 : 启动"
    docker-compose up -d
    echo "正在等待节点的启动完成,等待10秒"
    sleep 10

    CHENPeer0Cli="CORE_PEER_ADDRESS=peer0.chen.com:7051 CORE_PEER_LOCALMSPID=CHENMSP CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/chen.com/users/Admin@chen.com/msp"
    CHENPeer1Cli="CORE_PEER_ADDRESS=peer1.chen.com:7051 CORE_PEER_LOCALMSPID=CHENMSP CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/chen.com/users/Admin@chen.com/msp"
    ZHOUPeer0Cli="CORE_PEER_ADDRESS=peer0.zhou.com:7051 CORE_PEER_LOCALMSPID=ZHOUMSP CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/zhou.com/users/Admin@zhou.com/msp"
    ZHOUPeer1Cli="CORE_PEER_ADDRESS=peer1.zhou.com:7051 CORE_PEER_LOCALMSPID=ZHOUMSP CORE_PEER_MSPCONFIGPATH=/etc/hyperledger/peer/zhou.com/users/Admin@zhou.com/msp"

    echo "七、创建通道"
    docker exec cli bash -c "$CHENPeer0Cli peer channel create -o orderer.zxy.com:7050 -c appchannel -f /etc/hyperledger/config/appchannel.tx"

    echo "八、将所有节点加入通道"
    docker exec cli bash -c "$CHENPeer0Cli peer channel join -b appchannel.block"
    docker exec cli bash -c "$CHENPeer1Cli peer channel join -b appchannel.block"
    docker exec cli bash -c "$ZHOUPeer0Cli peer channel join -b appchannel.block"
    docker exec cli bash -c "$ZHOUPeer1Cli peer channel join -b appchannel.block"

    echo "九、更新锚节点"
    docker exec cli bash -c "$CHENPeer0Cli peer channel update -o orderer.zxy.com:7050 -c appchannel -f /etc/hyperledger/config/CHENAnchor.tx"
    docker exec cli bash -c "$ZHOUPeer0Cli peer channel update -o orderer.zxy.com:7050 -c appchannel -f /etc/hyperledger/config/ZHOUAnchor.tx"

    # -n 链码名,可以自己随便设置
    # -v 版本号
    # -p 链码目录,在 /opt/gopath/src/ 目录下
    echo "十、安装链码"
    docker exec cli bash -c "$CHENPeer0Cli peer chaincode install -n fabric-realty -v 1.0.0 -l golang -p chaincode"
    docker exec cli bash -c "$ZHOUPeer0Cli peer chaincode install -n fabric-realty -v 1.0.0 -l golang -p chaincode"

    # 只需要其中一个节点实例化
    # -n 对应上一步安装链码的名字
    # -v 版本号
    # -C 是通道,在fabric的世界,一个通道就是一条不同的链
    # -c 为传参,传入init参数
    echo "十一、实例化链码"
    docker exec cli bash -c "$CHENPeer0Cli peer chaincode instantiate -o orderer.zxy.com:7050 -C appchannel -n fabric-realty -l golang -v 1.0.0 -c '{\"Args\":[\"init\"]}' -P \"AND ('CHENMSP.member','ZHOUMSP.member')\""

    echo "正在等待链码实例化完成,等待5秒"
    sleep 5

    # 进行链码交互,验证链码是否正确安装及区块链网络能否正常工作
    echo "十二、验证链码"
    docker exec cli bash -c "$CHENPeer0Cli peer chaincode invoke -C appchannel -n fabric-realty -c '{\"Args\":[\"hello\"]}'"
    docker exec cli bash -c "$ZHOUPeer0Cli peer chaincode invoke -C appchannel -n fabric-realty -c '{\"Args\":[\"hello\"]}'"
  5. 停止网络

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    #!/bin/bash

    # 清除链码容器
    function clearContainers() {
    CONTAINER_IDS=$(docker ps -a | awk '($2 ~ /dev-peer.*fabric-realty.*/) {print $1}')
    if [ -z "$CONTAINER_IDS" -o "$CONTAINER_IDS" == " " ]; then
    echo "---- No containers available for deletion ----"
    else
    docker rm -f $CONTAINER_IDS
    fi
    }

    # 清除链码镜像
    function removeUnwantedImages() {
    DOCKER_IMAGE_IDS=$(docker images | awk '($1 ~ /dev-peer.*fabric-realty.*/) {print $3}')
    if [ -z "$DOCKER_IMAGE_IDS" -o "$DOCKER_IMAGE_IDS" == " " ]; then
    echo "---- No images available for deletion ----"
    else
    docker rmi -f $DOCKER_IMAGE_IDS
    fi
    }

    echo "清理环境"
    mkdir -p config
    mkdir -p crypto-config
    rm -rf config/*
    rm -rf crypto-config/*
    docker-compose down -v
    clearContainers
    removeUnwantedImages
    echo "清理完毕"

13、根据顺序启动即可,经测试,应用接口均可用 http://localhost:8000/api/v1/