公众号介绍

https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html
最重要的一点:
公众平台以access_token为接口调用凭据,来调用接口,所有接口的调用需要先获取access_token,access_token在2小时内有效,过期需要重新获取,但1天内获取次数有限,开发者需自行存储,详见获取接口调用凭据(access_token)文档。

公众号配置

开通<网页授权获取用户基本信息>接口

在微信公众号请求用户网页授权之前,开发者需要先到公众平台官网中的“开发 - 接口权限 - 网页服务 - 网页帐号 - 网页授权获取用户基本信息”的配置选项中,修改授权回调域名。请注意,这里填写的是域名(是一个字符串),而不是URL,因此请勿加 http:// 等协议头;。

授权回调域名配置规范为全域名,比如需要网页授权的域名为:www.qq.com,配置以后此域名下面的页面http://www.qq.com/music.html 、 http://www.qq.com/login.html 都可以进行OAuth2.0鉴权。但http://pay.qq.com 、 http://music.qq.com 、 http://qq.com 无法进行OAuth2.0鉴权

开通<获取access_token>接口

开通<获取jsapi_ticket>接口

以上三个是最核心的接口必须开通,其他接口根据需要开通

配置<JS接口安全域名>

先登录微信公众平台进入“公众号设置”的“功能设置”里填写“JS接口安全域名”。

备注:登录后可在“开发者中心”查看对应的接口权限。

配置ip白名单

登录微信公众平台–>开发>基本配置>ip白名单

提前将服务器 IP 地址添加到 IP 白名单中,否则将无法调用成功。小程序无需配置 IP 白名单。

微信支付产品介绍

(1)付款码支付
付款码-线下支付,商户展示一个有金额二维码,用户扫描二维码并输入密码进行支付。

(2)JSAPI支付
JSAPI-公众号支付,商户通过公众号调起微信支付页面,支付页面已固定金额,用户输入密码进行支付。
JSAPI-线下支付,商户展示一个无金额二维码,用户扫描二维码并输入金额和密码进行支付。
JSAPI-PC网站支付,商户展示一个无金额二维码,用户扫描二维码并输入金额和密码进行支付。

(3)小程序支付
小程序支付,商户通过小程序调起微信支付页面,支付页面已固定金额,用户输入密码进行支付。

(4)Native支付
Native-线下支付,商户展示一个有金额二维码,用户扫描二维码并输入密码进行支付。
Native-PC网站支付,商户展示一个有金额二维码,用户扫描二维码并输入密码进行支付。

(5)APP支付
在商户自己开发的手机App(Android、IOS)上调起微信支付页面,支付页面已固定金额,用户输入密码进行支付。

(6)刷脸支付
不清楚,如果需要使用自己去查询官方文档。

以上支付只是客户端的不用应用场景,但对后台开发人员来说都一样,因为微信有统一的支付接口:https://api.mch.weixin.qq.com/pay/unifiedorder
一般来说我们选JSAP支付就行了,
JSAP支付和Native支付的区别就是JSAPI展示的是无金额二维码,Native展示的是有金额二维码。

微信支付接入流程

(1)获取商户号
提交资料=>签署协议=>获取商户号

(2)获取 APPID
步骤:注册服务号=>服务号认证=>获取APPID=>绑定商户号

(3)获取API秘钥
登录商户平台=>选择账户中心=>安全中心=>API安全=>设置API密钥

(4)获取APlv3秘钥
登录商户平台=>选择账户中心=>安全中心=>API安全=>设置APIV3密钥

(5)申请商户API证书
APIv3版本的所有接口都需要;APIv2版本的高级接口需要(如:退款、企业红包、企业付款等)
登录商户平台=>选择账户中心=>安全中心=>API安全=>申请API证书

下载证书工具=>解压(下载的exe其实是个解压工具)证书工具=>运行证书工具=>选择证书保存路径=>点击申请证书=>填写商户号和商户名称=>下一步=>复制请求字符串=>回到商户平台=>粘贴请求字符串=>输入商户平台密码=>复制证书字符串=>回到证书工具=>下一步=>粘贴证书字符串=>下一步=>查看证书文件夹=>解压zip=>完工。

解压后apiclient_cert.p12、apiclient_cert.pem、apiclient_key.pem就是我们的商户API证书,其中apiclient_cert.p12是后台开发调用退款接口需要的文件,其他两个文件我没发现有地方需要使用它们。

附三个文件的介绍:
证书pkcs12格式(apiclient_cert.p12):包含了私钥信息的证书文件,为p12(pfx)格式,由微信支付签发给您用来标识和界定您的身份。部分安全性要求较高的API需要使用该证书来确认您的调用身份。
证书pem格式(apiclient_cert.pem):从apiclient_cert.p12中导出证书部分的文件,为pem格式,证书序列号也可以从这个文件里解析得到
证书密钥pem格式(apiclient_key.pem):从apiclient_cert.p12中导出密钥部分的文件。

(6)获取微信平台证书
可以预先下载,也可以通过编程的方式获取。

商户号配置

配置支付目录

支付授权目录说明:
1、商户最后请求拉起微信支付收银台的页面地址我们称之为“支付目录”,例如:https://www.weixin.com/pay.php。
2、商户实际的支付目录必须和在微信支付商户平台设置的一致,否则会报错“当前页面的URL未注册:”

支付授权目录设置说明:
登录微信支付商户平台(pay.weixin.qq.com)–>产品中心–>开发配置,设置后一般5分钟内生效。

