基于 FISCO BCOS 的学生档案管理系统

FISCO BCOSSoliditySpring Boot联盟链智能合约

前言

最近学习了联盟链开发,用 FISCO BCOS 搭建了一个学生档案管理系统。前后端都能跑通,数据真的写到了链上。

这篇文章记录一下从智能合约到前后端交互的完整过程。

技术栈

层级技术
智能合约Solidity 0.4.25
区块链平台FISCO BCOS(本地 WSL Ubuntu 22.04)
管理工具WeBASE-Front
后端Spring Boot + FISCO BCOS Java SDK
前端HTML + 原生 JavaScript

数据流架构

┌──────────────┐       HTTP        ┌──────────────┐       SDK        ┌──────────────┐
│              │   POST /addStudent│              │   load + execute │              │
│   前端 HTML  │ ───────────────→ │  Spring Boot │ ──────────────→ │ FISCO BCOS   │
│   + JS       │                   │  Controller  │                  │ 链上合约     │
│              │ ←─────────────── │  → Service   │ ←────────────── │              │
│  127.0.0.1   │    JSON Response  │  127.0.0.1   │  TransactionReceipt │ 127.0.0.1  │
│   :5500      │                   │   :8080      │                  │ :20200/:20201│
└──────────────┘                   └──────────────┘                  └──────────────┘

一、智能合约

合约实现了学生档案的完整 CRUD,带有管理员/老师权限控制。

数据结构

struct Student {
    string sName;       // 姓名
    uint256 sId;        // 学号
    uint8 sAge;         // 年龄
    address studentAddr; // 钱包地址
    bool graduated;     // 是否毕业
}

权限控制

address public admin;                           // 管理员(部署者)
mapping(address => bool) private teachers;      // 老师列表
mapping(uint256 => Student) private stuMap;     // 学生表
mapping(uint256 => bool) private studentID;     // 学号是否存在
mapping(address => uint256) private addressToId; // 地址→学号

modifier onlyTeacher() {
    require(teachers[msg.sender], "Not teacher");
    _;
}

modifier onlyAdmin() {
    require(admin == msg.sender, "You are not admin");
    _;
}

完整合约代码

pragma solidity ^0.4.25;
pragma experimental ABIEncoderV2;

contract StudentManage {

    struct Student {
        string sName;
        uint256 sId;
        uint8 sAge;
        address studentAddr;
        bool graduated;
    }

    address public admin;

    constructor() {
        admin = msg.sender;
    }

    mapping(address => bool) private teachers;
    mapping(uint256 => Student) private stuMap;
    mapping(uint256 => bool) private studentID;
    mapping(address => uint256) private addressToId;

    modifier onlyTeacher() {
        require(teachers[msg.sender], "Not teacher");
        _;
    }

    modifier onlyAdmin() {
        require(admin == msg.sender, "You are not admin");
        _;
    }

    // ========== 事件 ==========

    event StudentAdd(address indexed teacher, uint256 indexed sId, string sName, uint8 sAge);
    event GraduationUpdated(uint256 indexed sId, bool graduated);
    event Studentrm(address indexed teacher, uint256 indexed sId);
    event TeacherAdd(address indexed teacher, bool added, address indexed by);

    // ========== 学生操作 ==========

    function addStudent(string memory _sName, uint256 _sId, uint8 _sAge, address _studentAddr)
        public onlyTeacher
    {
        require(!studentID[_sId], "sId is true");
        stuMap[_sId].sName = _sName;
        stuMap[_sId].sId = _sId;
        stuMap[_sId].sAge = _sAge;
        stuMap[_sId].studentAddr = _studentAddr;
        stuMap[_sId].graduated = false;
        addressToId[_studentAddr] = _sId;
        studentID[_sId] = true;
        emit StudentAdd(msg.sender, _sId, _sName, _sAge);
    }

    function getStudent(uint256 _sId) public view returns(Student memory) {
        require(studentID[_sId], "sId is false");
        return stuMap[_sId];
    }

    function rmStudent(uint256 _sId) public onlyTeacher {
        require(studentID[_sId], "sId is false");
        address studentAddr = stuMap[_sId].studentAddr;
        delete addressToId[studentAddr];
        delete stuMap[_sId];
        studentID[_sId] = false;
        emit Studentrm(msg.sender, _sId);
    }

    function setGraduated(uint256 _sId, bool _graduated) public onlyTeacher {
        require(studentID[_sId], "Student not found");
        stuMap[_sId].graduated = _graduated;
        emit GraduationUpdated(_sId, _graduated);
    }

    function getMyArchive() public view returns(Student memory) {
        uint256 sId = addressToId[msg.sender];
        require(studentID[sId], "error address");
        return stuMap[sId];
    }

    // ========== 管理员操作 ==========

    function addTeacher(address _teachers) public onlyAdmin {
        teachers[_teachers] = true;
        emit TeacherAdd(_teachers, teachers[msg.sender], msg.sender);
    }

    function rmTeacher(address _teachers) public onlyAdmin {
        require(teachers[_teachers], "Not a teacher");
        teachers[_teachers] = false;
        emit TeacherAdd(_teachers, teachers[msg.sender], msg.sender);
    }
}

