从零到一:联盟链全栈开发学习笔记

FISCO BCOSMyBatisSpring BootVue.js联盟链全栈开发

前言

学了一段时间的联盟链开发,从最开始连数据库都不会操作,到现在能独立搭建一个前后端分离的全栈应用,数据既能写到链上也能存进数据库。

这篇文章把学到的东西梳理一遍,既是笔记,也希望能帮到同样在学的朋友。

技术栈总览

层级技术作用
区块链平台FISCO BCOS(WSL Ubuntu 22.04)联盟链节点,存储链上数据
智能合约Solidity 0.4.25定义链上数据结构和业务逻辑
后端框架Spring Boot接收前端请求,处理业务逻辑
数据访问JDBC → MyBatis连接数据库,执行增删改查
区块链对接FISCO BCOS Java SDK后端与链上合约交互
前端HTML/JS → Vue.js用户界面,数据展示与交互

整体架构:

┌──────────────┐      HTTP       ┌──────────────┐      SDK        ┌──────────────┐
│              │   POST /api/    │              │   ABI + BIN     │              │
│   前端 Vue   │ ──────────────→ │  Spring Boot │ ─────────────→ │ FISCO BCOS   │
│   + Axios    │                 │  Controller  │                 │ 链上合约     │
│              │ ←────────────── │  → Service   │ ←───────────── │              │
│  :5173       │   JSON Response │  → Mapper    │  Receipt/Query  │ :20200/:20201│
│              │                 │   → MySQL    │                 │              │
└──────────────┘                 └──────────────┘                 └──────────────┘

一、后端数据层:JDBC 与 MyBatis

Java 连接数据库有两层关系:JDBC 是底层基础,MyBatis 是上层封装

JDBC(Java Database Connectivity) 是 Java 操作数据库的原始方式,核心就是加载驱动、获取连接、写 SQL、执行、解析结果。能用,但每次都要手写大量模板代码,效率低。

MyBatis 在 JDBC 基础上做了封装:SQL 写在 XML 或注解里,Java 方法只关心业务参数和返回对象。不用手动拼 SQL、不用手动解析结果集,一个方法做一件事。

简单说:JDBC 是地基,MyBatis 是在地基上盖的房子。实际开发中直接用 MyBatis,但了解 JDBC 能帮你理解 MyBatis 背后做了什么。

二、FISCO BCOS Java SDK

2.1 SDK 是什么

FISCO BCOS 提供了 Java SDK,让后端代码能通过 Java 方法直接与链上合约交互。不需要手拼 HTTP 请求,调用合约就像调普通 Java 方法一样。

2.2 关键文件及放置位置

FISCO BCOS 智能合约编译后会生成三个文件,加上连接链需要的证书,一共四类文件:

src/main/resources/
├── sdk.toml                    ← SDK 连接配置
├── abi/
│   └── StudentManage.abi       ← 合约 ABI(接口描述)
├── bin/
│   └── StudentManage.bin       ← 合约 BIN(编译字节码)
├── sdk/
│   ├── ca.crt                  ← CA 证书
│   ├── sdk.crt                 ← SDK 证书
│   └── sdk.key                 ← SDK 私钥
└── pem/
    └── admin.pem               ← 部署合约的账户私钥

2.3 三个编译文件的作用

文件全称作用
.abiApplication Binary Interface描述合约有哪些方法、参数是什么类型
.binBytecode合约的字节码,部署时上传到链上
.pemPEM Key部署/调用合约的账户私钥

简单理解

ABI 是”说明书”,告诉 SDK 怎么调用合约;BIN 是”合同原件”,部署到链上;PEM 是”签名章”,证明是你在操作。

2.4 后端加载 SDK

@Service
public class StudentManageService {

    private static final String CONTRACT_ADDRESS = "0x464bf4acd48735a21326ed589d54620e06bbe400";

    // 1. 读取配置,建立连接
    BcosSDK sdk = BcosSDK.build("src/main/resources/sdk.toml");
    Client client = sdk.getClient(1);
    CryptoKeyPair credential = client.getCryptoSuite().getCryptoKeyPair();

    // 2. 加载合约(通过 ABI + 地址 + 签名账户)
    StudentManage contract = StudentManage.load(CONTRACT_ADDRESS, client, credential);

    // 3. 调用合约方法(和调用普通 Java 方法一样)
    public String addStudent(String sName, Long sId, Long sAge, String addr) {
        try {
            TransactionReceipt receipt = contract.addStudent(
                sName,
                BigInteger.valueOf(sId),
                BigInteger.valueOf(sAge),
                addr
            );
            return "添加成功";
        } catch (Exception e) {
            return "添加失败: " + e.getMessage();
        }
    }
}

2.5 SDK 配置文件

# sdk.toml
[cryptoMaterial]
certPath = "sdk"                     # 证书目录

[network]
peers=["127.0.0.1:20200", "127.0.0.1:20201"]  # 节点地址

[threadPool]
maxBlockingQueueSize = "102400"      # 线程池配置

常见问题

  • 证书路径写错会报 SSL handshake 错误
  • 节点地址要和 nodes.json 中的一致
  • 账户私钥(pem)必须和部署合约时用的账户匹配

三、前端演进:HTML/JS → Vue.js

3.1 阶段一:原生 HTML + JavaScript

最早用原生 JS 写前端,通过 fetch 发请求:

// 获取输入框的值,打包发送
function addStudent() {
    var name = document.getElementById('name').value;
    var sid = document.getElementById('sid').value;
    var arr = [name, sid];

    fetch('http://localhost:8080/addStudent', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(arr)
    })
    .then(res => res.json())
    .then(data => alert('提交成功'));
}

