公众号介绍
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?";
// 重定向获取openid
public 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);
@Autowired
private 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查openid
String 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")
@ResponseBody
public 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 + "×tamp=" + 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.currentTimeMillis
private 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.currentTimeMillis
private 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;
}
}
准备好固定的资源
//公众号appid
public static final String appId = "wxf546a5afce347110";
//公众号appSecret
public 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)
@ResponseBody
public 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 certs
SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, fileName.toCharArray()).build();
// Allow TLSv1 protocol only
SSLConnectionSocketFactory 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 stub
return null;
}
}
}
}