基于区块链的校园二手书交易系统

这是本人从某宝搞的一个基于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克隆。得到如图文件夹。

git clone https://gitee.com/songer123/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">&#xe65e;</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">&#xe65e;</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">&#xe65e;</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集成以及开发四个步骤

相关推荐
希望永不加班3 小时前
SpringBoot 自动配置类加载顺序与优先级
java·spring boot·后端·spring·mybatis
pjwonline13 小时前
反向仲裁:去中心化知识网络中的社会性共识引擎
网络·人工智能·去中心化·区块链·智能合约
孜孜不倦不忘初心4 小时前
Vue 项目结构与命名规范
vue.js·代码规范
凯尔萨厮5 小时前
创建SpringWeb项目(Spring3.2+)
spring·mvc
Flittly5 小时前
【SpringAIAlibaba新手村系列】(16)调用百度 MCP 服务
java·笔记·spring·ai·springboot
希望永不加班5 小时前
SpringBoot 事件机制:ApplicationEvent 与监听器
java·开发语言·spring boot·后端·spring
账号已注销free6 小时前
Vue3项目中给组件命名的方式
vue.js
前端那点事6 小时前
VueUse 全面指南|Vue3组合式工具集实战
vue.js
前端那点事6 小时前
Vue3+Pinia实战完整版|从入门到精通,替代Vuex的状态管理首选
vue.js
Devin_chen6 小时前
Pinia 渐进式学习指南
前端·vue.js