
实现了一个简单的商店功能。它定义了三个结构体:用户、商家和商品,以及对应的映射关系。它提供了一些方法用于注册用户和商家,创建商品,更新用户余额,购买商品等等。它还包括一些修饰符,用于限制只有用户或商家可以调用特定的方法。用户购买商品主要涉及到的是,检测当前用户是否有用户的权限,商家生产出售商品检测当前的用户是否有商家的权限。
主要的三个结构体
UserMerchantCommodity三个结构体分别是用户、商家、商品。
具体业务关系如下:
userMap 和 merchantMap 中将对应的用户地址和信息映射起来。createCommodity 方法创建一种新的商品,包括商品名称、价格和数量,并将此商品信息存储在 commodityMap 和 commoditys 数组中,同时将商品数量映射到相应的商家地址(即 commodityToMerchantMap)上。updateBalance 方法更新其账户余额。buyCommodity 方法购买已有的商品,其中需要传入商品ID和购买数量。如果用户余额足够且商品库存充足,则会购买成功,同时商家收到相应的款项。queryAllCommoditys 方法查看所有商品并按照分页方式展示。queryUserInfo 和 queryMerchantInfo 方法查询自己的信息。pragma solidity ^0.4.25;
pragma experimental ABIEncoderV2;
contract Shop {
// 用户的结构体
struct User {
address userAddress;
string userName;
uint256 userBalance;
Role role;
}
// 商家的结构体
struct Merchant {
address merchantAddress;
uint256 merchantBalance;
Role role;
}
// 商品的结构体
struct Commodity {
uint256 commodityId;
string commodityName;
uint256 commodityPrice;
uint256 commodityQuantity;
}
// 资产ID
uint256 public commodityCount;
// 角色枚举
enum Role { UserType,MerchantType }
// 地址映射用户信息
mapping(address => User) userMap;
// 地址映射商家信息
mapping(address => Merchant) merchantMap;
// 商品ID映射商品信息
mapping(uint256 => Commodity) commodityMap;
// 商品ID映射售卖的商家
mapping(uint256 => address) commodityToMerchantMap;
// 存储所有商品
Commodity[] commoditys;
// 判断当前是不是用户
modifier AuthUser(address _userAddress) {
require(userMap[_userAddress].role == Role.UserType,"当前不是用户");
_;
}
// 判断当前是不是商家
modifier AuthMerchant(address _merchantAddress) {
require(merchantMap[_merchantAddress].role == Role.MerchantType,"当前不是商家");
_;
}
// 注册事件
event Registered(address indexed _Address);
// 商家添加商品事件
event CreateCommodity(address indexed _Address,string _name);
// 用户更新余额事件
event UpdateBalance(address indexed _Address,uint256 indexed _amount);
// 用户购买商品事件
event BuyCommodity(address indexed _Address,uint256 indexed _commodityId);
// 企业注册
function registerUser(
address _userAddress,
string _userName
) public {
User memory user = User({
userAddress: _userAddress,
userName: _userName,
userBalance: 0,
role: Role.UserType
});
userMap[_userAddress] = user;
emit Registered(_userAddress);
}
// 商家注册
function registerMerchant(
address _merchantAddress,
uint256 _merchantBalance
) public {
Merchant memory merchant = Merchant({
merchantAddress: _merchantAddress,
merchantBalance: _merchantBalance,
role: Role.MerchantType
});
merchantMap[_merchantAddress] = merchant;
emit Registered(_merchantAddress);
}
// 创建资产
function createCommodity(
string memory _name,
uint256 _price,
uint256 _quantity
) public AuthMerchant(msg.sender) {
Commodity memory commodity = Commodity({
commodityId: commodityCount,
commodityName: _name,
commodityPrice: _price,
commodityQuantity: _quantity
});
commodityMap[commodityCount] = commodity;
commoditys.push(commodity);
commodityToMerchantMap[commodityCount] = msg.sender;
commodityCount++;
emit CreateCommodity(msg.sender,_name);
}
// 更新余额
function updateBalance(
address _userAddress,
uint256 _amount
) public AuthUser(msg.sender) {
userMap[_userAddress].userBalance += _amount;
emit UpdateBalance(msg.sender,_amount);
}
// 用户购买一个物品
function buyCommodity(
uint256 _commodityId,
uint256 _amount
) public AuthUser(msg.sender) {
require(userMap[msg.sender].userBalance >= _amount,"当前余额不足");
require(commodityMap[_commodityId].commodityQuantity != 0,"当前的物品没有库存");
userMap[msg.sender].userBalance -= _amount;
commodityMap[_commodityId].commodityQuantity--;
merchantMap[commodityToMerchantMap[_commodityId]].merchantBalance += _amount;
for (uint i = 0; i < commoditys.length; i++){
if (commoditys[i].commodityId == _commodityId){
commoditys[i].commodityQuantity--;
}
}
emit BuyCommodity(msg.sender,_commodityId);
}
// 分页查询商品
function queryAllCommoditys(
uint256 page,
uint256 pageSize
) public returns(Commodity[] memory) {
require(page > 0, "页数不能为0");
uint256 startIndex = (page - 1) * pageSize; // 计算起始索引
uint256 endIndex = startIndex + pageSize > commoditys.length ? commoditys.length : startIndex + pageSize; // 计算结束索引
Commodity[] memory CommodityArr = new Commodity[](endIndex - startIndex); // 创建每页大小的 Enterprise 数组
for (uint i = startIndex; i < endIndex; i++){
CommodityArr[i - startIndex] = commoditys[i];
}
return CommodityArr;
}
// 查询用户信息
function queryUserInfo() public returns(
address,
string memory,
uint256,
Role
) {
User memory user = userMap[msg.sender];
return (user.userAddress,user.userName,user.userBalance,user.role);
}
// 查询商家信息
function queryMerchantInfo() public AuthMerchant(msg.sender) returns(
address,
uint256,
Role
) {
Merchant memory merchant = merchantMap[msg.sender];
return (merchant.merchantAddress,merchant.merchantBalance,merchant.role);
}
}
使用基于FISCO BCOS区块链平台,以及WeBASE-Front部署智能合约,导出Java项目。

