简介:一套基于区块链的图片版权保护系统毕业设计项目,面向计算机相关专业学生、区块链入门开发者及需要完成期末大作业或课程设计的人群。项目针对传统图片版权确权难、追溯难的问题,围绕版权存证、验证与追溯场景,完整实现了区块链与Web端的交互流程。源码已本地编译通过,评审分达95分以上,难度适中且经过助教审定,可直接运行学习。压缩包内含131个文件,以Python后端逻辑、JavaScript交互脚本、CSS页面样式及GIF操作演示为主,同时包含HTML页面、XML配置、Markdown说明文档等,整体大小仅675KB,目录结构清晰,便于按模块阅读、调试和二次开发。目前已有217人学习下载,适合需要一套可运行、可讲解、可扩展的区块链应用案例,用于毕业设计答辩或项目经验积累的读者。
1. 从一张图的归属争议,到区块链存证的必要性
图片版权纠纷里,最被动的不是拿不出原图,而是拿不出“这张图在某个时间点已经属于我”的可信证明。传统登记方式周期长、成本高,登记机构本身也可能成为被攻击的目标。这个系统把图片的哈希摘要、版权人、时间戳写入区块链,用分布式账本把“谁在什么时候存过什么文件”这件事固定下来,后续任何人发起校验时都能比对链上记录,而不需要依赖某个中心化服务器做背书。
整个资源包中的源码经过本地编译,评审分达到 95 分以上,前端基于 layui 构建,后端接口完整,附带详细设计文档。对正在准备区块链方向期末大作业、毕业设计的同学,以及想了解存证类 DApp 从合约到管理后台该如何拆解的开发者,这套基于区块链的图片版权保护系统项目源码都可以直接作为复现起点。
2. 系统架构与版权存证的核心机制
在拆代码之前,先想清楚毕业设计最容易翻车的点:把区块链当数据库用,所有数据都往链上写。这个项目里,链上只保存“图片指纹 + 版权人 + 时间戳 + 存证编号”这类摘要信息,原始图片和详细元数据仍放在本地文件目录与 MySQL 中。这样既满足不可篡改的校验需求,又绕开了把大文件写进区块导致手续费高、同步成本大的问题。
2.1 分层架构与模块划分
项目采用前后端分离的结构组织,服务端负责业务编排,区块链节点负责共识记账,Web 后台使用 layui 渲染。各模块职责如下表。
| 模块 | 技术选型 | 职责 |
|---|---|---|
| 接入层 | Spring Boot Controller | 处理上传、查询、校验请求,返回统一 JSON |
| 业务层 | Service + TransactionManager | 计算图片指纹、控制版权登记状态流转 |
| 数据层 | MySQL + MyBatis | 存储图片元数据、登记记录、用户信息 |
| 区块链层 | Web3j + 本地测试链 | 部署合约、发送存证交易、读取交易回执 |
| 前端展示 | layui + layer + jQuery | 管理后台、上传交互、存证记录展示 |
这个分层里最容易忽略的是事务边界。版权登记是跨库跨链的流程:先在 MySQL 插入一条记录,再发送链上交易。如果链上交易失败而数据库已经提交,就会出现业务库里显示“已登记”、链上却查不到存证的脏数据。项目里的标准做法是先置状态为“待上链”,拿到链上交易回执后再更新为“已上链”,同时记录交易哈希作为对账凭证。
2.2 图片指纹与区块链存证流程的衔接
图片内容本身没有写入链上交易,链上存的是用 SHA-256 计算的摘要。上传图片后,服务端读取文件二进制流,计算摘要,再配合版权人钱包地址与登记时间组装成存证内容。摘要算法对文件内容非常敏感:哪怕只改动一个像素,重新计算的哈希就会完全不同。因此校验时只需要对比“库中记录的哈希”与“当前文件的哈希”,就能判断是否有篡改。
以下代码是项目在上传接口中计算图片指纹的简化实现。
public String calculateFileHash(MultipartFile file) throws Exception { MessageDigest digest = MessageDigest.getInstance("SHA-256"); try (InputStream is = file.getInputStream()) { byte[] buffer = new byte[8192]; int len; while ((len = is.read(buffer)) != -1) { digest.update(buffer, 0, len); } } // 转成 64 位十六进制字符串,作为图片唯一指纹 return Hex.encodeHexString(digest.digest()); }上面代码分块读取文件字节流并更新摘要对象,最后输出 64 位十六进制串。缓冲区8192直接影响大图上传时的内存占用,如果部署环境内存只有 512 MB,可以把缓冲区调小到 4096,必要时先落盘再计算,避免同时持有上传字节数组和计算结果两份内存。图片元信息(作者、作品名称、说明等)进入 MySQL,链上只放指纹与归属信息,两者通过版权编号关联。如果后续想和图像 AI 识别方向结合,也只需要在指纹计算前增加一个特征提取模块,链上依然只保存摘要结果。
2.3 为什么需要链上校验而不只是数据库校验
如果校验逻辑只依赖数据库,管理员一旦拥有过高权限就能直接改记录,版权证明失去公信力。区块链在这里提供的不是“更快”的查询,而是“更难被单点篡改”的校验凭证。存证交易一旦被打包进区块,任何人都无法删除或修改内容;即使数据库被清空,只要保留交易哈希,依然能从链上还原登记信息。
联调阶段还有一个容易踩的点是交易确认时延。本地测试链通常 5 到 15 秒出块,后端必须监听交易回执,而不是发送交易后立即返回成功。常见错误做法是发送后马上查询链上结果,因为区块尚未打包而查不到,误判为存证失败。正确做法是在TransactionReceipt回调中更新登记状态,或轮询等待确认数达到 1 再返回结果。
2.4 版权登记表设计与状态流转
数据库设计要配合前述的两阶段提交。版权记录表至少需要保留待上链与已上链两种状态,初始化脚本如下。
CREATE TABLE copyright_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, copyright_id VARCHAR(32) NOT NULL UNIQUE, title VARCHAR(128) NOT NULL, author VARCHAR(64) NOT NULL, image_hash CHAR(64) NOT NULL, file_path VARCHAR(256) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0-待上链 1-已上链', tx_hash VARCHAR(128), create_time DATETIME DEFAULT CURRENT_TIMESTAMP );image_hash固定为 64 字符,与 SHA-256 输出长度对应,字段类型不要用 TEXT,否则索引和比对都会慢。tx_hash允许为空,表示尚未处理成功;一旦收到回执就更新并附带状态值。状态流转在业务层用枚举控制,枚举的说明直接与合约事件对应,前端展示时也能明确区分“登记中”和“已完成”。
3. 智能合约与后端服务:从零搭建可运行的存证链路
存证链路的核心是智能合约。项目用 Solidity 编写了精简的版权存证合约,只实现登记、查询、校验三个动作,没有引入复杂权限管理与通证经济,却覆盖了评分点。合约部署在本地测试链上,后端通过 Web3j 生成的 Java 包装类与合约交互。如果之前只写过 CRUD 接口,这一章会告诉你合约与后端是怎么通过地址、ABI、Gas 参数接起来的。
3.1 合约的数据结构与存证方法
合约用映射保存版权记录,键是图片哈希,值是包含版权人地址、登记时间、存证编号的结构体。用图片哈希做键而不是自增 ID,是因为哈希天然唯一,校验时可直接通过哈希查证是否存在登记记录。合约定义如下。
pragma solidity ^0.8.0; contract CopyrightRegistry { struct Copyright { address owner; // 版权归属地址 uint256 timestamp; // 上链时间 string copyrightId; // 业务编号 } // 图片哈希 => 版权信息 mapping(string => Copyright) public records; event Registered(string indexed hash, address owner, uint256 timestamp); function register(string memory hash, string memory copyrightId) public { require(records[hash].owner == address(0), "already registered"); records[hash] = Copyright(msg.sender, block.timestamp, copyrightId); emit Registered(hash, msg.sender, block.timestamp); } function verify(string memory hash) public view returns (address, uint256) { Copyright memory c = records[hash]; require(c.owner != address(0), "not found"); return (c.owner, c.timestamp); } }require(records[hash].owner == address(0))在合约层面阻止同一张图片重复登记。事件Registered会把登记行为写入交易日志,后端可订阅事件做异步对账。合约省去了 owner 权限控制,任何地址都能调用register;如果系统只允许管理员登记,需要加入 onlyOwner 修饰器,常见做法是在构造函数中记录部署者,再用require(msg.sender == owner)做访问控制。
3.2 后端如何调用合约完成登记
后端使用 Web3j 连接本地节点。调用合约前,需要通过合约地址和 ABI 加载合约对象,再构建交易。存证交易通常不需要附带以太币,所以 value 保持为 0,但 Gas 上限必须设置合理,否则合约中稍复杂的映射写入会耗尽默认 Gas 导致交易失败。
CopyrightRegistry contract = CopyrightRegistry.load( contractAddress, web3j, credentials, DefaultGasProvider.GAS_PRICE, DefaultGasProvider.GAS_LIMIT); TransactionReceipt receipt = contract.register(imageHash, copyrightId).send(); String txHash = receipt.getTransactionHash(); copyrightRecordService.updateChainStatus(copyrightId, txHash, 1);contractAddress来自部署脚本输出,部署后必须写进配置文件,不能散落在代码里。register方法返回的TransactionReceipt带有交易哈希与区块号;如果后续要跨节点核对存证,区块号也要写进数据库。发送交易与更新状态之间不要插入耗时操作,否则用户反复点击提交,会在链上留下多条等待中的交易。
3.3 查询与验权接口的参数约定
后端对外暴露的接口采用 RESTful 风格,返回统一 JSON 结构。设计时要区分“登记”与“校验”两个动作:登记接口接收文件元数据,校验接口接收待验证文件和版权编号,服务端重新计算哈希并与链上记录比对。接口参数约定如下表。
| 接口路径 | 方法 | 关键参数 | 返回字段 |
|---|---|---|---|
| /api/copyright/upload | POST | file、author、title | copyrightId、imageHash、txHash |
| /api/copyright/verify | POST | file、copyrightId | verified、owner、timestamp |
| /api/copyright/query | GET | copyrightId | 完整登记信息 |
校验接口容易忽略的细节是图像再编码问题:图片经过压缩软件或通信传输后可能被重新编码,导致客户端哈希与服务端不一致。常见解决方式是前端只上传原文件,由服务端统一计算;如果要依赖客户端结果,需要提前约定编码格式与压缩质量,否则会出现“自己存证,自己校验失败”的尴尬结果。
3.4 合约编译与 Web3j 包装类生成
源码包中没有直接给出全部 Java 包装类是合理的,因为不同环境的 Web3j 版本生成代码有差异。重新生成包装类的命令如下。
solc --abi --bin CopyrightRegistry.sol -o build/ web3j generate solidity -a build/CopyrightRegistry.abi -b build/CopyrightRegistry.bin -o src/main/java -p com.example.contract第一行先编译合约得到 ABI 和 BIN 文件,第二行生成 Java 合约类。新版本 Web3j 使用generate solidity子命令,旧版本是generate,两者参数不兼容;生成后检查类名是否为CopyrightRegistry,如果发现类名带数字后缀,多半是关闭 IDE 后冲突文件未清理,执行 clean 后再生成即可。
4. layui 前端与可视化:从静态资源到可操作的管理后台
Web 管理端使用 layui 2.x 系列资源包(layui.css、layer.css、laydate.css、iconfont 等),没有引入 Vue 或 React,页面由服务端模板配合静态资源渲染。这样打包体积小、部署简单,很适合需要现场演示的答辩场景。下面以版权登记与存证记录查询两个页面为例,说明前端交互与后端接口如何串起来。
4.1 资源目录与 layui 组件的引入方式
解压项目 zip 包后,static目录下有完整的 layui 资源,包括 css、font、images 等子目录。页面只需引入 layui.css 与 layui.js,再使用layui.use按需加载模块。项目里常见的错误是只引用了 layer.css 而没有同时引入基础样式,导致弹窗与图标错乱。正确做法是先引入基础样式,再按功能加载扩展模块。功能页面对应关系如下表。
| 功能页面 | 使用的 layui 组件 | 对接接口 |
|---|---|---|
| 版权登记 | upload、form、layer | POST /api/copyright/upload |
| 存证记录查询 | table、page | GET /api/copyright/query |
| 版权校验 | form、layer | POST /api/copyright/verify |
<link rel="stylesheet" href="/static/layui/css/layui.css"> <script src="/static/layui/layui.js"></script>layui.css 是所有组件的基础样式;laydate.css 只在用到日期选择器时才引入;code.css 用于代码高亮。资源包内的 iconfont.eot、woff、ttf 对应图标库在不同浏览器的字体格式,部署时不能只复制 css 而漏掉 font 目录,否则所有图标都会变成方框。static/layui/images/下几个数字命名的 gif(22.gif、24.gif、59.gif)是表格加载、上传等待等场景的进度动画,部署时与 css、font 一起原样保留即可。layui.mobile.css 属于移动端适配,后台管理页面未使用,但保留在目录里不会引发冲突。
4.2 上传与登记页面的关键写法
登记页面的核心交互是:选择图片、填写版权人、点击上传、异步调用后端接口,拿到版权编号后通过 layer 弹窗提示。下面代码展示 layui.upload 渲染上传组件并完成登记提交。
layui.use(['upload', 'layer', 'form'], function () { var upload = layui.upload, layer = layui.layer; upload.render({ elem: '#uploadBtn', url: '/api/copyright/upload', accept: 'images', exts: 'jpg|png|jpeg|webp', size: 2048, done: function (res) { if (res.code === 0) { layer.msg('登记成功,编号:' + res.data.copyrightId); $('#txHash').val(res.data.txHash); tableIns.reload(); } else { layer.msg(res.msg, {icon: 2}); } }, error: function () { layer.msg('上传接口异常', {icon: 2}); } }); });参数accept: 'images'会把文件选择框过滤为图片类型,但无法阻止改名伪造类型,后端仍需校验文件头;exts限定扩展名,size单位是 KB,2048 表示限制为 2 MB。done回调中的tableIns.reload()会在登记完成后自动刷新下方存证记录表格,这个联动在答辩演示时是加分项。需要留意的是 layui 的upload模块在上传前后不会重置表单值,失败后需要手动清空文件选择控件。
4.3 用 table 组件展示链上存证记录
存证记录页面使用 table 组件,通过 cols 配置列,通过 url 拉取列表数据。展示层的重点是格式处理:时间戳转成日期,交易哈希截断显示,避免超长字段挤压其他列。
table.render({ elem: '#copyrightTable', url: '/api/copyright/query', page: true, cols: [[ {field: 'copyrightId', title: '存证编号', width: 120}, {field: 'author', title: '版权人', width: 100}, {field: 'imageHash', title: '图片指纹', width: 200, templet: function (d) { return d.imageHash.substring(0, 16) + '...'; }}, {field: 'createTime', title: '登记时间', templet: function (d) { return new Date(d.createTime).toLocaleString(); }}, {field: 'txHash', title: '交易哈希', width: 200} ]] });templet是行模板函数,用于对字段二次加工。上述配置中 imageHash 只展示前 16 位,createTime 被格式化为本地时间。交易哈希建议用templet输出带>curl -F "file=@test.png" -F "author=zhangsan" \ http://localhost:8080/api/copyright/upload curl -F "file=@test_tampered.png" -F "copyrightId=20240001" \ http://localhost:8080/api/copyright/verify
注意两个请求中图片必须分别提交,verify 接口不会读取服务端存储的旧文件,而是实时计算当前文件的哈希。要彻底确认上链,可在 RPC 接口执行eth_getTransactionReceipt,检查日志中是否有 Registered 事件的主题;交易存在但事件为空,通常意味着合约调用未实际执行,需要检查 Gas 上限或方法签名。结合链上日志与数据库记录双向核对,才算把存证流程验证到位。
本文还有配套的精品资源,点击获取