|
|
@@ -0,0 +1,215 @@
|
|
|
+package com.uas.weixin.pay;
|
|
|
+
|
|
|
+import com.thoughtworks.xstream.XStream;
|
|
|
+import com.thoughtworks.xstream.io.xml.DomDriver;
|
|
|
+import com.thoughtworks.xstream.io.xml.XmlFriendlyNameCoder;
|
|
|
+import com.uas.weixin.pay.model.OrderReturnInfo;
|
|
|
+import com.uas.weixin.pay.support.HttpRequest;
|
|
|
+import com.uas.weixin.pay.support.HttpRequest.MapEntryConverter;
|
|
|
+import com.uas.weixin.pay.support.JacksonUtils;
|
|
|
+import com.uas.weixin.pay.support.RandomStringGenerator;
|
|
|
+import com.uas.weixin.pay.support.Signature;
|
|
|
+import java.io.IOException;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.TreeMap;
|
|
|
+import org.apache.commons.lang3.Validate;
|
|
|
+import org.apache.http.HttpEntity;
|
|
|
+import org.apache.http.HttpResponse;
|
|
|
+import org.apache.http.client.HttpClient;
|
|
|
+import org.apache.http.client.methods.HttpGet;
|
|
|
+import org.apache.http.impl.client.HttpClients;
|
|
|
+import org.apache.http.util.EntityUtils;
|
|
|
+import org.apache.logging.log4j.LogManager;
|
|
|
+import org.apache.logging.log4j.Logger;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 微信支付接口抽象层,默认使用MD5签名
|
|
|
+ *
|
|
|
+ * @author huxz
|
|
|
+ */
|
|
|
+public class WeixinPay {
|
|
|
+
|
|
|
+ private static final Logger logger = LogManager.getLogger();
|
|
|
+
|
|
|
+ private static final String SIGN_TYPE = "MD5";
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 微信分配的小程序ID
|
|
|
+ */
|
|
|
+ private final String appId;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 小程序的 app secret
|
|
|
+ */
|
|
|
+ private final String secret;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 微信支付分配的商户号
|
|
|
+ */
|
|
|
+ private final String mchId;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 微信支付用户秘钥
|
|
|
+ */
|
|
|
+ private final String key;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 异步接收微信支付结果通知的回调地址,通知url必须为外网可访问的url
|
|
|
+ */
|
|
|
+ private String notifyUrl;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 交易类型 ,小程序取值如下:JSAPI
|
|
|
+ */
|
|
|
+ private String tradeType;
|
|
|
+
|
|
|
+ public WeixinPay(String appId, String secret, String mchId, String key) {
|
|
|
+ this.appId = appId;
|
|
|
+ this.secret = secret;
|
|
|
+ this.mchId = mchId;
|
|
|
+ this.key = key;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setNotifyUrl(String notifyUrl) {
|
|
|
+ this.notifyUrl = notifyUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ public void setTradeType(String tradeType) {
|
|
|
+ this.tradeType = tradeType;
|
|
|
+ }
|
|
|
+
|
|
|
+ public String getKey() {
|
|
|
+ return key;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 小程序获取用户openId
|
|
|
+ *
|
|
|
+ * @param code 用户登录code
|
|
|
+ * @return openId
|
|
|
+ */
|
|
|
+ public String getOpenId(String code) throws IOException {
|
|
|
+ String requestUrl = String.format("https://api.weixin.qq.com/sns/jscode2session?" +
|
|
|
+ "appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", this.appId, this.secret, code);
|
|
|
+ HttpGet httpGet = new HttpGet(requestUrl);
|
|
|
+
|
|
|
+ HttpClient httpClient = HttpClients.createDefault();
|
|
|
+ HttpResponse res = httpClient.execute(httpGet);
|
|
|
+ HttpEntity entity = res.getEntity();
|
|
|
+ String result = EntityUtils.toString(entity, "UTF-8");
|
|
|
+
|
|
|
+ Map map = JacksonUtils.fromJson(result, Map.class);
|
|
|
+ Validate.notNull(map, "获取用户唯一标识响应数据失败");
|
|
|
+
|
|
|
+ Validate.isTrue(map.get("errcode") == null, (String) map.get("errmsg"));
|
|
|
+ return (String) map.get("openid");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取用户预支付id
|
|
|
+ *
|
|
|
+ * @param openId 用户位置标识,用于小程序
|
|
|
+ * @param orderNo 订单编号
|
|
|
+ * @param body 商品简单描述
|
|
|
+ * @param totalFee 订单总金额,单位为分
|
|
|
+ * @param ip APP和网页支付提交用户端ip
|
|
|
+ * @return 预支付交易会话标识
|
|
|
+ */
|
|
|
+ public String getPrepareId(String openId, String orderNo, String body, Integer totalFee, String ip) {
|
|
|
+ Validate.notBlank(openId, "用户唯一标识不能为空");
|
|
|
+ Validate.notBlank(orderNo, "用户订单编号不能为空");
|
|
|
+ Validate.notBlank(body, "商品简单描述不能为空");
|
|
|
+ Validate.isTrue(totalFee != null && totalFee >= 0, "订单总金额不能为空或负数");
|
|
|
+ Validate.notBlank(ip, "终端IP不能为空");
|
|
|
+
|
|
|
+ TreeMap<String, Object> params = new TreeMap<>();
|
|
|
+ params.put("appid", this.appId);
|
|
|
+ params.put("mch_id", this.mchId);
|
|
|
+ params.put("nonce_str", RandomStringGenerator.getRandomStringByLength(32));
|
|
|
+ params.put("sign_type", SIGN_TYPE);
|
|
|
+ params.put("body", body);
|
|
|
+ params.put("out_trade_no", orderNo);
|
|
|
+ params.put("total_fee", totalFee);
|
|
|
+ params.put("spbill_create_ip", ip);
|
|
|
+ params.put("notify_url", this.notifyUrl);
|
|
|
+ params.put("trade_type", this.tradeType);
|
|
|
+ params.put("openid", openId);
|
|
|
+
|
|
|
+ params.put("sign", Signature.sign(params, this.key));
|
|
|
+
|
|
|
+ try {
|
|
|
+ String result = HttpRequest.sendPost("https://api.mch.weixin.qq.com/pay/unifiedorder", params);
|
|
|
+ logger.debug("统一下单接口响应结果: {}", result);
|
|
|
+
|
|
|
+ XStream xStream = new XStream();
|
|
|
+ XStream.setupDefaultSecurity(xStream);
|
|
|
+ xStream.allowTypes(new Class[]{OrderReturnInfo.class});
|
|
|
+ xStream.alias("xml", OrderReturnInfo.class);
|
|
|
+
|
|
|
+ OrderReturnInfo returnInfo = (OrderReturnInfo) xStream.fromXML(result);
|
|
|
+
|
|
|
+ Validate.notNull(returnInfo, "响应结果解析失败");
|
|
|
+ Validate.isTrue("SUCCESS".equals(returnInfo.getReturn_code()), returnInfo.getReturn_msg());
|
|
|
+ Validate.isTrue("SUCCESS".equals(returnInfo.getResult_code()), returnInfo.getErr_code_des());
|
|
|
+
|
|
|
+ logger.debug("用户预支付ID: {}", returnInfo.getPrepay_id());
|
|
|
+ return returnInfo.getPrepay_id();
|
|
|
+ } catch (IOException e) {
|
|
|
+ e.printStackTrace();
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 小程序调起支付数据签名
|
|
|
+ *
|
|
|
+ * @param prepayId 预支付ID
|
|
|
+ * @return 调起支付所需参数
|
|
|
+ */
|
|
|
+ public Map<String, Object> resign(String prepayId) {
|
|
|
+ Validate.notBlank(prepayId, "用户预支付Id不能为空");
|
|
|
+
|
|
|
+ TreeMap<String, Object> params = new TreeMap<>();
|
|
|
+ params.put("appId", this.appId);
|
|
|
+ long time = System.currentTimeMillis() / 1000;
|
|
|
+ params.put("timeStamp", time);
|
|
|
+ params.put("nonceStr", RandomStringGenerator.getRandomStringByLength(32));
|
|
|
+ params.put("package", "prepay_id=" + prepayId);
|
|
|
+ params.put("signType", SIGN_TYPE);
|
|
|
+
|
|
|
+ params.put("paySign", Signature.sign(params, this.key));
|
|
|
+
|
|
|
+ // 删除参数appId
|
|
|
+ params.remove("appId");
|
|
|
+ return params;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 将Map数据转换成XML字符串
|
|
|
+ *
|
|
|
+ * @param params 转换参数
|
|
|
+ * @return xml字符串
|
|
|
+ */
|
|
|
+ public static String translateXML(Map<String, Object> params) {
|
|
|
+ XStream stream = new XStream(new DomDriver("UTF-8", new XmlFriendlyNameCoder("-_", "_")));
|
|
|
+ XStream.setupDefaultSecurity(stream);
|
|
|
+ stream.allowTypes(new Class[]{Map.class});
|
|
|
+
|
|
|
+ stream.alias("xml", Map.class);
|
|
|
+ stream.registerConverter(new MapEntryConverter());
|
|
|
+
|
|
|
+ return stream.toXML(params);
|
|
|
+ }
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ public static Map<String, Object> translateMap(String xml) {
|
|
|
+ XStream xStream = new XStream();
|
|
|
+ XStream.setupDefaultSecurity(xStream);
|
|
|
+ xStream.allowTypes(new Class[]{Map.class});
|
|
|
+
|
|
|
+ xStream.alias("xml", Map.class);
|
|
|
+ xStream.registerConverter(new MapEntryConverter());
|
|
|
+
|
|
|
+ return (Map<String, Object>) xStream.fromXML(xml);
|
|
|
+ }
|
|
|
+}
|