使用IDEA查看当前的项目。

使用场景
Webase-front部署智能合约时,私钥的切换通常用于以下场景:
在这些场景下,私钥的切换可以确保智能合约的安全性和可信度,同时也可以保护账户的私密信息不被泄露。
私钥存储的方式
数据库中或者本地文件。PKCS12加密存储,而PEM格式明文存储。这里我选择将私钥加密存储到数据库中,通过用户的地址去查询该地址的私钥或者公钥的方式,用户需要权限的时候,通过用户的地址获取私钥,通过加载用户的私钥传入调用合约的方法中。脱离默认的合约调用者。
针对Java SDK调用合约,有基于ABI和基于BIN的方式调用合约。
如下是完整的项目结构图。

虽然把私钥和公钥存储在Mysql中,但是还是不算很安全,所以存储的时候需要加盐加密。
create database shop default character set utf8mb4;
create table shop_userKey(
user_address varchar(60) comment '用户地址',
user_privateKey text comment '用户私钥',
user_publicKey text comment '用户公钥'
) comment '用户的公私钥表';
在dependencies中添加相对应的依赖。
compile group: 'com.baomidou', name: 'mybatis-plus-boot-starter', version: '3.5.0'
implementation 'mysql:mysql-connector-java:8.0.27'
implementation group: 'com.alibaba', name: 'fastjson', version: '2.0.24'
implementation group: 'org.apache.directory.studio', name: 'org.apache.commons.codec', version: '1.8'
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/shop?serverTimezone=UTC&useSSL=false
mybatis.configuration.cache-enabled=true
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
mybatis-plus.mapper-locations=classpath:/mapper/*.xml
mybatis-plus.global-config.id-type=auto
mybatis-plus.global-config.db-config.table-prefix=shop_
创建UserKey对象,主要是用来操作数据库。在用户注册的时候,需要给用户返回地址和公私钥,私钥仅提供一次下载。
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
@TableName(value = "shop_userKey")
public class UserKey {
@TableField(value = "user_address")
private String userAddress;
@TableField(value = "user_privateKey")
private String userPrivateKey;
@TableField(value = "user_publicKey")
private String userPublicKey;
}
创建User对象,用于返回前端页面用户的数据。
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class User {
// 用户地址
private String userAddress;
// 用户名称
private String userName;
// 用户余额
private int userBalance;
// 用户角色
private int role;
}
创建Merchant对象,用于返回前端页面商家的信息。
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.math.BigInteger;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Merchant {
// 商家地址
private String merchantAddress;
// 商家余额
private int merchantBalance;
// 用户角色
private int role;
}
创建Commodity的对象,用于返回前端页面的商品信息或者商品集合
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
import java.math.BigInteger;
@Data
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class Commodity {
// 商品ID
private int commodityId;
// 商品名称
private String commodityName;
// 商品价格
private int commodityPrice;
// 商品数量
private int commodityQuantity;
}
创建UserKeyMapper的接口,继承Mybatis-plus的接口,实现对数据库的CRUD操作。
这里主要是查询和删除两个操作,后续测试暂时需要用到这两个操作,如果有具体的场景还需要详细的操作。
@Mapper
public interface UserKeyMapper extends BaseMapper<UserKey> {
}
public interface UserKeyService extends IService<UserKey> {
public UserKey selectByUserAddress(String address);
public boolean deleteByUserAddress(String address);
}
@Service
public class UserKeyServiceImpl extends ServiceImpl<UserKeyMapper, UserKey> implements UserKeyService {
@Autowired
private UserKeyMapper userKeyMapper;
@Override
public UserKey selectByUserAddress(String address) {
if (address.isEmpty()){
return null;
}
LambdaQueryWrapper<UserKey> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserKey::getUserAddress, address);
return userKeyMapper.selectOne(queryWrapper);
}
@Override
public boolean deleteByUserAddress(String address) {
if (address.isEmpty()){
return false;
}
LambdaQueryWrapper<UserKey> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserKey::getUserAddress, address);
userKeyMapper.delete(queryWrapper);
return true;
}
}
AES算法是一种对称密钥加密算法,用于保护数据的机密性。它使用相同的密钥进行加密和解密操作。在AES算法中,数据被分成固定长度的块,并通过多轮的密钥混合和替换运算来加密。解密则通过逆向的操作将密文转换为明文。AES算法以其高安全性、高效性和广泛应用而受到广泛认可。
package org.example.Shop.utils;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Base64;
public class EncryptionUtils {
private static final String ALGORITHM = "AES"; // 加密算法
private static final String TRANSFORMATION = "AES/ECB/PKCS5Padding"; // 加密模式
private static final String KEY = "MySecretKey12345";
/**
* 加密数据
*
* @param data 待加密的数据
* @param KEY 密钥
* @return 加密后的数据
*/
public static String encrypt(String data) throws Exception {
Key secretKey = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptedBytes);
}
/**
* 解密数据
*
* @param encryptedData 加密后的数据
* @param KEY 密钥
* @return 解密后的数据
*/
public static String decrypt(String encryptedData) throws Exception {
Key secretKey = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decodedBytes = Base64.getDecoder().decode(encryptedData);
byte[] decryptedBytes = cipher.doFinal(decodedBytes);
return new String(decryptedBytes, StandardCharsets.UTF_8);
}
}
导出Java项目之后,在service层中,有写好的业务方法,可以直接调用,但是有一些方法和业务并不满足需求,比如需要切换账户调用合约,需要加载用户的私钥再调用合约进行交易。
Java SDK的org.fisco.bcos.sdk.crypto.CryptoSuite提供了账户生成功能。
// 创建非国密类型的CryptoSuite
CryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);
// 随机生成非国密公私钥对
CryptoKeyPair cryptoKeyPair = cryptoSuite.createKeyPair();
// 获取账户地址
String accountAddress = cryptoKeyPair.getAddress();
// 获取账户私钥
String hexPrivateKey = cryptoKeyPair.getHexPrivateKey();
// 获取账户公钥
String hexPublicKey = cryptoKeyPair.getHexPublicKey();
加载用户的私钥:
CryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);
// 加载用户私钥
CryptoKeyPair cryptoKeyPair = cryptoSuite.getKeyPairFactory().createKeyPair(EncryptionUtils.decrypt(userKey.getUserPrivateKey()));
由于我这里是随机生产的地址,所以没用pem和p12的方式保存和获取。
如下图:
商家需要创建一个苹果,但是创建苹果的用户和地址必须是商家的,不能是其他未注册或者是购买商品的用户去创建的。

