|
|
@@ -0,0 +1,556 @@
|
|
|
+package com.ruoyi.app.pay;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
+import com.ruoyi.app.order.dto.OrderPushBodyDto;
|
|
|
+import com.ruoyi.app.utils.PayPush;
|
|
|
+import com.ruoyi.app.utils.event.PushEventService;
|
|
|
+import com.ruoyi.app.utils.omg.OmgCheckMacValue;
|
|
|
+import com.ruoyi.app.utils.omg.OmgPay;
|
|
|
+import com.ruoyi.app.utils.omg.OmgPayConfig;
|
|
|
+import com.ruoyi.common.annotation.Anonymous;
|
|
|
+import com.ruoyi.common.annotation.RepeatSubmit;
|
|
|
+import com.ruoyi.common.core.controller.BaseController;
|
|
|
+import com.ruoyi.common.core.domain.AjaxResult;
|
|
|
+import com.ruoyi.common.utils.MessageUtils;
|
|
|
+import com.ruoyi.system.domain.InfoUser;
|
|
|
+import com.ruoyi.system.domain.IpnLog;
|
|
|
+import com.ruoyi.system.domain.PosOrder;
|
|
|
+import com.ruoyi.system.domain.PosOrderOmgPayment;
|
|
|
+import com.ruoyi.system.domain.PosStoreOmg;
|
|
|
+import com.ruoyi.system.service.IInfoUserService;
|
|
|
+import com.ruoyi.system.service.IIpnLogService;
|
|
|
+import com.ruoyi.system.service.IPosOrderOmgPaymentService;
|
|
|
+import com.ruoyi.system.service.IPosOrderOmgRefundService;
|
|
|
+import com.ruoyi.system.service.IPosOrderService;
|
|
|
+import com.ruoyi.system.service.IPosStoreOmgService;
|
|
|
+import com.ruoyi.system.utils.Auth;
|
|
|
+import com.ruoyi.system.utils.JwtUtil;
|
|
|
+import com.ruoyi.system.utils.OrderLogHelper;
|
|
|
+import jakarta.servlet.http.HttpServletRequest;
|
|
|
+import jakarta.servlet.http.HttpServletResponse;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.web.bind.annotation.GetMapping;
|
|
|
+import org.springframework.web.bind.annotation.PathVariable;
|
|
|
+import org.springframework.web.bind.annotation.PostMapping;
|
|
|
+import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
+import org.springframework.web.bind.annotation.RequestMethod;
|
|
|
+import org.springframework.web.bind.annotation.RequestParam;
|
|
|
+import org.springframework.web.bind.annotation.RestController;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.net.URLEncoder;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.text.SimpleDateFormat;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.Enumeration;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * OMG(歐買尬/FunPoint) AIO 线上支付 Controller(独立于 newebpay,零蓝新依赖)。
|
|
|
+ *
|
|
|
+ * <p>本期实现:
|
|
|
+ * <ul>
|
|
|
+ * <li>{@code POST /pay/omg/create} — 发起 AIO 幕前支付(US1,@Auth)。返回含 gatewayUrl 的 form 字段,
|
|
|
+ * 前端构建隐藏 form submit 到 OMG 收银台(ChoosePayment=ALL)。</li>
|
|
|
+ * <li>{@code POST /pay/omg/notify} — OMG 服务端回调(US2,@Anonymous)。验签 CheckMacValue + 幂等 + 金额 +
|
|
|
+ * RtnCode/SimulatePaid 校验 → markSuccess → 更新订单 payStatus=1 + 推送用户/商家 → 回纯串 {@code 1|OK}。</li>
|
|
|
+ * <li>{@code GET|POST /pay/omg/return} — 完成页引导(US2,@Anonymous)。仅 302 回前端结果页,不改订单状态。</li>
|
|
|
+ * </ul>
|
|
|
+ *
|
|
|
+ * @author ruoyi
|
|
|
+ * @date 2026-07-29
|
|
|
+ */
|
|
|
+@RestController
|
|
|
+@RequestMapping("/pay/omg")
|
|
|
+public class OmgPayController extends BaseController {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(OmgPayController.class);
|
|
|
+
|
|
|
+ /** payType 取值:OMG 在线支付(发起时写入 pos_order.pay_type;具体方式如 Credit_CreditCard、ATM 系列、CVS 系列等由回调写入 pos_order_omg_payment.pay_type)。 */
|
|
|
+ public static final String PAY_TYPE_OMG = "7";
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private IPosOrderService posOrderService;
|
|
|
+ @Autowired
|
|
|
+ private IPosStoreOmgService storeOmgService;
|
|
|
+ @Autowired
|
|
|
+ private IPosOrderOmgPaymentService paymentService;
|
|
|
+ @Autowired
|
|
|
+ private OmgPay omgPay;
|
|
|
+ @Autowired
|
|
|
+ private IIpnLogService ipnLogService;
|
|
|
+ @Autowired
|
|
|
+ private IInfoUserService infoUserService;
|
|
|
+ @Autowired
|
|
|
+ private PushEventService pushEventService;
|
|
|
+ @Autowired
|
|
|
+ private OrderLogHelper orderLogHelper;
|
|
|
+ @Autowired
|
|
|
+ private IPosOrderOmgRefundService refundService;
|
|
|
+
|
|
|
+ @Value("${omg.base-url}")
|
|
|
+ private String baseUrl;
|
|
|
+ @Value("${omg.return-url}")
|
|
|
+ private String returnUrl;
|
|
|
+ @Value("${omg.order-result-url}")
|
|
|
+ private String orderResultUrl;
|
|
|
+
|
|
|
+ // ============================ US1:发起 AIO 幕前支付 ============================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 发起 OMG AIO 幕前支付。校验订单归属/未支付/金额 → 查门店启用凭证 → 生成 MerchantTradeNo →
|
|
|
+ * 组参 + CheckMacValue → 落流水(pay_status=0) + 更新订单 payType=7/payUrl → 返回 form 字段供前端 Form Post。
|
|
|
+ */
|
|
|
+ @Anonymous
|
|
|
+ @Auth
|
|
|
+ @RepeatSubmit(interval = 1000, message = "请求过于频繁")
|
|
|
+ @PostMapping("/create")
|
|
|
+ public AjaxResult create(@RequestParam String orderid, HttpServletRequest request) {
|
|
|
+ String token = request.getHeader("token");
|
|
|
+ String userId;
|
|
|
+ try {
|
|
|
+ userId = new JwtUtil().getusid(token);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return error(MessageUtils.message("no.order.id.error"));
|
|
|
+ }
|
|
|
+ if (userId == null || userId.isEmpty()) {
|
|
|
+ return error("请先登录");
|
|
|
+ }
|
|
|
+
|
|
|
+ PosOrder order = posOrderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", orderid));
|
|
|
+ if (order == null) {
|
|
|
+ return error(MessageUtils.message("no.order.id.error"));
|
|
|
+ }
|
|
|
+ if (order.getUserId() == null || !userId.equals(String.valueOf(order.getUserId()))) {
|
|
|
+ return error("无权操作该订单");
|
|
|
+ }
|
|
|
+ if (order.getPayStatus() != null && order.getPayStatus() == 1L) {
|
|
|
+ return error("订单已支付");
|
|
|
+ }
|
|
|
+ if (order.getAmount() == null || order.getAmount() <= 0) {
|
|
|
+ return error("订单金额异常");
|
|
|
+ }
|
|
|
+ if (order.getMdId() == null) {
|
|
|
+ return error("订单门店缺失");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 门店 OMG 凭证(已开通且启用)→ 转 OmgPayConfig(Controller 层完成,避免 system→admin 反向依赖)
|
|
|
+ PosStoreOmg cred = storeOmgService.getEnabledCredential(order.getMdId());
|
|
|
+ if (cred == null) {
|
|
|
+ return error("该门店暂不支持线上支付");
|
|
|
+ }
|
|
|
+ OmgPayConfig cfg = new OmgPayConfig(cred.getMerchantId(), cred.getHashKey(), cred.getHashIv());
|
|
|
+
|
|
|
+ String merchantTradeNo = genMerchantTradeNo(orderid);
|
|
|
+
|
|
|
+ // OMG AIO 参数(contracts/api.md §A1/B1);InvoiceMark=N 固定(发票走 ezPay);EncryptType=1 固定(SHA256)
|
|
|
+ Map<String, String> params = new LinkedHashMap<>();
|
|
|
+ params.put("MerchantID", cred.getMerchantId());
|
|
|
+ params.put("MerchantTradeNo", merchantTradeNo);
|
|
|
+ params.put("MerchantTradeDate", new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(new Date()));
|
|
|
+ params.put("PaymentType", "aio");
|
|
|
+ params.put("TotalAmount", String.valueOf(order.getAmount()));
|
|
|
+ params.put("ReturnURL", returnUrl);
|
|
|
+ params.put("ChoosePayment", "ALL");
|
|
|
+ params.put("EncryptType", "1");
|
|
|
+ params.put("ItemName", "order " + orderid);
|
|
|
+ params.put("InvoiceMark", "N");
|
|
|
+ if (orderResultUrl != null && !orderResultUrl.isEmpty()) {
|
|
|
+ params.put("OrderResultURL", orderResultUrl);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 组参 + CheckMacValue,返回含 gatewayUrl 的 form 字段
|
|
|
+ Map<String, String> form = omgPay.createAioForm(baseUrl, cfg, params);
|
|
|
+
|
|
|
+ // 落 OMG 支付流水(pay_status=0)
|
|
|
+ paymentService.createPayment(orderid, merchantTradeNo, order.getMdId(), cred.getMerchantId(), order.getAmount(), "ALL");
|
|
|
+
|
|
|
+ // 更新订单 payType=7 / payUrl=gatewayUrl(仅更这两个字段)
|
|
|
+ PosOrder upd = new PosOrder();
|
|
|
+ upd.setId(order.getId());
|
|
|
+ upd.setPayType(PAY_TYPE_OMG);
|
|
|
+ upd.setPayUrl(form.get("gatewayUrl"));
|
|
|
+ posOrderService.saveOrUpdate(upd);
|
|
|
+
|
|
|
+ log.info("[OMG] create orderid={}, merchantTradeNo={}, amount={}, gatewayUrl={}",
|
|
|
+ orderid, merchantTradeNo, order.getAmount(), form.get("gatewayUrl"));
|
|
|
+ return success(form);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** MerchantTradeNo = "OMG" + ddId(清洗为英数,≤20 字元)。重新发起的幂等/后缀策略见 D6,MVP 先用 ddId。 */
|
|
|
+ private String genMerchantTradeNo(String ddId) {
|
|
|
+ String cleaned = ddId == null ? "" : ddId.replaceAll("[^0-9A-Za-z]", "");
|
|
|
+ String no = "OMG" + cleaned;
|
|
|
+ if (no.length() > 20) {
|
|
|
+ no = no.substring(0, 20);
|
|
|
+ }
|
|
|
+ return no;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ============================ US2:ReturnURL 支付结果回调 ============================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * OMG 支付结果回调(@Anonymous,OMG 服务端 Form Post)。OMG 明文参数 + 单 CheckMacValue(无 AES 解密)。
|
|
|
+ *
|
|
|
+ * <p>记 IPN → 按 MerchantID 查凭证 → 验签 → trade_no 幂等 → 金额校验 → RtnCode==1 且 SimulatePaid!=1
|
|
|
+ * → markSuccess + 推送 → 回纯串 {@code 1|OK}(注意:非 JSON,与蓝新不同)。任何异常/校验失败都仍回 1|OK
|
|
|
+ * (OMG 收到非成功会重试),但绝不错误更新订单。
|
|
|
+ */
|
|
|
+ @Anonymous
|
|
|
+ @PostMapping(value = "/notify", produces = "text/plain;charset=UTF-8")
|
|
|
+ public String notify(HttpServletRequest request) {
|
|
|
+ Map<String, String> form = collectForm(request);
|
|
|
+
|
|
|
+ // 记录 IPN 日志
|
|
|
+ try {
|
|
|
+ IpnLog ipnLog = new IpnLog();
|
|
|
+ ipnLog.setIp(new com.ruoyi.app.utils.IpUtils().getIpAddr(request));
|
|
|
+ ipnLog.setIpnLog(form.toString());
|
|
|
+ ipnLogService.insertIpnLog(ipnLog);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("记 OMG IPN 日志失败", e);
|
|
|
+ }
|
|
|
+
|
|
|
+ String merchantId = form.get("MerchantID");
|
|
|
+ PosStoreOmg cred = storeOmgService.getEnabledCredentialByMerchantId(merchantId);
|
|
|
+ if (cred == null) {
|
|
|
+ log.warn("OMG 回调无匹配凭证: merchantId={}", merchantId);
|
|
|
+ return "1|OK";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验签(OMG 无解密,直接对全部回调参 + CheckMacValue 重算比对)
|
|
|
+ if (!OmgCheckMacValue.verify(form, cred.getHashKey(), cred.getHashIv())) {
|
|
|
+ log.warn("OMG 回调验签失败: merchantId={}", merchantId);
|
|
|
+ return "1|OK";
|
|
|
+ }
|
|
|
+
|
|
|
+ String tradeNo = form.get("TradeNo");
|
|
|
+ String merchantTradeNo = form.get("MerchantTradeNo");
|
|
|
+ int rtnCode = toInt(form.get("RtnCode"), -1);
|
|
|
+ int tradeAmt = toInt(form.get("TradeAmt"), -1);
|
|
|
+ String simulatePaid = form.get("SimulatePaid");
|
|
|
+ String paymentType = form.get("PaymentType");
|
|
|
+ String paymentDate = form.get("PaymentDate");
|
|
|
+
|
|
|
+ // 幂等:同 tradeNo 已成功则直接返回
|
|
|
+ PosOrderOmgPayment exist = paymentService.getByTradeNo(tradeNo);
|
|
|
+ if (exist != null && Integer.valueOf(1).equals(exist.getPayStatus())) {
|
|
|
+ return "1|OK";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 订单关联:由 MerchantTradeNo 反查流水与订单
|
|
|
+ PosOrderOmgPayment payment = paymentService.getByMerchantTradeNo(merchantTradeNo);
|
|
|
+ if (payment == null) {
|
|
|
+ log.warn("OMG 回调无对应发起记录: merchantTradeNo={}", merchantTradeNo);
|
|
|
+ return "1|OK";
|
|
|
+ }
|
|
|
+ String ddId = payment.getDdId();
|
|
|
+ PosOrder order = posOrderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", ddId));
|
|
|
+ if (order == null) {
|
|
|
+ log.warn("OMG 回调订单不存在: ddId={}", ddId);
|
|
|
+ return "1|OK";
|
|
|
+ }
|
|
|
+
|
|
|
+ // 金额校验(以平台实际应收金额为准)
|
|
|
+ if (order.getAmount() == null || tradeAmt != order.getAmount()) {
|
|
|
+ log.error("OMG 回调金额不符: ddId={}, orderAmt={}, callbackAmt={}", ddId, order.getAmount(), tradeAmt);
|
|
|
+ return "1|OK";
|
|
|
+ }
|
|
|
+
|
|
|
+ if (rtnCode == 1 && !"1".equals(simulatePaid)) {
|
|
|
+ // 核销成功
|
|
|
+ int n = paymentService.markSuccess(payment.getId(), tradeNo, paymentType, rtnCode,
|
|
|
+ form.get("RtnMsg"), form.get("AuthCode"), parsePayTime(paymentDate), form.toString());
|
|
|
+ if (n > 0) {
|
|
|
+ // 首次成功:更新订单 state=0(待接单)/payStatus=1 + 推送用户/商家
|
|
|
+ handlePaymentSuccess(order);
|
|
|
+ }
|
|
|
+ } else if (rtnCode != 1) {
|
|
|
+ paymentService.markFail(payment.getId(), rtnCode, form.get("RtnMsg"), form.toString());
|
|
|
+ log.warn("OMG 回调交易失败: ddId={}, rtnCode={}, rtnMsg={}", ddId, rtnCode, form.get("RtnMsg"));
|
|
|
+ } else {
|
|
|
+ // SimulatePaid=1 模拟支付:不发货,仅记录
|
|
|
+ log.warn("OMG 回调为模拟支付(SimulatePaid=1),不发货: ddId={}", ddId);
|
|
|
+ }
|
|
|
+ return "1|OK";
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 支付完成返回页(@Anonymous)。仅 302 引导回前端结果页(带 ddId),<b>不</b>改订单状态(以 notify 为准)。
|
|
|
+ */
|
|
|
+ @Anonymous
|
|
|
+ @RequestMapping(value = "/return", method = {RequestMethod.GET, RequestMethod.POST})
|
|
|
+ public void returnCallback(HttpServletRequest request, HttpServletResponse response) throws IOException {
|
|
|
+ String ddId = "";
|
|
|
+ try {
|
|
|
+ Map<String, String> form = collectForm(request);
|
|
|
+ String mtn = form.get("MerchantTradeNo");
|
|
|
+ // MerchantTradeNo = "OMG" + ddId,去前缀还原 ddId
|
|
|
+ if (mtn != null && mtn.startsWith("OMG")) {
|
|
|
+ ddId = mtn.substring(3);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("OMG ReturnURL 解析失败", e);
|
|
|
+ }
|
|
|
+ String sep = orderResultUrl.contains("?") ? "&" : "?";
|
|
|
+ response.sendRedirect(orderResultUrl + sep + "ddId=" + URLEncoder.encode(ddId, StandardCharsets.UTF_8));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ============================ US3:ATM/超商 取号回调 + 取号查询 ============================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * OMG ATM/超商取号回调(@Anonymous,PaymentInfoURL)。验签后把虚帐/缴费码原始报文
|
|
|
+ * (BankCode/vAccount/ExpireDate 或 PaymentNo/ExpireDate 等)落到流水的 callbackRaw,不改 pay_status;
|
|
|
+ * 实际付款后 OMG 再回调 /pay/omg/notify(RtnCode=1)走 US2 核销。回纯串 {@code 1|OK}。
|
|
|
+ */
|
|
|
+ @Anonymous
|
|
|
+ @PostMapping(value = "/paymentInfo", produces = "text/plain;charset=UTF-8")
|
|
|
+ public String paymentInfoCallback(HttpServletRequest request) {
|
|
|
+ Map<String, String> form = collectForm(request);
|
|
|
+ try {
|
|
|
+ IpnLog ipnLog = new IpnLog();
|
|
|
+ ipnLog.setIp(new com.ruoyi.app.utils.IpUtils().getIpAddr(request));
|
|
|
+ ipnLog.setIpnLog(form.toString());
|
|
|
+ ipnLogService.insertIpnLog(ipnLog);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("记 OMG paymentInfo IPN 日志失败", e);
|
|
|
+ }
|
|
|
+
|
|
|
+ String merchantId = form.get("MerchantID");
|
|
|
+ PosStoreOmg cred = storeOmgService.getEnabledCredentialByMerchantId(merchantId);
|
|
|
+ if (cred == null) {
|
|
|
+ log.warn("OMG paymentInfo 无匹配凭证: merchantId={}", merchantId);
|
|
|
+ return "1|OK";
|
|
|
+ }
|
|
|
+ if (!OmgCheckMacValue.verify(form, cred.getHashKey(), cred.getHashIv())) {
|
|
|
+ log.warn("OMG paymentInfo 验签失败: merchantId={}", merchantId);
|
|
|
+ return "1|OK";
|
|
|
+ }
|
|
|
+
|
|
|
+ String merchantTradeNo = form.get("MerchantTradeNo");
|
|
|
+ PosOrderOmgPayment payment = paymentService.getByMerchantTradeNo(merchantTradeNo);
|
|
|
+ if (payment == null) {
|
|
|
+ log.warn("OMG paymentInfo 无对应发起记录: merchantTradeNo={}", merchantTradeNo);
|
|
|
+ return "1|OK";
|
|
|
+ }
|
|
|
+ // 落取号信息(BankCode/vAccount/ExpireDate 或 PaymentNo/ExpireDate),不改 pay_status
|
|
|
+ paymentService.markPaymentInfo(payment.getId(), form.get("TradeNo"), form.toString());
|
|
|
+ log.info("[OMG] paymentInfo 已记录取号信息: ddId={}, merchantTradeNo={}", payment.getDdId(), merchantTradeNo);
|
|
|
+ return "1|OK";
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 取号信息查询(前端 ATM/超商结果页展示虚帐/缴费码 + 期限用,@Auth)。
|
|
|
+ * 返回 payType/amount/payStatus + 解析后的取号字段 info。
|
|
|
+ */
|
|
|
+ @Anonymous
|
|
|
+ @Auth
|
|
|
+ @GetMapping("/paymentInfo/{orderid}")
|
|
|
+ public AjaxResult getPaymentInfo(@PathVariable String orderid, HttpServletRequest request) {
|
|
|
+ String token = request.getHeader("token");
|
|
|
+ String userId;
|
|
|
+ try {
|
|
|
+ userId = new JwtUtil().getusid(token);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return error(MessageUtils.message("no.order.id.error"));
|
|
|
+ }
|
|
|
+ PosOrder order = posOrderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", orderid));
|
|
|
+ if (order == null || order.getUserId() == null || !userId.equals(String.valueOf(order.getUserId()))) {
|
|
|
+ return error("无权操作该订单");
|
|
|
+ }
|
|
|
+ PosOrderOmgPayment p = paymentService.getLatestByDdId(orderid);
|
|
|
+ if (p == null) {
|
|
|
+ return error("无支付记录");
|
|
|
+ }
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("payType", p.getPayType());
|
|
|
+ result.put("amount", p.getAmount());
|
|
|
+ result.put("payStatus", p.getPayStatus());
|
|
|
+ result.put("info", parseKv(p.getCallbackRaw()));
|
|
|
+ return success(result);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ============================ US4:订单取消与退款 ============================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * OMG 退款(@Auth,供取消链路/管理员调用)。信用卡(含 Apple Pay) 调 DoAction(Action=R 退刷);
|
|
|
+ * ATM/超商/BarcodeATM 无退款 API → 记录待人工在 OMG 后台处理。仅正式端点可用(stage DoAction 不可用)。
|
|
|
+ */
|
|
|
+ @Anonymous
|
|
|
+ @Auth
|
|
|
+ @PostMapping("/refund")
|
|
|
+ public AjaxResult refund(@RequestParam String orderid, HttpServletRequest request) {
|
|
|
+ String token = request.getHeader("token");
|
|
|
+ try {
|
|
|
+ new JwtUtil().getusid(token);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return error(MessageUtils.message("no.order.id.error"));
|
|
|
+ }
|
|
|
+ PosOrder order = posOrderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", orderid));
|
|
|
+ if (order == null) {
|
|
|
+ return error("订单不存在");
|
|
|
+ }
|
|
|
+ return refundOrder(order);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * OMG 退款核心(public,供取消链路 {@code cancelOrder} 直接调用,免重复实现)。
|
|
|
+ * 信用卡(含 Apple Pay) 调 DoAction(Action=R 退刷);ATM/超商/BarcodeATM 记人工。仅正式端点可用。
|
|
|
+ * 返回 success=已退款;error=需人工或失败(refund 已落记录可重试)。
|
|
|
+ */
|
|
|
+ public AjaxResult refundOrder(PosOrder order) {
|
|
|
+ if (order == null) {
|
|
|
+ return error("订单不存在");
|
|
|
+ }
|
|
|
+ if (!PAY_TYPE_OMG.equals(order.getPayType())) {
|
|
|
+ return error("该订单非 OMG 支付");
|
|
|
+ }
|
|
|
+ if (order.getPayStatus() == null || order.getPayStatus() != 1L) {
|
|
|
+ return error("订单未支付,无需退款");
|
|
|
+ }
|
|
|
+ String ddId = String.valueOf(order.getDdId());
|
|
|
+ PosOrderOmgPayment payment = paymentService.getLatestByDdId(ddId);
|
|
|
+ if (payment == null) {
|
|
|
+ return error("无 OMG 支付流水");
|
|
|
+ }
|
|
|
+ int amount = order.getAmount() == null ? 0 : order.getAmount();
|
|
|
+ String payType = payment.getPayType();
|
|
|
+
|
|
|
+ // ATM/超商/BarcodeATM 无退款 API → 记录待人工
|
|
|
+ if (payType == null || payType.startsWith("ATM_") || payType.startsWith("CVS_") || payType.startsWith("BarcodeATM_")) {
|
|
|
+ refundService.record(payment.getId(), ddId, payment.getTradeNo(), null, amount, null,
|
|
|
+ "延期支付方式无退款API,待人工在 OMG 后台处理", "");
|
|
|
+ return error("该支付方式需人工在 OMG 后台退款");
|
|
|
+ }
|
|
|
+
|
|
|
+ PosStoreOmg cred = storeOmgService.getEnabledCredential(order.getMdId());
|
|
|
+ if (cred == null) {
|
|
|
+ return error("门店 OMG 凭证不可用");
|
|
|
+ }
|
|
|
+ OmgPayConfig cfg = new OmgPayConfig(cred.getMerchantId(), cred.getHashKey(), cred.getHashIv());
|
|
|
+
|
|
|
+ // 信用卡(含 Apple Pay)→ DoAction(Action=R 退刷);MVP 统一 R,失败再按状态分支(D7)
|
|
|
+ Map<String, String> resp;
|
|
|
+ try {
|
|
|
+ resp = omgPay.doAction(baseUrl, cfg, payment.getMerchantTradeNo(), payment.getTradeNo(), "R", amount);
|
|
|
+ } catch (Exception e) {
|
|
|
+ refundService.record(payment.getId(), ddId, payment.getTradeNo(), "R", amount, null,
|
|
|
+ "ERROR: " + (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()), "");
|
|
|
+ return error("OMG 退款服务暂不可用");
|
|
|
+ }
|
|
|
+ int rtnCode = toInt(resp == null ? null : resp.get("RtnCode"), -1);
|
|
|
+ String rtnMsg = resp == null ? "" : resp.get("RtnMsg");
|
|
|
+ refundService.record(payment.getId(), ddId, payment.getTradeNo(), "R", amount, rtnCode, rtnMsg,
|
|
|
+ resp == null ? "" : resp.toString());
|
|
|
+
|
|
|
+ if (rtnCode == 1) {
|
|
|
+ paymentService.markRefunded(payment.getId());
|
|
|
+ PosOrder upd = new PosOrder();
|
|
|
+ upd.setId(order.getId());
|
|
|
+ upd.setPayStatus(2L);
|
|
|
+ posOrderService.saveOrUpdate(upd);
|
|
|
+ orderLogHelper.logSync(ddId, 0, null, "系统", "OMG 信用卡退款成功");
|
|
|
+ return success("退款成功");
|
|
|
+ }
|
|
|
+ return error("OMG 退款失败:" + rtnMsg);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ============================ 支付成功业务链路(参照 PosOrderController.sendHdfkMessage 货到付款同款) ============================
|
|
|
+
|
|
|
+ /** 支付成功:更新订单 state=0/payStatus=1 + 订单日志 + 推送用户(支付成功)/商家(新订单)。骑手推送平台当前为空实现,此处不补。 */
|
|
|
+ private void handlePaymentSuccess(PosOrder order) {
|
|
|
+ try {
|
|
|
+ PosOrder upd = new PosOrder();
|
|
|
+ upd.setId(order.getId());
|
|
|
+ upd.setState(0L);
|
|
|
+ upd.setPayStatus(1L);
|
|
|
+ posOrderService.saveOrUpdate(upd);
|
|
|
+
|
|
|
+ orderLogHelper.logSync(String.valueOf(order.getDdId()), 0, null, "系统", "系统收到OMG支付成功回调");
|
|
|
+
|
|
|
+ String ddId = String.valueOf(order.getDdId());
|
|
|
+ String state = String.valueOf(order.getState());
|
|
|
+ String title = MessageUtils.message("no.message.push.message");
|
|
|
+ String body = OrderPushBodyDto.getJson(ddId, state, 0);
|
|
|
+
|
|
|
+ InfoUser user = order.getUserId() == null ? null : infoUserService.getById(order.getUserId());
|
|
|
+ if (user != null) {
|
|
|
+ PayPush push = new PayPush();
|
|
|
+ push.apppush(user.getCid(), title, MessageUtils.message("no.message.push.payment.success"), body);
|
|
|
+ pushEventService.PublisherEvent(user.getUserId(), title, MessageUtils.message("no.message.push.payment.success"), body);
|
|
|
+ }
|
|
|
+
|
|
|
+ InfoUser sh = order.getShId() == null ? null : infoUserService.getById(order.getShId());
|
|
|
+ if (sh != null) {
|
|
|
+ PayPush push = new PayPush();
|
|
|
+ push.shpush(sh.getCid(), title, MessageUtils.message("no.message.push.new.order"), body);
|
|
|
+ pushEventService.PublisherEvent(sh.getUserId(), title, MessageUtils.message("no.message.push.new.order"), body);
|
|
|
+ }
|
|
|
+ // 可接单骑手推送:平台 PosOrderController.sendQsPush 当前为空实现(与货到付款/蓝新一致),此处不补
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("OMG 支付成功业务处理异常: ddId={}", order.getDdId(), e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // ============================ 辅助 ============================
|
|
|
+
|
|
|
+ /** 收集 form-urlencoded 回调参数为 Map(OMG 明文参数,含 CheckMacValue)。 */
|
|
|
+ private Map<String, String> collectForm(HttpServletRequest req) {
|
|
|
+ Map<String, String> map = new LinkedHashMap<>();
|
|
|
+ Enumeration<String> names = req.getParameterNames();
|
|
|
+ while (names.hasMoreElements()) {
|
|
|
+ String n = names.nextElement();
|
|
|
+ map.put(n, req.getParameter(n));
|
|
|
+ }
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
+ private int toInt(String s, int def) {
|
|
|
+ if (s == null || s.isEmpty()) {
|
|
|
+ return def;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return Integer.parseInt(s);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return def;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 解析 callbackRaw(LinkedHashMap.toString 形如 {A=1, B=2})为 Map,供前端读 ATM/超商取号信息。 */
|
|
|
+ private Map<String, String> parseKv(String kv) {
|
|
|
+ Map<String, String> map = new LinkedHashMap<>();
|
|
|
+ if (kv == null || kv.isEmpty()) {
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+ String s = kv;
|
|
|
+ if (s.startsWith("{") && s.endsWith("}")) {
|
|
|
+ s = s.substring(1, s.length() - 1);
|
|
|
+ }
|
|
|
+ for (String pair : s.split(",")) {
|
|
|
+ int idx = pair.indexOf('=');
|
|
|
+ if (idx > 0) {
|
|
|
+ map.put(pair.substring(0, idx).trim(), pair.substring(idx + 1).trim());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return map;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 解析回调 PaymentDate(兼容 yyyy/MM/dd 与 yyyy-MM-dd),失败取当前时间。 */
|
|
|
+ private Date parsePayTime(String s) {
|
|
|
+ if (s == null || s.isEmpty()) {
|
|
|
+ return new Date();
|
|
|
+ }
|
|
|
+ String[] fmts = {"yyyy/MM/dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss"};
|
|
|
+ for (String f : fmts) {
|
|
|
+ try {
|
|
|
+ return new SimpleDateFormat(f).parse(s);
|
|
|
+ } catch (Exception ignore) {
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return new Date();
|
|
|
+ }
|
|
|
+}
|