☰
区块链医疗记录存储系统:哈希上链与授权访问设计
2026/10/10 11:02:20 网站建设 项目流程

简介:基于区块链的医疗记录存储系统毕业设计项目,面向计算机相关专业正在完成毕业设计、课程设计或期末大作业的学生,也适合需要区块链实战项目的学习者。项目经导师指导并通过认可,源码均经过严格调试,能够直接运行,可整体作为毕设交付方案使用。资源包共207个文件,约11.3MB,包含Java、Go等后端源码,yaml、properties、xml等工程配置,SQL数据库脚本,以及大量pem、crt、key、priv_sk等区块链节点证书与密钥文件;同时附带答辩PPT、论文报告、中期报告、任务书等材料,覆盖从开题到答辩的完整流程。当前已有275人学习下载,适合作为医疗信息化与区块链技术结合方向的完整参考案例。读者可按目录结构对照源码和文档,理解系统架构、链上数据存储流程与共识机制部署细节,也可将其中配置、脚本和排错要点迁移到同类项目中。

1. 基于区块链的医疗记录存储系统:这个毕设项目到底要解决什么

你在医院调阅自己的病历是不是经常被拒绝?转诊时要带一堆胶片和报告,换一家医院就要重新检查一遍。这个课题想做的事情,就是把医疗记录的“所有权”和“可信度”还给你——原始文件保存在链下,文件和操作者信息经过处理后再上链,之后谁看过这份记录、有没有被篡改,都能被追溯。

用区块链把每条记录的指纹、上传者、授权关系和时间戳写到链上,原始文件本身放在链下存储,再通过哈希比对保证一致性。对于做毕设的人,它最大的价值是既能展示区块链落地,又有明确的工程链路:前端上传、后端处理、链码读写、IPFS存储、哈希校验。源码包里的答辩PPT和论文报告只是锦上添花,关键的还是你自己把这套链路讲清楚。

这个课题适合手里有源码但还没跑通的人,也适合刚拿到选题、打算从零搭一遍的人。下面从一个做过区块链应用开发的角度,拆一套能复现的落地流程,并把这些在你眼前可能遇到的坑标出来。

2. 为什么医疗记录存储要用区块链:核心思路与选型理由

2.1 链上存什么、链下存什么:哈希上链与原始数据分离

“把病历和CT影像直接写进区块链”是我看过最多翻车的设计。区块链的账本在每一个peer节点都存一份,假设一份影像有30MB,四个节点就要复制120MB,而且每次写都要经过背书、排序、提交一堆流程,网络很快就扛不住了。更现实的理由是成本:在以太坊主网上存几十字节的哈希只要几美元,存一张图片要花掉一个让普通用户肉疼的数字。医疗虽然常走联盟链,不存在公链gas,但账本膨胀带来的存储和性能问题是一样的。

所以这个系统的核心思路是“哈希上链、原件下放”。给原始文件算一个SHA-256摘要,把摘要、记录ID、患者ID、上传医生、记录类型、时间戳作为元数据写入链码状态。原始文件通过IPFS存到链下,或者在局域网里挂一个MinIO。查询时客户端重新计算文件哈希,和链上哈希做比较,一致就说明没有被篡改。这个做法在所有区块链存证类项目里都是底线,区块链溯源系统代码里也基本都是这个思路。

这里有个容易被忽略的边界:哈希比对只能证明“从上传之后没有被改过”,不能证明“上传时它就是真实病历”。医生在造假的数据上正常算哈希,校验一样能通过。所以论文里必须补上身份认证环节,例如用医疗机构签发的证书做背书,或者在链码里记录上传者的证书标识。答辩时把这条边界主动讲出来,比被老师问到再解释要体面得多。

2.2 联盟链还是公链:Hyperledger Fabric 是毕设的主流选择

公链完全公开,每个节点都能看到交易内容,这在医疗记录场景里是致命问题。虽然可以给数据加密再上链,但密钥怎么发、怎么回收、链上能不能完成解密授权,都会把简单问题复杂化。医疗记录需要的是“授权后可访问”,不是“公开可验证”。联盟链天然设置了准入门槛,只有带着合法证书的机构和用户才能加入网络。因此,多数医疗存证毕设都选择Hyperledger Fabric,还有少数用Ethereum私有链或WeDP。

Fabric赢在三点:成员服务(MSP)单独抽出来,由Fabric CA负责身份签发;支持通道隔离,不同机构可以到不同账本;链码的背书策略可以指定多家医院共同签名才有效。这三点对应到系统设计里分别是:谁有资格接入、谁能看到哪些账本、什么样的记录算机构间认可。写论文时把这三个问题讲清楚,区块链部分就不会被当成“套壳”。

