这是本人从某宝搞的一个基于springboot+vue的校园二手书交易平台,试着将其集成区块链,改造为区块链项目。
区块链是使用Hyperledger Fabric,有考虑过fisco,但是fabric的智能合约由于可以不需要像solidity的address字段,使用id可以直接绑定,所以不需要去增加项目中的图书订单的字段,也不需要单独存储一个交易哈希的字段,非常的方便。
一、项目启动预览
1.使用navicat运行db.sql,把数据导入mysql数据库

2.使用idea打开项目,并设置maven为自己的maven 3,然后去pom.xml安装对应的mysql依赖

3.项目依赖安装完毕,运行springboot,使用浏览器打开,用户账号有三个 a1/123456, a2/123456, a3/123456,管理员账号有一个 admin/admin
http://localhost:8080/ershoushujiaoyipingtai/front/index.html
预览此项目,需要加入区块链的元素的部分很明显在图书订单模块,订单的状态分为已支付、已发货、已收货和退款四种状态,所以看可以理解为一个溯源系统,用来记录图书订单商品,一共需要修改的部分有如下几个地方:
(1)创建订单(createOrder):进入图书模块点击图书,下面会有添加到购物车按钮和立即购买的按钮,无论是哪个,区块链记录订单的地方一定发生在交易,所以区块链中创建订单的触发在于确认下单的支付按钮和购物车的点击购买按钮如下,这是第一个点,所处前端页面在resources/front/pages/tushuOrder/confirm.html和resources/front/pages/card/list.html

(2)修改订单(updateOrder):此页面在个人中心的图书订单部分和后台的图书订单管理部分,当点击退款、发货和收货的时候触发。


(3)查看订单溯源,直接在表格中加这个内容实在太过拥挤,所以放在后台部分的图书订单管理详情页会比较好,在返回按钮之前去加上。

二、Hyperledger Fabric褡裢
使用vmware搭建虚拟机,由于开发需要消耗大量内存,所以实际操作如果能力足够还是以服务器版本为好,不过这里为了演示,还是使用桌面版本的ubuntu22。
1.打开虚拟机终端安装需要的前置包
sudo apt install openssh-server vim git curl docker-compose jq -y
启动docker
sudo systemctl start docker
添加docker的用户名,我这里叫fabric,你可以改成自己的
sudo usermod -a -G docker fabric
配置docker开机自动启动
sudo systemctl enable docker
重启虚拟机
sudo reboot
2.安装fabric的生产网络。由于目前docker已经无法拉取fabric的镜像,所以可以使用我上传在github的fabric-deploy,直接使用git克隆。得到如图文件夹。

直接运行bash deploy.sh完成fabric单机网络的部署,部署成功之后在浏览器输入ip:8080进入区块链浏览器。用户名和密码为 admin/adminpw,进入此页代表成功了
bash deploy.sh

