feat:订单支付

pull/1121/head
梁宇奇 2025-04-08 22:01:46 +08:00
parent e9ba3fe08a
commit a7084826ed
16 changed files with 494 additions and 2 deletions

View File

@ -0,0 +1,180 @@
package com.ruoyi.web.controller.xkt;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil;
import com.alipay.api.AlipayApiException;
import com.alipay.api.internal.util.AlipaySignature;
import com.ruoyi.common.core.controller.XktBaseController;
import com.ruoyi.xkt.domain.AlipayCallback;
import com.ruoyi.xkt.domain.StoreOrder;
import com.ruoyi.xkt.enums.EProcessStatus;
import com.ruoyi.xkt.manager.impl.AliPaymentMangerImpl;
import com.ruoyi.xkt.service.IStoreOrderService;
import io.swagger.annotations.Api;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* @author liangyq
* @date 2025-04-07 18:31
*/
@Api(tags = "支付宝回调接口")
@RestController
@RequestMapping("/rest/v1/alipay")
public class AlipayController extends XktBaseController {
private static final String SUCCESS = "success";
private static final String FAILURE = "failure";
@Autowired
private IStoreOrderService storeOrderService;
@Autowired
private AliPaymentMangerImpl paymentManger;
/**
*
* <p>
* 1. success
* 4m10m10m1h2h6h15h
* 2. notify_urlHTML
* success
* 3. POST request.Form("out_trade_no")$_POST['out_trade_no']
* 4. notify_id
*
* @param request
* @return
*/
@PostMapping("/notify")
public String notify(HttpServletRequest request) {
Map<String, String> params = getParamsMap(request);
boolean signVerified = false;
try {
//验证签名
signVerified = AlipaySignature.rsaCheckV1(params, paymentManger.getAlipayPublicKey(),
paymentManger.getCharset(), paymentManger.getSignType());
} catch (AlipayApiException e) {
logger.error("支付宝验签异常", e);
}
if (signVerified) {
AlipayCallback alipayCallback = trans2DO(params);
//需要严格按照如下描述校验通知数据的正确性:
//1. 商家需要验证该通知数据中的 out_trade_no 是否为商家系统中创建的订单号。
StoreOrder order = storeOrderService.getByOrderNo(alipayCallback.getOutTradeNo());
if (order == null) {
logger.info("支付宝回调订单匹配失败:{}", params);
return FAILURE;
}
//2. 判断 total_amount 是否确实为该订单的实际金额(即商家订单创建时的金额)。
if (!NumberUtil.equals(order.getTotalAmount(), alipayCallback.getTotalAmount())) {
logger.info("支付宝回调订单金额匹配失败:{}", params);
return FAILURE;
}
//3. 校验通知中的 seller_id或者 seller_email是否为 out_trade_no 这笔单据的对应的操作方
// (有的时候,一个商家可能有多个 seller_id/seller_email
//4. 验证 app_id 是否为该商家本身。
if (!StrUtil.equals(paymentManger.getAppId(), alipayCallback.getAppId())) {
logger.info("支付宝回调应用ID异常:{}", params);
return FAILURE;
}
//5. 处理支付宝回调信息
paymentManger.processAlipayCallback(alipayCallback);
return SUCCESS;
} else {
logger.info("支付宝验签未通过:{}", params);
return FAILURE;
}
}
/**
*
*
* @param request
* @return
*/
private Map<String, String> getParamsMap(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
Map requestParams = request.getParameterMap();
for (Iterator iter = requestParams.keySet().iterator(); iter.hasNext(); ) {
String name = (String) iter.next();
String[] values = (String[]) requestParams.get(name);
String valueStr = "";
for (int i = 0; i < values.length; i++) {
valueStr = (i == values.length - 1) ? valueStr + values[i]
: valueStr + values[i] + ",";
}
params.put(name, valueStr);
}
return params;
}
/**
* do
*
* @param params
* @return
*/
private AlipayCallback trans2DO(Map<String, String> params) {
AlipayCallback alipayCallback = new AlipayCallback();
alipayCallback.setNotifyType(params.get("notify_type"));
alipayCallback.setNotifyId(params.get("notify_id"));
alipayCallback.setAppId(params.get("app_id"));
alipayCallback.setCharset(params.get("charset"));
alipayCallback.setVersion(params.get("version"));
alipayCallback.setSignType(params.get("sign_type"));
alipayCallback.setSign(params.get("sign"));
alipayCallback.setTradeNo(params.get("trade_no"));
alipayCallback.setOutTradeNo(params.get("out_trade_no"));
alipayCallback.setOutBizNo(params.get("out_biz_no"));
alipayCallback.setBuyerId(params.get("buyer_id"));
alipayCallback.setBuyerLogonId(params.get("buyer_logon_id"));
alipayCallback.setSellerId(params.get("seller_id"));
alipayCallback.setSellerEmail(params.get("seller_email"));
alipayCallback.setTradeStatus(params.get("trade_status"));
String totalAmountStr = params.get("total_amount");
alipayCallback.setTotalAmount(trans2BigDecimal(totalAmountStr));
String receiptAmountStr = params.get("receipt_amount");
alipayCallback.setReceiptAmount(trans2BigDecimal(receiptAmountStr));
String invoiceAmountStr = params.get("invoice_amount");
alipayCallback.setInvoiceAmount(trans2BigDecimal(invoiceAmountStr));
String buyerPayAmountStr = params.get("buyer_pay_amount");
alipayCallback.setBuyerPayAmount(trans2BigDecimal(buyerPayAmountStr));
String pointAmountStr = params.get("point_amount");
alipayCallback.setPointAmount(trans2BigDecimal(pointAmountStr));
String refundFeeStr = params.get("refund_fee");
alipayCallback.setRefundFee(trans2BigDecimal(refundFeeStr));
alipayCallback.setSubject(params.get("subject"));
alipayCallback.setBody(params.get("body"));
alipayCallback.setGmtCreate(params.get("gmt_create"));
alipayCallback.setGmtPayment(params.get("gmt_payment"));
alipayCallback.setGmtRefund(params.get("gmt_refund"));
alipayCallback.setGmtClose(params.get("gmt_close"));
alipayCallback.setFundBillList(params.get("fund_bill_list"));
alipayCallback.setPassbackParams(params.get("passback_params"));
alipayCallback.setVoucherDetailList(params.get("voucher_detail_list"));
alipayCallback.setProcessStatus(EProcessStatus.INIT.getValue());
return alipayCallback;
}
/**
* BigDecimal
*
* @param value
* @return
*/
private BigDecimal trans2BigDecimal(String value) {
if (StrUtil.isBlank(value)) {
return BigDecimal.ZERO;
}
return new BigDecimal(value);
}
}