当然,Fabric的学习曲线也确实陡峭。先要装Docker和docker-compose,再拉镜像,还要会操作peer、orderer容器。很多同学在network.sh up这一步就卡住,然后放弃。我的建议是不要一开始看源码,先用官方示例把网络跑起来,理解peer、orderer、CA三条链路,再回来读项目里的链码,否则你只会改几个状态值,答辩时一追问就露馅。

我一般会让团队采用“先通官方test-network,再替换链码,最后接业务接口”的路线。这也是下面几节要走的路线。

2.3 最小数据模型:患者、记录、授权关系怎么设计

链上数据模型不要一开始就设计成几十个字段。毕设系统最实用的是四个结构:患者、医疗记录、授权关系、操作日志。患者结构用来存患者的链上ID、姓名哈希、登记时间;医疗记录结构存记录ID、患者ID、上传医生ID、记录类型、IPFS哈希、文件SHA-256、时间戳;授权结构存授权方、被授权方、记录ID或者记录类型、授权截止时间;操作日志记录每一次查看和修改。

为什么要单独建授权关系?因为区块链本身只负责“这是真的”,不负责“谁可以看”。可观测和可授权是两个维度,如果把权限做成智能合约里的一大段if-else,后面加一种角色就要升级一次链码。把授权关系也做成状态数据,就能用一条grantAccess记录解决阅读权限问题。这也是Fabric链码里最常见的做法:权限数据以键值形式保存在世界态中,查询前先做一次授权检查。

表格式的字段设计,我一般会在实验报告里给一张字段表:

字段含义存储位置示例
record_id记录唯一ID链上pre-001-20241120-001
patient_id患者链上ID链上pat-10001
file_hash原始文件SHA-256链上9d8f3a...
ipfs_cid原始文件IPFS地址链上QmY...
record_type记录类型链上影像/检验/诊断
valid是否有效链上true
created_at写链时间链上2024-11-20 10:30:00

链下数据库另外存明文姓名和完整文件路径,链上尽量只存哈希和ID。这样做既满足“可审计”,又不会因为账本公开导致全量隐私泄露。你在答辩PPT里放这张表,老师会觉得你有工程分界意识。

3. 从零跑通源码:环境准备与最小启动流程

3.1 环境清单:Docker、Node.js、Go 等工具怎么配

拿到一个区块链毕设源码包,先别急着跑。源码包里一般会有区块链网络脚本、链码、后端服务、前端页面四块。源码注释里常写“开箱即用”,实际还是要自己装环境。我一般建议按下面的清单准备:

  • Docker Engine 20.10+ 和 docker compose
  • Go 1.17+(部分链码需要编译)
  • Node.js 14+(跑Fabric链码和前端)
  • Python 3.8+(可选,写测试脚本)
  • jq、curl 用于调试

装完先验证环境,不要等跑起来才发现Docker没起:

docker version --format '{{.Server.Version}}' docker compose version go version node -v

这里的重点是Docker。Fabric容器全部跑在Docker里,如果你在Windows上用了旧版Docker Toolbox,后面会出现端口映射不到宿主机的问题。遇到类似情况,优先检查Docker Desktop是否为WSL2模式,再检查docker ps能否看到容器。环境这件事,一次配好能省掉后面三天的排错时间。

3.2 用 test-network 起一条 Fabric 联盟链

Fabric官方仓库里带了test-network脚本,这是最省事的起步方式。克隆仓库到本地后,先做一次清理,再启动网络并创建通道:

git clone https://github.com/hyperledger/fabric-samples.git cd fabric-samples/test-network ./network.sh down ./network.sh up createChannel

第一次启动会拉取一堆Docker镜像,时间取决于网速,可能十几分钟。network.sh down会把之前的容器、卷和生成的证书全部清掉,防止残留影响下一次启动。up createChannel表示启动网络并创建一个名为mychannel的通道,这个通道名在后端对接时要保持完全一致。

启动成功后会看到类似“Creating peer0.org1.example.com ... done”的提示。此时用docker ps检查,至少应该有orderer、peer0.org1、peer0.org2以及对应的CA容器在运行。如果某个容器一直在重启,先看日志,不要急着反复执行network.sh up。

3.3 部署链码并用命令行完成一次写入与查询

