新闻详情

新闻详情

首页 / 资讯中心 / 详情

区块链电子投票防篡改实战:Spring Boot与Vue构建可审计系统

发布时间:2026/10/2 10:47:14来源:尧图网络
区块链电子投票防篡改实战:Spring Boot与Vue构建可审计系统
简介一套基于Java与Vue的区块链电子投票防篡改系统设计文档面向具备Java和Vue开发基础的软件工程师、全栈开发者及区块链技术爱好者适用于学校选举、社区自治、企业股东会、政府公共事务决策等需要高可信度投票的场景。文档系统涵盖项目背景与目标、技术架构、核心功能模块、数据库设计、前后端代码实现及部署方案并通过区块类Block、区块链类Blockchain、SHA-256加密工具类、投票数据模型、智能合约判重与自动计票机制等代码示例完整展示前后端交互逻辑与区块链底层实现。资源共1个docx文档压缩包约90KB目录结构清晰从项目背景、挑战及解决方案到项目模型架构、核心算法、代码详解循序渐进便于按模块查阅。已有80人学习浏览可作为二次开发的开源模板用于构建定制化的去中心化投票或数据存证系统。1. 电子投票防篡改到底难在哪先想清楚要防谁单位搞职代会投票结果公布后总有人质疑“这票到底有没有人动过”。数据库里 UPDATE 一条记录太容易了查日志也查不出所以然。这正是电子投票最尴尬的地方系统能跑但给不出“没人改过”的证据。区块链这套账本结构天然就是为了这个场景设计的——选票打包进带哈希链的区块改任何一笔都会让后续所有区块校验失败。再加上 Java 和 Vue 做工程落地Spring Boot 负责出块、校验和事务管理Vue 负责投票界面和链数据的可视化前后端分离既能跑通一条完整的投票流程又能随时把“防篡改”这件事现场演示给质疑的人看。这套方案特别适合三类人要交课设但缺完整工程的在校生、需要给内部投票或调查系统加上“可审计能力”的后端工程师以及想弄明白区块链到底怎么接进业务系统而不是只看科普的开发。2. 从投票场景到区块结构账本、共识与数据库表怎么分工2.1 一次投票要经过哪些环节先把流程走一遍不管链上怎么玩业务上你都需要先回答一个问题选票从哪来、到哪去、谁能看到。常见做法是管理员建选举、导入候选人和选民名单系统给每个选民发放一次性投票券选民登录后凭券投出选票选票先进待确认池出块节点把池子里的选票打包成区块区块追加到本地链前端通过轮询把“已确认”状态反馈给选民。这套流程最容易被忽略的是链上保存的不是明文选民 ID而是投票券的 SHA-256 哈希。这样即使数据库被拖走也没法把某张票直接对应到某个真实姓名匿名性保住了。而防篡改靠的是区块头里的哈希链和 Merkle 根跟匿名没有冲突。整体上我用的是“链上只存投票事务、链下存业务配置”的分工方式。选民白名单、候选人名单、选举项目配置这些会变动又不敏感的静态数据放数据库里就好每笔选票事务一旦上链就永不修改。这个分工决定了后面所有表结构和接口设计所以先想清楚再动手比先写代码再补架构要省事得多。2.2 区块头里放了什么字段拆开看链上最小的可校验单位是区块。演示项目里区块头不需要模仿比特币那么大留这几个字段就够了字段类型说明indexint区块高度创世区块为 0timestamplong出块时间epoch 毫秒previousHashString前一区块的 SHA-256 哈希merkleRootString该区块所有投票事务的 Merkle 根hashString当前区块哈希generatorString出块节点标识多节点时有用previousHash 是整条链的校验主线。生成新块时上一块的 hash 会参与当前块 hash 的计算只要历史任意一个区块被改动从它之后每一块的 previousHash 都会对不上校验接口一跑马上定位到断裂位置。区块自身 hash 的计算我固定成一种拼接方式index:timestamp:previousHash:merkleRoot。这里要特别注意拼接格式必须是全项目统一的常量否则前端、后端、测试脚本各算各的最终校验结果永远对不上。后续章节写代码时我会再强调一次。2.3 投票事务与 Merkle 根打包选票时要做的事一个区块可以包含多笔投票事务。为了不把整块事务都放进区块头我们把事务列表压缩成一个 Merkle 根。构造方法很机械先把事务按 txId 排序然后两两做 SHA-256逐层向上合并直到只剩一个根字符串。为什么要先排序因为同一批事务不同顺序算出的根完全不同。出块时事务从数据库查出来若不强制 ORDER BY tx_id两次出块得到的顺序就可能不一致一旦不一致Merkle 根就变了整条链的确定性就被破坏。这是我在多个项目里踩过的实际坑先在这里埋个伏笔第 5 章会展开讲。工程实现上演示项目不需要真把 Merkle 树完整建出来。你只需要一层一层两两合并最终拿到一个 root 字符串即可。只有在要做轻节点证明时才需要保存完整的树层级结构。2.4 数据库建表block、vote_tx、voter、candidate 怎么落库数据库在整个系统里负责两部分一是保存链本身二是保存投票业务数据。链上数据要出问题能定位业务数据要能支持幂等约束。下面是演示项目里最常用的一套建表 SQLCREATE TABLE block ( block_index INT PRIMARY KEY, block_hash VARCHAR(64) NOT NULL, previous_hash VARCHAR(64) NOT NULL, merkle_root VARCHAR(64) NOT NULL, timestamp BIGINT NOT NULL, generator VARCHAR(32) DEFAULT node1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE vote_tx ( tx_id VARCHAR(36) PRIMARY KEY, election_id INT NOT NULL, voter_hash VARCHAR(64) NOT NULL, candidate_id INT NOT NULL, block_index INT, tx_status TINYINT DEFAULT 0 COMMENT 0-待打包 1-已上链, timestamp BIGINT NOT NULL, UNIQUE KEY uk_voter_election (voter_hash, election_id), KEY idx_block (block_index) ); CREATE TABLE voter ( voter_id INT PRIMARY KEY, voter_name VARCHAR(50), voucher_hash VARCHAR(64) UNIQUE ); CREATE TABLE candidate ( candidate_id INT PRIMARY KEY, candidate_name VARCHAR(50), election_id INT NOT NULL );voter_hash 和 election_id 的联合唯一索引是最后一道闸门。即使前端重复点击、后端重复收到请求数据库层面也会拒绝第二次插入保证同一场选举里同一个投票券只能投一次。block 表里的 block_hash 和 vote_tx 表里的 block_index 不设外键原因很简单链校验逻辑要能在发现篡改时快速定位是哪一块坏了而不被数据库约束挡住。存储上让两者保持松耦合校验逻辑完全交给 service 层计算。2.5 共识选型演示项目为什么不挖矿PoW 在教学上有名但用在投票场景其实很浪费。内部系统常见的做法是配置一个受信任的出块节点设定“每满 N 笔打包一次”或“每 10 秒打包一次”校验时用最长链规则选取主链。做课设或小型内部投票这套结构比 PoW 更可控。不用太纠结去中心化程度。区块链在投票里的价值主要是不可篡改加可审计而不是去中心化投票。这个定位想清楚选型就会踏实很多出块节点可以只有一个校验逻辑可以开放给任意客户端信任边界放在“出块节点不造假”上而“上链之后没人能改”由哈希链保证。3. Java 后端落地在 Spring Boot 里实现区块生成与链校验3.1 项目骨架与依赖先把 Spring Boot 工程搭出来Java 端我习惯用 Spring Boot 做工程骨架版本 2.7 或 3.x 都行关键是依赖要精简。核心依赖就三个spring-boot-starter-web、spring-boot-starter-data-jpa、对应数据库驱动。演示项目用 H2 内存库跑会很方便生产换 MySQL 只要改连接配置。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdcom.h2database/groupId artifactIdh2/artifactId scoperuntime/scope /dependency工程目录我按 controller / service / entity / repository / util 分包这是常规写法不花哨但每个人接手都能快速找到对应代码。Block、VoteTx、Voter、Candidate 四个实体类对应第 2 章的四张表用 JPA 注解映射即可。3.2 区块实体与 SHA-256 哈希工具先给区块建实体类并写一个统一的哈希工具。这是整条链的地基必须保证计算方式唯一。Entity Table(name block) public class Block { Id Column(name block_index) private Integer index; Column(name block_hash) private String hash; Column(name previous_hash) private String previousHash; Column(name merkle_root) private String merkleRoot; Column(name timestamp) private Long timestamp; Column(name generator) private String generator; }public class HashUtil { private static final String HEX_CHARS 0123456789abcdef; public static String sha256(String data) { try { MessageDigest md MessageDigest.getInstance(SHA-256); byte[] digest md.digest(data.getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(digest.length * 2); for (byte b : digest) { sb.append(HEX_CHARS.charAt((b 4) 0x0F)); sb.append(HEX_CHARS.charAt(b 0x0F)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(SHA-256 algorithm not available, e); } } public static String calculateBlockHash(int index, long timestamp, String previousHash, String merkleRoot) { return sha256(index : timestamp : previousHash : merkleRoot); } }这里的核心是 calculateBlockHash 的拼接格式。四个字段的顺序一旦定下来所有地方都要沿用包括第 6 章要写的验证脚本。如果你在别处用 JSON 字符串参与哈希计算那 JSON 序列化顺序不同会造成哈希结果完全不一样这是最容易翻车的地方。时间戳参数用的是 long不是 Date。原因有两个一是 epoch 毫秒在前后端之间传输和比较都最可靠二是出块逻辑里要校验时间戳严格递增long 直接做大小比较比 Date 序列化干净得多。3.3 投票事务生成与幂等校验投票事务是链上的最小数据单元每个事务代表一张选票。事务生成时的重点是匿名化和幂等校验。Service public class VoteService { Autowired private VoteTxRepository voteTxRepository; public VoteTx createVoteTx(String voucher, Integer electionId, Integer candidateId) { String voterHash HashUtil.sha256(voucher); if (voteTxRepository.existsByVoterHashAndElectionId(voterHash, electionId)) { throw new ResponseStatusException(HttpStatus.CONFLICT, 该投票券已投过票); } VoteTx tx new VoteTx(); tx.setTxId(UUID.randomUUID().toString()); tx.setElectionId(electionId); tx.setVoterHash(voterHash); tx.setCandidateId(candidateId); tx.setTimestamp(System.currentTimeMillis()); tx.setTxStatus(0); return voteTxRepository.save(tx); } }逻辑说明投票券 voucher 是一次性的投票人从前端输入框提交后后端只保存 voucher 的哈希不保存明文。这样即使数据库泄露也无法直接把票跟人对应起来。existsByVoterHashAndElectionId 是业务层的第一道幂等校验数据库唯一索引是第二道兜底。参数上说明两个点candidateId 合法性的校验可以放在进入待打包池之前避免无效选票进链timestamp 这里记录的是选民提交时间跟出块时间不是一回事区块的 timestamp 在出块时另外生成。这两者分开会省去很多后端和前端的时间错乱问题。3.4 出块逻辑把待确认事务打包成区块出块是整个系统里最像“核心机制”的部分。逻辑是从待确认事务池里取所有状态为 0 的事务排序后计算 Merkle 根再基于上一个区块计算新哈希。Service public class BlockchainService { Autowired private BlockRepository blockRepository; Autowired private VoteTxRepository voteTxRepository; public synchronized Block generateBlock() { ListVoteTx pendingTxs voteTxRepository.findByTxStatus(0); if (pendingTxs.isEmpty()) { return null; } pendingTxs.sort(Comparator.comparing(VoteTx::getTxId)); ListString txDataList pendingTxs.stream() .map(tx - tx.getTxId() : tx.getVoterHash() : tx.getCandidateId()) .collect(Collectors.toList()); String merkleRoot MerkleUtil.buildMerkleRoot(txDataList); Block lastBlock blockRepository.findTopByOrderByIndexDesc(); int newIndex lastBlock.getIndex() 1; long timestamp System.currentTimeMillis(); String hash HashUtil.calculateBlockHash( newIndex, timestamp, lastBlock.getHash(), merkleRoot); Block newBlock new Block(); newBlock.setIndex(newIndex); newBlock.setTimestamp(timestamp); newBlock.setPreviousHash(lastBlock.getHash()); newBlock.setMerkleRoot(merkleRoot); newBlock.setHash(hash); newBlock.setGenerator(node1); blockRepository.save(newBlock); pendingTxs.forEach(tx - tx.setBlockIndex(newIndex)); pendingTxs.forEach(tx - tx.setTxStatus(1)); voteTxRepository.saveAll(pendingTxs); return newBlock; } }这段代码有两个细节值得反复看。第一个是 synchronized 关键字它保证同一时刻只有一个线程能出块。并发投票时如果不加锁两个请求可能同时读到同一个 lastBlock生成两个 previousHash 相同的“兄弟区块”链就分叉了。第二个是 pendingTxs 按 txId 排序这保证同一批事务无论从哪个入口拿出来计算出的 Merkle 根都一致。MerkleUtil.buildMerkleRoot 的实现很朴素先把字符串列表复制一份当长度大于 1 时相邻两项拼起来做 sha256长度是奇数就把最后一项直接上提直到剩一个值。这个算法不需要造树对象代码量小且可读。3.5 链校验逻辑把“防篡改”变成一个接口有了区块和事务防篡改能力最终要落到一个可验证的方法上。校验逻辑从创世区块开始逐个区块往前算。public ChainVerifyResult verifyChain() { ListBlock blocks blockRepository.findAllByOrderByIndexAsc(); for (int i 1; i blocks.size(); i) { Block prev blocks.get(i - 1); Block cur blocks.get(i); String expectedHash HashUtil.calculateBlockHash( cur.getIndex(), cur.getTimestamp(), prev.getHash(), cur.getMerkleRoot()); if (!expectedHash.equals(cur.getHash())) { return new ChainVerifyResult(false, i, 区块哈希与内容不匹配疑似被篡改); } if (!cur.getPreviousHash().equals(prev.getHash())) { return new ChainVerifyResult(false, i, 区块链接断裂); } } return new ChainVerifyResult(true, null, 链完整); }这里有两个检查点。第一根据当前区块内容重新计算哈希跟存储的 hash 对比能发现“区块内容被改”的情况第二检查当前区块的 previousHash 是否等于上一区块的 hash能发现“链被重新拼接”的情况。两者都过了才能认为整条链到目前为止是可信的。需要提一个边界verifyChain 只能验证“从创世区块开始哈希是否一致”它不能防止管理员在出块时故意放一个错误的时间戳或选票。因为出块节点本身受信任所以这个模型里“防篡改”防的是上链之后的数据改动。如果出块节点也被攻破那就需要换共识算法例如 PBFT 或权威证明那是另一个量级的工程问题。3.6 REST API 一览前端要的接口就五个后端只要暴露五个接口前端就能跑完整的投票加验证流程方法路径参数返回说明POST/api/votevoucher, electionId, candidateId返回 txId 和状态GET/api/tx/{txId}路径参数返回事务详情与区块索引GET/api/blocks无返回全部区块GET/api/blocks/{index}/transactions路径参数返回指定区块内的事务列表GET/api/chain/verify无返回校验结果和可疑区块索引Controller 层不要写业务逻辑直接调 service。比如投票接口就是这样PostMapping(/api/vote) public VoteTx vote(RequestBody VoteRequest req) { return voteService.createVoteTx(req.getVoucher(), req.getElectionId(), req.getCandidateId()); }这也是 Spring Boot 前后端分离项目里最常见也最稳妥的写法。接口返回结构我统一用对象本身加 HTTP 状态码错误场景用 ResponseStatusException 抛出去前端 axios 能直接拿到 message 展示给用户。4. Vue 前端与 GUI 设计投票页和区块链浏览器合二为一4.1 项目骨架Vite Vue3 Element Plus前端我用 Vite 搭 Vue3 工程组件库选 Element Plus再加 axios 和 dayjs 两个小依赖。Vite 比 Vue CLI 更轻启动快Element Plus 的表格、表单、消息提示能直接拼出后台类界面特别适合投票项目这种“表单提交加列表展示”的场景。npm create vitelatest voting-ui -- --template vue cd voting-ui npm install element-plus axios dayjs vue-router npm run dev说明create vite 生成的是 Vue3 工程Element Plus 需要全量引入或按需引入。演示项目全量引入就行省配置。dayjs 用来处理时间戳格式化后面联调时会发现它是必需品。4.2 路由设计投票页和链浏览器页各司其职整个前端就两个主页面外加一个详情页但路由依然值得认真设计。投票页负责提交选票链浏览器页负责展示区块和事务验证结果可以内嵌在链浏览器页里。import { createRouter, createWebHistory } from vue-router const routes [ { path: /, redirect: /vote }, { path: /vote, component: () import(../views/VoteView.vue) }, { path: /chain, component: () import(../views/ChainView.vue) }, { path: /tx/:txId, component: () import(../views/TxDetailView.vue) } ] export default createRouter({ history: createWebHistory(), routes })路由这里有一个实用点/tx/:txId 是动态路由点击表格里的事务 ID 能跳到详情页vue-router 用 $route.params.txId 取参数。这种“列表进详情”的模式在投票系统里比弹窗更清晰浏览器前进后退都自然。4.3 投票页提交选票并回显“待确认 / 已上链”投票页的关键交互是用户输入投票券、选择候选人和选举项目点击提交后看到自己这笔票的状态变化。状态从待打包到已上链全部由后端返回。template el-form label-width100px el-form-item label投票券 el-input v-modelvoucher placeholder请输入一次性投票券 / /el-form-item el-form-item label选举项目 el-select v-modelelectionId placeholder选择选举 el-option :value1 label2024年度职代会 / /el-select /el-form-item el-form-item label候选人 el-radio-group v-modelcandidateId el-radio :value101候选人A/el-radio el-radio :value102候选人B/el-radio /el-radio-group /el-form-item el-button typeprimary clicksubmitVote提交选票/el-button el-tag :typestatus CONFIRMED ? success : warning {{ statusText }} /el-tag /el-form /template script setup import { ref } from vue import axios from axios const voucher ref() const electionId ref(null) const candidateId ref(null) const status ref() const txId ref() let timer null async function submitVote() { const resp await axios.post(/api/vote, { voucher: voucher.value, electionId: electionId.value, candidateId: candidateId.value }) txId.value resp.data.txId status.value PENDING timer setInterval(checkStatus, 3000) } async function checkStatus() { const resp await axios.get(/api/tx/${txId.value}) if (resp.data.txStatus 1) { status.value CONFIRMED clearInterval(timer) } } /script轮询是个刻意选择的方案。WebSocket 当然更实时但在这个项目里轮询 3 秒一次的代价很低实现简单任何人都能看懂。前端拿到 txStatus 为 1 就表示这笔事务已经进入区块并且整条链通过校验。提交按钮这里要防重复点击否则同一个投票券会连续提交两次。前端要做的只是一行 disabled 绑定真正的兜底约束还是后端那张唯一索引表。这个点会在第 5 章再展开。4.4 链浏览器页区块列表与一键校验链浏览器页是这套系统最能体现“防篡改”的设计左侧展示区块列表右侧展示选中区块里的事务顶部放一个“校验整条链”按钮。template el-row el-col :span14 el-table :datablocks row-clickloadTransactions el-column propindex label区块高度 width100 / el-column proptimestamp label出块时间 width180 / el-column prophash label区块哈希 show-overflow-tooltip / /el-table /el-col el-col :span10 el-button typedanger clickverifyChain校验整条链/el-button el-alert v-ifverifyResult :typeverifyResult.valid ? success : error :titleverifyResult.message / el-table :datatxs el-column proptxId label事务ID show-overflow-tooltip / el-column propcandidateId label候选人 width80 / /el-table /el-col /el-row /template script setup import { ref, onMounted } from vue import axios from axios const blocks ref([]) const txs ref([]) const verifyResult ref(null) async function loadChain() { const resp await axios.get(/api/blocks) blocks.value resp.data } async function loadTransactions(row) { const resp await axios.get(/api/blocks/${row.index}/transactions) txs.value resp.data } async function verifyChain() { const resp await axios.get(/api/chain/verify) verifyResult.value resp.data } onMounted(loadChain) /script校验按钮是整个 GUI 里最能打动演示对象的功能。点击之后后端从头开始重新算每个区块的哈希只要有人改过库里的任何一条选票返回里就会带上“第几块哈希不匹配”。前端用一个红色 alert 呈现出来比任何解说都有说服力。4.5 前后端联调代理配置和时间戳格式化前端工程默认跑在 5173 端口后端跑在 8080开发环境下最常见的做法是在 Vite 里配代理而不是在后端开全局 CORS。export default { server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }解释一下为什么不用 CORS全局允许跨域会让生产环境多一个暴露面而且一旦涉及带凭证的请求浏览器会强制校验 Credentials 与 Allow-Origin 的匹配调试起来更麻烦。开发环境用代理生产环境把前端 build 产物放到 Nginx 里反代到后端这是最规范的链路。时间戳也是一个联调里最容易翻车的细节。后端返回的 timestamp 是 epoch 毫秒前端在模板里直接显示会是一串数字。正确做法是用 dayjs 格式化import dayjs from dayjs function formatTime(ts) { return dayjs(ts).format(YYYY-MM-DD HH:mm:ss) }这个习惯能直接避免第 5 章要写的“时间差了 8 小时”问题。5. 避坑与排查5 个让投票系统翻车的真实问题5.1 修改系统时间导致区块时间戳乱序现象管理员或运维为了校准时间改了服务器系统时钟之后新生成的区块 timestamp 比前一个区块小前端链浏览器里区块列表顺序看起来是乱的甚至有的轮询脚本会误判为校验失败。原因出块代码用了 System.currentTimeMillis()它取的是本机系统时间。系统时间一旦被拨回去新块时间戳就小于旧块。区块链只保证哈希链完整不保证业务层时间展示顺序合理。解决在生成新区块前增加时间戳校验。常见的兜底做法是判断当前时间是否小于上一个区块的 timestamp如果是就强制把新块时间戳设成上一区块时间戳加 1 毫秒。这个处理同样适用于多节点部署时节点间时钟不同步的情况。long now System.currentTimeMillis(); long lastTs lastBlock.getTimestamp(); if (now lastTs) { now lastTs 1; }别小看这一行。投票系统的时间戳经常要参与对账和审计时序乱了整个链条的“可审计性”就会被打折扣。5.2 并发投票触发哈希竞态链分叉现象两个选举人几乎同时点击“提交选票”后端两个请求都触发出块逻辑最后生成两个区块它们的 previousHash 指向同一个父区块。前端一查发现链有两棵分支verify 接口只验证主链部分选票“消失”在分支里。原因出块方法没有加锁。两个线程同时执行 generateBlock()都读取了同一个 lastBlock然后各自算出一个新的 hash这两个区块内容不同但父区块相同形成分叉。解决给 generateBlock() 加上 synchronized。这个锁粒度要覆盖“读取 lastBlock”到“保存新块”的整个过程而不是只锁一行代码。如果你用了分布式部署进程间还需要引入分布式锁课程设计和单机演示用 synchronized 就足够。5.3 Merkle 树叶子顺序不一致导致校验失败现象同一批事务第一次出块时 merkleRoot 算出来是 a第二次重新从一个备份数据库恢复后重跑相同事务merkleRoot 变成了 b。链上所有后续区块哈希全部校验失败但数据内容看起来没有任何变化。原因事务列表的查询没有强制排序。数据库在没有 ORDER BY 的情况下返回的行序不保证稳定不同的查询计划、不同的索引选择可能让同一批数据顺序不同。Merkle 树对顺序极其敏感顺序一变根就变。解决出块和校验两端都必须基于同一个稳定排序构造 Merkle 树。我用的是 tx_id 排序因为它全局唯一不会出现并列。如果你的系统用自增 ID 也没问题关键是显式排序且全项目统一。5.4 前端时间显示差 8 小时现象浏览器里投票时间比北京时间晚 8 小时或者早上投的票显示成前一天晚上。查看后端返回的 JSONtimestamp 字段是一串数字前端直接 new Date(timestamp) 又转了一次时区把偏移量重复计算了。原因Java 端把 Date 序列化默认转成 UTC 格式或者前端把已经带时区的字符串再当成无时区字符串解析。核心问题是前后端没有约定统一的时间传输格式。解决全链路用 epoch 毫秒传不在 JSON 里传日期字符串。前端拿到后统一用 dayjs(ts).format(YYYY-MM-DD HH:mm:ss) 显示。这样时区只跟浏览器本地设置有关不会发生二次偏移。5.5 重复点击导致重复投票数据库兜底没生效现象同一个投票券短时间被提交两次待确认池里出现两笔 voter_hash 和 election_id 相同的记录。后一次提交时业务层的 existsByVoterHashAndElectionId 校验没有拦住因为第一次事务还没提交第二次请求已经在另一个线程里做查询。原因这是典型的“先查后插”竞态。两条并发请求同时查到“不存在”然后又同时插入业务层防重在并发下失效。如果你只写了业务层判断没建唯一索引就会出现这种漏网之鱼。解决数据库表上必须建联合唯一索引靠数据库强约束兜底。插入时捕获 DuplicateKeyException转成业务提示“该投票券已投过票”。前端的按钮 disable 只是体验优化真正防重复的是数据库。ALTER TABLE vote_tx ADD UNIQUE KEY uk_voter_election (voter_hash, election_id);这个案例也适合当作 Java 面试八股文里“并发下幂等怎么做”的现实答案接口防重只是第一层唯一索引才是最后防线。6. 防篡改验证技巧写一个能当场抓篡改的回归测试这节讲一个我最常用的验证手段人为改数据库里的一条选票再调用校验接口看系统能不能精准报出是哪一块的数据出了问题。这个方法很适合写进自动化回归测试每次改完代码跑一遍能直接证明防篡改链路没有失效。第一步是准备一条有效测试数据跑通完整投票流程让系统生成 3 到 5 个区块记下最后一笔事务的 txId 和所在区块索引。这一步可以用接口自动化完成也可以用 Cypress 这类前端测试工具驱动 GUI 点一遍。关键是后续对比依赖这条基准数据。第二步是篡改。在数据库里直接执行一条 UPDATE把其中一笔票的目标候选人改掉mysql -u root -p -e UPDATE vote_tx SET candidate_id candidate_id 1 WHERE tx_id a30f5c2e-8c1e-4b3a-9d1e-6284a1f8b2c3; 第三步是调用校验接口预期返回结果里 valid 为 false并且篡改索引指向该事务所在区块。如果你改的是已上链事务校验接口会从那个区块开始一直报错到最后因为后续所有区块的 previousHash 都对不上。你可以写一个简单的 shell 断言VERIFY$(curl -s http://localhost:8080/api/chain/verify) if [[ $VERIFY *false* ]]; then echo 防篡改检测生效 else echo 检测失败请检查校验逻辑 exit 1 fi第四步是把数据改回去再次调用校验接口应该恢复 valid 为 true。这一步容易忽略但放到自动化回归测试里异常重要测试结束后必须恢复现场否则下一次跑测试会拿“脏库”做基准。我再补充一个能写进接口测试的细节校验接口不仅要返回 true/false还应该返回可疑区块索引。有了索引测试断言可以写得非常精确而不是只判断一个布尔值。这个设计在第 3 章的 verifyChain 里已经预留了返回结构里包含 index 字段前端那红色 alert 也是靠它显示“第 3 块被篡改”。这套验证方法的价值在于它把“区块链防篡改”从概念变成可演示、可测的行为。每次我给这类投票系统做交付或评审都会保留这个测试脚本作为最终验收项。它不复杂但能拦住大多数“改了数据但忘记改哈希”的低级错误。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Themida 2.3.9.0 实战指南:Windows 二进制保护与硬件绑定授权 2026/10/2 11:45:51

Themida 2.3.9.0 实战指南:Windows 二进制保护与硬件绑定授权

简介:本资源为Themida 2.3.9.0中文多语免费版程序加密保护工具,面向软件开发者、逆向工程学习者及安全防护实践者,解决商用软件试用版与完整版的防破解、反调试、防内存转储等核心安全分发难题。压缩包共298个文件,含72个inc头文件…

阅读更多 →
2026年AI写作辅助平台盘点:用TaoToken统一Key打通12款神器,高效完成选题大纲、撰稿和降重 2026/10/2 11:45:45

2026年AI写作辅助平台盘点:用TaoToken统一Key打通12款神器,高效完成选题大纲、撰稿和降重

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
离线安装 K3S 实战:用 TaoToken 统一 Key 打通 Helm、MySQL 与 Longhorn 的本地验证链路 2026/10/2 11:45:45

离线安装 K3S 实战:用 TaoToken 统一 Key 打通 Helm、MySQL 与 Longhorn 的本地验证链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
基于AndroidX的跑步App毕设源码:从解压到联调避坑指南 2026/10/2 11:45:45

基于AndroidX的跑步App毕设源码:从解压到联调避坑指南

简介:在Android原生开发中,androidx作为Google推出的兼容层,已成为新项目的基础依赖。它将旧support库重构,统一了组件包名与行为,让App在不同系统版本间保持一致体验。实际工程中,一个完整的跑步类应用通常…

阅读更多 →
【AI智能体报告】OpenManus 实战拆解:从 MetaGPT 到开源 AI 助手的落地路径与 TaoToken 接入 2026/10/2 11:45:44

【AI智能体报告】OpenManus 实战拆解:从 MetaGPT 到开源 AI 助手的落地路径与 TaoToken 接入

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
大模型垂直领域微调系列(二):ms-swift 框架全景与 TaoToken 统一接入实践 2026/10/2 11:45:44

大模型垂直领域微调系列(二):ms-swift 框架全景与 TaoToken 统一接入实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