微信支付
- 付款码支付
付款码支付是指用户展示微信钱包内的“付款码”给商户系统扫描后直接完成支付,适用于线下场所面对面收银的场景,例如商超、便利店、餐饮、医院、学校、电影院和旅游景区等具有明确经营地址的实体场所 - JSAPI支付
JSAPI支付是指商户通过调用微信支付提供的JSAPI接口,在支付场景中调起微信支付模块完成收款。
应用场景有:
线下场所:调用接口生成二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付
公众号场景:用户在微信公众账号内进入商家公众号,打开某个主页面,完成支付
PC网站场景:在网站中展示二维码,用户扫描二维码后在微信浏览器中打开页面后完成支付
- 小程序支付
小程序支付是指商户通过调用微信支付小程序支付接口,在微信小程序平台内实现支付功能;用户打开商家助手小程序下单,输入支付密码并完成支付后,返回商家小程序。 - Native支付
Native支付是指商户系统按微信支付协议生成支付二维码,用户再用微信“扫一扫”完成支付的模式。该模式适用于PC网站、实体店单品或订单、媒体广告支付等场景。 - APP支付
APP支付是指商户通过在移动端应用APP中集成开放SDK调起微信支付模块来完成支付。适用于在移动端APP中集成微信支付功能的场景。 - 刷脸支付
刷脸支付是指用户在刷脸设备前通过摄像头刷脸、识别身份后进行的一种支付方式,安全便捷。适用于线下实体场所的收银场景,如商超、餐饮、便利店、医院、学校等。
支付流程
三个关键步骤是需要后台人员去实现的
核心依赖
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.9</version>
</dependency>
主要是用来向微信服务器发送http请求的客户端
API证书
拿营业执照申请API证书
下单请求
根据接口文档,通过httpClient发送请求
请求体:
{
"mchid": "1900006XXX",
"out_trade_no": "native12177525012014070332333",
"appid": "wxdace645e0bc2cXXX",
"description": "Image形象店-深圳腾大-QQ公仔",
"notify_url": "https://weixin.qq.com/",
"amount": {
"total": 1,
"currency": "CNY"
}
}
响应体
{
"code_url": "weixin://wxpay/bizpayurl?pr=p4lpSuKzz"
}
请求体参数可以封装成对应的对象
/**
* 订单请求体
* */
@Data
@Builder
public class NativePayParams {
/**
* 应用id
* */
private String appid;
/**
* 商户id
* */
private String mchid;
/**
* 商品描述
* */
private String description;
/**
* 订单号
* */
private String out_trade_no;
/**
* 回调通知地址
* */
private String notify_url;
/**
* 订单金额
* */
private Amount amount;
}
/**
* 金额对象
* */
@Data
@Builder
public class Amount {
/**
* 总金额
* */
private Integer total;
/**
* 货币单位
* */
private String currency;
}
public class PayService {
private CloseableHttpClient httpClient;
/**
* 必备的校验参数:需要拿营业执照向腾讯申请
* */
/**
* 商户API私钥
* */
private String privateKey="";
private String mchId="";
/**
* 证书序列号
* */
private String mchSerialNo="";
/**
* API v3密钥
* */
private String apiV3Key="";
public static void main(String[] args) {
try {
PayService payService = new PayService();
payService.setup();
payService.CreateOrder();
payService.after();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 初始化
* */
public void setup() throws IOException {
// 加载商户私钥(privateKey:私钥字符串)
PrivateKey merchantPrivateKey = PemUtil
.loadPrivateKey(new ByteArrayInputStream(privateKey.getBytes("utf-8")));
// 加载平台证书(mchId:商户号,mchSerialNo:商户证书序列号,apiV3Key:V3密钥)
AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
new WechatPay2Credentials(mchId, new PrivateKeySigner(mchSerialNo, merchantPrivateKey)),apiV3Key.getBytes("utf-8"));
// 初始化httpClient
httpClient = WechatPayHttpClientBuilder.create()
.withMerchant(mchId, mchSerialNo, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier)).build();
}
/**
* 创建并发送订单
* */
public void CreateOrder() throws Exception{
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/native");
// 封装请求体
String reqdata = "json字符串";
StringEntity entity = new StringEntity(reqdata,"utf-8");
entity.setContentType("application/json");
httpPost.setEntity(entity);
httpPost.setHeader("Accept", "application/json");
//发送http请求
CloseableHttpResponse response = httpClient.execute(httpPost);
try {
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200) { //处理成功
System.out.println("success,return body = " + EntityUtils.toString(response.getEntity()));
} else if (statusCode == 204) { //处理成功,无返回Body
System.out.println("success");
} else {
System.out.println("failed,resp code = " + statusCode+ ",return body = " + EntityUtils.toString(response.getEntity()));
throw new IOException("request failed");
}
} finally {
response.close();
}
}
/**
* 关闭连接
* */
public void after() throws IOException {
httpClient.close();
}
}
支付结果
微信官方会异步通知商户支付结果,商户也可以自行查询支付结果
异步通知
在发送下单请求时,需要指定notify_url,这是一个接口地址,微信会访问这个接口,并通过http传递支付结果
写一个接口来接收通知就行了
接口要接收的请求体
{
"id": "EV-2018022511223320873",
"create_time": "2015-05-20T13:29:35+08:00",
"resource_type": "encrypt-resource",
"event_type": "TRANSACTION.SUCCESS",
"summary": "支付成功",
"resource": {
"original_type": "transaction",
"algorithm": "AEAD_AES_256_GCM",
"ciphertext": "",
"associated_data": "",
"nonce": ""
}
}
/**
* 回调通知
* */
@Data
public class NotifyDTO {
/**
* 通知的唯一ID
* */
private String id;
/**
* 通知创建的时间
* */
private String create_time;
/**
* 通知的类型
* */
private String event_type;
/**
* 通知的资源数据类型
* */
private String resource_type;
/**
* 回调摘要
* */
private String summary;
/**
* 通知资源数据
* */
private ResourceDTO resourceDTO;
}
/**
* 资源
* */
@Data
public class ResourceDTO {
/**
* 对开启结果数据进行加密的加密算法
* */
private String algorithm;
/**
* Base64编码后的开启/停用结果数据密文
* */
private String ciphertext;
/**
* 附加数据
* */
private String associated_data;
/**
* 原始回调类型,为transaction
* */
private String original_type;
/**
* 加密使用的随机串
* */
private String nonce;
}
接口返回结果
/**
* 接收失败时,需要返回一个对象给微信
* */
@Data
public class ErrorDTO {
/**
* 错误码,SUCCESS为清算机构接收成功,其他错误码为失败
* */
private String code;
/**
* 返回信息,如非空,为错误原因
* */
private String message;
}
接口设计
/**
* 与微信的交互
*/
@RestController
@RequestMapping("/pay")
public class PayController {
@Resource
NativePayService nativePayService;
@PostMapping("/notify")
public ErrorDTO getNotify(@RequestBody NotifyDTO notifyDTO) {
nativePayService.payNotify(notifyDTO);
return new ErrorDTO();
}
}
解密Resource
解密微信发来的Resource部分,可以拿到订单对应的编号,从而根据该编号完成支付流程
AES-256-GCM是一种对称密钥加密,也就是说,加密解密,只考虑apiV3Key密钥即可
/**
* 处理支付业务
* */
@Service
public class NativeServiceImpl implements NativePayService{
/**
* 对称密钥
* */
private static final String apiV3Key="";
/**
* 订单号
* */
public static String tradeNo;
@Override
public ErrorDTO payNotify(NotifyDTO notifyDTO) {
try {
AesUtil aesUtil = new AesUtil(apiV3Key.getBytes());
ResourceDTO resourceDTO = notifyDTO.getResourceDTO();
String json = aesUtil.decryptToString(
resourceDTO.getAssociated_data().getBytes(),
resourceDTO.getNonce().getBytes(),
resourceDTO.getCiphertext()
);
Map map = JSONUtil.parseObj(json);
tradeNo = map.get("out_trade_no").toString();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
return null;
}
}
主动查询订单
后台需要不断轮询,向微信查询订单支付的结果,直到获得成功的结果
使用定时任务即可
请求体
响应体
{
"appid" : "wxd678efh567hg6787",
"mchid" : "1230000109",
"out_trade_no" : "1217752501201407033233368018",
"transaction_id" : "1217752501201407033233368018",
"trade_type" : "MICROPAY",
"trade_state" : "SUCCESS",
"trade_state_desc" : "支付失败,请重新下单支付",
"bank_type" : "CMC",
"attach" : "自定义数据",
"success_time" : "2018-06-08T10:34:56+08:00",
"payer" : {
"openid" : "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o\t"
},
"amount" : {
"total" : 100,
"payer_total" : 100,
"currency" : "CNY",
"payer_currency" : "CNY"
},
"scene_info" : {
"device_id" : "013467007045764"
},
"promotion_detail" : [
{
"coupon_id" : "109519",
"name" : "单品惠-6",
"scope" : "GLOBAL",
"type" : "CASH",
"amount" : 100,
"stock_id" : "931386",
"wechatpay_contribute" : 0,
"merchant_contribute" : 0,
"other_contribute" : 0,
"currency" : "CNY",
"goods_detail" : [
{
"goods_id" : "M1006",
"quantity" : 1,
"unit_price" : 100,
"discount_amount" : 1,
"goods_remark" : "商品备注信息"
}
]
}
]
}
支付宝
沙箱环境
访问支付宝接口需要的验证信息在这里获取
如果是生产环境则需要拿营业执照向官方申请密钥和证书
加密原理
非对称加密:
- 通信双方分别创建公钥和私钥,
- 并且保证公钥所加密的信息,只有配对的私钥可以解密,
- 接下来,双方公开交换公钥,通信时,使用对方的公钥进行加密,
- 如此,就能确保对方能够通过自己的私钥解密
显然,这种做法并不安全,第三方可以直接拦截一方发送的公钥,将其调包成自己的公钥,这样一来,另一方使用该公钥加密的所有信息,都能被第三方轻易解密
二维码信息载体
一维码只有宽度表示信息,二维码长度和宽度都能储存信息
二维码生成过程:
- 数据经过编码之后得到一个二进制串
- 数据串将和纠错码交织在一起
- 依据相应版本二维码的规范,利用不同尺寸的方块填充得到二维码
对接支付宝
依赖:
<!-- https://mvnrepository.com/artifact/com.alipay.sdk/alipay-easysdk -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-easysdk</artifactId>
<version>2.2.3</version>
</dependency>
配置文件
server:
port: 15100
pay:
alipay:
protocol: https
gatewayHost: openapi-sandbox.dl.alipaydev.com
signType: RSA2
appId: #应用Id
merchantPrivateKey: #应用私钥
alipayPublicKey: #支付宝公钥
notifyUrl: #支付宝发送通知地址
这里的notifyUrl指的是公网的url,如果想要本机接收,需要先进行内网穿透
内网穿透:
教程参考:内网穿透教程
工具:cpolar
命令:
cpolar authtoken <令牌> #将authtoken保存到本机,
cpolar http <要暴露的端口号> #在这个端口上开一个通道连接到cpolar
@RestController
@RequestMapping("/alipay")
@Slf4j
public class AliPayController {
@Resource
Config config;
@Resource
AliPayConfig aliPayConfig;
@GetMapping("/code")
public String pay() throws Exception {
try {
//1.添加配置项
Factory.setOptions(config);
//2.调用接口发送请求
AlipayTradePrecreateResponse response = Factory
.Payment
.FaceToFace()
.preCreate(
aliPayConfig.getSubject(),
"123456",
"10"
);
//3.解析响应结果
String httpBody = response.getHttpBody();
JSONObject jsonObject = JSONUtil.parseObj(httpBody);
//3.2获取订单号
String qrCode = jsonObject.getJSONObject("alipay_trade_precreate_response").get("qr_code").toString();
log.info("返回二维码:{}", qrCode);
QrCodeUtil.generate(
qrCode,
500,
500,
new File("D:/test2.jpg")
);
return qrCode;
} catch (Exception e) {
log.error("发生错误:{}", e.getClass().getName() + e.getMessage());
e.printStackTrace();
return "获取二维码失败,请稍后再试";
}
}
@PostMapping("/notify")
public String notify(HttpServletRequest request) {
String outTradeNo = request.getParameter("out_trade_no");
log.info("订单{}支付成功", outTradeNo);
return "success";
}
@GetMapping("/query")
public String queryOrder() {
try {
Factory.setOptions(config);
AlipayTradeQueryResponse response = Factory.Payment.Common().query("123456");
log.info("支付结果:{}", response);
return response.getHttpBody();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}