添加支付宝支付能力
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 支付宝支付回调实现
|
||||
* <p>依赖:MemberProductService(会员订单处理)、PayAlipayFlowMapper(更新支付流水)</p>
|
||||
* <p>使用表:bg_pay_alipay_flow(更新流水状态)、bg_member_order(通过MemberProductService处理)</p>
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
@Service
|
||||
@Primary
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class AlipayNotifyMessageAbstractImpl implements AlipayNotifyMessageAbstract {
|
||||
|
||||
private final MemberProductService memberProductService;
|
||||
private final PayAlipayFlowMapper payAlipayFlowMapper;
|
||||
|
||||
/**
|
||||
* 支付回调
|
||||
* <p>1. 判断流水状态防重复处理 2. 更新支付宝流水 3. 调用公共订单处理逻辑</p>
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void payMessageHandle(Map<String, String> 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<PayAlipayFlow>().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<PayAlipayFlow>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -63,6 +63,11 @@
|
||||
<artifactId>wechatpay-java</artifactId>
|
||||
<version>0.2.15</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.alipay.sdk</groupId>
|
||||
<artifactId>alipay-sdk-java</artifactId>
|
||||
<version>4.40.791.ALL</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.aliyun</groupId>
|
||||
<artifactId>dysmsapi20170525</artifactId>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<String, String> params = new HashMap<>();
|
||||
Map<String, String[]> 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";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.jiayunet.alipay;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 支付宝支付回调消息处理接口
|
||||
* 使用前需要实现接口
|
||||
*
|
||||
* @author zk
|
||||
*/
|
||||
public interface AlipayNotifyMessageAbstract {
|
||||
|
||||
/**
|
||||
* 支付结果回调消息处理
|
||||
*
|
||||
* @param params 回调参数(已验签)
|
||||
*/
|
||||
default void payMessageHandle(Map<String, String> params) {
|
||||
System.out.println("支付宝支付的支付结果回调消息未处理");
|
||||
}
|
||||
}
|
||||
@@ -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("支付宝下单异常");
|
||||
}
|
||||
}
|
||||
@@ -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<PayAlipayFlow> {
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
* <p>记录支付宝支付的下单和回调信息</p>
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
Reference in New Issue
Block a user