소스 검색

新增微信/支付宝支付(使用商户配置)

zhangchen 1 개월 전
부모
커밋
ba06581970

+ 107 - 0
alien-entity/src/main/java/shop/alien/entity/store/MerchantPaymentOrder.java

@@ -0,0 +1,107 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 商户支付单表(关联 user_reservation_order,记录每笔支付与支付宝/微信流水)
+ *
+ * @author system
+ */
+@Data
+@JsonInclude
+@TableName("merchant_payment_order")
+@ApiModel(value = "MerchantPaymentOrder对象", description = "商户支付单表")
+public class MerchantPaymentOrder {
+
+    @ApiModelProperty(value = "主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "支付单号(业务侧唯一)")
+    @TableField("payment_no")
+    private String paymentNo;
+
+    @ApiModelProperty(value = "业务订单类型: reservation_order-预订订单")
+    @TableField("order_type")
+    private String orderType;
+
+    @ApiModelProperty(value = "业务订单主键 user_reservation_order.id")
+    @TableField("order_id")
+    private Integer orderId;
+
+    @ApiModelProperty(value = "业务订单编号 user_reservation_order.order_sn")
+    @TableField("order_sn")
+    private String orderSn;
+
+    @ApiModelProperty(value = "门店ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "支付方式: alipay-支付宝, wechatPay-微信")
+    @TableField("pay_type")
+    private String payType;
+
+    @ApiModelProperty(value = "商户订单号(传给支付宝/微信)")
+    @TableField("out_trade_no")
+    private String outTradeNo;
+
+    @ApiModelProperty(value = "第三方交易号(支付宝trade_no/微信transaction_id)")
+    @TableField("trade_no")
+    private String tradeNo;
+
+    @ApiModelProperty(value = "支付金额(元)")
+    @TableField("pay_amount")
+    private BigDecimal payAmount;
+
+    @ApiModelProperty(value = "支付状态: 0-待支付, 1-已支付, 2-已关闭, 3-已退款")
+    @TableField("pay_status")
+    private Integer payStatus;
+
+    @ApiModelProperty(value = "支付完成时间")
+    @TableField("pay_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date payTime;
+
+    @ApiModelProperty(value = "付款人用户ID")
+    @TableField("payer_user_id")
+    private Integer payerUserId;
+
+    @ApiModelProperty(value = "订单描述/商品标题")
+    @TableField("subject")
+    private String subject;
+
+    @ApiModelProperty(value = "删除标记 0:未删除 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "更新时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}

+ 3 - 0
alien-entity/src/main/java/shop/alien/entity/store/StorePaymentConfig.java

@@ -54,6 +54,9 @@ public class StorePaymentConfig {
     @TableField("app_public_cert_name")
     private String appPublicCertName;
 
+    @ApiModelProperty(value = "支付宝公钥证书文件(存储文件内容)")
+    @TableField("alipay_public_cert")
+    private String alipayPublicCert;
 
     @ApiModelProperty(value = "支付宝公钥证书路径")
     @TableField("alipay_public_cert_path")

+ 15 - 0
alien-entity/src/main/java/shop/alien/mapper/MerchantPaymentOrderMapper.java

@@ -0,0 +1,15 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.store.MerchantPaymentOrder;
+
+/**
+ * 商户支付单表 Mapper 接口
+ *
+ * @author system
+ */
+@Mapper
+public interface MerchantPaymentOrderMapper extends BaseMapper<MerchantPaymentOrder> {
+
+}

+ 84 - 0
alien-store/src/main/java/shop/alien/store/controller/MerchantPaymentController.java

@@ -0,0 +1,84 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.store.strategy.merchantPayment.MerchantPaymentStrategyFactory;
+
+import java.util.Map;
+
+/**
+ * 商户支付接口(预订订单 user_reservation_order,使用 StorePaymentConfig 按门店配置)
+ * 与 /payment 现有接口隔离,不影响原有支付接口。
+ *
+ * @author system
+ */
+@Slf4j
+@Api(tags = {"商户支付(预订订单)"})
+@RestController
+@RequestMapping("/merchantPayment")
+@RequiredArgsConstructor
+public class MerchantPaymentController {
+
+    private final MerchantPaymentStrategyFactory merchantPaymentStrategyFactory;
+
+    @ApiOperation("预订订单-创建预支付(支付宝,使用门店支付配置)")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "orderId", value = "预订订单ID user_reservation_order.id", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "amountYuan", value = "支付金额(元)", required = true, paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "subject", value = "订单描述/商品标题", required = true, paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "userId", value = "用户ID", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "payType", value = "支付类型 alipay", required = true, paramType = "query", dataType = "String")
+    })
+    @PostMapping("/prePay")
+    public R<Map<String, Object>> prePay(
+            @RequestParam Integer storeId,
+            @RequestParam Integer orderId,
+            @RequestParam String amountYuan,
+            @RequestParam String subject,
+            @RequestParam Integer userId,
+            @RequestParam(defaultValue = "alipay") String payType) {
+        log.info("MerchantPaymentController.prePay storeId={}, orderId={}, payType={}", storeId, orderId, payType);
+        return merchantPaymentStrategyFactory.getStrategy(payType).createPrePay(storeId, orderId, amountYuan, subject, userId);
+    }
+
+    @ApiOperation("预订订单-查询支付状态")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "outTradeNo", value = "商户订单号(预支付返回)", required = true, paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "payType", value = "支付类型 alipay", required = true, paramType = "query", dataType = "String")
+    })
+    @GetMapping("/queryStatus")
+    public R<Object> queryStatus(
+            @RequestParam Integer storeId,
+            @RequestParam String outTradeNo,
+            @RequestParam(defaultValue = "alipay") String payType) {
+        log.info("MerchantPaymentController.queryStatus storeId={}, outTradeNo={}", storeId, outTradeNo);
+        return merchantPaymentStrategyFactory.getStrategy(payType).queryPayStatus(storeId, outTradeNo);
+    }
+
+    @ApiOperation("预订订单-退款")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", required = true, paramType = "query", dataType = "int"),
+            @ApiImplicitParam(name = "outTradeNo", value = "商户订单号", required = true, paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "refundAmount", value = "退款金额(元)", required = true, paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "refundReason", value = "退款原因", paramType = "query", dataType = "String"),
+            @ApiImplicitParam(name = "payType", value = "支付类型 alipay", required = true, paramType = "query", dataType = "String")
+    })
+    @PostMapping("/refund")
+    public R<String> refund(
+            @RequestParam Integer storeId,
+            @RequestParam String outTradeNo,
+            @RequestParam String refundAmount,
+            @RequestParam(required = false) String refundReason,
+            @RequestParam(defaultValue = "alipay") String payType) {
+        log.info("MerchantPaymentController.refund storeId={}, outTradeNo={}", storeId, outTradeNo);
+        return merchantPaymentStrategyFactory.getStrategy(payType).refund(storeId, outTradeNo, refundAmount, refundReason);
+    }
+}

