基于 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);
交互流程
点击按钮 → 获取四个输入框的值 → 放进数组 →
fetchPOST 到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
小结
这个项目虽然简单,但覆盖了联盟链应用开发的核心流程:
- 写合约 — Solidity 定义数据结构和业务逻辑
- 部署上链 — 通过 WeBASE-Front 或控制台部署
- Java SDK 对接 — Spring Boot 后端封装链上操作
- 前端交互 — 原生 JS 通过 REST API 调用后端
下一步可以考虑:
- 完善美化前端界面 — 目前是最简 HTML,可以加入响应式布局、表单校验、操作反馈动画,提升用户体验
- 完善后端 REST API — 统一返回格式、增加参数校验、补充增删改查全套接口,让 API 规范可用
- 提升安全性 — 私钥加密存储、接口鉴权、输入过滤防注入、敏感操作二次确认,保障链上数据安全