diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000000..435b7250ba --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,11 @@ +version: 2.1 +jobs: + build: + docker: + - image: cimg/openjdk:11.0 + steps: + - checkout + - run: + name: Build + command: mvn -B -DskipTests clean package -Dcheckstyle.skip=true + diff --git a/.gitee/ISSUE_TEMPLATE.md b/.gitee/ISSUE_TEMPLATE.md index cdae693d35..a0b60ba750 100644 --- a/.gitee/ISSUE_TEMPLATE.md +++ b/.gitee/ISSUE_TEMPLATE.md @@ -1,4 +1,4 @@ -强烈建议大家到 `github` 相关页面提交问题,方便统一查询管理,具体页面地址:https://github.com/Wechat-Group/WxJava/issues +强烈建议大家到 `github` 相关页面提交问题,方便统一查询管理,具体页面地址:https://github.com/binarywang/WxJava/issues 当然如果必须在这里提问,请务必按以下格式填写,谢谢配合~ diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index ca05962438..ced7d6de0c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,6 +1,6 @@ --- name: Bug报告 -about: 如果发现Bug,请告诉我们,我们会尽快修复 +about: 本项目仅对最新版本进行维护,使用老版本出现问题的盆友,请先升级到最新版本,升级完后如果发现bug依然存在,请继续填写此issue。 title: '' labels: '' assignees: '' @@ -8,17 +8,21 @@ assignees: '' --- # 提问前,请确保阅读过项目首页说明以及wiki开发文档相关内容,尤其是常见问题部分。完成内容后,请务必移除包括本句在内的无用内容,以免影响阅读,否则直接关闭,谢谢合作~ -# 另外如果确认属于bug,而且已明确如何修复,请参考贡献指南直接提交PR,省的浪费时间在这里描述问题,非常感谢配合 + +## 另外如果确认属于bug,而且已明确如何修复,请参考贡献指南直接提交PR,省的浪费时间在这里描述问题,非常感谢配合 ### 简要描述 -__简单概括描述下你所遇到的问题。__ +__请简单概括描述下你所遇到的问题。__ ### 模块版本情况 * WxJava 模块名: -* WxJava 版本号: +* WxJava 版本号:(旧版本不予支持,谢谢配合) ### 详细描述 __尽量详细描述。请不要使用截图,尽量使用文字描述,代码直接贴上来,日志则请附在后面所示区域。__ ### 日志 -__将日志放在 [pastebin](https://paste.ubuntu.com/) 或者其他地方,并将其url地址贴在这里__ +__如果日志不多,直接使用md代码引用格式贴在此处,否则如果太长,请将日志放在 [pastebin](https://paste.ubuntu.com/) 或者其他地方,然后将其url地址贴在这里__ +``` +日志请写于此处 +``` diff --git a/.github/agents/my-agent.agent.md b/.github/agents/my-agent.agent.md new file mode 100644 index 0000000000..bd1b4572eb --- /dev/null +++ b/.github/agents/my-agent.agent.md @@ -0,0 +1,16 @@ +--- +# Fill in the fields below to create a basic custom agent for your repository. +# The Copilot CLI can be used for local testing: https://gh.io/customagents/cli +# To make this agent available, merge this file into the default repository branch. +# For format details, see: https://gh.io/customagents/config + +name: 全部用中文 +description: 需要用中文,包括PR标题和分析总结过程 +--- + +# My Agent + +- 1、请使用中文输出思考过程和总结,包括PR标题,提交commit信息也要使用中文; +- 2、生成代码时需要提供必要的单元测试代码; +- 3、实现接口时请严格按照官方文档编写代码,严禁瞎编乱造、臆想并实现不存在的接口; +- 4、新增加的代码如果标记作者信息,请注意不要把作者名设为binarywang或者其他无关人员,要改为 GitHub Copilot。 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..cad29d96d9 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,202 @@ +# Copilot Instruction +请始终使用中文生成 Pull Request 的标题、描述和提交信息 + + +# WxJava - 微信 Java SDK 开发说明 + +WxJava 是一个支持多种微信平台的完整 Java SDK,包含公众号、小程序、微信支付、企业微信、开放平台、视频号、企点等多种功能模块。 + +**请始终优先参考本说明,只有在遇到与此内容不一致的意外信息时,才退而使用搜索或 bash 命令。** + +## 高效开发指南 + +### 前置条件与环境准备 +- **Java 要求**:JDK 8+(项目最低目标为 Java 8) +- **Maven**:推荐 Maven 3.6+(已验证 Maven 3.9.11) +- **IDE**:推荐使用 IntelliJ IDEA(项目针对 IDEA 优化) + +### 引导、构建与校验 +克隆仓库后按顺序执行以下命令: + +```bash +# 1. 基础编译(请勿中断 - 约需 4-5 分钟) +mvn clean compile -DskipTests=true --no-transfer-progress +# 超时时间:建议设置 8 分钟以上。实际时间:约 4 分钟 + +# 2. 完整打包(请勿中断 - 约需 2-3 分钟) +mvn clean package -DskipTests=true --no-transfer-progress +# 超时时间:建议设置 5 分钟以上。实际时间:约 2 分钟 + +# 3. 代码质量校验(请勿中断 - 约需 45-60 秒) +mvn checkstyle:check --no-transfer-progress +# 超时时间:建议设置 3 分钟以上。实际时间:约 50 秒 +``` + +重要时间说明: +- 绝对不要中断任意 Maven 构建命令 +- 编译阶段耗时最长(约 4 分钟),原因是项目包含 34 个模块 +- 后续构建会更快,因为存在增量编译 +- 始终使用 `--no-transfer-progress` 以减少日志噪音 + +### 测试结构 +- **测试框架**:TestNG(非 JUnit) +- **测试文件**:共有 298 个测试文件 +- **默认行为**:pom.xml 中默认禁用测试(`true`) +- **测试配置**:测试需要通过 test-config.xml 提供真实的微信 API 凭据 +- **注意**:没有真实微信 API 凭据请不要尝试运行测试,测试将会失败 + +## 项目结构与导航 + +### 核心 SDK 模块(主要开发区) +- `weixin-java-common/` - 通用工具与基础类(最重要) +- `weixin-java-mp/` - 公众号 SDK +- `weixin-java-pay/` - 微信支付 SDK +- `weixin-java-miniapp/` - 小程序 SDK +- `weixin-java-cp/` - 企业微信 SDK +- `weixin-java-open/` - 开放平台 SDK +- `weixin-java-channel/` - 视频号 / Channel SDK +- `weixin-java-qidian/` - 企点 SDK + +### 框架集成模块 +- `spring-boot-starters/` - Spring Boot 自动配置 starter +- `solon-plugins/` - Solon 框架插件 +- `weixin-graal/` - GraalVM 本地镜像支持 + +### 配置与质量控制 +- `quality-checks/google_checks.xml` - Checkstyle 配置 +- `.editorconfig` - 代码格式规则(2 个空格等于 1 个制表) +- `pom.xml` - 根级 Maven 配置 + +## 开发工作流 + +### 修改代码的流程 +1. 修改前务必先构建以建立干净基线: + ```bash + mvn clean compile --no-transfer-progress + ``` + +2. 遵循代码风格(由 checkstyle 强制): + - 缩进使用 2 个空格(不要用制表符) + - 遵循 Google Java 风格指南 + - 在 IDE 中安装 EditorConfig 插件 + +3. 增量验证修改: + ```bash + # 每次修改后运行: + mvn compile --no-transfer-progress + mvn checkstyle:check --no-transfer-progress + ``` + +### 提交修改前的必须校验 +请务必按顺序完成以下校验步骤: + +1. 代码风格校验: + ```bash + mvn checkstyle:check --no-transfer-progress + # 必须通过 - 约需 50 秒 + ``` + +2. 完整清理构建: + ```bash + mvn clean package -DskipTests=true --no-transfer-progress + # 必须成功 - 约需 2 分钟 + ``` + +3. 文档:为公共方法和类补充或更新 javadoc +4. 贡献规范:遵循 `CONTRIBUTING.md`,Pull Request 必须以 `develop` 分支为目标 + +## 模块依赖与构建顺序 + +### 核心模块依赖(构建顺序) +1. `weixin-graal`(GraalVM 支持) +2. `weixin-java-common`(所有模块的基础) +3. 核心 SDK 模块(mp、pay、miniapp、cp、open、channel、qidian) +4. 框架集成(spring-boot-starters、solon-plugins) + +### 主要关系模式 +- 所有 SDK 模块都依赖于 `weixin-java-common` +- Spring Boot starters 依赖对应的 SDK 模块 +- Solon 插件遵循与 Spring Boot starters 相同的依赖模式 +- 每个模块都有单账号与多账号配置支持 + +## 常见任务与命令 + +### 验证指定模块 +```bash +# 构建单个模块(将 'weixin-java-mp' 替换为目标模块): +cd weixin-java-mp +mvn clean compile --no-transfer-progress +``` + +### 检查依赖 +```bash +# 分析依赖树: +mvn dependency:tree --no-transfer-progress + +# 检查依赖更新: +./others/check-dependency-updates.sh +``` + +### 发布与发布准备 +```bash +# 版本检查: +mvn versions:display-property-updates --no-transfer-progress + +# 部署(需要凭据): +mvn clean deploy -P release --no-transfer-progress +``` + +## 重要文件与位置 + +### 配置文件 +- `pom.xml` - 根级 Maven 配置与依赖管理 +- `quality-checks/google_checks.xml` - Checkstyle 规则 +- `.editorconfig` - IDE 格式化配置 +- `.github/workflows/maven-publish.yml` - CI/CD 工作流 + +### 文档 +- `README.md` - 项目概览与使用说明(中文) +- `CONTRIBUTING.md` - 贡献指南 +- `demo.md` - 示例项目与演示链接 +- 每个模块均有单独的文档与示例 + +### 测试资源 +- `*/src/test/resources/test-config.sample.xml` - 测试配置模板 +- 测试运行需要真实的微信 API 凭据 + +## SDK 使用模式 + +### Maven 依赖示例 +```xml + + com.github.binarywang + weixin-java-mp + 4.7.0 + +``` + +### 常见开发区域 +- **API 客户端实现**:位于 `*/service/impl/` 目录 +- **模型类**:位于 `*/bean/` 目录 +- **配置**:位于 `*/config/` 目录 +- **工具类**:位于 `weixin-java-common` 的 `*/util/` 目录 + +## 故障排查 + +### 构建问题 +- **OutOfMemoryError**:增加 Maven 内存:`export MAVEN_OPTS="-Xmx2g"` +- **编译失败**:通常为依赖问题 - 先执行 `mvn clean` +- **Checkstyle 失败**:检查 IDE 的 `.editorconfig` 设置 + +### 常见陷阱 +- **测试默认跳过**:这是正常现象 — 测试需要微信 API 凭据 +- **多模块变更**:总是在仓库根目录构建,而不是单独模块 +- **分支目标**:Pull Request 必须以 `develop` 分支为目标,而不是 `master` 或 `release` + +## 性能说明 +- **首次构建**:由于依赖下载,耗时 4-5 分钟 +- **增量构建**:通常更快(约 30-60 秒) +- **Checkstyle**:运行迅速(约 50 秒),应当经常运行 +- **IDE 性能**:项目使用 Lombok,请确保启用注解处理 + +注意:本项目为 SDK 库项目,而非可运行应用。修改应以 API 功能为主,不要改动应用级行为。 diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 0000000000..e556fa9854 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,17 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 60 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - pinned + - security +# Label to use when marking an issue as stale +staleLabel: stale +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml new file mode 100644 index 0000000000..a12c20b112 --- /dev/null +++ b/.github/workflows/maven-publish.yml @@ -0,0 +1,109 @@ +name: Publish to Maven Central +on: + push: + branches: + - develop + +permissions: + contents: write + +concurrency: + group: maven-publish-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Detect and tag release version from commit message + id: version_detect + run: | + COMMIT_MSG=$(git log -1 --pretty=%B) + VERSION="" + TAG="" + IS_RELEASE="false" + if [[ "$COMMIT_MSG" =~ ^:bookmark:\ 发布\ ([0-9]+\.[0-9]+\.[0-9]+)\.B\ 测试版本 ]]; then + BASE_VER="${BASH_REMATCH[1]}" + VERSION="${BASE_VER}.B" + TAG="v${BASE_VER}" + IS_RELEASE="true" + echo "Matched test release commit: VERSION=$VERSION, TAG=$TAG" + # 检查并打tag + if git tag | grep -q "^$TAG$"; then + echo "Tag $TAG already exists." + else + git config user.name "Binary Wang" + git config user.email "a@binarywang.com" + git tag -a "$TAG" -m "Release $TAG" + git push origin "$TAG" + echo "Tag $TAG created and pushed." + fi + elif [[ "$COMMIT_MSG" =~ ^:bookmark:\ 发布\ ([0-9]+\.[0-9]+\.[0-9]+)\ 正式版本 ]]; then + VERSION="${BASH_REMATCH[1]}" + TAG="v${VERSION}" + IS_RELEASE="true" + echo "Matched formal release commit: VERSION=$VERSION, TAG=$TAG" + # 检查并打tag + if git tag | grep -q "^$TAG$"; then + echo "Tag $TAG already exists." + else + git config user.name "Binary Wang" + git config user.email "a@binarywang.com" + git tag -a "$TAG" -m "Release $TAG" + git push origin "$TAG" + echo "Tag $TAG created and pushed." + fi + fi + echo "is_release=$IS_RELEASE" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Set up Java + uses: actions/setup-java@v4 + with: + java-version: '8' + distribution: 'temurin' + server-id: central + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE + cache: maven + + - name: Verify GPG keys + run: | + echo "Available GPG Keys:" + gpg --list-secret-keys --keyid-format LONG + + - name: Generate and set version + id: set_version + run: | + if [[ "${{ steps.version_detect.outputs.is_release }}" == "true" ]]; then + VERSION="${{ steps.version_detect.outputs.version }}" + else + git describe --tags 2>/dev/null || echo "no tag" + TIMESTAMP=$(date +'%Y%m%d.%H%M%S') + GIT_DESCRIBE=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.0.1") + VERSION="${GIT_DESCRIBE}-${TIMESTAMP}" + fi + echo "Final version: $VERSION" + echo "VERSION=$VERSION" >> $GITHUB_ENV + mvn versions:set -DnewVersion=$VERSION --no-transfer-progress + env: + TZ: Asia/Shanghai + + - name: Publish to Maven Central + run: | + mvn clean deploy -P release \ + -Dmaven.test.skip=true \ + -Dgpg.args="--batch --yes --pinentry-mode loopback" \ + --no-transfer-progress + env: + MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} + MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} diff --git a/.gitignore b/.gitignore index db0804163d..6a5b5f7519 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +.bash +.history + *.class test-output @@ -50,3 +53,4 @@ sonar-project.properties # STS .factorypath +*.zip diff --git a/.travis.yml b/.travis.yml index 2b128c8a08..99850df729 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: java - jdk: - openjdk8 script: "mvn clean package -DskipTests=true -Dcheckstyle.skip=true" @@ -15,4 +14,4 @@ cache: notifications: email: - - binarywang@vip.qq.com + - a@binarywang.com diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..d7a5b96b34 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ +## Review guidelines + +- 重点检查空指针、并发、资源释放、兼容性问题。 +- 不要只做代码风格建议,优先指出真实 bug 和回归风险。 +- 中文回复 review 结论。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 75db09403b..0b16b4779e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,8 +1,8 @@ # 代码贡献指南 -1. 首先非常欢迎和感谢对本项目发起Pull Request的同学。 -1. **特别提示:请务必在develop分支提交PR,master分支目前仅是正式版的代码,即发布正式版本后才会从develop分支进行合并。** +1. 首先非常欢迎和感谢对本项目发起 `Pull Request` 的热心小伙伴们。 +1. **特别提示:请务必在 `develop` 分支提交 `PR`,`release` 分支目前仅是正式版的代码,即发布正式版本后才会从 `develop` 分支进行合并。** 1. 本项目代码风格为使用2个空格代表一个Tab,因此在提交代码时请注意一下,否则很容易在IDE格式化代码后与原代码产生大量diff,这样会给其他人阅读代码带来极大的困扰。 -1. 为了便于设置,本项目引入editorconfig支持,请使用Eclipse的同学在贡献代码前安装相关插件,而IntelliJ IDEA新版本自带支持,如果没有可自行安装插件。 +1. 为了便于设置,本项目引入`editorconfig`支持,请使用Eclipse的同学在贡献代码前安装相关插件,而`IntelliJ IDEA`新版本自带支持,如果没有可自行安装插件。 1. **提交代码前,请检查代码是否已经格式化,并且保证新增加或者修改的方法都有完整的参数说明,而public方法必须拥有相应的单元测试并通过测试。** 1. 本项目可以采用两种方式接受代码贡献: - 第一种就是基于[Git Flow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow)开发流程,因此在发起Pull Request的时候请选择develop分支,详细步骤参考后文,推荐使用此种方式贡献代码。 @@ -24,11 +24,11 @@ $ #do some change on the content $ git commit -am "Fix issue #1: change something" $ git push ``` -* 在 GitHub 网站上提交 Pull Request。 +* 在 `GitHub` 或 `Gitee` 网站上提交 `Pull Request`。 * 定期使用项目仓库内容更新自己仓库内容。 ```bash -$ git remote add upstream https://github.com/Wechat-Group/WxJava +$ git remote add upstream https://github.com/binarywang/WxJava $ git fetch upstream $ git checkout develop $ git rebase upstream/develop diff --git a/LICENSE b/LICENSE index 0c8a80022e..7783de532a 100644 --- a/LICENSE +++ b/LICENSE @@ -37,7 +37,7 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and -If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. +If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. diff --git a/README.md b/README.md index 170fc40250..ad3e59ace7 100644 --- a/README.md +++ b/README.md @@ -1,115 +1,258 @@ -## WxJava - 微信开发 Java SDK(开发工具包) [![LICENSE](https://img.shields.io/badge/License-Anti%20996-blue.svg)](https://github.com/996icu/996.ICU/blob/master/LICENSE) [![Badge](https://img.shields.io/badge/Link-996.icu-red.svg)](https://996.icu/#/zh_CN) [![Badge](https://img.shields.io/badge/Link-京东内购福利-red.svg)](https://mp.weixin.qq.com/s/dfwatgMgARaBjh421Todyg) - -[![码云Gitee](https://gitee.com/binary/weixin-java-tools/badge/star.svg?theme=blue)](https://gitee.com/binary/weixin-java-tools) -[![Github](http://github-svg-buttons.herokuapp.com/star.svg?user=Wechat-Group&repo=WxJava&style=flat&background=1081C1)](https://github.com/Wechat-Group/WxJava) -[![GitHub release](https://img.shields.io/github/release/Wechat-Group/WxJava.svg)](https://github.com/Wechat-Group/WxJava/releases) -[![Maven Central](https://img.shields.io/maven-central/v/com.github.binarywang/wx-java.svg)](http://mvnrepository.com/artifact/com.github.binarywang/wx-java) -[![Build Status](https://travis-ci.org/Wechat-Group/WxJava.svg?branch=develop)](https://travis-ci.org/Wechat-Group/WxJava) -[![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-提供支持-blue.svg)](https://www.jetbrains.com/?from=WxJava-weixin-java-tools) +## WxJava - 微信开发 Java SDK +[![Github](https://img.shields.io/github/stars/binarywang/WxJava?logo=github&style=flat&label=Stars)](https://github.com/binarywang/WxJava) +[![Gitee](https://gitee.com/binary/weixin-java-tools/badge/star.svg?theme=blue)](https://gitee.com/binary/weixin-java-tools) +[![GitCode](https://gitcode.com/binary/WxJava/star/badge.svg)](https://gitcode.com/binary/WxJava) + +[![GitHub release](https://img.shields.io/github/release/binarywang/WxJava?label=Release)](https://github.com/binarywang/WxJava/releases) +[![Maven Central](https://img.shields.io/maven-central/v/com.github.binarywang/wx-java?label=Maven)](https://central.sonatype.com/artifact/com.github.binarywang/wx-java/versions) +[![Build Status](https://img.shields.io/circleci/project/github/binarywang/WxJava/develop.svg?sanitize=true&label=Build)](https://circleci.com/gh/binarywang/WxJava/tree/develop) +[![使用IntelliJ IDEA开发维护](https://img.shields.io/badge/IntelliJ%20IDEA-支持-blue.svg)](https://www.jetbrains.com/?from=WxJava-weixin-java-tools) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -#### 支持包括微信支付、开放平台、公众号、企业微信/企业号、小程序等微信功能的后端开发。 +
+ + Featured|HelloGitHub + + + binarywang%2FWxJava | 趋势转变 + +
+ +### 微信 `Java` 开发工具包,支持包括微信支付、开放平台、公众号、企业微信、视频号、小程序等微信功能模块的后端开发。 - - - - + +
+### 特别赞助 +
+ + + + + + + + + + + + - - - - - - -
+ + ccflow + +
+ + 计全支付Jeepay,开源支付系统 + + + + Mall4j + +
- + mp qrcode + + + 赞助商招募中 + + + ad - - - - - - - - - - - - - - - - -
+
+ + +### 目录索引 +- [快速开始(3分钟)](#快速开始3分钟) +- [我该选哪个模块?](#我该选哪个模块) +- [Maven 引用方式](#maven-引用方式) +- [最小示例](#最小示例) +- [重要信息](#重要信息) +- [其他说明](#其他说明) +- [版本说明](#版本说明) +- [应用案例](#应用案例) +- [特别赞助](#特别赞助) +- [贡献者列表](#贡献者列表) + +### 快速开始(3分钟) +1. 根据业务场景选择模块(见下方“我该选哪个模块?”) +2. 引入 Maven 依赖并选择对应模块 +3. 参考最小示例完成初始化并调用 API + +### 我该选哪个模块? + +| 业务场景 | 模块 | artifactId | +|---|---|---| +| 微信公众号开发 | MP | `weixin-java-mp` | +| 微信小程序开发 | MiniApp | `weixin-java-miniapp` | +| 微信支付 | Pay | `weixin-java-pay` | +| 企业微信 | CP | `weixin-java-cp` | +| 微信开放平台(第三方平台) | Open | `weixin-java-open` | +| 视频号 / 微信小店 | Channel | `weixin-java-channel` | + +> 移动端(iOS/Android)微信登录、分享等能力仍需集成微信官方客户端 SDK;本项目为服务端 SDK。 ### 重要信息 -1. **2019-11-25 发布 [【3.6.0正式版】](https://github.com/Wechat-Group/WxJava/releases)**! -1. 新手重要提示:本项目仅是一个SDK开发工具包,未提供Web实现,建议使用 `maven` 或 `gradle` 引用本项目即可使用本SDK提供的各种功能,详情可参考 **[【Demo项目】](demo.md)** 或本项目中的部分单元测试代码;另外微信开发新手请务必阅读[【开发文档 Wiki 首页】](https://github.com/Wechat-Group/WxJava/wiki)的常见问题部分,可以少走很多弯路,节省不少时间。 -1. 技术交流群:想获得QQ群/微信群/钉钉企业群等信息的同学,请使用微信扫描上面的微信公众号二维码关注 `WxJava` 后点击相关菜单即可获取加入方式,同时也可以在微信中搜索 `weixin-java-tools` 或 `WxJava` 后选择正确的公众号进行关注,该公众号会及时通知SDK相关更新信息,并不定期分享微信Java开发相关技术知识; -1. 付费QQ群:(**注意:刚入群会有5分钟禁言,稍等片刻即可正常发言**) [![加入QQ群](https://img.shields.io/badge/QQ群-343954419-blue.svg)](http://shang.qq.com/wpa/qunwpa?idkey=731dc3e7ea31ebe25376cc1a791445468612c63fd0e9e05399b088ec81fd9e15) 或 [![加入QQ群](https://img.shields.io/badge/QQ群-343954419-blue.svg)](http://jq.qq.com/?_wv=1027&k=40lRskK),或者请自行搜索群号`343954419`进行添加;当然由于某种原因无法入群的,可关注公众号后获取其他群的加入方式; -1. 钉钉企业群:[请点击链接申请加入](https://h5.dingtalk.com/inviteColleague/index.html#/invite/9ed100cc4a/E1DF918E32E398D191E7FE61FE0552A6) 或者 [用手机钉钉APP扫码](https://gitee.com/binary/weixin-java-tools/raw/master/images/qrcodes/ding.jpg) 申请加入。 -1. 微信开发新手或者Java开发新手在群内提问或新开Issue提问前,请先阅读[【提问的智慧】](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md),并确保已查阅过 [【开发文档Wiki】](https://github.com/wechat-group/WxJava/wiki) ,避免浪费大家的宝贵时间; -1. 寻求帮助时需贴代码或大长串异常信息的,请利用 http://paste.ubuntu.com +1. [`WxJava` 荣获 `GitCode` 2024年度十大开源社区奖项](https://mp.weixin.qq.com/s/wM_UlMsDm3IZ1CPPDvcvQw)。 +2. 项目合作洽谈请联系微信`binary0000`(在微信里自行搜索并添加好友,请注明来意,如有关于SDK问题需讨论请参考下文入群讨论,不要加此微信)。 +3. **2026-01-03 发布 [【4.8.0正式版】](https://mp.weixin.qq.com/s/mJoFtGc25pXCn3uZRh6Q-w)**! +5. 贡献源码可以参考视频:[【贡献源码全过程(上集)】](https://mp.weixin.qq.com/s/3xUZSATWwHR_gZZm207h7Q)、[【贡献源码全过程(下集)】](https://mp.weixin.qq.com/s/nyzJwVVoYSJ4hSbwyvTx9A) ,友情提供:[程序员小山与Bug](https://space.bilibili.com/473631007) +6. 新手重要提示:本项目仅是一个SDK开发工具包,未提供Web实现,建议使用 `maven` 或 `gradle` 引用本项目即可使用本SDK提供的各种功能,详情可参考 **[【Demo项目】](demo.md)** 或本项目中的部分单元测试代码; +7. 微信开发新手请务必阅读【开发文档】([Gitee Wiki](https://gitee.com/binary/weixin-java-tools/wikis/Home) 或者 [Github Wiki](https://github.com/binarywang/WxJava/wiki))的常见问题部分,可以少走很多弯路,节省不少时间。 +8. 技术交流群:想获得QQ群/微信群/钉钉企业群等信息的同学,请使用微信扫描上面的微信公众号二维码关注 `WxJava` 后点击相关菜单即可获取加入方式,同时也可以在微信中搜索 `weixin-java-tools` 或 `WxJava` 后选择正确的公众号进行关注,该公众号会及时通知SDK相关更新信息,并不定期分享微信Java开发相关技术知识; +9. 钉钉技术交流群:`32206329`(技术交流2群), `30294972`(技术交流1群,目前已满),`35724728`(通知群,实时通知Github项目变更记录)。 +10. 微信开发新手或者Java开发新手在群内提问或新开Issue提问前,请先阅读[【提问的智慧】](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md),并确保已查阅过 [【开发文档Wiki】](https://github.com/binarywang/WxJava/wiki) ,避免浪费大家的宝贵时间; +11. 寻求帮助时需贴代码或大长串异常信息的,请利用 http://paste.ubuntu.com -------------------------------- ### 其他说明 1. **阅读源码的同学请注意,本SDK为简化代码编译时加入了`lombok`支持,如果不了解`lombok`的话,请先学习下相关知识,比如可以阅读[此文章](https://mp.weixin.qq.com/s/cUc-bUcprycADfNepnSwZQ);** -1. 如有新功能需求,发现BUG,或者由于微信官方接口调整导致的代码问题,可以直接在[【Issues】](https://github.com/Wechat-Group/WxJava/issues)页提出issue,便于讨论追踪问题; -1. 如果需要贡献代码,请务必在提交PR之前先仔细阅读[【代码贡献指南】](CONTRIBUTING.md),谢谢理解配合; -1. 本SDK要求的最低JDK版本是1.7,还在使用JDK6的用户请参考[【此项目】]( https://github.com/binarywang/weixin-java-tools-for-jdk6) ,而其他更早的JDK版本则需要自己改造实现。 -1. [开源中国本项目的首页](https://www.oschina.net/p/weixin-java-tools-new),欢迎大家积极留言评分 🙂 -1. SDK开发文档请查阅 [【开发文档Wiki】](https://github.com/wechat-group/WxJava/wiki),部分文档可能未能及时更新,如有发现,可以及时上报或者自行修改。 -1. **如果本开发工具包对您有所帮助,欢迎对我们的努力进行肯定,可以直接前往[【托管于码云的项目首页】](http://gitee.com/binary/weixin-java-tools),在页尾部分找到“捐助”按钮进行打赏,多多益善 😄。非常感谢各位打赏和捐助的同学!** -1. 各个模块的Javadoc可以在线查看:[weixin-java-miniapp](http://binary.ac.cn/weixin-java-miniapp-javadoc/)、[weixin-java-pay](http://binary.ac.cn/weixin-java-pay-javadoc/)、[weixin-java-mp](http://binary.ac.cn/weixin-java-mp-javadoc/)、[weixin-java-common](http://binary.ac.cn/weixin-java-common-javadoc/)、[weixin-java-cp](http://binary.ac.cn/weixin-java-cp-javadoc/)、[weixin-java-open](http://binary.ac.cn/weixin-java-open-javadoc/) -1. 本SDK项目在以下代码托管网站同步更新: +2. 如有新功能需求,发现BUG,或者由于微信官方接口调整导致的代码问题,可以直接在[【Issues】](https://github.com/binarywang/WxJava/issues)页提出issue,便于讨论追踪问题; +3. 如果需要贡献代码,请务必在提交PR之前先仔细阅读[【代码贡献指南】](CONTRIBUTING.md),谢谢理解配合; +4. 目前本`SDK`最新版本要求的`JDK`最低版本是`8`,使用`7`的同学可以使用`WxJava` `3.8.0`及以前版本,而还在使用`JDK`6的用户请参考[【此项目】]( https://github.com/binarywang/weixin-java-tools-for-jdk6) ,而其他更早的JDK版本则需要自己改造实现。 +5. [本项目在开源中国的页面](https://www.oschina.net/p/weixin-java-tools-new),欢迎大家积极留言评分 🙂 +6. SDK开发文档请查阅 [【开发文档Wiki】](https://github.com/binarywang/WxJava/wiki),部分文档可能未能及时更新,如有发现,可以及时上报或者自行修改。 +7. **如果本开发工具包对您有所帮助,欢迎对我们的努力进行肯定,可以直接前往[【托管于码云的项目首页】](http://gitee.com/binary/weixin-java-tools),在页尾部分找到“捐助”按钮进行打赏,多多益善 😄。非常感谢各位打赏和捐助的同学!** +8. 各个模块的Javadoc可以在线查看:[weixin-java-miniapp](http://binary.ac.cn/weixin-java-miniapp-javadoc/)、[weixin-java-pay](http://binary.ac.cn/weixin-java-pay-javadoc/)、[weixin-java-mp](http://binary.ac.cn/weixin-java-mp-javadoc/)、[weixin-java-common](http://binary.ac.cn/weixin-java-common-javadoc/)、[weixin-java-cp](http://binary.ac.cn/weixin-java-cp-javadoc/)、[weixin-java-open](http://binary.ac.cn/weixin-java-open-javadoc/) +9. 本SDK项目在以下代码托管网站同步更新: * 码云:https://gitee.com/binary/weixin-java-tools -* GitHub:https://github.com/wechat-group/WxJava +* GitHub:https://github.com/binarywang/WxJava --------------------------------- ### Maven 引用方式 -注意:最新版本(包括测试版)为 [![Maven Central](https://img.shields.io/maven-central/v/com.github.binarywang/wx-java.svg)](http://mvnrepository.com/artifact/com.github.binarywang/wx-java),以下为最新正式版。 +注意:最新版本(包括测试版)为 [![Maven Central](https://img.shields.io/maven-central/v/com.github.binarywang/wx-java.svg)](https://central.sonatype.com/artifact/com.github.binarywang/wx-java/versions),以下为最新正式版。 + +#### 方式一:使用 BOM 统一管理版本(推荐) + +如果同时使用多个 WxJava 模块,推荐通过 BOM 统一管理版本,无需为每个模块单独指定版本号。 +`wx-java-bom` 从 **4.8.3.B** 版本开始提供,请使用该版本或更高版本: + +```xml + + 4.8.3.B + + + + + + com.github.binarywang + wx-java-bom + ${wx-java.version} + pom + import + + + +``` + +之后直接引入所需模块,无需指定版本: + +```xml + + com.github.binarywang + weixin-java-mp + + + com.github.binarywang + weixin-java-pay + +``` + +#### 方式二:直接引用单个模块 ```xml com.github.binarywang (不同模块参考下文) - 3.6.0 + 4.8.0 ``` - 微信小程序:`weixin-java-miniapp` - 微信支付:`weixin-java-pay` - 微信开放平台:`weixin-java-open` - - 公众号(包括订阅号和服务号):`weixin-java-mp` - - 企业号/企业微信:`weixin-java-cp` + - 微信公众号:`weixin-java-mp` + - 企业微信:`weixin-java-cp` + - 微信视频号/微信小店:`weixin-java-channel` + +**注意**: +- **移动应用开发**:如果你的移动应用(iOS/Android App)需要接入微信登录、分享等功能: + - 微信登录(网页授权):使用 `weixin-java-open` 模块,在服务端处理 OAuth 授权 + - 微信支付:使用 `weixin-java-pay` 模块 + - 客户端集成:需使用微信官方提供的移动端SDK(iOS/Android),本项目为服务端SDK +- **微信开放平台**(`weixin-java-open`)主要用于第三方平台,代公众号或小程序进行开发和管理 + +--------------------------------- +### 最小示例 + +
+公众号(MP)示例:获取 AccessToken + +```java +WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl(); +config.setAppId("your-app-id"); +config.setSecret("your-secret"); + +WxMpService wxMpService = new WxMpServiceImpl(); +wxMpService.setWxMpConfigStorage(config); + +String accessToken = wxMpService.getAccessToken(); +System.out.println(accessToken); +``` + +
+ +
+小程序(MiniApp)示例:code2Session + +```java +WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl(); +config.setAppid("your-app-id"); +config.setSecret("your-secret"); + +WxMaService wxMaService = new WxMaServiceImpl(); +wxMaService.setWxMaConfig(config); + +WxMaJscode2SessionResult result = wxMaService.getUserService().getSessionInfo("js-code"); +System.out.println(result.getOpenid()); +``` + +
+ --------------------------------- ### 版本说明
点此展开查看 -1. 本项目定为大约每两个月发布一次正式版(同时 `develop` 分支代码合并进入 `master` 分支),版本号格式为 `X.X.0`(如`2.1.0`,`2.2.0`等),遇到重大问题需修复会及时提交新版本,欢迎大家随时提交Pull Request; -1. BUG修复和新特性一般会先发布成小版本作为临时测试版本(如`3.1.8.B`,即尾号不为0,并添加B,以区别于正式版),代码仅存在于 `develop` 分支中; -1. 目前最新版本号为 [![Maven Central](https://img.shields.io/maven-central/v/com.github.binarywang/wx-java.svg)](http://mvnrepository.com/artifact/com.github.binarywang/wx-java) ,也可以通过访问链接 [【微信支付】](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.github.binarywang%22%20AND%20a%3A%22weixin-java-pay%22) 、[【微信小程序】](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.github.binarywang%22%20AND%20a%3A%22weixin-java-miniapp%22) 、[【公众号】](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.github.binarywang%22%20AND%20a%3A%22weixin-java-mp%22) 、[【企业微信】](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.github.binarywang%22%20AND%20a%3A%22weixin-java-cp%22)、[【开放平台】](http://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22com.github.binarywang%22%20AND%20a%3A%22weixin-java-open%22) -分别查看所有最新的版本。 +1. 本项目定为大约每半年左右发布一次正式版,遇到重大问题需修复会及时提交新版本,欢迎大家随时提交 `Pull Request`; +2. 每次代码更新都会自动构建出新版本方便及时尝鲜,版本号格式为 `x.x.x-时间戳`; +3. 发布正式版时,`develop` 分支代码合并进入 `release` 分支),版本号格式为 `X.X.0`(如`2.1.0`,`2.2.0`等); +4. 每隔一段时间后,会发布测试版本(如`3.6.8.B`,即尾号不为0,并添加B,以区别于正式版),代码仅存在于 `develop` 分支中; +5. 目前最新版本号为 [![Maven Central](https://img.shields.io/maven-central/v/com.github.binarywang/wx-java.svg)](http://mvnrepository.com/artifact/com.github.binarywang/wx-java) ,也可以通过访问以下链接分别查看各个模块最新的版本: +[【微信支付】](https://central.sonatype.com/artifact/com.github.binarywang/weixin-java-pay/versions) 、[【小程序】](https://central.sonatype.com/artifact/com.github.binarywang/weixin-java-miniapp/versions) 、[【公众号】](https://central.sonatype.com/artifact/com.github.binarywang/weixin-java-mp/versions) 、[【企业微信】](https://central.sonatype.com/artifact/com.github.binarywang/weixin-java-cp/versions)、[【开放平台】](https://central.sonatype.com/artifact/com.github.binarywang/weixin-java-open/versions)、[【视频号】](https://central.sonatype.com/artifact/com.github.binarywang/weixin-java-channel/versions) +
---------------------------------- -### 使用案例 -完整案例登记列表,请[【访问这里】](https://github.com/Wechat-Group/weixin-java-tools/issues/729)查看,欢迎登记更多的案例。 +### 应用案例 +完整案例登记列表,请[【访问这里】](https://github.com/binarywang/WxJava/issues/729)查看,欢迎登记更多的案例。 -以下为部分案例列表: +
+以下为节选的部分案例, 点此展开查看 #### 开源项目: - 基于微信公众号的签到、抽奖、发送弹幕程序:https://github.com/workcheng/weiya -- XxPay聚合支付:https://github.com/jmdhappy/xxpay-master +- Jeepay 支付系统:https://gitee.com/jeequan/jeepay - 微同商城:https://gitee.com/fuyang_lipengjun/platform - 微信点餐系统:https://github.com/sqmax/springboot-project - 专注批量推送的小而美的工具:https://github.com/rememberber/WePush - yshop意象商城系统:https://gitee.com/guchengwuyue/yshopmall +- wx-manage(微信公众号管理项目):https://github.com/niefy/wx-manage +- 基于若依开发的微信公众号管理系统:https://gitee.com/joolun/JooLun-wx +- SAAS微信小程序电商:https://gitee.com/wei-it/weiit-saas +- mall4j 电商商城系统:https://gitee.com/gz-yami/mall4j #### 小程序: - (京东)友家铺子,友家铺子店长版,京粉精选 @@ -120,6 +263,15 @@ - 360考试宝典 - 民医台 - 来一团商家版 +- 史必达(史丹利) +- 嘀嗒云印 +- 维沃吼吼 +- 王朝社区(比亚迪新能源社区) +- 极吼吼手机上门回收换新 +- 未来信封 +- 5G惠享 +- 生菜wordpress转小程序 +- 丽日购 #### 公众号: - 中国电信上海网厅(sh_189) @@ -131,40 +283,35 @@ - 光环云社群 - 手机排队 - [全民约跑健身便利店](http://www.oneminsport.com/) -- [洽洽食品](https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=gQFM8TwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAycDRPOXBZbVZib2UxMDAwME0wN2gAAgRIu4RbAwQAAAAA)、[洽洽合伙人](https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket=gQFP8jwAAAAAAAAAAS5odHRwOi8vd2VpeGluLnFxLmNvbS9xLzAyOUpJaU5VcXBlWTAxMDAwME0wN1oAAgSau4RbAwQAAAAA) - 民医台 - YshopMall +- 好行景区直通车以及全国40多个公众号 +- 我奥篮球公众号 +- 未来信封官微 +- 银川智云问诊 +- 5G惠享 -#### 企业号/企业微信: -- 洽洽企业号 +#### 企业微信: - HTC企业微信 +- 掌上史丹利 +- 药店益 #### 其他: - 高善人力资源 -- [小猪餐餐](http://www.xzcancan.com/) -- [餐饮系统](http://canyin.daydao.com) +- 小猪餐餐 +- 餐饮系统 - 微信公众号管理系统:http://demo.joolun.com - 锐捷网络:Saleslink +
+ ---------------------------------- ### 贡献者列表 -特别感谢参与贡献的所有同学,所有贡献者列表请在[此处](https://github.com/Wechat-Group/WxJava/graphs/contributors)查看,欢迎大家继续踊跃贡献代码! -
-点击此处展开查看贡献次数最多的几位同学 - -1. [chanjarster (Daniel Qian)](http://github.com/chanjarster) -1. [binarywang (Binary Wang)](http://github.com/binarywang) -1. [mgcnrx11](http://github.com/mgcnrx11) -1. [007gzs](http://github.com/007gzs) -1. [aimilin6688 (Jonk)](http://github.com/aimilin6688) -1. [kakotor](http://github.com/kakotor) -1. [kareanyi (MillerLin)](http://github.com/kareanyi) -1. [tianmu](http://github.com/tianmu) -1. [rememberber (周波)](http://github.com/rememberber) -1. [charmingoh (Charming)](http://github.com/charmingoh) +特别感谢参与贡献的所有同学,所有贡献者列表请在[此处](https://github.com/binarywang/WxJava/graphs/contributors)查看,欢迎大家继续踊跃贡献代码! -
+ + + ### GitHub Stargazers over time - -[![Stargazers over time](https://starchart.cc/Wechat-Group/WxJava.svg)](https://starchart.cc/Wechat-Group/WxJava) +[![Star History Chart](https://api.star-history.com/svg?repos=binarywang/WxJava&type=Date)](https://star-history.com/#binarywang/WxJava&Date) diff --git a/demo.md b/demo.md index f779c53341..d305fc2121 100644 --- a/demo.md +++ b/demo.md @@ -1,5 +1,5 @@ - ## `Demo` 项目 + ### 说明 1. 在码云和 `GitHub` 上均可访问,会尽量保持同步,请根据自己情况选用。 1. 一般来说,`Github`上的版本应该是最新的,但也有可能没及时同步,此种情况下请以 `Github` 上的版本为准,有问题也请在 `Github` 对应项目 `Issues` 页面提问)。 @@ -7,15 +7,18 @@ ### `Spring Boot Starter` 实现 - 微信支付:[点击查看使用方法](https://github.com/Wechat-Group/WxJava/tree/master/spring-boot-starters/wx-java-pay-spring-boot-starter) + - [使用该 `starter` 实现的微信支付`Demo`](https://github.com/binarywang/wx-java-pay-demo) - 微信公众号:[点击查看使用方法](https://github.com/Wechat-Group/WxJava/tree/master/spring-boot-starters/wx-java-mp-spring-boot-starter) + - [使用该 `starter` 实现的公众号 `Demo`](https://github.com/binarywang/wx-java-mp-demo) - 微信小程序:[点击查看使用方法](https://github.com/Wechat-Group/WxJava/tree/master/spring-boot-starters/wx-java-miniapp-spring-boot-starter) + - [使用该 `starter` 实现的小程序 `Demo`](https://github.com/binarywang/wx-java-miniapp-demo) ### Demo 列表 1. 微信支付 Demo:[GitHub](http://github.com/binarywang/weixin-java-pay-demo)、[码云](http://gitee.com/binary/weixin-java-pay-demo) -1. 企业号/企业微信 Demo:[GitHub](http://github.com/binarywang/weixin-java-cp-demo)、[码云](http://gitee.com/binary/weixin-java-cp-demo) +1. 企业号/企业微信 Demo:[GitHub](http://github.com/binarywang/weixin-java-cp-demo)、[码云](http://gitee.com/binary/weixin-java-cp-demo) 1. 微信小程序 Demo:[GitHub](http://github.com/binarywang/weixin-java-miniapp-demo)、[码云](http://gitee.com/binary/weixin-java-miniapp-demo) -1. 开放平台 Demo:[GitHub](http://github.com/Wechat-Group/weixin-java-open-demo)、[码云](http://gitee.com/binary/weixin-java-open-demo) -1. 公众号 Demo: +1. 开放平台 Demo:[GitHub](http://github.com/Wechat-Group/weixin-java-open-demo)、[码云](http://gitee.com/binary/weixin-java-open-demo) +1. 微信公众号 Demo: - 使用 `Spring MVC` 实现的公众号 Demo:[GitHub](http://github.com/binarywang/weixin-java-mp-demo-springmvc)、[码云](https://gitee.com/binary/weixin-java-mp-demo) - - 使用 `Spring Boot` 实现的公众号 Demo(支持多公众号):[GitHub](http://github.com/binarywang/weixin-java-mp-demo-springboot)、[码云](http://gitee.com/binary/weixin-java-mp-demo-springboot) - - 含公众号和部分微信支付代码的 Demo:[GitHub](http://github.com/Wechat-Group/weixin-java-springmvc)、[码云](http://gitee.com/binary/weixin-java-tools-springmvc) + - 使用 `Spring Boot` 实现的公众号 Demo(支持多公众号):[GitHub](http://github.com/binarywang/weixin-java-mp-demo)、[码云](http://gitee.com/binary/weixin-java-mp-demo-springboot) + - 含公众号和部分微信支付代码的 Demo:[GitHub](http://github.com/Wechat-Group/weixin-java-demo-springmvc)、[码云](http://gitee.com/binary/weixin-java-tools-springmvc) diff --git a/docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md b/docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md new file mode 100644 index 0000000000..b64e4612b9 --- /dev/null +++ b/docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md @@ -0,0 +1,297 @@ +# 企业微信会话存档SDK安全使用指南 + +## 说明 +该方案已废弃,请使用新版本:[CP_MSG_AUDIT_THREADLOCAL_LIFECYCLE_REFACTOR.md](CP_MSG_AUDIT_THREADLOCAL_LIFECYCLE_REFACTOR.md) +## 问题背景 + +在使用企业微信会话存档功能时,部分开发者遇到了JVM崩溃的问题。典型错误信息如下: + +``` +SIGSEGV (0xb) at pc=0x00007fcd50460d93 +Problematic frame: +C [libWeWorkFinanceSdk_Java.so+0x260d93] WeWorkFinanceSdk::TryRefresh(std::string const&, std::string const&, int)+0x23 +``` + +## 问题原因 + +旧版API设计存在以下问题: + +1. **SDK生命周期管理混乱** + - `getChatDatas()` 方法会返回SDK实例给调用方 + - 开发者需要手动调用 `Finance.DestroySdk()` 来销毁SDK + - 但SDK在框架内部有7200秒的缓存机制 + +2. **手动销毁导致缓存失效** + - 当开发者手动销毁SDK后,框架缓存中的SDK引用变为无效 + - 后续调用(如 `getMediaFile()`)仍然使用已销毁的SDK + - 底层C++库访问无效指针,导致SIGSEGV错误 + +3. **多线程并发问题** + - 在多线程环境下,一个线程销毁SDK后 + - 其他线程仍在使用该SDK,导致崩溃 + +## 解决方案 + +从 **4.8.0** 版本开始,WxJava提供了新的安全API,完全由框架管理SDK生命周期。 + +### 新API列表 + +| 旧API(已废弃) | 新API(推荐使用) | 说明 | +|----------------|------------------|------| +| `getChatDatas()` | `getChatRecords()` | 拉取聊天记录,不暴露SDK | +| `getDecryptData(sdk, ...)` | `getDecryptChatData(...)` | 解密聊天数据,无需传入SDK | +| `getChatPlainText(sdk, ...)` | `getChatRecordPlainText(...)` | 获取明文数据,无需传入SDK | +| `getMediaFile(sdk, ...)` | `downloadMediaFile(...)` | 下载媒体文件,无需传入SDK | + +### 使用示例 + +#### 错误用法(旧API,已废弃) + +```java +// ❌ 不推荐:容易导致JVM崩溃 +WxCpMsgAuditService msgAuditService = wxCpService.getMsgAuditService(); + +// 拉取聊天记录 +WxCpChatDatas chatDatas = msgAuditService.getChatDatas(seq, 1000L, null, null, 1000L); + +for (WxCpChatDatas.WxCpChatData chatData : chatDatas.getChatData()) { + // 解密数据 + WxCpChatModel model = msgAuditService.getDecryptData(chatDatas.getSdk(), chatData, 2); + + // 下载媒体文件 + if ("image".equals(model.getMsgType())) { + String sdkFileId = model.getImage().getSdkFileId(); + msgAuditService.getMediaFile(chatDatas.getSdk(), sdkFileId, null, null, 1000L, targetPath); + } +} + +// ❌ 危险操作:手动销毁SDK可能导致后续调用崩溃 +Finance.DestroySdk(chatDatas.getSdk()); +``` + +#### 正确用法(新API,推荐) + +```java +// ✅ 推荐:SDK生命周期由框架自动管理,安全可靠 +WxCpMsgAuditService msgAuditService = wxCpService.getMsgAuditService(); + +// 拉取聊天记录(不返回SDK) +List chatRecords = + msgAuditService.getChatRecords(seq, 1000L, null, null, 1000L); + +for (WxCpChatDatas.WxCpChatData chatData : chatRecords) { + // 解密数据(无需传入SDK) + WxCpChatModel model = msgAuditService.getDecryptChatData(chatData, 2); + + // 下载媒体文件(无需传入SDK) + if ("image".equals(model.getMsgType())) { + String sdkFileId = model.getImage().getSdkFileId(); + msgAuditService.downloadMediaFile(sdkFileId, null, null, 1000L, targetPath); + } +} + +// ✅ 无需手动销毁SDK,框架会自动管理 +``` + +### 完整示例:拉取并处理会话存档 + +```java +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.api.WxCpMsgAuditService; +import me.chanjar.weixin.cp.bean.msgaudit.WxCpChatDatas; +import me.chanjar.weixin.cp.bean.msgaudit.WxCpChatModel; +import me.chanjar.weixin.cp.constant.WxCpConsts; + +import java.util.List; + +public class MsgAuditExample { + + private final WxCpService wxCpService; + + public void processMessages(long seq) throws Exception { + WxCpMsgAuditService msgAuditService = wxCpService.getMsgAuditService(); + + // 拉取聊天记录 + List chatRecords = + msgAuditService.getChatRecords(seq, 1000L, null, null, 1000L); + + for (WxCpChatDatas.WxCpChatData chatData : chatRecords) { + seq = chatData.getSeq(); + + // 获取明文数据 + String plainText = msgAuditService.getChatRecordPlainText(chatData, 2); + WxCpChatModel model = WxCpChatModel.fromJson(plainText); + + // 处理不同类型的消息 + switch (model.getMsgType()) { + case WxCpConsts.MsgAuditMediaType.TEXT: + processTextMessage(model); + break; + + case WxCpConsts.MsgAuditMediaType.IMAGE: + processImageMessage(model, msgAuditService); + break; + + case WxCpConsts.MsgAuditMediaType.FILE: + processFileMessage(model, msgAuditService); + break; + + default: + // 处理其他类型消息 + break; + } + } + } + + private void processTextMessage(WxCpChatModel model) { + String content = model.getText().getContent(); + System.out.println("文本消息:" + content); + } + + private void processImageMessage(WxCpChatModel model, WxCpMsgAuditService msgAuditService) + throws Exception { + String sdkFileId = model.getImage().getSdkFileId(); + String md5Sum = model.getImage().getMd5Sum(); + String targetPath = "/path/to/save/" + md5Sum + ".jpg"; + + // 下载图片(无需传入SDK) + msgAuditService.downloadMediaFile(sdkFileId, null, null, 1000L, targetPath); + System.out.println("图片已保存:" + targetPath); + } + + private void processFileMessage(WxCpChatModel model, WxCpMsgAuditService msgAuditService) + throws Exception { + String sdkFileId = model.getFile().getSdkFileId(); + String fileName = model.getFile().getFileName(); + String targetPath = "/path/to/save/" + fileName; + + // 下载文件(无需传入SDK) + msgAuditService.downloadMediaFile(sdkFileId, null, null, 1000L, targetPath); + System.out.println("文件已保存:" + targetPath); + } +} +``` + +### 使用Lambda处理媒体文件流 + +新API同样支持使用Lambda表达式处理媒体文件的数据流: + +```java +msgAuditService.downloadMediaFile(sdkFileId, null, null, 1000L, data -> { + try { + // 处理每个数据分片(大文件会分片传输) + // 例如:上传到云存储、写入数据库等 + uploadToCloud(data); + } catch (Exception e) { + e.printStackTrace(); + } +}); +``` + +## 技术实现原理 + +### 引用计数机制 + +新API在内部实现了SDK引用计数机制: + +1. **获取SDK时**:引用计数 +1 +2. **使用完成后**:引用计数 -1 +3. **计数归零时**:SDK被自动释放 + +```java +// 框架内部实现(简化版) +public void downloadMediaFile(String sdkFileId, ...) { + long sdk = initSdk(); // 获取或初始化SDK + configStorage.incrementMsgAuditSdkRefCount(sdk); // 引用计数 +1 + + try { + // 执行实际操作 + getMediaFile(sdk, sdkFileId, ...); + } finally { + // 确保引用计数一定会减少 + configStorage.decrementMsgAuditSdkRefCount(sdk); // 引用计数 -1 + } +} +``` + +### SDK缓存机制 + +SDK初始化后会缓存7200秒(企业微信官方文档规定),避免频繁初始化: + +- **首次调用**:初始化新的SDK +- **7200秒内**:复用缓存的SDK +- **超过7200秒**:重新初始化SDK + +新API的引用计数机制与缓存机制完美配合,确保SDK不会被提前销毁。 + +## 迁移指南 + +### 第一步:使用新API替换旧API + +查找代码中的旧API调用: + +```java +// 查找模式 +getChatDatas( +getDecryptData(.*sdk +getChatPlainText(.*sdk +getMediaFile(.*sdk +Finance.DestroySdk( +``` + +替换为对应的新API(参考前面的对照表)。 + +### 第二步:移除手动SDK管理代码 + +删除所有 `Finance.DestroySdk()` 调用,SDK生命周期由框架自动管理。 + +### 第三步:测试验证 + +1. 在测试环境验证新API功能正常 +2. 观察日志,确认没有SDK相关的错误 +3. 进行压力测试,验证多线程环境下的稳定性 + +## 常见问题 + +### Q1: 旧代码会立即停止工作吗? + +**A:** 不会。旧API被标记为 `@Deprecated`,但仍然可用,只是不推荐继续使用。建议尽快迁移到新API以避免潜在问题。 + +### Q2: 如何知道SDK是否被正确释放? + +**A:** 框架会自动管理SDK生命周期,开发者无需关心。如果需要调试,可以查看配置存储中的引用计数。 + +### Q3: 多线程环境下新API安全吗? + +**A:** 是的。新API使用了引用计数机制,配合 `synchronized` 关键字,确保多线程环境下的安全性。 + +### Q4: 性能会受影响吗? + +**A:** 不会。新API在实现上增加了引用计数的开销,但这是轻量级的操作(原子操作),对性能影响可以忽略不计。SDK缓存机制保持不变。 + +### Q5: 可以同时使用新旧API吗? + +**A:** 技术上可以,但强烈不推荐。混用可能导致SDK生命周期管理混乱,建议统一使用新API。 + +## 相关链接 + +- [企业微信会话存档官方文档](https://developer.work.weixin.qq.com/document/path/91360) +- [WxJava GitHub 仓库](https://github.com/binarywang/WxJava) +- [问题反馈](https://github.com/binarywang/WxJava/issues) + +## 版本要求 + +- **最低版本**: 4.8.0 +- **推荐版本**: 最新版本 + +## 反馈与支持 + +如果在使用过程中遇到问题,请: + +1. 查看本文档的常见问题部分 +2. 在 GitHub 上提交 Issue +3. 加入微信群获取社区支持 + +--- + +**最后更新时间**: 2026-01-14 diff --git a/docs/CP_MSG_AUDIT_THREADLOCAL_LIFECYCLE_REFACTOR.md b/docs/CP_MSG_AUDIT_THREADLOCAL_LIFECYCLE_REFACTOR.md new file mode 100644 index 0000000000..072ceefd0c --- /dev/null +++ b/docs/CP_MSG_AUDIT_THREADLOCAL_LIFECYCLE_REFACTOR.md @@ -0,0 +1,204 @@ +# 会话存档SDK生命周期重构方案 + +## Context + +当前实现(4.8.x)通过"共享SDK + 引用计数 + 7200秒过期"来管理会话存档SDK生命周期。 +该方案存在以下核心问题: + +1. **频繁初始化/销毁**:每次调用 `releaseSdk()` 后引用计数归零即销毁SDK。对于"拉取→解密→下载媒体"这类典型串行调用链,每步操作都会触发重新初始化。 +2. **7200秒过期规则无依据**:官方文档FAQ明确说"不需要每次new/init sdk,可以在多次拉取中复用同一个sdk",无任何7200秒过期说明。 +3. **线程安全问题**:企微技术人员建议"一个线程一个SDK实例",当前设计多线程共享同一SDK实例,存在并发安全隐患。 + +--- + +## 推荐方案:ThreadLocal SDK 模式 + +> **核心原则**:每个线程拥有独立SDK实例,懒初始化,生命周期与线程绑定。 + +### 设计要点 + +- 使用 `ThreadLocal` 为每个线程持有独立SDK +- SDK在线程首次调用时初始化,后续所有操作复用(无需重复初始化) +- 移除7200秒过期机制 +- 移除引用计数机制(每线程独占,无需计数) +- 提供显式清理接口:`closeThreadLocalSdk()`(线程结束时调)、`closeAllSdks()`(应用关闭时调) + +### 生命周期示意 + +``` +Thread A: init SDK_A → getChatRecords → getDecryptChatData → downloadMediaFile → [任务结束后调closeThreadLocalSdk] +Thread B: init SDK_B → getChatRecords → getDecryptChatData → downloadMediaFile → ... +Thread C: init SDK_C → ... +``` + +--- + +## 涉及文件 + +| 文件 | 变更类型 | +|------|--------| +| `weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java` | 主要重构 | +| `weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java` | 新增接口方法 | +| `weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java` | 废弃旧SDK管理方法 | +| `weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java` | 废弃旧字段/方法 | +| `weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpMsgAuditTest.java` | 补充测试 | +| `docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md` | 更新文档 | + +--- + +## 详细变更 + +### 1. WxCpMsgAuditServiceImpl(主要变更) + +**新增字段:** +```java +/** 每个线程持有独立SDK实例 */ +private final ThreadLocal threadLocalSdk = new ThreadLocal<>(); + +/** 跟踪所有已创建SDK,用于统一清理 */ +private final Set managedSdks = ConcurrentHashMap.newKeySet(); +``` + +**废弃字段/方法:** +- 废弃常量 `SDK_EXPIRES_TIME = 7200`(无官方依据) +- 废弃 `initSdk()`(由 `getOrInitThreadLocalSdk()` 替代) +- 废弃 `acquireSdk()` / `releaseSdk()`(由ThreadLocal模式替代) + +**新增核心方法:** + +```java +/** + * 获取当前线程的SDK,不存在则创建。SDK在线程内跨调用复用,无需每次重新初始化。 + */ +private long getOrInitThreadLocalSdk() throws WxErrorException { + Long sdk = threadLocalSdk.get(); + if (sdk != null && sdk > 0) { + return sdk; + } + long newSdk = createSdk(); + threadLocalSdk.set(newSdk); + managedSdks.add(newSdk); + log.info("线程 [{}] 初始化会话存档SDK成功,sdk={}", Thread.currentThread().getName(), newSdk); + return newSdk; +} + +/** + * 创建并初始化一个新SDK(私有,只在当前线程无SDK时调用) + */ +private long createSdk() throws WxErrorException { + WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage(); + // ... 与现有 initSdk() 内的库加载+Finance.NewSdk()+Finance.Init() 逻辑一致 ... + // 注意:Finance.loadingLibraries() 是幂等的(System.load内部防重复),多线程调用安全 +} + +/** + * 关闭当前线程持有的SDK,释放本地资源。 + * 在线程任务结束时调用(如定时任务finally块,或线程池线程销毁时)。 + */ +public void closeThreadLocalSdk() { + Long sdk = threadLocalSdk.get(); + if (sdk != null && sdk > 0) { + Finance.DestroySdk(sdk); + managedSdks.remove(sdk); + threadLocalSdk.remove(); + log.info("线程 [{}] 关闭会话存档SDK,sdk={}", Thread.currentThread().getName(), sdk); + } +} + +/** + * 关闭所有线程持有的SDK。应用关闭时调用(如Spring @PreDestroy / Shutdown Hook)。 + */ +public void closeAllSdks() { + managedSdks.forEach(sdk -> { + Finance.DestroySdk(sdk); + log.info("关闭会话存档SDK,sdk={}", sdk); + }); + managedSdks.clear(); + threadLocalSdk.remove(); +} +``` + +**更新新API方法(getChatRecords / getDecryptChatData / getChatRecordPlainText / downloadMediaFile):** +- 调用 `getOrInitThreadLocalSdk()` 替代 `acquireSdk()` +- 移除 try-finally 中的 `releaseSdk(sdk)` 调用(SDK不再每次释放) +- 方法变得更简洁:直接使用sdk,无需包装计数 + +**保留旧API方法不变(getChatDatas / getDecryptData / getChatPlainText / getMediaFile):** +- 保持 @Deprecated 标注 +- 内部调用改为 `getOrInitThreadLocalSdk()` 以保持一致性(旧方法也受益于ThreadLocal) +- 移除对 `initSdk()` 的依赖 + +### 2. WxCpMsgAuditService(接口新增) + +```java +/** + * 关闭当前线程持有的SDK,释放native资源。 + * Finance.DestroySdk() 不会随线程结束自动执行,无论线程池还是独立线程, + * 均应在任务结束的finally块中调用本方法,防止native内存、连接等资源泄漏。 + */ +void closeThreadLocalSdk(); + +/** + * 关闭所有会话存档SDK实例。 + * 适用于应用关闭时(如Spring Bean销毁阶段)统一释放资源。 + */ +void closeAllSdks(); +``` + +### 3. WxCpConfigStorage(废弃旧SDK管理API) + +对以下方法标记 `@Deprecated`(保留实现,不做破坏性删除): +- `getMsgAuditSdk()` / `updateMsgAuditSdk()` / `expireMsgAuditSdk()` / `isMsgAuditSdkExpired()` +- `acquireMsgAuditSdk()` / `releaseMsgAuditSdk()` +- `incrementMsgAuditSdkRefCount()` / `decrementMsgAuditSdkRefCount()` / `getMsgAuditSdkRefCount()` + +### 4. WxCpDefaultConfigImpl(废弃旧字段) + +- 将 `msgAuditSdk`、`msgAuditSdkExpiresTime`、`msgAuditSdkRefCount` 字段标记 `@Deprecated` +- 对应的 getter/setter/acquire/release 方法标记 `@Deprecated` +- 保留实现,确保向后兼容 + +--- + +## 使用示例(更新文档) + +```java +// ✅ 典型用法(一次任务中串行调用,SDK在同线程内复用,无重复初始化) +WxCpMsgAuditService msgAuditService = wxCpService.getMsgAuditService(); + +try { + List records = msgAuditService.getChatRecords(seq, 100L, null, null, 30L); + for (WxCpChatDatas.WxCpChatData record : records) { + WxCpChatModel model = msgAuditService.getDecryptChatData(record, 2); + if ("image".equals(model.getMsgType())) { + msgAuditService.downloadMediaFile(model.getImage().getSdkFileId(), null, null, 30L, "/tmp/img.jpg"); + } + } +} finally { + // 无论线程池还是独立线程,均建议在 finally 中显式调用。 + // Finance.DestroySdk() 不会随线程结束自动执行,依赖 closeAllSdks() 兜底会造成 + // native 内存/连接资源的延迟泄漏,对定时任务等长期运行场景尤其有害。 + msgAuditService.closeThreadLocalSdk(); +} + +// 应用关闭时(Spring @PreDestroy 或 Shutdown Hook) +// msgAuditService.closeAllSdks(); +``` + +--- + +## 注意事项 + +1. **线程池场景下必须调用 `closeThreadLocalSdk()`**:线程池中线程会被复用,如不主动清理,下次任务仍会使用旧线程的SDK。对于计划任务/批处理,建议在 finally 块中调用。 +2. **独立线程同样建议显式关闭**:`Finance.DestroySdk()` 是 native 调用,不会随线程结束自动执行,JVM GC 也不会触发它。依赖 `closeAllSdks()` 兜底意味着 native 内存、网络连接等资源在整个应用运行期间一直持有,对定时任务等高频场景会持续积累,建议统一在 finally 块中调用 `closeThreadLocalSdk()`。 +3. **多企业(多CorpId)场景**:`threadLocalSdk` 是实例字段(非static),不同 `WxCpMsgAuditServiceImpl` 实例(不同企业)的ThreadLocal独立,互不影响。 +4. **库加载幂等性**:`Finance.loadingLibraries()` 底层调用 `System.load()`,JVM保证同一库不重复加载,多线程并发调用安全。 + +--- + +## 验证方式 + +1. **单元测试**:在 `WxCpMsgAuditTest` 中添加测试,验证同线程多次调用不触发重新初始化(可通过日志或mock Finance验证) +2. **多线程压测**:多线程并发调用 `getChatRecords` + `getDecryptChatData`,观察无JVM崩溃 +3. **线程池复用测试**:使用固定线程池多次提交任务,验证 `closeThreadLocalSdk()` 后下次任务能正确重新初始化SDK +4. **应用关闭测试**:调用 `closeAllSdks()`,验证所有线程的SDK被正确销毁 diff --git a/docs/CommonUploadParam-FormFields-Usage.md b/docs/CommonUploadParam-FormFields-Usage.md new file mode 100644 index 0000000000..2d95d7c5c9 --- /dev/null +++ b/docs/CommonUploadParam-FormFields-Usage.md @@ -0,0 +1,169 @@ +# CommonUploadParam 额外表单字段功能使用示例 + +## 背景 + +微信公众号在上传永久视频素材时,需要在POST请求中同时提交文件和一个名为`description`的表单字段,该字段包含视频的描述信息(JSON格式)。 + +根据微信公众号文档: +> 在上传视频素材时需要POST另一个表单,id为description,包含素材的描述信息,内容格式为JSON,格式如下: +> ```json +> { +> "title": "VIDEO_TITLE", +> "introduction": "INTRODUCTION" +> } +> ``` + +## 解决方案 + +`CommonUploadParam` 类已经扩展支持额外的表单字段,可以在上传文件的同时提交其他表单数据。 + +## 使用示例 + +### 1. 基本用法 - 上传永久视频素材 + +```java +import me.chanjar.weixin.common.bean.CommonUploadParam; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; +import me.chanjar.weixin.mp.api.WxMpService; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +public class VideoMaterialUploadExample { + + public void uploadVideoMaterial(WxMpService wxMpService) throws Exception { + // 准备视频文件 + File videoFile = new File("/path/to/video.mp4"); + + // 创建上传参数 + CommonUploadParam uploadParam = CommonUploadParam.fromFile("media", videoFile); + + // 准备视频描述信息(JSON格式) + Map description = new HashMap<>(); + description.put("title", "我的视频标题"); + description.put("introduction", "这是一个精彩的视频介绍"); + String descriptionJson = WxGsonBuilder.create().toJson(description); + + // 添加description表单字段 + uploadParam.addFormField("description", descriptionJson); + + // 调用微信API上传 + String url = "https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=ACCESS_TOKEN&type=video"; + String response = wxMpService.upload(url, uploadParam); + + System.out.println("上传成功:" + response); + } +} +``` + +### 2. 链式调用风格 + +```java +import me.chanjar.weixin.common.bean.CommonUploadParam; +import com.google.gson.JsonObject; + +public class ChainStyleExample { + + public void uploadWithChainStyle(WxMpService wxMpService) throws Exception { + File videoFile = new File("/path/to/video.mp4"); + + // 准备描述信息 + JsonObject description = new JsonObject(); + description.addProperty("title", "视频标题"); + description.addProperty("introduction", "视频介绍"); + + // 使用链式调用 + String response = wxMpService.upload( + "https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=ACCESS_TOKEN&type=video", + CommonUploadParam.fromFile("media", videoFile) + .addFormField("description", description.toString()) + ); + + System.out.println("上传成功:" + response); + } +} +``` + +### 3. 多个额外表单字段 + +```java +import me.chanjar.weixin.common.bean.CommonUploadParam; + +public class MultipleFormFieldsExample { + + public void uploadWithMultipleFields(WxMpService wxMpService) throws Exception { + File file = new File("/path/to/file.jpg"); + + // 可以添加多个表单字段 + CommonUploadParam uploadParam = CommonUploadParam.fromFile("media", file) + .addFormField("field1", "value1") + .addFormField("field2", "value2") + .addFormField("field3", "value3"); + + String response = wxMpService.upload("https://api.weixin.qq.com/some/upload/url", uploadParam); + + System.out.println("上传成功:" + response); + } +} +``` + +### 4. 从字节数组上传并添加表单字段 + +```java +import me.chanjar.weixin.common.bean.CommonUploadParam; + +public class ByteArrayUploadExample { + + public void uploadFromBytes(WxMpService wxMpService) throws Exception { + // 从字节数组创建上传参数 + byte[] fileBytes = getFileBytes(); + + CommonUploadParam uploadParam = CommonUploadParam + .fromBytes("media", "video.mp4", fileBytes) + .addFormField("description", "{\"title\":\"标题\",\"introduction\":\"介绍\"}"); + + String response = wxMpService.upload( + "https://api.weixin.qq.com/cgi-bin/material/add_material?access_token=ACCESS_TOKEN&type=video", + uploadParam + ); + + System.out.println("上传成功:" + response); + } + + private byte[] getFileBytes() { + // 获取文件字节数组的逻辑 + return new byte[0]; + } +} +``` + +## API 说明 + +### CommonUploadParam 类 + +#### 构造方法 +- `fromFile(String name, File file)` - 从文件创建上传参数 +- `fromBytes(String name, String fileName, byte[] bytes)` - 从字节数组创建上传参数 + +#### 方法 +- `addFormField(String fieldName, String fieldValue)` - 添加额外的表单字段,返回当前对象支持链式调用 +- `getFormFields()` - 获取所有额外的表单字段(Map类型) +- `setFormFields(Map formFields)` - 设置额外的表单字段 + +#### 属性 +- `name` - 文件对应的接口参数名称(如:media) +- `data` - 上传数据(CommonUploadData对象) +- `formFields` - 额外的表单字段(可选,Map类型) + +## 注意事项 + +1. **表单字段是可选的**:如果不需要额外的表单字段,可以不调用`addFormField`方法 +2. **JSON格式**:对于需要JSON格式的表单字段(如description),需要先将对象转换为JSON字符串 +3. **编码**:表单字段值会使用UTF-8编码 +4. **所有HTTP客户端支持**:该功能在所有HTTP客户端实现中都得到支持(OkHttp、Apache HttpClient、HttpComponents、JoddHttp) + +## 兼容性 + +- 对于通过 `fromFile`、`fromBytes` 等工厂方法创建 `CommonUploadParam` 的代码,本功能在行为层面是向后兼容的,现有代码无需修改即可继续工作。 +- 如果之前直接使用构造函数(例如 `new CommonUploadParam(name, data)`)创建对象,由于新增了 `formFields` 字段,构造函数签名可能发生变化,升级后需要改为使用上述工厂方法或根据新构造函数签名调整代码。 diff --git a/docs/HTTPCLIENT_UPGRADE_GUIDE.md b/docs/HTTPCLIENT_UPGRADE_GUIDE.md new file mode 100644 index 0000000000..5cabb10674 --- /dev/null +++ b/docs/HTTPCLIENT_UPGRADE_GUIDE.md @@ -0,0 +1,199 @@ +# HttpClient 升级指南 + +## 概述 + +从 WxJava 4.7.x 版本开始,项目开始支持并推荐使用 **Apache HttpClient 5.x**(HttpComponents Client 5),同时保持对 HttpClient 4.x 的向后兼容。 + +## 为什么升级? + +1. **Apache HttpClient 5.x 是最新稳定版本**:提供更好的性能和更多的功能 +2. **HttpClient 4.x 已经进入维护模式**:不再积极开发新功能 +3. **更好的安全性**:HttpClient 5.x 包含最新的安全更新和改进 +4. **向前兼容**:为未来的开发做好准备 + +## 支持的 HTTP 客户端 + +| HTTP 客户端 | 版本 | 配置值 | 状态 | 说明 | +|------------|------|--------|------|------| +| Apache HttpClient 5.x | 5.5 | `HttpComponents` | ⭐ 推荐 | 最新稳定版本 | +| Apache HttpClient 4.x | 4.5.13 | `HttpClient` | ✅ 支持 | 向后兼容 | +| OkHttp | 4.12.0 | `OkHttp` | ✅ 支持 | 需自行添加依赖 | +| Jodd-http | 6.3.0 | `JoddHttp` | ✅ 支持 | 需自行添加依赖 | + +## 模块支持情况 + +| 模块 | HttpClient 5.x 支持 | 默认客户端 | +|------|-------------------|-----------| +| weixin-java-mp(公众号) | ✅ 是 | HttpComponents (5.x) | +| weixin-java-cp(企业微信) | ⚠️ 视集成方式而定 | 参考对应 starter 配置 | +| weixin-java-channel(视频号) | ✅ 是 | HttpComponents (5.x) | +| weixin-java-qidian(企点) | ✅ 是 | HttpComponents (5.x) | +| weixin-java-miniapp(小程序) | ✅ 是 | HttpComponents (5.x) | +| weixin-java-pay(支付) | ✅ 是 | HttpComponents (5.x) | +| weixin-java-open(开放平台) | ✅ 是 | HttpComponents (5.x) | + +**注意**: +- **weixin-java-cp 模块**的支持情况取决于具体使用的 Starter 版本,请参考对应模块文档。 + +## 对现有项目的影响 + +### 对新项目 +- **无需任何修改**,直接使用最新版本即可 +- 支持 HttpClient 5.x 的模块会自动使用 HttpComponents (5.x) + +### 对现有项目 +- **向后兼容**:不需要修改任何代码 +- 如果希望继续使用 HttpClient 4.x,只需在配置中显式指定,pay 模块会自动包含 httpclient4 依赖(因为某些接口必须使用 httpclient4) + 其他模块(mp、miniapp、cp、open、channel、qidian)如果需要使用 httpclient4,必须显式在项目中添加 httpclient4 依赖 + +## 迁移步骤 + +### 1. 更新 WxJava 版本 + +在 `pom.xml` 中更新版本: + +```xml + + com.github.binarywang + weixin-java-mp + 最新版本 + +``` + +### 2. 检查配置(可选) + +#### Spring Boot 项目 + +在 `application.properties` 或 `application.yml` 中: + +```properties +# 使用 HttpClient 5.x(推荐,无需配置,已经是默认值) +wx.mp.config-storage.http-client-type=HttpComponents + +# 或者继续使用 HttpClient 4.x +wx.mp.config-storage.http-client-type=HttpClient +``` + +#### 纯 Java 项目 + +```java +// 使用 HttpClient 5.x(推荐) +WxMpService wxMpService = new WxMpServiceHttpComponentsImpl(); + +// 或者继续使用 HttpClient 4.x +WxMpService wxMpService = new WxMpServiceHttpClientImpl(); +``` + +### 3. 测试应用 + +升级后,建议进行全面测试以确保一切正常工作。 + +## 常见问题 + +### Q: 升级后会不会破坏现有代码? +A: 不会。项目保持完全向后兼容,HttpClient 4.x 的所有实现都保持不变。 + +### Q: 我需要修改代码吗? +A: 大多数情况下不需要。如果希望继续使用 HttpClient 4.x,只需在配置中指定 `http-client-type=HttpClient` ,并引入 HttpClient 4.x 依赖即可。 + +### Q: 我可以在同一个项目中同时使用两个版本吗? +A: 可以。不同的模块可以配置使用不同的 HTTP 客户端。例如,MP 模块使用 HttpClient 5.x,pay 模块部分接口仍使用 HttpClient 4.x,但也可以按需配置为 HttpClient 5.x。 + +### Q: 如何排除不需要的依赖? +A: 如果只想使用一个版本,可以在 `pom.xml` 中排除另一个: + +```xml + + com.github.binarywang + weixin-java-mp + 最新版本 + + + + org.apache.httpcomponents + httpclient + + + org.apache.httpcomponents + httpmime + + + +``` + +## 配置参考 + +### Spring Boot 完整配置示例 + +```properties +# 公众号配置 +wx.mp.app-id=your_app_id +wx.mp.secret=your_secret +wx.mp.token=your_token +wx.mp.aes-key=your_aes_key + +# HTTP 客户端配置 +wx.mp.config-storage.http-client-type=HttpComponents # HttpComponents, HttpClient, OkHttp, JoddHttp + +# HTTP 代理配置(可选) +wx.mp.config-storage.http-proxy-host=proxy.example.com +wx.mp.config-storage.http-proxy-port=8080 +wx.mp.config-storage.http-proxy-username=proxy_user +wx.mp.config-storage.http-proxy-password=proxy_pass + +# 超时配置(可选) +wx.mp.config-storage.connection-timeout=5000 +wx.mp.config-storage.so-timeout=5000 +wx.mp.config-storage.connection-request-timeout=5000 +``` + +## 技术细节 + +### HttpClient 4.x 与 5.x 的主要区别 + +1. **包名变更**: + - HttpClient 4.x: `org.apache.http.*` + - HttpClient 5.x: `org.apache.hc.client5.*`, `org.apache.hc.core5.*` + +2. **API 改进**: + - HttpClient 5.x 提供更现代的 API 设计 + - 更好的异步支持 + - 改进的连接池管理 + +3. **性能优化**: + - HttpClient 5.x 包含多项性能优化 + - 更好的资源管理 + +### 项目中的实现 + +WxJava 项目通过策略模式支持多种 HTTP 客户端: + +``` +weixin-java-common/ +├── util/http/ +│ ├── apache/ # HttpClient 4.x 实现 +│ ├── hc/ # HttpClient 5.x (HttpComponents) 实现 +│ ├── okhttp/ # OkHttp 实现 +│ └── jodd/ # Jodd-http 实现 +``` + +每个模块都有对应的 Service 实现类: +- `*ServiceHttpClientImpl` - 使用 HttpClient 4.x +- `*ServiceHttpComponentsImpl` - 使用 HttpClient 5.x +- `*ServiceOkHttpImpl` - 使用 OkHttp +- `*ServiceJoddHttpImpl` - 使用 Jodd-http + +## 反馈与支持 + +如果在升级过程中遇到问题,请: + +1. 查看 [项目 Wiki](https://github.com/binarywang/WxJava/wiki) +2. 在 [GitHub Issues](https://github.com/binarywang/WxJava/issues) 中搜索或提交问题 +3. 加入技术交流群(见 README.md) + +## 总结 + +- ✅ **推荐使用 HttpClient 5.x**:性能更好,功能更强 +- ✅ **向后兼容**:可以继续使用 HttpClient 4.x +- ✅ **灵活配置**:支持多种 HTTP 客户端,按需选择 +- ✅ **平滑迁移**:无需修改代码,仅需配置,若不使用 HttpClient 5.x ,引入其他依赖即可 diff --git a/docs/MINIAPP_KEFU_SERVICE.md b/docs/MINIAPP_KEFU_SERVICE.md new file mode 100644 index 0000000000..96cf4c3831 --- /dev/null +++ b/docs/MINIAPP_KEFU_SERVICE.md @@ -0,0 +1,80 @@ +# WeChat Mini Program Customer Service Management + +This document describes the new customer service management functionality added to the WxJava Mini Program SDK. + +## Overview + +Previously, the mini program module only had: +- `WxMaCustomserviceWorkService` - For binding mini programs to enterprise WeChat customer service +- `WxMaMsgService.sendKefuMsg()` - For sending customer service messages + +The new `WxMaKefuService` adds comprehensive customer service management capabilities: + +## Features + +### Customer Service Account Management +- `kfList()` - Get list of customer service accounts +- `kfAccountAdd()` - Add new customer service account +- `kfAccountUpdate()` - Update customer service account +- `kfAccountDel()` - Delete customer service account + +### Session Management +- `kfSessionCreate()` - Create customer service session +- `kfSessionClose()` - Close customer service session +- `kfSessionGet()` - Get customer session status +- `kfSessionList()` - Get customer service session list + +## Usage Example + +```java +// Get the customer service management service +WxMaKefuService kefuService = wxMaService.getKefuService(); + +// Add a new customer service account +WxMaKfAccountRequest request = WxMaKfAccountRequest.builder() + .kfAccount("service001@example") + .kfNick("Customer Service 001") + .kfPwd("password123") + .build(); +boolean result = kefuService.kfAccountAdd(request); + +// Create a session between user and customer service +boolean sessionResult = kefuService.kfSessionCreate("user_openid", "service001@example"); + +// Get customer service list +WxMaKfList kfList = kefuService.kfList(); +``` + +## Bean Classes + +### Request Objects +- `WxMaKfAccountRequest` - For customer service account operations +- `WxMaKfSessionRequest` - For session operations + +### Response Objects +- `WxMaKfInfo` - Customer service account information +- `WxMaKfList` - List of customer service accounts +- `WxMaKfSession` - Session information +- `WxMaKfSessionList` - List of sessions + +## API Endpoints + +The service uses the following WeChat Mini Program API endpoints: +- `https://api.weixin.qq.com/cgi-bin/customservice/getkflist` - Get customer service list +- `https://api.weixin.qq.com/customservice/kfaccount/add` - Add customer service account +- `https://api.weixin.qq.com/customservice/kfaccount/update` - Update customer service account +- `https://api.weixin.qq.com/customservice/kfaccount/del` - Delete customer service account +- `https://api.weixin.qq.com/customservice/kfsession/create` - Create session +- `https://api.weixin.qq.com/customservice/kfsession/close` - Close session +- `https://api.weixin.qq.com/customservice/kfsession/getsession` - Get session +- `https://api.weixin.qq.com/customservice/kfsession/getsessionlist` - Get session list + +## Integration + +The service is automatically available through the main `WxMaService` interface: + +```java +WxMaKefuService kefuService = wxMaService.getKefuService(); +``` + +This fills the gap mentioned in the original issue and provides full customer service management capabilities for WeChat Mini Programs. \ No newline at end of file diff --git a/docs/NEW_TRANSFER_API_SUPPORT.md b/docs/NEW_TRANSFER_API_SUPPORT.md new file mode 100644 index 0000000000..835ff7d518 --- /dev/null +++ b/docs/NEW_TRANSFER_API_SUPPORT.md @@ -0,0 +1,154 @@ +# 微信支付新版商户转账API支持 + +## 问题解答 + +**问题**: 新开通的商户号只能使用最新版本的商户转账接口,WxJava是否支持? + +**答案**: **WxJava 已经完整支持新版商户转账API!** 从2025年1月15日开始生效的新版转账API已在WxJava中实现。 + +## 新版转账API特性 + +### 1. API接口对比 + +| 特性 | 传统转账API | 新版转账API (2025.1.15+) | +|------|-------------|-------------------------| +| **服务类** | `MerchantTransferService` | `TransferService` | +| **API路径** | `/v3/transfer/batches` | `/v3/fund-app/mch-transfer/transfer-bills` | +| **转账方式** | 批量转账 | 单笔转账 | +| **场景支持** | 基础场景 | 丰富场景(如佣金报酬等) | +| **撤销功能** | ❌ 不支持 | ✅ 支持 | +| **授权模式** | 仅需确认模式 | ✅ 支持免确认授权模式 | +| **适用范围** | 所有商户 | **新开通商户必须使用** | + +### 2. 新版API功能列表 + +✅ **发起转账** - `transferBills()` +✅ **查询转账** - `getBillsByOutBillNo()` / `getBillsByTransferBillNo()` +✅ **撤销转账** - `transformBillsCancel()` +✅ **回调通知** - `parseTransferBillsNotifyResult()` +✅ **RSA加密** - 自动处理用户姓名加密 +✅ **场景支持** - 支持多种转账场景ID +✅ **授权模式** - 支持免确认收款授权模式 + +### 3. 收款授权模式支持 + +**新增功能:免确认收款授权模式** + +- **需确认收款授权模式**(默认):用户需要手动确认才能收款 +- **免确认收款授权模式**:用户授权后,收款无需确认,转账直接到账 + +#### 使用方法 + +```java +// 免确认授权模式 - 提升用户体验 +TransferBillsRequest request = TransferBillsRequest.newBuilder() + .receiptAuthorizationMode(WxPayConstants.ReceiptAuthorizationMode.NO_CONFIRM_RECEIPT_AUTHORIZATION) + // 其他参数... + .build(); + +// 需确认授权模式(默认) +TransferBillsRequest request2 = TransferBillsRequest.newBuilder() + .receiptAuthorizationMode(WxPayConstants.ReceiptAuthorizationMode.CONFIRM_RECEIPT_AUTHORIZATION) + // 其他参数... + .build(); +``` + +## 快速开始 + +### 1. 获取服务实例 + +```java +// 获取WxPayService实例 +WxPayService wxPayService = new WxPayServiceImpl(); +wxPayService.setConfig(config); + +// 获取新版转账服务 - 这就是新开通商户需要使用的服务! +TransferService transferService = wxPayService.getTransferService(); +``` + +### 2. 发起转账(新版API) + +```java +// 构建转账请求 +TransferBillsRequest request = TransferBillsRequest.newBuilder() + .appid("your_appid") // 应用ID + .outBillNo("T" + System.currentTimeMillis()) // 商户转账单号 + .transferSceneId("1005") // 转账场景ID(佣金报酬) + .openid("user_openid") // 用户openid + .userName("张三") // 收款用户姓名(可选,自动加密) + .transferAmount(100) // 转账金额(分) + .transferRemark("佣金报酬") // 转账备注 + .build(); + +// 发起转账 +TransferBillsResult result = transferService.transferBills(request); +System.out.println("转账成功,微信转账单号:" + result.getTransferBillNo()); +``` + +### 3. 查询转账结果 + +```java +// 通过商户单号查询 +TransferBillsGetResult result = transferService.getBillsByOutBillNo("T1642567890123"); + +// 通过微信转账单号查询 +TransferBillsGetResult result2 = transferService.getBillsByTransferBillNo("wx_transfer_bill_no"); + +System.out.println("转账状态:" + result.getState()); +``` + +### 4. 撤销转账(新功能) + +```java +// 撤销转账 +TransferBillsCancelResult cancelResult = transferService.transformBillsCancel("T1642567890123"); +System.out.println("撤销状态:" + cancelResult.getState()); +``` + +## 重要说明 + +### 转账场景ID (transfer_scene_id) +- **1005**: 佣金报酬(常用场景) +- 其他场景需要在微信商户平台申请 + +### 转账状态说明 +- **ACCEPTED**: 转账已受理 +- **PROCESSING**: 转账处理中 +- **SUCCESS**: 转账成功 +- **FAIL**: 转账失败 +- **CANCELLED**: 转账撤销完成 + +### 新开通商户使用建议 + +1. **优先使用** `TransferService` (新版API) +2. **不要使用** `MerchantTransferService` (可能不支持) +3. **必须设置** 转账场景ID (`transfer_scene_id`) +4. **建议开启** 回调通知以实时获取转账结果 + +## 完整示例代码 + +详细的使用示例请参考: +- 📄 [NEW_TRANSFER_API_USAGE.md](./NEW_TRANSFER_API_USAGE.md) - 详细使用指南 +- 💻 [NewTransferApiExample.java](./weixin-java-pay/src/main/java/com/github/binarywang/wxpay/example/NewTransferApiExample.java) - 完整代码示例 + +## 常见问题 + +**Q: 我是新开通的商户,应该使用哪个服务?** +A: 使用 `TransferService`,这是专为新版API设计的服务。 + +**Q: 新版API和旧版API有什么区别?** +A: 新版API使用单笔转账模式,支持更丰富的转账场景,并且支持撤销功能。 + +**Q: 如何设置转账场景ID?** +A: 在商户平台申请相应场景,常用的佣金报酬场景ID是"1005"。 + +**Q: 用户姓名需要加密吗?** +A: WxJava会自动处理RSA加密,您只需要传入明文姓名即可。 + +## 版本要求 + +- WxJava 版本:4.7.0+ +- 支持时间:2025年1月15日+ +- 适用商户:所有商户(新开通商户强制使用) + +通过以上说明,新开通的微信支付商户可以放心使用WxJava进行商户转账操作! \ No newline at end of file diff --git a/docs/NEW_TRANSFER_API_USAGE.md b/docs/NEW_TRANSFER_API_USAGE.md new file mode 100644 index 0000000000..7b1a8da4ea --- /dev/null +++ b/docs/NEW_TRANSFER_API_USAGE.md @@ -0,0 +1,242 @@ +# 微信支付新版商户转账API使用指南 + +## 概述 + +从2025年1月15日开始,微信支付推出了新版的商户转账API。新开通的商户号只能使用最新版本的商户转账接口。WxJava 已经完整支持新版转账API。 + +## API对比 + +### 传统转账API (仍然支持) +- **服务类**: `MerchantTransferService` +- **API前缀**: `/v3/transfer/batches` +- **特点**: 支持批量转账,一次可以转账给多个用户 + +### 新版转账API (2025.1.15+) +- **服务类**: `TransferService` +- **API前缀**: `/v3/fund-app/mch-transfer/transfer-bills` +- **特点**: 单笔转账,支持更丰富的转账场景 + +## 收款授权模式功能 + +### 授权模式说明 + +微信支付转账支持两种收款授权模式: + +#### 1. 需确认收款授权模式(默认) +- **常量**: `WxPayConstants.ReceiptAuthorizationMode.CONFIRM_RECEIPT_AUTHORIZATION` +- **特点**: 用户收到转账后需要手动点击确认才能到账 +- **适用场景**: 一般的转账场景 +- **用户体验**: 安全性高,但需要额外操作 + +#### 2. 免确认收款授权模式 +- **常量**: `WxPayConstants.ReceiptAuthorizationMode.NO_CONFIRM_RECEIPT_AUTHORIZATION` +- **特点**: 用户事先授权后,转账直接到账,无需确认 +- **适用场景**: 高频转账场景,如佣金发放、返现等 +- **用户体验**: 体验流畅,无需额外操作 +- **前提条件**: 需要用户事先进行授权 + +### 使用示例 + +#### 免确认授权模式转账 + +```java +TransferBillsRequest request = TransferBillsRequest.newBuilder() + .appid("your_appid") + .outBillNo("NO_CONFIRM_" + System.currentTimeMillis()) + .transferSceneId("1005") // 佣金报酬场景 + .openid("user_openid") + .transferAmount(200) // 2元 + .transferRemark("免确认收款转账") + .receiptAuthorizationMode(WxPayConstants.ReceiptAuthorizationMode.NO_CONFIRM_RECEIPT_AUTHORIZATION) + .userRecvPerception("Y") + .build(); + +try { + TransferBillsResult result = transferService.transferBills(request); + System.out.println("转账成功,直接到账:" + result.getTransferBillNo()); +} catch (WxPayException e) { + if ("USER_NOT_AUTHORIZED".equals(e.getErrCode())) { + System.err.println("用户未授权免确认收款,请先引导用户进行授权"); + } +} +``` + +#### 需确认授权模式转账(默认) + +```java +TransferBillsRequest request = TransferBillsRequest.newBuilder() + .appid("your_appid") + .outBillNo("CONFIRM_" + System.currentTimeMillis()) + .transferSceneId("1005") + .openid("user_openid") + .transferAmount(150) // 1.5元 + .transferRemark("需确认收款转账") + // .receiptAuthorizationMode(...) // 不设置时使用默认的确认模式 + .userRecvPerception("Y") + .build(); + +TransferBillsResult result = transferService.transferBills(request); +System.out.println("转账发起成功,等待用户确认:" + result.getPackageInfo()); +``` + +### 错误处理 + +使用免确认授权模式时,需要处理以下可能的错误: + +```java +try { + TransferBillsResult result = transferService.transferBills(request); +} catch (WxPayException e) { + switch (e.getErrCode()) { + case "USER_NOT_AUTHORIZED": + // 用户未授权免确认收款 + System.err.println("请先引导用户进行免确认收款授权"); + // 可以引导用户到授权页面 + break; + case "AUTHORIZATION_EXPIRED": + // 授权已过期 + System.err.println("用户授权已过期,请重新授权"); + break; + default: + System.err.println("转账失败:" + e.getMessage()); + } +} +``` + +### 使用建议 + +1. **高频转账场景**推荐使用免确认模式,提升用户体验 +2. **首次使用**需引导用户进行授权 +3. **处理异常**妥善处理授权相关异常,提供友好的错误提示 +4. **场景选择**根据业务场景选择合适的授权模式 + +## 使用新版转账API + +### 1. 获取服务实例 + +```java +// 获取WxPayService实例 +WxPayService wxPayService = new WxPayServiceImpl(); +wxPayService.setConfig(config); + +// 获取新版转账服务 +TransferService transferService = wxPayService.getTransferService(); +``` + +### 2. 发起转账 + +```java +// 构建转账请求 +TransferBillsRequest request = TransferBillsRequest.newBuilder() + .appid("your_appid") // 应用ID + .outBillNo("T" + System.currentTimeMillis()) // 商户转账单号 + .transferSceneId("1005") // 转账场景ID(佣金报酬) + .openid("user_openid") // 用户openid + .userName("张三") // 收款用户姓名(可选,需要加密) + .transferAmount(100) // 转账金额(分) + .transferRemark("佣金报酬") // 转账备注 + .notifyUrl("https://your-domain.com/notify") // 回调地址(可选) + .userRecvPerception("Y") // 用户收款感知(可选) + .build(); + +try { + TransferBillsResult result = transferService.transferBills(request); + System.out.println("转账成功,微信转账单号:" + result.getTransferBillNo()); + System.out.println("状态:" + result.getState()); +} catch (WxPayException e) { + System.err.println("转账失败:" + e.getMessage()); +} +``` + +### 3. 查询转账结果 + +```java +// 通过商户单号查询 +String outBillNo = "T1642567890123"; +TransferBillsGetResult result = transferService.getBillsByOutBillNo(outBillNo); + +// 通过微信转账单号查询 +String transferBillNo = "1000000000000000000000000001"; +TransferBillsGetResult result2 = transferService.getBillsByTransferBillNo(transferBillNo); + +System.out.println("转账状态:" + result.getState()); +System.out.println("转账金额:" + result.getTransferAmount()); +``` + +### 4. 撤销转账 + +```java +// 撤销转账(仅在特定状态下可撤销) +String outBillNo = "T1642567890123"; +TransferBillsCancelResult cancelResult = transferService.transformBillsCancel(outBillNo); +System.out.println("撤销结果:" + cancelResult.getState()); +``` + +### 5. 处理回调通知 + +```java +// 在回调接口中处理通知 +@PostMapping("/transfer/notify") +public String handleTransferNotify(HttpServletRequest request) throws Exception { + String notifyData = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8); + + // 构建签名头 + SignatureHeader header = new SignatureHeader(); + header.setTimeStamp(request.getHeader("Wechatpay-Timestamp")); + header.setNonce(request.getHeader("Wechatpay-Nonce")); + header.setSignature(request.getHeader("Wechatpay-Signature")); + header.setSerial(request.getHeader("Wechatpay-Serial")); + + try { + TransferBillsNotifyResult notifyResult = transferService.parseTransferBillsNotifyResult(notifyData, header); + + // 处理业务逻辑 + String outBillNo = notifyResult.getOutBillNo(); + String state = notifyResult.getState(); + + System.out.println("转账单号:" + outBillNo + ",状态:" + state); + + return "SUCCESS"; + } catch (WxPayException e) { + System.err.println("验签失败:" + e.getMessage()); + return "FAIL"; + } +} +``` + +## 重要参数说明 + +### 转账场景ID (transfer_scene_id) +- **1005**: 佣金报酬(常用) +- 其他场景ID需要在商户平台申请 + +### 转账状态 +- **PROCESSING**: 转账中 +- **SUCCESS**: 转账成功 +- **FAILED**: 转账失败 +- **REFUNDED**: 已退款 + +### 用户收款感知 (user_recv_perception) +- **Y**: 用户会收到微信转账通知 +- **N**: 用户不会收到微信转账通知 + +## 新旧API对比总结 + +| 特性 | 传统API (MerchantTransferService) | 新版API (TransferService) | +|------|----------------------------------|---------------------------| +| 发起方式 | 批量转账 | 单笔转账 | +| API路径 | `/v3/transfer/batches` | `/v3/fund-app/mch-transfer/transfer-bills` | +| 场景支持 | 基础转账场景 | 丰富的转账场景 | +| 回调通知 | 支持 | 支持 | +| 撤销功能 | 不支持 | 支持 | +| 适用商户 | 所有商户 | 新开通商户必须使用 | + +## 注意事项 + +1. **新开通的商户号**: 必须使用新版API (`TransferService`) +2. **转账场景ID**: 需要在商户平台申请相应的转账场景 +3. **用户姓名加密**: 如果传入用户姓名,会自动进行RSA加密 +4. **回调验签**: 建议开启回调验签以确保安全性 +5. **错误处理**: 妥善处理各种异常情况 + +通过以上指南,您可以轻松使用WxJava的新版商户转账API功能。 \ No newline at end of file diff --git a/docs/QUARKUS_SUPPORT.md b/docs/QUARKUS_SUPPORT.md new file mode 100644 index 0000000000..c20fb2c28b --- /dev/null +++ b/docs/QUARKUS_SUPPORT.md @@ -0,0 +1,112 @@ +# WxJava Quarkus/GraalVM Native Image Support + +## 概述 + +从 4.7.8.B 版本开始,WxJava 提供了对 Quarkus 和 GraalVM Native Image 的支持。这允许您将使用 WxJava 的应用程序编译为原生可执行文件,从而获得更快的启动速度和更低的内存占用。 + +## 问题背景 + +在之前的版本中,使用 Quarkus 构建 Native Image 时会遇到以下错误: + +``` +Error: Unsupported features in 3 methods +Detailed message: +Error: Detected an instance of Random/SplittableRandom class in the image heap. +Instances created during image generation have cached seed values and don't behave as expected. +The culprit object has been instantiated by the 'org.apache.http.impl.auth.NTLMEngineImpl' class initializer +``` + +## 解决方案 + +为了解决这个问题,WxJava 进行了以下改进: + +### 1. Random 实例的延迟初始化 + +所有 `java.util.Random` 实例都已改为延迟初始化,避免在类加载时创建: + +- `RandomUtils` - 使用双重检查锁定模式延迟初始化 +- `SignUtils` - 使用双重检查锁定模式延迟初始化 +- `WxCryptUtil` - 使用双重检查锁定模式延迟初始化 + +### 2. Native Image 配置 + +在 `weixin-java-common` 模块中添加了 GraalVM Native Image 配置文件: + +- `META-INF/native-image/com.github.binarywang/weixin-java-common/native-image.properties` + - 配置 Apache HttpClient 相关类在运行时初始化,避免在构建时创建 SecureRandom 实例 + +- `META-INF/native-image/com.github.binarywang/weixin-java-common/reflect-config.json` + - 配置反射访问的类和方法 + +## 使用方式 + +### Quarkus 项目配置 + +在您的 Quarkus 项目中使用 WxJava,只需正常引入依赖即可: + +```xml + + com.github.binarywang + weixin-java-miniapp + 4.7.8.B + +``` + +### 构建 Native Image + +使用 Quarkus 构建原生可执行文件: + +```bash +./mvnw package -Pnative +``` + +或使用容器构建: + +```bash +./mvnw package -Pnative -Dquarkus.native.container-build=true +``` + +### GraalVM Native Image + +如果直接使用 GraalVM Native Image 工具: + +```bash +native-image --no-fallback \ + -H:+ReportExceptionStackTraces \ + -jar your-application.jar +``` + +WxJava 的配置文件会自动被 Native Image 工具识别和应用。 + +## 测试验证 + +建议在构建 Native Image 后进行以下测试: + +1. 验证应用程序可以正常启动 +2. 验证微信 API 调用功能正常 +3. 验证随机字符串生成功能正常 +4. 验证加密/解密功能正常 + +## 已知限制 + +- 本配置主要针对 Quarkus 3.x 和 GraalVM 22.x+ 版本进行测试 +- 如果使用其他 Native Image 构建工具(如 Spring Native),可能需要额外配置 +- 部分反射使用可能需要根据实际使用的 WxJava 功能进行调整 + +## 问题反馈 + +如果在使用 Quarkus/GraalVM Native Image 时遇到问题,请通过以下方式反馈: + +1. 在 [GitHub Issues](https://github.com/binarywang/WxJava/issues) 提交问题 +2. 提供详细的错误信息和 Native Image 构建日志 +3. 说明使用的 Quarkus 版本和 GraalVM 版本 + +## 参考资料 + +- [Quarkus 官方文档](https://quarkus.io/) +- [GraalVM Native Image 文档](https://www.graalvm.org/latest/reference-manual/native-image/) +- [Quarkus Tips for Writing Native Applications](https://quarkus.io/guides/writing-native-applications-tips) + +## 贡献 + +欢迎提交 PR 完善 Quarkus/GraalVM 支持!如果您发现了新的兼容性问题或有改进建议,请参考 [代码贡献指南](CONTRIBUTING.md)。 diff --git a/images/api-signature/api-signature-1.png b/images/api-signature/api-signature-1.png new file mode 100644 index 0000000000..e4d4e1e278 Binary files /dev/null and b/images/api-signature/api-signature-1.png differ diff --git a/images/api-signature/api-signature-2.png b/images/api-signature/api-signature-2.png new file mode 100644 index 0000000000..30982f498b Binary files /dev/null and b/images/api-signature/api-signature-2.png differ diff --git a/images/banners/planB.jpg b/images/banners/planB.jpg deleted file mode 100644 index 139957fbef..0000000000 Binary files a/images/banners/planB.jpg and /dev/null differ diff --git a/images/banners/vultr.jpg b/images/banners/vultr.jpg deleted file mode 100644 index 80cf3c2b5e..0000000000 Binary files a/images/banners/vultr.jpg and /dev/null differ diff --git a/others/fenxiang.md b/others/fenxiang.md deleted file mode 100644 index 8b13789179..0000000000 --- a/others/fenxiang.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/others/mvnw b/others/mvnw old mode 100644 new mode 100755 diff --git a/others/weixin-java-config/README.md b/others/weixin-java-config/README.md new file mode 100644 index 0000000000..aa70de9579 --- /dev/null +++ b/others/weixin-java-config/README.md @@ -0,0 +1,424 @@ +# weixin-java-config +1.目录说明:多配置文件目录 + +2.项目多配置集锦 +```yml +wechat: + pay: #微信服务商支付 + configs: + - appId: wxe97b2x9c2b3d #spAppId + mchId: 16486610 #服务商商户 + subAppId: wx118cexxe3c07679 #子appId + subMchId: 16496705 #子商户 + apiV3Key: Dc1DBwSc094jAKDGR5aqqb7PTHr #apiV3密钥 + privateKeyPath: classpath:cert/apiclient_key.pem #服务商证书文件,apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径(可以配置绝对路径) + privateCertPath: classpath:cert/apiclient_cert.pem #apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径 + miniapp: #小程序 + configs: + - appid: wx118ce3xxc76ccg + secret: 8a132a276ee2f8fb58b1ed8f2 + token: #微信小程序消息服务器配置的token + aesKey: #微信小程序消息服务器配置的EncodingAESKey + msgDataFormat: JSON + cp: #企业微信 + corpId: wwa3be8efd2addfgj + appConfigs: + - agentId: 10001 #客户联系 + secret: T5fTj1n-sBAT4rKNW5c9IYNfPdXZ8-oGol5tX + token: 2bSNqTcLtFYBUa1u2 + aesKey: AXazu2Xyw44SNY1x8go2phn9p9B2O9oiEfqPN + - agentId: 10003 #会话内容存档 + secret: xIpum7Yt4NMXcyxdzcQ2l_46BG4QIQDR57MhA + token: + aesKey: + - agentId: 3010011 #打卡 + secret: 3i2Mhfusifaw_-04bMYI8OoKGxPe9mDALbUxV + token: + aesKey: + - agentId: 19998 #通讯录同步 + secret: rNyDae0Pg-3d-wqTd_ozMSJfF0DEjTCz3b_pr + token: xUke8yZciAZqImGZ + aesKey: EUTVyArqJcfnpFiudxjRpuOexNqBoPbwrNG3R + - agentId: 20000 #微盘 + secret: D-TVMvUji7PZZdjhZOSgiy2MTuBd0OCdvI_zi + token: + aesKey: +``` + +3.主要代码 +###### 1)微信服务商支付 +```java +@Data +@ConfigurationProperties(prefix = "wechat.pay") +public class WxPayProperties { + + private List configs; + + @Getter + @Setter + public static class Config { + + private String appId; + private String mchId; + private String subAppId; + private String subMchId; + private String apiV3Key; + private String privateKeyPath; + private String privateCertPath; + + } + +} +``` +```java +@Configuration +@EnableConfigurationProperties(WxPayProperties.class) +@AllArgsConstructor +public class WxPayConfiguration { + + private WxPayProperties properties; + + @Bean + public WxPayService wxPayService() { + + // 多配置 + WxPayService wxPayService = new WxPayServiceImpl(); + Map payConfigs = this.properties.getConfigs().stream().map(config -> { + WxPayConfig payConfig = new WxPayConfig(); + payConfig.setAppId(StringUtils.trimToNull(config.getAppId())); + payConfig.setMchId(StringUtils.trimToNull(config.getMchId())); + payConfig.setSubAppId(StringUtils.trimToNull(config.getSubAppId())); + payConfig.setSubMchId(StringUtils.trimToNull(config.getSubMchId())); + payConfig.setApiV3Key(StringUtils.trimToNull(config.getApiV3Key())); + payConfig.setPrivateKeyPath(StringUtils.trimToNull(config.getPrivateKeyPath())); + payConfig.setPrivateCertPath(StringUtils.trimToNull(config.getPrivateCertPath())); + + // 可以指定是否使用沙箱环境 + payConfig.setUseSandboxEnv(false); + return payConfig; + }).collect(Collectors.toMap(config -> config.getSubMchId(), a -> a)); + + wxPayService.setMultiConfig(payConfigs); + return wxPayService; + } + +} +``` +###### 2)微信小程序 +```java +@Setter +@Getter +@ConfigurationProperties(prefix = "wechat.miniapp") +public class WxMaProperties { + + private List configs; + + @Data + public static class Config { + + /** + * 设置微信小程序的appid + */ + private String appid; + + /** + * 设置微信小程序的Secret + */ + private String secret; + + /** + * 设置微信小程序消息服务器配置的token + */ + private String token; + + /** + * 设置微信小程序消息服务器配置的EncodingAESKey + */ + private String aesKey; + + /** + * 消息格式,XML或者JSON + */ + private String msgDataFormat; + + } + +} +``` +```java +@Configuration +@EnableConfigurationProperties(WxMaProperties.class) +public class WxMaConfiguration { + + private WxMaProperties properties; + private static Map maServices; + private static final Map routers = Maps.newHashMap(); + + @Autowired + public WxMaConfiguration(WxMaProperties properties) { + this.properties = properties; + } + + public static WxMaService getMaService(String appId) { + WxMaService wxService = maServices.get(appId); + Optional.ofNullable(wxService).orElseThrow(() -> new RuntimeException("没有配置appId")); + return wxService; + } + + public static WxMaMessageRouter getRouter(String appId) { + return routers.get(appId); + } + + @PostConstruct + public void init() { + List configs = this.properties.getConfigs(); + if (configs == null) { + return; + } + + maServices = configs.stream().map(a -> { + // 多配置 + WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl(); + config.setAppid(a.getAppid()); + config.setSecret(a.getSecret()); + config.setToken(a.getToken()); + config.setAesKey(a.getAesKey()); + config.setMsgDataFormat(a.getMsgDataFormat()); + + WxMaService service = new WxMaServiceImpl(); + service.setWxMaConfig(config); + + routers.put(a.getAppid(), this.newRouter(service)); + return service; + }).collect(Collectors.toMap(s -> s.getWxMaConfig().getAppid(), a -> a)); + } + + private WxMaMessageRouter newRouter(WxMaService service) { + final WxMaMessageRouter router = new WxMaMessageRouter(service); + router + .rule().handler(logHandler).next() + .rule().async(false).content("订阅消息").handler(subscribeMsgHandler).end() + .rule().async(false).content("文本").handler(textHandler).end() + .rule().async(false).content("图片").handler(picHandler).end() + .rule().async(false).content("二维码").handler(qrcodeHandler).end(); + return router; + } + + private final WxMaMessageHandler subscribeMsgHandler = (wxMessage, context, service, sessionManager) -> { + service.getMsgService().sendSubscribeMsg(WxMaSubscribeMessage.builder() + .templateId("此处更换为自己的模板id") + .data(Lists.newArrayList( + new WxMaSubscribeMessage.MsgData("keyword1", "339208499"))) + .toUser(wxMessage.getFromUser()) + .build()); + return null; + }; + + private final WxMaMessageHandler logHandler = (wxMessage, context, service, sessionManager) -> { + log.info("收到logHandler消息:" + wxMessage.toString()); + service.getMsgService().sendKefuMsg(WxMaKefuMessage.newTextBuilder().content("收到信息为:" + wxMessage.toJson()) + .toUser(wxMessage.getFromUser()).build()); + return null; + }; + + private final WxMaMessageHandler textHandler = (wxMessage, context, service, sessionManager) -> { + log.info("收到textHandler消息:" + wxMessage.toString()); + service.getMsgService().sendKefuMsg(WxMaKefuMessage.newTextBuilder().content("回复文本消息") + .toUser(wxMessage.getFromUser()).build()); + return null; + }; + + private final WxMaMessageHandler picHandler = (wxMessage, context, service, sessionManager) -> { + log.info("收到picHandler消息:" + wxMessage.toString()); + try { + WxMediaUploadResult uploadResult = service.getMediaService() + .uploadMedia("image", "png", + ClassLoader.getSystemResourceAsStream("tmp.png")); + service.getMsgService().sendKefuMsg( + WxMaKefuMessage + .newImageBuilder() + .mediaId(uploadResult.getMediaId()) + .toUser(wxMessage.getFromUser()) + .build()); + } catch (WxErrorException e) { + e.printStackTrace(); + } + + return null; + }; + + private final WxMaMessageHandler qrcodeHandler = (wxMessage, context, service, sessionManager) -> { + log.info("收到qrcodeHandler消息:" + wxMessage.toString()); + try { + final File file = service.getQrcodeService().createQrcode("123", 430); + WxMediaUploadResult uploadResult = service.getMediaService().uploadMedia("image", file); + service.getMsgService().sendKefuMsg( + WxMaKefuMessage + .newImageBuilder() + .mediaId(uploadResult.getMediaId()) + .toUser(wxMessage.getFromUser()) + .build()); + } catch (WxErrorException e) { + e.printStackTrace(); + } + + return null; + }; + +} +``` +###### 3)企业微信 +```java +@Getter +@Setter +@ConfigurationProperties(prefix = "wechat.cp") +public class WxCpProperties { + + /** + * 设置企业微信的corpId + */ + private String corpId; + + private List appConfigs; + + @Getter + @Setter + public static class AppConfig { + /** + * 设置企业微信应用的AgentId + */ + private Integer agentId; + + /** + * 设置企业微信应用的Secret + */ + private String secret; + + /** + * 设置企业微信应用的token + */ + private String token; + + /** + * 设置企业微信应用的EncodingAESKey + */ + private String aesKey; + + } + +} +``` +```java +@Configuration +@EnableConfigurationProperties(WxCpProperties.class) +public class WxCpConfiguration { + + private LogHandler logHandler; + private NullHandler nullHandler; + private LocationHandler locationHandler; + private MenuHandler menuHandler; + private MsgHandler msgHandler; + private UnsubscribeHandler unsubscribeHandler; + private SubscribeHandler subscribeHandler; + + private WxCpProperties properties; + + private static Map routers = Maps.newHashMap(); + private static Map cpServices = Maps.newHashMap(); + + @Autowired + public WxCpConfiguration(LogHandler logHandler, NullHandler nullHandler, LocationHandler locationHandler, + MenuHandler menuHandler, MsgHandler msgHandler, UnsubscribeHandler unsubscribeHandler, + SubscribeHandler subscribeHandler, WxCpProperties properties) { + this.logHandler = logHandler; + this.nullHandler = nullHandler; + this.locationHandler = locationHandler; + this.menuHandler = menuHandler; + this.msgHandler = msgHandler; + this.unsubscribeHandler = unsubscribeHandler; + this.subscribeHandler = subscribeHandler; + this.properties = properties; + } + + + public static Map getRouters() { + return routers; + } + + + public static WxCpService getCpService(Integer agentId) { + WxCpService cpService = cpServices.get(agentId); + Optional.ofNullable(cpService).orElseThrow(() -> new RuntimeException("cpService不能为空")); + return cpService; + } + + @PostConstruct + public void initServices() { + cpServices = this.properties.getAppConfigs().stream().map(a -> { + val configStorage = new WxCpDefaultConfigImpl(); + configStorage.setCorpId(this.properties.getCorpId()); + configStorage.setAgentId(a.getAgentId()); + configStorage.setCorpSecret(a.getSecret()); + configStorage.setToken(a.getToken()); + configStorage.setAesKey(a.getAesKey()); + + val service = new WxCpServiceImpl(); + service.setWxCpConfigStorage(configStorage); + + routers.put(a.getAgentId(), this.newRouter(service)); + return service; + }).collect(Collectors.toMap(service -> service.getWxCpConfigStorage().getAgentId(), a -> a)); + } + + private WxCpMessageRouter newRouter(WxCpService wxCpService) { + final val newRouter = new WxCpMessageRouter(wxCpService); + + // 记录所有事件的日志 (异步执行) + newRouter.rule().handler(this.logHandler).next(); + + // 自定义菜单事件 + newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxConsts.MenuButtonType.CLICK).handler(this.menuHandler).end(); + + // 点击菜单链接事件(这里使用了一个空的处理器,可以根据自己需要进行扩展) + newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxConsts.MenuButtonType.VIEW).handler(this.nullHandler).end(); + + // 关注事件 + newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxConsts.EventType.SUBSCRIBE).handler(this.subscribeHandler) + .end(); + + // 取消关注事件 + newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxConsts.EventType.UNSUBSCRIBE) + .handler(this.unsubscribeHandler).end(); + + // 上报地理位置事件 + newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxConsts.EventType.LOCATION).handler(this.locationHandler) + .end(); + + // 接收地理位置消息 + newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.LOCATION) + .handler(this.locationHandler).end(); + + // 扫码事件(这里使用了一个空的处理器,可以根据自己需要进行扩展) + newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxConsts.EventType.SCAN).handler(this.nullHandler).end(); + + newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxCpConsts.EventType.CHANGE_CONTACT).handler(new ContactChangeHandler()).end(); + + newRouter.rule().async(false).msgType(WxConsts.XmlMsgType.EVENT) + .event(WxCpConsts.EventType.ENTER_AGENT).handler(new EnterAgentHandler()).end(); + + // 默认 + newRouter.rule().async(false).handler(this.msgHandler).end(); + + return newRouter; + } + +} +``` +4.其他请移步wiki:[GitHub wiki](https://github.com/Wechat-Group/WxJava/wiki) diff --git a/others/weixin-java-osgi/pom.xml b/others/weixin-java-osgi/pom.xml index 107a78b4a2..b8531da88d 100644 --- a/others/weixin-java-osgi/pom.xml +++ b/others/weixin-java-osgi/pom.xml @@ -6,7 +6,7 @@ com.github.binarywang wx-java - 2.6.0 + 4.6.0 weixin-java-osgi @@ -28,7 +28,7 @@ com.thoughtworks.xstream xstream - 1.4.7 + 1.4.21 provided diff --git a/pom.xml b/pom.xml index 930c7ce901..09d30e185f 100644 --- a/pom.xml +++ b/pom.xml @@ -1,12 +1,9 @@ - + 4.0.0 com.github.binarywang wx-java - 3.6.0 + 4.8.3.B pom WxJava - Weixin/Wechat Java SDK 微信开发Java SDK @@ -15,7 +12,7 @@ The Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt + https://www.apache.org/licenses/LICENSE-2.0.txt @@ -90,6 +87,26 @@ liuxinghao1988@gmail.com https://github.com/howardliu-cn + + huangxiaoming + huangxm129@163.com + https://github.com/huangxm129 + + + xiaohe + xiaohe@53jy.net + https://github.com/xiaohe-53 + + + Wang_Wong + wangkaikate@163.com + https://github.com/0katekate0 + + + Bincent + hongbin.hsu@qq.com + https://gitee.com/bincent + @@ -99,46 +116,59 @@ + weixin-graal weixin-java-common weixin-java-cp weixin-java-mp weixin-java-pay weixin-java-miniapp weixin-java-open + weixin-java-qidian + weixin-java-aispeech + weixin-java-channel spring-boot-starters + solon-plugins + wx-java-bom - 1.7 - 1.7 + 1.8 + 1.8 UTF-8 - 4.5 - 9.4.17.v20190418 + 4.5.13 + 5.5.2 + 9.4.57.v20241219 + 1.84 - com.github.binarywang qrcode-utils - 1.1 + 1.3 - org.jodd jodd-http - 3.7.1 + 6.3.0 provided com.squareup.okhttp3 okhttp - 3.7.0 + 4.12.0 provided + + + org.apache.httpcomponents.client5 + httpclient5 + ${httpclient5.version} + + org.apache.httpcomponents httpclient @@ -152,69 +182,87 @@ commons-codec commons-codec - 1.10 + 1.13 commons-io commons-io - 2.5 + 2.14.0 org.apache.commons commons-lang3 - 3.5 + 3.18.0 org.slf4j slf4j-api - 1.7.24 + 1.7.30 com.thoughtworks.xstream xstream - 1.4.11 + 1.4.21 - com.google.guava guava - 20.0 + 33.3.1-jre com.google.code.gson gson - 2.8.0 + 2.13.1 + + + com.fasterxml.jackson + jackson-bom + 2.18.4 + pom + import - + joda-time joda-time - 2.9.7 - test + 2.10.6 + + ch.qos.logback logback-classic - 1.1.11 + 1.3.12 test com.google.inject guice - 3.0 + 4.2.3 test org.testng testng - 6.10 + 7.5.1 + test + + + guice + com.google.inject + + + org.yaml + snakeyaml + + org.mockito - mockito-all - 1.9.5 + mockito-core + 4.11.0 test @@ -235,33 +283,72 @@ 3.0.0 test + + com.github.dreamhead + moco-runner + 1.1.0 + test + redis.clients jedis - 2.9.0 + 3.3.0 + provided + + + com.github.jedis-lock + jedis-lock + 1.0.0 + provided + + + org.redisson + redisson + 3.23.3 + true + provided + + + com.fasterxml.jackson.core + jackson-core + + + org.jodd + jodd-core + + + org.reactivestreams + reactive-streams + + + + + org.springframework.data + spring-data-redis + 2.3.3.RELEASE + true provided org.projectlombok lombok - 1.18.8 + 1.18.30 provided + + org.bouncycastle + bcpkix-jdk18on + ${bouncycastle.version} + + + org.bouncycastle + bcprov-jdk18on + ${bouncycastle.version} + - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - doclint-java8-disable @@ -280,7 +367,7 @@ org.apache.maven.plugins maven-source-plugin - 2.2.1 + 3.1.0 attach-sources @@ -311,7 +398,7 @@ org.apache.maven.plugins maven-gpg-plugin - 1.6 + 3.1.0 sign-artifacts @@ -325,6 +412,14 @@ + + + native-image + + false + + + @@ -343,14 +438,14 @@ - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.3 + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 true - ossrh - https://oss.sonatype.org/ - true + Release WxJava:${project.version} + central + true @@ -384,6 +479,21 @@ + + org.apache.maven.plugins + maven-source-plugin + 3.3.0 + + + attach-sources + + verify + + jar-no-fork + + + + diff --git a/solon-plugins/pom.xml b/solon-plugins/pom.xml new file mode 100644 index 0000000000..87401a2c97 --- /dev/null +++ b/solon-plugins/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + com.github.binarywang + wx-java + 4.8.3.B + + pom + wx-java-solon-plugins + WxJava - Solon Plugins + WxJava 各个模块的 Solon Plugin + + + 3.2.0 + + + + wx-java-miniapp-multi-solon-plugin + wx-java-miniapp-solon-plugin + wx-java-mp-multi-solon-plugin + wx-java-mp-solon-plugin + wx-java-pay-solon-plugin + wx-java-open-solon-plugin + wx-java-qidian-solon-plugin + wx-java-cp-multi-solon-plugin + wx-java-cp-solon-plugin + wx-java-channel-solon-plugin + wx-java-channel-multi-solon-plugin + + + + + org.noear + solon + ${solon.version} + + + org.projectlombok + lombok + provided + + + org.noear + solon-test + ${solon.version} + test + + + diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/README.md b/solon-plugins/wx-java-channel-multi-solon-plugin/README.md new file mode 100644 index 0000000000..6285f54953 --- /dev/null +++ b/solon-plugins/wx-java-channel-multi-solon-plugin/README.md @@ -0,0 +1,111 @@ +# wx-java-channel-multi-solon-plugin + +## 快速开始 + +1. 引入依赖 + ```xml + + + com.github.binarywang + wx-java-channel-multi-solon-plugin + ${version} + + + + + redis.clients + jedis + ${jedis.version} + + + + + org.redisson + redisson + ${redisson.version} + + + ``` +2. 添加配置(app.properties) + ```properties + # 视频号配置 + ## 应用 1 配置(必填) + wx.channel.apps.tenantId1.app-id=@appId + wx.channel.apps.tenantId1.secret=@secret + ## 选填 + wx.channel.apps.tenantId1.use-stable-access-token=false + wx.channel.apps.tenantId1.token= + wx.channel.apps.tenantId1.aes-key= + ## 应用 2 配置(必填) + wx.channel.apps.tenantId2.app-id=@appId + wx.channel.apps.tenantId2.secret=@secret + ## 选填 + wx.channel.apps.tenantId2.use-stable-access-token=false + wx.channel.apps.tenantId2.token= + wx.channel.apps.tenantId2.aes-key= + + # ConfigStorage 配置(选填) + ## 配置类型: memory(默认), jedis, redisson, redis_template + wx.channel.config-storage.type=memory + ## 相关redis前缀配置: wx:channel:multi(默认) + wx.channel.config-storage.key-prefix=wx:channel:multi + wx.channel.config-storage.redis.host=127.0.0.1 + wx.channel.config-storage.redis.port=6379 + wx.channel.config-storage.redis.password=123456 + + # http 客户端配置(选填) + ## # http客户端类型: http_client(默认) + wx.channel.config-storage.http-client-type=http_client + wx.channel.config-storage.http-proxy-host= + wx.channel.config-storage.http-proxy-port= + wx.channel.config-storage.http-proxy-username= + wx.channel.config-storage.http-proxy-password= + ## 最大重试次数,默认:5 次,如果小于 0,则为 0 + wx.channel.config-storage.max-retry-times=5 + ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000 + wx.channel.config-storage.retry-sleep-millis=1000 + ``` +3. 自动注入的类型:`WxChannelMultiServices` + +4. 使用样例 + + ```java + import com.binarywang.solon.wxjava.channel.service.WxChannelMultiServices; + import me.chanjar.weixin.channel.api.WxChannelService; + import me.chanjar.weixin.channel.api.WxFinderLiveService; + import me.chanjar.weixin.channel.bean.lead.component.response.FinderAttrResponse; + import me.chanjar.weixin.common.error.WxErrorException; + import org.noear.solon.annotation.Component; + import org.noear.solon.annotation.Inject; + + @Component + public class DemoService { + @Inject + private WxChannelMultiServices wxChannelMultiServices; + + public void test() throws WxErrorException { + // 应用 1 的 WxChannelService + WxChannelService wxChannelService1 = wxChannelMultiServices.getWxChannelService("tenantId1"); + WxFinderLiveService finderLiveService = wxChannelService1.getFinderLiveService(); + FinderAttrResponse response1 = finderLiveService.getFinderAttrByAppid(); + // todo ... + + // 应用 2 的 WxChannelService + WxChannelService wxChannelService2 = wxChannelMultiServices.getWxChannelService("tenantId2"); + WxFinderLiveService finderLiveService2 = wxChannelService2.getFinderLiveService(); + FinderAttrResponse response2 = finderLiveService2.getFinderAttrByAppid(); + // todo ... + + // 应用 3 的 WxChannelService + WxChannelService wxChannelService3 = wxChannelMultiServices.getWxChannelService("tenantId3"); + // 判断是否为空 + if (wxChannelService3 == null) { + // todo wxChannelService3 为空,请先配置 tenantId3 微信视频号应用参数 + return; + } + WxFinderLiveService finderLiveService3 = wxChannelService3.getFinderLiveService(); + FinderAttrResponse response3 = finderLiveService3.getFinderAttrByAppid(); + // todo ... + } + } + ``` diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml new file mode 100644 index 0000000000..d99f9a67c1 --- /dev/null +++ b/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml @@ -0,0 +1,43 @@ + + + + wx-java-solon-plugins + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-channel-multi-solon-plugin + WxJava - Solon Plugin for Channel::支持多账号配置 + 微信视频号开发的 Solon Plugin::支持多账号配置 + + + + com.github.binarywang + weixin-java-channel + ${project.version} + + + redis.clients + jedis + provided + + + org.redisson + redisson + provided + + + org.jodd + jodd-http + provided + + + com.squareup.okhttp3 + okhttp + provided + + + diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/configuration/services/AbstractWxChannelConfiguration.java b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/configuration/services/AbstractWxChannelConfiguration.java new file mode 100644 index 0000000000..eb80b5f7f3 --- /dev/null +++ b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/configuration/services/AbstractWxChannelConfiguration.java @@ -0,0 +1,146 @@ +package com.binarywang.solon.wxjava.channel.configuration.services; + +import com.binarywang.solon.wxjava.channel.enums.HttpClientType; +import com.binarywang.solon.wxjava.channel.properties.WxChannelMultiProperties; +import com.binarywang.solon.wxjava.channel.properties.WxChannelSingleProperties; +import com.binarywang.solon.wxjava.channel.service.WxChannelMultiServices; +import com.binarywang.solon.wxjava.channel.service.WxChannelMultiServicesImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.channel.api.WxChannelService; +import me.chanjar.weixin.channel.api.impl.WxChannelServiceHttpComponentsImpl; +import me.chanjar.weixin.channel.api.impl.WxChannelServiceHttpClientImpl; +import me.chanjar.weixin.channel.api.impl.WxChannelServiceImpl; +import me.chanjar.weixin.channel.config.WxChannelConfig; +import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * WxChannelConfigStorage 抽象配置类 + * + * @author Winnie 2024/9/13 + * @author noear + */ +@RequiredArgsConstructor +@Slf4j +public abstract class AbstractWxChannelConfiguration { + protected WxChannelMultiServices wxChannelMultiServices(WxChannelMultiProperties wxChannelMultiProperties) { + Map appsMap = wxChannelMultiProperties.getApps(); + if (appsMap == null || appsMap.isEmpty()) { + log.warn("微信视频号应用参数未配置,通过 WxChannelMultiServices#getWxChannelService(\"tenantId\")获取实例将返回空"); + return new WxChannelMultiServicesImpl(); + } + /** + * 校验 appId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。 + * + * 查看 {@link me.chanjar.weixin.channel.config.impl.WxChannelRedisConfigImpl#setAppid(String)} + */ + Collection apps = appsMap.values(); + if (apps.size() > 1) { + // 校验 appId 是否唯一 + boolean multi = apps.stream() + // 没有 appId,如果不判断是否为空,这里会报 NPE 异常 + .collect(Collectors.groupingBy(c -> c.getAppId() == null ? 0 : c.getAppId(), Collectors.counting())) + .entrySet().stream().anyMatch(e -> e.getValue() > 1); + if (multi) { + throw new RuntimeException("请确保微信视频号配置 appId 的唯一性"); + } + } + WxChannelMultiServicesImpl services = new WxChannelMultiServicesImpl(); + + Set> entries = appsMap.entrySet(); + for (Map.Entry entry : entries) { + String tenantId = entry.getKey(); + WxChannelSingleProperties wxChannelSingleProperties = entry.getValue(); + WxChannelDefaultConfigImpl storage = this.wxChannelConfigStorage(wxChannelMultiProperties); + this.configApp(storage, wxChannelSingleProperties); + this.configHttp(storage, wxChannelMultiProperties.getConfigStorage()); + WxChannelService wxChannelService = this.wxChannelService(storage, wxChannelMultiProperties); + services.addWxChannelService(tenantId, wxChannelService); + } + return services; + } + + /** + * 配置 WxChannelDefaultConfigImpl + * + * @param wxChannelMultiProperties 参数 + * @return WxChannelDefaultConfigImpl + */ + protected abstract WxChannelDefaultConfigImpl wxChannelConfigStorage(WxChannelMultiProperties wxChannelMultiProperties); + + public WxChannelService wxChannelService(WxChannelConfig wxChannelConfig, WxChannelMultiProperties wxChannelMultiProperties) { + WxChannelMultiProperties.ConfigStorage storage = wxChannelMultiProperties.getConfigStorage(); + HttpClientType httpClientType = storage.getHttpClientType(); + WxChannelService wxChannelService; + switch (httpClientType) { +// case OK_HTTP: +// wxChannelService = new WxChannelServiceOkHttpImpl(false, false); +// break; + case HTTP_CLIENT: + wxChannelService = new WxChannelServiceHttpClientImpl(); + break; + case HTTP_COMPONENTS: + wxChannelService = new WxChannelServiceHttpComponentsImpl(); + break; + default: + wxChannelService = new WxChannelServiceImpl(); + break; + } + + wxChannelService.setConfig(wxChannelConfig); + int maxRetryTimes = storage.getMaxRetryTimes(); + if (maxRetryTimes < 0) { + maxRetryTimes = 0; + } + int retrySleepMillis = storage.getRetrySleepMillis(); + if (retrySleepMillis < 0) { + retrySleepMillis = 1000; + } + wxChannelService.setRetrySleepMillis(retrySleepMillis); + wxChannelService.setMaxRetryTimes(maxRetryTimes); + return wxChannelService; + } + + private void configApp(WxChannelDefaultConfigImpl config, WxChannelSingleProperties wxChannelSingleProperties) { + String appId = wxChannelSingleProperties.getAppId(); + String appSecret = wxChannelSingleProperties.getSecret(); + String token = wxChannelSingleProperties.getToken(); + String aesKey = wxChannelSingleProperties.getAesKey(); + boolean useStableAccessToken = wxChannelSingleProperties.isUseStableAccessToken(); + + config.setAppid(appId); + config.setSecret(appSecret); + if (StringUtils.isNotBlank(token)) { + config.setToken(token); + } + if (StringUtils.isNotBlank(aesKey)) { + config.setAesKey(aesKey); + } + config.setStableAccessToken(useStableAccessToken); + } + + private void configHttp(WxChannelDefaultConfigImpl config, WxChannelMultiProperties.ConfigStorage storage) { + String httpProxyHost = storage.getHttpProxyHost(); + Integer httpProxyPort = storage.getHttpProxyPort(); + String httpProxyUsername = storage.getHttpProxyUsername(); + String httpProxyPassword = storage.getHttpProxyPassword(); + if (StringUtils.isNotBlank(httpProxyHost)) { + config.setHttpProxyHost(httpProxyHost); + if (httpProxyPort != null) { + config.setHttpProxyPort(httpProxyPort); + } + if (StringUtils.isNotBlank(httpProxyUsername)) { + config.setHttpProxyUsername(httpProxyUsername); + } + if (StringUtils.isNotBlank(httpProxyPassword)) { + config.setHttpProxyPassword(httpProxyPassword); + } + } + } +} diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/configuration/services/WxChannelInJedisConfiguration.java b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/configuration/services/WxChannelInJedisConfiguration.java new file mode 100644 index 0000000000..68afc13320 --- /dev/null +++ b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/configuration/services/WxChannelInJedisConfiguration.java @@ -0,0 +1,77 @@ +package com.binarywang.solon.wxjava.channel.configuration.services; + +import com.binarywang.solon.wxjava.channel.properties.WxChannelMultiProperties; +import com.binarywang.solon.wxjava.channel.properties.WxChannelMultiRedisProperties; +import com.binarywang.solon.wxjava.channel.service.WxChannelMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl; +import me.chanjar.weixin.channel.config.impl.WxChannelRedisConfigImpl; +import me.chanjar.weixin.common.redis.JedisWxRedisOps; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * 自动装配基于 jedis 策略配置 + * + * @author Winnie 2024/9/13 + * @author noear + */ +@Configuration +@Condition( + onProperty = "${"+WxChannelMultiProperties.PREFIX + ".configStorage.type} = jedis", + onClass = JedisPool.class +) +@RequiredArgsConstructor +public class WxChannelInJedisConfiguration extends AbstractWxChannelConfiguration { + private final WxChannelMultiProperties wxChannelMultiProperties; + private final AppContext applicationContext; + + @Bean + public WxChannelMultiServices wxChannelMultiServices() { + return this.wxChannelMultiServices(wxChannelMultiProperties); + } + + @Override + protected WxChannelDefaultConfigImpl wxChannelConfigStorage(WxChannelMultiProperties wxChannelMultiProperties) { + return this.configRedis(wxChannelMultiProperties); + } + + private WxChannelDefaultConfigImpl configRedis(WxChannelMultiProperties wxChannelMultiProperties) { + WxChannelMultiRedisProperties wxChannelMultiRedisProperties = wxChannelMultiProperties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (wxChannelMultiRedisProperties != null && StringUtils.isNotEmpty(wxChannelMultiRedisProperties.getHost())) { + jedisPool = getJedisPool(wxChannelMultiProperties); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + return new WxChannelRedisConfigImpl(new JedisWxRedisOps(jedisPool), wxChannelMultiProperties.getConfigStorage().getKeyPrefix()); + } + + private JedisPool getJedisPool(WxChannelMultiProperties wxChannelMultiProperties) { + WxChannelMultiProperties.ConfigStorage storage = wxChannelMultiProperties.getConfigStorage(); + WxChannelMultiRedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(), redis.getDatabase()); + } +} diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/configuration/services/WxChannelInMemoryConfiguration.java b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/configuration/services/WxChannelInMemoryConfiguration.java new file mode 100644 index 0000000000..71cd5ca33c --- /dev/null +++ b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/configuration/services/WxChannelInMemoryConfiguration.java @@ -0,0 +1,40 @@ +package com.binarywang.solon.wxjava.channel.configuration.services; + +import com.binarywang.solon.wxjava.channel.properties.WxChannelMultiProperties; +import com.binarywang.solon.wxjava.channel.service.WxChannelMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import redis.clients.jedis.JedisPool; + +/** + * 自动装配基于内存策略配置 + * + * @author Winnie 2024/9/13 + * @author noear + */ +@Configuration +@Condition( + onProperty = "${"+WxChannelMultiProperties.PREFIX + ".configStorage.type} = memory", + onClass = JedisPool.class +) +@RequiredArgsConstructor +public class WxChannelInMemoryConfiguration extends AbstractWxChannelConfiguration { + private final WxChannelMultiProperties wxChannelMultiProperties; + + @Bean + public WxChannelMultiServices wxChannelMultiServices() { + return this.wxChannelMultiServices(wxChannelMultiProperties); + } + + @Override + protected WxChannelDefaultConfigImpl wxChannelConfigStorage(WxChannelMultiProperties wxChannelMultiProperties) { + return this.configInMemory(); + } + + private WxChannelDefaultConfigImpl configInMemory() { + return new WxChannelDefaultConfigImpl(); + } +} diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/configuration/services/WxChannelInRedissonConfiguration.java b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/configuration/services/WxChannelInRedissonConfiguration.java new file mode 100644 index 0000000000..fce6a735ea --- /dev/null +++ b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/configuration/services/WxChannelInRedissonConfiguration.java @@ -0,0 +1,65 @@ +package com.binarywang.solon.wxjava.channel.configuration.services; + +import com.binarywang.solon.wxjava.channel.properties.WxChannelMultiProperties; +import com.binarywang.solon.wxjava.channel.properties.WxChannelMultiRedisProperties; +import com.binarywang.solon.wxjava.channel.service.WxChannelMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl; +import me.chanjar.weixin.channel.config.impl.WxChannelRedissonConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; + +/** + * 自动装配基于 redisson 策略配置 + * + * @author Winnie 2024/9/13 + * @author noear + */ +@Configuration +@Condition( + onProperty = "${"+WxChannelMultiProperties.PREFIX + ".configStorage.type} = redisson", + onClass = Redisson.class +) +@RequiredArgsConstructor +public class WxChannelInRedissonConfiguration extends AbstractWxChannelConfiguration { + private final WxChannelMultiProperties wxChannelMultiProperties; + private final AppContext applicationContext; + + @Bean + public WxChannelMultiServices wxChannelMultiServices() { + return this.wxChannelMultiServices(wxChannelMultiProperties); + } + + @Override + protected WxChannelDefaultConfigImpl wxChannelConfigStorage(WxChannelMultiProperties wxChannelMultiProperties) { + return this.configRedisson(wxChannelMultiProperties); + } + + private WxChannelDefaultConfigImpl configRedisson(WxChannelMultiProperties wxChannelMultiProperties) { + WxChannelMultiRedisProperties redisProperties = wxChannelMultiProperties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + redissonClient = getRedissonClient(wxChannelMultiProperties); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxChannelRedissonConfigImpl(redissonClient, wxChannelMultiProperties.getConfigStorage().getKeyPrefix()); + } + + private RedissonClient getRedissonClient(WxChannelMultiProperties wxChannelMultiProperties) { + WxChannelMultiProperties.ConfigStorage storage = wxChannelMultiProperties.getConfigStorage(); + WxChannelMultiRedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer().setAddress("redis://" + redis.getHost() + ":" + redis.getPort()).setDatabase(redis.getDatabase()).setPassword(redis.getPassword()); + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/enums/HttpClientType.java b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/enums/HttpClientType.java new file mode 100644 index 0000000000..c34533c6d1 --- /dev/null +++ b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/enums/HttpClientType.java @@ -0,0 +1,23 @@ +package com.binarywang.solon.wxjava.channel.enums; + +/** + * httpclient类型 + * + * @author Winnie + * @date 2024/9/13 + */ +public enum HttpClientType { + /** + * HttpClient + */ + HTTP_CLIENT, + /** + * HttpComponents + */ + HTTP_COMPONENTS + // WxChannelServiceOkHttpImpl 实现经测试无法正常完成业务固暂不支持OK_HTTP方式 +// /** +// * OkHttp. +// */ +// OK_HTTP, +} diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/enums/StorageType.java b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/enums/StorageType.java new file mode 100644 index 0000000000..a1b710cd2a --- /dev/null +++ b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/enums/StorageType.java @@ -0,0 +1,26 @@ +package com.binarywang.solon.wxjava.channel.enums; + +/** + * storage类型 + * + * @author Winnie + * @date 2024/9/13 + */ +public enum StorageType { + /** + * 内存 + */ + MEMORY, + /** + * redis(JedisClient) + */ + JEDIS, + /** + * redis(Redisson) + */ + REDISSON, + /** + * redis(RedisTemplate) + */ + REDIS_TEMPLATE +} diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/integration/WxChannelMultiPluginImpl.java b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/integration/WxChannelMultiPluginImpl.java new file mode 100644 index 0000000000..3b84794eac --- /dev/null +++ b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/integration/WxChannelMultiPluginImpl.java @@ -0,0 +1,25 @@ +package com.binarywang.solon.wxjava.channel.integration; + +import com.binarywang.solon.wxjava.channel.configuration.services.WxChannelInJedisConfiguration; +import com.binarywang.solon.wxjava.channel.configuration.services.WxChannelInMemoryConfiguration; +import com.binarywang.solon.wxjava.channel.configuration.services.WxChannelInRedissonConfiguration; +import com.binarywang.solon.wxjava.channel.properties.WxChannelMultiProperties; +import org.noear.solon.core.AppContext; +import org.noear.solon.core.Plugin; + +/** + * 微信视频号自动注册 + * + * @author Winnie 2024/9/13 + * @author noear 2024/10/9 created + */ +public class WxChannelMultiPluginImpl implements Plugin { + @Override + public void start(AppContext context) throws Throwable { + context.beanMake(WxChannelMultiProperties.class); + + context.beanMake(WxChannelInJedisConfiguration.class); + context.beanMake(WxChannelInMemoryConfiguration.class); + context.beanMake(WxChannelInRedissonConfiguration.class); + } +} diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/properties/WxChannelMultiProperties.java b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/properties/WxChannelMultiProperties.java new file mode 100644 index 0000000000..ca99e522b9 --- /dev/null +++ b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/properties/WxChannelMultiProperties.java @@ -0,0 +1,96 @@ +package com.binarywang.solon.wxjava.channel.properties; + +import com.binarywang.solon.wxjava.channel.enums.HttpClientType; +import com.binarywang.solon.wxjava.channel.enums.StorageType; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * 微信多视频号接入相关配置属性 + * + * @author Winnie + * @date 2024/9/13 + */ +@Data +@NoArgsConstructor +@Configuration +@Inject("${" + WxChannelMultiProperties.PREFIX +"}") +public class WxChannelMultiProperties implements Serializable { + private static final long serialVersionUID = - 8361973118805546037L; + public static final String PREFIX = "wx.channel"; + + private Map apps = new HashMap<>(); + + /** + * 存储策略 + */ + private final ConfigStorage configStorage = new ConfigStorage(); + + @Data + @NoArgsConstructor + public static class ConfigStorage implements Serializable { + private static final long serialVersionUID = - 5152619132544179942L; + + /** + * 存储类型. + */ + private StorageType type = StorageType.MEMORY; + + /** + * 指定key前缀. + */ + private String keyPrefix = "wx:channel:multi"; + + /** + * redis连接配置. + */ + private final WxChannelMultiRedisProperties redis = new WxChannelMultiRedisProperties(); + + /** + * http客户端类型. + */ + private HttpClientType httpClientType = HttpClientType.HTTP_COMPONENTS; + + /** + * http代理主机. + */ + private String httpProxyHost; + + /** + * http代理端口. + */ + private Integer httpProxyPort; + + /** + * http代理用户名. + */ + private String httpProxyUsername; + + /** + * http代理密码. + */ + private String httpProxyPassword; + + /** + * http 请求最大重试次数 + * + *

{@link me.chanjar.weixin.channel.api.WxChannelService#setMaxRetryTimes(int)}

+ *

{@link me.chanjar.weixin.channel.api.impl.BaseWxChannelServiceImpl#setMaxRetryTimes(int)}

+ */ + private int maxRetryTimes = 5; + + /** + * http 请求重试间隔 + * + *

{@link me.chanjar.weixin.channel.api.WxChannelService#setRetrySleepMillis(int)}

+ *

{@link me.chanjar.weixin.channel.api.impl.BaseWxChannelServiceImpl#setRetrySleepMillis(int)}

+ */ + private int retrySleepMillis = 1000; + } +} diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/properties/WxChannelMultiRedisProperties.java b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/properties/WxChannelMultiRedisProperties.java new file mode 100644 index 0000000000..36c649b311 --- /dev/null +++ b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/properties/WxChannelMultiRedisProperties.java @@ -0,0 +1,63 @@ +package com.binarywang.solon.wxjava.channel.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * Redis配置 + * + * @author Winnie + * @date 2024/9/13 + */ +@Data +@NoArgsConstructor +public class WxChannelMultiRedisProperties implements Serializable { + private static final long serialVersionUID = 9061055444734277357L; + + /** + * 主机地址. + */ + private String host = "127.0.0.1"; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + /** + * 最大活动连接数 + */ + private Integer maxActive; + + /** + * 最大空闲连接数 + */ + private Integer maxIdle; + + /** + * 最小空闲连接数 + */ + private Integer minIdle; + + /** + * 最大等待时间 + */ + private Integer maxWaitMillis; +} diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/properties/WxChannelSingleProperties.java b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/properties/WxChannelSingleProperties.java new file mode 100644 index 0000000000..438c3ecb03 --- /dev/null +++ b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/properties/WxChannelSingleProperties.java @@ -0,0 +1,43 @@ +package com.binarywang.solon.wxjava.channel.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 微信视频号相关配置属性 + * + * @author Winnie + * @date 2024/9/13 + */ +@Data +@NoArgsConstructor +public class WxChannelSingleProperties implements Serializable { + private static final long serialVersionUID = 5306630351265124825L; + + /** + * 设置微信视频号的 appid. + */ + private String appId; + + /** + * 设置微信视频号的 secret. + */ + private String secret; + + /** + * 设置微信视频号的 token. + */ + private String token; + + /** + * 设置微信视频号的 EncodingAESKey. + */ + private String aesKey; + + /** + * 是否使用稳定版 Access Token + */ + private boolean useStableAccessToken = false; +} diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/service/WxChannelMultiServices.java b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/service/WxChannelMultiServices.java new file mode 100644 index 0000000000..f12461e197 --- /dev/null +++ b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/service/WxChannelMultiServices.java @@ -0,0 +1,26 @@ +package com.binarywang.solon.wxjava.channel.service; + +import me.chanjar.weixin.channel.api.WxChannelService; + +/** + * 视频号 {@link WxChannelService} 所有实例存放类. + * + * @author Winnie + * @date 2024/9/13 + */ +public interface WxChannelMultiServices { + /** + * 通过租户 Id 获取 WxChannelService + * + * @param tenantId 租户 Id + * @return WxChannelService + */ + WxChannelService getWxChannelService(String tenantId); + + /** + * 根据租户 Id,从列表中移除一个 WxChannelService 实例 + * + * @param tenantId 租户 Id + */ + void removeWxChannelService(String tenantId); +} diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/service/WxChannelMultiServicesImpl.java b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/service/WxChannelMultiServicesImpl.java new file mode 100644 index 0000000000..8420e29d73 --- /dev/null +++ b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/service/WxChannelMultiServicesImpl.java @@ -0,0 +1,36 @@ +package com.binarywang.solon.wxjava.channel.service; + +import me.chanjar.weixin.channel.api.WxChannelService; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 视频号 {@link WxChannelMultiServices} 实现 + * + * @author Winnie + * @date 2024/9/13 + */ +public class WxChannelMultiServicesImpl implements WxChannelMultiServices { + private final Map services = new ConcurrentHashMap<>(); + + @Override + public WxChannelService getWxChannelService(String tenantId) { + return this.services.get(tenantId); + } + + /** + * 根据租户 Id,添加一个 WxChannelService 到列表 + * + * @param tenantId 租户 Id + * @param wxChannelService WxChannelService 实例 + */ + public void addWxChannelService(String tenantId, WxChannelService wxChannelService) { + this.services.put(tenantId, wxChannelService); + } + + @Override + public void removeWxChannelService(String tenantId) { + this.services.remove(tenantId); + } +} diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-multi-channel-solon-plugin.properties b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-multi-channel-solon-plugin.properties new file mode 100644 index 0000000000..b9fc24b210 --- /dev/null +++ b/solon-plugins/wx-java-channel-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-multi-channel-solon-plugin.properties @@ -0,0 +1,2 @@ +solon.plugin=com.binarywang.solon.wxjava.channel.integration.WxChannelMultiPluginImpl +solon.plugin.priority=10 diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-channel-multi-solon-plugin/src/test/java/features/test/LoadTest.java new file mode 100644 index 0000000000..d049f5a51a --- /dev/null +++ b/solon-plugins/wx-java-channel-multi-solon-plugin/src/test/java/features/test/LoadTest.java @@ -0,0 +1,15 @@ +package features.test; + +import org.junit.jupiter.api.Test; +import org.noear.solon.test.SolonTest; + +/** + * @author noear 2024/9/4 created + */ +@SolonTest +public class LoadTest { + @Test + public void load(){ + + } +} diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/src/test/resources/app.properties b/solon-plugins/wx-java-channel-multi-solon-plugin/src/test/resources/app.properties new file mode 100644 index 0000000000..c90a560a82 --- /dev/null +++ b/solon-plugins/wx-java-channel-multi-solon-plugin/src/test/resources/app.properties @@ -0,0 +1,36 @@ +# 视频号配置 +## 应用 1 配置(必填) +wx.channel.apps.tenantId1.app-id=appId +wx.channel.apps.tenantId1.secret=secret +## 选填 +wx.channel.apps.tenantId1.use-stable-access-token=false +wx.channel.apps.tenantId1.token= +wx.channel.apps.tenantId1.aes-key= +## 应用 2 配置(必填) +wx.channel.apps.tenantId2.app-id=@appId +wx.channel.apps.tenantId2.secret=@secret +## 选填 +wx.channel.apps.tenantId2.use-stable-access-token=false +wx.channel.apps.tenantId2.token= +wx.channel.apps.tenantId2.aes-key= + +# ConfigStorage 配置(选填) +## 配置类型: memory(默认), jedis, redisson, redis_template +wx.channel.config-storage.type=memory +## 相关redis前缀配置: wx:channel:multi(默认) +wx.channel.config-storage.key-prefix=wx:channel:multi +wx.channel.config-storage.redis.host=127.0.0.1 +wx.channel.config-storage.redis.port=6379 +wx.channel.config-storage.redis.password=123456 + +# http 客户端配置(选填) +## # http客户端类型: http_client(默认) +wx.channel.config-storage.http-client-type=http_client +wx.channel.config-storage.http-proxy-host= +wx.channel.config-storage.http-proxy-port= +wx.channel.config-storage.http-proxy-username= +wx.channel.config-storage.http-proxy-password= +## 最大重试次数,默认:5 次,如果小于 0,则为 0 +wx.channel.config-storage.max-retry-times=5 +## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000 +wx.channel.config-storage.retry-sleep-millis=1000 diff --git a/solon-plugins/wx-java-channel-solon-plugin/README.md b/solon-plugins/wx-java-channel-solon-plugin/README.md new file mode 100644 index 0000000000..a7168a8edc --- /dev/null +++ b/solon-plugins/wx-java-channel-solon-plugin/README.md @@ -0,0 +1,92 @@ +# wx-java-channel-solon-plugin + +## 快速开始 +1. 引入依赖 + ```xml + + + com.github.binarywang + wx-java-channel-solon-plugin + ${version} + + + + + redis.clients + jedis + ${jedis.version} + + + + + org.redisson + redisson + ${redisson.version} + + + ``` +2. 添加配置(app.properties) + ```properties + # 视频号配置(必填) + ## 视频号小店的appId和secret + wx.channel.app-id=@appId + wx.channel.secret=@secret + # 视频号配置 选填 + ## 设置视频号小店消息服务器配置的token + wx.channel.token=@token + ## 设置视频号小店消息服务器配置的EncodingAESKey + wx.channel.aes-key= + ## 支持JSON或者XML格式,默认JSON + wx.channel.msg-data-format=JSON + ## 是否使用稳定版 Access Token + wx.channel.use-stable-access-token=false + + + # ConfigStorage 配置(选填) + ## 配置类型: memory(默认), jedis, redisson, redis_template + wx.channel.config-storage.type=memory + ## 相关redis前缀配置: wx:channel(默认) + wx.channel.config-storage.key-prefix=wx:channel + wx.channel.config-storage.redis.host=127.0.0.1 + wx.channel.config-storage.redis.port=6379 + wx.channel.config-storage.redis.password=123456 + + + # http 客户端配置(选填) + ## # http客户端类型: http_client(默认) + wx.channel.config-storage.http-client-type=http_client + wx.channel.config-storage.http-proxy-host= + wx.channel.config-storage.http-proxy-port= + wx.channel.config-storage.http-proxy-username= + wx.channel.config-storage.http-proxy-password= + ## 最大重试次数,默认:5 次,如果小于 0,则为 0 + wx.channel.config-storage.max-retry-times=5 + ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000 + wx.channel.config-storage.retry-sleep-millis=1000 + ``` +3. 自动注入的类型 +- `WxChannelService` +- `WxChannelConfig` +4. 使用样例 + +```java +import me.chanjar.weixin.channel.api.WxChannelService; +import me.chanjar.weixin.channel.bean.shop.ShopInfoResponse; +import me.chanjar.weixin.channel.util.JsonUtils; +import me.chanjar.weixin.common.error.WxErrorException; +import org.noear.solon.annotation.Inject; + +@Component +public class DemoService { + @Inject + private WxChannelService wxChannelService; + + public String getShopInfo() throws WxErrorException { + // 获取店铺基本信息 + ShopInfoResponse response = wxChannelService.getBasicService().getShopInfo(); + // 此处为演示,如果要返回response的结果,建议自己封装一个VO,避免直接返回response + return JsonUtils.encode(response); + } +} +``` + diff --git a/solon-plugins/wx-java-channel-solon-plugin/pom.xml b/solon-plugins/wx-java-channel-solon-plugin/pom.xml new file mode 100644 index 0000000000..a26072f8c4 --- /dev/null +++ b/solon-plugins/wx-java-channel-solon-plugin/pom.xml @@ -0,0 +1,31 @@ + + + wx-java-solon-plugins + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-channel-solon-plugin + WxJava - Solon Plugin for Channel + 微信视频号开发的 Solon Plugin + + + + com.github.binarywang + weixin-java-channel + ${project.version} + + + redis.clients + jedis + provided + + + org.redisson + redisson + provided + + + diff --git a/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/config/WxChannelServiceAutoConfiguration.java b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/config/WxChannelServiceAutoConfiguration.java new file mode 100644 index 0000000000..9ffccc64bf --- /dev/null +++ b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/config/WxChannelServiceAutoConfiguration.java @@ -0,0 +1,35 @@ +package com.binarywang.solon.wxjava.channel.config; + + +import com.binarywang.solon.wxjava.channel.properties.WxChannelProperties; +import lombok.AllArgsConstructor; +import me.chanjar.weixin.channel.api.WxChannelService; +import me.chanjar.weixin.channel.api.impl.WxChannelServiceImpl; +import me.chanjar.weixin.channel.config.WxChannelConfig; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +/** + * 微信小程序平台相关服务自动注册 + * + * @author Zeyes + */ +@Configuration +@AllArgsConstructor +public class WxChannelServiceAutoConfiguration { + private final WxChannelProperties properties; + + /** + * Channel Service + * + * @return Channel Service + */ + @Bean + @Condition(onMissingBean=WxChannelService.class, onBean = WxChannelConfig.class) + public WxChannelService wxChannelService(WxChannelConfig wxChannelConfig) { + WxChannelService wxChannelService = new WxChannelServiceImpl(); + wxChannelService.setConfig(wxChannelConfig); + return wxChannelService; + } +} diff --git a/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/config/storage/AbstractWxChannelConfigStorageConfiguration.java b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/config/storage/AbstractWxChannelConfigStorageConfiguration.java new file mode 100644 index 0000000000..2df3dbf23f --- /dev/null +++ b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/config/storage/AbstractWxChannelConfigStorageConfiguration.java @@ -0,0 +1,40 @@ +package com.binarywang.solon.wxjava.channel.config.storage; + +import com.binarywang.solon.wxjava.channel.properties.WxChannelProperties; +import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl; +import org.apache.commons.lang3.StringUtils; + +/** + * @author Zeyes + */ +public abstract class AbstractWxChannelConfigStorageConfiguration { + + protected WxChannelDefaultConfigImpl config(WxChannelDefaultConfigImpl config, WxChannelProperties properties) { + config.setAppid(StringUtils.trimToNull(properties.getAppid())); + config.setSecret(StringUtils.trimToNull(properties.getSecret())); + config.setToken(StringUtils.trimToNull(properties.getToken())); + config.setAesKey(StringUtils.trimToNull(properties.getAesKey())); + config.setMsgDataFormat(StringUtils.trimToNull(properties.getMsgDataFormat())); + config.setStableAccessToken(properties.isUseStableAccessToken()); + + WxChannelProperties.ConfigStorage configStorageProperties = properties.getConfigStorage(); + config.setHttpProxyHost(configStorageProperties.getHttpProxyHost()); + config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername()); + config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword()); + if (configStorageProperties.getHttpProxyPort() != null) { + config.setHttpProxyPort(configStorageProperties.getHttpProxyPort()); + } + + int maxRetryTimes = configStorageProperties.getMaxRetryTimes(); + if (configStorageProperties.getMaxRetryTimes() < 0) { + maxRetryTimes = 0; + } + int retrySleepMillis = configStorageProperties.getRetrySleepMillis(); + if (retrySleepMillis < 0) { + retrySleepMillis = 1000; + } + config.setRetrySleepMillis(retrySleepMillis); + config.setMaxRetryTimes(maxRetryTimes); + return config; + } +} diff --git a/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/config/storage/WxChannelInJedisConfigStorageConfiguration.java b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/config/storage/WxChannelInJedisConfigStorageConfiguration.java new file mode 100644 index 0000000000..f074241914 --- /dev/null +++ b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/config/storage/WxChannelInJedisConfigStorageConfiguration.java @@ -0,0 +1,74 @@ +package com.binarywang.solon.wxjava.channel.config.storage; + + +import com.binarywang.solon.wxjava.channel.properties.RedisProperties; +import com.binarywang.solon.wxjava.channel.properties.WxChannelProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.channel.config.WxChannelConfig; +import me.chanjar.weixin.channel.config.impl.WxChannelRedisConfigImpl; +import me.chanjar.weixin.common.redis.JedisWxRedisOps; +import me.chanjar.weixin.common.redis.WxRedisOps; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * @author Zeyes + * @author noear + */ +@Configuration +@Condition( + onProperty = "${"+WxChannelProperties.PREFIX + ".configStorage.type} = jedis", + onClass = JedisPool.class +) +@RequiredArgsConstructor +public class WxChannelInJedisConfigStorageConfiguration extends AbstractWxChannelConfigStorageConfiguration { + private final WxChannelProperties properties; + private final AppContext applicationContext; + + @Bean + @Condition(onMissingBean=WxChannelConfig.class) + public WxChannelConfig wxChannelConfig() { + WxChannelRedisConfigImpl config = getWxChannelRedisConfig(); + return this.config(config, properties); + } + + private WxChannelRedisConfigImpl getWxChannelRedisConfig() { + RedisProperties redisProperties = properties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + jedisPool = getJedisPool(); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + WxRedisOps redisOps = new JedisWxRedisOps(jedisPool); + return new WxChannelRedisConfigImpl(redisOps, properties.getConfigStorage().getKeyPrefix()); + } + + private JedisPool getJedisPool() { + WxChannelProperties.ConfigStorage storage = properties.getConfigStorage(); + RedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(), redis.getDatabase()); + } +} diff --git a/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/config/storage/WxChannelInMemoryConfigStorageConfiguration.java b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/config/storage/WxChannelInMemoryConfigStorageConfiguration.java new file mode 100644 index 0000000000..a560db29ac --- /dev/null +++ b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/config/storage/WxChannelInMemoryConfigStorageConfiguration.java @@ -0,0 +1,29 @@ +package com.binarywang.solon.wxjava.channel.config.storage; + + +import com.binarywang.solon.wxjava.channel.properties.WxChannelProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.channel.config.WxChannelConfig; +import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +/** + * @author Zeyes + */ +@Configuration +@Condition( + onProperty = "${"+WxChannelProperties.PREFIX + ".configStorage.type:memory} = memory" +) +@RequiredArgsConstructor +public class WxChannelInMemoryConfigStorageConfiguration extends AbstractWxChannelConfigStorageConfiguration { + private final WxChannelProperties properties; + + @Bean + @Condition(onMissingBean = WxChannelProperties.class) + public WxChannelConfig wxChannelConfig() { + WxChannelDefaultConfigImpl config = new WxChannelDefaultConfigImpl(); + return this.config(config, properties); + } +} diff --git a/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/config/storage/WxChannelInRedissonConfigStorageConfiguration.java b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/config/storage/WxChannelInRedissonConfigStorageConfiguration.java new file mode 100644 index 0000000000..cd4de68f21 --- /dev/null +++ b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/config/storage/WxChannelInRedissonConfigStorageConfiguration.java @@ -0,0 +1,62 @@ +package com.binarywang.solon.wxjava.channel.config.storage; + + +import com.binarywang.solon.wxjava.channel.properties.RedisProperties; +import com.binarywang.solon.wxjava.channel.properties.WxChannelProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.channel.config.WxChannelConfig; +import me.chanjar.weixin.channel.config.impl.WxChannelRedissonConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; + +/** + * @author Zeyes + */ +@Configuration +@Condition( + onProperty = "${"+WxChannelProperties.PREFIX + ".configStorage.type} = redisson", + onClass = Redisson.class +) +@RequiredArgsConstructor +public class WxChannelInRedissonConfigStorageConfiguration extends AbstractWxChannelConfigStorageConfiguration { + private final WxChannelProperties properties; + private final AppContext applicationContext; + + @Bean + @Condition(onMissingBean=WxChannelConfig.class) + public WxChannelConfig wxChannelConfig() { + WxChannelRedissonConfigImpl config = getWxChannelRedissonConfig(); + return this.config(config, properties); + } + + private WxChannelRedissonConfigImpl getWxChannelRedissonConfig() { + RedisProperties redisProperties = properties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + redissonClient = getRedissonClient(); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxChannelRedissonConfigImpl(redissonClient, properties.getConfigStorage().getKeyPrefix()); + } + + private RedissonClient getRedissonClient() { + WxChannelProperties.ConfigStorage storage = properties.getConfigStorage(); + RedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redis.getHost() + ":" + redis.getPort()) + .setDatabase(redis.getDatabase()) + .setPassword(redis.getPassword()); + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/enums/HttpClientType.java b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/enums/HttpClientType.java new file mode 100644 index 0000000000..5614f63e86 --- /dev/null +++ b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/enums/HttpClientType.java @@ -0,0 +1,17 @@ +package com.binarywang.solon.wxjava.channel.enums; + +/** + * httpclient类型 + * + * @author Zeyes + */ +public enum HttpClientType { + /** + * HttpClient. + */ + HttpClient, + /** + * HttpComponents. + */ + HttpComponents, +} diff --git a/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/enums/StorageType.java b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/enums/StorageType.java new file mode 100644 index 0000000000..976f869438 --- /dev/null +++ b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/enums/StorageType.java @@ -0,0 +1,25 @@ +package com.binarywang.solon.wxjava.channel.enums; + +/** + * storage类型 + * + * @author Zeyes + */ +public enum StorageType { + /** + * 内存 + */ + Memory, + /** + * redis(JedisClient) + */ + Jedis, + /** + * redis(Redisson) + */ + Redisson, + /** + * redis(RedisTemplate) + */ + RedisTemplate +} diff --git a/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/integration/WxChannelPluginImpl.java b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/integration/WxChannelPluginImpl.java new file mode 100644 index 0000000000..0377bc6f41 --- /dev/null +++ b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/integration/WxChannelPluginImpl.java @@ -0,0 +1,25 @@ +package com.binarywang.solon.wxjava.channel.integration; + + +import com.binarywang.solon.wxjava.channel.config.WxChannelServiceAutoConfiguration; +import com.binarywang.solon.wxjava.channel.config.storage.WxChannelInJedisConfigStorageConfiguration; +import com.binarywang.solon.wxjava.channel.config.storage.WxChannelInMemoryConfigStorageConfiguration; +import com.binarywang.solon.wxjava.channel.config.storage.WxChannelInRedissonConfigStorageConfiguration; +import com.binarywang.solon.wxjava.channel.properties.WxChannelProperties; +import org.noear.solon.core.AppContext; +import org.noear.solon.core.Plugin; + +/** + * @author noear 2024/9/2 created + */ +public class WxChannelPluginImpl implements Plugin { + @Override + public void start(AppContext context) throws Throwable { + context.beanMake(WxChannelProperties.class); + context.beanMake(WxChannelServiceAutoConfiguration.class); + + context.beanMake(WxChannelInMemoryConfigStorageConfiguration.class); + context.beanMake(WxChannelInJedisConfigStorageConfiguration.class); + context.beanMake(WxChannelInRedissonConfigStorageConfiguration.class); + } +} diff --git a/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/properties/RedisProperties.java b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/properties/RedisProperties.java new file mode 100644 index 0000000000..b74ad89f4e --- /dev/null +++ b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/properties/RedisProperties.java @@ -0,0 +1,42 @@ +package com.binarywang.solon.wxjava.channel.properties; + +import lombok.Data; + +/** + * redis 配置 + * + * @author Zeyes + */ +@Data +public class RedisProperties { + + /** + * 主机地址,不填则从solon容器内获取JedisPool + */ + private String host; + + /** + * 端口号 + */ + private int port = 6379; + + /** + * 密码 + */ + private String password; + + /** + * 超时 + */ + private int timeout = 2000; + + /** + * 数据库 + */ + private int database = 0; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/properties/WxChannelProperties.java b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/properties/WxChannelProperties.java new file mode 100644 index 0000000000..89b81b7d9f --- /dev/null +++ b/solon-plugins/wx-java-channel-solon-plugin/src/main/java/com/binarywang/solon/wxjava/channel/properties/WxChannelProperties.java @@ -0,0 +1,114 @@ +package com.binarywang.solon.wxjava.channel.properties; + +import com.binarywang.solon.wxjava.channel.enums.HttpClientType; +import com.binarywang.solon.wxjava.channel.enums.StorageType; +import lombok.Data; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +/** + * 属性配置类 + * + * @author Zeyes + */ +@Data +@Configuration +@Inject("${" + WxChannelProperties.PREFIX +"}") +public class WxChannelProperties { + public static final String PREFIX = "wx.channel"; + + /** + * 设置视频号小店的appid + */ + private String appid; + + /** + * 设置视频号小店的Secret + */ + private String secret; + + /** + * 设置视频号小店消息服务器配置的token. + */ + private String token; + + /** + * 设置视频号小店消息服务器配置的EncodingAESKey + */ + private String aesKey; + + /** + * 消息格式,XML或者JSON + */ + private String msgDataFormat = "JSON"; + + /** + * 是否使用稳定版 Access Token + */ + private boolean useStableAccessToken = false; + + /** + * 存储策略 + */ + private final ConfigStorage configStorage = new ConfigStorage(); + + @Data + public static class ConfigStorage { + + /** + * 存储类型 + */ + private StorageType type = StorageType.Memory; + + /** + * 指定key前缀 + */ + private String keyPrefix = "wh"; + + /** + * redis连接配置 + */ + private final RedisProperties redis = new RedisProperties(); + + /** + * http客户端类型 + */ + private HttpClientType httpClientType = HttpClientType.HttpComponents; + + /** + * http代理主机 + */ + private String httpProxyHost; + + /** + * http代理端口 + */ + private Integer httpProxyPort; + + /** + * http代理用户名 + */ + private String httpProxyUsername; + + /** + * http代理密码 + */ + private String httpProxyPassword; + + /** + * http 请求重试间隔 + *
+     *   {@link me.chanjar.weixin.channel.api.BaseWxChannelService#setRetrySleepMillis(int)}
+     * 
+ */ + private int retrySleepMillis = 1000; + /** + * http 请求最大重试次数 + *
+     *   {@link me.chanjar.weixin.channel.api.BaseWxChannelService#setMaxRetryTimes(int)}
+     * 
+ */ + private int maxRetryTimes = 5; + } + +} diff --git a/solon-plugins/wx-java-channel-solon-plugin/src/main/resources/META-INF/solon/wx-java-channel-solon-plugin.properties b/solon-plugins/wx-java-channel-solon-plugin/src/main/resources/META-INF/solon/wx-java-channel-solon-plugin.properties new file mode 100644 index 0000000000..d8ec8f5112 --- /dev/null +++ b/solon-plugins/wx-java-channel-solon-plugin/src/main/resources/META-INF/solon/wx-java-channel-solon-plugin.properties @@ -0,0 +1,2 @@ +solon.plugin=com.binarywang.solon.wxjava.channel.integration.WxChannelPluginImpl +solon.plugin.priority=10 diff --git a/solon-plugins/wx-java-channel-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-channel-solon-plugin/src/test/java/features/test/LoadTest.java new file mode 100644 index 0000000000..d049f5a51a --- /dev/null +++ b/solon-plugins/wx-java-channel-solon-plugin/src/test/java/features/test/LoadTest.java @@ -0,0 +1,15 @@ +package features.test; + +import org.junit.jupiter.api.Test; +import org.noear.solon.test.SolonTest; + +/** + * @author noear 2024/9/4 created + */ +@SolonTest +public class LoadTest { + @Test + public void load(){ + + } +} diff --git a/solon-plugins/wx-java-channel-solon-plugin/src/test/resources/app.yml b/solon-plugins/wx-java-channel-solon-plugin/src/test/resources/app.yml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/README.md b/solon-plugins/wx-java-cp-multi-solon-plugin/README.md new file mode 100644 index 0000000000..8eb467f98f --- /dev/null +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/README.md @@ -0,0 +1,108 @@ +# wx-java-cp-multi-solon-plugin + +企业微信多账号配置 + +- 实现多 WxCpService 初始化。 +- 未实现 WxCpTpService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。 +- 未实现 WxCpCgService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。 + +## 关于 corp-secret 的说明 + +企业微信中不同功能模块对应不同的 `corp-secret`,每种 Secret 只对对应模块的接口具有调用权限: + +| Secret 类型 | 获取位置 | 可调用的接口 | 是否需要 agent-id | +|---|---|---|---| +| 自建应用 Secret | 应用管理 → 自建应用 → 选择应用 → 查看 Secret | 该应用有权限的接口 | **必填** | +| 通讯录同步 Secret | 管理工具 → 通讯录同步 → 查看 Secret | 部门/成员增删改查等通讯录接口 | **不填** | +| 客户联系 Secret | 客户联系 → API → Secret | 客户联系相关接口 | 不填 | + +> **常见问题**: +> - 使用自建应用 Secret + agent-id 可以获取部门列表,但**无法更新部门**(因为写接口需要通讯录同步权限) +> - 使用通讯录同步 Secret 可以同步部门,但**调用某些需要 agent-id 的应用接口会报错** + +如需同时使用多种权限范围,可在 `wx.cp.corps` 下配置多个条目,每个条目使用对应权限的 Secret,通过不同的 `tenantId` 区分后使用。 + +> **注意**: +> 当前插件实现会校验同一 `corp-id` 下的 `agent-id` **必须唯一**,并且 **只能有一个条目不填写 `agent-id`**。 +> 如果在同一 `corp-id` 下同时配置多个未填写 `agent-id` 的条目,会因 token/ticket 缓存 key 冲突而在启动时直接抛异常。 +## 快速开始 + +1. 引入依赖 + ```xml + + com.github.binarywang + wx-java-cp-multi-solon-plugin + ${version} + + ``` +2. 添加配置(app.properties) + ```properties + # 自建应用 1 配置(使用自建应用 Secret,需填写 agent-id) + wx.cp.corps.app1.corp-id = @corp-id + wx.cp.corps.app1.corp-secret = @自建应用的Secret(在"应用管理-自建应用"中查看) + wx.cp.corps.app1.agent-id = @自建应用的AgentId + ## 选填 + wx.cp.corps.app1.token = @token + wx.cp.corps.app1.aes-key = @aes-key + wx.cp.corps.app1.msg-audit-priKey = @msg-audit-priKey + wx.cp.corps.app1.msg-audit-lib-path = @msg-audit-lib-path + + # 通讯录同步配置(使用通讯录同步 Secret,不需要填写 agent-id) + # 此配置用于部门、成员的增删改查等通讯录管理操作 + wx.cp.corps.contact.corp-id = @corp-id + wx.cp.corps.contact.corp-secret = @通讯录同步的Secret(在"管理工具-通讯录同步"中查看) + ## agent-id 不填,通讯录同步不需要 agentId + + # 公共配置 + ## ConfigStorage 配置(选填) + wx.cp.config-storage.type=memory # 配置类型: memory(默认), jedis, redisson, redistemplate + ## http 客户端配置(选填) + ## # http客户端类型: http_client(默认), ok_http, jodd_http + wx.cp.config-storage.http-client-type=http_client + wx.cp.config-storage.http-proxy-host= + wx.cp.config-storage.http-proxy-port= + wx.cp.config-storage.http-proxy-username= + wx.cp.config-storage.http-proxy-password= + ## 最大重试次数,默认:5 次,如果小于 0,则为 0 + wx.cp.config-storage.max-retry-times=5 + ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000 + wx.cp.config-storage.retry-sleep-millis=1000 + ``` +3. 支持自动注入的类型: `WxCpMultiServices` + +4. 使用样例 + +```java +import com.binarywang.solon.wxjava.cp_multi.service.WxCpMultiServices; +import me.chanjar.weixin.cp.api.WxCpDepartmentService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.api.WxCpUserService; +import me.chanjar.weixin.cp.bean.WxCpDepart; +import org.noear.solon.annotation.Component; +import org.noear.solon.annotation.Inject; + +@Component +public class DemoService { + @Inject + private WxCpMultiServices wxCpMultiServices; + + public void test() { + // 使用自建应用的 WxCpService(对应 corp-secret 为自建应用 Secret) + WxCpService appService = wxCpMultiServices.getWxCpService("app1"); + WxCpUserService userService = appService.getUserService(); + userService.getUserId("xxx"); + // todo ... + + // 使用通讯录同步的 WxCpService(对应 corp-secret 为通讯录同步 Secret) + // 通讯录同步 Secret 具有部门/成员增删改查等权限 + WxCpService contactService = wxCpMultiServices.getWxCpService("contact"); + WxCpDepartmentService departmentService = contactService.getDepartmentService(); + // 更新部门示例(WxCpDepart 包含 id、name、parentId 等字段) + WxCpDepart depart = new WxCpDepart(); + depart.setId(100L); + depart.setName("新部门名称"); + departmentService.update(depart); + // todo ... + } +} +``` diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml new file mode 100644 index 0000000000..9ccd05578b --- /dev/null +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml @@ -0,0 +1,32 @@ + + + + wx-java-solon-plugins + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-cp-multi-solon-plugin + WxJava - Solon Plugin for WxCp::支持多账号配置 + 微信企业号开发的 Solon Plugin::支持多账号配置 + + + + com.github.binarywang + weixin-java-cp + ${project.version} + + + redis.clients + jedis + provided + + + org.redisson + redisson + provided + + + diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/AbstractWxCpConfiguration.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/AbstractWxCpConfiguration.java new file mode 100644 index 0000000000..25b4ab3747 --- /dev/null +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/AbstractWxCpConfiguration.java @@ -0,0 +1,170 @@ +package com.binarywang.solon.wxjava.cp_multi.configuration.services; + +import com.binarywang.solon.wxjava.cp_multi.properties.WxCpMultiProperties; +import com.binarywang.solon.wxjava.cp_multi.properties.WxCpSingleProperties; +import com.binarywang.solon.wxjava.cp_multi.service.WxCpMultiServices; +import com.binarywang.solon.wxjava.cp_multi.service.WxCpMultiServicesImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.api.impl.*; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * WxCpConfigStorage 抽象配置类 + * + * @author yl + * created on 2023/10/16 + */ +@RequiredArgsConstructor +@Slf4j +public abstract class AbstractWxCpConfiguration { + + protected WxCpMultiServices wxCpMultiServices(WxCpMultiProperties wxCpMultiProperties) { + Map corps = wxCpMultiProperties.getCorps(); + if (corps == null || corps.isEmpty()) { + log.warn("企业微信应用参数未配置,通过 WxCpMultiServices#getWxCpService(\"tenantId\")获取实例将返回空"); + return new WxCpMultiServicesImpl(); + } + /** + * 校验同一个企业下,agentId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。 + * + *

同一企业(corpId 相同)下可配置多个条目以使用不同的权限 Secret,例如:

+ *
    + *
  • 自建应用条目:填写应用对应的 corpSecret 和 agentId
  • + *
  • 通讯录同步条目:填写通讯录同步 Secret,agentId 可不填(null)
  • + *
+ *

但同一 corpId 下不允许出现重复的 agentId(包括多个 null)。

+ * + * 查看 {@link me.chanjar.weixin.cp.config.impl.AbstractWxCpInRedisConfigImpl#setAgentId(Integer)} + */ + Collection corpList = corps.values(); + if (corpList.size() > 1) { + // 先按 corpId 分组统计 + Map> corpsMap = corpList.stream() + .collect(Collectors.groupingBy(WxCpSingleProperties::getCorpId)); + Set>> entries = corpsMap.entrySet(); + for (Map.Entry> entry : entries) { + String corpId = entry.getKey(); + // 校验每个企业下,agentId 是否唯一 + boolean multi = entry.getValue().stream() + // 通讯录没有 agentId,使用字符串转换避免 null 与 agentId=0 冲突 + .collect(Collectors.groupingBy(c -> Objects.toString(c.getAgentId(), "null"), Collectors.counting())) + .entrySet().stream().anyMatch(e -> e.getValue() > 1); + if (multi) { + throw new RuntimeException("请确保企业微信配置唯一性[" + corpId + "]"); + } + } + } + WxCpMultiServicesImpl services = new WxCpMultiServicesImpl(); + + Set> entries = corps.entrySet(); + for (Map.Entry entry : entries) { + String tenantId = entry.getKey(); + WxCpSingleProperties wxCpSingleProperties = entry.getValue(); + WxCpDefaultConfigImpl storage = this.wxCpConfigStorage(wxCpMultiProperties); + this.configCorp(storage, wxCpSingleProperties); + this.configHttp(storage, wxCpMultiProperties.getConfigStorage()); + WxCpService wxCpService = this.wxCpService(storage, wxCpMultiProperties.getConfigStorage()); + services.addWxCpService(tenantId, wxCpService); + } + return services; + } + + /** + * 配置 WxCpDefaultConfigImpl + * + * @param wxCpMultiProperties 参数 + * @return WxCpDefaultConfigImpl + */ + protected abstract WxCpDefaultConfigImpl wxCpConfigStorage(WxCpMultiProperties wxCpMultiProperties); + + private WxCpService wxCpService(WxCpConfigStorage wxCpConfigStorage, WxCpMultiProperties.ConfigStorage storage) { + WxCpMultiProperties.HttpClientType httpClientType = storage.getHttpClientType(); + WxCpService wxCpService; + switch (httpClientType) { + case OK_HTTP: + wxCpService = new WxCpServiceOkHttpImpl(); + break; + case JODD_HTTP: + wxCpService = new WxCpServiceJoddHttpImpl(); + break; + case HTTP_CLIENT: + wxCpService = new WxCpServiceApacheHttpClientImpl(); + break; + case HTTP_COMPONENTS: + wxCpService = new WxCpServiceHttpComponentsImpl(); + break; + default: + wxCpService = new WxCpServiceImpl(); + break; + } + wxCpService.setWxCpConfigStorage(wxCpConfigStorage); + int maxRetryTimes = storage.getMaxRetryTimes(); + if (maxRetryTimes < 0) { + maxRetryTimes = 0; + } + int retrySleepMillis = storage.getRetrySleepMillis(); + if (retrySleepMillis < 0) { + retrySleepMillis = 1000; + } + wxCpService.setRetrySleepMillis(retrySleepMillis); + wxCpService.setMaxRetryTimes(maxRetryTimes); + return wxCpService; + } + + private void configCorp(WxCpDefaultConfigImpl config, WxCpSingleProperties wxCpSingleProperties) { + String corpId = wxCpSingleProperties.getCorpId(); + String corpSecret = wxCpSingleProperties.getCorpSecret(); + Integer agentId = wxCpSingleProperties.getAgentId(); + String token = wxCpSingleProperties.getToken(); + String aesKey = wxCpSingleProperties.getAesKey(); + // 企业微信,私钥,会话存档路径 + String msgAuditPriKey = wxCpSingleProperties.getMsgAuditPriKey(); + String msgAuditLibPath = wxCpSingleProperties.getMsgAuditLibPath(); + + config.setCorpId(corpId); + config.setCorpSecret(corpSecret); + config.setAgentId(agentId); + if (StringUtils.isNotBlank(token)) { + config.setToken(token); + } + if (StringUtils.isNotBlank(aesKey)) { + config.setAesKey(aesKey); + } + if (StringUtils.isNotBlank(msgAuditPriKey)) { + config.setMsgAuditPriKey(msgAuditPriKey); + } + if (StringUtils.isNotBlank(msgAuditLibPath)) { + config.setMsgAuditLibPath(msgAuditLibPath); + } + } + + private void configHttp(WxCpDefaultConfigImpl config, WxCpMultiProperties.ConfigStorage storage) { + String httpProxyHost = storage.getHttpProxyHost(); + Integer httpProxyPort = storage.getHttpProxyPort(); + String httpProxyUsername = storage.getHttpProxyUsername(); + String httpProxyPassword = storage.getHttpProxyPassword(); + if (StringUtils.isNotBlank(httpProxyHost)) { + config.setHttpProxyHost(httpProxyHost); + if (httpProxyPort != null) { + config.setHttpProxyPort(httpProxyPort); + } + if (StringUtils.isNotBlank(httpProxyUsername)) { + config.setHttpProxyUsername(httpProxyUsername); + } + if (StringUtils.isNotBlank(httpProxyPassword)) { + config.setHttpProxyPassword(httpProxyPassword); + } + } + } +} diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/WxCpInJedisConfiguration.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/WxCpInJedisConfiguration.java new file mode 100644 index 0000000000..71f5fd6725 --- /dev/null +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/WxCpInJedisConfiguration.java @@ -0,0 +1,77 @@ +package com.binarywang.solon.wxjava.cp_multi.configuration.services; + +import com.binarywang.solon.wxjava.cp_multi.properties.WxCpMultiProperties; +import com.binarywang.solon.wxjava.cp_multi.properties.WxCpMultiRedisProperties; +import com.binarywang.solon.wxjava.cp_multi.service.WxCpMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import me.chanjar.weixin.cp.config.impl.WxCpJedisConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * 自动装配基于 jedis 策略配置 + * + * @author yl + * created on 2023/10/16 + */ +@Configuration +@Condition( + onProperty = "${"+WxCpMultiProperties.PREFIX + ".configStorage.type} = jedis", + onClass = JedisPool.class +) +@RequiredArgsConstructor +public class WxCpInJedisConfiguration extends AbstractWxCpConfiguration { + private final WxCpMultiProperties wxCpMultiProperties; + private final AppContext applicationContext; + + @Bean + public WxCpMultiServices wxCpMultiServices() { + return this.wxCpMultiServices(wxCpMultiProperties); + } + + @Override + protected WxCpDefaultConfigImpl wxCpConfigStorage(WxCpMultiProperties wxCpMultiProperties) { + return this.configRedis(wxCpMultiProperties); + } + + private WxCpDefaultConfigImpl configRedis(WxCpMultiProperties wxCpMultiProperties) { + WxCpMultiRedisProperties wxCpMultiRedisProperties = wxCpMultiProperties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (wxCpMultiRedisProperties != null && StringUtils.isNotEmpty(wxCpMultiRedisProperties.getHost())) { + jedisPool = getJedisPool(wxCpMultiProperties); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + return new WxCpJedisConfigImpl(jedisPool, wxCpMultiProperties.getConfigStorage().getKeyPrefix()); + } + + private JedisPool getJedisPool(WxCpMultiProperties wxCpMultiProperties) { + WxCpMultiProperties.ConfigStorage storage = wxCpMultiProperties.getConfigStorage(); + WxCpMultiRedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), + redis.getTimeout(), redis.getPassword(), redis.getDatabase()); + } +} diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/WxCpInMemoryConfiguration.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/WxCpInMemoryConfiguration.java new file mode 100644 index 0000000000..3dfb36e258 --- /dev/null +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/WxCpInMemoryConfiguration.java @@ -0,0 +1,38 @@ +package com.binarywang.solon.wxjava.cp_multi.configuration.services; + +import com.binarywang.solon.wxjava.cp_multi.properties.WxCpMultiProperties; +import com.binarywang.solon.wxjava.cp_multi.service.WxCpMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +/** + * 自动装配基于内存策略配置 + * + * @author yl + * created on 2023/10/16 + */ +@Configuration +@Condition( + onProperty = "${"+WxCpMultiProperties.PREFIX + ".configStorage.type:memory} = memory" +) +@RequiredArgsConstructor +public class WxCpInMemoryConfiguration extends AbstractWxCpConfiguration { + private final WxCpMultiProperties wxCpMultiProperties; + + @Bean + public WxCpMultiServices wxCpMultiServices() { + return this.wxCpMultiServices(wxCpMultiProperties); + } + + @Override + protected WxCpDefaultConfigImpl wxCpConfigStorage(WxCpMultiProperties wxCpMultiProperties) { + return this.configInMemory(); + } + + private WxCpDefaultConfigImpl configInMemory() { + return new WxCpDefaultConfigImpl(); + } +} diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/WxCpInRedissonConfiguration.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/WxCpInRedissonConfiguration.java new file mode 100644 index 0000000000..6700570af8 --- /dev/null +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/configuration/services/WxCpInRedissonConfiguration.java @@ -0,0 +1,68 @@ +package com.binarywang.solon.wxjava.cp_multi.configuration.services; + +import com.binarywang.solon.wxjava.cp_multi.properties.WxCpMultiProperties; +import com.binarywang.solon.wxjava.cp_multi.properties.WxCpMultiRedisProperties; +import com.binarywang.solon.wxjava.cp_multi.service.WxCpMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import me.chanjar.weixin.cp.config.impl.WxCpRedissonConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; + +/** + * 自动装配基于 redisson 策略配置 + * + * @author yl + * created on 2023/10/16 + */ +@Configuration +@Condition( + onProperty = "${"+WxCpMultiProperties.PREFIX + ".configStorage.type} = redisson", + onClass = Redisson.class +) +@RequiredArgsConstructor +public class WxCpInRedissonConfiguration extends AbstractWxCpConfiguration { + private final WxCpMultiProperties wxCpMultiProperties; + private final AppContext applicationContext; + + @Bean + public WxCpMultiServices wxCpMultiServices() { + return this.wxCpMultiServices(wxCpMultiProperties); + } + + @Override + protected WxCpDefaultConfigImpl wxCpConfigStorage(WxCpMultiProperties wxCpMultiProperties) { + return this.configRedisson(wxCpMultiProperties); + } + + private WxCpDefaultConfigImpl configRedisson(WxCpMultiProperties wxCpMultiProperties) { + WxCpMultiRedisProperties redisProperties = wxCpMultiProperties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + redissonClient = getRedissonClient(wxCpMultiProperties); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxCpRedissonConfigImpl(redissonClient, wxCpMultiProperties.getConfigStorage().getKeyPrefix()); + } + + private RedissonClient getRedissonClient(WxCpMultiProperties wxCpMultiProperties) { + WxCpMultiProperties.ConfigStorage storage = wxCpMultiProperties.getConfigStorage(); + WxCpMultiRedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redis.getHost() + ":" + redis.getPort()) + .setDatabase(redis.getDatabase()) + .setPassword(redis.getPassword()); + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/integration/WxCpMultiPluginImpl.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/integration/WxCpMultiPluginImpl.java new file mode 100644 index 0000000000..b2a078c727 --- /dev/null +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/integration/WxCpMultiPluginImpl.java @@ -0,0 +1,21 @@ +package com.binarywang.solon.wxjava.cp_multi.integration; + +import com.binarywang.solon.wxjava.cp_multi.configuration.services.WxCpInJedisConfiguration; +import com.binarywang.solon.wxjava.cp_multi.configuration.services.WxCpInMemoryConfiguration; +import com.binarywang.solon.wxjava.cp_multi.configuration.services.WxCpInRedissonConfiguration; +import com.binarywang.solon.wxjava.cp_multi.properties.WxCpMultiProperties; +import org.noear.solon.core.AppContext; +import org.noear.solon.core.Plugin; + +/** + * @author noear 2024/9/2 created + */ +public class WxCpMultiPluginImpl implements Plugin { + @Override + public void start(AppContext context) throws Throwable { + context.beanMake(WxCpMultiProperties.class); + context.beanMake(WxCpInJedisConfiguration.class); + context.beanMake(WxCpInMemoryConfiguration.class); + context.beanMake(WxCpInRedissonConfiguration.class); + } +} diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpMultiProperties.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpMultiProperties.java new file mode 100644 index 0000000000..2d4bffae66 --- /dev/null +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpMultiProperties.java @@ -0,0 +1,133 @@ +package com.binarywang.solon.wxjava.cp_multi.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * 企业微信多企业接入相关配置属性 + * + * @author yl + * created on 2023/10/16 + */ +@Data +@NoArgsConstructor +@Configuration +@Inject("${" + WxCpMultiProperties.PREFIX + "}") +public class WxCpMultiProperties implements Serializable { + private static final long serialVersionUID = -1569510477055668503L; + public static final String PREFIX = "wx.cp"; + + private Map corps = new HashMap<>(); + + /** + * 配置存储策略,默认内存 + */ + private ConfigStorage configStorage = new ConfigStorage(); + + @Data + @NoArgsConstructor + public static class ConfigStorage implements Serializable { + private static final long serialVersionUID = 4815731027000065434L; + /** + * 存储类型 + */ + private StorageType type = StorageType.memory; + + /** + * 指定key前缀 + */ + private String keyPrefix = "wx:cp"; + + /** + * redis连接配置 + */ + private WxCpMultiRedisProperties redis = new WxCpMultiRedisProperties(); + + /** + * http客户端类型. + */ + private HttpClientType httpClientType = HttpClientType.HTTP_COMPONENTS; + + /** + * http代理主机 + */ + private String httpProxyHost; + + /** + * http代理端口 + */ + private Integer httpProxyPort; + + /** + * http代理用户名 + */ + private String httpProxyUsername; + + /** + * http代理密码 + */ + private String httpProxyPassword; + + /** + * http 请求最大重试次数 + *
+     *   {@link me.chanjar.weixin.cp.api.WxCpService#setMaxRetryTimes(int)}
+     *   {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setMaxRetryTimes(int)}
+     * 
+ */ + private int maxRetryTimes = 5; + + /** + * http 请求重试间隔 + *
+     *   {@link me.chanjar.weixin.cp.api.WxCpService#setRetrySleepMillis(int)}
+     *   {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setRetrySleepMillis(int)}
+     * 
+ */ + private int retrySleepMillis = 1000; + } + + public enum StorageType { + /** + * 内存 + */ + memory, + /** + * jedis + */ + jedis, + /** + * redisson + */ + redisson, + /** + * redistemplate + */ + redistemplate + } + + public enum HttpClientType { + /** + * HttpClient + */ + HTTP_CLIENT, + /** + * HttpComponents + */ + HTTP_COMPONENTS, + /** + * OkHttp + */ + OK_HTTP, + /** + * JoddHttp + */ + JODD_HTTP + } +} diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpMultiRedisProperties.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpMultiRedisProperties.java new file mode 100644 index 0000000000..14952d69d9 --- /dev/null +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpMultiRedisProperties.java @@ -0,0 +1,48 @@ +package com.binarywang.solon.wxjava.cp_multi.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * Redis配置. + * + * @author yl + * created on 2023/10/16 + */ +@Data +@NoArgsConstructor +public class WxCpMultiRedisProperties implements Serializable { + private static final long serialVersionUID = -5924815351660074401L; + + /** + * 主机地址. + */ + private String host; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpSingleProperties.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpSingleProperties.java new file mode 100644 index 0000000000..6f7f633c3f --- /dev/null +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/properties/WxCpSingleProperties.java @@ -0,0 +1,68 @@ +package com.binarywang.solon.wxjava.cp_multi.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 企业微信企业相关配置属性 + * + *

企业微信中不同的 corpSecret 对应不同的权限范围,常见的有:

+ *
    + *
  • 自建应用 Secret:在"应用管理 - 自建应用"中查看,只能调用该应用有权限的接口
  • + *
  • 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看,用于管理部门和成员(增删改查)
  • + *
  • 客户联系 Secret:在"客户联系"中查看,用于客户联系相关接口
  • + *
+ *

如需同时使用多种权限范围(例如:既要操作通讯录,又要调用自建应用接口), + * 可在 {@code wx.cp.corps} 下配置多个条目,每个条目使用对应权限的 {@code corpSecret}, + * 其中通讯录同步的条目无需填写 {@code agentId}。

+ * + * @author yl + * created on 2023/10/16 + */ +@Data +@NoArgsConstructor +public class WxCpSingleProperties implements Serializable { + private static final long serialVersionUID = -7502823825007859418L; + /** + * 微信企业号 corpId + */ + private String corpId; + /** + * 微信企业号 corpSecret(权限密钥) + * + *

企业微信针对不同的功能模块提供了不同的 Secret,每种 Secret 只对对应模块的接口有调用权限:

+ *
    + *
  • 自建应用 Secret:在"应用管理 - 自建应用"中找到对应应用,查看其 Secret, + * 使用时需同时配置对应的 {@code agentId}
  • + *
  • 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看, + * 使用此 Secret 可管理部门、成员,无需配置 {@code agentId}
  • + *
  • 其他 Secret(客户联系等):根据需要在企业微信后台查看对应 Secret
  • + *
+ */ + private String corpSecret; + /** + * 微信企业号应用 token + */ + private String token; + /** + * 微信企业号应用 ID(AgentId) + * + *

使用自建应用 Secret 时,需要填写对应应用的 AgentId。

+ *

使用通讯录同步 Secret 时,无需填写此字段。

+ */ + private Integer agentId; + /** + * 微信企业号应用 EncodingAESKey + */ + private String aesKey; + /** + * 微信企业号应用 会话存档私钥 + */ + private String msgAuditPriKey; + /** + * 微信企业号应用 会话存档类库路径 + */ + private String msgAuditLibPath; +} diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/service/WxCpMultiServices.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/service/WxCpMultiServices.java new file mode 100644 index 0000000000..c66c28233d --- /dev/null +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/service/WxCpMultiServices.java @@ -0,0 +1,26 @@ +package com.binarywang.solon.wxjava.cp_multi.service; + +import me.chanjar.weixin.cp.api.WxCpService; + +/** + * 企业微信 {@link WxCpService} 所有实例存放类. + * + * @author yl + * created on 2023/10/16 + */ +public interface WxCpMultiServices { + /** + * 通过租户 Id 获取 WxCpService + * + * @param tenantId 租户 Id + * @return WxCpService + */ + WxCpService getWxCpService(String tenantId); + + /** + * 根据租户 Id,从列表中移除一个 WxCpService 实例 + * + * @param tenantId 租户 Id + */ + void removeWxCpService(String tenantId); +} diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/service/WxCpMultiServicesImpl.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/service/WxCpMultiServicesImpl.java new file mode 100644 index 0000000000..d7833a05f9 --- /dev/null +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp_multi/service/WxCpMultiServicesImpl.java @@ -0,0 +1,42 @@ +package com.binarywang.solon.wxjava.cp_multi.service; + +import me.chanjar.weixin.cp.api.WxCpService; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 企业微信 {@link WxCpMultiServices} 默认实现 + * + * @author yl + * created on 2023/10/16 + */ +public class WxCpMultiServicesImpl implements WxCpMultiServices { + private final Map services = new ConcurrentHashMap<>(); + + /** + * 通过租户 Id 获取 WxCpService + * + * @param tenantId 租户 Id + * @return WxCpService + */ + @Override + public WxCpService getWxCpService(String tenantId) { + return this.services.get(tenantId); + } + + /** + * 根据租户 Id,添加一个 WxCpService 到列表 + * + * @param tenantId 租户 Id + * @param wxCpService WxCpService 实例 + */ + public void addWxCpService(String tenantId, WxCpService wxCpService) { + this.services.put(tenantId, wxCpService); + } + + @Override + public void removeWxCpService(String tenantId) { + this.services.remove(tenantId); + } +} diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-cp-multi-solon-plugin.properties b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-cp-multi-solon-plugin.properties new file mode 100644 index 0000000000..eb537e9a66 --- /dev/null +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-cp-multi-solon-plugin.properties @@ -0,0 +1,2 @@ +solon.plugin=com.binarywang.solon.wxjava.cp_multi.integration.WxCpMultiPluginImpl +solon.plugin.priority=10 diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-cp-multi-solon-plugin/src/test/java/features/test/LoadTest.java new file mode 100644 index 0000000000..d049f5a51a --- /dev/null +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/test/java/features/test/LoadTest.java @@ -0,0 +1,15 @@ +package features.test; + +import org.junit.jupiter.api.Test; +import org.noear.solon.test.SolonTest; + +/** + * @author noear 2024/9/4 created + */ +@SolonTest +public class LoadTest { + @Test + public void load(){ + + } +} diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/src/test/resources/app.properties b/solon-plugins/wx-java-cp-multi-solon-plugin/src/test/resources/app.properties new file mode 100644 index 0000000000..0602c0a807 --- /dev/null +++ b/solon-plugins/wx-java-cp-multi-solon-plugin/src/test/resources/app.properties @@ -0,0 +1,19 @@ +# ?? 1 ?? +wx.cp.corps.tenantId1.corp-id = @corp-id +wx.cp.corps.tenantId1.corp-secret = @corp-secret + ## ?? +wx.cp.corps.tenantId1.agent-id = @agent-id +wx.cp.corps.tenantId1.token = @token +wx.cp.corps.tenantId1.aes-key = @aes-key +wx.cp.corps.tenantId1.msg-audit-priKey = @msg-audit-priKey +wx.cp.corps.tenantId1.msg-audit-lib-path = @msg-audit-lib-path + + # ?? 2 ?? +wx.cp.corps.tenantId2.corp-id = @corp-id +wx.cp.corps.tenantId2.corp-secret = @corp-secret + ## ?? +wx.cp.corps.tenantId2.agent-id = @agent-id +wx.cp.corps.tenantId2.token = @token +wx.cp.corps.tenantId2.aes-key = @aes-key +wx.cp.corps.tenantId2.msg-audit-priKey = @msg-audit-priKey +wx.cp.corps.tenantId2.msg-audit-lib-path = @msg-audit-lib-path diff --git a/solon-plugins/wx-java-cp-solon-plugin/README.md b/solon-plugins/wx-java-cp-solon-plugin/README.md new file mode 100644 index 0000000000..04d5dfab58 --- /dev/null +++ b/solon-plugins/wx-java-cp-solon-plugin/README.md @@ -0,0 +1,41 @@ +# wx-java-cp-solon-plugin + +## 快速开始 + +1. 引入依赖 + ```xml + + com.github.binarywang + wx-java-cp-solon-plugin + ${version} + + ``` +2. 添加配置(app.properties) + ```properties + # 企业微信号配置(必填) + wx.cp.corp-id = @corp-id + wx.cp.corp-secret = @corp-secret + # 选填 + wx.cp.agent-id = @agent-id + wx.cp.token = @token + wx.cp.aes-key = @aes-key + wx.cp.msg-audit-priKey = @msg-audit-priKey + wx.cp.msg-audit-lib-path = @msg-audit-lib-path + # ConfigStorage 配置(选填) + wx.cp.config-storage.type=memory # 配置类型: memory(默认), jedis, redisson, redistemplate + # http 客户端配置(选填) + wx.cp.config-storage.http-proxy-host= + wx.cp.config-storage.http-proxy-port= + wx.cp.config-storage.http-proxy-username= + wx.cp.config-storage.http-proxy-password= + # 最大重试次数,默认:5 次,如果小于 0,则为 0 + wx.cp.config-storage.max-retry-times=5 + # 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000 + wx.cp.config-storage.retry-sleep-millis=1000 + ``` +3. 支持自动注入的类型: `WxCpService`, `WxCpConfigStorage` + +4. 覆盖自动配置: 自定义注入的bean会覆盖自动注入的 + +- WxCpService +- WxCpConfigStorage diff --git a/solon-plugins/wx-java-cp-solon-plugin/pom.xml b/solon-plugins/wx-java-cp-solon-plugin/pom.xml new file mode 100644 index 0000000000..367d2a338c --- /dev/null +++ b/solon-plugins/wx-java-cp-solon-plugin/pom.xml @@ -0,0 +1,30 @@ + + + + wx-java-solon-plugins + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-cp-solon-plugin + WxJava - Solon Plugin for WxCp + 微信企业号开发的 Solon Plugin + + + + com.github.binarywang + weixin-java-cp + ${project.version} + + + redis.clients + jedis + + + org.redisson + redisson + + + diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/config/WxCpServiceAutoConfiguration.java b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/config/WxCpServiceAutoConfiguration.java new file mode 100644 index 0000000000..82aeeaf859 --- /dev/null +++ b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/config/WxCpServiceAutoConfiguration.java @@ -0,0 +1,43 @@ +package com.binarywang.solon.wxjava.cp.config; + +import com.binarywang.solon.wxjava.cp.properties.WxCpProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +/** + * 企业微信平台相关服务自动注册 + * + * @author yl + * created on 2021/12/6 + */ +@Configuration +@RequiredArgsConstructor +public class WxCpServiceAutoConfiguration { + private final WxCpProperties wxCpProperties; + + @Bean + @Condition(onMissingBean = WxCpService.class, + onBean = WxCpConfigStorage.class) + public WxCpService wxCpService(WxCpConfigStorage wxCpConfigStorage) { + WxCpService wxCpService = new WxCpServiceImpl(); + wxCpService.setWxCpConfigStorage(wxCpConfigStorage); + + WxCpProperties.ConfigStorage storage = wxCpProperties.getConfigStorage(); + int maxRetryTimes = storage.getMaxRetryTimes(); + if (maxRetryTimes < 0) { + maxRetryTimes = 0; + } + int retrySleepMillis = storage.getRetrySleepMillis(); + if (retrySleepMillis < 0) { + retrySleepMillis = 1000; + } + wxCpService.setRetrySleepMillis(retrySleepMillis); + wxCpService.setMaxRetryTimes(maxRetryTimes); + return wxCpService; + } +} diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/integration/WxCpPluginImpl.java b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/integration/WxCpPluginImpl.java new file mode 100644 index 0000000000..fda64b3a17 --- /dev/null +++ b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/integration/WxCpPluginImpl.java @@ -0,0 +1,25 @@ +package com.binarywang.solon.wxjava.cp.integration; + +import com.binarywang.solon.wxjava.cp.config.WxCpServiceAutoConfiguration; +import com.binarywang.solon.wxjava.cp.properties.WxCpProperties; +import com.binarywang.solon.wxjava.cp.storage.WxCpInJedisConfigStorageConfiguration; +import com.binarywang.solon.wxjava.cp.storage.WxCpInMemoryConfigStorageConfiguration; +import com.binarywang.solon.wxjava.cp.storage.WxCpInRedissonConfigStorageConfiguration; +import org.noear.solon.core.AppContext; +import org.noear.solon.core.Plugin; + +/** + * @author noear 2024/9/2 created + */ +public class WxCpPluginImpl implements Plugin { + @Override + public void start(AppContext context) throws Throwable { + context.beanMake(WxCpProperties.class); + + context.beanMake(WxCpServiceAutoConfiguration.class); + + context.beanMake(WxCpInMemoryConfigStorageConfiguration.class); + context.beanMake(WxCpInJedisConfigStorageConfiguration.class); + context.beanMake(WxCpInRedissonConfigStorageConfiguration.class); + } +} diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/properties/WxCpProperties.java b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/properties/WxCpProperties.java new file mode 100644 index 0000000000..60524f5228 --- /dev/null +++ b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/properties/WxCpProperties.java @@ -0,0 +1,133 @@ +package com.binarywang.solon.wxjava.cp.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +import java.io.Serializable; + +/** + * 企业微信接入相关配置属性 + * + * @author yl + * created on 2021/12/6 + */ +@Data +@NoArgsConstructor +@Configuration +@Inject("${" + WxCpProperties.PREFIX + "}") +public class WxCpProperties { + public static final String PREFIX = "wx.cp"; + + /** + * 微信企业号 corpId + */ + private String corpId; + /** + * 微信企业号 corpSecret + */ + private String corpSecret; + /** + * 微信企业号应用 token + */ + private String token; + /** + * 微信企业号应用 ID + */ + private Integer agentId; + /** + * 微信企业号应用 EncodingAESKey + */ + private String aesKey; + /** + * 微信企业号应用 会话存档私钥 + */ + private String msgAuditPriKey; + /** + * 微信企业号应用 会话存档类库路径 + */ + private String msgAuditLibPath; + + /** + * 配置存储策略,默认内存 + */ + private ConfigStorage configStorage = new ConfigStorage(); + + @Data + @NoArgsConstructor + public static class ConfigStorage implements Serializable { + private static final long serialVersionUID = 4815731027000065434L; + /** + * 存储类型 + */ + private StorageType type = StorageType.memory; + + /** + * 指定key前缀 + */ + private String keyPrefix = "wx:cp"; + + /** + * redis连接配置 + */ + private WxCpRedisProperties redis = new WxCpRedisProperties(); + + /** + * http代理主机 + */ + private String httpProxyHost; + + /** + * http代理端口 + */ + private Integer httpProxyPort; + + /** + * http代理用户名 + */ + private String httpProxyUsername; + + /** + * http代理密码 + */ + private String httpProxyPassword; + + /** + * http 请求最大重试次数 + *
+     *   {@link me.chanjar.weixin.cp.api.WxCpService#setMaxRetryTimes(int)}
+     *   {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setMaxRetryTimes(int)}
+     * 
+ */ + private int maxRetryTimes = 5; + + /** + * http 请求重试间隔 + *
+     *   {@link me.chanjar.weixin.cp.api.WxCpService#setRetrySleepMillis(int)}
+     *   {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setRetrySleepMillis(int)}
+     * 
+ */ + private int retrySleepMillis = 1000; + } + + public enum StorageType { + /** + * 内存 + */ + memory, + /** + * jedis + */ + jedis, + /** + * redisson + */ + redisson, + /** + * redistemplate + */ + redistemplate + } +} diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/properties/WxCpRedisProperties.java b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/properties/WxCpRedisProperties.java new file mode 100644 index 0000000000..43b8788d3f --- /dev/null +++ b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/properties/WxCpRedisProperties.java @@ -0,0 +1,46 @@ +package com.binarywang.solon.wxjava.cp.properties; + +import lombok.Data; + +import java.io.Serializable; + +/** + * Redis配置. + * + * @author yl + * created on 2023/04/23 + */ +@Data +public class WxCpRedisProperties implements Serializable { + private static final long serialVersionUID = -5924815351660074401L; + + /** + * 主机地址. + */ + private String host; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/AbstractWxCpConfigStorageConfiguration.java b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/AbstractWxCpConfigStorageConfiguration.java new file mode 100644 index 0000000000..9fcdd5779a --- /dev/null +++ b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/AbstractWxCpConfigStorageConfiguration.java @@ -0,0 +1,61 @@ +package com.binarywang.solon.wxjava.cp.storage; + +import com.binarywang.solon.wxjava.cp.properties.WxCpProperties; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import org.apache.commons.lang3.StringUtils; + +/** + * WxCpConfigStorage 抽象配置类 + * + * @author yl & Wang_Wong + * created on 2021/12/6 + */ +public abstract class AbstractWxCpConfigStorageConfiguration { + + protected WxCpDefaultConfigImpl config(WxCpDefaultConfigImpl config, WxCpProperties properties) { + String corpId = properties.getCorpId(); + String corpSecret = properties.getCorpSecret(); + Integer agentId = properties.getAgentId(); + String token = properties.getToken(); + String aesKey = properties.getAesKey(); + // 企业微信,私钥,会话存档路径 + String msgAuditPriKey = properties.getMsgAuditPriKey(); + String msgAuditLibPath = properties.getMsgAuditLibPath(); + + config.setCorpId(corpId); + config.setCorpSecret(corpSecret); + config.setAgentId(agentId); + if (StringUtils.isNotBlank(token)) { + config.setToken(token); + } + if (StringUtils.isNotBlank(aesKey)) { + config.setAesKey(aesKey); + } + if (StringUtils.isNotBlank(msgAuditPriKey)) { + config.setMsgAuditPriKey(msgAuditPriKey); + } + if (StringUtils.isNotBlank(msgAuditLibPath)) { + config.setMsgAuditLibPath(msgAuditLibPath); + } + + WxCpProperties.ConfigStorage storage = properties.getConfigStorage(); + String httpProxyHost = storage.getHttpProxyHost(); + Integer httpProxyPort = storage.getHttpProxyPort(); + String httpProxyUsername = storage.getHttpProxyUsername(); + String httpProxyPassword = storage.getHttpProxyPassword(); + if (StringUtils.isNotBlank(httpProxyHost)) { + config.setHttpProxyHost(httpProxyHost); + if (httpProxyPort != null) { + config.setHttpProxyPort(httpProxyPort); + } + if (StringUtils.isNotBlank(httpProxyUsername)) { + config.setHttpProxyUsername(httpProxyUsername); + } + if (StringUtils.isNotBlank(httpProxyPassword)) { + config.setHttpProxyPassword(httpProxyPassword); + } + } + return config; + } + +} diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/WxCpInJedisConfigStorageConfiguration.java b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/WxCpInJedisConfigStorageConfiguration.java new file mode 100644 index 0000000000..f6f6931992 --- /dev/null +++ b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/WxCpInJedisConfigStorageConfiguration.java @@ -0,0 +1,74 @@ +package com.binarywang.solon.wxjava.cp.storage; + +import com.binarywang.solon.wxjava.cp.properties.WxCpProperties; +import com.binarywang.solon.wxjava.cp.properties.WxCpRedisProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import me.chanjar.weixin.cp.config.impl.WxCpJedisConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * 自动装配基于 jedis 策略配置 + * + * @author yl + * created on 2023/04/23 + */ +@Configuration +@Condition( + onProperty = "${"+WxCpProperties.PREFIX + ".configStorage.type} = jedis", + onClass = JedisPool.class +) +@RequiredArgsConstructor +public class WxCpInJedisConfigStorageConfiguration extends AbstractWxCpConfigStorageConfiguration { + private final WxCpProperties wxCpProperties; + private final AppContext applicationContext; + + @Bean + @Condition(onMissingBean=WxCpConfigStorage.class) + public WxCpConfigStorage wxCpConfigStorage() { + WxCpDefaultConfigImpl config = getConfigStorage(); + return this.config(config, wxCpProperties); + } + + private WxCpJedisConfigImpl getConfigStorage() { + WxCpRedisProperties wxCpRedisProperties = wxCpProperties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (wxCpRedisProperties != null && StringUtils.isNotEmpty(wxCpRedisProperties.getHost())) { + jedisPool = getJedisPool(); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + return new WxCpJedisConfigImpl(jedisPool, wxCpProperties.getConfigStorage().getKeyPrefix()); + } + + private JedisPool getJedisPool() { + WxCpProperties.ConfigStorage storage = wxCpProperties.getConfigStorage(); + WxCpRedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), + redis.getTimeout(), redis.getPassword(), redis.getDatabase()); + } +} diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/WxCpInMemoryConfigStorageConfiguration.java b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/WxCpInMemoryConfigStorageConfiguration.java new file mode 100644 index 0000000000..2776fea368 --- /dev/null +++ b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/WxCpInMemoryConfigStorageConfiguration.java @@ -0,0 +1,31 @@ +package com.binarywang.solon.wxjava.cp.storage; + +import com.binarywang.solon.wxjava.cp.properties.WxCpProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +/** + * 自动装配基于内存策略配置 + * + * @author yl + * created on 2021/12/6 + */ +@Configuration +@Condition( + onProperty = "${"+WxCpProperties.PREFIX + ".configStorage.type:memory} = memory" +) +@RequiredArgsConstructor +public class WxCpInMemoryConfigStorageConfiguration extends AbstractWxCpConfigStorageConfiguration { + private final WxCpProperties wxCpProperties; + + @Bean + @Condition(onMissingBean=WxCpConfigStorage.class) + public WxCpConfigStorage wxCpConfigStorage() { + WxCpDefaultConfigImpl config = new WxCpDefaultConfigImpl(); + return this.config(config, wxCpProperties); + } +} diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/WxCpInRedissonConfigStorageConfiguration.java b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/WxCpInRedissonConfigStorageConfiguration.java new file mode 100644 index 0000000000..0aef4d520a --- /dev/null +++ b/solon-plugins/wx-java-cp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/cp/storage/WxCpInRedissonConfigStorageConfiguration.java @@ -0,0 +1,65 @@ +package com.binarywang.solon.wxjava.cp.storage; + +import com.binarywang.solon.wxjava.cp.properties.WxCpProperties; +import com.binarywang.solon.wxjava.cp.properties.WxCpRedisProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import me.chanjar.weixin.cp.config.impl.WxCpRedissonConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; + +/** + * 自动装配基于 redisson 策略配置 + * + * @author yl + * created on 2023/04/23 + */ +@Configuration +@Condition( + onProperty = "${"+WxCpProperties.PREFIX + ".configStorage.type} = redisson", + onClass = Redisson.class +) +@RequiredArgsConstructor +public class WxCpInRedissonConfigStorageConfiguration extends AbstractWxCpConfigStorageConfiguration { + private final WxCpProperties wxCpProperties; + private final AppContext applicationContext; + + @Bean + @Condition(onMissingBean=WxCpConfigStorage.class) + public WxCpConfigStorage wxCpConfigStorage() { + WxCpDefaultConfigImpl config = getConfigStorage(); + return this.config(config, wxCpProperties); + } + + private WxCpRedissonConfigImpl getConfigStorage() { + WxCpRedisProperties redisProperties = wxCpProperties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + redissonClient = getRedissonClient(); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxCpRedissonConfigImpl(redissonClient, wxCpProperties.getConfigStorage().getKeyPrefix()); + } + + private RedissonClient getRedissonClient() { + WxCpProperties.ConfigStorage storage = wxCpProperties.getConfigStorage(); + WxCpRedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redis.getHost() + ":" + redis.getPort()) + .setDatabase(redis.getDatabase()) + .setPassword(redis.getPassword()); + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/main/resources/META-INF/solon/wx-java-cp-solon-plugin.properties b/solon-plugins/wx-java-cp-solon-plugin/src/main/resources/META-INF/solon/wx-java-cp-solon-plugin.properties new file mode 100644 index 0000000000..c765affecb --- /dev/null +++ b/solon-plugins/wx-java-cp-solon-plugin/src/main/resources/META-INF/solon/wx-java-cp-solon-plugin.properties @@ -0,0 +1,2 @@ +solon.plugin=com.binarywang.solon.wxjava.cp.integration.WxCpPluginImpl +solon.plugin.priority=10 diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-cp-solon-plugin/src/test/java/features/test/LoadTest.java new file mode 100644 index 0000000000..d049f5a51a --- /dev/null +++ b/solon-plugins/wx-java-cp-solon-plugin/src/test/java/features/test/LoadTest.java @@ -0,0 +1,15 @@ +package features.test; + +import org.junit.jupiter.api.Test; +import org.noear.solon.test.SolonTest; + +/** + * @author noear 2024/9/4 created + */ +@SolonTest +public class LoadTest { + @Test + public void load(){ + + } +} diff --git a/solon-plugins/wx-java-cp-solon-plugin/src/test/resources/app.properties b/solon-plugins/wx-java-cp-solon-plugin/src/test/resources/app.properties new file mode 100644 index 0000000000..0c99c8b64d --- /dev/null +++ b/solon-plugins/wx-java-cp-solon-plugin/src/test/resources/app.properties @@ -0,0 +1,20 @@ +# ???????(??) +wx.cp.corp-id = @corp-id +wx.cp.corp-secret = @corp-secret +# ?? +wx.cp.agent-id = @agent-id +wx.cp.token = @token +wx.cp.aes-key = @aes-key +wx.cp.msg-audit-priKey = @msg-audit-priKey +wx.cp.msg-audit-lib-path = @msg-audit-lib-path +# ConfigStorage ?????? +wx.cp.config-storage.type=memory # ????: memory(??), jedis, redisson, redistemplate +# http ????????? +wx.cp.config-storage.http-proxy-host= +wx.cp.config-storage.http-proxy-port= +wx.cp.config-storage.http-proxy-username= +wx.cp.config-storage.http-proxy-password= +# ??????????5 ?????? 0??? 0 +wx.cp.config-storage.max-retry-times=5 +# ????????????1000 ??????? 0??? 1000 +wx.cp.config-storage.retry-sleep-millis=1000 diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/README.md b/solon-plugins/wx-java-miniapp-multi-solon-plugin/README.md new file mode 100644 index 0000000000..4555a4fc5e --- /dev/null +++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/README.md @@ -0,0 +1,95 @@ +# wx-java-miniapp-multi-solon-plugin + +## 快速开始 + +1. 引入依赖 + ```xml + + com.github.binarywang + wx-java-miniapp-multi-solon-plugin + ${version} + + ``` +2. 添加配置(app.properties) + ```properties + # 公众号配置 + ## 应用 1 配置(必填) + wx.ma.apps.tenantId1.app-id=appId + wx.ma.apps.tenantId1.app-secret=@secret + ## 选填 + wx.ma.apps.tenantId1.token=@token + wx.ma.apps.tenantId1.aes-key=@aesKey + wx.ma.apps.tenantId1.use-stable-access-token=@useStableAccessToken + ## 应用 2 配置(必填) + wx.ma.apps.tenantId2.app-id=@appId + wx.ma.apps.tenantId2.app-secret =@secret + ## 选填 + wx.ma.apps.tenantId2.token=@token + wx.ma.apps.tenantId2.aes-key=@aesKey + wx.ma.apps.tenantId2.use-stable-access-token=@useStableAccessToken + + # ConfigStorage 配置(选填) + ## 配置类型: memory(默认), jedis, redisson + wx.ma.config-storage.type=memory + ## 相关redis前缀配置: wx:ma:multi(默认) + wx.ma.config-storage.key-prefix=wx:ma:multi + wx.ma.config-storage.redis.host=127.0.0.1 + wx.ma.config-storage.redis.port=6379 + ## 单机和 sentinel 同时存在时,优先使用sentinel配置 + # wx.ma.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379 + # wx.ma.config-storage.redis.sentinel-name=mymaster + + # http 客户端配置(选填) + ## # http客户端类型: http_client(默认), ok_http, jodd_http + wx.ma.config-storage.http-client-type=http_client + wx.ma.config-storage.http-proxy-host= + wx.ma.config-storage.http-proxy-port= + wx.ma.config-storage.http-proxy-username= + wx.ma.config-storage.http-proxy-password= + ## 最大重试次数,默认:5 次,如果小于 0,则为 0 + wx.ma.config-storage.max-retry-times=5 + ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000 + wx.ma.config-storage.retry-sleep-millis=1000 + ``` +3. 自动注入的类型:`WxMaMultiServices` + +4. 使用样例 + +```java +import com.binarywang.solon.wxjava.miniapp.service.WxMaMultiServices; +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.binarywang.wx.miniapp.api.WxMaUserService; +import org.noear.solon.annotation.Component; +import org.noear.solon.annotation.Inject; + +@Component +public class DemoService { + @Inject + private WxMaMultiServices wxMaMultiServices; + + public void test() { + // 应用 1 的 WxMaService + WxMaService wxMaService1 = wxMaMultiServices.getWxMaService("tenantId1"); + WxMaUserService userService1 = wxMaService1.getUserService(); + userService1.userInfo("xxx"); + // todo ... + + // 应用 2 的 WxMaService + WxMaService wxMaService2 = wxMaMultiServices.getWxMaService("tenantId2"); + WxMaUserService userService2 = wxMaService2.getUserService(); + userService2.userInfo("xxx"); + // todo ... + + // 应用 3 的 WxMaService + WxMaService wxMaService3 = wxMaMultiServices.getWxMaService("tenantId3"); + // 判断是否为空 + if (wxMaService3 == null) { + // todo wxMaService3 为空,请先配置 tenantId3 微信公众号应用参数 + return; + } + WxMaUserService userService3 = wxMaService3.getUserService(); + userService3.userInfo("xxx"); + // todo ... + } +} +``` diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml new file mode 100644 index 0000000000..9ea8b7caff --- /dev/null +++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml @@ -0,0 +1,43 @@ + + + + wx-java-solon-plugins + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-miniapp-multi-solon-plugin + WxJava - Solon Plugin for MiniApp::支持多账号配置 + 微信公众号开发的 Solon Plugin::支持多账号配置 + + + + com.github.binarywang + weixin-java-miniapp + ${project.version} + + + redis.clients + jedis + provided + + + org.redisson + redisson + provided + + + org.jodd + jodd-http + provided + + + com.squareup.okhttp3 + okhttp + provided + + + diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java new file mode 100644 index 0000000000..8ad85c96b8 --- /dev/null +++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java @@ -0,0 +1,151 @@ +package com.binarywang.solon.wxjava.miniapp.configuration.services; + +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceHttpClientImpl; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceHttpComponentsImpl; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceJoddHttpImpl; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceOkHttpImpl; +import cn.binarywang.wx.miniapp.config.WxMaConfig; +import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; +import com.binarywang.solon.wxjava.miniapp.properties.WxMaMultiProperties; +import com.binarywang.solon.wxjava.miniapp.properties.WxMaSingleProperties; +import com.binarywang.solon.wxjava.miniapp.service.WxMaMultiServices; +import com.binarywang.solon.wxjava.miniapp.service.WxMaMultiServicesImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * WxMaConfigStorage 抽象配置类 + * + * @author monch + * created on 2024/9/6 + */ +@RequiredArgsConstructor +@Slf4j +public abstract class AbstractWxMaConfiguration { + + protected WxMaMultiServices wxMaMultiServices(WxMaMultiProperties wxMaMultiProperties) { + Map appsMap = wxMaMultiProperties.getApps(); + if (appsMap == null || appsMap.isEmpty()) { + log.warn("微信公众号应用参数未配置,通过 WxMaMultiServices#getWxMaService(\"tenantId\")获取实例将返回空"); + return new WxMaMultiServicesImpl(); + } + /** + * 校验 appId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。 + * + * 查看 {@link cn.binarywang.wx.miniapp.config.impl.WxMaRedisConfigImpl#setAppId(String)} + */ + Collection apps = appsMap.values(); + if (apps.size() > 1) { + // 校验 appId 是否唯一 + boolean multi = apps.stream() + // 没有 appId,如果不判断是否为空,这里会报 NPE 异常 + .collect(Collectors.groupingBy(c -> c.getAppId() == null ? 0 : c.getAppId(), Collectors.counting())) + .entrySet().stream().anyMatch(e -> e.getValue() > 1); + if (multi) { + throw new RuntimeException("请确保微信公众号配置 appId 的唯一性"); + } + } + WxMaMultiServicesImpl services = new WxMaMultiServicesImpl(); + + Set> entries = appsMap.entrySet(); + for (Map.Entry entry : entries) { + String tenantId = entry.getKey(); + WxMaSingleProperties wxMaSingleProperties = entry.getValue(); + WxMaDefaultConfigImpl storage = this.wxMaConfigStorage(wxMaMultiProperties); + this.configApp(storage, wxMaSingleProperties); + this.configHttp(storage, wxMaMultiProperties.getConfigStorage()); + WxMaService wxMaService = this.wxMaService(storage, wxMaMultiProperties); + services.addWxMaService(tenantId, wxMaService); + } + return services; + } + + /** + * 配置 WxMaDefaultConfigImpl + * + * @param wxMaMultiProperties 参数 + * @return WxMaDefaultConfigImpl + */ + protected abstract WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties); + + public WxMaService wxMaService(WxMaConfig wxMaConfig, WxMaMultiProperties wxMaMultiProperties) { + WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage(); + WxMaMultiProperties.HttpClientType httpClientType = storage.getHttpClientType(); + WxMaService wxMaService; + switch (httpClientType) { + case OK_HTTP: + wxMaService = new WxMaServiceOkHttpImpl(); + break; + case JODD_HTTP: + wxMaService = new WxMaServiceJoddHttpImpl(); + break; + case HTTP_CLIENT: + wxMaService = new WxMaServiceHttpClientImpl(); + break; + case HTTP_COMPONENTS: + wxMaService = new WxMaServiceHttpComponentsImpl(); + break; + default: + wxMaService = new WxMaServiceImpl(); + break; + } + + wxMaService.setWxMaConfig(wxMaConfig); + int maxRetryTimes = storage.getMaxRetryTimes(); + if (maxRetryTimes < 0) { + maxRetryTimes = 0; + } + int retrySleepMillis = storage.getRetrySleepMillis(); + if (retrySleepMillis < 0) { + retrySleepMillis = 1000; + } + wxMaService.setRetrySleepMillis(retrySleepMillis); + wxMaService.setMaxRetryTimes(maxRetryTimes); + return wxMaService; + } + + private void configApp(WxMaDefaultConfigImpl config, WxMaSingleProperties corpProperties) { + String appId = corpProperties.getAppId(); + String appSecret = corpProperties.getAppSecret(); + String token = corpProperties.getToken(); + String aesKey = corpProperties.getAesKey(); + boolean useStableAccessToken = corpProperties.isUseStableAccessToken(); + + config.setAppid(appId); + config.setSecret(appSecret); + if (StringUtils.isNotBlank(token)) { + config.setToken(token); + } + if (StringUtils.isNotBlank(aesKey)) { + config.setAesKey(aesKey); + } + config.useStableAccessToken(useStableAccessToken); + } + + private void configHttp(WxMaDefaultConfigImpl config, WxMaMultiProperties.ConfigStorage storage) { + String httpProxyHost = storage.getHttpProxyHost(); + Integer httpProxyPort = storage.getHttpProxyPort(); + String httpProxyUsername = storage.getHttpProxyUsername(); + String httpProxyPassword = storage.getHttpProxyPassword(); + if (StringUtils.isNotBlank(httpProxyHost)) { + config.setHttpProxyHost(httpProxyHost); + if (httpProxyPort != null) { + config.setHttpProxyPort(httpProxyPort); + } + if (StringUtils.isNotBlank(httpProxyUsername)) { + config.setHttpProxyUsername(httpProxyUsername); + } + if (StringUtils.isNotBlank(httpProxyPassword)) { + config.setHttpProxyPassword(httpProxyPassword); + } + } + } +} diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/WxMaInJedisConfiguration.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/WxMaInJedisConfiguration.java new file mode 100644 index 0000000000..24950fae10 --- /dev/null +++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/WxMaInJedisConfiguration.java @@ -0,0 +1,77 @@ +package com.binarywang.solon.wxjava.miniapp.configuration.services; + +import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; +import cn.binarywang.wx.miniapp.config.impl.WxMaRedisConfigImpl; +import com.binarywang.solon.wxjava.miniapp.properties.WxMaMultiProperties; +import com.binarywang.solon.wxjava.miniapp.properties.WxMaMultiRedisProperties; +import com.binarywang.solon.wxjava.miniapp.service.WxMaMultiServices; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * 自动装配基于 jedis 策略配置 + * + * @author monch + * created on 2024/9/6 + */ +@Configuration +@Condition( + onProperty = "${"+WxMaMultiProperties.PREFIX + ".configStorage.type} = jedis", + onClass = JedisPool.class +) +@RequiredArgsConstructor +public class WxMaInJedisConfiguration extends AbstractWxMaConfiguration { + private final WxMaMultiProperties wxMaMultiProperties; + private final AppContext applicationContext; + + @Bean + public WxMaMultiServices wxMaMultiServices() { + return this.wxMaMultiServices(wxMaMultiProperties); + } + + @Override + protected WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties) { + return this.configRedis(wxMaMultiProperties); + } + + private WxMaDefaultConfigImpl configRedis(WxMaMultiProperties wxMaMultiProperties) { + WxMaMultiRedisProperties wxMaMultiRedisProperties = wxMaMultiProperties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (wxMaMultiRedisProperties != null && StringUtils.isNotEmpty(wxMaMultiRedisProperties.getHost())) { + jedisPool = getJedisPool(wxMaMultiProperties); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + return new WxMaRedisConfigImpl(jedisPool); + } + + private JedisPool getJedisPool(WxMaMultiProperties wxMaMultiProperties) { + WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage(); + WxMaMultiRedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), + redis.getTimeout(), redis.getPassword(), redis.getDatabase()); + } +} diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/WxMaInMemoryConfiguration.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/WxMaInMemoryConfiguration.java new file mode 100644 index 0000000000..0b9ef1c4a8 --- /dev/null +++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/WxMaInMemoryConfiguration.java @@ -0,0 +1,39 @@ +package com.binarywang.solon.wxjava.miniapp.configuration.services; + +import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; +import com.binarywang.solon.wxjava.miniapp.properties.WxMaMultiProperties; +import com.binarywang.solon.wxjava.miniapp.service.WxMaMultiServices; +import lombok.RequiredArgsConstructor; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +/** + * 自动装配基于内存策略配置 + * + * @author monch + * created on 2024/9/6 + */ +@Configuration +@Condition( + onProperty = "${"+WxMaMultiProperties.PREFIX + ".configStorage.type:memory} = memory" +) +@RequiredArgsConstructor +public class WxMaInMemoryConfiguration extends AbstractWxMaConfiguration { + private final WxMaMultiProperties wxMaMultiProperties; + + @Bean + public WxMaMultiServices wxMaMultiServices() { + return this.wxMaMultiServices(wxMaMultiProperties); + } + + @Override + protected WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties) { + return this.configInMemory(); + } + + private WxMaDefaultConfigImpl configInMemory() { + return new WxMaDefaultConfigImpl(); + // return new WxMaDefaultConfigImpl(); + } +} diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/WxMaInRedissonConfiguration.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/WxMaInRedissonConfiguration.java new file mode 100644 index 0000000000..4e97071f01 --- /dev/null +++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/configuration/services/WxMaInRedissonConfiguration.java @@ -0,0 +1,68 @@ +package com.binarywang.solon.wxjava.miniapp.configuration.services; + +import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; +import cn.binarywang.wx.miniapp.config.impl.WxMaRedissonConfigImpl; +import com.binarywang.solon.wxjava.miniapp.properties.WxMaMultiProperties; +import com.binarywang.solon.wxjava.miniapp.properties.WxMaMultiRedisProperties; +import com.binarywang.solon.wxjava.miniapp.service.WxMaMultiServices; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; + +/** + * 自动装配基于 redisson 策略配置 + * + * @author monch + * created on 2024/9/6 + */ +@Configuration +@Condition( + onProperty = "${"+WxMaMultiProperties.PREFIX + ".configStorage.type} = redisson", + onClass = Redisson.class +) +@RequiredArgsConstructor +public class WxMaInRedissonConfiguration extends AbstractWxMaConfiguration { + private final WxMaMultiProperties wxMaMultiProperties; + private final AppContext applicationContext; + + @Bean + public WxMaMultiServices wxMaMultiServices() { + return this.wxMaMultiServices(wxMaMultiProperties); + } + + @Override + protected WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties) { + return this.configRedisson(wxMaMultiProperties); + } + + private WxMaDefaultConfigImpl configRedisson(WxMaMultiProperties wxMaMultiProperties) { + WxMaMultiRedisProperties redisProperties = wxMaMultiProperties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + redissonClient = getRedissonClient(wxMaMultiProperties); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxMaRedissonConfigImpl(redissonClient, wxMaMultiProperties.getConfigStorage().getKeyPrefix()); + } + + private RedissonClient getRedissonClient(WxMaMultiProperties wxMaMultiProperties) { + WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage(); + WxMaMultiRedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redis.getHost() + ":" + redis.getPort()) + .setDatabase(redis.getDatabase()) + .setPassword(redis.getPassword()); + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/integration/WxMiniappMultiPluginImpl.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/integration/WxMiniappMultiPluginImpl.java new file mode 100644 index 0000000000..c1153be1bb --- /dev/null +++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/integration/WxMiniappMultiPluginImpl.java @@ -0,0 +1,22 @@ +package com.binarywang.solon.wxjava.miniapp.integration; + +import com.binarywang.solon.wxjava.miniapp.configuration.services.WxMaInJedisConfiguration; +import com.binarywang.solon.wxjava.miniapp.configuration.services.WxMaInMemoryConfiguration; +import com.binarywang.solon.wxjava.miniapp.configuration.services.WxMaInRedissonConfiguration; +import com.binarywang.solon.wxjava.miniapp.properties.WxMaMultiProperties; +import org.noear.solon.core.AppContext; +import org.noear.solon.core.Plugin; + +/** + * @author noear 2024/10/9 created + */ +public class WxMiniappMultiPluginImpl implements Plugin { + @Override + public void start(AppContext context) throws Throwable { + context.beanMake(WxMaMultiProperties.class); + + context.beanMake(WxMaInJedisConfiguration.class); + context.beanMake(WxMaInMemoryConfiguration.class); + context.beanMake(WxMaInRedissonConfiguration.class); + } +} diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaMultiProperties.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaMultiProperties.java new file mode 100644 index 0000000000..f99d6280ec --- /dev/null +++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaMultiProperties.java @@ -0,0 +1,158 @@ +package com.binarywang.solon.wxjava.miniapp.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * @author monch created on 2024/9/6 + * @author noear + */ +@Data +@NoArgsConstructor +@Configuration +@Inject("${" + WxMaMultiProperties.PREFIX + "}") +public class WxMaMultiProperties implements Serializable { + private static final long serialVersionUID = -5358245184407791011L; + public static final String PREFIX = "wx.ma"; + + private Map apps = new HashMap<>(); + + /** + * 自定义host配置 + */ + private HostConfig hosts; + + /** + * 存储策略 + */ + private final ConfigStorage configStorage = new ConfigStorage(); + + @Data + @NoArgsConstructor + public static class HostConfig implements Serializable { + private static final long serialVersionUID = -4172767630740346001L; + + /** + * 对应于:https://api.weixin.qq.com + */ + private String apiHost; + + /** + * 对应于:https://open.weixin.qq.com + */ + private String openHost; + + /** + * 对应于:https://mp.weixin.qq.com + */ + private String mpHost; + } + + @Data + @NoArgsConstructor + public static class ConfigStorage implements Serializable { + private static final long serialVersionUID = 4815731027000065434L; + + /** + * 存储类型. + */ + private StorageType type = StorageType.MEMORY; + + /** + * 指定key前缀. + */ + private String keyPrefix = "wx:ma:multi"; + + /** + * redis连接配置. + */ + private final WxMaMultiRedisProperties redis = new WxMaMultiRedisProperties(); + + /** + * http客户端类型. + */ + private HttpClientType httpClientType = HttpClientType.HTTP_COMPONENTS; + + /** + * http代理主机. + */ + private String httpProxyHost; + + /** + * http代理端口. + */ + private Integer httpProxyPort; + + /** + * http代理用户名. + */ + private String httpProxyUsername; + + /** + * http代理密码. + */ + private String httpProxyPassword; + + /** + * http 请求最大重试次数 + *
+     *   {@link cn.binarywang.wx.miniapp.api.WxMaService#setMaxRetryTimes(int)}
+     *   {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setMaxRetryTimes(int)}
+     * 
+ */ + private int maxRetryTimes = 5; + + /** + * http 请求重试间隔 + *
+     *   {@link cn.binarywang.wx.miniapp.api.WxMaService#setRetrySleepMillis(int)}
+     *   {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setRetrySleepMillis(int)}
+     * 
+ */ + private int retrySleepMillis = 1000; + } + + public enum StorageType { + /** + * 内存 + */ + MEMORY, + /** + * jedis + */ + JEDIS, + /** + * redisson + */ + REDISSON, + /** + * redisTemplate + */ + REDIS_TEMPLATE + } + + public enum HttpClientType { + /** + * HttpClient + */ + HTTP_CLIENT, + /** + * OkHttp + */ + OK_HTTP, + /** + * JoddHttp + */ + JODD_HTTP, + /** + * HttpComponents + */ + HTTP_COMPONENTS + } +} diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaMultiRedisProperties.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaMultiRedisProperties.java new file mode 100644 index 0000000000..1f4c07806e --- /dev/null +++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaMultiRedisProperties.java @@ -0,0 +1,56 @@ +package com.binarywang.solon.wxjava.miniapp.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * @author monch + * created on 2024/9/6 + */ +@Data +@NoArgsConstructor +public class WxMaMultiRedisProperties implements Serializable { + private static final long serialVersionUID = -5924815351660074401L; + + /** + * 主机地址. + */ + private String host = "127.0.0.1"; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + /** + * sentinel ips + */ + private String sentinelIps; + + /** + * sentinel name + */ + private String sentinelName; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaSingleProperties.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaSingleProperties.java new file mode 100644 index 0000000000..f61985716e --- /dev/null +++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaSingleProperties.java @@ -0,0 +1,40 @@ +package com.binarywang.solon.wxjava.miniapp.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * @author monch + * created on 2024/9/6 + */ +@Data +@NoArgsConstructor +public class WxMaSingleProperties implements Serializable { + private static final long serialVersionUID = 1980986361098922525L; + /** + * 设置微信公众号的 appid. + */ + private String appId; + + /** + * 设置微信公众号的 app secret. + */ + private String appSecret; + + /** + * 设置微信公众号的 token. + */ + private String token; + + /** + * 设置微信公众号的 EncodingAESKey. + */ + private String aesKey; + + /** + * 是否使用稳定版 Access Token + */ + private boolean useStableAccessToken = false; +} diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/service/WxMaMultiServices.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/service/WxMaMultiServices.java new file mode 100644 index 0000000000..80d073cceb --- /dev/null +++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/service/WxMaMultiServices.java @@ -0,0 +1,27 @@ +package com.binarywang.solon.wxjava.miniapp.service; + + +import cn.binarywang.wx.miniapp.api.WxMaService; + +/** + * 微信小程序 {@link WxMaService} 所有实例存放类. + * + * @author monch + * created on 2024/9/6 + */ +public interface WxMaMultiServices { + /** + * 通过租户 Id 获取 WxMaService + * + * @param tenantId 租户 Id + * @return WxMaService + */ + WxMaService getWxMaService(String tenantId); + + /** + * 根据租户 Id,从列表中移除一个 WxMaService 实例 + * + * @param tenantId 租户 Id + */ + void removeWxMaService(String tenantId); +} diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/service/WxMaMultiServicesImpl.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/service/WxMaMultiServicesImpl.java new file mode 100644 index 0000000000..d0ba21cdb8 --- /dev/null +++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/service/WxMaMultiServicesImpl.java @@ -0,0 +1,36 @@ +package com.binarywang.solon.wxjava.miniapp.service; + +import cn.binarywang.wx.miniapp.api.WxMaService; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 微信小程序 {@link WxMaMultiServices} 默认实现 + * + * @author monch + * created on 2024/9/6 + */ +public class WxMaMultiServicesImpl implements WxMaMultiServices { + private final Map services = new ConcurrentHashMap<>(); + + @Override + public WxMaService getWxMaService(String tenantId) { + return this.services.get(tenantId); + } + + /** + * 根据租户 Id,添加一个 WxMaService 到列表 + * + * @param tenantId 租户 Id + * @param wxMaService WxMaService 实例 + */ + public void addWxMaService(String tenantId, WxMaService wxMaService) { + this.services.put(tenantId, wxMaService); + } + + @Override + public void removeWxMaService(String tenantId) { + this.services.remove(tenantId); + } +} diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-miniapp-multi-solon-plugin.properties b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-miniapp-multi-solon-plugin.properties new file mode 100644 index 0000000000..9d3e2557a8 --- /dev/null +++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-miniapp-multi-solon-plugin.properties @@ -0,0 +1,2 @@ +solon.plugin=com.binarywang.solon.wxjava.miniapp.integration.WxMiniappMultiPluginImpl +solon.plugin.priority=10 diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/test/java/features/test/LoadTest.java new file mode 100644 index 0000000000..d049f5a51a --- /dev/null +++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/test/java/features/test/LoadTest.java @@ -0,0 +1,15 @@ +package features.test; + +import org.junit.jupiter.api.Test; +import org.noear.solon.test.SolonTest; + +/** + * @author noear 2024/9/4 created + */ +@SolonTest +public class LoadTest { + @Test + public void load(){ + + } +} diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/test/resources/app.properties b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/test/resources/app.properties new file mode 100644 index 0000000000..6522b172c6 --- /dev/null +++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/src/test/resources/app.properties @@ -0,0 +1,38 @@ +# 公众号配置 +## 应用 1 配置(必填) +wx.ma.apps.tenantId1.app-id=appId +wx.ma.apps.tenantId1.app-secret=@secret +## 选填 +wx.ma.apps.tenantId1.token=@token +wx.ma.apps.tenantId1.aes-key=@aesKey +wx.ma.apps.tenantId1.use-stable-access-token=@useStableAccessToken +## 应用 2 配置(必填) +wx.ma.apps.tenantId2.app-id=@appId +wx.ma.apps.tenantId2.app-secret =@secret +## 选填 +wx.ma.apps.tenantId2.token=@token +wx.ma.apps.tenantId2.aes-key=@aesKey +wx.ma.apps.tenantId2.use-stable-access-token=@useStableAccessToken + +# ConfigStorage 配置(选填) +## 配置类型: memory(默认), jedis, redisson +wx.ma.config-storage.type=memory +## 相关redis前缀配置: wx:ma:multi(默认) +wx.ma.config-storage.key-prefix=wx:ma:multi +wx.ma.config-storage.redis.host=127.0.0.1 +wx.ma.config-storage.redis.port=6379 +## 单机和 sentinel 同时存在时,优先使用sentinel配置 +# wx.ma.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379 +# wx.ma.config-storage.redis.sentinel-name=mymaster + +# http 客户端配置(选填) +## # http客户端类型: http_client(默认), ok_http, jodd_http +wx.ma.config-storage.http-client-type=http_client +wx.ma.config-storage.http-proxy-host= +wx.ma.config-storage.http-proxy-port= +wx.ma.config-storage.http-proxy-username= +wx.ma.config-storage.http-proxy-password= +## 最大重试次数,默认:5 次,如果小于 0,则为 0 +wx.ma.config-storage.max-retry-times=5 +## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000 +wx.ma.config-storage.retry-sleep-millis=1000 diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/README.md b/solon-plugins/wx-java-miniapp-solon-plugin/README.md new file mode 100644 index 0000000000..3d1d7517f7 --- /dev/null +++ b/solon-plugins/wx-java-miniapp-solon-plugin/README.md @@ -0,0 +1,35 @@ +# wx-java-miniapp-solon-plugin +## 快速开始 +1. 引入依赖 + ```xml + + com.github.binarywang + wx-java-miniapp-solon-plugin + ${version} + + ``` +2. 添加配置(app.properties) + ```properties + # 公众号配置(必填) + wx.miniapp.appid = appId + wx.miniapp.secret = @secret + wx.miniapp.token = @token + wx.miniapp.aesKey = @aesKey + wx.miniapp.msgDataFormat = @msgDataFormat # 消息格式,XML或者JSON. + # 存储配置redis(可选) + # 注意: 指定redis.host值后不会使用容器注入的redis连接(JedisPool) + wx.miniapp.config-storage.type = Jedis # 配置类型: Memory(默认), Jedis, RedisTemplate + wx.miniapp.config-storage.key-prefix = wa # 相关redis前缀配置: wa(默认) + wx.miniapp.config-storage.redis.host = 127.0.0.1 + wx.miniapp.config-storage.redis.port = 6379 + # http客户端配置 + wx.miniapp.config-storage.http-client-type=HttpClient # http客户端类型: HttpClient(默认), OkHttp, JoddHttp + wx.miniapp.config-storage.http-proxy-host= + wx.miniapp.config-storage.http-proxy-port= + wx.miniapp.config-storage.http-proxy-username= + wx.miniapp.config-storage.http-proxy-password= + ``` +3. 自动注入的类型 +- `WxMaService` +- `WxMaConfig` + diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml b/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml new file mode 100644 index 0000000000..0651e3b9b5 --- /dev/null +++ b/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml @@ -0,0 +1,43 @@ + + + + wx-java-solon-plugins + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-miniapp-solon-plugin + WxJava - Solon Plugin for MiniApp + 微信小程序开发的 Solon Plugin + + + + com.github.binarywang + weixin-java-miniapp + ${project.version} + + + redis.clients + jedis + provided + + + org.redisson + redisson + provided + + + org.jodd + jodd-http + provided + + + com.squareup.okhttp3 + okhttp + provided + + + + diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/WxMaServiceAutoConfiguration.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/WxMaServiceAutoConfiguration.java new file mode 100644 index 0000000000..78f95380b2 --- /dev/null +++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/WxMaServiceAutoConfiguration.java @@ -0,0 +1,58 @@ +package com.binarywang.solon.wxjava.miniapp.config; + +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceHttpClientImpl; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceHttpComponentsImpl; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceJoddHttpImpl; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceOkHttpImpl; +import cn.binarywang.wx.miniapp.config.WxMaConfig; +import com.binarywang.solon.wxjava.miniapp.enums.HttpClientType; +import com.binarywang.solon.wxjava.miniapp.properties.WxMaProperties; +import lombok.AllArgsConstructor; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +/** + * 微信小程序平台相关服务自动注册. + * + * @author someone TaoYu + */ +@Configuration +@AllArgsConstructor +public class WxMaServiceAutoConfiguration { + + private final WxMaProperties wxMaProperties; + + /** + * 小程序service. + * + * @return 小程序service + */ + @Bean + @Condition(onMissingBean=WxMaService.class, onBean=WxMaConfig.class) + public WxMaService wxMaService(WxMaConfig wxMaConfig) { + HttpClientType httpClientType = wxMaProperties.getConfigStorage().getHttpClientType(); + WxMaService wxMaService; + switch (httpClientType) { + case OkHttp: + wxMaService = new WxMaServiceOkHttpImpl(); + break; + case JoddHttp: + wxMaService = new WxMaServiceJoddHttpImpl(); + break; + case HttpClient: + wxMaService = new WxMaServiceHttpClientImpl(); + break; + case HttpComponents: + wxMaService = new WxMaServiceHttpComponentsImpl(); + break; + default: + wxMaService = new WxMaServiceImpl(); + break; + } + wxMaService.setWxMaConfig(wxMaConfig); + return wxMaService; + } +} diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/AbstractWxMaConfigStorageConfiguration.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/AbstractWxMaConfigStorageConfiguration.java new file mode 100644 index 0000000000..acc147a705 --- /dev/null +++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/AbstractWxMaConfigStorageConfiguration.java @@ -0,0 +1,40 @@ +package com.binarywang.solon.wxjava.miniapp.config.storage; + +import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; +import com.binarywang.solon.wxjava.miniapp.properties.WxMaProperties; +import org.apache.commons.lang3.StringUtils; + +/** + * @author yl TaoYu + */ +public abstract class AbstractWxMaConfigStorageConfiguration { + + protected WxMaDefaultConfigImpl config(WxMaDefaultConfigImpl config, WxMaProperties properties) { + config.setAppid(StringUtils.trimToNull(properties.getAppid())); + config.setSecret(StringUtils.trimToNull(properties.getSecret())); + config.setToken(StringUtils.trimToNull(properties.getToken())); + config.setAesKey(StringUtils.trimToNull(properties.getAesKey())); + config.setMsgDataFormat(StringUtils.trimToNull(properties.getMsgDataFormat())); + config.useStableAccessToken(properties.isUseStableAccessToken()); + + WxMaProperties.ConfigStorage configStorageProperties = properties.getConfigStorage(); + config.setHttpProxyHost(configStorageProperties.getHttpProxyHost()); + config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername()); + config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword()); + if (configStorageProperties.getHttpProxyPort() != null) { + config.setHttpProxyPort(configStorageProperties.getHttpProxyPort()); + } + + int maxRetryTimes = configStorageProperties.getMaxRetryTimes(); + if (configStorageProperties.getMaxRetryTimes() < 0) { + maxRetryTimes = 0; + } + int retrySleepMillis = configStorageProperties.getRetrySleepMillis(); + if (retrySleepMillis < 0) { + retrySleepMillis = 1000; + } + config.setRetrySleepMillis(retrySleepMillis); + config.setMaxRetryTimes(maxRetryTimes); + return config; + } +} diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/WxMaInJedisConfigStorageConfiguration.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/WxMaInJedisConfigStorageConfiguration.java new file mode 100644 index 0000000000..da8c4701ba --- /dev/null +++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/WxMaInJedisConfigStorageConfiguration.java @@ -0,0 +1,72 @@ +package com.binarywang.solon.wxjava.miniapp.config.storage; + +import cn.binarywang.wx.miniapp.config.WxMaConfig; +import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl; +import com.binarywang.solon.wxjava.miniapp.properties.RedisProperties; +import com.binarywang.solon.wxjava.miniapp.properties.WxMaProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.redis.JedisWxRedisOps; +import me.chanjar.weixin.common.redis.WxRedisOps; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * @author yl TaoYu + */ +@Configuration +@Condition( + onProperty = "${"+WxMaProperties.PREFIX + ".configStorage.type} = jedis", + onClass = JedisPool.class +) +@RequiredArgsConstructor +public class WxMaInJedisConfigStorageConfiguration extends AbstractWxMaConfigStorageConfiguration { + private final WxMaProperties properties; + private final AppContext applicationContext; + + @Bean + @Condition(onMissingBean=WxMaConfig.class) + public WxMaConfig wxMaConfig() { + WxMaRedisBetterConfigImpl config = getWxMaRedisBetterConfigImpl(); + return this.config(config, properties); + } + + private WxMaRedisBetterConfigImpl getWxMaRedisBetterConfigImpl() { + RedisProperties redisProperties = properties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + jedisPool = getJedisPool(); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + WxRedisOps redisOps = new JedisWxRedisOps(jedisPool); + return new WxMaRedisBetterConfigImpl(redisOps, properties.getConfigStorage().getKeyPrefix()); + } + + private JedisPool getJedisPool() { + WxMaProperties.ConfigStorage storage = properties.getConfigStorage(); + RedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(), redis.getDatabase()); + } +} diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/WxMaInMemoryConfigStorageConfiguration.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/WxMaInMemoryConfigStorageConfiguration.java new file mode 100644 index 0000000000..958742d2aa --- /dev/null +++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/WxMaInMemoryConfigStorageConfiguration.java @@ -0,0 +1,28 @@ +package com.binarywang.solon.wxjava.miniapp.config.storage; + +import cn.binarywang.wx.miniapp.config.WxMaConfig; +import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; +import com.binarywang.solon.wxjava.miniapp.properties.WxMaProperties; +import lombok.RequiredArgsConstructor; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +/** + * @author yl TaoYu + */ +@Configuration +@Condition( + onProperty = "${"+WxMaProperties.PREFIX + ".configStorage.type:memory} = memory" +) +@RequiredArgsConstructor +public class WxMaInMemoryConfigStorageConfiguration extends AbstractWxMaConfigStorageConfiguration { + private final WxMaProperties properties; + + @Bean + @Condition(onMissingBean=WxMaConfig.class) + public WxMaConfig wxMaConfig() { + WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl(); + return this.config(config, properties); + } +} diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/WxMaInRedissonConfigStorageConfiguration.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/WxMaInRedissonConfigStorageConfiguration.java new file mode 100644 index 0000000000..af7c11448e --- /dev/null +++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/config/storage/WxMaInRedissonConfigStorageConfiguration.java @@ -0,0 +1,61 @@ +package com.binarywang.solon.wxjava.miniapp.config.storage; + +import cn.binarywang.wx.miniapp.config.WxMaConfig; +import cn.binarywang.wx.miniapp.config.impl.WxMaRedissonConfigImpl; +import com.binarywang.solon.wxjava.miniapp.properties.RedisProperties; +import com.binarywang.solon.wxjava.miniapp.properties.WxMaProperties; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; + +/** + * @author yl TaoYu + */ +@Configuration +@Condition( + onProperty = "${"+WxMaProperties.PREFIX + ".configStorage.type} = redisson", + onClass = Redisson.class +) +@RequiredArgsConstructor +public class WxMaInRedissonConfigStorageConfiguration extends AbstractWxMaConfigStorageConfiguration { + private final WxMaProperties properties; + private final AppContext applicationContext; + + @Bean + @Condition(onMissingBean=WxMaConfig.class) + public WxMaConfig wxMaConfig() { + WxMaRedissonConfigImpl config = getWxMaInRedissonConfigStorage(); + return this.config(config, properties); + } + + private WxMaRedissonConfigImpl getWxMaInRedissonConfigStorage() { + RedisProperties redisProperties = properties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + redissonClient = getRedissonClient(); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxMaRedissonConfigImpl(redissonClient, properties.getConfigStorage().getKeyPrefix()); + } + + private RedissonClient getRedissonClient() { + WxMaProperties.ConfigStorage storage = properties.getConfigStorage(); + RedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redis.getHost() + ":" + redis.getPort()) + .setDatabase(redis.getDatabase()) + .setPassword(redis.getPassword()); + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/enums/HttpClientType.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/enums/HttpClientType.java new file mode 100644 index 0000000000..d116a30cf6 --- /dev/null +++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/enums/HttpClientType.java @@ -0,0 +1,26 @@ +package com.binarywang.solon.wxjava.miniapp.enums; + +/** + * httpclient类型. + * + * @author Binary Wang + * created on 2020-05-25 + */ +public enum HttpClientType { + /** + * HttpClient. + */ + HttpClient, + /** + * OkHttp. + */ + OkHttp, + /** + * JoddHttp. + */ + JoddHttp, + /** + * HttpComponents. + */ + HttpComponents, +} diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/enums/StorageType.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/enums/StorageType.java new file mode 100644 index 0000000000..b82261ba8a --- /dev/null +++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/enums/StorageType.java @@ -0,0 +1,26 @@ +package com.binarywang.solon.wxjava.miniapp.enums; + +/** + * storage类型. + * + * @author Binary Wang + * created on 2020-05-25 + */ +public enum StorageType { + /** + * 内存. + */ + Memory, + /** + * redis(JedisClient). + */ + Jedis, + /** + * redis(Redisson). + */ + Redisson, + /** + * redis(RedisTemplate). + */ + RedisTemplate +} diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/integration/WxMiniappPluginImpl.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/integration/WxMiniappPluginImpl.java new file mode 100644 index 0000000000..88d1c3023a --- /dev/null +++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/integration/WxMiniappPluginImpl.java @@ -0,0 +1,25 @@ +package com.binarywang.solon.wxjava.miniapp.integration; + +import com.binarywang.solon.wxjava.miniapp.config.WxMaServiceAutoConfiguration; +import com.binarywang.solon.wxjava.miniapp.config.storage.WxMaInJedisConfigStorageConfiguration; +import com.binarywang.solon.wxjava.miniapp.config.storage.WxMaInMemoryConfigStorageConfiguration; +import com.binarywang.solon.wxjava.miniapp.config.storage.WxMaInRedissonConfigStorageConfiguration; +import com.binarywang.solon.wxjava.miniapp.properties.WxMaProperties; +import org.noear.solon.core.AppContext; +import org.noear.solon.core.Plugin; + +/** + * @author noear 2024/9/2 created + */ +public class WxMiniappPluginImpl implements Plugin { + @Override + public void start(AppContext context) throws Throwable { + context.beanMake(WxMaProperties.class); + + context.beanMake(WxMaServiceAutoConfiguration.class); + + context.beanMake(WxMaInMemoryConfigStorageConfiguration.class); + context.beanMake(WxMaInJedisConfigStorageConfiguration.class); + context.beanMake(WxMaInRedissonConfigStorageConfiguration.class); + } +} diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/RedisProperties.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/RedisProperties.java new file mode 100644 index 0000000000..021a4b1b6b --- /dev/null +++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/RedisProperties.java @@ -0,0 +1,43 @@ +package com.binarywang.solon.wxjava.miniapp.properties; + +import lombok.Data; + +/** + * redis 配置. + * + * @author Binary Wang + * created on 2020-08-30 + */ +@Data +public class RedisProperties { + + /** + * 主机地址.不填则从solon容器内获取JedisPool + */ + private String host; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaProperties.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaProperties.java new file mode 100644 index 0000000000..4493b6aec5 --- /dev/null +++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/miniapp/properties/WxMaProperties.java @@ -0,0 +1,117 @@ +package com.binarywang.solon.wxjava.miniapp.properties; + +import com.binarywang.solon.wxjava.miniapp.enums.HttpClientType; +import com.binarywang.solon.wxjava.miniapp.enums.StorageType; +import lombok.Data; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +import static com.binarywang.solon.wxjava.miniapp.properties.WxMaProperties.PREFIX; + +/** + * 属性配置类. + * + * @author Binary Wang + * created on 2019-08-10 + */ +@Data +@Configuration +@Inject("${" + PREFIX + "}") +public class WxMaProperties { + public static final String PREFIX = "wx.miniapp"; + + /** + * 设置微信小程序的appid. + */ + private String appid; + + /** + * 设置微信小程序的Secret. + */ + private String secret; + + /** + * 设置微信小程序消息服务器配置的token. + */ + private String token; + + /** + * 设置微信小程序消息服务器配置的EncodingAESKey. + */ + private String aesKey; + + /** + * 消息格式,XML或者JSON. + */ + private String msgDataFormat; + + /** + * 是否使用稳定版 Access Token + */ + private boolean useStableAccessToken = false; + + /** + * 存储策略 + */ + private final ConfigStorage configStorage = new ConfigStorage(); + + @Data + public static class ConfigStorage { + + /** + * 存储类型. + */ + private StorageType type = StorageType.Memory; + + /** + * 指定key前缀. + */ + private String keyPrefix = "wa"; + + /** + * redis连接配置. + */ + private final RedisProperties redis = new RedisProperties(); + + /** + * http客户端类型. + */ + private HttpClientType httpClientType = HttpClientType.HttpComponents; + + /** + * http代理主机. + */ + private String httpProxyHost; + + /** + * http代理端口. + */ + private Integer httpProxyPort; + + /** + * http代理用户名. + */ + private String httpProxyUsername; + + /** + * http代理密码. + */ + private String httpProxyPassword; + + /** + * http 请求重试间隔 + *
+     *   {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setRetrySleepMillis(int)}
+     * 
+ */ + private int retrySleepMillis = 1000; + /** + * http 请求最大重试次数 + *
+     *   {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setMaxRetryTimes(int)}
+     * 
+ */ + private int maxRetryTimes = 5; + } + +} diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/main/resources/META-INF/solon/wx-java-miniapp-solon-plugin.properties b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/resources/META-INF/solon/wx-java-miniapp-solon-plugin.properties new file mode 100644 index 0000000000..ba1049647e --- /dev/null +++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/main/resources/META-INF/solon/wx-java-miniapp-solon-plugin.properties @@ -0,0 +1,2 @@ +solon.plugin=com.binarywang.solon.wxjava.miniapp.integration.WxMiniappPluginImpl +solon.plugin.priority=10 diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-miniapp-solon-plugin/src/test/java/features/test/LoadTest.java new file mode 100644 index 0000000000..d049f5a51a --- /dev/null +++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/test/java/features/test/LoadTest.java @@ -0,0 +1,15 @@ +package features.test; + +import org.junit.jupiter.api.Test; +import org.noear.solon.test.SolonTest; + +/** + * @author noear 2024/9/4 created + */ +@SolonTest +public class LoadTest { + @Test + public void load(){ + + } +} diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/src/test/resources/app.properties b/solon-plugins/wx-java-miniapp-solon-plugin/src/test/resources/app.properties new file mode 100644 index 0000000000..22a9a4e627 --- /dev/null +++ b/solon-plugins/wx-java-miniapp-solon-plugin/src/test/resources/app.properties @@ -0,0 +1,18 @@ +# ?????(??) +wx.miniapp.appid = appId +wx.miniapp.secret = @secret +wx.miniapp.token = @token +wx.miniapp.aesKey = @aesKey +wx.miniapp.msgDataFormat = @msgDataFormat # ?????XML??JSON. +# ????redis(??) +# ??: ??redis.host???????????redis??(JedisPool) +wx.miniapp.config-storage.type = Jedis # ????: Memory(??), Jedis, RedisTemplate +wx.miniapp.config-storage.key-prefix = wa # ??redis????: wa(??) +wx.miniapp.config-storage.redis.host = 127.0.0.1 +wx.miniapp.config-storage.redis.port = 6379 +# http????? +wx.miniapp.config-storage.http-client-type=HttpClient # http?????: HttpClient(??), OkHttp, JoddHttp +wx.miniapp.config-storage.http-proxy-host= +wx.miniapp.config-storage.http-proxy-port= +wx.miniapp.config-storage.http-proxy-username= +wx.miniapp.config-storage.http-proxy-password= diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/README.md b/solon-plugins/wx-java-mp-multi-solon-plugin/README.md new file mode 100644 index 0000000000..0d2b332d5a --- /dev/null +++ b/solon-plugins/wx-java-mp-multi-solon-plugin/README.md @@ -0,0 +1,100 @@ +# wx-java-mp-multi-solon-plugin + +## 快速开始 + +1. 引入依赖 + ```xml + + com.github.binarywang + wx-java-mp-multi-solon-plugin + ${version} + + ``` +2. 添加配置(app.properties) + ```properties + # 公众号配置 + ## 应用 1 配置(必填) + wx.mp.apps.tenantId1.app-id=appId + wx.mp.apps.tenantId1.app-secret=@secret + ## 选填 + wx.mp.apps.tenantId1.token=@token + wx.mp.apps.tenantId1.aes-key=@aesKey + wx.mp.apps.tenantId1.use-stable-access-token=@useStableAccessToken + ## 应用 2 配置(必填) + wx.mp.apps.tenantId2.app-id=@appId + wx.mp.apps.tenantId2.app-secret =@secret + ## 选填 + wx.mp.apps.tenantId2.token=@token + wx.mp.apps.tenantId2.aes-key=@aesKey + wx.mp.apps.tenantId2.use-stable-access-token=@useStableAccessToken + + # ConfigStorage 配置(选填) + ## 配置类型: memory(默认), jedis, redisson, redis_template + wx.mp.config-storage.type=memory + ## 相关redis前缀配置: wx:mp:multi(默认) + wx.mp.config-storage.key-prefix=wx:mp:multi + wx.mp.config-storage.redis.host=127.0.0.1 + wx.mp.config-storage.redis.port=6379 + ## 单机和 sentinel 同时存在时,优先使用sentinel配置 + # wx.mp.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379 + # wx.mp.config-storage.redis.sentinel-name=mymaster + + # http 客户端配置(选填) + ## # http客户端类型: http_client(默认), ok_http, jodd_http + wx.mp.config-storage.http-client-type=http_client + wx.mp.config-storage.http-proxy-host= + wx.mp.config-storage.http-proxy-port= + wx.mp.config-storage.http-proxy-username= + wx.mp.config-storage.http-proxy-password= + ## 最大重试次数,默认:5 次,如果小于 0,则为 0 + wx.mp.config-storage.max-retry-times=5 + ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000 + wx.mp.config-storage.retry-sleep-millis=1000 + + # 公众号地址 host 配置 + # wx.mp.hosts.api-host=http://proxy.com/ + # wx.mp.hosts.open-host=http://proxy.com/ + # wx.mp.hosts.mp-host=http://proxy.com/ + ``` +3. 自动注入的类型:`WxMpMultiServices` + +4. 使用样例 + +```java +import com.binarywang.solon.wxjava.mp_multi.service.WxMpMultiServices; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.api.WxMpUserService; +import org.noear.solon.annotation.Component; +import org.noear.solon.annotation.Inject; + +@Component +public class DemoService { + @Inject + private WxMpMultiServices wxMpMultiServices; + + public void test() { + // 应用 1 的 WxMpService + WxMpService wxMpService1 = wxMpMultiServices.getWxMpService("tenantId1"); + WxMpUserService userService1 = wxMpService1.getUserService(); + userService1.userInfo("xxx"); + // todo ... + + // 应用 2 的 WxMpService + WxMpService wxMpService2 = wxMpMultiServices.getWxMpService("tenantId2"); + WxMpUserService userService2 = wxMpService2.getUserService(); + userService2.userInfo("xxx"); + // todo ... + + // 应用 3 的 WxMpService + WxMpService wxMpService3 = wxMpMultiServices.getWxMpService("tenantId3"); + // 判断是否为空 + if (wxMpService3 == null) { + // todo wxMpService3 为空,请先配置 tenantId3 微信公众号应用参数 + return; + } + WxMpUserService userService3 = wxMpService3.getUserService(); + userService3.userInfo("xxx"); + // todo ... + } +} +``` diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml new file mode 100644 index 0000000000..4dc7eae667 --- /dev/null +++ b/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml @@ -0,0 +1,44 @@ + + + + wx-java-solon-plugins + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-mp-multi-solon-plugin + WxJava - Solon Plugin for MP::支持多账号配置 + 微信公众号开发的 Solon Plugin::支持多账号配置 + + + + com.github.binarywang + weixin-java-mp + ${project.version} + + + redis.clients + jedis + provided + + + org.redisson + redisson + provided + + + org.jodd + jodd-http + provided + + + com.squareup.okhttp3 + okhttp + provided + + + + diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/AbstractWxMpConfiguration.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/AbstractWxMpConfiguration.java new file mode 100644 index 0000000000..a51c6eaaea --- /dev/null +++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/AbstractWxMpConfiguration.java @@ -0,0 +1,169 @@ +package com.binarywang.solon.wxjava.mp_multi.configuration.services; + +import com.binarywang.solon.wxjava.mp_multi.properties.WxMpMultiProperties; +import com.binarywang.solon.wxjava.mp_multi.properties.WxMpSingleProperties; +import com.binarywang.solon.wxjava.mp_multi.service.WxMpMultiServices; +import com.binarywang.solon.wxjava.mp_multi.service.WxMpMultiServicesImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpClientImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpComponentsImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceJoddHttpImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceOkHttpImpl; +import me.chanjar.weixin.mp.config.WxMpConfigStorage; +import me.chanjar.weixin.mp.config.WxMpHostConfig; +import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * WxMpConfigStorage 抽象配置类 + * + * @author yl + * created on 2024/1/23 + */ +@RequiredArgsConstructor +@Slf4j +public abstract class AbstractWxMpConfiguration { + + protected WxMpMultiServices wxMpMultiServices(WxMpMultiProperties wxCpMultiProperties) { + Map appsMap = wxCpMultiProperties.getApps(); + if (appsMap == null || appsMap.isEmpty()) { + log.warn("微信公众号应用参数未配置,通过 WxMpMultiServices#getWxMpService(\"tenantId\")获取实例将返回空"); + return new WxMpMultiServicesImpl(); + } + /** + * 校验 appId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。 + * + * 查看 {@link me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl#setAppId(String)} + */ + Collection apps = appsMap.values(); + if (apps.size() > 1) { + // 校验 appId 是否唯一 + boolean multi = apps.stream() + // 没有 appId,如果不判断是否为空,这里会报 NPE 异常 + .collect(Collectors.groupingBy(c -> c.getAppId() == null ? 0 : c.getAppId(), Collectors.counting())) + .entrySet().stream().anyMatch(e -> e.getValue() > 1); + if (multi) { + throw new RuntimeException("请确保微信公众号配置 appId 的唯一性"); + } + } + WxMpMultiServicesImpl services = new WxMpMultiServicesImpl(); + + Set> entries = appsMap.entrySet(); + for (Map.Entry entry : entries) { + String tenantId = entry.getKey(); + WxMpSingleProperties wxMpSingleProperties = entry.getValue(); + WxMpDefaultConfigImpl storage = this.wxMpConfigStorage(wxCpMultiProperties); + this.configApp(storage, wxMpSingleProperties); + this.configHttp(storage, wxCpMultiProperties.getConfigStorage()); + this.configHost(storage, wxCpMultiProperties.getHosts()); + WxMpService wxCpService = this.wxMpService(storage, wxCpMultiProperties); + services.addWxMpService(tenantId, wxCpService); + } + return services; + } + + /** + * 配置 WxMpDefaultConfigImpl + * + * @param wxMpMultiProperties 参数 + * @return WxMpDefaultConfigImpl + */ + protected abstract WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxMpMultiProperties); + + public WxMpService wxMpService(WxMpConfigStorage configStorage, WxMpMultiProperties wxMpMultiProperties) { + WxMpMultiProperties.ConfigStorage storage = wxMpMultiProperties.getConfigStorage(); + WxMpMultiProperties.HttpClientType httpClientType = storage.getHttpClientType(); + WxMpService wxMpService; + switch (httpClientType) { + case OK_HTTP: + wxMpService = new WxMpServiceOkHttpImpl(); + break; + case JODD_HTTP: + wxMpService = new WxMpServiceJoddHttpImpl(); + break; + case HTTP_CLIENT: + wxMpService = new WxMpServiceHttpClientImpl(); + break; + case HTTP_COMPONENTS: + wxMpService = new WxMpServiceHttpComponentsImpl(); + break; + default: + wxMpService = new WxMpServiceImpl(); + break; + } + + wxMpService.setWxMpConfigStorage(configStorage); + int maxRetryTimes = storage.getMaxRetryTimes(); + if (maxRetryTimes < 0) { + maxRetryTimes = 0; + } + int retrySleepMillis = storage.getRetrySleepMillis(); + if (retrySleepMillis < 0) { + retrySleepMillis = 1000; + } + wxMpService.setRetrySleepMillis(retrySleepMillis); + wxMpService.setMaxRetryTimes(maxRetryTimes); + return wxMpService; + } + + private void configApp(WxMpDefaultConfigImpl config, WxMpSingleProperties corpProperties) { + String appId = corpProperties.getAppId(); + String appSecret = corpProperties.getAppSecret(); + String token = corpProperties.getToken(); + String aesKey = corpProperties.getAesKey(); + boolean useStableAccessToken = corpProperties.isUseStableAccessToken(); + + config.setAppId(appId); + config.setSecret(appSecret); + if (StringUtils.isNotBlank(token)) { + config.setToken(token); + } + if (StringUtils.isNotBlank(aesKey)) { + config.setAesKey(aesKey); + } + config.setUseStableAccessToken(useStableAccessToken); + } + + private void configHttp(WxMpDefaultConfigImpl config, WxMpMultiProperties.ConfigStorage storage) { + String httpProxyHost = storage.getHttpProxyHost(); + Integer httpProxyPort = storage.getHttpProxyPort(); + String httpProxyUsername = storage.getHttpProxyUsername(); + String httpProxyPassword = storage.getHttpProxyPassword(); + if (StringUtils.isNotBlank(httpProxyHost)) { + config.setHttpProxyHost(httpProxyHost); + if (httpProxyPort != null) { + config.setHttpProxyPort(httpProxyPort); + } + if (StringUtils.isNotBlank(httpProxyUsername)) { + config.setHttpProxyUsername(httpProxyUsername); + } + if (StringUtils.isNotBlank(httpProxyPassword)) { + config.setHttpProxyPassword(httpProxyPassword); + } + } + } + + /** + * wx host config + */ + private void configHost(WxMpDefaultConfigImpl config, WxMpMultiProperties.HostConfig hostConfig) { + if (hostConfig != null) { + String apiHost = hostConfig.getApiHost(); + String mpHost = hostConfig.getMpHost(); + String openHost = hostConfig.getOpenHost(); + WxMpHostConfig wxMpHostConfig = new WxMpHostConfig(); + wxMpHostConfig.setApiHost(StringUtils.isNotBlank(apiHost) ? apiHost : null); + wxMpHostConfig.setMpHost(StringUtils.isNotBlank(mpHost) ? mpHost : null); + wxMpHostConfig.setOpenHost(StringUtils.isNotBlank(openHost) ? openHost : null); + config.setHostConfig(wxMpHostConfig); + } + } +} diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/WxMpInJedisConfiguration.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/WxMpInJedisConfiguration.java new file mode 100644 index 0000000000..c00898a82d --- /dev/null +++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/WxMpInJedisConfiguration.java @@ -0,0 +1,78 @@ +package com.binarywang.solon.wxjava.mp_multi.configuration.services; + +import com.binarywang.solon.wxjava.mp_multi.properties.WxMpMultiProperties; +import com.binarywang.solon.wxjava.mp_multi.properties.WxMpMultiRedisProperties; +import com.binarywang.solon.wxjava.mp_multi.service.WxMpMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.redis.JedisWxRedisOps; +import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; +import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * 自动装配基于 jedis 策略配置 + * + * @author yl + * created on 2024/1/23 + */ +@Configuration +@Condition( + onProperty = "${"+WxMpMultiProperties.PREFIX + ".configStorage.type} = jedis", + onClass = JedisPool.class +) +@RequiredArgsConstructor +public class WxMpInJedisConfiguration extends AbstractWxMpConfiguration { + private final WxMpMultiProperties wxCpMultiProperties; + private final AppContext applicationContext; + + @Bean + public WxMpMultiServices wxMpMultiServices() { + return this.wxMpMultiServices(wxCpMultiProperties); + } + + @Override + protected WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxCpMultiProperties) { + return this.configRedis(wxCpMultiProperties); + } + + private WxMpDefaultConfigImpl configRedis(WxMpMultiProperties wxCpMultiProperties) { + WxMpMultiRedisProperties wxCpMultiRedisProperties = wxCpMultiProperties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (wxCpMultiRedisProperties != null && StringUtils.isNotEmpty(wxCpMultiRedisProperties.getHost())) { + jedisPool = getJedisPool(wxCpMultiProperties); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + return new WxMpRedisConfigImpl(new JedisWxRedisOps(jedisPool), wxCpMultiProperties.getConfigStorage().getKeyPrefix()); + } + + private JedisPool getJedisPool(WxMpMultiProperties wxCpMultiProperties) { + WxMpMultiProperties.ConfigStorage storage = wxCpMultiProperties.getConfigStorage(); + WxMpMultiRedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), + redis.getTimeout(), redis.getPassword(), redis.getDatabase()); + } +} diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/WxMpInMemoryConfiguration.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/WxMpInMemoryConfiguration.java new file mode 100644 index 0000000000..74bc13e03e --- /dev/null +++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/WxMpInMemoryConfiguration.java @@ -0,0 +1,40 @@ +package com.binarywang.solon.wxjava.mp_multi.configuration.services; + +import com.binarywang.solon.wxjava.mp_multi.properties.WxMpMultiProperties; +import com.binarywang.solon.wxjava.mp_multi.service.WxMpMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; +import me.chanjar.weixin.mp.config.impl.WxMpMapConfigImpl; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +/** + * 自动装配基于内存策略配置 + * + * @author yl + * created on 2024/1/23 + */ +@Configuration +@Condition( + onProperty = "${"+WxMpMultiProperties.PREFIX + ".configStorage.type:memory} = memory" +) +@RequiredArgsConstructor +public class WxMpInMemoryConfiguration extends AbstractWxMpConfiguration { + private final WxMpMultiProperties wxCpMultiProperties; + + @Bean + public WxMpMultiServices wxCpMultiServices() { + return this.wxMpMultiServices(wxCpMultiProperties); + } + + @Override + protected WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxCpMultiProperties) { + return this.configInMemory(); + } + + private WxMpDefaultConfigImpl configInMemory() { + return new WxMpMapConfigImpl(); + // return new WxMpDefaultConfigImpl(); + } +} diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/WxMpInRedissonConfiguration.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/WxMpInRedissonConfiguration.java new file mode 100644 index 0000000000..89ffdfd912 --- /dev/null +++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/configuration/services/WxMpInRedissonConfiguration.java @@ -0,0 +1,68 @@ +package com.binarywang.solon.wxjava.mp_multi.configuration.services; + +import com.binarywang.solon.wxjava.mp_multi.properties.WxMpMultiProperties; +import com.binarywang.solon.wxjava.mp_multi.properties.WxMpMultiRedisProperties; +import com.binarywang.solon.wxjava.mp_multi.service.WxMpMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; +import me.chanjar.weixin.mp.config.impl.WxMpRedissonConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; + +/** + * 自动装配基于 redisson 策略配置 + * + * @author yl + * created on 2024/1/23 + */ +@Configuration +@Condition( + onProperty = "${"+WxMpMultiProperties.PREFIX + ".configStorage.type} = redisson", + onClass = Redisson.class +) +@RequiredArgsConstructor +public class WxMpInRedissonConfiguration extends AbstractWxMpConfiguration { + private final WxMpMultiProperties wxCpMultiProperties; + private final AppContext applicationContext; + + @Bean + public WxMpMultiServices wxMpMultiServices() { + return this.wxMpMultiServices(wxCpMultiProperties); + } + + @Override + protected WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxCpMultiProperties) { + return this.configRedisson(wxCpMultiProperties); + } + + private WxMpDefaultConfigImpl configRedisson(WxMpMultiProperties wxCpMultiProperties) { + WxMpMultiRedisProperties redisProperties = wxCpMultiProperties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + redissonClient = getRedissonClient(wxCpMultiProperties); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxMpRedissonConfigImpl(redissonClient, wxCpMultiProperties.getConfigStorage().getKeyPrefix()); + } + + private RedissonClient getRedissonClient(WxMpMultiProperties wxCpMultiProperties) { + WxMpMultiProperties.ConfigStorage storage = wxCpMultiProperties.getConfigStorage(); + WxMpMultiRedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redis.getHost() + ":" + redis.getPort()) + .setDatabase(redis.getDatabase()) + .setPassword(redis.getPassword()); + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/integration/WxMpMultiPluginImpl.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/integration/WxMpMultiPluginImpl.java new file mode 100644 index 0000000000..3629a8f78f --- /dev/null +++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/integration/WxMpMultiPluginImpl.java @@ -0,0 +1,23 @@ +package com.binarywang.solon.wxjava.mp_multi.integration; + +import com.binarywang.solon.wxjava.mp_multi.configuration.services.WxMpInJedisConfiguration; +import com.binarywang.solon.wxjava.mp_multi.configuration.services.WxMpInMemoryConfiguration; +import com.binarywang.solon.wxjava.mp_multi.configuration.services.WxMpInRedissonConfiguration; +import com.binarywang.solon.wxjava.mp_multi.properties.WxMpMultiProperties; +import org.noear.solon.core.AppContext; +import org.noear.solon.core.Plugin; + +/** + * @author noear 2024/9/2 created + */ +public class WxMpMultiPluginImpl implements Plugin { + @Override + public void start(AppContext context) throws Throwable { + context.beanMake(WxMpMultiProperties.class); + + context.beanMake(WxMpInJedisConfiguration.class); + context.beanMake(WxMpInMemoryConfiguration.class); + context.beanMake(WxMpInRedissonConfiguration.class); + + } +} diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/properties/WxMpMultiProperties.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/properties/WxMpMultiProperties.java new file mode 100644 index 0000000000..3d47f71381 --- /dev/null +++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/properties/WxMpMultiProperties.java @@ -0,0 +1,158 @@ +package com.binarywang.solon.wxjava.mp_multi.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * @author yl + * created on 2024/1/23 + */ +@Data +@NoArgsConstructor +@Configuration +@Inject("${"+WxMpMultiProperties.PREFIX+"}") +public class WxMpMultiProperties implements Serializable { + private static final long serialVersionUID = -5358245184407791011L; + public static final String PREFIX = "wx.mp"; + + private Map apps = new HashMap<>(); + + /** + * 自定义host配置 + */ + private HostConfig hosts; + + /** + * 存储策略 + */ + private final ConfigStorage configStorage = new ConfigStorage(); + + @Data + @NoArgsConstructor + public static class HostConfig implements Serializable { + private static final long serialVersionUID = -4172767630740346001L; + + /** + * 对应于:https://api.weixin.qq.com + */ + private String apiHost; + + /** + * 对应于:https://open.weixin.qq.com + */ + private String openHost; + + /** + * 对应于:https://mp.weixin.qq.com + */ + private String mpHost; + } + + @Data + @NoArgsConstructor + public static class ConfigStorage implements Serializable { + private static final long serialVersionUID = 4815731027000065434L; + + /** + * 存储类型. + */ + private StorageType type = StorageType.MEMORY; + + /** + * 指定key前缀. + */ + private String keyPrefix = "wx:mp:multi"; + + /** + * redis连接配置. + */ + private final WxMpMultiRedisProperties redis = new WxMpMultiRedisProperties(); + + /** + * http客户端类型. + */ + private HttpClientType httpClientType = HttpClientType.HTTP_COMPONENTS; + + /** + * http代理主机. + */ + private String httpProxyHost; + + /** + * http代理端口. + */ + private Integer httpProxyPort; + + /** + * http代理用户名. + */ + private String httpProxyUsername; + + /** + * http代理密码. + */ + private String httpProxyPassword; + + /** + * http 请求最大重试次数 + *
+     *   {@link me.chanjar.weixin.mp.api.WxMpService#setMaxRetryTimes(int)}
+     *   {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setMaxRetryTimes(int)}
+     * 
+ */ + private int maxRetryTimes = 5; + + /** + * http 请求重试间隔 + *
+     *   {@link me.chanjar.weixin.mp.api.WxMpService#setRetrySleepMillis(int)}
+     *   {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setRetrySleepMillis(int)}
+     * 
+ */ + private int retrySleepMillis = 1000; + } + + public enum StorageType { + /** + * 内存 + */ + MEMORY, + /** + * jedis + */ + JEDIS, + /** + * redisson + */ + REDISSON, + /** + * redisTemplate + */ + REDIS_TEMPLATE + } + + public enum HttpClientType { + /** + * HttpClient + */ + HTTP_CLIENT, + /** + * OkHttp + */ + OK_HTTP, + /** + * JoddHttp + */ + JODD_HTTP, + /** + * HttpComponents + */ + HTTP_COMPONENTS + } +} diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/properties/WxMpMultiRedisProperties.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/properties/WxMpMultiRedisProperties.java new file mode 100644 index 0000000000..12646d4eaf --- /dev/null +++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/properties/WxMpMultiRedisProperties.java @@ -0,0 +1,56 @@ +package com.binarywang.solon.wxjava.mp_multi.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * @author yl + * created on 2024/1/23 + */ +@Data +@NoArgsConstructor +public class WxMpMultiRedisProperties implements Serializable { + private static final long serialVersionUID = -5924815351660074401L; + + /** + * 主机地址. + */ + private String host = "127.0.0.1"; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + /** + * sentinel ips + */ + private String sentinelIps; + + /** + * sentinel name + */ + private String sentinelName; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/properties/WxMpSingleProperties.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/properties/WxMpSingleProperties.java new file mode 100644 index 0000000000..22938cb67c --- /dev/null +++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/properties/WxMpSingleProperties.java @@ -0,0 +1,40 @@ +package com.binarywang.solon.wxjava.mp_multi.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * @author yl + * created on 2024/1/23 + */ +@Data +@NoArgsConstructor +public class WxMpSingleProperties implements Serializable { + private static final long serialVersionUID = 1980986361098922525L; + /** + * 设置微信公众号的 appid. + */ + private String appId; + + /** + * 设置微信公众号的 app secret. + */ + private String appSecret; + + /** + * 设置微信公众号的 token. + */ + private String token; + + /** + * 设置微信公众号的 EncodingAESKey. + */ + private String aesKey; + + /** + * 是否使用稳定版 Access Token + */ + private boolean useStableAccessToken = false; +} diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/service/WxMpMultiServices.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/service/WxMpMultiServices.java new file mode 100644 index 0000000000..a59b5962ad --- /dev/null +++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/service/WxMpMultiServices.java @@ -0,0 +1,27 @@ +package com.binarywang.solon.wxjava.mp_multi.service; + + +import me.chanjar.weixin.mp.api.WxMpService; + +/** + * 企业微信 {@link WxMpService} 所有实例存放类. + * + * @author yl + * created on 2024/1/23 + */ +public interface WxMpMultiServices { + /** + * 通过租户 Id 获取 WxMpService + * + * @param tenantId 租户 Id + * @return WxMpService + */ + WxMpService getWxMpService(String tenantId); + + /** + * 根据租户 Id,从列表中移除一个 WxMpService 实例 + * + * @param tenantId 租户 Id + */ + void removeWxMpService(String tenantId); +} diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/service/WxMpMultiServicesImpl.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/service/WxMpMultiServicesImpl.java new file mode 100644 index 0000000000..d87cd4e8df --- /dev/null +++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp_multi/service/WxMpMultiServicesImpl.java @@ -0,0 +1,36 @@ +package com.binarywang.solon.wxjava.mp_multi.service; + +import me.chanjar.weixin.mp.api.WxMpService; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 企业微信 {@link WxMpMultiServices} 默认实现 + * + * @author yl + * created on 2024/1/23 + */ +public class WxMpMultiServicesImpl implements WxMpMultiServices { + private final Map services = new ConcurrentHashMap<>(); + + @Override + public WxMpService getWxMpService(String tenantId) { + return this.services.get(tenantId); + } + + /** + * 根据租户 Id,添加一个 WxMpService 到列表 + * + * @param tenantId 租户 Id + * @param wxMpService WxMpService 实例 + */ + public void addWxMpService(String tenantId, WxMpService wxMpService) { + this.services.put(tenantId, wxMpService); + } + + @Override + public void removeWxMpService(String tenantId) { + this.services.remove(tenantId); + } +} diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-mp-multi-solon-plugin.properties b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-mp-multi-solon-plugin.properties new file mode 100644 index 0000000000..11c68ccc81 --- /dev/null +++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/main/resources/META-INF/solon/wx-java-mp-multi-solon-plugin.properties @@ -0,0 +1,2 @@ +solon.plugin=com.binarywang.solon.wxjava.mp_multi.integration.WxMpMultiPluginImpl +solon.plugin.priority=10 diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-mp-multi-solon-plugin/src/test/java/features/test/LoadTest.java new file mode 100644 index 0000000000..d049f5a51a --- /dev/null +++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/test/java/features/test/LoadTest.java @@ -0,0 +1,15 @@ +package features.test; + +import org.junit.jupiter.api.Test; +import org.noear.solon.test.SolonTest; + +/** + * @author noear 2024/9/4 created + */ +@SolonTest +public class LoadTest { + @Test + public void load(){ + + } +} diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/src/test/resources/app.properties b/solon-plugins/wx-java-mp-multi-solon-plugin/src/test/resources/app.properties new file mode 100644 index 0000000000..3f3b21657c --- /dev/null +++ b/solon-plugins/wx-java-mp-multi-solon-plugin/src/test/resources/app.properties @@ -0,0 +1,23 @@ +# ????? +## ?? 1 ??(??) +wx.mp.apps.tenantId1.app-id=appId +wx.mp.apps.tenantId1.app-secret=@secret +## ?? +wx.mp.apps.tenantId1.token=@token +wx.mp.apps.tenantId1.aes-key=@aesKey +wx.mp.apps.tenantId1.use-stable-access-token=@useStableAccessToken +## ?? 2 ??(??) +wx.mp.apps.tenantId2.app-id=@appId +wx.mp.apps.tenantId2.app-secret =@secret +## ?? +wx.mp.apps.tenantId2.token=@token +wx.mp.apps.tenantId2.aes-key=@aesKey +wx.mp.apps.tenantId2.use-stable-access-token=@useStableAccessToken + +# ConfigStorage ?????? +## ????: memory(??), jedis, redisson, redis_template +wx.mp.config-storage.type=memory +## ??redis????: wx:mp:multi(??) +wx.mp.config-storage.key-prefix=wx:mp:multi +wx.mp.config-storage.redis.host=127.0.0.1 +wx.mp.config-storage.redis.port=6379 diff --git a/solon-plugins/wx-java-mp-solon-plugin/README.md b/solon-plugins/wx-java-mp-solon-plugin/README.md new file mode 100644 index 0000000000..58dcbfddbe --- /dev/null +++ b/solon-plugins/wx-java-mp-solon-plugin/README.md @@ -0,0 +1,46 @@ +# wx-java-mp-solon-plugin + +## 快速开始 + +1. 引入依赖 + ```xml + + com.github.binarywang + wx-java-mp-solon-plugin + ${version} + + ``` +2. 添加配置(app.properties) + ```properties + # 公众号配置(必填) + wx.mp.app-id=appId + wx.mp.secret=@secret + wx.mp.token=@token + wx.mp.aes-key=@aesKey + wx.mp.use-stable-access-token=@useStableAccessToken + # 存储配置redis(可选) + wx.mp.config-storage.type= edis # 配置类型: Memory(默认), Jedis, RedisTemplate + wx.mp.config-storage.key-prefix=wx # 相关redis前缀配置: wx(默认) + wx.mp.config-storage.redis.host=127.0.0.1 + wx.mp.config-storage.redis.port=6379 + #单机和sentinel同时存在时,优先使用sentinel配置 + #wx.mp.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379 + #wx.mp.config-storage.redis.sentinel-name=mymaster + # http客户端配置 + wx.mp.config-storage.http-client-type=httpclient # http客户端类型: HttpClient(默认), OkHttp, JoddHttp + wx.mp.config-storage.http-proxy-host= + wx.mp.config-storage.http-proxy-port= + wx.mp.config-storage.http-proxy-username= + wx.mp.config-storage.http-proxy-password= + # 公众号地址host配置 + #wx.mp.hosts.api-host=http://proxy.com/ + #wx.mp.hosts.open-host=http://proxy.com/ + #wx.mp.hosts.mp-host=http://proxy.com/ + ``` +3. 自动注入的类型 + +- `WxMpService` +- `WxMpConfigStorage` + +4、参考demo: +https://github.com/binarywang/wx-java-mp-demo diff --git a/solon-plugins/wx-java-mp-solon-plugin/pom.xml b/solon-plugins/wx-java-mp-solon-plugin/pom.xml new file mode 100644 index 0000000000..e0c79f79bf --- /dev/null +++ b/solon-plugins/wx-java-mp-solon-plugin/pom.xml @@ -0,0 +1,44 @@ + + + + wx-java-solon-plugins + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-mp-solon-plugin + WxJava - Solon Plugin for MP + 微信公众号开发的 Solon Plugin + + + + com.github.binarywang + weixin-java-mp + ${project.version} + + + redis.clients + jedis + provided + + + org.redisson + redisson + provided + + + org.jodd + jodd-http + provided + + + com.squareup.okhttp3 + okhttp + provided + + + + diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/WxMpServiceAutoConfiguration.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/WxMpServiceAutoConfiguration.java new file mode 100644 index 0000000000..334ccf7abe --- /dev/null +++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/WxMpServiceAutoConfiguration.java @@ -0,0 +1,71 @@ +package com.binarywang.solon.wxjava.mp.config; + +import com.binarywang.solon.wxjava.mp.enums.HttpClientType; +import com.binarywang.solon.wxjava.mp.properties.WxMpProperties; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpClientImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpComponentsImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceJoddHttpImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceOkHttpImpl; +import me.chanjar.weixin.mp.config.WxMpConfigStorage; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +/** + * 微信公众号相关服务自动注册. + * + * @author someone + */ +@Configuration +public class WxMpServiceAutoConfiguration { + + @Bean + @Condition(onMissingBean = WxMpService.class) + public WxMpService wxMpService(WxMpConfigStorage configStorage, WxMpProperties wxMpProperties) { + HttpClientType httpClientType = wxMpProperties.getConfigStorage().getHttpClientType(); + WxMpService wxMpService; + switch (httpClientType) { + case OkHttp: + wxMpService = newWxMpServiceOkHttpImpl(); + break; + case JoddHttp: + wxMpService = newWxMpServiceJoddHttpImpl(); + break; + case HttpClient: + wxMpService = newWxMpServiceHttpClientImpl(); + break; + case HttpComponents: + wxMpService = newWxMpServiceHttpComponentsImpl(); + break; + default: + wxMpService = newWxMpServiceImpl(); + break; + } + + wxMpService.setWxMpConfigStorage(configStorage); + return wxMpService; + } + + private WxMpService newWxMpServiceImpl() { + return new WxMpServiceImpl(); + } + + private WxMpService newWxMpServiceHttpClientImpl() { + return new WxMpServiceHttpClientImpl(); + } + + private WxMpService newWxMpServiceOkHttpImpl() { + return new WxMpServiceOkHttpImpl(); + } + + private WxMpService newWxMpServiceJoddHttpImpl() { + return new WxMpServiceJoddHttpImpl(); + } + + private WxMpService newWxMpServiceHttpComponentsImpl() { + return new WxMpServiceHttpComponentsImpl(); + } + +} diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/AbstractWxMpConfigStorageConfiguration.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/AbstractWxMpConfigStorageConfiguration.java new file mode 100644 index 0000000000..663bb13340 --- /dev/null +++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/AbstractWxMpConfigStorageConfiguration.java @@ -0,0 +1,27 @@ +package com.binarywang.solon.wxjava.mp.config.storage; + +import com.binarywang.solon.wxjava.mp.properties.WxMpProperties; +import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; + +/** + * @author zhangyl + */ +public abstract class AbstractWxMpConfigStorageConfiguration { + + protected WxMpDefaultConfigImpl config(WxMpDefaultConfigImpl config, WxMpProperties properties) { + config.setAppId(properties.getAppId()); + config.setSecret(properties.getSecret()); + config.setToken(properties.getToken()); + config.setAesKey(properties.getAesKey()); + config.setUseStableAccessToken(properties.isUseStableAccessToken()); + + WxMpProperties.ConfigStorage configStorageProperties = properties.getConfigStorage(); + config.setHttpProxyHost(configStorageProperties.getHttpProxyHost()); + config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername()); + config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword()); + if (configStorageProperties.getHttpProxyPort() != null) { + config.setHttpProxyPort(configStorageProperties.getHttpProxyPort()); + } + return config; + } +} diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInJedisConfigStorageConfiguration.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInJedisConfigStorageConfiguration.java new file mode 100644 index 0000000000..a949ccfaca --- /dev/null +++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInJedisConfigStorageConfiguration.java @@ -0,0 +1,76 @@ +package com.binarywang.solon.wxjava.mp.config.storage; + +import com.binarywang.solon.wxjava.mp.properties.RedisProperties; +import com.binarywang.solon.wxjava.mp.properties.WxMpProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.redis.JedisWxRedisOps; +import me.chanjar.weixin.common.redis.WxRedisOps; +import me.chanjar.weixin.mp.config.WxMpConfigStorage; +import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * @author zhangyl + */ +@Configuration +@Condition( + onProperty = "${" + WxMpProperties.PREFIX + ".config-storage.type} = jedis", + onClass = Jedis.class +) +@RequiredArgsConstructor +public class WxMpInJedisConfigStorageConfiguration extends AbstractWxMpConfigStorageConfiguration { + private final WxMpProperties properties; + private final AppContext applicationContext; + + @Bean + @Condition(onMissingBean = WxMpConfigStorage.class) + public WxMpConfigStorage wxMpConfigStorage() { + WxMpRedisConfigImpl config = getWxMpRedisConfigImpl(); + return this.config(config, properties); + } + + private WxMpRedisConfigImpl getWxMpRedisConfigImpl() { + RedisProperties redisProperties = properties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + jedisPool = applicationContext.getBean("wxMpJedisPool"); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + WxRedisOps redisOps = new JedisWxRedisOps(jedisPool); + return new WxMpRedisConfigImpl(redisOps, properties.getConfigStorage().getKeyPrefix()); + } + + @Bean + @Condition(onProperty = "${" + WxMpProperties.PREFIX + ".config-storage.redis.host}") + public JedisPool wxMpJedisPool() { + WxMpProperties.ConfigStorage storage = properties.getConfigStorage(); + RedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(), + redis.getDatabase()); + } +} diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInMemoryConfigStorageConfiguration.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInMemoryConfigStorageConfiguration.java new file mode 100644 index 0000000000..88994fcf42 --- /dev/null +++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInMemoryConfigStorageConfiguration.java @@ -0,0 +1,29 @@ +package com.binarywang.solon.wxjava.mp.config.storage; + +import com.binarywang.solon.wxjava.mp.properties.WxMpProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.mp.config.WxMpConfigStorage; +import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +/** + * @author zhangyl + */ +@Configuration +@Condition( + onProperty = "${" + WxMpProperties.PREFIX + ".config-storage.type:memory} = memory" +) +@RequiredArgsConstructor +public class WxMpInMemoryConfigStorageConfiguration extends AbstractWxMpConfigStorageConfiguration { + private final WxMpProperties properties; + + @Bean + @Condition(onMissingBean = WxMpConfigStorage.class) + public WxMpConfigStorage wxMpConfigStorage() { + WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl(); + config(config, properties); + return config; + } +} diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java new file mode 100644 index 0000000000..c1f5ebf0f3 --- /dev/null +++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java @@ -0,0 +1,65 @@ +package com.binarywang.solon.wxjava.mp.config.storage; + +import com.binarywang.solon.wxjava.mp.properties.RedisProperties; +import com.binarywang.solon.wxjava.mp.properties.WxMpProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.mp.config.WxMpConfigStorage; +import me.chanjar.weixin.mp.config.impl.WxMpRedissonConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; + +/** + * @author zhangyl + */ +@Configuration +@Condition( + onProperty = "${" + WxMpProperties.PREFIX + ".config-storage.type} = redisson", + onClass = Redisson.class +) +@RequiredArgsConstructor +public class WxMpInRedissonConfigStorageConfiguration extends AbstractWxMpConfigStorageConfiguration { + private final WxMpProperties properties; + private final AppContext applicationContext; + + @Bean + @Condition(onMissingBean = WxMpConfigStorage.class) + public WxMpConfigStorage wxMaConfig() { + WxMpRedissonConfigImpl config = getWxMpInRedissonConfigStorage(); + return this.config(config, properties); + } + + private WxMpRedissonConfigImpl getWxMpInRedissonConfigStorage() { + RedisProperties redisProperties = properties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + redissonClient = applicationContext.getBean("wxMpRedissonClient"); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxMpRedissonConfigImpl(redissonClient, properties.getConfigStorage().getKeyPrefix()); + } + + @Bean + @Condition(onProperty = "${" + WxMpProperties.PREFIX + ".config-storage.redis.host}") + public RedissonClient wxMpRedissonClient() { + WxMpProperties.ConfigStorage storage = properties.getConfigStorage(); + RedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redis.getHost() + ":" + redis.getPort()) + .setDatabase(redis.getDatabase()); + if (StringUtils.isNotBlank(redis.getPassword())) { + config.useSingleServer().setPassword(redis.getPassword()); + } + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/enums/HttpClientType.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/enums/HttpClientType.java new file mode 100644 index 0000000000..2858d77e43 --- /dev/null +++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/enums/HttpClientType.java @@ -0,0 +1,26 @@ +package com.binarywang.solon.wxjava.mp.enums; + +/** + * httpclient类型. + * + * @author Binary Wang + * created on 2020-08-30 + */ +public enum HttpClientType { + /** + * HttpClient. + */ + HttpClient, + /** + * OkHttp. + */ + OkHttp, + /** + * JoddHttp. + */ + JoddHttp, + /** + * HttpComponents. + */ + HttpComponents, +} diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/enums/StorageType.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/enums/StorageType.java new file mode 100644 index 0000000000..34433a8230 --- /dev/null +++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/enums/StorageType.java @@ -0,0 +1,26 @@ +package com.binarywang.solon.wxjava.mp.enums; + +/** + * storage类型. + * + * @author Binary Wang + * created on 2020-08-30 + */ +public enum StorageType { + /** + * 内存. + */ + Memory, + /** + * redis(JedisClient). + */ + Jedis, + /** + * redis(Redisson). + */ + Redisson, + /** + * redis(RedisTemplate). + */ + RedisTemplate +} diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/integration/WxMpPluginImpl.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/integration/WxMpPluginImpl.java new file mode 100644 index 0000000000..285d871f25 --- /dev/null +++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/integration/WxMpPluginImpl.java @@ -0,0 +1,29 @@ +package com.binarywang.solon.wxjava.mp.integration; + +import com.binarywang.solon.wxjava.mp.config.WxMpServiceAutoConfiguration; +import com.binarywang.solon.wxjava.mp.config.storage.WxMpInJedisConfigStorageConfiguration; +import com.binarywang.solon.wxjava.mp.config.storage.WxMpInMemoryConfigStorageConfiguration; +import com.binarywang.solon.wxjava.mp.config.storage.WxMpInRedissonConfigStorageConfiguration; +import com.binarywang.solon.wxjava.mp.properties.WxMpProperties; +import org.noear.solon.core.AppContext; +import org.noear.solon.core.Plugin; +import org.noear.solon.core.util.ClassUtil; + +/** + * @author noear 2024/9/2 created + */ +public class WxMpPluginImpl implements Plugin { + @Override + public void start(AppContext context) throws Throwable { + context.beanMake(WxMpProperties.class); + context.beanMake(WxMpServiceAutoConfiguration.class); + + context.beanMake(WxMpInMemoryConfigStorageConfiguration.class); + if (ClassUtil.loadClass("redis.clients.jedis.Jedis") != null) { + context.beanMake(WxMpInJedisConfigStorageConfiguration.class); + } + if (ClassUtil.loadClass("org.redisson.api.RedissonClient") != null) { + context.beanMake(WxMpInRedissonConfigStorageConfiguration.class); + } + } +} diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/HostConfig.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/HostConfig.java new file mode 100644 index 0000000000..8ccedf9294 --- /dev/null +++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/HostConfig.java @@ -0,0 +1,27 @@ +package com.binarywang.solon.wxjava.mp.properties; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class HostConfig implements Serializable { + + private static final long serialVersionUID = -4172767630740346001L; + + /** + * 对应于:https://api.weixin.qq.com + */ + private String apiHost; + + /** + * 对应于:https://open.weixin.qq.com + */ + private String openHost; + + /** + * 对应于:https://mp.weixin.qq.com + */ + private String mpHost; + +} diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/RedisProperties.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/RedisProperties.java new file mode 100644 index 0000000000..0376f947a7 --- /dev/null +++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/RedisProperties.java @@ -0,0 +1,56 @@ +package com.binarywang.solon.wxjava.mp.properties; + +import lombok.Data; + +import java.io.Serializable; + +/** + * redis 配置属性. + * + * @author Binary Wang + * created on 2020-08-30 + */ +@Data +public class RedisProperties implements Serializable { + private static final long serialVersionUID = -5924815351660074401L; + + /** + * 主机地址. + */ + private String host = "127.0.0.1"; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + /** + * sentinel ips + */ + private String sentinelIps; + + /** + * sentinel name + */ + private String sentinelName; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/WxMpProperties.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/WxMpProperties.java new file mode 100644 index 0000000000..0dcc6b41d3 --- /dev/null +++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/WxMpProperties.java @@ -0,0 +1,106 @@ +package com.binarywang.solon.wxjava.mp.properties; + +import com.binarywang.solon.wxjava.mp.enums.HttpClientType; +import com.binarywang.solon.wxjava.mp.enums.StorageType; +import lombok.Data; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +import java.io.Serializable; + +import static com.binarywang.solon.wxjava.mp.enums.StorageType.Memory; +import static com.binarywang.solon.wxjava.mp.properties.WxMpProperties.PREFIX; + +/** + * 微信接入相关配置属性. + * + * @author someone + */ +@Data +@Configuration +@Inject("${" + PREFIX + "}") +public class WxMpProperties { + public static final String PREFIX = "wx.mp"; + + /** + * 设置微信公众号的appid. + */ + private String appId; + + /** + * 设置微信公众号的app secret. + */ + private String secret; + + /** + * 设置微信公众号的token. + */ + private String token; + + /** + * 设置微信公众号的EncodingAESKey. + */ + private String aesKey; + + /** + * 是否使用稳定版 Access Token + */ + private boolean useStableAccessToken = false; + + /** + * 自定义host配置 + */ + private HostConfig hosts; + + /** + * 存储策略 + */ + private final ConfigStorage configStorage = new ConfigStorage(); + + @Data + public static class ConfigStorage implements Serializable { + private static final long serialVersionUID = 4815731027000065434L; + + /** + * 存储类型. + */ + private StorageType type = Memory; + + /** + * 指定key前缀. + */ + private String keyPrefix = "wx"; + + /** + * redis连接配置. + */ + private final RedisProperties redis = new RedisProperties(); + + /** + * http客户端类型. + */ + private HttpClientType httpClientType = HttpClientType.HttpComponents; + + /** + * http代理主机. + */ + private String httpProxyHost; + + /** + * http代理端口. + */ + private Integer httpProxyPort; + + /** + * http代理用户名. + */ + private String httpProxyUsername; + + /** + * http代理密码. + */ + private String httpProxyPassword; + + } + +} diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/main/resources/META-INF/solon/wx-java-mp-solon-plugin.properties b/solon-plugins/wx-java-mp-solon-plugin/src/main/resources/META-INF/solon/wx-java-mp-solon-plugin.properties new file mode 100644 index 0000000000..c80357184c --- /dev/null +++ b/solon-plugins/wx-java-mp-solon-plugin/src/main/resources/META-INF/solon/wx-java-mp-solon-plugin.properties @@ -0,0 +1,2 @@ +solon.plugin=com.binarywang.solon.wxjava.mp.integration.WxMpPluginImpl +solon.plugin.priority=10 diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-mp-solon-plugin/src/test/java/features/test/LoadTest.java new file mode 100644 index 0000000000..d049f5a51a --- /dev/null +++ b/solon-plugins/wx-java-mp-solon-plugin/src/test/java/features/test/LoadTest.java @@ -0,0 +1,15 @@ +package features.test; + +import org.junit.jupiter.api.Test; +import org.noear.solon.test.SolonTest; + +/** + * @author noear 2024/9/4 created + */ +@SolonTest +public class LoadTest { + @Test + public void load(){ + + } +} diff --git a/solon-plugins/wx-java-mp-solon-plugin/src/test/resources/app.properties b/solon-plugins/wx-java-mp-solon-plugin/src/test/resources/app.properties new file mode 100644 index 0000000000..06abfa5bb8 --- /dev/null +++ b/solon-plugins/wx-java-mp-solon-plugin/src/test/resources/app.properties @@ -0,0 +1,11 @@ +# ?????(??) +wx.mp.app-id=appId +wx.mp.secret=@secret +wx.mp.token=@token +wx.mp.aes-key=@aesKey +wx.mp.use-stable-access-token=@useStableAccessToken +# ????redis(??) # ????: Memory(??), Jedis, RedisTemplate +wx.mp.config-storage.type=memory +wx.mp.config-storage.key-prefix=wx +wx.mp.config-storage.redis.host=127.0.0.1 +wx.mp.config-storage.redis.port=6379 diff --git a/solon-plugins/wx-java-open-solon-plugin/README.md b/solon-plugins/wx-java-open-solon-plugin/README.md new file mode 100644 index 0000000000..619e28dbdd --- /dev/null +++ b/solon-plugins/wx-java-open-solon-plugin/README.md @@ -0,0 +1,39 @@ +# wx-java-open-solon-plugin +## 快速开始 +1. 引入依赖 + ```xml + + com.github.binarywang + wx-java-open-solon-plugin + ${version} + + ``` +2. 添加配置(app.properties) + ```properties + # 公众号配置(必填) + wx.open.appId = appId + wx.open.secret = @secret + wx.open.token = @token + wx.open.aesKey = @aesKey + # 存储配置redis(可选) + # 优先注入容器的(JedisPool, RedissonClient), 当配置了wx.open.config-storage.redis.host, 不会使用容器注入redis连接配置 + wx.open.config-storage.type = redis # 配置类型: memory(默认), redis(jedis), jedis, redisson, redistemplate + wx.open.config-storage.key-prefix = wx # 相关redis前缀配置: wx(默认) + wx.open.config-storage.redis.host = 127.0.0.1 + wx.open.config-storage.redis.port = 6379 + # http客户端配置 + wx.open.config-storage.http-client-type=httpclient # http客户端类型: httpclient(默认) + wx.open.config-storage.http-proxy-host= + wx.open.config-storage.http-proxy-port= + wx.open.config-storage.http-proxy-username= + wx.open.config-storage.http-proxy-password= + # 最大重试次数,默认:5 次,如果小于 0,则为 0 + wx.open.config-storage.max-retry-times=5 + # 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000 + wx.open.config-storage.retry-sleep-millis=1000 + ``` +3. 支持自动注入的类型: `WxOpenService, WxOpenMessageRouter, WxOpenComponentService` + +4. 覆盖自动配置: 自定义注入的bean会覆盖自动注入的 + - WxOpenConfigStorage + - WxOpenService diff --git a/solon-plugins/wx-java-open-solon-plugin/pom.xml b/solon-plugins/wx-java-open-solon-plugin/pom.xml new file mode 100644 index 0000000000..4cd4b1ac56 --- /dev/null +++ b/solon-plugins/wx-java-open-solon-plugin/pom.xml @@ -0,0 +1,32 @@ + + + + wx-java-solon-plugins + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-open-solon-plugin + WxJava - Solon Plugin for WxOpen + 微信开放平台开发的 Solon Plugin + + + + com.github.binarywang + weixin-java-open + ${project.version} + + + redis.clients + jedis + + + org.redisson + redisson + + + + diff --git a/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/WxOpenServiceAutoConfiguration.java b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/WxOpenServiceAutoConfiguration.java new file mode 100644 index 0000000000..7bda6816ed --- /dev/null +++ b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/WxOpenServiceAutoConfiguration.java @@ -0,0 +1,37 @@ +package com.binarywang.solon.wxjava.open.config; + +import me.chanjar.weixin.open.api.WxOpenComponentService; +import me.chanjar.weixin.open.api.WxOpenConfigStorage; +import me.chanjar.weixin.open.api.WxOpenService; +import me.chanjar.weixin.open.api.impl.WxOpenMessageRouter; +import me.chanjar.weixin.open.api.impl.WxOpenServiceImpl; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +/** + * 微信开放平台相关服务自动注册. + * + * @author someone + */ +@Configuration +public class WxOpenServiceAutoConfiguration { + + @Bean + @Condition(onMissingBean = WxOpenService.class, onBean = WxOpenConfigStorage.class) + public WxOpenService wxOpenService(WxOpenConfigStorage wxOpenConfigStorage) { + WxOpenService wxOpenService = new WxOpenServiceImpl(); + wxOpenService.setWxOpenConfigStorage(wxOpenConfigStorage); + return wxOpenService; + } + + @Bean + public WxOpenMessageRouter wxOpenMessageRouter(WxOpenService wxOpenService) { + return new WxOpenMessageRouter(wxOpenService); + } + + @Bean + public WxOpenComponentService wxOpenComponentService(WxOpenService wxOpenService) { + return wxOpenService.getWxOpenComponentService(); + } +} diff --git a/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/AbstractWxOpenConfigStorageConfiguration.java b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/AbstractWxOpenConfigStorageConfiguration.java new file mode 100644 index 0000000000..4a65b311d9 --- /dev/null +++ b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/AbstractWxOpenConfigStorageConfiguration.java @@ -0,0 +1,33 @@ +package com.binarywang.solon.wxjava.open.config.storage; + +import com.binarywang.solon.wxjava.open.properties.WxOpenProperties; +import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage; + +/** + * @author yl + */ +public abstract class AbstractWxOpenConfigStorageConfiguration { + + protected WxOpenInMemoryConfigStorage config(WxOpenInMemoryConfigStorage config, WxOpenProperties properties) { + WxOpenProperties.ConfigStorage storage = properties.getConfigStorage(); + config.setWxOpenInfo(properties.getAppId(), properties.getSecret(), properties.getToken(), properties.getAesKey()); + config.setHttpProxyHost(storage.getHttpProxyHost()); + config.setHttpProxyUsername(storage.getHttpProxyUsername()); + config.setHttpProxyPassword(storage.getHttpProxyPassword()); + Integer httpProxyPort = storage.getHttpProxyPort(); + if (httpProxyPort != null) { + config.setHttpProxyPort(httpProxyPort); + } + int maxRetryTimes = storage.getMaxRetryTimes(); + if (maxRetryTimes < 0) { + maxRetryTimes = 0; + } + int retrySleepMillis = storage.getRetrySleepMillis(); + if (retrySleepMillis < 0) { + retrySleepMillis = 1000; + } + config.setRetrySleepMillis(retrySleepMillis); + config.setMaxRetryTimes(maxRetryTimes); + return config; + } +} diff --git a/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/WxOpenInJedisConfigStorageConfiguration.java b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/WxOpenInJedisConfigStorageConfiguration.java new file mode 100644 index 0000000000..59e65ef48c --- /dev/null +++ b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/WxOpenInJedisConfigStorageConfiguration.java @@ -0,0 +1,71 @@ +package com.binarywang.solon.wxjava.open.config.storage; + +import com.binarywang.solon.wxjava.open.properties.WxOpenProperties; +import com.binarywang.solon.wxjava.open.properties.WxOpenRedisProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.open.api.WxOpenConfigStorage; +import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage; +import me.chanjar.weixin.open.api.impl.WxOpenInRedisConfigStorage; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * @author yl + */ +@Configuration +@Condition( + onProperty = "${"+WxOpenProperties.PREFIX + ".configStorage.type} = jedis", + onClass = JedisPool.class +) +@RequiredArgsConstructor +public class WxOpenInJedisConfigStorageConfiguration extends AbstractWxOpenConfigStorageConfiguration { + private final WxOpenProperties properties; + private final AppContext applicationContext; + + @Bean + @Condition(onMissingBean=WxOpenConfigStorage.class) + public WxOpenConfigStorage wxOpenConfigStorage() { + WxOpenInMemoryConfigStorage config = getWxOpenInRedisConfigStorage(); + return this.config(config, properties); + } + + private WxOpenInRedisConfigStorage getWxOpenInRedisConfigStorage() { + WxOpenRedisProperties wxOpenRedisProperties = properties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (wxOpenRedisProperties != null && StringUtils.isNotEmpty(wxOpenRedisProperties.getHost())) { + jedisPool = getJedisPool(); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + return new WxOpenInRedisConfigStorage(jedisPool, properties.getConfigStorage().getKeyPrefix()); + } + + private JedisPool getJedisPool() { + WxOpenProperties.ConfigStorage storage = properties.getConfigStorage(); + WxOpenRedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), + redis.getTimeout(), redis.getPassword(), redis.getDatabase()); + } +} diff --git a/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/WxOpenInMemoryConfigStorageConfiguration.java b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/WxOpenInMemoryConfigStorageConfiguration.java new file mode 100644 index 0000000000..756b6525fc --- /dev/null +++ b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/WxOpenInMemoryConfigStorageConfiguration.java @@ -0,0 +1,28 @@ +package com.binarywang.solon.wxjava.open.config.storage; + +import com.binarywang.solon.wxjava.open.properties.WxOpenProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.open.api.WxOpenConfigStorage; +import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +/** + * @author yl + */ +@Configuration +@Condition( + onProperty = "${"+WxOpenProperties.PREFIX + ".configStorage.type:memory} = memory" +) +@RequiredArgsConstructor +public class WxOpenInMemoryConfigStorageConfiguration extends AbstractWxOpenConfigStorageConfiguration { + private final WxOpenProperties properties; + + @Bean + @Condition(onMissingBean=WxOpenConfigStorage.class) + public WxOpenConfigStorage wxOpenConfigStorage() { + WxOpenInMemoryConfigStorage config = new WxOpenInMemoryConfigStorage(); + return this.config(config, properties); + } +} diff --git a/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/WxOpenInRedissonConfigStorageConfiguration.java b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/WxOpenInRedissonConfigStorageConfiguration.java new file mode 100644 index 0000000000..70844e2888 --- /dev/null +++ b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/config/storage/WxOpenInRedissonConfigStorageConfiguration.java @@ -0,0 +1,62 @@ +package com.binarywang.solon.wxjava.open.config.storage; + +import com.binarywang.solon.wxjava.open.properties.WxOpenProperties; +import com.binarywang.solon.wxjava.open.properties.WxOpenRedisProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.open.api.WxOpenConfigStorage; +import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage; +import me.chanjar.weixin.open.api.impl.WxOpenInRedissonConfigStorage; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.core.AppContext; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; + +/** + * @author yl + */ +@Configuration +@Condition( + onProperty = "${"+WxOpenProperties.PREFIX + ".configStorage.type} = redisson", + onClass = Redisson.class +) +@RequiredArgsConstructor +public class WxOpenInRedissonConfigStorageConfiguration extends AbstractWxOpenConfigStorageConfiguration { + private final WxOpenProperties properties; + private final AppContext applicationContext; + + @Bean + @Condition(onMissingBean=WxOpenConfigStorage.class) + public WxOpenConfigStorage wxOpenConfigStorage() { + WxOpenInMemoryConfigStorage config = getWxOpenInRedissonConfigStorage(); + return this.config(config, properties); + } + + private WxOpenInRedissonConfigStorage getWxOpenInRedissonConfigStorage() { + WxOpenRedisProperties wxOpenRedisProperties = properties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (wxOpenRedisProperties != null && StringUtils.isNotEmpty(wxOpenRedisProperties.getHost())) { + redissonClient = getRedissonClient(); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxOpenInRedissonConfigStorage(redissonClient, properties.getConfigStorage().getKeyPrefix()); + } + + private RedissonClient getRedissonClient() { + WxOpenProperties.ConfigStorage storage = properties.getConfigStorage(); + WxOpenRedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redis.getHost() + ":" + redis.getPort()) + .setDatabase(redis.getDatabase()) + .setPassword(redis.getPassword()); + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/integration/WxOpenPluginImpl.java b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/integration/WxOpenPluginImpl.java new file mode 100644 index 0000000000..29352d81f0 --- /dev/null +++ b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/integration/WxOpenPluginImpl.java @@ -0,0 +1,25 @@ +package com.binarywang.solon.wxjava.open.integration; + +import com.binarywang.solon.wxjava.open.config.WxOpenServiceAutoConfiguration; +import com.binarywang.solon.wxjava.open.config.storage.WxOpenInJedisConfigStorageConfiguration; +import com.binarywang.solon.wxjava.open.config.storage.WxOpenInMemoryConfigStorageConfiguration; +import com.binarywang.solon.wxjava.open.config.storage.WxOpenInRedissonConfigStorageConfiguration; +import com.binarywang.solon.wxjava.open.properties.WxOpenProperties; +import org.noear.solon.core.AppContext; +import org.noear.solon.core.Plugin; + +/** + * @author noear 2024/9/2 created + */ +public class WxOpenPluginImpl implements Plugin { + @Override + public void start(AppContext context) throws Throwable { + context.beanMake(WxOpenProperties.class); + + context.beanMake(WxOpenServiceAutoConfiguration.class); + + context.beanMake(WxOpenInMemoryConfigStorageConfiguration.class); + context.beanMake(WxOpenInJedisConfigStorageConfiguration.class); + context.beanMake(WxOpenInRedissonConfigStorageConfiguration.class); + } +} diff --git a/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/properties/WxOpenProperties.java b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/properties/WxOpenProperties.java new file mode 100644 index 0000000000..4ec34c02b8 --- /dev/null +++ b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/properties/WxOpenProperties.java @@ -0,0 +1,138 @@ +package com.binarywang.solon.wxjava.open.properties; + +import lombok.Data; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +import java.io.Serializable; + +import static com.binarywang.solon.wxjava.open.properties.WxOpenProperties.PREFIX; +import static com.binarywang.solon.wxjava.open.properties.WxOpenProperties.StorageType.memory; + + +/** + * 微信接入相关配置属性. + * + * @author someone + */ +@Data +@Configuration +@Inject("${"+PREFIX+"}") +public class WxOpenProperties { + public static final String PREFIX = "wx.open"; + + /** + * 设置微信开放平台的appid. + */ + private String appId; + + /** + * 设置微信开放平台的app secret. + */ + private String secret; + + /** + * 设置微信开放平台的token. + */ + private String token; + + /** + * 设置微信开放平台的EncodingAESKey. + */ + private String aesKey; + + /** + * 存储策略. + */ + private ConfigStorage configStorage = new ConfigStorage(); + + + @Data + public static class ConfigStorage implements Serializable { + private static final long serialVersionUID = 4815731027000065434L; + + /** + * 存储类型. + */ + private StorageType type = memory; + + /** + * 指定key前缀. + */ + private String keyPrefix = "wx:open"; + + /** + * redis连接配置. + */ + private WxOpenRedisProperties redis = new WxOpenRedisProperties(); + + /** + * http客户端类型. + */ + private HttpClientType httpClientType = HttpClientType.httpclient; + + /** + * http代理主机. + */ + private String httpProxyHost; + + /** + * http代理端口. + */ + private Integer httpProxyPort; + + /** + * http代理用户名. + */ + private String httpProxyUsername; + + /** + * http代理密码. + */ + private String httpProxyPassword; + + /** + * http 请求重试间隔 + *
+     *   {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setRetrySleepMillis(int)}
+     *   {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setRetrySleepMillis(int)}
+     * 
+ */ + private int retrySleepMillis = 1000; + /** + * http 请求最大重试次数 + *
+     *   {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setMaxRetryTimes(int)}
+     *   {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setMaxRetryTimes(int)}
+     * 
+ */ + private int maxRetryTimes = 5; + + } + + public enum StorageType { + /** + * 内存. + */ + memory, + /** + * jedis. + */ + jedis, + /** + * redisson. + */ + redisson, + /** + * redistemplate + */ + redistemplate + } + + public enum HttpClientType { + /** + * HttpClient. + */ + httpclient + } +} diff --git a/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/properties/WxOpenRedisProperties.java b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/properties/WxOpenRedisProperties.java new file mode 100644 index 0000000000..6b7a2d8654 --- /dev/null +++ b/solon-plugins/wx-java-open-solon-plugin/src/main/java/com/binarywang/solon/wxjava/open/properties/WxOpenRedisProperties.java @@ -0,0 +1,45 @@ +package com.binarywang.solon.wxjava.open.properties; + +import lombok.Data; + +import java.io.Serializable; + +/** + * Redis配置. + * + * @author someone + */ +@Data +public class WxOpenRedisProperties implements Serializable { + private static final long serialVersionUID = -5924815351660074401L; + + /** + * 主机地址. + */ + private String host; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/solon-plugins/wx-java-open-solon-plugin/src/main/resources/META-INF/solon/wx-java-open-solon-plugin.properties b/solon-plugins/wx-java-open-solon-plugin/src/main/resources/META-INF/solon/wx-java-open-solon-plugin.properties new file mode 100644 index 0000000000..289aca5eeb --- /dev/null +++ b/solon-plugins/wx-java-open-solon-plugin/src/main/resources/META-INF/solon/wx-java-open-solon-plugin.properties @@ -0,0 +1,2 @@ +solon.plugin=com.binarywang.solon.wxjava.open.integration.WxOpenPluginImpl +solon.plugin.priority=10 diff --git a/solon-plugins/wx-java-open-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-open-solon-plugin/src/test/java/features/test/LoadTest.java new file mode 100644 index 0000000000..d049f5a51a --- /dev/null +++ b/solon-plugins/wx-java-open-solon-plugin/src/test/java/features/test/LoadTest.java @@ -0,0 +1,15 @@ +package features.test; + +import org.junit.jupiter.api.Test; +import org.noear.solon.test.SolonTest; + +/** + * @author noear 2024/9/4 created + */ +@SolonTest +public class LoadTest { + @Test + public void load(){ + + } +} diff --git a/solon-plugins/wx-java-open-solon-plugin/src/test/resources/app.properties b/solon-plugins/wx-java-open-solon-plugin/src/test/resources/app.properties new file mode 100644 index 0000000000..fc2e79c95b --- /dev/null +++ b/solon-plugins/wx-java-open-solon-plugin/src/test/resources/app.properties @@ -0,0 +1,11 @@ +# ?????(??) +wx.open.appId = appId +wx.open.secret = @secret +wx.open.token = @token +wx.open.aesKey = @aesKey +# ????redis(??) +# ???????(JedisPool, RedissonClient), ????wx.open.config-storage.redis.host, ????????redis???? +wx.open.config-storage.type = redis # ????: memory(??), redis(jedis), jedis, redisson, redistemplate +wx.open.config-storage.key-prefix = wx # ??redis????: wx(??) +wx.open.config-storage.redis.host = 127.0.0.1 +wx.open.config-storage.redis.port = 6379 diff --git a/solon-plugins/wx-java-pay-solon-plugin/README.md b/solon-plugins/wx-java-pay-solon-plugin/README.md new file mode 100644 index 0000000000..8ff3416293 --- /dev/null +++ b/solon-plugins/wx-java-pay-solon-plugin/README.md @@ -0,0 +1,45 @@ +# 使用说明 +1. 在自己的Solon项目里,引入maven依赖 +```xml + + com.github.binarywang + wx-java-pay-solon-plugin + ${version} + + ``` +2. 添加配置(app.yml) +###### 1)V2版本 +```yml +wx: + pay: + appId: + mchId: + mchKey: + keyPath: +``` +###### 2)V3版本 +```yml +wx: + pay: + appId: xxxxxxxxxxx + mchId: 15xxxxxxxxx #商户id + apiHostUrl: http://10.0.0.1:3128 # 可选:代理主机 + apiHostUrlPath: /api-weixin # 可选:代理入口前缀 + apiV3Key: Dc1DBwSc094jACxxxxxxxxxxxxxxx #V3密钥 + certSerialNo: 62C6CEAA360BCxxxxxxxxxxxxxxx + privateKeyPath: classpath:cert/apiclient_key.pem #apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径 + privateCertPath: classpath:cert/apiclient_cert.pem #apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径 +``` +###### 3)V3服务商版本 +```yml +wx: + pay: #微信服务商支付 + configs: + - appId: wxe97b2x9c2b3d #spAppId + mchId: 16486610 #服务商商户 + subAppId: wx118cexxe3c07679 #子appId + subMchId: 16496705 #子商户 + apiV3Key: Dc1DBwSc094jAKDGR5aqqb7PTHr #apiV3密钥 + privateKeyPath: classpath:cert/apiclient_key.pem #服务商证书文件,apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径(可以配置绝对路径) + privateCertPath: classpath:cert/apiclient_cert.pem #apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径 +``` diff --git a/solon-plugins/wx-java-pay-solon-plugin/pom.xml b/solon-plugins/wx-java-pay-solon-plugin/pom.xml new file mode 100644 index 0000000000..607c138fd3 --- /dev/null +++ b/solon-plugins/wx-java-pay-solon-plugin/pom.xml @@ -0,0 +1,24 @@ + + + + wx-java-solon-plugins + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-pay-solon-plugin + WxJava - Solon Plugin for WxPay + 微信支付开发的 Solon Plugin + + + + com.github.binarywang + weixin-java-pay + ${project.version} + + + + diff --git a/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/config/WxPayAutoConfiguration.java b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/config/WxPayAutoConfiguration.java new file mode 100644 index 0000000000..c311a099a2 --- /dev/null +++ b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/config/WxPayAutoConfiguration.java @@ -0,0 +1,70 @@ +package com.binarywang.solon.wxjava.pay.config; + +import com.binarywang.solon.wxjava.pay.properties.WxPayProperties; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +/** + *
+ *  微信支付自动配置
+ *  Created by BinaryWang on 2019/4/17.
+ * 
+ * + * @author Binary Wang + */ +@Configuration +@Condition( + onProperty = "${wx.pay.enabled:true} = true", + onClass=WxPayService.class +) +public class WxPayAutoConfiguration { + private WxPayProperties properties; + + public WxPayAutoConfiguration(WxPayProperties properties) { + this.properties = properties; + } + + /** + * 构造微信支付服务对象. + * + * @return 微信支付service + */ + @Bean + @Condition(onMissingBean=WxPayService.class) + public WxPayService wxPayService() { + final WxPayServiceImpl wxPayService = new WxPayServiceImpl(); + WxPayConfig payConfig = new WxPayConfig(); + payConfig.setAppId(StringUtils.trimToNull(this.properties.getAppId())); + payConfig.setMchId(StringUtils.trimToNull(this.properties.getMchId())); + payConfig.setMchKey(StringUtils.trimToNull(this.properties.getMchKey())); + payConfig.setSubAppId(StringUtils.trimToNull(this.properties.getSubAppId())); + payConfig.setSubMchId(StringUtils.trimToNull(this.properties.getSubMchId())); + payConfig.setKeyPath(StringUtils.trimToNull(this.properties.getKeyPath())); + payConfig.setUseSandboxEnv(this.properties.isUseSandboxEnv()); + payConfig.setNotifyUrl(StringUtils.trimToNull(this.properties.getNotifyUrl())); + payConfig.setRefundNotifyUrl(StringUtils.trimToNull(this.properties.getRefundNotifyUrl())); + //以下是apiv3以及支付分相关 + payConfig.setServiceId(StringUtils.trimToNull(this.properties.getServiceId())); + payConfig.setPayScoreNotifyUrl(StringUtils.trimToNull(this.properties.getPayScoreNotifyUrl())); + payConfig.setPayScorePermissionNotifyUrl(StringUtils.trimToNull(this.properties.getPayScorePermissionNotifyUrl())); + payConfig.setPrivateKeyPath(StringUtils.trimToNull(this.properties.getPrivateKeyPath())); + payConfig.setPrivateCertPath(StringUtils.trimToNull(this.properties.getPrivateCertPath())); + payConfig.setCertSerialNo(StringUtils.trimToNull(this.properties.getCertSerialNo())); + payConfig.setApiV3Key(StringUtils.trimToNull(this.properties.getApiV3Key())); + payConfig.setPublicKeyId(StringUtils.trimToNull(this.properties.getPublicKeyId())); + payConfig.setPublicKeyPath(StringUtils.trimToNull(this.properties.getPublicKeyPath())); + payConfig.setApiHostUrl(StringUtils.trimToNull(this.properties.getApiHostUrl())); + payConfig.setApiHostUrlPath(StringUtils.trimToNull(this.properties.getApiHostUrlPath())); + payConfig.setStrictlyNeedWechatPaySerial(this.properties.isStrictlyNeedWechatPaySerial()); + payConfig.setFullPublicKeyModel(this.properties.isFullPublicKeyModel()); + + wxPayService.setConfig(payConfig); + return wxPayService; + } + +} diff --git a/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/integration/WxPayPluginImpl.java b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/integration/WxPayPluginImpl.java new file mode 100644 index 0000000000..e7ba275ca0 --- /dev/null +++ b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/integration/WxPayPluginImpl.java @@ -0,0 +1,18 @@ +package com.binarywang.solon.wxjava.pay.integration; + +import com.binarywang.solon.wxjava.pay.config.WxPayAutoConfiguration; +import com.binarywang.solon.wxjava.pay.properties.WxPayProperties; +import org.noear.solon.core.AppContext; +import org.noear.solon.core.Plugin; + +/** + * @author noear 2024/9/2 created + */ +public class WxPayPluginImpl implements Plugin { + @Override + public void start(AppContext context) throws Throwable { + context.beanMake(WxPayProperties.class); + + context.beanMake(WxPayAutoConfiguration.class); + } +} diff --git a/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/properties/WxPayProperties.java b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/properties/WxPayProperties.java new file mode 100644 index 0000000000..fe024f59f1 --- /dev/null +++ b/solon-plugins/wx-java-pay-solon-plugin/src/main/java/com/binarywang/solon/wxjava/pay/properties/WxPayProperties.java @@ -0,0 +1,132 @@ +package com.binarywang.solon.wxjava.pay.properties; + +import lombok.Data; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +/** + *
+ *  微信支付属性配置类
+ * Created by Binary Wang on 2019/4/17.
+ * 
+ * + * @author Binary Wang + */ +@Data +@Configuration +@Inject("${wx.pay}") +public class WxPayProperties { + /** + * 设置微信公众号或者小程序等的appid. + */ + private String appId; + + /** + * 微信支付商户号. + */ + private String mchId; + + /** + * 微信支付商户密钥. + */ + private String mchKey; + + /** + * 服务商模式下的子商户公众账号ID,普通模式请不要配置,请在配置文件中将对应项删除. + */ + private String subAppId; + + /** + * 服务商模式下的子商户号,普通模式请不要配置,最好是请在配置文件中将对应项删除. + */ + private String subMchId; + + /** + * apiclient_cert.p12文件的绝对路径,或者如果放在项目中,请以classpath:开头指定. + */ + private String keyPath; + + /** + * 微信支付分serviceId + */ + private String serviceId; + + /** + * 证书序列号 + */ + private String certSerialNo; + + /** + * apiV3秘钥 + */ + private String apiV3Key; + + /** + * 微信支付分回调地址 + */ + private String payScoreNotifyUrl; + + /** + * apiv3 商户apiclient_key.pem + */ + private String privateKeyPath; + + /** + * apiv3 商户apiclient_cert.pem + */ + private String privateCertPath; + + /** + * 微信支付是否使用仿真测试环境. + * 默认不使用 + */ + private boolean useSandboxEnv; + + /** + * 微信支付异步回调地址,通知url必须为直接可访问的url,不能携带参数 + */ + private String notifyUrl; + + /** + * 退款结果异步回调地址,通知url必须为直接可访问的url,不能携带参数. + */ + private String refundNotifyUrl; + + /** + * 微信支付分授权回调地址 + */ + private String payScorePermissionNotifyUrl; + + /** + * 公钥ID + */ + private String publicKeyId; + + /** + * pub_key.pem证书文件的绝对路径或者以classpath:开头的类路径. + */ + private String publicKeyPath; + + /** + * 自定义API主机地址,用于替换默认的 https://api.mch.weixin.qq.com + * 例如:http://proxy.company.com:8080 + */ + private String apiHostUrl; + + /** + * 自定义API主机路径前缀(用于代理入口前缀) + * 例如:/api-weixin + */ + private String apiHostUrlPath; + + /** + * 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认添加 + */ + private boolean strictlyNeedWechatPaySerial = true; + + /** + * 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认使用 + */ + private boolean fullPublicKeyModel = true; + +} diff --git a/solon-plugins/wx-java-pay-solon-plugin/src/main/resources/META-INF/solon/wx-java-pay-solon-plugin.properties b/solon-plugins/wx-java-pay-solon-plugin/src/main/resources/META-INF/solon/wx-java-pay-solon-plugin.properties new file mode 100644 index 0000000000..98783176e2 --- /dev/null +++ b/solon-plugins/wx-java-pay-solon-plugin/src/main/resources/META-INF/solon/wx-java-pay-solon-plugin.properties @@ -0,0 +1,2 @@ +solon.plugin=com.binarywang.solon.wxjava.pay.integration.WxPayPluginImpl +solon.plugin.priority=10 diff --git a/solon-plugins/wx-java-pay-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-pay-solon-plugin/src/test/java/features/test/LoadTest.java new file mode 100644 index 0000000000..d049f5a51a --- /dev/null +++ b/solon-plugins/wx-java-pay-solon-plugin/src/test/java/features/test/LoadTest.java @@ -0,0 +1,15 @@ +package features.test; + +import org.junit.jupiter.api.Test; +import org.noear.solon.test.SolonTest; + +/** + * @author noear 2024/9/4 created + */ +@SolonTest +public class LoadTest { + @Test + public void load(){ + + } +} diff --git a/solon-plugins/wx-java-pay-solon-plugin/src/test/resources/app.yml b/solon-plugins/wx-java-pay-solon-plugin/src/test/resources/app.yml new file mode 100644 index 0000000000..1d6a61d7e5 --- /dev/null +++ b/solon-plugins/wx-java-pay-solon-plugin/src/test/resources/app.yml @@ -0,0 +1,6 @@ +wx: + pay: + appId: + mchId: + mchKey: + keyPath: diff --git a/solon-plugins/wx-java-qidian-solon-plugin/README.md b/solon-plugins/wx-java-qidian-solon-plugin/README.md new file mode 100644 index 0000000000..42daa3e4c8 --- /dev/null +++ b/solon-plugins/wx-java-qidian-solon-plugin/README.md @@ -0,0 +1,45 @@ +# wx-java-qidian-solon-plugin + +## 快速开始 + +1. 引入依赖 + ```xml + + com.github.binarywang + wx-java-qidian-solon-plugin + ${version} + + ``` +2. 添加配置(app.properties) + ```properties + # 公众号配置(必填) + wx.qidian.appId = appId + wx.qidian.secret = @secret + wx.qidian.token = @token + wx.qidian.aesKey = @aesKey + # 存储配置redis(可选) + wx.qidian.config-storage.type = Jedis # 配置类型: Memory(默认), Jedis, RedisTemplate + wx.qidian.config-storage.key-prefix = wx # 相关redis前缀配置: wx(默认) + wx.qidian.config-storage.redis.host = 127.0.0.1 + wx.qidian.config-storage.redis.port = 6379 + #单机和sentinel同时存在时,优先使用sentinel配置 + #wx.qidian.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379 + #wx.qidian.config-storage.redis.sentinel-name=mymaster + # http客户端配置 + wx.qidian.config-storage.http-client-type=httpclient # http客户端类型: HttpClient(默认), OkHttp, JoddHttp + wx.qidian.config-storage.http-proxy-host= + wx.qidian.config-storage.http-proxy-port= + wx.qidian.config-storage.http-proxy-username= + wx.qidian.config-storage.http-proxy-password= + # 公众号地址host配置 + #wx.qidian.hosts.api-host=http://proxy.com/ + #wx.qidian.hosts.open-host=http://proxy.com/ + #wx.qidian.hosts.mp-host=http://proxy.com/ + ``` +3. 自动注入的类型 + +- `WxQidianService` +- `WxQidianConfigStorage` + +4、参考 demo: +https://github.com/binarywang/wx-java-mp-demo diff --git a/solon-plugins/wx-java-qidian-solon-plugin/pom.xml b/solon-plugins/wx-java-qidian-solon-plugin/pom.xml new file mode 100644 index 0000000000..f83c8a8066 --- /dev/null +++ b/solon-plugins/wx-java-qidian-solon-plugin/pom.xml @@ -0,0 +1,38 @@ + + + + wx-java-solon-plugins + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-qidian-solon-plugin + WxJava - Solon Plugin for QiDian + 腾讯企点的 Solon Plugin + + + + com.github.binarywang + weixin-java-qidian + ${project.version} + + + redis.clients + jedis + 4.3.2 + compile + + + org.jodd + jodd-http + provided + + + com.squareup.okhttp3 + okhttp + provided + + + + diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/config/WxQidianServiceAutoConfiguration.java b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/config/WxQidianServiceAutoConfiguration.java new file mode 100644 index 0000000000..02ec06cd25 --- /dev/null +++ b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/config/WxQidianServiceAutoConfiguration.java @@ -0,0 +1,71 @@ +package com.binarywang.solon.wxjava.qidian.config; + +import com.binarywang.solon.wxjava.qidian.enums.HttpClientType; +import com.binarywang.solon.wxjava.qidian.properties.WxQidianProperties; +import me.chanjar.weixin.qidian.api.WxQidianService; +import me.chanjar.weixin.qidian.api.impl.WxQidianServiceHttpClientImpl; +import me.chanjar.weixin.qidian.api.impl.WxQidianServiceHttpComponentsImpl; +import me.chanjar.weixin.qidian.api.impl.WxQidianServiceImpl; +import me.chanjar.weixin.qidian.api.impl.WxQidianServiceJoddHttpImpl; +import me.chanjar.weixin.qidian.api.impl.WxQidianServiceOkHttpImpl; +import me.chanjar.weixin.qidian.config.WxQidianConfigStorage; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; + +/** + * 腾讯企点相关服务自动注册. + * + * @author alegria + */ +@Configuration +public class WxQidianServiceAutoConfiguration { + + @Bean + @Condition(onMissingBean = WxQidianService.class) + public WxQidianService wxQidianService(WxQidianConfigStorage configStorage, WxQidianProperties wxQidianProperties) { + HttpClientType httpClientType = wxQidianProperties.getConfigStorage().getHttpClientType(); + WxQidianService wxQidianService; + switch (httpClientType) { + case OkHttp: + wxQidianService = newWxQidianServiceOkHttpImpl(); + break; + case JoddHttp: + wxQidianService = newWxQidianServiceJoddHttpImpl(); + break; + case HttpClient: + wxQidianService = newWxQidianServiceHttpClientImpl(); + break; + case HttpComponents: + wxQidianService = newWxQidianServiceHttpComponentsImpl(); + break; + default: + wxQidianService = newWxQidianServiceImpl(); + break; + } + + wxQidianService.setWxMpConfigStorage(configStorage); + return wxQidianService; + } + + private WxQidianService newWxQidianServiceImpl() { + return new WxQidianServiceImpl(); + } + + private WxQidianService newWxQidianServiceHttpClientImpl() { + return new WxQidianServiceHttpClientImpl(); + } + + private WxQidianService newWxQidianServiceOkHttpImpl() { + return new WxQidianServiceOkHttpImpl(); + } + + private WxQidianService newWxQidianServiceJoddHttpImpl() { + return new WxQidianServiceJoddHttpImpl(); + } + + private WxQidianService newWxQidianServiceHttpComponentsImpl() { + return new WxQidianServiceHttpComponentsImpl(); + } + +} diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/config/WxQidianStorageAutoConfiguration.java b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/config/WxQidianStorageAutoConfiguration.java new file mode 100644 index 0000000000..a99a8178c9 --- /dev/null +++ b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/config/WxQidianStorageAutoConfiguration.java @@ -0,0 +1,137 @@ +package com.binarywang.solon.wxjava.qidian.config; + +import com.binarywang.solon.wxjava.qidian.enums.StorageType; +import com.binarywang.solon.wxjava.qidian.properties.RedisProperties; +import com.binarywang.solon.wxjava.qidian.properties.WxQidianProperties; +import com.google.common.collect.Sets; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.redis.JedisWxRedisOps; +import me.chanjar.weixin.common.redis.WxRedisOps; +import me.chanjar.weixin.qidian.bean.WxQidianHostConfig; +import me.chanjar.weixin.qidian.config.WxQidianConfigStorage; +import me.chanjar.weixin.qidian.config.impl.WxQidianDefaultConfigImpl; +import me.chanjar.weixin.qidian.config.impl.WxQidianRedisConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.noear.solon.annotation.Bean; +import org.noear.solon.annotation.Condition; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; +import org.noear.solon.core.AppContext; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.JedisSentinelPool; +import redis.clients.jedis.util.Pool; + +import java.time.Duration; +import java.util.Set; + +/** + * 腾讯企点存储策略自动配置. + * + * @author alegria + */ +@Slf4j +@Configuration +@RequiredArgsConstructor +public class WxQidianStorageAutoConfiguration { + private final AppContext applicationContext; + + private final WxQidianProperties wxQidianProperties; + + @Inject("${wx.mp.config-storage.redis.host:") + private String redisHost; + + @Inject("${wx.mp.configStorage.redis.host:") + private String redisHost2; + + @Bean + @Condition(onMissingBean=WxQidianConfigStorage.class) + public WxQidianConfigStorage wxQidianConfigStorage() { + StorageType type = wxQidianProperties.getConfigStorage().getType(); + WxQidianConfigStorage config; + switch (type) { + case Jedis: + config = jedisConfigStorage(); + break; + default: + config = defaultConfigStorage(); + break; + } + // wx host config + if (null != wxQidianProperties.getHosts() && StringUtils.isNotEmpty(wxQidianProperties.getHosts().getApiHost())) { + WxQidianHostConfig hostConfig = new WxQidianHostConfig(); + hostConfig.setApiHost(wxQidianProperties.getHosts().getApiHost()); + hostConfig.setQidianHost(wxQidianProperties.getHosts().getQidianHost()); + hostConfig.setOpenHost(wxQidianProperties.getHosts().getOpenHost()); + config.setHostConfig(hostConfig); + } + return config; + } + + private WxQidianConfigStorage defaultConfigStorage() { + WxQidianDefaultConfigImpl config = new WxQidianDefaultConfigImpl(); + setWxMpInfo(config); + return config; + } + + private WxQidianConfigStorage jedisConfigStorage() { + Pool jedisPool; + if (StringUtils.isNotEmpty(redisHost) || StringUtils.isNotEmpty(redisHost2)) { + jedisPool = getJedisPool(); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + WxRedisOps redisOps = new JedisWxRedisOps(jedisPool); + WxQidianRedisConfigImpl wxQidianRedisConfig = new WxQidianRedisConfigImpl(redisOps, + wxQidianProperties.getConfigStorage().getKeyPrefix()); + setWxMpInfo(wxQidianRedisConfig); + return wxQidianRedisConfig; + } + + private void setWxMpInfo(WxQidianDefaultConfigImpl config) { + WxQidianProperties properties = wxQidianProperties; + WxQidianProperties.ConfigStorage configStorageProperties = properties.getConfigStorage(); + config.setAppId(properties.getAppId()); + config.setSecret(properties.getSecret()); + config.setToken(properties.getToken()); + config.setAesKey(properties.getAesKey()); + + config.setHttpProxyHost(configStorageProperties.getHttpProxyHost()); + config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername()); + config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword()); + if (configStorageProperties.getHttpProxyPort() != null) { + config.setHttpProxyPort(configStorageProperties.getHttpProxyPort()); + } + } + + private Pool getJedisPool() { + WxQidianProperties.ConfigStorage storage = wxQidianProperties.getConfigStorage(); + RedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWait(Duration.ofMillis(redis.getMaxWaitMillis())); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + if (StringUtils.isNotEmpty(redis.getSentinelIps())) { + + Set sentinels = Sets.newHashSet(redis.getSentinelIps().split(",")); + return new JedisSentinelPool(redis.getSentinelName(), sentinels,config); + } + + return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(), + redis.getDatabase()); + } +} diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/enums/HttpClientType.java b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/enums/HttpClientType.java new file mode 100644 index 0000000000..5729ab8fda --- /dev/null +++ b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/enums/HttpClientType.java @@ -0,0 +1,26 @@ +package com.binarywang.solon.wxjava.qidian.enums; + +/** + * httpclient类型. + * + * @author Binary Wang + * created on 2020-08-30 + */ +public enum HttpClientType { + /** + * HttpClient. + */ + HttpClient, + /** + * OkHttp. + */ + OkHttp, + /** + * JoddHttp. + */ + JoddHttp, + /** + * HttpComponents. + */ + HttpComponents, +} diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/enums/StorageType.java b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/enums/StorageType.java new file mode 100644 index 0000000000..c4bd2f72cf --- /dev/null +++ b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/enums/StorageType.java @@ -0,0 +1,26 @@ +package com.binarywang.solon.wxjava.qidian.enums; + +/** + * storage类型. + * + * @author Binary Wang + * created on 2020-08-30 + */ +public enum StorageType { + /** + * 内存. + */ + Memory, + /** + * redis(JedisClient). + */ + Jedis, + /** + * redis(Redisson). + */ + Redisson, + /** + * redis(RedisTemplate). + */ + RedisTemplate +} diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/integration/WxQidianPluginImpl.java b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/integration/WxQidianPluginImpl.java new file mode 100644 index 0000000000..2a97b512fd --- /dev/null +++ b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/integration/WxQidianPluginImpl.java @@ -0,0 +1,20 @@ +package com.binarywang.solon.wxjava.qidian.integration; + +import com.binarywang.solon.wxjava.qidian.config.WxQidianServiceAutoConfiguration; +import com.binarywang.solon.wxjava.qidian.config.WxQidianStorageAutoConfiguration; +import com.binarywang.solon.wxjava.qidian.properties.WxQidianProperties; +import org.noear.solon.core.AppContext; +import org.noear.solon.core.Plugin; + +/** + * @author noear 2024/9/2 created + */ +public class WxQidianPluginImpl implements Plugin{ + @Override + public void start(AppContext context) throws Throwable { + context.beanMake(WxQidianProperties.class); + + context.beanMake(WxQidianStorageAutoConfiguration.class); + context.beanMake(WxQidianServiceAutoConfiguration.class); + } +} diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/properties/HostConfig.java b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/properties/HostConfig.java new file mode 100644 index 0000000000..08546d8da6 --- /dev/null +++ b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/properties/HostConfig.java @@ -0,0 +1,18 @@ +package com.binarywang.solon.wxjava.qidian.properties; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class HostConfig implements Serializable { + + private static final long serialVersionUID = -4172767630740346001L; + + private String apiHost; + + private String openHost; + + private String qidianHost; + +} diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/properties/RedisProperties.java b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/properties/RedisProperties.java new file mode 100644 index 0000000000..a6b10a9e0f --- /dev/null +++ b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/properties/RedisProperties.java @@ -0,0 +1,56 @@ +package com.binarywang.solon.wxjava.qidian.properties; + +import lombok.Data; + +import java.io.Serializable; + +/** + * redis 配置属性. + * + * @author Binary Wang + * created on 2020-08-30 + */ +@Data +public class RedisProperties implements Serializable { + private static final long serialVersionUID = -5924815351660074401L; + + /** + * 主机地址. + */ + private String host = "127.0.0.1"; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + /** + * sentinel ips + */ + private String sentinelIps; + + /** + * sentinel name + */ + private String sentinelName; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/properties/WxQidianProperties.java b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/properties/WxQidianProperties.java new file mode 100644 index 0000000000..e99f882e6f --- /dev/null +++ b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/properties/WxQidianProperties.java @@ -0,0 +1,101 @@ +package com.binarywang.solon.wxjava.qidian.properties; + +import com.binarywang.solon.wxjava.qidian.enums.HttpClientType; +import com.binarywang.solon.wxjava.qidian.enums.StorageType; +import lombok.Data; +import org.noear.solon.annotation.Configuration; +import org.noear.solon.annotation.Inject; + +import java.io.Serializable; + +import static com.binarywang.solon.wxjava.qidian.enums.StorageType.Memory; +import static com.binarywang.solon.wxjava.qidian.properties.WxQidianProperties.PREFIX; + +/** + * 企点接入相关配置属性. + * + * @author someone + */ +@Data +@Configuration +@Inject("${" + PREFIX + "}") +public class WxQidianProperties { + public static final String PREFIX = "wx.qidian"; + + /** + * 设置腾讯企点的appid. + */ + private String appId; + + /** + * 设置腾讯企点的app secret. + */ + private String secret; + + /** + * 设置腾讯企点的token. + */ + private String token; + + /** + * 设置腾讯企点的EncodingAESKey. + */ + private String aesKey; + + /** + * 自定义host配置 + */ + private HostConfig hosts; + + /** + * 存储策略 + */ + private ConfigStorage configStorage = new ConfigStorage(); + + @Data + public static class ConfigStorage implements Serializable { + private static final long serialVersionUID = 4815731027000065434L; + + /** + * 存储类型. + */ + private StorageType type = Memory; + + /** + * 指定key前缀. + */ + private String keyPrefix = "wx"; + + /** + * redis连接配置. + */ + private RedisProperties redis = new RedisProperties(); + + /** + * http客户端类型. + */ + private HttpClientType httpClientType = HttpClientType.HttpComponents; + + /** + * http代理主机. + */ + private String httpProxyHost; + + /** + * http代理端口. + */ + private Integer httpProxyPort; + + /** + * http代理用户名. + */ + private String httpProxyUsername; + + /** + * http代理密码. + */ + private String httpProxyPassword; + + } + +} diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/main/resources/META-INF/solon/wx-java-qidian-solon-plugin.properties b/solon-plugins/wx-java-qidian-solon-plugin/src/main/resources/META-INF/solon/wx-java-qidian-solon-plugin.properties new file mode 100644 index 0000000000..a60c450b06 --- /dev/null +++ b/solon-plugins/wx-java-qidian-solon-plugin/src/main/resources/META-INF/solon/wx-java-qidian-solon-plugin.properties @@ -0,0 +1,2 @@ +solon.plugin=com.binarywang.solon.wxjava.qidian.integration.WxQidianPluginImpl +solon.plugin.priority=10 diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/test/java/features/test/LoadTest.java b/solon-plugins/wx-java-qidian-solon-plugin/src/test/java/features/test/LoadTest.java new file mode 100644 index 0000000000..d049f5a51a --- /dev/null +++ b/solon-plugins/wx-java-qidian-solon-plugin/src/test/java/features/test/LoadTest.java @@ -0,0 +1,15 @@ +package features.test; + +import org.junit.jupiter.api.Test; +import org.noear.solon.test.SolonTest; + +/** + * @author noear 2024/9/4 created + */ +@SolonTest +public class LoadTest { + @Test + public void load(){ + + } +} diff --git a/solon-plugins/wx-java-qidian-solon-plugin/src/test/resources/app.yml b/solon-plugins/wx-java-qidian-solon-plugin/src/test/resources/app.yml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spring-boot-starters/MULTI_TENANT_MODE_IMPROVEMENT.md b/spring-boot-starters/MULTI_TENANT_MODE_IMPROVEMENT.md new file mode 100644 index 0000000000..6581f6207d --- /dev/null +++ b/spring-boot-starters/MULTI_TENANT_MODE_IMPROVEMENT.md @@ -0,0 +1,160 @@ +# 多租户模式配置改进说明 + +## 问题背景 + +用户在 issue #3835 中提出了一个架构设计问题: + +> 基础 Wx 实现类中已经有 configMap 了,可以用 configMap 来存储不同的小程序配置。不同的配置,都是复用同一个 http 客户端。为什么在各个 spring-boot-starter 中又单独创建类来存储不同的配置?从 spring 的配置来看,http 客户端只有一个,不同小程序配置可以实现多租户,所以似乎没必要单独再建新类存放?重复创建,增加了 http 客户端的成本?直接使用 Wx 实现类中已经有 configMap 不是更好吗? + +## 解决方案 + +从 4.8.0 版本开始,我们为多租户 Spring Boot Starter 提供了**两种实现模式**供用户选择: + +### 1. 隔离模式(ISOLATED,默认) + +**实现方式**:为每个租户创建独立的 WxService 实例,每个实例拥有独立的 HTTP 客户端。 + +**优点**: +- ✅ 线程安全,无需担心并发问题 +- ✅ 不依赖 ThreadLocal,适合异步/响应式编程 +- ✅ 租户间完全隔离,互不影响 + +**缺点**: +- ❌ 每个租户创建独立的 HTTP 客户端,资源占用较多 +- ❌ 适合租户数量不多的场景(建议 < 50 个租户) + +**代码实现**:`WxMaMultiServicesImpl`, `WxMpMultiServicesImpl` 等 + +### 2. 共享模式(SHARED,新增) + +**实现方式**:使用单个 WxService 实例管理所有租户配置,通过 ThreadLocal 切换租户,所有租户共享同一个 HTTP 客户端。 + +**优点**: +- ✅ 共享 HTTP 客户端,大幅节省资源 +- ✅ 适合租户数量较多的场景(支持 100+ 租户) +- ✅ 内存占用更小 + +**缺点**: +- ❌ 依赖 ThreadLocal 切换配置,在异步场景需要特别注意 +- ❌ 需要注意线程上下文传递 + +**代码实现**:`WxMaMultiServicesSharedImpl`, `WxMpMultiServicesSharedImpl` 等 + +## 使用方式 + +### 配置示例 + +```yaml +wx: + ma: # 或 mp, cp, channel + apps: + tenant1: + app-id: wxd898fcb01713c555 + app-secret: 47a2422a5d04a27e2b3ed1f1f0b0dbad + tenant2: + app-id: wx1234567890abcdef + app-secret: 1234567890abcdef1234567890abcdef + + config-storage: + type: memory + http-client-type: http_client + # 多租户模式配置(新增) + multi-tenant-mode: shared # isolated(默认)或 shared +``` + +### 代码使用(两种模式代码完全相同) + +```java +@RestController +public class WxController { + @Autowired + private WxMaMultiServices wxMaMultiServices; // 或 WxMpMultiServices + + @GetMapping("/api/{tenantId}") + public String handle(@PathVariable String tenantId) { + WxMaService wxService = wxMaMultiServices.getWxMaService(tenantId); + // 使用 wxService 调用微信 API + return wxService.getAccessToken(); + } +} +``` + +## 性能对比 + +以 100 个租户为例: + +| 指标 | 隔离模式 | 共享模式 | +|------|---------|---------| +| HTTP 客户端数量 | 100 个 | 1 个 | +| 内存占用(估算) | ~500MB | ~50MB | +| 线程安全 | ✅ 完全安全 | ⚠️ 需注意异步场景 | +| 性能 | 略高(无 ThreadLocal 切换) | 略低(有 ThreadLocal 切换) | +| 适用场景 | 中小规模 | 大规模 | + +## 支持的模块 + +目前已实现共享模式支持的模块: + +- ✅ **小程序(MiniApp)**:`wx-java-miniapp-multi-spring-boot-starter` +- ✅ **公众号(MP)**:`wx-java-mp-multi-spring-boot-starter` + +后续版本将支持: +- ⏳ 企业微信(CP) +- ⏳ 视频号(Channel) +- ⏳ 企业微信第三方应用(CP-TP) + +## 迁移指南 + +### 从旧版本升级 + +升级到 4.8.0+ 后: + +1. **默认行为不变**:如果不配置 `multi-tenant-mode`,将继续使用隔离模式(与旧版本行为一致) +2. **向后兼容**:所有现有代码无需修改 +3. **可选升级**:如需节省资源,可配置 `multi-tenant-mode: shared` 启用共享模式 + +### 选择建议 + +**使用隔离模式(ISOLATED)的场景**: +- 租户数量较少(< 50 个) +- 使用异步编程、响应式编程 +- 对线程安全有严格要求 +- 对资源占用不敏感 + +**使用共享模式(SHARED)的场景**: +- 租户数量较多(> 50 个) +- 同步编程场景 +- 对资源占用敏感 +- 可以接受 ThreadLocal 的约束 + +## 注意事项 + +### 共享模式下的异步编程 + +如果使用共享模式,在异步编程时需要注意 ThreadLocal 的传递: + +```java +// ❌ 错误:异步线程无法获取到正确的配置 +CompletableFuture.runAsync(() -> { + wxService.getUserService().getUserInfo(...); // 可能使用错误的租户配置 +}); + +// ✅ 正确:在主线程获取必要信息,传递给异步线程 +String appId = wxService.getWxMaConfig().getAppid(); +CompletableFuture.runAsync(() -> { + log.info("AppId: {}", appId); // 使用已获取的配置信息 +}); +``` + +## 详细文档 + +- 小程序模块详细说明:[spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md](spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md) + +## 相关链接 + +- Issue: [#3835](https://github.com/binarywang/WxJava/issues/3835) +- Pull Request: [#3840](https://github.com/binarywang/WxJava/pull/3840) + +## 致谢 + +感谢 issue 提出者对项目架构的深入思考和建议,这帮助我们提供了更灵活、更高效的多租户解决方案。 diff --git a/spring-boot-starters/pom.xml b/spring-boot-starters/pom.xml index a04dc13547..07a1226e6f 100644 --- a/spring-boot-starters/pom.xml +++ b/spring-boot-starters/pom.xml @@ -6,7 +6,7 @@ com.github.binarywang wx-java - 3.6.0 + 4.8.3.B pom wx-java-spring-boot-starters @@ -14,14 +14,24 @@ WxJava 各个模块的 Spring Boot Starter - 2.1.4.RELEASE + 2.5.15 + wx-java-miniapp-multi-spring-boot-starter wx-java-miniapp-spring-boot-starter + wx-java-mp-multi-spring-boot-starter wx-java-mp-spring-boot-starter wx-java-pay-spring-boot-starter + wx-java-pay-multi-spring-boot-starter + wx-java-open-multi-spring-boot-starter wx-java-open-spring-boot-starter + wx-java-qidian-spring-boot-starter + wx-java-cp-multi-spring-boot-starter + wx-java-cp-tp-multi-spring-boot-starter + wx-java-cp-spring-boot-starter + wx-java-channel-spring-boot-starter + wx-java-channel-multi-spring-boot-starter diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/README.md b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/README.md new file mode 100644 index 0000000000..c2f082bec8 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/README.md @@ -0,0 +1,123 @@ +# wx-java-channel-multi-spring-boot-starter + +## 快速开始 + +1. 引入依赖 + ```xml + + + com.github.binarywang + wx-java-channel-multi-spring-boot-starter + ${version} + + + + + redis.clients + jedis + ${jedis.version} + + + + + org.redisson + redisson + ${redisson.version} + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + ``` +2. 添加配置(application.properties) + ```properties + # 视频号配置 + ## 应用 1 配置(必填) + wx.channel.apps.tenantId1.app-id=@appId + wx.channel.apps.tenantId1.secret=@secret + ## 选填 + wx.channel.apps.tenantId1.use-stable-access-token=false + wx.channel.apps.tenantId1.token= + wx.channel.apps.tenantId1.aes-key= + ## 应用 2 配置(必填) + wx.channel.apps.tenantId2.app-id=@appId + wx.channel.apps.tenantId2.secret=@secret + ## 选填 + wx.channel.apps.tenantId2.use-stable-access-token=false + wx.channel.apps.tenantId2.token= + wx.channel.apps.tenantId2.aes-key= + + # ConfigStorage 配置(选填) + ## 配置类型: memory(默认), jedis, redisson, redis_template + wx.channel.config-storage.type=memory + ## 相关redis前缀配置: wx:channel:multi(默认) + wx.channel.config-storage.key-prefix=wx:channel:multi + wx.channel.config-storage.redis.host=127.0.0.1 + wx.channel.config-storage.redis.port=6379 + wx.channel.config-storage.redis.password=123456 + + # redis_template 方式使用spring data redis配置 + spring.data.redis.database=0 + spring.data.redis.host=127.0.0.1 + spring.data.redis.password=123456 + spring.data.redis.port=6379 + + # http 客户端配置(选填) + ## # http客户端类型: http_client(默认) + wx.channel.config-storage.http-client-type=http_client + wx.channel.config-storage.http-proxy-host= + wx.channel.config-storage.http-proxy-port= + wx.channel.config-storage.http-proxy-username= + wx.channel.config-storage.http-proxy-password= + ## 最大重试次数,默认:5 次,如果小于 0,则为 0 + wx.channel.config-storage.max-retry-times=5 + ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000 + wx.channel.config-storage.retry-sleep-millis=1000 + ``` +3. 自动注入的类型:`WxChannelMultiServices` + +4. 使用样例 + + ```java + import com.binarywang.spring.starter.wxjava.channel.service.WxChannelMultiServices; + import me.chanjar.weixin.channel.api.WxChannelService; + import me.chanjar.weixin.channel.api.WxFinderLiveService; + import me.chanjar.weixin.channel.bean.lead.component.response.FinderAttrResponse; + import me.chanjar.weixin.common.error.WxErrorException; + import org.springframework.beans.factory.annotation.Autowired; + import org.springframework.stereotype.Service; + + @Service + public class DemoService { + @Autowired + private WxChannelMultiServices wxChannelMultiServices; + + public void test() throws WxErrorException { + // 应用 1 的 WxChannelService + WxChannelService wxChannelService1 = wxChannelMultiServices.getWxChannelService("tenantId1"); + WxFinderLiveService finderLiveService = wxChannelService1.getFinderLiveService(); + FinderAttrResponse response1 = finderLiveService.getFinderAttrByAppid(); + // todo ... + + // 应用 2 的 WxChannelService + WxChannelService wxChannelService2 = wxChannelMultiServices.getWxChannelService("tenantId2"); + WxFinderLiveService finderLiveService2 = wxChannelService2.getFinderLiveService(); + FinderAttrResponse response2 = finderLiveService2.getFinderAttrByAppid(); + // todo ... + + // 应用 3 的 WxChannelService + WxChannelService wxChannelService3 = wxChannelMultiServices.getWxChannelService("tenantId3"); + // 判断是否为空 + if (wxChannelService3 == null) { + // todo wxChannelService3 为空,请先配置 tenantId3 微信视频号应用参数 + return; + } + WxFinderLiveService finderLiveService3 = wxChannelService3.getFinderLiveService(); + FinderAttrResponse response3 = finderLiveService3.getFinderAttrByAppid(); + // todo ... + } + } + ``` diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/pom.xml new file mode 100644 index 0000000000..c3c3441c9b --- /dev/null +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/pom.xml @@ -0,0 +1,71 @@ + + + + wx-java-spring-boot-starters + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-channel-multi-spring-boot-starter + WxJava - Spring Boot Starter for Channel::支持多账号配置 + 微信视频号开发的 Spring Boot Starter::支持多账号配置 + + + + com.github.binarywang + weixin-java-channel + ${project.version} + + + redis.clients + jedis + provided + + + org.redisson + redisson + provided + + + org.springframework.data + spring-data-redis + provided + + + org.jodd + jodd-http + provided + + + com.squareup.okhttp3 + okhttp + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + + diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/autoconfigure/WxChannelMultiAutoConfiguration.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/autoconfigure/WxChannelMultiAutoConfiguration.java new file mode 100644 index 0000000000..e6ef922b43 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/autoconfigure/WxChannelMultiAutoConfiguration.java @@ -0,0 +1,15 @@ +package com.binarywang.spring.starter.wxjava.channel.autoconfigure; + +import com.binarywang.spring.starter.wxjava.channel.configuration.WxChannelMultiServiceConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * 微信视频号自动注册 + * + * @author Winnie + * @date 2024/9/13 + */ +@Configuration +@Import(WxChannelMultiServiceConfiguration.class) +public class WxChannelMultiAutoConfiguration {} diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/WxChannelMultiServiceConfiguration.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/WxChannelMultiServiceConfiguration.java new file mode 100644 index 0000000000..17cd0da723 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/WxChannelMultiServiceConfiguration.java @@ -0,0 +1,21 @@ +package com.binarywang.spring.starter.wxjava.channel.configuration; + +import com.binarywang.spring.starter.wxjava.channel.configuration.services.WxChannelInJedisConfiguration; +import com.binarywang.spring.starter.wxjava.channel.configuration.services.WxChannelInMemoryConfiguration; +import com.binarywang.spring.starter.wxjava.channel.configuration.services.WxChannelInRedisTemplateConfiguration; +import com.binarywang.spring.starter.wxjava.channel.configuration.services.WxChannelInRedissonConfiguration; +import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelMultiProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * 微信视频号相关服务自动注册 + * + * @author Winnie + * @date 2024/9/13 + */ +@Configuration +@EnableConfigurationProperties(WxChannelMultiProperties.class) +@Import({WxChannelInJedisConfiguration.class, WxChannelInMemoryConfiguration.class, WxChannelInRedissonConfiguration.class, WxChannelInRedisTemplateConfiguration.class}) +public class WxChannelMultiServiceConfiguration {} diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/AbstractWxChannelConfiguration.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/AbstractWxChannelConfiguration.java new file mode 100644 index 0000000000..e2f9f87f5a --- /dev/null +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/AbstractWxChannelConfiguration.java @@ -0,0 +1,148 @@ +package com.binarywang.spring.starter.wxjava.channel.configuration.services; + +import com.binarywang.spring.starter.wxjava.channel.enums.HttpClientType; +import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelMultiProperties; +import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelSingleProperties; +import com.binarywang.spring.starter.wxjava.channel.service.WxChannelMultiServices; +import com.binarywang.spring.starter.wxjava.channel.service.WxChannelMultiServicesImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.channel.api.WxChannelService; +import me.chanjar.weixin.channel.api.impl.WxChannelServiceHttpClientImpl; +import me.chanjar.weixin.channel.api.impl.WxChannelServiceHttpComponentsImpl; +import me.chanjar.weixin.channel.api.impl.WxChannelServiceImpl; +import me.chanjar.weixin.channel.config.WxChannelConfig; +import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * WxChannelConfigStorage 抽象配置类 + * + * @author Winnie + * @date 2024/9/13 + */ +@RequiredArgsConstructor +@Slf4j +public abstract class AbstractWxChannelConfiguration { + protected WxChannelMultiServices wxChannelMultiServices(WxChannelMultiProperties wxChannelMultiProperties) { + Map appsMap = wxChannelMultiProperties.getApps(); + if (appsMap == null || appsMap.isEmpty()) { + log.warn("微信视频号应用参数未配置,通过 WxChannelMultiServices#getWxChannelService(\"tenantId\")获取实例将返回空"); + return new WxChannelMultiServicesImpl(); + } + /** + * 校验 appId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。 + * + * 查看 {@link me.chanjar.weixin.channel.config.impl.WxChannelRedisConfigImpl#setAppid(String)} + */ + Collection apps = appsMap.values(); + if (apps.size() > 1) { + // 校验 appId 是否唯一 + boolean multi = apps.stream() + // 没有 appId,如果不判断是否为空,这里会报 NPE 异常 + .collect(Collectors.groupingBy(c -> c.getAppId() == null ? 0 : c.getAppId(), Collectors.counting())) + .entrySet().stream().anyMatch(e -> e.getValue() > 1); + if (multi) { + throw new RuntimeException("请确保微信视频号配置 appId 的唯一性"); + } + } + WxChannelMultiServicesImpl services = new WxChannelMultiServicesImpl(); + + Set> entries = appsMap.entrySet(); + for (Map.Entry entry : entries) { + String tenantId = entry.getKey(); + WxChannelSingleProperties wxChannelSingleProperties = entry.getValue(); + WxChannelDefaultConfigImpl storage = this.wxChannelConfigStorage(wxChannelMultiProperties); + this.configApp(storage, wxChannelSingleProperties); + this.configHttp(storage, wxChannelMultiProperties.getConfigStorage()); + WxChannelService wxChannelService = this.wxChannelService(storage, wxChannelMultiProperties); + services.addWxChannelService(tenantId, wxChannelService); + } + return services; + } + + /** + * 配置 WxChannelDefaultConfigImpl + * + * @param wxChannelMultiProperties 参数 + * @return WxChannelDefaultConfigImpl + */ + protected abstract WxChannelDefaultConfigImpl wxChannelConfigStorage(WxChannelMultiProperties wxChannelMultiProperties); + + public WxChannelService wxChannelService(WxChannelConfig wxChannelConfig, WxChannelMultiProperties wxChannelMultiProperties) { + WxChannelMultiProperties.ConfigStorage storage = wxChannelMultiProperties.getConfigStorage(); + HttpClientType httpClientType = storage.getHttpClientType(); + WxChannelService wxChannelService; + switch (httpClientType) { +// case OK_HTTP: +// wxChannelService = new WxChannelServiceOkHttpImpl(false, false); +// break; + case HTTP_CLIENT: + wxChannelService = new WxChannelServiceHttpClientImpl(); + break; + case HTTP_COMPONENTS: + wxChannelService = new WxChannelServiceHttpComponentsImpl(); + break; + default: + wxChannelService = new WxChannelServiceImpl(); + break; + } + + wxChannelService.setConfig(wxChannelConfig); + int maxRetryTimes = storage.getMaxRetryTimes(); + if (maxRetryTimes < 0) { + maxRetryTimes = 0; + } + int retrySleepMillis = storage.getRetrySleepMillis(); + if (retrySleepMillis < 0) { + retrySleepMillis = 1000; + } + wxChannelService.setRetrySleepMillis(retrySleepMillis); + wxChannelService.setMaxRetryTimes(maxRetryTimes); + return wxChannelService; + } + + private void configApp(WxChannelDefaultConfigImpl config, WxChannelSingleProperties wxChannelSingleProperties) { + String appId = wxChannelSingleProperties.getAppId(); + String appSecret = wxChannelSingleProperties.getSecret(); + String token = wxChannelSingleProperties.getToken(); + String aesKey = wxChannelSingleProperties.getAesKey(); + boolean useStableAccessToken = wxChannelSingleProperties.isUseStableAccessToken(); + + config.setAppid(appId); + config.setSecret(appSecret); + if (StringUtils.isNotBlank(token)) { + config.setToken(token); + } + if (StringUtils.isNotBlank(aesKey)) { + config.setAesKey(aesKey); + } + config.setStableAccessToken(useStableAccessToken); + config.setApiHostUrl(StringUtils.trimToNull(wxChannelSingleProperties.getApiHostUrl())); + config.setAccessTokenUrl(StringUtils.trimToNull(wxChannelSingleProperties.getAccessTokenUrl())); + } + + private void configHttp(WxChannelDefaultConfigImpl config, WxChannelMultiProperties.ConfigStorage storage) { + String httpProxyHost = storage.getHttpProxyHost(); + Integer httpProxyPort = storage.getHttpProxyPort(); + String httpProxyUsername = storage.getHttpProxyUsername(); + String httpProxyPassword = storage.getHttpProxyPassword(); + if (StringUtils.isNotBlank(httpProxyHost)) { + config.setHttpProxyHost(httpProxyHost); + if (httpProxyPort != null) { + config.setHttpProxyPort(httpProxyPort); + } + if (StringUtils.isNotBlank(httpProxyUsername)) { + config.setHttpProxyUsername(httpProxyUsername); + } + if (StringUtils.isNotBlank(httpProxyPassword)) { + config.setHttpProxyPassword(httpProxyPassword); + } + } + } +} diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInJedisConfiguration.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInJedisConfiguration.java new file mode 100644 index 0000000000..d19b0fc4b5 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInJedisConfiguration.java @@ -0,0 +1,74 @@ +package com.binarywang.spring.starter.wxjava.channel.configuration.services; + +import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelMultiProperties; +import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelMultiRedisProperties; +import com.binarywang.spring.starter.wxjava.channel.service.WxChannelMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl; +import me.chanjar.weixin.channel.config.impl.WxChannelRedisConfigImpl; +import me.chanjar.weixin.common.redis.JedisWxRedisOps; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * 自动装配基于 jedis 策略配置 + * + * @author Winnie + * @date 2024/9/13 + */ +@Configuration +@ConditionalOnProperty(prefix = WxChannelMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis") +@RequiredArgsConstructor +public class WxChannelInJedisConfiguration extends AbstractWxChannelConfiguration { + private final WxChannelMultiProperties wxChannelMultiProperties; + private final ApplicationContext applicationContext; + + @Bean + public WxChannelMultiServices wxChannelMultiServices() { + return this.wxChannelMultiServices(wxChannelMultiProperties); + } + + @Override + protected WxChannelDefaultConfigImpl wxChannelConfigStorage(WxChannelMultiProperties wxChannelMultiProperties) { + return this.configRedis(wxChannelMultiProperties); + } + + private WxChannelDefaultConfigImpl configRedis(WxChannelMultiProperties wxChannelMultiProperties) { + WxChannelMultiRedisProperties wxChannelMultiRedisProperties = wxChannelMultiProperties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (wxChannelMultiRedisProperties != null && StringUtils.isNotEmpty(wxChannelMultiRedisProperties.getHost())) { + jedisPool = getJedisPool(wxChannelMultiProperties); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + return new WxChannelRedisConfigImpl(new JedisWxRedisOps(jedisPool), wxChannelMultiProperties.getConfigStorage().getKeyPrefix()); + } + + private JedisPool getJedisPool(WxChannelMultiProperties wxChannelMultiProperties) { + WxChannelMultiProperties.ConfigStorage storage = wxChannelMultiProperties.getConfigStorage(); + WxChannelMultiRedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(), redis.getDatabase()); + } +} diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInMemoryConfiguration.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInMemoryConfiguration.java new file mode 100644 index 0000000000..77bb221f25 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInMemoryConfiguration.java @@ -0,0 +1,36 @@ +package com.binarywang.spring.starter.wxjava.channel.configuration.services; + +import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelMultiProperties; +import com.binarywang.spring.starter.wxjava.channel.service.WxChannelMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 自动装配基于内存策略配置 + * + * @author Winnie + * @date 2024/9/13 + */ +@Configuration +@ConditionalOnProperty(prefix = WxChannelMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "memory", matchIfMissing = true) +@RequiredArgsConstructor +public class WxChannelInMemoryConfiguration extends AbstractWxChannelConfiguration { + private final WxChannelMultiProperties wxChannelMultiProperties; + + @Bean + public WxChannelMultiServices wxChannelMultiServices() { + return this.wxChannelMultiServices(wxChannelMultiProperties); + } + + @Override + protected WxChannelDefaultConfigImpl wxChannelConfigStorage(WxChannelMultiProperties wxChannelMultiProperties) { + return this.configInMemory(); + } + + private WxChannelDefaultConfigImpl configInMemory() { + return new WxChannelDefaultConfigImpl(); + } +} diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInRedisTemplateConfiguration.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInRedisTemplateConfiguration.java new file mode 100644 index 0000000000..f9defdb94a --- /dev/null +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInRedisTemplateConfiguration.java @@ -0,0 +1,42 @@ +package com.binarywang.spring.starter.wxjava.channel.configuration.services; + +import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelMultiProperties; +import com.binarywang.spring.starter.wxjava.channel.service.WxChannelMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl; +import me.chanjar.weixin.channel.config.impl.WxChannelRedisConfigImpl; +import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * 自动装配基于 redisTemplate 策略配置 + * + * @author Winnie + * @date 2024/9/13 + */ +@Configuration +@ConditionalOnProperty(prefix = WxChannelMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redis_template") +@RequiredArgsConstructor +public class WxChannelInRedisTemplateConfiguration extends AbstractWxChannelConfiguration { + private final WxChannelMultiProperties wxChannelMultiProperties; + private final ApplicationContext applicationContext; + + @Bean + public WxChannelMultiServices wxChannelMultiServices() { + return this.wxChannelMultiServices(wxChannelMultiProperties); + } + + @Override + protected WxChannelDefaultConfigImpl wxChannelConfigStorage(WxChannelMultiProperties wxChannelMultiProperties) { + return this.configRedisTemplate(wxChannelMultiProperties); + } + + private WxChannelDefaultConfigImpl configRedisTemplate(WxChannelMultiProperties wxChannelMultiProperties) { + StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class); + return new WxChannelRedisConfigImpl(new RedisTemplateWxRedisOps(redisTemplate), wxChannelMultiProperties.getConfigStorage().getKeyPrefix()); + } +} diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInRedissonConfiguration.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInRedissonConfiguration.java new file mode 100644 index 0000000000..fa4798a18b --- /dev/null +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/configuration/services/WxChannelInRedissonConfiguration.java @@ -0,0 +1,62 @@ +package com.binarywang.spring.starter.wxjava.channel.configuration.services; + +import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelMultiProperties; +import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelMultiRedisProperties; +import com.binarywang.spring.starter.wxjava.channel.service.WxChannelMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl; +import me.chanjar.weixin.channel.config.impl.WxChannelRedissonConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 自动装配基于 redisson 策略配置 + * + * @author Winnie + * @date 2024/9/13 + */ +@Configuration +@ConditionalOnProperty(prefix = WxChannelMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson") +@RequiredArgsConstructor +public class WxChannelInRedissonConfiguration extends AbstractWxChannelConfiguration { + private final WxChannelMultiProperties wxChannelMultiProperties; + private final ApplicationContext applicationContext; + + @Bean + public WxChannelMultiServices wxChannelMultiServices() { + return this.wxChannelMultiServices(wxChannelMultiProperties); + } + + @Override + protected WxChannelDefaultConfigImpl wxChannelConfigStorage(WxChannelMultiProperties wxChannelMultiProperties) { + return this.configRedisson(wxChannelMultiProperties); + } + + private WxChannelDefaultConfigImpl configRedisson(WxChannelMultiProperties wxChannelMultiProperties) { + WxChannelMultiRedisProperties redisProperties = wxChannelMultiProperties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + redissonClient = getRedissonClient(wxChannelMultiProperties); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxChannelRedissonConfigImpl(redissonClient, wxChannelMultiProperties.getConfigStorage().getKeyPrefix()); + } + + private RedissonClient getRedissonClient(WxChannelMultiProperties wxChannelMultiProperties) { + WxChannelMultiProperties.ConfigStorage storage = wxChannelMultiProperties.getConfigStorage(); + WxChannelMultiRedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer().setAddress("redis://" + redis.getHost() + ":" + redis.getPort()).setDatabase(redis.getDatabase()).setPassword(redis.getPassword()); + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/HttpClientType.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/HttpClientType.java new file mode 100644 index 0000000000..adc8a2b52b --- /dev/null +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/HttpClientType.java @@ -0,0 +1,23 @@ +package com.binarywang.spring.starter.wxjava.channel.enums; + +/** + * httpclient类型 + * + * @author Winnie + * @date 2024/9/13 + */ +public enum HttpClientType { + /** + * HttpClient. + */ + HTTP_CLIENT, + // WxChannelServiceOkHttpImpl 实现经测试无法正常完成业务固暂不支持OK_HTTP方式 +// /** +// * OkHttp. +// */ +// OK_HTTP, + /** + * HttpComponents. + */ + HTTP_COMPONENTS, +} diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/StorageType.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/StorageType.java new file mode 100644 index 0000000000..0ee69eca73 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/StorageType.java @@ -0,0 +1,26 @@ +package com.binarywang.spring.starter.wxjava.channel.enums; + +/** + * storage类型 + * + * @author Winnie + * @date 2024/9/13 + */ +public enum StorageType { + /** + * 内存 + */ + MEMORY, + /** + * redis(JedisClient) + */ + JEDIS, + /** + * redis(Redisson) + */ + REDISSON, + /** + * redis(RedisTemplate) + */ + REDIS_TEMPLATE +} diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelMultiProperties.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelMultiProperties.java new file mode 100644 index 0000000000..d22f560282 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelMultiProperties.java @@ -0,0 +1,96 @@ +package com.binarywang.spring.starter.wxjava.channel.properties; + +import com.binarywang.spring.starter.wxjava.channel.enums.HttpClientType; +import com.binarywang.spring.starter.wxjava.channel.enums.StorageType; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * 微信多视频号接入相关配置属性 + * + * @author Winnie + * @date 2024/9/13 + */ +@Data +@NoArgsConstructor +@ConfigurationProperties(WxChannelMultiProperties.PREFIX) +public class WxChannelMultiProperties implements Serializable { + private static final long serialVersionUID = - 8361973118805546037L; + public static final String PREFIX = "wx.channel"; + + private Map apps = new HashMap<>(); + + /** + * 存储策略 + */ + private final ConfigStorage configStorage = new ConfigStorage(); + + @Data + @NoArgsConstructor + public static class ConfigStorage implements Serializable { + private static final long serialVersionUID = - 5152619132544179942L; + + /** + * 存储类型. + */ + private StorageType type = StorageType.MEMORY; + + /** + * 指定key前缀. + */ + private String keyPrefix = "wx:channel:multi"; + + /** + * redis连接配置. + */ + @NestedConfigurationProperty + private final WxChannelMultiRedisProperties redis = new WxChannelMultiRedisProperties(); + + /** + * http客户端类型. + */ + private HttpClientType httpClientType = HttpClientType.HTTP_CLIENT; + + /** + * http代理主机. + */ + private String httpProxyHost; + + /** + * http代理端口. + */ + private Integer httpProxyPort; + + /** + * http代理用户名. + */ + private String httpProxyUsername; + + /** + * http代理密码. + */ + private String httpProxyPassword; + + /** + * http 请求最大重试次数 + * + *

{@link me.chanjar.weixin.channel.api.WxChannelService#setMaxRetryTimes(int)}

+ *

{@link me.chanjar.weixin.channel.api.impl.BaseWxChannelServiceImpl#setMaxRetryTimes(int)}

+ */ + private int maxRetryTimes = 5; + + /** + * http 请求重试间隔 + * + *

{@link me.chanjar.weixin.channel.api.WxChannelService#setRetrySleepMillis(int)}

+ *

{@link me.chanjar.weixin.channel.api.impl.BaseWxChannelServiceImpl#setRetrySleepMillis(int)}

+ */ + private int retrySleepMillis = 1000; + } +} diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelMultiRedisProperties.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelMultiRedisProperties.java new file mode 100644 index 0000000000..99c426765c --- /dev/null +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelMultiRedisProperties.java @@ -0,0 +1,63 @@ +package com.binarywang.spring.starter.wxjava.channel.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * Redis配置 + * + * @author Winnie + * @date 2024/9/13 + */ +@Data +@NoArgsConstructor +public class WxChannelMultiRedisProperties implements Serializable { + private static final long serialVersionUID = 9061055444734277357L; + + /** + * 主机地址. + */ + private String host = "127.0.0.1"; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + /** + * 最大活动连接数 + */ + private Integer maxActive; + + /** + * 最大空闲连接数 + */ + private Integer maxIdle; + + /** + * 最小空闲连接数 + */ + private Integer minIdle; + + /** + * 最大等待时间 + */ + private Integer maxWaitMillis; +} diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelSingleProperties.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelSingleProperties.java new file mode 100644 index 0000000000..4b613e3bf6 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelSingleProperties.java @@ -0,0 +1,55 @@ +package com.binarywang.spring.starter.wxjava.channel.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 微信视频号相关配置属性 + * + * @author Winnie + * @date 2024/9/13 + */ +@Data +@NoArgsConstructor +public class WxChannelSingleProperties implements Serializable { + private static final long serialVersionUID = 5306630351265124825L; + + /** + * 设置微信视频号的 appid. + */ + private String appId; + + /** + * 设置微信视频号的 secret. + */ + private String secret; + + /** + * 设置微信视频号的 token. + */ + private String token; + + /** + * 设置微信视频号的 EncodingAESKey. + */ + private String aesKey; + + /** + * 是否使用稳定版 Access Token + */ + private boolean useStableAccessToken = false; + + /** + * 自定义API主机地址,用于替换默认的 https://api.weixin.qq.com + * 例如:http://proxy.company.com:8080 + */ + private String apiHostUrl; + + /** + * 自定义获取AccessToken地址,用于向自定义统一服务获取AccessToken + * 例如:http://proxy.company.com:8080/oauth/token + */ + private String accessTokenUrl; +} diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/service/WxChannelMultiServices.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/service/WxChannelMultiServices.java new file mode 100644 index 0000000000..acd4ebf20b --- /dev/null +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/service/WxChannelMultiServices.java @@ -0,0 +1,26 @@ +package com.binarywang.spring.starter.wxjava.channel.service; + +import me.chanjar.weixin.channel.api.WxChannelService; + +/** + * 视频号 {@link WxChannelService} 所有实例存放类. + * + * @author Winnie + * @date 2024/9/13 + */ +public interface WxChannelMultiServices { + /** + * 通过租户 Id 获取 WxChannelService + * + * @param tenantId 租户 Id + * @return WxChannelService + */ + WxChannelService getWxChannelService(String tenantId); + + /** + * 根据租户 Id,从列表中移除一个 WxChannelService 实例 + * + * @param tenantId 租户 Id + */ + void removeWxChannelService(String tenantId); +} diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/service/WxChannelMultiServicesImpl.java b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/service/WxChannelMultiServicesImpl.java new file mode 100644 index 0000000000..1673289cb5 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/service/WxChannelMultiServicesImpl.java @@ -0,0 +1,36 @@ +package com.binarywang.spring.starter.wxjava.channel.service; + +import me.chanjar.weixin.channel.api.WxChannelService; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 视频号 {@link WxChannelMultiServices} 实现 + * + * @author Winnie + * @date 2024/9/13 + */ +public class WxChannelMultiServicesImpl implements WxChannelMultiServices { + private final Map services = new ConcurrentHashMap<>(); + + @Override + public WxChannelService getWxChannelService(String tenantId) { + return this.services.get(tenantId); + } + + /** + * 根据租户 Id,添加一个 WxChannelService 到列表 + * + * @param tenantId 租户 Id + * @param wxChannelService WxChannelService 实例 + */ + public void addWxChannelService(String tenantId, WxChannelService wxChannelService) { + this.services.put(tenantId, wxChannelService); + } + + @Override + public void removeWxChannelService(String tenantId) { + this.services.remove(tenantId); + } +} diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..2c5a939c32 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.binarywang.spring.starter.wxjava.channel.autoconfigure.WxChannelMultiAutoConfiguration diff --git a/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..d21a2cdc8d --- /dev/null +++ b/spring-boot-starters/wx-java-channel-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.binarywang.spring.starter.wxjava.channel.autoconfigure.WxChannelMultiAutoConfiguration diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/README.md b/spring-boot-starters/wx-java-channel-spring-boot-starter/README.md new file mode 100644 index 0000000000..398001a286 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/README.md @@ -0,0 +1,103 @@ +# wx-java-channel-spring-boot-starter + +## 快速开始 +1. 引入依赖 + ```xml + + + com.github.binarywang + wx-java-channel-spring-boot-starter + ${version} + + + + + redis.clients + jedis + ${jedis.version} + + + + + org.redisson + redisson + ${redisson.version} + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + ``` +2. 添加配置(application.properties) + ```properties + # 视频号配置(必填) + ## 视频号小店的appId和secret + wx.channel.app-id=@appId + wx.channel.secret=@secret + # 视频号配置 选填 + ## 设置视频号小店消息服务器配置的token + wx.channel.token=@token + ## 设置视频号小店消息服务器配置的EncodingAESKey + wx.channel.aes-key= + ## 支持JSON或者XML格式,默认JSON + wx.channel.msg-data-format=JSON + ## 是否使用稳定版 Access Token + wx.channel.use-stable-access-token=false + + + # ConfigStorage 配置(选填) + ## 配置类型: memory(默认), jedis, redisson, redis_template + wx.channel.config-storage.type=memory + ## 相关redis前缀配置: wx:channel(默认) + wx.channel.config-storage.key-prefix=wx:channel + wx.channel.config-storage.redis.host=127.0.0.1 + wx.channel.config-storage.redis.port=6379 + wx.channel.config-storage.redis.password=123456 + + # redis_template 方式使用spring data redis配置 + spring.data.redis.database=0 + spring.data.redis.host=127.0.0.1 + spring.data.redis.password=123456 + spring.data.redis.port=6379 + + # http 客户端配置(选填) + ## # http客户端类型: http_client(默认) + wx.channel.config-storage.http-client-type=http_client + wx.channel.config-storage.http-proxy-host= + wx.channel.config-storage.http-proxy-port= + wx.channel.config-storage.http-proxy-username= + wx.channel.config-storage.http-proxy-password= + ## 最大重试次数,默认:5 次,如果小于 0,则为 0 + wx.channel.config-storage.max-retry-times=5 + ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000 + wx.channel.config-storage.retry-sleep-millis=1000 + ``` +3. 自动注入的类型 +- `WxChannelService` +- `WxChannelConfig` +4. 使用样例 +```java +import me.chanjar.weixin.channel.api.WxChannelService; +import me.chanjar.weixin.channel.bean.shop.ShopInfoResponse; +import me.chanjar.weixin.channel.util.JsonUtils; +import me.chanjar.weixin.common.error.WxErrorException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class DemoService { + @Autowired + private WxChannelService wxChannelService; + + public String getShopInfo() throws WxErrorException { + // 获取店铺基本信息 + ShopInfoResponse response = wxChannelService.getBasicService().getShopInfo(); + // 此处为演示,如果要返回response的结果,建议自己封装一个VO,避免直接返回response + return JsonUtils.encode(response); + } +} +``` + diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml new file mode 100644 index 0000000000..f74d3bfaae --- /dev/null +++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml @@ -0,0 +1,59 @@ + + + wx-java-spring-boot-starters + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-channel-spring-boot-starter + WxJava - Spring Boot Starter for Channel + 微信视频号开发的 Spring Boot Starter + + + + com.github.binarywang + weixin-java-channel + ${project.version} + + + redis.clients + jedis + provided + + + org.redisson + redisson + provided + + + org.springframework.data + spring-data-redis + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + + diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/WxChannelAutoConfiguration.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/WxChannelAutoConfiguration.java new file mode 100644 index 0000000000..ad9d90b28d --- /dev/null +++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/WxChannelAutoConfiguration.java @@ -0,0 +1,20 @@ +package com.binarywang.spring.starter.wxjava.channel.config; + +import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * 自动配置 + * + * @author Zeyes + */ +@Configuration +@EnableConfigurationProperties(WxChannelProperties.class) +@Import({ + WxChannelStorageAutoConfiguration.class, + WxChannelServiceAutoConfiguration.class +}) +public class WxChannelAutoConfiguration { +} diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/WxChannelServiceAutoConfiguration.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/WxChannelServiceAutoConfiguration.java new file mode 100644 index 0000000000..5276a803e7 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/WxChannelServiceAutoConfiguration.java @@ -0,0 +1,37 @@ +package com.binarywang.spring.starter.wxjava.channel.config; + + +import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelProperties; +import lombok.AllArgsConstructor; +import me.chanjar.weixin.channel.api.WxChannelService; +import me.chanjar.weixin.channel.api.impl.WxChannelServiceImpl; +import me.chanjar.weixin.channel.config.WxChannelConfig; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 微信小程序平台相关服务自动注册 + * + * @author Zeyes + */ +@Configuration +@AllArgsConstructor +public class WxChannelServiceAutoConfiguration { + private final WxChannelProperties properties; + + /** + * Channel Service + * + * @return Channel Service + */ + @Bean + @ConditionalOnMissingBean(WxChannelService.class) + @ConditionalOnBean(WxChannelConfig.class) + public WxChannelService wxChannelService(WxChannelConfig wxChannelConfig) { + WxChannelService wxChannelService = new WxChannelServiceImpl(); + wxChannelService.setConfig(wxChannelConfig); + return wxChannelService; + } +} diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/WxChannelStorageAutoConfiguration.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/WxChannelStorageAutoConfiguration.java new file mode 100644 index 0000000000..66f2276a35 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/WxChannelStorageAutoConfiguration.java @@ -0,0 +1,23 @@ +package com.binarywang.spring.starter.wxjava.channel.config; + +import com.binarywang.spring.starter.wxjava.channel.config.storage.WxChannelInJedisConfigStorageConfiguration; +import com.binarywang.spring.starter.wxjava.channel.config.storage.WxChannelInMemoryConfigStorageConfiguration; +import com.binarywang.spring.starter.wxjava.channel.config.storage.WxChannelInRedisTemplateConfigStorageConfiguration; +import com.binarywang.spring.starter.wxjava.channel.config.storage.WxChannelInRedissonConfigStorageConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * 微信小程序存储策略自动配置 + * + * @author Zeyes + */ +@Configuration +@Import({ + WxChannelInMemoryConfigStorageConfiguration.class, + WxChannelInJedisConfigStorageConfiguration.class, + WxChannelInRedisTemplateConfigStorageConfiguration.class, + WxChannelInRedissonConfigStorageConfiguration.class +}) +public class WxChannelStorageAutoConfiguration { +} diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/AbstractWxChannelConfigStorageConfiguration.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/AbstractWxChannelConfigStorageConfiguration.java new file mode 100644 index 0000000000..2a7978640d --- /dev/null +++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/AbstractWxChannelConfigStorageConfiguration.java @@ -0,0 +1,42 @@ +package com.binarywang.spring.starter.wxjava.channel.config.storage; + +import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelProperties; +import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl; +import org.apache.commons.lang3.StringUtils; + +/** + * @author Zeyes + */ +public abstract class AbstractWxChannelConfigStorageConfiguration { + + protected WxChannelDefaultConfigImpl config(WxChannelDefaultConfigImpl config, WxChannelProperties properties) { + config.setAppid(StringUtils.trimToNull(properties.getAppid())); + config.setSecret(StringUtils.trimToNull(properties.getSecret())); + config.setToken(StringUtils.trimToNull(properties.getToken())); + config.setAesKey(StringUtils.trimToNull(properties.getAesKey())); + config.setMsgDataFormat(StringUtils.trimToNull(properties.getMsgDataFormat())); + config.setStableAccessToken(properties.isUseStableAccessToken()); + config.setApiHostUrl(StringUtils.trimToNull(properties.getApiHostUrl())); + config.setAccessTokenUrl(StringUtils.trimToNull(properties.getAccessTokenUrl())); + + WxChannelProperties.ConfigStorage configStorageProperties = properties.getConfigStorage(); + config.setHttpProxyHost(configStorageProperties.getHttpProxyHost()); + config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername()); + config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword()); + if (configStorageProperties.getHttpProxyPort() != null) { + config.setHttpProxyPort(configStorageProperties.getHttpProxyPort()); + } + + int maxRetryTimes = configStorageProperties.getMaxRetryTimes(); + if (configStorageProperties.getMaxRetryTimes() < 0) { + maxRetryTimes = 0; + } + int retrySleepMillis = configStorageProperties.getRetrySleepMillis(); + if (retrySleepMillis < 0) { + retrySleepMillis = 1000; + } + config.setRetrySleepMillis(retrySleepMillis); + config.setMaxRetryTimes(maxRetryTimes); + return config; + } +} diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInJedisConfigStorageConfiguration.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInJedisConfigStorageConfiguration.java new file mode 100644 index 0000000000..f88548c3e9 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInJedisConfigStorageConfiguration.java @@ -0,0 +1,73 @@ +package com.binarywang.spring.starter.wxjava.channel.config.storage; + + +import com.binarywang.spring.starter.wxjava.channel.properties.RedisProperties; +import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.channel.config.WxChannelConfig; +import me.chanjar.weixin.channel.config.impl.WxChannelRedisConfigImpl; +import me.chanjar.weixin.common.redis.JedisWxRedisOps; +import me.chanjar.weixin.common.redis.WxRedisOps; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * @author Zeyes + */ +@Configuration +@ConditionalOnProperty(prefix = WxChannelProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis") +@ConditionalOnClass({JedisPool.class, JedisPoolConfig.class}) +@RequiredArgsConstructor +public class WxChannelInJedisConfigStorageConfiguration extends AbstractWxChannelConfigStorageConfiguration { + private final WxChannelProperties properties; + private final ApplicationContext applicationContext; + + @Bean + @ConditionalOnMissingBean(WxChannelConfig.class) + public WxChannelConfig wxChannelConfig() { + WxChannelRedisConfigImpl config = getWxChannelRedisConfig(); + return this.config(config, properties); + } + + private WxChannelRedisConfigImpl getWxChannelRedisConfig() { + RedisProperties redisProperties = properties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + jedisPool = getJedisPool(); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + WxRedisOps redisOps = new JedisWxRedisOps(jedisPool); + return new WxChannelRedisConfigImpl(redisOps, properties.getConfigStorage().getKeyPrefix()); + } + + private JedisPool getJedisPool() { + WxChannelProperties.ConfigStorage storage = properties.getConfigStorage(); + RedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(), redis.getDatabase()); + } +} diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInMemoryConfigStorageConfiguration.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInMemoryConfigStorageConfiguration.java new file mode 100644 index 0000000000..deb586ae7b --- /dev/null +++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInMemoryConfigStorageConfiguration.java @@ -0,0 +1,29 @@ +package com.binarywang.spring.starter.wxjava.channel.config.storage; + + +import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.channel.config.WxChannelConfig; +import me.chanjar.weixin.channel.config.impl.WxChannelDefaultConfigImpl; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Zeyes + */ +@Configuration +@ConditionalOnProperty(prefix = WxChannelProperties.PREFIX + ".config-storage", name = "type", + matchIfMissing = true, havingValue = "memory") +@RequiredArgsConstructor +public class WxChannelInMemoryConfigStorageConfiguration extends AbstractWxChannelConfigStorageConfiguration { + private final WxChannelProperties properties; + + @Bean + @ConditionalOnMissingBean(WxChannelProperties.class) + public WxChannelConfig wxChannelConfig() { + WxChannelDefaultConfigImpl config = new WxChannelDefaultConfigImpl(); + return this.config(config, properties); + } +} diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInRedisTemplateConfigStorageConfiguration.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInRedisTemplateConfigStorageConfiguration.java new file mode 100644 index 0000000000..e190fbd755 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInRedisTemplateConfigStorageConfiguration.java @@ -0,0 +1,40 @@ +package com.binarywang.spring.starter.wxjava.channel.config.storage; + +import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.channel.config.WxChannelConfig; +import me.chanjar.weixin.channel.config.impl.WxChannelRedisConfigImpl; +import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps; +import me.chanjar.weixin.common.redis.WxRedisOps; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * @author Zeyes + */ +@Configuration +@ConditionalOnProperty(prefix = WxChannelProperties.PREFIX + ".config-storage", name = "type", havingValue = "redistemplate") +@ConditionalOnClass(StringRedisTemplate.class) +@RequiredArgsConstructor +public class WxChannelInRedisTemplateConfigStorageConfiguration extends AbstractWxChannelConfigStorageConfiguration { + private final WxChannelProperties properties; + private final ApplicationContext applicationContext; + + @Bean + @ConditionalOnMissingBean(WxChannelConfig.class) + public WxChannelConfig wxChannelConfig() { + WxChannelRedisConfigImpl config = getWxChannelInRedisTemplateConfig(); + return this.config(config, properties); + } + + private WxChannelRedisConfigImpl getWxChannelInRedisTemplateConfig() { + StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class); + WxRedisOps redisOps = new RedisTemplateWxRedisOps(redisTemplate); + return new WxChannelRedisConfigImpl(redisOps, properties.getConfigStorage().getKeyPrefix()); + } +} diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInRedissonConfigStorageConfiguration.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInRedissonConfigStorageConfiguration.java new file mode 100644 index 0000000000..16db4395a7 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/config/storage/WxChannelInRedissonConfigStorageConfiguration.java @@ -0,0 +1,62 @@ +package com.binarywang.spring.starter.wxjava.channel.config.storage; + + +import com.binarywang.spring.starter.wxjava.channel.properties.RedisProperties; +import com.binarywang.spring.starter.wxjava.channel.properties.WxChannelProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.channel.config.WxChannelConfig; +import me.chanjar.weixin.channel.config.impl.WxChannelRedissonConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Zeyes + */ +@Configuration +@ConditionalOnProperty(prefix = WxChannelProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson") +@ConditionalOnClass({Redisson.class, RedissonClient.class}) +@RequiredArgsConstructor +public class WxChannelInRedissonConfigStorageConfiguration extends AbstractWxChannelConfigStorageConfiguration { + private final WxChannelProperties properties; + private final ApplicationContext applicationContext; + + @Bean + @ConditionalOnMissingBean(WxChannelConfig.class) + public WxChannelConfig wxChannelConfig() { + WxChannelRedissonConfigImpl config = getWxChannelRedissonConfig(); + return this.config(config, properties); + } + + private WxChannelRedissonConfigImpl getWxChannelRedissonConfig() { + RedisProperties redisProperties = properties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + redissonClient = getRedissonClient(); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxChannelRedissonConfigImpl(redissonClient, properties.getConfigStorage().getKeyPrefix()); + } + + private RedissonClient getRedissonClient() { + WxChannelProperties.ConfigStorage storage = properties.getConfigStorage(); + RedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redis.getHost() + ":" + redis.getPort()) + .setDatabase(redis.getDatabase()) + .setPassword(redis.getPassword()); + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/HttpClientType.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/HttpClientType.java new file mode 100644 index 0000000000..e4b3f3ad16 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/HttpClientType.java @@ -0,0 +1,17 @@ +package com.binarywang.spring.starter.wxjava.channel.enums; + +/** + * httpclient类型 + * + * @author Zeyes + */ +public enum HttpClientType { + /** + * HttpClient. + */ + HttpClient, + /** + * HttpComponents. + */ + HttpComponents, +} diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/StorageType.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/StorageType.java new file mode 100644 index 0000000000..59b27fc022 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/enums/StorageType.java @@ -0,0 +1,25 @@ +package com.binarywang.spring.starter.wxjava.channel.enums; + +/** + * storage类型 + * + * @author Zeyes + */ +public enum StorageType { + /** + * 内存 + */ + Memory, + /** + * redis(JedisClient) + */ + Jedis, + /** + * redis(Redisson) + */ + Redisson, + /** + * redis(RedisTemplate) + */ + RedisTemplate +} diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/RedisProperties.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/RedisProperties.java new file mode 100644 index 0000000000..19f27d0682 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/RedisProperties.java @@ -0,0 +1,42 @@ +package com.binarywang.spring.starter.wxjava.channel.properties; + +import lombok.Data; + +/** + * redis 配置 + * + * @author Zeyes + */ +@Data +public class RedisProperties { + + /** + * 主机地址,不填则从spring容器内获取JedisPool + */ + private String host; + + /** + * 端口号 + */ + private int port = 6379; + + /** + * 密码 + */ + private String password; + + /** + * 超时 + */ + private int timeout = 2000; + + /** + * 数据库 + */ + private int database = 0; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelProperties.java b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelProperties.java new file mode 100644 index 0000000000..f43d297e0b --- /dev/null +++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/channel/properties/WxChannelProperties.java @@ -0,0 +1,126 @@ +package com.binarywang.spring.starter.wxjava.channel.properties; + +import com.binarywang.spring.starter.wxjava.channel.enums.HttpClientType; +import com.binarywang.spring.starter.wxjava.channel.enums.StorageType; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +/** + * 属性配置类 + * + * @author Zeyes + */ +@Data +@ConfigurationProperties(prefix = WxChannelProperties.PREFIX) +public class WxChannelProperties { + public static final String PREFIX = "wx.channel"; + + /** + * 设置视频号小店的appid + */ + private String appid; + + /** + * 设置视频号小店的Secret + */ + private String secret; + + /** + * 设置视频号小店消息服务器配置的token. + */ + private String token; + + /** + * 设置视频号小店消息服务器配置的EncodingAESKey + */ + private String aesKey; + + /** + * 消息格式,XML或者JSON + */ + private String msgDataFormat = "JSON"; + + /** + * 是否使用稳定版 Access Token + */ + private boolean useStableAccessToken = false; + + /** + * 自定义API主机地址,用于替换默认的 https://api.weixin.qq.com + * 例如:http://proxy.company.com:8080 + */ + private String apiHostUrl; + + /** + * 自定义获取AccessToken地址,用于向自定义统一服务获取AccessToken + * 例如:http://proxy.company.com:8080/oauth/token + */ + private String accessTokenUrl; + + /** + * 存储策略 + */ + private final ConfigStorage configStorage = new ConfigStorage(); + + @Data + public static class ConfigStorage { + + /** + * 存储类型 + */ + private StorageType type = StorageType.Memory; + + /** + * 指定key前缀 + */ + private String keyPrefix = "wh"; + + /** + * redis连接配置 + */ + @NestedConfigurationProperty + private final RedisProperties redis = new RedisProperties(); + + /** + * http客户端类型 + */ + private HttpClientType httpClientType = HttpClientType.HttpComponents; + + /** + * http代理主机 + */ + private String httpProxyHost; + + /** + * http代理端口 + */ + private Integer httpProxyPort; + + /** + * http代理用户名 + */ + private String httpProxyUsername; + + /** + * http代理密码 + */ + private String httpProxyPassword; + + /** + * http 请求重试间隔 + *
+     *   {@link me.chanjar.weixin.channel.api.BaseWxChannelService#setRetrySleepMillis(int)}
+     * 
+ */ + private int retrySleepMillis = 1000; + /** + * http 请求最大重试次数 + *
+     *   {@link me.chanjar.weixin.channel.api.BaseWxChannelService#setMaxRetryTimes(int)}
+     * 
+ */ + private int maxRetryTimes = 5; + } + +} diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..a9401752a0 --- /dev/null +++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + com.binarywang.spring.starter.wxjava.channel.config.WxChannelAutoConfiguration diff --git a/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..99ccbadbbc --- /dev/null +++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.binarywang.spring.starter.wxjava.channel.config.WxChannelAutoConfiguration diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/README.md b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/README.md new file mode 100644 index 0000000000..0f0b74695e --- /dev/null +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/README.md @@ -0,0 +1,112 @@ +# wx-java-cp-multi-spring-boot-starter + +企业微信多账号配置 + +- 实现多 WxCpService 初始化。 +- 未实现 WxCpTpService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。 +- 未实现 WxCpCgService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。 + +## 关于 corp-secret 的说明 + +企业微信中不同功能模块对应不同的 `corp-secret`,每种 Secret 只对对应模块的接口具有调用权限: + +| Secret 类型 | 获取位置 | 可调用的接口 | 是否需要 agent-id | +|---|---|---|---| +| 自建应用 Secret | 应用管理 → 自建应用 → 选择应用 → 查看 Secret | 该应用有权限的接口 | **必填** | +| 通讯录同步 Secret | 管理工具 → 通讯录同步 → 查看 Secret | 部门/成员增删改查等通讯录接口 | **不填** | +| 客户联系 Secret | 客户联系 → API → Secret | 客户联系相关接口 | 不填 | + +> **常见问题**: +> - 使用自建应用 Secret + agent-id 可以获取部门列表,但**无法更新部门**(因为写接口需要通讯录同步权限) +> - 使用通讯录同步 Secret 可以同步部门,但**调用某些需要 agent-id 的应用接口会报错** + +如需同时使用多种权限范围,可在 `wx.cp.corps` 下配置多个条目,每个条目使用对应权限的 Secret,通过不同的 `tenantId` 区分后使用。 + +> **配置限制说明**: +> - 当前 starter 实现会校验:同一 `corp-id` 下,`agent-id` **必须唯一** +> - 同一 `corp-id` 下,**只能有一个条目不填 `agent-id`** +> - 否则会因为 token/ticket 缓存 key 冲突而在启动时直接抛异常 +> +> 因此,像"通讯录同步 Secret""客户联系 Secret"这类通常不填写 `agent-id` 的配置,**不能**在同一个 `corp-id` 下同时配置多个 `agent-id` 均为空的条目;如确有多个条目,请确保其中最多只有一个未填写 `agent-id`。 + +## 快速开始 + +1. 引入依赖 + ```xml + + com.github.binarywang + wx-java-cp-multi-spring-boot-starter + ${version} + + ``` +2. 添加配置(application.properties) + ```properties + # 自建应用 1 配置(使用自建应用 Secret,需填写 agent-id) + wx.cp.corps.app1.corp-id = @corp-id + wx.cp.corps.app1.corp-secret = @自建应用的Secret(在"应用管理-自建应用"中查看) + wx.cp.corps.app1.agent-id = @自建应用的AgentId + ## 选填 + wx.cp.corps.app1.token = @token + wx.cp.corps.app1.aes-key = @aes-key + wx.cp.corps.app1.msg-audit-priKey = @msg-audit-priKey + wx.cp.corps.app1.msg-audit-lib-path = @msg-audit-lib-path + + # 通讯录同步配置(使用通讯录同步 Secret,不需要填写 agent-id) + # 此配置用于部门、成员的增删改查等通讯录管理操作 + wx.cp.corps.contact.corp-id = @corp-id + wx.cp.corps.contact.corp-secret = @通讯录同步的Secret(在"管理工具-通讯录同步"中查看) + ## agent-id 不填,通讯录同步不需要 agentId + + # 公共配置 + ## ConfigStorage 配置(选填) + wx.cp.config-storage.type=memory # 配置类型: memory(默认), jedis, redisson, redistemplate + ## http 客户端配置(选填) + ## # http客户端类型: http_client(默认), ok_http, jodd_http + wx.cp.config-storage.http-client-type=http_client + wx.cp.config-storage.http-proxy-host= + wx.cp.config-storage.http-proxy-port= + wx.cp.config-storage.http-proxy-username= + wx.cp.config-storage.http-proxy-password= + ## 最大重试次数,默认:5 次,如果小于 0,则为 0 + wx.cp.config-storage.max-retry-times=5 + ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000 + wx.cp.config-storage.retry-sleep-millis=1000 + ``` +3. 支持自动注入的类型: `WxCpMultiServices` + +4. 使用样例 + +```java +import com.binarywang.spring.starter.wxjava.cp.service.WxCpMultiServices; +import me.chanjar.weixin.cp.api.WxCpDepartmentService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.api.WxCpUserService; +import me.chanjar.weixin.cp.bean.WxCpDepart; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class DemoService { + @Autowired + private WxCpMultiServices wxCpMultiServices; + + public void test() { + // 使用自建应用的 WxCpService(对应 corp-secret 为自建应用 Secret) + WxCpService appService = wxCpMultiServices.getWxCpService("app1"); + WxCpUserService userService = appService.getUserService(); + userService.getUserId("xxx"); + // todo ... + + // 使用通讯录同步的 WxCpService(对应 corp-secret 为通讯录同步 Secret) + // 通讯录同步 Secret 具有部门/成员增删改查等权限 + WxCpService contactService = wxCpMultiServices.getWxCpService("contact"); + WxCpDepartmentService departmentService = contactService.getDepartmentService(); + // 更新部门示例(WxCpDepart 包含 id、name、parentId 等字段) + WxCpDepart depart = new WxCpDepart(); + depart.setId(100L); + depart.setName("新部门名称"); + departmentService.update(depart); + // todo ... + } +} +``` diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/pom.xml new file mode 100644 index 0000000000..0cb592a7fc --- /dev/null +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/pom.xml @@ -0,0 +1,60 @@ + + + + wx-java-spring-boot-starters + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-cp-multi-spring-boot-starter + WxJava - Spring Boot Starter for WxCp::支持多账号配置 + 微信企业号开发的 Spring Boot Starter::支持多账号配置 + + + + com.github.binarywang + weixin-java-cp + ${project.version} + + + redis.clients + jedis + provided + + + org.redisson + redisson + provided + + + org.springframework.data + spring-data-redis + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + + diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/autoconfigure/WxCpMultiAutoConfiguration.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/autoconfigure/WxCpMultiAutoConfiguration.java new file mode 100644 index 0000000000..40a6d9048d --- /dev/null +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/autoconfigure/WxCpMultiAutoConfiguration.java @@ -0,0 +1,16 @@ +package com.binarywang.spring.starter.wxjava.cp.autoconfigure; + +import com.binarywang.spring.starter.wxjava.cp.configuration.WxCpMultiServicesAutoConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * 企业微信自动注册 + * + * @author yl + * created on 2023/10/16 + */ +@Configuration +@Import(WxCpMultiServicesAutoConfiguration.class) +public class WxCpMultiAutoConfiguration { +} diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/WxCpMultiServicesAutoConfiguration.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/WxCpMultiServicesAutoConfiguration.java new file mode 100644 index 0000000000..12a4947301 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/WxCpMultiServicesAutoConfiguration.java @@ -0,0 +1,27 @@ +package com.binarywang.spring.starter.wxjava.cp.configuration; + +import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpInJedisConfiguration; +import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpInMemoryConfiguration; +import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpInRedisTemplateConfiguration; +import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpInRedissonConfiguration; +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpMultiProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * 企业微信平台相关服务自动注册 + * + * @author yl + * created on 2023/10/16 + */ +@Configuration +@EnableConfigurationProperties(WxCpMultiProperties.class) +@Import({ + WxCpInJedisConfiguration.class, + WxCpInMemoryConfiguration.class, + WxCpInRedissonConfiguration.class, + WxCpInRedisTemplateConfiguration.class +}) +public class WxCpMultiServicesAutoConfiguration { +} diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpConfiguration.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpConfiguration.java new file mode 100644 index 0000000000..a10bdf9bed --- /dev/null +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpConfiguration.java @@ -0,0 +1,173 @@ +package com.binarywang.spring.starter.wxjava.cp.configuration.services; + +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpMultiProperties; +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpSingleProperties; +import com.binarywang.spring.starter.wxjava.cp.service.WxCpMultiServices; +import com.binarywang.spring.starter.wxjava.cp.service.WxCpMultiServicesImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.api.impl.WxCpServiceApacheHttpClientImpl; +import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl; +import me.chanjar.weixin.cp.api.impl.WxCpServiceJoddHttpImpl; +import me.chanjar.weixin.cp.api.impl.WxCpServiceOkHttpImpl; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * WxCpConfigStorage 抽象配置类 + * + * @author yl + * created on 2023/10/16 + */ +@RequiredArgsConstructor +@Slf4j +public abstract class AbstractWxCpConfiguration { + + protected WxCpMultiServices wxCpMultiServices(WxCpMultiProperties wxCpMultiProperties) { + Map corps = wxCpMultiProperties.getCorps(); + if (corps == null || corps.isEmpty()) { + log.warn("企业微信应用参数未配置,通过 WxCpMultiServices#getWxCpService(\"tenantId\")获取实例将返回空"); + return new WxCpMultiServicesImpl(); + } + /** + * 校验同一个企业下,agentId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。 + * + *

同一企业(corpId 相同)下可配置多个条目以使用不同的权限 Secret,例如:

+ *
    + *
  • 自建应用条目:填写应用对应的 corpSecret 和 agentId
  • + *
  • 通讯录同步条目:填写通讯录同步 Secret,agentId 可不填(null)
  • + *
+ *

但同一 corpId 下不允许出现重复的 agentId(包括多个 null)。

+ * + * 查看 {@link me.chanjar.weixin.cp.config.impl.AbstractWxCpInRedisConfigImpl#setAgentId(Integer)} + */ + Collection corpList = corps.values(); + if (corpList.size() > 1) { + // 先按 corpId 分组统计 + Map> corpsMap = corpList.stream() + .collect(Collectors.groupingBy(WxCpSingleProperties::getCorpId)); + Set>> entries = corpsMap.entrySet(); + for (Map.Entry> entry : entries) { + String corpId = entry.getKey(); + // 校验每个企业下,agentId 是否唯一 + boolean multi = entry.getValue().stream() + // 通讯录没有 agentId,使用字符串转换避免 null 与 agentId=0 冲突 + .collect(Collectors.groupingBy(c -> Objects.toString(c.getAgentId(), "null"), Collectors.counting())) + .entrySet().stream().anyMatch(e -> e.getValue() > 1); + if (multi) { + throw new RuntimeException("请确保企业微信配置唯一性[" + corpId + "]"); + } + } + } + WxCpMultiServicesImpl services = new WxCpMultiServicesImpl(); + + Set> entries = corps.entrySet(); + for (Map.Entry entry : entries) { + String tenantId = entry.getKey(); + WxCpSingleProperties wxCpSingleProperties = entry.getValue(); + WxCpDefaultConfigImpl storage = this.wxCpConfigStorage(wxCpMultiProperties); + this.configCorp(storage, wxCpSingleProperties); + this.configHttp(storage, wxCpMultiProperties.getConfigStorage()); + WxCpService wxCpService = this.wxCpService(storage, wxCpMultiProperties.getConfigStorage()); + services.addWxCpService(tenantId, wxCpService); + } + return services; + } + + /** + * 配置 WxCpDefaultConfigImpl + * + * @param wxCpMultiProperties 参数 + * @return WxCpDefaultConfigImpl + */ + protected abstract WxCpDefaultConfigImpl wxCpConfigStorage(WxCpMultiProperties wxCpMultiProperties); + + private WxCpService wxCpService(WxCpConfigStorage wxCpConfigStorage, WxCpMultiProperties.ConfigStorage storage) { + WxCpMultiProperties.HttpClientType httpClientType = storage.getHttpClientType(); + WxCpService wxCpService; + switch (httpClientType) { + case OK_HTTP: + wxCpService = new WxCpServiceOkHttpImpl(); + break; + case JODD_HTTP: + wxCpService = new WxCpServiceJoddHttpImpl(); + break; + case HTTP_CLIENT: + wxCpService = new WxCpServiceApacheHttpClientImpl(); + break; + default: + wxCpService = new WxCpServiceImpl(); + break; + } + wxCpService.setWxCpConfigStorage(wxCpConfigStorage); + int maxRetryTimes = storage.getMaxRetryTimes(); + if (maxRetryTimes < 0) { + maxRetryTimes = 0; + } + int retrySleepMillis = storage.getRetrySleepMillis(); + if (retrySleepMillis < 0) { + retrySleepMillis = 1000; + } + wxCpService.setRetrySleepMillis(retrySleepMillis); + wxCpService.setMaxRetryTimes(maxRetryTimes); + return wxCpService; + } + + private void configCorp(WxCpDefaultConfigImpl config, WxCpSingleProperties wxCpSingleProperties) { + String corpId = wxCpSingleProperties.getCorpId(); + String corpSecret = wxCpSingleProperties.getCorpSecret(); + Integer agentId = wxCpSingleProperties.getAgentId(); + String token = wxCpSingleProperties.getToken(); + String aesKey = wxCpSingleProperties.getAesKey(); + // 企业微信,私钥,会话存档路径 + String msgAuditPriKey = wxCpSingleProperties.getMsgAuditPriKey(); + String msgAuditLibPath = wxCpSingleProperties.getMsgAuditLibPath(); + + config.setCorpId(corpId); + config.setCorpSecret(corpSecret); + config.setAgentId(agentId); + if (StringUtils.isNotBlank(token)) { + config.setToken(token); + } + if (StringUtils.isNotBlank(aesKey)) { + config.setAesKey(aesKey); + } + if (StringUtils.isNotBlank(msgAuditPriKey)) { + config.setMsgAuditPriKey(msgAuditPriKey); + } + if (StringUtils.isNotBlank(msgAuditLibPath)) { + config.setMsgAuditLibPath(msgAuditLibPath); + } + if (StringUtils.isNotBlank(wxCpSingleProperties.getBaseApiUrl())) { + config.setBaseApiUrl(wxCpSingleProperties.getBaseApiUrl()); + } + } + + private void configHttp(WxCpDefaultConfigImpl config, WxCpMultiProperties.ConfigStorage storage) { + String httpProxyHost = storage.getHttpProxyHost(); + Integer httpProxyPort = storage.getHttpProxyPort(); + String httpProxyUsername = storage.getHttpProxyUsername(); + String httpProxyPassword = storage.getHttpProxyPassword(); + if (StringUtils.isNotBlank(httpProxyHost)) { + config.setHttpProxyHost(httpProxyHost); + if (httpProxyPort != null) { + config.setHttpProxyPort(httpProxyPort); + } + if (StringUtils.isNotBlank(httpProxyUsername)) { + config.setHttpProxyUsername(httpProxyUsername); + } + if (StringUtils.isNotBlank(httpProxyPassword)) { + config.setHttpProxyPassword(httpProxyPassword); + } + } + } +} diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInJedisConfiguration.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInJedisConfiguration.java new file mode 100644 index 0000000000..e03647cb63 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInJedisConfiguration.java @@ -0,0 +1,76 @@ +package com.binarywang.spring.starter.wxjava.cp.configuration.services; + +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpMultiProperties; +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpMultiRedisProperties; +import com.binarywang.spring.starter.wxjava.cp.service.WxCpMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import me.chanjar.weixin.cp.config.impl.WxCpJedisConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * 自动装配基于 jedis 策略配置 + * + * @author yl + * created on 2023/10/16 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxCpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis" +) +@RequiredArgsConstructor +public class WxCpInJedisConfiguration extends AbstractWxCpConfiguration { + private final WxCpMultiProperties wxCpMultiProperties; + private final ApplicationContext applicationContext; + + @Bean + public WxCpMultiServices wxCpMultiServices() { + return this.wxCpMultiServices(wxCpMultiProperties); + } + + @Override + protected WxCpDefaultConfigImpl wxCpConfigStorage(WxCpMultiProperties wxCpMultiProperties) { + return this.configRedis(wxCpMultiProperties); + } + + private WxCpDefaultConfigImpl configRedis(WxCpMultiProperties wxCpMultiProperties) { + WxCpMultiRedisProperties wxCpMultiRedisProperties = wxCpMultiProperties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (wxCpMultiRedisProperties != null && StringUtils.isNotEmpty(wxCpMultiRedisProperties.getHost())) { + jedisPool = getJedisPool(wxCpMultiProperties); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + return new WxCpJedisConfigImpl(jedisPool, wxCpMultiProperties.getConfigStorage().getKeyPrefix()); + } + + private JedisPool getJedisPool(WxCpMultiProperties wxCpMultiProperties) { + WxCpMultiProperties.ConfigStorage storage = wxCpMultiProperties.getConfigStorage(); + WxCpMultiRedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), + redis.getTimeout(), redis.getPassword(), redis.getDatabase()); + } +} diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInMemoryConfiguration.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInMemoryConfiguration.java new file mode 100644 index 0000000000..29593667ed --- /dev/null +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInMemoryConfiguration.java @@ -0,0 +1,38 @@ +package com.binarywang.spring.starter.wxjava.cp.configuration.services; + +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpMultiProperties; +import com.binarywang.spring.starter.wxjava.cp.service.WxCpMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 自动装配基于内存策略配置 + * + * @author yl + * created on 2023/10/16 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxCpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "memory", matchIfMissing = true +) +@RequiredArgsConstructor +public class WxCpInMemoryConfiguration extends AbstractWxCpConfiguration { + private final WxCpMultiProperties wxCpMultiProperties; + + @Bean + public WxCpMultiServices wxCpMultiServices() { + return this.wxCpMultiServices(wxCpMultiProperties); + } + + @Override + protected WxCpDefaultConfigImpl wxCpConfigStorage(WxCpMultiProperties wxCpMultiProperties) { + return this.configInMemory(); + } + + private WxCpDefaultConfigImpl configInMemory() { + return new WxCpDefaultConfigImpl(); + } +} diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInRedisTemplateConfiguration.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInRedisTemplateConfiguration.java new file mode 100644 index 0000000000..374c5cdfb0 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInRedisTemplateConfiguration.java @@ -0,0 +1,43 @@ +package com.binarywang.spring.starter.wxjava.cp.configuration.services; + +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpMultiProperties; +import com.binarywang.spring.starter.wxjava.cp.service.WxCpMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import me.chanjar.weixin.cp.config.impl.WxCpRedisTemplateConfigImpl; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * 自动装配基于 redisTemplate 策略配置 + * + * @author yl + * created on 2023/10/16 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxCpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redistemplate" +) +@RequiredArgsConstructor +public class WxCpInRedisTemplateConfiguration extends AbstractWxCpConfiguration { + private final WxCpMultiProperties wxCpMultiProperties; + private final ApplicationContext applicationContext; + + @Bean + public WxCpMultiServices wxCpMultiServices() { + return this.wxCpMultiServices(wxCpMultiProperties); + } + + @Override + protected WxCpDefaultConfigImpl wxCpConfigStorage(WxCpMultiProperties wxCpMultiProperties) { + return this.configRedisTemplate(wxCpMultiProperties); + } + + private WxCpDefaultConfigImpl configRedisTemplate(WxCpMultiProperties wxCpMultiProperties) { + StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class); + return new WxCpRedisTemplateConfigImpl(redisTemplate, wxCpMultiProperties.getConfigStorage().getKeyPrefix()); + } +} diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInRedissonConfiguration.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInRedissonConfiguration.java new file mode 100644 index 0000000000..c0753a44aa --- /dev/null +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpInRedissonConfiguration.java @@ -0,0 +1,67 @@ +package com.binarywang.spring.starter.wxjava.cp.configuration.services; + +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpMultiProperties; +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpMultiRedisProperties; +import com.binarywang.spring.starter.wxjava.cp.service.WxCpMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import me.chanjar.weixin.cp.config.impl.WxCpRedissonConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 自动装配基于 redisson 策略配置 + * + * @author yl + * created on 2023/10/16 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxCpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson" +) +@RequiredArgsConstructor +public class WxCpInRedissonConfiguration extends AbstractWxCpConfiguration { + private final WxCpMultiProperties wxCpMultiProperties; + private final ApplicationContext applicationContext; + + @Bean + public WxCpMultiServices wxCpMultiServices() { + return this.wxCpMultiServices(wxCpMultiProperties); + } + + @Override + protected WxCpDefaultConfigImpl wxCpConfigStorage(WxCpMultiProperties wxCpMultiProperties) { + return this.configRedisson(wxCpMultiProperties); + } + + private WxCpDefaultConfigImpl configRedisson(WxCpMultiProperties wxCpMultiProperties) { + WxCpMultiRedisProperties redisProperties = wxCpMultiProperties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + redissonClient = getRedissonClient(wxCpMultiProperties); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxCpRedissonConfigImpl(redissonClient, wxCpMultiProperties.getConfigStorage().getKeyPrefix()); + } + + private RedissonClient getRedissonClient(WxCpMultiProperties wxCpMultiProperties) { + WxCpMultiProperties.ConfigStorage storage = wxCpMultiProperties.getConfigStorage(); + WxCpMultiRedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redis.getHost() + ":" + redis.getPort()) + .setDatabase(redis.getDatabase()) + .setPassword(redis.getPassword()); + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpMultiProperties.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpMultiProperties.java new file mode 100644 index 0000000000..ab694a30b2 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpMultiProperties.java @@ -0,0 +1,129 @@ +package com.binarywang.spring.starter.wxjava.cp.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * 企业微信多企业接入相关配置属性 + * + * @author yl + * created on 2023/10/16 + */ +@Data +@NoArgsConstructor +@ConfigurationProperties(prefix = WxCpMultiProperties.PREFIX) +public class WxCpMultiProperties implements Serializable { + private static final long serialVersionUID = -1569510477055668503L; + public static final String PREFIX = "wx.cp"; + + private Map corps = new HashMap<>(); + + /** + * 配置存储策略,默认内存 + */ + private ConfigStorage configStorage = new ConfigStorage(); + + @Data + @NoArgsConstructor + public static class ConfigStorage implements Serializable { + private static final long serialVersionUID = 4815731027000065434L; + /** + * 存储类型 + */ + private StorageType type = StorageType.memory; + + /** + * 指定key前缀 + */ + private String keyPrefix = "wx:cp"; + + /** + * redis连接配置 + */ + @NestedConfigurationProperty + private WxCpMultiRedisProperties redis = new WxCpMultiRedisProperties(); + + /** + * http客户端类型. + */ + private HttpClientType httpClientType = HttpClientType.HTTP_CLIENT; + + /** + * http代理主机 + */ + private String httpProxyHost; + + /** + * http代理端口 + */ + private Integer httpProxyPort; + + /** + * http代理用户名 + */ + private String httpProxyUsername; + + /** + * http代理密码 + */ + private String httpProxyPassword; + + /** + * http 请求最大重试次数 + *
+     *   {@link me.chanjar.weixin.cp.api.WxCpService#setMaxRetryTimes(int)}
+     *   {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setMaxRetryTimes(int)}
+     * 
+ */ + private int maxRetryTimes = 5; + + /** + * http 请求重试间隔 + *
+     *   {@link me.chanjar.weixin.cp.api.WxCpService#setRetrySleepMillis(int)}
+     *   {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setRetrySleepMillis(int)}
+     * 
+ */ + private int retrySleepMillis = 1000; + } + + public enum StorageType { + /** + * 内存 + */ + memory, + /** + * jedis + */ + jedis, + /** + * redisson + */ + redisson, + /** + * redistemplate + */ + redistemplate + } + + public enum HttpClientType { + /** + * HttpClient + */ + HTTP_CLIENT, + /** + * OkHttp + */ + OK_HTTP, + /** + * JoddHttp + */ + JODD_HTTP + } +} diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpMultiRedisProperties.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpMultiRedisProperties.java new file mode 100644 index 0000000000..ea1f257c41 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpMultiRedisProperties.java @@ -0,0 +1,48 @@ +package com.binarywang.spring.starter.wxjava.cp.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * Redis配置. + * + * @author yl + * created on 2023/10/16 + */ +@Data +@NoArgsConstructor +public class WxCpMultiRedisProperties implements Serializable { + private static final long serialVersionUID = -5924815351660074401L; + + /** + * 主机地址. + */ + private String host; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpSingleProperties.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpSingleProperties.java new file mode 100644 index 0000000000..fcfa654a15 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpSingleProperties.java @@ -0,0 +1,74 @@ +package com.binarywang.spring.starter.wxjava.cp.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 企业微信企业相关配置属性 + * + *

企业微信中不同的 corpSecret 对应不同的权限范围,常见的有:

+ *
    + *
  • 自建应用 Secret:在"应用管理 - 自建应用"中查看,只能调用该应用有权限的接口
  • + *
  • 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看,用于管理部门和成员(增删改查)
  • + *
  • 客户联系 Secret:在"客户联系"中查看,用于客户联系相关接口
  • + *
+ *

如需同时使用多种权限范围(例如:既要操作通讯录,又要调用自建应用接口), + * 可在 {@code wx.cp.corps} 下配置多个条目,每个条目使用对应权限的 {@code corpSecret}, + * 其中通讯录同步的条目无需填写 {@code agentId}。

+ * + * @author yl + * created on 2023/10/16 + */ +@Data +@NoArgsConstructor +public class WxCpSingleProperties implements Serializable { + private static final long serialVersionUID = -7502823825007859418L; + /** + * 微信企业号 corpId + */ + private String corpId; + /** + * 微信企业号 corpSecret(权限密钥) + * + *

企业微信针对不同的功能模块提供了不同的 Secret,每种 Secret 只对对应模块的接口有调用权限:

+ *
    + *
  • 自建应用 Secret:在"应用管理 - 自建应用"中找到对应应用,查看其 Secret, + * 使用时需同时配置对应的 {@code agentId}
  • + *
  • 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看, + * 使用此 Secret 可管理部门、成员,无需配置 {@code agentId}
  • + *
  • 其他 Secret(客户联系等):根据需要在企业微信后台查看对应 Secret
  • + *
+ */ + private String corpSecret; + /** + * 微信企业号应用 token + */ + private String token; + /** + * 微信企业号应用 ID(AgentId) + * + *

使用自建应用 Secret 时,需要填写对应应用的 AgentId。

+ *

使用通讯录同步 Secret 时,无需填写此字段。

+ */ + private Integer agentId; + /** + * 微信企业号应用 EncodingAESKey + */ + private String aesKey; + /** + * 微信企业号应用 会话存档私钥 + */ + private String msgAuditPriKey; + /** + * 微信企业号应用 会话存档类库路径 + */ + private String msgAuditLibPath; + + /** + * 自定义企业微信服务器baseUrl,用于替换默认的 https://qyapi.weixin.qq.com + * 例如:http://proxy.company.com:8080 + */ + private String baseApiUrl; +} diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpMultiServices.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpMultiServices.java new file mode 100644 index 0000000000..dfcb25631d --- /dev/null +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpMultiServices.java @@ -0,0 +1,26 @@ +package com.binarywang.spring.starter.wxjava.cp.service; + +import me.chanjar.weixin.cp.api.WxCpService; + +/** + * 企业微信 {@link WxCpService} 所有实例存放类. + * + * @author yl + * created on 2023/10/16 + */ +public interface WxCpMultiServices { + /** + * 通过租户 Id 获取 WxCpService + * + * @param tenantId 租户 Id + * @return WxCpService + */ + WxCpService getWxCpService(String tenantId); + + /** + * 根据租户 Id,从列表中移除一个 WxCpService 实例 + * + * @param tenantId 租户 Id + */ + void removeWxCpService(String tenantId); +} diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpMultiServicesImpl.java b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpMultiServicesImpl.java new file mode 100644 index 0000000000..19eae24159 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpMultiServicesImpl.java @@ -0,0 +1,42 @@ +package com.binarywang.spring.starter.wxjava.cp.service; + +import me.chanjar.weixin.cp.api.WxCpService; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 企业微信 {@link WxCpMultiServices} 默认实现 + * + * @author yl + * created on 2023/10/16 + */ +public class WxCpMultiServicesImpl implements WxCpMultiServices { + private final Map services = new ConcurrentHashMap<>(); + + /** + * 通过租户 Id 获取 WxCpService + * + * @param tenantId 租户 Id + * @return WxCpService + */ + @Override + public WxCpService getWxCpService(String tenantId) { + return this.services.get(tenantId); + } + + /** + * 根据租户 Id,添加一个 WxCpService 到列表 + * + * @param tenantId 租户 Id + * @param wxCpService WxCpService 实例 + */ + public void addWxCpService(String tenantId, WxCpService wxCpService) { + this.services.put(tenantId, wxCpService); + } + + @Override + public void removeWxCpService(String tenantId) { + this.services.remove(tenantId); + } +} diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..6010561a96 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.binarywang.spring.starter.wxjava.cp.autoconfigure.WxCpMultiAutoConfiguration diff --git a/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..3c48ec34e1 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.binarywang.spring.starter.wxjava.cp.autoconfigure.WxCpMultiAutoConfiguration diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/README.md b/spring-boot-starters/wx-java-cp-spring-boot-starter/README.md new file mode 100644 index 0000000000..d6c1abc945 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/README.md @@ -0,0 +1,41 @@ +# wx-java-cp-spring-boot-starter + +## 快速开始 + +1. 引入依赖 + ```xml + + com.github.binarywang + wx-java-cp-spring-boot-starter + ${version} + + ``` +2. 添加配置(application.properties) + ```properties + # 企业微信号配置(必填) + wx.cp.corp-id = @corp-id + wx.cp.corp-secret = @corp-secret + # 选填 + wx.cp.agent-id = @agent-id + wx.cp.token = @token + wx.cp.aes-key = @aes-key + wx.cp.msg-audit-priKey = @msg-audit-priKey + wx.cp.msg-audit-lib-path = @msg-audit-lib-path + # ConfigStorage 配置(选填) + wx.cp.config-storage.type=memory # 配置类型: memory(默认), jedis, redisson, redistemplate + # http 客户端配置(选填) + wx.cp.config-storage.http-proxy-host= + wx.cp.config-storage.http-proxy-port= + wx.cp.config-storage.http-proxy-username= + wx.cp.config-storage.http-proxy-password= + # 最大重试次数,默认:5 次,如果小于 0,则为 0 + wx.cp.config-storage.max-retry-times=5 + # 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000 + wx.cp.config-storage.retry-sleep-millis=1000 + ``` +3. 支持自动注入的类型: `WxCpService`, `WxCpConfigStorage` + +4. 覆盖自动配置: 自定义注入的bean会覆盖自动注入的 + +- WxCpService +- WxCpConfigStorage diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml new file mode 100644 index 0000000000..881064d493 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml @@ -0,0 +1,57 @@ + + + + wx-java-spring-boot-starters + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-cp-spring-boot-starter + WxJava - Spring Boot Starter for WxCp + 微信企业号开发的 Spring Boot Starter + + + + com.github.binarywang + weixin-java-cp + ${project.version} + + + redis.clients + jedis + + + org.redisson + redisson + + + org.springframework.data + spring-data-redis + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + + diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/config/WxCpAutoConfiguration.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/config/WxCpAutoConfiguration.java new file mode 100644 index 0000000000..f78c39dd45 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/config/WxCpAutoConfiguration.java @@ -0,0 +1,21 @@ +package com.binarywang.spring.starter.wxjava.cp.config; + +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * 企业微信自动注册 + * + * @author yl + * created on 2021/12/6 + */ +@Configuration +@EnableConfigurationProperties(WxCpProperties.class) +@Import({ + WxCpStorageAutoConfiguration.class, + WxCpServiceAutoConfiguration.class +}) +public class WxCpAutoConfiguration { +} diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/config/WxCpServiceAutoConfiguration.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/config/WxCpServiceAutoConfiguration.java new file mode 100644 index 0000000000..70c4045259 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/config/WxCpServiceAutoConfiguration.java @@ -0,0 +1,44 @@ +package com.binarywang.spring.starter.wxjava.cp.config; + +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.api.impl.WxCpServiceImpl; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 企业微信平台相关服务自动注册 + * + * @author yl + * created on 2021/12/6 + */ +@Configuration +@RequiredArgsConstructor +public class WxCpServiceAutoConfiguration { + private final WxCpProperties wxCpProperties; + + @Bean + @ConditionalOnMissingBean + @ConditionalOnBean(WxCpConfigStorage.class) + public WxCpService wxCpService(WxCpConfigStorage wxCpConfigStorage) { + WxCpService wxCpService = new WxCpServiceImpl(); + wxCpService.setWxCpConfigStorage(wxCpConfigStorage); + + WxCpProperties.ConfigStorage storage = wxCpProperties.getConfigStorage(); + int maxRetryTimes = storage.getMaxRetryTimes(); + if (maxRetryTimes < 0) { + maxRetryTimes = 0; + } + int retrySleepMillis = storage.getRetrySleepMillis(); + if (retrySleepMillis < 0) { + retrySleepMillis = 1000; + } + wxCpService.setRetrySleepMillis(retrySleepMillis); + wxCpService.setMaxRetryTimes(maxRetryTimes); + return wxCpService; + } +} diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/config/WxCpStorageAutoConfiguration.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/config/WxCpStorageAutoConfiguration.java new file mode 100644 index 0000000000..1c7d80b84e --- /dev/null +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/config/WxCpStorageAutoConfiguration.java @@ -0,0 +1,24 @@ +package com.binarywang.spring.starter.wxjava.cp.config; + +import com.binarywang.spring.starter.wxjava.cp.storage.WxCpInJedisConfigStorageConfiguration; +import com.binarywang.spring.starter.wxjava.cp.storage.WxCpInMemoryConfigStorageConfiguration; +import com.binarywang.spring.starter.wxjava.cp.storage.WxCpInRedisTemplateConfigStorageConfiguration; +import com.binarywang.spring.starter.wxjava.cp.storage.WxCpInRedissonConfigStorageConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * 企业微信存储策略自动配置 + * + * @author yl + * created on 2021/12/6 + */ +@Configuration +@Import({ + WxCpInMemoryConfigStorageConfiguration.class, + WxCpInJedisConfigStorageConfiguration.class, + WxCpInRedissonConfigStorageConfiguration.class, + WxCpInRedisTemplateConfigStorageConfiguration.class +}) +public class WxCpStorageAutoConfiguration { +} diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpProperties.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpProperties.java new file mode 100644 index 0000000000..c93a7e187f --- /dev/null +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpProperties.java @@ -0,0 +1,139 @@ +package com.binarywang.spring.starter.wxjava.cp.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import java.io.Serializable; + +/** + * 企业微信接入相关配置属性 + * + * @author yl + * created on 2021/12/6 + */ +@Data +@NoArgsConstructor +@ConfigurationProperties(prefix = WxCpProperties.PREFIX) +public class WxCpProperties { + public static final String PREFIX = "wx.cp"; + + /** + * 微信企业号 corpId + */ + private String corpId; + /** + * 微信企业号 corpSecret + */ + private String corpSecret; + /** + * 微信企业号应用 token + */ + private String token; + /** + * 微信企业号应用 ID + */ + private Integer agentId; + /** + * 微信企业号应用 EncodingAESKey + */ + private String aesKey; + /** + * 微信企业号应用 会话存档私钥 + */ + private String msgAuditPriKey; + /** + * 微信企业号应用 会话存档类库路径 + */ + private String msgAuditLibPath; + + /** + * 自定义企业微信服务器baseUrl,用于替换默认的 https://qyapi.weixin.qq.com + * 例如:http://proxy.company.com:8080 + */ + private String baseApiUrl; + + /** + * 配置存储策略,默认内存 + */ + private ConfigStorage configStorage = new ConfigStorage(); + + @Data + @NoArgsConstructor + public static class ConfigStorage implements Serializable { + private static final long serialVersionUID = 4815731027000065434L; + /** + * 存储类型 + */ + private StorageType type = StorageType.memory; + + /** + * 指定key前缀 + */ + private String keyPrefix = "wx:cp"; + + /** + * redis连接配置 + */ + @NestedConfigurationProperty + private WxCpRedisProperties redis = new WxCpRedisProperties(); + + /** + * http代理主机 + */ + private String httpProxyHost; + + /** + * http代理端口 + */ + private Integer httpProxyPort; + + /** + * http代理用户名 + */ + private String httpProxyUsername; + + /** + * http代理密码 + */ + private String httpProxyPassword; + + /** + * http 请求最大重试次数 + *
+     *   {@link me.chanjar.weixin.cp.api.WxCpService#setMaxRetryTimes(int)}
+     *   {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setMaxRetryTimes(int)}
+     * 
+ */ + private int maxRetryTimes = 5; + + /** + * http 请求重试间隔 + *
+     *   {@link me.chanjar.weixin.cp.api.WxCpService#setRetrySleepMillis(int)}
+     *   {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setRetrySleepMillis(int)}
+     * 
+ */ + private int retrySleepMillis = 1000; + } + + public enum StorageType { + /** + * 内存 + */ + memory, + /** + * jedis + */ + jedis, + /** + * redisson + */ + redisson, + /** + * redistemplate + */ + redistemplate + } +} diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpRedisProperties.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpRedisProperties.java new file mode 100644 index 0000000000..63a7fe01e0 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpRedisProperties.java @@ -0,0 +1,46 @@ +package com.binarywang.spring.starter.wxjava.cp.properties; + +import lombok.Data; + +import java.io.Serializable; + +/** + * Redis配置. + * + * @author yl + * created on 2023/04/23 + */ +@Data +public class WxCpRedisProperties implements Serializable { + private static final long serialVersionUID = -5924815351660074401L; + + /** + * 主机地址. + */ + private String host; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/AbstractWxCpConfigStorageConfiguration.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/AbstractWxCpConfigStorageConfiguration.java new file mode 100644 index 0000000000..2b1d8c13c5 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/AbstractWxCpConfigStorageConfiguration.java @@ -0,0 +1,64 @@ +package com.binarywang.spring.starter.wxjava.cp.storage; + +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpProperties; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import org.apache.commons.lang3.StringUtils; + +/** + * WxCpConfigStorage 抽象配置类 + * + * @author yl & Wang_Wong + * created on 2021/12/6 + */ +public abstract class AbstractWxCpConfigStorageConfiguration { + + protected WxCpDefaultConfigImpl config(WxCpDefaultConfigImpl config, WxCpProperties properties) { + String corpId = properties.getCorpId(); + String corpSecret = properties.getCorpSecret(); + Integer agentId = properties.getAgentId(); + String token = properties.getToken(); + String aesKey = properties.getAesKey(); + // 企业微信,私钥,会话存档路径 + String msgAuditPriKey = properties.getMsgAuditPriKey(); + String msgAuditLibPath = properties.getMsgAuditLibPath(); + + config.setCorpId(corpId); + config.setCorpSecret(corpSecret); + config.setAgentId(agentId); + if (StringUtils.isNotBlank(token)) { + config.setToken(token); + } + if (StringUtils.isNotBlank(aesKey)) { + config.setAesKey(aesKey); + } + if (StringUtils.isNotBlank(msgAuditPriKey)) { + config.setMsgAuditPriKey(msgAuditPriKey); + } + if (StringUtils.isNotBlank(msgAuditLibPath)) { + config.setMsgAuditLibPath(msgAuditLibPath); + } + if (StringUtils.isNotBlank(properties.getBaseApiUrl())) { + config.setBaseApiUrl(properties.getBaseApiUrl()); + } + + WxCpProperties.ConfigStorage storage = properties.getConfigStorage(); + String httpProxyHost = storage.getHttpProxyHost(); + Integer httpProxyPort = storage.getHttpProxyPort(); + String httpProxyUsername = storage.getHttpProxyUsername(); + String httpProxyPassword = storage.getHttpProxyPassword(); + if (StringUtils.isNotBlank(httpProxyHost)) { + config.setHttpProxyHost(httpProxyHost); + if (httpProxyPort != null) { + config.setHttpProxyPort(httpProxyPort); + } + if (StringUtils.isNotBlank(httpProxyUsername)) { + config.setHttpProxyUsername(httpProxyUsername); + } + if (StringUtils.isNotBlank(httpProxyPassword)) { + config.setHttpProxyPassword(httpProxyPassword); + } + } + return config; + } + +} diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInJedisConfigStorageConfiguration.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInJedisConfigStorageConfiguration.java new file mode 100644 index 0000000000..246971baed --- /dev/null +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInJedisConfigStorageConfiguration.java @@ -0,0 +1,74 @@ +package com.binarywang.spring.starter.wxjava.cp.storage; + +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpProperties; +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpRedisProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import me.chanjar.weixin.cp.config.impl.WxCpJedisConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * 自动装配基于 jedis 策略配置 + * + * @author yl + * created on 2023/04/23 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxCpProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis" +) +@RequiredArgsConstructor +public class WxCpInJedisConfigStorageConfiguration extends AbstractWxCpConfigStorageConfiguration { + private final WxCpProperties wxCpProperties; + private final ApplicationContext applicationContext; + + @Bean + @ConditionalOnMissingBean(WxCpConfigStorage.class) + public WxCpConfigStorage wxCpConfigStorage() { + WxCpDefaultConfigImpl config = getConfigStorage(); + return this.config(config, wxCpProperties); + } + + private WxCpJedisConfigImpl getConfigStorage() { + WxCpRedisProperties wxCpRedisProperties = wxCpProperties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (wxCpRedisProperties != null && StringUtils.isNotEmpty(wxCpRedisProperties.getHost())) { + jedisPool = getJedisPool(); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + return new WxCpJedisConfigImpl(jedisPool, wxCpProperties.getConfigStorage().getKeyPrefix()); + } + + private JedisPool getJedisPool() { + WxCpProperties.ConfigStorage storage = wxCpProperties.getConfigStorage(); + WxCpRedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), + redis.getTimeout(), redis.getPassword(), redis.getDatabase()); + } +} diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInMemoryConfigStorageConfiguration.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInMemoryConfigStorageConfiguration.java new file mode 100644 index 0000000000..3722bd07d1 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInMemoryConfigStorageConfiguration.java @@ -0,0 +1,33 @@ +package com.binarywang.spring.starter.wxjava.cp.storage; + +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 自动装配基于内存策略配置 + * + * @author yl + * created on 2021/12/6 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxCpProperties.PREFIX + ".config-storage", name = "type", + matchIfMissing = true, havingValue = "memory" +) +@RequiredArgsConstructor +public class WxCpInMemoryConfigStorageConfiguration extends AbstractWxCpConfigStorageConfiguration { + private final WxCpProperties wxCpProperties; + + @Bean + @ConditionalOnMissingBean(WxCpConfigStorage.class) + public WxCpConfigStorage wxCpConfigStorage() { + WxCpDefaultConfigImpl config = new WxCpDefaultConfigImpl(); + return this.config(config, wxCpProperties); + } +} diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInRedisTemplateConfigStorageConfiguration.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInRedisTemplateConfigStorageConfiguration.java new file mode 100644 index 0000000000..879568b16a --- /dev/null +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInRedisTemplateConfigStorageConfiguration.java @@ -0,0 +1,41 @@ +package com.binarywang.spring.starter.wxjava.cp.storage; + +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import me.chanjar.weixin.cp.config.impl.WxCpRedisTemplateConfigImpl; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * 自动装配基于 redisTemplate 策略配置 + * + * @author yl + * created on 2023/04/23 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxCpProperties.PREFIX + ".config-storage", name = "type", havingValue = "redistemplate" +) +@RequiredArgsConstructor +public class WxCpInRedisTemplateConfigStorageConfiguration extends AbstractWxCpConfigStorageConfiguration { + private final WxCpProperties wxCpProperties; + private final ApplicationContext applicationContext; + + @Bean + @ConditionalOnMissingBean(WxCpConfigStorage.class) + public WxCpConfigStorage wxCpConfigStorage() { + WxCpDefaultConfigImpl config = getConfigStorage(); + return this.config(config, wxCpProperties); + } + + private WxCpRedisTemplateConfigImpl getConfigStorage() { + StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class); + return new WxCpRedisTemplateConfigImpl(redisTemplate, wxCpProperties.getConfigStorage().getKeyPrefix()); + } +} diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInRedissonConfigStorageConfiguration.java b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInRedissonConfigStorageConfiguration.java new file mode 100644 index 0000000000..060b894fd1 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/storage/WxCpInRedissonConfigStorageConfiguration.java @@ -0,0 +1,65 @@ +package com.binarywang.spring.starter.wxjava.cp.storage; + +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpProperties; +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpRedisProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import me.chanjar.weixin.cp.config.impl.WxCpRedissonConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 自动装配基于 redisson 策略配置 + * + * @author yl + * created on 2023/04/23 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxCpProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson" +) +@RequiredArgsConstructor +public class WxCpInRedissonConfigStorageConfiguration extends AbstractWxCpConfigStorageConfiguration { + private final WxCpProperties wxCpProperties; + private final ApplicationContext applicationContext; + + @Bean + @ConditionalOnMissingBean(WxCpConfigStorage.class) + public WxCpConfigStorage wxCpConfigStorage() { + WxCpDefaultConfigImpl config = getConfigStorage(); + return this.config(config, wxCpProperties); + } + + private WxCpRedissonConfigImpl getConfigStorage() { + WxCpRedisProperties redisProperties = wxCpProperties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + redissonClient = getRedissonClient(); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxCpRedissonConfigImpl(redissonClient, wxCpProperties.getConfigStorage().getKeyPrefix()); + } + + private RedissonClient getRedissonClient() { + WxCpProperties.ConfigStorage storage = wxCpProperties.getConfigStorage(); + WxCpRedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redis.getHost() + ":" + redis.getPort()) + .setDatabase(redis.getDatabase()) + .setPassword(redis.getPassword()); + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..c2ef7f6354 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.binarywang.spring.starter.wxjava.cp.config.WxCpAutoConfiguration diff --git a/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..0beff3f862 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.binarywang.spring.starter.wxjava.cp.config.WxCpAutoConfiguration diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/README.md b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/README.md new file mode 100644 index 0000000000..624c6b3150 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/README.md @@ -0,0 +1,97 @@ +# wx-java-cp-multi-spring-boot-starter + +企业微信多账号配置 + +- 实现多 WxCpService 初始化。 +- 未实现 WxCpTpService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。 +- 未实现 WxCpCgService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。 + +## 快速开始 + +1. 引入依赖 + ```xml + + com.github.binarywang + wx-java-cp-multi-spring-boot-starter + ${version} + + ``` +2. 添加配置(application.properties) + ```properties + # 应用 1 配置 + wx.cp.corps.tenantId1.corp-id = @corp-id + wx.cp.corps.tenantId1.corp-secret = @corp-secret + ## 选填 + wx.cp.corps.tenantId1.agent-id = @agent-id + wx.cp.corps.tenantId1.token = @token + wx.cp.corps.tenantId1.aes-key = @aes-key + wx.cp.corps.tenantId1.msg-audit-priKey = @msg-audit-priKey + wx.cp.corps.tenantId1.msg-audit-lib-path = @msg-audit-lib-path + + # 应用 2 配置 + wx.cp.corps.tenantId2.corp-id = @corp-id + wx.cp.corps.tenantId2.corp-secret = @corp-secret + ## 选填 + wx.cp.corps.tenantId2.agent-id = @agent-id + wx.cp.corps.tenantId2.token = @token + wx.cp.corps.tenantId2.aes-key = @aes-key + wx.cp.corps.tenantId2.msg-audit-priKey = @msg-audit-priKey + wx.cp.corps.tenantId2.msg-audit-lib-path = @msg-audit-lib-path + + # 公共配置 + ## ConfigStorage 配置(选填) + wx.cp.config-storage.type=memory # 配置类型: memory(默认), jedis, redisson, redistemplate + ## http 客户端配置(选填) + ## # http客户端类型: http_client(默认), ok_http, jodd_http + wx.cp.config-storage.http-client-type=http_client + wx.cp.config-storage.http-proxy-host= + wx.cp.config-storage.http-proxy-port= + wx.cp.config-storage.http-proxy-username= + wx.cp.config-storage.http-proxy-password= + ## 最大重试次数,默认:5 次,如果小于 0,则为 0 + wx.cp.config-storage.max-retry-times=5 + ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000 + wx.cp.config-storage.retry-sleep-millis=1000 + ``` +3. 支持自动注入的类型: `WxCpMultiServices` + +4. 使用样例 + +```java +import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServices; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.api.WxCpUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class DemoService { + @Autowired + private WxCpTpMultiServices wxCpTpMultiServices; + + public void test() { + // 应用 1 的 WxCpService + WxCpService wxCpService1 = wxCpMultiServices.getWxCpService("tenantId1"); + WxCpUserService userService1 = wxCpService1.getUserService(); + userService1.getUserId("xxx"); + // todo ... + + // 应用 2 的 WxCpService + WxCpService wxCpService2 = wxCpMultiServices.getWxCpService("tenantId2"); + WxCpUserService userService2 = wxCpService2.getUserService(); + userService2.getUserId("xxx"); + // todo ... + + // 应用 3 的 WxCpService + WxCpService wxCpService3 = wxCpMultiServices.getWxCpService("tenantId3"); + // 判断是否为空 + if (wxCpService3 == null) { + // todo wxCpService3 为空,请先配置 tenantId3 企业微信应用参数 + return; + } + WxCpUserService userService3 = wxCpService3.getUserService(); + userService3.getUserId("xxx"); + // todo ... + } +} +``` diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/pom.xml new file mode 100644 index 0000000000..b3bd632cad --- /dev/null +++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/pom.xml @@ -0,0 +1,60 @@ + + + + wx-java-spring-boot-starters + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-cp-tp-multi-spring-boot-starter + WxJava - Spring Boot Starter for WxCp::支持多账号配置 + 微信企业号开发的 Spring Boot Starter::支持多账号配置 + + + + com.github.binarywang + weixin-java-cp + ${project.version} + + + redis.clients + jedis + provided + + + org.redisson + redisson + provided + + + org.springframework.data + spring-data-redis + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + + diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/autoconfigure/WxCpTpMultiAutoConfiguration.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/autoconfigure/WxCpTpMultiAutoConfiguration.java new file mode 100644 index 0000000000..1ec07c5c5b --- /dev/null +++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/autoconfigure/WxCpTpMultiAutoConfiguration.java @@ -0,0 +1,16 @@ +package com.binarywang.spring.starter.wxjava.cp.autoconfigure; + +import com.binarywang.spring.starter.wxjava.cp.configuration.WxCpTpMultiServicesAutoConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * 企业微信自动注册 + * + * @author yl + * created on 2023/10/16 + */ +@Configuration +@Import(WxCpTpMultiServicesAutoConfiguration.class) +public class WxCpTpMultiAutoConfiguration { +} diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/WxCpTpMultiServicesAutoConfiguration.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/WxCpTpMultiServicesAutoConfiguration.java new file mode 100644 index 0000000000..1f6e784236 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/WxCpTpMultiServicesAutoConfiguration.java @@ -0,0 +1,27 @@ +package com.binarywang.spring.starter.wxjava.cp.configuration; + +import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpTpInJedisTpConfiguration; +import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpTpInMemoryTpConfiguration; +import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpTpInRedisTemplateTpConfiguration; +import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpTpInRedissonTpConfiguration; +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * 企业微信平台相关服务自动注册 + * + * @author yl + * created on 2023/10/16 + */ +@Configuration +@EnableConfigurationProperties(WxCpTpMultiProperties.class) +@Import({ + WxCpTpInJedisTpConfiguration.class, + WxCpTpInMemoryTpConfiguration.class, + WxCpTpInRedissonTpConfiguration.class, + WxCpTpInRedisTemplateTpConfiguration.class +}) +public class WxCpTpMultiServicesAutoConfiguration { +} diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpTpConfiguration.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpTpConfiguration.java new file mode 100644 index 0000000000..2404dee068 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/AbstractWxCpTpConfiguration.java @@ -0,0 +1,139 @@ +package com.binarywang.spring.starter.wxjava.cp.configuration.services; + +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiProperties; +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpSingleProperties; +import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServices; +import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServicesImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; +import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl; +import me.chanjar.weixin.cp.tp.service.WxCpTpService; +import me.chanjar.weixin.cp.tp.service.impl.WxCpTpServiceApacheHttpClientImpl; +import me.chanjar.weixin.cp.tp.service.impl.WxCpTpServiceImpl; +import me.chanjar.weixin.cp.tp.service.impl.WxCpTpServiceJoddHttpImpl; +import me.chanjar.weixin.cp.tp.service.impl.WxCpTpServiceOkHttpImpl; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * WxCpConfigStorage 抽象配置类 + * + * @author yl + * created on 2023/10/16 + */ +@RequiredArgsConstructor +@Slf4j +public abstract class AbstractWxCpTpConfiguration { + + /** + * + * @param wxCpTpMultiProperties 应用列表配置 + * @param services 用于支持,应用启动之后,可以调用这个接口添加新服务对象。主要是配置是从数据库中读取的 + * @return + */ + public WxCpTpMultiServices wxCpMultiServices(WxCpTpMultiProperties wxCpTpMultiProperties,WxCpTpMultiServices services) { + Map corps = wxCpTpMultiProperties.getCorps(); + if (corps == null || corps.isEmpty()) { + log.warn("企业微信应用参数未配置,通过 WxCpMultiServices#getWxCpTpService(\"tenantId\")获取实例将返回空"); + return new WxCpTpMultiServicesImpl(); + } + + if (services == null) { + services = new WxCpTpMultiServicesImpl(); + } + + Set> entries = corps.entrySet(); + for (Map.Entry entry : entries) { + String tenantId = entry.getKey(); + WxCpTpSingleProperties wxCpTpSingleProperties = entry.getValue(); + WxCpTpDefaultConfigImpl storage = this.wxCpTpConfigStorage(wxCpTpMultiProperties); + this.configCorp(storage, wxCpTpSingleProperties); + this.configHttp(storage, wxCpTpMultiProperties.getConfigStorage()); + WxCpTpService wxCpTpService = this.wxCpTpService(storage, wxCpTpMultiProperties.getConfigStorage()); + if (services.getWxCpTpService(tenantId) == null) { + // 不存在的才会添加到服务列表中 + services.addWxCpTpService(tenantId, wxCpTpService); + } + } + return services; + } + + /** + * 配置 WxCpDefaultConfigImpl + * + * @param wxCpTpMultiProperties 参数 + * @return WxCpDefaultConfigImpl + */ + protected abstract WxCpTpDefaultConfigImpl wxCpTpConfigStorage(WxCpTpMultiProperties wxCpTpMultiProperties); + + private WxCpTpService wxCpTpService(WxCpTpConfigStorage wxCpTpConfigStorage, WxCpTpMultiProperties.ConfigStorage storage) { + WxCpTpMultiProperties.HttpClientType httpClientType = storage.getHttpClientType(); + WxCpTpService cpTpService; + switch (httpClientType) { + case OK_HTTP: + cpTpService = new WxCpTpServiceOkHttpImpl(); + break; + case JODD_HTTP: + cpTpService = new WxCpTpServiceJoddHttpImpl(); + break; + case HTTP_CLIENT: + cpTpService = new WxCpTpServiceApacheHttpClientImpl(); + break; + default: + cpTpService = new WxCpTpServiceImpl(); + break; + } + cpTpService.setWxCpTpConfigStorage(wxCpTpConfigStorage); + int maxRetryTimes = storage.getMaxRetryTimes(); + if (maxRetryTimes < 0) { + maxRetryTimes = 0; + } + int retrySleepMillis = storage.getRetrySleepMillis(); + if (retrySleepMillis < 0) { + retrySleepMillis = 1000; + } + cpTpService.setRetrySleepMillis(retrySleepMillis); + cpTpService.setMaxRetryTimes(maxRetryTimes); + return cpTpService; + } + + private void configCorp(WxCpTpDefaultConfigImpl config, WxCpTpSingleProperties wxCpTpSingleProperties) { + String corpId = wxCpTpSingleProperties.getCorpId(); + String providerSecret = wxCpTpSingleProperties.getProviderSecret(); + String suiteId = wxCpTpSingleProperties.getSuiteId(); + String token = wxCpTpSingleProperties.getToken(); + String suiteSecret = wxCpTpSingleProperties.getSuiteSecret(); + // 企业微信,私钥,会话存档路径 + config.setCorpId(corpId); + config.setProviderSecret(providerSecret); + config.setEncodingAESKey(wxCpTpSingleProperties.getEncodingAESKey()); + config.setSuiteId(suiteId); + config.setToken(token); + config.setSuiteSecret(suiteSecret); + } + + private void configHttp(WxCpTpDefaultConfigImpl config, WxCpTpMultiProperties.ConfigStorage storage) { + String httpProxyHost = storage.getHttpProxyHost(); + Integer httpProxyPort = storage.getHttpProxyPort(); + String httpProxyUsername = storage.getHttpProxyUsername(); + String httpProxyPassword = storage.getHttpProxyPassword(); + if (StringUtils.isNotBlank(httpProxyHost)) { + config.setHttpProxyHost(httpProxyHost); + if (httpProxyPort != null) { + config.setHttpProxyPort(httpProxyPort); + } + if (StringUtils.isNotBlank(httpProxyUsername)) { + config.setHttpProxyUsername(httpProxyUsername); + } + if (StringUtils.isNotBlank(httpProxyPassword)) { + config.setHttpProxyPassword(httpProxyPassword); + } + } + } +} diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInJedisTpConfiguration.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInJedisTpConfiguration.java new file mode 100644 index 0000000000..f3034ac007 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInJedisTpConfiguration.java @@ -0,0 +1,78 @@ +package com.binarywang.spring.starter.wxjava.cp.configuration.services; + +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiProperties; +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiRedisProperties; +import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import me.chanjar.weixin.cp.config.impl.WxCpJedisConfigImpl; +import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl; +import me.chanjar.weixin.cp.config.impl.WxCpTpJedisConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * 自动装配基于 jedis 策略配置 + * + * @author yl + * created on 2023/10/16 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxCpTpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis" +) +@RequiredArgsConstructor +public class WxCpTpInJedisTpConfiguration extends AbstractWxCpTpConfiguration { + private final WxCpTpMultiProperties wxCpTpMultiProperties; + private final ApplicationContext applicationContext; + + @Bean + public WxCpTpMultiServices wxCpMultiServices() { + return this.wxCpMultiServices(wxCpTpMultiProperties,null); + } + + @Override + protected WxCpTpDefaultConfigImpl wxCpTpConfigStorage(WxCpTpMultiProperties wxCpTpMultiProperties) { + return this.configRedis(wxCpTpMultiProperties); + } + + private WxCpTpDefaultConfigImpl configRedis(WxCpTpMultiProperties wxCpTpMultiProperties) { + WxCpTpMultiRedisProperties wxCpTpMultiRedisProperties = wxCpTpMultiProperties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (wxCpTpMultiRedisProperties != null && StringUtils.isNotEmpty(wxCpTpMultiRedisProperties.getHost())) { + jedisPool = getJedisPool(wxCpTpMultiProperties); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + return new WxCpTpJedisConfigImpl(jedisPool, wxCpTpMultiProperties.getConfigStorage().getKeyPrefix()); + } + + private JedisPool getJedisPool(WxCpTpMultiProperties wxCpTpMultiProperties) { + WxCpTpMultiProperties.ConfigStorage storage = wxCpTpMultiProperties.getConfigStorage(); + WxCpTpMultiRedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), + redis.getTimeout(), redis.getPassword(), redis.getDatabase()); + } +} diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInMemoryTpConfiguration.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInMemoryTpConfiguration.java new file mode 100644 index 0000000000..5e460abb26 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInMemoryTpConfiguration.java @@ -0,0 +1,39 @@ +package com.binarywang.spring.starter.wxjava.cp.configuration.services; + +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiProperties; +import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 自动装配基于内存策略配置 + * + * @author yl + * created on 2023/10/16 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxCpTpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "memory", matchIfMissing = true +) +@RequiredArgsConstructor +public class WxCpTpInMemoryTpConfiguration extends AbstractWxCpTpConfiguration { + private final WxCpTpMultiProperties wxCpTpMultiProperties; + + @Bean + public WxCpTpMultiServices wxCpMultiServices() { + return this.wxCpMultiServices(wxCpTpMultiProperties,null); + } + + @Override + protected WxCpTpDefaultConfigImpl wxCpTpConfigStorage(WxCpTpMultiProperties wxCpTpMultiProperties) { + return this.configInMemory(); + } + + private WxCpTpDefaultConfigImpl configInMemory() { + return new WxCpTpDefaultConfigImpl(); + } +} diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInRedisTemplateTpConfiguration.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInRedisTemplateTpConfiguration.java new file mode 100644 index 0000000000..1faa37862c --- /dev/null +++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInRedisTemplateTpConfiguration.java @@ -0,0 +1,45 @@ +package com.binarywang.spring.starter.wxjava.cp.configuration.services; + +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiProperties; +import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl; +import me.chanjar.weixin.cp.config.impl.WxCpRedisTemplateConfigImpl; +import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl; +import me.chanjar.weixin.cp.config.impl.WxCpTpRedisTemplateConfigImpl; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * 自动装配基于 redisTemplate 策略配置 + * + * @author yl + * created on 2023/10/16 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxCpTpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redistemplate" +) +@RequiredArgsConstructor +public class WxCpTpInRedisTemplateTpConfiguration extends AbstractWxCpTpConfiguration { + private final WxCpTpMultiProperties wxCpTpMultiProperties; + private final ApplicationContext applicationContext; + + @Bean + public WxCpTpMultiServices wxCpMultiServices() { + return this.wxCpMultiServices(wxCpTpMultiProperties,null); + } + + @Override + protected WxCpTpDefaultConfigImpl wxCpTpConfigStorage(WxCpTpMultiProperties wxCpTpMultiProperties) { + return this.configRedisTemplate(wxCpTpMultiProperties); + } + + private WxCpTpDefaultConfigImpl configRedisTemplate(WxCpTpMultiProperties wxCpTpMultiProperties) { + StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class); + return new WxCpTpRedisTemplateConfigImpl(redisTemplate, wxCpTpMultiProperties.getConfigStorage().getKeyPrefix()); + } +} diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInRedissonTpConfiguration.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInRedissonTpConfiguration.java new file mode 100644 index 0000000000..bd16db37ea --- /dev/null +++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/configuration/services/WxCpTpInRedissonTpConfiguration.java @@ -0,0 +1,68 @@ +package com.binarywang.spring.starter.wxjava.cp.configuration.services; + +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiProperties; +import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiRedisProperties; +import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl; +import me.chanjar.weixin.cp.config.impl.AbstractWxCpTpInRedisConfigImpl; +import me.chanjar.weixin.cp.config.impl.WxCpTpRedissonConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 自动装配基于 redisson 策略配置 + * + * @author yl + * created on 2023/10/16 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxCpTpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson" +) +@RequiredArgsConstructor +public class WxCpTpInRedissonTpConfiguration extends AbstractWxCpTpConfiguration { + private final WxCpTpMultiProperties wxCpTpMultiProperties; + private final ApplicationContext applicationContext; + + @Bean + public WxCpTpMultiServices wxCpMultiServices() { + return this.wxCpMultiServices(wxCpTpMultiProperties,null); + } + + @Override + protected WxCpTpDefaultConfigImpl wxCpTpConfigStorage(WxCpTpMultiProperties wxCpTpMultiProperties) { + return this.configRedisson(wxCpTpMultiProperties); + } + + private WxCpTpDefaultConfigImpl configRedisson(WxCpTpMultiProperties wxCpTpMultiProperties) { + WxCpTpMultiRedisProperties redisProperties = wxCpTpMultiProperties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + redissonClient = getRedissonClient(wxCpTpMultiProperties); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxCpTpRedissonConfigImpl(redissonClient, wxCpTpMultiProperties.getConfigStorage().getKeyPrefix()); + } + + private RedissonClient getRedissonClient(WxCpTpMultiProperties wxCpTpMultiProperties) { + WxCpTpMultiProperties.ConfigStorage storage = wxCpTpMultiProperties.getConfigStorage(); + WxCpTpMultiRedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redis.getHost() + ":" + redis.getPort()) + .setDatabase(redis.getDatabase()) + .setPassword(redis.getPassword()); + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpTpMultiProperties.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpTpMultiProperties.java new file mode 100644 index 0000000000..771b1b6de7 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpTpMultiProperties.java @@ -0,0 +1,129 @@ +package com.binarywang.spring.starter.wxjava.cp.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * 企业微信多企业接入相关配置属性 + * + * @author yl + * created on 2023/10/16 + */ +@Data +@NoArgsConstructor +@ConfigurationProperties(prefix = WxCpTpMultiProperties.PREFIX) +public class WxCpTpMultiProperties implements Serializable { + private static final long serialVersionUID = -1569510477055668503L; + public static final String PREFIX = "wx.cp.tp"; + + private Map corps = new HashMap<>(); + + /** + * 配置存储策略,默认内存 + */ + private ConfigStorage configStorage = new ConfigStorage(); + + @Data + @NoArgsConstructor + public static class ConfigStorage implements Serializable { + private static final long serialVersionUID = 4815731027000065434L; + /** + * 存储类型 + */ + private StorageType type = StorageType.memory; + + /** + * 指定key前缀 + */ + private String keyPrefix = "wx:cp:tp"; + + /** + * redis连接配置 + */ + @NestedConfigurationProperty + private WxCpTpMultiRedisProperties redis = new WxCpTpMultiRedisProperties(); + + /** + * http客户端类型. + */ + private HttpClientType httpClientType = HttpClientType.HTTP_CLIENT; + + /** + * http代理主机 + */ + private String httpProxyHost; + + /** + * http代理端口 + */ + private Integer httpProxyPort; + + /** + * http代理用户名 + */ + private String httpProxyUsername; + + /** + * http代理密码 + */ + private String httpProxyPassword; + + /** + * http 请求最大重试次数 + *
+     *   {@link me.chanjar.weixin.cp.api.WxCpService#setMaxRetryTimes(int)}
+     *   {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setMaxRetryTimes(int)}
+     * 
+ */ + private int maxRetryTimes = 5; + + /** + * http 请求重试间隔 + *
+     *   {@link me.chanjar.weixin.cp.api.WxCpService#setRetrySleepMillis(int)}
+     *   {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setRetrySleepMillis(int)}
+     * 
+ */ + private int retrySleepMillis = 1000; + } + + public enum StorageType { + /** + * 内存 + */ + memory, + /** + * jedis + */ + jedis, + /** + * redisson + */ + redisson, + /** + * redistemplate + */ + redistemplate + } + + public enum HttpClientType { + /** + * HttpClient + */ + HTTP_CLIENT, + /** + * OkHttp + */ + OK_HTTP, + /** + * JoddHttp + */ + JODD_HTTP + } +} diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpTpMultiRedisProperties.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpTpMultiRedisProperties.java new file mode 100644 index 0000000000..b94711216f --- /dev/null +++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpTpMultiRedisProperties.java @@ -0,0 +1,48 @@ +package com.binarywang.spring.starter.wxjava.cp.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * Redis配置. + * + * @author yl + * created on 2023/10/16 + */ +@Data +@NoArgsConstructor +public class WxCpTpMultiRedisProperties implements Serializable { + private static final long serialVersionUID = -5924815351660074401L; + + /** + * 主机地址. + */ + private String host; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpTpSingleProperties.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpTpSingleProperties.java new file mode 100644 index 0000000000..02a52657db --- /dev/null +++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/properties/WxCpTpSingleProperties.java @@ -0,0 +1,43 @@ +package com.binarywang.spring.starter.wxjava.cp.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 企业微信企业相关配置属性 + * + * @author yl + * created on 2023/10/16 + */ +@Data +@NoArgsConstructor +public class WxCpTpSingleProperties implements Serializable { + private static final long serialVersionUID = -7502823825007859418L; + /** + * 微信企业号 corpId + */ + private String corpId; + /** + * 微信企业号 服务商 providerSecret + */ + private String providerSecret; + /** + * 微信企业号应用 token + */ + private String token; + + private String encodingAESKey; + + /** + * 微信企业号 第三方 应用 ID + */ + private String suiteId; + /** + * 微信企业号应用 + */ + private String suiteSecret; + + +} diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpTpMultiServices.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpTpMultiServices.java new file mode 100644 index 0000000000..c0a9faf51e --- /dev/null +++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpTpMultiServices.java @@ -0,0 +1,29 @@ +package com.binarywang.spring.starter.wxjava.cp.service; + + +import me.chanjar.weixin.cp.tp.service.WxCpTpService; + +/** + * 企业微信 {@link WxCpTpService} 所有实例存放类. + * + * @author yl + * created on 2023/10/16 + */ +public interface WxCpTpMultiServices { + /** + * 通过租户 Id 获取 WxCpTpService + * + * @param tenantId 租户 Id + * @return WxCpTpService + */ + WxCpTpService getWxCpTpService(String tenantId); + + void addWxCpTpService(String tenantId, WxCpTpService wxCpService); + + /** + * 根据租户 Id,从列表中移除一个 WxCpTpService 实例 + * + * @param tenantId 租户 Id + */ + void removeWxCpTpService(String tenantId); +} diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpTpMultiServicesImpl.java b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpTpMultiServicesImpl.java new file mode 100644 index 0000000000..84b381230c --- /dev/null +++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/cp/service/WxCpTpMultiServicesImpl.java @@ -0,0 +1,44 @@ +package com.binarywang.spring.starter.wxjava.cp.service; + + +import me.chanjar.weixin.cp.tp.service.WxCpTpService; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 企业微信 {@link WxCpTpMultiServices} 默认实现 + * + * @author yl + * created on 2023/10/16 + */ +public class WxCpTpMultiServicesImpl implements WxCpTpMultiServices { + private final Map services = new ConcurrentHashMap<>(); + + /** + * 通过租户 Id 获取 WxCpTpService + * + * @param tenantId 租户 Id + * @return WxCpTpService + */ + @Override + public WxCpTpService getWxCpTpService(String tenantId) { + return this.services.get(tenantId); + } + + /** + * 根据租户 Id,添加一个 WxCpTpService 到列表 + * + * @param tenantId 租户 Id + * @param wxCpService WxCpTpService 实例 + */ + @Override + public void addWxCpTpService(String tenantId, WxCpTpService wxCpService) { + this.services.put(tenantId, wxCpService); + } + + @Override + public void removeWxCpTpService(String tenantId) { + this.services.remove(tenantId); + } +} diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..9d11107229 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.binarywang.spring.starter.wxjava.cp.autoconfigure.WxCpTpMultiAutoConfiguration diff --git a/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..5de0e9f139 --- /dev/null +++ b/spring-boot-starters/wx-java-cp-tp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.binarywang.spring.starter.wxjava.cp.autoconfigure.WxCpTpMultiAutoConfiguration diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md new file mode 100644 index 0000000000..6dd1d110c3 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/MULTI_TENANT_MODE.md @@ -0,0 +1,205 @@ +# 微信小程序多租户配置说明 + +## 多租户模式对比 + +从 4.8.0 版本开始,wx-java-miniapp-multi-spring-boot-starter 支持两种多租户实现模式: + +### 1. 隔离模式(ISOLATED,默认) + +每个租户创建独立的 `WxMaService` 实例,各自拥有独立的 HTTP 客户端。 + +**优点:** +- 线程安全,无需担心并发问题 +- 不依赖 ThreadLocal,适合异步/响应式编程 +- 租户间完全隔离,互不影响 + +**缺点:** +- 每个租户创建独立的 HTTP 客户端,资源占用较多 +- 适合租户数量不多的场景(建议 < 50 个租户) + +**适用场景:** +- SaaS 应用,租户数量较少 +- 异步编程、响应式编程场景 +- 对线程安全有严格要求 + +### 2. 共享模式(SHARED) + +使用单个 `WxMaService` 实例管理所有租户配置,所有租户共享同一个 HTTP 客户端。 + +**优点:** +- 共享 HTTP 客户端,大幅节省资源 +- 适合租户数量较多的场景(支持 100+ 租户) +- 内存占用更小 + +**缺点:** +- 依赖 ThreadLocal 切换配置,在异步场景需要特别注意 +- 需要注意线程上下文传递 + +**适用场景:** +- 租户数量较多(> 50 个) +- 同步编程场景 +- 对资源占用有严格要求 + +## 配置方式 + +### 使用隔离模式(默认) + +```yaml +wx: + ma: + # 多租户配置 + apps: + tenant1: + app-id: wxd898fcb01713c555 + app-secret: 47a2422a5d04a27e2b3ed1f1f0b0dbad + token: aBcDeFg123456 + aes-key: abcdefgh123456abcdefgh123456abc + tenant2: + app-id: wx1234567890abcdef + app-secret: 1234567890abcdef1234567890abcdef + token: token123 + aes-key: aeskey123aeskey123aeskey123aes + + # 配置存储(可选) + config-storage: + type: memory # memory, jedis, redisson, redis_template + http-client-type: http_client # http_client, ok_http, jodd_http + # multi-tenant-mode: isolated # 默认值,可以不配置 +``` + +### 使用共享模式 + +```yaml +wx: + ma: + # 多租户配置 + apps: + tenant1: + app-id: wxd898fcb01713c555 + app-secret: 47a2422a5d04a27e2b3ed1f1f0b0dbad + tenant2: + app-id: wx1234567890abcdef + app-secret: 1234567890abcdef1234567890abcdef + # ... 可配置更多租户 + + # 配置存储 + config-storage: + type: memory + http-client-type: http_client + multi-tenant-mode: shared # 启用共享模式 +``` + +## 代码使用 + +两种模式下的代码使用方式**完全相同**: + +```java +@RestController +@RequestMapping("/ma") +public class MiniAppController { + + @Autowired + private WxMaMultiServices wxMaMultiServices; + + @GetMapping("/userInfo/{tenantId}") + public String getUserInfo(@PathVariable String tenantId, @RequestParam String code) { + // 获取指定租户的 WxMaService + WxMaService wxMaService = wxMaMultiServices.getWxMaService(tenantId); + + try { + WxMaJscode2SessionResult session = wxMaService.jsCode2SessionInfo(code); + return "OpenId: " + session.getOpenid(); + } catch (WxErrorException e) { + return "错误: " + e.getMessage(); + } + } +} +``` + +## 性能对比 + +以 100 个租户为例: + +| 指标 | 隔离模式 | 共享模式 | +|------|---------|---------| +| HTTP 客户端数量 | 100 个 | 1 个 | +| 内存占用(估算) | ~500MB | ~50MB | +| 线程安全 | ✅ 完全安全 | ⚠️ 需注意异步场景 | +| 性能 | 略高(无 ThreadLocal 切换) | 略低(有 ThreadLocal 切换) | +| 适用场景 | 中小规模 | 大规模 | + +## 注意事项 + +### 共享模式下的异步编程 + +如果使用共享模式,在异步编程时需要注意 ThreadLocal 的传递: + +```java +@Service +public class MiniAppService { + + @Autowired + private WxMaMultiServices wxMaMultiServices; + + public void asyncOperation(String tenantId) { + WxMaService wxMaService = wxMaMultiServices.getWxMaService(tenantId); + + // ❌ 错误:异步线程无法获取到正确的配置 + CompletableFuture.runAsync(() -> { + // 这里 wxMaService.getWxMaConfig() 可能返回错误的配置 + wxMaService.getUserService().getUserInfo(...); + }); + + // ✅ 正确:在主线程获取配置,传递给异步线程 + WxMaConfig config = wxMaService.getWxMaConfig(); + String appId = config.getAppid(); + CompletableFuture.runAsync(() -> { + // 使用已获取的配置信息 + log.info("AppId: {}", appId); + }); + } +} +``` + +### 动态添加/删除租户 + +两种模式都支持运行时动态添加或删除租户配置。 + +## 迁移指南 + +如果您正在使用旧版本,升级到 4.8.0+ 后: + +1. **默认行为不变**:如果不配置 `multi-tenant-mode`,将继续使用隔离模式(与旧版本行为一致) +2. **向后兼容**:所有现有代码无需修改 +3. **可选升级**:如需节省资源,可配置 `multi-tenant-mode: shared` 启用共享模式 + +## 源码分析 + +issue讨论地址:[#3835](https://github.com/binarywang/WxJava/issues/3835) + +### 为什么有两种设计? + +1. **基础实现类的 `configMap`**: + - 位置:`BaseWxMaServiceImpl` + - 特点:单个 Service 实例 + 多个配置 + ThreadLocal 切换 + - 设计目的:支持在一个应用中管理多个小程序账号 + +2. **Spring Boot Starter 的 `services` Map**: + - 位置:`WxMaMultiServicesImpl` + - 特点:多个 Service 实例 + 每个实例一个配置 + - 设计目的:为 Spring Boot 提供更符合依赖注入风格的多租户支持 + +### 新版本改进 + +新版本通过配置项让用户自主选择实现方式: + +``` +用户 → WxMaMultiServices 接口 + ↓ + ┌────┴────┐ + ↓ ↓ +隔离模式 共享模式 +(多Service) (单Service+configMap) +``` + +这样既保留了线程安全的优势(隔离模式),又提供了资源节省的选项(共享模式)。 diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/README.md b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/README.md new file mode 100644 index 0000000000..ccc0d5bf5f --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/README.md @@ -0,0 +1,96 @@ +# wx-java-miniapp-multi-spring-boot-starter + +## 快速开始 + +1. 引入依赖 + ```xml + + com.github.binarywang + wx-java-miniapp-multi-spring-boot-starter + ${version} + + ``` +2. 添加配置(application.properties) + ```properties + # 公众号配置 + ## 应用 1 配置(必填) + wx.ma.apps.tenantId1.app-id=appId + wx.ma.apps.tenantId1.app-secret=@secret + ## 选填 + wx.ma.apps.tenantId1.token=@token + wx.ma.apps.tenantId1.aes-key=@aesKey + wx.ma.apps.tenantId1.use-stable-access-token=@useStableAccessToken + ## 应用 2 配置(必填) + wx.ma.apps.tenantId2.app-id=@appId + wx.ma.apps.tenantId2.app-secret =@secret + ## 选填 + wx.ma.apps.tenantId2.token=@token + wx.ma.apps.tenantId2.aes-key=@aesKey + wx.ma.apps.tenantId2.use-stable-access-token=@useStableAccessToken + + # ConfigStorage 配置(选填) + ## 配置类型: memory(默认), jedis, redisson + wx.ma.config-storage.type=memory + ## 相关redis前缀配置: wx:ma:multi(默认) + wx.ma.config-storage.key-prefix=wx:ma:multi + wx.ma.config-storage.redis.host=127.0.0.1 + wx.ma.config-storage.redis.port=6379 + ## 单机和 sentinel 同时存在时,优先使用sentinel配置 + # wx.ma.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379 + # wx.ma.config-storage.redis.sentinel-name=mymaster + + # http 客户端配置(选填) + ## # http客户端类型: http_client(默认), ok_http, jodd_http + wx.ma.config-storage.http-client-type=http_client + wx.ma.config-storage.http-proxy-host= + wx.ma.config-storage.http-proxy-port= + wx.ma.config-storage.http-proxy-username= + wx.ma.config-storage.http-proxy-password= + ## 最大重试次数,默认:5 次,如果小于 0,则为 0 + wx.ma.config-storage.max-retry-times=5 + ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000 + wx.ma.config-storage.retry-sleep-millis=1000 + ``` +3. 自动注入的类型:`WxMaMultiServices` + +4. 使用样例 + +```java +import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices; +import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices; +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.binarywang.wx.miniapp.api.WxMaUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class DemoService { + @Autowired + private WxMaMultiServices wxMaMultiServices; + + public void test() { + // 应用 1 的 WxMaService + WxMaService wxMaService1 = wxMaMultiServices.getWxMaService("tenantId1"); + WxMaUserService userService1 = wxMaService1.getUserService(); + userService1.userInfo("xxx"); + // todo ... + + // 应用 2 的 WxMaService + WxMaService wxMaService2 = wxMaMultiServices.getWxMaService("tenantId2"); + WxMaUserService userService2 = wxMaService2.getUserService(); + userService2.userInfo("xxx"); + // todo ... + + // 应用 3 的 WxMaService + WxMaService wxMaService3 = wxMaMultiServices.getWxMaService("tenantId3"); + // 判断是否为空 + if (wxMaService3 == null) { + // todo wxMaService3 为空,请先配置 tenantId3 微信公众号应用参数 + return; + } + WxMaUserService userService3 = wxMaService3.getUserService(); + userService3.userInfo("xxx"); + // todo ... + } +} +``` diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml new file mode 100644 index 0000000000..744ba094a1 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml @@ -0,0 +1,72 @@ + + + + wx-java-spring-boot-starters + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-miniapp-multi-spring-boot-starter + WxJava - Spring Boot Starter for MiniApp::支持多账号配置 + 微信公众号开发的 Spring Boot Starter::支持多账号配置 + + + + com.github.binarywang + weixin-java-miniapp + ${project.version} + + + redis.clients + jedis + provided + + + org.redisson + redisson + provided + + + org.springframework.data + spring-data-redis + provided + + + org.jodd + jodd-http + provided + + + com.squareup.okhttp3 + okhttp + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + + + diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/autoconfigure/WxMaMultiAutoConfiguration.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/autoconfigure/WxMaMultiAutoConfiguration.java new file mode 100644 index 0000000000..bd03751c37 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/autoconfigure/WxMaMultiAutoConfiguration.java @@ -0,0 +1,14 @@ +package com.binarywang.spring.starter.wxjava.miniapp.autoconfigure; + +import com.binarywang.spring.starter.wxjava.miniapp.configuration.WxMaMultiServiceConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * @author monch + * created on 2024/9/6 + */ +@Configuration +@Import(WxMaMultiServiceConfiguration.class) +public class WxMaMultiAutoConfiguration { +} diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/WxMaMultiServiceConfiguration.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/WxMaMultiServiceConfiguration.java new file mode 100644 index 0000000000..e1db56cfc7 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/WxMaMultiServiceConfiguration.java @@ -0,0 +1,27 @@ +package com.binarywang.spring.starter.wxjava.miniapp.configuration; + +import com.binarywang.spring.starter.wxjava.miniapp.configuration.services.WxMaInJedisConfiguration; +import com.binarywang.spring.starter.wxjava.miniapp.configuration.services.WxMaInMemoryConfiguration; +import com.binarywang.spring.starter.wxjava.miniapp.configuration.services.WxMaInRedisTemplateConfiguration; +import com.binarywang.spring.starter.wxjava.miniapp.configuration.services.WxMaInRedissonConfiguration; +import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * 微信小程序相关服务自动注册 + * + * @author monch + * created on 2024/9/6 + */ +@Configuration +@EnableConfigurationProperties(WxMaMultiProperties.class) +@Import({ + WxMaInJedisConfiguration.class, + WxMaInMemoryConfiguration.class, + WxMaInRedissonConfiguration.class, + WxMaInRedisTemplateConfiguration.class +}) +public class WxMaMultiServiceConfiguration { +} diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java new file mode 100644 index 0000000000..fba9d875ee --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/AbstractWxMaConfiguration.java @@ -0,0 +1,212 @@ +package com.binarywang.spring.starter.wxjava.miniapp.configuration.services; + +import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiProperties; +import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaSingleProperties; +import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices; +import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServicesImpl; +import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServicesSharedImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceHttpClientImpl; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceJoddHttpImpl; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceOkHttpImpl; +import cn.binarywang.wx.miniapp.config.WxMaConfig; +import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; + +/** + * WxMaConfigStorage 抽象配置类 + * + * @author monch + * created on 2024/9/6 + */ +@RequiredArgsConstructor +@Slf4j +public abstract class AbstractWxMaConfiguration { + + protected WxMaMultiServices wxMaMultiServices(WxMaMultiProperties wxMaMultiProperties) { + Map appsMap = wxMaMultiProperties.getApps(); + if (appsMap == null || appsMap.isEmpty()) { + log.warn("微信小程序应用参数未配置,通过 WxMaMultiServices#getWxMaService(\"tenantId\")获取实例将返回空"); + return new WxMaMultiServicesImpl(); + } + + /** + * 校验 appId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。 + * + * 查看 {@link cn.binarywang.wx.miniapp.config.impl.WxMaRedisConfigImpl#setAppId(String)} + */ + Collection apps = appsMap.values(); + if (apps.size() > 1) { + // 校验 appId 是否唯一 + boolean multi = apps.stream() + // 没有 appId,如果不判断是否为空,这里会报 NPE 异常 + .collect(Collectors.groupingBy(c -> c.getAppId() == null ? 0 : c.getAppId(), Collectors.counting())) + .entrySet().stream().anyMatch(e -> e.getValue() > 1); + if (multi) { + throw new RuntimeException("请确保微信小程序配置 appId 的唯一性"); + } + } + + // 根据配置选择多租户模式 + WxMaMultiProperties.MultiTenantMode mode = wxMaMultiProperties.getConfigStorage().getMultiTenantMode(); + if (mode == WxMaMultiProperties.MultiTenantMode.SHARED) { + return createSharedMultiServices(appsMap, wxMaMultiProperties); + } else { + return createIsolatedMultiServices(appsMap, wxMaMultiProperties); + } + } + + /** + * 创建隔离模式的多租户服务(每个租户独立 WxMaService 实例) + */ + private WxMaMultiServices createIsolatedMultiServices( + Map appsMap, + WxMaMultiProperties wxMaMultiProperties) { + + WxMaMultiServicesImpl services = new WxMaMultiServicesImpl(); + Set> entries = appsMap.entrySet(); + + for (Map.Entry entry : entries) { + String tenantId = entry.getKey(); + WxMaSingleProperties wxMaSingleProperties = entry.getValue(); + WxMaDefaultConfigImpl storage = this.wxMaConfigStorage(wxMaMultiProperties); + this.configApp(storage, wxMaSingleProperties); + this.configHttp(storage, wxMaMultiProperties.getConfigStorage()); + WxMaService wxMaService = this.wxMaService(storage, wxMaMultiProperties); + services.addWxMaService(tenantId, wxMaService); + } + + log.info("微信小程序多租户服务初始化完成,使用隔离模式(ISOLATED),共配置 {} 个租户", appsMap.size()); + return services; + } + + /** + * 创建共享模式的多租户服务(单个 WxMaService 实例管理多个配置) + */ + private WxMaMultiServices createSharedMultiServices( + Map appsMap, + WxMaMultiProperties wxMaMultiProperties) { + + // 创建共享的 WxMaService 实例 + WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage(); + WxMaService sharedService = createWxMaServiceByType(storage.getHttpClientType()); + configureWxMaService(sharedService, storage); + + // 准备所有租户的配置,使用 TreeMap 保证顺序一致性 + Map configsMap = new HashMap<>(); + String defaultTenantId = new TreeMap<>(appsMap).firstKey(); + + for (Map.Entry entry : appsMap.entrySet()) { + String tenantId = entry.getKey(); + WxMaSingleProperties wxMaSingleProperties = entry.getValue(); + WxMaDefaultConfigImpl config = this.wxMaConfigStorage(wxMaMultiProperties); + this.configApp(config, wxMaSingleProperties); + this.configHttp(config, storage); + configsMap.put(tenantId, config); + } + + // 设置多配置到共享的 WxMaService + sharedService.setMultiConfigs(configsMap, defaultTenantId); + + log.info("微信小程序多租户服务初始化完成,使用共享模式(SHARED),共配置 {} 个租户,共享一个 HTTP 客户端", appsMap.size()); + return new WxMaMultiServicesSharedImpl(sharedService); + } + + /** + * 根据类型创建 WxMaService 实例 + */ + private WxMaService createWxMaServiceByType(WxMaMultiProperties.HttpClientType httpClientType) { + switch (httpClientType) { + case OK_HTTP: + return new WxMaServiceOkHttpImpl(); + case JODD_HTTP: + return new WxMaServiceJoddHttpImpl(); + case HTTP_CLIENT: + return new WxMaServiceHttpClientImpl(); + default: + return new WxMaServiceImpl(); + } + } + + /** + * 配置 WxMaService 的通用参数 + */ + private void configureWxMaService(WxMaService wxMaService, WxMaMultiProperties.ConfigStorage storage) { + int maxRetryTimes = storage.getMaxRetryTimes(); + if (maxRetryTimes < 0) { + maxRetryTimes = 0; + } + int retrySleepMillis = storage.getRetrySleepMillis(); + if (retrySleepMillis < 0) { + retrySleepMillis = 1000; + } + wxMaService.setRetrySleepMillis(retrySleepMillis); + wxMaService.setMaxRetryTimes(maxRetryTimes); + } + + /** + * 配置 WxMaDefaultConfigImpl + * + * @param wxMaMultiProperties 参数 + * @return WxMaDefaultConfigImpl + */ + protected abstract WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties); + + public WxMaService wxMaService(WxMaConfig wxMaConfig, WxMaMultiProperties wxMaMultiProperties) { + WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage(); + WxMaService wxMaService = createWxMaServiceByType(storage.getHttpClientType()); + wxMaService.setWxMaConfig(wxMaConfig); + configureWxMaService(wxMaService, storage); + return wxMaService; + } + + private void configApp(WxMaDefaultConfigImpl config, WxMaSingleProperties properties) { + String appId = properties.getAppId(); + String appSecret = properties.getAppSecret(); + String token = properties.getToken(); + String aesKey = properties.getAesKey(); + boolean useStableAccessToken = properties.isUseStableAccessToken(); + + config.setAppid(appId); + config.setSecret(appSecret); + if (StringUtils.isNotBlank(token)) { + config.setToken(token); + } + if (StringUtils.isNotBlank(aesKey)) { + config.setAesKey(aesKey); + } + config.setMsgDataFormat(properties.getMsgDataFormat()); + config.useStableAccessToken(useStableAccessToken); + config.setApiHostUrl(StringUtils.trimToNull(properties.getApiHostUrl())); + config.setAccessTokenUrl(StringUtils.trimToNull(properties.getAccessTokenUrl())); + } + + private void configHttp(WxMaDefaultConfigImpl config, WxMaMultiProperties.ConfigStorage storage) { + String httpProxyHost = storage.getHttpProxyHost(); + Integer httpProxyPort = storage.getHttpProxyPort(); + String httpProxyUsername = storage.getHttpProxyUsername(); + String httpProxyPassword = storage.getHttpProxyPassword(); + if (StringUtils.isNotBlank(httpProxyHost)) { + config.setHttpProxyHost(httpProxyHost); + if (httpProxyPort != null) { + config.setHttpProxyPort(httpProxyPort); + } + if (StringUtils.isNotBlank(httpProxyUsername)) { + config.setHttpProxyUsername(httpProxyUsername); + } + if (StringUtils.isNotBlank(httpProxyPassword)) { + config.setHttpProxyPassword(httpProxyPassword); + } + } + } +} diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInJedisConfiguration.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInJedisConfiguration.java new file mode 100644 index 0000000000..52eeffe7e4 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInJedisConfiguration.java @@ -0,0 +1,76 @@ +package com.binarywang.spring.starter.wxjava.miniapp.configuration.services; + +import cn.binarywang.wx.miniapp.config.impl.WxMaRedisConfigImpl; +import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiProperties; +import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiRedisProperties; +import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices; +import lombok.RequiredArgsConstructor; +import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * 自动装配基于 jedis 策略配置 + * + * @author monch + * created on 2024/9/6 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxMaMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis" +) +@RequiredArgsConstructor +public class WxMaInJedisConfiguration extends AbstractWxMaConfiguration { + private final WxMaMultiProperties wxMaMultiProperties; + private final ApplicationContext applicationContext; + + @Bean + public WxMaMultiServices wxMaMultiServices() { + return this.wxMaMultiServices(wxMaMultiProperties); + } + + @Override + protected WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties) { + return this.configRedis(wxMaMultiProperties); + } + + private WxMaDefaultConfigImpl configRedis(WxMaMultiProperties wxMaMultiProperties) { + WxMaMultiRedisProperties wxMaMultiRedisProperties = wxMaMultiProperties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (wxMaMultiRedisProperties != null && StringUtils.isNotEmpty(wxMaMultiRedisProperties.getHost())) { + jedisPool = getJedisPool(wxMaMultiProperties); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + return new WxMaRedisConfigImpl(jedisPool); + } + + private JedisPool getJedisPool(WxMaMultiProperties wxMaMultiProperties) { + WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage(); + WxMaMultiRedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), + redis.getTimeout(), redis.getPassword(), redis.getDatabase()); + } +} diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInMemoryConfiguration.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInMemoryConfiguration.java new file mode 100644 index 0000000000..3c8202a6b3 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInMemoryConfiguration.java @@ -0,0 +1,39 @@ +package com.binarywang.spring.starter.wxjava.miniapp.configuration.services; + +import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; +import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiProperties; +import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 自动装配基于内存策略配置 + * + * @author monch + * created on 2024/9/6 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxMaMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "memory", matchIfMissing = true +) +@RequiredArgsConstructor +public class WxMaInMemoryConfiguration extends AbstractWxMaConfiguration { + private final WxMaMultiProperties wxMaMultiProperties; + + @Bean + public WxMaMultiServices wxMaMultiServices() { + return this.wxMaMultiServices(wxMaMultiProperties); + } + + @Override + protected WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties) { + return this.configInMemory(); + } + + private WxMaDefaultConfigImpl configInMemory() { + return new WxMaDefaultConfigImpl(); + // return new WxMaDefaultConfigImpl(); + } +} diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInRedisTemplateConfiguration.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInRedisTemplateConfiguration.java new file mode 100644 index 0000000000..fc88a0578a --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInRedisTemplateConfiguration.java @@ -0,0 +1,43 @@ +package com.binarywang.spring.starter.wxjava.miniapp.configuration.services; + +import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; +import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl; +import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiProperties; +import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * 自动装配基于 redisTemplate 策略配置 + * + * @author hb0730 2025/9/10 + */ +@Configuration +@ConditionalOnProperty(prefix = WxMaMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redis_template") +@RequiredArgsConstructor +public class WxMaInRedisTemplateConfiguration extends AbstractWxMaConfiguration { + private final WxMaMultiProperties wxMaMultiProperties; + private final ApplicationContext applicationContext; + + @Bean + public WxMaMultiServices wxMaMultiServices() { + return this.wxMaMultiServices(wxMaMultiProperties); + } + + @Override + protected WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties) { + return this.configRedisTemplate(wxMaMultiProperties); + } + + private WxMaDefaultConfigImpl configRedisTemplate(WxMaMultiProperties wxMaMultiProperties) { + StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class); + RedisTemplateWxRedisOps wxRedisOps = new RedisTemplateWxRedisOps(redisTemplate); + return new WxMaRedisBetterConfigImpl(wxRedisOps, wxMaMultiProperties.getConfigStorage().getKeyPrefix()); + } + +} diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInRedissonConfiguration.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInRedissonConfiguration.java new file mode 100644 index 0000000000..c1915400d3 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/configuration/services/WxMaInRedissonConfiguration.java @@ -0,0 +1,67 @@ +package com.binarywang.spring.starter.wxjava.miniapp.configuration.services; + +import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; +import cn.binarywang.wx.miniapp.config.impl.WxMaRedissonConfigImpl; +import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiProperties; +import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiRedisProperties; +import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 自动装配基于 redisson 策略配置 + * + * @author monch + * created on 2024/9/6 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxMaMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson" +) +@RequiredArgsConstructor +public class WxMaInRedissonConfiguration extends AbstractWxMaConfiguration { + private final WxMaMultiProperties wxMaMultiProperties; + private final ApplicationContext applicationContext; + + @Bean + public WxMaMultiServices wxMaMultiServices() { + return this.wxMaMultiServices(wxMaMultiProperties); + } + + @Override + protected WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties) { + return this.configRedisson(wxMaMultiProperties); + } + + private WxMaDefaultConfigImpl configRedisson(WxMaMultiProperties wxMaMultiProperties) { + WxMaMultiRedisProperties redisProperties = wxMaMultiProperties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + redissonClient = getRedissonClient(wxMaMultiProperties); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxMaRedissonConfigImpl(redissonClient, wxMaMultiProperties.getConfigStorage().getKeyPrefix()); + } + + private RedissonClient getRedissonClient(WxMaMultiProperties wxMaMultiProperties) { + WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage(); + WxMaMultiRedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redis.getHost() + ":" + redis.getPort()) + .setDatabase(redis.getDatabase()) + .setPassword(redis.getPassword()); + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaMultiProperties.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaMultiProperties.java new file mode 100644 index 0000000000..201aceb8bf --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaMultiProperties.java @@ -0,0 +1,178 @@ +package com.binarywang.spring.starter.wxjava.miniapp.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * @author monch + * created on 2024/9/6 + */ +@Data +@NoArgsConstructor +@ConfigurationProperties(WxMaMultiProperties.PREFIX) +public class WxMaMultiProperties implements Serializable { + private static final long serialVersionUID = -5358245184407791011L; + public static final String PREFIX = "wx.ma"; + + private Map apps = new HashMap<>(); + + /** + * 自定义host配置 + */ + private HostConfig hosts; + + /** + * 存储策略 + */ + private final ConfigStorage configStorage = new ConfigStorage(); + + @Data + @NoArgsConstructor + public static class HostConfig implements Serializable { + private static final long serialVersionUID = -4172767630740346001L; + + /** + * 对应于:https://api.weixin.qq.com + */ + private String apiHost; + + /** + * 对应于:https://open.weixin.qq.com + */ + private String openHost; + + /** + * 对应于:https://mp.weixin.qq.com + */ + private String mpHost; + } + + @Data + @NoArgsConstructor + public static class ConfigStorage implements Serializable { + private static final long serialVersionUID = 4815731027000065434L; + + /** + * 存储类型. + */ + private StorageType type = StorageType.MEMORY; + + /** + * 指定key前缀. + */ + private String keyPrefix = "wx:ma:multi"; + + /** + * redis连接配置. + */ + @NestedConfigurationProperty + private final WxMaMultiRedisProperties redis = new WxMaMultiRedisProperties(); + + /** + * http客户端类型. + */ + private HttpClientType httpClientType = HttpClientType.HTTP_CLIENT; + + /** + * http代理主机. + */ + private String httpProxyHost; + + /** + * http代理端口. + */ + private Integer httpProxyPort; + + /** + * http代理用户名. + */ + private String httpProxyUsername; + + /** + * http代理密码. + */ + private String httpProxyPassword; + + /** + * http 请求最大重试次数 + *
+     *   {@link cn.binarywang.wx.miniapp.api.WxMaService#setMaxRetryTimes(int)}
+     *   {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setMaxRetryTimes(int)}
+     * 
+ */ + private int maxRetryTimes = 5; + + /** + * http 请求重试间隔 + *
+     *   {@link cn.binarywang.wx.miniapp.api.WxMaService#setRetrySleepMillis(int)}
+     *   {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setRetrySleepMillis(int)}
+     * 
+ */ + private int retrySleepMillis = 1000; + + /** + * 多租户实现模式. + *
    + *
  • ISOLATED: 为每个租户创建独立的 WxMaService 实例(默认)
  • + *
  • SHARED: 使用单个 WxMaService 实例管理所有租户配置,共享 HTTP 客户端
  • + *
+ */ + private MultiTenantMode multiTenantMode = MultiTenantMode.ISOLATED; + } + + public enum StorageType { + /** + * 内存 + */ + MEMORY, + /** + * jedis + */ + JEDIS, + /** + * redisson + */ + REDISSON, + /** + * redisTemplate + */ + REDIS_TEMPLATE + } + + public enum HttpClientType { + /** + * HttpClient + */ + HTTP_CLIENT, + /** + * OkHttp + */ + OK_HTTP, + /** + * JoddHttp + */ + JODD_HTTP + } + + public enum MultiTenantMode { + /** + * 隔离模式:为每个租户创建独立的 WxMaService 实例. + * 优点:线程安全,不依赖 ThreadLocal + * 缺点:每个租户创建独立的 HTTP 客户端,资源占用较多 + */ + ISOLATED, + /** + * 共享模式:使用单个 WxMaService 实例管理所有租户配置. + * 优点:共享 HTTP 客户端,节省资源 + * 缺点:依赖 ThreadLocal 切换配置,异步场景需注意 + */ + SHARED + } +} diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaMultiRedisProperties.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaMultiRedisProperties.java new file mode 100644 index 0000000000..67562c69a4 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaMultiRedisProperties.java @@ -0,0 +1,56 @@ +package com.binarywang.spring.starter.wxjava.miniapp.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * @author monch + * created on 2024/9/6 + */ +@Data +@NoArgsConstructor +public class WxMaMultiRedisProperties implements Serializable { + private static final long serialVersionUID = -5924815351660074401L; + + /** + * 主机地址. + */ + private String host = "127.0.0.1"; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + /** + * sentinel ips + */ + private String sentinelIps; + + /** + * sentinel name + */ + private String sentinelName; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaSingleProperties.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaSingleProperties.java new file mode 100644 index 0000000000..5defae5514 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaSingleProperties.java @@ -0,0 +1,57 @@ +package com.binarywang.spring.starter.wxjava.miniapp.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * @author monch + * created on 2024/9/6 + */ +@Data +@NoArgsConstructor +public class WxMaSingleProperties implements Serializable { + private static final long serialVersionUID = 1980986361098922525L; + /** + * 设置微信公众号的 appid. + */ + private String appId; + + /** + * 设置微信公众号的 app secret. + */ + private String appSecret; + + /** + * 设置微信公众号的 token. + */ + private String token; + + /** + * 设置微信公众号的 EncodingAESKey. + */ + private String aesKey; + + /** + * 消息格式,XML或者JSON. + */ + private String msgDataFormat; + + /** + * 是否使用稳定版 Access Token + */ + private boolean useStableAccessToken = false; + + /** + * 自定义API主机地址,用于替换默认的 https://api.weixin.qq.com + * 例如:http://proxy.company.com:8080 + */ + private String apiHostUrl; + + /** + * 自定义获取AccessToken地址,用于向自定义统一服务获取AccessToken + * 例如:http://proxy.company.com:8080/oauth/token + */ + private String accessTokenUrl; +} diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServices.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServices.java new file mode 100644 index 0000000000..90fce690c7 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServices.java @@ -0,0 +1,27 @@ +package com.binarywang.spring.starter.wxjava.miniapp.service; + + +import cn.binarywang.wx.miniapp.api.WxMaService; + +/** + * 微信小程序 {@link WxMaService} 所有实例存放类. + * + * @author monch + * created on 2024/9/6 + */ +public interface WxMaMultiServices { + /** + * 通过租户 Id 获取 WxMaService + * + * @param tenantId 租户 Id + * @return WxMaService + */ + WxMaService getWxMaService(String tenantId); + + /** + * 根据租户 Id,从列表中移除一个 WxMaService 实例 + * + * @param tenantId 租户 Id + */ + void removeWxMaService(String tenantId); +} diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesImpl.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesImpl.java new file mode 100644 index 0000000000..913a371f52 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesImpl.java @@ -0,0 +1,36 @@ +package com.binarywang.spring.starter.wxjava.miniapp.service; + +import cn.binarywang.wx.miniapp.api.WxMaService; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 微信小程序 {@link com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices} 默认实现 + * + * @author monch + * created on 2024/9/6 + */ +public class WxMaMultiServicesImpl implements com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices { + private final Map services = new ConcurrentHashMap<>(); + + @Override + public WxMaService getWxMaService(String tenantId) { + return this.services.get(tenantId); + } + + /** + * 根据租户 Id,添加一个 WxMaService 到列表 + * + * @param tenantId 租户 Id + * @param wxMaService WxMaService 实例 + */ + public void addWxMaService(String tenantId, WxMaService wxMaService) { + this.services.put(tenantId, wxMaService); + } + + @Override + public void removeWxMaService(String tenantId) { + this.services.remove(tenantId); + } +} diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java new file mode 100644 index 0000000000..40a01fb52e --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/service/WxMaMultiServicesSharedImpl.java @@ -0,0 +1,53 @@ +package com.binarywang.spring.starter.wxjava.miniapp.service; + +import cn.binarywang.wx.miniapp.api.WxMaService; +import lombok.RequiredArgsConstructor; + +/** + * 微信小程序 {@link WxMaMultiServices} 共享式实现. + *

+ * 使用单个 WxMaService 实例管理多个租户配置,通过 switchover 切换租户。 + * 相比 {@link WxMaMultiServicesImpl},此实现共享 HTTP 客户端,节省资源。 + *

+ *

+ * 注意:由于使用 ThreadLocal 切换配置,在异步或多线程场景需要特别注意线程上下文切换。 + *

+ * + * @author Binary Wang + * created on 2026/1/9 + */ +@RequiredArgsConstructor +public class WxMaMultiServicesSharedImpl implements WxMaMultiServices { + private final WxMaService sharedWxMaService; + + @Override + public WxMaService getWxMaService(String tenantId) { + if (tenantId == null) { + return null; + } + // 使用 switchover 检查配置是否存在,保持与隔离模式 API 行为一致(不存在时返回 null) + if (!sharedWxMaService.switchover(tenantId)) { + return null; + } + return sharedWxMaService; + } + + @Override + public void removeWxMaService(String tenantId) { + if (tenantId != null) { + sharedWxMaService.removeConfig(tenantId); + } + } + + /** + * 添加租户配置到共享的 WxMaService 实例 + * + * @param tenantId 租户 ID + * @param wxMaService 要添加配置的 WxMaService(仅使用其配置,不使用其实例) + */ + public void addWxMaService(String tenantId, WxMaService wxMaService) { + if (tenantId != null && wxMaService != null) { + sharedWxMaService.addConfig(tenantId, wxMaService.getWxMaConfig()); + } + } +} diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..bc9bec9bfb --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.binarywang.spring.starter.wxjava.miniapp.autoconfigure.WxMaMultiAutoConfiguration diff --git a/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..3023f06bdd --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.binarywang.spring.starter.wxjava.miniapp.autoconfigure.WxMaMultiAutoConfiguration diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md index fe75ef4e3e..cbf0b53925 100644 --- a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md +++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/README.md @@ -1,26 +1,36 @@ -# 使用说明 -1. 在自己的Spring Boot项目里,引入maven依赖 -```xml +# wx-java-miniapp-spring-boot-starter +## 快速开始 +1. 引入依赖 + ```xml com.github.binarywang wx-java-miniapp-spring-boot-starter ${version} - ``` -2. 添加配置(application.yml) -```yml -wx: - miniapp: - appid: 111 - secret: 111 - token: 111 - aesKey: 111 - msgDataFormat: JSON -``` - - - - - - + ``` +2. 添加配置(application.properties) + ```properties + # 小程序配置(必填) + wx.miniapp.appid = appId + wx.miniapp.secret = @secret + wx.miniapp.token = @token + wx.miniapp.aesKey = @aesKey + wx.miniapp.msgDataFormat = @msgDataFormat # 消息格式,XML或者JSON. + wx.miniapp.use-stable-access-token=@useStableAccessToken + # 存储配置redis(可选) + # 注意: 指定redis.host值后不会使用容器注入的redis连接(JedisPool) + wx.miniapp.config-storage.type = Jedis # 配置类型: Memory(默认), Jedis, RedisTemplate + wx.miniapp.config-storage.key-prefix = wa # 相关redis前缀配置: wa(默认) + wx.miniapp.config-storage.redis.host = 127.0.0.1 + wx.miniapp.config-storage.redis.port = 6379 + # http客户端配置 + wx.miniapp.config-storage.http-client-type=HttpClient # http客户端类型: HttpClient(默认), OkHttp, JoddHttp + wx.miniapp.config-storage.http-proxy-host= + wx.miniapp.config-storage.http-proxy-port= + wx.miniapp.config-storage.http-proxy-username= + wx.miniapp.config-storage.http-proxy-password= + ``` +3. 自动注入的类型 +- `WxMaService` +- `WxMaConfig` diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml index c538357675..1088b711e7 100644 --- a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml @@ -1,11 +1,10 @@ - wx-java-spring-boot-starters com.github.binarywang - 3.6.0 + 4.8.3.B 4.0.0 @@ -19,6 +18,31 @@ weixin-java-miniapp ${project.version}
+ + redis.clients + jedis + provided + + + org.redisson + redisson + provided + + + org.springframework.data + spring-data-redis + provided + + + org.jodd + jodd-http + provided + + + com.squareup.okhttp3 + okhttp + provided + diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaAutoConfiguration.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaAutoConfiguration.java index 109b398d13..67a7efaecf 100644 --- a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaAutoConfiguration.java +++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaAutoConfiguration.java @@ -1,50 +1,21 @@ package com.binarywang.spring.starter.wxjava.miniapp.config; -import cn.binarywang.wx.miniapp.api.WxMaService; -import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; -import cn.binarywang.wx.miniapp.config.WxMaConfig; -import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties; -import lombok.AllArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; /** * 自动配置. * * @author Binary Wang - * @date 2019-08-10 + * created on 2019-08-10 */ -@AllArgsConstructor @Configuration -@ConditionalOnClass(WxMaService.class) @EnableConfigurationProperties(WxMaProperties.class) -@ConditionalOnProperty(prefix = "wx.miniapp", value = "enabled", matchIfMissing = true) +@Import({ + WxMaStorageAutoConfiguration.class, + WxMaServiceAutoConfiguration.class +}) public class WxMaAutoConfiguration { - private WxMaProperties properties; - - /** - * 小程序service. - * - * @return 小程序service - */ - @Bean - @ConditionalOnMissingBean(WxMaService.class) - public WxMaService service() { - WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl(); - config.setAppid(StringUtils.trimToNull(this.properties.getAppid())); - config.setSecret(StringUtils.trimToNull(this.properties.getSecret())); - config.setToken(StringUtils.trimToNull(this.properties.getToken())); - config.setAesKey(StringUtils.trimToNull(this.properties.getAesKey())); - config.setMsgDataFormat(StringUtils.trimToNull(this.properties.getMsgDataFormat())); - - final WxMaServiceImpl service = new WxMaServiceImpl(); - service.setWxMaConfig(config); - return service; - } } diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaServiceAutoConfiguration.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaServiceAutoConfiguration.java new file mode 100644 index 0000000000..f03d3f1493 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaServiceAutoConfiguration.java @@ -0,0 +1,60 @@ +package com.binarywang.spring.starter.wxjava.miniapp.config; + +import cn.binarywang.wx.miniapp.api.WxMaService; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceHttpClientImpl; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceHttpComponentsImpl; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceJoddHttpImpl; +import cn.binarywang.wx.miniapp.api.impl.WxMaServiceOkHttpImpl; +import cn.binarywang.wx.miniapp.config.WxMaConfig; +import com.binarywang.spring.starter.wxjava.miniapp.enums.HttpClientType; +import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties; +import lombok.AllArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 微信小程序平台相关服务自动注册. + * + * @author someone TaoYu + */ +@Configuration +@AllArgsConstructor +public class WxMaServiceAutoConfiguration { + + private final WxMaProperties wxMaProperties; + + /** + * 小程序service. + * + * @return 小程序service + */ + @Bean + @ConditionalOnMissingBean(WxMaService.class) + @ConditionalOnBean(WxMaConfig.class) + public WxMaService wxMaService(WxMaConfig wxMaConfig) { + HttpClientType httpClientType = wxMaProperties.getConfigStorage().getHttpClientType(); + WxMaService wxMaService; + switch (httpClientType) { + case OkHttp: + wxMaService = new WxMaServiceOkHttpImpl(); + break; + case JoddHttp: + wxMaService = new WxMaServiceJoddHttpImpl(); + break; + case HttpClient: + wxMaService = new WxMaServiceHttpClientImpl(); + break; + case HttpComponents: + wxMaService = new WxMaServiceHttpComponentsImpl(); + break; + default: + wxMaService = new WxMaServiceImpl(); + break; + } + wxMaService.setWxMaConfig(wxMaConfig); + return wxMaService; + } +} diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaStorageAutoConfiguration.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaStorageAutoConfiguration.java new file mode 100644 index 0000000000..0f0477a8b0 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/WxMaStorageAutoConfiguration.java @@ -0,0 +1,23 @@ +package com.binarywang.spring.starter.wxjava.miniapp.config; + +import com.binarywang.spring.starter.wxjava.miniapp.config.storage.WxMaInJedisConfigStorageConfiguration; +import com.binarywang.spring.starter.wxjava.miniapp.config.storage.WxMaInMemoryConfigStorageConfiguration; +import com.binarywang.spring.starter.wxjava.miniapp.config.storage.WxMaInRedisTemplateConfigStorageConfiguration; +import com.binarywang.spring.starter.wxjava.miniapp.config.storage.WxMaInRedissonConfigStorageConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * 微信小程序存储策略自动配置. + * + * @author someone TaoYu + */ +@Configuration +@Import({ + WxMaInMemoryConfigStorageConfiguration.class, + WxMaInJedisConfigStorageConfiguration.class, + WxMaInRedisTemplateConfigStorageConfiguration.class, + WxMaInRedissonConfigStorageConfiguration.class +}) +public class WxMaStorageAutoConfiguration { +} diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/AbstractWxMaConfigStorageConfiguration.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/AbstractWxMaConfigStorageConfiguration.java new file mode 100644 index 0000000000..abcd83e848 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/AbstractWxMaConfigStorageConfiguration.java @@ -0,0 +1,58 @@ +package com.binarywang.spring.starter.wxjava.miniapp.config.storage; + +import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; +import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties; +import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; +import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder; +import org.apache.commons.lang3.StringUtils; + +/** + * @author yl TaoYu + */ +public abstract class AbstractWxMaConfigStorageConfiguration { + + protected WxMaDefaultConfigImpl config(WxMaDefaultConfigImpl config, WxMaProperties properties) { + WxMaProperties.ConfigStorage storage = properties.getConfigStorage(); + config.setAppid(StringUtils.trimToNull(properties.getAppid())); + config.setSecret(StringUtils.trimToNull(properties.getSecret())); + config.setToken(StringUtils.trimToNull(properties.getToken())); + config.setAesKey(StringUtils.trimToNull(properties.getAesKey())); + config.setMsgDataFormat(StringUtils.trimToNull(properties.getMsgDataFormat())); + config.useStableAccessToken(properties.isUseStableAccessToken()); + config.setApiHostUrl(StringUtils.trimToNull(properties.getApiHostUrl())); + config.setAccessTokenUrl(StringUtils.trimToNull(properties.getAccessTokenUrl())); + + WxMaProperties.ConfigStorage configStorageProperties = properties.getConfigStorage(); + config.setHttpProxyHost(configStorageProperties.getHttpProxyHost()); + config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername()); + config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword()); + if (configStorageProperties.getHttpProxyPort() != null) { + config.setHttpProxyPort(configStorageProperties.getHttpProxyPort()); + } + + // 设置自定义的HttpClient超时配置 + ApacheHttpClientBuilder clientBuilder = config.getApacheHttpClientBuilder(); + if (clientBuilder == null) { + clientBuilder = DefaultApacheHttpClientBuilder.get(); + } + if (clientBuilder instanceof DefaultApacheHttpClientBuilder) { + DefaultApacheHttpClientBuilder defaultBuilder = (DefaultApacheHttpClientBuilder) clientBuilder; + defaultBuilder.setConnectionTimeout(storage.getConnectionTimeout()); + defaultBuilder.setSoTimeout(storage.getSoTimeout()); + defaultBuilder.setConnectionRequestTimeout(storage.getConnectionRequestTimeout()); + config.setApacheHttpClientBuilder(defaultBuilder); + } + + int maxRetryTimes = configStorageProperties.getMaxRetryTimes(); + if (configStorageProperties.getMaxRetryTimes() < 0) { + maxRetryTimes = 0; + } + int retrySleepMillis = configStorageProperties.getRetrySleepMillis(); + if (retrySleepMillis < 0) { + retrySleepMillis = 1000; + } + config.setRetrySleepMillis(retrySleepMillis); + config.setMaxRetryTimes(maxRetryTimes); + return config; + } +} diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInJedisConfigStorageConfiguration.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInJedisConfigStorageConfiguration.java new file mode 100644 index 0000000000..93b901ebf8 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInJedisConfigStorageConfiguration.java @@ -0,0 +1,72 @@ +package com.binarywang.spring.starter.wxjava.miniapp.config.storage; + +import cn.binarywang.wx.miniapp.config.WxMaConfig; +import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl; +import com.binarywang.spring.starter.wxjava.miniapp.properties.RedisProperties; +import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.redis.JedisWxRedisOps; +import me.chanjar.weixin.common.redis.WxRedisOps; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * @author yl TaoYu + */ +@Configuration +@ConditionalOnProperty(prefix = WxMaProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis") +@ConditionalOnClass({JedisPool.class, JedisPoolConfig.class}) +@RequiredArgsConstructor +public class WxMaInJedisConfigStorageConfiguration extends AbstractWxMaConfigStorageConfiguration { + private final WxMaProperties properties; + private final ApplicationContext applicationContext; + + @Bean + @ConditionalOnMissingBean(WxMaConfig.class) + public WxMaConfig wxMaConfig() { + WxMaRedisBetterConfigImpl config = getWxMaRedisBetterConfigImpl(); + return this.config(config, properties); + } + + private WxMaRedisBetterConfigImpl getWxMaRedisBetterConfigImpl() { + RedisProperties redisProperties = properties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + jedisPool = getJedisPool(); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + WxRedisOps redisOps = new JedisWxRedisOps(jedisPool); + return new WxMaRedisBetterConfigImpl(redisOps, properties.getConfigStorage().getKeyPrefix()); + } + + private JedisPool getJedisPool() { + WxMaProperties.ConfigStorage storage = properties.getConfigStorage(); + RedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(), redis.getDatabase()); + } +} diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInMemoryConfigStorageConfiguration.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInMemoryConfigStorageConfiguration.java new file mode 100644 index 0000000000..44e727af83 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInMemoryConfigStorageConfiguration.java @@ -0,0 +1,28 @@ +package com.binarywang.spring.starter.wxjava.miniapp.config.storage; + +import cn.binarywang.wx.miniapp.config.WxMaConfig; +import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl; +import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author yl TaoYu + */ +@Configuration +@ConditionalOnProperty(prefix = WxMaProperties.PREFIX + ".config-storage", name = "type", + matchIfMissing = true, havingValue = "memory") +@RequiredArgsConstructor +public class WxMaInMemoryConfigStorageConfiguration extends AbstractWxMaConfigStorageConfiguration { + private final WxMaProperties properties; + + @Bean + @ConditionalOnMissingBean(WxMaConfig.class) + public WxMaConfig wxMaConfig() { + WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl(); + return this.config(config, properties); + } +} diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInRedisTemplateConfigStorageConfiguration.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInRedisTemplateConfigStorageConfiguration.java new file mode 100644 index 0000000000..81cf8c6559 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInRedisTemplateConfigStorageConfiguration.java @@ -0,0 +1,40 @@ +package com.binarywang.spring.starter.wxjava.miniapp.config.storage; + +import cn.binarywang.wx.miniapp.config.WxMaConfig; +import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl; +import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps; +import me.chanjar.weixin.common.redis.WxRedisOps; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * @author yl TaoYu + */ +@Configuration +@ConditionalOnProperty(prefix = WxMaProperties.PREFIX + ".config-storage", name = "type", havingValue = "redistemplate") +@ConditionalOnClass(StringRedisTemplate.class) +@RequiredArgsConstructor +public class WxMaInRedisTemplateConfigStorageConfiguration extends AbstractWxMaConfigStorageConfiguration { + private final WxMaProperties properties; + private final ApplicationContext applicationContext; + + @Bean + @ConditionalOnMissingBean(WxMaConfig.class) + public WxMaConfig wxMaConfig() { + WxMaRedisBetterConfigImpl config = getWxMaInRedisTemplateConfigStorage(); + return this.config(config, properties); + } + + private WxMaRedisBetterConfigImpl getWxMaInRedisTemplateConfigStorage() { + StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class); + WxRedisOps redisOps = new RedisTemplateWxRedisOps(redisTemplate); + return new WxMaRedisBetterConfigImpl(redisOps, properties.getConfigStorage().getKeyPrefix()); + } +} diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInRedissonConfigStorageConfiguration.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInRedissonConfigStorageConfiguration.java new file mode 100644 index 0000000000..2a030b5f9e --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/config/storage/WxMaInRedissonConfigStorageConfiguration.java @@ -0,0 +1,61 @@ +package com.binarywang.spring.starter.wxjava.miniapp.config.storage; + +import cn.binarywang.wx.miniapp.config.WxMaConfig; +import cn.binarywang.wx.miniapp.config.impl.WxMaRedissonConfigImpl; +import com.binarywang.spring.starter.wxjava.miniapp.properties.RedisProperties; +import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author yl TaoYu + */ +@Configuration +@ConditionalOnProperty(prefix = WxMaProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson") +@ConditionalOnClass({Redisson.class, RedissonClient.class}) +@RequiredArgsConstructor +public class WxMaInRedissonConfigStorageConfiguration extends AbstractWxMaConfigStorageConfiguration { + private final WxMaProperties properties; + private final ApplicationContext applicationContext; + + @Bean + @ConditionalOnMissingBean(WxMaConfig.class) + public WxMaConfig wxMaConfig() { + WxMaRedissonConfigImpl config = getWxMaInRedissonConfigStorage(); + return this.config(config, properties); + } + + private WxMaRedissonConfigImpl getWxMaInRedissonConfigStorage() { + RedisProperties redisProperties = properties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + redissonClient = getRedissonClient(); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxMaRedissonConfigImpl(redissonClient, properties.getConfigStorage().getKeyPrefix()); + } + + private RedissonClient getRedissonClient() { + WxMaProperties.ConfigStorage storage = properties.getConfigStorage(); + RedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redis.getHost() + ":" + redis.getPort()) + .setDatabase(redis.getDatabase()) + .setPassword(redis.getPassword()); + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/enums/HttpClientType.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/enums/HttpClientType.java new file mode 100644 index 0000000000..48549e4399 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/enums/HttpClientType.java @@ -0,0 +1,26 @@ +package com.binarywang.spring.starter.wxjava.miniapp.enums; + +/** + * httpclient类型. + * + * @author Binary Wang + * created on 2020-05-25 + */ +public enum HttpClientType { + /** + * HttpClient (Apache HttpClient 4.x). + */ + HttpClient, + /** + * OkHttp. + */ + OkHttp, + /** + * JoddHttp. + */ + JoddHttp, + /** + * HttpComponents (Apache HttpClient 5.x). + */ + HttpComponents, +} diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/enums/StorageType.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/enums/StorageType.java new file mode 100644 index 0000000000..31c6e4b602 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/enums/StorageType.java @@ -0,0 +1,26 @@ +package com.binarywang.spring.starter.wxjava.miniapp.enums; + +/** + * storage类型. + * + * @author Binary Wang + * created on 2020-05-25 + */ +public enum StorageType { + /** + * 内存. + */ + Memory, + /** + * redis(JedisClient). + */ + Jedis, + /** + * redis(Redisson). + */ + Redisson, + /** + * redis(RedisTemplate). + */ + RedisTemplate +} diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/RedisProperties.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/RedisProperties.java new file mode 100644 index 0000000000..75e3740a19 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/RedisProperties.java @@ -0,0 +1,43 @@ +package com.binarywang.spring.starter.wxjava.miniapp.properties; + +import lombok.Data; + +/** + * redis 配置. + * + * @author Binary Wang + * created on 2020-08-30 + */ +@Data +public class RedisProperties { + + /** + * 主机地址.不填则从spring容器内获取JedisPool + */ + private String host; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaProperties.java b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaProperties.java index 6477e61fa2..82f1500941 100644 --- a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaProperties.java +++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/miniapp/properties/WxMaProperties.java @@ -1,17 +1,24 @@ package com.binarywang.spring.starter.wxjava.miniapp.properties; +import com.binarywang.spring.starter.wxjava.miniapp.enums.HttpClientType; +import com.binarywang.spring.starter.wxjava.miniapp.enums.StorageType; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import static com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaProperties.PREFIX; /** * 属性配置类. * * @author Binary Wang - * @date 2019-08-10 + * created on 2019-08-10 */ @Data -@ConfigurationProperties(prefix = "wx.miniapp") +@ConfigurationProperties(prefix = PREFIX) public class WxMaProperties { + public static final String PREFIX = "wx.miniapp"; + /** * 设置微信小程序的appid. */ @@ -36,4 +43,102 @@ public class WxMaProperties { * 消息格式,XML或者JSON. */ private String msgDataFormat; + + /** + * 是否使用稳定版 Access Token + */ + private boolean useStableAccessToken = false; + + /** + * 自定义API主机地址,用于替换默认的 https://api.weixin.qq.com + * 例如:http://proxy.company.com:8080 + */ + private String apiHostUrl; + + /** + * 自定义获取AccessToken地址,用于向自定义统一服务获取AccessToken + * 例如:http://proxy.company.com:8080/oauth/token + */ + private String accessTokenUrl; + + /** + * 存储策略 + */ + private final ConfigStorage configStorage = new ConfigStorage(); + + @Data + public static class ConfigStorage { + + /** + * 存储类型. + */ + private StorageType type = StorageType.Memory; + + /** + * 指定key前缀. + */ + private String keyPrefix = "wa"; + + /** + * redis连接配置. + */ + @NestedConfigurationProperty + private final RedisProperties redis = new RedisProperties(); + + /** + * http客户端类型. + */ + private HttpClientType httpClientType = HttpClientType.HttpComponents; + + /** + * http代理主机. + */ + private String httpProxyHost; + + /** + * http代理端口. + */ + private Integer httpProxyPort; + + /** + * http代理用户名. + */ + private String httpProxyUsername; + + /** + * http代理密码. + */ + private String httpProxyPassword; + + /** + * http 请求重试间隔 + *
+     *   {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setRetrySleepMillis(int)}
+     * 
+ */ + private int retrySleepMillis = 1000; + /** + * http 请求最大重试次数 + *
+     *   {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setMaxRetryTimes(int)}
+     * 
+ */ + private int maxRetryTimes = 5; + + /** + * 连接超时时间,单位毫秒 + */ + private int connectionTimeout = 5000; + + /** + * 读数据超时时间,即socketTimeout,单位毫秒 + */ + private int soTimeout = 5000; + + /** + * 从连接池获取链接的超时时间,单位毫秒 + */ + private int connectionRequestTimeout = 5000; + } + } diff --git a/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..6644fa9701 --- /dev/null +++ b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.binarywang.spring.starter.wxjava.miniapp.config.WxMaAutoConfiguration diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/README.md b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/README.md new file mode 100644 index 0000000000..26b593addd --- /dev/null +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/README.md @@ -0,0 +1,100 @@ +# wx-java-mp-multi-spring-boot-starter + +## 快速开始 + +1. 引入依赖 + ```xml + + com.github.binarywang + wx-java-mp-multi-spring-boot-starter + ${version} + + ``` +2. 添加配置(application.properties) + ```properties + # 公众号配置 + ## 应用 1 配置(必填) + wx.mp.apps.tenantId1.app-id=appId + wx.mp.apps.tenantId1.app-secret=@secret + ## 选填 + wx.mp.apps.tenantId1.token=@token + wx.mp.apps.tenantId1.aes-key=@aesKey + wx.mp.apps.tenantId1.use-stable-access-token=@useStableAccessToken + ## 应用 2 配置(必填) + wx.mp.apps.tenantId2.app-id=@appId + wx.mp.apps.tenantId2.app-secret =@secret + ## 选填 + wx.mp.apps.tenantId2.token=@token + wx.mp.apps.tenantId2.aes-key=@aesKey + wx.mp.apps.tenantId2.use-stable-access-token=@useStableAccessToken + + # ConfigStorage 配置(选填) + ## 配置类型: memory(默认), jedis, redisson, redis_template + wx.mp.config-storage.type=memory + ## 相关redis前缀配置: wx:mp:multi(默认) + wx.mp.config-storage.key-prefix=wx:mp:multi + wx.mp.config-storage.redis.host=127.0.0.1 + wx.mp.config-storage.redis.port=6379 + ## 单机和 sentinel 同时存在时,优先使用sentinel配置 + # wx.mp.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379 + # wx.mp.config-storage.redis.sentinel-name=mymaster + + # http 客户端配置(选填) + ## # http客户端类型: http_client(默认), ok_http, jodd_http + wx.mp.config-storage.http-client-type=http_client + wx.mp.config-storage.http-proxy-host= + wx.mp.config-storage.http-proxy-port= + wx.mp.config-storage.http-proxy-username= + wx.mp.config-storage.http-proxy-password= + ## 最大重试次数,默认:5 次,如果小于 0,则为 0 + wx.mp.config-storage.max-retry-times=5 + ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000 + wx.mp.config-storage.retry-sleep-millis=1000 + + # 公众号地址 host 配置 + # wx.mp.hosts.api-host=http://proxy.com/ + # wx.mp.hosts.open-host=http://proxy.com/ + # wx.mp.hosts.mp-host=http://proxy.com/ + ``` +3. 自动注入的类型:`WxMpMultiServices` + +4. 使用样例 + +```java +import com.binarywang.spring.starter.wxjava.mp.service.WxMaMultiServices; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.api.WxMpUserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class DemoService { + @Autowired + private WxMpMultiServices wxMpMultiServices; + + public void test() { + // 应用 1 的 WxMpService + WxMpService wxMpService1 = wxMpMultiServices.getWxMpService("tenantId1"); + WxMpUserService userService1 = wxMpService1.getUserService(); + userService1.userInfo("xxx"); + // todo ... + + // 应用 2 的 WxMpService + WxMpService wxMpService2 = wxMpMultiServices.getWxMpService("tenantId2"); + WxMpUserService userService2 = wxMpService2.getUserService(); + userService2.userInfo("xxx"); + // todo ... + + // 应用 3 的 WxMpService + WxMpService wxMpService3 = wxMpMultiServices.getWxMpService("tenantId3"); + // 判断是否为空 + if (wxMpService3 == null) { + // todo wxMpService3 为空,请先配置 tenantId3 微信公众号应用参数 + return; + } + WxMpUserService userService3 = wxMpService3.getUserService(); + userService3.userInfo("xxx"); + // todo ... + } +} +``` diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/pom.xml new file mode 100644 index 0000000000..de88f187a7 --- /dev/null +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/pom.xml @@ -0,0 +1,77 @@ + + + + wx-java-spring-boot-starters + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-mp-multi-spring-boot-starter + WxJava - Spring Boot Starter for MP::支持多账号配置 + 微信公众号开发的 Spring Boot Starter::支持多账号配置 + + + + com.github.binarywang + weixin-java-mp + ${project.version} + + + redis.clients + jedis + provided + + + org.redisson + redisson + provided + + + org.springframework.data + spring-data-redis + provided + + + org.jodd + jodd-http + provided + + + com.squareup.okhttp3 + okhttp + provided + + + org.apache.httpcomponents.client5 + httpclient5 + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + + + diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/autoconfigure/WxMpMultiAutoConfiguration.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/autoconfigure/WxMpMultiAutoConfiguration.java new file mode 100644 index 0000000000..21ec0925d3 --- /dev/null +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/autoconfigure/WxMpMultiAutoConfiguration.java @@ -0,0 +1,14 @@ +package com.binarywang.spring.starter.wxjava.mp.autoconfigure; + +import com.binarywang.spring.starter.wxjava.mp.configuration.WxMpMultiServiceConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * @author yl + * created on 2024/1/23 + */ +@Configuration +@Import(WxMpMultiServiceConfiguration.class) +public class WxMpMultiAutoConfiguration { +} diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/WxMpMultiServiceConfiguration.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/WxMpMultiServiceConfiguration.java new file mode 100644 index 0000000000..35a53d0ccd --- /dev/null +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/WxMpMultiServiceConfiguration.java @@ -0,0 +1,27 @@ +package com.binarywang.spring.starter.wxjava.mp.configuration; + +import com.binarywang.spring.starter.wxjava.mp.configuration.services.WxMpInJedisConfiguration; +import com.binarywang.spring.starter.wxjava.mp.configuration.services.WxMpInMemoryConfiguration; +import com.binarywang.spring.starter.wxjava.mp.configuration.services.WxMpInRedisTemplateConfiguration; +import com.binarywang.spring.starter.wxjava.mp.configuration.services.WxMpInRedissonConfiguration; +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpMultiProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * 微信公众号相关服务自动注册 + * + * @author yl + * created on 2024/1/23 + */ +@Configuration +@EnableConfigurationProperties(WxMpMultiProperties.class) +@Import({ + WxMpInJedisConfiguration.class, + WxMpInMemoryConfiguration.class, + WxMpInRedissonConfiguration.class, + WxMpInRedisTemplateConfiguration.class +}) +public class WxMpMultiServiceConfiguration { +} diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/AbstractWxMpConfiguration.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/AbstractWxMpConfiguration.java new file mode 100644 index 0000000000..46724c625f --- /dev/null +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/AbstractWxMpConfiguration.java @@ -0,0 +1,231 @@ +package com.binarywang.spring.starter.wxjava.mp.configuration.services; + +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpMultiProperties; +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpSingleProperties; +import com.binarywang.spring.starter.wxjava.mp.service.WxMpMultiServices; +import com.binarywang.spring.starter.wxjava.mp.service.WxMpMultiServicesImpl; +import com.binarywang.spring.starter.wxjava.mp.service.WxMpMultiServicesSharedImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpClientImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpComponentsImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceJoddHttpImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceOkHttpImpl; +import me.chanjar.weixin.mp.config.WxMpConfigStorage; +import me.chanjar.weixin.mp.config.WxMpHostConfig; +import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.stream.Collectors; + +/** + * WxMpConfigStorage 抽象配置类 + * + * @author yl + * created on 2024/1/23 + */ +@RequiredArgsConstructor +@Slf4j +public abstract class AbstractWxMpConfiguration { + + protected WxMpMultiServices wxMpMultiServices(WxMpMultiProperties wxMpMultiProperties) { + Map appsMap = wxMpMultiProperties.getApps(); + if (appsMap == null || appsMap.isEmpty()) { + log.warn("微信公众号应用参数未配置,通过 WxMpMultiServices#getWxMpService(\"tenantId\")获取实例将返回空"); + return new WxMpMultiServicesImpl(); + } + + /** + * 校验 appId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。 + * + * 查看 {@link me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl#setAppId(String)} + */ + Collection apps = appsMap.values(); + if (apps.size() > 1) { + // 校验 appId 是否唯一 + boolean multi = apps.stream() + // 没有 appId,如果不判断是否为空,这里会报 NPE 异常 + .collect(Collectors.groupingBy(c -> c.getAppId() == null ? 0 : c.getAppId(), Collectors.counting())) + .entrySet().stream().anyMatch(e -> e.getValue() > 1); + if (multi) { + throw new RuntimeException("请确保微信公众号配置 appId 的唯一性"); + } + } + + // 根据配置选择多租户模式 + WxMpMultiProperties.MultiTenantMode mode = wxMpMultiProperties.getConfigStorage().getMultiTenantMode(); + if (mode == WxMpMultiProperties.MultiTenantMode.SHARED) { + return createSharedMultiServices(appsMap, wxMpMultiProperties); + } else { + return createIsolatedMultiServices(appsMap, wxMpMultiProperties); + } + } + + /** + * 创建隔离模式的多租户服务(每个租户独立 WxMpService 实例) + */ + private WxMpMultiServices createIsolatedMultiServices( + Map appsMap, + WxMpMultiProperties wxMpMultiProperties) { + + WxMpMultiServicesImpl services = new WxMpMultiServicesImpl(); + Set> entries = appsMap.entrySet(); + + for (Map.Entry entry : entries) { + String tenantId = entry.getKey(); + WxMpSingleProperties wxMpSingleProperties = entry.getValue(); + WxMpDefaultConfigImpl storage = this.wxMpConfigStorage(wxMpMultiProperties); + this.configApp(storage, wxMpSingleProperties); + this.configHttp(storage, wxMpMultiProperties.getConfigStorage()); + this.configHost(storage, wxMpMultiProperties.getHosts()); + WxMpService wxMpService = this.wxMpService(storage, wxMpMultiProperties); + services.addWxMpService(tenantId, wxMpService); + } + + log.info("微信公众号多租户服务初始化完成,使用隔离模式(ISOLATED),共配置 {} 个租户", appsMap.size()); + return services; + } + + /** + * 创建共享模式的多租户服务(单个 WxMpService 实例管理多个配置) + */ + private WxMpMultiServices createSharedMultiServices( + Map appsMap, + WxMpMultiProperties wxMpMultiProperties) { + + // 创建共享的 WxMpService 实例 + WxMpMultiProperties.ConfigStorage storage = wxMpMultiProperties.getConfigStorage(); + WxMpService sharedService = createWxMpServiceByType(storage.getHttpClientType()); + configureWxMpService(sharedService, storage); + + // 准备所有租户的配置,使用 TreeMap 保证顺序一致性 + Map configsMap = new HashMap<>(); + String defaultTenantId = new TreeMap<>(appsMap).firstKey(); + + for (Map.Entry entry : appsMap.entrySet()) { + String tenantId = entry.getKey(); + WxMpSingleProperties wxMpSingleProperties = entry.getValue(); + WxMpDefaultConfigImpl config = this.wxMpConfigStorage(wxMpMultiProperties); + this.configApp(config, wxMpSingleProperties); + this.configHttp(config, storage); + this.configHost(config, wxMpMultiProperties.getHosts()); + configsMap.put(tenantId, config); + } + + // 设置多配置到共享的 WxMpService + sharedService.setMultiConfigStorages(configsMap, defaultTenantId); + + log.info("微信公众号多租户服务初始化完成,使用共享模式(SHARED),共配置 {} 个租户,共享一个 HTTP 客户端", appsMap.size()); + return new WxMpMultiServicesSharedImpl(sharedService); + } + + /** + * 根据类型创建 WxMpService 实例 + */ + private WxMpService createWxMpServiceByType(WxMpMultiProperties.HttpClientType httpClientType) { + switch (httpClientType) { + case OK_HTTP: + return new WxMpServiceOkHttpImpl(); + case JODD_HTTP: + return new WxMpServiceJoddHttpImpl(); + case HTTP_CLIENT: + return new WxMpServiceHttpClientImpl(); + case HTTP_COMPONENTS: + return new WxMpServiceHttpComponentsImpl(); + default: + return new WxMpServiceImpl(); + } + } + + /** + * 配置 WxMpService 的通用参数 + */ + private void configureWxMpService(WxMpService wxMpService, WxMpMultiProperties.ConfigStorage storage) { + int maxRetryTimes = storage.getMaxRetryTimes(); + if (maxRetryTimes < 0) { + maxRetryTimes = 0; + } + int retrySleepMillis = storage.getRetrySleepMillis(); + if (retrySleepMillis < 0) { + retrySleepMillis = 1000; + } + wxMpService.setRetrySleepMillis(retrySleepMillis); + wxMpService.setMaxRetryTimes(maxRetryTimes); + } + + /** + * 配置 WxMpDefaultConfigImpl + * + * @param wxMpMultiProperties 参数 + * @return WxMpDefaultConfigImpl + */ + protected abstract WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxMpMultiProperties); + + public WxMpService wxMpService(WxMpConfigStorage configStorage, WxMpMultiProperties wxMpMultiProperties) { + WxMpMultiProperties.ConfigStorage storage = wxMpMultiProperties.getConfigStorage(); + WxMpService wxMpService = createWxMpServiceByType(storage.getHttpClientType()); + wxMpService.setWxMpConfigStorage(configStorage); + configureWxMpService(wxMpService, storage); + return wxMpService; + } + + private void configApp(WxMpDefaultConfigImpl config, WxMpSingleProperties corpProperties) { + String appId = corpProperties.getAppId(); + String appSecret = corpProperties.getAppSecret(); + String token = corpProperties.getToken(); + String aesKey = corpProperties.getAesKey(); + boolean useStableAccessToken = corpProperties.isUseStableAccessToken(); + + config.setAppId(appId); + config.setSecret(appSecret); + if (StringUtils.isNotBlank(token)) { + config.setToken(token); + } + if (StringUtils.isNotBlank(aesKey)) { + config.setAesKey(aesKey); + } + config.setUseStableAccessToken(useStableAccessToken); + } + + private void configHttp(WxMpDefaultConfigImpl config, WxMpMultiProperties.ConfigStorage storage) { + String httpProxyHost = storage.getHttpProxyHost(); + Integer httpProxyPort = storage.getHttpProxyPort(); + String httpProxyUsername = storage.getHttpProxyUsername(); + String httpProxyPassword = storage.getHttpProxyPassword(); + if (StringUtils.isNotBlank(httpProxyHost)) { + config.setHttpProxyHost(httpProxyHost); + if (httpProxyPort != null) { + config.setHttpProxyPort(httpProxyPort); + } + if (StringUtils.isNotBlank(httpProxyUsername)) { + config.setHttpProxyUsername(httpProxyUsername); + } + if (StringUtils.isNotBlank(httpProxyPassword)) { + config.setHttpProxyPassword(httpProxyPassword); + } + } + } + + /** + * wx host config + */ + private void configHost(WxMpDefaultConfigImpl config, WxMpMultiProperties.HostConfig hostConfig) { + if (hostConfig != null) { + String apiHost = hostConfig.getApiHost(); + String mpHost = hostConfig.getMpHost(); + String openHost = hostConfig.getOpenHost(); + WxMpHostConfig wxMpHostConfig = new WxMpHostConfig(); + wxMpHostConfig.setApiHost(StringUtils.isNotBlank(apiHost) ? apiHost : null); + wxMpHostConfig.setMpHost(StringUtils.isNotBlank(mpHost) ? mpHost : null); + wxMpHostConfig.setOpenHost(StringUtils.isNotBlank(openHost) ? openHost : null); + config.setHostConfig(wxMpHostConfig); + } + } +} diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInJedisConfiguration.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInJedisConfiguration.java new file mode 100644 index 0000000000..c137d0c087 --- /dev/null +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInJedisConfiguration.java @@ -0,0 +1,77 @@ +package com.binarywang.spring.starter.wxjava.mp.configuration.services; + +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpMultiProperties; +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpMultiRedisProperties; +import com.binarywang.spring.starter.wxjava.mp.service.WxMpMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.redis.JedisWxRedisOps; +import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; +import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * 自动装配基于 jedis 策略配置 + * + * @author yl + * created on 2024/1/23 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxMpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis" +) +@RequiredArgsConstructor +public class WxMpInJedisConfiguration extends AbstractWxMpConfiguration { + private final WxMpMultiProperties wxMpMultiProperties; + private final ApplicationContext applicationContext; + + @Bean + public WxMpMultiServices wxMpMultiServices() { + return this.wxMpMultiServices(wxMpMultiProperties); + } + + @Override + protected WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxMpMultiProperties) { + return this.configRedis(wxMpMultiProperties); + } + + private WxMpDefaultConfigImpl configRedis(WxMpMultiProperties wxMpMultiProperties) { + WxMpMultiRedisProperties wxMpMultiRedisProperties = wxMpMultiProperties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (wxMpMultiRedisProperties != null && StringUtils.isNotEmpty(wxMpMultiRedisProperties.getHost())) { + jedisPool = getJedisPool(wxMpMultiProperties); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + return new WxMpRedisConfigImpl(new JedisWxRedisOps(jedisPool), wxMpMultiProperties.getConfigStorage().getKeyPrefix()); + } + + private JedisPool getJedisPool(WxMpMultiProperties wxMpMultiProperties) { + WxMpMultiProperties.ConfigStorage storage = wxMpMultiProperties.getConfigStorage(); + WxMpMultiRedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), + redis.getTimeout(), redis.getPassword(), redis.getDatabase()); + } +} diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInMemoryConfiguration.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInMemoryConfiguration.java new file mode 100644 index 0000000000..cd90eba114 --- /dev/null +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInMemoryConfiguration.java @@ -0,0 +1,40 @@ +package com.binarywang.spring.starter.wxjava.mp.configuration.services; + +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpMultiProperties; +import com.binarywang.spring.starter.wxjava.mp.service.WxMpMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; +import me.chanjar.weixin.mp.config.impl.WxMpMapConfigImpl; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 自动装配基于内存策略配置 + * + * @author yl + * created on 2024/1/23 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxMpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "memory", matchIfMissing = true +) +@RequiredArgsConstructor +public class WxMpInMemoryConfiguration extends AbstractWxMpConfiguration { + private final WxMpMultiProperties wxMpMultiProperties; + + @Bean + public WxMpMultiServices wxMpMultiServices() { + return this.wxMpMultiServices(wxMpMultiProperties); + } + + @Override + protected WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxMpMultiProperties) { + return this.configInMemory(); + } + + private WxMpDefaultConfigImpl configInMemory() { + return new WxMpMapConfigImpl(); + // return new WxMpDefaultConfigImpl(); + } +} diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInRedisTemplateConfiguration.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInRedisTemplateConfiguration.java new file mode 100644 index 0000000000..fd96176a8a --- /dev/null +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInRedisTemplateConfiguration.java @@ -0,0 +1,45 @@ +package com.binarywang.spring.starter.wxjava.mp.configuration.services; + +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpMultiProperties; +import com.binarywang.spring.starter.wxjava.mp.service.WxMpMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps; +import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; +import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * 自动装配基于 redisTemplate 策略配置 + * + * @author yl + * created on 2024/1/23 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxMpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redis_template" +) +@RequiredArgsConstructor +public class WxMpInRedisTemplateConfiguration extends AbstractWxMpConfiguration { + private final WxMpMultiProperties WxMpMultiProperties; + private final ApplicationContext applicationContext; + + @Bean + public WxMpMultiServices wxMpMultiServices() { + return this.wxMpMultiServices(WxMpMultiProperties); + } + + @Override + protected WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxMpMultiProperties) { + return this.configRedisTemplate(WxMpMultiProperties); + } + + private WxMpDefaultConfigImpl configRedisTemplate(WxMpMultiProperties wxMpMultiProperties) { + StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class); + return new WxMpRedisConfigImpl(new RedisTemplateWxRedisOps(redisTemplate), + wxMpMultiProperties.getConfigStorage().getKeyPrefix()); + } +} diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInRedissonConfiguration.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInRedissonConfiguration.java new file mode 100644 index 0000000000..a2b606c4a6 --- /dev/null +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/configuration/services/WxMpInRedissonConfiguration.java @@ -0,0 +1,67 @@ +package com.binarywang.spring.starter.wxjava.mp.configuration.services; + +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpMultiProperties; +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpMultiRedisProperties; +import com.binarywang.spring.starter.wxjava.mp.service.WxMpMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; +import me.chanjar.weixin.mp.config.impl.WxMpRedissonConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 自动装配基于 redisson 策略配置 + * + * @author yl + * created on 2024/1/23 + */ +@Configuration +@ConditionalOnProperty( + prefix = WxMpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson" +) +@RequiredArgsConstructor +public class WxMpInRedissonConfiguration extends AbstractWxMpConfiguration { + private final WxMpMultiProperties wxMpMultiProperties; + private final ApplicationContext applicationContext; + + @Bean + public WxMpMultiServices wxMpMultiServices() { + return this.wxMpMultiServices(wxMpMultiProperties); + } + + @Override + protected WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxMpMultiProperties) { + return this.configRedisson(wxMpMultiProperties); + } + + private WxMpDefaultConfigImpl configRedisson(WxMpMultiProperties wxMpMultiProperties) { + WxMpMultiRedisProperties redisProperties = wxMpMultiProperties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + redissonClient = getRedissonClient(wxMpMultiProperties); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxMpRedissonConfigImpl(redissonClient, wxMpMultiProperties.getConfigStorage().getKeyPrefix()); + } + + private RedissonClient getRedissonClient(WxMpMultiProperties wxMpMultiProperties) { + WxMpMultiProperties.ConfigStorage storage = wxMpMultiProperties.getConfigStorage(); + WxMpMultiRedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redis.getHost() + ":" + redis.getPort()) + .setDatabase(redis.getDatabase()) + .setPassword(redis.getPassword()); + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpMultiProperties.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpMultiProperties.java new file mode 100644 index 0000000000..9dd95f9531 --- /dev/null +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpMultiProperties.java @@ -0,0 +1,182 @@ +package com.binarywang.spring.starter.wxjava.mp.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * @author yl + * created on 2024/1/23 + */ +@Data +@NoArgsConstructor +@ConfigurationProperties(WxMpMultiProperties.PREFIX) +public class WxMpMultiProperties implements Serializable { + private static final long serialVersionUID = -5358245184407791011L; + public static final String PREFIX = "wx.mp"; + + private Map apps = new HashMap<>(); + + /** + * 自定义host配置 + */ + private HostConfig hosts; + + /** + * 存储策略 + */ + private final ConfigStorage configStorage = new ConfigStorage(); + + @Data + @NoArgsConstructor + public static class HostConfig implements Serializable { + private static final long serialVersionUID = -4172767630740346001L; + + /** + * 对应于:https://api.weixin.qq.com + */ + private String apiHost; + + /** + * 对应于:https://open.weixin.qq.com + */ + private String openHost; + + /** + * 对应于:https://mp.weixin.qq.com + */ + private String mpHost; + } + + @Data + @NoArgsConstructor + public static class ConfigStorage implements Serializable { + private static final long serialVersionUID = 4815731027000065434L; + + /** + * 存储类型. + */ + private StorageType type = StorageType.MEMORY; + + /** + * 指定key前缀. + */ + private String keyPrefix = "wx:mp:multi"; + + /** + * redis连接配置. + */ + @NestedConfigurationProperty + private final WxMpMultiRedisProperties redis = new WxMpMultiRedisProperties(); + + /** + * http客户端类型. + */ + private HttpClientType httpClientType = HttpClientType.HTTP_CLIENT; + + /** + * http代理主机. + */ + private String httpProxyHost; + + /** + * http代理端口. + */ + private Integer httpProxyPort; + + /** + * http代理用户名. + */ + private String httpProxyUsername; + + /** + * http代理密码. + */ + private String httpProxyPassword; + + /** + * http 请求最大重试次数 + *
+     *   {@link me.chanjar.weixin.mp.api.WxMpService#setMaxRetryTimes(int)}
+     *   {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setMaxRetryTimes(int)}
+     * 
+ */ + private int maxRetryTimes = 5; + + /** + * http 请求重试间隔 + *
+     *   {@link me.chanjar.weixin.mp.api.WxMpService#setRetrySleepMillis(int)}
+     *   {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setRetrySleepMillis(int)}
+     * 
+ */ + private int retrySleepMillis = 1000; + + /** + * 多租户实现模式. + *
    + *
  • ISOLATED: 为每个租户创建独立的 WxMpService 实例(默认)
  • + *
  • SHARED: 使用单个 WxMpService 实例管理所有租户配置,共享 HTTP 客户端
  • + *
+ */ + private MultiTenantMode multiTenantMode = MultiTenantMode.ISOLATED; + } + + public enum StorageType { + /** + * 内存 + */ + MEMORY, + /** + * jedis + */ + JEDIS, + /** + * redisson + */ + REDISSON, + /** + * redisTemplate + */ + REDIS_TEMPLATE + } + + public enum HttpClientType { + /** + * HttpClient + */ + HTTP_CLIENT, + /** + * HttpComponents + */ + HTTP_COMPONENTS, + /** + * OkHttp + */ + OK_HTTP, + /** + * JoddHttp + */ + JODD_HTTP + } + + public enum MultiTenantMode { + /** + * 隔离模式:为每个租户创建独立的 WxMpService 实例. + * 优点:线程安全,不依赖 ThreadLocal + * 缺点:每个租户创建独立的 HTTP 客户端,资源占用较多 + */ + ISOLATED, + /** + * 共享模式:使用单个 WxMpService 实例管理所有租户配置. + * 优点:共享 HTTP 客户端,节省资源 + * 缺点:依赖 ThreadLocal 切换配置,异步场景需注意 + */ + SHARED + } +} diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpMultiRedisProperties.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpMultiRedisProperties.java new file mode 100644 index 0000000000..38cae8bdac --- /dev/null +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpMultiRedisProperties.java @@ -0,0 +1,56 @@ +package com.binarywang.spring.starter.wxjava.mp.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * @author yl + * created on 2024/1/23 + */ +@Data +@NoArgsConstructor +public class WxMpMultiRedisProperties implements Serializable { + private static final long serialVersionUID = -5924815351660074401L; + + /** + * 主机地址. + */ + private String host = "127.0.0.1"; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + /** + * sentinel ips + */ + private String sentinelIps; + + /** + * sentinel name + */ + private String sentinelName; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpSingleProperties.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpSingleProperties.java new file mode 100644 index 0000000000..6302784bf0 --- /dev/null +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpSingleProperties.java @@ -0,0 +1,40 @@ +package com.binarywang.spring.starter.wxjava.mp.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * @author yl + * created on 2024/1/23 + */ +@Data +@NoArgsConstructor +public class WxMpSingleProperties implements Serializable { + private static final long serialVersionUID = 1980986361098922525L; + /** + * 设置微信公众号的 appid. + */ + private String appId; + + /** + * 设置微信公众号的 app secret. + */ + private String appSecret; + + /** + * 设置微信公众号的 token. + */ + private String token; + + /** + * 设置微信公众号的 EncodingAESKey. + */ + private String aesKey; + + /** + * 是否使用稳定版 Access Token + */ + private boolean useStableAccessToken = false; +} diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServices.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServices.java new file mode 100644 index 0000000000..69122e5277 --- /dev/null +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServices.java @@ -0,0 +1,27 @@ +package com.binarywang.spring.starter.wxjava.mp.service; + + +import me.chanjar.weixin.mp.api.WxMpService; + +/** + * 企业微信 {@link WxMpService} 所有实例存放类. + * + * @author yl + * created on 2024/1/23 + */ +public interface WxMpMultiServices { + /** + * 通过租户 Id 获取 WxMpService + * + * @param tenantId 租户 Id + * @return WxMpService + */ + WxMpService getWxMpService(String tenantId); + + /** + * 根据租户 Id,从列表中移除一个 WxMpService 实例 + * + * @param tenantId 租户 Id + */ + void removeWxMpService(String tenantId); +} diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesImpl.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesImpl.java new file mode 100644 index 0000000000..e5f358abe2 --- /dev/null +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesImpl.java @@ -0,0 +1,36 @@ +package com.binarywang.spring.starter.wxjava.mp.service; + +import me.chanjar.weixin.mp.api.WxMpService; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 企业微信 {@link WxMpMultiServices} 默认实现 + * + * @author yl + * created on 2024/1/23 + */ +public class WxMpMultiServicesImpl implements WxMpMultiServices { + private final Map services = new ConcurrentHashMap<>(); + + @Override + public WxMpService getWxMpService(String tenantId) { + return this.services.get(tenantId); + } + + /** + * 根据租户 Id,添加一个 WxMpService 到列表 + * + * @param tenantId 租户 Id + * @param wxMpService WxMpService 实例 + */ + public void addWxMpService(String tenantId, WxMpService wxMpService) { + this.services.put(tenantId, wxMpService); + } + + @Override + public void removeWxMpService(String tenantId) { + this.services.remove(tenantId); + } +} diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java new file mode 100644 index 0000000000..ca9123c572 --- /dev/null +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/service/WxMpMultiServicesSharedImpl.java @@ -0,0 +1,53 @@ +package com.binarywang.spring.starter.wxjava.mp.service; + +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.mp.api.WxMpService; + +/** + * 微信公众号 {@link WxMpMultiServices} 共享式实现. + *

+ * 使用单个 WxMpService 实例管理多个租户配置,通过 switchover 切换租户。 + * 相比 {@link WxMpMultiServicesImpl},此实现共享 HTTP 客户端,节省资源。 + *

+ *

+ * 注意:由于使用 ThreadLocal 切换配置,在异步或多线程场景需要特别注意线程上下文切换。 + *

+ * + * @author Binary Wang + * created on 2026/1/9 + */ +@RequiredArgsConstructor +public class WxMpMultiServicesSharedImpl implements WxMpMultiServices { + private final WxMpService sharedWxMpService; + + @Override + public WxMpService getWxMpService(String tenantId) { + if (tenantId == null) { + return null; + } + // 使用 switchover 检查配置是否存在,保持与隔离模式 API 行为一致(不存在时返回 null) + if (!sharedWxMpService.switchover(tenantId)) { + return null; + } + return sharedWxMpService; + } + + @Override + public void removeWxMpService(String tenantId) { + if (tenantId != null) { + sharedWxMpService.removeConfigStorage(tenantId); + } + } + + /** + * 添加租户配置到共享的 WxMpService 实例 + * + * @param tenantId 租户 ID + * @param wxMpService 要添加配置的 WxMpService(仅使用其配置,不使用其实例) + */ + public void addWxMpService(String tenantId, WxMpService wxMpService) { + if (tenantId != null && wxMpService != null) { + sharedWxMpService.addConfigStorage(tenantId, wxMpService.getWxMpConfigStorage()); + } + } +} diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..d20dc22dc3 --- /dev/null +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.binarywang.spring.starter.wxjava.mp.autoconfigure.WxMpMultiAutoConfiguration diff --git a/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..324e3555ba --- /dev/null +++ b/spring-boot-starters/wx-java-mp-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.binarywang.spring.starter.wxjava.mp.autoconfigure.WxMpMultiAutoConfiguration diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md b/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md index ec7f343c62..091912cfad 100644 --- a/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md @@ -1,5 +1,7 @@ -# wx-java-mp-starter +# wx-java-mp-spring-boot-starter + ## 快速开始 + 1. 引入依赖 ```xml @@ -11,22 +13,34 @@ 2. 添加配置(application.properties) ```properties # 公众号配置(必填) - wx.mp.appId = @appId - wx.mp.secret = @secret - wx.mp.token = @token - wx.mp.aesKey = @aesKey - # 存储配置redis(可选) - wx.mp.config-storage.type = redis - wx.mp.config-storage.redis.host = 127.0.0.1 - wx.mp.config-storage.redis.port = 6379 + wx.mp.app-id=appId + wx.mp.secret=@secret + wx.mp.token=@token + wx.mp.aes-key=@aesKey + wx.mp.use-stable-access-token=@useStableAccessToken + # 存储配置redis(可选) + wx.mp.config-storage.type= edis # 配置类型: Memory(默认), Jedis, RedisTemplate + wx.mp.config-storage.key-prefix=wx # 相关redis前缀配置: wx(默认) + wx.mp.config-storage.redis.host=127.0.0.1 + wx.mp.config-storage.redis.port=6379 + #单机和sentinel同时存在时,优先使用sentinel配置 + #wx.mp.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379 + #wx.mp.config-storage.redis.sentinel-name=mymaster + # http客户端配置 + wx.mp.config-storage.http-client-type=HttpComponents # http客户端类型: HttpComponents(Apache HttpClient 5.x,推荐), HttpClient(Apache HttpClient 4.x), OkHttp, JoddHttp + wx.mp.config-storage.http-proxy-host= + wx.mp.config-storage.http-proxy-port= + wx.mp.config-storage.http-proxy-username= + wx.mp.config-storage.http-proxy-password= + # 公众号地址host配置 + #wx.mp.hosts.api-host=http://proxy.com/ + #wx.mp.hosts.open-host=http://proxy.com/ + #wx.mp.hosts.mp-host=http://proxy.com/ ``` -3. 支持自动注入的类型 - -`WxMpService`以及相关的服务类, 比如: `wxMpService.getXxxService`。 - - - - - +3. 自动注入的类型 +- `WxMpService` +- `WxMpConfigStorage` +4、参考demo: +https://github.com/binarywang/wx-java-mp-demo diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml index d45c1bd729..672cf2e35c 100644 --- a/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ wx-java-spring-boot-starters com.github.binarywang - 3.6.0 + 4.8.3.B 4.0.0 @@ -22,7 +22,32 @@ redis.clients jedis - compile + provided + + + org.springframework.data + spring-data-redis + provided + + + org.jodd + jodd-http + provided + + + com.squareup.okhttp3 + okhttp + provided + + + org.apache.httpcomponents.client5 + httpclient5 + provided + + + org.redisson + redisson + provided diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpAutoConfiguration.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpAutoConfiguration.java index e2b0a60d2b..b2e3848ab8 100644 --- a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpAutoConfiguration.java +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpAutoConfiguration.java @@ -12,6 +12,6 @@ */ @Configuration @EnableConfigurationProperties(WxMpProperties.class) -@Import({WxMpStorageAutoConfiguration.class, WxMpServiceAutoConfiguration.class}) +@Import({ WxMpStorageAutoConfiguration.class, WxMpServiceAutoConfiguration.class }) public class WxMpAutoConfiguration { } diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpServiceAutoConfiguration.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpServiceAutoConfiguration.java index 7d96226733..dc6dcafb82 100644 --- a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpServiceAutoConfiguration.java +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpServiceAutoConfiguration.java @@ -1,7 +1,13 @@ package com.binarywang.spring.starter.wxjava.mp.config; -import me.chanjar.weixin.mp.api.*; +import com.binarywang.spring.starter.wxjava.mp.enums.HttpClientType; +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; +import me.chanjar.weixin.mp.api.WxMpService; +import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpClientImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceHttpComponentsImpl; import me.chanjar.weixin.mp.api.impl.WxMpServiceImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceJoddHttpImpl; +import me.chanjar.weixin.mp.api.impl.WxMpServiceOkHttpImpl; import me.chanjar.weixin.mp.config.WxMpConfigStorage; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; @@ -17,115 +23,49 @@ public class WxMpServiceAutoConfiguration { @Bean @ConditionalOnMissingBean - public WxMpService wxMpService(WxMpConfigStorage configStorage) { - WxMpService wxMpService = new WxMpServiceImpl(); + public WxMpService wxMpService(WxMpConfigStorage configStorage, WxMpProperties wxMpProperties) { + HttpClientType httpClientType = wxMpProperties.getConfigStorage().getHttpClientType(); + WxMpService wxMpService; + switch (httpClientType) { + case OkHttp: + wxMpService = newWxMpServiceOkHttpImpl(); + break; + case JoddHttp: + wxMpService = newWxMpServiceJoddHttpImpl(); + break; + case HttpClient: + wxMpService = newWxMpServiceHttpClientImpl(); + break; + case HttpComponents: + wxMpService = newWxMpServiceHttpComponentsImpl(); + break; + default: + wxMpService = newWxMpServiceImpl(); + break; + } + wxMpService.setWxMpConfigStorage(configStorage); return wxMpService; } - @Bean - public WxMpKefuService wxMpKefuService(WxMpService wxMpService) { - return wxMpService.getKefuService(); - } - - @Bean - public WxMpMaterialService wxMpMaterialService(WxMpService wxMpService) { - return wxMpService.getMaterialService(); - } - - @Bean - public WxMpMenuService wxMpMenuService(WxMpService wxMpService) { - return wxMpService.getMenuService(); - } - - @Bean - public WxMpUserService wxMpUserService(WxMpService wxMpService) { - return wxMpService.getUserService(); - } - - @Bean - public WxMpUserTagService wxMpUserTagService(WxMpService wxMpService) { - return wxMpService.getUserTagService(); - } - - @Bean - public WxMpQrcodeService wxMpQrcodeService(WxMpService wxMpService) { - return wxMpService.getQrcodeService(); - } - - @Bean - public WxMpCardService wxMpCardService(WxMpService wxMpService) { - return wxMpService.getCardService(); - } - - @Bean - public WxMpDataCubeService wxMpDataCubeService(WxMpService wxMpService) { - return wxMpService.getDataCubeService(); - } - - @Bean - public WxMpUserBlacklistService wxMpUserBlacklistService(WxMpService wxMpService) { - return wxMpService.getBlackListService(); - } - - @Bean - public WxMpStoreService wxMpStoreService(WxMpService wxMpService) { - return wxMpService.getStoreService(); - } - - @Bean - public WxMpTemplateMsgService wxMpTemplateMsgService(WxMpService wxMpService) { - return wxMpService.getTemplateMsgService(); - } - - @Bean - public WxMpSubscribeMsgService wxMpSubscribeMsgService(WxMpService wxMpService) { - return wxMpService.getSubscribeMsgService(); + private WxMpService newWxMpServiceImpl() { + return new WxMpServiceImpl(); } - @Bean - public WxMpDeviceService wxMpDeviceService(WxMpService wxMpService) { - return wxMpService.getDeviceService(); + private WxMpService newWxMpServiceHttpClientImpl() { + return new WxMpServiceHttpClientImpl(); } - @Bean - public WxMpShakeService wxMpShakeService(WxMpService wxMpService) { - return wxMpService.getShakeService(); + private WxMpService newWxMpServiceOkHttpImpl() { + return new WxMpServiceOkHttpImpl(); } - @Bean - public WxMpMemberCardService wxMpMemberCardService(WxMpService wxMpService) { - return wxMpService.getMemberCardService(); - } - - @Bean - public WxMpMassMessageService wxMpMassMessageService(WxMpService wxMpService) { - return wxMpService.getMassMessageService(); + private WxMpService newWxMpServiceJoddHttpImpl() { + return new WxMpServiceJoddHttpImpl(); } - @Bean - public WxMpAiOpenService wxMpAiOpenService(WxMpService wxMpService) { - return wxMpService.getAiOpenService(); - } - - @Bean - public WxMpWifiService wxMpWifiService(WxMpService wxMpService) { - return wxMpService.getWifiService(); - } - - @Bean - public WxMpMarketingService wxMpMarketingService(WxMpService wxMpService) { - return wxMpService.getMarketingService(); - } - - @Bean - public WxMpCommentService wxMpCommentService(WxMpService wxMpService) { - return wxMpService.getCommentService(); - } - - @Bean - public WxMpOcrService wxMpOcrService(WxMpService wxMpService) { - return wxMpService.getOcrService(); + private WxMpService newWxMpServiceHttpComponentsImpl() { + return new WxMpServiceHttpComponentsImpl(); } } diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpStorageAutoConfiguration.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpStorageAutoConfiguration.java index 18024707a4..cab3cb17b2 100644 --- a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpStorageAutoConfiguration.java +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/WxMpStorageAutoConfiguration.java @@ -1,88 +1,26 @@ package com.binarywang.spring.starter.wxjava.mp.config; -import com.binarywang.spring.starter.wxjava.mp.properties.RedisProperties; -import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; +import com.binarywang.spring.starter.wxjava.mp.config.storage.WxMpInJedisConfigStorageConfiguration; +import com.binarywang.spring.starter.wxjava.mp.config.storage.WxMpInMemoryConfigStorageConfiguration; +import com.binarywang.spring.starter.wxjava.mp.config.storage.WxMpInRedisTemplateConfigStorageConfiguration; +import com.binarywang.spring.starter.wxjava.mp.config.storage.WxMpInRedissonConfigStorageConfiguration; import lombok.RequiredArgsConstructor; -import me.chanjar.weixin.mp.config.WxMpConfigStorage; -import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; -import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import redis.clients.jedis.JedisPool; -import redis.clients.jedis.JedisPoolConfig; +import org.springframework.context.annotation.Import; /** * 微信公众号存储策略自动配置. * - * @author someone + * @author Luo */ @Configuration +@Import({ + WxMpInMemoryConfigStorageConfiguration.class, + WxMpInJedisConfigStorageConfiguration.class, + WxMpInRedisTemplateConfigStorageConfiguration.class, + WxMpInRedissonConfigStorageConfiguration.class +}) @RequiredArgsConstructor public class WxMpStorageAutoConfiguration { - private final WxMpProperties properties; - @Autowired(required = false) - private JedisPool jedisPool; - - @Bean - @ConditionalOnMissingBean(WxMpConfigStorage.class) - public WxMpConfigStorage wxMpInMemoryConfigStorage() { - WxMpProperties.ConfigStorage storage = properties.getConfigStorage(); - WxMpProperties.StorageType type = storage.getType(); - - if (type == WxMpProperties.StorageType.redis) { - return getWxMpInRedisConfigStorage(); - } - return getWxMpInMemoryConfigStorage(); - } - - private WxMpDefaultConfigImpl getWxMpInMemoryConfigStorage() { - WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl(); - setWxMpInfo(config); - return config; - } - - private WxMpRedisConfigImpl getWxMpInRedisConfigStorage() { - JedisPool poolToUse = jedisPool; - if (poolToUse == null) { - poolToUse = getJedisPool(); - } - WxMpRedisConfigImpl config = new WxMpRedisConfigImpl(poolToUse); - setWxMpInfo(config); - return config; - } - - private void setWxMpInfo(WxMpDefaultConfigImpl config) { - config.setAppId(properties.getAppId()); - config.setSecret(properties.getSecret()); - config.setToken(properties.getToken()); - config.setAesKey(properties.getAesKey()); - } - - private JedisPool getJedisPool() { - WxMpProperties.ConfigStorage storage = properties.getConfigStorage(); - RedisProperties redis = storage.getRedis(); - - JedisPoolConfig config = new JedisPoolConfig(); - if (redis.getMaxActive() != null) { - config.setMaxTotal(redis.getMaxActive()); - } - if (redis.getMaxIdle() != null) { - config.setMaxIdle(redis.getMaxIdle()); - } - if (redis.getMaxWaitMillis() != null) { - config.setMaxWaitMillis(redis.getMaxWaitMillis()); - } - if (redis.getMinIdle() != null) { - config.setMinIdle(redis.getMinIdle()); - } - config.setTestOnBorrow(true); - config.setTestWhileIdle(true); - - JedisPool pool = new JedisPool(config, redis.getHost(), redis.getPort(), - redis.getTimeout(), redis.getPassword(), redis.getDatabase()); - return pool; - } } diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/AbstractWxMpConfigStorageConfiguration.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/AbstractWxMpConfigStorageConfiguration.java new file mode 100644 index 0000000000..e39a8bf4d9 --- /dev/null +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/AbstractWxMpConfigStorageConfiguration.java @@ -0,0 +1,54 @@ +package com.binarywang.spring.starter.wxjava.mp.config.storage; + +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; +import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; +import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder; +import me.chanjar.weixin.mp.config.WxMpHostConfig; +import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; +import org.apache.commons.lang3.StringUtils; + +/** + * @author zhangyl + */ +public abstract class AbstractWxMpConfigStorageConfiguration { + + protected WxMpDefaultConfigImpl config(WxMpDefaultConfigImpl config, WxMpProperties properties) { + config.setAppId(properties.getAppId()); + config.setSecret(properties.getSecret()); + config.setToken(properties.getToken()); + config.setAesKey(properties.getAesKey()); + config.setUseStableAccessToken(properties.isUseStableAccessToken()); + + WxMpProperties.ConfigStorage configStorageProperties = properties.getConfigStorage(); + config.setHttpProxyHost(configStorageProperties.getHttpProxyHost()); + config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername()); + config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword()); + if (configStorageProperties.getHttpProxyPort() != null) { + config.setHttpProxyPort(configStorageProperties.getHttpProxyPort()); + } + + // 设置自定义的 HttpClient 超时配置 + ApacheHttpClientBuilder clientBuilder = config.getApacheHttpClientBuilder(); + if (clientBuilder == null) { + clientBuilder = DefaultApacheHttpClientBuilder.get(); + } + if (clientBuilder instanceof DefaultApacheHttpClientBuilder) { + DefaultApacheHttpClientBuilder defaultBuilder = (DefaultApacheHttpClientBuilder) clientBuilder; + defaultBuilder.setConnectionTimeout(configStorageProperties.getConnectionTimeout()); + defaultBuilder.setSoTimeout(configStorageProperties.getSoTimeout()); + defaultBuilder.setConnectionRequestTimeout(configStorageProperties.getConnectionRequestTimeout()); + config.setApacheHttpClientBuilder(defaultBuilder); + } + + // wx host config + if (null != properties.getHosts() && StringUtils.isNotEmpty(properties.getHosts().getApiHost())) { + WxMpHostConfig hostConfig = new WxMpHostConfig(); + hostConfig.setApiHost(properties.getHosts().getApiHost()); + hostConfig.setOpenHost(properties.getHosts().getOpenHost()); + hostConfig.setMpHost(properties.getHosts().getMpHost()); + config.setHostConfig(hostConfig); + } + + return config; + } +} diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInJedisConfigStorageConfiguration.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInJedisConfigStorageConfiguration.java new file mode 100644 index 0000000000..c21418a6f6 --- /dev/null +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInJedisConfigStorageConfiguration.java @@ -0,0 +1,80 @@ +package com.binarywang.spring.starter.wxjava.mp.config.storage; + +import com.binarywang.spring.starter.wxjava.mp.properties.RedisProperties; +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.redis.JedisWxRedisOps; +import me.chanjar.weixin.common.redis.WxRedisOps; +import me.chanjar.weixin.mp.config.WxMpConfigStorage; +import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * @author zhangyl + */ +@Configuration +@ConditionalOnProperty( + prefix = WxMpProperties.PREFIX + ".config-storage", + name = "type", + havingValue = "jedis" +) +@ConditionalOnClass(Jedis.class) +@RequiredArgsConstructor +public class WxMpInJedisConfigStorageConfiguration extends AbstractWxMpConfigStorageConfiguration { + private final WxMpProperties properties; + private final ApplicationContext applicationContext; + + @Bean + @ConditionalOnMissingBean(WxMpConfigStorage.class) + public WxMpConfigStorage wxMpConfigStorage() { + WxMpRedisConfigImpl config = getWxMpRedisConfigImpl(); + return this.config(config, properties); + } + + private WxMpRedisConfigImpl getWxMpRedisConfigImpl() { + RedisProperties redisProperties = properties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + jedisPool = applicationContext.getBean("wxMpJedisPool", JedisPool.class); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + WxRedisOps redisOps = new JedisWxRedisOps(jedisPool); + return new WxMpRedisConfigImpl(redisOps, properties.getConfigStorage().getKeyPrefix()); + } + + @Bean + @ConditionalOnProperty(prefix = WxMpProperties.PREFIX + ".config-storage.redis", name = "host") + public JedisPool wxMpJedisPool() { + WxMpProperties.ConfigStorage storage = properties.getConfigStorage(); + RedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(), + redis.getDatabase()); + } +} diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInMemoryConfigStorageConfiguration.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInMemoryConfigStorageConfiguration.java new file mode 100644 index 0000000000..16eada73ae --- /dev/null +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInMemoryConfigStorageConfiguration.java @@ -0,0 +1,33 @@ +package com.binarywang.spring.starter.wxjava.mp.config.storage; + +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.mp.config.WxMpConfigStorage; +import me.chanjar.weixin.mp.config.impl.WxMpDefaultConfigImpl; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author zhangyl + */ +@Configuration +@ConditionalOnProperty( + prefix = WxMpProperties.PREFIX + ".config-storage", + name = "type", + havingValue = "memory", + matchIfMissing = true +) +@RequiredArgsConstructor +public class WxMpInMemoryConfigStorageConfiguration extends AbstractWxMpConfigStorageConfiguration { + private final WxMpProperties properties; + + @Bean + @ConditionalOnMissingBean(WxMpConfigStorage.class) + public WxMpConfigStorage wxMpConfigStorage() { + WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl(); + config(config, properties); + return config; + } +} diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInRedisTemplateConfigStorageConfiguration.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInRedisTemplateConfigStorageConfiguration.java new file mode 100644 index 0000000000..0305ca4f8e --- /dev/null +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInRedisTemplateConfigStorageConfiguration.java @@ -0,0 +1,46 @@ +package com.binarywang.spring.starter.wxjava.mp.config.storage; + +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps; +import me.chanjar.weixin.common.redis.WxRedisOps; +import me.chanjar.weixin.mp.config.WxMpConfigStorage; +import me.chanjar.weixin.mp.config.impl.WxMpRedisConfigImpl; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * @author zhangyl + */ +@Slf4j +@Configuration +@ConditionalOnProperty( + prefix = WxMpProperties.PREFIX + ".config-storage", + name = "type", + havingValue = "redistemplate" +) +@ConditionalOnClass(StringRedisTemplate.class) +@RequiredArgsConstructor +public class WxMpInRedisTemplateConfigStorageConfiguration extends AbstractWxMpConfigStorageConfiguration { + private final WxMpProperties properties; + private final ApplicationContext applicationContext; + + @Bean + @ConditionalOnMissingBean(WxMpConfigStorage.class) + public WxMpConfigStorage wxMpConfigStorage() { + WxMpRedisConfigImpl config = getWxMpInRedisTemplateConfigStorage(); + return this.config(config, properties); + } + + private WxMpRedisConfigImpl getWxMpInRedisTemplateConfigStorage() { + StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class); + WxRedisOps redisOps = new RedisTemplateWxRedisOps(redisTemplate); + return new WxMpRedisConfigImpl(redisOps, properties.getConfigStorage().getKeyPrefix()); + } +} diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java new file mode 100644 index 0000000000..75b736f53f --- /dev/null +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/config/storage/WxMpInRedissonConfigStorageConfiguration.java @@ -0,0 +1,69 @@ +package com.binarywang.spring.starter.wxjava.mp.config.storage; + +import com.binarywang.spring.starter.wxjava.mp.properties.RedisProperties; +import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.mp.config.WxMpConfigStorage; +import me.chanjar.weixin.mp.config.impl.WxMpRedissonConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author zhangyl + */ +@Configuration +@ConditionalOnProperty( + prefix = WxMpProperties.PREFIX + ".config-storage", + name = "type", + havingValue = "redisson" +) +@ConditionalOnClass({Redisson.class, RedissonClient.class}) +@RequiredArgsConstructor +public class WxMpInRedissonConfigStorageConfiguration extends AbstractWxMpConfigStorageConfiguration { + private final WxMpProperties properties; + private final ApplicationContext applicationContext; + + @Bean + @ConditionalOnMissingBean(WxMpConfigStorage.class) + public WxMpConfigStorage wxMpConfigStorage() { + WxMpRedissonConfigImpl config = getWxMpInRedissonConfigStorage(); + return this.config(config, properties); + } + + private WxMpRedissonConfigImpl getWxMpInRedissonConfigStorage() { + RedisProperties redisProperties = properties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + redissonClient = applicationContext.getBean("wxMpRedissonClient", RedissonClient.class); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxMpRedissonConfigImpl(redissonClient, properties.getConfigStorage().getKeyPrefix()); + } + + @Bean + @ConditionalOnProperty(prefix = WxMpProperties.PREFIX + ".config-storage.redis", name = "host") + public RedissonClient wxMpRedissonClient() { + WxMpProperties.ConfigStorage storage = properties.getConfigStorage(); + RedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redis.getHost() + ":" + redis.getPort()) + .setDatabase(redis.getDatabase()); + if (StringUtils.isNotBlank(redis.getPassword())) { + config.useSingleServer().setPassword(redis.getPassword()); + } + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/enums/HttpClientType.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/enums/HttpClientType.java new file mode 100644 index 0000000000..0bf034417f --- /dev/null +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/enums/HttpClientType.java @@ -0,0 +1,26 @@ +package com.binarywang.spring.starter.wxjava.mp.enums; + +/** + * httpclient类型. + * + * @author Binary Wang + * created on 2020-08-30 + */ +public enum HttpClientType { + /** + * HttpClient. + */ + HttpClient, + /** + * OkHttp. + */ + OkHttp, + /** + * JoddHttp. + */ + JoddHttp, + /** + * HttpComponents (Apache HttpClient 5.x). + */ + HttpComponents, +} diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/enums/StorageType.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/enums/StorageType.java new file mode 100644 index 0000000000..05ed6ce393 --- /dev/null +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/enums/StorageType.java @@ -0,0 +1,26 @@ +package com.binarywang.spring.starter.wxjava.mp.enums; + +/** + * storage类型. + * + * @author Binary Wang + * created on 2020-08-30 + */ +public enum StorageType { + /** + * 内存. + */ + Memory, + /** + * redis(JedisClient). + */ + Jedis, + /** + * redis(Redisson). + */ + Redisson, + /** + * redis(RedisTemplate). + */ + RedisTemplate +} diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/HostConfig.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/HostConfig.java new file mode 100644 index 0000000000..5b29400738 --- /dev/null +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/HostConfig.java @@ -0,0 +1,27 @@ +package com.binarywang.spring.starter.wxjava.mp.properties; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class HostConfig implements Serializable { + + private static final long serialVersionUID = -4172767630740346001L; + + /** + * 对应于:https://api.weixin.qq.com + */ + private String apiHost; + + /** + * 对应于:https://open.weixin.qq.com + */ + private String openHost; + + /** + * 对应于:https://mp.weixin.qq.com + */ + private String mpHost; + +} diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/RedisProperties.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/RedisProperties.java index d95e8df984..573c87630f 100644 --- a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/RedisProperties.java +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/RedisProperties.java @@ -5,9 +5,10 @@ import java.io.Serializable; /** - * Redis配置. + * redis 配置属性. * - * @author someone + * @author Binary Wang + * created on 2020-08-30 */ @Data public class RedisProperties implements Serializable { @@ -38,6 +39,16 @@ public class RedisProperties implements Serializable { */ private int database = 0; + /** + * sentinel ips + */ + private String sentinelIps; + + /** + * sentinel name + */ + private String sentinelName; + private Integer maxActive; private Integer maxIdle; private Integer maxWaitMillis; diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpProperties.java b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpProperties.java index 0dbca9e778..a6c6e3b6bd 100644 --- a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpProperties.java +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/mp/properties/WxMpProperties.java @@ -1,13 +1,15 @@ package com.binarywang.spring.starter.wxjava.mp.properties; +import com.binarywang.spring.starter.wxjava.mp.enums.HttpClientType; +import com.binarywang.spring.starter.wxjava.mp.enums.StorageType; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; import java.io.Serializable; +import static com.binarywang.spring.starter.wxjava.mp.enums.StorageType.Memory; import static com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties.PREFIX; -import static com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties.StorageType.memory; - /** * 微信接入相关配置属性. @@ -40,29 +42,81 @@ public class WxMpProperties { private String aesKey; /** - * 存储策略, memory, redis. + * 是否使用稳定版 Access Token */ - private ConfigStorage configStorage = new ConfigStorage(); + private boolean useStableAccessToken = false; + /** + * 自定义host配置 + */ + @NestedConfigurationProperty + private HostConfig hosts; + + /** + * 存储策略 + */ + private final ConfigStorage configStorage = new ConfigStorage(); @Data public static class ConfigStorage implements Serializable { private static final long serialVersionUID = 4815731027000065434L; - private StorageType type = memory; + /** + * 存储类型. + */ + private StorageType type = Memory; - private RedisProperties redis = new RedisProperties(); + /** + * 指定key前缀. + */ + private String keyPrefix = "wx"; - } + /** + * redis连接配置. + */ + @NestedConfigurationProperty + private final RedisProperties redis = new RedisProperties(); + + /** + * http客户端类型. + */ + private HttpClientType httpClientType = HttpClientType.HttpComponents; + + /** + * http代理主机. + */ + private String httpProxyHost; + + /** + * http代理端口. + */ + private Integer httpProxyPort; - public enum StorageType { /** - * 内存. + * http代理用户名. */ - memory, + private String httpProxyUsername; + + /** + * http代理密码. + */ + private String httpProxyPassword; + /** - * redis. + * 连接超时时间,单位毫秒 */ - redis + private int connectionTimeout = 5000; + + /** + * 读数据超时时间,即socketTimeout,单位毫秒 + */ + private int soTimeout = 5000; + + /** + * 从连接池获取链接的超时时间,单位毫秒 + */ + private int connectionRequestTimeout = 5000; + } + } diff --git a/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..cdffb05c9e --- /dev/null +++ b/spring-boot-starters/wx-java-mp-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.binarywang.spring.starter.wxjava.mp.config.WxMpAutoConfiguration diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/README.md b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/README.md new file mode 100644 index 0000000000..ab5afa5449 --- /dev/null +++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/README.md @@ -0,0 +1,98 @@ +# wx-java-open-multi-spring-boot-starter + +## 快速开始 + +1. 引入依赖 + ```xml + + com.github.binarywang + wx-java-open-multi-spring-boot-starter + ${version} + + ``` +2. 添加配置(application.properties) + ```properties + # 开放平台配置 + ## 应用 1 配置(必填) + wx.open.apps.tenantId1.app-id=appId + wx.open.apps.tenantId1.secret=@secret + ## 选填 + wx.open.apps.tenantId1.token=@token + wx.open.apps.tenantId1.aes-key=@aesKey + wx.open.apps.tenantId1.api-host-url=@apiHostUrl + wx.open.apps.tenantId1.access-token-url=@accessTokenUrl + ## 应用 2 配置(必填) + wx.open.apps.tenantId2.app-id=@appId + wx.open.apps.tenantId2.secret=@secret + ## 选填 + wx.open.apps.tenantId2.token=@token + wx.open.apps.tenantId2.aes-key=@aesKey + wx.open.apps.tenantId2.api-host-url=@apiHostUrl + wx.open.apps.tenantId2.access-token-url=@accessTokenUrl + + # ConfigStorage 配置(选填) + ## 配置类型: memory(默认), jedis, redisson, redistemplate + wx.open.config-storage.type=memory + ## 相关redis前缀配置: wx:open:multi(默认) + wx.open.config-storage.key-prefix=wx:open:multi + wx.open.config-storage.redis.host=127.0.0.1 + wx.open.config-storage.redis.port=6379 + ## 注意:当前版本暂不支持 sentinel 配置,以下配置仅作为预留 + # wx.open.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379 + # wx.open.config-storage.redis.sentinel-name=mymaster + + # http 客户端配置(选填) + wx.open.config-storage.http-proxy-host= + wx.open.config-storage.http-proxy-port= + wx.open.config-storage.http-proxy-username= + wx.open.config-storage.http-proxy-password= + ## 最大重试次数,默认:5 次,如果小于 0,则为 0 + wx.open.config-storage.max-retry-times=5 + ## 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000 + wx.open.config-storage.retry-sleep-millis=1000 + ## 连接超时时间,单位毫秒,默认:5000 + wx.open.config-storage.connection-timeout=5000 + ## 读数据超时时间,即socketTimeout,单位毫秒,默认:5000 + wx.open.config-storage.so-timeout=5000 + ## 从连接池获取链接的超时时间,单位毫秒,默认:5000 + wx.open.config-storage.connection-request-timeout=5000 + ``` +3. 自动注入的类型:`WxOpenMultiServices` + +4. 使用样例 + +```java +import com.binarywang.spring.starter.wxjava.open.service.WxOpenMultiServices; +import me.chanjar.weixin.open.api.WxOpenService; +import me.chanjar.weixin.open.api.WxOpenComponentService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class DemoService { + @Autowired + private WxOpenMultiServices wxOpenMultiServices; + + public void test() { + // 应用 1 的 WxOpenService + WxOpenService wxOpenService1 = wxOpenMultiServices.getWxOpenService("tenantId1"); + WxOpenComponentService componentService1 = wxOpenService1.getWxOpenComponentService(); + // todo ... + + // 应用 2 的 WxOpenService + WxOpenService wxOpenService2 = wxOpenMultiServices.getWxOpenService("tenantId2"); + WxOpenComponentService componentService2 = wxOpenService2.getWxOpenComponentService(); + // todo ... + + // 应用 3 的 WxOpenService + WxOpenService wxOpenService3 = wxOpenMultiServices.getWxOpenService("tenantId3"); + // 判断是否为空 + if (wxOpenService3 == null) { + // todo wxOpenService3 为空,请先配置 tenantId3 微信开放平台应用参数 + return; + } + WxOpenComponentService componentService3 = wxOpenService3.getWxOpenComponentService(); + // todo ... + } +} +``` diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml new file mode 100644 index 0000000000..dea66a5a35 --- /dev/null +++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml @@ -0,0 +1,62 @@ + + + + wx-java-spring-boot-starters + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-open-multi-spring-boot-starter + WxJava - Spring Boot Starter for WxOpen::支持多账号配置 + 微信开放平台开发的 Spring Boot Starter::支持多账号配置 + + + + com.github.binarywang + weixin-java-open + ${project.version} + + + redis.clients + jedis + provided + + + org.redisson + redisson + provided + + + org.springframework.data + spring-data-redis + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + + + diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/autoconfigure/WxOpenMultiAutoConfiguration.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/autoconfigure/WxOpenMultiAutoConfiguration.java new file mode 100644 index 0000000000..749130f517 --- /dev/null +++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/autoconfigure/WxOpenMultiAutoConfiguration.java @@ -0,0 +1,15 @@ +package com.binarywang.spring.starter.wxjava.open.autoconfigure; + +import com.binarywang.spring.starter.wxjava.open.configuration.WxOpenMultiServiceConfiguration; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * 微信开放平台多账号自动配置 + * + * @author Binary Wang + */ +@Configuration +@Import(WxOpenMultiServiceConfiguration.class) +public class WxOpenMultiAutoConfiguration { +} diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/WxOpenMultiServiceConfiguration.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/WxOpenMultiServiceConfiguration.java new file mode 100644 index 0000000000..e858185e30 --- /dev/null +++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/WxOpenMultiServiceConfiguration.java @@ -0,0 +1,26 @@ +package com.binarywang.spring.starter.wxjava.open.configuration; + +import com.binarywang.spring.starter.wxjava.open.configuration.services.WxOpenInJedisConfiguration; +import com.binarywang.spring.starter.wxjava.open.configuration.services.WxOpenInMemoryConfiguration; +import com.binarywang.spring.starter.wxjava.open.configuration.services.WxOpenInRedisTemplateConfiguration; +import com.binarywang.spring.starter.wxjava.open.configuration.services.WxOpenInRedissonConfiguration; +import com.binarywang.spring.starter.wxjava.open.properties.WxOpenMultiProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * 微信开放平台相关服务自动注册 + * + * @author Binary Wang + */ +@Configuration +@EnableConfigurationProperties(WxOpenMultiProperties.class) +@Import({ + WxOpenInJedisConfiguration.class, + WxOpenInMemoryConfiguration.class, + WxOpenInRedissonConfiguration.class, + WxOpenInRedisTemplateConfiguration.class +}) +public class WxOpenMultiServiceConfiguration { +} diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/AbstractWxOpenConfiguration.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/AbstractWxOpenConfiguration.java new file mode 100644 index 0000000000..0c63878783 --- /dev/null +++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/AbstractWxOpenConfiguration.java @@ -0,0 +1,153 @@ +package com.binarywang.spring.starter.wxjava.open.configuration.services; + +import com.binarywang.spring.starter.wxjava.open.properties.WxOpenMultiProperties; +import com.binarywang.spring.starter.wxjava.open.properties.WxOpenSingleProperties; +import com.binarywang.spring.starter.wxjava.open.service.WxOpenMultiServices; +import com.binarywang.spring.starter.wxjava.open.service.WxOpenMultiServicesImpl; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; +import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder; +import me.chanjar.weixin.open.api.WxOpenConfigStorage; +import me.chanjar.weixin.open.api.WxOpenService; +import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage; +import me.chanjar.weixin.open.api.impl.WxOpenServiceImpl; +import org.apache.commons.lang3.StringUtils; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * WxOpenConfigStorage 抽象配置类 + * + * @author Binary Wang + */ +@RequiredArgsConstructor +@Slf4j +public abstract class AbstractWxOpenConfiguration { + + protected WxOpenMultiServices wxOpenMultiServices(WxOpenMultiProperties wxOpenMultiProperties) { + Map appsMap = wxOpenMultiProperties.getApps(); + if (appsMap == null || appsMap.isEmpty()) { + log.warn("微信开放平台应用参数未配置,通过 WxOpenMultiServices#getWxOpenService(\"tenantId\")获取实例将返回空"); + return new WxOpenMultiServicesImpl(); + } + /** + * 校验 appId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。 + */ + Collection apps = appsMap.values(); + if (apps.size() > 1) { + // 校验 appId 是否唯一 + String nullAppIdPlaceholder = "__NULL_APP_ID__"; + boolean multi = apps.stream() + // 没有 appId,如果不判断是否为空,这里会报 NPE 异常 + .collect(Collectors.groupingBy(c -> c.getAppId() == null ? nullAppIdPlaceholder : c.getAppId(), Collectors.counting())) + .entrySet().stream().anyMatch(e -> e.getValue() > 1); + if (multi) { + throw new RuntimeException("请确保微信开放平台配置 appId 的唯一性"); + } + } + WxOpenMultiServicesImpl services = new WxOpenMultiServicesImpl(); + + Set> entries = appsMap.entrySet(); + for (Map.Entry entry : entries) { + String tenantId = entry.getKey(); + WxOpenSingleProperties wxOpenSingleProperties = entry.getValue(); + WxOpenInMemoryConfigStorage storage = this.wxOpenConfigStorage(wxOpenMultiProperties); + this.configApp(storage, wxOpenSingleProperties); + this.configHttp(storage, wxOpenMultiProperties.getConfigStorage()); + WxOpenService wxOpenService = this.wxOpenService(storage, wxOpenMultiProperties); + services.addWxOpenService(tenantId, wxOpenService); + } + return services; + } + + /** + * 配置 WxOpenInMemoryConfigStorage + * + * @param wxOpenMultiProperties 参数 + * @return WxOpenInMemoryConfigStorage + */ + protected abstract WxOpenInMemoryConfigStorage wxOpenConfigStorage(WxOpenMultiProperties wxOpenMultiProperties); + + public WxOpenService wxOpenService(WxOpenConfigStorage configStorage, WxOpenMultiProperties wxOpenMultiProperties) { + WxOpenService wxOpenService = new WxOpenServiceImpl(); + wxOpenService.setWxOpenConfigStorage(configStorage); + return wxOpenService; + } + + private void configApp(WxOpenInMemoryConfigStorage config, WxOpenSingleProperties appProperties) { + String appId = appProperties.getAppId(); + String secret = appProperties.getSecret(); + String token = appProperties.getToken(); + String aesKey = appProperties.getAesKey(); + String apiHostUrl = appProperties.getApiHostUrl(); + String accessTokenUrl = appProperties.getAccessTokenUrl(); + + // appId 和 secret 是必需的 + if (StringUtils.isBlank(appId)) { + throw new IllegalArgumentException("微信开放平台 appId 不能为空"); + } + if (StringUtils.isBlank(secret)) { + throw new IllegalArgumentException("微信开放平台 secret 不能为空"); + } + + config.setComponentAppId(appId); + config.setComponentAppSecret(secret); + if (StringUtils.isNotBlank(token)) { + config.setComponentToken(token); + } + if (StringUtils.isNotBlank(aesKey)) { + config.setComponentAesKey(aesKey); + } + // 设置URL配置 + config.setApiHostUrl(StringUtils.trimToNull(apiHostUrl)); + config.setAccessTokenUrl(StringUtils.trimToNull(accessTokenUrl)); + } + + private void configHttp(WxOpenInMemoryConfigStorage config, WxOpenMultiProperties.ConfigStorage storage) { + String httpProxyHost = storage.getHttpProxyHost(); + Integer httpProxyPort = storage.getHttpProxyPort(); + String httpProxyUsername = storage.getHttpProxyUsername(); + String httpProxyPassword = storage.getHttpProxyPassword(); + if (StringUtils.isNotBlank(httpProxyHost)) { + config.setHttpProxyHost(httpProxyHost); + if (httpProxyPort != null) { + config.setHttpProxyPort(httpProxyPort); + } + if (StringUtils.isNotBlank(httpProxyUsername)) { + config.setHttpProxyUsername(httpProxyUsername); + } + if (StringUtils.isNotBlank(httpProxyPassword)) { + config.setHttpProxyPassword(httpProxyPassword); + } + } + + // 设置重试配置 + int maxRetryTimes = storage.getMaxRetryTimes(); + if (maxRetryTimes < 0) { + maxRetryTimes = 0; + } + int retrySleepMillis = storage.getRetrySleepMillis(); + if (retrySleepMillis < 0) { + retrySleepMillis = 1000; + } + config.setRetrySleepMillis(retrySleepMillis); + config.setMaxRetryTimes(maxRetryTimes); + + // 设置自定义的HttpClient超时配置 + ApacheHttpClientBuilder clientBuilder = config.getApacheHttpClientBuilder(); + if (clientBuilder == null) { + clientBuilder = DefaultApacheHttpClientBuilder.get(); + } + if (clientBuilder instanceof DefaultApacheHttpClientBuilder) { + DefaultApacheHttpClientBuilder defaultBuilder = (DefaultApacheHttpClientBuilder) clientBuilder; + defaultBuilder.setConnectionTimeout(storage.getConnectionTimeout()); + defaultBuilder.setSoTimeout(storage.getSoTimeout()); + defaultBuilder.setConnectionRequestTimeout(storage.getConnectionRequestTimeout()); + config.setApacheHttpClientBuilder(defaultBuilder); + } + } +} diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInJedisConfiguration.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInJedisConfiguration.java new file mode 100644 index 0000000000..bb9577b99b --- /dev/null +++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInJedisConfiguration.java @@ -0,0 +1,78 @@ +package com.binarywang.spring.starter.wxjava.open.configuration.services; + +import com.binarywang.spring.starter.wxjava.open.properties.WxOpenMultiProperties; +import com.binarywang.spring.starter.wxjava.open.properties.WxOpenMultiRedisProperties; +import com.binarywang.spring.starter.wxjava.open.service.WxOpenMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage; +import me.chanjar.weixin.open.api.impl.WxOpenInRedisConfigStorage; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + + +/** + * 自动装配基于 jedis 策略配置 + * + * @author Binary Wang + */ +@Configuration +@ConditionalOnProperty( + prefix = WxOpenMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "JEDIS" +) +@ConditionalOnClass({JedisPool.class, JedisPoolConfig.class}) +@RequiredArgsConstructor +public class WxOpenInJedisConfiguration extends AbstractWxOpenConfiguration { + private final WxOpenMultiProperties wxOpenMultiProperties; + private final ApplicationContext applicationContext; + + @Bean + public WxOpenMultiServices wxOpenMultiServices() { + return this.wxOpenMultiServices(wxOpenMultiProperties); + } + + @Override + protected WxOpenInMemoryConfigStorage wxOpenConfigStorage(WxOpenMultiProperties wxOpenMultiProperties) { + return this.configJedis(wxOpenMultiProperties); + } + + private WxOpenInRedisConfigStorage configJedis(WxOpenMultiProperties wxOpenMultiProperties) { + WxOpenMultiRedisProperties redisProperties = wxOpenMultiProperties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + jedisPool = getJedisPool(wxOpenMultiProperties); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + return new WxOpenInRedisConfigStorage(jedisPool, wxOpenMultiProperties.getConfigStorage().getKeyPrefix()); + } + + private JedisPool getJedisPool(WxOpenMultiProperties wxOpenMultiProperties) { + WxOpenMultiProperties.ConfigStorage storage = wxOpenMultiProperties.getConfigStorage(); + WxOpenMultiRedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), + redis.getTimeout(), redis.getPassword(), redis.getDatabase()); + } +} diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInMemoryConfiguration.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInMemoryConfiguration.java new file mode 100644 index 0000000000..f7448a0875 --- /dev/null +++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInMemoryConfiguration.java @@ -0,0 +1,33 @@ +package com.binarywang.spring.starter.wxjava.open.configuration.services; + +import com.binarywang.spring.starter.wxjava.open.properties.WxOpenMultiProperties; +import com.binarywang.spring.starter.wxjava.open.service.WxOpenMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 自动装配基于内存策略配置 + * + * @author someone + */ +@Configuration +@ConditionalOnProperty( + prefix = WxOpenMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "MEMORY", matchIfMissing = true +) +@RequiredArgsConstructor +public class WxOpenInMemoryConfiguration extends AbstractWxOpenConfiguration { + private final WxOpenMultiProperties wxOpenMultiProperties; + + @Bean + public WxOpenMultiServices wxOpenMultiServices() { + return this.wxOpenMultiServices(wxOpenMultiProperties); + } + + @Override + protected WxOpenInMemoryConfigStorage wxOpenConfigStorage(WxOpenMultiProperties wxOpenMultiProperties) { + return new WxOpenInMemoryConfigStorage(); + } +} diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInRedisTemplateConfiguration.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInRedisTemplateConfiguration.java new file mode 100644 index 0000000000..6208c90fe5 --- /dev/null +++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInRedisTemplateConfiguration.java @@ -0,0 +1,44 @@ +package com.binarywang.spring.starter.wxjava.open.configuration.services; + +import com.binarywang.spring.starter.wxjava.open.properties.WxOpenMultiProperties; +import com.binarywang.spring.starter.wxjava.open.service.WxOpenMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage; +import me.chanjar.weixin.open.api.impl.WxOpenInRedisTemplateConfigStorage; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * 自动装配基于 redis template 策略配置 + * + * @author Binary Wang + */ +@Configuration +@ConditionalOnProperty( + prefix = WxOpenMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redistemplate" +) +@ConditionalOnClass(StringRedisTemplate.class) +@RequiredArgsConstructor +public class WxOpenInRedisTemplateConfiguration extends AbstractWxOpenConfiguration { + private final WxOpenMultiProperties wxOpenMultiProperties; + private final ApplicationContext applicationContext; + + @Bean + public WxOpenMultiServices wxOpenMultiServices() { + return this.wxOpenMultiServices(wxOpenMultiProperties); + } + + @Override + protected WxOpenInMemoryConfigStorage wxOpenConfigStorage(WxOpenMultiProperties wxOpenMultiProperties) { + return this.configRedisTemplate(); + } + + private WxOpenInRedisTemplateConfigStorage configRedisTemplate() { + StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class); + return new WxOpenInRedisTemplateConfigStorage(redisTemplate, wxOpenMultiProperties.getConfigStorage().getKeyPrefix()); + } +} diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInRedissonConfiguration.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInRedissonConfiguration.java new file mode 100644 index 0000000000..97569f3baf --- /dev/null +++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/configuration/services/WxOpenInRedissonConfiguration.java @@ -0,0 +1,68 @@ +package com.binarywang.spring.starter.wxjava.open.configuration.services; + +import com.binarywang.spring.starter.wxjava.open.properties.WxOpenMultiProperties; +import com.binarywang.spring.starter.wxjava.open.properties.WxOpenMultiRedisProperties; +import com.binarywang.spring.starter.wxjava.open.service.WxOpenMultiServices; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage; +import me.chanjar.weixin.open.api.impl.WxOpenInRedissonConfigStorage; +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 自动装配基于 redisson 策略配置 + * + * @author Binary Wang + */ +@Configuration +@ConditionalOnProperty( + prefix = WxOpenMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson" +) +@ConditionalOnClass({Redisson.class, RedissonClient.class}) +@RequiredArgsConstructor +public class WxOpenInRedissonConfiguration extends AbstractWxOpenConfiguration { + private final WxOpenMultiProperties wxOpenMultiProperties; + private final ApplicationContext applicationContext; + + @Bean + public WxOpenMultiServices wxOpenMultiServices() { + return this.wxOpenMultiServices(wxOpenMultiProperties); + } + + @Override + protected WxOpenInMemoryConfigStorage wxOpenConfigStorage(WxOpenMultiProperties wxOpenMultiProperties) { + return this.configRedisson(wxOpenMultiProperties); + } + + private WxOpenInRedissonConfigStorage configRedisson(WxOpenMultiProperties wxOpenMultiProperties) { + WxOpenMultiRedisProperties redisProperties = wxOpenMultiProperties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + redissonClient = getRedissonClient(wxOpenMultiProperties); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxOpenInRedissonConfigStorage(redissonClient, wxOpenMultiProperties.getConfigStorage().getKeyPrefix()); + } + + private RedissonClient getRedissonClient(WxOpenMultiProperties wxOpenMultiProperties) { + WxOpenMultiProperties.ConfigStorage storage = wxOpenMultiProperties.getConfigStorage(); + WxOpenMultiRedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redis.getHost() + ":" + redis.getPort()) + .setDatabase(redis.getDatabase()) + .setPassword(redis.getPassword()); + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenMultiProperties.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenMultiProperties.java new file mode 100644 index 0000000000..95e5b66712 --- /dev/null +++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenMultiProperties.java @@ -0,0 +1,125 @@ +package com.binarywang.spring.starter.wxjava.open.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * 微信开放平台多账号配置属性. + * + * @author Binary Wang + */ +@Data +@NoArgsConstructor +@ConfigurationProperties(WxOpenMultiProperties.PREFIX) +public class WxOpenMultiProperties implements Serializable { + private static final long serialVersionUID = -5358245184407791011L; + public static final String PREFIX = "wx.open"; + + private Map apps = new HashMap<>(); + + /** + * 存储策略 + */ + private final ConfigStorage configStorage = new ConfigStorage(); + + @Data + @NoArgsConstructor + public static class ConfigStorage implements Serializable { + private static final long serialVersionUID = 4815731027000065434L; + + /** + * 存储类型. + */ + private StorageType type = StorageType.memory; + + /** + * 指定key前缀. + */ + private String keyPrefix = "wx:open:multi"; + + /** + * redis连接配置. + */ + @NestedConfigurationProperty + private final WxOpenMultiRedisProperties redis = new WxOpenMultiRedisProperties(); + + /** + * http代理主机. + */ + private String httpProxyHost; + + /** + * http代理端口. + */ + private Integer httpProxyPort; + + /** + * http代理用户名. + */ + private String httpProxyUsername; + + /** + * http代理密码. + */ + private String httpProxyPassword; + + /** + * http 请求最大重试次数 + *
+     *   {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setMaxRetryTimes(int)}
+     *   {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setMaxRetryTimes(int)}
+     * 
+ */ + private int maxRetryTimes = 5; + + /** + * http 请求重试间隔 + *
+     *   {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setRetrySleepMillis(int)}
+     *   {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setRetrySleepMillis(int)}
+     * 
+ */ + private int retrySleepMillis = 1000; + + /** + * 连接超时时间,单位毫秒 + */ + private int connectionTimeout = 5000; + + /** + * 读数据超时时间,即socketTimeout,单位毫秒 + */ + private int soTimeout = 5000; + + /** + * 从连接池获取链接的超时时间,单位毫秒 + */ + private int connectionRequestTimeout = 5000; + } + + public enum StorageType { + /** + * 内存 + */ + memory, + /** + * jedis + */ + jedis, + /** + * redisson + */ + redisson, + /** + * redisTemplate + */ + redistemplate + } + +} diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenMultiRedisProperties.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenMultiRedisProperties.java new file mode 100644 index 0000000000..ae6d5368d7 --- /dev/null +++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenMultiRedisProperties.java @@ -0,0 +1,57 @@ +package com.binarywang.spring.starter.wxjava.open.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 微信开放平台多账号Redis配置. + * + * @author Binary Wang + */ +@Data +@NoArgsConstructor +public class WxOpenMultiRedisProperties implements Serializable { + private static final long serialVersionUID = -5924815351660074401L; + + /** + * 主机地址. + */ + private String host = "127.0.0.1"; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + /** + * sentinel ips + */ + private String sentinelIps; + + /** + * sentinel name + */ + private String sentinelName; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenSingleProperties.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenSingleProperties.java new file mode 100644 index 0000000000..116da323dc --- /dev/null +++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenSingleProperties.java @@ -0,0 +1,49 @@ +package com.binarywang.spring.starter.wxjava.open.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 微信开放平台单个应用配置. + * + * @author Binary Wang + */ +@Data +@NoArgsConstructor +public class WxOpenSingleProperties implements Serializable { + private static final long serialVersionUID = 1980986361098922525L; + + /** + * 设置微信开放平台的appid. + */ + private String appId; + + /** + * 设置微信开放平台的app secret. + */ + private String secret; + + /** + * 设置微信开放平台的token. + */ + private String token; + + /** + * 设置微信开放平台的EncodingAESKey. + */ + private String aesKey; + + /** + * 自定义API主机地址,用于替换默认的 https://api.weixin.qq.com + * 例如:http://proxy.company.com:8080 + */ + private String apiHostUrl; + + /** + * 自定义获取AccessToken地址,用于向自定义统一服务获取AccessToken + * 例如:http://proxy.company.com:8080/oauth/token + */ + private String accessTokenUrl; +} diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/service/WxOpenMultiServices.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/service/WxOpenMultiServices.java new file mode 100644 index 0000000000..9228071a10 --- /dev/null +++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/service/WxOpenMultiServices.java @@ -0,0 +1,26 @@ +package com.binarywang.spring.starter.wxjava.open.service; + + +import me.chanjar.weixin.open.api.WxOpenService; + +/** + * 微信开放平台 {@link WxOpenService} 所有实例存放类. + * + * @author binarywang + */ +public interface WxOpenMultiServices { + /** + * 通过租户 Id 获取 WxOpenService + * + * @param tenantId 租户 Id + * @return WxOpenService + */ + WxOpenService getWxOpenService(String tenantId); + + /** + * 根据租户 Id,从列表中移除一个 WxOpenService 实例 + * + * @param tenantId 租户 Id + */ + void removeWxOpenService(String tenantId); +} diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/service/WxOpenMultiServicesImpl.java b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/service/WxOpenMultiServicesImpl.java new file mode 100644 index 0000000000..76fb139e6c --- /dev/null +++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/service/WxOpenMultiServicesImpl.java @@ -0,0 +1,35 @@ +package com.binarywang.spring.starter.wxjava.open.service; + +import me.chanjar.weixin.open.api.WxOpenService; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 微信开放平台 {@link WxOpenMultiServices} 默认实现 + * + * @author Binary Wang + */ +public class WxOpenMultiServicesImpl implements WxOpenMultiServices { + private final Map services = new ConcurrentHashMap<>(); + + @Override + public WxOpenService getWxOpenService(String tenantId) { + return this.services.get(tenantId); + } + + /** + * 根据租户 Id,添加一个 WxOpenService 到列表 + * + * @param tenantId 租户 Id + * @param wxOpenService WxOpenService 实例 + */ + public void addWxOpenService(String tenantId, WxOpenService wxOpenService) { + this.services.put(tenantId, wxOpenService); + } + + @Override + public void removeWxOpenService(String tenantId) { + this.services.remove(tenantId); + } +} diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..a61d0018db --- /dev/null +++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.binarywang.spring.starter.wxjava.open.autoconfigure.WxOpenMultiAutoConfiguration \ No newline at end of file diff --git a/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..ddc66af02c --- /dev/null +++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.binarywang.spring.starter.wxjava.open.autoconfigure.WxOpenMultiAutoConfiguration \ No newline at end of file diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/README.md b/spring-boot-starters/wx-java-open-spring-boot-starter/README.md index fd00f03531..12650ac931 100644 --- a/spring-boot-starters/wx-java-open-spring-boot-starter/README.md +++ b/spring-boot-starters/wx-java-open-spring-boot-starter/README.md @@ -9,26 +9,31 @@
``` 2. 添加配置(application.properties) - ``` - # 开放平台配置(必填) - wx.open.appId = @appId - wx.open.secret = @secret - wx.open.token = @token - wx.open.aesKey = @aesKey - # 存储配置redis(可选), 优先使用(wx.open.config-storage.redis)配置的redis, 支持自定注入的JedisPool - wx.open.config-storage.type = redis # 可选值, memory(默认), redis - wx.open.config-storage.redis.host = 127.0.0.1 - wx.open.config-storage.redis.port = 6379 + ```properties + # 公众号配置(必填) + wx.open.appId = appId + wx.open.secret = @secret + wx.open.token = @token + wx.open.aesKey = @aesKey + # 存储配置redis(可选) + # 优先注入容器的(JedisPool, RedissonClient), 当配置了wx.open.config-storage.redis.host, 不会使用容器注入redis连接配置 + wx.open.config-storage.type = redis # 配置类型: memory(默认), redis(jedis), jedis, redisson, redistemplate + wx.open.config-storage.key-prefix = wx # 相关redis前缀配置: wx(默认) + wx.open.config-storage.redis.host = 127.0.0.1 + wx.open.config-storage.redis.port = 6379 + # http客户端配置 + wx.open.config-storage.http-client-type=httpclient # http客户端类型: httpclient(默认) + wx.open.config-storage.http-proxy-host= + wx.open.config-storage.http-proxy-port= + wx.open.config-storage.http-proxy-username= + wx.open.config-storage.http-proxy-password= + # 最大重试次数,默认:5 次,如果小于 0,则为 0 + wx.open.config-storage.max-retry-times=5 + # 重试时间间隔步进,默认:1000 毫秒,如果小于 0,则为 1000 + wx.open.config-storage.retry-sleep-millis=1000 ``` 3. 支持自动注入的类型: `WxOpenService, WxOpenMessageRouter, WxOpenComponentService` 4. 覆盖自动配置: 自定义注入的bean会覆盖自动注入的 - WxOpenConfigStorage - WxOpenService - - - - - - - diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml index 52ab7e3da5..22dbd864df 100644 --- a/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ wx-java-spring-boot-starters com.github.binarywang - 3.6.0 + 4.8.3.B 4.0.0 @@ -22,7 +22,14 @@ redis.clients jedis - compile + + + org.redisson + redisson + + + org.springframework.data + spring-data-redis diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenAutoConfiguration.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenAutoConfiguration.java index 0f7ecf3e8c..724d4a2f80 100644 --- a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenAutoConfiguration.java +++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenAutoConfiguration.java @@ -12,6 +12,9 @@ */ @Configuration @EnableConfigurationProperties(WxOpenProperties.class) -@Import({WxOpenStorageAutoConfiguration.class, WxOpenServiceAutoConfiguration.class}) +@Import({ + WxOpenStorageAutoConfiguration.class, + WxOpenServiceAutoConfiguration.class +}) public class WxOpenAutoConfiguration { } diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenServiceAutoConfiguration.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenServiceAutoConfiguration.java index a211486840..e532f3c160 100644 --- a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenServiceAutoConfiguration.java +++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenServiceAutoConfiguration.java @@ -5,6 +5,7 @@ import me.chanjar.weixin.open.api.WxOpenService; import me.chanjar.weixin.open.api.impl.WxOpenMessageRouter; import me.chanjar.weixin.open.api.impl.WxOpenServiceImpl; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -19,13 +20,15 @@ public class WxOpenServiceAutoConfiguration { @Bean @ConditionalOnMissingBean - public WxOpenService wxOpenService(WxOpenConfigStorage configStorage) { + @ConditionalOnBean(WxOpenConfigStorage.class) + public WxOpenService wxOpenService(WxOpenConfigStorage wxOpenConfigStorage) { WxOpenService wxOpenService = new WxOpenServiceImpl(); - wxOpenService.setWxOpenConfigStorage(configStorage); + wxOpenService.setWxOpenConfigStorage(wxOpenConfigStorage); return wxOpenService; } @Bean + @ConditionalOnMissingBean public WxOpenMessageRouter wxOpenMessageRouter(WxOpenService wxOpenService) { return new WxOpenMessageRouter(wxOpenService); } @@ -34,6 +37,4 @@ public WxOpenMessageRouter wxOpenMessageRouter(WxOpenService wxOpenService) { public WxOpenComponentService wxOpenComponentService(WxOpenService wxOpenService) { return wxOpenService.getWxOpenComponentService(); } - - } diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenStorageAutoConfiguration.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenStorageAutoConfiguration.java index 5b57551973..efbefbe0a1 100644 --- a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenStorageAutoConfiguration.java +++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/WxOpenStorageAutoConfiguration.java @@ -1,19 +1,11 @@ package com.binarywang.spring.starter.wxjava.open.config; -import com.binarywang.spring.starter.wxjava.open.properties.RedisProperties; -import com.binarywang.spring.starter.wxjava.open.properties.WxOpenProperties; -import lombok.RequiredArgsConstructor; -import me.chanjar.weixin.open.api.WxOpenConfigStorage; -import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage; -import me.chanjar.weixin.open.api.impl.WxOpenInRedisConfigStorage; -import org.apache.commons.lang3.StringUtils; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.context.annotation.Bean; +import com.binarywang.spring.starter.wxjava.open.config.storage.WxOpenInJedisConfigStorageConfiguration; +import com.binarywang.spring.starter.wxjava.open.config.storage.WxOpenInMemoryConfigStorageConfiguration; +import com.binarywang.spring.starter.wxjava.open.config.storage.WxOpenInRedisTemplateConfigStorageConfiguration; +import com.binarywang.spring.starter.wxjava.open.config.storage.WxOpenInRedissonConfigStorageConfiguration; import org.springframework.context.annotation.Configuration; -import redis.clients.jedis.JedisPool; -import redis.clients.jedis.JedisPoolConfig; +import org.springframework.context.annotation.Import; /** * 微信公众号存储策略自动配置. @@ -21,73 +13,11 @@ * @author someone */ @Configuration -@RequiredArgsConstructor +@Import({ + WxOpenInMemoryConfigStorageConfiguration.class, + WxOpenInRedisTemplateConfigStorageConfiguration.class, + WxOpenInJedisConfigStorageConfiguration.class, + WxOpenInRedissonConfigStorageConfiguration.class +}) public class WxOpenStorageAutoConfiguration { - private final WxOpenProperties properties; - - @Autowired(required = false) - private JedisPool jedisPool; - - @Value("${wx.open.config-storage.redis.host:}") - private String redisHost; - - @Bean - @ConditionalOnMissingBean(WxOpenConfigStorage.class) - public WxOpenConfigStorage wxOpenConfigStorage() { - WxOpenProperties.ConfigStorage storage = properties.getConfigStorage(); - WxOpenProperties.StorageType type = storage.getType(); - - if (type == WxOpenProperties.StorageType.redis) { - return getWxOpenInRedisConfigStorage(); - } - return getWxOpenInMemoryConfigStorage(); - } - - private WxOpenInMemoryConfigStorage getWxOpenInMemoryConfigStorage() { - WxOpenInMemoryConfigStorage config = new WxOpenInMemoryConfigStorage(); - setWxOpenInfo(config); - return config; - } - - private WxOpenInRedisConfigStorage getWxOpenInRedisConfigStorage() { - JedisPool poolToUse = jedisPool; - if (jedisPool == null || StringUtils.isNotEmpty(redisHost)) { - poolToUse = getJedisPool(); - } - WxOpenInRedisConfigStorage config = new WxOpenInRedisConfigStorage(poolToUse); - setWxOpenInfo(config); - return config; - } - - private void setWxOpenInfo(WxOpenConfigStorage config) { - config.setComponentAppId(properties.getAppId()); - config.setComponentAppSecret(properties.getSecret()); - config.setComponentToken(properties.getToken()); - config.setComponentAesKey(properties.getAesKey()); - } - - private JedisPool getJedisPool() { - WxOpenProperties.ConfigStorage storage = properties.getConfigStorage(); - RedisProperties redis = storage.getRedis(); - - JedisPoolConfig config = new JedisPoolConfig(); - if (redis.getMaxActive() != null) { - config.setMaxTotal(redis.getMaxActive()); - } - if (redis.getMaxIdle() != null) { - config.setMaxIdle(redis.getMaxIdle()); - } - if (redis.getMaxWaitMillis() != null) { - config.setMaxWaitMillis(redis.getMaxWaitMillis()); - } - if (redis.getMinIdle() != null) { - config.setMinIdle(redis.getMinIdle()); - } - config.setTestOnBorrow(true); - config.setTestWhileIdle(true); - - JedisPool pool = new JedisPool(config, redis.getHost(), redis.getPort(), - redis.getTimeout(), redis.getPassword(), redis.getDatabase()); - return pool; - } } diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/AbstractWxOpenConfigStorageConfiguration.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/AbstractWxOpenConfigStorageConfiguration.java new file mode 100644 index 0000000000..91db545ab9 --- /dev/null +++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/AbstractWxOpenConfigStorageConfiguration.java @@ -0,0 +1,54 @@ +package com.binarywang.spring.starter.wxjava.open.config.storage; + +import com.binarywang.spring.starter.wxjava.open.properties.WxOpenProperties; +import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; +import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder; +import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage; +import org.apache.commons.lang3.StringUtils; + +/** + * @author yl + */ +public abstract class AbstractWxOpenConfigStorageConfiguration { + + protected WxOpenInMemoryConfigStorage config(WxOpenInMemoryConfigStorage config, WxOpenProperties properties) { + WxOpenProperties.ConfigStorage storage = properties.getConfigStorage(); + config.setWxOpenInfo(properties.getAppId(), properties.getSecret(), properties.getToken(), properties.getAesKey()); + config.setHttpProxyHost(storage.getHttpProxyHost()); + config.setHttpProxyUsername(storage.getHttpProxyUsername()); + config.setHttpProxyPassword(storage.getHttpProxyPassword()); + Integer httpProxyPort = storage.getHttpProxyPort(); + if (httpProxyPort != null) { + config.setHttpProxyPort(httpProxyPort); + } + int maxRetryTimes = storage.getMaxRetryTimes(); + if (maxRetryTimes < 0) { + maxRetryTimes = 0; + } + int retrySleepMillis = storage.getRetrySleepMillis(); + if (retrySleepMillis < 0) { + retrySleepMillis = 1000; + } + config.setRetrySleepMillis(retrySleepMillis); + config.setMaxRetryTimes(maxRetryTimes); + + // 设置URL配置 + config.setApiHostUrl(StringUtils.trimToNull(properties.getApiHostUrl())); + config.setAccessTokenUrl(StringUtils.trimToNull(properties.getAccessTokenUrl())); + + // 设置自定义的HttpClient超时配置 + ApacheHttpClientBuilder clientBuilder = config.getApacheHttpClientBuilder(); + if (clientBuilder == null) { + clientBuilder = DefaultApacheHttpClientBuilder.get(); + } + if (clientBuilder instanceof DefaultApacheHttpClientBuilder) { + DefaultApacheHttpClientBuilder defaultBuilder = (DefaultApacheHttpClientBuilder) clientBuilder; + defaultBuilder.setConnectionTimeout(storage.getConnectionTimeout()); + defaultBuilder.setSoTimeout(storage.getSoTimeout()); + defaultBuilder.setConnectionRequestTimeout(storage.getConnectionRequestTimeout()); + config.setApacheHttpClientBuilder(defaultBuilder); + } + + return config; + } +} diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInJedisConfigStorageConfiguration.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInJedisConfigStorageConfiguration.java new file mode 100644 index 0000000000..73a0183d72 --- /dev/null +++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInJedisConfigStorageConfiguration.java @@ -0,0 +1,73 @@ +package com.binarywang.spring.starter.wxjava.open.config.storage; + +import com.binarywang.spring.starter.wxjava.open.properties.WxOpenProperties; +import com.binarywang.spring.starter.wxjava.open.properties.WxOpenRedisProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.open.api.WxOpenConfigStorage; +import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage; +import me.chanjar.weixin.open.api.impl.WxOpenInRedisConfigStorage; +import org.apache.commons.lang3.StringUtils; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +/** + * @author yl + */ +@Configuration +@ConditionalOnProperty( + prefix = WxOpenProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis" +) +@ConditionalOnClass({JedisPool.class, JedisPoolConfig.class}) +@RequiredArgsConstructor +public class WxOpenInJedisConfigStorageConfiguration extends AbstractWxOpenConfigStorageConfiguration { + private final WxOpenProperties properties; + private final ApplicationContext applicationContext; + + @Bean + @ConditionalOnMissingBean(WxOpenConfigStorage.class) + public WxOpenConfigStorage wxOpenConfigStorage() { + WxOpenInMemoryConfigStorage config = getWxOpenInRedisConfigStorage(); + return this.config(config, properties); + } + + private WxOpenInRedisConfigStorage getWxOpenInRedisConfigStorage() { + WxOpenRedisProperties wxOpenRedisProperties = properties.getConfigStorage().getRedis(); + JedisPool jedisPool; + if (wxOpenRedisProperties != null && StringUtils.isNotEmpty(wxOpenRedisProperties.getHost())) { + jedisPool = getJedisPool(); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + return new WxOpenInRedisConfigStorage(jedisPool, properties.getConfigStorage().getKeyPrefix()); + } + + private JedisPool getJedisPool() { + WxOpenProperties.ConfigStorage storage = properties.getConfigStorage(); + WxOpenRedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + + return new JedisPool(config, redis.getHost(), redis.getPort(), + redis.getTimeout(), redis.getPassword(), redis.getDatabase()); + } +} diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInMemoryConfigStorageConfiguration.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInMemoryConfigStorageConfiguration.java new file mode 100644 index 0000000000..ef17905493 --- /dev/null +++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInMemoryConfigStorageConfiguration.java @@ -0,0 +1,30 @@ +package com.binarywang.spring.starter.wxjava.open.config.storage; + +import com.binarywang.spring.starter.wxjava.open.properties.WxOpenProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.open.api.WxOpenConfigStorage; +import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author yl + */ +@Configuration +@ConditionalOnProperty( + prefix = WxOpenProperties.PREFIX + ".config-storage", name = "type", + matchIfMissing = true, havingValue = "memory" +) +@RequiredArgsConstructor +public class WxOpenInMemoryConfigStorageConfiguration extends AbstractWxOpenConfigStorageConfiguration { + private final WxOpenProperties properties; + + @Bean + @ConditionalOnMissingBean(WxOpenConfigStorage.class) + public WxOpenConfigStorage wxOpenConfigStorage() { + WxOpenInMemoryConfigStorage config = new WxOpenInMemoryConfigStorage(); + return this.config(config, properties); + } +} diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInRedisTemplateConfigStorageConfiguration.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInRedisTemplateConfigStorageConfiguration.java new file mode 100644 index 0000000000..79521c921a --- /dev/null +++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInRedisTemplateConfigStorageConfiguration.java @@ -0,0 +1,41 @@ +package com.binarywang.spring.starter.wxjava.open.config.storage; + +import com.binarywang.spring.starter.wxjava.open.properties.WxOpenProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.open.api.WxOpenConfigStorage; +import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage; +import me.chanjar.weixin.open.api.impl.WxOpenInRedisTemplateConfigStorage; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * @author yl + */ +@Configuration +@ConditionalOnProperty( + prefix = WxOpenProperties.PREFIX + ".config-storage", name = "type", havingValue = "redistemplate" +) +@ConditionalOnClass(StringRedisTemplate.class) +@RequiredArgsConstructor +public class WxOpenInRedisTemplateConfigStorageConfiguration extends AbstractWxOpenConfigStorageConfiguration { + private final WxOpenProperties properties; + private final ApplicationContext applicationContext; + + @Bean + @ConditionalOnMissingBean(WxOpenConfigStorage.class) + public WxOpenConfigStorage wxOpenConfigStorage() { + WxOpenInMemoryConfigStorage config = getWxOpenInRedisTemplateConfigStorage(); + return this.config(config, properties); + } + + private WxOpenInRedisTemplateConfigStorage getWxOpenInRedisTemplateConfigStorage() { + StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class); + return new WxOpenInRedisTemplateConfigStorage(redisTemplate, properties.getConfigStorage().getKeyPrefix()); + } +} diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInRedissonConfigStorageConfiguration.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInRedissonConfigStorageConfiguration.java new file mode 100644 index 0000000000..ea1dce3670 --- /dev/null +++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/config/storage/WxOpenInRedissonConfigStorageConfiguration.java @@ -0,0 +1,64 @@ +package com.binarywang.spring.starter.wxjava.open.config.storage; + +import com.binarywang.spring.starter.wxjava.open.properties.WxOpenProperties; +import com.binarywang.spring.starter.wxjava.open.properties.WxOpenRedisProperties; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.open.api.WxOpenConfigStorage; +import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage; +import me.chanjar.weixin.open.api.impl.WxOpenInRedissonConfigStorage; +import org.apache.commons.lang3.StringUtils; +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author yl + */ +@Configuration +@ConditionalOnProperty( + prefix = WxOpenProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson" +) +@ConditionalOnClass({Redisson.class, RedissonClient.class}) +@RequiredArgsConstructor +public class WxOpenInRedissonConfigStorageConfiguration extends AbstractWxOpenConfigStorageConfiguration { + private final WxOpenProperties properties; + private final ApplicationContext applicationContext; + + @Bean + @ConditionalOnMissingBean(WxOpenConfigStorage.class) + public WxOpenConfigStorage wxOpenConfigStorage() { + WxOpenInMemoryConfigStorage config = getWxOpenInRedissonConfigStorage(); + return this.config(config, properties); + } + + private WxOpenInRedissonConfigStorage getWxOpenInRedissonConfigStorage() { + WxOpenRedisProperties wxOpenRedisProperties = properties.getConfigStorage().getRedis(); + RedissonClient redissonClient; + if (wxOpenRedisProperties != null && StringUtils.isNotEmpty(wxOpenRedisProperties.getHost())) { + redissonClient = getRedissonClient(); + } else { + redissonClient = applicationContext.getBean(RedissonClient.class); + } + return new WxOpenInRedissonConfigStorage(redissonClient, properties.getConfigStorage().getKeyPrefix()); + } + + private RedissonClient getRedissonClient() { + WxOpenProperties.ConfigStorage storage = properties.getConfigStorage(); + WxOpenRedisProperties redis = storage.getRedis(); + + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://" + redis.getHost() + ":" + redis.getPort()) + .setDatabase(redis.getDatabase()) + .setPassword(redis.getPassword()); + config.setTransportMode(TransportMode.NIO); + return Redisson.create(config); + } +} diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/RedisProperties.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/RedisProperties.java deleted file mode 100644 index 565afa07f5..0000000000 --- a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/RedisProperties.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.binarywang.spring.starter.wxjava.open.properties; - -import lombok.Data; - -import java.io.Serializable; - -/** - * Redis配置. - * - * @author someone - */ -@Data -public class RedisProperties implements Serializable { - private static final long serialVersionUID = -5924815351660074401L; - - /** - * 主机地址. - */ - private String host = "127.0.0.1"; - - /** - * 端口号. - */ - private int port = 6379; - - /** - * 密码. - */ - private String password; - - /** - * 超时. - */ - private int timeout = 2000; - - /** - * 数据库. - */ - private int database = 0; - - private Integer maxActive; - private Integer maxIdle; - private Integer maxWaitMillis; - private Integer minIdle; -} diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenProperties.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenProperties.java index 64cc3d0672..248c6eedf6 100644 --- a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenProperties.java +++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenProperties.java @@ -2,6 +2,7 @@ import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; import java.io.Serializable; @@ -40,7 +41,19 @@ public class WxOpenProperties { private String aesKey; /** - * 存储策略, memory, redis. + * 自定义API主机地址,用于替换默认的 https://api.weixin.qq.com + * 例如:http://proxy.company.com:8080 + */ + private String apiHostUrl; + + /** + * 自定义获取AccessToken地址,用于向自定义统一服务获取AccessToken + * 例如:http://proxy.company.com:8080/oauth/token + */ + private String accessTokenUrl; + + /** + * 存储策略. */ private ConfigStorage configStorage = new ConfigStorage(); @@ -49,9 +62,78 @@ public class WxOpenProperties { public static class ConfigStorage implements Serializable { private static final long serialVersionUID = 4815731027000065434L; + /** + * 存储类型. + */ private StorageType type = memory; - private RedisProperties redis = new RedisProperties(); + /** + * 指定key前缀. + */ + private String keyPrefix = "wx:open"; + + /** + * redis连接配置. + */ + @NestedConfigurationProperty + private WxOpenRedisProperties redis = new WxOpenRedisProperties(); + + /** + * http客户端类型. + */ + private HttpClientType httpClientType = HttpClientType.httpclient; + + /** + * http代理主机. + */ + private String httpProxyHost; + + /** + * http代理端口. + */ + private Integer httpProxyPort; + + /** + * http代理用户名. + */ + private String httpProxyUsername; + + /** + * http代理密码. + */ + private String httpProxyPassword; + + /** + * http 请求重试间隔 + *
+     *   {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setRetrySleepMillis(int)}
+     *   {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setRetrySleepMillis(int)}
+     * 
+ */ + private int retrySleepMillis = 1000; + /** + * http 请求最大重试次数 + *
+     *   {@link me.chanjar.weixin.mp.api.impl.BaseWxMpServiceImpl#setMaxRetryTimes(int)}
+     *   {@link cn.binarywang.wx.miniapp.api.impl.BaseWxMaServiceImpl#setMaxRetryTimes(int)}
+     * 
+ */ + private int maxRetryTimes = 5; + + /** + * 连接超时时间,单位毫秒 + */ + private int connectionTimeout = 5000; + + /** + * 读数据超时时间,即socketTimeout,单位毫秒 + */ + private int soTimeout = 5000; + + /** + * 从连接池获取链接的超时时间,单位毫秒 + */ + private int connectionRequestTimeout = 5000; } @@ -61,8 +143,23 @@ public enum StorageType { */ memory, /** - * redis. + * jedis. + */ + jedis, + /** + * redisson. + */ + redisson, + /** + * redistemplate + */ + redistemplate + } + + public enum HttpClientType { + /** + * HttpClient. */ - redis + httpclient } } diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenRedisProperties.java b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenRedisProperties.java new file mode 100644 index 0000000000..0aafc73da6 --- /dev/null +++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenRedisProperties.java @@ -0,0 +1,45 @@ +package com.binarywang.spring.starter.wxjava.open.properties; + +import lombok.Data; + +import java.io.Serializable; + +/** + * Redis配置. + * + * @author someone + */ +@Data +public class WxOpenRedisProperties implements Serializable { + private static final long serialVersionUID = -5924815351660074401L; + + /** + * 主机地址. + */ + private String host; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/resources/META-INF/spring.factories index d46458f9db..0e5975cf18 100644 --- a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/resources/META-INF/spring.factories +++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -1 +1,2 @@ -org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.binarywang.spring.starter.wxjava.open.config.WxOpenAutoConfiguration +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.binarywang.spring.starter.wxjava.open.config.WxOpenAutoConfiguration diff --git a/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..ce327ba462 --- /dev/null +++ b/spring-boot-starters/wx-java-open-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.binarywang.spring.starter.wxjava.open.config.WxOpenAutoConfiguration diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/README.md b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/README.md new file mode 100644 index 0000000000..1ae4ac6299 --- /dev/null +++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/README.md @@ -0,0 +1,317 @@ +# wx-java-pay-multi-spring-boot-starter + +## 快速开始 + +本starter支持微信支付多公众号关联配置,适用于以下场景: +- 一个服务商需要为多个公众号提供支付服务 +- 一个系统需要支持多个公众号的支付业务 +- 需要根据不同的appId动态切换支付配置 + +## 使用说明 + +### 1. 引入依赖 + +在项目的 `pom.xml` 中添加以下依赖: + +```xml + + com.github.binarywang + wx-java-pay-multi-spring-boot-starter + ${version} + +``` + +### 2. 添加配置 + +在 `application.yml` 或 `application.properties` 中配置多个公众号的支付信息。 + +#### 配置示例(application.yml) + +##### V2版本配置 +```yml +wx: + pay: + configs: + # 配置1 - 可以使用appId作为key + wx1234567890abcdef: + appId: wx1234567890abcdef + mchId: 1234567890 + mchKey: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + keyPath: classpath:cert/app1/apiclient_cert.p12 + notifyUrl: https://example.com/pay/notify + # 配置2 - 也可以使用自定义标识作为key + config2: + appId: wx9876543210fedcba + mchId: 9876543210 + mchKey: yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy + keyPath: classpath:cert/app2/apiclient_cert.p12 + notifyUrl: https://example.com/pay/notify +``` + +##### V3版本配置 +```yml +wx: + pay: + configs: + # 公众号1配置 + wx1234567890abcdef: + appId: wx1234567890abcdef + mchId: 1234567890 + apiV3Key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + certSerialNo: 62C6CEAA360BCxxxxxxxxxxxxxxx + privateKeyPath: classpath:cert/app1/apiclient_key.pem + privateCertPath: classpath:cert/app1/apiclient_cert.pem + notifyUrl: https://example.com/pay/notify + # 公众号2配置 + wx9876543210fedcba: + appId: wx9876543210fedcba + mchId: 9876543210 + apiV3Key: yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy + certSerialNo: 73D7DFBB471CDxxxxxxxxxxxxxxx + privateKeyPath: classpath:cert/app2/apiclient_key.pem + privateCertPath: classpath:cert/app2/apiclient_cert.pem + notifyUrl: https://example.com/pay/notify +``` + +##### V3服务商版本配置 +```yml +wx: + pay: + configs: + # 服务商为公众号1提供服务 + config1: + appId: wxe97b2x9c2b3d # 服务商appId + mchId: 16486610 # 服务商商户号 + subAppId: wx118cexxe3c07679 # 子商户公众号appId + subMchId: 16496705 # 子商户号 + apiV3Key: Dc1DBwSc094jAKDGR5aqqb7PTHr + privateKeyPath: classpath:cert/apiclient_key.pem + privateCertPath: classpath:cert/apiclient_cert.pem + # 服务商为公众号2提供服务 + config2: + appId: wxe97b2x9c2b3d # 服务商appId(可以相同) + mchId: 16486610 # 服务商商户号(可以相同) + subAppId: wx228dexxf4d18890 # 子商户公众号appId(不同) + subMchId: 16496706 # 子商户号(不同) + apiV3Key: Dc1DBwSc094jAKDGR5aqqb7PTHr + privateKeyPath: classpath:cert/apiclient_key.pem + privateCertPath: classpath:cert/apiclient_cert.pem +``` + +#### 配置示例(application.properties) + +```properties +# 公众号1配置 +wx.pay.configs.wx1234567890abcdef.app-id=wx1234567890abcdef +wx.pay.configs.wx1234567890abcdef.mch-id=1234567890 +wx.pay.configs.wx1234567890abcdef.api-v3-key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +wx.pay.configs.wx1234567890abcdef.cert-serial-no=62C6CEAA360BCxxxxxxxxxxxxxxx +wx.pay.configs.wx1234567890abcdef.private-key-path=classpath:cert/app1/apiclient_key.pem +wx.pay.configs.wx1234567890abcdef.private-cert-path=classpath:cert/app1/apiclient_cert.pem +wx.pay.configs.wx1234567890abcdef.notify-url=https://example.com/pay/notify + +# 公众号2配置 +wx.pay.configs.wx9876543210fedcba.app-id=wx9876543210fedcba +wx.pay.configs.wx9876543210fedcba.mch-id=9876543210 +wx.pay.configs.wx9876543210fedcba.api-v3-key=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy +wx.pay.configs.wx9876543210fedcba.cert-serial-no=73D7DFBB471CDxxxxxxxxxxxxxxx +wx.pay.configs.wx9876543210fedcba.private-key-path=classpath:cert/app2/apiclient_key.pem +wx.pay.configs.wx9876543210fedcba.private-cert-path=classpath:cert/app2/apiclient_cert.pem +wx.pay.configs.wx9876543210fedcba.notify-url=https://example.com/pay/notify +``` + +### 3. 使用示例 + +自动注入的类型:`WxPayMultiServices` + +```java +import com.binarywang.spring.starter.wxjava.pay.service.WxPayMultiServices; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; +import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryV3Result; +import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result; +import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; +import com.github.binarywang.wxpay.service.WxPayService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class PayService { + @Autowired + private WxPayMultiServices wxPayMultiServices; + + /** + * 为不同的公众号创建支付订单 + * + * @param configKey 配置标识(即 wx.pay.configs.<configKey> 中的 key,可以是 appId 或自定义标识) + */ + public void createOrder(String configKey, String openId, Integer totalFee, String body) throws Exception { + // 根据配置标识获取对应的WxPayService + WxPayService wxPayService = wxPayMultiServices.getWxPayService(configKey); + + if (wxPayService == null) { + throw new IllegalArgumentException("未找到配置标识对应的微信支付配置: " + configKey); + } + + // 使用WxPayService进行支付操作 + WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); + request.setOutTradeNo(generateOutTradeNo()); + request.setDescription(body); + request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(totalFee)); + request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(openId)); + request.setNotifyUrl(wxPayService.getConfig().getNotifyUrl()); + + // V3统一下单 + WxPayUnifiedOrderV3Result.JsapiResult result = + wxPayService.createOrderV3(TradeTypeEnum.JSAPI, request); + + // 返回给前端用于调起支付 + // ... + } + + /** + * 服务商模式示例 + */ + public void serviceProviderExample(String configKey) throws Exception { + // 使用配置标识获取WxPayService + WxPayService wxPayService = wxPayMultiServices.getWxPayService(configKey); + + if (wxPayService == null) { + throw new IllegalArgumentException("未找到配置: " + configKey); + } + + // 获取子商户的配置信息 + String subAppId = wxPayService.getConfig().getSubAppId(); + String subMchId = wxPayService.getConfig().getSubMchId(); + + // 进行支付操作 + // ... + } + + /** + * 查询订单示例 + * + * @param configKey 配置标识(即 wx.pay.configs.<configKey> 中的 key) + */ + public void queryOrder(String configKey, String outTradeNo) throws Exception { + WxPayService wxPayService = wxPayMultiServices.getWxPayService(configKey); + + if (wxPayService == null) { + throw new IllegalArgumentException("未找到配置标识对应的微信支付配置: " + configKey); + } + + // 查询订单 + WxPayOrderQueryV3Result result = wxPayService.queryOrderV3(null, outTradeNo); + // 处理查询结果 + // ... + } + + private String generateOutTradeNo() { + // 生成商户订单号 + return "ORDER_" + System.currentTimeMillis(); + } +} +``` + +### 4. 配置说明 + +#### 必填配置项 + +| 配置项 | 说明 | 示例 | +|--------|------|------| +| appId | 公众号或小程序的appId | wx1234567890abcdef | +| mchId | 商户号 | 1234567890 | + +#### V2版本配置项 + +| 配置项 | 说明 | 是否必填 | +|--------|------|----------| +| mchKey | 商户密钥 | 是(V2) | +| keyPath | p12证书文件路径 | 部分接口需要 | + +#### V3版本配置项 + +| 配置项 | 说明 | 是否必填 | +|--------|------|----------| +| apiV3Key | API V3密钥 | 是(V3) | +| certSerialNo | 证书序列号 | 是(V3) | +| privateKeyPath | apiclient_key.pem路径 | 是(V3) | +| privateCertPath | apiclient_cert.pem路径 | 是(V3) | + +#### 服务商模式配置项 + +| 配置项 | 说明 | 是否必填 | +|--------|------|----------| +| subAppId | 子商户公众号appId | 服务商模式必填 | +| subMchId | 子商户号 | 服务商模式必填 | + +#### 可选配置项 + +| 配置项 | 说明 | 默认值 | +|--------|------|--------| +| notifyUrl | 支付结果通知URL | 无 | +| refundNotifyUrl | 退款结果通知URL | 无 | +| serviceId | 微信支付分serviceId | 无 | +| payScoreNotifyUrl | 支付分回调地址 | 无 | +| payScorePermissionNotifyUrl | 支付分授权回调地址 | 无 | +| useSandboxEnv | 是否使用沙箱环境 | false | +| apiHostUrl | 自定义API主机地址 | https://api.mch.weixin.qq.com | +| apiHostUrlPath | 自定义API主机路径前缀(代理入口前缀) | 空 | +| strictlyNeedWechatPaySerial | 是否所有V3请求都添加序列号头 | true | +| fullPublicKeyModel | 是否完全使用公钥模式 | true | +| publicKeyId | 公钥ID | 无 | +| publicKeyPath | 公钥文件路径 | 无 | + +## 常见问题 + +### 1. 如何选择配置的key? + +配置的key(即 `wx.pay.configs.` 中的 `` 部分)可以自由选择: +- 可以使用appId作为key(如 `wx.pay.configs.wx1234567890abcdef`),这样调用 `getWxPayService("wx1234567890abcdef")` 时就像直接用 appId 获取服务 +- 可以使用自定义标识(如 `wx.pay.configs.config1`),调用时使用 `getWxPayService("config1")` + +**注意**:`getWxPayService(configKey)` 方法的参数是配置文件中定义的 key,而不是 appId。只有当你使用 appId 作为配置 key 时,才能直接传入 appId。 + +### 2. V2和V3配置可以混用吗? + +可以。不同的配置可以使用不同的版本,例如: +```yml +wx: + pay: + configs: + app1: # V2配置 + appId: wx111 + mchId: 111 + mchKey: xxx + app2: # V3配置 + appId: wx222 + mchId: 222 + apiV3Key: yyy + privateKeyPath: xxx +``` + +### 3. 证书文件如何放置? + +证书文件可以放在以下位置: +- `src/main/resources` 目录下,使用 `classpath:` 前缀 +- 服务器绝对路径,直接填写完整路径 +- 建议为不同配置使用不同的目录组织证书 + +### 4. 服务商模式如何配置? + +服务商模式需要同时配置服务商信息和子商户信息: +- `appId` 和 `mchId` 填写服务商的信息 +- `subAppId` 和 `subMchId` 填写子商户的信息 + +## 注意事项 + +1. **配置安全**:生产环境中的密钥、证书等敏感信息,建议使用配置中心或环境变量管理 +2. **证书管理**:不同公众号的证书文件要分开存放,避免混淆 +3. **懒加载**:WxPayService 实例采用懒加载策略,只有在首次调用时才会创建 +4. **线程安全**:WxPayMultiServices 的实现是线程安全的 +5. **配置更新**:如需动态更新配置,可调用 `removeWxPayService(configKey)` 方法移除缓存的实例 + +## 更多信息 + +- [WxJava 项目首页](https://github.com/Wechat-Group/WxJava) +- [微信支付V2文档](https://pay.weixin.qq.com/doc/v2) +- [微信支付V3接口文档](https://pay.weixin.qq.com/doc/v3/merchant/4012062524) diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/pom.xml new file mode 100644 index 0000000000..c416b5ba40 --- /dev/null +++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/pom.xml @@ -0,0 +1,53 @@ + + + + wx-java-spring-boot-starters + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-pay-multi-spring-boot-starter + WxJava - Spring Boot Starter for Pay::支持多公众号关联配置 + 微信支付开发的 Spring Boot Starter::支持多公众号关联配置 + + + + com.github.binarywang + weixin-java-pay + ${project.version} + + + org.springframework.boot + spring-boot-starter-test + ${spring.boot.version} + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + + + diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayMultiAutoConfiguration.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayMultiAutoConfiguration.java new file mode 100644 index 0000000000..08ddafbf9c --- /dev/null +++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayMultiAutoConfiguration.java @@ -0,0 +1,38 @@ +package com.binarywang.spring.starter.wxjava.pay.config; + +import com.binarywang.spring.starter.wxjava.pay.properties.WxPayMultiProperties; +import com.binarywang.spring.starter.wxjava.pay.service.WxPayMultiServices; +import com.binarywang.spring.starter.wxjava.pay.service.WxPayMultiServicesImpl; +import com.github.binarywang.wxpay.service.WxPayService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 微信支付多公众号关联自动配置. + * + * @author Binary Wang + */ +@Slf4j +@Configuration +@EnableConfigurationProperties(WxPayMultiProperties.class) +@ConditionalOnClass(WxPayService.class) +@ConditionalOnProperty(prefix = WxPayMultiProperties.PREFIX, value = "enabled", matchIfMissing = true) +public class WxPayMultiAutoConfiguration { + + /** + * 构造微信支付多服务管理对象. + * + * @param wxPayMultiProperties 多配置属性 + * @return 微信支付多服务管理对象 + */ + @Bean + @ConditionalOnMissingBean(WxPayMultiServices.class) + public WxPayMultiServices wxPayMultiServices(WxPayMultiProperties wxPayMultiProperties) { + return new WxPayMultiServicesImpl(wxPayMultiProperties); + } +} diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayMultiProperties.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayMultiProperties.java new file mode 100644 index 0000000000..8d1180b0e4 --- /dev/null +++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayMultiProperties.java @@ -0,0 +1,27 @@ +package com.binarywang.spring.starter.wxjava.pay.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * 微信支付多公众号关联配置属性类. + * + * @author Binary Wang + */ +@Data +@NoArgsConstructor +@ConfigurationProperties(WxPayMultiProperties.PREFIX) +public class WxPayMultiProperties implements Serializable { + private static final long serialVersionUID = -8015955705346835955L; + public static final String PREFIX = "wx.pay"; + + /** + * 多个公众号的配置信息,key 可以是 appId 或自定义的标识. + */ + private Map configs = new HashMap<>(); +} diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPaySingleProperties.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPaySingleProperties.java new file mode 100644 index 0000000000..ef936fc234 --- /dev/null +++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPaySingleProperties.java @@ -0,0 +1,130 @@ +package com.binarywang.spring.starter.wxjava.pay.properties; + +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; + +/** + * 微信支付单个公众号配置属性类. + * + * @author Binary Wang + */ +@Data +@NoArgsConstructor +public class WxPaySingleProperties implements Serializable { + private static final long serialVersionUID = 3978986361098922525L; + + /** + * 设置微信公众号或者小程序等的appid. + */ + private String appId; + + /** + * 微信支付商户号. + */ + private String mchId; + + /** + * 微信支付商户密钥. + */ + private String mchKey; + + /** + * 服务商模式下的子商户公众账号ID,普通模式请不要配置. + */ + private String subAppId; + + /** + * 服务商模式下的子商户号,普通模式请不要配置. + */ + private String subMchId; + + /** + * apiclient_cert.p12文件的绝对路径,或者如果放在项目中,请以classpath:开头指定. + */ + private String keyPath; + + /** + * 微信支付分serviceId. + */ + private String serviceId; + + /** + * 证书序列号. + */ + private String certSerialNo; + + /** + * apiV3秘钥. + */ + private String apiV3Key; + + /** + * 微信支付异步回调地址,通知url必须为直接可访问的url,不能携带参数. + */ + private String notifyUrl; + + /** + * 退款结果异步回调地址,通知url必须为直接可访问的url,不能携带参数. + */ + private String refundNotifyUrl; + + /** + * 微信支付分回调地址. + */ + private String payScoreNotifyUrl; + + /** + * 微信支付分授权回调地址. + */ + private String payScorePermissionNotifyUrl; + + /** + * apiv3 商户apiclient_key.pem. + */ + private String privateKeyPath; + + /** + * apiv3 商户apiclient_cert.pem. + */ + private String privateCertPath; + + /** + * 公钥ID. + */ + private String publicKeyId; + + /** + * pub_key.pem证书文件的绝对路径或者以classpath:开头的类路径. + */ + private String publicKeyPath; + + /** + * 微信支付是否使用仿真测试环境. + * 默认不使用. + */ + private boolean useSandboxEnv = false; + + /** + * 自定义API主机地址,用于替换默认的 https://api.mch.weixin.qq.com. + * 例如:http://proxy.company.com:8080 + */ + private String apiHostUrl; + + /** + * 自定义API主机路径前缀(用于代理入口前缀). + * 例如:/api-weixin + */ + private String apiHostUrlPath; + + /** + * 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认添加. + */ + private boolean strictlyNeedWechatPaySerial = true; + + /** + * 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认使用. + */ + private boolean fullPublicKeyModel = true; +} diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServices.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServices.java new file mode 100644 index 0000000000..3e0b7a999f --- /dev/null +++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServices.java @@ -0,0 +1,33 @@ +package com.binarywang.spring.starter.wxjava.pay.service; + +import com.github.binarywang.wxpay.service.WxPayService; + +/** + * 微信支付 {@link WxPayService} 所有实例存放类. + * + * @author Binary Wang + */ +public interface WxPayMultiServices { + /** + * 通过配置标识获取 WxPayService. + *

+ * 注意:configKey 是配置文件中定义的 key(如 wx.pay.configs.<configKey>.xxx), + * 而不是 appId。如果使用 appId 作为配置 key,则可以直接传入 appId。 + *

+ * + * @param configKey 配置标识(配置文件中 wx.pay.configs 下的 key) + * @return WxPayService + */ + WxPayService getWxPayService(String configKey); + + /** + * 根据配置标识,从列表中移除一个 WxPayService 实例. + *

+ * 注意:configKey 是配置文件中定义的 key(如 wx.pay.configs.<configKey>.xxx), + * 而不是 appId。如果使用 appId 作为配置 key,则可以直接传入 appId。 + *

+ * + * @param configKey 配置标识(配置文件中 wx.pay.configs 下的 key) + */ + void removeWxPayService(String configKey); +} diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServicesImpl.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServicesImpl.java new file mode 100644 index 0000000000..7cbcceabb4 --- /dev/null +++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/service/WxPayMultiServicesImpl.java @@ -0,0 +1,93 @@ +package com.binarywang.spring.starter.wxjava.pay.service; + +import com.binarywang.spring.starter.wxjava.pay.properties.WxPayMultiProperties; +import com.binarywang.spring.starter.wxjava.pay.properties.WxPaySingleProperties; +import com.github.binarywang.wxpay.config.WxPayConfig; +import com.github.binarywang.wxpay.service.WxPayService; +import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 微信支付多服务管理实现类. + * + * @author Binary Wang + */ +@Slf4j +public class WxPayMultiServicesImpl implements WxPayMultiServices { + private final Map services = new ConcurrentHashMap<>(); + private final WxPayMultiProperties wxPayMultiProperties; + + public WxPayMultiServicesImpl(WxPayMultiProperties wxPayMultiProperties) { + this.wxPayMultiProperties = wxPayMultiProperties; + } + + @Override + public WxPayService getWxPayService(String configKey) { + if (StringUtils.isBlank(configKey)) { + log.warn("配置标识为空,无法获取WxPayService"); + return null; + } + + // 使用 computeIfAbsent 实现线程安全的懒加载,避免使用 synchronized(this) 带来的性能问题 + return services.computeIfAbsent(configKey, key -> { + WxPaySingleProperties properties = wxPayMultiProperties.getConfigs().get(key); + if (properties == null) { + log.warn("未找到配置标识为[{}]的微信支付配置", key); + return null; + } + return this.buildWxPayService(properties); + }); + } + + @Override + public void removeWxPayService(String configKey) { + if (StringUtils.isBlank(configKey)) { + log.warn("配置标识为空,无法移除WxPayService"); + return; + } + services.remove(configKey); + } + + /** + * 根据配置构建 WxPayService. + * + * @param properties 单个配置属性 + * @return WxPayService + */ + private WxPayService buildWxPayService(WxPaySingleProperties properties) { + WxPayServiceImpl wxPayService = new WxPayServiceImpl(); + WxPayConfig payConfig = new WxPayConfig(); + + payConfig.setAppId(StringUtils.trimToNull(properties.getAppId())); + payConfig.setMchId(StringUtils.trimToNull(properties.getMchId())); + payConfig.setMchKey(StringUtils.trimToNull(properties.getMchKey())); + payConfig.setSubAppId(StringUtils.trimToNull(properties.getSubAppId())); + payConfig.setSubMchId(StringUtils.trimToNull(properties.getSubMchId())); + payConfig.setKeyPath(StringUtils.trimToNull(properties.getKeyPath())); + payConfig.setUseSandboxEnv(properties.isUseSandboxEnv()); + payConfig.setNotifyUrl(StringUtils.trimToNull(properties.getNotifyUrl())); + payConfig.setRefundNotifyUrl(StringUtils.trimToNull(properties.getRefundNotifyUrl())); + + // 以下是apiv3以及支付分相关 + payConfig.setServiceId(StringUtils.trimToNull(properties.getServiceId())); + payConfig.setPayScoreNotifyUrl(StringUtils.trimToNull(properties.getPayScoreNotifyUrl())); + payConfig.setPayScorePermissionNotifyUrl(StringUtils.trimToNull(properties.getPayScorePermissionNotifyUrl())); + payConfig.setPrivateKeyPath(StringUtils.trimToNull(properties.getPrivateKeyPath())); + payConfig.setPrivateCertPath(StringUtils.trimToNull(properties.getPrivateCertPath())); + payConfig.setCertSerialNo(StringUtils.trimToNull(properties.getCertSerialNo())); + payConfig.setApiV3Key(StringUtils.trimToNull(properties.getApiV3Key())); + payConfig.setPublicKeyId(StringUtils.trimToNull(properties.getPublicKeyId())); + payConfig.setPublicKeyPath(StringUtils.trimToNull(properties.getPublicKeyPath())); + payConfig.setApiHostUrl(StringUtils.trimToNull(properties.getApiHostUrl())); + payConfig.setApiHostUrlPath(StringUtils.trimToNull(properties.getApiHostUrlPath())); + payConfig.setStrictlyNeedWechatPaySerial(properties.isStrictlyNeedWechatPaySerial()); + payConfig.setFullPublicKeyModel(properties.isFullPublicKeyModel()); + + wxPayService.setConfig(payConfig); + return wxPayService; + } +} diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..d257d37276 --- /dev/null +++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +com.binarywang.spring.starter.wxjava.pay.config.WxPayMultiAutoConfiguration diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..39e3342f4a --- /dev/null +++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,2 @@ +com.binarywang.spring.starter.wxjava.pay.config.WxPayMultiAutoConfiguration + diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java new file mode 100644 index 0000000000..87132fdcf3 --- /dev/null +++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/WxPayMultiServicesTest.java @@ -0,0 +1,109 @@ +package com.binarywang.spring.starter.wxjava.pay; + +import com.binarywang.spring.starter.wxjava.pay.config.WxPayMultiAutoConfiguration; +import com.binarywang.spring.starter.wxjava.pay.properties.WxPayMultiProperties; +import com.binarywang.spring.starter.wxjava.pay.properties.WxPaySingleProperties; +import com.binarywang.spring.starter.wxjava.pay.service.WxPayMultiServices; +import com.github.binarywang.wxpay.service.WxPayService; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * 微信支付多公众号关联配置测试. + * + * @author Binary Wang + */ +@SpringBootTest(classes = {WxPayMultiAutoConfiguration.class, WxPayMultiServicesTest.TestApplication.class}) +@TestPropertySource(properties = { + "wx.pay.configs.app1.app-id=wx1111111111111111", + "wx.pay.configs.app1.mch-id=1111111111", + "wx.pay.configs.app1.mch-key=11111111111111111111111111111111", + "wx.pay.configs.app1.notify-url=https://example.com/pay/notify", + "wx.pay.configs.app2.app-id=wx2222222222222222", + "wx.pay.configs.app2.mch-id=2222222222", + "wx.pay.configs.app2.api-host-url=http://10.0.0.1:3128", + "wx.pay.configs.app2.api-host-url-path=/api-weixin", + "wx.pay.configs.app2.apiv3-key=22222222222222222222222222222222", + "wx.pay.configs.app2.cert-serial-no=2222222222222222", + "wx.pay.configs.app2.private-key-path=classpath:cert/apiclient_key.pem", + "wx.pay.configs.app2.private-cert-path=classpath:cert/apiclient_cert.pem" +}) +public class WxPayMultiServicesTest { + + @Autowired + private WxPayMultiServices wxPayMultiServices; + + @Autowired + private WxPayMultiProperties wxPayMultiProperties; + + @Test + public void testConfiguration() { + assertNotNull(wxPayMultiServices, "WxPayMultiServices should be autowired"); + assertNotNull(wxPayMultiProperties, "WxPayMultiProperties should be autowired"); + + // 验证配置正确加载 + assertEquals(2, wxPayMultiProperties.getConfigs().size(), "Should have 2 configurations"); + + WxPaySingleProperties app1Config = wxPayMultiProperties.getConfigs().get("app1"); + assertNotNull(app1Config, "app1 configuration should exist"); + assertEquals("wx1111111111111111", app1Config.getAppId()); + assertEquals("1111111111", app1Config.getMchId()); + assertEquals("11111111111111111111111111111111", app1Config.getMchKey()); + + WxPaySingleProperties app2Config = wxPayMultiProperties.getConfigs().get("app2"); + assertNotNull(app2Config, "app2 configuration should exist"); + assertEquals("wx2222222222222222", app2Config.getAppId()); + assertEquals("2222222222", app2Config.getMchId()); + assertEquals("http://10.0.0.1:3128", app2Config.getApiHostUrl()); + assertEquals("/api-weixin", app2Config.getApiHostUrlPath()); + assertEquals("22222222222222222222222222222222", app2Config.getApiV3Key()); + } + + @Test + public void testGetWxPayService() { + WxPayService app1Service = wxPayMultiServices.getWxPayService("app1"); + assertNotNull(app1Service, "Should get WxPayService for app1"); + assertEquals("wx1111111111111111", app1Service.getConfig().getAppId()); + assertEquals("1111111111", app1Service.getConfig().getMchId()); + + WxPayService app2Service = wxPayMultiServices.getWxPayService("app2"); + assertNotNull(app2Service, "Should get WxPayService for app2"); + assertEquals("wx2222222222222222", app2Service.getConfig().getAppId()); + assertEquals("2222222222", app2Service.getConfig().getMchId()); + assertEquals("/api-weixin", app2Service.getConfig().getApiHostUrlPath()); + + // 测试相同key返回相同实例 + WxPayService app1ServiceAgain = wxPayMultiServices.getWxPayService("app1"); + assertSame(app1Service, app1ServiceAgain, "Should return the same instance for the same key"); + } + + @Test + public void testGetWxPayServiceWithInvalidKey() { + WxPayService service = wxPayMultiServices.getWxPayService("nonexistent"); + assertNull(service, "Should return null for non-existent key"); + } + + @Test + public void testRemoveWxPayService() { + // 首先获取一个服务实例 + WxPayService app1Service = wxPayMultiServices.getWxPayService("app1"); + assertNotNull(app1Service, "Should get WxPayService for app1"); + + // 移除服务 + wxPayMultiServices.removeWxPayService("app1"); + + // 再次获取时应该创建新实例 + WxPayService app1ServiceNew = wxPayMultiServices.getWxPayService("app1"); + assertNotNull(app1ServiceNew, "Should get new WxPayService for app1"); + assertNotSame(app1Service, app1ServiceNew, "Should return a new instance after removal"); + } + + @SpringBootApplication + static class TestApplication { + } +} diff --git a/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/example/WxPayMultiExample.java b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/example/WxPayMultiExample.java new file mode 100644 index 0000000000..48ae32d5b4 --- /dev/null +++ b/spring-boot-starters/wx-java-pay-multi-spring-boot-starter/src/test/java/com/binarywang/spring/starter/wxjava/pay/example/WxPayMultiExample.java @@ -0,0 +1,249 @@ +package com.binarywang.spring.starter.wxjava.pay.example; + +import com.binarywang.spring.starter.wxjava.pay.service.WxPayMultiServices; +import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request; +import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request; +import com.github.binarywang.wxpay.bean.result.WxPayOrderQueryV3Result; +import com.github.binarywang.wxpay.bean.result.WxPayRefundV3Result; +import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderV3Result; +import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum; +import com.github.binarywang.wxpay.service.WxPayService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +/** + * 微信支付多公众号关联使用示例. + *

+ * 本示例展示了如何使用 wx-java-pay-multi-spring-boot-starter 来管理多个公众号的支付配置。 + *

+ * + * @author Binary Wang + */ +@Slf4j +@Service +public class WxPayMultiExample { + + @Autowired + private WxPayMultiServices wxPayMultiServices; + + /** + * 示例1:根据appId创建支付订单. + *

+ * 适用场景:系统需要支持多个公众号,根据用户所在的公众号动态选择支付配置 + *

+ * + * @param appId 公众号appId + * @param openId 用户的openId + * @param totalFee 支付金额(分) + * @param body 商品描述 + * @return JSAPI支付参数 + */ + public WxPayUnifiedOrderV3Result.JsapiResult createJsapiOrder(String appId, String openId, + Integer totalFee, String body) { + try { + // 根据appId获取对应的WxPayService + WxPayService wxPayService = wxPayMultiServices.getWxPayService(appId); + + if (wxPayService == null) { + log.error("未找到appId对应的微信支付配置: {}", appId); + throw new IllegalArgumentException("未找到appId对应的微信支付配置"); + } + + // 构建支付请求 + WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); + request.setOutTradeNo(generateOutTradeNo()); + request.setDescription(body); + request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(totalFee)); + request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(openId)); + request.setNotifyUrl(wxPayService.getConfig().getNotifyUrl()); + + // 调用微信支付API创建订单 + WxPayUnifiedOrderV3Result.JsapiResult result = + wxPayService.createOrderV3(TradeTypeEnum.JSAPI, request); + + log.info("创建JSAPI支付订单成功,appId: {}, outTradeNo: {}", appId, request.getOutTradeNo()); + return result; + + } catch (Exception e) { + log.error("创建JSAPI支付订单失败,appId: {}", appId, e); + throw new RuntimeException("创建支付订单失败", e); + } + } + + /** + * 示例2:服务商模式 - 为不同子商户创建订单. + *

+ * 适用场景:服务商为多个子商户提供支付服务 + *

+ * + * @param configKey 配置标识(在配置文件中定义) + * @param subOpenId 子商户用户的openId + * @param totalFee 支付金额(分) + * @param body 商品描述 + * @return JSAPI支付参数 + */ + public WxPayUnifiedOrderV3Result.JsapiResult createPartnerOrder(String configKey, String subOpenId, + Integer totalFee, String body) { + try { + // 根据配置标识获取WxPayService + WxPayService wxPayService = wxPayMultiServices.getWxPayService(configKey); + + if (wxPayService == null) { + log.error("未找到配置: {}", configKey); + throw new IllegalArgumentException("未找到配置"); + } + + // 获取子商户信息 + String subAppId = wxPayService.getConfig().getSubAppId(); + String subMchId = wxPayService.getConfig().getSubMchId(); + log.info("使用服务商模式,子商户appId: {}, 子商户号: {}", subAppId, subMchId); + + // 构建支付请求 + WxPayUnifiedOrderV3Request request = new WxPayUnifiedOrderV3Request(); + request.setOutTradeNo(generateOutTradeNo()); + request.setDescription(body); + request.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(totalFee)); + request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(subOpenId)); + request.setNotifyUrl(wxPayService.getConfig().getNotifyUrl()); + + // 调用微信支付API创建订单 + WxPayUnifiedOrderV3Result.JsapiResult result = + wxPayService.createOrderV3(TradeTypeEnum.JSAPI, request); + + log.info("创建服务商支付订单成功,配置: {}, outTradeNo: {}", configKey, request.getOutTradeNo()); + return result; + + } catch (Exception e) { + log.error("创建服务商支付订单失败,配置: {}", configKey, e); + throw new RuntimeException("创建支付订单失败", e); + } + } + + /** + * 示例3:查询订单状态. + *

+ * 适用场景:查询不同公众号的订单支付状态 + *

+ * + * @param appId 公众号appId + * @param outTradeNo 商户订单号 + * @return 订单状态 + */ + public String queryOrderStatus(String appId, String outTradeNo) { + try { + WxPayService wxPayService = wxPayMultiServices.getWxPayService(appId); + + if (wxPayService == null) { + log.error("未找到appId对应的微信支付配置: {}", appId); + throw new IllegalArgumentException("未找到appId对应的微信支付配置"); + } + + // 查询订单 + WxPayOrderQueryV3Result result = wxPayService.queryOrderV3(null, outTradeNo); + String tradeState = result.getTradeState(); + + log.info("查询订单状态成功,appId: {}, outTradeNo: {}, 状态: {}", appId, outTradeNo, tradeState); + return tradeState; + + } catch (Exception e) { + log.error("查询订单状态失败,appId: {}, outTradeNo: {}", appId, outTradeNo, e); + throw new RuntimeException("查询订单失败", e); + } + } + + /** + * 示例4:申请退款. + *

+ * 适用场景:为不同公众号的订单申请退款 + *

+ * + * @param appId 公众号appId + * @param outTradeNo 商户订单号 + * @param refundFee 退款金额(分) + * @param totalFee 订单总金额(分) + * @param reason 退款原因 + * @return 退款单号 + */ + public String refund(String appId, String outTradeNo, Integer refundFee, + Integer totalFee, String reason) { + try { + WxPayService wxPayService = wxPayMultiServices.getWxPayService(appId); + + if (wxPayService == null) { + log.error("未找到appId对应的微信支付配置: {}", appId); + throw new IllegalArgumentException("未找到appId对应的微信支付配置"); + } + + // 构建退款请求 + com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request request = + new com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request(); + request.setOutTradeNo(outTradeNo); + request.setOutRefundNo(generateRefundNo()); + request.setReason(reason); + request.setNotifyUrl(wxPayService.getConfig().getRefundNotifyUrl()); + + com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request.Amount amount = + new com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request.Amount(); + amount.setRefund(refundFee); + amount.setTotal(totalFee); + amount.setCurrency("CNY"); + request.setAmount(amount); + + // 调用微信支付API申请退款 + WxPayRefundV3Result result = wxPayService.refundV3(request); + + log.info("申请退款成功,appId: {}, outTradeNo: {}, outRefundNo: {}", + appId, outTradeNo, request.getOutRefundNo()); + return request.getOutRefundNo(); + + } catch (Exception e) { + log.error("申请退款失败,appId: {}, outTradeNo: {}", appId, outTradeNo, e); + throw new RuntimeException("申请退款失败", e); + } + } + + /** + * 示例5:动态管理配置. + *

+ * 适用场景:需要在运行时更新配置(如证书更新后需要重新加载) + *

+ * + * @param configKey 配置标识 + */ + public void reloadConfig(String configKey) { + try { + // 移除缓存的WxPayService实例 + wxPayMultiServices.removeWxPayService(configKey); + log.info("移除配置成功,下次获取时将重新创建: {}", configKey); + + // 下次调用 getWxPayService 时会重新创建实例 + WxPayService wxPayService = wxPayMultiServices.getWxPayService(configKey); + if (wxPayService != null) { + log.info("重新加载配置成功: {}", configKey); + } + + } catch (Exception e) { + log.error("重新加载配置失败: {}", configKey, e); + throw new RuntimeException("重新加载配置失败", e); + } + } + + /** + * 生成商户订单号. + * + * @return 商户订单号 + */ + private String generateOutTradeNo() { + return "ORDER_" + System.currentTimeMillis(); + } + + /** + * 生成商户退款单号. + * + * @return 商户退款单号 + */ + private String generateRefundNo() { + return "REFUND_" + System.currentTimeMillis(); + } +} diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/README.md b/spring-boot-starters/wx-java-pay-spring-boot-starter/README.md index a4d91fade0..bed890d5e8 100644 --- a/spring-boot-starters/wx-java-pay-spring-boot-starter/README.md +++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/README.md @@ -8,20 +8,38 @@ ``` 2. 添加配置(application.yml) +###### 1)V2版本 ```yml wx: pay: appId: mchId: mchKey: - subAppId: - subMchId: keyPath: ``` - - - - - - - +###### 2)V3版本 +```yml +wx: + pay: + appId: xxxxxxxxxxx + mchId: 15xxxxxxxxx #商户id + apiHostUrl: http://10.0.0.1:3128 # 可选:代理主机 + apiHostUrlPath: /api-weixin # 可选:代理入口前缀 + apiV3Key: Dc1DBwSc094jACxxxxxxxxxxxxxxx #V3密钥 + certSerialNo: 62C6CEAA360BCxxxxxxxxxxxxxxx + privateKeyPath: classpath:cert/apiclient_key.pem #apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径 + privateCertPath: classpath:cert/apiclient_cert.pem #apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径 +``` +###### 3)V3服务商版本 +```yml +wx: + pay: #微信服务商支付 + configs: + - appId: wxe97b2x9c2b3d #spAppId + mchId: 16486610 #服务商商户 + subAppId: wx118cexxe3c07679 #子appId + subMchId: 16496705 #子商户 + apiV3Key: Dc1DBwSc094jAKDGR5aqqb7PTHr #apiV3密钥 + privateKeyPath: classpath:cert/apiclient_key.pem #服务商证书文件,apiclient_key.pem证书文件的绝对路径或者以classpath:开头的类路径(可以配置绝对路径) + privateCertPath: classpath:cert/apiclient_cert.pem #apiclient_cert.pem证书文件的绝对路径或者以classpath:开头的类路径 +``` diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml index 6c643615c0..3c1313bc22 100644 --- a/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/pom.xml @@ -5,7 +5,7 @@ wx-java-spring-boot-starters com.github.binarywang - 3.6.0 + 4.8.3.B 4.0.0 diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java index 43b2114e6e..7e748ba1a3 100644 --- a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java +++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/config/WxPayAutoConfiguration.java @@ -49,6 +49,23 @@ public WxPayService wxPayService() { payConfig.setSubAppId(StringUtils.trimToNull(this.properties.getSubAppId())); payConfig.setSubMchId(StringUtils.trimToNull(this.properties.getSubMchId())); payConfig.setKeyPath(StringUtils.trimToNull(this.properties.getKeyPath())); + payConfig.setUseSandboxEnv(this.properties.isUseSandboxEnv()); + payConfig.setNotifyUrl(StringUtils.trimToNull(this.properties.getNotifyUrl())); + payConfig.setRefundNotifyUrl(StringUtils.trimToNull(this.properties.getRefundNotifyUrl())); + //以下是apiv3以及支付分相关 + payConfig.setServiceId(StringUtils.trimToNull(this.properties.getServiceId())); + payConfig.setPayScoreNotifyUrl(StringUtils.trimToNull(this.properties.getPayScoreNotifyUrl())); + payConfig.setPayScorePermissionNotifyUrl(StringUtils.trimToNull(this.properties.getPayScorePermissionNotifyUrl())); + payConfig.setPrivateKeyPath(StringUtils.trimToNull(this.properties.getPrivateKeyPath())); + payConfig.setPrivateCertPath(StringUtils.trimToNull(this.properties.getPrivateCertPath())); + payConfig.setCertSerialNo(StringUtils.trimToNull(this.properties.getCertSerialNo())); + payConfig.setApiV3Key(StringUtils.trimToNull(this.properties.getApiV3Key())); + payConfig.setPublicKeyId(StringUtils.trimToNull(this.properties.getPublicKeyId())); + payConfig.setPublicKeyPath(StringUtils.trimToNull(this.properties.getPublicKeyPath())); + payConfig.setApiHostUrl(StringUtils.trimToNull(this.properties.getApiHostUrl())); + payConfig.setApiHostUrlPath(StringUtils.trimToNull(this.properties.getApiHostUrlPath())); + payConfig.setStrictlyNeedWechatPaySerial(this.properties.isStrictlyNeedWechatPaySerial()); + payConfig.setFullPublicKeyModel(this.properties.isFullPublicKeyModel()); wxPayService.setConfig(payConfig); return wxPayService; diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java index fe8a215650..49045c4ee0 100644 --- a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java +++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/pay/properties/WxPayProperties.java @@ -43,4 +43,88 @@ public class WxPayProperties { * apiclient_cert.p12文件的绝对路径,或者如果放在项目中,请以classpath:开头指定. */ private String keyPath; + + /** + * 微信支付分serviceId + */ + private String serviceId; + + /** + * 证书序列号 + */ + private String certSerialNo; + + /** + * apiV3秘钥 + */ + private String apiV3Key; + + /** + * 微信支付异步回调地址,通知url必须为直接可访问的url,不能携带参数 + */ + private String notifyUrl; + + /** + * 退款结果异步回调地址,通知url必须为直接可访问的url,不能携带参数 + */ + private String refundNotifyUrl; + + /** + * 微信支付分回调地址 + */ + private String payScoreNotifyUrl; + + /** + * 微信支付分授权回调地址 + */ + private String payScorePermissionNotifyUrl; + + /** + * apiv3 商户apiclient_key.pem + */ + private String privateKeyPath; + + /** + * apiv3 商户apiclient_cert.pem + */ + private String privateCertPath; + + /** + * 公钥ID + */ + private String publicKeyId; + + /** + * pub_key.pem证书文件的绝对路径或者以classpath:开头的类路径. + */ + private String publicKeyPath; + + /** + * 微信支付是否使用仿真测试环境. + * 默认不使用 + */ + private boolean useSandboxEnv; + + /** + * 自定义API主机地址,用于替换默认的 https://api.mch.weixin.qq.com + * 例如:http://proxy.company.com:8080 + */ + private String apiHostUrl; + + /** + * 自定义API主机路径前缀(用于代理入口前缀) + * 例如:/api-weixin + */ + private String apiHostUrlPath; + + /** + * 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认添加 + */ + private boolean strictlyNeedWechatPaySerial = true; + + /** + * 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认使用 + */ + private boolean fullPublicKeyModel = true; + } diff --git a/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..28cbbace5f --- /dev/null +++ b/spring-boot-starters/wx-java-pay-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.binarywang.spring.starter.wxjava.pay.config.WxPayAutoConfiguration diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/README.md b/spring-boot-starters/wx-java-qidian-spring-boot-starter/README.md new file mode 100644 index 0000000000..34069fa1fe --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/README.md @@ -0,0 +1,45 @@ +# wx-java-qidian-spring-boot-starter + +## 快速开始 + +1. 引入依赖 + ```xml + + com.github.binarywang + wx-java-qidian-spring-boot-starter + ${version} + + ``` +2. 添加配置(application.properties) + ```properties + # 公众号配置(必填) + wx.qidian.appId = appId + wx.qidian.secret = @secret + wx.qidian.token = @token + wx.qidian.aesKey = @aesKey + # 存储配置redis(可选) + wx.qidian.config-storage.type = Jedis # 配置类型: Memory(默认), Jedis, RedisTemplate + wx.qidian.config-storage.key-prefix = wx # 相关redis前缀配置: wx(默认) + wx.qidian.config-storage.redis.host = 127.0.0.1 + wx.qidian.config-storage.redis.port = 6379 + #单机和sentinel同时存在时,优先使用sentinel配置 + #wx.qidian.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379 + #wx.qidian.config-storage.redis.sentinel-name=mymaster + # http客户端配置 + wx.qidian.config-storage.http-client-type=httpclient # http客户端类型: HttpClient(默认), OkHttp, JoddHttp + wx.qidian.config-storage.http-proxy-host= + wx.qidian.config-storage.http-proxy-port= + wx.qidian.config-storage.http-proxy-username= + wx.qidian.config-storage.http-proxy-password= + # 公众号地址host配置 + #wx.qidian.hosts.api-host=http://proxy.com/ + #wx.qidian.hosts.open-host=http://proxy.com/ + #wx.qidian.hosts.mp-host=http://proxy.com/ + ``` +3. 自动注入的类型 + +- `WxQidianService` +- `WxQidianConfigStorage` + +4、参考 demo: +https://github.com/binarywang/wx-java-mp-demo diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml new file mode 100644 index 0000000000..d9b845adb1 --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml @@ -0,0 +1,66 @@ + + + + wx-java-spring-boot-starters + com.github.binarywang + 4.8.3.B + + 4.0.0 + + wx-java-qidian-spring-boot-starter + WxJava - Spring Boot Starter for QiDian + 腾讯企点的 Spring Boot Starter + + + + com.github.binarywang + weixin-java-qidian + ${project.version} + + + redis.clients + jedis + 4.3.2 + compile + + + org.springframework.data + spring-data-redis + provided + + + org.jodd + jodd-http + provided + + + com.squareup.okhttp3 + okhttp + provided + + + + + + + org.springframework.boot + spring-boot-maven-plugin + ${spring.boot.version} + + + org.apache.maven.plugins + maven-source-plugin + 2.2.1 + + + attach-sources + + jar-no-fork + + + + + + + + diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianAutoConfiguration.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianAutoConfiguration.java new file mode 100644 index 0000000000..bb66fde262 --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianAutoConfiguration.java @@ -0,0 +1,17 @@ +package com.binarywang.spring.starter.wxjava.qidian.config; + +import com.binarywang.spring.starter.wxjava.qidian.properties.WxQidianProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * . + * + * @author someone + */ +@Configuration +@EnableConfigurationProperties(WxQidianProperties.class) +@Import({ WxQidianStorageAutoConfiguration.class, WxQidianServiceAutoConfiguration.class }) +public class WxQidianAutoConfiguration { +} diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianServiceAutoConfiguration.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianServiceAutoConfiguration.java new file mode 100644 index 0000000000..3af628d01e --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianServiceAutoConfiguration.java @@ -0,0 +1,63 @@ +package com.binarywang.spring.starter.wxjava.qidian.config; + +import com.binarywang.spring.starter.wxjava.qidian.enums.HttpClientType; +import com.binarywang.spring.starter.wxjava.qidian.properties.WxQidianProperties; +import me.chanjar.weixin.qidian.api.WxQidianService; +import me.chanjar.weixin.qidian.api.impl.WxQidianServiceHttpClientImpl; +import me.chanjar.weixin.qidian.api.impl.WxQidianServiceImpl; +import me.chanjar.weixin.qidian.api.impl.WxQidianServiceJoddHttpImpl; +import me.chanjar.weixin.qidian.api.impl.WxQidianServiceOkHttpImpl; +import me.chanjar.weixin.qidian.config.WxQidianConfigStorage; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * 腾讯企点相关服务自动注册. + * + * @author alegria + */ +@Configuration +public class WxQidianServiceAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + public WxQidianService wxQidianService(WxQidianConfigStorage configStorage, WxQidianProperties wxQidianProperties) { + HttpClientType httpClientType = wxQidianProperties.getConfigStorage().getHttpClientType(); + WxQidianService wxQidianService; + switch (httpClientType) { + case OkHttp: + wxQidianService = newWxQidianServiceOkHttpImpl(); + break; + case JoddHttp: + wxQidianService = newWxQidianServiceJoddHttpImpl(); + break; + case HttpClient: + wxQidianService = newWxQidianServiceHttpClientImpl(); + break; + default: + wxQidianService = newWxQidianServiceImpl(); + break; + } + + wxQidianService.setWxMpConfigStorage(configStorage); + return wxQidianService; + } + + private WxQidianService newWxQidianServiceImpl() { + return new WxQidianServiceImpl(); + } + + private WxQidianService newWxQidianServiceHttpClientImpl() { + return new WxQidianServiceHttpClientImpl(); + } + + private WxQidianService newWxQidianServiceOkHttpImpl() { + return new WxQidianServiceOkHttpImpl(); + } + + private WxQidianService newWxQidianServiceJoddHttpImpl() { + return new WxQidianServiceJoddHttpImpl(); + } + +} diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianStorageAutoConfiguration.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianStorageAutoConfiguration.java new file mode 100644 index 0000000000..01ba91b565 --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/config/WxQidianStorageAutoConfiguration.java @@ -0,0 +1,168 @@ +package com.binarywang.spring.starter.wxjava.qidian.config; + +import com.binarywang.spring.starter.wxjava.qidian.enums.StorageType; +import com.binarywang.spring.starter.wxjava.qidian.properties.RedisProperties; +import com.binarywang.spring.starter.wxjava.qidian.properties.WxQidianProperties; +import com.google.common.collect.Sets; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.redis.JedisWxRedisOps; +import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps; +import me.chanjar.weixin.common.redis.WxRedisOps; +import me.chanjar.weixin.qidian.bean.WxQidianHostConfig; +import me.chanjar.weixin.qidian.config.WxQidianConfigStorage; +import me.chanjar.weixin.qidian.config.impl.WxQidianDefaultConfigImpl; +import me.chanjar.weixin.qidian.config.impl.WxQidianRedisConfigImpl; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.core.StringRedisTemplate; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.JedisSentinelPool; +import redis.clients.jedis.util.Pool; + +import java.util.Set; + +/** + * 腾讯企点存储策略自动配置. + * + * @author alegria + */ +@Slf4j +@Configuration +@RequiredArgsConstructor +public class WxQidianStorageAutoConfiguration { + private final ApplicationContext applicationContext; + + private final WxQidianProperties wxQidianProperties; + + @Value("${wx.mp.config-storage.redis.host:") + private String redisHost; + + @Value("${wx.mp.configStorage.redis.host:") + private String redisHost2; + + @Bean + @ConditionalOnMissingBean(WxQidianConfigStorage.class) + public WxQidianConfigStorage wxQidianConfigStorage() { + StorageType type = wxQidianProperties.getConfigStorage().getType(); + WxQidianConfigStorage config; + switch (type) { + case Jedis: + config = jedisConfigStorage(); + break; + case RedisTemplate: + config = redisTemplateConfigStorage(); + break; + default: + config = defaultConfigStorage(); + break; + } + // wx host config + if (null != wxQidianProperties.getHosts() && StringUtils.isNotEmpty(wxQidianProperties.getHosts().getApiHost())) { + WxQidianHostConfig hostConfig = new WxQidianHostConfig(); + hostConfig.setApiHost(wxQidianProperties.getHosts().getApiHost()); + hostConfig.setQidianHost(wxQidianProperties.getHosts().getQidianHost()); + hostConfig.setOpenHost(wxQidianProperties.getHosts().getOpenHost()); + config.setHostConfig(hostConfig); + } + return config; + } + + private WxQidianConfigStorage defaultConfigStorage() { + WxQidianDefaultConfigImpl config = new WxQidianDefaultConfigImpl(); + setWxMpInfo(config); + return config; + } + + private WxQidianConfigStorage jedisConfigStorage() { + Pool jedisPool; + if (StringUtils.isNotEmpty(redisHost) || StringUtils.isNotEmpty(redisHost2)) { + jedisPool = getJedisPool(); + } else { + jedisPool = applicationContext.getBean(JedisPool.class); + } + WxRedisOps redisOps = new JedisWxRedisOps(jedisPool); + WxQidianRedisConfigImpl wxQidianRedisConfig = new WxQidianRedisConfigImpl(redisOps, + wxQidianProperties.getConfigStorage().getKeyPrefix()); + setWxMpInfo(wxQidianRedisConfig); + return wxQidianRedisConfig; + } + + private WxQidianConfigStorage redisTemplateConfigStorage() { + StringRedisTemplate redisTemplate = null; + try { + redisTemplate = applicationContext.getBean(StringRedisTemplate.class); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + try { + if (null == redisTemplate) { + redisTemplate = (StringRedisTemplate) applicationContext.getBean("stringRedisTemplate"); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + } + + if (null == redisTemplate) { + redisTemplate = (StringRedisTemplate) applicationContext.getBean("redisTemplate"); + } + + WxRedisOps redisOps = new RedisTemplateWxRedisOps(redisTemplate); + WxQidianRedisConfigImpl wxMpRedisConfig = new WxQidianRedisConfigImpl(redisOps, + wxQidianProperties.getConfigStorage().getKeyPrefix()); + + setWxMpInfo(wxMpRedisConfig); + return wxMpRedisConfig; + } + + private void setWxMpInfo(WxQidianDefaultConfigImpl config) { + WxQidianProperties properties = wxQidianProperties; + WxQidianProperties.ConfigStorage configStorageProperties = properties.getConfigStorage(); + config.setAppId(properties.getAppId()); + config.setSecret(properties.getSecret()); + config.setToken(properties.getToken()); + config.setAesKey(properties.getAesKey()); + + config.setHttpProxyHost(configStorageProperties.getHttpProxyHost()); + config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername()); + config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword()); + if (configStorageProperties.getHttpProxyPort() != null) { + config.setHttpProxyPort(configStorageProperties.getHttpProxyPort()); + } + } + + private Pool getJedisPool() { + WxQidianProperties.ConfigStorage storage = wxQidianProperties.getConfigStorage(); + RedisProperties redis = storage.getRedis(); + + JedisPoolConfig config = new JedisPoolConfig(); + if (redis.getMaxActive() != null) { + config.setMaxTotal(redis.getMaxActive()); + } + if (redis.getMaxIdle() != null) { + config.setMaxIdle(redis.getMaxIdle()); + } + if (redis.getMaxWaitMillis() != null) { + config.setMaxWaitMillis(redis.getMaxWaitMillis()); + } + if (redis.getMinIdle() != null) { + config.setMinIdle(redis.getMinIdle()); + } + config.setTestOnBorrow(true); + config.setTestWhileIdle(true); + if (StringUtils.isNotEmpty(redis.getSentinelIps())) { + + Set sentinels = Sets.newHashSet(redis.getSentinelIps().split(",")); + return new JedisSentinelPool(redis.getSentinelName(), sentinels,config); + } + + return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(), + redis.getDatabase()); + } +} diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/HttpClientType.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/HttpClientType.java new file mode 100644 index 0000000000..04589a911b --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/HttpClientType.java @@ -0,0 +1,26 @@ +package com.binarywang.spring.starter.wxjava.qidian.enums; + +/** + * httpclient类型. + * + * @author Binary Wang + * created on 2020-08-30 + */ +public enum HttpClientType { + /** + * HttpClient. + */ + HttpClient, + /** + * OkHttp. + */ + OkHttp, + /** + * JoddHttp. + */ + JoddHttp, + /** + * HttpComponents. + */ + HttpComponents, +} diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/StorageType.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/StorageType.java new file mode 100644 index 0000000000..f4e26bc156 --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/enums/StorageType.java @@ -0,0 +1,26 @@ +package com.binarywang.spring.starter.wxjava.qidian.enums; + +/** + * storage类型. + * + * @author Binary Wang + * created on 2020-08-30 + */ +public enum StorageType { + /** + * 内存. + */ + Memory, + /** + * redis(JedisClient). + */ + Jedis, + /** + * redis(Redisson). + */ + Redisson, + /** + * redis(RedisTemplate). + */ + RedisTemplate +} diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/HostConfig.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/HostConfig.java new file mode 100644 index 0000000000..92ade849fa --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/HostConfig.java @@ -0,0 +1,18 @@ +package com.binarywang.spring.starter.wxjava.qidian.properties; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class HostConfig implements Serializable { + + private static final long serialVersionUID = -4172767630740346001L; + + private String apiHost; + + private String openHost; + + private String qidianHost; + +} diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/RedisProperties.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/RedisProperties.java new file mode 100644 index 0000000000..abfad572e7 --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/RedisProperties.java @@ -0,0 +1,56 @@ +package com.binarywang.spring.starter.wxjava.qidian.properties; + +import lombok.Data; + +import java.io.Serializable; + +/** + * redis 配置属性. + * + * @author Binary Wang + * created on 2020-08-30 + */ +@Data +public class RedisProperties implements Serializable { + private static final long serialVersionUID = -5924815351660074401L; + + /** + * 主机地址. + */ + private String host = "127.0.0.1"; + + /** + * 端口号. + */ + private int port = 6379; + + /** + * 密码. + */ + private String password; + + /** + * 超时. + */ + private int timeout = 2000; + + /** + * 数据库. + */ + private int database = 0; + + /** + * sentinel ips + */ + private String sentinelIps; + + /** + * sentinel name + */ + private String sentinelName; + + private Integer maxActive; + private Integer maxIdle; + private Integer maxWaitMillis; + private Integer minIdle; +} diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/WxQidianProperties.java b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/WxQidianProperties.java new file mode 100644 index 0000000000..bec5dfcce0 --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/qidian/properties/WxQidianProperties.java @@ -0,0 +1,99 @@ +package com.binarywang.spring.starter.wxjava.qidian.properties; + +import com.binarywang.spring.starter.wxjava.qidian.enums.HttpClientType; +import com.binarywang.spring.starter.wxjava.qidian.enums.StorageType; +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.io.Serializable; + +import static com.binarywang.spring.starter.wxjava.qidian.enums.StorageType.Memory; +import static com.binarywang.spring.starter.wxjava.qidian.properties.WxQidianProperties.PREFIX; + +/** + * 企点接入相关配置属性. + * + * @author someone + */ +@Data +@ConfigurationProperties(PREFIX) +public class WxQidianProperties { + public static final String PREFIX = "wx.qidian"; + + /** + * 设置腾讯企点的appid. + */ + private String appId; + + /** + * 设置腾讯企点的app secret. + */ + private String secret; + + /** + * 设置腾讯企点的token. + */ + private String token; + + /** + * 设置腾讯企点的EncodingAESKey. + */ + private String aesKey; + + /** + * 自定义host配置 + */ + private HostConfig hosts; + + /** + * 存储策略 + */ + private ConfigStorage configStorage = new ConfigStorage(); + + @Data + public static class ConfigStorage implements Serializable { + private static final long serialVersionUID = 4815731027000065434L; + + /** + * 存储类型. + */ + private StorageType type = Memory; + + /** + * 指定key前缀. + */ + private String keyPrefix = "wx"; + + /** + * redis连接配置. + */ + private RedisProperties redis = new RedisProperties(); + + /** + * http客户端类型. + */ + private HttpClientType httpClientType = HttpClientType.HttpComponents; + + /** + * http代理主机. + */ + private String httpProxyHost; + + /** + * http代理端口. + */ + private Integer httpProxyPort; + + /** + * http代理用户名. + */ + private String httpProxyUsername; + + /** + * http代理密码. + */ + private String httpProxyPassword; + + } + +} diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/resources/META-INF/spring.factories b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000000..bfcb7bf919 --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.binarywang.spring.starter.wxjava.qidian.config.WxQidianAutoConfiguration diff --git a/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000000..6e7e511448 --- /dev/null +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +com.binarywang.spring.starter.wxjava.qidian.config.WxQidianAutoConfiguration diff --git a/weixin-graal/pom.xml b/weixin-graal/pom.xml new file mode 100644 index 0000000000..9c23e95add --- /dev/null +++ b/weixin-graal/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + + com.github.binarywang + wx-java + 4.8.3.B + + + weixin-graal + WxJava - Graal + 微信开发Java内部配合graal以产生native-image配置的辅助工具, 可以通过项目的 native-image Profile 来启用: mvn -P native-image ... + + + + + + org.projectlombok + lombok + compile + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + none + + + + + + diff --git a/weixin-graal/src/main/java/com/github/binarywang/wx/graal/GraalProcessor.java b/weixin-graal/src/main/java/com/github/binarywang/wx/graal/GraalProcessor.java new file mode 100644 index 0000000000..a983a51897 --- /dev/null +++ b/weixin-graal/src/main/java/com/github/binarywang/wx/graal/GraalProcessor.java @@ -0,0 +1,177 @@ +package com.github.binarywang.wx.graal; + +import lombok.Data; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import javax.tools.FileObject; +import javax.tools.StandardLocation; +import java.io.IOException; +import java.io.Writer; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; + +/** + * 目前仅仅处理@Data,且必须在lombok自己的processor之前执行,千万注意!!!!! + * + * @author outersky + */ +@SupportedAnnotationTypes("lombok.Data") +@SupportedSourceVersion(SourceVersion.RELEASE_8) +public class GraalProcessor extends AbstractProcessor { + private static final String REFLECTION_CONFIG_JSON = "reflection-config.json"; + private static final String NATIVE_IMAGE_PROPERTIES = "native-image.properties"; + + private final SortedSet classSet = new TreeSet<>(); + private String shortestPackageName = null; + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + for (TypeElement annotatedClass : ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(Data.class))) { + registerClass(annotatedClass.getQualifiedName().toString()); + handleSuperClass(annotatedClass); + } + + //只有最后一轮才可以写文件,否则文件会被重复打开,报错! + if (!roundEnv.processingOver()) { + return false; + } + + // 如果没有文件要写,跳过 + if (classSet.isEmpty()) { + return false; + } + + writeFiles(); + + //必须返回false,以便让lombok能继续处理。 + return false; + } + + /** + * 设置当前最短的package名称 + * + * @param packageName 包名 + */ + private void setShortestPackageName(String packageName) { + if (shortestPackageName == null) { + shortestPackageName = packageName; + } else if (packageName.length() < shortestPackageName.length()) { + shortestPackageName = packageName; + } + } + + /** + * 更加完整的类名来获取package名称 + * + * @param fullClassName 完整的类名 + * @return package name + */ + private String getPackageName(String fullClassName) { + int last = fullClassName.lastIndexOf('.'); + if (last == -1) { + return fullClassName; + } + return fullClassName.substring(0, last); + } + + /** + * 保存文件 + * META-INF/native-image/.../reflection-config.json + * META-INF/native-image/.../native-image.properties + */ + private void writeFiles() { + String basePackage = shortestPackageName; + + String module; + if (basePackage.contains(".")) { + final int i = basePackage.lastIndexOf('.'); + module = basePackage.substring(i + 1); + basePackage = basePackage.substring(0, i); + } else { + module = basePackage; + } + + String path = "META-INF/native-image/" + basePackage + "/" + module + "/"; + String reflectFile = path + REFLECTION_CONFIG_JSON; + String propsFile = path + NATIVE_IMAGE_PROPERTIES; + try { + FileObject fileObject = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", propsFile); + try (Writer writer = fileObject.openWriter();) { + writer.append("Args = -H:ReflectionConfigurationResources=${.}/" + REFLECTION_CONFIG_JSON); + } + } catch (IOException e) { + e.printStackTrace(); + } + + try { + FileObject fileObject = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", reflectFile); + try (Writer writer = fileObject.openWriter();) { + writer.write("[\n"); + boolean first = true; + for (String name : classSet) { + if (first) { + first = false; + } else { + writer.write(","); + } + writer.write(assetGraalJsonElement(name)); + writer.append('\n'); + } + writer.write("]"); + } + } catch (IOException e) { + e.printStackTrace(); + } + + } + + private String assetGraalJsonElement(String className) { + return "{\n" + + " \"name\" : \"" + className + "\",\n" + + " \"allDeclaredFields\":true,\n" + + " \"allDeclaredMethods\":true,\n" + + " \"allDeclaredConstructors\":true,\n" + + " \"allPublicMethods\" : true\n" + + "}"; + } + + /** + * 登记一个class + * + * @param className 完整的类名 + */ + private void registerClass(String className) { + classSet.add(className); + setShortestPackageName(getPackageName(className)); + } + + /** + * 获取一个类型的所有的父类,并登记 + * + * @param typeElement 类型元素 + */ + private void handleSuperClass(TypeElement typeElement) { + TypeMirror superclass = typeElement.getSuperclass(); + if (superclass.getKind() == TypeKind.DECLARED) { + TypeElement s = (TypeElement) ((DeclaredType) superclass).asElement(); + String sName = s.toString(); + // ignore java.**/javax.** + if (sName.startsWith("java.") || sName.startsWith("javax.")) { + return; + } + registerClass(sName); + handleSuperClass(s); + } + } + +} diff --git a/weixin-graal/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/weixin-graal/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000000..f358b92ef9 --- /dev/null +++ b/weixin-graal/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +com.github.binarywang.wx.graal.GraalProcessor diff --git a/weixin-java-aispeech/pom.xml b/weixin-java-aispeech/pom.xml new file mode 100644 index 0000000000..2ca8aa84d8 --- /dev/null +++ b/weixin-java-aispeech/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + com.github.binarywang + wx-java + 4.8.3.B + + + weixin-java-aispeech + WxJava - Aispeech Java SDK + 微信智能对话 Java SDK + + + + com.github.binarywang + weixin-java-common + ${project.version} + + + + org.apache.httpcomponents.client5 + httpclient5 + + + + org.testng + testng + test + + + org.projectlombok + lombok + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + src/test/resources/testng.xml + + + + + + + + + native-image + + false + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + + com.github.binarywang.wx.graal.GraalProcessor,lombok.launch.AnnotationProcessorHider$AnnotationProcessor,lombok.launch.AnnotationProcessorHider$ClaimingProcessor + + + + com.github.binarywang + weixin-graal + ${project.version} + + + + + + + + + diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechDialogService.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechDialogService.java new file mode 100644 index 0000000000..51d46562cb --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechDialogService.java @@ -0,0 +1,23 @@ +package me.chanjar.weixin.aispeech.api; + +import java.util.List; +import me.chanjar.weixin.aispeech.bean.dialog.AsyncTaskResult; +import me.chanjar.weixin.aispeech.bean.dialog.BotIntent; +import me.chanjar.weixin.aispeech.bean.dialog.DialogQueryRequest; +import me.chanjar.weixin.aispeech.bean.dialog.DialogResult; +import me.chanjar.weixin.aispeech.bean.dialog.PublishProgress; +import me.chanjar.weixin.common.error.WxErrorException; + +public interface WxAispeechDialogService { + String getAccessToken(String appid, String account) throws WxErrorException; + + String importBotJson(int mode, List data) throws WxErrorException; + + String publishBot() throws WxErrorException; + + PublishProgress getPublishProgress(String env) throws WxErrorException; + + AsyncTaskResult queryAsyncTask(String taskId) throws WxErrorException; + + DialogResult query(DialogQueryRequest request) throws WxErrorException; +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechKnowledgeService.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechKnowledgeService.java new file mode 100644 index 0000000000..fa27d48235 --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechKnowledgeService.java @@ -0,0 +1,51 @@ +package me.chanjar.weixin.aispeech.api; + +import java.io.File; +import java.util.List; +import java.util.Map; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeInfo; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeManualCreateRequest; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveProgress; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveRequest; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeTagRequest; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUpdateRequest; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUrlCreateRequest; +import me.chanjar.weixin.common.error.WxErrorException; + +public interface WxAispeechKnowledgeService { + KnowledgeInfo createKnowledgeByFile(String knowledgeBaseId, File file, String title, String description, String metadata) + throws WxErrorException; + + KnowledgeInfo createKnowledgeByUrl(String knowledgeBaseId, KnowledgeUrlCreateRequest request) throws WxErrorException; + + KnowledgeInfo createKnowledgeByManual(String knowledgeBaseId, KnowledgeManualCreateRequest request) throws WxErrorException; + + List listKnowledge(String knowledgeBaseId, Integer page, Integer pageSize) throws WxErrorException; + + List listKnowledgeByIds(List knowledgeIds) throws WxErrorException; + + KnowledgeInfo getKnowledge(String knowledgeId) throws WxErrorException; + + KnowledgeInfo updateKnowledge(String knowledgeId, KnowledgeUpdateRequest request) throws WxErrorException; + + KnowledgeInfo updateManualKnowledge(String knowledgeId, KnowledgeManualCreateRequest request) throws WxErrorException; + + boolean deleteKnowledge(String knowledgeId) throws WxErrorException; + + boolean updateKnowledgeTags(List knowledgeIds, Long tagId) throws WxErrorException; + + List searchKnowledge(String keyword, String knowledgeBaseId, Integer page, Integer pageSize) + throws WxErrorException; + + String moveKnowledge(KnowledgeMoveRequest request) throws WxErrorException; + + KnowledgeMoveProgress getMoveProgress(String taskId) throws WxErrorException; + + boolean createKnowledgeBaseTag(String knowledgeBaseId, KnowledgeTagRequest request) throws WxErrorException; + + boolean updateKnowledgeBaseTag(String knowledgeBaseId, String tagId, KnowledgeTagRequest request) throws WxErrorException; + + String postRaw(String path, Object requestBody) throws WxErrorException; + + String getRaw(String path, Map queryParams) throws WxErrorException; +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechService.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechService.java new file mode 100644 index 0000000000..08ccf837e4 --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/WxAispeechService.java @@ -0,0 +1,13 @@ +package me.chanjar.weixin.aispeech.api; + +import me.chanjar.weixin.aispeech.config.WxAispeechConfigStorage; + +public interface WxAispeechService { + WxAispeechDialogService getDialogService(); + + WxAispeechKnowledgeService getKnowledgeService(); + + WxAispeechConfigStorage getConfigStorage(); + + void setConfigStorage(WxAispeechConfigStorage configStorage); +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechDialogServiceImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechDialogServiceImpl.java new file mode 100644 index 0000000000..9bd53b454e --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechDialogServiceImpl.java @@ -0,0 +1,129 @@ +package me.chanjar.weixin.aispeech.api.impl; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import me.chanjar.weixin.aispeech.api.WxAispeechDialogService; +import me.chanjar.weixin.aispeech.bean.dialog.AispeechApiResponse; +import me.chanjar.weixin.aispeech.bean.dialog.AsyncTaskResult; +import me.chanjar.weixin.aispeech.bean.dialog.BotIntent; +import me.chanjar.weixin.aispeech.bean.dialog.DialogQueryRequest; +import me.chanjar.weixin.aispeech.bean.dialog.DialogResult; +import me.chanjar.weixin.aispeech.bean.dialog.PublishProgress; +import me.chanjar.weixin.aispeech.util.WxAispeechSignUtil; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; +import org.apache.commons.lang3.StringUtils; + +public class WxAispeechDialogServiceImpl implements WxAispeechDialogService { + private final WxAispeechServiceImpl service; + + public WxAispeechDialogServiceImpl(WxAispeechServiceImpl service) { + this.service = service; + } + + @Override + public String getAccessToken(String appid, String account) throws WxErrorException { + Map request = new HashMap<>(); + if (StringUtils.isNotBlank(account)) { + request.put("account", account); + } + + String response = service.executeDialogPost("/v2/token", request, false, appid); + Type type = new TypeToken>() { } .getType(); + AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type); + ensureSuccess(result); + String token = result.getData().get("access_token").getAsString(); + service.getConfigStorage().setOpenAiToken(token); + return token; + } + + @Override + public String importBotJson(int mode, List data) throws WxErrorException { + Map request = new HashMap<>(); + request.put("mode", mode); + request.put("data", data); + + String response = service.executeDialogPost("/v2/bot/import/json", request, true, null); + Type type = new TypeToken>() { } .getType(); + AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type); + ensureSuccess(result); + return result.getData().get("task_id").getAsString(); + } + + @Override + public String publishBot() throws WxErrorException { + String response = service.executeDialogPost("/v2/bot/publish", "{}", true, null); + Type type = new TypeToken>() { } .getType(); + AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type); + ensureSuccess(result); + return result.getRequestId(); + } + + @Override + public PublishProgress getPublishProgress(String env) throws WxErrorException { + Map request = new HashMap<>(); + request.put("env", env); + + String response = service.executeDialogPost("/v2/bot/effective_progress", request, true, null); + Type type = new TypeToken>() { } .getType(); + AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type); + ensureSuccess(result); + return result.getData(); + } + + @Override + public AsyncTaskResult queryAsyncTask(String taskId) throws WxErrorException { + Map request = new HashMap<>(); + request.put("task_id", taskId); + + String response = service.executeDialogPost("/v2/async/fetch", request, true, null); + Type type = new TypeToken>() { } .getType(); + AispeechApiResponse result = WxGsonBuilder.create().fromJson(response, type); + ensureSuccess(result); + return result.getData(); + } + + @Override + public DialogResult query(DialogQueryRequest request) throws WxErrorException { + String json = WxGsonBuilder.create().toJson(request); + String encrypted = WxAispeechSignUtil.encryptAesCbcToBase64(json, service.getConfigStorage().getAesKey()); + String response = service.executeDialogPost("/v2/bot/query", encrypted, true, null); + + String responseJson = response; + if (!looksLikeJson(response)) { + responseJson = WxAispeechSignUtil.decryptAesCbcFromBase64(response, service.getConfigStorage().getAesKey()); + } + + Type type = new TypeToken>() { } .getType(); + AispeechApiResponse result = WxGsonBuilder.create().fromJson(responseJson, type); + ensureSuccess(result); + + DialogResult dialogResult = result.getData(); + if (dialogResult != null && looksLikeJson(dialogResult.getAnswer())) { + dialogResult.setRawAnswer(WxGsonBuilder.create().fromJson(dialogResult.getAnswer(), JsonElement.class)); + } + return dialogResult; + } + + private boolean looksLikeJson(String value) { + return StringUtils.isNotBlank(value) && (value.startsWith("{") || value.startsWith("[")); + } + + private void ensureSuccess(AispeechApiResponse response) throws WxErrorException { + if (response == null) { + throw new WxErrorException("响应为空"); + } + if (response.getCode() == null || response.getCode() != 0) { + throw new WxErrorException(WxError.builder() + .errorCode(response.getCode() == null ? -1 : response.getCode()) + .errorMsg(response.getMsg()) + .build()); + } + } +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImpl.java new file mode 100644 index 0000000000..708f12890d --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechKnowledgeServiceImpl.java @@ -0,0 +1,184 @@ +package me.chanjar.weixin.aispeech.api.impl; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import java.io.File; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; +import me.chanjar.weixin.aispeech.api.WxAispeechKnowledgeService; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeInfo; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeListResult; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeManualCreateRequest; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveProgress; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeMoveRequest; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeTagRequest; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUpdateRequest; +import me.chanjar.weixin.aispeech.bean.knowledge.KnowledgeUrlCreateRequest; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; +import org.apache.commons.lang3.StringUtils; + +public class WxAispeechKnowledgeServiceImpl implements WxAispeechKnowledgeService { + private final WxAispeechServiceImpl service; + + public WxAispeechKnowledgeServiceImpl(WxAispeechServiceImpl service) { + this.service = service; + } + + @Override + public KnowledgeInfo createKnowledgeByFile(String knowledgeBaseId, File file, String title, String description, String metadata) + throws WxErrorException { + String response = service.executeKnowledgeMultipartPost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge/file", + file, title, description, metadata); + return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class); + } + + @Override + public KnowledgeInfo createKnowledgeByUrl(String knowledgeBaseId, KnowledgeUrlCreateRequest request) + throws WxErrorException { + String response = service.executeKnowledgePost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge/url", request); + return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class); + } + + @Override + public KnowledgeInfo createKnowledgeByManual(String knowledgeBaseId, KnowledgeManualCreateRequest request) + throws WxErrorException { + String response = service.executeKnowledgePost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge/manual", request); + return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class); + } + + @Override + public List listKnowledge(String knowledgeBaseId, Integer page, Integer pageSize) + throws WxErrorException { + Map query = new HashMap<>(); + query.put("page", page == null ? null : String.valueOf(page)); + query.put("page_size", pageSize == null ? null : String.valueOf(pageSize)); + String response = service.executeKnowledgeGet("/api/v1/knowledge-bases/" + knowledgeBaseId + "/knowledge", query); + KnowledgeListResult result = WxGsonBuilder.create().fromJson(response, KnowledgeListResult.class); + return result == null ? null : result.getData(); + } + + @Override + public List listKnowledgeByIds(List knowledgeIds) throws WxErrorException { + if (knowledgeIds == null || knowledgeIds.isEmpty()) { + return null; + } + StringJoiner joiner = new StringJoiner(","); + for (String knowledgeId : knowledgeIds) { + if (StringUtils.isNotBlank(knowledgeId)) { + joiner.add(knowledgeId); + } + } + if (joiner.length() == 0) { + return null; + } + + Map query = new HashMap<>(); + query.put("ids", joiner.toString()); + String response = service.executeKnowledgeGet("/api/v1/knowledge/batch", query); + return parseKnowledgeInfoList(response); + } + + @Override + public KnowledgeInfo getKnowledge(String knowledgeId) throws WxErrorException { + String response = service.executeKnowledgeGet("/api/v1/knowledge/" + knowledgeId, null); + return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class); + } + + @Override + public KnowledgeInfo updateKnowledge(String knowledgeId, KnowledgeUpdateRequest request) throws WxErrorException { + String response = service.executeKnowledgePut("/api/v1/knowledge/" + knowledgeId, request); + return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class); + } + + @Override + public KnowledgeInfo updateManualKnowledge(String knowledgeId, KnowledgeManualCreateRequest request) throws WxErrorException { + String response = service.executeKnowledgePut("/api/v1/knowledge/manual/" + knowledgeId, request); + return WxGsonBuilder.create().fromJson(response, KnowledgeInfo.class); + } + + @Override + public boolean deleteKnowledge(String knowledgeId) throws WxErrorException { + service.executeKnowledgeDelete("/api/v1/knowledge/" + knowledgeId); + return true; + } + + @Override + public boolean updateKnowledgeTags(List knowledgeIds, Long tagId) throws WxErrorException { + if (knowledgeIds == null || knowledgeIds.isEmpty() || tagId == null) { + return false; + } + + Map request = new HashMap<>(); + request.put("knowledge_ids", knowledgeIds); + request.put("tag_id", tagId); + String response = service.executeKnowledgePut("/api/v1/knowledge/tags", request); + return StringUtils.isNotBlank(response); + } + + @Override + public List searchKnowledge(String keyword, String knowledgeBaseId, Integer page, Integer pageSize) + throws WxErrorException { + Map query = new HashMap<>(); + query.put("keyword", keyword); + query.put("knowledge_base_id", knowledgeBaseId); + query.put("page", page == null ? null : String.valueOf(page)); + query.put("page_size", pageSize == null ? null : String.valueOf(pageSize)); + String response = service.executeKnowledgeGet("/api/v1/knowledge/search", query); + return parseKnowledgeInfoList(response); + } + + @Override + public String moveKnowledge(KnowledgeMoveRequest request) throws WxErrorException { + return service.executeKnowledgePost("/api/v1/knowledge/move", request); + } + + @Override + public KnowledgeMoveProgress getMoveProgress(String taskId) throws WxErrorException { + String response = service.executeKnowledgeGet("/api/v1/knowledge/move/progress/" + taskId, null); + return WxGsonBuilder.create().fromJson(response, KnowledgeMoveProgress.class); + } + + @Override + public boolean createKnowledgeBaseTag(String knowledgeBaseId, KnowledgeTagRequest request) throws WxErrorException { + String response = service.executeKnowledgePost("/api/v1/knowledge-bases/" + knowledgeBaseId + "/tags", request); + return StringUtils.isNotBlank(response); + } + + @Override + public boolean updateKnowledgeBaseTag(String knowledgeBaseId, String tagId, KnowledgeTagRequest request) + throws WxErrorException { + String response = service.executeKnowledgePut("/api/v1/knowledge-bases/" + knowledgeBaseId + "/tags/" + tagId, request); + return StringUtils.isNotBlank(response); + } + + @Override + public String postRaw(String path, Object requestBody) throws WxErrorException { + return service.executeKnowledgePost(path, requestBody); + } + + @Override + public String getRaw(String path, Map queryParams) throws WxErrorException { + return service.executeKnowledgeGet(path, queryParams); + } + + private List parseKnowledgeInfoList(String response) { + if (StringUtils.isBlank(response)) { + return null; + } + + JsonElement element = WxGsonBuilder.create().fromJson(response, JsonElement.class); + Type listType = new TypeToken>() { } .getType(); + if (element != null && element.isJsonObject()) { + JsonObject object = element.getAsJsonObject(); + if (object.has("data")) { + return WxGsonBuilder.create().fromJson(object.get("data"), listType); + } + } + return WxGsonBuilder.create().fromJson(element, listType); + } +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpClientImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpClientImpl.java new file mode 100644 index 0000000000..e37d60e352 --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpClientImpl.java @@ -0,0 +1,4 @@ +package me.chanjar.weixin.aispeech.api.impl; + +public class WxAispeechServiceHttpClientImpl extends WxAispeechServiceHttpComponentsImpl { +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpComponentsImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpComponentsImpl.java new file mode 100644 index 0000000000..ac91d98938 --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceHttpComponentsImpl.java @@ -0,0 +1,4 @@ +package me.chanjar.weixin.aispeech.api.impl; + +public class WxAispeechServiceHttpComponentsImpl extends WxAispeechServiceImpl { +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceImpl.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceImpl.java new file mode 100644 index 0000000000..37a657cef2 --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/api/impl/WxAispeechServiceImpl.java @@ -0,0 +1,250 @@ +package me.chanjar.weixin.aispeech.api.impl; + +import com.google.gson.Gson; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import java.util.UUID; +import me.chanjar.weixin.aispeech.api.WxAispeechDialogService; +import me.chanjar.weixin.aispeech.api.WxAispeechKnowledgeService; +import me.chanjar.weixin.aispeech.api.WxAispeechService; +import me.chanjar.weixin.aispeech.config.WxAispeechConfigStorage; +import me.chanjar.weixin.aispeech.util.WxAispeechSignUtil; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.hc.DefaultHttpComponentsClientBuilder; +import me.chanjar.weixin.common.util.http.hc.HttpComponentsClientBuilder; +import org.apache.commons.lang3.StringUtils; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.classic.methods.HttpPut; +import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.apache.hc.core5.net.URIBuilder; +import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; + +public class WxAispeechServiceImpl implements WxAispeechService { + private static final Gson GSON = new Gson(); + + private final WxAispeechDialogService dialogService = new WxAispeechDialogServiceImpl(this); + private final WxAispeechKnowledgeService knowledgeService = new WxAispeechKnowledgeServiceImpl(this); + + private WxAispeechConfigStorage configStorage; + private CloseableHttpClient httpClient; + private HttpHost proxy; + + @Override + public WxAispeechDialogService getDialogService() { + return dialogService; + } + + @Override + public WxAispeechKnowledgeService getKnowledgeService() { + return knowledgeService; + } + + @Override + public WxAispeechConfigStorage getConfigStorage() { + return configStorage; + } + + @Override + public void setConfigStorage(WxAispeechConfigStorage configStorage) { + this.configStorage = configStorage; + this.initHttp(); + } + + protected void initHttp() { + HttpComponentsClientBuilder builder = configStorage.getHttpComponentsClientBuilder(); + if (builder == null) { + builder = DefaultHttpComponentsClientBuilder.get(); + } + + builder.httpProxyHost(configStorage.getHttpProxyHost()) + .httpProxyPort(configStorage.getHttpProxyPort()) + .httpProxyUsername(configStorage.getHttpProxyUsername()) + .httpProxyPassword(configStorage.getHttpProxyPassword() == null ? null : + configStorage.getHttpProxyPassword().toCharArray()); + + if (configStorage.getHttpProxyHost() != null && configStorage.getHttpProxyPort() > 0) { + this.proxy = new HttpHost(configStorage.getHttpProxyHost(), configStorage.getHttpProxyPort()); + } else { + this.proxy = null; + } + + this.httpClient = builder.build(); + } + + protected String executeDialogPost(String path, Object requestBody, boolean withOpenToken, String appid) + throws WxErrorException { + String body = toBody(requestBody); + String requestId = UUID.randomUUID().toString(); + long timestamp = System.currentTimeMillis() / 1000; + String nonce = randomNonce(); + String sign = WxAispeechSignUtil.calcDialogSign(configStorage.getToken(), timestamp, nonce, body); + String resolvedAppid = StringUtils.defaultIfBlank(appid, configStorage.getAppid()); + + HttpPost request = new HttpPost(configStorage.getDialogApiBaseUrl() + path); + request.setHeader("request_id", requestId); + request.setHeader("timestamp", String.valueOf(timestamp)); + request.setHeader("nonce", nonce); + request.setHeader("sign", sign); + request.setHeader("Content-Type", ContentType.APPLICATION_JSON.getMimeType()); + if (withOpenToken) { + if (StringUtils.isBlank(configStorage.getOpenAiToken())) { + throw new WxErrorException("X-OPENAI-TOKEN不能为空,请先调用getAccessToken或手动设置"); + } + request.setHeader("X-OPENAI-TOKEN", configStorage.getOpenAiToken()); + } else { + if (StringUtils.isBlank(resolvedAppid)) { + throw new WxErrorException("X-APPID不能为空"); + } + request.setHeader("X-APPID", resolvedAppid); + } + request.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON)); + return executeRequest(request); + } + + protected String executeKnowledgeGet(String path, Map queryParams) throws WxErrorException { + try { + URIBuilder builder = new URIBuilder(configStorage.getKnowledgeApiBaseUrl() + path); + if (queryParams != null) { + for (Map.Entry entry : queryParams.entrySet()) { + if (entry.getValue() != null) { + builder.addParameter(entry.getKey(), entry.getValue()); + } + } + } + HttpGet request = new HttpGet(builder.build()); + enrichKnowledgeHeaders(request, ""); + return executeRequest(request); + } catch (Exception e) { + throw toWxErrorException(e); + } + } + + protected String executeKnowledgePost(String path, Object requestBody) throws WxErrorException { + String body = toBody(requestBody); + HttpPost request = new HttpPost(configStorage.getKnowledgeApiBaseUrl() + path); + request.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON)); + enrichKnowledgeHeaders(request, body); + return executeRequest(request); + } + + protected String executeKnowledgePut(String path, Object requestBody) throws WxErrorException { + String body = toBody(requestBody); + HttpPut request = new HttpPut(configStorage.getKnowledgeApiBaseUrl() + path); + request.setEntity(new StringEntity(body, ContentType.APPLICATION_JSON)); + enrichKnowledgeHeaders(request, body); + return executeRequest(request); + } + + protected String executeKnowledgeMultipartPost(String path, File file, String title, String description, String metadata) + throws WxErrorException { + HttpPost request = new HttpPost(configStorage.getKnowledgeApiBaseUrl() + path); + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + builder.addBinaryBody("file", file, ContentType.DEFAULT_BINARY, file.getName()); + if (StringUtils.isNotBlank(title)) { + builder.addTextBody("title", title, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)); + } + if (StringUtils.isNotBlank(description)) { + builder.addTextBody("description", description, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)); + } + if (StringUtils.isNotBlank(metadata)) { + builder.addTextBody("metadata", metadata, ContentType.APPLICATION_JSON); + } + HttpEntity entity = builder.build(); + request.setEntity(entity); + if (entity.getContentType() != null) { + request.setHeader("Content-Type", entity.getContentType()); + } + enrichKnowledgeHeaders(request, ""); + return executeRequest(request); + } + + protected String executeKnowledgeDelete(String path) throws WxErrorException { + HttpUriRequestBase request = new HttpUriRequestBase("DELETE", URI.create(configStorage.getKnowledgeApiBaseUrl() + path)); + enrichKnowledgeHeaders(request, ""); + return executeRequest(request); + } + + private void enrichKnowledgeHeaders(HttpUriRequestBase request, String body) throws WxErrorException { + if (StringUtils.isBlank(configStorage.getAppid())) { + throw new WxErrorException("知识助理请求需要配置appid"); + } + if (StringUtils.isBlank(configStorage.getSecretKey())) { + throw new WxErrorException("知识助理请求需要配置secretKey"); + } + + String requestId = UUID.randomUUID().toString(); + long timestamp = System.currentTimeMillis() / 1000; + String nonce = randomNonce(); + String signature = WxAispeechSignUtil.calcKnowledgeSignature(configStorage.getSecretKey(), timestamp, nonce, + requestId, body); + + request.setHeader("X-APPID", configStorage.getAppid()); + request.setHeader("X-Request-ID", requestId); + request.setHeader("X-Timestamp", String.valueOf(timestamp)); + request.setHeader("X-Nonce", nonce); + request.setHeader("X-Signature", signature); + if (!request.containsHeader("Content-Type")) { + request.setHeader("Content-Type", ContentType.APPLICATION_JSON.getMimeType()); + } + } + + private String executeRequest(HttpUriRequestBase request) throws WxErrorException { + if (this.proxy != null) { + RequestConfig requestConfig = RequestConfig.custom().setProxy(this.proxy).build(); + request.setConfig(requestConfig); + } + + try (CloseableHttpResponse response = httpClient.execute(request)) { + int statusCode = response.getCode(); + HttpEntity entity = response.getEntity(); + String body = entity == null ? "" : EntityUtils.toString(entity, StandardCharsets.UTF_8); + if (statusCode >= 200 && statusCode < 300) { + return body; + } + + throw new WxErrorException(WxError.builder().errorCode(statusCode).errorMsg(body).build()); + } catch (IOException | ParseException e) { + throw toWxErrorException(e); + } + } + + protected T fromJson(String json, Class clazz) { + return GSON.fromJson(json, clazz); + } + + private String toBody(Object requestBody) { + if (requestBody == null) { + return "{}"; + } + if (requestBody instanceof String) { + return (String) requestBody; + } + return GSON.toJson(requestBody); + } + + private WxErrorException toWxErrorException(Exception e) { + if (e instanceof WxErrorException) { + return (WxErrorException) e; + } + return new WxErrorException(e); + } + + private String randomNonce() { + return UUID.randomUUID().toString().replace("-", "").substring(0, 16); + } +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AispeechApiResponse.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AispeechApiResponse.java new file mode 100644 index 0000000000..24595b8b46 --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AispeechApiResponse.java @@ -0,0 +1,29 @@ +package me.chanjar.weixin.aispeech.bean.dialog; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +@Data +public class AispeechApiResponse { + private Integer code; + private String msg; + @SerializedName("request_id") + private String requestId; + private T data; + + public Integer getCode() { + return code; + } + + public String getMsg() { + return msg; + } + + public String getRequestId() { + return requestId; + } + + public T getData() { + return data; + } +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AsyncTaskResult.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AsyncTaskResult.java new file mode 100644 index 0000000000..a806fb368a --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/AsyncTaskResult.java @@ -0,0 +1,33 @@ +package me.chanjar.weixin.aispeech.bean.dialog; + +import com.google.gson.JsonElement; +import java.util.List; +import lombok.Data; + +@Data +public class AsyncTaskResult { + private Integer state; + private String msg; + private Integer progress; + private Long start; + private Long end; + private String url; + private Integer totalCount; + private Integer successCount; + private Integer failCount; + private JsonElement successSkillInfo; + private List successSkillInfoList; + + @Data + public static class SkillInfo { + private Long id; + private String name; + private List intents; + } + + @Data + public static class IntentInfo { + private Long id; + private String name; + } +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/BotIntent.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/BotIntent.java new file mode 100644 index 0000000000..3927461fc8 --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/BotIntent.java @@ -0,0 +1,13 @@ +package me.chanjar.weixin.aispeech.bean.dialog; + +import java.util.List; +import lombok.Data; + +@Data +public class BotIntent { + private String skill; + private String intent; + private Boolean disable; + private List questions; + private List answers; +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogQueryRequest.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogQueryRequest.java new file mode 100644 index 0000000000..dd748957ff --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogQueryRequest.java @@ -0,0 +1,19 @@ +package me.chanjar.weixin.aispeech.bean.dialog; + +import com.google.gson.annotations.SerializedName; +import java.util.List; +import lombok.Data; + +@Data +public class DialogQueryRequest { + private String query; + private String env; + @SerializedName("first_priority_skills") + private List firstPrioritySkills; + @SerializedName("second_priority_skills") + private List secondPrioritySkills; + @SerializedName("user_name") + private String userName; + private String avatar; + private String userid; +} diff --git a/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogResult.java b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogResult.java new file mode 100644 index 0000000000..575628dc10 --- /dev/null +++ b/weixin-java-aispeech/src/main/java/me/chanjar/weixin/aispeech/bean/dialog/DialogResult.java @@ -0,0 +1,47 @@ +package me.chanjar.weixin.aispeech.bean.dialog; + +import com.google.gson.JsonElement; +import com.google.gson.annotations.SerializedName; +import java.util.List; +import lombok.Data; + +@Data +public class DialogResult { + private String answer; + @SerializedName("answer_type") + private String answerType; + @SerializedName("skill_name") + private String skillName; + @SerializedName("intent_name") + private String intentName; + @SerializedName("msg_id") + private String msgId; + private List
+ + + native-image + + false + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + + com.github.binarywang.wx.graal.GraalProcessor,lombok.launch.AnnotationProcessorHider$AnnotationProcessor,lombok.launch.AnnotationProcessorHider$ClaimingProcessor + + + + com.github.binarywang + weixin-graal + ${project.version} + + + + + + + + + + diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxConsts.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxConsts.java index e63b4a8c6b..5129410999 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxConsts.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxConsts.java @@ -1,17 +1,42 @@ package me.chanjar.weixin.common.api; +import lombok.experimental.UtilityClass; + +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; +import static me.chanjar.weixin.common.error.WxMpErrorMsgEnum.*; + /** * 微信开发所使用到的常量类. * - * @author Daniel Qian & binarywang + * @author Daniel Qian, binarywang, Wang_Wong */ +@UtilityClass public class WxConsts { + /** + * access_token 相关错误代码 + *
+   * 发生以下情况时尝试刷新access_token
+   * 40001 获取access_token时AppSecret错误,或者access_token无效
+   * 42001 access_token超时
+   * 40014 不合法的access_token,请开发者认真比对access_token的有效性(如是否过期),或查看是否正在为恰当的公众号调用接口
+   * 
+ */ + public static final List ACCESS_TOKEN_ERROR_CODES = Arrays.asList(CODE_40001.getCode(), + CODE_40014.getCode(), CODE_42001.getCode()); + + /** + * 微信接口返回的参数errcode. + */ + public static final String ERR_CODE = "errcode"; + /** * 微信推送过来的消息的类型,和发送给微信xml格式消息的消息类型. */ + @UtilityClass public static class XmlMsgType { public static final String TEXT = "text"; public static final String IMAGE = "image"; @@ -28,11 +53,15 @@ public static class XmlMsgType { public static final String DEVICE_STATUS = "device_status"; public static final String HARDWARE = "hardware"; public static final String TRANSFER_CUSTOMER_SERVICE = "transfer_customer_service"; + public static final String TRANSFER_BIZ_AI_IVR = "transfer_biz_ai_ivr"; + public static final String UPDATE_TASKCARD = "update_taskcard"; + public static final String UPDATE_BUTTON = "update_button"; } /** * 主动发送消息(即客服消息)的消息类型. */ + @UtilityClass public static class KefuMsgType { /** * 文本消息. @@ -103,11 +132,98 @@ public static class KefuMsgType { * 小程序通知消息. */ public static final String MINIPROGRAM_NOTICE = "miniprogram_notice"; + + /** + * 模板卡片消息. + */ + public static final String TEMPLATE_CARD = "template_card"; + + /** + * 发送图文消息(点击跳转到图文消息页面)使用通过 “发布” 系列接口得到的 article_id(草稿箱功能上线后不再支持客服接口中带 media_id 的 mpnews 类型的图文消息) + */ + public static final String MP_NEWS_ARTICLE = "mpnewsarticle"; + } + + /** + * 发送「学校通知」类型 + * https://developer.work.weixin.qq.com/document/path/92321 + */ + @UtilityClass + public static class SchoolContactMsgType { + + /** + * 文本消息. + */ + public static final String TEXT = "text"; + + /** + * 图片消息. + */ + public static final String IMAGE = "image"; + + /** + * 语音消息. + */ + public static final String VOICE = "voice"; + + /** + * 视频消息. + */ + public static final String VIDEO = "video"; + + /** + * 文件消息 + */ + public static final String FILE = "file"; + + /** + * 图文消息 + */ + public static final String NEWS = "news"; + + /** + * 图文消息(mpnews) + */ + public static final String MPNEWS = "mpnews"; + + /** + * 小程序消息 + */ + public static final String MINIPROGRAM = "miniprogram"; + + } + + /** + * 企业微信模板卡片消息的卡片类型 + */ + @UtilityClass + public static class TemplateCardType { + /** + * 文本通知型卡片 + */ + public static final String TEXT_NOTICE = "text_notice"; + /** + * 图文展示型卡片 + */ + public static final String NEWS_NOTICE = "news_notice"; + /** + * 按钮交互型卡片 + */ + public static final String BUTTON_INTERACTION = "button_interaction"; + /** + * 投票选择型卡片 + */ + public static final String VOTE_INTERACTION = "vote_interaction"; + /** + * 多项选择型卡片 + */ + public static final String MULTIPLE_INTERACTION = "multiple_interaction"; } /** * 表示是否是保密消息,0表示否,1表示是,默认0. */ + @UtilityClass public static class KefuMsgSafe { public static final String NO = "0"; public static final String YES = "1"; @@ -116,17 +232,20 @@ public static class KefuMsgSafe { /** * 群发消息的消息类型. */ + @UtilityClass public static class MassMsgType { public static final String MPNEWS = "mpnews"; public static final String TEXT = "text"; public static final String VOICE = "voice"; public static final String IMAGE = "image"; + public static final String IMAGES = "images"; public static final String MPVIDEO = "mpvideo"; } /** * 群发消息后微信端推送给服务器的反馈消息. */ + @UtilityClass public static class MassMsgStatus { public static final String SEND_SUCCESS = "send success"; public static final String SEND_FAIL = "send fail"; @@ -139,6 +258,12 @@ public static class MassMsgStatus { public static final String ERR_20013 = "err(20013)"; public static final String ERR_22000 = "err(22000)"; public static final String ERR_21000 = "err(21000)"; + public static final String ERR_30001 = "err(30001)"; + public static final String ERR_30002 = "err(30002)"; + public static final String ERR_30003 = "err(30003)"; + public static final String ERR_40001 = "err(40001)"; + public static final String ERR_40002 = "err(40002)"; + /** * 群发反馈消息代码所对应的文字描述. @@ -157,12 +282,18 @@ public static class MassMsgStatus { STATUS_DESC.put(ERR_20013, "涉嫌版权"); STATUS_DESC.put(ERR_22000, "涉嫌互推_互相宣传"); STATUS_DESC.put(ERR_21000, "涉嫌其他"); + STATUS_DESC.put(ERR_30001, "原创校验出现系统错误且用户选择了被判为转载就不群发"); + STATUS_DESC.put(ERR_30002, "原创校验被判定为不能群发"); + STATUS_DESC.put(ERR_30003, "原创校验被判定为转载文且用户选择了被判为转载就不群发"); + STATUS_DESC.put(ERR_40001, "管理员拒绝"); + STATUS_DESC.put(ERR_40002, "管理员30分钟内无响应,超时"); } } /** * 微信端推送过来的事件类型. */ + @UtilityClass public static class EventType { public static final String SUBSCRIBE = "subscribe"; public static final String UNSUBSCRIBE = "unsubscribe"; @@ -171,6 +302,8 @@ public static class EventType { public static final String CLICK = "CLICK"; public static final String VIEW = "VIEW"; public static final String MASS_SEND_JOB_FINISH = "MASSSENDJOBFINISH"; + + public static final String SYS_APPROVAL_CHANGE = "sys_approval_change"; /** * 扫码推事件的事件推送 */ @@ -196,7 +329,23 @@ public static class EventType { */ public static final String LOCATION_SELECT = "location_select"; + /** + * 授权用户资料变更事件 + * 1、 当部分用户的资料存在风险时,平台会对用户资料进行清理,并通过消息推送服务器通知最近30天授权过的公众号开发者,我们建议开发者留意响应该事件,及时主动更新或清理用户的头像及昵称,降低风险。 + * 2、 当用户撤回授权信息时,平台会通过消息推送服务器通知给公众号开发者,请开发者注意及时删除用户信息。 + */ + public static final String USER_INFO_MODIFIED = "user_info_modified"; + + /** + * 用户撤回授权事件 + */ + public static final String USER_AUTHORIZATION_REVOKE = "user_authorization_revoke"; + + /** + * 群发模板回调事件 + */ public static final String TEMPLATE_SEND_JOB_FINISH = "TEMPLATESENDJOBFINISH"; + /** * 微信小店 订单付款通知. */ @@ -222,6 +371,10 @@ public static class EventType { */ public static final String CARD_USER_GIFTING_CARD = "user_gifting_card"; + /** + * 异步安全校验事件 + */ + public static final String WXA_MEDIA_CHECK = "wxa_media_check"; /** * 卡券事件:用户核销卡券 @@ -280,6 +433,77 @@ public static class EventType { */ public static final String WEAPP_AUDIT_FAIL = "weapp_audit_fail"; + + /** + * 小程序审核事件:审核延后 + */ + public static final String WEAPP_AUDIT_DELAY = "weapp_audit_delay"; + + /** + * 小程序自定义交易组件支付通知 + */ + public static final String OPEN_PRODUCT_ORDER_PAY = "open_product_order_pay"; + /** + * 点击菜单跳转小程序的事件推送 + */ + public static final String VIEW_MINIPROGRAM = "view_miniprogram"; + + /** + * 订阅通知事件:用户操作订阅通知弹窗 + */ + public static final String SUBSCRIBE_MSG_POPUP_EVENT = "subscribe_msg_popup_event"; + + /** + * 订阅通知事件:用户管理订阅通知 + */ + public static final String SUBSCRIBE_MSG_CHANGE_EVENT = "subscribe_msg_change_event"; + + /** + * 订阅通知事件:发送订阅通知回调 + */ + public static final String SUBSCRIBE_MSG_SENT_EVENT = "subscribe_msg_sent_event"; + + /** + * 名称审核事件 + */ + public static final String WXA_NICKNAME_AUDIT = "wxa_nickname_audit"; + /** + * 小程序违规记录事件 + */ + public static final String WXA_ILLEGAL_RECORD = "wxa_illegal_record"; + /** + * 小程序申诉记录推送 + */ + public static final String WXA_APPEAL_RECORD = "wxa_appeal_record"; + /** + * 隐私权限审核结果推送 + */ + public static final String WXA_PRIVACY_APPLY = "wxa_privacy_apply"; + /** + * 类目审核结果事件推送 + */ + public static final String WXA_CATEGORY_AUDIT = "wxa_category_audit"; + /** + * 小程序微信认证支付成功事件 + */ + public static final String WX_VERIFY_PAY_SUCC = "wx_verify_pay_succ"; + /** + * 小程序微信认证派单事件 + */ + public static final String WX_VERIFY_DISPATCH = "wx_verify_dispatch"; + /** + * 提醒需要上传发货信息事件:曾经发过货的小程序,订单超过48小时未发货时 + */ + public static final String TRADE_MANAGE_REMIND_SHIPPING = "trade_manage_remind_shipping"; + /** + * 订单完成发货时、订单结算时 + */ + public static final String TRADE_MANAGE_ORDER_SETTLEMENT = "trade_manage_order_settlement"; + /** + * 虚拟支付 iOS 退款查询通知 + * 文档:https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/virtual-payment/ios.html + */ + public static final String XPAY_SUBSCRIBE_IOS_REFUND_QUERY_NOTIFY = "xpay_subscribe_ios_refund_query_notify"; } /** @@ -296,6 +520,7 @@ public static class MediaFileType { /** * 自定义菜单的按钮类型. */ + @UtilityClass public static class MenuButtonType { /** * 点击推事件. @@ -346,6 +571,7 @@ public static class MenuButtonType { /** * oauth2网页授权的scope. */ + @UtilityClass public static class OAuth2Scope { /** * 不弹出授权页面,直接跳转,只能获取用户openid. @@ -366,6 +592,7 @@ public static class OAuth2Scope { /** * 网页应用登录授权作用域. */ + @UtilityClass public static class QrConnectScope { public static final String SNSAPI_LOGIN = "snsapi_login"; } @@ -373,6 +600,7 @@ public static class QrConnectScope { /** * 永久素材类型. */ + @UtilityClass public static class MaterialType { public static final String NEWS = "news"; public static final String VOICE = "voice"; @@ -384,6 +612,7 @@ public static class MaterialType { /** * 网络检测入参. */ + @UtilityClass public static class NetCheckArgs { public static final String ACTIONDNS = "dns"; public static final String ACTIONPING = "ping"; @@ -394,5 +623,33 @@ public static class NetCheckArgs { public static final String OPERATORDEFAULT = "DEFAULT"; } + /** + * appId 类型 + */ + @UtilityClass + public static class AppIdType { + /** + * 公众号appId类型 + */ + public static final String MP_TYPE = "mp"; + /** + * 小程序appId类型 + */ + public static final String MINI_TYPE = "mini"; + } + /** + * 新建文章类型 + */ + @UtilityClass + public static class ArticleType { + /** + * 图文消息 + */ + public static final String NEWS = "news"; + /** + * 图片消息 + */ + public static final String NEWS_PIC = "newspic"; + } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxMessageInMemoryDuplicateChecker.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxMessageInMemoryDuplicateChecker.java index d7ac36c7c6..88c3aeae69 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxMessageInMemoryDuplicateChecker.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxMessageInMemoryDuplicateChecker.java @@ -8,10 +8,12 @@ *
  * 默认消息重复检查器.
  * 将每个消息id保存在内存里,每隔5秒清理已经过期的消息id,每个消息id的过期时间是15秒
+ * 替换类WxMessageInMemoryDuplicateCheckerSingleton
  * 
* * @author Daniel Qian */ +@Deprecated public class WxMessageInMemoryDuplicateChecker implements WxMessageDuplicateChecker { /** @@ -61,23 +63,20 @@ protected void checkBackgroundProcessStarted() { if (this.backgroundProcessStarted.getAndSet(true)) { return; } - Thread t = new Thread(new Runnable() { - @Override - public void run() { - try { - while (true) { - Thread.sleep(WxMessageInMemoryDuplicateChecker.this.clearPeriod); - Long now = System.currentTimeMillis(); - for (Map.Entry entry : - WxMessageInMemoryDuplicateChecker.this.msgId2Timestamp.entrySet()) { - if (now - entry.getValue() > WxMessageInMemoryDuplicateChecker.this.timeToLive) { - WxMessageInMemoryDuplicateChecker.this.msgId2Timestamp.entrySet().remove(entry); - } + Thread t = new Thread(() -> { + try { + while (true) { + Thread.sleep(WxMessageInMemoryDuplicateChecker.this.clearPeriod); + Long now = System.currentTimeMillis(); + for (Map.Entry entry : + WxMessageInMemoryDuplicateChecker.this.msgId2Timestamp.entrySet()) { + if (now - entry.getValue() > WxMessageInMemoryDuplicateChecker.this.timeToLive) { + WxMessageInMemoryDuplicateChecker.this.msgId2Timestamp.entrySet().remove(entry); } } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); } }); t.setDaemon(true); diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxMessageInMemoryDuplicateCheckerSingleton.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxMessageInMemoryDuplicateCheckerSingleton.java new file mode 100644 index 0000000000..befd367fe0 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxMessageInMemoryDuplicateCheckerSingleton.java @@ -0,0 +1,91 @@ +package me.chanjar.weixin.common.api; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * @author jiangby + * @version 1.0 + *

+ * 消息去重,记录消息ID首次出现时的时间戳, + * 15S后定时任务触发时废除该记录消息ID + *

+ * created on 2022/5/26 1:32 + */ +@Slf4j +public class WxMessageInMemoryDuplicateCheckerSingleton implements WxMessageDuplicateChecker { + + /** + * 一个消息ID在内存的过期时间:15秒. + */ + private static final Long TIME_TO_LIVE = 15L; + + /** + * 每隔多少周期检查消息ID是否过期:5秒. + */ + private static final Long CLEAR_PERIOD = 5L; + + /** + * 线程池 + */ + private static final ScheduledThreadPoolExecutor SCHEDULED_THREAD_POOL_EXECUTOR = new ScheduledThreadPoolExecutor(1, + new ThreadFactoryBuilder().setNameFormat("wxMessage-memory-pool-%d").setDaemon(true).build(), new ThreadPoolExecutor.AbortPolicy()); + + /** + * 消息id->消息时间戳的map. + */ + private static final ConcurrentHashMap MSG_ID_2_TIMESTAMP = new ConcurrentHashMap<>(); + + static { + SCHEDULED_THREAD_POOL_EXECUTOR.scheduleAtFixedRate(() -> { + try { + Long now = System.currentTimeMillis(); + MSG_ID_2_TIMESTAMP.entrySet().removeIf(entry -> now - entry.getValue() > TIME_TO_LIVE * 1000); + } catch (Exception ex) { + log.error("重复消息去重任务出现异常", ex); + } + }, 1, CLEAR_PERIOD, TimeUnit.SECONDS); + } + + /** + * 私有化构造方法,避免外部调用 + */ + private WxMessageInMemoryDuplicateCheckerSingleton() { + } + + /** + * 获取单例 + * + * @return 单例对象 + */ + public static WxMessageInMemoryDuplicateCheckerSingleton getInstance() { + return WxMessageInnerClass.CHECKER_SINGLETON; + } + + /** + * 内部类实现单例 + */ + private static class WxMessageInnerClass { + static final WxMessageInMemoryDuplicateCheckerSingleton CHECKER_SINGLETON = new WxMessageInMemoryDuplicateCheckerSingleton(); + } + + /** + * messageId是否重复 + * + * @param messageId messageId + * @return 是否 + */ + @Override + public boolean isDuplicate(String messageId) { + if (messageId == null) { + return false; + } + Long timestamp = MSG_ID_2_TIMESTAMP.putIfAbsent(messageId, System.currentTimeMillis()); + return timestamp != null; + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxMessageInRedisDuplicateChecker.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxMessageInRedisDuplicateChecker.java new file mode 100644 index 0000000000..88c5e9a4e5 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxMessageInRedisDuplicateChecker.java @@ -0,0 +1,47 @@ +package me.chanjar.weixin.common.api; + +import lombok.RequiredArgsConstructor; +import org.redisson.api.RBucket; +import org.redisson.api.RedissonClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.TimeUnit; + +/** + * 利用redis检查消息是否重复 + * + */ +@RequiredArgsConstructor +public class WxMessageInRedisDuplicateChecker implements WxMessageDuplicateChecker { + + /** + * 过期时间 + */ + private int expire = 10; + + private final Logger log = LoggerFactory.getLogger(getClass()); + + private final RedissonClient redissonClient; + + /** + * messageId是否重复 + * + * @param messageId messageId + * @return 是否 + */ + @Override + public boolean isDuplicate(String messageId) { + RBucket r = redissonClient.getBucket("wx:message:duplicate:check:" + messageId); + boolean setSuccess = r.trySet("1", expire, TimeUnit.SECONDS); + return !setSuccess; + } + + public int getExpire() { + return expire; + } + + public void setExpire(int expire) { + this.expire = expire; + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/CommonUploadData.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/CommonUploadData.java new file mode 100644 index 0000000000..ea76137f6b --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/CommonUploadData.java @@ -0,0 +1,76 @@ +package me.chanjar.weixin.common.bean; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.lang.Nullable; + +import java.io.*; +import java.nio.file.Files; + +/** + * 通用文件上传数据 + * + * @author 广州跨界 + * created on 2024/01/11 + */ +@Slf4j +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CommonUploadData implements Serializable { + + /** + * 文件名,如:1.jpg + */ + @Nullable + private String fileName; + + /** + * 文件内容 + * + * @see FileInputStream 文件输入流 + * @see ByteArrayInputStream 字节输入流 + */ + @NotNull + private InputStream inputStream; + + /** + * 文件内容长度(字节数) + */ + private long length; + + /** + * 从文件构造 + * + * @param file 文件 + * @return 通用文件上传数据 + */ + @SneakyThrows + public static CommonUploadData fromFile(File file) { + return new CommonUploadData(file.getName(), Files.newInputStream(file.toPath()), file.length()); + } + + + /** + * 读取所有字节,此方法会关闭输入流 + * + * @return 字节数组 + */ + @SneakyThrows + public byte[] readAllBytes() { + byte[] bytes = new byte[(int) length]; + //noinspection ResultOfMethodCallIgnored + inputStream.read(bytes); + inputStream.close(); + return bytes; + } + + @Override + public String toString() { + return String.format("{fileName:%s, length:%s}", fileName, length); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/CommonUploadParam.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/CommonUploadParam.java new file mode 100644 index 0000000000..42e1869502 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/CommonUploadParam.java @@ -0,0 +1,109 @@ +package me.chanjar.weixin.common.bean; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.SneakyThrows; +import org.jetbrains.annotations.NotNull; +import org.springframework.lang.Nullable; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * 通用文件上传参数 + * + * @author 广州跨界 + * created on 2024/01/11 + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CommonUploadParam implements Serializable { + + /** + * 文件对应的接口参数名称(非文件名),如:media + */ + @NotNull + private String name; + + /** + * 上传数据 + */ + @NotNull + private CommonUploadData data; + + /** + * 额外的表单字段,用于在上传文件的同时提交其他表单数据 + * 例如:上传视频素材时需要提交description字段(JSON格式的视频描述信息) + */ + @Nullable + private Map formFields; + + /** + * 为保持向后兼容保留的 2 参数构造函数。 + *

+ * 仅设置文件参数名和上传数据,额外表单字段将为 {@code null}。 + * + * @param name 参数名,如:media + * @param data 上传数据 + * @deprecated 请使用包含 formFields 参数的构造函数或静态工厂方法 {@link #fromFile(String, File)}、{@link #fromBytes(String, String, byte[])} + */ + @Deprecated + public CommonUploadParam(@NotNull String name, @NotNull CommonUploadData data) { + this(name, data, null); + } + + /** + * 从文件构造 + * + * @param name 参数名,如:media + * @param file 文件 + * @return 文件上传参数对象 + */ + @SneakyThrows + public static CommonUploadParam fromFile(String name, File file) { + return new CommonUploadParam(name, CommonUploadData.fromFile(file), null); + } + + /** + * 从字节数组构造 + * + * @param name 参数名,如:media + * @param bytes 字节数组 + * @return 文件上传参数对象 + */ + @SneakyThrows + public static CommonUploadParam fromBytes(String name, @Nullable String fileName, byte[] bytes) { + return new CommonUploadParam(name, new CommonUploadData(fileName, new ByteArrayInputStream(bytes), bytes.length), null); + } + + /** + * 添加额外的表单字段 + * + * @param fieldName 表单字段名 + * @param fieldValue 表单字段值 + * @return 当前对象,支持链式调用 + */ + public CommonUploadParam addFormField(String fieldName, String fieldValue) { + if (fieldName == null || fieldName.trim().isEmpty()) { + throw new IllegalArgumentException("表单字段名不能为空"); + } + if (fieldValue == null) { + throw new IllegalArgumentException("表单字段值不能为null"); + } + if (this.formFields == null) { + this.formFields = new HashMap<>(); + } + this.formFields.put(fieldName, fieldValue); + return this; + } + + @Override + public String toString() { + return String.format("{name:%s, data:%s, formFields:%s}", name, data, formFields); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ToJson.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ToJson.java new file mode 100644 index 0000000000..6f10e60b71 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ToJson.java @@ -0,0 +1,15 @@ +package me.chanjar.weixin.common.bean; + +/** + * 包含toJson()方法的接口. + * + * @author Binary Wang + * created on 2020-10-05 + */ +public interface ToJson { + /** + * 转换为json字符串 + * @return json字符串 + */ + String toJson(); +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxAccessTokenEntity.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxAccessTokenEntity.java new file mode 100644 index 0000000000..fe19817b2b --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxAccessTokenEntity.java @@ -0,0 +1,19 @@ +package me.chanjar.weixin.common.bean; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +/** + * token + * + * @author cn + */ +@Getter +@Setter +@ToString(callSuper = true) +@EqualsAndHashCode(callSuper = true) +public class WxAccessTokenEntity extends WxAccessToken { + private String appid; +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxNetCheckResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxNetCheckResult.java index 2df2417f57..b5f5762cb3 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxNetCheckResult.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxNetCheckResult.java @@ -13,7 +13,6 @@ */ @Data public class WxNetCheckResult implements Serializable { - private static final long serialVersionUID = 6918924418847404172L; private List dnsInfos = new ArrayList<>(); diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxOAuth2UserInfo.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxOAuth2UserInfo.java new file mode 100644 index 0000000000..906e9de2b1 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxOAuth2UserInfo.java @@ -0,0 +1,66 @@ +package me.chanjar.weixin.common.bean; + + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; + +import java.io.Serializable; + +/** + * oauth2用户个人信息. + * + * @author Binary Wang + * created on 2020-10-11 + */ +@Data +public class WxOAuth2UserInfo implements Serializable { + private static final long serialVersionUID = 3181943506448954725L; + + /** + * openid 普通用户的标识,对当前开发者帐号唯一 + */ + private String openid; + /** + * nickname 普通用户昵称 + */ + private String nickname; + /** + * sex 普通用户性别,1为男性,2为女性 + */ + private Integer sex; + /** + * city 普通用户个人资料填写的城市 + */ + private String city; + + /** + * province 普通用户个人资料填写的省份 + */ + private String province; + /** + * country 国家,如中国为CN + */ + private String country; + /** + * headimgurl 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像), + * 用户没有头像时该项为空 + */ + @SerializedName("headimgurl") + private String headImgUrl; + /** + * unionid 用户统一标识。针对一个微信开放平台帐号下的应用,同一用户的unionid是唯一的。 + */ + @SerializedName("unionid") + private String unionId; + /** + * privilege 用户特权信息,json数组,如微信沃卡用户为(chinaunicom) + */ + @SerializedName("privilege") + private String[] privileges; + + + public static WxOAuth2UserInfo fromJson(String json) { + return WxGsonBuilder.create().fromJson(json, WxOAuth2UserInfo.class); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/imgproc/WxImgProcAiCropResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/imgproc/WxImgProcAiCropResult.java new file mode 100644 index 0000000000..4cfc514d70 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/imgproc/WxImgProcAiCropResult.java @@ -0,0 +1,69 @@ +package me.chanjar.weixin.common.bean.imgproc; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; + +import java.io.Serializable; +import java.util.List; + +/** + * @author Theo Nie + */ +@Data +public class WxImgProcAiCropResult implements Serializable { + private static final long serialVersionUID = -6470673963772979463L; + + @SerializedName("img_size") + private ImgSize imgSize; + + @SerializedName("results") + private List results; + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } + + public static WxImgProcAiCropResult fromJson(String json) { + return WxGsonBuilder.create().fromJson(json, WxImgProcAiCropResult.class); + } + + @Data + public static class ImgSize implements Serializable { + private static final long serialVersionUID = -6470673963772979463L; + + @SerializedName("w") + private int w; + + @SerializedName("h") + private int h; + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } + } + + @Data + public static class Results implements Serializable { + private static final long serialVersionUID = -6470673963772979463L; + + @SerializedName("crop_left") + private int cropLeft; + + @SerializedName("crop_top") + private int cropTop; + + @SerializedName("crop_right") + private int cropRight; + + @SerializedName("crop_bottom") + private int cropBottom; + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/imgproc/WxImgProcQrCodeResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/imgproc/WxImgProcQrCodeResult.java new file mode 100644 index 0000000000..c257146092 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/imgproc/WxImgProcQrCodeResult.java @@ -0,0 +1,103 @@ +package me.chanjar.weixin.common.bean.imgproc; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; + +import java.io.Serializable; +import java.util.List; + +/** + * 二维码/条码识别返回结果 + * + * @author Theo Nie + */ +@Data +public class WxImgProcQrCodeResult implements Serializable { + private static final long serialVersionUID = -1194154790100866123L; + + @SerializedName("img_size") + private ImgSize imgSize; + + @SerializedName("code_results") + private List codeResults; + + @Data + public static class ImgSize implements Serializable { + private static final long serialVersionUID = -8847603245514017839L; + + @SerializedName("w") + private int w; + @SerializedName("h") + private int h; + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } + } + + @Data + public static class CodeResults implements Serializable { + private static final long serialVersionUID = -6138135951229076759L; + + @SerializedName("type_name") + private String typeName; + + @SerializedName("data") + private String data; + + @SerializedName("pos") + private Pos pos; + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } + + @Data + public static class Pos implements Serializable { + private static final long serialVersionUID = 7754894061212819602L; + @SerializedName("left_top") + private Coordinate leftTop; + + @SerializedName("right_top") + private Coordinate rightTop; + + @SerializedName("right_bottom") + private Coordinate rightBottom; + + @SerializedName("left_bottom") + private Coordinate leftBottom; + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } + + @Data + public static class Coordinate implements Serializable { + private static final long serialVersionUID = 8930443668927359677L; + @SerializedName("x") + private int x; + + @SerializedName("y") + private int y; + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } + } + } + } + + public static WxImgProcQrCodeResult fromJson(String json) { + return WxGsonBuilder.create().fromJson(json, WxImgProcQrCodeResult.class); + } + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/imgproc/WxImgProcSuperResolutionResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/imgproc/WxImgProcSuperResolutionResult.java new file mode 100644 index 0000000000..2ce5d3829f --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/imgproc/WxImgProcSuperResolutionResult.java @@ -0,0 +1,28 @@ +package me.chanjar.weixin.common.bean.imgproc; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; + +import java.io.Serializable; + +/** + * 图片高清化返回结果 + * @author Theo Nie + */ +@Data +public class WxImgProcSuperResolutionResult implements Serializable { + private static final long serialVersionUID = 8007440280170407021L; + + @SerializedName("media_id") + private String mediaId; + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } + + public static WxImgProcSuperResolutionResult fromJson(String json) { + return WxGsonBuilder.create().fromJson(json, WxImgProcSuperResolutionResult.class); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/menu/WxMenuButton.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/menu/WxMenuButton.java index 114e267d41..344f544bad 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/menu/WxMenuButton.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/menu/WxMenuButton.java @@ -58,6 +58,15 @@ public class WxMenuButton implements Serializable { @SerializedName("media_id") private String mediaId; + /** + *

+   * 调用发布图文接口获得的article_id.
+   * article_id类型和article_view_limited类型必须
+   * 
+ */ + @SerializedName("article_id") + private String articleId; + /** *
    * 小程序的appid.
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/menu/WxMenuRule.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/menu/WxMenuRule.java
index 49d4e891c4..437b4ba9fe 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/menu/WxMenuRule.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/menu/WxMenuRule.java
@@ -24,6 +24,7 @@ public class WxMenuRule implements Serializable {
   private String country;
   private String province;
   private String city;
+  @SerializedName("client_platform_type")
   private String clientPlatformType;
   private String language;
 
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/oauth2/WxOAuth2AccessToken.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/oauth2/WxOAuth2AccessToken.java
new file mode 100644
index 0000000000..b339844ad6
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/oauth2/WxOAuth2AccessToken.java
@@ -0,0 +1,58 @@
+package me.chanjar.weixin.common.bean.oauth2;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+
+import java.io.Serializable;
+
+/**
+ * OAuth2 AccessToken
+ * 

+ * 参考:{@code https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842} + *

+ * + * @author Daniel Qian + */ +@Data +public class WxOAuth2AccessToken implements Serializable { + private static final long serialVersionUID = -1345910558078620805L; + + @SerializedName("access_token") + private String accessToken; + + @SerializedName("expires_in") + private int expiresIn = -1; + + @SerializedName("refresh_token") + private String refreshToken; + + @SerializedName("openid") + private String openId; + + @SerializedName("scope") + private String scope; + /** + * 是否为快照页模式虚拟账号,只有当用户是快照页模式虚拟账号时返回,值为1 + */ + @SerializedName("is_snapshotuser") + private Integer snapshotUser; + + /** + * 本接口在scope参数为snsapi_base时不再提供unionID字段。 + *

+ * 参考:{@code https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncement&announce_id=11513156443eZYea&version=&lang=zh_CN} + *

+ */ + @SerializedName("unionid") + private String unionId; + + public static WxOAuth2AccessToken fromJson(String json) { + return WxGsonBuilder.create().fromJson(json, WxOAuth2AccessToken.class); + } + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrBankCardResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrBankCardResult.java new file mode 100644 index 0000000000..d5ff0b901d --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrBankCardResult.java @@ -0,0 +1,29 @@ +package me.chanjar.weixin.common.bean.ocr; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; + +import java.io.Serializable; + +/** + * 银行卡OCR识别结果 + * + * @author Theo Nie + */ +@Data +public class WxOcrBankCardResult implements Serializable { + + private static final long serialVersionUID = 554136620394204143L; + @SerializedName("number") + private String number; + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } + + public static WxOcrBankCardResult fromJson(String json) { + return WxGsonBuilder.create().fromJson(json, WxOcrBankCardResult.class); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrBizLicenseResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrBizLicenseResult.java new file mode 100644 index 0000000000..2e83443e95 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrBizLicenseResult.java @@ -0,0 +1,108 @@ +package me.chanjar.weixin.common.bean.ocr; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; + +import java.io.Serializable; + +/** + * @author Theo Nie + */ +@Data +public class WxOcrBizLicenseResult implements Serializable { + private static final long serialVersionUID = -5007671093920178291L; + + /** + * 注册号 + */ + @SerializedName("reg_num") + private String regNum; + /** + * 编号 + */ + @SerializedName("serial") + private String serial; + /** + * 法定代表人姓名 + */ + @SerializedName("legal_representative") + private String legalRepresentative; + /** + * 企业名称 + */ + @SerializedName("enterprise_name") + private String enterpriseName; + /** + * 组成形式 + */ + @SerializedName("type_of_organization") + private String typeOfOrganization; + /** + * 经营场所/企业住所 + */ + @SerializedName("address") + private String address; + /** + * 公司类型 + */ + @SerializedName("type_of_enterprise") + private String typeOfEnterprise; + /** + * 经营范围 + */ + @SerializedName("business_scope") + private String businessScope; + /** + * 注册资本 + */ + @SerializedName("registered_capital") + private String registeredCapital; + /** + * 实收资本 + */ + @SerializedName("paid_in_capital") + private String paidInCapital; + /** + * 营业期限 + */ + @SerializedName("valid_period") + private String validPeriod; + /** + * 注册日期/成立日期 + */ + @SerializedName("registered_date") + private String registeredDate; + /** + * 营业执照位置 + */ + @SerializedName("cert_position") + private CertPosition certPosition; + /** + * 图片大小 + */ + @SerializedName("img_size") + private WxOcrImgSize imgSize; + + public static WxOcrBizLicenseResult fromJson(String json) { + return WxGsonBuilder.create().fromJson(json, WxOcrBizLicenseResult.class); + } + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } + + @Data + public static class CertPosition implements Serializable { + private static final long serialVersionUID = 290286813344131863L; + + @SerializedName("pos") + private WxOcrPos pos; + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrCommResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrCommResult.java new file mode 100644 index 0000000000..5f56d16e3c --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrCommResult.java @@ -0,0 +1,45 @@ +package me.chanjar.weixin.common.bean.ocr; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; + +import java.io.Serializable; +import java.util.List; + +/** + * @author Theo Nie + */ +@Data +public class WxOcrCommResult implements Serializable { + private static final long serialVersionUID = 455833771627756440L; + + @SerializedName("img_size") + private WxOcrImgSize imgSize; + @SerializedName("items") + private List items; + + public static WxOcrCommResult fromJson(String json) { + return WxGsonBuilder.create().fromJson(json, WxOcrCommResult.class); + } + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } + + @Data + public static class Items implements Serializable { + private static final long serialVersionUID = 3066181677009102791L; + + @SerializedName("text") + private String text; + @SerializedName("pos") + private WxOcrPos pos; + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrDrivingLicenseResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrDrivingLicenseResult.java new file mode 100644 index 0000000000..c9306200de --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrDrivingLicenseResult.java @@ -0,0 +1,80 @@ +package me.chanjar.weixin.common.bean.ocr; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; + +import java.io.Serializable; + +/** + * @author Theo Nie + */ +@Data +public class WxOcrDrivingLicenseResult implements Serializable { + private static final long serialVersionUID = -6984670645802585738L; + + /** + * 证号 + */ + @SerializedName("id_num") + private String idNum; + /** + * 姓名 + */ + @SerializedName("name") + private String name; + /** + * 性别 + */ + @SerializedName("sex") + private String sex; + /** + * 国籍 + */ + @SerializedName("nationality") + private String nationality; + /** + * 住址 + */ + @SerializedName("address") + private String address; + /** + * 出生日期 + */ + @SerializedName("birth_date") + private String birthDate; + /** + * 初次领证日期 + */ + @SerializedName("issue_date") + private String issueDate; + /** + * 准驾车型 + */ + @SerializedName("car_class") + private String carClass; + /** + * 有效期限起始日 + */ + @SerializedName("valid_from") + private String validFrom; + /** + * 有效期限终止日 + */ + @SerializedName("valid_to") + private String validTo; + /** + * 印章文字 + */ + @SerializedName("official_seal") + private String officialSeal; + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } + + public static WxOcrDrivingLicenseResult fromJson(String json) { + return WxGsonBuilder.create().fromJson(json, WxOcrDrivingLicenseResult.class); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrDrivingResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrDrivingResult.java new file mode 100644 index 0000000000..b486baf1c4 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrDrivingResult.java @@ -0,0 +1,133 @@ +package me.chanjar.weixin.common.bean.ocr; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; + +import java.io.Serializable; + +/** + * @author Theo Nie + */ +@Data +public class WxOcrDrivingResult implements Serializable { + private static final long serialVersionUID = -7477484374200211303L; + + /** + * 车牌号码 + */ + @SerializedName("plate_num") + private String plateNum; + /** + * 车辆类型 + */ + @SerializedName("vehicle_type") + private String vehicleType; + /** + * 所有人 + */ + @SerializedName("owner") + private String owner; + /** + * 住址 + */ + @SerializedName("addr") + private String addr; + /** + * 使用性质 + */ + @SerializedName("use_character") + private String useCharacter; + /** + * 品牌型号 + */ + @SerializedName("model") + private String model; + /** + * 车辆识别代码 + */ + @SerializedName("vin") + private String vin; + /** + * 发动机号码 + */ + @SerializedName("engine_num") + private String engineNum; + /** + * 注册日期 + */ + @SerializedName("register_date") + private String registerDate; + /** + * 发证日期 + */ + @SerializedName("issue_date") + private String issueDate; + /** + * 车牌号码 + */ + @SerializedName("plate_num_b") + private String plateNumB; + /** + * 号牌 + */ + @SerializedName("record") + private String record; + /** + * 核定载人数 + */ + @SerializedName("passengers_num") + private String passengersNum; + /** + * 总质量 + */ + @SerializedName("total_quality") + private String totalQuality; + /** + * 整备质量 + */ + @SerializedName("prepare_quality") + private String prepareQuality; + /** + * 外廓尺寸 + */ + @SerializedName("overall_size") + private String overallSize; + /** + * 卡片正面位置(检测到卡片正面才会返回) + */ + @SerializedName("card_position_front") + private CardPosition cardPositionFront; + /** + * 卡片反面位置(检测到卡片反面才会返回) + */ + @SerializedName("card_position_back") + private CardPosition cardPositionBack; + /** + * 图片大小 + */ + @SerializedName("img_size") + private WxOcrImgSize imgSize; + + @Data + public static class CardPosition implements Serializable { + private static final long serialVersionUID = 2884515165228160517L; + + @SerializedName("pos") + private WxOcrPos pos; + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } + } + + public static WxOcrDrivingResult fromJson(String json) { + return WxGsonBuilder.create().fromJson(json, WxOcrDrivingResult.class); + } + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrIdCardResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrIdCardResult.java new file mode 100644 index 0000000000..a50bd96e55 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrIdCardResult.java @@ -0,0 +1,38 @@ +package me.chanjar.weixin.common.bean.ocr; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; + +import java.io.Serializable; + +/** + * OCR身份证识别结果. + * + * @author Binary Wang + * created on 2019-06-23 + */ +@Data +public class WxOcrIdCardResult implements Serializable { + private static final long serialVersionUID = 8184352486986729980L; + + @SerializedName("type") + private String type; + @SerializedName("name") + private String name; + @SerializedName("id") + private String id; + @SerializedName("addr") + private String addr; + @SerializedName("gender") + private String gender; + @SerializedName("nationality") + private String nationality; + @SerializedName("valid_date") + private String validDate; + + public static WxOcrIdCardResult fromJson(String json) { + return WxGsonBuilder.create().fromJson(json, WxOcrIdCardResult.class); + } + +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrImgSize.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrImgSize.java new file mode 100644 index 0000000000..f5446ab405 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrImgSize.java @@ -0,0 +1,25 @@ +package me.chanjar.weixin.common.bean.ocr; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; + +import java.io.Serializable; + +/** + * @author Theo Nie + */ +@Data +public class WxOcrImgSize implements Serializable { + private static final long serialVersionUID = 5234409123551074168L; + + @SerializedName("w") + private int w; + @SerializedName("h") + private int h; + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrPos.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrPos.java new file mode 100644 index 0000000000..54089f3235 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrPos.java @@ -0,0 +1,43 @@ +package me.chanjar.weixin.common.bean.ocr; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; + +import java.io.Serializable; + +/** + * @author Theo Nie + */ +@Data +public class WxOcrPos implements Serializable { + private static final long serialVersionUID = 4204160206873907920L; + + @SerializedName("left_top") + private Coordinate leftTop; + @SerializedName("right_top") + private Coordinate rightTop; + @SerializedName("right_bottom") + private Coordinate rightBottom; + @SerializedName("left_bottom") + private Coordinate leftBottom; + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } + + @Data + public static class Coordinate implements Serializable { + private static final long serialVersionUID = 8675059935386304399L; + @SerializedName("x") + private int x; + @SerializedName("y") + private int y; + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxMinishopImageUploadCustomizeResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxMinishopImageUploadCustomizeResult.java new file mode 100644 index 0000000000..5427d5cada --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxMinishopImageUploadCustomizeResult.java @@ -0,0 +1,40 @@ +package me.chanjar.weixin.common.bean.result; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import lombok.Data; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; + +import java.io.Serializable; + +@Data +public class WxMinishopImageUploadCustomizeResult implements Serializable { + private String errcode; + private String errmsg; + + private WxMinishopPicFileCustomizeResult imgInfo; + + public static WxMinishopImageUploadCustomizeResult fromJson(String json) { + JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject(); + WxMinishopImageUploadCustomizeResult result = new WxMinishopImageUploadCustomizeResult(); + result.setErrcode(jsonObject.get(WxConsts.ERR_CODE).getAsNumber().toString()); + if (result.getErrcode().equals("0")) { + WxMinishopPicFileCustomizeResult picFileResult = new WxMinishopPicFileCustomizeResult(); + JsonObject picObject = jsonObject.get("img_info").getAsJsonObject(); + if (picObject.has("media_id")) { + picFileResult.setMediaId(picObject.get("media_id").getAsString()); + } + if (picObject.has("temp_img_url")) { + picFileResult.setTempImgUrl(picObject.get("temp_img_url").getAsString()); + } + result.setImgInfo(picFileResult); + } + return result; + } + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxMinishopImageUploadResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxMinishopImageUploadResult.java new file mode 100644 index 0000000000..9c2cbaf3ba --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxMinishopImageUploadResult.java @@ -0,0 +1,46 @@ +package me.chanjar.weixin.common.bean.result; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import lombok.Data; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; + +import java.io.Serializable; + +@Data +public class WxMinishopImageUploadResult implements Serializable { + private static final long serialVersionUID = 330834334738622332L; + + private String errcode; + private String errmsg; + + + private WxMinishopPicFileResult picFile; + + + public static WxMinishopImageUploadResult fromJson(String json) { + JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject(); + WxMinishopImageUploadResult result = new WxMinishopImageUploadResult(); + result.setErrcode(jsonObject.get(WxConsts.ERR_CODE).getAsNumber().toString()); + if (result.getErrcode().equals("0")) { + WxMinishopPicFileResult picFileResult = new WxMinishopPicFileResult(); + JsonObject picObject = jsonObject.get("pic_file").getAsJsonObject(); + JsonElement mediaId = picObject.get("media_id"); + picFileResult.setMediaId(mediaId==null ? "" : mediaId.getAsString()); + JsonElement payMediaId = picObject.get("pay_media_id"); + picFileResult.setPayMediaId(payMediaId==null ? "" : payMediaId.getAsString()); + JsonElement tempImgUrl = picObject.get("temp_img_url"); + picFileResult.setTempImgUrl(tempImgUrl==null ? "" : tempImgUrl.getAsString()); + result.setPicFile(picFileResult); + + } + return result; + } + + @Override + public String toString() { + return WxGsonBuilder.create().toJson(this); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxMinishopPicFileCustomizeResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxMinishopPicFileCustomizeResult.java new file mode 100644 index 0000000000..8f2f36f8dd --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxMinishopPicFileCustomizeResult.java @@ -0,0 +1,11 @@ +package me.chanjar.weixin.common.bean.result; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class WxMinishopPicFileCustomizeResult implements Serializable { + private String mediaId; + private String tempImgUrl; +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxMinishopPicFileResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxMinishopPicFileResult.java new file mode 100644 index 0000000000..2ae2e2320b --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/result/WxMinishopPicFileResult.java @@ -0,0 +1,12 @@ +package me.chanjar.weixin.common.bean.result; + +import lombok.Data; + +import java.io.Serializable; + +@Data +public class WxMinishopPicFileResult implements Serializable { + private String mediaId; + private String payMediaId; + private String tempImgUrl; +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/subscribemsg/CategoryData.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/subscribemsg/CategoryData.java new file mode 100644 index 0000000000..997beb91ac --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/subscribemsg/CategoryData.java @@ -0,0 +1,19 @@ +package me.chanjar.weixin.common.bean.subscribemsg; + +import lombok.Data; + +import java.io.Serializable; + +/** + * . + * + * @author Binary Wang + * created on 2021-01-27 + */ +@Data +public class CategoryData implements Serializable { + private static final long serialVersionUID = -5935548352317679892L; + + private int id; + private String name; +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/subscribemsg/PubTemplateKeyword.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/subscribemsg/PubTemplateKeyword.java new file mode 100644 index 0000000000..3f4681047b --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/subscribemsg/PubTemplateKeyword.java @@ -0,0 +1,21 @@ +package me.chanjar.weixin.common.bean.subscribemsg; + +import lombok.Data; + +import java.io.Serializable; + +/** + * . + * + * @author Binary Wang + * created on 2021-01-27 + */ +@Data +public class PubTemplateKeyword implements Serializable { + private static final long serialVersionUID = -1100641668859815647L; + + private int kid; + private String name; + private String example; + private String rule; +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/subscribemsg/PubTemplateTitleListResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/subscribemsg/PubTemplateTitleListResult.java new file mode 100644 index 0000000000..32154a14f0 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/subscribemsg/PubTemplateTitleListResult.java @@ -0,0 +1,36 @@ +package me.chanjar.weixin.common.bean.subscribemsg; + +import lombok.Data; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; + +import java.io.Serializable; +import java.util.List; + +/** + * @author ArBing + */ +@Data +public class PubTemplateTitleListResult implements Serializable { + private static final long serialVersionUID = -7718911668757837527L; + + private int count; + + private List data; + + public static PubTemplateTitleListResult fromJson(String json) { + return WxGsonBuilder.create().fromJson(json, PubTemplateTitleListResult.class); + } + + @Data + public static class TemplateItem implements Serializable { + private static final long serialVersionUID = 6888726696879905332L; + + private Integer type; + + private Integer tid; + + private String categoryId; + + private String title; + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/subscribemsg/TemplateInfo.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/subscribemsg/TemplateInfo.java new file mode 100644 index 0000000000..64222480ad --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/subscribemsg/TemplateInfo.java @@ -0,0 +1,22 @@ +package me.chanjar.weixin.common.bean.subscribemsg; + +import lombok.Data; + +import java.io.Serializable; + +/** + * . + * + * @author Binary Wang + * created on 2021-01-27 + */ +@Data +public class TemplateInfo implements Serializable { + private static final long serialVersionUID = 6971785763573992264L; + + private String priTmplId; + private String title; + private String content; + private String example; + private int type; +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/enums/TicketType.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/enums/TicketType.java new file mode 100644 index 0000000000..afbd1ec382 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/enums/TicketType.java @@ -0,0 +1,35 @@ +package me.chanjar.weixin.common.enums; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +/** + *
+ * ticket类型枚举
+ * Created by Binary Wang on 2018/11/18.
+ * 
+ * + * @author Binary Wang + */ +@Getter +@RequiredArgsConstructor +public enum TicketType { + /** + * jsapi + */ + JSAPI("jsapi"), + /** + * sdk + */ + SDK("2"), + /** + * 微信卡券 + */ + WX_CARD("wx_card"); + + /** + * type代码 + */ + private final String code; + +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/WxType.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/enums/WxType.java similarity index 80% rename from weixin-java-common/src/main/java/me/chanjar/weixin/common/WxType.java rename to weixin-java-common/src/main/java/me/chanjar/weixin/common/enums/WxType.java index 4230506763..9d7d601a0a 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/WxType.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/enums/WxType.java @@ -1,4 +1,4 @@ -package me.chanjar.weixin.common; +package me.chanjar.weixin.common.enums; /** *
@@ -28,5 +28,10 @@ public enum WxType {
   /**
    * 微信支付.
    */
-  Pay;
+  Pay,
+  /**
+   * 微信视频号
+   */
+  Channel,
+  ;
 }
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxChannelErrorMsgEnum.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxChannelErrorMsgEnum.java
new file mode 100644
index 0000000000..3491e74dc8
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxChannelErrorMsgEnum.java
@@ -0,0 +1,172 @@
+package me.chanjar.weixin.common.error;
+
+import com.google.common.collect.Maps;
+import java.util.Map;
+
+/**
+ *
+ * 
+ *     微信小店公共错误码.
+ *     参考文档:微信小店公共错误码
+ * 
+ * + * @author Zeyes + */ +public enum WxChannelErrorMsgEnum { + /** + * 系统繁忙,此时请开发者稍候再试 system error + */ + CODE_1(-1, "系统繁忙,此时请开发者稍候再试"), + /** + * 请求成功 ok + */ + CODE_0(0, "请求成功"), + /** + * 获取 access_token 时 AppSecret 错误,或者 access_token 无效。请开发者认真检查 AppSecret 的正确性 + * invalid credential, access_token is invalid or not latest, could get access_token by getStableAccessToken, more details at https://mmbizurl.cn/s/JtxxFh33r + */ + CODE_40001(40001, "获取 access_token 时 AppSecret 错误,或者 access_token 无效。请开发者认真检查 AppSecret 的正确性"), + /** + * 请检查 openid 的正确性 + * invalid openid + */ + CODE_40003(40003, "请检查 openid 的正确性"), + /** + * 请检查 appid 的正确性,避免异常字符,注意大小写 + * invalid appid + */ + CODE_40013(40013, "请检查 appid 的正确性,避免异常字符,注意大小写"), + /** + * 请检查API的URL是否与文档一致 + * invalid url + */ + CODE_40066(40066, "请检查API的URL是否与文档一致"), + /** + * 缺少 access_token 参数 + * access_token missing + */ + CODE_41001(41001, "缺少 access_token 参数"), + /** + * 请检查URL参数中是否有 ?appid= + * appid missing + */ + CODE_41002(41002, "请检查URL参数中是否有 ?appid="), + /** + * 请检查POST json中是否包含component_ appid宇段 + * missing component_appid + */ + CODE_41018(41018, "请检查POST json中是否包含component_ appid宇段"), + /** + * access_token失效,需要重新获取新的access_token + * access_token expired + */ + CODE_42001(42001, "access_token失效,需要重新获取新的access_token"), + /** + * 请检查发起API请求的Method是否为POST + * require POST method + */ + CODE_43002(43002, "请检查发起API请求的Method是否为POST"), + /** + * 请使用HTTPS方式清求,不要使用HTTP方式 + * require https + */ + CODE_43003(43003, "请使用HTTPS方式清求,不要使用HTTP方式"), + /** + * POST 的数据包为空 + * empty post data + */ + CODE_44002(44002, "POST 的数据包为空"), + /** + * 请对数据进行压缩 + * content size out of limit + */ + CODE_45002(45002, "请对数据进行压缩"), + /** + * 查看调用次数是否符合预期,可通过get_api_quota接口获取每天的调用quota;用完后可通过clear_quota进行清空 + * reach max api daily quota limit + */ + CODE_45009(45009, "查看调用次数是否符合预期,可通过get_api_quota接口获取每天的调用quota;用完后可通过clear_quota进行清空"), + /** + * 命中每分钟的频率限制 + * api minute-quota reach limit must slower retry next minute + */ + CODE_45011(45011, "命中每分钟的频率限制"), + /** + * 需要登录 channels.weixin.qq.com/shop 配置IP白名单 + * access clientip is not registered, not in ip-white-list + */ + CODE_45035(45035, "需要登录 channels.weixin.qq.com/shop 配置IP白名单"), + /** + * 解析 JSON/XML 内容错误 + * data format error + */ + CODE_47001(47001, "解析 JSON/XML 内容错误"), + /** + * 没有该接口权限 + * api unauthorized + */ + CODE_48001(48001, "没有该接口权限"), + /** + * 接口被禁用 + * api forbidden for irregularities + */ + CODE_48004(48004, "接口被禁用"), + /** + * 请找用户获取该api授权 + * user unauthorized + */ + CODE_50001(50001, "请找用户获取该api授权"), + /** + * 请检查封禁原因 + * user limited + */ + CODE_50002(50002, "请检查封禁原因"), + /** + * 需要登录 channels.weixin.qq.com/shop 配置IP白名单 + * access clientip is not registered, not in ip-white-list + */ + CODE_61004(61004, "需要登录 channels.weixin.qq.com/shop 配置IP白名单"), + /** + * 请检查第三方平台服务商检查已获取的授权集 + * api is unauthorized to component + */ + CODE_61007(61007, "请检查第三方平台服务商检查已获取的授权集"), + /** + * 需要登录 channels.weixin.qq.com/shop 继续完成注销 + * 账号发起注销,进入注销公示期 + */ + CODE_10080000(10080000, "需要登录 channels.weixin.qq.com/shop 继续完成注销"), + /** + * 账号已注销 + */ + CODE_10080001(10080001, "账号已注销"), + /** + * 小店的视频号带货身份为达人号,不允许使用该功能,如需使用,请将带货身份修改为商家 + */ + CODE_10080002(10080002, "小店的视频号带货身份为达人号,不允许使用该功能,如需使用,请将带货身份修改为商家"), + + ; + + private final int code; + private final String msg; + + WxChannelErrorMsgEnum(int code, String msg) { + this.code = code; + this.msg = msg; + } + + static final Map valueMap = Maps.newHashMap(); + + static { + for (WxChannelErrorMsgEnum value : WxChannelErrorMsgEnum.values()) { + valueMap.put(value.code, value.msg); + } + } + + /** + * 通过错误代码查找其中文含义. + */ + public static String findMsgByCode(int code) { + return valueMap.getOrDefault(code, null); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxCpErrorMsgEnum.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxCpErrorMsgEnum.java index 5529d69759..356d1dbbf9 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxCpErrorMsgEnum.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxCpErrorMsgEnum.java @@ -1,11 +1,14 @@ package me.chanjar.weixin.common.error; +import com.google.common.collect.Maps; import lombok.Getter; +import java.util.Map; + /** *
  * 企业微信全局错误码.
- * 参考文档:企业微信全局错误码
+ * 参考文档:企业微信全局错误码
  * Created by Binary Wang on 2018/5/13.
  * 
* @@ -245,6 +248,10 @@ public enum WxCpErrorMsgEnum { * 不合法的URL;缺少主页URL参数,或者URL不合法(链接需要带上协议头,以 http:// 或者 https:// 开头). */ CODE_40094(40094, "不合法的URL;缺少主页URL参数,或者URL不合法(链接需要带上协议头,以 http:// 或者 https:// 开头)"), + /** + * 不合法的外部联系人userid + */ + CODE_40096(40096,"不合法的外部联系人userid"), /** * 缺少access_token参数. */ @@ -446,7 +453,7 @@ public enum WxCpErrorMsgEnum { */ CODE_60008(60008, "部门已存在;部门ID或者部门名称已存在"), /** - * 部门名称含有非法字符;不能含有 \\:?*“< >| 等字符. + * {@code 部门名称含有非法字符;不能含有 \\:?*"< >| 等字符.} */ CODE_60009(60009, "部门名称含有非法字符;不能含有 \\ :?*“< >| 等字符"), /** @@ -514,7 +521,7 @@ public enum WxCpErrorMsgEnum { */ CODE_60124(60124, "无效的父部门id;父部门不存在通讯录中"), /** - * 非法部门名字;不能为空,且不能超过64字节,且不能含有\\:*?”< >|等字符. + * {@code 非法部门名字;不能为空,且不能超过64字节,且不能含有\\:*?"< >|等字符.} */ CODE_60125(60125, "非法部门名字;不能为空,且不能超过64字节,且不能含有\\:*?”< >|等字符"), /** @@ -609,6 +616,322 @@ public enum WxCpErrorMsgEnum { * 不符合的state参数;必须是[a-zA-Z0-9]的参数值,长度不可超过128个字节. */ CODE_84025(84025, "不符合的state参数;必须是[a-zA-Z0-9]的参数值,长度不可超过128个字节"), + /** + * 缺少caller参数. + */ + CODE_84052(84052, "缺少caller参数"), + /** + * 缺少callee参数. + */ + CODE_84053(84053, "缺少callee参数"), + /** + * 缺少auth_corpid参数. + */ + CODE_84054(84054, "缺少auth_corpid参数"), + /** + * 超过拨打公费电话频率。排查方法:同一个客服5秒内只能调用api拨打一次公费电话 + */ + CODE_84055(84055, "超过拨打公费电话频率。排查方法:同一个客服5秒内只能调用api拨打一次公费电话"), + /** + * 被拨打用户安装应用时未授权拨打公费电话权限. + */ + CODE_84056(84056, "被拨打用户安装应用时未授权拨打公费电话权限"), + /** + * 公费电话余额不足. + */ + CODE_84057(84057, "公费电话余额不足"), + /** + * caller + */ + CODE_84058(84058, "caller 呼叫号码不支持"), + /** + * 号码非法. + */ + CODE_84059(84059, "号码非法"), + /** + * callee + */ + CODE_84060(84060, "callee 呼叫号码不支持"), + /** + * 不存在外部联系人的关系. + */ + CODE_84061(84061, "不存在外部联系人的关系"), + /** + * 未开启公费电话应用. + */ + CODE_84062(84062, "未开启公费电话应用"), + /** + * caller不存在. + */ + CODE_84063(84063, "caller不存在"), + /** + * callee不存在. + */ + CODE_84064(84064, "callee不存在"), + /** + * caller跟callee电话号码一致。排查方法:不允许自己拨打给自己 + */ + CODE_84065(84065, "caller跟callee电话号码一致。排查方法:不允许自己拨打给自己"), + /** + * 服务商拨打次数超过限制。排查方法:单个企业管理员,在一天(以上午10 + */ + CODE_84066(84066, "服务商拨打次数超过限制。排查方法:单个企业管理员,在一天(以上午10:00为起始时间)内,对应单个服务商,只能被呼叫【4】次。"), + /** + * 管理员收到的服务商公费电话个数超过限制。排查方法:单个企业管理员,在一天(以上午10 + */ + CODE_84067(84067, "管理员收到的服务商公费电话个数超过限制。排查方法:单个企业管理员,在一天(以上午10:00为起始时间)内,一共只能被【3】个服务商成功呼叫。"), + /** + * 拨打方被限制拨打公费电话. + */ + CODE_84069(84069, "拨打方被限制拨打公费电话"), + /** + * 不支持的电话号码。排查方法:拨打方或者被拨打方电话号码不支持 + */ + CODE_84070(84070, "不支持的电话号码。排查方法:拨打方或者被拨打方电话号码不支持"), + /** + * 不合法的外部联系人授权码。排查方法:非法或者已经消费过 + */ + CODE_84071(84071, "不合法的外部联系人授权码。排查方法:非法或者已经消费过"), + /** + * 应用未配置客服. + */ + CODE_84072(84072, "应用未配置客服"), + /** + * 客服userid不在应用配置的客服列表中. + */ + CODE_84073(84073, "客服userid不在应用配置的客服列表中"), + /** + * 没有外部联系人权限. + */ + CODE_84074(84074, "没有外部联系人权限"), + /** + * 不合法或过期的authcode. + */ + CODE_84075(84075, "不合法或过期的authcode"), + /** + * 缺失authcode. + */ + CODE_84076(84076, "缺失authcode"), + /** + * 订单价格过高,无法受理. + */ + CODE_84077(84077, "订单价格过高,无法受理"), + /** + * 购买人数不正确. + */ + CODE_84078(84078, "购买人数不正确"), + /** + * 价格策略不存在. + */ + CODE_84079(84079, "价格策略不存在"), + /** + * 订单不存在. + */ + CODE_84080(84080, "订单不存在"), + /** + * 存在未支付订单. + */ + CODE_84081(84081, "存在未支付订单"), + /** + * 存在申请退款中的订单. + */ + CODE_84082(84082, "存在申请退款中的订单"), + /** + * 非服务人员. + */ + CODE_84083(84083, "非服务人员"), + /** + * 非跟进用户. + */ + CODE_84084(84084, "非跟进用户"), + /** + * 应用已下架. + */ + CODE_84085(84085, "应用已下架"), + /** + * 订单人数超过可购买最大人数. + */ + CODE_84086(84086, "订单人数超过可购买最大人数"), + /** + * 打开订单支付前禁止关闭订单. + */ + CODE_84087(84087, "打开订单支付前禁止关闭订单"), + /** + * 禁止关闭已支付的订单. + */ + CODE_84088(84088, "禁止关闭已支付的订单"), + /** + * 订单已支付. + */ + CODE_84089(84089, "订单已支付"), + /** + * 缺失user_ticket. + */ + CODE_84090(84090, "缺失user_ticket"), + /** + * 订单价格不可低于下限. + */ + CODE_84091(84091, "订单价格不可低于下限"), + /** + * 无法发起代下单操作. + */ + CODE_84092(84092, "无法发起代下单操作"), + /** + * 代理关系已占用,无法代下单. + */ + CODE_84093(84093, "代理关系已占用,无法代下单"), + /** + * 该应用未配置代理分润规则,请先联系应用服务商处理. + */ + CODE_84094(84094, "该应用未配置代理分润规则,请先联系应用服务商处理"), + /** + * 免费试用版,无法扩容. + */ + CODE_84095(84095, "免费试用版,无法扩容"), + /** + * 免费试用版,无法续期. + */ + CODE_84096(84096, "免费试用版,无法续期"), + /** + * 当前企业有未处理订单. + */ + CODE_84097(84097, "当前企业有未处理订单"), + /** + * 固定总量,无法扩容. + */ + CODE_84098(84098, "固定总量,无法扩容"), + /** + * 非购买状态,无法扩容. + */ + CODE_84099(84099, "非购买状态,无法扩容"), + /** + * 未购买过此应用,无法续期. + */ + CODE_84100(84100, "未购买过此应用,无法续期"), + /** + * 企业已试用付费版本,无法全新购买. + */ + CODE_84101(84101, "企业已试用付费版本,无法全新购买"), + /** + * 企业当前应用状态已过期,无法扩容. + */ + CODE_84102(84102, "企业当前应用状态已过期,无法扩容"), + /** + * 仅可修改未支付订单. + */ + CODE_84103(84103, "仅可修改未支付订单"), + /** + * 订单已支付,无法修改. + */ + CODE_84104(84104, "订单已支付,无法修改"), + /** + * 订单已被取消,无法修改. + */ + CODE_84105(84105, "订单已被取消,无法修改"), + /** + * 企业含有该应用的待支付订单,无法代下单. + */ + CODE_84106(84106, "企业含有该应用的待支付订单,无法代下单"), + /** + * 企业含有该应用的退款中订单,无法代下单. + */ + CODE_84107(84107, "企业含有该应用的退款中订单,无法代下单"), + /** + * 企业含有该应用的待生效订单,无法代下单. + */ + CODE_84108(84108, "企业含有该应用的待生效订单,无法代下单"), + /** + * 订单定价不能未0. + */ + CODE_84109(84109, "订单定价不能未0"), + /** + * 新安装应用不在试用状态,无法升级为付费版. + */ + CODE_84110(84110, "新安装应用不在试用状态,无法升级为付费版"), + /** + * 无足够可用优惠券. + */ + CODE_84111(84111, "无足够可用优惠券"), + /** + * 无法关闭未支付订单. + */ + CODE_84112(84112, "无法关闭未支付订单"), + /** + * 无付费信息. + */ + CODE_84113(84113, "无付费信息"), + /** + * 虚拟版本不支持下单. + */ + CODE_84114(84114, "虚拟版本不支持下单"), + /** + * 虚拟版本不支持扩容. + */ + CODE_84115(84115, "虚拟版本不支持扩容"), + /** + * 虚拟版本不支持续期. + */ + CODE_84116(84116, "虚拟版本不支持续期"), + /** + * 在虚拟正式版期内不能扩容. + */ + CODE_84117(84117, "在虚拟正式版期内不能扩容"), + /** + * 虚拟正式版期内不能变更版本. + */ + CODE_84118(84118, "虚拟正式版期内不能变更版本"), + /** + * 当前企业未报备,无法进行代下单. + */ + CODE_84119(84119, "当前企业未报备,无法进行代下单"), + /** + * 当前应用版本已删除. + */ + CODE_84120(84120, "当前应用版本已删除"), + /** + * 应用版本已删除,无法扩容. + */ + CODE_84121(84121, "应用版本已删除,无法扩容"), + /** + * 应用版本已删除,无法续期. + */ + CODE_84122(84122, "应用版本已删除,无法续期"), + /** + * 非虚拟版本,无法升级. + */ + CODE_84123(84123, "非虚拟版本,无法升级"), + /** + * 非行业方案订单,不能添加部分应用版本的订单. + */ + CODE_84124(84124, "非行业方案订单,不能添加部分应用版本的订单"), + /** + * 购买人数不能少于最少购买人数. + */ + CODE_84125(84125, "购买人数不能少于最少购买人数"), + /** + * 购买人数不能多于最大购买人数. + */ + CODE_84126(84126, "购买人数不能多于最大购买人数"), + /** + * 无应用管理权限. + */ + CODE_84127(84127, "无应用管理权限"), + /** + * 无该行业方案下全部应用的管理权限. + */ + CODE_84128(84128, "无该行业方案下全部应用的管理权限"), + /** + * 付费策略已被删除,无法下单. + */ + CODE_84129(84129, "付费策略已被删除,无法下单"), + /** + * 订单生效时间不合法. + */ + CODE_84130(84130, "订单生效时间不合法"), + /** + * 文件转译解析错误。排查方法:只支持utf8文件转译,可能是不支持的文件类型或者格式 + */ + CODE_84200(84200, "文件转译解析错误。排查方法:只支持utf8文件转译,可能是不支持的文件类型或者格式"), /** * 包含不合法的词语. */ @@ -753,10 +1076,46 @@ public enum WxCpErrorMsgEnum { * 获取打卡记录时间间隔超限;保证开始时间大于0 且结束时间大于 0 且结束时间大于开始时间,且间隔少于93天. */ CODE_301024(301024, "获取打卡记录时间间隔超限;保证开始时间大于0 且结束时间大于 0 且结束时间大于开始时间,且间隔少于93天"), + /** + * 提交审批单请求参数错误 + */ + CODE_301025(301025, "提交审批单请求参数错误"), /** * 不允许更新该用户的userid. */ CODE_301036(301036, "不允许更新该用户的userid"), + /** + * 无审批应用权限,或者提单者不在审批应用/自建应用的可见范围 + */ + CODE_301055(301055, "无审批应用权限,或者提单者不在审批应用/自建应用的可见范围"), + /** + * 审批应用已停用 + */ + CODE_301056(301056, "审批应用已停用"), + /** + * 通用错误码,提交审批单内部接口失败 + */ + CODE_301057(301057, "通用错误码,提交审批单内部接口失败"), + /** + * 输入userid无对应成员 + */ + CODE_301069(301069,"输入userid无对应成员"), + /** + * 系统错误,请稍后再试 + */ + CODE_301070(301070,"系统错误,请稍后再试"), + /** + * 企业内有其他人员有相似人脸,此情况下人脸仍然会录入成功 + */ + CODE_301071(301071,"企业内有其他人员有相似人脸,此情况下人脸仍然会录入成功"), + /** + * 人脸图像数据错误请更换图片 + */ + CODE_301072(301072,"企业内有其他人员有相似人脸,此情况下人脸仍然会录入成功"), + /** + * 输入参数错误 + */ + CODE_301075(301075,"输入参数错误"), /** * 批量导入任务的文件中userid有重复. */ @@ -782,24 +1141,26 @@ public enum WxCpErrorMsgEnum { */ CODE_2000002(2000002, "CorpId参数无效;指定的CorpId不存在"); - private int code; - private String msg; + private final int code; + private final String msg; WxCpErrorMsgEnum(int code, String msg) { this.code = code; this.msg = msg; } + static final Map valueMap = Maps.newHashMap(); + + static { + for (WxCpErrorMsgEnum value : WxCpErrorMsgEnum.values()) { + valueMap.put(value.code, value.msg); + } + } + /** * 通过错误代码查找其中文含义.. */ public static String findMsgByCode(int code) { - for (WxCpErrorMsgEnum value : WxCpErrorMsgEnum.values()) { - if (value.code == code) { - return value.msg; - } - } - - return null; + return valueMap.getOrDefault(code, null); } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxError.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxError.java index d53e7fb25c..1aab7f1f20 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxError.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxError.java @@ -1,8 +1,10 @@ package me.chanjar.weixin.common.error; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; -import me.chanjar.weixin.common.WxType; +import lombok.NoArgsConstructor; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.util.json.WxGsonBuilder; import org.apache.commons.lang3.StringUtils; @@ -10,13 +12,17 @@ /** * 微信错误码. + *

* 请阅读: - * 公众平台:全局返回码说明 + * 公众平台:全局返回码说明 * 企业微信:全局错误码 + *

* - * @author Daniel Qian & Binary Wang + * @author Daniel Qian, Binary Wang */ @Data +@NoArgsConstructor +@AllArgsConstructor @Builder public class WxError implements Serializable { private static final long serialVersionUID = 7869786563361406291L; @@ -39,6 +45,11 @@ public class WxError implements Serializable { private String json; + public WxError(int errorCode, String errorMsg) { + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + public static WxError fromJson(String json) { return fromJson(json, null); } @@ -75,6 +86,20 @@ public static WxError fromJson(String json, WxType type) { } break; } + case Open: { + final String msg = WxOpenErrorMsgEnum.findMsgByCode(wxError.getErrorCode()); + if (msg != null) { + wxError.setErrorMsg(msg); + } + break; + } + case Channel: { + final String msg = WxChannelErrorMsgEnum.findMsgByCode(wxError.getErrorCode()); + if (msg != null) { + wxError.setErrorMsg(msg); + } + break; + } default: return wxError; } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxErrorException.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxErrorException.java index 6e9a2c538d..992081da07 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxErrorException.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxErrorException.java @@ -6,7 +6,13 @@ public class WxErrorException extends Exception { private static final long serialVersionUID = -6357149550353160810L; - private WxError error; + private final WxError error; + + private static final int DEFAULT_ERROR_CODE = -99; + + public WxErrorException(String message) { + this(WxError.builder().errorCode(DEFAULT_ERROR_CODE).errorMsg(message).build()); + } public WxErrorException(WxError error) { super(error.toString()); @@ -18,9 +24,12 @@ public WxErrorException(WxError error, Throwable cause) { this.error = error; } + public WxErrorException(Throwable cause) { + super(cause.getMessage(), cause); + this.error = WxError.builder().errorCode(DEFAULT_ERROR_CODE).errorMsg(cause.getMessage()).build(); + } + public WxError getError() { return this.error; } - - } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnum.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnum.java index f1ced3fd09..3f380543b0 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnum.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnum.java @@ -1,7 +1,10 @@ package me.chanjar.weixin.common.error; +import com.google.common.collect.Maps; import lombok.Getter; +import java.util.Map; + /** * 微信小程序错误码 * @@ -43,23 +46,23 @@ public enum WxMaErrorMsgEnum { */ CODE_40003(40003, "openid 不正确"), /** - *
    * 无效媒体文件类型
-   * 对应操作:uploadTempMedia
+   * 

+ * 对应操作:{@code uploadTempMedia} * 对应地址: - * POST https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE + * {@code POST https://api.weixin.qq.com/cgi-bin/media/upload?access_token=ACCESS_TOKEN&type=TYPE} * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/uploadTempMedia.html - *

+ *

*/ CODE_40004(40004, "无效媒体文件类型"), /** - *
    * 无效媒体文件 ID.
-   * 对应操作:getTempMedia
+   * 

+ * 对应操作:{@code getTempMedia} * 对应地址: - * GET https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID + * {@code GET https://api.weixin.qq.com/cgi-bin/media/get?access_token=ACCESS_TOKEN&media_id=MEDIA_ID} * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/getTempMedia.html - *

+ *

*/ CODE_40007(40007, "无效媒体文件 ID"), /** @@ -67,11 +70,10 @@ public enum WxMaErrorMsgEnum { * appid不正确,或者不符合绑定关系要求. * 对应操作:sendUniformMessage * 对应地址: - * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN - * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html + * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/openApi-mgnt/clearQuota.html *
*/ - CODE_40013(40013, "appid不正确,或者不符合绑定关系要求"), + CODE_40013(40013, "appid不正确/不合法(避免异常字符,注意大小写),或者不符合绑定关系要求"), /** *
    * template_id 不正确.
@@ -97,29 +99,29 @@ public enum WxMaErrorMsgEnum {
    */
   CODE_41028(41028, "form_id 不正确,或者过期"),
   /**
-   * 
    * code 或 template_id 不正确.
-   * 对应操作:code2Session, sendUniformMessage, sendTemplateMessage
+   * 

+ * 对应操作:{@code code2Session}, {@code sendUniformMessage}, {@code sendTemplateMessage} * 对应地址: - * GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code + * {@code GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code} * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/code2Session.html * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/template-message/sendTemplateMessage.html - *

+ *

*/ CODE_41029(41029, "请求的参数不正确"), /** - *
    * form_id 已被使用,或者所传page页面不存在,或者小程序没有发布
-   * 对应操作:sendUniformMessage, getWXACodeUnlimit
+   * 

+ * 对应操作:{@code sendUniformMessage}, {@code getWXACodeUnlimit} * 对应地址: * POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN * POST https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=ACCESS_TOKEN * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/uniform-message/sendUniformMessage.html - * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/qr-code/getWXACodeUnlimit.html - *

+ * https://developers.weixin.qq.com/miniprogram/dev/api/open-api/qr-code/getWXACodeUnlimit.html + *

*/ CODE_41030(41030, "请求的参数不正确"), /** @@ -136,13 +138,13 @@ public enum WxMaErrorMsgEnum { */ CODE_45009(45009, "调用分钟频率受限"), /** - *
    * 频率限制,每个用户每分钟100次.
-   * 对应操作:code2Session
+   * 

+ * 对应操作:{@code code2Session} * 对应地址: - * GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code + * {@code GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code} * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/code2Session.html - *

+ *

*/ CODE_45011(45011, "频率限制,每个用户每分钟100次"), /** @@ -188,12 +190,13 @@ public enum WxMaErrorMsgEnum { */ CODE_45072(45072, "command字段取值不对"), /** - *
    * 下发输入状态,需要之前30秒内跟用户有过消息交互.
-   * 对应操作:customerTyping
+   * 

+ * 对应操作:{@code customerTyping} * 对应地址: * POST https://api.weixin.qq.com/cgi-bin/message/custom/typing?access_token=ACCESS_TOKEN * 参考文档地址: https://developers.weixin.qq.com/miniprogram/dev/api/open-api/customer-message/customerTyping.html + *

*/ CODE_45080(45080, "下发输入状态,需要之前30秒内跟用户有过消息交互"), /** @@ -264,6 +267,50 @@ public enum WxMaErrorMsgEnum { * activity_id 过期. */ CODE_47504(47504, "activity_id 过期"), + /** + * api 禁止清零调用次数,因为清零次数达到上限 + * + * @see 参考文档 + */ + CODE_48006(48006, "api 禁止清零调用次数,因为清零次数达到上限"), + + /** + * rid不存在 + * + * @see 参考文档 + */ + CODE_76001(76001, "rid不存在"), + /** + * rid为空或者格式错误 + * + * @see 参考文档 + */ + CODE_76002(76002, "rid为空或者格式错误"), + /** + * 当前账号无权查询该rid,该rid属于其他账号调用所产生 + * + * @see 参考文档 + */ + CODE_76003(76003, "当前账号无权查询该rid,该rid属于其他账号调用所产生"), + /** + * rid过期 + * + * @see 参考文档 + */ + CODE_76004(76004, "rid过期,仅支持持续7天内的rid"), + /** + * cgi_path填错了 + * + * @see 参考文档 + */ + CODE_76021(76021, "cgi_path填错了"), + /** + * 当前调用接口使用的token与api所属账号不符 + * + * @see 参考文档 + */ + CODE_76022(76022, "当前调用接口使用的token与api所属账号不符,详情可看注意事项的说明"), + /** * 没有绑定开放平台帐号. */ @@ -340,6 +387,17 @@ public enum WxMaErrorMsgEnum { CODE_91017(91017, "+号规则 不同类型关联名主体不一致"), CODE_40097(40097, "参数错误"), + /** + * 缺少 appid 参数 + * 参考文档 + */ + CODE_41002(41002, "缺少 appid 参数"), + /** + * 缺少 secret 参数 + * 参考文档 + */ + CODE_41004(41004, "缺少 secret 参数"), + CODE_41006(41006, "media_id 不能为空"), @@ -409,7 +467,7 @@ public enum WxMaErrorMsgEnum { CODE_85012(85012, "无效的审核 id"), - CODE_87013(87013, "撤回次数达到上限(每天一次,每个月 10 次)"), + CODE_87013(87013, "撤回次数达到上限(每天5次,每个月 10 次)"), CODE_85019(85019, "没有审核版本"), @@ -452,26 +510,384 @@ public enum WxMaErrorMsgEnum { CODE_43101(43101, "用户拒绝接受消息,如果用户之前曾经订阅过,则表示用户取消了订阅关系"), CODE_47003(47003, "模板参数不准确,可能为空或者不满足规则,errmsg会提示具体是哪个字段出错"), + + /** + * 小程序绑定体验者 + */ + CODE_85001(85001, "微信号不存在或微信号设置为不可搜索"), + + CODE_85002(85002, "小程序绑定的体验者数量达到上限"), + + CODE_85003(85003, "微信号绑定的小程序体验者达到上限"), + + CODE_85004(85004, "微信号已经绑定"), + + /** + * 53010 + * 名称格式不合法 + */ + CODE_53010(53010, "名称格式不合法"), + + /** + * 53011 + * 名称检测命中频率限制 + */ + CODE_53011(53011, "名称检测命中频率限制"), + + /** + * 53012 + * 禁止使用该名称 + */ + CODE_53012(53012, "禁止使用该名称"), + + /** + * 53013 + * 公众号:名称与已有公众号名称重复;小程序:该名称与已有小程序名称重复 + */ + CODE_53013(53013, "公众号:名称与已有公众号名称重复;小程序:该名称与已有小程序名称重复"), + + /** + * 53014 + * 公众号:公众号已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A};小程序:小程序已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A} + */ + CODE_53014(53014, "公众号:公众号已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A};小程序:小程序已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A}"), + + /** + * 53015 + * 公众号:该名称与已有小程序名称重复,需与该小程序帐号相同主体才可申请;小程序:该名称与已有公众号名称重复,需与该公众号帐号相同主体才可申请 + */ + CODE_53015(53015, "公众号:该名称与已有小程序名称重复,需与该小程序帐号相同主体才可申请;小程序:该名称与已有公众号名称重复,需与该公众号帐号相同主体才可申请"), + + /** + * 53016 + * 公众号:该名称与已有多个小程序名称重复,暂不支持申请;小程序:该名称与已有多个公众号名称重复,暂不支持申请 + */ + CODE_53016(53016, "公众号:该名称与已有多个小程序名称重复,暂不支持申请;小程序:该名称与已有多个公众号名称重复,暂不支持申请"), + + /** + * 53017 + * 公众号:小程序已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A};小程序:公众号已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A} + */ + CODE_53017(53017, "公众号:小程序已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A};小程序:公众号已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A}"), + + /** + * 53018 + * 名称命中微信号 + */ + CODE_53018(53018, "名称命中微信号"), + + /** + * 53019 + * 名称在保护期内 + */ + CODE_53019(53019, "名称在保护期内"), + + /** + * 61070 + * 法人姓名与微信号不一致 name, wechat name not in accordance + */ + CODE_61070(61070, "法人姓名与微信号不一致"), + + /** + * 85015 + * 该账号不是小程序账号 + */ + CODE_85015(85015, "该账号不是小程序账号"), + + /** + * 85066 + * 链接错误 + */ + CODE_85066(85066, "链接错误"), + + /** + * 85068 + * 测试链接不是子链接 + */ + CODE_85068(85068, "测试链接不是子链接"), + + /** + * 85069 + * 校验文件失败 + */ + CODE_85069(85069, "校验文件失败"), + + /** + * 85070 + * 个人类型小程序无法设置二维码规则 + */ + CODE_85070(85070, "个人类型小程序无法设置二维码规则"), + + /** + * 85071 + * 已添加该链接,请勿重复添加 + */ + CODE_85071(85071, "已添加该链接,请勿重复添加"), + + /** + * 85072 + * 该链接已被占用 + */ + CODE_85072(85072, "该链接已被占用"), + + /** + * 85073 + * 二维码规则已满 + */ + CODE_85073(85073, "二维码规则已满"), + + /** + * 85074 + * 小程序未发布, 小程序必须先发布代码才可以发布二维码跳转规则 + */ + CODE_85074(85074, "小程序未发布, 小程序必须先发布代码才可以发布二维码跳转规则"), + + /** + * 85075 + * 个人类型小程序无法设置二维码规则 + */ + CODE_85075(85075, "个人类型小程序无法设置二维码规则"), + + /** + * 86004 + * 无效微信号 invalid wechat + */ + CODE_86004(86004, "无效微信号"), + + /** + * 89247 + * 内部错误 inner error + */ + CODE_89247(89247, "内部错误"), + + /** + * 89248 + * 企业代码类型无效,请选择正确类型填写 invalid code_type type + */ + CODE_89248(89248, "企业代码类型无效,请选择正确类型填写"), + + /** + * 89249 + * 该主体已有任务执行中,距上次任务 24h 后再试 task running + */ + CODE_89249(89249, "该主体已有任务执行中,距上次任务 24h 后再试"), + + /** + * 89250 + * 未找到该任务 task not found + */ + CODE_89250(89250, "未找到该任务"), + + + /** + * 89251 + * 待法人人脸核身校验 legal person checking + */ + CODE_89251(89251, "待法人人脸核身校验"), + + /** + * 89252 + * {@code 法人&企业信息一致性校验中 front checking} + */ + CODE_89252(89252, "法人&企业信息一致性校验中"), + + /** + * 89253 + * 缺少参数 lack of some params + */ + CODE_89253(89253, "缺少参数s"), + + + /** + * 89254 + * 第三方权限集不全,补全权限集全网发布后生效 lack of some component rights + */ + CODE_89254(89254, "第三方权限集不全,补全权限集全网发布后生效"), + + /** + * 89255 + * code参数无效,请检查code长度以及内容是否正确 code参数无效,请检查code长度以及内容是否正确_; + * 注意code_type的值不同需要传的code长度不一样 ;注意code_type的值不同需要传的code长度不一样 enterprise code_invalid invalid + */ + CODE_89255(89255, "code参数无效,请检查code长度以及内容是否正确_;注意code_type的值不同需要传的code长度不一样 ;注意code_type的值不同需要传的code长度不一样"), + +// CODE_504002(-504002, "云函数未找到 Function not found"), + + /** + * 半屏小程序系统错误 + */ + CODE_89408(89408, "半屏小程序系统错误"), + + /** + * 获取半屏小程序列表参数错误 + */ + CODE_89409(89409, "获取半屏小程序列表参数错误"), + + /** + * 添加半屏小程序appid参数错误 + */ + CODE_89410(89410, "添加半屏小程序appid参数错误"), + + /** + * 添加半屏小程序appid参数为空 + */ + CODE_89411(89411, "添加半屏小程序appid参数为空"), + + /** + * 添加半屏小程序申请理由不得超过30个字 + */ + CODE_89412(89412, "添加半屏小程序申请理由不得超过30个字"), + + /** + * 该小程序被申请次数已达24h限制 + */ + CODE_89413(89413, "该小程序被申请次数已达24h限制"), + + /** + * 每天仅允许申请50次半屏小程序 + */ + CODE_89414(89414, "每天仅允许申请50次半屏小程序"), + + /** + * 删除半屏小程序appid参数为空 + */ + CODE_89415(89415, "删除半屏小程序appid参数为空"), + + /** + * 取消半屏小程序授权appid参数为空 + */ + CODE_89416(89416, "取消半屏小程序授权appid参数为空"), + + /** + * 修改半屏小程序方式flag参数错误 + */ + CODE_89417(89417, "修改半屏小程序方式flag参数错误"), + + /** + * 获取半屏小程序每日申请次数失败 + */ + CODE_89418(89418, "获取半屏小程序每日申请次数失败"), + + /** + * 获取半屏小程序每日授权次数失败 + */ + CODE_89419(89419, "获取半屏小程序每日授权次数失败"), + + /** + * 不支持添加个人主体小程序 + */ + CODE_89420(89420, "不支持添加个人主体小程序"), + + /** + * 删除数据未找到 + */ + CODE_89421(89421, "删除数据未找到"), + + /** + * 删除状态异常 + */ + CODE_89422(89422, "删除状态异常"), + + /** + * 申请次数添加到达上限 + */ + CODE_89423(89423, "申请次数添加到达上限"), + + /** + * 申请添加已超时 + */ + CODE_89425(89425, "申请添加已超时"), + + /** + * 申请添加状态异常 + */ + CODE_89426(89426, "申请添加状态异常"), + + /** + * 申请号和授权号相同 + */ + CODE_89427(89427, "申请号和授权号相同"), + + /** + * 该小程序已申请,不允许重复添加 + */ + CODE_89428(89428, "该小程序已申请,不允许重复添加"), + + /** + * 已到达同一小程序每日最多申请次数 + */ + CODE_89429(89429, "已到达同一小程序每日最多申请次数"), + + /** + * 该小程序已设置自动拒绝申请 + */ + CODE_89430(89430, "该小程序已设置自动拒绝申请"), + + /** + * 不支持此类型小程序 + */ + CODE_89431(89431, "不支持此类型小程序"), + + /** + * 不是小程序 + */ + CODE_89432(89432, "不是小程序"), + + /** + * 授权次数到达上限 + */ + CODE_89424(89424, "授权次数到达上限"), + + /** + * 微信小程序虚拟支付错误码 + * + * @see 虚拟支付 API 文档 + */ + CODE_268490001(268490001, "openid错误"), + CODE_268490002(268490002, "请求参数字段错误,具体看errmsg"), + CODE_268490003(268490003, "签名错误"), + CODE_268490004(268490004, "重复操作(赠送和代币支付和充值广告金相关接口会返回,表示之前的操作已经成功)"), + CODE_268490005(268490005, "订单已经通过cancel_currency_pay接口退款,不支持再退款"), + CODE_268490006(268490006, "代币的退款/支付操作金额不足"), + CODE_268490007(268490007, "图片或文字存在敏感内容,禁止使用"), + CODE_268490008(268490008, "代币未发布,不允许进行代币操作"), + CODE_268490009(268490009, "用户session_key不存在或已过期,请重新登录"), + CODE_268490011(268490011, "数据生成中,请稍后调用本接口获取"), + CODE_268490012(268490012, "批量任务运行中,请等待完成后才能再次运行"), + CODE_268490013(268490013, "禁止对核销状态的单进行退款"), + CODE_268490014(268490014, "退款操作进行中,稍后可以使用相同参数重试"), + CODE_268490015(268490015, "频率限制"), + CODE_268490016(268490016, "退款的left_fee字段与实际不符,请通过query_order接口查询确认"), + CODE_268490018(268490018, "广告金充值账户行业id不匹配"), + CODE_268490019(268490019, "广告金充值账户id已绑定其他appid"), + CODE_268490020(268490020, "广告金充值账户主体名称错误"), + CODE_268490021(268490021, "账户未完成进件"), + CODE_268490022(268490022, "广告金充值账户无效"), + CODE_268490023(268490023, "广告金余额不足"), + CODE_268490024(268490024, "广告金充值金额必须大于0"), + ; - private int code; - private String msg; + private final int code; + private final String msg; WxMaErrorMsgEnum(int code, String msg) { this.code = code; this.msg = msg; } + static final Map valueMap = Maps.newHashMap(); + + static { + for (WxMaErrorMsgEnum value : WxMaErrorMsgEnum.values()) { + valueMap.put(value.code, value.msg); + } + } + /** * 通过错误代码查找其中文含义. */ public static String findMsgByCode(int code) { - for (WxMaErrorMsgEnum value : WxMaErrorMsgEnum.values()) { - if (value.code == code) { - return value.msg; - } - } - - return null; + return valueMap.getOrDefault(code, null); } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMpErrorMsgEnum.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMpErrorMsgEnum.java index c75f759660..fdfb397cb3 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMpErrorMsgEnum.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMpErrorMsgEnum.java @@ -1,11 +1,14 @@ package me.chanjar.weixin.common.error; +import com.google.common.collect.Maps; import lombok.Getter; +import java.util.Map; + /** *
  * 微信公众平台全局返回码.
- * 参考文档:公众平台全局返回码
+ * 参考文档:公众平台全局返回码
  * Created by Binary Wang on 2018/5/13.
  * 
* @@ -86,9 +89,9 @@ public enum WxMpErrorMsgEnum { */ CODE_40016(40016, "不合法的按钮个数"), /** - * 不合法的按钮个数. + * 不合法的按钮类型. */ - CODE_40017(40017, "不合法的按钮个数"), + CODE_40017(40017, "不合法的按钮类型"), /** * 不合法的按钮名字长度. */ @@ -209,6 +212,10 @@ public enum WxMpErrorMsgEnum { * 请勿添加其他公众号的主页链接. */ CODE_40155(40155, "请勿添加其他公众号的主页链接"), + /** + * oauth_code已使用 + */ + CODE_40163(40163, "oauth_code已使用"), /** * 缺少 access_token 参数. */ @@ -369,6 +376,18 @@ public enum WxMpErrorMsgEnum { * 非法的tag_id. */ CODE_45159(45159, "非法的tag_id"), + /** + * 相同 clientmsgid 已存在群发记录,返回数据中带有已存在的群发任务的 msgid + */ + CODE_45065(45065, "相同 clientmsgid 已存在群发记录,返回数据中带有已存在的群发任务的 msgid"), + /** + * 相同 clientmsgid 重试速度过快,请间隔1分钟重试 + */ + CODE_45066(45066, "相同 clientmsgid 重试速度过快,请间隔1分钟重试"), + /** + * clientmsgid 长度超过限制 + */ + CODE_45067(45067, "clientmsgid 长度超过限制"), /** * 不存在媒体数据. */ @@ -644,24 +663,26 @@ public enum WxMpErrorMsgEnum { */ CODE_45084(45084, "没有设置 speed 参数"); - private int code; - private String msg; + private final int code; + private final String msg; WxMpErrorMsgEnum(int code, String msg) { this.code = code; this.msg = msg; } + static final Map valueMap = Maps.newHashMap(); + + static { + for (WxMpErrorMsgEnum value : WxMpErrorMsgEnum.values()) { + valueMap.put(value.code, value.msg); + } + } + /** * 通过错误代码查找其中文含义.. */ public static String findMsgByCode(int code) { - for (WxMpErrorMsgEnum value : WxMpErrorMsgEnum.values()) { - if (value.code == code) { - return value.msg; - } - } - - return null; + return valueMap.getOrDefault(code, null); } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxOpenErrorMsgEnum.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxOpenErrorMsgEnum.java new file mode 100644 index 0000000000..ba910e988b --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxOpenErrorMsgEnum.java @@ -0,0 +1,9207 @@ +package me.chanjar.weixin.common.error; + +import com.google.common.collect.Maps; +import lombok.Getter; + +import java.util.Map; + +/** + *
+ *     微信开放平台全局返回码.
+ *     参考文档:开放平台全局返回码
+ * 
+ * + * @author Lam Jerry + */ + +@Getter +public enum WxOpenErrorMsgEnum { + /** + * 系统繁忙,此时请开发者稍候再试 system error + */ + CODE_1(-1, "系统繁忙,此时请开发者稍候再试"), + + /** + * 请求成功 ok + */ + CODE_0(0, "请求成功"), + + /** + * POST参数非法 + */ + CODE_1003(1003, "POST参数非法"), + + /** + * 商品id不存在 + */ + CODE_20002(20002, "商品id不存在"), + + /** + * 获取 access_token 时 AppSecret 错误,或者 access_token 无效。请开发者认真比对 AppSecret 的正确性,或查看是否正在为恰当的公众号调用接口 invalid credential, access_token is invalid or not latest + */ + CODE_40001(40001, "获取 access_token 时 AppSecret 错误,或者 access_token 无效。请开发者认真比对 AppSecret 的正确性,或查看是否正在为恰当的公众号调用接口"), + + /** + * 不合法的凭证类型 invalid grant_type + */ + CODE_40002(40002, "不合法的凭证类型"), + + /** + * 不合法的 OpenID ,请开发者确认 OpenID (该用户)是否已关注公众号,或是否是其他公众号的 OpenID invalid openid + */ + CODE_40003(40003, "不合法的 OpenID ,请开发者确认 OpenID (该用户)是否已关注公众号,或是否是其他公众号的 OpenID"), + + /** + * 不合法的媒体文件类型 invalid media type + */ + CODE_40004(40004, "不合法的媒体文件类型"), + + /** + * 上传素材文件格式不对 invalid file type + */ + CODE_40005(40005, "上传素材文件格式不对"), + + /** + * 上传素材文件大小超出限制 invalid meida size + */ + CODE_40006(40006, "上传素材文件大小超出限制"), + + /** + * 不合法的媒体文件 id invalid media_id + */ + CODE_40007(40007, "不合法的媒体文件 id"), + + /** + * 不合法的消息类型 invalid message type + */ + CODE_40008(40008, "不合法的消息类型"), + + /** + * 图片尺寸太大 invalid image size + */ + CODE_40009(40009, "图片尺寸太大"), + + /** + * 不合法的语音文件大小 invalid voice size + */ + CODE_40010(40010, "不合法的语音文件大小"), + + /** + * 不合法的视频文件大小 invalid video size + */ + CODE_40011(40011, "不合法的视频文件大小"), + + /** + * 不合法的缩略图文件大小 invalid thumb size + */ + CODE_40012(40012, "不合法的缩略图文件大小"), + + /** + * 不合法的appid invalid appid + */ + CODE_40013(40013, "不合法的appid"), + + /** + * 不合法的 access_token ,请开发者认真比对 access_token 的有效性(如是否过期),或查看是否正在为恰当的公众号调用接口 invalid access_token + */ + CODE_40014(40014, "不合法的 access_token ,请开发者认真比对 access_token 的有效性(如是否过期),或查看是否正在为恰当的公众号调用接口"), + + /** + * 不合法的菜单类型 invalid menu type + */ + CODE_40015(40015, "不合法的菜单类型"), + + /** + * 不合法的按钮个数 invalid button size + */ + CODE_40016(40016, "不合法的按钮个数"), + + /** + * 不合法的按钮类型 invalid button type + */ + CODE_40017(40017, "不合法的按钮类型"), + + /** + * 不合法的按钮名字长度 invalid button name size + */ + CODE_40018(40018, "不合法的按钮名字长度"), + + /** + * 不合法的按钮 KEY 长度 invalid button key size + */ + CODE_40019(40019, "不合法的按钮 KEY 长度"), + + /** + * 不合法的按钮 URL 长度 invalid button url size + */ + CODE_40020(40020, "不合法的按钮 URL 长度"), + + /** + * 不合法的菜单版本号 invalid menu version + */ + CODE_40021(40021, "不合法的菜单版本号"), + + /** + * 不合法的子菜单级数 invalid sub_menu level + */ + CODE_40022(40022, "不合法的子菜单级数"), + + /** + * 不合法的子菜单按钮个数 invalid sub button size + */ + CODE_40023(40023, "不合法的子菜单按钮个数"), + + /** + * 不合法的子菜单按钮类型 invalid sub button type + */ + CODE_40024(40024, "不合法的子菜单按钮类型"), + + /** + * 不合法的子菜单按钮名字长度 invalid sub button name size + */ + CODE_40025(40025, "不合法的子菜单按钮名字长度"), + + /** + * 不合法的子菜单按钮 KEY 长度 invalid sub button key size + */ + CODE_40026(40026, "不合法的子菜单按钮 KEY 长度"), + + /** + * 不合法的子菜单按钮 URL 长度 invalid sub button url size + */ + CODE_40027(40027, "不合法的子菜单按钮 URL 长度"), + + /** + * 不合法的自定义菜单使用用户 invalid menu api user + */ + CODE_40028(40028, "不合法的自定义菜单使用用户"), + + /** + * 无效的 oauth_code invalid code + */ + CODE_40029(40029, "无效的 oauth_code"), + + /** + * 不合法的 refresh_token invalid refresh_token + */ + CODE_40030(40030, "不合法的 refresh_token"), + + /** + * 不合法的 openid 列表 invalid openid list + */ + CODE_40031(40031, "不合法的 openid 列表"), + + /** + * 不合法的 openid 列表长度 invalid openid list size + */ + CODE_40032(40032, "不合法的 openid 列表长度"), + + /** + * 不合法的请求字符,不能包含 \\uxxxx 格式的字符 invalid charset. please check your request, if include \\uxxxx will create fail! + */ + CODE_40033(40033, "不合法的请求字符,不能包含 \\uxxxx 格式的字符"), + + /** + * invalid template size + */ + CODE_40034(40034, "invalid template size"), + + /** + * 不合法的参数 invalid args size + */ + CODE_40035(40035, "不合法的参数"), + + /** + * 不合法的 template_id 长度 invalid template_id size + */ + CODE_40036(40036, "不合法的 template_id 长度"), + + /** + * 不合法的 template_id invalid template_id + */ + CODE_40037(40037, "不合法的 template_id"), + + /** + * 不合法的请求格式 invalid packaging type + */ + CODE_40038(40038, "不合法的请求格式"), + + /** + * 不合法的 URL 长度 invalid url size + */ + CODE_40039(40039, "不合法的 URL 长度"), + + /** + * invalid plugin token + */ + CODE_40040(40040, "invalid plugin token"), + + /** + * invalid plugin id + */ + CODE_40041(40041, "invalid plugin id"), + + /** + * invalid plugin session + */ + CODE_40042(40042, "invalid plugin session"), + + /** + * invalid fav type + */ + CODE_40043(40043, "invalid fav type"), + + /** + * invalid size in link.title + */ + CODE_40044(40044, "invalid size in link.title"), + + /** + * invalid size in link.description + */ + CODE_40045(40045, "invalid size in link.description"), + + /** + * invalid size in link.iconurl + */ + CODE_40046(40046, "invalid size in link.iconurl"), + + /** + * invalid size in link.url + */ + CODE_40047(40047, "invalid size in link.url"), + + /** + * 无效的url invalid url domain + */ + CODE_40048(40048, "无效的url"), + + /** + * invalid score report type + */ + CODE_40049(40049, "invalid score report type"), + + /** + * 不合法的分组 id invalid timeline type + */ + CODE_40050(40050, "不合法的分组 id"), + + /** + * 分组名字不合法 invalid group name + */ + CODE_40051(40051, "分组名字不合法"), + + /** + * invalid action name + */ + CODE_40052(40052, "invalid action name"), + + /** + * invalid action info, please check document + */ + CODE_40053(40053, "invalid action info, please check document"), + + /** + * 不合法的子菜单按钮 url 域名 invalid sub button url domain + */ + CODE_40054(40054, "不合法的子菜单按钮 url 域名"), + + /** + * 不合法的菜单按钮 url 域名 invalid button url domain + */ + CODE_40055(40055, "不合法的菜单按钮 url 域名"), + + /** + * invalid serial code + */ + CODE_40056(40056, "invalid serial code"), + + /** + * invalid tabbar size + */ + CODE_40057(40057, "invalid tabbar size"), + + /** + * invalid tabbar name size + */ + CODE_40058(40058, "invalid tabbar name size"), + + /** + * invalid msg id + */ + CODE_40059(40059, "invalid msg id"), + + /** + * 删除单篇图文时,指定的 article_idx 不合法 invalid article idx + */ + CODE_40060(40060, "删除单篇图文时,指定的 article_idx 不合法"), + + /** + * invalid title size + */ + CODE_40062(40062, "invalid title size"), + + /** + * invalid message_ext size + */ + CODE_40063(40063, "invalid message_ext size"), + + /** + * invalid app type + */ + CODE_40064(40064, "invalid app type"), + + /** + * invalid msg status + */ + CODE_40065(40065, "invalid msg status"), + + /** + * 不合法的 url ,递交的页面被sitemap标记为拦截 invalid url + */ + CODE_40066(40066, "不合法的 url ,递交的页面被sitemap标记为拦截"), + + /** + * invalid tvid + */ + CODE_40067(40067, "invalid tvid"), + + /** + * contain mailcious url + */ + CODE_40068(40068, "contain mailcious url"), + + /** + * invalid hardware type + */ + CODE_40069(40069, "invalid hardware type"), + + /** + * invalid sku info + */ + CODE_40070(40070, "invalid sku info"), + + /** + * invalid card type + */ + CODE_40071(40071, "invalid card type"), + + /** + * invalid location id + */ + CODE_40072(40072, "invalid location id"), + + /** + * invalid card id + */ + CODE_40073(40073, "invalid card id"), + + /** + * invalid pay template id + */ + CODE_40074(40074, "invalid pay template id"), + + /** + * invalid encrypt code + */ + CODE_40075(40075, "invalid encrypt code"), + + /** + * invalid color id + */ + CODE_40076(40076, "invalid color id"), + + /** + * invalid score type + */ + CODE_40077(40077, "invalid score type"), + + /** + * invalid card status + */ + CODE_40078(40078, "invalid card status"), + + /** + * invalid time + */ + CODE_40079(40079, "invalid time"), + + /** + * invalid card ext + */ + CODE_40080(40080, "invalid card ext"), + + /** + * invalid template_id + */ + CODE_40081(40081, "invalid template_id"), + + /** + * invalid banner picture size + */ + CODE_40082(40082, "invalid banner picture size"), + + /** + * invalid banner url size + */ + CODE_40083(40083, "invalid banner url size"), + + /** + * invalid button desc size + */ + CODE_40084(40084, "invalid button desc size"), + + /** + * invalid button url size + */ + CODE_40085(40085, "invalid button url size"), + + /** + * invalid sharelink logo size + */ + CODE_40086(40086, "invalid sharelink logo size"), + + /** + * invalid sharelink desc size + */ + CODE_40087(40087, "invalid sharelink desc size"), + + /** + * invalid sharelink title size + */ + CODE_40088(40088, "invalid sharelink title size"), + + /** + * invalid platform id + */ + CODE_40089(40089, "invalid platform id"), + + /** + * invalid request source (bad client ip) + */ + CODE_40090(40090, "invalid request source (bad client ip)"), + + /** + * invalid component ticket + */ + CODE_40091(40091, "invalid component ticket"), + + /** + * invalid remark name + */ + CODE_40092(40092, "invalid remark name"), + + /** + * not completely ok, err_item will return location_id=-1,check your required_fields in json. + */ + CODE_40093(40093, "not completely ok, err_item will return location_id=-1,check your required_fields in json."), + + /** + * invalid component credential + */ + CODE_40094(40094, "invalid component credential"), + + /** + * bad source of caller + */ + CODE_40095(40095, "bad source of caller"), + + /** + * invalid biztype + */ + CODE_40096(40096, "invalid biztype"), + + /** + * 参数错误 invalid args + */ + CODE_40097(40097, "参数错误"), + + /** + * invalid poiid + */ + CODE_40098(40098, "invalid poiid"), + + /** + * invalid code, this code has consumed. + */ + CODE_40099(40099, "invalid code, this code has consumed."), + + /** + * {@code invalid DateInfo, Make Sure OldDateInfoType==NewDateInfoType && NewBeginTime<=OldBeginTime && OldEndTime<= NewEndTime} + */ + CODE_40100(40100, "invalid DateInfo, Make Sure OldDateInfoType==NewDateInfoType && NewBeginTime<=OldBeginTime && OldEndTime<= NewEndTime"), + + /** + * missing parameter + */ + CODE_40101(40101, "missing parameter"), + + /** + * invalid industry id + */ + CODE_40102(40102, "invalid industry id"), + + /** + * invalid industry index + */ + CODE_40103(40103, "invalid industry index"), + + /** + * invalid category id + */ + CODE_40104(40104, "invalid category id"), + + /** + * invalid view type + */ + CODE_40105(40105, "invalid view type"), + + /** + * invalid user name + */ + CODE_40106(40106, "invalid user name"), + + /** + * invalid card id! 1,card status must verify ok; 2,this card must have location_id + */ + CODE_40107(40107, "invalid card id! 1,card status must verify ok; 2,this card must have location_id"), + + /** + * invalid client version + */ + CODE_40108(40108, "invalid client version"), + + /** + * {@code too many code size, must <= 100} + */ + CODE_40109(40109, "too many code size, must <= 100"), + + /** + * have empty code + */ + CODE_40110(40110, "have empty code"), + + /** + * have same code + */ + CODE_40111(40111, "have same code"), + + /** + * can not set bind openid + */ + CODE_40112(40112, "can not set bind openid"), + + /** + * unsupported file type + */ + CODE_40113(40113, "unsupported file type"), + + /** + * invalid index value + */ + CODE_40114(40114, "invalid index value"), + + /** + * invalid session from + */ + CODE_40115(40115, "invalid session from"), + + /** + * invalid code + */ + CODE_40116(40116, "invalid code"), + + /** + * 分组名字不合法 invalid button media_id size + */ + CODE_40117(40117, "分组名字不合法"), + + /** + * media_id 大小不合法 invalid sub button media_id size + */ + CODE_40118(40118, "media_id 大小不合法"), + + /** + * button 类型错误 invalid use button type + */ + CODE_40119(40119, "button 类型错误"), + + /** + * 子 button 类型错误 invalid use sub button type + */ + CODE_40120(40120, "子 button 类型错误"), + + /** + * 不合法的 media_id 类型 invalid media type in view_limited + */ + CODE_40121(40121, "不合法的 media_id 类型"), + + /** + * invalid card quantity + */ + CODE_40122(40122, "invalid card quantity"), + + /** + * invalid task_id + */ + CODE_40123(40123, "invalid task_id"), + + /** + * too many custom field! + */ + CODE_40124(40124, "too many custom field!"), + + /** + * 不合法的 AppID ,请开发者检查 AppID 的正确性,避免异常字符,注意大小写 invalid appsecret + */ + CODE_40125(40125, "不合法的 AppID ,请开发者检查 AppID 的正确性,避免异常字符,注意大小写"), + + /** + * invalid text size + */ + CODE_40126(40126, "invalid text size"), + + /** + * invalid user-card status! Hint: the card was given to user, but may be deleted or expired or set unavailable ! + */ + CODE_40127(40127, "invalid user-card status! Hint: the card was given to user, but may be deleted or expired or set unavailable !"), + + /** + * invalid media id! must be uploaded by api(cgi-bin/material/add_material) + */ + CODE_40128(40128, "invalid media id! must be uploaded by api(cgi-bin/material/add_material)"), + + /** + * invalid scene + */ + CODE_40129(40129, "invalid scene"), + + /** + * invalid openid list size, at least two openid + */ + CODE_40130(40130, "invalid openid list size, at least two openid"), + + /** + * out of limit of ticket + */ + CODE_40131(40131, "out of limit of ticket"), + + /** + * 微信号不合法 invalid username + */ + CODE_40132(40132, "微信号不合法"), + + /** + * invalid encryt data + */ + CODE_40133(40133, "invalid encryt data"), + + /** + * invalid not supply bonus, can not change card_id which supply bonus to be not supply + */ + CODE_40135(40135, "invalid not supply bonus, can not change card_id which supply bonus to be not supply"), + + /** + * {@code invalid use DepositCodeMode, make sure sku.quantity>DepositCode.quantity} + */ + CODE_40136(40136, "invalid use DepositCodeMode, make sure sku.quantity>DepositCode.quantity"), + + /** + * 不支持的图片格式 invalid image format + */ + CODE_40137(40137, "不支持的图片格式"), + + /** + * emphasis word can not be first neither remark + */ + CODE_40138(40138, "emphasis word can not be first neither remark"), + + /** + * invalid sub merchant id + */ + CODE_40139(40139, "invalid sub merchant id"), + + /** + * invalid sub merchant status + */ + CODE_40140(40140, "invalid sub merchant status"), + + /** + * invalid image url + */ + CODE_40141(40141, "invalid image url"), + + /** + * invalid sharecard parameters + */ + CODE_40142(40142, "invalid sharecard parameters"), + + /** + * invalid least cost info, should be 0 + */ + CODE_40143(40143, "invalid least cost info, should be 0"), + + /** + * 1)maybe share_card_list.num or consume_share_self_num too big; 2)maybe card_id_list also has self-card_id;3)maybe card_id_list has many different card_id;4)maybe both consume_share_self_num and share_card_list.num bigger than 0 + */ + CODE_40144(40144, "1)maybe share_card_list.num or consume_share_self_num too big; 2)maybe card_id_list also has self-card_id;3)maybe card_id_list has many different card_id;4)maybe both consume_share_self_num and share_card_list.num bigger than 0"), + + /** + * invalid update! Can not both set PayCell and CenterCellInfo(include: center_title, center_sub_title, center_url). + */ + CODE_40145(40145, "invalid update! Can not both set PayCell and CenterCellInfo(include: center_title, center_sub_title, center_url)."), + + /** + * invalid openid! card may be marked by other user! + */ + CODE_40146(40146, "invalid openid! card may be marked by other user!"), + + /** + * invalid consume! Consume time overranging restricts. + */ + CODE_40147(40147, "invalid consume! Consume time overranging restricts."), + + /** + * invalid friends card type + */ + CODE_40148(40148, "invalid friends card type"), + + /** + * invalid use time limit + */ + CODE_40149(40149, "invalid use time limit"), + + /** + * invalid card parameters + */ + CODE_40150(40150, "invalid card parameters"), + + /** + * invalid card info, text/pic hit antispam + */ + CODE_40151(40151, "invalid card info, text/pic hit antispam"), + + /** + * invalid group id + */ + CODE_40152(40152, "invalid group id"), + + /** + * self consume cell for friends card must need verify code + */ + CODE_40153(40153, "self consume cell for friends card must need verify code"), + + /** + * invalid voip parameters + */ + CODE_40154(40154, "invalid voip parameters"), + + /** + * 请勿添加其他公众号的主页链接 please don't contain other home page url + */ + CODE_40155(40155, "请勿添加其他公众号的主页链接"), + + /** + * invalid face recognize parameters + */ + CODE_40156(40156, "invalid face recognize parameters"), + + /** + * invalid picture, has no face + */ + CODE_40157(40157, "invalid picture, has no face"), + + /** + * invalid use_custom_code, need be false + */ + CODE_40158(40158, "invalid use_custom_code, need be false"), + + /** + * invalid length for path, or the data is not json string + */ + CODE_40159(40159, "invalid length for path, or the data is not json string"), + + /** + * invalid image file + */ + CODE_40160(40160, "invalid image file"), + + /** + * image file not match + */ + CODE_40161(40161, "image file not match"), + + /** + * invalid lifespan + */ + CODE_40162(40162, "invalid lifespan"), + + /** + * oauth_code已使用 code been used + */ + CODE_40163(40163, "oauth_code已使用"), + + /** + * invalid ip, not in whitelist + */ + CODE_40164(40164, "invalid ip, not in whitelist"), + + /** + * invalid weapp pagepath + */ + CODE_40165(40165, "invalid weapp pagepath"), + + /** + * invalid weapp appid + */ + CODE_40166(40166, "invalid weapp appid"), + + /** + * there is no relation with plugin appid + */ + CODE_40167(40167, "there is no relation with plugin appid"), + + /** + * unlinked weapp card + */ + CODE_40168(40168, "unlinked weapp card"), + + /** + * invalid length for scene, or the data is not json string + */ + CODE_40169(40169, "invalid length for scene, or the data is not json string"), + + /** + * args count exceed count limit + */ + CODE_40170(40170, "args count exceed count limit"), + + /** + * product id can not empty and the length cannot exceed 32 + */ + CODE_40171(40171, "product id can not empty and the length cannot exceed 32"), + + /** + * can not have same product id + */ + CODE_40172(40172, "can not have same product id"), + + /** + * there is no bind relation + */ + CODE_40173(40173, "there is no bind relation"), + + /** + * not card user + */ + CODE_40174(40174, "not card user"), + + /** + * invalid material id + */ + CODE_40175(40175, "invalid material id"), + + /** + * invalid template id + */ + CODE_40176(40176, "invalid template id"), + + /** + * invalid product id + */ + CODE_40177(40177, "invalid product id"), + + /** + * invalid sign + */ + CODE_40178(40178, "invalid sign"), + + /** + * Function is adjusted, rules are not allowed to add or update + */ + CODE_40179(40179, "Function is adjusted, rules are not allowed to add or update"), + + /** + * invalid client tmp token + */ + CODE_40180(40180, "invalid client tmp token"), + + /** + * invalid opengid + */ + CODE_40181(40181, "invalid opengid"), + + /** + * invalid pack_id + */ + CODE_40182(40182, "invalid pack_id"), + + /** + * invalid product_appid, product_appid should bind with wxa_appid + */ + CODE_40183(40183, "invalid product_appid, product_appid should bind with wxa_appid"), + + /** + * invalid url path + */ + CODE_40184(40184, "invalid url path"), + + /** + * invalid auth_token, or auth_token is expired + */ + CODE_40185(40185, "invalid auth_token, or auth_token is expired"), + + /** + * invalid delegate + */ + CODE_40186(40186, "invalid delegate"), + + /** + * invalid ip + */ + CODE_40187(40187, "invalid ip"), + + /** + * invalid scope + */ + CODE_40188(40188, "invalid scope"), + + /** + * invalid width + */ + CODE_40189(40189, "invalid width"), + + /** + * invalid delegate time + */ + CODE_40190(40190, "invalid delegate time"), + + /** + * invalid pic_url + */ + CODE_40191(40191, "invalid pic_url"), + + /** + * invalid author in news + */ + CODE_40192(40192, "invalid author in news"), + + /** + * invalid recommend length + */ + CODE_40193(40193, "invalid recommend length"), + + /** + * illegal recommend + */ + CODE_40194(40194, "illegal recommend"), + + /** + * invalid show_num + */ + CODE_40195(40195, "invalid show_num"), + + /** + * invalid smartmsg media_id + */ + CODE_40196(40196, "invalid smartmsg media_id"), + + /** + * invalid smartmsg media num + */ + CODE_40197(40197, "invalid smartmsg media num"), + + /** + * invalid default msg article size, must be same as show_num + */ + CODE_40198(40198, "invalid default msg article size, must be same as show_num"), + + /** + * 运单 ID 不存在,未查到运单 waybill_id not found + */ + CODE_40199(40199, "运单 ID 不存在,未查到运单"), + + /** + * invalid account type + */ + CODE_40200(40200, "invalid account type"), + + /** + * invalid check url + */ + CODE_40201(40201, "invalid check url"), + + /** + * invalid check action + */ + CODE_40202(40202, "invalid check action"), + + /** + * invalid check operator + */ + CODE_40203(40203, "invalid check operator"), + + /** + * can not delete wash or rumor article + */ + CODE_40204(40204, "can not delete wash or rumor article"), + + /** + * invalid check keywords string + */ + CODE_40205(40205, "invalid check keywords string"), + + /** + * invalid check begin stamp + */ + CODE_40206(40206, "invalid check begin stamp"), + + /** + * invalid check alive seconds + */ + CODE_40207(40207, "invalid check alive seconds"), + + /** + * invalid check notify id + */ + CODE_40208(40208, "invalid check notify id"), + + /** + * invalid check notify msg + */ + CODE_40209(40209, "invalid check notify msg"), + + /** + * pages 中的path参数不存在或为空 invalid check wxa path + */ + CODE_40210(40210, "pages 中的path参数不存在或为空"), + + /** + * invalid scope_data + */ + CODE_40211(40211, "invalid scope_data"), + + /** + * {@code paegs 当中存在不合法的query,query格式遵循URL标准,即k1=v1&k2=v2 invalid query} + */ + CODE_40212(40212, "paegs 当中存在不合法的query,query格式遵循URL标准,即k1=v1&k2=v2"), + + /** + * invalid href tag + */ + CODE_40213(40213, "invalid href tag"), + + /** + * invalid href text + */ + CODE_40214(40214, "invalid href text"), + + /** + * invalid image count + */ + CODE_40215(40215, "invalid image count"), + + /** + * invalid desc + */ + CODE_40216(40216, "invalid desc"), + + /** + * invalid video count + */ + CODE_40217(40217, "invalid video count"), + + /** + * invalid video id + */ + CODE_40218(40218, "invalid video id"), + + /** + * pages不存在或者参数为空 pages is empty + */ + CODE_40219(40219, "pages不存在或者参数为空"), + + /** + * data_list is empty + */ + CODE_40220(40220, "data_list is empty"), + + /** + * invalid Content-Encoding + */ + CODE_40221(40221, "invalid Content-Encoding"), + + /** + * invalid request idc domain + */ + CODE_40222(40222, "invalid request idc domain"), + + /** + * empty media cover, please check the media + */ + CODE_40229(40229, "媒体封面为空,请添加媒体封面"), + + /** + * 缺少 access_token 参数 access_token missing + */ + CODE_41001(41001, "缺少 access_token 参数"), + + /** + * 缺少 appid 参数 appid missing + */ + CODE_41002(41002, "缺少 appid 参数"), + + /** + * 缺少 refresh_token 参数 refresh_token missing + */ + CODE_41003(41003, "缺少 refresh_token 参数"), + + /** + * 缺少 secret 参数 appsecret missing + */ + CODE_41004(41004, "缺少 secret 参数"), + + /** + * 缺少多媒体文件数据,传输素材无视频或图片内容 media data missing + */ + CODE_41005(41005, "缺少多媒体文件数据,传输素材无视频或图片内容"), + + /** + * 缺少 media_id 参数 media_id missing + */ + CODE_41006(41006, "缺少 media_id 参数"), + + /** + * 缺少子菜单数据 sub_menu data missing + */ + CODE_41007(41007, "缺少子菜单数据"), + + /** + * 缺少 oauth code missing code + */ + CODE_41008(41008, "缺少 oauth code"), + + /** + * 缺少 openid missing openid + */ + CODE_41009(41009, "缺少 openid"), + + /** + * 缺失 url 参数 missing url + */ + CODE_41010(41010, "缺失 url 参数"), + + /** + * missing required fields! please check document and request json! + */ + CODE_41011(41011, "missing required fields! please check document and request json!"), + + /** + * missing card id + */ + CODE_41012(41012, "missing card id"), + + /** + * missing code + */ + CODE_41013(41013, "missing code"), + + /** + * missing ticket_class + */ + CODE_41014(41014, "missing ticket_class"), + + /** + * missing show_time + */ + CODE_41015(41015, "missing show_time"), + + /** + * missing screening_room + */ + CODE_41016(41016, "missing screening_room"), + + /** + * missing seat_number + */ + CODE_41017(41017, "missing seat_number"), + + /** + * missing component_appid + */ + CODE_41018(41018, "missing component_appid"), + + /** + * missing platform_secret + */ + CODE_41019(41019, "missing platform_secret"), + + /** + * missing platform_ticket + */ + CODE_41020(41020, "missing platform_ticket"), + + /** + * missing component_access_token + */ + CODE_41021(41021, "missing component_access_token"), + + /** + * missing "display" field + */ + CODE_41024(41024, "missing \"display\" field"), + + /** + * poi_list empty + */ + CODE_41025(41025, "poi_list empty"), + + /** + * missing image list info, text maybe empty + */ + CODE_41026(41026, "missing image list info, text maybe empty"), + + /** + * missing voip call key + */ + CODE_41027(41027, "missing voip call key"), + + /** + * invalid form id + */ + CODE_41028(41028, "invalid form id"), + + /** + * form id used count reach limit + */ + CODE_41029(41029, "form id used count reach limit"), + + /** + * page路径不正确,需要保证在现网版本小程序中存在,与app.json保持一致 invalid page + */ + CODE_41030(41030, "page路径不正确,需要保证在现网版本小程序中存在,与app.json保持一致"), + + /** + * the form id have been blocked! + */ + CODE_41031(41031, "the form id have been blocked!"), + + /** + * not allow to send message with submitted form id, for punishment + */ + CODE_41032(41032, "not allow to send message with submitted form id, for punishment"), + + /** + * 只允许通过api创建的小程序使用 invaid register type + */ + CODE_41033(41033, "只允许通过api创建的小程序使用"), + + /** + * not allow to send message with submitted form id, for punishment + */ + CODE_41034(41034, "not allow to send message with submitted form id, for punishment"), + + /** + * not allow to send message with prepay id, for punishment + */ + CODE_41035(41035, "not allow to send message with prepay id, for punishment"), + + /** + * appid ad cid + */ + CODE_41036(41036, "appid ad cid"), + + /** + * appid ad_mch_appid + */ + CODE_41037(41037, "appid ad_mch_appid"), + + /** + * appid pos_type + */ + CODE_41038(41038, "appid pos_type"), + + /** + * access_token 超时,请检查 access_token 的有效期,请参考基础支持 - 获取 access_token 中,对 access_token 的详细机制说明 access_token expired + */ + CODE_42001(42001, "access_token 超时,请检查 access_token 的有效期,请参考基础支持 - 获取 access_token 中,对 access_token 的详细机制说明"), + + /** + * refresh_token 超时 refresh_token expired + */ + CODE_42002(42002, "refresh_token 超时"), + + /** + * oauth_code 超时 code expired + */ + CODE_42003(42003, "oauth_code 超时"), + + /** + * plugin token expired + */ + CODE_42004(42004, "plugin token expired"), + + /** + * api usage expired + */ + CODE_42005(42005, "api usage expired"), + + /** + * component_access_token expired + */ + CODE_42006(42006, "component_access_token expired"), + + /** + * 用户修改微信密码, accesstoken 和 refreshtoken 失效,需要重新授权 access_token and refresh_token exception + */ + CODE_42007(42007, "用户修改微信密码, accesstoken 和 refreshtoken 失效,需要重新授权"), + + /** + * voip call key expired + */ + CODE_42008(42008, "voip call key expired"), + + /** + * client tmp token expired + */ + CODE_42009(42009, "client tmp token expired"), + + /** + * 需要 GET 请求 require GET method + */ + CODE_43001(43001, "需要 GET 请求"), + + /** + * 需要 POST 请求 require POST method + */ + CODE_43002(43002, "需要 POST 请求"), + + /** + * 需要 HTTPS 请求 require https + */ + CODE_43003(43003, "需要 HTTPS 请求"), + + /** + * 需要接收者关注 require subscribe + */ + CODE_43004(43004, "需要接收者关注"), + + /** + * 需要好友关系 require friend relations + */ + CODE_43005(43005, "需要好友关系"), + + /** + * require not block + */ + CODE_43006(43006, "require not block"), + + /** + * require bizuser authorize + */ + CODE_43007(43007, "require bizuser authorize"), + + /** + * require biz pay auth + */ + CODE_43008(43008, "require biz pay auth"), + + /** + * can not use custom code, need authorize + */ + CODE_43009(43009, "can not use custom code, need authorize"), + + /** + * can not use balance, need authorize + */ + CODE_43010(43010, "can not use balance, need authorize"), + + /** + * can not use bonus, need authorize + */ + CODE_43011(43011, "can not use bonus, need authorize"), + + /** + * can not use custom url, need authorize + */ + CODE_43012(43012, "can not use custom url, need authorize"), + + /** + * can not use shake card, need authorize + */ + CODE_43013(43013, "can not use shake card, need authorize"), + + /** + * require check agent + */ + CODE_43014(43014, "require check agent"), + + /** + * require authorize by wechat team to use this function! + */ + CODE_43015(43015, "require authorize by wechat team to use this function!"), + + /** + * 小程序未认证 require verify + */ + CODE_43016(43016, "小程序未认证"), + + /** + * require location id! + */ + CODE_43017(43017, "require location id!"), + + /** + * code has no been mark! + */ + CODE_43018(43018, "code has no been mark!"), + + /** + * 需要将接收者从黑名单中移除 require remove blacklist + */ + CODE_43019(43019, "需要将接收者从黑名单中移除"), + + /** + * change template too frequently + */ + CODE_43100(43100, "change template too frequently"), + + /** + * 用户拒绝接受消息,如果用户之前曾经订阅过,则表示用户取消了订阅关系 user refuse to accept the msg + */ + CODE_43101(43101, "用户拒绝接受消息,如果用户之前曾经订阅过,则表示用户取消了订阅关系"), + + /** + * the tempalte is not subscriptiontype + */ + CODE_43102(43102, "the tempalte is not subscriptiontype"), + + /** + * the api only can cancel the subscription + */ + CODE_43103(43103, "the api only can cancel the subscription"), + + /** + * this appid does not have permission + */ + CODE_43104(43104, "this appid does not have permission"), + + /** + * news has no binding relation with template_id + */ + CODE_43105(43105, "news has no binding relation with template_id"), + + /** + * not allow to add template, for punishment + */ + CODE_43106(43106, "not allow to add template, for punishment"), + + /** + * 多媒体文件为空 empty media data + */ + CODE_44001(44001, "多媒体文件为空"), + + /** + * POST 的数据包为空 empty post data + */ + CODE_44002(44002, "POST 的数据包为空"), + + /** + * 图文消息内容为空 empty news data + */ + CODE_44003(44003, "图文消息内容为空"), + + /** + * 文本消息内容为空 empty content + */ + CODE_44004(44004, "文本消息内容为空"), + + /** + * 空白的列表 empty list size + */ + CODE_44005(44005, "空白的列表"), + + /** + * empty file data + */ + CODE_44006(44006, "empty file data"), + + /** + * repeated msg id + */ + CODE_44007(44007, "repeated msg id"), + + /** + * image url size out of limit + */ + CODE_44997(44997, "image url size out of limit"), + + /** + * keyword string media size out of limit + */ + CODE_44998(44998, "keyword string media size out of limit"), + + /** + * keywords list size out of limit + */ + CODE_44999(44999, "keywords list size out of limit"), + + /** + * msg_id size out of limit + */ + CODE_45000(45000, "msg_id size out of limit"), + + /** + * 多媒体文件大小超过限制 media size out of limit + */ + CODE_45001(45001, "多媒体文件大小超过限制"), + + /** + * 消息内容超过限制 content size out of limit + */ + CODE_45002(45002, "消息内容超过限制"), + + /** + * 标题字段超过限制 title size out of limit + */ + CODE_45003(45003, "标题字段超过限制"), + + /** + * 描述字段超过限制 description size out of limit + */ + CODE_45004(45004, "描述字段超过限制"), + + /** + * 链接字段超过限制 url size out of limit + */ + CODE_45005(45005, "链接字段超过限制"), + + /** + * 图片链接字段超过限制 picurl size out of limit + */ + CODE_45006(45006, "图片链接字段超过限制"), + + /** + * 语音播放时间超过限制 playtime out of limit + */ + CODE_45007(45007, "语音播放时间超过限制"), + + /** + * 图文消息超过限制 article size out of limit + */ + CODE_45008(45008, "图文消息超过限制"), + + /** + * 接口调用超过限制 reach max api daily quota limit + */ + CODE_45009(45009, "接口调用超过限制"), + + /** + * 创建菜单个数超过限制 create menu limit + */ + CODE_45010(45010, "创建菜单个数超过限制"), + + /** + * API 调用太频繁,请稍候再试 api minute-quota reach limit, must slower, retry next minute + */ + CODE_45011(45011, "API 调用太频繁,请稍候再试"), + + /** + * 模板大小超过限制 template size out of limit + */ + CODE_45012(45012, "模板大小超过限制"), + + /** + * too many template args + */ + CODE_45013(45013, "too many template args"), + + /** + * template message size out of limit + */ + CODE_45014(45014, "template message size out of limit"), + + /** + * 回复时间超过限制 response out of time limit or subscription is canceled + */ + CODE_45015(45015, "回复时间超过限制"), + + /** + * 系统分组,不允许修改 can't modify sys group + */ + CODE_45016(45016, "系统分组,不允许修改"), + + /** + * 分组名字过长 can't set group name too long sys group + */ + CODE_45017(45017, "分组名字过长"), + + /** + * 分组数量超过上限 too many group now, no need to add new + */ + CODE_45018(45018, "分组数量超过上限"), + + /** + * too many openid, please input less + */ + CODE_45019(45019, "too many openid, please input less"), + + /** + * too many image, please input less + */ + CODE_45020(45020, "too many image, please input less"), + + /** + * some argument may be out of length limit! please check document and request json! + */ + CODE_45021(45021, "some argument may be out of length limit! please check document and request json!"), + + /** + * bonus is out of limit + */ + CODE_45022(45022, "bonus is out of limit"), + + /** + * balance is out of limit + */ + CODE_45023(45023, "balance is out of limit"), + + /** + * rank template number is out of limit + */ + CODE_45024(45024, "rank template number is out of limit"), + + /** + * poiid count is out of limit + */ + CODE_45025(45025, "poiid count is out of limit"), + + /** + * template num exceeds limit + */ + CODE_45026(45026, "template num exceeds limit"), + + /** + * template conflict with industry + */ + CODE_45027(45027, "template conflict with industry"), + + /** + * has no masssend quota + */ + CODE_45028(45028, "has no masssend quota"), + + /** + * qrcode count out of limit + */ + CODE_45029(45029, "qrcode count out of limit"), + + /** + * limit cardid, not support this function + */ + CODE_45030(45030, "limit cardid, not support this function"), + + /** + * stock is out of limit + */ + CODE_45031(45031, "stock is out of limit"), + + /** + * not inner ip for special acct in white-list + */ + CODE_45032(45032, "not inner ip for special acct in white-list"), + + /** + * user get card num is out of get_limit + */ + CODE_45033(45033, "user get card num is out of get_limit"), + + /** + * media file count is out of limit + */ + CODE_45034(45034, "media file count is out of limit"), + + /** + * access clientip is not registered, not in ip-white-list + */ + CODE_45035(45035, "access clientip is not registered, not in ip-white-list"), + + /** + * User receive announcement limit + */ + CODE_45036(45036, "User receive announcement limit"), + + /** + * user out of time limit or never talked in tempsession + */ + CODE_45037(45037, "user out of time limit or never talked in tempsession"), + + /** + * user subscribed, cannot use tempsession api + */ + CODE_45038(45038, "user subscribed, cannot use tempsession api"), + + /** + * card_list_size out of limit + */ + CODE_45039(45039, "card_list_size out of limit"), + + /** + * reach max monthly quota limit + */ + CODE_45040(45040, "reach max monthly quota limit"), + + /** + * this card reach total sku quantity limit! + */ + CODE_45041(45041, "this card reach total sku quantity limit!"), + + /** + * limit card type, this card type can NOT create by sub merchant + */ + CODE_45042(45042, "limit card type, this card type can NOT create by sub merchant"), + + /** + * can not set share_friends=true because has no Abstract Or Text_Img_List has no img Or image url not valid + */ + CODE_45043(45043, "can not set share_friends=true because has no Abstract Or Text_Img_List has no img Or image url not valid"), + + /** + * icon url size in abstract is out of limit + */ + CODE_45044(45044, "icon url size in abstract is out of limit"), + + /** + * unauthorized friends card, please contact administrator + */ + CODE_45045(45045, "unauthorized friends card, please contact administrator"), + + /** + * operate field conflict, CenterCell, PayCell, SelfConsumeCell conflict + */ + CODE_45046(45046, "operate field conflict, CenterCell, PayCell, SelfConsumeCell conflict"), + + /** + * 客服接口下行条数超过上限 out of response count limit + */ + CODE_45047(45047, "客服接口下行条数超过上限"), + + /** + * menu use invalid type + */ + CODE_45048(45048, "menu use invalid type"), + + /** + * ivr use invalid type + */ + CODE_45049(45049, "ivr use invalid type"), + + /** + * custom msg use invalid type + */ + CODE_45050(45050, "custom msg use invalid type"), + + /** + * template msg use invalid link + */ + CODE_45051(45051, "template msg use invalid link"), + + /** + * masssend msg use invalid type + */ + CODE_45052(45052, "masssend msg use invalid type"), + + /** + * exceed consume verify code size + */ + CODE_45053(45053, "exceed consume verify code size"), + + /** + * below consume verify code size + */ + CODE_45054(45054, "below consume verify code size"), + + /** + * the code is not in consume verify code charset + */ + CODE_45055(45055, "the code is not in consume verify code charset"), + + /** + * too many tag now, no need to add new + */ + CODE_45056(45056, "too many tag now, no need to add new"), + + /** + * can't delete the tag that has too many fans + */ + CODE_45057(45057, "can't delete the tag that has too many fans"), + + /** + * can't modify sys tag + */ + CODE_45058(45058, "can't modify sys tag"), + + /** + * can not tagging one user too much + */ + CODE_45059(45059, "can not tagging one user too much"), + + /** + * media is applied in ivr or menu, can not be deleted + */ + CODE_45060(45060, "media is applied in ivr or menu, can not be deleted"), + + /** + * maybe the update frequency is too often, please try again + */ + CODE_45061(45061, "maybe the update frequency is too often, please try again"), + + /** + * has agreement ad. please use mp.weixin.qq.com + */ + CODE_45062(45062, "has agreement ad. please use mp.weixin.qq.com"), + + /** + * accesstoken is not xiaochengxu + */ + CODE_45063(45063, "accesstoken is not xiaochengxu"), + + /** + * 创建菜单包含未关联的小程序 no permission to use weapp in menu + */ + CODE_45064(45064, "创建菜单包含未关联的小程序"), + + /** + * 相同 clientmsgid 已存在群发记录,返回数据中带有已存在的群发任务的 msgid clientmsgid exist + */ + CODE_45065(45065, "相同 clientmsgid 已存在群发记录,返回数据中带有已存在的群发任务的 msgid"), + + /** + * 相同 clientmsgid 重试速度过快,请间隔1分钟重试 same clientmsgid retry too fast + */ + CODE_45066(45066, "相同 clientmsgid 重试速度过快,请间隔1分钟重试"), + + /** + * clientmsgid 长度超过限制 clientmsgid size out of limit + */ + CODE_45067(45067, "clientmsgid 长度超过限制"), + + /** + * file size out of limit + */ + CODE_45068(45068, "file size out of limit"), + + /** + * product list size out of limit + */ + CODE_45069(45069, "product list size out of limit"), + + /** + * the business account have been created + */ + CODE_45070(45070, "the business account have been created"), + + /** + * business account not found + */ + CODE_45071(45071, "business account not found"), + + /** + * command字段取值不对 invalid command + */ + CODE_45072(45072, "command字段取值不对"), + + /** + * not inner vip for sns in white list + */ + CODE_45073(45073, "not inner vip for sns in white list"), + + /** + * material list size out of limit, you should delete the useless material + */ + CODE_45074(45074, "material list size out of limit, you should delete the useless material"), + + /** + * invalid keyword id + */ + CODE_45075(45075, "invalid keyword id"), + + /** + * invalid count + */ + CODE_45076(45076, "invalid count"), + + /** + * number of business account reach limit + */ + CODE_45077(45077, "number of business account reach limit"), + + /** + * nickname is illegal! + */ + CODE_45078(45078, "nickname is illegal!"), + + /** + * nickname is forbidden!(matched forbidden keyword) + */ + CODE_45079(45079, "nickname is forbidden!(matched forbidden keyword)"), + + /** + * 下发输入状态,需要之前30秒内跟用户有过消息交互 need sending message to user, or recving message from user in the last 30 seconds before typing + */ + CODE_45080(45080, "下发输入状态,需要之前30秒内跟用户有过消息交互"), + + /** + * 已经在输入状态,不可重复下发 you are already typing + */ + CODE_45081(45081, "已经在输入状态,不可重复下发"), + + /** + * need icp license for the url domain + */ + CODE_45082(45082, "need icp license for the url domain"), + + /** + * the speed out of range + */ + CODE_45083(45083, "the speed out of range"), + + /** + * No speed message + */ + CODE_45084(45084, "No speed message"), + + /** + * speed server err + */ + CODE_45085(45085, "speed server err"), + + /** + * invalid attrbute 'data-miniprogram-appid' + */ + CODE_45086(45086, "invalid attrbute 'data-miniprogram-appid'"), + + /** + * customer service message from this account have been blocked! + */ + CODE_45087(45087, "customer service message from this account have been blocked!"), + + /** + * action size out of limit + */ + CODE_45088(45088, "action size out of limit"), + + /** + * expired + */ + CODE_45089(45089, "expired"), + + /** + * invalid group msg ticket + */ + CODE_45090(45090, "invalid group msg ticket"), + + /** + * account_name is illegal! + */ + CODE_45091(45091, "account_name is illegal!"), + + /** + * no voice data + */ + CODE_45092(45092, "no voice data"), + + /** + * no quota to send msg + */ + CODE_45093(45093, "no quota to send msg"), + + /** + * not allow to send custom message when user enter session, for punishment + */ + CODE_45094(45094, "not allow to send custom message when user enter session, for punishment"), + + /** + * not allow to modify stock for the advertisement batch + */ + CODE_45095(45095, "not allow to modify stock for the advertisement batch"), + + /** + * invalid qrcode + */ + CODE_45096(45096, "invalid qrcode"), + + /** + * invalid qrcode prefix + */ + CODE_45097(45097, "invalid qrcode prefix"), + + /** + * msgmenu list size is out of limit + */ + CODE_45098(45098, "msgmenu list size is out of limit"), + + /** + * msgmenu item content size is out of limit + */ + CODE_45099(45099, "msgmenu item content size is out of limit"), + + /** + * invalid size of keyword_id_list + */ + CODE_45100(45100, "invalid size of keyword_id_list"), + + /** + * hit upload limit + */ + CODE_45101(45101, "hit upload limit"), + + /** + * this api have been blocked temporarily. + */ + CODE_45102(45102, "this api have been blocked temporarily."), + + /** + * This API has been unsupported + */ + CODE_45103(45103, "This API has been unsupported"), + + /** + * reach max domain quota limit + */ + CODE_45104(45104, "reach max domain quota limit"), + + /** + * the consume verify code not found + */ + CODE_45154(45154, "the consume verify code not found"), + + /** + * the consume verify code is existed + */ + CODE_45155(45155, "the consume verify code is existed"), + + /** + * the consume verify code's length not invalid + */ + CODE_45156(45156, "the consume verify code's length not invalid"), + + /** + * invalid tag name + */ + CODE_45157(45157, "invalid tag name"), + + /** + * tag name too long + */ + CODE_45158(45158, "tag name too long"), + + /** + * invalid tag id + */ + CODE_45159(45159, "invalid tag id"), + + /** + * invalid category to create card + */ + CODE_45160(45160, "invalid category to create card"), + + /** + * this video id must be generated by calling upload api + */ + CODE_45161(45161, "this video id must be generated by calling upload api"), + + /** + * invalid type + */ + CODE_45162(45162, "invalid type"), + + /** + * invalid sort_method + */ + CODE_45163(45163, "invalid sort_method"), + + /** + * invalid offset + */ + CODE_45164(45164, "invalid offset"), + + /** + * invalid limit + */ + CODE_45165(45165, "invalid limit"), + + /** + * invalid content + */ + CODE_45166(45166, "invalid content"), + + /** + * invalid voip call key + */ + CODE_45167(45167, "invalid voip call key"), + + /** + * keyword in blacklist + */ + CODE_45168(45168, "keyword in blacklist"), + + /** + * part or whole of the requests from the very app is temporary blocked by supervisor + */ + CODE_45501(45501, "part or whole of the requests from the very app is temporary blocked by supervisor"), + + /** + * 不存在媒体数据,media_id 不存在 media data no exist + */ + CODE_46001(46001, "不存在媒体数据,media_id 不存在"), + + /** + * 不存在的菜单版本 menu version no exist + */ + CODE_46002(46002, "不存在的菜单版本"), + + /** + * 不存在的菜单数据 menu no exist + */ + CODE_46003(46003, "不存在的菜单数据"), + + /** + * 不存在的用户 user no exist + */ + CODE_46004(46004, "不存在的用户"), + + /** + * poi no exist + */ + CODE_46005(46005, "poi no exist"), + + /** + * voip file not exist + */ + CODE_46006(46006, "voip file not exist"), + + /** + * file being transcoded, please try later + */ + CODE_46007(46007, "file being transcoded, please try later"), + + /** + * result id not exist + */ + CODE_46008(46008, "result id not exist"), + + /** + * there is no user data + */ + CODE_46009(46009, "there is no user data"), + + /** + * this api have been not supported since 2020-01-11 00:00:00, please use new api(subscribeMessage)! + */ + CODE_46101(46101, "this api have been not supported since 2020-01-11 00:00:00, please use new api(subscribeMessage)!"), + + /** + * 解析 JSON/XML 内容错误 data format error + */ + CODE_47001(47001, "解析 JSON/XML 内容错误"), + + /** + * data format error, do NOT use json unicode encode (\\uxxxx\\uxxxx), please use utf8 encoded text! + */ + CODE_47002(47002, "data format error, do NOT use json unicode encode (\\uxxxx\\uxxxx), please use utf8 encoded text!"), + + /** + * 模板参数不准确,可能为空或者不满足规则,errmsg会提示具体是哪个字段出错 argument invalid! + */ + CODE_47003(47003, "模板参数不准确,可能为空或者不满足规则,errmsg会提示具体是哪个字段出错"), + + /** + * 每次提交的页面数超过1000(备注:每次提交页面数应小于或等于1000) submit pages count more than each quota + */ + CODE_47004(47004, "每次提交的页面数超过1000(备注:每次提交页面数应小于或等于1000)"), + + /** + * tabbar no exist + */ + CODE_47005(47005, "tabbar no exist"), + + /** + * 当天提交页面数达到了配额上限,请明天再试 submit pages count reach daily limit, please try tomorrow + */ + CODE_47006(47006, "当天提交页面数达到了配额上限,请明天再试"), + + /** + * 搜索结果总数超过了1000条 search results count more than limit + */ + CODE_47101(47101, "搜索结果总数超过了1000条"), + + /** + * next_page_info参数错误 next_page_info error + */ + CODE_47102(47102, "next_page_info参数错误"), + + /** + * 参数 activity_id 错误 activity_id error + */ + CODE_47501(47501, "参数 activity_id 错误"), + + /** + * 参数 target_state 错误 target_state error + */ + CODE_47502(47502, "参数 target_state 错误"), + + /** + * 参数 version_type 错误 version_type error + */ + CODE_47503(47503, "参数 version_type 错误"), + + /** + * activity_id activity_id expired time + */ + CODE_47504(47504, "activity_id"), + + /** + * api 功能未授权,请确认公众号已获得该接口,可以在公众平台官网 - 开发者中心页中查看接口权限 api unauthorized + */ + CODE_48001(48001, "api 功能未授权,请确认公众号已获得该接口,可以在公众平台官网 - 开发者中心页中查看接口权限"), + + /** + * 粉丝拒收消息(粉丝在公众号选项中,关闭了 “ 接收消息 ” ) user block receive message + */ + CODE_48002(48002, "粉丝拒收消息(粉丝在公众号选项中,关闭了 “ 接收消息 ” )"), + + /** + * user not agree mass-send protocol + */ + CODE_48003(48003, "user not agree mass-send protocol"), + + /** + * api 接口被封禁,请登录 mp.weixin.qq.com 查看详情 api forbidden for irregularities, view detail on mp.weixin.qq.com + */ + CODE_48004(48004, "api 接口被封禁,请登录 mp.weixin.qq.com 查看详情"), + + /** + * api 禁止删除被自动回复和自定义菜单引用的素材 forbid to delete material used by auto-reply or menu + */ + CODE_48005(48005, "api 禁止删除被自动回复和自定义菜单引用的素材"), + + /** + * api 禁止清零调用次数,因为清零次数达到上限 forbid to clear quota because of reaching the limit + */ + CODE_48006(48006, "api 禁止清零调用次数,因为清零次数达到上限"), + + /** + * forbid to use other's voip call key + */ + CODE_48007(48007, "forbid to use other's voip call key"), + + /** + * 没有该类型消息的发送权限 no permission for this msgtype + */ + CODE_48008(48008, "没有该类型消息的发送权限"), + + /** + * this api is expired + */ + CODE_48009(48009, "this api is expired"), + + /** + * forbid to modify the material, please see more information on mp.weixin.qq.com + */ + CODE_48010(48010, "forbid to modify the material, please see more information on mp.weixin.qq.com"), + + /** + * disabled template id + */ + CODE_48011(48011, "disabled template id"), + + /** + * invalid token + */ + CODE_48012(48012, "invalid token"), + + /** + * 该视频非新接口上传,不能用于视频消息群发 + */ + CODE_48013(48013, "该视频非新接口上传,不能用于视频消息群发"), + + /** + * 该视频审核状态异常,请检查后重试 + */ + CODE_48014(48014, "该视频审核状态异常,请检查后重试"), + + /** + * 该账号无留言功能权限 + */ + CODE_48015(48015, "该账号无留言功能权限"), + + /** + * 该账号不满足智能配置"观看更多"视频条件 + */ + CODE_48016(48016, "该账号不满足智能配置\"观看更多\"视频条件"), + + /** + * not same appid with appid of access_token + */ + CODE_49001(49001, "not same appid with appid of access_token"), + + /** + * empty openid or transid + */ + CODE_49002(49002, "empty openid or transid"), + + /** + * not match openid with appid + */ + CODE_49003(49003, "not match openid with appid"), + + /** + * not match signature + */ + CODE_49004(49004, "not match signature"), + + /** + * not existed transid + */ + CODE_49005(49005, "not existed transid"), + + /** + * missing arg two_dim_code + */ + CODE_49006(49006, "missing arg two_dim_code"), + + /** + * invalid two_dim_code + */ + CODE_49007(49007, "invalid two_dim_code"), + + /** + * invalid qrcode + */ + CODE_49008(49008, "invalid qrcode"), + + /** + * missing arg qrcode + */ + CODE_49009(49009, "missing arg qrcode"), + + /** + * invalid partner id + */ + CODE_49010(49010, "invalid partner id"), + + /** + * not existed feedbackid + */ + CODE_49300(49300, "not existed feedbackid"), + + /** + * feedback exist + */ + CODE_49301(49301, "feedback exist"), + + /** + * feedback status already changed + */ + CODE_49302(49302, "feedback status already changed"), + + /** + * 用户未授权该 api api unauthorized or user unauthorized + */ + CODE_50001(50001, "用户未授权该 api"), + + /** + * 用户受限,可能是用户帐号被冻结或注销 user limited + */ + CODE_50002(50002, "用户受限,可能是用户帐号被冻结或注销"), + + /** + * user unexpected, maybe not in white list + */ + CODE_50003(50003, "user unexpected, maybe not in white list"), + + /** + * user not allow to use accesstoken, maybe for punishment + */ + CODE_50004(50004, "user not allow to use accesstoken, maybe for punishment"), + + /** + * 用户未关注公众号 user is unsubscribed + */ + CODE_50005(50005, "用户未关注公众号"), + + /** + * user has switched off friends authorization + */ + CODE_50006(50006, "user has switched off friends authorization"), + + /** + * enterprise father account not exist + */ + CODE_51000(51000, "enterprise father account not exist"), + + /** + * enterprise child account not belong to the father + */ + CODE_51001(51001, "enterprise child account not belong to the father"), + + /** + * enterprise verify message not correct + */ + CODE_51002(51002, "enterprise verify message not correct"), + + /** + * invalid enterprise child list size + */ + CODE_51003(51003, "invalid enterprise child list size"), + + /** + * not a enterprise father account + */ + CODE_51004(51004, "not a enterprise father account"), + + /** + * not a enterprise child account + */ + CODE_51005(51005, "not a enterprise child account"), + + /** + * invalid nick name + */ + CODE_51006(51006, "invalid nick name"), + + /** + * not a enterprise account + */ + CODE_51007(51007, "not a enterprise account"), + + /** + * invalid email + */ + CODE_51008(51008, "invalid email"), + + /** + * invalid pwd + */ + CODE_51009(51009, "invalid pwd"), + + /** + * repeated email + */ + CODE_51010(51010, "repeated email"), + + /** + * access deny + */ + CODE_51011(51011, "access deny"), + + /** + * need verify code + */ + CODE_51012(51012, "need verify code"), + + /** + * wrong verify code + */ + CODE_51013(51013, "wrong verify code"), + + /** + * need modify pwd + */ + CODE_51014(51014, "need modify pwd"), + + /** + * user not exist + */ + CODE_51015(51015, "user not exist"), + + /** + * tv info not exist + */ + CODE_51020(51020, "tv info not exist"), + + /** + * stamp crossed + */ + CODE_51021(51021, "stamp crossed"), + + /** + * invalid stamp range + */ + CODE_51022(51022, "invalid stamp range"), + + /** + * stamp not match date + */ + CODE_51023(51023, "stamp not match date"), + + /** + * empty program name + */ + CODE_51024(51024, "empty program name"), + + /** + * empty action url + */ + CODE_51025(51025, "empty action url"), + + /** + * program name size out of limit + */ + CODE_51026(51026, "program name size out of limit"), + + /** + * action url size out of limit + */ + CODE_51027(51027, "action url size out of limit"), + + /** + * invalid program name + */ + CODE_51028(51028, "invalid program name"), + + /** + * invalid action url + */ + CODE_51029(51029, "invalid action url"), + + /** + * invalid action id + */ + CODE_51030(51030, "invalid action id"), + + /** + * invalid action offset + */ + CODE_51031(51031, "invalid action offset"), + + /** + * empty action title + */ + CODE_51032(51032, "empty action title"), + + /** + * action title size out of limit + */ + CODE_51033(51033, "action title size out of limit"), + + /** + * empty action icon url + */ + CODE_51034(51034, "empty action icon url"), + + /** + * action icon url out of limit + */ + CODE_51035(51035, "action icon url out of limit"), + + /** + * pic is not from cdn + */ + CODE_52000(52000, "pic is not from cdn"), + + /** + * wechat price is not less than origin price + */ + CODE_52001(52001, "wechat price is not less than origin price"), + + /** + * category/sku is wrong + */ + CODE_52002(52002, "category/sku is wrong"), + + /** + * product id not existed + */ + CODE_52003(52003, "product id not existed"), + + /** + * category id is not exist, or doesn't has sub category + */ + CODE_52004(52004, "category id is not exist, or doesn't has sub category"), + + /** + * quantity is zero + */ + CODE_52005(52005, "quantity is zero"), + + /** + * area code is invalid + */ + CODE_52006(52006, "area code is invalid"), + + /** + * express template param is error + */ + CODE_52007(52007, "express template param is error"), + + /** + * express template id is not existed + */ + CODE_52008(52008, "express template id is not existed"), + + /** + * group name is empty + */ + CODE_52009(52009, "group name is empty"), + + /** + * group id is not existed + */ + CODE_52010(52010, "group id is not existed"), + + /** + * mod_action is invalid + */ + CODE_52011(52011, "mod_action is invalid"), + + /** + * shelf components count is greater than 20 + */ + CODE_52012(52012, "shelf components count is greater than 20"), + + /** + * shelf component is empty + */ + CODE_52013(52013, "shelf component is empty"), + + /** + * shelf id is not existed + */ + CODE_52014(52014, "shelf id is not existed"), + + /** + * order id is not existed + */ + CODE_52015(52015, "order id is not existed"), + + /** + * order filter param is invalid + */ + CODE_52016(52016, "order filter param is invalid"), + + /** + * order express param is invalid + */ + CODE_52017(52017, "order express param is invalid"), + + /** + * order delivery param is invalid + */ + CODE_52018(52018, "order delivery param is invalid"), + + /** + * brand name empty + */ + CODE_52019(52019, "brand name empty"), + + /** + * principal limit exceed + */ + CODE_53000(53000, "principal limit exceed"), + + /** + * principal in black list + */ + CODE_53001(53001, "principal in black list"), + + /** + * mobile limit exceed + */ + CODE_53002(53002, "mobile limit exceed"), + + /** + * idcard limit exceed + */ + CODE_53003(53003, "idcard limit exceed"), + + /** + * 名称格式不合法 nickname invalid + */ + CODE_53010(53010, "名称格式不合法"), + + /** + * 名称检测命中频率限制 check nickname too frequently + */ + CODE_53011(53011, "名称检测命中频率限制"), + + /** + * 禁止使用该名称 nickname ban + */ + CODE_53012(53012, "禁止使用该名称"), + + /** + * 公众号:名称与已有公众号名称重复;小程序:该名称与已有小程序名称重复 nickname has been occupied + */ + CODE_53013(53013, "公众号:名称与已有公众号名称重复;小程序:该名称与已有小程序名称重复"), + + /** + * 公众号:公众号已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A};小程序:小程序已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A} + */ + CODE_53014(53014, "公众号:公众号已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A};小程序:小程序已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A}"), + + /** + * 公众号:该名称与已有小程序名称重复,需与该小程序帐号相同主体才可申请;小程序:该名称与已有公众号名称重复,需与该公众号帐号相同主体才可申请 + */ + CODE_53015(53015, "公众号:该名称与已有小程序名称重复,需与该小程序帐号相同主体才可申请;小程序:该名称与已有公众号名称重复,需与该公众号帐号相同主体才可申请"), + + /** + * 公众号:该名称与已有多个小程序名称重复,暂不支持申请;小程序:该名称与已有多个公众号名称重复,暂不支持申请 + */ + CODE_53016(53016, "公众号:该名称与已有多个小程序名称重复,暂不支持申请;小程序:该名称与已有多个公众号名称重复,暂不支持申请"), + + /** + * 公众号:小程序已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A};小程序:公众号已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A} + */ + CODE_53017(53017, "公众号:小程序已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A};小程序:公众号已有{名称 A+}时,需与该帐号相同主体才可申请{名称 A}"), + + /** + * 名称命中微信号 nickname hit alias + */ + CODE_53018(53018, "名称命中微信号"), + + /** + * 名称在保护期内 nickname protected by infringement + */ + CODE_53019(53019, "名称在保护期内"), + + /** + * order not found + */ + CODE_53100(53100, "订单不存在"), + + /** + * order already paid + */ + CODE_53101(53101, "已经支付的订单"), + + /** + * already has checking order, can not apply + */ + CODE_53102(53102, "已有检查单,不能申请"), + + /** + * order can not do refill + */ + CODE_53103(53103, "order can not do refill"), + + /** + * 本月功能介绍修改次数已用完 modify signature quota limit exceed + */ + CODE_53200(53200, "本月功能介绍修改次数已用完"), + + /** + * 功能介绍内容命中黑名单关键字 signature in black list, can not use + */ + CODE_53201(53201, "功能介绍内容命中黑名单关键字"), + + /** + * 本月头像修改次数已用完 modify avatar quota limit exceed + */ + CODE_53202(53202, "本月头像修改次数已用完"), + + /** + * can't be modified for the time being + */ + CODE_53203(53203, "暂时还不能修改"), + + /** + * signature invalid + */ + CODE_53204(53204, "无效签名"), + + /** + * 超出每月次数限制 + */ + CODE_53300(53300, "超出每月次数限制"), + + /** + * 超出可配置类目总数限制 + */ + CODE_53301(53301, "超出可配置类目总数限制"), + + /** + * 当前账号主体类型不允许设置此种类目 + */ + CODE_53302(53302, "当前账号主体类型不允许设置此种类目"), + + /** + * 提交的参数不合法 + */ + CODE_53303(53303, "提交的参数不合法"), + + /** + * 与已有类目重复 + */ + CODE_53304(53304, "与已有类目重复"), + + /** + * 包含未通过IPC校验的类目 + */ + CODE_53305(53305, "包含未通过IPC校验的类目"), + + /** + * 修改类目只允许修改类目资质,不允许修改类目ID + */ + CODE_53306(53306, "修改类目只允许修改类目资质,不允许修改类目ID"), + + /** + * 只有审核失败的类目允许修改 + */ + CODE_53307(53307, "只有审核失败的类目允许修改"), + + /** + * 审核中的类目不允许删除 + */ + CODE_53308(53308, "审核中的类目不允许删除"), + + /** + * 社交红包不允许删除 + */ + CODE_53309(53309, "社交红包不允许删除"), + + /** + * 类目超过上限,但是可以添加apply_reason参数申请更多类目 + */ + CODE_53310(53310, "类目超过上限,但是可以添加apply_reason参数申请更多类目"), + + /** + * 需要提交资料信息 + */ + CODE_53311(53311, "需要提交资料信息"), + + /** + * empty jsapi name + */ + CODE_60005(60005, "空的jsapi名称"), + + /** + * user cancel the auth + */ + CODE_60006(60006, "用户取消该授权"), + + /** + * invalid component type + */ + CODE_61000(61000, "无效的第三方类型"), + + /** + * component type and component appid is not match + */ + CODE_61001(61001, "第三方类型与第三方APPID不匹配"), + + /** + * the third appid is not open KF + */ + CODE_61002(61002, "第三方APPID没有开放客服"), + + /** + * component is not authorized by this account + */ + CODE_61003(61003, "帐号未授权"), + + /** + * api 功能未授权,请确认公众号/小程序已获得该接口,可以在公众平台官网 - 开发者中心页中查看接口权限 access clientip is not registered + */ + CODE_61004(61004, "api 功能未授权,请确认公众号/小程序已获得该接口,可以在公众平台官网 - 开发者中心页中查看接口权限"), + + /** + * component ticket is expired + */ + CODE_61005(61005, "ticket 已过期"), + + /** + * component ticket is invalid + */ + CODE_61006(61006, "无效 ticket"), + + /** + * api is unauthorized to component + */ + CODE_61007(61007, "接口未授权给第三方平台"), + + /** + * component req key is duplicated + */ + CODE_61008(61008, "第三方请求的key存在重复"), + + /** + * code is invalid + */ + CODE_61009(61009, "无效code"), + + /** + * code is expired + */ + CODE_61010(61010, "code已过期"), + + /** + * invalid component + */ + CODE_61011(61011, "无效的第三方平台"), + + /** + * invalid option name + */ + CODE_61012(61012, "无效的选项名称"), + + /** + * invalid option value + */ + CODE_61013(61013, "无效的选项值"), + + /** + * must use component token for component api + */ + CODE_61014(61014, "必须使用component接口的token"), + + /** + * must use biz account token for not component api + */ + CODE_61015(61015, "必须使用商业帐号token,而不是component api"), + + /** + * function category of API need be confirmed by component + */ + CODE_61016(61016, "function category of API need be confirmed by component"), + + /** + * function category is not authorized + */ + CODE_61017(61017, "function category is not authorized"), + + /** + * already confirm + */ + CODE_61018(61018, "已确认"), + + /** + * not need confirm + */ + CODE_61019(61019, "不需要确认"), + + /** + * err parameter + */ + CODE_61020(61020, "err parameter"), + + /** + * can't confirm + */ + CODE_61021(61021, "can't confirm"), + + /** + * can't resubmit + */ + CODE_61022(61022, "can't resubmit"), + + /** + * refresh_token is invalid + */ + CODE_61023(61023, "refresh_token is invalid"), + + /** + * must use api(api_component_token) to get token for component acct + */ + CODE_61024(61024, "must use api(api_component_token) to get token for component acct"), + + /** + * read-only option + */ + CODE_61025(61025, "read-only option"), + + /** + * register access deny + */ + CODE_61026(61026, "register access deny"), + + /** + * register limit exceed + */ + CODE_61027(61027, "register limit exceed"), + + /** + * component is unpublished + */ + CODE_61028(61028, "component is unpublished"), + + /** + * component need republish with base category + */ + CODE_61029(61029, "component need republish with base category"), + + /** + * component cancel authorization not allowed + */ + CODE_61030(61030, "component cancel authorization not allowed"), + + /** + * invalid realname type + */ + CODE_61051(61051, "invalid realname type"), + + /** + * need to be certified + */ + CODE_61052(61052, "need to be certified"), + + /** + * realname exceed limits + */ + CODE_61053(61053, "realname exceed limits"), + + /** + * realname in black list + */ + CODE_61054(61054, "realname in black list"), + + /** + * exceed quota per month + */ + CODE_61055(61055, "exceed quota per month"), + + /** + * copy_wx_verify is required option + */ + CODE_61056(61056, "copy_wx_verify is required option"), + + /** + * invalid ticket + */ + CODE_61058(61058, "invalid ticket"), + + /** + * overseas access deny + */ + CODE_61061(61061, "overseas access deny"), + + /** + * admin exceed limits + */ + CODE_61063(61063, "admin exceed limits"), + + /** + * admin in black list + */ + CODE_61064(61064, "admin in black list"), + + /** + * idcard exceed limits + */ + CODE_61065(61065, "idcard exceed limits"), + + /** + * idcard in black list + */ + CODE_61066(61066, "idcard in black list"), + + /** + * mobile exceed limits + */ + CODE_61067(61067, "mobile exceed limits"), + + /** + * mobile in black list + */ + CODE_61068(61068, "mobile in black list"), + + /** + * invalid admin + */ + CODE_61069(61069, "invalid admin"), + + /** + * name, idcard, wechat name not in accordance + */ + CODE_61070(61070, "name, idcard, wechat name not in accordance"), + + /** + * invalid url + */ + CODE_61100(61100, "invalid url"), + + /** + * invalid openid + */ + CODE_61101(61101, "invalid openid"), + + /** + * share relation not existed + */ + CODE_61102(61102, "share relation not existed"), + + /** + * product wording not set + */ + CODE_61200(61200, "product wording not set"), + + /** + * invalid base info + */ + CODE_61300(61300, "invalid base info"), + + /** + * invalid detail info + */ + CODE_61301(61301, "invalid detail info"), + + /** + * invalid action info + */ + CODE_61302(61302, "invalid action info"), + + /** + * brand info not exist + */ + CODE_61303(61303, "brand info not exist"), + + /** + * invalid product id + */ + CODE_61304(61304, "invalid product id"), + + /** + * invalid key info + */ + CODE_61305(61305, "invalid key info"), + + /** + * invalid appid + */ + CODE_61306(61306, "invalid appid"), + + /** + * invalid card id + */ + CODE_61307(61307, "invalid card id"), + + /** + * base info not exist + */ + CODE_61308(61308, "base info not exist"), + + /** + * detail info not exist + */ + CODE_61309(61309, "detail info not exist"), + + /** + * action info not exist + */ + CODE_61310(61310, "action info not exist"), + + /** + * invalid media info + */ + CODE_61311(61311, "invalid media info"), + + /** + * invalid buffer size + */ + CODE_61312(61312, "invalid buffer size"), + + /** + * invalid buffer + */ + CODE_61313(61313, "invalid buffer"), + + /** + * invalid qrcode extinfo + */ + CODE_61314(61314, "invalid qrcode extinfo"), + + /** + * invalid local ext info + */ + CODE_61315(61315, "invalid local ext info"), + + /** + * key conflict + */ + CODE_61316(61316, "key conflict"), + + /** + * ticket invalid + */ + CODE_61317(61317, "ticket invalid"), + + /** + * verify not pass + */ + CODE_61318(61318, "verify not pass"), + + /** + * category invalid + */ + CODE_61319(61319, "category invalid"), + + /** + * merchant info not exist + */ + CODE_61320(61320, "merchant info not exist"), + + /** + * cate id is a leaf node + */ + CODE_61321(61321, "cate id is a leaf node"), + + /** + * category id no permision + */ + CODE_61322(61322, "category id no permision"), + + /** + * barcode no permision + */ + CODE_61323(61323, "barcode no permision"), + + /** + * exceed max action num + */ + CODE_61324(61324, "exceed max action num"), + + /** + * brandinfo invalid store mgr type + */ + CODE_61325(61325, "brandinfo invalid store mgr type"), + + /** + * anti-spam blocked + */ + CODE_61326(61326, "anti-spam blocked"), + + /** + * comment reach limit + */ + CODE_61327(61327, "comment reach limit"), + + /** + * comment data is not the newest + */ + CODE_61328(61328, "comment data is not the newest"), + + /** + * comment hit ban word + */ + CODE_61329(61329, "comment hit ban word"), + + /** + * image already add + */ + CODE_61330(61330, "image already add"), + + /** + * image never add + */ + CODE_61331(61331, "image never add"), + + /** + * warning, image quanlity too low + */ + CODE_61332(61332, "warning, image quanlity too low"), + + /** + * warning, image simility too high + */ + CODE_61333(61333, "warning, image simility too high"), + + /** + * product not exists + */ + CODE_61334(61334, "product not exists"), + + /** + * key apply fail + */ + CODE_61335(61335, "key apply fail"), + + /** + * check status fail + */ + CODE_61336(61336, "check status fail"), + + /** + * product already exists + */ + CODE_61337(61337, "product already exists"), + + /** + * forbid delete + */ + CODE_61338(61338, "forbid delete"), + + /** + * firmcode claimed + */ + CODE_61339(61339, "firmcode claimed"), + + /** + * check firm info fail + */ + CODE_61340(61340, "check firm info fail"), + + /** + * too many white list uin + */ + CODE_61341(61341, "too many white list uin"), + + /** + * keystandard not match + */ + CODE_61342(61342, "keystandard not match"), + + /** + * keystandard error + */ + CODE_61343(61343, "keystandard error"), + + /** + * id map not exists + */ + CODE_61344(61344, "id map not exists"), + + /** + * invalid action code + */ + CODE_61345(61345, "invalid action code"), + + /** + * invalid actioninfo store + */ + CODE_61346(61346, "invalid actioninfo store"), + + /** + * invalid actioninfo media + */ + CODE_61347(61347, "invalid actioninfo media"), + + /** + * invalid actioninfo text + */ + CODE_61348(61348, "invalid actioninfo text"), + + /** + * invalid input data + */ + CODE_61350(61350, "invalid input data"), + + /** + * input data exceed max size + */ + CODE_61351(61351, "input data exceed max size"), + + /** + * kf_account error + */ + CODE_61400(61400, "kf_account error"), + + /** + * kf system alredy transfer + */ + CODE_61401(61401, "kf system alredy transfer"), + + /** + * 系统错误 (system error) + */ + CODE_61450(61450, "系统错误 (system error)"), + + /** + * 参数错误 (invalid parameter) + */ + CODE_61451(61451, "参数错误 (invalid parameter)"), + + /** + * 无效客服账号 (invalid kf_account) + */ + CODE_61452(61452, "无效客服账号 (invalid kf_account)"), + + /** + * 客服帐号已存在 (kf_account exsited) + */ + CODE_61453(61453, "客服帐号已存在 (kf_account exsited)"), + + /** + * 客服帐号名长度超过限制 ( 仅允许 10 个英文字符,不包括 @ 及 @ 后的公众号的微信号 )(invalid kf_acount length) + */ + CODE_61454(61454, "客服帐号名长度超过限制 ( 仅允许 10 个英文字符,不包括 @ 及 @ 后的公众号的微信号 )(invalid kf_acount length)"), + + /** + * 客服帐号名包含非法字符 ( 仅允许英文 + 数字 )(illegal character in kf_account) + */ + CODE_61455(61455, "客服帐号名包含非法字符 ( 仅允许英文 + 数字 )(illegal character in kf_account)"), + + /** + * 客服帐号个数超过限制 (10 个客服账号 )(kf_account count exceeded) + */ + CODE_61456(61456, "客服帐号个数超过限制 (10 个客服账号 )(kf_account count exceeded)"), + + /** + * 无效头像文件类型 (invalid file type) + */ + CODE_61457(61457, "无效头像文件类型 (invalid file type)"), + + /** + * 日期格式错误 date format error + */ + CODE_61500(61500, "日期格式错误"), + + /** + * date range error + */ + CODE_61501(61501, "date range error"), + + /** + * this is game miniprogram, data api is not supported + */ + CODE_61502(61502, "this is game miniprogram, data api is not supported"), + + /** + * data not ready, please try later + */ + CODE_61503(61503, "data not ready, please try later"), + + /** + * trying to access other's app + */ + CODE_62001(62001, "trying to access other's app"), + + /** + * app name already exists + */ + CODE_62002(62002, "app name already exists"), + + /** + * please provide at least one platform + */ + CODE_62003(62003, "please provide at least one platform"), + + /** + * invalid app name + */ + CODE_62004(62004, "invalid app name"), + + /** + * invalid app id + */ + CODE_62005(62005, "invalid app id"), + + /** + * 部分参数为空 some arguments is empty + */ + CODE_63001(63001, "部分参数为空"), + + /** + * 无效的签名 invalid signature + */ + CODE_63002(63002, "无效的签名"), + + /** + * invalid signature method + */ + CODE_63003(63003, "invalid signature method"), + + /** + * no authroize + */ + CODE_63004(63004, "no authroize"), + + /** + * gen ticket fail + */ + CODE_63149(63149, "gen ticket fail"), + + /** + * set ticket fail + */ + CODE_63152(63152, "set ticket fail"), + + /** + * shortid decode fail + */ + CODE_63153(63153, "shortid decode fail"), + + /** + * invalid status + */ + CODE_63154(63154, "invalid status"), + + /** + * invalid color + */ + CODE_63155(63155, "invalid color"), + + /** + * invalid tag + */ + CODE_63156(63156, "invalid tag"), + + /** + * invalid recommend + */ + CODE_63157(63157, "invalid recommend"), + + /** + * branditem out of limits + */ + CODE_63158(63158, "branditem out of limits"), + + /** + * retail_price empty + */ + CODE_63159(63159, "retail_price empty"), + + /** + * priceinfo invalid + */ + CODE_63160(63160, "priceinfo invalid"), + + /** + * antifake module num limit + */ + CODE_63161(63161, "antifake module num limit"), + + /** + * antifake native_type err + */ + CODE_63162(63162, "antifake native_type err"), + + /** + * antifake link not exists + */ + CODE_63163(63163, "antifake link not exists"), + + /** + * module type not exist + */ + CODE_63164(63164, "module type not exist"), + + /** + * module info not exist + */ + CODE_63165(63165, "module info not exist"), + + /** + * item is beding verified + */ + CODE_63166(63166, "item is beding verified"), + + /** + * item not published + */ + CODE_63167(63167, "item not published"), + + /** + * verify not pass + */ + CODE_63168(63168, "verify not pass"), + + /** + * already published + */ + CODE_63169(63169, "already published"), + + /** + * only banner or media + */ + CODE_63170(63170, "only banner or media"), + + /** + * card num limit + */ + CODE_63171(63171, "card num limit"), + + /** + * user num limit + */ + CODE_63172(63172, "user num limit"), + + /** + * text num limit + */ + CODE_63173(63173, "text num limit"), + + /** + * link card user sum limit + */ + CODE_63174(63174, "link card user sum limit"), + + /** + * detail info error + */ + CODE_63175(63175, "detail info error"), + + /** + * not this type + */ + CODE_63176(63176, "not this type"), + + /** + * src or secretkey or version or expired_time is wrong + */ + CODE_63177(63177, "src or secretkey or version or expired_time is wrong"), + + /** + * appid wrong + */ + CODE_63178(63178, "appid wrong"), + + /** + * openid num limit + */ + CODE_63179(63179, "openid num limit"), + + /** + * this app msg not found + */ + CODE_63180(63180, "this app msg not found"), + + /** + * get history app msg end + */ + CODE_63181(63181, "get history app msg end"), + + /** + * openid_list empty + */ + CODE_63182(63182, "openid_list empty"), + + /** + * unknown deeplink type + */ + CODE_65001(65001, "unknown deeplink type"), + + /** + * deeplink unauthorized + */ + CODE_65002(65002, "deeplink unauthorized"), + + /** + * bad deeplink + */ + CODE_65003(65003, "bad deeplink"), + + /** + * deeplinks of the very type are supposed to have short-life + */ + CODE_65004(65004, "deeplinks of the very type are supposed to have short-life"), + + /** + * invalid categories + */ + CODE_65104(65104, "invalid categories"), + + /** + * invalid photo url + */ + CODE_65105(65105, "invalid photo url"), + + /** + * poi audit state must be approved + */ + CODE_65106(65106, "poi audit state must be approved"), + + /** + * poi not allowed modify now + */ + CODE_65107(65107, "poi not allowed modify now"), + + /** + * invalid business name + */ + CODE_65109(65109, "invalid business name"), + + /** + * invalid address + */ + CODE_65110(65110, "invalid address"), + + /** + * invalid telephone + */ + CODE_65111(65111, "invalid telephone"), + + /** + * invalid city + */ + CODE_65112(65112, "invalid city"), + + /** + * invalid province + */ + CODE_65113(65113, "invalid province"), + + /** + * photo list empty + */ + CODE_65114(65114, "photo list empty"), + + /** + * poi_id is not exist + */ + CODE_65115(65115, "poi_id is not exist"), + + /** + * poi has been deleted + */ + CODE_65116(65116, "poi has been deleted"), + + /** + * cannot delete poi + */ + CODE_65117(65117, "cannot delete poi"), + + /** + * store status is invalid + */ + CODE_65118(65118, "store status is invalid"), + + /** + * lack of qualification for relevant principals + */ + CODE_65119(65119, "lack of qualification for relevant principals"), + + /** + * category info is not found + */ + CODE_65120(65120, "category info is not found"), + + /** + * room_name is empty, please check your input + */ + CODE_65201(65201, "room_name is empty, please check your input"), + + /** + * user_id is empty, please check your input + */ + CODE_65202(65202, "user_id is empty, please check your input"), + + /** + * invalid check ticket + */ + CODE_65203(65203, "invalid check ticket"), + + /** + * invalid check ticket opt code + */ + CODE_65204(65204, "invalid check ticket opt code"), + + /** + * check ticket out of time + */ + CODE_65205(65205, "check ticket out of time"), + + /** + * 不存在此 menuid 对应的个性化菜单 this menu is not conditionalmenu + */ + CODE_65301(65301, "不存在此 menuid 对应的个性化菜单"), + + /** + * 没有相应的用户 no such user + */ + CODE_65302(65302, "没有相应的用户"), + + /** + * 没有默认菜单,不能创建个性化菜单 there is no selfmenu, please create selfmenu first + */ + CODE_65303(65303, "没有默认菜单,不能创建个性化菜单"), + + /** + * MatchRule 信息为空 match rule empty + */ + CODE_65304(65304, "MatchRule 信息为空"), + + /** + * 个性化菜单数量受限 menu count limit + */ + CODE_65305(65305, "个性化菜单数量受限"), + + /** + * 不支持个性化菜单的帐号 conditional menu not support + */ + CODE_65306(65306, "不支持个性化菜单的帐号"), + + /** + * 个性化菜单信息为空 conditional menu is empty + */ + CODE_65307(65307, "个性化菜单信息为空"), + + /** + * 包含没有响应类型的 button exist empty button act + */ + CODE_65308(65308, "包含没有响应类型的 button"), + + /** + * 个性化菜单开关处于关闭状态 conditional menu switch is closed + */ + CODE_65309(65309, "个性化菜单开关处于关闭状态"), + + /** + * 填写了省份或城市信息,国家信息不能为空 region info: country is empty + */ + CODE_65310(65310, "填写了省份或城市信息,国家信息不能为空"), + + /** + * 填写了城市信息,省份信息不能为空 region info: province is empty + */ + CODE_65311(65311, "填写了城市信息,省份信息不能为空"), + + /** + * 不合法的国家信息 invalid country info + */ + CODE_65312(65312, "不合法的国家信息"), + + /** + * 不合法的省份信息 invalid province info + */ + CODE_65313(65313, "不合法的省份信息"), + + /** + * 不合法的城市信息 invalid city info + */ + CODE_65314(65314, "不合法的城市信息"), + + /** + * not fans + */ + CODE_65315(65315, "not fans"), + + /** + * 该公众号的菜单设置了过多的域名外跳(最多跳转到 3 个域名的链接) domain count reach limit + */ + CODE_65316(65316, "该公众号的菜单设置了过多的域名外跳(最多跳转到 3 个域名的链接)"), + + /** + * 不合法的 URL contain invalid url + */ + CODE_65317(65317, "不合法的 URL"), + + /** + * must use utf-8 charset + */ + CODE_65318(65318, "must use utf-8 charset"), + + /** + * not allow to create menu + */ + CODE_65319(65319, "not allow to create menu"), + + /** + * please enable new custom service, or wait for a while if you have enabled + */ + CODE_65400(65400, "please enable new custom service, or wait for a while if you have enabled"), + + /** + * invalid custom service account + */ + CODE_65401(65401, "invalid custom service account"), + + /** + * the custom service account need to bind a wechat user + */ + CODE_65402(65402, "the custom service account need to bind a wechat user"), + + /** + * illegal nickname + */ + CODE_65403(65403, "illegal nickname"), + + /** + * illegal custom service account + */ + CODE_65404(65404, "illegal custom service account"), + + /** + * custom service account number reach limit + */ + CODE_65405(65405, "custom service account number reach limit"), + + /** + * custom service account exists + */ + CODE_65406(65406, "custom service account exists"), + + /** + * the wechat user have been one of your workers + */ + CODE_65407(65407, "the wechat user have been one of your workers"), + + /** + * you have already invited the wechat user + */ + CODE_65408(65408, "you have already invited the wechat user"), + + /** + * wechat account invalid + */ + CODE_65409(65409, "wechat account invalid"), + + /** + * too many custom service accounts bound by the worker + */ + CODE_65410(65410, "too many custom service accounts bound by the worker"), + + /** + * a effective invitation to bind the custom service account exists + */ + CODE_65411(65411, "a effective invitation to bind the custom service account exists"), + + /** + * the custom service account have been bound by a wechat user + */ + CODE_65412(65412, "the custom service account have been bound by a wechat user"), + + /** + * no effective session for the customer + */ + CODE_65413(65413, "no effective session for the customer"), + + /** + * another worker is serving the customer + */ + CODE_65414(65414, "another worker is serving the customer"), + + /** + * the worker is not online + */ + CODE_65415(65415, "the worker is not online"), + + /** + * param invalid, please check + */ + CODE_65416(65416, "param invalid, please check"), + + /** + * it is too long from the starttime to endtime + */ + CODE_65417(65417, "it is too long from the starttime to endtime"), + + /** + * homepage not exists + */ + CODE_65450(65450, "homepage not exists"), + + /** + * invalid store type + */ + CODE_68002(68002, "invalid store type"), + + /** + * invalid store name + */ + CODE_68003(68003, "invalid store name"), + + /** + * invalid store wxa path + */ + CODE_68004(68004, "invalid store wxa path"), + + /** + * miss store wxa path + */ + CODE_68005(68005, "miss store wxa path"), + + /** + * invalid kefu type + */ + CODE_68006(68006, "invalid kefu type"), + + /** + * invalid kefu wxa path + */ + CODE_68007(68007, "invalid kefu wxa path"), + + /** + * invalid kefu phone number + */ + CODE_68008(68008, "invalid kefu phone number"), + + /** + * invalid sub mch id + */ + CODE_68009(68009, "invalid sub mch id"), + + /** + * store id has exist + */ + CODE_68010(68010, "store id has exist"), + + /** + * miss store name + */ + CODE_68011(68011, "miss store name"), + + /** + * miss create time + */ + CODE_68012(68012, "miss create time"), + + /** + * invalid status + */ + CODE_68013(68013, "invalid status"), + + /** + * invalid receiver info + */ + CODE_68014(68014, "invalid receiver info"), + + /** + * invalid product + */ + CODE_68015(68015, "invalid product"), + + /** + * invalid pay type + */ + CODE_68016(68016, "invalid pay type"), + + /** + * invalid fast mail no + */ + CODE_68017(68017, "invalid fast mail no"), + + /** + * invalid busi id + */ + CODE_68018(68018, "invalid busi id"), + + /** + * miss product sku + */ + CODE_68019(68019, "miss product sku"), + + /** + * invalid service type + */ + CODE_68020(68020, "invalid service type"), + + /** + * invalid service status + */ + CODE_68021(68021, "invalid service status"), + + /** + * invalid service_id + */ + CODE_68022(68022, "invalid service_id"), + + /** + * service_id has exist + */ + CODE_68023(68023, "service_id has exist"), + + /** + * miss service wxa path + */ + CODE_68024(68024, "miss service wxa path"), + + /** + * invalid product sku + */ + CODE_68025(68025, "invalid product sku"), + + /** + * invalid product spu + */ + CODE_68026(68026, "invalid product spu"), + + /** + * miss product spu + */ + CODE_68027(68027, "miss product spu"), + + /** + * can not find product spu and spu in order list + */ + CODE_68028(68028, "can not find product spu and spu in order list"), + + /** + * sku and spu duplicated + */ + CODE_68029(68029, "sku and spu duplicated"), + + /** + * busi_id has exist + */ + CODE_68030(68030, "busi_id has exist"), + + /** + * update fail + */ + CODE_68031(68031, "update fail"), + + /** + * busi_id not exist + */ + CODE_68032(68032, "busi_id not exist"), + + /** + * store no exist + */ + CODE_68033(68033, "store no exist"), + + /** + * miss product number + */ + CODE_68034(68034, "miss product number"), + + /** + * miss wxa order detail path + */ + CODE_68035(68035, "miss wxa order detail path"), + + /** + * there is no enough products to refund + */ + CODE_68036(68036, "there is no enough products to refund"), + + /** + * invalid refund info + */ + CODE_68037(68037, "invalid refund info"), + + /** + * shipped but no fast mail info + */ + CODE_68038(68038, "shipped but no fast mail info"), + + /** + * invalid wechat pay no + */ + CODE_68039(68039, "invalid wechat pay no"), + + /** + * all product has been refunded, the order can not be finished + */ + CODE_68040(68040, "all product has been refunded, the order can not be finished"), + + /** + * invalid service create time, it must bigger than the time of order + */ + CODE_68041(68041, "invalid service create time, it must bigger than the time of order"), + + /** + * invalid total cost, it must be smaller than the sum of product and shipping cost + */ + CODE_68042(68042, "invalid total cost, it must be smaller than the sum of product and shipping cost"), + + /** + * invalid role + */ + CODE_68043(68043, "invalid role"), + + /** + * invalid service_available args + */ + CODE_68044(68044, "invalid service_available args"), + + /** + * invalid order type + */ + CODE_68045(68045, "invalid order type"), + + /** + * invalid order deliver type + */ + CODE_68046(68046, "invalid order deliver type"), + + /** + * require store_id + */ + CODE_68500(68500, "require store_id"), + + /** + * invalid store_id + */ + CODE_68501(68501, "invalid store_id"), + + /** + * invalid parameter, parameter is zero or missing + */ + CODE_71001(71001, "invalid parameter, parameter is zero or missing"), + + /** + * invalid orderid, may be the other parameter not fit with orderid + */ + CODE_71002(71002, "invalid orderid, may be the other parameter not fit with orderid"), + + /** + * coin not enough + */ + CODE_71003(71003, "coin not enough"), + + /** + * card is expired + */ + CODE_71004(71004, "card is expired"), + + /** + * limit exe count + */ + CODE_71005(71005, "limit exe count"), + + /** + * {@code limit coin count, 1 <= coin_count <= 100000} + */ + CODE_71006(71006, "limit coin count, 1 <= coin_count <= 100000"), + + /** + * order finish + */ + CODE_71007(71007, "order finish"), + + /** + * order time out + */ + CODE_71008(71008, "order time out"), + + /** + * no match card + */ + CODE_72001(72001, "no match card"), + + /** + * mchid is not bind appid + */ + CODE_72002(72002, "mchid is not bind appid"), + + /** + * invalid card type, need member card + */ + CODE_72003(72003, "invalid card type, need member card"), + + /** + * mchid is occupied by the other appid + */ + CODE_72004(72004, "mchid is occupied by the other appid"), + + /** + * out of mchid size limit + */ + CODE_72005(72005, "out of mchid size limit"), + + /** + * invald title + */ + CODE_72006(72006, "invald title"), + + /** + * invalid reduce cost, can not less than 100 + */ + CODE_72007(72007, "invalid reduce cost, can not less than 100"), + + /** + * invalid least cost, most larger than reduce cost + */ + CODE_72008(72008, "invalid least cost, most larger than reduce cost"), + + /** + * invalid get limit, can not over 50 + */ + CODE_72009(72009, "invalid get limit, can not over 50"), + + /** + * invalid mchid + */ + CODE_72010(72010, "invalid mchid"), + + /** + * invalid activate_ticket.Maybe this ticket is not belong this AppId + */ + CODE_72011(72011, "invalid activate_ticket.Maybe this ticket is not belong this AppId"), + + /** + * activate_ticket has been expired + */ + CODE_72012(72012, "activate_ticket has been expired"), + + /** + * unauthorized order_id or authorization is expired + */ + CODE_72013(72013, "unauthorized order_id or authorization is expired"), + + /** + * task card share stock can not modify stock + */ + CODE_72014(72014, "task card share stock can not modify stock"), + + /** + * unauthorized create invoice + */ + CODE_72015(72015, "unauthorized create invoice"), + + /** + * unauthorized create member card + */ + CODE_72016(72016, "unauthorized create member card"), + + /** + * invalid invoice title + */ + CODE_72017(72017, "invalid invoice title"), + + /** + * duplicate order id, invoice had inserted to user + */ + CODE_72018(72018, "duplicate order id, invoice had inserted to user"), + + /** + * {@code limit msg operation card list size, must <= 5} + */ + CODE_72019(72019, "limit msg operation card list size, must <= 5"), + + /** + * limit consume in use limit + */ + CODE_72020(72020, "limit consume in use limit"), + + /** + * unauthorized create general card + */ + CODE_72021(72021, "unauthorized create general card"), + + /** + * user unexpected, please add user to white list + */ + CODE_72022(72022, "user unexpected, please add user to white list"), + + /** + * invoice has been lock by others + */ + CODE_72023(72023, "invoice has been lock by others"), + + /** + * invoice status error + */ + CODE_72024(72024, "invoice status error"), + + /** + * invoice token error + */ + CODE_72025(72025, "invoice token error"), + + /** + * need set wx_activate true + */ + CODE_72026(72026, "need set wx_activate true"), + + /** + * invoice action error + */ + CODE_72027(72027, "invoice action error"), + + /** + * invoice never set pay mch info + */ + CODE_72028(72028, "invoice never set pay mch info"), + + /** + * invoice never set auth field + */ + CODE_72029(72029, "invoice never set auth field"), + + /** + * invalid mchid + */ + CODE_72030(72030, "invalid mchid"), + + /** + * invalid params + */ + CODE_72031(72031, "invalid params"), + + /** + * pay gift card rule expired + */ + CODE_72032(72032, "pay gift card rule expired"), + + /** + * pay gift card rule status err + */ + CODE_72033(72033, "pay gift card rule status err"), + + /** + * invlid rule id + */ + CODE_72034(72034, "invlid rule id"), + + /** + * biz reject insert + */ + CODE_72035(72035, "biz reject insert"), + + /** + * invoice is busy, try again please + */ + CODE_72036(72036, "invoice is busy, try again please"), + + /** + * invoice owner error + */ + CODE_72037(72037, "invoice owner error"), + + /** + * invoice order never auth + */ + CODE_72038(72038, "invoice order never auth"), + + /** + * invoice must be lock first + */ + CODE_72039(72039, "invoice must be lock first"), + + /** + * invoice pdf error + */ + CODE_72040(72040, "invoice pdf error"), + + /** + * billing_code and billing_no invalid + */ + CODE_72041(72041, "billing_code and billing_no invalid"), + + /** + * billing_code and billing_no repeated + */ + CODE_72042(72042, "billing_code and billing_no repeated"), + + /** + * billing_code or billing_no size error + */ + CODE_72043(72043, "billing_code or billing_no size error"), + + /** + * scan text out of time + */ + CODE_72044(72044, "scan text out of time"), + + /** + * check_code is empty + */ + CODE_72045(72045, "check_code is empty"), + + /** + * pdf_url is invalid + */ + CODE_72046(72046, "pdf_url is invalid"), + + /** + * pdf billing_code or pdf billing_no is error + */ + CODE_72047(72047, "pdf billing_code or pdf billing_no is error"), + + /** + * insert too many invoice, need auth again + */ + CODE_72048(72048, "insert too many invoice, need auth again"), + + /** + * never auth + */ + CODE_72049(72049, "never auth"), + + /** + * auth expired, need auth again + */ + CODE_72050(72050, "auth expired, need auth again"), + + /** + * app type error + */ + CODE_72051(72051, "app type error"), + + /** + * get too many invoice + */ + CODE_72052(72052, "get too many invoice"), + + /** + * user never auth + */ + CODE_72053(72053, "user never auth"), + + /** + * invoices is inserting, wait a moment please + */ + CODE_72054(72054, "invoices is inserting, wait a moment please"), + + /** + * too many invoices + */ + CODE_72055(72055, "too many invoices"), + + /** + * order_id repeated, please check order_id + */ + CODE_72056(72056, "order_id repeated, please check order_id"), + + /** + * today insert limit + */ + CODE_72057(72057, "today insert limit"), + + /** + * callback biz error + */ + CODE_72058(72058, "callback biz error"), + + /** + * this invoice is giving to others, wait a moment please + */ + CODE_72059(72059, "this invoice is giving to others, wait a moment please"), + + /** + * this invoice has been cancelled, check the reimburse_status please + */ + CODE_72060(72060, "this invoice has been cancelled, check the reimburse_status please"), + + /** + * this invoice has been closed, check the reimburse_status please + */ + CODE_72061(72061, "this invoice has been closed, check the reimburse_status please"), + + /** + * this code_auth_key is limited, try other code_auth_key please + */ + CODE_72062(72062, "this code_auth_key is limited, try other code_auth_key please"), + + /** + * biz contact is empty, set contact first please + */ + CODE_72063(72063, "biz contact is empty, set contact first please"), + + /** + * tbc error + */ + CODE_72064(72064, "tbc error"), + + /** + * tbc logic error + */ + CODE_72065(72065, "tbc logic error"), + + /** + * the card is send for advertisement, not allow modify time and budget + */ + CODE_72066(72066, "the card is send for advertisement, not allow modify time and budget"), + + /** + * BatchInsertAuthKey_Expired + */ + CODE_72067(72067, "BatchInsertAuthKey_Expired"), + + /** + * BatchInsertAuthKey_Owner + */ + CODE_72068(72068, "BatchInsertAuthKey_Owner"), + + /** + * BATCHTASKRUN_ERROR + */ + CODE_72069(72069, "BATCHTASKRUN_ERROR"), + + /** + * BIZ_TITLE_KEY_OUT_TIME + */ + CODE_72070(72070, "BIZ_TITLE_KEY_OUT_TIME"), + + /** + * Discern_GaoPeng_Error + */ + CODE_72071(72071, "Discern_GaoPeng_Error"), + + /** + * Discern_Type_Error + */ + CODE_72072(72072, "Discern_Type_Error"), + + /** + * Fee_Error + */ + CODE_72073(72073, "Fee_Error"), + + /** + * HAS_Auth + */ + CODE_72074(72074, "HAS_Auth"), + + /** + * HAS_SEND + */ + CODE_72075(72075, "HAS_SEND"), + + /** + * INVOICESIGN + */ + CODE_72076(72076, "INVOICESIGN"), + + /** + * KEY_DELETED + */ + CODE_72077(72077, "KEY_DELETED"), + + /** + * KEY_EXPIRED + */ + CODE_72078(72078, "KEY_EXPIRED"), + + /** + * MOUNT_ERROR + */ + CODE_72079(72079, "MOUNT_ERROR"), + + /** + * NO_FOUND + */ + CODE_72080(72080, "NO_FOUND"), + + /** + * No_Pull_Pdf + */ + CODE_72081(72081, "No_Pull_Pdf"), + + /** + * PDF_CHECK_ERROR + */ + CODE_72082(72082, "PDF_CHECK_ERROR"), + + /** + * PULL_PDF_FAIL + */ + CODE_72083(72083, "PULL_PDF_FAIL"), + + /** + * PUSH_BIZ_EMPTY + */ + CODE_72084(72084, "PUSH_BIZ_EMPTY"), + + /** + * SDK_APPID_ERROR + */ + CODE_72085(72085, "SDK_APPID_ERROR"), + + /** + * SDK_BIZ_ERROR + */ + CODE_72086(72086, "SDK_BIZ_ERROR"), + + /** + * SDK_URL_ERROR + */ + CODE_72087(72087, "SDK_URL_ERROR"), + + /** + * Search_Title_Fail + */ + CODE_72088(72088, "Search_Title_Fail"), + + /** + * TITLE_BUSY + */ + CODE_72089(72089, "TITLE_BUSY"), + + /** + * TITLE_NO_FOUND + */ + CODE_72090(72090, "TITLE_NO_FOUND"), + + /** + * TOKEN_ERR + */ + CODE_72091(72091, "TOKEN_ERR"), + + /** + * USER_TITLE_NOT_FOUND + */ + CODE_72092(72092, "USER_TITLE_NOT_FOUND"), + + /** + * Verify_3rd_Fail + */ + CODE_72093(72093, "Verify_3rd_Fail"), + + /** + * sys error make out invoice failed + */ + CODE_73000(73000, "sys error make out invoice failed"), + + /** + * wxopenid error + */ + CODE_73001(73001, "wxopenid error"), + + /** + * ddh orderid empty + */ + CODE_73002(73002, "ddh orderid empty"), + + /** + * wxopenid empty + */ + CODE_73003(73003, "wxopenid empty"), + + /** + * fpqqlsh empty + */ + CODE_73004(73004, "fpqqlsh empty"), + + /** + * not a commercial + */ + CODE_73005(73005, "not a commercial"), + + /** + * kplx empty + */ + CODE_73006(73006, "kplx empty"), + + /** + * nsrmc empty + */ + CODE_73007(73007, "nsrmc empty"), + + /** + * nsrdz empty + */ + CODE_73008(73008, "nsrdz empty"), + + /** + * nsrdh empty + */ + CODE_73009(73009, "nsrdh empty"), + + /** + * ghfmc empty + */ + CODE_73010(73010, "ghfmc empty"), + + /** + * kpr empty + */ + CODE_73011(73011, "kpr empty"), + + /** + * jshj empty + */ + CODE_73012(73012, "jshj empty"), + + /** + * hjje empty + */ + CODE_73013(73013, "hjje empty"), + + /** + * hjse empty + */ + CODE_73014(73014, "hjse empty"), + + /** + * hylx empty + */ + CODE_73015(73015, "hylx empty"), + + /** + * nsrsbh empty + */ + CODE_73016(73016, "nsrsbh empty"), + + /** + * kaipiao plat error + */ + CODE_73100(73100, "kaipiao plat error"), + + /** + * nsrsbh not cmp + */ + CODE_73101(73101, "nsrsbh not cmp"), + + /** + * invalid wxa appid in url_cell, wxa appid is need to bind biz appid + */ + CODE_73103(73103, "invalid wxa appid in url_cell, wxa appid is need to bind biz appid"), + + /** + * reach frequency limit + */ + CODE_73104(73104, "reach frequency limit"), + + /** + * Kp plat make invoice timeout, please try again with the same fpqqlsh + */ + CODE_73105(73105, "Kp plat make invoice timeout, please try again with the same fpqqlsh"), + + /** + * Fpqqlsh exist with different ddh + */ + CODE_73106(73106, "Fpqqlsh exist with different ddh"), + + /** + * Fpqqlsh is processing, please wait and query later + */ + CODE_73107(73107, "Fpqqlsh is processing, please wait and query later"), + + /** + * This ddh with other fpqqlsh already exist + */ + CODE_73108(73108, "This ddh with other fpqqlsh already exist"), + + /** + * This Fpqqlsh not exist in kpplat + */ + CODE_73109(73109, "This Fpqqlsh not exist in kpplat"), + + /** + * get card detail by card id and code fail + */ + CODE_73200(73200, "get card detail by card id and code fail"), + + /** + * get cloud invoice record fail + */ + CODE_73201(73201, "get cloud invoice record fail"), + + /** + * get appinfo fail + */ + CODE_73202(73202, "get appinfo fail"), + + /** + * get invoice category or rule kv error + */ + CODE_73203(73203, "get invoice category or rule kv error"), + + /** + * request card not exist + */ + CODE_73204(73204, "request card not exist"), + + /** + * 朋友的券玩法升级中,当前暂停创建,请创建其他类型卡券 + */ + CODE_73205(73205, "朋友的券玩法升级中,当前暂停创建,请创建其他类型卡券"), + + /** + * 朋友的券玩法升级中,当前暂停券点充值,请创建其他类型卡券 + */ + CODE_73206(73206, "朋友的券玩法升级中,当前暂停券点充值,请创建其他类型卡券"), + + /** + * 朋友的券玩法升级中,当前暂停开通券点账户 + */ + CODE_73207(73207, "朋友的券玩法升级中,当前暂停开通券点账户"), + + /** + * 朋友的券玩法升级中,当前不支持修改库存 + */ + CODE_73208(73208, "朋友的券玩法升级中,当前不支持修改库存"), + + /** + * 朋友的券玩法升级中,当前不支持修改有效期 + */ + CODE_73209(73209, "朋友的券玩法升级中,当前不支持修改有效期"), + + /** + * 当前批次不支持修改卡券批次库存 + */ + CODE_73210(73210, "当前批次不支持修改卡券批次库存"), + + /** + * 不再支持配置网页链接跳转,请选择小程序替代 + */ + CODE_73211(73211, "不再支持配置网页链接跳转,请选择小程序替代"), + + /** + * unauthorized backup member + */ + CODE_73212(73212, "unauthorized backup member"), + + /** + * invalid code type + */ + CODE_73213(73213, "invalid code type"), + + /** + * the user is already a member + */ + CODE_73214(73214, "the user is already a member"), + + /** + * 支付打通券能力已下线,请直接使用微信支付代金券API:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/marketing/convention/chapter1_1.shtml + */ + CODE_73215(73215, "支付打通券能力已下线,请直接使用微信支付代金券API:https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/marketing/convention/chapter1_1.shtml"), + + /** + * 不合法的按钮名字,请从中选择一个:使用礼品卡/立即使用/去点外卖 + */ + CODE_73216(73216, "不合法的按钮名字,请从中选择一个:使用礼品卡/立即使用/去点外卖"), + + /** + * 礼品卡本身没有设置appname和path,不允许在修改接口设置 + */ + CODE_73217(73217, "礼品卡本身没有设置appname和path,不允许在修改接口设置"), + + /** + * 未授权使用礼品卡落地页跳转小程序功能 + */ + CODE_73218(73218, "未授权使用礼品卡落地页跳转小程序功能"), + + /** + * not find this wx_hotel_id info + */ + CODE_74000(74000, "not find this wx_hotel_id info"), + + /** + * request some param empty + */ + CODE_74001(74001, "request some param empty"), + + /** + * request some param error + */ + CODE_74002(74002, "request some param error"), + + /** + * request some param error + */ + CODE_74003(74003, "request some param error"), + + /** + * datetime error + */ + CODE_74004(74004, "datetime error"), + + /** + * checkin mode error + */ + CODE_74005(74005, "checkin mode error"), + + /** + * carid from error + */ + CODE_74007(74007, "carid from error"), + + /** + * this hotel routecode not exist + */ + CODE_74008(74008, "this hotel routecode not exist"), + + /** + * this hotel routecode info error contract developer + */ + CODE_74009(74009, "this hotel routecode info error contract developer"), + + /** + * maybe not support report mode + */ + CODE_74010(74010, "maybe not support report mode"), + + /** + * pic deocde not ok maybe its not good picdata + */ + CODE_74011(74011, "pic deocde not ok maybe its not good picdata"), + + /** + * verify sys erro + */ + CODE_74021(74021, "verify sys erro"), + + /** + * inner police erro + */ + CODE_74022(74022, "inner police erro"), + + /** + * unable to detect the face + */ + CODE_74023(74023, "unable to detect the face"), + + /** + * report checkin 2 lvye sys erro + */ + CODE_74040(74040, "report checkin 2 lvye sys erro"), + + /** + * report checkou 2 lvye sys erro + */ + CODE_74041(74041, "report checkou 2 lvye sys erro"), + + /** + * some param emtpy please check + */ + CODE_75001(75001, "some param emtpy please check"), + + /** + * param illegal please check + */ + CODE_75002(75002, "param illegal please check"), + + /** + * sys error kv store error + */ + CODE_75003(75003, "sys error kv store error"), + + /** + * sys error kvstring store error + */ + CODE_75004(75004, "sys error kvstring store error"), + + /** + * product not exist please check your product_id + */ + CODE_75005(75005, "product not exist please check your product_id"), + + /** + * order not exist please check order_id and buyer_appid + */ + CODE_75006(75006, "order not exist please check order_id and buyer_appid"), + + /** + * do not allow this status to change please check this order_id status now + */ + CODE_75007(75007, "do not allow this status to change please check this order_id status now"), + + /** + * product has exist please use new id + */ + CODE_75008(75008, "product has exist please use new id"), + + /** + * notify order status failed + */ + CODE_75009(75009, "notify order status failed"), + + /** + * buyer bussiness info not exist + */ + CODE_75010(75010, "buyer bussiness info not exist"), + + /** + * you had registered + */ + CODE_75011(75011, "you had registered"), + + /** + * store image key to kv error, please try again + */ + CODE_75012(75012, "store image key to kv error, please try again"), + + /** + * get image fail, please check you image key + */ + CODE_75013(75013, "get image fail, please check you image key"), + + /** + * this key is not belong to you + */ + CODE_75014(75014, "this key is not belong to you"), + + /** + * this key is expired + */ + CODE_75015(75015, "this key is expired"), + + /** + * encrypt decode key fail + */ + CODE_75016(75016, "encrypt decode key fail"), + + /** + * encrypt encode key fail + */ + CODE_75017(75017, "encrypt encode key fail"), + + /** + * bind buyer business info fail please contact us + */ + CODE_75018(75018, "bind buyer business info fail please contact us"), + + /** + * this key is empty, user may not upload file + */ + CODE_75019(75019, "this key is empty, user may not upload file"), + + /** + * 系统错误,请稍后再试 + */ + CODE_80000(80000, "系统错误,请稍后再试"), + + /** + * 参数格式校验错误 + */ + CODE_80001(80001, "参数格式校验错误"), + + /** + * 签名失败 + */ + CODE_80002(80002, "签名失败"), + + /** + * 该日期订单未生成 + */ + CODE_80003(80003, "该日期订单未生成"), + + /** + * 用户未绑卡 + */ + CODE_80004(80004, "用户未绑卡"), + + /** + * 姓名不符 + */ + CODE_80005(80005, "姓名不符"), + + /** + * 身份证不符 + */ + CODE_80006(80006, "身份证不符"), + + /** + * 获取城市信息失败 + */ + CODE_80007(80007, "获取城市信息失败"), + + /** + * 未找到指定少儿信息 + */ + CODE_80008(80008, "未找到指定少儿信息"), + + /** + * 少儿身份证不符 + */ + CODE_80009(80009, "少儿身份证不符"), + + /** + * 少儿未绑定 + */ + CODE_80010(80010, "少儿未绑定"), + + /** + * 签约号不符 + */ + CODE_80011(80011, "签约号不符"), + + /** + * 该地区局方配置不存在 + */ + CODE_80012(80012, "该地区局方配置不存在"), + + /** + * 调用方appid与局方配置不匹配 + */ + CODE_80013(80013, "调用方appid与局方配置不匹配"), + + /** + * 获取消息账号失败 + */ + CODE_80014(80014, "获取消息账号失败"), + + /** + * 非法的插件版本 + */ + CODE_80066(80066, "非法的插件版本"), + + /** + * 找不到使用的插件 + */ + CODE_80067(80067, "找不到使用的插件"), + + /** + * 没有权限使用该插件 + */ + CODE_80082(80082, "没有权限使用该插件"), + + /** + * 商家未接入 + */ + CODE_80101(80101, "商家未接入"), + + /** + * 实名校验code不存在 + */ + CODE_80111(80111, "实名校验code不存在"), + + /** + * code并发冲突 + */ + CODE_80112(80112, "code并发冲突"), + + /** + * 无效code + */ + CODE_80113(80113, "无效code"), + + /** + * report_type无效 + */ + CODE_80201(80201, "report_type无效"), + + /** + * service_type无效 + */ + CODE_80202(80202, "service_type无效"), + + /** + * 申请单不存在 + */ + CODE_80300(80300, "申请单不存在"), + + /** + * 申请单不属于该账号 + */ + CODE_80301(80301, "申请单不属于该账号"), + + /** + * 激活号段有重叠 + */ + CODE_80302(80302, "激活号段有重叠"), + + /** + * 码格式错误 + */ + CODE_80303(80303, "码格式错误"), + + /** + * 该码未激活 + */ + CODE_80304(80304, "该码未激活"), + + /** + * 激活失败 + */ + CODE_80305(80305, "激活失败"), + + /** + * 码索引超出申请范围 + */ + CODE_80306(80306, "码索引超出申请范围"), + + /** + * 申请已存在 + */ + CODE_80307(80307, "申请已存在"), + + /** + * 子任务未完成 + */ + CODE_80308(80308, "子任务未完成"), + + /** + * 子任务文件过期 + */ + CODE_80309(80309, "子任务文件过期"), + + /** + * 子任务不存在 + */ + CODE_80310(80310, "子任务不存在"), + + /** + * 获取文件失败 + */ + CODE_80311(80311, "获取文件失败"), + + /** + * 加密数据失败 + */ + CODE_80312(80312, "加密数据失败"), + + /** + * 加密数据密钥不存在,请联系接口人申请 + */ + CODE_80313(80313, "加密数据密钥不存在,请联系接口人申请"), + + /** + * can not set page_id in AddGiftCardPage + */ + CODE_81000(81000, "can not set page_id in AddGiftCardPage"), + + /** + * card_list is empty + */ + CODE_81001(81001, "card_list is empty"), + + /** + * card_id is not giftcard + */ + CODE_81002(81002, "card_id is not giftcard"), + + /** + * banner_pic_url is empty + */ + CODE_81004(81004, "banner_pic_url is empty"), + + /** + * banner_pic_url is not from cdn + */ + CODE_81005(81005, "banner_pic_url is not from cdn"), + + /** + * giftcard_wrap_pic_url_list is empty + */ + CODE_81006(81006, "giftcard_wrap_pic_url_list is empty"), + + /** + * giftcard_wrap_pic_url_list is not from cdn + */ + CODE_81007(81007, "giftcard_wrap_pic_url_list is not from cdn"), + + /** + * address is empty + */ + CODE_81008(81008, "address is empty"), + + /** + * service_phone is invalid + */ + CODE_81009(81009, "service_phone is invalid"), + + /** + * biz_description is empty + */ + CODE_81010(81010, "biz_description is empty"), + + /** + * invalid page_id + */ + CODE_81011(81011, "invalid page_id"), + + /** + * invalid order_id + */ + CODE_81012(81012, "invalid order_id"), + + /** + * invalid TIME_RANGE, begin_time + 31day must less than end_time + */ + CODE_81013(81013, "invalid TIME_RANGE, begin_time + 31day must less than end_time"), + + /** + * invalid count! count must equal or less than 100 + */ + CODE_81014(81014, "invalid count! count must equal or less than 100"), + + /** + * invalid category_index OR category.title is empty OR is_banner but has_category_index + */ + CODE_81015(81015, "invalid category_index OR category.title is empty OR is_banner but has_category_index"), + + /** + * is_banner is more than 1 + */ + CODE_81016(81016, "is_banner is more than 1"), + + /** + * order status error, please check pay status or gifting_status + */ + CODE_81017(81017, "order status error, please check pay status or gifting_status"), + + /** + * refund reduplicate, the order is already refunded + */ + CODE_81018(81018, "refund reduplicate, the order is already refunded"), + + /** + * lock order fail! the order is refunding by another request + */ + CODE_81019(81019, "lock order fail! the order is refunding by another request"), + + /** + * Invalid Args! page_id.size!=0 but all==true, or page_id.size==0 but all==false. + */ + CODE_81020(81020, "Invalid Args! page_id.size!=0 but all==true, or page_id.size==0 but all==false."), + + /** + * Empty theme_pic_url. + */ + CODE_81021(81021, "Empty theme_pic_url."), + + /** + * Empty theme.title. + */ + CODE_81022(81022, "Empty theme.title."), + + /** + * Empty theme.title_title. + */ + CODE_81023(81023, "Empty theme.title_title."), + + /** + * Empty theme.item_list. + */ + CODE_81024(81024, "Empty theme.item_list."), + + /** + * Empty theme.pic_item_list. + */ + CODE_81025(81025, "Empty theme.pic_item_list."), + + /** + * Invalid theme.title.length . + */ + CODE_81026(81026, "Invalid theme.title.length ."), + + /** + * Empty background_pic_url. + */ + CODE_81027(81027, "Empty background_pic_url."), + + /** + * Empty default_gifting_msg. + */ + CODE_81028(81028, "Empty default_gifting_msg."), + + /** + * Duplicate order_id + */ + CODE_81029(81029, "Duplicate order_id"), + + /** + * PreAlloc code fail + */ + CODE_81030(81030, "PreAlloc code fail"), + + /** + * Too many theme participate_activity + */ + CODE_81031(81031, "Too many theme participate_activity"), + + /** + * biz_template_id not exist + */ + CODE_82000(82000, "biz_template_id not exist"), + + /** + * result_page_style_id not exist + */ + CODE_82001(82001, "result_page_style_id not exist"), + + /** + * deal_msg_style_id not exist + */ + CODE_82002(82002, "deal_msg_style_id not exist"), + + /** + * card_style_id not exist + */ + CODE_82003(82003, "card_style_id not exist"), + + /** + * biz template not audit OK + */ + CODE_82010(82010, "biz template not audit OK"), + + /** + * biz template banned + */ + CODE_82011(82011, "biz template banned"), + + /** + * user not use service first + */ + CODE_82020(82020, "user not use service first"), + + /** + * exceed long period + */ + CODE_82021(82021, "exceed long period"), + + /** + * exceed long period max send cnt + */ + CODE_82022(82022, "exceed long period max send cnt"), + + /** + * exceed short period max send cnt + */ + CODE_82023(82023, "exceed short period max send cnt"), + + /** + * exceed data size limit + */ + CODE_82024(82024, "exceed data size limit"), + + /** + * invalid url + */ + CODE_82025(82025, "invalid url"), + + /** + * service disabled + */ + CODE_82026(82026, "service disabled"), + + /** + * invalid miniprogram appid + */ + CODE_82027(82027, "invalid miniprogram appid"), + + /** + * wx_cs_code should not be empty. + */ + CODE_82100(82100, "wx_cs_code should not be empty."), + + /** + * wx_cs_code is invalid. + */ + CODE_82101(82101, "wx_cs_code is invalid."), + + /** + * wx_cs_code is expire. + */ + CODE_82102(82102, "wx_cs_code is expire."), + + /** + * user_ip should not be empty. + */ + CODE_82103(82103, "user_ip should not be empty."), + + /** + * 公众平台账号与服务id不匹配 + */ + CODE_82200(82200, "公众平台账号与服务id不匹配"), + + /** + * 该停车场已存在,请勿重复添加 + */ + CODE_82201(82201, "该停车场已存在,请勿重复添加"), + + /** + * 该停车场信息不存在,请先导入 + */ + CODE_82202(82202, "该停车场信息不存在,请先导入"), + + /** + * 停车场价格格式不正确 + */ + CODE_82203(82203, "停车场价格格式不正确"), + + /** + * appid与code不匹配 + */ + CODE_82204(82204, "appid与code不匹配"), + + /** + * wx_park_code字段为空 + */ + CODE_82205(82205, "wx_park_code字段为空"), + + /** + * wx_park_code无效或已过期 + */ + CODE_82206(82206, "wx_park_code无效或已过期"), + + /** + * 电话字段为空 + */ + CODE_82207(82207, "电话字段为空"), + + /** + * 关闭时间格式不正确 + */ + CODE_82208(82208, "关闭时间格式不正确"), + + /** + * 该appid不支持开通城市服务插件 + */ + CODE_82300(82300, "该appid不支持开通城市服务插件"), + + /** + * 添加插件失败 + */ + CODE_82301(82301, "添加插件失败"), + + /** + * 未添加城市服务插件 + */ + CODE_82302(82302, "未添加城市服务插件"), + + /** + * fileid无效 + */ + CODE_82303(82303, "fileid无效"), + + /** + * 临时文件过期 + */ + CODE_82304(82304, "临时文件过期"), + + /** + * there is some param not exist + */ + CODE_83000(83000, "there is some param not exist"), + + /** + * system error + */ + CODE_83001(83001, "system error"), + + /** + * create_url_sence_failed + */ + CODE_83002(83002, "create_url_sence_failed"), + + /** + * appid maybe error or retry + */ + CODE_83003(83003, "appid maybe error or retry"), + + /** + * create appid auth failed or retry + */ + CODE_83004(83004, "create appid auth failed or retry"), + + /** + * wxwebencrytoken errro + */ + CODE_83005(83005, "wxwebencrytoken errro"), + + /** + * wxwebencrytoken expired or no exist + */ + CODE_83006(83006, "wxwebencrytoken expired or no exist"), + + /** + * wxwebencrytoken expired + */ + CODE_83007(83007, "wxwebencrytoken expired"), + + /** + * wxwebencrytoken no auth + */ + CODE_83008(83008, "wxwebencrytoken no auth"), + + /** + * wxwebencrytoken not the mate with openid + */ + CODE_83009(83009, "wxwebencrytoken not the mate with openid"), + + /** + * no exist service + */ + CODE_83200(83200, "no exist service"), + + /** + * uin has not the service + */ + CODE_83201(83201, "uin has not the service"), + + /** + * params is not json or not json array + */ + CODE_83202(83202, "params is not json or not json array"), + + /** + * params num exceed 10 + */ + CODE_83203(83203, "params num exceed 10"), + + /** + * object has not key + */ + CODE_83204(83204, "object has not key"), + + /** + * key is not string + */ + CODE_83205(83205, "key is not string"), + + /** + * object has not value + */ + CODE_83206(83206, "object has not value"), + + /** + * value is not string + */ + CODE_83207(83207, "value is not string"), + + /** + * key or value is empty + */ + CODE_83208(83208, "key or value is empty"), + + /** + * key exist repeated + */ + CODE_83209(83209, "key exist repeated"), + + /** + * invalid identify id + */ + CODE_84001(84001, "invalid identify id"), + + /** + * user data expired + */ + CODE_84002(84002, "user data expired"), + + /** + * user data not exist + */ + CODE_84003(84003, "user data not exist"), + + /** + * video upload fail! + */ + CODE_84004(84004, "video upload fail!"), + + /** + * video download fail! please try again + */ + CODE_84005(84005, "video download fail! please try again"), + + /** + * name or id_card_number empty + */ + CODE_84006(84006, "name or id_card_number empty"), + + /** + * 微信号不存在或微信号设置为不可搜索 user not exist or user cannot be searched + */ + CODE_85001(85001, "微信号不存在或微信号设置为不可搜索"), + + /** + * 小程序绑定的体验者数量达到上限 number of tester reach bind limit + */ + CODE_85002(85002, "小程序绑定的体验者数量达到上限"), + + /** + * 微信号绑定的小程序体验者达到上限 user already bind too many weapps + */ + CODE_85003(85003, "微信号绑定的小程序体验者达到上限"), + + /** + * 微信号已经绑定 user already bind + */ + CODE_85004(85004, "微信号已经绑定"), + + /** + * appid not bind weapp + */ + CODE_85005(85005, "appid not bind weapp"), + + /** + * 标签格式错误 tag is in invalid format + */ + CODE_85006(85006, "标签格式错误"), + + /** + * 页面路径错误 page is in invalid format + */ + CODE_85007(85007, "页面路径错误"), + + /** + * 当前小程序没有已经审核通过的类目,请添加类目成功后重试 category is in invalid format + */ + CODE_85008(85008, "当前小程序没有已经审核通过的类目,请添加类目成功后重试"), + + /** + * 已经有正在审核的版本 already submit a version under auditing + */ + CODE_85009(85009, "已经有正在审核的版本"), + + /** + * item_list 有项目为空 missing required data + */ + CODE_85010(85010, "item_list 有项目为空"), + + /** + * 标题填写错误 title is in invalid format + */ + CODE_85011(85011, "标题填写错误"), + + /** + * 无效的审核 id invalid audit id + */ + CODE_85012(85012, "无效的审核 id"), + + /** + * 无效的自定义配置 invalid ext_json, parse fail or containing invalid path + */ + CODE_85013(85013, "无效的自定义配置"), + + /** + * 无效的模板编号 template not exist + */ + CODE_85014(85014, "无效的模板编号"), + + /** + * 该账号不是小程序账号/版本输入错误 + */ + CODE_85015(85015, "该账号不是小程序账号/版本输入错误"), + + /** + * 版本输入错误 + */ +// CODE_85015(85015, "版本输入错误"), + + /** + * 域名数量超过限制 ,总数不能超过1000 exceed valid domain count + */ + CODE_85016(85016, "域名数量超过限制 ,总数不能超过1000"), + + /** + * 没有新增域名,请确认小程序已经添加了域名或该域名是否没有在第三方平台添加 no domain to modify after filtered, please confirm the domain has been set in miniprogram or open + */ + CODE_85017(85017, "没有新增域名,请确认小程序已经添加了域名或该域名是否没有在第三方平台添加"), + + /** + * 域名没有在第三方平台设置 + */ + CODE_85018(85018, "域名没有在第三方平台设置"), + + /** + * 没有审核版本 no version is under auditing + */ + CODE_85019(85019, "没有审核版本"), + + /** + * 审核状态未满足发布 status not allowed + */ + CODE_85020(85020, "审核状态未满足发布"), + + /** + * status not allowed + */ + CODE_85021(85021, "status not allowed"), + + /** + * invalid action + */ + CODE_85022(85022, "invalid action"), + + /** + * 审核列表填写的项目数不在 1-5 以内 item size is not in valid range + */ + CODE_85023(85023, "审核列表填写的项目数不在 1-5 以内"), + + /** + * need complete material + */ + CODE_85024(85024, "need complete material"), + + /** + * this phone reach bind limit + */ + CODE_85025(85025, "this phone reach bind limit"), + + /** + * this wechat account reach bind limit + */ + CODE_85026(85026, "this wechat account reach bind limit"), + + /** + * this idcard reach bind limit + */ + CODE_85027(85027, "this idcard reach bind limit"), + + /** + * this contractor reach bind limit + */ + CODE_85028(85028, "this contractor reach bind limit"), + + /** + * nickname has used + */ + CODE_85029(85029, "nickname has used"), + + /** + * invalid nickname size(4-30) + */ + CODE_85030(85030, "invalid nickname size(4-30)"), + + /** + * nickname is forbidden + */ + CODE_85031(85031, "nickname is forbidden"), + + /** + * nickname is complained + */ + CODE_85032(85032, "nickname is complained"), + + /** + * nickname is illegal + */ + CODE_85033(85033, "nickname is illegal"), + + /** + * nickname is protected + */ + CODE_85034(85034, "nickname is protected"), + + /** + * nickname is forbidden for different contractor + */ + CODE_85035(85035, "nickname is forbidden for different contractor"), + + /** + * introduction is illegal + */ + CODE_85036(85036, "introduction is illegal"), + + /** + * store has added + */ + CODE_85038(85038, "store has added"), + + /** + * store has added by others + */ + CODE_85039(85039, "store has added by others"), + + /** + * store has added by yourseld + */ + CODE_85040(85040, "store has added by yourseld"), + + /** + * credential has used + */ + CODE_85041(85041, "credential has used"), + + /** + * nearby reach limit + */ + CODE_85042(85042, "nearby reach limit"), + + /** + * 模板错误 invalid template, something wrong? + */ + CODE_85043(85043, "模板错误"), + + /** + * 代码包超过大小限制 package exceed max limit + */ + CODE_85044(85044, "代码包超过大小限制"), + + /** + * ext_json 有不存在的路径 some path in ext_json not exist + */ + CODE_85045(85045, "ext_json 有不存在的路径"), + + /** + * tabBar 中缺少 path pagepath missing in tabbar list + */ + CODE_85046(85046, "tabBar 中缺少 path"), + + /** + * pages 字段为空 pages are empty + */ + CODE_85047(85047, "pages 字段为空"), + + /** + * ext_json 解析失败 parse ext_json fail + */ + CODE_85048(85048, "ext_json 解析失败"), + + /** + * reach headimg or introduction quota limit + */ + CODE_85049(85049, "reach headimg or introduction quota limit"), + + /** + * verifying, don't apply again + */ + CODE_85050(85050, "verifying, don't apply again"), + + /** + * version_desc或者preview_info超限 data too large + */ + CODE_85051(85051, "version_desc或者preview_info超限"), + + /** + * app is already released + */ + CODE_85052(85052, "app is already released"), + + /** + * please apply merchant first + */ + CODE_85053(85053, "please apply merchant first"), + + /** + * poi_id is null, please upgrade first + */ + CODE_85054(85054, "poi_id is null, please upgrade first"), + + /** + * map_poi_id is invalid + */ + CODE_85055(85055, "map_poi_id is invalid"), + + /** + * mediaid is invalid + */ + CODE_85056(85056, "mediaid is invalid"), + + /** + * invalid widget data format + */ + CODE_85057(85057, "invalid widget data format"), + + /** + * no valid audit_id exist + */ + CODE_85058(85058, "no valid audit_id exist"), + + /** + * overseas access deny + */ + CODE_85059(85059, "overseas access deny"), + + /** + * invalid taskid + */ + CODE_85060(85060, "invalid taskid"), + + /** + * this phone reach bind limit + */ + CODE_85061(85061, "this phone reach bind limit"), + + /** + * this phone in black list + */ + CODE_85062(85062, "this phone in black list"), + + /** + * idcard in black list + */ + CODE_85063(85063, "idcard in black list"), + + /** + * 找不到模板 template not found + */ + CODE_85064(85064, "找不到模板"), + + /** + * 模板库已满 template list is full + */ + CODE_85065(85065, "模板库已满"), + + /** + * 链接错误 illegal prefix + */ + CODE_85066(85066, "链接错误"), + + /** + * input data error + */ + CODE_85067(85067, "input data error"), + + /** + * 测试链接不是子链接 test url is not the sub prefix + */ + CODE_85068(85068, "测试链接不是子链接"), + + /** + * 校验文件失败 check confirm file fail + */ + CODE_85069(85069, "校验文件失败"), + + /** + * 个人类型小程序无法设置二维码规则 prefix in black list + */ + CODE_85070(85070, "个人类型小程序无法设置二维码规则"), + + /** + * 已添加该链接,请勿重复添加 prefix added repeated + */ + CODE_85071(85071, "已添加该链接,请勿重复添加"), + + /** + * 该链接已被占用 prefix owned by other + */ + CODE_85072(85072, "该链接已被占用"), + + /** + * 二维码规则已满 prefix beyond limit + */ + CODE_85073(85073, "二维码规则已满"), + + /** + * 小程序未发布, 小程序必须先发布代码才可以发布二维码跳转规则 not published + */ + CODE_85074(85074, "小程序未发布, 小程序必须先发布代码才可以发布二维码跳转规则"), + + /** + * 个人类型小程序无法设置二维码规则 can not access + */ + CODE_85075(85075, "个人类型小程序无法设置二维码规则"), + + /** + * 小程序类目信息失效(类目中含有官方下架的类目,请重新选择类目) some category you choose is no longger supported, please choose other category + */ + CODE_85077(85077, "小程序类目信息失效(类目中含有官方下架的类目,请重新选择类目)"), + + /** + * operator info error + */ + CODE_85078(85078, "operator info error"), + + /** + * 小程序没有线上版本,不能进行灰度 miniprogram has no online release + */ + CODE_85079(85079, "小程序没有线上版本,不能进行灰度"), + + /** + * 小程序提交的审核未审核通过 miniprogram commit not approved + */ + CODE_85080(85080, "小程序提交的审核未审核通过"), + + /** + * 无效的发布比例 invalid gray percentage + */ + CODE_85081(85081, "无效的发布比例"), + + /** + * 当前的发布比例需要比之前设置的高 gray percentage too low + */ + CODE_85082(85082, "当前的发布比例需要比之前设置的高"), + + /** + * 搜索标记位被封禁,无法修改 search status is banned + */ + CODE_85083(85083, "搜索标记位被封禁,无法修改"), + + /** + * 非法的 status 值,只能填 0 或者 1 search status invalid + */ + CODE_85084(85084, "非法的 status 值,只能填 0 或者 1"), + + /** + * 小程序提审数量已达本月上限,请点击查看 submit audit reach limit pleasetry later + */ + CODE_85085(85085, "小程序提审数量已达本月上限,请点击查看"), + + /** + * 提交代码审核之前需提前上传代码 must commit before submit audit + */ + CODE_85086(85086, "提交代码审核之前需提前上传代码"), + + /** + * 小程序已使用 api navigateToMiniProgram,请声明跳转 appid 列表后再次提交 navigatetominiprogram appid list empty + */ + CODE_85087(85087, "小程序已使用 api navigateToMiniProgram,请声明跳转 appid 列表后再次提交"), + + /** + * no qbase privilege + */ + CODE_85088(85088, "no qbase privilege"), + + /** + * config not found + */ + CODE_85089(85089, "config not found"), + + /** + * wait and commit for this exappid later + */ + CODE_85090(85090, "wait and commit for this exappid later"), + + /** + * 小程序的搜索开关被关闭。请访问设置页面打开开关再重试 search status was turned off + */ + CODE_85091(85091, "小程序的搜索开关被关闭。请访问设置页面打开开关再重试"), + + /** + * preview_info格式错误 invalid preview_info format + */ + CODE_85092(85092, "preview_info格式错误"), + + /** + * preview_info 视频或者图片个数超限 invalid preview_info size + */ + CODE_85093(85093, "preview_info 视频或者图片个数超限"), + + /** + * 需提供审核机制说明信息 need add ugc declare + */ + CODE_85094(85094, "需提供审核机制说明信息"), + + /** + * 小程序不能发送该运动类型或运动类型不存在 + */ + CODE_85101(85101, "小程序不能发送该运动类型或运动类型不存在"), + + /** + * 数值异常 + */ + CODE_85102(85102, "数值异常"), + + /** + * 不是由第三方代小程序进行调用 should be called only from third party + */ + CODE_86000(86000, "不是由第三方代小程序进行调用"), + + /** + * 不存在第三方的已经提交的代码 component experience version not exists + */ + CODE_86001(86001, "不存在第三方的已经提交的代码"), + + /** + * 小程序还未设置昵称、头像、简介。请先设置完后再重新提交 miniprogram have not completed init procedure + */ + CODE_86002(86002, "小程序还未设置昵称、头像、简介。请先设置完后再重新提交"), + + /** + * component do not has category mall + */ + CODE_86003(86003, "component do not has category mall"), + + /** + * invalid wechat + */ + CODE_86004(86004, "invalid wechat"), + + /** + * wechat limit frequency + */ + CODE_86005(86005, "wechat limit frequency"), + + /** + * has no quota to send group msg + */ + CODE_86006(86006, "has no quota to send group msg"), + + /** + * 小程序禁止提交 + */ + CODE_86007(86007, "小程序禁止提交"), + + /** + * 服务商被处罚,限制全部代码提审能力 + */ + CODE_86008(86008, "服务商被处罚,限制全部代码提审能力"), + + /** + * 服务商新增小程序代码提审能力被限制 + */ + CODE_86009(86009, "服务商新增小程序代码提审能力被限制"), + + /** + * 服务商迭代小程序代码提审能力被限制 + */ + CODE_86010(86010, "服务商迭代小程序代码提审能力被限制"), + + /** + * 小游戏不能提交 this is game miniprogram, submit audit is forbidden + */ + CODE_87006(87006, "小游戏不能提交"), + + /** + * session_key is not existd or expired + */ + CODE_87007(87007, "session_key is not existd or expired"), + + /** + * invalid sig_method + */ + CODE_87008(87008, "invalid sig_method"), + + /** + * 无效的签名 invalid signature + */ + CODE_87009(87009, "无效的签名"), + + /** + * invalid buffer size + */ + CODE_87010(87010, "invalid buffer size"), + + /** + * 现网已经在灰度发布,不能进行版本回退 wxa has a gray release plan, forbid revert release + */ + CODE_87011(87011, "现网已经在灰度发布,不能进行版本回退"), + + /** + * 该版本不能回退,可能的原因:1:无上一个线上版用于回退 2:此版本为已回退版本,不能回退 3:此版本为回退功能上线之前的版本,不能回退 forbid revert this version release + */ + CODE_87012(87012, "该版本不能回退,可能的原因:1:无上一个线上版用于回退 2:此版本为已回退版本,不能回退 3:此版本为回退功能上线之前的版本,不能回退"), + + /** + * 撤回次数达到上限(每天5次,每个月 10 次) no quota to undo code + */ + CODE_87013(87013, "撤回次数达到上限(每天5次,每个月 10 次)"), + + /** + * risky content + */ + CODE_87014(87014, "risky content"), + + /** + * query timeout, try a content with less size + */ + CODE_87015(87015, "query timeout, try a content with less size"), + + /** + * some key-value in list meet length exceed + */ + CODE_87016(87016, "some key-value in list meet length exceed"), + + /** + * user storage size exceed, delete some keys and try again + */ + CODE_87017(87017, "user storage size exceed, delete some keys and try again"), + + /** + * user has stored too much keys. delete some keys and try again + */ + CODE_87018(87018, "user has stored too much keys. delete some keys and try again"), + + /** + * some keys in list meet length exceed + */ + CODE_87019(87019, "some keys in list meet length exceed"), + + /** + * need friend + */ + CODE_87080(87080, "need friend"), + + /** + * invalid openid + */ + CODE_87081(87081, "invalid openid"), + + /** + * invalid key + */ + CODE_87082(87082, "invalid key"), + + /** + * invalid operation + */ + CODE_87083(87083, "invalid operation"), + + /** + * invalid opnum + */ + CODE_87084(87084, "invalid opnum"), + + /** + * check fail + */ + CODE_87085(87085, "check fail"), + + /** + * without comment privilege + */ + CODE_88000(88000, "without comment privilege"), + + /** + * msg_data is not exists + */ + CODE_88001(88001, "msg_data is not exists"), + + /** + * the article is limit for safety + */ + CODE_88002(88002, "the article is limit for safety"), + + /** + * elected comment upper limit + */ + CODE_88003(88003, "elected comment upper limit"), + + /** + * comment was deleted by user + */ + CODE_88004(88004, "comment was deleted by user"), + + /** + * already reply + */ + CODE_88005(88005, "already reply"), + + /** + * reply content beyond max len or content len is zero + */ + CODE_88007(88007, "reply content beyond max len or content len is zero"), + + /** + * comment is not exists + */ + CODE_88008(88008, "comment is not exists"), + + /** + * reply is not exists + */ + CODE_88009(88009, "reply is not exists"), + + /** + * {@code count range error. cout <= 0 or count > 50} + */ + CODE_88010(88010, "count range error. cout <= 0 or count > 50"), + + /** + * the article is limit for safety + */ + CODE_88011(88011, "the article is limit for safety"), + + /** + * account has bound open,该公众号/小程序已经绑定了开放平台帐号 account has bound open + */ + CODE_89000(89000, "account has bound open,该公众号/小程序已经绑定了开放平台帐号"), + + /** + * not same contractor,Authorizer 与开放平台帐号主体不相同 not same contractor + */ + CODE_89001(89001, "not same contractor,Authorizer 与开放平台帐号主体不相同"), + + /** + * open not exists,该公众号/小程序未绑定微信开放平台帐号。 open not exists + */ + CODE_89002(89002, "open not exists,该公众号/小程序未绑定微信开放平台帐号。"), + + /** + * 该开放平台帐号并非通过 api 创建,不允许操作 open is not created by api + */ + CODE_89003(89003, "该开放平台帐号并非通过 api 创建,不允许操作"), + + /** + * 该开放平台帐号所绑定的公众号/小程序已达上限(100 个) + */ + CODE_89004(89004, "该开放平台帐号所绑定的公众号/小程序已达上限(100 个)"), + + /** + * without add video ability, the ability was banned + */ + CODE_89005(89005, "without add video ability, the ability was banned"), + + /** + * without upload video ability, the ability was banned + */ + CODE_89006(89006, "without upload video ability, the ability was banned"), + + /** + * wxa quota limit + */ + CODE_89007(89007, "wxa quota limit"), + + /** + * overseas account can not link + */ + CODE_89008(89008, "overseas account can not link"), + + /** + * wxa reach link limit + */ + CODE_89009(89009, "wxa reach link limit"), + + /** + * link message has sent + */ + CODE_89010(89010, "link message has sent"), + + /** + * can not unlink nearby wxa + */ + CODE_89011(89011, "can not unlink nearby wxa"), + + /** + * can not unlink store or mall + */ + CODE_89012(89012, "can not unlink store or mall"), + + /** + * wxa is banned + */ + CODE_89013(89013, "wxa is banned"), + + /** + * support version error + */ + CODE_89014(89014, "support version error"), + + /** + * has linked wxa + */ + CODE_89015(89015, "has linked wxa"), + + /** + * reach same realname quota + */ + CODE_89016(89016, "reach same realname quota"), + + /** + * reach different realname quota + */ + CODE_89017(89017, "reach different realname quota"), + + /** + * unlink message has sent + */ + CODE_89018(89018, "unlink message has sent"), + + /** + * 业务域名无更改,无需重复设置 webview domain not change + */ + CODE_89019(89019, "业务域名无更改,无需重复设置"), + + /** + * 尚未设置小程序业务域名,请先在第三方平台中设置小程序业务域名后在调用本接口 open's webview domain is null! Need to set open's webview domain first! + */ + CODE_89020(89020, "尚未设置小程序业务域名,请先在第三方平台中设置小程序业务域名后在调用本接口"), + + /** + * 请求保存的域名不是第三方平台中已设置的小程序业务域名或子域名 request domain is not open's webview domain! + */ + CODE_89021(89021, "请求保存的域名不是第三方平台中已设置的小程序业务域名或子域名"), + + /** + * delete domain is not exist! + */ + CODE_89022(89022, "delete domain is not exist!"), + + /** + * 业务域名数量超过限制,最多可以添加100个业务域名 webview domain exceed limit + */ + CODE_89029(89029, "业务域名数量超过限制,最多可以添加100个业务域名"), + + /** + * operation reach month limit + */ + CODE_89030(89030, "operation reach month limit"), + + /** + * user bind reach limit + */ + CODE_89031(89031, "user bind reach limit"), + + /** + * weapp bind members reach limit + */ + CODE_89032(89032, "weapp bind members reach limit"), + + /** + * empty wx or openid + */ + CODE_89033(89033, "empty wx or openid"), + + /** + * userstr is invalid + */ + CODE_89034(89034, "userstr is invalid"), + + /** + * linking from mp + */ + CODE_89035(89035, "linking from mp"), + + /** + * 个人小程序不支持调用 setwebviewdomain 接口 not support single + */ + CODE_89231(89231, "个人小程序不支持调用 setwebviewdomain 接口"), + + /** + * hit black contractor + */ + CODE_89235(89235, "hit black contractor"), + + /** + * 该插件不能申请 this plugin can not apply + */ + CODE_89236(89236, "该插件不能申请"), + + /** + * 已经添加该插件 plugin has send apply message or already bind + */ + CODE_89237(89237, "已经添加该插件"), + + /** + * 申请或使用的插件已经达到上限 plugin count reach limit + */ + CODE_89238(89238, "申请或使用的插件已经达到上限"), + + /** + * 该插件不存在 plugin no exist + */ + CODE_89239(89239, "该插件不存在"), + + /** + * only applying status can be agreed or refused + */ + CODE_89240(89240, "only applying status can be agreed or refused"), + + /** + * only refused status can be deleted, please refused first + */ + CODE_89241(89241, "only refused status can be deleted, please refused first"), + + /** + * appid is no in the apply list, make sure appid is right + */ + CODE_89242(89242, "appid is no in the apply list, make sure appid is right"), + + /** + * 该申请为“待确认”状态,不可删除 can not delete apply request in 24 hours + */ + CODE_89243(89243, "该申请为“待确认”状态,不可删除"), + + /** + * 不存在该插件 appid plugin appid is no in the plugin list, make sure plugin appid is right + */ + CODE_89244(89244, "不存在该插件 appid"), + + /** + * mini program forbidden to link + */ + CODE_89245(89245, "mini program forbidden to link"), + + /** + * plugins with special category are used only by specific apps + */ + CODE_89246(89246, "plugins with special category are used only by specific apps"), + + /** + * 系统内部错误 inner error, retry after some while + */ + CODE_89247(89247, "系统内部错误"), + + /** + * invalid code type + */ + CODE_89248(89248, "invalid code type"), + + /** + * task running + */ + CODE_89249(89249, "task running"), + + /** + * 内部错误 inner error, retry after some while + */ + CODE_89250(89250, "内部错误"), + + /** + * 模板消息已下发,待法人人脸核身校验 legal person checking + */ + CODE_89251(89251, "模板消息已下发,待法人人脸核身校验"), + + /** + * {@code 法人&企业信息一致性校验中 front checking} + */ + CODE_89253(89253, "法人&企业信息一致性校验中"), + + /** + * lack of some component rights + */ + CODE_89254(89254, "lack of some component rights"), + + /** + * code参数无效,请检查code长度以及内容是否正确;注意code_type的值不同需要传的code长度不一样 enterprise code invalid + */ + CODE_89255(89255, "code参数无效,请检查code长度以及内容是否正确;注意code_type的值不同需要传的code长度不一样"), + + /** + * token 信息有误 no component info + */ + CODE_89256(89256, "token 信息有误"), + + /** + * 该插件版本不支持快速更新 no such version + */ + CODE_89257(89257, "该插件版本不支持快速更新"), + + /** + * 当前小程序帐号存在灰度发布中的版本,不可操作快速更新 code is gray online + */ + CODE_89258(89258, "当前小程序帐号存在灰度发布中的版本,不可操作快速更新"), + + /** + * zhibo plugin is not allow to delete + */ + CODE_89259(89259, "zhibo plugin is not allow to delete"), + + /** + * 订单无效 invalid trade + */ + CODE_89300(89300, "订单无效"), + + /** + * 系统不稳定,请稍后再试,如多次失败请通过社区反馈 + */ + CODE_89401(89401, "系统不稳定,请稍后再试,如多次失败请通过社区反馈"), + + /** + * 该小程序不在待审核队列,请检查是否已提交审核或已审完 + */ + CODE_89402(89402, "该小程序不在待审核队列,请检查是否已提交审核或已审完"), + + /** + * 本单属于平台不支持加急种类,请等待正常审核流程 + */ + CODE_89403(89403, "本单属于平台不支持加急种类,请等待正常审核流程"), + + /** + * 本单已加速成功,请勿重复提交 + */ + CODE_89404(89404, "本单已加速成功,请勿重复提交"), + + /** + * 本月加急额度已用完,请提高提审质量以获取更多额度 + */ + CODE_89405(89405, "本月加急额度已用完,请提高提审质量以获取更多额度"), + + /** + * 公众号有未处理的确认请求,请稍候重试 + */ + CODE_89501(89501, "公众号有未处理的确认请求,请稍候重试"), + + /** + * 请耐心等待管理员确认 + */ + CODE_89502(89502, "请耐心等待管理员确认"), + + /** + * 此次调用需要管理员确认,请耐心等候 + */ + CODE_89503(89503, "此次调用需要管理员确认,请耐心等候"), + + /** + * 正在等管理员确认,请耐心等待 + */ + CODE_89504(89504, "正在等管理员确认,请耐心等待"), + + /** + * 正在等管理员确认,请稍候重试 + */ + CODE_89505(89505, "正在等管理员确认,请稍候重试"), + + /** + * 该IP调用求请求已被公众号管理员拒绝,请24小时后再试,建议调用前与管理员沟通确认 + */ + CODE_89506(89506, "该IP调用求请求已被公众号管理员拒绝,请24小时后再试,建议调用前与管理员沟通确认"), + + /** + * 该IP调用求请求已被公众号管理员拒绝,请1小时后再试,建议调用前与管理员沟通确认 + */ + CODE_89507(89507, "该IP调用求请求已被公众号管理员拒绝,请1小时后再试,建议调用前与管理员沟通确认"), + + /** + * invalid order id + */ + CODE_90001(90001, "invalid order id"), + + /** + * invalid busi id + */ + CODE_90002(90002, "invalid busi id"), + + /** + * invalid bill date + */ + CODE_90003(90003, "invalid bill date"), + + /** + * invalid bill type + */ + CODE_90004(90004, "invalid bill type"), + + /** + * invalid platform + */ + CODE_90005(90005, "invalid platform"), + + /** + * bill not exists + */ + CODE_90006(90006, "bill not exists"), + + /** + * invalid openid + */ + CODE_90007(90007, "invalid openid"), + + /** + * mp_sig error + */ + CODE_90009(90009, "mp_sig error"), + + /** + * no session + */ + CODE_90010(90010, "no session"), + + /** + * sig error + */ + CODE_90011(90011, "sig error"), + + /** + * order exist + */ + CODE_90012(90012, "order exist"), + + /** + * balance not enough + */ + CODE_90013(90013, "balance not enough"), + + /** + * order has been confirmed + */ + CODE_90014(90014, "order has been confirmed"), + + /** + * order has been canceled + */ + CODE_90015(90015, "order has been canceled"), + + /** + * order is being processed + */ + CODE_90016(90016, "order is being processed"), + + /** + * no privilege + */ + CODE_90017(90017, "no privilege"), + + /** + * invalid parameter + */ + CODE_90018(90018, "invalid parameter"), + + /** + * 不是公众号快速创建的小程序 not fast register + */ + CODE_91001(91001, "不是公众号快速创建的小程序"), + + /** + * 小程序发布后不可改名 has published + */ + CODE_91002(91002, "小程序发布后不可改名"), + + /** + * 改名状态不合法 invalid change stat + */ + CODE_91003(91003, "改名状态不合法"), + + /** + * 昵称不合法 invalid nickname + */ + CODE_91004(91004, "昵称不合法"), + + /** + * 昵称 15 天主体保护 nickname protected + */ + CODE_91005(91005, "昵称 15 天主体保护"), + + /** + * 昵称命中微信号 nickname used by username + */ + CODE_91006(91006, "昵称命中微信号"), + + /** + * 昵称已被占用 nickname used + */ + CODE_91007(91007, "昵称已被占用"), + + /** + * 昵称命中 7 天侵权保护期 nickname protected + */ + CODE_91008(91008, "昵称命中 7 天侵权保护期"), + + /** + * 需要提交材料 nickname need proof + */ + CODE_91009(91009, "需要提交材料"), + + /** + * 其他错误 + */ + CODE_91010(91010, "其他错误"), + + /** + * 查不到昵称修改审核单信息 + */ + CODE_91011(91011, "查不到昵称修改审核单信息"), + + /** + * 其它错误 + */ + CODE_91012(91012, "其它错误"), + + /** + * 占用名字过多 lock name too more + */ + CODE_91013(91013, "占用名字过多"), + + /** + * +号规则 同一类型关联名主体不一致 diff master plus + */ + CODE_91014(91014, "+号规则 同一类型关联名主体不一致"), + + /** + * 原始名不同类型主体不一致 diff master + */ + CODE_91015(91015, "原始名不同类型主体不一致"), + + /** + * 名称占用者 ≥2 name more owner + */ + CODE_91016(91016, "名称占用者 ≥2"), + + /** + * +号规则 不同类型关联名主体不一致 other diff master plus + */ + CODE_91017(91017, "+号规则 不同类型关联名主体不一致"), + + /** + * 组织类型小程序发布后,侵权被清空昵称,需走认证改名 + */ + CODE_91018(91018, "组织类型小程序发布后,侵权被清空昵称,需走认证改名"), + + /** + * 小程序正在审核中 + */ + CODE_91019(91019, "小程序正在审核中"), + + /** + * 该经营资质已添加,请勿重复添加 + */ + CODE_92000(92000, "该经营资质已添加,请勿重复添加"), + + /** + * 附近地点添加数量达到上线,无法继续添加 + */ + CODE_92002(92002, "附近地点添加数量达到上线,无法继续添加"), + + /** + * 地点已被其它小程序占用 + */ + CODE_92003(92003, "地点已被其它小程序占用"), + + /** + * 附近功能被封禁 + */ + CODE_92004(92004, "附近功能被封禁"), + + /** + * 地点正在审核中 + */ + CODE_92005(92005, "地点正在审核中"), + + /** + * 地点正在展示小程序 + */ + CODE_92006(92006, "地点正在展示小程序"), + + /** + * 地点审核失败 + */ + CODE_92007(92007, "地点审核失败"), + + /** + * 小程序未展示在该地点 + */ + CODE_92008(92008, "小程序未展示在该地点"), + + /** + * 小程序未上架或不可见 + */ + CODE_93009(93009, "小程序未上架或不可见"), + + /** + * 地点不存在 + */ + CODE_93010(93010, "地点不存在"), + + /** + * 个人类型小程序不可用 + */ + CODE_93011(93011, "个人类型小程序不可用"), + + /** + * 非普通类型小程序(门店小程序、小店小程序等)不可用 + */ + CODE_93012(93012, "非普通类型小程序(门店小程序、小店小程序等)不可用"), + + /** + * 从腾讯地图获取地址详细信息失败 + */ + CODE_93013(93013, "从腾讯地图获取地址详细信息失败"), + + /** + * 同一资质证件号重复添加 + */ + CODE_93014(93014, "同一资质证件号重复添加"), + + /** + * 附近类目审核中 + */ + CODE_93015(93015, "附近类目审核中"), + + /** + * 服务标签个数超限制(官方最多5个,自定义最多4个) + */ + CODE_93016(93016, "服务标签个数超限制(官方最多5个,自定义最多4个)"), + + /** + * 服务标签或者客服的名字不符合要求 + */ + CODE_93017(93017, "服务标签或者客服的名字不符合要求"), + + /** + * 服务能力中填写的小程序appid不是同主体小程序 + */ + CODE_93018(93018, "服务能力中填写的小程序appid不是同主体小程序"), + + /** + * 申请类目之后才能添加附近地点 + */ + CODE_93019(93019, "申请类目之后才能添加附近地点"), + + /** + * qualification_list无效 + */ + CODE_93020(93020, "qualification_list无效"), + + /** + * company_name字段为空 + */ + CODE_93021(93021, "company_name字段为空"), + + /** + * credential字段为空 + */ + CODE_93022(93022, "credential字段为空"), + + /** + * address字段为空 + */ + CODE_93023(93023, "address字段为空"), + + /** + * qualification_list字段为空 + */ + CODE_93024(93024, "qualification_list字段为空"), + + /** + * 服务appid对应的path不存在 + */ + CODE_93025(93025, "服务appid对应的path不存在"), + + /** + * missing cert_serialno + */ + CODE_94001(94001, "missing cert_serialno"), + + /** + * use not register wechat pay + */ + CODE_94002(94002, "use not register wechat pay"), + + /** + * invalid sign + */ + CODE_94003(94003, "invalid sign"), + + /** + * user do not has real name info + */ + CODE_94004(94004, "user do not has real name info"), + + /** + * invalid user token + */ + CODE_94005(94005, "invalid user token"), + + /** + * appid unauthorized + */ + CODE_94006(94006, "appid unauthorized"), + + /** + * appid unbind mchid + */ + CODE_94007(94007, "appid unbind mchid"), + + /** + * invalid timestamp + */ + CODE_94008(94008, "invalid timestamp"), + + /** + * invalid cert_serialno, cert_serialno's size should be 40 + */ + CODE_94009(94009, "invalid cert_serialno, cert_serialno's size should be 40"), + + /** + * invalid mch_id + */ + CODE_94010(94010, "invalid mch_id"), + + /** + * timestamp expired + */ + CODE_94011(94011, "timestamp expired"), + + /** + * appid和商户号的绑定关系不存在 + */ + CODE_94012(94012, "appid和商户号的绑定关系不存在"), + + /** + * wxcode decode fail + */ + CODE_95001(95001, "wxcode decode fail"), + + /** + * wxcode recognize unautuorized + */ + CODE_95002(95002, "wxcode recognize unautuorized"), + + /** + * get product by page args invalid + */ + CODE_95101(95101, "get product by page args invalid"), + + /** + * get product materials by cond args invalid + */ + CODE_95102(95102, "get product materials by cond args invalid"), + + /** + * material id list size out of limit + */ + CODE_95103(95103, "material id list size out of limit"), + + /** + * import product frequence out of limit + */ + CODE_95104(95104, "import product frequence out of limit"), + + /** + * mp is importing products, api is rejected to import + */ + CODE_95105(95105, "mp is importing products, api is rejected to import"), + + /** + * api is rejected to import, need to set commission ratio on mp first + */ + CODE_95106(95106, "api is rejected to import, need to set commission ratio on mp first"), + + /** + * invalid image url + */ + CODE_101000(101000, "无效图片链接"), + + /** + * certificate not found + */ + CODE_101001(101001, "未找到证书"), + + /** + * not enough market quota + */ + CODE_101002(101002, "not enough market quota"), + + /** + * 入参错误 + */ + CODE_200002(200002, "入参错误"), + + /** + * 此账号已被封禁,无法操作 + */ + CODE_200011(200011, "此账号已被封禁,无法操作"), + + /** + * 个人模板数已达上限,上限25个 + */ + CODE_200012(200012, "个人模板数已达上限,上限25个"), + + /** + * 此模板已被封禁,无法选用 + */ + CODE_200013(200013, "此模板已被封禁,无法选用"), + + /** + * 模板 tid 参数错误 + */ + CODE_200014(200014, "模板 tid 参数错误"), + + /** + * start 参数错误 + */ + CODE_200016(200016, "start 参数错误"), + + /** + * limit 参数错误 + */ + CODE_200017(200017, "limit 参数错误"), + + /** + * 类目 ids 缺失 + */ + CODE_200018(200018, "类目 ids 缺失"), + + /** + * 类目 ids 不合法 + */ + CODE_200019(200019, "类目 ids 不合法"), + + /** + * 关键词列表 kidList 参数错误 + */ + CODE_200020(200020, "关键词列表 kidList 参数错误"), + + /** + * 场景描述 sceneDesc 参数错误 + */ + CODE_200021(200021, "场景描述 sceneDesc 参数错误"), + + /** + * {@code 禁止创建/更新商品(如商品创建功能被封禁) 或 禁止编辑&更新房间} + */ + CODE_300001(300001, "禁止创建/更新商品(如商品创建功能被封禁) 或 禁止编辑&更新房间"), + + /** + * 名称长度不符合规则 + */ + CODE_300002(300002, "名称长度不符合规则"), + + /** + * 价格输入不合规(如现价比原价大、传入价格非数字等) + */ + CODE_300003(300003, "价格输入不合规(如现价比原价大、传入价格非数字等)"), + + /** + * 商品名称存在违规违法内容 + */ + CODE_300004(300004, "商品名称存在违规违法内容"), + + /** + * 商品图片存在违规违法内容 + */ + CODE_300005(300005, "商品图片存在违规违法内容"), + + /** + * 图片上传失败(如mediaID过期) + */ + CODE_300006(300006, "图片上传失败(如mediaID过期)"), + + /** + * 线上小程序版本不存在该链接 + */ + CODE_300007(300007, "线上小程序版本不存在该链接"), + + /** + * 添加商品失败 + */ + CODE_300008(300008, "添加商品失败"), + + /** + * 商品审核撤回失败 + */ + CODE_300009(300009, "商品审核撤回失败"), + + /** + * 商品审核状态不对(如商品审核中) + */ + CODE_300010(300010, "商品审核状态不对(如商品审核中)"), + + /** + * 操作非法(API不允许操作非API创建的商品) + */ + CODE_300011(300011, "操作非法(API不允许操作非API创建的商品)"), + + /** + * 没有提审额度(每天500次提审额度) + */ + CODE_300012(300012, "没有提审额度(每天500次提审额度)"), + + /** + * 提审失败 + */ + CODE_300013(300013, "提审失败"), + + /** + * 审核中,无法删除(非零代表失败) + */ + CODE_300014(300014, "审核中,无法删除(非零代表失败)"), + + /** + * 商品未提审 + */ + CODE_300017(300017, "商品未提审"), + + /** + * 商品添加成功,审核失败 + */ + CODE_300021(300021, "商品添加成功,审核失败"), + + /** + * 此房间号不存在 + */ + CODE_300022(300022, "此房间号不存在"), + + /** + * 房间状态 拦截(当前房间状态不允许此操作) + */ + CODE_300023(300023, "房间状态 拦截(当前房间状态不允许此操作)"), + + /** + * 商品不存在 + */ + CODE_300024(300024, "商品不存在"), + + /** + * 商品审核未通过 + */ + CODE_300025(300025, "商品审核未通过"), + + /** + * 房间商品数量已经满额 + */ + CODE_300026(300026, "房间商品数量已经满额"), + + /** + * 导入商品失败 + */ + CODE_300027(300027, "导入商品失败"), + + /** + * 房间名称违规 + */ + CODE_300028(300028, "房间名称违规"), + + /** + * 主播昵称违规 + */ + CODE_300029(300029, "主播昵称违规"), + + /** + * 主播微信号不合法 + */ + CODE_300030(300030, "主播微信号不合法"), + + /** + * 直播间封面图不合规 + */ + CODE_300031(300031, "直播间封面图不合规"), + + /** + * 直播间分享图违规 + */ + CODE_300032(300032, "直播间分享图违规"), + + /** + * 添加商品超过直播间上限 + */ + CODE_300033(300033, "添加商品超过直播间上限"), + + /** + * 主播微信昵称长度不符合要求 + */ + CODE_300034(300034, "主播微信昵称长度不符合要求"), + + /** + * 主播微信号不存在 + */ + CODE_300035(300035, "主播微信号不存在"), + + /** + * 主播微信号未实名认证 + */ + CODE_300036(300036, "主播微信号未实名认证"), + + /** + * invalid file name + */ + CODE_600001(600001, "invalid file name"), + + /** + * no permission to upload file + */ + CODE_600002(600002, "no permission to upload file"), + + /** + * invalid size of source + */ + CODE_600003(600003, "invalid size of source"), + + /** + * 票据已存在 + */ + CODE_928000(928000, "票据已存在"), + + /** + * 票据不存在 + */ + CODE_928001(928001, "票据不存在"), + + /** + * sysem error + */ + CODE_930555(930555, "sysem error"), + + /** + * delivery timeout + */ + CODE_930556(930556, "delivery timeout"), + + /** + * delivery system error + */ + CODE_930557(930557, "delivery system error"), + + /** + * delivery logic error + */ + CODE_930558(930558, "delivery logic error"), + + /** + * 沙盒环境openid无效 invaild openid + */ + CODE_930559(930559, "沙盒环境openid无效"), + + /** + * shopid need bind first + */ + CODE_930560(930560, "shopid need bind first"), + + /** + * 参数错误 args error + */ + CODE_930561(930561, "参数错误"), + + /** + * order already exists + */ + CODE_930562(930562, "order already exists"), + + /** + * 订单不存在 order not exists + */ + CODE_930563(930563, "订单不存在"), + + /** + * 沙盒环境调用无配额 quota run out, try next day + */ + CODE_930564(930564, "沙盒环境调用无配额"), + + /** + * order finished + */ + CODE_930565(930565, "order finished"), + + /** + * not support, plz auth at mp.weixin.qq.com + */ + CODE_930566(930566, "not support, plz auth at mp.weixin.qq.com"), + + /** + * shop arg error + */ + CODE_930567(930567, "shop arg error"), + + /** + * 不支持个人类型小程序 not personal account + */ + CODE_930568(930568, "不支持个人类型小程序"), + + /** + * 已经开通不需要再开通 already open + */ + CODE_930569(930569, "已经开通不需要再开通"), + + /** + * cargo_first_class or cargo_second_class invalid + */ + CODE_930570(930570, "cargo_first_class or cargo_second_class invalid"), + + /** + * 该商户没有内测权限,请先申请权限: https://wj.qq.com/s2/7243532/fcfb/ + */ + CODE_930571(930571, "该商户没有内测权限,请先申请权限: https://wj.qq.com/s2/7243532/fcfb/"), + + /** + * fee already set + */ + CODE_931010(931010, "fee already set"), + + /** + * unbind download url + */ + CODE_6000100(6000100, "unbind download url"), + + /** + * no response data + */ + CODE_6000101(6000101, "no response data"), + + /** + * response data too big + */ + CODE_6000102(6000102, "response data too big"), + + /** + * POST 数据参数不合法 + */ + CODE_9001001(9001001, "POST 数据参数不合法"), + + /** + * 远端服务不可用 + */ + CODE_9001002(9001002, "远端服务不可用"), + + /** + * Ticket 不合法 + */ + CODE_9001003(9001003, "Ticket 不合法"), + + /** + * 获取摇周边用户信息失败 + */ + CODE_9001004(9001004, "获取摇周边用户信息失败"), + + /** + * 获取商户信息失败 + */ + CODE_9001005(9001005, "获取商户信息失败"), + + /** + * 获取 OpenID 失败 + */ + CODE_9001006(9001006, "获取 OpenID 失败"), + + /** + * 上传文件缺失 + */ + CODE_9001007(9001007, "上传文件缺失"), + + /** + * 上传素材的文件类型不合法 + */ + CODE_9001008(9001008, "上传素材的文件类型不合法"), + + /** + * 上传素材的文件尺寸不合法 + */ + CODE_9001009(9001009, "上传素材的文件尺寸不合法"), + + /** + * 上传失败 + */ + CODE_9001010(9001010, "上传失败"), + + /** + * 帐号不合法 + */ + CODE_9001020(9001020, "帐号不合法"), + + /** + * 已有设备激活率低于 50% ,不能新增设备 + */ + CODE_9001021(9001021, "已有设备激活率低于 50% ,不能新增设备"), + + /** + * 设备申请数不合法,必须为大于 0 的数字 + */ + CODE_9001022(9001022, "设备申请数不合法,必须为大于 0 的数字"), + + /** + * 已存在审核中的设备 ID 申请 + */ + CODE_9001023(9001023, "已存在审核中的设备 ID 申请"), + + /** + * 一次查询设备 ID 数量不能超过 50 + */ + CODE_9001024(9001024, "一次查询设备 ID 数量不能超过 50"), + + /** + * 设备 ID 不合法 + */ + CODE_9001025(9001025, "设备 ID 不合法"), + + /** + * 页面 ID 不合法 + */ + CODE_9001026(9001026, "页面 ID 不合法"), + + /** + * 页面参数不合法 + */ + CODE_9001027(9001027, "页面参数不合法"), + + /** + * 一次删除页面 ID 数量不能超过 10 + */ + CODE_9001028(9001028, "一次删除页面 ID 数量不能超过 10"), + + /** + * 页面已应用在设备中,请先解除应用关系再删除 + */ + CODE_9001029(9001029, "页面已应用在设备中,请先解除应用关系再删除"), + + /** + * 一次查询页面 ID 数量不能超过 50 + */ + CODE_9001030(9001030, "一次查询页面 ID 数量不能超过 50"), + + /** + * 时间区间不合法 + */ + CODE_9001031(9001031, "时间区间不合法"), + + /** + * 保存设备与页面的绑定关系参数错误 + */ + CODE_9001032(9001032, "保存设备与页面的绑定关系参数错误"), + + /** + * 门店 ID 不合法 + */ + CODE_9001033(9001033, "门店 ID 不合法"), + + /** + * 设备备注信息过长 + */ + CODE_9001034(9001034, "设备备注信息过长"), + + /** + * 设备申请参数不合法 + */ + CODE_9001035(9001035, "设备申请参数不合法"), + + /** + * 查询起始值 begin 不合法 + */ + CODE_9001036(9001036, "查询起始值 begin 不合法"), + + /** + * params invalid + */ + CODE_9002008(9002008, "params invalid"), + + /** + * shop id not exist + */ + CODE_9002009(9002009, "shop id not exist"), + + /** + * ssid or password should start with "WX" + */ + CODE_9002010(9002010, "ssid or password should start with \"WX\""), + + /** + * ssid can not include chinese + */ + CODE_9002011(9002011, "ssid can not include chinese"), + + /** + * passsword can not include chinese + */ + CODE_9002012(9002012, "passsword can not include chinese"), + + /** + * password must be between 8 and 24 characters + */ + CODE_9002013(9002013, "password must be between 8 and 24 characters"), + + /** + * device exist + */ + CODE_9002016(9002016, "device exist"), + + /** + * device not exist + */ + CODE_9002017(9002017, "device not exist"), + + /** + * the size of query list reach limit + */ + CODE_9002026(9002026, "the size of query list reach limit"), + + /** + * not allowed to modify, ensure you are an certified or component account + */ + CODE_9002041(9002041, "not allowed to modify, ensure you are an certified or component account"), + + /** + * invalid ssid, can not include none utf8 characters, and should be between 1 and 32 bytes + */ + CODE_9002044(9002044, "invalid ssid, can not include none utf8 characters, and should be between 1 and 32 bytes"), + + /** + * shop id has not be audited, this bar type is not enable + */ + CODE_9002052(9002052, "shop id has not be audited, this bar type is not enable"), + + /** + * protocol type is not same with the exist device + */ + CODE_9007003(9007003, "protocol type is not same with the exist device"), + + /** + * ssid not exist + */ + CODE_9007004(9007004, "ssid not exist"), + + /** + * device count limit + */ + CODE_9007005(9007005, "device count limit"), + + /** + * card info not exist + */ + CODE_9008001(9008001, "card info not exist"), + + /** + * card expiration time is invalid + */ + CODE_9008002(9008002, "card expiration time is invalid"), + + /** + * url size invalid, keep less than 255 + */ + CODE_9008003(9008003, "url size invalid, keep less than 255"), + + /** + * url can not include chinese + */ + CODE_9008004(9008004, "url can not include chinese"), + + /** + * order_id not exist + */ + CODE_9200001(9200001, "order_id not exist"), + + /** + * order of other biz + */ + CODE_9200002(9200002, "order of other biz"), + + /** + * blocked + */ + CODE_9200003(9200003, "blocked"), + + /** + * payment notice disabled + */ + CODE_9200211(9200211, "payment notice disabled"), + + /** + * payment notice not exist + */ + CODE_9200231(9200231, "payment notice not exist"), + + /** + * payment notice paid + */ + CODE_9200232(9200232, "payment notice paid"), + + /** + * payment notice canceled + */ + CODE_9200233(9200233, "payment notice canceled"), + + /** + * payment notice expired + */ + CODE_9200235(9200235, "payment notice expired"), + + /** + * bank not allow + */ + CODE_9200236(9200236, "bank not allow"), + + /** + * freq limit + */ + CODE_9200295(9200295, "freq limit"), + + /** + * suspend payment at current time + */ + CODE_9200297(9200297, "suspend payment at current time"), + + /** + * 3rd resp decrypt error + */ + CODE_9200298(9200298, "3rd resp decrypt error"), + + /** + * 3rd resp system error + */ + CODE_9200299(9200299, "3rd resp system error"), + + /** + * 3rd resp sign error + */ + CODE_9200300(9200300, "3rd resp sign error"), + + /** + * desc empty + */ + CODE_9201000(9201000, "desc empty"), + + /** + * fee not equal items' + */ + CODE_9201001(9201001, "fee not equal items'"), + + /** + * payment info incorrect + */ + CODE_9201002(9201002, "payment info incorrect"), + + /** + * fee is 0 + */ + CODE_9201003(9201003, "fee is 0"), + + /** + * payment expire date format error + */ + CODE_9201004(9201004, "payment expire date format error"), + + /** + * appid error + */ + CODE_9201005(9201005, "appid error"), + + /** + * payment order id error + */ + CODE_9201006(9201006, "payment order id error"), + + /** + * openid error + */ + CODE_9201007(9201007, "openid error"), + + /** + * return_url error + */ + CODE_9201008(9201008, "return_url error"), + + /** + * ip error + */ + CODE_9201009(9201009, "ip error"), + + /** + * order_id error + */ + CODE_9201010(9201010, "order_id error"), + + /** + * reason error + */ + CODE_9201011(9201011, "reason error"), + + /** + * mch_id error + */ + CODE_9201012(9201012, "mch_id error"), + + /** + * bill_date error + */ + CODE_9201013(9201013, "bill_date error"), + + /** + * bill_type error + */ + CODE_9201014(9201014, "bill_type error"), + + /** + * trade_type error + */ + CODE_9201015(9201015, "trade_type error"), + + /** + * bank_id error + */ + CODE_9201016(9201016, "bank_id error"), + + /** + * bank_account error + */ + CODE_9201017(9201017, "bank_account error"), + + /** + * payment_notice_no error + */ + CODE_9201018(9201018, "payment_notice_no error"), + + /** + * department_code error + */ + CODE_9201019(9201019, "department_code error"), + + /** + * payment_notice_type error + */ + CODE_9201020(9201020, "payment_notice_type error"), + + /** + * region_code error + */ + CODE_9201021(9201021, "region_code error"), + + /** + * department_name error + */ + CODE_9201022(9201022, "department_name error"), + + /** + * fee not equal finance's + */ + CODE_9201023(9201023, "fee not equal finance's"), + + /** + * refund_out_id error + */ + CODE_9201024(9201024, "refund_out_id error"), + + /** + * not combined order_id + */ + CODE_9201026(9201026, "not combined order_id"), + + /** + * partial sub order is test + */ + CODE_9201027(9201027, "partial sub order is test"), + + /** + * desc too long + */ + CODE_9201029(9201029, "desc too long"), + + /** + * sub order list size error + */ + CODE_9201031(9201031, "sub order list size error"), + + /** + * sub order repeated + */ + CODE_9201032(9201032, "sub order repeated"), + + /** + * auth_code empty + */ + CODE_9201033(9201033, "auth_code empty"), + + /** + * bank_id empty but mch_id not empty + */ + CODE_9201034(9201034, "bank_id empty but mch_id not empty"), + + /** + * sum of other fees exceed total fee + */ + CODE_9201035(9201035, "sum of other fees exceed total fee"), + + /** + * other user paying + */ + CODE_9202000(9202000, "other user paying"), + + /** + * pay process not finish + */ + CODE_9202001(9202001, "pay process not finish"), + + /** + * no refund permission + */ + CODE_9202002(9202002, "no refund permission"), + + /** + * ip limit + */ + CODE_9202003(9202003, "ip limit"), + + /** + * freq limit + */ + CODE_9202004(9202004, "freq limit"), + + /** + * user weixin account abnormal + */ + CODE_9202005(9202005, "user weixin account abnormal"), + + /** + * account balance not enough + */ + CODE_9202006(9202006, "account balance not enough"), + + /** + * refund request repeated + */ + CODE_9202010(9202010, "refund request repeated"), + + /** + * has refunded + */ + CODE_9202011(9202011, "has refunded"), + + /** + * refund exceed total fee + */ + CODE_9202012(9202012, "refund exceed total fee"), + + /** + * busi_id dup + */ + CODE_9202013(9202013, "busi_id dup"), + + /** + * not check sign + */ + CODE_9202016(9202016, "not check sign"), + + /** + * check sign failed + */ + CODE_9202017(9202017, "check sign failed"), + + /** + * sub order error + */ + CODE_9202018(9202018, "sub order error"), + + /** + * order status error + */ + CODE_9202020(9202020, "order status error"), + + /** + * unified order repeatedly + */ + CODE_9202021(9202021, "unified order repeatedly"), + + /** + * request to notification url fail + */ + CODE_9203000(9203000, "request to notification url fail"), + + /** + * http request fail + */ + CODE_9203001(9203001, "http request fail"), + + /** + * http response data error + */ + CODE_9203002(9203002, "http response data error"), + + /** + * http response data RSA decrypt fail + */ + CODE_9203003(9203003, "http response data RSA decrypt fail"), + + /** + * http response data AES decrypt fail + */ + CODE_9203004(9203004, "http response data AES decrypt fail"), + + /** + * system busy, please try again later + */ + CODE_9203999(9203999, "system busy, please try again later"), + + /** + * getrealname token error + */ + CODE_9204000(9204000, "getrealname token error"), + + /** + * getrealname user or token error + */ + CODE_9204001(9204001, "getrealname user or token error"), + + /** + * getrealname appid or token error + */ + CODE_9204002(9204002, "getrealname appid or token error"), + + /** + * finance conf not exist + */ + CODE_9205000(9205000, "finance conf not exist"), + + /** + * bank conf not exist + */ + CODE_9205001(9205001, "bank conf not exist"), + + /** + * wei ban ju conf not exist + */ + CODE_9205002(9205002, "wei ban ju conf not exist"), + + /** + * symmetric key conf not exist + */ + CODE_9205010(9205010, "symmetric key conf not exist"), + + /** + * out order id not exist + */ + CODE_9205101(9205101, "out order id not exist"), + + /** + * bill not exist + */ + CODE_9205201(9205201, "bill not exist"), + + /** + * 3rd resp pay_channel empty + */ + CODE_9206000(9206000, "3rd resp pay_channel empty"), + + /** + * 3rd resp order_id empty + */ + CODE_9206001(9206001, "3rd resp order_id empty"), + + /** + * 3rd resp bill_type_code empty + */ + CODE_9206002(9206002, "3rd resp bill_type_code empty"), + + /** + * 3rd resp bill_no empty + */ + CODE_9206003(9206003, "3rd resp bill_no empty"), + + /** + * 3rd resp empty + */ + CODE_9206200(9206200, "3rd resp empty"), + + /** + * 3rd resp not json + */ + CODE_9206201(9206201, "3rd resp not json"), + + /** + * connect 3rd error + */ + CODE_9206900(9206900, "connect 3rd error"), + + /** + * connect 3rd timeout + */ + CODE_9206901(9206901, "connect 3rd timeout"), + + /** + * read 3rd resp error + */ + CODE_9206910(9206910, "read 3rd resp error"), + + /** + * read 3rd resp timeout + */ + CODE_9206911(9206911, "read 3rd resp timeout"), + + /** + * boss error + */ + CODE_9207000(9207000, "boss error"), + + /** + * wechat pay error + */ + CODE_9207001(9207001, "wechat pay error"), + + /** + * boss param error + */ + CODE_9207002(9207002, "boss param error"), + + /** + * pay error + */ + CODE_9207003(9207003, "pay error"), + + /** + * auth_code expired + */ + CODE_9207004(9207004, "auth_code expired"), + + /** + * user balance not enough + */ + CODE_9207005(9207005, "user balance not enough"), + + /** + * card not support + */ + CODE_9207006(9207006, "card not support"), + + /** + * order reversed + */ + CODE_9207007(9207007, "order reversed"), + + /** + * user paying, need input password + */ + CODE_9207008(9207008, "user paying, need input password"), + + /** + * auth_code error + */ + CODE_9207009(9207009, "auth_code error"), + + /** + * auth_code invalid + */ + CODE_9207010(9207010, "auth_code invalid"), + + /** + * not allow to reverse when user paying + */ + CODE_9207011(9207011, "not allow to reverse when user paying"), + + /** + * order paid + */ + CODE_9207012(9207012, "order paid"), + + /** + * order closed + */ + CODE_9207013(9207013, "order closed"), + + /** + * vehicle not exists + */ + CODE_9207028(9207028, "vehicle not exists"), + + /** + * vehicle request blocked + */ + CODE_9207029(9207029, "vehicle request blocked"), + + /** + * vehicle auth error + */ + CODE_9207030(9207030, "vehicle auth error"), + + /** + * contract over limit + */ + CODE_9207031(9207031, "contract over limit"), + + /** + * trade error + */ + CODE_9207032(9207032, "trade error"), + + /** + * trade time invalid + */ + CODE_9207033(9207033, "trade time invalid"), + + /** + * channel type invalid + */ + CODE_9207034(9207034, "channel type invalid"), + + /** + * expire_time range error + */ + CODE_9207050(9207050, "expire_time range error"), + + /** + * query finance error + */ + CODE_9210000(9210000, "query finance error"), + + /** + * openid error + */ + CODE_9291000(9291000, "openid error"), + + /** + * openid appid not match + */ + CODE_9291001(9291001, "openid appid not match"), + + /** + * app_appid not exist + */ + CODE_9291002(9291002, "app_appid not exist"), + + /** + * app_appid not app + */ + CODE_9291003(9291003, "app_appid not app"), + + /** + * appid empty + */ + CODE_9291004(9291004, "appid empty"), + + /** + * appid not match access_token + */ + CODE_9291005(9291005, "appid not match access_token"), + + /** + * invalid sign + */ + CODE_9291006(9291006, "invalid sign"), + + /** + * backend logic error + */ + CODE_9299999(9299999, "backend logic error"), + + /** + * begin_time can not before now + */ + CODE_9300001(9300001, "begin_time can not before now"), + + /** + * end_time can not before now + */ + CODE_9300002(9300002, "end_time can not before now"), + + /** + * begin_time must less than end_time + */ + CODE_9300003(9300003, "begin_time must less than end_time"), + + /** + * {@code end_time - begin_time > 1year} + */ + CODE_9300004(9300004, "end_time - begin_time > 1year"), + + /** + * invalid max_partic_times + */ + CODE_9300005(9300005, "invalid max_partic_times"), + + /** + * invalid activity status + */ + CODE_9300006(9300006, "invalid activity status"), + + /** + * {@code gift_num must >0 and <=15} + */ + CODE_9300007(9300007, "gift_num must >0 and <=15"), + + /** + * invalid tiny appid + */ + CODE_9300008(9300008, "invalid tiny appid"), + + /** + * activity can not finish + */ + CODE_9300009(9300009, "activity can not finish"), + + /** + * {@code card_info_list must >= 2} + */ + CODE_9300010(9300010, "card_info_list must >= 2"), + + /** + * invalid card_id + */ + CODE_9300011(9300011, "invalid card_id"), + + /** + * card_id must belong this appid + */ + CODE_9300012(9300012, "card_id must belong this appid"), + + /** + * card_id is not swipe_card or pay.cash + */ + CODE_9300013(9300013, "card_id is not swipe_card or pay.cash"), + + /** + * some card_id is out of stock + */ + CODE_9300014(9300014, "some card_id is out of stock"), + + /** + * some card_id is invalid status + */ + CODE_9300015(9300015, "some card_id is invalid status"), + + /** + * membership or new/old tinyapp user only support one + */ + CODE_9300016(9300016, "membership or new/old tinyapp user only support one"), + + /** + * invalid logic for membership + */ + CODE_9300017(9300017, "invalid logic for membership"), + + /** + * invalid logic for tinyapp new/old user + */ + CODE_9300018(9300018, "invalid logic for tinyapp new/old user"), + + /** + * invalid activity type + */ + CODE_9300019(9300019, "invalid activity type"), + + /** + * invalid activity_id + */ + CODE_9300020(9300020, "invalid activity_id"), + + /** + * invalid help_max_times + */ + CODE_9300021(9300021, "invalid help_max_times"), + + /** + * invalid cover_url + */ + CODE_9300022(9300022, "invalid cover_url"), + + /** + * invalid gen_limit + */ + CODE_9300023(9300023, "invalid gen_limit"), + + /** + * card's end_time cannot early than act's end_time + */ + CODE_9300024(9300024, "card's end_time cannot early than act's end_time"), + + /** + * 快递侧逻辑错误,详细原因需要看 delivery_resultcode, 请先确认一下编码方式,python建议 json.dumps(b, ensure_ascii=False),php建议 json_encode($arr, JSON_UNESCAPED_UNICODE) Delivery side error + */ + CODE_9300501(9300501, "快递侧逻辑错误,详细原因需要看 delivery_resultcode, 请先确认一下编码方式,python建议 json.dumps(b, ensure_ascii=False),php建议 json_encode($arr, JSON_UNESCAPED_UNICODE)"), + + /** + * 快递公司系统错误 Delivery side sys error + */ + CODE_9300502(9300502, "快递公司系统错误"), + + /** + * delivery_id 不存在 Specified delivery id is not registerred + */ + CODE_9300503(9300503, "delivery_id 不存在"), + + /** + * service_type 不存在 Specified delivery id has beed banned + */ + CODE_9300504(9300504, "service_type 不存在"), + + /** + * Shop banned + */ + CODE_9300505(9300505, "Shop banned"), + + /** + * 运单 ID 已经存在轨迹,不可取消 Order can't cancel + */ + CODE_9300506(9300506, "运单 ID 已经存在轨迹,不可取消"), + + /** + * Token 不正确 invalid token, can't decryption or decryption result is different from the plaintext + */ + CODE_9300507(9300507, "Token 不正确"), + + /** + * order id has been used + */ + CODE_9300508(9300508, "order id has been used"), + + /** + * speed limit, retry too fast + */ + CODE_9300509(9300509, "speed limit, retry too fast"), + + /** + * invalid service type + */ + CODE_9300510(9300510, "invalid service type"), + + /** + * invalid branch id + */ + CODE_9300511(9300511, "invalid branch id"), + + /** + * 模板格式错误,渲染失败 invalid waybill template format + */ + CODE_9300512(9300512, "模板格式错误,渲染失败"), + + /** + * out of quota + */ + CODE_9300513(9300513, "out of quota"), + + /** + * add net branch fail, try update branch api + */ + CODE_9300514(9300514, "add net branch fail, try update branch api"), + + /** + * wxa appid not exist + */ + CODE_9300515(9300515, "wxa appid not exist"), + + /** + * wxa appid and current bizuin is not linked or not the same owner + */ + CODE_9300516(9300516, "wxa appid and current bizuin is not linked or not the same owner"), + + /** + * update_type 不正确,请使用"bind" 或者“unbind” invalid update_type, please use [bind] or [unbind] + */ + CODE_9300517(9300517, "update_type 不正确,请使用\"bind\" 或者“unbind”"), + + /** + * invalid delivery id + */ + CODE_9300520(9300520, "invalid delivery id"), + + /** + * the orderid is in our system, and waybill is generating + */ + CODE_9300521(9300521, "the orderid is in our system, and waybill is generating"), + + /** + * this orderid is repeated + */ + CODE_9300522(9300522, "this orderid is repeated"), + + /** + * quota is not enough; go to charge please + */ + CODE_9300523(9300523, "quota is not enough; go to charge please"), + + /** + * 订单已取消(一般为重复取消订单) order already canceled + */ + CODE_9300524(9300524, "订单已取消(一般为重复取消订单)"), + + /** + * bizid未绑定 biz id not bind + */ + CODE_9300525(9300525, "bizid未绑定"), + + /** + * 参数字段长度不正确 arg size exceed limit + */ + CODE_9300526(9300526, "参数字段长度不正确"), + + /** + * delivery does not support quota + */ + CODE_9300527(9300527, "delivery does not support quota"), + + /** + * invalid waybill_id + */ + CODE_9300528(9300528, "invalid waybill_id"), + + /** + * 账号已绑定过 biz_id already binded + */ + CODE_9300529(9300529, "账号已绑定过"), + + /** + * 解绑的biz_id不存在 biz_id is not exist + */ + CODE_9300530(9300530, "解绑的biz_id不存在"), + + /** + * bizid无效 或者密码错误 invalid biz_id or password + */ + CODE_9300531(9300531, "bizid无效 或者密码错误"), + + /** + * 绑定已提交,审核中 bind submit, and is checking + */ + CODE_9300532(9300532, "绑定已提交,审核中"), + + /** + * invalid tagid_list + */ + CODE_9300533(9300533, "invalid tagid_list"), + + /** + * add_source=2时,wx_appid和当前小程序不同主体 invalid appid, not same body + */ + CODE_9300534(9300534, "add_source=2时,wx_appid和当前小程序不同主体"), + + /** + * shop字段商品缩略图 url、商品名称为空或者非法,或者商品数量为0 invalid shop arg + */ + CODE_9300535(9300535, "shop字段商品缩略图 url、商品名称为空或者非法,或者商品数量为0"), + + /** + * add_source=2时,wx_appid无效 invalid wxa_appid + */ + CODE_9300536(9300536, "add_source=2时,wx_appid无效"), + + /** + * freq limit + */ + CODE_9300537(9300537, "freq limit"), + + /** + * input task empty + */ + CODE_9300538(9300538, "input task empty"), + + /** + * too many task + */ + CODE_9300539(9300539, "too many task"), + + /** + * task not exist + */ + CODE_9300540(9300540, "task not exist"), + + /** + * delivery callback error + */ + CODE_9300541(9300541, "delivery callback error"), + + /** + * id_card_no is invalid + */ + CODE_9300601(9300601, "id_card_no is invalid"), + + /** + * name is invalid + */ + CODE_9300602(9300602, "name is invalid"), + + /** + * plate_no is invalid + */ + CODE_9300603(9300603, "plate_no is invalid"), + + /** + * auth_key decode error + */ + CODE_9300604(9300604, "auth_key decode error"), + + /** + * auth_key is expired + */ + CODE_9300605(9300605, "auth_key is expired"), + + /** + * auth_key and appinfo not match + */ + CODE_9300606(9300606, "auth_key and appinfo not match"), + + /** + * user not confirm + */ + CODE_9300607(9300607, "user not confirm"), + + /** + * user confirm is expired + */ + CODE_9300608(9300608, "user confirm is expired"), + + /** + * api exceed limit + */ + CODE_9300609(9300609, "api exceed limit"), + + /** + * car license info is invalid + */ + CODE_9300610(9300610, "car license info is invalid"), + + /** + * varification type not support + */ + CODE_9300611(9300611, "varification type not support"), + + /** + * input param error + */ + CODE_9300701(9300701, "input param error"), + + /** + * this code has been used + */ + CODE_9300702(9300702, "this code has been used"), + + /** + * invalid date + */ + CODE_9300703(9300703, "invalid date"), + + /** + * not currently available + */ + CODE_9300704(9300704, "not currently available"), + + /** + * code not exist or expired + */ + CODE_9300705(9300705, "code not exist or expired"), + + /** + * code not exist or expired + */ + CODE_9300706(9300706, "code not exist or expired"), + + /** + * wxpay error + */ + CODE_9300707(9300707, "wxpay error"), + + /** + * wxpay overlimit + */ + CODE_9300708(9300708, "wxpay overlimit"), + + /** + * 无效的微信号 + */ + CODE_9300801(9300801, "无效的微信号"), + + /** + * 服务号未开通导购功能 + */ + CODE_9300802(9300802, "服务号未开通导购功能"), + + /** + * 微信号已经绑定为导购 + */ + CODE_9300803(9300803, "微信号已经绑定为导购"), + + /** + * 该微信号不是导购 + */ + CODE_9300804(9300804, "该微信号不是导购"), + + /** + * 微信号已经被其他账号绑定为导购 + */ + CODE_9300805(9300805, "微信号已经被其他账号绑定为导购"), + + /** + * 粉丝和导购不存在绑定关系 + */ + CODE_9300806(9300806, "粉丝和导购不存在绑定关系"), + + /** + * 标签值无效,不是可选标签值 + */ + CODE_9300807(9300807, "标签值无效,不是可选标签值"), + + /** + * 标签值不存在 + */ + CODE_9300808(9300808, "标签值不存在"), + + /** + * 展示标签值不存在 + */ + CODE_9300809(9300809, "展示标签值不存在"), + + /** + * 导购昵称太长,最多16个字符 + */ + CODE_9300810(9300810, "导购昵称太长,最多16个字符"), + + /** + * 只支持mmbiz.qpic.cn域名的图片 + */ + CODE_9300811(9300811, "只支持mmbiz.qpic.cn域名的图片"), + + /** + * 达到导购绑定个数限制 + */ + CODE_9300812(9300812, "达到导购绑定个数限制"), + + /** + * 达到导购粉丝绑定个数限制 + */ + CODE_9300813(9300813, "达到导购粉丝绑定个数限制"), + + /** + * 敏感词个数超过上限 + */ + CODE_9300814(9300814, "敏感词个数超过上限"), + + /** + * 快捷回复个数超过上限 + */ + CODE_9300815(9300815, "快捷回复个数超过上限"), + + /** + * 文字素材个数超过上限 + */ + CODE_9300816(9300816, "文字素材个数超过上限"), + + /** + * 小程序卡片素材个数超过上限 + */ + CODE_9300817(9300817, "小程序卡片素材个数超过上限"), + + /** + * 图片素材个数超过上限 + */ + CODE_9300818(9300818, "图片素材个数超过上限"), + + /** + * mediaid 有误 + */ + CODE_9300819(9300819, "mediaid 有误"), + + /** + * 可查询标签类别超过上限 + */ + CODE_9300820(9300820, "可查询标签类别超过上限"), + + /** + * 小程序卡片内appid不符合要求 + */ + CODE_9300821(9300821, "小程序卡片内appid不符合要求"), + + /** + * 标签类别的名字无效 + */ + CODE_9300822(9300822, "标签类别的名字无效"), + + /** + * 查询聊天记录时间参数有误 + */ + CODE_9300823(9300823, "查询聊天记录时间参数有误"), + + /** + * 自动回复字数太长 + */ + CODE_9300824(9300824, "自动回复字数太长"), + + /** + * 导购群组id错误 + */ + CODE_9300825(9300825, "导购群组id错误"), + + /** + * 维护中 + */ + CODE_9300826(9300826, "维护中"), + + /** + * invalid parameter + */ + CODE_9301001(9301001, "invalid parameter"), + + /** + * call api service failed + */ + CODE_9301002(9301002, "call api service failed"), + + /** + * internal exception + */ + CODE_9301003(9301003, "internal exception"), + + /** + * save data error + */ + CODE_9301004(9301004, "save data error"), + + /** + * invalid appid + */ + CODE_9301006(9301006, "invalid appid"), + + /** + * invalid api config + */ + CODE_9301007(9301007, "invalid api config"), + + /** + * invalid api info + */ + CODE_9301008(9301008, "invalid api info"), + + /** + * add result check failed + */ + CODE_9301009(9301009, "add result check failed"), + + /** + * consumption failure + */ + CODE_9301010(9301010, "consumption failure"), + + /** + * frequency limit reached + */ + CODE_9301011(9301011, "frequency limit reached"), + + /** + * service timeout + */ + CODE_9301012(9301012, "service timeout"), + + /** + * 该开发小程序已开通小程序直播权限,不支持发布版本。如需发版,请解绑开发小程序后再操作。 + */ + CODE_9400001(9400001, "该开发小程序已开通小程序直播权限,不支持发布版本。如需发版,请解绑开发小程序后再操作。"), + + /** + * 商品已存在 + */ + CODE_9401001(9401001, "商品已存在"), + + /** + * 商品不存在 + */ + CODE_9401002(9401002, "商品不存在"), + + /** + * 类目已存在 + */ + CODE_9401003(9401003, "类目已存在"), + + /** + * 类目不存在 + */ + CODE_9401004(9401004, "类目不存在"), + + /** + * SKU已存在 + */ + CODE_9401005(9401005, "SKU已存在"), + + /** + * SKU不存在 + */ + CODE_9401006(9401006, "SKU不存在"), + + /** + * 属性已存在 + */ + CODE_9401007(9401007, "属性已存在"), + + /** + * 属性不存在 + */ + CODE_9401008(9401008, "属性不存在"), + + /** + * 非法参数 + */ + CODE_9401020(9401020, "非法参数"), + + /** + * 没有商品权限 + */ + CODE_9401021(9401021, "没有商品权限"), + + /** + * SPU NOT ALLOW + */ + CODE_9401022(9401022, "SPU NOT ALLOW"), + + /** + * SPU_NOT_ALLOW_EDIT + */ + CODE_9401023(9401023, "SPU_NOT_ALLOW_EDIT"), + + /** + * SKU NOT ALLOW + */ + CODE_9401024(9401024, "SKU NOT ALLOW"), + + /** + * SKU_NOT_ALLOW_EDIT + */ + CODE_9401025(9401025, "SKU_NOT_ALLOW_EDIT"), + + /** + * limit too large + */ + CODE_9402001(9402001, "limit too large"), + + /** + * single send been blocked + */ + CODE_9402002(9402002, "single send been blocked"), + + /** + * all send been blocked + */ + CODE_9402003(9402003, "all send been blocked"), + + /** + * invalid msg id + */ + CODE_9402004(9402004, "invalid msg id"), + + /** + * send msg too quick + */ + CODE_9402005(9402005, "send msg too quick"), + + /** + * send to single user too quick + */ + CODE_9402006(9402006, "send to single user too quick"), + + /** + * send to all user too quick + */ + CODE_9402007(9402007, "send to all user too quick"), + + /** + * send type error + */ + CODE_9402008(9402008, "send type error"), + + /** + * can not send this msg + */ + CODE_9402009(9402009, "can not send this msg"), + + /** + * content too long or no content + */ + CODE_9402010(9402010, "content too long or no content"), + + /** + * path not exist + */ + CODE_9402011(9402011, "path not exist"), + + /** + * contain evil word + */ + CODE_9402012(9402012, "contain evil word"), + + /** + * path need html suffix + */ + CODE_9402013(9402013, "path need html suffix"), + + /** + * not open to personal body type + */ + CODE_9402014(9402014, "not open to personal body type"), + + /** + * not open to violation body type + */ + CODE_9402015(9402015, "not open to violation body type"), + + /** + * not open to low quality provider + */ + CODE_9402016(9402016, "not open to low quality provider"), + + /** + * invalid product_id + */ + CODE_9402101(9402101, "invalid product_id"), + + /** + * device_id count more than limit + */ + CODE_9402102(9402102, "device_id count more than limit"), + + /** + * 请勿频繁提交,待上一次操作完成后再提交 concurrent limit + */ + CODE_9402202(9402202, "请勿频繁提交,待上一次操作完成后再提交"), + + /** + * user not book this ad id + */ + CODE_9402301(9402301, "user not book this ad id"), + + /** + * 消息类型错误! + */ + CODE_9403000(9403000, "消息类型错误!"), + + /** + * 消息字段的内容过长! + */ + CODE_9403001(9403001, "消息字段的内容过长!"), + + /** + * 消息字段的内容违规! + */ + CODE_9403002(9403002, "消息字段的内容违规!"), + + /** + * 发送的微信号太多! + */ + CODE_9403003(9403003, "发送的微信号太多!"), + + /** + * 存在错误的微信号! + */ + CODE_9403004(9403004, "存在错误的微信号!"), + + /** + * 直播间列表为空 live room not exsits + */ + CODE_9410000(9410000, "直播间列表为空"), + + /** + * 获取房间失败 inner error: get room fail + */ + CODE_9410001(9410001, "获取房间失败"), + + /** + * 获取商品失败 inner error: get goods fail + */ + CODE_9410002(9410002, "获取商品失败"), + + /** + * 获取回放失败 inner error: get replay url fail + */ + CODE_9410003(9410003, "获取回放失败"); + + + private final int code; + private final String msg; + + WxOpenErrorMsgEnum(int code, String msg) { + this.code = code; + this.msg = msg; + } + + static final Map valueMap = Maps.newHashMap(); + + static { + for (WxOpenErrorMsgEnum value : WxOpenErrorMsgEnum.values()) { + valueMap.put(value.code, value.msg); + } + } + + /** + * 通过错误代码查找其中文含义. + */ + public static String findMsgByCode(int code) { + return valueMap.getOrDefault(code, null); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxRuntimeException.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxRuntimeException.java new file mode 100644 index 0000000000..e94e03db5d --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxRuntimeException.java @@ -0,0 +1,23 @@ +package me.chanjar.weixin.common.error; + +/** + * WxJava专用的runtime exception. + * + * @author Binary Wang + * created on 2020-09-26 + */ +public class WxRuntimeException extends RuntimeException { + private static final long serialVersionUID = 4881698471192264412L; + + public WxRuntimeException(Throwable e) { + super(e); + } + + public WxRuntimeException(String msg) { + super(msg); + } + + public WxRuntimeException(String msg, Throwable e) { + super(msg, e); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutor.java new file mode 100644 index 0000000000..a93cbe1e99 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutor.java @@ -0,0 +1,58 @@ +package me.chanjar.weixin.common.executor; + +import jodd.http.HttpConnectionProvider; +import jodd.http.ProxyInfo; +import me.chanjar.weixin.common.bean.CommonUploadParam; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.RequestExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; +import me.chanjar.weixin.common.util.http.ResponseHandler; +import me.chanjar.weixin.common.util.http.okhttp.OkHttpProxyInfo; +import okhttp3.OkHttpClient; + +import java.io.IOException; + +/** + * 通用文件上传执行器 + * + * @author 广州跨界 + * created on 2024/01/11 + */ +public abstract class CommonUploadRequestExecutor implements RequestExecutor { + + protected RequestHttp requestHttp; + + public CommonUploadRequestExecutor(RequestHttp requestHttp) { + this.requestHttp = requestHttp; + } + + @Override + public void execute(String uri, CommonUploadParam data, ResponseHandler handler, WxType wxType) throws WxErrorException, IOException { + handler.handle(this.execute(uri, data, wxType)); + } + + /** + * 构造通用文件上传执行器 + * + * @param requestHttp 请求信息 + * @return 执行器 + */ + @SuppressWarnings("unchecked") + public static RequestExecutor create(RequestHttp requestHttp) { + switch (requestHttp.getRequestType()) { + case APACHE_HTTP: + return new CommonUploadRequestExecutorApacheImpl( + (RequestHttp) requestHttp); + case JODD_HTTP: + return new CommonUploadRequestExecutorJoddHttpImpl((RequestHttp) requestHttp); + case OK_HTTP: + return new CommonUploadRequestExecutorOkHttpImpl((RequestHttp) requestHttp); + case HTTP_COMPONENTS: + return new CommonUploadRequestExecutorHttpComponentsImpl( + (RequestHttp) requestHttp); + default: + throw new IllegalArgumentException("不支持的http执行器类型:" + requestHttp.getRequestType()); + } + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorApacheImpl.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorApacheImpl.java new file mode 100644 index 0000000000..dba92e27da --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorApacheImpl.java @@ -0,0 +1,86 @@ +package me.chanjar.weixin.common.executor; + +import lombok.Getter; +import me.chanjar.weixin.common.bean.CommonUploadData; +import me.chanjar.weixin.common.bean.CommonUploadParam; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.RequestHttp; +import me.chanjar.weixin.common.util.http.apache.Utf8ResponseHandler; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.entity.mime.content.InputStreamBody; +import org.apache.http.impl.client.CloseableHttpClient; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Apache HttpClient 通用文件上传器 + * + * @author 广州跨界 + * created on 2024/01/11 + */ +public class CommonUploadRequestExecutorApacheImpl extends CommonUploadRequestExecutor { + + public CommonUploadRequestExecutorApacheImpl(RequestHttp requestHttp) { + super(requestHttp); + } + + @Override + public String execute(String uri, CommonUploadParam param, WxType wxType) throws WxErrorException, IOException { + HttpPost httpPost = new HttpPost(uri); + if (requestHttp.getRequestHttpProxy() != null) { + RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build(); + httpPost.setConfig(config); + } + if (param != null) { + CommonUploadData data = param.getData(); + InnerStreamBody part = new InnerStreamBody(data.getInputStream(), ContentType.DEFAULT_BINARY, data.getFileName(), data.getLength()); + MultipartEntityBuilder entityBuilder = MultipartEntityBuilder + .create() + .addPart(param.getName(), part) + .setMode(HttpMultipartMode.RFC6532); + + // 添加额外的表单字段 + if (param.getFormFields() != null && !param.getFormFields().isEmpty()) { + for (java.util.Map.Entry entry : param.getFormFields().entrySet()) { + entityBuilder.addTextBody(entry.getKey(), entry.getValue(), ContentType.TEXT_PLAIN.withCharset("UTF-8")); + } + } + + HttpEntity entity = entityBuilder.build(); + httpPost.setEntity(entity); + } + String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE); + if (StringUtils.isEmpty(responseContent)) { + throw new WxErrorException(String.format("上传失败,服务器响应空 url:%s param:%s", uri, param)); + } + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + return responseContent; + } + + /** + * 内部流 请求体 + */ + @Getter + public static class InnerStreamBody extends InputStreamBody { + + private final long contentLength; + + public InnerStreamBody(final InputStream in, final ContentType contentType, final String filename, long contentLength) { + super(in, contentType, filename); + this.contentLength = contentLength; + } + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorHttpComponentsImpl.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorHttpComponentsImpl.java new file mode 100644 index 0000000000..f79e4cd96f --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorHttpComponentsImpl.java @@ -0,0 +1,83 @@ +package me.chanjar.weixin.common.executor; + +import lombok.Getter; +import me.chanjar.weixin.common.bean.CommonUploadData; +import me.chanjar.weixin.common.bean.CommonUploadParam; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.RequestHttp; +import me.chanjar.weixin.common.util.http.hc.Utf8ResponseHandler; +import org.apache.commons.lang3.StringUtils; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.entity.mime.HttpMultipartMode; +import org.apache.hc.client5.http.entity.mime.InputStreamBody; +import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHost; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Apache HttpComponents 通用文件上传器 + */ +public class CommonUploadRequestExecutorHttpComponentsImpl extends CommonUploadRequestExecutor { + + public CommonUploadRequestExecutorHttpComponentsImpl(RequestHttp requestHttp) { + super(requestHttp); + } + + @Override + public String execute(String uri, CommonUploadParam param, WxType wxType) throws WxErrorException, IOException { + HttpPost httpPost = new HttpPost(uri); + if (requestHttp.getRequestHttpProxy() != null) { + RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build(); + httpPost.setConfig(config); + } + if (param != null) { + CommonUploadData data = param.getData(); + InnerStreamBody part = new InnerStreamBody(data.getInputStream(), ContentType.DEFAULT_BINARY, data.getFileName(), data.getLength()); + MultipartEntityBuilder entityBuilder = MultipartEntityBuilder + .create() + .addPart(param.getName(), part) + .setMode(HttpMultipartMode.EXTENDED); + + // 添加额外的表单字段 + if (param.getFormFields() != null && !param.getFormFields().isEmpty()) { + for (java.util.Map.Entry entry : param.getFormFields().entrySet()) { + entityBuilder.addTextBody(entry.getKey(), entry.getValue(), ContentType.TEXT_PLAIN.withCharset("UTF-8")); + } + } + + HttpEntity entity = entityBuilder.build(); + httpPost.setEntity(entity); + } + String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE); + if (StringUtils.isEmpty(responseContent)) { + throw new WxErrorException(String.format("上传失败,服务器响应空 url:%s param:%s", uri, param)); + } + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + return responseContent; + } + + /** + * 内部流 请求体 + */ + @Getter + public static class InnerStreamBody extends InputStreamBody { + + private final long contentLength; + + public InnerStreamBody(final InputStream in, final ContentType contentType, final String filename, long contentLength) { + super(in, contentType, filename); + this.contentLength = contentLength; + } + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorJoddHttpImpl.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorJoddHttpImpl.java new file mode 100644 index 0000000000..182820d076 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorJoddHttpImpl.java @@ -0,0 +1,99 @@ +package me.chanjar.weixin.common.executor; + +import jodd.http.HttpConnectionProvider; +import jodd.http.HttpRequest; +import jodd.http.HttpResponse; +import jodd.http.ProxyInfo; +import jodd.http.upload.Uploadable; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.SneakyThrows; +import me.chanjar.weixin.common.bean.CommonUploadData; +import me.chanjar.weixin.common.bean.CommonUploadParam; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.RequestHttp; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** + * JoddHttp 通用文件上传器 + * + * @author 广州跨界 + * created on 2024/01/11 + */ +public class CommonUploadRequestExecutorJoddHttpImpl extends CommonUploadRequestExecutor { + + public CommonUploadRequestExecutorJoddHttpImpl(RequestHttp requestHttp) { + super(requestHttp); + } + + @Override + public String execute(String uri, CommonUploadParam param, WxType wxType) throws WxErrorException, IOException { + HttpRequest request = HttpRequest.post(uri); + if (requestHttp.getRequestHttpProxy() != null) { + requestHttp.getRequestHttpClient().useProxy(requestHttp.getRequestHttpProxy()); + } + request.withConnectionProvider(requestHttp.getRequestHttpClient()); + request.form(param.getName(), new CommonUploadParamToUploadableAdapter(param.getData())); + + // 添加额外的表单字段 + if (param.getFormFields() != null && !param.getFormFields().isEmpty()) { + for (java.util.Map.Entry entry : param.getFormFields().entrySet()) { + request.form(entry.getKey(), entry.getValue()); + } + } + + HttpResponse response = request.send(); + response.charset(StandardCharsets.UTF_8.name()); + String responseContent = response.bodyText(); + if (responseContent.isEmpty()) { + throw new WxErrorException(String.format("上传失败,服务器响应空 url:%s param:%s", uri, param)); + } + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + return responseContent; + } + + /** + * 通用上传参数 到 Uploadable 的适配器 + */ + @Getter + @AllArgsConstructor + public static class CommonUploadParamToUploadableAdapter implements Uploadable { + + private CommonUploadData content; + + @SneakyThrows + @Override + public byte[] getBytes() { + return content.readAllBytes(); + } + + @Override + public String getFileName() { + return content.getFileName(); + } + + @Override + public String getMimeType() { + return null; + } + + @SneakyThrows + @Override + public int getSize() { + return (int) content.getLength(); + } + + @Override + public InputStream openInputStream() { + return content.getInputStream(); + } + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorOkHttpImpl.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorOkHttpImpl.java new file mode 100644 index 0000000000..6a0343980f --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorOkHttpImpl.java @@ -0,0 +1,99 @@ +package me.chanjar.weixin.common.executor; + +import lombok.AllArgsConstructor; +import me.chanjar.weixin.common.bean.CommonUploadData; +import me.chanjar.weixin.common.bean.CommonUploadParam; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.RequestHttp; +import me.chanjar.weixin.common.util.http.okhttp.OkHttpProxyInfo; +import okhttp3.*; +import okio.BufferedSink; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.InputStream; + +/** + * OkHttp 通用文件上传器 + * + * @author 广州跨界 + * created on 2024/01/11 + */ +public class CommonUploadRequestExecutorOkHttpImpl extends CommonUploadRequestExecutor { + + public CommonUploadRequestExecutorOkHttpImpl(RequestHttp requestHttp) { + super(requestHttp); + } + + @Override + public String execute(String uri, CommonUploadParam param, WxType wxType) throws WxErrorException, IOException { + RequestBody requestBody = new CommonUpdateDataToRequestBodyAdapter(param.getData()); + MultipartBody.Builder bodyBuilder = new MultipartBody.Builder() + .setType(MediaType.get("multipart/form-data")) + .addFormDataPart(param.getName(), param.getData().getFileName(), requestBody); + + // 添加额外的表单字段 + if (param.getFormFields() != null && !param.getFormFields().isEmpty()) { + for (java.util.Map.Entry entry : param.getFormFields().entrySet()) { + bodyBuilder.addFormDataPart(entry.getKey(), entry.getValue()); + } + } + + RequestBody body = bodyBuilder.build(); + Request request = new Request.Builder().url(uri).post(body).build(); + + try (Response response = requestHttp.getRequestHttpClient().newCall(request).execute()) { + ResponseBody responseBody = response.body(); + String responseContent = responseBody == null ? "" : responseBody.string(); + if (responseContent.isEmpty()) { + throw new WxErrorException(String.format("上传失败,服务器响应空 url:%s param:%s", uri, param)); + } + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + return responseContent; + } + } + + /** + * 通用上传输入 到 OkHttp 请求提 适配器 + */ + @AllArgsConstructor + public static class CommonUpdateDataToRequestBodyAdapter extends RequestBody { + + private static final MediaType CONTENT_TYPE = MediaType.get("application/octet-stream"); + + private CommonUploadData data; + + @Override + public long contentLength() { + return data.getLength(); + } + + @Nullable + @Override + public MediaType contentType() { + return CONTENT_TYPE; + } + + @Override + public void writeTo(@NotNull BufferedSink bufferedSink) throws IOException { + InputStream inputStream = data.getInputStream(); + int count; + byte[] buffer = new byte[4096]; + while ((count = inputStream.read(buffer)) != -1) { + bufferedSink.write(buffer, 0, count); + } + inputStream.close(); + } + + @Override + public boolean isOneShot() { + return true; + } + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/BaseWxRedisOps.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/BaseWxRedisOps.java new file mode 100644 index 0000000000..17e992ab25 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/BaseWxRedisOps.java @@ -0,0 +1,37 @@ +package me.chanjar.weixin.common.redis; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; + +/** + * 微信redis操作基本类 + *

+ * 非内置实现redis相关操作, 请实现该类 + */ +public abstract class BaseWxRedisOps implements WxRedisOps { + + @Override + public String getValue(String key) { + throw new UnsupportedOperationException(); + } + + @Override + public void setValue(String key, String value, int expire, TimeUnit timeUnit) { + throw new UnsupportedOperationException(); + } + + @Override + public Long getExpire(String key) { + throw new UnsupportedOperationException(); + } + + @Override + public void expire(String key, int expire, TimeUnit timeUnit) { + throw new UnsupportedOperationException(); + } + + @Override + public Lock getLock(String key) { + throw new UnsupportedOperationException(); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/JedisWxRedisOps.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/JedisWxRedisOps.java new file mode 100644 index 0000000000..e6e3f28d7e --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/JedisWxRedisOps.java @@ -0,0 +1,58 @@ +package me.chanjar.weixin.common.redis; + +import com.github.jedis.lock.JedisLock; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.util.locks.JedisDistributedLock; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.util.Pool; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; + +/** + * @author Mario Luo + * + * @deprecated 该类底层使用过时的8年不维护的 {@link JedisLock}组件,不可靠,不建议继续使用。请 + * 使用:{@link RedisTemplateWxRedisOps}、{@link RedissonWxRedisOps} 替换 + */ +@RequiredArgsConstructor +public class JedisWxRedisOps implements WxRedisOps { + private final Pool jedisPool; + + @Override + public String getValue(String key) { + try (Jedis jedis = this.jedisPool.getResource()) { + return jedis.get(key); + } + } + + @Override + public void setValue(String key, String value, int expire, TimeUnit timeUnit) { + try (Jedis jedis = this.jedisPool.getResource()) { + if (expire <= 0) { + jedis.set(key, value); + } else { + jedis.psetex(key, timeUnit.toMillis(expire), value); + } + } + } + + @Override + public Long getExpire(String key) { + try (Jedis jedis = this.jedisPool.getResource()) { + return jedis.ttl(key); + } + } + + @Override + public void expire(String key, int expire, TimeUnit timeUnit) { + try (Jedis jedis = this.jedisPool.getResource()) { + jedis.pexpire(key, timeUnit.toMillis(expire)); + } + } + + @Override + public Lock getLock(String key) { + return new JedisDistributedLock(jedisPool, key); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOps.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOps.java new file mode 100644 index 0000000000..d531a2a307 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOps.java @@ -0,0 +1,44 @@ +package me.chanjar.weixin.common.redis; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.util.locks.RedisTemplateSimpleDistributedLock; +import org.springframework.data.redis.core.StringRedisTemplate; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; + +@RequiredArgsConstructor +public class RedisTemplateWxRedisOps implements WxRedisOps { + + private final StringRedisTemplate redisTemplate; + + @Override + public String getValue(String key) { + return redisTemplate.opsForValue().get(key); + } + + @Override + public void setValue(String key, String value, int expire, TimeUnit timeUnit) { + if (expire <= 0) { + redisTemplate.opsForValue().set(key, value); + } else { + redisTemplate.opsForValue().set(key, value, expire, timeUnit); + } + } + + @Override + public Long getExpire(String key) { + return redisTemplate.getExpire(key, TimeUnit.SECONDS); + } + + @Override + public void expire(String key, int expire, TimeUnit timeUnit) { + redisTemplate.expire(key, expire, timeUnit); + } + + @Override + public Lock getLock(@NonNull String key) { + return new RedisTemplateSimpleDistributedLock(redisTemplate, key, 60 * 1000); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedissonWxRedisOps.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedissonWxRedisOps.java new file mode 100644 index 0000000000..d51cd3e1ad --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/RedissonWxRedisOps.java @@ -0,0 +1,47 @@ +package me.chanjar.weixin.common.redis; + +import lombok.RequiredArgsConstructor; +import org.redisson.api.RedissonClient; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; + +@RequiredArgsConstructor +public class RedissonWxRedisOps implements WxRedisOps { + + private final RedissonClient redissonClient; + + @Override + public String getValue(String key) { + Object value = redissonClient.getBucket(key).get(); + return value == null ? null : value.toString(); + } + + @Override + public void setValue(String key, String value, int expire, TimeUnit timeUnit) { + if (expire <= 0) { + redissonClient.getBucket(key).set(value); + } else { + redissonClient.getBucket(key).set(value, expire, timeUnit); + } + } + + @Override + public Long getExpire(String key) { + long expire = redissonClient.getBucket(key).remainTimeToLive(); + if (expire > 0) { + expire = expire / 1000; + } + return expire; + } + + @Override + public void expire(String key, int expire, TimeUnit timeUnit) { + redissonClient.getBucket(key).expire(expire, timeUnit); + } + + @Override + public Lock getLock(String key) { + return redissonClient.getLock(key); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/WxRedisOps.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/WxRedisOps.java new file mode 100644 index 0000000000..4912cbc5d8 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/WxRedisOps.java @@ -0,0 +1,28 @@ +package me.chanjar.weixin.common.redis; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; + +/** + * 微信Redis相关操作 + *

+ * 该接口不承诺稳定, 外部实现请继承{@link BaseWxRedisOps} + * + * @author Mario Luo + * @see BaseWxRedisOps 实现需要继承该类 + * @see JedisWxRedisOps jedis实现 + * @see RedissonWxRedisOps redisson实现 + * @see RedisTemplateWxRedisOps redisTemplate实现 + */ +public interface WxRedisOps { + + String getValue(String key); + + void setValue(String key, String value, int expire, TimeUnit timeUnit); + + Long getExpire(String key); + + void expire(String key, int expire, TimeUnit timeUnit); + + Lock getLock(String key); +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/requestexecuter/ocr/OcrDiscernApacheHttpRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/requestexecuter/ocr/OcrDiscernApacheHttpRequestExecutor.java new file mode 100644 index 0000000000..03bec013dd --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/requestexecuter/ocr/OcrDiscernApacheHttpRequestExecutor.java @@ -0,0 +1,52 @@ +package me.chanjar.weixin.common.requestexecuter.ocr; + +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.RequestHttp; +import me.chanjar.weixin.common.util.http.apache.Utf8ResponseHandler; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.impl.client.CloseableHttpClient; + +import java.io.File; +import java.io.IOException; + +/** + * . + * + * @author : zhayueran + * created on 2019/6/27 14:06 + */ +public class OcrDiscernApacheHttpRequestExecutor extends OcrDiscernRequestExecutor { + public OcrDiscernApacheHttpRequestExecutor(RequestHttp requestHttp) { + super(requestHttp); + } + + @Override + public String execute(String uri, File file, WxType wxType) throws WxErrorException, IOException { + HttpPost httpPost = new HttpPost(uri); + if (requestHttp.getRequestHttpProxy() != null) { + RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build(); + httpPost.setConfig(config); + } + if (file != null) { + HttpEntity entity = MultipartEntityBuilder + .create() + .addBinaryBody("file", file) + .setMode(HttpMultipartMode.RFC6532) + .build(); + httpPost.setEntity(entity); + } + String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE); + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + return responseContent; + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/requestexecuter/ocr/OcrDiscernHttpComponentsRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/requestexecuter/ocr/OcrDiscernHttpComponentsRequestExecutor.java new file mode 100644 index 0000000000..2d02c965a8 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/requestexecuter/ocr/OcrDiscernHttpComponentsRequestExecutor.java @@ -0,0 +1,46 @@ +package me.chanjar.weixin.common.requestexecuter.ocr; + +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.RequestHttp; +import me.chanjar.weixin.common.util.http.hc.Utf8ResponseHandler; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.entity.mime.HttpMultipartMode; +import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHost; + +import java.io.File; +import java.io.IOException; + +public class OcrDiscernHttpComponentsRequestExecutor extends OcrDiscernRequestExecutor { + public OcrDiscernHttpComponentsRequestExecutor(RequestHttp requestHttp) { + super(requestHttp); + } + + @Override + public String execute(String uri, File file, WxType wxType) throws WxErrorException, IOException { + HttpPost httpPost = new HttpPost(uri); + if (requestHttp.getRequestHttpProxy() != null) { + RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build(); + httpPost.setConfig(config); + } + if (file != null) { + HttpEntity entity = MultipartEntityBuilder + .create() + .addBinaryBody("file", file) + .setMode(HttpMultipartMode.EXTENDED) + .build(); + httpPost.setEntity(entity); + } + String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE); + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + return responseContent; + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/requestexecuter/ocr/OcrDiscernRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/requestexecuter/ocr/OcrDiscernRequestExecutor.java new file mode 100644 index 0000000000..542ab4a378 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/requestexecuter/ocr/OcrDiscernRequestExecutor.java @@ -0,0 +1,45 @@ +package me.chanjar.weixin.common.requestexecuter.ocr; + +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.RequestExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; +import me.chanjar.weixin.common.util.http.ResponseHandler; +import org.apache.http.HttpHost; +import org.apache.http.impl.client.CloseableHttpClient; + +import java.io.File; +import java.io.IOException; + +/** + * . + * + * @author zhayueran + * created on 2019/6/27 15:06 + */ +public abstract class OcrDiscernRequestExecutor implements RequestExecutor { + protected RequestHttp requestHttp; + + public OcrDiscernRequestExecutor(RequestHttp requestHttp) { + this.requestHttp = requestHttp; + } + + @Override + public void execute(String uri, File data, ResponseHandler handler, WxType wxType) throws WxErrorException, IOException { + handler.handle(this.execute(uri, data, wxType)); + } + + @SuppressWarnings("unchecked") + public static RequestExecutor create(RequestHttp requestHttp) { + switch (requestHttp.getRequestType()) { + case APACHE_HTTP: + return new OcrDiscernApacheHttpRequestExecutor( + (RequestHttp) requestHttp); + case HTTP_COMPONENTS: + return new OcrDiscernHttpComponentsRequestExecutor( + (RequestHttp) requestHttp); + default: + throw new IllegalArgumentException("不支持的http执行器类型:" + requestHttp.getRequestType()); + } + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxImgProcService.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxImgProcService.java new file mode 100644 index 0000000000..a9ef694ad5 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxImgProcService.java @@ -0,0 +1,121 @@ +package me.chanjar.weixin.common.service; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.bean.imgproc.WxImgProcAiCropResult; +import me.chanjar.weixin.common.bean.imgproc.WxImgProcQrCodeResult; +import me.chanjar.weixin.common.bean.imgproc.WxImgProcSuperResolutionResult; + +import java.io.File; + +/** + * 多项图像处理能力相关的API. + * https://developers.weixin.qq.com/doc/offiaccount/Intelligent_Interface/Img_Proc.html + * + * @author Theo Nie + */ +public interface WxImgProcService { + + /** + * 二维码/条码识别接口 + * 说明: + * 1.图片支持使用img参数实时上传,也支持使用img_url参数传送图片地址,由微信后台下载图片进行识别 + * 2.文件大小限制:小于2M + * 3.支持条码、二维码、DataMatrix和PDF417的识别。 + * 4.二维码、DataMatrix会返回位置坐标,条码和PDF417暂不返回位置坐标。 + * + * @param imgUrl 图片url地址 + * @return WxMpImgProcQrCodeResult + * @throws WxErrorException . + */ + WxImgProcQrCodeResult qrCode(String imgUrl) throws WxErrorException; + + /** + * 二维码/条码识别接口 + * 说明: + * 1.图片支持使用img参数实时上传,也支持使用img_url参数传送图片地址,由微信后台下载图片进行识别 + * 2.文件大小限制:小于2M + * 3.支持条码、二维码、DataMatrix和PDF417的识别。 + * 4.二维码、DataMatrix会返回位置坐标,条码和PDF417暂不返回位置坐标。 + * + * @param imgFile 图片文件对象 + * @return WxMpImgProcQrCodeResult + * @throws WxErrorException . + */ + WxImgProcQrCodeResult qrCode(File imgFile) throws WxErrorException; + + /** + * 图片高清化接口 + * 说明: + * 1.图片支持使用img参数实时上传,也支持使用img_url参数传送图片地址,由微信后台下载图片进行识别 + * 2.文件大小限制:小于2M + * 3.目前支持将图片超分辨率高清化2倍,即生成图片分辨率为原图2倍大小 + * 返回的media_id有效期为3天,期间可以通过“获取临时素材”接口获取图片二进制 + * + * @param imgUrl 图片url地址 + * @return WxMpImgProcSuperResolutionResult + * @throws WxErrorException . + */ + WxImgProcSuperResolutionResult superResolution(String imgUrl) throws WxErrorException; + + /** + * 图片高清化接口 + * 说明: + * 1.图片支持使用img参数实时上传,也支持使用img_url参数传送图片地址,由微信后台下载图片进行识别 + * 2.文件大小限制:小于2M + * 3.目前支持将图片超分辨率高清化2倍,即生成图片分辨率为原图2倍大小 + * 返回的media_id有效期为3天,期间可以通过“获取临时素材”接口获取图片二进制 + * + * @param imgFile 图片文件对象 + * @return WxMpImgProcSuperResolutionResult + * @throws WxErrorException . + */ + WxImgProcSuperResolutionResult superResolution(File imgFile) throws WxErrorException; + + /** + * 图片智能裁剪接口 + * 说明: + * 1.图片支持使用img参数实时上传,也支持使用img_url参数传送图片地址,由微信后台下载图片进行识别 + * 2.文件大小限制:小于2M + * 3.该接口默认使用最佳宽高比 + * @param imgUrl 图片url地址 + * @return WxMpImgProcAiCropResult + * @throws WxErrorException . + */ + WxImgProcAiCropResult aiCrop(String imgUrl) throws WxErrorException; + + /** + * 图片智能裁剪接口 + * 说明: + * 1.图片支持使用img参数实时上传,也支持使用img_url参数传送图片地址,由微信后台下载图片进行识别 + * 2.文件大小限制:小于2M + * @param imgUrl 图片url地址 + * @param ratios 宽高比,最多支持5个,请以英文逗号分隔 + * @return WxMpImgProcAiCropResult + * @throws WxErrorException . + */ + WxImgProcAiCropResult aiCrop(String imgUrl, String ratios) throws WxErrorException; + + /** + * 图片智能裁剪接口 + * 说明: + * 1.图片支持使用img参数实时上传,也支持使用img_url参数传送图片地址,由微信后台下载图片进行识别 + * 2.文件大小限制:小于2M + * 3.该接口默认使用最佳宽高比 + * @param imgFile 图片文件对象 + * @return WxMpImgProcAiCropResult + * @throws WxErrorException . + */ + WxImgProcAiCropResult aiCrop(File imgFile) throws WxErrorException; + + /** + * 图片智能裁剪接口 + * 说明: + * 1.图片支持使用img参数实时上传,也支持使用img_url参数传送图片地址,由微信后台下载图片进行识别 + * 2.文件大小限制:小于2M + * @param imgFile 图片文件对象 + * @param ratios 宽高比,最多支持5个,请以英文逗号分隔 + * @return WxMpImgProcAiCropResult + * @throws WxErrorException . + */ + WxImgProcAiCropResult aiCrop(File imgFile, String ratios) throws WxErrorException; +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxOAuth2Service.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxOAuth2Service.java new file mode 100644 index 0000000000..5dea04928e --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxOAuth2Service.java @@ -0,0 +1,83 @@ +package me.chanjar.weixin.common.service; + +import me.chanjar.weixin.common.bean.WxOAuth2UserInfo; +import me.chanjar.weixin.common.bean.oauth2.WxOAuth2AccessToken; +import me.chanjar.weixin.common.error.WxErrorException; + +/** + * oauth2 相关接口. + * + * @author Binary Wang + * created on 2020-08-08 + */ +public interface WxOAuth2Service { + /** + *

+   * 构造oauth2授权的url连接.
+   * 详情请见: 网页授权
+   * 
+ * + * @param redirectUri 用户授权完成后的重定向链接,无需urlencode, 方法内会进行encode + * @param scope scope,静默:snsapi_base, 带信息授权:snsapi_userinfo + * @param state state + * @return url + */ + String buildAuthorizationUrl(String redirectUri, String scope, String state); + + /** + *
+   * 用code换取oauth2的access token.
+   * 详情请见: 网页授权获取用户基本信息
+   * 
+ * + * @param code code + * @return token对象 + * @throws WxErrorException . + */ + WxOAuth2AccessToken getAccessToken(String code) throws WxErrorException; + + /** + * 用code换取oauth2的access token. + * + * @param appId the appid + * @param appSecret the secret + * @param code code + * @return token对象 + * @throws WxErrorException . + */ + WxOAuth2AccessToken getAccessToken(String appId, String appSecret, String code) throws WxErrorException; + + /** + *
+   * 刷新oauth2的access token.
+   * 
+ * + * @param refreshToken 刷新token + * @return 新的token对象 + * @throws WxErrorException . + */ + WxOAuth2AccessToken refreshAccessToken(String refreshToken) throws WxErrorException; + + /** + *
+   * 用oauth2获取用户信息, 当前面引导授权时的scope是snsapi_userinfo的时候才可以.
+   * 
+ * + * @param oAuth2AccessToken token对象 + * @param lang zh_CN, zh_TW, en + * @return 用户对象 + * @throws WxErrorException . + */ + WxOAuth2UserInfo getUserInfo(WxOAuth2AccessToken oAuth2AccessToken, String lang) throws WxErrorException; + + /** + *
+   * 验证oauth2的access token是否有效.
+   * 
+ * + * @param oAuth2AccessToken token对象 + * @return 是否有效 + */ + boolean validateAccessToken(WxOAuth2AccessToken oAuth2AccessToken); + +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxOAuth2ServiceDecorator.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxOAuth2ServiceDecorator.java new file mode 100644 index 0000000000..a495dbf828 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxOAuth2ServiceDecorator.java @@ -0,0 +1,16 @@ +package me.chanjar.weixin.common.service; + +import lombok.AllArgsConstructor; +import lombok.experimental.Delegate; + +/** + * 微信 oauth2服务 装饰器 + * + * @author 广州跨界 + */ +@AllArgsConstructor +public class WxOAuth2ServiceDecorator implements WxOAuth2Service { + + @Delegate + private final WxOAuth2Service wxOAuth2Service; +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxOcrService.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxOcrService.java new file mode 100644 index 0000000000..d0aeef8491 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxOcrService.java @@ -0,0 +1,133 @@ +package me.chanjar.weixin.common.service; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.bean.ocr.WxOcrBankCardResult; +import me.chanjar.weixin.common.bean.ocr.WxOcrBizLicenseResult; +import me.chanjar.weixin.common.bean.ocr.WxOcrCommResult; +import me.chanjar.weixin.common.bean.ocr.WxOcrDrivingLicenseResult; +import me.chanjar.weixin.common.bean.ocr.WxOcrDrivingResult; +import me.chanjar.weixin.common.bean.ocr.WxOcrIdCardResult; + +import java.io.File; + +/** + * 基于小程序或 H5 的身份证、银行卡、行驶证 OCR 识别. + *

+ * 参考:{@code https://mp.weixin.qq.com/wiki?t=resource/res_main&id=21516712284rHWMX} + *

+ * + * @author Binary Wang + * created on 2019-06-22 + */ +public interface WxOcrService { + + /** + * 身份证OCR识别接口. + * + * @param imgUrl 图片url地址 + * @return WxMpOcrIdCardResult + * @throws WxErrorException . + */ + WxOcrIdCardResult idCard(String imgUrl) throws WxErrorException; + + /** + * 身份证OCR识别接口. + * + * @param imgFile 图片文件对象 + * @return WxMpOcrIdCardResult + * @throws WxErrorException . + */ + WxOcrIdCardResult idCard(File imgFile) throws WxErrorException; + + /** + * 银行卡OCR识别接口 + * 文件大小限制:小于2M + * @param imgUrl 图片url地址 + * @return WxMpOcrBankCardResult + * @throws WxErrorException . + */ + WxOcrBankCardResult bankCard(String imgUrl) throws WxErrorException; + + /** + * 银行卡OCR识别接口 + * 文件大小限制:小于2M + * @param imgFile 图片文件对象 + * @return WxMpOcrBankCardResult + * @throws WxErrorException . + */ + WxOcrBankCardResult bankCard(File imgFile) throws WxErrorException; + + /** + * 行驶证OCR识别接口 + * 文件大小限制:小于2M + * @param imgUrl 图片url地址 + * @return WxMpOcrDrivingResult + * @throws WxErrorException . + */ + WxOcrDrivingResult driving(String imgUrl) throws WxErrorException; + + /** + * 行驶证OCR识别接口 + * 文件大小限制:小于2M + * @param imgFile 图片文件对象 + * @return WxMpOcrDrivingResult + * @throws WxErrorException . + */ + WxOcrDrivingResult driving(File imgFile) throws WxErrorException; + + /** + * 驾驶证OCR识别接口 + * 文件大小限制:小于2M + * @param imgUrl 图片url地址 + * @return WxMpOcrDrivingLicenseResult + * @throws WxErrorException . + */ + WxOcrDrivingLicenseResult drivingLicense(String imgUrl) throws WxErrorException; + + /** + * 驾驶证OCR识别接口 + * 文件大小限制:小于2M + * @param imgFile 图片文件对象 + * @return WxMpOcrDrivingLicenseResult + * @throws WxErrorException . + */ + WxOcrDrivingLicenseResult drivingLicense(File imgFile) throws WxErrorException; + + /** + * 营业执照OCR识别接口 + * 文件大小限制:小于2M + * @param imgUrl 图片url地址 + * @return WxMpOcrBizLicenseResult + * @throws WxErrorException . + */ + WxOcrBizLicenseResult bizLicense(String imgUrl) throws WxErrorException; + + /** + * 营业执照OCR识别接口 + * 文件大小限制:小于2M + * @param imgFile 图片文件对象 + * @return WxMpOcrBizLicenseResult + * @throws WxErrorException . + */ + WxOcrBizLicenseResult bizLicense(File imgFile) throws WxErrorException; + + /** + * 通用印刷体OCR识别接口 + * 文件大小限制:小于2M + * 适用于屏幕截图、印刷体照片等场景 + * @param imgUrl 图片url地址 + * @return WxMpOcrCommResult + * @throws WxErrorException . + */ + WxOcrCommResult comm(String imgUrl) throws WxErrorException; + + /** + * 通用印刷体OCR识别接口 + * 文件大小限制:小于2M + * 适用于屏幕截图、印刷体照片等场景 + * @param imgFile 图片文件对象 + * @return WxMpOcrCommResult + * @throws WxErrorException . + */ + WxOcrCommResult comm(File imgFile) throws WxErrorException; +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxService.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxService.java new file mode 100644 index 0000000000..f894cba44f --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxService.java @@ -0,0 +1,74 @@ +package me.chanjar.weixin.common.service; + +import com.google.gson.JsonObject; +import me.chanjar.weixin.common.bean.CommonUploadParam; +import me.chanjar.weixin.common.bean.ToJson; +import me.chanjar.weixin.common.error.WxErrorException; + +/** + * 微信服务接口. + * + * @author Binary Wang + * created on 2020-04-25 + */ +public interface WxService { + /** + * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的GET请求. + * + * @param queryParam 参数 + * @param url 请求接口地址 + * @return 接口响应字符串 + * @throws WxErrorException 异常 + */ + String get(String url, String queryParam) throws WxErrorException; + + /** + * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求. + * + * @param postData 请求参数json值 + * @param url 请求接口地址 + * @return 接口响应字符串 + * @throws WxErrorException 异常 + */ + String post(String url, String postData) throws WxErrorException; + + /** + * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求. + * + * @param url 请求接口地址 + * @param obj 请求对象 + * @return 接口响应字符串 + * @throws WxErrorException 异常 + */ + String post(String url, Object obj) throws WxErrorException; + + /** + * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求. + * + * @param url 请求接口地址 + * @param jsonObject 请求对象 + * @return 接口响应字符串 + * @throws WxErrorException 异常 + */ + String post(String url, JsonObject jsonObject) throws WxErrorException; + + /** + * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求. + * + * @param url 请求接口地址 + * @param obj 请求对象,实现了ToJson接口 + * @return 接口响应字符串 + * @throws WxErrorException 异常 + */ + String post(String url, ToJson obj) throws WxErrorException; + + /** + * 当本Service没有实现某个上传API的时候,可以用这个,针对所有微信API中的POST文件上传请求 + * + * @param url 请求接口地址 + * @param param 文件上传对象 + * @return 接口响应字符串 + * @throws WxErrorException 异常 + */ + String upload(String url, CommonUploadParam param) throws WxErrorException; +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/InternalSessionManager.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/InternalSessionManager.java index e3d9ab8351..24ea58ef38 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/InternalSessionManager.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/InternalSessionManager.java @@ -7,13 +7,12 @@ public interface InternalSessionManager { /** * Return the active Session, associated with this Manager, with the - * specified session id (if any); otherwise return null. + * specified session id (if any); otherwise return {@code null}. * * @param id The session id for the session to be returned + * @return the session or null * @throws IllegalStateException if a new session cannot be * instantiated for any reason - * @throws java.io.IOException if an input/output error occurs while - * processing this request */ InternalSession findSession(String id); diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/StandardSessionManager.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/StandardSessionManager.java index 2472cb44b8..8d994b9c36 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/StandardSessionManager.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/session/StandardSessionManager.java @@ -1,13 +1,12 @@ package me.chanjar.weixin.common.session; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; - +import me.chanjar.weixin.common.util.res.StringManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import me.chanjar.weixin.common.util.res.StringManager; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; /** * 基于内存的session manager. @@ -15,7 +14,6 @@ * @author Daniel Qian */ public class StandardSessionManager implements WxSessionManager, InternalSessionManager { - protected static final StringManager SM = StringManager.getManager(Constants.PACKAGE); /** * The descriptive name of this Manager implementation (for logging). @@ -51,7 +49,9 @@ public class StandardSessionManager implements WxSessionManager, InternalSession */ protected int maxInactiveInterval = 30 * 60; - // Number of sessions created by this manager + /** + * Number of sessions created by this manager + */ protected long sessionCounter = 0; protected volatile int maxActive = 0; @@ -98,7 +98,7 @@ public WxSession getSession(String sessionId, boolean create) { // Create a new session if requested and the response is not committed if (!create) { - return (null); + return null; } session = createSession(sessionId); @@ -127,7 +127,7 @@ public void remove(InternalSession session, boolean update) { @Override public InternalSession findSession(String id) { if (id == null) { - return (null); + return null; } return this.sessions.get(id); } @@ -154,12 +154,10 @@ public InternalSession createSession(String sessionId) { session.setValid(true); session.setCreationTime(System.currentTimeMillis()); session.setMaxInactiveInterval(this.maxInactiveInterval); - String id = sessionId; - session.setId(id); + session.setId(sessionId); this.sessionCounter++; - return (session); - + return session; } @@ -181,24 +179,19 @@ protected InternalSession getNewSession() { return new StandardSession(this); } - @Override public void add(InternalSession session) { - // 当第一次有session创建的时候,开启session清理线程 if (!this.backgroundProcessStarted.getAndSet(true)) { - Thread t = new Thread(new Runnable() { - @Override - public void run() { - while (true) { - try { - // 每秒清理一次 - Thread.sleep(StandardSessionManager.this.backgroundProcessorDelay * 1000L); - backgroundProcess(); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - StandardSessionManager.this.log.error("SessionManagerImpl.backgroundProcess error", e); - } + Thread t = new Thread(() -> { + while (true) { + try { + // 每秒清理一次 + Thread.sleep(StandardSessionManager.this.backgroundProcessorDelay * 1000L); + backgroundProcess(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + StandardSessionManager.this.log.error("SessionManagerImpl.backgroundProcess error", e); } } }); @@ -258,7 +251,7 @@ public void processExpires() { if (this.log.isDebugEnabled()) { this.log.debug("End expire sessions {} processingTime {} expired sessions: {}", getName(), timeEnd - timeNow, expireHere); } - this.processingTime += (timeEnd - timeNow); + this.processingTime += timeEnd - timeNow; } @@ -296,7 +289,7 @@ public void setBackgroundProcessorDelay(int backgroundProcessorDelay) { */ public String getName() { - return (name); + return name; } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/BeanUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/BeanUtils.java index 4b7f9be6a7..d3f8d00406 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/BeanUtils.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/BeanUtils.java @@ -2,7 +2,6 @@ import com.google.common.collect.Lists; import me.chanjar.weixin.common.annotation.Required; -import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; @@ -50,16 +49,15 @@ public static void checkRequiredFields(Object bean) throws WxErrorException { } } field.setAccessible(isAccessible); - } catch (SecurityException | IllegalArgumentException - | IllegalAccessException e) { + } catch (SecurityException | IllegalArgumentException | IllegalAccessException e) { log.error(e.getMessage(), e); } } if (!requiredFields.isEmpty()) { - String msg = "必填字段 " + requiredFields + " 必须提供值"; + String msg = String.format("必填字段【%s】必须提供值!", requiredFields); log.debug(msg); - throw new WxErrorException(WxError.builder().errorMsg(msg).build()); + throw new WxErrorException(msg); } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/DataUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/DataUtils.java index 983d9a668f..b8fb42e0e9 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/DataUtils.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/DataUtils.java @@ -1,5 +1,6 @@ package me.chanjar.weixin.common.util; +import org.apache.commons.lang3.RegExUtils; import org.apache.commons.lang3.StringUtils; /** @@ -17,7 +18,7 @@ public class DataUtils { public static E handleDataWithSecret(E data) { E dataForLog = data; if(data instanceof String && StringUtils.contains((String)data, "&secret=")){ - dataForLog = (E) StringUtils.replaceAll((String)data,"&secret=\\w+&","&secret=******&"); + dataForLog = (E) RegExUtils.replaceAll((String)data,"&secret=\\w+&","&secret=******&"); } return dataForLog; } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/LogExceptionHandler.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/LogExceptionHandler.java index 7487a0fe29..35b0eea822 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/LogExceptionHandler.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/LogExceptionHandler.java @@ -1,20 +1,17 @@ package me.chanjar.weixin.common.util; +import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.api.WxErrorExceptionHandler; import me.chanjar.weixin.common.error.WxErrorException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - +/** + * @author Daniel Qian + */ +@Slf4j public class LogExceptionHandler implements WxErrorExceptionHandler { - - private Logger log = LoggerFactory.getLogger(WxErrorExceptionHandler.class); - @Override public void handle(WxErrorException e) { - - this.log.error("Error happens", e); - + log.error("Error happens", e); } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/RandomUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/RandomUtils.java index bbb11992bc..a9017c0d16 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/RandomUtils.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/RandomUtils.java @@ -4,12 +4,24 @@ public class RandomUtils { private static final String RANDOM_STR = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - private static final java.util.Random RANDOM = new java.util.Random(); + private static volatile java.util.Random random; + + private static java.util.Random getRandom() { + if (random == null) { + synchronized (RandomUtils.class) { + if (random == null) { + random = new java.util.Random(); + } + } + } + return random; + } public static String getRandomStr() { StringBuilder sb = new StringBuilder(); + java.util.Random r = getRandom(); for (int i = 0; i < 16; i++) { - sb.append(RANDOM_STR.charAt(RANDOM.nextInt(RANDOM_STR.length()))); + sb.append(RANDOM_STR.charAt(r.nextInt(RANDOM_STR.length()))); } return sb.toString(); } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/SignUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/SignUtils.java index fc3579d45c..1886209f98 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/SignUtils.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/SignUtils.java @@ -25,6 +25,7 @@ public class SignUtils { * * @param message 签名数据 * @param key 签名密钥 + * @return 签名结果 */ public static String createHmacSha256Sign(String message, String key) { try { diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/XmlUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/XmlUtils.java index cd3a7a984c..67faf319f4 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/XmlUtils.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/XmlUtils.java @@ -1,21 +1,20 @@ package me.chanjar.weixin.common.util; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import me.chanjar.weixin.common.error.WxRuntimeException; +import org.dom4j.*; +import org.dom4j.io.SAXReader; +import org.dom4j.tree.DefaultText; +import org.xml.sax.SAXException; + import java.io.StringReader; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; - -import org.dom4j.Document; -import org.dom4j.DocumentException; -import org.dom4j.Element; -import org.dom4j.Node; -import org.dom4j.io.SAXReader; -import org.dom4j.tree.DefaultText; - -import com.google.common.collect.Lists; -import com.google.common.collect.Maps; -import com.google.common.collect.Sets; +import java.util.stream.Collectors; /** *
@@ -31,32 +30,51 @@ public static Map xml2Map(String xmlString) {
     Map map = new HashMap<>(16);
     try {
       SAXReader saxReader = new SAXReader();
+      saxReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+      saxReader.setFeature("http://javax.xml.XMLConstants/feature/secure-processing", true);
+      saxReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+      saxReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
+      saxReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
       Document doc = saxReader.read(new StringReader(xmlString));
       Element root = doc.getRootElement();
       List elements = root.elements();
       for (Element element : elements) {
-          map.put(element.getName(), element2MapOrString(element));
+        String elementName = element.getName();
+        if (map.containsKey(elementName)) {
+          if (map.get(elementName) instanceof List) {
+            ((List) map.get(elementName)).add(element2MapOrString(element));
+          } else {
+            List value = Lists.newArrayList(map.get(elementName));
+            value.add(element2MapOrString(element));
+            map.put(elementName, value);
+          }
+        } else {
+          map.put(elementName, element2MapOrString(element));
+        }
       }
-    } catch (DocumentException e) {
-      throw new RuntimeException(e);
+    } catch (DocumentException | SAXException e) {
+      throw new WxRuntimeException(e);
     }
 
     return map;
   }
 
   private static Object element2MapOrString(Element element) {
-    Map result = Maps.newHashMap();
 
-    final List content = element.content();
-    if (content.size() <= 1) {
+    final List nodes = element.content();
+    final List names = names(nodes);
+
+    // 判断节点下有无非文本节点(非Text和CDATA),如无,直接取Text文本内容
+    if (names.isEmpty()) {
       return element.getText();
     }
 
-    final Set names = names(content);
-    if (names.size() == 1) {
+    Map result = Maps.newHashMap();
+    Set distinctNames = Sets.newHashSet(names);
+    if (distinctNames.size() == 1) {
       // 说明是个列表,各个子对象是相同的name
       List list = Lists.newArrayList();
-      for (Node node : content) {
+      for (Node node : nodes) {
         if (node instanceof DefaultText) {
           continue;
         }
@@ -67,8 +85,8 @@ private static Object element2MapOrString(Element element) {
       }
 
       result.put(names.iterator().next(), list);
-    } else {
-      for (Node node : content) {
+    } else if (distinctNames.size() == names.size()) {
+      for (Node node : nodes) {
         if (node instanceof DefaultText) {
           continue;
         }
@@ -77,15 +95,41 @@ private static Object element2MapOrString(Element element) {
           result.put(node.getName(), element2MapOrString((Element) node));
         }
       }
+    } else {
+      // 说明有重复name,但不是全部都相同
+      Map namesCountMap = names.stream().collect(Collectors.groupingBy(a -> a, Collectors.counting()));
+      for (Node node : nodes) {
+        if (node instanceof DefaultText) {
+          continue;
+        }
+
+        if (node instanceof Element) {
+          String nodeName = node.getName();
+          if (namesCountMap.get(nodeName) == 1) {
+            result.put(nodeName, element2MapOrString((Element) node));
+          } else {
+            List values;
+            if (result.containsKey(nodeName)) {
+              values = (List) result.get(nodeName);
+            } else {
+              values = Lists.newArrayList();
+              result.put(nodeName, values);
+            }
+
+            values.add(element2MapOrString((Element) node));
+          }
+        }
+      }
     }
 
     return result;
   }
 
-  private static Set names(List nodes) {
-    Set names = Sets.newHashSet();
+  private static List names(List nodes) {
+    List names = Lists.newArrayList();
     for (Node node : nodes) {
-      if (node instanceof DefaultText) {
+      // 如果节点类型是Text或CDATA跳过
+      if (node instanceof DefaultText || node instanceof CDATA) {
         continue;
       }
       names.add(node.getName());
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/SHA1.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/SHA1.java
index c82f94d871..43cc54b43d 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/SHA1.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/SHA1.java
@@ -8,7 +8,7 @@
 /**
  *
  * @author Daniel Qian
- * @date 14/10/19
+ * created on  14/10/19
  */
 public class SHA1 {
 
@@ -29,7 +29,10 @@ public static String gen(String... arr) {
   }
 
   /**
-   * 用&串接arr参数,生成sha1 digest.
+   * {@code 用&串接arr参数,生成sha1 digest.}
+   *
+   * @param arr 参数数组
+   * @return sha1摘要
    */
   public static String genWithAmple(String... arr) {
     if (StringUtils.isAnyEmpty(arr)) {
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java
index 5c16e71cec..50362636fc 100755
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/WxCryptUtil.java
@@ -1,21 +1,25 @@
 package me.chanjar.weixin.common.util.crypto;
 
-import java.io.StringReader;
-import java.nio.charset.Charset;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Random;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import me.chanjar.weixin.common.error.WxRuntimeException;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.StringUtils;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+import org.xml.sax.InputSource;
+
 import javax.crypto.Cipher;
 import javax.crypto.spec.IvParameterSpec;
 import javax.crypto.spec.SecretKeySpec;
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
-
-import org.apache.commons.codec.binary.Base64;
-import org.w3c.dom.Document;
-import org.w3c.dom.Element;
-import org.xml.sax.InputSource;
+import java.io.StringReader;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Random;
 
 /**
  * 
@@ -33,19 +37,29 @@ public class WxCryptUtil {
   private static final Base64 BASE64 = new Base64();
   private static final Charset CHARSET = StandardCharsets.UTF_8;
 
-  private static final ThreadLocal BUILDER_LOCAL = new ThreadLocal() {
-    @Override
-    protected DocumentBuilder initialValue() {
-      try {
-        final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
-        factory.setExpandEntityReferences(false);
-        factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
-        return factory.newDocumentBuilder();
-      } catch (ParserConfigurationException exc) {
-        throw new IllegalArgumentException(exc);
+  private static volatile Random random;
+
+  private static Random getRandom() {
+    if (random == null) {
+      synchronized (WxCryptUtil.class) {
+        if (random == null) {
+          random = new Random();
+        }
       }
     }
-  };
+    return random;
+  }
+
+  private static final ThreadLocal BUILDER_LOCAL = ThreadLocal.withInitial(() -> {
+    try {
+      final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+      factory.setExpandEntityReferences(false);
+      factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+      return factory.newDocumentBuilder();
+    } catch (ParserConfigurationException exc) {
+      throw new IllegalArgumentException(exc);
+    }
+  });
 
   protected byte[] aesKey;
   protected String token;
@@ -64,7 +78,7 @@ public WxCryptUtil() {
   public WxCryptUtil(String token, String encodingAesKey, String appidOrCorpid) {
     this.token = token;
     this.appidOrCorpid = appidOrCorpid;
-    this.aesKey = Base64.decodeBase64(encodingAesKey + "=");
+    this.aesKey = Base64.decodeBase64(StringUtils.remove(encodingAesKey, " "));
   }
 
   private static String extractEncryptPart(String xml) {
@@ -75,7 +89,7 @@ private static String extractEncryptPart(String xml) {
       Element root = document.getDocumentElement();
       return root.getElementsByTagName("Encrypt").item(0).getTextContent();
     } catch (Exception e) {
-      throw new RuntimeException(e);
+      throw new WxRuntimeException(e);
     }
   }
 
@@ -108,10 +122,10 @@ private static int bytesNetworkOrder2Number(byte[] bytesInNetworkOrder) {
    */
   private static String genRandomStr() {
     String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
-    Random random = new Random();
+    Random r = getRandom();
     StringBuilder sb = new StringBuilder();
     for (int i = 0; i < 16; i++) {
-      int number = random.nextInt(base.length());
+      int number = r.nextInt(base.length());
       sb.append(base.charAt(number));
     }
     return sb.toString();
@@ -157,13 +171,37 @@ public String encrypt(String plainText) {
     return generateXml(encryptedXml, signature, timeStamp, nonce);
   }
 
+  /**
+   * 将公众平台回复用户的消息加密打包.
+   * 
    + *
  1. 对要发送的消息进行AES-CBC加密
  2. + *
  3. 生成安全签名
  4. + *
  5. 将消息密文和安全签名打包成xml格式
  6. + *
+ * + * @param plainText 公众平台待回复用户的消息,xml格式的字符串 + * @return 加密消息所需的值对象 + */ + public EncryptContext encryptContext(String plainText) { + // 加密 + String encryptedXml = encrypt(genRandomStr(), plainText); + + // 生成安全签名 + String timeStamp = Long.toString(System.currentTimeMillis() / 1000L); + String nonce = genRandomStr(); + + String signature = SHA1.gen(this.token, timeStamp, nonce, encryptedXml); + return new EncryptContext(encryptedXml, signature, timeStamp, nonce); + } + /** * 对明文进行加密. * + * @param randomStr 随机字符串 * @param plainText 需要加密的明文 * @return 加密后base64编码的字符串 */ - protected String encrypt(String randomStr, String plainText) { + public String encrypt(String randomStr, String plainText) { ByteGroup byteCollector = new ByteGroup(); byte[] randomStringBytes = randomStr.getBytes(CHARSET); byte[] plainTextBytes = plainText.getBytes(CHARSET); @@ -196,7 +234,7 @@ protected String encrypt(String randomStr, String plainText) { // 使用BASE64对加密后的字符串进行编码 return BASE64.encodeToString(encrypted); } catch (Exception e) { - throw new RuntimeException(e); + throw new WxRuntimeException(e); } } @@ -211,22 +249,58 @@ protected String encrypt(String randomStr, String plainText) { * @param msgSignature 签名串,对应URL参数的msg_signature * @param timeStamp 时间戳,对应URL参数的timestamp * @param nonce 随机串,对应URL参数的nonce - * @param encryptedXml 密文,对应POST请求的数据 + * @param encryptedXml 包含 Encrypt 密文的 xml,对应POST请求的数据 * @return 解密后的原文 */ - public String decrypt(String msgSignature, String timeStamp, String nonce, String encryptedXml) { + public String decryptXml(String msgSignature, String timeStamp, String nonce, String encryptedXml) { // 密钥,公众账号的app corpSecret // 提取密文 String cipherText = extractEncryptPart(encryptedXml); + return decryptContent(msgSignature, timeStamp, nonce, cipherText); + } + /** + * 检验消息的真实性,并且获取解密后的明文. + *
    + *
  1. 利用收到的密文生成安全签名,进行签名验证
  2. + *
  3. 若验证通过,则提取xml中的加密消息
  4. + *
  5. 对消息进行解密
  6. + *
+ * + * @param msgSignature 签名串,对应URL参数的msg_signature + * @param timeStamp 时间戳,对应URL参数的timestamp + * @param nonce 随机串,对应URL参数的nonce + * @param encryptedXml 包含 Encrypt 密文的 xml,对应POST请求的数据 + * @return 解密后的原文 + * @deprecated 由于语义不清晰,置为过时方法,请查看替代方法 {@link #decryptXml} + */ + @Deprecated + public String decrypt(String msgSignature, String timeStamp, String nonce, String encryptedXml) { + return decryptXml(msgSignature, timeStamp, nonce, encryptedXml); + } + + /** + * 检验消息的真实性,并且获取解密后的明文. + *
    + *
  1. 利用收到的密文生成安全签名,进行签名验证
  2. + *
  3. 若验证通过,则提取xml中的加密消息
  4. + *
  5. 对消息进行解密
  6. + *
+ * + * @param msgSignature 签名串,对应URL参数的msg_signature + * @param timeStamp 时间戳,对应URL参数的timestamp + * @param nonce 随机串,对应URL参数的nonce + * @param encryptedContent 加密文本体 + * @return 解密后的原文 + */ + public String decryptContent(String msgSignature, String timeStamp, String nonce, String encryptedContent) { // 验证安全签名 - String signature = SHA1.gen(this.token, timeStamp, nonce, cipherText); + String signature = SHA1.gen(this.token, timeStamp, nonce, encryptedContent); if (!signature.equals(msgSignature)) { - throw new RuntimeException("加密消息签名校验失败"); + throw new WxRuntimeException("加密消息签名校验失败"); } - // 解密 - return decrypt(cipherText); + return decrypt(encryptedContent); } /** @@ -250,7 +324,7 @@ public String decrypt(String cipherText) { // 解密 original = cipher.doFinal(encrypted); } catch (Exception e) { - throw new RuntimeException(e); + throw new WxRuntimeException(e); } String xmlContent; @@ -260,23 +334,45 @@ public String decrypt(String cipherText) { byte[] bytes = PKCS7Encoder.decode(original); // 分离16位随机字符串,网络字节序和AppId + if (bytes == null || bytes.length < 20) { + throw new WxRuntimeException("解密后数据长度异常,可能为错误的密文或EncodingAESKey"); + } byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20); int xmlLength = bytesNetworkOrder2Number(networkOrder); - xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET); - fromAppid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), CHARSET); + // 长度边界校验,避免非法长度导致的越界/参数异常 + int startIndex = 20; + int endIndex = startIndex + xmlLength; + if (xmlLength < 0 || endIndex > bytes.length) { + throw new WxRuntimeException("解密后数据格式非法:消息长度不正确,可能为错误的密文或EncodingAESKey"); + } + + xmlContent = new String(Arrays.copyOfRange(bytes, startIndex, endIndex), CHARSET); + fromAppid = new String(Arrays.copyOfRange(bytes, endIndex, bytes.length), CHARSET); } catch (Exception e) { - throw new RuntimeException(e); + if (e instanceof WxRuntimeException) { + throw (WxRuntimeException) e; + } else { + throw new WxRuntimeException(e); + } } // appid不相同的情况 暂时忽略这段判断 -// if (!fromAppid.equals(this.appidOrCorpid)) { -// throw new RuntimeException("AppID不正确,请核实!"); -// } + // if (!fromAppid.equals(this.appidOrCorpid)) { + // throw new WxRuntimeException("AppID不正确,请核实!"); + // } return xmlContent; } + @Data + @AllArgsConstructor + public static class EncryptContext { + private String encrypt; + private String signature; + private String timeStamp; + private String nonce; + } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/fs/FileUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/fs/FileUtils.java index 1b051a4718..4a267153c6 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/fs/FileUtils.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/fs/FileUtils.java @@ -1,10 +1,19 @@ package me.chanjar.weixin.common.util.fs; +import org.apache.commons.io.IOUtils; + import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; import java.nio.file.Files; +import java.util.Base64; + +import static org.apache.commons.io.FileUtils.openOutputStream; +/** + * @author Daniel Qian + */ public class FileUtils { /** @@ -15,14 +24,21 @@ public class FileUtils { * @param ext 扩展名 * @param tmpDirFile 临时文件夹目录 */ - public static File createTmpFile(InputStream inputStream, String name, String ext, File tmpDirFile) throws IOException { + public static File createTmpFile(InputStream inputStream, String name, String ext, File tmpDirFile) + throws IOException { File resultFile = File.createTempFile(name, '.' + ext, tmpDirFile); resultFile.deleteOnExit(); - org.apache.commons.io.FileUtils.copyToFile(inputStream, resultFile); + copyToFile(inputStream, resultFile); return resultFile; } + private static void copyToFile(final InputStream source, final File destination) throws IOException { + try (InputStream in = source; OutputStream out = openOutputStream(destination)) { + IOUtils.copy(in, out); + } + } + /** * 创建临时文件. * @@ -31,7 +47,35 @@ public static File createTmpFile(InputStream inputStream, String name, String ex * @param ext 扩展名 */ public static File createTmpFile(InputStream inputStream, String name, String ext) throws IOException { - return createTmpFile(inputStream, name, ext, Files.createTempDirectory("weixin-java-tools-temp").toFile()); + return createTmpFile(inputStream, name, ext, Files.createTempDirectory("wxjava-temp").toFile()); + } + + /** + * 文件流生成base64 + * + * @param in 文件流 + * @return base64编码 + */ + public static String imageToBase64ByStream(InputStream in) { + byte[] data = null; + // 读取图片字节数组 + try { + data = new byte[in.available()]; + in.read(data); + // 返回Base64编码过的字节数组字符串 + return Base64.getEncoder().encodeToString(data); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return null; } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/BaseMediaDownloadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/BaseMediaDownloadRequestExecutor.java index 5c49c0f78d..8304742524 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/BaseMediaDownloadRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/BaseMediaDownloadRequestExecutor.java @@ -1,13 +1,18 @@ package me.chanjar.weixin.common.util.http; -import java.io.File; -import java.io.IOException; - -import me.chanjar.weixin.common.WxType; +import jodd.http.HttpConnectionProvider; +import jodd.http.ProxyInfo; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.util.http.apache.ApacheMediaDownloadRequestExecutor; +import me.chanjar.weixin.common.util.http.hc.HttpComponentsMediaDownloadRequestExecutor; import me.chanjar.weixin.common.util.http.jodd.JoddHttpMediaDownloadRequestExecutor; import me.chanjar.weixin.common.util.http.okhttp.OkHttpMediaDownloadRequestExecutor; +import me.chanjar.weixin.common.util.http.okhttp.OkHttpProxyInfo; +import okhttp3.OkHttpClient; + +import java.io.File; +import java.io.IOException; /** * 下载媒体文件请求执行器. @@ -30,16 +35,21 @@ public void execute(String uri, String data, ResponseHandler handler, WxTy handler.handle(this.execute(uri, data, wxType)); } - public static RequestExecutor create(RequestHttp requestHttp, File tmpDirFile) { + @SuppressWarnings("unchecked") + public static RequestExecutor create(RequestHttp requestHttp, File tmpDirFile) { switch (requestHttp.getRequestType()) { case APACHE_HTTP: - return new ApacheMediaDownloadRequestExecutor(requestHttp, tmpDirFile); + return new ApacheMediaDownloadRequestExecutor( + (RequestHttp) requestHttp, tmpDirFile); case JODD_HTTP: - return new JoddHttpMediaDownloadRequestExecutor(requestHttp, tmpDirFile); + return new JoddHttpMediaDownloadRequestExecutor((RequestHttp) requestHttp, tmpDirFile); case OK_HTTP: - return new OkHttpMediaDownloadRequestExecutor(requestHttp, tmpDirFile); + return new OkHttpMediaDownloadRequestExecutor((RequestHttp) requestHttp, tmpDirFile); + case HTTP_COMPONENTS: + return new HttpComponentsMediaDownloadRequestExecutor( + (RequestHttp) requestHttp, tmpDirFile); default: - return null; + throw new IllegalArgumentException("不支持的http执行器类型:" + requestHttp.getRequestType()); } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpClientType.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpClientType.java new file mode 100644 index 0000000000..a4e22be9b4 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpClientType.java @@ -0,0 +1,23 @@ +package me.chanjar.weixin.common.util.http; + +/** + * Created by ecoolper on 2017/4/28. + */ +public enum HttpClientType { + /** + * jodd-http. + */ + JODD_HTTP, + /** + * apache httpclient 4.x. + */ + APACHE_HTTP, + /** + * okhttp. + */ + OK_HTTP, + /** + * apache httpclient 5.x. + */ + HTTP_COMPONENTS +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpResponseProxy.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpResponseProxy.java index 0d68518849..e45294b503 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpResponseProxy.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpResponseProxy.java @@ -1,90 +1,71 @@ package me.chanjar.weixin.common.util.http; -import jodd.http.HttpResponse; -import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; -import okhttp3.Response; -import org.apache.http.Header; -import org.apache.http.client.methods.CloseableHttpResponse; - +import me.chanjar.weixin.common.util.http.apache.ApacheHttpResponseProxy; +import me.chanjar.weixin.common.util.http.hc.HttpComponentsResponseProxy; +import me.chanjar.weixin.common.util.http.jodd.JoddHttpResponseProxy; +import me.chanjar.weixin.common.util.http.okhttp.OkHttpResponseProxy; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; import java.util.regex.Matcher; import java.util.regex.Pattern; /** *
- * 三种http框架的response代理类,方便提取公共方法
+ * http 框架的 response 代理类,方便提取公共方法
  * Created by Binary Wang on 2017-8-3.
  * 
* * @author Binary Wang */ -public class HttpResponseProxy { - private static final Pattern PATTERN = Pattern.compile(".*filename=\"(.*)\""); - - private CloseableHttpResponse apacheHttpResponse; - private HttpResponse joddHttpResponse; - private Response okHttpResponse; +public interface HttpResponseProxy { - public HttpResponseProxy(CloseableHttpResponse apacheHttpResponse) { - this.apacheHttpResponse = apacheHttpResponse; + static ApacheHttpResponseProxy from(org.apache.http.client.methods.CloseableHttpResponse response) { + return new ApacheHttpResponseProxy(response); } - public HttpResponseProxy(HttpResponse joddHttpResponse) { - this.joddHttpResponse = joddHttpResponse; + static HttpComponentsResponseProxy from(org.apache.hc.client5.http.impl.classic.CloseableHttpResponse response) { + return new HttpComponentsResponseProxy(response); } - public HttpResponseProxy(Response okHttpResponse) { - this.okHttpResponse = okHttpResponse; + static JoddHttpResponseProxy from(jodd.http.HttpResponse response) { + return new JoddHttpResponseProxy(response); } - public String getFileName() throws WxErrorException { - //由于对象只能由一个构造方法实现,因此三个response对象必定且只有一个不为空 - if (this.apacheHttpResponse != null) { - return this.getFileName(this.apacheHttpResponse); - } - - if (this.joddHttpResponse != null) { - return this.getFileName(this.joddHttpResponse); - } - - if (this.okHttpResponse != null) { - return this.getFileName(this.okHttpResponse); - } - - //cannot happen - return null; + static OkHttpResponseProxy from(okhttp3.Response response) { + return new OkHttpResponseProxy(response); } - private String getFileName(CloseableHttpResponse response) throws WxErrorException { - Header[] contentDispositionHeader = response.getHeaders("Content-disposition"); - if (contentDispositionHeader == null || contentDispositionHeader.length == 0) { - throw new WxErrorException(WxError.builder().errorMsg("无法获取到文件名").errorCode(99999).build()); - } - - return this.extractFileNameFromContentString(contentDispositionHeader[0].getValue()); - } + String getFileName() throws WxErrorException; - private String getFileName(HttpResponse response) throws WxErrorException { - String content = response.header("Content-disposition"); - return this.extractFileNameFromContentString(content); - } - - private String getFileName(Response response) throws WxErrorException { - String content = response.header("Content-disposition"); - return this.extractFileNameFromContentString(content); - } + static String extractFileNameFromContentString(String content) throws WxErrorException { + if (content == null || content.isEmpty()) { + throw new WxErrorException("无法获取到文件名,content为空"); + } - private String extractFileNameFromContentString(String content) throws WxErrorException { - if (content == null || content.length() == 0) { - throw new WxErrorException(WxError.builder().errorMsg("无法获取到文件名").errorCode(99999).build()); + // 查找filename*=utf-8''开头的部分 + Pattern pattern = Pattern.compile("filename\\*=utf-8''(.*?)($|;|\\s|,)"); + Matcher matcher = pattern.matcher(content); + if (matcher.find()) { + String encodedFileName = matcher.group(1); + // 解码URL编码的文件名 + try { + return URLDecoder.decode(encodedFileName, StandardCharsets.UTF_8.name()); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } } - Matcher m = PATTERN.matcher(content); - if (m.matches()) { - return m.group(1); + // 查找普通filename="..."部分 + pattern = Pattern.compile("filename=\"(.*?)\""); + matcher = pattern.matcher(content); + if (matcher.find()) { + return new String(matcher.group(1).getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8); } - throw new WxErrorException(WxError.builder().errorMsg("无法获取到文件名").errorCode(99999).build()); + throw new WxErrorException("无法获取到文件名,header信息有问题"); } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpType.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpType.java deleted file mode 100644 index eff5907f7a..0000000000 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpType.java +++ /dev/null @@ -1,19 +0,0 @@ -package me.chanjar.weixin.common.util.http; - -/** - * Created by ecoolper on 2017/4/28. - */ -public enum HttpType { - /** - * jodd-http. - */ - JODD_HTTP, - /** - * apache httpclient. - */ - APACHE_HTTP, - /** - * okhttp. - */ - OK_HTTP -} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/InputStreamData.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/InputStreamData.java new file mode 100644 index 0000000000..f03932984f --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/InputStreamData.java @@ -0,0 +1,28 @@ +package me.chanjar.weixin.common.util.http; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; + +import java.io.InputStream; +import java.io.Serializable; + +/** + * 输入流数据. + *

+ * InputStreamData + *

+ * + * @author zichuan.zhou91@gmail.com + * created on 2022/2/15 + */ +@Data +@Accessors(chain = true) +@NoArgsConstructor +@AllArgsConstructor +public class InputStreamData implements Serializable { + private static final long serialVersionUID = -4627006604779378520L; + private InputStream inputStream; + private String filename; +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaInputStreamUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaInputStreamUploadRequestExecutor.java new file mode 100644 index 0000000000..22c426ca54 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaInputStreamUploadRequestExecutor.java @@ -0,0 +1,53 @@ +package me.chanjar.weixin.common.util.http; + +import jodd.http.HttpConnectionProvider; +import jodd.http.ProxyInfo; +import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.apache.ApacheMediaInputStreamUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.hc.HttpComponentsMediaInputStreamUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.jodd.JoddHttpMediaInputStreamUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.okhttp.OkHttpMediaInputStreamUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.okhttp.OkHttpProxyInfo; +import okhttp3.OkHttpClient; + +import java.io.IOException; + +/** + * 上传媒体文件请求执行器. + * 请求的参数是File, 返回的结果是String + * + * @author Daniel Qian + */ +public abstract class MediaInputStreamUploadRequestExecutor implements RequestExecutor { + protected RequestHttp requestHttp; + + public MediaInputStreamUploadRequestExecutor(RequestHttp requestHttp) { + this.requestHttp = requestHttp; + } + + @Override + public void execute(String uri, InputStreamData data, ResponseHandler handler, WxType wxType) throws WxErrorException, IOException { + handler.handle(this.execute(uri, data, wxType)); + } + + @SuppressWarnings("unchecked") + public static RequestExecutor create(RequestHttp requestHttp) { + switch (requestHttp.getRequestType()) { + case APACHE_HTTP: + return new ApacheMediaInputStreamUploadRequestExecutor( + (RequestHttp) requestHttp); + case JODD_HTTP: + return new JoddHttpMediaInputStreamUploadRequestExecutor((RequestHttp) requestHttp); + case OK_HTTP: + return new OkHttpMediaInputStreamUploadRequestExecutor((RequestHttp) requestHttp); + case HTTP_COMPONENTS: + return new HttpComponentsMediaInputStreamUploadRequestExecutor( + (RequestHttp) requestHttp); + default: + throw new IllegalArgumentException("不支持的http执行器类型:" + requestHttp.getRequestType()); + } + } + +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaUploadRequestExecutor.java index df1444cc75..2d16e714e9 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaUploadRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaUploadRequestExecutor.java @@ -1,25 +1,36 @@ package me.chanjar.weixin.common.util.http; -import java.io.File; -import java.io.IOException; - -import me.chanjar.weixin.common.WxType; +import jodd.http.HttpConnectionProvider; +import jodd.http.ProxyInfo; +import me.chanjar.weixin.common.bean.CommonUploadParam; import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.service.WxService; import me.chanjar.weixin.common.util.http.apache.ApacheMediaUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.hc.HttpComponentsMediaUploadRequestExecutor; import me.chanjar.weixin.common.util.http.jodd.JoddHttpMediaUploadRequestExecutor; import me.chanjar.weixin.common.util.http.okhttp.OkHttpMediaUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.okhttp.OkHttpProxyInfo; +import okhttp3.OkHttpClient; + +import java.io.File; +import java.io.IOException; /** * 上传媒体文件请求执行器. * 请求的参数是File, 返回的结果是String * * @author Daniel Qian + * @see WxService#upload(String, CommonUploadParam) 通用的上传,封装接口是推荐调用此方法 + * @see CommonUploadParam 通用的上传参数 + * @deprecated 不应该继续使用执行器的方式上传文件,封装上传接口时应调用通用的文件上传,而旧代码也应该逐步迁移为新的上传方式 */ +@Deprecated public abstract class MediaUploadRequestExecutor implements RequestExecutor { protected RequestHttp requestHttp; - public MediaUploadRequestExecutor(RequestHttp requestHttp) { + public MediaUploadRequestExecutor(RequestHttp requestHttp) { this.requestHttp = requestHttp; } @@ -28,16 +39,21 @@ public void execute(String uri, File data, ResponseHandler handler.handle(this.execute(uri, data, wxType)); } - public static RequestExecutor create(RequestHttp requestHttp) { + @SuppressWarnings("unchecked") + public static RequestExecutor create(RequestHttp requestHttp) { switch (requestHttp.getRequestType()) { case APACHE_HTTP: - return new ApacheMediaUploadRequestExecutor(requestHttp); + return new ApacheMediaUploadRequestExecutor( + (RequestHttp) requestHttp); case JODD_HTTP: - return new JoddHttpMediaUploadRequestExecutor(requestHttp); + return new JoddHttpMediaUploadRequestExecutor((RequestHttp) requestHttp); case OK_HTTP: - return new OkHttpMediaUploadRequestExecutor(requestHttp); + return new OkHttpMediaUploadRequestExecutor((RequestHttp) requestHttp); + case HTTP_COMPONENTS: + return new HttpComponentsMediaUploadRequestExecutor( + (RequestHttp) requestHttp); default: - return null; + throw new IllegalArgumentException("不支持的http执行器类型:" + requestHttp.getRequestType()); } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MinishopUploadRequestCustomizeExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MinishopUploadRequestCustomizeExecutor.java new file mode 100644 index 0000000000..0e8684a1db --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MinishopUploadRequestCustomizeExecutor.java @@ -0,0 +1,57 @@ +package me.chanjar.weixin.common.util.http; + +import jodd.http.HttpConnectionProvider; +import jodd.http.ProxyInfo; +import me.chanjar.weixin.common.bean.result.WxMinishopImageUploadCustomizeResult; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.apache.ApacheMinishopMediaUploadRequestCustomizeExecutor; +import me.chanjar.weixin.common.util.http.hc.HttpComponentsMinishopMediaUploadRequestCustomizeExecutor; +import me.chanjar.weixin.common.util.http.jodd.JoddHttpMinishopMediaUploadRequestCustomizeExecutor; +import me.chanjar.weixin.common.util.http.okhttp.OkHttpMinishopMediaUploadRequestCustomizeExecutor; +import me.chanjar.weixin.common.util.http.okhttp.OkHttpProxyInfo; +import okhttp3.OkHttpClient; + +import java.io.File; +import java.io.IOException; + +public abstract class MinishopUploadRequestCustomizeExecutor implements RequestExecutor { + protected RequestHttp requestHttp; + protected String respType; + protected String uploadType; + protected String imgUrl; + + public MinishopUploadRequestCustomizeExecutor(RequestHttp requestHttp, String respType, String imgUrl) { + this.requestHttp = requestHttp; + this.respType = respType; + if (imgUrl == null || imgUrl.isEmpty()) { + this.uploadType = "0"; + } else { + this.uploadType = "1"; + this.imgUrl = imgUrl; + } + } + + @Override + public void execute(String uri, File data, ResponseHandler handler, WxType wxType) throws WxErrorException, IOException { + handler.handle(this.execute(uri, data, wxType)); + } + + @SuppressWarnings("unchecked") + public static RequestExecutor create(RequestHttp requestHttp, String respType, String imgUrl) { + switch (requestHttp.getRequestType()) { + case APACHE_HTTP: + return new ApacheMinishopMediaUploadRequestCustomizeExecutor( + (RequestHttp) requestHttp, respType, imgUrl); + case JODD_HTTP: + return new JoddHttpMinishopMediaUploadRequestCustomizeExecutor((RequestHttp) requestHttp, respType, imgUrl); + case OK_HTTP: + return new OkHttpMinishopMediaUploadRequestCustomizeExecutor((RequestHttp) requestHttp, respType, imgUrl); + case HTTP_COMPONENTS: + return new HttpComponentsMinishopMediaUploadRequestCustomizeExecutor( + (RequestHttp) requestHttp, respType, imgUrl); + default: + throw new IllegalArgumentException("不支持的http执行器类型:" + requestHttp.getRequestType()); + } + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MinishopUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MinishopUploadRequestExecutor.java new file mode 100644 index 0000000000..e6018a7791 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MinishopUploadRequestExecutor.java @@ -0,0 +1,47 @@ +package me.chanjar.weixin.common.util.http; + +import jodd.http.HttpConnectionProvider; +import jodd.http.ProxyInfo; +import me.chanjar.weixin.common.bean.result.WxMinishopImageUploadResult; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.apache.ApacheMinishopMediaUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.hc.HttpComponentsMinishopMediaUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.jodd.JoddHttpMinishopMediaUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.okhttp.OkHttpMinishopMediaUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.okhttp.OkHttpProxyInfo; +import okhttp3.OkHttpClient; + +import java.io.File; +import java.io.IOException; + +public abstract class MinishopUploadRequestExecutor implements RequestExecutor { + protected RequestHttp requestHttp; + + public MinishopUploadRequestExecutor(RequestHttp requestHttp) { + this.requestHttp = requestHttp; + } + + @Override + public void execute(String uri, File data, ResponseHandler handler, WxType wxType) throws WxErrorException, IOException { + handler.handle(this.execute(uri, data, wxType)); + } + + @SuppressWarnings("unchecked") + public static RequestExecutor create(RequestHttp requestHttp) { + switch (requestHttp.getRequestType()) { + case APACHE_HTTP: + return new ApacheMinishopMediaUploadRequestExecutor( + (RequestHttp) requestHttp); + case JODD_HTTP: + return new JoddHttpMinishopMediaUploadRequestExecutor((RequestHttp) requestHttp); + case OK_HTTP: + return new OkHttpMinishopMediaUploadRequestExecutor((RequestHttp) requestHttp); + case HTTP_COMPONENTS: + return new HttpComponentsMinishopMediaUploadRequestExecutor( + (RequestHttp) requestHttp); + default: + throw new IllegalArgumentException("不支持的http执行器类型:" + requestHttp.getRequestType()); + } + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/RequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/RequestExecutor.java index 3d0b772f94..b5e394756e 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/RequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/RequestExecutor.java @@ -1,6 +1,6 @@ package me.chanjar.weixin.common.util.http; -import me.chanjar.weixin.common.WxType; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxErrorException; import java.io.IOException; @@ -37,4 +37,6 @@ public interface RequestExecutor { * @throws IOException io异常 */ void execute(String uri, E data, ResponseHandler handler, WxType wxType) throws WxErrorException, IOException; + + } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/RequestHttp.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/RequestHttp.java index b7bc850f8f..36be78b8ae 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/RequestHttp.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/RequestHttp.java @@ -26,6 +26,6 @@ public interface RequestHttp { * * @return HttpType */ - HttpType getRequestType(); + HttpClientType getRequestType(); } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimpleGetRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimpleGetRequestExecutor.java index 05c396e029..a880a9323c 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimpleGetRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimpleGetRequestExecutor.java @@ -1,12 +1,18 @@ package me.chanjar.weixin.common.util.http; -import java.io.IOException; - -import me.chanjar.weixin.common.WxType; +import jodd.http.HttpConnectionProvider; +import jodd.http.ProxyInfo; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; -import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientSimpleGetRequestExecutor; +import me.chanjar.weixin.common.util.http.apache.ApacheSimpleGetRequestExecutor; +import me.chanjar.weixin.common.util.http.hc.HttpComponentsSimpleGetRequestExecutor; import me.chanjar.weixin.common.util.http.jodd.JoddHttpSimpleGetRequestExecutor; +import me.chanjar.weixin.common.util.http.okhttp.OkHttpProxyInfo; import me.chanjar.weixin.common.util.http.okhttp.OkHttpSimpleGetRequestExecutor; +import okhttp3.OkHttpClient; + +import java.io.IOException; /** * 简单的GET请求执行器. @@ -26,17 +32,30 @@ public void execute(String uri, String data, ResponseHandler handler, Wx handler.handle(this.execute(uri, data, wxType)); } - public static RequestExecutor create(RequestHttp requestHttp) { + @SuppressWarnings("unchecked") + public static RequestExecutor create(RequestHttp requestHttp) { switch (requestHttp.getRequestType()) { case APACHE_HTTP: - return new ApacheHttpClientSimpleGetRequestExecutor(requestHttp); + return new ApacheSimpleGetRequestExecutor( + (RequestHttp) requestHttp); case JODD_HTTP: - return new JoddHttpSimpleGetRequestExecutor(requestHttp); + return new JoddHttpSimpleGetRequestExecutor((RequestHttp) requestHttp); case OK_HTTP: - return new OkHttpSimpleGetRequestExecutor(requestHttp); + return new OkHttpSimpleGetRequestExecutor((RequestHttp) requestHttp); + case HTTP_COMPONENTS: + return new HttpComponentsSimpleGetRequestExecutor( + (RequestHttp) requestHttp); default: - throw new IllegalArgumentException("非法请求参数"); + throw new IllegalArgumentException("不支持的http执行器类型:" + requestHttp.getRequestType()); } } + protected String handleResponse(WxType wxType, String responseContent) throws WxErrorException { + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + + return responseContent; + } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimplePostRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimplePostRequestExecutor.java index 692387f9f2..2cc086cd0f 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimplePostRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/SimplePostRequestExecutor.java @@ -1,15 +1,21 @@ package me.chanjar.weixin.common.util.http; -import java.io.IOException; - -import me.chanjar.weixin.common.WxType; +import jodd.http.HttpConnectionProvider; +import jodd.http.ProxyInfo; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.util.http.apache.ApacheSimplePostRequestExecutor; +import me.chanjar.weixin.common.util.http.hc.HttpComponentsSimplePostRequestExecutor; import me.chanjar.weixin.common.util.http.jodd.JoddHttpSimplePostRequestExecutor; +import me.chanjar.weixin.common.util.http.okhttp.OkHttpProxyInfo; import me.chanjar.weixin.common.util.http.okhttp.OkHttpSimplePostRequestExecutor; +import okhttp3.OkHttpClient; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; /** - * 用装饰模式实现 * 简单的POST请求执行器,请求的参数是String, 返回的结果也是String * * @author Daniel Qian @@ -17,7 +23,7 @@ public abstract class SimplePostRequestExecutor implements RequestExecutor { protected RequestHttp requestHttp; - public SimplePostRequestExecutor(RequestHttp requestHttp) { + public SimplePostRequestExecutor(RequestHttp requestHttp) { this.requestHttp = requestHttp; } @@ -27,17 +33,39 @@ public void execute(String uri, String data, ResponseHandler handler, Wx handler.handle(this.execute(uri, data, wxType)); } - public static RequestExecutor create(RequestHttp requestHttp) { + @SuppressWarnings("unchecked") + public static RequestExecutor create(RequestHttp requestHttp) { switch (requestHttp.getRequestType()) { case APACHE_HTTP: - return new ApacheSimplePostRequestExecutor(requestHttp); + return new ApacheSimplePostRequestExecutor( + (RequestHttp) requestHttp); case JODD_HTTP: - return new JoddHttpSimplePostRequestExecutor(requestHttp); + return new JoddHttpSimplePostRequestExecutor((RequestHttp) requestHttp); case OK_HTTP: - return new OkHttpSimplePostRequestExecutor(requestHttp); + return new OkHttpSimplePostRequestExecutor((RequestHttp) requestHttp); + case HTTP_COMPONENTS: + return new HttpComponentsSimplePostRequestExecutor( + (RequestHttp) requestHttp); default: - throw new IllegalArgumentException("非法请求参数"); + throw new IllegalArgumentException("不支持的http执行器类型:" + requestHttp.getRequestType()); } } + @NotNull + public String handleResponse(WxType wxType, String responseContent) throws WxErrorException { + if (responseContent.isEmpty()) { + throw new WxErrorException("无响应内容"); + } + + if (responseContent.startsWith("")) { + //xml格式输出直接返回 + return responseContent; + } + + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + return responseContent; + } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/WxDnsResolver.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/WxDnsResolver.java index 6c6137089a..ff0977e8d8 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/WxDnsResolver.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/WxDnsResolver.java @@ -18,8 +18,8 @@ */ public class WxDnsResolver implements DnsResolver { - private final static String WECHAT_API_URL = "api.weixin.qq.com"; - private static Map MAPPINGS = new HashMap(); + private static final String WECHAT_API_URL = "api.weixin.qq.com"; + private static Map MAPPINGS = new HashMap<>(); protected final Logger log = LoggerFactory.getLogger(WxDnsResolver.class); private String wxApiIp; @@ -38,7 +38,7 @@ private void init() { } catch (UnknownHostException e) { //如果初始化DNS配置失败则使用默认配置,不影响服务的启动 log.error("init WxDnsResolver error", e); - MAPPINGS = new HashMap(); + MAPPINGS = new HashMap<>(); } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheBasicResponseHandler.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheBasicResponseHandler.java new file mode 100644 index 0000000000..a91fc383ca --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheBasicResponseHandler.java @@ -0,0 +1,9 @@ +package me.chanjar.weixin.common.util.http.apache; + +import org.apache.http.impl.client.BasicResponseHandler; + +public class ApacheBasicResponseHandler extends BasicResponseHandler { + + public static final ApacheBasicResponseHandler INSTANCE = new ApacheBasicResponseHandler(); + +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpClientBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpClientBuilder.java index fcd56c48a7..5b13e7cc17 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpClientBuilder.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpClientBuilder.java @@ -1,5 +1,7 @@ package me.chanjar.weixin.common.util.http.apache; +import org.apache.http.client.HttpRequestRetryHandler; +import org.apache.http.conn.ConnectionKeepAliveStrategy; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; @@ -19,26 +21,66 @@ public interface ApacheHttpClientBuilder { /** * 代理服务器地址. + * + * @param httpProxyHost 代理服务器地址 + * @return ApacheHttpClientBuilder */ ApacheHttpClientBuilder httpProxyHost(String httpProxyHost); /** * 代理服务器端口. + * + * @param httpProxyPort 代理服务器端口 + * @return ApacheHttpClientBuilder */ ApacheHttpClientBuilder httpProxyPort(int httpProxyPort); /** * 代理服务器用户名. + * + * @param httpProxyUsername 代理服务器用户名 + * @return ApacheHttpClientBuilder */ ApacheHttpClientBuilder httpProxyUsername(String httpProxyUsername); /** * 代理服务器密码. + * + * @param httpProxyPassword 代理服务器密码 + * @return ApacheHttpClientBuilder */ ApacheHttpClientBuilder httpProxyPassword(String httpProxyPassword); + /** + * 重试策略. + * + * @param httpRequestRetryHandler 重试处理器 + * @return ApacheHttpClientBuilder + */ + ApacheHttpClientBuilder httpRequestRetryHandler(HttpRequestRetryHandler httpRequestRetryHandler); + + /** + * 超时时间. + * + * @param keepAliveStrategy 保持连接策略 + * @return ApacheHttpClientBuilder + */ + ApacheHttpClientBuilder keepAliveStrategy(ConnectionKeepAliveStrategy keepAliveStrategy); + /** * ssl连接socket工厂. + * + * @param sslConnectionSocketFactory SSL连接Socket工厂 + * @return ApacheHttpClientBuilder */ ApacheHttpClientBuilder sslConnectionSocketFactory(SSLConnectionSocketFactory sslConnectionSocketFactory); + + /** + * 支持的TLS协议版本. + * Supported TLS protocol versions. + * + * @param supportedProtocols 支持的协议版本数组 + * @return ApacheHttpClientBuilder + */ + ApacheHttpClientBuilder supportedProtocols(String[] supportedProtocols); } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpClientSimpleGetRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpClientSimpleGetRequestExecutor.java deleted file mode 100644 index cc830013d2..0000000000 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpClientSimpleGetRequestExecutor.java +++ /dev/null @@ -1,53 +0,0 @@ -package me.chanjar.weixin.common.util.http.apache; - -import me.chanjar.weixin.common.WxType; -import me.chanjar.weixin.common.error.WxError; -import me.chanjar.weixin.common.error.WxErrorException; -import me.chanjar.weixin.common.util.http.RequestHttp; -import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor; -import org.apache.http.HttpHost; -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.impl.client.CloseableHttpClient; - -import java.io.IOException; - -/** - * . - * - * @author ecoolper - * @date 2017/5/4 - */ -public class ApacheHttpClientSimpleGetRequestExecutor extends SimpleGetRequestExecutor { - public ApacheHttpClientSimpleGetRequestExecutor(RequestHttp requestHttp) { - super(requestHttp); - } - - @Override - public String execute(String uri, String queryParam, WxType wxType) throws WxErrorException, IOException { - if (queryParam != null) { - if (uri.indexOf('?') == -1) { - uri += '?'; - } - uri += uri.endsWith("?") ? queryParam : '&' + queryParam; - } - HttpGet httpGet = new HttpGet(uri); - if (requestHttp.getRequestHttpProxy() != null) { - RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build(); - httpGet.setConfig(config); - } - - try (CloseableHttpResponse response = requestHttp.getRequestHttpClient().execute(httpGet)) { - String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); - WxError error = WxError.fromJson(responseContent, wxType); - if (error.getErrorCode() != 0) { - throw new WxErrorException(error); - } - return responseContent; - } finally { - httpGet.releaseConnection(); - } - } - -} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpDnsClientBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpDnsClientBuilder.java index fe5472f3c0..b3ebf350be 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpDnsClientBuilder.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpDnsClientBuilder.java @@ -1,12 +1,7 @@ package me.chanjar.weixin.common.util.http.apache; -import java.io.IOException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; - import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpHost; -import org.apache.http.annotation.NotThreadSafe; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; @@ -15,6 +10,7 @@ import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.config.SocketConfig; +import org.apache.http.conn.ConnectionKeepAliveStrategy; import org.apache.http.conn.DnsResolver; import org.apache.http.conn.HttpClientConnectionManager; import org.apache.http.conn.socket.ConnectionSocketFactory; @@ -29,6 +25,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.concurrent.NotThreadSafe; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + /** * httpclient 连接管理器 自带DNS解析. *

大部分代码拷贝自:DefaultApacheHttpClientBuilder

@@ -48,21 +49,19 @@ public class ApacheHttpDnsClientBuilder implements ApacheHttpClientBuilder { private int maxTotalConn = 50; private String userAgent; - private DnsResolver dnsResover; + private DnsResolver dnsResolver; - private HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() { - @Override - public boolean retryRequest(IOException exception, int executionCount, HttpContext context) { - return false; - } - }; + private HttpRequestRetryHandler httpRequestRetryHandler = (IOException exception, int executionCount, HttpContext context) -> false; private SSLConnectionSocketFactory sslConnectionSocketFactory = SSLConnectionSocketFactory.getSocketFactory(); private PlainConnectionSocketFactory plainConnectionSocketFactory = PlainConnectionSocketFactory.getSocketFactory(); private String httpProxyHost; + private ConnectionKeepAliveStrategy keepAliveStrategy; + private int httpProxyPort; private String httpProxyUsername; private String httpProxyPassword; + /** * 闲置连接监控线程. */ @@ -100,12 +99,31 @@ public ApacheHttpClientBuilder httpProxyPassword(String httpProxyPassword) { return this; } + @Override + public ApacheHttpClientBuilder httpRequestRetryHandler(HttpRequestRetryHandler httpRequestRetryHandler) { + this.httpRequestRetryHandler = httpRequestRetryHandler; + return this; + } + + @Override + public ApacheHttpClientBuilder keepAliveStrategy(ConnectionKeepAliveStrategy keepAliveStrategy) { + this.keepAliveStrategy = keepAliveStrategy; + return this; + } + @Override public ApacheHttpClientBuilder sslConnectionSocketFactory(SSLConnectionSocketFactory sslConnectionSocketFactory) { this.sslConnectionSocketFactory = sslConnectionSocketFactory; return this; } + @Override + public ApacheHttpClientBuilder supportedProtocols(String[] supportedProtocols) { + // This implementation doesn't use the supportedProtocols parameter as it relies on the provided SSLConnectionSocketFactory + // Users should configure the SSLConnectionSocketFactory with desired protocols before setting it + return this; + } + /** * 获取链接的超时时间设置,默认3000ms *

@@ -205,11 +223,11 @@ private synchronized void prepare() { @SuppressWarnings("resource") PoolingHttpClientConnectionManager connectionManager; - if (dnsResover != null) { + if (dnsResolver != null) { if (log.isDebugEnabled()) { log.debug("specified dns resolver."); } - connectionManager = new PoolingHttpClientConnectionManager(registry, dnsResover); + connectionManager = new PoolingHttpClientConnectionManager(registry, dnsResolver); } else { if (log.isDebugEnabled()) { log.debug("Not specified dns resolver."); @@ -257,12 +275,12 @@ public CloseableHttpClient build() { return this.httpClientBuilder.build(); } - public DnsResolver getDnsResover() { - return dnsResover; + public DnsResolver getDnsResolver() { + return dnsResolver; } - public void setDnsResover(DnsResolver dnsResover) { - this.dnsResover = dnsResover; + public void setDnsResolver(DnsResolver dnsResolver) { + this.dnsResolver = dnsResolver; } public static class IdleConnectionMonitorThread extends Thread { diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpResponseProxy.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpResponseProxy.java new file mode 100644 index 0000000000..06439d3879 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpResponseProxy.java @@ -0,0 +1,25 @@ +package me.chanjar.weixin.common.util.http.apache; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.HttpResponseProxy; +import org.apache.http.Header; +import org.apache.http.client.methods.CloseableHttpResponse; + +public class ApacheHttpResponseProxy implements HttpResponseProxy { + + private final CloseableHttpResponse httpResponse; + + public ApacheHttpResponseProxy(CloseableHttpResponse closeableHttpResponse) { + this.httpResponse = closeableHttpResponse; + } + + @Override + public String getFileName() throws WxErrorException { + Header[] contentDispositionHeader = this.httpResponse.getHeaders("Content-disposition"); + if (contentDispositionHeader == null || contentDispositionHeader.length == 0) { + throw new WxErrorException("无法获取到文件名,Content-disposition为空"); + } + + return HttpResponseProxy.extractFileNameFromContentString(contentDispositionHeader[0].getValue()); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaDownloadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaDownloadRequestExecutor.java index 51c37f59a5..554dc8df7b 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaDownloadRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaDownloadRequestExecutor.java @@ -1,6 +1,6 @@ package me.chanjar.weixin.common.util.http.apache; -import me.chanjar.weixin.common.WxType; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.util.fs.FileUtils; @@ -25,10 +25,10 @@ * . * * @author ecoolper - * @date 2017/5/5 + * created on 2017/5/5 */ public class ApacheMediaDownloadRequestExecutor extends BaseMediaDownloadRequestExecutor { - public ApacheMediaDownloadRequestExecutor(RequestHttp requestHttp, File tmpDirFile) { + public ApacheMediaDownloadRequestExecutor(RequestHttp requestHttp, File tmpDirFile) { super(requestHttp, tmpDirFile); } @@ -58,16 +58,17 @@ public File execute(String uri, String queryParam, WxType wxType) throws WxError } } - String fileName = new HttpResponseProxy(response).getFileName(); + String fileName = HttpResponseProxy.from(response).getFileName(); if (StringUtils.isBlank(fileName)) { fileName = String.valueOf(System.currentTimeMillis()); } - return FileUtils.createTmpFile(inputStream, FilenameUtils.getBaseName(fileName), FilenameUtils.getExtension(fileName), - super.tmpDirFile); + String baseName = FilenameUtils.getBaseName(fileName); + if (StringUtils.isBlank(fileName) || baseName.length() < 3) { + baseName = String.valueOf(System.currentTimeMillis()); + } - } finally { - httpGet.releaseConnection(); + return FileUtils.createTmpFile(inputStream, baseName, FilenameUtils.getExtension(fileName), super.tmpDirFile); } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaInputStreamUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaInputStreamUploadRequestExecutor.java new file mode 100644 index 0000000000..43a5d604b0 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaInputStreamUploadRequestExecutor.java @@ -0,0 +1,54 @@ +package me.chanjar.weixin.common.util.http.apache; + +import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.InputStreamData; +import me.chanjar.weixin.common.util.http.MediaInputStreamUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.impl.client.CloseableHttpClient; + +import java.io.IOException; + +/** + * 文件输入流上传. + * + * @author meiqin.zhou91@gmail.com + * created on 2022/02/15 + */ +public class ApacheMediaInputStreamUploadRequestExecutor extends MediaInputStreamUploadRequestExecutor { + public ApacheMediaInputStreamUploadRequestExecutor(RequestHttp requestHttp) { + super(requestHttp); + } + + @Override + public WxMediaUploadResult execute(String uri, InputStreamData data, WxType wxType) throws WxErrorException, IOException { + HttpPost httpPost = new HttpPost(uri); + if (requestHttp.getRequestHttpProxy() != null) { + RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build(); + httpPost.setConfig(config); + } + if (data != null) { + HttpEntity entity = MultipartEntityBuilder + .create() + .addBinaryBody("media", data.getInputStream(), ContentType.DEFAULT_BINARY, data.getFilename()) + .setMode(HttpMultipartMode.RFC6532) + .build(); + httpPost.setEntity(entity); + } + String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE); + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + return WxMediaUploadResult.fromJson(responseContent); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaUploadRequestExecutor.java index 3c1b15dc9a..5d3eae174f 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaUploadRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaUploadRequestExecutor.java @@ -1,7 +1,7 @@ package me.chanjar.weixin.common.util.http.apache; -import me.chanjar.weixin.common.WxType; import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor; @@ -9,7 +9,6 @@ import org.apache.http.HttpEntity; import org.apache.http.HttpHost; import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.mime.HttpMultipartMode; import org.apache.http.entity.mime.MultipartEntityBuilder; @@ -22,7 +21,7 @@ * Created by ecoolper on 2017/5/5. */ public class ApacheMediaUploadRequestExecutor extends MediaUploadRequestExecutor { - public ApacheMediaUploadRequestExecutor(RequestHttp requestHttp) { + public ApacheMediaUploadRequestExecutor(RequestHttp requestHttp) { super(requestHttp); } @@ -41,15 +40,11 @@ public WxMediaUploadResult execute(String uri, File file, WxType wxType) throws .build(); httpPost.setEntity(entity); } - try (CloseableHttpResponse response = requestHttp.getRequestHttpClient().execute(httpPost)) { - String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); - WxError error = WxError.fromJson(responseContent, wxType); - if (error.getErrorCode() != 0) { - throw new WxErrorException(error); - } - return WxMediaUploadResult.fromJson(responseContent); - } finally { - httpPost.releaseConnection(); + String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE); + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); } + return WxMediaUploadResult.fromJson(responseContent); } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMinishopMediaUploadRequestCustomizeExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMinishopMediaUploadRequestCustomizeExecutor.java new file mode 100644 index 0000000000..48fafc3401 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMinishopMediaUploadRequestCustomizeExecutor.java @@ -0,0 +1,68 @@ +package me.chanjar.weixin.common.util.http.apache; + +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.result.WxMinishopImageUploadCustomizeResult; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.MinishopUploadRequestCustomizeExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.impl.client.CloseableHttpClient; + +import java.io.File; +import java.io.IOException; + +/** + * Created by liming1019 on 2021/8/10. + */ +@Slf4j +public class ApacheMinishopMediaUploadRequestCustomizeExecutor extends MinishopUploadRequestCustomizeExecutor { + public ApacheMinishopMediaUploadRequestCustomizeExecutor(RequestHttp requestHttp, String respType, String imgUrl) { + super(requestHttp, respType, imgUrl); + } + + @Override + public WxMinishopImageUploadCustomizeResult execute(String uri, File file, WxType wxType) throws WxErrorException, IOException { + HttpPost httpPost = new HttpPost(uri); + if (requestHttp.getRequestHttpProxy() != null) { + RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build(); + httpPost.setConfig(config); + } + if (this.uploadType.equals("0")) { + if (file == null) { + throw new WxErrorException("上传文件为空"); + } + HttpEntity entity = MultipartEntityBuilder + .create() + .addBinaryBody("media", file) + .addTextBody("resp_type", this.respType) + .addTextBody("upload_type", this.uploadType) + .setMode(HttpMultipartMode.RFC6532) + .build(); + httpPost.setEntity(entity); + } + else { + HttpEntity entity = MultipartEntityBuilder + .create() + .addTextBody("resp_type", this.respType) + .addTextBody("upload_type", this.uploadType) + .addTextBody("img_url", this.imgUrl) + .setMode(HttpMultipartMode.RFC6532) + .build(); + httpPost.setEntity(entity); + } + String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE); + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + log.info("responseContent: {}", responseContent); + return WxMinishopImageUploadCustomizeResult.fromJson(responseContent); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMinishopMediaUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMinishopMediaUploadRequestExecutor.java new file mode 100644 index 0000000000..f76d4e8642 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMinishopMediaUploadRequestExecutor.java @@ -0,0 +1,53 @@ +package me.chanjar.weixin.common.util.http.apache; + +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.result.WxMinishopImageUploadResult; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.MinishopUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; +import org.apache.http.HttpEntity; +import org.apache.http.HttpHost; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.impl.client.CloseableHttpClient; + +import java.io.File; +import java.io.IOException; + +/** + * Created by ecoolper on 2017/5/5. + */ +@Slf4j +public class ApacheMinishopMediaUploadRequestExecutor extends MinishopUploadRequestExecutor { + public ApacheMinishopMediaUploadRequestExecutor(RequestHttp requestHttp) { + super(requestHttp); + } + + @Override + public WxMinishopImageUploadResult execute(String uri, File file, WxType wxType) throws WxErrorException, IOException { + HttpPost httpPost = new HttpPost(uri); + if (requestHttp.getRequestHttpProxy() != null) { + RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build(); + httpPost.setConfig(config); + } + if (file != null) { + HttpEntity entity = MultipartEntityBuilder + .create() + .addBinaryBody("media", file) + .setMode(HttpMultipartMode.RFC6532) + .build(); + httpPost.setEntity(entity); + } + String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE); + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + log.info("responseContent: {}", responseContent); + return WxMinishopImageUploadResult.fromJson(responseContent); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheSimpleGetRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheSimpleGetRequestExecutor.java new file mode 100644 index 0000000000..08ccf1b252 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheSimpleGetRequestExecutor.java @@ -0,0 +1,43 @@ +package me.chanjar.weixin.common.util.http.apache; + +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.RequestHttp; +import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor; +import org.apache.http.HttpHost; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; + +import java.io.IOException; + +/** + * . + * + * @author ecoolper + * created on 2017/5/4 + */ +public class ApacheSimpleGetRequestExecutor extends SimpleGetRequestExecutor { + public ApacheSimpleGetRequestExecutor(RequestHttp requestHttp) { + super(requestHttp); + } + + @Override + public String execute(String uri, String queryParam, WxType wxType) throws WxErrorException, IOException { + if (queryParam != null) { + if (uri.indexOf('?') == -1) { + uri += '?'; + } + uri += uri.endsWith("?") ? queryParam : '&' + queryParam; + } + HttpGet httpGet = new HttpGet(uri); + if (requestHttp.getRequestHttpProxy() != null) { + RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build(); + httpGet.setConfig(config); + } + + String responseContent = requestHttp.getRequestHttpClient().execute(httpGet, Utf8ResponseHandler.INSTANCE); + return handleResponse(wxType, responseContent); + } + +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheSimplePostRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheSimplePostRequestExecutor.java index 3405aaab78..65af81690d 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheSimplePostRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheSimplePostRequestExecutor.java @@ -1,28 +1,27 @@ package me.chanjar.weixin.common.util.http.apache; -import me.chanjar.weixin.common.WxType; -import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.util.http.RequestHttp; import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor; -import org.apache.http.Consts; import org.apache.http.HttpHost; import org.apache.http.client.config.RequestConfig; -import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import java.io.IOException; +import java.nio.charset.StandardCharsets; /** * . * * @author ecoolper - * @date 2017/5/4 + * created on 2017/5/4 */ public class ApacheSimplePostRequestExecutor extends SimplePostRequestExecutor { - public ApacheSimplePostRequestExecutor(RequestHttp requestHttp) { + public ApacheSimplePostRequestExecutor(RequestHttp requestHttp) { super(requestHttp); } @@ -35,28 +34,12 @@ public String execute(String uri, String postEntity, WxType wxType) throws WxErr } if (postEntity != null) { - StringEntity entity = new StringEntity(postEntity, Consts.UTF_8); + StringEntity entity = new StringEntity(postEntity, ContentType.APPLICATION_JSON.withCharset(StandardCharsets.UTF_8)); httpPost.setEntity(entity); } - try (CloseableHttpResponse response = requestHttp.getRequestHttpClient().execute(httpPost)) { - String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); - if (responseContent.isEmpty()) { - throw new WxErrorException(WxError.builder().errorCode(9999).errorMsg("无响应内容").build()); - } - - if (responseContent.startsWith("")) { - //xml格式输出直接返回 - return responseContent; - } - - WxError error = WxError.fromJson(responseContent, wxType); - if (error.getErrorCode() != 0) { - throw new WxErrorException(error); - } - return responseContent; - } finally { - httpPost.releaseConnection(); - } + String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE); + return this.handleResponse(wxType, responseContent); } + } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ByteArrayResponseHandler.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ByteArrayResponseHandler.java new file mode 100644 index 0000000000..776b7e32f1 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ByteArrayResponseHandler.java @@ -0,0 +1,17 @@ +package me.chanjar.weixin.common.util.http.apache; + +import org.apache.http.HttpEntity; +import org.apache.http.impl.client.AbstractResponseHandler; +import org.apache.http.util.EntityUtils; + +import java.io.IOException; + +public class ByteArrayResponseHandler extends AbstractResponseHandler { + + public static final ByteArrayResponseHandler INSTANCE = new ByteArrayResponseHandler(); + + @Override + public byte[] handleEntity(HttpEntity entity) throws IOException { + return EntityUtils.toByteArray(entity); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilder.java index dfca21a7b2..ef7120b768 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilder.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilder.java @@ -1,8 +1,11 @@ package me.chanjar.weixin.common.util.http.apache; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpHost; -import org.apache.http.annotation.NotThreadSafe; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.HttpResponseInterceptor; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; @@ -11,6 +14,7 @@ import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.config.SocketConfig; +import org.apache.http.conn.ConnectionKeepAliveStrategy; import org.apache.http.conn.HttpClientConnectionManager; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; @@ -21,18 +25,15 @@ import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; -import org.apache.http.protocol.HttpContext; import org.apache.http.ssl.SSLContexts; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import javax.annotation.concurrent.NotThreadSafe; import javax.net.ssl.SSLContext; -import java.io.IOException; import java.security.KeyManagementException; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; @@ -41,26 +42,87 @@ * * @author kakotor */ +@Slf4j +@Data @NotThreadSafe public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder { - protected final Logger log = LoggerFactory.getLogger(DefaultApacheHttpClientBuilder.class); private final AtomicBoolean prepared = new AtomicBoolean(false); + + /** + * 获取链接的超时时间设置 + *

+ * 设置为零时不超时,一直等待. + * 设置为负数是使用系统默认设置(非3000ms的默认值,而是httpClient的默认设置). + *

+ */ private int connectionRequestTimeout = 3000; + + /** + * 建立链接的超时时间,默认为5000ms.由于是在链接池获取链接,此设置应该并不起什么作用 + *

+ * 设置为零时不超时,一直等待. + * 设置为负数是使用系统默认设置(非上述的5000ms的默认值,而是httpclient的默认设置). + *

+ */ private int connectionTimeout = 5000; + /** + * 默认NIO的socket超时设置,默认5000ms. + */ private int soTimeout = 5000; + /** + * 空闲链接的超时时间,默认60000ms. + *

+ * 超时的链接将在下一次空闲链接检查是被销毁 + *

+ */ private int idleConnTimeout = 60000; + /** + * 检查空间链接的间隔周期,默认60000ms. + */ private int checkWaitTime = 60000; + /** + * 每路的最大链接数,默认10 + */ private int maxConnPerHost = 10; + /** + * 最大总连接数,默认50 + */ private int maxTotalConn = 50; + /** + * 自定义httpclient的User Agent + */ private String userAgent; - private HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() { - @Override - public boolean retryRequest(IOException exception, int executionCount, HttpContext context) { - return false; - } - }; + + /** + * 支持的TLS协议版本,默认支持现代TLS版本 + * Supported TLS protocol versions, defaults to modern TLS versions + */ + private String[] supportedProtocols = {"TLSv1.2", "TLSv1.3", "TLSv1.1", "TLSv1"}; + + /** + * 自定义请求拦截器 + */ + private List requestInterceptors = new ArrayList<>(); + + /** + * 自定义响应拦截器 + */ + private List responseInterceptors = new ArrayList<>(); + + /** + * 自定义重试策略 + */ + private HttpRequestRetryHandler httpRequestRetryHandler; + + /** + * 自定义KeepAlive策略 + */ + private ConnectionKeepAliveStrategy connectionKeepAliveStrategy; + + private final HttpRequestRetryHandler defaultHttpRequestRetryHandler = (exception, executionCount, context) -> false; + private SSLConnectionSocketFactory sslConnectionSocketFactory = SSLConnectionSocketFactory.getSocketFactory(); - private PlainConnectionSocketFactory plainConnectionSocketFactory = PlainConnectionSocketFactory.getSocketFactory(); + private final PlainConnectionSocketFactory plainConnectionSocketFactory = PlainConnectionSocketFactory.getSocketFactory(); private String httpProxyHost; private int httpProxyPort; private String httpProxyUsername; @@ -106,93 +168,27 @@ public ApacheHttpClientBuilder httpProxyPassword(String httpProxyPassword) { } @Override - public ApacheHttpClientBuilder sslConnectionSocketFactory(SSLConnectionSocketFactory sslConnectionSocketFactory) { - this.sslConnectionSocketFactory = sslConnectionSocketFactory; + public ApacheHttpClientBuilder httpRequestRetryHandler(HttpRequestRetryHandler httpRequestRetryHandler) { + this.httpRequestRetryHandler = httpRequestRetryHandler; return this; } - /** - * 获取链接的超时时间设置,默认3000ms - *

- * 设置为零时不超时,一直等待. - * 设置为负数是使用系统默认设置(非上述的3000ms的默认值,而是httpclient的默认设置). - *

- * - * @param connectionRequestTimeout 获取链接的超时时间设置(单位毫秒),默认3000ms - */ - public void setConnectionRequestTimeout(int connectionRequestTimeout) { - this.connectionRequestTimeout = connectionRequestTimeout; - } - - /** - * 建立链接的超时时间,默认为5000ms.由于是在链接池获取链接,此设置应该并不起什么作用 - *

- * 设置为零时不超时,一直等待. - * 设置为负数是使用系统默认设置(非上述的5000ms的默认值,而是httpclient的默认设置). - *

- * - * @param connectionTimeout 建立链接的超时时间设置(单位毫秒),默认5000ms - */ - public void setConnectionTimeout(int connectionTimeout) { - this.connectionTimeout = connectionTimeout; - } - - /** - * 默认NIO的socket超时设置,默认5000ms. - * - * @param soTimeout 默认NIO的socket超时设置,默认5000ms. - * @see java.net.SocketOptions#SO_TIMEOUT - */ - public void setSoTimeout(int soTimeout) { - this.soTimeout = soTimeout; - } - - /** - * 空闲链接的超时时间,默认60000ms. - *

- * 超时的链接将在下一次空闲链接检查是被销毁 - *

- * - * @param idleConnTimeout 空闲链接的超时时间,默认60000ms. - */ - public void setIdleConnTimeout(int idleConnTimeout) { - this.idleConnTimeout = idleConnTimeout; - } - - /** - * 检查空间链接的间隔周期,默认60000ms. - * - * @param checkWaitTime 检查空间链接的间隔周期,默认60000ms. - */ - public void setCheckWaitTime(int checkWaitTime) { - this.checkWaitTime = checkWaitTime; - } - - /** - * 每路的最大链接数,默认10 - * - * @param maxConnPerHost 每路的最大链接数,默认10 - */ - public void setMaxConnPerHost(int maxConnPerHost) { - this.maxConnPerHost = maxConnPerHost; + @Override + public ApacheHttpClientBuilder keepAliveStrategy(ConnectionKeepAliveStrategy keepAliveStrategy) { + this.connectionKeepAliveStrategy = keepAliveStrategy; + return this; } - /** - * 最大总连接数,默认50 - * - * @param maxTotalConn 最大总连接数,默认50 - */ - public void setMaxTotalConn(int maxTotalConn) { - this.maxTotalConn = maxTotalConn; + @Override + public ApacheHttpClientBuilder sslConnectionSocketFactory(SSLConnectionSocketFactory sslConnectionSocketFactory) { + this.sslConnectionSocketFactory = sslConnectionSocketFactory; + return this; } - /** - * 自定义httpclient的User Agent - * - * @param userAgent User Agent - */ - public void setUserAgent(String userAgent) { - this.userAgent = userAgent; + @Override + public ApacheHttpClientBuilder supportedProtocols(String[] supportedProtocols) { + this.supportedProtocols = supportedProtocols; + return this; } public IdleConnectionMonitorThread getIdleConnectionMonitorThread() { @@ -208,7 +204,6 @@ private synchronized void prepare() { .register("https", this.sslConnectionSocketFactory) .build(); - @SuppressWarnings("resource") PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry); connectionManager.setMaxTotal(this.maxTotalConn); connectionManager.setDefaultMaxPerRoute(this.maxConnPerHost); @@ -232,7 +227,16 @@ private synchronized void prepare() { .setConnectTimeout(this.connectionTimeout) .setConnectionRequestTimeout(this.connectionRequestTimeout) .build() - ).setRetryHandler(this.httpRequestRetryHandler); + ); + + // 设置重试策略,没有则使用默认 + httpRequestRetryHandler = httpRequestRetryHandler == null ? defaultHttpRequestRetryHandler : httpRequestRetryHandler; + httpClientBuilder.setRetryHandler(httpRequestRetryHandler); + + // 设置KeepAliveStrategy,没有使用默认 + if (connectionKeepAliveStrategy != null) { + httpClientBuilder.setKeepAliveStrategy(connectionKeepAliveStrategy); + } if (StringUtils.isNotBlank(this.httpProxyHost) && StringUtils.isNotBlank(this.httpProxyUsername)) { // 使用代理服务器 需要用户认证的代理服务器 @@ -247,6 +251,12 @@ private synchronized void prepare() { httpClientBuilder.setUserAgent(this.userAgent); } + //添加自定义的请求拦截器 + requestInterceptors.forEach(httpClientBuilder::addInterceptorFirst); + + //添加自定义的响应拦截器 + responseInterceptors.forEach(httpClientBuilder::addInterceptorLast); + this.closeableHttpClient = httpClientBuilder.build(); prepared.set(true); } @@ -255,20 +265,15 @@ private SSLConnectionSocketFactory buildSSLConnectionSocketFactory() { try { SSLContext sslcontext = SSLContexts.custom() //忽略掉对服务器端证书的校验 - .loadTrustMaterial(new TrustStrategy() { - @Override - public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException { - return true; - } - }).build(); + .loadTrustMaterial((TrustStrategy) (chain, authType) -> true).build(); return new SSLConnectionSocketFactory( sslcontext, - new String[]{"TLSv1"}, + this.supportedProtocols, null, SSLConnectionSocketFactory.getDefaultHostnameVerifier()); } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) { - this.log.error(e.getMessage(), e); + log.error("构建SSL连接工厂时发生异常!", e); } return null; diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/InputStreamResponseHandler.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/InputStreamResponseHandler.java index 5c72744cb0..1568362611 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/InputStreamResponseHandler.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/InputStreamResponseHandler.java @@ -1,33 +1,23 @@ package me.chanjar.weixin.common.util.http.apache; -import java.io.IOException; -import java.io.InputStream; - import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.StatusLine; -import org.apache.http.client.HttpResponseException; import org.apache.http.client.ResponseHandler; -import org.apache.http.util.EntityUtils; +import org.apache.http.impl.client.AbstractResponseHandler; + +import java.io.IOException; +import java.io.InputStream; /** * 输入流响应处理器. * - * @author Daniel Qian + * @author altusea */ -public class InputStreamResponseHandler implements ResponseHandler { +public class InputStreamResponseHandler extends AbstractResponseHandler { + public static final ResponseHandler INSTANCE = new InputStreamResponseHandler(); - private static final int STATUS_CODE_300 = 300; @Override - public InputStream handleResponse(final HttpResponse response) throws IOException { - final StatusLine statusLine = response.getStatusLine(); - final HttpEntity entity = response.getEntity(); - if (statusLine.getStatusCode() >= STATUS_CODE_300) { - EntityUtils.consume(entity); - throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase()); - } - return entity == null ? null : entity.getContent(); + public InputStream handleEntity(HttpEntity entity) throws IOException { + return entity.getContent(); } - } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/Utf8ResponseHandler.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/Utf8ResponseHandler.java index 697d4695e2..40d96e3ca1 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/Utf8ResponseHandler.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/Utf8ResponseHandler.java @@ -1,33 +1,24 @@ package me.chanjar.weixin.common.util.http.apache; -import org.apache.http.Consts; import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.StatusLine; -import org.apache.http.client.HttpResponseException; import org.apache.http.client.ResponseHandler; +import org.apache.http.impl.client.AbstractResponseHandler; import org.apache.http.util.EntityUtils; import java.io.IOException; +import java.nio.charset.StandardCharsets; /** - * copy from {@link org.apache.http.impl.client.BasicResponseHandler} + * Utf8ResponseHandler * - * @author Daniel Qian + * @author altusea */ -public class Utf8ResponseHandler implements ResponseHandler { +public class Utf8ResponseHandler extends AbstractResponseHandler { public static final ResponseHandler INSTANCE = new Utf8ResponseHandler(); @Override - public String handleResponse(final HttpResponse response) throws IOException { - final StatusLine statusLine = response.getStatusLine(); - final HttpEntity entity = response.getEntity(); - if (statusLine.getStatusCode() >= 300) { - EntityUtils.consume(entity); - throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase()); - } - return entity == null ? null : EntityUtils.toString(entity, Consts.UTF_8); + public String handleEntity(HttpEntity entity) throws IOException { + return EntityUtils.toString(entity, StandardCharsets.UTF_8); } - } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/BasicResponseHandler.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/BasicResponseHandler.java new file mode 100644 index 0000000000..f69e14a240 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/BasicResponseHandler.java @@ -0,0 +1,14 @@ +package me.chanjar.weixin.common.util.http.hc; + +import org.apache.hc.client5.http.impl.classic.BasicHttpClientResponseHandler; + +/** + * ApacheBasicResponseHandler + * + * @author altusea + */ +public class BasicResponseHandler extends BasicHttpClientResponseHandler { + + public static final BasicHttpClientResponseHandler INSTANCE = new BasicHttpClientResponseHandler(); + +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/ByteArrayResponseHandler.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/ByteArrayResponseHandler.java new file mode 100644 index 0000000000..e4a314f866 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/ByteArrayResponseHandler.java @@ -0,0 +1,22 @@ +package me.chanjar.weixin.common.util.http.hc; + +import org.apache.hc.client5.http.impl.classic.AbstractHttpClientResponseHandler; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.io.entity.EntityUtils; + +import java.io.IOException; + +/** + * ByteArrayResponseHandler + * + * @author altusea + */ +public class ByteArrayResponseHandler extends AbstractHttpClientResponseHandler { + + public static final ByteArrayResponseHandler INSTANCE = new ByteArrayResponseHandler(); + + @Override + public byte[] handleEntity(HttpEntity entity) throws IOException { + return EntityUtils.toByteArray(entity); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/DefaultHttpComponentsClientBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/DefaultHttpComponentsClientBuilder.java new file mode 100644 index 0000000000..4915e31a16 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/DefaultHttpComponentsClientBuilder.java @@ -0,0 +1,249 @@ +package me.chanjar.weixin.common.util.http.hc; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; +import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hc.client5.http.ConnectionKeepAliveStrategy; +import org.apache.hc.client5.http.HttpRequestRetryStrategy; +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.config.ConnectionConfig; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.client5.http.ssl.TrustAllStrategy; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.HttpResponseInterceptor; +import org.apache.hc.core5.http.io.SocketConfig; +import org.apache.hc.core5.ssl.SSLContexts; + +import javax.annotation.concurrent.NotThreadSafe; +import javax.net.ssl.SSLContext; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * DefaultApacheHttpClientBuilder + * + * @author altusea + */ +@Slf4j +@Data +@NotThreadSafe +public class DefaultHttpComponentsClientBuilder implements HttpComponentsClientBuilder { + + private final AtomicBoolean prepared = new AtomicBoolean(false); + + /** + * 获取链接的超时时间设置 + *

+ * 设置为零时不超时,一直等待. + * 设置为负数是使用系统默认设置(非3000ms的默认值,而是httpClient的默认设置). + *

+ */ + private int connectionRequestTimeout = 3000; + + /** + * 建立链接的超时时间,默认为5000ms.由于是在链接池获取链接,此设置应该并不起什么作用 + *

+ * 设置为零时不超时,一直等待. + * 设置为负数是使用系统默认设置(非上述的5000ms的默认值,而是httpclient的默认设置). + *

+ */ + private int connectionTimeout = 5000; + /** + * 默认NIO的socket超时设置,默认5000ms. + */ + private int soTimeout = 5000; + /** + * 空闲链接的超时时间,默认60000ms. + *

+ * 超时的链接将在下一次空闲链接检查是被销毁 + *

+ */ + private int idleConnTimeout = 60000; + /** + * 检查空间链接的间隔周期,默认60000ms. + */ + private int checkWaitTime = 60000; + /** + * 每路的最大链接数,默认10 + */ + private int maxConnPerHost = 10; + /** + * 最大总连接数,默认50 + */ + private int maxTotalConn = 50; + /** + * 自定义httpclient的User Agent + */ + private String userAgent; + + /** + * 自定义请求拦截器 + */ + private List requestInterceptors = new ArrayList<>(); + + /** + * 自定义响应拦截器 + */ + private List responseInterceptors = new ArrayList<>(); + + /** + * 自定义重试策略 + */ + private HttpRequestRetryStrategy httpRequestRetryStrategy; + + /** + * 自定义KeepAlive策略 + */ + private ConnectionKeepAliveStrategy connectionKeepAliveStrategy; + + private String httpProxyHost; + private int httpProxyPort; + private String httpProxyUsername; + private char[] httpProxyPassword; + /** + * 持有client对象,仅初始化一次,避免多service实例的时候造成重复初始化的问题 + */ + private CloseableHttpClient closeableHttpClient; + + private DefaultHttpComponentsClientBuilder() { + } + + public static DefaultHttpComponentsClientBuilder get() { + return SingletonHolder.INSTANCE; + } + + @Override + public HttpComponentsClientBuilder httpProxyHost(String httpProxyHost) { + this.httpProxyHost = httpProxyHost; + return this; + } + + @Override + public HttpComponentsClientBuilder httpProxyPort(int httpProxyPort) { + this.httpProxyPort = httpProxyPort; + return this; + } + + @Override + public HttpComponentsClientBuilder httpProxyUsername(String httpProxyUsername) { + this.httpProxyUsername = httpProxyUsername; + return this; + } + + @Override + public HttpComponentsClientBuilder httpProxyPassword(char[] httpProxyPassword) { + this.httpProxyPassword = httpProxyPassword; + return this; + } + + @Override + public HttpComponentsClientBuilder httpRequestRetryStrategy(HttpRequestRetryStrategy httpRequestRetryStrategy) { + this.httpRequestRetryStrategy = httpRequestRetryStrategy; + return this; + } + + @Override + public HttpComponentsClientBuilder keepAliveStrategy(ConnectionKeepAliveStrategy keepAliveStrategy) { + this.connectionKeepAliveStrategy = keepAliveStrategy; + return this; + } + + private synchronized void prepare() { + if (prepared.get()) { + return; + } + + SSLContext sslcontext; + try { + sslcontext = SSLContexts.custom() + .loadTrustMaterial(TrustAllStrategy.INSTANCE) // 忽略对服务器端证书的校验 + .build(); + } catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) { + log.error("构建 SSLContext 时发生异常!", e); + throw new RuntimeException(e); + } + + PoolingHttpClientConnectionManager connManager = PoolingHttpClientConnectionManagerBuilder.create() + .setTlsSocketStrategy(new DefaultClientTlsStrategy(sslcontext, NoopHostnameVerifier.INSTANCE)) + .setMaxConnTotal(this.maxTotalConn) + .setMaxConnPerRoute(this.maxConnPerHost) + .setDefaultSocketConfig(SocketConfig.custom() + .setSoTimeout(this.soTimeout, TimeUnit.MILLISECONDS) + .build()) + .setDefaultConnectionConfig(ConnectionConfig.custom() + .setConnectTimeout(this.connectionTimeout, TimeUnit.MILLISECONDS) + .build()) + .build(); + + HttpClientBuilder httpClientBuilder = HttpClients.custom() + .setConnectionManager(connManager) + .setConnectionManagerShared(true) + .setDefaultRequestConfig(RequestConfig.custom() + .setConnectionRequestTimeout(this.connectionRequestTimeout, TimeUnit.MILLISECONDS) + .build() + ); + + // 设置重试策略,没有则使用默认 + httpClientBuilder.setRetryStrategy(ObjectUtils.defaultIfNull(httpRequestRetryStrategy, NoopRetryStrategy.INSTANCE)); + + // 设置KeepAliveStrategy,没有使用默认 + if (connectionKeepAliveStrategy != null) { + httpClientBuilder.setKeepAliveStrategy(connectionKeepAliveStrategy); + } + + if (StringUtils.isNotBlank(this.httpProxyHost) && StringUtils.isNotBlank(this.httpProxyUsername)) { + // 使用代理服务器 需要用户认证的代理服务器 + BasicCredentialsProvider provider = new BasicCredentialsProvider(); + provider.setCredentials(new AuthScope(this.httpProxyHost, this.httpProxyPort), + new UsernamePasswordCredentials(this.httpProxyUsername, this.httpProxyPassword)); + httpClientBuilder.setDefaultCredentialsProvider(provider); + httpClientBuilder.setProxy(new HttpHost(this.httpProxyHost, this.httpProxyPort)); + } + + if (StringUtils.isNotBlank(this.userAgent)) { + httpClientBuilder.setUserAgent(this.userAgent); + } + + //添加自定义的请求拦截器 + requestInterceptors.forEach(httpClientBuilder::addRequestInterceptorFirst); + + //添加自定义的响应拦截器 + responseInterceptors.forEach(httpClientBuilder::addResponseInterceptorLast); + + this.closeableHttpClient = httpClientBuilder.build(); + prepared.set(true); + } + + @Override + public CloseableHttpClient build() { + if (!prepared.get()) { + prepare(); + } + return this.closeableHttpClient; + } + + /** + * DefaultApacheHttpClientBuilder 改为单例模式,并持有唯一的CloseableHttpClient(仅首次调用创建) + */ + private static class SingletonHolder { + private static final DefaultHttpComponentsClientBuilder INSTANCE = new DefaultHttpComponentsClientBuilder(); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsClientBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsClientBuilder.java new file mode 100644 index 0000000000..66cd58e15f --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsClientBuilder.java @@ -0,0 +1,51 @@ +package me.chanjar.weixin.common.util.http.hc; + +import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; +import org.apache.hc.client5.http.ConnectionKeepAliveStrategy; +import org.apache.hc.client5.http.HttpRequestRetryStrategy; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; + +/** + * httpclient build interface. + * + * @author altusea + */ +public interface HttpComponentsClientBuilder { + + /** + * 构建httpclient实例. + * + * @return new instance of CloseableHttpClient + */ + CloseableHttpClient build(); + + /** + * 代理服务器地址. + */ + HttpComponentsClientBuilder httpProxyHost(String httpProxyHost); + + /** + * 代理服务器端口. + */ + HttpComponentsClientBuilder httpProxyPort(int httpProxyPort); + + /** + * 代理服务器用户名. + */ + HttpComponentsClientBuilder httpProxyUsername(String httpProxyUsername); + + /** + * 代理服务器密码. + */ + HttpComponentsClientBuilder httpProxyPassword(char[] httpProxyPassword); + + /** + * 重试策略. + */ + HttpComponentsClientBuilder httpRequestRetryStrategy(HttpRequestRetryStrategy httpRequestRetryStrategy); + + /** + * 超时时间. + */ + HttpComponentsClientBuilder keepAliveStrategy(ConnectionKeepAliveStrategy keepAliveStrategy); +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsMediaDownloadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsMediaDownloadRequestExecutor.java new file mode 100644 index 0000000000..26fbed93f3 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsMediaDownloadRequestExecutor.java @@ -0,0 +1,79 @@ +package me.chanjar.weixin.common.util.http.hc; + +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.fs.FileUtils; +import me.chanjar.weixin.common.util.http.BaseMediaDownloadRequestExecutor; +import me.chanjar.weixin.common.util.http.HttpResponseProxy; +import me.chanjar.weixin.common.util.http.RequestHttp; +import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.hc.client5.http.ClientProtocolException; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHost; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +/** + * ApacheMediaDownloadRequestExecutor + * + * @author altusea + */ +public class HttpComponentsMediaDownloadRequestExecutor extends BaseMediaDownloadRequestExecutor { + + public HttpComponentsMediaDownloadRequestExecutor(RequestHttp requestHttp, File tmpDirFile) { + super(requestHttp, tmpDirFile); + } + + @Override + public File execute(String uri, String queryParam, WxType wxType) throws WxErrorException, IOException { + if (queryParam != null) { + if (uri.indexOf('?') == -1) { + uri += '?'; + } + uri += uri.endsWith("?") ? queryParam : '&' + queryParam; + } + + HttpGet httpGet = new HttpGet(uri); + if (requestHttp.getRequestHttpProxy() != null) { + RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build(); + httpGet.setConfig(config); + } + + try (CloseableHttpResponse response = requestHttp.getRequestHttpClient().execute(httpGet); + InputStream inputStream = InputStreamResponseHandler.INSTANCE.handleResponse(response)) { + Header[] contentTypeHeader = response.getHeaders("Content-Type"); + if (contentTypeHeader != null && contentTypeHeader.length > 0) { + if (contentTypeHeader[0].getValue().startsWith(ContentType.APPLICATION_JSON.getMimeType())) { + // application/json; encoding=utf-8 下载媒体文件出错 + String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); + throw new WxErrorException(WxError.fromJson(responseContent, wxType)); + } + } + + String fileName = HttpResponseProxy.from(response).getFileName(); + if (StringUtils.isBlank(fileName)) { + fileName = String.valueOf(System.currentTimeMillis()); + } + + String baseName = FilenameUtils.getBaseName(fileName); + if (StringUtils.isBlank(fileName) || baseName.length() < 3) { + baseName = String.valueOf(System.currentTimeMillis()); + } + + return FileUtils.createTmpFile(inputStream, baseName, FilenameUtils.getExtension(fileName), super.tmpDirFile); + } catch (HttpException httpException) { + throw new ClientProtocolException(httpException.getMessage(), httpException); + } + } + +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsMediaInputStreamUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsMediaInputStreamUploadRequestExecutor.java new file mode 100644 index 0000000000..4853b1572b --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsMediaInputStreamUploadRequestExecutor.java @@ -0,0 +1,54 @@ +package me.chanjar.weixin.common.util.http.hc; + +import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.InputStreamData; +import me.chanjar.weixin.common.util.http.MediaInputStreamUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.entity.mime.HttpMultipartMode; +import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHost; + +import java.io.IOException; + +/** + * 文件输入流上传. + * + * @author altusea + */ +public class HttpComponentsMediaInputStreamUploadRequestExecutor extends MediaInputStreamUploadRequestExecutor { + + public HttpComponentsMediaInputStreamUploadRequestExecutor(RequestHttp requestHttp) { + super(requestHttp); + } + + @Override + public WxMediaUploadResult execute(String uri, InputStreamData data, WxType wxType) throws WxErrorException, IOException { + HttpPost httpPost = new HttpPost(uri); + if (requestHttp.getRequestHttpProxy() != null) { + RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build(); + httpPost.setConfig(config); + } + if (data != null) { + HttpEntity entity = MultipartEntityBuilder + .create() + .addBinaryBody("media", data.getInputStream(), ContentType.DEFAULT_BINARY, data.getFilename()) + .setMode(HttpMultipartMode.EXTENDED) + .build(); + httpPost.setEntity(entity); + } + String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE); + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + return WxMediaUploadResult.fromJson(responseContent); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsMediaUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsMediaUploadRequestExecutor.java new file mode 100644 index 0000000000..e65d855d52 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsMediaUploadRequestExecutor.java @@ -0,0 +1,53 @@ +package me.chanjar.weixin.common.util.http.hc; + +import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.entity.mime.HttpMultipartMode; +import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHost; + +import java.io.File; +import java.io.IOException; + +/** + * ApacheMediaUploadRequestExecutor + * + * @author altusea + */ +public class HttpComponentsMediaUploadRequestExecutor extends MediaUploadRequestExecutor { + + public HttpComponentsMediaUploadRequestExecutor(RequestHttp requestHttp) { + super(requestHttp); + } + + @Override + public WxMediaUploadResult execute(String uri, File file, WxType wxType) throws WxErrorException, IOException { + HttpPost httpPost = new HttpPost(uri); + if (requestHttp.getRequestHttpProxy() != null) { + RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build(); + httpPost.setConfig(config); + } + if (file != null) { + HttpEntity entity = MultipartEntityBuilder + .create() + .addBinaryBody("media", file) + .setMode(HttpMultipartMode.EXTENDED) + .build(); + httpPost.setEntity(entity); + } + String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE); + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + return WxMediaUploadResult.fromJson(responseContent); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsMinishopMediaUploadRequestCustomizeExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsMinishopMediaUploadRequestCustomizeExecutor.java new file mode 100644 index 0000000000..711f538309 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsMinishopMediaUploadRequestCustomizeExecutor.java @@ -0,0 +1,71 @@ +package me.chanjar.weixin.common.util.http.hc; + +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.result.WxMinishopImageUploadCustomizeResult; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.MinishopUploadRequestCustomizeExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.entity.mime.HttpMultipartMode; +import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHost; + +import java.io.File; +import java.io.IOException; + +/** + * ApacheMinishopMediaUploadRequestCustomizeExecutor + * + * @author altusea + */ +@Slf4j +public class HttpComponentsMinishopMediaUploadRequestCustomizeExecutor extends MinishopUploadRequestCustomizeExecutor { + + public HttpComponentsMinishopMediaUploadRequestCustomizeExecutor(RequestHttp requestHttp, String respType, String imgUrl) { + super(requestHttp, respType, imgUrl); + } + + @Override + public WxMinishopImageUploadCustomizeResult execute(String uri, File file, WxType wxType) throws WxErrorException, IOException { + HttpPost httpPost = new HttpPost(uri); + if (requestHttp.getRequestHttpProxy() != null) { + RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build(); + httpPost.setConfig(config); + } + if (this.uploadType.equals("0")) { + if (file == null) { + throw new WxErrorException("上传文件为空"); + } + HttpEntity entity = MultipartEntityBuilder + .create() + .addBinaryBody("media", file) + .addTextBody("resp_type", this.respType) + .addTextBody("upload_type", this.uploadType) + .setMode(HttpMultipartMode.EXTENDED) + .build(); + httpPost.setEntity(entity); + } + else { + HttpEntity entity = MultipartEntityBuilder + .create() + .addTextBody("resp_type", this.respType) + .addTextBody("upload_type", this.uploadType) + .addTextBody("img_url", this.imgUrl) + .setMode(org.apache.hc.client5.http.entity.mime.HttpMultipartMode.EXTENDED) + .build(); + httpPost.setEntity(entity); + } + String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE); + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + log.info("responseContent: {}", responseContent); + return WxMinishopImageUploadCustomizeResult.fromJson(responseContent); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsMinishopMediaUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsMinishopMediaUploadRequestExecutor.java new file mode 100644 index 0000000000..72c1f2765f --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsMinishopMediaUploadRequestExecutor.java @@ -0,0 +1,56 @@ +package me.chanjar.weixin.common.util.http.hc; + +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.result.WxMinishopImageUploadResult; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.MinishopUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.entity.mime.HttpMultipartMode; +import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHost; + +import java.io.File; +import java.io.IOException; + +/** + * ApacheMinishopMediaUploadRequestExecutor + * + * @author altusea + */ +@Slf4j +public class HttpComponentsMinishopMediaUploadRequestExecutor extends MinishopUploadRequestExecutor { + + public HttpComponentsMinishopMediaUploadRequestExecutor(RequestHttp requestHttp) { + super(requestHttp); + } + + @Override + public WxMinishopImageUploadResult execute(String uri, File file, WxType wxType) throws WxErrorException, IOException { + HttpPost httpPost = new HttpPost(uri); + if (requestHttp.getRequestHttpProxy() != null) { + RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build(); + httpPost.setConfig(config); + } + if (file != null) { + HttpEntity entity = MultipartEntityBuilder + .create() + .addBinaryBody("media", file) + .setMode(HttpMultipartMode.EXTENDED) + .build(); + httpPost.setEntity(entity); + } + String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE); + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + log.info("responseContent: {}", responseContent); + return WxMinishopImageUploadResult.fromJson(responseContent); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsResponseProxy.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsResponseProxy.java new file mode 100644 index 0000000000..d55ff0735f --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsResponseProxy.java @@ -0,0 +1,25 @@ +package me.chanjar.weixin.common.util.http.hc; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.HttpResponseProxy; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.Header; + +public class HttpComponentsResponseProxy implements HttpResponseProxy { + + private final CloseableHttpResponse response; + + public HttpComponentsResponseProxy(CloseableHttpResponse closeableHttpResponse) { + this.response = closeableHttpResponse; + } + + @Override + public String getFileName() throws WxErrorException { + Header[] contentDispositionHeader = this.response.getHeaders("Content-disposition"); + if (contentDispositionHeader == null || contentDispositionHeader.length == 0) { + throw new WxErrorException("无法获取到文件名,Content-disposition为空"); + } + + return HttpResponseProxy.extractFileNameFromContentString(contentDispositionHeader[0].getValue()); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsSimpleGetRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsSimpleGetRequestExecutor.java new file mode 100644 index 0000000000..0d212fe7e2 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsSimpleGetRequestExecutor.java @@ -0,0 +1,43 @@ +package me.chanjar.weixin.common.util.http.hc; + +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.RequestHttp; +import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.HttpHost; + +import java.io.IOException; + +/** + * ApacheSimpleGetRequestExecutor + * + * @author altusea + */ +public class HttpComponentsSimpleGetRequestExecutor extends SimpleGetRequestExecutor { + + public HttpComponentsSimpleGetRequestExecutor(RequestHttp requestHttp) { + super(requestHttp); + } + + @Override + public String execute(String uri, String queryParam, WxType wxType) throws WxErrorException, IOException { + if (queryParam != null) { + if (uri.indexOf('?') == -1) { + uri += '?'; + } + uri += uri.endsWith("?") ? queryParam : '&' + queryParam; + } + HttpGet httpGet = new HttpGet(uri); + if (requestHttp.getRequestHttpProxy() != null) { + RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build(); + httpGet.setConfig(config); + } + + String responseContent = requestHttp.getRequestHttpClient().execute(httpGet, Utf8ResponseHandler.INSTANCE); + return handleResponse(wxType, responseContent); + } + +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsSimplePostRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsSimplePostRequestExecutor.java new file mode 100644 index 0000000000..45d2ca9f6e --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/HttpComponentsSimplePostRequestExecutor.java @@ -0,0 +1,45 @@ +package me.chanjar.weixin.common.util.http.hc; + +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.RequestHttp; +import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.io.entity.StringEntity; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * ApacheSimplePostRequestExecutor + * + * @author altusea + */ +public class HttpComponentsSimplePostRequestExecutor extends SimplePostRequestExecutor { + + public HttpComponentsSimplePostRequestExecutor(RequestHttp requestHttp) { + super(requestHttp); + } + + @Override + public String execute(String uri, String postEntity, WxType wxType) throws WxErrorException, IOException { + HttpPost httpPost = new HttpPost(uri); + if (requestHttp.getRequestHttpProxy() != null) { + RequestConfig config = RequestConfig.custom().setProxy(requestHttp.getRequestHttpProxy()).build(); + httpPost.setConfig(config); + } + + if (postEntity != null) { + StringEntity entity = new StringEntity(postEntity, ContentType.APPLICATION_JSON.withCharset(StandardCharsets.UTF_8)); + httpPost.setEntity(entity); + } + + String responseContent = requestHttp.getRequestHttpClient().execute(httpPost, Utf8ResponseHandler.INSTANCE); + return this.handleResponse(wxType, responseContent); + } + +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/InputStreamResponseHandler.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/InputStreamResponseHandler.java new file mode 100644 index 0000000000..27308151f7 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/InputStreamResponseHandler.java @@ -0,0 +1,23 @@ +package me.chanjar.weixin.common.util.http.hc; + +import org.apache.hc.client5.http.impl.classic.AbstractHttpClientResponseHandler; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; + +import java.io.IOException; +import java.io.InputStream; + +/** + * InputStreamResponseHandler + * + * @author altusea + */ +public class InputStreamResponseHandler extends AbstractHttpClientResponseHandler { + + public static final HttpClientResponseHandler INSTANCE = new InputStreamResponseHandler(); + + @Override + public InputStream handleEntity(HttpEntity entity) throws IOException { + return entity.getContent(); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/NoopRetryStrategy.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/NoopRetryStrategy.java new file mode 100644 index 0000000000..9b4e3bc384 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/NoopRetryStrategy.java @@ -0,0 +1,34 @@ +package me.chanjar.weixin.common.util.http.hc; + +import org.apache.hc.client5.http.HttpRequestRetryStrategy; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.apache.hc.core5.util.TimeValue; + +import java.io.IOException; + +/** + * NoopRetryStrategy + * + * @author altusea + */ +public class NoopRetryStrategy implements HttpRequestRetryStrategy { + + public static final HttpRequestRetryStrategy INSTANCE = new NoopRetryStrategy(); + + @Override + public boolean retryRequest(HttpRequest request, IOException exception, int execCount, HttpContext context) { + return false; + } + + @Override + public boolean retryRequest(HttpResponse response, int execCount, HttpContext context) { + return false; + } + + @Override + public TimeValue getRetryInterval(HttpResponse response, int execCount, HttpContext context) { + return TimeValue.ZERO_MILLISECONDS; + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/Utf8ResponseHandler.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/Utf8ResponseHandler.java new file mode 100644 index 0000000000..81699ef57b --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/hc/Utf8ResponseHandler.java @@ -0,0 +1,30 @@ +package me.chanjar.weixin.common.util.http.hc; + +import org.apache.hc.client5.http.ClientProtocolException; +import org.apache.hc.client5.http.impl.classic.AbstractHttpClientResponseHandler; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.io.HttpClientResponseHandler; +import org.apache.hc.core5.http.io.entity.EntityUtils; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * Utf8ResponseHandler + * + * @author altusea + */ +public class Utf8ResponseHandler extends AbstractHttpClientResponseHandler { + + public static final HttpClientResponseHandler INSTANCE = new Utf8ResponseHandler(); + + @Override + public String handleEntity(HttpEntity entity) throws IOException { + try { + return EntityUtils.toString(entity, StandardCharsets.UTF_8); + } catch (final ParseException ex) { + throw new ClientProtocolException(ex); + } + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaDownloadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaDownloadRequestExecutor.java index 4f310274df..bc2fbc17f3 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaDownloadRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaDownloadRequestExecutor.java @@ -4,8 +4,7 @@ import jodd.http.HttpRequest; import jodd.http.HttpResponse; import jodd.http.ProxyInfo; -import jodd.util.StringPool; -import me.chanjar.weixin.common.WxType; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.util.fs.FileUtils; @@ -19,15 +18,16 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; /** * . * * @author ecoolper - * @date 2017/5/5 + * created on 2017/5/5 */ public class JoddHttpMediaDownloadRequestExecutor extends BaseMediaDownloadRequestExecutor { - public JoddHttpMediaDownloadRequestExecutor(RequestHttp requestHttp, File tmpDirFile) { + public JoddHttpMediaDownloadRequestExecutor(RequestHttp requestHttp, File tmpDirFile) { super(requestHttp, tmpDirFile); } @@ -47,7 +47,7 @@ public File execute(String uri, String queryParam, WxType wxType) throws WxError request.withConnectionProvider(requestHttp.getRequestHttpClient()); HttpResponse response = request.send(); - response.charset(StringPool.UTF_8); + response.charset(StandardCharsets.UTF_8.name()); String contentType = response.header("Content-Type"); if (contentType != null && contentType.startsWith("application/json")) { @@ -55,14 +55,19 @@ public File execute(String uri, String queryParam, WxType wxType) throws WxError throw new WxErrorException(WxError.fromJson(response.bodyText(), wxType)); } - String fileName = new HttpResponseProxy(response).getFileName(); + String fileName = HttpResponseProxy.from(response).getFileName(); if (StringUtils.isBlank(fileName)) { return null; } + String baseName = FilenameUtils.getBaseName(fileName); + if (StringUtils.isBlank(fileName) || baseName.length() < 3) { + baseName = String.valueOf(System.currentTimeMillis()); + } + try (InputStream inputStream = new ByteArrayInputStream(response.bodyBytes())) { return FileUtils.createTmpFile(inputStream, - FilenameUtils.getBaseName(fileName), + baseName, FilenameUtils.getExtension(fileName), super.tmpDirFile); } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaInputStreamUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaInputStreamUploadRequestExecutor.java new file mode 100644 index 0000000000..915db21c65 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaInputStreamUploadRequestExecutor.java @@ -0,0 +1,61 @@ +package me.chanjar.weixin.common.util.http.jodd; + +import jodd.http.HttpConnectionProvider; +import jodd.http.HttpRequest; +import jodd.http.HttpResponse; +import jodd.http.ProxyInfo; +import jodd.http.upload.ByteArrayUploadable; +import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.InputStreamData; +import me.chanjar.weixin.common.util.http.MediaInputStreamUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +/** + * 文件输入流上传. + * + * @author meiqin.zhou91@gmail.com + * created on 2022/02/15 + */ +public class JoddHttpMediaInputStreamUploadRequestExecutor extends MediaInputStreamUploadRequestExecutor { + public JoddHttpMediaInputStreamUploadRequestExecutor(RequestHttp requestHttp) { + super(requestHttp); + } + + @Override + public WxMediaUploadResult execute(String uri, InputStreamData data, WxType wxType) throws WxErrorException, IOException { + HttpRequest request = HttpRequest.post(uri); + if (requestHttp.getRequestHttpProxy() != null) { + requestHttp.getRequestHttpClient().useProxy(requestHttp.getRequestHttpProxy()); + } + request.withConnectionProvider(requestHttp.getRequestHttpClient()); + request.form("media", new ByteArrayUploadable(this.toByteArray(data.getInputStream()), data.getFilename())); + HttpResponse response = request.send(); + response.charset(StandardCharsets.UTF_8.name()); + + String responseContent = response.bodyText(); + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + return WxMediaUploadResult.fromJson(responseContent); + } + + public byte[] toByteArray(InputStream input) throws IOException { + try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { + byte[] buffer = new byte[4096]; + int n = 0; + while (-1 != (n = input.read(buffer))) { + output.write(buffer, 0, n); + } + return output.toByteArray(); + } + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaUploadRequestExecutor.java index 3c7122a1f8..1ed59a71da 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaUploadRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMediaUploadRequestExecutor.java @@ -4,8 +4,7 @@ import jodd.http.HttpRequest; import jodd.http.HttpResponse; import jodd.http.ProxyInfo; -import jodd.util.StringPool; -import me.chanjar.weixin.common.WxType; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; @@ -14,15 +13,16 @@ import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; /** * . * * @author ecoolper - * @date 2017/5/5 + * created on 2017/5/5 */ public class JoddHttpMediaUploadRequestExecutor extends MediaUploadRequestExecutor { - public JoddHttpMediaUploadRequestExecutor(RequestHttp requestHttp) { + public JoddHttpMediaUploadRequestExecutor(RequestHttp requestHttp) { super(requestHttp); } @@ -35,7 +35,7 @@ public WxMediaUploadResult execute(String uri, File file, WxType wxType) throws request.withConnectionProvider(requestHttp.getRequestHttpClient()); request.form("media", file); HttpResponse response = request.send(); - response.charset(StringPool.UTF_8); + response.charset(StandardCharsets.UTF_8.name()); String responseContent = response.bodyText(); WxError error = WxError.fromJson(responseContent, wxType); diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMinishopMediaUploadRequestCustomizeExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMinishopMediaUploadRequestCustomizeExecutor.java new file mode 100644 index 0000000000..66074d8103 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMinishopMediaUploadRequestCustomizeExecutor.java @@ -0,0 +1,58 @@ +package me.chanjar.weixin.common.util.http.jodd; + +import jodd.http.HttpConnectionProvider; +import jodd.http.HttpRequest; +import jodd.http.HttpResponse; +import jodd.http.ProxyInfo; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.result.WxMinishopImageUploadCustomizeResult; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.MinishopUploadRequestCustomizeExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * @author liming1019 + * created on 2021/8/10 + */ +@Slf4j +public class JoddHttpMinishopMediaUploadRequestCustomizeExecutor extends MinishopUploadRequestCustomizeExecutor { + public JoddHttpMinishopMediaUploadRequestCustomizeExecutor(RequestHttp requestHttp, String respType, String imgUrl) { + super(requestHttp, respType, imgUrl); + } + + @Override + public WxMinishopImageUploadCustomizeResult execute(String uri, File file, WxType wxType) throws WxErrorException, IOException { + HttpRequest request = HttpRequest.post(uri); + if (requestHttp.getRequestHttpProxy() != null) { + requestHttp.getRequestHttpClient().useProxy(requestHttp.getRequestHttpProxy()); + } + request.withConnectionProvider(requestHttp.getRequestHttpClient()); + if (this.uploadType.equals("0")) { + request.form("resp_type", this.respType, + "upload_type", this.uploadType, + "media", file); + } + else { + request.form("resp_type", this.respType, + "upload_type", this.uploadType, + "img_url", this.imgUrl); + } + HttpResponse response = request.send(); + response.charset(StandardCharsets.UTF_8.name()); + + String responseContent = response.bodyText(); + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + log.info("responseContent: {}", responseContent); + + return WxMinishopImageUploadCustomizeResult.fromJson(responseContent); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMinishopMediaUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMinishopMediaUploadRequestExecutor.java new file mode 100644 index 0000000000..c7c35dd798 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpMinishopMediaUploadRequestExecutor.java @@ -0,0 +1,51 @@ +package me.chanjar.weixin.common.util.http.jodd; + +import jodd.http.HttpConnectionProvider; +import jodd.http.HttpRequest; +import jodd.http.HttpResponse; +import jodd.http.ProxyInfo; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.result.WxMinishopImageUploadResult; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.MinishopUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * . + * + * @author ecoolper + * created on 2017/5/5 + */ +@Slf4j +public class JoddHttpMinishopMediaUploadRequestExecutor extends MinishopUploadRequestExecutor { + public JoddHttpMinishopMediaUploadRequestExecutor(RequestHttp requestHttp) { + super(requestHttp); + } + + @Override + public WxMinishopImageUploadResult execute(String uri, File file, WxType wxType) throws WxErrorException, IOException { + HttpRequest request = HttpRequest.post(uri); + if (requestHttp.getRequestHttpProxy() != null) { + requestHttp.getRequestHttpClient().useProxy(requestHttp.getRequestHttpProxy()); + } + request.withConnectionProvider(requestHttp.getRequestHttpClient()); + request.form("media", file); + HttpResponse response = request.send(); + response.charset(StandardCharsets.UTF_8.name()); + + String responseContent = response.bodyText(); + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + log.info("responseContent: {}", responseContent); + + return WxMinishopImageUploadResult.fromJson(responseContent); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpResponseProxy.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpResponseProxy.java new file mode 100644 index 0000000000..1bda38a497 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpResponseProxy.java @@ -0,0 +1,20 @@ +package me.chanjar.weixin.common.util.http.jodd; + +import jodd.http.HttpResponse; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.HttpResponseProxy; + +public class JoddHttpResponseProxy implements HttpResponseProxy { + + private final HttpResponse response; + + public JoddHttpResponseProxy(HttpResponse httpResponse) { + this.response = httpResponse; + } + + @Override + public String getFileName() throws WxErrorException { + String content = response.header("Content-disposition"); + return HttpResponseProxy.extractFileNameFromContentString(content); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimpleGetRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimpleGetRequestExecutor.java index c93bd4b180..ed8288b04f 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimpleGetRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimpleGetRequestExecutor.java @@ -4,23 +4,22 @@ import jodd.http.HttpRequest; import jodd.http.HttpResponse; import jodd.http.ProxyInfo; -import jodd.util.StringPool; -import me.chanjar.weixin.common.WxType; -import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.util.http.RequestHttp; import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor; import java.io.IOException; +import java.nio.charset.StandardCharsets; /** * . * * @author ecoolper - * @date 2017/5/4 + * created on 2017/5/4 */ public class JoddHttpSimpleGetRequestExecutor extends SimpleGetRequestExecutor { - public JoddHttpSimpleGetRequestExecutor(RequestHttp requestHttp) { + public JoddHttpSimpleGetRequestExecutor(RequestHttp requestHttp) { super(requestHttp); } @@ -39,15 +38,9 @@ public String execute(String uri, String queryParam, WxType wxType) throws WxErr } request.withConnectionProvider(requestHttp.getRequestHttpClient()); HttpResponse response = request.send(); - response.charset(StringPool.UTF_8); + response.charset(StandardCharsets.UTF_8.name()); - String responseContent = response.bodyText(); - - WxError error = WxError.fromJson(responseContent, wxType); - if (error.getErrorCode() != 0) { - throw new WxErrorException(error); - } - return responseContent; + return handleResponse(wxType, response.bodyText()); } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimplePostRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimplePostRequestExecutor.java index 75114d6a7d..095493c75e 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimplePostRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/jodd/JoddHttpSimplePostRequestExecutor.java @@ -4,23 +4,22 @@ import jodd.http.HttpRequest; import jodd.http.HttpResponse; import jodd.http.ProxyInfo; -import jodd.util.StringPool; -import me.chanjar.weixin.common.WxType; -import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.util.http.RequestHttp; import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor; import java.io.IOException; +import java.nio.charset.StandardCharsets; /** * . * * @author ecoolper - * @date 2017/5/4 + * created on 2017/5/4 */ public class JoddHttpSimplePostRequestExecutor extends SimplePostRequestExecutor { - public JoddHttpSimplePostRequestExecutor(RequestHttp requestHttp) { + public JoddHttpSimplePostRequestExecutor(RequestHttp requestHttp) { super(requestHttp); } @@ -35,26 +34,13 @@ public String execute(String uri, String postEntity, WxType wxType) throws WxErr } request.withConnectionProvider(provider); if (postEntity != null) { + request.contentType("application/json", "utf-8"); request.bodyText(postEntity); } HttpResponse response = request.send(); - response.charset(StringPool.UTF_8); + response.charset(StandardCharsets.UTF_8.name()); - String responseContent = response.bodyText(); - if (responseContent.isEmpty()) { - throw new WxErrorException(WxError.builder().errorCode(9999).errorMsg("无响应内容").build()); - } - - if (responseContent.startsWith("")) { - //xml格式输出直接返回 - return responseContent; - } - - WxError error = WxError.fromJson(responseContent, wxType); - if (error.getErrorCode() != 0) { - throw new WxErrorException(error); - } - return responseContent; + return this.handleResponse(wxType, response.bodyText()); } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/DefaultOkHttpClientBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/DefaultOkHttpClientBuilder.java new file mode 100644 index 0000000000..5ebe0ff1fa --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/DefaultOkHttpClientBuilder.java @@ -0,0 +1,263 @@ +package me.chanjar.weixin.common.util.http.okhttp; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import okhttp3.*; + +import javax.annotation.concurrent.NotThreadSafe; +import java.net.Proxy; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author wulang + **/ +@Slf4j +@Data +@NotThreadSafe +public class DefaultOkHttpClientBuilder implements OkHttpClientBuilder { + + private final AtomicBoolean prepared = new AtomicBoolean(false); + + /** + * 代理 + */ + private Proxy proxy; + + /** + * 授权 + */ + private Authenticator authenticator; + + /** + * 拦截器 + */ + private final List interceptorList = new ArrayList<>(); + + /** + * 请求调度管理 + */ + private Dispatcher dispatcher; + + /** + * 连接池 + */ + private ConnectionPool connectionPool; + + /** + * 监听网络请求过程 + */ + private EventListener.Factory eventListenerFactory; + + /** + * 是否支持失败重连 + */ + private Boolean retryOnConnectionFailure; + + /** + * 是否允许重定向操作 + */ + private Boolean followRedirects; + + /** + * 连接建立的超时时长 + */ + private Long connectTimeout; + + /** + * 连接建立的超时时间单位 + */ + private TimeUnit connectTimeUnit; + + /** + * 完整的请求过程超时时长 + */ + private Long callTimeout; + + /** + * 完整的请求过程超时时间单位 + */ + private TimeUnit callTimeUnit; + + /** + * 连接的IO读操作超时时长 + */ + private Long readTimeout; + + /** + * 连接的IO读操作超时时间单位 + */ + private TimeUnit readTimeUnit; + + /** + * 连接的IO写操作超时时长 + */ + private Long writeTimeout; + + /** + * 连接的IO写操作超时时间单位 + */ + private TimeUnit writeTimeUnit; + + /** + * ping的时间间隔 + */ + private Integer pingInterval; + + /** + * 持有client对象,仅初始化一次,避免多service实例的时候造成重复初始化的问题 + */ + private OkHttpClient okHttpClient; + + private DefaultOkHttpClientBuilder() { + + } + + public static DefaultOkHttpClientBuilder get() { + return DefaultOkHttpClientBuilder.SingletonHolder.INSTANCE; + } + + @Override + public OkHttpClient build() { + if (!prepared.get()) { + prepare(); + } + return this.okHttpClient; + } + + @Override + public OkHttpClientBuilder proxy(Proxy proxy) { + this.proxy = proxy; + return this; + } + + @Override + public OkHttpClientBuilder authenticator(Authenticator authenticator) { + this.authenticator = authenticator; + return this; + } + + @Override + public OkHttpClientBuilder addInterceptor(Interceptor interceptor) { + this.interceptorList.add(interceptor); + return this; + } + + @Override + public OkHttpClientBuilder setDispatcher(Dispatcher dispatcher) { + this.dispatcher = dispatcher; + return this; + } + + @Override + public OkHttpClientBuilder setConnectionPool(ConnectionPool connectionPool) { + this.connectionPool = connectionPool; + return this; + } + + @Override + public OkHttpClientBuilder setEventListenerFactory(EventListener.Factory eventListenerFactory) { + this.eventListenerFactory = eventListenerFactory; + return this; + } + + @Override + public OkHttpClientBuilder setRetryOnConnectionFailure(Boolean retryOnConnectionFailure) { + this.retryOnConnectionFailure = retryOnConnectionFailure; + return this; + } + + @Override + public OkHttpClientBuilder setFollowRedirects(Boolean followRedirects) { + this.followRedirects = followRedirects; + return this; + } + + @Override + public OkHttpClientBuilder connectTimeout(Long timeout, TimeUnit unit) { + this.connectTimeout = timeout; + this.connectTimeUnit = unit; + return this; + } + + @Override + public OkHttpClientBuilder callTimeout(Long timeout, TimeUnit unit) { + this.callTimeout = timeout; + this.callTimeUnit = unit; + return this; + } + + @Override + public OkHttpClientBuilder readTimeout(Long timeout, TimeUnit unit) { + this.readTimeout = timeout; + this.readTimeUnit = unit; + return this; + } + + @Override + public OkHttpClientBuilder writeTimeout(Long timeout, TimeUnit unit) { + this.writeTimeout = timeout; + this.writeTimeUnit = unit; + return this; + } + + @Override + public OkHttpClientBuilder setPingInterval(Integer pingInterval) { + this.pingInterval = pingInterval; + return this; + } + + private synchronized void prepare() { + if (prepared.get()) { + return; + } + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + if (this.authenticator != null) { + builder.authenticator(this.authenticator); + } + if (this.proxy != null) { + builder.proxy(this.proxy); + } + for (Interceptor interceptor : this.interceptorList) { + builder.addInterceptor(interceptor); + } + if (this.dispatcher != null) { + builder.dispatcher(dispatcher); + } + if (this.connectionPool != null) { + builder.connectionPool(connectionPool); + } + if (this.eventListenerFactory != null) { + builder.eventListenerFactory(this.eventListenerFactory); + } + if (this.retryOnConnectionFailure != null) { + builder.setRetryOnConnectionFailure$okhttp(this.retryOnConnectionFailure); + } + if (this.followRedirects != null) { + builder.followRedirects(this.followRedirects); + } + if (this.connectTimeout != null && this.connectTimeUnit != null) { + builder.connectTimeout(this.connectTimeout, this.connectTimeUnit); + } + if (this.callTimeout != null && this.callTimeUnit != null) { + builder.callTimeout(this.callTimeout, this.callTimeUnit); + } + if (this.readTimeout != null && this.readTimeUnit != null) { + builder.readTimeout(this.readTimeout, this.readTimeUnit); + } + if (this.writeTimeout != null && this.writeTimeUnit != null) { + builder.writeTimeout(this.writeTimeout, this.writeTimeUnit); + } + if (this.pingInterval != null) { + builder.setPingInterval$okhttp(this.pingInterval); + } + this.okHttpClient = builder.build(); + prepared.set(true); + } + + private static class SingletonHolder { + private static final DefaultOkHttpClientBuilder INSTANCE = new DefaultOkHttpClientBuilder(); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpClientBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpClientBuilder.java new file mode 100644 index 0000000000..0a2586a4bd --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpClientBuilder.java @@ -0,0 +1,126 @@ +package me.chanjar.weixin.common.util.http.okhttp; + +import okhttp3.*; + +import java.net.Proxy; +import java.util.concurrent.TimeUnit; + +/** + * @author wulang + **/ +public interface OkHttpClientBuilder { + /** + * 构建OkHttpClient实例. + * + * @return OkHttpClient + */ + OkHttpClient build(); + + /** + * 代理 + * + * @param proxy Proxy + * @return OkHttpClientBuilder + */ + OkHttpClientBuilder proxy(Proxy proxy); + + /** + * 授权 + * + * @param authenticator Authenticator + * @return OkHttpClientBuilder + */ + OkHttpClientBuilder authenticator(Authenticator authenticator); + + /** + * 拦截器 + * + * @param interceptor Interceptor + * @return OkHttpClientBuilder + */ + OkHttpClientBuilder addInterceptor(Interceptor interceptor); + + /** + * 请求调度管理 + * + * @param dispatcher Dispatcher + * @return OkHttpClientBuilder + */ + OkHttpClientBuilder setDispatcher(Dispatcher dispatcher); + + /** + * 连接池 + * + * @param connectionPool ConnectionPool + * @return OkHttpClientBuilder + */ + OkHttpClientBuilder setConnectionPool(ConnectionPool connectionPool); + + /** + * 监听网络请求过程 + * + * @param eventListenerFactory EventListener + * @return OkHttpClientBuilder + */ + OkHttpClientBuilder setEventListenerFactory(EventListener.Factory eventListenerFactory); + + /** + * 是否支持失败重连 + * + * @param retryOnConnectionFailure retryOnConnectionFailure + * @return OkHttpClientBuilder + */ + OkHttpClientBuilder setRetryOnConnectionFailure(Boolean retryOnConnectionFailure); + + /** + * 是否允许重定向操作 + * + * @param followRedirects followRedirects + * @return OkHttpClientBuilder + */ + OkHttpClientBuilder setFollowRedirects(Boolean followRedirects); + + /** + * 连接建立的超时时间 + * + * @param timeout 时长 + * @param unit 时间单位 + * @return OkHttpClientBuilder + */ + OkHttpClientBuilder connectTimeout(Long timeout, TimeUnit unit); + + /** + * 完整的请求过程超时时间 + * + * @param timeout 时长 + * @param unit 时间单位 + * @return OkHttpClientBuilder + */ + OkHttpClientBuilder callTimeout(Long timeout, TimeUnit unit); + + /** + * 连接的IO读操作超时时间 + * + * @param timeout 时长 + * @param unit 时间单位 + * @return OkHttpClientBuilder + */ + OkHttpClientBuilder readTimeout(Long timeout, TimeUnit unit); + + /** + * 连接的IO写操作超时时间 + * + * @param timeout 时长 + * @param unit 时间单位 + * @return OkHttpClientBuilder + */ + OkHttpClientBuilder writeTimeout(Long timeout, TimeUnit unit); + + /** + * ping的时间间隔 + * + * @param pingInterval ping的时间间隔 + * @return OkHttpClientBuilder + */ + OkHttpClientBuilder setPingInterval(Integer pingInterval); +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpDnsClientBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpDnsClientBuilder.java new file mode 100644 index 0000000000..c346747527 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpDnsClientBuilder.java @@ -0,0 +1,257 @@ +package me.chanjar.weixin.common.util.http.okhttp; + +import okhttp3.*; + +import java.net.Proxy; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * OkHttpClient 连接管理器 多一个DNS解析. + *

大部分代码拷贝自:DefaultOkHttpClientBuilder

+ * + * @author wulang + **/ +public class OkHttpDnsClientBuilder implements OkHttpClientBuilder { + + /** + * 代理 + */ + private Proxy proxy; + + /** + * 授权 + */ + private Authenticator authenticator; + + /** + * 拦截器 + */ + private final List interceptorList = new ArrayList<>(); + + /** + * 请求调度管理 + */ + private Dispatcher dispatcher; + + /** + * 连接池 + */ + private ConnectionPool connectionPool; + + /** + * 监听网络请求过程 + */ + private EventListener.Factory eventListenerFactory; + + /** + * 是否支持失败重连 + */ + private Boolean retryOnConnectionFailure; + + /** + * 是否允许重定向操作 + */ + private Boolean followRedirects; + + /** + * 连接建立的超时时长 + */ + private Long connectTimeout; + + /** + * 连接建立的超时时间单位 + */ + private TimeUnit connectTimeUnit; + + /** + * 完整的请求过程超时时长 + */ + private Long callTimeout; + + /** + * 完整的请求过程超时时间单位 + */ + private TimeUnit callTimeUnit; + + /** + * 连接的IO读操作超时时长 + */ + private Long readTimeout; + + /** + * 连接的IO读操作超时时间单位 + */ + private TimeUnit readTimeUnit; + + /** + * 连接的IO写操作超时时长 + */ + private Long writeTimeout; + + /** + * 连接的IO写操作超时时间单位 + */ + private TimeUnit writeTimeUnit; + + /** + * ping的时间间隔 + */ + private Integer pingInterval; + + private Dns dns; + + private OkHttpClient okHttpClient; + + private OkHttpDnsClientBuilder() { + + } + + public static OkHttpDnsClientBuilder get() { + return new OkHttpDnsClientBuilder(); + } + + public Dns getDns() { + return dns; + } + + public void setDns(Dns dns) { + this.dns = dns; + } + + @Override + public OkHttpClient build() { + prepare(); + return this.okHttpClient; + } + + @Override + public OkHttpClientBuilder proxy(Proxy proxy) { + this.proxy = proxy; + return this; + } + + @Override + public OkHttpClientBuilder authenticator(Authenticator authenticator) { + this.authenticator = authenticator; + return this; + } + + @Override + public OkHttpClientBuilder addInterceptor(Interceptor interceptor) { + this.interceptorList.add(interceptor); + return this; + } + + @Override + public OkHttpClientBuilder setDispatcher(Dispatcher dispatcher) { + this.dispatcher = dispatcher; + return this; + } + + @Override + public OkHttpClientBuilder setConnectionPool(ConnectionPool connectionPool) { + this.connectionPool = connectionPool; + return this; + } + + @Override + public OkHttpClientBuilder setEventListenerFactory(EventListener.Factory eventListenerFactory) { + this.eventListenerFactory = eventListenerFactory; + return this; + } + + @Override + public OkHttpClientBuilder setRetryOnConnectionFailure(Boolean retryOnConnectionFailure) { + this.retryOnConnectionFailure = retryOnConnectionFailure; + return this; + } + + @Override + public OkHttpClientBuilder setFollowRedirects(Boolean followRedirects) { + this.followRedirects = followRedirects; + return this; + } + + @Override + public OkHttpClientBuilder connectTimeout(Long timeout, TimeUnit unit) { + this.connectTimeout = timeout; + this.connectTimeUnit = unit; + return this; + } + + @Override + public OkHttpClientBuilder callTimeout(Long timeout, TimeUnit unit) { + this.callTimeout = timeout; + this.callTimeUnit = unit; + return this; + } + + @Override + public OkHttpClientBuilder readTimeout(Long timeout, TimeUnit unit) { + this.readTimeout = timeout; + this.readTimeUnit = unit; + return this; + } + + @Override + public OkHttpClientBuilder writeTimeout(Long timeout, TimeUnit unit) { + this.writeTimeout = timeout; + this.writeTimeUnit = unit; + return this; + } + + @Override + public OkHttpClientBuilder setPingInterval(Integer pingInterval) { + this.pingInterval = pingInterval; + return this; + } + + private synchronized void prepare() { + OkHttpClient.Builder builder = new OkHttpClient.Builder(); + if (this.authenticator != null) { + builder.authenticator(this.authenticator); + } + if (this.proxy != null) { + builder.proxy(this.proxy); + } + for (Interceptor interceptor : this.interceptorList) { + builder.addInterceptor(interceptor); + } + if (this.dispatcher != null) { + builder.dispatcher(dispatcher); + } + if (this.connectionPool != null) { + builder.connectionPool(connectionPool); + } + if (this.eventListenerFactory != null) { + builder.eventListenerFactory(this.eventListenerFactory); + } + if (this.retryOnConnectionFailure != null) { + builder.setRetryOnConnectionFailure$okhttp(this.retryOnConnectionFailure); + } + if (this.followRedirects != null) { + builder.followRedirects(this.followRedirects); + } + if (this.dns != null) { + builder.dns(this.dns); + } + if (this.connectTimeout != null && this.connectTimeUnit != null) { + builder.connectTimeout(this.connectTimeout, this.connectTimeUnit); + } + if (this.callTimeout != null && this.callTimeUnit != null) { + builder.callTimeout(this.callTimeout, this.callTimeUnit); + } + if (this.readTimeout != null && this.readTimeUnit != null) { + builder.readTimeout(this.readTimeout, this.readTimeUnit); + } + if (this.writeTimeout != null && this.writeTimeUnit != null) { + builder.writeTimeout(this.writeTimeout, this.writeTimeUnit); + } + if (this.pingInterval != null) { + builder.setPingInterval$okhttp(this.pingInterval); + } + this.okHttpClient = builder.build(); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaDownloadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaDownloadRequestExecutor.java index 729f1e186f..0610d3f51c 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaDownloadRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaDownloadRequestExecutor.java @@ -1,7 +1,7 @@ package me.chanjar.weixin.common.util.http.okhttp; import lombok.extern.slf4j.Slf4j; -import me.chanjar.weixin.common.WxType; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.util.http.BaseMediaDownloadRequestExecutor; @@ -14,8 +14,6 @@ import okio.Okio; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang3.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; @@ -23,11 +21,11 @@ /** *. * @author ecoolper - * @date 2017/5/5 + * created on 2017/5/5 */ @Slf4j public class OkHttpMediaDownloadRequestExecutor extends BaseMediaDownloadRequestExecutor { - public OkHttpMediaDownloadRequestExecutor(RequestHttp requestHttp, File tmpDirFile) { + public OkHttpMediaDownloadRequestExecutor(RequestHttp requestHttp, File tmpDirFile) { super(requestHttp, tmpDirFile); } @@ -53,13 +51,18 @@ public File execute(String uri, String queryParam, WxType wxType) throws WxError throw new WxErrorException(WxError.fromJson(response.body().string(), wxType)); } - String fileName = new HttpResponseProxy(response).getFileName(); + String fileName = HttpResponseProxy.from(response).getFileName(); if (StringUtils.isBlank(fileName)) { return null; } + String baseName = FilenameUtils.getBaseName(fileName); + if (StringUtils.isBlank(fileName) || baseName.length() < 3) { + baseName = String.valueOf(System.currentTimeMillis()); + } + File file = File.createTempFile( - FilenameUtils.getBaseName(fileName), "." + FilenameUtils.getExtension(fileName), super.tmpDirFile + baseName, "." + FilenameUtils.getExtension(fileName), super.tmpDirFile ); try (BufferedSink sink = Okio.buffer(Okio.sink(file))) { diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaInputStreamUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaInputStreamUploadRequestExecutor.java new file mode 100644 index 0000000000..c30cc619aa --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaInputStreamUploadRequestExecutor.java @@ -0,0 +1,56 @@ +package me.chanjar.weixin.common.util.http.okhttp; + +import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.InputStreamData; +import me.chanjar.weixin.common.util.http.MediaInputStreamUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; +import okhttp3.*; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * 文件输入流上传. + * + * @author meiqin.zhou91@gmail.com + * created on 2022/02/15 + */ +public class OkHttpMediaInputStreamUploadRequestExecutor extends MediaInputStreamUploadRequestExecutor { + public OkHttpMediaInputStreamUploadRequestExecutor(RequestHttp requestHttp) { + super(requestHttp); + } + + @Override + public WxMediaUploadResult execute(String uri, InputStreamData data, WxType wxType) throws WxErrorException, IOException { + + RequestBody body = new MultipartBody.Builder() + .setType(MediaType.parse("multipart/form-data")) + .addFormDataPart("media", data.getFilename(), RequestBody.create(this.toByteArray(data.getInputStream()), MediaType.parse("application/octet-stream"))) + .build(); + Request request = new Request.Builder().url(uri).post(body).build(); + + Response response = requestHttp.getRequestHttpClient().newCall(request).execute(); + String responseContent = response.body().string(); + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + return WxMediaUploadResult.fromJson(responseContent); + } + + + public byte[] toByteArray(InputStream input) throws IOException { + try (ByteArrayOutputStream output = new ByteArrayOutputStream()) { + byte[] buffer = new byte[4096]; + int n = 0; + while (-1 != (n = input.read(buffer))) { + output.write(buffer, 0, n); + } + return output.toByteArray(); + } + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaUploadRequestExecutor.java index 2fef1f93ca..6a7b0b794d 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaUploadRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMediaUploadRequestExecutor.java @@ -1,6 +1,6 @@ package me.chanjar.weixin.common.util.http.okhttp; -import me.chanjar.weixin.common.WxType; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; @@ -15,10 +15,10 @@ * . * * @author ecoolper - * @date 2017/5/5 + * created on 2017/5/5 */ public class OkHttpMediaUploadRequestExecutor extends MediaUploadRequestExecutor { - public OkHttpMediaUploadRequestExecutor(RequestHttp requestHttp) { + public OkHttpMediaUploadRequestExecutor(RequestHttp requestHttp) { super(requestHttp); } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMinishopMediaUploadRequestCustomizeExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMinishopMediaUploadRequestCustomizeExecutor.java new file mode 100644 index 0000000000..a2c78f423b --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMinishopMediaUploadRequestCustomizeExecutor.java @@ -0,0 +1,58 @@ +package me.chanjar.weixin.common.util.http.okhttp; + +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.result.WxMinishopImageUploadCustomizeResult; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.MinishopUploadRequestCustomizeExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; +import okhttp3.*; + +import java.io.File; +import java.io.IOException; + +/** + * @author liming1019 + * created on 2021/8/10 + */ +@Slf4j +public class OkHttpMinishopMediaUploadRequestCustomizeExecutor extends MinishopUploadRequestCustomizeExecutor { + public OkHttpMinishopMediaUploadRequestCustomizeExecutor(RequestHttp requestHttp, String respType, String imgUrl) { + super(requestHttp, respType, imgUrl); + } + + @Override + public WxMinishopImageUploadCustomizeResult execute(String uri, File file, WxType wxType) throws WxErrorException, IOException { + + RequestBody body = null; + if (this.uploadType.equals("0")) { + body = new MultipartBody.Builder() + .setType(MediaType.parse("multipart/form-data")) + .addFormDataPart("resp_type", this.respType) + .addFormDataPart("upload_type", this.uploadType) + .addFormDataPart("media", file.getName(), RequestBody.create(MediaType.parse("application/octet-stream"), file)) + .build(); + } + else { + body = new MultipartBody.Builder() + .setType(MediaType.parse("multipart/form-data")) + .addFormDataPart("resp_type", this.respType) + .addFormDataPart("upload_type", this.uploadType) + .addFormDataPart("img_url", this.imgUrl) + .build(); + } + Request request = new Request.Builder().url(uri).post(body).build(); + + Response response = requestHttp.getRequestHttpClient().newCall(request).execute(); + String responseContent = response.body().string(); + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + log.info("responseContent: {}", responseContent); + + return WxMinishopImageUploadCustomizeResult.fromJson(responseContent); + } + +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMinishopMediaUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMinishopMediaUploadRequestExecutor.java new file mode 100644 index 0000000000..f2df3c7e73 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpMinishopMediaUploadRequestExecutor.java @@ -0,0 +1,49 @@ +package me.chanjar.weixin.common.util.http.okhttp; + +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.result.WxMinishopImageUploadResult; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.MinishopUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; +import okhttp3.*; + +import java.io.File; +import java.io.IOException; + +/** + * . + * + * @author ecoolper + * created on 2017/5/5 + */ +@Slf4j +public class OkHttpMinishopMediaUploadRequestExecutor extends MinishopUploadRequestExecutor { + public OkHttpMinishopMediaUploadRequestExecutor(RequestHttp requestHttp) { + super(requestHttp); + } + + @Override + public WxMinishopImageUploadResult execute(String uri, File file, WxType wxType) throws WxErrorException, IOException { + + RequestBody body = new MultipartBody.Builder() + .setType(MediaType.parse("multipart/form-data")) + .addFormDataPart("media", + file.getName(), + RequestBody.create(MediaType.parse("application/octet-stream"), file)) + .build(); + Request request = new Request.Builder().url(uri).post(body).build(); + + Response response = requestHttp.getRequestHttpClient().newCall(request).execute(); + String responseContent = response.body().string(); + WxError error = WxError.fromJson(responseContent, wxType); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + log.info("responseContent: {}", responseContent); + + return WxMinishopImageUploadResult.fromJson(responseContent); + } + +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpResponseProxy.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpResponseProxy.java new file mode 100644 index 0000000000..95c290735c --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpResponseProxy.java @@ -0,0 +1,20 @@ +package me.chanjar.weixin.common.util.http.okhttp; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.HttpResponseProxy; +import okhttp3.Response; + +public class OkHttpResponseProxy implements HttpResponseProxy { + + private final Response response; + + public OkHttpResponseProxy(Response response) { + this.response = response; + } + + @Override + public String getFileName() throws WxErrorException { + String content = this.response.header("Content-disposition"); + return HttpResponseProxy.extractFileNameFromContentString(content); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpSimpleGetRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpSimpleGetRequestExecutor.java index dbaa27c544..d475222872 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpSimpleGetRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpSimpleGetRequestExecutor.java @@ -1,7 +1,6 @@ package me.chanjar.weixin.common.util.http.okhttp; -import me.chanjar.weixin.common.WxType; -import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.util.http.RequestHttp; import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor; @@ -12,13 +11,13 @@ import java.io.IOException; /** - * . + * * * @author ecoolper - * @date 2017/5/4 + * created on 2017/5/4 */ public class OkHttpSimpleGetRequestExecutor extends SimpleGetRequestExecutor { - public OkHttpSimpleGetRequestExecutor(RequestHttp requestHttp) { + public OkHttpSimpleGetRequestExecutor(RequestHttp requestHttp) { super(requestHttp); } @@ -35,12 +34,7 @@ public String execute(String uri, String queryParam, WxType wxType) throws WxErr OkHttpClient client = requestHttp.getRequestHttpClient(); Request request = new Request.Builder().url(uri).build(); Response response = client.newCall(request).execute(); - String responseContent = response.body().string(); - WxError error = WxError.fromJson(responseContent, wxType); - if (error.getErrorCode() != 0) { - throw new WxErrorException(error); - } - return responseContent; + return this.handleResponse(wxType, response.body().string()); } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpSimplePostRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpSimplePostRequestExecutor.java index 3be6152055..3044f29d60 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpSimplePostRequestExecutor.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/okhttp/OkHttpSimplePostRequestExecutor.java @@ -1,39 +1,33 @@ package me.chanjar.weixin.common.util.http.okhttp; import lombok.extern.slf4j.Slf4j; -import me.chanjar.weixin.common.WxType; -import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.util.http.RequestHttp; import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor; import okhttp3.*; import java.io.IOException; +import java.util.Objects; /** * . * * @author ecoolper - * @date 2017/5/4 + * created on 2017/5/4 */ @Slf4j public class OkHttpSimplePostRequestExecutor extends SimplePostRequestExecutor { - public OkHttpSimplePostRequestExecutor(RequestHttp requestHttp) { + public OkHttpSimplePostRequestExecutor(RequestHttp requestHttp) { super(requestHttp); } @Override public String execute(String uri, String postEntity, WxType wxType) throws WxErrorException, IOException { - RequestBody body = RequestBody.create(MediaType.parse("text/plain; charset=utf-8"), postEntity); + RequestBody body = RequestBody.Companion.create(postEntity, MediaType.parse("application/json; charset=utf-8")); Request request = new Request.Builder().url(uri).post(body).build(); Response response = requestHttp.getRequestHttpClient().newCall(request).execute(); - String responseContent = response.body().string(); - WxError error = WxError.fromJson(responseContent, wxType); - if (error.getErrorCode() != 0) { - throw new WxErrorException(error); - } - - return responseContent; + return this.handleResponse(wxType, Objects.requireNonNull(response.body()).string()); } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonHelper.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonHelper.java index 882853945a..0d807402ac 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonHelper.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonHelper.java @@ -4,6 +4,7 @@ import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import me.chanjar.weixin.common.error.WxRuntimeException; import java.util.List; @@ -151,4 +152,54 @@ public static Long[] getLongArray(JsonObject o, String string) { public static JsonArray getAsJsonArray(JsonElement element) { return element == null ? null : element.getAsJsonArray(); } + + /** + * 快速构建JsonObject对象,批量添加一堆属性 + * + * @param keyOrValue 包含key或value的数组 + * @return JsonObject对象. + */ + public static JsonObject buildJsonObject(Object... keyOrValue) { + JsonObject result = new JsonObject(); + put(result, keyOrValue); + return result; + } + + /** + * 批量向JsonObject对象中添加属性 + * + * @param jsonObject 原始JsonObject对象 + * @param keyOrValue 包含key或value的数组 + */ + public static void put(JsonObject jsonObject, Object... keyOrValue) { + if (keyOrValue.length % 2 == 1) { + throw new WxRuntimeException("参数个数必须为偶数"); + } + + for (int i = 0; i < keyOrValue.length / 2; i++) { + final Object key = keyOrValue[2 * i]; + final Object value = keyOrValue[2 * i + 1]; + if (value == null) { + jsonObject.add(key.toString(), null); + continue; + } + + if (value instanceof Boolean) { + jsonObject.addProperty(key.toString(), (Boolean) value); + } else if (value instanceof Character) { + jsonObject.addProperty(key.toString(), (Character) value); + } else if (value instanceof Number) { + jsonObject.addProperty(key.toString(), (Number) value); + } else if (value instanceof JsonElement) { + jsonObject.add(key.toString(), (JsonElement) value); + } else if (value instanceof List) { + JsonArray array = new JsonArray(); + ((List) value).forEach(a -> array.add(a.toString())); + jsonObject.add(key.toString(), array); + } else { + jsonObject.add(key.toString(), WxGsonBuilder.create().toJsonTree(value)); + } + } + + } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonParser.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonParser.java new file mode 100644 index 0000000000..caa07d0eaf --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonParser.java @@ -0,0 +1,25 @@ +package me.chanjar.weixin.common.util.json; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.stream.JsonReader; + +import java.io.Reader; + +/** + * @author niefy + */ +public class GsonParser { + + public static JsonObject parse(String json) { + return new JsonParser().parse(json).getAsJsonObject(); + } + + public static JsonObject parse(Reader json) { + return new JsonParser().parse(json).getAsJsonObject(); + } + + public static JsonObject parse(JsonReader json) { + return new JsonParser().parse(json).getAsJsonObject(); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxErrorAdapter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxErrorAdapter.java index 0ea52b9a86..c9301a7750 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxErrorAdapter.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxErrorAdapter.java @@ -1,6 +1,7 @@ package me.chanjar.weixin.common.util.json; import com.google.gson.*; +import me.chanjar.weixin.common.api.WxConsts; import me.chanjar.weixin.common.error.WxError; import java.lang.reflect.Type; @@ -16,8 +17,8 @@ public WxError deserialize(JsonElement json, Type typeOfT, JsonDeserializationCo WxError.WxErrorBuilder errorBuilder = WxError.builder(); JsonObject wxErrorJsonObject = json.getAsJsonObject(); - if (wxErrorJsonObject.get("errcode") != null && !wxErrorJsonObject.get("errcode").isJsonNull()) { - errorBuilder.errorCode(GsonHelper.getAsPrimitiveInt(wxErrorJsonObject.get("errcode"))); + if (wxErrorJsonObject.get(WxConsts.ERR_CODE) != null && !wxErrorJsonObject.get(WxConsts.ERR_CODE).isJsonNull()) { + errorBuilder.errorCode(GsonHelper.getAsPrimitiveInt(wxErrorJsonObject.get(WxConsts.ERR_CODE))); } if (wxErrorJsonObject.get("errmsg") != null && !wxErrorJsonObject.get("errmsg").isJsonNull()) { errorBuilder.errorMsg(GsonHelper.getAsString(wxErrorJsonObject.get("errmsg"))); diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxGsonBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxGsonBuilder.java index b52bad9531..8f3dafe48a 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxGsonBuilder.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxGsonBuilder.java @@ -1,5 +1,7 @@ package me.chanjar.weixin.common.util.json; +import com.google.gson.ExclusionStrategy; +import com.google.gson.FieldAttributes; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import me.chanjar.weixin.common.bean.WxAccessToken; @@ -7,14 +9,18 @@ import me.chanjar.weixin.common.bean.menu.WxMenu; import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; +import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; + +import java.io.File; +import java.util.Objects; /** * . * @author chanjarster */ public class WxGsonBuilder { - private static final GsonBuilder INSTANCE = new GsonBuilder(); + private static volatile Gson GSON_INSTANCE; static { INSTANCE.disableHtmlEscaping(); @@ -24,10 +30,33 @@ public class WxGsonBuilder { INSTANCE.registerTypeAdapter(WxMediaUploadResult.class, new WxMediaUploadResultAdapter()); INSTANCE.registerTypeAdapter(WxNetCheckResult.class, new WxNetCheckResultGsonAdapter()); + INSTANCE.setExclusionStrategies(new ExclusionStrategy() { + @Override + public boolean shouldSkipField(FieldAttributes fieldAttributes) { + return false; + } + + @Override + public boolean shouldSkipClass(Class aClass) { + return aClass == File.class || aClass == ApacheHttpClientBuilder.class; + } + }); } + /** + * 创建Gson实例 + * + * @return Gson实例 + */ public static Gson create() { - return INSTANCE.create(); + if (Objects.isNull(GSON_INSTANCE)) { + synchronized (INSTANCE) { + if (Objects.isNull(GSON_INSTANCE)) { + GSON_INSTANCE = INSTANCE.create(); + } + } + } + return GSON_INSTANCE; } } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxMenuGsonAdapter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxMenuGsonAdapter.java index 31c3c0204c..5e7f9b41d9 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxMenuGsonAdapter.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxMenuGsonAdapter.java @@ -1,11 +1,3 @@ -/* - * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved. - * - * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended - * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction - * arose from modification of the original source, or other redistribution of this source - * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD. - */ package me.chanjar.weixin.common.util.json; import com.google.gson.*; @@ -14,6 +6,7 @@ import me.chanjar.weixin.common.bean.menu.WxMenuRule; import java.lang.reflect.Type; +import java.util.Optional; /** @@ -21,95 +14,111 @@ */ public class WxMenuGsonAdapter implements JsonSerializer, JsonDeserializer { + // JSON字段常量定义 + private static final String FIELD_BUTTON = "button"; + private static final String FIELD_MATCH_RULE = "matchrule"; + private static final String FIELD_SUB_BUTTON = "sub_button"; + private static final String FIELD_MENU = "menu"; + + // 菜单按钮字段常量 + private static final String FIELD_TYPE = "type"; + private static final String FIELD_NAME = "name"; + private static final String FIELD_KEY = "key"; + private static final String FIELD_URL = "url"; + private static final String FIELD_MEDIA_ID = "media_id"; + private static final String FIELD_ARTICLE_ID = "article_id"; + private static final String FIELD_APP_ID = "appid"; + private static final String FIELD_PAGE_PATH = "pagepath"; + + // 菜单规则字段常量 + private static final String FIELD_TAG_ID = "tag_id"; + private static final String FIELD_SEX = "sex"; + private static final String FIELD_COUNTRY = "country"; + private static final String FIELD_PROVINCE = "province"; + private static final String FIELD_CITY = "city"; + private static final String FIELD_CLIENT_PLATFORM_TYPE = "client_platform_type"; + private static final String FIELD_LANGUAGE = "language"; + @Override public JsonElement serialize(WxMenu menu, Type typeOfSrc, JsonSerializationContext context) { JsonObject json = new JsonObject(); - JsonArray buttonArray = new JsonArray(); - for (WxMenuButton button : menu.getButtons()) { - JsonObject buttonJson = convertToJson(button); - buttonArray.add(buttonJson); - } - json.add("button", buttonArray); - + Optional.ofNullable(menu.getButtons()) + .ifPresent(buttons -> buttons.stream() + .map(this::convertToJson) + .forEach(buttonArray::add)); + json.add(FIELD_BUTTON, buttonArray); if (menu.getMatchRule() != null) { - json.add("matchrule", convertToJson(menu.getMatchRule())); + json.add(FIELD_MATCH_RULE, convertToJson(menu.getMatchRule())); } - return json; } protected JsonObject convertToJson(WxMenuButton button) { JsonObject buttonJson = new JsonObject(); - buttonJson.addProperty("type", button.getType()); - buttonJson.addProperty("name", button.getName()); - buttonJson.addProperty("key", button.getKey()); - buttonJson.addProperty("url", button.getUrl()); - buttonJson.addProperty("media_id", button.getMediaId()); - buttonJson.addProperty("appid", button.getAppId()); - buttonJson.addProperty("pagepath", button.getPagePath()); - if (button.getSubButtons() != null && button.getSubButtons().size() > 0) { + addPropertyIfNotNull(buttonJson, FIELD_TYPE, button.getType()); + addPropertyIfNotNull(buttonJson, FIELD_NAME, button.getName()); + addPropertyIfNotNull(buttonJson, FIELD_KEY, button.getKey()); + addPropertyIfNotNull(buttonJson, FIELD_URL, button.getUrl()); + addPropertyIfNotNull(buttonJson, FIELD_MEDIA_ID, button.getMediaId()); + addPropertyIfNotNull(buttonJson, FIELD_ARTICLE_ID, button.getArticleId()); + addPropertyIfNotNull(buttonJson, FIELD_APP_ID, button.getAppId()); + addPropertyIfNotNull(buttonJson, FIELD_PAGE_PATH, button.getPagePath()); + if (button.getSubButtons() != null && !button.getSubButtons().isEmpty()) { JsonArray buttonArray = new JsonArray(); - for (WxMenuButton sub_button : button.getSubButtons()) { - buttonArray.add(convertToJson(sub_button)); - } - buttonJson.add("sub_button", buttonArray); + button.getSubButtons().stream() + .map(this::convertToJson) + .forEach(buttonArray::add); + buttonJson.add(FIELD_SUB_BUTTON, buttonArray); } return buttonJson; } protected JsonObject convertToJson(WxMenuRule menuRule) { JsonObject matchRule = new JsonObject(); - matchRule.addProperty("tag_id", menuRule.getTagId()); - matchRule.addProperty("sex", menuRule.getSex()); - matchRule.addProperty("country", menuRule.getCountry()); - matchRule.addProperty("province", menuRule.getProvince()); - matchRule.addProperty("city", menuRule.getCity()); - matchRule.addProperty("client_platform_type", menuRule.getClientPlatformType()); - matchRule.addProperty("language", menuRule.getLanguage()); + addPropertyIfNotNull(matchRule, FIELD_TAG_ID, menuRule.getTagId()); + addPropertyIfNotNull(matchRule, FIELD_SEX, menuRule.getSex()); + addPropertyIfNotNull(matchRule, FIELD_COUNTRY, menuRule.getCountry()); + addPropertyIfNotNull(matchRule, FIELD_PROVINCE, menuRule.getProvince()); + addPropertyIfNotNull(matchRule, FIELD_CITY, menuRule.getCity()); + addPropertyIfNotNull(matchRule, FIELD_CLIENT_PLATFORM_TYPE, menuRule.getClientPlatformType()); + addPropertyIfNotNull(matchRule, FIELD_LANGUAGE, menuRule.getLanguage()); return matchRule; } - @Deprecated - private WxMenuRule convertToRule(JsonObject json) { - WxMenuRule menuRule = new WxMenuRule(); - //变态的微信接口,这里居然反人类的使用和序列化时不一样的名字 - //menuRule.setTagId(GsonHelper.getString(json,"tag_id")); - menuRule.setTagId(GsonHelper.getString(json, "group_id")); - menuRule.setSex(GsonHelper.getString(json, "sex")); - menuRule.setCountry(GsonHelper.getString(json, "country")); - menuRule.setProvince(GsonHelper.getString(json, "province")); - menuRule.setCity(GsonHelper.getString(json, "city")); - menuRule.setClientPlatformType(GsonHelper.getString(json, "client_platform_type")); - menuRule.setLanguage(GsonHelper.getString(json, "language")); - return menuRule; + private void addPropertyIfNotNull(JsonObject obj, String key, String value) { + if (value != null) { + obj.addProperty(key, value); + } } @Override public WxMenu deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { - /* - * 操蛋的微信 - * 创建菜单时是 { button : ... } - * 查询菜单时是 { menu : { button : ... } } - * 现在企业号升级为企业微信后,没有此问题,因此需要单独处理 - */ - JsonArray buttonsJson = json.getAsJsonObject().get("menu").getAsJsonObject().get("button").getAsJsonArray(); - return this.buildMenuFromJson(buttonsJson); + JsonObject root = json.getAsJsonObject(); + JsonArray buttonsJson = null; + if (root.has(FIELD_MENU)) { + JsonObject menuObj = root.getAsJsonObject(FIELD_MENU); + buttonsJson = menuObj.getAsJsonArray(FIELD_BUTTON); + } else if (root.has(FIELD_BUTTON)) { + buttonsJson = root.getAsJsonArray(FIELD_BUTTON); + } + if (buttonsJson == null) { + throw new JsonParseException("No button array found in menu JSON"); + } + return buildMenuFromJson(buttonsJson); } protected WxMenu buildMenuFromJson(JsonArray buttonsJson) { WxMenu menu = new WxMenu(); - for (int i = 0; i < buttonsJson.size(); i++) { - JsonObject buttonJson = buttonsJson.get(i).getAsJsonObject(); + for (JsonElement btnElem : buttonsJson) { + JsonObject buttonJson = btnElem.getAsJsonObject(); WxMenuButton button = convertFromJson(buttonJson); menu.getButtons().add(button); - if (buttonJson.get("sub_button") == null || buttonJson.get("sub_button").isJsonNull()) { - continue; - } - JsonArray sub_buttonsJson = buttonJson.get("sub_button").getAsJsonArray(); - for (int j = 0; j < sub_buttonsJson.size(); j++) { - JsonObject sub_buttonJson = sub_buttonsJson.get(j).getAsJsonObject(); - button.getSubButtons().add(convertFromJson(sub_buttonJson)); + if (buttonJson.has(FIELD_SUB_BUTTON) && buttonJson.get(FIELD_SUB_BUTTON).isJsonArray()) { + JsonArray sub_buttonsJson = buttonJson.getAsJsonArray(FIELD_SUB_BUTTON); + for (JsonElement subBtnElem : sub_buttonsJson) { + button.getSubButtons().add(convertFromJson(subBtnElem.getAsJsonObject())); + } } } return menu; @@ -117,13 +126,14 @@ protected WxMenu buildMenuFromJson(JsonArray buttonsJson) { protected WxMenuButton convertFromJson(JsonObject json) { WxMenuButton button = new WxMenuButton(); - button.setName(GsonHelper.getString(json, "name")); - button.setKey(GsonHelper.getString(json, "key")); - button.setUrl(GsonHelper.getString(json, "url")); - button.setType(GsonHelper.getString(json, "type")); - button.setMediaId(GsonHelper.getString(json, "media_id")); - button.setAppId(GsonHelper.getString(json, "appid")); - button.setPagePath(GsonHelper.getString(json, "pagepath")); + button.setName(GsonHelper.getString(json, FIELD_NAME)); + button.setKey(GsonHelper.getString(json, FIELD_KEY)); + button.setUrl(GsonHelper.getString(json, FIELD_URL)); + button.setType(GsonHelper.getString(json, FIELD_TYPE)); + button.setMediaId(GsonHelper.getString(json, FIELD_MEDIA_ID)); + button.setArticleId(GsonHelper.getString(json, FIELD_ARTICLE_ID)); + button.setAppId(GsonHelper.getString(json, FIELD_APP_ID)); + button.setPagePath(GsonHelper.getString(json, FIELD_PAGE_PATH)); return button; } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxNetCheckResultGsonAdapter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxNetCheckResultGsonAdapter.java index 65c15fbc38..61492cbc7a 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxNetCheckResultGsonAdapter.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/WxNetCheckResultGsonAdapter.java @@ -20,7 +20,7 @@ public WxNetCheckResult deserialize(JsonElement json, Type typeOfT, JsonDeserial JsonArray dnssJson = json.getAsJsonObject().get("dns").getAsJsonArray(); List dnsInfoList = new ArrayList<>(); - if (dnssJson != null && dnssJson.size() > 0) { + if (dnssJson != null && !dnssJson.isEmpty()) { for (int i = 0; i < dnssJson.size(); i++) { JsonObject buttonJson = dnssJson.get(i).getAsJsonObject(); WxNetCheckResult.WxNetCheckDnsInfo dnsInfo = new WxNetCheckResult.WxNetCheckDnsInfo(); @@ -32,7 +32,7 @@ public WxNetCheckResult deserialize(JsonElement json, Type typeOfT, JsonDeserial JsonArray pingsJson = json.getAsJsonObject().get("ping").getAsJsonArray(); List pingInfoList = new ArrayList<>(); - if (pingsJson != null && pingsJson.size() > 0) { + if (pingsJson != null && !pingsJson.isEmpty()) { for (int i = 0; i < pingsJson.size(); i++) { JsonObject pingJson = pingsJson.get(i).getAsJsonObject(); WxNetCheckResult.WxNetCheckPingInfo pingInfo = new WxNetCheckResult.WxNetCheckPingInfo(); diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/JedisDistributedLock.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/JedisDistributedLock.java new file mode 100644 index 0000000000..115777d1c9 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/JedisDistributedLock.java @@ -0,0 +1,76 @@ +package me.chanjar.weixin.common.util.locks; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; + +import com.github.jedis.lock.JedisLock; +import me.chanjar.weixin.common.error.WxRuntimeException; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.util.Pool; + +/** + * JedisPool 分布式锁 + * @deprecated 不建议使用jedis-lock这个过期组件,不可靠 + * + * @author 007 + */ +@Deprecated +public class JedisDistributedLock implements Lock { + private final Pool jedisPool; + private final JedisLock lock; + + public JedisDistributedLock(Pool jedisPool, String key){ + this.jedisPool = jedisPool; + this.lock = new JedisLock(key); + } + + @Override + public void lock() { + try (Jedis jedis = jedisPool.getResource()) { + if (!lock.acquire(jedis)) { + throw new WxRuntimeException("acquire timeouted"); + } + } catch (InterruptedException e) { + throw new WxRuntimeException("lock failed", e); + } + } + + @Override + public void lockInterruptibly() throws InterruptedException { + try (Jedis jedis = jedisPool.getResource()) { + if (!lock.acquire(jedis)) { + throw new WxRuntimeException("acquire timeouted"); + } + } + } + + @Override + public boolean tryLock() { + try (Jedis jedis = jedisPool.getResource()) { + return lock.acquire(jedis); + } catch (InterruptedException e) { + throw new WxRuntimeException("lock failed", e); + } + } + + @Override + public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { + try (Jedis jedis = jedisPool.getResource()) { + return lock.acquire(jedis); + } + } + + @Override + public void unlock() { + try (Jedis jedis = jedisPool.getResource()) { + lock.release(jedis); + } + } + + @Override + public Condition newCondition() { + throw new WxRuntimeException("unsupported method"); + } + +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java new file mode 100644 index 0000000000..3f5ce4d692 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java @@ -0,0 +1,115 @@ +package me.chanjar.weixin.common.util.locks; + +import lombok.Getter; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.core.script.RedisScript; + +import java.util.Collections; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; + +/** + * 实现简单的redis分布式锁, 支持重入, 不是红锁 + * + * @see reids distlock + */ +public class RedisTemplateSimpleDistributedLock implements Lock { + + @Getter + private final StringRedisTemplate redisTemplate; + @Getter + private final String key; + @Getter + private final int leaseMilliseconds; + + private final ThreadLocal valueThreadLocal = new ThreadLocal<>(); + + public RedisTemplateSimpleDistributedLock( StringRedisTemplate redisTemplate, int leaseMilliseconds) { + this(redisTemplate, "lock:" + UUID.randomUUID().toString(), leaseMilliseconds); + } + + public RedisTemplateSimpleDistributedLock( StringRedisTemplate redisTemplate, String key, int leaseMilliseconds) { + if (leaseMilliseconds <= 0) { + throw new IllegalArgumentException("Parameter 'leaseMilliseconds' must grate then 0: " + leaseMilliseconds); + } + this.redisTemplate = redisTemplate; + this.key = key; + this.leaseMilliseconds = leaseMilliseconds; + } + + @Override + public void lock() { + while (!tryLock()) { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + // Ignore + } + } + } + + @Override + public void lockInterruptibly() throws InterruptedException { + while (!tryLock()) { + Thread.sleep(1000); + } + } + + @Override + public boolean tryLock() { + String value = valueThreadLocal.get(); + if (value == null || value.isEmpty()) { + value = UUID.randomUUID().toString(); + valueThreadLocal.set(value); + } + + // Use high-level StringRedisTemplate API to ensure consistent key serialization + Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(key, value, leaseMilliseconds, TimeUnit.MILLISECONDS); + if (Boolean.TRUE.equals(lockAcquired)) { + return true; + } + + // Check if we already hold the lock (reentrant behavior) + String currentValue = redisTemplate.opsForValue().get(key); + return value.equals(currentValue); + } + + @Override + public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { + long waitMs = unit.toMillis(time); + boolean locked = tryLock(); + while (!locked && waitMs > 0) { + long sleep = waitMs < 1000 ? waitMs : 1000; + Thread.sleep(sleep); + waitMs -= sleep; + locked = tryLock(); + } + return locked; + } + + @Override + public void unlock() { + if (valueThreadLocal.get() != null) { + // 提示: 必须指定returnType, 类型: 此处必须为Long, 不能是Integer + RedisScript script = new DefaultRedisScript<>("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", Long.class); + redisTemplate.execute(script, Collections.singletonList(key), valueThreadLocal.get()); + valueThreadLocal.remove(); + } + } + + @Override + public Condition newCondition() { + throw new UnsupportedOperationException(); + } + + /** + * 获取当前锁的值 + * return 返回null意味着没有加锁, 但是返回非null值并不以为着当前加锁成功(redis中key可能自动过期) + */ + public String getLockSecretValue() { + return valueThreadLocal.get(); + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/res/StringManager.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/res/StringManager.java index e5bdb38804..fd2f13a553 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/res/StringManager.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/res/StringManager.java @@ -102,7 +102,7 @@ private StringManager(String packageName, Locale locale) { * * @param packageName The package name */ - public static final synchronized StringManager getManager( + public static synchronized StringManager getManager( String packageName) { return getManager(packageName, Locale.getDefault()); } @@ -115,7 +115,7 @@ public static final synchronized StringManager getManager( * @param packageName The package name * @param locale The Locale */ - public static final synchronized StringManager getManager( + public static synchronized StringManager getManager( String packageName, Locale locale) { Map map = MANAGERS.get(packageName); diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/IntegerArrayConverter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/IntegerArrayConverter.java index 3a82b213ca..710547c746 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/IntegerArrayConverter.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/IntegerArrayConverter.java @@ -9,7 +9,7 @@ * Integer型数组转换器. * * @author Binary Wang - * @date 2019-08-22 + * created on 2019-08-22 */ public class IntegerArrayConverter extends StringConverter { @Override @@ -24,6 +24,11 @@ public String toString(Object obj) { @Override public Object fromString(String str) { + + if (str == null || str.isEmpty()) { + return null; + } + final Iterable iterable = Splitter.on(",").split(str); final String[] strings = Iterables.toArray(iterable, String.class); Integer[] result = new Integer[strings.length]; diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/LongArrayConverter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/LongArrayConverter.java index a383c59674..ca5f8ac9a4 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/LongArrayConverter.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/LongArrayConverter.java @@ -9,7 +9,7 @@ * Long型数组转换器. * * @author Binary Wang - * @date 2019-08-22 + * created on 2019-08-22 */ public class LongArrayConverter extends StringConverter { @Override diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/StringArrayConverter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/StringArrayConverter.java new file mode 100644 index 0000000000..44d2926f42 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/StringArrayConverter.java @@ -0,0 +1,30 @@ +package me.chanjar.weixin.common.util.xml; + +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; +import com.google.common.collect.Iterables; +import com.thoughtworks.xstream.converters.basic.StringConverter; + + +/** + * String 数组转换 + * @author chily.lin + */ +public class StringArrayConverter extends StringConverter { + @Override + public boolean canConvert(Class type) { + return type == String[].class; + } + + @Override + public String toString(Object obj) { + return ""; + } + + @Override + public Object fromString(String str) { + final Iterable iterable = Splitter.on(",").split(str); + String[] results = Iterables.toArray(iterable, String.class); + return results; + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamCDataListConverter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamCDataListConverter.java new file mode 100644 index 0000000000..0b55a9c037 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamCDataListConverter.java @@ -0,0 +1,54 @@ +package me.chanjar.weixin.common.util.xml; + +import com.thoughtworks.xstream.converters.Converter; +import com.thoughtworks.xstream.converters.MarshallingContext; +import com.thoughtworks.xstream.converters.UnmarshallingContext; +import com.thoughtworks.xstream.io.HierarchicalStreamReader; +import com.thoughtworks.xstream.io.HierarchicalStreamWriter; + +/** + * 兼容两种格式的字符串列表转换器: + *
    + *
  • 旧格式(4.8.0之前):<MemChangeList><![CDATA[id1,id2]]></MemChangeList>
  • + *
  • 新格式(4.8.0起):<MemChangeList><Item><![CDATA[id1]]></Item></MemChangeList>
  • + *
+ * 解析结果统一为逗号分隔的字符串。 + */ +public class XStreamCDataListConverter implements Converter { + + @Override + public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingContext context) { + if (source != null) { + writer.setValue(""); + } + } + + @Override + public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) { + if (reader.hasMoreChildren()) { + // 新格式:含有 子元素 + StringBuilder sb = new StringBuilder(); + while (reader.hasMoreChildren()) { + reader.moveDown(); + String value = reader.getValue(); + if (value != null && !value.isEmpty()) { + if (sb.length() > 0) { + sb.append(","); + } + sb.append(value); + } + reader.moveUp(); + } + return sb.length() > 0 ? sb.toString() : null; + } else { + // 旧格式:直接 CDATA 文本 + String value = reader.getValue(); + return (value != null && !value.isEmpty()) ? value : null; + } + } + + @Override + public boolean canConvert(Class type) { + return type == String.class; + } +} diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamInitializer.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamInitializer.java index 639fcf08d1..51cd1e980c 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamInitializer.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamInitializer.java @@ -1,15 +1,36 @@ package me.chanjar.weixin.common.util.xml; -import java.io.Writer; - import com.thoughtworks.xstream.XStream; +import com.thoughtworks.xstream.converters.basic.*; +import com.thoughtworks.xstream.converters.collections.CollectionConverter; import com.thoughtworks.xstream.converters.reflection.PureJavaReflectionProvider; +import com.thoughtworks.xstream.converters.reflection.ReflectionConverter; import com.thoughtworks.xstream.core.util.QuickWriter; import com.thoughtworks.xstream.io.HierarchicalStreamWriter; import com.thoughtworks.xstream.io.xml.PrettyPrintWriter; import com.thoughtworks.xstream.io.xml.XppDriver; +import com.thoughtworks.xstream.security.NoTypePermission; +import com.thoughtworks.xstream.security.WildcardTypePermission; +import java.io.Writer; +/** + * The type X stream initializer. + * + * @author Daniel Qian + */ public class XStreamInitializer { + + public static ClassLoader classLoader; + + /** + * 设置类加载器 + * + * @param classLoaderInfo 类加载器 + */ + public static void setClassLoader(ClassLoader classLoaderInfo) { + classLoader = classLoaderInfo; + } + private static final XppDriver XPP_DRIVER = new XppDriver() { @Override public HierarchicalStreamWriter createWriter(Writer out) { @@ -18,6 +39,8 @@ public HierarchicalStreamWriter createWriter(Writer out) { private static final String SUFFIX_CDATA = "]]>"; private static final String PREFIX_MEDIA_ID = ""; private static final String SUFFIX_MEDIA_ID = ""; + private static final String PREFIX_REPLACE_NAME = ""; + private static final String SUFFIX_REPLACE_NAME = ""; @Override protected void writeText(QuickWriter writer, String text) { @@ -25,6 +48,8 @@ protected void writeText(QuickWriter writer, String text) { writer.write(text); } else if (text.startsWith(PREFIX_MEDIA_ID) && text.endsWith(SUFFIX_MEDIA_ID)) { writer.write(text); + } else if (text.startsWith(PREFIX_REPLACE_NAME) && text.endsWith(SUFFIX_REPLACE_NAME)){ + writer.write(text); } else { super.writeText(writer, text); } @@ -40,16 +65,43 @@ public String encodeNode(String name) { } }; + /** + * Gets instance. + * + * @return the instance + */ public static XStream getInstance() { - XStream xstream = new XStream(new PureJavaReflectionProvider(), XPP_DRIVER); + XStream xstream = new XStream(new PureJavaReflectionProvider(), XPP_DRIVER) { + // only register the converters we need; other converters generate a private access warning in the console on Java9+... + @Override + protected void setupConverters() { + registerConverter(new NullConverter(), PRIORITY_VERY_HIGH); + registerConverter(new IntConverter(), PRIORITY_NORMAL); + registerConverter(new FloatConverter(), PRIORITY_NORMAL); + registerConverter(new DoubleConverter(), PRIORITY_NORMAL); + registerConverter(new LongConverter(), PRIORITY_NORMAL); + registerConverter(new ShortConverter(), PRIORITY_NORMAL); + registerConverter(new BooleanConverter(), PRIORITY_NORMAL); + registerConverter(new ByteConverter(), PRIORITY_NORMAL); + registerConverter(new StringConverter(), PRIORITY_NORMAL); + registerConverter(new DateConverter(), PRIORITY_NORMAL); + registerConverter(new CollectionConverter(getMapper()), PRIORITY_NORMAL); + registerConverter(new ReflectionConverter(getMapper(), getReflectionProvider()), PRIORITY_VERY_LOW); + } + }; xstream.ignoreUnknownElements(); xstream.setMode(XStream.NO_REFERENCES); - XStream.setupDefaultSecurity(xstream); - xstream.allowTypesByWildcard(new String[]{ - "me.chanjar.weixin.**", "cn.binarywang.wx.**", "com.github.binarywang.**" - }); + xstream.autodetectAnnotations(true); - xstream.setClassLoader(Thread.currentThread().getContextClassLoader()); + // setup proper security by limiting which classes can be loaded by XStream + xstream.addPermission(NoTypePermission.NONE); + xstream.addPermission(new WildcardTypePermission(new String[]{ + "me.chanjar.weixin.**", "cn.binarywang.wx.**", "com.github.binarywang.**" + })); + if (null == classLoader) { + classLoader = Thread.currentThread().getContextClassLoader(); + } + xstream.setClassLoader(classLoader); return xstream; } diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamReplaceNameConverter.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamReplaceNameConverter.java new file mode 100644 index 0000000000..a136934383 --- /dev/null +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/xml/XStreamReplaceNameConverter.java @@ -0,0 +1,8 @@ +package me.chanjar.weixin.common.util.xml; + +public class XStreamReplaceNameConverter extends XStreamCDataConverter { + @Override + public String toString(Object obj) { + return "" + super.toString(obj) + ""; + } +} diff --git a/weixin-java-common/src/main/resources/META-INF/native-image/com.github.binarywang/weixin-java-common/native-image.properties b/weixin-java-common/src/main/resources/META-INF/native-image/com.github.binarywang/weixin-java-common/native-image.properties new file mode 100644 index 0000000000..e1e601713f --- /dev/null +++ b/weixin-java-common/src/main/resources/META-INF/native-image/com.github.binarywang/weixin-java-common/native-image.properties @@ -0,0 +1,4 @@ +Args = --initialize-at-run-time=org.apache.http.impl.auth.NTLMEngineImpl \ + --initialize-at-run-time=org.apache.http.impl.auth.NTLMEngine \ + --initialize-at-run-time=org.apache.http.impl.auth.KerberosScheme \ + --initialize-at-run-time=org.apache.http.impl.auth.SPNegoScheme diff --git a/weixin-java-common/src/main/resources/META-INF/native-image/com.github.binarywang/weixin-java-common/reflect-config.json b/weixin-java-common/src/main/resources/META-INF/native-image/com.github.binarywang/weixin-java-common/reflect-config.json new file mode 100644 index 0000000000..3bf76c8dab --- /dev/null +++ b/weixin-java-common/src/main/resources/META-INF/native-image/com.github.binarywang/weixin-java-common/reflect-config.json @@ -0,0 +1,14 @@ +[ + { + "name": "me.chanjar.weixin.common.util.RandomUtils", + "methods": [ + {"name": "getRandomStr", "parameterTypes": []} + ] + }, + { + "name": "me.chanjar.weixin.common.util.crypto.WxCryptUtil", + "allDeclaredConstructors": true, + "allDeclaredMethods": true, + "allDeclaredFields": true + } +] diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/api/WxMessageInMemoryDuplicateCheckerSingletonTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/api/WxMessageInMemoryDuplicateCheckerSingletonTest.java new file mode 100644 index 0000000000..df1ee7e2bc --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/api/WxMessageInMemoryDuplicateCheckerSingletonTest.java @@ -0,0 +1,44 @@ +package me.chanjar.weixin.common.api; + +import org.testng.annotations.Test; + +import java.util.concurrent.TimeUnit; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * @author jiangby + * @version 1.0 + * created on 2022/5/26 1:46 + */ +@Test +public class WxMessageInMemoryDuplicateCheckerSingletonTest { + + private static WxMessageInMemoryDuplicateCheckerSingleton checkerSingleton = WxMessageInMemoryDuplicateCheckerSingleton.getInstance(); + + public void test() throws InterruptedException { + Long[] msgIds = new Long[]{1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L}; + + // 第一次检查 + for (Long msgId : msgIds) { + boolean result = checkerSingleton.isDuplicate(String.valueOf(msgId)); + assertFalse(result); + } + + // 初始化后1S进行检查 每五秒检查一次,过期时间为15秒,过15秒再检查 + TimeUnit.SECONDS.sleep(15); + for (Long msgId : msgIds) { + boolean result = checkerSingleton.isDuplicate(String.valueOf(msgId)); + assertTrue(result); + } + + // 过6秒再检查 + TimeUnit.SECONDS.sleep(6); + for (Long msgId : msgIds) { + boolean result = checkerSingleton.isDuplicate(String.valueOf(msgId)); + assertFalse(result); + } + + } +} diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/api/WxMessageInMemoryDuplicateCheckerTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/api/WxMessageInMemoryDuplicateCheckerTest.java index 6f98b3d986..fd8819272c 100644 --- a/weixin-java-common/src/test/java/me/chanjar/weixin/common/api/WxMessageInMemoryDuplicateCheckerTest.java +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/api/WxMessageInMemoryDuplicateCheckerTest.java @@ -1,6 +1,5 @@ package me.chanjar.weixin.common.api; -import org.testng.Assert; import org.testng.annotations.Test; import java.util.concurrent.TimeUnit; diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/api/WxMessageInRedisDuplicateCheckerTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/api/WxMessageInRedisDuplicateCheckerTest.java new file mode 100644 index 0000000000..382618862a --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/api/WxMessageInRedisDuplicateCheckerTest.java @@ -0,0 +1,57 @@ +package me.chanjar.weixin.common.api; + +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.util.concurrent.TimeUnit; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +@Test +public class WxMessageInRedisDuplicateCheckerTest { + + private RedissonClient redissonClient; + + @BeforeTest + public void init() { + Config config = new Config(); + config.useSingleServer().setAddress("redis://127.0.0.1:6379"); + config.setTransportMode(TransportMode.NIO); + this.redissonClient = Redisson.create(config); + checker = new WxMessageInRedisDuplicateChecker(redissonClient); + checker.setExpire(2); + } + + private WxMessageInRedisDuplicateChecker checker; + + public void test() throws InterruptedException { + Long[] msgIds = new Long[]{1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L}; + + // 第一次检查 + for (Long msgId : msgIds) { + boolean result = checker.isDuplicate(String.valueOf(msgId)); + assertFalse(result); + } + + // 过1秒再检查 + TimeUnit.SECONDS.sleep(1); + for (Long msgId : msgIds) { + boolean result = checker.isDuplicate(String.valueOf(msgId)); + assertTrue(result); + } + + // 过1.5秒再检查 + TimeUnit.MILLISECONDS.sleep(1500L); + for (Long msgId : msgIds) { + boolean result = checker.isDuplicate(String.valueOf(msgId)); + assertFalse(result); + } + + } + +} diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/CommonUploadParamTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/CommonUploadParamTest.java new file mode 100644 index 0000000000..05c8b379d3 --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/CommonUploadParamTest.java @@ -0,0 +1,119 @@ +package me.chanjar.weixin.common.bean; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.File; +import java.util.HashMap; +import java.util.Map; + +/** + * CommonUploadParam 单元测试 + * + * @author Binary Wang + */ +@Test +public class CommonUploadParamTest { + + @Test + public void testFromFile() { + File file = new File("test.txt"); + CommonUploadParam param = CommonUploadParam.fromFile("media", file); + + Assert.assertNotNull(param); + Assert.assertEquals(param.getName(), "media"); + Assert.assertNotNull(param.getData()); + Assert.assertNull(param.getFormFields()); + } + + @Test + public void testFromBytes() { + byte[] bytes = "test content".getBytes(); + CommonUploadParam param = CommonUploadParam.fromBytes("media", "test.txt", bytes); + + Assert.assertNotNull(param); + Assert.assertEquals(param.getName(), "media"); + Assert.assertNotNull(param.getData()); + Assert.assertEquals(param.getData().getFileName(), "test.txt"); + Assert.assertNull(param.getFormFields()); + } + + @Test + public void testAddFormField() { + File file = new File("test.txt"); + CommonUploadParam param = CommonUploadParam.fromFile("media", file); + + // 添加单个表单字段 + param.addFormField("title", "测试标题"); + + Assert.assertNotNull(param.getFormFields()); + Assert.assertEquals(param.getFormFields().size(), 1); + Assert.assertEquals(param.getFormFields().get("title"), "测试标题"); + + // 添加多个表单字段 + param.addFormField("introduction", "测试介绍"); + + Assert.assertEquals(param.getFormFields().size(), 2); + Assert.assertEquals(param.getFormFields().get("introduction"), "测试介绍"); + } + + @Test + public void testAddFormFieldChaining() { + File file = new File("test.txt"); + CommonUploadParam param = CommonUploadParam.fromFile("media", file) + .addFormField("title", "测试标题") + .addFormField("introduction", "测试介绍"); + + Assert.assertNotNull(param.getFormFields()); + Assert.assertEquals(param.getFormFields().size(), 2); + Assert.assertEquals(param.getFormFields().get("title"), "测试标题"); + Assert.assertEquals(param.getFormFields().get("introduction"), "测试介绍"); + } + + @Test + public void testConstructorWithFormFields() { + CommonUploadData data = new CommonUploadData("test.txt", null, 0); + Map formFields = new HashMap<>(); + formFields.put("title", "测试标题"); + formFields.put("introduction", "测试介绍"); + + CommonUploadParam param = new CommonUploadParam("media", data, formFields); + + Assert.assertNotNull(param.getFormFields()); + Assert.assertEquals(param.getFormFields().size(), 2); + Assert.assertEquals(param.getFormFields().get("title"), "测试标题"); + Assert.assertEquals(param.getFormFields().get("introduction"), "测试介绍"); + } + + @Test + public void testToString() { + File file = new File("test.txt"); + CommonUploadParam param = CommonUploadParam.fromFile("media", file) + .addFormField("title", "测试标题"); + + String str = param.toString(); + Assert.assertTrue(str.contains("name:media")); + Assert.assertTrue(str.contains("formFields:")); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testAddFormFieldWithNullFieldName() { + File file = new File("test.txt"); + CommonUploadParam param = CommonUploadParam.fromFile("media", file); + param.addFormField(null, "value"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testAddFormFieldWithEmptyFieldName() { + File file = new File("test.txt"); + CommonUploadParam param = CommonUploadParam.fromFile("media", file); + param.addFormField("", "value"); + } + + @Test(expectedExceptions = IllegalArgumentException.class) + public void testAddFormFieldWithNullFieldValue() { + File file = new File("test.txt"); + CommonUploadParam param = CommonUploadParam.fromFile("media", file); + param.addFormField("fieldName", null); + } +} diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxNetCheckResultTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxNetCheckResultTest.java new file mode 100644 index 0000000000..049b28227f --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/bean/WxNetCheckResultTest.java @@ -0,0 +1,58 @@ +package me.chanjar.weixin.common.bean; + +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * + * @author Binary Wang + * created on 2020-06-06 + */ +public class WxNetCheckResultTest { + + @Test + public void testFromJson() { + String json = "{\n" + + " \"dns\": [\n" + + " {\n" + + " \"ip\": \"111.161.64.40\", \n" + + " \"real_operator\": \"UNICOM\"\n" + + " }, \n" + + " {\n" + + " \"ip\": \"111.161.64.48\", \n" + + " \"real_operator\": \"UNICOM\"\n" + + " }\n" + + " ], \n" + + " \"ping\": [\n" + + " {\n" + + " \"ip\": \"111.161.64.40\", \n" + + " \"from_operator\": \"UNICOM\"," + + " \"package_loss\": \"0%\", \n" + + " \"time\": \"23.079ms\"\n" + + " }, \n" + + " {\n" + + " \"ip\": \"111.161.64.48\", \n" + + " \"from_operator\": \"UNICOM\", \n" + + " \"package_loss\": \"0%\", \n" + + " \"time\": \"21.434ms\"\n" + + " }\n" + + " ]\n" + + "}"; + WxNetCheckResult result = WxNetCheckResult.fromJson(json); + Assert.assertNotNull(result); + Assert.assertNotNull(result.getDnsInfos()); + + WxNetCheckResult.WxNetCheckDnsInfo dnsInfo = new WxNetCheckResult.WxNetCheckDnsInfo(); + dnsInfo.setIp("111.161.64.40"); + dnsInfo.setRealOperator("UNICOM"); + Assert.assertEquals(result.getDnsInfos().get(0), dnsInfo); + + WxNetCheckResult.WxNetCheckPingInfo pingInfo = new WxNetCheckResult.WxNetCheckPingInfo(); + pingInfo.setTime("21.434ms"); + pingInfo.setFromOperator("UNICOM"); + pingInfo.setIp("111.161.64.48"); + pingInfo.setPackageLoss("0%"); + Assert.assertEquals(result.getPingInfos().get(1), pingInfo); + + } +} diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/error/WxErrorTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/error/WxErrorTest.java index 79b80e63aa..456a58ad76 100644 --- a/weixin-java-common/src/test/java/me/chanjar/weixin/common/error/WxErrorTest.java +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/error/WxErrorTest.java @@ -1,6 +1,6 @@ package me.chanjar.weixin.common.error; -import me.chanjar.weixin.common.WxType; +import me.chanjar.weixin.common.enums.WxType; import org.testng.annotations.Test; import static org.testng.Assert.assertEquals; diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnumTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnumTest.java new file mode 100644 index 0000000000..66147bb7ec --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnumTest.java @@ -0,0 +1,62 @@ +package me.chanjar.weixin.common.error; + +import org.testng.annotations.Test; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; + +/** + * 微信小程序错误码枚举测试 + * + * @author GitHub Copilot + */ +@Test +public class WxMaErrorMsgEnumTest { + + public void testFindMsgByCodeForExistingCode() { + String msg = WxMaErrorMsgEnum.findMsgByCode(40001); + assertNotNull(msg); + } + + public void testFindMsgByCodeForNonExistingCode() { + String msg = WxMaErrorMsgEnum.findMsgByCode(999999); + assertNull(msg); + } + + /** + * 验证微信小程序虚拟支付错误码 + */ + public void testVirtualPaymentErrorCodes() { + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490001), "openid错误"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490002), "请求参数字段错误,具体看errmsg"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490003), "签名错误"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490004), "重复操作(赠送和代币支付和充值广告金相关接口会返回,表示之前的操作已经成功)"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490005), "订单已经通过cancel_currency_pay接口退款,不支持再退款"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490006), "代币的退款/支付操作金额不足"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490007), "图片或文字存在敏感内容,禁止使用"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490008), "代币未发布,不允许进行代币操作"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490009), "用户session_key不存在或已过期,请重新登录"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490011), "数据生成中,请稍后调用本接口获取"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490012), "批量任务运行中,请等待完成后才能再次运行"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490013), "禁止对核销状态的单进行退款"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490014), "退款操作进行中,稍后可以使用相同参数重试"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490015), "频率限制"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490016), "退款的left_fee字段与实际不符,请通过query_order接口查询确认"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490018), "广告金充值账户行业id不匹配"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490019), "广告金充值账户id已绑定其他appid"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490020), "广告金充值账户主体名称错误"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490021), "账户未完成进件"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490022), "广告金充值账户无效"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490023), "广告金余额不足"); + assertEquals(WxMaErrorMsgEnum.findMsgByCode(268490024), "广告金充值金额必须大于0"); + } + + /** + * 验证虚拟支付错误码中不存在的编号(如268490010、268490017)返回null + */ + public void testVirtualPaymentMissingCodes() { + assertNull(WxMaErrorMsgEnum.findMsgByCode(268490010)); + assertNull(WxMaErrorMsgEnum.findMsgByCode(268490017)); + } +} diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/CommonWxRedisOpsTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/CommonWxRedisOpsTest.java new file mode 100644 index 0000000000..fb53c8c4b6 --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/CommonWxRedisOpsTest.java @@ -0,0 +1,62 @@ +package me.chanjar.weixin.common.redis; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.concurrent.TimeUnit; + +public class CommonWxRedisOpsTest { + + protected WxRedisOps wxRedisOps; + private String key = "access_token"; + private String value = String.valueOf(System.currentTimeMillis()); + + @Test + public void testGetValue() { + wxRedisOps.setValue(key, value, 3, TimeUnit.SECONDS); + Assert.assertEquals(wxRedisOps.getValue(key), value); + } + + @Test + public void testSetValue() { + String key = "access_token", value = String.valueOf(System.currentTimeMillis()); + wxRedisOps.setValue(key, value, -1, TimeUnit.SECONDS); + wxRedisOps.setValue(key, value, 0, TimeUnit.SECONDS); + wxRedisOps.setValue(key, value, 1, TimeUnit.SECONDS); + } + + @Test + public void testGetExpire() { + String key = "access_token", value = String.valueOf(System.currentTimeMillis()); + wxRedisOps.setValue(key, value, -1, TimeUnit.SECONDS); + Assert.assertTrue(wxRedisOps.getExpire(key) < 0); + wxRedisOps.setValue(key, value, 4, TimeUnit.SECONDS); + Long expireSeconds = wxRedisOps.getExpire(key); + Assert.assertTrue(expireSeconds <= 4 && expireSeconds >= 0); + } + + @Test + public void testGetExpireForNonExistentKey() { + String nonExistentKey = "non_existent_key_" + System.currentTimeMillis(); + Long expire = wxRedisOps.getExpire(nonExistentKey); + // 对于不存在的 key,底层使用 getExpire(key, TimeUnit.SECONDS) 时应返回负值 + // Spring Data Redis 2.x 和 3.x 约定:-2 表示 key 不存在,-1 表示 key 没有过期时间 + // 因此这里不应返回 null,而应返回一个小于 0 的值 + Assert.assertNotNull(expire, "Non-existent key should not have null expiration"); + Assert.assertTrue(expire < 0, "Non-existent key should have negative expiration"); + } + + @Test + public void testExpire() { + String key = "access_token", value = String.valueOf(System.currentTimeMillis()); + wxRedisOps.setValue(key, value, -1, TimeUnit.SECONDS); + wxRedisOps.expire(key, 4, TimeUnit.SECONDS); + Long expireSeconds = wxRedisOps.getExpire(key); + Assert.assertTrue(expireSeconds <= 4 && expireSeconds >= 0); + } + + @Test + public void testGetLock() { + Assert.assertNotNull(wxRedisOps.getLock("access_token_lock")); + } +} diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/JedisWxRedisOpsTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/JedisWxRedisOpsTest.java new file mode 100644 index 0000000000..2ff2c37b81 --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/JedisWxRedisOpsTest.java @@ -0,0 +1,21 @@ +package me.chanjar.weixin.common.redis; + +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; +import redis.clients.jedis.JedisPool; + +public class JedisWxRedisOpsTest extends CommonWxRedisOpsTest { + + JedisPool jedisPool; + + @BeforeTest + public void init() { + this.jedisPool = new JedisPool("127.0.0.1", 6379); + this.wxRedisOps = new JedisWxRedisOps(jedisPool); + } + + @AfterTest + public void destroy() { + this.jedisPool.close(); + } +} diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOpsTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOpsTest.java new file mode 100644 index 0000000000..bf3b35a7cc --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedisTemplateWxRedisOpsTest.java @@ -0,0 +1,26 @@ +package me.chanjar.weixin.common.redis; + +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; + +public class RedisTemplateWxRedisOpsTest extends CommonWxRedisOpsTest { + + StringRedisTemplate redisTemplate; + + @BeforeTest + public void init() { + JedisConnectionFactory connectionFactory = new JedisConnectionFactory(); + connectionFactory.setHostName("127.0.0.1"); + connectionFactory.setPort(6379); + connectionFactory.afterPropertiesSet(); + StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory); + this.redisTemplate = redisTemplate; + this.wxRedisOps = new RedisTemplateWxRedisOps(this.redisTemplate); + } + + @AfterTest + public void destroy() { + } +} diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedissonWxRedisOpsTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedissonWxRedisOpsTest.java new file mode 100644 index 0000000000..48cf7b29be --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/redis/RedissonWxRedisOpsTest.java @@ -0,0 +1,27 @@ +package me.chanjar.weixin.common.redis; + +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.config.TransportMode; +import org.testng.annotations.AfterTest; +import org.testng.annotations.BeforeTest; + +public class RedissonWxRedisOpsTest extends CommonWxRedisOpsTest { + + RedissonClient redissonClient; + + @BeforeTest + public void init() { + Config config = new Config(); + config.useSingleServer().setAddress("redis://127.0.0.1:6379"); + config.setTransportMode(TransportMode.NIO); + this.redissonClient = Redisson.create(config); + this.wxRedisOps = new RedissonWxRedisOps(this.redissonClient); + } + + @AfterTest + public void destroy() { + this.redissonClient.shutdown(); + } +} diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/XmlUtilsTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/XmlUtilsTest.java index 1afd1c1d9c..ff34475ef2 100644 --- a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/XmlUtilsTest.java +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/XmlUtilsTest.java @@ -1,10 +1,10 @@ package me.chanjar.weixin.common.util; +import org.testng.annotations.Test; + import java.util.List; import java.util.Map; -import org.testng.annotations.*; - import static org.assertj.core.api.Assertions.assertThat; /** @@ -17,6 +17,17 @@ */ public class XmlUtilsTest { + @Test(expectedExceptions = {RuntimeException.class}) + public void testXml2Map_xxe() { + String xml = "\n" + + "\n" + + "\n" + + "]>\n" + + ""; + XmlUtils.xml2Map(xml); + } + @Test public void testXml2Map() { String xml = "\n" + @@ -52,27 +63,54 @@ public void testXml2Map() { assertThat(map).isNotNull(); final Map copyrightCheckResult = (Map) map.get("CopyrightCheckResult"); List> resultList = (List>) ((Map) copyrightCheckResult.get("ResultList")).get("item"); - assertThat(copyrightCheckResult).isNotNull(); - assertThat(copyrightCheckResult.get("Count")).isEqualTo("2"); - assertThat(copyrightCheckResult.get("CheckState")).isEqualTo("2"); + assertThat(copyrightCheckResult) + .isNotNull() + .containsEntry("Count", "2") + .containsEntry("CheckState", "2"); + + assertThat(resultList.get(0)).containsEntry("ArticleIdx", "1") + .containsEntry("UserDeclareState", "0") + .containsEntry("AuditState", "2") + .containsEntry("OriginalArticleUrl", "Url_1") + .containsEntry("OriginalArticleType", "1") + .containsEntry("CanReprint", "1") + .containsEntry("NeedReplaceContent", "1") + .containsEntry("NeedShowReprintSource", "1"); + + assertThat(resultList.get(1)).containsEntry("ArticleIdx", "2") + .containsEntry("UserDeclareState", "0") + .containsEntry("AuditState", "2") + .containsEntry("OriginalArticleUrl", "Url_2") + .containsEntry("OriginalArticleType", "1") + .containsEntry("CanReprint", "1") + .containsEntry("NeedReplaceContent", "1") + .containsEntry("NeedShowReprintSource", "1"); + } + + @Test + public void testXml2Map_another() { + String xml = " 1481013459 2247503051 0 1 1 2 "; + + final Map map = XmlUtils.xml2Map(xml); + assertThat(map).isNotNull() + .containsEntry("ToUserName", "gh_4d00ed8d6399") + .containsEntry("FromUserName", "oV5CrjpxgaGXNHIQigzNlgLTnwic") + .containsEntry("CreateTime", "1481013459") + .containsEntry("MsgType", "event"); + + Map publishEventInfo = (Map) map.get("PublishEventInfo"); + assertThat(publishEventInfo).containsEntry("publish_id", "2247503051") + .containsEntry("publish_status", "0") + .containsEntry("article_id", "b5O2OUs25HBxRceL7hfReg-U9QGeq9zQjiDvy WP4Hq4"); - assertThat(resultList.get(0).get("ArticleIdx")).isEqualTo("1"); - assertThat(resultList.get(0).get("UserDeclareState")).isEqualTo("0"); - assertThat(resultList.get(0).get("AuditState")).isEqualTo("2"); - assertThat(resultList.get(0).get("OriginalArticleUrl")).isEqualTo("Url_1"); - assertThat(resultList.get(0).get("OriginalArticleType")).isEqualTo("1"); - assertThat(resultList.get(0).get("CanReprint")).isEqualTo("1"); - assertThat(resultList.get(0).get("NeedReplaceContent")).isEqualTo("1"); - assertThat(resultList.get(0).get("NeedShowReprintSource")).isEqualTo("1"); + Map articleDetail = (Map) publishEventInfo.get("article_detail"); + assertThat(articleDetail).containsEntry("count", "1"); + List< Map> item = (List>) articleDetail.get("item"); + assertThat(item.get(0)).containsEntry("idx", "1") + .containsEntry("article_url", "ARTICLE_URL"); - assertThat(resultList.get(1).get("ArticleIdx")).isEqualTo("2"); - assertThat(resultList.get(1).get("UserDeclareState")).isEqualTo("0"); - assertThat(resultList.get(1).get("AuditState")).isEqualTo("2"); - assertThat(resultList.get(1).get("OriginalArticleUrl")).isEqualTo("Url_2"); - assertThat(resultList.get(1).get("OriginalArticleType")).isEqualTo("1"); - assertThat(resultList.get(1).get("CanReprint")).isEqualTo("1"); - assertThat(resultList.get(1).get("NeedReplaceContent")).isEqualTo("1"); - assertThat(resultList.get(1).get("NeedShowReprintSource")).isEqualTo("1"); + assertThat(item.get(1)).containsEntry("idx", "2") + .containsEntry("article_url", "ARTICLE_URL_2"); } } diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/crypto/WxCryptUtilTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/crypto/WxCryptUtilTest.java index 82cfa9d2d6..b61696c1ea 100755 --- a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/crypto/WxCryptUtilTest.java +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/crypto/WxCryptUtilTest.java @@ -96,7 +96,7 @@ public void testValidateSignatureError() throws ParserConfigurationException, SA String encrypt = nodelist1.item(0).getTextContent(); String fromXML = String.format(this.xmlFormat, encrypt); - pc.decrypt("12345", this.timestamp, this.nonce, fromXML); // 这里签名错误 + pc.decryptXml("12345", this.timestamp, this.nonce, fromXML); // 这里签名错误 } catch (RuntimeException e) { assertEquals(e.getMessage(), "加密消息签名校验失败"); return; diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/fs/FileUtilsTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/fs/FileUtilsTest.java new file mode 100644 index 0000000000..5a25fb1493 --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/fs/FileUtilsTest.java @@ -0,0 +1,34 @@ +package me.chanjar.weixin.common.util.fs; + +import org.apache.commons.io.IOUtils; +import org.testng.annotations.Test; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class FileUtilsTest { + + @Test + public void testCreateTmpFile() throws IOException { + String strings = "abc"; + File tmpFile = FileUtils.createTmpFile(new ByteArrayInputStream(strings.getBytes()), "name", "txt"); + System.out.println(tmpFile); + List lines = IOUtils.readLines(Files.newInputStream(tmpFile.toPath()), Charset.defaultCharset()); + assertThat(lines).hasSize(1); + assertThat(lines.get(0)).isEqualTo(strings); + } + + @Test + public void testTestCreateTmpFile() { + } + + @Test + public void testImageToBase64ByStream() { + } +} diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/HttpResponseProxyTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/HttpResponseProxyTest.java new file mode 100644 index 0000000000..1b20b98d74 --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/HttpResponseProxyTest.java @@ -0,0 +1,27 @@ +package me.chanjar.weixin.common.util.http; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.apache.ApacheHttpResponseProxy; +import org.testng.annotations.Test; + +import static org.testng.Assert.*; + +public class HttpResponseProxyTest { + + @Test + public void testExtractFileNameFromContentString() throws WxErrorException { + String content = "attachment; filename*=utf-8''%E6%B5%8B%E8%AF%95.xlsx; filename=\"æµ�è¯�.xlsx\""; + String filename = HttpResponseProxy.extractFileNameFromContentString(content); + assertNotNull(filename); + assertEquals(filename, "测试.xlsx"); + } + + @Test + public void testExtractFileNameFromContentString_another() throws WxErrorException { + String content = "attachment; filename*=utf-8''%E8%90%A5%E4%B8%9A%E6%89%A7%E7%85%A7.jpg; filename=\"è�¥ä¸�æ�§ç�§.jpg\""; +// String content = "attachment; filename=\"è�¥ä¸�æ�§ç�§.jpg\""; + String filename = HttpResponseProxy.extractFileNameFromContentString(content); + assertNotNull(filename); + assertEquals(filename, "营业执照.jpg"); + } +} diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilderTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilderTest.java index 24a45eea09..7296d29d44 100644 --- a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilderTest.java +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/apache/DefaultApacheHttpClientBuilderTest.java @@ -1,14 +1,24 @@ package me.chanjar.weixin.common.util.http.apache; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.HttpResponseInterceptor; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.protocol.BasicHttpContext; +import org.apache.http.protocol.HttpContext; +import org.apache.http.protocol.HttpCoreContext; import org.testng.Assert; import org.testng.annotations.Test; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; public class DefaultApacheHttpClientBuilderTest { @Test @@ -24,7 +34,7 @@ public void testBuild() throws Exception { } for (TestThread testThread : threadList) { testThread.join(); - Assert.assertNotEquals(-1,testThread.getRespState(),"请求响应code不应为-1"); + Assert.assertNotEquals(-1, testThread.getRespState(), "请求响应code不应为-1"); } for (int i = 1; i < threadList.size(); i++) { @@ -38,6 +48,47 @@ public void testBuild() throws Exception { } } + @Test + void testHttpClientWithInterceptor() throws Exception { + DefaultApacheHttpClientBuilder builder = DefaultApacheHttpClientBuilder.get(); + + + List interceptorOrder = new ArrayList<>(); + + HttpRequestInterceptor requestInterceptor1 = (request, context) -> { + context.setAttribute("interceptor_called", "requestInterceptor1"); + interceptorOrder.add("requestInterceptor1"); + }; + + HttpRequestInterceptor requestInterceptor2 = (request, context) -> { + interceptorOrder.add("requestInterceptor2"); + }; + + HttpResponseInterceptor responseInterceptor1 = (response, context) -> { + interceptorOrder.add("responseInterceptor1"); + }; + + HttpResponseInterceptor responseInterceptor2 = (response, context) -> { + interceptorOrder.add("responseInterceptor2"); + }; + + builder.setRequestInterceptors(Stream.of(requestInterceptor1, requestInterceptor2).collect(Collectors.toList())); + builder.setResponseInterceptors(Stream.of(responseInterceptor1, responseInterceptor2).collect(Collectors.toList())); + + try (CloseableHttpClient client = builder.build()) { + HttpUriRequest request = new HttpGet("http://localhost:8080"); + HttpContext context = HttpClientContext.create(); + try (CloseableHttpResponse resp = client.execute(request, context)) { + Assert.assertEquals(context.getAttribute("interceptor_called"), "requestInterceptor1", "成功调用 requestInterceptor1 并向 content 中写入了数据"); + + // 测试拦截器执行顺序 + Assert.assertEquals(interceptorOrder.get(0), "requestInterceptor1"); + Assert.assertEquals(interceptorOrder.get(1), "requestInterceptor2"); + Assert.assertEquals(interceptorOrder.get(2), "responseInterceptor1"); + Assert.assertEquals(interceptorOrder.get(3), "responseInterceptor2"); + } + } + } public static class TestThread extends Thread { private CloseableHttpClient client; @@ -47,7 +98,7 @@ public static class TestThread extends Thread { public void run() { client = DefaultApacheHttpClientBuilder.get().build(); HttpGet httpGet = new HttpGet("http://www.sina.com.cn/"); - try (CloseableHttpResponse resp = client.execute(httpGet)){ + try (CloseableHttpResponse resp = client.execute(httpGet)) { respState = resp.getStatusLine().getStatusCode(); } catch (IOException ignored) { } diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/apache/SSLConfigurationTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/apache/SSLConfigurationTest.java new file mode 100644 index 0000000000..cecda5ca54 --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/apache/SSLConfigurationTest.java @@ -0,0 +1,116 @@ +package me.chanjar.weixin.common.util.http.apache; + +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.impl.client.CloseableHttpClient; +import org.testng.Assert; +import org.testng.annotations.Test; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; + +/** + * 测试SSL配置,特别是TLS协议版本配置 + * Test SSL configuration, especially TLS protocol version configuration + */ +public class SSLConfigurationTest { + + @Test + public void testDefaultTLSProtocols() throws Exception { + // Create a new instance to check the default configuration + Class builderClass = DefaultApacheHttpClientBuilder.class; + Object builder = builderClass.getDeclaredMethod("get").invoke(null); + + // 验证默认支持的TLS协议版本包含现代版本 + Field supportedProtocolsField = builderClass.getDeclaredField("supportedProtocols"); + supportedProtocolsField.setAccessible(true); + String[] supportedProtocols = (String[]) supportedProtocolsField.get(builder); + + List protocolList = Arrays.asList(supportedProtocols); + + System.out.println("Default supported TLS protocols: " + Arrays.toString(supportedProtocols)); + + // 主要验证:应该支持TLS 1.2和/或1.3 (现代安全版本) + // Main validation: Should support TLS 1.2 and/or 1.3 (modern secure versions) + Assert.assertTrue(protocolList.contains("TLSv1.2"), "Should support TLS 1.2"); + Assert.assertTrue(protocolList.contains("TLSv1.3"), "Should support TLS 1.3"); + + // 验证不再是只有TLS 1.0 (这是导致原问题的根本原因) + // Verify it's no longer just TLS 1.0 (which was the root cause of the original issue) + Assert.assertTrue(protocolList.size() > 0, "Should support at least one TLS version"); + boolean hasModernTLS = protocolList.contains("TLSv1.2") || protocolList.contains("TLSv1.3"); + Assert.assertTrue(hasModernTLS, "Should support at least one modern TLS version (1.2 or 1.3)"); + + // 验证不是原来的老旧配置 (只有 "TLSv1") + // Verify it's not the old configuration (only "TLSv1") + boolean isOldConfig = protocolList.size() == 1 && protocolList.contains("TLSv1"); + Assert.assertFalse(isOldConfig, "Should not be the old configuration that only supported TLS 1.0"); + } + + @Test + public void testCustomTLSProtocols() throws Exception { + // Test that we can set custom TLS protocols + String[] customProtocols = {"TLSv1.2", "TLSv1.3"}; + + // Create a new builder instance using reflection to avoid singleton issues in testing + Class builderClass = DefaultApacheHttpClientBuilder.class; + Constructor constructor = builderClass.getDeclaredConstructor(); + constructor.setAccessible(true); + Object builder = constructor.newInstance(); + + // Set custom protocols + builderClass.getMethod("supportedProtocols", String[].class).invoke(builder, (Object) customProtocols); + + Field supportedProtocolsField = builderClass.getDeclaredField("supportedProtocols"); + supportedProtocolsField.setAccessible(true); + String[] actualProtocols = (String[]) supportedProtocolsField.get(builder); + + Assert.assertEquals(actualProtocols, customProtocols, "Custom protocols should be set correctly"); + + System.out.println("Custom supported TLS protocols: " + Arrays.toString(actualProtocols)); + } + + @Test + public void testSSLContextCreation() throws Exception { + DefaultApacheHttpClientBuilder builder = DefaultApacheHttpClientBuilder.get(); + + // 构建HTTP客户端以验证SSL工厂是否正确创建 + CloseableHttpClient client = builder.build(); + Assert.assertNotNull(client, "HTTP client should be created successfully"); + + // 验证SSL上下文支持现代TLS协议 + SSLContext sslContext = SSLContext.getDefault(); + SSLSocketFactory socketFactory = sslContext.getSocketFactory(); + + // 创建一个SSL socket来检查支持的协议 + try (SSLSocket socket = (SSLSocket) socketFactory.createSocket()) { + String[] supportedProtocols = socket.getSupportedProtocols(); + List supportedList = Arrays.asList(supportedProtocols); + + // JVM应该支持TLS 1.2(在JDK 8+中默认可用) + Assert.assertTrue(supportedList.contains("TLSv1.2"), + "JVM should support TLS 1.2. Supported protocols: " + Arrays.toString(supportedProtocols)); + + System.out.println("JVM supported TLS protocols: " + Arrays.toString(supportedProtocols)); + } + + client.close(); + } + + @Test + public void testBuilderChaining() { + DefaultApacheHttpClientBuilder builder = DefaultApacheHttpClientBuilder.get(); + + // 测试方法链调用 + ApacheHttpClientBuilder result = builder + .supportedProtocols(new String[]{"TLSv1.2", "TLSv1.3"}) + .httpProxyHost("proxy.example.com") + .httpProxyPort(8080); + + Assert.assertSame(result, builder, "Builder methods should return the same instance for method chaining"); + } +} \ No newline at end of file diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/apache/SSLIntegrationTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/apache/SSLIntegrationTest.java new file mode 100644 index 0000000000..e732360e87 --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/apache/SSLIntegrationTest.java @@ -0,0 +1,73 @@ +package me.chanjar.weixin.common.util.http.apache; + +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.CloseableHttpClient; +import org.testng.Assert; +import org.testng.annotations.Test; + +/** + * 集成测试 - 验证SSL配置可以正常访问HTTPS网站 + * Integration test - Verify SSL configuration can access HTTPS websites properly + */ +public class SSLIntegrationTest { + + @Test + public void testHTTPSConnectionWithModernTLS() throws Exception { + DefaultApacheHttpClientBuilder builder = DefaultApacheHttpClientBuilder.get(); + + // 使用默认配置(支持现代TLS版本)创建客户端 + CloseableHttpClient client = builder.build(); + + // 测试访问一个需要现代TLS的网站 + // Test accessing a website that requires modern TLS + HttpGet httpGet = new HttpGet("https://api.weixin.qq.com/"); + + try (CloseableHttpResponse response = client.execute(httpGet)) { + // 验证能够成功建立HTTPS连接(不管响应内容是什么) + // Verify that HTTPS connection can be established successfully (regardless of response content) + Assert.assertNotNull(response, "Should be able to establish HTTPS connection"); + Assert.assertNotNull(response.getStatusLine(), "Should receive a status response"); + + int statusCode = response.getStatusLine().getStatusCode(); + // 任何HTTP状态码都表示SSL握手成功 + // Any HTTP status code indicates successful SSL handshake + Assert.assertTrue(statusCode > 0, "Should receive a valid HTTP status code, got: " + statusCode); + + System.out.println("HTTPS connection test successful. Status: " + response.getStatusLine()); + } catch (javax.net.ssl.SSLHandshakeException e) { + Assert.fail("SSL handshake should not fail with modern TLS configuration. Error: " + e.getMessage()); + } finally { + client.close(); + } + } + + @Test + public void testCustomTLSConfiguration() throws Exception { + DefaultApacheHttpClientBuilder builder = DefaultApacheHttpClientBuilder.get(); + + // 配置为只支持TLS 1.2和1.3(最安全的配置) + // Configure to only support TLS 1.2 and 1.3 (most secure configuration) + builder.supportedProtocols(new String[]{"TLSv1.2", "TLSv1.3"}); + + CloseableHttpClient client = builder.build(); + + // 测试这个配置是否能正常工作 + HttpGet httpGet = new HttpGet("https://httpbin.org/get"); + + try (CloseableHttpResponse response = client.execute(httpGet)) { + Assert.assertNotNull(response, "Should be able to establish HTTPS connection with TLS 1.2/1.3"); + int statusCode = response.getStatusLine().getStatusCode(); + Assert.assertEquals(statusCode, 200, "Should get HTTP 200 response from httpbin.org"); + + System.out.println("Custom TLS configuration test successful. Status: " + response.getStatusLine()); + } catch (javax.net.ssl.SSLHandshakeException e) { + // 这个测试可能会因为网络环境而失败,所以我们只是记录警告 + // This test might fail due to network environment, so we just log a warning + System.out.println("Warning: SSL handshake failed with custom TLS config: " + e.getMessage()); + System.out.println("This might be due to network restrictions in the test environment."); + } finally { + client.close(); + } + } +} \ No newline at end of file diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/okhttp/DefaultOkHttpClientBuilderTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/okhttp/DefaultOkHttpClientBuilderTest.java new file mode 100644 index 0000000000..d742845b6c --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/http/okhttp/DefaultOkHttpClientBuilderTest.java @@ -0,0 +1,66 @@ +package me.chanjar.weixin.common.util.http.okhttp; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class DefaultOkHttpClientBuilderTest { + @Test + public void testBuild() throws Exception { + DefaultOkHttpClientBuilder builder1 = DefaultOkHttpClientBuilder.get(); + DefaultOkHttpClientBuilder builder2 = DefaultOkHttpClientBuilder.get(); + Assert.assertSame(builder1, builder2, "DefaultOkHttpClientBuilder为单例,获取到的对象应该相同"); + List threadList = new ArrayList<>(10); + for (int i = 0; i < 10; i++) { + DefaultOkHttpClientBuilderTest.TestThread thread = new DefaultOkHttpClientBuilderTest.TestThread(); + thread.start(); + threadList.add(thread); + } + for (DefaultOkHttpClientBuilderTest.TestThread testThread : threadList) { + testThread.join(); + Assert.assertNotEquals(-1, testThread.getRespState(), "请求响应code不应为-1"); + } + + for (int i = 1; i < threadList.size(); i++) { + DefaultOkHttpClientBuilderTest.TestThread thread1 = threadList.get(i - 1); + DefaultOkHttpClientBuilderTest.TestThread thread2 = threadList.get(i); + Assert.assertSame( + thread1.getClient(), + thread2.getClient(), + "DefaultOkHttpClientBuilderTest为单例,并持有了相同的OkHttpClient" + ); + } + } + + public static class TestThread extends Thread { + private OkHttpClient client; + private int respState = -1; + + @Override + public void run() { + client = DefaultOkHttpClientBuilder.get().build(); + Request request = new Request.Builder() + .url("http://www.sina.com.cn/") + .build(); + try (Response response = client.newCall(request).execute()) { + respState = response.code(); + } catch (IOException e) { + // ignore + } + } + + public OkHttpClient getClient() { + return client; + } + + public int getRespState() { + return respState; + } + } +} diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/json/GsonHelperTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/json/GsonHelperTest.java new file mode 100644 index 0000000000..bafe3c30d1 --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/json/GsonHelperTest.java @@ -0,0 +1,139 @@ +package me.chanjar.weixin.common.util.json; + +import com.google.gson.JsonObject; +import org.testng.annotations.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * GsonHelper 的单元测试. + * + * @author Binary Wang + * created on 2020-09-04 + */ +public class GsonHelperTest { + + @Test + public void testIsNull() { + } + + @Test + public void testIsNotNull() { + } + + @Test + public void testGetLong() { + } + + @Test + public void testGetPrimitiveLong() { + } + + @Test + public void testGetInteger() { + } + + @Test + public void testGetPrimitiveInteger() { + } + + @Test + public void testGetDouble() { + } + + @Test + public void testGetPrimitiveDouble() { + } + + @Test + public void testGetFloat() { + } + + @Test + public void testGetPrimitiveFloat() { + } + + @Test + public void testGetBoolean() { + } + + @Test + public void testGetString() { + } + + @Test + public void testGetAsString() { + } + + @Test + public void testGetAsLong() { + } + + @Test + public void testGetAsPrimitiveLong() { + } + + @Test + public void testGetAsInteger() { + } + + @Test + public void testGetAsPrimitiveInt() { + } + + @Test + public void testGetAsBoolean() { + } + + @Test + public void testGetAsPrimitiveBool() { + } + + @Test + public void testGetAsDouble() { + } + + @Test + public void testGetAsPrimitiveDouble() { + } + + @Test + public void testGetAsFloat() { + } + + @Test + public void testGetAsPrimitiveFloat() { + } + + @Test + public void testGetIntArray() { + } + + @Test + public void testGetStringArray() { + } + + @Test + public void testGetLongArray() { + } + + @Test + public void testGetAsJsonArray() { + } + + @Test + public void testBuildSimpleJsonObject() { + try { + GsonHelper.buildJsonObject(1, 2, 3); + } catch (RuntimeException e) { + assertThat(e.getMessage()).isEqualTo("参数个数必须为偶数"); + } + + System.out.println(GsonHelper.buildJsonObject(1, 2)); + System.out.println(GsonHelper.buildJsonObject(1, null)); + System.out.println(GsonHelper.buildJsonObject("int", 1, "float", 2.1f, "double", 2.5)); + System.out.println(GsonHelper.buildJsonObject("boolean", true, "string", "1av")); + System.out.println(GsonHelper.buildJsonObject(1, true, "jsonElement", new JsonObject())); + System.out.println(GsonHelper.buildJsonObject("num", 2, "string", "cde", "char", 'a', "bool", true)); + } +} diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/json/GsonParserTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/json/GsonParserTest.java new file mode 100644 index 0000000000..ea069d4155 --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/json/GsonParserTest.java @@ -0,0 +1,47 @@ +package me.chanjar.weixin.common.util.json; + +import com.google.gson.JsonObject; +import com.google.gson.stream.JsonReader; +import org.testng.annotations.Test; + +import java.io.StringReader; + +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; + +/** + * GsonParser 测试类 + * + * @author Binary Wang + */ +public class GsonParserTest { + + @Test + public void testParseString() { + String json = "{\"code\":\"ALREADY_EXISTS\",\"message\":\"当前订单已关闭,可查询订单了解关闭原因\"}"; + JsonObject jsonObject = GsonParser.parse(json); + assertNotNull(jsonObject); + assertEquals(jsonObject.get("code").getAsString(), "ALREADY_EXISTS"); + assertEquals(jsonObject.get("message").getAsString(), "当前订单已关闭,可查询订单了解关闭原因"); + } + + @Test + public void testParseReader() { + String json = "{\"code\":\"SUCCESS\",\"message\":\"处理成功\"}"; + StringReader reader = new StringReader(json); + JsonObject jsonObject = GsonParser.parse(reader); + assertNotNull(jsonObject); + assertEquals(jsonObject.get("code").getAsString(), "SUCCESS"); + assertEquals(jsonObject.get("message").getAsString(), "处理成功"); + } + + @Test + public void testParseJsonReader() { + String json = "{\"errcode\":0,\"errmsg\":\"ok\"}"; + JsonReader jsonReader = new JsonReader(new StringReader(json)); + JsonObject jsonObject = GsonParser.parse(jsonReader); + assertNotNull(jsonObject); + assertEquals(jsonObject.get("errcode").getAsInt(), 0); + assertEquals(jsonObject.get("errmsg").getAsString(), "ok"); + } +} diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockSerializationTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockSerializationTest.java new file mode 100644 index 0000000000..ea4a131d37 --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockSerializationTest.java @@ -0,0 +1,100 @@ +package me.chanjar.weixin.common.util.locks; + +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import static org.testng.Assert.*; + +/** + * 测试 RedisTemplateSimpleDistributedLock 在自定义 Key 序列化时的兼容性 + * + * 这个测试验证修复后的实现确保 tryLock 和 unlock 使用一致的键序列化方式 + */ +@Test(enabled = false) // 默认禁用,需要Redis实例才能运行 +public class RedisTemplateSimpleDistributedLockSerializationTest { + + private RedisTemplateSimpleDistributedLock redisLock; + private StringRedisTemplate redisTemplate; + + @BeforeTest + public void init() { + JedisConnectionFactory connectionFactory = new JedisConnectionFactory(); + connectionFactory.setHostName("127.0.0.1"); + connectionFactory.setPort(6379); + connectionFactory.afterPropertiesSet(); + + // 创建一个带自定义键序列化的 StringRedisTemplate + StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory); + + // 使用自定义键序列化器,模拟在键前面添加前缀的场景 + redisTemplate.setKeySerializer(new StringRedisSerializer() { + @Override + public byte[] serialize(String string) { + if (string == null) return null; + // 添加 "System:" 前缀,模拟用户自定义的键序列化 + return super.serialize("System:" + string); + } + + @Override + public String deserialize(byte[] bytes) { + if (bytes == null) return null; + String result = super.deserialize(bytes); + // 移除前缀进行反序列化 + return result != null && result.startsWith("System:") ? result.substring(7) : result; + } + }); + + this.redisTemplate = redisTemplate; + this.redisLock = new RedisTemplateSimpleDistributedLock(redisTemplate, "test_lock_key", 60000); + } + + @Test(description = "测试自定义键序列化器下的锁操作一致性") + public void testLockConsistencyWithCustomKeySerializer() { + // 1. 获取锁应该成功 + assertTrue(redisLock.tryLock(), "第一次获取锁应该成功"); + assertNotNull(redisLock.getLockSecretValue(), "锁值应该存在"); + + // 2. 验证键已正确存储(通过 redisTemplate 直接查询) + String actualValue = redisTemplate.opsForValue().get("test_lock_key"); + assertEquals(actualValue, redisLock.getLockSecretValue(), "通过 redisTemplate 查询的值应该与锁值相同"); + + // 3. 再次尝试获取同一把锁应该成功(可重入) + assertTrue(redisLock.tryLock(), "可重入锁应该再次获取成功"); + + // 4. 释放锁应该成功 + redisLock.unlock(); + assertNull(redisLock.getLockSecretValue(), "释放锁后锁值应该为空"); + + // 5. 验证键已被删除 + actualValue = redisTemplate.opsForValue().get("test_lock_key"); + assertNull(actualValue, "释放锁后 Redis 中的键应该被删除"); + + // 6. 释放已释放的锁应该是安全的 + redisLock.unlock(); // 不应该抛出异常 + } + + @Test(description = "测试不同线程使用相同键的锁排他性") + public void testLockExclusivityWithCustomKeySerializer() throws InterruptedException { + // 第一个锁实例获取锁 + assertTrue(redisLock.tryLock(), "第一个锁实例应该成功获取锁"); + + // 创建第二个锁实例使用相同的键 + RedisTemplateSimpleDistributedLock anotherLock = new RedisTemplateSimpleDistributedLock( + redisTemplate, "test_lock_key", 60000); + + // 第二个锁实例不应该能获取锁 + assertFalse(anotherLock.tryLock(), "第二个锁实例不应该能获取已被占用的锁"); + + // 释放第一个锁 + redisLock.unlock(); + + // 现在第二个锁实例应该能获取锁 + assertTrue(anotherLock.tryLock(), "第一个锁释放后,第二个锁实例应该能获取锁"); + + // 清理 + anotherLock.unlock(); + } +} \ No newline at end of file diff --git a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockTest.java new file mode 100644 index 0000000000..b278eeafa0 --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockTest.java @@ -0,0 +1,105 @@ +package me.chanjar.weixin.common.util.locks; + +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.testng.Assert.*; + +@Slf4j +@Test(enabled = true) +public class RedisTemplateSimpleDistributedLockTest { + + private static final String KEY_PREFIX = "System:"; + RedisTemplateSimpleDistributedLock redisLock; + + StringRedisTemplate redisTemplate; + + AtomicInteger lockCurrentExecuteCounter; + + @BeforeTest + public void init() { + JedisConnectionFactory connectionFactory = new JedisConnectionFactory(); + connectionFactory.setHostName("127.0.0.1"); + connectionFactory.setPort(6379); + connectionFactory.afterPropertiesSet(); + StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory); + // 自定义序列化器,为 key 自动加前缀 + redisTemplate.setKeySerializer(new StringRedisSerializer() { + @NotNull + @Override + public byte[] serialize(String string) { + if (string == null) { + return super.serialize(null); + } + // 添加前缀 + return super.serialize(KEY_PREFIX + string); + } + + @NotNull + @Override + public String deserialize(byte[] bytes) { + String key = super.deserialize(bytes); + if (key.startsWith(KEY_PREFIX)) { + return key.substring(KEY_PREFIX.length()); + } + return key; + } + }); + this.redisTemplate = redisTemplate; + this.redisLock = new RedisTemplateSimpleDistributedLock(redisTemplate, 60000); + this.lockCurrentExecuteCounter = new AtomicInteger(0); + } + + @Test(description = "多线程测试锁排他性") + public void testLockExclusive() throws InterruptedException { + int threadSize = 100; + final CountDownLatch startLatch = new CountDownLatch(threadSize); + final CountDownLatch endLatch = new CountDownLatch(threadSize); + + for (int i = 0; i < threadSize; i++) { + new Thread(() -> { + try { + startLatch.await(); + } catch (InterruptedException e) { + log.error("unexpected exception", e); + } + + redisLock.lock(); + assertEquals(lockCurrentExecuteCounter.incrementAndGet(), 1, "临界区同时只能有一个线程执行"); + lockCurrentExecuteCounter.decrementAndGet(); + redisLock.unlock(); + + endLatch.countDown(); + }).start(); + startLatch.countDown(); + } + endLatch.await(); + } + + @Test + public void testTryLock() throws InterruptedException { + assertTrue(redisLock.tryLock(3, TimeUnit.SECONDS), "第一次加锁应该成功"); + assertNotNull(redisLock.getLockSecretValue()); + String redisValue = this.redisTemplate.opsForValue().get(redisLock.getKey()); + assertEquals(redisValue, redisLock.getLockSecretValue()); + + redisLock.unlock(); + assertNull(redisLock.getLockSecretValue()); + redisValue = this.redisTemplate.opsForValue().get(redisLock.getKey()); + assertNull(redisValue, "释放锁后key会被删除"); + + redisLock.unlock(); + } + + +} + diff --git a/weixin-java-cp/APPROVAL_WORKFLOW_GUIDE.md b/weixin-java-cp/APPROVAL_WORKFLOW_GUIDE.md new file mode 100644 index 0000000000..d2c533b5e5 --- /dev/null +++ b/weixin-java-cp/APPROVAL_WORKFLOW_GUIDE.md @@ -0,0 +1,141 @@ +# WeChat Enterprise Workflow Approval Guide +# 企业微信流程审批功能使用指南 + +## Overview / 概述 + +WxJava SDK provides comprehensive support for WeChat Enterprise workflow approval (企业微信流程审批), including both traditional OA approval and the approval process engine. + +WxJava SDK 提供全面的企业微信流程审批支持,包括传统OA审批和审批流程引擎。 + +## Current Implementation Status / 当前实现状态 + +### ✅ Fully Implemented APIs / 已完整实现的API + +1. **Submit Approval Application / 提交审批申请** + - Endpoint: `/cgi-bin/oa/applyevent` + - Documentation: [91853](https://work.weixin.qq.com/api/doc/90000/90135/91853) + - Implementation: `WxCpOaService.apply(WxCpOaApplyEventRequest)` + +2. **Get Approval Details / 获取审批申请详情** + - Endpoint: `/cgi-bin/oa/getapprovaldetail` + - Implementation: `WxCpOaService.getApprovalDetail(String spNo)` + +3. **Batch Get Approval Numbers / 批量获取审批单号** + - Endpoint: `/cgi-bin/oa/getapprovalinfo` + - Implementation: `WxCpOaService.getApprovalInfo(...)` + +4. **Approval Process Engine / 审批流程引擎** + - Endpoint: `/cgi-bin/corp/getopenapprovaldata` + - Implementation: `WxCpOaAgentService.getOpenApprovalData(String thirdNo)` + +5. **Template Management / 模板管理** + - Create: `WxCpOaService.createOaApprovalTemplate(...)` + - Update: `WxCpOaService.updateOaApprovalTemplate(...)` + - Get Details: `WxCpOaService.getTemplateDetail(...)` + +## Usage Examples / 使用示例 + +### 1. Submit Approval Application / 提交审批申请 + +```java +// Create approval request +WxCpOaApplyEventRequest request = new WxCpOaApplyEventRequest() + .setCreatorUserId("userId") + .setTemplateId("templateId") + .setUseTemplateApprover(0) + .setApprovers(Arrays.asList( + new WxCpOaApplyEventRequest.Approver() + .setAttr(2) + .setUserIds(new String[]{"approver1", "approver2"}) + )) + .setNotifiers(new String[]{"notifier1", "notifier2"}) + .setNotifyType(1) + .setApplyData(new WxCpOaApplyEventRequest.ApplyData() + .setContents(Arrays.asList( + new ApplyDataContent() + .setControl("Text") + .setId("Text-1234567890") + .setValue(new ContentValue().setText("Approval content")) + )) + ); + +// Submit approval +String spNo = wxCpService.getOaService().apply(request); +``` + +### 2. Get Approval Details / 获取审批详情 + +```java +// Get approval details by approval number +WxCpApprovalDetailResult result = wxCpService.getOaService() + .getApprovalDetail("approval_number"); + +WxCpApprovalDetailResult.WxCpApprovalDetail detail = result.getInfo(); +System.out.println("Approval Status: " + detail.getSpStatus()); +System.out.println("Approval Name: " + detail.getSpName()); +``` + +### 3. Batch Get Approval Information / 批量获取审批信息 + +```java +// Get approval info with filters +Date startTime = new Date(System.currentTimeMillis() - 7 * 24 * 60 * 60 * 1000); +Date endTime = new Date(); + +WxCpApprovalInfo approvalInfo = wxCpService.getOaService() + .getApprovalInfo(startTime, endTime, "0", 100, null); + +List spNumbers = approvalInfo.getSpNoList(); +``` + +### 4. Third-Party Application Support / 第三方应用支持 + +```java +// For third-party applications +WxCpTpOAService tpOaService = wxCpTpService.getOaService(); + +// Submit approval for specific corp +String spNo = tpOaService.apply(request, "corpId"); + +// Get approval details for specific corp +WxCpApprovalDetailResult detail = tpOaService.getApprovalDetail("spNo", "corpId"); +``` + +## Multi-Account Configuration / 多账号配置支持 + +WxJava supports multi-account configurations for enterprise scenarios: + +```java +// Spring Boot configuration example +@Autowired +private WxCpMultiServices wxCpMultiServices; + +// Get service for specific corp +WxCpService wxCpService = wxCpMultiServices.getWxCpService("corpId"); +WxCpOaService oaService = wxCpService.getOaService(); +``` + +## Available Data Models / 可用数据模型 + +- `WxCpOaApplyEventRequest` - Approval application request +- `WxCpApprovalDetailResult` - Approval details response +- `WxCpApprovalInfo` - Batch approval information +- `WxCpXmlApprovalInfo` - XML approval message handling +- `WxCpOaApprovalTemplate` - Approval template management + +## Documentation References / 文档参考 + +- [Submit Approval Application (91853)](https://work.weixin.qq.com/api/doc/90000/90135/91853) +- [Get Approval Details (91983)](https://work.weixin.qq.com/api/doc/90000/90135/91983) +- [Batch Get Approval Numbers (91816)](https://work.weixin.qq.com/api/doc/90000/90135/91816) +- [Approval Process Engine (90269)](https://developer.work.weixin.qq.com/document/path/90269) + +## Conclusion / 结论 + +WxJava already provides comprehensive support for WeChat Enterprise workflow approval. The "new version" (新版) approval functionality referenced in issue requests is **already fully implemented** and available for use. + +WxJava 已经提供了企业微信流程审批的全面支持。问题中提到的"新版"流程审批功能**已经完全实现**并可使用。 + +For questions about specific usage, please refer to the test cases in `WxCpOaServiceImplTest` and the comprehensive API documentation. + +有关具体使用问题,请参考 `WxCpOaServiceImplTest` 中的测试用例和全面的API文档。 \ No newline at end of file diff --git a/weixin-java-cp/INTELLIGENT_ROBOT.md b/weixin-java-cp/INTELLIGENT_ROBOT.md new file mode 100644 index 0000000000..18dd0c677f --- /dev/null +++ b/weixin-java-cp/INTELLIGENT_ROBOT.md @@ -0,0 +1,159 @@ +# 企业微信智能机器人接口 + +本模块提供企业微信智能机器人相关的API接口实现。 + +## 官方文档 + +- [企业微信智能机器人接口](https://developer.work.weixin.qq.com/document/path/101039) + +## 接口说明 + +### 获取服务实例 + +```java +WxCpService wxCpService = ...; // 初始化企业微信服务 +WxCpIntelligentRobotService robotService = wxCpService.getIntelligentRobotService(); +``` + +### 创建智能机器人 + +```java +WxCpIntelligentRobotCreateRequest request = new WxCpIntelligentRobotCreateRequest(); +request.setName("我的智能机器人"); +request.setDescription("这是一个智能客服机器人"); +request.setAvatar("http://example.com/avatar.jpg"); + +WxCpIntelligentRobotCreateResponse response = robotService.createRobot(request); +String robotId = response.getRobotId(); +``` + +### 更新智能机器人 + +```java +WxCpIntelligentRobotUpdateRequest request = new WxCpIntelligentRobotUpdateRequest(); +request.setRobotId("robot_id_here"); +request.setName("更新后的机器人名称"); +request.setDescription("更新后的描述"); +request.setStatus(1); // 1:启用, 0:停用 + +robotService.updateRobot(request); +``` + +### 查询智能机器人 + +```java +String robotId = "robot_id_here"; +WxCpIntelligentRobot robot = robotService.getRobot(robotId); + +System.out.println("机器人名称: " + robot.getName()); +System.out.println("机器人状态: " + robot.getStatus()); +``` + +### 智能对话 + +```java +WxCpIntelligentRobotChatRequest request = new WxCpIntelligentRobotChatRequest(); +request.setRobotId("robot_id_here"); +request.setUserid("user123"); +request.setMessage("你好,请问如何使用这个功能?"); +request.setSessionId("session123"); // 可选,用于保持会话连续性 + +WxCpIntelligentRobotChatResponse response = robotService.chat(request); +String reply = response.getReply(); +String sessionId = response.getSessionId(); +``` + +### 重置会话 + +```java +String robotId = "robot_id_here"; +String userid = "user123"; +String sessionId = "session123"; + +robotService.resetSession(robotId, userid, sessionId); +``` + +### 主动发送消息 + +智能机器人可以主动向用户发送消息,用于推送通知或提醒。 + +```java +WxCpIntelligentRobotSendMessageRequest request = new WxCpIntelligentRobotSendMessageRequest(); +request.setRobotId("robot_id_here"); +request.setUserid("user123"); +request.setMessage("您好,这是来自智能机器人的主动消息"); +request.setSessionId("session123"); // 可选,用于保持会话连续性 + +WxCpIntelligentRobotSendMessageResponse response = robotService.sendMessage(request); +String msgId = response.getMsgId(); +String sessionId = response.getSessionId(); +``` + +### 接收用户消息 + +当用户向智能机器人发送消息时,企业微信会通过回调接口推送消息。可以使用 `WxCpXmlMessage` 接收和解析这些消息: + +```java +// 在接收回调消息的接口中 +WxCpXmlMessage message = WxCpXmlMessage.fromEncryptedXml( + requestBody, wxCpConfigStorage, timestamp, nonce, msgSignature +); + +// 获取智能机器人相关字段 +String robotId = message.getRobotId(); // 机器人ID +String sessionId = message.getSessionId(); // 会话ID +String content = message.getContent(); // 消息内容 +String fromUser = message.getFromUserName(); // 发送用户 + +// 处理消息并回复 +// ... +``` + +对于智能机器人 API 模式的 JSON 回调消息,可使用 `WxCpIntelligentRobotMessage` 解析: + +```java +WxCpIntelligentRobotMessage callbackMessage = + robotService.parseCallbackMessage(jsonBody); +String botId = callbackMessage.getAiBotId(); +String userId = callbackMessage.getFrom().getUserid(); +String msgType = callbackMessage.getMsgType(); +``` + +### 删除智能机器人 + +```java +String robotId = "robot_id_here"; +robotService.deleteRobot(robotId); +``` + +## 主要类说明 + +### 请求类 + +- `WxCpIntelligentRobotCreateRequest`: 创建机器人请求 +- `WxCpIntelligentRobotUpdateRequest`: 更新机器人请求 +- `WxCpIntelligentRobotChatRequest`: 智能对话请求 +- `WxCpIntelligentRobotSendMessageRequest`: 主动发送消息请求 + +### 响应类 + +- `WxCpIntelligentRobotCreateResponse`: 创建机器人响应 +- `WxCpIntelligentRobotChatResponse`: 智能对话响应 +- `WxCpIntelligentRobotSendMessageResponse`: 主动发送消息响应 +- `WxCpIntelligentRobot`: 机器人信息实体 + +### 消息接收 + +- `WxCpXmlMessage`: 支持接收智能机器人回调消息,包含 `robotId` 和 `sessionId` 字段 + +### 服务接口 + +- `WxCpIntelligentRobotService`: 智能机器人服务接口 +- `WxCpIntelligentRobotServiceImpl`: 智能机器人服务实现 + +## 注意事项 + +1. 需要确保企业微信应用具有智能机器人相关权限 +2. 智能机器人功能可能需要特定的企业微信版本支持 +3. 会话ID可以用于保持对话的连续性,提升用户体验 +4. 机器人状态: 0表示停用,1表示启用 diff --git a/weixin-java-cp/pom.xml b/weixin-java-cp/pom.xml index 2dc09480d4..d9d8694352 100644 --- a/weixin-java-cp/pom.xml +++ b/weixin-java-cp/pom.xml @@ -7,7 +7,7 @@ com.github.binarywang wx-java - 3.6.0 + 4.8.3.B weixin-java-cp @@ -30,6 +30,16 @@ okhttp provided + + org.apache.httpcomponents + httpclient + provided + + + org.apache.httpcomponents.client5 + httpclient5 + provided + redis.clients jedis @@ -38,6 +48,15 @@ org.slf4j slf4j-api + + + org.redisson + redisson + + + org.springframework.data + spring-data-redis + org.testng @@ -46,9 +65,10 @@ org.mockito - mockito-all + mockito-core test + com.google.inject guice @@ -73,12 +93,27 @@ org.projectlombok lombok + + org.bouncycastle + bcprov-jdk18on + org.assertj assertj-guava test + + com.github.dreamhead + moco-runner + test + + + + com.fasterxml.jackson.core + jackson-core + test + @@ -90,9 +125,50 @@ src/test/resources/testng.xml + + --add-opens java.base/java.lang=ALL-UNNAMED + --add-opens java.base/java.lang.reflect=ALL-UNNAMED + --add-opens java.base/java.io=ALL-UNNAMED + --add-opens java.base/java.security=ALL-UNNAMED + --add-opens java.base/java.util=ALL-UNNAMED + --add-opens java.management/javax.management=ALL-UNNAMED + --add-opens java.naming/javax.naming=ALL-UNNAMED + + + + native-image + + false + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + + com.github.binarywang.wx.graal.GraalProcessor,lombok.launch.AnnotationProcessorHider$AnnotationProcessor,lombok.launch.AnnotationProcessorHider$ClaimingProcessor + + + + com.github.binarywang + weixin-graal + ${project.version} + + + + + + + + + + diff --git a/weixin-java-cp/src/main/java/com/tencent/wework/Finance.java b/weixin-java-cp/src/main/java/com/tencent/wework/Finance.java new file mode 100644 index 0000000000..4d6406bd18 --- /dev/null +++ b/weixin-java-cp/src/main/java/com/tencent/wework/Finance.java @@ -0,0 +1,252 @@ +package com.tencent.wework; + +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + +/** + * 企业微信会话内容存档Finance类 + * 注意: + * 此类必须配置在com.tencent.wework路径底下,否则会报错:java.lang.UnsatisfiedLinkError: com.xxx.Finance.NewSdk() + *

+ * Q:JAVA版本的sdk报错UnsatisfiedLinkError? + * A:请检查是否修改了sdk的包名。 + *

+ * 官方文档 + * + * @author Wang_Wong created on 2022-01-17 + */ +@Slf4j +public class Finance { + + private static volatile long sdk = -1L; + private static Finance finance = null; + private static final String SO_FILE = "so"; + private static final String DLL_FILE = "dll"; + + /** + * New sdk long. + * + * @return the long + */ + public static native long NewSdk(); + + /** + * 初始化函数 + * Return值=0表示该API调用成功 + * + * @param sdk the sdk + * @param corpid the corpid + * @param secret the secret + * @return 返回是否初始化成功 0 - 成功 !=0 - 失败 + */ + public static native int Init(long sdk, String corpid, String secret); + + /** + * 拉取聊天记录函数 + * Return值=0表示该API调用成功 + * + * @param sdk the sdk + * @param seq the seq + * @param limit the limit + * @param proxy the proxy + * @param passwd the passwd + * @param timeout the timeout + * @param chatData the chat data + * @return 返回是否调用成功 0 - 成功 !=0 - 失败 + */ + public static native int GetChatData(long sdk, long seq, long limit, String proxy, String passwd, long timeout, long chatData); + + /** + * 拉取媒体消息函数 + * Return值=0表示该API调用成功 + * + * @param sdk the sdk + * @param indexbuf the indexbuf + * @param sdkField the sdk field + * @param proxy the proxy + * @param passwd the passwd + * @param timeout the timeout + * @param mediaData the media data + * @return 返回是否调用成功 0 - 成功 !=0 - 失败 + */ + public static native int GetMediaData(long sdk, String indexbuf, String sdkField, String proxy, String passwd, long timeout, long mediaData); + + /** + * 解析密文 + * + * @param sdk the sdk + * @param encrypt_key the encrypt key + * @param encrypt_msg the encrypt msg + * @param msg the msg + * @return 返回是否调用成功 0 - 成功 !=0 - 失败 + */ + public static native int DecryptData(long sdk, String encrypt_key, String encrypt_msg, long msg); + + /** + * Destroy sdk. + * + * @param sdk the sdk + */ + public static native void DestroySdk(long sdk); + + /** + * New slice long. + * + * @return the long + */ + public static native long NewSlice(); + + /** + * 释放slice ,和NewSlice成对使用 + * + * @param slice the slice + */ + public static native void FreeSlice(long slice); + + /** + * 获取slice内容 + * + * @param slice the slice + * @return 内容 string + */ + public static native String GetContentFromSlice(long slice); + + /** + * 获取slice内容长度 + * + * @param slice the slice + * @return 内容 int + */ + public static native int GetSliceLen(long slice); + + /** + * New media data long. + * + * @return the long + */ + public static native long NewMediaData(); + + /** + * Free media data. + * + * @param mediaData the media data + */ + public static native void FreeMediaData(long mediaData); + + /** + * 获取 mediadata outindex + * + * @param mediaData the media data + * @return outindex string + */ + public static native String GetOutIndexBuf(long mediaData); + + /** + * 获取 mediadata data数据 + * + * @param mediaData the media data + * @return data byte [ ] + */ + public static native byte[] GetData(long mediaData); + + /** + * Get index len int. + * + * @param mediaData the media data + * @return the int + */ + public static native int GetIndexLen(long mediaData); + + /** + * Get data len int. + * + * @param mediaData the media data + * @return the int + */ + public static native int GetDataLen(long mediaData); + + /** + * Is media data finish int. + * + * @param mediaData the media data + * @return 1完成 、0未完成 + * 判断mediadata是否结束 + */ + public static native int IsMediaDataFinish(long mediaData); + + /** + * 判断Windows环境 + * + * @return boolean boolean + */ + public static boolean isWindows() { + String osName = System.getProperties().getProperty("os.name"); + log.info("Loading System Libraries, Current OS Version Is: {}", osName); + return osName.toUpperCase().contains("WINDOWS"); + } + + /** + * 加载系统类库 + * + * @param libFiles 类库配置文件 + * @param prefixPath 类库文件的前缀路径 + */ + public Finance(List libFiles, String prefixPath) { + boolean isWindows = Finance.isWindows(); + for (String file : libFiles) { + String suffix = file.substring(file.lastIndexOf(".") + 1); + if (isWindows) { + // 加载dll文件 + if (suffix.equalsIgnoreCase(DLL_FILE)) { + System.load(prefixPath + file); + } + } else { + // 加载so文件 + if (suffix.equalsIgnoreCase(SO_FILE)) { + System.load(prefixPath + file); + } + } + } + + } + + /** + * 初始化类库文件 + * + * @param libFiles the lib files + * @param prefixPath the prefix path + * @return finance finance + */ + public static synchronized Finance loadingLibraries(List libFiles, String prefixPath) { + if (finance != null) { + return finance; + } + finance = new Finance(libFiles, prefixPath); + return finance; + } + + /** + * 单例sdk + * + * @return long + */ + public static synchronized long SingletonSDK() { + if (sdk > 0) { + return sdk; + } + sdk = Finance.NewSdk(); + return sdk; + } + + /** + * 销毁sdk,保证线程可见性 + * + * @param destroySDK the destroy sdk + */ + public static synchronized void DestroySingletonSDK(long destroySDK) { + sdk = 0L; + Finance.DestroySdk(destroySDK); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpAgentService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpAgentService.java index d57ca56c21..05f06f1da9 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpAgentService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpAgentService.java @@ -2,13 +2,14 @@ import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.cp.bean.WxCpAgent; +import me.chanjar.weixin.cp.bean.WxCpTpAdmin; import java.util.List; /** *

  *  管理企业号应用
- *  文档地址:https://work.weixin.qq.com/api/doc#10087
+ *  文档地址:...
  *  Created by huansinho on 2018/4/13.
  * 
* @@ -18,12 +19,13 @@ public interface WxCpAgentService { /** *
    * 获取企业号应用信息
-   * 该API用于获取企业号某个应用的基本信息,包括头像、昵称、帐号类型、认证类型、可见范围等信息
-   * 详情请见: https://work.weixin.qq.com/api/doc#10087
+   * 该API用于获取企业号某个应用的基本信息,包括头像、昵称、账号类型、认证类型、可见范围等信息
+   * 详情请见: ...
    * 
* * @param agentId 企业应用的id - * @return 部门id + * @return wx cp agent + * @throws WxErrorException the wx error exception */ WxCpAgent get(Integer agentId) throws WxErrorException; @@ -31,10 +33,11 @@ public interface WxCpAgentService { *
    * 设置应用.
    * 仅企业可调用,可设置当前凭证对应的应用;第三方不可调用。
-   * 详情请见: https://work.weixin.qq.com/api/doc#10088
+   * 详情请见: ...
    * 
* * @param agentInfo 应用信息 + * @throws WxErrorException the wx error exception */ void set(WxCpAgent agentInfo) throws WxErrorException; @@ -42,10 +45,26 @@ public interface WxCpAgentService { *
    * 获取应用列表.
    * 企业仅可获取当前凭证对应的应用;第三方仅可获取被授权的应用。
-   * 详情请见: https://work.weixin.qq.com/api/doc#11214
+   * 详情请见: ...
    * 
* + * @return the list + * @throws WxErrorException the wx error exception */ List list() throws WxErrorException; + /** + *
+   * 获取应用管理员列表
+   * 第三方服务商可以用此接口获取授权企业中某个第三方应用或者代开发应用的管理员列表(不包括外部管理员),
+   * 以便服务商在用户进入应用主页之后根据是否管理员身份做权限的区分。
+   * 详情请见: 文档
+   * 
+ * + * @param agentId 应用id + * @return admin list + * @throws WxErrorException the wx error exception + */ + WxCpTpAdmin getAdminList(Integer agentId) throws WxErrorException; + } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpAgentWorkBenchService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpAgentWorkBenchService.java new file mode 100644 index 0000000000..67c57a8a88 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpAgentWorkBenchService.java @@ -0,0 +1,47 @@ +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.WxCpAgentWorkBench; + +/** + * The interface Wx cp agent work bench service. + * 工作台自定义展示 + * + * @author songshiyu + * created on 16:16 2020/9/27 + */ +public interface WxCpAgentWorkBenchService { + + /** + * Sets work bench template. + * + * @param wxCpAgentWorkBench the wx cp agent work bench + * @throws WxErrorException the wx error exception + */ + void setWorkBenchTemplate(WxCpAgentWorkBench wxCpAgentWorkBench) throws WxErrorException; + + /** + * Gets work bench template. + * + * @param agentid the agentid + * @return the work bench template + * @throws WxErrorException the wx error exception + */ + String getWorkBenchTemplate(Long agentid) throws WxErrorException; + + /** + * Sets work bench data. + * + * @param wxCpAgentWorkBench the wx cp agent work bench + * @throws WxErrorException the wx error exception + */ + void setWorkBenchData(WxCpAgentWorkBench wxCpAgentWorkBench) throws WxErrorException; + + /** + * Batch sets work bench data. + * + * @param wxCpAgentWorkBench the wx cp agent work bench + * @throws WxErrorException the wx error exception + */ + void batchSetWorkBenchData(WxCpAgentWorkBench wxCpAgentWorkBench) throws WxErrorException; +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpChatService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpChatService.java index 741ee906d5..6cbe2ec8ac 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpChatService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpChatService.java @@ -1,8 +1,8 @@ package me.chanjar.weixin.cp.api; import me.chanjar.weixin.common.error.WxErrorException; -import me.chanjar.weixin.cp.bean.WxCpAppChatMessage; import me.chanjar.weixin.cp.bean.WxCpChat; +import me.chanjar.weixin.cp.bean.message.WxCpAppChatMessage; import java.util.List; @@ -12,10 +12,6 @@ * @author gaigeshen */ public interface WxCpChatService { - - @Deprecated - String chatCreate(String name, String owner, List users, String chatId) throws WxErrorException; - /** * 创建群聊会话,注意:刚创建的群,如果没有下发消息,在企业微信不会出现该群. * @@ -23,14 +19,11 @@ public interface WxCpChatService { * @param owner 指定群主的id。如果不指定,系统会随机从userlist中选一人作为群主 * @param users 群成员id列表。至少2人,至多500人 * @param chatId 群聊的唯一标志,不能与已有的群重复;字符串类型,最长32个字符。只允许字符0-9及字母a-zA-Z。如果不填,系统会随机生成群id - * @return 创建的群聊会话chatId + * @return 创建的群聊会话chatId string * @throws WxErrorException 异常 */ String create(String name, String owner, List users, String chatId) throws WxErrorException; - @Deprecated - void chatUpdate(String chatId, String name, String owner, List usersToAdd, List usersToDelete) throws WxErrorException; - /** * 修改群聊会话. * @@ -43,14 +36,11 @@ public interface WxCpChatService { */ void update(String chatId, String name, String owner, List usersToAdd, List usersToDelete) throws WxErrorException; - @Deprecated - WxCpChat chatGet(String chatId) throws WxErrorException; - /** * 获取群聊会话. * * @param chatId 群聊编号 - * @return 群聊会话 + * @return 群聊会话 wx cp chat * @throws WxErrorException 异常 */ WxCpChat get(String chatId) throws WxErrorException; @@ -59,7 +49,7 @@ public interface WxCpChatService { * 应用支持推送文本、图片、视频、文件、图文等类型. * 请求方式: POST(HTTPS) * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/appchat/send?access_token=ACCESS_TOKEN - * 文档地址:https://work.weixin.qq.com/api/doc#90000/90135/90248 + * 文档地址:... * * @param message 要发送的消息内容对象 * @throws WxErrorException 异常 diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpCorpGroupService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpCorpGroupService.java new file mode 100644 index 0000000000..69aea4bca7 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpCorpGroupService.java @@ -0,0 +1,27 @@ +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.corpgroup.WxCpCorpGroupCorp; + +import java.util.List; + +/** + * 企业互联相关接口 + * + * @author libo <422423229@qq.com> + * @since 2023-02-27 9:57 PM + */ +public interface WxCpCorpGroupService { + /** + * List app share info list. + * + * @param agentId the agent id + * @param businessType the business type + * @param corpId the corp id + * @param limit the limit + * @param cursor the cursor + * @return the list + * @throws WxErrorException the wx error exception + */ + List listAppShareInfo(Integer agentId, Integer businessType, String corpId, Integer limit, String cursor) throws WxErrorException; +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpDepartmentService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpDepartmentService.java index c86816b7f2..75bf02a64b 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpDepartmentService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpDepartmentService.java @@ -19,31 +19,55 @@ public interface WxCpDepartmentService { *
    * 部门管理接口 - 创建部门.
    * 最多支持创建500个部门
-   * 详情请见: https://work.weixin.qq.com/api/doc#90000/90135/90205
+   * 详情请见: ...
    * 
* * @param depart 部门 - * @return 部门id + * @return 部门id long * @throws WxErrorException 异常 */ Long create(WxCpDepart depart) throws WxErrorException; + /** + *
+   * 部门管理接口 - 获取单个部门详情.
+   * 详情请见: ...
+   * 
+ * + * @param id 部门id + * @return 部门信息 wx cp depart + * @throws WxErrorException 异常 + */ + WxCpDepart get(Long id) throws WxErrorException; + /** *
    * 部门管理接口 - 获取部门列表.
-   * 详情请见: https://work.weixin.qq.com/api/doc#90000/90135/90208
+   * 详情请见: ...
    * 
* * @param id 部门id。获取指定部门及其下的子部门。非必需,可为null - * @return 获取的部门列表 + * @return 获取的部门列表 list * @throws WxErrorException 异常 */ List list(Long id) throws WxErrorException; + /** + *
+   * 部门管理接口 - 获取子部门ID列表.
+   * 详情请见: ...
+   * 
+ * + * @param id 部门id。获取指定部门及其下的子部门(以及子部门的子部门等等,递归)。 如果不填,默认获取全量组织架构 + * @return 子部门ID列表 list + * @throws WxErrorException 异常 + */ + List simpleList(Long id) throws WxErrorException; + /** *
    * 部门管理接口 - 更新部门.
-   * 详情请见: https://work.weixin.qq.com/api/doc#90000/90135/90206
+   * 详情请见: ...
    * 如果id为0(未部门),1(黑名单),2(星标组),或者不存在的id,微信会返回系统繁忙的错误
    * 
* @@ -55,7 +79,7 @@ public interface WxCpDepartmentService { /** *
    * 部门管理接口 - 删除部门.
-   * 详情请见: https://work.weixin.qq.com/api/doc#90000/90135/90207
+   * 详情请见: ...
    * 应用须拥有指定部门的管理权限
    * 
* diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExportService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExportService.java new file mode 100644 index 0000000000..a2c7adabea --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExportService.java @@ -0,0 +1,99 @@ +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.export.WxCpExportRequest; +import me.chanjar.weixin.cp.bean.export.WxCpExportResult; + +/** + * 异步导出接口 + * + * @author zhongjun created on 2022/4/21 + */ +public interface WxCpExportService { + + /** + *
+   *
+   * 导出成员
+   *
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/export/simple_user?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/94849
+   * 
+ * + * @param params 导出参数 + * @return jobId 异步任务id + * @throws WxErrorException . + */ + String simpleUser(WxCpExportRequest params) throws WxErrorException; + + /** + *
+   *
+   * 导出成员详情
+   *
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/export/user?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/94851
+   * 
+ * + * @param params 导出参数 + * @return jobId 异步任务id + * @throws WxErrorException . + */ + String user(WxCpExportRequest params) throws WxErrorException; + + /** + *
+   *
+   * 导出部门
+   *
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/export/department?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/94852
+   * 
+ * + * @param params 导出参数 + * @return jobId 异步任务id + * @throws WxErrorException . + */ + String department(WxCpExportRequest params) throws WxErrorException; + + /** + *
+   *
+   * 导出标签成员
+   *
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/export/taguser?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/94853
+   * 
+ * + * @param params 导出参数 + * @return jobId 异步任务id + * @throws WxErrorException . + */ + String tagUser(WxCpExportRequest params) throws WxErrorException; + + /** + *
+   *
+   * 获取导出结果
+   *
+   * 请求方式:GET(HTTPS)
+   * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/export/get_result?access_token=ACCESS_TOKEN&jobid=jobid_xxxxxxxxxxxxxxx}
+   *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/94854
+   * 返回的url文件下载解密可参考 CSDN
+   * 
+ * + * @param jobId 异步任务id + * @return 导出结果 result + * @throws WxErrorException . + */ + WxCpExportResult getResult(String jobId) throws WxErrorException; +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExternalContactService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExternalContactService.java index 92f2258696..6de9f9226d 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExternalContactService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExternalContactService.java @@ -1,9 +1,20 @@ package me.chanjar.weixin.cp.api; -import me.chanjar.weixin.common.error.WxErrorException; -import me.chanjar.weixin.cp.bean.WxCpUserExternalContactInfo; - +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Date; import java.util.List; +import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; +import me.chanjar.weixin.cp.bean.external.*; +import me.chanjar.weixin.cp.bean.external.acquisition.*; +import me.chanjar.weixin.cp.bean.external.contact.*; +import me.chanjar.weixin.cp.bean.external.interceptrule.WxCpInterceptRule; +import me.chanjar.weixin.cp.bean.external.interceptrule.WxCpInterceptRuleAddRequest; +import me.chanjar.weixin.cp.bean.external.interceptrule.WxCpInterceptRuleInfo; +import me.chanjar.weixin.cp.bean.external.interceptrule.WxCpInterceptRuleList; /** *
@@ -14,6 +25,103 @@
  * @author JoeCao
  */
 public interface WxCpExternalContactService {
+
+  /**
+   * 配置客户联系「联系我」方式
+   * 
+   * 企业可以在管理后台-客户联系中配置成员的「联系我」的二维码或者小程序按钮,客户通过扫描二维码或点击小程序上的按钮,即可获取成员联系方式,主动联系到成员。
+   * 企业可通过此接口为具有客户联系功能的成员生成专属的「联系我」二维码或者「联系我」按钮。
+   * 如果配置的是「联系我」按钮,需要开发者的小程序接入小程序插件。
+   *
+   * 注意:
+   * 通过API添加的「联系我」不会在管理端进行展示,每个企业可通过API最多配置50万个「联系我」。
+   * 用户需要妥善存储返回的config_id,config_id丢失可能导致用户无法编辑或删除「联系我」。
+   * 临时会话模式不占用「联系我」数量,但每日最多添加10万个,并且仅支持单人。
+   * 临时会话模式的二维码,添加好友完成后该二维码即刻失效。
+   * 文档地址
+   * 
+ * + * @param info 客户联系「联系我」方式 + * @return wx cp contact way result + * @throws WxErrorException the wx error exception + */ + WxCpContactWayResult addContactWay(WxCpContactWayInfo info) throws WxErrorException; + + /** + * 获取企业已配置的「联系我」方式 + * + *
+   * 批量获取企业配置的「联系我」二维码和「联系我」小程序按钮。
+   * 
+ * + * @param configId 联系方式的配置id,必填 + * @return contact way + * @throws WxErrorException the wx error exception + */ + WxCpContactWayInfo getContactWay(String configId) throws WxErrorException; + + /** + * 获取企业已配置的「联系我」列表 + * + *
+   * 获取企业配置的「联系我」二维码和「联系我」小程序插件列表。不包含临时会话。
+   * 注意,该接口仅可获取2021年7月10日以后创建的「联系我」
+   * 
+ * + * 文档地址: 获取企业已配置的「联系我」列表 + * + * @param startTime 「联系我」创建起始时间戳, 默认为90天前 + * @param endTime 「联系我」创建结束时间戳, 默认为当前时间 + * @param cursor 分页查询使用的游标,为上次请求返回的 next_cursor + * @param limit 每次查询的分页大小,默认为100条,最多支持1000条 + * @return contact way configId + * @throws WxErrorException the wx error exception + */ + WxCpContactWayList listContactWay(Long startTime, Long endTime, String cursor, Long limit) throws WxErrorException; + + /** + * 更新企业已配置的「联系我」方式 + * + *
+   * 更新企业配置的「联系我」二维码和「联系我」小程序按钮中的信息,如使用人员和备注等。
+   * 
+ * + * @param info 客户联系「联系我」方式 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp updateContactWay(WxCpContactWayInfo info) throws WxErrorException; + + /** + * 删除企业已配置的「联系我」方式 + * + *
+   * 删除一个已配置的「联系我」二维码或者「联系我」小程序按钮。
+   * 
+ * + * @param configId 企业联系方式的配置id,必填 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp deleteContactWay(String configId) throws WxErrorException; + + /** + * 结束临时会话 + * + *
+   * 将指定的企业成员和客户之前的临时会话断开,断开前会自动下发已配置的结束语。
+   *
+   * 注意:请保证传入的企业成员和客户之间有仍然有效的临时会话, 通过其他方式的添加外部联系人无法通过此接口关闭会话。
+   * 
+ * + * @param userId the user id + * @param externalUserId the external user id + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp closeTempChat(String userId, String externalUserId) throws WxErrorException; + + /** * 获取外部联系人详情. *
@@ -23,26 +131,329 @@ public interface WxCpExternalContactService {
    * 第三方应用调用时,返回的跟进人follow_user仅包含应用可见范围之内的成员。
    * 
* - * @param userId 外部联系人的userid + * @param externalUserId 外部联系人的userid + * @return . external contact + * @throws WxErrorException the wx error exception + * @deprecated 建议使用 {@link #getContactDetail(String, String)} + */ + @Deprecated + WxCpExternalContactInfo getExternalContact(String externalUserId) throws WxErrorException; + + /** + * 获取客户详情. + *
+   *
+   * 企业可通过此接口,根据外部联系人的userid(如何获取?),拉取客户详情。
+   *
+   * 请求方式:GET(HTTPS)
+   * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get?access_token=ACCESS_TOKEN&external_userid=EXTERNAL_USERID}
+   *
+   * 权限说明:
+   *
+   * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?);
+   * 第三方/自建应用调用时,返回的跟进人follow_user仅包含应用可见范围之内的成员。
+   * 
+ * + * @param externalUserId 外部联系人的userid,注意不是企业成员的帐号 + * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 + * @return . contact detail + * @throws WxErrorException . + */ + WxCpExternalContactInfo getContactDetail(String externalUserId, String cursor) throws WxErrorException; + + /** + * 企业和服务商可通过此接口,将微信外部联系人的userid转为微信openid,用于调用支付相关接口。暂不支持企业微信外部联系人(ExternalUserid为wo开头)的userid转openid。 + * + * @param externalUserid 微信外部联系人的userid + * @return 该企业的外部联系人openid string + * @throws WxErrorException . + */ + String convertToOpenid(String externalUserid) throws WxErrorException; + + /** + * 服务商为企业代开发微信小程序的场景,服务商可通过此接口,将微信客户的unionid转为external_userid。 + *
+   *
+   * 文档地址:https://work.weixin.qq.com/api/doc/90001/90143/93274
+   *
+   * 服务商代开发小程序指企业使用的小程序为企业主体的,非服务商主体的小程序。
+   * 场景:企业客户在微信端从企业主体的小程序(非服务商应用)登录,同时企业在企业微信安装了服务商的第三方应用,服务商可以调用该接口将登录用户的unionid转换为服务商全局唯一的外部联系人id
+   *
+   * 权限说明:
+   *
+   * 仅认证企业可调用
+   * unionid必须是企业主体下的unionid。即unionid的主体(为绑定了该小程序的微信开放平台账号主体)需与当前企业的主体一致。
+   * unionid的主体(即微信开放平台账号主体)需认证
+   * 该客户的跟进人必须在应用的可见范围之内
+   * 
+ * + * @param unionid 微信客户的unionid + * @param openid the openid + * @return 该企业的外部联系人ID string + * @throws WxErrorException . + */ + String unionidToExternalUserid(String unionid, String openid) throws WxErrorException; + + /** + * 配置客户群进群方式 + * 企业可以在管理后台-客户联系中配置「加入群聊」的二维码或者小程序按钮,客户通过扫描二维码或点击小程序上的按钮,即可加入特定的客户群。 + * 企业可通过此接口为具有客户联系功能的成员生成专属的二维码或者小程序按钮。 + * 如果配置的是小程序按钮,需要开发者的小程序接入小程序插件。 + * 注意: + * 通过API添加的配置不会在管理端进行展示,每个企业可通过API最多配置50万个「加入群聊」(与「联系我」共用50万的额度)。 + * 文档地址:https://developer.work.weixin.qq.com/document/path/92229 + * + * @param wxCpGroupJoinWayInfo the wx cp group join way info + * @return {@link WxCpGroupJoinWayResult} + * @throws WxErrorException the wx error exception + */ + WxCpGroupJoinWayResult addJoinWay(WxCpGroupJoinWayInfo wxCpGroupJoinWayInfo) throws WxErrorException; + + /** + * 更新客户群进群方式配置 + * 更新进群方式配置信息。注意:使用覆盖的方式更新。 + * 文档地址:https://developer.work.weixin.qq.com/document/path/92229 + * + * @param wxCpGroupJoinWayInfo the wx cp group join way info + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp updateJoinWay(WxCpGroupJoinWayInfo wxCpGroupJoinWayInfo) throws WxErrorException; + + /** + * 获取客户群进群方式配置 + * 获取企业配置的群二维码或小程序按钮。 + * 文档地址:https://developer.work.weixin.qq.com/document/path/92229 + * + * @param configId the config id + * @return join way + * @throws WxErrorException the wx error exception + */ + WxCpGroupJoinWayInfo getJoinWay(String configId) throws WxErrorException; + + /** + * 删除客户群进群方式配置 + * 文档地址:https://developer.work.weixin.qq.com/document/path/92229 + * + * @param configId the config id + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp delJoinWay(String configId) throws WxErrorException; + + /** + * 代开发应用external_userid转换 + *
+   *
+   * 文档地址:https://work.weixin.qq.com/api/doc/90001/90143/95195
+   *
+   * 企业同时安装服务商第三方应用以及授权代开发自建应用的时,服务商可使用该接口将代开发应用获取到的外部联系人id跟第三方应用的id进行关联,
+   * 该接口可将代开发自建应用获取到的external_userid转换为服务商第三方应用的external_userid。
+   *
+   * 权限说明:
+   *
+   * {@code 该企业授权了该服务商第三方应用,且授权的第三方应用具备“企业客户权限->客户基础信息”权限}
+   * 该客户的跟进人必须在应用的可见范围之内
+   * {@code 应用需具备“企业客户权限->客户基础信息”权限}
+   * 
+ * + * @param externalUserid 代开发自建应用获取到的外部联系人ID + * @return 该服务商第三方应用下的企业的外部联系人ID string + * @throws WxErrorException . + */ + String toServiceExternalUserid(String externalUserid) throws WxErrorException; + + /** + * 将代开发应用或第三方应用获取的externaluserid转换成自建应用的externaluserid + *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/95884#external-userid%E8%BD%AC%E6%8D%A2
+   *
+   * 权限说明:
+   *
+   * 需要使用自建应用或基础应用的access_token
+   * 客户的跟进人,或者用户所在客户群的群主,需要同时在access_token和source_agentid所对应应用的可见范围内
+   * 
+ * + * @param externalUserid 服务商主体的external_userid,必须是source_agentid对应的应用所获取 + * @param sourceAgentId 企业授权的代开发自建应用或第三方应用的agentid + * @return 企业的external_userid + * @throws WxErrorException 微信错误异常 + */ + String fromServiceExternalUserid(String externalUserid, String sourceAgentId) throws WxErrorException; + + /** + * 企业客户微信unionid的升级 - unionid查询external_userid + *
+   *
+   * 文档地址:https://open.work.weixin.qq.com/api/doc/35863#4.2%20unionid%E6%9F%A5%E8%AF%A2external_userid
+   *
+   * 当微信用户在微信中使用第三方应用的小程序或公众号时,第三方可将获取到的unionid与openid,调用此接口转换为企业客户external_userid。
+   * 该接口调用频次有限,每个服务商每小时仅可调用1万次,仅用于微信用户主动使用第三方应用的场景来调用,服务商切不可滥用。
+   * 同时建议服务商的小程序路径或公众号页面链接带上corpid参数,如此可明确地转换出该企业对应的external_userid,以获得更好的性能。
+   *
+   * 权限说明:
+   *
+   * 该企业授权了该服务商第三方应用
+   * 调用频率最大为10000次/小时
+   * unionid和openid的主体需与服务商的主体一致
+   * openid与unionid必须是在同一个小程序或同一个公众号获取到的
+   * 
+ * + * @param unionid 微信客户的unionid + * @param openid 微信客户的openid + * @param corpid 需要换取的企业corpid,不填则拉取所有企业 + * @return 该服务商第三方应用下的企业的外部联系人ID wx cp external user id list + * @throws WxErrorException . + */ + WxCpExternalUserIdList unionidToExternalUserid3rd(String unionid, String openid, String corpid) throws WxErrorException; + + /** + * 转换external_userid + *
+   *
+   * 文档地址:https://open.work.weixin.qq.com/api/doc/35863#转换external_userid
+   *
+   * 对于历史已授权的企业,在2022年3月1号之前,所有接口与回调返回的external_userid仍然为旧的external_userid,
+   * 从2022年3月1号0点开始,所有输入与返回的external_userid字段,将启用升级后的external_userid。
+   * 所以服务商需要在此之前完成历史数据的迁移整改
+   *
+   * 权限说明:
+   *
+   * 该企业授权了该服务商第三方应用
+   * external_userid对应的跟进人需要在应用可见范围内
+   * 
+ * + * @param externalUserIdList 微信客户的unionid + * @return List 新外部联系人id + * @throws WxErrorException . + */ + WxCpNewExternalUserIdList getNewExternalUserId(String[] externalUserIdList) throws WxErrorException; + + /** + * 设置迁移完成 + *
+   *
+   * 文档地址:https://open.work.weixin.qq.com/api/doc/35863#转换external_userid
+   *
+   * 企业授权确认之后,且服务商完成了新旧external_userid的迁移,即可主动将该企业设置为“迁移完成”,
+   * 设置之后,从该企业获取到的将是新的external_userid。注意,该接口需要使用provider_access_token来调用,
+   * 对于有多个应用的服务商,可逐个应用进行external_userid的转换,最后再使用provider_access_token调用该接口完成设置。
+   *
+   * 权限说明:
+   *
+   * 该企业授权了该服务商第三方应用
+   * 
+ * + * @param corpid 企业corpid + * @return wx cp base resp + * @throws WxErrorException . + */ + WxCpBaseResp finishExternalUserIdMigration(String corpid) throws WxErrorException; + + /** + * 客户群opengid转换 + *
+   *
+   * 文档地址:https://open.work.weixin.qq.com/api/doc/90000/90135/94822
+   *
+   * 用户在微信里的客户群里打开小程序时,某些场景下可以获取到群的opengid,如果该群是企业微信的客户群,
+   * 则企业或第三方可以调用此接口将一个opengid转换为客户群chat_id
+   *
+   * 权限说明:
+   *
+   * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)
+   * {@code 第三方应用需具有“企业客户权限->客户基础信息”权限}
+   * 对于第三方/自建应用,群主必须在应用的可见范围
+   * 仅支持企业服务人员创建的客户群
+   * 仅可转换出自己企业下的客户群chat_id
+   * 
+ * + * @param opengid 小程序在微信获取到的群ID,参见wx.getGroupEnterInfo(https://developers.weixin.qq + * .com/miniprogram/dev/api/open-api/group/wx.getGroupEnterInfo.html) + * @return 客户群ID ,可以用来调用获取客户群详情 + * @throws WxErrorException . + */ + String opengidToChatid(String opengid) throws WxErrorException; + + /** + * 批量获取客户详情. + *
+   *
+   * 企业/第三方可通过此接口获取指定成员添加的客户信息列表。
+   *
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/batch/get_by_user?access_token=ACCESS_TOKEN
+   *
+   * 权限说明:
+   *
+   * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?);
+   * 第三方/自建应用调用时,返回的跟进人follow_user仅包含应用可见范围之内的成员。
+   * 
+ * + * @param userIdList 企业成员的userid列表,注意不是外部联系人的帐号 + * @param cursor the cursor + * @param limit the limit + * @return wx cp user external contact batch info + * @throws WxErrorException . + */ + WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String cursor, + Integer limit) + throws WxErrorException; + + /** + * 获取已服务的外部联系人 + *
+   *  企业可通过此接口获取所有已服务的外部联系人,及其添加人和加入的群聊。
+   * 外部联系人分为客户和其他外部联系人,如果是客户,接口将返回外部联系人临时ID和externaluserid;如果是其他外部联系人,接口将只返回外部联系人临时ID。
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/contact_list?access_token=ACCESS_TOKEN
+   * 文档地址: https://developer.work.weixin.qq.com/document/path/99434
+   * 
+ * + * 注意:企业可通过外部联系人临时ID排除重复数据,外部联系人临时ID有效期为4小时。 + * + * @param cursor the cursor + * @param limit the limit + * @return 已服务的外部联系人列表 + * @throws WxErrorException . + */ + WxCpExternalContactListInfo getContactList(String cursor, Integer limit) throws WxErrorException; + + /** + * 修改客户备注信息. + *
+   * 企业可通过此接口修改指定用户添加的客户的备注信息。
+   * 请求方式: POST(HTTP)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/remark?access_token=ACCESS_TOKEN
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/92115
+   * 
+ * + * @param request 备注信息请求 + * @throws WxErrorException . */ - WxCpUserExternalContactInfo getExternalContact(String userId) throws WxErrorException; + void updateRemark(WxCpUpdateRemarkRequest request) throws WxErrorException; /** - * 获取外部联系人列表. + * 获取客户列表. *
-   *   企业可通过此接口获取指定成员添加的客户列表。
-   *   客户是指配置了客户联系功能的成员所添加的外部联系人。
-   *   没有配置客户联系功能的成员,所添加的外部联系人将不会作为客户返回。
+   *   企业可通过此接口获取指定成员添加的客户列表。客户是指配置了客户联系功能的成员所添加的外部联系人。没有配置客户联系功能的成员,所添加的外部联系人将不会作为客户返回。
+   *
+   * 请求方式:GET(HTTPS)
+   * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list?access_token=ACCESS_TOKEN&userid=USERID}
+   *
+   * 权限说明:
+   *
+   * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?);
    * 第三方应用需拥有“企业客户”权限。
-   * 第三方应用调用时,返回的跟进人follow_user仅包含应用可见范围之内的成员。
+   * 第三方/自建应用只能获取到可见范围内的配置了客户联系功能的成员。
    * 
* - * @param userId 外部联系人的userid + * @param userId 企业成员的userid * @return List of External wx id + * @throws WxErrorException . */ List listExternalContacts(String userId) throws WxErrorException; - /** * 企业和第三方服务商可通过此接口获取配置了客户联系功能的成员(Customer Contact)列表。 *
@@ -52,7 +463,943 @@ public interface WxCpExternalContactService {
    * 
* * @return List of CpUser id + * @throws WxErrorException . + */ + List listFollowers() throws WxErrorException; + + /** + * 获取待分配的离职成员列表 + * 企业和第三方可通过此接口,获取所有离职成员的客户列表,并可进一步调用分配离职成员的客户接口将这些客户重新分配给其他企业成员。 + + * + * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_unassigned_list?access_token=ACCESS_TOKEN + * + * @param pageId 分页查询,要查询页号,从0开始 + * @param cursor 分页查询游标,字符串类型,适用于数据量较大的情况,如果使用该参数则无需填写page_id,该参数由上一次调用返回 + * @param pageSize 每次返回的最大记录数,默认为1000,最大值为1000 + * @return wx cp user external unassign list + * @throws WxErrorException the wx error exception + */ + WxCpUserExternalUnassignList listUnassignedList(Integer pageId, String cursor, Integer pageSize) throws WxErrorException; + + /** + * 企业可通过此接口,将已离职成员的外部联系人分配给另一个成员接替联系。 + * + * @param externalUserid the external userid + * @param handOverUserid the hand over userid + * @param takeOverUserid the take over userid + * @return wx cp base resp + * @throws WxErrorException the wx error exception + * @deprecated 此后续将不再更新维护, 建议使用 {@link #transferCustomer(WxCpUserTransferCustomerReq)} + */ + @Deprecated + WxCpBaseResp transferExternalContact(String externalUserid, String handOverUserid, String takeOverUserid) throws WxErrorException; + + /** + * 企业可通过此接口,转接在职成员的客户给其他成员。 + *
+   * external_userid必须是handover_userid的客户(即配置了客户联系功能的成员所添加的联系人)。
+   * 在职成员的每位客户最多被分配2次。客户被转接成功后,将有90个自然日的服务关系保护期,保护期内的客户无法再次被分配。
+   *
+   * 权限说明:
+   * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+   * {@code 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限}
+   * 接替成员必须在此第三方应用或自建应用的可见范围内。
+   * 接替成员需要配置了客户联系功能。
+   * 接替成员需要在企业微信激活且已经过实名认证。
+   * 
+ * + * @param req 转接在职成员的客户给其他成员请求实体 + * @return wx cp base resp + * @throws WxErrorException the wx error exception */ - List listFollowUser() throws WxErrorException; + WxCpUserTransferCustomerResp transferCustomer(WxCpUserTransferCustomerReq req) throws WxErrorException; + /** + * 企业和第三方可通过此接口查询在职成员的客户转接情况。 + *
+   * 权限说明:
+   *
+   * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+   * {@code 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限}
+   * 接替成员必须在此第三方应用或自建应用的可见范围内。
+   * 
+ * + * @param handOverUserid 原添加成员的userid + * @param takeOverUserid 接替成员的userid + * @param cursor 分页查询的cursor,每个分页返回的数据不会超过1000条;不填或为空表示获取第一个分页; + * @return 客户转接接口实体 wx cp user transfer result resp + * @throws WxErrorException the wx error exception + */ + WxCpUserTransferResultResp transferResult(String handOverUserid, String takeOverUserid, + String cursor) throws WxErrorException; + + /** + * 企业可通过此接口,分配离职成员的客户给其他成员。 + *
+   * handover_userid必须是已离职用户。
+   * external_userid必须是handover_userid的客户(即配置了客户联系功能的成员所添加的联系人)。
+   * 在职成员的每位客户最多被分配2次。客户被转接成功后,将有90个自然日的服务关系保护期,保护期内的客户无法再次被分配。
+
+   *
+   * 权限说明:
+
+   *
+   * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+   * {@code 第三方应用需拥有“企业客户权限->客户联系->离职分配”权限}
+   * 接替成员必须在此第三方应用或自建应用的可见范围内。
+   * 接替成员需要配置了客户联系功能。
+   * 接替成员需要在企业微信激活且已经过实名认证。
+   * 
+ * + * @param req 转接在职成员的客户给其他成员请求实体 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpUserTransferCustomerResp resignedTransferCustomer(WxCpUserTransferCustomerReq req) throws WxErrorException; + + /** + * 企业和第三方可通过此接口查询离职成员的客户分配情况。 + *
+   * 权限说明:
+
+   *
+   * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+   * {@code 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限}
+   * 接替成员必须在此第三方应用或自建应用的可见范围内。
+   * 
+ * + * @param handOverUserid 原添加成员的userid + * @param takeOverUserid 接替成员的userid + * @param cursor 分页查询的cursor,每个分页返回的数据不会超过1000条;不填或为空表示获取第一个分页; + * @return 客户转接接口实体 wx cp user transfer result resp + * @throws WxErrorException the wx error exception + */ + WxCpUserTransferResultResp resignedTransferResult(String handOverUserid, String takeOverUserid, + String cursor) throws WxErrorException; + + /** + *
+   * 该接口用于获取配置过客户群管理的客户群列表。
+   * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+   * 暂不支持第三方调用。
+   * 微信文档:https://work.weixin.qq.com/api/doc/90000/90135/92119
+   * 
+ * + * @param pageIndex the page index + * @param pageSize the page size + * @param status the status + * @param userIds the user ids + * @param partyIds the party ids + * @return the wx cp user external group chat list + * @throws WxErrorException the wx error exception + * @deprecated 请使用 {@link WxCpExternalContactService#listGroupChat(Integer, String, int, String[])} + */ + @Deprecated + WxCpUserExternalGroupChatList listGroupChat(Integer pageIndex, Integer pageSize, int status, String[] userIds, + String[] partyIds) throws WxErrorException; + + /** + *
+   * 该接口用于获取配置过客户群管理的客户群列表。
+   * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+   * 暂不支持第三方调用。
+   * 微信文档:https://work.weixin.qq.com/api/doc/90000/90135/92119
+   * 
+ * + * @param limit 分页,预期请求的数据量,取值范围 1 ~ 1000 + * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用不填 + * @param status 客户群跟进状态过滤。0 - 所有列表(即不过滤) 1 - 离职待继承 2 - 离职继承中 3 - 离职继承完成 默认为0 + * @param userIds 群主过滤。如果不填,表示获取应用可见范围内全部群主的数据(但是不建议这么用,如果可见范围人数超过1000人,为了防止数据包过大,会报错 81017);用户ID列表。最多100个 + * @return the wx cp user external group chat list + * @throws WxErrorException the wx error exception + */ + WxCpUserExternalGroupChatList listGroupChat(Integer limit, String cursor, int status, String[] userIds) throws WxErrorException; + + /** + *
+   * 通过客户群ID,获取详情。包括群名、群成员列表、群成员入群时间、入群方式。(客户群是由具有客户群使用权限的成员创建的外部群)
+   * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+   * 暂不支持第三方调用。
+   * 微信文档:https://work.weixin.qq.com/api/doc/90000/90135/92122
+   * 
+ * + * @param chatId the chat id + * @param needName the need name + * @return group chat + * @throws WxErrorException the wx error exception + */ + WxCpUserExternalGroupChatInfo getGroupChat(String chatId, Integer needName) throws WxErrorException; + + /** + * 企业可通过此接口,将已离职成员为群主的群,分配给另一个客服成员。 + * + *
+   * 注意::
+
+   *
+   * 群主离职了的客户群,才可继承
+   * 继承给的新群主,必须是配置了客户联系功能的成员
+   * 继承给的新群主,必须有设置实名
+   * 继承给的新群主,必须有激活企业微信
+   * 同一个人的群,限制每天最多分配300个给新群主
+
+   *
+   * 权限说明:
+
+   *
+   * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+   * {@code 第三方应用需拥有“企业客户权限->客户联系->分配离职成员的客户群”权限}
+   * 对于第三方/自建应用,群主必须在应用的可见范围。
+   * 
+ * + * @param chatIds 需要转群主的客户群ID列表。取值范围: 1 ~ 100 + * @param newOwner 新群主ID + * @return 分配结果 ,主要是分配失败的群列表 + * @throws WxErrorException the wx error exception + */ + WxCpUserExternalGroupChatTransferResp transferGroupChat(String[] chatIds, String newOwner) throws WxErrorException; + + + /** + * 企业可通过此接口,将在职成员为群主的群,分配给另一个客服成员。 + *
+   * 注意:
+   * 继承给的新群主,必须是配置了客户联系功能的成员
+   * 继承给的新群主,必须有设置实名
+   * 继承给的新群主,必须有激活企业微信
+   * 同一个人的群,限制每天最多分配300个给新群主
+   * 为保障客户服务体验,90个自然日内,在职成员的每个客户群仅可被转接2次。
+   * 
+ * + * @param chatIds 需要转群主的客户群ID列表。取值范围: 1 ~ 100 + * @param newOwner 新群主ID + * @return 分配结果 ,主要是分配失败的群列表 + * @throws WxErrorException the wx error exception + */ + WxCpUserExternalGroupChatTransferResp onjobTransferGroupChat(String[] chatIds, String newOwner) throws WxErrorException; + + /** + *
+   * 企业可通过此接口获取成员联系客户的数据,包括发起申请数、新增客户数、聊天数、发送消息数和删除/拉黑成员的客户数等指标。
+   * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+   * 第三方应用需拥有“企业客户”权限。
+   * 第三方/自建应用调用时传入的userid和partyid要在应用的可见范围内;
+   * 
+ * + * @param startTime the start time + * @param endTime the end time + * @param userIds the user ids + * @param partyIds the party ids + * @return user behavior statistic + * @throws WxErrorException the wx error exception + */ + WxCpUserExternalUserBehaviorStatistic getUserBehaviorStatistic(Date startTime, Date endTime, String[] userIds, + String[] partyIds) throws WxErrorException; + + /** + *
+   * 获取指定日期全天的统计数据。注意,企业微信仅存储60天的数据。
+   * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
+   * 暂不支持第三方调用。
+   * 
+ * + * @param startTime the start time + * @param orderBy the order by + * @param orderAsc the order asc + * @param pageIndex the page index + * @param pageSize the page size + * @param userIds the user ids + * @param partyIds the party ids + * @return group chat statistic + * @throws WxErrorException the wx error exception + */ + WxCpUserExternalGroupChatStatistic getGroupChatStatistic(Date startTime, Integer orderBy, Integer orderAsc, + Integer pageIndex, Integer pageSize, String[] userIds, + String[] partyIds) throws WxErrorException; + + /** + * 添加企业群发消息任务 + * 企业可通过此接口添加企业群发消息的任务并通知客服人员发送给相关客户或客户群。(注:企业微信终端需升级到2.7.5版本及以上) + * 注意:调用该接口并不会直接发送消息给客户/客户群,需要相关的客服人员操作以后才会实际发送(客服人员的企业微信需要升级到2.7.5及以上版本) + * 同一个企业每个自然月内仅可针对一个客户/客户群发送4条消息,超过限制的用户将会被忽略。 + + * + * 请求方式: POST(HTTP) + + * + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_msg_template?access_token=ACCESS_TOKEN + + * + * 文档地址 + * + * @param wxCpMsgTemplate the wx cp msg template + * @return the wx cp msg template add result + * @throws WxErrorException the wx error exception + */ + WxCpMsgTemplateAddResult addMsgTemplate(WxCpMsgTemplate wxCpMsgTemplate) throws WxErrorException; + + + /** + * 提醒成员群发 + * 企业和第三方应用可调用此接口,重新触发群发通知,提醒成员完成群发任务,24小时内每个群发最多触发三次提醒。 + + * + * 请求方式: POST(HTTPS) + + * + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/remind_groupmsg_send?access_token=ACCESS_TOKEN + * + * 文档地址 + * + * @param msgId 群发消息的id,通过获取群发记录列表接口返回 + * @return the wx cp msg template add result + * @throws WxErrorException 微信错误异常 + */ + WxCpBaseResp remindGroupMsgSend(String msgId) throws WxErrorException; + + + /** + * 停止企业群发 + * 企业和第三方应用可调用此接口,停止无需成员继续发送的企业群发 + *

+ * 请求方式: POST(HTTPS) + *

+ * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/cancel_groupmsg_send?access_token=ACCESS_TOKEN + * + * 文档地址 + * + * @param msgId 群发消息的id,通过获取群发记录列表接口返回 + * @return the wx cp msg template add result + * @throws WxErrorException 微信错误异常 + */ + WxCpBaseResp cancelGroupMsgSend(String msgId) throws WxErrorException; + + + /** + * 发送新客户欢迎语 + *

+   * 企业微信在向企业推送添加外部联系人事件时,会额外返回一个welcome_code,企业以此为凭据调用接口,即可通过成员向新添加的客户发送个性化的欢迎语。
+   * 为了保证用户体验以及避免滥用,企业仅可在收到相关事件后20秒内调用,且只可调用一次。
+   * 如果企业已经在管理端为相关成员配置了可用的欢迎语,则推送添加外部联系人事件时不会返回welcome_code。
+   * 每次添加新客户时可能有多个企业自建应用/第三方应用收到带有welcome_code的回调事件,但仅有最先调用的可以发送成功。后续调用将返回41051(externaluser has started chatting)错误,请用户根据实际使用需求,合理设置应用可见范围,避免冲突。
+   * 请求方式: POST(HTTP)
+   *
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/send_welcome_msg?access_token=ACCESS_TOKEN
+   *
+   * 文档地址
+   * 
+ * + * @param msg . + * @throws WxErrorException . + */ + void sendWelcomeMsg(WxCpWelcomeMsg msg) throws WxErrorException; + + /** + *
+   * 企业可通过此接口获取企业客户标签详情。
+   * 
+ * + * @param tagId the tag id + * @return corp tag list + * @throws WxErrorException the wx error exception + */ + WxCpUserExternalTagGroupList getCorpTagList(String[] tagId) throws WxErrorException; + + /** + *
+   * 企业可通过此接口获取企业客户标签详情。
+   * 若tag_id和group_id均为空,则返回所有标签。
+   * 同时传递tag_id和group_id时,忽略tag_id,仅以group_id作为过滤条件。
+   * 
+ * + * @param tagId the tag id + * @param groupId the tagGroup id + * @return corp tag list + * @throws WxErrorException the wx error exception + */ + WxCpUserExternalTagGroupList getCorpTagList(String[] tagId, String[] groupId) throws WxErrorException; + + /** + *
+   * 企业可通过此接口向客户标签库中添加新的标签组和标签,每个企业最多可配置3000个企业标签。
+   * 暂不支持第三方调用。
+   * 
+ * + * @param tagGroup the tag group + * @return wx cp user external tag group info + * @throws WxErrorException the wx error exception + */ + WxCpUserExternalTagGroupInfo addCorpTag(WxCpUserExternalTagGroupInfo tagGroup) throws WxErrorException; + + /** + *
+   * 企业可通过此接口编辑客户标签/标签组的名称或次序值。
+   * 暂不支持第三方调用。
+   * 
+ * + * @param id the id + * @param name the name + * @param order the order + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp editCorpTag(String id, String name, Integer order) throws WxErrorException; + + /** + *
+   * 企业可通过此接口删除客户标签库中的标签,或删除整个标签组。
+   * 暂不支持第三方调用。
+   * 
+ * + * @param tagId the tag id + * @param groupId the group id + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp delCorpTag(String[] tagId, String[] groupId) throws WxErrorException; + + /** + *
+   * 企业可通过此接口为指定成员的客户添加上由企业统一配置的标签。
+   * 文档地址
+   * 
+ * + * @param userid the userid + * @param externalUserid the external userid + * @param addTag the add tag + * @param removeTag the remove tag + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp markTag(String userid, String externalUserid, String[] addTag, String[] removeTag) throws WxErrorException; + + /** + *
+   *   企业和第三方应用可通过该接口创建客户朋友圈的发表任务。
+   *   文档地址
+   * 
+ * + * @param task the task + * @return wx cp add moment result + * @throws WxErrorException the wx error exception + */ + WxCpAddMomentResult addMomentTask(WxCpAddMomentTask task) throws WxErrorException; + + /** + *
+   * 由于发表任务的创建是异步执行的,应用需要再调用该接口以获取创建的结果。
+   * 文档地址
+   * 
+ * + * @param jobId 异步任务id,最大长度为64字节,由创建发表内容到客户朋友圈任务接口获取 + * @return moment task result + * @throws WxErrorException the wx error exception + */ + WxCpGetMomentTaskResult getMomentTaskResult(String jobId) throws WxErrorException; + + + /** + *
+   *   停止发表企业朋友圈。
+   *   文档地址
+   * 
+ * + * @param momentId 朋友圈id,可通过获取客户朋友圈企业发表的列表接口获取朋友圈企业发表的列表 + * @return wx cp add moment result + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp cancelMomentTask(String momentId) throws WxErrorException; + + + /** + *
+   * 获取客户朋友圈全部的发表记录 获取企业全部的发表列表
+   * 文档地址
+   * 
+ * + * @param startTime 朋友圈记录开始时间。Unix时间戳 + * @param endTime 朋友圈记录结束时间。Unix时间戳 + * @param creator 朋友圈创建人的userid + * @param filterType 朋友圈类型。0:企业发表 1:个人发表 2:所有,包括个人创建以及企业创建,默认情况下为所有类型 + * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 + * @param limit 返回的最大记录数,整型,最大值100,默认值100,超过最大值时取默认值 + * @return moment list + * @throws WxErrorException the wx error exception + */ + WxCpGetMomentList getMomentList(Long startTime, Long endTime, String creator, Integer filterType, + String cursor, Integer limit) throws WxErrorException; + + /** + *
+   * 获取客户朋友圈全部的发表记录 获取客户朋友圈企业发表的列表
+   * 文档地址
+   * 
+ * + * @param momentId 朋友圈id,仅支持企业发表的朋友圈id + * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 + * @param limit 返回的最大记录数,整型,最大值1000,默认值500,超过最大值时取默认值 + * @return moment task + * @throws WxErrorException the wx error exception + */ + WxCpGetMomentTask getMomentTask(String momentId, String cursor, Integer limit) + throws WxErrorException; + + /** + *
+   * 获取客户朋友圈全部的发表记录 获取客户朋友圈发表时选择的可见范围
+   * 文档地址
+   * 
+ * + * @param momentId 朋友圈id + * @param userId 企业发表成员userid,如果是企业创建的朋友圈,可以通过获取客户朋友圈企业发表的 + * 列表获取已发表成员userid,如果是个人创建的朋友圈,创建人userid就是企业发表成员userid + * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 + * @param limit 返回的最大记录数,整型,最大值1000,默认值500,超过最大值时取默认值 + * @return moment customer list + * @throws WxErrorException the wx error exception + */ + WxCpGetMomentCustomerList getMomentCustomerList(String momentId, String userId, + String cursor, Integer limit) throws WxErrorException; + + /** + *
+   * 获取客户朋友圈全部的发表记录 获取客户朋友圈发表后的可见客户列表
+   * 文档地址
+   * 
+ * + * @param momentId 朋友圈id + * @param userId 企业发表成员userid,如果是企业创建的朋友圈,可以通过获取客户朋友圈企业发表的列表获取已发表成员userid, + * 如果是个人创建的朋友圈,创建人userid就是企业发表成员userid + * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 + * @param limit 返回的最大记录数,整型,最大值5000,默认值3000,超过最大值时取默认值 + * @return moment send result + * @throws WxErrorException the wx error exception + */ + WxCpGetMomentSendResult getMomentSendResult(String momentId, String userId, + String cursor, Integer limit) throws WxErrorException; + + /** + *
+   * 获取客户朋友圈全部的发表记录 获取客户朋友圈的互动数据
+   * 文档地址
+   * 
+ * + * @param momentId 朋友圈id + * @param userId 企业发表成员userid,如果是企业创建的朋友圈,可以通过获取客户朋友圈企业发表的列表获取已发表成员userid, + * 如果是个人创建的朋友圈,创建人userid就是企业发表成员userid + * @return moment comments + * @throws WxErrorException the wx error exception + */ + WxCpGetMomentComments getMomentComments(String momentId, String userId) + throws WxErrorException; + + /** + *
+   * 企业和第三方应用可通过此接口获取企业与成员的群发记录。
+   * 文档地址
+   * 
+ * + * @param chatType 群发任务的类型,默认为single,表示发送给客户,group表示发送给客户群 + * @param startTime 群发任务记录开始时间 + * @param endTime 群发任务记录结束时间 + * @param creator 群发任务创建人企业账号id + * @param filterType 创建人类型。0:企业发表 1:个人发表 2:所有,包括个人创建以及企业创建,默认情况下为所有类型 + * @param limit 返回的最大记录数,整型,最大值100,默认值50,超过最大值时取默认值 + * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpGroupMsgListResult getGroupMsgListV2(String chatType, Date startTime, Date endTime, + String creator, Integer filterType, Integer limit, String cursor) throws WxErrorException; + + /** + *
+   * 企业和第三方应用可通过此接口获取企业与成员的群发记录。
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/93338
+   * 
+ * + * @param msgid 群发消息的id,通过获取群发记录列表接口返回 + * @param userid 发送成员userid,通过获取群发成员发送任务列表接口返回 + * @param limit 返回的最大记录数,整型,最大值1000,默认值500,超过最大值时取默认值 + * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpGroupMsgSendResult getGroupMsgSendResult(String msgid, String userid, Integer limit, String cursor) throws WxErrorException; + + /** + *
+   * 企业跟第三方应用可通过该接口获取到创建企业群发的群发发送结果。
+   * 文档
+   * 
+ * + * @param msgid 群发消息的id,通过创建企业群发接口返回 + * @param limit 返回的最大记录数,整型,最大值10000,默认值10000 + * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpGroupMsgResult getGroupMsgResult(String msgid, Integer limit, String cursor) throws WxErrorException; + + /** + *
+   * 获取群发成员发送任务列表。
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/93338
+   * 
+ * + * @param msgid 群发消息的id,通过获取群发记录列表接口返回 + * @param limit 返回的最大记录数,整型,最大值1000,默认值500,超过最大值时取默认值 + * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpGroupMsgTaskResult getGroupMsgTask(String msgid, Integer limit, String cursor) throws WxErrorException; + + /** + *
+   * 添加入群欢迎语素材。
+   * 文档地址:https://open.work.weixin.qq.com/api/doc/90000/90135/92366
+   * 
+ * + * @param template 素材内容 + * @return template_id 欢迎语素材id + * @throws WxErrorException the wx error exception + */ + String addGroupWelcomeTemplate(WxCpGroupWelcomeTemplateResult template) throws WxErrorException; + + /** + *
+   * 编辑入群欢迎语素材。
+   * 文档地址:https://open.work.weixin.qq.com/api/doc/90000/90135/92366
+   * 
+ * + * @param template the template + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp editGroupWelcomeTemplate(WxCpGroupWelcomeTemplateResult template) throws WxErrorException; + + /** + *
+   * 获取入群欢迎语素材。
+   * 文档地址:https://open.work.weixin.qq.com/api/doc/90000/90135/92366
+   * 
+ * + * @param templateId 群欢迎语的素材id + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpGroupWelcomeTemplateResult getGroupWelcomeTemplate(String templateId) throws WxErrorException; + + /** + *
+   * 删除入群欢迎语素材。
+   * 企业可通过此API删除入群欢迎语素材,且仅能删除调用方自己创建的入群欢迎语素材。
+   * 文档地址:https://open.work.weixin.qq.com/api/doc/90000/90135/92366
+   * 
+ * + * @param templateId 群欢迎语的素材id + * @param agentId the agent id + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp delGroupWelcomeTemplate(String templateId, String agentId) throws WxErrorException; + + /** + *
+   * 获取商品图册列表
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/95096
+   * 
+ * + * @param limit 返回的最大记录数,整型,最大值100,默认值50,超过最大值时取默认值 + * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpProductAlbumListResult getProductAlbumList(Integer limit, String cursor) throws WxErrorException; + + /** + *
+   * 获取商品图册
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/95096
+   * 
+ * + * @param productId 商品id + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpProductAlbumResult getProductAlbum(String productId) throws WxErrorException; + + /** + *
+   * 上传附件资源
+   * ...
+   * 
+ * + * @param mediaType the media type + * @param fileType the file type + * @param attachmentType the attachment type + * @param inputStream the input stream + * @return wx media upload result + * @throws WxErrorException the wx error exception + * @throws IOException the io exception + */ + WxMediaUploadResult uploadAttachment(String mediaType, String fileType, Integer attachmentType, + InputStream inputStream) throws WxErrorException, IOException; + + /** + *
+   * 上传附件资源
+   * ...
+   * 
+ * + * @param mediaType the media type + * @param attachmentType the attachment type + * @param file the file + * @return wx media upload result + * @throws WxErrorException the wx error exception + */ + WxMediaUploadResult uploadAttachment(String mediaType, Integer attachmentType, File file) + throws WxErrorException; + + /** + *
+   * 新建敏感词规则
+   * 企业和第三方应用可以通过此接口新建敏感词规则
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_intercept_rule?access_token=ACCESS_TOKEN
+   * 
+ * @param ruleAddRequest the rule add request + * @return 规则id + * @throws WxErrorException the wx error exception + */ + String addInterceptRule(WxCpInterceptRuleAddRequest ruleAddRequest) throws WxErrorException; + + /** + *
+   * 修改敏感词规则
+   * 文档地址
+   * 企业和第三方应用可以通过此接口修改敏感词规则
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/update_intercept_rule?access_token=ACCESS_TOKEN
+   * 
+ * @param interceptRule the rule + * @throws WxErrorException the wx error exception + */ + void updateInterceptRule(WxCpInterceptRule interceptRule) throws WxErrorException; + + /** + *
+   * 删除敏感词规则
+   * 企业和第三方应用可以通过此接口修改敏感词规则
+   * 请求方式:POST(HTTPS)
+   * 请求地址
+   * 
+ * @param ruleId 规则id + * @throws WxErrorException the wx error exception + */ + void delInterceptRule(String ruleId) throws WxErrorException; + + /** + * 获取敏感词规则列表 + * + * 企业和第三方应用可以通过此接口获取所有设置的敏感词规则列表。 + * 请求方式:GET(HTTPS) + * 文档地址:获取敏感词规则列表 + * + * @return WxCpInterceptRuleList 敏感词规则列表 + * @throws WxErrorException 微信API异常 + */ + WxCpInterceptRuleList getInterceptRuleList() throws WxErrorException; + + /** + * 获取敏感词详情 + * + * 企业和第三方应用可以通过此接口获取单个敏感词规则的详细信息。 + * 请求方式:GET(HTTPS) + * 文档地址:获取敏感词详情 + * + * @param ruleId 敏感词规则ID + * @return WxCpInterceptRuleInfo 敏感词规则详情 + * @throws WxErrorException 微信API异常 + */ + WxCpInterceptRuleInfo getInterceptRuleDetail(String ruleId) throws WxErrorException; + + /** + *
+   * 创建商品图册
+   * 企业和第三方应用可以通过此接口增加商品
+   * 请求方式:POST(HTTPS)
+   * 请求地址:
+   * https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_product_album?access_token=ACCESS_TOKEN
+   * 文档地址
+   * 
+ * @param wxCpProductAlbumInfo 商品图册信息 + * @return 商品id string + * @throws WxErrorException the wx error exception + */ + String addProductAlbum(WxCpProductAlbumInfo wxCpProductAlbumInfo) throws WxErrorException; + + /** + *
+   * 编辑商品图册
+   * 企业和第三方应用可以通过此接口修改商品信息
+   * 请求方式:POST(HTTPS)
+   * 请求地址:
+   * https://qyapi.weixin.qq.com/cgi-bin/externalcontact/update_product_album?access_token=ACCESS_TOKEN
+   * 文档地址
+   * 
+ * @param wxCpProductAlbumInfo 商品图册信息 + * @throws WxErrorException the wx error exception + */ + void updateProductAlbum(WxCpProductAlbumInfo wxCpProductAlbumInfo) throws WxErrorException; + + /** + *
+   * 删除商品图册
+   * 企业和第三方应用可以通过此接口删除商品信息
+   * 请求方式:POST(HTTPS)
+   * 请求地址:
+   * https://qyapi.weixin.qq.com/cgi-bin/externalcontact/delete_product_album?access_token=ACCESS_TOKEN
+   *
+   * 文档地址
+   * 
+ * @param productId 商品id + * @throws WxErrorException the wx error exception + */ + void deleteProductAlbum(String productId) throws WxErrorException; + + /** + *
+   * 获取获客链接列表
+   * 企业可通过此接口获取当前仍然有效的获客链接。
+   * 请求方式:POST(HTTPS)
+   * 请求地址:
+   * 接口地址
+   *
+   * 文档地址
+   * 
+ * @param limit 商品id + * @param cursor 商品id + * @return 获客链接列表 + * @throws WxErrorException the wx error exception + */ + WxCpCustomerAcquisitionList customerAcquisitionLinkList(Integer limit, String cursor) throws WxErrorException; + + /** + *
+   * 获取获客链接详情
+   * 企业可通过此接口根据获客链接id获取链接配置详情。。
+   * 请求方式:POST(HTTPS)
+   * 请求地址:
+   * 接口地址
+   *
+   * 文档地址
+   * 
+ * @param linkId 获客链接ID + * @return 获客链接详情 + * @throws WxErrorException the wx error exception + */ + WxCpCustomerAcquisitionInfo customerAcquisitionLinkGet(String linkId) throws WxErrorException; + + /** + *
+   * 创建获客链接
+   * 企业可通过此接口创建新的获客链接。
+   * 请求方式:POST(HTTPS)
+   * 请求地址:
+   * 接口地址
+   * 文档地址
+   * 
+ * + * @param wxCpCustomerAcquisitionRequest 创建链接请求 + * @return 创建链接详情 + * @throws WxErrorException the wx error exception + */ + WxCpCustomerAcquisitionCreateResult customerAcquisitionLinkCreate(WxCpCustomerAcquisitionRequest wxCpCustomerAcquisitionRequest) throws WxErrorException; + + /** + *
+   * 编辑获客链接
+   * 企业可通过此接口编辑获客链接,修改获客链接的关联范围或修改获客链接的名称。
+   * 请求方式:POST(HTTPS)
+   * 请求地址:
+   * 接口地址
+   * 文档地址
+   * 
+ * + * @param wxCpCustomerAcquisitionRequest 编辑链接请求 + * @return 编辑链接详情 + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp customerAcquisitionUpdate(WxCpCustomerAcquisitionRequest wxCpCustomerAcquisitionRequest) throws WxErrorException; + + /** + *
+   * 删除获客链接
+   * 企业可通过此接口删除获客链接,删除后的获客链接将无法继续使用。
+   * 请求方式:POST(HTTPS)
+   * 请求地址:
+   * 接口地址
+   * 文档地址
+   * 
+ * + * @param linkId 获客链接的id + * @return 删除结果 + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp customerAcquisitionLinkDelete(String linkId) throws WxErrorException; + + /** + *
+   * 获取获客客户列表
+   * 企业可通过此接口获取到由指定的获客链接添加的客户列表。
+   * 请求方式:POST(HTTPS)
+   * 请求地址:
+   * 接口地址
+   * 文档地址
+   * 
+ * + * @param linkId 获客链接id + * @param limit 返回的最大记录数,整型,最大值1000 + * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 + * @return 由获客链接添加的客户信息列表 + * @throws WxErrorException the wx error exception + */ + WxCpCustomerAcquisitionCustomerList customerAcquisitionCustomer(String linkId, Integer limit, String cursor) throws WxErrorException; + + /** + *
+   * 查询剩余使用量
+   * 企业可通过此接口查询当前剩余的使用量。
+   * 请求方式:GET(HTTPS)
+   * 请求地址:
+   * 接口地址
+   * 文档地址
+   * 
+ * + * @return 剩余使用量 + * @throws WxErrorException the wx error exception + */ + WxCpCustomerAcquisitionQuota customerAcquisitionQuota() throws WxErrorException; + + + /** + * 查询链接使用详情 + * 服务商可通过此接口查询指定组件授权的获客链接在指定时间范围内的访问情况。 + * + * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/statistic?access_token=ACCESS_TOKEN + * + * @author Hugo + * @since 2023/12/5 14:34 + * @param linkId 获客链接的id + * @param startTime 统计起始时间 + * @param endTime 统计结束时间 + * @return 点击链接客户数和新增客户数 + * @throws WxErrorException the wx error exception + */ + WxCpCustomerAcquisitionStatistic customerAcquisitionStatistic(String linkId, Date startTime, Date endTime) throws WxErrorException; } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpGroupRobotService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpGroupRobotService.java new file mode 100644 index 0000000000..b8ccea5e50 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpGroupRobotService.java @@ -0,0 +1,136 @@ +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.article.NewArticle; +import me.chanjar.weixin.cp.bean.message.WxCpGroupRobotMessage; + +import java.util.List; + +/** + * 微信群机器人消息发送api + * 文档地址:https://work.weixin.qq.com/help?doc_id=13376 + * 调用地址:https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key= + * + * @author yr created on 2020-8-20 + */ +public interface WxCpGroupRobotService { + + /** + * 发送text类型的消息 + * + * @param content 文本内容,最长不超过2048个字节,必须是utf8编码 + * @param mentionedList userId的列表,提醒群中的指定成员(@某个成员),@all表示提醒所有人,如果开发者获取不到userId,可以使用mentioned_mobile_list + * @param mobileList 手机号列表,提醒手机号对应的群成员(@某个成员),@all表示提醒所有人 + * @throws WxErrorException 异常 + */ + void sendText(String content, List mentionedList, List mobileList) throws WxErrorException; + + /** + * 发送markdown类型的消息 + * + * @param content markdown内容,最长不超过4096个字节,必须是utf8编码 + * @throws WxErrorException 异常 + */ + void sendMarkdown(String content) throws WxErrorException; + + /** + * 发送image类型的消息 + * + * @param base64 图片内容的base64编码 + * @param md5 图片内容(base64编码前)的md5值 + * @throws WxErrorException 异常 + */ + void sendImage(String base64, String md5) throws WxErrorException; + + /** + * 发送news类型的消息 + * + * @param articleList 图文消息,支持1到8条图文 + * @throws WxErrorException 异常 + */ + void sendNews(List articleList) throws WxErrorException; + + /** + * 发送text类型的消息 + * + * @param webhookUrl webhook地址 + * @param content 文本内容,最长不超过2048个字节,必须是utf8编码 + * @param mentionedList userId的列表,提醒群中的指定成员(@某个成员),@all表示提醒所有人,如果开发者获取不到userId,可以使用mentioned_mobile_list + * @param mobileList 手机号列表,提醒手机号对应的群成员(@某个成员),@all表示提醒所有人 + * @throws WxErrorException 异常 + */ + void sendText(String webhookUrl, String content, List mentionedList, List mobileList) throws WxErrorException; + + /** + * 发送markdown类型的消息 + * + * @param webhookUrl webhook地址 + * @param content markdown内容,最长不超过4096个字节,必须是utf8编码 + * @throws WxErrorException 异常 + */ + void sendMarkdown(String webhookUrl, String content) throws WxErrorException; + + /** + * 发送markdown_v2类型的消息 + * + * @param content markdown内容,最长不超过4096个字节,必须是utf8编码 + * @throws WxErrorException 异常 + */ + void sendMarkdownV2(String content) throws WxErrorException; + + /** + * 发送markdown_v2类型的消息 + * + * @param webhookUrl webhook地址 + * @param content markdown内容,最长不超过4096个字节,必须是utf8编码 + * @throws WxErrorException 异常 + */ + void sendMarkdownV2(String webhookUrl, String content) throws WxErrorException; + + /** + * 发送image类型的消息 + * + * @param webhookUrl webhook地址 + * @param base64 图片内容的base64编码 + * @param md5 图片内容(base64编码前)的md5值 + * @throws WxErrorException 异常 + */ + void sendImage(String webhookUrl, String base64, String md5) throws WxErrorException; + + /** + * 发送news类型的消息 + * + * @param webhookUrl webhook地址 + * @param articleList 图文消息,支持1到8条图文 + * @throws WxErrorException 异常 + */ + void sendNews(String webhookUrl, List articleList) throws WxErrorException; + + /** + * 发送文件类型的消息 + * + * @param webhookUrl webhook地址 + * @param mediaId 文件id + * @throws WxErrorException 异常 + */ + void sendFile(String webhookUrl, String mediaId) throws WxErrorException; + + /** + * 发送文件类型的消息 + * + * @param webhookUrl webhook地址 + * @param mediaId 语音文件id + * @throws WxErrorException 异常 + */ + void sendVoice(String webhookUrl, String mediaId) throws WxErrorException; + + /** + * 发送模板卡片消息 + * + * @param webhookUrl webhook地址 + * @param wxCpGroupRobotMessage 群机器人消息 + * @throws WxErrorException 异常 + */ + void sendTemplateCardMessage(String webhookUrl, WxCpGroupRobotMessage wxCpGroupRobotMessage) throws WxErrorException; + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpHrService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpHrService.java new file mode 100644 index 0000000000..d9d6ed0129 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpHrService.java @@ -0,0 +1,68 @@ +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.hr.WxCpHrEmployeeFieldData; +import me.chanjar.weixin.cp.bean.hr.WxCpHrEmployeeFieldDataResp; +import me.chanjar.weixin.cp.bean.hr.WxCpHrEmployeeFieldInfoResp; + +import java.util.List; + +/** + * 人事助手相关接口. + * 官方文档:... + * + * @author copilot + */ +public interface WxCpHrService { + + /** + * 获取员工档案字段信息. + *

+ * 请求方式:POST(HTTPS) + * 请求地址:... + * 权限说明: + * 需要配置人事助手的secret,调用接口前需给对应成员赋予人事小助手应用的权限。 + * + * @param fields 指定字段key列表,不填则返回全部字段 + * @return 字段信息响应 wx cp hr employee field info resp + * @throws WxErrorException the wx error exception + */ + WxCpHrEmployeeFieldInfoResp getFieldInfo(List fields) throws WxErrorException; + + /** + * 获取员工档案数据. + *

+ * 请求方式:POST(HTTPS) + * 请求地址:... + * 权限说明: + * 需要配置人事助手的secret,调用接口前需给对应成员赋予人事小助手应用的权限。 + * + * @param userid 员工userid + * @param fields 指定字段key列表 + * @return 员工档案数据响应 wx cp hr employee field data resp + * @throws WxErrorException the wx error exception + */ + WxCpHrEmployeeFieldDataResp getEmployeeFieldInfo(String userid, List fields) throws WxErrorException; + + /** + * 获取员工档案数据. + *

+ * 请求方式:POST(HTTPS) + * 请求地址:... + */ + WxCpHrEmployeeFieldDataResp getEmployeeFieldInfo(String userid, boolean getAll, List fields) throws WxErrorException; + + /** + * 更新员工档案数据. + *

+ * 请求方式:POST(HTTPS) + * 请求地址:... + * 权限说明: + * 需要配置人事助手的secret,调用接口前需给对应成员赋予人事小助手应用的权限。 + * + * @param userid 员工userid + * @param fieldList 字段数据列表 + * @throws WxErrorException the wx error exception + */ + void updateEmployeeFieldInfo(String userid, List fieldList) throws WxErrorException; +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpIntelligentRobotService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpIntelligentRobotService.java new file mode 100644 index 0000000000..58f4373ceb --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpIntelligentRobotService.java @@ -0,0 +1,85 @@ +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.intelligentrobot.*; + +/** + * 企业微信智能机器人接口 + * 官方文档: https://developer.work.weixin.qq.com/document/path/101039 + * + * @author Binary Wang + */ +public interface WxCpIntelligentRobotService { + + /** + * 创建智能机器人 + * + * @param request 创建请求参数 + * @return 创建结果 + * @throws WxErrorException 微信接口异常 + */ + WxCpIntelligentRobotCreateResponse createRobot(WxCpIntelligentRobotCreateRequest request) throws WxErrorException; + + /** + * 删除智能机器人 + * + * @param robotId 机器人ID + * @throws WxErrorException 微信接口异常 + */ + void deleteRobot(String robotId) throws WxErrorException; + + /** + * 更新智能机器人 + * + * @param request 更新请求参数 + * @throws WxErrorException 微信接口异常 + */ + void updateRobot(WxCpIntelligentRobotUpdateRequest request) throws WxErrorException; + + /** + * 查询智能机器人 + * + * @param robotId 机器人ID + * @return 机器人信息 + * @throws WxErrorException 微信接口异常 + */ + WxCpIntelligentRobot getRobot(String robotId) throws WxErrorException; + + /** + * 智能机器人会话 + * + * @param request 聊天请求参数 + * @return 聊天响应 + * @throws WxErrorException 微信接口异常 + */ + WxCpIntelligentRobotChatResponse chat(WxCpIntelligentRobotChatRequest request) throws WxErrorException; + + /** + * 重置智能机器人会话 + * + * @param robotId 机器人ID + * @param userid 用户ID + * @param sessionId 会话ID + * @throws WxErrorException 微信接口异常 + */ + void resetSession(String robotId, String userid, String sessionId) throws WxErrorException; + + /** + * 智能机器人主动发送消息 + * 官方文档: https://developer.work.weixin.qq.com/document/path/100719 + * + * @param request 发送消息请求参数 + * @return 发送消息响应 + * @throws WxErrorException 微信接口异常 + */ + WxCpIntelligentRobotSendMessageResponse sendMessage(WxCpIntelligentRobotSendMessageRequest request) throws WxErrorException; + + /** + * 解析智能机器人 API 模式回调消息. + * + * @param callbackMessageJson 回调消息JSON + * @return 解析后的回调消息对象 + */ + WxCpIntelligentRobotMessage parseCallbackMessage(String callbackMessageJson); + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpKfService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpKfService.java new file mode 100644 index 0000000000..046cfbc5bb --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpKfService.java @@ -0,0 +1,294 @@ +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; +import me.chanjar.weixin.cp.bean.kf.*; + +import java.util.List; + +/** + * 微信客服接口 + *

+ * 微信客服由腾讯微信团队为企业打造,用于满足企业的客服需求,帮助企业做好客户服务。企业可以在微信内、外各个场景中接入微信客服, + * 用户可以发起咨询,企业可以进行回复。 + * 企业可在微信客服官网使用企业微信扫码开通微信客服,开通后即可使用。 + * + * @author Fu created on 2022/1/19 19:25 + */ +public interface WxCpKfService { + + /** + * 添加客服帐号,并可设置客服名称和头像。目前一家企业最多可添加10个客服帐号 + * + * @param add 客服帐号信息 + * @return result -新创建的客服帐号ID + * @throws WxErrorException 异常 + */ + WxCpKfAccountAddResp addAccount(WxCpKfAccountAdd add) throws WxErrorException; + + /** + * 修改已有的客服帐号,可修改客服名称和头像。 + * + * @param upd 新的客服账号信息 + * @return result wx cp base resp + * @throws WxErrorException 异常 + */ + WxCpBaseResp updAccount(WxCpKfAccountUpd upd) throws WxErrorException; + + /** + * 删除已有的客服帐号 + * + * @param del 要删除的客服帐号 + * @return result wx cp base resp + * @throws WxErrorException 异常 + */ + WxCpBaseResp delAccount(WxCpKfAccountDel del) throws WxErrorException; + + /** + * 获取客服帐号列表,包括所有的客服帐号的客服ID、名称和头像。 + * + * @param offset 分页,偏移量, 默认为0 + * @param limit 分页,预期请求的数据量,默认为100,取值范围 1 ~ 100 + * @return 客服帐号列表 wx cp kf account list resp + * @throws WxErrorException 异常 + */ + WxCpKfAccountListResp listAccount(Integer offset, Integer limit) throws WxErrorException; + + /** + * 企业可通过此接口获取带有不同参数的客服链接,不同客服帐号对应不同的客服链接。获取后,企业可将链接嵌入到网页等场景中, + * 微信用户点击链接即可向对应的客服帐号发起咨询。企业可依据参数来识别用户的咨询来源等 + * + * @param link 参数 + * @return 链接 account link + * @throws WxErrorException 异常 + */ + WxCpKfAccountLinkResp getAccountLink(WxCpKfAccountLink link) throws WxErrorException; + + /** + * 接待人员管理 + * 添加指定客服帐号的接待人员,每个客服帐号目前最多可添加500个接待人员。 + * + * @param openKfid 客服帐号ID + * @param userIdList 接待人员userid列表。第三方应用填密文userid,即open_userid 可填充个数:1 ~ 100。超过100个需分批调用。 + * @return 添加客服账号结果 wx cp kf servicer op resp + * @throws WxErrorException 异常 + */ + WxCpKfServicerOpResp addServicer(String openKfid, List userIdList) throws WxErrorException; + + /** + * 接待人员管理 + * 添加指定客服账号的接待人员,每个客服账号目前最多可添加2000个接待人员,20个部门。 + * userid_list和department_id_list至少需要填其中一个 + * + * @param openKfid 客服帐号ID + * @param userIdList 接待人员userid列表。第三方应用填密文userid,即open_userid 可填充个数:1 ~ 100。超过100个需分批调用。 + * @param departmentIdList 接待人员部门id列表 可填充个数:0 ~ 20。 + * @return 添加客服账号结果 wx cp kf servicer op resp + * @throws WxErrorException 异常 + */ + WxCpKfServicerOpResp addServicer(String openKfid, List userIdList,List departmentIdList) throws WxErrorException; + + /** + * 接待人员管理 + * 从客服帐号删除接待人员 + * + * @param openKfid 客服帐号ID + * @param userIdList 接待人员userid列表。第三方应用填密文userid,即open_userid 可填充个数:1 ~ 100。超过100个需分批调用。 + * @return 删除客服账号结果 wx cp kf servicer op resp + * @throws WxErrorException 异常 + */ + WxCpKfServicerOpResp delServicer(String openKfid, List userIdList) throws WxErrorException; + + /** + * 接待人员管理 + * 从客服帐号删除接待人员 + * userid_list和department_id_list至少需要填其中一个 + * + * @param openKfid 客服帐号ID + * @param userIdList 接待人员userid列表。第三方应用填密文userid,即open_userid 可填充个数:1 ~ 100。超过100个需分批调用。 + * @param departmentIdList 接待人员部门id列表 可填充个数:0 ~ 100。超过100个需分批调用。 + * @return 删除客服账号结果 wx cp kf servicer op resp + * @throws WxErrorException 异常 + */ + WxCpKfServicerOpResp delServicer(String openKfid, List userIdList, List departmentIdList) throws WxErrorException; + + /** + * 接待人员管理 + * 获取某个客服帐号的接待人员列表 + * + * @param openKfid 客服帐号ID + * @return 接待人员列表 wx cp kf servicer list resp + * @throws WxErrorException 异常 + */ + WxCpKfServicerListResp listServicer(String openKfid) throws WxErrorException; + + /** + * 分配客服会话 + * 获取会话状态 + * + * @param openKfid 客服帐号ID + * @param externalUserId 微信客户的external_userid + * @return service state + * @throws WxErrorException the wx error exception + */ + WxCpKfServiceStateResp getServiceState(String openKfid, String externalUserId) + throws WxErrorException; + + /** + * 分配客服会话 + * 变更会话状态 + * + * @param openKfid 客服帐号ID + * @param externalUserId 微信客户的external_userid + * @param serviceState 变更的目标状态,状态定义和所允许的变更可参考概述中的流程图和表格 + * @param servicerUserId 接待人员的userid。第三方应用填密文userid,即open_userid。当state=3时要求必填,接待人员须处于“正在接待”中。 + * @return 部分状态返回回复语code wx cp kf service state trans resp + * @throws WxErrorException the wx error exception + */ + WxCpKfServiceStateTransResp transServiceState(String openKfid, String externalUserId, + Integer serviceState, String servicerUserId) throws WxErrorException; + + /** + * 读取消息 + * 微信客户发送的消息、接待人员在企业微信回复的消息、发送消息接口发送失败事件(如被用户拒收)、客户点击菜单消息的回复消息, + * 可以通过该接口获取具体的消息内容和事件。不支持读取通过发送消息接口发送的消息。 + * 支持的消息类型:文本、图片、语音、视频、文件、位置、链接、名片、小程序、菜单、事件。 + * + * @param cursor 上一次调用时返回的next_cursor,第一次拉取可以不填。不多于64字节 + * @param token 回调事件返回的token字段,10分钟内有效;可不填,如果不填接口有严格的频率限制。不多于128字节 + * @param limit 期望请求的数据量,默认值和最大值都为1000。 注意:可能会出现返回条数少于limit的情况,需结合返回的has_more字段判断是否继续请求。 + * @param voiceFormat 语音消息类型,0-Amr 1-Silk,默认0。可通过该参数控制返回的语音格式 + * @return 微信消息 wx cp kf msg list resp + * @throws WxErrorException 异常 + */ + @Deprecated + WxCpKfMsgListResp syncMsg(String cursor, String token, Integer limit, Integer voiceFormat) + throws WxErrorException; + + WxCpKfMsgListResp syncMsg(String cursor, String token, Integer limit, Integer voiceFormat,String open_kfid) + throws WxErrorException; + + /** + * 发送消息 + * 当微信客户处于“新接入待处理”或“由智能助手接待”状态下,可调用该接口给用户发送消息。 + * 注意仅当微信客户在主动发送消息给客服后的48小时内,企业可发送消息给客户,最多可发送5条消息;若用户继续发送消息,企业可再次下发消息。 + * 支持发送消息类型:文本、图片、语音、视频、文件、图文、小程序、菜单消息、地理位置。 + * + * @param request 发送信息 + * @return 发送结果 wx cp kf msg send resp + * @throws WxErrorException 异常 + */ + WxCpKfMsgSendResp sendMsg(WxCpKfMsgSendRequest request) throws WxErrorException; + + /** + * 发送欢迎语等事件响应消息 + * 当特定的事件回调消息包含code字段,或通过接口变更到特定的会话状态,会返回code字段。 + * 开发者可以此code为凭证,调用该接口给用户发送相应事件场景下的消息,如客服欢迎语、客服提示语和会话结束语等。 + * 除"用户进入会话事件"以外,响应消息仅支持会话处于获取该code的会话状态时发送,如将会话转入待接入池时获得的code仅能在会话状态为”待接入池排队中“时发送。 + *

+ * 目前支持的事件场景和相关约束如下: + *

+ * 事件场景 允许下发条数 code有效期 支持的消息类型 获取code途径 + * 用户进入会话,用于发送客服欢迎语 1条 20秒 文本、菜单 事件回调 + * 进入接待池,用于发送排队提示语等 1条 48小时 文本 转接会话接口 + * 从接待池接入会话,用于发送非工作 + * 时间的提示语或超时未回复的提示语 + * 等 1条 48小时 文本 事件回调、转接会话接口 + * 结束会话,用于发送结束会话提示语 + * 或满意度评价等 1条 20秒 文本、菜单 事件回调、转接会话接口 + * + * @param request the request + * @return wx cp kf msg send resp + * @throws WxErrorException the wx error exception + */ + WxCpKfMsgSendResp sendMsgOnEvent(WxCpKfMsgSendRequest request) throws WxErrorException; + + /** + * 获取客户基础信息 + * + * @param externalUserIdList the external user id list + * @return wx cp kf customer batch get resp + * @throws WxErrorException the wx error exception + */ + WxCpKfCustomerBatchGetResp customerBatchGet(List externalUserIdList) + throws WxErrorException; + + /** + *

+   * 获取「客户数据统计」企业汇总数据
+   * 通过此接口,可以获取咨询会话数、咨询客户数等企业汇总统计数据
+   * 请求方式:POST(HTTPS)
+   * 请求地址:
+   * https://qyapi.weixin.qq.com/cgi-bin/kf/get_corp_statistic?access_token=ACCESS_TOKEN
+   * 文档地址:
+   * https://developer.work.weixin.qq.com/document/path/95489
+   * 
+ * @param request 查询参数 + * @return 客户数据统计 -企业汇总数据 + * @throws WxErrorException the wx error exception + */ + WxCpKfGetCorpStatisticResp getCorpStatistic(WxCpKfGetCorpStatisticRequest request) throws WxErrorException; + + /** + *
+   * 获取「客户数据统计」接待人员明细数据
+   * 通过此接口,可获取接入人工会话数、咨询会话数等与接待人员相关的统计信息
+   * 请求方式:POST(HTTPS)
+   * 请求地址:
+   * https://qyapi.weixin.qq.com/cgi-bin/kf/get_servicer_statistic?access_token=ACCESS_TOKEN
+   * 文档地址:
+   * https://developer.work.weixin.qq.com/document/path/95490
+   * 
+ * @param request 查询参数 + * @return 客户数据统计 -企业汇总数据 + * @throws WxErrorException the wx error exception + */ + WxCpKfGetServicerStatisticResp getServicerStatistic(WxCpKfGetServicerStatisticRequest request) throws WxErrorException; + + // 「升级服务」配置 + + /** + * 获取配置的专员与客户群 + * + * @return upgrade service config + * @throws WxErrorException the wx error exception + */ + WxCpKfServiceUpgradeConfigResp getUpgradeServiceConfig() throws WxErrorException; + + /** + * 升级专员服务 + * + * @param openKfid 客服帐号ID + * @param externalUserId 微信客户的external_userid + * @param userid 服务专员的userid + * @param wording 推荐语 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp upgradeMemberService(String openKfid, String externalUserId, + String userid, String wording) throws WxErrorException; + + /** + * 升级客户群服务 + * + * @param openKfid 客服帐号ID + * @param externalUserId 微信客户的external_userid + * @param chatId 客户群id + * @param wording 推荐语 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp upgradeGroupchatService(String openKfid, String externalUserId, + String chatId, String wording) throws WxErrorException; + + /** + * 为客户取消推荐 + * + * @param openKfid 客服帐号ID + * @param externalUserId 微信客户的external_userid + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp cancelUpgradeService(String openKfid, String externalUserId) + throws WxErrorException; +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpLivingService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpLivingService.java new file mode 100644 index 0000000000..63fabad7a1 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpLivingService.java @@ -0,0 +1,124 @@ +package me.chanjar.weixin.cp.api; + +import lombok.NonNull; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.living.*; + +/** + * 企业微信直播接口. + * 官方文档:https://work.weixin.qq.com/api/doc/90000/90135/93633 + * + * @author Wang_Wong created on 2021-12-21 + */ +public interface WxCpLivingService { + + /** + * 获取微信观看直播凭证 + * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/living/get_living_code?access_token=ACCESS_TOKEN + * + * @param openId 用户openid + * @param livingId 直播id + * @return living_code 微信观看直播凭证 + * @throws WxErrorException the wx error exception + */ + String getLivingCode(@NonNull String openId, @NonNull String livingId) throws WxErrorException; + + /** + * 获取直播详情 + * 请求方式:GET(HTTPS) + * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/living/get_living_info?access_token=ACCESS_TOKEN&livingid=LIVINGID} + * + * @param livingId 直播id + * @return 获取的直播详情 living info + * @throws WxErrorException the wx error exception + */ + WxCpLivingInfo getLivingInfo(@NonNull String livingId) throws WxErrorException; + + /** + * 获取直播观看明细 + * 通过该接口可以获取所有观看直播的人员统计 + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/living/get_watch_stat?access_token=ACCESS_TOKEN + * + * @param livingId 直播id + * @param nextKey 上一次调用时返回的next_key,初次调用可以填”0” + * @return watch stat + * @throws WxErrorException the wx error exception + */ + WxCpWatchStat getWatchStat(@NonNull String livingId, String nextKey) throws WxErrorException; + + /** + * 获取成员直播ID列表 + * 通过此接口可以获取指定成员的所有直播ID + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/living/get_user_all_livingid?access_token=ACCESS_TOKEN + * + * @param userId 企业成员的userid + * @param cursor 上一次调用时返回的next_cursor,第一次拉取可以不填 + * @param limit 每次拉取的数据量,默认值和最大值都为100 + * @return user all living id + * @throws WxErrorException the wx error exception + */ + WxCpLivingResult.LivingIdResult getUserAllLivingId(@NonNull String userId, String cursor, Integer limit) throws WxErrorException; + + /** + * 获取跳转小程序商城的直播观众信息 + * 通过此接口,开发者可获取跳转小程序商城的直播间(“推广产品”直播)观众id、邀请人id及对应直播间id,以打通卖货直播的“人货场”信息闭环。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/living/get_living_share_info?access_token=ACCESS_TOKEN + * + * @param wwShareCode "推广产品"直播观众跳转小程序商城时会在小程序path中带上ww_share_code=xxxxx参数 + * @return living share info + * @throws WxErrorException the wx error exception + */ + WxCpLivingShareInfo getLivingShareInfo(@NonNull String wwShareCode) throws WxErrorException; + + /** + * 创建预约直播 + * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/living/create?access_token=ACCESS_TOKEN + * + * @param request 创建预约直播请求参数. + * @return livingId (直播id) + * @throws WxErrorException the wx error exception + */ + String livingCreate(WxCpLivingCreateRequest request) throws WxErrorException; + + /** + * 修改预约直播 + * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/living/modify?access_token=ACCESS_TOKEN + * + * @param request 修改预约直播请求参数. + * @return wx cp living result + * @throws WxErrorException the wx error exception + */ + WxCpLivingResult livingModify(WxCpLivingModifyRequest request) throws WxErrorException; + + /** + * 取消预约直播 + * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/living/cancel?access_token=ACCESS_TOKEN + * + * @param livingId 直播id,仅允许取消预约状态下的直播id + * @return wx cp living result + * @throws WxErrorException the wx error exception + */ + WxCpLivingResult livingCancel(@NonNull String livingId) throws WxErrorException; + + /** + * 删除直播回放 + * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/living/delete_replay_data?access_token=ACCESS_TOKEN + * + * @param livingId 直播id + * @return wx cp living result + * @throws WxErrorException the wx error exception + */ + WxCpLivingResult deleteReplayData(@NonNull String livingId) throws WxErrorException; + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java index a51e04e175..dd5ce594b2 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMediaService.java @@ -2,6 +2,8 @@ import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.media.MediaUploadByUrlReq; +import me.chanjar.weixin.cp.bean.media.MediaUploadByUrlResult; import java.io.File; import java.io.IOException; @@ -31,16 +33,62 @@ public interface WxCpMediaService { * @param mediaType 媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts} * @param fileType 文件类型,请看{@link me.chanjar.weixin.common.api.WxConsts} * @param inputStream 输入流,需要调用方控制关闭该输入流 + * @return the wx media upload result + * @throws WxErrorException the wx error exception + * @throws IOException the io exception */ WxMediaUploadResult upload(String mediaType, String fileType, InputStream inputStream) throws WxErrorException, IOException; + /** + *

+   *   上传多媒体文件.
+   * 
+ * + * @param mediaType 媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts} + * @param filename 文件名.例如:wework.txt + * @param url 远程链接 + * @return wx media upload result + * @throws WxErrorException the wx error exception + * @throws IOException the io exception + */ + WxMediaUploadResult upload(String mediaType, String filename, String url) + throws WxErrorException, IOException; + + /** + *
+   *   上传多媒体文件.
+   * 
+ * + * @param mediaType 媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts} + * @param file 文件对象, 上传的文件内容 + * @param filename 上传内容的实际文件名.例如:wework.txt + * @return wx media upload result + * @throws WxErrorException the wx error exception + */ + WxMediaUploadResult upload(String mediaType, File file, String filename) throws WxErrorException; + + /** + *
+   *   上传多媒体文件.
+   * 
+ * + * @param mediaType 媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts} + * @param inputStream 上传的文件内容 + * @param filename 上传内容的实际文件名.例如:wework.txt + * @return wx media upload result + * @throws WxErrorException the wx error exception + */ + WxMediaUploadResult upload(String mediaType, InputStream inputStream, String filename) throws WxErrorException; + /** * 上传多媒体文件. * * @param mediaType 媒体类型 * @param file 文件对象 - * @see #upload(String, String, InputStream) + * @return the wx media upload result + * @throws WxErrorException the wx error exception + * @see #upload(String, String, InputStream) #upload(String, String, InputStream) */ WxMediaUploadResult upload(String mediaType, File file) throws WxErrorException; @@ -52,7 +100,8 @@ WxMediaUploadResult upload(String mediaType, String fileType, InputStream inputS *
* * @param mediaId 媒体id - * @return 保存到本地的临时文件 + * @return 保存到本地的临时文件 file + * @throws WxErrorException the wx error exception */ File download(String mediaId) throws WxErrorException; @@ -61,13 +110,14 @@ WxMediaUploadResult upload(String mediaType, String fileType, InputStream inputS * 获取高清语音素材. * 可以使用本接口获取从JSSDK的uploadVoice接口上传的临时语音素材,格式为speex,16K采样率。该音频比上文的临时素材获取接口(格式为amr,8K采样率)更加清晰,适合用作语音识别等对音质要求较高的业务。 * 请求方式:GET(HTTPS) - * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/media/get/jssdk?access_token=ACCESS_TOKEN&media_id=MEDIA_ID + * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/media/get/jssdk?access_token=ACCESS_TOKEN&media_id=MEDIA_ID} * 仅企业微信2.4及以上版本支持。 - * 文档地址:https://work.weixin.qq.com/api/doc#90000/90135/90255 + * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/90255 *
* * @param mediaId 媒体id - * @return 保存到本地的临时文件 + * @return 保存到本地的临时文件 jssdk file + * @throws WxErrorException the wx error exception */ File getJssdkFile(String mediaId) throws WxErrorException; @@ -81,7 +131,25 @@ WxMediaUploadResult upload(String mediaType, String fileType, InputStream inputS * * * @param file 上传的文件对象 - * @return 返回图片url + * @return 返回图片url string + * @throws WxErrorException the wx error exception */ String uploadImg(File file) throws WxErrorException; + + /** + * 生成异步上传任务 + * 跟上传临时素材拿到的media_id使用场景是不通用的,目前适配的接口如下:https://developer.work.weixin.qq.com/document/path/96488#%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF%E8%AF%B4%E6%98%8E + * @param req 请求参数 + * @return 返回异步任务id + * @throws WxErrorException the wx error exception + */ + String uploadByUrl(MediaUploadByUrlReq req) throws WxErrorException; + + /** + * 查询异步任务结果 + * @param jobId 任务id。最长为128字节,60分钟内有效 + * @return 返回异步任务结果 + * @throws WxErrorException the wx error exception + */ + MediaUploadByUrlResult uploadByUrl(String jobId) throws WxErrorException; } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMeetingService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMeetingService.java new file mode 100644 index 0000000000..d761f99d0b --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMeetingService.java @@ -0,0 +1,95 @@ +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.oa.meeting.WxCpMeeting; +import me.chanjar.weixin.cp.bean.oa.meeting.WxCpMeetingUpdateResult; +import me.chanjar.weixin.cp.bean.oa.meeting.WxCpUserMeetingIdResult; + +/** + * 企业微信日程接口. + * 企业和开发者通过会议接口可以便捷地预定及管理会议,用于小组周会、部门例会等场景。 + * 调用接口的应用自动成为会议创建者,也可指定成员作为会议管理员辅助管理。 + * 官方文档:https://developer.work.weixin.qq.com/document/path/93626 + * + * @author wangmeng3486 created on 2023-01-31 + */ +public interface WxCpMeetingService { + /** + * 创建预约会议 + *

+ * 该接口用于创建一个预约会议。 + *

+ * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/meeting/create?access_token=ACCESS_TOKEN + * + * @param meeting the meeting + * @return 会议ID string + * @throws WxErrorException the wx error exception + */ + String create(WxCpMeeting meeting) throws WxErrorException; + + /** + * 修改预约会议 + *

+ * 该接口用于修改一个指定的预约会议。。 + *

+ * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/meeting/update?access_token=ACCESS_TOKEN + * + * @param meeting the meeting + * @return wx cp meeting update result + * @throws WxErrorException the wx error exception + */ + WxCpMeetingUpdateResult update(WxCpMeeting meeting) throws WxErrorException; + + + /** + * 取消预约会议 + * 该接口用于取消一个指定的预约会议。 + *

+ * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/meeting/cancel?access_token=ACCESS_TOKEN + * + * @param meetingId 会议ID + * @throws WxErrorException the wx error exception + */ + void cancel(String meetingId) throws WxErrorException; + + /** + * 获取会议详情 + *

+ * 该接口用于获取指定会议的详情内容。 + *

+ * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/meeting/get?access_token=ACCESS_TOKEN + * + * @param meetingId the meeting ids + * @return the details + * @throws WxErrorException the wx error exception + */ + WxCpMeeting getDetail(String meetingId) throws WxErrorException; + + /** + * 获取成员会议ID列表 + * 该接口用于获取指定成员指定时间内的会议ID列表。 + *

+ * 权限说明: + * 只能拉取该应用创建的会议ID + * 自建应用需要配置在“可调用接口的应用”列表 + * 第三方服务商创建应用的时候,需要开启“会议接口权限” + * 代开发自建应用需要授权“会议接口权限” + *

+ * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/meeting/get_user_meetingid?access_token=ACCESS_TOKEN + * + * @param userId 企业成员的userid + * @param cursor 上一次调用时返回的cursor,初次调用可以填"0" + * @param limit 每次拉取的数据量,默认值和最大值都为100 + * @param beginTime 开始时间 + * @param endTime 结束时间,时间跨度不超过180天。如果begin_time和end_time都没填的话,默认end_time为当前时间 + * @return result of listUserMeetingIds + * @throws WxErrorException the wx error exception + */ + WxCpUserMeetingIdResult getUserMeetingIds(String userId, String cursor, Integer limit, + Long beginTime, Long endTime) throws WxErrorException; +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMenuService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMenuService.java index 309b981211..07f300dd14 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMenuService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMenuService.java @@ -22,7 +22,8 @@ public interface WxCpMenuService { * * * @param menu 菜单对象 - * @see #create(Integer, WxMenu) + * @throws WxErrorException the wx error exception + * @see #create(Integer, WxMenu) #create(Integer, WxMenu) */ void create(WxMenu menu) throws WxErrorException; @@ -36,7 +37,8 @@ public interface WxCpMenuService { * * @param agentId 企业号应用的id * @param menu 菜单对象 - * @see #create(me.chanjar.weixin.common.bean.menu.WxMenu) + * @throws WxErrorException the wx error exception + * @see #create(me.chanjar.weixin.common.bean.menu.WxMenu) #create(me.chanjar.weixin.common.bean.menu.WxMenu) */ void create(Integer agentId, WxMenu menu) throws WxErrorException; @@ -48,7 +50,8 @@ public interface WxCpMenuService { * 注意: 这个方法使用WxCpConfigStorage里的agentId * * - * @see #delete(Integer) + * @throws WxErrorException the wx error exception + * @see #delete(Integer) #delete(Integer) */ void delete() throws WxErrorException; @@ -61,7 +64,8 @@ public interface WxCpMenuService { * * * @param agentId 企业号应用的id - * @see #delete() + * @throws WxErrorException the wx error exception + * @see #delete() #delete() */ void delete(Integer agentId) throws WxErrorException; @@ -73,7 +77,9 @@ public interface WxCpMenuService { * 注意: 这个方法使用WxCpConfigStorage里的agentId * * - * @see #get(Integer) + * @return the wx menu + * @throws WxErrorException the wx error exception + * @see #get(Integer) #get(Integer) */ WxMenu get() throws WxErrorException; @@ -86,7 +92,9 @@ public interface WxCpMenuService { * * * @param agentId 企业号应用的id - * @see #get() + * @return the wx menu + * @throws WxErrorException the wx error exception + * @see #get() #get() */ WxMenu get(Integer agentId) throws WxErrorException; } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMessageService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMessageService.java new file mode 100644 index 0000000000..534cc89b36 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMessageService.java @@ -0,0 +1,81 @@ +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.message.*; + +/** + * 消息推送接口. + * + * @author Binary Wang created on 2020 -08-30 + */ +public interface WxCpMessageService { + /** + *

+   * 发送消息
+   * 详情请见: https://work.weixin.qq.com/api/doc/90000/90135/90236
+   * 
+ * + * @param message 要发送的消息对象 + * @return the wx cp message send result + * @throws WxErrorException the wx error exception + */ + WxCpMessageSendResult send(WxCpMessage message) throws WxErrorException; + + /** + *
+   * 查询应用消息发送统计
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/message/get_statistics?access_token=ACCESS_TOKEN
+   *
+   * 详情请见: https://work.weixin.qq.com/api/doc/90000/90135/92369
+   * 
+ * + * @param timeType 查询哪天的数据,0:当天;1:昨天。默认为0。 + * @return 统计结果 statistics + * @throws WxErrorException the wx error exception + */ + WxCpMessageSendStatistics getStatistics(int timeType) throws WxErrorException; + + /** + *
+   * 互联企业的应用支持推送文本、图片、视频、文件、图文等类型。
+   *
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/linkedcorp/message/send?access_token=ACCESS_TOKEN
+   * 文章地址:https://work.weixin.qq.com/api/doc/90000/90135/90250
+   * 
+ * + * @param message 要发送的消息对象 + * @return the wx cp message send result + * @throws WxErrorException the wx error exception + */ + WxCpLinkedCorpMessageSendResult sendLinkedCorpMessage(WxCpLinkedCorpMessage message) throws WxErrorException; + + /** + * 发送「学校通知」 + * https://developer.work.weixin.qq.com/document/path/92321 + *

+ * 学校可以通过此接口来给家长发送不同类型的学校通知,来满足多种场景下的学校通知需求。目前支持的消息类型为文本、图片、语音、视频、文件、图文。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/externalcontact/message/send?access_token=ACCESS_TOKEN + * + * @param message 要发送的消息对象 + * @return wx cp school contact message send result + * @throws WxErrorException the wx error exception + */ + WxCpSchoolContactMessageSendResult sendSchoolContactMessage(WxCpSchoolContactMessage message) throws WxErrorException; + + /** + *

+   * 撤回应用消息
+   *
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/message/recall?access_token=ACCESS_TOKEN
+   * 文档地址: https://developer.work.weixin.qq.com/document/path/94867
+   * 
+ * + * @param msgId 消息id + * @throws WxErrorException 异常 + */ + void recall(String msgId) throws WxErrorException; + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java new file mode 100644 index 0000000000..5e8811953f --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpMsgAuditService.java @@ -0,0 +1,234 @@ +package me.chanjar.weixin.cp.api; + +import lombok.NonNull; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.msgaudit.*; + +import java.util.List; +import java.util.function.Consumer; + +/** + * 会话内容存档接口. + * 官方文档:https://developer.work.weixin.qq.com/document/path/91360 + *

+ * 如需自行实现,亦可调用Finance类库函数,进行实现: + * com.tencent.wework.Finance + * + * @author Wang_Wong created on 2022-01-14 + */ +public interface WxCpMsgAuditService { + + /** + * 拉取聊天记录函数 + * + * @param seq 从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0 + * @param limit 一次拉取的消息条数,最大值1000条,超过1000条会返回错误 + * @param proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081,如果没有传null + * @param passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123,如果没有传null + * @param timeout 超时时间,根据实际需要填写 + * @return 返回是否调用成功 chat datas + * @throws Exception the exception + * @deprecated 请使用 {@link #getChatRecords(long, long, String, String, long)} 代替, + * 该方法会将SDK暴露给调用方,容易导致SDK生命周期管理混乱,引发JVM崩溃 + */ + @Deprecated + WxCpChatDatas getChatDatas(long seq, @NonNull long limit, String proxy, String passwd, @NonNull long timeout) throws Exception; + + /** + * 拉取聊天记录函数(推荐使用) + * 该方法不会将SDK暴露给调用方,SDK生命周期由框架自动管理,更加安全 + * + * @param seq 从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0 + * @param limit 一次拉取的消息条数,最大值1000条,超过1000条会返回错误 + * @param proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081,如果没有传null + * @param passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123,如果没有传null + * @param timeout 超时时间,根据实际需要填写 + * @return 返回聊天记录列表,不包含SDK信息 + * @throws Exception the exception + */ + List getChatRecords(long seq, @NonNull long limit, String proxy, String passwd, @NonNull long timeout) throws Exception; + + /** + * 获取解密的聊天数据Model + * + * @param sdk getChatDatas()获取到的sdk + * @param chatData getChatDatas()获取到的聊天数据 + * @param pkcs1 使用什么方式进行解密,1代表使用PKCS1进行解密,2代表PKCS8进行解密 ... + * @return 解密后的聊天数据 decrypt data + * @throws Exception the exception + * @deprecated 请使用 {@link #getDecryptChatData(WxCpChatDatas.WxCpChatData, Integer)} 代替, + * 该方法需要传入SDK,容易导致SDK生命周期管理混乱,引发JVM崩溃 + */ + @Deprecated + WxCpChatModel getDecryptData(@NonNull long sdk, @NonNull WxCpChatDatas.WxCpChatData chatData, + @NonNull Integer pkcs1) throws Exception; + + /** + * 获取解密的聊天数据Model(推荐使用) + * 该方法不需要传入SDK,SDK由框架自动管理,更加安全 + * + * @param chatData 聊天数据 + * @param pkcs1 使用什么方式进行解密,1代表使用PKCS1进行解密,2代表PKCS8进行解密 ... + * @return 解密后的聊天数据 + * @throws Exception the exception + */ + WxCpChatModel getDecryptChatData(@NonNull WxCpChatDatas.WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception; + + /** + * 获取解密的聊天数据明文 + * + * @param sdk getChatDatas()获取到的sdk + * @param chatData getChatDatas()获取到的聊天数据 + * @param pkcs1 使用什么方式进行解密,1代表使用PKCS1进行解密,2代表PKCS8进行解密 ... + * @return 解密后的明文 chat plain text + * @throws Exception the exception + * @deprecated 请使用 {@link #getChatRecordPlainText(WxCpChatDatas.WxCpChatData, Integer)} 代替, + * 该方法需要传入SDK,容易导致SDK生命周期管理混乱,引发JVM崩溃 + */ + @Deprecated + String getChatPlainText(@NonNull long sdk, @NonNull WxCpChatDatas.WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception; + + /** + * 获取解密的聊天数据明文(推荐使用) + * 该方法不需要传入SDK,SDK由框架自动管理,更加安全 + * + * @param chatData 聊天数据 + * @param pkcs1 使用什么方式进行解密,1代表使用PKCS1进行解密,2代表PKCS8进行解密 ... + * @return 解密后的明文 + * @throws Exception the exception + */ + String getChatRecordPlainText(@NonNull WxCpChatDatas.WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception; + + /** + * 获取媒体文件 + * 针对图片、文件等媒体数据,提供sdk接口拉取数据内容。 + *

+ * 注意: + * 根据上面返回的文件类型,拼接好存放文件的绝对路径即可。此时绝对路径写入文件流,来达到获取媒体文件的目的。 + * 详情可以看官方文档,亦可阅读此接口源码。 + * + * @param sdk getChatDatas()获取到的sdk,注意,每次获取的sdk会不一样 + * @param sdkfileid 消息体内容中的sdkfileid信息 + * @param proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081,如果没有传null + * @param passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123,如果没有传null + * @param timeout 超时时间,分片数据需累加到文件存储。单次最大返回512K字节,如果文件比较大,自行设置长一点,比如timeout=10000 + * @param targetFilePath 目标文件绝对路径+实际文件名,比如:/usr/local/file/20220114/474f866b39d10718810d55262af82662.gif + * @throws WxErrorException the wx error exception + * @deprecated 请使用 {@link #downloadMediaFile(String, String, String, long, String)} 代替, + * 该方法需要传入SDK,容易导致SDK生命周期管理混乱,引发JVM崩溃 + */ + @Deprecated + void getMediaFile(@NonNull long sdk, @NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, + @NonNull String targetFilePath) throws WxErrorException; + + /** + * 获取媒体文件(推荐使用) + * 该方法不需要传入SDK,SDK由框架自动管理,更加安全 + * 针对图片、文件等媒体数据,提供sdk接口拉取数据内容。 + *

+ * 注意: + * 根据上面返回的文件类型,拼接好存放文件的绝对路径即可。此时绝对路径写入文件流,来达到获取媒体文件的目的。 + * 详情可以看官方文档,亦可阅读此接口源码。 + * + * @param sdkfileid 消息体内容中的sdkfileid信息 + * @param proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081,如果没有传null + * @param passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123,如果没有传null + * @param timeout 超时时间,分片数据需累加到文件存储。单次最大返回512K字节,如果文件比较大,自行设置长一点,比如timeout=10000 + * @param targetFilePath 目标文件绝对路径+实际文件名,比如:/usr/local/file/20220114/474f866b39d10718810d55262af82662.gif + * @throws WxErrorException the wx error exception + */ + void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, + @NonNull String targetFilePath) throws WxErrorException; + + /** + * 获取媒体文件 传入一个lambda,each所有的数据分片byte[],更加灵活 + * 针对图片、文件等媒体数据,提供sdk接口拉取数据内容。 + * 详情可以看官方文档,亦可阅读此接口源码。 + * + * @param sdk getChatDatas()获取到的sdk,注意,每次获取的sdk会不一样 + * @param sdkfileid 消息体内容中的sdkfileid信息 + * @param proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081,如果没有传null + * @param passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123,如果没有传null + * @param timeout 超时时间,分片数据需累加到文件存储。单次最大返回512K字节,如果文件比较大,自行设置长一点,比如timeout=10000 + * @param action 传入一个lambda,each所有的数据分片 + * @throws WxErrorException the wx error exception + * @deprecated 请使用 {@link #downloadMediaFile(String, String, String, long, Consumer)} 代替, + * 该方法需要传入SDK,容易导致SDK生命周期管理混乱,引发JVM崩溃 + */ + @Deprecated + void getMediaFile(@NonNull long sdk, @NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, + @NonNull Consumer action) throws WxErrorException; + + /** + * 获取媒体文件 传入一个lambda,each所有的数据分片byte[],更加灵活(推荐使用) + * 该方法不需要传入SDK,SDK由框架自动管理,更加安全 + * 针对图片、文件等媒体数据,提供sdk接口拉取数据内容。 + * 详情可以看官方文档,亦可阅读此接口源码。 + * + * @param sdkfileid 消息体内容中的sdkfileid信息 + * @param proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081,如果没有传null + * @param passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123,如果没有传null + * @param timeout 超时时间,分片数据需累加到文件存储。单次最大返回512K字节,如果文件比较大,自行设置长一点,比如timeout=10000 + * @param action 传入一个lambda,each所有的数据分片 + * @throws WxErrorException the wx error exception + */ + void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, + @NonNull Consumer action) throws WxErrorException; + + /** + * 获取会话内容存档开启成员列表 + * 企业可通过此接口,获取企业开启会话内容存档的成员列表 + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/msgaudit/get_permit_user_list?access_token=ACCESS_TOKEN + * + * @param type 拉取对应版本的开启成员列表。1表示办公版;2表示服务版;3表示企业版。非必填,不填写的时候返回全量成员列表。 + * @return permit user list + * @throws WxErrorException the wx error exception + */ + List getPermitUserList(Integer type) throws WxErrorException; + + /** + * 获取会话内容存档内部群信息 + * 企业可通过此接口,获取会话内容存档本企业的内部群信息,包括群名称、群主id、公告、群创建时间以及所有群成员的id与加入时间。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/msgaudit/groupchat/get?access_token=ACCESS_TOKEN + * + * @param roomid 待查询的群id + * @return group chat + * @throws WxErrorException the wx error exception + */ + WxCpGroupChat getGroupChat(@NonNull String roomid) throws WxErrorException; + + /** + * 获取会话同意情况 + * 企业可通过下述接口,获取会话中外部成员的同意情况 + *

+ * 单聊请求地址:https://qyapi.weixin.qq.com/cgi-bin/msgaudit/check_single_agree?access_token=ACCESS_TOKEN + *

+ * 请求方式:POST(HTTPS) + * + * @param checkAgreeRequest 待查询的会话信息 + * @return wx cp agree info + * @throws WxErrorException the wx error exception + */ + WxCpAgreeInfo checkSingleAgree(@NonNull WxCpCheckAgreeRequest checkAgreeRequest) throws WxErrorException; + + /** + * 关闭当前线程持有的SDK,释放本地资源。 + *

+ * 在线程池场景下,任务结束后必须在 finally 块中调用此方法,防止SDK实例随线程复用而泄漏。 + * 独立线程或一次性任务也建议调用,以主动释放原生资源。 + */ + void closeThreadLocalSdk(); + + /** + * 关闭所有会话存档SDK实例,释放全部原生资源。 + *

+ * 适用于应用关闭阶段(如 Spring Bean 销毁阶段 {@code @PreDestroy} 或 Shutdown Hook)。 + * 调用后,所有线程的SDK均不可再使用。 + */ + void closeAllSdks(); + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOAuth2Service.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOAuth2Service.java index 7c42ea63fc..1824196720 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOAuth2Service.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOAuth2Service.java @@ -3,12 +3,15 @@ import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.cp.bean.WxCpOauth2UserInfo; import me.chanjar.weixin.cp.bean.WxCpUserDetail; +import me.chanjar.weixin.cp.bean.workbench.WxCpSecondVerificationInfo; /** *

  * OAuth2相关管理接口.
  *  Created by BinaryWang on 2017/6/24.
  * 
+ *

+ * 文档1:https://developer.work.weixin.qq.com/document/path/91856 * * @author Binary Wang */ @@ -20,7 +23,7 @@ public interface WxCpOAuth2Service { * * * @param state 状态码 - * @return url + * @return url string */ String buildAuthorizationUrl(String state); @@ -32,7 +35,7 @@ public interface WxCpOAuth2Service { * * @param redirectUri 跳转链接地址 * @param state 状态码 - * @return url + * @return url string */ String buildAuthorizationUrl(String redirectUri, String state); @@ -45,7 +48,7 @@ public interface WxCpOAuth2Service { * @param redirectUri 跳转链接地址 * @param state 状态码 * @param scope 取值参考me.chanjar.weixin.common.api.WxConsts.OAuth2Scope类 - * @return url + * @return url string */ String buildAuthorizationUrl(String redirectUri, String state, String scope); @@ -59,9 +62,9 @@ public interface WxCpOAuth2Service { * * * @param code 微信oauth授权返回的代码 - * @return WxCpOauth2UserInfo + * @return WxCpOauth2UserInfo user info * @throws WxErrorException 异常 - * @see #getUserInfo(Integer, String) + * @see #getUserInfo(Integer, String) #getUserInfo(Integer, String) */ WxCpOauth2UserInfo getUserInfo(String code) throws WxErrorException; @@ -78,27 +81,75 @@ public interface WxCpOAuth2Service { * * @param agentId 企业号应用的id * @param code 通过成员授权获取到的code,最大为512字节。每次成员授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。 - * @return WxCpOauth2UserInfo + * @return WxCpOauth2UserInfo user info * @throws WxErrorException 异常 - * @see #getUserInfo(String) + * @see #getUserInfo(String) #getUserInfo(String) */ WxCpOauth2UserInfo getUserInfo(Integer agentId, String code) throws WxErrorException; /** + * 获取家校访问用户身份 + * 该接口用于根据code获取家长或者学生信息 *

-   * 使用user_ticket获取成员详情.
+   * 请求方式:GET(HTTPS)
+   * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/getuserinfo?access_token=ACCESS_TOKEN&code=CODE}
+   * 
+ * + * @param code the code + * @return school user info + * @throws WxErrorException the wx error exception + */ + WxCpOauth2UserInfo getSchoolUserInfo(String code) throws WxErrorException; + + /** + *
+   * 使用user_ticket获取成员详情
    *
-   * 文档地址:https://work.weixin.qq.com/api/doc#10028/%E4%BD%BF%E7%94%A8user_ticket%E8%8E%B7%E5%8F%96%E6%88%90%E5%91%98%E8%AF%A6%E6%83%85
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/95833
    * 请求方式:POST(HTTPS)
-   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/getuserdetail?access_token=ACCESS_TOKEN
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/auth/getuserdetail?access_token=ACCESS_TOKEN
    *
-   * 权限说明:
-   * 需要有对应应用的使用权限,且成员必须在授权应用的可见范围内。
+   * 注意: 原/cgi-bin/user/getuserdetail接口的url已变更为/cgi-bin/auth/getuserdetail,旧接口暂时还可以使用,但建议使用新接口
+   *
+   * 权限说明:需要有对应应用的使用权限,且成员必须在授权应用的可见范围内。
+   * 适用范围:企业内部开发、服务商代开发
    * 
* * @param userTicket 成员票据 - * @return WxCpUserDetail + * @return WxCpUserDetail user detail * @throws WxErrorException 异常 */ WxCpUserDetail getUserDetail(String userTicket) throws WxErrorException; + + /** + *
+   * 获取用户登录身份
+   * {@code https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=ACCESS_TOKEN&code=CODE}
+   * 该接口可使用用户登录成功颁发的code来获取成员信息,适用于自建应用与代开发应用
+   *
+   * 注意: 旧的/user/getuserinfo 接口的url已变更为auth/getuserinfo,不过旧接口依旧可以使用,建议是关注新接口即可
+   *
+   * 适用范围:身份验证中网页授权开发和企业微信Web登录的获取用户登录身份
+   * 
+ * + * @param code 通过成员授权获取到的code,最大为512字节。每次成员授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期。 + * @return WxCpOauth2UserInfo user info + * @throws WxErrorException 异常 + * @see #getUserInfo(Integer, String) #getUserInfo(Integer, String) + */ + WxCpOauth2UserInfo getAuthUserInfo(String code) throws WxErrorException; + + /** + * 获取用户二次验证信息 + *
+   * api: https://qyapi.weixin.qq.com/cgi-bin/auth/get_tfa_info?access_token=ACCESS_TOKEN
+   * 权限说明:仅『通讯录同步』或者自建应用可调用,如用自建应用调用,用户需要在二次验证范围和应用可见范围内。
+   * 并发限制:20
+   * 
+ * + * @param code 用户进入二次验证页面时,企业微信颁发的code,每次成员授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期 + * @return 二次验证授权码,开发者可以调用通过二次验证接口,解锁企业微信终端.tfa_code有效期五分钟,且只能使用一次。 + * @throws WxErrorException 微信错误异常 + */ + WxCpSecondVerificationInfo getTfaInfo(String code) throws WxErrorException; } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaAgentService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaAgentService.java new file mode 100644 index 0000000000..6b8b98877b --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaAgentService.java @@ -0,0 +1,28 @@ +package me.chanjar.weixin.cp.api; + +import lombok.NonNull; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.oa.selfagent.WxCpOpenApprovalData; + +/** + * 企业微信自建应用接口. + * https://developer.work.weixin.qq.com/document/path/90269 + * + * @author Wang_Wong created on 2022-04-06 + */ +public interface WxCpOaAgentService { + + /** + * 查询第三方应用审批申请当前状态 + * 开发者也可主动查询审批单的当前审批状态。 + *

+ * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/corp/getopenapprovaldata?access_token=ACCESS_TOKEN + * + * @param thirdNo the third no + * @return open approval data + * @throws WxErrorException the wx error exception + */ + WxCpOpenApprovalData getOpenApprovalData(@NonNull String thirdNo) throws WxErrorException; + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaCalendarService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaCalendarService.java new file mode 100644 index 0000000000..50d5e8d946 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaCalendarService.java @@ -0,0 +1,82 @@ +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.oa.calendar.WxCpOaCalendar; + +import java.util.List; + +/** + * 企业微信日历接口. + * + * @author Binary Wang created on 2020-09-20 + */ +public interface WxCpOaCalendarService { + /** + * 创建日历. + *

+   * 该接口用于通过应用在企业内创建一个日历。
+   * 注: 企业微信需要更新到3.0.2及以上版本
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/calendar/add?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/92618
+   * 
+ * + * @param calendar 日历对象 + * @return 日历ID string + * @throws WxErrorException . + */ + String add(WxCpOaCalendar calendar) throws WxErrorException; + + /** + * 更新日历. + *
+   * 该接口用于修改指定日历的信息。
+   * 注意,更新操作是覆盖式,而不是增量式
+   * 企业微信需要更新到3.0.2及以上版本
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/calendar/update?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/92619
+   * 
+ * + * @param calendar 日历对象 + * @throws WxErrorException . + */ + void update(WxCpOaCalendar calendar) throws WxErrorException; + + /** + * 获取日历. + *
+   * 该接口用于获取应用在企业内创建的日历信息。
+   *
+   * 注: 企业微信需要更新到3.0.2及以上版本
+   *
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/calendar/get?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/92621
+   * 
+ * + * @param calIds 日历id列表 + * @return 日历对象列表 list + * @throws WxErrorException . + */ + List get(List calIds) throws WxErrorException; + + /** + * 删除日历. + *
+   * 该接口用于删除指定日历。
+   * 注: 企业微信需要更新到3.0.2及以上版本
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/calendar/del?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/92620
+   * 
+ * + * @param calId 日历id + * @throws WxErrorException . + */ + void delete(String calId) throws WxErrorException; +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaMailService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaMailService.java new file mode 100644 index 0000000000..07786080fd --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaMailService.java @@ -0,0 +1,57 @@ +package me.chanjar.weixin.cp.api; + +import lombok.NonNull; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; +import me.chanjar.weixin.cp.bean.oa.mail.WxCpMailCommonSendRequest; +import me.chanjar.weixin.cp.bean.oa.mail.WxCpMailMeetingSendRequest; +import me.chanjar.weixin.cp.bean.oa.mail.WxCpMailScheduleSendRequest; + +/** + * 企业微信y邮件相关接口. + * 邮件 + * + * @author Hugo + */ +public interface WxCpOaMailService { + + /** + * 发送普通邮件 + * 应用可以通过该接口发送普通邮件,支持附件能力。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param request 发送普通邮件请求参数 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp mailCommonSend(@NonNull WxCpMailCommonSendRequest request) throws WxErrorException; + + /** + * 发送日程邮件 + * 应用可以通过该接口发送日程邮件。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param request 发送日程邮件请求参数 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp mailScheduleSend(@NonNull WxCpMailScheduleSendRequest request) throws WxErrorException; + + /** + * 发送会议邮件 + * 应用可以通过该接口发送会议邮件。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param request 发送会议邮件请求参数 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp mailMeetingSend(@NonNull WxCpMailMeetingSendRequest request) throws WxErrorException; + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaMeetingRoomService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaMeetingRoomService.java new file mode 100644 index 0000000000..cc039fd9f5 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaMeetingRoomService.java @@ -0,0 +1,173 @@ +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.oa.meetingroom.*; + +import java.util.List; + +/** + * 企业微信会议室接口. + * + * @author lm93129 created on 2022年8月12日22:33:36 + */ +public interface WxCpOaMeetingRoomService { + /** + * 创建会议室. + *

+   * 该接口用于通过应用在企业内创建一个会议室。
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/add?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/93619
+   * 
+ * + * @param meetingRoom 会议室对象 + * @return 会议室ID string + * @throws WxErrorException . + */ + String addMeetingRoom(WxCpOaMeetingRoom meetingRoom) throws WxErrorException; + + /** + * 查询会议室. + *
+   * 该接口用于通过应用在企业内查询会议室列表。
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/list?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/93619
+   * 
+ * + * @param meetingRoomRequest 会议室查询对象 + * @return 会议室ID list + * @throws WxErrorException . + */ + List listMeetingRoom(WxCpOaMeetingRoom meetingRoomRequest) throws WxErrorException; + + /** + * 编辑会议室. + *
+   * 该接口用于通过应用在企业内编辑会议室。
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/edit?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/93619
+   * 
+ * + * @param meetingRoom 会议室对象 + * @throws WxErrorException . + */ + void editMeetingRoom(WxCpOaMeetingRoom meetingRoom) throws WxErrorException; + + /** + * 删除会议室. + *
+   * 企业可通过此接口删除指定的会议室。
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/del?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/93619
+   * 
+ * + * @param meetingRoomId 会议室ID + * @throws WxErrorException . + */ + void deleteMeetingRoom(Integer meetingRoomId) throws WxErrorException; + + /** + * 查询会议室的预定信息. + *
+   * 企业可通过此接口查询相关会议室在指定时间段的预定情况,如是否已被预定,预定者的userid等信息,不支持跨天查询。
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/get_booking_info?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/93620
+   * 
+ * + * @param wxCpOaMeetingRoomBookingInfoRequest 会议室预定信息查询对象 + * @return 会议室预定信息 + * @throws WxErrorException . + */ + WxCpOaMeetingRoomBookingInfoResult getMeetingRoomBookingInfo(WxCpOaMeetingRoomBookingInfoRequest wxCpOaMeetingRoomBookingInfoRequest) throws WxErrorException; + + /** + * 预定会议室. + *
+   * 企业可通过此接口预定会议室并自动关联日程。
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/book?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/93620
+   * 
+ * + * @param wxCpOaMeetingRoomBookRequest 会议室预定对象 + * @return 预定结果 + * @throws WxErrorException . + */ + WxCpOaMeetingRoomBookResult bookingMeetingRoom(WxCpOaMeetingRoomBookRequest wxCpOaMeetingRoomBookRequest) throws WxErrorException; + + /** + * 通过日程预定会议室. + *
+   * 企业可通过此接口为指定日程预定会议室,支持重复日程预定。
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/book_by_schedule?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/93620
+   * 
+ * + * @param wxCpOaMeetingRoomBookByScheduleRequest 会议室预定对象 + * @return 预定结果 + * @throws WxErrorException . + */ + WxCpOaMeetingRoomBookResult bookingMeetingRoomBySchedule(WxCpOaMeetingRoomBookByScheduleRequest wxCpOaMeetingRoomBookByScheduleRequest) throws WxErrorException; + + /** + * 通过会议预定会议室. + *
+   * 企业可通过此接口为指定会议预定会议室,支持重复会议预定。
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/book_by_meeting?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/93620
+   * 
+ * + * @param wxCpOaMeetingRoomBookByMeetingRequest 会议室预定对象 + * @return 预定结果 + * @throws WxErrorException . + */ + WxCpOaMeetingRoomBookResult bookingMeetingRoomByMeeting(WxCpOaMeetingRoomBookByMeetingRequest wxCpOaMeetingRoomBookByMeetingRequest) throws WxErrorException; + + + /** + * 取消预定会议室. + *
+   * 企业可通过此接口取消会议室的预定
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/cancel_book?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/93620
+   * 
+ * + * @param wxCpOaMeetingRoomCancelBookRequest 取消预定会议室对象 + * @throws WxErrorException . + */ + void cancelBookMeetingRoom(WxCpOaMeetingRoomCancelBookRequest wxCpOaMeetingRoomCancelBookRequest) throws WxErrorException; + + + /** + * 根据会议室预定ID查询预定详情. + *
+   * 企业可通过此接口根据预定id查询相关会议室的预定情况
+   * 请求方式: POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/bookinfo/get?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/93620
+   * 
+ * + * @param wxCpOaMeetingRoomBookingInfoByBookingIdRequest 根据会议室预定ID查询预定详情对象 + * @return 预定详情 + * @throws WxErrorException . + */ + WxCpOaMeetingRoomBookingInfoByBookingIdResult getBookingInfoByBookingId(WxCpOaMeetingRoomBookingInfoByBookingIdRequest wxCpOaMeetingRoomBookingInfoByBookingIdRequest) throws WxErrorException; + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaScheduleService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaScheduleService.java new file mode 100644 index 0000000000..70c108a059 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaScheduleService.java @@ -0,0 +1,87 @@ +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.oa.WxCpOaSchedule; + +import java.util.List; + +/** + * 企业微信日程接口. + * 官方文档:https://work.weixin.qq.com/api/doc/90000/90135/93648 + * + * @author Binary Wang created on 2020 -12-25 + */ +public interface WxCpOaScheduleService { + /** + * 创建日程 + *

+ * 该接口用于在日历中创建一个日程。 + *

+ * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/schedule/add?access_token=ACCESS_TOKEN + * + * @param schedule the schedule + * @param agentId 授权方安装的应用agentid。仅旧的第三方多应用套件需要填此参数 + * @return 日程ID string + * @throws WxErrorException the wx error exception + */ + String add(WxCpOaSchedule schedule, Integer agentId) throws WxErrorException; + + /** + * 更新日程 + *

+ * 该接口用于在日历中更新指定的日程。 + *

+ * 注意,更新操作是覆盖式,而不是增量式 + * 不可更新组织者和日程所属日历ID + *

+ * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/schedule/update?access_token=ACCESS_TOKEN + * + * @param schedule the schedule + * @throws WxErrorException the wx error exception + */ + void update(WxCpOaSchedule schedule) throws WxErrorException; + + /** + * 获取日程详情 + *

+ * 该接口用于获取指定的日程详情。 + *

+ * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/schedule/get?access_token=ACCESS_TOKEN + * + * @param scheduleIds the schedule ids + * @return the details + * @throws WxErrorException the wx error exception + */ + List getDetails(List scheduleIds) throws WxErrorException; + + /** + * 取消日程 + * 该接口用于取消指定的日程。 + *

+ * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/schedule/del?access_token=ACCESS_TOKEN + * + * @param scheduleId 日程id + * @throws WxErrorException the wx error exception + */ + void delete(String scheduleId) throws WxErrorException; + + /** + * 获取日历下的日程列表 + * 该接口用于获取指定的日历下的日程列表。 + * 仅可获取应用自己创建的日历下的日程。 + *

+ * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/schedule/get_by_calendar?access_token=ACCESS_TOKEN + * + * @param calId 日历ID + * @param offset 分页,偏移量, 默认为0 + * @param limit 分页,预期请求的数据量,默认为500,取值范围 1 ~ 1000 + * @return the string + * @throws WxErrorException the wx error exception + */ + List listByCalendar(String calId, Integer offset, Integer limit) throws WxErrorException; +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaService.java index c6f90d3e64..3494dcfa4e 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaService.java @@ -1,10 +1,9 @@ package me.chanjar.weixin.cp.api; +import lombok.NonNull; import me.chanjar.weixin.common.error.WxErrorException; -import me.chanjar.weixin.cp.bean.WxCpApprovalDataResult; -import me.chanjar.weixin.cp.bean.WxCpCheckinData; -import me.chanjar.weixin.cp.bean.WxCpCheckinOption; -import me.chanjar.weixin.cp.bean.WxCpDialRecord; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; +import me.chanjar.weixin.cp.bean.oa.*; import java.util.Date; import java.util.List; @@ -12,53 +11,331 @@ /** * 企业微信OA相关接口. * - * @author Element - * @date 2019-04-06 10:52 + * @author Element, Wang_Wong + * @since 2019-04-06 10:52 */ public interface WxCpOaService { + /** + *

提交审批申请
+   * 调试工具
+   * 企业可通过审批应用或自建应用Secret调用本接口,代应用可见范围内员工在企业微信“审批应用”内提交指定类型的审批申请。
+   *
+   * 请求方式:POST(HTTPS)
+   * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent?access_token=ACCESS_TOKEN
+   * 文档地址
+   * 
+ * + * @param request 请求 + * @return 表单提交成功后 ,返回的表单编号 + * @throws WxErrorException . + */ + String apply(WxCpOaApplyEventRequest request) throws WxErrorException; + /** *
    *  获取打卡数据
-   *  API doc : https://work.weixin.qq.com/api/doc#90000/90135/90262
+   *  文档地址
    * 
* * @param openCheckinDataType 打卡类型。1:上下班打卡;2:外出打卡;3:全部打卡 * @param startTime 获取打卡记录的开始时间 * @param endTime 获取打卡记录的结束时间 * @param userIdList 需要获取打卡记录的用户列表 - * @return 打卡数据列表 + * @return 打卡数据列表 checkin data * @throws WxErrorException 异常 */ - List getCheckinData(Integer openCheckinDataType, Date startTime, Date endTime, List userIdList) throws WxErrorException; + List getCheckinData(Integer openCheckinDataType, Date startTime, Date endTime, + List userIdList) throws WxErrorException; /** *
    *   获取打卡规则
-   *   API doc : https://work.weixin.qq.com/api/doc#90000/90135/90263
+   *  文档地址
    * 
* * @param datetime 需要获取规则的当天日期 * @param userIdList 需要获取打卡规则的用户列表 - * @return 打卡规则列表 - * @throws WxErrorException 异常 + * @return 打卡规则列表 checkin option + * @throws WxErrorException . */ List getCheckinOption(Date datetime, List userIdList) throws WxErrorException; + /** *
-   *   获取审批数据
-   *   通过本接口来获取公司一段时间内的审批记录。一次拉取调用最多拉取10000个审批记录,可以通过多次拉取的方式来满足需求,但调用频率不可超过600次/分。
-   *   API doc : https://work.weixin.qq.com/api/doc#90000/90135/91530
+   *   获取企业所有打卡规则
+   * 文档地址
    * 
* - * @param startTime 获取审批记录的开始时间 - * @param endTime 获取审批记录的结束时间 - * @param nextSpnum 第一个拉取的审批单号,不填从该时间段的第一个审批单拉取 - * @throws WxErrorException 异常 + * @return 打卡规则列表 crop checkin option + * @throws WxErrorException the wx error exception + */ + List getCropCheckinOption() throws WxErrorException; + + /** + *
+   *
+   * 批量获取审批单号
+   *
+   * 审批应用及有权限的自建应用,可通过Secret调用本接口,以获取企业一段时间内企业微信“审批应用”单据的审批编号,支持按模板类型、申请人、部门、申请单审批状态等条件筛选。
+   * 自建应用调用此接口,需在“管理后台-应用管理-审批-API-审批数据权限”中,授权应用允许提交审批单据。
+   *
+   * 一次拉取调用最多拉取100个审批记录,可以通过多次拉取的方式来满足需求,但调用频率不可超过600次/分。
+   *
+   * 文档地址
+   * 
+ * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param cursor 分页查询游标,默认为0,后续使用返回的next_cursor进行分页拉取 + * @param size 一次请求拉取审批单数量,默认值为100,上限值为100 + * @param filters 筛选条件,可对批量拉取的审批申请设置约束条件,支持设置多个条件,nullable + * @return WxCpApprovalInfo approval info + * @throws WxErrorException . + */ + @Deprecated + WxCpApprovalInfo getApprovalInfo(@NonNull Date startTime, @NonNull Date endTime, Integer cursor, Integer size, + List filters) throws WxErrorException; + + /** + * short method + * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return WxCpApprovalInfo approval info + * @throws WxErrorException . + * @see me.chanjar.weixin.cp.api.WxCpOaService#getApprovalInfo me.chanjar.weixin.cp.api + * .WxCpOaService#getApprovalInfome.chanjar.weixin.cp.api.WxCpOaService#getApprovalInfo + */ + @Deprecated + WxCpApprovalInfo getApprovalInfo(@NonNull Date startTime, @NonNull Date endTime) throws WxErrorException; + + + /** + *
+   *
+   * 批量获取审批单号
+   *
+   * 审批应用及有权限的自建应用,可通过Secret调用本接口,以获取企业一段时间内企业微信“审批应用”单据的审批编号,支持按模板类型、申请人、部门、申请单审批状态等条件筛选。
+   * 自建应用调用此接口,需在“管理后台-应用管理-审批-API-审批数据权限”中,授权应用允许提交审批单据。
+   *
+   * 一次拉取调用最多拉取100个审批记录,可以通过多次拉取的方式来满足需求,但调用频率不可超过600次/分。
+   *
+   * 文档地址
+   *
+   * 1 接口频率限制 600次/分钟
+   * 2 请求的参数endtime需要大于startime, 起始时间跨度不能超过31天;
+   * 3 老的分页游标字段cursor和next_cursor待废弃,请开发者使用新字段new_cursor和new_next_cursor。
+   * 
+ * + * @param startTime 开始时间 + * @param endTime 结束时间 + * @param newCursor 分页查询游标,默认为0,后续使用返回的next_cursor进行分页拉取 + * @param size 一次请求拉取审批单数量,默认值为100,上限值为100 + * @param filters 筛选条件,可对批量拉取的审批申请设置约束条件,支持设置多个条件,nullable + * @return WxCpApprovalInfo approval info + * @throws WxErrorException . + */ + WxCpApprovalInfo getApprovalInfo(@NonNull Date startTime, @NonNull Date endTime, String newCursor, Integer size, + List filters) throws WxErrorException; + + + /** + *
+   *   获取审批申请详情
+   *
+   *   企业可通过审批应用或自建应用Secret调用本接口,根据审批单号查询企业微信“审批应用”的审批申请详情。
+   *
+   *  文档地址
+   * 
+ * + * @param spNo 审批单编号。 + * @return WxCpApprovaldetail approval detail + * @throws WxErrorException . + */ + WxCpApprovalDetailResult getApprovalDetail(@NonNull String spNo) throws WxErrorException; + + + /** + * 获取企业假期管理配置 + * 企业可通过审批应用或自建应用Secret调用本接口,获取可见范围内员工的“假期管理”配置,包括:各个假期的id、名称、请假单位、时长计算方式、发放规则等。 + * 第三方应用可获取应用可见范围内员工的“假期管理”配置,包括:各个假期的id、名称、请假单位、时长计算方式、发放规则等。 + *

+ * 请求方式:GET(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/oa/vacation/getcorpconf?access_token=ACCESS_TOKEN + * + * @return corp conf + * @throws WxErrorException the wx error exception + */ + WxCpCorpConfInfo getCorpConf() throws WxErrorException; + + + /** + * 获取成员假期余额 + * 企业可通过审批应用或自建应用Secret调用本接口,获取可见范围内各个员工的假期余额数据。 + * 第三方应用可获取应用可见范围内各个员工的假期余额数据。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/oa/vacation/getuservacationquota?access_token=ACCESS_TOKEN + * + * @param userId 需要获取假期余额的成员的userid + * @return user vacation quota + * @throws WxErrorException the wx error exception + */ + WxCpUserVacationQuota getUserVacationQuota(@NonNull String userId) throws WxErrorException; + + + /** + * 获取审批数据(旧) + * 提示:推荐使用新接口“批量获取审批单号”及“获取审批申请详情”,此接口后续将不再维护、逐步下线。 + * 通过本接口来获取公司一段时间内的审批记录。一次拉取调用最多拉取100个审批记录,可以通过多次拉取的方式来满足需求,但调用频率不可超过600次/分。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/corp/getapprovaldata?access_token=ACCESS_TOKEN + * + * @param startTime 获取审批记录的开始时间。Unix时间戳 + * @param endTime 获取审批记录的结束时间。Unix时间戳 + * @param nextSpNum 第一个拉取的审批单号,不填从该时间段的第一个审批单拉取 + * @return approval data + * @throws WxErrorException the wx error exception + */ + WxCpGetApprovalData getApprovalData(@NonNull Long startTime, @NonNull Long endTime, Long nextSpNum) throws WxErrorException; + + + /** + * 修改成员假期余额 + * 企业可通过审批应用或自建应用Secret调用本接口,修改可见范围内员工的“假期余额”。 + * 第三方应用可通过应本接口修改应用可见范围内指定员工的“假期余额”。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/oa/vacation/setoneuserquota?access_token=ACCESS_TOKEN + * + * @param userId 需要修改假期余额的成员的userid + * @param vacationId 假期id + * @param leftDuration 设置的假期余额,单位为秒,不能大于1000天或24000小时,当假期时间刻度为按小时请假时,必须为360整倍数,即0.1小时整倍数,按天请假时,必须为8640整倍数,即0.1天整倍数 + * @param timeAttr 假期时间刻度:0-按天请假;1-按小时请假 + * @param remarks 修改备注,用于显示在假期余额的修改记录当中,可对修改行为作说明,不超过200字符 + * @return one user quota + * @throws WxErrorException the wx error exception */ - WxCpApprovalDataResult getApprovalData(Date startTime, Date endTime, Long nextSpnum) throws WxErrorException; + WxCpBaseResp setOneUserQuota(@NonNull String userId, @NonNull Integer vacationId, @NonNull Integer leftDuration, + @NonNull Integer timeAttr, String remarks) throws WxErrorException; - List getDialRecord(Date startTime, Date endTime, Integer offset, Integer limit) throws WxErrorException; + /** + * 获取公费电话拨打记录 + * + * @param startTime 查询的起始时间戳 + * @param endTime 查询的结束时间戳 + * @param offset 分页查询的偏移量 + * @param limit 分页查询的每页大小,默认为100条,如该参数大于100则按100处理 + * @return . dial record + * @throws WxErrorException . + */ + List getDialRecord(Date startTime, Date endTime, Integer offset, + Integer limit) throws WxErrorException; + + /** + * 获取审批模板详情 + * + * @param templateId 模板ID + * @return . template detail + * @throws WxErrorException . + */ + WxCpOaApprovalTemplateResult getTemplateDetail(@NonNull String templateId) throws WxErrorException; + + /** + * 创建审批模板 + *
+ * 可以调用此接口创建审批模板。创建新模板后,管理后台及审批应用内将生成对应模板,并生效默认流程和规则配置。 + *

+   *  文档地址: https://developer.work.weixin.qq.com/document/path/97437
+   *  权限说明
+   * • 仅『审批』系统应用、自建应用和代开发自建应用可调用。
+   * 
+ * + * @param cpTemplate cpTemplate + * @return templateId + * @throws WxErrorException . + */ + String createOaApprovalTemplate(WxCpOaApprovalTemplate cpTemplate) throws WxErrorException; + + /** + * 更新审批模板 + *
+ * 可调用本接口更新审批模板。更新模板后,管理后台及审批应用内将更新原模板的内容,已配置的审批流程和规则不变。 + *
+   *  文档地址: https://developer.work.weixin.qq.com/document/path/97438
+   *  权限说明
+   * • 仅『审批』系统应用,自建应用和代开发自建应用可调用
+   * • 所有应用都可以通过本接口更新自己的模板
+   * • 『审批』系统应用可以修改管理员手动创建的模板
+   * • 自建应用和代开发自建应用不可通过本接口更新其他应用创建的模板
+   * 
+ * + * @param wxCpTemplate wxCpTemplate + * @throws WxErrorException . + */ + void updateOaApprovalTemplate(WxCpOaApprovalTemplate wxCpTemplate) throws WxErrorException; + + /** + * 获取打卡日报数据 + * + * @param startTime 获取日报的开始时间 + * @param endTime 获取日报的结束时间 + * @param userIdList 获取日报的userid列表 + * @return 日报数据列表 checkin day data + * @throws WxErrorException the wx error exception + */ + List getCheckinDayData(Date startTime, Date endTime, List userIdList) throws WxErrorException; + + + /** + * 获取打卡月报数据 + * + * @param startTime 获取月报的开始时间 + * @param endTime 获取月报的结束时间 + * @param userIdList 获取月报的userid列表 + * @return 月报数据列表 checkin month data + * @throws WxErrorException the wx error exception + */ + List getCheckinMonthData(Date startTime, Date endTime, List userIdList) throws WxErrorException; + + /** + * 获取打卡人员排班信息 + * + * @param startTime 获取排班信息的开始时间。Unix时间戳 + * @param endTime 获取排班信息的结束时间。Unix时间戳(与starttime跨度不超过一个月) + * @param userIdList 需要获取排班信息的用户列表(不超过100个) + * @return 排班表信息 checkin schedule list + * @throws WxErrorException the wx error exception + */ + List getCheckinScheduleList(Date startTime, Date endTime, List userIdList) throws WxErrorException; + + + /** + * 为打卡人员排班 + * + * @param wxCpSetCheckinSchedule the wx cp set checkin schedule + * @throws WxErrorException the wx error exception + */ + void setCheckinScheduleList(WxCpSetCheckinSchedule wxCpSetCheckinSchedule) throws WxErrorException; + + /** + *
+   * 录入打卡人员人脸信息
+   * 企业可通过打卡应用Secret调用本接口,为企业打卡人员录入人脸信息,人脸信息仅用于人脸打卡。
+   * 上传图片大小限制:图片数据不超过1M
+   * 请求方式:POST(HTTPS)
+   * 请求地址:
+   * https://qyapi.weixin.qq.com/cgi-bin/checkin/addcheckinuserface?access_token=ACCESS_TOKEN
+   * 文档地址:
+   * https://developer.work.weixin.qq.com/document/path/93378
+   * 
+ * @param userId 需要录入的用户id + * @param userFace 需要录入的人脸图片数据,需要将图片数据base64处理后填入,对已录入的人脸会进行更新处理 + * @throws WxErrorException the wx error exception + */ + void addCheckInUserFace(String userId, String userFace) throws WxErrorException; } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaWeDocService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaWeDocService.java new file mode 100644 index 0000000000..712bc2a89c --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaWeDocService.java @@ -0,0 +1,543 @@ +package me.chanjar.weixin.cp.api; + +import lombok.NonNull; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; +import me.chanjar.weixin.cp.bean.oa.doc.*; + +import java.io.File; +import java.util.Collections; +import java.util.List; + +/** + * 企业微信文档相关接口. + * 文档 + * + * @author Hugo + */ +public interface WxCpOaWeDocService { + + /** + * 新建文档 + * 该接口用于新建文档和表格,新建收集表可前往 收集表管理 查看。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/create_doc?access_token=ACCESS_TOKEN + * + * @param request 新建文档对应请求参数 + * @return url:新建文档的访问链接,docid:新建文档的docid + * @throws WxErrorException the wx error exception + */ + WxCpDocCreateData docCreate(@NonNull WxCpDocCreateRequest request) throws WxErrorException; + + /** + * 重命名文档/收集表 + * 该接口用于对指定文档/收集表进行重命名。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/rename_doc?access_token=ACCESS_TOKEN + * + * @param request 重命名文档/收集表 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp docRename(@NonNull WxCpDocRenameRequest request) throws WxErrorException; + + /** + * 删除文档/收集表 + * 该接口用于删除指定文档/收集表。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/del_doc?access_token=ACCESS_TOKEN + * + * @param docId 文档docid(docid、formid只能填其中一个) + * @param formId 收集表id(docid、formid只能填其中一个) + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp docDelete(String docId, String formId) throws WxErrorException; + + /** + * 获取文档基础信息 + * 该接口用于获取指定文档的基础信息。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/get_doc_base_info?access_token=ACCESS_TOKEN + * + * @param docId 文档docid + * @return wx cp doc info + * @throws WxErrorException the wx error exception + */ + WxCpDocInfo docInfo(@NonNull String docId) throws WxErrorException; + + /** + * 分享文档 + * 该接口用于获取文档的分享链接。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/doc_share?access_token=ACCESS_TOKEN + * + * @param docId 文档docid + * @return url 文档分享链接 + * @throws WxErrorException the wx error exception + */ + WxCpDocShare docShare(@NonNull String docId) throws WxErrorException; + + /** + * 分享文档/收集表 + * 该接口用于获取文档或收集表的分享链接。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/doc_share?access_token=ACCESS_TOKEN + * + * @param request 分享请求,docid/formid 二选一 + * @return url 文档分享链接 + * @throws WxErrorException the wx error exception + */ + WxCpDocShare docShare(@NonNull WxCpDocShareRequest request) throws WxErrorException; + + /** + * 获取文档权限信息 + * 该接口用于获取文档、表格、智能表格的查看规则、文档通知范围及权限、安全设置信息。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/doc_get_auth?access_token=ACCESS_TOKEN + * + * @param docId 文档docid + * @return 文档权限信息 + * @throws WxErrorException the wx error exception + */ + WxCpDocAuthInfo docGetAuth(@NonNull String docId) throws WxErrorException; + + /** + * 修改文档查看规则 + * 该接口用于修改文档、表格、智能表格查看规则。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/mod_doc_join_rule?access_token=ACCESS_TOKEN + * + * @param request 修改文档查看规则请求 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp docModifyJoinRule(@NonNull WxCpDocModifyJoinRuleRequest request) throws WxErrorException; + + /** + * 修改文档通知范围及权限 + * 该接口用于修改文档、表格、智能表格通知范围列表。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/mod_doc_member?access_token=ACCESS_TOKEN + * + * @param request 修改文档通知范围及权限请求 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp docModifyMember(@NonNull WxCpDocModifyMemberRequest request) throws WxErrorException; + + /** + * 修改文档安全设置 + * 该接口用于修改文档、表格、智能表格的安全设置。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/mod_doc_safty_setting?access_token=ACCESS_TOKEN + * + * @param request 修改文档安全设置请求 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp docModifySafetySetting( + @NonNull WxCpDocModifySafetySettingRequest request + ) throws WxErrorException; + + /** + * @deprecated Use {@link #docModifySafetySetting(WxCpDocModifySafetySettingRequest)} instead. + */ + @Deprecated + default WxCpBaseResp docModifySaftySetting( + @NonNull WxCpDocModifySaftySettingRequest request + ) throws WxErrorException { + WxCpDocModifySafetySettingRequest newReq = + WxCpDocModifySafetySettingRequest.builder() + .docId(request.getDocId()) + .enableReadonlyCopy(request.getEnableReadonlyCopy()) + .watermark(request.getWatermark()) + .build(); + return docModifySafetySetting(newReq); + } + + /** + * 编辑表格内容 + * 该接口可以对一个在线表格批量执行多个更新操作 + *

+ * 注意: + * 1.批量更新请求中的各个操作会逐个按顺序执行,直到全部执行完成则请求返回,或者其中一个操作报错则不再继续执行后续的操作 + * 2.每一个更新操作在执行之前都会做请求校验(包括权限校验、参数校验等等),如果校验未通过则该更新操作会报错并返回,不再执行后续操作 + * 3.单次批量更新请求的操作数量 <= 5 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/spreadsheet/batch_update?access_token=ACCESS_TOKEN + * + * @param request 编辑表格内容请求参数 + * @return 编辑表格内容批量更新的响应结果 + * @throws WxErrorException the wx error exception + */ + WxCpDocSheetBatchUpdateResponse docBatchUpdate(@NonNull WxCpDocSheetBatchUpdateRequest request) throws WxErrorException; + + /** + * 获取表格行列信息 + * 该接口用于获取在线表格的工作表、行数、列数等。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/spreadsheet/get_sheet_properties?access_token=ACCESS_TOKEN + * + * @param docId 在线表格的docid + * @return 返回表格行列信息 + * @throws WxErrorException + */ + WxCpDocSheetProperties getSheetProperties(@NonNull String docId) throws WxErrorException; + + + /** + * 本接口用于获取指定范围内的在线表格信息,单次查询的范围大小需满足以下限制: + *

+ * 查询范围行数 <=1000 + * 查询范围列数 <=200 + * 范围内的总单元格数量 <=10000 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/spreadsheet/get_sheet_range_data?access_token=ACCESS_TOKEN + * + * @param request 获取指定范围内的在线表格信息请求参数 + * @return 返回指定范围内的在线表格信息 + * @throws WxErrorException + */ + WxCpDocSheetData getSheetRangeData(@NonNull WxCpDocSheetGetDataRequest request) throws WxErrorException; + + /** + * 获取文档数据 + * 该接口用于获取在线文档内容数据。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/get_doc_data?access_token=ACCESS_TOKEN + * + * @param request 获取文档数据请求参数 + * @return 文档内容数据 + * @throws WxErrorException the wx error exception + */ + WxCpDocData docGetData(@NonNull WxCpDocGetDataRequest request) throws WxErrorException; + + /** + * 编辑文档内容 + * 该接口用于编辑在线文档内容。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/mod_doc?access_token=ACCESS_TOKEN + * + * @param request 编辑文档内容请求参数 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp docModify(@NonNull WxCpDocModifyRequest request) throws WxErrorException; + + /** + * 上传文档图片 + * 该接口用于上传在线文档编辑时使用的图片资源。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/upload_doc_image?access_token=ACCESS_TOKEN + * + * @param file 图片文件 + * @return 上传结果 + * @throws WxErrorException the wx error exception + */ + WxCpDocImageUploadResult docUploadImage(@NonNull File file) throws WxErrorException; + + /** + * 添加文档高级功能账号 + * 该接口用于为在线文档添加高级功能账号。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/add_admin?access_token=ACCESS_TOKEN + * + * @param request 文档高级功能账号请求 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp docAddAdmin(@NonNull WxCpDocAdminRequest request) throws WxErrorException; + + /** + * 删除文档高级功能账号 + * 该接口用于删除在线文档的高级功能账号。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/del_admin?access_token=ACCESS_TOKEN + * + * @param request 文档高级功能账号请求 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp docDeleteAdmin(@NonNull WxCpDocAdminRequest request) throws WxErrorException; + + /** + * 获取文档高级功能账号列表 + * 该接口用于获取在线文档的高级功能账号列表。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/get_admin_list?access_token=ACCESS_TOKEN + * + * @param docId 文档 docid + * @return 文档高级功能账号列表 + * @throws WxErrorException the wx error exception + */ + WxCpDocAdminListResult docGetAdminList(@NonNull String docId) throws WxErrorException; + + /** + * 获取智能表格内容权限 + * 该接口用于获取智能表格字段/记录等内容权限信息。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/smartsheet/get_sheet_auth?access_token=ACCESS_TOKEN + * + * @param request 智能表格内容权限请求 + * @return 智能表格内容权限 + * @throws WxErrorException the wx error exception + */ + WxCpDocSmartSheetAuth smartSheetGetAuth(@NonNull WxCpDocSmartSheetAuthRequest request) throws WxErrorException; + + /** + * 修改智能表格内容权限 + * 该接口用于修改智能表格字段/记录等内容权限信息。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/smartsheet/mod_sheet_auth?access_token=ACCESS_TOKEN + * + * @param request 修改智能表格内容权限请求 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp smartSheetModifyAuth(@NonNull WxCpDocSmartSheetModifyAuthRequest request) throws WxErrorException; + + /** + * 获取智能表格工作表信息. + * + * @param request 智能表格请求 + * @return 智能表格工作表信息 + * @throws WxErrorException the wx error exception + */ + WxCpDocSmartSheetResult smartSheetGetSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException; + + /** + * 新增智能表格工作表. + * + * @param request 智能表格请求 + * @return 智能表格工作表信息 + * @throws WxErrorException the wx error exception + */ + WxCpDocSmartSheetResult smartSheetAddSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException; + + /** + * 删除智能表格工作表. + * + * @param request 智能表格请求 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp smartSheetDeleteSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException; + + /** + * 更新智能表格工作表. + * + * @param request 智能表格请求 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp smartSheetUpdateSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException; + + /** + * 获取智能表格视图. + * + * @param request 智能表格请求 + * @return 智能表格视图 + * @throws WxErrorException the wx error exception + */ + WxCpDocSmartSheetResult smartSheetGetViews(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException; + + /** + * 新增智能表格视图. + * + * @param request 智能表格请求 + * @return 智能表格视图 + * @throws WxErrorException the wx error exception + */ + WxCpDocSmartSheetResult smartSheetAddView(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException; + + /** + * 删除智能表格视图. + * + * @param request 智能表格请求 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp smartSheetDeleteViews(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException; + + /** + * 更新智能表格视图. + * + * @param request 智能表格请求 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp smartSheetUpdateView(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException; + + /** + * 获取智能表格字段. + * + * @param request 智能表格请求 + * @return 智能表格字段 + * @throws WxErrorException the wx error exception + */ + WxCpDocSmartSheetResult smartSheetGetFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException; + + /** + * 新增智能表格字段. + * + * @param request 智能表格请求 + * @return 智能表格字段 + * @throws WxErrorException the wx error exception + */ + WxCpDocSmartSheetResult smartSheetAddFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException; + + /** + * 删除智能表格字段. + * + * @param request 智能表格请求 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp smartSheetDeleteFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException; + + /** + * 更新智能表格字段. + * + * @param request 智能表格请求 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp smartSheetUpdateFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException; + + /** + * 获取智能表格记录. + * + * @param request 智能表格请求 + * @return 智能表格记录 + * @throws WxErrorException the wx error exception + */ + WxCpDocSmartSheetResult smartSheetGetRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException; + + /** + * 新增智能表格记录. + * + * @param request 智能表格请求 + * @return 智能表格记录 + * @throws WxErrorException the wx error exception + */ + WxCpDocSmartSheetResult smartSheetAddRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException; + + /** + * 删除智能表格记录. + * + * @param request 智能表格请求 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp smartSheetDeleteRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException; + + /** + * 更新智能表格记录. + * + * @param request 智能表格请求 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp smartSheetUpdateRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException; + + /** + * 创建收集表 + * 该接口用于创建收集表。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/create_collect?access_token=ACCESS_TOKEN + * + * @param request 创建收集表请求 + * @return 创建收集表结果 + * @throws WxErrorException the wx error exception + */ + WxCpFormCreateResult formCreate(@NonNull WxCpFormCreateRequest request) throws WxErrorException; + + /** + * 编辑收集表 + * 该接口用于编辑收集表。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/modify_collect?access_token=ACCESS_TOKEN + * + * @param request 编辑收集表请求 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp formModify(@NonNull WxCpFormModifyRequest request) throws WxErrorException; + + /** + * 获取收集表信息 + * 该接口用于读取收集表的信息。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/get_form_info?access_token=ACCESS_TOKEN + * + * @param formId 收集表id + * @return 收集表信息 + * @throws WxErrorException the wx error exception + */ + WxCpFormInfoResult formInfo(@NonNull String formId) throws WxErrorException; + + /** + * 获取收集表统计信息 + * 该接口用于获取收集表的统计信息、已回答成员列表和未回答成员列表。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/get_form_statistic?access_token=ACCESS_TOKEN + * + * @param requests 收集表统计请求数组 + * @return 收集表统计结果(包含 statistic_list) + * @throws WxErrorException the wx error exception + */ + WxCpFormStatisticResult formStatistic(@NonNull List requests) throws WxErrorException; + + /** + * 单个收集表统计查询的兼容封装,底层仍按官方数组请求发送。 + * + * @param request 收集表统计请求 + * @return 收集表统计信息 + * @throws WxErrorException the wx error exception + */ + default WxCpFormStatistic formStatistic(@NonNull WxCpFormStatisticRequest request) throws WxErrorException { + WxCpFormStatisticResult result = formStatistic(Collections.singletonList(request)); + List list = result == null ? null : result.getStatisticList(); + return list == null || list.isEmpty() ? null : list.get(0); + } + + /** + * 获取收集表答案 + * 该接口用于读取收集表的答案。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/wedoc/get_form_answer?access_token=ACCESS_TOKEN + * + * @param request 收集表答案请求 + * @return 收集表答案 + * @throws WxErrorException the wx error exception + */ + WxCpFormAnswer formAnswer(@NonNull WxCpFormAnswerRequest request) throws WxErrorException; + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaWeDriveService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaWeDriveService.java new file mode 100644 index 0000000000..e7217616b8 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaWeDriveService.java @@ -0,0 +1,292 @@ +package me.chanjar.weixin.cp.api; + +import lombok.NonNull; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; +import me.chanjar.weixin.cp.bean.oa.wedrive.*; + +import java.util.List; + +/** + * 企业微信微盘相关接口. + * ... + * + * @author Wang_Wong created on 2022-04-22 + */ +public interface WxCpOaWeDriveService { + + /** + * 新建空间 + * 该接口用于在微盘内新建空间,可以指定人创建空间。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param request 新建空间对应请求参数 + * @return spaceid (空间id) + * @throws WxErrorException the wx error exception + */ + WxCpSpaceCreateData spaceCreate(@NonNull WxCpSpaceCreateRequest request) throws WxErrorException; + + /** + * 重命名空间 + * 该接口用于重命名已有空间,接收userid参数,以空间管理员身份来重命名。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param request 重命名空间的请求参数 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp spaceRename(@NonNull WxCpSpaceRenameRequest request) throws WxErrorException; + + /** + * 解散空间 + * 该接口用于解散已有空间,需要以空间管理员身份来解散。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param spaceId the space id + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp spaceDismiss(@NonNull String spaceId) throws WxErrorException; + + /** + * 获取空间信息 + * 该接口用于获取空间成员列表、信息、权限等信息。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param spaceId the space id + * @return wx cp space info + * @throws WxErrorException the wx error exception + */ + WxCpSpaceInfo spaceInfo(@NonNull String spaceId) throws WxErrorException; + + /** + * 添加成员/部门 + * 该接口用于对指定空间添加成员/部门,可一次性添加多个。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param request 添加成员/部门请求参数 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp spaceAclAdd(@NonNull WxCpSpaceAclAddRequest request) throws WxErrorException; + + /** + * 移除成员/部门 + * 该接口用于对指定空间移除成员/部门,操作者需要有移除权限。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param request 移除成员/部门请求参数 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp spaceAclDel(@NonNull WxCpSpaceAclDelRequest request) throws WxErrorException; + + /** + * 权限管理 + * 该接口用于修改空间权限,需要传入userid,修改权限范围继承传入用户的权限范围。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param request 权限管理请求参数 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp spaceSetting(@NonNull WxCpSpaceSettingRequest request) throws WxErrorException; + + /** + * 获取邀请链接 + * 该接口用于获取空间邀请分享链接。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param spaceId the space id + * @return wx cp space share + * @throws WxErrorException the wx error exception + */ + WxCpSpaceShare spaceShare(@NonNull String spaceId) throws WxErrorException; + + /** + * 获取文件列表 + * 该接口用于获取指定地址下的文件列表。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param request 获取文件列表请求参数 + * @return wx cp file list + * @throws WxErrorException the wx error exception + */ + WxCpFileList fileList(@NonNull WxCpFileListRequest request) throws WxErrorException; + + /** + * 上传文件 + * 该接口用于向微盘中的指定位置上传文件。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param request 上传文件请求参数 + * @return wx cp file upload + * @throws WxErrorException the wx error exception + */ + WxCpFileUpload fileUpload(@NonNull WxCpFileUploadRequest request) throws WxErrorException; + + /** + * 下载文件 + * 该接口用于下载文件,请求的userid需有下载权限。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param fileId 文件fileid(只支持下载普通文件,不支持下载文件夹或微文档) + * @param selectedTicket 微盘和文件选择器jsapi返回的selectedTicket。若填此参数,则不需要填fileid。 + * @return { + * "errcode": 0, + * "errmsg": "ok", + * "download_url": "DOWNLOAD_URL", + * "cookie_name": "COOKIE_NAME", + * "cookie_value": "COOKIE_VALUE" + * } + * @throws WxErrorException the wx error exception + */ + WxCpFileDownload fileDownload(String fileId, String selectedTicket) throws WxErrorException; + + /** + * 重命名文件 + * 该接口用于对指定文件进行重命名。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param fileId the file id + * @param newName the new name + * @return wx cp file rename + * @throws WxErrorException the wx error exception + */ + WxCpFileRename fileRename(@NonNull String fileId, @NonNull String newName) throws WxErrorException; + + /** + * 新建文件夹/文档 + * 该接口用于在微盘指定位置新建文件夹、文档(更多文档接口能力可见文档API接口说明)。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param spaceId 空间spaceid + * @param fatherId 父目录fileid, 在根目录时为空间spaceid + * @param fileType 文件类型, 1:文件夹 3:文档(文档) 4:文档(表格) + * @param fileName 文件名字(注意:文件名最多填255个字符, 英文算1个, 汉字算2个) + * @return wx cp file create + * @throws WxErrorException the wx error exception + */ + WxCpFileCreate fileCreate(@NonNull String spaceId, @NonNull String fatherId, @NonNull Integer fileType, + @NonNull String fileName) throws WxErrorException; + + /** + * 移动文件 + * 该接口用于将文件移动到指定位置。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param request 移动文件的请求参数 + * @return wx cp file move + * @throws WxErrorException the wx error exception + */ + WxCpFileMove fileMove(@NonNull WxCpFileMoveRequest request) throws WxErrorException; + + /** + * 删除文件 + * 该接口用于删除指定文件。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param fileIds 文件fileid列表 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp fileDelete(@NonNull List fileIds) throws WxErrorException; + + /** + * 文件信息 + * 该接口用于获取指定文件的信息。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param fileId the file id + * @return wx cp file info + * @throws WxErrorException the wx error exception + */ + WxCpFileInfo fileInfo(@NonNull String fileId) throws WxErrorException; + + /** + * 新增指定人 + * 该接口用于对指定文件添加指定人/部门。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param request 新增指定人请求参数 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp fileAclAdd(@NonNull WxCpFileAclAddRequest request) throws WxErrorException; + + /** + * 删除指定人 + * 该接口用于删除指定文件的指定人/部门。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param request 请求参数 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp fileAclDel(@NonNull WxCpFileAclDelRequest request) throws WxErrorException; + + /** + * 分享设置 + * 该接口用于文件的分享设置。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param fileId the file id + * @param authScope the auth scope + * @param auth the auth + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp fileSetting(@NonNull String fileId, @NonNull Integer authScope, Integer auth) throws WxErrorException; + + /** + * 获取分享链接 + * 该接口用于获取文件的分享链接。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param fileId the file id + * @return wx cp file share + * @throws WxErrorException the wx error exception + */ + WxCpFileShare fileShare(@NonNull String fileId) throws WxErrorException; + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolHealthService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolHealthService.java new file mode 100644 index 0000000000..091f242820 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolHealthService.java @@ -0,0 +1,73 @@ +package me.chanjar.weixin.cp.api; + +import lombok.NonNull; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.school.health.WxCpGetHealthReportStat; +import me.chanjar.weixin.cp.bean.school.health.WxCpGetReportAnswer; +import me.chanjar.weixin.cp.bean.school.health.WxCpGetReportJobIds; +import me.chanjar.weixin.cp.bean.school.health.WxCpGetReportJobInfo; + +/** + * 企业微信家校应用 健康上报接口. + * https://developer.work.weixin.qq.com/document/path/93676 + * + * @author Wang_Wong created on : 2022/5/31 9:10 + */ +public interface WxCpSchoolHealthService { + + /** + * 获取健康上报使用统计 + * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/health/get_health_report_stat?access_token=ACCESS_TOKEN + * + * @param date 具体某天的使用统计,最长支持获取30天前数据 + * @return health report stat + * @throws WxErrorException the wx error exception + */ + WxCpGetHealthReportStat getHealthReportStat(@NonNull String date) throws WxErrorException; + + /** + * 获取健康上报任务ID列表 + * 通过此接口可以获取企业当前正在运行的上报任务ID列表。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/health/get_report_jobids?access_token=ACCESS_TOKEN + * + * @param offset 否 分页,偏移量, 默认为0 + * @param limit 否 分页,预期请求的数据量,默认为100,取值范围 1 ~ 100 + * @return report job ids + * @throws WxErrorException the wx error exception + */ + WxCpGetReportJobIds getReportJobIds(Integer offset, Integer limit) throws WxErrorException; + + /** + * 获取健康上报任务详情 + * 通过此接口可以获取指定的健康上报任务详情。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/health/get_report_job_info?access_token=ACCESS_TOKEN + * + * @param jobId 是 任务ID + * @param date 是 具体某天任务详情,仅支持获取最近14天数据 + * @return report job info + * @throws WxErrorException the wx error exception + */ + WxCpGetReportJobInfo getReportJobInfo(@NonNull String jobId, @NonNull String date) throws WxErrorException; + + /** + * 获取用户填写答案 + * 通过此接口可以获取指定的健康上报任务详情。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/health/get_report_answer?access_token=ACCESS_TOKEN + * + * @param jobId the job id + * @param date the date + * @param offset the offset + * @param limit the limit + * @return report answer + * @throws WxErrorException the wx error exception + */ + WxCpGetReportAnswer getReportAnswer(@NonNull String jobId, @NonNull String date, Integer offset, Integer limit) throws WxErrorException; + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolService.java new file mode 100644 index 0000000000..5f1d41c197 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolService.java @@ -0,0 +1,148 @@ +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.living.WxCpLivingResult; +import me.chanjar.weixin.cp.bean.school.*; + +import java.util.List; + +/** + * 企业微信家校应用 复学码相关接口. + * https://developer.work.weixin.qq.com/document/path/93744 + *

+ * 权限说明: + * 仅复学码应用可以调用 + * + * @author Wang_Wong created on : 2022/5/31 9:10 + */ +public interface WxCpSchoolService { + + /** + * 获取老师健康信息 + * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/school/user/get_teacher_customize_health_info?access_token=ACCESS_TOKEN + * + * @param date the date + * @param nextKey the next key + * @param limit the limit + * @return teacher customize health info + * @throws WxErrorException the wx error exception + */ + WxCpCustomizeHealthInfo getTeacherCustomizeHealthInfo(String date, String nextKey, Integer limit) throws WxErrorException; + + /** + * 获取学生健康信息 + * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/school/user/get_student_customize_health_info?access_token=ACCESS_TOKEN + * + * @param date the date + * @param nextKey the next key + * @param limit the limit + * @return student customize health info + * @throws WxErrorException the wx error exception + */ + WxCpCustomizeHealthInfo getStudentCustomizeHealthInfo(String date, String nextKey, Integer limit) throws WxErrorException; + + /** + * 获取师生健康码 + * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/get_health_qrcode?access_token=ACCESS_TOKEN + * + * @param userIds the user ids + * @param type the type + * @return health qr code + * @throws WxErrorException the wx error exception + */ + WxCpResultList getHealthQrCode(List userIds, Integer type) throws WxErrorException; + + /** + * 获取学生付款结果 + * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/school/get_payment_result?access_token=ACCESS_TOKEN + * + * @param paymentId the payment id + * @return payment result + * @throws WxErrorException the wx error exception + */ + WxCpPaymentResult getPaymentResult(String paymentId) throws WxErrorException; + + /** + * 获取订单详情 + * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/school/get_trade?access_token=ACCESS_TOKEN + * + * @param paymentId the payment id + * @param tradeNo the trade no + * @return trade + * @throws WxErrorException the wx error exception + */ + WxCpTrade getTrade(String paymentId, String tradeNo) throws WxErrorException; + + /** + * 获取直播详情 + *

+   * 请求方式:GET(HTTPS)
+   * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/living/get_living_info?access_token=ACCESS_TOKEN&livingid=LIVINGID}
+   * 
+ * + * @param livingId the living id + * @return living info + * @throws WxErrorException the wx error exception + */ + WxCpSchoolLivingInfo getLivingInfo(String livingId) throws WxErrorException; + + /** + * 获取老师直播ID列表 + * 通过此接口可以获取指定老师的所有直播ID + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/living/get_user_all_livingid?access_token=ACCESS_TOKEN + * + * @param userId the user id + * @param cursor the cursor + * @param limit the limit + * @return user all living id + * @throws WxErrorException the wx error exception + */ + WxCpLivingResult.LivingIdResult getUserAllLivingId(String userId, String cursor, Integer limit) throws WxErrorException; + + /** + * 获取观看直播统计 + * 通过该接口可以获取所有观看直播的人员统计 + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/living/get_watch_stat?access_token=ACCESS_TOKEN + * + * @param livingId the living id + * @param nextKey the next key + * @return watch stat + * @throws WxErrorException the wx error exception + */ + WxCpSchoolWatchStat getWatchStat(String livingId, String nextKey) throws WxErrorException; + + /** + * 获取未观看直播统计 + * 通过该接口可以获取未观看直播的学生统计,学生的家长必须是已经关注「学校通知」才会纳入统计范围。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/living/get_unwatch_stat?access_token=ACCESS_TOKEN + * + * @param livingId the living id + * @param nextKey the next key + * @return unwatch stat + * @throws WxErrorException the wx error exception + */ + WxCpSchoolUnwatchStat getUnwatchStat(String livingId, String nextKey) throws WxErrorException; + + /** + * 删除直播回放 + * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/living/delete_replay_data?access_token=ACCESS_TOKEN + * + * @param livingId the living id + * @return wx cp living result + * @throws WxErrorException the wx error exception + */ + WxCpLivingResult deleteReplayData(String livingId) throws WxErrorException; + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolUserService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolUserService.java new file mode 100644 index 0000000000..d004ca8aa5 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolUserService.java @@ -0,0 +1,375 @@ +package me.chanjar.weixin.cp.api; + +import lombok.NonNull; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; +import me.chanjar.weixin.cp.bean.WxCpOauth2UserInfo; +import me.chanjar.weixin.cp.bean.school.user.*; + +import java.util.List; + +/** + * 企业微信家校沟通相关接口. + * https://developer.work.weixin.qq.com/document/path/91638 + * + * @author Wang_Wong created on : 2022/6/18 9:10 + */ +public interface WxCpSchoolUserService { + + /** + * 获取访问用户身份 + * 该接口用于根据code获取成员信息 + *

+   * 请求方式:GET(HTTPS)
+   * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN&code=CODE}
+   * 
+ * + * @param code the code + * @return user info + * @throws WxErrorException the wx error exception + */ + WxCpOauth2UserInfo getUserInfo(@NonNull String code) throws WxErrorException; + + /** + * 获取家校访问用户身份 + * 该接口用于根据code获取家长或者学生信息 + *
+   * 请求方式:GET(HTTPS)
+   * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/getuserinfo?access_token=ACCESS_TOKEN&code=CODE}
+   * 
+ * + * @param code the code + * @return school user info + * @throws WxErrorException the wx error exception + */ + WxCpOauth2UserInfo getSchoolUserInfo(@NonNull String code) throws WxErrorException; + + /** + * 创建学生 + * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/create_student?access_token=ACCESS_TOKEN + * + * @param studentUserId the student user id + * @param name the name + * @param departments the departments + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp createStudent(@NonNull String studentUserId, @NonNull String name, @NonNull List departments) throws WxErrorException; + + /** + * 批量创建学生 + * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/batch_create_student?access_token=ACCESS_TOKEN + * + * @param request the request + * @return wx cp batch result list + * @throws WxErrorException the wx error exception + */ + WxCpBatchResultList batchCreateStudent(@NonNull WxCpBatchCreateStudentRequest request) throws WxErrorException; + + /** + * 批量删除学生 + * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/batch_delete_student?access_token=ACCESS_TOKEN + * + * @param request the request + * @return wx cp batch result list + * @throws WxErrorException the wx error exception + */ + WxCpBatchResultList batchDeleteStudent(@NonNull WxCpBatchDeleteStudentRequest request) throws WxErrorException; + + /** + * 批量更新学生 + * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/batch_update_student?access_token=ACCESS_TOKEN + * + * @param request the request + * @return wx cp batch result list + * @throws WxErrorException the wx error exception + */ + WxCpBatchResultList batchUpdateStudent(@NonNull WxCpBatchUpdateStudentRequest request) throws WxErrorException; + + /** + * 删除学生 + *
+   * 请求方式:GET(HTTPS)
+   * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/delete_student?access_token=ACCESS_TOKEN&userid=USERID}
+   * 
+ * + * @param studentUserId the student user id + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp deleteStudent(@NonNull String studentUserId) throws WxErrorException; + + /** + * 更新学生 + * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/update_student?access_token=ACCESS_TOKEN + * + * @param studentUserId the student user id + * @param newStudentUserId the new student user id + * @param name the name + * @param departments the departments + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp updateStudent(@NonNull String studentUserId, String newStudentUserId, String name, + List departments) throws WxErrorException; + + /** + * 创建家长 + * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/create_parent?access_token=ACCESS_TOKEN + * + * @param request the request + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp createParent(@NonNull WxCpCreateParentRequest request) throws WxErrorException; + + /** + * 批量创建家长 + * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/batch_create_parent?access_token=ACCESS_TOKEN + * + * @param request the request + * @return wx cp batch result list + * @throws WxErrorException the wx error exception + */ + WxCpBatchResultList batchCreateParent(@NonNull WxCpBatchCreateParentRequest request) throws WxErrorException; + + /** + * 批量删除家长 + * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/batch_delete_parent?access_token=ACCESS_TOKEN + * + * @param userIdList the user id list + * @return wx cp batch result list + * @throws WxErrorException the wx error exception + */ + WxCpBatchResultList batchDeleteParent(@NonNull String... userIdList) throws WxErrorException; + + /** + * 批量更新家长 + * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/batch_update_parent?access_token=ACCESS_TOKEN + * + * @param request the request + * @return wx cp batch result list + * @throws WxErrorException the wx error exception + */ + WxCpBatchResultList batchUpdateParent(@NonNull WxCpBatchUpdateParentRequest request) throws WxErrorException; + + /** + * 读取学生或家长 + *
+   * 请求方式:GET(HTTPS)
+   * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/get?access_token=ACCESS_TOKEN&userid=USERID}
+   * 
+ * + * @param userId the user id + * @return user + * @throws WxErrorException the wx error exception + */ + WxCpUserResult getUser(@NonNull String userId) throws WxErrorException; + + /** + * 获取部门成员详情 + *
+   * 请求方式:GET(HTTPS)
+   * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/list?access_token=ACCESS_TOKEN&department_id=DEPARTMENT_ID&fetch_child=FETCH_CHILD}
+   * 
+ * + * @param departmentId 获取的部门id + * @param fetchChild 1/0:是否递归获取子部门下面的成员 + * @return user list + * @throws WxErrorException the wx error exception + */ + WxCpUserListResult getUserList(@NonNull Integer departmentId, Integer fetchChild) throws WxErrorException; + + /** + * 获取部门家长详情 + *
+   * 请求方式:GET(HTTPS)
+   * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/list_parent?access_token=ACCESS_TOKEN&department_id=DEPARTMENT_ID}
+   * 
+ * + * @param departmentId 获取的部门id + * @return user list parent + * @throws WxErrorException the wx error exception + */ + WxCpListParentResult getUserListParent(@NonNull Integer departmentId) throws WxErrorException; + + /** + * 更新家长 + * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/update_parent?access_token=ACCESS_TOKEN + * + * @param request the request + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp updateParent(@NonNull WxCpUpdateParentRequest request) throws WxErrorException; + + /** + * 删除家长 + *
+   * 请求方式:GET(HTTPS)
+   * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/delete_parent?access_token=ACCESS_TOKEN&userid=USERID}
+   * 
+ * + * @param userId the user id + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp deleteParent(@NonNull String userId) throws WxErrorException; + + /** + * 设置家校通讯录自动同步模式 + * 企业和第三方可通过此接口修改家校通讯录与班级标签之间的自动同步模式,注意,一旦设置禁止自动同步,将无法再次开启。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/set_arch_sync_mode?access_token=ACCESS_TOKEN + * + * @param archSyncMode 家校通讯录同步模式:1-禁止将标签同步至家校通讯录,2-禁止将家校通讯录同步至标签,3-禁止家校通讯录和标签相互同步 + * @return arch sync mode + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp setArchSyncMode(@NonNull Integer archSyncMode) throws WxErrorException; + + /** + * 创建部门 + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/department/create?access_token=ACCESS_TOKEN + * + * @param request 请求参数对象 + * @return wx cp create department + * @throws WxErrorException the wx error exception + */ + WxCpCreateDepartment createDepartment(@NonNull WxCpCreateDepartmentRequest request) throws WxErrorException; + + /** + * 更新部门 + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/department/update?access_token=ACCESS_TOKEN + * + * @param request the request + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp updateDepartment(@NonNull WxCpUpdateDepartmentRequest request) throws WxErrorException; + + /** + * 删除部门 + * 请求方式:GET(HTTPS) + * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/department/delete?access_token=ACCESS_TOKEN&id=ID} + * + * @param id the id + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp deleteDepartment(Integer id) throws WxErrorException; + + /** + * 设置关注「学校通知」的模式 + * 可通过此接口修改家长关注「学校通知」的模式:“可扫码填写资料加入”或“禁止扫码填写资料加入” + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/set_subscribe_mode?access_token=ACCESS_TOKEN + * + * @param subscribeMode 关注模式, 1:可扫码填写资料加入, 2:禁止扫码填写资料加入 + * @return subscribe mode + * @throws WxErrorException the wx error exception + */ + WxCpBaseResp setSubscribeMode(@NonNull Integer subscribeMode) throws WxErrorException; + + /** + * 获取关注「学校通知」的模式 + * 可通过此接口获取家长关注「学校通知」的模式:“可扫码填写资料加入”或“禁止扫码填写资料加入” + *

+ * 请求方式:GET(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_subscribe_mode?access_token=ACCESS_TOKEN + * + * @return subscribe mode + * @throws WxErrorException the wx error exception + */ + Integer getSubscribeMode() throws WxErrorException; + + /** + * 获取外部联系人详情 + * 学校可通过此接口,根据外部联系人的userid(如何获取?),拉取外部联系人详情。 + * + * 请求方式:GET(HTTPS) + * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get?access_token=ACCESS_TOKEN&external_userid=EXTERNAL_USERID} + * + * @param externalUserId 外部联系人的userid,注意不是学校成员的帐号 + * @return external contact + * @throws WxErrorException the wx error exception + */ + WxCpExternalContact getExternalContact(@NonNull String externalUserId) throws WxErrorException; + + /** + * 获取可使用的家长范围 + * 获取可在微信「学校通知-学校应用」使用该应用的家长范围,以学生或部门列表的形式返回。应用只能给该列表下的家长发送「学校通知」。注意该范围只能由学校的系统管理员在「管理端-家校沟通-配置」配置。 + * + * 请求方式:GET(HTTPS) + * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/agent/get_allow_scope?access_token=ACCESS_TOKEN&agentid=AGENTID} + * + * @param agentId the agent id + * @return allow scope + * @throws WxErrorException the wx error exception + */ + WxCpAllowScope getAllowScope(@NonNull Integer agentId) throws WxErrorException; + + /** + * 外部联系人openid转换 + * 企业和服务商可通过此接口,将微信外部联系人的userid(如何获取?)转为微信openid,用于调用支付相关接口。暂不支持企业微信外部联系人(ExternalUserid为wo开头)的userid转openid。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/convert_to_openid?access_token=ACCESS_TOKEN + * + * @param externalUserId the external user id + * @return string + * @throws WxErrorException the wx error exception + */ + String convertToOpenId(@NonNull String externalUserId) throws WxErrorException; + + /** + * 获取部门列表 + * 请求方式:GET(HTTPS) + * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/department/list?access_token=ACCESS_TOKEN&id=ID} + * + * @param id 部门id。获取指定部门及其下的子部门。 如果不填,默认获取全量组织架构 + * @return wx cp department list + * @throws WxErrorException the wx error exception + */ + WxCpDepartmentList listDepartment(Integer id) throws WxErrorException; + + /** + * 获取「学校通知」二维码 + * 学校可通过此接口获取「学校通知」二维码,家长可通过扫描此二维码关注「学校通知」并接收学校推送的消息。 + * 请求方式:GET(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_subscribe_qr_code?access_token=ACCESS_TOKEN + * + * @return subscribe qr code + * @throws WxErrorException the wx error exception + */ + WxCpSubscribeQrCode getSubscribeQrCode() throws WxErrorException; + + /** + * 修改自动升年级的配置 + * 请求方式: POST(HTTPS) + * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/school/set_upgrade_info?access_token=ACCESS_TOKEN + * + * @param upgradeTime the upgrade time + * @param upgradeSwitch the upgrade switch + * @return upgrade info + * @throws WxErrorException the wx error exception + */ + WxCpSetUpgradeInfo setUpgradeInfo(Long upgradeTime, Integer upgradeSwitch) throws WxErrorException; + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java index 73776228ed..f66acc0252 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java @@ -2,14 +2,14 @@ import me.chanjar.weixin.common.bean.WxJsapiSignature; import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.service.WxService; import me.chanjar.weixin.common.session.WxSession; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor; import me.chanjar.weixin.common.util.http.RequestExecutor; import me.chanjar.weixin.common.util.http.RequestHttp; +import me.chanjar.weixin.cp.bean.WxCpAgentJsapiSignature; import me.chanjar.weixin.cp.bean.WxCpMaJsCode2SessionResult; -import me.chanjar.weixin.cp.bean.WxCpMessage; -import me.chanjar.weixin.cp.bean.WxCpMessageSendResult; import me.chanjar.weixin.cp.bean.WxCpProviderToken; import me.chanjar.weixin.cp.config.WxCpConfigStorage; @@ -18,7 +18,7 @@ * * @author chanjaster */ -public interface WxCpService { +public interface WxCpService extends WxService { /** *

    * 验证推送过来的消息的正确性
@@ -29,13 +29,16 @@ public interface WxCpService {
    * @param timestamp    时间戳
    * @param nonce        随机数
    * @param data         微信传输过来的数据,有可能是echoStr,有可能是xml消息
+   * @return the boolean
    */
   boolean checkSignature(String msgSignature, String timestamp, String nonce, String data);
 
   /**
    * 获取access_token, 不强制刷新access_token
    *
-   * @see #getAccessToken(boolean)
+   * @return the access token
+   * @throws WxErrorException the wx error exception
+   * @see #getAccessToken(boolean) #getAccessToken(boolean)#getAccessToken(boolean)#getAccessToken(boolean)
    */
   String getAccessToken() throws WxErrorException;
 
@@ -49,13 +52,30 @@ public interface WxCpService {
    * 
* * @param forceRefresh 强制刷新 + * @return the access token + * @throws WxErrorException the wx error exception */ String getAccessToken(boolean forceRefresh) throws WxErrorException; + /** + *
+   * 获取会话存档access_token,本方法线程安全
+   * 会话存档相关接口需要使用会话存档secret获取单独的access_token
+   * 详情请见: https://developer.work.weixin.qq.com/document/path/91782
+   * 
+ * + * @param forceRefresh 强制刷新 + * @return 会话存档专用的access token + * @throws WxErrorException the wx error exception + */ + String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException; + /** * 获得jsapi_ticket,不强制刷新jsapi_ticket * - * @see #getJsapiTicket(boolean) + * @return the jsapi ticket + * @throws WxErrorException the wx error exception + * @see #getJsapiTicket(boolean) #getJsapiTicket(boolean)#getJsapiTicket(boolean)#getJsapiTicket(boolean) */ String getJsapiTicket() throws WxErrorException; @@ -68,6 +88,8 @@ public interface WxCpService { * * * @param forceRefresh 强制刷新 + * @return the jsapi ticket + * @throws WxErrorException the wx error exception */ String getJsapiTicket(boolean forceRefresh) throws WxErrorException; @@ -78,7 +100,9 @@ public interface WxCpService { * 签名的jsapi_ticket必须使用以下接口获取。且必须用wx.agentConfig中的agentid对应的应用secret去获取access_token。 * 签名用的noncestr和timestamp必须与wx.agentConfig中的nonceStr和timestamp相同。 * - * @see #getJsapiTicket(boolean) + * @return the agent jsapi ticket + * @throws WxErrorException the wx error exception + * @see #getJsapiTicket(boolean) #getJsapiTicket(boolean)#getJsapiTicket(boolean)#getJsapiTicket(boolean) */ String getAgentJsapiTicket() throws WxErrorException; @@ -96,6 +120,8 @@ public interface WxCpService { * * * @param forceRefresh 强制刷新 + * @return the agent jsapi ticket + * @throws WxErrorException the wx error exception */ String getAgentJsapiTicket(boolean forceRefresh) throws WxErrorException; @@ -107,36 +133,55 @@ public interface WxCpService { * * * @param url url + * @return the wx jsapi signature + * @throws WxErrorException the wx error exception */ WxJsapiSignature createJsapiSignature(String url) throws WxErrorException; /** *
-   * 发送消息
-   * 详情请见: http://qydev.weixin.qq.com/wiki/index.php?title=%E5%8F%91%E9%80%81%E6%8E%A5%E5%8F%A3%E8%AF%B4%E6%98%8E
+   *   创建调用wx.agentConfig时所需要的签名
+   *
+   * 详情请见:https://open.work.weixin.qq.com/api/doc/90000/90136/94313
    * 
* - * @param message 要发送的消息对象 + * @param url url + * @return the agent jsapi signature + * @throws WxErrorException the wx error exception */ - WxCpMessageSendResult messageSend(WxCpMessage message) throws WxErrorException; + WxCpAgentJsapiSignature createAgentJsapiSignature(String url) throws WxErrorException; /** * 小程序登录凭证校验 * * @param jsCode 登录时获取的 code + * @return the wx cp ma js code 2 session result + * @throws WxErrorException the wx error exception */ WxCpMaJsCode2SessionResult jsCode2Session(String jsCode) throws WxErrorException; /** *
-   * 获取微信服务器的ip段
+   * 获取企业微信回调IP段
    * http://qydev.weixin.qq.com/wiki/index.php?title=回调模式#.E8.8E.B7.E5.8F.96.E5.BE.AE.E4.BF.A1.E6.9C.8D.E5.8A.A1.E5.99.A8.E7.9A.84ip.E6.AE.B5
    * 
* * @return { "ip_list": ["101.226.103.*", "101.226.62.*"] } + * @throws WxErrorException the wx error exception */ String[] getCallbackIp() throws WxErrorException; + /** + *
+   * 获取企业微信接口IP段
+   * https://developer.work.weixin.qq.com/document/path/92520
+   * 
+ * + * @return 企业微信接口IP段 + * @throws WxErrorException the wx error exception + */ + String[] getApiDomainIp() throws WxErrorException; + /** *
    * 获取服务商凭证
@@ -147,31 +192,33 @@ public interface WxCpService {
    *
    * @param corpId         服务商的corpid
    * @param providerSecret 服务商的secret,在服务商管理后台可见
-   * @return {
-   * "errcode":0 ,
-   * "errmsg":"ok" ,
-   * "provider_access_token":"enLSZ5xxxxxxJRL",
-   * "expires_in":7200
-   * }
+   * @return { "errcode":0 , "errmsg":"ok" , "provider_access_token":"enLSZ5xxxxxxJRL", "expires_in":7200 }
    * @throws WxErrorException .
    */
   WxCpProviderToken getProviderToken(String corpId, String providerSecret) throws WxErrorException;
 
   /**
-   * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的GET请求
+   * 当不需要自动带accessToken的时候,可以用这个发起post请求
    *
-   * @param url        接口地址
-   * @param queryParam 请求参数
+   * @param url      接口地址
+   * @param postData 请求body字符串
+   * @return the string
+   * @throws WxErrorException the wx error exception
    */
-  String get(String url, String queryParam) throws WxErrorException;
+  String postWithoutToken(String url, String postData) throws WxErrorException;
 
   /**
-   * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求
+   * 
+   * 使用会话存档access token发起post请求
+   * 会话存档相关API需要使用会话存档专用的secret获取独立的access token
+   * 
* * @param url 接口地址 * @param postData 请求body字符串 + * @return the string + * @throws WxErrorException the wx error exception */ - String post(String url, String postData) throws WxErrorException; + String postForMsgAudit(String url, String postData) throws WxErrorException; /** *
@@ -180,11 +227,13 @@ public interface WxCpService {
    * 可以参考,{@link MediaUploadRequestExecutor}的实现方法
    * 
* + * @param 请求值类型 + * @param 返回值类型 * @param executor 执行器 * @param uri 请求地址 * @param data 参数 - * @param 请求值类型 - * @param 返回值类型 + * @return the t + * @throws WxErrorException the wx error exception */ T execute(RequestExecutor executor, String uri, E data) throws WxErrorException; @@ -212,6 +261,7 @@ public interface WxCpService { * 获取某个sessionId对应的session,如果sessionId没有对应的session,则新建一个并返回。 * * @param id id可以为任意字符串,建议使用FromUserName作为id + * @return the session */ WxSession getSession(String id); @@ -220,13 +270,14 @@ public interface WxCpService { * * @param id id可以为任意字符串,建议使用FromUserName作为id * @param create 是否新建 + * @return the session */ WxSession getSession(String id, boolean create); /** * 获取WxSessionManager 对象 * - * @return WxSessionManager + * @return WxSessionManager session manager */ WxSessionManager getSessionManager(); @@ -244,20 +295,37 @@ public interface WxCpService { * 上传部门列表覆盖企业号上的部门信息 * * @param mediaId 媒体id + * @return the string + * @throws WxErrorException the wx error exception */ String replaceParty(String mediaId) throws WxErrorException; + /** + * 上传用户列表,增量更新成员 + * + * @param mediaId 媒体id + * @return jobId 异步任务id + * @throws WxErrorException the wx error exception + */ + String syncUser(String mediaId) throws WxErrorException; + /** * 上传用户列表覆盖企业号上的用户信息 * * @param mediaId 媒体id + * @return the string + * @throws WxErrorException the wx error exception */ String replaceUser(String mediaId) throws WxErrorException; /** * 获取异步任务结果 + * + * @param jobId 异步任务id + * @return the task result + * @throws WxErrorException the wx error exception */ - String getTaskResult(String joinId) throws WxErrorException; + String getTaskResult(String jobId) throws WxErrorException; /** * 初始化http请求对象 @@ -265,9 +333,9 @@ public interface WxCpService { void initHttp(); /** - * 获取WxMpConfigStorage 对象 + * 获取WxCpConfigStorage 对象 * - * @return WxMpConfigStorage + * @return WxCpConfigStorage wx cp config storage */ WxCpConfigStorage getWxCpConfigStorage(); @@ -278,70 +346,292 @@ public interface WxCpService { */ void setWxCpConfigStorage(WxCpConfigStorage wxConfigProvider); + /** + * 构造扫码登录链接 - 构造独立窗口登录二维码 + * + * @param redirectUri 重定向地址,需要进行UrlEncode + * @param state 用于保持请求和回调的状态,授权请求后原样带回给企业。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议企业带上该参数,可设置为简单的随机数加session进行校验 + * @return . string + */ + String buildQrConnectUrl(String redirectUri, String state); + /** * 获取部门相关接口的服务类对象 + * + * @return the department service */ WxCpDepartmentService getDepartmentService(); /** * 获取媒体相关接口的服务类对象 + * + * @return the media service */ WxCpMediaService getMediaService(); /** * 获取菜单相关接口的服务类对象 + * + * @return the menu service */ WxCpMenuService getMenuService(); /** * 获取Oauth2相关接口的服务类对象 + * + * @return the oauth 2 service */ WxCpOAuth2Service getOauth2Service(); /** * 获取标签相关接口的服务类对象 + * + * @return the tag service */ WxCpTagService getTagService(); /** * 获取用户相关接口的服务类对象 + * + * @return the user service */ WxCpUserService getUserService(); + /** + * Gets external contact service. + * + * @return the external contact service + */ WxCpExternalContactService getExternalContactService(); /** * 获取群聊服务 * - * @return 群聊服务 + * @return 群聊服务 chat service */ WxCpChatService getChatService(); /** * 获取任务卡片服务 * - * @return 任务卡片服务 + * @return 任务卡片服务 task card service */ WxCpTaskCardService getTaskCardService(); + /** + * Gets agent service. + * + * @return the agent service + */ WxCpAgentService getAgentService(); - WxCpOaService getOAService(); + /** + * Gets message service. + * + * @return the message service + */ + WxCpMessageService getMessageService(); + + /** + * 获取OA相关接口的服务类对象. + * + * @return the oa service + */ + WxCpOaService getOaService(); + + /** + * 获取家校应用复学码相关接口的服务类对象 + * + * @return school service + */ + WxCpSchoolService getSchoolService(); + + /** + * 获取家校沟通相关接口的服务类对象 + * + * @return school user service + */ + WxCpSchoolUserService getSchoolUserService(); + + /** + * 获取家校应用健康上报的服务类对象 + * + * @return school health service + */ + WxCpSchoolHealthService getSchoolHealthService(); + + /** + * 获取直播相关接口的服务类对象 + * + * @return the Living service + */ + WxCpLivingService getLivingService(); + + /** + * 获取OA 自建应用相关接口的服务类对象 + * + * @return oa agent service + */ + WxCpOaAgentService getOaAgentService(); + + /** + * 获取OA效率工具 微盘的服务类对象 + * + * @return oa we drive service + */ + WxCpOaWeDriveService getOaWeDriveService(); + + /** + * 获取OA效率工具 文档的服务类对象 + * + * @return oa we doc service + */ + WxCpOaWeDocService getOaWeDocService(); + + /** + * 获取会话存档相关接口的服务类对象 + * + * @return msg audit service + */ + WxCpMsgAuditService getMsgAuditService(); + + /** + * 获取日历相关接口的服务类对象 + * + * @return the oa calendar service + */ + WxCpOaCalendarService getOaCalendarService(); + + /** + * 获取会议室相关接口的服务类对象 + * + * @return the oa meetingroom service + */ + WxCpOaMeetingRoomService getOaMeetingRoomService(); + + /** + * 获取日程相关接口的服务类对象 + * + * @return the oa schedule service + */ + WxCpOaScheduleService getOaScheduleService(); + + /** + * 获取群机器人消息推送服务 + * + * @return 群机器人消息推送服务 group robot service + */ + WxCpGroupRobotService getGroupRobotService(); + + /** + * 获取工作台服务 + * + * @return the workbench service + */ + WxCpAgentWorkBenchService getWorkBenchService(); + + /** + * 获取微信客服服务 + * + * @return 微信客服服务 kf service + */ + WxCpKfService getKfService(); /** * http请求对象 + * + * @return the request http */ RequestHttp getRequestHttp(); + /** + * Sets user service. + * + * @param userService the user service + */ void setUserService(WxCpUserService userService); + /** + * Sets department service. + * + * @param departmentService the department service + */ void setDepartmentService(WxCpDepartmentService departmentService); + /** + * Sets media service. + * + * @param mediaService the media service + */ void setMediaService(WxCpMediaService mediaService); + /** + * Sets menu service. + * + * @param menuService the menu service + */ void setMenuService(WxCpMenuService menuService); + /** + * Sets oauth 2 service. + * + * @param oauth2Service the oauth 2 service + */ void setOauth2Service(WxCpOAuth2Service oauth2Service); + /** + * Sets tag service. + * + * @param tagService the tag service + */ void setTagService(WxCpTagService tagService); + + /** + * Sets kf service. + * + * @param kfService the kf service + */ + void setKfService(WxCpKfService kfService); + + /** + * 获取异步导出服务 + * + * @return 异步导出服务 export service + */ + WxCpExportService getExportService(); + + /** + * 设置异步导出服务 + * + * @param exportService 异步导出服务 + */ + void setExportService(WxCpExportService exportService); + + /** + * 相关接口的服务类对象 + * + * @return the meeting service + */ + WxCpMeetingService getMeetingService(); + + /** + * 企业互联的服务类对象 + * + * @return 企业互联服务对象 + */ + WxCpCorpGroupService getCorpGroupService(); + + /** + * 获取智能机器人服务 + * + * @return 智能机器人服务 intelligent robot service + */ + WxCpIntelligentRobotService getIntelligentRobotService(); + + /** + * 获取人事助手服务 + * + * @return 人事助手服务 hr service + */ + WxCpHrService getHrService(); } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTagService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTagService.java index 52bf932303..4469bcc9e9 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTagService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTagService.java @@ -17,7 +17,6 @@ * @author Binary Wang */ public interface WxCpTagService { - /** * 创建标签. *
@@ -27,22 +26,11 @@ public interface WxCpTagService {
    *
    * @param name 标签名称,长度限制为32个字以内(汉字或英文字母),标签名不可与其他标签重名。
    * @param id   标签id,非负整型,指定此参数时新增的标签会生成对应的标签id,不指定时则以目前最大的id自增。
-   * @return 标签id
+   * @return 标签id string
    * @throws WxErrorException .
    */
   String create(String name, Integer id) throws WxErrorException;
 
-  /**
-   * 创建标签.
-   *
-   * @param tagName 标签名
-   * @return 标签id
-   * @throws WxErrorException .
-   * @deprecated 建议使用 {@link #create(String, Integer)},其中后面的参数可以为空
-   */
-  @Deprecated
-  String create(String tagName) throws WxErrorException;
-
   /**
    * 更新标签.
    *
@@ -63,7 +51,7 @@ public interface WxCpTagService {
   /**
    * 获得标签列表.
    *
-   * @return 标签列表
+   * @return 标签列表 list
    * @throws WxErrorException .
    */
   List listAll() throws WxErrorException;
@@ -72,7 +60,7 @@ public interface WxCpTagService {
    * 获取标签成员.
    *
    * @param tagId 标签ID
-   * @return 成员列表
+   * @return 成员列表 list
    * @throws WxErrorException .
    */
   List listUsersByTagId(String tagId) throws WxErrorException;
@@ -82,7 +70,7 @@ public interface WxCpTagService {
    * 对应: http://qydev.weixin.qq.com/wiki/index.php?title=管理标签 中的get接口
    *
    * @param tagId 标签id
-   * @return .
+   * @return . wx cp tag get result
    * @throws WxErrorException .
    */
   WxCpTagGetResult get(String tagId) throws WxErrorException;
@@ -93,7 +81,7 @@ public interface WxCpTagService {
    * @param tagId    标签id
    * @param userIds  用户ID 列表
    * @param partyIds 企业部门ID列表
-   * @return .
+   * @return . wx cp tag add or remove users result
    * @throws WxErrorException .
    */
   WxCpTagAddOrRemoveUsersResult addUsers2Tag(String tagId, List userIds, List partyIds) throws WxErrorException;
@@ -104,7 +92,7 @@ public interface WxCpTagService {
    * @param tagId    标签id
    * @param userIds  用户id列表
    * @param partyIds 企业部门ID列表
-   * @return .
+   * @return . wx cp tag add or remove users result
    * @throws WxErrorException .
    */
   WxCpTagAddOrRemoveUsersResult removeUsersFromTag(String tagId, List userIds, List partyIds) throws WxErrorException;
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTaskCardService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTaskCardService.java
index 5bf50d36dc..303d22f692 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTaskCardService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTaskCardService.java
@@ -1,6 +1,7 @@
 package me.chanjar.weixin.cp.api;
 
 import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.bean.message.TemplateCardMessage;
 
 import java.util.List;
 
@@ -8,10 +9,10 @@
  * 
  *  任务卡片管理接口.
  *  Created by Jeff on 2019-05-16.
+ *  Updted by HeXiao on 2022-03-09.
  * 
* - * @author Jeff - * @date 2019-05-16 + * @author Jeff created on 2019-05-16 */ public interface WxCpTaskCardService { @@ -23,9 +24,30 @@ public interface WxCpTaskCardService { * 注意: 这个方法使用WxCpConfigStorage里的agentId *
* - * @param userIds 企业的成员ID列表 - * @param taskId 任务卡片ID - * @param clickedKey 已点击按钮的Key + * @param userIds 企业的成员ID列表 + * @param taskId 任务卡片ID + * @param replaceName 替换文案 + * @throws WxErrorException the wx error exception */ - void update(List userIds, String taskId, String clickedKey) throws WxErrorException; + void update(List userIds, String taskId, String replaceName) throws WxErrorException; + + + /** + * 更新按钮为不可点击状态 + * 详情请见https://developer.work.weixin.qq.com/document/path/94888#%E6%9B%B4%E6%96%B0%E6%8C%89%E9%92%AE%E4%B8%BA%E4%B8 + * %8D%E5%8F%AF%E7%82%B9%E5%87%BB%E7%8A%B6%E6%80%81 + * + * @param userIds 企业的成员ID列表 + * @param partyIds 企业的部门ID列表 + * @param tagIds 企业的标签ID列表 + * @param atAll 更新整个任务接收人员 + * @param responseCode 更新卡片所需要消费的code,可通过发消息接口和回调接口返回值获取,一个code只能调用一次该接口,且只能在24小时内调用 + * @param replaceName 需要更新的按钮的文案 + * @throws WxErrorException the wx error exception + */ + void updateTemplateCardButton(List userIds, List partyIds, + List tagIds, Integer atAll, String responseCode, + String replaceName) throws WxErrorException; + + void updateTemplateCardButton(TemplateCardMessage templateCardMessage) throws WxErrorException; } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTpService.java deleted file mode 100644 index edc2f552ce..0000000000 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTpService.java +++ /dev/null @@ -1,168 +0,0 @@ -package me.chanjar.weixin.cp.api; - -import me.chanjar.weixin.common.bean.WxAccessToken; -import me.chanjar.weixin.common.error.WxErrorException; -import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor; -import me.chanjar.weixin.common.util.http.RequestExecutor; -import me.chanjar.weixin.common.util.http.RequestHttp; -import me.chanjar.weixin.cp.bean.WxCpMaJsCode2SessionResult; -import me.chanjar.weixin.cp.bean.WxCpTpCorp; -import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; - -/** - * 微信第三方应用API的Service. - * - * @author zhenjun cai - */ -public interface WxCpTpService { - /** - *
-   * 验证推送过来的消息的正确性
-   * 详情请见: https://work.weixin.qq.com/api/doc#90000/90139/90968/消息体签名校验
-   * 
- * - * @param msgSignature 消息签名 - * @param timestamp 时间戳 - * @param nonce 随机数 - * @param data 微信传输过来的数据,有可能是echoStr,有可能是xml消息 - */ - boolean checkSignature(String msgSignature, String timestamp, String nonce, String data); - - /** - * 获取suite_access_token, 不强制刷新suite_access_token - * - * @see #getSuiteAccessToken(boolean) - */ - String getSuiteAccessToken() throws WxErrorException; - - /** - *
-   * 获取suite_access_token,本方法线程安全
-   * 且在多线程同时刷新时只刷新一次,避免超出2000次/日的调用次数上限
-   * 另:本service的所有方法都会在suite_access_token过期是调用此方法
-   * 程序员在非必要情况下尽量不要主动调用此方法
-   * 详情请见: https://work.weixin.qq.com/api/doc#90001/90143/90600
-   * 
- * - * @param forceRefresh 强制刷新 - */ - String getSuiteAccessToken(boolean forceRefresh) throws WxErrorException; - - /** - * 获得suite_ticket,不强制刷新suite_ticket - * - * @see #getSuiteTicket(boolean) - */ - String getSuiteTicket() throws WxErrorException; - - /** - *
-   * 获得suite_ticket
-   * 由于suite_ticket是微信服务器定时推送(每10分钟),不能主动获取,如果碰到过期只能抛异常
-   *
-   * 详情请见:https://work.weixin.qq.com/api/doc#90001/90143/90628
-   * 
- * - * @param forceRefresh 强制刷新 - */ - String getSuiteTicket(boolean forceRefresh) throws WxErrorException; - - /** - * 小程序登录凭证校验 - * - * @param jsCode 登录时获取的 code - */ - WxCpMaJsCode2SessionResult jsCode2Session(String jsCode) throws WxErrorException; - - /** - * 获取企业凭证 - * - * @param authCorpid 授权方corpid - * @param permanentCode 永久授权码,通过get_permanent_code获取 - */ - WxAccessToken getCorpToken(String authCorpid, String permanentCode) throws WxErrorException; - - /** - * 获取企业永久授权码 . - * - * @param authCode . - * @return . - */ - WxCpTpCorp getPermanentCode(String authCode) throws WxErrorException; - - /** - * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的GET请求. - * - * @param url 接口地址 - * @param queryParam 请求参数 - */ - String get(String url, String queryParam) throws WxErrorException; - - /** - * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求. - * - * @param url 接口地址 - * @param postData 请求body字符串 - */ - String post(String url, String postData) throws WxErrorException; - - /** - *
-   * Service没有实现某个API的时候,可以用这个,
-   * 比{@link #get}和{@link #post}方法更灵活,可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型。
-   * 可以参考,{@link MediaUploadRequestExecutor}的实现方法
-   * 
- * - * @param executor 执行器 - * @param uri 请求地址 - * @param data 参数 - * @param 请求值类型 - * @param 返回值类型 - */ - T execute(RequestExecutor executor, String uri, E data) throws WxErrorException; - - /** - *
-   * 设置当微信系统响应系统繁忙时,要等待多少 retrySleepMillis(ms) * 2^(重试次数 - 1) 再发起重试.
-   * 默认:1000ms
-   * 
- * - * @param retrySleepMillis 重试休息时间 - */ - void setRetrySleepMillis(int retrySleepMillis); - - /** - *
-   * 设置当微信系统响应系统繁忙时,最大重试次数.
-   * 默认:5次
-   * 
- * - * @param maxRetryTimes 最大重试次数 - */ - void setMaxRetryTimes(int maxRetryTimes); - - /** - * 初始化http请求对象 - */ - void initHttp(); - - /** - * 获取WxMpConfigStorage 对象. - * - * @return WxMpConfigStorage - */ - WxCpTpConfigStorage getWxCpTpConfigStorage(); - - /** - * 注入 {@link WxCpTpConfigStorage} 的实现. - * - * @param wxConfigProvider 配置对象 - */ - void setWxCpTpConfigStorage(WxCpTpConfigStorage wxConfigProvider); - - /** - * http请求对象. - */ - RequestHttp getRequestHttp(); - -} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpUserService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpUserService.java index 8cc77a2d55..7a7b5f40a8 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpUserService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpUserService.java @@ -2,9 +2,14 @@ import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.cp.bean.WxCpInviteResult; +import me.chanjar.weixin.cp.bean.WxCpOpenUseridToUseridResult; import me.chanjar.weixin.cp.bean.WxCpUser; -import me.chanjar.weixin.cp.bean.WxCpUserExternalContactInfo; +import me.chanjar.weixin.cp.bean.WxCpUseridToOpenUseridResult; +import me.chanjar.weixin.cp.bean.external.contact.WxCpExternalContactInfo; +import me.chanjar.weixin.cp.bean.user.WxCpDeptUserResult; +import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.Map; @@ -25,19 +30,24 @@ public interface WxCpUserService { *
* * @param userId 用户id + * @throws WxErrorException the wx error exception */ void authenticate(String userId) throws WxErrorException; /** *
-   * 获取部门成员(详情).
+   * 获取部门成员详情
+   * 请求方式:GET(HTTPS)
+   * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token=ACCESS_TOKEN&department_id=DEPARTMENT_ID&fetch_child=FETCH_CHILD}
    *
-   * http://qydev.weixin.qq.com/wiki/index.php?title=管理成员#.E8.8E.B7.E5.8F.96.E9.83.A8.E9.97.A8.E6.88.90.E5.91.98.28.E8.AF.A6.E6.83.85.29
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/90201
    * 
* * @param departId 必填。部门id * @param fetchChild 非必填。1/0:是否递归获取子部门下面的成员 * @param status 非必填。0获取全部员工,1获取已关注成员列表,2获取禁用成员列表,4获取未关注成员列表。status可叠加 + * @return the list + * @throws WxErrorException the wx error exception */ List listByDepartment(Long departId, Boolean fetchChild, Integer status) throws WxErrorException; @@ -51,6 +61,8 @@ public interface WxCpUserService { * @param departId 必填。部门id * @param fetchChild 非必填。1/0:是否递归获取子部门下面的成员 * @param status 非必填。0获取全部员工,1获取已关注成员列表,2获取禁用成员列表,4获取未关注成员列表。status可叠加 + * @return the list + * @throws WxErrorException the wx error exception */ List listSimpleByDepartment(Long departId, Boolean fetchChild, Integer status) throws WxErrorException; @@ -58,6 +70,7 @@ public interface WxCpUserService { * 新建用户. * * @param user 用户对象 + * @throws WxErrorException the wx error exception */ void create(WxCpUser user) throws WxErrorException; @@ -65,6 +78,7 @@ public interface WxCpUserService { * 更新用户. * * @param user 用户对象 + * @throws WxErrorException the wx error exception */ void update(WxCpUser user) throws WxErrorException; @@ -75,6 +89,7 @@ public interface WxCpUserService { * * * @param userIds 员工UserID列表。对应管理端的帐号 + * @throws WxErrorException the wx error exception */ void delete(String... userIds) throws WxErrorException; @@ -82,6 +97,8 @@ public interface WxCpUserService { * 获取用户. * * @param userid 用户id + * @return the by id + * @throws WxErrorException the wx error exception */ WxCpUser getById(String userid) throws WxErrorException; @@ -97,6 +114,8 @@ public interface WxCpUserService { * @param userIds 成员ID列表, 最多支持1000个。 * @param partyIds 部门ID列表,最多支持100个。 * @param tagIds 标签ID列表,最多支持100个。 + * @return the wx cp invite result + * @throws WxErrorException the wx error exception */ WxCpInviteResult invite(List userIds, List partyIds, List tagIds) throws WxErrorException; @@ -114,9 +133,9 @@ public interface WxCpUserService { * * @param userId 企业内的成员id * @param agentId 非必填,整型,仅用于发红包。其它场景该参数不要填,如微信支付、企业转账、电子发票 - * @return map对象,可能包含以下值: - * - openid 企业微信成员userid对应的openid,若有传参agentid,则是针对该agentid的openid。否则是针对企业微信corpid的openid - * - appid 应用的appid,若请求包中不包含agentid则不返回appid。该appid在使用微信红包时会用到 + * @return map对象 ,可能包含以下值: - openid 企业微信成员userid对应的openid,若有传参agentid,则是针对该agentid的openid。否则是针对企业微信corpid的openid - + * appid 应用的appid,若请求包中不包含agentid则不返回appid。该appid在使用微信红包时会用到 + * @throws WxErrorException the wx error exception */ Map userId2Openid(String userId, Integer agentId) throws WxErrorException; @@ -134,6 +153,7 @@ public interface WxCpUserService { * * @param openid 在使用微信支付、微信红包和企业转账之后,返回结果的openid * @return userid 该openid在企业微信对应的成员userid + * @throws WxErrorException the wx error exception */ String openid2UserId(String openid) throws WxErrorException; @@ -149,11 +169,29 @@ public interface WxCpUserService { * * * @param mobile 手机号码。长度为5~32个字节 - * @return userid mobile对应的成员userid + * @return userid mobile对应的成员userid * @throws WxErrorException . */ String getUserId(String mobile) throws WxErrorException; + /** + *
+   *
+   * 通过邮箱获取其所对应的userid。
+   *
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/get_userid_by_email?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/95895
+   * 
+ * + * @param email 邮箱 + * @param emailType 邮箱类型:1-企业邮箱;2-个人邮箱 + * @return userid email对应的成员userid + * @throws WxErrorException . + */ + String getUserIdByEmail(String email,int emailType) throws WxErrorException; + /** * 获取外部联系人详情. *
@@ -164,10 +202,86 @@ public interface WxCpUserService {
    * 
* * @param userId 外部联系人的userid - * @return 联系人详情 + * @return 联系人详情 external contact + * @throws WxErrorException . + */ + WxCpExternalContactInfo getExternalContact(String userId) throws WxErrorException; + + /** + *
+   *
+   * 获取加入企业二维码。
+   *
+   * 请求方式:GET(HTTPS)
+   * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/corp/get_join_qrcode?access_token=ACCESS_TOKEN&size_type=SIZE_TYPE}
+   *
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/91714
+   * 
+ * + * @param sizeType qrcode尺寸类型,1: 171 x 171; 2: 399 x 399; 3: 741 x 741; 4: 2052 x 2052 + * @return join_qrcode 二维码链接,有效期7天 + * @throws WxErrorException . + */ + String getJoinQrCode(int sizeType) throws WxErrorException; + + /** + *
+   *
+   * 获取企业活跃成员数。
+   *
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/get_active_stat?access_token=ACCESS_TOKEN
+   *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/92714
+   * 
+ * + * @param date 具体某天的活跃人数,最长支持获取30天前数据 + * @return join_qrcode 活跃成员数 * @throws WxErrorException . */ - WxCpUserExternalContactInfo getExternalContact(String userId) throws WxErrorException; + Integer getActiveStat(Date date) throws WxErrorException; + + /** + * userid转换为open_userid + * 将自建应用或代开发应用获取的userid转换为第三方应用的userid + * https://developer.work.weixin.qq.com/document/path/95603 + * + * @param useridList the userid list + * @return the WxCpUseridToOpenUseridResult + * @throws WxErrorException the wx error exception + */ + WxCpUseridToOpenUseridResult useridToOpenUserid(ArrayList useridList) throws WxErrorException; + /** + * open_userid转换为userid + * 将代开发应用或第三方应用获取的密文open_userid转换为明文userid + *
+   * 文档地址:https://developer.work.weixin.qq.com/document/path/95884#userid%E8%BD%AC%E6%8D%A2
+   *
+   * 权限说明:
+   *
+   * 需要使用自建应用或基础应用的access_token
+   * 成员需要同时在access_token和source_agentid所对应应用的可见范围内
+   * 
+ * @param openUseridList open_userid列表,最多不超过1000个。必须是source_agentid对应的应用所获取 + * @param sourceAgentId 企业授权的代开发自建应用或第三方应用的agentid + * @return the WxCpOpenUseridToUseridResult + * @throws WxErrorException the wx error exception + */ + WxCpOpenUseridToUseridResult openUseridToUserid(List openUseridList, String sourceAgentId) throws WxErrorException; + + /** + * 获取成员ID列表 + * 获取企业成员的userid与对应的部门ID列表,预计于2022年8月8号发布。若需要获取其他字段,参见「适配建议」。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/list_id?access_token=ACCESS_TOKEN + * + * @param cursor the cursor + * @param limit the limit + * @return user list id + * @throws WxErrorException the wx error exception + */ + WxCpDeptUserResult getUserListId(String cursor, Integer limit) throws WxErrorException; } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java index 9c7396ab1b..7c72cb9a8c 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java @@ -2,30 +2,31 @@ import com.google.common.base.Joiner; import com.google.gson.JsonArray; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import lombok.extern.slf4j.Slf4j; -import me.chanjar.weixin.common.WxType; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.common.bean.CommonUploadParam; +import me.chanjar.weixin.common.bean.ToJson; import me.chanjar.weixin.common.bean.WxJsapiSignature; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.error.WxRuntimeException; +import me.chanjar.weixin.common.executor.CommonUploadRequestExecutor; import me.chanjar.weixin.common.session.StandardSessionManager; import me.chanjar.weixin.common.session.WxSession; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.common.util.DataUtils; import me.chanjar.weixin.common.util.RandomUtils; import me.chanjar.weixin.common.util.crypto.SHA1; -import me.chanjar.weixin.common.util.http.RequestExecutor; -import me.chanjar.weixin.common.util.http.RequestHttp; -import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor; -import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor; +import me.chanjar.weixin.common.util.http.*; +import me.chanjar.weixin.common.util.json.GsonParser; import me.chanjar.weixin.cp.api.*; +import me.chanjar.weixin.cp.bean.WxCpAgentJsapiSignature; import me.chanjar.weixin.cp.bean.WxCpMaJsCode2SessionResult; -import me.chanjar.weixin.cp.bean.WxCpMessage; -import me.chanjar.weixin.cp.bean.WxCpMessageSendResult; import me.chanjar.weixin.cp.bean.WxCpProviderToken; import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import org.apache.commons.lang3.StringUtils; import java.io.File; import java.io.IOException; @@ -37,21 +38,45 @@ /** * . * + * @param the type parameter + * @param

the type parameter * @author chanjarster */ @Slf4j public abstract class BaseWxCpServiceImpl implements WxCpService, RequestHttp { private WxCpUserService userService = new WxCpUserServiceImpl(this); - private WxCpChatService chatService = new WxCpChatServiceImpl(this); + private final WxCpChatService chatService = new WxCpChatServiceImpl(this); private WxCpDepartmentService departmentService = new WxCpDepartmentServiceImpl(this); private WxCpMediaService mediaService = new WxCpMediaServiceImpl(this); private WxCpMenuService menuService = new WxCpMenuServiceImpl(this); private WxCpOAuth2Service oauth2Service = new WxCpOAuth2ServiceImpl(this); private WxCpTagService tagService = new WxCpTagServiceImpl(this); private WxCpAgentService agentService = new WxCpAgentServiceImpl(this); - private WxCpOaService oaService = new WxCpOaServiceImpl(this); - private WxCpTaskCardService taskCardService = new WxCpTaskCardServiceImpl(this); - private WxCpExternalContactService externalContactService = new WxCpExternalContactServiceImpl(this); + private final WxCpOaService oaService = new WxCpOaServiceImpl(this); + private final WxCpSchoolService schoolService = new WxCpSchoolServiceImpl(this); + private final WxCpSchoolUserService schoolUserService = new WxCpSchoolUserServiceImpl(this); + private final WxCpSchoolHealthService schoolHealthService = new WxCpSchoolHealthServiceImpl(this); + private final WxCpLivingService livingService = new WxCpLivingServiceImpl(this); + private final WxCpOaAgentService oaAgentService = new WxCpOaAgentServiceImpl(this); + private final WxCpOaWeDriveService oaWeDriveService = new WxCpOaWeDriveServiceImpl(this); + private final WxCpOaWeDocService oaWeDocService = new WxCpOaWeDocServiceImpl(this); + private final WxCpMsgAuditService msgAuditService = new WxCpMsgAuditServiceImpl(this); + private final WxCpTaskCardService taskCardService = new WxCpTaskCardServiceImpl(this); + private final WxCpExternalContactService externalContactService = new WxCpExternalContactServiceImpl(this); + private final WxCpGroupRobotService groupRobotService = new WxCpGroupRobotServiceImpl(this); + private final WxCpMessageService messageService = new WxCpMessageServiceImpl(this); + private final WxCpOaCalendarService oaCalendarService = new WxCpOaCalendarServiceImpl(this); + private final WxCpOaMeetingRoomService oaMeetingRoomService = new WxCpOaMeetingRoomServiceImpl(this); + private final WxCpOaScheduleService oaScheduleService = new WxCpOaOaScheduleServiceImpl(this); + private final WxCpAgentWorkBenchService workBenchService = new WxCpAgentWorkBenchServiceImpl(this); + private WxCpKfService kfService = new WxCpKfServiceImpl(this); + + private WxCpExportService exportService = new WxCpExportServiceImpl(this); + + private final WxCpMeetingService meetingService = new WxCpMeetingServiceImpl(this); + private final WxCpCorpGroupService corpGroupService = new WxCpCorpGroupServiceImpl(this); + private final WxCpIntelligentRobotService intelligentRobotService = new WxCpIntelligentRobotServiceImpl(this); + private final WxCpHrService hrService = new WxCpHrServiceImpl(this); /** * 全局的是否正在刷新access token的锁. @@ -68,6 +93,9 @@ public abstract class BaseWxCpServiceImpl implements WxCpService, RequestH */ protected final Object globalAgentJsapiTicketRefreshLock = new Object(); + /** + * The Config storage. + */ protected WxCpConfigStorage configStorage; private WxSessionManager sessionManager = new StandardSessionManager(); @@ -85,7 +113,7 @@ public boolean checkSignature(String msgSignature, String timestamp, String nonc return SHA1.gen(this.configStorage.getToken(), timestamp, nonce, data) .equals(msgSignature); } catch (Exception e) { - log.error("Checking signature failed, and the reason is :" + e.getMessage()); + log.error("Checking signature failed, and the reason is :{}", e.getMessage()); return false; } } @@ -110,7 +138,7 @@ public String getAgentJsapiTicket(boolean forceRefresh) throws WxErrorException synchronized (this.globalAgentJsapiTicketRefreshLock) { if (this.configStorage.isAgentJsapiTicketExpired()) { String responseContent = this.get(this.configStorage.getApiUrl(GET_AGENT_CONFIG_TICKET), null); - JsonObject jsonObject = new JsonParser().parse(responseContent).getAsJsonObject(); + JsonObject jsonObject = GsonParser.parse(responseContent); this.configStorage.updateAgentJsapiTicket(jsonObject.get("ticket").getAsString(), jsonObject.get("expires_in").getAsInt()); } @@ -135,7 +163,7 @@ public String getJsapiTicket(boolean forceRefresh) throws WxErrorException { synchronized (this.globalJsapiTicketRefreshLock) { if (this.configStorage.isJsapiTicketExpired()) { String responseContent = this.get(this.configStorage.getApiUrl(GET_JSAPI_TICKET), null); - JsonObject tmpJsonObject = new JsonParser().parse(responseContent).getAsJsonObject(); + JsonObject tmpJsonObject = GsonParser.parse(responseContent); this.configStorage.updateJsapiTicket(tmpJsonObject.get("ticket").getAsString(), tmpJsonObject.get("expires_in").getAsInt()); } @@ -169,13 +197,27 @@ public WxJsapiSignature createJsapiSignature(String url) throws WxErrorException } @Override - public WxCpMessageSendResult messageSend(WxCpMessage message) throws WxErrorException { - Integer agentId = message.getAgentId(); - if (null == agentId) { - message.setAgentId(this.getWxCpConfigStorage().getAgentId()); - } + public WxCpAgentJsapiSignature createAgentJsapiSignature(String url) throws WxErrorException { + long timestamp = System.currentTimeMillis() / 1000; + String noncestr = RandomUtils.getRandomStr(); + String jsapiTicket = getAgentJsapiTicket(false); + String signature = SHA1.genWithAmple( + "jsapi_ticket=" + jsapiTicket, + "noncestr=" + noncestr, + "timestamp=" + timestamp, + "url=" + url + ); - return WxCpMessageSendResult.fromJson(this.post(this.configStorage.getApiUrl(MESSAGE_SEND), message.toJson())); + WxCpAgentJsapiSignature jsapiSignature = new WxCpAgentJsapiSignature(); + jsapiSignature.setTimestamp(timestamp); + jsapiSignature.setNonceStr(noncestr); + jsapiSignature.setUrl(url); + jsapiSignature.setSignature(signature); + + jsapiSignature.setCorpid(this.configStorage.getCorpId()); + jsapiSignature.setAgentid(this.configStorage.getAgentId()); + + return jsapiSignature; } @Override @@ -190,9 +232,25 @@ public WxCpMaJsCode2SessionResult jsCode2Session(String jsCode) throws WxErrorEx @Override public String[] getCallbackIp() throws WxErrorException { - String responseContent = get(this.configStorage.getApiUrl(GET_CALLBACK_IP), null); - JsonElement tmpJsonElement = new JsonParser().parse(responseContent); - JsonArray jsonArray = tmpJsonElement.getAsJsonObject().get("ip_list").getAsJsonArray(); + return getIp(GET_CALLBACK_IP); + } + + @Override + public String[] getApiDomainIp() throws WxErrorException { + return getIp(GET_API_DOMAIN_IP); + } + + /** + * 获取 IP + * + * @param suffixUrl 接口URL 后缀 + * @return 返回结果 + * @throws WxErrorException 异常信息 + */ + private String[] getIp(String suffixUrl) throws WxErrorException { + String responseContent = get(this.configStorage.getApiUrl(suffixUrl), null); + JsonObject tmpJsonObject = GsonParser.parse(responseContent); + JsonArray jsonArray = tmpJsonObject.get("ip_list").getAsJsonArray(); String[] ips = new String[jsonArray.size()]; for (int i = 0; i < jsonArray.size(); i++) { ips[i] = jsonArray.get(i).getAsString(); @@ -205,7 +263,8 @@ public WxCpProviderToken getProviderToken(String corpId, String providerSecret) JsonObject jsonObject = new JsonObject(); jsonObject.addProperty("corpid", corpId); jsonObject.addProperty("provider_secret", providerSecret); - return WxCpProviderToken.fromJson(this.post(this.configStorage.getApiUrl(GET_PROVIDER_TOKEN), jsonObject.toString())); + return WxCpProviderToken.fromJson(this.post(this.configStorage.getApiUrl(Tp.GET_PROVIDER_TOKEN), + jsonObject.toString())); } @Override @@ -218,6 +277,42 @@ public String post(String url, String postData) throws WxErrorException { return execute(SimplePostRequestExecutor.create(this), url, postData); } + @Override + public String post(String url, JsonObject jsonObject) throws WxErrorException { + return this.post(url, jsonObject.toString()); + } + + @Override + public String post(String url, ToJson obj) throws WxErrorException { + return this.post(url, obj.toJson()); + } + + @Override + public String upload(String url, CommonUploadParam param) throws WxErrorException { + RequestExecutor executor = CommonUploadRequestExecutor.create(getRequestHttp()); + return this.execute(executor, url, param); + } + + @Override + public String post(String url, Object obj) throws WxErrorException { + return this.post(url, obj.toString()); + } + + @Override + public String postWithoutToken(String url, String postData) throws WxErrorException { + return this.executeNormal(SimplePostRequestExecutor.create(this), url, postData); + } + + @Override + public String postForMsgAudit(String url, String postData) throws WxErrorException { + // 获取会话存档专用的access token + String msgAuditAccessToken = getMsgAuditAccessToken(false); + // 拼接access_token参数 + String urlWithToken = url + (url.contains("?") ? "&" : "?") + "access_token=" + msgAuditAccessToken; + // 使用executeNormal方法,不自动添加token + return this.executeNormal(SimplePostRequestExecutor.create(this), urlWithToken, postData); + } + /** * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求. */ @@ -226,12 +321,12 @@ public T execute(RequestExecutor executor, String uri, E data) thro int retryTimes = 0; do { try { - return this.executeInternal(executor, uri, data); + return this.executeInternal(executor, uri, data, false); } catch (WxErrorException e) { if (retryTimes + 1 > this.maxRetryTimes) { log.warn("重试达到最大次数【{}】", this.maxRetryTimes); //最后一次重试失败后,直接抛出异常,不再等待 - throw new RuntimeException("微信服务端异常,超出重试次数"); + throw new WxRuntimeException("微信服务端异常,超出重试次数"); } WxError error = e.getError(); @@ -253,10 +348,22 @@ public T execute(RequestExecutor executor, String uri, E data) thro } while (retryTimes++ < this.maxRetryTimes); log.warn("重试达到最大次数【{}】", this.maxRetryTimes); - throw new RuntimeException("微信服务端异常,超出重试次数"); + throw new WxRuntimeException("微信服务端异常,超出重试次数"); } - protected T executeInternal(RequestExecutor executor, String uri, E data) throws WxErrorException { + /** + * Execute internal t. + * + * @param the type parameter + * @param the type parameter + * @param executor the executor + * @param uri the uri + * @param data the data + * @param doNotAutoRefresh the do not auto refresh + * @return the t + * @throws WxErrorException the wx error exception + */ + protected T executeInternal(RequestExecutor executor, String uri, E data, boolean doNotAutoRefresh) throws WxErrorException { E dataForLog = DataUtils.handleDataWithSecret(data); if (uri.contains("access_token=")) { @@ -272,26 +379,47 @@ protected T executeInternal(RequestExecutor executor, String uri, E return result; } catch (WxErrorException e) { WxError error = e.getError(); - /* - * 发生以下情况时尝试刷新access_token - * 40001 获取access_token时AppSecret错误,或者access_token无效 - * 42001 access_token超时 - * 40014 不合法的access_token,请开发者认真比对access_token的有效性(如是否过期),或查看是否正在为恰当的公众号调用接口 - */ - if (error.getErrorCode() == 42001 || error.getErrorCode() == 40001 || error.getErrorCode() == 40014) { + + if (WxConsts.ACCESS_TOKEN_ERROR_CODES.contains(error.getErrorCode())) { // 强制设置wxCpConfigStorage它的access token过期了,这样在下一次请求里就会刷新access token this.configStorage.expireAccessToken(); - return execute(executor, uri, data); + if (this.getWxCpConfigStorage().autoRefreshToken() && !doNotAutoRefresh) { + log.warn("即将重新获取新的access_token,错误代码:{},错误信息:{}", error.getErrorCode(), error.getErrorMsg()); + //下一次不再自动重试 + //当小程序误调用第三方平台专属接口时,第三方无法使用小程序的access token,如果可以继续自动获取token会导致无限循环重试,直到栈溢出 + return this.executeInternal(executor, uri, data, true); + } } if (error.getErrorCode() != 0) { - log.error("\n【请求地址】: {}\n【请求参数】:{}\n【错误信息】:{}", uriWithAccessToken, dataForLog, error); + log.warn("\n【请求地址】: {}\n【请求参数】:{}\n【错误信息】:{}", uriWithAccessToken, dataForLog, error); throw new WxErrorException(error, e); } return null; } catch (IOException e) { - log.error("\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uriWithAccessToken, dataForLog, e.getMessage()); - throw new RuntimeException(e); + log.warn("\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uriWithAccessToken, dataForLog, e.getMessage()); + throw new WxRuntimeException(e); + } + } + + /** + * 普通请求,不自动带accessToken + */ + private T executeNormal(RequestExecutor executor, String uri, E data) throws WxErrorException { + try { + T result = executor.execute(uri, data, WxType.CP); + log.debug("\n【请求地址】: {}\n【请求参数】:{}\n【响应数据】:{}", uri, data, result); + return result; + } catch (WxErrorException e) { + WxError error = e.getError(); + if (error.getErrorCode() != 0) { + log.error("\n【请求地址】: {}\n【请求参数】:{}\n【错误信息】:{}", uri, data, error); + throw new WxErrorException(error, e); + } + return null; + } catch (IOException e) { + log.error("\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uri, data, e.getMessage()); + throw new WxErrorException(e); } } @@ -345,6 +473,15 @@ public String replaceParty(String mediaId) throws WxErrorException { return post(this.configStorage.getApiUrl(BATCH_REPLACE_PARTY), jsonObject.toString()); } + @Override + public String syncUser(String mediaId) throws WxErrorException { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("media_id", mediaId); + String responseContent = post(this.configStorage.getApiUrl(BATCH_SYNC_USER), jsonObject.toString()); + JsonObject tmpJson = GsonParser.parse(responseContent); + return tmpJson.get("jobid").getAsString(); + } + @Override public String replaceUser(String mediaId) throws WxErrorException { JsonObject jsonObject = new JsonObject(); @@ -353,15 +490,33 @@ public String replaceUser(String mediaId) throws WxErrorException { } @Override - public String getTaskResult(String joinId) throws WxErrorException { - String url = this.configStorage.getApiUrl(BATCH_GET_RESULT + joinId); + public String getTaskResult(String jobId) throws WxErrorException { + String url = this.configStorage.getApiUrl(BATCH_GET_RESULT + jobId); return get(url, null); } + @Override + public String buildQrConnectUrl(String redirectUri, String state) { + return String.format("https://open.work.weixin.qq.com/wwopen/sso/qrConnect?appid=%s&agentid=%s&redirect_uri=%s" + + "&state=%s", + this.configStorage.getCorpId(), this.configStorage.getAgentId(), + URIUtil.encodeURIComponent(redirectUri), StringUtils.trimToEmpty(state)); + } + + /** + * Gets tmp dir file. + * + * @return the tmp dir file + */ public File getTmpDirFile() { return this.tmpDirFile; } + /** + * Sets tmp dir file. + * + * @param tmpDirFile the tmp dir file + */ public void setTmpDirFile(File tmpDirFile) { this.tmpDirFile = tmpDirFile; } @@ -407,10 +562,70 @@ public WxCpChatService getChatService() { } @Override - public WxCpOaService getOAService() { + public WxCpOaService getOaService() { return oaService; } + @Override + public WxCpSchoolService getSchoolService() { + return schoolService; + } + + @Override + public WxCpSchoolUserService getSchoolUserService() { + return schoolUserService; + } + + @Override + public WxCpSchoolHealthService getSchoolHealthService() { + return schoolHealthService; + } + + @Override + public WxCpLivingService getLivingService() { + return livingService; + } + + @Override + public WxCpOaAgentService getOaAgentService() { + return oaAgentService; + } + + @Override + public WxCpOaWeDriveService getOaWeDriveService() { + return oaWeDriveService; + } + + @Override + public WxCpOaWeDocService getOaWeDocService() { + return oaWeDocService; + } + + @Override + public WxCpMsgAuditService getMsgAuditService() { + return msgAuditService; + } + + @Override + public WxCpOaCalendarService getOaCalendarService() { + return this.oaCalendarService; + } + + @Override + public WxCpOaMeetingRoomService getOaMeetingRoomService() { + return this.oaMeetingRoomService; + } + + @Override + public WxCpGroupRobotService getGroupRobotService() { + return groupRobotService; + } + + @Override + public WxCpAgentWorkBenchService getWorkBenchService() { + return workBenchService; + } + @Override public WxCpTaskCardService getTaskCardService() { return taskCardService; @@ -456,7 +671,63 @@ public WxCpAgentService getAgentService() { return agentService; } + @Override + public WxCpMessageService getMessageService() { + return this.messageService; + } + + /** + * Sets agent service. + * + * @param agentService the agent service + */ public void setAgentService(WxCpAgentService agentService) { this.agentService = agentService; } + + @Override + public WxCpOaScheduleService getOaScheduleService() { + return this.oaScheduleService; + } + + @Override + public WxCpKfService getKfService() { + return kfService; + } + + @Override + public void setKfService(WxCpKfService kfService) { + this.kfService = kfService; + } + + + @Override + public WxCpExportService getExportService() { + return exportService; + } + + @Override + public void setExportService(WxCpExportService exportService) { + this.exportService = exportService; + } + + @Override + public WxCpMeetingService getMeetingService() { + return meetingService; + } + + @Override + public WxCpCorpGroupService getCorpGroupService() { + return corpGroupService; + } + + @Override + public WxCpIntelligentRobotService getIntelligentRobotService() { + return this.intelligentRobotService; + } + + @Override + public WxCpHrService getHrService() { + return this.hrService; + } } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpTpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpTpServiceImpl.java deleted file mode 100644 index fe8e3c08e3..0000000000 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpTpServiceImpl.java +++ /dev/null @@ -1,241 +0,0 @@ -package me.chanjar.weixin.cp.api.impl; - -import com.google.common.base.Joiner; -import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import lombok.extern.slf4j.Slf4j; -import me.chanjar.weixin.common.WxType; -import me.chanjar.weixin.common.bean.WxAccessToken; -import me.chanjar.weixin.common.error.WxError; -import me.chanjar.weixin.common.error.WxErrorException; -import me.chanjar.weixin.common.util.DataUtils; -import me.chanjar.weixin.common.util.crypto.SHA1; -import me.chanjar.weixin.common.util.http.RequestExecutor; -import me.chanjar.weixin.common.util.http.RequestHttp; -import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor; -import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor; -import me.chanjar.weixin.cp.api.WxCpTpService; -import me.chanjar.weixin.cp.bean.WxCpMaJsCode2SessionResult; -import me.chanjar.weixin.cp.bean.WxCpTpCorp; -import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; - -import java.io.File; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Tp.*; - -/** - * . - * - * @author zhenjun cai - */ -@Slf4j -public abstract class BaseWxCpTpServiceImpl implements WxCpTpService, RequestHttp { - - /** - * 全局的是否正在刷新access token的锁. - */ - protected final Object globalSuiteAccessTokenRefreshLock = new Object(); - - /** - * 全局的是否正在刷新jsapi_ticket的锁. - */ - protected final Object globalSuiteTicketRefreshLock = new Object(); - - protected WxCpTpConfigStorage configStorage; - - /** - * 临时文件目录. - */ - private File tmpDirFile; - private int retrySleepMillis = 1000; - private int maxRetryTimes = 5; - - @Override - public boolean checkSignature(String msgSignature, String timestamp, String nonce, String data) { - try { - return SHA1.gen(this.configStorage.getToken(), timestamp, nonce, data) - .equals(msgSignature); - } catch (Exception e) { - log.error("Checking signature failed, and the reason is :" + e.getMessage()); - return false; - } - } - - @Override - public String getSuiteAccessToken() throws WxErrorException { - return getSuiteAccessToken(false); - } - - @Override - public String getSuiteTicket() throws WxErrorException { - return getSuiteTicket(false); - } - - @Override - public String getSuiteTicket(boolean forceRefresh) throws WxErrorException { -// suite ticket由微信服务器推送,不能强制刷新 -// if (forceRefresh) { -// this.configStorage.expireSuiteTicket(); -// } - - if (this.configStorage.isSuiteTicketExpired()) { - // 本地suite ticket 不存在或者过期 - WxError wxError = WxError.fromJson("{\"errcode\":40085, \"errmsg\":\"invaild suite ticket\"}", WxType.CP); - throw new WxErrorException(wxError); - } - return this.configStorage.getSuiteTicket(); - } - - - @Override - public WxCpMaJsCode2SessionResult jsCode2Session(String jsCode) throws WxErrorException { - Map params = new HashMap<>(2); - params.put("js_code", jsCode); - params.put("grant_type", "authorization_code"); - - final String url = configStorage.getApiUrl(JSCODE_TO_SESSION); - return WxCpMaJsCode2SessionResult.fromJson(this.get(url, Joiner.on("&").withKeyValueSeparator("=").join(params))); - } - - - @Override - public WxAccessToken getCorpToken(String authCorpid, String permanentCode) throws WxErrorException { - JsonObject jsonObject = new JsonObject(); - jsonObject.addProperty("auth_corpid", authCorpid); - jsonObject.addProperty("permanent_code", permanentCode); - String result = post(configStorage.getApiUrl(GET_CORP_TOKEN), jsonObject.toString()); - - return WxAccessToken.fromJson(result); - } - - @Override - public WxCpTpCorp getPermanentCode(String authCode) throws WxErrorException { - JsonObject jsonObject = new JsonObject(); - jsonObject.addProperty("auth_code", authCode); - - String result = post(configStorage.getApiUrl(GET_PERMANENT_CODE), jsonObject.toString()); - jsonObject = new JsonParser().parse(result).getAsJsonObject(); - WxCpTpCorp wxCpTpCorp = WxCpTpCorp.fromJson(jsonObject.get("auth_corp_info").getAsJsonObject().toString()); - wxCpTpCorp.setPermanentCode(jsonObject.get("permanent_code").getAsString()); - return wxCpTpCorp; - } - - @Override - public String get(String url, String queryParam) throws WxErrorException { - return execute(SimpleGetRequestExecutor.create(this), url, queryParam); - } - - @Override - public String post(String url, String postData) throws WxErrorException { - return execute(SimplePostRequestExecutor.create(this), url, postData); - } - - /** - * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求. - */ - @Override - public T execute(RequestExecutor executor, String uri, E data) throws WxErrorException { - int retryTimes = 0; - do { - try { - return this.executeInternal(executor, uri, data); - } catch (WxErrorException e) { - if (retryTimes + 1 > this.maxRetryTimes) { - log.warn("重试达到最大次数【{}】", this.maxRetryTimes); - //最后一次重试失败后,直接抛出异常,不再等待 - throw new RuntimeException("微信服务端异常,超出重试次数"); - } - - WxError error = e.getError(); - /* - * -1 系统繁忙, 1000ms后重试 - */ - if (error.getErrorCode() == -1) { - int sleepMillis = this.retrySleepMillis * (1 << retryTimes); - try { - log.debug("微信系统繁忙,{} ms 后重试(第{}次)", sleepMillis, retryTimes + 1); - Thread.sleep(sleepMillis); - } catch (InterruptedException e1) { - Thread.currentThread().interrupt(); - } - } else { - throw e; - } - } - } while (retryTimes++ < this.maxRetryTimes); - - log.warn("重试达到最大次数【{}】", this.maxRetryTimes); - throw new RuntimeException("微信服务端异常,超出重试次数"); - } - - protected T executeInternal(RequestExecutor executor, String uri, E data) throws WxErrorException { - E dataForLog = DataUtils.handleDataWithSecret(data); - - if (uri.contains("suite_access_token=")) { - throw new IllegalArgumentException("uri参数中不允许有suite_access_token: " + uri); - } - String suiteAccessToken = getSuiteAccessToken(false); - - String uriWithAccessToken = uri + (uri.contains("?") ? "&" : "?") + "suite_access_token=" + suiteAccessToken; - - try { - T result = executor.execute(uriWithAccessToken, data, WxType.CP); - log.debug("\n【请求地址】: {}\n【请求参数】:{}\n【响应数据】:{}", uriWithAccessToken, dataForLog, result); - return result; - } catch (WxErrorException e) { - WxError error = e.getError(); - /* - * 发生以下情况时尝试刷新suite_access_token - * 42009 suite_access_token已过期 - */ - if (error.getErrorCode() == 42009) { - // 强制设置wxCpTpConfigStorage它的suite access token过期了,这样在下一次请求里就会刷新suite access token - this.configStorage.expireSuiteAccessToken(); - return execute(executor, uri, data); - } - - if (error.getErrorCode() != 0) { - log.error("\n【请求地址】: {}\n【请求参数】:{}\n【错误信息】:{}", uriWithAccessToken, dataForLog, error); - throw new WxErrorException(error, e); - } - return null; - } catch (IOException e) { - log.error("\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uriWithAccessToken, dataForLog, e.getMessage()); - throw new RuntimeException(e); - } - } - - @Override - public void setWxCpTpConfigStorage(WxCpTpConfigStorage wxConfigProvider) { - this.configStorage = wxConfigProvider; - this.initHttp(); - } - - @Override - public void setRetrySleepMillis(int retrySleepMillis) { - this.retrySleepMillis = retrySleepMillis; - } - - - @Override - public void setMaxRetryTimes(int maxRetryTimes) { - this.maxRetryTimes = maxRetryTimes; - } - - public File getTmpDirFile() { - return this.tmpDirFile; - } - - public void setTmpDirFile(File tmpDirFile) { - this.tmpDirFile = tmpDirFile; - } - - @Override - public RequestHttp getRequestHttp() { - return this; - } - -} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpAgentServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpAgentServiceImpl.java index 94aa977b25..cc08d33bb1 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpAgentServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpAgentServiceImpl.java @@ -1,15 +1,17 @@ package me.chanjar.weixin.cp.api.impl; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; import com.google.gson.reflect.TypeToken; import lombok.RequiredArgsConstructor; -import me.chanjar.weixin.common.WxType; +import me.chanjar.weixin.common.api.WxConsts; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.json.GsonParser; import me.chanjar.weixin.cp.api.WxCpAgentService; import me.chanjar.weixin.cp.api.WxCpService; import me.chanjar.weixin.cp.bean.WxCpAgent; +import me.chanjar.weixin.cp.bean.WxCpTpAdmin; import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; import java.util.List; @@ -27,7 +29,7 @@ */ @RequiredArgsConstructor public class WxCpAgentServiceImpl implements WxCpAgentService { - private static final JsonParser JSON_PARSER = new JsonParser(); + private final WxCpService mainService; @@ -45,8 +47,8 @@ public WxCpAgent get(Integer agentId) throws WxErrorException { public void set(WxCpAgent agentInfo) throws WxErrorException { String url = this.mainService.getWxCpConfigStorage().getApiUrl(AGENT_SET); String responseContent = this.mainService.post(url, agentInfo.toJson()); - JsonObject jsonObject = JSON_PARSER.parse(responseContent).getAsJsonObject(); - if (jsonObject.get("errcode").getAsInt() != 0) { + JsonObject jsonObject = GsonParser.parse(responseContent); + if (jsonObject.get(WxConsts.ERR_CODE).getAsInt() != 0) { throw new WxErrorException(WxError.fromJson(responseContent, WxType.CP)); } } @@ -55,8 +57,8 @@ public void set(WxCpAgent agentInfo) throws WxErrorException { public List list() throws WxErrorException { String url = this.mainService.getWxCpConfigStorage().getApiUrl(AGENT_LIST); String responseContent = this.mainService.get(url, null); - JsonObject jsonObject = JSON_PARSER.parse(responseContent).getAsJsonObject(); - if (jsonObject.get("errcode").getAsInt() != 0) { + JsonObject jsonObject = GsonParser.parse(responseContent); + if (jsonObject.get(WxConsts.ERR_CODE).getAsInt() != 0) { throw new WxErrorException(WxError.fromJson(responseContent, WxType.CP)); } @@ -64,4 +66,21 @@ public List list() throws WxErrorException { }.getType()); } + @Override + public WxCpTpAdmin getAdminList(Integer agentId) throws WxErrorException { + if (agentId == null) { + throw new IllegalArgumentException("缺少agentid参数"); + } + + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("agentid", agentId); + String url = this.mainService.getWxCpConfigStorage().getApiUrl(AGENT_GET_ADMIN_LIST); + String responseContent = this.mainService.post(url, jsonObject.toString()); + JsonObject respObj = GsonParser.parse(responseContent); + if (respObj.get(WxConsts.ERR_CODE).getAsInt() != 0) { + throw new WxErrorException(WxError.fromJson(responseContent, WxType.CP)); + } + return WxCpTpAdmin.fromJson(responseContent); + } + } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpAgentWorkBenchServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpAgentWorkBenchServiceImpl.java new file mode 100644 index 0000000000..b0bbb38642 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpAgentWorkBenchServiceImpl.java @@ -0,0 +1,47 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.gson.JsonObject; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.api.WxCpAgentWorkBenchService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.WxCpAgentWorkBench; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.WorkBench.*; + +/** + * 工作台自定义展示实现 + * + * @author songshiyu + * created at 11:24 2020/9/28 + */ +@RequiredArgsConstructor +public class WxCpAgentWorkBenchServiceImpl implements WxCpAgentWorkBenchService { + private final WxCpService mainService; + + @Override + public void setWorkBenchTemplate(WxCpAgentWorkBench wxCpAgentWorkBench) throws WxErrorException { + final String url = String.format(this.mainService.getWxCpConfigStorage().getApiUrl(WORKBENCH_TEMPLATE_SET)); + this.mainService.post(url, wxCpAgentWorkBench.toTemplateString()); + } + + @Override + public String getWorkBenchTemplate(Long agentId) throws WxErrorException { + final String url = String.format(this.mainService.getWxCpConfigStorage().getApiUrl(WORKBENCH_TEMPLATE_GET)); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("agentid", agentId); + return this.mainService.post(url, jsonObject.toString()); + } + + @Override + public void setWorkBenchData(WxCpAgentWorkBench wxCpAgentWorkBench) throws WxErrorException { + final String url = String.format(this.mainService.getWxCpConfigStorage().getApiUrl(WORKBENCH_DATA_SET)); + this.mainService.post(url, wxCpAgentWorkBench.toUserDataString()); + } + + @Override + public void batchSetWorkBenchData(WxCpAgentWorkBench wxCpAgentWorkBench) throws WxErrorException { + final String url = String.format(this.mainService.getWxCpConfigStorage().getApiUrl(WORKBENCH_BATCH_DATA_SET)); + this.mainService.post(url, wxCpAgentWorkBench.toBatchUserDataString()); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpChatServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpChatServiceImpl.java index b1f374c071..c47785f6e5 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpChatServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpChatServiceImpl.java @@ -1,14 +1,13 @@ package me.chanjar.weixin.cp.api.impl; -import com.google.gson.JsonParser; import lombok.RequiredArgsConstructor; import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.json.GsonParser; import me.chanjar.weixin.common.util.json.WxGsonBuilder; import me.chanjar.weixin.cp.api.WxCpChatService; import me.chanjar.weixin.cp.api.WxCpService; -import me.chanjar.weixin.cp.bean.WxCpAppChatMessage; import me.chanjar.weixin.cp.bean.WxCpChat; -import me.chanjar.weixin.cp.constant.WxCpApiPathConsts; +import me.chanjar.weixin.cp.bean.message.WxCpAppChatMessage; import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; import org.apache.commons.lang3.StringUtils; @@ -25,11 +24,10 @@ */ @RequiredArgsConstructor public class WxCpChatServiceImpl implements WxCpChatService { - private static final JsonParser JSON_PARSER = new JsonParser(); private final WxCpService cpService; @Override - public String chatCreate(String name, String owner, List users, String chatId) throws WxErrorException { + public String create(String name, String owner, List users, String chatId) throws WxErrorException { Map data = new HashMap<>(4); if (StringUtils.isNotBlank(name)) { data.put("name", name); @@ -45,16 +43,11 @@ public String chatCreate(String name, String owner, List users, String c } final String url = this.cpService.getWxCpConfigStorage().getApiUrl(APPCHAT_CREATE); String result = this.cpService.post(url, WxGsonBuilder.create().toJson(data)); - return new JsonParser().parse(result).getAsJsonObject().get("chatid").getAsString(); - } - - @Override - public String create(String name, String owner, List users, String chatId) throws WxErrorException { - return this.chatCreate(name, owner, users, chatId); + return GsonParser.parse(result).get("chatid").getAsString(); } @Override - public void chatUpdate(String chatId, String name, String owner, List usersToAdd, List usersToDelete) + public void update(String chatId, String name, String owner, List usersToAdd, List usersToDelete) throws WxErrorException { Map data = new HashMap<>(5); if (StringUtils.isNotBlank(chatId)) { @@ -78,24 +71,13 @@ public void chatUpdate(String chatId, String name, String owner, List us } @Override - public void update(String chatId, String name, String owner, List usersToAdd, List usersToDelete) - throws WxErrorException { - chatUpdate(chatId, name, owner, usersToAdd, usersToDelete); - } - - @Override - public WxCpChat chatGet(String chatId) throws WxErrorException { + public WxCpChat get(String chatId) throws WxErrorException { final String url = this.cpService.getWxCpConfigStorage().getApiUrl(APPCHAT_GET_CHATID + chatId); String result = this.cpService.get(url, null); - final String chatInfo = JSON_PARSER.parse(result).getAsJsonObject().getAsJsonObject("chat_info").toString(); + final String chatInfo = GsonParser.parse(result).getAsJsonObject("chat_info").toString(); return WxCpGsonBuilder.create().fromJson(chatInfo, WxCpChat.class); } - @Override - public WxCpChat get(String chatId) throws WxErrorException { - return this.chatGet(chatId); - } - @Override public void sendMsg(WxCpAppChatMessage message) throws WxErrorException { this.cpService.post(this.cpService.getWxCpConfigStorage().getApiUrl(APPCHAT_SEND), message.toJson()); diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpCorpGroupServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpCorpGroupServiceImpl.java new file mode 100644 index 0000000000..e3dc1cbe1c --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpCorpGroupServiceImpl.java @@ -0,0 +1,45 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.json.GsonParser; +import me.chanjar.weixin.cp.api.WxCpCorpGroupService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.corpgroup.WxCpCorpGroupCorp; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.util.List; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.CorpGroup.LIST_SHARE_APP_INFO; + +/** + * 企业互联相关接口实现类 + * + * @author libo <422423229@qq.com> + * @since 2023-02-27 9:57 PM + */ +@RequiredArgsConstructor +public class WxCpCorpGroupServiceImpl implements WxCpCorpGroupService { + private final WxCpService cpService; + + @Override + public List listAppShareInfo(Integer agentId, Integer businessType, String corpId, + Integer limit, String cursor) throws WxErrorException { + final String url = this.cpService.getWxCpConfigStorage().getApiUrl(LIST_SHARE_APP_INFO); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("agentid", agentId); + jsonObject.addProperty("corpid", corpId); + jsonObject.addProperty("business_type", businessType); + jsonObject.addProperty("limit", limit); + jsonObject.addProperty("cursor", cursor); + String responseContent = this.cpService.post(url, jsonObject); + JsonObject tmpJson = GsonParser.parse(responseContent); + + return WxCpGsonBuilder.create().fromJson(tmpJson.get("corp_list"), + new TypeToken>() { + }.getType() + ); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpDepartmentServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpDepartmentServiceImpl.java index fb2224e335..b6d9cf29b1 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpDepartmentServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpDepartmentServiceImpl.java @@ -1,15 +1,14 @@ package me.chanjar.weixin.cp.api.impl; -import com.google.gson.JsonElement; -import com.google.gson.JsonParser; +import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; import lombok.RequiredArgsConstructor; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.util.json.GsonHelper; +import me.chanjar.weixin.common.util.json.GsonParser; import me.chanjar.weixin.cp.api.WxCpDepartmentService; import me.chanjar.weixin.cp.api.WxCpService; import me.chanjar.weixin.cp.bean.WxCpDepart; -import me.chanjar.weixin.cp.constant.WxCpApiPathConsts; import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; import java.util.List; @@ -32,8 +31,20 @@ public class WxCpDepartmentServiceImpl implements WxCpDepartmentService { public Long create(WxCpDepart depart) throws WxErrorException { String url = this.mainService.getWxCpConfigStorage().getApiUrl(DEPARTMENT_CREATE); String responseContent = this.mainService.post(url, depart.toJson()); - JsonElement tmpJsonElement = new JsonParser().parse(responseContent); - return GsonHelper.getAsLong(tmpJsonElement.getAsJsonObject().get("id")); + JsonObject tmpJsonObject = GsonParser.parse(responseContent); + return GsonHelper.getAsLong(tmpJsonObject.get("id")); + } + + @Override + public WxCpDepart get(Long id) throws WxErrorException { + String url = String.format(this.mainService.getWxCpConfigStorage().getApiUrl(DEPARTMENT_GET), id); + String responseContent = this.mainService.get(url, null); + JsonObject tmpJsonObject = GsonParser.parse(responseContent); + return WxCpGsonBuilder.create() + .fromJson(tmpJsonObject.get("department"), + new TypeToken() { + }.getType() + ); } @Override @@ -56,9 +67,25 @@ public List list(Long id) throws WxErrorException { } String responseContent = this.mainService.get(url, null); - JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + JsonObject tmpJsonObject = GsonParser.parse(responseContent); + return WxCpGsonBuilder.create() + .fromJson(tmpJsonObject.get("department"), + new TypeToken>() { + }.getType() + ); + } + + @Override + public List simpleList(Long id) throws WxErrorException { + String url = this.mainService.getWxCpConfigStorage().getApiUrl(DEPARTMENT_SIMPLE_LIST); + if (id != null) { + url += "?id=" + id; + } + + String responseContent = this.mainService.get(url, null); + JsonObject tmpJsonObject = GsonParser.parse(responseContent); return WxCpGsonBuilder.create() - .fromJson(tmpJsonElement.getAsJsonObject().get("department"), + .fromJson(tmpJsonObject.get("department_id"), new TypeToken>() { }.getType() ); diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpExportServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpExportServiceImpl.java new file mode 100644 index 0000000000..638dd4e1c3 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpExportServiceImpl.java @@ -0,0 +1,58 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.gson.JsonObject; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.json.GsonParser; +import me.chanjar.weixin.cp.api.WxCpExportService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.export.WxCpExportRequest; +import me.chanjar.weixin.cp.bean.export.WxCpExportResult; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Export.*; + +/** + * 异步导出接口 + * + * @author zhongjun created on 2022/4/21 + */ +@RequiredArgsConstructor +public class WxCpExportServiceImpl implements WxCpExportService { + + private final WxCpService mainService; + + @Override + public String simpleUser(WxCpExportRequest params) throws WxErrorException { + return export(SIMPLE_USER, params); + } + + @Override + public String user(WxCpExportRequest params) throws WxErrorException { + return export(USER, params); + } + + @Override + public String department(WxCpExportRequest params) throws WxErrorException { + return export(DEPARTMENT, params); + } + + @Override + public String tagUser(WxCpExportRequest params) throws WxErrorException { + return export(TAG_USER, params); + } + + @Override + public WxCpExportResult getResult(String jobId) throws WxErrorException { + String url = String.format(this.mainService.getWxCpConfigStorage().getApiUrl(GET_RESULT), jobId); + String responseContent = this.mainService.get(url, null); + return WxCpGsonBuilder.create().fromJson(responseContent, WxCpExportResult.class); + } + + private String export(String path, WxCpExportRequest params) throws WxErrorException { + String url = this.mainService.getWxCpConfigStorage().getApiUrl(path); + String responseContent = this.mainService.post(url, params.toJson()); + JsonObject tmpJson = GsonParser.parse(responseContent); + return tmpJson.get("jobid").getAsString(); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpExternalContactServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpExternalContactServiceImpl.java index 8a5b7d56e8..d43589595f 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpExternalContactServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpExternalContactServiceImpl.java @@ -1,41 +1,919 @@ package me.chanjar.weixin.cp.api.impl; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.ExternalContact.*; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.UUID; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; +import me.chanjar.weixin.common.error.WxCpErrorMsgEnum; import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.error.WxRuntimeException; +import me.chanjar.weixin.common.util.BeanUtils; +import me.chanjar.weixin.common.util.fs.FileUtils; +import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor; +import me.chanjar.weixin.common.util.json.GsonHelper; +import me.chanjar.weixin.common.util.json.GsonParser; import me.chanjar.weixin.cp.api.WxCpExternalContactService; import me.chanjar.weixin.cp.api.WxCpService; -import me.chanjar.weixin.cp.bean.WxCpUserExternalContactInfo; -import me.chanjar.weixin.cp.bean.WxCpUserExternalContactList; -import me.chanjar.weixin.cp.bean.WxCpUserWithExternalPermission; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; +import me.chanjar.weixin.cp.bean.external.*; +import me.chanjar.weixin.cp.bean.external.acquisition.*; +import me.chanjar.weixin.cp.bean.external.contact.*; +import me.chanjar.weixin.cp.bean.external.interceptrule.WxCpInterceptRule; +import me.chanjar.weixin.cp.bean.external.interceptrule.WxCpInterceptRuleAddRequest; +import me.chanjar.weixin.cp.bean.external.interceptrule.WxCpInterceptRuleInfo; +import me.chanjar.weixin.cp.bean.external.interceptrule.WxCpInterceptRuleList; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; -import java.util.List; +/** + * The type Wx cp external contact service. + * + * @author 曹祖鹏, yuanqixun, Mr.Pan, Wang_Wong + */ +@RequiredArgsConstructor +public class WxCpExternalContactServiceImpl implements WxCpExternalContactService { + private final WxCpService mainService; -import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.ExternalContact.*; + @Override + public WxCpContactWayResult addContactWay(WxCpContactWayInfo info) throws WxErrorException { -public class WxCpExternalContactServiceImpl implements WxCpExternalContactService { - private WxCpService mainService; + if (info.getContactWay().getUsers() != null && info.getContactWay().getUsers().size() > 100) { + throw new WxRuntimeException("「联系我」使用人数默认限制不超过100人(包括部门展开后的人数)"); + } + + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(ADD_CONTACT_WAY); - public WxCpExternalContactServiceImpl(WxCpService mainService) { - this.mainService = mainService; + return WxCpContactWayResult.fromJson(this.mainService.post(url, info.getContactWay().toJson())); } @Override - public WxCpUserExternalContactInfo getExternalContact(String userId) throws WxErrorException { - final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_EXTERNAL_CONTACT + userId); - String responseContent = this.mainService.get(url, null); - return WxCpUserExternalContactInfo.fromJson(responseContent); + public WxCpContactWayInfo getContactWay(String configId) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("config_id", configId); + + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_CONTACT_WAY); + return WxCpContactWayInfo.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpContactWayList listContactWay(Long startTime, Long endTime, String cursor, Long limit) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("start_time", startTime); + json.addProperty("end_time", endTime); + json.addProperty("cursor", cursor); + json.addProperty("limit", limit); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(LIST_CONTACT_WAY); + return WxCpContactWayList.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpBaseResp updateContactWay(WxCpContactWayInfo info) throws WxErrorException { + if (StringUtils.isBlank(info.getContactWay().getConfigId())) { + throw new WxRuntimeException("更新「联系我」方式需要指定configId"); + } + if (info.getContactWay().getUsers() != null && info.getContactWay().getUsers().size() > 100) { + throw new WxRuntimeException("「联系我」使用人数默认限制不超过100人(包括部门展开后的人数)"); + } + + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(UPDATE_CONTACT_WAY); + + return WxCpBaseResp.fromJson(this.mainService.post(url, info.getContactWay().toJson())); + } + + @Override + public WxCpBaseResp deleteContactWay(String configId) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("config_id", configId); + + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(DEL_CONTACT_WAY); + + return WxCpBaseResp.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpBaseResp closeTempChat(String userId, String externalUserId) throws WxErrorException { + + JsonObject json = new JsonObject(); + json.addProperty("userid", userId); + json.addProperty("external_userid", externalUserId); + + + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(CLOSE_TEMP_CHAT); + + return WxCpBaseResp.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpExternalContactInfo getExternalContact(String externalUserId) throws WxErrorException { + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_EXTERNAL_CONTACT + externalUserId); + return WxCpExternalContactInfo.fromJson(this.mainService.get(url, null)); + } + + @Override + public WxCpExternalContactInfo getContactDetail(String externalUserId, String cursor) throws WxErrorException { + String params = externalUserId; + if (StringUtils.isNotEmpty(cursor)) { + params = params + "&cursor=" + cursor; + } + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_CONTACT_DETAIL + params); + return WxCpExternalContactInfo.fromJson(this.mainService.get(url, null)); + } + + @Override + public String convertToOpenid(String externalUserId) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("external_userid", externalUserId); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(CONVERT_TO_OPENID); + String responseContent = this.mainService.post(url, json.toString()); + return GsonParser.parse(responseContent).get("openid").getAsString(); + } + + @Override + public String unionidToExternalUserid(String unionid, String openid) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("unionid", unionid); + if (StringUtils.isNotEmpty(openid)) { + json.addProperty("openid", openid); + } + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(UNIONID_TO_EXTERNAL_USERID); + String responseContent = this.mainService.post(url, json.toString()); + return GsonParser.parse(responseContent).get("external_userid").getAsString(); + } + + @Override + public String toServiceExternalUserid(String externalUserid) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("external_userid", externalUserid); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(TO_SERVICE_EXTERNAL_USERID); + String responseContent = this.mainService.post(url, json.toString()); + return GsonParser.parse(responseContent).get("external_userid").getAsString(); + } + + @Override + public String fromServiceExternalUserid(String externalUserid, String sourceAgentId) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("external_userid", externalUserid); + json.addProperty("source_agentid", sourceAgentId); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(FROM_SERVICE_EXTERNAL_USERID); + String responseContent = this.mainService.post(url, json.toString()); + return GsonParser.parse(responseContent).get("external_userid").getAsString(); + } + + @Override + public WxCpExternalUserIdList unionidToExternalUserid3rd(String unionid, String openid, + String corpid) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("unionid", unionid); + json.addProperty("openid", openid); + if (StringUtils.isNotEmpty(corpid)) { + json.addProperty("corpid", corpid); + } + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(UNIONID_TO_EXTERNAL_USERID_3RD); + return WxCpExternalUserIdList.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpNewExternalUserIdList getNewExternalUserId(String[] externalUserIdList) throws WxErrorException { + JsonObject json = new JsonObject(); + if (ArrayUtils.isNotEmpty(externalUserIdList)) { + json.add("external_userid_list", new Gson().toJsonTree(externalUserIdList).getAsJsonArray()); + } + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_NEW_EXTERNAL_USERID); + return WxCpNewExternalUserIdList.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpBaseResp finishExternalUserIdMigration(String corpid) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("corpid", corpid); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(FINISH_EXTERNAL_USERID_MIGRATION); + return WxCpBaseResp.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public String opengidToChatid(String opengid) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("opengid", opengid); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(OPENID_TO_CHATID); + String responseContent = this.mainService.post(url, json.toString()); + return GsonParser.parse(responseContent).get("chat_id").getAsString(); + } + + @Override + public WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String cursor, Integer limit) + throws WxErrorException { + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_CONTACT_DETAIL_BATCH); + JsonObject json = new JsonObject(); + json.add("userid_list", new Gson().toJsonTree(userIdList).getAsJsonArray()); + if (StringUtils.isNotBlank(cursor)) { + json.addProperty("cursor", cursor); + } + if (limit != null) { + json.addProperty("limit", limit); + } + String responseContent = this.mainService.post(url, json.toString()); + return WxCpExternalContactBatchInfo.fromJson(responseContent); + } + + @Override + public WxCpExternalContactListInfo getContactList(String cursor, Integer limit) throws WxErrorException { + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_CONTACT_LIST); + JsonObject json = new JsonObject(); + if (StringUtils.isNotBlank(cursor)) { + json.addProperty("cursor", cursor); + } + if (limit != null) { + json.addProperty("limit", limit); + } + String responseContent = this.mainService.post(url, json.toString()); + return WxCpExternalContactListInfo.fromJson(responseContent); + } + + @Override + public void updateRemark(WxCpUpdateRemarkRequest request) throws WxErrorException { + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(UPDATE_REMARK); + this.mainService.post(url, request.toJson()); } @Override public List listExternalContacts(String userId) throws WxErrorException { final String url = this.mainService.getWxCpConfigStorage().getApiUrl(LIST_EXTERNAL_CONTACT + userId); - String responseContent = this.mainService.get(url, null); - return WxCpUserExternalContactList.fromJson(responseContent).getExternalUserId(); + try { + String responseContent = this.mainService.get(url, null); + return WxCpUserExternalContactList.fromJson(responseContent).getExternalUserId(); + } catch (WxErrorException e) { + // not external contact,无客户则返回空列表 + if (e.getError().getErrorCode() == WxCpErrorMsgEnum.CODE_84061.getCode()) { + return Collections.emptyList(); + } + throw e; + } } @Override - public List listFollowUser() throws WxErrorException { + public List listFollowers() throws WxErrorException { final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_FOLLOW_USER_LIST); String responseContent = this.mainService.get(url, null); - return WxCpUserWithExternalPermission.fromJson(responseContent).getFollowUser(); + return WxCpUserWithExternalPermission.fromJson(responseContent).getFollowers(); + } + + @Override + public WxCpUserExternalUnassignList listUnassignedList(Integer pageIndex, String cursor, Integer pageSize) throws WxErrorException { + JsonObject json = new JsonObject(); + if (pageIndex != null) { + json.addProperty("page_id", pageIndex); + } + json.addProperty("cursor", StringUtils.isEmpty(cursor) ? "" : cursor); + json.addProperty("page_size", pageSize == null ? 1000 : pageSize); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(LIST_UNASSIGNED_CONTACT); + return WxCpUserExternalUnassignList.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpBaseResp transferExternalContact(String externalUserid, String handOverUserid, String takeOverUserid) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("external_userid", externalUserid); + json.addProperty("handover_userid", handOverUserid); + json.addProperty("takeover_userid", takeOverUserid); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(TRANSFER_UNASSIGNED_CONTACT); + return WxCpBaseResp.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpUserTransferCustomerResp transferCustomer(WxCpUserTransferCustomerReq req) throws WxErrorException { + BeanUtils.checkRequiredFields(req); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(TRANSFER_CUSTOMER); + final String result = this.mainService.post(url, req.toJson()); + return WxCpUserTransferCustomerResp.fromJson(result); + } + + @Override + public WxCpUserTransferResultResp transferResult(String handOverUserid, String takeOverUserid, + String cursor) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("cursor", cursor); + json.addProperty("handover_userid", handOverUserid); + json.addProperty("takeover_userid", takeOverUserid); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(TRANSFER_RESULT); + return WxCpUserTransferResultResp.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpUserTransferCustomerResp resignedTransferCustomer(WxCpUserTransferCustomerReq req) + throws WxErrorException { + BeanUtils.checkRequiredFields(req); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(RESIGNED_TRANSFER_CUSTOMER); + return WxCpUserTransferCustomerResp.fromJson(this.mainService.post(url, req.toJson())); + } + + @Override + public WxCpUserTransferResultResp resignedTransferResult(String handOverUserid, + String takeOverUserid, String cursor) + throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("cursor", cursor); + json.addProperty("handover_userid", handOverUserid); + json.addProperty("takeover_userid", takeOverUserid); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(RESIGNED_TRANSFER_RESULT); + return WxCpUserTransferResultResp.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpUserExternalGroupChatList listGroupChat(Integer pageIndex, Integer pageSize, int status, + String[] userIds, String[] partyIds) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("offset", pageIndex == null ? 0 : pageIndex); + json.addProperty("limit", pageSize == null ? 100 : pageSize); + json.addProperty("status_filter", status); + if (ArrayUtils.isNotEmpty(userIds) || ArrayUtils.isNotEmpty(partyIds)) { + JsonObject ownerFilter = new JsonObject(); + if (ArrayUtils.isNotEmpty(userIds)) { + ownerFilter.add("userid_list", new Gson().toJsonTree(userIds).getAsJsonArray()); + } + if (ArrayUtils.isNotEmpty(partyIds)) { + ownerFilter.add("partyid_list", new Gson().toJsonTree(partyIds).getAsJsonArray()); + } + json.add("owner_filter", ownerFilter); + } + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GROUP_CHAT_LIST); + return WxCpUserExternalGroupChatList.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpUserExternalGroupChatList listGroupChat(Integer limit, String cursor, int status, String[] userIds) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("cursor", cursor == null ? "" : cursor); + json.addProperty("limit", limit == null ? 100 : limit); + json.addProperty("status_filter", status); + if (ArrayUtils.isNotEmpty(userIds)) { + JsonObject ownerFilter = new JsonObject(); + if (ArrayUtils.isNotEmpty(userIds)) { + ownerFilter.add("userid_list", new Gson().toJsonTree(userIds).getAsJsonArray()); + } + json.add("owner_filter", ownerFilter); + } + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GROUP_CHAT_LIST); + return WxCpUserExternalGroupChatList.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpUserExternalGroupChatInfo getGroupChat(String chatId, Integer needName) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("chat_id", chatId); + json.addProperty("need_name", needName); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GROUP_CHAT_INFO); + return WxCpUserExternalGroupChatInfo.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpUserExternalGroupChatTransferResp transferGroupChat(String[] chatIds, String newOwner) throws WxErrorException { + JsonObject json = new JsonObject(); + if (ArrayUtils.isNotEmpty(chatIds)) { + json.add("chat_id_list", new Gson().toJsonTree(chatIds).getAsJsonArray()); + } + json.addProperty("new_owner", newOwner); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GROUP_CHAT_TRANSFER); + return WxCpUserExternalGroupChatTransferResp.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpUserExternalGroupChatTransferResp onjobTransferGroupChat(String[] chatIds, String newOwner) throws WxErrorException { + JsonObject json = new JsonObject(); + if (ArrayUtils.isNotEmpty(chatIds)) { + json.add("chat_id_list", new Gson().toJsonTree(chatIds).getAsJsonArray()); + } + json.addProperty("new_owner", newOwner); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GROUP_CHAT_ONJOB_TRANSFER); + return WxCpUserExternalGroupChatTransferResp.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpUserExternalUserBehaviorStatistic getUserBehaviorStatistic(Date startTime, Date endTime, + String[] userIds, String[] partyIds) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("start_time", startTime.getTime() / 1000); + json.addProperty("end_time", endTime.getTime() / 1000); + if (ArrayUtils.isNotEmpty(userIds) || ArrayUtils.isNotEmpty(partyIds)) { + if (ArrayUtils.isNotEmpty(userIds)) { + json.add("userid", new Gson().toJsonTree(userIds).getAsJsonArray()); + } + if (ArrayUtils.isNotEmpty(partyIds)) { + json.add("partyid", new Gson().toJsonTree(partyIds).getAsJsonArray()); + } + } + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(LIST_USER_BEHAVIOR_DATA); + return WxCpUserExternalUserBehaviorStatistic.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpUserExternalGroupChatStatistic getGroupChatStatistic(Date startTime, Integer orderBy, Integer orderAsc, + Integer pageIndex, Integer pageSize, + String[] userIds, String[] partyIds) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("day_begin_time", startTime.getTime() / 1000); + json.addProperty("order_by", orderBy == null ? 1 : orderBy); + json.addProperty("order_asc", orderAsc == null ? 0 : orderAsc); + json.addProperty("offset", pageIndex == null ? 0 : pageIndex); + json.addProperty("limit", pageSize == null ? 500 : pageSize); + if (ArrayUtils.isNotEmpty(userIds) || ArrayUtils.isNotEmpty(partyIds)) { + JsonObject ownerFilter = new JsonObject(); + if (ArrayUtils.isNotEmpty(userIds)) { + ownerFilter.add("userid_list", new Gson().toJsonTree(userIds).getAsJsonArray()); + } + if (ArrayUtils.isNotEmpty(partyIds)) { + ownerFilter.add("partyid_list", new Gson().toJsonTree(partyIds).getAsJsonArray()); + } + json.add("owner_filter", ownerFilter); + } + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(LIST_GROUP_CHAT_DATA); + return WxCpUserExternalGroupChatStatistic.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpMsgTemplateAddResult addMsgTemplate(WxCpMsgTemplate wxCpMsgTemplate) throws WxErrorException { + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(ADD_MSG_TEMPLATE); + return WxCpMsgTemplateAddResult.fromJson(this.mainService.post(url, wxCpMsgTemplate.toJson())); + } + + @Override + public WxCpBaseResp remindGroupMsgSend(String msgId) throws WxErrorException { + JsonObject params = new JsonObject(); + params.addProperty("msgid", msgId); + final String url = this.mainService.getWxCpConfigStorage() + .getApiUrl(REMIND_GROUP_MSG_SEND); + return WxCpBaseResp.fromJson(this.mainService.post(url, params.toString())); + } + + @Override + public WxCpBaseResp cancelGroupMsgSend(String msgId) throws WxErrorException { + JsonObject params = new JsonObject(); + params.addProperty("msgid", msgId); + final String url = this.mainService.getWxCpConfigStorage() + .getApiUrl(CANCEL_GROUP_MSG_SEND); + return WxCpBaseResp.fromJson(this.mainService.post(url, params.toString())); + } + + @Override + public void sendWelcomeMsg(WxCpWelcomeMsg msg) throws WxErrorException { + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(SEND_WELCOME_MSG); + this.mainService.post(url, msg.toJson()); + } + + @Override + public WxCpUserExternalTagGroupList getCorpTagList(String[] tagId) throws WxErrorException { + JsonObject json = new JsonObject(); + if (ArrayUtils.isNotEmpty(tagId)) { + json.add("tag_id", new Gson().toJsonTree(tagId).getAsJsonArray()); + } + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_CORP_TAG_LIST); + return WxCpUserExternalTagGroupList.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpUserExternalTagGroupList getCorpTagList(String[] tagId, String[] groupId) throws WxErrorException { + JsonObject json = new JsonObject(); + if (ArrayUtils.isNotEmpty(tagId)) { + json.add("tag_id", new Gson().toJsonTree(tagId).getAsJsonArray()); + } + if (ArrayUtils.isNotEmpty(groupId)) { + json.add("group_id", new Gson().toJsonTree(groupId).getAsJsonArray()); + } + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_CORP_TAG_LIST); + return WxCpUserExternalTagGroupList.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpUserExternalTagGroupInfo addCorpTag(WxCpUserExternalTagGroupInfo tagGroup) throws WxErrorException { + + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(ADD_CORP_TAG); + return WxCpUserExternalTagGroupInfo.fromJson(this.mainService.post(url, tagGroup.getTagGroup().toJson())); + } + + @Override + public WxCpBaseResp editCorpTag(String id, String name, Integer order) throws WxErrorException { + + JsonObject json = new JsonObject(); + json.addProperty("id", id); + json.addProperty("name", name); + json.addProperty("order", order); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(EDIT_CORP_TAG); + return WxCpBaseResp.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpBaseResp delCorpTag(String[] tagId, String[] groupId) throws WxErrorException { + JsonObject json = new JsonObject(); + if (ArrayUtils.isNotEmpty(tagId)) { + json.add("tag_id", new Gson().toJsonTree(tagId).getAsJsonArray()); + } + if (ArrayUtils.isNotEmpty(groupId)) { + json.add("group_id", new Gson().toJsonTree(groupId).getAsJsonArray()); + } + + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(DEL_CORP_TAG); + return WxCpBaseResp.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpBaseResp markTag(String userid, String externalUserid, String[] addTag, String[] removeTag) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("userid", userid); + json.addProperty("external_userid", externalUserid); + + if (ArrayUtils.isNotEmpty(addTag)) { + json.add("add_tag", new Gson().toJsonTree(addTag).getAsJsonArray()); + } + if (ArrayUtils.isNotEmpty(removeTag)) { + json.add("remove_tag", new Gson().toJsonTree(removeTag).getAsJsonArray()); + } + + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(MARK_TAG); + return WxCpBaseResp.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpAddMomentResult addMomentTask(WxCpAddMomentTask task) throws WxErrorException { + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(ADD_MOMENT_TASK); + return WxCpAddMomentResult.fromJson(this.mainService.post(url, task.toJson())); + } + + @Override + public WxCpGetMomentTaskResult getMomentTaskResult(String jobId) throws WxErrorException { + String params = "&jobid=" + jobId; + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_MOMENT_TASK_RESULT); + return WxCpGetMomentTaskResult.fromJson(this.mainService.get(url, params)); + } + + @Override + public WxCpBaseResp cancelMomentTask(String momentId) throws WxErrorException { + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(CANCEL_MOMENT_TASK); + JsonObject json = new JsonObject(); + json.addProperty("moment_id", momentId); + return WxCpBaseResp.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpGetMomentList getMomentList(Long startTime, Long endTime, String creator, Integer filterType, + String cursor, Integer limit) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("start_time", startTime); + json.addProperty("end_time", endTime); + if (!StringUtils.isEmpty(creator)) { + json.addProperty("creator", creator); + } + if (filterType != null) { + json.addProperty("filter_type", filterType); + } + if (!StringUtils.isEmpty(cursor)) { + json.addProperty("cursor", cursor); + } + if (limit != null) { + json.addProperty("limit", limit); + } + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_MOMENT_LIST); + return WxCpGetMomentList.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpGetMomentTask getMomentTask(String momentId, String cursor, Integer limit) + throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("moment_id", momentId); + if (!StringUtils.isEmpty(cursor)) { + json.addProperty("cursor", cursor); + } + if (limit != null) { + json.addProperty("limit", limit); + } + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_MOMENT_TASK); + return WxCpGetMomentTask.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpGetMomentCustomerList getMomentCustomerList(String momentId, String userId, + String cursor, Integer limit) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("moment_id", momentId); + json.addProperty("userid", userId); + if (!StringUtils.isEmpty(cursor)) { + json.addProperty("cursor", cursor); + } + if (limit != null) { + json.addProperty("limit", limit); + } + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_MOMENT_CUSTOMER_LIST); + return WxCpGetMomentCustomerList.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpGetMomentSendResult getMomentSendResult(String momentId, String userId, + String cursor, Integer limit) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("moment_id", momentId); + json.addProperty("userid", userId); + if (!StringUtils.isEmpty(cursor)) { + json.addProperty("cursor", cursor); + } + if (limit != null) { + json.addProperty("limit", limit); + } + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_MOMENT_SEND_RESULT); + return WxCpGetMomentSendResult.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpGetMomentComments getMomentComments(String momentId, String userId) + throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("moment_id", momentId); + json.addProperty("userid", userId); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_MOMENT_COMMENTS); + return WxCpGetMomentComments.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpGroupMsgListResult getGroupMsgListV2(String chatType, Date startTime, Date endTime, + String creator, Integer filterType, Integer limit, String cursor) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("chat_type", chatType); + json.addProperty("start_time", startTime.getTime() / 1000); + json.addProperty("end_time", endTime.getTime() / 1000); + json.addProperty("creator", creator); + json.addProperty("filter_type", filterType); + json.addProperty("limit", limit); + json.addProperty("cursor", cursor); + + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_GROUP_MSG_LIST_V2); + return WxCpGroupMsgListResult.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpGroupMsgSendResult getGroupMsgSendResult(String msgid, String userid, Integer limit, String cursor) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("msgid", msgid); + json.addProperty("userid", userid); + json.addProperty("limit", limit); + json.addProperty("cursor", cursor); + + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_GROUP_MSG_SEND_RESULT); + return WxCpGroupMsgSendResult.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpGroupMsgResult getGroupMsgResult(String msgid, Integer limit, String cursor) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("msgid", msgid); + json.addProperty("limit", limit); + json.addProperty("cursor", cursor); + + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_GROUP_MSG_RESULT); + return WxCpGroupMsgResult.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpGroupMsgTaskResult getGroupMsgTask(String msgid, Integer limit, String cursor) throws WxErrorException { + + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_GROUP_MSG_TASK); + return WxCpGroupMsgTaskResult.fromJson(this.mainService.post(url, + GsonHelper.buildJsonObject("msgid", msgid, + "limit", limit, + "cursor", cursor))); + } + + @Override + public String addGroupWelcomeTemplate(WxCpGroupWelcomeTemplateResult template) throws WxErrorException { + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GROUP_WELCOME_TEMPLATE_ADD); + final String responseContent = this.mainService.post(url, template.toJson()); + return GsonParser.parse(responseContent).get("template_id").getAsString(); + } + + @Override + public WxCpBaseResp editGroupWelcomeTemplate(WxCpGroupWelcomeTemplateResult template) throws WxErrorException { + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GROUP_WELCOME_TEMPLATE_EDIT); + return WxCpGroupWelcomeTemplateResult.fromJson(this.mainService.post(url, template.toJson())); + } + + @Override + public WxCpGroupWelcomeTemplateResult getGroupWelcomeTemplate(String templateId) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("template_id", templateId); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GROUP_WELCOME_TEMPLATE_GET); + return WxCpGroupWelcomeTemplateResult.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpBaseResp delGroupWelcomeTemplate(String templateId, String agentId) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("template_id", templateId); + if (!StringUtils.isEmpty(agentId)) { + json.addProperty("agentid", agentId); + } + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GROUP_WELCOME_TEMPLATE_DEL); + return WxCpBaseResp.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpProductAlbumListResult getProductAlbumList(Integer limit, String cursor) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("limit", limit); + json.addProperty("cursor", cursor); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_PRODUCT_ALBUM_LIST); + return WxCpProductAlbumListResult.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxCpProductAlbumResult getProductAlbum(String productId) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("product_id", productId); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_PRODUCT_ALBUM); + return WxCpProductAlbumResult.fromJson(this.mainService.post(url, json.toString())); + } + + @Override + public WxMediaUploadResult uploadAttachment(String mediaType, String fileType, Integer attachmentType, + InputStream inputStream) throws WxErrorException, IOException { + return uploadAttachment(mediaType, attachmentType, FileUtils.createTmpFile(inputStream, + UUID.randomUUID().toString(), fileType)); + } + + @Override + public WxMediaUploadResult uploadAttachment(String mediaType, Integer attachmentType, File file) + throws WxErrorException { + String params = "?media_type=" + mediaType + "&attachment_type=" + attachmentType; + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(UPLOAD_ATTACHMENT + params); + return this.mainService.execute(MediaUploadRequestExecutor.create( + this.mainService.getRequestHttp()), url, file); + } + + @Override + public String addInterceptRule(WxCpInterceptRuleAddRequest ruleAddRequest) throws WxErrorException { + String responseContent = this.mainService.post(this.mainService.getWxCpConfigStorage() + .getApiUrl(ADD_INTERCEPT_RULE), ruleAddRequest); + return GsonParser.parse(responseContent).get("rule_id").getAsString(); + } + + @Override + public void updateInterceptRule(WxCpInterceptRule interceptRule) throws WxErrorException { + this.mainService.post(this.mainService.getWxCpConfigStorage().getApiUrl(UPDATE_INTERCEPT_RULE), + interceptRule); + } + + @Override + public void delInterceptRule(String ruleId) throws WxErrorException { + this.mainService.post(this.mainService.getWxCpConfigStorage().getApiUrl(DEL_INTERCEPT_RULE), + GsonHelper.buildJsonObject("rule_id", ruleId)); + } + + @Override + public WxCpInterceptRuleList getInterceptRuleList() throws WxErrorException { + String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_INTERCEPT_RULE_LIST); + return WxCpInterceptRuleList.fromJson(this.mainService.get(url,null)); + } + + @Override + public WxCpInterceptRuleInfo getInterceptRuleDetail(String ruleId) throws WxErrorException { + String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_INTERCEPT_RULE); + String json = this.mainService.post(url, GsonHelper.buildJsonObject("rule_id", ruleId)); + return WxCpInterceptRuleInfo.fromJson(json); + } + + @Override + public String addProductAlbum(WxCpProductAlbumInfo wxCpProductAlbumInfo) throws WxErrorException { + String url = this.mainService.getWxCpConfigStorage().getApiUrl(ADD_PRODUCT_ALBUM); + String responseContent = this.mainService.post(url, wxCpProductAlbumInfo.toJson()); + return GsonParser.parse(responseContent).get("product_id").getAsString(); + } + + @Override + public void updateProductAlbum(WxCpProductAlbumInfo wxCpProductAlbumInfo) throws WxErrorException { + String url = this.mainService.getWxCpConfigStorage().getApiUrl(UPDATE_PRODUCT_ALBUM); + this.mainService.post(url, wxCpProductAlbumInfo.toJson()); + } + + @Override + public void deleteProductAlbum(String productId) throws WxErrorException { + JsonObject o = new JsonObject(); + o.addProperty("product_id", productId); + String url = this.mainService.getWxCpConfigStorage().getApiUrl(DELETE_PRODUCT_ALBUM); + this.mainService.post(url, o.toString()); + } + + @Override + public WxCpCustomerAcquisitionList customerAcquisitionLinkList(Integer limit, String cursor) throws WxErrorException { + JsonObject o = new JsonObject(); + o.addProperty("limit", limit); + o.addProperty("cursor", cursor); + + String url = this.mainService.getWxCpConfigStorage().getApiUrl(CUSTOMER_ACQUISITION_LINK_LIST); + return WxCpCustomerAcquisitionList.fromJson(this.mainService.post(url, o)); + } + + @Override + public WxCpCustomerAcquisitionInfo customerAcquisitionLinkGet(String linkId) throws WxErrorException { + JsonObject o = new JsonObject(); + o.addProperty("link_id", linkId); + + String url = this.mainService.getWxCpConfigStorage().getApiUrl(CUSTOMER_ACQUISITION_LINK_GET); + return WxCpCustomerAcquisitionInfo.fromJson(this.mainService.post(url, o)); + } + + @Override + public WxCpCustomerAcquisitionCreateResult customerAcquisitionLinkCreate(WxCpCustomerAcquisitionRequest wxCpCustomerAcquisitionRequest) throws WxErrorException { + String url = this.mainService.getWxCpConfigStorage().getApiUrl(CUSTOMER_ACQUISITION_LINK_CREATE); + return WxCpCustomerAcquisitionCreateResult.fromJson(this.mainService.post(url, wxCpCustomerAcquisitionRequest.toJson())); + } + + @Override + public WxCpBaseResp customerAcquisitionUpdate(WxCpCustomerAcquisitionRequest wxCpCustomerAcquisitionRequest) throws WxErrorException { + String url = this.mainService.getWxCpConfigStorage().getApiUrl(CUSTOMER_ACQUISITION_LINK_UPDATE); + return WxCpBaseResp.fromJson(this.mainService.post(url, wxCpCustomerAcquisitionRequest.toJson())); + } + + @Override + public WxCpBaseResp customerAcquisitionLinkDelete(String linkId) throws WxErrorException { + JsonObject o = new JsonObject(); + o.addProperty("link_id", linkId); + + String url = this.mainService.getWxCpConfigStorage().getApiUrl(CUSTOMER_ACQUISITION_LINK_DELETE); + return WxCpBaseResp.fromJson(this.mainService.post(url, o)); + } + + @Override + public WxCpCustomerAcquisitionCustomerList customerAcquisitionCustomer(String linkId, Integer limit, String cursor) throws WxErrorException { + JsonObject o = new JsonObject(); + o.addProperty("link_id", linkId); + o.addProperty("limit", limit); + o.addProperty("cursor", cursor); + + String url = this.mainService.getWxCpConfigStorage().getApiUrl(CUSTOMER_ACQUISITION_CUSTOMER); + return WxCpCustomerAcquisitionCustomerList.fromJson(this.mainService.post(url, o)); + } + + @Override + public WxCpCustomerAcquisitionQuota customerAcquisitionQuota() throws WxErrorException { + String url = this.mainService.getWxCpConfigStorage().getApiUrl(CUSTOMER_ACQUISITION_QUOTA); + return WxCpCustomerAcquisitionQuota.fromJson(this.mainService.get(url, null)); + } + + @Override + public WxCpCustomerAcquisitionStatistic customerAcquisitionStatistic(String linkId, @NonNull Date startTime, + @NonNull Date endTime) throws WxErrorException { + long endTimestamp = endTime.getTime() / 1000L; + long startTimestamp = startTime.getTime() / 1000L; + + JsonObject o = new JsonObject(); + o.addProperty("link_id", linkId); + o.addProperty("start_time", startTimestamp); + o.addProperty("end_time", endTimestamp); + + String url = this.mainService.getWxCpConfigStorage().getApiUrl(CUSTOMER_ACQUISITION_STATISTIC); + return WxCpCustomerAcquisitionStatistic.fromJson(this.mainService.post(url, o)); + } + + + @Override + public WxCpGroupJoinWayResult addJoinWay(WxCpGroupJoinWayInfo wxCpGroupJoinWayInfo) throws WxErrorException { + if (wxCpGroupJoinWayInfo.getJoinWay().getChatIdList() != null && wxCpGroupJoinWayInfo.getJoinWay().getChatIdList().size() > 5) { + throw new WxRuntimeException("使用该配置的客户群ID列表,支持5个"); + } + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(ADD_JOIN_WAY); + + return WxCpGroupJoinWayResult.fromJson(this.mainService.post(url, wxCpGroupJoinWayInfo.getJoinWay().toJson())); + } + + @Override + public WxCpBaseResp updateJoinWay(WxCpGroupJoinWayInfo wxCpGroupJoinWayInfo) throws WxErrorException { + if (wxCpGroupJoinWayInfo.getJoinWay().getChatIdList() != null && wxCpGroupJoinWayInfo.getJoinWay().getChatIdList().size() > 5) { + throw new WxRuntimeException("使用该配置的客户群ID列表,支持5个"); + } + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(UPDATE_JOIN_WAY); + return WxCpBaseResp.fromJson(this.mainService.post(url, wxCpGroupJoinWayInfo.getJoinWay().toJson())); + } + + @Override + public WxCpGroupJoinWayInfo getJoinWay(String configId) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("config_id", configId); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_JOIN_WAY); + + return WxCpGroupJoinWayInfo.fromJson(this.mainService.post(url, json)); + } + + @Override + public WxCpBaseResp delJoinWay(String configId) throws WxErrorException { + JsonObject json = new JsonObject(); + json.addProperty("config_id", configId); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(DEL_JOIN_WAY); + return WxCpBaseResp.fromJson(this.mainService.post(url, json)); } } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpGroupRobotServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpGroupRobotServiceImpl.java new file mode 100644 index 0000000000..8373c6c8ee --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpGroupRobotServiceImpl.java @@ -0,0 +1,121 @@ +package me.chanjar.weixin.cp.api.impl; + +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.api.WxCpGroupRobotService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.article.NewArticle; +import me.chanjar.weixin.cp.bean.message.WxCpGroupRobotMessage; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import me.chanjar.weixin.cp.constant.WxCpApiPathConsts; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; + +import static me.chanjar.weixin.cp.constant.WxCpConsts.GroupRobotMsgType; + +/** + * 企业微信群机器人消息发送api 实现 + * + * @author yr created on 2020-08-20 + */ +@RequiredArgsConstructor +public class WxCpGroupRobotServiceImpl implements WxCpGroupRobotService { + private final WxCpService cpService; + + private String getWebhookUrl() throws WxErrorException { + WxCpConfigStorage wxCpConfigStorage = this.cpService.getWxCpConfigStorage(); + final String webhookKey = wxCpConfigStorage.getWebhookKey(); + if (StringUtils.isEmpty(webhookKey)) { + throw new WxErrorException("请先设置WebhookKey"); + } + return wxCpConfigStorage.getApiUrl(WxCpApiPathConsts.WEBHOOK_SEND) + webhookKey; + } + + @Override + public void sendText(String content, List mentionedList, List mobileList) throws WxErrorException { + this.sendText(this.getWebhookUrl(), content, mentionedList, mobileList); + } + + @Override + public void sendMarkdown(String content) throws WxErrorException { + this.sendMarkdown(this.getWebhookUrl(), content); + } + + @Override + public void sendMarkdownV2(String content) throws WxErrorException { + this.sendMarkdownV2(this.getWebhookUrl(), content); + } + + @Override + public void sendImage(String base64, String md5) throws WxErrorException { + this.sendImage(this.getWebhookUrl(), base64, md5); + } + + @Override + public void sendNews(List articleList) throws WxErrorException { + this.sendNews(this.getWebhookUrl(), articleList); + } + + @Override + public void sendText(String webhookUrl, String content, List mentionedList, List mobileList) throws WxErrorException { + this.cpService.postWithoutToken(webhookUrl, new WxCpGroupRobotMessage() + .setMsgType(GroupRobotMsgType.TEXT) + .setContent(content) + .setMentionedList(mentionedList) + .setMentionedMobileList(mobileList) + .toJson()); + } + + @Override + public void sendMarkdown(String webhookUrl, String content) throws WxErrorException { + this.cpService.postWithoutToken(webhookUrl, new WxCpGroupRobotMessage() + .setMsgType(GroupRobotMsgType.MARKDOWN) + .setContent(content) + .toJson()); + } + + @Override + public void sendMarkdownV2(String webhookUrl, String content) throws WxErrorException { + this.cpService.postWithoutToken(webhookUrl, new WxCpGroupRobotMessage() + .setMsgType(GroupRobotMsgType.MARKDOWN_V2) + .setContent(content) + .toJson()); + } + + @Override + public void sendImage(String webhookUrl, String base64, String md5) throws WxErrorException { + this.cpService.postWithoutToken(webhookUrl, new WxCpGroupRobotMessage() + .setMsgType(GroupRobotMsgType.IMAGE) + .setBase64(base64) + .setMd5(md5).toJson()); + } + + @Override + public void sendNews(String webhookUrl, List articleList) throws WxErrorException { + this.cpService.postWithoutToken(webhookUrl, new WxCpGroupRobotMessage() + .setMsgType(GroupRobotMsgType.NEWS) + .setArticles(articleList).toJson()); + } + + @Override + public void sendFile(String webhookUrl, String mediaId) throws WxErrorException { + this.cpService.postWithoutToken(webhookUrl, new WxCpGroupRobotMessage() + .setMsgType(GroupRobotMsgType.FILE) + .setMediaId(mediaId).toJson()); + } + + + @Override + public void sendVoice(String webhookUrl, String mediaId) throws WxErrorException { + this.cpService.postWithoutToken(webhookUrl, new WxCpGroupRobotMessage() + .setMsgType(GroupRobotMsgType.VOICE) + .setMediaId(mediaId).toJson()); + } + + @Override + public void sendTemplateCardMessage(String webhookUrl, WxCpGroupRobotMessage wxCpGroupRobotMessage) throws WxErrorException { + this.cpService.postWithoutToken(webhookUrl, wxCpGroupRobotMessage.toJson()); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpHrServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpHrServiceImpl.java new file mode 100644 index 0000000000..df71643d4c --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpHrServiceImpl.java @@ -0,0 +1,80 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.gson.JsonObject; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.api.WxCpHrService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.hr.WxCpHrEmployeeFieldData; +import me.chanjar.weixin.cp.bean.hr.WxCpHrEmployeeFieldDataResp; +import me.chanjar.weixin.cp.bean.hr.WxCpHrEmployeeFieldInfoResp; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.util.List; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Hr.*; + +/** + * 人事助手相关接口实现类. + * 官方文档:https://developer.work.weixin.qq.com/document/path/99132 + * + * @author leejoker created on 2024-01-01 + */ +@RequiredArgsConstructor +public class WxCpHrServiceImpl implements WxCpHrService { + + private final WxCpService cpService; + + @Override + public WxCpHrEmployeeFieldInfoResp getFieldInfo(List fields) throws WxErrorException { + JsonObject jsonObject = new JsonObject(); + if (fields != null && !fields.isEmpty()) { + jsonObject.add("fields", WxCpGsonBuilder.create().toJsonTree(fields)); + } + String response = this.cpService.post( + this.cpService.getWxCpConfigStorage().getApiUrl(GET_FIELD_INFO), + jsonObject.toString() + ); + return WxCpHrEmployeeFieldInfoResp.fromJson(response); + } + + @Override + public WxCpHrEmployeeFieldDataResp getEmployeeFieldInfo(String userid, List fields) throws WxErrorException { + return getEmployeeFieldInfo(userid, false, fields); + } + + @Override + public WxCpHrEmployeeFieldDataResp getEmployeeFieldInfo(String userid, boolean getAll, List fields) throws WxErrorException { + if (userid == null || userid.trim().isEmpty()) { + throw new IllegalArgumentException("userid 不能为空"); + } + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("userid", userid); + jsonObject.addProperty("get_all", getAll); + if (fields != null && !fields.isEmpty()) { + jsonObject.add("fields", WxCpGsonBuilder.create().toJsonTree(fields)); + } + String response = this.cpService.post( + this.cpService.getWxCpConfigStorage().getApiUrl(GET_EMPLOYEE_FIELD_INFO), + jsonObject.toString() + ); + return WxCpHrEmployeeFieldDataResp.fromJson(response); + } + + @Override + public void updateEmployeeFieldInfo(String userid, List fieldList) throws WxErrorException { + if (userid == null || userid.trim().isEmpty()) { + throw new IllegalArgumentException("userid 不能为空"); + } + if (fieldList == null || fieldList.isEmpty()) { + throw new IllegalArgumentException("fieldList 不能为空"); + } + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("userid", userid); + jsonObject.add("field_list", WxCpGsonBuilder.create().toJsonTree(fieldList)); + this.cpService.post( + this.cpService.getWxCpConfigStorage().getApiUrl(UPDATE_EMPLOYEE_FIELD_INFO), + jsonObject.toString() + ); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpIntelligentRobotServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpIntelligentRobotServiceImpl.java new file mode 100644 index 0000000000..aba1ee85c4 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpIntelligentRobotServiceImpl.java @@ -0,0 +1,75 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.gson.JsonObject; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.api.WxCpIntelligentRobotService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.intelligentrobot.*; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.IntelligentRobot.*; + +/** + * 企业微信智能机器人接口实现 + * + * @author Binary Wang + */ +@RequiredArgsConstructor +public class WxCpIntelligentRobotServiceImpl implements WxCpIntelligentRobotService { + + private final WxCpService cpService; + + @Override + public WxCpIntelligentRobotCreateResponse createRobot(WxCpIntelligentRobotCreateRequest request) throws WxErrorException { + String responseText = this.cpService.post(CREATE_ROBOT, request.toJson()); + return WxCpIntelligentRobotCreateResponse.fromJson(responseText); + } + + @Override + public void deleteRobot(String robotId) throws WxErrorException { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("robot_id", robotId); + this.cpService.post(DELETE_ROBOT, jsonObject.toString()); + } + + @Override + public void updateRobot(WxCpIntelligentRobotUpdateRequest request) throws WxErrorException { + this.cpService.post(UPDATE_ROBOT, request.toJson()); + } + + @Override + public WxCpIntelligentRobot getRobot(String robotId) throws WxErrorException { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("robot_id", robotId); + String responseText = this.cpService.post(GET_ROBOT, jsonObject.toString()); + return WxCpIntelligentRobot.fromJson(responseText); + } + + @Override + public WxCpIntelligentRobotChatResponse chat(WxCpIntelligentRobotChatRequest request) throws WxErrorException { + String responseText = this.cpService.post(CHAT, request.toJson()); + return WxCpIntelligentRobotChatResponse.fromJson(responseText); + } + + @Override + public void resetSession(String robotId, String userid, String sessionId) throws WxErrorException { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("robot_id", robotId); + jsonObject.addProperty("userid", userid); + jsonObject.addProperty("session_id", sessionId); + this.cpService.post(RESET_SESSION, jsonObject.toString()); + } + + @Override + public WxCpIntelligentRobotSendMessageResponse sendMessage(WxCpIntelligentRobotSendMessageRequest request) throws WxErrorException { + String responseText = this.cpService.post(SEND_MESSAGE, request.toJson()); + return WxCpIntelligentRobotSendMessageResponse.fromJson(responseText); + } + + @Override + public WxCpIntelligentRobotMessage parseCallbackMessage(String callbackMessageJson) { + return WxCpIntelligentRobotMessage.fromJson(callbackMessageJson); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpKfServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpKfServiceImpl.java new file mode 100644 index 0000000000..be4f2a5850 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpKfServiceImpl.java @@ -0,0 +1,320 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.api.WxCpKfService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; +import me.chanjar.weixin.cp.bean.kf.*; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.util.List; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Kf.*; + +/** + * 微信客服接口-服务实现 + * + * @author Fu created on 2022/1/19 19:41 + */ +@RequiredArgsConstructor +public class WxCpKfServiceImpl implements WxCpKfService { + private final WxCpService cpService; + private static final Gson GSON = new GsonBuilder().create(); + + @Override + public WxCpKfAccountAddResp addAccount(WxCpKfAccountAdd add) throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(ACCOUNT_ADD); + String responseContent = cpService.post(url, WxCpGsonBuilder.create().toJson(add)); + return WxCpKfAccountAddResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp updAccount(WxCpKfAccountUpd upd) throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(ACCOUNT_UPD); + String responseContent = cpService.post(url, WxCpGsonBuilder.create().toJson(upd)); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp delAccount(WxCpKfAccountDel del) throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(ACCOUNT_DEL); + String responseContent = cpService.post(url, WxCpGsonBuilder.create().toJson(del)); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpKfAccountListResp listAccount(Integer offset, Integer limit) throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(ACCOUNT_LIST); + JsonObject json = new JsonObject(); + if (offset != null) { + json.addProperty("offset", offset); + } + if (limit != null) { + json.addProperty("limit", limit); + } + String responseContent = cpService.post(url, json.toString()); + return WxCpKfAccountListResp.fromJson(responseContent); + } + + @Override + public WxCpKfAccountLinkResp getAccountLink(WxCpKfAccountLink link) throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(ADD_CONTACT_WAY); + String responseContent = cpService.post(url, WxCpGsonBuilder.create().toJson(link)); + return WxCpKfAccountLinkResp.fromJson(responseContent); + } + + @Override + public WxCpKfServicerOpResp addServicer(String openKfid, List userIdList) throws WxErrorException { + return servicerOp(openKfid, userIdList, null, SERVICER_ADD); + } + + @Override + public WxCpKfServicerOpResp addServicer(String openKfId, List userIdList, List departmentIdList) throws WxErrorException { + validateParameters(SERVICER_ADD, userIdList, departmentIdList); + return servicerOp(openKfId, userIdList, departmentIdList, SERVICER_ADD); + } + + @Override + public WxCpKfServicerOpResp delServicer(String openKfid, List userIdList) throws WxErrorException { + return servicerOp(openKfid, userIdList, null, SERVICER_DEL); + } + + @Override + public WxCpKfServicerOpResp delServicer(String openKfid, List userIdList, List departmentIdList) throws WxErrorException { + validateParameters(SERVICER_DEL, userIdList, departmentIdList); + return servicerOp(openKfid, userIdList, departmentIdList, SERVICER_DEL); + } + + private void validateParameters(String uri, List userIdList, List departmentIdList) { + if ((userIdList == null || userIdList.isEmpty()) && (departmentIdList == null || departmentIdList.isEmpty())) { + throw new IllegalArgumentException("userid_list和department_id_list至少需要填其中一个"); + } + if (SERVICER_DEL.equals(uri)) { + if (userIdList != null && userIdList.size() > 100) { + throw new IllegalArgumentException("可填充个数:0 ~ 100。超过100个需分批调用。"); + } + if (departmentIdList != null && departmentIdList.size() > 100) { + throw new IllegalArgumentException("可填充个数:0 ~ 100。超过100个需分批调用。"); + } + } else { + if (userIdList != null && userIdList.size() > 100) { + throw new IllegalArgumentException("可填充个数:0 ~ 100。超过100个需分批调用。"); + } + if (departmentIdList != null && departmentIdList.size() > 20) { + throw new IllegalArgumentException("可填充个数:0 ~ 20。"); + } + } + } + + private WxCpKfServicerOpResp servicerOp(String openKfid, List userIdList, List departmentIdList, String uri) throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(uri); + + JsonObject json = new JsonObject(); + json.addProperty("open_kfid", openKfid); + if (userIdList != null && !userIdList.isEmpty()) { + JsonArray userIdArray = new JsonArray(); + userIdList.forEach(userIdArray::add); + json.add("userid_list", userIdArray); + } + if (departmentIdList != null && !departmentIdList.isEmpty()) { + JsonArray departmentIdArray = new JsonArray(); + departmentIdList.forEach(departmentIdArray::add); + json.add("department_id_list", departmentIdArray); + } + String responseContent = cpService.post(url, json.toString()); + return WxCpKfServicerOpResp.fromJson(responseContent); + } + + @Override + public WxCpKfServicerListResp listServicer(String openKfid) throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(SERVICER_LIST + openKfid); + String responseContent = cpService.get(url, null); + return WxCpKfServicerListResp.fromJson(responseContent); + } + + @Override + public WxCpKfServiceStateResp getServiceState(String openKfid, String externalUserId) + throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(SERVICE_STATE_GET); + + JsonObject json = new JsonObject(); + json.addProperty("open_kfid", openKfid); + json.addProperty("external_userid", externalUserId); + + String responseContent = cpService.post(url, json.toString()); + return WxCpKfServiceStateResp.fromJson(responseContent); + } + + @Override + public WxCpKfServiceStateTransResp transServiceState(String openKfid, String externalUserId, + Integer serviceState, String servicerUserId) throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(SERVICE_STATE_TRANS); + + JsonObject json = new JsonObject(); + json.addProperty("open_kfid", openKfid); + json.addProperty("external_userid", externalUserId); + json.addProperty("service_state", serviceState); + json.addProperty("servicer_userid", servicerUserId); + + String responseContent = cpService.post(url, json.toString()); + return WxCpKfServiceStateTransResp.fromJson(responseContent); + } + + @Override + public WxCpKfMsgListResp syncMsg(String cursor, String token, Integer limit, Integer voiceFormat) + throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(SYNC_MSG); + + JsonObject json = new JsonObject(); + if (cursor != null) { + json.addProperty("cursor", cursor); + } + if (token != null) { + json.addProperty("token", token); + } + if (limit != null) { + json.addProperty("limit", limit); + } + if (voiceFormat != null) { + json.addProperty("voice_format", voiceFormat); + } + + String responseContent = cpService.post(url, json); + return WxCpKfMsgListResp.fromJson(responseContent); + } + + @Override + public WxCpKfMsgListResp syncMsg(String cursor, String token, Integer limit, Integer voiceFormat, String openKfId) throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(SYNC_MSG); + + JsonObject json = new JsonObject(); + if (cursor!=null) { + json.addProperty("cursor", cursor); + } + if (token!=null) { + json.addProperty("token", token); + } + if (limit!=null) { + json.addProperty("limit", limit); + } + if (voiceFormat!=null) { + json.addProperty("voice_format", voiceFormat); + } + if (openKfId != null) { + json.addProperty("open_kfid", openKfId); + } + + String responseContent = cpService.post(url, json); + return WxCpKfMsgListResp.fromJson(responseContent); + } + + @Override + public WxCpKfMsgSendResp sendMsg(WxCpKfMsgSendRequest request) throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(SEND_MSG); + + String responseContent = cpService.post(url, GSON.toJson(request)); + + return WxCpKfMsgSendResp.fromJson(responseContent); + } + + @Override + public WxCpKfMsgSendResp sendMsgOnEvent(WxCpKfMsgSendRequest request) throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(SEND_MSG_ON_EVENT); + + String responseContent = cpService.post(url, GSON.toJson(request)); + + return WxCpKfMsgSendResp.fromJson(responseContent); + } + + @Override + public WxCpKfCustomerBatchGetResp customerBatchGet(List externalUserIdList) + throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(CUSTOMER_BATCH_GET); + + JsonArray array = new JsonArray(); + + externalUserIdList.forEach(array::add); + JsonObject json = new JsonObject(); + json.add("external_userid_list", array); + String responseContent = cpService.post(url, json.toString()); + return WxCpKfCustomerBatchGetResp.fromJson(responseContent); + } + + @Override + public WxCpKfServiceUpgradeConfigResp getUpgradeServiceConfig() throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(CUSTOMER_GET_UPGRADE_SERVICE_CONFIG); + + String response = cpService.get(url, null); + return WxCpKfServiceUpgradeConfigResp.fromJson(response); + } + + @Override + public WxCpBaseResp upgradeMemberService(String openKfid, String externalUserId, + String userid, String wording) throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(CUSTOMER_UPGRADE_SERVICE); + + JsonObject json = new JsonObject(); + json.addProperty("open_kfid", openKfid); + json.addProperty("external_userid", externalUserId); + json.addProperty("type", 1); + + JsonObject memberJson = new JsonObject(); + memberJson.addProperty("userid", userid); + memberJson.addProperty("wording", wording); + json.add("member", memberJson); + + String response = cpService.post(url, json); + return WxCpBaseResp.fromJson(response); + } + + @Override + public WxCpBaseResp upgradeGroupchatService(String openKfid, String externalUserId, + String chatId, String wording) throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(CUSTOMER_UPGRADE_SERVICE); + + JsonObject json = new JsonObject(); + json.addProperty("open_kfid", openKfid); + json.addProperty("external_userid", externalUserId); + json.addProperty("type", 2); + + JsonObject groupchatJson = new JsonObject(); + groupchatJson.addProperty("chat_id", chatId); + groupchatJson.addProperty("wording", wording); + json.add("groupchat", groupchatJson); + + String response = cpService.post(url, json); + return WxCpBaseResp.fromJson(response); + } + + @Override + public WxCpBaseResp cancelUpgradeService(String openKfid, String externalUserId) + throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(CUSTOMER_CANCEL_UPGRADE_SERVICE); + + JsonObject json = new JsonObject(); + json.addProperty("open_kfid", openKfid); + json.addProperty("external_userid", externalUserId); + String response = cpService.post(url, json); + return WxCpBaseResp.fromJson(response); + } + + @Override + public WxCpKfGetCorpStatisticResp getCorpStatistic(WxCpKfGetCorpStatisticRequest request) throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(GET_CORP_STATISTIC); + String responseContent = cpService.post(url, GSON.toJson(request)); + return WxCpKfGetCorpStatisticResp.fromJson(responseContent); + } + + @Override + public WxCpKfGetServicerStatisticResp getServicerStatistic(WxCpKfGetServicerStatisticRequest request) throws WxErrorException { + String url = cpService.getWxCpConfigStorage().getApiUrl(GET_SERVICER_STATISTIC); + String responseContent = cpService.post(url, GSON.toJson(request)); + return WxCpKfGetServicerStatisticResp.fromJson(responseContent); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpLivingServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpLivingServiceImpl.java new file mode 100644 index 0000000000..b3d9e9a36e --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpLivingServiceImpl.java @@ -0,0 +1,119 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.json.GsonHelper; +import me.chanjar.weixin.common.util.json.GsonParser; +import me.chanjar.weixin.cp.api.WxCpLivingService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.living.*; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; +import org.apache.commons.lang3.StringUtils; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Living.*; + +/** + * 企业微信直播接口实现类. + * https://developer.work.weixin.qq.com/document/path/93633 + * + * @author Wang_Wong created on 2021-12-21 + */ +@Slf4j +@RequiredArgsConstructor +public class WxCpLivingServiceImpl implements WxCpLivingService { + private final WxCpService cpService; + + @Override + public String getLivingCode(String openId, String livingId) throws WxErrorException { + final String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_LIVING_CODE); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("openid", openId); + jsonObject.addProperty("livingid", livingId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return GsonHelper.getString(GsonParser.parse(responseContent), "living_code"); + } + + @Override + public WxCpLivingInfo getLivingInfo(String livingId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_LIVING_INFO) + livingId; + String responseContent = this.cpService.get(apiUrl, null); + return WxCpGsonBuilder.create() + .fromJson(GsonParser.parse(responseContent).get("living_info"), + new TypeToken() { + }.getType() + ); + } + + @Override + public WxCpWatchStat getWatchStat(String livingId, String nextKey) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_WATCH_STAT); + JsonObject jsonObject = new JsonObject(); + if (StringUtils.isNotBlank(nextKey)) { + jsonObject.addProperty("next_key", nextKey); + } + jsonObject.addProperty("livingid", livingId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpWatchStat.fromJson(responseContent); + } + + @Override + public WxCpLivingResult.LivingIdResult getUserAllLivingId(String userId, String cursor, Integer limit) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_USER_ALL_LIVINGID); + JsonObject jsonObject = new JsonObject(); + if (cursor != null) { + jsonObject.addProperty("cursor", cursor); + } + if (limit != null) { + jsonObject.addProperty("limit", limit); + } + jsonObject.addProperty("userid", userId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpLivingResult.LivingIdResult.fromJson(responseContent); + } + + @Override + public WxCpLivingShareInfo getLivingShareInfo(String wwShareCode) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_LIVING_SHARE_INFO); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("ww_share_code", wwShareCode); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpLivingShareInfo.fromJson(responseContent); + } + + @Override + public String livingCreate(WxCpLivingCreateRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(CREATE); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return GsonHelper.getString(GsonParser.parse(responseContent), "livingid"); + } + + @Override + public WxCpLivingResult livingModify(WxCpLivingModifyRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(MODIFY); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpLivingResult.fromJson(responseContent); + } + + @Override + public WxCpLivingResult livingCancel(@NonNull String livingId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(CANCEL); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("livingid", livingId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpLivingResult.fromJson(responseContent); + } + + @Override + public WxCpLivingResult deleteReplayData(@NonNull String livingId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(DELETE_REPLAY_DATA); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("livingid", livingId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpLivingResult.fromJson(responseContent); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImpl.java index 55579ff051..a128a35ccb 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMediaServiceImpl.java @@ -1,22 +1,37 @@ package me.chanjar.weixin.cp.api.impl; +import com.google.gson.JsonObject; import lombok.RequiredArgsConstructor; import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.error.WxRuntimeException; import me.chanjar.weixin.common.util.fs.FileUtils; import me.chanjar.weixin.common.util.http.BaseMediaDownloadRequestExecutor; +import me.chanjar.weixin.common.util.http.InputStreamData; +import me.chanjar.weixin.common.util.http.MediaInputStreamUploadRequestExecutor; import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor; -import me.chanjar.weixin.common.util.http.RequestExecutor; +import me.chanjar.weixin.common.util.json.GsonHelper; +import me.chanjar.weixin.common.util.json.GsonParser; import me.chanjar.weixin.cp.api.WxCpMediaService; import me.chanjar.weixin.cp.api.WxCpService; -import me.chanjar.weixin.cp.constant.WxCpApiPathConsts; +import me.chanjar.weixin.cp.bean.media.MediaUploadByUrlReq; +import me.chanjar.weixin.cp.bean.media.MediaUploadByUrlResult; +import org.apache.commons.io.IOUtils; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.file.Files; import java.util.UUID; -import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Media.*; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Media.GET_UPLOAD_BY_URL_RESULT; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Media.IMG_UPLOAD; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Media.JSSDK_MEDIA_GET; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Media.MEDIA_GET; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Media.MEDIA_UPLOAD; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Media.UPLOAD_BY_URL; /** *

@@ -36,6 +51,52 @@ public WxMediaUploadResult upload(String mediaType, String fileType, InputStream
     return this.upload(mediaType, FileUtils.createTmpFile(inputStream, UUID.randomUUID().toString(), fileType));
   }
 
+  @Override
+  public WxMediaUploadResult upload(String mediaType, String filename, String url) throws WxErrorException,
+    IOException {
+    HttpURLConnection conn = null;
+    InputStream inputStream = null;
+    try {
+      URL remote = new URL(url);
+      conn = (HttpURLConnection) remote.openConnection();
+      //设置超时间为3秒
+      conn.setConnectTimeout(60 * 1000);
+      //防止屏蔽程序抓取而返回403错误
+      conn.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
+      inputStream = conn.getInputStream();
+      return this.mainService.execute(MediaInputStreamUploadRequestExecutor.create(this.mainService.getRequestHttp())
+        , this.mainService.getWxCpConfigStorage().getApiUrl(MEDIA_UPLOAD + mediaType),
+        new InputStreamData(inputStream, filename));
+    } finally {
+      IOUtils.closeQuietly(inputStream);
+      if (conn != null) {
+        conn.disconnect();
+      }
+    }
+  }
+
+  @Override
+  public WxMediaUploadResult upload(String mediaType, File file, String filename) throws WxErrorException {
+    if(!file.exists()){
+      throw new WxRuntimeException("文件[" + file.getAbsolutePath() + "]不存在");
+    }
+    try (InputStream inputStream = Files.newInputStream(file.toPath())) {
+      return this.mainService.execute(MediaInputStreamUploadRequestExecutor.create(this.mainService.getRequestHttp())
+        , this.mainService.getWxCpConfigStorage().getApiUrl(MEDIA_UPLOAD + mediaType),
+        new InputStreamData(inputStream, filename));
+    } catch (IOException e) {
+      throw new WxRuntimeException(e);
+    }
+  }
+
+  @Override
+  public WxMediaUploadResult upload(String mediaType, InputStream inputStream, String filename) throws WxErrorException{
+    return this.mainService.execute(MediaInputStreamUploadRequestExecutor.create(this.mainService.getRequestHttp())
+      , this.mainService.getWxCpConfigStorage().getApiUrl(MEDIA_UPLOAD + mediaType),
+      new InputStreamData(inputStream, filename));
+  }
+
+
   @Override
   public WxMediaUploadResult upload(String mediaType, File file) throws WxErrorException {
     return this.mainService.execute(MediaUploadRequestExecutor.create(this.mainService.getRequestHttp()),
@@ -64,4 +125,20 @@ public String uploadImg(File file) throws WxErrorException {
     return this.mainService.execute(MediaUploadRequestExecutor.create(this.mainService.getRequestHttp()), url, file)
       .getUrl();
   }
+
+  @Override
+  public String uploadByUrl(MediaUploadByUrlReq req) throws WxErrorException {
+    final String url = this.mainService.getWxCpConfigStorage().getApiUrl(UPLOAD_BY_URL);
+    String responseContent = this.mainService.post(url, req.toJson());
+    return GsonHelper.getString(GsonParser.parse(responseContent), "jobid");
+  }
+
+  @Override
+  public MediaUploadByUrlResult uploadByUrl(String jobId) throws WxErrorException {
+    final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_UPLOAD_BY_URL_RESULT);
+    JsonObject jsonObject = new JsonObject();
+    jsonObject.addProperty("jobid", jobId);
+    String post = this.mainService.post(url, jsonObject.toString());
+    return MediaUploadByUrlResult.fromJson(post);
+  }
 }
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMeetingServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMeetingServiceImpl.java
new file mode 100644
index 0000000000..341bc97eab
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMeetingServiceImpl.java
@@ -0,0 +1,77 @@
+package me.chanjar.weixin.cp.api.impl;
+
+import com.google.common.collect.ImmutableMap;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.api.WxCpMeetingService;
+import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.bean.oa.meeting.WxCpMeeting;
+import me.chanjar.weixin.cp.bean.oa.meeting.WxCpMeetingUpdateResult;
+import me.chanjar.weixin.cp.bean.oa.meeting.WxCpUserMeetingIdResult;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Oa.*;
+
+/**
+ * 企业微信日程接口.
+ * 企业和开发者通过会议接口可以便捷地预定及管理会议,用于小组周会、部门例会等场景。
+ * 调用接口的应用自动成为会议创建者,也可指定成员作为会议管理员辅助管理。
+ * 官方文档:https://developer.work.weixin.qq.com/document/path/93626
+ *
+ * @author wangmeng3486 created on  2023-01-31
+ */
+@RequiredArgsConstructor
+public class WxCpMeetingServiceImpl implements WxCpMeetingService {
+  private final WxCpService cpService;
+
+  @Override
+  public String create(WxCpMeeting meeting) throws WxErrorException {
+    return this.cpService.post(this.cpService.getWxCpConfigStorage().getApiUrl(MEETING_ADD),
+      WxCpGsonBuilder.create().toJson(meeting));
+  }
+
+  @Override
+  public WxCpMeetingUpdateResult update(WxCpMeeting meeting) throws WxErrorException {
+    final String response = this.cpService.post(this.cpService.getWxCpConfigStorage().getApiUrl(MEETING_UPDATE),
+      WxCpGsonBuilder.create().toJson(meeting));
+    return WxCpGsonBuilder.create().fromJson(response, WxCpMeetingUpdateResult.class);
+  }
+
+  @Override
+  public void cancel(String meetingId) throws WxErrorException {
+    this.cpService.post(this.cpService.getWxCpConfigStorage().getApiUrl(MEETING_CANCEL),
+      WxCpGsonBuilder.create().toJson(ImmutableMap.of("meetingid", meetingId)));
+  }
+
+  @Override
+  public WxCpMeeting getDetail(String meetingId) throws WxErrorException {
+    final String response = this.cpService.post(this.cpService.getWxCpConfigStorage().getApiUrl(MEETING_DETAIL),
+      WxCpGsonBuilder.create().toJson(ImmutableMap.of("meetingid", meetingId)));
+    return WxCpGsonBuilder.create().fromJson(response, WxCpMeeting.class);
+  }
+
+  @Override
+  public WxCpUserMeetingIdResult getUserMeetingIds(String userId, String cursor, Integer limit,
+                                                   Long beginTime, Long endTime) throws WxErrorException {
+    final Map param = new HashMap<>(3);
+    param.put("userid", userId);
+    if (cursor != null) {
+      param.put("cursor", cursor);
+    }
+    if (limit != null) {
+      param.put("limit", limit);
+    }
+    if (beginTime != null) {
+      param.put("begin_time", beginTime);
+    }
+    if (endTime != null) {
+      param.put("end_time", endTime);
+    }
+    final String response = this.cpService.post(this.cpService.getWxCpConfigStorage().getApiUrl(GET_USER_MEETING_ID),
+      WxCpGsonBuilder.create().toJson(param));
+    return WxCpGsonBuilder.create().fromJson(response, WxCpUserMeetingIdResult.class);
+  }
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMenuServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMenuServiceImpl.java
index 85abe71f45..d008e77083 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMenuServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMenuServiceImpl.java
@@ -5,7 +5,6 @@
 import me.chanjar.weixin.common.error.WxErrorException;
 import me.chanjar.weixin.cp.api.WxCpMenuService;
 import me.chanjar.weixin.cp.api.WxCpService;
-import me.chanjar.weixin.cp.constant.WxCpApiPathConsts;
 import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
 
 import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Menu.*;
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMessageServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMessageServiceImpl.java
new file mode 100644
index 0000000000..6daea8ef2f
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMessageServiceImpl.java
@@ -0,0 +1,68 @@
+package me.chanjar.weixin.cp.api.impl;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.gson.JsonObject;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.api.WxCpMessageService;
+import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.bean.message.*;
+import me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Message;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+/**
+ * 消息推送接口实现类.
+ *
+ * @author Binary Wang created on  2020-08-30
+ */
+@RequiredArgsConstructor
+public class WxCpMessageServiceImpl implements WxCpMessageService {
+  private final WxCpService cpService;
+
+  @Override
+  public WxCpMessageSendResult send(WxCpMessage message) throws WxErrorException {
+    Integer agentId = message.getAgentId();
+    if (null == agentId) {
+      message.setAgentId(this.cpService.getWxCpConfigStorage().getAgentId());
+    }
+
+    return WxCpMessageSendResult.fromJson(this.cpService.post(this.cpService.getWxCpConfigStorage()
+      .getApiUrl(Message.MESSAGE_SEND), message.toJson()));
+  }
+
+  @Override
+  public WxCpMessageSendStatistics getStatistics(int timeType) throws WxErrorException {
+    return WxCpMessageSendStatistics.fromJson(this.cpService.post(this.cpService.getWxCpConfigStorage().getApiUrl(Message.GET_STATISTICS),
+      WxCpGsonBuilder.create().toJson(ImmutableMap.of("time_type", timeType))));
+  }
+
+  @Override
+  public WxCpLinkedCorpMessageSendResult sendLinkedCorpMessage(WxCpLinkedCorpMessage message) throws WxErrorException {
+    Integer agentId = message.getAgentId();
+    if (null == agentId) {
+      message.setAgentId(this.cpService.getWxCpConfigStorage().getAgentId());
+    }
+
+    return WxCpLinkedCorpMessageSendResult.fromJson(this.cpService.post(this.cpService.getWxCpConfigStorage()
+      .getApiUrl(Message.LINKEDCORP_MESSAGE_SEND), message.toJson()));
+  }
+
+  @Override
+  public WxCpSchoolContactMessageSendResult sendSchoolContactMessage(WxCpSchoolContactMessage message) throws WxErrorException {
+    if (null == message.getAgentId()) {
+      message.setAgentId(this.cpService.getWxCpConfigStorage().getAgentId());
+    }
+
+    return WxCpSchoolContactMessageSendResult.fromJson(this.cpService.post(this.cpService.getWxCpConfigStorage()
+      .getApiUrl(Message.EXTERNAL_CONTACT_MESSAGE_SEND), message.toJson()));
+  }
+
+  @Override
+  public void recall(String msgId) throws WxErrorException {
+    JsonObject jsonObject = new JsonObject();
+    jsonObject.addProperty("msgid", msgId);
+    String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(Message.MESSAGE_RECALL);
+    this.cpService.post(apiUrl, jsonObject.toString());
+  }
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java
new file mode 100644
index 0000000000..be6588bc7b
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMsgAuditServiceImpl.java
@@ -0,0 +1,414 @@
+package me.chanjar.weixin.cp.api.impl;
+
+import com.google.gson.JsonObject;
+import com.google.gson.reflect.TypeToken;
+import com.tencent.wework.Finance;
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.json.GsonParser;
+import me.chanjar.weixin.cp.api.WxCpMsgAuditService;
+import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.bean.msgaudit.*;
+import me.chanjar.weixin.cp.config.WxCpConfigStorage;
+import me.chanjar.weixin.cp.util.crypto.WxCpCryptUtil;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.MsgAudit.*;
+
+/**
+ * 会话内容存档接口实现类.
+ *
+ * @author Wang_Wong  created on  2022-01-17
+ */
+@Slf4j
+@RequiredArgsConstructor
+public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService {
+  private final WxCpService cpService;
+
+  /** 每个线程持有独立 SDK 实例,懒初始化,线程内跨调用复用 */
+  private final ThreadLocal threadLocalSdk = new ThreadLocal<>();
+
+  /** 跟踪所有已创建的 SDK,用于 closeAllSdks() 统一清理 */
+  private final Set managedSdks = ConcurrentHashMap.newKeySet();
+
+  @Override
+  public WxCpChatDatas getChatDatas(long seq, @NonNull long limit, String proxy, String passwd,
+                                    @NonNull long timeout) throws Exception {
+    // 旧版 API:每次调用创建新 SDK,由调用方负责通过 Finance.DestroySdk(chatDatas.getSdk()) 释放
+    long sdk = this.createSdk();
+
+    long slice = Finance.NewSlice();
+    long ret = Finance.GetChatData(sdk, seq, limit, proxy, passwd, timeout, slice);
+    if (ret != 0) {
+      Finance.FreeSlice(slice);
+      throw new WxErrorException("getchatdata err ret " + ret);
+    }
+
+    // 拉取会话存档
+    String content = Finance.GetContentFromSlice(slice);
+    Finance.FreeSlice(slice);
+    WxCpChatDatas chatDatas = WxCpChatDatas.fromJson(content);
+    if (chatDatas.getErrCode().intValue() != 0) {
+      throw new WxErrorException(chatDatas.toJson());
+    }
+
+    chatDatas.setSdk(sdk);
+    return chatDatas;
+  }
+
+  /**
+   * 获取当前线程的 SDK,不存在则初始化。
+   * SDK 在线程内跨调用复用,无需每次重新初始化。
+   *
+   * @return sdk id
+   * @throws WxErrorException 初始化失败时抛出异常
+   */
+  private long getOrInitThreadLocalSdk() throws WxErrorException {
+    Long sdk = threadLocalSdk.get();
+    if (sdk != null && sdk > 0) {
+      // 校验句柄是否仍受管理:closeAllSdks() 后其他线程 ThreadLocal 可能保留已销毁的 id
+      if (managedSdks.contains(sdk)) {
+        return sdk;
+      }
+      log.warn("线程 [{}] 发现已失效的会话存档SDK句柄 sdk={},请检查调用逻辑", Thread.currentThread().getName(), sdk);
+      threadLocalSdk.remove();
+      throw new WxErrorException("线程 [" + Thread.currentThread().getName() + "] 获取会话存档SDK失败,请检查是否已调用 closeAllSdks()");
+    }
+    long newSdk = createSdk();
+    threadLocalSdk.set(newSdk);
+    managedSdks.add(newSdk);
+    log.info("线程 [{}] 初始化会话存档SDK成功,sdk={}", Thread.currentThread().getName(), newSdk);
+    return newSdk;
+  }
+
+  /**
+   * 创建并初始化一个新的会话存档 SDK 实例。
+   * 

通常通过 {@link #getOrInitThreadLocalSdk()} 间接调用以复用 ThreadLocal 中的实例; + * 旧版直接暴露 sdk 的 API(如 {@link #getChatDatas})也会直接调用本方法,此时 SDK 由调用方自行管理。

+ *

Finance.loadingLibraries() 底层依赖 System.load(),JVM 保证同一库不重复加载,多线程并发调用安全。

+ */ + private long createSdk() throws WxErrorException { + WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage(); + + String configPath = configStorage.getMsgAuditLibPath(); + if (StringUtils.isEmpty(configPath)) { + throw new WxErrorException("请配置会话存档sdk文件的路径,不要配错了!!"); + } + + // 替换斜杠 + String replacePath = configPath.replace("\\", "/"); + // 获取最后一个斜杠的下标,用作分割路径 + int lastIndex = replacePath.lastIndexOf("/") + 1; + // 获取完整路径的前缀路径 + String prefixPath = replacePath.substring(0, lastIndex); + // 获取后缀的所有文件,目的遍历所有文件 + String suffixFiles = replacePath.substring(lastIndex); + + // 包含so文件 + String[] libFiles = suffixFiles.split(","); + if (libFiles.length <= 0) { + throw new WxErrorException("请仔细配置会话存档文件路径!!"); + } + + List libList = Arrays.asList(libFiles); + // 判断windows系统会话存档sdk中dll的加载,因为会互相依赖所以是有顺序的,否则会导致无法加载sdk #2598 + List osLib = new LinkedList<>(); + List fileLib = new ArrayList<>(); + libList.forEach(s -> { + if (s.contains("lib")) { + osLib.add(s); + } else { + fileLib.add(s); + } + }); + osLib.addAll(fileLib); + + Finance.loadingLibraries(osLib, prefixPath); + long sdk = Finance.NewSdk(); + // 因为会话存档单独有个secret,优先使用会话存档的secret + String msgAuditSecret = configStorage.getMsgAuditSecret(); + if (StringUtils.isEmpty(msgAuditSecret)) { + msgAuditSecret = configStorage.getCorpSecret(); + } + long ret = Finance.Init(sdk, configStorage.getCorpId(), msgAuditSecret); + if (ret != 0) { + Finance.DestroySdk(sdk); + throw new WxErrorException("init sdk err ret " + ret); + } + return sdk; + } + + @Override + public void closeThreadLocalSdk() { + Long sdk = threadLocalSdk.get(); + // 先从 managedSdks 摘除,摘除成功才调 DestroySdk,防止与 closeAllSdks() 并发时 double-free + if (sdk != null && managedSdks.remove(sdk)) { + Finance.DestroySdk(sdk); + log.info("线程 [{}] 关闭会话存档SDK,sdk={}", Thread.currentThread().getName(), sdk); + } + threadLocalSdk.remove(); + } + + @Override + public void closeAllSdks() { + // 逐一 remove 后再 Destroy,防止与 closeThreadLocalSdk() 并发时 double-free + Long[] sdks = managedSdks.toArray(new Long[0]); + for (Long sdk : sdks) { + if (managedSdks.remove(sdk)) { + Finance.DestroySdk(sdk); + log.info("关闭会话存档SDK,sdk={}", sdk); + } + } + threadLocalSdk.remove(); + } + + @Override + public WxCpChatModel getDecryptData(@NonNull long sdk, @NonNull WxCpChatDatas.WxCpChatData chatData, + @NonNull Integer pkcs1) throws Exception { + String plainText = this.decryptChatData(sdk, chatData, pkcs1); + return WxCpChatModel.fromJson(plainText); + } + + /** + * Decrypt chat data string. + * + * @param sdk the sdk + * @param chatData the chat data + * @param pkcs1 the pkcs 1 + * @return the string + * @throws Exception the exception + */ + public String decryptChatData(long sdk, WxCpChatDatas.WxCpChatData chatData, Integer pkcs1) throws Exception { + // 企业获取的会话内容,使用企业自行配置的消息加密公钥进行加密,企业可用自行保存的私钥解开会话内容数据。 + // msgAuditPriKey 会话存档私钥不能为空 + String priKey = cpService.getWxCpConfigStorage().getMsgAuditPriKey(); + if (StringUtils.isEmpty(priKey)) { + throw new WxErrorException("请配置会话存档私钥【msgAuditPriKey】"); + } + + String decryptByPriKey = WxCpCryptUtil.decryptPriKey(chatData.getEncryptRandomKey(), priKey, pkcs1); + // 每次使用DecryptData解密会话存档前需要调用NewSlice获取一个slice,在使用完slice中数据后,还需要调用FreeSlice释放。 + long msg = Finance.NewSlice(); + + // 解密会话存档内容 + // sdk不会要求用户传入rsa私钥,保证用户会话存档数据只有自己能够解密。 + // 此处需要用户先用rsa私钥解密encrypt_random_key后,作为encrypt_key参数传入sdk来解密encrypt_chat_msg获取会话存档明文。 + int ret = Finance.DecryptData(sdk, decryptByPriKey, chatData.getEncryptChatMsg(), msg); + if (ret != 0) { + Finance.FreeSlice(msg); + throw new WxErrorException("msg err ret " + ret); + } + + // 明文 + String plainText = Finance.GetContentFromSlice(msg); + Finance.FreeSlice(msg); + return plainText; + } + + @Override + public String getChatPlainText(@NonNull long sdk, WxCpChatDatas.@NonNull WxCpChatData chatData, + @NonNull Integer pkcs1) throws Exception { + return this.decryptChatData(sdk, chatData, pkcs1); + } + + @Override + public void getMediaFile(@NonNull long sdk, @NonNull String sdkfileid, String proxy, String passwd, + @NonNull long timeout, @NonNull String targetFilePath) throws WxErrorException { + /** + * 1、媒体文件每次拉取的最大size为512k,因此超过512k的文件需要分片拉取。 + * 2、若该文件未拉取完整,sdk的IsMediaDataFinish接口会返回0,同时通过GetOutIndexBuf接口返回下次拉取需要传入GetMediaData的indexbuf。 + * 3、indexbuf一般格式如右侧所示,”Range:bytes=524288-1048575“:表示这次拉取的是从524288到1048575的分片。单个文件首次拉取填写的indexbuf + * 为空字符串,拉取后续分片时直接填入上次返回的indexbuf即可。 + */ + File targetFile = new File(targetFilePath); + File parentDir = targetFile.getParentFile(); + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs(); + } + this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, i -> { + try { + // 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。 + try (FileOutputStream outputStream = new FileOutputStream(targetFile, true)) { + outputStream.write(i); + } + } catch (Exception e) { + log.error("写入媒体文件分片失败,targetFilePath={}", targetFilePath, e); + } + }); + } + + @Override + public void getMediaFile(@NonNull long sdk, @NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, @NonNull Consumer action) throws WxErrorException { + /** + * 1、媒体文件每次拉取的最大size为512k,因此超过512k的文件需要分片拉取。 + * 2、若该文件未拉取完整,sdk的IsMediaDataFinish接口会返回0,同时通过GetOutIndexBuf接口返回下次拉取需要传入GetMediaData的indexbuf。 + * 3、indexbuf一般格式如右侧所示,”Range:bytes=524288-1048575“:表示这次拉取的是从524288到1048575的分片。单个文件首次拉取填写的indexbuf为空字符串,拉取后续分片时直接填入上次返回的indexbuf即可。 + */ + String indexbuf = ""; + int ret, data_len = 0; + log.debug("正在分片拉取媒体文件 sdkFileId为{}", sdkfileid); + while (true) { + long mediaData = Finance.NewMediaData(); + ret = Finance.GetMediaData(sdk, indexbuf, sdkfileid, proxy, passwd, timeout, mediaData); + if (ret != 0) { + Finance.FreeMediaData(mediaData); + throw new WxErrorException("getmediadata err ret " + ret); + } + + data_len += Finance.GetDataLen(mediaData); + log.debug("正在分片拉取媒体文件 len:{}, data_len:{}, is_finish:{} \n", Finance.GetIndexLen(mediaData), data_len, Finance.IsMediaDataFinish(mediaData)); + + try { + // 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。 + action.accept(Finance.GetData(mediaData)); + } catch (Exception e) { + log.error("处理媒体文件分片失败,sdkfileid={}", sdkfileid, e); + } + + if (Finance.IsMediaDataFinish(mediaData) == 1) { + // 已经拉取完成最后一个分片 + Finance.FreeMediaData(mediaData); + break; + } else { + // 获取下次拉取需要使用的indexbuf + indexbuf = Finance.GetOutIndexBuf(mediaData); + Finance.FreeMediaData(mediaData); + } + } + } + + @Override + public List getPermitUserList(Integer type) throws WxErrorException { + final String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_PERMIT_USER_LIST); + JsonObject jsonObject = new JsonObject(); + if (type != null) { + jsonObject.addProperty("type", type); + } + String responseContent = this.cpService.postForMsgAudit(apiUrl, jsonObject.toString()); + return WxCpGsonBuilder.create().fromJson(GsonParser.parse(responseContent).getAsJsonArray("ids"), + new TypeToken>() { + }.getType()); + } + + @Override + public WxCpGroupChat getGroupChat(@NonNull String roomid) throws WxErrorException { + final String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_GROUP_CHAT); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("roomid", roomid); + String responseContent = this.cpService.postForMsgAudit(apiUrl, jsonObject.toString()); + return WxCpGroupChat.fromJson(responseContent); + } + + @Override + public WxCpAgreeInfo checkSingleAgree(@NonNull WxCpCheckAgreeRequest checkAgreeRequest) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(CHECK_SINGLE_AGREE); + String responseContent = this.cpService.postForMsgAudit(apiUrl, checkAgreeRequest.toJson()); + return WxCpAgreeInfo.fromJson(responseContent); + } + + @Override + public List getChatRecords(long seq, @NonNull long limit, String proxy, String passwd, + @NonNull long timeout) throws Exception { + long sdk = this.getOrInitThreadLocalSdk(); + + long slice = Finance.NewSlice(); + long ret = Finance.GetChatData(sdk, seq, limit, proxy, passwd, timeout, slice); + if (ret != 0) { + Finance.FreeSlice(slice); + throw new WxErrorException("getchatdata err ret " + ret); + } + + // 拉取会话存档 + String content = Finance.GetContentFromSlice(slice); + Finance.FreeSlice(slice); + WxCpChatDatas chatDatas = WxCpChatDatas.fromJson(content); + if (chatDatas.getErrCode().intValue() != 0) { + throw new WxErrorException(chatDatas.toJson()); + } + + List chatDataList = chatDatas.getChatData(); + return chatDataList != null ? chatDataList : Collections.emptyList(); + } + + @Override + public WxCpChatModel getDecryptChatData(@NonNull WxCpChatDatas.WxCpChatData chatData, + @NonNull Integer pkcs1) throws Exception { + long sdk = this.getOrInitThreadLocalSdk(); + String plainText = this.decryptChatData(sdk, chatData, pkcs1); + return WxCpChatModel.fromJson(plainText); + } + + @Override + public String getChatRecordPlainText(@NonNull WxCpChatDatas.WxCpChatData chatData, + @NonNull Integer pkcs1) throws Exception { + long sdk = this.getOrInitThreadLocalSdk(); + return this.decryptChatData(sdk, chatData, pkcs1); + } + + @Override + public void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, + @NonNull String targetFilePath) throws WxErrorException { + long sdk; + try { + sdk = this.getOrInitThreadLocalSdk(); + } catch (Exception e) { + throw new WxErrorException(e); + } + + // 使用AtomicReference捕获Lambda中的异常,以便在执行完后抛出 + final java.util.concurrent.atomic.AtomicReference exceptionHolder = new java.util.concurrent.atomic.AtomicReference<>(); + + File targetFile = new File(targetFilePath); + File parentDir = targetFile.getParentFile(); + if (parentDir != null && !parentDir.exists()) { + parentDir.mkdirs(); + } + this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, i -> { + // 如果之前已经发生异常,不再继续处理 + if (exceptionHolder.get() != null) { + return; + } + try { + // 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。 + try (FileOutputStream outputStream = new FileOutputStream(targetFile, true)) { + outputStream.write(i); + } + } catch (Exception e) { + exceptionHolder.set(e); + } + }); + + // 检查是否发生异常,如果有则抛出 + Exception caughtException = exceptionHolder.get(); + if (caughtException != null) { + throw new WxErrorException(caughtException); + } + } + + @Override + public void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, + @NonNull Consumer action) throws WxErrorException { + long sdk; + try { + sdk = this.getOrInitThreadLocalSdk(); + } catch (Exception e) { + throw new WxErrorException(e); + } + this.getMediaFile(sdk, sdkfileid, proxy, passwd, timeout, action); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOAuth2ServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOAuth2ServiceImpl.java index 2005bc36eb..d04a051c0e 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOAuth2ServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOAuth2ServiceImpl.java @@ -1,24 +1,21 @@ package me.chanjar.weixin.cp.api.impl; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import com.google.gson.JsonParser; -import lombok.AllArgsConstructor; import lombok.RequiredArgsConstructor; -import me.chanjar.weixin.common.api.WxConsts; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.util.http.URIUtil; import me.chanjar.weixin.common.util.json.GsonHelper; +import me.chanjar.weixin.common.util.json.GsonParser; import me.chanjar.weixin.cp.api.WxCpOAuth2Service; import me.chanjar.weixin.cp.api.WxCpService; import me.chanjar.weixin.cp.bean.WxCpOauth2UserInfo; import me.chanjar.weixin.cp.bean.WxCpUserDetail; -import me.chanjar.weixin.cp.constant.WxCpApiPathConsts; +import me.chanjar.weixin.cp.bean.workbench.WxCpSecondVerificationInfo; import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; +import java.util.Optional; + import static me.chanjar.weixin.common.api.WxConsts.OAuth2Scope.*; -import static me.chanjar.weixin.common.api.WxConsts.OAuth2Scope.SNSAPI_PRIVATEINFO; -import static me.chanjar.weixin.common.api.WxConsts.OAuth2Scope.SNSAPI_USERINFO; import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.OAuth2.*; /** @@ -73,16 +70,34 @@ public WxCpOauth2UserInfo getUserInfo(String code) throws WxErrorException { @Override public WxCpOauth2UserInfo getUserInfo(Integer agentId, String code) throws WxErrorException { - String responseText = this.mainService.get(String.format(this.mainService.getWxCpConfigStorage().getApiUrl(GET_USER_INFO), code, agentId), null); - JsonElement je = new JsonParser().parse(responseText); - JsonObject jo = je.getAsJsonObject(); + String responseText = + this.mainService.get(String.format(this.mainService.getWxCpConfigStorage().getApiUrl(GET_USER_INFO), code, + agentId), null); + JsonObject jo = GsonParser.parse(responseText); return WxCpOauth2UserInfo.builder() - .userId(GsonHelper.getString(jo, "UserId")) + .userId(Optional.ofNullable(GsonHelper.getString(jo, "UserId")).orElse(GsonHelper.getString(jo, "userid"))) .deviceId(GsonHelper.getString(jo, "DeviceId")) - .openId(GsonHelper.getString(jo, "OpenId")) + .openId(Optional.ofNullable(GsonHelper.getString(jo, "OpenId")).orElse(GsonHelper.getString(jo, "openid"))) .userTicket(GsonHelper.getString(jo, "user_ticket")) .expiresIn(GsonHelper.getString(jo, "expires_in")) + .externalUserId(GsonHelper.getString(jo, "external_userid")) + .parentUserId(GsonHelper.getString(jo, "parent_userid")) + .studentUserId(GsonHelper.getString(jo, "student_userid")) + .build(); + } + + @Override + public WxCpOauth2UserInfo getSchoolUserInfo(String code) throws WxErrorException { + String responseText = + this.mainService.get(String.format(this.mainService.getWxCpConfigStorage().getApiUrl(GET_SCHOOL_USER_INFO), + code), null); + JsonObject jo = GsonParser.parse(responseText); + + return WxCpOauth2UserInfo.builder() + .deviceId(GsonHelper.getString(jo, "DeviceId")) + .parentUserId(GsonHelper.getString(jo, "parent_userid")) + .studentUserId(GsonHelper.getString(jo, "student_userid")) .build(); } @@ -90,7 +105,29 @@ public WxCpOauth2UserInfo getUserInfo(Integer agentId, String code) throws WxErr public WxCpUserDetail getUserDetail(String userTicket) throws WxErrorException { JsonObject param = new JsonObject(); param.addProperty("user_ticket", userTicket); - String responseText = this.mainService.post(this.mainService.getWxCpConfigStorage().getApiUrl(GET_USER_DETAIL), param.toString()); + String responseText = this.mainService.post(this.mainService.getWxCpConfigStorage().getApiUrl(GET_USER_DETAIL), + param.toString()); return WxCpGsonBuilder.create().fromJson(responseText, WxCpUserDetail.class); } + + @Override + public WxCpOauth2UserInfo getAuthUserInfo(String code) throws WxErrorException { + String responseText = + this.mainService.get(String.format(this.mainService.getWxCpConfigStorage().getApiUrl(GET_USER_AUTH_INFO), code), null); + JsonObject jo = GsonParser.parse(responseText); + + return WxCpOauth2UserInfo.builder() + .userId(GsonHelper.getString(jo, "userid")) + .openId(GsonHelper.getString(jo, "openid")) + .userTicket(GsonHelper.getString(jo, "user_ticket")) + .externalUserId(GsonHelper.getString(jo, "external_userid")) + .build(); + } + + @Override + public WxCpSecondVerificationInfo getTfaInfo(String code) throws WxErrorException { + String responseText = this.mainService.post(this.mainService.getWxCpConfigStorage().getApiUrl(GET_TFA_INFO), + GsonHelper.buildJsonObject("code", code)); + return WxCpGsonBuilder.create().fromJson(responseText, WxCpSecondVerificationInfo.class); + } } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOMailServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOMailServiceImpl.java new file mode 100644 index 0000000000..c3844464e0 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOMailServiceImpl.java @@ -0,0 +1,80 @@ +package me.chanjar.weixin.cp.api.impl; + +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.api.WxCpOaMailService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; +import me.chanjar.weixin.cp.bean.oa.mail.WxCpMailCommonSendRequest; +import me.chanjar.weixin.cp.bean.oa.mail.WxCpMailMeetingSendRequest; +import me.chanjar.weixin.cp.bean.oa.mail.WxCpMailScheduleSendRequest; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Oa.EXMAIL_APP_COMPOSE_SEND; + +/** + * 企业微信邮件接口实现类. + * + * @author Hugo + */ +@Slf4j +@RequiredArgsConstructor +public class WxCpOMailServiceImpl implements WxCpOaMailService { + private final WxCpService cpService; + + /** + * 发送普通邮件 + * 应用可以通过该接口发送普通邮件,支持附件能力。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param request 发送普通邮件请求参数 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + @Override + public WxCpBaseResp mailCommonSend(@NonNull WxCpMailCommonSendRequest request) throws WxErrorException { + return this.mailSend(request.toJson()); + } + + /** + * 发送日程邮件 + * 应用可以通过该接口发送日程邮件。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param request 发送日程邮件请求参数 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + @Override + public WxCpBaseResp mailScheduleSend(@NonNull WxCpMailScheduleSendRequest request) throws WxErrorException { + return this.mailSend(request.toJson()); + } + + /** + * 发送会议邮件 + * 应用可以通过该接口发送会议邮件。 + *

+ * 请求方式:POST(HTTPS) + * 请求地址: ... + * + * @param request 发送会议邮件请求参数 + * @return wx cp base resp + * @throws WxErrorException the wx error exception + */ + @Override + public WxCpBaseResp mailMeetingSend(@NonNull WxCpMailMeetingSendRequest request) throws WxErrorException { + + return this.mailSend(request.toJson()); + } + + private WxCpBaseResp mailSend(String request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(EXMAIL_APP_COMPOSE_SEND); + String responseContent = this.cpService.post(apiUrl, request); + return WxCpBaseResp.fromJson(responseContent); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaAgentServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaAgentServiceImpl.java new file mode 100644 index 0000000000..250ee0cb24 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaAgentServiceImpl.java @@ -0,0 +1,40 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.gson.JsonObject; +import com.google.gson.reflect.TypeToken; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.json.GsonParser; +import me.chanjar.weixin.cp.api.WxCpOaAgentService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.oa.selfagent.WxCpOpenApprovalData; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Oa.GET_OPEN_APPROVAL_DATA; + +/** + * 企业微信自建应用接口实现类. + * + * @author Wang_Wong created on 2022-04-06 + */ +@Slf4j +@RequiredArgsConstructor +public class WxCpOaAgentServiceImpl implements WxCpOaAgentService { + private final WxCpService cpService; + + @Override + public WxCpOpenApprovalData getOpenApprovalData(@NonNull String thirdNo) throws WxErrorException { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("thirdNo", thirdNo); + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_OPEN_APPROVAL_DATA); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpGsonBuilder.create() + .fromJson(GsonParser.parse(responseContent).get("data"), + new TypeToken() { + }.getType() + ); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaCalendarServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaCalendarServiceImpl.java new file mode 100644 index 0000000000..ef24204493 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaCalendarServiceImpl.java @@ -0,0 +1,50 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.gson.reflect.TypeToken; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.json.GsonHelper; +import me.chanjar.weixin.common.util.json.GsonParser; +import me.chanjar.weixin.cp.api.WxCpOaCalendarService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.oa.calendar.WxCpOaCalendar; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.util.List; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Oa.*; + +/** + * . + * + * @author Binary Wang created on 2020-09-20 + */ +@RequiredArgsConstructor +public class WxCpOaCalendarServiceImpl implements WxCpOaCalendarService { + private final WxCpService wxCpService; + + @Override + public String add(WxCpOaCalendar calendar) throws WxErrorException { + return this.wxCpService.post(this.wxCpService.getWxCpConfigStorage().getApiUrl(CALENDAR_ADD), calendar); + } + + @Override + public void update(WxCpOaCalendar calendar) throws WxErrorException { + this.wxCpService.post(this.wxCpService.getWxCpConfigStorage().getApiUrl(CALENDAR_UPDATE), calendar); + } + + @Override + public List get(List calIds) throws WxErrorException { + String response = this.wxCpService.post(this.wxCpService.getWxCpConfigStorage().getApiUrl(CALENDAR_GET), + GsonHelper.buildJsonObject("cal_id_list", calIds)); + return WxCpGsonBuilder.create().fromJson(GsonParser.parse(response).get("calendar_list").getAsJsonArray().toString(), + new TypeToken>() { + }.getType()); + } + + @Override + public void delete(String calId) throws WxErrorException { + this.wxCpService.post(this.wxCpService.getWxCpConfigStorage().getApiUrl(CALENDAR_DEL), + GsonHelper.buildJsonObject("cal_id", calId)); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaMeetingRoomServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaMeetingRoomServiceImpl.java new file mode 100644 index 0000000000..9c32a45235 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaMeetingRoomServiceImpl.java @@ -0,0 +1,87 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.gson.reflect.TypeToken; +import lombok.RequiredArgsConstructor; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.json.GsonHelper; +import me.chanjar.weixin.common.util.json.GsonParser; +import me.chanjar.weixin.cp.api.WxCpOaMeetingRoomService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.oa.meetingroom.*; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.util.List; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Oa.*; + +/** + * The type Wx cp oa meeting room service. + * + * @author fcat + * @version 1.0 Create by 2022/8/12 23:49 + */ +@RequiredArgsConstructor +public class WxCpOaMeetingRoomServiceImpl implements WxCpOaMeetingRoomService { + private final WxCpService wxCpService; + + @Override + public String addMeetingRoom(WxCpOaMeetingRoom meetingRoom) throws WxErrorException { + return this.wxCpService.post(this.wxCpService.getWxCpConfigStorage().getApiUrl(MEETINGROOM_ADD), meetingRoom); + } + + @Override + public List listMeetingRoom(WxCpOaMeetingRoom meetingRoomRequest) throws WxErrorException { + String response = this.wxCpService.post(this.wxCpService.getWxCpConfigStorage().getApiUrl(MEETINGROOM_LIST), + meetingRoomRequest); + return WxCpGsonBuilder.create().fromJson(GsonParser.parse(response).get("meetingroom_list").getAsJsonArray().toString(), + new TypeToken>() { + }.getType()); + } + + @Override + public void editMeetingRoom(WxCpOaMeetingRoom meetingRoom) throws WxErrorException { + this.wxCpService.post(this.wxCpService.getWxCpConfigStorage().getApiUrl(MEETINGROOM_EDIT), meetingRoom); + } + + @Override + public void deleteMeetingRoom(Integer meetingRoomId) throws WxErrorException { + this.wxCpService.post(this.wxCpService.getWxCpConfigStorage().getApiUrl(MEETINGROOM_DEL), + GsonHelper.buildJsonObject("meetingroom_id", meetingRoomId)); + } + + @Override + public WxCpOaMeetingRoomBookingInfoResult getMeetingRoomBookingInfo(WxCpOaMeetingRoomBookingInfoRequest wxCpOaMeetingRoomBookingInfoRequest) throws WxErrorException { + String response = this.wxCpService.post(this.wxCpService.getWxCpConfigStorage().getApiUrl(MEETINGROOM_GET_BOOKING_INFO), wxCpOaMeetingRoomBookingInfoRequest); + return WxCpOaMeetingRoomBookingInfoResult.fromJson(response); + } + + @Override + public WxCpOaMeetingRoomBookResult bookingMeetingRoom(WxCpOaMeetingRoomBookRequest wxCpOaMeetingRoomBookRequest) throws WxErrorException { + String response = this.wxCpService.post(this.wxCpService.getWxCpConfigStorage().getApiUrl(MEETINGROOM_BOOK), wxCpOaMeetingRoomBookRequest); + return WxCpOaMeetingRoomBookResult.fromJson(response); + } + + @Override + public WxCpOaMeetingRoomBookResult bookingMeetingRoomBySchedule(WxCpOaMeetingRoomBookByScheduleRequest wxCpOaMeetingRoomBookByScheduleRequest) throws WxErrorException { + String response = this.wxCpService.post(this.wxCpService.getWxCpConfigStorage().getApiUrl(MEETINGROOM_BOOK_BY_SCHEDULE), wxCpOaMeetingRoomBookByScheduleRequest); + return WxCpOaMeetingRoomBookResult.fromJson(response); + } + + @Override + public WxCpOaMeetingRoomBookResult bookingMeetingRoomByMeeting(WxCpOaMeetingRoomBookByMeetingRequest wxCpOaMeetingRoomBookByMeetingRequest) throws WxErrorException { + String response = this.wxCpService.post(this.wxCpService.getWxCpConfigStorage().getApiUrl(MEETINGROOM_BOOK_BY_MEETING), wxCpOaMeetingRoomBookByMeetingRequest); + return WxCpOaMeetingRoomBookResult.fromJson(response); + } + + @Override + public void cancelBookMeetingRoom(WxCpOaMeetingRoomCancelBookRequest wxCpOaMeetingRoomCancelBookRequest) throws WxErrorException { + this.wxCpService.post(this.wxCpService.getWxCpConfigStorage().getApiUrl(MEETINGROOM_CANCEL_BOOK), wxCpOaMeetingRoomCancelBookRequest); + + } + + @Override + public WxCpOaMeetingRoomBookingInfoByBookingIdResult getBookingInfoByBookingId(WxCpOaMeetingRoomBookingInfoByBookingIdRequest wxCpOaMeetingRoomBookingInfoByBookingIdRequest) throws WxErrorException { + String response = this.wxCpService.post(this.wxCpService.getWxCpConfigStorage().getApiUrl(MEETINGROOM_BOOKINFO_GET), wxCpOaMeetingRoomBookingInfoByBookingIdRequest); + return WxCpOaMeetingRoomBookingInfoByBookingIdResult.fromJson(response); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaOaScheduleServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaOaScheduleServiceImpl.java new file mode 100644 index 0000000000..c9a6161b2e --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaOaScheduleServiceImpl.java @@ -0,0 +1,81 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.reflect.TypeToken; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.json.GsonParser; +import me.chanjar.weixin.cp.api.WxCpOaScheduleService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.oa.WxCpOaSchedule; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Oa.*; + +/** + * 企业微信日程接口实现类. + * + * @author Binary Wang created on 2020-12-25 + */ +@Slf4j +@RequiredArgsConstructor +public class WxCpOaOaScheduleServiceImpl implements WxCpOaScheduleService { + private final WxCpService cpService; + + @Override + public String add(WxCpOaSchedule schedule, Integer agentId) throws WxErrorException { + Map param; + if (agentId == null) { + param = ImmutableMap.of("schedule", schedule); + } else { + param = ImmutableMap.of("schedule", schedule, "agentid", agentId); + } + + return this.cpService.post(this.cpService.getWxCpConfigStorage().getApiUrl(SCHEDULE_ADD), + WxCpGsonBuilder.create().toJson(param)); + } + + @Override + public void update(WxCpOaSchedule schedule) throws WxErrorException { + this.cpService.post(this.cpService.getWxCpConfigStorage().getApiUrl(SCHEDULE_UPDATE), + WxCpGsonBuilder.create().toJson(ImmutableMap.of("schedule", schedule))); + } + + @Override + public List getDetails(List scheduleIds) throws WxErrorException { + final String response = this.cpService.post(this.cpService.getWxCpConfigStorage().getApiUrl(SCHEDULE_GET), + WxCpGsonBuilder.create().toJson(ImmutableMap.of("schedule_id_list", scheduleIds))); + return WxCpGsonBuilder.create().fromJson(GsonParser.parse(response).get("schedule_list"), + new TypeToken>() { + }.getType()); + } + + @Override + public void delete(String scheduleId) throws WxErrorException { + this.cpService.post(this.cpService.getWxCpConfigStorage().getApiUrl(SCHEDULE_DEL), + WxCpGsonBuilder.create().toJson(ImmutableMap.of("schedule_id", scheduleId))); + } + + @Override + public List listByCalendar(String calId, Integer offset, Integer limit) throws WxErrorException { + final Map param = new HashMap<>(3); + param.put("cal_id", calId); + if (offset != null) { + param.put("offset", offset); + } + if (limit != null) { + param.put("limit", limit); + } + final String response = this.cpService.post(this.cpService.getWxCpConfigStorage().getApiUrl(SCHEDULE_LIST), + WxCpGsonBuilder.create().toJson(param)); + return WxCpGsonBuilder.create().fromJson(GsonParser.parse(response).get("schedule_list"), + new TypeToken>() { + }.getType()); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaServiceImpl.java index b8449ced13..59cde79a93 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaServiceImpl.java @@ -1,20 +1,20 @@ package me.chanjar.weixin.cp.api.impl; import com.google.gson.JsonArray; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.reflect.TypeToken; +import lombok.NonNull; import lombok.RequiredArgsConstructor; import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.error.WxRuntimeException; +import me.chanjar.weixin.common.util.json.GsonParser; import me.chanjar.weixin.cp.api.WxCpOaService; import me.chanjar.weixin.cp.api.WxCpService; -import me.chanjar.weixin.cp.bean.WxCpApprovalDataResult; -import me.chanjar.weixin.cp.bean.WxCpCheckinData; -import me.chanjar.weixin.cp.bean.WxCpCheckinOption; -import me.chanjar.weixin.cp.bean.WxCpDialRecord; -import me.chanjar.weixin.cp.constant.WxCpApiPathConsts; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; +import me.chanjar.weixin.cp.bean.oa.*; import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; +import org.apache.commons.lang3.StringUtils; import java.util.Date; import java.util.List; @@ -22,39 +22,45 @@ import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Oa.*; /** - * . + * 企业微信 OA 接口实现 * - * @author Element - * @date 2019-04-06 11:20 + * @author Element created on 2019-04-06 11:20 */ @RequiredArgsConstructor public class WxCpOaServiceImpl implements WxCpOaService { private final WxCpService mainService; + private static final int MONTH_SECONDS = 31 * 24 * 60 * 60; + private static final int USER_IDS_LIMIT = 100; + @Override - public List getCheckinData(Integer openCheckinDataType, Date startTime, Date endTime, - List userIdList) throws WxErrorException { - if (startTime == null || endTime == null) { - throw new RuntimeException("starttime and endtime can't be null"); - } + public String apply(WxCpOaApplyEventRequest request) throws WxErrorException { + String responseContent = this.mainService.post(this.mainService.getWxCpConfigStorage().getApiUrl(APPLY_EVENT), + request.toJson()); + return GsonParser.parse(responseContent).get("sp_no").getAsString(); + } - if (userIdList == null || userIdList.size() > 100) { - throw new RuntimeException("用户列表不能为空,不超过100个,若用户超过100个,请分批获取"); + @Override + public List getCheckinData(Integer openCheckinDataType, @NonNull Date startTime, + @NonNull Date endTime, + List userIdList) throws WxErrorException { + if (userIdList == null || userIdList.size() > USER_IDS_LIMIT) { + throw new WxRuntimeException("用户列表不能为空,不超过 " + USER_IDS_LIMIT + " 个,若用户超过 " + USER_IDS_LIMIT + " 个,请分批获取"); } - long endtimestamp = endTime.getTime() / 1000L; - long starttimestamp = startTime.getTime() / 1000L; + long endTimestamp = endTime.getTime() / 1000L; + long startTimestamp = startTime.getTime() / 1000L; - if (endtimestamp - starttimestamp < 0 || endtimestamp - starttimestamp >= 30 * 24 * 60 * 60) { - throw new RuntimeException("获取记录时间跨度不超过一个月"); + if (endTimestamp - startTimestamp < 0 || endTimestamp - startTimestamp > MONTH_SECONDS) { + throw new WxRuntimeException("获取记录时间跨度不超过一个月"); } JsonObject jsonObject = new JsonObject(); JsonArray jsonArray = new JsonArray(); jsonObject.addProperty("opencheckindatatype", openCheckinDataType); - jsonObject.addProperty("starttime", starttimestamp); - jsonObject.addProperty("endtime", endtimestamp); + jsonObject.addProperty("starttime", startTimestamp); + jsonObject.addProperty("endtime", endTimestamp); for (String userid : userIdList) { jsonArray.add(userid); @@ -64,23 +70,17 @@ public List getCheckinData(Integer openCheckinDataType, Date st final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_CHECKIN_DATA); String responseContent = this.mainService.post(url, jsonObject.toString()); - JsonElement tmpJsonElement = new JsonParser().parse(responseContent); - return WxCpGsonBuilder.create() - .fromJson( - tmpJsonElement.getAsJsonObject().get("checkindata"), - new TypeToken>() { - }.getType() - ); + JsonObject tmpJson = GsonParser.parse(responseContent); + return WxCpGsonBuilder.create().fromJson(tmpJson.get("checkindata"), + new TypeToken>() { + }.getType() + ); } @Override - public List getCheckinOption(Date datetime, List userIdList) throws WxErrorException { - if (datetime == null) { - throw new RuntimeException("datetime can't be null"); - } - - if (userIdList == null || userIdList.size() > 100) { - throw new RuntimeException("用户列表不能为空,不超过100个,若用户超过100个,请分批获取"); + public List getCheckinOption(@NonNull Date datetime, List userIdList) throws WxErrorException { + if (userIdList == null || userIdList.size() > USER_IDS_LIMIT) { + throw new WxRuntimeException("用户列表不能为空,不超过 " + USER_IDS_LIMIT + " 个,若用户超过 " + USER_IDS_LIMIT + " 个,请分批获取"); } JsonArray jsonArray = new JsonArray(); @@ -94,28 +94,158 @@ public List getCheckinOption(Date datetime, List user final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_CHECKIN_OPTION); String responseContent = this.mainService.post(url, jsonObject.toString()); - JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + JsonObject tmpJson = GsonParser.parse(responseContent); + + return WxCpGsonBuilder.create().fromJson(tmpJson.get("info"), + new TypeToken>() { + }.getType() + ); + } + + @Override + public List getCropCheckinOption() throws WxErrorException { + JsonObject jsonObject = new JsonObject(); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_CORP_CHECKIN_OPTION); + String responseContent = this.mainService.post(url, jsonObject.toString()); + JsonObject tmpJson = GsonParser.parse(responseContent); + + return WxCpGsonBuilder.create().fromJson(tmpJson.get("group"), + new TypeToken>() { + }.getType() + ); + } + + @Override + public WxCpApprovalInfo getApprovalInfo(@NonNull Date startTime, @NonNull Date endTime, + Integer cursor, Integer size, List filters) + throws WxErrorException { + if (cursor == null) { + cursor = 0; + } + + if (size == null) { + size = 100; + } + + if (size < 0 || size > 100) { + throw new IllegalArgumentException("size参数错误,请使用[1-100]填充,默认100"); + } + + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("starttime", startTime.getTime() / 1000L); + jsonObject.addProperty("endtime", endTime.getTime() / 1000L); + jsonObject.addProperty("size", size); + jsonObject.addProperty("cursor", cursor); + + if (filters != null && !filters.isEmpty()) { + JsonArray filterJsonArray = new JsonArray(); + for (WxCpApprovalInfoQueryFilter filter : filters) { + filterJsonArray.add(JsonParser.parseString(filter.toJson())); + } + jsonObject.add("filters", filterJsonArray); + } + + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_APPROVAL_INFO); + String responseContent = this.mainService.post(url, jsonObject.toString()); - return WxCpGsonBuilder.create() - .fromJson( - tmpJsonElement.getAsJsonObject().get("info"), - new TypeToken>() { - }.getType() - ); + return WxCpGsonBuilder.create().fromJson(responseContent, WxCpApprovalInfo.class); } @Override - public WxCpApprovalDataResult getApprovalData(Date startTime, Date endTime, Long nextSpnum) throws WxErrorException { + public WxCpApprovalInfo getApprovalInfo(@NonNull Date startTime, @NonNull Date endTime) throws WxErrorException { + return this.getApprovalInfo(startTime, endTime, 0, null, null); + } + + @Override + public WxCpApprovalInfo getApprovalInfo(@NonNull Date startTime, @NonNull Date endTime, String newCursor, + Integer size, List filters) + throws WxErrorException { + if (newCursor == null) { + newCursor = ""; + } + + if (size == null) { + size = 100; + } + + if (size < 0 || size > 100) { + throw new IllegalArgumentException("size参数错误,请使用[1-100]填充,默认100"); + } + JsonObject jsonObject = new JsonObject(); jsonObject.addProperty("starttime", startTime.getTime() / 1000L); jsonObject.addProperty("endtime", endTime.getTime() / 1000L); - if (nextSpnum != null) { - jsonObject.addProperty("next_spnum", nextSpnum); + jsonObject.addProperty("size", size); + jsonObject.addProperty("new_cursor", newCursor); + + if (filters != null && !filters.isEmpty()) { + JsonArray filterJsonArray = new JsonArray(); + for (WxCpApprovalInfoQueryFilter filter : filters) { + filterJsonArray.add(JsonParser.parseString(filter.toJson())); + } + jsonObject.add("filters", filterJsonArray); } + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_APPROVAL_INFO); + String responseContent = this.mainService.post(url, jsonObject.toString()); + + return WxCpGsonBuilder.create().fromJson(responseContent, WxCpApprovalInfo.class); + } + + @Override + public WxCpApprovalDetailResult getApprovalDetail(@NonNull String spNo) throws WxErrorException { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("sp_no", spNo); + + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_APPROVAL_DETAIL); + String responseContent = this.mainService.post(url, jsonObject.toString()); + + return WxCpGsonBuilder.create().fromJson(responseContent, WxCpApprovalDetailResult.class); + } + + @Override + public WxCpCorpConfInfo getCorpConf() throws WxErrorException { + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_CORP_CONF); + String responseContent = this.mainService.get(url, null); + return WxCpCorpConfInfo.fromJson(responseContent); + } + + @Override + public WxCpUserVacationQuota getUserVacationQuota(@NonNull String userId) throws WxErrorException { + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_USER_VACATION_QUOTA); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("userid", userId); + String responseContent = this.mainService.post(url, jsonObject.toString()); + return WxCpUserVacationQuota.fromJson(responseContent); + } + + @Override + public WxCpGetApprovalData getApprovalData(@NonNull Long startTime, @NonNull Long endTime, Long nextSpNum) throws WxErrorException { final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_APPROVAL_DATA); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("starttime", startTime); + jsonObject.addProperty("endtime", endTime); + if (nextSpNum != null) { + jsonObject.addProperty("next_spnum", nextSpNum); + } + String responseContent = this.mainService.post(url, jsonObject.toString()); + return WxCpGetApprovalData.fromJson(responseContent); + } + + @Override + public WxCpBaseResp setOneUserQuota(@NonNull String userId, @NonNull Integer vacationId, + @NonNull Integer leftDuration, @NonNull Integer timeAttr, String remarks) throws WxErrorException { + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(SET_ONE_USER_QUOTA); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("userid", userId); + jsonObject.addProperty("vacation_id", vacationId); + jsonObject.addProperty("leftduration", leftDuration); + jsonObject.addProperty("time_attr", timeAttr); + if (StringUtils.isNotEmpty(remarks)) { + jsonObject.addProperty("remarks", remarks); + } String responseContent = this.mainService.post(url, jsonObject.toString()); - return WxCpGsonBuilder.create().fromJson(responseContent, WxCpApprovalDataResult.class); + return WxCpBaseResp.fromJson(responseContent); } @Override @@ -135,12 +265,11 @@ public List getDialRecord(Date startTime, Date endTime, Integer jsonObject.addProperty("limit", limit); if (startTime != null && endTime != null) { - long endtimestamp = endTime.getTime() / 1000L; long starttimestamp = startTime.getTime() / 1000L; - if (endtimestamp - starttimestamp < 0 || endtimestamp - starttimestamp >= 30 * 24 * 60 * 60) { - throw new RuntimeException("受限于网络传输,起止时间的最大跨度为30天,如超过30天,则以结束时间为基准向前取30天进行查询"); + if (endtimestamp - starttimestamp < 0 || endtimestamp - starttimestamp >= MONTH_SECONDS) { + throw new WxRuntimeException("受限于网络传输,起止时间的最大跨度为30天,如超过30天,则以结束时间为基准向前取30天进行查询"); } jsonObject.addProperty("start_time", starttimestamp); @@ -149,11 +278,143 @@ public List getDialRecord(Date startTime, Date endTime, Integer final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_DIAL_RECORD); String responseContent = this.mainService.post(url, jsonObject.toString()); - JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + JsonObject tmpJson = GsonParser.parse(responseContent); - return WxCpGsonBuilder.create().fromJson(tmpJsonElement.getAsJsonObject().get("record"), + return WxCpGsonBuilder.create().fromJson(tmpJson.get("record"), new TypeToken>() { }.getType() ); } + + @Override + public WxCpOaApprovalTemplateResult getTemplateDetail(@NonNull String templateId) throws WxErrorException { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("template_id", templateId); + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_TEMPLATE_DETAIL); + String responseContent = this.mainService.post(url, jsonObject.toString()); + return WxCpGsonBuilder.create().fromJson(responseContent, WxCpOaApprovalTemplateResult.class); + } + + @Override + public String createOaApprovalTemplate(WxCpOaApprovalTemplate cpTemplate) throws WxErrorException { + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(CREATE_TEMPLATE); + String responseContent = this.mainService.post(url, WxCpGsonBuilder.create().toJson(cpTemplate)); + JsonObject tmpJson = GsonParser.parse(responseContent); + return tmpJson.get("template_id").getAsString(); + } + + @Override + public void updateOaApprovalTemplate(WxCpOaApprovalTemplate wxCpTemplate) throws WxErrorException { + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(UPDATE_TEMPLATE); + this.mainService.post(url, WxCpGsonBuilder.create().toJson(wxCpTemplate)); + } + + @Override + public List getCheckinDayData(@NonNull Date startTime, @NonNull Date endTime, + List userIdList) + throws WxErrorException { + if (userIdList == null || userIdList.size() > USER_IDS_LIMIT) { + throw new WxRuntimeException("用户列表不能为空,不超过 " + USER_IDS_LIMIT + " 个,若用户超过 " + USER_IDS_LIMIT + " 个,请分批获取"); + } + + long endTimestamp = endTime.getTime() / 1000L; + long startTimestamp = startTime.getTime() / 1000L; + + JsonObject jsonObject = new JsonObject(); + JsonArray jsonArray = new JsonArray(); + + jsonObject.addProperty("starttime", startTimestamp); + jsonObject.addProperty("endtime", endTimestamp); + + for (String userid : userIdList) { + jsonArray.add(userid); + } + jsonObject.add("useridlist", jsonArray); + + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_CHECKIN_DAY_DATA); + String responseContent = this.mainService.post(url, jsonObject.toString()); + JsonObject tmpJson = GsonParser.parse(responseContent); + return WxCpGsonBuilder.create().fromJson(tmpJson.get("datas"), + new TypeToken>() { + }.getType() + ); + } + + @Override + public List getCheckinMonthData(@NonNull Date startTime, @NonNull Date endTime, + List userIdList) + throws WxErrorException { + if (userIdList == null || userIdList.size() > USER_IDS_LIMIT) { + throw new WxRuntimeException("用户列表不能为空,不超过 " + USER_IDS_LIMIT + " 个,若用户超过 " + USER_IDS_LIMIT + " 个,请分批获取"); + } + + long endTimestamp = endTime.getTime() / 1000L; + long startTimestamp = startTime.getTime() / 1000L; + + JsonObject jsonObject = new JsonObject(); + JsonArray jsonArray = new JsonArray(); + + jsonObject.addProperty("starttime", startTimestamp); + jsonObject.addProperty("endtime", endTimestamp); + + for (String userid : userIdList) { + jsonArray.add(userid); + } + jsonObject.add("useridlist", jsonArray); + + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_CHECKIN_MONTH_DATA); + String responseContent = this.mainService.post(url, jsonObject.toString()); + JsonObject tmpJson = GsonParser.parse(responseContent); + return WxCpGsonBuilder.create().fromJson(tmpJson.get("datas"), + new TypeToken>() { + }.getType() + ); + } + + @Override + public List getCheckinScheduleList(@NonNull Date startTime, @NonNull Date endTime, + List userIdList) + throws WxErrorException { + if (userIdList == null || userIdList.size() > USER_IDS_LIMIT) { + throw new WxRuntimeException("用户列表不能为空,不超过 " + USER_IDS_LIMIT + " 个,若用户超过 " + USER_IDS_LIMIT + " 个,请分批获取"); + } + + long endTimestamp = endTime.getTime() / 1000L; + long startTimestamp = startTime.getTime() / 1000L; + + + JsonObject jsonObject = new JsonObject(); + JsonArray jsonArray = new JsonArray(); + + jsonObject.addProperty("starttime", startTimestamp); + jsonObject.addProperty("endtime", endTimestamp); + + for (String userid : userIdList) { + jsonArray.add(userid); + } + jsonObject.add("useridlist", jsonArray); + + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_CHECKIN_SCHEDULE_DATA); + String responseContent = this.mainService.post(url, jsonObject.toString()); + JsonObject tmpJson = GsonParser.parse(responseContent); + return WxCpGsonBuilder.create().fromJson(tmpJson.get("schedule_list"), + new TypeToken>() { + }.getType() + ); + } + + @Override + public void setCheckinScheduleList(WxCpSetCheckinSchedule wxCpSetCheckinSchedule) throws WxErrorException { + final String url = this.mainService.getWxCpConfigStorage().getApiUrl(SET_CHECKIN_SCHEDULE_DATA); + this.mainService.post(url, WxCpGsonBuilder.create().toJson(wxCpSetCheckinSchedule)); + } + + @Override + public void addCheckInUserFace(String userId, String userFace) throws WxErrorException { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("userid", userId); + jsonObject.addProperty("userface", userFace); + String url = this.mainService.getWxCpConfigStorage().getApiUrl(ADD_CHECK_IN_USER_FACE); + this.mainService.post(url, jsonObject.toString()); + } } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaWeDocServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaWeDocServiceImpl.java new file mode 100644 index 0000000000..7f5bf004db --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaWeDocServiceImpl.java @@ -0,0 +1,336 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.gson.JsonObject; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.bean.CommonUploadParam; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.api.WxCpOaWeDocService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; +import me.chanjar.weixin.cp.bean.oa.doc.*; + +import java.io.File; +import java.util.List; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Oa.*; + +/** + * 企业微信文档接口实现类. + * + * @author Wang_Wong created on 2022-04-22 + */ +@Slf4j +@RequiredArgsConstructor +public class WxCpOaWeDocServiceImpl implements WxCpOaWeDocService { + private final WxCpService cpService; + + @Override + public WxCpDocCreateData docCreate(@NonNull WxCpDocCreateRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_CREATE_DOC); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpDocCreateData.fromJson(responseContent); + } + + @Override + public WxCpBaseResp docRename(@NonNull WxCpDocRenameRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_RENAME_DOC); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp docDelete(String docId, String formId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_DEL_DOC); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("docid", docId); + jsonObject.addProperty("formid", formId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpDocInfo docInfo(@NonNull String docId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_GET_DOC_BASE_INFO); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("docid", docId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpDocInfo.fromJson(responseContent); + } + + @Override + public WxCpDocShare docShare(@NonNull String docId) throws WxErrorException { + return docShare(WxCpDocShareRequest.builder().docId(docId).build()); + } + + @Override + public WxCpDocShare docShare(@NonNull WxCpDocShareRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_DOC_SHARE); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpDocShare.fromJson(responseContent); + } + + @Override + public WxCpDocAuthInfo docGetAuth(@NonNull String docId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_DOC_GET_AUTH); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("docid", docId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpDocAuthInfo.fromJson(responseContent); + } + + @Override + public WxCpBaseResp docModifyJoinRule(@NonNull WxCpDocModifyJoinRuleRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_MOD_DOC_JOIN_RULE); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp docModifyMember(@NonNull WxCpDocModifyMemberRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_MOD_DOC_MEMBER); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp docModifySafetySetting( + @NonNull WxCpDocModifySafetySettingRequest request + ) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage() + .getApiUrl(WEDOC_MOD_DOC_SAFETY_SETTING); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpDocSheetBatchUpdateResponse docBatchUpdate(@NonNull WxCpDocSheetBatchUpdateRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SPREADSHEET_BATCH_UPDATE); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpDocSheetBatchUpdateResponse.fromJson(responseContent); + } + + @Override + public WxCpDocSheetProperties getSheetProperties(@NonNull String docId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SPREADSHEET_GET_SHEET_PROPERTIES); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("docid", docId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpDocSheetProperties.fromJson(responseContent); + } + + @Override + public WxCpDocSheetData getSheetRangeData(@NonNull WxCpDocSheetGetDataRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SPREADSHEET_GET_SHEET_RANGE_DATA); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpDocSheetData.fromJson(responseContent); + } + + @Override + public WxCpDocData docGetData(@NonNull WxCpDocGetDataRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_GET_DOC_DATA); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpDocData.fromJson(responseContent); + } + + @Override + public WxCpBaseResp docModify(@NonNull WxCpDocModifyRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_MOD_DOC); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpDocImageUploadResult docUploadImage(@NonNull File file) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_UPLOAD_DOC_IMAGE); + String responseContent = this.cpService.upload(apiUrl, CommonUploadParam.fromFile("media", file)); + return WxCpDocImageUploadResult.fromJson(responseContent); + } + + @Override + public WxCpBaseResp docAddAdmin(@NonNull WxCpDocAdminRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_ADD_ADMIN); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp docDeleteAdmin(@NonNull WxCpDocAdminRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_DEL_ADMIN); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpDocAdminListResult docGetAdminList(@NonNull String docId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_GET_ADMIN_LIST); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("docid", docId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpDocAdminListResult.fromJson(responseContent); + } + + @Override + public WxCpDocSmartSheetAuth smartSheetGetAuth(@NonNull WxCpDocSmartSheetAuthRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_GET_SHEET_AUTH); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpDocSmartSheetAuth.fromJson(responseContent); + } + + @Override + public WxCpBaseResp smartSheetModifyAuth(@NonNull WxCpDocSmartSheetModifyAuthRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_MOD_SHEET_AUTH); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpDocSmartSheetResult smartSheetGetSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_GET_SHEET); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpDocSmartSheetResult.fromJson(responseContent); + } + + @Override + public WxCpDocSmartSheetResult smartSheetAddSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_ADD_SHEET); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpDocSmartSheetResult.fromJson(responseContent); + } + + @Override + public WxCpBaseResp smartSheetDeleteSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_DELETE_SHEET); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp smartSheetUpdateSheet(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_UPDATE_SHEET); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpDocSmartSheetResult smartSheetGetViews(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_GET_VIEWS); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpDocSmartSheetResult.fromJson(responseContent); + } + + @Override + public WxCpDocSmartSheetResult smartSheetAddView(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_ADD_VIEW); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpDocSmartSheetResult.fromJson(responseContent); + } + + @Override + public WxCpBaseResp smartSheetDeleteViews(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_DELETE_VIEWS); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp smartSheetUpdateView(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_UPDATE_VIEW); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpDocSmartSheetResult smartSheetGetFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_GET_FIELDS); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpDocSmartSheetResult.fromJson(responseContent); + } + + @Override + public WxCpDocSmartSheetResult smartSheetAddFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_ADD_FIELDS); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpDocSmartSheetResult.fromJson(responseContent); + } + + @Override + public WxCpBaseResp smartSheetDeleteFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_DELETE_FIELDS); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp smartSheetUpdateFields(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_UPDATE_FIELDS); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpDocSmartSheetResult smartSheetGetRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_GET_RECORDS); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpDocSmartSheetResult.fromJson(responseContent); + } + + @Override + public WxCpDocSmartSheetResult smartSheetAddRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_ADD_RECORDS); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpDocSmartSheetResult.fromJson(responseContent); + } + + @Override + public WxCpBaseResp smartSheetDeleteRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_DELETE_RECORDS); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp smartSheetUpdateRecords(@NonNull WxCpDocSmartSheetRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_SMARTSHEET_UPDATE_RECORDS); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpFormCreateResult formCreate(@NonNull WxCpFormCreateRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_CREATE_FORM); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpFormCreateResult.fromJson(responseContent); + } + + @Override + public WxCpBaseResp formModify(@NonNull WxCpFormModifyRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_MODIFY_FORM); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpFormInfoResult formInfo(@NonNull String formId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_GET_FORM_INFO); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("formid", formId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpFormInfoResult.fromJson(responseContent); + } + + @Override + public WxCpFormStatisticResult formStatistic(@NonNull List requests) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_GET_FORM_STATISTIC); + String responseContent = this.cpService.post(apiUrl, WxCpFormStatisticRequest.toJson(requests)); + return WxCpFormStatisticResult.fromJson(responseContent); + } + + @Override + public WxCpFormAnswer formAnswer(@NonNull WxCpFormAnswerRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(WEDOC_GET_FORM_ANSWER); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpFormAnswer.fromJson(responseContent); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaWeDriveServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaWeDriveServiceImpl.java new file mode 100644 index 0000000000..a41195ae84 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpOaWeDriveServiceImpl.java @@ -0,0 +1,196 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.gson.JsonObject; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.api.WxCpOaWeDriveService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; +import me.chanjar.weixin.cp.bean.oa.wedrive.*; + +import java.util.List; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Oa.*; + +/** + * 企业微信微盘接口实现类. + * + * @author Wang_Wong created on 2022-04-22 + */ +@Slf4j +@RequiredArgsConstructor +public class WxCpOaWeDriveServiceImpl implements WxCpOaWeDriveService { + private final WxCpService cpService; + + @Override + public WxCpSpaceCreateData spaceCreate(@NonNull WxCpSpaceCreateRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(SPACE_CREATE); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpSpaceCreateData.fromJson(responseContent); + } + + @Override + public WxCpBaseResp spaceRename(@NonNull WxCpSpaceRenameRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(SPACE_RENAME); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp spaceDismiss(@NonNull String spaceId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(SPACE_DISMISS); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("spaceid", spaceId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpSpaceInfo spaceInfo(@NonNull String spaceId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(SPACE_INFO); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("spaceid", spaceId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpSpaceInfo.fromJson(responseContent); + } + + @Override + public WxCpBaseResp spaceAclAdd(@NonNull WxCpSpaceAclAddRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(SPACE_ACL_ADD); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp spaceAclDel(@NonNull WxCpSpaceAclDelRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(SPACE_ACL_DEL); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp spaceSetting(@NonNull WxCpSpaceSettingRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(SPACE_SETTING); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpSpaceShare spaceShare(@NonNull String spaceId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(SPACE_SHARE); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("spaceid", spaceId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpSpaceShare.fromJson(responseContent); + } + + @Override + public WxCpFileList fileList(@NonNull WxCpFileListRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(FILE_LIST); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpFileList.fromJson(responseContent); + } + + @Override + public WxCpFileUpload fileUpload(@NonNull WxCpFileUploadRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(FILE_UPLOAD); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpFileUpload.fromJson(responseContent); + } + + @Override + public WxCpFileDownload fileDownload(@NonNull String userId, @NonNull String fileId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(FILE_DOWNLOAD); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("userid", userId); + jsonObject.addProperty("fileid", fileId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpFileDownload.fromJson(responseContent); + } + + @Override + public WxCpFileRename fileRename(@NonNull String fileId, @NonNull String newName) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(FILE_RENAME); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("fileid", fileId); + jsonObject.addProperty("new_name", newName); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpFileRename.fromJson(responseContent); + } + + @Override + public WxCpFileCreate fileCreate(@NonNull String spaceId, @NonNull String fatherId, + @NonNull Integer fileType, @NonNull String fileName) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(FILE_CREATE); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("spaceid", spaceId); + jsonObject.addProperty("fatherid", fatherId); + jsonObject.addProperty("file_type", fileType); + jsonObject.addProperty("file_name", fileName); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpFileCreate.fromJson(responseContent); + } + + @Override + public WxCpFileMove fileMove(@NonNull WxCpFileMoveRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(FILE_MOVE); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpFileMove.fromJson(responseContent); + } + + @Override + public WxCpBaseResp fileDelete(@NonNull List fileIds) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(FILE_DELETE); + WxCpFileDeleteRequest request = new WxCpFileDeleteRequest(fileIds); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp fileAclAdd(@NonNull WxCpFileAclAddRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(FILE_ACL_ADD); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp fileAclDel(@NonNull WxCpFileAclDelRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(FILE_ACL_DEL); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp fileSetting(@NonNull String fileId, @NonNull Integer authScope, Integer auth) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(FILE_SETTING); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("fileid", fileId); + jsonObject.addProperty("auth_scope", authScope); + if (auth != null) { + jsonObject.addProperty("auth", auth); + } + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpFileShare fileShare(@NonNull String fileId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(FILE_SHARE); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("fileid", fileId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpFileShare.fromJson(responseContent); + } + + @Override + public WxCpFileInfo fileInfo(@NonNull String fileId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(FILE_INFO); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("fileid", fileId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpFileInfo.fromJson(responseContent); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpSchoolHealthServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpSchoolHealthServiceImpl.java new file mode 100644 index 0000000000..60f379da81 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpSchoolHealthServiceImpl.java @@ -0,0 +1,76 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.gson.JsonObject; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.api.WxCpSchoolHealthService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.school.health.WxCpGetHealthReportStat; +import me.chanjar.weixin.cp.bean.school.health.WxCpGetReportAnswer; +import me.chanjar.weixin.cp.bean.school.health.WxCpGetReportJobIds; +import me.chanjar.weixin.cp.bean.school.health.WxCpGetReportJobInfo; + +import java.util.Optional; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.School.*; + +/** + * 企业微信家校应用 健康上报接口实现类. + * + * @author Wang_Wong created on : 2022/5/31 9:16 + */ +@Slf4j +@RequiredArgsConstructor +public class WxCpSchoolHealthServiceImpl implements WxCpSchoolHealthService { + + private final WxCpService cpService; + + @Override + public WxCpGetHealthReportStat getHealthReportStat(@NonNull String date) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_HEALTH_REPORT_STAT); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("date", date); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpGetHealthReportStat.fromJson(responseContent); + } + + @Override + public WxCpGetReportJobIds getReportJobIds(Integer offset, Integer limit) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_REPORT_JOBIDS); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("offset", Optional.ofNullable(offset).orElse(0)); + jsonObject.addProperty("limit", Optional.ofNullable(limit).orElse(100)); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpGetReportJobIds.fromJson(responseContent); + } + + @Override + public WxCpGetReportJobInfo getReportJobInfo(@NonNull String jobId, @NonNull String date) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_REPORT_JOB_INFO); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("jobid", jobId); + jsonObject.addProperty("date", date); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpGetReportJobInfo.fromJson(responseContent); + } + + @Override + public WxCpGetReportAnswer getReportAnswer(@NonNull String jobId, @NonNull String date, Integer offset, + Integer limit) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_REPORT_ANSWER); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("jobid", jobId); + jsonObject.addProperty("date", date); + if (offset != null) { + jsonObject.addProperty("offset", offset); + } + if (limit != null) { + jsonObject.addProperty("limit", limit); + } + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpGetReportAnswer.fromJson(responseContent); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpSchoolServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpSchoolServiceImpl.java new file mode 100644 index 0000000000..c503aebdfd --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpSchoolServiceImpl.java @@ -0,0 +1,126 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.gson.JsonObject; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.api.WxCpSchoolService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.living.WxCpLivingResult; +import me.chanjar.weixin.cp.bean.school.*; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; +import java.util.Optional; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.School.*; + +/** + * 企业微信家校应用 复学码相关接口实现类. + * https://developer.work.weixin.qq.com/document/path/93744 + * + * @author Wang_Wong created on : 2022/6/1 14:05 + */ +@Slf4j +@RequiredArgsConstructor +public class WxCpSchoolServiceImpl implements WxCpSchoolService { + + private final WxCpService cpService; + + @Override + public WxCpCustomizeHealthInfo getTeacherCustomizeHealthInfo(String date, String nextKey, Integer limit) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_TEACHER_CUSTOMIZE_HEALTH_INFO); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("date", date); + jsonObject.addProperty("limit", Optional.ofNullable(limit).orElse(100)); + if (nextKey != null) { + jsonObject.addProperty("next_key", nextKey); + } + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpCustomizeHealthInfo.fromJson(responseContent); + } + + @Override + public WxCpCustomizeHealthInfo getStudentCustomizeHealthInfo(String date, String nextKey, Integer limit) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_STUDENT_CUSTOMIZE_HEALTH_INFO); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("date", date); + jsonObject.addProperty("limit", Optional.ofNullable(limit).orElse(100)); + if (nextKey != null) { + jsonObject.addProperty("next_key", nextKey); + } + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpCustomizeHealthInfo.fromJson(responseContent); + } + + @Override + public WxCpResultList getHealthQrCode(List userIds, Integer type) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_HEALTH_QRCODE); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("type", type); + jsonObject.addProperty("userids", userIds.toString()); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpResultList.fromJson(responseContent); + } + + @Override + public WxCpPaymentResult getPaymentResult(String paymentId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_PAYMENT_RESULT); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("payment_id", paymentId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpPaymentResult.fromJson(responseContent); + } + + @Override + public WxCpTrade getTrade(String paymentId, String tradeNo) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_TRADE); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("payment_id", paymentId); + jsonObject.addProperty("trade_no", tradeNo); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpTrade.fromJson(responseContent); + } + + @Override + public WxCpSchoolLivingInfo getLivingInfo(String livingId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_LIVING_INFO) + livingId; + String responseContent = this.cpService.get(apiUrl, null); + return WxCpSchoolLivingInfo.fromJson(responseContent); + } + + @Override + public WxCpLivingResult.LivingIdResult getUserAllLivingId(String userId, String cursor, Integer limit) throws WxErrorException { + return this.cpService.getLivingService().getUserAllLivingId(userId, cursor, limit); + } + + @Override + public WxCpSchoolWatchStat getWatchStat(String livingId, String nextKey) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_WATCH_STAT); + JsonObject jsonObject = new JsonObject(); + if (StringUtils.isNotBlank(nextKey)) { + jsonObject.addProperty("next_key", nextKey); + } + jsonObject.addProperty("livingid", livingId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpSchoolWatchStat.fromJson(responseContent); + } + + @Override + public WxCpSchoolUnwatchStat getUnwatchStat(String livingId, String nextKey) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_UNWATCH_STAT); + JsonObject jsonObject = new JsonObject(); + if (StringUtils.isNotBlank(nextKey)) { + jsonObject.addProperty("next_key", nextKey); + } + jsonObject.addProperty("livingid", livingId); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpSchoolUnwatchStat.fromJson(responseContent); + } + + @Override + public WxCpLivingResult deleteReplayData(String livingId) throws WxErrorException { + return cpService.getLivingService().deleteReplayData(livingId); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpSchoolUserServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpSchoolUserServiceImpl.java new file mode 100644 index 0000000000..bdb067f923 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpSchoolUserServiceImpl.java @@ -0,0 +1,276 @@ +package me.chanjar.weixin.cp.api.impl; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.json.GsonParser; +import me.chanjar.weixin.cp.api.WxCpSchoolUserService; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; +import me.chanjar.weixin.cp.bean.WxCpOauth2UserInfo; +import me.chanjar.weixin.cp.bean.school.user.*; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; +import java.util.Objects; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.ExternalContact.*; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.School.*; + +/** + * 企业微信家校沟通相关接口. + * https://developer.work.weixin.qq.com/document/path/91638 + * + * @author Wang_Wong created on : 2022/6/18 9:10 + */ +@Slf4j +@RequiredArgsConstructor +public class WxCpSchoolUserServiceImpl implements WxCpSchoolUserService { + + private final WxCpService cpService; + + @Override + public WxCpOauth2UserInfo getUserInfo(@NonNull String code) throws WxErrorException { + return cpService.getOauth2Service().getUserInfo(code); + } + + @Override + public WxCpOauth2UserInfo getSchoolUserInfo(@NonNull String code) throws WxErrorException { + return cpService.getOauth2Service().getSchoolUserInfo(code); + } + + @Override + public WxCpBaseResp createStudent(@NonNull String studentUserId, @NonNull String name, + @NonNull List departments) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(CREATE_STUDENT); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("student_userid", studentUserId); + jsonObject.addProperty("name", name); + JsonArray jsonArray = new JsonArray(); + for (Integer depart : departments) { + jsonArray.add(new JsonPrimitive(depart)); + } + jsonObject.add("department", jsonArray); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBatchResultList batchCreateStudent(@NonNull WxCpBatchCreateStudentRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(BATCH_CREATE_STUDENT); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBatchResultList.fromJson(responseContent); + } + + @Override + public WxCpBatchResultList batchDeleteStudent(@NonNull WxCpBatchDeleteStudentRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(BATCH_DELETE_STUDENT); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBatchResultList.fromJson(responseContent); + } + + @Override + public WxCpBatchResultList batchUpdateStudent(@NonNull WxCpBatchUpdateStudentRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(BATCH_UPDATE_STUDENT); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBatchResultList.fromJson(responseContent); + } + + @Override + public WxCpBaseResp deleteStudent(@NonNull String studentUserId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(DELETE_STUDENT) + studentUserId; + String responseContent = this.cpService.get(apiUrl, null); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp updateStudent(@NonNull String studentUserId, String newStudentUserId, String name, + List departments) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(UPDATE_STUDENT); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("student_userid", studentUserId); + if (StringUtils.isNotEmpty(newStudentUserId)) { + jsonObject.addProperty("new_student_userid", newStudentUserId); + } + if (StringUtils.isNotEmpty(name)) { + jsonObject.addProperty("name", name); + } + if (departments != null && !departments.isEmpty()) { + JsonArray jsonArray = new JsonArray(); + for (Integer depart : departments) { + jsonArray.add(new JsonPrimitive(depart)); + } + jsonObject.add("department", jsonArray); + } + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp createParent(@NonNull WxCpCreateParentRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(CREATE_PARENT); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBatchResultList batchCreateParent(@NonNull WxCpBatchCreateParentRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(BATCH_CREATE_PARENT); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBatchResultList.fromJson(responseContent); + } + + @Override + public WxCpBatchResultList batchDeleteParent(@NonNull String... userIdList) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(BATCH_DELETE_PARENT); + JsonObject jsonObject = new JsonObject(); + JsonArray jsonArray = new JsonArray(); + for (String userId : userIdList) { + jsonArray.add(new JsonPrimitive(userId)); + } + jsonObject.add("useridlist", jsonArray); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpBatchResultList.fromJson(responseContent); + } + + @Override + public WxCpBatchResultList batchUpdateParent(@NonNull WxCpBatchUpdateParentRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(BATCH_UPDATE_PARENT); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBatchResultList.fromJson(responseContent); + } + + @Override + public WxCpUserResult getUser(@NonNull String userId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_USER) + userId; + String responseContent = this.cpService.get(apiUrl, null); + return WxCpUserResult.fromJson(responseContent); + } + + @Override + public WxCpUserListResult getUserList(@NonNull Integer departmentId, Integer fetchChild) throws WxErrorException { + String apiUrl = String.format(this.cpService.getWxCpConfigStorage().getApiUrl(GET_USER_LIST), departmentId, + fetchChild); + String responseContent = this.cpService.get(apiUrl, null); + return WxCpUserListResult.fromJson(responseContent); + } + + @Override + public WxCpListParentResult getUserListParent(@NonNull Integer departmentId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_USER_LIST_PARENT) + departmentId; + String responseContent = this.cpService.get(apiUrl, null); + return WxCpListParentResult.fromJson(responseContent); + } + + @Override + public WxCpBaseResp updateParent(@NonNull WxCpUpdateParentRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(UPDATE_PARENT); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp deleteParent(@NonNull String userId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(DELETE_PARENT) + userId; + String responseContent = this.cpService.get(apiUrl, null); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp setArchSyncMode(@NonNull Integer archSyncMode) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(SET_ARCH_SYNC_MODE); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("arch_sync_mode", archSyncMode); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpCreateDepartment createDepartment(@NonNull WxCpCreateDepartmentRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(DEPARTMENT_CREATE); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpCreateDepartment.fromJson(responseContent); + } + + @Override + public WxCpBaseResp updateDepartment(@NonNull WxCpUpdateDepartmentRequest request) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(DEPARTMENT_UPDATE); + String responseContent = this.cpService.post(apiUrl, request.toJson()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp deleteDepartment(Integer id) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(DEPARTMENT_DELETE) + id; + String responseContent = this.cpService.get(apiUrl, null); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public WxCpBaseResp setSubscribeMode(@NonNull Integer subscribeMode) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(SET_SUBSCRIBE_MODE); + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("subscribe_mode", subscribeMode); + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpBaseResp.fromJson(responseContent); + } + + @Override + public Integer getSubscribeMode() throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_SUBSCRIBE_MODE); + String responseContent = this.cpService.get(apiUrl, null); + return GsonParser.parse(responseContent).get("subscribe_mode").getAsInt(); + } + + @Override + public WxCpExternalContact getExternalContact(@NonNull String externalUserId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(EXTERNAL_CONTACT_GET) + externalUserId; + String responseContent = this.cpService.get(apiUrl, null); + return WxCpExternalContact.fromJson(responseContent); + } + + @Override + public WxCpAllowScope getAllowScope(@NonNull Integer agentId) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_ALLOW_SCOPE) + agentId; + String responseContent = this.cpService.get(apiUrl, null); + return WxCpAllowScope.fromJson(responseContent); + } + + @Override + public String convertToOpenId(@NonNull String externalUserId) throws WxErrorException { + return cpService.getExternalContactService().convertToOpenid(externalUserId); + } + + @Override + public WxCpDepartmentList listDepartment(Integer id) throws WxErrorException { + String apiUrl = Objects.isNull(id) ? this.cpService.getWxCpConfigStorage().getApiUrl(DEPARTMENT_LIST) : String.format("%s?id=%s", this.cpService.getWxCpConfigStorage().getApiUrl(DEPARTMENT_LIST), id); + String responseContent = this.cpService.get(apiUrl, null); + return WxCpDepartmentList.fromJson(responseContent); + } + + @Override + public WxCpSubscribeQrCode getSubscribeQrCode() throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_SUBSCRIBE_QR_CODE); + String responseContent = this.cpService.get(apiUrl, null); + return WxCpSubscribeQrCode.fromJson(responseContent); + } + + @Override + public WxCpSetUpgradeInfo setUpgradeInfo(Long upgradeTime, Integer upgradeSwitch) throws WxErrorException { + String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(SET_UPGRADE_INFO); + JsonObject jsonObject = new JsonObject(); + if (upgradeTime != null) { + jsonObject.addProperty("upgrade_time", upgradeTime); + } + if (upgradeSwitch != null) { + jsonObject.addProperty("upgrade_switch", upgradeSwitch); + } + String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + return WxCpSetUpgradeInfo.fromJson(responseContent); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java index d181af6749..ef78116e12 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java @@ -1,25 +1,27 @@ package me.chanjar.weixin.cp.api.impl; - -import me.chanjar.weixin.common.WxType; import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; -import me.chanjar.weixin.common.util.http.HttpType; +import me.chanjar.weixin.common.error.WxRuntimeException; +import me.chanjar.weixin.common.util.http.HttpClientType; +import me.chanjar.weixin.common.util.http.apache.ApacheBasicResponseHandler; import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder; import me.chanjar.weixin.cp.config.WxCpConfigStorage; import me.chanjar.weixin.cp.constant.WxCpApiPathConsts; import org.apache.http.HttpHost; 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.impl.client.BasicResponseHandler; import org.apache.http.impl.client.CloseableHttpClient; import java.io.IOException; +import java.util.concurrent.locks.Lock; /** + * The type Wx cp service apache http client. + * * @author someone */ public class WxCpServiceApacheHttpClientImpl extends BaseWxCpServiceImpl { @@ -37,8 +39,8 @@ public HttpHost getRequestHttpProxy() { } @Override - public HttpType getRequestType() { - return HttpType.APACHE_HTTP; + public HttpClientType getRequestType() { + return HttpClientType.APACHE_HTTP; } @Override @@ -48,7 +50,8 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException { } synchronized (this.globalAccessTokenRefreshLock) { - String url = String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN), this.configStorage.getCorpId(), this.configStorage.getCorpSecret()); + String url = String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN), + this.configStorage.getCorpId(), this.configStorage.getCorpSecret()); try { HttpGet httpGet = new HttpGet(url); @@ -57,13 +60,7 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException { .setProxy(this.httpProxy).build(); httpGet.setConfig(config); } - String resultContent; - try (CloseableHttpClient httpClient = getRequestHttpClient(); - CloseableHttpResponse response = httpClient.execute(httpGet)) { - resultContent = new BasicResponseHandler().handleResponse(response); - } finally { - httpGet.releaseConnection(); - } + String resultContent = getRequestHttpClient().execute(httpGet, ApacheBasicResponseHandler.INSTANCE); WxError error = WxError.fromJson(resultContent, WxType.CP); if (error.getErrorCode() != 0) { throw new WxErrorException(error); @@ -72,12 +69,57 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException { WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); this.configStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); } catch (IOException e) { - throw new RuntimeException(e); + throw new WxRuntimeException(e); } } return this.configStorage.getAccessToken(); } + @Override + public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException { + if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getMsgAuditAccessToken(); + } + + Lock lock = this.configStorage.getMsgAuditAccessTokenLock(); + lock.lock(); + try { + // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷 + if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getMsgAuditAccessToken(); + } + // 使用会话存档secret获取access_token + String msgAuditSecret = this.configStorage.getMsgAuditSecret(); + if (msgAuditSecret == null || msgAuditSecret.trim().isEmpty()) { + throw new WxErrorException("会话存档secret未配置"); + } + String url = String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN), + this.configStorage.getCorpId(), msgAuditSecret); + + try { + HttpGet httpGet = new HttpGet(url); + if (this.httpProxy != null) { + RequestConfig config = RequestConfig.custom() + .setProxy(this.httpProxy).build(); + httpGet.setConfig(config); + } + String resultContent = getRequestHttpClient().execute(httpGet, ApacheBasicResponseHandler.INSTANCE); + WxError error = WxError.fromJson(resultContent, WxType.CP); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + + WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); + this.configStorage.updateMsgAuditAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); + } catch (IOException e) { + throw new WxRuntimeException(e); + } + } finally { + lock.unlock(); + } + return this.configStorage.getMsgAuditAccessToken(); + } + @Override public void initHttp() { ApacheHttpClientBuilder apacheHttpClientBuilder = this.configStorage diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceHttpComponentsImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceHttpComponentsImpl.java new file mode 100644 index 0000000000..3ca041e7ec --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceHttpComponentsImpl.java @@ -0,0 +1,146 @@ +package me.chanjar.weixin.cp.api.impl; + +import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.error.WxRuntimeException; +import me.chanjar.weixin.common.util.http.HttpClientType; +import me.chanjar.weixin.common.util.http.hc.BasicResponseHandler; +import me.chanjar.weixin.common.util.http.hc.DefaultHttpComponentsClientBuilder; +import me.chanjar.weixin.common.util.http.hc.HttpComponentsClientBuilder; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import me.chanjar.weixin.cp.constant.WxCpApiPathConsts; +import org.apache.hc.client5.http.classic.methods.HttpGet; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.HttpHost; + +import java.io.IOException; +import java.util.concurrent.locks.Lock; + +/** + * The type Wx cp service apache http client. + * + * @author altusea + */ +public class WxCpServiceHttpComponentsImpl extends BaseWxCpServiceImpl { + + private CloseableHttpClient httpClient; + private HttpHost httpProxy; + + @Override + public CloseableHttpClient getRequestHttpClient() { + return httpClient; + } + + @Override + public HttpHost getRequestHttpProxy() { + return httpProxy; + } + + @Override + public HttpClientType getRequestType() { + return HttpClientType.HTTP_COMPONENTS; + } + + @Override + public String getAccessToken(boolean forceRefresh) throws WxErrorException { + if (!this.configStorage.isAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getAccessToken(); + } + + synchronized (this.globalAccessTokenRefreshLock) { + String url = String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN), + this.configStorage.getCorpId(), this.configStorage.getCorpSecret()); + + try { + HttpGet httpGet = new HttpGet(url); + if (this.httpProxy != null) { + RequestConfig config = RequestConfig.custom() + .setProxy(this.httpProxy).build(); + httpGet.setConfig(config); + } + String resultContent = getRequestHttpClient().execute(httpGet, BasicResponseHandler.INSTANCE); + WxError error = WxError.fromJson(resultContent, WxType.CP); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + + WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); + this.configStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); + } catch (IOException e) { + throw new WxRuntimeException(e); + } + } + return this.configStorage.getAccessToken(); + } + + @Override + public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException { + if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getMsgAuditAccessToken(); + } + + Lock lock = this.configStorage.getMsgAuditAccessTokenLock(); + lock.lock(); + try { + // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷 + if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getMsgAuditAccessToken(); + } + // 使用会话存档secret获取access_token + String msgAuditSecret = this.configStorage.getMsgAuditSecret(); + if (msgAuditSecret == null || msgAuditSecret.trim().isEmpty()) { + throw new WxErrorException("会话存档secret未配置"); + } + String url = String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN), + this.configStorage.getCorpId(), msgAuditSecret); + + try { + HttpGet httpGet = new HttpGet(url); + if (this.httpProxy != null) { + RequestConfig config = RequestConfig.custom() + .setProxy(this.httpProxy).build(); + httpGet.setConfig(config); + } + String resultContent = getRequestHttpClient().execute(httpGet, BasicResponseHandler.INSTANCE); + WxError error = WxError.fromJson(resultContent, WxType.CP); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + + WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); + this.configStorage.updateMsgAuditAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); + } catch (IOException e) { + throw new WxRuntimeException(e); + } + } finally { + lock.unlock(); + } + return this.configStorage.getMsgAuditAccessToken(); + } + + @Override + public void initHttp() { + HttpComponentsClientBuilder apacheHttpClientBuilder = DefaultHttpComponentsClientBuilder.get(); + + apacheHttpClientBuilder.httpProxyHost(this.configStorage.getHttpProxyHost()) + .httpProxyPort(this.configStorage.getHttpProxyPort()) + .httpProxyUsername(this.configStorage.getHttpProxyUsername()) + .httpProxyPassword(this.configStorage.getHttpProxyPassword() == null ? null : + this.configStorage.getHttpProxyPassword().toCharArray()); + + if (this.configStorage.getHttpProxyHost() != null && this.configStorage.getHttpProxyPort() > 0) { + this.httpProxy = new HttpHost(this.configStorage.getHttpProxyHost(), this.configStorage.getHttpProxyPort()); + } + + this.httpClient = apacheHttpClientBuilder.build(); + } + + @Override + public WxCpConfigStorage getWxCpConfigStorage() { + return this.configStorage; + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java index a7538c6ead..7b651cbc08 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceImpl.java @@ -1,12 +1,164 @@ package me.chanjar.weixin.cp.api.impl; +import com.google.gson.JsonObject; +import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.enums.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.error.WxRuntimeException; +import me.chanjar.weixin.common.util.http.apache.ApacheBasicResponseHandler; +import me.chanjar.weixin.common.util.json.GsonParser; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import me.chanjar.weixin.cp.constant.WxCpApiPathConsts; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.HttpGet; + +import java.io.IOException; +import java.util.concurrent.locks.Lock; + +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.GET_AGENT_CONFIG_TICKET; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.GET_JSAPI_TICKET; + /** *

  *  默认接口实现类,使用apache httpclient实现
  * Created by Binary Wang on 2017-5-27.
  * 
+ *
+ * 增加分布式锁(基于WxCpConfigStorage实现)的支持
+ * Updated by yuanqixun on 2020-05-13
+ * 
* * @author Binary Wang */ public class WxCpServiceImpl extends WxCpServiceApacheHttpClientImpl { + @Override + public String getAccessToken(boolean forceRefresh) throws WxErrorException { + final WxCpConfigStorage configStorage = getWxCpConfigStorage(); + if (!configStorage.isAccessTokenExpired() && !forceRefresh) { + return configStorage.getAccessToken(); + } + Lock lock = configStorage.getAccessTokenLock(); + lock.lock(); + try { + // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷 + if (!configStorage.isAccessTokenExpired() && !forceRefresh) { + return configStorage.getAccessToken(); + } + String url = String.format(configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN), + this.configStorage.getCorpId(), this.configStorage.getCorpSecret()); + try { + HttpGet httpGet = new HttpGet(url); + if (getRequestHttpProxy() != null) { + RequestConfig config = RequestConfig.custom().setProxy(getRequestHttpProxy()).build(); + httpGet.setConfig(config); + } + String resultContent = getRequestHttpClient().execute(httpGet, ApacheBasicResponseHandler.INSTANCE); + WxError error = WxError.fromJson(resultContent, WxType.CP); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + + WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); + configStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); + } catch (IOException e) { + throw new WxRuntimeException(e); + } + } finally { + lock.unlock(); + } + return configStorage.getAccessToken(); + } + + @Override + public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException { + final WxCpConfigStorage configStorage = getWxCpConfigStorage(); + if (!configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) { + return configStorage.getMsgAuditAccessToken(); + } + Lock lock = configStorage.getMsgAuditAccessTokenLock(); + lock.lock(); + try { + // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷 + if (!configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) { + return configStorage.getMsgAuditAccessToken(); + } + // 使用会话存档secret获取access_token + String msgAuditSecret = configStorage.getMsgAuditSecret(); + if (msgAuditSecret == null || msgAuditSecret.trim().isEmpty()) { + throw new WxErrorException("会话存档secret未配置"); + } + String url = String.format(configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN), + this.configStorage.getCorpId(), msgAuditSecret); + try { + HttpGet httpGet = new HttpGet(url); + if (getRequestHttpProxy() != null) { + RequestConfig config = RequestConfig.custom().setProxy(getRequestHttpProxy()).build(); + httpGet.setConfig(config); + } + String resultContent = getRequestHttpClient().execute(httpGet, ApacheBasicResponseHandler.INSTANCE); + WxError error = WxError.fromJson(resultContent, WxType.CP); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + + WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); + configStorage.updateMsgAuditAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); + } catch (IOException e) { + throw new WxRuntimeException(e); + } + } finally { + lock.unlock(); + } + return configStorage.getMsgAuditAccessToken(); + } + + @Override + public String getAgentJsapiTicket(boolean forceRefresh) throws WxErrorException { + final WxCpConfigStorage configStorage = getWxCpConfigStorage(); + if (forceRefresh) { + configStorage.expireAgentJsapiTicket(); + } + if (configStorage.isAgentJsapiTicketExpired()) { + Lock lock = configStorage.getAgentJsapiTicketLock(); + lock.lock(); + try { + // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷 + if (configStorage.isAgentJsapiTicketExpired()) { + String responseContent = this.get(configStorage.getApiUrl(GET_AGENT_CONFIG_TICKET), null); + JsonObject jsonObject = GsonParser.parse(responseContent); + configStorage.updateAgentJsapiTicket(jsonObject.get("ticket").getAsString(), + jsonObject.get("expires_in").getAsInt()); + } + } finally { + lock.unlock(); + } + } + return configStorage.getAgentJsapiTicket(); + } + + @Override + public String getJsapiTicket(boolean forceRefresh) throws WxErrorException { + final WxCpConfigStorage configStorage = getWxCpConfigStorage(); + if (forceRefresh) { + configStorage.expireJsapiTicket(); + } + + if (configStorage.isJsapiTicketExpired()) { + Lock lock = configStorage.getJsapiTicketLock(); + lock.lock(); + try { + // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷 + if (configStorage.isJsapiTicketExpired()) { + String responseContent = this.get(configStorage.getApiUrl(GET_JSAPI_TICKET), null); + JsonObject tmpJsonObject = GsonParser.parse(responseContent); + configStorage.updateJsapiTicket(tmpJsonObject.get("ticket").getAsString(), + tmpJsonObject.get("expires_in").getAsInt()); + } + } finally { + lock.unlock(); + } + } + return configStorage.getJsapiTicket(); + } } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java index cc76e9cf4c..eba9315649 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceJoddHttpImpl.java @@ -3,17 +3,21 @@ import jodd.http.HttpConnectionProvider; import jodd.http.HttpRequest; import jodd.http.HttpResponse; -import jodd.http.JoddHttp; import jodd.http.ProxyInfo; -import me.chanjar.weixin.common.WxType; +import jodd.http.net.SocketHttpConnectionProvider; import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; -import me.chanjar.weixin.common.util.http.HttpType; +import me.chanjar.weixin.common.util.http.HttpClientType; import me.chanjar.weixin.cp.config.WxCpConfigStorage; import me.chanjar.weixin.cp.constant.WxCpApiPathConsts; +import java.util.concurrent.locks.Lock; + /** + * The type Wx cp service jodd http. + * * @author someone */ public class WxCpServiceJoddHttpImpl extends BaseWxCpServiceImpl { @@ -31,8 +35,8 @@ public ProxyInfo getRequestHttpProxy() { } @Override - public HttpType getRequestType() { - return HttpType.JODD_HTTP; + public HttpClientType getRequestType() { + return HttpClientType.JODD_HTTP; } @Override @@ -61,6 +65,45 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException { return this.configStorage.getAccessToken(); } + @Override + public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException { + if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getMsgAuditAccessToken(); + } + + Lock lock = this.configStorage.getMsgAuditAccessTokenLock(); + lock.lock(); + try { + // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷 + if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getMsgAuditAccessToken(); + } + // 使用会话存档secret获取access_token + String msgAuditSecret = this.configStorage.getMsgAuditSecret(); + if (msgAuditSecret == null || msgAuditSecret.trim().isEmpty()) { + throw new WxErrorException("会话存档secret未配置"); + } + HttpRequest request = HttpRequest.get(String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN), + this.configStorage.getCorpId(), msgAuditSecret)); + if (this.httpProxy != null) { + httpClient.useProxy(this.httpProxy); + } + request.withConnectionProvider(httpClient); + HttpResponse response = request.send(); + + String resultContent = response.bodyText(); + WxError error = WxError.fromJson(resultContent, WxType.CP); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); + this.configStorage.updateMsgAuditAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); + } finally { + lock.unlock(); + } + return this.configStorage.getMsgAuditAccessToken(); + } + @Override public void initHttp() { if (this.configStorage.getHttpProxyHost() != null && this.configStorage.getHttpProxyPort() > 0) { @@ -68,7 +111,7 @@ public void initHttp() { configStorage.getHttpProxyPort(), configStorage.getHttpProxyUsername(), configStorage.getHttpProxyPassword()); } - httpClient = JoddHttp.httpConnectionProvider; + httpClient = new SocketHttpConnectionProvider(); } @Override diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOkHttpImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOkHttpImpl.java index 596dc0608e..ce77b37805 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOkHttpImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOkHttpImpl.java @@ -1,20 +1,24 @@ package me.chanjar.weixin.cp.api.impl; import lombok.extern.slf4j.Slf4j; -import me.chanjar.weixin.common.WxType; import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; -import me.chanjar.weixin.common.util.http.HttpType; +import me.chanjar.weixin.common.util.http.HttpClientType; +import me.chanjar.weixin.common.util.http.okhttp.DefaultOkHttpClientBuilder; import me.chanjar.weixin.common.util.http.okhttp.OkHttpProxyInfo; import me.chanjar.weixin.cp.config.WxCpConfigStorage; import okhttp3.*; import java.io.IOException; +import java.util.concurrent.locks.Lock; import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.GET_TOKEN; /** + * The type Wx cp service ok http. + * * @author someone */ @Slf4j @@ -33,8 +37,8 @@ public OkHttpProxyInfo getRequestHttpProxy() { } @Override - public HttpType getRequestType() { - return HttpType.OK_HTTP; + public HttpClientType getRequestType() { + return HttpClientType.OK_HTTP; } @Override @@ -48,7 +52,8 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException { OkHttpClient client = getRequestHttpClient(); //请求的request Request request = new Request.Builder() - .url(String.format(this.configStorage.getApiUrl(GET_TOKEN), this.configStorage.getCorpId(), this.configStorage.getCorpSecret())) + .url(String.format(this.configStorage.getApiUrl(GET_TOKEN), this.configStorage.getCorpId(), + this.configStorage.getCorpSecret())) .get() .build(); String resultContent = null; @@ -70,6 +75,52 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException { return this.configStorage.getAccessToken(); } + @Override + public String getMsgAuditAccessToken(boolean forceRefresh) throws WxErrorException { + if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getMsgAuditAccessToken(); + } + + Lock lock = this.configStorage.getMsgAuditAccessTokenLock(); + lock.lock(); + try { + // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷 + if (!this.configStorage.isMsgAuditAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getMsgAuditAccessToken(); + } + // 使用会话存档secret获取access_token + String msgAuditSecret = this.configStorage.getMsgAuditSecret(); + if (msgAuditSecret == null || msgAuditSecret.trim().isEmpty()) { + throw new WxErrorException("会话存档secret未配置"); + } + //得到httpClient + OkHttpClient client = getRequestHttpClient(); + //请求的request + Request request = new Request.Builder() + .url(String.format(this.configStorage.getApiUrl(GET_TOKEN), this.configStorage.getCorpId(), + msgAuditSecret)) + .get() + .build(); + String resultContent = null; + try (Response response = client.newCall(request).execute()) { + resultContent = response.body().string(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } + + WxError error = WxError.fromJson(resultContent, WxType.CP); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); + this.configStorage.updateMsgAuditAccessToken(accessToken.getAccessToken(), + accessToken.getExpiresIn()); + } finally { + lock.unlock(); + } + return this.configStorage.getMsgAuditAccessToken(); + } + @Override public void initHttp() { log.debug("WxCpServiceOkHttpImpl initHttp"); @@ -79,24 +130,22 @@ public void initHttp() { configStorage.getHttpProxyPort(), configStorage.getHttpProxyUsername(), configStorage.getHttpProxyPassword()); - } - - OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); - if (httpProxy != null) { + OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder(); clientBuilder.proxy(getRequestHttpProxy().getProxy()); - //设置授权 - clientBuilder.authenticator(new Authenticator() { + clientBuilder.proxyAuthenticator(new Authenticator() { @Override public Request authenticate(Route route, Response response) throws IOException { String credential = Credentials.basic(httpProxy.getProxyUsername(), httpProxy.getProxyPassword()); return response.request().newBuilder() - .header("Authorization", credential) + .header("Proxy-Authorization", credential) .build(); } }); + httpClient = clientBuilder.build(); + } else { + httpClient = DefaultOkHttpClientBuilder.get().build(); } - httpClient = clientBuilder.build(); } @Override diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOnTpImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOnTpImpl.java index 35eab626a7..207681c7ae 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOnTpImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOnTpImpl.java @@ -3,7 +3,7 @@ import lombok.RequiredArgsConstructor; import me.chanjar.weixin.common.bean.WxAccessToken; import me.chanjar.weixin.common.error.WxErrorException; -import me.chanjar.weixin.cp.api.WxCpTpService; +import me.chanjar.weixin.cp.tp.service.WxCpTpService; /** *
@@ -24,7 +24,8 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
     }
     //access token通过第三方应用service获取
     //corpSecret对应企业永久授权码
-    WxAccessToken accessToken = wxCpTpService.getCorpToken(this.configStorage.getCorpId(), this.configStorage.getCorpSecret());
+    WxAccessToken accessToken = wxCpTpService.getCorpToken(this.configStorage.getCorpId(),
+      this.configStorage.getCorpSecret());
 
     this.configStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());
     return this.configStorage.getAccessToken();
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpTagServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpTagServiceImpl.java
index 753f2493a1..e73ef98a98 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpTagServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpTagServiceImpl.java
@@ -1,9 +1,12 @@
 package me.chanjar.weixin.cp.api.impl;
 
-import com.google.gson.*;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
 import com.google.gson.reflect.TypeToken;
 import lombok.RequiredArgsConstructor;
 import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.json.GsonParser;
 import me.chanjar.weixin.cp.api.WxCpService;
 import me.chanjar.weixin.cp.api.WxCpTagService;
 import me.chanjar.weixin.cp.bean.WxCpTag;
@@ -39,18 +42,11 @@ public String create(String name, Integer id) throws WxErrorException {
     return this.create(o);
   }
 
-  @Override
-  public String create(String tagName) throws WxErrorException {
-    JsonObject o = new JsonObject();
-    o.addProperty("tagname", tagName);
-    return this.create(o);
-  }
-
   private String create(JsonObject param) throws WxErrorException {
     String url = this.mainService.getWxCpConfigStorage().getApiUrl(TAG_CREATE);
     String responseContent = this.mainService.post(url, param.toString());
-    JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
-    return tmpJsonElement.getAsJsonObject().get("tagid").getAsString();
+    JsonObject jsonObject = GsonParser.parse(responseContent);
+    return jsonObject.get("tagid").getAsString();
   }
 
   @Override
@@ -72,10 +68,10 @@ public void delete(String tagId) throws WxErrorException {
   public List listAll() throws WxErrorException {
     String url = this.mainService.getWxCpConfigStorage().getApiUrl(TAG_LIST);
     String responseContent = this.mainService.get(url, null);
-    JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
+    JsonObject tmpJson = GsonParser.parse(responseContent);
     return WxCpGsonBuilder.create()
       .fromJson(
-        tmpJsonElement.getAsJsonObject().get("taglist"),
+        tmpJson.get("taglist"),
         new TypeToken>() {
         }.getType()
       );
@@ -85,10 +81,10 @@ public List listAll() throws WxErrorException {
   public List listUsersByTagId(String tagId) throws WxErrorException {
     String url = String.format(this.mainService.getWxCpConfigStorage().getApiUrl(TAG_GET), tagId);
     String responseContent = this.mainService.get(url, null);
-    JsonElement tmpJsonElement = new JsonParser().parse(responseContent);
+    JsonObject tmpJson = GsonParser.parse(responseContent);
     return WxCpGsonBuilder.create()
       .fromJson(
-        tmpJsonElement.getAsJsonObject().get("userlist"),
+        tmpJson.get("userlist"),
         new TypeToken>() {
         }.getType()
       );
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpTaskCardServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpTaskCardServiceImpl.java
index 97530a6e9d..8469451428 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpTaskCardServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpTaskCardServiceImpl.java
@@ -5,13 +5,14 @@
 import me.chanjar.weixin.common.util.json.WxGsonBuilder;
 import me.chanjar.weixin.cp.api.WxCpService;
 import me.chanjar.weixin.cp.api.WxCpTaskCardService;
-import me.chanjar.weixin.cp.constant.WxCpApiPathConsts;
+import me.chanjar.weixin.cp.bean.message.TemplateCardMessage;
 
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.TaskCard.*;
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.TaskCard.UPDATE_TASK_CARD;
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.TaskCard.UPDATE_TEMPLATE_CARD;
 
 /**
  * 
@@ -19,24 +20,51 @@
  *  Created by Jeff on 2019-05-16.
  * 
* - * @author Jeff - * @date 2019-05-16 + * @author Jeff created on 2019-05-16 */ @RequiredArgsConstructor public class WxCpTaskCardServiceImpl implements WxCpTaskCardService { private final WxCpService mainService; @Override - public void update(List userIds, String taskId, String clickedKey) throws WxErrorException { + public void update(List userIds, String taskId, String replaceName) throws WxErrorException { Integer agentId = this.mainService.getWxCpConfigStorage().getAgentId(); Map data = new HashMap<>(4); data.put("userids", userIds); data.put("agentid", agentId); data.put("task_id", taskId); - data.put("clicked_key", clickedKey); + // 文档地址:https://open.work.weixin.qq.com/wwopen/devtool/interface?doc_id=16386 + data.put("clicked_key", replaceName); String url = this.mainService.getWxCpConfigStorage().getApiUrl(UPDATE_TASK_CARD); this.mainService.post(url, WxGsonBuilder.create().toJson(data)); } + + @Override + public void updateTemplateCardButton(List userIds, List partyIds, + List tagIds, Integer atAll, + String responseCode, String replaceName) throws WxErrorException { + Integer agentId = this.mainService.getWxCpConfigStorage().getAgentId(); + Map data = new HashMap<>(7); + data.put("userids", userIds); + data.put("partyids", partyIds); + data.put("tagids", tagIds); + data.put("atall", atAll); + data.put("agentid", agentId); + data.put("response_code", responseCode); + Map btnMap = new HashMap<>(); + btnMap.put("replace_name", replaceName); + data.put("button", btnMap); + + String url = this.mainService.getWxCpConfigStorage().getApiUrl(UPDATE_TEMPLATE_CARD); + this.mainService.post(url, WxGsonBuilder.create().toJson(data)); + + } + + @Override + public void updateTemplateCardButton(TemplateCardMessage templateCardMessage) throws WxErrorException { + String url = this.mainService.getWxCpConfigStorage().getApiUrl(UPDATE_TEMPLATE_CARD); + this.mainService.post(url, templateCardMessage.toJson()); + } } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpUserServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpUserServiceImpl.java index b9844d39b9..f1556d4e31 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpUserServiceImpl.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpUserServiceImpl.java @@ -1,17 +1,27 @@ package me.chanjar.weixin.cp.api.impl; import com.google.common.collect.Maps; -import com.google.gson.*; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; import com.google.gson.reflect.TypeToken; import lombok.RequiredArgsConstructor; import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.json.GsonParser; import me.chanjar.weixin.cp.api.WxCpService; import me.chanjar.weixin.cp.api.WxCpUserService; import me.chanjar.weixin.cp.bean.WxCpInviteResult; +import me.chanjar.weixin.cp.bean.WxCpOpenUseridToUseridResult; import me.chanjar.weixin.cp.bean.WxCpUser; -import me.chanjar.weixin.cp.bean.WxCpUserExternalContactInfo; +import me.chanjar.weixin.cp.bean.WxCpUseridToOpenUseridResult; +import me.chanjar.weixin.cp.bean.external.contact.WxCpExternalContactInfo; +import me.chanjar.weixin.cp.bean.user.WxCpDeptUserResult; import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; +import org.apache.commons.lang3.time.FastDateFormat; +import java.text.Format; +import java.util.ArrayList; +import java.util.Date; import java.util.List; import java.util.Map; @@ -26,6 +36,8 @@ */ @RequiredArgsConstructor public class WxCpUserServiceImpl implements WxCpUserService { + private final Format dateFormat = FastDateFormat.getInstance("yyyy-MM-dd"); + private final WxCpService mainService; @Override @@ -84,9 +96,9 @@ public List listByDepartment(Long departId, Boolean fetchChild, Intege String url = this.mainService.getWxCpConfigStorage().getApiUrl(USER_LIST + departId); String responseContent = this.mainService.get(url, params); - JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + JsonObject jsonObject = GsonParser.parse(responseContent); return WxCpGsonBuilder.create() - .fromJson(tmpJsonElement.getAsJsonObject().get("userlist"), + .fromJson(jsonObject.get("userlist"), new TypeToken>() { }.getType() ); @@ -107,10 +119,10 @@ public List listSimpleByDepartment(Long departId, Boolean fetchChild, String url = this.mainService.getWxCpConfigStorage().getApiUrl(USER_SIMPLE_LIST + departId); String responseContent = this.mainService.get(url, params); - JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + JsonObject tmpJson = GsonParser.parse(responseContent); return WxCpGsonBuilder.create() .fromJson( - tmpJsonElement.getAsJsonObject().get("userlist"), + tmpJson.get("userlist"), new TypeToken>() { }.getType() ); @@ -158,14 +170,14 @@ public Map userId2Openid(String userId, Integer agentId) throws } String responseContent = this.mainService.post(url, jsonObject.toString()); - JsonElement tmpJsonElement = new JsonParser().parse(responseContent); + JsonObject tmpJson = GsonParser.parse(responseContent); Map result = Maps.newHashMap(); - if (tmpJsonElement.getAsJsonObject().get("openid") != null) { - result.put("openid", tmpJsonElement.getAsJsonObject().get("openid").getAsString()); + if (tmpJson.get("openid") != null) { + result.put("openid", tmpJson.get("openid").getAsString()); } - if (tmpJsonElement.getAsJsonObject().get("appid") != null) { - result.put("appid", tmpJsonElement.getAsJsonObject().get("appid").getAsString()); + if (tmpJson.get("appid") != null) { + result.put("appid", tmpJson.get("appid").getAsString()); } return result; @@ -177,8 +189,8 @@ public String openid2UserId(String openid) throws WxErrorException { jsonObject.addProperty("openid", openid); String url = this.mainService.getWxCpConfigStorage().getApiUrl(USER_CONVERT_TO_USERID); String responseContent = this.mainService.post(url, jsonObject.toString()); - JsonElement tmpJsonElement = new JsonParser().parse(responseContent); - return tmpJsonElement.getAsJsonObject().get("userid").getAsString(); + JsonObject tmpJson = GsonParser.parse(responseContent); + return tmpJson.get("userid").getAsString(); } @Override @@ -187,14 +199,86 @@ public String getUserId(String mobile) throws WxErrorException { jsonObject.addProperty("mobile", mobile); String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_USER_ID); String responseContent = this.mainService.post(url, jsonObject.toString()); - JsonElement tmpJsonElement = new JsonParser().parse(responseContent); - return tmpJsonElement.getAsJsonObject().get("userid").getAsString(); + JsonObject tmpJson = GsonParser.parse(responseContent); + return tmpJson.get("userid").getAsString(); + } + + @Override + public String getUserIdByEmail(String email, int emailType) throws WxErrorException { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("email", email); + jsonObject.addProperty("email_type", emailType); + String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_USER_ID_BY_EMAIL); + String responseContent = this.mainService.post(url, jsonObject.toString()); + JsonObject tmpJson = GsonParser.parse(responseContent); + return tmpJson.get("userid").getAsString(); } @Override - public WxCpUserExternalContactInfo getExternalContact(String userId) throws WxErrorException { + public WxCpExternalContactInfo getExternalContact(String userId) throws WxErrorException { String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_EXTERNAL_CONTACT + userId); String responseContent = this.mainService.get(url, null); - return WxCpUserExternalContactInfo.fromJson(responseContent); + return WxCpExternalContactInfo.fromJson(responseContent); + } + + @Override + public String getJoinQrCode(int sizeType) throws WxErrorException { + String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_JOIN_QR_CODE + sizeType); + String responseContent = this.mainService.get(url, null); + JsonObject tmpJson = GsonParser.parse(responseContent); + return tmpJson.get("join_qrcode").getAsString(); } + + @Override + public Integer getActiveStat(Date date) throws WxErrorException { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("date", this.dateFormat.format(date)); + String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_ACTIVE_STAT); + String responseContent = this.mainService.post(url, jsonObject.toString()); + JsonObject tmpJson = GsonParser.parse(responseContent); + return tmpJson.get("active_cnt").getAsInt(); + } + + @Override + public WxCpUseridToOpenUseridResult useridToOpenUserid(ArrayList useridList) throws WxErrorException { + JsonObject jsonObject = new JsonObject(); + JsonArray jsonArray = new JsonArray(); + for (String userid : useridList) { + jsonArray.add(userid); + } + jsonObject.add("userid_list", jsonArray); + String url = this.mainService.getWxCpConfigStorage().getApiUrl(USERID_TO_OPEN_USERID); + String responseContent = this.mainService.post(url, jsonObject.toString()); + return WxCpUseridToOpenUseridResult.fromJson(responseContent); + } + + @Override + public WxCpOpenUseridToUseridResult openUseridToUserid(List openUseridList, String sourceAgentId) throws WxErrorException { + JsonObject jsonObject = new JsonObject(); + JsonArray jsonArray = new JsonArray(); + for (String openUserid : openUseridList) { + jsonArray.add(openUserid); + } + jsonObject.add("open_userid_list", jsonArray); + jsonObject.addProperty("source_agentid", sourceAgentId); + String url = this.mainService.getWxCpConfigStorage().getApiUrl(OPEN_USERID_TO_USERID); + String responseContent = this.mainService.post(url, jsonObject.toString()); + return WxCpOpenUseridToUseridResult.fromJson(responseContent); + } + + @Override + public WxCpDeptUserResult getUserListId(String cursor, Integer limit) throws WxErrorException { + String apiUrl = this.mainService.getWxCpConfigStorage().getApiUrl(USER_LIST_ID); + JsonObject jsonObject = new JsonObject(); + if (cursor != null) { + jsonObject.addProperty("cursor", cursor); + } + if (limit != null) { + jsonObject.addProperty("limit", limit); + } + String responseContent = this.mainService.post(apiUrl, jsonObject.toString()); + return WxCpDeptUserResult.fromJson(responseContent); + } + + } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/Gender.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/Gender.java index 2b6e26efde..b47697ffac 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/Gender.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/Gender.java @@ -1,5 +1,8 @@ package me.chanjar.weixin.cp.bean; +import lombok.AllArgsConstructor; +import lombok.Getter; + /** *
  *  性别枚举
@@ -8,7 +11,13 @@
  *
  * @author Binary Wang
  */
+@Getter
+@AllArgsConstructor
 public enum Gender {
+  /**
+   * 未定义
+   */
+  UNDEFINED("未定义", "0"),
   /**
    * 男
    */
@@ -18,28 +27,20 @@ public enum Gender {
    */
   FEMALE("女", "2");
 
-  private String genderName;
-  private String code;
-
-  Gender(String genderName, String code) {
-    this.genderName = genderName;
-    this.code = code;
-  }
-
-  public String getGenderName() {
-    return this.genderName;
-  }
-
-  public String getCode() {
-    return this.code;
-  }
+  private final String genderName;
+  private final String code;
 
+  /**
+   * From code gender.
+   *
+   * @param code the code
+   * @return the gender
+   */
   public static Gender fromCode(String code) {
-    if ("1".equals(code)) {
-      return Gender.MALE;
-    }
-    if ("2".equals(code)) {
-      return Gender.FEMALE;
+    for (Gender a : Gender.values()) {
+      if (a.code.equals(code)) {
+        return a;
+      }
     }
 
     return null;
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpAgent.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpAgent.java
index 18dfb346ce..5d61b3a199 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpAgent.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpAgent.java
@@ -1,8 +1,5 @@
 package me.chanjar.weixin.cp.bean;
 
-import java.io.Serializable;
-import java.util.List;
-
 import com.google.gson.annotations.SerializedName;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
@@ -10,6 +7,9 @@
 import lombok.NoArgsConstructor;
 import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
 
+import java.io.Serializable;
+import java.util.List;
+
 /**
  * 
  * 企业号应用信息.
@@ -70,36 +70,64 @@ public class WxCpAgent implements Serializable {
   @SerializedName("home_url")
   private String homeUrl;
 
+  @SerializedName("customized_publish_status")
+  private Integer customizedPublishStatus;
+
+  /**
+   * From json wx cp agent.
+   *
+   * @param json the json
+   * @return the wx cp agent
+   */
   public static WxCpAgent fromJson(String json) {
     return WxCpGsonBuilder.create().fromJson(json, WxCpAgent.class);
   }
 
+  /**
+   * To json string.
+   *
+   * @return the string
+   */
   public String toJson() {
     return WxCpGsonBuilder.create().toJson(this);
   }
 
+  /**
+   * The type Users.
+   */
   @Data
   public static class Users implements Serializable {
     private static final long serialVersionUID = 8801100463558788565L;
+
     @SerializedName("user")
     private List users;
   }
 
+  /**
+   * The type User.
+   */
   @Data
-  public class User implements Serializable {
+  public static class User implements Serializable {
     private static final long serialVersionUID = 7287632514385508024L;
+
     @SerializedName("userid")
     private String userId;
   }
 
+  /**
+   * The type Parties.
+   */
   @Data
-  public class Parties {
+  public static class Parties {
     @SerializedName("partyid")
-    private List partyIds = null;
+    private List partyIds = null;
   }
 
+  /**
+   * The type Tags.
+   */
   @Data
-  public class Tags {
+  public static class Tags {
     @SerializedName("tagid")
     private List tagIds = null;
   }
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpAgentJsapiSignature.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpAgentJsapiSignature.java
new file mode 100644
index 0000000000..4562d9b9b0
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpAgentJsapiSignature.java
@@ -0,0 +1,31 @@
+package me.chanjar.weixin.cp.bean;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 调用wx.agentConfig时所需要的签名信息
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class WxCpAgentJsapiSignature implements Serializable {
+  private static final long serialVersionUID = 2650119900835832545L;
+
+  private String url;
+
+  private String corpid;
+
+  private Integer agentid;
+
+  private long timestamp;
+
+  private String nonceStr;
+
+  private String signature;
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpAgentWorkBench.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpAgentWorkBench.java
new file mode 100644
index 0000000000..4c17397ecd
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpAgentWorkBench.java
@@ -0,0 +1,262 @@
+package me.chanjar.weixin.cp.bean;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+import me.chanjar.weixin.cp.bean.workbench.WorkBenchKeyData;
+import me.chanjar.weixin.cp.bean.workbench.WorkBenchList;
+import me.chanjar.weixin.cp.constant.WxCpConsts;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * The type Wx cp agent work bench.
+ *
+ * @author songshiyu  created on  : create in 16:09 2020/9/27 工作台自定义展示
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class WxCpAgentWorkBench implements Serializable {
+  private static final long serialVersionUID = -4136604790232843229L;
+
+  /**
+   * 展示类型,目前支持 “keydata”、 “image”、 “list” 、”webview”
+   */
+  private String type;
+  /**
+   * 用户的userid
+   */
+  private String userId;
+  /**
+   * 用户的userIds
+   */
+  private List useridList;
+  /**
+   * 应用id
+   */
+  private Long agentId;
+  /**
+   * 点击跳转url,若不填且应用设置了主页url,则跳转到主页url,否则跳到应用会话窗口
+   */
+  private String jumpUrl;
+  /**
+   * 若应用为小程序类型,该字段填小程序pagepath,若未设置,跳到小程序主页
+   */
+  private String pagePath;
+  /**
+   * 图片url:图片的最佳比例为3.35:1;webview:渲染展示的url
+   */
+  private String url;
+  /**
+   * 是否覆盖用户工作台的数据。设置为true的时候,会覆盖企业所有用户当前设置的数据。若设置为false,则不会覆盖用户当前设置的所有数据
+   */
+  private Boolean replaceUserData;
+  /**
+   * 是否开启webview内的链接跳转能力,默认值为false。注意:开启之后,会使jump_url失效。 链接跳转仅支持以下schema方式:wxwork://openurl?url=xxxx,注意url需要进行编码。
+   * 参考示例:今日要闻
+   */
+  private Boolean enableWebviewClick;
+  /**
+   * 高度。可以有两种选择:single_row与double_row。当为single_row时,高度为106px(如果隐藏标题则为147px)。
+   * 当为double_row时,高度固定为171px(如果隐藏标题则为212px)。默认值为double_row
+   */
+  private String height;
+  /**
+   * 是否要隐藏展示了应用名称的标题部分,默认值为false。
+   */
+  private Boolean hideTitle;
+
+  private List keyDataList;
+
+  private List lists;
+
+  /**
+   * 生成模板Json字符串
+   *
+   * @return the string
+   */
+  public String toTemplateString() {
+    JsonObject templateObject = new JsonObject();
+    templateObject.addProperty("agentid", this.agentId);
+    templateObject.addProperty("type", this.type);
+    if (this.replaceUserData != null) {
+      templateObject.addProperty("replace_user_data", this.replaceUserData);
+    }
+    this.handle(templateObject);
+    return templateObject.toString();
+  }
+
+  /**
+   * 生成用户数据Json字符串
+   *
+   * @return the string
+   */
+  public String toUserDataString() {
+    JsonObject userDataObject = new JsonObject();
+    userDataObject.addProperty("agentid", this.agentId);
+    userDataObject.addProperty("userid", this.userId);
+    userDataObject.addProperty("type", this.type);
+    this.handle(userDataObject);
+    return userDataObject.toString();
+  }
+
+  /**
+   * 生成批量用户数据Json字符串
+   *
+   * @return the string
+   */
+  public String toBatchUserDataString() {
+    JsonObject userDataObject = new JsonObject();
+    userDataObject.addProperty("agentid", this.agentId);
+    JsonArray useridList = WxGsonBuilder.create().toJsonTree(this.useridList).getAsJsonArray();
+    userDataObject.add("userid_list", useridList);
+    this.handleBatch(userDataObject);
+    return userDataObject.toString();
+  }
+
+  /**
+   * 处理不用类型的工作台数据
+   */
+  private void handle(JsonObject templateObject) {
+    switch (this.getType()) {
+      case WxCpConsts.WorkBenchType.KEYDATA: {
+        JsonArray keyDataArray = new JsonArray();
+        JsonObject itemsObject = new JsonObject();
+        for (WorkBenchKeyData keyDataItem : this.keyDataList) {
+          JsonObject keyDataObject = new JsonObject();
+          keyDataObject.addProperty("key", keyDataItem.getKey());
+          keyDataObject.addProperty("data", keyDataItem.getData());
+          keyDataObject.addProperty("jump_url", keyDataItem.getJumpUrl());
+          keyDataObject.addProperty("pagepath", keyDataItem.getPagePath());
+          keyDataArray.add(keyDataObject);
+        }
+        itemsObject.add("items", keyDataArray);
+        templateObject.add("keydata", itemsObject);
+        break;
+      }
+      case WxCpConsts.WorkBenchType.IMAGE: {
+        JsonObject image = new JsonObject();
+        image.addProperty("url", this.url);
+        image.addProperty("jump_url", this.jumpUrl);
+        image.addProperty("pagepath", this.pagePath);
+        templateObject.add("image", image);
+        break;
+      }
+      case WxCpConsts.WorkBenchType.LIST: {
+        JsonArray listArray = new JsonArray();
+        JsonObject itemsObject = new JsonObject();
+        for (WorkBenchList listItem : this.lists) {
+          JsonObject listObject = new JsonObject();
+          listObject.addProperty("title", listItem.getTitle());
+          listObject.addProperty("jump_url", listItem.getJumpUrl());
+          listObject.addProperty("pagepath", listItem.getPagePath());
+          listArray.add(listObject);
+        }
+        itemsObject.add("items", listArray);
+        templateObject.add("list", itemsObject);
+        break;
+      }
+      case WxCpConsts.WorkBenchType.WEBVIEW: {
+        JsonObject webview = new JsonObject();
+        webview.addProperty("url", this.url);
+        webview.addProperty("jump_url", this.jumpUrl);
+        webview.addProperty("pagepath", this.pagePath);
+        if (this.enableWebviewClick != null) {
+          webview.addProperty("enable_webview_click", this.enableWebviewClick);
+        }
+        webview.addProperty("height", this.height);
+        if (this.hideTitle != null) {
+          webview.addProperty("hide_title", this.hideTitle);
+        }
+        templateObject.add("webview", webview);
+        break;
+      }
+      default: {
+        //do nothing
+      }
+    }
+  }
+
+  /**
+   * 处理不用类型的工作台数据
+   */
+  private void handleBatch(JsonObject templateObject) {
+    switch (this.getType()) {
+      case WxCpConsts.WorkBenchType.KEYDATA: {
+        JsonArray keyDataArray = new JsonArray();
+        JsonObject itemsObject = new JsonObject();
+        for (WorkBenchKeyData keyDataItem : this.keyDataList) {
+          JsonObject keyDataObject = new JsonObject();
+          keyDataObject.addProperty("key", keyDataItem.getKey());
+          keyDataObject.addProperty("data", keyDataItem.getData());
+          keyDataObject.addProperty("jump_url", keyDataItem.getJumpUrl());
+          keyDataObject.addProperty("pagepath", keyDataItem.getPagePath());
+          keyDataArray.add(keyDataObject);
+        }
+        itemsObject.add("items", keyDataArray);
+        JsonObject dataObject = new JsonObject();
+        dataObject.addProperty("type", WxCpConsts.WorkBenchType.KEYDATA);
+        dataObject.add("keydata", itemsObject);
+        templateObject.add("data", dataObject);
+        break;
+      }
+      case WxCpConsts.WorkBenchType.IMAGE: {
+        JsonObject image = new JsonObject();
+        image.addProperty("url", this.url);
+        image.addProperty("jump_url", this.jumpUrl);
+        image.addProperty("pagepath", this.pagePath);
+        JsonObject dataObject = new JsonObject();
+        dataObject.addProperty("type", WxCpConsts.WorkBenchType.IMAGE);
+        dataObject.add("image", image);
+        templateObject.add("data", dataObject);
+        break;
+      }
+      case WxCpConsts.WorkBenchType.LIST: {
+        JsonArray listArray = new JsonArray();
+        JsonObject itemsObject = new JsonObject();
+        for (WorkBenchList listItem : this.lists) {
+          JsonObject listObject = new JsonObject();
+          listObject.addProperty("title", listItem.getTitle());
+          listObject.addProperty("jump_url", listItem.getJumpUrl());
+          listObject.addProperty("pagepath", listItem.getPagePath());
+          listArray.add(listObject);
+        }
+        itemsObject.add("items", listArray);
+        JsonObject dataObject = new JsonObject();
+        dataObject.addProperty("type", WxCpConsts.WorkBenchType.LIST);
+        dataObject.add("list", itemsObject);
+        templateObject.add("data", dataObject);
+        break;
+      }
+      case WxCpConsts.WorkBenchType.WEBVIEW: {
+        JsonObject webview = new JsonObject();
+        webview.addProperty("url", this.url);
+        webview.addProperty("jump_url", this.jumpUrl);
+        webview.addProperty("pagepath", this.pagePath);
+        if (this.enableWebviewClick != null) {
+          webview.addProperty("enable_webview_click", this.enableWebviewClick);
+        }
+        webview.addProperty("height", this.height);
+        if (this.hideTitle != null) {
+          webview.addProperty("hide_title", this.hideTitle);
+        }
+        JsonObject dataObject = new JsonObject();
+        dataObject.addProperty("type", WxCpConsts.WorkBenchType.WEBVIEW);
+        dataObject.add("webview", webview);
+        templateObject.add("data", dataObject);
+        break;
+      }
+      default: {
+        //do nothing
+      }
+    }
+  }
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpApprovalDataResult.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpApprovalDataResult.java
deleted file mode 100644
index 383c526568..0000000000
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpApprovalDataResult.java
+++ /dev/null
@@ -1,68 +0,0 @@
-package me.chanjar.weixin.cp.bean;
-
-import com.google.gson.annotations.SerializedName;
-import lombok.Data;
-
-import java.io.Serializable;
-import java.util.Map;
-
-/**
- * 企业微信 OA 审批数据.
- *
- * @author Element
- * @date 2019-04-06 14:36
- */
-@Data
-public class WxCpApprovalDataResult implements Serializable {
-  private static final long serialVersionUID = -1046940445840716590L;
-
-  @SerializedName("errcode")
-  private Integer errCode;
-
-  @SerializedName("errmsg")
-  private String errMsg;
-
-  private Integer count;
-
-  private Integer total;
-
-  @SerializedName("next_spnum")
-  private Long nextSpNum;
-
-  private WxCpApprovalData[] data;
-
-
-  @Data
-  public static class WxCpApprovalData implements Serializable {
-    private static final long serialVersionUID = -3051785319608491640L;
-    @SerializedName("spname")
-    private String spName;
-
-    @SerializedName("apply_name")
-    private String applyName;
-
-    @SerializedName("apply_org")
-    private String applyOrg;
-
-    @SerializedName("approval_name")
-    private String[] approvalName;
-
-    @SerializedName("notify_name")
-    private String[] notifyName;
-
-    @SerializedName("sp_status")
-    private Integer spStatus;
-
-    @SerializedName("sp_num")
-    private Long spNum;
-
-    @SerializedName("apply_time")
-    private Long applyTime;
-
-    @SerializedName("apply_user_id")
-    private String applyUserId;
-
-    @SerializedName("comm")
-    private Map comm;
-  }
-}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpBaseResp.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpBaseResp.java
new file mode 100644
index 0000000000..a895c38a8f
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpBaseResp.java
@@ -0,0 +1,61 @@
+package me.chanjar.weixin.cp.bean;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Getter;
+import lombok.Setter;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.io.Serializable;
+
+/**
+ * 返回结果
+ *
+ * @author yqx, WangWong
+ * @since 2020/3/16
+ */
+@Getter
+@Setter
+public class WxCpBaseResp implements Serializable {
+  private static final long serialVersionUID = -4301684507150486556L;
+
+  /**
+   * The Errcode.
+   */
+  @SerializedName("errcode")
+  protected Long errcode;
+
+  /**
+   * The Errmsg.
+   */
+  @SerializedName("errmsg")
+  protected String errmsg;
+
+  /**
+   * Success boolean.
+   *
+   * @return the boolean
+   */
+  public boolean success() {
+    return getErrcode() == 0;
+  }
+
+  /**
+   * From json wx cp base resp.
+   *
+   * @param json the json
+   * @return the wx cp base resp
+   */
+  public static WxCpBaseResp fromJson(String json) {
+    return WxCpGsonBuilder.create().fromJson(json, WxCpBaseResp.class);
+  }
+
+  /**
+   * To json string.
+   *
+   * @return the string
+   */
+  public String toJson() {
+    return WxCpGsonBuilder.create().toJson(this);
+  }
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpChat.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpChat.java
index 1f593e4746..eb014c595a 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpChat.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpChat.java
@@ -1,20 +1,22 @@
-package me.chanjar.weixin.cp.bean;
-
-import java.util.List;
-
-import lombok.Data;
-
-/**
- * 群聊
- *
- * @author gaigeshen
- */
-@Data
-public class WxCpChat {
-  
-  private String id;
-  private String name;
-  private String owner;
-  private List users;
-
-}
+package me.chanjar.weixin.cp.bean;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 群聊
+ *
+ * @author gaigeshen
+ */
+@Data
+public class WxCpChat implements Serializable {
+  private static final long serialVersionUID = -4301684507150486556L;
+
+  private String id;
+  private String name;
+  private String owner;
+  private List users;
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpCheckinData.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpCheckinData.java
deleted file mode 100644
index d2fbf0f9d7..0000000000
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpCheckinData.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package me.chanjar.weixin.cp.bean;
-
-import com.google.gson.annotations.SerializedName;
-import lombok.Data;
-
-import java.io.Serializable;
-import java.util.List;
-
-/**
- * 企业微信打卡数据.
- *
- * @author Element
- * @date 2019-04-06 11:01
- */
-@Data
-public class WxCpCheckinData implements Serializable {
-  private static final long serialVersionUID = 1915820330847799605L;
-
-  @SerializedName("userid")
-  private String userId;
-
-  @SerializedName("groupname")
-  private String groupName;
-
-  @SerializedName("checkin_type")
-  private String checkinType;
-
-  @SerializedName("exception_type")
-  private String exceptionType;
-
-  @SerializedName("checkin_time")
-  private Long checkinTime;
-
-  @SerializedName("location_title")
-  private String locationTitle;
-
-  @SerializedName("location_detail")
-  private String locationDetail;
-
-  @SerializedName("wifiname")
-  private String wifiName;
-
-  @SerializedName("wifimac")
-  private String wifiMac;
-
-  private String notes;
-
-  @SerializedName("mediaids")
-  private List mediaIds;
-}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpCheckinOption.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpCheckinOption.java
deleted file mode 100644
index c554e3d706..0000000000
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpCheckinOption.java
+++ /dev/null
@@ -1,145 +0,0 @@
-package me.chanjar.weixin.cp.bean;
-
-import com.google.gson.annotations.SerializedName;
-import lombok.Data;
-
-import java.io.Serializable;
-import java.util.List;
-
-/**
- * 企业微信打卡规则.
- *
- * @author Element
- * @date 2019-04-06 13:22
- */
-@Data
-public class WxCpCheckinOption implements Serializable {
-  private static final long serialVersionUID = -1964233697990417482L;
-
-  @SerializedName("userid")
-  private String userId;
-
-  private Group group;
-
-  @Data
-  public static class CheckinDate implements Serializable {
-    private static final long serialVersionUID = -5601722383347110974L;
-
-    private List workdays;
-
-    @SerializedName("checkintime")
-    private CheckinTime[] checkinTime;
-
-    @SerializedName("flex_time")
-    private Long flexTime;
-
-    @SerializedName("noneed_offwork")
-    private Boolean noNeedOffwork;
-
-    @SerializedName("limit_aheadtime")
-    private Long limitAheadTime;
-  }
-
-  @Data
-  public static class CheckinTime implements Serializable {
-    private static final long serialVersionUID = -8579954143265336276L;
-
-    @SerializedName("work_sec")
-    private Long workSec;
-
-    @SerializedName("off_work_sec")
-    private Long offWorkSec;
-
-    @SerializedName("remind_work_sec")
-    private Long remindWorkSec;
-
-    @SerializedName("remind_off_work_sec")
-    private Long remindOffWorkSec;
-  }
-
-  @Data
-  public static class Group implements Serializable {
-
-    private static final long serialVersionUID = -5888406969613403044L;
-
-    @SerializedName("groupid")
-    private Long id;
-
-    @SerializedName("groupname")
-    private String name;
-
-    @SerializedName("grouptype")
-    private Integer type;
-
-    @SerializedName("checkindate")
-    private List checkinDate;
-
-    @SerializedName("spe_workdays")
-    private List speWorkdays;
-
-    @SerializedName("spe_offdays")
-    private List speOffdays;
-
-    @SerializedName("sync_holidays")
-    private Boolean syncHolidays;
-
-    @SerializedName("need_photo")
-    private Boolean needPhoto;
-
-    @SerializedName("note_can_use_local_pic")
-    private Boolean noteCanUseLocalPic;
-
-    @SerializedName("allow_checkin_offworkday")
-    private Boolean allowCheckinOffWorkday;
-
-    @SerializedName("allow_apply_offworkday")
-    private Boolean allowApplyOffWorkday;
-
-    @SerializedName("wifimac_infos")
-    private List wifiMacInfos;
-
-    @SerializedName("loc_infos")
-    private List locInfos;
-
-  }
-
-  @Data
-  public static class WifiMacInfo implements Serializable {
-    private static final long serialVersionUID = -4657809185716627368L;
-
-    @SerializedName("wifiname")
-    private String name;
-
-    @SerializedName("wifimac")
-    private String mac;
-  }
-
-  @Data
-  public static class LocInfo implements Serializable {
-    private static final long serialVersionUID = -618965280668099608L;
-
-    private Long lat;
-    private Long lng;
-
-    @SerializedName("loc_title")
-    private String title;
-
-    @SerializedName("loc_detail")
-    private String detail;
-
-    private Long distance;
-  }
-
-  @Data
-  public static class SpeDay implements Serializable {
-    private static final long serialVersionUID = -3538818921359212748L;
-
-    private Long timestamp;
-    private String notes;
-
-    @SerializedName("checkintime")
-    private List checkinTime;
-
-  }
-
-}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpDepart.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpDepart.java
index dc71e027e3..bc54e7e806 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpDepart.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpDepart.java
@@ -1,28 +1,41 @@
 package me.chanjar.weixin.cp.bean;
 
-import java.io.Serializable;
-
 import lombok.Data;
 import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
 
+import java.io.Serializable;
+
 /**
- * 微信部门.
+ * 企业微信的部门.
  *
  * @author Daniel Qian
  */
 @Data
 public class WxCpDepart implements Serializable {
-
   private static final long serialVersionUID = -5028321625140879571L;
+
   private Long id;
   private String name;
+  private String enName;
+  private String[] departmentLeader;
   private Long parentId;
   private Long order;
 
+  /**
+   * From json wx cp depart.
+   *
+   * @param json the json
+   * @return the wx cp depart
+   */
   public static WxCpDepart fromJson(String json) {
     return WxCpGsonBuilder.create().fromJson(json, WxCpDepart.class);
   }
 
+  /**
+   * To json string.
+   *
+   * @return the string
+   */
   public String toJson() {
     return WxCpGsonBuilder.create().toJson(this);
   }
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpInviteResult.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpInviteResult.java
index ccf6fc94db..3cbeb7ce7b 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpInviteResult.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpInviteResult.java
@@ -1,16 +1,11 @@
 package me.chanjar.weixin.cp.bean;
 
-import java.io.Serializable;
-import java.util.Collections;
-import java.util.List;
-
-import org.apache.commons.lang3.StringUtils;
-
-import com.google.common.base.Splitter;
 import com.google.gson.annotations.SerializedName;
 import lombok.Data;
 import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
 
+import java.io.Serializable;
+
 /**
  * 邀请成员的结果对象类.
  * Created by Binary Wang on 2018-5-13.
@@ -26,6 +21,12 @@ public String toString() {
     return WxCpGsonBuilder.create().toJson(this);
   }
 
+  /**
+   * From json wx cp invite result.
+   *
+   * @param json the json
+   * @return the wx cp invite result
+   */
   public static WxCpInviteResult fromJson(String json) {
     return WxCpGsonBuilder.create().fromJson(json, WxCpInviteResult.class);
   }
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpMaJsCode2SessionResult.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpMaJsCode2SessionResult.java
index 90f1ae840c..f4de0b988a 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpMaJsCode2SessionResult.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpMaJsCode2SessionResult.java
@@ -11,6 +11,7 @@
  * 小程序登录凭证校验
  * 文档地址:https://work.weixin.qq.com/api/doc#90000/90136/90289/wx.qy.login
  * 
+ * * @author Binary Wang */ @Data @@ -26,6 +27,12 @@ public class WxCpMaJsCode2SessionResult implements Serializable { @SerializedName("corpid") private String corpId; + /** + * From json wx cp ma js code 2 session result. + * + * @param json the json + * @return the wx cp ma js code 2 session result + */ public static WxCpMaJsCode2SessionResult fromJson(String json) { return WxCpGsonBuilder.create().fromJson(json, WxCpMaJsCode2SessionResult.class); } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpMessage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpMessage.java deleted file mode 100644 index b39c2229e0..0000000000 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpMessage.java +++ /dev/null @@ -1,333 +0,0 @@ -package me.chanjar.weixin.cp.bean; - -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import lombok.Data; -import me.chanjar.weixin.common.api.WxConsts.KefuMsgType; -import me.chanjar.weixin.cp.bean.article.MpnewsArticle; -import me.chanjar.weixin.cp.bean.article.NewArticle; -import me.chanjar.weixin.cp.bean.messagebuilder.*; -import me.chanjar.weixin.cp.bean.taskcard.TaskCardButton; -import org.apache.commons.lang3.StringUtils; - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -import static me.chanjar.weixin.common.api.WxConsts.KefuMsgType.*; - -/** - * 消息. - * - * @author Daniel Qian - */ -@Data -public class WxCpMessage implements Serializable { - private static final long serialVersionUID = -2082278303476631708L; - - private String toUser; - private String toParty; - private String toTag; - private Integer agentId; - private String msgType; - private String content; - private String mediaId; - private String thumbMediaId; - private String title; - private String description; - private String musicUrl; - private String hqMusicUrl; - private String safe; - private String url; - private String btnTxt; - private List articles = new ArrayList<>(); - private List mpnewsArticles = new ArrayList<>(); - private String appId; - private String page; - private Boolean emphasisFirstItem; - private Map contentItems; - - /** - * 任务卡片特有的属性. - */ - private String taskId; - private List taskButtons = new ArrayList<>(); - - /** - * 获得文本消息builder. - */ - public static TextBuilder TEXT() { - return new TextBuilder(); - } - - /** - * 获得文本卡片消息builder. - */ - public static TextCardBuilder TEXTCARD() { - return new TextCardBuilder(); - } - - /** - * 获得图片消息builder. - */ - public static ImageBuilder IMAGE() { - return new ImageBuilder(); - } - - /** - * 获得语音消息builder. - */ - public static VoiceBuilder VOICE() { - return new VoiceBuilder(); - } - - /** - * 获得视频消息builder. - */ - public static VideoBuilder VIDEO() { - return new VideoBuilder(); - } - - /** - * 获得图文消息builder. - */ - public static NewsBuilder NEWS() { - return new NewsBuilder(); - } - - /** - * 获得mpnews图文消息builder. - */ - public static MpnewsBuilder MPNEWS() { - return new MpnewsBuilder(); - } - - /** - * 获得markdown消息builder. - */ - public static MarkdownMsgBuilder MARKDOWN() { - return new MarkdownMsgBuilder(); - } - - /** - * 获得文件消息builder. - */ - public static FileBuilder FILE() { - return new FileBuilder(); - } - - /** - * 获得任务卡片消息builder. - */ - public static TaskCardBuilder TASKCARD() { - return new TaskCardBuilder(); - } - - /** - * 获得小程序通知消息builder. - */ - public static MiniProgramNoticeMsgBuilder newMiniProgramNoticeBuilder() { - return new MiniProgramNoticeMsgBuilder(); - } - - /** - *
-   * 请使用.
-   * {@link KefuMsgType#TEXT}
-   * {@link KefuMsgType#IMAGE}
-   * {@link KefuMsgType#VOICE}
-   * {@link KefuMsgType#MUSIC}
-   * {@link KefuMsgType#VIDEO}
-   * {@link KefuMsgType#NEWS}
-   * {@link KefuMsgType#MPNEWS}
-   * {@link KefuMsgType#MARKDOWN}
-   * {@link KefuMsgType#TASKCARD}
-   * {@link KefuMsgType#MINIPROGRAM_NOTICE}
-   * 
- * - * @param msgType 消息类型 - */ - public void setMsgType(String msgType) { - this.msgType = msgType; - } - - public String toJson() { - JsonObject messageJson = new JsonObject(); - if (this.getAgentId() != null) { - messageJson.addProperty("agentid", this.getAgentId()); - } - - if (StringUtils.isNotBlank(this.getToUser())) { - messageJson.addProperty("touser", this.getToUser()); - } - - messageJson.addProperty("msgtype", this.getMsgType()); - - if (StringUtils.isNotBlank(this.getToParty())) { - messageJson.addProperty("toparty", this.getToParty()); - } - - if (StringUtils.isNotBlank(this.getToTag())) { - messageJson.addProperty("totag", this.getToTag()); - } - - this.handleMsgType(messageJson); - - if (StringUtils.isNotBlank(this.getSafe())) { - messageJson.addProperty("safe", this.getSafe()); - } - - return messageJson.toString(); - } - - private void handleMsgType(JsonObject messageJson) { - switch (this.getMsgType()) { - case TEXT: { - JsonObject text = new JsonObject(); - text.addProperty("content", this.getContent()); - messageJson.add("text", text); - break; - } - case MARKDOWN: { - JsonObject text = new JsonObject(); - text.addProperty("content", this.getContent()); - messageJson.add("markdown", text); - break; - } - case TEXTCARD: { - JsonObject text = new JsonObject(); - text.addProperty("title", this.getTitle()); - text.addProperty("description", this.getDescription()); - text.addProperty("url", this.getUrl()); - text.addProperty("btntxt", this.getBtnTxt()); - messageJson.add("textcard", text); - break; - } - case IMAGE: { - JsonObject image = new JsonObject(); - image.addProperty("media_id", this.getMediaId()); - messageJson.add("image", image); - break; - } - case FILE: { - JsonObject image = new JsonObject(); - image.addProperty("media_id", this.getMediaId()); - messageJson.add("file", image); - break; - } - case VOICE: { - JsonObject voice = new JsonObject(); - voice.addProperty("media_id", this.getMediaId()); - messageJson.add("voice", voice); - break; - } - case VIDEO: { - JsonObject video = new JsonObject(); - video.addProperty("media_id", this.getMediaId()); - video.addProperty("thumb_media_id", this.getThumbMediaId()); - video.addProperty("title", this.getTitle()); - video.addProperty("description", this.getDescription()); - messageJson.add("video", video); - break; - } - case NEWS: { - JsonObject newsJsonObject = new JsonObject(); - JsonArray articleJsonArray = new JsonArray(); - for (NewArticle article : this.getArticles()) { - JsonObject articleJson = new JsonObject(); - articleJson.addProperty("title", article.getTitle()); - articleJson.addProperty("description", article.getDescription()); - articleJson.addProperty("url", article.getUrl()); - articleJson.addProperty("picurl", article.getPicUrl()); - articleJsonArray.add(articleJson); - } - newsJsonObject.add("articles", articleJsonArray); - messageJson.add("news", newsJsonObject); - break; - } - case MPNEWS: { - JsonObject newsJsonObject = new JsonObject(); - if (this.getMediaId() != null) { - newsJsonObject.addProperty("media_id", this.getMediaId()); - } else { - JsonArray articleJsonArray = new JsonArray(); - for (MpnewsArticle article : this.getMpnewsArticles()) { - JsonObject articleJson = new JsonObject(); - articleJson.addProperty("title", article.getTitle()); - articleJson.addProperty("thumb_media_id", article.getThumbMediaId()); - articleJson.addProperty("author", article.getAuthor()); - articleJson.addProperty("content_source_url", article.getContentSourceUrl()); - articleJson.addProperty("content", article.getContent()); - articleJson.addProperty("digest", article.getDigest()); - articleJson.addProperty("show_cover_pic", article.getShowCoverPic()); - articleJsonArray.add(articleJson); - } - - newsJsonObject.add("articles", articleJsonArray); - } - messageJson.add("mpnews", newsJsonObject); - break; - } - case TASKCARD: { - JsonObject text = new JsonObject(); - text.addProperty("title", this.getTitle()); - text.addProperty("description", this.getDescription()); - - if (StringUtils.isNotBlank(this.getUrl())) { - text.addProperty("url", this.getUrl()); - } - - text.addProperty("task_id", this.getTaskId()); - - JsonArray buttonJsonArray = new JsonArray(); - for (TaskCardButton button : this.getTaskButtons()) { - JsonObject buttonJson = new JsonObject(); - buttonJson.addProperty("key", button.getKey()); - buttonJson.addProperty("name", button.getName()); - - if (StringUtils.isNotBlank(button.getReplaceName())) { - buttonJson.addProperty("replace_name", button.getReplaceName()); - } - - if (StringUtils.isNotBlank(button.getColor())) { - buttonJson.addProperty("color", button.getColor()); - } - - if (button.getBold() != null) { - buttonJson.addProperty("is_bold", button.getBold()); - } - - buttonJsonArray.add(buttonJson); - } - text.add("btn", buttonJsonArray); - - messageJson.add("taskcard", text); - break; - } - case MINIPROGRAM_NOTICE: { - JsonObject notice = new JsonObject(); - notice.addProperty("appid", this.getAppId()); - notice.addProperty("page", this.getPage()); - notice.addProperty("description", this.getDescription()); - notice.addProperty("title", this.getTitle()); - notice.addProperty("emphasis_first_item", this.getEmphasisFirstItem()); - JsonArray content = new JsonArray(); - for (Map.Entry item : this.getContentItems().entrySet()) { - JsonObject articleJson = new JsonObject(); - articleJson.addProperty("key", item.getKey()); - articleJson.addProperty("value", item.getValue()); - content.add(articleJson); - } - notice.add("content_item", content); - - messageJson.add("miniprogram_notice", notice); - break; - } - default: { - // do nothing - } - } - } - -} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpMessageSendResult.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpMessageSendResult.java deleted file mode 100644 index d850adebcc..0000000000 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpMessageSendResult.java +++ /dev/null @@ -1,68 +0,0 @@ -package me.chanjar.weixin.cp.bean; - -import java.io.Serializable; -import java.util.Collections; -import java.util.List; - -import org.apache.commons.lang3.StringUtils; - -import com.google.common.base.Splitter; -import com.google.gson.annotations.SerializedName; -import lombok.Data; -import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; - -/** - * 消息发送结果对象类. - * Created by Binary Wang on 2017-6-22. - * - * @author Binary Wang - */ -@Data -public class WxCpMessageSendResult implements Serializable { - private static final long serialVersionUID = 916455987193190004L; - - @Override - public String toString() { - return WxCpGsonBuilder.create().toJson(this); - } - - public static WxCpMessageSendResult fromJson(String json) { - return WxCpGsonBuilder.create().fromJson(json, WxCpMessageSendResult.class); - } - - @SerializedName("errcode") - private Integer errCode; - - @SerializedName("errmsg") - private String errMsg; - - @SerializedName("invaliduser") - private String invalidUser; - - @SerializedName("invalidparty") - private String invalidParty; - - @SerializedName("invalidtag") - private String invalidTag; - - - public List getInvalidUserList() { - return this.content2List(this.invalidUser); - } - - private List content2List(String content) { - if (StringUtils.isBlank(content)) { - return Collections.emptyList(); - } - - return Splitter.on("|").splitToList(content); - } - - public List getInvalidPartyList() { - return this.content2List(this.invalidParty); - } - - public List getInvalidTagList() { - return this.content2List(this.invalidTag); - } -} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpOauth2UserInfo.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpOauth2UserInfo.java index 9122f18d3a..433e54a680 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpOauth2UserInfo.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpOauth2UserInfo.java @@ -6,11 +6,15 @@ import lombok.NoArgsConstructor; import lombok.experimental.Accessors; +import java.io.Serializable; + /** *
  *  用oauth2获取用户信息的结果类
  *  Created by BinaryWang on 2019/5/26.
  * 
+ *

+ * 文档1:https://developer.work.weixin.qq.com/document/path/91707 * * @author Binary Wang */ @@ -19,10 +23,16 @@ @NoArgsConstructor @AllArgsConstructor @Builder -public class WxCpOauth2UserInfo { +public class WxCpOauth2UserInfo implements Serializable { + private static final long serialVersionUID = -4301684507150486556L; + private String openId; private String deviceId; private String userId; private String userTicket; private String expiresIn; + private String externalUserId; + private String parentUserId; + private String studentUserId; + } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpOpenUseridToUserid.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpOpenUseridToUserid.java new file mode 100644 index 0000000000..ec4d276e0a --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpOpenUseridToUserid.java @@ -0,0 +1,40 @@ +package me.chanjar.weixin.cp.bean; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.io.Serializable; + +/** + * userid转换 + * 将代开发应用或第三方应用获取的密文open_userid转换为明文userid + * 中间对象 + * @author yiyingcanfeng + */ +@Data +public class WxCpOpenUseridToUserid implements Serializable { + private static final long serialVersionUID = 1714909184316350423L; + + @Override + public String toString() { + return WxCpGsonBuilder.create().toJson(this); + } + + /** + * From json wx cp open userid to userid result. + * + * @param json the json + * @return the wx cp open userid to userid result. + */ + public static WxCpOpenUseridToUserid fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpOpenUseridToUserid.class); + } + + @SerializedName("userid") + private String userid; + + @SerializedName("open_userid") + private String openUserid; + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpOpenUseridToUseridResult.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpOpenUseridToUseridResult.java new file mode 100644 index 0000000000..122c3a0dc6 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpOpenUseridToUseridResult.java @@ -0,0 +1,47 @@ +package me.chanjar.weixin.cp.bean; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.io.Serializable; +import java.util.List; + +/** + * userid转换 + * 将代开发应用或第三方应用获取的密文open_userid转换为明文userid + * @author yiyingcanfeng + */ +@Data +public class WxCpOpenUseridToUseridResult implements Serializable { + private static final long serialVersionUID = 5179329535139861515L; + + @Override + public String toString() { + return WxCpGsonBuilder.create().toJson(this); + } + + /** + * From json wx cp open userid to userid result. + * + * @param json the json + * @return the wx cp open userid to userid result + */ + public static WxCpOpenUseridToUseridResult fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpOpenUseridToUseridResult.class); + } + + @SerializedName("errcode") + private Integer errCode; + + @SerializedName("errmsg") + private String errMsg; + + @SerializedName("userid_list") + private List useridList; + + @SerializedName("invalid_open_userid_list") + private List invalidOpenUseridList; + + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpProviderToken.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpProviderToken.java index 2c98f8e3fd..872b96d93f 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpProviderToken.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpProviderToken.java @@ -4,14 +4,17 @@ import lombok.Data; import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; +import java.io.Serializable; + /** * 服务商凭证. * - * @author Binary Wang - * @date 2019-11-02 + * @author Binary Wang created on 2019-11-02 */ @Data -public class WxCpProviderToken { +public class WxCpProviderToken implements Serializable { + private static final long serialVersionUID = -4301684507150486556L; + /** * 服务商的access_token,最长为512字节。 */ @@ -24,6 +27,12 @@ public class WxCpProviderToken { @SerializedName("expires_in") private Integer expiresIn; + /** + * From json wx cp provider token. + * + * @param json the json + * @return the wx cp provider token + */ public static WxCpProviderToken fromJson(String json) { return WxCpGsonBuilder.create().fromJson(json, WxCpProviderToken.class); } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTag.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTag.java index f6b9fa0276..33d3d07b29 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTag.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTag.java @@ -1,14 +1,15 @@ package me.chanjar.weixin.cp.bean; -import java.io.Serializable; - import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; +import java.io.Serializable; + /** * Created by Daniel Qian. + * * @author Daniel Qian */ @Data @@ -22,10 +23,21 @@ public class WxCpTag implements Serializable { private String name; + /** + * From json wx cp tag. + * + * @param json the json + * @return the wx cp tag + */ public static WxCpTag fromJson(String json) { return WxCpGsonBuilder.create().fromJson(json, WxCpTag.class); } + /** + * To json string. + * + * @return the string + */ public String toJson() { return WxCpGsonBuilder.create().toJson(this); } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTagAddOrRemoveUsersResult.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTagAddOrRemoveUsersResult.java index 037740ca96..c590c6c0e8 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTagAddOrRemoveUsersResult.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTagAddOrRemoveUsersResult.java @@ -1,15 +1,14 @@ package me.chanjar.weixin.cp.bean; -import java.io.Serializable; -import java.util.Collections; -import java.util.List; - -import org.apache.commons.lang3.StringUtils; - import com.google.common.base.Splitter; import com.google.gson.annotations.SerializedName; import lombok.Data; import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; +import org.apache.commons.lang3.StringUtils; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; /** * 为标签添加或移除用户结果对象类. @@ -26,6 +25,12 @@ public String toString() { return WxCpGsonBuilder.create().toJson(this); } + /** + * From json wx cp tag add or remove users result. + * + * @param json the json + * @return the wx cp tag add or remove users result + */ public static WxCpTagAddOrRemoveUsersResult fromJson(String json) { return WxCpGsonBuilder.create().fromJson(json, WxCpTagAddOrRemoveUsersResult.class); } @@ -42,6 +47,11 @@ public static WxCpTagAddOrRemoveUsersResult fromJson(String json) { @SerializedName("invalidparty") private String[] invalidParty; + /** + * Gets invalid user list. + * + * @return the invalid user list + */ public List getInvalidUserList() { return this.content2List(this.invalidUsers); } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTagGetResult.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTagGetResult.java index 244419b062..3dc34ab654 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTagGetResult.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTagGetResult.java @@ -46,10 +46,21 @@ public class WxCpTagGetResult implements Serializable { @SerializedName("tagname") private String tagname; + /** + * From json wx cp tag get result. + * + * @param json the json + * @return the wx cp tag get result + */ public static WxCpTagGetResult fromJson(String json) { return WxCpGsonBuilder.create().fromJson(json, WxCpTagGetResult.class); } + /** + * To json string. + * + * @return the string + */ public String toJson() { return WxCpGsonBuilder.create().toJson(this); } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTaskCardUpdateResult.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTaskCardUpdateResult.java index c86b255b44..d4cee5549c 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTaskCardUpdateResult.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTaskCardUpdateResult.java @@ -16,8 +16,7 @@ * Created by Jeff on 2019-05-16. *

* - * @author Jeff - * @date 2019-05-16 + * @author Jeff created on 2019-05-16 */ @Data @AllArgsConstructor @@ -36,6 +35,12 @@ public class WxCpTaskCardUpdateResult implements Serializable { @SerializedName("invaliduser") private List invalidUsers; + /** + * From json wx cp task card update result. + * + * @param json the json + * @return the wx cp task card update result + */ public static WxCpTaskCardUpdateResult fromJson(String json) { return WxCpGsonBuilder.create().fromJson(json, WxCpTaskCardUpdateResult.class); } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpAdmin.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpAdmin.java new file mode 100644 index 0000000000..776726de80 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpAdmin.java @@ -0,0 +1,62 @@ +package me.chanjar.weixin.cp.bean; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import me.chanjar.weixin.common.util.json.WxGsonBuilder; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.util.List; + +/** + * 应用的管理员 + * + * @author huangxiaoming + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class WxCpTpAdmin extends WxCpBaseResp { + private static final long serialVersionUID = -5028321625140879571L; + + @SerializedName("admin") + private List admin; + + /** + * The type Admin. + */ + @Getter + @Setter + public static class Admin extends WxCpBaseResp { + private static final long serialVersionUID = -5028321625140879571L; + + @SerializedName("userid") + private String userId; + + @SerializedName("open_userid") + private String openUserId; + + @SerializedName("auth_type") + private Integer authType; + + public String toJson() { + return WxGsonBuilder.create().toJson(this); + } + } + + /** + * From json wx cp tp admin. + * + * @param json the json + * @return the wx cp tp admin + */ + public static WxCpTpAdmin fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpAdmin.class); + } + + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpAppQrcode.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpAppQrcode.java new file mode 100644 index 0000000000..ada85c760c --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpAppQrcode.java @@ -0,0 +1,35 @@ +package me.chanjar.weixin.cp.bean; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +/** + * 应用的管理员 + * + * @author huangxiaoming + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class WxCpTpAppQrcode extends WxCpBaseResp { + private static final long serialVersionUID = -5028321625140879571L; + + @SerializedName("qrcode") + private String qrcode; + + /** + * From json wx cp tp admin. + * + * @param json the json + * @return the wx cp tp admin + */ + public static WxCpTpAppQrcode fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpAppQrcode.class); + } + + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpAuthInfo.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpAuthInfo.java new file mode 100644 index 0000000000..9919fd72b8 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpAuthInfo.java @@ -0,0 +1,324 @@ +package me.chanjar.weixin.cp.bean; + +import com.google.gson.annotations.SerializedName; +import lombok.Getter; +import lombok.Setter; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.io.Serializable; +import java.util.List; + +/** + * 服务商模式获取授权信息 + * + * @author yuanqixun + */ +@Getter +@Setter +public class WxCpTpAuthInfo extends WxCpBaseResp { + private static final long serialVersionUID = -5028321625140879571L; + + /** + * 服务商信息 + */ + @SerializedName("dealer_corp_info") + private DealerCorpInfo dealerCorpInfo; + + /** + * 授权企业信息 + */ + @SerializedName("auth_corp_info") + private AuthCorpInfo authCorpInfo; + + /** + * 授权信息。如果是通讯录应用,且没开启实体应用,是没有该项的。通讯录应用拥有企业通讯录的全部信息读写权限 + */ + @SerializedName("auth_info") + private AuthInfo authInfo; + + + /** + * 企业当前生效的版本信息 + */ + @SerializedName("edition_info") + private EditionInfo editionInfo; + + /** + * The type Dealer corp info. + */ + @Getter + @Setter + public static class DealerCorpInfo extends WxCpBaseResp { + private static final long serialVersionUID = -5028321625140879571L; + + @SerializedName("corpid") + private String corpId; + + @SerializedName("corp_name") + private String corpName; + } + + /** + * The type Auth corp info. + */ + @Getter + @Setter + public static class AuthCorpInfo implements Serializable { + private static final long serialVersionUID = -5028321625140879571L; + + @SerializedName("corpid") + private String corpId; + + @SerializedName("corp_name") + private String corpName; + + @SerializedName("corp_type") + private String corpType; + + @SerializedName("corp_square_logo_url") + private String corpSquareLogoUrl; + + @SerializedName("corp_round_logo_url") + private String corpRoundLogoUrl; + + @SerializedName("corp_user_max") + private String corpUserMax; + + @SerializedName("corp_agent_max") + private String corpAgentMax; + + /** + * 所绑定的企业微信主体名称(仅认证过的企业有) + */ + @SerializedName("corp_full_name") + private String corpFullName; + + /** + * 认证到期时间 + */ + @SerializedName("verified_end_time") + private Long verifiedEndTime; + + /** + * 企业类型,1. 企业; 2. 政府以及事业单位; 3. 其他组织, 4.团队号 + */ + @SerializedName("subject_type") + private Integer subjectType; + + /** + * 授权企业在微工作台(原企业号)的二维码,可用于关注微工作台 + */ + @SerializedName("corp_wxqrcode") + private String corpWxQrcode; + + @SerializedName("corp_scale") + private String corpScale; + + @SerializedName("corp_industry") + private String corpIndustry; + + @SerializedName("corp_sub_industry") + private String corpSubIndustry; + + @SerializedName("location") + private String location; + + } + + /** + * 授权信息 + */ + @Getter + @Setter + public static class AuthInfo implements Serializable { + private static final long serialVersionUID = -5028321625140879571L; + + /** + * 授权的应用信息,注意是一个数组,但仅旧的多应用套件授权时会返回多个agent,对新的单应用授权,永远只返回一个agent + */ + @SerializedName("agent") + private List agents; + + } + + /** + * 企业当前生效的版本信息 + */ + @Getter + @Setter + public static class EditionInfo implements Serializable { + private static final long serialVersionUID = -5028321625140879571L; + + /** + * 授权的应用信息,注意是一个数组,但仅旧的多应用套件授权时会返回多个agent,对新的单应用授权,永远只返回一个agent + */ + @SerializedName("agent") + private List agents; + + } + + /** + * The type Agent. + */ + @Getter + @Setter + public static class Agent implements Serializable { + private static final long serialVersionUID = -5028321625140879571L; + + @SerializedName("agentid") + private Integer agentId; + + @SerializedName("name") + private String name; + + @SerializedName("round_logo_url") + private String roundLogoUrl; + + @SerializedName("square_logo_url") + private String squareLogoUrl; + + /** + * 旧的多应用套件中的对应应用id,新开发者请忽略 + */ + @SerializedName("appid") + @Deprecated + private String appid; + + /** + * 授权模式,0为管理员授权;1为成员授权 + */ + @SerializedName("auth_mode") + private Integer authMode; + + /** + * 是否为代开发自建应用 + */ + @SerializedName("is_customized_app") + private Boolean isCustomizedApp; + + /** + * 应用权限 + */ + @SerializedName("privilege") + private Privilege privilege; + + /** + * 版本id + */ + @SerializedName("edition_id") + private String editionId; + + /** + * 版本名称 + */ + @SerializedName("edition_name") + private String editionName; + + /** + * 付费状态 + *
+ *
    + *
  • 0-没有付费;
  • + *
  • 1-限时试用;
  • + *
  • 2-试用过期;
  • + *
  • 3-购买期内;
  • + *
  • 4-购买过期;
  • + *
  • 5-不限时试用;
  • + *
  • 6-购买期内,但是人数超标, 注意,超标后还可以用7天;
  • + *
  • 7-购买期内,但是人数超标, 且已经超标试用7天
  • + *
+ */ + @SerializedName("app_status") + private Integer appStatus; + + /** + * 用户上限。 + *

特别注意, 以下情况该字段无意义,可以忽略:

+ *
    + *
  • 1. 固定总价购买
  • + *
  • 2. app_status = 限时试用/试用过期/不限时试用
  • + *
  • 3. 在第2条“app_status=不限时试用”的情况下,如果该应用的配置为“小企业无使用限制”,user_limit有效,且为限制的人数
  • + *
+ */ + @SerializedName("user_limit") + private Long userLimit; + + /** + * 版本到期时间, 秒级时间戳, 根据需要自行乘以1000(根据购买版本,可能是试用到期时间或付费使用到期时间)。 + *

特别注意,以下情况该字段无意义,可以忽略:

+ *
    + *
  • 1. app_status = 不限时试用
  • + *
+ */ + @SerializedName("expired_time") + private Long expiredTime; + + /** + * 是否虚拟版本 + */ + @SerializedName("is_virtual_version") + private Boolean isVirtualVersion; + + /** + * 是否由互联企业分享安装。详见 企业互联 + */ + @SerializedName("is_shared_from_other_corp") + private Boolean isSharedFromOtherCorp; + } + + /** + * 应用对应的权限 + */ + @Getter + @Setter + public static class Privilege implements Serializable { + private static final long serialVersionUID = -5028321625140879571L; + + /** + * 权限等级。 + * 1:通讯录基本信息只读 + * 2:通讯录全部信息只读 + * 3:通讯录全部信息读写 + * 4:单个基本信息只读 + * 5:通讯录全部信息只写 + */ + @SerializedName("level") + private Integer level; + + @SerializedName("allow_party") + private List allowParties; + + @SerializedName("allow_user") + private List allowUsers; + + @SerializedName("allow_tag") + private List allowTags; + + @SerializedName("extra_party") + private List extraParties; + + @SerializedName("extra_user") + private List extraUsers; + + @SerializedName("extra_tag") + private List extraTags; + + } + + + /** + * From json wx cp tp auth info. + * + * @param json the json + * @return the wx cp tp auth info + */ + public static WxCpTpAuthInfo fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpAuthInfo.class); + } + + @Override + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpContactSearch.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpContactSearch.java new file mode 100644 index 0000000000..11c653a433 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpContactSearch.java @@ -0,0 +1,71 @@ +package me.chanjar.weixin.cp.bean; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.experimental.Accessors; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.io.Serializable; + +/** + * The type Wx cp tp contact search. + * + * @author uianz + * @since 2020 /12/23 下午 02:43 + */ +@Data +@Accessors(chain = true) +public class WxCpTpContactSearch implements Serializable { + private static final long serialVersionUID = -4301684507150486556L; + + /** + * 查询的企业corpid + */ + @SerializedName("auth_corpid") + private String authCorpId; + + /** + * 搜索关键词。当查询用户时应为用户名称、名称拼音或者英文名;当查询部门时应为部门名称或者部门名称拼音 + */ + @SerializedName("query_word") + private String queryWord; + + /** + * 查询类型 1:查询用户,返回用户userid列表 2:查询部门,返回部门id列表。 不填该字段或者填0代表同时查询部门跟用户 + */ + @SerializedName("query_type") + private Integer type; + + /** + * 应用id,若非0则只返回应用可见范围内的用户或者部门信息 + */ + @SerializedName("agentid") + private Integer agentId; + + /** + * 查询返回的最大数量,默认为50,最多为200,查询返回的数量可能小于limit指定的值 + */ + @SerializedName("limit") + private Integer limit; + + /** + * 如果需要精确匹配用户名称或者部门名称或者英文名,不填则默认为模糊匹配;1:匹配用户名称或者部门名称 2:匹配用户英文名 + */ + @SerializedName("full_match_field") + private Integer fullMatchField; + + /** + * 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 + */ + @SerializedName("cursor") + private String cursor; + + /** + * To json string. + * + * @return the string + */ + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpContactSearchResp.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpContactSearchResp.java new file mode 100644 index 0000000000..074b30bc0e --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpContactSearchResp.java @@ -0,0 +1,77 @@ +package me.chanjar.weixin.cp.bean; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.io.Serializable; +import java.util.List; + +/** + * The type Wx cp tp contact search resp. + * + * @author uianz + * @since 2020 /12/23 下午 02:55 + */ +@EqualsAndHashCode(callSuper = true) +@Data +public class WxCpTpContactSearchResp extends WxCpBaseResp { + + @SerializedName("is_last") + private Boolean isLast; + + @SerializedName("query_result") + private QueryResult queryResult; + + @SerializedName("next_cursor") + private String nextCursor; + + /** + * The type Query result. + */ + @Data + public static class QueryResult implements Serializable { + private static final long serialVersionUID = -4301684507150486556L; + + @SerializedName("user") + private User user; + @SerializedName("party") + private Party party; + + /** + * The type User. + */ + @Data + public static class User implements Serializable { + private static final long serialVersionUID = -4301684507150486556L; + @SerializedName("userid") + private List userid; + @SerializedName("open_userid") + private List openUserId; + } + + /** + * The type Party. + */ + @Data + public static class Party implements Serializable { + private static final long serialVersionUID = -4301684507150486556L; + + @SerializedName("department_id") + private List departmentId; + } + + } + + /** + * From json wx cp tp contact search resp. + * + * @param json the json + * @return the wx cp tp contact search resp + */ + public static WxCpTpContactSearchResp fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpContactSearchResp.class); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpConvertTmpExternalUserIdResult.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpConvertTmpExternalUserIdResult.java new file mode 100644 index 0000000000..9bca31c2d0 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpConvertTmpExternalUserIdResult.java @@ -0,0 +1,41 @@ +package me.chanjar.weixin.cp.bean; + +import com.google.gson.annotations.SerializedName; +import lombok.Getter; +import lombok.Setter; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.util.List; + +@Setter +@Getter +public class WxCpTpConvertTmpExternalUserIdResult extends WxCpBaseResp { + + + @SerializedName("invalid_tmp_external_userid_list") + private List results; + + @Getter + @Setter + public static class Results { + + @SerializedName("tmp_external_userid") + private String tmpExternalUserId; + + @SerializedName("external_userid") + private String externalUserId; + + @SerializedName("corpid") + private String corpId; + + @SerializedName("userid") + private String userId; + } + + @SerializedName("invalid_tmp_external_userid_list") + private List invalidTmpExternalUserIdList; + + public static WxCpTpConvertTmpExternalUserIdResult fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpConvertTmpExternalUserIdResult.class); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpCorp.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpCorp.java index 9ca59b843d..939a4eddf6 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpCorp.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpCorp.java @@ -1,42 +1,54 @@ -package me.chanjar.weixin.cp.bean; - -import java.io.Serializable; - -import com.google.gson.annotations.SerializedName; - -import lombok.Data; -import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; - -/** - * 微信部门. - * - * @author Daniel Qian - */ -@Data -public class WxCpTpCorp implements Serializable { - - private static final long serialVersionUID = -5028321625140879571L; - @SerializedName("corpid") - private String corpId; - @SerializedName("corp_name") - private String corpName; - @SerializedName("corp_full_name") - private String corpFullName; - @SerializedName("corp_type") - private String corpType; - @SerializedName("corp_square_logo_url") - private String corpSquareLogoUrl; - @SerializedName("corp_user_max") - private String corpUserMax; - @SerializedName("permanent_code") - private String permanentCode; - - public static WxCpTpCorp fromJson(String json) { - return WxCpGsonBuilder.create().fromJson(json, WxCpTpCorp.class); - } - - public String toJson() { - return WxCpGsonBuilder.create().toJson(this); - } - -} +package me.chanjar.weixin.cp.bean; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.io.Serializable; + +/** + * 微信部门. + * + * @author Daniel Qian + */ +@Data +public class WxCpTpCorp implements Serializable { + + private static final long serialVersionUID = -5028321625140879571L; + @SerializedName("corpid") + private String corpId; + @SerializedName("corp_name") + private String corpName; + @SerializedName("corp_full_name") + private String corpFullName; + @SerializedName("corp_type") + private String corpType; + @SerializedName("corp_square_logo_url") + private String corpSquareLogoUrl; + @SerializedName("corp_user_max") + private String corpUserMax; + @SerializedName("permanent_code") + private String permanentCode; + @SerializedName("auth_info") + private String authInfo; + + /** + * From json wx cp tp corp. + * + * @param json the json + * @return the wx cp tp corp + */ + public static WxCpTpCorp fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpCorp.class); + } + + /** + * To json string. + * + * @return the string + */ + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpCorpId2OpenCorpId.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpCorpId2OpenCorpId.java new file mode 100644 index 0000000000..73dfd49064 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpCorpId2OpenCorpId.java @@ -0,0 +1,35 @@ +package me.chanjar.weixin.cp.bean; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import lombok.EqualsAndHashCode; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +/** + * 应用的管理员 + * + * @author huangxiaoming + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class WxCpTpCorpId2OpenCorpId extends WxCpBaseResp { + private static final long serialVersionUID = -5028321625140879571L; + + @SerializedName("open_corpid") + private String openCorpId; + + /** + * From json wx cp tp admin. + * + * @param json the json + * @return the wx cp tp admin + */ + public static WxCpTpCorpId2OpenCorpId fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpCorpId2OpenCorpId.class); + } + + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpCustomizedAppDetail.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpCustomizedAppDetail.java new file mode 100644 index 0000000000..f9ca645b82 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpCustomizedAppDetail.java @@ -0,0 +1,221 @@ +package me.chanjar.weixin.cp.bean; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.io.Serializable; +import java.util.List; + +/** + * 代开发应用详情. + * + * @author Binary Wang + * created on 2026-01-14 + */ +@Data +public class WxCpTpCustomizedAppDetail extends WxCpBaseResp { + + /** + * 授权方企业id + */ + @SerializedName("auth_corpid") + private String authCorpId; + + /** + * 授权方企业名称 + */ + @SerializedName("auth_corp_name") + private String authCorpName; + + /** + * 授权方企业方形头像 + */ + @SerializedName("auth_corp_square_logo_url") + private String authCorpSquareLogoUrl; + + /** + * 授权方企业圆形头像 + */ + @SerializedName("auth_corp_round_logo_url") + private String authCorpRoundLogoUrl; + + /** + * 授权方企业类型,1. 企业; 2. 政府以及事业单位; 3. 其他组织, 4.团队小型企业(原企业微信认证版用户) + */ + @SerializedName("auth_corp_type") + private Integer authCorpType; + + /** + * 授权方企业在微工作台(原企业号)的二维码,可用于关注微工作台 + */ + @SerializedName("auth_corp_qrcode_url") + private String authCorpQrcodeUrl; + + /** + * 授权方企业用户规模 + */ + @SerializedName("auth_corp_user_limit") + private Integer authCorpUserLimit; + + /** + * 授权方企业的主体名称(仅认证或验证过的企业有),即企业全称 + */ + @SerializedName("auth_corp_full_name") + private String authCorpFullName; + + /** + * 企业类型,1. 已验证企业;2. 已认证企业 + */ + @SerializedName("auth_corp_verified_type") + private Integer authCorpVerifiedType; + + /** + * 授权方企业所属行业 + */ + @SerializedName("auth_corp_industry") + private String authCorpIndustry; + + /** + * 授权方企业所属子行业 + */ + @SerializedName("auth_corp_sub_industry") + private String authCorpSubIndustry; + + /** + * 授权方企业所在地址 + */ + @SerializedName("auth_corp_location") + private String authCorpLocation; + + /** + * 代开发自建应用详情 + */ + @SerializedName("customized_app_list") + private List customizedAppList; + + /** + * From json wx cp tp customized app detail. + * + * @param json the json + * @return the wx cp tp customized app detail + */ + public static WxCpTpCustomizedAppDetail fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpCustomizedAppDetail.class); + } + + @Override + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + + /** + * 代开发自建应用信息 + */ + @Data + public static class CustomizedApp implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 代开发自建应用的agentid + */ + @SerializedName("agentid") + private Integer agentId; + + /** + * 代开发自建应用对应的模板id + */ + @SerializedName("template_id") + private String templateId; + + /** + * 代开发自建应用的name + */ + @SerializedName("name") + private String name; + + /** + * 代开发自建应用的description + */ + @SerializedName("description") + private String description; + + /** + * 代开发自建应用的logo url + */ + @SerializedName("logo_url") + private String logoUrl; + + /** + * 代开发自建应用的可见范围 + */ + @SerializedName("allow_userinfos") + private AllowUserInfos allowUserInfos; + + /** + * 代开发自建应用是否被禁用 + */ + @SerializedName("close") + private Integer close; + + /** + * 代开发自建应用主页url + */ + @SerializedName("home_url") + private String homeUrl; + + /** + * 代开发自建应用的模式,0 = 代开发自建应用;1 = 第三方应用代开发 + */ + @SerializedName("app_type") + private Integer appType; + } + + /** + * 应用可见范围 + */ + @Data + public static class AllowUserInfos implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 应用可见范围(成员) + */ + @SerializedName("user") + private List users; + + /** + * 应用可见范围(部门) + */ + @SerializedName("department") + private List departments; + } + + /** + * 成员信息 + */ + @Data + public static class User implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 成员userid + */ + @SerializedName("userid") + private String userId; + } + + /** + * 部门信息 + */ + @Data + public static class Department implements Serializable { + private static final long serialVersionUID = 1L; + + /** + * 部门id + */ + @SerializedName("id") + private Integer id; + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpDepart.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpDepart.java new file mode 100644 index 0000000000..39d3601a2f --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpDepart.java @@ -0,0 +1,42 @@ +package me.chanjar.weixin.cp.bean; + +import lombok.Data; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.io.Serializable; + +/** + * 企业微信的部门. + * + * @author Daniel Qian + */ +@Data +public class WxCpTpDepart implements Serializable { + private static final long serialVersionUID = -5028321625140879571L; + + private Integer id; + private String name; + private String enName; + private Integer parentid; + private Integer order; + + /** + * From json wx cp tp depart. + * + * @param json the json + * @return the wx cp tp depart + */ + public static WxCpTpDepart fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpDepart.class); + } + + /** + * To json string. + * + * @return the string + */ + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpOpenKfIdConvertResult.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpOpenKfIdConvertResult.java new file mode 100644 index 0000000000..b6b0e9ef82 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpOpenKfIdConvertResult.java @@ -0,0 +1,46 @@ +package me.chanjar.weixin.cp.bean; + +import com.google.gson.annotations.SerializedName; +import lombok.Getter; +import lombok.Setter; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.util.List; + +@Setter +@Getter +public class WxCpTpOpenKfIdConvertResult extends WxCpBaseResp { + + /** + * 微信客服ID转换结果 + */ + @SerializedName("items") + private List items; + + /** + * 无法转换的微信客服ID列表 + */ + @SerializedName("invalid_open_kfid_list") + private List invalidOpenKfIdList; + + @Getter + @Setter + public static class Item { + + /*** + * 企业主体下的微信客服ID + */ + @SerializedName("open_kfid") + private String openKfId; + + /** + * 服务商主体下的微信客服ID,如果传入的open_kfid已经是服务商主体下的ID,则new_open_kfid与open_kfid相同。 + */ + @SerializedName("new_open_kfid") + private String newOpenKfId; + } + + public static WxCpTpOpenKfIdConvertResult fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpOpenKfIdConvertResult.class); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpPermanentCodeInfo.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpPermanentCodeInfo.java new file mode 100644 index 0000000000..5330194abe --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpPermanentCodeInfo.java @@ -0,0 +1,386 @@ +package me.chanjar.weixin.cp.bean; + +import com.google.gson.annotations.SerializedName; +import lombok.Getter; +import lombok.Setter; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.io.Serializable; +import java.util.List; + +/** + * 服务商模式获取永久授权码信息 + * + * @author yunaqixun + */ +@Getter +@Setter +public class WxCpTpPermanentCodeInfo extends WxCpBaseResp { + + private static final long serialVersionUID = -5028321625140879571L; + + @SerializedName("access_token") + private String accessToken; + + @SerializedName("expires_in") + private Long expiresIn; + + @SerializedName("permanent_code") + private String permanentCode; + + /** + * 授权企业信息 + */ + @SerializedName("auth_corp_info") + private AuthCorpInfo authCorpInfo; + + /** + * 授权信息。如果是通讯录应用,且没开启实体应用,是没有该项的。通讯录应用拥有企业通讯录的全部信息读写权限 + */ + @SerializedName("auth_info") + private AuthInfo authInfo; + + /** + * 授权用户信息 + */ + @SerializedName("auth_user_info") + private AuthUserInfo authUserInfo; + + /** + * 推广二维码安装相关信息 + */ + @SerializedName("register_code_info") + private RegisterCodeInfo registerCodeInfo; + + /** + * 企业当前生效的版本信息 + */ + @SerializedName("edition_info") + private EditionInfo editionInfo; + + /** + * 安装应用时,扫码或者授权链接中带的state值。详见state说明 + * state说明: + * 目前会返回state包含以下几个场景。 + * (1)扫带参二维码授权代开发模版。 + */ + @SerializedName("state") + private String state; + + /** + * The type Auth corp info. + */ + @Getter + @Setter + public static class AuthCorpInfo implements Serializable { + private static final long serialVersionUID = -5028321625140879571L; + + @SerializedName("corpid") + private String corpId; + + @SerializedName("corp_name") + private String corpName; + + @SerializedName("corp_type") + private String corpType; + + @SerializedName("corp_square_logo_url") + private String corpSquareLogoUrl; + + @SerializedName("corp_round_logo_url") + private String corpRoundLogoUrl; + + @SerializedName("corp_user_max") + private String corpUserMax; + + @SerializedName("corp_agent_max") + private String corpAgentMax; + + /** + * 所绑定的企业微信主体名称(仅认证过的企业有) + */ + @SerializedName("corp_full_name") + private String corpFullName; + + /** + * 认证到期时间 + */ + @SerializedName("verified_end_time") + private Long verifiedEndTime; + + /** + * 企业类型,1. 企业; 2. 政府以及事业单位; 3. 其他组织, 4.团队号 + */ + @SerializedName("subject_type") + private Integer subjectType; + + /** + * 授权企业在微工作台(原企业号)的二维码,可用于关注微工作台 + */ + @SerializedName("corp_wxqrcode") + private String corpWxQrcode; + + @SerializedName("corp_scale") + private String corpScale; + + @SerializedName("corp_industry") + private String corpIndustry; + + @SerializedName("corp_sub_industry") + private String corpSubIndustry; + + @SerializedName("location") + private String location; + + } + + /** + * 授权信息 + */ + @Getter + @Setter + public static class AuthInfo implements Serializable { + private static final long serialVersionUID = -5028321625140879571L; + + /** + * 授权的应用信息,注意是一个数组,但仅旧的多应用套件授权时会返回多个agent,对新的单应用授权,永远只返回一个agent + */ + @SerializedName("agent") + private List agents; + + } + + /** + * 企业当前生效的版本信息 + */ + @Getter + @Setter + public static class EditionInfo implements Serializable { + private static final long serialVersionUID = -5028321625140879571L; + + /** + * 授权的应用信息,注意是一个数组,但仅旧的多应用套件授权时会返回多个agent,对新的单应用授权,永远只返回一个agent + */ + @SerializedName("agent") + private List agents; + + } + + /** + * The type Agent. + */ + @Getter + @Setter + public static class Agent implements Serializable { + private static final long serialVersionUID = -5028321625140879571L; + + @SerializedName("agentid") + private Integer agentId; + + @SerializedName("name") + private String name; + + @SerializedName("round_logo_url") + private String roundLogoUrl; + + @SerializedName("square_logo_url") + private String squareLogoUrl; + + /** + * 旧的多应用套件中的对应应用id,新开发者请忽略 + */ + @SerializedName("appid") + @Deprecated + private String appid; + + /** + * 授权模式,0为管理员授权;1为成员授权 + */ + @SerializedName("auth_mode") + private Integer authMode; + + /** + * 是否为代开发自建应用 + */ + @SerializedName("is_customized_app") + private Boolean isCustomizedApp; + + /** + * 应用权限 + */ + @SerializedName("privilege") + private Privilege privilege; + + /** + * 版本id + */ + @SerializedName("edition_id") + private String editionId; + + /** + * 版本名称 + */ + @SerializedName("edition_name") + private String editionName; + + /** + * 付费状态 + *
+ *
    + *
  • 0-没有付费;
  • + *
  • 1-限时试用;
  • + *
  • 2-试用过期;
  • + *
  • 3-购买期内;
  • + *
  • 4-购买过期;
  • + *
  • 5-不限时试用;
  • + *
  • 6-购买期内,但是人数超标, 注意,超标后还可以用7天;
  • + *
  • 7-购买期内,但是人数超标, 且已经超标试用7天
  • + *
+ */ + @SerializedName("app_status") + private Integer appStatus; + + /** + * 用户上限。 + *

特别注意, 以下情况该字段无意义,可以忽略:

+ *
    + *
  • 1. 固定总价购买
  • + *
  • 2. app_status = 限时试用/试用过期/不限时试用
  • + *
  • 3. 在第2条“app_status=不限时试用”的情况下,如果该应用的配置为“小企业无使用限制”,user_limit有效,且为限制的人数
  • + *
+ */ + @SerializedName("user_limit") + private Long userLimit; + + /** + * 版本到期时间, 秒级时间戳, 根据需要自行乘以1000(根据购买版本,可能是试用到期时间或付费使用到期时间)。 + *

特别注意,以下情况该字段无意义,可以忽略:

+ *
    + *
  • 1. app_status = 不限时试用
  • + *
+ */ + @SerializedName("expired_time") + private Long expiredTime; + + /** + * 是否虚拟版本 + */ + @SerializedName("is_virtual_version") + private Boolean isVirtualVersion; + + /** + * 是否由互联企业分享安装。详见 企业互联 + */ + @SerializedName("is_shared_from_other_corp") + private Boolean isSharedFromOtherCorp; + + } + + /** + * 授权人员信息 + */ + @Getter + @Setter + public static class AuthUserInfo implements Serializable { + private static final long serialVersionUID = -5028321625140879571L; + + @SerializedName("userid") + private String userId; + + @SerializedName("name") + private String name; + + @SerializedName("avatar") + private String avatar; + + /** + * 授权管理员的open_userid,可能为空 + */ + @SerializedName("open_userid") + private String openUserid; + } + + /** + * 推广二维码安装相关信息 + */ + @Getter + @Setter + public static class RegisterCodeInfo implements Serializable { + private static final long serialVersionUID = -5028321625140879571L; + + /** + * 注册码 + */ + @SerializedName("register_code") + private String registerCode; + + /** + * 推广包ID + */ + @SerializedName("template_id") + private String templateId; + + /** + * 仅当获取注册码指定该字段时才返回 + */ + @SerializedName("state") + private String state; + + } + + /** + * 应用对应的权限 + */ + @Getter + @Setter + public static class Privilege implements Serializable { + private static final long serialVersionUID = -5028321625140879571L; + + /** + * 权限等级。 + * 1:通讯录基本信息只读 + * 2:通讯录全部信息只读 + * 3:通讯录全部信息读写 + * 4:单个基本信息只读 + * 5:通讯录全部信息只写 + */ + @SerializedName("level") + private Integer level; + + @SerializedName("allow_party") + private List allowParties; + + @SerializedName("allow_user") + private List allowUsers; + + @SerializedName("allow_tag") + private List allowTags; + + @SerializedName("extra_party") + private List extraParties; + + @SerializedName("extra_user") + private List extraUsers; + + @SerializedName("extra_tag") + private List extraTags; + + + } + + /** + * From json wx cp tp permanent code info. + * + * @param json the json + * @return the wx cp tp permanent code info + */ + public static WxCpTpPermanentCodeInfo fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpPermanentCodeInfo.class); + } + + @Override + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpPreauthCode.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpPreauthCode.java new file mode 100644 index 0000000000..31c61b3a2b --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpPreauthCode.java @@ -0,0 +1,38 @@ +package me.chanjar.weixin.cp.bean; + +import com.google.gson.annotations.SerializedName; +import lombok.Getter; +import lombok.Setter; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +/** + * 预授权码返回 + * + * @author yqx created on 2020/3/19 + */ +@Getter +@Setter +public class WxCpTpPreauthCode extends WxCpBaseResp { + + /** + * The Pre auth code. + */ + @SerializedName("pre_auth_code") + String preAuthCode; + + /** + * The Expires in. + */ + @SerializedName("expires_in") + Long expiresIn; + + /** + * From json wx cp tp preauth code. + * + * @param json the json + * @return the wx cp tp preauth code + */ + public static WxCpTpPreauthCode fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpPreauthCode.class); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpProlongTryResult.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpProlongTryResult.java new file mode 100644 index 0000000000..427e020a2f --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpProlongTryResult.java @@ -0,0 +1,49 @@ +package me.chanjar.weixin.cp.bean; + +import com.google.gson.annotations.SerializedName; +import lombok.Getter; +import lombok.Setter; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +/** + * 应用市场延长试用期结果 + * + * @author leiguoqing created on 2022年4月24日 + */ +@Getter +@Setter +public class WxCpTpProlongTryResult extends WxCpBaseResp { + + /** + * The constant serialVersionUID. + */ + private static final long serialVersionUID = -5028321625140879571L; + + /** + * 延长后的试用到期时间(秒级时间戳) + */ + @SerializedName("try_end_time") + private Long tryEndTime; + + + /** + * From json wx cp tp order list get result. + * + * @param json the json + * @return the wx cp tp order list get result + */ + public static WxCpTpProlongTryResult fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpProlongTryResult.class); + } + + /** + * To json string. + * + * @return the string + */ + @Override + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpTag.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpTag.java new file mode 100644 index 0000000000..a73ec171b4 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpTag.java @@ -0,0 +1,34 @@ +package me.chanjar.weixin.cp.bean; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.io.Serializable; + +/** + * The type Wx cp tp tag. + * + * @author zhangq + * @since 2021-02-14 16:15 + */ +@Data +public class WxCpTpTag implements Serializable { + private static final long serialVersionUID = 581740383760234134L; + + @SerializedName("tagid") + private String tagId; + + @SerializedName("tagname") + private String tagName; + + /** + * Deserialize wx cp tp tag. + * + * @param json the json + * @return the wx cp tp tag + */ + public static WxCpTpTag deserialize(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpTag.class); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpTagAddOrRemoveUsersResult.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpTagAddOrRemoveUsersResult.java new file mode 100644 index 0000000000..565cbb408c --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpTagAddOrRemoveUsersResult.java @@ -0,0 +1,23 @@ +package me.chanjar.weixin.cp.bean; + +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +/** + * 企业微信第三方开发-增加标签成员成员api响应体 + * + * @author zhangq + * @since 2021/2/14 16:44 + */ +public class WxCpTpTagAddOrRemoveUsersResult extends WxCpTagAddOrRemoveUsersResult { + private static final long serialVersionUID = 3490401800490702052L; + + /** + * Deserialize wx cp tp tag add or remove users result. + * + * @param json the json + * @return the wx cp tp tag add or remove users result + */ + public static WxCpTpTagAddOrRemoveUsersResult deserialize(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpTagAddOrRemoveUsersResult.class); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpTagGetResult.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpTagGetResult.java new file mode 100644 index 0000000000..134656e438 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpTagGetResult.java @@ -0,0 +1,24 @@ +package me.chanjar.weixin.cp.bean; + +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +/** + * 获取标签成员接口响应体 + * + * @author zhangq + * @since 2021/2/14 16:28 + */ +public class WxCpTpTagGetResult extends WxCpTagGetResult { + private static final long serialVersionUID = 9051748686315562400L; + + /** + * Deserialize wx cp tp tag get result. + * + * @param json the json + * @return the wx cp tp tag get result + */ + public static WxCpTpTagGetResult deserialize(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpTagGetResult.class); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpTagIdListConvertResult.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpTagIdListConvertResult.java new file mode 100644 index 0000000000..e24d36d4d0 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpTagIdListConvertResult.java @@ -0,0 +1,51 @@ +package me.chanjar.weixin.cp.bean; + + +import com.google.gson.annotations.SerializedName; +import lombok.Getter; +import lombok.Setter; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.util.List; + +@Getter +@Setter +public class WxCpTpTagIdListConvertResult extends WxCpBaseResp { + + private static final long serialVersionUID = -6153589164415497369L; + + + /** + * 客户标签转换结果 + */ + @SerializedName("items") + private List items; + + /** + * 无法转换的客户标签ID列表 + */ + @SerializedName("invalid_external_tagid_list") + private List invalidExternalTagIdList; + + + @Getter + @Setter + public static class Item { + + /** + * 企业主体下的客户标签ID + */ + @SerializedName("external_tagid") + private String externalTagId; + + /** + * 服务商主体下的客户标签ID,如果传入的external_tagid已经是服务商主体下的ID,则open_external_tagid与external_tagid相同。 + */ + @SerializedName("open_external_tagid") + private String openExternalTagId; + } + + public static WxCpTpTagIdListConvertResult fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpTagIdListConvertResult.class); + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpTemplateList.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpTemplateList.java new file mode 100644 index 0000000000..83abbb6e7d --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpTemplateList.java @@ -0,0 +1,83 @@ +package me.chanjar.weixin.cp.bean; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.io.Serializable; +import java.util.List; + +/** + * 应用模板列表. + * + * @author Binary Wang + * created on 2026-01-14 + */ +@Data +public class WxCpTpTemplateList extends WxCpBaseResp { + + /** + * 应用模板列表 + */ + @SerializedName("template_list") + private List