在UserKeyServiceImpl的实现类中,添加一个方法makeNewUserKey(),用于生成用户新注册时候随机生成的地址和公私钥,以及加密公私钥返回给数据库。
@Service
public class UserKeyServiceImpl extends ServiceImpl<UserKeyMapper, UserKey> implements UserKeyService {
@Autowired
private UserKeyMapper userKeyMapper;
@SneakyThrows
@Override
public UserKey makeNewUserKey() {
// 创建非国密类型的CryptoSuite
CryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);
// 随机生成非国密公私钥对
CryptoKeyPair cryptoKeyPair = cryptoSuite.createKeyPair();
// 获取账户地址
String keyPairAddress = cryptoKeyPair.getAddress();
// 获取账户私钥
String hexPrivateKey = cryptoKeyPair.getHexPrivateKey();
// 获取账户公钥
String hexPublicKey = cryptoKeyPair.getHexPublicKey();
UserKey userKey = new UserKey();
userKey.setUserAddress(keyPairAddress);
// 对数据进行AES算法加密
userKey.setUserPrivateKey(EncryptionUtils.encrypt(hexPrivateKey));
userKey.setUserPublicKey(EncryptionUtils.encrypt(hexPublicKey));
// 加密后存储数据库
userKeyMapper.insert(userKey);
return userKey;
}
// 其他业务代码
......
}
如下的操作是在service层的默认导出的ShopService类中修改。

