diff --git a/weixin4j-wxa/pom.xml b/weixin4j-wxa/pom.xml index 81c827b4..8b2a39e6 100644 --- a/weixin4j-wxa/pom.xml +++ b/weixin4j-wxa/pom.xml @@ -25,6 +25,11 @@ weixin4j-base ${project.version} + + com.foxinmy + weixin4j-mp + ${project.version} + commons-codec commons-codec diff --git a/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/WeixinProxy.java b/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/WeixinProxy.java index 90484f17..e21ded07 100644 --- a/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/WeixinProxy.java +++ b/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/WeixinProxy.java @@ -1,7 +1,14 @@ package com.foxinmy.weixin4j.wxa; +import com.foxinmy.weixin4j.cache.CacheStorager; +import com.foxinmy.weixin4j.cache.FileCacheStorager; +import com.foxinmy.weixin4j.model.Token; import com.foxinmy.weixin4j.model.WeixinAccount; +import com.foxinmy.weixin4j.mp.token.WeixinTokenCreator; +import com.foxinmy.weixin4j.token.TokenCreator; +import com.foxinmy.weixin4j.token.TokenManager; import com.foxinmy.weixin4j.wxa.api.LoginApi; +import com.foxinmy.weixin4j.wxa.api.QrCodeApi; /** * @since 1.8 @@ -9,18 +16,60 @@ import com.foxinmy.weixin4j.wxa.api.LoginApi; public class WeixinProxy { private final LoginApi loginApi; + private final QrCodeApi qrCodeApi; - public WeixinProxy(WeixinAccount weixinAccount) { + public WeixinProxy( + WeixinAccount weixinAccount + ) { + this( + weixinAccount, + new FileCacheStorager() + ); + } + + public WeixinProxy( + WeixinAccount weixinAccount, + CacheStorager cacheStorager + ) { + this( + weixinAccount, + new WeixinTokenCreator(weixinAccount.getId(), weixinAccount.getSecret()), + cacheStorager + ); + } + + private WeixinProxy( + WeixinAccount weixinAccount, + TokenCreator tokenCreator, + CacheStorager cacheStorager + ) { if (weixinAccount == null) { throw new IllegalArgumentException( "weixinAccount must not be empty"); } + if (tokenCreator == null) { + throw new IllegalArgumentException( + "tokenCreator must not be empty"); + } + + if (cacheStorager == null) { + throw new IllegalArgumentException( + "cacheStorager must not be empty"); + } + + final TokenManager tokenManager = new TokenManager(tokenCreator, cacheStorager); + this.loginApi = new LoginApi(weixinAccount); + this.qrCodeApi = new QrCodeApi(tokenManager); } public LoginApi getLoginApi() { return loginApi; } + public QrCodeApi getQrCodeApi() { + return qrCodeApi; + } + } diff --git a/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/Color.java b/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/Color.java new file mode 100644 index 00000000..6f881fd7 --- /dev/null +++ b/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/Color.java @@ -0,0 +1,46 @@ +package com.foxinmy.weixin4j.wxa.api; + +import java.io.Serializable; + +class Color implements Serializable { + + private static final long serialVersionUID = 2018052201L; + + private String r; + private String g; + private String b; + + public Color() { + } + + public Color(java.awt.Color color) { + this.r = String.valueOf(color.getRed()); + this.g = String.valueOf(color.getGreen()); + this.b = String.valueOf(color.getBlue()); + } + + public String getR() { + return r; + } + + public void setR(String r) { + this.r = r; + } + + public String getG() { + return g; + } + + public void setG(String g) { + this.g = g; + } + + public String getB() { + return b; + } + + public void setB(String b) { + this.b = b; + } + +} diff --git a/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/QrCodeApi.java b/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/QrCodeApi.java new file mode 100644 index 00000000..4f0890a4 --- /dev/null +++ b/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/QrCodeApi.java @@ -0,0 +1,166 @@ +package com.foxinmy.weixin4j.wxa.api; + +import java.awt.Color; +import java.io.IOException; + +import com.alibaba.fastjson.JSON; +import com.foxinmy.weixin4j.exception.WeixinException; +import com.foxinmy.weixin4j.http.ContentType; +import com.foxinmy.weixin4j.http.weixin.WeixinResponse; +import com.foxinmy.weixin4j.token.TokenManager; + +/** + * 获取二维码. + * + *

+ * 通过后台接口可以获取小程序任意页面的二维码,扫描该二维码可以直接进入小程序对应的页面。 + * 目前微信支持两种二维码,小程序码,小程序二维码。 + *

+ * + * @see 获取二维码 + * @since 1.8 + */ +public class QrCodeApi extends WxaApi { + + private final TokenManager tokenManager; + + public QrCodeApi(TokenManager tokenManager) { + this.tokenManager = tokenManager; + } + + /** + * 获取小程序码. + * + *

接口A: 适用于需要的码数量较少的业务场景

+ *

+ * 注意:通过该接口生成的小程序码,永久有效,数量限制见微信小程序文档文末说明,请谨慎使用。 + * 用户扫描该码进入小程序后,将直接进入 path 对应的页面。 + *

+ * + * @param path 不能为空,最大长度 128 字节 + * @param width 二维码的宽度,默认值 430 + * @param autoColor 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调 + * @param lineColor authColor 为 false 时生效 + * @param hyaline 是否需要透明底色, 为true时,生成透明底色的小程序码 + * @return image bytes of WXA code. + * @throws WeixinException indicates getting access token failed or getting WXA code failed. + * + * @see 获取小程序码 + */ + public byte[] getWxaCode( + String path, + Integer width, + Boolean autoColor, + Color lineColor, + Boolean hyaline + ) throws WeixinException { + final String accessToken = tokenManager.getAccessToken(); + final String getWxaCodeUri = this.getRequestUri("wxa_getwxacode", accessToken); + final WxaCodeParameter param = new WxaCodeParameter(path, width, autoColor, lineColor, hyaline); + return this.postAsImageBytes(getWxaCodeUri, param); + } + + /** + * 获取小程序码. + * + *

接口B:适用于需要的码数量极多的业务场景

+ *

+ * 注意:通过该接口生成的小程序码,永久有效,数量暂无限制。 + * 用户扫描该码进入小程序后,开发者需在对应页面获取的码中 scene 字段的值,再做处理逻辑。 + * 使用如下代码可以获取到二维码中的 scene 字段的值。 + * 调试阶段可以使用开发工具的条件编译自定义参数 scene=xxxx 进行模拟, + * 开发工具模拟时的 scene 的参数值需要进行 urlencode。 + *

+ *

+	 * // 这是首页的 js
+	 * Page({
+	 *   onLoad: function(options) {
+	 *     // options 中的 scene 需要使用 decodeURIComponent 才能获取到生成二维码时传入的 scene
+	 *     var scene = decodeURIComponent(options.scene)
+	 *   }
+	 * })
+	 * 
+ * + * @param scene 最大32个可见字符,只支持数字, + * 大小写英文以及部分特殊字符:!#$&'()*+,/:;=?@-._~, + * 其它字符请自行编码为合法字符(因不支持%,中文无法使用 urlencode 处理,请使用其他编码方式) + * @param page 必须是已经发布的小程序存在的页面(否则报错), + * 例如 "pages/index/index",根路径前不要填加'/', + * 不能携带参数(参数请放在scene字段里), + * 如果不填写这个字段,默认跳主页面 + * @param width 二维码的宽度,默认值 430 + * @param autoColor 自动配置线条颜色,如果颜色依然是黑色,则说明不建议配置主色调,默认值 false + * @param lineColor autoColor 为 false 时生效,使用 rgb 设置颜色 + * @param hyaline 是否需要透明底色,为true时,生成透明底色的小程序码 + * @return image bytes of WXA code. + * @throws WeixinException indicates getting access token failed or getting WXA code failed. + * + * @see 获取小程序码 + */ + public byte[] getWxaCodeUnlimit( + String scene, + String page, + Integer width, + Boolean autoColor, + Color lineColor, + Boolean hyaline + ) throws WeixinException { + final String accessToken = tokenManager.getAccessToken(); + final String getWxaCodeUnlimitUri = this.getRequestUri("wxa_getwxacodeunlimit", accessToken); + final WxaCodeUnlimitParameter param = new WxaCodeUnlimitParameter(scene, page, width, autoColor, lineColor, hyaline); + return this.postAsImageBytes(getWxaCodeUnlimitUri, param); + } + + /** + * 获取小程序二维码. + * + *

接口C:适用于需要的码数量较少的业务场景

+ *

+ * 注意:通过该接口生成的小程序二维码,永久有效,数量限制见微信小程序文档文末说明,请谨慎使用。 + * 用户扫描该码进入小程序后,将直接进入 path 对应的页面。 + *

+ * + * @param path 不能为空,最大长度 128 字节 + * @param width 二维码的宽度,默认值 430 + * @return image bytes of WXA QR code. + * @throws WeixinException indicates getting access token failed or getting WXA QR code failed. + * + * @see 获取小程序二维码 + */ + public byte[] createWxaQrCode( + String path, + Integer width + ) throws WeixinException { + final String accessToken = tokenManager.getAccessToken(); + final String createWxaQrCode = this.getRequestUri("wxaapp_createwxaqrcode", accessToken); + final WxaQrCodeParameter param = new WxaQrCodeParameter(path, width); + return this.postAsImageBytes(createWxaQrCode, param); + } + + private byte[] postAsImageBytes(String uri, Object param) throws WeixinException { + final String body = JSON.toJSONString(param); + final WeixinResponse response = this.weixinExecutor.post(uri, body); + return toImageBytes(response); + } + + private byte[] toImageBytes(WeixinResponse response) throws WeixinException { + try { + return readImageBytes(response); + } catch (IOException e) { + throw new WeixinException(e); + } catch (WxaCodeError e) { + throw new WeixinException(Integer.toString(e.getErrcode()), e.getErrmsg()); + } + } + + private byte[] readImageBytes(WeixinResponse response) throws IOException, WxaCodeError { + final String contentType = response.getHeaders().getContentType(); + if (contentType != null && contentType.equals(ContentType.APPLICATION_JSON.getMimeType().getType())) { + WxaCodeError wxaCodeError = JSON.parseObject(response.getContent(), WxaCodeError.class); + throw wxaCodeError; + } else { + return response.getContent(); + } + } + +} diff --git a/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/WxaCodeError.java b/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/WxaCodeError.java new file mode 100644 index 00000000..ff31c025 --- /dev/null +++ b/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/WxaCodeError.java @@ -0,0 +1,39 @@ +package com.foxinmy.weixin4j.wxa.api; + +class WxaCodeError extends Exception { + + private static final long serialVersionUID = 2018052201L; + + private int errcode; + private String errmsg; + + public WxaCodeError() { + } + + public WxaCodeError(int errcode, String errmsg) { + this.errcode = errcode; + this.errmsg = errmsg; + } + + public int getErrcode() { + return errcode; + } + + public void setErrcode(int errcode) { + this.errcode = errcode; + } + + public String getErrmsg() { + return errmsg; + } + + public void setErrmsg(String errmsg) { + this.errmsg = errmsg; + } + + @Override + public String getMessage() { + return errmsg; + } + +} diff --git a/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/WxaCodeParameter.java b/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/WxaCodeParameter.java new file mode 100644 index 00000000..1854e37b --- /dev/null +++ b/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/WxaCodeParameter.java @@ -0,0 +1,76 @@ +package com.foxinmy.weixin4j.wxa.api; + +import java.io.Serializable; + +import com.alibaba.fastjson.annotation.JSONField; + +class WxaCodeParameter implements Serializable { + + private static final long serialVersionUID = 2018052201L; + + private String path; + private Integer width; + private Boolean autoColor; + private Color color; + private Boolean hyaline; + + public WxaCodeParameter( + String path, + Integer width, + Boolean autoColor, + java.awt.Color color, + Boolean hyaline + ) { + this.path = path; + this.width = width; + this.autoColor = autoColor; + if (color != null) { + this.color = new Color(color); + } + this.hyaline = hyaline; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public Integer getWidth() { + return width; + } + + public void setWidth(Integer width) { + this.width = width; + } + + @JSONField(name = "auto_color") + public Boolean getAutoColor() { + return autoColor; + } + + public void setAutoColor(Boolean autoColor) { + this.autoColor = autoColor; + } + + @JSONField(name = "line_color") + public Color getColor() { + return color; + } + + public void setColor(Color color) { + this.color = color; + } + + @JSONField(name = "is_hyaline") + public Boolean getHyaline() { + return hyaline; + } + + public void setHyaline(Boolean hyaline) { + this.hyaline = hyaline; + } + +} diff --git a/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/WxaCodeUnlimitParameter.java b/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/WxaCodeUnlimitParameter.java new file mode 100644 index 00000000..303175b4 --- /dev/null +++ b/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/WxaCodeUnlimitParameter.java @@ -0,0 +1,87 @@ +package com.foxinmy.weixin4j.wxa.api; + +import java.io.Serializable; + +import com.alibaba.fastjson.annotation.JSONField; + +class WxaCodeUnlimitParameter implements Serializable { + + private static final long serialVersionUID = 2018052201L; + + private String scene; + private String page; + private Integer width; + private Boolean autoColor; + private Color color; + private Boolean hyaline; + + public WxaCodeUnlimitParameter( + String scene, + String page, + Integer width, + Boolean autoColor, + java.awt.Color color, + Boolean hyaline + ) { + this.scene = scene; + this.page = page; + this.width = width; + this.autoColor = autoColor; + if (color != null) { + this.color = new Color(color); + } + this.hyaline = hyaline; + } + + public String getScene() { + return scene; + } + + public void setScene(String scene) { + this.scene = scene; + } + + public String getPage() { + return page; + } + + public void setPage(String page) { + this.page = page; + } + + public Integer getWidth() { + return width; + } + + public void setWidth(Integer width) { + this.width = width; + } + + @JSONField(name = "auto_color") + public Boolean getAutoColor() { + return autoColor; + } + + public void setAutoColor(Boolean autoColor) { + this.autoColor = autoColor; + } + + @JSONField(name = "line_color") + public Color getColor() { + return color; + } + + public void setColor(Color color) { + this.color = color; + } + + @JSONField(name = "is_hyaline") + public Boolean getHyaline() { + return hyaline; + } + + public void setHyaline(Boolean hyaline) { + this.hyaline = hyaline; + } + +} diff --git a/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/WxaQrCodeParameter.java b/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/WxaQrCodeParameter.java new file mode 100644 index 00000000..0470faae --- /dev/null +++ b/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/WxaQrCodeParameter.java @@ -0,0 +1,33 @@ +package com.foxinmy.weixin4j.wxa.api; + +import java.io.Serializable; + +class WxaQrCodeParameter implements Serializable { + + private static final long serialVersionUID = 2018052201L; + + private String path; + private Integer width; + + public WxaQrCodeParameter(String path, Integer width) { + this.path = path; + this.width = width; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public Integer getWidth() { + return width; + } + + public void setWidth(Integer width) { + this.width = width; + } + +} diff --git a/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/weixin.properties b/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/weixin.properties index e92081b2..72a3725c 100644 --- a/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/weixin.properties +++ b/weixin4j-wxa/src/main/java/com/foxinmy/weixin4j/wxa/api/weixin.properties @@ -6,3 +6,12 @@ api_base_url=https://api.weixin.qq.com # \u767b\u5f55\u51ed\u8bc1\u6821\u9a8c sns_jscode2session={api_base_url}/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=%s + +# \u83b7\u53d6\u5c0f\u7a0b\u5e8f\u7801 \u63a5\u53e3A: \u9002\u7528\u4e8e\u9700\u8981\u7684\u7801\u6570\u91cf\u8f83\u5c11\u7684\u4e1a\u52a1\u573a\u666f +wxa_getwxacode={api_base_url}/wxa/getwxacode?access_token=%s + +# \u83b7\u53d6\u5c0f\u7a0b\u5e8f\u7801 \u63a5\u53e3B\uff1a\u9002\u7528\u4e8e\u9700\u8981\u7684\u7801\u6570\u91cf\u6781\u591a\u7684\u4e1a\u52a1\u573a\u666f +wxa_getwxacodeunlimit={api_base_url}/wxa/getwxacodeunlimit?access_token=%s + +# \u83b7\u53d6\u5c0f\u7a0b\u5e8f\u4e8c\u7ef4\u7801 \u63a5\u53e3C\uff1a\u9002\u7528\u4e8e\u9700\u8981\u7684\u7801\u6570\u91cf\u8f83\u5c11\u7684\u4e1a\u52a1\u573a\u666f +wxaapp_createwxaqrcode={api_base_url}/cgi-bin/wxaapp/createwxaqrcode?access_token=%s diff --git a/weixin4j-wxa/src/test/java/com/foxinmy/weixin4j/wxa/api/WxaCodeParameterTest.java b/weixin4j-wxa/src/test/java/com/foxinmy/weixin4j/wxa/api/WxaCodeParameterTest.java new file mode 100644 index 00000000..3f2f39c9 --- /dev/null +++ b/weixin4j-wxa/src/test/java/com/foxinmy/weixin4j/wxa/api/WxaCodeParameterTest.java @@ -0,0 +1,21 @@ +package com.foxinmy.weixin4j.wxa.api; + +import static org.junit.Assert.assertTrue; + +import java.awt.Color; + +import org.junit.Test; + +import com.alibaba.fastjson.JSON; + +public class WxaCodeParameterTest { + + @Test + public void test() { + WxaCodeParameter param = new WxaCodeParameter("pages/index/index", 430, false, new Color(1, 2, 3), true); + String json = JSON.toJSONString(param); + assertTrue(json.contains("\"line_color\":")); + assertTrue(json.contains("\"is_hyaline\":true")); + } + +} diff --git a/weixin4j-wxa/src/test/java/com/foxinmy/weixin4j/wxa/api/WxaCodeUnlimitParameterTest.java b/weixin4j-wxa/src/test/java/com/foxinmy/weixin4j/wxa/api/WxaCodeUnlimitParameterTest.java new file mode 100644 index 00000000..54c2d1dd --- /dev/null +++ b/weixin4j-wxa/src/test/java/com/foxinmy/weixin4j/wxa/api/WxaCodeUnlimitParameterTest.java @@ -0,0 +1,21 @@ +package com.foxinmy.weixin4j.wxa.api; + +import static org.junit.Assert.assertTrue; + +import java.awt.Color; + +import org.junit.Test; + +import com.alibaba.fastjson.JSON; + +public class WxaCodeUnlimitParameterTest { + + @Test + public void test() { + WxaCodeUnlimitParameter param = new WxaCodeUnlimitParameter("myScene", "pages/index/index", 430, false, new Color(1, 2, 3), true); + String json = JSON.toJSONString(param); + assertTrue(json.contains("\"line_color\":")); + assertTrue(json.contains("\"is_hyaline\":true")); + } + +} diff --git a/weixin4j-wxa/src/test/java/com/foxinmy/weixin4j/wxa/api/WxaQrCodeParameterTest.java b/weixin4j-wxa/src/test/java/com/foxinmy/weixin4j/wxa/api/WxaQrCodeParameterTest.java new file mode 100644 index 00000000..5d0b677e --- /dev/null +++ b/weixin4j-wxa/src/test/java/com/foxinmy/weixin4j/wxa/api/WxaQrCodeParameterTest.java @@ -0,0 +1,19 @@ +package com.foxinmy.weixin4j.wxa.api; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.alibaba.fastjson.JSON; + +public class WxaQrCodeParameterTest { + + @Test + public void test() { + WxaQrCodeParameter param = new WxaQrCodeParameter("myPath", 100); + String json = JSON.toJSONString(param); + assertTrue(json.contains("\"path\":\"myPath\"")); + assertTrue(json.contains("\"width\":100")); + } + +}