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`是否与此值一致