想对接支付,就需要申请对应的支付渠道。否则你只能模拟一个支付和支付成功,来走完自己的流程。
目前国内主要有微信支付和支付宝支付两种主流支付方式,但是微信支付不支持个体户,因此这里选择支付宝的沙箱支付。
其实支付流程基本如下图所示:
现在可以先去支付宝开发平台申请资质,https://open.alipay.com/develop/manage
然后直接下载手机版沙箱支付宝。
实现方案
以下是测试用例(java版):
public class PayTest {
// 「沙箱环境」应用ID - 您的APPID,收款账号既是你的APPID对应支付宝账号。获取地址;https://open.alipay.com/develop/sandbox/app
public static String app_id = "你的APPID";
// 「沙箱环境」商户私钥,你的PKCS8格式RSA2私钥
public static String app_private_key = "你的商户私钥"
// 「沙箱环境」支付宝公钥
public static String alipay_public_key = "你的支付宝公钥"
// 「沙箱环境」服务器异步通知页面路径。如果非公网地址建议使用 natapp 等内网穿透工具
public static String notify_url = "异步支付结果通知地址";
// 「沙箱环境」页面跳转同步通知页面路径 需http://格式的完整路径,必须外网可以正常访问,才会同步跳转
public static String return_url = "同步页面回调地址,可自动返回商户页面";
// 「沙箱环境」
public static String gatewayUrl = "支付宝网关地址";
// 签名方式
public static String sign_type = "RSA2";
// 字符编码格式
public static String charset = "utf-8";
@Test
public void test_AliPay() throws AlipayApiException {
AlipayClient alipayClient = new DefaultAlipayClient(gatewayUrl,
app_id,
app_private_key,
"json",
charset,
alipay_public_key,
sign_type);
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest(); // 发送请求的 Request类
request.setNotifyUrl(notify_url);
// request.setReturnUrl(return_url);
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", "daniel82AAAA000032333361X02"); // 我们自己生成的订单编号
bizContent.put("total_amount", "0.01"); // 订单的总金额
bizContent.put("subject", "测试商品"); // 支付的名称
bizContent.put("product_code", "FAST_INSTANT_TRADE_PAY"); // 固定配置
request.setBizContent(bizContent.toString());
String form = alipayClient.pageExecute(request).getBody();
log.info("测试结果:{}", form);
}
}
- 这是支付宝沙箱支付 SDK 支付宝扫码的调用案例,本身这个 SDK 支持了支付宝所有的支付方式,非常方便。
- 支付宝沙箱不影响实际环境,且容易申请,如果可能,可以申请微信支付、
- 案例运行成功后,会拿到一个html表单,这是一个动态表单,直接加载html就可以去往支付界面。
具体实现
创建订单
@RequestMapping(value = "create_pay_order", method = RequestMethod.POST)
public Response<String> createParOrder(@RequestHeader("Authorization") String token, @RequestParam Integer productId) {
try {
// 1. 校验token
boolean success = authService.checkToken(token);
if (!success) {
return Response.<String>builder()
.code(Constants.ResponseCode.TOKEN_ERROR.getCode())
.info(Constants.ResponseCode.TOKEN_ERROR.getInfo())
.build();
}
// 2. token解析
String openId = authService.getOpenId(token);
assert openId != null;
log.info("用户商品下单,根据商品ID创建支付单开始 openid:{} productId:{}", openId, productId);
ShopCartEntity shopCartEntity = ShopCartEntity.builder()
.openid(openId)
.productId(productId)
.build();
PayOrderEntity orderEntity = orderService.createOrder(shopCartEntity);
log.info("用户商品下单,根据商品ID创建支付单完成 openid: {} productId: {} orderPay: {}", openId, productId, orderEntity.toString());
return Response.<String>builder()
.code(Constants.ResponseCode.SUCCESS.getCode())
.info(Constants.ResponseCode.SUCCESS.getInfo())
.data(orderEntity.getPayUrl())
.build();
}catch (Exception e) {
log.error("用户商品下单,根据商品ID创建支付单失败", e);
return Response.<String>builder()
.code(Constants.ResponseCode.UN_ERROR.getCode())
.info(Constants.ResponseCode.UN_ERROR.getInfo())
.build();
}
}
public interface IOrderService {
/**
* 用户下单,通过购物车信息,返回下单后的支付单
*
* @param shopCartEntity 简单购物车
* @return 支付单实体对象
*/
PayOrderEntity createOrder(ShopCartEntity shopCartEntity);
//... 省略其他方法
}
@Override
public PayOrderEntity createOrder(ShopCartEntity shopCartEntity) throws Exception {
String openid = shopCartEntity.getOpenid();
// 1. 查询有效的未支付订单
UnpaidOrderEntity unpaidOrderEntity = orderRepository.queryUnpaidOrder(shopCartEntity);
if (unpaidOrderEntity != null && unpaidOrderEntity.getPayStatus().equals(PayStatusVO.WAIT)) {
log.info("创建订单-存在,已存在未支付订单,返回 openid: {} orderId: {} payUrl: {}", openid, unpaidOrderEntity.getOrderId(), unpaidOrderEntity.getPayUrl());
return PayOrderEntity.builder()
.openid(openid)
.payStatus(unpaidOrderEntity.getPayStatus())
.payUrl(unpaidOrderEntity.getPayUrl())
.orderId(unpaidOrderEntity.getOrderId())
.build();
}else if (unpaidOrderEntity != null && unpaidOrderEntity.getPayStatus().equals(PayStatusVO.CREATE)) {
log.info("创建订单-存在,存在未创建支付单订单,返回 openid: {} orderId: {}", openid, unpaidOrderEntity.getOrderId());
PayOrderEntity payOrderEntity = this.doPrepayOrder(openid, unpaidOrderEntity.getOrderId(), unpaidOrderEntity.getProductName(), unpaidOrderEntity.getTotalAmount());
log.info("创建订单-完成,生成支付单。openid: {} orderId: {} payUrl: {}", openid, payOrderEntity.getOrderId(), payOrderEntity.getPayUrl());
return payOrderEntity;
}
// 2. 查询商品
ProductEntity productEntity = orderRepository.queryProduct(shopCartEntity.getProductId());
// 如果所购商品已下线
if (!productEntity.isAvailable()) {
throw new ChatGPTException(Constants.ResponseCode.ORDER_PRODUCT_ERR.getCode(), Constants.ResponseCode.ORDER_PRODUCT_ERR.getInfo());
}
// 3. 保存订单
OrderEntity orderEntity = this.doSaveOrder(openid, productEntity);
// 4. 创建支付
PayOrderEntity payOrderEntity = this.doPrepayOrder(openid, orderEntity.getOrderId(), productEntity.getProductName(), orderEntity.getTotalAmount());
log.info("创建订单-完成,生成支付单。openid: {} orderId: {} payUrl: {}", openid, orderEntity.getOrderId(), payOrderEntity.getPayUrl());
return payOrderEntity;
}
- 创建订单前,要查询有效的未支付订单,如果存在直接返回支付宝支付界面。避免创建一堆的订单。
- 此外我们做流程分析时候知道,还有可能是订单存在,但无支付单。那么这个时候需要主动创建一条支付单,再返回。
- 如果确实需要创建新订单,则需要根据购物车商品ID,查询出对应的商品信息,创建并保存订单,最后再创建支付单更新到订单上。
支付回调
@PostMapping("pay_notify")
public void payNotify(HttpServletRequest request, HttpServletResponse response) throws IOException {
try {
log.info("支付回调,消息接收 {}", request.getParameter("trade_status"));
if (request.getParameter("trade_status").equals("TRADE_SUCCESS")) {
Map<String, String> params = new HashMap<>();
Map<String, String[]> 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);
}
String tradeNo = params.get("out_trade_no");
String gmtPayment = params.get("gmt_payment");
String alipayTradeNo = params.get("trade_no");
String sign = params.get("sign");
Double total_amount = Double.valueOf(params.get("total_amount"));
boolean checkSignature = AlipaySignature.rsaCheckV1(params, ALIPAY_PUBLIC_KEY, AlipayConfig.CHARSET, AlipayConfig.SIGNTYPE);
// 支付宝验签通过
if (checkSignature) {
// 验签通过
log.info("支付回调,交易名称: {}", params.get("subject"));
log.info("支付回调,交易状态: {}", params.get("trade_status"));
log.info("支付回调,支付宝交易凭证号: {}", params.get("trade_no"));
log.info("支付回调,商户订单号: {}", params.get("out_trade_no"));
log.info("支付回调,交易金额: {}", params.get("total_amount"));
log.info("支付回调,买家在支付宝唯一id: {}", params.get("buyer_id"));
log.info("支付回调,买家付款时间: {}", params.get("gmt_payment"));
log.info("支付回调,买家付款金额: {}", params.get("buyer_pay_amount"));
log.info("支付回调,支付回调,更新订单 {}", tradeNo);
// 更新订单为已支付
boolean isSuccess = orderService.changeOrderPaySuccess(tradeNo, alipayTradeNo, BigDecimal.valueOf(total_amount), dateFormat.parse(gmtPayment));
if (isSuccess) {
// 发布消息
// eventBus.post(tradeNo);
rabbitTemplate.convertAndSend(PayNotifyConfig.PAYNOTIFY_EXCHANGE_FANOUT,"", tradeNo);
}
response.getWriter().write("<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>");
}else {
response.getWriter().write("<xml><return_code><![CDATA[FAIL]]></return_code></xml>");
}
}
}catch (Exception e) {
log.error("支付失败", e);
response.getWriter().write("<xml><return_code><![CDATA[FAIL]]></return_code></xml>");
}
}
- 支付回调主要以支付宝支付的消息通知为主,更新订单并发送消息。这两步操作都是非常快的,不会让支付回调超时。
- 所以一般我们不是通过支付回调直接发货,因为发货流程更长,MQ消息解耦则是更好的方式。
- 注意 eventBus 是 Guava 的消息总线,类似于 MQ 消息。为了应对高并发已经改为了rabbitmq,如果业务简单可以继续用Guava
到这支付功能基本实现了,不过实际的商品下单支付场景肯定还会复杂,并且还得考虑异常,比如下单未支付,支付未发货,消息通知异常等等。小伙伴们可以思考哦。