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)。 * *

独立于 newebpay 包,签名统一用 {@link OmgCheckMacValue}(SHA256),无 AES、无解密。 * *

三类操作: *

* * @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 createAioForm(String baseUrl, OmgPayConfig cfg, Map params) { Map 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 queryTrade(String baseUrl, OmgPayConfig cfg, String merchantTradeNo) throws Exception { Map 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 doAction(String baseUrl, OmgPayConfig cfg, String merchantTradeNo, String tradeNo, String action, int totalAmount) throws Exception { Map 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 params) throws Exception { List pairs = new ArrayList<>(); for (Map.Entry 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 parseKvResponse(String resp) { Map 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; } }