行业知识
微信小程序商家转账到零钱【java】
2026-10-20 02:29:31
来源:互联网转载
微信小程序商家转账到零钱
- 微信支付平台 开通权限
- 开发文档
- 代码
- pom 文件
- HttpUtil
- WechatPayV3Util
- ApiUrl
- 提现记录表
- controller 商家转账到零钱
- ServiceImpl
- controller 通过商家明细单号查询明细单
- ServiceImpl
微信支付平台 开通权限
商家转账到零钱官���简介
登录微信支付平台 点击产品中心
产品大全 -> 我的 -> 运营工具
然后根据要求 上传资料 提交开通申请 他说得是7-15天但是快一点也就两三天之内就有结果了
开通之后需要设置一些东西
点击前往功能
- 设置转账账户
- 设置转账发起方式 有两种 页面发起 && API发起
- 设置转账配置 转账额度啥的
- 重要: 设置访问IP 就是白名单的意思 如果本地的话查看外网IP(http://ifconfig.me ),服务器的自己找,hhhc(公司应该有记录)
开发文档
ps:很好找的其实,我刚开始还找了半天,哎
微信支付文档中心
进入之后点击 API文档
到下面这个页面(把目录都收起来就好找了)
然后点击运营工具就看见了
- emmm 再多写点吧 转账API 地址 和 查询API 地址也给你吧
都在这了,自己拿吧hhh
商家转账到零钱API传送门
通过商家批次单号查询转账结果传送门
ps :okok 好人做到底了
代码
pom 文件
com.github.wechatpay-apiv3 wechatpay-apache-httpclient 0.4.7
HttpUtil
import lombok.extern.slf4j.Slf4j;import org.apache.http.HttpEntity;import org.apache.http.client.methods.CloseableHttpResponse;import org.apache.http.client.methods.HttpGet;import org.apache.http.client.methods.HttpPost;import org.apache.http.entity.StringEntity;import org.apache.http.impl.client.CloseableHttpClient;import org.apache.http.impl.client.HttpClients;import org.apache.http.util.EntityUtils;import java.io.IOException;import java.util.Arrays;import static com.wechat.pay.contrib.apache.httpclient.constant.WechatPayHttpHeaders.WECHAT_PAY_SERIAL;import static org.apache.http.HttpHeaders.ACCEPT;import static org.apache.http.HttpHeaders.CONTENT_TYPE;import static org.apache.http.entity.ContentType.APPLICATION_JSON;/** * @Author: 小新 * @Date: 2023/12/29 10:01 * 微信支付专用类 请求操作方法 */@Slf4jpublic class HttpUtil { /** * 发起批量转账API 批量转账到零钱 * * @param requestUrl * @param requestJson 组合参数 * @param wechatPayserialNo 商户证书序列号 * @param mchID 商户号 * @param privatekeypath 商户私钥证书路径 * @return */ public static String postTransBatRequest( String requestUrl, String requestJson, String wechatPayserialNo, String mchID4M, String privatekeypath) { CloseableHttpClient httpclient = HttpClients.createDefault(); CloseableHttpResponse response = null; HttpEntity entity = null; try { HttpPost httpPost = new HttpPost(requestUrl); // NOTE: 建议指定charset=utf-8。低于4.4.6版本的HttpCore,不能正确的设置字符集,可能导致签名错误 httpPost.addHeader("Content-Type", "application/json"); httpPost.addHeader("Accept", "application/json"); httpPost.addHeader("Wechatpay-Serial", wechatPayserialNo); /*****************核心认证 start*****************/ String strToken = WechatPayV3Util.getToken("POST", "/v3/transfer/batches", requestJson,mchID4M,wechatPayserialNo, privatekeypath); log.error("微信转账token "+strToken); // 添加认证信息 httpPost.addHeader("Authorization", "WECHATPAY2-SHA256-RSA2048" + " " + strToken); /*****************核心认证 end*****************/ httpPost.setEntity(new StringEntity(requestJson, "UTF-8")); //发起转账请求 response = httpclient.execute(httpPost); entity = response.getEntity();//获取返回的数据 log.info("返回请求ID :{} ", Arrays.toString(response.getHeaders("Request-ID"))); return EntityUtils.toString(entity); } catch (Exception e) { e.printStackTrace(); } finally { // 关闭流 try { if (httpclient != null) { httpclient.close(); } } catch (IOException e) { e.printStackTrace(); } } return null; } /** * 查询账单 * @param requestUrl 请求地址 * @param wechatPayserialNo 商户证书序列号 * @param privatekeypath 商户私钥证书路径 * @return */ public static String getTransBatRequest( String requestUrl, String wechatPayserialNo, String mchId, String privatekeypath, String servletPath){ CloseableHttpResponse response = null; HttpEntity entity = null; CloseableHttpClient httpClient = HttpClients.createDefault(); try { HttpGet httpGet = new HttpGet(requestUrl); // NOTE: 建议指定charset=utf-8。低于4.4.6版本的HttpCore,不能正确的设置字符集,可能导致签名错误 httpGet.addHeader("Content-Type", "application/json"); httpGet.addHeader("Accept", "application/json"); httpGet.addHeader("Wechatpay-Serial", wechatPayserialNo); /*****************核心认证 start*****************/ String strToken = null; try { strToken = WechatPayV3Util.getToken("GET", servletPath, "", mchId, wechatPayserialNo, privatekeypath); } catch (Exception e) { log.error("请求异常:{}", e.getMessage()); e.printStackTrace(); } log.info("token " + strToken); // 添加认证信息 httpGet.addHeader("Authorization", "WECHATPAY2-SHA256-RSA2048" + " " + strToken); /*****************核心认证 end*****************/ //发起查询请求 response = httpClient.execute(httpGet); log.info("响应: {}", response); entity = response.getEntity();//获取返回的数据 return EntityUtils.toString(entity); } catch (Exception e) { e.printStackTrace(); } finally { // 关闭流 try { if (httpClient != null) { httpClient.close(); } } catch (IOException e) { e.printStackTrace(); } } return null; }}WechatPayV3Util
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;import lombok.extern.slf4j.Slf4j;import org.apache.commons.codec.binary.Base64;import org.apache.commons.lang3.StringUtils;import java.io.*;import java.nio.charset.StandardCharsets;import java.nio.file.Files;import java.nio.file.Paths;import java.security.KeyFactory;import java.security.NoSuchAlgorithmException;import java.security.PrivateKey;import java.security.Signature;import java.security.cert.X509Certificate;import java.security.spec.InvalidKeySpecException;import java.security.spec.PKCS8EncodedKeySpec;import java.util.Random;/** * @Author: 小新 * @Date: 2023/12/29 9:58 */@Slf4jpublic class VechatPayV3Util { /** * @param method 请求方法 post * @param canonicalUrl 请求地址 * @param body 请求参数 * @param merchantId 商户号 * @param certSerialNo 商户证书序列号 * @param keyPath 私钥商户证书地址 * @return * @throws Exception */ public static String getToken( String method, String canonicalUrl, String body, String merchantId, String certSerialNo, String keyPath) throws Exception { String signStr = ""; //获取32位随机字符串 String nonceStr = getRandomString(32); //当前系统运行时间 long timestamp = System.currentTimeMillis() / 1000; if (StringUtils.isEmpty(body)) { body = ""; } String message = buildMessage(method, canonicalUrl, timestamp, nonceStr, body); //签名操作 //签名操作 String signature = sign(message.getBytes(StandardCharsets.UTF_8), keyPath); //组装参数 signStr = "mchid=\"" + merchantId + "\",timestamp=\"" + timestamp+ "\",nonce_str=\"" + nonceStr + "\",serial_no=\"" + certSerialNo + "\",signature=\"" + signature + "\""; return signStr; } public static String buildMessage(String method, String canonicalUrl, long timestamp, String nonceStr, String body) { return method + "\n" + canonicalUrl + "\n" + timestamp + "\n" + nonceStr + "\n" + body + "\n"; } public static String sign(byte[] message, String keyPath) throws Exception { Signature sign = Signature.getInstance("SHA256withRSA"); sign.initSign(getPrivateKey(keyPath)); sign.update(message); return Base64.encodeBase64String(sign.sign()); } /** * @param filename 私钥文件路径 (required) * @return 私钥对象 */ public static PrivateKey getPrivateKey(String filename) throws IOException { //todo 我放在项目里的 也可以放在服务器上 都可以 看自己需求了 String content = new String(Files.readAllBytes(Paths.get("src/main/resources" + filename)), "utf-8"); try { String privateKey = content.replace("-----BEGIN PRIVATE KEY-----", "") .replace("-----END PRIVATE KEY-----", "") .replaceAll("\\s+", ""); KeyFactory kf = KeyFactory.getInstance("RSA"); return kf.generatePrivate( new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey))); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("不支持RSA", e); } catch (InvalidKeySpecException e) { throw new RuntimeException("无效的密钥格式"); } } /** * 获取随机位数的字符串 * @param length 随机数长度 * @return */ public static String getRandomString(int length) { String base = "abcdefghijklmnopqrstuvwxyz0123456789"; Random random = new Random(); StringBuffer sb = new StringBuffer(); for (int i = 0; iApiUrl
/** * 第三方api地址 */public class ApiUrl {//ps: 当时申请商家转账还没通过的时候 想着先写代码,结果写错了,写成了企业转账到零钱,吐血 /** * 企业转账到零钱 */ //public static String WECHAT_TRANSFERS_URL = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers"; /** * 商家转账到零钱 */ public static String WECHAT_TRANSFERS_URL = "https://api.mch.weixin.qq.com/v3/transfer/batches"; /** * 商户批次号查询 */ public static String SELECT_ORDER_URL = "https://api.mch.weixin.qq.com/v3/transfer/batches/out-batch-no";}好像就没有了 对了 那个提现记录表也给你们吧 mysql的,看不上的你们就自己设计一个hhh
提现记录表
CREATE TABLE `tb_weixin_entpay_form` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID', `total_amount` decimal(19,2) DEFAULT NULL COMMENT '提现金额', `user_name` varchar(255) DEFAULT NULL COMMENT '用户名', `open_id` varchar(255) DEFAULT NULL COMMENT 'openid', `partner_trade_no` varchar(255) DEFAULT NULL COMMENT '提现单号', `progress` varchar(255) DEFAULT NULL COMMENT '提现进度 SUCCESS INPROGRESS FAIL', `wx_type` varchar(255) DEFAULT NULL COMMENT '类型', `user_id` bigint(20) DEFAULT NULL COMMENT '用户ID', `start_time` datetime DEFAULT NULL COMMENT '开始时间', `end_time` datetime DEFAULT NULL COMMENT '结束时间', `batch_id` varchar(255) DEFAULT NULL COMMENT '微信明细单号', PRIMARY KEY (`id`))comment '提现记录表';
下面的就是根据自己的业务来写了
controller 商家转账到零钱
@PostMapping("/transfer") public Result transfer(@RequestParam(name = "amount", required = true) String amount, HttpServletRequest request) { Long userId = (Long) request.getAttribute("userId"); return wxPayService.transfer(amount, userId, request); }ServiceImpl
@Value("${wx.WeChat.AppID}") private String AppId; @Value("${wx.WeChat.SerialNo}") private String SerialNo; // 支付证书序列号 @Value("${wx.WeChat.Privatekeypath}") private String Privatekeypath; // 我的是本地项目resource下的 /cert/apiclient_key.pem 这个 (别弄错了) // todo 当时因为这个项目 证书给我了但是我不知道是哪个序列号 因为有好多个 ,所以又找了个根据商户私钥来找序列号// 在哪找到的呢 emm 还是告诉你们吧,藏在代码里 应该看的人不多嘿嘿嘿//https://pay.weixin.qq.com/docs/merchant/development/interface-rules/signature-faqs.html/** 有需要的可以试试 1900009191_20180326_cert.pem 这个就是你本地存放地址了 window + R 就行* openssl x509 -noout -modulus -in 1900009191_20180326_cert.pemModulus=C6D43C87B991...openssl rsa -noout -modulus -in 1900009191_20180326_key.pemModulus=C6D43C87B991...*//** * 商家转账到零钱 * @param amount * @param userId * @param request * @return */ @Override @Transactional(rollbackFor = Exception.class) public Result transfer(String amount, Long userId, HttpServletRequest request) { // 查询当前用户 LbUser lbUser = lbUserMapper.selectById(userId); if (lbUser == null) { return new Result("400", "当前用户未注册"); } // 查询是否绑定微信 List userWxList = lbUserWxMapper.findByUserIdAndAppid(userId,AppId); if (userWxList.isEmpty()){ return new Result("400", "请先绑定微信"); } // 查询余额 List accountList = tbUserAccountMapper.selectList(new QueryWrapper().lambda().eq(TbUserAccount::getUserId, userId).orderByDesc(TbUserAccount::getId)); if (accountList.isEmpty()) { return new Result("400", "当前用户未登录"); } // 是否可以提现 TbUserAccount tbUserAccount = accountList.get(0); BigDecimal availableBalance = tbUserAccount.getAvailableBalance(); if (availableBalance.compareTo(new BigDecimal(amount))controller 通过商家明细单号查询明细单
@GetMapping("/getTransBat") public Result getTransBat(@RequestParam(name = "outBatchNo", required = true) String outBatchNo) { return wxPayService.getTransBat(outBatchNo); }ServiceImpl
@Overridepublic Result getTransBat(String outBatchNo) { String result = HttpUtil.getTransBatRequest( ApiUrl.SELECT_ORDER_URL+"/"+outBatchNo+"?need_query_detail=True&detail_status=ALL", SerialNo, wxPayConfig.getMchId(), Privatekeypath,"/v3/transfer/batches/out-batch-no"+"/"+outBatchNo+"?need_query_detail=True&detail_status=ALL"); JSONObject results = JSONObject.parseObject(result); return new Result("200",results); }/**{ "code": "200", "data": { "offset": 0, "transfer_detail_list": [ { "detail_status": "SUCCESS", // 这个就是返回的结果了 还有失败和进行中什么的 文档里都有,自己看吧 "out_detail_no": "不给看", "detail_id": "不给看" } ], "limit": 20, "transfer_batch": { "mchid": "不给看", "create_time": "也不给看", "success_amount": 不给看, "batch_id": "不给看", "success_num": 1, "batch_status": "FINISHED", "out_batch_no": "不给看", "batch_name": "提现", "update_time": "也不给看", "fail_amount": 0, "total_amount": 100,//分 "appid": "不给看", "total_num": 1, "batch_remark": "提现", "batch_type": "API", "fail_num": 0, "transfer_scene_id": "不给看" // 这是场景ID } }, "message": "操作成功"}*/跟转账的参数也大差不差 返回没处理 直接返回了 懒狗hhhhc
- okok 到这应该就没有了 ,对了,我还写了个定时器来查询结果更新一些数据的,但是我给公司留了不少坑hhhc,反正能用,你们看看乐子就行。
import com.alibaba.fastjson.JSONArray;import com.alibaba.fastjson.JSONObject;import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;import lombok.extern.slf4j.Slf4j;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Configuration;import org.springframework.scheduling.annotation.Scheduled;import org.springframework.transaction.annotation.Transactional;import javax.annotation.Resource;import java.math.BigDecimal;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Date;import java.util.List;/** * @Author: 小新 * @Date: 2023/12/29 16:05 */@Slf4j@Configurationpublic class WexinEntPayTask { @Resource private WxPayConfig wxPayConfig; @Value("${wx.WeChat.SerialNo}") private String SerialNo; @Value("${wx.WeChat.Privatekeypath}") private String Privatekeypath; @Resource private WeixinEntPayFormMapper weixinEntPayFormMapper; @Resource private LbUserMapper lbUserMapper; @Resource private TbUserAccountMapper tbUserAccountMapper; @Resource private LbUserLawyerMessageLogMapper lbUserLawyerMessageLogMapper; @Resource private UrlLinkUtil urlLinkUtil; @Resource private ShlianLuUtil shlianLuUtil; private static final String SUCCESS = "SUCCESS"; private static final String FAIL = "FAIL"; @Scheduled(cron = "0 */3 * * * ?") @Transactional void selectWexinEnPay() { log.info("==========开始查询转账记录================"); // 查询所有进行中的转账记录 List list = weixinEntPayFormMapper.selectList(new QueryWrapper().lambda().eq(WeixinEntPayForm::getProgress, "INPROGRESS")); if (list.isEmpty()) { log.info("没有转账记录"); return; } for (WeixinEntPayForm weixinEntPayForm : list) { LbUser user = lbUserMapper.selectById(weixinEntPayForm.getUserId()); if (user == null) { log.info("用户不存在"); continue; } try { String result = HttpUtil.getTransBatRequest(buildUrl(weixinEntPayForm), SerialNo, wxPayConfig.getMchId(), Privatekeypath, buildOutBatchNoUrl(weixinEntPayForm)); JSONObject results = JSONObject.parseObject(result); processTransferResult(weixinEntPayForm, user, results); log.info("=========={} 转账记录查询及后续操作完成================",weixinEntPayForm.getId()); } catch (Exception e) { log.error("查询转账记录出现错误:{}, 商户批次单号:{}", e.getMessage(), weixinEntPayForm.getPartnerTradeNo()); continue; } } } /** * 查询请求地址 * @param weixinEntPayForm * @return */ private String buildUrl(WeixinEntPayForm weixinEntPayForm) { return ApiUrl.SELECT_ORDER_URL + "/" + weixinEntPayForm.getPartnerTradeNo() + "?need_query_detail=True&detail_status=ALL"; } /** * 相对路径 * @param weixinEntPayForm * @return */ private String buildOutBatchNoUrl(WeixinEntPayForm weixinEntPayForm) { return "/v3/transfer/batches/out-batch-no" + "/" + weixinEntPayForm.getPartnerTradeNo() + "?need_query_detail=True&detail_status=ALL"; } /** * 根据请求结果 进行处理 * @param weixinEntPayForm 转账记录 * @param user 用户 * @param results 转账结果返回 * @throws ParseException */ private void processTransferResult(WeixinEntPayForm weixinEntPayForm, LbUser user, JSONObject results) throws ParseException { int updateAccount = 0; boolean flag = true; JSONArray transferDetailList = results.getJSONArray("transfer_detail_list"); String detailStatus = transferDetailList.getJSONObject(0).getString("detail_status"); if (SUCCESS.equals(detailStatus)) { // 转账成功 updateAccount = updateAccount(weixinEntPayForm); weixinEntPayForm.setProgress(SUCCESS); } else if (FAIL.equals(detailStatus)) { // 转账失败 weixinEntPayForm.setProgress(FAIL); } flag = flag && (updateAccount == 1); if (flag) { updateOrderAndSendMessage(weixinEntPayForm, user, results); } } /** * 更新余额 * @param weixinEntPayForm * @return */ private int updateAccount(WeixinEntPayForm weixinEntPayForm) { List accountList = tbUserAccountMapper.selectList(new QueryWrapper().lambda().eq(TbUserAccount::getUserId, weixinEntPayForm.getUserId()).orderByDesc(TbUserAccount::getId)); if (!accountList.isEmpty()) { TbUserAccount tbUserAccount = accountList.get(0); // 更新余额 BigDecimal subtract = tbUserAccount.getAvailableBalance().subtract(weixinEntPayForm.getTotalAmount()); tbUserAccount.setAvailableBalance(subtract); tbUserAccount.setBalance(subtract); return tbUserAccountMapper.updateById(tbUserAccount); } return 0; } /** * 更新 转账记录 和 发送消息 * @param weixinEntPayForm * @param user * @param results * @throws ParseException */ private void updateOrderAndSendMessage(WeixinEntPayForm weixinEntPayForm, LbUser user, JSONObject results) throws ParseException { // 处理时间 Date updateTime = parseUpdateTime(results); weixinEntPayForm.setEndTime(updateTime == null ? new Date() : updateTime); boolean flag = weixinEntPayFormMapper.updateById(weixinEntPayForm) == 1; try { LbUserLawyerMessageLog lbUserLawyerMessageLog = buildMessageLog(weixinEntPayForm, user); flag = flag && (lbUserLawyerMessageLogMapper.insert(lbUserLawyerMessageLog) == 1); } catch (Exception e) { log.info("存储消息失败 原因:{}", e.getMessage()); } if (flag) { if (SUCCESS.equals(weixinEntPayForm.getProgress())) { sendTransferSMS(user,SUCCESS); } else { sendTransferSMS(user,FAIL); } } else { log.info("转账短信发送失败"); } } /** * 处理时间 * @param results * @return * @throws ParseException */ private Date parseUpdateTime(JSONObject results) throws ParseException { String timeWithTimeZone = results.getJSONObject("transfer_batch").getString("update_time"); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); try { return dateFormat.parse(timeWithTimeZone); } catch (ParseException e) { log.info("日期处理失败"); return null; } } /** * 构建短信记录 * @param weixinEntPayForm * @param user * @return */ private LbUserLawyerMessageLog buildMessageLog(WeixinEntPayForm weixinEntPayForm, LbUser user) { LbUserLawyerMessageLog lbUserLawyerMessageLog = new LbUserLawyerMessageLog(); lbUserLawyerMessageLog.setUserId(weixinEntPayForm.getUserId()); lbUserLawyerMessageLog.setCreatedTime(new Date()); lbUserLawyerMessageLog.setTopicLawyerId(0L); lbUserLawyerMessageLog.setIdentityType(user.getIsAuth()); lbUserLawyerMessageLog.setReadIsNot(1); lbUserLawyerMessageLog.setMessageType(SUCCESS.equals(weixinEntPayForm.getProgress()) ? UserMessageType.TRANSFERSUCCESS : UserMessageType.TRANSFERFAIL); return lbUserLawyerMessageLog; } /** * 发送短信 * @param user * @param successOrFail */ private void sendTransferSMS(LbUser user,String successOrFail) { // 发送短信 String smsResult2 = shlianLuUtil.sendSMSTemplate(new String[]{user.getPhone()}, SUCCESS.equals(successOrFail) ? SmsTemplateIdUtil.transferSuccess : SmsTemplateIdUtil.transferFail, new String[]{ user.getNickname(), urlLinkUtil.jumpAppletShortUrl("")}); log.info("转账短信发送返回 smsResult2:{}", smsResult2); }}- 老板应该看不见吧 hhhc 这次就到这吧,家人们看看乐子。
- okok 到这应该就没有了 ,对了,我还写了个定时器来查询结果更新一些数据的,但是我给公司留了不少坑hhhc,反正能用,你们看看乐子就行。
- emmm 再多写点吧 转账API 地址 和 查询API 地址也给你吧
上一篇:lunix服务器怎么更改时间?
下一篇:dlink打印服务器如何使用?