+ 35 - 0
alien-store/src/main/java/shop/alien/store/service/MerchantPaymentOrderService.java

@@ -0,0 +1,35 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.MerchantPaymentOrder;
+
+/**
+ * 商户支付单表 服务类
+ *
+ * @author system
+ */
+public interface MerchantPaymentOrderService extends IService<MerchantPaymentOrder> {
+
+    /**
+     * 根据商户订单号查询支付单
+     *
+     * @param outTradeNo 商户订单号
+     * @return 支付单,不存在返回 null
+     */
+    MerchantPaymentOrder getByOutTradeNo(String outTradeNo);
+
+    /**
+     * 生成支付单号,格式:PAY + yyyyMMddHHmmss + 4位随机
+     *
+     * @return 支付单号
+     */
+    String generatePaymentNo();
+
+    /**
+     * 按订单ID将支付单逻辑删除(delete_flag=1)
+     *
+     * @param orderId 业务订单ID user_reservation_order.id
+     * @return 更新的条数
+     */
+    int logicDeleteByOrderId(Integer orderId);
+}

+ 35 - 0
alien-store/src/main/java/shop/alien/store/service/UserReservationOrderService.java

@@ -0,0 +1,35 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.UserReservationOrder;
+
+/**
+ * 用户预订订单表 服务类
+ *
+ * @author system
+ */
+public interface UserReservationOrderService extends IService<UserReservationOrder> {
+
+    /**
+     * 根据订单编号查询
+     *
+     * @param orderSn 订单编号
+     * @return 订单,不存在返回 null
+     */
+    UserReservationOrder getByOrderSn(String orderSn);
+
+    /**
+     * 根据支付单商户订单号查询(通过 merchant_payment_order 关联)
+     *
+     * @param outTradeNo 商户订单号
+     * @return 预订订单,不存在返回 null
+     */
+    UserReservationOrder getByOutTradeNo(String outTradeNo);
+
+    /**
+     * 生成订单编号,格式:YS + yyyyMMdd + 6位序列
+     *
+     * @return 订单编号
+     */
+    String generateOrderSn();
+}

