AllinpaySDK.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. package common.utils.tl;
  2. import cn.hutool.core.util.RandomUtil;
  3. import cn.hutool.crypto.SmUtil;
  4. import cn.hutool.crypto.asymmetric.SM2;
  5. import cn.hutool.json.JSONObject;
  6. import cn.hutool.json.JSONUtil;
  7. import com.jfinal.kit.HttpKit;
  8. import com.jfinal.kit.StrKit;
  9. import java.nio.charset.StandardCharsets;
  10. import java.util.*;
  11. import java.util.Base64;
  12. /**
  13. * 通联支付SDK
  14. * 支持H5收银台支付功能
  15. *
  16. * @author skyff
  17. * @date 2024
  18. */
  19. public class AllinpaySDK {
  20. // 生产环境配置
  21. private static final String BASE_URL = "https://vsp.allinpay.com";
  22. private static final String UNIFIED_PAY_URL = BASE_URL + "/apiweb/unitorder/pay";
  23. private static final String QUERY_URL = "https://syb.allinpay.com/apiweb/unitorder/query";
  24. // 商户配置信息(测试用静态配置)
  25. private static final String APP_ID = "00000051";
  26. private static final String CUS_ID = "990581007426001";
  27. private static final String VERSION = "11";
  28. private static final String SIGN_TYPE = "SM2";
  29. // SM2密钥对(测试用)
  30. private static final String SM2_PRIVATE_KEY = "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQgNqz1EieIP8QVzV7vEmx5e8f7XN7/MIzoeXgEinxcG0agCgYIKoEcz1UBgi2hRANCAAQNfkEgaCQ4cdZ4aD2LWMcnkk5LALQfL05oY8x8XQDIyUM44N15YcTwtFNvHYgyeNRa93vlEUutp935n6rp4yuf";
  31. // 通联公钥(用于验签,实际使用时需要从通联获取)
  32. private static final String ALLINPAY_PUBLIC_KEY = "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAEDX5BIGgkOHHWeGg9i1jHJ5JOSwC0Hy9OaGPMfF0AyMlDOODdeWHE8LRTbx2IMnjUWvd75RFLrae9+Z+q6eMrnw==";
  33. private static final SM2 sm2 = SmUtil.sm2(SM2_PRIVATE_KEY, ALLINPAY_PUBLIC_KEY);
  34. /**
  35. * 统一支付接口
  36. *
  37. * @param trxamt 交易金额(分)
  38. * @param reqsn 商户交易单号
  39. * @param paytype 交易方式
  40. * @param body 订单标题
  41. * @param remark 备注
  42. * @param acct 支付平台用户标识
  43. * @param notify_url 交易结果通知地址
  44. * @param limit_pay 支付限制
  45. * @param idno 证件号
  46. * @param truename 付款人真实姓名
  47. * @param sub_appid 微信子appid
  48. * @return 支付结果
  49. */
  50. public static PaymentResult unifiedPay(Long trxamt, String reqsn, String paytype, String body, String remark,
  51. String acct, String notify_url, String limit_pay, String idno,
  52. String truename, String sub_appid) {
  53. try {
  54. // 1. 构建请求参数
  55. Map<String, String> params = new TreeMap<>();
  56. params.put("appid", APP_ID);
  57. params.put("cusid", CUS_ID);
  58. params.put("version", VERSION);
  59. params.put("signtype", SIGN_TYPE);
  60. params.put("randomstr", RandomUtil.randomString(8));
  61. params.put("trxamt", String.valueOf(trxamt));
  62. params.put("reqsn", reqsn);
  63. params.put("paytype", paytype);
  64. params.put("body", body);
  65. params.put("remark", remark);
  66. params.put("notify_url", notify_url);
  67. // 添加可选参数
  68. if (StrKit.notBlank(acct)) params.put("acct", acct);
  69. if (StrKit.notBlank(limit_pay)) params.put("limit_pay", limit_pay);
  70. if (StrKit.notBlank(idno)) params.put("idno", idno);
  71. if (StrKit.notBlank(truename)) params.put("truename", truename);
  72. if (StrKit.notBlank(sub_appid)) params.put("sub_appid", sub_appid);
  73. // 2. 生成签名
  74. String sign = generateSign(params);
  75. params.put("sign", sign);
  76. // 3. 发送请求
  77. String response = sendPostRequest(UNIFIED_PAY_URL, params);
  78. // 4. 解析响应
  79. return parsePaymentResponse(response);
  80. } catch (Exception e) {
  81. e.printStackTrace();
  82. return PaymentResult.error("支付请求失败: " + e.getMessage());
  83. }
  84. }
  85. /**
  86. * 查询支付结果
  87. *
  88. * @param reqsn 商户交易单号
  89. * @return 查询结果
  90. */
  91. public static QueryResult queryPayment(String reqsn) {
  92. try {
  93. // 1. 构建请求参数
  94. Map<String, String> params = new TreeMap<>();
  95. params.put("appid", APP_ID);
  96. params.put("cusid", CUS_ID);
  97. params.put("version", VERSION);
  98. params.put("signtype", SIGN_TYPE);
  99. params.put("randomstr", RandomUtil.randomString(8));
  100. params.put("reqsn", reqsn);
  101. // 2. 生成签名
  102. String sign = generateSign(params);
  103. params.put("sign", sign);
  104. // 3. 发送请求
  105. String response = sendPostRequest(QUERY_URL, params);
  106. // 4. 解析响应
  107. return parseQueryResponse(response);
  108. } catch (Exception e) {
  109. e.printStackTrace();
  110. return QueryResult.error("查询失败: " + e.getMessage());
  111. }
  112. }
  113. /**
  114. * 处理异步通知
  115. *
  116. * @param notifyParams 通知参数
  117. * @return 处理结果
  118. */
  119. public static NotifyResult handleNotify(Map<String, String> notifyParams) {
  120. try {
  121. System.out.println("收到异步通知: " + notifyParams);
  122. // 1. 验证签名
  123. String sign = notifyParams.get("sign");
  124. if (sign == null || sign.trim().isEmpty()) {
  125. return NotifyResult.error("签名为空");
  126. }
  127. // 构建验签参数
  128. Map<String, String> verifyParams = new TreeMap<>();
  129. for (Map.Entry<String, String> entry : notifyParams.entrySet()) {
  130. if (!"sign".equals(entry.getKey()) && entry.getValue() != null) {
  131. verifyParams.put(entry.getKey(), entry.getValue());
  132. }
  133. }
  134. String signString = buildSignString(verifyParams);
  135. System.out.println("通知验签字符串: " + signString);
  136. // 使用通联公钥验签
  137. boolean verifyResult = sm2.verify(signString.getBytes(StandardCharsets.UTF_8), sign.getBytes(StandardCharsets.UTF_8), APP_ID.getBytes(StandardCharsets.UTF_8));
  138. if (!verifyResult) {
  139. return NotifyResult.error("签名验证失败");
  140. }
  141. // 2. 解析通知内容
  142. String reqsn = notifyParams.get("reqsn");
  143. String trxstatus = notifyParams.get("trxstatus");
  144. String trxid = notifyParams.get("trxid");
  145. String trxamt = notifyParams.get("trxamt");
  146. return NotifyResult.success(reqsn, trxstatus, trxid, trxamt, "通知处理成功");
  147. } catch (Exception e) {
  148. e.printStackTrace();
  149. return NotifyResult.error("通知处理失败: " + e.getMessage());
  150. }
  151. }
  152. /**
  153. * 生成SM2签名
  154. */
  155. private static String generateSign(Map<String, String> params) throws Exception {
  156. // 1. 排序并拼接参数(排除sign字段)
  157. String signString = buildSignString(params);
  158. System.out.println("签名字符串: " + signString);
  159. // 2. 使用SM2私钥签名
  160. byte[] signBytes = sm2.sign(signString.getBytes(StandardCharsets.UTF_8), APP_ID.getBytes(StandardCharsets.UTF_8));
  161. String sign = Base64.getEncoder().encodeToString(signBytes);
  162. System.out.println("生成的签名: " + sign);
  163. return sign;
  164. }
  165. /**
  166. * 构建签名字符串
  167. */
  168. private static String buildSignString(Map<String, String> params) {
  169. StringBuilder sb = new StringBuilder();
  170. boolean first = true;
  171. for (Map.Entry<String, String> entry : params.entrySet()) {
  172. String key = entry.getKey();
  173. String value = entry.getValue();
  174. // 排除sign字段和空值
  175. if (!"sign".equals(key) && value != null && !value.trim().isEmpty()) {
  176. if (!first) {
  177. sb.append("&");
  178. }
  179. sb.append(key).append("=").append(value);
  180. first = false;
  181. }
  182. }
  183. return sb.toString();
  184. }
  185. /**
  186. * 发送POST请求
  187. */
  188. private static String sendPostRequest(String url, Map<String, String> params) throws Exception {
  189. // 构建表单参数
  190. StringBuilder formData = new StringBuilder();
  191. boolean first = true;
  192. for (Map.Entry<String, String> entry : params.entrySet()) {
  193. if (!first) {
  194. formData.append("&");
  195. }
  196. formData.append(entry.getKey()).append("=").append(entry.getValue());
  197. first = false;
  198. }
  199. // 设置请求头
  200. Map<String, String> headers = new HashMap<>();
  201. headers.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
  202. // 使用JFinal的HttpKit发送请求
  203. String response = HttpKit.post(url, null, formData.toString(), headers);
  204. if (StrKit.isBlank(response)) {
  205. throw new RuntimeException("API响应为空");
  206. }
  207. return response;
  208. }
  209. /**
  210. * 解析支付响应
  211. */
  212. private static PaymentResult parsePaymentResponse(String response) {
  213. try {
  214. System.out.println("支付响应: " + response);
  215. JSONObject jsonResponse = JSONUtil.parseObj(response);
  216. // 验证响应签名
  217. if (!verifyResponseSign(jsonResponse)) {
  218. return PaymentResult.error("响应签名验证失败");
  219. }
  220. String retcode = jsonResponse.getStr("retcode");
  221. String retmsg = jsonResponse.getStr("retmsg");
  222. if ("SUCCESS".equals(retcode)) {
  223. String payUrl = jsonResponse.getStr("payinfo");
  224. return PaymentResult.success(payUrl, "支付链接生成成功");
  225. } else {
  226. return PaymentResult.error("支付失败: " + retmsg);
  227. }
  228. } catch (Exception e) {
  229. e.printStackTrace();
  230. return PaymentResult.error("响应解析失败: " + e.getMessage());
  231. }
  232. }
  233. /**
  234. * 解析查询响应
  235. */
  236. private static QueryResult parseQueryResponse(String response) {
  237. try {
  238. System.out.println("查询响应: " + response);
  239. JSONObject jsonResponse = JSONUtil.parseObj(response);
  240. // 验证响应签名
  241. if (!verifyResponseSign(jsonResponse)) {
  242. return QueryResult.error("响应签名验证失败");
  243. }
  244. String retcode = jsonResponse.getStr("retcode");
  245. String retmsg = jsonResponse.getStr("retmsg");
  246. if ("SUCCESS".equals(retcode)) {
  247. String trxstatus = jsonResponse.getStr("trxstatus");
  248. String trxid = jsonResponse.getStr("trxid");
  249. String trxamt = jsonResponse.getStr("trxamt");
  250. return QueryResult.success(trxstatus, trxid, trxamt, "查询成功");
  251. } else {
  252. return QueryResult.error("查询失败: " + retmsg);
  253. }
  254. } catch (Exception e) {
  255. e.printStackTrace();
  256. return QueryResult.error("响应解析失败: " + e.getMessage());
  257. }
  258. }
  259. /**
  260. * 验证响应签名
  261. */
  262. private static boolean verifyResponseSign(JSONObject response) {
  263. try {
  264. String sign = response.getStr("sign");
  265. if (sign == null || sign.trim().isEmpty()) {
  266. return false;
  267. }
  268. // 构建验签字符串
  269. Map<String, String> params = new TreeMap<>();
  270. for (String key : response.keySet()) {
  271. if (!"sign".equals(key)) {
  272. Object value = response.get(key);
  273. if (value != null) {
  274. params.put(key, value.toString());
  275. }
  276. }
  277. }
  278. String signString = buildSignString(params);
  279. System.out.println("验签字符串: " + signString);
  280. // 使用通联公钥验签
  281. boolean result = sm2.verify(signString.getBytes(StandardCharsets.UTF_8), Base64.getDecoder().decode(sign), APP_ID.getBytes(StandardCharsets.UTF_8));
  282. System.out.println("验签结果: " + result);
  283. return result;
  284. } catch (Exception e) {
  285. e.printStackTrace();
  286. return false;
  287. }
  288. }
  289. /**
  290. * 支付结果类
  291. */
  292. public static class PaymentResult {
  293. private boolean success;
  294. private String payUrl;
  295. private String message;
  296. private String errorCode;
  297. private PaymentResult(boolean success, String payUrl, String message, String errorCode) {
  298. this.success = success;
  299. this.payUrl = payUrl;
  300. this.message = message;
  301. this.errorCode = errorCode;
  302. }
  303. public static PaymentResult success(String payUrl, String message) {
  304. return new PaymentResult(true, payUrl, message, null);
  305. }
  306. public static PaymentResult error(String message) {
  307. return new PaymentResult(false, null, message, "ERROR");
  308. }
  309. // Getters
  310. public boolean isSuccess() { return success; }
  311. public String getPayUrl() { return payUrl; }
  312. public String getMessage() { return message; }
  313. public String getErrorCode() { return errorCode; }
  314. @Override
  315. public String toString() {
  316. return "PaymentResult{" +
  317. "success=" + success +
  318. ", payUrl='" + payUrl + '\'' +
  319. ", message='" + message + '\'' +
  320. ", errorCode='" + errorCode + '\'' +
  321. '}';
  322. }
  323. }
  324. /**
  325. * 查询结果类
  326. */
  327. public static class QueryResult {
  328. private boolean success;
  329. private String trxstatus;
  330. private String trxid;
  331. private String trxamt;
  332. private String message;
  333. private String errorCode;
  334. private QueryResult(boolean success, String trxstatus, String trxid, String trxamt, String message, String errorCode) {
  335. this.success = success;
  336. this.trxstatus = trxstatus;
  337. this.trxid = trxid;
  338. this.trxamt = trxamt;
  339. this.message = message;
  340. this.errorCode = errorCode;
  341. }
  342. public static QueryResult success(String trxstatus, String trxid, String trxamt, String message) {
  343. return new QueryResult(true, trxstatus, trxid, trxamt, message, null);
  344. }
  345. public static QueryResult error(String message) {
  346. return new QueryResult(false, null, null, null, message, "ERROR");
  347. }
  348. // Getters
  349. public boolean isSuccess() { return success; }
  350. public String getTrxstatus() { return trxstatus; }
  351. public String getTrxid() { return trxid; }
  352. public String getTrxamt() { return trxamt; }
  353. public String getMessage() { return message; }
  354. public String getErrorCode() { return errorCode; }
  355. @Override
  356. public String toString() {
  357. return "QueryResult{" +
  358. "success=" + success +
  359. ", trxstatus='" + trxstatus + '\'' +
  360. ", trxid='" + trxid + '\'' +
  361. ", trxamt='" + trxamt + '\'' +
  362. ", message='" + message + '\'' +
  363. ", errorCode='" + errorCode + '\'' +
  364. '}';
  365. }
  366. }
  367. /**
  368. * 通知结果类
  369. */
  370. public static class NotifyResult {
  371. private boolean success;
  372. private String reqsn;
  373. private String trxstatus;
  374. private String trxid;
  375. private String trxamt;
  376. private String message;
  377. private String errorCode;
  378. private NotifyResult(boolean success, String reqsn, String trxstatus, String trxid, String trxamt, String message, String errorCode) {
  379. this.success = success;
  380. this.reqsn = reqsn;
  381. this.trxstatus = trxstatus;
  382. this.trxid = trxid;
  383. this.trxamt = trxamt;
  384. this.message = message;
  385. this.errorCode = errorCode;
  386. }
  387. public static NotifyResult success(String reqsn, String trxstatus, String trxid, String trxamt, String message) {
  388. return new NotifyResult(true, reqsn, trxstatus, trxid, trxamt, message, null);
  389. }
  390. public static NotifyResult error(String message) {
  391. return new NotifyResult(false, null, null, null, null, message, "ERROR");
  392. }
  393. // Getters
  394. public boolean isSuccess() { return success; }
  395. public String getReqsn() { return reqsn; }
  396. public String getTrxstatus() { return trxstatus; }
  397. public String getTrxid() { return trxid; }
  398. public String getTrxamt() { return trxamt; }
  399. public String getMessage() { return message; }
  400. public String getErrorCode() { return errorCode; }
  401. @Override
  402. public String toString() {
  403. return "NotifyResult{" +
  404. "success=" + success +
  405. ", reqsn='" + reqsn + '\'' +
  406. ", trxstatus='" + trxstatus + '\'' +
  407. ", trxid='" + trxid + '\'' +
  408. ", trxamt='" + trxamt + '\'' +
  409. ", message='" + message + '\'' +
  410. ", errorCode='" + errorCode + '\'' +
  411. '}';
  412. }
  413. }
  414. /**
  415. * 测试主方法
  416. */
  417. public static void main(String[] args) {
  418. System.out.println("=== 通联支付SDK测试 ===");
  419. // 测试参数
  420. Long trxamt = 100L; // 1元(分为单位)
  421. String reqsn = "TEST" + System.currentTimeMillis(); // 商户订单号
  422. String body = "测试商品";
  423. String remark = "SDK测试订单";
  424. String notify_url = "https://your-domain.com/api/payment/callback";
  425. System.out.println("测试订单号: " + reqsn);
  426. System.out.println("交易金额: " + trxamt + "分");
  427. // 测试1: 统一支付
  428. System.out.println("\n=== 测试统一支付 ===");
  429. PaymentResult payResult = unifiedPay(trxamt, reqsn, "W02", body, remark, "o9o43wz9sV5x8P-MA2b02mtYw4Co", notify_url, null, null, null, null);
  430. System.out.println("支付结果: " + payResult);
  431. if (payResult.isSuccess()) {
  432. System.out.println("支付链接: " + payResult.getPayUrl());
  433. System.out.println("请在浏览器中打开上述链接完成支付");
  434. // 测试2: 查询支付结果
  435. System.out.println("\n=== 测试查询支付结果 ===");
  436. QueryResult queryResult = queryPayment(reqsn);
  437. System.out.println("查询结果: " + queryResult);
  438. } else {
  439. System.out.println("支付失败: " + payResult.getMessage());
  440. }
  441. // 测试3: 模拟异步通知处理
  442. System.out.println("\n=== 测试异步通知处理 ===");
  443. Map<String, String> notifyParams = new HashMap<>();
  444. notifyParams.put("reqsn", reqsn);
  445. notifyParams.put("trxstatus", "0000");
  446. notifyParams.put("trxid", "TL" + System.currentTimeMillis());
  447. notifyParams.put("trxamt", String.valueOf(trxamt));
  448. notifyParams.put("sign", "test_sign"); // 实际使用时这里应该是真实的签名
  449. NotifyResult notifyResult = handleNotify(notifyParams);
  450. System.out.println("通知处理结果: " + notifyResult);
  451. System.out.println("\n=== 测试完成 ===");
  452. }
  453. }