微信支付——商家转账

1.新商户说明

        新商户现在注册是没有以前的商家转账到零钱了,没有这个功能的人不要尝试用商家转账到零钱的接口,因为用到最后会发现,没有权限。本人踩过这坑。新商户用最新的商家转账旧商户有商家转账到零钱的可以用。

        

接下来我会用最新的2025年的商家转账功能。

2.密匙、证书及权限申请开通

1.最细节最新注册开通方法参考官方文档(从产品介绍——>权限申请):产品介绍_商家转账|微信支付商户文档中心

2.本人开通说明(也是参考官方文档开通的,但是更简洁)

  • 点击进入商户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参考官网解决:https://github.com/binarywang/WxJava/wiki/%E5%8A%A0%E8%A7%A3%E5%AF%86%E7%9A%84%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86%E5%8A%9E%E6%B3%95

问题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) {
      }
    })
  }

调用结果

### Java 实现微信商家转账 API #### 准备工作 为了能够成功调用微信支付的批量转账功能,开发者要完成一系列准备工作。这包括但不限于注册成为微信支付的服务商并获得相应的商户号以及密钥等必要凭证[^2]。 #### 构建求参数 当准备就绪之后,在执行转账操作之前还要构建必要的求参数。这些参数通常涉及接收方的信息、金额以及其他可能影响交易处理的数据字段。对于具体的参数列表及其含义应当参照官方的产品文档来确保准确性[^1]。 #### 示例代码展示 下面给出一段用于实现日期格式转换的方法`getDateByRFC3339()`作为辅助函数的一部分,该函数会将给定的时间字符串按照指定格式进行重写以便于后续使用: ```java public static String getDateByRFC3339(String time){ DateTime dateTime = new DateTime(time); long timeInMillis = dateTime.toCalendar(Locale.getDefault()).getTimeInMillis(); Date date = new Date(timeInMillis); SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmsss"); String formattedTime = format.format(date); return formattedTime; } ``` 此段代码并非直接参与转账过程的核心逻辑,而是展示了如何利用Java处理时间戳的一种方式,这对于某些场景下的应用可能是有用的前置步骤之一[^3]。 #### 发起转账求 实际发起转账时,则依照API规定组装完整的HTTP POST求,其中包含了上述提到的各种必项,并通过HTTPS协议提交至微信服务器端口。注意此时应采用最新的安全标准如TLS 1.2以上版本以保障通信的安全性。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值