| @@ -19,3 +19,4 @@ target | |||
| sw-pom.xml | |||
| *.iml | |||
| test-config.xml | |||
| .idea | |||
| @@ -1,5 +1,7 @@ | |||
| package me.chanjar.weixin.common.util.crypto; | |||
| import org.apache.commons.codec.digest.DigestUtils; | |||
| import java.security.MessageDigest; | |||
| import java.security.NoSuchAlgorithmException; | |||
| import java.util.Arrays; | |||
| @@ -21,7 +23,7 @@ public class SHA1 { | |||
| for (String a : arr) { | |||
| sb.append(a); | |||
| } | |||
| return genStr(sb.toString()); | |||
| return DigestUtils.sha1Hex(sb.toString()); | |||
| } | |||
| /** | |||
| @@ -40,25 +42,6 @@ public class SHA1 { | |||
| sb.append('&'); | |||
| } | |||
| } | |||
| return genStr(sb.toString()); | |||
| } | |||
| public static String genStr(String str) throws NoSuchAlgorithmException { | |||
| MessageDigest sha1 = MessageDigest.getInstance("SHA1"); | |||
| sha1.update(str.getBytes()); | |||
| byte[] output = sha1.digest(); | |||
| return bytesToHex(output); | |||
| return DigestUtils.sha1Hex(sb.toString()); | |||
| } | |||
| protected static String bytesToHex(byte[] b) { | |||
| char hexDigit[] = { '0', '1', '2', '3', '4', '5', '6', '7', | |||
| '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' }; | |||
| StringBuffer buf = new StringBuffer(); | |||
| for (int j = 0; j < b.length; j++) { | |||
| buf.append(hexDigit[(b[j] >> 4) & 0x0f]); | |||
| buf.append(hexDigit[b[j] & 0x0f]); | |||
| } | |||
| return buf.toString(); | |||
| } | |||
| } | |||
| } | |||
| @@ -14,6 +14,7 @@ | |||
| package me.chanjar.weixin.common.util.crypto; | |||
| import org.apache.commons.codec.binary.Base64; | |||
| import org.apache.commons.codec.digest.DigestUtils; | |||
| import org.w3c.dom.Document; | |||
| import org.w3c.dom.Element; | |||
| import org.xml.sax.InputSource; | |||
| @@ -27,8 +28,7 @@ import javax.xml.parsers.ParserConfigurationException; | |||
| import java.io.StringReader; | |||
| import java.nio.charset.Charset; | |||
| import java.security.NoSuchAlgorithmException; | |||
| import java.util.Arrays; | |||
| import java.util.Random; | |||
| import java.util.*; | |||
| public class WxCryptUtil { | |||
| @@ -224,6 +224,36 @@ public class WxCryptUtil { | |||
| } | |||
| /** | |||
| * 微信公众号支付签名算法(详见:http://pay.weixin.qq.com/wiki/doc/api/index.php?chapter=4_3) | |||
| * @param packageParams 原始参数 | |||
| * @param signKey 加密Key(即 商户Key) | |||
| * @param charset 编码 | |||
| * @return 签名字符串 | |||
| */ | |||
| public static String createSign(Map<String, String> packageParams, String signKey) { | |||
| SortedMap<String, String> sortedMap = new TreeMap<String, String>(); | |||
| sortedMap.putAll(packageParams); | |||
| List<String> keys = new ArrayList<String>(packageParams.keySet()); | |||
| Collections.sort(keys); | |||
| StringBuffer toSign = new StringBuffer(); | |||
| for (String key : keys) { | |||
| String value = packageParams.get(key); | |||
| if (null != value && !"".equals(value) && !"sign".equals(key) | |||
| && !"key".equals(key)) { | |||
| toSign.append(key + "=" + value + "&"); | |||
| } | |||
| } | |||
| toSign.append("key=" + signKey); | |||
| System.out.println(toSign.toString()); | |||
| String sign = DigestUtils.md5Hex(toSign.toString()) | |||
| .toUpperCase(); | |||
| return sign; | |||
| } | |||
| /** | |||
| * 将一个数字转换成生成4个字节的网络字节序bytes数组 | |||
| * | |||
| @@ -50,6 +50,10 @@ public interface WxMpConfigStorage { | |||
| public String getSecret(); | |||
| public String getPartnerId(); | |||
| public String getPartnerKey(); | |||
| public String getToken(); | |||
| public String getAesKey(); | |||
| @@ -11,6 +11,8 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
| protected volatile String appId; | |||
| protected volatile String secret; | |||
| protected volatile String partnerId; | |||
| protected volatile String partnerKey; | |||
| protected volatile String token; | |||
| protected volatile String accessToken; | |||
| protected volatile String aesKey; | |||
| @@ -168,6 +170,8 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
| "appId='" + appId + '\'' + | |||
| ", secret='" + secret + '\'' + | |||
| ", token='" + token + '\'' + | |||
| ", partnerId='" + partnerId + '\'' + | |||
| ", partnerKey='" + partnerKey + '\'' + | |||
| ", accessToken='" + accessToken + '\'' + | |||
| ", aesKey='" + aesKey + '\'' + | |||
| ", expiresTime=" + expiresTime + | |||
| @@ -179,4 +183,22 @@ public class WxMpInMemoryConfigStorage implements WxMpConfigStorage { | |||
| ", jsapiTicketExpiresTime='" + jsapiTicketExpiresTime + '\'' + | |||
| '}'; | |||
| } | |||
| @Override | |||
| public String getPartnerId() { | |||
| return partnerId; | |||
| } | |||
| public void setPartnerId(String partnerId) { | |||
| this.partnerId = partnerId; | |||
| } | |||
| @Override | |||
| public String getPartnerKey() { | |||
| return partnerKey; | |||
| } | |||
| public void setPartnerKey(String partnerKey) { | |||
| this.partnerKey = partnerKey; | |||
| } | |||
| } | |||
| @@ -11,9 +11,11 @@ import me.chanjar.weixin.mp.bean.result.*; | |||
| import java.io.File; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| import java.math.BigDecimal; | |||
| import java.text.SimpleDateFormat; | |||
| import java.util.Date; | |||
| import java.util.List; | |||
| import java.util.Map; | |||
| /** | |||
| * 微信API的Service | |||
| @@ -332,6 +334,18 @@ public interface WxMpService { | |||
| */ | |||
| public WxMpQrCodeTicket qrCodeCreateLastTicket(int scene_id) throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| * 换取永久字符串二维码ticket | |||
| * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=生成带参数的二维码 | |||
| * </pre> | |||
| * | |||
| * @param scene_str 参数。字符串类型长度现在为1到64 | |||
| * @return | |||
| * @throws WxErrorException | |||
| */ | |||
| public WxMpQrCodeTicket qrCodeCreateLastTicket(String scene_str) throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| * 换取二维码图片文件,jpg格式 | |||
| @@ -528,4 +542,32 @@ public interface WxMpService { | |||
| */ | |||
| void setMaxRetryTimes(int maxRetryTimes); | |||
| /** | |||
| * 统一下单(详见http://pay.weixin.qq.com/wiki/doc/api/index.php?chapter=9_1) | |||
| * 在发起微信支付前,需要调用统一下单接口,获取"预支付交易会话标识" | |||
| * @param openId 支付人openId | |||
| * @param outTradeNo 商户端对应订单号 | |||
| * @param amt 金额(单位元) | |||
| * @param body 商品描述 | |||
| * @param tradeType 交易类型 JSAPI,NATIVE,APP,WAP | |||
| * @param ip 发起支付的客户端IP | |||
| * @param notifyUrl 通知地址 | |||
| * @return | |||
| */ | |||
| WxMpPrepayIdResult getPrepayId(String openId, String outTradeNo, double amt, String body, String tradeType, String ip, String notifyUrl); | |||
| /** | |||
| * 该接口调用“统一下单”接口,并拼装JSSDK发起支付请求需要的参数 | |||
| * 详见http://mp.weixin.qq.com/wiki/7/aaa137b55fb2e0456bf8dd9148dd613f.html#.E5.8F.91.E8.B5.B7.E4.B8.80.E4.B8.AA.E5.BE.AE.E4.BF.A1.E6.94.AF.E4.BB.98.E8.AF.B7.E6.B1.82 | |||
| * @param openId 支付人openId | |||
| * @param outTradeNo 商户端对应订单号 | |||
| * @param amt 金额(单位元) | |||
| * @param body 商品描述 | |||
| * @param tradeType 交易类型 JSAPI,NATIVE,APP,WAP | |||
| * @param ip 发起支付的客户端IP | |||
| * @param notifyUrl 通知地址 | |||
| * @return | |||
| */ | |||
| Map<String, String> getJSSDKPayInfo(String openId, String outTradeNo, double amt, String body, String tradeType, String ip, String notifyUrl); | |||
| } | |||
| @@ -6,6 +6,7 @@ import com.google.gson.JsonObject; | |||
| import com.google.gson.internal.Streams; | |||
| import com.google.gson.reflect.TypeToken; | |||
| import com.google.gson.stream.JsonReader; | |||
| import com.thoughtworks.xstream.XStream; | |||
| import me.chanjar.weixin.common.bean.WxAccessToken; | |||
| import me.chanjar.weixin.common.bean.WxMenu; | |||
| import me.chanjar.weixin.common.bean.WxJsapiSignature; | |||
| @@ -17,13 +18,16 @@ import me.chanjar.weixin.common.session.WxSessionManager; | |||
| import me.chanjar.weixin.common.util.RandomUtils; | |||
| import me.chanjar.weixin.common.util.StringUtils; | |||
| import me.chanjar.weixin.common.util.crypto.SHA1; | |||
| import me.chanjar.weixin.common.util.crypto.WxCryptUtil; | |||
| import me.chanjar.weixin.common.util.fs.FileUtils; | |||
| import me.chanjar.weixin.common.util.http.*; | |||
| import me.chanjar.weixin.common.util.json.GsonHelper; | |||
| import me.chanjar.weixin.common.util.xml.XStreamInitializer; | |||
| import me.chanjar.weixin.mp.bean.*; | |||
| import me.chanjar.weixin.mp.bean.result.*; | |||
| import me.chanjar.weixin.mp.util.http.QrCodeRequestExecutor; | |||
| import me.chanjar.weixin.mp.util.json.WxMpGsonBuilder; | |||
| import org.apache.http.Consts; | |||
| import org.apache.http.HttpHost; | |||
| import org.apache.http.auth.AuthScope; | |||
| import org.apache.http.auth.UsernamePasswordCredentials; | |||
| @@ -32,6 +36,8 @@ import org.apache.http.client.CredentialsProvider; | |||
| import org.apache.http.client.config.RequestConfig; | |||
| import org.apache.http.client.methods.CloseableHttpResponse; | |||
| import org.apache.http.client.methods.HttpGet; | |||
| import org.apache.http.client.methods.HttpPost; | |||
| import org.apache.http.entity.StringEntity; | |||
| import org.apache.http.impl.client.BasicCredentialsProvider; | |||
| import org.apache.http.impl.client.BasicResponseHandler; | |||
| import org.apache.http.impl.client.CloseableHttpClient; | |||
| @@ -43,10 +49,9 @@ import java.io.File; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| import java.io.StringReader; | |||
| import java.math.BigDecimal; | |||
| import java.security.NoSuchAlgorithmException; | |||
| import java.util.Date; | |||
| import java.util.List; | |||
| import java.util.UUID; | |||
| import java.util.*; | |||
| public class WxMpServiceImpl implements WxMpService { | |||
| @@ -332,6 +337,19 @@ public class WxMpServiceImpl implements WxMpService { | |||
| String responseContent = execute(new SimplePostRequestExecutor(), url, json.toString()); | |||
| return WxMpQrCodeTicket.fromJson(responseContent); | |||
| } | |||
| public WxMpQrCodeTicket qrCodeCreateLastTicket(String scene_str) throws WxErrorException { | |||
| String url = "https://api.weixin.qq.com/cgi-bin/qrcode/create"; | |||
| JsonObject json = new JsonObject(); | |||
| json.addProperty("action_name", "QR_LIMIT_STR_SCENE"); | |||
| JsonObject actionInfo = new JsonObject(); | |||
| JsonObject scene = new JsonObject(); | |||
| scene.addProperty("scene_str", scene_str); | |||
| actionInfo.add("scene", scene); | |||
| json.add("action_info", actionInfo); | |||
| String responseContent = execute(new SimplePostRequestExecutor(), url, json.toString()); | |||
| return WxMpQrCodeTicket.fromJson(responseContent); | |||
| } | |||
| public File qrCodePicture(WxMpQrCodeTicket ticket) throws WxErrorException { | |||
| String url = "https://mp.weixin.qq.com/cgi-bin/showqrcode"; | |||
| @@ -613,4 +631,77 @@ public class WxMpServiceImpl implements WxMpService { | |||
| this.maxRetryTimes = maxRetryTimes; | |||
| } | |||
| @Override | |||
| public WxMpPrepayIdResult getPrepayId(String openId, String outTradeNo, double amt, String body, String tradeType, String ip, String callbackUrl) { | |||
| String nonce_str = System.currentTimeMillis() + ""; | |||
| SortedMap<String, String> packageParams = new TreeMap<String, String>(); | |||
| packageParams.put("appid", wxMpConfigStorage.getAppId()); | |||
| packageParams.put("mch_id", wxMpConfigStorage.getPartnerId()); | |||
| packageParams.put("nonce_str", nonce_str); | |||
| packageParams.put("body", body); | |||
| packageParams.put("out_trade_no", outTradeNo); | |||
| packageParams.put("total_fee", (int)(amt*100) + ""); | |||
| packageParams.put("spbill_create_ip", ip); | |||
| packageParams.put("notify_url", callbackUrl); | |||
| packageParams.put("trade_type", tradeType); | |||
| packageParams.put("openid", openId); | |||
| String sign = WxCryptUtil.createSign(packageParams, wxMpConfigStorage.getPartnerKey()); | |||
| String xml = "<xml>" + | |||
| "<appid>" + wxMpConfigStorage.getAppId() + "</appid>" + | |||
| "<mch_id>" + wxMpConfigStorage.getPartnerId() + "</mch_id>" + | |||
| "<nonce_str>" + nonce_str + "</nonce_str>" + | |||
| "<sign>" + sign + "</sign>" + | |||
| "<body><![CDATA[" + body + "]]></body>" + | |||
| "<out_trade_no>" + outTradeNo + "</out_trade_no>" + | |||
| "<total_fee>" + packageParams.get("total_fee") + "</total_fee>" + | |||
| "<spbill_create_ip>" + ip + "</spbill_create_ip>" + | |||
| "<notify_url>" + callbackUrl + "</notify_url>" + | |||
| "<trade_type>" + tradeType + "</trade_type>" + | |||
| "<openid>" + openId + "</openid>" + | |||
| "</xml>"; | |||
| HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/pay/unifiedorder"); | |||
| if (httpProxy != null) { | |||
| RequestConfig config = RequestConfig.custom().setProxy(httpProxy).build(); | |||
| httpPost.setConfig(config); | |||
| } | |||
| StringEntity entity = new StringEntity(xml, Consts.UTF_8); | |||
| httpPost.setEntity(entity); | |||
| try { | |||
| CloseableHttpResponse response = httpClient.execute(httpPost); | |||
| String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); | |||
| XStream xstream = XStreamInitializer.getInstance(); | |||
| xstream.alias("xml", WxMpPrepayIdResult.class); | |||
| WxMpPrepayIdResult wxMpPrepayIdResult = (WxMpPrepayIdResult) xstream.fromXML(responseContent); | |||
| return wxMpPrepayIdResult; | |||
| } catch (IOException e) { | |||
| e.printStackTrace(); | |||
| } | |||
| return new WxMpPrepayIdResult(); | |||
| } | |||
| @Override | |||
| public Map<String, String> getJSSDKPayInfo(String openId, String outTradeNo, double amt, String body, String tradeType, String ip, String callbackUrl) { | |||
| WxMpPrepayIdResult wxMpPrepayIdResult = getPrepayId(openId, outTradeNo, amt, body, tradeType, ip, callbackUrl); | |||
| String prepayId = wxMpPrepayIdResult.getPrepay_id(); | |||
| if (prepayId == null || prepayId.equals("")) { | |||
| throw new RuntimeException("get prepayid error"); | |||
| } | |||
| Map<String, String> payInfo = new HashMap<String, String>(); | |||
| payInfo.put("appId", wxMpConfigStorage.getAppId()); | |||
| // 支付签名时间戳,注意微信jssdk中的所有使用timestamp字段均为小写。但最新版的支付后台生成签名使用的timeStamp字段名需大写其中的S字符 | |||
| payInfo.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000)); | |||
| payInfo.put("nonceStr", System.currentTimeMillis() + ""); | |||
| payInfo.put("package", "prepay_id=" + prepayId); | |||
| payInfo.put("signType", "MD5"); | |||
| String finalSign = WxCryptUtil.createSign(payInfo, wxMpConfigStorage.getPartnerKey()); | |||
| payInfo.put("sign", finalSign); | |||
| return payInfo; | |||
| } | |||
| } | |||
| @@ -0,0 +1,98 @@ | |||
| package me.chanjar.weixin.mp.bean.result; | |||
| import java.io.Serializable; | |||
| /** | |||
| * <pre> | |||
| * 群发消息一发送就返回的结果 | |||
| * | |||
| * 真正的群发消息是否发送成功要看 | |||
| * http://mp.weixin.qq.com/wiki/index.php?title=高级群发接口#.E4.BA.8B.E4.BB.B6.E6.8E.A8.E9.80.81.E7.BE.A4.E5.8F.91.E7.BB.93.E6.9E.9C | |||
| * | |||
| * </pre> | |||
| * | |||
| * @author chanjarster | |||
| */ | |||
| public class WxMpPrepayIdResult implements Serializable { | |||
| private String return_code; | |||
| private String return_msg; | |||
| private String appid; | |||
| private String mch_id; | |||
| private String nonce_str; | |||
| private String sign; | |||
| private String result_code; | |||
| private String prepay_id; | |||
| private String trade_type; | |||
| 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; | |||
| } | |||
| } | |||