View File

@ -188,6 +188,10 @@ public class Constants
public static final Integer SIZE_41 = 41;
public static final Integer SIZE_42 = 42;
public static final Integer SIZE_43 = 43;
/**
* ID
*/
public static final Long PLATFORM_INTERNAL_ACCOUNT_ID = 1L;

View File

@ -111,10 +111,12 @@ public class SecurityConfig
.authorizeHttpRequests((requests) -> {
permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll());
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
requests.antMatchers("/login", "/register", "/captchaImage", "/xkt/test/execute").permitAll()
requests.antMatchers("/login", "/register", "/captchaImage").permitAll()
// 静态资源,可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
//支付宝回调
.antMatchers("/rest/v1/alipay-notify").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
})

View File

@ -0,0 +1,23 @@
package com.ruoyi.xkt.dto.payment;
import com.ruoyi.xkt.domain.PaymentBill;
import com.ruoyi.xkt.domain.PaymentBillDetail;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* @author liangyq
* @date 2025-04-08 21:18
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PaymentBillInfo {
private PaymentBill paymentBill;
private List<PaymentBillDetail> paymentBillDetails;
}

View File

@ -0,0 +1,30 @@
package com.ruoyi.xkt.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author liangyq
* @date 2025-04-07 23:42
*/
@Getter
@AllArgsConstructor
public enum EProcessStatus {
INIT(1, "初始"),
PROCESSING(2, "处理中"),
SUCCESS(3, "处理成功"),
FAILURE(4, "处理失败");
private final Integer value;
private final String label;
public static EProcessStatus of(Integer value) {
for (EProcessStatus e : EProcessStatus.values()) {
if (e.getValue().equals(value)) {
return e;
}
}
return null;
}
}