这是一个公共的方法,可以复用,用户和商家传递私钥对合约的调用。
/**
* 加载用户账户切换业务
* @param cryptoKeyPair
* @param params
* @return
* @throws Exception
*/
private TransactionResponse getTransactionResponse(CryptoKeyPair cryptoKeyPair,String funcName,List<Object> params) throws Exception {
// 过创建和使用AssembleTransactionProcessor对象来调用和查询等操作。不部署合约,那么就不需要复制binary文件
AssembleTransactionProcessor transactionProcessor = TransactionProcessorFactory
.createAssembleTransactionProcessor(client, cryptoKeyPair,"src/main/resources/abi/","");
// 使用同步方式发送交易
TransactionResponse transactionResponse = transactionProcessor
.sendTransactionAndGetResponseByContractLoader("Shop",this.address,funcName, params);
return transactionResponse;
}
商家注册之后需要保存地址信息,调用合约的时候携带了商家的地址,通过商家的地址查询获取数据库中加密的私钥,然后该方法主要是商家传入私钥和合约方法,通过加载商家的私钥,即可调用创建商品的方法。
/**
* 商家添加商品信息
* @param input
* @param cryptoKeyPair
* @param params
* @return
* @throws Exception
*/
public boolean createCommodity(CryptoKeyPair cryptoKeyPair,ShopCreateCommodityInputBO input) throws Exception {
TransactionResponse transactionResponse = getTransactionResponse(cryptoKeyPair,"createCommodity", input.toArgs());
return transactionResponse.getReceiptMessages().equals("Success");
}
/**
* 查询商家的信息
* @param cryptoKeyPair
* @param params
* @return
* @throws Exception
*/
public Merchant queryMerchantInfo(CryptoKeyPair cryptoKeyPair, List<Object> params) throws Exception {
TransactionResponse transactionResponse = getTransactionResponse(cryptoKeyPair,"queryMerchantInfo", params);
return Optional.of(transactionResponse)
.filter(response -> "Success".equals(response.getReceiptMessages()))
.map(response -> JSON.parseArray(response.getValues()))
.filter(objects -> objects.size() >= 3)
.map(objects -> {
Merchant merchant = new Merchant();
merchant.setMerchantAddress(objects.getString(0));
merchant.setMerchantBalance(objects.getInteger(1));
merchant.setRole(objects.getInteger(2));
return merchant;
})
.orElse(null);
}
需要用户加载私钥进行的操作的是:
这里的操作和上面是一样的,都是需要验证用户加载私钥后,是否为当前私钥的用户地址。
/**
* 用户更新账户余额
* @param cryptoKeyPair
* @param input
* @return
* @throws Exception
*/
public TransactionResponse updateBalance(CryptoKeyPair cryptoKeyPair,ShopUpdateBalanceInputBO input) throws Exception {
return getTransactionResponse(cryptoKeyPair,"updateBalance", input.toArgs());
}
/**
* 用户购买商品
* @param cryptoKeyPair
* @param input
* @return
* @throws Exception
*/
public TransactionResponse buyCommodity(CryptoKeyPair cryptoKeyPair,ShopBuyCommodityInputBO input) throws Exception {
return getTransactionResponse(cryptoKeyPair,"buyCommodity", input.toArgs());
}
@Service
@NoArgsConstructor
@Data
public class ShopService {
public static final String ABI = org.example.Shop.utils.IOUtil.readResourceAsString("abi/Shop.abi");
public static final String BINARY = org.example.Shop.utils.IOUtil.readResourceAsString("bin/ecc/Shop.bin");
public static final String SM_BINARY = org.example.Shop.utils.IOUtil.readResourceAsString("bin/sm/Shop.bin");
@Value("${system.contract.shopAddress}")
private String address;
@Autowired
private Client client;
AssembleTransactionProcessor txProcessor;
@PostConstruct
public void init() throws Exception {
this.txProcessor = TransactionProcessorFactory.createAssembleTransactionProcessor(this.client, this.client.getCryptoSuite().getCryptoKeyPair());
}
/**
* 用户更新账户余额
* @param cryptoKeyPair
* @param input
* @return
* @throws Exception
*/
public TransactionResponse updateBalance(CryptoKeyPair cryptoKeyPair,ShopUpdateBalanceInputBO input) throws Exception {
return getTransactionResponse(cryptoKeyPair,"updateBalance", input.toArgs());
}
/**
* 用户购买商品
* @param cryptoKeyPair
* @param input
* @return
* @throws Exception
*/
public TransactionResponse buyCommodity(CryptoKeyPair cryptoKeyPair,ShopBuyCommodityInputBO input) throws Exception {
return getTransactionResponse(cryptoKeyPair,"buyCommodity", input.toArgs());
}
public TransactionResponse registerUser(ShopRegisterUserInputBO input) throws Exception {
return this.txProcessor.sendTransactionAndGetResponse(this.address, ABI, "registerUser", input.toArgs());
}
/**
* 查询商家的信息
* @param cryptoKeyPair
* @param params
* @return
* @throws Exception
*/
public Merchant queryMerchantInfo(CryptoKeyPair cryptoKeyPair, List<Object> params) throws Exception {
TransactionResponse transactionResponse = getTransactionResponse(cryptoKeyPair,"queryMerchantInfo", params);
return Optional.of(transactionResponse)
.filter(response -> "Success".equals(response.getReceiptMessages()))
.map(response -> JSON.parseArray(response.getValues()))
.filter(objects -> objects.size() >= 3)
.map(objects -> {
Merchant merchant = new Merchant();
merchant.setMerchantAddress(objects.getString(0));
merchant.setMerchantBalance(objects.getInteger(1));
merchant.setRole(objects.getInteger(2));
return merchant;
})
.orElse(null);
}
/**
* 加载用户账户切换业务
* @param cryptoKeyPair
* @param params
* @return
* @throws Exception
*/
private TransactionResponse getTransactionResponse(CryptoKeyPair cryptoKeyPair,String funcName,List<Object> params) throws Exception {
AssembleTransactionProcessor transactionProcessor = TransactionProcessorFactory
.createAssembleTransactionProcessor(client, cryptoKeyPair,"src/main/resources/abi/","");
TransactionResponse transactionResponse = transactionProcessor
.sendTransactionAndGetResponseByContractLoader("Shop",this.address,funcName, params);
return transactionResponse;
}
/**
* 商家添加商品信息
* @param input
* @param cryptoKeyPair
* @param params
* @return
* @throws Exception
*/
public boolean createCommodity(CryptoKeyPair cryptoKeyPair,ShopCreateCommodityInputBO input) throws Exception {
TransactionResponse transactionResponse = getTransactionResponse(cryptoKeyPair,"createCommodity", input.toArgs());
return transactionResponse.getReceiptMessages().equals("Success");
}
public TransactionResponse queryAllCommoditys(ShopQueryAllCommoditysInputBO input) throws Exception {
return this.txProcessor.sendTransactionAndGetResponse(this.address, ABI, "queryAllCommoditys", input.toArgs());
}
public TransactionResponse queryUserInfo() throws Exception {
return this.txProcessor.sendTransactionAndGetResponse(this.address, ABI, "queryUserInfo", Arrays.asList());
}
public TransactionResponse registerMerchant(ShopRegisterMerchantInputBO input) throws Exception {
return this.txProcessor.sendTransactionAndGetResponse(this.address, ABI, "registerMerchant", input.toArgs());
}
public CallResponse commodityCount() throws Exception {
return this.txProcessor.sendCall(this.client.getCryptoSuite().getCryptoKeyPair().getAddress(), this.address, ABI, "commodityCount", Arrays.asList());
}
}
这里主要是做测试,为了更加的安全隐患,实际开发模式中需要使用POST请求,不能暴露出来。
主要实现的是:
| API接口 | 请求类型 | 请求参数 |
|---|---|---|
| /register | GET | 无 |
| /register/user | POST | ShopRegisterUserInputBO |
| /register/merchant | POST | ShopRegisterMerchantInputBO |
用户的请求接口示例:
http://localhost:8088/register/user
{
"_userAddress": "0xaf9f11d639600dffa32ce7f1c3474d1e280cf791",
"_userName": "张三"
}
商家的请求接口示例:
http://localhost:8088/register/merchant
{
"_merchantAddress": "0xaf9f11d639600dffa32ce7f1c3474d1e280cf791",
"_merchantBalance": 1000
}
@RestController
@RequestMapping("register")
public class RegisterController {
@Autowired
private UserKeyService userKeyService;
@Autowired
private ShopService shopService;
/**
* 用户注册创建新的随机地址和公私钥
* @return
*/
@SneakyThrows
@GetMapping
public CommonResponse getAddressAndPrivateKey(){
return CommonResponse.ok("Success",userKeyService.makeNewUserKey());
}
/**
* 用户注册
* @param registerUserInputBO
* @return
*/
@SneakyThrows
@PostMapping("user")
public CommonResponse registerUser(@RequestBody ShopRegisterUserInputBO registerUserInputBO){
if (StringUtils.isBlank(registerUserInputBO.get_userAddress()) || StringUtils.isBlank(registerUserInputBO.get_userName())){
return CommonResponse.fail("400",new RuntimeException("参数为空"));
}
TransactionResponse transactionResponse = shopService.registerUser(registerUserInputBO);
return CommonResponse.ok("注册成功",transactionResponse.getReceiptMessages());
}
/**
* 商家注册
* @param registerMerchantInputBO
* @return
*/
@SneakyThrows
@PostMapping("merchant")
public CommonResponse registerMerchant(@RequestBody ShopRegisterMerchantInputBO registerMerchantInputBO){
if (StringUtils.isBlank(registerMerchantInputBO.get_merchantAddress())){
return CommonResponse.fail("400",new RuntimeException("参数为空"));
}
TransactionResponse transactionResponse = shopService.registerMerchant(registerMerchantInputBO);
return CommonResponse.ok("注册成功",transactionResponse.getReceiptMessages());
}
}
主要实现的是:
| API接口 | 请求类型 | 请求参数 |
|---|---|---|
| /users/{address} | POST | ShopQueryAllCommoditysInputBO |
| /users/{address} | POST | ShopBuyCommodityInputBO |
| /users/{address} | PUT | BigInteger |
用户的接口示例:
// 查询商品的列表
POST http://localhost:8088/users/{address}
{
"page": 1,
"pageSize": 4
}
// 用户购买商品
POST http://localhost:8088/users/{address}
{
"_commodityId": 1,
"_amount": 100
}
// 更新用户的余额
PUT http://localhost:8088/users/{address}?balance=1000
@RestController
@RequestMapping("users")
public class UserController {
@Autowired
private Client client;
@Autowired
private ShopService shopService;
@Autowired
private UserKeyService userKeyService;
@SneakyThrows
@GetMapping("{address}")
public CommonResponse getCommodityList(@PathVariable String address,@RequestBody ShopQueryAllCommoditysInputBO commoditysInputBO){
if (address.isEmpty() || commoditysInputBO.toArgs().isEmpty()){
return CommonResponse.fail("400",new RuntimeException("查询异常"));
}
TransactionResponse transactionResponse = shopService.queryAllCommoditys(commoditysInputBO);
if (transactionResponse.getReceiptMessages().equals("Success")){
String responseValues = transactionResponse.getValues();
int pageSize = commoditysInputBO.getPageSize();
// 使用 intStream.range() 方法生成 [0, pageSize-1] 的整数流
List<Commodity> commodities = IntStream.range(0, pageSize)
// 把每一个整数映射成一个 JSONArray 对象
.mapToObj(i -> JSON.parseArray(responseValues).getJSONArray(0).getJSONArray(i))
// 把每个 JSONArray 转换成 Commodity 对象
.map(jsonArray -> {
// 根据 jsonArray 创建一个 Commodity 对象
Commodity commodity = new Commodity();
commodity.setCommodityId((Integer) jsonArray.get(0));
commodity.setCommodityName((String) jsonArray.get(1));
commodity.setCommodityPrice((Integer) jsonArray.get(2));
commodity.setCommodityQuantity((Integer) jsonArray.get(3));
return commodity;
})
// 把所有 Commodity 对象收集到一个 List 中
.collect(Collectors.toList());
return CommonResponse.ok("查询成功",commodities);
}
return CommonResponse.fail("400",new RuntimeException("查询异常"));
}
@SneakyThrows
@PostMapping("{address}")
public CommonResponse buyCommodity(@PathVariable String address, @RequestBody ShopBuyCommodityInputBO commodityInputBO){
if (StringUtils.isBlank(address)){
return CommonResponse.fail("400", new RuntimeException("当前用户无效"));
}
// 获取当前用户的私钥,并使用其创建加密套件及密钥对
UserKey userKey = userKeyService.selectByUserAddress(address);
CryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);
CryptoKeyPair cryptoKeyPair = cryptoSuite.getKeyPairFactory().createKeyPair(EncryptionUtils.decrypt(userKey.getUserPrivateKey()));
// 购买商品操作
TransactionResponse transactionResponse = shopService.buyCommodity(cryptoKeyPair, commodityInputBO);
if (transactionResponse.getReceiptMessages().equals("Success")){
return CommonResponse.ok("购买成功",address);
}
return CommonResponse.fail("400",new RuntimeException("购买失败"));
}
@SneakyThrows
@PutMapping("{address}")
public CommonResponse updateBalance(@PathVariable String address,@RequestParam BigInteger balance){
if (StringUtils.isBlank(address)){
return CommonResponse.fail("400", new RuntimeException("当前用户无效"));
}
// 获取当前用户的私钥,并使用其创建加密套件及密钥对
UserKey userKey = userKeyService.selectByUserAddress(address);
CryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);
CryptoKeyPair cryptoKeyPair = cryptoSuite
.getKeyPairFactory()
.createKeyPair(EncryptionUtils.decrypt(userKey.getUserPrivateKey()));
ShopUpdateBalanceInputBO balanceInputBO = new ShopUpdateBalanceInputBO(address, balance);
// 更新账户余额操作
TransactionResponse transactionResponse = shopService.updateBalance(cryptoKeyPair,balanceInputBO);
if (transactionResponse.getReceiptMessages().equals("Success")){
return CommonResponse.ok("更新成功",balance);
}
return CommonResponse.fail("400",new RuntimeException("更新失败"));
}
}
主要实现的是:
| API接口 | 请求类型 | 请求参数 |
|---|---|---|
| /merchants?address={address} | GET | address |
| /merchants/address | POST | ShopCreateCommodityInputBO |
商家的接口请求示例:
GET http://localhost:8088/merchants/?address={address}
POST http://localhost:8088/merchants/address
{
"_name": "苹果",
"_price": 100,
"_quantity": 20
}
@RestController
@RequestMapping("merchants")
public class MerchantController {
@Autowired
private UserKeyService userKeyService;
@Autowired
private ShopService shopService;
/**
* 查询商家信息接口
*
* @param address 用户地址(商家地址)
* @return 响应结果对象
*/
@SneakyThrows
@GetMapping
public CommonResponse queryMerchant(@RequestParam String address) {
if (StringUtils.isEmpty(address)){
return CommonResponse.fail("400",new RuntimeException("查询失败"));
}
// 获取商家用户的私钥,并使用其创建加密套件及密钥对
UserKey userKey = userKeyService.selectByUserAddress(address);
CryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);
CryptoKeyPair cryptoKeyPair = null;
cryptoKeyPair = cryptoSuite.getKeyPairFactory().createKeyPair(EncryptionUtils.decrypt(userKey.getUserPrivateKey()));
List<Object> params = new ArrayList<>();
// 调用查询商家信息 假设这是商家的内部信息 普通用户不可查询
Merchant merchant = shopService.queryMerchantInfo(cryptoKeyPair, params);
if (Objects.isNull(merchant)){
return CommonResponse.fail("400",new RuntimeException("当前角色不是商家"));
}
return CommonResponse.ok("查询成功",merchant);
}
/**
* 创建商品接口
*
* @param address 用户地址(商家地址)
* @param createCommodityInputBO 商品信息参数对象
* @return 响应结果对象
*/
@SneakyThrows
@PostMapping("{address}")
public CommonResponse createCommodity(@PathVariable String address,@RequestBody ShopCreateCommodityInputBO createCommodityInputBO){
if (StringUtils.isEmpty(createCommodityInputBO.get_name())){
return CommonResponse.fail("400",new RuntimeException("商品的名称不能为空"));
}
// 获取商家用户的私钥,并使用其创建加密套件及密钥对
UserKey userKey = userKeyService.selectByUserAddress(address);
System.out.println(userKey.getUserPrivateKey());
CryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);
CryptoKeyPair cryptoKeyPair = cryptoSuite.getKeyPairFactory().createKeyPair(EncryptionUtils.decrypt(userKey.getUserPrivateKey()));
// 商家调用创建商品
boolean commodity = shopService.createCommodity(cryptoKeyPair, createCommodityInputBO);
return CommonResponse.ok("创建成功",commodity);
}
}
GET http://localhost:8088/register
POST http://localhost:8088/register/user
1.发送GET请求获取如下的信息:
0x17b1730bb77db57313ac59c20c0aaae2efbaeab6dzuF553WpMInWPeLqjP/dyZ4s381sEPeQni4+mY/L7Y50P46JZkLXiV7RaMYXjMxz1AD+cq9ktjWH4ZCX2jOXAbb0lJO2pSgRxctIDarhYM=
2.使用上面的用户地址,去注册一个叫”张三“的用户。如下图是注册成功。

