在软件开发中,我们会经常遇到微信支付功能的需求,本文将介绍如何实现微信的退付款功能。包括:“客户端扫描二维码发起微信支付”、“客户端请求服务器,进行微信支付”、“服务器收到客户端的微信支付请求,进行微信支付”、“服务器端返回微信支付结果给客户端”、“客户端收到服务器返回的微信支付结果”、“客户端向服务器发起微信退款请求”、“服务器收到客户端微信退款申请,进行微信退款”、“将微信退款结果返回给客户端”。
1、客户端扫描二维码发起微信支付。
客户端首先对支付金额进行格式化:
String dPaymentMoney = WXPayUtil.formatAmountToPayViaWX(payAmount);
/**
* 当amount非常大时,为了不让传递给WX的金额因为科学记数法出现较大误差,要使用本接口而非formatAmount(double amount)
*/
public static String formatAmountToPayViaWX(double amount) {
BigDecimal fen = new BigDecimal(100);
BigDecimal yuan = new BigDecimal(amount);
//
DecimalFormat df = new DecimalFormat("0.00");
df.setMaximumFractionDigits(2); // 四舍五入到小数点两位
return df.format(fen.multiply(yuan).doubleValue()).replaceAll(",", "");
}
新建WXPayInfo类的实例,设置微信支付金额支付授权码,授权码为扫描二维码后得到的微信付款授权码:
WXPayInfo wxPayInfo = new WXPayInfo();
wxPayInfo.setAuth_code(wxPayAuthCode);
wxPayInfo.setTotal_fee(dPaymentMoney);
WXPayInfo主要封装了微信支付需要的字段:
public class WXPayInfo extends BaseModel {
public static final WXPayInfoField field = new WXPayInfoField();
public static final String HTTP_WXPayInfo_MicroPay = "wxpay/microPayEx.bx";
public static final String HTTP_WXPayInfo_Reverse = "wxpay/reverseEx.bx";
public static final String HTTP_WXPayInfo_Refund = "wxpay/refundEx.bx";
@Transient
protected String sub_mch_id; // 子商户号
@Transient
protected String nonce_str; // 随机字符串
@Transient
protected String sign; // 签名
@Transient
protected String body; // 商品描述
@Transient
protected String out_trade_no; // 随机字符串
@Transient
protected String total_fee; // 总金额
@Transient
protected String spbill_create_ip; // 终端IP
@Transient
protected String auth_code; // 授权码
@Transient
protected String transaction_id; // 微信支付订单号
@Transient
protected String refund_desc; // 退款原因
@Transient
protected String out_refund_no; // 商户退款单号
@Transient
protected String refund_fee; // 申请退款金额
@Transient
protected String refund_id; // 微信退款单号
protected String couponCode; //微信支付时使用的优惠券码
@Transient
protected int bonusIsChanged; // 是否进行积分变动
@Transient
protected int vipID;
@Transient
protected int wxVipID;
@Transient
protected int wxVipCardDetailID;
……
微信支付需要联网,所以需要设备连接了网络:
//结算时选择了微信支付,然后断网,在这里进行判断。支付一次失败后(短暂断网),不会将session设置为null,下次支付仍然可以使用微信支付
if (!NetworkUtils.isNetworkAvalible(getActivity())) {
wxPayHttpEvent.setLastErrorCode(ErrorInfo.EnumErrorCode.EC_InvalidSession);
hm.setErrorCode(EC_InvalidSession);
return false;
}
//判断网络是否连接
public static boolean isNetworkAvalible(Context context) {
// 获得网络状态管理器
ConnectivityManager connectivityManager = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
if (connectivityManager != null) {
// 建立网络数组
NetworkInfo[] net_info = connectivityManager.getAllNetworkInfo();
if (net_info != null) {
for (int i = 0; i < net_info.length; i++) {
// 判断获得的网络状态是否是处于连接状态
if (net_info[i].getState() == NetworkInfo.State.CONNECTED) {
return true;
} else {
log.info("网络不可用!");
}
}
}
}
return false;
}
2、客户端请求服务器,进行微信支付。
public boolean microPayAsync(BaseModel bm) {//TODO 暂没有指定session
log.info("正在执行WXPayHttpBO的microPayAsync");
// 判断session是否为空,是否为断网情况
if (GlobalController.getInstance().getSessionID() == null) {
httpEvent.setLastErrorCode(ErrorInfo.EnumErrorCode.EC_InvalidSession);
return false;
}
WXPayInfo wxPayInfo = (WXPayInfo) bm;
RequestBody body = new FormBody.Builder()
.add(WXPayInfo.field.getFIELD_NAME_auth_code(), wxPayInfo.getAuth_code()) //
.add(WXPayInfo.field.getFIELD_NAME_total_fee(), wxPayInfo.getTotal_fee()) //
.add(WXPayInfo.field.getFIELD_NAME_couponCode(), (wxPayInfo.getCouponCode()) == null ? "" : wxPayInfo.getCouponCode()) //
.add(WXPayInfo.field.getFIELD_NAME_bonusIsChanged(), String.valueOf(wxPayInfo.getBonusIsChanged())) //
.add(WXPayInfo.field.getFIELD_NAME_vipID(), String.valueOf(wxPayInfo.getVipID())) //
.add(WXPayInfo.field.getFIELD_NAME_wxVipID(), String.valueOf(wxPayInfo.getWxVipID())) //
.add(WXPayInfo.field.getFIELD_NAME_wxVipCardDetailID(), String.valueOf(wxPayInfo.getWxVipCardDetailID())) //
.build();
Request req = new Request.Builder()
.url(Configuration.HTTP_IP + WXPayInfo.HTTP_WXPayInfo_MicroPay)
.addHeader(BaseHttpBO.COOKIE, GlobalController.getInstance().getSessionID())
.post(body)
.build();
HttpRequestUnit hru = new RequestWXPayMicroPay();
hru.setRequest(req);
hru.setTimeout(TIME_OUT);
hru.setbPostEventToUI(true);
httpEvent.setEventProcessed(false);
httpEvent.setStatus(BaseEvent.EnumEventStatus.EES_Http_ToDo);
hru.setEvent(httpEvent);
HttpRequestManager.getCache(HttpRequestManager.EnumDomainType.EDT_Communication).pushHttpRequest(hru);
return true;
}
3、服务器收到客户端的微信支付请求,进行微信支付。
服务器收到微信支付请求,首先对支付金额的格式和大小做检查:
Double mount = BaseModel.TOLERANCE;
do {
try {
mount = Double.parseDouble(wxPayInfo.getTotal_fee());
logger.debug("App传递的金额=" + mount);
if (wxPayInfo.getTotal_fee().length() - wxPayInfo.getTotal_fee().indexOf(".") == 3) { // App传递的金额一定有2位小数
break;
}
} catch (Exception ex) {
}
logger.info("订单金额格式错误");
return null;
} while (false);
if (mount <= BaseModel.TOLERANCE) {
if (!StringUtils.isEmpty(wxPayInfo.getCouponCode())) {
// 商家创建满10-10的优惠券,顾客有此优惠券,并且购买商品的金额为10元,那么优惠后则为0元,微信不支持支付0元
params.put(BaseAction.JSON_ERROR_KEY, EnumErrorCode.EC_BusinessLogicNotDefined.toString());
params.put(BaseAction.KEY_HTMLTable_Parameter_msg, "此单优惠后价格为 0 元,不支持微信支付");
return JSONObject.fromObject(params, JsonUtil.jsonConfig).toString();
} else {
return "";
}
}
根据公众号appid、商户号mchid,密钥secret和证书cert创建WXPayConfigImpl实例:
WXPayConfigImpl config = new WXPayConfigImpl(appid, mchid, secret, cert);
Appid、mchid、secret、cert在配置文件中获取,cert要放在一个安全地地方:
env.properties配置文件
#运行环境
env=
#支付服务商公众号APPID
env.appid=
#服务商商户号
env.mchid=
#服务商API密钥
env.secret=
#服务商证书路径
env.cert=
使用@Value获取配置文件信息:
@Value("${env.appid}")
private String appid;
@Value("${env.mchid}")
private String mchid;
@Value("${env.secret}")
private String secret;
@Value("${env.cert}")
private String cert;
WXPayConfigImpl类继承了WXPayConfig:
查看代码
public class WXPayConfigImpl implements WXPayConfig {
private static Log logger = LogFactory.getLog(WXPayConfigImpl.class);
// 支付服务商
/** 服务商 公众号APPID 防止泄露 */
private static String AppID = null;
/** 服务商 商户号 防止泄露 */
private static String MchID = null;
/** 服务商 API 密钥 防止泄露 */
private static String KEY = null;
private byte[] certData;
/** 初始化配置
*
* @param env
* @param appid
* @param mchid
* @param secret
*/
public WXPayConfigImpl(final String appid, final String mchid, final String secret, final String cert) {
try {
if (StringUtils.isEmpty(BaseAction.ENV.getName()) || StringUtils.isEmpty(appid) || StringUtils.isEmpty(mchid) || StringUtils.isEmpty(secret) || StringUtils.isEmpty(cert)) {
logger.info("微信支付初始化配置错误,请检查env.properties文件配置是否正确!!");
logger.info("env=" + BaseAction.ENV);
logger.info("appid=" + appid);
logger.info("mchid=" + mchid);
logger.info("secret=" + secret);
logger.info("cert=" + cert);
return;
}
// 必须放在KEY前面
AppID = appid;
MchID = mchid;
if (EnumEnv.DEV.getName().equals(BaseAction.ENV.getName())) {
if (BaseAction.UseSandBox) {
KEY = getSandboxSignKey(secret);
// 沙箱环境下让其每次请求微信时休眠5秒,不要频繁请求,否则可能发生意外情况
Thread.sleep(5000);
} else {
KEY = secret;
}
} else if (EnumEnv.SIT.getName().equals(BaseAction.ENV.getName()) || EnumEnv.UAT.getName().equals(BaseAction.ENV.getName()) || EnumEnv.PROD.getName().equals(BaseAction.ENV.getName())) {
KEY = secret;
BaseAction.UseSandBox = false;
} else {
logger.info("env=" + BaseAction.ENV);
logger.info("运行环境配置错误,请检查env.properties配置是否正确!!");
return;
}
File file = new File(cert); // TODO 证书需要放在安全的地方!!
InputStream certStream = new FileInputStream(file);
this.certData = new byte[(int) file.length()];
certStream.read(this.certData);
certStream.close();
} catch (Exception e) {
logger.info("微信支付初始化配置异常:" + e.getMessage());
}
}
public String getAppID() {
return AppID;
}
public String getMchID() {
return MchID;
}
public String getKey() {
return KEY;
}
public InputStream getCertStream() {
ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);
return certBis;
}
public int getHttpConnectTimeoutMs() {
return 10000;
}
public int getHttpReadTimeoutMs() {
return 10000;
}
/** @param secret
* 真实环境的key <br>
* 根据真实环境的key获取沙箱环境下的KEY */
public String getSandboxSignKey(String secret) {
WXPay wxPay = new WXPay(this);
try {
Map<String, String> params = new HashMap<String, String>();
params.put("mch_id", this.getMchID());
params.put("nonce_str", WXPayUtil.generateNonceStr());
params.put("sign", WXPayUtil.generateSignature(params, secret));
String strXML = wxPay.requestWithoutCert("https://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey", params, this.getHttpConnectTimeoutMs(), this.getHttpReadTimeoutMs());
Map<String, String> result = WXPayUtil.xmlToMap(strXML);
logger.info("沙箱KEY:" + result);
if ("SUCCESS".equals(result.get(BaseWxModel.WXPay_RETURN))) {
return result.get("sandbox_signkey");
}
return null;
} catch (Exception e) {
System.out.println("获取沙箱KEY异常:" + e.getMessage());
return null;
}
}
}
根据WXPayConfigImpl的实例创建WXPay的实例:
WXPay wxpay = new WXPay(config, WXPayConstants.SignType.MD5, UseSandBox);
获取交易单号:
String nonce_str = WXPayUtil.generateNonceStr();
// String sign = WXPayUtil.generateSignature(microPayData, config.getKey(),
// WXPayConstants.SignType.MD5);
String out_trade_no = String.valueOf(System.currentTimeMillis() % 1000000) + nonce_str.substring(6);
将子商户好、商品描述、交易单号、终端IP、订单总金额、授权码放进hashmap容器中:
Company company = getCompanyFromSession(session);
microPayData.put(WXPayInfo.field.getFIELD_NAME_sub_mch_id(), company.getSubmchid());
microPayData.put(WXPayInfo.field.getFIELD_NAME_body(), "博昕-微信付款码支付-测试"); // 商品描述
microPayData.put(WXPayInfo.field.getFIELD_NAME_out_trade_no(), out_trade_no); // 交易单号
microPayData.put(WXPayInfo.field.getFIELD_NAME_spbill_create_ip(), "127.0.0.1"); // 终端IP
if (BaseAction.UseSandBox) {
microPayData.put(WXPayInfo.field.getFIELD_NAME_total_fee(), "1"); // 订单总金额,单位为分,只能为整数
microPayData.put(WXPayInfo.field.getFIELD_NAME_auth_code(), "134526050509995238"); // 沙箱环境下的支付授权码
} else {
String amount = wxPayInfo.getTotal_fee().substring(0, wxPayInfo.getTotal_fee().indexOf("."));
microPayData.put(WXPayInfo.field.getFIELD_NAME_total_fee(), amount); // 订单总金额,单位为分,只能为整数
microPayData.put(WXPayInfo.field.getFIELD_NAME_auth_code(), wxPayInfo.getAuth_code()); // 授权码
}
请求微信服务器进行微信支付:
Map<String, String> resp1 = wxpay.microPay(microPayData);
根据微信支付返回信息,判断是否支付成功:
if (resp1.get(BaseWxModel.WXPay_RETURN).equals(BaseWxModel.WXPay_SUCCESS) && resp1.get(BaseWxModel.WXPay_RESULT).equals(BaseWxModel.WXPay_SUCCESS)) {
logger.info("付款码支付成功!!!");
params.put(BaseAction.JSON_ERROR_KEY, EnumErrorCode.EC_NoError.toString());
} else if (resp1.get(BaseWxModel.WXPay_RETURN).equals(BaseWxModel.WXPay_SUCCESS) && resp1.get(BaseWxModel.WXPay_RESULT).equals(BaseWxModel.WXPay_FAIL) && resp1.get("err_code").equals("USERPAYING")) {
// 用户支付中,等待10秒,然后调用被扫订单结果查询API,查询当前订单的不同状态,决定下一步的操作
Thread.sleep(10 * 1000);
// 从公司的缓存拿到子商户号
// 查询订单
Map<String, String> orderQueryData = new HashMap<String, String>();
orderQueryData.put(WXPayInfo.field.getFIELD_NAME_sub_mch_id(), company.getSubmchid());
// orderQueryData.put(WXPayInfo.field.getFIELD_NAME_nonce_str(), nonce_str);
// orderQueryData.put(WXPayInfo.field.getFIELD_NAME_sign(), sign);
orderQueryData.put(WXPayInfo.field.getFIELD_NAME_out_trade_no(), out_trade_no);
//
int timeOut = 30; // 官方建议30秒
Map<String, String> resp2 = wxpay.orderQuery(orderQueryData);
logger.info("orderQueryData : " + resp2);
while (resp2.get(BaseWxModel.WXPay_RETURN).equals(BaseWxModel.WXPay_SUCCESS) && resp2.get(BaseWxModel.WXPay_RESULT).equals(BaseWxModel.WXPay_SUCCESS) && resp2.get("trade_state").equals("USERPAYING") && timeOut-- > 0) {
int timespan = 5;
Thread.sleep(timespan * 1000);
timeOut -= timespan;
if (timeOut <= 0) {
break;
}
resp2 = wxpay.orderQuery(orderQueryData);
logger.info("orderQuerOyData : " + resp2);
}
params.putAll(resp2);
if (resp2.get(BaseWxModel.WXPay_RETURN).equals(BaseWxModel.WXPay_SUCCESS) && resp2.get(BaseWxModel.WXPay_RESULT).equals(BaseWxModel.WXPay_SUCCESS) && resp2.get("trade_state").equals(BaseWxModel.WXPay_SUCCESS)) {
logger.info("查询订单 --> 付款码支付成功!!!");
params.put(KEY_HTMLTable_Parameter_msg, resp2.get(BaseWxModel.WXPAY_ERR_CODE_DES));
params.put(JSON_ERROR_KEY, EnumErrorCode.EC_NoError.toString());
} else {
logger.info("查询订单 --> 付款码支付失败!!!");
logger.info("用户支付未完成,请撤销订单!!!");
params.put(JSON_ERROR_KEY, EnumErrorCode.EC_OtherError.toString());
params.put(KEY_HTMLTable_Parameter_msg, resp2.get(BaseWxModel.WXPAY_ERR_CODE_DES));
}
} else {
logger.info("付款码支付失败!!!");
params.put(BaseAction.JSON_ERROR_KEY, EnumErrorCode.EC_OtherError.toString());
params.put(KEY_HTMLTable_Parameter_msg, "服务器异常");
}
4、服务器端返回微信支付结果给客户端。
return JSONObject.fromObject(params, JsonUtil.jsonConfig).toString();
5、客户端收到服务器返回的微信支付结果。
if (wxPayHttpEvent.getStatus() != BaseEvent.EnumEventStatus.EES_Http_Done) {
log.info("微信支付超时....");
hm.setErrorCode(ErrorInfo.EnumErrorCode.EC_Timeout);
return false;
}
if (wxPayHttpEvent.getLastErrorCode() != ErrorInfo.EnumErrorCode.EC_NoError) { // 支付失败后,不会将session设置为null,下次支付仍然可以使用微信支付
log.info("微信支付失败....");
hm.setErrorCode(EC_WechatServerError);
hm.setMsg(wxPayHttpEvent.getLastErrorMessage());
return false;
}
支付成功后,客户端设置零售单的相关字段:
paymentCode = paymentCode | wechatPayCode;
BaseActivity.retailTrade.setPaymentType(paymentCode);
//
// TODO 这里etWechatPayingAmount是文本框输入的金额
wetchatAmount = Double.valueOf(etWechatPayingAmount.getText().toString());
// wetchatAmount = GeneralUtil.sum(Double.valueOf(etWechatPayingAmount.getText().toString()), payAmount);
BaseActivity.retailTrade.setAmountWeChat(wetchatAmount);
//
microPayResponse = wxPayHttpEvent.getMicroPayResponse();
BaseActivity.retailTrade.setWxOrderSN(microPayResponse.get("transaction_id") == null ? "" : microPayResponse.get("transaction_id"));
BaseActivity.retailTrade.setWxTradeNO(microPayResponse.get("out_trade_no") == null ? "" : microPayResponse.get("out_trade_no"));
BaseActivity.retailTrade.setWxRefundSubMchID(microPayResponse.get("sub_mch_id") == null ? "" : microPayResponse.get("sub_mch_id"));
BaseActivity.retailTrade.setWxRefundDesc(microPayResponse.get("refund_desc") == null ? "" : microPayResponse.get("refund_desc"));
6、客户端向服务器发起微信退款请求。
客户端请求服务器,进行微信退款:
public boolean refundAsync(BaseModel bm) {
log.info("正在执行WXPayHttpBO的refundAsync,bm=" + (bm == null ? null : bm.toString()));
RetailTrade rt = (RetailTrade) bm;
//
RequestBody body = new FormBody.Builder()
.add(rt.field.getFIELD_NAME_amount(), String.valueOf(rt.getAmount())) //退款金额...
// .add(rt.getFIELD_NAME_int1(), "1") //...
.add(rt.getFIELD_NAME_wxOrderSN(), rt.getWxOrderSN() == null ? "" : rt.getWxOrderSN())
.add(rt.getFIELD_NAME_wxTradeNO(), rt.getWxTradeNO() == null ? "" : rt.getWxTradeNO())
//TODO 沙箱环境可能不会返回sub_mch_id,所以如果为空就先写死;真实环境必定会返回sub_mch_id。
.add(rt.getFIELD_NAME_wxRefundSubMchID(), rt.getWxRefundSubMchID() == null ? Constants.submchid : rt.getWxRefundSubMchID())
.add(rt.getFIELD_NAME_wxRefundDesc(), rt.getWxRefundDesc() == null ? "" : rt.getWxRefundDesc())
.build();
Request req = new Request.Builder()
.url(Configuration.HTTP_IP + WXPayInfo.HTTP_WXPayInfo_Refund)
.addHeader(BaseHttpBO.COOKIE, GlobalController.getInstance().getSessionID())
.post(body)
.build();
HttpRequestUnit hru = new RequestWXPayRefund();
hru.setRequest(req);
hru.setTimeout(TIME_OUT);
hru.setbPostEventToUI(true);
httpEvent.setEventProcessed(false);
httpEvent.setStatus(BaseEvent.EnumEventStatus.EES_Http_ToDo);
hru.setEvent(httpEvent);
HttpRequestManager.getCache(HttpRequestManager.EnumDomainType.EDT_Communication).pushHttpRequest(hru);
return true;
}
7、服务器收到客户端微信退款申请,进行微信退款。
创建WXPayConfigImpl的实例,获取退款单号,签名sign:
WXPayConfigImpl config = new WXPayConfigImpl(appid, mchid, secret, cert);
WXPay wxpay = new WXPay(config, WXPayConstants.SignType.MD5, UseSandBox);
Map<String, Object> params = new HashMap<>();
Map<String, String> refundData = new HashMap<String, String>();
String nonce_str = WXPayUtil.generateNonceStr();
String sign = WXPayUtil.generateSignature(refundData, config.getKey(), WXPayConstants.SignType.MD5);
String out_refund_no = String.valueOf(System.currentTimeMillis() % 1000000) + nonce_str.substring(12);
将退款信息放到hahmap容器中:
Company company = getCompanyFromSession(session);
refundData.put(WXPayInfo.field.getFIELD_NAME_sub_mch_id(), company.getSubmchid());
// refundData.put(WXPayInfo.field.getFIELD_NAME_sub_mch_id(),
// rt.getWxRefundSubMchID());// 子商户号
refundData.put(WXPayInfo.field.getFIELD_NAME_nonce_str(), nonce_str); // 随机字符串
refundData.put(WXPayInfo.field.getFIELD_NAME_sign(), sign); // 签名
refundData.put(WXPayInfo.field.getFIELD_NAME_transaction_id(), rt.getWxOrderSN()); // 微信订单号
refundData.put(WXPayInfo.field.getFIELD_NAME_out_trade_no(), rt.getWxTradeNO()); // 商户订单号
refundData.put(WXPayInfo.field.getFIELD_NAME_refund_desc(), rt.getWxRefundDesc() == null ? "" : rt.getWxRefundDesc()); // 退款原因 非必填
refundData.put(WXPayInfo.field.getFIELD_NAME_out_refund_no(), out_refund_no); // 商户退款单号 自定义
//
if (UseSandBox) {
refundData.put(WXPayInfo.field.getFIELD_NAME_total_fee(), "502"); // 订单金额
refundData.put(WXPayInfo.field.getFIELD_NAME_refund_fee(), "502"); // 申请退款金额
} else {
// 需要对金额进行转换。
String amount = String.valueOf(rt.getAmount()).substring(0, String.valueOf(rt.getAmount()).indexOf("."));
refundData.put(WXPayInfo.field.getFIELD_NAME_total_fee(), amount); // 订单金额
refundData.put(WXPayInfo.field.getFIELD_NAME_refund_fee(), amount); // 申请退款金额
}
进行微信退款:
//
Map<String, String> resp = wxpay.refund(refundData);
logger.info("refundData:" + resp);
Thread.sleep(1000);
if (resp.get(BaseWxModel.WXPay_RETURN).equals(BaseWxModel.WXPay_SUCCESS) && resp.get(BaseWxModel.WXPay_RESULT).equals(BaseWxModel.WXPay_SUCCESS)) {
logger.info("退款成功!!!");
params.put(BaseAction.JSON_ERROR_KEY, EnumErrorCode.EC_NoError.toString());
} else {
logger.info("退款失败!!!");
params.put(KEY_HTMLTable_Parameter_msg, resp.get(BaseWxModel.WXPAY_ERR_CODE_DES)); // 微信返回的错误信息
params.put(BaseAction.JSON_ERROR_KEY, EnumErrorCode.EC_OtherError.toString());
}
params.putAll(resp);
8、将微信退款结果返回给客户端。
return JSONObject.fromObject(params, JsonUtil.jsonConfig).toString();