支付宝开放平台 (alipay.com)
沙箱:
产品介绍 - 支付宝文档中心 (alipay.com):电脑网站支付的所有文档
下载密钥生成工具:密钥工具下载 - 支付宝文档中心 (alipay.com)
按照官方教程去生成自己的私钥。
配置properties,方便配置支付宝appid,私钥密钥等
@Data
@ConfigurationProperties(prefix = "app.pay.alipay")
public class AlipayProperties {
// 应用ID,您的APPID,收款账号既是您的APPID对应支付宝账号
private String app_id ;
// 商户私钥,您的PKCS8格式RSA2私钥
private String merchant_private_key;
// 支付宝公钥,查看地址:https://openhome.alipay.com/platform/keyManage.htm 对应APPID下的支付宝公钥。
private String alipay_public_key;
// 服务器异步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
private String notify_url ;
// 页面跳转同步通知页面路径 需http://格式的完整路径,不能加?id=123这类自定义参数,必须外网可以正常访问
private String return_url;
// 签名方式
private String sign_type;
// 字符编码格式
private String charset;
// 支付宝网关
private String gatewayUrl;
}
在config类中放一个AlipayClient。避免每次调用都需要专门new一个。
@EnableConfigurationProperties(AlipayProperties.class)
@Configuration
public class AlipayConfig {
@Bean
AlipayClient alipayClient(AlipayProperties alipayProperties){
return new DefaultAlipayClient( alipayProperties.getGatewayUrl(),
alipayProperties.getApp_id(),
alipayProperties.getMerchant_private_key(),
"json",
alipayProperties.getCharset(),
alipayProperties.getAlipay_public_key(),
alipayProperties.getSign_type());
}
}
在payService实现生成支付页的方法
@Override
public String generatePayPage(Long orderId, Long userId) throws AlipayApiException {
//创建一个AlipayClient
//创建一个支付请求
AlipayTradePagePayRequest alipayRequest = new AlipayTradePagePayRequest();
alipayRequest.setReturnUrl(alipayProperties.getReturn_url());//同步回调 支付成功后浏览器跳转到的地址
alipayRequest.setNotifyUrl(alipayProperties.getNotify_url());//通知回调 支付成功后通知的地址
//准备待支付的订单数据
//远程调用订单服务获取其基本信息 基于此数据生成订单页
OrderInfo orderInfo = orderFeignClient.getOrderInfoById(orderId).getData();
//商户订单号,商户网站订单系统中唯一订单号,必填
String outTradeNo = orderInfo.getOutTradeNo();
//付款金额,必填
BigDecimal totalAmount =orderInfo.getTotalAmount();
//订单名称,必填
String orderName = "尚品汇-订单-"+outTradeNo;
//商品描述,可空
String tradeBody = orderInfo.getTradeBody();
Map<String,Object> bizContent= new HashMap<>();
bizContent.put("out_trade_no",outTradeNo);
bizContent.put("total_amount",totalAmount);
bizContent.put("subject",orderName);
bizContent.put("body",tradeBody);
bizContent.put("product_code","FAST_INSTANT_TRADE_PAY");
alipayRequest.setBizContent(JSON.toJSONString(bizContent));
//请求
String page = alipayClient.pageExecute(alipayRequest).getBody();
return page;
}
需要一个方法去接受支付宝支付成功的回调。此处将其放入mq消息队列中。等待消费。修改订单状态
/**
* 支付成功后支付宝会给这里发送支付结果通知 异步
* @param params
* @return
*/
@PostMapping("/notify/success")
public String paySuccessNotify(@RequestParam Map<String,String> params) throws AlipayApiException {
log.info("收到支付宝支付消息通知:{}", JSON.toJSONString(params));
//验证签名
boolean signVerified = AlipaySignature.rsaCheckV1(params,
alipayProperties.getAlipay_public_key(),
alipayProperties.getCharset(),
alipayProperties.getSign_type());//调用SDK验证签名
if(signVerified){
log.info("验签通过,准备修改订单状态");
String trade_status = params.get("trade_status");
if("TRADE_SUCCESS".equals(trade_status)){
//修改订单状态 通过消息传递机制
mqService.send(params, MqConst.ORDER_EVENT_EXCHANGE,MqConst.ORDER_PAYED_RK);
}
}
//什么时候给支付宝返回success
return "success";
}
修改订单状态 如果用户在临关单前的极限时间支付后,为了避免用户订单被强制改为关单。我们需要设置较高的优先级。
@Override
public void payedOrder(String outTradeNo, Long userId) {
//关单消息和支付消息同时抵达的话,以支付为准,将其改为已支付
//订单是未支付或是已关闭都可以改为已支付
ProcessStatus payed = ProcessStatus.PAID;
//修改订单状态为已支付
boolean update = orderInfoService.lambdaUpdate()
.set(OrderInfo::getOrderStatus, payed.getOrderStatus().name())
.set(OrderInfo::getProcessStatus, payed.name())
.eq(OrderInfo::getUserId, userId)
.eq(OrderInfo::getOutTradeNo, outTradeNo)
.in(OrderInfo::getOrderStatus, OrderStatus.UNPAID.name(), OrderStatus.CLOSED.name())
.in(OrderInfo::getProcessStatus, ProcessStatus.UNPAID.name(), ProcessStatus.CLOSED.name())
.update();
log.info("修改订单:{} 状态为已支付成功:{}",outTradeNo,update);
}
修改状态后,将payment_info也存入数据
/**
* 监听所有成功单队列
*/
@RabbitListener(queues = MqConst.ORDER_PAYED_QUEUE)
public void listen(Message message, Channel channel) throws IOException {
long tag = message.getMessageProperties().getDeliveryTag();
String json = new String(message.getBody());
try {
Map<String, String> content = JSON.parseObject(json, new TypeReference<Map<String, String>>() {
});
log.info("修改订单状态为已支付");
//订单的唯一对外交易号
String out_trade_no = content.get("out_trade_no");
//知道用户id
String[] split = out_trade_no.split("-");
Long userId = Long.parseLong(split[split.length - 1]);
//根据唯一对外交易号和用户id修改
orderBizService.payedOrder(out_trade_no,userId);
PaymentInfo info = preparePaymentInfo(json, content, out_trade_no, userId);
paymentInfoService.save(info);
channel.basicAck(tag,false);
} catch (NumberFormatException | IOException e) {
mqService.retry(channel,tag,json,5);
}
}
private PaymentInfo preparePaymentInfo(String json, Map<String, String> content, String out_trade_no, Long userId) {
//保存此次支付的回调信息到payment_info里
PaymentInfo info = new PaymentInfo();
//查询orderInfo
OrderInfo orderInfo = orderInfoService.lambdaQuery()
.eq(OrderInfo::getOutTradeNo, out_trade_no)
.eq(OrderInfo::getUserId, userId).one();
info.setOutTradeNo(out_trade_no);
info.setUserId(userId);
info.setOrderId(orderInfo.getId());
info.setPaymentType(orderInfo.getPaymentWay());
//支付宝给的流水号
String trade_no = content.get("trade_no");
info.setTradeNo(trade_no);
String total_amount = content.get("total_amount");
info.setTotalAmount(new BigDecimal(total_amount));
info.setSubject(content.get("subject"));
info.setPaymentStatus(content.get("trade_status"));
info.setCreateTime(new Date());
info.setCallbackTime(new Date());
info.setCallbackContent(json);
return info;
}
C扫B需求要求可以手机网页交互,微信JSAPI/NATIVE支付符合需求。
总结步骤:
参考文档:https://pay.weixin.qq.com/wiki/doc/api/index.html
支付产品: https://pay.weixin.qq.com/static/product/product_index.shtml
退款:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=9_4
微信支付目前有两个大版本是公司使用的分别是V2 和 V3 版本,旧项目-V2 新项目-V3
V3新版本:https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay-1.shtml
官方SDK与DEMO代码:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=11_1
开发流程:
注意:demo用的是微信V2版本,实际项目用的是微信V3版本
NATIVE代码实现:
(1)添加依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.github.tedzhdzgroupId>
<artifactId>wxpay-sdkartifactId>
<version>3.0.10version>
dependency>
(2)编写配置类
package com.itheima.pay.config;
import com.github.wxpay.sdk.IWXPayDomain;
import com.github.wxpay.sdk.WXPayConfig;
import com.github.wxpay.sdk.WXPayConstants;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
public class WXPayConfigCustom extends WXPayConfig {
/**
* 开发者ID(AppID)
* @return
*/
@Override
protected String getAppID() {
return "wx0ca99a203b1e9943";
}
/**
* 商户号
* @return
*/
@Override
protected String getMchID() {
return "1561414331";
}
/**
* appkey API密钥
* @return
*/
@Override
protected String getKey() {
return "CZBK51236435wxpay435434323FFDuis";
}
// 退款:必须强制使用API证书
@Override
protected InputStream getCertStream() {
try {
String path = ClassLoader.getSystemResource("").getPath();
return new FileInputStream(new File(path+"apiclient_cert.p12"));
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return null;
}
@Override
protected IWXPayDomain getWXPayDomain() {
return new IWXPayDomain() {
@Override
public void report(String s, long l, Exception e) {
}
@Override
public DomainInfo getDomain(WXPayConfig wxPayConfig) {
return new DomainInfo(WXPayConstants.DOMAIN_API, true);
}
};
}
}
注意:
(3)编写下单测试方法
package com.itheima.pay.controller;
import com.github.binarywang.wxpay.bean.notify.WxPayNotifyResponse;
import com.github.wxpay.sdk.WXPay;
import com.github.wxpay.sdk.WXPayUtil;
import com.itheima.pay.config.WXPayConfigCustom;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* @Description:
* @Version: V1.0
*/
@Slf4j
@RestController
@RequestMapping("wxpay")
public class WxpayController {
/**
* 支付回调通知
* @param request
* @param response
* @return
*/
@RequestMapping("notify")
public String payNotify(HttpServletRequest request, HttpServletResponse response) {
try {
String xmlResult = IOUtils.toString(request.getInputStream(), request.getCharacterEncoding());
Map<String, String> map = WXPayUtil.xmlToMap(xmlResult);
// 加入自己处理订单的业务逻辑,需要判断订单是否已经支付过,否则可能会重复调用
String orderId = map.get("out_trade_no");
String tradeNo = map.get("transaction_id");
String totalFee = map.get("total_fee");
String returnCode = map.get("return_code");
String resultCode = map.get("result_code");
return WxPayNotifyResponse.success("处理成功!");
} catch (Exception e) {
log.error("微信回调结果异常,异常原因{}", e.getMessage());
return WxPayNotifyResponse.fail(e.getMessage());
}
}
/**
* 下单操作
* @param code
* @return
* @throws Exception
*/
@GetMapping("unifiedOrder/{code}")
public String unifiedOrder(@PathVariable String code) throws Exception {
WXPayConfigCustom config = new WXPayConfigCustom();
WXPay wxpay = new WXPay(config);
Map<String, String> data = new HashMap<String, String>();
data.put("body", "餐掌柜-餐饮消费");
// data.put("out_trade_no", "2138091910595900001012");
data.put("out_trade_no", code);
data.put("device_info", "");
data.put("fee_type", "CNY");
data.put("total_fee", "1");
data.put("spbill_create_ip", "123.12.12.123");
data.put("notify_url", "http://itheima.ngrok2.xiaomiqiu.cn/wxpay/notify");
data.put("trade_type", "NATIVE"); // NATIVE 指定为扫码支付 JSAPI 网站支付
// data.put("openid", "12");
try {
Map<String, String> resp = wxpay.unifiedOrder(data);
System.out.println("支付结果:"+resp);
return resp.get("code_url");
} catch (Exception e) {
e.printStackTrace();
}
return "OK";
}
/**
* 退款
* @param code 订单号
* @param refund_no 退款号
* @return
* @throws Exception
*/
@GetMapping("refunds/{code}/{refund_no}")
public Map<String, String> refunds(@PathVariable String code, @PathVariable String refund_no) throws Exception {
WXPayConfigCustom config = new WXPayConfigCustom();
WXPay wxpay = new WXPay(config);
Map<String, String> data = new HashMap<String, String>();
data.put("out_trade_no", code);
data.put("out_refund_no", refund_no);
data.put("notify_url", "http://484cd438.cpolar.io/wxpay/notify");
data.put("refund_desc", "已经售罄");
data.put("refund_fee", "1");
data.put("total_fee", "1");
data.put("refund_fee_type", "CNY");
System.out.println("请求参数:" + data);
Map<String, String> map = wxpay.refund(data);
return map;
}
}
V3版本。文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3_partner/index.shtml
微信官方并没有提供类似支付宝的EasySDK,只提供了基于HttpClient封装的SDK包,在项目中我们对于此SDK做了二次封装。微信接口都是基于RESTful进行提供的。
/**
* 微信支付远程调用对象
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class WechatPayHttpClient {
private String mchId; //商户号
private String appId; //应用号
private String privateKey; //私钥字符串
private String mchSerialNo; //商户证书序列号
private String apiV3Key; //V3密钥
private String domain; //请求域名
private String notifyUrl; //请求地址
public static WechatPayHttpClient get(Long enterpriseId) {
// 查询配置
PayChannelService payChannelService = SpringUtil.getBean(PayChannelService.class);
PayChannelEntity payChannel = payChannelService.findByEnterpriseId(enterpriseId, TradingConstant.TRADING_CHANNEL_WECHAT_PAY);
if (ObjectUtil.isEmpty(payChannel)) {
throw new SLException(TradingEnum.CONFIG_EMPTY);
}
//通过渠道对象转化成微信支付的client对象
JSONObject otherConfig = JSONUtil.parseObj(payChannel.getOtherConfig());
return WechatPayHttpClient.builder()
.appId(payChannel.getAppId())
.domain(payChannel.getDomain())
.privateKey(payChannel.getMerchantPrivateKey())
.mchId(otherConfig.getStr("mchId"))
.mchSerialNo(otherConfig.getStr("mchSerialNo"))
.apiV3Key(otherConfig.getStr("apiV3Key"))
.notifyUrl(payChannel.getNotifyUrl())
.build();
}
/***
* 构建CloseableHttpClient远程请求对象
* @return org.apache.http.impl.client.CloseableHttpClient
*/
public CloseableHttpClient createHttpClient() throws Exception {
// 加载商户私钥(privateKey:私钥字符串)
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes(StandardCharsets.UTF_8)));
// 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3密钥)
PrivateKeySigner privateKeySigner = new PrivateKeySigner(mchSerialNo, merchantPrivateKey);
WechatPay2Credentials wechatPay2Credentials = new WechatPay2Credentials(mchId, privateKeySigner);
// 向证书管理器增加需要自动更新平台证书的商户信息
CertificatesManager certificatesManager = CertificatesManager.getInstance();
certificatesManager.putMerchant(mchId, wechatPay2Credentials, apiV3Key.getBytes(StandardCharsets.UTF_8));
// 初始化httpClient
return com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, merchantPrivateKey)
.withValidator(new WechatPay2Validator(certificatesManager.getVerifier(mchId)))
.build();
}
/***
* 支持post请求的远程调用
*
* @param apiPath api地址
* @param params 携带请求参数
* @return 返回字符串
*/
public WeChatResponse doPost(String apiPath, Map<String, Object> params) throws Exception {
String url = StrUtil.format("https://{}{}", this.domain, apiPath);
HttpPost httpPost = new HttpPost(url);
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type", "application/json; charset=utf-8");
String body = JSONUtil.toJsonStr(params);
httpPost.setEntity(new StringEntity(body, CharsetUtil.UTF_8));
CloseableHttpResponse response = this.createHttpClient().execute(httpPost);
return new WeChatResponse(response);
}
/***
* 支持get请求的远程调用
* @param apiPath api地址
* @param params 在路径中请求的参数
* @return 返回字符串
*/
public WeChatResponse doGet(String apiPath, Map<String, Object> params) throws Exception {
URI uri = UrlBuilder.create()
.setHost(this.domain)
.setScheme("https")
.setPath(UrlPath.of(apiPath, CharsetUtil.CHARSET_UTF_8))
.setQuery(UrlQuery.of(params))
.setCharset(CharsetUtil.CHARSET_UTF_8)
.toURI();
return this.doGet(uri);
}
/***
* 支持get请求的远程调用
* @param apiPath api地址
* @return 返回字符串
*/
public WeChatResponse doGet(String apiPath) throws Exception {
URI uri = UrlBuilder.create()
.setHost(this.domain)
.setScheme("https")
.setPath(UrlPath.of(apiPath, CharsetUtil.CHARSET_UTF_8))
.setCharset(CharsetUtil.CHARSET_UTF_8)
.toURI();
return this.doGet(uri);
}
private WeChatResponse doGet(URI uri) throws Exception {
HttpGet httpGet = new HttpGet(uri);
httpGet.addHeader("Accept", "application/json");
CloseableHttpResponse response = this.createHttpClient().execute(httpGet);
return new WeChatResponse(response);
}
}
代码说明:
get(Long enterpriseId)
方法查询商户对应的配置信息,最后封装到WechatPayHttpClient
对象中。createHttpClient()
方法封装了请求微信接口必要的参数,最后返回CloseableHttpClient
对象。doGet()、doPost()
方便对微信接口进行调用。 try {
WeChatResponse response = client.doPost(apiPath, params);
if(!response.isOk()){
throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
}
tradingEntity.setPlaceOrderCode(Convert.toStr(response.getStatus())); //返回的编码
tradingEntity.setPlaceOrderMsg(JSONUtil.parseObj(response.getBody()).getStr("code_url")); //二维码需要展现的信息
tradingEntity.setPlaceOrderJson(JSONUtil.toJsonStr(response));
tradingEntity.setTradingState(TradingStateEnum.FKZ);
} catch (Exception e) {
throw new SLException(TradingEnum.NATIVE_PAY_FAIL);
}
PS:接收回调地址的话需要用到内网穿透工具,如果没开内网穿透则无法接收到。推荐用cpolar或者natapp