3.这里显示注册成功,然后我们通过AES算法的加密工具类解密如下:
得到解密后的私钥: c8c25e5a9bc3485e90a2cab239861eae9e51a2d57bbb60ea7eda9a4c83267eac

4.在WeBASE-Front中导入私钥,并且创建zhangsan的测试用户。

成功导入之后,直接使用这个账号调用queryUserInfo的合约。因为我写查看用户合约的时候我加了检测当前地址是否有权限。

可以发现如下是直接交易成功,并且我是能查看到张三的信息的。
0x17B1730bb77db57313aC59C20C0AAAe2EFBAeAB6对比一下上面接口的测试获取的用户地址,用户地址一模一样。

GET http://localhost:8088/register
POST http://localhost:8088/register/merchant
1.发送GET请求获取如下的信息:
0x4ab865d51fbf01e2a102a86e56fe9e7358382dceSxWg6lFxmvmp/uyIA0GBmkE1iaCh6jioFigyk+VSbTP4OuyK4HGSbinGKFTzDmwft7dz5hOsiJyUgQ0lQXeuqwbb0lJO2pSgRxctIDarhYM=
2.使用新的用户地址注册商家,注册资金为1000元。

3.通过解密后得到商家的私钥
f143333a17243b5d804c4162adbb68bf28fc75dfb7e3db680d48a1f1e2dfcb48在WeBASE-Front导入私钥商家的名称为merchant,调用查询商家的方法。

