Commit b95690ff46d7fd4f5063edad5a84b0b8813d8dff

Authored by graypxl
1 parent bff17062

feat(pay): 支持支付宝小程序支付

sql/mysql/ruoyi-vue-pro.sql
... ... @@ -547,6 +547,7 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
547 547 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (71, 11, '支付宝 Wap 网站支付', 'alipay_wap', 'pay_channel_code', 0, 'primary', '', '支付宝 Wap 网站支付', '1', '2021-12-03 10:42:26', '1', '2026-08-14 01:49:42', b'0');
548 548 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (72, 12, '支付宝 App 支付', 'alipay_app', 'pay_channel_code', 0, 'primary', '', '支付宝 App 支付', '1', '2021-12-03 10:42:55', '1', '2026-08-14 01:49:42', b'0');
549 549 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (73, 14, '支付宝扫码支付', 'alipay_qr', 'pay_channel_code', 0, 'primary', '', '支付宝扫码支付', '1', '2021-12-03 10:43:10', '1', '2026-08-14 01:49:42', b'0');
  550 +INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1292, 15, '支付宝小程序支付', 'alipay_lite', 'pay_channel_code', 0, 'primary', '', '支付宝小程序支付', '1', '2026-08-18 00:00:00', '1', '2026-08-18 00:00:00', b'0');
550 551 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (74, 10, '通知成功', '10', 'pay_notify_status', 0, 'success', '', '通知成功', '1', '2021-12-03 11:02:41', '1', '2026-08-14 01:49:42', b'0');
551 552 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (75, 20, '通知失败', '20', 'pay_notify_status', 0, 'danger', '', '通知失败', '1', '2021-12-03 11:02:59', '1', '2026-08-14 01:49:42', b'0');
552 553 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (76, 0, '等待通知', '0', 'pay_notify_status', 0, 'info', '', '未通知', '1', '2021-12-03 11:03:10', '1', '2026-08-14 01:49:42', b'0');
... ...
sql/postgresql/ruoyi-vue-pro.sql
... ... @@ -817,6 +817,7 @@ INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_t
817 817 INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (117, 11, '支付宝 Wap 网站支付', 'alipay_wap', 'pay_channel_code', 0, 'primary', '', '支付宝 Wap 网站支付', '1', '2021-12-03 10:42:26', '1', '2023-07-19 20:09:16', '0');
818 818 INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (118, 12, '支付宝 App 支付', 'alipay_app', 'pay_channel_code', 0, 'primary', '', '支付宝 App 支付', '1', '2021-12-03 10:42:55', '1', '2023-07-19 20:09:20', '0');
819 819 INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (119, 14, '支付宝扫码支付', 'alipay_qr', 'pay_channel_code', 0, 'primary', '', '支付宝扫码支付', '1', '2021-12-03 10:43:10', '1', '2023-07-19 20:09:28', '0');
  820 +INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (3449, 15, '支付宝小程序支付', 'alipay_lite', 'pay_channel_code', 0, 'primary', '', '支付宝小程序支付', '1', '2026-08-18 00:00:00', '1', '2026-08-18 00:00:00', '0');
