AllinpaySDK.java 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  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 VERSION = "12";
  22. private static final String SIGN_TYPE = "SM2";
  23. private static final String APP_ID = "00375513";
  24. private static final String CUS_ID = "660222053994DLK";
  25. // SM2密钥对
  26. private static final String SM2_PRIVATE_KEY = System.getenv("TL_SM2_PRIVATE_KEY");
  27. // 通联公钥(用于验签,实际使用时需要从通联获取)
  28. private static final String ALLINPAY_PUBLIC_KEY = System.getenv("TL_SM2_PUBLIC_KEY");
  29. // 生产环境配置
  30. private static final String BASE_URL = "https://syb.allinpay.com";
  31. private static final String H5_PAY_URL = BASE_URL + "/apiweb/h5unionpay/unionorder";
  32. private static final String QUERY_URL = BASE_URL + "/apiweb/unitorder/query";
  33. private static final SM2 sm2 = SmUtil.sm2(SM2_PRIVATE_KEY, ALLINPAY_PUBLIC_KEY);
  34. /**
  35. * H5收银台支付
  36. *
  37. * @param trxamt 交易金额(分)
  38. * @param reqsn 商户订单号
  39. * @param body 订单标题
  40. * @param remark 订单备注信息
  41. * @param notify_url 服务器异步通知页面路径
  42. * @param returl 页面跳转同步通知页面路径
  43. * @param charset 参数字符编码集
  44. * @param validtime 有效时间(分钟)
  45. * @param limit_pay 支付限制
  46. * @param truename 付款人真实姓名
  47. * @param idno 证件号
  48. * @return 支付结果
  49. */
  50. public static PaymentResult h5Pay(Long trxamt, String reqsn, String body, String remark,
  51. String notify_url, String returl, String charset,
  52. Integer validtime, String limit_pay, String truename, String idno) {
  53. try {
  54. // 1. 构建请求参数
  55. Map<String, String> params = new TreeMap<>();
  56. params.put("cusid", CUS_ID);
  57. params.put("appid", APP_ID);
  58. params.put("version", VERSION);
  59. params.put("trxamt", String.valueOf(trxamt));
  60. params.put("signtype", SIGN_TYPE);
  61. params.put("randomstr", RandomUtil.randomString(8));
  62. params.put("reqsn", reqsn);
  63. params.put("body", body);
  64. params.put("remark", remark);
  65. params.put("returl", returl);
  66. // 添加可选参数
  67. if (StrKit.notBlank(returl)) params.put("returl", returl);
  68. if (StrKit.notBlank(charset)) params.put("charset", charset);
  69. if (validtime != null) params.put("validtime", String.valueOf(validtime));
  70. if (StrKit.notBlank(limit_pay)) params.put("limit_pay", limit_pay);
  71. if (StrKit.notBlank(truename)) params.put("truename", truename);
  72. if (StrKit.notBlank(idno)) params.put("idno", idno);
  73. // 2. 生成签名
  74. String sign = generateSign(params);
  75. params.put("sign", sign);
  76. // 3. 发送请求
  77. String response = sendPostRequest(H5_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. String finalUrl = url + "?" + formData;
  203. // 使用JFinal的HttpKit发送请求
  204. String response = HttpKit.post(finalUrl, null, null, headers);
  205. if (StrKit.isBlank(response)) {
  206. throw new RuntimeException("API响应为空");
  207. }
  208. return response;
  209. }
  210. /**
  211. * 解析支付响应
  212. */
  213. private static PaymentResult parsePaymentResponse(String response) {
  214. try {
  215. System.out.println("支付响应: " + response);
  216. JSONObject jsonResponse = JSONUtil.parseObj(response);
  217. // 验证响应签名
  218. if (!verifyResponseSign(jsonResponse)) {
  219. return PaymentResult.error("响应签名验证失败");
  220. }
  221. String retcode = jsonResponse.getStr("retcode");
  222. String retmsg = jsonResponse.getStr("retmsg");
  223. if ("SUCCESS".equals(retcode)) {
  224. String payUrl = jsonResponse.getStr("payinfo");
  225. return PaymentResult.success(payUrl, "支付链接生成成功");
  226. } else {
  227. return PaymentResult.error("支付失败: " + retmsg);
  228. }
  229. } catch (Exception e) {
  230. e.printStackTrace();
  231. return PaymentResult.error("响应解析失败: " + e.getMessage());
  232. }
  233. }
  234. /**
  235. * 解析查询响应
  236. */
  237. private static QueryResult parseQueryResponse(String response) {
  238. try {
  239. System.out.println("查询响应: " + response);
  240. JSONObject jsonResponse = JSONUtil.parseObj(response);
  241. // 验证响应签名
  242. if (!verifyResponseSign(jsonResponse)) {
  243. return QueryResult.error("响应签名验证失败");
  244. }
  245. String retcode = jsonResponse.getStr("retcode");
  246. String retmsg = jsonResponse.getStr("retmsg");
  247. if ("SUCCESS".equals(retcode)) {
  248. String trxstatus = jsonResponse.getStr("trxstatus");
  249. String trxid = jsonResponse.getStr("trxid");
  250. String trxamt = jsonResponse.getStr("trxamt");
  251. return QueryResult.success(trxstatus, trxid, trxamt, "查询成功");
  252. } else {
  253. return QueryResult.error("查询失败: " + retmsg);
  254. }
  255. } catch (Exception e) {
  256. e.printStackTrace();
  257. return QueryResult.error("响应解析失败: " + e.getMessage());
  258. }
  259. }
  260. /**
  261. * 验证响应签名
  262. */
  263. private static boolean verifyResponseSign(JSONObject response) {
  264. try {
  265. String sign = response.getStr("sign");
  266. if (sign == null || sign.trim().isEmpty()) {
  267. return false;
  268. }
  269. // 构建验签字符串
  270. Map<String, String> params = new TreeMap<>();
  271. for (String key : response.keySet()) {
  272. if (!"sign".equals(key)) {
  273. Object value = response.get(key);
  274. if (value != null) {
  275. params.put(key, value.toString());
  276. }
  277. }
  278. }
  279. String signString = buildSignString(params);
  280. System.out.println("验签字符串: " + signString);
  281. // 使用通联公钥验签
  282. boolean result = sm2.verify(signString.getBytes(StandardCharsets.UTF_8), Base64.getDecoder().decode(sign), APP_ID.getBytes(StandardCharsets.UTF_8));
  283. System.out.println("验签结果: " + result);
  284. return result;
  285. } catch (Exception e) {
  286. e.printStackTrace();
  287. return false;
  288. }
  289. }
  290. /**
  291. * 支付结果类
  292. */
  293. public static class PaymentResult {
  294. private boolean success;
  295. private String payUrl;
  296. private String message;
  297. private String errorCode;
  298. private PaymentResult(boolean success, String payUrl, String message, String errorCode) {
  299. this.success = success;
  300. this.payUrl = payUrl;
  301. this.message = message;
  302. this.errorCode = errorCode;
  303. }
  304. public static PaymentResult success(String payUrl, String message) {
  305. return new PaymentResult(true, payUrl, message, null);
  306. }
  307. public static PaymentResult error(String message) {
  308. return new PaymentResult(false, null, message, "ERROR");
  309. }
  310. // Getters
  311. public boolean isSuccess() { return success; }
  312. public String getPayUrl() { return payUrl; }
  313. public String getMessage() { return message; }
  314. public String getErrorCode() { return errorCode; }
  315. @Override
  316. public String toString() {
  317. return "PaymentResult{" +
  318. "success=" + success +
  319. ", payUrl='" + payUrl + '\'' +
  320. ", message='" + message + '\'' +
  321. ", errorCode='" + errorCode + '\'' +
  322. '}';
  323. }
  324. }
  325. /**
  326. * 查询结果类
  327. */
  328. public static class QueryResult {
  329. private boolean success;
  330. private String trxstatus;
  331. private String trxid;
  332. private String trxamt;
  333. private String message;
  334. private String errorCode;
  335. private QueryResult(boolean success, String trxstatus, String trxid, String trxamt, String message, String errorCode) {
  336. this.success = success;
  337. this.trxstatus = trxstatus;
  338. this.trxid = trxid;
  339. this.trxamt = trxamt;
  340. this.message = message;
  341. this.errorCode = errorCode;
  342. }
  343. public static QueryResult success(String trxstatus, String trxid, String trxamt, String message) {
  344. return new QueryResult(true, trxstatus, trxid, trxamt, message, null);
  345. }
  346. public static QueryResult error(String message) {
  347. return new QueryResult(false, null, null, null, message, "ERROR");
  348. }
  349. // Getters
  350. public boolean isSuccess() { return success; }
  351. public String getTrxstatus() { return trxstatus; }
  352. public String getTrxid() { return trxid; }
  353. public String getTrxamt() { return trxamt; }
  354. public String getMessage() { return message; }
  355. public String getErrorCode() { return errorCode; }
  356. @Override
  357. public String toString() {
  358. return "QueryResult{" +
  359. "success=" + success +
  360. ", trxstatus='" + trxstatus + '\'' +
  361. ", trxid='" + trxid + '\'' +
  362. ", trxamt='" + trxamt + '\'' +
  363. ", message='" + message + '\'' +
  364. ", errorCode='" + errorCode + '\'' +
  365. '}';
  366. }
  367. }
  368. /**
  369. * 通知结果类
  370. */
  371. public static class NotifyResult {
  372. private boolean success;
  373. private String reqsn;
  374. private String trxstatus;
  375. private String trxid;
  376. private String trxamt;
  377. private String message;
  378. private String errorCode;
  379. private NotifyResult(boolean success, String reqsn, String trxstatus, String trxid, String trxamt, String message, String errorCode) {
  380. this.success = success;
  381. this.reqsn = reqsn;
  382. this.trxstatus = trxstatus;
  383. this.trxid = trxid;
  384. this.trxamt = trxamt;
  385. this.message = message;
  386. this.errorCode = errorCode;
  387. }
  388. public static NotifyResult success(String reqsn, String trxstatus, String trxid, String trxamt, String message) {
  389. return new NotifyResult(true, reqsn, trxstatus, trxid, trxamt, message, null);
  390. }
  391. public static NotifyResult error(String message) {
  392. return new NotifyResult(false, null, null, null, null, message, "ERROR");
  393. }
  394. // Getters
  395. public boolean isSuccess() { return success; }
  396. public String getReqsn() { return reqsn; }
  397. public String getTrxstatus() { return trxstatus; }
  398. public String getTrxid() { return trxid; }
  399. public String getTrxamt() { return trxamt; }
  400. public String getMessage() { return message; }
  401. public String getErrorCode() { return errorCode; }
  402. @Override
  403. public String toString() {
  404. return "NotifyResult{" +
  405. "success=" + success +
  406. ", reqsn='" + reqsn + '\'' +
  407. ", trxstatus='" + trxstatus + '\'' +
  408. ", trxid='" + trxid + '\'' +
  409. ", trxamt='" + trxamt + '\'' +
  410. ", message='" + message + '\'' +
  411. ", errorCode='" + errorCode + '\'' +
  412. '}';
  413. }
  414. }
  415. /**
  416. * 测试主方法
  417. */
  418. public static void main(String[] args) {
  419. System.out.println("=== 通联支付SDK测试 ===");
  420. // 测试参数
  421. Long trxamt = 100L; // 1元(分为单位)
  422. String reqsn = "TEST" + System.currentTimeMillis(); // 商户订单号
  423. String body = "book";
  424. String remark = "SDKbook";
  425. String notify_url = "https://dlsh-nft.com.cn/api/hello/";
  426. System.out.println("测试订单号: " + reqsn);
  427. System.out.println("交易金额: " + trxamt + "分");
  428. // 测试1: H5收银台支付
  429. System.out.println("\n=== 测试H5收银台支付 ===");
  430. PaymentResult payResult = h5Pay(trxamt, reqsn, body, remark, notify_url,
  431. "https://dlsh-nft.com.cn", "UTF-8",
  432. 30, null, null, null);
  433. System.out.println("支付结果: " + payResult);
  434. if (payResult.isSuccess()) {
  435. System.out.println("支付链接: " + payResult.getPayUrl());
  436. System.out.println("请在浏览器中打开上述链接完成支付");
  437. // 测试2: 查询支付结果
  438. System.out.println("\n=== 测试查询支付结果 ===");
  439. QueryResult queryResult = queryPayment(reqsn);
  440. System.out.println("查询结果: " + queryResult);
  441. } else {
  442. System.out.println("支付失败: " + payResult.getMessage());
  443. }
  444. // 测试3: 模拟异步通知处理
  445. System.out.println("\n=== 测试异步通知处理 ===");
  446. Map<String, String> notifyParams = new HashMap<>();
  447. notifyParams.put("reqsn", reqsn);
  448. notifyParams.put("trxstatus", "0000");
  449. notifyParams.put("trxid", "TL" + System.currentTimeMillis());
  450. notifyParams.put("trxamt", String.valueOf(trxamt));
  451. notifyParams.put("sign", "test_sign"); // 实际使用时这里应该是真实的签名
  452. NotifyResult notifyResult = handleNotify(notifyParams);
  453. System.out.println("通知处理结果: " + notifyResult);
  454. System.out.println("\n=== 测试完成 ===");
  455. }
  456. }