| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- package com.ruoyi.app.utils.omg;
- import org.apache.http.NameValuePair;
- import org.apache.http.client.entity.UrlEncodedFormEntity;
- import org.apache.http.client.methods.CloseableHttpResponse;
- import org.apache.http.client.methods.HttpPost;
- import org.apache.http.impl.client.CloseableHttpClient;
- import org.apache.http.impl.client.HttpClients;
- import org.apache.http.message.BasicNameValuePair;
- import org.apache.http.util.EntityUtils;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.stereotype.Component;
- import java.nio.charset.StandardCharsets;
- import java.util.ArrayList;
- import java.util.LinkedHashMap;
- import java.util.List;
- import java.util.Map;
- /**
- * OMG(歐買尬/FunPoint) AIO 支付 HTTP 客户端与组参工具(httpclient4)。
- *
- * <p>独立于 newebpay 包,签名统一用 {@link OmgCheckMacValue}(SHA256),无 AES、无解密。
- *
- * <p>三类操作:
- * <ul>
- * <li>{@link #createAioForm}:幕前下单。后端组参 + 算 CheckMacValue,返回含 gatewayUrl 的 form 字段 Map,
- * 前端构建隐藏 form 自动 submit 到 OMG 收银台(本身不发 HTTP)。</li>
- * <li>{@link #queryTrade}:订单查询 QueryTradeInfo/V5(凭证验证探测 / 回调漏收补单),后端 POST,响应 k=v。</li>
- * <li>{@link #doAction}:信用卡退款/取消 CreditDetail/DoAction(US4),后端 POST,响应 k=v。</li>
- * </ul>
- *
- * @author ruoyi
- * @date 2026-07-29
- */
- @Component
- public class OmgPay {
- private static final Logger log = LoggerFactory.getLogger(OmgPay.class);
- /** 幕前下单端点(前端 Form Post 目标) */
- public static final String URL_AIO_CHECKOUT = "/Cashier/AioCheckOut/V5";
- /** 订单查询端点 */
- public static final String URL_QUERY_TRADE_INFO = "/Cashier/QueryTradeInfo/V5";
- /** 信用卡退款/取消端点 */
- public static final String URL_DO_ACTION = "/CreditDetail/DoAction";
- /**
- * 幕前下单组参:对业务参数生成 CheckMacValue,返回含 gatewayUrl 的 form 字段 Map。
- * 前端用返回字段构建隐藏 form,action={@code gatewayUrl},其余为 input,submit 跳转 OMG 收银台。
- *
- * @param baseUrl OMG 根地址(测试 payment-stage.funpoint.com.tw / 正式 payment.funpoint.com.tw)
- * @param cfg 门店 OMG 凭证
- * @param params 业务参数(MerchantID/MerchantTradeNo/MerchantTradeDate/PaymentType=aio/TotalAmount/
- * ReturnURL/ChoosePayment=ALL/EncryptType=1/ItemName 等,不含 CheckMacValue)
- * @return form 字段 Map(含 CheckMacValue 与 gatewayUrl;gatewayUrl 不参与签名)
- */
- public Map<String, String> createAioForm(String baseUrl, OmgPayConfig cfg, Map<String, String> params) {
- Map<String, String> form = new LinkedHashMap<>(params);
- // 仅对业务参数签名(form 此刻不含 CheckMacValue/gatewayUrl)
- String checkMacValue = OmgCheckMacValue.generate(form, cfg.getHashKey(), cfg.getHashIv());
- form.put("CheckMacValue", checkMacValue);
- form.put("gatewayUrl", baseUrl + URL_AIO_CHECKOUT);
- log.info("[OMG] createAioForm >>> gatewayUrl={}, MerchantTradeNo={}, MerchantID={}",
- form.get("gatewayUrl"), form.get("MerchantTradeNo"), form.get("MerchantID"));
- return form;
- }
- /**
- * 订单查询 QueryTradeInfo/V5(凭证验证探测 / 回调漏收补单)。响应为 k=v 文本,解析为 Map。
- * TradeStatus:0 未付 / 1 已付 / 10200095 失败。
- */
- public Map<String, String> queryTrade(String baseUrl, OmgPayConfig cfg, String merchantTradeNo) throws Exception {
- Map<String, String> params = new LinkedHashMap<>();
- params.put("MerchantID", cfg.getMerchantId());
- params.put("MerchantTradeNo", merchantTradeNo);
- params.put("TimeStamp", String.valueOf(System.currentTimeMillis() / 1000L));
- params.put("CheckMacValue", OmgCheckMacValue.generate(params, cfg.getHashKey(), cfg.getHashIv()));
- String resp = postForm(baseUrl + URL_QUERY_TRADE_INFO, params);
- return parseKvResponse(resp);
- }
- /**
- * 信用卡退款/取消 CreditDetail/DoAction(US4)。Action:C 關帳 / R 退刷 / E 取消 / N 放棄。
- * 仅正式端点可用(stage 不可用)。响应为 k=v 文本,RtnCode=1 成功。
- */
- public Map<String, String> doAction(String baseUrl, OmgPayConfig cfg, String merchantTradeNo,
- String tradeNo, String action, int totalAmount) throws Exception {
- Map<String, String> params = new LinkedHashMap<>();
- params.put("MerchantID", cfg.getMerchantId());
- params.put("MerchantTradeNo", merchantTradeNo);
- params.put("TradeNo", tradeNo);
- params.put("Action", action);
- params.put("TotalAmount", String.valueOf(totalAmount));
- params.put("CheckMacValue", OmgCheckMacValue.generate(params, cfg.getHashKey(), cfg.getHashIv()));
- String resp = postForm(baseUrl + URL_DO_ACTION, params);
- return parseKvResponse(resp);
- }
- /** 表单 POST(application/x-www-form-urlencoded),返回响应文本。 */
- private String postForm(String url, Map<String, String> params) throws Exception {
- List<NameValuePair> pairs = new ArrayList<>();
- for (Map.Entry<String, String> e : params.entrySet()) {
- pairs.add(new BasicNameValuePair(e.getKey(), e.getValue()));
- }
- HttpPost post = new HttpPost(url);
- post.setEntity(new UrlEncodedFormEntity(pairs, StandardCharsets.UTF_8));
- log.info("[OMG] postForm >>> url={}, params={}", url, params);
- try (CloseableHttpClient client = HttpClients.createDefault();
- CloseableHttpResponse res = client.execute(post)) {
- String resp = EntityUtils.toString(res.getEntity(), StandardCharsets.UTF_8);
- log.info("[OMG] postForm <<< resp={}", resp);
- return resp;
- }
- }
- /** 解析 OMG k=v 文本响应(QueryTradeInfo/DoAction 返回 text/html,以 & 分隔)。 */
- private Map<String, String> parseKvResponse(String resp) {
- Map<String, String> map = new LinkedHashMap<>();
- if (resp == null || resp.isEmpty()) {
- return map;
- }
- for (String pair : resp.split("&")) {
- int idx = pair.indexOf('=');
- if (idx >= 0) {
- map.put(pair.substring(0, idx), pair.substring(idx + 1));
- } else {
- map.put(pair, "");
- }
- }
- return map;
- }
- }
|