发布时间:2025-12-09 11:51:15 浏览次数:2
付款码支付是指用户展示微信钱包内的“付款码”给商户系统扫描后直接完成支付,适用于线下场所面对面收银的场景,例如商超、便利店、餐饮、医院、学校、电影院和旅游景区等具有明确经营地址的实体场所
商户平台-产品中心-付款码支付-申请开通
用户打开付款码支付
线下收银员在自己的商户系统生成支付订单,商户系统发送支付请求
微信支付后台系统收到支付请求,根据验证密码规则判断是否验证用户的支付密码,不需要验证密码的交易直接发起扣款,需要验证密码的交易会弹出密码输入框。支付成功后微信端会弹出成功页面,支付失败会弹出错误提示
商户接入微信支付,调用API必须遵循以下规则:
传输方式:为保证交易安全性,采用HTTPS传输
提交方式:采用POST方法提交
数据格式:提交和返回数据都为XML格式,根节点名为xml
字符编码:统一采用UTF-8字符编码
签名算法:MD5,后续会兼容SHA1、SHA256、HMAC等。
签名要求:请求和接收数据均需要校验签名
证书要求:调用申请退款、撤销订单接口需要商户证书
判断逻辑:先判断协议字段返回,再判断业务返回,最后判断交易状态
注意
必须严格按照API的说明进行一单一支付,一单一红包,一单一付款,在未得到支付系统明确的回复之前不要换单,防止重复支付或者重复付款
根据商户具体的情况,付款码支付接入模式可分为:商户后台接入和门店接入;
根据用户是否需要输入支付密码可分为:免密模式和验密模式。
接入模式
商户后台接入
该模式适合具备统一后台系统的商户。门店收银台与商户后台通信,商户后台系统负责与微信支付系统发送交易请求和接收返回结果。
主要是商户通过扫描用户的付款码信息,商户后台生成支付订单 并向微信后台提交请求 ,微信后台向用户发送验证请求,用户填写密码支付后 ,微信后台同时向用户和商户后台发送支付成功信息
门店接入
该模式适合门店收银台通过公网直接与微信后台通信的商户。门店收银台直接发起交易请求和处理返回结果。商户可以根据实际需要,处理门店和商户后台系统之间的其它业务流程。
签名算法步骤
设所有发送或者接收到的数据为集合M,将集合M内非空参数值的参数按照参数名ASCII码从小到大排序(字典序),使用URL键值对的格式(即key1=value1&key2=value2…)拼接成字符串stringA
特别注意以下重要规则:
◆ 参数名ASCII码从小到大排序(字典序);
◆ 如果参数的值为空不参与签名;
◆ 参数名区分大小写;
◆ 验证调用返回或微信主动通知签名时,传送的sign参数不参与签名,将生成的签名与该sign值作校验。
◆ 微信接口可能增加字段,验证签名时必须支持增加的扩展字段
在stringA最后拼接上key得到stringSignTemp字符串,并对stringSignTemp进行MD5运算,再将得到的字符串所有字符转换为大写,得到sign值signValue( key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置)
假设传送的参数如下:
appid:wxd930ea5d5a258f4f // appid
mch_id:10000100 // 商户id
device_info:1000 //设备id
body:test //内容
nonce_str:ibuaiVcKdpRxkhJA //随机字符串
3.1: 对参数按照key=value的格式,并按照参数名ASCII字典序排序如下
stringA="appid=wxd930ea5d5a258f4f&body=test&device_info=1000&mch_id=10000100&nonce_str=ibuaiVcKdpRxkhJA";
3.2: 拼接API密钥
stringSignTemp=stringA+"&key=192006250b4c09247ec02edce69f6a2d" //注:key为商户平台设置的密钥key
sign=MD5(stringSignTemp).toUpperCase()="9A0A8659F005D6984697E2CA0A9CF3B7" //注:MD5签名方式
sign=hash_hmac("sha256",stringSignTemp,key).toUpperCase()="6A9AE1657590FD6257D693A078E1C3E4BB6BA4DC30B23E0EE2496E54170DACD6" //注:HMAC-SHA256签名方式
3.3: 最后发送的数据
<xml>
<appid>wxd930ea5d5a258f4f</appid>
<mch_id>10000100</mch_id>
<device_info>1000</device_info>
<body>test</body>
<nonce_str>ibuaiVcKdpRxkhJA</nonce_str>
<sign>9A0A8659F005D6984697E2CA0A9CF3B7</sign>
</xml>
准备工作
appId: 微信分配的应用ID
mchId: 微信支付分配的商户号
统一下单
收银员使用扫码设备读取微信用户付款码以后,二维码或条码信息会传送至商户收银台,由商户收银台或者商户后台调用该接口发起支付
2.1 请求url: https://api.mch.weixin.qq.com/pay/micropay
2.2 请求方式:post
2.3 数据格式:xml
2.4 请求参数
| appid | 微信分配的应用ID |
|---|---|
| mch_id | 微信支付分配的商户号 |
| nonce_str | 随机字符串 |
| sign | 签名 |
| body | 商品描述 |
| out_trade_no | 商户订单号 |
| total_fee | 总金额 |
| spbill_create_ip | 终端IP |
| auth_code | 付款码 |
请求实例
<xml>
<appid>wx2421b1c4370ec43b</appid>
<attach>订单额外描述</attach>
<auth_code>120269300684844649</auth_code>
<body>付款码支付测试</body>
<device_info>1000</device_info>
<goods_tag></goods_tag>
<mch_id>10000100</mch_id>
<sub_mch_id>10000101</sub_mch_id>
<nonce_str>8aaee146b1dee7cec9100add9b96cbe2</nonce_str>
<out_trade_no>1415757673</out_trade_no>
<spbill_create_ip>14.17.22.52</spbill_create_ip>
<time_expire></time_expire>
<total_fee>1</total_fee>
<sign>C29DB7DB1FD4136B84AE35604756362C</sign>
</xml>
<!--微信支付 -->
<dependency>
<groupId>com.github.javen205</groupId>
<artifactId>IJPay-WxPay</artifactId>
<version>2.7.1</version>
</dependency>
<!-- jfinal 默认工具 -->
<dependency>
<groupId>com.jfinal</groupId>
<artifactId>jfinal</artifactId>
<version>3.3</version>
</dependency>
<dependency>
<groupId>com.jfinal</groupId>
<artifactId>jetty-server</artifactId>
<version>8.1.8</version>
</dependency>
@Data
@Component
@ConfigurationProperties(prefix = "wx") // 从yml文件获取到配置信息
/**
* 功能描述:
* 〈微信常量配置类〉
* @author : hxz
* @date : 2021/2/5 11:04
*/
public class WxPayBean {
/**appId**/
private String appId;
/**秘钥**/
private String appSecret;
/**商户id**/
private String mchId;
/**商户支付秘钥**/
private String partnerKey;
/** 商户证书API 证书中的 p12绝对路径**/
private String certPath;
/** 证书的秘钥 默认为mchId**/
private String certPass;
}
server:
port: 9090
wx:
appId: ********
appSecret: ********
micropayUrl: ********
partnerKey: ********
mchId: ********
@RestController
@Slf4j
@RequestMapping("/wxPay")
public class WxPayController {
@Autowired
private WxPayBean wxPayBean;
private static final String USER_PAYING = "USERPAYING";
/**
* 功能描述:
* 〈付款码支付〉
* @param request 1
* @return : java.lang.String
* @author : hxz
* @date : 2021/2/5 11:30
*/
@RequestMapping(value = "/micropay", method = {RequestMethod.POST, RequestMethod.GET})
public Object microPay(HttpServletRequest request) {
//获取到付款码
String authCode = request.getParameter("auth_code");
//获取到金额
String totalFee = request.getParameter("total_fee");
if (StrKit.isBlank(totalFee)) {
return new RuntimeException("金额不能为空");
}
if (StrKit.isBlank(authCode)) {
return new RuntimeException("auth_code参数错误");
}
// 终端IP
String ip = IpKit.getRealIp(request);
if (StrKit.isBlank(ip)) {
ip = "127.0.0.1";
}
Map<String, String> params = MicroPayModel.builder()
.appid(wxPayBean.getAppId())
.mch_id(wxPayBean.getMchId())
.nonce_str(WxPayKit.generateStr())
.body("IJPay 让支付触手可及-刷卡支付")
.attach("Node.js 版:https://gitee.com/javen205/TNWXX")
.out_trade_no(WxPayKit.generateStr())
.total_fee("1")
.spbill_create_ip(ip)
.auth_code(authCode)
.build()
.creatSign(wxPayBean.getPartnerKey(), SignType.HMACSHA256);
String xmlResult = WxPayApi.micropay(false, params);
//同步返回结果
log.info("xmlResult:" + xmlResult);
Map<String, String> result = WxPayKit.xmlToMap(xmlResult);
String returnCode = result.get("return_code");
String returnMsg = result.get("return_msg");
if (!WxPayKit.codeIsOk(returnCode)) {
//通讯失败
String errCode = result.get("err_code");
if (StrKit.notBlank(errCode)) {
//用户支付中,需要输入密码
if (USER_PAYING.equals(errCode)) {
//等待5秒后调用【查询订单API】
}
}
log.info("提交刷卡支付失败>>" + xmlResult);
return new AjaxResult().addError(returnMsg);
}
String resultCode = result.get("result_code");
if (!WxPayKit.codeIsOk(resultCode)) {
log.info("支付失败>>" + xmlResult);
String errCodeDes = result.get("err_code_des");
return new AjaxResult().addError(errCodeDes);
}
//支付成功
return new AjaxResult().success(xmlResult);
}
}
| 返回状态码 | return_code | SUCCESS/FAIL 此字段是通信标识,非交易标识,交易是否成功需要查看result_code来判断 示例值:SUCCESS |
|---|---|---|
| 返回信息 | return_msg | 返回信息,如非空,为错误原因 签名失败 参数格式校验错误 示例值:签名失败 |
| USERPAYING | 用户支付中,需要输入密码 | 等待5秒,然后调用被扫订单结果查询API,查询当前订单的不同状态,决定下一步的操作。 |
|---|---|---|
该接口提供所有微信支付订单的查询,商户可以通过查询订单接口主动查询订单状态,完成下一步的业务逻辑
接口说明
请求url: https://api.mch.weixin.qq.com/pay/orderquery
请求方法: POST
数据格式:xml
请求参数
| 应用ID | appid | 微信分配的应用ID |
|---|---|---|
| 商户号 | mch_id | 微信支付分配的商户号 |
| 商户订单号 | out_trade_no | 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_- |
| 示例值:1217752501201407033233368018 | ||
| 或者 | ||
| 微信支付订单号 | transaction_id | 微信的订单号,优先使用 示例值:1217752501201407033233368018 |
| 随机字符串 | nonce_str | 随机字符串,不长于32位。推荐随机数生成算法 示例值:5K8264ILTKCH16CQ2502SI8ZNMTM67VS |
| 签名 | sign | 签名,详见签名生成算法 示例值:C380BEC2BFD727A4B6845133519F3AD6 |
请求实例
<xml>
<appid>wx2421b1c4370ec43b</appid>
<mch_id>10000100</mch_id>
<nonce_str>ec2316275641faa3aacf3cc599e8730f</nonce_str>
<transaction_id>1008450740201411110005820873</transaction_id>
<sign>FDD167FAA73459FD921B144BAF4F4CA2</sign>
</xml>:
代码
/**
* 功能描述:
* 〈查询接口〉
* @param transactionId 1
* @param outTradeNo 2
* @return : java.lang.String
* @author : hxz
* @date : 2021/2/5 16:13
*/
@RequestMapping(value = "/queryOrder", method = {RequestMethod.POST, RequestMethod.GET})
public String queryOrder(@RequestParam(value = "transactionId", required = false) String transactionId, @RequestParam(value = "outTradeNo", required = false) String outTradeNo) {
try {
Map<String, String> params = OrderQueryModel.builder()
.appid(wxPayBean.getAppId())
.mch_id(wxPayBean.getMchId())
// 微信的订单号,优先使用
.transaction_id(transactionId)
//订单号
.out_trade_no(outTradeNo)
.nonce_str(WxPayKit.generateStr())
.build()
.creatSign(wxPayBean.getPartnerKey(), SignType.MD5);
log.info("请求参数:{}", WxPayKit.toXml(params));
String query = WxPayApi.orderQuery(false,params);
log.info("查询结果: {}", query);
return query;
} catch (Exception e) {
e.printStackTrace();
return "系统错误";
}
}
返回参数看文档
支付交易返回失败或支付系统超时,调用该接口撤销交易。如果此订单用户支付失败,微信支付系统会将此订单关闭;如果用户支付成功,微信支付系统会将此订单资金退还给用户
一般使用定时任务查询订单中超时或者支付失败的数据进行订单撤销
接口说明
请求url: https://api.mch.weixin.qq.com/secapi/pay/reverse
请求方法: POST
数据格式:xml
请求参数
| 应用ID | appid | 微信分配的应用ID |
|---|---|---|
| 商户号 | mch_id | 微信支付分配的商户号 |
| 商户订单号 | out_trade_no | 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_- |
| 示例值:1217752501201407033233368018 | ||
| 或者 | ||
| 微信支付订单号 | transaction_id | 微信的订单号,优先使用 示例值:1217752501201407033233368018 |
| 随机字符串 | nonce_str | 随机字符串,不长于32位。推荐随机数生成算法 示例值:5K8264ILTKCH16CQ2502SI8ZNMTM67VS |
| 签名 | sign | 签名,详见签名生成算法 示例值:C380BEC2BFD727A4B6845133519F3AD6 |
请求实例
<xml>
<appid>wx2421b1c4370ec43b</appid>
<mch_id>10000100</mch_id>
<nonce_str>ec2316275641faa3aacf3cc599e8730f</nonce_str>
<transaction_id>1008450740201411110005820873</transaction_id>
<sign>FDD167FAA73459FD921B144BAF4F4CA2</sign>
</xml>
代码 这里撤销是用证书的 文档中 不需要证书 所以可以根据需求更改方法orderReverse()
/**
* 功能描述:
* 〈订单撤销〉
* @param transactionId 1
* @param outTradeNo 2
* @return : java.lang.String
* @author : hxz
* @date : 2021/2/5 16:57
*/
@RequestMapping(value = "/orderReverse", method = {RequestMethod.POST, RequestMethod.GET})
public String orderReverse(@RequestParam(value = "transactionId", required = false) String transactionId, @RequestParam(value = "outTradeNo", required = false) String outTradeNo) {
try {
Map<String, String> params = OrderQueryModel.builder()
.appid(wxPayBean.getAppId())
.mch_id(wxPayBean.getMchId())
// 微信的订单号,优先使用
.transaction_id(transactionId)
//订单号
.out_trade_no(outTradeNo)
.nonce_str(WxPayKit.generateStr())
.build()
.creatSign(wxPayBean.getPartnerKey(), SignType.MD5);
log.info("请求参数:{}", WxPayKit.toXml(params));
//CertPass 证书的秘钥
String query = WxPayApi.orderReverse(params,wxPayBean.getCertPath(),wxPayBean.getMchId());
log.info("查询结果: {}", query);
return query;
} catch (Exception e) {
e.printStackTrace();
return "系统错误";
}
}
当交易发生之后一段时间内,由于买家或者卖家的原因需要退款时,卖家可以通过退款接口将支付款退还给买家,微信支付将在收到退款请求并且验证成功之后,按照退款规则将支付款按原路退到买家帐号上
接口说明
请求url: https://api.mch.weixin.qq.com/secapi/pay/reverse
请求方法: POST
数据格式:xml
需要双向证书
请求参数
| 应用ID | appid | 微信分配的应用ID |
|---|---|---|
| 退款结果通知url | notify_url | 异步接收微信支付退款结果通知的回调地址,通知URL必须为外网可访问的url,不允许带参数 如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效 |
| 商户号 | mch_id | 微信支付分配的商户号 |
| 商户订单号 | out_trade_no | 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_- |
| 示例值:1217752501201407033233368018 | ||
| 或者 | ||
| 微信支付订单号 | transaction_id | 微信的订单号,优先使用 示例值:1217752501201407033233368018 |
| 随机字符串 | nonce_str | 随机字符串,不长于32位。推荐随机数生成算法 示例值:5K8264ILTKCH16CQ2502SI8ZNMTM67VS |
| 签名 | sign | 签名,详见签名生成算法 示例值:C380BEC2BFD727A4B6845133519F3AD6 |
| 商户退款单号 | out_refund_no | 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_- |
| 订单金额 | total_fee | 订单总金额,单位为分,只能为整数 |
| 申请退款金额 | refund_fee | 退款总金额,单位为分,只能为整数,可部分退款。详见交易金额 |
请求实例
<xml>
<appid>wx2421b1c4370ec43b</appid>
<mch_id>10000100</mch_id>
<nonce_str>6cefdb308e1e2e8aabd48cf79e546a02</nonce_str>
<sub_mch_id>1415701182</sub_mch_id>
<out_refund_no>1415701182</out_refund_no>
<out_trade_no>1415757673</out_trade_no>
<refund_fee>1</refund_fee>
<total_fee>1</total_fee>
<transaction_id>4006252001201705123297353072</transaction_id>
<sign>FE56DD4AA85C0EECA82C35595A69E153</sign>
</xml>
代码 refundNotifyUrl 退款的回调路径
/**
* 微信退款
*/
@RequestMapping(value = "/refund", method = {RequestMethod.POST, RequestMethod.GET})
public String refund(@RequestParam(value = "transactionId", required = false) String transactionId, @RequestParam(value = "outTradeNo", required = false) String outTradeNo) {
try {
log.info("transactionId: {} outTradeNo:{}", transactionId, outTradeNo);
if (StrKit.isBlank(outTradeNo) && StrKit.isBlank(transactionId)) {
return "transactionId、out_trade_no二选一";
}
Map<String, String> params = RefundModel.builder()
.appid(wxPayBean.getAppId())
.mch_id(wxPayBean.getMchId())
.nonce_str(WxPayKit.generateStr())
.transaction_id(transactionId)
.out_trade_no(outTradeNo)
.out_refund_no(WxPayKit.generateStr())
.total_fee("1")
.refund_fee("1")
.notify_url(refundNotifyUrl)
.build()
.creatSign(wxPayBean.getPartnerKey(), SignType.MD5);
String refundStr = WxPayApi.orderRefund(false, params, wxPayBean.getCertPath(), wxPayBean.getMchId());
log.info("refundStr: {}", refundStr);
return refundStr;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
返回参数看文档
提交退款申请后,通过调用该接口查询退款状态。退款有一定延时,用零钱支付的退款20分钟内到账,银行卡支付的退款3个工作日后重新查询退款状态
接口说明
请求url: https://api.mch.weixin.qq.com/pay/refundquery
请求方法: POST
数据格式:xml
需要双向证书
请求参数
| 应用ID | appid | 微信分配的应用ID |
|---|---|---|
| 商户号 | mch_id | 微信支付分配的商户号 |
| 随机字符串 | nonce_str | 随机字符串,不长于32位。推荐随机数生成算法 示例值:5K8264ILTKCH16CQ2502SI8ZNMTM67VS |
| 签名 | sign | 签名,详见签名生成算法 示例值:C380BEC2BFD727A4B6845133519F3AD6 |
| 微信退款单号 | refund_id | 微信退款单号 |
| 商户退款单号 | out_refund_no | 商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_- |
| 微信支付订单号 | transaction_id | 微信的订单号,优先使用 |
| 商户订单号 | out_trade_no | 商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_- |
| 偏移量 | offset | 偏移量,当部分退款次数超过10次时可使用,表示返回的查询结果从这个偏移量开始取记录 如15 |
请求实例
<xml>
<appid>wx2421b1c4370ec43b</appid>
<mch_id>10000100</mch_id>
<nonce_str>0b9f35f484df17a732e537c37708d1d0</nonce_str>
<out_refund_no>123456789</out_refund_no>
<out_trade_no>1415757673</out_trade_no>
<sub_mch_id>1900000109</sub_mch_id>
<sign>66FFB727015F450D167EF38CCC549521</sign>
</xml>
代码
/**
* 微信退款查询
*/
@RequestMapping(value = "/refundQuery", method = {RequestMethod.POST, RequestMethod.GET})
public String refundQuery(@RequestParam("transactionId") String transactionId,
@RequestParam("out_trade_no") String outTradeNo,
@RequestParam("out_refund_no") String outRefundNo,
@RequestParam("refund_id") String refundId) {
Map<String, String> params = RefundQueryModel.builder()
.appid(wxPayBean.getAppId())
.mch_id(wxPayBean.getMchId())
.nonce_str(WxPayKit.generateStr())
.transaction_id(transactionId)
.out_trade_no(outTradeNo)
.out_refund_no(outRefundNo)
.refund_id(refundId)
.build()
.creatSign(wxPayBean.getPartnerKey(), SignType.MD5);
return WxPayApi.orderRefundQuery(false, params);
}
返回参数看文档
当商户申请的退款有结果后(退款状态为:退款成功、退款关闭、退款异常),微信会把相关支付结果及用户信息通过数据流的形式发送给商户,商户需要接收处理,并按文档规范返回应答。
接口说明
请求路径 : 该链接是通过【统一下单API】中提交的参数notify_url设置,如果链接无法访问,商户将无法接收到微信通知。 通知url必须为直接可访问的url,不能携带参数。示例:notify_url:“https://pay.weixin.qq.com/wxpay/pay.action”
通知规则
当商户申请的退款有结果后(退款状态为:退款成功、退款关闭、退款异常),微信后台会把相关确认结果和订单信息发送给商户,商户需要接收处理该消息,并返回应答。
后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信会判定本次通知失败,重新发送通知,直到成功为止(在通知一直不成功的情况下,微信总共会发起多次通知,通知频率为15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h - 总计 24h4m),但微信不保证通知最终一定能成功。
代码
/**
* 退款通知
*/
@RequestMapping(value = "/refundNotify", method = {RequestMethod.POST, RequestMethod.GET})
public String refundNotify(HttpServletRequest request) {
String xmlMsg = HttpKit.readData(request);
log.info("退款通知=" + xmlMsg);
Map<String, String> params = WxPayKit.xmlToMap(xmlMsg);
String returnCode = params.get("return_code");
// 注意重复通知的情况,同一订单号可能收到多次通知,请注意一定先判断订单状态
if (WxPayKit.codeIsOk(returnCode)) {
String reqInfo = params.get("req_info");
String decryptData = WxPayKit.decryptData(reqInfo, WxPayApiConfigKit.getWxPayApiConfig().getPartnerKey());
log.info("退款通知解密后的数据=" + decryptData);
// 更新订单信息
// 发送通知等
Map<String, String> xml = new HashMap<String, String>(2);
xml.put("return_code", "SUCCESS");
xml.put("return_msg", "OK");
return WxPayKit.toXml(xml);
}
return null;
}
返回参数看文档