+ 56 - 0
alien-store/src/main/java/shop/alien/store/service/impl/MerchantPaymentOrderServiceImpl.java

@@ -0,0 +1,56 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.MerchantPaymentOrder;
+import shop.alien.mapper.MerchantPaymentOrderMapper;
+import shop.alien.store.service.MerchantPaymentOrderService;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * 商户支付单表 服务实现类
+ *
+ * @author system
+ */
+@Slf4j
+@Service
+@Transactional(rollbackFor = Exception.class)
+@RequiredArgsConstructor
+public class MerchantPaymentOrderServiceImpl extends ServiceImpl<MerchantPaymentOrderMapper, MerchantPaymentOrder> implements MerchantPaymentOrderService {
+
+    @Override
+    public MerchantPaymentOrder getByOutTradeNo(String outTradeNo) {
+        if (outTradeNo == null || outTradeNo.trim().isEmpty()) {
+            return null;
+        }
+        LambdaQueryWrapper<MerchantPaymentOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(MerchantPaymentOrder::getOutTradeNo, outTradeNo);
+        return this.getOne(wrapper);
+    }
+
+    @Override
+    public String generatePaymentNo() {
+        String dateStr = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
+        int random = ThreadLocalRandom.current().nextInt(10000);
+        return "PAY" + dateStr + String.format("%04d", random);
+    }
+
+    @Override
+    public int logicDeleteByOrderId(Integer orderId) {
+        if (orderId == null) {
+            return 0;
+        }
+        LambdaUpdateWrapper<MerchantPaymentOrder> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(MerchantPaymentOrder::getOrderId, orderId);
+        wrapper.set(MerchantPaymentOrder::getDeleteFlag, 1);
+        return baseMapper.update(null, wrapper);
+    }
+}

+ 60 - 0
alien-store/src/main/java/shop/alien/store/service/impl/UserReservationOrderServiceImpl.java

@@ -0,0 +1,60 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.MerchantPaymentOrder;
+import shop.alien.entity.store.UserReservationOrder;
+import shop.alien.mapper.UserReservationOrderMapper;
+import shop.alien.store.service.MerchantPaymentOrderService;
+import shop.alien.store.service.UserReservationOrderService;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.concurrent.ThreadLocalRandom;
+
+/**
+ * 用户预订订单表 服务实现类
+ *
+ * @author system
+ */
+@Slf4j
+@Service
+@Transactional(rollbackFor = Exception.class)
+@RequiredArgsConstructor
+public class UserReservationOrderServiceImpl extends ServiceImpl<UserReservationOrderMapper, UserReservationOrder> implements UserReservationOrderService {
+
+    private final MerchantPaymentOrderService merchantPaymentOrderService;
+
+    @Override
+    public UserReservationOrder getByOrderSn(String orderSn) {
+        if (orderSn == null || orderSn.trim().isEmpty()) {
+            return null;
+        }
+        LambdaQueryWrapper<UserReservationOrder> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(UserReservationOrder::getOrderSn, orderSn);
+        return this.getOne(wrapper);
+    }
+
+    @Override
+    public UserReservationOrder getByOutTradeNo(String outTradeNo) {
+        if (outTradeNo == null || outTradeNo.trim().isEmpty()) {
+            return null;
+        }
+        MerchantPaymentOrder paymentOrder = merchantPaymentOrderService.getByOutTradeNo(outTradeNo);
+        if (paymentOrder == null) {
+            return null;
+        }
+        return this.getById(paymentOrder.getOrderId());
+    }
+
+    @Override
+    public String generateOrderSn() {
+        String dateStr = new SimpleDateFormat("yyyyMMdd").format(new Date());
+        int random = ThreadLocalRandom.current().nextInt(1000000);
+        return "YS" + dateStr + String.format("%06d", random);
+    }
+}