820 821 INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (120, 10, '通知成功', '10', 'pay_notify_status', 0, 'success', '', '通知成功', '1', '2021-12-03 11:02:41', '1', '2023-07-19 10:08:19', '0');
821 822 INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (121, 20, '通知失败', '20', 'pay_notify_status', 0, 'danger', '', '通知失败', '1', '2021-12-03 11:02:59', '1', '2023-07-19 10:08:21', '0');
822 823 INSERT INTO system_dict_data (id, sort, label, value, dict_type, status, color_type, css_class, remark, creator, create_time, updater, update_time, deleted) VALUES (122, 0, '等待通知', '0', 'pay_notify_status', 0, 'info', '', '未通知', '1', '2021-12-03 11:03:10', '1', '2023-07-19 10:08:24', '0');
... ...
yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/enums/PayChannelEnum.java
... ... @@ -29,6 +29,7 @@ public enum PayChannelEnum implements ArrayValuable<String> {
29 29 ALIPAY_APP("alipay_app", "支付宝App 支付"),
30 30 ALIPAY_QR("alipay_qr", "支付宝扫码支付"),
31 31 ALIPAY_BAR("alipay_bar", "支付宝条码支付"),
  32 + ALIPAY_LITE("alipay_lite", "支付宝小程序支付"),
32 33  
33 34 MOCK("mock", "模拟支付"),
34 35  
... ...
yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/PayClientFactoryImpl.java
... ... @@ -54,6 +54,7 @@ public class PayClientFactoryImpl implements PayClientFactory {
54 54 clientClass.put(ALIPAY_APP, AlipayAppPayClient.class);
55 55 clientClass.put(ALIPAY_PC, AlipayPcPayClient.class);
56 56 clientClass.put(ALIPAY_BAR, AlipayBarPayClient.class);
  57 + clientClass.put(ALIPAY_LITE, AlipayLitePayClient.class);
57 58 // 钱包支付客户端
58 59 clientClass.put(WALLET, WalletPayClient.class);
59 60 // Mock 支付客户端
... ...
yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AlipayLitePayClient.java 0 → 100644
  1 +package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay;
  2 +
  3 +import cn.hutool.core.map.MapUtil;
  4 +import cn.hutool.core.util.StrUtil;
  5 +import cn.iocoder.yudao.module.pay.enums.PayChannelEnum;
  6 +import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
  7 +import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
  8 +import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
  9 +import com.alipay.api.AlipayApiException;
  10 +import com.alipay.api.domain.AlipayTradeCreateModel;
  11 +import com.alipay.api.request.AlipayTradeCreateRequest;
  12 +import com.alipay.api.response.AlipayTradeCreateResponse;
  13 +
  14 +import java.util.Objects;
  15 +
  16 +import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.invalidParamException;
  17 +import static cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE;
  18 +
  19 +/**
  20 + * 支付宝【小程序】的 PayClient 实现类
  21 + *
  22 + * 文档:<a href="https://opendocs.alipay.com/mini/6039ed0c_alipay.trade.create">统一收单交易创建</a>
  23 + */
  24 +public class AlipayLitePayClient extends AbstractAlipayPayClient {
  25 +
  26 + public static final String BUYER_ID_KEY = "buyer_id";
  27 + public static final String BUYER_OPEN_ID_KEY = "buyer_open_id";
  28 +
  29 + public AlipayLitePayClient(Long channelId, AlipayPayClientConfig config) {
  30 + super(channelId, PayChannelEnum.ALIPAY_LITE.getCode(), config);
  31 + }
  32 +
  33 + @Override
  34 + public PayOrderRespDTO doUnifiedOrder(PayOrderUnifiedReqDTO reqDTO) throws AlipayApiException {
  35 + AlipayTradeCreateModel model = new AlipayTradeCreateModel();
  36 + model.setOutTradeNo(reqDTO.getOutTradeNo());
  37 + model.setSubject(reqDTO.getSubject());
  38 + model.setBody(reqDTO.getBody());
  39 + model.setTotalAmount(formatAmount(reqDTO.getPrice()));
  40 + model.setTimeExpire(formatTime(reqDTO.getExpireTime()));
  41 + model.setProductCode("JSAPI_PAY");
  42 + model.setOpAppId(config.getAppId());
  43 + applyBuyerIdentity(model, reqDTO);
  44 +
  45 + AlipayTradeCreateRequest request = new AlipayTradeCreateRequest();
  46 + request.setBizModel(model);
  47 + request.setNotifyUrl(reqDTO.getNotifyUrl());
  48 +
  49 + AlipayTradeCreateResponse response;
  50 + if (Objects.equals(config.getMode(), MODE_CERTIFICATE)) {
  51 + response = client.certificateExecute(request);
  52 + } else {
  53 + response = client.execute(request);
  54 + }
  55 + if (!response.isSuccess()) {
  56 + return buildClosedPayOrderRespDTO(reqDTO, response);
  57 + }
  58 + return PayOrderRespDTO.waitingOf(PayOrderDisplayModeEnum.APP.getMode(), response.getTradeNo(),
  59 + reqDTO.getOutTradeNo(), response);
  60 + }
  61 +
  62 + static void applyBuyerIdentity(AlipayTradeCreateModel model, PayOrderUnifiedReqDTO reqDTO) {
  63 + String buyerId = MapUtil.getStr(reqDTO.getChannelExtras(), BUYER_ID_KEY);
  64 + String buyerOpenId = MapUtil.getStr(reqDTO.getChannelExtras(), BUYER_OPEN_ID_KEY);
  65 + if (StrUtil.isBlank(buyerOpenId)) {
  66 + buyerOpenId = MapUtil.getStr(reqDTO.getChannelExtras(), "openid");
  67 + }
  68 + if (isAlipayUserId(buyerId)) {
  69 + model.setBuyerId(buyerId);
  70 + } else if (StrUtil.isNotBlank(buyerOpenId)) {
  71 + model.setBuyerOpenId(buyerOpenId);
  72 + } else if (StrUtil.isNotBlank(buyerId)) {
  73 + model.setBuyerOpenId(buyerId);
  74 + } else {
  75 + throw invalidParamException("支付请求的 buyer_open_id 不能为空!");
  76 + }
  77 + }
  78 +
  79 + static boolean isAlipayUserId(String value) {
  80 + return StrUtil.isNotBlank(value) && value.startsWith("2088") && value.length() <= 28
  81 + && StrUtil.isNumeric(value);
  82 + }
  83 +
  84 +}
... ...
yudao-module-pay/src/test/java/cn/iocoder/yudao/module/pay/framework/pay/core/client/impl/alipay/AlipayLitePayClientTest.java 0 → 100644
  1 +package cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay;
  2 +
  3 +import cn.iocoder.yudao.framework.common.exception.ServiceException;
  4 +import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO;
  5 +import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO;
  6 +import cn.iocoder.yudao.module.pay.framework.pay.core.enums.PayOrderDisplayModeEnum;
  7 +import com.alipay.api.AlipayApiException;
  8 +import com.alipay.api.domain.AlipayTradeCreateModel;
  9 +import com.alipay.api.request.AlipayTradeCreateRequest;
  10 +import com.alipay.api.response.AlipayTradeCreateResponse;
  11 +import org.junit.jupiter.api.BeforeEach;
  12 +import org.junit.jupiter.api.DisplayName;
  13 +import org.junit.jupiter.api.Test;
  14 +import org.mockito.ArgumentMatcher;
  15 +import org.mockito.InjectMocks;
  16 +
  17 +import java.util.HashMap;
  18 +import java.util.Map;
  19 +
  20 +import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
  21 +import static cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum.CLOSED;
  22 +import static cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum.WAITING;
  23 +import static cn.iocoder.yudao.module.pay.framework.pay.core.client.impl.alipay.AlipayPayClientConfig.MODE_CERTIFICATE;
  24 +import static org.junit.jupiter.api.Assertions.*;
  25 +import static org.mockito.ArgumentMatchers.argThat;
  26 +import static org.mockito.Mockito.when;
  27 +
  28 +/**
  29 + * {@link AlipayLitePayClient} 单元测试
  30 + */
  31 +public class AlipayLitePayClientTest extends AbstractAlipayClientTest {
  32 +
  33 + @InjectMocks
  34 + private AlipayLitePayClient client = new AlipayLitePayClient(randomLongId(), config);
  35 +
  36 + @BeforeEach
  37 + public void setUp() {
  38 + setClient(client);
  39 + }
  40 +
  41 + @Test
  42 + @DisplayName("支付宝小程序支付:buyer_open_id 下单成功")
  43 + public void testUnifiedOrder_buyerOpenId() throws AlipayApiException {
  44 + String buyerOpenId = "060d4dhFytu7VF5haNiDacG7b-n5toPeevhdLa07Kz9ass8";
  45 + String tradeNo = randomString();
  46 + AlipayTradeCreateResponse response = randomPojo(AlipayTradeCreateResponse.class, o -> {
  47 + o.setSubCode("");
  48 + o.setTradeNo(tradeNo);
  49 + });
  50 + when(defaultAlipayClient.execute(argThat(assertRequest(config.getAppId(), null, buyerOpenId))))
  51 + .thenReturn(response);
  52 + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), randomString(), randomInteger());
  53 + reqDTO.setChannelExtras(Map.of(AlipayLitePayClient.BUYER_OPEN_ID_KEY, buyerOpenId));
  54 +
  55 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
  56 +
  57 + assertEquals(WAITING.getStatus(), resp.getStatus());
  58 + assertEquals(PayOrderDisplayModeEnum.APP.getMode(), resp.getDisplayMode());
  59 + assertEquals(tradeNo, resp.getDisplayContent());
  60 + assertSame(response, resp.getRawData());
  61 + }
  62 +
  63 + @Test
  64 + @DisplayName("支付宝小程序支付:buyer_id 下单成功")
  65 + public void testUnifiedOrder_buyerId() throws AlipayApiException {
  66 + String buyerId = "2088123456789012";
  67 + AlipayTradeCreateResponse response = randomPojo(AlipayTradeCreateResponse.class, o -> o.setSubCode(""));
  68 + when(defaultAlipayClient.execute(argThat(assertRequest(config.getAppId(), buyerId, null))))
  69 + .thenReturn(response);
  70 + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), randomString(), randomInteger());
  71 + reqDTO.setChannelExtras(Map.of(AlipayLitePayClient.BUYER_ID_KEY, buyerId));
  72 +
  73 + assertEquals(WAITING.getStatus(), client.unifiedOrder(reqDTO).getStatus());
  74 + }
  75 +
  76 + @Test
  77 + @DisplayName("支付宝小程序支付:兼容 openid 参数")
  78 + public void testUnifiedOrder_openidAlias() throws AlipayApiException {
  79 + String buyerOpenId = randomString();
  80 + AlipayTradeCreateResponse response = randomPojo(AlipayTradeCreateResponse.class, o -> o.setSubCode(""));
  81 + when(defaultAlipayClient.execute(argThat(assertRequest(config.getAppId(), null, buyerOpenId))))
  82 + .thenReturn(response);
  83 + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), randomString(), randomInteger());
  84 + reqDTO.setChannelExtras(Map.of("openid", buyerOpenId));
  85 +
  86 + assertEquals(WAITING.getStatus(), client.unifiedOrder(reqDTO).getStatus());
  87 + }
  88 +
  89 + @Test
  90 + @DisplayName("支付宝小程序支付:证书模式下单成功")
  91 + public void testUnifiedOrder_certificateMode() throws AlipayApiException {
  92 + config.setMode(MODE_CERTIFICATE);
  93 + String buyerOpenId = randomString();
  94 + AlipayTradeCreateResponse response = randomPojo(AlipayTradeCreateResponse.class, o -> o.setSubCode(""));
  95 + when(defaultAlipayClient.certificateExecute(argThat(assertRequest(config.getAppId(), null, buyerOpenId))))
  96 + .thenReturn(response);
  97 + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), randomString(), randomInteger());
  98 + reqDTO.setChannelExtras(Map.of(AlipayLitePayClient.BUYER_OPEN_ID_KEY, buyerOpenId));
  99 +
  100 + assertEquals(WAITING.getStatus(), client.unifiedOrder(reqDTO).getStatus());
  101 + }
  102 +
  103 + @Test
  104 + @DisplayName("支付宝小程序支付:渠道返回失败")
  105 + public void testUnifiedOrder_channelFailed() throws AlipayApiException {
  106 + String subCode = randomString();
  107 + String subMsg = randomString();
  108 + AlipayTradeCreateResponse response = randomPojo(AlipayTradeCreateResponse.class, o -> {
  109 + o.setSubCode(subCode);
  110 + o.setSubMsg(subMsg);
  111 + });
  112 + when(defaultAlipayClient.execute(argThat((ArgumentMatcher<AlipayTradeCreateRequest>) request -> true)))
  113 + .thenReturn(response);
  114 + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), randomString(), randomInteger());
  115 + reqDTO.setChannelExtras(Map.of(AlipayLitePayClient.BUYER_OPEN_ID_KEY, randomString()));
  116 +
  117 + PayOrderRespDTO resp = client.unifiedOrder(reqDTO);
  118 +
  119 + assertEquals(CLOSED.getStatus(), resp.getStatus());
  120 + assertEquals(subCode, resp.getChannelErrorCode());
  121 + assertEquals(subMsg, resp.getChannelErrorMsg());
  122 + }
  123 +
  124 + @Test
  125 + @DisplayName("支付宝小程序支付:买家标识为空")
  126 + public void testUnifiedOrder_buyerEmpty() {
  127 + PayOrderUnifiedReqDTO reqDTO = buildOrderUnifiedReqDTO(randomURL(), randomString(), randomInteger());
  128 + reqDTO.setChannelExtras(new HashMap<>());
  129 +
  130 + assertThrows(ServiceException.class, () -> client.unifiedOrder(reqDTO));
  131 + }
  132 +
  133 + private static ArgumentMatcher<AlipayTradeCreateRequest> assertRequest(String appId, String buyerId,
  134 + String buyerOpenId) {
  135 + return request -> {
  136 + AlipayTradeCreateModel model = (AlipayTradeCreateModel) request.getBizModel();
  137 + assertEquals("JSAPI_PAY", model.getProductCode());
  138 + assertEquals(appId, model.getOpAppId());
  139 + assertEquals(buyerId, model.getBuyerId());
  140 + assertEquals(buyerOpenId, model.getBuyerOpenId());
  141 + return true;
  142 + };
  143 + }
  144 +
  145 +}
... ...