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、无解密。
*
*
三类操作:
*
* - {@link #createAioForm}:幕前下单。后端组参 + 算 CheckMacValue,返回含 gatewayUrl 的 form 字段 Map,
* 前端构建隐藏 form 自动 submit 到 OMG 收银台(本身不发 HTTP)。
* - {@link #queryTrade}:订单查询 QueryTradeInfo/V5(凭证验证探测 / 回调漏收补单),后端 POST,响应 k=v。
* - {@link #doAction}:信用卡退款/取消 CreditDetail/DoAction(US4),后端 POST,响应 k=v。
*
*
* @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;
}
}