ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于区块链的能源交易系统:智能合约开发与链上撮合实践

2026/9/17 16:42:57 拓冰建站 浏览量
基于区块链的能源交易系统:智能合约开发与链上撮合实践 简介基于区块链的能源交易系统毕业设计项目涵盖源码与完整文档说明适用于计算机、人工智能、自动化等专业学生的课程设计、大作业或毕业设计参考。该项目以Go与Node.js为后端Vue与TypeScript构建前端并包含Solidity智能合约可帮助读者理解区块链在能源交易场景中的落地流程适合具备一定开发基础的学习者二次开发或功能扩展。包内共165个文件包含后端go模块、前端vue/tsx组件、智能合约sol、样式css、json配置及项目说明文档压缩包约55.63MB目录结构清晰便于按模块查阅。目前已有103人学习下载。资源配套了手册docx、运行脚本bat、操作演示mp4等结合详细文档可快速还原项目环境适合从零梳理能源交易系统的架构设计、核心逻辑与部署细节源码均经过调试测试可在此基础上修改调整实现个性化功能。1. 基于区块链的能源交易系统毕设到底在做一件什么事分布式光伏、小型储能和充电桩接入后能源交易已经从“电网统一购售”变成“多个产消者之间的点对点买卖”。传统平台要承担记账、对账、信任背书的工作交易流程长、手续费高、数据不透明。基于区块链的能源交易系统核心就是把订单挂单、自动匹配、余额结算、交易存证搬到链上让参与方共用一套不可篡改的账本用智能合约取代人工审核。这个题目适合两类人一是还没完整做过区块链应用想用一次毕业设计把 Solidity、前端、事件索引和文档串起来的人二是已经写过简单 ERC-20 合约但没处理过“链上撮合 链下业务”边界的开发者。源码部分通常包括智能合约、部署脚本、前端页面、后端同步服务文档说明则要把架构设计、数据表、接口清单、测试结果讲完整。下面按“先定模型、再写合约、后做联调”的顺序把一套能复现的落地方案讲清楚。2. 从交易流程到智能合约能源交易的链上记账与撮合2.1 先画交易流程再写合约别急着动 Solidity写合约前我一般会先用一张流程图把参与角色和状态变化定下来。能源交易系统看起来简单实际运行时有四种角色买方产消者、卖方产消者、电网运营商、监管审计方。链上并不需要把四类角色的所有业务都搬上去只需要处理“资金安全”和“交易存证”其余信息放在链下的业务数据库里。一个最小的闭环流程是这样卖方发布售电订单写明售电量和单价买方发布购电订单写明需求量和接受价格撮合引擎按“价格优先、时间优先”匹配匹配成功后智能合约完成资金划转并生成一条不可删除的成交记录链下系统再根据成交记录做电费账单、绿证核销和电网调度申报。所以区块链在系统里承担的是“结算层”和“存证层”而不是“调度层”。明确这一点后合约的数据结构就非常清晰了核心只有三类账户余额、挂单信息、成交明细。把这三张表设计好后面所有接口都围绕它们展开。2.2 最小可运行的 Solidity 合约挂单、匹配、结算一次讲清下面是一个面向毕业设计的最小合约骨架它没有做成完整的订单簿但把能源交易最关键的“挂单—匹配—资金划转”用 Solidity 表现了出来。// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; contract EnergyTrade { enum OrderType { BUY, SELL } struct Order { OrderType orderType; address trader; uint256 energyWh; // 电量单位 Wh uint256 priceWeiPerWh; // 单价单位 Wei / Wh bool active; } // 账户的资金余额单位 Wei mapping(address uint256) public balances; // 订单序号到订单的映射 mapping(uint256 Order) public orders; uint256 public orderSeq; event OrderCreated( uint256 indexed orderId, address indexed trader, OrderType orderType, uint256 energyWh, uint256 priceWeiPerWh ); event OrderMatched( uint256 indexed buyOrderId, uint256 indexed sellOrderId, uint256 matchedWh, uint256 settleWei ); function placeOrder( OrderType orderType, uint256 energyWh, uint256 priceWeiPerWh ) external payable returns (uint256 orderId) { require(energyWh 0, amount must be positive); orderSeq 1; orders[orderSeq] Order(orderType, msg.sender, energyWh, priceWeiPerWh, true); emit OrderCreated(orderSeq, msg.sender, orderType, energyWh, priceWeiPerWh); return orderSeq; } function matchOrder(uint256 buyOrderId, uint256 sellOrderId) external { Order storage buyer orders[buyOrderId]; Order storage seller orders[sellOrderId]; require(buyer.active seller.active, order not active); require(buyer.orderType OrderType.BUY, buy id invalid); require(seller.orderType OrderType.SELL, sell id invalid); require(buyer.priceWeiPerWh seller.priceWeiPerWh, price not acceptable); uint256 matchedWh buyer.energyWh seller.energyWh ? buyer.energyWh : seller.energyWh; uint256 settleWei matchedWh * seller.priceWeiPerWh; require(balances[buyer.trader] settleWei, buyer balance insufficient); require(balances[seller.trader] settleWei balances[seller.trader], balance overflow); balances[buyer.trader] - settleWei; balances[seller.trader] settleWei; buyer.energyWh - matchedWh; seller.energyWh - matchedWh; if (buyer.energyWh 0) buyer.active false; if (seller.energyWh 0) seller.active false; emit OrderMatched(buyOrderId, sellOrderId, matchedWh, settleWei); } }这段代码的逻辑按四个要点理解。第一energyWh用“Wh”做基本单位避免小数运算如果业务上需要支持小数电量可以在前端显示层换算成 kWh 或 MWh链上始终用整数。第二priceWeiPerWh表示每一 Wh 电量的价格同样用整数。第三matchOrder是外部调用的成交入口真正的撮合算法可以放在服务器端链上只负责验证买卖价格是否可成交。第四代码里没有加手续费毕业设计通常不需要但如果你需要过网费可以在settleWei上拆分出一个手续费字段。2.3 必须定死的 5 个合约参数这个合约能跑通但离一个“可演示”的系统还有距离。在写前端和后端同步服务之前先要确定以下参数。参数名建议取值说明电量精度1 Wh所有合约计算用整数 WhKWh 换算由前端处理价格精度1 Wei / Wh避免浮点数价格上下限由业务校验最小挂单量1000 Wh防止大量小额订单刷爆区块空间订单有效期15 分钟过期订单由定时任务批量撤销成交优先级价格优先时间优先买单取价格最高者卖单取价格最低者最小挂单量和订单有效期不是写在合约里的固定常量而是由链下服务在调用placeOrder前校验。这样做的原因是毕业设计的评审老师更关心你能不能解释“参数为什么这么设”而不是合约里是否写了常数。有效期的实现也简单给Order增加一个uint256 deadline字段撮合前判断block.timestamp deadline。2.4 合约接口层前端到底要调哪几个方法完整前端不只需要placeOrder和matchOrder还需要查询接口。下面是一组常见的方法清单可以直接作为前后端开发的分工依据。方法名输入输出用途balances(address)用户地址余额判断用户是否有足够资金交易orders(uint256)订单序号订单结构体展示单笔订单详情orderSeq()无当前订单总数分页查询订单列表placeOrder(...)类型、电量、单价订单号前端提交挂单matchOrder(...)买单号、卖单号交易事件触发成交结算其中orders(uint256)只能查单笔订单想展示“所有挂单”就必须维护一个索引服务。这也是第三个章节要解决的问题链上事件怎么同步到业务数据库。3. 从源码跑通本地开发环境Ganache 上的合约部署与联调3.1 源码目录结构先搞清楚每个文件夹的作用下载一份“基于区块链的能源交易系统项目源码”后第一步不是直接npm start而是先看目录结构。毕业设计源码常见的是 Hardhat 或 Truffle 工程我惯用的目录结构是下面这样。路径作用contracts/Solidity 合约源码scripts/部署、批量结算、事件同步脚本test/合约单元测试backend/Node.js 或 Java 后端服务frontend/Vue 或 React 前端界面docs/架构图、数据库设计、接口文档hardhat.config.js编译器版本和网络配置看到源码后先检查三个文件hardhat.config.js是否配置了本地网络.env里是否有私钥占位package.json里是否包含hardhat、ethers、express等依赖。缺少任何一个程序都可能跑不起来。3.2 本地环境最小启动命令下面是一个能直接跑通的最小 Hardhat 配置不需要真实测试链本地模拟即可完成演示。npm init -y npm install --save-dev hardhat nomiclabs/hardhat-ethers ethers npx hardhat init npx hardhat nodenpx hardhat init会生成一个示例工程npx hardhat node会在http://127.0.0.1:8545启动一个本地测试链同时输出 20 个带 100 ETH 余额的测试账户。这个模式比直接用 Remix 更适合毕设因为脚本和前端都跑在同一个网络里方便联调。然后创建hardhat.config.jsmodule.exports { solidity: 0.8.20, networks: { localhost: { url: http://127.0.0.1:8545, chainId: 31337, accounts: { mnemonic: test test test test test test test test test test test junk } } } };这里的accounts使用测试助记词是为了让部署脚本和前端共用同一组地址。千万不要把真实钱包的助记词写进源码这是毕业设计代码最容易出现的低级错误。3.3 部署脚本与事件监听把链上订单同步到数据库部署脚本的核心逻辑只有三步。const hre require(hardhat); async function main() { const EnergyTrade await hre.ethers.getContractFactory(EnergyTrade); const energyTrade await EnergyTrade.deploy(); await energyTrade.deployed(); console.log(EnergyTrade deployed to:, energyTrade.address); } main().catch((error) { console.error(error); process.exit(1); });这段脚本先调用getContractFactory读取编译好的合约然后deploy()返回合约部署者对象deployed()等待交易确认。部署完成后把合约地址记录到frontend/.env或者backend/config.js前端才能通过这个地址调用合约。事件监听是很多项目源码容易忽略的部分。页面上的订单列表不可能是每次刷新都去遍历全部区块正确做法是用一个 Node.js 服务监听OrderCreated和OrderMatched把数据写入 MySQL 或 MongoDB。const { ethers } require(ethers); const { MongoClient } require(mongodb); const provider new ethers.providers.JsonRpcProvider(http://127.0.0.1:8545); const contract new ethers.Contract( process.env.CONTRACT_ADDRESS, [event OrderCreated(uint256 indexed orderId, address trader, uint8 orderType, uint256 energyWh, uint256 priceWeiPerWh)], provider ); async function syncEvent() { const db (await MongoClient.connect(process.env.MONGO_URL)).db(energy); contract.on(OrderCreated, (orderId, trader, orderType, energyWh, priceWeiPerWh, event) { db.collection(orders).updateOne( { orderId: orderId.toNumber() }, { $set: { trader, orderType: orderType 0 ? BUY : SELL, energyWh: energyWh.toNumber(), priceWeiPerWh: priceWeiPerWh.toNumber(), txHash: event.transactionHash, blockNumber: event.blockNumber } }, { upsert: true } ); }); } syncEvent();这个脚本解决了“区块上的数据如何变成业务数据”的核心问题。event.transactionHash是交易哈希blockNumber是上链区块号这两个字段要保留到数据库里作为审计追溯的关键证据。要注意orderId和energyWh可能是BigNumber直接存数据库会丢精度所以需要调用.toNumber()或.toString()。3.4 合约自动化测试把“老师会问的异常场景”写进 test 文件部署脚本跑通不代表系统可靠很多毕业设计在答辩演示时突然报错原因是没有做基本的异常测试。一个最低限度的test/energyTrade.js要覆盖三种场景正常成交、价格不接受、买家余额不足。const { expect } require(chai); describe(EnergyTrade, function () { it(should match buy and sell order when price is acceptable, async function () { const [buyer, seller] await ethers.getSigners(); const EnergyTrade await ethers.getContractFactory(EnergyTrade); const trade await EnergyTrade.deploy(); await trade.placeOrder(1, 1000, 10); await trade.placeOrder(0, 1000, 20); await expect(trade.matchOrder(1, 2)) .to.emit(trade, OrderMatched); }); });这里的placeOrder参数顺序是“类型、电量、单价”1代表 SELL0代表 BUY。测试的意义不只是让源码好看更重要的是当你修改合约后能快速判断是否破坏了原有逻辑。合理做法至少写一个npm test命令答辩演示前先跑一遍。3.5 联调阶段最常见的 4 个坑跑本地项目时报错最集中的地方不是 Solidity 语法而是环境参数不一致。我把常见的坑整理成表。错误现象原因调整方式Transaction ran out of gas合约部署或调用时 gas 设置太少在配置中把gas调大或移除手动 gas 限制Nonce too low本地测试钱包连续快速发交易nonce 未同步重启 Hardhat node使用官方ethers的 nonce 管理call revert exception前端用了错误合约地址或网络不是本地链确认部署日志中的合约地址和前端配置一致数据库里查不到订单事件事件监听在部署合约之前已经启动重启同步脚本从0区块重新扫描最后一个问题尤其隐蔽。为了避免它我会在同步脚本里记录lastSyncedBlock启动时把它改成合约部署的区块高度或直接设置fromBlock: earliest。4. 链上透明与数据隐私的取舍性能瓶颈落在哪4.1 为什么报价单不能全量上链区块链的核心优势是数据不可篡改但这也意味着链上的每条有效数据都要被所有节点复制、校验、存储。能源交易的特点是订单数量大、单笔金额小、时段性强如果把每一笔报价都直接写成合约状态变量很快会把区块塞满交易确认时间也会变长。实际项目里常见的分层办法是把“报价意图”放在链下撮合系统把“成交结果”放在链上。具体地说用户在前端填写买电或卖电订单后请求先进入一个中心化的订单池撮合引擎在内存里完成排序和匹配匹配成功的订单才打包批量提交到智能合约结算。这样既保留了区块链的可信结算又避开了全量撮合的性能瓶颈。4.2 链下撮合、链上结算一个可运行的最小匹配逻辑链下撮合算法不需要复杂它本质上是“买价从高到低、卖价从低到高”的双向队列匹配。function matchOffChain(sellOrders, buyOrders) { const sells [...sellOrders].sort((a, b) a.priceWeiPerWh - b.priceWeiPerWh); const buys [...buyOrders].sort((a, b) b.priceWeiPerWh - a.priceWeiPerWh); const results []; let i 0; let j 0; while (i buys.length j sells.length) { if (buys[i].priceWeiPerWh sells[j].priceWeiPerWh) break; const matchedWh Math.min(buys[i].energyWh, sells[j].energyWh); results.push({ buyOrderId: buys[i].orderId, sellOrderId: sells[j].orderId, matchedWh, priceWeiPerWh: sells[j].priceWeiPerWh }); buys[i].energyWh - matchedWh; sells[j].energyWh - matchedWh; if (buys[i].energyWh 0) i; if (sells[j].energyWh 0) j; } return results; }这段脚本可以放在后端服务里也可以写成一个独立的演示接口。它的运行前提是订单池已经从数据库或缓存中加载了所有未成交订单。匹配结果的每一行都会作为一笔待结算交易调用链上的matchOrder。这种方案的好处是链上只处理已经匹配成功的订单gas 消耗总量大幅下降。4.3 批量结算与 gas 预算参数表逐笔调用合约会产生大量交易每笔都要等待用户钱包确认。对于实验系统可以把撮合结果聚合成一个批量数组用脚本一次性提交到合约。参数建议值设置理由批量包大小50 笔单笔成交约消耗 5 万到 10 万 gas包太大容易触顶区块确认等待1 个区块本地演示用测试链确认等待太久影响演示节奏失败重试次数3 次避免网络抖动导致批量提交失败批量结算间隔30 秒兼顾页面更新速度和链上数据密度批量提交并不一定要改合约。你可以在后端脚本中把matchOrder的多个调用放进同一个交易序列用Promise.all发送然后统一等待交易回执。如果合约本身支持数组参数也可以把matchOrder重载成matchOrders(uint256[] buyIds, uint256[] sellIds)这样一次交易完成多笔结算更适合展示高性能。4.4 隐私保护哪些数据需要加密或不上链区块链透明是一把双刃剑。能源交易中的用户地址、成交电量、电价对普通参与者可见但部分数据如“用户真实身份对应的账号”“电表编号”属于隐私信息。毕设可以不实现加密算法但文档里要说明方案。常见做法是链上只存哈希值。比如用户真实身份信息先通过keccak256(identity salt)生成哈希再写入用户表链上账户通过公钥地址关联不直接暴露手机号或身份证号。还可以在合约里增加一个bool public isPrivate开关私有订单的匹配完全在链下完成链上只记录结算金额和成交哈希不记录所属用户。对于毕业设计做到这一步已经足够。真正的高性能方案如状态通道和侧链原理上并不复杂状态通道让参与方在链下多次更新余额只在最终关闭通道时上链侧链把高频撮合放到另一条链上定期向主链提交汇总结果。方案撮合位置最终结算位置实现成本全量链上撮合智能合约主链低但 gas 最高链下撮合链上结算后端服务主链中适合毕设状态通道通道内主链高需要设计挑战周期侧链侧链主链汇总高需运行多条链毕设答辩被问“你这个系统能支撑多少用户”时不要回答“无限”而要正面描述瓶颈区块 gas 上限、批量结算间隔、链下撮合服务的吞吐量。这比空谈性能更有说服力。5. 文档说明和演示技巧让毕设从能跑变成答辩能过5.1 文档说明里必须有的 5 张图源码能跑只能说明你完成了工程文档能说清设计依据才能让项目立住。毕业设计文档部分至少要有下面 5 张图。第一张是系统架构图画出前端、后端、智能合约、区块链网络、数据库五个模块之间的调用关系。第二张是角色用例图明确买方、卖方、管理员、审计方各自能做什么操作。第三张是交易时序图从“买家下单”到“事件同步数据库”的完整链路。第四张是合约存储结构图用表格或框图展示Order结构体、balances、orders的存储关系。第五张是部署拓扑图说明本地演示时需要启动的几个进程Hardhat node、后端 API、事件同步服务、前端页面。5.2 答辩演示时的 4 个验证步骤演示最怕的是“点两下界面就说成功”评审看不到数据变化。我会按下面的顺序演示每一步都有明确的链上证据。步骤操作预期结果1打开前端创建两个测试账户两个账户显示初始余额2卖方挂单 1000 Wh单价 15买方挂单 1000 Wh单价 20订单列表出现两条未成交记录3触发自动撮合订单列表状态变为“已成交”双方余额发生变化4打开区块链浏览器或npx hardhat console查询成交事件能看到OrderMatched事件和交易哈希第 4 步是最能拉开完成度差距的地方。提前在脚本里打印交易哈希并把哈希粘贴到文档演示页评审点开就能看到原始成交记录这比截图管用得多。5.3 用一张数据记录表串联源码和文档文档说明不需要把所有代码贴进去但需要保留一份演示数据记录表证明系统是可复现的。区块高度交易哈希卖方账户买方账户成交量(Wh)成交单价结算金额状态12840x8f2a...0x3B24...0xA71c...10001515000 Wei成功13010x9c1e...0x3B24...0xC0ff...500189000 Wei成功这张表可以和事件监听脚本的输出对应上。答辩前重新跑一次npm run sync把表里的交易哈希换成当天生成的新哈希既真实又能体现工作量。最后的落地技巧是把整个演示流程写成一个demo.sh脚本依次执行“启动 localhost 节点、部署合约、启动事件同步、启动前端”这样即使现场网络环境有问题也能在 30 秒内恢复演示。把脚本里的关键输出比如合约地址和交易哈希同时打印到终端和本地日志文件方便随时回放链上记录。本文还有配套的精品资源点击获取