问题: 每个输入框都要 document.getElementById,数据多了代码就乱。没有视图和数据的绑定,改了数据还要手动改 DOM。

3.2 阶段二:Vue.js(选项式 API)

Vue.js 的核心思想是数据驱动视图——数据变了,界面自动更新。

基础用法

<template>
  <div>
    <input v-model="student.name" placeholder="姓名">
    <input v-model.number="student.sid" placeholder="学号">
    <input v-model.number="student.age" placeholder="年龄">
    <button @click="addStudent">添加学生</button>

    <ul>
      <li v-for="s in students" :key="s.sid">
        {{ s.name }} - {{ s.sid }} - {{ s.age }}岁
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  data() {
    return {
      student: { name: '', sid: null, age: null },
      students: []
    }
  },
  methods: {
    async addStudent() {
      const res = await fetch('/api/addStudent', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(this.student)
      });
      const data = await res.json();
      if (data.status === 'ok') {
        this.students.push({ ...this.student });
        this.student = { name: '', sid: null, age: null };
      }
    }
  }
}
</script>

双向绑定(v-model)

v-model 让表单输入和数据自动同步:

用户输入 "张三" → v-model → student.name = "张三" → 视图自动更新

不需要手动 getElementById,不需要手动赋值。数据是唯一的真相来源。

选项式 API 核心结构

export default {
  data()      { /* 响应式数据 */ },
  computed: { /* 计算属性 */ },
  methods:  { /* 方法 */ },
  mounted()  { /* 组件挂载后执行 */ }
}
选项用途
data()定义组件的状态变量
methods定义业务方法
computed基于已有数据自动计算
mounted页面加载后自动执行(如获取列表)

3.3 路由(vue-router)

单页应用需要在不同页面间切换,vue-router 处理这个:

// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
import AddStudent from '../views/AddStudent.vue'

const routes = [
  { path: '/', component: Home },
  { path: '/add', component: AddStudent }
]

export default createRouter({
  history: createWebHistory(),
  routes
})
<!-- App.vue -->
<template>
  <nav>
    <router-link to="/">首页</router-link>
    <router-link to="/add">添加学生</router-link>
  </nav>
  <router-view />
</template>

3.4 前端演进对比

对比项原生 HTML/JSVue.js
数据绑定手动 DOM 操作v-model 双向绑定
页面切换多个 HTML 文件单页 + 路由
状态管理变量散落各处data() 统一管理
代码复用组件化
开发体验繁琐流畅

四、全栈业务流程

整理一下完整的数据流转:

用户操作前端

前端收集数据(v-model 绑定)

发送 HTTP 请求(Axios / fetch)

后端 Controller 接收

必要判断(参数校验、权限检查)

    ├──→ 数据上链(Java SDK → 智能合约)
    │         ↓
    │    TransactionReceipt(上链成功/失败)

    └──→ 数据库存储(MyBatis → MySQL)

         操作结果

后端返回成功或异常(JSON)

前端展示结果

为什么要做「双写」?

  • 链上数据:不可篡改,公开透明,适合存关键凭证
  • 数据库:查询快,支持复杂搜索,适合存日常业务数据
  • 两者互补,链上保真,库中保效

五、项目结构

StudentManager_2/
├── src/main/java/cn/emllm/
│   ├── StudentManager2Application.java     ← Spring Boot 启动类
│   ├── MyStudentController/
│   │   └── MyStudentController.java        ← 接收前端请求
│   ├── MyStudentManagerService/
│   │   └── MyStudentManagerService.java    ← SDK 交互 + 业务逻辑
│   ├── Fisco/
│   │   └── StudentManage.java              ← 合约加载(ABI+BIN 生成)
│   ├── FiscoMapper/
│   │   └── FiscoMapper.java                ← MyBatis 数据库操作
│   ├── Student/
│   │   └── Student.java                    ← 学生实体类
│   └── Teacher/
│       └── Teacher.java                    ← 老师实体类

├── src/main/resources/
│   ├── application.yml                     ← Spring Boot 配置
│   ├── sdk.toml                            ← FISCO BCOS SDK 连接配置
│   ├── abi/StudentManage.abi               ← 合约接口描述
│   ├── bin/StudentManage.bin               ← 合约字节码
│   ├── pem/admin.pem                       ← 部署账户私钥
│   ├── sdk/                                ← SDK 证书
│   │   ├── ca.crt
│   │   ├── sdk.crt
│   │   └── sdk.key
│   ├── static/index.html                   ← 静态页面
│   └── templates/                          ← 模板文件

└── studentmanager_2_vue/                   ← Vue.js 前端
    ├── src/
    │   ├── App.vue                         ← 根组件
    │   ├── main.js                         ← 入口文件
    │   ├── components/
    │   │   └── addStudent.vue              ← 添加学生组件
    │   └── assets/                         ← 样式文件
    └── vite.config.js                      ← Vite 构建配置

六、未来规划

现在能跑通基本流程,但还有很多可以完善的地方:

方向内容
角色登录区分管理员、老师、学生三种角色,不同权限看到不同功能
数据校验前端表单校验 + 后端参数验证,拦截非法输入

小结

作为一个初学者,从零开始学前后端加联盟链上链,整体进度还是比较快的。

期间后端代码没有用 AI 生成,遇到报错都是自己先看,实在搞不懂再去问 AI 为什么报错,从错误中吸取教训。这种渐进式的学习方式虽然慢一点,但每一步都是自己走过来的,理解也更扎实。

下一步继续完善角色登录和数据校验,把基础打牢。