package io.ants.modules.utils.config;
import lombok.Data;
import java.io.Serializable;
@Data
public class TokenPayConfig implements Serializable {
private static final long serialVersionUID = 1L;
//tokenpay 站点地址
private String webSiteUrl="";
private String currency="USDT_TRC20";
private String ApiToken="";
private String callBackUrl="";
private String redirectUrl="";
}//通过本地配置项反序列化为tokenpay配置
package io.ants.modules.utils.factory;
import io.ants.common.utils.SpringContextUtils;
import io.ants.modules.sys.service.SysConfigService;
import io.ants.modules.utils.ConfigConstantEnum;
import io.ants.modules.utils.config.TokenPayConfig;
import io.ants.modules.utils.service.TokenPayService;
public class TokenPayFactory {
private static SysConfigService sysConfigService;
static {
TokenPayFactory.sysConfigService= (SysConfigService) SpringContextUtils.getBean("sysConfigService");
}
public static TokenPayService build(){
TokenPayConfig config = sysConfigService.getConfigObject(ConfigConstantEnum.TOKEN_PAY_CONF.getConfKey(), TokenPayConfig.class);
return new TokenPayService(config);
}
}//tokenPay 计算签名,发送待付订单类
package io.ants.modules.utils.service;
import com.alibaba.fastjson.JSONObject;
import io.ants.common.utils.DataTypeConversionUtil;
import io.ants.common.utils.HttpRequest;
import io.ants.common.utils.R;
import io.ants.modules.utils.config.TokenPayConfig;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.MessageDigest;
import java.util.Map;
import java.util.TreeMap;
public class TokenPayService {
protected Logger logger = LoggerFactory.getLogger(getClass());
private static TokenPayConfig config;
public TokenPayService(TokenPayConfig config) {
TokenPayService.config=config;
}
public TokenPayConfig getConfig(){
return TokenPayService.config;
}
public String getSignStr(JSONObject postData){
if (null==postData || postData.isEmpty()){
return "";
}
try{
// 使用TreeMap存储排序后的键值对
Map<String, Object> sortedData = new TreeMap<>(postData);
StringBuilder concatenatedStr = new StringBuilder();
for (String key : sortedData.keySet()) {
if (!key.equalsIgnoreCase("Signature")){
concatenatedStr.append(key).append("=").append(sortedData.get(key)).append("&");
}
}
String finalStr = concatenatedStr.substring(0, concatenatedStr.length() - 1) + config.getApiToken();
logger.info(finalStr);
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] data = finalStr.getBytes();
byte[] md5hash = md.digest(data);
StringBuilder hexString = new StringBuilder();
for (byte b : md5hash) {
String hex = Integer.toHexString(0xFF & b);
if (hex.length() == 1) {
hexString.append("0");
}
hexString.append(hex);
}
return hexString.toString();
}catch (Exception e){
e.printStackTrace();
}
return "";
}
public R sendOrder(String productName, Long amount, String serialNumber){
String eMsg="";
try{
JSONObject postData=new JSONObject();
postData.put("OutOrderId",serialNumber);
postData.put("OrderUserKey","productName="+productName);
postData.put("ActualAmount",amount);
postData.put("Currency",config.getCurrency());
postData.put("NotifyUrl",config.getCallBackUrl());
postData.put("RedirectUrl",config.getRedirectUrl());
postData.put("Signature",this.getSignStr(postData));
String r= HttpRequest.okHttpPost(config.getWebSiteUrl()+"/CreateOrder",postData.toJSONString());
if (StringUtils.isNotBlank(r)){
logger.info(r);
JSONObject jsonObject= DataTypeConversionUtil.string2Json(r);
if (null!=jsonObject && jsonObject.containsKey("success") && true==jsonObject.getBoolean("success")) {
return R.ok().put("data",jsonObject);
}else{
return R.error(r);
}
}
}catch (Exception e){
eMsg=e.getMessage();
e.printStackTrace();
}
return R.error(eMsg);
}
public static void main(String[] args) {
TokenPayConfig conf=new TokenPayConfig();
conf.setWebSiteUrl("http://tokenpay.xxx.top");
conf.setApiToken("666666");
conf.setCurrency("USDT_TRC20");
conf.setCallBackUrl("http://demo.xxx.com");
conf.setRedirectUrl("http://demo.xxx.com");
new TokenPayService(conf).sendOrder("cdn",1000l,"1122");
}
}callBack
工具类的DataTypeConversionUtil.string2Entity 自行封装
@Override
public String tokenPayCallback(HttpServletRequest request, HttpServletResponse response) {
if (!request.getMethod().equalsIgnoreCase("post")){
response.setStatus(403);
return "";
}
TokenPayService tokenPayService=TokenPayFactory.build();
//TokenPayConfig tokenPayConfig= tokenPayService.getConfig();
String payload = this.getPostBody(request);
if (StringUtils.isBlank(payload)){
response.setStatus(403);
return "";
}
JSONObject postData=DataTypeConversionUtil.string2Json(payload);
if (!postData.containsKey("Signature")){
response.setStatus(403);
return "";
}
String sign=tokenPayService.getSignStr(postData);
if (!sign.equalsIgnoreCase(postData.getString("Signature"))){
logger.error(" tokenPayCallback sign error:"+payload);
}else{
TokenPayCallBackBodyVo vo=DataTypeConversionUtil.string2Entity(payload,TokenPayCallBackBodyVo.class);
if (null!=vo){
logger.info("订单:"+payload);
if (1==vo.getStatus()){
// 处理支付成功逻辑
String orderId=vo.getOutOrderId();
Integer payPaid=Integer.parseInt(vo.getActualAmount()) ;
String outTradeId=vo.getId();
Integer payType= PayTypeEnum.PRO_TYPE_TOKENPAY.getId();
//记录支付成功事件,处理业务
//this.payResultSub(orderId,payPaid,outTradeId,payType,payload);
response.setStatus(200);
return "ok";
}
}
}
response.setStatus(403);
return "";
}////---原文档
# 其他系统对接`TokenPay`
> 也可参考仓库内现有的独角数卡插件对接
## 1. 创建`TokenPay`订单
URL: `/CreateOrder`
类型: `POST`
`Content-Type: application/json`
| 字段 | 类型 | 必填 | 说明 |
| ---- | ---- | ---- | ---- |
| OutOrderId | string | 是 | 外部订单号 |
| OrderUserKey | string | 是 | 支付用户标识,建议传用户联系方式或用户ID等`能识别用户身份的字符串`。使用动态地址时,会根据此字段关联收款地址,传递用户ID等,可以保证系统后续还会为此用户分配此地址。如需要每个订单一个新地址,可向此字段传递`外部订单号`。 |
| ActualAmount | decimal | 是 | 订单实际支付的法币金额,法币币种依据配置文件中的`BaseCurrency`决定,`保留两位小数` |
| Currency | Enum,支持`USDT_TRC20`、`TRX`等 | 是 | 加密货币的币种,直接以`原样字符串`传递即可 |
| PassThroughInfo | 不限长度的任意字符串 | 否 | 在回调通知或订单信息中原样返回 |
| NotifyUrl | string? | 否 | 异步通知URL |
| RedirectUrl | string? | 否 | 订单支付或过期后跳转的URL |
| Signature | string | 是 | 参数签名,参见下方参数签名生成规则 |
### ①示例POST参数
```json
{
"OutOrderId": "AJIHK72N34BR2CWG",
"OrderUserKey": "admin@qq.com",
"ActualAmount": 15,
"Currency": "TRX",
"NotifyUrl": "http://localhost:1011/pay/tokenpay/notify_url",
"RedirectUrl": "http://localhost:1011/pay/tokenpay/return_url?order_id=AJIHK72N34BR2CWG"
}
```
### ②按照ASCII排序后拼接
`ActualAmount=15&Currency=TRX&NotifyUrl=http://localhost:1011/pay/tokenpay/notify_url&OrderUserKey=admin@qq.com&OutOrderId=AJIHK72N34BR2CWG&RedirectUrl=http://localhost:1011/pay/tokenpay/return_url?order_id=AJIHK72N34BR2CWG`
异步通知密钥为:`666`
拼接密钥后
`ActualAmount=15&Currency=TRX&NotifyUrl=http://localhost:1011/pay/tokenpay/notify_url&OrderUserKey=admin@qq.com&OutOrderId=AJIHK72N34BR2CWG&RedirectUrl=http://localhost:1011/pay/tokenpay/return_url?order_id=AJIHK72N34BR2CWG666`
### ③计算MD5
`e9765880db6081496456283678e70152`
### ④POST参数增加`Signature`
```json
{
"OutOrderId": "AJIHK72N34BR2CWG",
"OrderUserKey": "admin@qq.com",
"ActualAmount": 15,
"Currency": "TRX",
"NotifyUrl": "http://localhost:1011/pay/tokenpay/notify_url",
"RedirectUrl": "http://localhost:1011/pay/tokenpay/return_url?order_id=AJIHK72N34BR2CWG",
"Signature": "e9765880db6081496456283678e70152"
}
```
### ⑤返回数据示例
创建订单成功的返回示例
```json
{
"success": true,
"message": "创建订单成功!",
"data": "http://127.0.0.1:5000/Pay?Id=6324ddd2-4677-7914-0010-702806ae9766",
"info": {
"ActualAmount": "15",//法币金额
"Amount": "227.34",//支付的区块链货币金额
"BaseCurrency": "CNY",//法币币种
"BlockChainName": "TRON",//付款区块链
"CurrencyName": "TRX", //付款币种
"ExpireTime": "2023-04-28 14:04:57", //付款过期时间
"Id": "644bc479-df0c-3f1c-00fe-9cb3012b148b", //订单Id
"OrderUserKey": "admin@qq.com", //用户识别Key
"OutOrderId": "AJIHK72N34BR2CWG", //商户订单号
"QrCodeBase64": "data:image/png;base64,xxxxxxxxx", //base64格式的图片
"QrCodeLink": "http://127.0.0.1:5000/GetQrCode?Id=644bc479-df0c-3f1c-00fe-9cb3012b148b", //二维码图片链接,如需修改图片尺寸,可拼接参数 &Size=xxx, 这里的xxx为数字,表示图片宽高,默认为300
"ToAddress": "TLUF41C386CMU1Wc8pTSCE4QaiZ2xkhTCb" //付款地址
}
}
```
创建订单失败的返回示例
```json
{
"success": false,
"message": "签名验证失败!"
}
```
## 2. `TokenPay`的异步回调参数
>如接口返回的状态码不是`200`,或者响应的内容不是字符串`ok`,视为回调失败。
>回调失败后将会在一分钟后重试,总共重试两次。
URL: `创建订单`接口传递的`NotifyUrl`字段内的URL
类型: `POST`
`Content-Type: application/json`
| 字段 | 类型 |说明 |
| ---- | ---- | ---- |
| Id | string | TokenPay内部订单号 |
| BlockTransactionId | string | 区块哈希 |
| OutOrderId | string | 外部订单号,调用 `创建订单` 接口时传递的外部订单号 |
| OrderUserKey | string | 支付用户标识,调用 `创建订单` 接口时传递的支付用户标识 |
| PayTime | string | 支付时间,示例:`2022-09-15 16:00:00` |
| BlockchainName | string | 区块链名称 |
| Currency | string | 币种,`USDT_TRC20`、`TRX`等,如配置了`EVMChains.json`,原生币格式为`EVM_[ChainNameEN]_[BaseCoin]`,ERC20代币格式为:`EVM_[ChainNameEN]_[Erc20.Name]_[ERC20Name]`,如BSC的原生币为`EVM_BSC_BNB`,BSC的USDT代币为`EVM_BSC_USDT_BEP20` |
| CurrencyName | string | 币种名称 |
| BaseCurrency | string | 法币币种,支持CNY、USD、EUR、GBP、AUD、HKD、TWD、SGD |
| Amount | string | 订单金额,此金额为法币`BaseCurrency`转换为`Currency`币种后的金额 |
| ActualAmount | string | 订单金额,此金额为法币金额 |
| FromAddress | string | 付款地址 |
| ToAddress | string | 收款地址 |
| Status | int | 状态 0 等待支付 1 已支付 2 订单过期 |
| PassThroughInfo | string | 创建订单如提供了此字段,在回调通知或订单信息中会原样返回 |
| Signature | string | 签名,`接口请务必验证此参数!!!`将除`Signature`字段外的所有字段,按照字母升序排序。按顺序拼接为`key1=value1&key2=value2`形式,然后在末尾拼接上`异步通知密钥`,将此字符串计算MD5,即为签名。 |
### ①示例POST参数
```json
{
"ActualAmount": "15",
"Amount": "34.91",
"BaseCurrency": "CNY",
"BlockChainName": "TRON",
"BlockTransactionId": "375859c36dc5f5d227b10912b5ec70d36dd34446028064956cb60cdbb74432f5",
"Currency": "TRX",
"CurrencyName": "TRX",
"ExpireTime": "2022-09-15 17:08:23",
"FromAddress": "TYYjzt6AWhe9hAg9DrhiYXEWKDksyohgQa",
"Id": "63234df7-55bf-93fc-0010-67be493c0c27",
"OrderUserKey": null,
"OutOrderId": "E6COE6FGZMO5AXSK",
"PassThroughInfo": null,
"PayTime": "2022-09-15 16:08:39",
"Status": 1,
"ToAddress": "TLUF41C386CMU1Wc8pTSCE4QaiZ2xkhTCb"
}
```
### ②按照ASCII排序后拼接
`ActualAmount=15&Amount=34.91&BaseCurrency=CNY&BlockchainName=TRON&BlockTransactionId=375859c36dc5f5d227b10912b5ec70d36dd34446028064956cb60cdbb74432f5&Currency=TRX&CurrencyName=TRX&ExpireTime=2022-09-15 17:08:23&FromAddress=TYYjzt6AWhe9hAg9DrhiYXEWKDksyohgQa&Id=63234df7-55bf-93fc-0010-67be493c0c27&OrderUserKey=&OutOrderId=E6COE6FGZMO5AXSK&PassThroughInfo=&PayTime=2022-09-15 16:08:39&Status=1&ToAddress=TLUF41C386CMU1Wc8pTSCE4QaiZ2xkhTCb`
异步通知密钥为:`666`
拼接密钥后
`ActualAmount=15&Amount=34.91&BaseCurrency=CNY&BlockchainName=TRON&BlockTransactionId=375859c36dc5f5d227b10912b5ec70d36dd34446028064956cb60cdbb74432f5&Currency=TRX&CurrencyName=TRX&ExpireTime=2022-09-15 17:08:23&FromAddress=TYYjzt6AWhe9hAg9DrhiYXEWKDksyohgQa&Id=63234df7-55bf-93fc-0010-67be493c0c27&OrderUserKey=&OutOrderId=E6COE6FGZMO5AXSK&PassThroughInfo=&PayTime=2022-09-15 16:08:39&Status=1&ToAddress=TLUF41C386CMU1Wc8pTSCE4QaiZ2xkhTCb666`
### ③计算MD5
`6a3bde5d21f5cfea0c8a81ea7f3a9d44`
对比POST中的`Signature`是否与此值一致