目录
[1.1 安装FISCO BCOS节点](#1.1 安装FISCO BCOS节点)
[1.2 创建Java项目](#1.2 创建Java项目)
[2.1 创建Voting.sol](#2.1 创建Voting.sol)
[2.2 编译合约](#2.2 编译合约)
[三、Java SDK配置与实现](#三、Java SDK配置与实现)
[3.1 配置文件](#3.1 配置文件)
[3.2 SDK配置类](#3.2 SDK配置类)
[3.3 合约服务类](#3.3 合约服务类)
[3.4 控制器类](#3.4 控制器类)
[3.5 主应用类](#3.5 主应用类)
[4.1 创建证书目录结构](#4.1 创建证书目录结构)
[4.2 编译和运行](#4.2 编译和运行)
[4.3 使用Postman测试API](#4.3 使用Postman测试API)
[1. 部署合约](#1. 部署合约)
[2. 授予投票权](#2. 授予投票权)
[3. 投票](#3. 投票)
[4. 查询获胜者](#4. 查询获胜者)
[5. 查询所有提案](#5. 查询所有提案)
[5.1 事件监听器](#5.1 事件监听器)
[5.2 数据库集成(可选)](#5.2 数据库集成(可选))
[6.1 安全性考虑](#6.1 安全性考虑)
[6.2 性能优化](#6.2 性能优化)
[6.3 错误处理](#6.3 错误处理)
设计方案
我们使用FISCO BCOS Java SDK来调用智能合约。以下是步骤:
-
1、环境准备:确保已经安装并启动了FISCO BCOS节点,并且已经生成了SDK所需的证书(ca.crt、sdk.crt、sdk.key)。
-
2、编写Solidity投票智能合约(同上)。
-
3、编译Solidity合约,获取abi和bin文件。
-
4、使用Java SDK进行合约部署和调用。

我们将使用一个简单的Java项目,基于Spring Boot框架,使用Java SDK与链交互。
下面为您提供完整的FISCO BCOS投票合约部署和Java SDK调用实现方案。
一、环境准备
1.1 安装FISCO BCOS节点
bash
# 下载安装脚本
curl -LO https://github.com/FISCO-BCOS/FISCO-BCOS/releases/download/v3.4.0/build_chain.sh && chmod u+x build_chain.sh
# 构建单节点链
./build_chain.sh -l 127.0.0.1:4 -p 30300,20200,8545
# 启动节点
./nodes/127.0.0.1/start_all.sh
# 检查节点状态
./nodes/127.0.0.1/fisco-bcos -v
# 获取节点证书
cd nodes/127.0.0.1/
ls sdk/ # 查看ca.crt, sdk.crt, sdk.key等文件
1.2 创建Java项目
使用Maven创建项目:
XML
<!-- pom.xml -->
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.fisco.bcos</groupId>
<artifactId>voting-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<java.version>1.8</java.version>
<fisco-bcos-java-sdk.version>3.4.0</fisco-bcos-java-sdk.version>
<spring-boot.version>2.7.0</spring-boot.version>
</properties>
<dependencies>
<!-- FISCO BCOS Java SDK -->
<dependency>
<groupId>org.fisco-bcos.java-sdk</groupId>
<artifactId>fisco-bcos-java-sdk</artifactId>
<version>${fisco-bcos-java-sdk.version}</version>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring-boot.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<!-- JSON -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.23</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</build>
</project>
二、Solidity投票智能合约
2.1 创建Voting.sol
javascript
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Voting {
// 投票者信息
struct Voter {
uint weight; // 投票权重
bool voted; // 是否已投票
uint8 voteIndex; // 投票提案索引
address delegate; // 委托投票地址
}
// 提案信息
struct Proposal {
bytes32 name; // 提案名称(bytes32格式)
uint voteCount; // 得票数
}
// 状态变量
address public chairperson; // 主持人地址
mapping(address => Voter) public voters; // 投票者映射
Proposal[] public proposals; // 提案列表
// 事件定义
event VoterRegistered(address indexed voter);
event Voted(address indexed voter, uint indexed proposalIndex);
event Delegated(address indexed from, address indexed to);
event ProposalCreated(bytes32 indexed name);
event VotingResult(uint winningProposalIndex, bytes32 winningProposalName);
/**
* @dev 构造函数,初始化提案
* @param proposalNames 提案名称数组
*/
constructor(bytes32[] memory proposalNames) {
chairperson = msg.sender;
// 主持人自动获得投票权
voters[chairperson] = Voter({
weight: 1,
voted: false,
voteIndex: 0,
delegate: address(0)
});
// 创建提案
for (uint i = 0; i < proposalNames.length; i++) {
proposals.push(Proposal({
name: proposalNames[i],
voteCount: 0
}));
emit ProposalCreated(proposalNames[i]);
}
}
/**
* @dev 主持人授予投票权
* @param voter 投票者地址
*/
function giveRightToVote(address voter) public {
require(msg.sender == chairperson, "Only chairperson can give voting rights");
require(!voters[voter].voted, "The voter already voted");
require(voters[voter].weight == 0, "Voter already has right");
voters[voter].weight = 1;
emit VoterRegistered(voter);
}
/**
* @dev 批量授予投票权
* @param votersArray 投票者地址数组
*/
function giveRightToVoteBatch(address[] calldata votersArray) public {
require(msg.sender == chairperson, "Only chairperson can give voting rights");
for (uint i = 0; i < votersArray.length; i++) {
address voter = votersArray[i];
if (!voters[voter].voted && voters[voter].weight == 0) {
voters[voter].weight = 1;
emit VoterRegistered(voter);
}
}
}
/**
* @dev 委托投票
* @param to 被委托人地址
*/
function delegate(address to) public {
Voter storage sender = voters[msg.sender];
require(!sender.voted, "You already voted");
require(to != msg.sender, "Self-delegation is disallowed");
require(voters[to].weight > 0, "Delegate has no voting right");
// 查找最终委托地址
while (voters[to].delegate != address(0)) {
to = voters[to].delegate;
require(to != msg.sender, "Found loop in delegation");
}
sender.voted = true;
sender.delegate = to;
Voter storage delegateTo = voters[to];
if (delegateTo.voted) {
// 被委托人已投票,增加对应提案的票数
proposals[delegateTo.voteIndex].voteCount += sender.weight;
} else {
// 被委托人未投票,增加其权重
delegateTo.weight += sender.weight;
}
emit Delegated(msg.sender, to);
}
/**
* @dev 投票
* @param proposalIndex 提案索引
*/
function vote(uint8 proposalIndex) public {
Voter storage sender = voters[msg.sender];
require(sender.weight > 0, "Has no right to vote");
require(!sender.voted, "Already voted");
require(proposalIndex < proposals.length, "Invalid proposal index");
sender.voted = true;
sender.voteIndex = proposalIndex;
// 计票
proposals[proposalIndex].voteCount += sender.weight;
emit Voted(msg.sender, proposalIndex);
}
/**
* @dev 计算获胜提案
* @return winningProposalIndex 获胜提案索引
*/
function winningProposal() public view returns (uint winningProposalIndex) {
uint winningVoteCount = 0;
for (uint i = 0; i < proposals.length; i++) {
if (proposals[i].voteCount > winningVoteCount) {
winningVoteCount = proposals[i].voteCount;
winningProposalIndex = i;
}
}
}
/**
* @dev 获取获胜提案名称
* @return winnerName 获胜提案名称
*/
function winnerName() public view returns (bytes32 winnerName) {
winnerName = proposals[winningProposal()].name;
}
/**
* @dev 结束投票并公布结果
*/
function endVoting() public returns (uint, bytes32) {
require(msg.sender == chairperson, "Only chairperson can end voting");
uint winningIndex = winningProposal();
bytes32 winningName = proposals[winningIndex].name;
emit VotingResult(winningIndex, winningName);
return (winningIndex, winningName);
}
/**
* @dev 获取提案数量
* @return 提案数量
*/
function getProposalsCount() public view returns (uint) {
return proposals.length;
}
/**
* @dev 获取提案详情
* @param index 提案索引
* @return name 提案名称
* @return voteCount 得票数
*/
function getProposal(uint index) public view returns (bytes32 name, uint voteCount) {
require(index < proposals.length, "Invalid proposal index");
Proposal memory proposal = proposals[index];
return (proposal.name, proposal.voteCount);
}
/**
* @dev 获取所有提案信息
* @return names 提案名称数组
* @return voteCounts 得票数数组
*/
function getAllProposals() public view returns (bytes32[] memory names, uint[] memory voteCounts) {
uint length = proposals.length;
names = new bytes32[](length);
voteCounts = new uint[](length);
for (uint i = 0; i < length; i++) {
names[i] = proposals[i].name;
voteCounts[i] = proposals[i].voteCount;
}
}
/**
* @dev 获取投票者信息
* @param voterAddress 投票者地址
* @return weight 投票权重
* @return voted 是否已投票
* @return voteIndex 投票提案索引
* @return delegate 委托地址
*/
function getVoter(address voterAddress) public view returns (
uint weight,
bool voted,
uint8 voteIndex,
address delegate
) {
Voter memory voter = voters[voterAddress];
return (voter.weight, voter.voted, voter.voteIndex, voter.delegate);
}
}
2.2 编译合约
bash
# 安装solc编译器
npm install -g solc
# 编译合约
solcjs --bin --abi Voting.sol -o build/
# 生成Java wrapper
# 使用FISCO BCOS提供的sol2java.sh工具
./sol2java.sh org.fisco.bcos.voting build/Voting.sol
三、Java SDK配置与实现
3.1 配置文件
XML
# src/main/resources/application.yml
spring:
application:
name: fisco-voting-demo
fisco:
bcos:
# 链配置
chain-id: 1
group-id: 1
# 节点连接配置
nodes:
- 127.0.0.1:20200
# 证书配置
cert-path: classpath:conf/ca.crt
private-key: classpath:conf/sdk.key
cert-file: classpath:conf/sdk.crt
# 账户配置
account:
key-file: classpath:conf/sdk.key
password: ""
# 合约配置
contract:
voting:
address: "" # 部署后更新
abi: classpath:contracts/Voting.abi
bin: classpath:contracts/Voting.bin
3.2 SDK配置类
java
// src/main/java/com/fisco/bcos/config/FiscoConfig.java
package com.fisco.bcos.config;
import lombok.Data;
import org.fisco.bcos.sdk.v3.BcosSDK;
import org.fisco.bcos.sdk.v3.client.Client;
import org.fisco.bcos.sdk.v3.config.ConfigOption;
import org.fisco.bcos.sdk.v3.config.exceptions.ConfigException;
import org.fisco.bcos.sdk.v3.crypto.keypair.CryptoKeyPair;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
@Configuration
@Data
public class FiscoConfig {
@Value("${fisco.bcos.chain-id}")
private int chainId;
@Value("${fisco.bcos.group-id}")
private int groupId;
@Value("${fisco.bcos.nodes}")
private String[] nodes;
@Value("${fisco.bcos.cert-path}")
private String certPath;
@Value("${fisco.bcos.cert-file}")
private String certFile;
@Value("${fisco.bcos.private-key}")
private String privateKey;
@Bean
public ConfigOption configOption() throws IOException, ConfigException {
// 读取证书文件
ClassPathResource caResource = new ClassPathResource(certPath);
ClassPathResource sslCertResource = new ClassPathResource(certFile);
ClassPathResource sslKeyResource = new ClassPathResource(privateKey);
// 构建配置选项
org.fisco.bcos.sdk.v3.config.model.ConfigProperty configProperty =
new org.fisco.bcos.sdk.v3.config.model.ConfigProperty();
configProperty.setCryptoMaterial(
new org.fisco.bcos.sdk.v3.config.model.CryptoMaterialConfig(
caResource.getFile().getAbsolutePath(),
sslCertResource.getFile().getAbsolutePath(),
sslKeyResource.getFile().getAbsolutePath(),
null,
null
)
);
configProperty.setNetwork(
new org.fisco.bcos.sdk.v3.config.model.NetworkConfig(
Arrays.asList(nodes)
)
);
configProperty.setAmop(null);
configProperty.setAccount(null);
configProperty.setThreadPool(null);
return new ConfigOption(configProperty, "config.toml");
}
@Bean
public BcosSDK bcosSDK(ConfigOption configOption) {
return new BcosSDK(configOption);
}
@Bean
public Client client(BcosSDK bcosSDK) {
return bcosSDK.getClient(groupId);
}
@Bean
public CryptoKeyPair cryptoKeyPair(Client client) throws IOException {
// 从文件加载私钥
ClassPathResource keyResource = new ClassPathResource(privateKey);
String privateKeyHex = org.apache.commons.io.FileUtils.readFileToString(
keyResource.getFile(), "UTF-8"
).trim();
// 创建密钥对
return client.getCryptoSuite().createKeyPair(privateKeyHex);
}
}
3.3 合约服务类
java
// src/main/java/com/fisco/bcos/service/VotingService.java
package com.fisco.bcos.service;
import com.fisco.bcos.config.FiscoConfig;
import lombok.extern.slf4j.Slf4j;
import org.fisco.bcos.sdk.v3.client.Client;
import org.fisco.bcos.sdk.v3.codec.datatypes.generated.tuples.generated.Tuple4;
import org.fisco.bcos.sdk.v3.codec.datatypes.generated.tuples.generated.Tuple2;
import org.fisco.bcos.sdk.v3.contract.Contract;
import org.fisco.bcos.sdk.v3.crypto.keypair.CryptoKeyPair;
import org.fisco.bcos.sdk.v3.model.TransactionReceipt;
import org.fisco.bcos.sdk.v3.transaction.codec.decode.TransactionDecoderInterface;
import org.fisco.bcos.sdk.v3.transaction.codec.decode.TransactionDecoderService;
import org.fisco.bcos.sdk.v3.transaction.model.exception.ContractException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.springframework.util.StreamUtils;
import javax.annotation.PostConstruct;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.util.*;
@Service
@Slf4j
public class VotingService {
@Autowired
private Client client;
@Autowired
private CryptoKeyPair cryptoKeyPair;
@Autowired
private FiscoConfig fiscoConfig;
@Value("${fisco.bcos.contract.voting.abi}")
private String abiPath;
@Value("${fisco.bcos.contract.voting.bin}")
private String binPath;
@Value("${fisco.bcos.contract.voting.address}")
private String contractAddress;
private Voting votingContract;
private String abiContent;
private String binContent;
private TransactionDecoderInterface txDecoder;
@PostConstruct
public void init() throws IOException {
// 读取ABI和BIN文件
ClassPathResource abiResource = new ClassPathResource(abiPath);
ClassPathResource binResource = new ClassPathResource(binPath);
abiContent = StreamUtils.copyToString(abiResource.getInputStream(), StandardCharsets.UTF_8);
binContent = StreamUtils.copyToString(binResource.getInputStream(), StandardCharsets.UTF_8);
txDecoder = new TransactionDecoderService(client.getCryptoSuite());
// 如果合约地址已配置,则加载合约
if (contractAddress != null && !contractAddress.isEmpty()) {
loadContract(contractAddress);
}
}
/**
* 部署投票合约
*/
public Map<String, Object> deployContract(List<String> proposalNames) throws ContractException, IOException {
log.info("Deploying voting contract with proposals: {}", proposalNames);
// 转换提案名称为bytes32
List<byte[]> bytes32Proposals = new ArrayList<>();
for (String name : proposalNames) {
byte[] bytes32 = new byte[32];
byte[] nameBytes = name.getBytes(StandardCharsets.UTF_8);
System.arraycopy(nameBytes, 0, bytes32, 0, Math.min(nameBytes.length, 32));
bytes32Proposals.add(bytes32);
}
// 部署合约
votingContract = Voting.deploy(
client,
cryptoKeyPair,
abiContent,
binContent,
bytes32Proposals
);
contractAddress = votingContract.getContractAddress();
log.info("Contract deployed at address: {}", contractAddress);
// 解析部署交易回执
TransactionReceipt receipt = votingContract.getTransactionReceipt();
Map<String, Object> result = new HashMap<>();
result.put("contractAddress", contractAddress);
result.put("transactionHash", receipt.getTransactionHash());
result.put("status", receipt.getStatus());
result.put("blockNumber", receipt.getBlockNumber());
result.put("gasUsed", receipt.getGasUsed());
result.put("proposals", proposalNames);
return result;
}
/**
* 加载已部署的合约
*/
public void loadContract(String contractAddress) {
log.info("Loading contract from address: {}", contractAddress);
votingContract = Voting.load(
contractAddress,
abiContent,
client,
cryptoKeyPair
);
this.contractAddress = contractAddress;
log.info("Contract loaded successfully");
}
/**
* 授予投票权
*/
public Map<String, Object> giveRightToVote(String voterAddress) throws ContractException {
log.info("Giving voting right to: {}", voterAddress);
TransactionReceipt receipt = votingContract.giveRightToVote(voterAddress);
Map<String, Object> result = parseReceipt(receipt);
log.info("Voting right granted to {}", voterAddress);
return result;
}
/**
* 批量授予投票权
*/
public Map<String, Object> giveRightToVoteBatch(List<String> voterAddresses) throws ContractException {
log.info("Batch giving voting rights to: {}", voterAddresses);
TransactionReceipt receipt = votingContract.giveRightToVoteBatch(voterAddresses);
Map<String, Object> result = parseReceipt(receipt);
log.info("Batch voting rights granted successfully");
return result;
}
/**
* 投票
*/
public Map<String, Object> vote(int proposalIndex) throws ContractException {
log.info("Voting for proposal index: {}", proposalIndex);
TransactionReceipt receipt = votingContract.vote(BigInteger.valueOf(proposalIndex));
Map<String, Object> result = parseReceipt(receipt);
// 解析投票事件
List<Map<String, Object>> events = decodeEvents(receipt, "Voted");
result.put("events", events);
log.info("Vote cast for proposal {}", proposalIndex);
return result;
}
/**
* 委托投票
*/
public Map<String, Object> delegate(String toAddress) throws ContractException {
log.info("Delegating vote to: {}", toAddress);
TransactionReceipt receipt = votingContract.delegate(toAddress);
Map<String, Object> result = parseReceipt(receipt);
// 解析委托事件
List<Map<String, Object>> events = decodeEvents(receipt, "Delegated");
result.put("events", events);
log.info("Vote delegated to {}", toAddress);
return result;
}
/**
* 获取获胜提案
*/
public Map<String, Object> getWinner() throws ContractException {
log.info("Getting winning proposal");
BigInteger winningIndex = votingContract.winningProposal();
byte[] winningNameBytes = votingContract.winnerName();
// 转换bytes32为字符串
String winningName = new String(winningNameBytes, StandardCharsets.UTF_8).trim();
Map<String, Object> result = new HashMap<>();
result.put("winningIndex", winningIndex);
result.put("winningName", winningName);
log.info("Winning proposal: {} (index: {})", winningName, winningIndex);
return result;
}
/**
* 结束投票并公布结果
*/
public Map<String, Object> endVoting() throws ContractException {
log.info("Ending voting and announcing results");
TransactionReceipt receipt = votingContract.endVoting();
Map<String, Object> result = parseReceipt(receipt);
// 解析投票结果事件
List<Map<String, Object>> events = decodeEvents(receipt, "VotingResult");
result.put("votingResult", events);
log.info("Voting ended successfully");
return result;
}
/**
* 获取所有提案信息
*/
public List<Map<String, Object>> getAllProposals() throws ContractException {
log.info("Getting all proposals");
BigInteger count = votingContract.getProposalsCount();
List<Map<String, Object>> proposals = new ArrayList<>();
for (int i = 0; i < count.intValue(); i++) {
Tuple2<byte[], BigInteger> proposal = votingContract.getProposal(BigInteger.valueOf(i));
Map<String, Object> proposalInfo = new HashMap<>();
proposalInfo.put("index", i);
proposalInfo.put("name", new String(proposal.getValue1(), StandardCharsets.UTF_8).trim());
proposalInfo.put("voteCount", proposal.getValue2());
proposals.add(proposalInfo);
}
log.info("Found {} proposals", proposals.size());
return proposals;
}
/**
* 获取提案数量
*/
public int getProposalCount() throws ContractException {
BigInteger count = votingContract.getProposalsCount();
return count.intValue();
}
/**
* 获取投票者信息
*/
public Map<String, Object> getVoterInfo(String voterAddress) throws ContractException {
log.info("Getting voter info for: {}", voterAddress);
Tuple4<BigInteger, Boolean, BigInteger, String> voter = votingContract.getVoter(voterAddress);
Map<String, Object> voterInfo = new HashMap<>();
voterInfo.put("address", voterAddress);
voterInfo.put("weight", voter.getValue1());
voterInfo.put("voted", voter.getValue2());
voterInfo.put("voteIndex", voter.getValue3());
voterInfo.put("delegate", voter.getValue4());
log.info("Voter info retrieved");
return voterInfo;
}
/**
* 获取合约地址
*/
public String getContractAddress() {
return contractAddress;
}
/**
* 解析交易回执
*/
private Map<String, Object> parseReceipt(TransactionReceipt receipt) {
Map<String, Object> result = new HashMap<>();
result.put("transactionHash", receipt.getTransactionHash());
result.put("status", receipt.getStatus());
result.put("blockNumber", receipt.getBlockNumber());
result.put("gasUsed", receipt.getGasUsed());
result.put("from", receipt.getFrom());
result.put("to", receipt.getTo());
result.put("output", receipt.getOutput());
return result;
}
/**
* 解码事件日志
*/
private List<Map<String, Object>> decodeEvents(TransactionReceipt receipt, String eventName) {
List<Map<String, Object>> events = new ArrayList<>();
try {
org.fisco.bcos.sdk.v3.codec.EventValues eventValues =
txDecoder.decodeEvents(receipt.getLogEntries(), abiContent, eventName);
if (eventValues != null && eventValues.getNonIndexedValues() != null) {
for (int i = 0; i < eventValues.getNonIndexedValues().size(); i++) {
Map<String, Object> event = new HashMap<>();
event.put("name", eventName);
event.put("value", eventValues.getNonIndexedValues().get(i).getValue());
events.add(event);
}
}
} catch (Exception e) {
log.error("Failed to decode events: {}", e.getMessage());
}
return events;
}
}
// 合约Java Wrapper类(通过sol2java.sh生成)
// 此处为简化版,实际应使用工具生成
class Voting extends Contract {
public Voting(String contractAddress, String abi, Client client, CryptoKeyPair credential) {
super(abi, contractAddress, client, credential);
}
public static Voting deploy(Client client, CryptoKeyPair credential, String abi, String binary, List<byte[]> proposalNames) {
return deploy(Voting.class, client, credential, abi, binary, "", proposalNames);
}
public static Voting load(String contractAddress, String abi, Client client, CryptoKeyPair credential) {
return new Voting(contractAddress, abi, client, credential);
}
public TransactionReceipt giveRightToVote(String voter) {
return executeTransaction("giveRightToVote", voter);
}
public TransactionReceipt giveRightToVoteBatch(List<String> voters) {
return executeTransaction("giveRightToVoteBatch", voters);
}
public TransactionReceipt vote(BigInteger proposalIndex) {
return executeTransaction("vote", proposalIndex);
}
public TransactionReceipt delegate(String to) {
return executeTransaction("delegate", to);
}
public BigInteger winningProposal() throws ContractException {
return call("winningProposal");
}
public byte[] winnerName() throws ContractException {
return call("winnerName");
}
public TransactionReceipt endVoting() {
return executeTransaction("endVoting");
}
public BigInteger getProposalsCount() throws ContractException {
return call("getProposalsCount");
}
public Tuple2<byte[], BigInteger> getProposal(BigInteger index) throws ContractException {
return call("getProposal", index);
}
public Tuple4<BigInteger, Boolean, BigInteger, String> getVoter(String voterAddress) throws ContractException {
return call("getVoter", voterAddress);
}
}
3.4 控制器类
java
// src/main/java/com/fisco/bcos/controller/VotingController.java
package com.fisco.bcos.controller;
import com.fisco.bcos.service.VotingService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/voting")
@Slf4j
public class VotingController {
@Autowired
private VotingService votingService;
/**
* 部署投票合约
*/
@PostMapping("/deploy")
public Map<String, Object> deployContract(@RequestBody Map<String, Object> request) {
try {
@SuppressWarnings("unchecked")
List<String> proposals = (List<String>) request.get("proposals");
if (proposals == null || proposals.isEmpty()) {
proposals = Arrays.asList("Proposal A", "Proposal B", "Proposal C");
}
Map<String, Object> result = votingService.deployContract(proposals);
result.put("success", true);
result.put("message", "Contract deployed successfully");
return result;
} catch (Exception e) {
log.error("Failed to deploy contract: {}", e.getMessage(), e);
return createErrorResponse("Deploy failed: " + e.getMessage());
}
}
/**
* 加载合约
*/
@PostMapping("/load")
public Map<String, Object> loadContract(@RequestBody Map<String, Object> request) {
try {
String contractAddress = (String) request.get("contractAddress");
votingService.loadContract(contractAddress);
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "Contract loaded successfully");
result.put("contractAddress", contractAddress);
return result;
} catch (Exception e) {
log.error("Failed to load contract: {}", e.getMessage(), e);
return createErrorResponse("Load failed: " + e.getMessage());
}
}
/**
* 授予投票权
*/
@PostMapping("/grant")
public Map<String, Object> grantVotingRight(@RequestBody Map<String, Object> request) {
try {
String voterAddress = (String) request.get("voterAddress");
Map<String, Object> result = votingService.giveRightToVote(voterAddress);
result.put("success", true);
result.put("message", "Voting right granted");
return result;
} catch (Exception e) {
log.error("Failed to grant voting right: {}", e.getMessage(), e);
return createErrorResponse("Grant failed: " + e.getMessage());
}
}
/**
* 批量授予投票权
*/
@PostMapping("/grant/batch")
public Map<String, Object> grantVotingRightBatch(@RequestBody Map<String, Object> request) {
try {
@SuppressWarnings("unchecked")
List<String> voterAddresses = (List<String>) request.get("voterAddresses");
Map<String, Object> result = votingService.giveRightToVoteBatch(voterAddresses);
result.put("success", true);
result.put("message", "Batch voting rights granted");
return result;
} catch (Exception e) {
log.error("Failed to grant batch voting rights: {}", e.getMessage(), e);
return createErrorResponse("Batch grant failed: " + e.getMessage());
}
}
/**
* 投票
*/
@PostMapping("/vote")
public Map<String, Object> vote(@RequestBody Map<String, Object> request) {
try {
Integer proposalIndex = (Integer) request.get("proposalIndex");
Map<String, Object> result = votingService.vote(proposalIndex);
result.put("success", true);
result.put("message", "Vote cast successfully");
return result;
} catch (Exception e) {
log.error("Failed to vote: {}", e.getMessage(), e);
return createErrorResponse("Vote failed: " + e.getMessage());
}
}
/**
* 委托投票
*/
@PostMapping("/delegate")
public Map<String, Object> delegate(@RequestBody Map<String, Object> request) {
try {
String toAddress = (String) request.get("toAddress");
Map<String, Object> result = votingService.delegate(toAddress);
result.put("success", true);
result.put("message", "Vote delegated successfully");
return result;
} catch (Exception e) {
log.error("Failed to delegate: {}", e.getMessage(), e);
return createErrorResponse("Delegate failed: " + e.getMessage());
}
}
/**
* 获取获胜提案
*/
@GetMapping("/winner")
public Map<String, Object> getWinner() {
try {
Map<String, Object> result = votingService.getWinner();
result.put("success", true);
result.put("message", "Winner retrieved successfully");
return result;
} catch (Exception e) {
log.error("Failed to get winner: {}", e.getMessage(), e);
return createErrorResponse("Get winner failed: " + e.getMessage());
}
}
/**
* 结束投票
*/
@PostMapping("/end")
public Map<String, Object> endVoting() {
try {
Map<String, Object> result = votingService.endVoting();
result.put("success", true);
result.put("message", "Voting ended successfully");
return result;
} catch (Exception e) {
log.error("Failed to end voting: {}", e.getMessage(), e);
return createErrorResponse("End voting failed: " + e.getMessage());
}
}
/**
* 获取所有提案
*/
@GetMapping("/proposals")
public Map<String, Object> getAllProposals() {
try {
List<Map<String, Object>> proposals = votingService.getAllProposals();
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "Proposals retrieved successfully");
result.put("proposals", proposals);
result.put("count", proposals.size());
return result;
} catch (Exception e) {
log.error("Failed to get proposals: {}", e.getMessage(), e);
return createErrorResponse("Get proposals failed: " + e.getMessage());
}
}
/**
* 获取投票者信息
*/
@GetMapping("/voter/{address}")
public Map<String, Object> getVoterInfo(@PathVariable String address) {
try {
Map<String, Object> voterInfo = votingService.getVoterInfo(address);
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("message", "Voter info retrieved successfully");
result.put("voter", voterInfo);
return result;
} catch (Exception e) {
log.error("Failed to get voter info: {}", e.getMessage(), e);
return createErrorResponse("Get voter info failed: " + e.getMessage());
}
}
/**
* 获取合约地址
*/
@GetMapping("/address")
public Map<String, Object> getContractAddress() {
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("contractAddress", votingService.getContractAddress());
return result;
}
/**
* 健康检查
*/
@GetMapping("/health")
public Map<String, Object> healthCheck() {
Map<String, Object> result = new HashMap<>();
result.put("status", "UP");
result.put("service", "FISCO BCOS Voting Service");
result.put("timestamp", System.currentTimeMillis());
return result;
}
private Map<String, Object> createErrorResponse(String message) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", message);
return error;
}
}
3.5 主应用类
java
// src/main/java/com/fisco/bcos/VotingApplication.java
package com.fisco.bcos;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableConfigurationProperties
public class VotingApplication {
public static void main(String[] args) {
SpringApplication.run(VotingApplication.class, args);
}
}
四、部署与测试
4.1 创建证书目录结构
XML
src/main/resources/
├── conf/
│ ├── ca.crt
│ ├── sdk.crt
│ └── sdk.key
├── contracts/
│ ├── Voting.abi
│ └── Voting.bin
└── application.yml
4.2 编译和运行
bash
# 编译项目
mvn clean package
# 运行应用
java -jar target/voting-demo-1.0.0.jar
# 或使用Maven运行
mvn spring-boot:run
4.3 使用Postman测试API
1. 部署合约
bash
POST http://localhost:8080/api/voting/deploy
Content-Type: application/json
{
"proposals": ["区块链升级", "社区治理", "技术改进"]
}
2. 授予投票权
bash
POST http://localhost:8080/api/voting/grant
Content-Type: application/json
{
"voterAddress": "0xYourVoterAddress"
}
3. 投票
bash
POST http://localhost:8080/api/voting/vote
Content-Type: application/json
{
"proposalIndex": 0
}
4. 查询获胜者
bash
GET http://localhost:8080/api/voting/winner
5. 查询所有提案
bash
GET http://localhost:8080/api/voting/proposals
五、高级功能扩展
5.1 事件监听器
java
// src/main/java/com/fisco/bcos/listener/VotingEventListener.java
package com.fisco.bcos.listener;
import lombok.extern.slf4j.Slf4j;
import org.fisco.bcos.sdk.v3.BcosSDK;
import org.fisco.bcos.sdk.v3.client.Client;
import org.fisco.bcos.sdk.v3.codec.EventValues;
import org.fisco.bcos.sdk.v3.eventsub.EventCallback;
import org.fisco.bcos.sdk.v3.eventsub.EventSubParams;
import org.fisco.bcos.sdk.v3.model.EventLog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.math.BigInteger;
import java.util.List;
@Component
@Slf4j
public class VotingEventListener {
@Autowired
private Client client;
@Autowired
private BcosSDK bcosSDK;
private String contractAddress = "your_contract_address";
@PostConstruct
public void init() {
subscribeToVotingEvents();
}
private void subscribeToVotingEvents() {
try {
// 订阅Voted事件
EventSubParams votedEventParams = new EventSubParams();
votedEventParams.setFromBlock(BigInteger.ZERO);
votedEventParams.setToBlock(BigInteger.valueOf(-1)); // 最新块
votedEventParams.setAddresses(Arrays.asList(contractAddress));
votedEventParams.setTopics(Arrays.asList(
client.getCryptoSuite().hash("Voted(address,uint256)")
));
bcosSDK.getEventSubscribe().subscribeEvent(votedEventParams, new EventCallback() {
@Override
public void onReceiveLog(EventLog eventLog) {
log.info("Voted event received: {}", eventLog);
// 处理投票事件
processVotedEvent(eventLog);
}
});
log.info("Subscribed to voting events");
} catch (Exception e) {
log.error("Failed to subscribe to events: {}", e.getMessage(), e);
}
}
private void processVotedEvent(EventLog eventLog) {
try {
String voter = "0x" + eventLog.getTopics().get(1).substring(26);
BigInteger proposalIndex = new BigInteger(eventLog.getTopics().get(2).substring(2), 16);
log.info("Voter {} voted for proposal {}", voter, proposalIndex);
// 可以在这里触发业务逻辑,如发送通知、更新数据库等
} catch (Exception e) {
log.error("Failed to process voted event: {}", e.getMessage());
}
}
}
5.2 数据库集成(可选)
java
// 存储投票记录到数据库
@Service
public class VotingRecordService {
@Autowired
private JdbcTemplate jdbcTemplate;
public void saveVotingRecord(String voter, int proposalIndex, String txHash) {
String sql = "INSERT INTO voting_records (voter_address, proposal_index, transaction_hash, vote_time) " +
"VALUES (?, ?, ?, NOW())";
jdbcTemplate.update(sql, voter, proposalIndex, txHash);
}
public List<Map<String, Object>> getVotingHistory(String voter) {
String sql = "SELECT * FROM voting_records WHERE voter_address = ? ORDER BY vote_time DESC";
return jdbcTemplate.queryForList(sql, voter);
}
}
六、注意事项
6.1 安全性考虑
-
私钥管理:不要将私钥硬编码在代码中,使用环境变量或密钥管理服务
-
输入验证:所有API输入都应进行严格验证
-
权限控制:实现基于角色的访问控制
-
事务管理:确保操作的原子性和一致性
6.2 性能优化
-
连接池:配置合适的连接池大小
-
缓存:对频繁查询的数据使用缓存
-
批量操作:使用批量方法减少交易数量
-
异步处理:对耗时操作使用异步处理
6.3 错误处理
-
合约异常:正确处理ContractException
-
网络异常:实现重试机制
-
资源清理:确保正确释放资源
-
日志记录:记录详细的错误信息以便排查
这个完整的Java SDK实现方案提供了从环境搭建、合约部署到API调用的全流程。您可以根据实际需求进行调整和扩展。