AllinpaySDK.java 19 KB

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