支付宝简介
使用流程
支付宝开放平台 (alipay.com)
- 1、登录支付宝开放平台-创建应用
- 得到app_id
- 2、绑定应用:在产品绑定中,绑定支付宝的相关功能
- 3、配置应用:
- 配置各种加密方式
- 4、提交审核:
- 5、把支付宝整个功能整合项目
沙箱:
- 支付宝提前为我们创建好一个账号,用这个账号可以体验支付宝的所有功能;
- 应用开发期间用沙箱账号,上线以后替换成线上账号即可。
产品介绍 - 支付宝文档中心 (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 {@BeanAlipayClient 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实现生成支付页的方法
@Overridepublic 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);}}//什么时候给支付宝返回successreturn "success";}
修改订单状态 如果用户在临关单前的极限时间支付后,为了避免用户订单被强制改为关单。我们需要设置较高的优先级。
@Overridepublic 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");//知道用户idString[] 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();//查询orderInfoOrderInfo 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;}
微信支付
微信支付V2
准备环境
C扫B需求要求可以手机网页交互,微信JSAPI/NATIVE支付符合需求。
总结步骤:
- 微信支付商户平台申请支付商户,填写企业资质认证后,可以在用户信息中获取 商户号
- 以企业身份注册微信公众号 https://mp.weixin.qq.com/
- 登录公众号,点击左侧菜单 “微信支付” 开通微信支付,需要提供营业执照、身份证等信息。
- 缴纳认证费用,等待1-3工作日审核。
- 审核通过后在支付设置中获取 APPID
- 审核通过后需要在 微信支付商户平台 中绑定APPID
- 绑定成功后需要申请 支付相关API秘钥
快速入门
参考文档: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.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency><groupId>com.github.tedzhdz</groupId><artifactId>wxpay-sdk</artifactId><version>3.0.10</version>
</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*/@Overrideprotected String getAppID() {return "wx0ca99a203b1e9943";}/*** 商户号* @return*/@Overrideprotected String getMchID() {return "1561414331";}/*** appkey API密钥* @return*/@Overrideprotected String getKey() {return "CZBK51236435wxpay435434323FFDuis";}// 退款:必须强制使用API证书@Overrideprotected InputStream getCertStream() {try {String path = ClassLoader.getSystemResource("").getPath();return new FileInputStream(new File(path+"apiclient_cert.p12"));} catch (FileNotFoundException e) {e.printStackTrace();}return null;}@Overrideprotected IWXPayDomain getWXPayDomain() {return new IWXPayDomain() {@Overridepublic void report(String s, long l, Exception e) {}@Overridepublic DomainInfo getDomain(WXPayConfig wxPayConfig) {return new DomainInfo(WXPayConstants.DOMAIN_API, true);}};}
}
注意:
- 退款操作需要API证书,将资料中的证书(apiclient_cert.p12)放到resources路径下
(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
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));// 初始化httpClientreturn 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