diff --git a/client-api/src/main/java/org/jiayunet/pojo/dto/memberProduct/CreateOrderDto.java b/client-api/src/main/java/org/jiayunet/pojo/dto/memberProduct/CreateOrderDto.java index 0dfd6b3..69f89f1 100644 --- a/client-api/src/main/java/org/jiayunet/pojo/dto/memberProduct/CreateOrderDto.java +++ b/client-api/src/main/java/org/jiayunet/pojo/dto/memberProduct/CreateOrderDto.java @@ -13,6 +13,9 @@ public class CreateOrderDto { /** 订单ID,用于轮询状态 */ private Long orderId; - /** 微信支付二维码链接 */ - private String codeUrl; + /** 支付渠道 1=微信 2=支付宝 */ + private Integer payChannel; + + /** 支付数据,微信=二维码URL,支付宝=HTML表单,前端根据payChannel判断渲染方式 */ + private String payData; } diff --git a/client-api/src/main/java/org/jiayunet/service/AlipayNotifyMessageAbstractImpl.java b/client-api/src/main/java/org/jiayunet/service/AlipayNotifyMessageAbstractImpl.java new file mode 100644 index 0000000..1cbafcf --- /dev/null +++ b/client-api/src/main/java/org/jiayunet/service/AlipayNotifyMessageAbstractImpl.java @@ -0,0 +1,77 @@ +package org.jiayunet.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jiayunet.alipay.AlipayNotifyMessageAbstract; +import org.jiayunet.mapper.PayAlipayFlowMapper; +import org.jiayunet.pojo.po.PayAlipayFlow; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Instant; +import java.util.Map; + +/** + * 支付宝支付回调实现 + *

依赖:MemberProductService(会员订单处理)、PayAlipayFlowMapper(更新支付流水)

+ *

使用表:bg_pay_alipay_flow(更新流水状态)、bg_member_order(通过MemberProductService处理)

+ * + * @author zk + */ +@Service +@Primary +@Slf4j +@AllArgsConstructor +public class AlipayNotifyMessageAbstractImpl implements AlipayNotifyMessageAbstract { + + private final MemberProductService memberProductService; + private final PayAlipayFlowMapper payAlipayFlowMapper; + + /** + * 支付回调 + *

1. 判断流水状态防重复处理 2. 更新支付宝流水 3. 调用公共订单处理逻辑

+ */ + @Override + @Transactional(rollbackFor = Exception.class) + public void payMessageHandle(Map params) { + String outTradeNo = params.get("out_trade_no"); + String tradeNo = params.get("trade_no"); + String tradeStatus = params.get("trade_status"); + + log.info("支付宝回调 outTradeNo:{} tradeNo:{} tradeStatus:{}", outTradeNo, tradeNo, tradeStatus); + + // 只处理支付成功 + if (!"TRADE_SUCCESS".equals(tradeStatus) && !"TRADE_FINISHED".equals(tradeStatus)) { + log.info("支付宝回调非成功状态,跳过 outTradeNo:{} tradeStatus:{}", outTradeNo, tradeStatus); + return; + } + + // 1. 查流水,已处理则直接返回(防重复回调) + PayAlipayFlow flow = payAlipayFlowMapper.selectOne( + new LambdaQueryWrapper().eq(PayAlipayFlow::getOrderNo, outTradeNo) + ); + if (flow == null) { + log.warn("支付宝回调流水不存在 outTradeNo:{}", outTradeNo); + return; + } + if (flow.getStatus() != 0) { + log.info("支付宝流水已处理,跳过 outTradeNo:{} status:{}", outTradeNo, flow.getStatus()); + return; + } + + // 2. 更新支付宝流水 + payAlipayFlowMapper.update(null, new LambdaUpdateWrapper() + .eq(PayAlipayFlow::getId, flow.getId()) + .eq(PayAlipayFlow::getStatus, 0) + .set(PayAlipayFlow::getStatus, 1) + .set(PayAlipayFlow::getTradeNo, tradeNo) + .set(PayAlipayFlow::getSuccessTime, Instant.now()) + .set(PayAlipayFlow::getNotifyData, params.toString())); + + // 3. 调用公共订单处理(更新订单状态 + 发放权限) + memberProductService.handlePaySuccess(outTradeNo); + } +} diff --git a/client-api/src/main/java/org/jiayunet/service/MemberProductService.java b/client-api/src/main/java/org/jiayunet/service/MemberProductService.java index 14ae528..229cba1 100644 --- a/client-api/src/main/java/org/jiayunet/service/MemberProductService.java +++ b/client-api/src/main/java/org/jiayunet/service/MemberProductService.java @@ -13,6 +13,7 @@ import org.jiayunet.pojo.dto.memberProduct.OrderDetailDto; import org.jiayunet.pojo.po.*; import org.jiayunet.tool.UserSecurityTool; import org.jiayunet.wxPay.WxNativePayAbility; +import org.jiayunet.alipay.AlipayPagePayAbility; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -61,6 +62,12 @@ public class MemberProductService { @Autowired private WxNativePayAbility wxNativePayAbility; + @Autowired + private AlipayPagePayAbility alipayPagePayAbility; + + @Autowired + private PayAlipayFlowMapper payAlipayFlowMapper; + @Value("${wx_pay.merchant_id:}") private String merchantId; @@ -109,19 +116,19 @@ public class MemberProductService { .eq(MemberOrder::getId, order.getId()) .set(MemberOrder::getOrderNo, orderNo)); - // 4. 调支付渠道下单,拿二维码URL - String codeUrl; + // 4. 调支付渠道下单 + String payData; if (payChannel == 1) { - codeUrl = prepayWechat(orderNo, product); + payData = prepayWechat(orderNo, product); } else { - // TODO 支付宝当面付 - codeUrl = prepayAlipay(orderNo, product); + payData = prepayAlipay(orderNo, product); } // 5. 返回 CreateOrderDto dto = new CreateOrderDto(); dto.setOrderId(order.getId()); - dto.setCodeUrl(codeUrl); + dto.setPayChannel(payChannel); + dto.setPayData(payData); return dto; } @@ -218,11 +225,22 @@ public class MemberProductService { } /** - * 支付宝当面付下单 - * TODO 接入支付宝当面付SDK,返回二维码URL + * 支付宝电脑网站支付下单 */ private String prepayAlipay(String orderNo, MemberProduct product) { - throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR, "支付宝支付暂未开放"); + // 创建支付宝流水 + PayAlipayFlow flow = new PayAlipayFlow(); + flow.setOrderType("member"); + flow.setOrderNo(orderNo); + flow.setTotal(product.getPrice()); + flow.setStatus(0); + payAlipayFlowMapper.insert(flow); + + // 金额转元(分→元) + String totalAmount = String.format("%.2f", product.getPrice() / 100.0); + + // 调支付宝下单,返回HTML表单 + return alipayPagePayAbility.prepay(orderNo, totalAmount, product.getProductName()); } /** diff --git a/client-api/src/main/resources/application-local.yml b/client-api/src/main/resources/application-local.yml index 7c6d172..cf66511 100644 --- a/client-api/src/main/resources/application-local.yml +++ b/client-api/src/main/resources/application-local.yml @@ -33,6 +33,15 @@ wx_pay: #回调域名地址 notify_domain: ${API_NOTIFY_DOMAIN:http://127.0.0.1:8080/api/} +# 支付宝支付 +alipay: + # status close:关闭 open:打开 + status: close + app_id: ${ALIPAY_APP_ID:your_app_id} + private_key: ${ALIPAY_PRIVATE_KEY:your_private_key} + alipay_public_key: ${ALIPAY_PUBLIC_KEY:your_alipay_public_key} + notify_domain: ${API_NOTIFY_DOMAIN:http://127.0.0.1:8080/api/} + app: # 加密秘钥配置 secret: diff --git a/common/pom.xml b/common/pom.xml index 7579879..e4d1821 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -63,6 +63,11 @@ wechatpay-java 0.2.15 + + com.alipay.sdk + alipay-sdk-java + 4.40.791.ALL + com.aliyun dysmsapi20170525 diff --git a/common/src/main/java/org/jiayunet/alipay/AlipayClientConfig.java b/common/src/main/java/org/jiayunet/alipay/AlipayClientConfig.java new file mode 100644 index 0000000..d89886d --- /dev/null +++ b/common/src/main/java/org/jiayunet/alipay/AlipayClientConfig.java @@ -0,0 +1,41 @@ +package org.jiayunet.alipay; + +import com.alipay.api.AlipayClient; +import com.alipay.api.AlipayConfig; +import com.alipay.api.DefaultAlipayClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 支付宝客户端配置 + * + * @author zk + */ +@Configuration +@ConditionalOnProperty(name = "alipay.status", havingValue = "open") +public class AlipayClientConfig { + + @Value("${alipay.app_id}") + private String appId; + + @Value("${alipay.private_key}") + private String privateKey; + + @Value("${alipay.alipay_public_key}") + private String alipayPublicKey; + + @Bean + public AlipayClient alipayClient() throws Exception { + AlipayConfig config = new AlipayConfig(); + config.setServerUrl("https://openapi.alipay.com/gateway.do"); + config.setAppId(appId); + config.setPrivateKey(privateKey); + config.setFormat("json"); + config.setAlipayPublicKey(alipayPublicKey); + config.setCharset("UTF-8"); + config.setSignType("RSA2"); + return new DefaultAlipayClient(config); + } +} diff --git a/common/src/main/java/org/jiayunet/alipay/AlipayNotifyController.java b/common/src/main/java/org/jiayunet/alipay/AlipayNotifyController.java new file mode 100644 index 0000000..31167d0 --- /dev/null +++ b/common/src/main/java/org/jiayunet/alipay/AlipayNotifyController.java @@ -0,0 +1,74 @@ +package org.jiayunet.alipay; + +import com.alipay.api.AlipayApiException; +import com.alipay.api.internal.util.AlipaySignature; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +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.util.HashMap; +import java.util.Map; + +/** + * 支付宝支付回调入口 + * + * @author zk + */ +@RestController +@ConditionalOnProperty(name = "alipay.status", havingValue = "open") +@RequestMapping("/public/alipayNotify") +@Slf4j +public class AlipayNotifyController { + + @Value("${alipay.alipay_public_key}") + private String alipayPublicKey; + + @Autowired + private AlipayNotifyMessageAbstract alipayNotifyMessageAbstract; + + /** + * 支付结果回调 + */ + @PostMapping("/pay") + public String pay(HttpServletRequest request) { + // 1. 获取所有回调参数 + Map params = new HashMap<>(); + Map requestParams = request.getParameterMap(); + for (String name : requestParams.keySet()) { + String[] values = requestParams.get(name); + String valueStr = String.join(",", values); + params.put(name, valueStr); + } + + log.info("收到支付宝支付回调数据:{}", params); + + // 2. SDK验签 + boolean signVerified; + try { + signVerified = AlipaySignature.rsaCheckV1(params, alipayPublicKey, "UTF-8", "RSA2"); + } catch (AlipayApiException e) { + log.error("支付宝回调验签异常", e); + return "failure"; + } + + if (!signVerified) { + log.error("支付宝回调验签失败 params:{}", params); + return "failure"; + } + + // 3. 验签通过,处理业务 + try { + alipayNotifyMessageAbstract.payMessageHandle(params); + } catch (Exception e) { + log.error("支付宝回调业务处理异常", e); + return "failure"; + } + + return "success"; + } +} diff --git a/common/src/main/java/org/jiayunet/alipay/AlipayNotifyMessageAbstract.java b/common/src/main/java/org/jiayunet/alipay/AlipayNotifyMessageAbstract.java new file mode 100644 index 0000000..d58d5dc --- /dev/null +++ b/common/src/main/java/org/jiayunet/alipay/AlipayNotifyMessageAbstract.java @@ -0,0 +1,21 @@ +package org.jiayunet.alipay; + +import java.util.Map; + +/** + * 支付宝支付回调消息处理接口 + * 使用前需要实现接口 + * + * @author zk + */ +public interface AlipayNotifyMessageAbstract { + + /** + * 支付结果回调消息处理 + * + * @param params 回调参数(已验签) + */ + default void payMessageHandle(Map params) { + System.out.println("支付宝支付的支付结果回调消息未处理"); + } +} diff --git a/common/src/main/java/org/jiayunet/alipay/AlipayPagePayAbility.java b/common/src/main/java/org/jiayunet/alipay/AlipayPagePayAbility.java new file mode 100644 index 0000000..2216ed5 --- /dev/null +++ b/common/src/main/java/org/jiayunet/alipay/AlipayPagePayAbility.java @@ -0,0 +1,61 @@ +package org.jiayunet.alipay; + +import com.alipay.api.AlipayApiException; +import com.alipay.api.AlipayClient; +import com.alipay.api.domain.AlipayTradePagePayModel; +import com.alipay.api.request.AlipayTradePagePayRequest; +import com.alipay.api.response.AlipayTradePagePayResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +/** + * 支付宝电脑网站支付能力 + * + * @author zk + */ +@ConditionalOnProperty(name = "alipay.status", havingValue = "open") +@Component +@Slf4j +public class AlipayPagePayAbility { + + @Autowired + private AlipayClient alipayClient; + + @Value("${alipay.notify_domain}") + private String notifyDomain; + + /** + * 电脑网站支付下单 + * + * @param outTradeNo 商户订单号 + * @param totalAmount 金额(元),如 "39.00" + * @param subject 订单标题 + * @return HTML表单字符串,前端直接渲染(内嵌二维码) + */ + public String prepay(String outTradeNo, String totalAmount, String subject) { + AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); + request.setNotifyUrl(notifyDomain + "/public/alipayNotify/pay"); + log.info("以默认设置支付宝回调地址为:{}", request.getNotifyUrl()); + + AlipayTradePagePayModel model = new AlipayTradePagePayModel(); + model.setOutTradeNo(outTradeNo); + model.setTotalAmount(totalAmount); + model.setSubject(subject); + model.setProductCode("FAST_INSTANT_TRADE_PAY"); + model.setQrPayMode("4"); + model.setQrcodeWidth(200L); + + request.setBizModel(model); + + try { + AlipayTradePagePayResponse response = alipayClient.pageExecute(request, "POST"); + return response.getBody(); + } catch (AlipayApiException e) { + log.error("支付宝下单异常 outTradeNo:{} error:{}", outTradeNo, e.getMessage()); + } + throw new RuntimeException("支付宝下单异常"); + } +} diff --git a/manager/src/main/java/org/jiayunet/mapper/PayAlipayFlowMapper.java b/manager/src/main/java/org/jiayunet/mapper/PayAlipayFlowMapper.java new file mode 100644 index 0000000..e15d4df --- /dev/null +++ b/manager/src/main/java/org/jiayunet/mapper/PayAlipayFlowMapper.java @@ -0,0 +1,14 @@ +package org.jiayunet.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.jiayunet.pojo.po.PayAlipayFlow; + +/** + * 支付宝支付流水 Mapper + * + * @author zk + */ +@Mapper +public interface PayAlipayFlowMapper extends CommonMapper { + +} diff --git a/manager/src/main/java/org/jiayunet/pojo/po/PayAlipayFlow.java b/manager/src/main/java/org/jiayunet/pojo/po/PayAlipayFlow.java new file mode 100644 index 0000000..acc2af6 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/pojo/po/PayAlipayFlow.java @@ -0,0 +1,49 @@ +package org.jiayunet.pojo.po; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.Instant; + +/** + * 支付宝支付流水表(bg_pay_alipay_flow) + *

记录支付宝支付的下单和回调信息

+ * + * @author zk + */ +@Data +@TableName(value = "bg_pay_alipay_flow") +public class PayAlipayFlow { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + + /** 订单类型,member=会员订单,后续可扩展其他业务类型 */ + private String orderType; + + /** 商户订单号,对应支付宝 out_trade_no,关联业务订单表 */ + private String orderNo; + + /** 支付宝交易号,支付成功后由回调返回 */ + private String tradeNo; + + /** 订单金额(分) */ + private Integer total; + + /** 流水状态 0=待支付 1=已支付 2=已关闭 */ + private Integer status; + + /** 支付成功时间,系统收到成功回调的时间 */ + private Instant successTime; + + /** 回调原始参数完整数据,用于问题排查 */ + private String notifyData; + + /** 记录创建时间(下单时写入) */ + private Instant createTime; + + /** 记录更新时间(回调时更新) */ + private Instant updateTime; +}