+ 53 - 0
alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/MerchantPaymentStrategy.java

@@ -0,0 +1,53 @@
+package shop.alien.store.strategy.merchantPayment;
+
+import shop.alien.entity.result.R;
+
+import java.util.Map;
+
+/**
+ * 商户支付策略接口(预订订单等,使用 StorePaymentConfig 按门店配置)
+ * 与 strategy.payment 现有接口隔离,不影响原有 /payment 接口。
+ *
+ * @author system
+ */
+public interface MerchantPaymentStrategy {
+
+    /**
+     * 创建预支付(使用门店支付配置)
+     *
+     * @param storeId    门店ID
+     * @param orderId    预订订单ID user_reservation_order.id
+     * @param amountYuan 支付金额(元,字符串)
+     * @param subject    订单描述
+     * @param userId     用户ID
+     * @return 含 orderStr、outTradeNo、orderSn、orderId 等
+     */
+    R<Map<String, Object>> createPrePay(Integer storeId, Integer orderId, String amountYuan, String subject, Integer userId);
+
+    /**
+     * 查询支付状态(根据商户订单号查第三方并更新订单与支付单)
+     *
+     * @param storeId    门店ID
+     * @param outTradeNo 商户订单号
+     * @return 支付成功返回 R.success,否则 R.fail
+     */
+    R<Object> queryPayStatus(Integer storeId, String outTradeNo);
+
+    /**
+     * 退款
+     *
+     * @param storeId      门店ID
+     * @param outTradeNo   商户订单号
+     * @param refundAmount 退款金额(元,字符串)
+     * @param refundReason 退款原因
+     * @return 成功 R.data("退款成功"),失败 R.fail
+     */
+    R<String> refund(Integer storeId, String outTradeNo, String refundAmount, String refundReason);
+
+    /**
+     * 策略类型,如 alipay、wechatPay
+     *
+     * @return 支付类型
+     */
+    String getType();
+}

+ 47 - 0
alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/MerchantPaymentStrategyFactory.java

@@ -0,0 +1,47 @@
+package shop.alien.store.strategy.merchantPayment;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 商户支付策略工厂(预订订单等,按门店配置)
+ *
+ * @author system
+ */
+@Slf4j
+@Component
+public class MerchantPaymentStrategyFactory {
+
+    @Autowired
+    private List<MerchantPaymentStrategy> merchantPaymentStrategies;
+
+    private final Map<String, MerchantPaymentStrategy> strategyMap = new HashMap<>();
+
+    @PostConstruct
+    public void init() {
+        if (merchantPaymentStrategies != null && !merchantPaymentStrategies.isEmpty()) {
+            for (MerchantPaymentStrategy strategy : merchantPaymentStrategies) {
+                strategyMap.put(strategy.getType(), strategy);
+                log.info("注册商户支付策略: {} -> {}", strategy.getType(), strategy.getClass().getSimpleName());
+            }
+        }
+    }
+
+    public MerchantPaymentStrategy getStrategy(String type) {
+        MerchantPaymentStrategy strategy = strategyMap.get(type);
+        if (strategy == null) {
+            throw new IllegalArgumentException("不支持的商户支付类型: " + type);
+        }
+        return strategy;
+    }
+
+    public boolean supports(String type) {
+        return strategyMap.containsKey(type);
+    }
+}

+ 353 - 0
alien-store/src/main/java/shop/alien/store/strategy/merchantPayment/impl/MerchantAlipayPaymentStrategyImpl.java

@@ -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;
+    }
+}