调用当前的queryMerchantInfo的函数,查看交易如下:
0x4aB865D51fbf01E2A102A86e56Fe9E7358382DcE
当我使用zhangsan的用户去查看企业的内部信息的时候,会交易失败。显示当前不是商家。

4.使用接口测试查询商家的信息,结果和上面一样。

POST http://localhost:8088/merchants/0x4ab865d51fbf01e2a102a86e56fe9e7358382dce
1.使用商家的地址0x4ab865d51fbf01e2a102a86e56fe9e7358382dce,添加两个商品,一个是苹果,一个是香蕉。如下添加成功。


2.使用用户分页查询当前的所有数据。

PUT http://localhost:8088/users/{address}?balance=1000
POST http://localhost:8088/users/{address}
1.给用户的账户更新余额100元。然后查询当前用户的余额。

如下是使用WeBASE-Front查看的余额。

2.用户购买商品,确定只有注册的用户是可以购买成功的。

通过WeBASE-Front调用zhangsan的账户查看个人信息,成功扣款和购买商品。

查看当前的所有商品信息,香蕉已经扣款数量为1。

查看当前的商家余额,已经到账100元。

1.我特意在合约中用户购买商品的业务写了触发事件,假设用户购买了该商品,会记录当前的地址是谁购买的,可以通过查看Event事件的msg.sender的地址是不是该用户的。
如下是合约的详细部分:

