diff --git a/src/main/java/io/github/dbstarll/weixin/sdk/SecretHolder.java b/src/main/java/io/github/dbstarll/weixin/sdk/SecretHolder.java new file mode 100644 index 0000000..29ef577 --- /dev/null +++ b/src/main/java/io/github/dbstarll/weixin/sdk/SecretHolder.java @@ -0,0 +1,11 @@ +package io.github.dbstarll.weixin.sdk; + +public interface SecretHolder { + /** + * 根据AppId获得Secret. + * + * @param appId appId + * @return appId对应的secret + */ + String getSecret(String appId); +} diff --git a/src/main/java/io/github/dbstarll/weixin/sdk/WeChatApi.java b/src/main/java/io/github/dbstarll/weixin/sdk/WeChatApi.java new file mode 100644 index 0000000..5373190 --- /dev/null +++ b/src/main/java/io/github/dbstarll/weixin/sdk/WeChatApi.java @@ -0,0 +1,80 @@ +package io.github.dbstarll.weixin.sdk; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.github.dbstarll.utils.http.client.request.RelativeUriResolver; +import io.github.dbstarll.utils.json.jackson.JsonApiClient; +import io.github.dbstarll.utils.net.api.ApiException; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.core5.http.ClassicHttpRequest; +import org.apache.hc.core5.http.io.support.ClassicRequestBuilder; + +import java.io.IOException; + +import static org.apache.commons.lang3.Validate.notBlank; +import static org.apache.commons.lang3.Validate.notNull; + +public class WeChatApi extends JsonApiClient { + private final SecretHolder secretHolder; + + /** + * 构造WeChatApi. + * + * @param httpClient httpClient + * @param objectMapper objectMapper + * @param secretHolder SecretHolder + */ + public WeChatApi(final HttpClient httpClient, final ObjectMapper objectMapper, final SecretHolder secretHolder) { + super(httpClient, true, objectMapper); + this.secretHolder = notNull(secretHolder, "secretHolder not set"); + setUriResolver(new RelativeUriResolver("https://api.weixin.qq.com")); + } + + @Override + protected T postProcessing(final ClassicHttpRequest request, final T executeResult) throws ApiException { + final T superResult = super.postProcessing(request, executeResult); + if (superResult instanceof ObjectNode) { + final ObjectNode node = (ObjectNode) superResult; + final int errcode = node.path("errcode").asInt(0); + if (errcode != 0) { + throw new WeChatResponseException(errcode, node.path("errmsg").asText()); + } + } + return superResult; + } + + /** + * 登录凭证校验. + * + * @param appId 小程序 appId + * @param code 登录时获取的 code + * @return 登录凭证 + * @throws IOException in case of a problem or the connection was aborted + * @throws ApiException in case of an api error + */ + public ObjectNode session(final String appId, final String code) throws IOException, ApiException { + return execute(auth(post("/sns/jscode2session") + .addParameter("grant_type", "authorization_code") + .addParameter("js_code", notBlank(code, "code not set")), appId), ObjectNode.class); + } + + /** + * 获取小程序全局唯一后台接口调用凭据,token有效期为7200s,开发者需要进行妥善保存. + * + * @param appId 小程序唯一凭证,即 AppID,可在「微信公众平台 - 设置 - 开发设置」页中获得 + * @return 接口调用凭据 + * @throws IOException in case of a problem or the connection was aborted + * @throws ApiException in case of an api error + */ + public ObjectNode accessToken(final String appId) throws IOException, ApiException { + return execute(auth(get("/cgi-bin/token") + .addParameter("grant_type", "client_credential"), appId), ObjectNode.class); + } + + private ClassicHttpRequest auth(final ClassicRequestBuilder builder, final String appId) { + return builder + .addParameter("appid", notBlank(appId, "appId not set")) + .addParameter("secret", notBlank(secretHolder.getSecret(appId), "secret not found for {}", appId)) + .build(); + } +} diff --git a/src/main/java/io/github/dbstarll/weixin/sdk/WeChatResponseException.java b/src/main/java/io/github/dbstarll/weixin/sdk/WeChatResponseException.java new file mode 100644 index 0000000..afed168 --- /dev/null +++ b/src/main/java/io/github/dbstarll/weixin/sdk/WeChatResponseException.java @@ -0,0 +1,31 @@ +package io.github.dbstarll.weixin.sdk; + +import io.github.dbstarll.utils.net.api.ApiResponseException; +import org.apache.commons.lang3.StringUtils; +import org.apache.hc.client5.http.HttpResponseException; + +public class WeChatResponseException extends ApiResponseException { + private static final String RID_SPLIT_TOKEN = " rid: "; + + private final String rid; + + /** + * 构建WeChatResponseException. + * + * @param errCode 错误码 + * @param errMsg 错误信息 + */ + public WeChatResponseException(final int errCode, final String errMsg) { + super(new HttpResponseException(errCode, StringUtils.substringBefore(errMsg, RID_SPLIT_TOKEN))); + this.rid = StringUtils.substringAfter(errMsg, RID_SPLIT_TOKEN); + } + + /** + * 获得rid信息. + * + * @return rid信息 + */ + public final String getRid() { + return rid; + } +} diff --git a/src/test/java/io/github/dbstarll/weixin/sdk/WeChatApiTest.java b/src/test/java/io/github/dbstarll/weixin/sdk/WeChatApiTest.java new file mode 100644 index 0000000..cb985ef --- /dev/null +++ b/src/test/java/io/github/dbstarll/weixin/sdk/WeChatApiTest.java @@ -0,0 +1,42 @@ +package io.github.dbstarll.weixin.sdk; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.dbstarll.utils.http.client.HttpClientFactory; +import org.apache.hc.client5.http.classic.HttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.ThrowingConsumer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +class WeChatApiTest { + private void useClient(final ThrowingConsumer consumer) throws Throwable { + try (CloseableHttpClient client = new HttpClientFactory().build()) { + consumer.accept(client); + } + } + + private void useApi(final ThrowingConsumer consumer, final SecretHolder secretHolder) throws Throwable { + useClient(httpClient -> consumer.accept(new WeChatApi(httpClient, new ObjectMapper(), secretHolder))); + } + + @Test + void nullSecretHolder() { + final Exception e = assertThrowsExactly(NullPointerException.class, + () -> useApi(api -> { + }, null)); + assertEquals("secretHolder not set", e.getMessage()); + } + + @Test + void accessToken() throws Throwable { + useApi(api -> { + final WeChatResponseException e = assertThrowsExactly(WeChatResponseException.class, () -> api.accessToken("appId")); + assertEquals(40013, e.getStatusCode()); + assertEquals("invalid appid", e.getReasonPhrase()); + assertNotNull(e.getRid()); + }, appId -> "secret"); + } +} \ No newline at end of file