支付授权目录校验规则说明:
1、如果支付授权目录设置为顶级域名(例如:https://www.weixin.com/ ),那么只校验顶级域名,不校验后缀;
2、如果支付授权目录设置为多级目录,就会进行全匹配,例如设置支付授权目录为https://www.weixin.com/abc/123/,则实际请求页面目录不能为https://www.weixin.com/abc/,也不能为https://www.weixin.com/abc/123/pay/,必须为https://www.weixin.com/abc/123/

开发介绍

纯手写,没有使用微信提供的JavaSDK(因为也没几句代码)。
使用JSAPI-公众号支付支付方式进行开发。
项目技术选型:
非前后端分离的传统SSM架构,非微服务,非分布式,单服务器部署。

支付流程

假设我们的公众号已经开发好了,上面有个商品,用户点击购买进行下单,OK,这是前端支付事件的最源头。
从这个最源头出发,实际上的流程是:

用户点击下单=>调用后台系统生成订单=>后台调用微信统一支付接口并获取prepay_id=>返回公众号页面,使用JSAPI调起微信支付页面,并传入prepay_id=>用户输入支付密码完成支付=>微信支付成功回调后台系统=>后台系统修改订单的支付状态。

OpenID

OpenID是微信公众号中为了鉴别用户而设计的唯一标识,每个用户针对每个公众号会产生一个安全的OpenID。

我们后台如果要设计一个微信用户表用来储存微信用户信息,那这个表和我们系统原来的用户表一定是多对一的,因为我们可能会有多个公众号,这样一来一个用户就有多个OpenID。

一般来说,在用户第一次访问公众号时(即从公众号菜单点进来的)获取OpenID,获取一次后返回给公众号前端页面,前端每次调用后台接口时,都传入这个OpenID,后台接口就知道是哪个用户在操作了,相当于PC网站的Session。

OpenID一旦拿到手,需要在页面上和后台之间反复传输已避免丢失(因为你不保存在前端页面,就需要后台接口每次都需要调用微信以获取OpenID,很浪费时间的,而且OpenID又不会变化)。

不想传来传去的话,可以将OpenID存到session中,也行,但是session一失效又得重新获取,而且重新获取时重定向不知道原页面了,只能重定向到首页。只能说和页面传值的方式对比,各有利弊。
以前的微信公众号不能使用session,现在的微信公众号是可以使用session的,和pc浏览器的session无异。

看这里,很重要:
微信获取OpenID需要通过重定向https://open.weixin.qq.com/connect/oauth2/authorize这个链接,然后再定义一个接口接收微信的回调请求,然后在回调接口里再去调用https://api.weixin.qq.com/sns/oauth2/access_token接口获取OpenID。

获取OpenID的微信接口1:

定义goods/menu接口(公众号菜单配的链接对应的后台接口):

@Controller
@RequestMapping("/goods")
public class GoodsController extends WechatController {// 用户通过微信内置浏览器访问此接口// 如果用户在微信客户端中访问第三方网页,公众号可以通过微信网页授权机制,来获取用户基本信息(目前我们只需要openid),进而实现业务逻辑。@RequestMapping(value = "/menu", method = RequestMethod.GET)public String menu(HttpServletRequest request, HttpServletResponse response) throws Exception {if (Utils.isEmptyTrim(request.getSession().getAttribute("openId"))) {//如果没有获取openId,那么去获取完openId再来String redirectUrl="http://www.xuexibisai.com/wechat/code/1";WechatUtil.redirectGetOpenId(request, response, redirectUrl);return null;}else {return "forward:/goods/list.do";//转发至goods/list.do}}
}
public class WechatUtil {public static final String redirect_openId_url = "https://open.weixin.qq.com/connect/oauth2/authorize?";// 重定向获取openidpublic static void redirectGetOpenId(HttpServletRequest request, HttpServletResponse response, String url) {try {LOG.debug("微信认证后待访问url:" + url);// scope参数只有两种应用授权作用域,snsapi_base// (不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo// (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且, 即使在未关注的情况下,只要用户授权,也能获取其信息 )// state参数是开发者自定义的参数,重定向后会带上state参数,开发者可以填写a-zA-Z0-9的参数值,最多128字节response.sendRedirect(redirect_openId_url +"appid=" + WechatUtil.appId + "&redirect_uri=" + URLEncoder.encode(url, "utf-8")+ "&response_type=code&scope=snsapi_base&state=1#wechat_redirect");// 如果用户同意授权,页面将跳转至 redirect_uri/?code=CODE&state=STATE。} catch (Exception e) {LOG.error("异常", e);}}
}

获取OpenID的微信接口2:

定义wechat/code接口:

@Controller
@RequestMapping("/wechat")
public class WechatController{private static final Logger LOG = LoggerFactory.getLogger(WechatController.class);@Autowiredprivate WxUserInfoMapper wxUserInfoMapper;// 经过微信重定向后,会返回code,然后根据code获取openid,并将openid返回给页面@RequestMapping(value = "/code/{menuType}", method = RequestMethod.GET)public ModelAndView code(HttpServletRequest request, HttpServletResponse response, String code,@PathVariable(value="menuType") Integer menuType) throws Exception {//通过code查openidString openId = WechatUtil.getOpenId(code);if (Utils.isEmptyTrim(openId)) {return WebUtil.getErrorPage("获取openId失败", LOG, "");}// 通过openid查用户信息JSONObject jsonObject = WechatUtil.getUserInfo(openId,0);LOG.debug("最新的微信用户信息:" + jsonObject.toJSONString());if (Utils.isEmpty(jsonObject, "openid")) {return WebUtil.getErrorPage("查询微信用户信息失败", LOG, "");}// 查数据库用户信息WxUserInfo wxUserInfo = wxUserInfoMapper.selectByOpenId(openId);if (!Utils.isEmpty(jsonObject, "openid")) {if (wxUserInfo == null) {// 如果数据库没有就插入用户信息wxUserInfo = new WxUserInfo();wxUserInfo.setAppId(WechatUtil.appId);wxUserInfo.setOpenid(openId);wxUserInfo.setCreateTime(new Date());}wxUserInfo.setNickname(jsonObject.getString("nickname"));wxUserInfo.setHeadimgurl(jsonObject.getString("headimgurl"));wxUserInfo.setSex(jsonObject.getInteger("sex"));wxUserInfo.setCity(jsonObject.getString("city"));wxUserInfo.setProvince(jsonObject.getString("province"));wxUserInfo.setCountry(jsonObject.getString("country"));wxUserInfo.setSubscribe(jsonObject.getInteger("subscribe"));wxUserInfo.setSubscribeTime(jsonObject.getLong("subscribe_time"));wxUserInfo.setGroupid(jsonObject.getInteger("groupid"));wxUserInfo.setRemark(jsonObject.getString("remark"));if (!Utils.isEmpty(jsonObject.getString("unionid"))) {wxUserInfo.setUnionid(jsonObject.getString("unionid"));}wxUserInfo.setUpdateTime(new Date());if (Utils.isEmpty(wxUserInfo.getUserId())) {wxUserInfoMapper.insertSelective(wxUserInfo);LOG.debug("添加微信用户信息成功");} else {wxUserInfoMapper.updateByPrimaryKeySelective(wxUserInfo);LOG.debug("更新微信用户信息成功");}request.getSession().setAttribute("openId", openId);if (menuType==0) {return new ModelAndView("forward:goods_list");//跳转至商品首页}else if(menuType==1) {return new ModelAndView("forward:user_list");}}return WebUtil.getErrorPage("查询微信用户信息失败", LOG, "");}
}
public class WechatUtil {public static final String get_openId_url = "https://api.weixin.qq.com/sns/oauth2/access_token?";/*** 通过授权令牌获取用户openId*/public static String getOpenId(String code) {String openid = null;StringBuilder url = new StringBuilder();url.append(get_openId_url);url.append("&appid=" + appId);url.append("&secret=" + appSecret);url.append("&code=").append(code);url.append("&grant_type=authorization_code");log.debug("url=" + url.toString());String ret = sendDataHttpsViaGet(url.toString());log.debug("获取openId" + ret);JSONObject obj = JSONObject.parseObject(ret);String errcode = obj.getString("errcode");if (StringUtils.isBlank(errcode)) {openid = obj.getString("openid");}return openid;}
}

假如用户

微信的接口鉴权机制

获取openId其实只是获取用户相关信息,我们要想调用后续的其他接口,还必须获取,access_token和jsapi_ticket。

获取jsapi_ticket之前需要获取access_token,access_token是微信公众号开发所有后台接口都需要传入的参数。

jsapi_ticket

jsapi_ticket是前端js接口声明(wx.config)时所需的信息要用到。

$(function() {$.ajax({url : '${ctx}/goods/sign.do',async : false,data : {'url' : window.location.href},dataType : 'json',type : 'post',success : function(result) {var data = result.data;if (data.appid == null || data.appid == "") {return;}wx.config({debug : false,appId : data.appid,timestamp : data.timestamp,nonceStr : data.nonceStr,signature : data.signature,//这个signature就是jsapi_ticket通过加密解密转化来的。jsApiList : [ "chooseWXPay" ]});}});wx.error(function(res) {alert("页面鉴权失败:"+res);});wx.ready(function() {});
});
@Controller
@RequestMapping("/goods")
public class GoodsController extends WechatController {private static final Logger LOG = LoggerFactory.getLogger(GoodsController.class);@RequestMapping(value = "/sign.do")@ResponseBodypublic Map<String, Object> sign(HttpServletRequest request, HttpServletResponse response, String url) {if (StringUtils.isBlank(url)) {LOG.info("url传递失败");return PortalUtil.fail("传递失败");}Map<String, String> ret = WechatUtil.sign(url);LOG.debug("ret:=" + ret);ret.put("appid", WechatUtil.appId);return PortalUtil.success(ret);}
}
public class WechatUtil {public static Map<String, String> sign(String url) {Map<String, String> ret = new HashMap<String, String>();String nonceStr = createNonceStr();String timestamp = createTimestamp();String string1;String signature = "";// 注意这里参数名必须全部小写,且必须有序string1 = "jsapi_ticket=" + getJsapiTicket() + "&noncestr=" + nonceStr + "&timestamp=" + timestamp + "&url=" + url;try {MessageDigest crypt = MessageDigest.getInstance("SHA-1");crypt.reset();crypt.update(string1.getBytes("UTF-8"));signature = byteToHex(crypt.digest());} catch (NoSuchAlgorithmException e) {e.printStackTrace();} catch (UnsupportedEncodingException e) {e.printStackTrace();}ret.put("url", url);ret.put("nonceStr", nonceStr);ret.put("timestamp", timestamp);ret.put("signature", signature);return ret;}public static final String get_jsapi_ticket_url="https://api.weixin.qq.com/cgi-bin/ticket/getticket?";private static String jsapiTicket;private static long jsapiTicketTime;// 获取凭证时的时间,单位:毫秒数,System.currentTimeMillisprivate static int jsapiTicketExpireTime;// 凭证有效时间,单位:秒private static void clearJsapiTicket() {jsapiTicket = "";jsapiTicketExpireTime = 0;jsapiTicketTime=0;}public static String getJsapiTicket() {if (!Utils.isEmptyTrim(jsapiTicket) && jsapiTicketTime > 0 && jsapiTicketExpireTime > 0) {long extime = jsapiTicketTime + (jsapiTicketExpireTime * 1000);long nowTime = System.currentTimeMillis();if (extime - nowTime > 1000) {// 仍然有效return jsapiTicket;}}try {String url = get_jsapi_ticket_url+"access_token=" + getAccessToken()+ "&type=jsapi";String response = sendDataHttpsViaGet(url);JSONObject json = JSONObject.parseObject(response);log.info(json.toJSONString());String errmsg=json.getString("errmsg");String errcode=json.getString("errcode");if(!errmsg.equals("ok")&&!errcode.equals("0")){return null;}log.debug("获取到新的JsapiTicket:"+json.toJSONString());jsapiTicketExpireTime = json.getIntValue("expires_in");jsapiTicket = json.getString("ticket");jsapiTicketTime=System.currentTimeMillis();} catch (Exception e) {LOG.error("getjsapiTicket-error", e);clearJsapiTicket();}LOG.debug("jsapiTicket===" + jsapiTicket);System.out.println("jsapiTicket===" + jsapiTicket);return jsapiTicket;}
}

access_token

要想获取jsapi_ticket,就要先获取access_token。
不要频繁去调用微信接口获取access_token,每天调用获取access_token接口的次数微信有限制,只需要判断过期时再去获取access_token。

public class WechatUtil {public static final String get_access_token_url="https://api.weixin.qq.com/cgi-bin/token?";private static String accessToken;private static long accessTokenTime;// 获取凭证时的时间,单位:毫秒数,System.currentTimeMillisprivate static int accessTokenExpireTime;// 凭证有效时间,单位:秒private static void clearAccessToken() {accessToken = "";accessTokenExpireTime = 0;accessTokenTime=0;}public static String getAccessToken() {if (!Utils.isEmptyTrim(accessToken) && accessTokenTime > 0 && accessTokenExpireTime > 0) {long extime = accessTokenTime + (accessTokenExpireTime * 1000);long nowTime = System.currentTimeMillis();if (extime - nowTime > 1000) {// 仍然有效return accessToken;}}try {String url = get_access_token_url + "&grant_type=client_credential&appid=" + appId + "&secret="+ appSecret;log.debug("进来url:="+url);String response = sendDataHttpsViaGet(url);if(StringUtils.isBlank(response)){log.debug("response为空:="+response);clearAccessToken();return null;}JSONObject json = JSONObject.parseObject(response);if(json==null){log.debug("json为空:="+json);clearAccessToken();return null;}if(StringUtils.isNotBlank(json.getString("errcode"))){clearAccessToken();return null;}log.debug("获取到新的JsapiTicket:"+json.toJSONString());accessTokenExpireTime = json.getIntValue("expires_in");accessToken = json.getString("access_token");accessTokenTime=System.currentTimeMillis();} catch (Exception e) {LOG.error("getAccessToken-error", e);clearAccessToken();}LOG.debug("accessToken===" + accessToken);System.out.println("accessToken===" + accessToken);return accessToken;}
}

准备好固定的资源

 //公众号appidpublic static final String appId = "wxf546a5afce347110";//公众号appSecretpublic static final String appSecret = "wxf546a5afce347xxx";//商户号public static final String wxMerchantNo = "1272569000";//商户签名加密key key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置public static final String wxMerchantApiKey = "xxxx";//私钥信息的证书文件private static final String certFilePath = "C:\\Users\\Public\\Downloads\\apiclient_cert.p12";

设计我们的表结构

这里只列出最核心的用户表、微信用户表、订单表,其他的表跟微信支付的业务关系不大。

用户表

public class UserInfo {/*** 用户ID*/private Long id;/*** 用户名称*/private String username;/*** 手机号*/private String phoneNumber;/*** 用户密码*/private String password;/*** 有效性,-1删除,0禁用,1有效*/private Integer valid;/*** 创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;
}

微信用户表

public class WxUserInfo {/*** 微信用户ID,*/private Long id;/*** 用户ID,关联用户表的id,可以为空(除非你的公众号是不注册就不让用。。。)*/private Long userId;/*** 是否关注,0=用户未关注公众号,1=用户关注了公众号*/private Integer subscribe;/*** 微信appid*/private String appId;/*** 微信openId*/private String openid;/*** 昵称*/private String nickname;/*** 用户的性别,值为1时是男性,值为2时是女性,值为0时是未知*/private Integer sex;/*** 城市*/private String city;/*** 国家*/private String country;/*** 省份*/private String province;/*** 头像*/private String headimgurl;/*** 订阅时间*/private Long subscribeTime;/*** 微信unionid,用来关联小程序*/private String unionid;/*** 备注*/private String remark;/*** 用户所在的分组ID*/private Integer groupid;/*** 创建时间*/private Date createTime;/*** 更新时间*/private Date updateTime;
}

订单表

public class OrderInfo {/*** 主键 数据库列名:<order_id> 数据库类型:<bigint> 内容长度:<19> 默认值:<null> 是否允许空值:<false>*/private Long orderId;/*** 商户订单号,也就是我们自己的订单号,需要唯一,微信要求最长不超过32个字符 数据库列名:<out_trade_no>* 数据库类型:<varchar> 内容长度:<32> 默认值:<null> 是否允许空值:<false>*/private String outTradeNo;/*** appid 数据库列名:<app_id> 数据库类型:<varchar> 内容长度:<50> 默认值:<null> 是否允许空值:<false>*/private String appId;/*** 支付金额(微信要求单位:分) 数据库列名:<total_fee> 数据库类型:<varchar> 内容长度:<100> 默认值:<null>* 是否允许空值:<false>*/private String totalFee;/*** 请求ip 数据库列名:<spbill_create_ip> 数据库类型:<varchar> 内容长度:<512> 默认值:<null>* 是否允许空值:<true>*/private String spbillCreateIp;/*** 订单交易类型 取值如下:JSAPI,NATIVE,APP,WAP 数据库列名:<trade_type> 数据库类型:<varchar>* 内容长度:<30> 默认值:<null> 是否允许空值:<false>*/private String tradeType;/*** 商户号,微信支付商户唯一标识 数据库列名:<mch_id> 数据库类型:<varchar> 内容长度:<30> 默认值:<null>* 是否允许空值:<false>*/private String mchId;/*** openid 数据库列名:<open_id> 数据库类型:<varchar> 内容长度:<100> 默认值:<null>* 是否允许空值:<true>*/private String openId;/*** 微信订单号 数据库列名:<transaction_id> 数据库类型:<varchar> 内容长度:<64> 默认值:<null>* 是否允许空值:<true>*/private String transactionId;/*** 订单状态,1=生成待提交,2=生成已提交,3=支付完成,4=支付已取消 数据库列名:<status> 数据库类型:<tinyint>* 内容长度:<3> 默认值:<null> 是否允许空值:<false>*/private Integer status;public static final Integer status_1 = 1;public static final Integer status_2 = 2;public static final Integer status_3 = 3;public static final Integer status_4 = 4;public static final Map<Integer, String> status_map = new LinkedHashMap<Integer, String>(4);static {status_map.put(status_1, "待生成");status_map.put(status_2, "待支付");status_map.put(status_3, "支付成功");status_map.put(status_4, "支付失败");}private String statusDetail;public String getStatusDetail() {statusDetail = status_map.get(status);return statusDetail;}public void setStatusDetail(String statusDetail) {this.statusDetail = statusDetail;}/*** 退款状态 0:未退款 1:退款成功 2:退款失败 3:退款中*/private Integer refundStatus;private String refundStatusDesc;public String getRefundStatusDesc() {this.refundStatusDesc = REFUND_STATUS_MAP.get(this.refundStatus);return this.refundStatusDesc;}public static Map<Integer, String> REFUND_STATUS_MAP = new HashMap<Integer, String>();static {REFUND_STATUS_MAP.put(Constant.PAY_ORDER_FLOW_REFUND_STATUS_NOT, "待审核未退款");REFUND_STATUS_MAP.put(Constant.PAY_ORDER_FLOW_REFUND_STATUS_SUCCESS, "退款成功");REFUND_STATUS_MAP.put(Constant.PAY_ORDER_FLOW_REFUND_STATUS_FAIL, "退款失败");REFUND_STATUS_MAP.put(Constant.PAY_ORDER_FLOW_REFUND_STATUS_ING, "退款中");REFUND_STATUS_MAP.put(Constant.PAY_ORDER_FLOW_REFUND_STATUS_APPROVAL_AGREE, "审核通过待退款");REFUND_STATUS_MAP.put(Constant.PAY_ORDER_FLOW_REFUND_STATUS_APPROVAL_REJECT, "审核拒绝");}/*** 退款时间*/private Date refundTime;/*** 退款失败原因*/private String refundReason;private String refundOrderNo;/*** 备注 数据库列名:<remark> 数据库类型:<varchar> 内容长度:<128> 默认值:<null> 是否允许空值:<true>*/private String remark;/*** 数据创建时间 数据库列名:<create_date> 数据库类型:<timestamp> 内容长度:<0>* 默认值:<CURRENT_TIMESTAMP> 是否允许空值:<false>*/private Date createDate;/*** 数据修改时间 数据库列名:<update_date> 数据库类型:<timestamp> 内容长度:<0> 默认值:<null>* 是否允许空值:<true>*/private Date updateDate;/*** 微信昵称*/private String nickname;/*** 回调时间*/private Date callbackDate;
}

开发

编写支付页面,使用wx.chooseWXPay调起微信支付界面

<html>
<script type="text/javascript">
$.ajax({url : '${ctx}/goods/sign.do',async : false,data : {'url' : window.location.href},dataType : 'json',type : 'post',success : function(result) {var data = result.data;if (data.appid == null || data.appid == "") {return;}wx.config({debug : false,appId : data.appid,timestamp : data.timestamp,nonceStr : data.nonceStr,signature : data.signature,jsApiList : [ "chooseWXPay" ]});}
});
wx.error(function(res) {alert("页面鉴权失败:"+res);
});
wx.ready(function() {});var flag = false;
function pay(btn) {if (flag == true) {alert("您已经支付过了,请不要重复支付哦!");return;}$.ajax({url : '${ctx}/wechat/pay.do',//后端提供wx.chooseWXPay的参数type : 'post','data' : {"productId" : $("#productId").val(),"openId" : $("#openId").val()},dataType : 'json',aysnc : false,success : function(res) {if (res && res.code) {if(res.code == "200"){var data = res.data;wx.chooseWXPay({'debug' : true,'timestamp': data.timeStamp,'nonceStr': data.nonceStr,'package': 'prepay_id=' + data.prepayId, 'signType': 'MD5','paySign': data.sign,success : function(res) {flag = true;alert("支付成功");$(btn).prop("disabled", "disabled");},fail:function(err) {alert("系统错误:"+err);}});}else{alert("系统错误:"+res.msg);}} else {alert("系统错误:"+res);}}});
}
</script>
展示商品信息
<input id="productId" name="productId" value="${(product.productId)!''}" type="hidden"/>
<input id="openId" name="openId" value="${openId!''}" type="hidden"/>
<input type="button" id="payBtn" value="支付" onclick="pay(this)"/>
</html>

wx.chooseWXPay详解:

wx.chooseWXPay({timeStamp: 0, // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符nonceStr: '', // 支付签名随机串,不长于 32 位package: '', // 统一支付接口返回的prepay_id参数值,提交格式如:prepay_id=***)signType: '', // 签名方式,默认为'SHA1',使用新版支付需传入'MD5'paySign: '', // 支付签名success: function (res) {// 支付成功后的回调函数}
});

wx.chooseWXPay有一个替代品:

WeixinJSBridge.invoke('getBrandWCPayRequest', {"appId" : "wx2421b1c4370ec43b",     //公众号名称,由商户传入     "timeStamp":" 1395712654",         //时间戳,自1970年以来的秒数     "nonceStr" : "e61463f8efa94090b1f366cccfbbb444", //随机串     "package" : "prepay_id=u802345jgfjsdfgsdg888",     "signType" : "MD5",         //微信签名方式:     "paySign" : "70EA570631E4BB79628FBCA90534C63FF7FADD89" //微信签名 },function(res){     if(res.err_msg == "get_brand_wcpay_request:ok" ) {}     // 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回    ok,但并不保证它绝对可靠。 });

编写后台支付接口/wechat/pay.do

 //订单商品清单public static class ItemDTO{//示例伪代码private long productId;private int count;}// 下单,生成prepay_id给前端H5,然后通过jsapi调用wx.chooseWXPay@RequestMapping(value = "/pay.do", method = RequestMethod.POST)@ResponseBodypublic Object pay(HttpServletRequest request, HttpServletResponse response, List<ItemDTO> items,String openId) throws IOException {String logPrefix = "支付>>>";LOG.debug(logPrefix + "参数,"+items);if (Utils.isEmpty(openId)) {return JsonResult.getFailResultAndLog("openId为空", LOG, logPrefix);}WxUserInfo wxUser = wxUserInfoMapper.selectByOpenId(openId);if (wxUser == null) {return JsonResult.getFailResultAndLog("微信用户未录入", LOG, logPrefix);}WeixinResponseDTO  weixinResponse= WechatUtil.pay(request, items, openId);if (weixinResponse == null) {return JsonResult.getFailResultAndLog("发起支付失败", LOG, logPrefix);}// LOG.debug(logPrefix + "前端需要的支付参数config=" + config);Map<String, String> wxResp = WechatUtil.getPayConfig(weixinResponse.getPrepay_id());return JsonResult.getSuccessResultByData(wxResp);}
public class WechatUtil {public static Map<String, String> getPayConfig(String prepay_id) {SortedMap<String, String> json = new TreeMap<String, String>();json.put("appId", appId);json.put("timeStamp", System.currentTimeMillis() + "");json.put("nonceStr", UUID.randomUUID().toString().replaceAll("-", ""));json.put("package", "prepay_id=" + prepay_id);json.put("signType", "MD5");String sign = getSign(json, wxMerchantApiKey);json.put("paySign", sign);log.debug("支付json:" + JSONObject.toJSONString(json));return json;}
}

调用微信支付统一接口unifiedorder

微信统一下单参数封装

public class WeixinOrderDTO {/*** appId 必填*/private String appId;/*** 商户号  必填*/private String machId;/*** 设备号*/private String deviceInfo;/*** 随机字符串   必填*/private String nonceStr;/*** 签名  必填*/private String sign;/*** 商品描述  必填*/private String body;/*** 商品详情*/private String detail;/*** 附加数据*/private String attach;/*** 商户订单号   必填*/private String outTradeNo;/***货币类型*/private String feeType;/*** 总金额   必填*/private int totalFee;/*** 终端Ip   必填*/private String spbillCreateIp;/*** 交易起始时间*/private String timeStart;/*** 交易结束时间*/private String timeExpire;/*** 商品标记*/private String goodsTag;/*** 通知地址  必填*/private String notifyUrl;/*** 交易类型  必填*/private String tradeType;/*** 商品ID*/private String productId;/*** 指定支付方式*/private String limitPay;/*** 用户标识*/private String openId;/*** 微信退款商户唯一订单号*/private String outRefundNo;public String getAppId() {return appId;}public void setAppId(String appId) {this.appId = appId;}public String getMachId() {return machId;}public void setMachId(String machId) {this.machId = machId;}public String getDeviceInfo() {return deviceInfo;}public void setDeviceInfo(String deviceInfo) {this.deviceInfo = deviceInfo;}public String getNonceStr() {return nonceStr;}public void setNonceStr(String nonceStr) {this.nonceStr = nonceStr;}public String getSign() {return sign;}public void setSign(String sign) {this.sign = sign;}public String getBody() {return body;}public void setBody(String body) {this.body = body;}public String getDetail() {return detail;}public void setDetail(String detail) {this.detail = detail;}public String getAttach() {return attach;}public void setAttach(String attach) {this.attach = attach;}public String getOutTradeNo() {return outTradeNo;}public void setOutTradeNo(String outTradeNo) {this.outTradeNo = outTradeNo;}public String getFeeType() {return feeType;}public void setFeeType(String feeType) {this.feeType = feeType;}public int getTotalFee() {return totalFee;}public void setTotalFee(int totalFee) {this.totalFee = totalFee;}public String getSpbillCreateIp() {return spbillCreateIp;}public void setSpbillCreateIp(String spbillCreateIp) {this.spbillCreateIp = spbillCreateIp;}public String getTimeStart() {return timeStart;}public void setTimeStart(String timeStart) {this.timeStart = timeStart;}public String getTimeExpire() {return timeExpire;}public void setTimeExpire(String timeExpire) {this.timeExpire = timeExpire;}public String getGoodsTag() {return goodsTag;}public void setGoodsTag(String goodsTag) {this.goodsTag = goodsTag;}public String getNotifyUrl() {return notifyUrl;}public void setNotifyUrl(String notifyUrl) {this.notifyUrl = notifyUrl;}public String getTradeType() {return tradeType;}public void setTradeType(String tradeType) {this.tradeType = tradeType;}public String getProductId() {return productId;}public void setProductId(String productId) {this.productId = productId;}public String getLimitPay() {return limitPay;}public void setLimitPay(String limitPay) {this.limitPay = limitPay;}public String getOpenId() {return openId;}public void setOpenId(String openId) {this.openId = openId;}public String getOutRefundNo() {return outRefundNo;}public void setOutRefundNo(String outRefundNo) {this.outRefundNo = outRefundNo;}}

微信统一下单接口调用

public class WechatUtil {// 商户号public static final String wxMerchantNo = "1272569000";/*** 商户签名加密key key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置*/public static final String wxMerchantApiKey = "xxxx";/*** 微信二维码支付-微信统一下单*/public static WeixinResponseDTO pay(HttpServletRequest request, List<ItemDTO> products, String openId) {String outTradeNo = genOutTradeNo();String wxOrderShowName = "赶快付款,来不及解释了!";LOG.debug("微信统一下单>>微信支付流水单号:" + outTradeNo + ":==" + products);OrderInfo orderInfo = new OrderInfo();orderInfo.setStatus(OrderInfo.status_1);orderInfo.setOutTradeNo(outTradeNo);orderInfo.setOpenId(openId);// 根据商品和数量计算出金额// ...// 生成订单插入数据库orderInfoMapper.insert(orderInfo);WeixinOrderDTO wxOrder = new WeixinOrderDTO();wxOrder.setAppId(appId);wxOrder.setBody(wxOrderShowName);// 支付界面显示的标题wxOrder.setOutTradeNo(outTradeNo);// 我们自己的订单号// 商户号 必填wxOrder.setMachId(wxMerchantNo);wxOrder.setNonceStr(UUID.randomUUID().toString().replaceAll("-", ""));String notifyUrl = "http://www.xuexibisai.com/wechat/callback.do";wxOrder.setNotifyUrl(notifyUrl);wxOrder.setOpenId(openId);wxOrder.setTotalFee(new BigDecimal(orderInfo.getTotalFee()).multiply(new BigDecimal(100)).intValue());// 金额单位元转分wxOrder.setTradeType("JSAPI");wxOrder.setSpbillCreateIp("127.0.0.1");String str = unifiedorder(wxOrder, wxMerchantApiKey);LOG.debug("微信统一下单>>微信返回数据:" + str);if (StringUtils.isBlank(str)) {LOG.error("微信统一下单>>微信统一下单失败,微信未返回数据");return null;}XStream xs = new XStream(new DomDriver());xs.alias("xml", WeixinResponseDTO.class);WeixinResponseDTO weixinResponse = (WeixinResponseDTO) xs.fromXML(str);if (weixinResponse == null) {LOG.error("微信统一下单>>解析微信返回数据失败");return weixinResponse;}if (StringUtils.isBlank(weixinResponse.getResult_code()) || StringUtils.isBlank(weixinResponse.getReturn_code()) || !weixinResponse.getResult_code().equals("SUCCESS")|| !weixinResponse.getReturn_code().equals("SUCCESS")) {LOG.error("微信统一下单>>微信返回数据return_code为失败");return weixinResponse;}boolean flag = validateResponseSign(weixinResponse, wxMerchantApiKey);if (!flag) {LOG.error("微信统一下单>>微信返回数据apikey校验失败");return weixinResponse;}String prepay_id = weixinResponse.getPrepay_id();LOG.debug("微信统一下单>>prepay_id:" + prepay_id);if (!Utils.isEmptyTrim(prepay_id)) {orderInfo.setStatus(OrderInfo.status_2);orderInfoMapper.update(orderInfo);}return weixinResponse;}public static boolean validateResponseSign(WeixinResponseDTO weixinResponse, String apiKey) {Map<String, String> map = new TreeMap<String, String>();if (StringUtils.isNotBlank(weixinResponse.getAppid())) {map.put("appid", weixinResponse.getAppid());}if (StringUtils.isNotBlank(weixinResponse.getMch_id())) {map.put("mch_id", weixinResponse.getMch_id());}if (StringUtils.isNotBlank(weixinResponse.getNonce_str())) {map.put("nonce_str", weixinResponse.getNonce_str());}if (StringUtils.isNotBlank(weixinResponse.getPrepay_id())) {map.put("prepay_id", weixinResponse.getPrepay_id());}if (StringUtils.isNotBlank(weixinResponse.getResult_code())) {map.put("result_code", weixinResponse.getResult_code());}if (StringUtils.isNotBlank(weixinResponse.getReturn_code())) {map.put("return_code", weixinResponse.getReturn_code());}if (StringUtils.isNotBlank(weixinResponse.getTrade_type())) {map.put("trade_type", weixinResponse.getTrade_type());}if (StringUtils.isNotBlank(weixinResponse.getReturn_msg())) {map.put("return_msg", weixinResponse.getReturn_msg());}if (StringUtils.isNotBlank(weixinResponse.getCode_url())) {map.put("code_url", weixinResponse.getCode_url());}if (StringUtils.isNotBlank(weixinResponse.getMweb_url())) {map.put("mweb_url", weixinResponse.getMweb_url());}if (StringUtils.isBlank(weixinResponse.getSign())) {return false;}String sign = getSign(map, apiKey);log.debug("-----------对比后的sign1111" + sign);return sign.equals(weixinResponse.getSign());}public static final String unifiedorder_url = "https://api.mch.weixin.qq.com/pay/unifiedorder";/*** 微信支付下单*/public static String unifiedorder(WeixinOrderDTO weixinOrder, String key) {Map<String, String> map = new TreeMap<String, String>();StringBuffer sb = new StringBuffer();sb.append("<xml>");sb.append("<appid><![CDATA[" + weixinOrder.getAppId() + "]]></appid>");if (StringUtils.isNotBlank(weixinOrder.getBody())) {sb.append("<body><![CDATA[" + weixinOrder.getBody() + "]]></body>");map.put("body", weixinOrder.getBody());}sb.append("<mch_id><![CDATA[" + weixinOrder.getMachId() + "]]></mch_id>");sb.append("<nonce_str><![CDATA[" + weixinOrder.getNonceStr() + "]]></nonce_str>");sb.append("<notify_url><![CDATA[" + weixinOrder.getNotifyUrl() + "]]></notify_url>");if (!StringUtils.isEmpty(weixinOrder.getOpenId())) {sb.append("<openid><![CDATA[" + weixinOrder.getOpenId() + "]]></openid>");}sb.append("<out_trade_no><![CDATA[" + weixinOrder.getOutTradeNo() + "]]></out_trade_no>");sb.append("<spbill_create_ip><![CDATA[" + weixinOrder.getSpbillCreateIp() + "]]></spbill_create_ip>");sb.append("<total_fee><![CDATA[" + weixinOrder.getTotalFee() + "]]></total_fee>");// 单位是分,不是元sb.append("<trade_type><![CDATA[" + weixinOrder.getTradeType() + "]]></trade_type>");map.put("appid", weixinOrder.getAppId());map.put("mch_id", weixinOrder.getMachId());map.put("nonce_str", weixinOrder.getNonceStr());map.put("notify_url", weixinOrder.getNotifyUrl());if (!StringUtils.isEmpty(weixinOrder.getOpenId())) {map.put("openid", weixinOrder.getOpenId());}map.put("out_trade_no", weixinOrder.getOutTradeNo());map.put("spbill_create_ip", weixinOrder.getSpbillCreateIp() + "");map.put("total_fee", weixinOrder.getTotalFee() + "");map.put("trade_type", weixinOrder.getTradeType());String sign = getSign(map, key);sb.append("<sign><![CDATA[" + sign + "]]></sign>");sb.append("</xml>");String str = null;log.debug("url:" + unifiedorder_url + "xml:" + sb.toString());try {str = postByBody(unifiedorder_url, sb.toString(), "text/xml;charset=utf-8");} catch (HttpException e) {log.error(e.getMessage(), e);e.printStackTrace();} catch (IOException e) {log.error(e.getMessage(), e);e.printStackTrace();}return str;}
}

统一下单返回结果示例:

"<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg>"+"<appid><![CDATA[wxf546a5afce347110]]></appid>"+"<mch_id><![CDATA[1272569110]]></mch_id>"+"<nonce_str><![CDATA[LHxao3Tj84t1xsKi]]></nonce_str>"+"<sign><![CDATA[358FD39B7F0B7333666CA55BDBC1C88F]]></sign>"+"<result_code><![CDATA[SUCCESS]]></result_code>"+"<transaction_id><![CDATA[4200000167201808189301147765]]></transaction_id>"+"<out_trade_no><![CDATA[180818456205632937]]></out_trade_no>"+"<out_refund_no><![CDATA[180818457380003106]]></out_refund_no>"+"<refund_id><![CDATA[50000507922018081805532655669]]></refund_id>"+"<refund_channel><![CDATA[]]></refund_channel>"+"<refund_fee>6000</refund_fee>"+"<coupon_refund_fee>12</coupon_refund_fee>"+"<total_fee>6000</total_fee>"+"<cash_fee>5988</cash_fee>"+"<coupon_refund_count>1</coupon_refund_count>"+"<coupon_refund_fee_0>12</coupon_refund_fee_0>"+"<coupon_refund_id_0><![CDATA[2000000042245560146]]></coupon_refund_id_0>"+"<cash_refund_fee>5988</cash_refund_fee>"+
"</xml>"

统一下单返回结果封装

import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import com.thoughtworks.xstream.io.xml.DomDriver;@XStreamAlias("xml")
public class WeixinResponseDTO {@XStreamAlias("return_code")private String return_code;@XStreamAlias("return_msg")private String return_msg;@XStreamAlias("appid")private String appid;@XStreamAlias("mch_appid")private String mch_appid;@XStreamAlias("mchid")private String mchid;@XStreamAlias("device_info")private String device_info;@XStreamAlias("partner_trade_no")private String partner_trade_no;@XStreamAlias("payment_no")private String payment_no;@XStreamAlias("payment_time")private String payment_time;@XStreamAlias("mch_id")private String mch_id;@XStreamAlias("nonce_str")private String nonce_str;@XStreamAlias("sign")private String sign;@XStreamAlias("result_code")private String result_code;@XStreamAlias("prepay_id")private String prepay_id;@XStreamAlias("trade_type")private String trade_type;//二维码@XStreamAlias("code_url")private String code_url;//app支付@XStreamAlias("mweb_url")    private String mweb_url;//退款参数@XStreamAlias("err_code")private String err_code;  //错误@XStreamAlias("err_code_des")private String err_code_des;    //错误信息@XStreamAlias("transaction_id")private String transaction_id;  //微信订单号@XStreamAlias("out_trade_no")private String out_trade_no; //商户系统内部的订单号@XStreamAlias("out_refund_no")private String out_refund_no;  //商户退款单号@XStreamAlias("refund_id")private String refund_id;      //微信退款单号@XStreamAlias("refund_channel")private String refund_channel;    //退款渠道@XStreamAlias("refund_fee")private int refund_fee;         //退款金额@XStreamAlias("coupon_refund_fee")private int coupon_refund_fee;   //代金券或立减优惠退款金额=订单金额-现金退款金额,注意:立减优惠金额不会退回@XStreamAlias("total_fee")private int total_fee;          //订单总金额@XStreamAlias("cash_fee")private int cash_fee;            //现金支付金额@XStreamAlias("coupon_refund_count")private int coupon_refund_count; //代金券或立减优惠使用数量  0@XStreamAlias("cash_refund_fee")private int cash_refund_fee;        //代金券或立减优惠退款金额  0//微信使用代金券购买退款时返回参数会增加,导致xml解析出错,退款失败 zw 2018年8月23日@XStreamAlias("coupon_refund_fee_0")private int coupon_refund_fee_0;@XStreamAlias("coupon_refund_id_0")private String coupon_refund_id_0;public String getReturn_code() {return return_code;}public void setReturn_code(String return_code) {this.return_code = return_code;}public String getReturn_msg() {return return_msg;}public void setReturn_msg(String return_msg) {this.return_msg = return_msg;}public String getAppid() {return appid;}public void setAppid(String appid) {this.appid = appid;}public String getMch_id() {return mch_id;}public void setMch_id(String mch_id) {this.mch_id = mch_id;}public String getNonce_str() {return nonce_str;}public void setNonce_str(String nonce_str) {this.nonce_str = nonce_str;}public String getSign() {return sign;}public void setSign(String sign) {this.sign = sign;}public String getResult_code() {return result_code;}public void setResult_code(String result_code) {this.result_code = result_code;}public String getPrepay_id() {return prepay_id;}public void setPrepay_id(String prepay_id) {this.prepay_id = prepay_id;}public String getTrade_type() {return trade_type;}public void setTrade_type(String trade_type) {this.trade_type = trade_type;}public String getTransaction_id() {return transaction_id;}public void setTransaction_id(String transaction_id) {this.transaction_id = transaction_id;}public String getOut_trade_no() {return out_trade_no;}public void setOut_trade_no(String out_trade_no) {this.out_trade_no = out_trade_no;}public String getOut_refund_no() {return out_refund_no;}public void setOut_refund_no(String out_refund_no) {this.out_refund_no = out_refund_no;}public String getRefund_id() {return refund_id;}public void setRefund_id(String refund_id) {this.refund_id = refund_id;}public String getRefund_channel() {return refund_channel;}public void setRefund_channel(String refund_channel) {this.refund_channel = refund_channel;}public int getRefund_fee() {return refund_fee;}public void setRefund_fee(int refund_fee) {this.refund_fee = refund_fee;}public int getCoupon_refund_fee() {return coupon_refund_fee;}public void setCoupon_refund_fee(int coupon_refund_fee) {this.coupon_refund_fee = coupon_refund_fee;}public int getTotal_fee() {return total_fee;}public void setTotal_fee(int total_fee) {this.total_fee = total_fee;}public int getCash_fee() {return cash_fee;}public void setCash_fee(int cash_fee) {this.cash_fee = cash_fee;}public int getCoupon_refund_count() {return coupon_refund_count;}public void setCoupon_refund_count(int coupon_refund_count) {this.coupon_refund_count = coupon_refund_count;}public int getCash_refund_fee() {return cash_refund_fee;}public void setCash_refund_fee(int cash_refund_fee) {this.cash_refund_fee = cash_refund_fee;}public String getCode_url() {return code_url;}public void setCode_url(String code_url) {this.code_url = code_url;}public boolean isSuccess(){return "SUCCESS".equalsIgnoreCase(this.getReturn_code()) && "SUCCESS".equalsIgnoreCase(this.getResult_code());}public String getErr_code() {return err_code;}public void setErr_code(String err_code) {this.err_code = err_code;}public String getErr_code_des() {return err_code_des;}public void setErr_code_des(String err_code_des) {this.err_code_des = err_code_des;}public int getCoupon_refund_fee_0() {return coupon_refund_fee_0;}public void setCoupon_refund_fee_0(int coupon_refund_fee_0) {this.coupon_refund_fee_0 = coupon_refund_fee_0;}public String getCoupon_refund_id_0() {return coupon_refund_id_0;}public void setCoupon_refund_id_0(String coupon_refund_id_0) {this.coupon_refund_id_0 = coupon_refund_id_0;}public String getMweb_url() {return mweb_url;}public void setMweb_url(String mweb_url) {this.mweb_url = mweb_url;}public String getMch_appid() {return mch_appid;}public void setMch_appid(String mch_appid) {this.mch_appid = mch_appid;}public String getMchid() {return mchid;}public void setMchid(String mchid) {this.mchid = mchid;}public String getDevice_info() {return device_info;}public void setDevice_info(String device_info) {this.device_info = device_info;}public String getPartner_trade_no() {return partner_trade_no;}public void setPartner_trade_no(String partner_trade_no) {this.partner_trade_no = partner_trade_no;}public String getPayment_no() {return payment_no;}public void setPayment_no(String payment_no) {this.payment_no = payment_no;}public String getPayment_time() {return payment_time;}public void setPayment_time(String payment_time) {this.payment_time = payment_time;}
}

回调接口/wechat/callback.do

 /*** 账单结算微信支付-支付回调* * @param body*            微信传递的消息体* @author tangzhichao 20200922 新增*/@RequestMapping(value = "/callback.do")public void balancePayNotify(HttpServletRequest request, HttpServletResponse resp) throws IOException {LOG.debug("账单结算微信二维码支付---支付回调...");try {String param = WebUtil.getParams(request);if (StringUtils.isBlank(param)) {LOG.error("支付回调>>>回调信息为空");WechatUtil.fail(resp);return;}XStream xs = new XStream(new DomDriver());xs.alias("xml", WeChatBuyPostDTO.class);WeChatBuyPostDTO weChatBuyPost = (WeChatBuyPostDTO) xs.fromXML(param);if (weChatBuyPost == null) {LOG.error("支付回调>>>回调信息解析失败");WechatUtil.fail(resp);return;}String outTradeNo = weChatBuyPost.getOut_trade_no();if (Utils.isEmptyTrim(outTradeNo)) {LOG.error("支付回调>>>回调信息out_trade_no为空");WechatUtil.fail(resp);return;}LOG.debug("支付回调>>>订单Id:{}", outTradeNo);OrderInfo wxPayMonthBalanceOrder = orderInfoMapper.selectByOutTradeNo(outTradeNo);// 根据我们的订单号去数据库查出对应的订单if (wxPayMonthBalanceOrder == null) {LOG.error("支付回调>>>未找到订单号" + outTradeNo);WechatUtil.fail(resp);return;}if (StringUtils.isBlank(weChatBuyPost.getResult_code()) || StringUtils.isBlank(weChatBuyPost.getReturn_code()) || !weChatBuyPost.getResult_code().equals("SUCCESS")) {LOG.error("支付回调>>>回调信息result_code返回失败,具体信息:{}", JSONObject.toJSONString(weChatBuyPost));failAndUp(resp, wxPayMonthBalanceOrder, weChatBuyPost);return;}if (!WechatUtil.verifySign(param, WechatUtil.wxMerchantApiKey)) {LOG.error("支付回调>>>回调信息apikey校验失败");failAndUp(resp, wxPayMonthBalanceOrder, weChatBuyPost);return;}LOG.debug("支付回调>>>debug-判断status>>>" + wxPayMonthBalanceOrder.getStatus());if (WxPayMonthBalanceOrder.status_2.equals(wxPayMonthBalanceOrder.getStatus())) {// ...你的业务逻辑} else {LOG.debug("支付回调>>>重复回调...");}LOG.debug("支付回调>>>微信回调成功");} catch (Exception e) {LOG.error("支付回调>>>异常", e);failByCallback(resp);return;}LOG.debug("支付回调>>>debug-响应微信文件流>>>");WechatUtil.outText(resp, "text/xml", "utf-8", "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>");}private void failAndUp(HttpServletResponse resp, OrderInfo orderInfo, WeChatBuyPostDTO weChatBuyPost) throws IOException {orderInfo.setStatus(OrderInfo.status_4);orderInfoMapper.update(orderInfo);WechatUtil.fail(resp);}
public class WechatUtil {public static boolean verifySign(String xml, String key){Map<String,String> params = new TreeMap<String,String>();String primtiveSign = "";try {Document doc = DocumentHelper.parseText(xml);Element root = doc.getRootElement();Iterator<Element> it = root.elementIterator();while(it.hasNext()){Element e = it.next();if("sign".equals(e.getName().trim())){primtiveSign = e.getTextTrim();continue;}if(StringUtils.isNotBlank(e.getText())){params.put(e.getName(), e.getTextTrim());}}String newSign = getSign(params,key);boolean res =  newSign.equals(primtiveSign);log.debug("WeiXinManager:verifySign---->支付成功检查是否合法sign:" + res);return res;} catch (DocumentException e) {log.error("WeiXinManager:verifySign--->message = {}, trace = {}", e.getMessage(), e);}return false;}
}

退款WechatUtil .refund(OrderInfo orderInfo)

public class WechatUtil {private static final String certFilePath = "C:\\Users\\Public\\Downloads\\apiclient_cert.p12";public static void refund(OrderInfo orderInfo){if (StringUtils.isBlank(orderInfo.getRefundOrderNo())) {orderInfo.setRefundOrderNo(genRefundOrderNo());}orderInfo.setRefundStatus(Constant.PAY_ORDER_FLOW_REFUND_STATUS_ING);int row = orderInfoMapper.updateByPrimaryKey(orderInfo);if (row == 0) {LOG.error("更新退款 商户订单号失败:{}", orderInfo.getOrderId());return;}LOG.debug("修改退款操作,将订单改为退款中:" + orderInfo.getOrderId());WeixinOrderDTO order = new WeixinOrderDTO();order.setAppId(appId);order.setMachId(wxMerchantNo);order.setOutTradeNo(orderInfo.getOutTradeNo());order.setNonceStr(UUID.randomUUID().toString().replaceAll("-", ""));order.setTotalFee(orderInfo.getTotalFee().multiply(new BigDecimal(100)).intValue());//微信对此退款要求使用同一个商户退款订单号order.setOutRefundNo(orderInfo.getRefundOrderNo());String str = refund(order, wxMerchantApiKey, certFilePath);if (StringUtils.isEmpty(str)) {LOG.error("退款失败,请求退款返回异常");orderInfo.setRefundReason("退款失败,请求退款返回异常");}LOG.debug("退款请求返回:" + str);XStream xs = new XStream(new DomDriver());xs.alias("xml", WeixinResponseDTO.class);WeixinResponseDTO weixinResponse = (WeixinResponseDTO) xs.fromXML(str);if (weixinResponse == null) {LOG.error("解析xml失败");orderInfo.setRefundReason("退款失败,退款结果解析失败");// return;}boolean flag = validateResponseSignByRefund(weixinResponse, wxMerchantApiKey);if (!flag) {LOG.error("签名错误");/*** 注释return,此处会造成微信返回失败时直接return,出现退款失败的订单一直处于退款中状态,无法重新退款*/// return;orderInfo.setRefundReason("签名错误");}// 微信退款单号String refundId = weixinResponse.getRefund_id();LOG.debug("微信退款单号:" + refundId);if (weixinResponse.isSuccess()) {orderInfo.setRefundStatus(Constant.PAY_ORDER_FLOW_REFUND_STATUS_SUCCESS);orderInfo.setRefundReason(weixinResponse.getReturn_msg());LOG.debug("微信退款成功" + orderInfo.getOrderId());//...你的业务逻辑}else {orderInfo.setRefundStatus(Constant.PAY_ORDER_FLOW_REFUND_STATUS_FAIL);//注释,此处微信返回的return_msg只是通信的结果信息,并不是退款失败的原因// orderInfo.setRefundReason(weixinResponse.getReturn_msg());if (weixinResponse.getResult_code().equalsIgnoreCase("FAIL")) {orderInfo.setRefundReason(weixinResponse.getErr_code_des());}}orderInfo.setRefundTime(new Date());orderInfoMapper.updateByPrimaryKey(orderInfo);}private static int se = 0;public static synchronized String genRefundOrderNo() {String nextFrozeId = "";if (se > 99)se = 0;nextFrozeId = new SimpleDateFormat("yyyyMMdd").format(new java.util.Date()).substring(2, 8)+ ("" + new java.util.Date().getTime()).substring(3, 13) + createSerial("" + se, 2);se++;if (nextFrozeId.length() != 18)throw new RuntimeException("申请号长度错误[" + nextFrozeId + "]");return nextFrozeId;}public static String createSerial(String src, int len) {String dest = "";if (src.length() >= len) {dest = src.substring(0, len);} else {dest = createSameChar("0", len - src.length()) + src;}return dest;}public static String createSameChar(String src, int len) {StringBuffer sb = new StringBuffer();for (int i = 0; i < len; i++) {sb.append(src);}return sb.toString();}/*** 微信退款校验Sign* @return*/public static boolean validateResponseSignByRefund(WeixinResponseDTO weixinResponse, String apiKey){Map<String, String> map = new TreeMap<String, String>();if (StringUtils.isNotBlank(weixinResponse.getAppid())) {map.put("appid", weixinResponse.getAppid());}if (StringUtils.isNotBlank(weixinResponse.getMch_id())) {map.put("mch_id", weixinResponse.getMch_id());}if (StringUtils.isNotBlank(weixinResponse.getNonce_str())) {map.put("nonce_str", weixinResponse.getNonce_str());}if (StringUtils.isNotBlank(weixinResponse.getResult_code())) {map.put("result_code", weixinResponse.getResult_code());}if (StringUtils.isNotBlank(weixinResponse.getReturn_code())) {map.put("return_code", weixinResponse.getReturn_code());}if (StringUtils.isNotBlank(weixinResponse.getReturn_msg())) {map.put("return_msg", weixinResponse.getReturn_msg());}if (StringUtils.isNotBlank(weixinResponse.getTransaction_id())) {map.put("transaction_id", weixinResponse.getTransaction_id());}if (StringUtils.isNotBlank(weixinResponse.getOut_trade_no())) {map.put("out_trade_no", weixinResponse.getOut_trade_no());}if (StringUtils.isNotBlank(weixinResponse.getOut_refund_no())) {map.put("out_refund_no", weixinResponse.getOut_refund_no());}if (StringUtils.isNotBlank(weixinResponse.getRefund_id())) {map.put("refund_id", weixinResponse.getRefund_id());}if (StringUtils.isNotBlank(weixinResponse.getRefund_channel())) {map.put("refund_channel", weixinResponse.getRefund_channel());}if (weixinResponse.getRefund_fee() >= 0) {map.put("refund_fee", weixinResponse.getRefund_fee()+"");}if (weixinResponse.getCoupon_refund_fee() >= 0) {map.put("coupon_refund_fee", weixinResponse.getCoupon_refund_fee()+"");}if (weixinResponse.getTotal_fee() >= 0) {map.put("total_fee", weixinResponse.getTotal_fee()+"");}if (weixinResponse.getCash_fee() >= 0) {map.put("cash_fee", weixinResponse.getCash_fee()+"");}if (weixinResponse.getCoupon_refund_count() >= 0) {map.put("coupon_refund_count", weixinResponse.getCoupon_refund_count()+"");}if (weixinResponse.getCash_refund_fee() >= 0) {map.put("cash_refund_fee", weixinResponse.getCash_refund_fee()+"");}if (StringUtils.isBlank(weixinResponse.getSign())) {return false;}String sign = getSign(map, apiKey);LOG.debug("sign:"+sign);LOG.debug("requestSign:"+weixinResponse.getSign());return sign.equals(weixinResponse.getSign());}public static final String refund_url = "https://api.mch.weixin.qq.com/secapi/pay/refund";/*** 微信退款资金来源:refund_account* REFUND_SOURCE_UNSETTLED_FUNDS:未结算资金退款(默认使用未结算资金退款),这种资金来源,只能使用订单收入的金额,不确定性较大,容易出现退款失败* REFUND_SOURCE_RECHARGE_FUNDS:可用余额退款,这种方式稳定,账户资金不够时直接充值就可以了*/public static final String REFUND_SOURCE_UNSETTLED_FUNDS = "REFUND_SOURCE_UNSETTLED_FUNDS";public static final String REFUND_SOURCE_RECHARGE_FUNDS = "REFUND_SOURCE_RECHARGE_FUNDS";/*** 退款请求* * @param weixinOrder*            退款参数* @param key*            签名key* @param path*            签权路径* @return*/public static String refund(WeixinOrderDTO weixinOrder, String key, String path) {Map<String, String> map = new TreeMap<String, String>();StringBuffer sb = new StringBuffer();sb.append("<xml>");sb.append("<appid><![CDATA[" + weixinOrder.getAppId() + "]]></appid>");sb.append("<mch_id><![CDATA[" + weixinOrder.getMachId() + "]]></mch_id>");sb.append("<nonce_str><![CDATA[" + weixinOrder.getNonceStr() + "]]></nonce_str>");sb.append("<op_user_id><![CDATA[" + weixinOrder.getMachId() + "]]></op_user_id>");sb.append("<out_refund_no><![CDATA[" + weixinOrder.getOutRefundNo() + "]]></out_refund_no>");sb.append("<out_trade_no><![CDATA[" + weixinOrder.getOutTradeNo() + "]]></out_trade_no>");sb.append("<refund_fee><![CDATA[" + weixinOrder.getTotalFee() + "]]></refund_fee>");// 退款资金来源sb.append("<refund_account><![CDATA[" + REFUND_SOURCE_RECHARGE_FUNDS + "]]></refund_account>");sb.append("<total_fee><![CDATA[" + weixinOrder.getTotalFee() + "]]></total_fee>");sb.append("<transaction_id><![CDATA[]]></transaction_id>");map.put("appid", weixinOrder.getAppId());map.put("mch_id", weixinOrder.getMachId());map.put("nonce_str", weixinOrder.getNonceStr());map.put("op_user_id", weixinOrder.getMachId());map.put("out_refund_no", weixinOrder.getOutRefundNo());map.put("out_trade_no", weixinOrder.getOutTradeNo());map.put("refund_account", REFUND_SOURCE_RECHARGE_FUNDS + "");map.put("refund_fee", weixinOrder.getTotalFee() + "");map.put("total_fee", weixinOrder.getTotalFee() + "");map.put("transaction_id", "");String sign = getSign(map, key);sb.append("<sign><![CDATA[" + sign + "]]></sign>");sb.append("</xml>");String str = null;LOG.debug("url:" + refund_url + "xml:" + sb.toString());try {str = postByBodyWithCert(refund_url, sb.toString(), "application/xml; charset=UTF-8", path);} catch (Exception e) {LOG.error(e.getMessage(), e);}return str;}public static String postByBodyWithCert(String url, String body, String contentType, String certPath) throws HttpException, Exception {KeyStore keyStore = KeyStore.getInstance("PKCS12");File file = new File(certPath);String fileName = file.getName();fileName = fileName.substring(0, fileName.lastIndexOf("."));FileInputStream instream = new FileInputStream(file);try {keyStore.load(instream, fileName.toCharArray());} finally {instream.close();}// Trust own CA and all self-signed certsSSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, fileName.toCharArray()).build();// Allow TLSv1 protocol onlySSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslcontext, new String[] { "TLSv1" }, null,SSLConnectionSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER);RequestConfig requestConfig = RequestConfig.custom().setConnectionRequestTimeout(TIMEOUT_SEC * 1000).setSocketTimeout(TIMEOUT_SEC * 1000).setConnectTimeout(1000).build();CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(sslsf).build();StringBuilder sb = new StringBuilder();try {HttpPost httpPost = new HttpPost(url);httpPost.setConfig(requestConfig);httpPost.addHeader("Content-Type", contentType);/** if (StringUtils.isNotBlank(body)) { body =* URLEncoder.encode(body, "UTF-8"); }*/HttpEntity se = new StringEntity(body, "UTF-8");httpPost.setEntity(se);CloseableHttpResponse response = httpclient.execute(httpPost);try {HttpEntity entity = response.getEntity();if (entity != null) {BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(entity.getContent()));String text;while ((text = bufferedReader.readLine()) != null) {sb.append(text);}}EntityUtils.consume(entity);} finally {response.close();}} finally {httpclient.close();}return sb.toString();}public static class MySSLProtocolSocketFactory implements ProtocolSocketFactory {private SSLContext sslcontext = null;public Socket createSocket(String host, int port, InetAddress clientHost, int clientPort) throws IOException, UnknownHostException {return getSSLContext().getSocketFactory().createSocket(host, port, clientHost, clientPort);}public Socket createSocket(String host, int port, InetAddress localAddress, int localPort, HttpConnectionParams params)throws IOException, UnknownHostException, ConnectTimeoutException {if (params == null) {throw new IllegalArgumentException("Parameters may not be null");}int timeout = params.getConnectionTimeout();SocketFactory socketfactory = getSSLContext().getSocketFactory();if (timeout == 0) {return socketfactory.createSocket(host, port, localAddress, localPort);} else {Socket socket = socketfactory.createSocket();SocketAddress localaddr = new InetSocketAddress(localAddress, localPort);SocketAddress remoteaddr = new InetSocketAddress(host, port);socket.bind(localaddr);socket.connect(remoteaddr, timeout);return socket;}}public Socket createSocket(String host, int port) throws IOException, UnknownHostException {return getSSLContext().getSocketFactory().createSocket(host, port);}private SSLContext createSSLContext() {SSLContext sslcontext = null;try {sslcontext = SSLContext.getInstance("SSL");sslcontext.init(null, new TrustManager[] { new TrustAnyTrustManager() }, new java.security.SecureRandom());} catch (Exception e) {e.printStackTrace();}return sslcontext;}private SSLContext getSSLContext() {if (this.sslcontext == null) {this.sslcontext = createSSLContext();}return this.sslcontext;}// 自定义私有类private static class TrustAnyTrustManager implements X509TrustManager {public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {// TODO Auto-generated method stub}public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {// TODO Auto-generated method stub}public X509Certificate[] getAcceptedIssuers() {// TODO Auto-generated method stubreturn null;}}}
}

微信支付实战(完整的代码,复制即可用)相关推荐

  1. 微信支付 php详解,PHP实现微信支付实战案例详解

    这次给大家带来PHP实现微信支付实战案例详解,PHP实现微信支付的注意事项有哪些,下面就是实战案例,一起来看一下. 前期准备: 1.微信认证服务号,并且开通了微信支付 2.微信支付SDK,下载地址:h ...

  2. php 工商银行公众号支付代码_php开发微信支付企业付款实例代码

    企业付款的应用场景: 公众号向已关注用户付款,比如处理退款.财务结算等,本文主要和大家分享php开发微信支付企业付款实例代码,希望能帮助到大家. 说明 1.证书需要用自己的商户里面的证书(注意:证书路 ...

  3. java集成微信支付(完整流程)

    java集成微信支付(完整流程) 1.申请微信支付能力 * 要想使用微信支付能力,不管是app支付.公众号支付.h5支付等支付方式都需要先在微信商户平台申请开通支付能力.* 申请开通支付能力的资料有公 ...

  4. springboot整合微信支付宝二合一扫码支付,完整实例代码

    项目采用springboot框架,支付宝的配置相对简单,微信的配置主要是要找对地方. 完整项目已上传.下载地址:https://download.csdn.net/download/miao5371/ ...

  5. Flutter之微信支付实战模板

    ​ ​ 活动地址:CSDN21天学习挑战赛 安装flutter插件 fluwx: ^3.8.5 #微信 注册微信接口 _initfluwx() async {print('78987979798797 ...

  6. 微信支付接口(公众号支付)+微信支付回调函数 附代码

    前段时间做微信支付,微信浏览器填写金额商品名之后提交跳转付款页面确认然后返回界面判断,今天来详细说下 国际惯例先贴代码 mcontroller.java public void wxpay() {if ...

  7. 在线支付系列【15】微信支付实战篇之集成查询订单、支付通知API

    有道无术,术尚可求,有术无道,止于术. 文章目录 前言 主动调用 商户订单号查询 回调通知 1. 添加通知回调地址 2. 通知处理 3. 通知接口 4. 测试 前言 在上篇文档中,我们简单实现了Nat ...

  8. 在线支付系列【22】微信支付实战篇之集成服务商API

    有道无术,术尚可求,有术无道,止于术. 文章目录 前言 1. 环境搭建 2. 特约商户进件 3. 统一下单 总结 前言 在上篇文档中,我们做好了接入前准备工作,接下来使用开源框架集成服务商相关API. ...

  9. uni-app H5兼容ios问题+微信扫一扫、微信支付等常用api代码封装

    最近公司需要用uni开发一个项目,项目中遇到的问题记录一下,方便下次不采坑 场景:         使用wx自带sdk完成,扫一扫.微信登录.微信支付 引用方式: // index.html引入 &l ...

最新文章

  1. 【OpenCV3】角点检测——cv::goodFeaturesToTrack()与cv::cornerSubPix()详解
  2. codeforces 1A-C语言解题报告
  3. python视窗编程_[PYTHON] 核心编程笔记(19.图形用户界面编程)
  4. webpack2入门概念
  5. sqlserver 递归查询
  6. Hadoop系列之DistributedCache用法
  7. Visual Studio 打开程序提示仅我的代码怎么办
  8. 中心管理cms服务器_如何查找网站使用的CMS,脚本,服务器,技术
  9. ie浏览器在线使用_教师资格证报名使用的不是ie系列浏览器怎么办?
  10. Python编程笔记(第一篇)Python基础语法
  11. ipsec和nat穿越
  12. hcfax2e伺服驱动器说明书_SD伺服驱动器说明书
  13. 超详细的Django面试题
  14. python求15 17 23 65 97的因数_Python练习题
  15. Sqlite字段长度填坑
  16. mac无法读取移动硬盘怎么办?mac怎么使用ntfs硬盘
  17. 三星S6D1121主控彩屏(240*320*18bit,262K)驱动程序
  18. 不小心把苹果手机备忘录删掉怎么恢复
  19. 网络工程——HCNA网络技术的简单配置
  20. 通过 磁带机 备份 恢复数据 --201308

热门文章

  1. 什么是AMD;什么是CMD
  2. 为什么word文档在前面打字会把后面的文字覆盖掉?
  3. 上级目录,同级目录简写【../,./】
  4. Python切图九宫格
  5. 阿里云同一账号下ECS服务器的克隆操作方法,你get到了吗
  6. 【评测】MP SARS-CoV-2单抗、重组蛋白
  7. python主进程 子进程_Python关闭主进程时关闭子进程
  8. PowerShield (软件加壳工具) PB混淆加密大师
  9. StringRedisTemplate、opsForValue、setIfAbsent、setIfPresent、getAndSet、increment、decrement用法
  10. 服务器系统迁移方案,服务器与应用系统迁移方案.doc