2.在用户购买商品的部分,也就是用户购买商品时候访问的接口,添加获取当前的合约调用的用户地址。用log的方式查看。
如下是添加的测试代码,用于观察。
@SneakyThrows
@PostMapping("{address}")
public CommonResponse buyCommodity(@PathVariable String address, @RequestBody ShopBuyCommodityInputBO commodityInputBO){
if (StringUtils.isBlank(address)){
return CommonResponse.fail("400", new RuntimeException("当前用户无效"));
}
// 获取当前用户的私钥,并使用其创建加密套件及密钥对
UserKey userKey = userKeyService.selectByUserAddress(address);
CryptoSuite cryptoSuite = new CryptoSuite(CryptoType.ECDSA_TYPE);
CryptoKeyPair cryptoKeyPair = cryptoSuite.getKeyPairFactory().createKeyPair(EncryptionUtils.decrypt(userKey.getUserPrivateKey()));
// 购买商品操作
TransactionResponse transactionResponse = shopService.buyCommodity(cryptoKeyPair, commodityInputBO);
if (transactionResponse.getReceiptMessages().equals("Success")){
log.info("======================================================");
log.info("++++++++++++当前的合约调用者:" + transactionResponse.getTransactionReceipt().getFrom());
log.info("++++++++++++当前的合约调用者:" + transactionResponse.getTransactionReceipt().getLogs().get(0).getTopics().get(1));
return CommonResponse.ok("购买成功",address);
}
return CommonResponse.fail("400",new RuntimeException("购买失败"));
}
调用此接口继续测试。