设计要点

  • admin 在构造函数中自动设为部署者地址
  • 只有 admin 能添加/删除老师(onlyAdmin
  • 只有老师能操作学生(onlyTeacher
  • 学生可通过 getMyArchive() 用自己的钱包地址查自己的档案
  • 每次写操作都 emit 事件,方便链上追踪

二、前端示例:添加学生

前端是简单的 HTML 页面,通过按钮监听获取输入框内容,打包成数组发送给后端。

HTML 结构

<div id="addStudent_div">
    <div style="text-align: center;">
        <span>添加学生</span>
    </div>
    <div>
        <span>请输入学生姓名</span>
        <input type="text" id="text_1">
    </div>
    <div>
        <span>请输入学生学号</span>
        <input type="text" id="text_2">
    </div>
    <div>
        <span>请输入学生年龄</span>
        <input type="text" id="text_3">
    </div>
    <div>
        <span>请输入学生钱包地址</span>
        <input type="text" id="text_4">
    </div>
    <div>
        <input type="button" value="确认" id="get_1">
    </div>
</div>

JavaScript 交互

var btn = document.getElementById('get_1');

function addStuendt() {
    // 获取四个输入框的值
    var v1 = document.getElementById('text_1').value;
    var v2 = document.getElementById('text_2').value;
    var v3 = document.getElementById('text_3').value;
    var v4 = document.getElementById('text_4').value;

    // 打包成数组
    var arr = [v1, v2, v3, v4];

    // POST 到后端
    fetch('http://localhost:8080/addStudent', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(arr)
    })
    .then(response => response.json())
    .then(result => {
        console.log('后端返回:', result);
        alert('提交成功');
    })
    .catch(error => {
        console.error('请求失败:', error);
        alert('提交失败');
    });
}

btn.addEventListener("click", addStuendt);

交互流程

点击按钮 → 获取四个输入框的值 → 放进数组 → fetch POST 到 localhost:8080/addStudent → 后端返回 JSON → 弹窗提示

三、后端示例:Spring Boot + Java SDK

后端分两层:Controller 接收前端请求,Service 调用 Java SDK 与链交互。

Controller — 接收请求

@RestController
@CrossOrigin("http://127.0.0.1:5500")
public class Controller {

    @Autowired
    private StudentManageService studentManageService;

    @PostMapping("/addStudent")
    public ResponseEntity<?> handleData(@RequestBody String[] requestData) {
        Long sId = Long.parseLong(requestData[1]);
        Long sAge = Long.parseLong(requestData[2]);
        studentManageService.addStudent(requestData[0], sId, sAge, requestData[3]);
        return ResponseEntity.ok().body(Map.of("status", "ok"));
    }
}

@CrossOrigin 允许前端(Live Server 5500 端口)跨域请求。String[] 对应前端发来的 [姓名, 学号, 年龄, 钱包地址]

Service — 调用 SDK 上链

@Service
public class StudentManageService {

    private static final String CONTRACT_ADDRESS =
        "0x464bf4acd48735a21326ed589d54620e06bbe400";

    // 初始化 SDK 连接
    BcosSDK sdk = BcosSDK.build("src/main/resources/config.toml");
    Client client = sdk.getClient(1);
    CryptoKeyPair credential = client.getCryptoSuite().getCryptoKeyPair();
    StudentManage contract;

    // 添加学生 → 写入链上
    public void addStudent(String sName, Long sId, Long sAge, String studentAddr) {
        contract = StudentManage.load(CONTRACT_ADDRESS, client, credential);
        try {
            TransactionReceipt receipt = contract.addStudent(
                sName,
                BigInteger.valueOf(sId),
                BigInteger.valueOf(sAge),
                studentAddr
            );
            System.out.println("添加成功");
            getStudent(sId);  // 添加后自动查询并打印
        } catch (Exception e) {
            System.out.println("添加失败");
        }
    }

    // 查询学生 → 从链上读取
    public void getStudent(Long sId) {
        contract = StudentManage.load(CONTRACT_ADDRESS, client, credential);
        try {
            StudentManage.Struct0 student =
                contract.getStudent(BigInteger.valueOf(sId));
            System.out.println("姓名: " + student.sName);
            System.out.println("学号: " + student.sId);
            System.out.println("年龄: " + student.sAge);
            System.out.println("地址: " + student.studentAddr);
        } catch (Exception e) {
            System.out.println("获取失败");
        }
    }
}

核心流程

load 加载合约实例 → 调用 addStudent 方法 → SDK 将调用编码为交易 → 发送到链上 → 交易上链 → 返回 TransactionReceipt

SDK 连接配置

# config.toml
[cryptoMaterial]
certPath = "conf"

[network]
peers=["127.0.0.1:20200", "127.0.0.1:20201"]

[account]
keyStoreDir = "account"
accountFileFormat = "pem"
accountFilePath = "src/main/resources/pem/admin.pem"

[threadPool]
maxBlockingQueueSize = "102400"

通过 config.toml 指定节点地址和证书路径。admin.pem 是部署合约时使用的私钥,位于 src/main/resources/pem/ 目录下。

四、运行效果

在 WeBASE-Front 或命令行中可以看到交易上链的日志输出。添加学生后,控制台会打印:

=== FISCO BCOS 连接测试 ===
添加成功
姓名: 张三
学号: 1
年龄: 20
地址: 0x1e14bf7f710eca575cfb365dc71d9b895b681c91

小结

这个项目虽然简单,但覆盖了联盟链应用开发的核心流程:

  1. 写合约 — Solidity 定义数据结构和业务逻辑
  2. 部署上链 — 通过 WeBASE-Front 或控制台部署
  3. Java SDK 对接 — Spring Boot 后端封装链上操作
  4. 前端交互 — 原生 JS 通过 REST API 调用后端

下一步可以考虑:

  • 完善美化前端界面 — 目前是最简 HTML,可以加入响应式布局、表单校验、操作反馈动画,提升用户体验
  • 完善后端 REST API — 统一返回格式、增加参数校验、补充增删改查全套接口,让 API 规范可用
  • 提升安全性 — 私钥加密存储、接口鉴权、输入过滤防注入、敏感操作二次确认,保障链上数据安全