前言
技术栈:springboot + sqlite
P2P
区块链中的点对点(P2P)技术基础是构建分布式网络的关键部分,它允许网络中的各个节点直接相互通信,无需通过中央服务器
特点:
- 去中心化:没有单一的控制点,每个节点都有相同的功能和权利
- 分布性:数据和服务分布在多个节点上
- 容错性:即使部分节点失败,网络仍然可以继续运行
主要组件:
- 节点:网络中的每个参与实体,包括全节点、轻节点等
- 消息传递:节点之间通过网络协议交换信息,包括交易数据、区块信息等
网络结构:
- 节点发现:新节点加入网络时需要找到其他节点建立连接。这可以通过引导节点、预先配置的节点列表或DNS种子等方式实现
- 消息传播:节点间的消息传播机制,确保交易和区块信息在整个网络中迅速扩散
- 网络拓扑:节点之间的连接模式,可以是完全去中心化的,也可以是混合型的(例如超级节点模型)
典型应用:比特币网络、以太坊网络
P2P下载器
原理
- 种子文件创建:上传者创建一个种子文件,其中包含了要下载文件的元信息,如文件名、大小、哈希值等。这个种子文件会被分享给其他用户,作为下载的起点
- 连接节点:下载者使用P2P下载器打开种子文件,软件根据其中的信息连接到 P2P 网络中的其他节点,这些节点可能是其他下载者或拥有文件的上传者
- 分块划分:文件被分成较小的块,每个块都有一个唯一的标识符。这些块可以从不同的节点处下载,实现并行下载以提高速度
- 块选择和下载:下载者从可用的节点列表中选择要下载的块。根据块的可用性和下载速度,P2P下载器动态地选择最优的节点进行下载,从多个源同时获取数据
- 块共享:下载者下载完成一个块后,也会变成一个可供其他下载者获取的源。这种共享机制使得更多的节点能够参与下载,提高整体的下载效率
- 文件组装:下载者完成所有块的下载后,P2P下载器会将这些块按照原始文件的结构和顺序进行组装,生成完整的文件
实现本地文件转移
采用 Springboot 三层架构
- Controller:处理来自用户的请求,并将请求映射到相应的业务逻辑或数据访问操作。控制器层是应用程序的入口点,它接收HTTP请求,并调用Service层的方法来处理业务逻辑
- Service:包含应用程序的核心业务逻辑。这一层处理复杂的业务规则和流程,并协调多个Repository的操作
- Repository:负责与数据库交互,实现数据的持久化和检索。Repository层封装了对数据库的访问细节,并提供了简单的接口给Service层使用
先准备一个 TestController
package com.test.controller;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@RestController
public class TestController {
public static void main(String[] args) throws IOException {
String sourceFilePath = "E:\\learn\\test_springboot\\test\\abc.txt";
String destinationFolderPath = "E:\\learn\\test_springboot\\test2\\";
Path sourcePath = Paths.get(sourceFilePath);
Path destFolderPath = Paths.get(destinationFolderPath);
Path destFilePath = destFolderPath.resolve(sourcePath.getFileName());
copyFileToFolder(sourcePath,destFilePath);
}
public static void copyFileToFolder(Path sourcePath, Path destFilePath) throws IOException {
// 检查源文件是否存在
if (!Files.exists(sourcePath)) {
throw new IOException("源文件不存在: " + sourcePath);
}
// 检查目标文件夹是否存在
if (!Files.isDirectory(destFilePath.getParent())) {
throw new IOException("目标文件不存在: " + destFilePath.getParent());
}
try (
FileInputStream fis = new FileInputStream(sourcePath.toFile());
FileOutputStream fos = new FileOutputStream(destFilePath.toFile())
) {
// 创建一个字节数组来存储文件内容
byte[] buffer = new byte[1024]; // 使用较大的缓冲区
int bytesRead;
// 循环读取文件内容直到读完
while ((bytesRead = fis.read(buffer)) != -1) {
// 写入到目标文件
fos.write(buffer, 0, bytesRead);
}
}
}
}
实现P2P节点的ip发现
首先是引导节点
git clone https://gitee.com/daitoulin/p2p_bootstrap.git
项目拉下来之后进行配置,把ip改成自己的
这样就在 8883 端口启动了引导节点
然后是对等节点
git clone https://gitee.com/daitoulin/p2p_node.git
同样进行配置,在 8887 端口
测试项目
成功连接引导节点,在引导节点处查看
输出了连接到引导节点的对等节点的信息
这样就完成了 p2p 下载器的节点发现功能
实现两个节点内的文件传输
用于实现两个对等节点通过引导节点的文件传输
准备一个测试参数的 /testParam 路由,接受 get 和 post 请求
@RequestMapping("/testParam")
public ResponseEntity<JSONResult> testParam(@RequestParam("username") String username){
JSONResult json = JSONResult.getInstance();
json.setCode("200");
json.setMsg("用户名为:" + username);
json.setContent("");
return new ResponseEntity<JSONResult>(json, HttpStatus.OK);
}
也可以写成在路由上传递参数的形式
@RequestMapping("/testPathVariable/{username}")
public ResponseEntity<JSONResult> testPathVariable(@PathVariable("username") String username){
JSONResult json = JSONResult.getInstance();
json.setCode("200");
json.setMsg("用户名为:" + username);
json.setContent("");
return new ResponseEntity<JSONResult>(json, HttpStatus.OK);
}
文件上传路由 /uploadFile
@PostMapping("/uploadFile")
public ResponseEntity<JSONResult> testUpload(MultipartFile multipartFile) throws IOException {
JSONResult json = JSONResult.getInstance();
String fileName = multipartFile.getOriginalFilename();
Path targetLocation = Paths.get(shareFilePath, fileName);
// 保存文件到指定位置
multipartFile.transferTo(targetLocation);
json.setCode("200");
json.setMsg("查询成功");
json.setContent("");
return new ResponseEntity<JSONResult>(json, HttpStatus.OK);
}
尝试传点文件
查询文件路由 /findDocument ,接收json请求体
@RequestMapping("/findDocument")
@CrossOrigin
public ResponseEntity<JSONResult> findDocument(@RequestBody QueryDocument qd) {
JSONResult json = JSONResult.getInstance();
if (qd.getIp() == null) {
json.setCode("401");
json.setMsg("ip为空无法查找");
json.setContent(null);
return new ResponseEntity<JSONResult>(json, HttpStatus.OK);
}
List<String> listFiles = listFiles(shareFilePath);
DocumentInfo documentInfo = new DocumentInfo();
documentInfo.setFileNames(listFiles);
String contentJson = new Gson().toJson(documentInfo);
json.setCode("200");
json.setMsg("查询成功");
json.setContent(contentJson);
return new ResponseEntity<JSONResult>(json, HttpStatus.OK);
}
public List<String> listFiles(String directoryPath) {
File directory = new File(directoryPath);
List<String> fileNames = new ArrayList<>();
if (directory.isDirectory()) {
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isFile()) {
System.out.println(file.getName());
fileNames.add(file.getName());
}
}
}
} else {
System.out.println("指定路径不是文件夹。");
}
return fileNames;
}
接下来在 /selectIp 路由查询所有注册的ip
@RequestMapping("/selectIp")
@CrossOrigin
public ResponseEntity<JSONResult> selectIp() {
JSONResult json = JSONResult.getInstance();
NettyKademliaDHTNode<String, String> node = tesst.getNode();
RoutingTable<BigInteger, NettyConnectionInfo, Bucket<BigInteger, NettyConnectionInfo>> routingTable = node.getRoutingTable();
List<String> ipList = new ArrayList<>();
ipList.add(nodeIp);
for (Bucket<BigInteger, NettyConnectionInfo> bucket : routingTable.getBuckets()) {
System.out.println(bucket);
List<BigInteger> nodeIds = bucket.getNodeIds();
if (nodeIds.size() != 0) {
for (BigInteger nodeId : nodeIds) {
ExternalNode<BigInteger, NettyConnectionInfo> nodeInfo = bucket.getNode(nodeId);
//System.out.println(nodeInfo);
NettyConnectionInfo connectionInfo = nodeInfo.getConnectionInfo();
//System.out.println(connectionInfo);
if (connectionInfo.getHost().equals(nodeIp)) {
break;
}
ipList.add(connectionInfo.getHost());
}
}
}
json.setCode("200");
json.setMsg("查询成功");
json.setContent(ipList);
return new ResponseEntity<JSONResult>(json, HttpStatus.OK);
}
然后是文件下载的路由 /downloadFile
@RequestMapping("/downloadFile")
public ResponseEntity<JSONResult> downloadFile(@RequestBody DownloadInfo dI) throws Exception {
JSONResult json = JSONResult.getInstance();
String url = "http://" + dI.getIp() + ":8888/download/"+ dI.getFileName();
ResponseEntity<byte[]> response = restTemplate.getForEntity(url, byte[].class);
if (response.getStatusCode().is2xxSuccessful()) {
byte[] fileContent = response.();
// 创建 ByteArrayInputStream
InputStream inputStream = new ByteArrayInputStream(fileContent);
// 保存文件到本地
File localFile = new File(shareFilePath, "received_" + System.currentTimeMillis() + "_" + dI.getFileName());
FileOutputStream fos = new FileOutputStream(localFile);
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
fos.write(buffer, 0, bytesRead);
}
json.setCode("200");
json.setMsg("保存成功");
json.setContent("");
return new ResponseEntity<JSONResult>(json, HttpStatus.OK);
}else {
json.setCode("-1");
json.setMsg("保存失败");
json.setContent("");
return new ResponseEntity<JSONResult>(json, HttpStatus.OK);
}
}
其中调用了这个路由 /download/ 来下载 shareFiles 里面的文件(测了一下发现这里的路径拼接有问题导致根本读取不了文件,遂自行修复)
@RequestMapping("/download/{fileName}")
public ResponseEntity<byte[]> download(@PathVariable String fileName) {
Path path = Paths.get(shareFilePath + "/" + fileName);
try {
byte[] fileContent = Files.readAllBytes(path);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_OCTET_STREAM);
return ResponseEntity.ok().headers(headers).body(fileContent);
} catch (IOException e) {
e.printStackTrace();
return ResponseEntity.notFound().build();
}
}
剩下的就是一些实体类的代码:
package com.example.p2pshare.entity;
import java.util.List;
public class DocumentInfo {
private String ip;
List<String> fileNames;
public List<String> getFileNames() {
return fileNames;
}
public void setFileNames(List<String> fileNames) {
this.fileNames = fileNames;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
@Override
public String toString() {
return "DocumentInfo{" +
"ip='" + ip + '\'' +
", fileNames=" + fileNames +
'}';
}
}
package com.example.p2pshare.entity;
public class DownloadInfo {
private String fileName;
private String ip;
private String targetIp;
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
public String getTargetIp() {
return targetIp;
}
public void setTargetIp(String targetIp) {
this.targetIp = targetIp;
}
@Override
public String toString() {
return "DownloadInfo{" +
"fileName='" + fileName + '\'' +
", ip='" + ip + '\'' +
", targetIp='" + targetIp + '\'' +
'}';
}
}
package com.example.p2pshare.entity;
public class QueryDocument {
private String ip;
public String getIp() {
return ip;
}
public void setIp(String ip) {
this.ip = ip;
}
@Override
public String toString() {
return "QueryDocument{" +
"ip='" + ip + '\'' +
'}';
}
}
共识机制
共识机制是一套算法、规则或协议,用于确保分布式系统中的多个节点能够就特定事务或状态达成一致意见
作用:
- 数据一致性和完整性
- 分布式事务处理
- 防止双重支付和欺诈
- 网络安全和抵抗攻击
- 去中心化控制
分类和特点
工作量证明(Proof of Work,PoW)
原理:基于哈希函数的不可预测性和随机性
- 选择交易和构建区块头
- 挑战难题
- 工作量竞赛
- 寻找满足条件的哈希值
流程:
- 交易池和构建区块:首先,网络中的节点将待确认的交易放入交易池。矿工从交易池中选择一定数量的交易,并将它们组合成一个区块
- 难题设置:网络设定一个难度目标,通常是一个表示难度的数字。这个数字决定了满足难题条件的哈希值需要以多少前导零开头
- 工作量竞赛:矿工开始尝试不同的Nonce值,将Nonce插入区块头中,然后计算区块头的哈希值
- 验证和广播:一旦矿工找到满足条件的哈希值,就将该区块广播给网络中的其他节点。其他节点会验证这个区块的工作量,以确保矿工的计算是有效的
- 奖励和新区块:如果其他节点验证通过,该区块将被添加到区块链中作为新的区块。矿工获得一定数量的奖励,通常包括新发行的代币和交易手续费。
权益证明(Proof of Stake, PoS)
与PoW中通过解决数学难题来获得权利不同,PoS中的权利分配是根据持有的代币数量来确定的
流程:
- 抵押代币
- 选择出块节点
- 验证和创建区块
- 奖励和惩罚
权益质押(Delegated Proof of Stake, DPoS)
共识机制的组合(Hybrid)
PoW实践
https://gitee.com/daitoulin/block_chain
依次启动p2p的两个节点,然后再启动 PoW 项目
区块表结构
采用 sqlite 构建区块链表,db文件保存在项目根目录下的 db/block/blockchain.db
建 block 表:
public static final String getBlockSql(String index) {
String sql = "CREATE TABLE IF NOT EXISTS [block" + index + "] (" +
"[ID] INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL," +
"[blockIndex] TEXT NOT NULL," +
"[preBlockHash] VARCHAR(300) NOT NULL," +
"[path] VARCHAR(300) UNIQUE NOT NULL," +
"[createTime] VARCHAR(50) NOT NULL," +
"[blockHash] VARCHAR(300) UNIQUE NOT NULL," +
"[randomNumber] TEXT NOT NULL," +
"[onMingChain] INTEGER DEFAULT '0' NOT NULL" +
")";
return sql;
}
- blockIndex:区块号
- preBlockHash:上一块区块的哈希
- path:区块对应的区块⽂件
- createTime:区块创建时间
- blockHash:本区块哈希
- randomNumber:随机幸运数
- onMingChain:该区块是否在主链上
dictionary 表:
public static final String getDicSql() {
String sql = "CREATE TABLE IF NOT EXISTS [dictionary] (" +
"[module] VARCHAR(20) NOT NULL," +
"[key] VARCHAR(20) NOT NULL," +
"[value] VARCHAR(20) NOT NULL" +
")";
return sql;
}
- module:模块
- key:键名
- value:键名对应的值
pending 表:用来存储等待打包的交易
public static final String getPendingSql() {
String sql = "CREATE TABLE IF NOT EXISTS [pending] (" +
"[orderNo] VARCHAR(200) UNIQUE NOT NULL," +
"[tradeBody] TEXT NOT NULL," +
"[tradeType] VARCHAR(1) NULL," +
"[createTime] VARCHAR(50) NOT NULL" +
")";
return sql;
}
- orderNo:交易号
- tradeBody:交易体,包含了交易数据等等
- tradeType:交易类型,普通交易或者合约交易
- createTime:交易的创建时间
难题设置以及工作量竞赛
区块实体类:
接下来看挖矿的逻辑:WorkThread.run
String time = DateUtils.getTime();
if (mining.isWork) {
System.out.println("开始运算---------------------");
int trueCount = 0;
int falseCount = 0;
String preBlockIndex = String.valueOf(UpdateTimer.currentMaxBlockIndex);
BlockServiceImpl.checkBlockTable(preBlockIndex);
List<Block> blocks = BlockServiceImpl.queryBlockByBlockIndex(preBlockIndex);
Block currentBlock = null;
if (blocks.size() > 0) {
currentBlock = blocks.get(0);
}
if (currentBlock == null) {
currentBlock = new Block();
currentBlock.setBlockHash("First block hash");
}
Dictionary diffWorkload = InitUtils.intiDifficulty();// 字典表的工作量配置
String maxBlockIndex = currentBlock.getBlockIndex();
String nextBlockIndex = getNextBlockIndex(maxBlockIndex);
首先定义了两个数字变量 trueCount 和 falseCount,用来判断其他节点给我们返回的结果
然后从定时任务中取出当前的区块号 currentBlock,对区块号进行校验,如果是第一块区块则需要进行特殊处理,因为第一块区块是没有前一块区块的哈希值的
接下来对 Block 对象进行赋值,需要取出交易池中的 pending 对象,将交易的哈希值取出进行接下来的计算工作
Block block = new Block();
Random r = new Random();
String rand = String.valueOf(r.nextInt(1000000));
block.setBlockIndex(nextBlockIndex);
block.setCreateTime(time);
block.setWorkLoad(diffWorkload.getValue());
block.setCreateTime(DateUtils.getTime());
block.setPreBlockHash(currentBlock.getBlockHash());
block.setRandomNumber(rand);
Date runDate = new Date();
String blockPath = DataUtils.getBlockPath(nextBlockIndex, runDate);// gen block file path
block.setPath(blockPath);
//取出交易
List<String> tradeNos = new ArrayList<>();
List<Pending> list = PendingServiceImpl.queryPendings();
if (list.size() != 0) {
for (int i = 0; i < list.size(); i++) {
tradeNos.add(list.get(i).getOrderNo());
}
}
block.setDataJson(list.toString());
现在定义一个验证规则,作为工作量证明的目标
以哈希运算结果的前缀为4个连续的零为例,将区块体对象的 workString 方法的输出作为输入进行哈希运算,同时使用 workLoad 作为一个随机变量来不断改变运算的结果
block.setWorkLoad(diffWorkload.getValue());
// ...
String outHash = EncryptUtil.encryptSHA256(block.workString());
System.out.println(outHash);
if (outHash.startsWith(diffWorkload.getValue())) {
System.out.println("挖到---------------------");
if (!mining.isWork) {
return;
}
在这个过程中,矿工不断尝试不同的随机值(workLoad),将区块体对象的字符串表示与该随机值进行组合,然后进行哈希运算。他们的目标是找到一个特定的随机值,使得通过哈希运算得到的结果满足一定的条件,例如在结果的前面有4个连续的零。这个过程需要不断尝试不同的随机值,直到找到一个符合条件的结果为止
简单来说就是其他的值都是固定的,只有这个随机数是变动的,所以就只能穷举一直算
计算结果广播以及工作量验证
计算出答案的人会将结果进行广播,在网络中的人都会接收到区块信息并对接收到的信息进行验证,验证通过就返回true,失败则返回false
block.setBlockHash(outHash);
for (String port : map.getFs().keySet()) {
Friends f = map.getFs().get(port);
String ip = f.getIp();
try {
String resp = HttpHelper.checkBlock(ip, block);
JSONObject response = new Gson().fromJson(resp, JSONObject.class);
if (response.getCode().equals("1")) {
Boolean isTrue = (Boolean) response.getO();
if (isTrue) {
trueCount = trueCount + 1;
} else {
falseCount = falseCount + 1;
}
}
} catch (Exception e) {
System.out.println(ip + "失败");
map.getFs().get(port).setFriendliness(0);
}
}
当正确的结果超过半数以上时,我们就认为这个节点是最先计算出正确答案的节点,拥有记账权,记录这个新的区块
int count = trueCount + falseCount;
boolean isTrueCountMajority = false;
if (count > 0) {
isTrueCountMajority = trueCount > (count / 2);
}
if (isTrueCountMajority) {
TradeBodyPool tbp = new TradeBodyPool();
Map<String, TradeObject> tbMap = new HashMap<>();
for (Pending p : list) {
TradeObject body = new Gson().fromJson(p.getTradeBody(), TradeObject.class);
tbMap.put(p.getOrderNo(), body);
}
List<Block> bs = BlockServiceImpl.queryBlockByBlockIndex(block.getBlockIndex());
if (bs.size() > 0) {
deletePending(tbp);//删除pending
continue;
}
tbp.setTbMap(tbMap);
BlockFile blockFile = new BlockFile();
blockFile.setTbMap(tbMap);
blockFile.setBlockIndex(block.getBlockIndex());
blockFile.setBlockHash(block.getBlockHash());
blockFile.setPreBlockHash(block.getPreBlockHash());
String blockString = new Gson().toJson(blockFile);//区块文件信息
BlockDownLoad bdl = new BlockDownLoad();
bdl.setBlock(block);
bdl.setBlockFileStr(blockString);
bdl.setMaxBlockIndex(block.getBlockIndex());
BlockServiceImpl.checkBlockTable(block.getBlockIndex());//检查表是否存在
BlockServiceImpl.save(block);//保存区块信息
BlockServiceImpl.saveBlockFile(bdl);//保存区块文件信息
DicServiceImpl.updateDicBlockIndex(block.getBlockIndex());//更新当前更新到的块号
DicServiceImpl.updateDicMainBockIndex(bdl.getMaxBlockIndex());//更新当前更新到的块号
UpdateTimer.currentBlockIndex = new BigInteger(block.getBlockIndex());
UpdateTimer.currentMaxBlockIndex = new BigInteger(block.getBlockIndex());
deletePending(tbp);//删除pending
Thread.sleep(2000);
mining.isWork = true;
}
其它节点验证计算结果:
收到广播的消息后,我们也会对传输的数据进行sha256哈希计算,这一步可以验证交易是否有被篡改过
然后用传输过来的区块号去本地数据库查询,查看是否已经存在该区块
查看传输过来的区块是否是当前正在计算的区块
查看区块号是否符合给定的计算条件(例如以0000开头的区块号)
@RequestMapping(value = "/mining/checkBlock", method = {RequestMethod.POST})
public ResponseEntity<JSONObject> checkBlock(@RequestBody Block b) {
JSONObject jo = new JSONObject();
Dictionary diffWorkload = InitUtils.intiDifficulty();// 字典表的工作量配置
String blockHash = EncryptUtil.encryptSHA256(b.workString());
List<Block> bs = BlockServiceImpl.queryBlockByBlockIndex(b.getBlockIndex());
if (bs.size() != 0 || Integer.valueOf(b.getBlockIndex()) - 1 < UpdateTimer.currentMaxBlockIndex.intValue() || !blockHash.equals(b.getBlockHash()) || !blockHash.startsWith(diffWorkload.getValue())) {
jo.setO(false);
} else {
mining.isWork = false;
jo.setO(true);
}
jo.setCode("1");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
/**
* 初始化字典挖矿难度
* @return
*/
public static Dictionary intiDifficulty(){
Dictionary dic = DicServiceImpl.queryDic(Dictionary.MODUAL_BLOCK, Dictionary.DIFFICULTY);
if(dic == null) {
dic = new Dictionary();
dic.setModule(Dictionary.MODUAL_BLOCK);
dic.setKey(Dictionary.DIFFICULTY);
dic.setValue("0000");//first block index
boolean b= DicServiceImpl.save(dic);
if(!b) {
return null;
}
}
return dic;
}
定时任务每秒向其他节点请求最新区块
通过已经保存的对等节点的ip进行最新区块的获取
public void updateBlock(String blockIndex, Mining mining, MapFriends map) throws Exception {
BlockDownLoad bdl = null;
for(String port:map.getFs().keySet()){
Friends f= map.getFs().get(port);
String ip=f.getIp();
if (f.getFriendliness() == 0){
continue;
}
NoticeParams np = new NoticeParams(blockIndex.toString(), ip,"");
bdl = HttpHelper.downLoadBlock(ip, 8001, np);//获取区块和区块内容
if(bdl == null) {
continue;
}
//检测当前区块是否已经存在
TradeBodyPool tbp = BlockBaseUtils.genTbp(bdl);
List<Block> bs=BlockServiceImpl.queryBlockByBlockIndex(bdl.getBlock().getBlockIndex());
if(bs.size() > 0 ){
deletePending(tbp);//删除pending
return;
}
BlockServiceImpl.checkBlockTable(bdl.getBlock().getBlockIndex());//检查表是否存在
BlockServiceImpl.save(bdl.getBlock());//保存区块DB
BlockServiceImpl.saveBlockFile(bdl);//保存区块文件
DicServiceImpl.updateDicBlockIndex(blockIndex);//更新当前更新到的块号
DicServiceImpl.updateDicMainBockIndex(bdl.getMaxBlockIndex());//更新当前更新到的块号
UpdateTimer.currentBlockIndex= new BigInteger(blockIndex) ;
UpdateTimer.currentMaxBlockIndex= new BigInteger(bdl.getMaxBlockIndex()) ;
deletePending(tbp);//删除pending
break;
}
数据库的保存以及修改
PendingServiceImpl.save,保存数据,同样功能的还有 BlockServiceImpl.save
public static void save(Pending pending) throws Exception {
Connection connection = null;
try {
connection = SQLiteHelper.getConnection();
connection.setAutoCommit(false);
String sql = "insert into [pending]([orderNo],[tradeBody],[createTime],[tradeType]) values(?,?,?,?);";
PreparedStatement statement = connection.prepareStatement(sql);
statement.setString(1, pending.getOrderNo()+ "");
statement.setString(2, pending.getTradeBody());
statement.setString(3, pending.getCreateTime());
statement.setString(4, pending.getTradeType());
statement.execute();
connection.commit();
} catch (Exception e) {
if(connection != null)
try {
connection.rollback();
} catch (Exception e1) {
e.getMessage();
}
throw new Exception(e.getMessage());
}finally {
SQLiteHelper.close(connection);
}
}
可以看到这里进行了sql预编译然后写入数据库,应该是不存在注入的(
DicServiceImpl类,修改数据
public static boolean update(Dictionary dic) {
Connection connection = null;
try {
connection = SQLiteHelper.getConnection();
connection.setAutoCommit(false);
String sql = "update [dictionary] set [value] = ? where [module]=? and [key]=?";
PreparedStatement statement = connection.prepareStatement(sql);
statement.setString(1, dic.getValue());
statement.setString(2, dic.getModule());
statement.setString(3, dic.getKey());
statement.execute();
connection.commit();
return true;
} catch (Exception e) {
if(connection != null) {
try {
connection.rollback();
} catch (SQLException e1) {
e.getMessage();
}
}
e.getMessage();
}finally {
SQLiteHelper.close(connection);
}
return false;
}
PendingServiceImpl类,删除数据
public static boolean deletePendings(List<String> tradeNos) {
if(tradeNos == null || tradeNos.size() == 0) {
return true;
}
Connection connection = null;
try {
connection = SQLiteHelper.getConnection();
connection.setAutoCommit(false);
PreparedStatement statement = connection.prepareStatement("delete from [pending] where [orderNo]=?");
for(int i = 0; i< tradeNos.size(); i++) {
System.out.println(tradeNos.get(i));
statement.setString(1, tradeNos.get(i));
statement.addBatch();
if(i % 100 ==0) {
statement.executeBatch();
statement.clearBatch();
}
}
statement.executeBatch();
statement.clearBatch();
connection.commit();
return true;
} catch (Exception e) {
try {
if(connection != null) {
connection.rollback();
}
} catch (SQLException e1) {
e.getMessage();
}
e.printStackTrace();
}finally {
SQLiteHelper.close(connection);
}
return false;
}
发送交易
@RequestMapping(value = "/data/trade", method = {RequestMethod.POST})
public ResponseEntity<JSONObject> trade(@RequestBody TradeObject tradeObject) {
JSONObject jo = new JSONObject();
List<Pending> pes = PendingServiceImpl.queryPendings();
String no = PendingServiceImpl.genTradeNo(tradeObject);
tradeObject.setHashNo(no);
String genTradeNo = PendingServiceImpl.genTradeNo(tradeObject);
for (Pending p : pes) {
if (p.getOrderNo().equals(genTradeNo)) {
jo.setCode("-1");
jo.setMsg("交易已存在");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
}
本地测试
访问 localhost:8001/starMining 直接开始挖矿,直到 hash 值前4位为 0000 视为成功
数据库中也存储了相关的信息
如果要进行交易,则需要启用三个ip不同的节点
区块链中的公钥私钥以及钱包
生成钱包地址公钥以及私钥
引入web3j包,当中内置了生成钱包地址公钥私钥的算法
<dependency>
<groupId>org.web3j</groupId>
<artifactId>core</artifactId>
<version>4.9.4</version>
</dependency>
生成公私钥
package com.example.testmysql.utils;
import com.example.testmysql.entity.EthWallet;
import org.web3j.crypto.ECKeyPair;
import org.web3j.crypto.Hash;
import org.web3j.crypto.Keys;
import org.web3j.crypto.Sign;
import org.web3j.crypto.Sign.SignatureData;
import java.math.BigInteger;
import java.security.InvalidAlgorithmParameterException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
public class EthUtils {
public static void main(String[] args) {
try {
// 1. 生成公钥和私钥对
ECKeyPair keyPair = Keys.createEcKeyPair();
BigInteger privateKey = keyPair.getPrivateKey();
BigInteger publicKey = keyPair.getPublicKey();
System.out.println("Private Key: " + privateKey.toString(16));
System.out.println("Public Key: " + publicKey.toString(16));
// 2. 计算以太坊地址
String address = publicKeyToAddress(publicKey);
System.out.println("Address: " + address);
// 3. 签名和验证
String message = "Hello blockchain";
SignatureData signature = signMessage(message, privateKey);
System.out.println("Signature: " +
bytesToHex(signature.getR()) +
bytesToHex(signature.getS()) +
String.format("%02x", signature.getV()[0]) // 处理 byte 数组的第一个字节
);
String signatureStr = bytesToHex(signature.getR()) + bytesToHex(signature.getS()) + String.format("%02x", signature.getV()[0]);
System.out.println(signatureStr);
boolean isValid = verifySignature(message, signature, address);
System.out.println("Is signature valid: " + isValid);
} catch (Exception e) {
e.printStackTrace();
}
}
public static String getSignStr(SignatureData signature){
return bytesToHex(signature.getR()) + bytesToHex(signature.getS()) + String.format("%02x", signature.getV()[0]);
}
public static EthWallet getWallet() throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, NoSuchProviderException {
EthWallet wallet = new EthWallet();
ECKeyPair keyPair = Keys.createEcKeyPair();
BigInteger privateKey = keyPair.getPrivateKey();
BigInteger publicKey = keyPair.getPublicKey();
String address = publicKeyToAddress(publicKey);
wallet.setPrivateKey(privateKey.toString(16));
wallet.setPublicKey(publicKey.toString(16));
wallet.setAddress(address);
return wallet;
}
public static String publicKeyToAddress(BigInteger publicKey) {
return "0x" + Keys.getAddress(publicKey);
}
public static SignatureData signMessage(String message, BigInteger privateKey) throws Exception {
byte[] messageHash = Hash.sha3(message.getBytes());
ECKeyPair keyPair = ECKeyPair.create(privateKey);
return Sign.signMessage(messageHash, keyPair, false);
}
public static boolean verifySignature(String message, SignatureData signature, String address) {
try {
byte[] messageHash = Hash.sha3(message.getBytes());
BigInteger recoveredPublicKey = Sign.signedMessageHashToKey(messageHash, signature);
String keysAddress = publicKeyToAddress(recoveredPublicKey);
System.out.println("keysAddress: " + keysAddress);
return keysAddress.equals(address);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
public static SignatureData stringToSignatureData(String signatureStr) {
// Ensure the string length is valid (r + s + v should be hex)
if (signatureStr.length() < 130) {
throw new IllegalArgumentException("Invalid signature string length.");
}
// Extract r, s, and v components
String rHex = signatureStr.substring(0, 64); // First 64 characters for r
String sHex = signatureStr.substring(64, 128); // Next 64 characters for s
String vHex = signatureStr.substring(128, 130); // Last 2 characters for v
// Convert hex to byte arrays
byte[] r = hexToBytes(rHex);
byte[] s = hexToBytes(sHex);
byte[] v = new byte[]{(byte) Integer.parseInt(vHex, 16)}; // Convert to byte
return new SignatureData(v, r, s);
}
// Helper method to convert hex string to byte array
public static byte[] hexToBytes(String hex) {
int length = hex.length();
byte[] data = new byte[length / 2];
for (int i = 0; i < length; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i + 1), 16));
}
return data;
}
}
Mysql
已配置
创建等会要用的表
create database testdb;
USE testdb;
CREATE TABLE `t_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`nickname` varchar(50) NOT NULL COMMENT '昵称',
`username` varchar(50) NOT NULL COMMENT '账号',
`password` varchar(50) NOT NULL COMMENT '密码',
`create_time` varchar(40) NOT NULL COMMENT '创建时间',
`del_status` int(4) NOT NULL COMMENT '删除状态0删除,1存在',
PRIMARY KEY (`id`)
);
MyBatis
作用:
- 简化数据库操作:MyBatis允许开发者通过 XML 或注解的⽅式定义 SQL 语句,简化了 JDBC 的繁琐操作,⽐如连接
管理、语句创建和结果处理。
- 灵活的 SQL 映射:与 ORM(对象关系映射)框架不同,MyBatis 允许开发者直接编写 SQL 语句,提供了更⼤的灵活
性和控制权,适合复杂的查询和性能调优。
- 对象映射:MyBatis 能够将数据库查询结果映射到 Java 对象,⽀持复杂的对象关系映射,使得数据处理更加便
捷。
引入mybatis
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.2</version>
</dependency>
配置文件
spring.application.name=testMysql
server.port=8087
### mybatis
mybatis.mapper-locations=classpath:/mybatis-mapper/*Mapper.xml
mybatis.configuration.map-underscore-to-camel-case=true
### xxl-job, datasource
spring.datasource.url=jdbc:mysql://localhost:3306/testdb?useUnicode=true&useSSL=false&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=your_passwd
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
### datasource-pool
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.maximum-pool-size=30
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.pool-name=HikariCP
spring.datasource.hikari.max-lifetime=900000
spring.datasource.hikari.connection-timeout=10000
spring.datasource.hikari.connection-test-query=SELECT 1
spring.datasource.hikari.validation-timeout=1000
用户控制器模块
package com.example.testmysql.controller;
import com.example.testmysql.entity.JSONObject;
import com.example.testmysql.entity.TUser;
import com.example.testmysql.entity.UpdateUser;
import com.example.testmysql.service.UserService;
import com.example.testmysql.utils.DateUtils;
import com.example.testmysql.utils.MD5Utils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@RestController
public class UserController {
@Resource
private UserService userService;
/**
* 注册
* @param user
* @return
*/
@RequestMapping(value = "/register")
public ResponseEntity<JSONObject> save(@RequestBody TUser user){
JSONObject jo = new JSONObject();
if (user.getUsername() == null || "".equals(user.getUsername())){
jo.setCode("-1");
jo.setMsg("username不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if (user.getPassword() == null || "".equals(user.getPassword())){
jo.setCode("-1");
jo.setMsg("password不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if (user.getNickname() == null || "".equals(user.getNickname())){
jo.setCode("-1");
jo.setMsg("nickname不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
TUser exist = userService.queryUserByUsername(user.getUsername());
if (exist != null){
jo.setCode("-1");
jo.setMsg("username已存在,请更改");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
user.setPassword(MD5Utils.string2MD5(user.getPassword()));
user.setCreateTime(DateUtils.getTime());
user.setDelStatus(1);
userService.save(user);
jo.setCode("1");
jo.setMsg("注册成功");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
/**
* 查询所有用户
* @return
*/
@RequestMapping(value = "/queryAllUser")
public ResponseEntity<JSONObject> queryAllUser(){
JSONObject jo = new JSONObject();
List<TUser> users = userService.queryAllUser();
jo.setCode("1");
jo.setMsg("查询成功");
jo.setO(users);
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
/**
* 登录
* @param user
* @return
*/
@RequestMapping(value = "/login")
public ResponseEntity<JSONObject> login(@RequestBody TUser user){
JSONObject jo = new JSONObject();
if (user.getUsername() == null || "".equals(user.getUsername())){
jo.setCode("-1");
jo.setMsg("username不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if (user.getPassword() == null || "".equals(user.getPassword())){
jo.setCode("-1");
jo.setMsg("password不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
TUser exist = userService.queryUserByUsername(user.getUsername());
if (exist == null){
jo.setCode("-1");
jo.setMsg("username不存在,请先注册");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if (!MD5Utils.passwordIsTrue(user.getPassword(),exist.getPassword())){
jo.setCode("-1");
jo.setMsg("密码错误,请重试");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
jo.setCode("1");
jo.setMsg("登录成功");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
/**
* 修改密码
* @param user
* @return
*/
@RequestMapping(value = "/updatePassword")
public ResponseEntity<JSONObject> updatePassword(@RequestBody UpdateUser user){
JSONObject jo = new JSONObject();
if (user.getUsername() == null || "".equals(user.getUsername())){
jo.setCode("-1");
jo.setMsg("username不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if (user.getOldPassword() == null || "".equals(user.getOldPassword())){
jo.setCode("-1");
jo.setMsg("旧密码不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if (user.getNewPassword() == null || "".equals(user.getNewPassword())){
jo.setCode("-1");
jo.setMsg("新密码不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
TUser exist = userService.queryUserByUsername(user.getUsername());
if (exist == null){
jo.setCode("-1");
jo.setMsg("username不存在,请先注册");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if (!MD5Utils.passwordIsTrue(user.getOldPassword(),exist.getPassword())){
jo.setCode("-1");
jo.setMsg("旧密码错误,请重试");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
user.setNewPassword(MD5Utils.string2MD5(user.getNewPassword()));
userService.updatePassword(user);
jo.setCode("1");
jo.setMsg("修改成功");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
/**
* 删除用户
* @param user
* @return
*/
@RequestMapping(value = "/deleteUser")
public ResponseEntity<JSONObject> deleteUser(@RequestBody TUser user) {
JSONObject jo = new JSONObject();
if (user.getUsername() == null || "".equals(user.getUsername())){
jo.setCode("-1");
jo.setMsg("username不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if (user.getPassword() == null || "".equals(user.getPassword())){
jo.setCode("-1");
jo.setMsg("password不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
TUser exist = userService.queryUserByUsername(user.getUsername());
if (exist == null){
jo.setCode("-1");
jo.setMsg("username不存在,请先注册");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if (!MD5Utils.passwordIsTrue(user.getPassword(),exist.getPassword())){
jo.setCode("-1");
jo.setMsg("密码错误,请重试");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
userService.deleteUser(user);
jo.setCode("1");
jo.setMsg("删除成功");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
}
对应操作的 mysql 语句存放在 resources/mybatis-mapper 下的 TUserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.testmysql.dao.UserDao">
<insert id="save" parameterType="com.example.testmysql.entity.TUser" useGeneratedKeys="true" keyProperty="id" >
insert into t_user (
`username`,
`password`,
`nickname`,
`create_time`,
`del_status`
) VALUES (
#{username},
#{password},
#{nickname},
#{createTime},
#{delStatus}
);
</insert>
<select id="queryUserByUsername" parameterType="string" resultType="com.example.testmysql.entity.TUser">
select * from t_user where username = #{username} and del_status = 1
</select>
<update id="updatePassword" parameterType="com.example.testmysql.entity.UpdateUser">
update t_user set password = #{newPassword} where username = #{username}
</update>
<update id="deleteUser" parameterType="com.example.testmysql.entity.UpdateUser">
update t_user set del_status = 0 where username = #{username}
</update>
<select id="queryAllUser" resultType="com.example.testmysql.entity.TUser">
select * from t_user
</select>
</mapper>
注册路由 /register:
查询路由 /queryAllUser:
修改密码路由 /updatePassword:
删除用户路由 /deleteUser:
编写书本模块
建表
CREATE TABLE `t_book` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`bookname` varchar(50) NOT NULL COMMENT '书名',
`price` double(11,2) NOT NULL COMMENT '价钱',
`publisher` varchar(50) NOT NULL COMMENT '出版社',
`status` int(4) NOT NULL COMMENT '状态0为借出,1为存在',
PRIMARY KEY (`id`)
);
实体类
package com.example.testmysql.entity;
public class TBook {
private int id;
private String bookname;
private double price;
private String publisher;
private int status;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getBookname() {
return bookname;
}
public void setBookname(String bookname) {
this.bookname = bookname;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
}
public String getPublisher() {
return publisher;
}
public void setPublisher(String publisher) {
this.publisher = publisher;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
}
service接口
package com.example.testmysql.service;
import com.example.testmysql.entity.TBook;
import java.util.List;
public interface BookService {
public int save(TBook book);
public TBook queryBookByBookname(String bookname);
public TBook queryStatusByBookname(TBook bookname);
List<TBook> queryBookByPublisher(TBook publisher);
}
Dao&Impl
package com.example.testmysql.dao;
import com.example.testmysql.entity.TBook;
import java.util.List;
public interface BookDao {
public int save(TBook tBook);
public TBook queryBookByBookname(String bookname);
public TBook queryStatusByBookname(TBook bookname);
List<TBook> queryBookByPublisher(TBook publisher);
}
package com.example.testmysql.service.impl;
import com.example.testmysql.dao.BookDao;
import com.example.testmysql.entity.TBook;
import com.example.testmysql.service.BookService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class BookServiceImpl implements BookService {
@Resource
private BookDao bookDao;
@Override
public int save(TBook book){return bookDao.save(book);}
@Override
public TBook queryBookByBookname(String bookname) {
return bookDao.queryBookByBookname(bookname);
}
@Override
public TBook queryStatusByBookname(TBook bookname) {
return bookDao.queryStatusByBookname(bookname);
}
@Override
public List<TBook> queryBookByPublisher(TBook publisher) {
return bookDao.queryBookByPublisher(publisher);
}
}
控制器
@RestController
public class BookController {
@Resource
private BookService bookService;
// 控制器路由
}
新增书本:
@RequestMapping(value = "/newBook")
public ResponseEntity<JSONObject> save(@RequestBody TBook book){
JSONObject jo = new JSONObject();
if(book.getBookname() == null || "".equals(book.getBookname())){
jo.setCode("-1");
jo.setMsg("bookname不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if(book.getPrice() <= 0 || !Double.toString(book.getPrice()).matches("\\d+(\\.\\d+)?") || "".equals(Double.toString(book.getPrice()))){
jo.setCode("-1");
jo.setMsg("price输入有误");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if(book.getPublisher() == null || "".equals(book.getPublisher())){
jo.setCode("-1");
jo.setMsg("publisher不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
TBook exist = bookService.queryBookByBookname(book.getBookname());
if (exist != null){
jo.setCode("-1");
jo.setMsg("bookname已存在,请更改");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
book.setStatus(1);
bookService.save(book);
jo.setCode("1");
jo.setMsg("新增书本成功");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
查询书籍是否存在:
@RequestMapping(value = "/queryStatusByBookname")
public ResponseEntity<JSONObject> queryStatusByBookname(@RequestBody TBook bookname){
JSONObject jo = new JSONObject();
try{
int Code = bookService.queryStatusByBookname(bookname).getStatus();
jo.setCode(String.valueOf(Code));
jo.setMsg("书籍存在");
}
catch (Exception e){
jo.setCode("-1");
jo.setMsg("书籍不存在");
}
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
查询指定出版社的所有书籍:
@RequestMapping(value = "/queryBookByPublisher")
public ResponseEntity<JSONObject> queryBookByPublisher(@RequestBody TBook publisher){
JSONObject jo = new JSONObject();
List<TBook> books = bookService.queryBookByPublisher(publisher);
if(books == null || books.isEmpty()){
jo.setCode("-1");
jo.setMsg("查询失败");
} else {
jo.setCode("1");
jo.setMsg("查询成功");
jo.setO(books);
}
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
mybatis xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.testmysql.dao.BookDao">
<insert id="save" parameterType="com.example.testmysql.entity.TBook" useGeneratedKeys="true" keyProperty="id" >
insert into t_book (
`bookname`,
`price`,
`publisher`,
`status`
) VALUES (
#{bookname},
#{price},
#{publisher},
#{status}
);
</insert>
<select id="queryBookByBookname" parameterType="string" resultType="com.example.testmysql.entity.TBook">
select * from t_book where bookname = #{bookname}
</select>
<select id="queryStatusByBookname" parameterType="string" resultType="com.example.testmysql.entity.TBook">
select status from t_book where bookname = #{bookname}
</select>
<select id="queryBookByPublisher" parameterType="com.example.testmysql.entity.TBook" resultType="com.example.testmysql.entity.TBook">
select * from t_book where publisher = #{publisher}
</select>
</mapper>
测试
新增书本
输入书名,查询书籍是否存在
输入出版社名字,查询该出版社的所有书籍:
区块链中的交易
在区块链中,交易是指两方之间价值的转移。这可以是货币、资产或数据的交换。
交易的基本概念建立在信任与透明的基础上,通过区块链的分布式账本,所有参与者都可以验证交易的有效性,确保没有人能篡改历史记录。这种机制使得区块链特别适用于金融系统和需要去中心化信任的场景
组成
- 金额(Amount):表示转账的具体数量
- 交易ID:为每笔交易生成的唯一标识符,用于区分不同的交易
- 时间戳:记录交易的创建时间,确保时间线的透明性
生命周期
- 创建交易:用户通过钱包软件输入交易的基本信息(接收方地址、金额等),并生成交易。
- 签名:交易使用用户的私钥进行数字签名,以证明其合法性。
- 广播:交易被发送到区块链网络,所有节点接收到该交易信息。
- 验证:节点验证交易的有效性,包括检查输入是否有效、签名是否正确等。
- 打包:经过验证的交易被矿工打包成区块,并与其他交易一起处理。
- 确认:矿工通过工作量证明等机制确认区块,交易被写入区块链。
- 最终性:交易一旦被多个区块确认后,便被认为是不可更改的,完成整个交易流程。
项目
https://gitee.com/daitoulin/block_contract.git
测试
开启三个ip不同的节点,2个对等节点和一个引导节点,作为引导节点的 ip 当服务端
测试发现源码中不存在 xxl-job-admin 服务,遂直接访问静态 browser 页面
分别访问 172.31.57.194:8001 和 192.168.64.196:8001 的 /starMining 路由开始挖矿,查看 index.html 可以看到最新出块
访问 /data/getWallet 生成钱包
@RequestMapping(value = "/data/getWallet")
public ResponseEntity<JSONObject> getWallet() throws Exception {
JSONObject jo = new JSONObject();
EthWallet wallet = EthUtils.getWallet();
jo.setO(wallet);
jo.setMsg("生成钱包成功");
jo.setCode("1");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
{"code":"1","msg":"生成钱包成功","o":{"publicKey":"7878b47d1309aeebe59e24429245861de952c512600742829ddf77240260cba20da5be0ae03e95f9c9fbc322fb56f848029e31af25632d7c9aa1343bf1f1cd02","privateKey":"1f7cf3d8bade4999ff8210bbf374558655c05e5fde80bdaf18d3ac76ea02bdd2","address":"0x91a5a06f3f3ed439a7c36f88b341883e77db92e1"}}
访问 /data/getTradeObject 获取交易对象,传入 privateKey、type、from(即上面的地址)、content
@RequestMapping(value = "/data/getTradeObject", method = {RequestMethod.POST})
public ResponseEntity<JSONObject> getTradeObject(@RequestBody TradeBO tradeBO) throws Exception {
JSONObject jo = new JSONObject();
if ("".equals(tradeBO.getContent()) || tradeBO.getContent() == null){
jo.setCode("-1");
jo.setMsg("交易content值不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if ("".equals(tradeBO.getFrom()) || tradeBO.getFrom() == null){
jo.setCode("-1");
jo.setMsg("交易from值不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if ("".equals(tradeBO.getPrivateKey()) || tradeBO.getPrivateKey() == null){
jo.setCode("-1");
jo.setMsg("私钥privateKey不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
BigInteger pri = new BigInteger(tradeBO.getPrivateKey(), 16);
TradeObject tradeObject = new TradeObject();
tradeObject.setFrom(tradeBO.getFrom());
tradeObject.setTo("system");
tradeObject.setType("1");
tradeObject.setContent(tradeBO.getContent());
tradeObject.setJsoncreatetime(DateUtils.getTime());
tradeObject.setObjToString(tradeObject.toString());
Sign.SignatureData signatureData = EthUtils.signMessage(tradeObject.toString(),pri);
String sign = EthUtils.getSignStr(signatureData);
tradeObject.setSign(sign);
jo.setO(tradeObject);
jo.setMsg("签名成功");
jo.setCode("1");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
{"code":"1","msg":"签名成功","o":{"from":"0x91a5a06f3f3ed439a7c36f88b341883e77db92e1","to":"system","hashNo":null,"type":"1","imgUrl":null,"content":"test1","blockIndex":null,"contentjson":null,"jsoncreatetime":"2024-11-01 18:09:03","uId":null,"annexPath":null,"filePath":null,"blockHash":null,"createTime":null,"sign":"62a6a0b47e3c05460b4f1343ef1c9f9b970f3f328d1d623d9134330ecc6d81355a7dfc1242a4d73d26d417320ca4a13337d172bc4af9349ba280eb70b2ed69f71b","contractContent":null,"paramStr":null,"lastData":null,"objToString":"TradeObject{from='0x91a5a06f3f3ed439a7c36f88b341883e77db92e1', to='system', type='1', imgUrl='null', content='test1', blockIndex='null', contentjson='null', jsoncreatetime='2024-11-01 18:09:03', uId='null', annexPath='null', filePath='null', contractContent='null', paramStr='null', lastData='null'}","dataStr":null}}
进行交易,访问 /data/trade
@RequestMapping(value = "/data/trade", method = {RequestMethod.POST})
public ResponseEntity<JSONObject> trade(@RequestBody TradeObject tradeObject) {
JSONObject jo = new JSONObject();
List<Pending> pes = PendingServiceImpl.queryPendings();
if ("".equals(tradeObject.getFrom()) || tradeObject.getFrom() == null){
jo.setCode("-1");
jo.setMsg("交易from值不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if("".equals(tradeObject.getTo()) || tradeObject.getTo() == null){
jo.setCode("-1");
jo.setMsg("交易to值不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if ("".equals(tradeObject.getObjToString()) || tradeObject.getObjToString() == null){
jo.setCode("-1");
jo.setMsg("交易objToString值不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if ("".equals(tradeObject.getSign()) || tradeObject.getSign() == null){
jo.setCode("-1");
jo.setMsg("交易sign值不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if ("".equals(tradeObject.getType()) || tradeObject.getType() == null){
jo.setCode("-1");
jo.setMsg("交易type值不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
String no = PendingServiceImpl.genTradeNo(tradeObject);
tradeObject.setHashNo(no);
for (Pending p : pes) {
if (p.getOrderNo().equals(no)) {
jo.setCode("-1");
jo.setMsg("交易已存在");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
}
Sign.SignatureData signatureData = EthUtils.stringToSignatureData(tradeObject.getSign());
//验证钱包地址
boolean isValid = EthUtils.verifySignature(tradeObject.getObjToString(), signatureData, tradeObject.getFrom());
if (!isValid){
jo.setCode("-1");
jo.setMsg("验签失败,请重新签名");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
//tradeObject.setObjToString("");
String body = new Gson().toJson(tradeObject);
try {
PendingServiceImpl.validateTradeNo(tradeObject);
Pending pending = new Pending();
pending.setTradeBody(body);
pending.setCreateTime(tradeObject.getJsoncreatetime());
pending.setOrderNo(tradeObject.getHashNo());
pending.setTradeType(tradeObject.getType());
PendingServiceImpl.save(pending);
for (String port : map.getFs().keySet()) {
Friends f = map.getFs().get(port);
String ip = f.getIp();
String url = "http://" + ip + ":8001/data/trade";
restTemplate.postForEntity(url, tradeObject, TradeObject.class);
}
jo.setCode("1");
jo.setMsg("成功");
} catch (Exception e) {
System.out.println(e.getMessage());
jo.setCode("-1");
jo.setMsg("失败");
}
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
直接post传入上面交易返回的 json 里 o 的内容
在最新的区块中可以看到交易信息,点击可以看到交易详情
智能合约
以太坊智能合约
此事在智能合约一文中亦有记载
在线IDE:https://remix.ethereum.org/
contracts文件夹下有3个.sol文件,就是官方给初学者准备的3个最基础的合约
1_Storage.sol
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.2 <0.9.0;
/**
* @title Storage
* @dev Store & retrieve value in a variable
* @custom:dev-run-script ./scripts/deploy_with_ethers.ts
*/
contract Storage {
uint256 number;
/**
* @dev Store value in variable
* @param num value to store
*/
function store(uint256 num) public {
number = num;
}
/**
* @dev Return value
* @return value of 'number'
*/
function retrieve() public view returns (uint256){
return number;
}
}
通过 store 方法给 number 赋值,retrieve 方法可以返回 number 的值
编译并部署
可以看到合约里拥有的方法,点击就可以对合约方法进行调用
注意这里要打开 store 方法折叠的部分才能传入数字,否则是字符串
智能合约系统
项目:https://gitee.com/daitoulin/contract_admin.git
数据库操作:上一个项目里的xxl_job.sql
/*
Navicat Premium Data Transfer
Source Server : abc
Source Server Type : MySQL
Source Server Version : 50728 (5.7.28-log)
Source Host : localhost:3306
Source Schema : xxl_job
Target Server Type : MySQL
Target Server Version : 50728 (5.7.28-log)
File Encoding : 65001
Date: 25/10/2024 23:35:48
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for t_contract
-- ----------------------------
DROP TABLE IF EXISTS `t_contract`;
CREATE TABLE `t_contract` (
`id` int(8) NOT NULL AUTO_INCREMENT,
`contract_content` text COLLATE utf8mb4_unicode_ci COMMENT '合约内容',
`contract_address` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '合约地址',
`wallet_address` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '钱包地址',
`data` text COLLATE utf8mb4_unicode_ci COMMENT '合约数据',
`create_time` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建时间',
`job_id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '任务id',
`last_data` text COLLATE utf8mb4_unicode_ci COMMENT '上一次数据',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='合约表,用来存储合约相关';
-- ----------------------------
-- Records of t_contract
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for xxl_job_group
-- ----------------------------
DROP TABLE IF EXISTS `xxl_job_group`;
CREATE TABLE `xxl_job_group` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`app_name` varchar(64) NOT NULL COMMENT '执行器AppName',
`title` varchar(12) NOT NULL COMMENT '执行器名称',
`address_type` tinyint(4) NOT NULL DEFAULT '0' COMMENT '执行器地址类型:0=自动注册、1=手动录入',
`address_list` text COMMENT '执行器地址列表,多地址逗号分隔',
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of xxl_job_group
-- ----------------------------
BEGIN;
INSERT INTO `xxl_job_group` (`id`, `app_name`, `title`, `address_type`, `address_list`, `update_time`) VALUES (1, 'xxl-job-executor-sample', '示例执行器', 0, NULL, '2024-10-25 23:34:57');
COMMIT;
-- ----------------------------
-- Table structure for xxl_job_info
-- ----------------------------
DROP TABLE IF EXISTS `xxl_job_info`;
CREATE TABLE `xxl_job_info` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
`job_desc` varchar(255) NOT NULL,
`add_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`author` varchar(64) DEFAULT NULL COMMENT '作者',
`alarm_email` varchar(255) DEFAULT NULL COMMENT '报警邮件',
`schedule_type` varchar(50) NOT NULL DEFAULT 'NONE' COMMENT '调度类型',
`schedule_conf` varchar(128) DEFAULT NULL COMMENT '调度配置,值含义取决于调度类型',
`misfire_strategy` varchar(50) NOT NULL DEFAULT 'DO_NOTHING' COMMENT '调度过期策略',
`executor_route_strategy` varchar(50) DEFAULT NULL COMMENT '执行器路由策略',
`executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
`executor_param` text COMMENT '执行器任务参数',
`executor_block_strategy` varchar(50) DEFAULT NULL COMMENT '阻塞处理策略',
`executor_timeout` int(11) NOT NULL DEFAULT '0' COMMENT '任务执行超时时间,单位秒',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
`glue_type` varchar(50) NOT NULL COMMENT 'GLUE类型',
`glue_source` mediumtext COMMENT 'GLUE源代码',
`glue_remark` varchar(128) DEFAULT NULL COMMENT 'GLUE备注',
`glue_updatetime` datetime DEFAULT NULL COMMENT 'GLUE更新时间',
`child_jobid` varchar(255) DEFAULT NULL COMMENT '子任务ID,多个逗号分隔',
`trigger_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '调度状态:0-停止,1-运行',
`trigger_last_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '上次调度时间',
`trigger_next_time` bigint(13) NOT NULL DEFAULT '0' COMMENT '下次调度时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of xxl_job_info
-- ----------------------------
BEGIN;
INSERT INTO `xxl_job_info` (`id`, `job_group`, `job_desc`, `add_time`, `update_time`, `author`, `alarm_email`, `schedule_type`, `schedule_conf`, `misfire_strategy`, `executor_route_strategy`, `executor_handler`, `executor_param`, `executor_block_strategy`, `executor_timeout`, `executor_fail_retry_count`, `glue_type`, `glue_source`, `glue_remark`, `glue_updatetime`, `child_jobid`, `trigger_status`, `trigger_last_time`, `trigger_next_time`) VALUES (1, 1, '测试任务1', '2018-11-03 22:21:31', '2018-11-03 22:21:31', 'XXL', '', 'CRON', '0 0 0 * * ? *', 'DO_NOTHING', 'FIRST', 'demoJobHandler', '', 'SERIAL_EXECUTION', 0, 0, 'BEAN', '', 'GLUE代码初始化', '2018-11-03 22:21:31', '', 0, 0, 0);
COMMIT;
-- ----------------------------
-- Table structure for xxl_job_lock
-- ----------------------------
DROP TABLE IF EXISTS `xxl_job_lock`;
CREATE TABLE `xxl_job_lock` (
`lock_name` varchar(50) NOT NULL COMMENT '锁名称',
PRIMARY KEY (`lock_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of xxl_job_lock
-- ----------------------------
BEGIN;
INSERT INTO `xxl_job_lock` (`lock_name`) VALUES ('schedule_lock');
COMMIT;
-- ----------------------------
-- Table structure for xxl_job_log
-- ----------------------------
DROP TABLE IF EXISTS `xxl_job_log`;
CREATE TABLE `xxl_job_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`job_group` int(11) NOT NULL COMMENT '执行器主键ID',
`job_id` int(11) NOT NULL COMMENT '任务,主键ID',
`executor_address` varchar(255) DEFAULT NULL COMMENT '执行器地址,本次执行的地址',
`executor_handler` varchar(255) DEFAULT NULL COMMENT '执行器任务handler',
`executor_param` text COMMENT '执行器任务参数',
`executor_sharding_param` varchar(20) DEFAULT NULL COMMENT '执行器任务分片参数,格式如 1/2',
`executor_fail_retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '失败重试次数',
`trigger_time` datetime DEFAULT NULL COMMENT '调度-时间',
`trigger_code` int(11) NOT NULL COMMENT '调度-结果',
`trigger_msg` text COMMENT '调度-日志',
`handle_time` datetime DEFAULT NULL COMMENT '执行-时间',
`handle_code` int(11) NOT NULL COMMENT '执行-状态',
`handle_msg` text COMMENT '执行-日志',
`alarm_status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '告警状态:0-默认、1-无需告警、2-告警成功、3-告警失败',
PRIMARY KEY (`id`),
KEY `I_trigger_time` (`trigger_time`),
KEY `I_handle_code` (`handle_code`)
) ENGINE=InnoDB AUTO_INCREMENT=70 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of xxl_job_log
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for xxl_job_log_report
-- ----------------------------
DROP TABLE IF EXISTS `xxl_job_log_report`;
CREATE TABLE `xxl_job_log_report` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`trigger_day` datetime DEFAULT NULL COMMENT '调度-时间',
`running_count` int(11) NOT NULL DEFAULT '0' COMMENT '运行中-日志数量',
`suc_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行成功-日志数量',
`fail_count` int(11) NOT NULL DEFAULT '0' COMMENT '执行失败-日志数量',
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `i_trigger_day` (`trigger_day`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of xxl_job_log_report
-- ----------------------------
BEGIN;
INSERT INTO `xxl_job_log_report` (`id`, `trigger_day`, `running_count`, `suc_count`, `fail_count`, `update_time`) VALUES (1, '2021-05-25 00:00:00', 0, 17, 5, NULL);
INSERT INTO `xxl_job_log_report` (`id`, `trigger_day`, `running_count`, `suc_count`, `fail_count`, `update_time`) VALUES (2, '2021-05-24 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` (`id`, `trigger_day`, `running_count`, `suc_count`, `fail_count`, `update_time`) VALUES (3, '2021-05-23 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` (`id`, `trigger_day`, `running_count`, `suc_count`, `fail_count`, `update_time`) VALUES (4, '2024-10-10 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` (`id`, `trigger_day`, `running_count`, `suc_count`, `fail_count`, `update_time`) VALUES (5, '2024-10-09 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` (`id`, `trigger_day`, `running_count`, `suc_count`, `fail_count`, `update_time`) VALUES (6, '2024-10-08 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` (`id`, `trigger_day`, `running_count`, `suc_count`, `fail_count`, `update_time`) VALUES (7, '2024-10-11 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` (`id`, `trigger_day`, `running_count`, `suc_count`, `fail_count`, `update_time`) VALUES (8, '2024-10-12 00:00:00', 0, 11, 1, NULL);
INSERT INTO `xxl_job_log_report` (`id`, `trigger_day`, `running_count`, `suc_count`, `fail_count`, `update_time`) VALUES (9, '2024-10-14 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` (`id`, `trigger_day`, `running_count`, `suc_count`, `fail_count`, `update_time`) VALUES (10, '2024-10-13 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` (`id`, `trigger_day`, `running_count`, `suc_count`, `fail_count`, `update_time`) VALUES (11, '2024-10-21 00:00:00', 0, 2, 6, NULL);
INSERT INTO `xxl_job_log_report` (`id`, `trigger_day`, `running_count`, `suc_count`, `fail_count`, `update_time`) VALUES (12, '2024-10-20 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` (`id`, `trigger_day`, `running_count`, `suc_count`, `fail_count`, `update_time`) VALUES (13, '2024-10-19 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` (`id`, `trigger_day`, `running_count`, `suc_count`, `fail_count`, `update_time`) VALUES (14, '2024-10-22 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` (`id`, `trigger_day`, `running_count`, `suc_count`, `fail_count`, `update_time`) VALUES (15, '2024-10-23 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` (`id`, `trigger_day`, `running_count`, `suc_count`, `fail_count`, `update_time`) VALUES (16, '2024-10-24 00:00:00', 0, 0, 0, NULL);
INSERT INTO `xxl_job_log_report` (`id`, `trigger_day`, `running_count`, `suc_count`, `fail_count`, `update_time`) VALUES (17, '2024-10-25 00:00:00', 0, 0, 0, NULL);
COMMIT;
-- ----------------------------
-- Table structure for xxl_job_logglue
-- ----------------------------
DROP TABLE IF EXISTS `xxl_job_logglue`;
CREATE TABLE `xxl_job_logglue` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`job_id` int(11) NOT NULL COMMENT '任务,主键ID',
`glue_type` varchar(50) DEFAULT NULL COMMENT 'GLUE类型',
`glue_source` mediumtext COMMENT 'GLUE源代码',
`glue_remark` varchar(128) NOT NULL COMMENT 'GLUE备注',
`add_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of xxl_job_logglue
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for xxl_job_registry
-- ----------------------------
DROP TABLE IF EXISTS `xxl_job_registry`;
CREATE TABLE `xxl_job_registry` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`registry_group` varchar(50) NOT NULL,
`registry_key` varchar(255) NOT NULL,
`registry_value` varchar(255) NOT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `i_g_k_v` (`registry_group`,`registry_key`,`registry_value`)
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of xxl_job_registry
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for xxl_job_user
-- ----------------------------
DROP TABLE IF EXISTS `xxl_job_user`;
CREATE TABLE `xxl_job_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) NOT NULL COMMENT '账号',
`password` varchar(50) NOT NULL COMMENT '密码',
`role` tinyint(4) NOT NULL COMMENT '角色:0-普通用户、1-管理员',
`permission` varchar(255) DEFAULT NULL COMMENT '权限:执行器ID列表,多个逗号分割',
PRIMARY KEY (`id`),
UNIQUE KEY `i_username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Records of xxl_job_user
-- ----------------------------
BEGIN;
INSERT INTO `xxl_job_user` (`id`, `username`, `password`, `role`, `permission`) VALUES (1, 'admin', 'e10adc3949ba59abbe56e057f20f883e', 1, NULL);
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
启动 xxl-hob-admin
节点成功注册到合约系统
创建合约
新增合约
- 私钥:区块链项目那边生成的ETH钱包私钥
- 钱包地址:区块链项目那边生成的ETH钱包地址
- 运行模式:选择GLUE(Java),我们要用java语言来写智能合约
对应的接口:
@RequestMapping("/add")
@ResponseBody
public ReturnT<String> add(XxlJobInfo jobInfo) {
jobInfo.setScheduleType("NONE");
String privateKey = jobInfo.getPrivateKey();
String walletAddress = jobInfo.getWalletAddress();
if (privateKey == null || "".equals(privateKey)){
return new ReturnT<String>(ReturnT.FAIL_CODE,"私钥不能为空");
}
if (walletAddress == null || "".equals(walletAddress)){
return new ReturnT<String>(ReturnT.FAIL_CODE,"钱包地址不能为空");
}
Credentials credentials = Credentials.create(privateKey);
// 获取 Ethereum 地址
String address = credentials.getAddress();
if (!address.equals(walletAddress)){
return new ReturnT<String>(ReturnT.FAIL_CODE,"私钥与钱包地址不匹配,请检查后再试");
}
TContract tContract = new TContract();
tContract.setContractContent(jobInfo.getGlueSource());
tContract.setWalletAddress(walletAddress);
String time = DateUtil.formatDateTime(new Date());
tContract.setCreateTime(time);
String s = encryptSHA1(walletAddress + time);
tContract.setContractAddress(s);
System.out.println(s);
ReturnT<String> add = xxlJobService.add(jobInfo);
tContract.setJobId(add.getContent());
contractDao.save(tContract);
System.out.println("jobId:" + add.getContent());
return add;
}
修改合约内容
点击 GLUE IDE 到合约修改页面,得到代码
package com.xxl.job.service.handler;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.IJobHandler;
import java.util.*;
import com.example.blockchain.service.impl.ContractServiceImpl;
import javax.annotation.Resource;
import com.example.blockchain.entity.TradeObject;
import com.google.gson.Gson;
import com.example.blockchain.entity.TContract;
public class DemoGlueJobHandler extends IJobHandler {
@Resource
private ContractServiceImpl contractServiceImpl;
@Override
public void execute() throws Exception {
XxlJobHelper.log("XXL-JOB, Hello World.");
String Param = XxlJobHelper.getJobParam();
HashMap<String, Object> map = contractServiceImpl.jsonToMap(Param);
System.out.println(map);
XxlJobHelper.log(map.toString());
TradeObject trade = new Gson().fromJson(map.get("tradeObject"),TradeObject.class);
TContract contract = new TContract();
String data = "";
trade.setDataStr(data);
contract.setId(Integer.valueOf(map.get("contractId")));
contract.setData(data);
contract.setLastData(trade.getLastData());
contractServiceImpl.setData(contract);
contractServiceImpl.toChain(trade);
}
}
调用合约
发现这个可以任意命令执行(
保存之后执行一次任务,以 json 形式传入我们的参数
{"privateKey":"1f7cf3d8bade4999ff8210bbf374558655c05e5fde80bdaf18d3ac76ea02bdd2","abc":"mygogogo"}
可以在日志里看到参数
json还有List形式和Map形式
List:{"privateKey":"your_private","printData":["abc","bcd"]}
Map:{"privateKey":"your_private","printData":{"eee":"sfsdf","www":"qqai"}}
抽奖程序:
package com.xxl.job.service.handler;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.IJobHandler;
import java.util.*;
import com.example.blockchain.service.impl.ContractServiceImpl;
import javax.annotation.Resource;
import com.example.blockchain.entity.TradeObject;
import com.google.gson.Gson;
import com.example.blockchain.entity.TContract;
public class DemoGlueJobHandler extends IJobHandler {
@Resource
private ContractServiceImpl contractServiceImpl;
@Override
public void execute() throws Exception {
XxlJobHelper.log("XXL-JOB, Hello World.");
String Param = XxlJobHelper.getJobParam();
HashMap<String, Object> map = contractServiceImpl.jsonToMap(Param);
System.out.println(map);
XxlJobHelper.log(map.toString());
TradeObject trade = new Gson().fromJson(map.get("tradeObject"),TradeObject.class);
TContract contract = new TContract();
String data = "";
List<String> participants = map.get("participants");
// 生成随机数并确定中奖者
Random random = new Random();
int index = random.nextInt(participants.size());
String winner = participants.get(index);
data = winner;
trade.setDataStr(data);
contract.setId(Integer.valueOf(map.get("contractId")));
contract.setData(data);
contract.setLastData(trade.getLastData());
contractServiceImpl.setData(contract);
contractServiceImpl.toChain(trade);
}
}
传入参数:
{"privateKey":"1f7cf3d8bade4999ff8210bbf374558655c05e5fde80bdaf18d3ac76ea02bdd2","participants":["Ano","Tmr","Soyo"]}
执行后在区块交易详情查看执行结果
项目尝试
https://gitee.com/daitoulin/suyuan_project.git
操作流程
启动项目、引导节点、对等节点(上次实验那个开着8001端口的服务)和 xxl-admin 服务
启动后会开始更新区块
接下来使用的钱包私钥和地址:
{"privateKey":"1f7cf3d8bade4999ff8210bbf374558655c05e5fde80bdaf18d3ac76ea02bdd2","address":"0x91a5a06f3f3ed439a7c36f88b341883e77db92e1"}
前往首页
package com.example.suyuan.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HtmlController {
@RequestMapping("/index")
public String index(){
return "index";
}
@RequestMapping("/code")
public String code(){
return "code";
}
@RequestMapping("/query")
public String query(){
return "query";
}
}
新增商品
package com.example.suyuan.controller;
import com.example.suyuan.dao.TProductDao;
import com.example.suyuan.entity.JSONObject;
import com.example.suyuan.entity.TProduct;
import com.example.suyuan.entity.bo.ProductBo;
import com.example.suyuan.utils.DateUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.web3j.crypto.Credentials;
import javax.annotation.Resource;
import java.math.BigInteger;
import java.util.List;
@RestController
public class ProductController {
@Resource
private TProductDao tProductDao;
@RequestMapping("/product/add")
public ResponseEntity<JSONObject> add(@RequestBody ProductBo productBo){
JSONObject jo = new JSONObject();
if ("".equals(productBo.getProductName()) || productBo.getProductName() == null){
jo.setCode("-1");
jo.setMsg("产品名不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if ("".equals(productBo.getProductDesc()) || productBo.getProductDesc() == null){
jo.setCode("-1");
jo.setMsg("产品简介不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if ("".equals(productBo.getAddress()) || productBo.getAddress() == null){
jo.setCode("-1");
jo.setMsg("钱包地址不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if ("".equals(productBo.getPrivateKey()) || productBo.getPrivateKey() == null){
jo.setCode("-1");
jo.setMsg("钱包私钥不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
TProduct exist = tProductDao.queryByName(productBo.getProductName());
if (exist != null){
jo.setCode("-1");
jo.setMsg("产品名已被使用,请换个名字吧");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
// 从私钥创建 Credentials 对象
Credentials credentials = Credentials.create(productBo.getPrivateKey());
// 获取 Ethereum 地址
String address = credentials.getAddress();
if (!address.equals(productBo.getAddress())){
jo.setCode("-1");
jo.setMsg("私钥与地址不匹配,禁止操作");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
TProduct tProduct = new TProduct();
tProduct.setProductName(productBo.getProductName());
tProduct.setProductDesc(productBo.getProductDesc());
tProduct.setAddress(productBo.getAddress());
tProduct.setCreateTime(DateUtils.getTime());
tProductDao.save(tProduct);
jo.setO("");
jo.setMsg("新增产品成功");
jo.setCode("1");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
@RequestMapping("/queryProducts")
public ResponseEntity<JSONObject> queryProducts(){
JSONObject jo = new JSONObject();
List<TProduct> product = tProductDao.queryAll();
if (product.size() == 0){
jo.setO("");
jo.setMsg("暂无数据");
jo.setCode("-1");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
jo.setO(product);
jo.setMsg("查询成功");
jo.setCode("1");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
}
新增溯源码
@RequestMapping("/createCode")
public ResponseEntity<JSONObject> createCode(@RequestBody ProductBo productBo){
JSONObject jo = new JSONObject();
if ("".equals(productBo.getPrivateKey()) || productBo.getPrivateKey() == null){
jo.setCode("-1");
jo.setMsg("钱包私钥不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if ("".equals(productBo.getId()) || productBo.getId() == 0){
jo.setCode("-1");
jo.setMsg("id不能为空,不能为0");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
TProduct tProduct = productDao.queryById(productBo.getId());
// 从私钥创建 Credentials 对象
Credentials credentials = Credentials.create(productBo.getPrivateKey());
// 获取 Ethereum 地址
String address = credentials.getAddress();
if (!address.equals(tProduct.getAddress())){
jo.setCode("-1");
jo.setMsg("私钥与地址不匹配,禁止操作");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
SnowflakeIdUtils idWorker = new SnowflakeIdUtils(3, 1);
String code = String.valueOf(idWorker.nextId());
String imgUrl = "http://"+ ip + ":8007/generateQRCode?code=" + code;
TCode tCode = new TCode();
tCode.setCode(code);
tCode.setCreateTime(DateUtils.getTime());
tCode.setProductName(tProduct.getProductName());
tCode.setAddress(tProduct.getAddress());
tCode.setImgUrl(imgUrl);
codeDao.save(tCode);
jo.setCode("1");
jo.setMsg("新增溯源码成功");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
查看溯源码
@RequestMapping("/queryAllCodeByProductId")
public ResponseEntity<JSONObject> queryAllCode(@RequestBody ProductBo productBo){
JSONObject jo = new JSONObject();
if ("".equals(productBo.getId()) || productBo.getId() == 0){
jo.setCode("-1");
jo.setMsg("id不能为空,不能为0");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
TProduct tProduct = productDao.queryById(productBo.getId());
Credentials credentials = Credentials.create(productBo.getPrivateKey());
// 获取 Ethereum 地址
String address = credentials.getAddress();
if (!address.equals(tProduct.getAddress())){
jo.setCode("-1");
jo.setMsg("私钥与地址不匹配,禁止操作");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
List<TCode> list = codeDao.queryByName(tProduct.getProductName());
if (list.size() == 0){
jo.setCode("-1");
jo.setMsg("暂无数据");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
jo.setO(list);
jo.setCode("1");
jo.setMsg("查询成功");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
新增流程
@RequestMapping("/toChain")
public ResponseEntity<JSONObject> toChain(@RequestBody ChainDataBo chainDataBo) throws Exception {
JSONObject jo = new JSONObject();
if ("".equals(chainDataBo.getCode()) || chainDataBo.getCode() == null){
jo.setCode("-1");
jo.setMsg("溯源码不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if ("".equals(chainDataBo.getPrivateKey()) || chainDataBo.getPrivateKey() == null){
jo.setCode("-1");
jo.setMsg("钱包私钥不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if ("".equals(chainDataBo.getContent()) || chainDataBo.getContent() == null){
jo.setCode("-1");
jo.setMsg("内容不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
if ("".equals(chainDataBo.getProcessName()) || chainDataBo.getProcessName() == null){
jo.setCode("-1");
jo.setMsg("流程名不能为空");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
TCode tCode = codeDao.queryByCode(chainDataBo.getCode());
TChainData tChainData = new TChainData();
tChainData.setFrom(tCode.getAddress());
tChainData.setTo("system");
tChainData.setContent(chainDataBo.getContent());
tChainData.setCreateTime(DateUtils.getTime());
tChainData.setCode(tCode.getCode());
tChainData.setProductName(tCode.getProductName());
tChainData.setProcessName(chainDataBo.getProcessName());
tChainData.setChainStatus("0");
tChainData.setBlockIndex("");
//String jsonStr = new Gson().toJson(tChainData.toString());
Gson gson = new GsonBuilder()
.disableHtmlEscaping() // 禁用 HTML 转义
.create();
String jsonStr = gson.toJson(tChainData);
BigInteger pri = new BigInteger(chainDataBo.getPrivateKey(), 16);
TradeObject tradeObject = new TradeObject();
tradeObject.setFrom(tCode.getAddress());
tradeObject.setTo("system");
tradeObject.setType("1");
tradeObject.setContent(jsonStr);
tradeObject.setJsoncreatetime(DateUtils.getTime());
tradeObject.setObjToString(tradeObject.toString());
Sign.SignatureData signatureData = EthUtils.signMessage(tradeObject.toString(),pri);
String sign = EthUtils.getSignStr(signatureData);
tradeObject.setSign(sign);
String hashNo = PendingUtils.genTradeNo(tradeObject);
tChainData.setHashNo(hashNo);
//保存上链数据到数据库
chainDataDao.save(tChainData);
//上链发送交易
String url = "http://" + ip + ":8001/data/trade";
restTemplate.postForEntity(url, tradeObject, TradeObject.class);
jo.setCode("1");
jo.setMsg("提交交易成功");
return new ResponseEntity<JSONObject>(jo, HttpStatus.OK);
}
下载溯源码
扫码的设备需要与区块链服务在同一个内网中
@RequestMapping(value = "/generateQRCode", produces = MediaType.IMAGE_PNG_VALUE)
@CrossOrigin
public void generateQRCode(HttpServletResponse response,@RequestParam String code) throws WriterException, IOException {
String url = "http://" + ip + ":8007/query?code=" + code;
// 设置响应类型
response.setContentType(MediaType.IMAGE_PNG_VALUE);
// 创建 QRCodeWriter 对象
QRCodeWriter qrCodeWriter = new QRCodeWriter();
// 设置编码提示
BitMatrix bitMatrix = qrCodeWriter.encode(url, BarcodeFormat.QR_CODE, 200, 200);
// 生成图像
int qrCodeSize = 200;
BufferedImage image = new BufferedImage(qrCodeSize, qrCodeSize, BufferedImage.TYPE_INT_RGB);
for (int x = 0; x < qrCodeSize; x++) {
for (int y = 0; y < qrCodeSize; y++) {
image.setRGB(x, y, bitMatrix.get(x, y) ? 0xFF000000 : 0xFFFFFFFF);
}
}
// 将图像写入响应输出流
try (ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream()) {
ImageIO.write(image, "PNG", pngOutputStream);
byte[] pngData = pngOutputStream.toByteArray();
response.getOutputStream().write(pngData);
}
}