3.查看当前的控制台输出情况。

4.使用WeBASE-Front的Event的事件查看。
从日志和Event事件查看,合约的调用者都是0x17b1730bb77db57313ac59c20c0aaae2efbaeab6地址。

根据以上的案例,可以理解私钥加载的使用场景。
我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div
我有一个Ruby程序,它使用rubyzip压缩XML文件的目录树。gem。我的问题是文件开始变得很重,我想提高压缩级别,因为压缩时间不是问题。我在rubyzipdocumentation中找不到一种为创建的ZIP文件指定压缩级别的方法。有人知道如何更改此设置吗?是否有另一个允许指定压缩级别的Ruby库? 最佳答案 这是我通过查看rubyzip内部创建的代码。level=Zlib::BEST_COMPRESSIONZip::ZipOutputStream.open(zip_file)do|zip|Dir.glob("**/*")d
类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc
很好奇,就使用rubyonrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提
假设我做了一个模块如下:m=Module.newdoclassCendend三个问题:除了对m的引用之外,还有什么方法可以访问C和m中的其他内容?我可以在创建匿名模块后为其命名吗(就像我输入“module...”一样)?如何在使用完匿名模块后将其删除,使其定义的常量不再存在? 最佳答案 三个答案:是的,使用ObjectSpace.此代码使c引用你的类(class)C不引用m:c=nilObjectSpace.each_object{|obj|c=objif(Class===objandobj.name=~/::C$/)}当然这取决于
我正在尝试使用ruby和Savon来使用网络服务。测试服务为http://www.webservicex.net/WS/WSDetails.aspx?WSID=9&CATID=2require'rubygems'require'savon'client=Savon::Client.new"http://www.webservicex.net/stockquote.asmx?WSDL"client.get_quotedo|soap|soap.body={:symbol=>"AAPL"}end返回SOAP异常。检查soap信封,在我看来soap请求没有正确的命名空间。任何人都可以建议我
关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。
我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po