OmgPay.java 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. package com.ruoyi.app.utils.omg;
  2. import org.apache.http.NameValuePair;
  3. import org.apache.http.client.entity.UrlEncodedFormEntity;
  4. import org.apache.http.client.methods.CloseableHttpResponse;
  5. import org.apache.http.client.methods.HttpPost;
  6. import org.apache.http.impl.client.CloseableHttpClient;
  7. import org.apache.http.impl.client.HttpClients;
  8. import org.apache.http.message.BasicNameValuePair;
  9. import org.apache.http.util.EntityUtils;
  10. import org.slf4j.Logger;
  11. import org.slf4j.LoggerFactory;
  12. import org.springframework.stereotype.Component;
  13. import java.nio.charset.StandardCharsets;
  14. import java.util.ArrayList;
  15. import java.util.LinkedHashMap;
  16. import java.util.List;
  17. import java.util.Map;
  18. /**
  19. * OMG(歐買尬/FunPoint) AIO 支付 HTTP 客户端与组参工具(httpclient4)。
  20. *
  21. * <p>独立于 newebpay 包,签名统一用 {@link OmgCheckMacValue}(SHA256),无 AES、无解密。
  22. *
  23. * <p>三类操作:
  24. * <ul>
  25. * <li>{@link #createAioForm}:幕前下单。后端组参 + 算 CheckMacValue,返回含 gatewayUrl 的 form 字段 Map,
  26. * 前端构建隐藏 form 自动 submit 到 OMG 收银台(本身不发 HTTP)。</li>
  27. * <li>{@link #queryTrade}:订单查询 QueryTradeInfo/V5(凭证验证探测 / 回调漏收补单),后端 POST,响应 k=v。</li>
  28. * <li>{@link #doAction}:信用卡退款/取消 CreditDetail/DoAction(US4),后端 POST,响应 k=v。</li>
  29. * </ul>
  30. *
  31. * @author ruoyi
  32. * @date 2026-07-29
  33. */
  34. @Component
  35. public class OmgPay {
  36. private static final Logger log = LoggerFactory.getLogger(OmgPay.class);
  37. /** 幕前下单端点(前端 Form Post 目标) */
  38. public static final String URL_AIO_CHECKOUT = "/Cashier/AioCheckOut/V5";
  39. /** 订单查询端点 */
  40. public static final String URL_QUERY_TRADE_INFO = "/Cashier/QueryTradeInfo/V5";
  41. /** 信用卡退款/取消端点 */
  42. public static final String URL_DO_ACTION = "/CreditDetail/DoAction";
  43. /**
  44. * 幕前下单组参:对业务参数生成 CheckMacValue,返回含 gatewayUrl 的 form 字段 Map。
  45. * 前端用返回字段构建隐藏 form,action={@code gatewayUrl},其余为 input,submit 跳转 OMG 收银台。
  46. *
  47. * @param baseUrl OMG 根地址(测试 payment-stage.funpoint.com.tw / 正式 payment.funpoint.com.tw)
  48. * @param cfg 门店 OMG 凭证
  49. * @param params 业务参数(MerchantID/MerchantTradeNo/MerchantTradeDate/PaymentType=aio/TotalAmount/
  50. * ReturnURL/ChoosePayment=ALL/EncryptType=1/ItemName 等,不含 CheckMacValue)
  51. * @return form 字段 Map(含 CheckMacValue 与 gatewayUrl;gatewayUrl 不参与签名)
  52. */
  53. public Map<String, String> createAioForm(String baseUrl, OmgPayConfig cfg, Map<String, String> params) {
  54. Map<String, String> form = new LinkedHashMap<>(params);
  55. // 仅对业务参数签名(form 此刻不含 CheckMacValue/gatewayUrl)
  56. String checkMacValue = OmgCheckMacValue.generate(form, cfg.getHashKey(), cfg.getHashIv());
  57. form.put("CheckMacValue", checkMacValue);
  58. form.put("gatewayUrl", baseUrl + URL_AIO_CHECKOUT);
  59. log.info("[OMG] createAioForm >>> gatewayUrl={}, MerchantTradeNo={}, MerchantID={}",
  60. form.get("gatewayUrl"), form.get("MerchantTradeNo"), form.get("MerchantID"));
  61. return form;
  62. }
  63. /**
  64. * 订单查询 QueryTradeInfo/V5(凭证验证探测 / 回调漏收补单)。响应为 k=v 文本,解析为 Map。
  65. * TradeStatus:0 未付 / 1 已付 / 10200095 失败。
  66. */
  67. public Map<String, String> queryTrade(String baseUrl, OmgPayConfig cfg, String merchantTradeNo) throws Exception {
  68. Map<String, String> params = new LinkedHashMap<>();
  69. params.put("MerchantID", cfg.getMerchantId());
  70. params.put("MerchantTradeNo", merchantTradeNo);
  71. params.put("TimeStamp", String.valueOf(System.currentTimeMillis() / 1000L));
  72. params.put("CheckMacValue", OmgCheckMacValue.generate(params, cfg.getHashKey(), cfg.getHashIv()));
  73. String resp = postForm(baseUrl + URL_QUERY_TRADE_INFO, params);
  74. return parseKvResponse(resp);
  75. }
  76. /**
  77. * 信用卡退款/取消 CreditDetail/DoAction(US4)。Action:C 關帳 / R 退刷 / E 取消 / N 放棄。
  78. * 仅正式端点可用(stage 不可用)。响应为 k=v 文本,RtnCode=1 成功。
  79. */
  80. public Map<String, String> doAction(String baseUrl, OmgPayConfig cfg, String merchantTradeNo,
  81. String tradeNo, String action, int totalAmount) throws Exception {
  82. Map<String, String> params = new LinkedHashMap<>();
  83. params.put("MerchantID", cfg.getMerchantId());
  84. params.put("MerchantTradeNo", merchantTradeNo);
  85. params.put("TradeNo", tradeNo);
  86. params.put("Action", action);
  87. params.put("TotalAmount", String.valueOf(totalAmount));
  88. params.put("CheckMacValue", OmgCheckMacValue.generate(params, cfg.getHashKey(), cfg.getHashIv()));
  89. String resp = postForm(baseUrl + URL_DO_ACTION, params);
  90. return parseKvResponse(resp);
  91. }
  92. /** 表单 POST(application/x-www-form-urlencoded),返回响应文本。 */
  93. private String postForm(String url, Map<String, String> params) throws Exception {
  94. List<NameValuePair> pairs = new ArrayList<>();
  95. for (Map.Entry<String, String> e : params.entrySet()) {
  96. pairs.add(new BasicNameValuePair(e.getKey(), e.getValue()));
  97. }
  98. HttpPost post = new HttpPost(url);
  99. post.setEntity(new UrlEncodedFormEntity(pairs, StandardCharsets.UTF_8));
  100. log.info("[OMG] postForm >>> url={}, params={}", url, params);
  101. try (CloseableHttpClient client = HttpClients.createDefault();
  102. CloseableHttpResponse res = client.execute(post)) {
  103. String resp = EntityUtils.toString(res.getEntity(), StandardCharsets.UTF_8);
  104. log.info("[OMG] postForm <<< resp={}", resp);
  105. return resp;
  106. }
  107. }
  108. /** 解析 OMG k=v 文本响应(QueryTradeInfo/DoAction 返回 text/html,以 & 分隔)。 */
  109. private Map<String, String> parseKvResponse(String resp) {
  110. Map<String, String> map = new LinkedHashMap<>();
  111. if (resp == null || resp.isEmpty()) {
  112. return map;
  113. }
  114. for (String pair : resp.split("&")) {
  115. int idx = pair.indexOf('=');
  116. if (idx >= 0) {
  117. map.put(pair.substring(0, idx), pair.substring(idx + 1));
  118. } else {
  119. map.put(pair, "");
  120. }
  121. }
  122. return map;
  123. }
  124. }