|
|
@@ -0,0 +1,353 @@
|
|
|
+package shop.alien.store.strategy.merchantPayment.impl;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import com.alipay.api.AlipayApiException;
|
|
|
+import com.alipay.api.AlipayClient;
|
|
|
+import com.alipay.api.DefaultAlipayClient;
|
|
|
+import com.alipay.api.domain.AlipayTradeAppPayModel;
|
|
|
+import com.alipay.api.domain.AlipayTradeRefundModel;
|
|
|
+import com.alipay.api.request.AlipayTradeAppPayRequest;
|
|
|
+import com.alipay.api.request.AlipayTradeQueryRequest;
|
|
|
+import com.alipay.api.request.AlipayTradeRefundRequest;
|
|
|
+import com.alipay.api.response.AlipayTradeAppPayResponse;
|
|
|
+import com.alipay.api.response.AlipayTradeQueryResponse;
|
|
|
+import com.alipay.api.response.AlipayTradeRefundResponse;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.data.redis.core.StringRedisTemplate;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+import shop.alien.entity.result.R;
|
|
|
+import shop.alien.entity.store.MerchantPaymentOrder;
|
|
|
+import shop.alien.entity.store.RefundRecord;
|
|
|
+import shop.alien.entity.store.StorePaymentConfig;
|
|
|
+import shop.alien.entity.store.UserReservationOrder;
|
|
|
+import shop.alien.store.service.MerchantPaymentOrderService;
|
|
|
+import shop.alien.store.service.RefundRecordService;
|
|
|
+import shop.alien.store.service.StorePaymentConfigService;
|
|
|
+import shop.alien.store.service.UserReservationOrderService;
|
|
|
+import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategy;
|
|
|
+import shop.alien.util.common.UniqueRandomNumGenerator;
|
|
|
+import shop.alien.util.common.constant.PaymentEnum;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.nio.file.Path;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.concurrent.TimeUnit;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 商户支付宝支付策略(预订订单,使用 StorePaymentConfig 按门店配置)
|
|
|
+ *
|
|
|
+ * @author system
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Component
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class MerchantAlipayPaymentStrategyImpl implements MerchantPaymentStrategy {
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 支付宝网关地址
|
|
|
+ */
|
|
|
+ @Value("${payment.aliPay.host}")
|
|
|
+ private String aliPayHost;
|
|
|
+
|
|
|
+ /** 预支付结果 Redis 缓存 key 前缀 */
|
|
|
+ private static final String REDIS_PREPAY_KEY_PREFIX = "merchant:alipay:prepay:order:";
|
|
|
+ /** 预支付缓存过期时间(秒),与支付宝预支付单 15 分钟一致 */
|
|
|
+ private static final long REDIS_PREPAY_EXPIRE_SECONDS = 15 * 60;
|
|
|
+
|
|
|
+ private final StorePaymentConfigService storePaymentConfigService;
|
|
|
+ private final UserReservationOrderService userReservationOrderService;
|
|
|
+ private final MerchantPaymentOrderService merchantPaymentOrderService;
|
|
|
+ private final RefundRecordService refundRecordService;
|
|
|
+ private final StringRedisTemplate stringRedisTemplate;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public R<Map<String, Object>> createPrePay(Integer storeId, Integer orderId, String amountYuan, String subject, Integer userId) {
|
|
|
+ if (storeId == null || orderId == null) {
|
|
|
+ return R.fail("门店ID和订单ID不能为空");
|
|
|
+ }
|
|
|
+ if (StringUtils.isBlank(amountYuan) || new BigDecimal(amountYuan).compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ return R.fail("支付金额必须大于0");
|
|
|
+ }
|
|
|
+ if (StringUtils.isBlank(subject)) {
|
|
|
+ return R.fail("订单描述不能为空");
|
|
|
+ }
|
|
|
+ StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
|
|
|
+ if (config == null) {
|
|
|
+ return R.fail("该门店未配置支付参数");
|
|
|
+ }
|
|
|
+ if (StringUtils.isBlank(config.getAppSecretCert()) || StringUtils.isBlank(config.getAppPublicCert())
|
|
|
+ || StringUtils.isBlank(config.getAlipayPublicCert()) || StringUtils.isBlank(config.getAlipayRootCert())) {
|
|
|
+ return R.fail("门店支付配置不完整(缺少应用私钥或证书)");
|
|
|
+ }
|
|
|
+ UserReservationOrder order = userReservationOrderService.getById(orderId);
|
|
|
+ if (order == null) {
|
|
|
+ return R.fail("预订订单不存在");
|
|
|
+ }
|
|
|
+ if (!order.getStoreId().equals(storeId)) {
|
|
|
+ return R.fail("订单与门店不匹配");
|
|
|
+ }
|
|
|
+ if (order.getPaymentStatus() != null && order.getPaymentStatus() == 1) {
|
|
|
+ return R.fail("订单已支付");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 再次调用时优先从 Redis 获取已生成的预支付信息(未过期则直接返回)
|
|
|
+ String redisKey = REDIS_PREPAY_KEY_PREFIX + orderId;
|
|
|
+ String cached = stringRedisTemplate.opsForValue().get(redisKey);
|
|
|
+ if (StringUtils.isNotBlank(cached)) {
|
|
|
+ try {
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ Map<String, Object> data = JSON.parseObject(cached, Map.class);
|
|
|
+ if (data != null && !data.isEmpty()) {
|
|
|
+ log.info("商户预订订单预支付命中缓存,orderId={}", orderId);
|
|
|
+ return R.data(data);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("解析预支付缓存失败,将重新发起预支付,orderId={}", orderId, e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 未命中缓存:先按订单ID将原支付单逻辑删除,再生成新预支付
|
|
|
+ int deleted = merchantPaymentOrderService.logicDeleteByOrderId(orderId);
|
|
|
+ if (deleted > 0) {
|
|
|
+ log.info("未命中缓存,已逻辑删除该订单下 {} 条支付单,orderId={}", deleted, orderId);
|
|
|
+ }
|
|
|
+
|
|
|
+ String outTradeNo = UniqueRandomNumGenerator.generateUniqueCode(19);
|
|
|
+ BigDecimal amount = new BigDecimal(amountYuan);
|
|
|
+
|
|
|
+ MerchantPaymentOrder paymentOrder = new MerchantPaymentOrder();
|
|
|
+ paymentOrder.setPaymentNo(merchantPaymentOrderService.generatePaymentNo());
|
|
|
+ paymentOrder.setOrderType("reservation_order");
|
|
|
+ paymentOrder.setOrderId(order.getId());
|
|
|
+ paymentOrder.setOrderSn(order.getOrderSn());
|
|
|
+ paymentOrder.setStoreId(storeId);
|
|
|
+ paymentOrder.setPayType(PaymentEnum.ALIPAY.getType());
|
|
|
+ paymentOrder.setOutTradeNo(outTradeNo);
|
|
|
+ paymentOrder.setPayAmount(amount);
|
|
|
+ paymentOrder.setPayStatus(0);
|
|
|
+ paymentOrder.setPayerUserId(userId);
|
|
|
+ paymentOrder.setSubject(subject);
|
|
|
+ paymentOrder.setCreatedTime(new Date());
|
|
|
+ paymentOrder.setUpdatedTime(new Date());
|
|
|
+ merchantPaymentOrderService.save(paymentOrder);
|
|
|
+
|
|
|
+ try {
|
|
|
+ com.alipay.api.AlipayConfig alipayConfig = buildAlipayConfigFromStore(config);
|
|
|
+ AlipayClient client = new DefaultAlipayClient(alipayConfig);
|
|
|
+ AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
|
|
|
+ AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
|
|
|
+ model.setOutTradeNo(outTradeNo);
|
|
|
+ model.setTotalAmount(amountYuan);
|
|
|
+ model.setSubject(subject);
|
|
|
+ // 预支付单有效时间 15 分钟(相对超时,格式 1m~15d)
|
|
|
+ model.setTimeoutExpress("15m");
|
|
|
+ request.setBizModel(model);
|
|
|
+ AlipayTradeAppPayResponse response = client.sdkExecute(request);
|
|
|
+ String orderStr = response.isSuccess() ? response.getBody() : "";
|
|
|
+
|
|
|
+ if (!response.isSuccess()) {
|
|
|
+ return R.fail("预支付失败:" + response.getSubMsg());
|
|
|
+ }
|
|
|
+
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("orderStr", orderStr);
|
|
|
+ data.put("outTradeNo", outTradeNo);
|
|
|
+ data.put("orderSn", order.getOrderSn());
|
|
|
+ data.put("orderId", order.getId());
|
|
|
+ data.put("paymentNo", paymentOrder.getPaymentNo());
|
|
|
+ stringRedisTemplate.opsForValue().set(redisKey, JSON.toJSONString(data), REDIS_PREPAY_EXPIRE_SECONDS, TimeUnit.SECONDS);
|
|
|
+ log.info("商户预订订单预支付成功并写入缓存,storeId={}, orderSn={}, outTradeNo={}", storeId, order.getOrderSn(), outTradeNo);
|
|
|
+ return R.data(data);
|
|
|
+ } catch (AlipayApiException e) {
|
|
|
+ log.error("商户预订订单预支付异常,storeId={}, orderId={}", storeId, orderId, e);
|
|
|
+ return R.fail("预支付失败:" + e.getErrMsg());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("构建支付宝配置异常,storeId={}", storeId, e);
|
|
|
+ return R.fail("预支付失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public R<Object> queryPayStatus(Integer storeId, String outTradeNo) {
|
|
|
+ if (storeId == null || StringUtils.isBlank(outTradeNo)) {
|
|
|
+ return R.fail("门店ID和商户订单号不能为空");
|
|
|
+ }
|
|
|
+ StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
|
|
|
+ if (config == null) {
|
|
|
+ return R.fail("该门店未配置支付参数");
|
|
|
+ }
|
|
|
+ MerchantPaymentOrder paymentOrder = merchantPaymentOrderService.getByOutTradeNo(outTradeNo);
|
|
|
+ if (paymentOrder == null) {
|
|
|
+ return R.fail("支付单不存在");
|
|
|
+ }
|
|
|
+ UserReservationOrder order = userReservationOrderService.getById(paymentOrder.getOrderId());
|
|
|
+ if (order == null) {
|
|
|
+ return R.fail("预订订单不存在");
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ com.alipay.api.AlipayConfig alipayConfig = buildAlipayConfigFromStore(config);
|
|
|
+ AlipayClient client = new DefaultAlipayClient(alipayConfig);
|
|
|
+ AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
|
|
|
+ JSONObject bizContent = new JSONObject();
|
|
|
+ bizContent.put("out_trade_no", outTradeNo);
|
|
|
+ request.setBizContent(bizContent.toJSONString());
|
|
|
+ AlipayTradeQueryResponse response = client.certificateExecute(request);
|
|
|
+ if (!response.isSuccess()) {
|
|
|
+ return R.fail("查询失败:" + response.getSubMsg());
|
|
|
+ }
|
|
|
+ String tradeStatus = response.getTradeStatus();
|
|
|
+ if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISHED".equals(tradeStatus)) {
|
|
|
+ Date now = new Date();
|
|
|
+ paymentOrder.setPayStatus(1);
|
|
|
+ paymentOrder.setTradeNo(response.getTradeNo());
|
|
|
+ paymentOrder.setPayTime(now);
|
|
|
+ paymentOrder.setUpdatedTime(now);
|
|
|
+ merchantPaymentOrderService.updateById(paymentOrder);
|
|
|
+
|
|
|
+ order.setPaymentStatus(1);
|
|
|
+ order.setPayTime(now);
|
|
|
+ order.setPaymentMethod("支付宝");
|
|
|
+ order.setOrderStatus(1);
|
|
|
+ if (StringUtils.isBlank(order.getVerificationCode())) {
|
|
|
+ order.setVerificationCode("YS" + UniqueRandomNumGenerator.generateUniqueCode(10));
|
|
|
+ }
|
|
|
+ order.setUpdatedTime(now);
|
|
|
+ userReservationOrderService.updateById(order);
|
|
|
+ return R.success("支付成功");
|
|
|
+ }
|
|
|
+ if ("TRADE_CLOSED".equals(tradeStatus)) {
|
|
|
+ return R.fail("交易已关闭");
|
|
|
+ }
|
|
|
+ if ("WAIT_BUYER_PAY".equals(tradeStatus)) {
|
|
|
+ return R.fail("等待买家付款");
|
|
|
+ }
|
|
|
+ return R.success("订单状态:" + tradeStatus);
|
|
|
+ } catch (AlipayApiException e) {
|
|
|
+ log.error("查询商户订单支付状态异常,outTradeNo={}", outTradeNo, e);
|
|
|
+ return R.fail("查询异常:" + e.getErrMsg());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("构建支付宝配置异常,storeId={}", storeId, e);
|
|
|
+ return R.fail("查询失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public R<String> refund(Integer storeId, String outTradeNo, String refundAmount, String refundReason) {
|
|
|
+ if (storeId == null || StringUtils.isBlank(outTradeNo)) {
|
|
|
+ return R.fail("门店ID和商户订单号不能为空");
|
|
|
+ }
|
|
|
+ if (StringUtils.isBlank(refundAmount) || new BigDecimal(refundAmount).compareTo(BigDecimal.ZERO) <= 0) {
|
|
|
+ return R.fail("退款金额必须大于0");
|
|
|
+ }
|
|
|
+ StorePaymentConfig config = storePaymentConfigService.getByStoreId(storeId);
|
|
|
+ if (config == null) {
|
|
|
+ return R.fail("该门店未配置支付参数");
|
|
|
+ }
|
|
|
+ MerchantPaymentOrder paymentOrder = merchantPaymentOrderService.getByOutTradeNo(outTradeNo);
|
|
|
+ if (paymentOrder == null) {
|
|
|
+ return R.fail("支付单不存在");
|
|
|
+ }
|
|
|
+ UserReservationOrder order = userReservationOrderService.getById(paymentOrder.getOrderId());
|
|
|
+ if (order == null) {
|
|
|
+ return R.fail("预订订单不存在");
|
|
|
+ }
|
|
|
+ if (order.getPaymentStatus() == null || order.getPaymentStatus() != 1) {
|
|
|
+ return R.fail("订单未支付或已退款,无法退款");
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ com.alipay.api.AlipayConfig alipayConfig = buildAlipayConfigFromStore(config);
|
|
|
+ AlipayClient client = new DefaultAlipayClient(alipayConfig);
|
|
|
+ AlipayTradeRefundRequest request = new AlipayTradeRefundRequest();
|
|
|
+ AlipayTradeRefundModel model = new AlipayTradeRefundModel();
|
|
|
+ model.setOutTradeNo(outTradeNo);
|
|
|
+ model.setRefundAmount(refundAmount);
|
|
|
+ model.setRefundReason(StringUtils.isNotBlank(refundReason) ? refundReason : "用户申请退款");
|
|
|
+ request.setBizModel(model);
|
|
|
+ AlipayTradeRefundResponse response = client.certificateExecute(request);
|
|
|
+ if (!response.isSuccess()) {
|
|
|
+ return R.fail("退款失败:" + response.getSubMsg());
|
|
|
+ }
|
|
|
+ JSONObject responseBody = JSONObject.parseObject(response.getBody());
|
|
|
+ JSONObject refundResponse = responseBody != null ? responseBody.getJSONObject("alipay_trade_refund_response") : null;
|
|
|
+ String tradeNo = refundResponse != null ? refundResponse.getString("trade_no") : null;
|
|
|
+
|
|
|
+ Date now = new Date();
|
|
|
+ BigDecimal refundAmountDecimal = new BigDecimal(refundAmount);
|
|
|
+ paymentOrder.setPayStatus(3);
|
|
|
+ paymentOrder.setUpdatedTime(now);
|
|
|
+ merchantPaymentOrderService.updateById(paymentOrder);
|
|
|
+
|
|
|
+ order.setPaymentStatus(2);
|
|
|
+ order.setRefundAmount(refundAmountDecimal);
|
|
|
+ order.setRefundTime(now);
|
|
|
+ order.setRefundReason(refundReason);
|
|
|
+ order.setUpdatedTime(now);
|
|
|
+ userReservationOrderService.updateById(order);
|
|
|
+
|
|
|
+ RefundRecord record = new RefundRecord();
|
|
|
+ record.setPayType(PaymentEnum.ALIPAY.getType());
|
|
|
+ record.setOutTradeNo(outTradeNo);
|
|
|
+ record.setTransactionId(tradeNo);
|
|
|
+ record.setOutRefundNo(UniqueRandomNumGenerator.generateUniqueCode(19));
|
|
|
+ record.setRefundStatus("SUCCESS");
|
|
|
+ if (order.getTotalAmount() != null) {
|
|
|
+ record.setTotalAmount(order.getTotalAmount().multiply(new BigDecimal(100)).longValue());
|
|
|
+ }
|
|
|
+ record.setRefundAmount(refundAmountDecimal.multiply(new BigDecimal(100)).longValue());
|
|
|
+ record.setRefundReason(refundReason);
|
|
|
+ record.setOrderId(String.valueOf(order.getId()));
|
|
|
+ record.setStoreId(storeId);
|
|
|
+ record.setUserId(order.getUserId());
|
|
|
+ record.setCreatedTime(now);
|
|
|
+ record.setDeleteFlag(0);
|
|
|
+ refundRecordService.save(record);
|
|
|
+
|
|
|
+ log.info("商户预订订单退款成功,outTradeNo={}", outTradeNo);
|
|
|
+ return R.data("退款成功");
|
|
|
+ } catch (AlipayApiException e) {
|
|
|
+ log.error("商户预订订单退款异常,outTradeNo={}", outTradeNo, e);
|
|
|
+ return R.fail("退款失败:" + e.getErrMsg());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("构建支付宝配置异常,storeId={}", storeId, e);
|
|
|
+ return R.fail("退款失败:" + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public String getType() {
|
|
|
+ return PaymentEnum.ALIPAY.getType();
|
|
|
+ }
|
|
|
+
|
|
|
+ private com.alipay.api.AlipayConfig buildAlipayConfigFromStore(StorePaymentConfig config) throws IOException {
|
|
|
+ com.alipay.api.AlipayConfig alipayConfig = new com.alipay.api.AlipayConfig();
|
|
|
+ alipayConfig.setServerUrl(aliPayHost);
|
|
|
+ alipayConfig.setAppId(config.getAppId());
|
|
|
+ alipayConfig.setPrivateKey(config.getAppSecretCert());
|
|
|
+ alipayConfig.setFormat("json");
|
|
|
+ alipayConfig.setCharset("UTF-8");
|
|
|
+ alipayConfig.setSignType("RSA2");
|
|
|
+
|
|
|
+ Path appCert = Files.createTempFile("alipay_app_", ".crt");
|
|
|
+ Files.write(appCert, (config.getAppPublicCert() != null ? config.getAppPublicCert() : "").getBytes(StandardCharsets.UTF_8));
|
|
|
+ alipayConfig.setAppCertPath(appCert.toAbsolutePath().toString());
|
|
|
+
|
|
|
+ Path alipayCert = Files.createTempFile("alipay_public_", ".crt");
|
|
|
+ Files.write(alipayCert, (config.getAlipayPublicCert() != null ? config.getAlipayPublicCert() : "").getBytes(StandardCharsets.UTF_8));
|
|
|
+ alipayConfig.setAlipayPublicCertPath(alipayCert.toAbsolutePath().toString());
|
|
|
+
|
|
|
+ Path rootCert = Files.createTempFile("alipay_root_", ".crt");
|
|
|
+ Files.write(rootCert, (config.getAlipayRootCert() != null ? config.getAlipayRootCert() : "").getBytes(StandardCharsets.UTF_8));
|
|
|
+ alipayConfig.setRootCertPath(rootCert.toAbsolutePath().toString());
|
|
|
+
|
|
|
+ return alipayConfig;
|
|
|
+ }
|
|
|
+}
|