|
@@ -1,8 +1,14 @@
|
|
|
package com.mdframe.forge.business.core.hotel.pay.service.impl;
|
|
package com.mdframe.forge.business.core.hotel.pay.service.impl;
|
|
|
|
|
|
|
|
|
|
+import com.alipay.api.AlipayClient;
|
|
|
|
|
+import com.alipay.api.request.AlipayTradeQueryRequest;
|
|
|
|
|
+import com.alipay.api.request.AlipayTradeWapPayRequest;
|
|
|
|
|
+import com.alipay.api.response.AlipayTradeQueryResponse;
|
|
|
|
|
+import com.alipay.api.response.AlipayTradeWapPayResponse;
|
|
|
import com.mdframe.forge.business.core.hotel.order.constant.HotelOrderConstants;
|
|
import com.mdframe.forge.business.core.hotel.order.constant.HotelOrderConstants;
|
|
|
import com.mdframe.forge.business.core.hotel.order.domain.HotelOrder;
|
|
import com.mdframe.forge.business.core.hotel.order.domain.HotelOrder;
|
|
|
import com.mdframe.forge.business.core.hotel.order.mapper.HotelOrderMapper;
|
|
import com.mdframe.forge.business.core.hotel.order.mapper.HotelOrderMapper;
|
|
|
|
|
+import com.mdframe.forge.business.core.hotel.pay.config.AlipayConfig;
|
|
|
import com.mdframe.forge.business.core.hotel.pay.domain.HotelPayLog;
|
|
import com.mdframe.forge.business.core.hotel.pay.domain.HotelPayLog;
|
|
|
import com.mdframe.forge.business.core.hotel.pay.mapper.HotelPayLogMapper;
|
|
import com.mdframe.forge.business.core.hotel.pay.mapper.HotelPayLogMapper;
|
|
|
import com.mdframe.forge.business.core.hotel.notification.manager.SseEmitterManager;
|
|
import com.mdframe.forge.business.core.hotel.notification.manager.SseEmitterManager;
|
|
@@ -16,6 +22,7 @@ import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
|
|
|
|
+import java.math.BigDecimal;
|
|
|
import java.time.LocalDateTime;
|
|
import java.time.LocalDateTime;
|
|
|
import java.time.format.DateTimeFormatter;
|
|
import java.time.format.DateTimeFormatter;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
@@ -37,6 +44,12 @@ public class HotelPayServiceImpl implements HotelPayService {
|
|
|
@Autowired
|
|
@Autowired
|
|
|
private SseEmitterManager sseEmitterManager;
|
|
private SseEmitterManager sseEmitterManager;
|
|
|
|
|
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private AlipayClient alipayClient;
|
|
|
|
|
+
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private AlipayConfig alipayConfig;
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 服务类型标识,用于判断当前是 AdminServer 还是 AppServer。
|
|
* 服务类型标识,用于判断当前是 AdminServer 还是 AppServer。
|
|
|
* 在 application.yml 中配置:server.type: admin 或 app
|
|
* 在 application.yml 中配置:server.type: admin 或 app
|
|
@@ -93,9 +106,45 @@ public class HotelPayServiceImpl implements HotelPayService {
|
|
|
payParams.put("payAmount", order.getTotalAmount());
|
|
payParams.put("payAmount", order.getTotalAmount());
|
|
|
payParams.put("paySource", payLog.getPaySource());
|
|
payParams.put("paySource", payLog.getPaySource());
|
|
|
|
|
|
|
|
- // TODO: 对接真实支付时,根据 paySource 调用微信/支付宝 SDK 生成支付参数
|
|
|
|
|
- // if ("WECHAT".equals(paySource)) { ... wxpay unified order ... }
|
|
|
|
|
- // if ("ALIPAY".equals(paySource)) { ... alipay trade create ... }
|
|
|
|
|
|
|
+ // 根据 paySource 调用不同支付渠道
|
|
|
|
|
+ if ("ALIPAY".equals(paySource)) {
|
|
|
|
|
+ // 支付宝 H5 手机网站支付:调用 alipay.trade.wap.pay
|
|
|
|
|
+ try {
|
|
|
|
|
+ AlipayTradeWapPayRequest request = new AlipayTradeWapPayRequest();
|
|
|
|
|
+ request.setNotifyUrl(alipayConfig.getNotifyUrl());
|
|
|
|
|
+ // 支付完成后跳转回 H5 支付页(自动检测已支付状态并跳转订单详情页)
|
|
|
|
|
+ String returnUrl = alipayConfig.getReturnUrl();
|
|
|
|
|
+ if (returnUrl != null && !returnUrl.isEmpty()) {
|
|
|
|
|
+ // 动态追加 orderId 参数
|
|
|
|
|
+ String separator = returnUrl.contains("?") ? "&" : "?";
|
|
|
|
|
+ request.setReturnUrl(returnUrl + separator + "orderId=" + orderId);
|
|
|
|
|
+ }
|
|
|
|
|
+ String bizContent = "{\"out_trade_no\":\"" + outTradeNo + "\","
|
|
|
|
|
+ + "\"total_amount\":\"" + order.getTotalAmount().toPlainString() + "\","
|
|
|
|
|
+ + "\"subject\":\"酒店客房送餐-" + order.getOrderNo() + "\","
|
|
|
|
|
+ + "\"product_code\":\"QUICK_WAP_WAY\","
|
|
|
|
|
+ + "\"timeout_express\":\"15m\"}";
|
|
|
|
|
+ request.setBizContent(bizContent);
|
|
|
|
|
+ AlipayTradeWapPayResponse response = alipayClient.pageExecute(request);
|
|
|
|
|
+ if (response.isSuccess()) {
|
|
|
|
|
+ String formHtml = response.getBody();
|
|
|
|
|
+ payParams.put("payChannel", "ALIPAY");
|
|
|
|
|
+ payParams.put("payForm", formHtml);
|
|
|
|
|
+ log.info("支付宝H5支付创建成功: outTradeNo={}", outTradeNo);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.error("支付宝H5支付创建失败: code={}, msg={}", response.getCode(), response.getMsg());
|
|
|
|
|
+ throw new BusinessException("支付宝创建支付失败: " + response.getSubMsg());
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (BusinessException e) {
|
|
|
|
|
+ throw e;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("调用支付宝SDK异常: orderId={}", orderId, e);
|
|
|
|
|
+ throw new BusinessException("调用支付宝支付失败: " + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // MOCK 模式:只返回基础参数
|
|
|
|
|
+ payParams.put("payChannel", "MOCK");
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
return payParams;
|
|
return payParams;
|
|
|
}
|
|
}
|
|
@@ -123,29 +172,78 @@ public class HotelPayServiceImpl implements HotelPayService {
|
|
|
|
|
|
|
|
// ==================== 支付回调 ====================
|
|
// ==================== 支付回调 ====================
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 处理支付回调(第三方支付平台异步通知)。
|
|
|
|
|
+ * <p>
|
|
|
|
|
+ * 安全机制:
|
|
|
|
|
+ * 1. 金额校验:验证回调金额与订单金额一致
|
|
|
|
|
+ * 2. app_id 校验:验证回调来源
|
|
|
|
|
+ * 3. 数据库级幂等:用 UPDATE WHERE pay_status != 1 防止并发重复处理
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param outTradeNo 商户订单号
|
|
|
|
|
+ * @param tradeNo 第三方交易号
|
|
|
|
|
+ * @param success 是否支付成功
|
|
|
|
|
+ * @param totalAmount 回调中的支付金额(元),用于校验
|
|
|
|
|
+ * @param appId 回调中的 app_id,用于校验
|
|
|
|
|
+ * @param rawContent 回调原文
|
|
|
|
|
+ */
|
|
|
@Override
|
|
@Override
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
- public void handlePayCallback(String outTradeNo, String tradeNo, boolean success, String rawContent) {
|
|
|
|
|
|
|
+ public void handlePayCallback(String outTradeNo, String tradeNo, boolean success,
|
|
|
|
|
+ String totalAmount, String appId, String rawContent) {
|
|
|
|
|
+ // 查询支付流水(忽略租户拦截,支付宝回调无租户上下文)
|
|
|
HotelPayLog payLog = payLogMapper.selectByOutTradeNo(outTradeNo);
|
|
HotelPayLog payLog = payLogMapper.selectByOutTradeNo(outTradeNo);
|
|
|
if (payLog == null) {
|
|
if (payLog == null) {
|
|
|
log.warn("支付回调: 找不到支付流水, outTradeNo={}", outTradeNo);
|
|
log.warn("支付回调: 找不到支付流水, outTradeNo={}", outTradeNo);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 幂等校验:如果已经处理过,直接返回
|
|
|
|
|
- if (payLog.getPayStatus() == HotelOrderConstants.PAY_STATUS_SUCCESS) {
|
|
|
|
|
- log.info("支付回调: 已处理过, outTradeNo={}", outTradeNo);
|
|
|
|
|
|
|
+ // 用 payLog 中的 tenantId 设置租户上下文,后续所有 DB 操作都需要租户上下文
|
|
|
|
|
+ TenantContextHolder.executeWithTenant(payLog.getTenantId(), new Runnable() {
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void run() {
|
|
|
|
|
+ processPayCallback(payLog, tradeNo, success, totalAmount, appId, rawContent);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 在租户上下文中处理支付回调。
|
|
|
|
|
+ */
|
|
|
|
|
+ private void processPayCallback(HotelPayLog payLog, String tradeNo, boolean success,
|
|
|
|
|
+ String totalAmount, String appId, String rawContent) {
|
|
|
|
|
+ // 安全校验 1:验证 app_id
|
|
|
|
|
+ if (appId != null && !appId.isEmpty() && !appId.equals(alipayConfig.getAppId())) {
|
|
|
|
|
+ log.error("支付回调: app_id不匹配, expected={}, actual={}", alipayConfig.getAppId(), appId);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 安全校验 2:验证支付金额
|
|
|
|
|
+ if (success && totalAmount != null && !totalAmount.isEmpty()) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ BigDecimal callbackAmount = new BigDecimal(totalAmount);
|
|
|
|
|
+ if (callbackAmount.compareTo(payLog.getPayAmount()) != 0) {
|
|
|
|
|
+ log.error("支付回调: 金额不匹配, expected={}, actual={}, outTradeNo={}",
|
|
|
|
|
+ payLog.getPayAmount(), callbackAmount, payLog.getOutTradeNo());
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ log.error("支付回调: 金额格式异常, totalAmount={}", totalAmount, e);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
if (success) {
|
|
if (success) {
|
|
|
- // 支付成功
|
|
|
|
|
- payLog.setTradeNo(tradeNo);
|
|
|
|
|
- payLog.setPayStatus(HotelOrderConstants.PAY_STATUS_SUCCESS);
|
|
|
|
|
- payLog.setPayTime(LocalDateTime.now());
|
|
|
|
|
- payLog.setNotifyTime(LocalDateTime.now());
|
|
|
|
|
- payLog.setNotifyContent(rawContent);
|
|
|
|
|
- payLogMapper.updateById(payLog);
|
|
|
|
|
|
|
+ // 数据库级幂等更新:仅当 pay_status != 1 时才更新,利用数据库行锁保证原子性
|
|
|
|
|
+ LocalDateTime now = LocalDateTime.now();
|
|
|
|
|
+ int updated = payLogMapper.updatePayStatusSuccessByIdempotent(
|
|
|
|
|
+ payLog.getId(), tradeNo, now, now, rawContent);
|
|
|
|
|
+
|
|
|
|
|
+ if (updated == 0) {
|
|
|
|
|
+ // 影响行数为 0,说明已经被其他回调处理过了(幂等)
|
|
|
|
|
+ log.info("支付回调: 已处理过(幂等), outTradeNo={}", payLog.getOutTradeNo());
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
// 更新订单状态
|
|
// 更新订单状态
|
|
|
HotelOrder order = orderMapper.selectById(payLog.getOrderId());
|
|
HotelOrder order = orderMapper.selectById(payLog.getOrderId());
|
|
@@ -154,23 +252,21 @@ public class HotelPayServiceImpl implements HotelPayService {
|
|
|
if (order.getPayStatus() == null || order.getPayStatus() != HotelOrderConstants.PAY_STATUS_SUCCESS) {
|
|
if (order.getPayStatus() == null || order.getPayStatus() != HotelOrderConstants.PAY_STATUS_SUCCESS) {
|
|
|
order.setStatus(HotelOrderConstants.STATUS_PLACED);
|
|
order.setStatus(HotelOrderConstants.STATUS_PLACED);
|
|
|
order.setPayStatus(HotelOrderConstants.PAY_STATUS_SUCCESS);
|
|
order.setPayStatus(HotelOrderConstants.PAY_STATUS_SUCCESS);
|
|
|
- order.setPayTime(LocalDateTime.now());
|
|
|
|
|
|
|
+ order.setPayTime(now);
|
|
|
order.setPayTradeNo(tradeNo);
|
|
order.setPayTradeNo(tradeNo);
|
|
|
order.setPaidAmount(payLog.getPayAmount());
|
|
order.setPaidAmount(payLog.getPayAmount());
|
|
|
order.setPaySource(payLog.getPaySource());
|
|
order.setPaySource(payLog.getPaySource());
|
|
|
orderMapper.updateById(order);
|
|
orderMapper.updateById(order);
|
|
|
- log.info("支付成功: orderId={}, orderNo={}, tradeNo={}", order.getId(), order.getOrderNo(), tradeNo);
|
|
|
|
|
|
|
+ log.info("支付成功: orderId={}, orderNo={}, tradeNo={}, amount={}",
|
|
|
|
|
+ order.getId(), order.getOrderNo(), tradeNo, payLog.getPayAmount());
|
|
|
|
|
|
|
|
// 发布新订单通知到SSE(触发PC端声音提醒)
|
|
// 发布新订单通知到SSE(触发PC端声音提醒)
|
|
|
try {
|
|
try {
|
|
|
- // 使用 @Value 注入的 serverType 判断当前服务类型
|
|
|
|
|
if ("app".equals(serverType)) {
|
|
if ("app".equals(serverType)) {
|
|
|
- // AppServer 服务:通过 HTTP 调用 AdminServer 的远程通知接口
|
|
|
|
|
log.info("检测到 AppServer 服务 (serverType={}),通过 HTTP 调用 AdminServer 推送 SSE 通知", serverType);
|
|
log.info("检测到 AppServer 服务 (serverType={}),通过 HTTP 调用 AdminServer 推送 SSE 通知", serverType);
|
|
|
notifyRemoteAdminServer(order.getId(), order.getOrderNo(), order.getRoomNo(),
|
|
notifyRemoteAdminServer(order.getId(), order.getOrderNo(), order.getRoomNo(),
|
|
|
order.getContactName(), order.getTotalAmount() != null ? order.getTotalAmount().toPlainString() : "0");
|
|
order.getContactName(), order.getTotalAmount() != null ? order.getTotalAmount().toPlainString() : "0");
|
|
|
} else {
|
|
} else {
|
|
|
- // AdminServer 服务:直接推送 SSE 通知
|
|
|
|
|
log.info("检测到 AdminServer 服务 (serverType={}),直接推送 SSE 通知", serverType);
|
|
log.info("检测到 AdminServer 服务 (serverType={}),直接推送 SSE 通知", serverType);
|
|
|
sseEmitterManager.notifyNewOrder(
|
|
sseEmitterManager.notifyNewOrder(
|
|
|
order.getId(),
|
|
order.getId(),
|
|
@@ -188,11 +284,11 @@ public class HotelPayServiceImpl implements HotelPayService {
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
// 支付失败
|
|
// 支付失败
|
|
|
- payLog.setPayStatus(2); // 支付失败
|
|
|
|
|
|
|
+ payLog.setPayStatus(2);
|
|
|
payLog.setNotifyTime(LocalDateTime.now());
|
|
payLog.setNotifyTime(LocalDateTime.now());
|
|
|
payLog.setNotifyContent(rawContent);
|
|
payLog.setNotifyContent(rawContent);
|
|
|
payLogMapper.updateById(payLog);
|
|
payLogMapper.updateById(payLog);
|
|
|
- log.warn("支付失败: outTradeNo={}", outTradeNo);
|
|
|
|
|
|
|
+ log.warn("支付失败: outTradeNo={}", payLog.getOutTradeNo());
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -225,6 +321,67 @@ public class HotelPayServiceImpl implements HotelPayService {
|
|
|
return vo;
|
|
return vo;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ==================== 主动查询支付宝订单状态(兖底机制) ====================
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public PayResultVO queryAlipayTradeStatus(Long tenantId, Long orderId) {
|
|
|
|
|
+ return TenantContextHolder.executeWithTenant(tenantId,
|
|
|
|
|
+ new java.util.function.Supplier<PayResultVO>() {
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public PayResultVO get() {
|
|
|
|
|
+ return doQueryAlipayTradeStatus(orderId);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private PayResultVO doQueryAlipayTradeStatus(Long orderId) {
|
|
|
|
|
+ HotelOrder order = orderMapper.selectById(orderId);
|
|
|
|
|
+ if (order == null) {
|
|
|
|
|
+ throw new BusinessException("订单不存在");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 如果订单已经支付成功,直接返回
|
|
|
|
|
+ if (order.getPayStatus() != null && order.getPayStatus() == HotelOrderConstants.PAY_STATUS_SUCCESS) {
|
|
|
|
|
+ return queryPayStatus(orderId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 查找该订单的最新支付流水,获取真实的 outTradeNo
|
|
|
|
|
+ HotelPayLog payLog = payLogMapper.selectLatestByOrderId(orderId);
|
|
|
|
|
+ if (payLog == null || payLog.getOutTradeNo() == null) {
|
|
|
|
|
+ log.warn("主动查询: 找不到支付流水, orderId={}", orderId);
|
|
|
|
|
+ return queryPayStatus(orderId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String outTradeNo = payLog.getOutTradeNo();
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
|
|
|
|
|
+ request.setBizContent("{\"out_trade_no\":\"" + outTradeNo + "\"}");
|
|
|
|
|
+ AlipayTradeQueryResponse response = alipayClient.execute(request);
|
|
|
|
|
+
|
|
|
|
|
+ if (response.isSuccess()) {
|
|
|
|
|
+ String tradeStatus = response.getTradeStatus();
|
|
|
|
|
+ log.info("主动查询支付宝订单状态: outTradeNo={}, tradeStatus={}", outTradeNo, tradeStatus);
|
|
|
|
|
+
|
|
|
|
|
+ if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) {
|
|
|
|
|
+ // 支付宝侧已支付成功,但本地未更新,触发回调处理
|
|
|
|
|
+ log.warn("支付宝侧已支付但本地未更新,触发补偿: outTradeNo={}, tradeNo={}",
|
|
|
|
|
+ outTradeNo, response.getTradeNo());
|
|
|
|
|
+ handlePayCallback(outTradeNo, response.getTradeNo(), true,
|
|
|
|
|
+ response.getTotalAmount(), alipayConfig.getAppId(),
|
|
|
|
|
+ "[主动查询补偿] tradeStatus=" + tradeStatus);
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ log.warn("主动查询支付宝订单失败: code={}, msg={}", response.getCode(), response.getSubMsg());
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("主动查询支付宝订单异常: orderId={}", orderId, e);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 无论查询结果如何,都返回当前本地状态
|
|
|
|
|
+ return queryPayStatus(orderId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// ==================== 取消超时订单 ====================
|
|
// ==================== 取消超时订单 ====================
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
@@ -275,11 +432,13 @@ public class HotelPayServiceImpl implements HotelPayService {
|
|
|
// 执行回调处理(在租户上下文中执行)
|
|
// 执行回调处理(在租户上下文中执行)
|
|
|
final String finalOutTradeNo = outTradeNo;
|
|
final String finalOutTradeNo = outTradeNo;
|
|
|
final String finalMockTradeNo = mockTradeNo;
|
|
final String finalMockTradeNo = mockTradeNo;
|
|
|
|
|
+ final String finalAmount = order.getTotalAmount().toPlainString();
|
|
|
TenantContextHolder.executeWithTenant(order.getTenantId(),
|
|
TenantContextHolder.executeWithTenant(order.getTenantId(),
|
|
|
new Runnable() {
|
|
new Runnable() {
|
|
|
@Override
|
|
@Override
|
|
|
public void run() {
|
|
public void run() {
|
|
|
handlePayCallback(finalOutTradeNo, finalMockTradeNo, true,
|
|
handlePayCallback(finalOutTradeNo, finalMockTradeNo, true,
|
|
|
|
|
+ finalAmount, alipayConfig.getAppId(),
|
|
|
"{\"mock\": true, \"trade_no\": \"" + finalMockTradeNo + "\"}");
|
|
"{\"mock\": true, \"trade_no\": \"" + finalMockTradeNo + "\"}");
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|