三、智能合约开发
1.使用go语言开发智能合约,可以使用goland创建项目book,并且创建book.go文件,然后安装对应的go语言依赖
go get github.com/hyperledger/fabric-contract-api-go/v2/contractapi
2.创建book.go,代码如下:
package main
import (
"encoding/json"
"fmt"
"github.com/hyperledger/fabric-contract-api-go/v2/contractapi"
"log"
)
type SmartContract struct {
contractapi.Contract
}
type BookOrder struct {
OrderId string `json:"orderId"`
Buyer string `json:"buyer"`
Phone string `json:"phone"`
Address string `json:"address"`
BookName string `json:"bookName"`
Number string `json:"number"`
DeliveryId string `json:"deliveryId"`
Delivery string `json:"delivery"`
Price float64 `json:"price"`
OrderType string `json:"orderType"`
PayType string `json:"payType"`
}
// 支付的时候触发
func (s *SmartContract) CreateBookOrder(ctx contractapi.TransactionContextInterface,
orderId, buyer, phone, address, bookName, number, deliveryId, delivery, orderType, payType string, price float64) error {
bookOrderByter, err := ctx.GetStub().GetState(orderId)
if err != nil {
return err
}
if bookOrderByter != nil {
return fmt.Errorf("%s is exist", orderId)
}
bookOrder := BookOrder{
OrderId: orderId,
Buyer: buyer,
Phone: phone,
Address: address,
BookName: bookName,
Number: number,
DeliveryId: deliveryId,
Delivery: delivery,
Price: price,
OrderType: orderType,
PayType: payType,
}
bookOrderBytes, _ := json.Marshal(bookOrder)
return ctx.GetStub().PutState(orderId, bookOrderBytes)
}
// 退款和收货的时候触发
func (s *SmartContract) UpdateBookOrder(ctx contractapi.TransactionContextInterface, orderId, newState, delivery, deliveryId string) error {
bookOrderByter, err := ctx.GetStub().GetState(orderId)
if err != nil {
return err
}
if bookOrderByter == nil {
return fmt.Errorf("%s does not exist", orderId)
}
var bookOrder BookOrder
err = json.Unmarshal(bookOrderByter, &bookOrder)
if err != nil {
return err
}
if bookOrder.OrderType == newState {
return fmt.Errorf("%s has been updated", orderId)
}
if delivery != "" && deliveryId != "" {
bookOrder.Delivery = delivery
bookOrder.DeliveryId = deliveryId
}
bookOrder.OrderType = newState
bookOrderBytes, _ := json.Marshal(bookOrder)
return ctx.GetStub().PutState(orderId, bookOrderBytes)
}
// 查询历史状态
func (s *SmartContract) GetBookOrder(ctx contractapi.TransactionContextInterface, orderId string) ([]string, error) {
bookOrderIterator, err := ctx.GetStub().GetHistoryForKey(orderId)
if err != nil {
return nil, err
}
defer bookOrderIterator.Close()
var records []string
for bookOrderIterator.HasNext() {
history, err := bookOrderIterator.Next()
if err != nil {
return nil, err
}
record := fmt.Sprintf("TxId: %s,Value: %s,timestamp: %s", history.TxId, history.Value, history.Timestamp)
records = append(records, record)
}
return records, nil
}
func main() {
assetChaincode, err := contractapi.NewChaincode(new(SmartContract))
if err != nil {
log.Panicf("Error creating asset-transfer-basic chaincode: %v", err)
}
if err := assetChaincode.Start(); err != nil {
log.Panicf("Error starting asset-transfer-basic chaincode: %v", err)
}
}
3.在go.mod里面设置go版本为1.21.3(因为fabric的docker中go版本为1.21.6),整体如下
module book
go 1.21.3
require github.com/hyperledger/fabric-contract-api-go/v2 v2.2.0
require (
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/hyperledger/fabric-chaincode-go/v2 v2.0.0 // indirect
github.com/hyperledger/fabric-protos-go-apiv2 v0.3.4 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.24.0 // indirect
golang.org/x/text v0.17.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
google.golang.org/grpc v1.67.0 // indirect
google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
4.把整体book文件夹通过winscp复制到虚拟机linux的fabric-deploy里面,运行bash code.sh book,得到下图表示安装成功
bash code.sh book

四、集成fabric sdk
sdk官网如下:https://github.com/hyperledger/fabric-gateway/blob/main/java/README.md
1.参考官网把项目的pom.xml插入依赖
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-bom</artifactId>
<version>4.31.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-bom</artifactId>
<version>1.73.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.hyperledger.fabric</groupId>
<artifactId>fabric-gateway</artifactId>
<version>1.8.0</version>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-api</artifactId>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<scope>runtime</scope>
</dependency>
</dependencise>
2.把linux里面fabric-deploy里面的crypto-config移动到项目resources文件夹下面

3.配置application.yml,设置和区块链的相关连接,添加到最下面,需要把endpoint改成自己虚拟机的
fabric:
msp:
id: Org1MSP
channel:
name: mychannel
chaincode:
name: book
peer:
endpoint: 192.168.116.132:7051
overrideAuth: peer0.org1.example.com
tls:
cert:
path: crypto-config/peerOrganizations/org1.example.com/peers/peer0.org1.example.com/tls/ca.crt
user:
cert:
path: crypto-config/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/signcerts/User1@org1.example.com-cert.pem
key:
path: crypto-config/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/keystore/priv_sk
4.在Java文件夹里面com文件夹下面添加fabric文件夹,和FabricGatewayConfig.java
添加代码如下:此代码为了拿到fabric的节点信息,生产contract连接的network实例
package com.fabric;
import io.grpc.Grpc;
import io.grpc.ManagedChannel;
import io.grpc.TlsChannelCredentials;
import org.hyperledger.fabric.client.Gateway;
import org.hyperledger.fabric.client.identity.Identities;
import org.hyperledger.fabric.client.identity.Identity;
import org.hyperledger.fabric.client.identity.Signer;
import org.hyperledger.fabric.client.identity.Signers;
import org.hyperledger.fabric.client.identity.X509Identity;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.security.InvalidKeyException;
import java.security.PrivateKey;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.concurrent.TimeUnit;
@Configuration
public class FabricGatewayConfig {
// @Value 拿到yml中的信息
@Value("${fabric.msp.id}")
private String mspId;
@Value("${fabric.peer.endpoint}")
private String peerEndpoint;
@Value("${fabric.peer.overrideAuth}")
private String overrideAuth;
@Value("${fabric.tls.cert.path}")
private String tlsCertPath;
@Value("${fabric.user.cert.path}")
private String userCertPath;
@Value("${fabric.user.key.path}")
private String userKeyPath;
/**
* 创建 gRPC channel Bean,从 Classpath 读取 TLS 认证
* (此方法內容与 properties 版本完全相同)
* @return ManagedChannel
* @throws IOException
*/
@Bean(destroyMethod = "shutdownNow")
public ManagedChannel managedChannel() throws IOException {
ClassPathResource tlsCertResource = new ClassPathResource(tlsCertPath);
TlsChannelCredentials.Builder credentialsBuilder = TlsChannelCredentials.newBuilder();
try (InputStream tlsCertInputStream = tlsCertResource.getInputStream()) {
credentialsBuilder.trustManager(tlsCertInputStream);
}
return Grpc.newChannelBuilder(peerEndpoint, credentialsBuilder.build())
.overrideAuthority(overrideAuth)
.build();
}
/**
* 创建 Gateway Bean
* (此方法內容与 properties 版本完全相同)
* @param channel gRPC channel
* @return Gateway
* @throws IOException
* @throws CertificateException
* @throws InvalidKeyException
*/
@Bean(destroyMethod = "close")
public Gateway gateway(ManagedChannel channel) throws IOException, CertificateException, InvalidKeyException {
return Gateway.newInstance()
.identity(newIdentity())
.signer(newSigner())
.connection(channel)
.evaluateOptions(options -> options.withDeadlineAfter(5, TimeUnit.SECONDS))
.endorseOptions(options -> options.withDeadlineAfter(15, TimeUnit.SECONDS))
.submitOptions(options -> options.withDeadlineAfter(5, TimeUnit.SECONDS))
.commitStatusOptions(options -> options.withDeadlineAfter(1, TimeUnit.MINUTES))
.connect();
}
private Identity newIdentity() throws IOException, CertificateException {
// (此方法內容與 properties 版本完全相同)
ClassPathResource certResource = new ClassPathResource(userCertPath);
try (Reader certReader = new InputStreamReader(certResource.getInputStream())) {
X509Certificate certificate = Identities.readX509Certificate(certReader);
return new X509Identity(mspId, certificate);
}
}
private Signer newSigner() throws IOException, InvalidKeyException {
// (此方法內容與 properties 版本完全相同)
ClassPathResource keyResource = new ClassPathResource(userKeyPath);
try (Reader keyReader = new InputStreamReader(keyResource.getInputStream())) {
PrivateKey privateKey = Identities.readPrivateKey(keyReader);
return Signers.newPrivateKeySigner(privateKey);
}
}
}
五、项目改造
1.订单发布
后端修改controller里面的TushuOrderController.java,添加代码如下(不管什么权限认证了,直接就是干):
@Value("${fabric.channel.name}")
private String channelName;
@Value("${fabric.chaincode.name}")
private String chaincodeName;
private Gateway gateway;
private Contract contract;
private final Gson gson = new GsonBuilder().setPrettyPrinting().create();
// 透过构造函数注入 Gateway Bean
public TushuOrderController(Gateway gateway) {
this.gateway = gateway;
}
// 在 Bean 初始化后,获取 Contract 实例
@PostConstruct
public void init() {
Network network = gateway.getNetwork(channelName);
this.contract = network.getContract(chaincodeName);
}
@PostMapping("/block/create")
public R createBookOrder(@RequestParam Map<String, Object> params) throws
EndorseException, CommitException, SubmitException, CommitStatusException {
Wrapper<TushuOrderEntity> queryWrapper = new EntityWrapper<>();
String length = (String) params.get("length");
String index = (String) params.get("index");
queryWrapper.orderBy("id",false);
TushuOrderEntity tushuOrderEntity = tushuOrderService.selectOne(queryWrapper);
int orderId = tushuOrderEntity.getId()+Integer.parseInt(index)-Integer.parseInt(length);
String buyer = (String) params.get("buyer");
String phone = (String) params.get("phone");
String address = (String) params.get("address");
String bookName = (String) params.get("bookName");
String number = (String) params.get("number");
String deliveryId = "";
String delivery = "";
String orderType = (String) params.get("orderType");
String payType = (String) params.get("payType");
String price = (String) params.get("price");
this.contract.submitTransaction("CreateBookOrder",String.valueOf(orderId),buyer,phone,address,
bookName,number,deliveryId,delivery,orderType,payType,price);
return R.ok();
}
@PostMapping("/block/update")
public R updateBookOrder(@RequestParam Map<String, Object> params) throws
EndorseException, CommitException, SubmitException, CommitStatusException{
String orderId = (String) params.get("orderId");
String newState = (String) params.get("newState");
String deliveryId = (String) params.get("deliveryId");
String delivery = (String) params.get("delivery");
this.contract.submitTransaction("UpdateBookOrder",orderId,newState,deliveryId,delivery);
return R.ok();
}
@GetMapping("/block/get")
public R getBookOrder(@RequestParam String orderId) throws GatewayException{
byte[] result = this.contract.evaluateTransaction("GetBookOrder",orderId);
String json = new String(result, StandardCharsets.UTF_8);
return R.ok().put("data", JSON.parse(json));
}
修改前端部分,resources/front/pages/tushuOrder/confirm.html。找到payClick部分替换成如下代码记得把对应按钮的class去掉 btn-theme
payClick() {
var index = layui.jquery('input[name=address]:checked').val();
if (!index) {
layui.layer.msg('请选择收货地址', {
time: 2000,
icon: 5
});
return false;
}
let data={
addressId: vue.addressList[index].id,
tushus:localStorage.getItem('tushus'),
yonghuId: localStorage.getItem('userid'),
tushuOrderPaymentTypes: vue.tushuOrderPaymentTypes,
}
var tushu = JSON.parse(data.tushus)
var count = 0
// 获取商品详情信息
layui.http.request(`tushuOrder/order`, 'POST', data, (res) => {
// 订单编号
var orderId = genTradeNo();
if(res.code == 0){
for (var i=0;i<tushu.length;i++){
var buyer = tushu[i].tushuZuozhe
var phone = this.addressList[index].addressPhone
var address = this.addressList[index].addressDizhi
var bookName = tushu[i].tushuName
var number = tushu[i].buyNumber
var orderType = "已支付"
var payType = "现金"
var price = (tushu[i].buyNumber*tushu[i].tushuNewMoney).toFixed(2)
layui.http.request(`tushuOrder/block/create`,'POST',{
length:tushu.length-1,index:i,
buyer,phone,address,bookName,number,orderType,payType,price
},(res) => {
if (res.code == 0){
count++
if (count==tushu.length){
alert("下单成功,点击确定后跳转 我的订单页面");
window.location.href='../tushuOrder/list.html';
}
}
})
}
}
});
}
(2)更改订单
resources/front/pages/tushuOrder/list.html,把其中的退款和收货分别改成(记得更改按钮的入参):
// 退款
,refund(id) {
var mymessage = confirm("确定要退款吗?");
if(!mymessage){
alert("已取消操作");
return false;
}
// 获取商品详情信息
layui.http.request(`tushuOrder/refund?id=`+id, 'get', {}, (res) => {
if(res.code==0){
layui.http.request(`tushuOrder/block/update`,'POST',{
orderId:id,newState:"退款",deliveryId:"",delivery:""
},(res)=>{
if (res.code==0){
layer.msg('成功退款', {
time: 2000,
icon: 6
});
window.location.reload();
}else{
layer.msg(res.msg, {
time: 2000,
icon: 2
});
}
})
}else{
layer.msg(res.msg, {
time: 2000,
icon: 2
});
}
});
}
// 收货
,receiving(id,delivery,deliveryId) {
var mymessage = confirm("确定要收货吗?");
if(!mymessage){
alert("已取消操作");
return false;
}
// 获取商品详情信息
layui.http.request(`tushuOrder/receiving?id=`+id, 'get', {}, (res) => {
if(res.code==0){
layui.http.request(`tushuOrder/block/update`,'POST',{
orderId:id,newState:"已收货",deliveryId:delivery,delivery:deliveryId
},(res)=>{
if (res.code==0){
layer.msg('成功收货', {
time: 2000,
icon: 6
});
window.location.reload();
}else{
layer.msg(res.msg, {
time: 2000,
icon: 2
});
}
})
}else{
layer.msg(res.msg, {
time: 2000,
icon: 2
});
}
});
}
(3)添加查看区块链的按钮,整体三个按钮:
<button v-if="item.tushuOrderTypes==3" @click="refund(item.id)" type="button" class="layui-btn layui-btn-sm layui-btn-radius layui-btn-warm">
<i class="layui-icon"></i> 退款
</button>
<button v-if="item.tushuOrderTypes==4" @click="receiving(item.id,item.tushuOrderCourierName,item.tushuOrderCourierNumber)" type="button" class="layui-btn layui-btn-sm layui-btn-radius layui-btn-warm">
<i class="layui-icon"></i> 收货
</button>
<button @click="look(item.id)" type="button" class="layui-btn layui-btn-sm layui-btn-radius layui-btn-warm">
<i class="layui-icon"></i> 查看区块链溯源信息
</button>
并且添加方法:
look(id) {
const loading = layui.layer.load(1, { shade: 0.2 });
// 第三个参数要传 {}(GET 也要占位)
layui.http.request(`tushuOrder/block/get?orderId=${id}`, "get", {}, (res) => {
try {
if (res.code !== 0 || !Array.isArray(res.data)) {
layui.layer.msg("查询失败:" + (res.msg || "无数据"), { icon: 5 });
return;
}
// 秒+纳秒 -> 可读时间
const toReadableTime = (sec, nanos) => {
const ms = Number(sec) * 1000 + Math.floor(Number(nanos) / 1e6);
// 24小时制,可自行改本地化选项
return new Date(ms).toLocaleString('zh-CN', { hour12: false });
};
// 逐条把 timestamp 替换成可读时间
const content = res.data.map(line => {
const m = line.match(/timestamp:\s*seconds:(\d+)\s+nanos:(\d+)/i);
if (m) {
const readable = toReadableTime(m[1], m[2]);
// 把原来的 timestamp: seconds:... nanos:... 替换为 time: YYYY-MM-DD ...
return line.replace(/timestamp:\s*seconds:\d+\s+nanos:\d+/i, `time: ${readable}`);
}
return line; // 没匹配到就原样返回
}).join("\n\n");
layui.layer.open({
type: 1,
title: "订单历史",
area: ["750px", "520px"],
content: `<pre style="padding:10px;white-space:pre-wrap;word-break:break-all;">${content}</pre>`
});
} finally {
layui.layer.close(loading);
}
});
}
**总结:**最后管理端是使用老版本的vue-cli-server,如果要改需要把node版本降低就没有继续下去了,整体修改过程来看,分为褡裢、合约、和sdk集成以及开发四个步骤