前言:
1、本教程非常简单,全源码
2、本文章仅介绍java后端对接流程,功能开通以及前端调用(基础版无需提交资料审核,不过额度只有200),请参考官方文档产品介绍_商家转账|微信支付商户文档中心
3、从去年年底开始,原微信商家转账到零钱功能已停止开放,替换为商家转账功能。与之前的商家转账到零钱相比,流程增加了用户确认步骤,通过API发起转账以后,还需要用户通过商户小程序点击确认(前端指引->JSAPI调起用户确认收款_商家转账|微信支付商户文档中心)
一、引入微信支付官方SDK(不需要重复造轮子)
<!-- 微信支付sdk -->
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-java</artifactId>
<version>0.2.15</version>
</dependency>
二、封装实体类
后端API一共三个方法,发起转账->查询转账->撤销转账,状态变更都有异步回调通知,所以我们封装以下实体类(请勿更改@serializedName注解)
1、请求实体类(只有发起转账需要封装,其它接口都是路径传参)
/**
* 发起商家转账参数
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TransferCreateRequest {
@SerializedName("appid")
private String appid;
@SerializedName("out_bill_no")
private String outBillNo;
@SerializedName("transfer_scene_id")
private String transferSceneId;
@SerializedName("openid")
private String openid;
@SerializedName("user_name")
private String userName;
@SerializedName("transfer_remark")
private String transferRemark;
@SerializedName("transfer_amount")
private Integer transferAmount;
@SerializedName("notify_url")
private String notifyUrl;
/**
* 【用户收款感知】
* 用户收款时感知到的收款原因将根据转账场景自动展示默认内容。
* 如有其他展示需求,可在本字段传入。
* 各场景展示的默认内容和支持传入的内容,可查看产品文档了解。
*/
@SerializedName("user_recv_perception")
private String userRecvPerception;
/**
* 【转账场景报备信息】 各转账场景下需报备的内容,商户需要按照所属转账场景规则传参,详见转账场景报备信息字段说明。
*/
@SerializedName("transfer_scene_report_infos")
private List<TransferSceneReportInfo> transferSceneReportInfos;
@Override
public String toString() {
return GsonUtil.getGson().toJson(this);
}
}
/**
* 转账场景报备信息
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TransferSceneReportInfo {
/**
* 【信息类型】 不能超过15个字符,商户所属转账场景下的信息类型,此字段内容为固定值,需严格按照转账场景报备信息字段说明传参。
*/
@SerializedName("info_type")
private String infoType;
/**
* 【信息内容】 不能超过32个字符,商户所属转账场景下的信息内容,商户可按实际业务场景自定义传参,需严格按照转账场景报备信息字段说明传参。
*/
@SerializedName("info_content")
private String infoContent;
@Override
public String toString() {
return GsonUtil.getGson().toJson(this);
}
}
2、响应实体类
@Data
public class TransferCreateResponse {
@SerializedName("transfer_bill_no")
private String transferBillNo;
@SerializedName("out_bill_no")
private String outBillNo;
@SerializedName("create_time")
private Date createTime;
/**
* 【单据状态】 商家转账订单状态
* 可选取值
* ACCEPTED: 转账已受理
* PROCESSING: 转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试。
* WAIT_USER_CONFIRM: 待收款用户确认,可拉起微信收款确认页面进行收款确认
* TRANSFERING: 转账中,可拉起微信收款确认页面再次重试确认收款
* SUCCESS: 转账成功
* FAIL: 转账失败
* CANCELING: 商户撤销请求受理成功,该笔转账正在撤销中
* CANCELLED: 转账撤销完成
*/
@SerializedName("state")
private String state;
/**
* 【失败原因】 订单已失败或者已退资金时,会返回订单失败原因
* <a href="https://pay.weixin.qq.com/doc/v3/merchant/4013774966">...</a>
*/
@SerializedName("fail_reason")
private String failReason;
/**
* 【跳转领取页面的package信息】 跳转微信支付收款页的package信息,APP调起用户确认收款或者JSAPI调起用户确认收款 时需要使用的参数。
* 单据创建后,用户24小时内不领取将过期关闭,建议拉起用户确认收款页面前,先查单据状态:如单据状态为待收款用户确认,可用之前的package信息拉起;单据到终态时需更换单号重新发起转账。
*/
@SerializedName("package_info")
private String packageInfo;
}
@Data
public class TransferQueryResponse {
@SerializedName("mch_id")
private String mchid;
@SerializedName("transfer_bill_no")
private String transferBillNo;
@SerializedName("out_bill_no")
private String outBillNo;
@SerializedName("appid")
private String appid;
/**
* 【单据状态】 商家转账订单状态
* 可选取值
* ACCEPTED: 转账已受理
* PROCESSING: 转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试。
* WAIT_USER_CONFIRM: 待收款用户确认,可拉起微信收款确认页面进行收款确认
* TRANSFERING: 转账中,可拉起微信收款确认页面再次重试确认收款
* SUCCESS: 转账成功
* FAIL: 转账失败
* CANCELING: 商户撤销请求受理成功,该笔转账正在撤销中
* CANCELLED: 转账撤销完成
*/
@SerializedName("state")
private String state;
@SerializedName("transfer_amount")
private Integer transferAmount;
@SerializedName("transfer_remark")
private String transferRemark;
@SerializedName("fail_reason")
private String failReason;
@SerializedName("openid")
private String openid;
@SerializedName("user_name")
private String userName;
/**
* 【单据创建时间】遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE,
* yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss.表示时分秒,
* TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。
* 例如:2015-05-20T13:29:35+08:00表示北京时间2015年05月20日13点29分35秒。
*/
@SerializedName("create_time")
private Date createTime;
/**
* 【最后一次状态变更时间】遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE,
* yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss.表示时分秒,
* TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。
* 例如:2015-05-20T13:29:35+08:00表示北京时间2015年05月20日13点29分35秒。
*/
@SerializedName("update_time")
private Date updateTime;
}
@Data
public class TransferCancelResponse {
/**
* 商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一
*/
@SerializedName("out_bill_no")
private String outBillNo;
/**
* 商家转账订单的主键,唯一定义此资源的标识
*/
@SerializedName("transfer_bill_no")
private String transferBillNo;
/**
* 【单据状态】 CANCELING: 撤销中;CANCELLED:已撤销
*/
@SerializedName("state")
private String state;
/**
* 【最后一次单据状态变更时间】 按照使用rfc3339所定义的格式,格式为yyyy-MM-DDThh:mm:ss+TIMEZONE
*/
@SerializedName("update_time")
private Date updateTime;
}
3、异步回调参数
@Data
public class TransferNotification {
@SerializedName("transfer_bill_no")
private String transferBillNo;
@SerializedName("out_bill_no")
private String outBillNo;
/**
* 【单据状态】 商家转账订单状态
* 可选取值
* ACCEPTED: 转账已受理
* PROCESSING: 转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试。
* WAIT_USER_CONFIRM: 待收款用户确认,可拉起微信收款确认页面进行收款确认
* TRANSFERING: 转账中,可拉起微信收款确认页面再次重试确认收款
* SUCCESS: 转账成功
* FAIL: 转账失败
* CANCELING: 商户撤销请求受理成功,该笔转账正在撤销中
* CANCELLED: 转账撤销完成
*/
@SerializedName("state")
private String state;
@SerializedName("mch_id")
private String mchId;
@SerializedName("transfer_amount")
private Integer transferAmount;
@SerializedName("openid")
private String openid;
/**
* 【失败原因】 订单已失败或者已退资金时,会返回订单失败原因
* <a href="https://pay.weixin.qq.com/doc/v3/merchant/4013774966">...</a>
*/
@SerializedName("fail_reason")
private String failReason;
/**
* 【单据创建时间】遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE,
* yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss.表示时分秒,
* TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。
* 例如:2015-05-20T13:29:35+08:00表示北京时间2015年05月20日13点29分35秒。
*/
@SerializedName("create_time")
private Date createTime;
/**
* 【最后一次状态变更时间】遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE,
* yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss.表示时分秒,
* TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。
* 例如:2015-05-20T13:29:35+08:00表示北京时间2015年05月20日13点29分35秒。
*/
@SerializedName("update_time")
private Date updateTime;
}
三、封装service(如果只兼容商家转账的朋友,且单商户配置的,可以在service上加@component注解,在构造方法上加入@autowired,即可实现单例模式(官方建议))
public class TransferNewService {
private final HttpClient httpClient;
private final PrivacyEncryptor encryptor;
private final PrivacyDecryptor decryptor;
/**
* 实际注入实体类为 RSAAutoCertificateConfig
* @param config 不要引用错了 包路径 com.wechat.pay.java.core.Config;
*/
public TransferNewService(Config config) {
this.httpClient =
new DefaultHttpClientBuilder()
.credential(requireNonNull(config.createCredential()))
.validator(requireNonNull(config.createValidator()))
.build();
this.encryptor = config.createEncryptor();
this.decryptor = config.createDecryptor();
}
/**
* 创建商家转账订单
* @param request 请求参数
* @return 响应参数
*/
public TransferCreateResponse createTransferOrder(TransferCreateRequest request) {
String requestPath = "https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills";
Objects.requireNonNull(this.encryptor);
request.setUserName(this.encryptor.encrypt(request.getUserName()));
return executeHttpRequest(requestPath,HttpMethod.POST,request.toString(),TransferCreateResponse.class);
}
/**
* 撤销商家转账订单
* 商户通过转账接口发起付款后,在用户确认收款之前可以通过该接口撤销付款。
* 该接口返回成功仅表示撤销请求已受理,
* 系统会异步处理退款等操作,
* 以最终查询单据返回状态为准。
* @param outBillNo 商户转账单号
* @return 响应参数
*/
public TransferCancelResponse cancelTransferOrder(String outBillNo){
String requestPath =
"https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}/cancel";
requestPath =
requestPath.replace("{" + "out_bill_no" + "}", outBillNo);
return executeHttpRequest(requestPath,HttpMethod.POST,null,TransferCancelResponse.class);
}
public TransferQueryResponse queryTransferByOutBillNo(String outBillNo){
String requestPath =
"https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}";
requestPath =
requestPath.replace("{" + "out_bill_no" + "}", outBillNo);
TransferQueryResponse response = executeHttpRequest(requestPath, HttpMethod.GET, null, TransferQueryResponse.class);
response.setUserName(decryptor.decrypt(response.getUserName()));
return response;
}
public TransferQueryResponse queryTransferByTransferBillNo(String transferBillNo){
String requestPath =
"https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/transfer-bill-no/{transfer_bill_no}";
requestPath =
requestPath.replace("{" + "transfer_bill_no" + "}", transferBillNo);
TransferQueryResponse response = executeHttpRequest(requestPath, HttpMethod.GET, null, TransferQueryResponse.class);
response.setUserName(decryptor.decrypt(response.getUserName()));
return response;
}
public <T> T executeHttpRequest(String requestPath,HttpMethod method,String body,Class<T> responseClass){
HttpHeaders headers = new HttpHeaders();
headers.addHeader(Constant.ACCEPT, MediaType.APPLICATION_JSON.getValue());
headers.addHeader(Constant.CONTENT_TYPE, MediaType.APPLICATION_JSON.getValue());
HttpRequest httpRequest =
new HttpRequest.Builder()
.httpMethod(method)
.url(requestPath)
.headers(headers)
.body(StringUtils.hasText(body)?new JsonRequestBody.Builder().body(body).build():null)
.build();
HttpResponse<T> httpResponse =
httpClient.execute(httpRequest, responseClass);
return httpResponse.getServiceResponse();
}
}
四、配置注入(单商户模式只需完成Config配置类注册即可调用)
非常重要****如果是24年11月以后注册的商户,请检查一下商户后台配置,评论区有兄弟提到平台证书模式可能新商户不支持
账户中心->API安全
根据自己的商户号证书类型,按照以下配置类类型注入到容器
1、使用公钥(注册以下配置类):
// 可以根据实际情况使用publicKeyFromPath或publicKey加载公钥
Config config =
new RSAPublicKeyConfig.Builder()
.merchantId("1900007291") //微信支付的商户号
.privateKeyFromPath("/Users/yourname/yourpath/apiclient_key.pem") // 商户API证书私钥的存放路径
.publicKeyFromPath("/Users/yourname/yourpath/pub_key.pem") //微信支付公钥的存放路径
.publicKeyId("PUB_KEY_ID_00000000000000000000000000000000") //微信支付公钥ID
.merchantSerialNumber("5157F09EFDC096DE15EBE81A47057A72********") //商户API证书序列号
.apiV3Key("F09E**") //APIv3密钥
.build();
2、使用平台证书:
Config rsaConfig = new RSAAutoCertificateConfig.Builder()
.merchantId(mchid)
.privateKeyFromPath(privateKeyFromPath)
.merchantSerialNumber(serialNumber)
.apiV3Key(apiV3Key)
.build();
单商户号可直接引入TransferNewService即可调用。
多商户或需要微信多执行器的朋友,需要单独开一个配置类,并且注入多个上图配置(或实现动态注册bean)
@Slf4j
@Component
public class WechatConfigManage {
private final Map<String, JsapiServiceExtension> paymentServiceMap;
private final Map<String, TransferNewService> transferNewServiceMap;
private final Map<String, RefundService> refundServiceMap;
private final Map<String, ServiceorderService> serviceorderServiceMap;
private final Map<String, Config> mchConfigs;
public WechatConfigManage(ApplicationContext applicationContext) {
//获取IOC容器中所有RSA配置类
mchConfigs = new HashMap<>();
Map<String, Config> configs = applicationContext.getBeansOfType(Config.class);
paymentServiceMap = new HashMap<>();
transferNewServiceMap = new HashMap<>();
refundServiceMap = new HashMap<>();
serviceorderServiceMap = new HashMap<>();
//根据不同的RSAConfig生成不同的service (key=mchid,value=service实例)
//不需要其它service的请删除其它service相关代码(serviceorderService也是自定义的微信支付分的)
for (String key : configs.keySet()) {
Config config = configs.get(key);
String mchid = config.createCredential().getMerchantId();
mchConfigs.put(mchid, config);
paymentServiceMap.put(mchid, new JsapiServiceExtension.Builder().config(config).build());
transferNewServiceMap.put(mchid, new TransferNewService(config));
refundServiceMap.put(mchid, new RefundService.Builder().config(config).build());
serviceorderServiceMap.put(mchid, new ServiceorderService(config));
}
}
/**
* 根据获取微信支付配置信息类(用于生成异步回调parser)
* @param mchid 商户号
* @return 配置信息
*/
public Config getRSAAConfig(String mchid){
return this.mchConfigs.get(mchid);
}
/**
* 获取商家转账service
* @param mchid 商户号
* @return service
*/
public TransferNewService getTransferService(String mchid){
TransferNewService service = this.transferNewServiceMap.get(mchid);
if (service==null) throw new PayException(PayExceptionCode.WECHAT_PAY_CONFIG_ERROR);
return service;
}
}
五、内部调用(请完整填写封装的请求参数,仅示范发起转账调用)
1、单商户模式直接引入TransferNewService
2、多商户模式:
六、异步回调(只要引入了第一步的依赖,以下方法适用于任何微信的异步回调)
1、业务逻辑请自己实现,因为流程较之前的微信转账到零钱有变化,一定要判断好异步回调返回的状态
2、如果多商户配置,请在发起转账请求时在异步回调地址用路径传参带上商户号(或者可以自定义商户号键值)
3、new NotificationParser(config),参数请按照注册的配置类类型强转(详情见第四步)
@Override
public void wechatTransferNotify(String mchid) {
HttpServletResponse response = ServletUtils.getResponse();
assert response != null;
try {
// 以支付通知回调为例,验签、解密并转换成 Transaction
RequestParam param = getRequestParam();
// 以下强转类型 请按照自己的公钥或证书配置类类型来转换
TransferNotification transfer = new NotificationParser((RSAAutoCertificateConfig)wechatConfigManage.getRSAAConfig(mchid)).parse(param, TransferNotification.class);
log.info("[微信转账异步回调] transfer:{}",transfer);
} catch (ValidationException e) {
// 签名验证失败,返回 401 UNAUTHORIZED 状态码
log.error("微信签名验证失败", e);
response.setStatus(401);
} catch (Exception e){
log.error("系统异常",e);
response.setStatus(500);
}
}
/**
* 获取请求参数 请求体-json(未json解析之前的原主体)+请求头
* @return 请求参数
*/
private RequestParam getRequestParam(){
HttpServletRequest request = ServletUtils.getRequest();
assert request != null;
String body = getReqStreamContent(request);
return new RequestParam.Builder()
.serialNumber(request.getHeader("Wechatpay-Serial"))
.nonce(request.getHeader("Wechatpay-Nonce"))
.signature(request.getHeader("Wechatpay-Signature"))
.timestamp(request.getHeader("Wechatpay-Timestamp"))
.body(body)
.build();
}
/**
* 获取请求流中的数据请求数据
* @param req 请求对象
* @return String
*/
public static String getReqStreamContent(HttpServletRequest req) {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(req.getInputStream(), StandardCharsets.UTF_8));
String line ;
StringBuilder sb = new StringBuilder();
while ((line = br.readLine()) != null){
sb.append(line);
}
br.close();
return sb.toString();
} catch (IOException e) {
log.error("[WechatUtils getReqStreamContent] get req fail req:{} error message:{}" ,req,e.getMessage());
}
return "fail";
}
写在最后:
之前对接了微信转账到零钱的朋友请注意,流程变化很大,发起转账成功以后,请一定要保存好返回的packageInfo数据,前端需要通过此数据,给用户弹出确认提示,且24小时内不确认会自动过期。