自己工作中对接完成之后的记录.
先是拿到微信对应的参数,放在application.yml中.
appid和secret是微信小程序或者公众号的参数
mchId是用于收款的商户号的id
mchSerialNo是商户证书序列号(可在微信支付平台商户信息中查看)
notifyUrl是支付成功后的回调地址,微信要求必须为https并且绑定域名,无法使用ip端口
apiV2Key和apiV3Key都在商户的API中心设置,分别对应两套版本,目前基本都使用apiV3,少量接口会使用到apiV2.
个人觉得在对接微信过程中最麻烦的就是签名的校验和解析,但是在apiV3的SDK中带的client已经封装好了签名相关功能,因此在对接文档的时候可以忽略相关的签名参数,这点很不错.如果是apiV2的接口则需要手动签名.
设置好配置文件之后需要配置一个微信client的config文件.
@Data
@Configuration
@ConfigurationProperties(prefix = "wx")
public class WechatPayConfig {
private String appid;
private String secret;
private String privateKey;
private String mchId;
private String mchSerialNo;
private String apiV2Key;
private String apiV3Key;
private String notifyUrl;
// private String version;
// private String subMchid;
// private String subApiV2Key;
// private static final String DEV = "dev";
// private static final String PROD = "prod";
public String getPrivateKey() {
return "-----BEGIN PRIVATE KEY-----\n" +
"MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDu4AhYeoJ2vCw/\n" +
"ElrzSyTnxlFw96g7WBIFHy64f/4CQXKkz5Wj9Gi9UZGwTaPj85c73SVvBU+rgPnH\n" +
"frhsB/cQ7xSCrBdcW+1KKiiM\n" +
"-----END PRIVATE KEY-----";
}
}
这里如果要分dev和prod两个版本,则可以使用version的切换在getPrivateKey方法中返回不同的两个key.同时就要在配置文件中设置
wx:
version: ${spring.profiles.active}
才可以在配置文件中获取到version为dev还是prod
其中suvMchid和subApiV2Key这些sub相关参数是服务商用的,如果是服务商对接的微信支付,那么sub就是服务商下的子商户相关参数,对应的接口api也不一样,这里记录的是非服务商的商户直接对接
配置好该配置类以后就可以在代码中注入来获取到微信client从而避免签名校验
@Autowired
private CloseableHttpClient wxHttpClient;
微信支付分为很多种支付方式,但其实对接完一种以后其他种大同小异,只是一些参数区别,这里以native支付举例,native支付的流程是:创建内部订单,拿到订单号和相关价格名称参数去调用微信的api,微信api返回一个二维码数据,把二维码返回给前端,进行扫码支付,支付完成之后微信回调我们项目的接口,修改订单状态,完成.也可以手动去查询微信接口获取结果,这里不记录退款等其他操作.
微信文档//https://pay.weixin.qq.com/docs/merchant/apis/native-payment/direct-jsons/native-prepay.html
controller层:
@PostMapping("/pay")
@ApiOperation(value = "微信支付")
public R generateSignature(@RequestBody @Valid WxPayOrderRequest request) {
return R.ok(payService.prePay(request));
}
其中入参为:
@Data
public class WxPayOrderRequest {
@NotBlank(message = "金额必填")
@ApiModelProperty(value = "支付金额,单位元")
private String realPrice;
@NotBlank(message = "订单号必填")
@ApiModelProperty(value = "系统内部订单号")
private String orderNo;
@NotBlank(message = "商品名称必填")
@ApiModelProperty("商品名称")
private String name;
}
service层,注意返回值给的是Map
/**
* 生成预支付交易单
*/
Map<String, String> prePay(WxPayOrderRequest request);
impl层,这里构建参数用的是jackson,可以替换为fastjson等都可以.
@Override
public Map<String, String> prePay(WxPayOrderRequest request) {
try {
//文档https://pay.weixin.qq.com/docs/merchant/apis/native-payment/direct-jsons/native-prepay.html
String url = "https://api.mch.weixin.qq.com/v3/pay/transactions/native";
HttpPost httpPost = new HttpPost(url);
//固定值
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type", "application/json; charset=utf-8");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectMapper objectMapper = new ObjectMapper();
//这里对照文档,使用jackson构建参数,也可以使用fastjson之类的
ObjectNode rootNode = objectMapper.createObjectNode();
rootNode.put("appid", payConfig.getAppid())
.put("mchid", payConfig.getMchId())
.put("description", request.getName())
.put("out_trade_no", request.getOrderNo())
.put("notify_url", payConfig.getNotifyUrl());
//子对象里的子字段
rootNode.putObject("amount")
.put("total", Integer.parseInt(yuanToFen(request.getRealPrice())));
System.out.println("rootNode=" + rootNode);
objectMapper.writeValue(bos, rootNode);
httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
//返回结果
CloseableHttpResponse response = wxHttpClient.execute(httpPost);
String bodyAsString = EntityUtils.toString(response.getEntity());
log.info("bodyAsString为{}",bodyAsString);
ObjectMapper obj = new ObjectMapper();
//解析结果
Map<String, String> map = obj.readValue(bodyAsString, Map.class);
log.info("创建订单map解析出来为{}",map);
//这里注意微信如果成功只返回code_url,失败会返回code,所以需要判断
if (map.get("code")!=null){
throw new RuntimeException("该订单号已支付!");
}
Map<String, String> result = new HashMap<>();
String codeUrl = map.get("code_url");
if (StrUtil.isBlank(codeUrl)) throw new RuntimeException("创建订单失败,请稍后重试");
//把url放到
String qrCode = QrCodeUtil.generateAsBase64(codeUrl, QrConfig.create(), "jpg");
result.put("code_url", qrCode);
return result;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("创建订单失败!");
}
}
然后是回调,回调地址在配置文件中配置,微信支付成功之后会调用这个接口
@Anonymous
@ApiOperation("微信支付回调")
@PostMapping("/callback")
public void callback(HttpServletRequest request, HttpServletResponse response) {
try {
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> map = JsonUtils.JsonToEntity(request.getInputStream(), Map.class);
String message = mapper.writeValueAsString(map);
log.info("微信支付通知:{}", message);
//这是返回给微信的结果,需要这两个参数
Map<String, String> res = new HashMap<>();
res.put("code", "SUCCESS");
res.put("message", "成功");
//支付成功回调
if ("TRANSACTION.SUCCESS".equals("" + map.get("event_type"))) {
//拿到回调内容中的source进行解密 解密后获取订单号,根据订单号去修改状态
Map<String, String> resource = (Map) map.get("resource");
log.info("回调中的resource为{}", resource);
//这里的AESUtils是微信的解密工具类,里面还会用到其他的一些依赖
String decrypt = AESUtils.decryptToString(payConfig.getApiV3Key().getBytes(),
resource.get("associated_data").getBytes(),
resource.get("nonce").getBytes(),
resource.get("ciphertext"));
log.info("decrypt为{}", decrypt);
//解密之后的参数重新构建为一个java对象,这个对象里的参数只能多不能少,如果少了会转换失败.所以我的对象里包含了商户订单的和服务商订单的一些参数.有很多参数是用不到的 常用就一个订单号
TbWxOrder params = mapper.readValue(decrypt, TbWxOrder.class);
log.info("解密消息:{}", mapper.writeValueAsString(params));
if ("SUCCESS".equals(params.getTradeState())) {
String orderNo = params.getOutTradeNo();
log.info("解密之后订单号为{}", orderNo);
//拿到订单号就可以查订单改状态 或者其他逻辑,这里其他逻辑的代码我就删除了
//修改订单状态
order.setStatus(1);
messageOrderService.updateById(order);
}
}
//回调完成,写入response结束回调方法
response.getWriter().write(mapper.writeValueAsString(res));
} catch (Exception e) {
log.info("解密过程出错了");
e.printStackTrace();
}
}
代码当中使用到的解析对象,里面有很多参数,这里的参数可以多不可以少,少了就会报json解析失败.所以我这个对象当中其实是多个接口的参数放在一起的,并不是一个接口的.
@Data
@Accessors(chain = true)
@JsonIgnoreProperties(ignoreUnknown = true)
public class TbWxOrder {
private String mchid;
private String appid;
@JsonProperty("sp_mchid")
private String spMchid;
@JsonProperty("sub_mchid")
private String subMchid;
@JsonProperty("sp_appid")
private String spAppid;
@JsonProperty("sub_appid")
private String subAppid;
@JsonProperty("out_trade_no")
private String outTradeNo;
@JsonProperty("transaction_id")
private String transactionId;
@JsonProperty("trade_type")
private String tradeType;
@JsonProperty("trade_state")
private String tradeState;
@JsonProperty("trade_state_desc")
private String tradeStateDesc;
@JsonProperty("bank_type")
private String bankType;
private String attach;
@JsonProperty("success_time")
private String successTime;
private Payer payer;
private Amount amount;
@JsonProperty("refund_id")
private String refundId;
@JsonProperty("out_refund_no")
private String outRefundNo;
private String status;
@JsonProperty("refund_status")
private String refundStatus;
private String channel;
@JsonProperty("user_received_account")
private String userReceivedAccount;
@JsonProperty("create_time")
private String createTime;
private String openid;
private Integer total;
private Integer payerTotal;
private String currency;
private String payerCurrency;
private Integer refund;
private Integer payerRefund;
}
最后是一个解密用的工具类,微信的回调内容是加密的,需要解密才能解析出内容
/**
* 解密微信callback回调的工具类
*/
@Slf4j
public class AESUtils {
// 加密模式
private static final String ALGORITHM = "AES/CBC/PKCS7Padding";
private static final String CHARSET_NAME = "UTF-8";
private static final String AES_NAME = "AES";
//解决java.security.NoSuchAlgorithmException: Cannot find any provider supporting AES/CBC/PKCS7Padding
static {
Security.addProvider(new BouncyCastleProvider());
}
/**
* 解密
*
* @param content 目标密文
* @param key 秘钥
* @param iv 偏移量
* @return
*/
public static String decrypt(@NotNull String content, @NotNull String key, @NotNull String iv) {
try {
Cipher cipher = Cipher.getInstance(ALGORITHM);
byte[] sessionKey = Base64.getDecoder().decode(key);
SecretKeySpec keySpec = new SecretKeySpec(sessionKey, AES_NAME);
byte[] ivByte = Base64.getDecoder().decode(iv);
AlgorithmParameterSpec paramSpec = new IvParameterSpec(ivByte);
cipher.init(Cipher.DECRYPT_MODE, keySpec, paramSpec);
return new String(cipher.doFinal(Base64.getDecoder().decode(content)), CHARSET_NAME);
} catch (Exception e) {
// log.error("解密失败:{}", e);
}
return "";
}
static final int TAG_LENGTH_BIT = 128;
public static String decryptToString(byte[] aesKey, byte[] associatedData, byte[] nonce, String ciphertext)
throws GeneralSecurityException, IOException {
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
SecretKeySpec key = new SecretKeySpec(aesKey, "AES");
GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, nonce);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
cipher.updateAAD(associatedData);
return new String(cipher.doFinal(Base64.getDecoder().decode(ciphertext)), CHARSET_NAME);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalArgumentException(e);
}
}
}
over