项目里的链码通常就是真正的业务逻辑所在。为了让你能读懂套路,下面这个链码用JavaScript实现,适合Fabric 2.x。它包含初始化账本、创建记录、查询记录三个方法:

'use strict'; const { Contract } = require('fabric-contract-api'); class MedicalRecordContract extends Contract { async initLedger(ctx) { const initial = { recordId: 'init-record-0001', patientId: 'pat-init', fileHash: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', ipfsCid: 'none', recordType: 'init', createdBy: 'admin', createdAt: new Date().toISOString(), valid: true }; await ctx.stub.putState(initial.recordId, Buffer.from(JSON.stringify(initial))); return 'init done'; } async createRecord(ctx, recordId, patientId, fileHash, ipfsCid, recordType, createdBy) { const exists = await ctx.stub.getState(recordId); if (exists && exists.length > 0) { throw new Error(`record ${recordId} already exists`); } const record = { recordId, patientId, fileHash, ipfsCid, recordType, createdBy, createdAt: new Date().toISOString(), valid: true }; await ctx.stub.putState(recordId, Buffer.from(JSON.stringify(record))); return JSON.stringify(record); } async queryRecord(ctx, recordId) { const data = await ctx.stub.getState(recordId); if (!data || data.length === 0) { throw new Error(`record ${recordId} does not exist`); } return data.toString(); } } module.exports = MedicalRecordContract;

逻辑说明:initLedger预置一条示例数据,方便测试;createRecord先检查主键冲突再写入,避免覆盖已有记录;queryRecord直接读世界态。每个方法的第一个参数是ctx,Fabric链码运行时会传入Stub接口,用来操作账本和获取交易信息。链上身份校验不能只靠前端传的createdBy字符串,应该从ctx.clientIdentity里取证书属性,否则任何人都能伪造上传者。

部署链码到刚才启动的网络:

cd fabric-samples/test-network ./network.sh deployCC -ccn medical -ccp ../medical-chaincode -ccl javascript

参数说明:-ccn是链码名称,-ccn medical表示链码叫medical,后续SDK调用时要一致;-ccp指向链码所在目录;-ccl是链码语言,这里用javascript。部署成功后,阶段性和链码相关的容器会陆续启动。此时再用peer命令调用一次,感受一下事务从提交到落账的流程。

这一步是我建议你也亲手做一遍的。因为论文里需要写“链码通过peer命令实现写入”,你不做一遍,答辩被问细节可能语焉不详。

3.4 后端接口怎么对接链码

项目里的后端通常用Node.js写,通过Fabric Gateway SDK连到peer节点。下面是一个最小连接示例:

const { connect, signers } = require('@hyperledger/fabric-gateway'); const grpc = require('@grpc/grpc-js'); async function createRecord({ recordId, patientId, fileHash, ipfsCid, recordType, createdBy }) { const identity = { mspId: 'Org1MSP', credentials: certData }; const signer = signers.newPrivateKeySigner(privateKeyData); const client = new grpc.Client('localhost:7051', grpc.credentials.createInsecure()); const gateway = connect({ identity, signer, client }); try { const network = gateway.getNetwork('mychannel'); const contract = network.getContract('medical'); const result = await contract.submitTransaction( 'createRecord', recordId, patientId, fileHash, ipfsCid, recordType, createdBy ); return Buffer.from(result).toString('utf8'); } finally { gateway.close(); client.close(); } }

参数说明:mspId要和组织对应,Org1MSP说明当前身份属于org1;证书和私钥从wallet目录读取,实际项目里不要硬编码;localhost:7051是peer0.org1的gRPC地址,如果服务跑在容器里要改成对应容器名。submitTransaction会走完整的背书、排序、提交流程,返回的是链码里的返回值。

到了这一步,“上链”已经通了。后面再把前端页面对接上来,一个能演示的闭环就成立了。

4. 医疗记录的上传与授权:关键接口和参数设计

4.1 用 IPFS 存原始文件,用链码存哈希

把原始文件交给IPFS是常见做法。本地跑一个IPFS节点:

ipfs init ipfs daemon

默认API地址是http://127.0.0.1:5001。下面这个Python脚本同时完成两件事:上传文件到IPFS,再计算文件SHA-256:

import requests import hashlib IPFS_API = "http://127.0.0.1:5001/api/v0/add" def file_sha256(path): h = hashlib.sha256() with open(path, "rb") as f: for chunk in iter(lambda: f.read(8192), b""): h.update(chunk) return h.hexdigest() def upload_ipfs(path): with open(path, "rb") as f: r = requests.post(IPFS_API, files={"file": f}, timeout=60) r.raise_for_status() return r.json()["Hash"] if __name__ == "__main__": filepath = "./sample_report.pdf" cid = upload_ipfs(filepath) sha = file_sha256(filepath) print(f"CID={cid}") print(f"SHA256={sha}")

逻辑说明:file_sha256分块读文件,避免一次性把大文件读进内存;upload_ipfs把文件POST到IPFS的add接口,返回的JSON里有Hash字段,这个值就是之后要存到链上的CID。打印出的SHA256要和链上记录的fileHash保持一致。

注意,IPFS本身没有权限控制,任何人拿到CID都能下载。敏感医疗文件必须在上传前加密。常见做法是用AES对称加密后上传,密钥单独管理,链上只存密文的哈希。如果你在答辩PPT里写“文件加密后上传”,老师会追问密钥放哪里,所以最好在方案里加一个独立的密钥服务,或者用“链下数据库存密钥,链码只存哈希”的说法兜底。

4.2 授权接口:只有被授权的医生能读

前面说链上只能做完整性和身份校验,访问控制也需要在链码里落一道。下面是链码里的授权和受权查询逻辑:

async grantAccess(ctx, recordId, grantor, grantee, ttlSeconds) { const accessKey = `access:${recordId}:${grantee}`; const permission = { recordId, grantor, grantee, grantedAt: new Date().toISOString(), expireAt: new Date(Date.now() + ttlSeconds * 1000).toISOString(), revoked: false }; await ctx.stub.putState(accessKey, Buffer.from(JSON.stringify(permission))); return accessKey; } async queryWithAuth(ctx, recordId, requester) { const accessKey = `access:${recordId}:${requester}`; const permRaw = await ctx.stub.getState(accessKey); if (!permRaw || permRaw.length === 0) { throw new Error('no permission'); } const permission = JSON.parse(permRaw.toString()); if (permission.revoked || new Date(permission.expireAt) < new Date()) { throw new Error('permission expired or revoked'); } return this.queryRecord(ctx, recordId); }

参数说明:ttlSeconds是授权有效秒数,例如30天就传2592000。grantAccess把授权关系写入键为access:recordId:grantee的状态,查询时反查同一个键。queryWithAuth先确认授权记录存在、未撤销、未过期,再调用之前的queryRecord。这样即使有人直接调用queryRecord绕过授权,也会因为没有授权记录而被拒绝,所以前面说“权限不能只在前端做隐藏”,意思就是要在链码里也挡一道。

4.3 参数表:记录类型、权限级别、过期时间

项目里的字段和参数不必完全和我一样,但建议先按下面的表对齐,再扩展:

记录类型是否允许患者自传默认授权时长链上字段示例
检验报告否,只能医生上传30天record_type=lab
影像报告否,只能设备上传30天record_type=image
诊断结论否,只能医生上传90天record_type=diag
患者自述是永久(手动撤销)record_type=self

过期时间太短会影响转诊场景,太长会让权限失去意义。最好在管理后台做成可配置,不要硬编码。链上的record_type要和前端的下拉选项一一对应,避免“前端传了‘CT’,链码里判断的是‘image’”这种低级错位。

4.4 前端调用链路的字段映射

前端上传一份检查报告时,完整的调用链是:前端先把文件POST到后端,后端计算SHA-256并上传IPFS,然后用Fabric Gateway SDK提交链码事务,最后把txId、CID、哈希和业务字段一起返回给前端。前端展示的字段和链上字段需要有一份映射关系:

{ "recordId": "rec-001", "patientId": "pat-10001", "fileHash": "9d8f3a...", "ipfsCid": "QmY...", "recordType": "image", "createdBy": "doctor-001", "txId": "a1b2c3..." }

txId是Fabric提交事务后返回的ID,它可以在浏览器或移动端展示成“存证编号”。如果用户拿着这个编号去问医生“我在区块链上的记录在哪”,医生能通过txId查到对应的链上交易,这就是区块链带来的体验差异。

5. 避坑指南:毕设里最容易翻车的 5 个问题

5.1 链码初始化报错:账本状态与 couchdb 索引不一致

现象:执行initLedger时报“Chaincode has been already initialized”,或者查询时返回空数据。原因:Fabric 2.x里链码的initLedger方法默认只允许执行一次,第二次调用不会覆盖;也有同学改过链码代码后直接重复部署,旧账本数据还在,新逻辑找不到旧数据。解决:重新设置链码版本或先清通道里的账本数据,最简单的做法是./network.sh down后重新up createChannel,再部署一遍。如果不想破坏已有交易数据,就要在链码里加初始化检查,在initLedger开头判断状态是否存在,存在则跳过。

5.2 文件上传超时:IPFS 网关与 docker 网络冲突

现象:前端上传一个几十MB的文件,等待很久后直接超时。原因:常见的不是IPFS慢,而是IPFS容器和Fabric容器不在同一个Docker网络里,或者回环地址被代理劫持。解决:先单独用curl http://127.0.0.1:5001/api/v0/version测试IPFS节点是否有响应;如果后端跑在Docker容器里,IPFS地址要写成http://host.docker.internal:5001,不能写127.0.0.1。老师问为什么慢,你可以回答“大文件上传前做了分块切片,IPFS只负责存储,不参与业务查询链路”。

5.3 权限控制失效:只做了前端隐藏,没做链上校验

现象:前端的“查看记录”按钮对非授权医生是隐藏的,但把前端代码打开,直接调用后端查询接口,照样能返回数据。原因:后端的查询函数只调了链码的queryRecord,没有先走queryWithAuth做授权检查。解决:后端所有读取接口必须统一走授权链码,不能有第二个查询入口。这个坑在答辩时很容易被老师用一个浏览器开发者工具就试出来,属于最伤的表现。其实Fabric里还可以用ctx.clientIdentity拿到调用者证书,在链码里直接判断其所属组织,建议在queryWithAuth里加上这一层,形成“证书身份+授权记录”双重校验。

5.4 答辩时区块链部分被老师追问:事务与隐私边界

现象:老师问“你的区块链和普通数据库有什么区别”,你只能说“区块链不可篡改”,再问“那数据存在IPFS上,IPFS不就篡改了?”你就接不上话。原因:没有把“链上存哈希、链下存原件”的边界讲透。解决:准备一张流程图,说明原始文件可以随时被替换,但链上的哈希是固定的;任何人替换文件后哈希不匹配,系统立刻发现。还要准备一句“区块链解决的是完整性和审计问题,不是机密性问题”,这句话能挡住一大半追问。

5.5 源码工程跑不起来:node_modules 和 fabric 镜像版本不对

现象:从网上下载的源码包,直接npm install后一堆依赖报错,或者启动时提示“failed to connect to peer”。原因:源码包是在某一版本的Fabric和Node.js下写的,新版本的fabric-gateway和旧版链码接口不兼容;有些包连@grpc/grpc-js版本也不匹配。解决:先查源码里的package.json,尽量安装和依赖锁文件匹配的版本;如果包太大,直接让后端接口改用REST方式或使用Fabric提供的官方CLI。不要盲目升级大版本,Fabric 2.x和3.x的API差距会让你的源码改起来很痛苦。动手前先备份一份源码包才是真正的“后悔药”。

6. 让毕设从“能跑”到“有亮点”:验证方法、测试用例与答辩话术

6.1 用一套测试脚本证明数据不可篡改

不要只演示一遍“上传成功”。准备一个verify.py脚本,先从链上取某条记录的fileHash,再对本地文件重新算哈希,故意改一个字节再算一次,然后输出“篡改前后对比”。这个脚本能在答辩时直接跑,三行print就能证明区块链的价值。脚本逻辑和我们第4章里的file_sha256一致,只是增加了从链上读哈希的步骤。

6.2 性能验证:交易吞吐与查询延迟怎么测

毕设不需要压测到上万TPS,但至少要有两台机器或一组脚本,用并发方式提交50笔记录,统计成功率、平均写延迟和查询延迟。Fabric的peer节点和orderer节点在同一台机器时,性能会偏低,这是正常现象。你在论文里解释清楚“单机环境限制了性能”,再给出一个“在内存充足情况下,500笔并发提交成功率接近100%”的结论,已经足够。

6.3 论文和答辩PPT里应该突出的三个图

第一张是“系统架构图”,画出前端、后端、Fabric网关、peer、orderer、CA、IPFS的关系。第二张是“上链流程图”,展示从文件上传到哈希计算再到链码提交的时序。第三张是“授权查询时序图”。这三张图比文字描述更有说服力。答辩时还要主动提一个设计习惯:所有对外查询接口都走授权链码,不让前端直接访问原始记录。这会让你看上去是有工程素养的,而不是只会照搬源码。

下次我做类似项目时,会先问自己“哪一环是区块链真正不可替代的”,而不是先搭框架。用这个标准去做毕设,你会发现前面80%的弯路都能省掉。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询