View File

@ -1,5 +1,6 @@
package com.ruoyi.xkt.manager.impl;
import cn.hutool.core.util.NumberUtil;
import com.alibaba.fastjson2.JSON;
import com.alipay.api.AlipayApiException;
import com.alipay.api.AlipayClient;
@ -7,23 +8,29 @@ import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.request.AlipayTradePagePayRequest;
import com.alipay.api.request.AlipayTradeWapPayRequest;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.xkt.domain.AlipayCallback;
import com.ruoyi.xkt.dto.order.StoreOrderInfo;
import com.ruoyi.xkt.dto.payment.AlipayReqDTO;
import com.ruoyi.xkt.enums.EPayChannel;
import com.ruoyi.xkt.enums.EPayFrom;
import com.ruoyi.xkt.manager.PaymentManager;
import com.ruoyi.xkt.service.IAlipayCallbackService;
import com.ruoyi.xkt.service.IStoreOrderService;
import io.jsonwebtoken.lang.Assert;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
/**
* @author liangyq
* @date 2025-04-06 19:36
*/
@Slf4j
@Getter
@Component
public class AliPaymentMangerImpl implements PaymentManager {
/**
@ -39,7 +46,7 @@ public class AliPaymentMangerImpl implements PaymentManager {
/**
* ,https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
*/
@Value("${alipay.publicKey:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkiTTeQLIkyVHnYiNmRKEtsdlwKftUySN1SLkrhn6CgmHl5ovjPeYteweZEZmsf2kt6wRnaLkODVP7xUQiRVC2cu6StdJyvDzyiYI00u72PvSOvaWHcpzgKqTFpGiQseJQlHnI8U3ob4PxfJylBy8RDQHG9fZwNY1WOCsnSb3m2ufV1EQIjndzTq13yQE6jCz639rO8atlAG3PtJW/QRiGUzyGaOuKsS4HRzPbbpmVtsXoN76+x+WLWkeqlTBEu35X4Hdbkf1C36wp3b68sI5fVyLksF6elRv/It4aUzjXSXbO/Dx+zvMIN01FgwaFV6nLh++k3qlmo87p4I+hvsGiQIDAQAB}")
@Value("${alipay.alipayPublicKey:MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkiTTeQLIkyVHnYiNmRKEtsdlwKftUySN1SLkrhn6CgmHl5ovjPeYteweZEZmsf2kt6wRnaLkODVP7xUQiRVC2cu6StdJyvDzyiYI00u72PvSOvaWHcpzgKqTFpGiQseJQlHnI8U3ob4PxfJylBy8RDQHG9fZwNY1WOCsnSb3m2ufV1EQIjndzTq13yQE6jCz639rO8atlAG3PtJW/QRiGUzyGaOuKsS4HRzPbbpmVtsXoN76+x+WLWkeqlTBEu35X4Hdbkf1C36wp3b68sI5fVyLksF6elRv/It4aUzjXSXbO/Dx+zvMIN01FgwaFV6nLh++k3qlmo87p4I+hvsGiQIDAQAB}")
private String alipayPublicKey;
/**
*
@ -69,6 +76,8 @@ public class AliPaymentMangerImpl implements PaymentManager {
@Autowired
private IStoreOrderService storeOrderService;
@Autowired
private IAlipayCallbackService alipayCallbackService;
@Override
public EPayChannel channel() {
@ -117,4 +126,19 @@ public class AliPaymentMangerImpl implements PaymentManager {
}
}
public void processAlipayCallback(AlipayCallback alipayCallback) {
AlipayCallback info = alipayCallbackService.getByNotifyId(alipayCallback.getNotifyId());
if (info == null) {
//保存到数据库
info = alipayCallback;
alipayCallbackService.insertAlipayCallback(info);
}
if (info.getRefundFee() != null &&
!NumberUtil.equals(info.getRefundFee(), BigDecimal.ZERO)) {
//如果有退款金额,可能是部分退款的回调,这里不做处理
return;
}
//TODO
}
}

View File

@ -2,12 +2,31 @@ package com.ruoyi.xkt.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.xkt.domain.InternalAccount;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import java.util.Collection;
import java.util.List;
/**
* @author liangyq
* @date 2025-04-02 12:48
*/
@Repository
public interface InternalAccountMapper extends BaseMapper<InternalAccount> {
/**
*
*
* @param id
* @return
*/
InternalAccount getForUpdate(@Param("id") Long id);
/**
*
*
* @param ids
* @return
*/
List<InternalAccount> listForUpdate(@Param("ids") Collection<Long> ids);
}

View File

@ -0,0 +1,27 @@
package com.ruoyi.xkt.service;
import com.ruoyi.xkt.domain.AlipayCallback;
/**
* @author liangyq
* @date 2025-04-08 17:39
*/
public interface IAlipayCallbackService {
/**
* ID
*
* @param notifyId
* @return
*/
AlipayCallback getByNotifyId(String notifyId);
/**
*
*
* @param alipayCallback
* @return
*/
int insertAlipayCallback(AlipayCallback alipayCallback);
}

View File

@ -0,0 +1,30 @@
package com.ruoyi.xkt.service;
import com.ruoyi.xkt.domain.InternalAccount;
import java.util.Collection;
import java.util.List;
/**
* @author liangyq
* @date 2025-04-08 21:14
*/
public interface IInternalAccountService {
/**
*
*
* @param id
* @return
*/
InternalAccount getWithLock(Long id);
/**
*
*
* @param ids
* @return
*/
List<InternalAccount> listWithLock(Collection<Long> ids);
}

View File

@ -0,0 +1,18 @@
package com.ruoyi.xkt.service;
import com.ruoyi.xkt.dto.order.StoreOrderInfo;
import com.ruoyi.xkt.dto.payment.PaymentBillInfo;
/**
* @author liangyq
* @date 2025-04-08 21:14
*/
public interface IPaymentBillService {
/**
*
*
* @param orderInfo
* @return
*/
PaymentBillInfo createCollectionBillByOrder(StoreOrderInfo orderInfo);
}

View File

@ -1,5 +1,6 @@
package com.ruoyi.xkt.service;
import com.ruoyi.xkt.domain.StoreOrder;
import com.ruoyi.xkt.dto.order.StoreOrderAddDTO;
import com.ruoyi.xkt.dto.order.StoreOrderInfo;
@ -16,6 +17,14 @@ public interface IStoreOrderService {
*/
StoreOrderInfo createOrder(StoreOrderAddDTO storeOrderAddDTO);
/**
*
*
* @param orderNo
* @return
*/
StoreOrder getByOrderNo(String orderNo);
/**
*
*

View File

@ -0,0 +1,36 @@
package com.ruoyi.xkt.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.xkt.domain.AlipayCallback;
import com.ruoyi.xkt.mapper.AlipayCallbackMapper;
import com.ruoyi.xkt.service.IAlipayCallbackService;
import io.jsonwebtoken.lang.Assert;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author liangyq
* @date 2025-04-08 17:40
*/
@Service
public class AlipayCallbackServiceImpl implements IAlipayCallbackService {
@Autowired
private AlipayCallbackMapper alipayCallbackMapper;
@Override
public AlipayCallback getByNotifyId(String notifyId) {
Assert.notNull(notifyId);
return alipayCallbackMapper.selectOne(Wrappers.lambdaQuery(AlipayCallback.class)
.eq(AlipayCallback::getNotifyId, notifyId));
}
@Transactional
@Override
public int insertAlipayCallback(AlipayCallback alipayCallback) {
Assert.notNull(alipayCallback);
return alipayCallbackMapper.insert(alipayCallback);
}
}

View File

@ -0,0 +1,37 @@
package com.ruoyi.xkt.service.impl;
import com.ruoyi.xkt.domain.InternalAccount;
import com.ruoyi.xkt.mapper.InternalAccountMapper;
import com.ruoyi.xkt.mapper.InternalAccountTransDetailMapper;
import com.ruoyi.xkt.service.IInternalAccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
import java.util.List;
/**
* @author liangyq
* @date 2025-04-08 21:14
*/
@Service
public class InternalAccountServiceImpl implements IInternalAccountService {
@Autowired
private InternalAccountMapper internalAccountMapper;
@Autowired
private InternalAccountTransDetailMapper internalAccountTransDetailMapper;
@Transactional
@Override
public InternalAccount getWithLock(Long id) {
return internalAccountMapper.getForUpdate(id);
}
@Transactional
@Override
public List<InternalAccount> listWithLock(Collection<Long> ids) {
return internalAccountMapper.listForUpdate(ids);
}
}

View File

@ -0,0 +1,35 @@
package com.ruoyi.xkt.service.impl;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.xkt.domain.InternalAccount;
import com.ruoyi.xkt.dto.order.StoreOrderInfo;
import com.ruoyi.xkt.dto.payment.PaymentBillInfo;
import com.ruoyi.xkt.mapper.PaymentBillDetailMapper;
import com.ruoyi.xkt.mapper.PaymentBillMapper;
import com.ruoyi.xkt.service.IInternalAccountService;
import com.ruoyi.xkt.service.IPaymentBillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @author liangyq
* @date 2025-04-08 21:14
*/
@Service
public class PaymentBillServiceImpl implements IPaymentBillService {
@Autowired
private PaymentBillMapper paymentBillMapper;
@Autowired
private PaymentBillDetailMapper paymentBillDetailMapper;
@Autowired
private IInternalAccountService internalAccountService;
@Transactional
@Override
public PaymentBillInfo createCollectionBillByOrder(StoreOrderInfo orderInfo) {
InternalAccount ca = internalAccountService.getWithLock(Constants.PLATFORM_INTERNAL_ACCOUNT_ID);
return null;
}
}

View File

@ -163,6 +163,13 @@ public class StoreOrderServiceImpl implements IStoreOrderService {
return new StoreOrderInfo(order, orderDetailList);
}
@Override
public StoreOrder getByOrderNo(String orderNo) {
Assert.notNull(orderNo);
return storeOrderMapper.selectOne(Wrappers.lambdaQuery(StoreOrder.class)
.eq(StoreOrder::getOrderNo, orderNo));
}
@Transactional
@Override
public StoreOrderInfo preparePayOrder(Long storeOrderId) {

View File

@ -2,4 +2,15 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.xkt.mapper.InternalAccountMapper">
<select id="getForUpdate" resultType="com.ruoyi.xkt.domain.InternalAccount">
SELECT *
FROM internal_account
WHERE id = #{id} FOR UPDATE
</select>
<select id="listForUpdate" resultType="com.ruoyi.xkt.domain.InternalAccount">
SELECT *
FROM internal_account
WHERE id IN
<foreach collection="ids" item="id" separator="," open="(" close=")">#{id}</foreach> FOR UPDATE
</select>
</mapper>