1.新商户说明
新商户现在注册是没有以前的商家转账到零钱了,没有这个功能的人不要尝试用商家转账到零钱的接口,因为用到最后会发现,没有权限。本人踩过这坑。新商户用最新的商家转账,旧商户有商家转账到零钱的可以用。
接下来我会用最新的2025年的商家转账功能。
2.密匙、证书及权限申请开通
1.最细节最新注册开通方法参考官方文档(从产品介绍——>权限申请):产品介绍_商家转账|微信支付商户文档中心
2.本人开通说明(也是参考官方文档开通的,但是更简洁)
- 申请官网:微信支付 - 中国领先的第三方支付平台 | 微信支付提供安全快捷的支付方式
- 进入账户中心->api管理
- 点击进入商户api证书->点击申请新证书->按照提示一步步注册
注意:记得保存证书的key.pem、cert.pem、p12证书、证书序列号(要用后面)
- 点击设置APIv2密匙->输入自定义的32位密匙
- 点击平台证书查看
- 点击设置APIv3密匙->输入自定义的32位密匙
到这一步代表证书及密匙设置完了,接下来还差权限的开通。
- 商家转账权限申请(产品中心)
- 点击商家转账申请开通->按照提示一步步注册就好
注意选择转账场景,按实际需求选。
- 注册完成界面
这里特别记住转账场景,我选的是现金营销 ID:1000,这个ID要记住后面要用
- 往下滑配置商家转账接口ip->进入点击添加自己服务的ip就行
注意这边必须加,不然调用退款的时候会报错提示此IP地址不允许调用该接口
商家转账到零钱接口 提示此IP地址不允许调用该接口? | 微信开放社区
这样就注册及申请密匙、证书、权限完毕,记住这些配置后面调用退款的时候要用。
3.java代码编写
1.导入pom(注意版本4.7.2.B,要高于或者等于这个,因为当前最新的商家转账api是2025-01-15日最新更新的)
maven仓库:https://mvnrepository.com/artifact/com.github.binarywang/wx-java
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>4.7.2.B</version>
</dependency>
2.创建WxPay配置bean
@Data
public class WxPay {
/**
* 设置微信公众号或者小程序等的appid
*/
private String appId;
/**
* 微信支付商户号
*/
private String mchId;
/**
* 微信支付商户密钥
*/
private String mchKey;
/**
* 服务商模式下的子商户公众账号ID,普通模式请不要配置,请在配置文件中将对应项删除
*/
private String subAppId;
/**
* 服务商模式下的子商户号,普通模式请不要配置,最好是请在配置文件中将对应项删除
*/
private String subMchId;
/**
* apiclient_cert.p12文件的绝对路径,或者如果放在项目中,请以classpath:开头指定
*/
private String keyPath;
private String apiV3Key;
private String certSerialNo;
private String privateKeyPath;
private String privateCertPath;
private String notifyUrl;
private String url;
private String accessToken;
private String appSecret;
private String refundNotify;
/**
* 通知模版,参考:shop.properties
*/
private String[] templateIds;
}
3.配置 application.yml
#微信支付
shop:
wxPay:
#小程序appId
appId: wxfxxxxx
#小程序密钥
appSecret: dxxxxxxxxx
#商户号
mchId: 1xxxxx
#商户密匙v2
mchKey: Txxxxxxxxx
#apiV3 秘钥
apiV3Key: xxxxxxxxxxxx
#商户证书序列号
certSerialNo: 7xxxxxxxxxxxxxxx
# 网站域名
url: https://xxxxx
#微信支付p12证书
keyPath: opt/pem/fd_apiclient_cert.p12
#生产 apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径 E:/work/project/ljf/3-java/orderdeliver/orderdeliver-admin/src/main/resources/pem/apiclient_key-jurongzhipin.pem
#/root/soft/apiclient_key_fuda.pem /opt/key/apiclient_key_fuda.pem
privateKeyPath: opt/pem/fd_apiclient_key.pem
#生产 apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径 /root/soft/apiclient_cert_fuda.pem /opt/key/apiclient_cert_fuda.pem
privateCertPath: opt/pem/fd_apiclient_cert.pem
# 微信支付异步回调本系统地址,通知url必须为直接可访问的url,不能携带参数
notifyUrl: /order/wx/payNotify
# 微信退款结果通知本系统地址,通知url必须为直接可访问的url,不能携带参数
refundNotify: /order/wx/refundNotify
# 通过code获取openid
accessToken: https://api.weixin.qq.com/sns/jscode2session?appid=
# 退款通知
templateIds[0]: bAj_qKhXOTS4eMyzVUgvUDXmjmH5Tt0XRqvp-6OuZOw
# 订单配送通知
templateIds[1]: uRmCdeh18sb5JmnqIGuV_ZSHsWtkVpkiGmil1JgGr-4
4.创建YamlPropertySourceFactory用于对象方式读取application.yml的配置
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.io.support.DefaultPropertySourceFactory;
import org.springframework.core.io.support.EncodedResource;
import java.io.IOException;
import java.util.Properties;
/**
* yml配置文件读取工厂方法重写
* @author laijiangfeng
* @date 2024/6/24 16:36
*/
public class YamlPropertySourceFactory extends DefaultPropertySourceFactory {
@Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
String sourceName = name != null ? name : resource.getResource().getFilename();
if (!resource.getResource().exists()) {
return new PropertiesPropertySource(sourceName, new Properties());
} else if (sourceName.endsWith(".yml") || sourceName.endsWith(".yaml")) {
Properties propertiesFromYaml = loadYml(resource);
return new PropertiesPropertySource(sourceName, propertiesFromYaml);
} else {
return super.createPropertySource(name, resource);
}
}
private Properties loadYml(EncodedResource resource) throws IOException {
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(resource.getResource());
factory.afterPropertiesSet();
return factory.getObject();
}
}
5.创建ShopBasicConfig用于读取application.yml的配置
@Data
@Component
@PropertySource(value = {"classpath:application.yml"},factory = YamlPropertySourceFactory.class)
@ConfigurationProperties(prefix = "shop")
public class ShopBasicConfig {
/**
* 微信支付配置
*/
private WxPay wxPay;
}
6.注册WxPayService bean到spring容器
@Configuration
@AllArgsConstructor
@ConditionalOnClass(WxPayService.class)
public class ShopBeanConfig {
private final ShopBasicConfig shopBasicConfig;
@Bean
public WxPay wxPay() {
return shopBasicConfig.getWxPay();
}
@Bean
public WxPayService wxService() {
WxPay wxPay = shopBasicConfig.getWxPay();
WxPayConfig payConfig = new WxPayConfig();
payConfig.setAppId(StringUtils.trimToNull(wxPay.getAppId()));
payConfig.setMchId(StringUtils.trimToNull(wxPay.getMchId()));
payConfig.setMchKey(StringUtils.trimToNull(wxPay.getMchKey()));
payConfig.setSubAppId(StringUtils.trimToNull(wxPay.getSubAppId()));
payConfig.setSubMchId(StringUtils.trimToNull(wxPay.getSubMchId()));
payConfig.setKeyPath(StringUtils.trimToNull(wxPay.getKeyPath()));
payConfig.setApiV3Key(StringUtils.trimToNull(wxPay.getApiV3Key()));
payConfig.setCertSerialNo(StringUtils.trimToNull(wxPay.getCertSerialNo()));
payConfig.setPrivateKeyPath(StringUtils.trimToNull(wxPay.getPrivateKeyPath()));
payConfig.setPrivateCertPath(StringUtils.trimToNull(wxPay.getPrivateCertPath()));
payConfig.setNotifyUrl(StringUtils.trimToNull(wxPay.getUrl())
+ StringUtils.trimToNull(wxPay.getNotifyUrl()));
// 可以指定是否使用沙箱环境
payConfig.setUseSandboxEnv(false);
WxPayService wxPayService = new WxPayServiceImpl();
wxPayService.setConfig(payConfig);
return wxPayService;
}
}
7.编写测试接口
参数说明参考官方文档:发起转账_商家转账|微信支付商户文档中心
/**
* @author laijiangfeng
* @date 2024/6/24 17:04
*/
@RestController
@RequestMapping("/order/wx")
public class WxController {
private static final Logger loggerPay = LoggerFactory.getLogger("sys-pay");
private final ReentrantLock transferNotifyLock = new ReentrantLock();
@Autowired
private WxPay wxPay;
@Autowired
private WxPayService wxPayService;
@RequestMapping("/entPay")
@ApiOperation(value = "商家转账给用户")
public TransferBillsResult entPay(){
// 创建请求参数
TransferBillsRequest request = new TransferBillsRequest();
// 设置商户关联的appid,微信公众号、小程序id
request.setAppid("wxxxxxx");
// 设置商户订单号,需保持唯一性,由数字字母组成
request.setOutBillNo("test"+System.currentTimeMillis());
// 设置转账场景ID
request.setTransferSceneId("1000");
// 设置收款方真实姓名,转账金额 >= 2,000元时,该笔明细必须填写
//request.setUserName("赖xx");
// 设置Openid,必须为关联appid的小程序或者公众号用户的openid
request.setOpenid("o6xxxxxx");
// 转账备注,用户收款时可见该备注信息,UTF8编码,最多允许32个字符
request.setTransferRemark("退款");
// 转账金额,单位为分
request.setTransferAmount(30);
// 转账成功回调接口
//request.setNotifyUrl();
// 用户收款时感知到的收款原因将根据转账场景自动展示默认内容
List<TransferBillsRequest.TransferSceneReportInfo> transferSceneReportInfos= new ArrayList<>();
//创建转账场景信息对象
TransferBillsRequest.TransferSceneReportInfo.TransferSceneReportInfoBuilder newBuilder = TransferBillsRequest.TransferSceneReportInfo.newBuilder();
TransferBillsRequest.TransferSceneReportInfo info = newBuilder.build();
//设置转账场景信息类型,这个要特别注意,要和商户设置的场景ID对应
info.setInfoType("活动名称");
//设置转账场景信息内容
info.setInfoContent("退款");
transferSceneReportInfos.add(info);
request.setTransferSceneReportInfos(transferSceneReportInfos);
try {
// 调用转账接口
TransferBillsResult result = wxPayService.getTransferService().transferBills(request);
return result;
} catch (WxPayException e) {
e.printStackTrace();
}
return null;
}
@RequestMapping("/cancelTransfer/{outBillNo}")
@ApiOperation(value = "撤销转账")
public TransferBillsCancelResult cancelTransfer(@PathVariable("outBillNo") String outBillNo){
try {
// 调用转账接口
TransferBillsCancelResult result = wxPayService.getTransferService().transformBillsCancel(outBillNo);
return result;
} catch (WxPayException e) {
e.printStackTrace();
}
return null;
}
/**
* 查询转账订单
* @param outBillNo 商户订单号
* @return 查询转账订单结果
*/
@RequestMapping("/getTransferOrder/{outBillNo}")
@ApiOperation(value = "查询转账订单")
public TransferBillsGetResult getTransferOrder(@PathVariable("outBillNo") String outBillNo){
try {
TransferBillsGetResult result = wxPayService.getTransferService().getBillsByOutBillNo(outBillNo);
return result;
} catch (WxPayException e) {
e.printStackTrace();
}
return null;
}
/**
* 商家转账回调通知
* @return 转账回调通知结果字符串
*/
@RequestMapping("/payTransferNotify")
@ApiOperation(value = "处理商家转账回调通")
public String payTransferNotify(HttpServletRequest request){
loggerPay.info("回调通知payNotify开始");
SignatureHeader header = new SignatureHeader();
header.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
header.setNonce(request.getHeader("Wechatpay-Nonce"));
header.setSignature(request.getHeader("Wechatpay-Signature"));
header.setSerial(request.getHeader("Wechatpay-Serial"));
String notifyData = fetchRequest2Str(request);
loggerPay.info("SignatureHeader:" + JSONUtil.toJsonStr(header));
loggerPay.info("notifyData:" + notifyData);
if (transferNotifyLock.tryLock()) {// 获取锁
try {
TransferBillsNotifyResult transferBillsNotifyResult = wxPayService.getTransferService().parseTransferBillsNotifyResult(notifyData, header);
loggerPay.info("TransferBillsNotifyResult:" + JSONUtil.toJsonStr(transferBillsNotifyResult));
// 获取基本信息
String state = transferBillsNotifyResult.getResult().getState();
if (WxPayConstants.TransformBillState.SUCCESS.equals(state)) {
loggerPay.info("转账成功");
return WxPayNotifyV3Response.success("转账成功");
} else if (WxPayConstants.TransformBillState.CANCELING.equals(state)) {
loggerPay.info("转账已撤销");
return WxPayNotifyV3Response.success("转账已撤销");
}else if (WxPayConstants.TransformBillState.FAIL.equals(state)) {
loggerPay.info("转账失败");
return WxPayNotifyV3Response.success("转账失败");
}
} catch (Exception e) {
e.printStackTrace();
loggerPay.info("回调通知payNotify失败 " + e.getMessage());
return WxPayNotifyV3Response.fail("失败");
} finally {
transferNotifyLock.unlock();// 释放锁
}
} else {
loggerPay.info("锁获取失败");
return WxPayNotifyV3Response.fail("锁获取失败");
}
loggerPay.info("回调通知payNotify结束:失败");
return WxPayNotifyV3Response.fail("失败");
}
/**
* 读取请求流数据
* @param request 请求对象
* @return 请求流数据字符串
*/
public String fetchRequest2Str(HttpServletRequest request) {
String reqStr = null;
BufferedReader streamReader = null;
try {
streamReader = new BufferedReader(new InputStreamReader(request.getInputStream(), "UTF-8"));
StringBuilder responseStrBuilder = new StringBuilder();
String inputStr;
while ((inputStr = streamReader.readLine()) != null) {
responseStrBuilder.append(inputStr);
}
reqStr = responseStrBuilder.toString();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (streamReader != null) {
streamReader.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return reqStr;
}
}
8.调用结果
4.遇到的问题解决
问题1:报错v3请求构造异常 Illegal key size
解决办法1:将项目jdk升级到15以上,本人升级到17得到解决。
问题2:Code=INVALID_REQUEST,ErrorMsg=此IP地址不允许调用该接口
解决办法:商户转账白名单ip未设置,参考上面第二步的ip设置,设置成你的公网ip
问题3:需要传入转账场景报备信息,请检查
解决办法:注意你注册的时候选的转账场景及ID,例如的注册的场景是现金营销ID:1000,代码中transferSceneId就是设置为1000,且InfoType必须为活动名称或奖励说明
转账场景参考官方文档第3点:产品介绍_商家转账|微信支付商户文档中心
更多细节请参考官方文档:产品介绍_商家转账|微信支付商户文档中心
调用weixin-java-pay jar请参考:https://github.com/binarywang/WxJava
调用类及方法请参考文档(可能未更新最新api):com.github.binarywang.wxpay.service (WxJava - PAY Java SDK 4.7.0 API)
5.小程序对接测试
html代码
<view class='container'>
<button class="listbtn" bindtap="confirmPay">收款</button>
</view>
js代码
confirmPay(){
request.request({
url: getApp()._url._url + 'order/wx/entPay',
header:{
"content-Type": "application/x-www-form-urlencoded",
"Authorization": 'Bearer '+ wx.getStorageSync("token")
},
method: "post",
data: {},
success: function(res) {
console.log(res)
if (wx.canIUse('requestMerchantTransfer')) {
wx.requestMerchantTransfer({
mchId: 'xxxx',
appId: 'xxxxxx',
package: res.data.packageInfo,
success: (res) => {
// res.err_msg将在页面展示成功后返回应用时返回ok,并不代表付款成功
console.log('success:', res);
},
fail: (res) => {
console.log('fail:', res);
},
});
} else {
wx.showModal({
content: '你的微信版本过低,请更新至最新版本。',
showCancel: false,
});
}
},
error: function(res) {
}
})
}
调用结果