2020年10月28日 | Wizzer | 评论 一、商户后台设置V3 Key密钥及下载V3 API证书(三个文件分别为apiclient_key.pem、apiclient_cert.pem、apiclient_cert.p12)二、设计表结构实现管理功能 package com.budwk.nb.wx.models; import com.budwk.nb.commons.base.model.BaseModel; import lombok.Data; import lombok.EqualsAndHashCode; import org.nutz.dao.entity.annotation.*; import org.nutz.dao.interceptor.annotation.PrevInsert; import java.io.Serializable; /** * 微信支付配置表 * @author wizzer@qq.com */ @Data @EqualsAndHashCode(callSuper = true) @Table("wx_pay") public class Wx_pay extends BaseModel implements Serializable { private static final long serialVersionUID = 1L; @Column @Name @Comment("ID") @ColDefine(type = ColType.VARCHAR, width = 32) @PrevInsert(els = {@EL("uuid()")}) private String id; @Column @ColDefine(type = ColType.VARCHAR, width = 32) private String name; @Column @ColDefine(type = ColType.VARCHAR, width = 32) private String mchid; @Column @ColDefine(type = ColType.VARCHAR, width = 50) private String v3key; /** * apiclient_key.pem 物理路径 */ @Column @ColDefine(type = ColType.VARCHAR, width = 255) private String v3keyPath; /** * apiclient_cert.pem 物理路径 */ @Column @ColDefine(type = ColType.VARCHAR, width = 255) private String v3certPath; /** * apiclient_cert.p12 物理路径 */ @Column @ColDefine(type = ColType.VARCHAR, width = 255) private String v3certP12Path; /** * 平台证书失效时间 */ @Column private Long expire_at; } package com.budwk.nb.wx.models; import com.budwk.nb.commons.base.model.BaseModel; import lombok.Data; import lombok.EqualsAndHashCode; import org.nutz.dao.entity.annotation.*; import org.nutz.dao.interceptor.annotation.PrevInsert; import java.io.Serializable; /** * 平台证书临存表 * @author wizzer@qq.com */ @Data @EqualsAndHashCode(callSuper = true) @Table("wx_pay_cert") @TableIndexes({@Index(name = "INDEX_WX_PAY_CERT", fields = {"mchid", "serial_no"}, unique = true)}) public class Wx_pay_cert extends BaseModel implements Serializable { private static final long serialVersionUID = 1L; @Column @Name @Comment("ID") @ColDefine(type = ColType.VARCHAR, width = 32) @PrevInsert(els = {@EL("uuid()")}) private String id; @Column @ColDefine(type = ColType.VARCHAR, width = 32) private String mchid; @Column @ColDefine(type = ColType.VARCHAR, width = 255) private String serial_no; @Column @ColDefine(type = ColType.VARCHAR, width = 255) private String effective_time; @Column private Long effective_at; @Column @ColDefine(type = ColType.VARCHAR, width = 255) private String expire_time; @Column private Long expire_at; @Column @ColDefine(type = ColType.VARCHAR, width = 255) private String algorithm; @Column @ColDefine(type = ColType.VARCHAR, width = 255) private String nonce; @Column @ColDefine(type = ColType.VARCHAR, width = 255) private String associated_data; @Column @ColDefine(type = ColType.TEXT) private String ciphertext; @Column @ColDefine(type = ColType.TEXT) private String certificate; } 三、封装下订单/JSAPI/平台证书更新等功能服务类 package com.budwk.nb.web.commons.ext.wx; import com.alibaba.dubbo.config.annotation.Reference; import com.budwk.nb.web.commons.base.Globals; import com.budwk.nb.wx.models.Wx_pay; import com.budwk.nb.wx.models.Wx_pay_cert; import com.budwk.nb.wx.services.WxPayCertService; import com.budwk.nb.wx.services.WxPayService; import org.nutz.dao.Chain; import org.nutz.dao.Cnd; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.json.Json; import org.nutz.lang.Strings; import org.nutz.lang.Times; import org.nutz.lang.util.NutMap; import org.nutz.log.Log; import org.nutz.log.Logs; import org.nutz.weixin.bean.WxPay3Response; import org.nutz.weixin.util.WxPay3Api; import org.nutz.weixin.util.WxPay3Util; import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.List; /** * @author wizzer@qq.com */ @IocBean public class WxPay3Service { private static final Log log = Logs.get(); private static final SimpleDateFormat DATE_TIME_ZONE = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX"); @Inject @Reference(check = false) private WxPayCertService wxPayCertService; @Inject @Reference(check = false) private WxPayService wxPayService; // 通过商户号获取 wx_pay 对象 public synchronized Wx_pay getWxPay(String mchid) { Wx_pay wxPay = Globals.WxPay3Map.getAs(mchid, Wx_pay.class); if (wxPay == null) { wxPay = wxPayService.fetch(Cnd.where("mchid", "=", mchid)); Globals.WxPay3Map.put(wxPay.getMchid(), wxPay); } checkPlatfromCerts(wxPay); return wxPay; } // 检查及更新平台证书机制 public void checkPlatfromCerts(Wx_pay wxPay) { if (wxPay == null) throw new IllegalStateException("Wx_pay is null"); if (wxPay.getExpire_at() == null || wxPay.getExpire_at() == 0 || wxPay.getExpire_at() < 8 * 3600 * 1000 + System.currentTimeMillis()) { getPlatfromCerts(wxPay.getMchid(), wxPay.getV3key(), wxPay.getV3keyPath(), wxPay.getV3certPath()); wxPay = wxPayService.fetch(Cnd.where("mchid", "=", wxPay.getMchid())); Globals.WxPay3Map.put(wxPay.getMchid(), wxPay); } } // jsapi 订单下单 public WxPay3Response v3_order_jsapi(String mchid, String body) throws Exception { log.debug("v3_order_jsapi body::" + body); String serialNo = WxPay3Util.getCertSerialNo(getWxPay(mchid).getV3certPath()); return WxPay3Api.v3_order_jsapi(mchid, serialNo, getWxPay(mchid).getV3keyPath(), body); } // 通过jsapi 订单号生成js参数 public NutMap v3_call_jsapi(String mchid, String appid, String prepay_id) throws Exception { return WxPay3Util.getJsapiSignMessage(appid, prepay_id, getWxPay(mchid).getV3keyPath()); } // 验证http响应签名结果 public boolean verifySignature(WxPay3Response wxPay3Response, String mchid) throws Exception { Wx_pay_cert wxPayCert = wxPayCertService.fetch(Cnd.where("mchid", "=", mchid).and("serial_no", "=", wxPay3Response.getHeader().get("Wechatpay-Serial"))); return WxPay3Util.verifySignature(wxPay3Response, wxPayCert.getCertificate()); } // 验证回调通知签名及内容 public String verifyNotify(String mchid, String serialNo, String body, String signature, String nonce, String timestamp) throws Exception { Wx_pay_cert wxPayCert = wxPayCertService.fetch(Cnd.where("mchid", "=", mchid).and("serial_no", "=", serialNo)); return WxPay3Util.verifyNotify(serialNo, body, signature, nonce, timestamp, getWxPay(mchid).getV3key(), wxPayCert.getCertificate()); } /** * 请求并保存新证书 * * @param mchid * @return */ public void getPlatfromCerts(String mchid, String v3Key, String v3KeyPatch, String v3CertPath) { try { wxPayCertService.clear(Cnd.where("mchid", "=", mchid).and("expire_at", "<", System.currentTimeMillis())); String serialNo = WxPay3Util.getCertSerialNo(v3CertPath); WxPay3Response wxPay3Response = WxPay3Api.v3_certificates(mchid, serialNo, v3KeyPatch); if (wxPay3Response.getStatus() == 200) { NutMap nutMap = Json.fromJson(NutMap.class, wxPay3Response.getBody()); List<NutMap> list = nutMap.getList("data", NutMap.class); for (NutMap cert : list) { Wx_pay_cert wxPayCert = new Wx_pay_cert(); wxPayCert.setMchid(mchid); wxPayCert.setEffective_time(cert.getString("effective_time")); wxPayCert.setExpire_time(cert.getString("expire_time")); long expire_at = 0; try { expire_at = Times.parse(DATE_TIME_ZONE, cert.getString("expire_time")).getTime(); wxPayCert.setEffective_at(Times.parse(DATE_TIME_ZONE, cert.getString("effective_time")).getTime()); wxPayCert.setExpire_at(expire_at); } catch (ParseException e) { e.printStackTrace(); } wxPayCert.setSerial_no(cert.getString("serial_no")); NutMap encrypt_certificate = cert.getAs("encrypt_certificate", NutMap.class); wxPayCert.setAlgorithm(encrypt_certificate.getString("algorithm")); wxPayCert.setAssociated_data(encrypt_certificate.getString("associated_data")); wxPayCert.setCiphertext(encrypt_certificate.getString("ciphertext")); wxPayCert.setNonce(encrypt_certificate.getString("nonce")); String platformCertificate = WxPay3Util.decryptToString(v3Key.getBytes(StandardCharsets.UTF_8), encrypt_certificate.getString("associated_data").getBytes(StandardCharsets.UTF_8), encrypt_certificate.getString("nonce").getBytes(StandardCharsets.UTF_8), encrypt_certificate.getString("ciphertext") ); wxPayCert.setCertificate(platformCertificate); try { wxPayCertService.insert(wxPayCert); } catch (Exception e) { //重复的插入会报错,不管它 } } Wx_pay_cert wxPayCert = wxPayCertService.fetch(Cnd.where("mchid", "=", mchid).orderBy("effective_at", "desc")); if (wxPayCert != null) { wxPayService.update(Chain.make("expire_at", wxPayCert.getExpire_at()), Cnd.where("mchid", "=", mchid)); } } } catch (Exception e) { log.errorf("获取平台证书失败,mchid=%s", mchid, e); } } } 四、小程序支付业务代码 @Test public void test_v3_order() throws Exception { String orderPayNo = R.UU32(); String orderId = R.UU32(); NutMap wxPayUnifiedOrder = NutMap.NEW(); wxPayUnifiedOrder.addv("appid", appid); wxPayUnifiedOrder.addv("mchid", mchid); wxPayUnifiedOrder.addv("description", new String(("LaiShop-order-" + orderId).getBytes(), StandardCharsets.UTF_8)); wxPayUnifiedOrder.addv("out_trade_no", orderPayNo); Date now = new Date(); wxPayUnifiedOrder.addv("time_expire", DateUtil.getDateAfterMinute(now, 30)); // 回调通知URL传递mchid商户号,便于系统支持接入N个小程序及支付商户账号 wxPayUnifiedOrder.addv("notify_url", Globals.AppDomain + "/shop/open/wxpay/" + mchid + "/notify"); wxPayUnifiedOrder.addv("amount", NutMap.NEW().addv("total", 1).addv("currency", "CNY")); wxPayUnifiedOrder.addv("payer", NutMap.NEW().addv("openid", "o9Bnd4lXKfNsOci-6H98zCMWyBps")); String body = Json.toJson(wxPayUnifiedOrder); System.out.println("body::" + body); WxPay3Response wxPay3Response = wxPay3Service.v3_order_jsapi(mchid, body); System.out.println("wxPay3Response::" + Json.toJson(wxPay3Response)); boolean verifySignature = wxPay3Service.verifySignature(wxPay3Response, mchid); System.out.println("verifySignature::" + verifySignature); NutMap v3order = Json.fromJson(NutMap.class, wxPay3Response.getBody()); NutMap resp = wxPay3Service.v3_call_jsapi(mchid, appid, v3order.getString("prepay_id")); System.out.println("resp::" + Json.toJson(resp)); } 五、回调通知业务代码 package com.budwk.nb.web.controllers.open.pay; import com.alibaba.dubbo.config.annotation.Reference; import com.budwk.nb.web.commons.ext.wx.WxPay3Service; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.servers.Server; import io.swagger.v3.oas.annotations.tags.Tag; import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.json.Json; import org.nutz.lang.Streams; import org.nutz.lang.util.NutMap; import org.nutz.log.Log; import org.nutz.log.Logs; import org.nutz.mvc.adaptor.VoidAdaptor; import org.nutz.mvc.annotation.AdaptBy; import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.Ok; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.Reader; import java.nio.charset.StandardCharsets; /** * @author wizzer@qq.com */ @IocBean @At("/shop/open/wxpay") @Ok("json") @OpenAPIDefinition(tags = {@Tag(name = "商城_微信支付回调")}, servers = @Server(url = "/")) public class WxPay3NotifyController { private static final Log log = Logs.get(); @Inject private WxPay3Service wxPay3Service; @At("/{mchid}/notify") @Ok("raw") @AdaptBy(type = VoidAdaptor.class) public void notify(String mchid, Reader reader, HttpServletRequest req, HttpServletResponse resp) throws IOException { try { NutMap map = NutMap.NEW(); String timestamp = req.getHeader("Wechatpay-Timestamp"); String nonce = req.getHeader("Wechatpay-Nonce"); String serialNo = req.getHeader("Wechatpay-Serial"); String signature = req.getHeader("Wechatpay-Signature"); log.debugf("timestamp=%s,nonce=%s,serialNo=%s,signature=%s", timestamp, nonce, serialNo, signature); String body = Streams.readAndClose(reader); // 需要通过证书序列号查找对应的证书,verifyNotify 中有验证证书的序列号 String plainText = wxPay3Service.verifyNotify(mchid, serialNo, body, signature, nonce, timestamp); log.debugf("支付通知明文=%s", plainText); NutMap res = Json.fromJson(NutMap.class, plainText); NutMap payer = res.getAs("payer", NutMap.class); String trade_state = res.getString("trade_state"); String out_trade_no = res.getString("out_trade_no"); String openid = payer.getString("openid"); boolean ok = true;//业务代码入库 if ("SUCCESS".equals(trade_state) && ok) { resp.setStatus(200); map.put("code", "SUCCESS"); map.put("message", "SUCCESS"); } else { resp.setStatus(500); map.put("code", "ERROR"); map.put("message", "签名错误"); } resp.setHeader("Content-type", "application/json"); resp.getOutputStream().write(Json.toJson(map).getBytes(StandardCharsets.UTF_8)); resp.flushBuffer(); } catch (Exception e) { e.printStackTrace(); } } } 本文档以 BudWk 框架代码为例,源码地址: https://gitee.com/budwk/budwk-nutzboot 演示地址: https://demo.budwk.com 1,258 total views, 1 views today