第一次做这个功能,坑有点多(官方坑也有),最终完全了付款到银行卡功能,一一为大家踩坑。文章只讲解关于转账至银行卡的业务逻辑,需要用到的工具类,还有具体做法都会贴,适合第一次开发此功能的人看。
官方文档:
一、准备
1.证书
按照文档操作,得到apiclient_cert.p12文件,将他放到项目内的resources/static内,如果要部署jar到云服务器的,可以把证书放在项目同文件。后面访问时需要证书路径。
2.rsa公钥文件
看文档内获取rsa公钥的文档,准备好请求参数,直接用postman请求,参数中随机值(自己生产也好,随便敲也好,跟示例参数一样多位的随机值就行),sign参数(用签名工具类把几个参数签下名)
签名方式和支付功能一样,可以直接用之前开发支付用的工具类,啥?之前没做支付,没有工具类?
那我贴一下吧:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.*;
/**
* 微信支付要用到的:xml解析工具类
*/
public class WXPayUtils {
/**
* XML格式字符串转换为Map
*
* @param strXML XML字符串
* @return XML数据转换后的Map
* @throws Exception
*/
public static Map<String, String> xmlToMap(String strXML) throws Exception {
try {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilder documentBuilder = WXPayXmlUtil.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
org.w3c.dom.Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
org.w3c.dom.Element element = (org.w3c.dom.Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
try {
stream.close();
} catch (Exception ex) {
// do nothing
}
return data;
} catch (Exception ex) {
WXPayUtils.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
throw ex;
}
}
/**
* 将Map转换为XML格式的字符串
*
* @param data Map类型数据
* @return XML格式的字符串
* @throws Exception
*/
public static String mapToXml(Map<String, String> data) throws Exception {
Document document = WXPayXmlUtil.newDocument();
Element root = document.createElement("xml");
document.appendChild(root);
for (String key: data.keySet()) {
String value = data.get(key);
if (value == null) {
value = "";
}
value = value.trim();
Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
root.appendChild(filed);
}
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
DOMSource source = new DOMSource(document);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
try {
writer.close();
}
catch (Exception ex) {
}
return output;
}
/**
* 生成微信支付sign
*/
public static String createSign(SortedMap<String, String> params, String key){
StringBuilder sb = new StringBuilder();
Set<Map.Entry<String, String>> es = params.entrySet();
Iterator<Map.Entry<String, String>> it = es.iterator();
while(it.hasNext()){
Map.Entry<String, String> entry = it.next();
String k = entry.getKey();
String v = entry.getValue();
if(null != v && !"".equals(v) && !"sign".equals(k) && !"key".equals(k)){
sb.append(k + "=" + v + "&");
}
}
sb.append("key=").append(key);
String sign = CommonUtils.MD5(sb.toString()).toUpperCase();
return sign;
}
/**
* 校验签名
* @param params
* @param key
* @return
*/
public static Boolean isCorrectSign(SortedMap<String, String> params, String key){
String sign = createSign(params, key);
String wxPaySign = params.get("sign").toUpperCase();
return wxPaySign.equals(sign);
}
/**
* 获取有序map
* @param map
*/
public static SortedMap<String, String> getSortedMap(Map<String, String> map){
SortedMap<String, String> sortedMap = new TreeMap<>();
Iterator<String> it = map.keySet().iterator();
while(it.hasNext()){
String key = it.next();
String value = map.get(key);
String temp = "";
if(null != value){
temp = value.trim();
}
sortedMap.put(key, value);
}
return sortedMap;
}
/**
* 日志
* @return
*/
public static Logger getLogger() {
Logger logger = LoggerFactory.getLogger("wxpay java sdk");
return logger;
}
/**
* 获取当前时间戳,单位秒
* @return
*/
public static long getCurrentTimestamp() {
return System.currentTimeMillis()/1000;
}
/**
* 获取当前时间戳,单位毫秒
* @return
*/
public static long getCurrentTimestampMs() {
return System.currentTimeMillis();
}
/**
* 生成UUID(用来表示一笔订单)
* @return
*/
public static String generateUUID(){
String uuid = UUID.randomUUID().toString()
.replaceAll("-","")
.substring(0,32);
return uuid;
}
}
——签好名就请求获取rsa公钥接口,拿到了RSA公钥字符串。
——不要高兴得太早,请把这个字符串复制进txt,名字随意英文,然后改后缀名.pem,因为你是Java你需要转pkcs#8
文档内有互转的方法:
openssl rsa -RSAPublicKey_in -in <filename> -pubout
——如果没有openssl,你需要安装并配置openssl,这个可以百度。安装好后配置环境变量,然后在bin路径输入cmd,再跑上面的代码,成功之后,转换结果会直接显示在命令行,请复制这些密钥字符串。
——接下来,删除首尾:-----BEGIN PUBLIC KEY-----和-----END PUBLIC KEY-----
——然后将剩下的字符串放进txt,该后缀名pem,再放进项目的resources/static内。
——截止目前,我们便得到了rsa公钥文件:rsa.pem
3.银行卡开户行代号(付款至银行卡需要微信自己的参数)
这个我觉得不用说那么详细了,我自己用的方式都很业余。我是从百度找到了一个阿里的开发性api,用于获取银行卡开户行,然后再把微信的代号填到对照表里。再整理成工具类,把银行卡号做参数就可以得出代号(有几个小银行没有在对照表里),从优化的角度来看,这种方式需要跨域请求,最好是用一种可以直接识别银行卡号开户行的工具类,然后把微信开户行代号表和工具类的银行名称对齐一下。这里就默认你已经获取了银行卡开户行代号。
二、入参准备
——商户号:自行获取
——商户订单号:在请求这个接口前,你需要先新增一个你目前系统的订单,就是在订单表里插入一条订单,这个订单号就是商户订单号。
——随机值:直接调用我上方的工具类,有个generateUUID()函数直接获取。
——银行卡号:这个就需要用rsa公钥加密了。你已经有了公钥文件,但需要转成PublicKey类才能使用我的加密工具类。下面分步叙述流程(结果将得到PublicKey文件):
1.获取公钥文件的字节输入流
InputStream rsaStream = getClass().getClassLoader().getResourceAsStream(RSA公钥文件的路径字符串);
2.用下面这个工具类转成字符串
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
/*IO流工具类*/
public class StreamUtil {
/**
* 读取 InputStream 到 String字符串中
*/
public static String readStream(InputStream in) {
try {
//<1>创建字节数组输出流,用来输出读取到的内容
ByteArrayOutputStream baos = new ByteArrayOutputStream();
//<2>创建缓存大小
byte[] buffer = new byte[1024]; // 1KB
//每次读取到内容的长度
int len = -1;
//<3>开始读取输入流中的内容
while ((len = in.read(buffer)) != -1) { //当等于-1说明没有数据可以读取了
baos.write(buffer, 0, len); //把读取到的内容写到输出流中
}
//<4> 把字节数组转换为字符串
String content = baos.toString();
//<5>关闭输入流和输出流
in.close();
baos.close();
//<6>返回字符串结果
return content;
} catch (Exception e) {
e.printStackTrace();
return e.getMessage();
}
}
}
3.再用下面这个工具类getPublicKey(String key)函数将公钥字符串转为PublicKey类(该工具类包含RSA加密方法)
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import javax.crypto.Cipher;
import sun.misc.BASE64Decoder;
public class RSAwxUtil {
public static byte[] decrypt(byte[] encryptedBytes, PrivateKey privateKey, int keyLength, int reserveSize, String cipherAlgorithm) throws Exception {
int keyByteSize = keyLength / 8;
int decryptBlockSize = keyByteSize - reserveSize;
int nBlock = encryptedBytes.length / keyByteSize;
ByteArrayOutputStream outbuf = null;
try {
Cipher cipher = Cipher.getInstance(cipherAlgorithm);
cipher.init(Cipher.DECRYPT_MODE, privateKey);
outbuf = new ByteArrayOutputStream(nBlock * decryptBlockSize);
for (int offset = 0; offset < encryptedBytes.length; offset += keyByteSize) {
int inputLen = encryptedBytes.length - offset;
if (inputLen > keyByteSize) {
inputLen = keyByteSize;
}
byte[] decryptedBlock = cipher.doFinal(encryptedBytes, offset, inputLen);
outbuf.write(decryptedBlock);
}
outbuf.flush();
return outbuf.toByteArray();
} catch (Exception e) {
throw new Exception("DEENCRYPT ERROR:", e);
} finally {
try{
if(outbuf != null){
outbuf.close();
}
}catch (Exception e){
outbuf = null;
throw new Exception("CLOSE ByteArrayOutputStream ERROR:", e);
}
}
}
public static byte[] encrypt(byte[] plainBytes, PublicKey publicKey, int keyLength, int reserveSize, String cipherAlgorithm) throws Exception {
int keyByteSize = keyLength / 8;
int encryptBlockSize = keyByteSize - reserveSize;
int nBlock = plainBytes.length / encryptBlockSize;
if ((plainBytes.length % encryptBlockSize) != 0) {
nBlock += 1;
}
ByteArrayOutputStream outbuf = null;
try {
Cipher cipher = Cipher.getInstance(cipherAlgorithm);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
outbuf = new ByteArrayOutputStream(nBlock * keyByteSize);
for (int offset = 0; offset < plainBytes.length; offset += encryptBlockSize) {
int inputLen = plainBytes.length - offset;
if (inputLen > encryptBlockSize) {
inputLen = encryptBlockSize;
}
byte[] encryptedBlock = cipher.doFinal(plainBytes, offset, inputLen);
outbuf.write(encryptedBlock);
}
outbuf.flush();
return outbuf.toByteArray();
} catch (Exception e) {
throw new Exception("ENCRYPT ERROR:", e);
} finally {
try{
if(outbuf != null){
outbuf.close();
}
}catch (Exception e){
outbuf = null;
throw new Exception("CLOSE ByteArrayOutputStream ERROR:", e);
}
}
}
public static PrivateKey getPriKey(String privateKeyPath,String keyAlgorithm){
PrivateKey privateKey = null;
InputStream inputStream = null;
try {
if(inputStream==null){
System.out.println("hahhah1!");
}
inputStream = new FileInputStream(privateKeyPath);
System.out.println("hahhah2!");
privateKey = getPrivateKey(inputStream,keyAlgorithm);
System.out.println("hahhah3!");
} catch (Exception e) {
System.out.println("加载私钥出错!");
} finally {
if (inputStream != null){
try {
inputStream.close();
}catch (Exception e){
System.out.println("加载私钥,关闭流时出错!");
}
}
}
return privateKey;
}
public static PublicKey getPubKey(String publicKeyPath,String keyAlgorithm){
PublicKey publicKey = null;
InputStream inputStream = null;
try
{
System.out.println("getPubkey 1......");
inputStream = new FileInputStream(publicKeyPath);
System.out.println("getPubkey 2......");
publicKey = getPublicKey(inputStream,keyAlgorithm);
System.out.println("getPubkey 3......");
} catch (Exception e) {
e.printStackTrace();//EAD PUBLIC KEY ERROR
System.out.println("加载公钥出错!");
} finally {
if (inputStream != null){
try {
inputStream.close();
}catch (Exception e){
System.out.println("加载公钥,关闭流时出错!");
}
}
}
return publicKey;
}
public static PublicKey getPublicKey(InputStream inputStream, String keyAlgorithm) throws Exception {
try
{
System.out.println("b1.........");
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
System.out.println("b2.........");
StringBuilder sb = new StringBuilder();
String readLine = null;
System.out.println("b3.........");
while ((readLine = br.readLine()) != null) {
if (readLine.charAt(0) == '-') {
continue;
} else {
sb.append(readLine);
sb.append('\r');
}
}
System.out.println("b4.........");
X509EncodedKeySpec pubX509 = new X509EncodedKeySpec(decodeBase64(sb.toString()));
System.out.println("b5.........");
KeyFactory keyFactory = KeyFactory.getInstance(keyAlgorithm);
System.out.println("b6.........");
//下行出错 java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: IOException: DerInputStream.getLength(): lengthTag=127, too big.
PublicKey publicKey = keyFactory.generatePublic(pubX509);
System.out.println("b7.........");
return publicKey;
} catch (Exception e) {
e.printStackTrace();
System.out.println("b8.........");
throw new Exception("1这里报异常了:"+e.getMessage(), e);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
inputStream = null;
throw new Exception("INPUT STREAM CLOSE ERROR:", e);
}
}
}
public static PrivateKey getPrivateKey(InputStream inputStream, String keyAlgorithm) throws Exception {
try {
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
StringBuilder sb = new StringBuilder();
String readLine = null;
while ((readLine = br.readLine()) != null) {
if (readLine.charAt(0) == '-') {
continue;
} else {
sb.append(readLine);
sb.append('\r');
}
}
System.out.println("hahhah4!"+decodeBase64(sb.toString()));
PKCS8EncodedKeySpec priPKCS8 = new PKCS8EncodedKeySpec(decodeBase64(sb.toString()));
System.out.println("hahhah5!");
KeyFactory keyFactory = KeyFactory.getInstance(keyAlgorithm);
System.out.println("hahhah6!");
PrivateKey privateKey = keyFactory.generatePrivate(priPKCS8);
System.out.println("hahhah7!");
return privateKey;
} catch (Exception e) {
throw new Exception("READ PRIVATE KEY ERROR:" ,e);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException e) {
inputStream = null;
throw new Exception("INPUT STREAM CLOSE ERROR:", e);
}
}
}
//一下面是base64的编码和解码
public static String encodeBase64(byte[]input) throws Exception{
Class clazz=Class.forName("com.sun.org.apache.xerces.internal.impl.dv.util.Base64");
Method mainMethod= clazz.getMethod("encode", byte[].class);
mainMethod.setAccessible(true);
Object retObj=mainMethod.invoke(null, new Object[]{input});
return (String)retObj;
}
/***
* decode by Base64
*/
public static byte[] decodeBase64(String input) throws Exception{
Class clazz=Class.forName("com.sun.org.apache.xerces.internal.impl.dv.util.Base64");
Method mainMethod= clazz.getMethod("decode", String.class);
mainMethod.setAccessible(true);
Object retObj=mainMethod.invoke(null, input);
return (byte[])retObj;
}
public static PublicKey getPublicKey(String key) throws Exception {
byte[] keyBytes;
keyBytes = (new BASE64Decoder()).decodeBuffer(key);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(keySpec);
return publicKey;
}
public static PrivateKey getPrivateKey(String key) throws Exception {
byte[] keyBytes;
keyBytes = (new BASE64Decoder()).decodeBuffer(key);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
return privateKey;
}
}
该步骤总代码:
PublicKey pub; try { pub = RSAwxUtil.getPublicKey(readStream(wxPayAppConfig.getRsaPublicKeyStream())); } catch (Exception e) { e.printStackTrace(); baseEntity.setStatus(2); baseEntity.setMessage("读取RSA公钥时异常:"+e.getMessage()); return baseEntity; }
加密流程(以卡号为例):
1.直接复制(文档要求)
String rsa ="RSA/ECB/OAEPWITHSHA-1ANDMGF1PADDING";
2.调用encrypt()函数,直接贴我的逻辑代码:
byte[] enc_bank_no_byte; try { enc_bank_no_byte = RSAwxUtil.encrypt(bank_card.getBytes(),pub,2048,11,rsa); } catch (Exception e) { e.printStackTrace(); baseEntity.setStatus(2); baseEntity.setMessage("对银行卡号进行加密时异常,异常原因:"+e.getMessage()); return baseEntity; } String enc_bank_no;//最终银行卡号参数 try { enc_bank_no = BASE64.encode(enc_bank_no_byte); } catch (Exception e) { e.printStackTrace(); baseEntity.setStatus(2); baseEntity.setMessage("将银行卡号byte转为字符串时异常,异常原因:"+e.getMessage()); return baseEntity; }
——真实姓名:加密过程一致,不赘述。
——银行卡代号:不赘述
——金额:这里要注意,没做过支付的朋友,这个单位是分不是元,用订单金额*100即可。
——签名:上面已经贴了签名工具类,下面会贴我的请求代码。
最终请求代码:
SortedMap<String, String> params = new TreeMap<>(); try{ params.put("mch_id",wxPayAppConfig.getMchID()); params.put("partner_trade_no",orderNo); // 商户订单号 params.put("nonce_str",WXPayUtils.generateUUID());//随机值 params.put("enc_bank_no",enc_bank_no);//加密后的银行卡号 params.put("enc_true_name",enc_true_name);//加密后的真实姓名 params.put("bank_code",bank_code);//银行卡代号 params.put("amount",amount);// 标价金额(单位为分) params.put("desc", "提现"); //sign签名 String sign = WXPayUtils.createSign(params, wxPayAppConfig.getKey()); params.put("sign",sign); } catch (Exception e){ e.printStackTrace(); baseEntity.setStatus(2); baseEntity.setMessage("请求前,封装入参时异常,异常原因:"+e.getMessage()); return baseEntity; }
因为入参格式要求xml,请用工具类(上面找)转换:
String payXml; try{ payXml = WXPayUtils.mapToXml(params); } catch (Exception e){ e.printStackTrace(); baseEntity.setStatus(2); baseEntity.setMessage("请求接口前,map转xml异常,异常原因:"+e.getMessage()); return baseEntity; }
到此,入参封装好了。
三、请求转账接口
直接调用请求类请求,下面会发,在请求之前,说一说证书路径。如果要将jar部署到云服务器上,可以将你的证书放在云服务器里和项目同一个目录,然后用以下方法直接获取路径:
String path = System.getProperty("user.dir")+"/apiclient_cert.p12";
下面是请求类:
import java.io.File;
import java.io.FileInputStream;
import java.security.KeyStore;
import javax.net.ssl.SSLContext;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContexts;
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;
/**
* This example demonstrates how to create secure connections with a custom SSL
* context.
*/
public class ClientCustomSSL {
@SuppressWarnings("deprecation")
public static String doRefund(String url, String data, String p12_path, String p12_key) throws Exception {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
FileInputStream instream = new FileInputStream(new File(p12_path));//P12文件目录
try {
/**
* 下载证书时的密码、默认密码是你的MCHID mch_id
* */
keyStore.load(instream, p12_key.toCharArray());
} finally {
instream.close();
}
// Trust own CA and all self-signed certs
/**
* 下载证书时的密码、默认密码是你的MCHID mch_id
* */
SSLContext sslcontext = SSLContexts.custom()
.loadKeyMaterial(keyStore, p12_key.toCharArray())//这里也是写密码的
.build();
// Allow TLSv1 protocol only
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
sslcontext,
SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);
CloseableHttpClient httpclient = HttpClients.custom()
.setSSLSocketFactory(sslsf)
.build();
try {
HttpPost httpost = new HttpPost(url);
httpost.addHeader("Connection", "keep-alive");
httpost.addHeader("Accept", "*/*");
httpost.addHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
httpost.addHeader("Host", "api.mch.weixin.qq.com");
httpost.addHeader("X-Requested-With", "XMLHttpRequest");
httpost.addHeader("Cache-Control", "max-age=0");
httpost.addHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.0) ");
httpost.setEntity(new StringEntity(data, "UTF-8"));
CloseableHttpResponse response = httpclient.execute(httpost);
try {
HttpEntity entity = response.getEntity();
String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8");
EntityUtils.consume(entity);
return jsonStr;
} finally {
response.close();
}
} finally {
httpclient.close();
}
}
}
我的请求代码:(请求类四个参数分别是:付款url,入参xml,证书路径,商户号)
String result; try{ result = ClientCustomSSL.doRefund(url, payXml,path,wxPayAppConfig.getMchID()); } catch (Exception e){ e.printStackTrace(); baseEntity.setStatus(2); baseEntity.setMessage("请求转账接口时异常,异常原因:"+e.getMessage()); return baseEntity; }
最后需要将xml转为map:
Map<String, String> payBankMap; try{ payBankMap = WXPayUtils.xmlToMap(s); } catch (Exception e){ e.printStackTrace(); baseEntity.setStatus(2); baseEntity.setMessage("请求结果xml数据转map时异常,异常原因:"+e.getMessage()); return baseEntity; }
至此,付款请求完成。
根据payBankMap.get("result_code")参数可以知道付款成功或失败。
最后不要忘记更新相关数据库表的业务逻辑,如:付款后,订单改为已付款之类的。
还有,请保证微信支付运营账户余额大于1.5元,因为每一笔打款订单,微信起步手续费1元。也就是说,就算你打款0.01元,将被扣1.01元。
因为文档没有体现手续费起步1元,我浪费了大半天时间百度,就连技术客服都不知道这个规则,还帮我看后台日志,最后问他同事才知道。这种坑实在是不应该,还有请求时突然不支持的协议(该文档已经屏蔽),从开发社区里看到,两年前的文档漏洞,今天还没改...
另外需要注意的是,每个商户最低打款额不同,有些商户是0.01元,也有看到有很多商户是0.03元。