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 c15adca964..ced7d6de0c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -12,7 +12,7 @@ assignees: '' ## 另外如果确认属于bug,而且已明确如何修复,请参考贡献指南直接提交PR,省的浪费时间在这里描述问题,非常感谢配合 ### 简要描述 -__简单概括描述下你所遇到的问题。__ +__请简单概括描述下你所遇到的问题。__ ### 模块版本情况 * WxJava 模块名: 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/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/.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 ba8e495afb..0b16b4779e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,5 @@ # 代码贡献指南 -1. 首先非常欢迎和感谢对本项目发起`Pull Request`的同学。 +1. 首先非常欢迎和感谢对本项目发起 `Pull Request` 的热心小伙伴们。 1. **特别提示:请务必在 `develop` 分支提交 `PR`,`release` 分支目前仅是正式版的代码,即发布正式版本后才会从 `develop` 分支进行合并。** 1. 本项目代码风格为使用2个空格代表一个Tab,因此在提交代码时请注意一下,否则很容易在IDE格式化代码后与原代码产生大量diff,这样会给其他人阅读代码带来极大的困扰。 1. 为了便于设置,本项目引入`editorconfig`支持,请使用Eclipse的同学在贡献代码前安装相关插件,而`IntelliJ IDEA`新版本自带支持,如果没有可自行安装插件。 @@ -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 7d72b14919..026529a1c8 100644 --- a/README.md +++ b/README.md @@ -1,111 +1,239 @@ -## 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) - -[![码云Gitee](https://gitee.com/binary/weixin-java-tools/badge/star.svg?theme=blue)](https://gitee.com/binary/weixin-java-tools) -[![Github](https://img.shields.io/github/stars/Wechat-Group/WxJava?logo=github&style=flat)](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.com/Wechat-Group/WxJava.svg?branch=develop)](https://travis-ci.com/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) -#### 微信`Java`开发工具包,支持包括微信支付、开放平台、公众号、企业微信/企业号、小程序等微信功能模块的后端开发。 +
+ + Featured|HelloGitHub + + + binarywang%2FWxJava | 趋势转变 + +
+ +### 微信 `Java` 开发工具包,支持包括微信支付、开放平台、公众号、企业微信、视频号、小程序等微信功能模块的后端开发。 +### 特别赞助
- 特别赞助 + + + + + + + + + + +
+ + 计全支付Jeepay,开源支付系统 + + + + Mall4j + +
+ + mp qrcode + + + 赞助商招募中 + + + ad + +
- - - - - - - - - - - - -
- - - -
- - 计全支付Jeepay,开源支付系统 - -
- - - - -
赞助商招募中,欢迎微信联系洽谈
-
- - - -
+ +### 目录索引 +- [快速开始(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. 项目合作洽谈,请联系微信`binary0000`(在微信里自行搜索并添加好友即可,请注明来意)。 -2. **2021-11-01 发布 [【4.2.0正式版】](https://mp.weixin.qq.com/s/nIk_xOf6dxkhKfqq830Cuw)**! -3. 贡献源码可以参考视频:[【贡献源码全过程(上集)】](https://mp.weixin.qq.com/s/3xUZSATWwHR_gZZm207h7Q)、[【贡献源码全过程(下集)】](https://mp.weixin.qq.com/s/nyzJwVVoYSJ4hSbwyvTx9A) ,友情提供:[程序员小山与Bug](https://space.bilibili.com/473631007) -4. 新手重要提示:本项目仅是一个SDK开发工具包,未提供Web实现,建议使用 `maven` 或 `gradle` 引用本项目即可使用本SDK提供的各种功能,详情可参考 **[【Demo项目】](demo.md)** 或本项目中的部分单元测试代码;另外微信开发新手请务必阅读[【开发文档 Wiki 首页】](https://github.com/Wechat-Group/WxJava/wiki)的常见问题部分,可以少走很多弯路,节省不少时间。 -5. 技术交流群:想获得QQ群/微信群/钉钉企业群等信息的同学,请使用微信扫描上面的微信公众号二维码关注 `WxJava` 后点击相关菜单即可获取加入方式,同时也可以在微信中搜索 `weixin-java-tools` 或 `WxJava` 后选择正确的公众号进行关注,该公众号会及时通知SDK相关更新信息,并不定期分享微信Java开发相关技术知识; -6. 钉钉技术交流群:`32206329`(技术交流2群), `30294972`(技术交流1群,目前已满),`35724728`(通知群,实时通知Github项目变更记录)。 -7. 微信开发新手或者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) ,避免浪费大家的宝贵时间; -8. 寻求帮助时需贴代码或大长串异常信息的,请利用 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`最低版本是`8`,使用`7`的同学可以使用`WxJava` `3.8.0`及以前版本,而还在使用`JDK`6的用户请参考[【此项目】]( 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 (不同模块参考下文) - 4.2.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.6.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 @@ -168,25 +296,15 @@ - 微信公众号管理系统:http://demo.joolun.com - 锐捷网络:Saleslink +
+ ---------------------------------- ### 贡献者列表 -特别感谢参与贡献的所有同学,所有贡献者列表请在[此处](https://github.com/Wechat-Group/WxJava/graphs/contributors)查看,欢迎大家继续踊跃贡献代码! -
-点击此处展开查看贡献次数最多的几位小伙伴 - -1. [chanjarster (Daniel Qian)](https://github.com/chanjarster) -1. [binarywang (Binary Wang)](https://github.com/binarywang) -1. [007gzs](https://github.com/007gzs) -1. [Silloy](https://github.com/silloy) -1. [mgcnrx11](https://github.com/mgcnrx11) -1. [yuanqixun](https://github.com/yuanqixun) -1. [kakotor](https://github.com/kakotor) -1. [aimilin6688 (Jonk)](https://github.com/aimilin6688) -1. [lkqm (Mario Luo)](https://github.com/lkqm) -1. [kareanyi (MillerLin)](https://github.com/kareanyi) +特别感谢参与贡献的所有同学,所有贡献者列表请在[此处](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 de5e462099..d305fc2121 100644 --- a/demo.md +++ b/demo.md @@ -14,12 +14,11 @@ - [使用该 `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) [![Build Status](https://travis-ci.org/binarywang/weixin-java-pay-demo.svg?branch=master)](https://travis-ci.org/binarywang/weixin-java-pay-demo) -1. 企业号/企业微信 Demo:[GitHub](http://github.com/binarywang/weixin-java-cp-demo)、[码云](http://gitee.com/binary/weixin-java-cp-demo) [![Build Status](https://travis-ci.org/binarywang/weixin-java-cp-demo.svg?branch=master)](https://travis-ci.org/binarywang/weixin-java-cp-demo) -1. 微信小程序 Demo:[GitHub](http://github.com/binarywang/weixin-java-miniapp-demo)、[码云](http://gitee.com/binary/weixin-java-miniapp-demo) [![Build Status](https://travis-ci.org/binarywang/weixin-java-miniapp-demo.svg?branch=master)](https://travis-ci.org/binarywang/weixin-java-miniapp-demo) -1. 开放平台 Demo:[GitHub](http://github.com/Wechat-Group/weixin-java-open-demo)、[码云](http://gitee.com/binary/weixin-java-open-demo) [![Build Status](https://travis-ci.org/Wechat-Group/weixin-java-open-demo.svg?branch=master)](https://travis-ci.org/Wechat-Group/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) [![Build Status](https://travis-ci.org/binarywang/weixin-java-mp-demo-springmvc.svg?branch=master)](https://travis-ci.org/binarywang/weixin-java-mp-demo-springmvc) - - 使用 `Spring Boot` 实现的公众号 Demo(支持多公众号):[GitHub](http://github.com/binarywang/weixin-java-mp-demo)、[码云](http://gitee.com/binary/weixin-java-mp-demo-springboot) [![Build Status](https://travis-ci.org/binarywang/weixin-java-mp-demo.svg?branch=master)](https://travis-ci.org/binarywang/weixin-java-mp-demo) - - 含公众号和部分微信支付代码的 Demo:[GitHub](http://github.com/Wechat-Group/weixin-java-demo-springmvc)、[码云](http://gitee.com/binary/weixin-java-tools-springmvc) [![Build Status](https://travis-ci.org/Wechat-Group/weixin-java-demo-springmvc.svg?branch=master)](https://travis-ci.org/Wechat-Group/weixin-java-demo-springmvc) - +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-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: + - 使用 `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)、[码云](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..83d9c08158 --- /dev/null +++ b/docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md @@ -0,0 +1,300 @@ +# 企业微信会话存档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已过期时**:SDK被销毁并清理缓存 +4. **计数归零但SDK未过期时**:保留缓存,供后续调用直接复用 + +> **注意**:引用计数归零并不等同于立即销毁SDK。只有在 SDK 已超过有效期的情况下,框架才会调用 `Finance.DestroySdk()` 释放资源。这一机制避免了每次 API 调用后的频繁初始化/销毁循环。 + +```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秒**:下次 `acquireMsgAuditSdk()` 返回0,触发重新初始化,旧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..1f073905f4 --- /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。对于"拉取→解密→下载媒体"这类典型串行调用链,每步操作都会触发重新初始化。~~ ✅ 已在 4.8.3.B+ 修复:引用计数归零时,仅在 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/ccflow.png b/images/banners/ccflow.png deleted file mode 100644 index 0b7d2b424a..0000000000 Binary files a/images/banners/ccflow.png and /dev/null 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/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 0018b73e5e..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.19 + 1.4.21 provided diff --git a/pom.xml b/pom.xml index 0c373d1670..ea7cf980e0 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.github.binarywang wx-java - 4.2.7.B + 4.8.4.B pom WxJava - Weixin/Wechat Java SDK 微信开发Java SDK @@ -12,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 @@ -92,6 +92,21 @@ 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 + @@ -109,7 +124,11 @@ weixin-java-miniapp weixin-java-open weixin-java-qidian + weixin-java-aispeech + weixin-java-channel spring-boot-starters + solon-plugins + wx-java-bom @@ -119,29 +138,37 @@ UTF-8 4.5.13 - 9.4.41.v20210516 + 5.5.2 + 9.4.57.v20241219 + 1.84 - com.github.binarywang qrcode-utils - 1.1 + 1.3 org.jodd jodd-http - 5.2.0 + 6.3.0 provided com.squareup.okhttp3 okhttp - 4.5.0 + 4.12.0 provided + + + org.apache.httpcomponents.client5 + httpclient5 + ${httpclient5.version} + + org.apache.httpcomponents httpclient @@ -155,17 +182,17 @@ commons-codec commons-codec - 1.10 + 1.13 commons-io commons-io - 2.7 + 2.14.0 org.apache.commons commons-lang3 - 3.10 + 3.18.0 org.slf4j @@ -175,35 +202,38 @@ com.thoughtworks.xstream xstream - 1.4.19 + 1.4.21 com.google.guava guava - 30.0-jre + 33.3.1-jre com.google.code.gson gson - 2.8.0 + 2.13.1 - com.fasterxml.jackson.dataformat - jackson-dataformat-xml - 2.13.0 + com.fasterxml.jackson + jackson-bom + 2.18.4 + pom + import - + joda-time joda-time 2.10.6 - test + + ch.qos.logback logback-classic - 1.2.3 + 1.3.12 test @@ -215,7 +245,8 @@ org.testng testng - 7.1.0 + 7.5.1 + test @@ -230,8 +261,8 @@ org.mockito - mockito-all - 1.10.19 + mockito-core + 4.11.0 test @@ -274,7 +305,7 @@ org.redisson redisson - 3.12.0 + 3.23.3 true provided @@ -302,28 +333,22 @@ org.projectlombok lombok - 1.18.8 + 1.18.30 provided org.bouncycastle - bcpkix-jdk15on - 1.68 + 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 @@ -342,7 +367,7 @@ org.apache.maven.plugins maven-source-plugin - 2.2.1 + 3.1.0 attach-sources @@ -373,7 +398,7 @@ org.apache.maven.plugins maven-gpg-plugin - 1.6 + 3.1.0 sign-artifacts @@ -413,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 @@ -454,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..620193dd4f --- /dev/null +++ b/solon-plugins/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + com.github.binarywang + wx-java + 4.8.4.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..682f1531ed --- /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.4.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..1e002c1e72 --- /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.4.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..48b3fae27e --- /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.4.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..4682d570cd --- /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.4.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..7cfdbcbaf2 --- /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.4.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..a540df9492 --- /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.4.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..359fffe823 --- /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.4.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..8678e82a10 --- /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.4.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..7121f2d700 --- /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() { + String configuredHost = applicationContext.cfg().get(WxMpProperties.PREFIX + ".config-storage.redis.host"); + RedissonClient redissonClient; + if (StringUtils.isNotEmpty(configuredHost)) { + 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..b4d9bc904c --- /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.4.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..e158b9f1a6 --- /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.4.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..b03fa0e22f --- /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.4.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 4ea942cf5e..6d3c7c7e00 100644 --- a/spring-boot-starters/pom.xml +++ b/spring-boot-starters/pom.xml @@ -1,10 +1,12 @@ - + 4.0.0 com.github.binarywang wx-java - 4.2.7.B + 4.8.4.B pom wx-java-spring-boot-starters @@ -12,16 +14,24 @@ WxJava 各个模块的 Spring Boot Starter - 2.5.3 + 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..d3cf55aac1 --- /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.4.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..d36164d58a --- /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.4.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..e4c3b1ba8d --- /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.4.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 index 439ee5c726..d6c1abc945 100644 --- a/spring-boot-starters/wx-java-cp-spring-boot-starter/README.md +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/README.md @@ -16,11 +16,13 @@ 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.agent-id = @agent-id + 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 默认,目前只支持 memory 类型,可以自行扩展 redis 等类型 + 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= 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 index 2f0e9af560..f2b6d12f8e 100644 --- a/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-cp-spring-boot-starter/pom.xml @@ -4,7 +4,7 @@ wx-java-spring-boot-starters com.github.binarywang - 4.2.7.B + 4.8.4.B 4.0.0 @@ -18,6 +18,18 @@ weixin-java-cp ${project.version}
+ + redis.clients + jedis + + + org.redisson + redisson + + + org.springframework.data + spring-data-redis + 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 index 194cf5c403..f78c39dd45 100644 --- 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 @@ -9,7 +9,7 @@ * 企业微信自动注册 * * @author yl - * @date 2021/12/6 + * created on 2021/12/6 */ @Configuration @EnableConfigurationProperties(WxCpProperties.class) 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 index 0e1db87a33..70c4045259 100644 --- 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 @@ -14,7 +14,7 @@ * 企业微信平台相关服务自动注册 * * @author yl - * @date 2021/12/6 + * created on 2021/12/6 */ @Configuration @RequiredArgsConstructor 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 index 5092b3b343..1c7d80b84e 100644 --- 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 @@ -1,6 +1,9 @@ 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; @@ -8,11 +11,14 @@ * 企业微信存储策略自动配置 * * @author yl - * @date 2021/12/6 + * created on 2021/12/6 */ @Configuration @Import({ - WxCpInMemoryConfigStorageConfiguration.class + 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 index b2cc778ac0..c93a7e187f 100644 --- 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 @@ -3,6 +3,7 @@ import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.NestedConfigurationProperty; import java.io.Serializable; @@ -10,7 +11,7 @@ * 企业微信接入相关配置属性 * * @author yl - * @date 2021/12/6 + * created on 2021/12/6 */ @Data @NoArgsConstructor @@ -38,11 +39,21 @@ public class WxCpProperties { * 微信企业号应用 EncodingAESKey */ private String aesKey; + /** + * 微信企业号应用 会话存档私钥 + */ + private String msgAuditPriKey; /** * 微信企业号应用 会话存档类库路径 */ private String msgAuditLibPath; + /** + * 自定义企业微信服务器baseUrl,用于替换默认的 https://qyapi.weixin.qq.com + * 例如:http://proxy.company.com:8080 + */ + private String baseApiUrl; + /** * 配置存储策略,默认内存 */ @@ -57,6 +68,17 @@ public static class ConfigStorage implements Serializable { */ private StorageType type = StorageType.memory; + /** + * 指定key前缀 + */ + private String keyPrefix = "wx:cp"; + + /** + * redis连接配置 + */ + @NestedConfigurationProperty + private WxCpRedisProperties redis = new WxCpRedisProperties(); + /** * http代理主机 */ @@ -100,6 +122,18 @@ public enum StorageType { /** * 内存 */ - memory + 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 index 17e940928d..2b1d8c13c5 100644 --- 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 @@ -7,29 +7,39 @@ /** * WxCpConfigStorage 抽象配置类 * - * @author yl - * @date 2021/12/6 + * @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(); - String token = properties.getToken(); 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 (agentId != null) { - config.setAgentId(agentId); - } 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(); @@ -50,4 +60,5 @@ protected WxCpDefaultConfigImpl config(WxCpDefaultConfigImpl config, WxCpPropert } 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 index e713e4394c..3722bd07d1 100644 --- 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 @@ -13,7 +13,7 @@ * 自动装配基于内存策略配置 * * @author yl - * @date 2021/12/6 + * created on 2021/12/6 */ @Configuration @ConditionalOnProperty( 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/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..fc3b490242 --- /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.4.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..ebb6deba33 --- /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.4.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 82f6bdd8b1..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 @@ -10,12 +10,13 @@ ``` 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 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 e7e6c113a6..9ebb1ee540 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 - 4.2.7.B + 4.8.4.B 4.0.0 @@ -32,7 +31,16 @@ org.springframework.data spring-data-redis - ${spring.boot.version} + 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 fbfae6dfe0..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 @@ -9,7 +9,7 @@ * 自动配置. * * @author Binary Wang - * @date 2019-08-10 + * created on 2019-08-10 */ @Configuration @EnableConfigurationProperties(WxMaProperties.class) 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 index 79c16fb053..f03d3f1493 100644 --- 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 @@ -2,6 +2,7 @@ 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; @@ -46,6 +47,9 @@ public WxMaService wxMaService(WxMaConfig wxMaConfig) { case HttpClient: wxMaService = new WxMaServiceHttpClientImpl(); break; + case HttpComponents: + wxMaService = new WxMaServiceHttpComponentsImpl(); + break; default: wxMaService = new WxMaServiceImpl(); break; 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 index 6f44ac27ee..abcd83e848 100644 --- 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 @@ -2,6 +2,8 @@ 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; /** @@ -10,11 +12,15 @@ 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()); @@ -24,6 +30,19 @@ protected WxMaDefaultConfigImpl config(WxMaDefaultConfigImpl config, WxMaPropert 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; 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 index 52a53debdc..48549e4399 100644 --- 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 @@ -4,11 +4,11 @@ * httpclient类型. * * @author Binary Wang - * @date 2020-05-25 + * created on 2020-05-25 */ public enum HttpClientType { /** - * HttpClient. + * HttpClient (Apache HttpClient 4.x). */ HttpClient, /** @@ -19,4 +19,8 @@ public enum HttpClientType { * 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 index bf9fd6b175..31c6e4b602 100644 --- 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 @@ -4,7 +4,7 @@ * storage类型. * * @author Binary Wang - * @date 2020-05-25 + * created on 2020-05-25 */ public enum StorageType { /** 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 index 9cfaf80e8d..75e3740a19 100644 --- 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 @@ -6,7 +6,7 @@ * redis 配置. * * @author Binary Wang - * @date 2020-08-30 + * created on 2020-08-30 */ @Data public class RedisProperties { 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 280330e928..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 @@ -12,7 +12,7 @@ * 属性配置类. * * @author Binary Wang - * @date 2019-08-10 + * created on 2019-08-10 */ @Data @ConfigurationProperties(prefix = PREFIX) @@ -44,6 +44,23 @@ public class WxMaProperties { */ 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; + /** * 存储策略 */ @@ -71,7 +88,7 @@ public static class ConfigStorage { /** * http客户端类型. */ - private HttpClientType httpClientType = HttpClientType.HttpClient; + private HttpClientType httpClientType = HttpClientType.HttpComponents; /** * http代理主机. @@ -107,6 +124,21 @@ public static class ConfigStorage { * */ 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..37bc9faedb --- /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.4.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 81a075432f..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-spring-boot-starter + ## 快速开始 + 1. 引入依赖 ```xml @@ -11,20 +13,21 @@ 2. 添加配置(application.properties) ```properties # 公众号配置(必填) - wx.mp.appId = appId - wx.mp.secret = @secret - wx.mp.token = @token - wx.mp.aesKey = @aesKey + 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 = Jedis # 配置类型: 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 + 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-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= @@ -35,13 +38,9 @@ #wx.mp.hosts.mp-host=http://proxy.com/ ``` 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 9324590eff..6e3d6da48c 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 - 4.2.7.B + 4.8.4.B 4.0.0 @@ -22,12 +22,11 @@ redis.clients jedis - compile + provided org.springframework.data spring-data-redis - ${spring.boot.version} provided @@ -40,6 +39,16 @@ 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/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 3b8733c286..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 @@ -4,6 +4,7 @@ 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; @@ -35,6 +36,9 @@ public WxMpService wxMpService(WxMpConfigStorage configStorage, WxMpProperties w case HttpClient: wxMpService = newWxMpServiceHttpClientImpl(); break; + case HttpComponents: + wxMpService = newWxMpServiceHttpComponentsImpl(); + break; default: wxMpService = newWxMpServiceImpl(); break; @@ -60,4 +64,8 @@ private WxMpService newWxMpServiceJoddHttpImpl() { return new WxMpServiceJoddHttpImpl(); } + 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 cf3c48656d..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,160 +1,26 @@ package com.binarywang.spring.starter.wxjava.mp.config; -import com.binarywang.spring.starter.wxjava.mp.enums.StorageType; -import com.binarywang.spring.starter.wxjava.mp.properties.RedisProperties; -import com.binarywang.spring.starter.wxjava.mp.properties.WxMpProperties; -import com.google.common.collect.Sets; +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 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.mp.config.WxMpHostConfig; -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.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.JedisPool; -import redis.clients.jedis.JedisPoolAbstract; -import redis.clients.jedis.JedisPoolConfig; -import redis.clients.jedis.JedisSentinelPool; - -import java.util.Set; +import org.springframework.context.annotation.Import; /** * 微信公众号存储策略自动配置. * * @author Luo */ -@Slf4j @Configuration +@Import({ + WxMpInMemoryConfigStorageConfiguration.class, + WxMpInJedisConfigStorageConfiguration.class, + WxMpInRedisTemplateConfigStorageConfiguration.class, + WxMpInRedissonConfigStorageConfiguration.class +}) @RequiredArgsConstructor public class WxMpStorageAutoConfiguration { - private final ApplicationContext applicationContext; - - private final WxMpProperties wxMpProperties; - - @Bean - @ConditionalOnMissingBean(WxMpConfigStorage.class) - public WxMpConfigStorage wxMpConfigStorage() { - StorageType type = wxMpProperties.getConfigStorage().getType(); - WxMpConfigStorage config; - switch (type) { - case Jedis: - config = jedisConfigStorage(); - break; - case RedisTemplate: - config = redisTemplateConfigStorage(); - break; - default: - config = defaultConfigStorage(); - break; - } - // wx host config - if (null != wxMpProperties.getHosts() && StringUtils.isNotEmpty(wxMpProperties.getHosts().getApiHost())) { - WxMpHostConfig hostConfig = new WxMpHostConfig(); - hostConfig.setApiHost(wxMpProperties.getHosts().getApiHost()); - hostConfig.setMpHost(wxMpProperties.getHosts().getMpHost()); - hostConfig.setOpenHost(wxMpProperties.getHosts().getOpenHost()); - config.setHostConfig(hostConfig); - } - return config; - } - - private WxMpConfigStorage defaultConfigStorage() { - WxMpDefaultConfigImpl config = new WxMpDefaultConfigImpl(); - setWxMpInfo(config); - return config; - } - - private WxMpConfigStorage jedisConfigStorage() { - JedisPoolAbstract jedisPool; - if (wxMpProperties.getConfigStorage() != null && wxMpProperties.getConfigStorage().getRedis() != null - && StringUtils.isNotEmpty(wxMpProperties.getConfigStorage().getRedis().getHost())) { - jedisPool = getJedisPool(); - } else { - jedisPool = applicationContext.getBean(JedisPool.class); - } - WxRedisOps redisOps = new JedisWxRedisOps(jedisPool); - WxMpRedisConfigImpl wxMpRedisConfig = new WxMpRedisConfigImpl(redisOps, - wxMpProperties.getConfigStorage().getKeyPrefix()); - setWxMpInfo(wxMpRedisConfig); - return wxMpRedisConfig; - } - - private WxMpConfigStorage 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); - WxMpRedisConfigImpl wxMpRedisConfig = new WxMpRedisConfigImpl(redisOps, - wxMpProperties.getConfigStorage().getKeyPrefix()); - - setWxMpInfo(wxMpRedisConfig); - return wxMpRedisConfig; - } - - private void setWxMpInfo(WxMpDefaultConfigImpl config) { - WxMpProperties properties = wxMpProperties; - WxMpProperties.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 JedisPoolAbstract getJedisPool() { - RedisProperties redis = wxMpProperties.getConfigStorage().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); - } - 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/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 index 1fa235e4af..0bf034417f 100644 --- 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 @@ -4,7 +4,7 @@ * httpclient类型. * * @author Binary Wang - * @date 2020-08-30 + * created on 2020-08-30 */ public enum HttpClientType { /** @@ -19,4 +19,8 @@ public enum HttpClientType { * 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 index 4bf4b07890..05ed6ce393 100644 --- 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 @@ -4,7 +4,7 @@ * storage类型. * * @author Binary Wang - * @date 2020-08-30 + * created on 2020-08-30 */ public enum StorageType { /** 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 index b8c0f1594f..5b29400738 100644 --- 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 @@ -9,10 +9,19 @@ 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 59f82558d7..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 @@ -8,7 +8,7 @@ * redis 配置属性. * * @author Binary Wang - * @date 2020-08-30 + * created on 2020-08-30 */ @Data public class RedisProperties implements Serializable { 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 89d0e6629d..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 @@ -41,9 +41,15 @@ public class WxMpProperties { */ private String aesKey; + /** + * 是否使用稳定版 Access Token + */ + private boolean useStableAccessToken = false; + /** * 自定义host配置 */ + @NestedConfigurationProperty private HostConfig hosts; /** @@ -74,7 +80,7 @@ public static class ConfigStorage implements Serializable { /** * http客户端类型. */ - private HttpClientType httpClientType = HttpClientType.HttpClient; + private HttpClientType httpClientType = HttpClientType.HttpComponents; /** * http代理主机. @@ -96,6 +102,21 @@ public static class ConfigStorage implements Serializable { */ private String httpProxyPassword; + /** + * 连接超时时间,单位毫秒 + */ + 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..e1a4509c6f --- /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.4.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/pom.xml b/spring-boot-starters/wx-java-open-spring-boot-starter/pom.xml index d0c6d4c8d8..e0d5779c6b 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 - 4.2.7.B + 4.8.4.B 4.0.0 @@ -22,18 +22,14 @@ redis.clients jedis - provided org.redisson redisson - provided org.springframework.data spring-data-redis - ${spring.boot.version} - provided 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 22b0a6621d..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 @@ -28,6 +28,7 @@ public WxOpenService wxOpenService(WxOpenConfigStorage wxOpenConfigStorage) { } @Bean + @ConditionalOnMissingBean public WxOpenMessageRouter wxOpenMessageRouter(WxOpenService wxOpenService) { return new WxOpenMessageRouter(wxOpenService); } 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 index ee0443c9ae..91db545ab9 100644 --- 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 @@ -1,7 +1,10 @@ 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 @@ -28,6 +31,24 @@ protected WxOpenInMemoryConfigStorage config(WxOpenInMemoryConfigStorage config, } 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 index 353b670e6a..73a0183d72 100644 --- 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 @@ -1,10 +1,8 @@ package com.binarywang.spring.starter.wxjava.open.config.storage; -import com.binarywang.spring.starter.wxjava.open.properties.RedisProperties; 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.common.redis.JedisWxRedisOps; -import me.chanjar.weixin.common.redis.WxRedisOps; import me.chanjar.weixin.open.api.WxOpenConfigStorage; import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage; import me.chanjar.weixin.open.api.impl.WxOpenInRedisConfigStorage; @@ -39,20 +37,19 @@ public WxOpenConfigStorage wxOpenConfigStorage() { } private WxOpenInRedisConfigStorage getWxOpenInRedisConfigStorage() { - RedisProperties redisProperties = properties.getConfigStorage().getRedis(); + WxOpenRedisProperties wxOpenRedisProperties = properties.getConfigStorage().getRedis(); JedisPool jedisPool; - if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + if (wxOpenRedisProperties != null && StringUtils.isNotEmpty(wxOpenRedisProperties.getHost())) { jedisPool = getJedisPool(); } else { jedisPool = applicationContext.getBean(JedisPool.class); } - WxRedisOps redisOps = new JedisWxRedisOps(jedisPool); - return new WxOpenInRedisConfigStorage(redisOps, properties.getConfigStorage().getKeyPrefix()); + return new WxOpenInRedisConfigStorage(jedisPool, properties.getConfigStorage().getKeyPrefix()); } private JedisPool getJedisPool() { WxOpenProperties.ConfigStorage storage = properties.getConfigStorage(); - RedisProperties redis = storage.getRedis(); + WxOpenRedisProperties redis = storage.getRedis(); JedisPoolConfig config = new JedisPoolConfig(); if (redis.getMaxActive() != null) { 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 index 85aa1d20e0..ea1dce3670 100644 --- 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 @@ -1,13 +1,11 @@ package com.binarywang.spring.starter.wxjava.open.config.storage; -import com.binarywang.spring.starter.wxjava.open.properties.RedisProperties; 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.common.redis.RedissonWxRedisOps; -import me.chanjar.weixin.common.redis.WxRedisOps; import me.chanjar.weixin.open.api.WxOpenConfigStorage; import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage; -import me.chanjar.weixin.open.api.impl.WxOpenInRedisConfigStorage; +import me.chanjar.weixin.open.api.impl.WxOpenInRedissonConfigStorage; import org.apache.commons.lang3.StringUtils; import org.redisson.Redisson; import org.redisson.api.RedissonClient; @@ -40,21 +38,20 @@ public WxOpenConfigStorage wxOpenConfigStorage() { return this.config(config, properties); } - private WxOpenInRedisConfigStorage getWxOpenInRedissonConfigStorage() { - RedisProperties redisProperties = properties.getConfigStorage().getRedis(); + private WxOpenInRedissonConfigStorage getWxOpenInRedissonConfigStorage() { + WxOpenRedisProperties wxOpenRedisProperties = properties.getConfigStorage().getRedis(); RedissonClient redissonClient; - if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) { + if (wxOpenRedisProperties != null && StringUtils.isNotEmpty(wxOpenRedisProperties.getHost())) { redissonClient = getRedissonClient(); } else { redissonClient = applicationContext.getBean(RedissonClient.class); } - WxRedisOps redisOps = new RedissonWxRedisOps(redissonClient); - return new WxOpenInRedisConfigStorage(redisOps, properties.getConfigStorage().getKeyPrefix()); + return new WxOpenInRedissonConfigStorage(redissonClient, properties.getConfigStorage().getKeyPrefix()); } private RedissonClient getRedissonClient() { WxOpenProperties.ConfigStorage storage = properties.getConfigStorage(); - RedisProperties redis = storage.getRedis(); + WxOpenRedisProperties redis = storage.getRedis(); Config config = new Config(); config.useSingleServer() 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 adb35c2fa3..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 @@ -40,6 +40,18 @@ public class WxOpenProperties { */ 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; + /** * 存储策略. */ @@ -58,13 +70,13 @@ public static class ConfigStorage implements Serializable { /** * 指定key前缀. */ - private String keyPrefix = "wx"; + private String keyPrefix = "wx:open"; /** * redis连接配置. */ @NestedConfigurationProperty - private RedisProperties redis = new RedisProperties(); + private WxOpenRedisProperties redis = new WxOpenRedisProperties(); /** * http客户端类型. @@ -108,6 +120,21 @@ public static class ConfigStorage implements Serializable { */ private int maxRetryTimes = 5; + /** + * 连接超时时间,单位毫秒 + */ + private int connectionTimeout = 5000; + + /** + * 读数据超时时间,即socketTimeout,单位毫秒 + */ + private int soTimeout = 5000; + + /** + * 从连接池获取链接的超时时间,单位毫秒 + */ + private int connectionRequestTimeout = 5000; + } public enum StorageType { 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/WxOpenRedisProperties.java similarity index 91% rename from spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/RedisProperties.java rename to spring-boot-starters/wx-java-open-spring-boot-starter/src/main/java/com/binarywang/spring/starter/wxjava/open/properties/WxOpenRedisProperties.java index a03d3a47f6..0aafc73da6 100644 --- 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/WxOpenRedisProperties.java @@ -10,7 +10,7 @@ * @author someone */ @Data -public class RedisProperties implements Serializable { +public class WxOpenRedisProperties implements Serializable { private static final long serialVersionUID = -5924815351660074401L; /** 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..a2320eca2a --- /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.4.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 e62ad4911b..67a26da606 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 - 4.2.7.B + 4.8.4.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 2dd44004a6..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,13 +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.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 940cdf5916..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 @@ -57,13 +57,28 @@ public class WxPayProperties { /** * apiV3秘钥 */ - private String apiv3Key; + private String apiV3Key; + + /** + * 微信支付异步回调地址,通知url必须为直接可访问的url,不能携带参数 + */ + private String notifyUrl; + + /** + * 退款结果异步回调地址,通知url必须为直接可访问的url,不能携带参数 + */ + private String refundNotifyUrl; /** * 微信支付分回调地址 */ private String payScoreNotifyUrl; + /** + * 微信支付分授权回调地址 + */ + private String payScorePermissionNotifyUrl; + /** * apiv3 商户apiclient_key.pem */ @@ -74,4 +89,42 @@ public class WxPayProperties { */ 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 index d676616de6..34069fa1fe 100644 --- a/spring-boot-starters/wx-java-qidian-spring-boot-starter/README.md +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/README.md @@ -13,33 +13,33 @@ 2. 添加配置(application.properties) ```properties # 公众号配置(必填) - wx.mp.appId = appId - wx.mp.secret = @secret - wx.mp.token = @token - wx.mp.aesKey = @aesKey + wx.qidian.appId = appId + wx.qidian.secret = @secret + wx.qidian.token = @token + wx.qidian.aesKey = @aesKey # 存储配置redis(可选) - wx.mp.config-storage.type = Jedis # 配置类型: 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 + 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.mp.config-storage.redis.sentinel-ips=127.0.0.1:16379,127.0.0.1:26379 - #wx.mp.config-storage.redis.sentinel-name=mymaster + #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.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= + 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.mp.hosts.api-host=http://proxy.com/ - #wx.mp.hosts.open-host=http://proxy.com/ - #wx.mp.hosts.mp-host=http://proxy.com/ + #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. 自动注入的类型 -- `WxMpService` -- `WxMpConfigStorage` +- `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 index 24b6cd963b..b07dc4276e 100644 --- a/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml +++ b/spring-boot-starters/wx-java-qidian-spring-boot-starter/pom.xml @@ -3,7 +3,7 @@ wx-java-spring-boot-starters com.github.binarywang - 4.2.7.B + 4.8.4.B 4.0.0 @@ -20,12 +20,12 @@ redis.clients jedis + 4.3.2 compile org.springframework.data spring-data-redis - ${spring.boot.version} provided 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 index 84163b005a..01ba91b565 100644 --- 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 @@ -20,10 +20,11 @@ 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.JedisPoolAbstract; import redis.clients.jedis.JedisPoolConfig; import redis.clients.jedis.JedisSentinelPool; +import redis.clients.jedis.util.Pool; import java.util.Set; @@ -80,7 +81,7 @@ private WxQidianConfigStorage defaultConfigStorage() { } private WxQidianConfigStorage jedisConfigStorage() { - JedisPoolAbstract jedisPool; + Pool jedisPool; if (StringUtils.isNotEmpty(redisHost) || StringUtils.isNotEmpty(redisHost2)) { jedisPool = getJedisPool(); } else { @@ -136,7 +137,7 @@ private void setWxMpInfo(WxQidianDefaultConfigImpl config) { } } - private JedisPoolAbstract getJedisPool() { + private Pool getJedisPool() { WxQidianProperties.ConfigStorage storage = wxQidianProperties.getConfigStorage(); RedisProperties redis = storage.getRedis(); @@ -156,8 +157,9 @@ private JedisPoolAbstract getJedisPool() { config.setTestOnBorrow(true); config.setTestWhileIdle(true); if (StringUtils.isNotEmpty(redis.getSentinelIps())) { + Set sentinels = Sets.newHashSet(redis.getSentinelIps().split(",")); - return new JedisSentinelPool(redis.getSentinelName(), sentinels); + return new JedisSentinelPool(redis.getSentinelName(), sentinels,config); } return new JedisPool(config, redis.getHost(), redis.getPort(), redis.getTimeout(), redis.getPassword(), 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 index 9418a8bec5..04589a911b 100644 --- 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 @@ -4,7 +4,7 @@ * httpclient类型. * * @author Binary Wang - * @date 2020-08-30 + * created on 2020-08-30 */ public enum HttpClientType { /** @@ -19,4 +19,8 @@ public enum HttpClientType { * 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 index e6ae0cab4f..f4e26bc156 100644 --- 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 @@ -4,7 +4,7 @@ * storage类型. * * @author Binary Wang - * @date 2020-08-30 + * created on 2020-08-30 */ public enum StorageType { /** 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 index b055b63fe9..abfad572e7 100644 --- 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 @@ -8,7 +8,7 @@ * redis 配置属性. * * @author Binary Wang - * @date 2020-08-30 + * created on 2020-08-30 */ @Data public class RedisProperties implements Serializable { 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 index ddecefb7e2..bec5dfcce0 100644 --- 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 @@ -72,7 +72,7 @@ public static class ConfigStorage implements Serializable { /** * http客户端类型. */ - private HttpClientType httpClientType = HttpClientType.HttpClient; + private HttpClientType httpClientType = HttpClientType.HttpComponents; /** * http代理主机. 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 index ddf5f714a5..eb12b9a728 100644 --- a/weixin-graal/pom.xml +++ b/weixin-graal/pom.xml @@ -6,7 +6,7 @@ com.github.binarywang wx-java - 4.2.7.B + 4.8.4.B weixin-graal diff --git a/weixin-java-aispeech/pom.xml b/weixin-java-aispeech/pom.xml new file mode 100644 index 0000000000..40c6557065 --- /dev/null +++ b/weixin-java-aispeech/pom.xml @@ -0,0 +1,83 @@ + + + 4.0.0 + + com.github.binarywang + wx-java + 4.8.4.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 - + - org.slf4j - slf4j-api - - - com.thoughtworks.xstream - xstream + org.apache.httpcomponents.client5 + httpclient5 + + org.apache.httpcomponents httpclient + provided commons-logging @@ -46,11 +45,21 @@ org.apache.httpcomponents httpmime + provided + + + + org.slf4j + slf4j-api + + + com.thoughtworks.xstream + xstream org.slf4j jcl-over-slf4j - 1.7.30 + 1.7.36 @@ -77,6 +86,11 @@ org.projectlombok lombok + + org.mockito + mockito-core + test + ch.qos.logback @@ -88,11 +102,7 @@ testng test - - org.mockito - mockito-all - test - + com.google.inject guice 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 35a201edb0..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,5 +1,7 @@ package me.chanjar.weixin.common.api; +import lombok.experimental.UtilityClass; + import java.util.Arrays; import java.util.HashMap; import java.util.List; @@ -10,8 +12,9 @@ /** * 微信开发所使用到的常量类. * - * @author Daniel Qian & binarywang + * @author Daniel Qian, binarywang, Wang_Wong */ +@UtilityClass public class WxConsts { /** * access_token 相关错误代码 @@ -25,9 +28,15 @@ public class WxConsts { 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"; @@ -44,6 +53,7 @@ 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"; } @@ -51,6 +61,7 @@ public static class XmlMsgType { /** * 主动发送消息(即客服消息)的消息类型. */ + @UtilityClass public static class KefuMsgType { /** * 文本消息. @@ -133,9 +144,59 @@ public static class KefuMsgType { 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 { /** * 文本通知型卡片 @@ -162,6 +223,7 @@ public static class TemplateCardType { /** * 表示是否是保密消息,0表示否,1表示是,默认0. */ + @UtilityClass public static class KefuMsgSafe { public static final String NO = "0"; public static final String YES = "1"; @@ -170,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"; @@ -228,6 +293,7 @@ public static class MassMsgStatus { /** * 微信端推送过来的事件类型. */ + @UtilityClass public static class EventType { public static final String SUBSCRIBE = "subscribe"; public static final String UNSUBSCRIBE = "unsubscribe"; @@ -236,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"; /** * 扫码推事件的事件推送 */ @@ -261,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"; + /** * 微信小店 订单付款通知. */ @@ -287,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"; /** * 卡券事件:用户核销卡券 @@ -345,11 +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"; } /** @@ -366,6 +520,7 @@ public static class MediaFileType { /** * 自定义菜单的按钮类型. */ + @UtilityClass public static class MenuButtonType { /** * 点击推事件. @@ -416,6 +571,7 @@ public static class MenuButtonType { /** * oauth2网页授权的scope. */ + @UtilityClass public static class OAuth2Scope { /** * 不弹出授权页面,直接跳转,只能获取用户openid. @@ -436,6 +592,7 @@ public static class OAuth2Scope { /** * 网页应用登录授权作用域. */ + @UtilityClass public static class QrConnectScope { public static final String SNSAPI_LOGIN = "snsapi_login"; } @@ -443,6 +600,7 @@ public static class QrConnectScope { /** * 永久素材类型. */ + @UtilityClass public static class MaterialType { public static final String NEWS = "news"; public static final String VOICE = "voice"; @@ -454,6 +612,7 @@ public static class MaterialType { /** * 网络检测入参. */ + @UtilityClass public static class NetCheckArgs { public static final String ACTIONDNS = "dns"; public static final String ACTIONPING = "ping"; @@ -467,6 +626,7 @@ public static class NetCheckArgs { /** * appId 类型 */ + @UtilityClass public static class AppIdType { /** * 公众号appId类型 @@ -477,4 +637,19 @@ public static class AppIdType { */ 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 465f35434b..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 { /** 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 index b8bfaabb01..6f10e60b71 100644 --- 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 @@ -4,7 +4,7 @@ * 包含toJson()方法的接口. * * @author Binary Wang - * @date 2020-10-05 + * created on 2020-10-05 */ public interface 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/WxOAuth2UserInfo.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/WxOAuth2UserInfo.java index 69518b2565..906e9de2b1 100644 --- 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 @@ -11,7 +11,7 @@ * oauth2用户个人信息. * * @author Binary Wang - * @date 2020-10-11 + * created on 2020-10-11 */ @Data public class WxOAuth2UserInfo implements Serializable { @@ -25,6 +25,23 @@ public class WxOAuth2UserInfo implements Serializable { * 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正方形头像), * 用户没有头像时该项为空 @@ -36,13 +53,13 @@ public class WxOAuth2UserInfo implements Serializable { */ @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/oauth2/WxOAuth2AccessToken.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/oauth2/WxOAuth2AccessToken.java index 0ab32d6574..b339844ad6 100644 --- 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 @@ -7,7 +7,10 @@ import java.io.Serializable; /** - * https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842 + * OAuth2 AccessToken + *

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

* * @author Daniel Qian */ @@ -29,10 +32,17 @@ public class WxOAuth2AccessToken implements Serializable { @SerializedName("scope") private String scope; + /** + * 是否为快照页模式虚拟账号,只有当用户是快照页模式虚拟账号时返回,值为1 + */ + @SerializedName("is_snapshotuser") + private Integer snapshotUser; /** - * https://mp.weixin.qq.com/cgi-bin/announce?action=getannouncement&announce_id=11513156443eZYea&version=&lang=zh_CN. * 本接口在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; 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 index 0b1e0ff838..a50bd96e55 100644 --- 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 @@ -10,7 +10,7 @@ * OCR身份证识别结果. * * @author Binary Wang - * @date 2019-06-23 + * created on 2019-06-23 */ @Data public class WxOcrIdCardResult implements Serializable { @@ -22,6 +22,12 @@ public class WxOcrIdCardResult implements Serializable { 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; 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 index 515189e469..5427d5cada 100644 --- 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 @@ -3,6 +3,7 @@ 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; @@ -15,9 +16,9 @@ public class WxMinishopImageUploadCustomizeResult implements Serializable { private WxMinishopPicFileCustomizeResult imgInfo; public static WxMinishopImageUploadCustomizeResult fromJson(String json) { - JsonObject jsonObject = new JsonParser().parse(json).getAsJsonObject(); + JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject(); WxMinishopImageUploadCustomizeResult result = new WxMinishopImageUploadCustomizeResult(); - result.setErrcode(jsonObject.get("errcode").getAsNumber().toString()); + 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(); 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 index 9aa7a81e2f..9c2cbaf3ba 100644 --- 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 @@ -1,8 +1,10 @@ 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; @@ -19,14 +21,18 @@ public class WxMinishopImageUploadResult implements Serializable { public static WxMinishopImageUploadResult fromJson(String json) { - JsonObject jsonObject = new JsonParser().parse(json).getAsJsonObject(); + JsonObject jsonObject = JsonParser.parseString(json).getAsJsonObject(); WxMinishopImageUploadResult result = new WxMinishopImageUploadResult(); - result.setErrcode(jsonObject.get("errcode").getAsNumber().toString()); + 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(); - picFileResult.setMediaId(picObject.get("media_id").getAsString()); - picFileResult.setPayMediaId(picObject.get("pay_media_id").getAsString()); + 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); } 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 index 1f77a1e6ab..2ae2e2320b 100644 --- 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 @@ -8,4 +8,5 @@ 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 index 3b2f332932..997beb91ac 100644 --- 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 @@ -8,7 +8,7 @@ * . * * @author Binary Wang - * @date 2021-01-27 + * created on 2021-01-27 */ @Data public class CategoryData implements Serializable { 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 index c44f2b0bdb..3f4681047b 100644 --- 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 @@ -8,7 +8,7 @@ * . * * @author Binary Wang - * @date 2021-01-27 + * created on 2021-01-27 */ @Data public class PubTemplateKeyword implements Serializable { 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 index b42924aa77..64222480ad 100644 --- 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 @@ -8,7 +8,7 @@ * . * * @author Binary Wang - * @date 2021-01-27 + * created on 2021-01-27 */ @Data public class TemplateInfo implements Serializable { diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/enums/WxType.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/enums/WxType.java index de047beb1d..9d7d601a0a 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/enums/WxType.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/enums/WxType.java @@ -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 61b863bf1a..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 @@ -248,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参数. */ @@ -449,7 +453,7 @@ public enum WxCpErrorMsgEnum { */ CODE_60008(60008, "部门已存在;部门ID或者部门名称已存在"), /** - * 部门名称含有非法字符;不能含有 \\:?*“< >| 等字符. + * {@code 部门名称含有非法字符;不能含有 \\:?*"< >| 等字符.} */ CODE_60009(60009, "部门名称含有非法字符;不能含有 \\ :?*“< >| 等字符"), /** @@ -517,7 +521,7 @@ public enum WxCpErrorMsgEnum { */ CODE_60124(60124, "无效的父部门id;父部门不存在通讯录中"), /** - * 非法部门名字;不能为空,且不能超过64字节,且不能含有\\:*?”< >|等字符. + * {@code 非法部门名字;不能为空,且不能超过64字节,且不能含有\\:*?"< >|等字符.} */ CODE_60125(60125, "非法部门名字;不能为空,且不能超过64字节,且不能含有\\:*?”< >|等字符"), /** @@ -1092,6 +1096,26 @@ public enum WxCpErrorMsgEnum { * 通用错误码,提交审批单内部接口失败 */ CODE_301057(301057, "通用错误码,提交审批单内部接口失败"), + /** + * 输入userid无对应成员 + */ + CODE_301069(301069,"输入userid无对应成员"), + /** + * 系统错误,请稍后再试 + */ + CODE_301070(301070,"系统错误,请稍后再试"), + /** + * 企业内有其他人员有相似人脸,此情况下人脸仍然会录入成功 + */ + CODE_301071(301071,"企业内有其他人员有相似人脸,此情况下人脸仍然会录入成功"), + /** + * 人脸图像数据错误请更换图片 + */ + CODE_301072(301072,"企业内有其他人员有相似人脸,此情况下人脸仍然会录入成功"), + /** + * 输入参数错误 + */ + CODE_301075(301075,"输入参数错误"), /** * 批量导入任务的文件中userid有重复. */ 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 1156636ea3..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 @@ -12,11 +12,13 @@ /** * 微信错误码. + *

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

* - * @author Daniel Qian & Binary Wang + * @author Daniel Qian, Binary Wang */ @Data @NoArgsConstructor @@ -91,6 +93,13 @@ public static WxError fromJson(String json, WxType type) { } 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/WxMaErrorMsgEnum.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxMaErrorMsgEnum.java index 10cbe5436f..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 @@ -46,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"), /** @@ -70,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 不正确.
@@ -100,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, "请求的参数不正确"), /** @@ -139,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次"), /** @@ -191,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秒内跟用户有过消息交互"), /** @@ -267,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所属账号不符,详情可看注意事项的说明"), + /** * 没有绑定开放平台帐号. */ @@ -343,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 不能为空"), @@ -632,7 +687,7 @@ public enum WxMaErrorMsgEnum { /** * 89252 - * 法人&企业信息一致性校验中 front checking + * {@code 法人&企业信息一致性校验中 front checking} */ CODE_89252(89252, "法人&企业信息一致性校验中"), @@ -657,6 +712,160 @@ public enum WxMaErrorMsgEnum { 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 final int code; 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 index edcc0a28d8..ba910e988b 100644 --- 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 @@ -527,7 +527,7 @@ public enum WxOpenErrorMsgEnum { CODE_40099(40099, "invalid code, this code has consumed."), /** - * invalid DateInfo, Make Sure OldDateInfoType==NewDateInfoType && NewBeginTime<=OldBeginTime && OldEndTime<= NewEndTime + * {@code invalid DateInfo, Make Sure OldDateInfoType==NewDateInfoType && NewBeginTime<=OldBeginTime && OldEndTime<= NewEndTime} */ CODE_40100(40100, "invalid DateInfo, Make Sure OldDateInfoType==NewDateInfoType && NewBeginTime<=OldBeginTime && OldEndTime<= NewEndTime"), @@ -572,7 +572,7 @@ public enum WxOpenErrorMsgEnum { CODE_40108(40108, "invalid client version"), /** - * too many code size, must <= 100 + * {@code too many code size, must <= 100} */ CODE_40109(40109, "too many code size, must <= 100"), @@ -702,7 +702,7 @@ public enum WxOpenErrorMsgEnum { CODE_40135(40135, "invalid not supply bonus, can not change card_id which supply bonus to be not supply"), /** - * invalid use DepositCodeMode, make sure sku.quantity>DepositCode.quantity + * {@code invalid use DepositCodeMode, make sure sku.quantity>DepositCode.quantity} */ CODE_40136(40136, "invalid use DepositCodeMode, make sure sku.quantity>DepositCode.quantity"), @@ -1082,7 +1082,7 @@ public enum WxOpenErrorMsgEnum { CODE_40211(40211, "invalid scope_data"), /** - * paegs 当中存在不合法的query,query格式遵循URL标准,即k1=v1&k2=v2 invalid query + * {@code paegs 当中存在不合法的query,query格式遵循URL标准,即k1=v1&k2=v2 invalid query} */ CODE_40212(40212, "paegs 当中存在不合法的query,query格式遵循URL标准,即k1=v1&k2=v2"), @@ -1136,6 +1136,11 @@ public enum WxOpenErrorMsgEnum { */ CODE_40222(40222, "invalid request idc domain"), + /** + * empty media cover, please check the media + */ + CODE_40229(40229, "媒体封面为空,请添加媒体封面"), + /** * 缺少 access_token 参数 access_token missing */ @@ -4237,7 +4242,7 @@ public enum WxOpenErrorMsgEnum { CODE_71005(71005, "limit exe count"), /** - * limit coin count, 1 <= coin_count <= 100000 + * {@code limit coin count, 1 <= coin_count <= 100000} */ CODE_71006(71006, "limit coin count, 1 <= coin_count <= 100000"), @@ -4342,7 +4347,7 @@ public enum WxOpenErrorMsgEnum { CODE_72018(72018, "duplicate order id, invoice had inserted to user"), /** - * limit msg operation card list size, must <= 5 + * {@code limit msg operation card list size, must <= 5} */ CODE_72019(72019, "limit msg operation card list size, must <= 5"), @@ -6427,7 +6432,7 @@ public enum WxOpenErrorMsgEnum { CODE_88009(88009, "reply is not exists"), /** - * count range error. cout <= 0 or count > 50 + * {@code count range error. cout <= 0 or count > 50} */ CODE_88010(88010, "count range error. cout <= 0 or count > 50"), @@ -6677,7 +6682,7 @@ public enum WxOpenErrorMsgEnum { CODE_89251(89251, "模板消息已下发,待法人人脸核身校验"), /** - * 法人&企业信息一致性校验中 front checking + * {@code 法人&企业信息一致性校验中 front checking} */ CODE_89253(89253, "法人&企业信息一致性校验中"), @@ -7252,7 +7257,7 @@ public enum WxOpenErrorMsgEnum { CODE_200021(200021, "场景描述 sceneDesc 参数错误"), /** - * 禁止创建/更新商品(如商品创建功能被封禁) 或 禁止编辑&更新房间 + * {@code 禁止创建/更新商品(如商品创建功能被封禁) 或 禁止编辑&更新房间} */ CODE_300001(300001, "禁止创建/更新商品(如商品创建功能被封禁) 或 禁止编辑&更新房间"), @@ -8377,7 +8382,7 @@ public enum WxOpenErrorMsgEnum { CODE_9300003(9300003, "begin_time must less than end_time"), /** - * end_time - begin_time > 1year + * {@code end_time - begin_time > 1year} */ CODE_9300004(9300004, "end_time - begin_time > 1year"), @@ -8392,7 +8397,7 @@ public enum WxOpenErrorMsgEnum { CODE_9300006(9300006, "invalid activity status"), /** - * gift_num must >0 and <=15 + * {@code gift_num must >0 and <=15} */ CODE_9300007(9300007, "gift_num must >0 and <=15"), @@ -8407,7 +8412,7 @@ public enum WxOpenErrorMsgEnum { CODE_9300009(9300009, "activity can not finish"), /** - * card_info_list must >= 2 + * {@code card_info_list must >= 2} */ CODE_9300010(9300010, "card_info_list must >= 2"), 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 index ccb8aecefb..e94e03db5d 100644 --- 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 @@ -4,7 +4,7 @@ * WxJava专用的runtime exception. * * @author Binary Wang - * @date 2020-09-26 + * created on 2020-09-26 */ public class WxRuntimeException extends RuntimeException { private static final long serialVersionUID = 4881698471192264412L; 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/JedisWxRedisOps.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/redis/JedisWxRedisOps.java index 2b9849ff92..e6e3f28d7e 100644 --- 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 @@ -1,5 +1,6 @@ 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; @@ -8,6 +9,12 @@ 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; 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 index 19d4046c92..d531a2a307 100644 --- 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 @@ -29,7 +29,7 @@ public void setValue(String key, String value, int expire, TimeUnit timeUnit) { @Override public Long getExpire(String key) { - return redisTemplate.getExpire(key); + return redisTemplate.getExpire(key, TimeUnit.SECONDS); } @Override 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 index 5489165e74..4912cbc5d8 100644 --- 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 @@ -8,6 +8,7 @@ *

* 该接口不承诺稳定, 外部实现请继承{@link BaseWxRedisOps} * + * @author Mario Luo * @see BaseWxRedisOps 实现需要继承该类 * @see JedisWxRedisOps jedis实现 * @see RedissonWxRedisOps redisson实现 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 index 2a84ac0e8b..03bec013dd 100644 --- 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 @@ -8,7 +8,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; @@ -21,10 +20,10 @@ * . * * @author : zhayueran - * @date 2019/6/27 14:06 + * created on 2019/6/27 14:06 */ public class OcrDiscernApacheHttpRequestExecutor extends OcrDiscernRequestExecutor { - public OcrDiscernApacheHttpRequestExecutor(RequestHttp requestHttp) { + public OcrDiscernApacheHttpRequestExecutor(RequestHttp requestHttp) { super(requestHttp); } @@ -43,15 +42,11 @@ public String execute(String uri, File file, WxType wxType) throws WxErrorExcept .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 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 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 index 38926e72e5..542ab4a378 100644 --- 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 @@ -5,6 +5,8 @@ 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; @@ -13,12 +15,12 @@ * . * * @author zhayueran - * @date 2019/6/27 15:06 + * created on 2019/6/27 15:06 */ public abstract class OcrDiscernRequestExecutor implements RequestExecutor { protected RequestHttp requestHttp; - public OcrDiscernRequestExecutor(RequestHttp requestHttp) { + public OcrDiscernRequestExecutor(RequestHttp requestHttp) { this.requestHttp = requestHttp; } @@ -27,12 +29,17 @@ public void execute(String uri, File data, ResponseHandler handler, WxTy 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 OcrDiscernApacheHttpRequestExecutor(requestHttp); + return new OcrDiscernApacheHttpRequestExecutor( + (RequestHttp) requestHttp); + case HTTP_COMPONENTS: + return new OcrDiscernHttpComponentsRequestExecutor( + (RequestHttp) requestHttp); default: - return null; + throw new IllegalArgumentException("不支持的http执行器类型:" + requestHttp.getRequestType()); } } } 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 index 5a53de010f..5dea04928e 100644 --- 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 @@ -8,13 +8,13 @@ * oauth2 相关接口. * * @author Binary Wang - * @date 2020-08-08 + * created on 2020-08-08 */ public interface WxOAuth2Service { /** *

    * 构造oauth2授权的url连接.
-   * 详情请见: https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
+   * 详情请见: 网页授权
    * 
* * @param redirectUri 用户授权完成后的重定向链接,无需urlencode, 方法内会进行encode @@ -27,7 +27,7 @@ public interface WxOAuth2Service { /** *
    * 用code换取oauth2的access token.
-   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=网页授权获取用户基本信息
+   * 详情请见: 网页授权获取用户基本信息
    * 
* * @param code code 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 index 7b4fe337e5..d0aeef8491 100644 --- 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 @@ -12,10 +12,12 @@ /** * 基于小程序或 H5 的身份证、银行卡、行驶证 OCR 识别. - * https://mp.weixin.qq.com/wiki?t=resource/res_main&id=21516712284rHWMX + *

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

* * @author Binary Wang - * @date 2019-06-22 + * created on 2019-06-22 */ public interface WxOcrService { 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 index 24897561cc..f894cba44f 100644 --- 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 @@ -1,6 +1,7 @@ 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; @@ -8,7 +9,7 @@ * 微信服务接口. * * @author Binary Wang - * @date 2020-04-25 + * created on 2020-04-25 */ public interface WxService { /** @@ -60,4 +61,14 @@ public interface WxService { * @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/util/BeanUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/BeanUtils.java index 73b6cff368..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; 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 7f7494431b..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 @@ -39,7 +39,18 @@ public static Map xml2Map(String 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 | SAXException e) { throw new WxRuntimeException(e); @@ -54,7 +65,7 @@ private static Object element2MapOrString(Element element) { final List names = names(nodes); // 判断节点下有无非文本节点(非Text和CDATA),如无,直接取Text文本内容 - if (names.size() < 1) { + if (names.isEmpty()) { return element.getText(); } 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 d44791eb18..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,10 +1,10 @@ package me.chanjar.weixin.common.util.crypto; -import com.google.common.base.CharMatcher; 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; @@ -37,6 +37,19 @@ public class WxCryptUtil { private static final Base64 BASE64 = new Base64(); private static final Charset CHARSET = StandardCharsets.UTF_8; + 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(); @@ -65,7 +78,7 @@ public WxCryptUtil() { public WxCryptUtil(String token, String encodingAesKey, String appidOrCorpid) { this.token = token; this.appidOrCorpid = appidOrCorpid; - this.aesKey = Base64.decodeBase64(CharMatcher.whitespace().removeFrom(encodingAesKey)); + this.aesKey = Base64.decodeBase64(StringUtils.remove(encodingAesKey, " ")); } private static String extractEncryptPart(String xml) { @@ -109,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(); @@ -184,6 +197,7 @@ public EncryptContext encryptContext(String plainText) { /** * 对明文进行加密. * + * @param randomStr 随机字符串 * @param plainText 需要加密的明文 * @return 加密后base64编码的字符串 */ @@ -320,14 +334,28 @@ 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 WxRuntimeException(e); + if (e instanceof WxRuntimeException) { + throw (WxRuntimeException) e; + } else { + throw new WxRuntimeException(e); + } } // appid不相同的情况 暂时忽略这段判断 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 d60f5cedd5..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 @@ -11,6 +11,9 @@ import static org.apache.commons.io.FileUtils.openOutputStream; +/** + * @author Daniel Qian + */ public class FileUtils { /** @@ -21,7 +24,8 @@ 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(); @@ -43,7 +47,7 @@ private static void copyToFile(final InputStream source, final File destination) * @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()); } /** 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 ed5ec17bc9..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 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/HttpType.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpClientType.java similarity index 59% rename from weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpType.java rename to weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/HttpClientType.java index eff5907f7a..a4e22be9b4 100644 --- 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/HttpClientType.java @@ -3,17 +3,21 @@ /** * Created by ecoolper on 2017/4/28. */ -public enum HttpType { +public enum HttpClientType { /** * jodd-http. */ JODD_HTTP, /** - * apache httpclient. + * apache httpclient 4.x. */ APACHE_HTTP, /** * okhttp. */ - OK_HTTP + 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 f338ece672..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,87 +1,68 @@ 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("无法获取到文件名,Content-disposition为空"); - } + String getFileName() throws WxErrorException; - return this.extractFileNameFromContentString(contentDispositionHeader[0].getValue()); - } - - 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); - } - - private String extractFileNameFromContentString(String content) throws WxErrorException { - if (content == null || content.length() == 0) { + static String extractFileNameFromContentString(String content) throws WxErrorException { + if (content == null || content.isEmpty()) { throw new WxErrorException("无法获取到文件名,content为空"); } - Matcher m = PATTERN.matcher(content); - if (m.matches()) { - return m.group(1); + // 查找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); + } + } + + // 查找普通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("无法获取到文件名,header信息有问题"); 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 index fe80af11eb..f03932984f 100644 --- 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 @@ -10,11 +10,12 @@ /** * 输入流数据. - *

+ *

* InputStreamData + *

* * @author zichuan.zhou91@gmail.com - * @date 2022/2/15 + * created on 2022/2/15 */ @Data @Accessors(chain = true) 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 index de4be21709..22c426ca54 100644 --- 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 @@ -1,11 +1,16 @@ 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; @@ -18,7 +23,7 @@ public abstract class MediaInputStreamUploadRequestExecutor implements RequestExecutor { protected RequestHttp requestHttp; - public MediaInputStreamUploadRequestExecutor(RequestHttp requestHttp) { + public MediaInputStreamUploadRequestExecutor(RequestHttp requestHttp) { this.requestHttp = requestHttp; } @@ -27,16 +32,21 @@ public void execute(String uri, InputStreamData data, ResponseHandler create(RequestHttp requestHttp) { + @SuppressWarnings("unchecked") + public static RequestExecutor create(RequestHttp requestHttp) { switch (requestHttp.getRequestType()) { case APACHE_HTTP: - return new ApacheMediaInputStreamUploadRequestExecutor(requestHttp); + return new ApacheMediaInputStreamUploadRequestExecutor( + (RequestHttp) requestHttp); case JODD_HTTP: - return new JoddHttpMediaInputStreamUploadRequestExecutor(requestHttp); + return new JoddHttpMediaInputStreamUploadRequestExecutor((RequestHttp) requestHttp); case OK_HTTP: - return new OkHttpMediaInputStreamUploadRequestExecutor(requestHttp); + return new OkHttpMediaInputStreamUploadRequestExecutor((RequestHttp) requestHttp); + case HTTP_COMPONENTS: + return new HttpComponentsMediaInputStreamUploadRequestExecutor( + (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/MediaUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/MediaUploadRequestExecutor.java index 14724412f1..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.enums.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 index e94b2d8d6a..0e8684a1db 100644 --- 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 @@ -1,11 +1,16 @@ 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; @@ -16,13 +21,12 @@ public abstract class MinishopUploadRequestCustomizeExecutor implements Re protected String uploadType; protected String imgUrl; - public MinishopUploadRequestCustomizeExecutor(RequestHttp requestHttp, String respType, 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 { + } else { this.uploadType = "1"; this.imgUrl = imgUrl; } @@ -33,16 +37,21 @@ public void execute(String uri, File data, ResponseHandler create(RequestHttp requestHttp, String respType, String imgUrl) { + @SuppressWarnings("unchecked") + public static RequestExecutor create(RequestHttp requestHttp, String respType, String imgUrl) { switch (requestHttp.getRequestType()) { case APACHE_HTTP: - return new ApacheMinishopMediaUploadRequestCustomizeExecutor(requestHttp, respType, imgUrl); + return new ApacheMinishopMediaUploadRequestCustomizeExecutor( + (RequestHttp) requestHttp, respType, imgUrl); case JODD_HTTP: - return new JoddHttpMinishopMediaUploadRequestCustomizeExecutor(requestHttp, respType, imgUrl); + return new JoddHttpMinishopMediaUploadRequestCustomizeExecutor((RequestHttp) requestHttp, respType, imgUrl); case OK_HTTP: - return new OkHttpMinishopMediaUploadRequestCustomizeExecutor(requestHttp, respType, imgUrl); + return new OkHttpMinishopMediaUploadRequestCustomizeExecutor((RequestHttp) requestHttp, respType, imgUrl); + case HTTP_COMPONENTS: + return new HttpComponentsMinishopMediaUploadRequestCustomizeExecutor( + (RequestHttp) requestHttp, respType, imgUrl); default: - return null; + 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 index ee4608edf3..e6018a7791 100644 --- 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 @@ -1,11 +1,16 @@ 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; @@ -13,7 +18,7 @@ public abstract class MinishopUploadRequestExecutor implements RequestExecutor { protected RequestHttp requestHttp; - public MinishopUploadRequestExecutor(RequestHttp requestHttp) { + public MinishopUploadRequestExecutor(RequestHttp requestHttp) { this.requestHttp = requestHttp; } @@ -22,16 +27,21 @@ public void execute(String uri, File data, ResponseHandler create(RequestHttp requestHttp) { + @SuppressWarnings("unchecked") + public static RequestExecutor create(RequestHttp requestHttp) { switch (requestHttp.getRequestType()) { case APACHE_HTTP: - return new ApacheMinishopMediaUploadRequestExecutor(requestHttp); + return new ApacheMinishopMediaUploadRequestExecutor( + (RequestHttp) requestHttp); case JODD_HTTP: - return new JoddHttpMinishopMediaUploadRequestExecutor(requestHttp); + return new JoddHttpMinishopMediaUploadRequestExecutor((RequestHttp) requestHttp); case OK_HTTP: - return new OkHttpMinishopMediaUploadRequestExecutor(requestHttp); + return new OkHttpMinishopMediaUploadRequestExecutor((RequestHttp) requestHttp); + case HTTP_COMPONENTS: + return new HttpComponentsMinishopMediaUploadRequestExecutor( + (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/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 266fd226e7..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,11 +1,16 @@ package me.chanjar.weixin.common.util.http; +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.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; @@ -27,16 +32,21 @@ public void execute(String uri, String data, ResponseHandler handler, Wx handler.handle(this.execute(uri, data, wxType)); } + @SuppressWarnings("unchecked") public static RequestExecutor create(RequestHttp requestHttp) { switch (requestHttp.getRequestType()) { case APACHE_HTTP: - return new ApacheSimpleGetRequestExecutor(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()); } } 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 0366b156af..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,11 +1,16 @@ package me.chanjar.weixin.common.util.http; +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; @@ -18,7 +23,7 @@ public abstract class SimplePostRequestExecutor implements RequestExecutor { protected RequestHttp requestHttp; - public SimplePostRequestExecutor(RequestHttp requestHttp) { + public SimplePostRequestExecutor(RequestHttp requestHttp) { this.requestHttp = requestHttp; } @@ -28,16 +33,21 @@ 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()); } } 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 0d5073de14..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 @@ -21,36 +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 ); + 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/ApacheHttpDnsClientBuilder.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheHttpDnsClientBuilder.java index 6a136600e5..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 @@ -117,6 +117,13 @@ public ApacheHttpClientBuilder sslConnectionSocketFactory(SSLConnectionSocketFac 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 *

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 5bb4aeba90..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 @@ -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,7 +58,7 @@ 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()); } @@ -68,11 +68,7 @@ public File execute(String uri, String queryParam, WxType wxType) throws WxError baseName = String.valueOf(System.currentTimeMillis()); } - return FileUtils.createTmpFile(inputStream, baseName, FilenameUtils.getExtension(fileName), - super.tmpDirFile); - - } 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 index 3e6d189e80..43a5d604b0 100644 --- 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 @@ -10,7 +10,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.ContentType; import org.apache.http.entity.mime.HttpMultipartMode; @@ -23,10 +22,10 @@ * 文件输入流上传. * * @author meiqin.zhou91@gmail.com - * @date 2022/02/15 + * created on 2022/02/15 */ public class ApacheMediaInputStreamUploadRequestExecutor extends MediaInputStreamUploadRequestExecutor { - public ApacheMediaInputStreamUploadRequestExecutor(RequestHttp requestHttp) { + public ApacheMediaInputStreamUploadRequestExecutor(RequestHttp requestHttp) { super(requestHttp); } @@ -45,15 +44,11 @@ public WxMediaUploadResult execute(String uri, InputStreamData data, WxType wxTy .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/ApacheMediaUploadRequestExecutor.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/http/apache/ApacheMediaUploadRequestExecutor.java index ca33b8641f..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.enums.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 index 9af02af5d0..48fafc3401 100644 --- 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 @@ -10,7 +10,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; @@ -24,7 +23,7 @@ */ @Slf4j public class ApacheMinishopMediaUploadRequestCustomizeExecutor extends MinishopUploadRequestCustomizeExecutor { - public ApacheMinishopMediaUploadRequestCustomizeExecutor(RequestHttp requestHttp, String respType, String imgUrl) { + public ApacheMinishopMediaUploadRequestCustomizeExecutor(RequestHttp requestHttp, String respType, String imgUrl) { super(requestHttp, respType, imgUrl); } @@ -58,16 +57,12 @@ public WxMinishopImageUploadCustomizeResult execute(String uri, File file, WxTyp .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); - } - log.info("responseContent: " + responseContent); - return WxMinishopImageUploadCustomizeResult.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); } + 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 index 7adc6a2cfa..f76d4e8642 100644 --- 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 @@ -5,13 +5,11 @@ 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.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.CloseableHttpResponse; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.mime.HttpMultipartMode; import org.apache.http.entity.mime.MultipartEntityBuilder; @@ -25,7 +23,7 @@ */ @Slf4j public class ApacheMinishopMediaUploadRequestExecutor extends MinishopUploadRequestExecutor { - public ApacheMinishopMediaUploadRequestExecutor(RequestHttp requestHttp) { + public ApacheMinishopMediaUploadRequestExecutor(RequestHttp requestHttp) { super(requestHttp); } @@ -44,16 +42,12 @@ public WxMinishopImageUploadResult execute(String uri, File file, WxType wxType) .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); - } - log.info("responseContent: " + responseContent); - return WxMinishopImageUploadResult.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); } + 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 index 6b365edd09..08ccf1b252 100644 --- 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 @@ -6,7 +6,6 @@ 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; @@ -16,10 +15,10 @@ * . * * @author ecoolper - * @date 2017/5/4 + * created on 2017/5/4 */ public class ApacheSimpleGetRequestExecutor extends SimpleGetRequestExecutor { - public ApacheSimpleGetRequestExecutor(RequestHttp requestHttp) { + public ApacheSimpleGetRequestExecutor(RequestHttp requestHttp) { super(requestHttp); } @@ -37,12 +36,8 @@ public String execute(String uri, String queryParam, WxType wxType) throws WxErr httpGet.setConfig(config); } - try (CloseableHttpResponse response = requestHttp.getRequestHttpClient().execute(httpGet)) { - String responseContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); - return handleResponse(wxType, responseContent); - } finally { - httpGet.releaseConnection(); - } + 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 4e6f31ec67..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 @@ -4,24 +4,24 @@ 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); } @@ -34,17 +34,12 @@ public String execute(String uri, String postEntity, WxType wxType) throws WxErr } if (postEntity != null) { - StringEntity entity = new StringEntity(postEntity, Consts.UTF_8); - entity.setContentType("application/json; charset=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); - return this.handleResponse(wxType, 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 198798c05a..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 @@ -4,6 +4,8 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpHost; +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; @@ -23,17 +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 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; @@ -55,7 +55,7 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder { * 设置为负数是使用系统默认设置(非3000ms的默认值,而是httpClient的默认设置). *

*/ - private int connectionRequestTimeout = -1; + private int connectionRequestTimeout = 3000; /** * 建立链接的超时时间,默认为5000ms.由于是在链接池获取链接,此设置应该并不起什么作用 @@ -93,6 +93,22 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder { */ private String userAgent; + /** + * 支持的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<>(); + /** * 自定义重试策略 */ @@ -169,6 +185,12 @@ public ApacheHttpClientBuilder sslConnectionSocketFactory(SSLConnectionSocketFac return this; } + @Override + public ApacheHttpClientBuilder supportedProtocols(String[] supportedProtocols) { + this.supportedProtocols = supportedProtocols; + return this; + } + public IdleConnectionMonitorThread getIdleConnectionMonitorThread() { return this.idleConnectionMonitorThread; } @@ -182,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); @@ -230,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); } @@ -238,16 +265,11 @@ 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) { 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 035726d44f..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.toString()); - } - 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 00df2a640a..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,7 +4,6 @@ import jodd.http.HttpRequest; import jodd.http.HttpResponse; import jodd.http.ProxyInfo; -import jodd.util.StringPool; import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; @@ -25,10 +24,10 @@ * . * * @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); } @@ -56,7 +55,7 @@ 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; } 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 index d0591aee9b..915db21c65 100644 --- 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 @@ -4,7 +4,7 @@ import jodd.http.HttpRequest; import jodd.http.HttpResponse; import jodd.http.ProxyInfo; -import jodd.http.up.ByteArrayUploadable; +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; @@ -22,10 +22,10 @@ * 文件输入流上传. * * @author meiqin.zhou91@gmail.com - * @date 2022/02/15 + * created on 2022/02/15 */ public class JoddHttpMediaInputStreamUploadRequestExecutor extends MediaInputStreamUploadRequestExecutor { - public JoddHttpMediaInputStreamUploadRequestExecutor(RequestHttp requestHttp) { + public JoddHttpMediaInputStreamUploadRequestExecutor(RequestHttp requestHttp) { super(requestHttp); } 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 89ea05a029..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,7 +4,6 @@ import jodd.http.HttpRequest; import jodd.http.HttpResponse; import jodd.http.ProxyInfo; -import jodd.util.StringPool; import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; import me.chanjar.weixin.common.error.WxError; @@ -20,10 +19,10 @@ * . * * @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); } 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 index e36f5a7a18..66074d8103 100644 --- 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 @@ -18,11 +18,11 @@ /** * @author liming1019 - * @date 2021/8/10 + * created on 2021/8/10 */ @Slf4j public class JoddHttpMinishopMediaUploadRequestCustomizeExecutor extends MinishopUploadRequestCustomizeExecutor { - public JoddHttpMinishopMediaUploadRequestCustomizeExecutor(RequestHttp requestHttp, String respType, String imgUrl) { + public JoddHttpMinishopMediaUploadRequestCustomizeExecutor(RequestHttp requestHttp, String respType, String imgUrl) { super(requestHttp, respType, imgUrl); } @@ -51,7 +51,7 @@ public WxMinishopImageUploadCustomizeResult execute(String uri, File file, WxTyp if (error.getErrorCode() != 0) { throw new WxErrorException(error); } - log.info("responseContent: " + responseContent); + 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 index 769153c59f..c7c35dd798 100644 --- 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 @@ -5,12 +5,10 @@ import jodd.http.HttpResponse; import jodd.http.ProxyInfo; import lombok.extern.slf4j.Slf4j; -import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; 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.MediaUploadRequestExecutor; import me.chanjar.weixin.common.util.http.MinishopUploadRequestExecutor; import me.chanjar.weixin.common.util.http.RequestHttp; @@ -22,11 +20,11 @@ * . * * @author ecoolper - * @date 2017/5/5 + * created on 2017/5/5 */ @Slf4j public class JoddHttpMinishopMediaUploadRequestExecutor extends MinishopUploadRequestExecutor { - public JoddHttpMinishopMediaUploadRequestExecutor(RequestHttp requestHttp) { + public JoddHttpMinishopMediaUploadRequestExecutor(RequestHttp requestHttp) { super(requestHttp); } @@ -46,7 +44,7 @@ public WxMinishopImageUploadResult execute(String uri, File file, WxType wxType) if (error.getErrorCode() != 0) { throw new WxErrorException(error); } - log.info("responseContent: " + responseContent); + 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 5960274eb6..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,7 +4,6 @@ import jodd.http.HttpRequest; import jodd.http.HttpResponse; import jodd.http.ProxyInfo; -import jodd.util.StringPool; import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.util.http.RequestHttp; @@ -17,10 +16,10 @@ * . * * @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); } 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 50360cae56..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,7 +4,6 @@ import jodd.http.HttpRequest; import jodd.http.HttpResponse; import jodd.http.ProxyInfo; -import jodd.util.StringPool; import me.chanjar.weixin.common.enums.WxType; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.util.http.RequestHttp; @@ -17,10 +16,10 @@ * . * * @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,6 +34,7 @@ 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(); 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 ff0cea1c2b..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 @@ -21,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); } @@ -51,7 +51,7 @@ 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; } 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 index ec85015b26..c30cc619aa 100644 --- 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 @@ -17,10 +17,10 @@ * 文件输入流上传. * * @author meiqin.zhou91@gmail.com - * @date 2022/02/15 + * created on 2022/02/15 */ public class OkHttpMediaInputStreamUploadRequestExecutor extends MediaInputStreamUploadRequestExecutor { - public OkHttpMediaInputStreamUploadRequestExecutor(RequestHttp requestHttp) { + public OkHttpMediaInputStreamUploadRequestExecutor(RequestHttp requestHttp) { super(requestHttp); } 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 6d2602d3df..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 @@ -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 index 367bddd60a..a2c78f423b 100644 --- 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 @@ -14,11 +14,11 @@ /** * @author liming1019 - * @date 2021/8/10 + * created on 2021/8/10 */ @Slf4j public class OkHttpMinishopMediaUploadRequestCustomizeExecutor extends MinishopUploadRequestCustomizeExecutor { - public OkHttpMinishopMediaUploadRequestCustomizeExecutor(RequestHttp requestHttp, String respType, String imgUrl) { + public OkHttpMinishopMediaUploadRequestCustomizeExecutor(RequestHttp requestHttp, String respType, String imgUrl) { super(requestHttp, respType, imgUrl); } @@ -50,7 +50,7 @@ public WxMinishopImageUploadCustomizeResult execute(String uri, File file, WxTyp if (error.getErrorCode() != 0) { throw new WxErrorException(error); } - log.info("responseContent: " + responseContent); + 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 index d8fd66baef..f2df3c7e73 100644 --- 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 @@ -1,12 +1,10 @@ package me.chanjar.weixin.common.util.http.okhttp; import lombok.extern.slf4j.Slf4j; -import me.chanjar.weixin.common.bean.result.WxMediaUploadResult; 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.MediaUploadRequestExecutor; import me.chanjar.weixin.common.util.http.MinishopUploadRequestExecutor; import me.chanjar.weixin.common.util.http.RequestHttp; import okhttp3.*; @@ -18,11 +16,11 @@ * . * * @author ecoolper - * @date 2017/5/5 + * created on 2017/5/5 */ @Slf4j public class OkHttpMinishopMediaUploadRequestExecutor extends MinishopUploadRequestExecutor { - public OkHttpMinishopMediaUploadRequestExecutor(RequestHttp requestHttp) { + public OkHttpMinishopMediaUploadRequestExecutor(RequestHttp requestHttp) { super(requestHttp); } @@ -43,7 +41,7 @@ public WxMinishopImageUploadResult execute(String uri, File file, WxType wxType) if (error.getErrorCode() != 0) { throw new WxErrorException(error); } - log.info("responseContent: " + responseContent); + 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 9be073e38a..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 @@ -11,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); } 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 788ea59260..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 @@ -14,17 +14,17 @@ * . * * @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.Companion.create(postEntity, MediaType.parse("text/plain; charset=utf-8")); + 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(); return this.handleResponse(wxType, Objects.requireNonNull(response.body()).string()); 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 index 061a3cb2ee..caa07d0eaf 100644 --- 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 @@ -10,17 +10,16 @@ * @author niefy */ public class GsonParser { - private static final JsonParser JSON_PARSER = new JsonParser(); public static JsonObject parse(String json) { - return JSON_PARSER.parse(json).getAsJsonObject(); + return new JsonParser().parse(json).getAsJsonObject(); } public static JsonObject parse(Reader json) { - return JSON_PARSER.parse(json).getAsJsonObject(); + return new JsonParser().parse(json).getAsJsonObject(); } public static JsonObject parse(JsonReader json) { - return JSON_PARSER.parse(json).getAsJsonObject(); + 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 ff260c16fb..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,6 +9,9 @@ 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; /** @@ -25,8 +30,24 @@ 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() { if (Objects.isNull(GSON_INSTANCE)) { synchronized (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 50d3b0d630..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,96 +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("article_id", button.getArticleId()); - 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; @@ -118,14 +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.setArticleId(GsonHelper.getString(json, "article_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/RedisTemplateSimpleDistributedLock.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java index 8c5ccc26fd..364b04b574 100644 --- 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 @@ -1,18 +1,11 @@ package me.chanjar.weixin.common.util.locks; import lombok.Getter; -import lombok.NonNull; -import org.jetbrains.annotations.NotNull; -import org.springframework.data.redis.connection.RedisStringCommands; -import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.data.redis.core.script.RedisScript; -import org.springframework.data.redis.core.types.Expiration; -import java.nio.charset.StandardCharsets; -import java.util.Arrays; -import java.util.List; +import java.util.Collections; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Condition; @@ -34,11 +27,11 @@ public class RedisTemplateSimpleDistributedLock implements Lock { private final ThreadLocal valueThreadLocal = new ThreadLocal<>(); - public RedisTemplateSimpleDistributedLock(@NonNull StringRedisTemplate redisTemplate, int leaseMilliseconds) { + public RedisTemplateSimpleDistributedLock( StringRedisTemplate redisTemplate, int leaseMilliseconds) { this(redisTemplate, "lock:" + UUID.randomUUID().toString(), leaseMilliseconds); } - public RedisTemplateSimpleDistributedLock(@NonNull StringRedisTemplate redisTemplate, @NonNull String key, int leaseMilliseconds) { + public RedisTemplateSimpleDistributedLock( StringRedisTemplate redisTemplate, String key, int leaseMilliseconds) { if (leaseMilliseconds <= 0) { throw new IllegalArgumentException("Parameter 'leaseMilliseconds' must grate then 0: " + leaseMilliseconds); } @@ -49,13 +42,17 @@ public RedisTemplateSimpleDistributedLock(@NonNull StringRedisTemplate redisTemp @Override public void lock() { + boolean interrupted = false; while (!tryLock()) { try { Thread.sleep(1000); } catch (InterruptedException e) { - // Ignore + interrupted = true; } } + if (interrupted) { + Thread.currentThread().interrupt(); + } } @Override @@ -68,23 +65,24 @@ public void lockInterruptibly() throws InterruptedException { @Override public boolean tryLock() { String value = valueThreadLocal.get(); - if (value == null || value.length() == 0) { + if (value == null || value.isEmpty()) { value = UUID.randomUUID().toString(); valueThreadLocal.set(value); } - final byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); - final byte[] valueBytes = value.getBytes(StandardCharsets.UTF_8); - List redisResults = redisTemplate.executePipelined((RedisCallback) connection -> { - connection.set(keyBytes, valueBytes, Expiration.milliseconds(leaseMilliseconds), RedisStringCommands.SetOption.SET_IF_ABSENT); - connection.get(keyBytes); - return null; - }); - Object currentLockSecret = redisResults.size() > 1 ? redisResults.get(1) : redisResults.get(0); - return currentLockSecret != null && currentLockSecret.toString().equals(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, @NotNull TimeUnit unit) throws InterruptedException { + public boolean tryLock(long time, TimeUnit unit) throws InterruptedException { long waitMs = unit.toMillis(time); boolean locked = tryLock(); while (!locked && waitMs > 0) { @@ -100,8 +98,8 @@ public boolean tryLock(long time, @NotNull TimeUnit unit) throws InterruptedExce 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, Arrays.asList(key), valueThreadLocal.get()); + 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(); } } 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/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 334d75ee32..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 @@ -11,7 +11,6 @@ import com.thoughtworks.xstream.io.xml.XppDriver; import com.thoughtworks.xstream.security.NoTypePermission; import com.thoughtworks.xstream.security.WildcardTypePermission; - import java.io.Writer; /** @@ -20,6 +19,18 @@ * @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) { @@ -87,7 +98,10 @@ protected void setupConverters() { xstream.addPermission(new WildcardTypePermission(new String[]{ "me.chanjar.weixin.**", "cn.binarywang.wx.**", "com.github.binarywang.**" })); - xstream.setClassLoader(Thread.currentThread().getContextClassLoader()); + if (null == classLoader) { + classLoader = Thread.currentThread().getContextClassLoader(); + } + xstream.setClassLoader(classLoader); return xstream; } 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/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 index 3f08b20bff..049b28227f 100644 --- 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 @@ -6,7 +6,7 @@ /** * * @author Binary Wang - * @date 2020-06-06 + * created on 2020-06-06 */ public class WxNetCheckResultTest { 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 index 96ba20ba2b..fb53c8c4b6 100644 --- 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 @@ -35,6 +35,17 @@ public void testGetExpire() { 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()); 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 index 396862e708..bafe3c30d1 100644 --- 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 @@ -9,7 +9,7 @@ * GsonHelper 的单元测试. * * @author Binary Wang - * @date 2020-09-04 + * created on 2020-09-04 */ public class GsonHelperTest { 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/RedisTemplateSimpleDistributedLockInterruptTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockInterruptTest.java new file mode 100644 index 0000000000..2b8ebccb10 --- /dev/null +++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLockInterruptTest.java @@ -0,0 +1,98 @@ +package me.chanjar.weixin.common.util.locks; + +import org.mockito.Mockito; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ValueOperations; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * 测试 RedisTemplateSimpleDistributedLock 的线程中断处理行为 + * + * @author GitHub Copilot + */ +public class RedisTemplateSimpleDistributedLockInterruptTest { + + private StringRedisTemplate mockRedisTemplate; + private ValueOperations mockValueOps; + private RedisTemplateSimpleDistributedLock lock; + + @BeforeMethod + @SuppressWarnings("unchecked") + public void setUp() { + mockRedisTemplate = Mockito.mock(StringRedisTemplate.class); + mockValueOps = Mockito.mock(ValueOperations.class); + Mockito.when(mockRedisTemplate.opsForValue()).thenReturn(mockValueOps); + lock = new RedisTemplateSimpleDistributedLock(mockRedisTemplate, "test_interrupt_lock", 60000); + } + + /** + * 测试 lock() 在 Thread.sleep 被中断时应恢复线程中断标志 + *

+ * 修复前:InterruptedException 被忽略(// Ignore),线程中断标志丢失 + * 修复后:调用 Thread.currentThread().interrupt() 恢复中断标志 + *

+ */ + @Test(description = "lock() 方法在中断时应恢复线程中断标志") + public void testLockRestoresInterruptedFlagAfterSleepInterruption() throws InterruptedException { + AtomicBoolean interruptedFlagAfterLock = new AtomicBoolean(false); + + // 第一次 setIfAbsent 返回 false(模拟锁被占用),第二次返回 true(模拟锁释放) + Mockito.when(mockValueOps.setIfAbsent(Mockito.anyString(), Mockito.anyString(), + Mockito.anyLong(), Mockito.any(TimeUnit.class))) + .thenReturn(false) + .thenReturn(true); + // get() 返回不同的值,确保不走可重入路径 + Mockito.when(mockValueOps.get(Mockito.anyString())).thenReturn("other-value"); + + Thread testThread = new Thread(() -> { + // 设置中断标志 + Thread.currentThread().interrupt(); + // 调用 lock(),第一次 tryLock 失败,sleep 会因中断标志立即抛出 InterruptedException + lock.lock(); + interruptedFlagAfterLock.set(Thread.currentThread().isInterrupted()); + }); + + testThread.start(); + testThread.join(5000); + + // 线程应该已经完成(不会永远阻塞) + Assert.assertFalse(testThread.isAlive(), "线程应该已完成"); + // 关键验证:中断标志应被恢复(而非被忽略丢失) + Assert.assertTrue(interruptedFlagAfterLock.get(), "lock()执行后线程中断标志应被恢复"); + } + + /** + * 测试 tryLock() 在 Redis 正常响应时的基本行为 + */ + @Test(description = "tryLock() 成功获取锁时应返回 true") + public void testTryLockSuccessfully() { + Mockito.when(mockValueOps.setIfAbsent(Mockito.anyString(), Mockito.anyString(), + Mockito.anyLong(), Mockito.any(TimeUnit.class))) + .thenReturn(true); + + boolean result = lock.tryLock(); + + Assert.assertTrue(result, "应成功获取锁"); + Assert.assertNotNull(lock.getLockSecretValue(), "锁值不应为null"); + } + + /** + * 测试 tryLock() 在锁已被其他线程持有时应返回 false + */ + @Test(description = "锁被占用时 tryLock() 应返回 false") + public void testTryLockWhenLockHeld() { + Mockito.when(mockValueOps.setIfAbsent(Mockito.anyString(), Mockito.anyString(), + Mockito.anyLong(), Mockito.any(TimeUnit.class))) + .thenReturn(false); + Mockito.when(mockValueOps.get(Mockito.anyString())).thenReturn("other-lock-value"); + + boolean result = lock.tryLock(); + + Assert.assertFalse(result, "锁被占用时应返回false"); + } +} 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 index 4b65e31f0b..b278eeafa0 100644 --- 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 @@ -1,8 +1,10 @@ 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; @@ -13,9 +15,10 @@ import static org.testng.Assert.*; @Slf4j -@Test(enabled = false) +@Test(enabled = true) public class RedisTemplateSimpleDistributedLockTest { + private static final String KEY_PREFIX = "System:"; RedisTemplateSimpleDistributedLock redisLock; StringRedisTemplate redisTemplate; @@ -29,6 +32,28 @@ public void init() { 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); 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 81745ee309..e789f7a73d 100644 --- a/weixin-java-cp/pom.xml +++ b/weixin-java-cp/pom.xml @@ -7,7 +7,7 @@ com.github.binarywang wx-java - 4.2.7.B + 4.8.4.B weixin-java-cp @@ -30,6 +30,16 @@ okhttp provided + + org.apache.httpcomponents + httpclient + provided + + + org.apache.httpcomponents.client5 + httpclient5 + provided + redis.clients jedis @@ -43,6 +53,11 @@ org.redisson redisson + + org.springframework.data + spring-data-redis + + org.testng testng @@ -50,9 +65,10 @@ org.mockito - mockito-all + mockito-core test + com.google.inject guice @@ -77,6 +93,10 @@ org.projectlombok lombok + + org.bouncycastle + bcprov-jdk18on + org.assertj @@ -88,6 +108,12 @@ moco-runner test + + + com.fasterxml.jackson.core + jackson-core + test + @@ -99,6 +125,15 @@ 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 + 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 index e653fd0f47..4d6406bd18 100644 --- a/weixin-java-cp/src/main/java/com/tencent/wework/Finance.java +++ b/weixin-java-cp/src/main/java/com/tencent/wework/Finance.java @@ -2,19 +2,19 @@ import lombok.extern.slf4j.Slf4j; +import java.util.List; + /** + * 企业微信会话内容存档Finance类 * 注意: - * 此类必须配置在com.tencent.wework路径底下,否则会报错: - * java.lang.UnsatisfiedLinkError: com.xxx.Finance.NewSdk() + * 此类必须配置在com.tencent.wework路径底下,否则会报错:java.lang.UnsatisfiedLinkError: com.xxx.Finance.NewSdk() *

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

- * 官方文档: - * https://developer.work.weixin.qq.com/document/path/91552 + * 官方文档 * - * @author Wang_Wong - * @date 2022-01-17 + * @author Wang_Wong created on 2022-01-17 */ @Slf4j public class Finance { @@ -24,116 +24,161 @@ public class Finance { private static final String SO_FILE = "so"; private static final String DLL_FILE = "dll"; - public native static long NewSdk(); + /** + * New sdk long. + * + * @return the long + */ + public static native long NewSdk(); /** * 初始化函数 * Return值=0表示该API调用成功 * - * @param [in] sdk NewSdk返回的sdk指针 - * @param [in] corpid 调用企业的企业id,例如:wwd08c8exxxx5ab44d,可以在企业微信管理端--我的企业--企业信息查看 - * @param [in] secret 聊天内容存档的Secret,可以在企业微信管理端--管理工具--聊天内容存档查看 - * @return 返回是否初始化成功 - * 0 - 成功 - * !=0 - 失败 + * @param sdk the sdk + * @param corpid the corpid + * @param secret the secret + * @return 返回是否初始化成功 0 - 成功 !=0 - 失败 */ - public native static int Init(long sdk, String corpid, String secret); + public static native int Init(long sdk, String corpid, String secret); /** * 拉取聊天记录函数 * Return值=0表示该API调用成功 * - * @param [in] sdk NewSdk返回的sdk指针 - * @param [in] seq 从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0 - * @param [in] limit 一次拉取的消息条数,最大值1000条,超过1000条会返回错误 - * @param [in] proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081 - * @param [in] passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123 - * @param [out] chatDatas 返回本次拉取消息的数据,slice结构体.内容包括errcode/errmsg,以及每条消息内容。 - * @return 返回是否调用成功 - * 0 - 成功 - * !=0 - 失败 + * @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 native static int GetChatData(long sdk, long seq, long limit, String proxy, String passwd, long timeout, long chatData); + public static native int GetChatData(long sdk, long seq, long limit, String proxy, String passwd, long timeout, long chatData); /** * 拉取媒体消息函数 * Return值=0表示该API调用成功 * - * @param [in] sdk NewSdk返回的sdk指针 - * @param [in] sdkFileid 从GetChatData返回的聊天消息中,媒体消息包括的sdkfileid - * @param [in] proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081 - * @param [in] passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123 - * @param [in] indexbuf 媒体消息分片拉取,需要填入每次拉取的索引信息。首次不需要填写,默认拉取512k,后续每次调用只需要将上次调用返回的outindexbuf填入即可。 - * @param [out] media_data 返回本次拉取的媒体数据.MediaData结构体.内容包括data(数据内容)/outindexbuf(下次索引)/is_finish(拉取完成标记) - * @return 返回是否调用成功 - * 0 - 成功 - * !=0 - 失败 + * @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 native static int GetMediaData(long sdk, String indexbuf, String sdkField, String proxy, String passwd, long timeout, long mediaData); + public static native int GetMediaData(long sdk, String indexbuf, String sdkField, String proxy, String passwd, long timeout, long mediaData); /** - * @param [in] encrypt_key, getchatdata返回的encrypt_key - * @param [in] encrypt_msg, getchatdata返回的content - * @param [out] msg, 解密的消息明文 - * @return 返回是否调用成功 - * 0 - 成功 - * !=0 - 失败 - * @brief 解析密文 + * 解析密文 + * + * @param sdk the sdk + * @param encrypt_key the encrypt key + * @param encrypt_msg the encrypt msg + * @param msg the msg + * @return 返回是否调用成功 0 - 成功 !=0 - 失败 */ - public native static int DecryptData(long sdk, String encrypt_key, String encrypt_msg, long msg); + public static native int DecryptData(long sdk, String encrypt_key, String encrypt_msg, long msg); - public native static void DestroySdk(long sdk); + /** + * Destroy sdk. + * + * @param sdk the sdk + */ + public static native void DestroySdk(long sdk); - public native static long NewSlice(); + /** + * New slice long. + * + * @return the long + */ + public static native long NewSlice(); /** - * @return - * @brief 释放slice,和NewSlice成对使用 + * 释放slice ,和NewSlice成对使用 + * + * @param slice the slice */ - public native static void FreeSlice(long slice); + public static native void FreeSlice(long slice); /** - * @return 内容 - * @brief 获取slice内容 + * 获取slice内容 + * + * @param slice the slice + * @return 内容 string */ - public native static String GetContentFromSlice(long slice); + public static native String GetContentFromSlice(long slice); /** - * @return 内容 - * @brief 获取slice内容长度 + * 获取slice内容长度 + * + * @param slice the slice + * @return 内容 int */ - public native static int GetSliceLen(long slice); + public static native int GetSliceLen(long slice); - public native static long NewMediaData(); + /** + * New media data long. + * + * @return the long + */ + public static native long NewMediaData(); - public native static void FreeMediaData(long mediaData); + /** + * Free media data. + * + * @param mediaData the media data + */ + public static native void FreeMediaData(long mediaData); /** - * @return outindex - * @brief 获取mediadata outindex + * 获取 mediadata outindex + * + * @param mediaData the media data + * @return outindex string */ - public native static String GetOutIndexBuf(long mediaData); + public static native String GetOutIndexBuf(long mediaData); /** - * @return data - * @brief 获取mediadata data数据 + * 获取 mediadata data数据 + * + * @param mediaData the media data + * @return data byte [ ] */ - public native static byte[] GetData(long mediaData); + public static native byte[] GetData(long mediaData); - public native static int GetIndexLen(long mediaData); + /** + * Get index len int. + * + * @param mediaData the media data + * @return the int + */ + public static native int GetIndexLen(long mediaData); - public native static int GetDataLen(long mediaData); + /** + * Get data len int. + * + * @param mediaData the media data + * @return the int + */ + public static native int GetDataLen(long mediaData); /** - * @return 1完成、0未完成 - * @brief 判断mediadata是否结束 + * Is media data finish int. + * + * @param mediaData the media data + * @return 1完成 、0未完成 + * 判断mediadata是否结束 */ - public native static int IsMediaDataFinish(long mediaData); + public static native int IsMediaDataFinish(long mediaData); /** * 判断Windows环境 * - * @return + * @return boolean boolean */ public static boolean isWindows() { String osName = System.getProperties().getProperty("os.name"); @@ -147,7 +192,7 @@ public static boolean isWindows() { * @param libFiles 类库配置文件 * @param prefixPath 类库文件的前缀路径 */ - public Finance(String[] libFiles, String prefixPath) { + public Finance(List libFiles, String prefixPath) { boolean isWindows = Finance.isWindows(); for (String file : libFiles) { String suffix = file.substring(file.lastIndexOf(".") + 1); @@ -169,11 +214,11 @@ public Finance(String[] libFiles, String prefixPath) { /** * 初始化类库文件 * - * @param libFiles - * @param prefixPath - * @return + * @param libFiles the lib files + * @param prefixPath the prefix path + * @return finance finance */ - public synchronized static Finance loadingLibraries(String[] libFiles, String prefixPath) { + public static synchronized Finance loadingLibraries(List libFiles, String prefixPath) { if (finance != null) { return finance; } @@ -184,9 +229,9 @@ public synchronized static Finance loadingLibraries(String[] libFiles, String pr /** * 单例sdk * - * @return + * @return long */ - public synchronized static long SingletonSDK() { + public static synchronized long SingletonSDK() { if (sdk > 0) { return sdk; } @@ -197,9 +242,9 @@ public synchronized static long SingletonSDK() { /** * 销毁sdk,保证线程可见性 * - * @return + * @param destroySDK the destroy sdk */ - public synchronized static void DestroySingletonSDK(long destroySDK) { + 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 index 7ee8210084..67c57a8a88 100644 --- 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 @@ -4,15 +4,44 @@ import me.chanjar.weixin.cp.bean.WxCpAgentWorkBench; /** + * The interface Wx cp agent work bench service. + * 工作台自定义展示 + * * @author songshiyu - * @date : create in 16:16 2020/9/27 - * @description: 工作台自定义展示:https://work.weixin.qq.com/api/doc/90000/90135/92535 + * 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 9c825d8917..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.message.WxCpAppChatMessage; import me.chanjar.weixin.cp.bean.WxCpChat; +import me.chanjar.weixin.cp.bean.message.WxCpAppChatMessage; import java.util.List; @@ -19,7 +19,7 @@ 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; @@ -40,7 +40,7 @@ public interface WxCpChatService { * 获取群聊会话. * * @param chatId 群聊编号 - * @return 群聊会话 + * @return 群聊会话 wx cp chat * @throws WxErrorException 异常 */ WxCpChat get(String chatId) throws WxErrorException; @@ -49,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 8e04977ff4..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 @@ -3,17 +3,18 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; -import lombok.NonNull; +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.oa.WxCpApprovalInfoQueryFilter; -import org.jetbrains.annotations.NotNull; - -import java.util.Date; -import java.util.List; +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; /** *
@@ -37,13 +38,14 @@ public interface WxCpExternalContactService {
    * 用户需要妥善存储返回的config_id,config_id丢失可能导致用户无法编辑或删除「联系我」。
    * 临时会话模式不占用「联系我」数量,但每日最多添加10万个,并且仅支持单人。
    * 临时会话模式的二维码,添加好友完成后该二维码即刻失效。
+   * 文档地址
    * 
* * @param info 客户联系「联系我」方式 * @return wx cp contact way result * @throws WxErrorException the wx error exception */ - WxCpContactWayResult addContactWay(@NonNull WxCpContactWayInfo info) throws WxErrorException; + WxCpContactWayResult addContactWay(WxCpContactWayInfo info) throws WxErrorException; /** * 获取企业已配置的「联系我」方式 @@ -56,7 +58,26 @@ public interface WxCpExternalContactService { * @return contact way * @throws WxErrorException the wx error exception */ - WxCpContactWayInfo getContactWay(@NonNull String configId) throws WxErrorException; + 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; /** * 更新企业已配置的「联系我」方式 @@ -69,7 +90,7 @@ public interface WxCpExternalContactService { * @return wx cp base resp * @throws WxErrorException the wx error exception */ - WxCpBaseResp updateContactWay(@NonNull WxCpContactWayInfo info) throws WxErrorException; + WxCpBaseResp updateContactWay(WxCpContactWayInfo info) throws WxErrorException; /** * 删除企业已配置的「联系我」方式 @@ -82,7 +103,7 @@ public interface WxCpExternalContactService { * @return wx cp base resp * @throws WxErrorException the wx error exception */ - WxCpBaseResp deleteContactWay(@NonNull String configId) throws WxErrorException; + WxCpBaseResp deleteContactWay(String configId) throws WxErrorException; /** * 结束临时会话 @@ -98,7 +119,7 @@ public interface WxCpExternalContactService { * @return wx cp base resp * @throws WxErrorException the wx error exception */ - WxCpBaseResp closeTempChat(@NonNull String userId, @NonNull String externalUserId) throws WxErrorException; + WxCpBaseResp closeTempChat(String userId, String externalUserId) throws WxErrorException; /** @@ -110,13 +131,13 @@ 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 userId) throws WxErrorException; + WxCpExternalContactInfo getExternalContact(String externalUserId) throws WxErrorException; /** * 获取客户详情. @@ -125,7 +146,7 @@ public interface WxCpExternalContactService { * 企业可通过此接口,根据外部联系人的userid(如何获取?),拉取客户详情。 * * 请求方式:GET(HTTPS) - * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get?access_token=ACCESS_TOKEN&external_userid=EXTERNAL_USERID + * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get?access_token=ACCESS_TOKEN&external_userid=EXTERNAL_USERID} * * 权限说明: * @@ -133,18 +154,18 @@ public interface WxCpExternalContactService { * 第三方/自建应用调用时,返回的跟进人follow_user仅包含应用可见范围之内的成员。 * * - * @param userId 外部联系人的userid,注意不是企业成员的帐号 + * @param externalUserId 外部联系人的userid,注意不是企业成员的帐号 * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 * @return . contact detail * @throws WxErrorException . */ - WxCpExternalContactInfo getContactDetail(String userId, String cursor) throws WxErrorException; + WxCpExternalContactInfo getContactDetail(String externalUserId, String cursor) throws WxErrorException; /** * 企业和服务商可通过此接口,将微信外部联系人的userid转为微信openid,用于调用支付相关接口。暂不支持企业微信外部联系人(ExternalUserid为wo开头)的userid转openid。 * * @param externalUserid 微信外部联系人的userid - * @return 该企业的外部联系人openid + * @return 该企业的外部联系人openid string * @throws WxErrorException . */ String convertToOpenid(String externalUserid) throws WxErrorException; @@ -167,10 +188,58 @@ public interface WxCpExternalContactService { * * * @param unionid 微信客户的unionid - * @return 该企业的外部联系人ID + * @param openid the openid + * @return 该企业的外部联系人ID string * @throws WxErrorException . */ - String unionidToExternalUserid(@NotNull String unionid,String openid) 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转换 @@ -183,16 +252,34 @@ public interface WxCpExternalContactService { * * 权限说明: * - * 该企业授权了该服务商第三方应用,且授权的第三方应用具备“企业客户权限->客户基础信息”权限 + * {@code 该企业授权了该服务商第三方应用,且授权的第三方应用具备“企业客户权限->客户基础信息”权限} * 该客户的跟进人必须在应用的可见范围之内 - * 应用需具备“企业客户权限->客户基础信息”权限 + * {@code 应用需具备“企业客户权限->客户基础信息”权限} * * * @param externalUserid 代开发自建应用获取到的外部联系人ID - * @return 该服务商第三方应用下的企业的外部联系人ID + * @return 该服务商第三方应用下的企业的外部联系人ID string * @throws WxErrorException . */ - String toServiceExternalUserid(@NotNull String externalUserid) 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 @@ -213,12 +300,12 @@ public interface WxCpExternalContactService { * * * @param unionid 微信客户的unionid - * @param openid 微信客户的openid - * @param corpid 需要换取的企业corpid,不填则拉取所有企业 - * @return 该服务商第三方应用下的企业的外部联系人ID + * @param openid 微信客户的openid + * @param corpid 需要换取的企业corpid,不填则拉取所有企业 + * @return 该服务商第三方应用下的企业的外部联系人ID wx cp external user id list * @throws WxErrorException . */ - WxCpExternalUserIdList unionidToExternalUserid3rd(@NotNull String unionid, @NotNull String openid, String corpid) throws WxErrorException; + WxCpExternalUserIdList unionidToExternalUserid3rd(String unionid, String openid, String corpid) throws WxErrorException; /** * 转换external_userid @@ -237,7 +324,7 @@ public interface WxCpExternalContactService { * * * @param externalUserIdList 微信客户的unionid - * @return List 新外部联系人id + * @return List 新外部联系人id * @throws WxErrorException . */ WxCpNewExternalUserIdList getNewExternalUserId(String[] externalUserIdList) throws WxErrorException; @@ -261,7 +348,7 @@ public interface WxCpExternalContactService { * @return wx cp base resp * @throws WxErrorException . */ - WxCpBaseResp finishExternalUserIdMigration(@NotNull String corpid) throws WxErrorException; + WxCpBaseResp finishExternalUserIdMigration(String corpid) throws WxErrorException; /** * 客户群opengid转换 @@ -275,17 +362,18 @@ public interface WxCpExternalContactService { * 权限说明: * * 企业需要使用“客户联系”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,可以用来调用获取客户群详情 + * @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(@NotNull String opengid) throws WxErrorException; + String opengidToChatid(String opengid) throws WxErrorException; /** * 批量获取客户详情. @@ -312,6 +400,25 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c 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; + /** * 修改客户备注信息. *
@@ -332,7 +439,7 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c
    *   企业可通过此接口获取指定成员添加的客户列表。客户是指配置了客户联系功能的成员所添加的外部联系人。没有配置客户联系功能的成员,所添加的外部联系人将不会作为客户返回。
    *
    * 请求方式:GET(HTTPS)
-   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list?access_token=ACCESS_TOKEN&userid=USERID
+   * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list?access_token=ACCESS_TOKEN&userid=USERID}
    *
    * 权限说明:
    *
@@ -361,14 +468,20 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c
   List listFollowers() throws WxErrorException;
 
   /**
-   * 企业和第三方可通过此接口,获取所有离职成员的客户列表,并可进一步调用离职成员的外部联系人再分配接口将这些客户重新分配给其他企业成员。
+   * 获取待分配的离职成员列表
+   * 企业和第三方可通过此接口,获取所有离职成员的客户列表,并可进一步调用分配离职成员的客户接口将这些客户重新分配给其他企业成员。
+
+   *
+   * 请求方式:POST(HTTPS)
+   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_unassigned_list?access_token=ACCESS_TOKEN
    *
-   * @param page     the page
-   * @param pageSize the page size
+   * @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 page, Integer pageSize) throws WxErrorException;
+  WxCpUserExternalUnassignList listUnassignedList(Integer pageId, String cursor, Integer pageSize) throws WxErrorException;
 
   /**
    * 企业可通过此接口,将已离职成员的外部联系人分配给另一个成员接替联系。
@@ -385,17 +498,17 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c
 
   /**
    * 企业可通过此接口,转接在职成员的客户给其他成员。
-   * 
+   * 
    * external_userid必须是handover_userid的客户(即配置了客户联系功能的成员所添加的联系人)。
    * 在职成员的每位客户最多被分配2次。客户被转接成功后,将有90个自然日的服务关系保护期,保护期内的客户无法再次被分配。
-   * 

+ * * 权限说明: - * * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。 - * 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限 + * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。 + * {@code 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限} * 接替成员必须在此第三方应用或自建应用的可见范围内。 * 接替成员需要配置了客户联系功能。 * 接替成员需要在企业微信激活且已经过实名认证。 - * + *

* * @param req 转接在职成员的客户给其他成员请求实体 * @return wx cp base resp @@ -405,37 +518,40 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c /** * 企业和第三方可通过此接口查询在职成员的客户转接情况。 - * + *
    * 权限说明:
-   * 

+ * * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。 - * 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限 + * {@code 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限} * 接替成员必须在此第三方应用或自建应用的可见范围内。 - * + *

* * @param handOverUserid 原添加成员的userid * @param takeOverUserid 接替成员的userid * @param cursor 分页查询的cursor,每个分页返回的数据不会超过1000条;不填或为空表示获取第一个分页; - * @return 客户转接接口实体 + * @return 客户转接接口实体 wx cp user transfer result resp * @throws WxErrorException the wx error exception */ - WxCpUserTransferResultResp transferResult(@NotNull String handOverUserid, @NotNull String takeOverUserid, String cursor) throws WxErrorException; + 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 @@ -445,21 +561,23 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c /** * 企业和第三方可通过此接口查询离职成员的客户分配情况。 - * + *
    * 权限说明:
-   * 

+ + * * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。 - * 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限 + * {@code 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限} * 接替成员必须在此第三方应用或自建应用的可见范围内。 - * + *

* * @param handOverUserid 原添加成员的userid * @param takeOverUserid 接替成员的userid * @param cursor 分页查询的cursor,每个分页返回的数据不会超过1000条;不填或为空表示获取第一个分页; - * @return 客户转接接口实体 + * @return 客户转接接口实体 wx cp user transfer result resp * @throws WxErrorException the wx error exception */ - WxCpUserTransferResultResp resignedTransferResult(@NotNull String handOverUserid, @NotNull String takeOverUserid, String cursor) throws WxErrorException; + WxCpUserTransferResultResp resignedTransferResult(String handOverUserid, String takeOverUserid, + String cursor) throws WxErrorException; /** *
@@ -479,7 +597,8 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c
    * @deprecated 请使用 {@link WxCpExternalContactService#listGroupChat(Integer, String, int, String[])}
    */
   @Deprecated
-  WxCpUserExternalGroupChatList listGroupChat(Integer pageIndex, Integer pageSize, int status, String[] userIds, String[] partyIds) throws WxErrorException;
+  WxCpUserExternalGroupChatList listGroupChat(Integer pageIndex, Integer pageSize, int status, String[] userIds,
+                                              String[] partyIds) throws WxErrorException;
 
   /**
    * 
@@ -506,7 +625,8 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c
    * 微信文档:https://work.weixin.qq.com/api/doc/90000/90135/92122
    * 
* - * @param chatId the chat id + * @param chatId the chat id + * @param needName the need name * @return group chat * @throws WxErrorException the wx error exception */ @@ -515,29 +635,51 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c /** * 企业可通过此接口,将已离职成员为群主的群,分配给另一个客服成员。 * - * + *
    * 注意::
-   * 

+ + * * 群主离职了的客户群,才可继承 * 继承给的新群主,必须是配置了客户联系功能的成员 * 继承给的新群主,必须有设置实名 * 继承给的新群主,必须有激活企业微信 * 同一个人的群,限制每天最多分配300个给新群主 - *

+ + * * 权限说明: - *

+ + * * 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。 - * 第三方应用需拥有“企业客户权限->客户联系->分配离职成员的客户群”权限 + * {@code 第三方应用需拥有“企业客户权限->客户联系->分配离职成员的客户群”权限} * 对于第三方/自建应用,群主必须在应用的可见范围。 - * + *

* * @param chatIds 需要转群主的客户群ID列表。取值范围: 1 ~ 100 * @param newOwner 新群主ID - * @return 分配结果,主要是分配失败的群列表 + * @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; + /** *
    * 企业可通过此接口获取成员联系客户的数据,包括发起申请数、新增客户数、聊天数、发送消息数和删除/拉黑成员的客户数等指标。
@@ -553,7 +695,8 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c
    * @return user behavior statistic
    * @throws WxErrorException the wx error exception
    */
-  WxCpUserExternalUserBehaviorStatistic getUserBehaviorStatistic(Date startTime, Date endTime, String[] userIds, String[] partyIds) throws WxErrorException;
+  WxCpUserExternalUserBehaviorStatistic getUserBehaviorStatistic(Date startTime, Date endTime, String[] userIds,
+                                                                 String[] partyIds) throws WxErrorException;
 
   /**
    * 
@@ -572,19 +715,24 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c
    * @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;
+  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 - *

- * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/92135 + + * + * 文档地址 * * @param wxCpMsgTemplate the wx cp msg template * @return the wx cp msg template add result @@ -592,6 +740,43 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c */ 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; + + /** * 发送新客户欢迎语 *

@@ -603,7 +788,7 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c
    *
    * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/send_welcome_msg?access_token=ACCESS_TOKEN
    *
-   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/92137
+   * 文档地址
    * 
* * @param msg . @@ -678,7 +863,7 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c /** *
    * 企业可通过此接口为指定成员的客户添加上由企业统一配置的标签。
-   * https://work.weixin.qq.com/api/doc/90000/90135/92117
+   * 文档地址
    * 
* * @param userid the userid @@ -692,10 +877,11 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c /** *
- *   企业和第三方应用可通过该接口创建客户朋友圈的发表任务。
- *   https://open.work.weixin.qq.com/api/doc/90000/90135/95094
+   *   企业和第三方应用可通过该接口创建客户朋友圈的发表任务。
+   *   文档地址
    * 
- * @param task + * + * @param task the task * @return wx cp add moment result * @throws WxErrorException the wx error exception */ @@ -704,41 +890,58 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c /** *
    * 由于发表任务的创建是异步执行的,应用需要再调用该接口以获取创建的结果。
-   * https://open.work.weixin.qq.com/api/doc/90000/90135/95094
+   * 文档地址
    * 
+ * * @param jobId 异步任务id,最大长度为64字节,由创建发表内容到客户朋友圈任务接口获取 - * @return - * @throws WxErrorException + * @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; + + /** *
    * 获取客户朋友圈全部的发表记录 获取企业全部的发表列表
-   * https://open.work.weixin.qq.com/api/doc/90000/90135/93333
+   * 文档地址
    * 
- * @param startTime 朋友圈记录开始时间。Unix时间戳 - * @param endTime 朋友圈记录结束时间。Unix时间戳 - * @param creator 朋友圈创建人的userid + * + * @param startTime 朋友圈记录开始时间。Unix时间戳 + * @param endTime 朋友圈记录结束时间。Unix时间戳 + * @param creator 朋友圈创建人的userid * @param filterType 朋友圈类型。0:企业发表 1:个人发表 2:所有,包括个人创建以及企业创建,默认情况下为所有类型 - * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 - * @param limit 返回的最大记录数,整型,最大值100,默认值100,超过最大值时取默认值 - * @return - * @throws WxErrorException + * @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; + String cursor, Integer limit) throws WxErrorException; /** *
    * 获取客户朋友圈全部的发表记录 获取客户朋友圈企业发表的列表
-   * https://open.work.weixin.qq.com/api/doc/90000/90135/93333
+   * 文档地址
    * 
+ * * @param momentId 朋友圈id,仅支持企业发表的朋友圈id - * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 - * @param limit 返回的最大记录数,整型,最大值1000,默认值500,超过最大值时取默认值 - * @return - * @throws WxErrorException + * @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; @@ -746,45 +949,48 @@ WxCpGetMomentTask getMomentTask(String momentId, String cursor, Integer limit) /** *
    * 获取客户朋友圈全部的发表记录 获取客户朋友圈发表时选择的可见范围
-   * https://open.work.weixin.qq.com/api/doc/90000/90135/93333
+   * 文档地址
    * 
+ * * @param momentId 朋友圈id - * @param userId 企业发表成员userid,如果是企业创建的朋友圈,可以通过获取客户朋友圈企业发表的 - * 列表获取已发表成员userid,如果是个人创建的朋友圈,创建人userid就是企业发表成员userid - * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 - * @param limit 返回的最大记录数,整型,最大值1000,默认值500,超过最大值时取默认值 - * @return - * @throws WxErrorException + * @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; + String cursor, Integer limit) throws WxErrorException; /** *
    * 获取客户朋友圈全部的发表记录 获取客户朋友圈发表后的可见客户列表
-   * https://open.work.weixin.qq.com/api/doc/90000/90135/93333
+   * 文档地址
    * 
+ * * @param momentId 朋友圈id - * @param userId 企业发表成员userid,如果是企业创建的朋友圈,可以通过获取客户朋友圈企业发表的列表获取已发表成员userid, - * 如果是个人创建的朋友圈,创建人userid就是企业发表成员userid - * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 - * @param limit 返回的最大记录数,整型,最大值5000,默认值3000,超过最大值时取默认值 - * @return - * @throws WxErrorException + * @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; + String cursor, Integer limit) throws WxErrorException; /** *
    * 获取客户朋友圈全部的发表记录 获取客户朋友圈的互动数据
-   * https://open.work.weixin.qq.com/api/doc/90000/90135/93333
+   * 文档地址
    * 
+ * * @param momentId 朋友圈id - * @param userId 企业发表成员userid,如果是企业创建的朋友圈,可以通过获取客户朋友圈企业发表的列表获取已发表成员userid, - * 如果是个人创建的朋友圈,创建人userid就是企业发表成员userid - * @return - * @throws WxErrorException + * @param userId 企业发表成员userid,如果是企业创建的朋友圈,可以通过获取客户朋友圈企业发表的列表获取已发表成员userid, + * 如果是个人创建的朋友圈,创建人userid就是企业发表成员userid + * @return moment comments + * @throws WxErrorException the wx error exception */ WxCpGetMomentComments getMomentComments(String momentId, String userId) throws WxErrorException; @@ -792,31 +998,32 @@ WxCpGetMomentComments getMomentComments(String momentId, String userId) /** *
    * 企业和第三方应用可通过此接口获取企业与成员的群发记录。
-   * https://work.weixin.qq.com/api/doc/90000/90135/93338
+   * 文档地址
    * 
* - * @param chatType 群发任务的类型,默认为single,表示发送给客户,group表示发送给客户群 - * @param startTime 群发任务记录开始时间 - * @param endTime 群发任务记录结束时间 - * @param creator 群发任务创建人企业账号id - * @param filterType 创建人类型。0:企业发表 1:个人发表 2:所有,包括个人创建以及企业创建,默认情况下为所有类型 - * @param limit 返回的最大记录数,整型,最大值100,默认值50,超过最大值时取默认值 - * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 + * @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, @NonNull Date startTime, @NonNull Date endTime, String creator, Integer filterType, Integer limit, String cursor) throws WxErrorException; + 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#获取企业群发成员执行结果
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/93338
    * 
* - * @param msgid 群发消息的id,通过获取群发记录列表接口返回 - * @param userid 发送成员userid,通过获取群发成员发送任务列表接口返回 - * @param limit 返回的最大记录数,整型,最大值1000,默认值500,超过最大值时取默认值 - * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 + * @param msgid 群发消息的id,通过获取群发记录列表接口返回 + * @param userid 发送成员userid,通过获取群发成员发送任务列表接口返回 + * @param limit 返回的最大记录数,整型,最大值1000,默认值500,超过最大值时取默认值 + * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 * @return wx cp base resp * @throws WxErrorException the wx error exception */ @@ -825,7 +1032,7 @@ WxCpGetMomentComments getMomentComments(String momentId, String userId) /** *
    * 企业跟第三方应用可通过该接口获取到创建企业群发的群发发送结果。
-   * https://work.weixin.qq.com/api/doc/16251
+   * 文档
    * 
* * @param msgid 群发消息的id,通过创建企业群发接口返回 @@ -834,30 +1041,30 @@ WxCpGetMomentComments getMomentComments(String momentId, String userId) * @return wx cp base resp * @throws WxErrorException the wx error exception */ - public WxCpGroupMsgResult getGroupMsgResult(String msgid, Integer limit, String cursor) throws WxErrorException; + WxCpGroupMsgResult getGroupMsgResult(String msgid, Integer limit, String cursor) throws WxErrorException; /** *
    * 获取群发成员发送任务列表。
-   * https://work.weixin.qq.com/api/doc/90000/90135/93338#获取群发成员发送任务列表
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/93338
    * 
* - * @param msgid 群发消息的id,通过获取群发记录列表接口返回 - * @param limit 返回的最大记录数,整型,最大值1000,默认值500,超过最大值时取默认值 - * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 + * @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; + WxCpGroupMsgTaskResult getGroupMsgTask(String msgid, Integer limit, String cursor) throws WxErrorException; /** *
    * 添加入群欢迎语素材。
-   * https://open.work.weixin.qq.com/api/doc/90000/90135/92366#添加入群欢迎语素材
+   * 文档地址:https://open.work.weixin.qq.com/api/doc/90000/90135/92366
    * 
* - * @param template 素材内容 - * @return template_id 欢迎语素材id + * @param template 素材内容 + * @return template_id 欢迎语素材id * @throws WxErrorException the wx error exception */ String addGroupWelcomeTemplate(WxCpGroupWelcomeTemplateResult template) throws WxErrorException; @@ -865,10 +1072,10 @@ WxCpGetMomentComments getMomentComments(String momentId, String userId) /** *
    * 编辑入群欢迎语素材。
-   * https://open.work.weixin.qq.com/api/doc/90000/90135/92366#编辑入群欢迎语素材
+   * 文档地址:https://open.work.weixin.qq.com/api/doc/90000/90135/92366
    * 
* - * @param template + * @param template the template * @return wx cp base resp * @throws WxErrorException the wx error exception */ @@ -877,37 +1084,37 @@ WxCpGetMomentComments getMomentComments(String momentId, String userId) /** *
    * 获取入群欢迎语素材。
-   * https://open.work.weixin.qq.com/api/doc/90000/90135/92366#获取入群欢迎语素材
+   * 文档地址:https://open.work.weixin.qq.com/api/doc/90000/90135/92366
    * 
* - * @param templateId 群欢迎语的素材id + * @param templateId 群欢迎语的素材id * @return wx cp base resp * @throws WxErrorException the wx error exception */ - WxCpGroupWelcomeTemplateResult getGroupWelcomeTemplate(@NotNull String templateId) throws WxErrorException; + WxCpGroupWelcomeTemplateResult getGroupWelcomeTemplate(String templateId) throws WxErrorException; /** *
    * 删除入群欢迎语素材。
    * 企业可通过此API删除入群欢迎语素材,且仅能删除调用方自己创建的入群欢迎语素材。
-   * https://open.work.weixin.qq.com/api/doc/90000/90135/92366#删除入群欢迎语素材
+   * 文档地址:https://open.work.weixin.qq.com/api/doc/90000/90135/92366
    * 
* - * @param templateId 群欢迎语的素材id - * @param templateId 授权方安装的应用agentid。仅旧的第三方多应用套件需要填此参数 + * @param templateId 群欢迎语的素材id + * @param agentId the agent id * @return wx cp base resp * @throws WxErrorException the wx error exception */ - WxCpBaseResp delGroupWelcomeTemplate(@NotNull String templateId, String agentId) throws WxErrorException; + WxCpBaseResp delGroupWelcomeTemplate(String templateId, String agentId) throws WxErrorException; /** *
-   * 获取商品图册
-   * https://work.weixin.qq.com/api/doc/90000/90135/95096#获取商品图册列表
+   * 获取商品图册列表
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/95096
    * 
* - * @param limit 返回的最大记录数,整型,最大值100,默认值50,超过最大值时取默认值 - * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 + * @param limit 返回的最大记录数,整型,最大值100,默认值50,超过最大值时取默认值 + * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 * @return wx cp base resp * @throws WxErrorException the wx error exception */ @@ -916,10 +1123,10 @@ WxCpGetMomentComments getMomentComments(String momentId, String userId) /** *
    * 获取商品图册
-   * https://work.weixin.qq.com/api/doc/90000/90135/95096#获取商品图册
+   * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/95096
    * 
* - * @param productId 商品id + * @param productId 商品id * @return wx cp base resp * @throws WxErrorException the wx error exception */ @@ -928,32 +1135,271 @@ WxCpGetMomentComments getMomentComments(String momentId, String userId) /** *
    * 上传附件资源
-   * https://open.work.weixin.qq.com/api/doc/90001/90143/95178
+   * ...
    * 
- * @param mediaType - * @param fileType - * @param attachmentType - * @param inputStream - * @return - * @throws WxErrorException - * @throws IOException + * + * @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; + InputStream inputStream) throws WxErrorException, IOException; /** *
    * 上传附件资源
-   * https://open.work.weixin.qq.com/api/doc/90001/90143/95178
+   * ...
    * 
- * @param mediaType - * @param attachmentType - * @param file - * @return - * @throws WxErrorException + * + * @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 index b5a9579e0d..b8ccea5e50 100644 --- 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 @@ -2,6 +2,7 @@ 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; @@ -10,8 +11,7 @@ * 文档地址:https://work.weixin.qq.com/help?doc_id=13376 * 调用地址:https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key= * - * @author yr - * @date 2020-8-20 + * @author yr created on 2020-8-20 */ public interface WxCpGroupRobotService { @@ -70,6 +70,23 @@ public interface WxCpGroupRobotService { */ 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类型的消息 * @@ -88,4 +105,32 @@ public interface WxCpGroupRobotService { * @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 index 0da548905c..046cfbc5bb 100644 --- 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 @@ -1,33 +1,19 @@ package me.chanjar.weixin.cp.api; -import java.util.List; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.cp.bean.WxCpBaseResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfAccountAdd; -import me.chanjar.weixin.cp.bean.kf.WxCpKfAccountAddResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfAccountDel; -import me.chanjar.weixin.cp.bean.kf.WxCpKfAccountLink; -import me.chanjar.weixin.cp.bean.kf.WxCpKfAccountLinkResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfAccountListResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfAccountUpd; -import me.chanjar.weixin.cp.bean.kf.WxCpKfCustomerBatchGetResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfMsgListResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfMsgSendRequest; -import me.chanjar.weixin.cp.bean.kf.WxCpKfMsgSendResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfServiceStateResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfServiceStateTransResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfServicerListResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfServicerOpResp; +import me.chanjar.weixin.cp.bean.kf.*; + +import java.util.List; /** * 微信客服接口 + *

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

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

* 事件场景 允许下发条数 code有效期 支持的消息类型 获取code途径 * 用户进入会话,用于发送客服欢迎语 1条 20秒 文本、菜单 事件回调 * 进入接待池,用于发送排队提示语等 1条 48小时 文本 转接会话接口 @@ -174,18 +196,99 @@ WxCpKfMsgListResp syncMsg(String cursor, String token, Integer limit, Integer vo * 等 1条 48小时 文本 事件回调、转接会话接口 * 结束会话,用于发送结束会话提示语 * 或满意度评价等 1条 20秒 文本、菜单 事件回调、转接会话接口 - * @param request - * @return - * @throws WxErrorException + * + * @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 - * @return - * @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 index 4b417e90f2..63fabad7a1 100644 --- 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 @@ -8,8 +8,7 @@ * 企业微信直播接口. * 官方文档:https://work.weixin.qq.com/api/doc/90000/90135/93633 * - * @author Wang_Wong - * @date 2021-12-21 + * @author Wang_Wong created on 2021-12-21 */ public interface WxCpLivingService { @@ -18,7 +17,7 @@ public interface WxCpLivingService { * 请求方式: POST(HTTPS) * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/living/get_living_code?access_token=ACCESS_TOKEN * - * @param openId 用户openid + * @param openId 用户openid * @param livingId 直播id * @return living_code 微信观看直播凭证 * @throws WxErrorException the wx error exception @@ -28,10 +27,10 @@ public interface WxCpLivingService { /** * 获取直播详情 * 请求方式:GET(HTTPS) - * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/living/get_living_info?access_token=ACCESS_TOKEN&livingid=LIVINGID + * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/living/get_living_info?access_token=ACCESS_TOKEN&livingid=LIVINGID} * * @param livingId 直播id - * @return 获取的直播详情 + * @return 获取的直播详情 living info * @throws WxErrorException the wx error exception */ WxCpLivingInfo getLivingInfo(@NonNull String livingId) throws WxErrorException; @@ -39,42 +38,42 @@ public interface WxCpLivingService { /** * 获取直播观看明细 * 通过该接口可以获取所有观看直播的人员统计 - * + *

* 请求方式: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 - * @throws WxErrorException + * @param nextKey 上一次调用时返回的next_key,初次调用可以填”0” + * @return watch stat + * @throws WxErrorException the wx error exception */ - WxCpWatchStat getWatchStat(@NonNull String livingId, Integer nextKey) throws WxErrorException; + 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 - * @throws WxErrorException + * @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 - * @throws WxErrorException + * @return living share info + * @throws WxErrorException the wx error exception */ WxCpLivingShareInfo getLivingShareInfo(@NonNull String wwShareCode) throws WxErrorException; @@ -84,8 +83,8 @@ public interface WxCpLivingService { * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/living/create?access_token=ACCESS_TOKEN * * @param request 创建预约直播请求参数. - * @return - * @throws WxErrorException + * @return livingId (直播id) + * @throws WxErrorException the wx error exception */ String livingCreate(WxCpLivingCreateRequest request) throws WxErrorException; @@ -95,8 +94,8 @@ public interface WxCpLivingService { * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/living/modify?access_token=ACCESS_TOKEN * * @param request 修改预约直播请求参数. - * @return - * @throws WxErrorException + * @return wx cp living result + * @throws WxErrorException the wx error exception */ WxCpLivingResult livingModify(WxCpLivingModifyRequest request) throws WxErrorException; @@ -106,8 +105,8 @@ public interface WxCpLivingService { * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/living/cancel?access_token=ACCESS_TOKEN * * @param livingId 直播id,仅允许取消预约状态下的直播id - * @return - * @throws WxErrorException + * @return wx cp living result + * @throws WxErrorException the wx error exception */ WxCpLivingResult livingCancel(@NonNull String livingId) throws WxErrorException; @@ -117,8 +116,8 @@ public interface WxCpLivingService { * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/living/delete_replay_data?access_token=ACCESS_TOKEN * * @param livingId 直播id - * @return - * @throws WxErrorException + * @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 d9b53f250e..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,6 +33,9 @@ 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; @@ -43,19 +48,47 @@ WxMediaUploadResult upload(String mediaType, String fileType, InputStream inputS * @param mediaType 媒体类型, 请看{@link me.chanjar.weixin.common.api.WxConsts} * @param filename 文件名.例如:wework.txt * @param url 远程链接 - * @return - * @throws WxErrorException - * @throws IOException + * @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; @@ -67,7 +100,8 @@ WxMediaUploadResult upload(String mediaType, String filename, String url) *
* * @param mediaId 媒体id - * @return 保存到本地的临时文件 + * @return 保存到本地的临时文件 file + * @throws WxErrorException the wx error exception */ File download(String mediaId) throws WxErrorException; @@ -76,13 +110,14 @@ WxMediaUploadResult upload(String mediaType, String filename, String url) * 获取高清语音素材. * 可以使用本接口获取从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; @@ -96,7 +131,25 @@ WxMediaUploadResult upload(String mediaType, String filename, String url) *
* * @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 index 1b66d00c07..534cc89b36 100644 --- 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 @@ -6,8 +6,7 @@ /** * 消息推送接口. * - * @author Binary Wang - * @date 2020 -08-30 + * @author Binary Wang created on 2020 -08-30 */ public interface WxCpMessageService { /** @@ -32,7 +31,7 @@ public interface WxCpMessageService { * * * @param timeType 查询哪天的数据,0:当天;1:昨天。默认为0。 - * @return 统计结果 + * @return 统计结果 statistics * @throws WxErrorException the wx error exception */ WxCpMessageSendStatistics getStatistics(int timeType) throws WxErrorException; @@ -50,4 +49,33 @@ public interface WxCpMessageService { * @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 index 63389aeb8c..5e8811953f 100644 --- 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 @@ -5,6 +5,7 @@ import me.chanjar.weixin.cp.bean.msgaudit.*; import java.util.List; +import java.util.function.Consumer; /** * 会话内容存档接口. @@ -13,8 +14,7 @@ * 如需自行实现,亦可调用Finance类库函数,进行实现: * com.tencent.wework.Finance * - * @author Wang_Wong - * @date 2022-01-14 + * @author Wang_Wong created on 2022-01-14 */ public interface WxCpMsgAuditService { @@ -26,32 +26,106 @@ public interface WxCpMsgAuditService { * @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 返回是否调用成功 + * @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 + * @throws Exception the exception */ - WxCpChatModel getDecryptData(@NonNull WxCpChatDatas.WxCpChatData chatData) throws 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 + * @throws Exception the exception */ - String getChatPlainText(@NonNull WxCpChatDatas.WxCpChatData chatData) throws 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接口拉取数据内容。 + *

* 注意: * 根据上面返回的文件类型,拼接好存放文件的绝对路径即可。此时绝对路径写入文件流,来达到获取媒体文件的目的。 * 详情可以看官方文档,亦可阅读此接口源码。 @@ -61,9 +135,45 @@ public interface WxCpMsgAuditService { * @param passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123,如果没有传null * @param timeout 超时时间,分片数据需累加到文件存储。单次最大返回512K字节,如果文件比较大,自行设置长一点,比如timeout=10000 * @param targetFilePath 目标文件绝对路径+实际文件名,比如:/usr/local/file/20220114/474f866b39d10718810d55262af82662.gif - * @throws WxErrorException + * @throws WxErrorException the wx error exception */ - void getMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout, @NonNull String targetFilePath) throws WxErrorException; + 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; /** * 获取会话内容存档开启成员列表 @@ -73,8 +183,8 @@ public interface WxCpMsgAuditService { * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/msgaudit/get_permit_user_list?access_token=ACCESS_TOKEN * * @param type 拉取对应版本的开启成员列表。1表示办公版;2表示服务版;3表示企业版。非必填,不填写的时候返回全量成员列表。 - * @return - * @throws WxErrorException + * @return permit user list + * @throws WxErrorException the wx error exception */ List getPermitUserList(Integer type) throws WxErrorException; @@ -86,8 +196,8 @@ public interface WxCpMsgAuditService { * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/msgaudit/groupchat/get?access_token=ACCESS_TOKEN * * @param roomid 待查询的群id - * @return - * @throws WxErrorException + * @return group chat + * @throws WxErrorException the wx error exception */ WxCpGroupChat getGroupChat(@NonNull String roomid) throws WxErrorException; @@ -100,9 +210,25 @@ public interface WxCpMsgAuditService { * 请求方式:POST(HTTPS) * * @param checkAgreeRequest 待查询的会话信息 - * @return - * @throws WxErrorException + * @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 index 91010ce212..50d5e8d946 100644 --- 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 @@ -8,8 +8,7 @@ /** * 企业微信日历接口. * - * @author Binary Wang - * @date 2020-09-20 + * @author Binary Wang created on 2020-09-20 */ public interface WxCpOaCalendarService { /** @@ -24,7 +23,7 @@ public interface WxCpOaCalendarService { * * * @param calendar 日历对象 - * @return 日历ID + * @return 日历ID string * @throws WxErrorException . */ String add(WxCpOaCalendar calendar) throws WxErrorException; @@ -60,7 +59,7 @@ public interface WxCpOaCalendarService { * * * @param calIds 日历id列表 - * @return 日历对象列表 + * @return 日历对象列表 list * @throws WxErrorException . */ List get(List calIds) 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 index c5b75bce17..70c108a059 100644 --- 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 @@ -9,8 +9,7 @@ * 企业微信日程接口. * 官方文档:https://work.weixin.qq.com/api/doc/90000/90135/93648 * - * @author Binary Wang - * @date 2020 -12-25 + * @author Binary Wang created on 2020 -12-25 */ public interface WxCpOaScheduleService { /** 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 7eb986dbbb..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 @@ -2,6 +2,7 @@ import lombok.NonNull; import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.WxCpBaseResp; import me.chanjar.weixin.cp.bean.oa.*; import java.util.Date; @@ -10,8 +11,8 @@ /** * 企业微信OA相关接口. * - * @author Element - * @date 2019-04-06 10:52 + * @author Element, Wang_Wong + * @since 2019-04-06 10:52 */ public interface WxCpOaService { @@ -22,7 +23,7 @@ public interface WxCpOaService { * * 请求方式:POST(HTTPS) * 请求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent?access_token=ACCESS_TOKEN - * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/91853 + * 文档地址 * * * @param request 请求 @@ -34,7 +35,7 @@ public interface WxCpOaService { /** *
    *  获取打卡数据
-   *  API doc : https://work.weixin.qq.com/api/doc#90000/90135/90262
+   *  文档地址
    * 
* * @param openCheckinDataType 打卡类型。1:上下班打卡;2:外出打卡;3:全部打卡 @@ -50,7 +51,7 @@ List getCheckinData(Integer openCheckinDataType, Date startTime /** *
    *   获取打卡规则
-   *   API doc : https://work.weixin.qq.com/api/doc#90000/90135/90263
+   *  文档地址
    * 
* * @param datetime 需要获取规则的当天日期 @@ -64,10 +65,10 @@ List getCheckinData(Integer openCheckinDataType, Date startTime /** *
    *   获取企业所有打卡规则
-   *   API doc : https://work.weixin.qq.com/api/doc/90000/90135/93384
+   * 文档地址
    * 
* - * @return 打卡规则列表 + * @return 打卡规则列表 crop checkin option * @throws WxErrorException the wx error exception */ List getCropCheckinOption() throws WxErrorException; @@ -82,7 +83,7 @@ List getCheckinData(Integer openCheckinDataType, Date startTime * * 一次拉取调用最多拉取100个审批记录,可以通过多次拉取的方式来满足需求,但调用频率不可超过600次/分。 * - * API doc : https://work.weixin.qq.com/api/doc/90000/90135/91816 + * 文档地址 * * * @param startTime 开始时间 @@ -93,6 +94,7 @@ List getCheckinData(Integer openCheckinDataType, Date startTime * @return WxCpApprovalInfo approval info * @throws WxErrorException . */ + @Deprecated WxCpApprovalInfo getApprovalInfo(@NonNull Date startTime, @NonNull Date endTime, Integer cursor, Integer size, List filters) throws WxErrorException; @@ -103,17 +105,49 @@ WxCpApprovalInfo getApprovalInfo(@NonNull Date startTime, @NonNull Date endTime, * @param endTime 结束时间 * @return WxCpApprovalInfo approval info * @throws WxErrorException . - * @see me.chanjar.weixin.cp.api.WxCpOaService#getApprovalInfo me.chanjar.weixin.cp.api.WxCpOaService#getApprovalInfo + * @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调用本接口,根据审批单号查询企业微信“审批应用”的审批申请详情。
    *
-   *   API Doc : https://work.weixin.qq.com/api/doc/90000/90135/91983
+   *  文档地址
    * 
* * @param spNo 审批单编号。 @@ -122,6 +156,73 @@ WxCpApprovalInfo getApprovalInfo(@NonNull Date startTime, @NonNull Date endTime, */ 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 + */ + WxCpBaseResp setOneUserQuota(@NonNull String userId, @NonNull Integer vacationId, @NonNull Integer leftDuration, + @NonNull Integer timeAttr, String remarks) throws WxErrorException; + + /** * 获取公费电话拨打记录 * @@ -142,8 +243,41 @@ List getDialRecord(Date startTime, Date endTime, Integer offset, * @return . template detail * @throws WxErrorException . */ - WxCpTemplateResult getTemplateDetail(@NonNull String templateId) 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; /** * 获取打卡日报数据 @@ -163,7 +297,7 @@ List getDialRecord(Date startTime, Date endTime, Integer offset, * @param startTime 获取月报的开始时间 * @param endTime 获取月报的结束时间 * @param userIdList 获取月报的userid列表 - * @return 月报数据列表 + * @return 月报数据列表 checkin month data * @throws WxErrorException the wx error exception */ List getCheckinMonthData(Date startTime, Date endTime, List userIdList) throws WxErrorException; @@ -174,7 +308,7 @@ List getDialRecord(Date startTime, Date endTime, Integer offset, * @param startTime 获取排班信息的开始时间。Unix时间戳 * @param endTime 获取排班信息的结束时间。Unix时间戳(与starttime跨度不超过一个月) * @param userIdList 需要获取排班信息的用户列表(不超过100个) - * @return 排班表信息 + * @return 排班表信息 checkin schedule list * @throws WxErrorException the wx error exception */ List getCheckinScheduleList(Date startTime, Date endTime, List userIdList) throws WxErrorException; @@ -187,4 +321,21 @@ List getDialRecord(Date startTime, Date endTime, Integer offset, * @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 473fb3dfcb..269a69a475 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 @@ -38,7 +38,7 @@ public interface WxCpService extends WxService { * * @return the access token * @throws WxErrorException the wx error exception - * @see #getAccessToken(boolean) #getAccessToken(boolean)#getAccessToken(boolean) + * @see #getAccessToken(boolean) #getAccessToken(boolean)#getAccessToken(boolean)#getAccessToken(boolean) */ String getAccessToken() throws WxErrorException; @@ -57,12 +57,38 @@ public interface WxCpService extends WxService { */ String getAccessToken(boolean forceRefresh) throws WxErrorException; + /** + *

+   * 获取通讯录同步access_token,本方法线程安全
+   * 通讯录同步相关接口仅支持通过"通讯录同步secret"调用,需要使用独立的access_token
+   * 详情请见: https://developer.work.weixin.qq.com/document/path/91579
+   * 
+ * + * @param forceRefresh 强制刷新 + * @return 通讯录同步专用的access token + * @throws WxErrorException the wx error exception + */ + String getContactAccessToken(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 * * @return the jsapi ticket * @throws WxErrorException the wx error exception - * @see #getJsapiTicket(boolean) #getJsapiTicket(boolean)#getJsapiTicket(boolean) + * @see #getJsapiTicket(boolean) #getJsapiTicket(boolean)#getJsapiTicket(boolean)#getJsapiTicket(boolean) */ String getJsapiTicket() throws WxErrorException; @@ -89,7 +115,7 @@ public interface WxCpService extends WxService { * * @return the agent jsapi ticket * @throws WxErrorException the wx error exception - * @see #getJsapiTicket(boolean) #getJsapiTicket(boolean)#getJsapiTicket(boolean) + * @see #getJsapiTicket(boolean) #getJsapiTicket(boolean)#getJsapiTicket(boolean)#getJsapiTicket(boolean) */ String getAgentJsapiTicket() throws WxErrorException; @@ -134,7 +160,7 @@ public interface WxCpService extends WxService { * * @param url url * @return the agent jsapi signature - * @throws WxErrorException + * @throws WxErrorException the wx error exception */ WxCpAgentJsapiSignature createAgentJsapiSignature(String url) throws WxErrorException; @@ -149,7 +175,7 @@ public interface WxCpService extends WxService { /** *
-   * 获取微信服务器的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
    * 
* @@ -158,6 +184,17 @@ public interface WxCpService extends WxService { */ 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; + /** *
    * 获取服务商凭证
@@ -183,6 +220,45 @@ public interface WxCpService extends WxService {
    */
   String postWithoutToken(String url, String postData) throws WxErrorException;
 
+  /**
+   * 
+   * 使用会话存档access token发起post请求
+   * 会话存档相关API需要使用会话存档专用的secret获取独立的access token
+   * 
+ * + * @param url 接口地址 + * @param postData 请求body字符串 + * @return the string + * @throws WxErrorException the wx error exception + */ + String postForMsgAudit(String url, String postData) throws WxErrorException; + + /** + *
+   * 使用通讯录同步access token发起get请求
+   * 通讯录同步相关API需要使用通讯录同步专用的secret获取独立的access token
+   * 
+ * + * @param url 接口地址 + * @param queryParam 请求参数 + * @return the string + * @throws WxErrorException the wx error exception + */ + String getForContact(String url, String queryParam) throws WxErrorException; + + /** + *
+   * 使用通讯录同步access token发起post请求
+   * 通讯录同步相关API需要使用通讯录同步专用的secret获取独立的access token
+   * 
+ * + * @param url 接口地址 + * @param postData 请求body字符串 + * @return the string + * @throws WxErrorException the wx error exception + */ + String postForContact(String url, String postData) throws WxErrorException; + /** *
    * Service没有实现某个API的时候,可以用这个,
@@ -263,6 +339,15 @@ public interface WxCpService extends WxService {
    */
   String replaceParty(String mediaId) throws WxErrorException;
 
+  /**
+   * 上传用户列表,增量更新成员
+   *
+   * @param mediaId 媒体id
+   * @return jobId 异步任务id
+   * @throws WxErrorException the wx error exception
+   */
+  String syncUser(String mediaId) throws WxErrorException;
+
   /**
    * 上传用户列表覆盖企业号上的用户信息
    *
@@ -275,11 +360,11 @@ public interface WxCpService extends WxService {
   /**
    * 获取异步任务结果
    *
-   * @param joinId the join id
+   * @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请求对象
@@ -302,9 +387,10 @@ public interface WxCpService extends WxService {
 
   /**
    * 构造扫码登录链接 - 构造独立窗口登录二维码
+   *
    * @param redirectUri 重定向地址,需要进行UrlEncode
-   * @param state 用于保持请求和回调的状态,授权请求后原样带回给企业。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议企业带上该参数,可设置为简单的随机数加session进行校验
-   * @return .
+   * @param state       用于保持请求和回调的状态,授权请求后原样带回给企业。该参数可用于防止csrf攻击(跨站请求伪造攻击),建议企业带上该参数,可设置为简单的随机数加session进行校验
+   * @return . string
    */
   String buildQrConnectUrl(String redirectUri, String state);
 
@@ -386,12 +472,33 @@ public interface WxCpService extends WxService {
   WxCpMessageService getMessageService();
 
   /**
-   * Gets oa service.
+   * 获取OA相关接口的服务类对象.
    *
    * @return the oa service
    */
   WxCpOaService getOaService();
 
+  /**
+   * 获取家校应用复学码相关接口的服务类对象
+   *
+   * @return school service
+   */
+  WxCpSchoolService getSchoolService();
+
+  /**
+   * 获取家校沟通相关接口的服务类对象
+   *
+   * @return school user service
+   */
+  WxCpSchoolUserService getSchoolUserService();
+
+  /**
+   * 获取家校应用健康上报的服务类对象
+   *
+   * @return school health service
+   */
+  WxCpSchoolHealthService getSchoolHealthService();
+
   /**
    * 获取直播相关接口的服务类对象
    *
@@ -399,10 +506,31 @@ public interface WxCpService extends WxService {
    */
   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
+   * @return msg audit service
    */
   WxCpMsgAuditService getMsgAuditService();
 
@@ -413,6 +541,13 @@ public interface WxCpService extends WxService {
    */
   WxCpOaCalendarService getOaCalendarService();
 
+  /**
+   * 获取会议室相关接口的服务类对象
+   *
+   * @return the oa meetingroom service
+   */
+  WxCpOaMeetingRoomService getOaMeetingRoomService();
+
   /**
    * 获取日程相关接口的服务类对象
    *
@@ -437,7 +572,7 @@ public interface WxCpService extends WxService {
   /**
    * 获取微信客服服务
    *
-   * @return 微信客服服务
+   * @return 微信客服服务 kf service
    */
   WxCpKfService getKfService();
 
@@ -496,4 +631,46 @@ public interface WxCpService extends WxService {
    * @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 045264f7d0..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
@@ -26,7 +26,7 @@ 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;
@@ -51,7 +51,7 @@ public interface WxCpTagService {
   /**
    * 获得标签列表.
    *
-   * @return 标签列表
+   * @return 标签列表 list
    * @throws WxErrorException .
    */
   List listAll() throws WxErrorException;
@@ -60,7 +60,7 @@ public interface WxCpTagService {
    * 获取标签成员.
    *
    * @param tagId 标签ID
-   * @return 成员列表
+   * @return 成员列表 list
    * @throws WxErrorException .
    */
   List listUsersByTagId(String tagId) throws WxErrorException;
@@ -70,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;
@@ -81,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;
@@ -92,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 3d97cb9283..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 userIds 企业的成员ID列表 + * @param taskId 任务卡片ID * @param replaceName 替换文案 + * @throws WxErrorException the wx error exception */ 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/WxCpUserService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpUserService.java index ede813a0a5..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.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; @@ -33,7 +38,7 @@ public interface WxCpUserService { *
    * 获取部门成员详情
    * 请求方式:GET(HTTPS)
-   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token=ACCESS_TOKEN&department_id=DEPARTMENT_ID&fetch_child=FETCH_CHILD
+   * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token=ACCESS_TOKEN&department_id=DEPARTMENT_ID&fetch_child=FETCH_CHILD}
    *
    * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/90201
    * 
@@ -128,7 +133,8 @@ 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; @@ -168,6 +174,24 @@ public interface WxCpUserService { */ 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; + /** * 获取外部联系人详情. *
@@ -189,7 +213,7 @@ public interface WxCpUserService {
    * 获取加入企业二维码。
    *
    * 请求方式:GET(HTTPS)
-   * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/corp/get_join_qrcode?access_token=ACCESS_TOKEN&size_type=SIZE_TYPE
+   * {@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
    * 
@@ -199,4 +223,65 @@ public interface WxCpUserService { * @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 . + */ + 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 cccbe8648d..a3ec703ca4 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 @@ -5,12 +5,14 @@ import com.google.gson.JsonObject; import lombok.extern.slf4j.Slf4j; 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; @@ -36,30 +38,46 @@ /** * . * + * @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 WxCpLivingService livingService = new WxCpLivingServiceImpl(this); - private WxCpMsgAuditService msgAuditService = new WxCpMsgAuditServiceImpl(this); - private WxCpTaskCardService taskCardService = new WxCpTaskCardServiceImpl(this); - private WxCpExternalContactService externalContactService = new WxCpExternalContactServiceImpl(this); - private WxCpGroupRobotService groupRobotService = new WxCpGroupRobotServiceImpl(this); - private WxCpMessageService messageService = new WxCpMessageServiceImpl(this); - private WxCpOaCalendarService oaCalendarService = new WxCpOaCalendarServiceImpl(this); - private WxCpOaScheduleService oaScheduleService = new WxCpOaOaScheduleServiceImpl(this); - private WxCpAgentWorkBenchService workBenchService = new WxCpAgentWorkBenchServiceImpl(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的锁. */ @@ -75,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(); @@ -92,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; } } @@ -211,7 +232,23 @@ public WxCpMaJsCode2SessionResult jsCode2Session(String jsCode) throws WxErrorEx @Override public String[] getCallbackIp() throws WxErrorException { - String responseContent = get(this.configStorage.getApiUrl(GET_CALLBACK_IP), null); + 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()]; @@ -226,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(Tp.GET_PROVIDER_TOKEN), jsonObject.toString())); + return WxCpProviderToken.fromJson(this.post(this.configStorage.getApiUrl(Tp.GET_PROVIDER_TOKEN), + jsonObject.toString())); } @Override @@ -249,6 +287,12 @@ 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()); @@ -259,6 +303,39 @@ public String postWithoutToken(String url, String postData) throws WxErrorExcept 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); + } + + @Override + public String getForContact(String url, String queryParam) throws WxErrorException { + // 获取通讯录同步专用的access token + String contactAccessToken = getContactAccessToken(false); + // 拼接access_token参数 + String urlWithToken = url + (url.contains("?") ? "&" : "?") + "access_token=" + contactAccessToken; + if (queryParam != null && !queryParam.isEmpty()) { + urlWithToken = urlWithToken + "&" + queryParam; + } + // 使用executeNormal方法,不自动添加token + return this.executeNormal(SimpleGetRequestExecutor.create(this), urlWithToken, null); + } + + @Override + public String postForContact(String url, String postData) throws WxErrorException { + // 获取通讯录同步专用的access token + String contactAccessToken = getContactAccessToken(false); + // 拼接access_token参数 + String urlWithToken = url + (url.contains("?") ? "&" : "?") + "access_token=" + contactAccessToken; + // 使用executeNormal方法,不自动添加token + return this.executeNormal(SimplePostRequestExecutor.create(this), urlWithToken, postData); + } + /** * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求. */ @@ -297,6 +374,18 @@ public T execute(RequestExecutor executor, String uri, E data) thro throw new WxRuntimeException("微信服务端异常,超出重试次数"); } + /** + * 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); @@ -326,12 +415,12 @@ protected T executeInternal(RequestExecutor executor, String uri, E } 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()); + log.warn("\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uriWithAccessToken, dataForLog, e.getMessage()); throw new WxRuntimeException(e); } } @@ -407,6 +496,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(); @@ -415,22 +513,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", + 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; } @@ -480,11 +589,41 @@ 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; @@ -495,6 +634,11 @@ public WxCpOaCalendarService getOaCalendarService() { return this.oaCalendarService; } + @Override + public WxCpOaMeetingRoomService getOaMeetingRoomService() { + return this.oaMeetingRoomService; + } + @Override public WxCpGroupRobotService getGroupRobotService() { return groupRobotService; @@ -555,6 +699,11 @@ public WxCpMessageService getMessageService() { return this.messageService; } + /** + * Sets agent service. + * + * @param agentService the agent service + */ public void setAgentService(WxCpAgentService agentService) { this.agentService = agentService; } @@ -573,4 +722,35 @@ public WxCpKfService getKfService() { 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/WxCpAgentServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpAgentServiceImpl.java index 4dd661fbf0..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 @@ -3,6 +3,7 @@ import com.google.gson.JsonObject; import com.google.gson.reflect.TypeToken; import lombok.RequiredArgsConstructor; +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; @@ -10,6 +11,7 @@ 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; @@ -46,7 +48,7 @@ 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 = GsonParser.parse(responseContent); - if (jsonObject.get("errcode").getAsInt() != 0) { + if (jsonObject.get(WxConsts.ERR_CODE).getAsInt() != 0) { throw new WxErrorException(WxError.fromJson(responseContent, WxType.CP)); } } @@ -56,7 +58,7 @@ public List list() throws WxErrorException { String url = this.mainService.getWxCpConfigStorage().getApiUrl(AGENT_LIST); String responseContent = this.mainService.get(url, null); JsonObject jsonObject = GsonParser.parse(responseContent); - if (jsonObject.get("errcode").getAsInt() != 0) { + 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 index 8c778197ce..b0bbb38642 100644 --- 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 @@ -7,14 +7,13 @@ import me.chanjar.weixin.cp.api.WxCpService; import me.chanjar.weixin.cp.bean.WxCpAgentWorkBench; -import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.WorkBench.WORKBENCH_DATA_SET; -import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.WorkBench.WORKBENCH_TEMPLATE_GET; -import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.WorkBench.WORKBENCH_TEMPLATE_SET; +import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.WorkBench.*; /** + * 工作台自定义展示实现 + * * @author songshiyu - * @date : create in 11:24 2020/9/28 - * @description: 工作台自定义展示实现 + * created at 11:24 2020/9/28 */ @RequiredArgsConstructor public class WxCpAgentWorkBenchServiceImpl implements WxCpAgentWorkBenchService { @@ -39,4 +38,10 @@ public void setWorkBenchData(WxCpAgentWorkBench wxCpAgentWorkBench) throws WxErr 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 7783422af9..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 @@ -6,8 +6,8 @@ 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.message.WxCpAppChatMessage; import me.chanjar.weixin.cp.bean.WxCpChat; +import me.chanjar.weixin.cp.bean.message.WxCpAppChatMessage; import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; import org.apache.commons.lang3.StringUtils; 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 3a5ef87985..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 @@ -35,6 +35,18 @@ public Long create(WxCpDepart depart) throws WxErrorException { 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 public void update(WxCpDepart group) throws WxErrorException { String url = this.mainService.getWxCpConfigStorage().getApiUrl(DEPARTMENT_UPDATE); @@ -62,4 +74,20 @@ public List list(Long id) throws WxErrorException { }.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(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 3997f50768..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,10 +1,15 @@ 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; @@ -15,54 +20,64 @@ 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.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 org.jetbrains.annotations.NotNull; - -import java.util.Collections; -import java.util.Date; -import java.util.List; - -import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.ExternalContact.*; /** - * @author 曹祖鹏 & yuanqixun & Mr.Pan + * The type Wx cp external contact service. + * + * @author 曹祖鹏, yuanqixun, Mr.Pan, Wang_Wong */ @RequiredArgsConstructor public class WxCpExternalContactServiceImpl implements WxCpExternalContactService { private final WxCpService mainService; @Override - public WxCpContactWayResult addContactWay(@NonNull WxCpContactWayInfo info) throws WxErrorException { + public WxCpContactWayResult addContactWay(WxCpContactWayInfo info) throws WxErrorException { if (info.getContactWay().getUsers() != null && info.getContactWay().getUsers().size() > 100) { throw new WxRuntimeException("「联系我」使用人数默认限制不超过100人(包括部门展开后的人数)"); } final String url = this.mainService.getWxCpConfigStorage().getApiUrl(ADD_CONTACT_WAY); - String responseContent = this.mainService.post(url, info.getContactWay().toJson()); - return WxCpContactWayResult.fromJson(responseContent); + return WxCpContactWayResult.fromJson(this.mainService.post(url, info.getContactWay().toJson())); } @Override - public WxCpContactWayInfo getContactWay(@NonNull String configId) throws WxErrorException { + 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); - String responseContent = this.mainService.post(url, json.toString()); - return WxCpContactWayInfo.fromJson(responseContent); + 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(@NonNull WxCpContactWayInfo info) throws WxErrorException { + public WxCpBaseResp updateContactWay(WxCpContactWayInfo info) throws WxErrorException { if (StringUtils.isBlank(info.getContactWay().getConfigId())) { throw new WxRuntimeException("更新「联系我」方式需要指定configId"); } @@ -71,24 +86,22 @@ public WxCpBaseResp updateContactWay(@NonNull WxCpContactWayInfo info) throws Wx } final String url = this.mainService.getWxCpConfigStorage().getApiUrl(UPDATE_CONTACT_WAY); - String responseContent = this.mainService.post(url, info.getContactWay().toJson()); - return WxCpBaseResp.fromJson(responseContent); + return WxCpBaseResp.fromJson(this.mainService.post(url, info.getContactWay().toJson())); } @Override - public WxCpBaseResp deleteContactWay(@NonNull String configId) throws WxErrorException { + 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); - String responseContent = this.mainService.post(url, json.toString()); - return WxCpBaseResp.fromJson(responseContent); + return WxCpBaseResp.fromJson(this.mainService.post(url, json.toString())); } @Override - public WxCpBaseResp closeTempChat(@NonNull String userId, @NonNull String externalUserId) throws WxErrorException { + public WxCpBaseResp closeTempChat(String userId, String externalUserId) throws WxErrorException { JsonObject json = new JsonObject(); json.addProperty("userid", userId); @@ -96,73 +109,77 @@ public WxCpBaseResp closeTempChat(@NonNull String userId, @NonNull String extern final String url = this.mainService.getWxCpConfigStorage().getApiUrl(CLOSE_TEMP_CHAT); - String responseContent = this.mainService.post(url, json.toString()); - return WxCpBaseResp.fromJson(responseContent); + return WxCpBaseResp.fromJson(this.mainService.post(url, json.toString())); } @Override - public WxCpExternalContactInfo getExternalContact(String userId) throws WxErrorException { - final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_EXTERNAL_CONTACT + userId); - String responseContent = this.mainService.get(url, null); - return WxCpExternalContactInfo.fromJson(responseContent); + 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 userId, String cursor) throws WxErrorException { - String params = userId; - if(StringUtils.isNotEmpty(cursor)){ + 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); - String responseContent = this.mainService.get(url, null); - return WxCpExternalContactInfo.fromJson(responseContent); + return WxCpExternalContactInfo.fromJson(this.mainService.get(url, null)); } @Override - public String convertToOpenid(@NotNull String externalUserId) throws WxErrorException { + 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()); - JsonObject tmpJson = GsonParser.parse(responseContent); - return tmpJson.get("openid").getAsString(); + return GsonParser.parse(responseContent).get("openid").getAsString(); } @Override - public String unionidToExternalUserid(@NotNull String unionid,String openid) throws WxErrorException { + 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); + 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()); - JsonObject tmpJson = GsonParser.parse(responseContent); - return tmpJson.get("external_userid").getAsString(); + return GsonParser.parse(responseContent).get("external_userid").getAsString(); } @Override - public String toServiceExternalUserid(@NotNull String externalUserid) throws WxErrorException { + 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()); - JsonObject tmpJson = GsonParser.parse(responseContent); - return tmpJson.get("external_userid").getAsString(); + 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(@NotNull String unionid, @NotNull String openid, String corpid) throws WxErrorException { + 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); + if (StringUtils.isNotEmpty(corpid)) { + json.addProperty("corpid", corpid); } final String url = this.mainService.getWxCpConfigStorage().getApiUrl(UNIONID_TO_EXTERNAL_USERID_3RD); - String responseContent = this.mainService.post(url, json.toString()); - return WxCpExternalUserIdList.fromJson(responseContent); + return WxCpExternalUserIdList.fromJson(this.mainService.post(url, json.toString())); } @Override @@ -172,38 +189,30 @@ public WxCpNewExternalUserIdList getNewExternalUserId(String[] externalUserIdLis json.add("external_userid_list", new Gson().toJsonTree(externalUserIdList).getAsJsonArray()); } final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_NEW_EXTERNAL_USERID); - String responseContent = this.mainService.post(url, json.toString()); - return WxCpNewExternalUserIdList.fromJson(responseContent); + return WxCpNewExternalUserIdList.fromJson(this.mainService.post(url, json.toString())); } @Override - public WxCpBaseResp finishExternalUserIdMigration(@NotNull String corpid) throws WxErrorException { + 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); - String responseContent = this.mainService.post(url, json.toString()); - return WxCpBaseResp.fromJson(responseContent); + return WxCpBaseResp.fromJson(this.mainService.post(url, json.toString())); } @Override - public String opengidToChatid(@NotNull String opengid) throws WxErrorException { + public String opengidToChatid(String opengid) throws WxErrorException { JsonObject json = new JsonObject(); - json.addProperty("opengid",opengid); + json.addProperty("opengid", opengid); final String url = this.mainService.getWxCpConfigStorage().getApiUrl(OPENID_TO_CHATID); String responseContent = this.mainService.post(url, json.toString()); - JsonObject tmpJson = GsonParser.parse(responseContent); - return tmpJson.get("chat_id").getAsString(); + return GsonParser.parse(responseContent).get("chat_id").getAsString(); } @Override - public WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, - String cursor, - Integer limit) + public WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String cursor, Integer limit) throws WxErrorException { - final String url = - this.mainService - .getWxCpConfigStorage() - .getApiUrl(GET_CONTACT_DETAIL_BATCH); + 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)) { @@ -216,6 +225,20 @@ public WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, 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); @@ -245,13 +268,15 @@ public List listFollowers() throws WxErrorException { } @Override - public WxCpUserExternalUnassignList listUnassignedList(Integer pageIndex, Integer pageSize) throws WxErrorException { + public WxCpUserExternalUnassignList listUnassignedList(Integer pageIndex, String cursor, Integer pageSize) throws WxErrorException { JsonObject json = new JsonObject(); - json.addProperty("page_id", pageIndex == null ? 0 : pageIndex); - json.addProperty("page_size", pageSize == null ? 100 : pageSize); + 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); - final String result = this.mainService.post(url, json.toString()); - return WxCpUserExternalUnassignList.fromJson(result); + return WxCpUserExternalUnassignList.fromJson(this.mainService.post(url, json.toString())); } @Override @@ -261,8 +286,7 @@ public WxCpBaseResp transferExternalContact(String externalUserid, String handOv json.addProperty("handover_userid", handOverUserid); json.addProperty("takeover_userid", takeOverUserid); final String url = this.mainService.getWxCpConfigStorage().getApiUrl(TRANSFER_UNASSIGNED_CONTACT); - final String result = this.mainService.post(url, json.toString()); - return WxCpBaseResp.fromJson(result); + return WxCpBaseResp.fromJson(this.mainService.post(url, json.toString())); } @Override @@ -274,37 +298,39 @@ public WxCpUserTransferCustomerResp transferCustomer(WxCpUserTransferCustomerReq } @Override - public WxCpUserTransferResultResp transferResult(@NotNull String handOverUserid, @NotNull String takeOverUserid, String cursor) throws WxErrorException { + 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); - final String result = this.mainService.post(url, json.toString()); - return WxCpUserTransferResultResp.fromJson(result); + return WxCpUserTransferResultResp.fromJson(this.mainService.post(url, json.toString())); } @Override - public WxCpUserTransferCustomerResp resignedTransferCustomer(WxCpUserTransferCustomerReq req) throws WxErrorException { + public WxCpUserTransferCustomerResp resignedTransferCustomer(WxCpUserTransferCustomerReq req) + throws WxErrorException { BeanUtils.checkRequiredFields(req); final String url = this.mainService.getWxCpConfigStorage().getApiUrl(RESIGNED_TRANSFER_CUSTOMER); - final String result = this.mainService.post(url, req.toJson()); - return WxCpUserTransferCustomerResp.fromJson(result); + return WxCpUserTransferCustomerResp.fromJson(this.mainService.post(url, req.toJson())); } @Override - public WxCpUserTransferResultResp resignedTransferResult(@NotNull String handOverUserid, @NotNull String takeOverUserid, String cursor) throws WxErrorException { + 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); - final String result = this.mainService.post(url, json.toString()); - return WxCpUserTransferResultResp.fromJson(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 { + 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); @@ -320,8 +346,7 @@ public WxCpUserExternalGroupChatList listGroupChat(Integer pageIndex, Integer pa json.add("owner_filter", ownerFilter); } final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GROUP_CHAT_LIST); - final String result = this.mainService.post(url, json.toString()); - return WxCpUserExternalGroupChatList.fromJson(result); + return WxCpUserExternalGroupChatList.fromJson(this.mainService.post(url, json.toString())); } @Override @@ -338,8 +363,7 @@ public WxCpUserExternalGroupChatList listGroupChat(Integer limit, String cursor, json.add("owner_filter", ownerFilter); } final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GROUP_CHAT_LIST); - final String result = this.mainService.post(url, json.toString()); - return WxCpUserExternalGroupChatList.fromJson(result); + return WxCpUserExternalGroupChatList.fromJson(this.mainService.post(url, json.toString())); } @Override @@ -348,8 +372,7 @@ public WxCpUserExternalGroupChatInfo getGroupChat(String chatId, Integer needNam json.addProperty("chat_id", chatId); json.addProperty("need_name", needName); final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GROUP_CHAT_INFO); - final String result = this.mainService.post(url, json.toString()); - return WxCpUserExternalGroupChatInfo.fromJson(result); + return WxCpUserExternalGroupChatInfo.fromJson(this.mainService.post(url, json.toString())); } @Override @@ -360,12 +383,23 @@ public WxCpUserExternalGroupChatTransferResp transferGroupChat(String[] chatIds, } json.addProperty("new_owner", newOwner); final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GROUP_CHAT_TRANSFER); - final String result = this.mainService.post(url, json.toString()); - return WxCpUserExternalGroupChatTransferResp.fromJson(result); + return WxCpUserExternalGroupChatTransferResp.fromJson(this.mainService.post(url, json.toString())); } @Override - public WxCpUserExternalUserBehaviorStatistic getUserBehaviorStatistic(Date startTime, Date endTime, String[] userIds, String[] partyIds) throws WxErrorException { + 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); @@ -378,12 +412,13 @@ public WxCpUserExternalUserBehaviorStatistic getUserBehaviorStatistic(Date start } } final String url = this.mainService.getWxCpConfigStorage().getApiUrl(LIST_USER_BEHAVIOR_DATA); - final String result = this.mainService.post(url, json.toString()); - return WxCpUserExternalUserBehaviorStatistic.fromJson(result); + 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 { + 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); @@ -401,15 +436,31 @@ public WxCpUserExternalGroupChatStatistic getGroupChatStatistic(Date startTime, json.add("owner_filter", ownerFilter); } final String url = this.mainService.getWxCpConfigStorage().getApiUrl(LIST_GROUP_CHAT_DATA); - final String result = this.mainService.post(url, json.toString()); - return WxCpUserExternalGroupChatStatistic.fromJson(result); + 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); - final String result = this.mainService.post(url, wxCpMsgTemplate.toJson()); - return WxCpMsgTemplateAddResult.fromJson(result); + 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 @@ -425,8 +476,7 @@ public WxCpUserExternalTagGroupList getCorpTagList(String[] tagId) throws WxErro json.add("tag_id", new Gson().toJsonTree(tagId).getAsJsonArray()); } final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_CORP_TAG_LIST); - final String result = this.mainService.post(url, json.toString()); - return WxCpUserExternalTagGroupList.fromJson(result); + return WxCpUserExternalTagGroupList.fromJson(this.mainService.post(url, json.toString())); } @Override @@ -439,16 +489,14 @@ public WxCpUserExternalTagGroupList getCorpTagList(String[] tagId, String[] grou json.add("group_id", new Gson().toJsonTree(groupId).getAsJsonArray()); } final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_CORP_TAG_LIST); - final String result = this.mainService.post(url, json.toString()); - return WxCpUserExternalTagGroupList.fromJson(result); + 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); - final String result = this.mainService.post(url, tagGroup.getTagGroup().toJson()); - return WxCpUserExternalTagGroupInfo.fromJson(result); + return WxCpUserExternalTagGroupInfo.fromJson(this.mainService.post(url, tagGroup.getTagGroup().toJson())); } @Override @@ -459,8 +507,7 @@ public WxCpBaseResp editCorpTag(String id, String name, Integer order) throws Wx json.addProperty("name", name); json.addProperty("order", order); final String url = this.mainService.getWxCpConfigStorage().getApiUrl(EDIT_CORP_TAG); - final String result = this.mainService.post(url, json.toString()); - return WxCpBaseResp.fromJson(result); + return WxCpBaseResp.fromJson(this.mainService.post(url, json.toString())); } @Override @@ -474,14 +521,11 @@ public WxCpBaseResp delCorpTag(String[] tagId, String[] groupId) throws WxErrorE } final String url = this.mainService.getWxCpConfigStorage().getApiUrl(DEL_CORP_TAG); - final String result = this.mainService.post(url, json.toString()); - return WxCpBaseResp.fromJson(result); + 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); @@ -494,46 +538,50 @@ public WxCpBaseResp markTag(String userid, String externalUserid, String[] addTa } final String url = this.mainService.getWxCpConfigStorage().getApiUrl(MARK_TAG); - final String result = this.mainService.post(url, json.toString()); - return WxCpBaseResp.fromJson(result); + 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); - final String result = this.mainService.post(url, task.toJson()); - return WxCpAddMomentResult.fromJson(result); + 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); - final String result = this.mainService.get(url, params); - return WxCpGetMomentTaskResult.fromJson(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 { + 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) { + if (filterType != null) { json.addProperty("filter_type", filterType); } if (!StringUtils.isEmpty(cursor)) { json.addProperty("cursor", cursor); } - if (limit!=null) { + if (limit != null) { json.addProperty("limit", limit); } final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_MOMENT_LIST); - final String result = this.mainService.post(url, json.toString()); - return WxCpGetMomentList.fromJson(result); + return WxCpGetMomentList.fromJson(this.mainService.post(url, json.toString())); } @Override @@ -544,46 +592,43 @@ public WxCpGetMomentTask getMomentTask(String momentId, String cursor, Integer l if (!StringUtils.isEmpty(cursor)) { json.addProperty("cursor", cursor); } - if (limit!=null) { + if (limit != null) { json.addProperty("limit", limit); } final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_MOMENT_TASK); - final String result = this.mainService.post(url, json.toString()); - return WxCpGetMomentTask.fromJson(result); + return WxCpGetMomentTask.fromJson(this.mainService.post(url, json.toString())); } @Override public WxCpGetMomentCustomerList getMomentCustomerList(String momentId, String userId, - String cursor, Integer limit) throws WxErrorException { + 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) { + if (limit != null) { json.addProperty("limit", limit); } final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_MOMENT_CUSTOMER_LIST); - final String result = this.mainService.post(url, json.toString()); - return WxCpGetMomentCustomerList.fromJson(result); + return WxCpGetMomentCustomerList.fromJson(this.mainService.post(url, json.toString())); } @Override public WxCpGetMomentSendResult getMomentSendResult(String momentId, String userId, - String cursor, Integer limit) throws WxErrorException { + 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) { + if (limit != null) { json.addProperty("limit", limit); } final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_MOMENT_SEND_RESULT); - final String result = this.mainService.post(url, json.toString()); - return WxCpGetMomentSendResult.fromJson(result); + return WxCpGetMomentSendResult.fromJson(this.mainService.post(url, json.toString())); } @Override @@ -593,28 +638,12 @@ public WxCpGetMomentComments getMomentComments(String momentId, String userId) json.addProperty("moment_id", momentId); json.addProperty("userid", userId); final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_MOMENT_COMMENTS); - final String result = this.mainService.post(url, json.toString()); - return WxCpGetMomentComments.fromJson(result); - } - - /** - *

-   * 企业和第三方应用可通过此接口获取企业与成员的群发记录。
-   * https://work.weixin.qq.com/api/doc/90000/90135/93338
-   * 
- * - * @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 - */ - @Override - public WxCpGroupMsgListResult getGroupMsgListV2(String chatType, @NonNull Date startTime, @NonNull Date endTime, String creator, Integer filterType, Integer limit, String cursor) throws WxErrorException { + 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); @@ -625,23 +654,9 @@ public WxCpGroupMsgListResult getGroupMsgListV2(String chatType, @NonNull Date s json.addProperty("cursor", cursor); final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_GROUP_MSG_LIST_V2); - final String result = this.mainService.post(url, json.toString()); - return WxCpGroupMsgListResult.fromJson(result); - } - - /** - *
-   * 企业和第三方应用可通过此接口获取企业与成员的群发记录。
-   * 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 - */ + 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(); @@ -651,22 +666,9 @@ public WxCpGroupMsgSendResult getGroupMsgSendResult(String msgid, String userid, json.addProperty("cursor", cursor); final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_GROUP_MSG_SEND_RESULT); - final String result = this.mainService.post(url, json.toString()); - return WxCpGroupMsgSendResult.fromJson(result); - } - - /** - *
-   * 企业跟第三方应用可通过该接口获取到创建企业群发的群发发送结果。
-   * https://work.weixin.qq.com/api/doc/16251
-   * 
- * - * @param msgid 群发消息的id,通过创建企业群发接口返回 - * @param limit 返回的最大记录数,整型,最大值10000,默认值10000 - * @param cursor 用于分页查询的游标,字符串类型,由上一次调用返回,首次调用可不填 - * @return wx cp base resp - * @throws WxErrorException the wx error exception - */ + 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(); @@ -675,155 +677,71 @@ public WxCpGroupMsgResult getGroupMsgResult(String msgid, Integer limit, String json.addProperty("cursor", cursor); final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_GROUP_MSG_RESULT); - final String result = this.mainService.post(url, json.toString()); - return WxCpGroupMsgResult.fromJson(result); - } - - /** - *
-   * 获取群发成员发送任务列表。
-   * 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 - */ + return WxCpGroupMsgResult.fromJson(this.mainService.post(url, json.toString())); + } + @Override public WxCpGroupMsgTaskResult getGroupMsgTask(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_TASK); - final String result = this.mainService.post(url, json.toString()); - return WxCpGroupMsgTaskResult.fromJson(result); + return WxCpGroupMsgTaskResult.fromJson(this.mainService.post(url, + GsonHelper.buildJsonObject("msgid", msgid, + "limit", limit, + "cursor", cursor))); } - /** - *
-   * 添加入群欢迎语素材。
-   * https://open.work.weixin.qq.com/api/doc/90000/90135/92366#添加入群欢迎语素材
-   * 
- * - * @param template 素材内容 - * @return template_id 欢迎语素材id - * @throws WxErrorException the wx error exception - */ @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()); - JsonObject tmpJson = GsonParser.parse(responseContent); - return tmpJson.get("template_id").getAsString(); + return GsonParser.parse(responseContent).get("template_id").getAsString(); } - /** - *
-   * 编辑入群欢迎语素材。
-   * https://open.work.weixin.qq.com/api/doc/90000/90135/92366#编辑入群欢迎语素材
-   * 
- * - * @param template - * @return wx cp base resp - * @throws WxErrorException the wx error exception - */ @Override public WxCpBaseResp editGroupWelcomeTemplate(WxCpGroupWelcomeTemplateResult template) throws WxErrorException { final String url = this.mainService.getWxCpConfigStorage().getApiUrl(GROUP_WELCOME_TEMPLATE_EDIT); - final String result = this.mainService.post(url, template.toJson()); - return WxCpGroupWelcomeTemplateResult.fromJson(result); + return WxCpGroupWelcomeTemplateResult.fromJson(this.mainService.post(url, template.toJson())); } - /** - *
-   * 获取入群欢迎语素材。
-   * 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 - */ @Override - public WxCpGroupWelcomeTemplateResult getGroupWelcomeTemplate(@NotNull String templateId) throws WxErrorException { + 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); - final String result = this.mainService.post(url, json.toString()); - return WxCpGroupWelcomeTemplateResult.fromJson(result); + return WxCpGroupWelcomeTemplateResult.fromJson(this.mainService.post(url, json.toString())); } - /** - *
-   * 删除入群欢迎语素材。
-   * 企业可通过此API删除入群欢迎语素材,且仅能删除调用方自己创建的入群欢迎语素材。
-   * https://open.work.weixin.qq.com/api/doc/90000/90135/92366#删除入群欢迎语素材
-   * 
- * - * @param templateId 群欢迎语的素材id - * @param agentId - * @return wx cp base resp - * @throws WxErrorException the wx error exception - */ @Override - public WxCpBaseResp delGroupWelcomeTemplate(@NotNull String templateId, String agentId) throws WxErrorException { + 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); - final String result = this.mainService.post(url, json.toString()); - return WxCpBaseResp.fromJson(result); - } - - /** - *
-   * 获取商品图册
-   * 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 - */ + 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); - final String result = this.mainService.post(url, json.toString()); - return WxCpProductAlbumListResult.fromJson(result); + return WxCpProductAlbumListResult.fromJson(this.mainService.post(url, json.toString())); } - /** - *
-   * 获取商品图册
-   * https://work.weixin.qq.com/api/doc/90000/90135/95096#获取商品图册
-   * 
- * - * @param productId 商品id - * @return wx cp base resp - * @throws WxErrorException the wx error exception - */ @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); - final String result = this.mainService.post(url, json.toString()); - return WxCpProductAlbumResult.fromJson(result); + return WxCpProductAlbumResult.fromJson(this.mainService.post(url, json.toString())); } @Override public WxMediaUploadResult uploadAttachment(String mediaType, String fileType, Integer attachmentType, - InputStream inputStream) throws WxErrorException, IOException { + InputStream inputStream) throws WxErrorException, IOException { return uploadAttachment(mediaType, attachmentType, FileUtils.createTmpFile(inputStream, UUID.randomUUID().toString(), fileType)); } @@ -836,4 +754,166 @@ public WxMediaUploadResult uploadAttachment(String mediaType, Integer attachment 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 index 9661adf478..8373c6c8ee 100644 --- 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 @@ -1,7 +1,6 @@ package me.chanjar.weixin.cp.api.impl; import lombok.RequiredArgsConstructor; -import me.chanjar.weixin.common.error.WxError; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.cp.api.WxCpGroupRobotService; import me.chanjar.weixin.cp.api.WxCpService; @@ -14,14 +13,11 @@ import java.util.List; import static me.chanjar.weixin.cp.constant.WxCpConsts.GroupRobotMsgType; -import static me.chanjar.weixin.cp.constant.WxCpConsts.GroupRobotMsgType.MARKDOWN; -import static me.chanjar.weixin.cp.constant.WxCpConsts.GroupRobotMsgType.TEXT; /** * 企业微信群机器人消息发送api 实现 * - * @author yr - * @date 2020-08-20 + * @author yr created on 2020-08-20 */ @RequiredArgsConstructor public class WxCpGroupRobotServiceImpl implements WxCpGroupRobotService { @@ -46,6 +42,11 @@ 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); @@ -59,7 +60,7 @@ public void sendNews(List articleList) throws WxErrorException { @Override public void sendText(String webhookUrl, String content, List mentionedList, List mobileList) throws WxErrorException { this.cpService.postWithoutToken(webhookUrl, new WxCpGroupRobotMessage() - .setMsgType(TEXT) + .setMsgType(GroupRobotMsgType.TEXT) .setContent(content) .setMentionedList(mentionedList) .setMentionedMobileList(mobileList) @@ -69,7 +70,15 @@ public void sendText(String webhookUrl, String content, List mentionedLi @Override public void sendMarkdown(String webhookUrl, String content) throws WxErrorException { this.cpService.postWithoutToken(webhookUrl, new WxCpGroupRobotMessage() - .setMsgType(MARKDOWN) + .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()); } @@ -89,4 +98,24 @@ public void sendNews(String webhookUrl, List articleList) throws WxE .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 index 12f1062b0c..be4f2a5850 100644 --- 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 @@ -4,36 +4,22 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonObject; -import java.util.List; 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.WxCpKfAccountAdd; -import me.chanjar.weixin.cp.bean.kf.WxCpKfAccountAddResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfAccountDel; -import me.chanjar.weixin.cp.bean.kf.WxCpKfAccountLink; -import me.chanjar.weixin.cp.bean.kf.WxCpKfAccountLinkResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfAccountListResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfAccountUpd; -import me.chanjar.weixin.cp.bean.kf.WxCpKfCustomerBatchGetResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfMsgListResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfMsgSendRequest; -import me.chanjar.weixin.cp.bean.kf.WxCpKfMsgSendResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfServiceStateResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfServiceStateTransResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfServicerListResp; -import me.chanjar.weixin.cp.bean.kf.WxCpKfServicerOpResp; +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 - * @date 2022/1/19 19:41 + * @author Fu created on 2022/1/19 19:41 */ @RequiredArgsConstructor public class WxCpKfServiceImpl implements WxCpKfService { @@ -62,9 +48,16 @@ public WxCpBaseResp delAccount(WxCpKfAccountDel del) throws WxErrorException { } @Override - public WxCpKfAccountListResp listAccount() throws WxErrorException { + public WxCpKfAccountListResp listAccount(Integer offset, Integer limit) throws WxErrorException { String url = cpService.getWxCpConfigStorage().getApiUrl(ACCOUNT_LIST); - String responseContent = cpService.post(url, "{}"); + 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); } @@ -77,23 +70,62 @@ public WxCpKfAccountLinkResp getAccountLink(WxCpKfAccountLink link) throws WxErr @Override public WxCpKfServicerOpResp addServicer(String openKfid, List userIdList) throws WxErrorException { - return servicerOp(openKfid, userIdList, SERVICER_ADD); + 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, SERVICER_DEL); + 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, String uri) throws WxErrorException { + 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); - JsonArray userIdArray = new JsonArray(); - userIdList.forEach(userIdArray::add); - json.add("userid_list", userIdArray); - + 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); } @@ -120,7 +152,7 @@ public WxCpKfServiceStateResp getServiceState(String openKfid, String externalUs @Override public WxCpKfServiceStateTransResp transServiceState(String openKfid, String externalUserId, - Integer serviceState, String servicerUserId) throws WxErrorException { + Integer serviceState, String servicerUserId) throws WxErrorException { String url = cpService.getWxCpConfigStorage().getApiUrl(SERVICE_STATE_TRANS); JsonObject json = new JsonObject(); @@ -138,6 +170,28 @@ public WxCpKfMsgListResp syncMsg(String cursor, String token, Integer limit, Int 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); @@ -151,6 +205,9 @@ public WxCpKfMsgListResp syncMsg(String cursor, String token, Integer limit, Int 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); @@ -188,4 +245,76 @@ public WxCpKfCustomerBatchGetResp customerBatchGet(List externalUserIdLi 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 index 5fdf18cf88..b3d9e9a36e 100644 --- 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 @@ -12,14 +12,15 @@ 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 - * @date 2021-12-21 + * @author Wang_Wong created on 2021-12-21 */ @Slf4j @RequiredArgsConstructor @@ -48,11 +49,11 @@ public WxCpLivingInfo getLivingInfo(String livingId) throws WxErrorException { } @Override - public WxCpWatchStat getWatchStat(String livingId, Integer nextKey) throws WxErrorException { + public WxCpWatchStat getWatchStat(String livingId, String nextKey) throws WxErrorException { String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_WATCH_STAT); JsonObject jsonObject = new JsonObject(); - if (nextKey != null) { - jsonObject.addProperty("next_key", String.valueOf(nextKey)); + if (StringUtils.isNotBlank(nextKey)) { + jsonObject.addProperty("next_key", nextKey); } jsonObject.addProperty("livingid", livingId); String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); 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 8e88aa20ea..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,24 +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.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.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; /** *
@@ -39,7 +52,8 @@ public WxMediaUploadResult upload(String mediaType, String fileType, InputStream
   }
 
   @Override
-  public WxMediaUploadResult upload(String mediaType, String filename, String url) throws WxErrorException, IOException {
+  public WxMediaUploadResult upload(String mediaType, String filename, String url) throws WxErrorException,
+    IOException {
     HttpURLConnection conn = null;
     InputStream inputStream = null;
     try {
@@ -50,20 +64,39 @@ public WxMediaUploadResult upload(String mediaType, String filename, String url)
       //防止屏蔽程序抓取而返回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));
+      return this.mainService.execute(MediaInputStreamUploadRequestExecutor.create(this.mainService.getRequestHttp())
+        , this.mainService.getWxCpConfigStorage().getApiUrl(MEDIA_UPLOAD + mediaType),
+        new InputStreamData(inputStream, filename));
     } finally {
-      if (inputStream != null) {
-        try {
-          inputStream.close();
-        } catch (IOException e) {
-        }
-      }
+      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()),
@@ -92,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/WxCpMessageServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMessageServiceImpl.java
index 9be2f60dfe..6daea8ef2f 100644
--- 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
@@ -1,6 +1,7 @@
 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;
@@ -12,8 +13,7 @@
 /**
  * 消息推送接口实现类.
  *
- * @author Binary Wang
- * @date 2020-08-30
+ * @author Binary Wang created on  2020-08-30
  */
 @RequiredArgsConstructor
 public class WxCpMessageServiceImpl implements WxCpMessageService {
@@ -46,4 +46,23 @@ public WxCpLinkedCorpMessageSendResult sendLinkedCorpMessage(WxCpLinkedCorpMessa
     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
index 6e47a4648c..be6588bc7b 100644
--- 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
@@ -11,40 +11,112 @@
 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
- * @date 2022-01-17
+ * @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 {
-    String configPath = cpService.getWxCpConfigStorage().getMsgAuditLibPath();
+  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("\\", "/"); - // 所有的后缀文件 - String suffixFiles = replacePath.substring(replacePath.lastIndexOf("/") + 1); - // 获取的前缀路径 - String prefixPath = replacePath.substring(0, replacePath.lastIndexOf("/") + 1); + // 获取最后一个斜杠的下标,用作分割路径 + int lastIndex = replacePath.lastIndexOf("/") + 1; + // 获取完整路径的前缀路径 + String prefixPath = replacePath.substring(0, lastIndex); + // 获取后缀的所有文件,目的遍历所有文件 + String suffixFiles = replacePath.substring(lastIndex); // 包含so文件 String[] libFiles = suffixFiles.split(","); @@ -52,56 +124,92 @@ public WxCpChatDatas getChatDatas(long seq, @NonNull long limit, String proxy, S throw new WxErrorException("请仔细配置会话存档文件路径!!"); } - Finance.loadingLibraries(libFiles, prefixPath); - long sdk = Finance.SingletonSDK(); + 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); - long ret = Finance.Init(sdk, cpService.getWxCpConfigStorage().getCorpId(), cpService.getWxCpConfigStorage().getCorpSecret()); + 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.DestroySingletonSDK(sdk); + Finance.DestroySdk(sdk); throw new WxErrorException("init sdk err ret " + ret); } + return sdk; + } - long slice = Finance.NewSlice(); - ret = Finance.GetChatData(sdk, seq, limit, proxy, passwd, timeout, slice); - if (ret != 0) { - Finance.FreeSlice(slice); - Finance.DestroySingletonSDK(sdk); - throw new WxErrorException("getchatdata err ret " + ret); + @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(); + } - // 拉取会话存档 - String content = Finance.GetContentFromSlice(slice); - Finance.FreeSlice(slice); - WxCpChatDatas chatDatas = WxCpChatDatas.fromJson(content); - if (chatDatas.getErrCode().intValue() != 0) { - throw new WxErrorException(chatDatas.toJson()); + @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); + } } - - return chatDatas; + threadLocalSdk.remove(); } @Override - public WxCpChatModel getDecryptData(@NonNull WxCpChatDatas.WxCpChatData chatData) throws Exception { - String plainText = this.decryptChatData(chatData); + 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); } - public String decryptChatData(WxCpChatDatas.WxCpChatData chatData) throws Exception { - // 企业获取的会话内容将用公钥加密,企业可用自行保存的私钥解开会话内容数据,aeskey不能为空 - String priKey = cpService.getWxCpConfigStorage().getAesKey(); + /** + * 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("请配置会话存档私钥【aesKey】"); + throw new WxErrorException("请配置会话存档私钥【msgAuditPriKey】"); } - String decryptByPriKey = WxCpCryptUtil.decryptByPriKey(chatData.getEncryptRandomKey(), priKey); + String decryptByPriKey = WxCpCryptUtil.decryptPriKey(chatData.getEncryptRandomKey(), priKey, pkcs1); // 每次使用DecryptData解密会话存档前需要调用NewSlice获取一个slice,在使用完slice中数据后,还需要调用FreeSlice释放。 - long sdk = Finance.SingletonSDK(); 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); - Finance.DestroySingletonSDK(sdk); throw new WxErrorException("msg err ret " + ret); } @@ -112,44 +220,63 @@ public String decryptChatData(WxCpChatDatas.WxCpChatData chatData) throws Except } @Override - public String getChatPlainText(WxCpChatDatas.@NonNull WxCpChatData chatData) throws Exception { - return this.decryptChatData(chatData); + 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 String sdkfileid, String proxy, String passwd, @NonNull long timeout, @NonNull String targetFilePath) throws WxErrorException { + 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即可。 + * 3、indexbuf一般格式如右侧所示,”Range:bytes=524288-1048575“:表示这次拉取的是从524288到1048575的分片。单个文件首次拉取填写的indexbuf + * 为空字符串,拉取后续分片时直接填入上次返回的indexbuf即可。 */ File targetFile = new File(targetFilePath); - if (!targetFile.getParentFile().exists()) { - targetFile.getParentFile().mkdirs(); + 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(); - long sdk = Finance.SingletonSDK(); ret = Finance.GetMediaData(sdk, indexbuf, sdkfileid, proxy, passwd, timeout, mediaData); if (ret != 0) { Finance.FreeMediaData(mediaData); - Finance.DestroySingletonSDK(sdk); throw new WxErrorException("getmediadata err ret " + ret); } data_len += Finance.GetDataLen(mediaData); - log.info("正在分片拉取媒体文件 len:{}, data_len:{}, is_finis:{} \n", Finance.GetIndexLen(mediaData), data_len, Finance.IsMediaDataFinish(mediaData)); + log.debug("正在分片拉取媒体文件 len:{}, data_len:{}, is_finish:{} \n", Finance.GetIndexLen(mediaData), data_len, Finance.IsMediaDataFinish(mediaData)); try { // 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。 - FileOutputStream outputStream = new FileOutputStream(new File(targetFilePath), true); - outputStream.write(Finance.GetData(mediaData)); - outputStream.close(); + action.accept(Finance.GetData(mediaData)); } catch (Exception e) { - e.printStackTrace(); + log.error("处理媒体文件分片失败,sdkfileid={}", sdkfileid, e); } if (Finance.IsMediaDataFinish(mediaData) == 1) { @@ -171,7 +298,7 @@ public List getPermitUserList(Integer type) throws WxErrorException { if (type != null) { jsonObject.addProperty("type", type); } - String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + String responseContent = this.cpService.postForMsgAudit(apiUrl, jsonObject.toString()); return WxCpGsonBuilder.create().fromJson(GsonParser.parse(responseContent).getAsJsonArray("ids"), new TypeToken>() { }.getType()); @@ -182,15 +309,106 @@ public WxCpGroupChat getGroupChat(@NonNull String roomid) throws WxErrorExceptio final String apiUrl = this.cpService.getWxCpConfigStorage().getApiUrl(GET_GROUP_CHAT); JsonObject jsonObject = new JsonObject(); jsonObject.addProperty("roomid", roomid); - String responseContent = this.cpService.post(apiUrl, jsonObject.toString()); + 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.post(apiUrl, checkAgreeRequest.toJson()); + 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 8f989f23d8..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 @@ -10,8 +10,11 @@ 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.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.cp.constant.WxCpApiPathConsts.OAuth2.*; @@ -67,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); + 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(); } @@ -84,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 index 7e604934b9..ef24204493 100644 --- 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 @@ -17,8 +17,7 @@ /** * . * - * @author Binary Wang - * @date 2020-09-20 + * @author Binary Wang created on 2020-09-20 */ @RequiredArgsConstructor public class WxCpOaCalendarServiceImpl implements WxCpOaCalendarService { 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 index ca33f7c66c..c9a6161b2e 100644 --- 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 @@ -21,8 +21,7 @@ /** * 企业微信日程接口实现类. * - * @author Binary Wang - * @date 2020-12-25 + * @author Binary Wang created on 2020-12-25 */ @Slf4j @RequiredArgsConstructor 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 3e8277a850..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 @@ -11,8 +11,10 @@ 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.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,8 +24,7 @@ /** * 企业微信 OA 接口实现 * - * @author Element - * @date 2019-04-06 11:20 + * @author Element created on 2019-04-06 11:20 */ @RequiredArgsConstructor public class WxCpOaServiceImpl implements WxCpOaService { @@ -40,7 +41,8 @@ public String apply(WxCpOaApplyEventRequest request) throws WxErrorException { } @Override - public List getCheckinData(Integer openCheckinDataType, @NonNull Date startTime, @NonNull Date endTime, + 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 + " 个,请分批获取"); @@ -138,7 +140,7 @@ public WxCpApprovalInfo getApprovalInfo(@NonNull Date startTime, @NonNull Date e if (filters != null && !filters.isEmpty()) { JsonArray filterJsonArray = new JsonArray(); for (WxCpApprovalInfoQueryFilter filter : filters) { - filterJsonArray.add(new JsonParser().parse(filter.toJson())); + filterJsonArray.add(JsonParser.parseString(filter.toJson())); } jsonObject.add("filters", filterJsonArray); } @@ -151,7 +153,43 @@ public WxCpApprovalInfo getApprovalInfo(@NonNull Date startTime, @NonNull Date e @Override public WxCpApprovalInfo getApprovalInfo(@NonNull Date startTime, @NonNull Date endTime) throws WxErrorException { - return this.getApprovalInfo(startTime, endTime, null, null, null); + 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); + 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 @@ -165,6 +203,51 @@ public WxCpApprovalDetailResult getApprovalDetail(@NonNull String spNo) throws W 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 WxCpBaseResp.fromJson(responseContent); + } + @Override public List getDialRecord(Date startTime, Date endTime, Integer offset, Integer limit) throws WxErrorException { @@ -204,16 +287,31 @@ public List getDialRecord(Date startTime, Date endTime, Integer } @Override - public WxCpTemplateResult getTemplateDetail(@NonNull String templateId) throws WxErrorException { + 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, WxCpTemplateResult.class); + 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 List getCheckinDayData(@NonNull Date startTime, @NonNull Date endTime, List userIdList) + 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 + " 个,请分批获取"); @@ -243,7 +341,8 @@ public List getCheckinDayData(@NonNull Date startTime, @NonN } @Override - public List getCheckinMonthData(@NonNull Date startTime, @NonNull Date endTime, List userIdList) + 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 + " 个,请分批获取"); @@ -273,7 +372,8 @@ public List getCheckinMonthData(@NonNull Date startTime, @ } @Override - public List getCheckinScheduleList(@NonNull Date startTime, @NonNull Date endTime, List userIdList) + 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 + " 个,请分批获取"); @@ -308,4 +408,13 @@ public void setCheckinScheduleList(WxCpSetCheckinSchedule wxCpSetCheckinSchedule 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 b428bc34aa..8285e59df2 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,26 +1,27 @@ package me.chanjar.weixin.cp.api.impl; - -import me.chanjar.weixin.common.enums.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.error.WxRuntimeException; -import me.chanjar.weixin.common.util.http.HttpType; +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 { @@ -38,8 +39,8 @@ public HttpHost getRequestHttpProxy() { } @Override - public HttpType getRequestType() { - return HttpType.APACHE_HTTP; + public HttpClientType getRequestType() { + return HttpClientType.APACHE_HTTP; } @Override @@ -49,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); @@ -58,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); @@ -79,6 +75,96 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException { return this.configStorage.getAccessToken(); } + @Override + public String getContactAccessToken(boolean forceRefresh) throws WxErrorException { + if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getContactAccessToken(); + } + + Lock lock = this.configStorage.getContactAccessTokenLock(); + lock.lock(); + try { + // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷 + if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getContactAccessToken(); + } + // 使用通讯录同步secret获取access_token + String contactSecret = this.configStorage.getContactSecret(); + if (contactSecret == null || contactSecret.trim().isEmpty()) { + throw new WxErrorException("通讯录同步secret未配置"); + } + String url = String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN), + this.configStorage.getCorpId(), contactSecret); + + 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.updateContactAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); + } catch (IOException e) { + throw new WxRuntimeException(e); + } + } finally { + lock.unlock(); + } + return this.configStorage.getContactAccessToken(); + } + + @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..129823df5a --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceHttpComponentsImpl.java @@ -0,0 +1,191 @@ +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 getContactAccessToken(boolean forceRefresh) throws WxErrorException { + if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getContactAccessToken(); + } + + Lock lock = this.configStorage.getContactAccessTokenLock(); + lock.lock(); + try { + // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷 + if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getContactAccessToken(); + } + // 使用通讯录同步secret获取access_token + String contactSecret = this.configStorage.getContactSecret(); + if (contactSecret == null || contactSecret.trim().isEmpty()) { + throw new WxErrorException("通讯录同步secret未配置"); + } + String url = String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN), + this.configStorage.getCorpId(), contactSecret); + + 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.updateContactAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); + } catch (IOException e) { + throw new WxRuntimeException(e); + } + } finally { + lock.unlock(); + } + return this.configStorage.getContactAccessToken(); + } + + @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 661a0ed79f..69cc074be9 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 @@ -6,14 +6,12 @@ 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.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; @@ -55,13 +53,7 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException { RequestConfig config = RequestConfig.custom().setProxy(getRequestHttpProxy()).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); @@ -78,6 +70,92 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException { return configStorage.getAccessToken(); } + @Override + public String getContactAccessToken(boolean forceRefresh) throws WxErrorException { + final WxCpConfigStorage configStorage = getWxCpConfigStorage(); + if (!configStorage.isContactAccessTokenExpired() && !forceRefresh) { + return configStorage.getContactAccessToken(); + } + Lock lock = configStorage.getContactAccessTokenLock(); + lock.lock(); + try { + // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷 + if (!configStorage.isContactAccessTokenExpired() && !forceRefresh) { + return configStorage.getContactAccessToken(); + } + // 使用通讯录同步secret获取access_token + String contactSecret = configStorage.getContactSecret(); + if (contactSecret == null || contactSecret.trim().isEmpty()) { + throw new WxErrorException("通讯录同步secret未配置"); + } + String url = String.format(configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN), + this.configStorage.getCorpId(), contactSecret); + 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.updateContactAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); + } catch (IOException e) { + throw new WxRuntimeException(e); + } + } finally { + lock.unlock(); + } + return configStorage.getContactAccessToken(); + } + + @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(); 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 1b53630688..ef6d86c665 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 @@ -5,15 +5,19 @@ import jodd.http.HttpResponse; import jodd.http.ProxyInfo; import jodd.http.net.SocketHttpConnectionProvider; -import me.chanjar.weixin.common.enums.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.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,84 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException { return this.configStorage.getAccessToken(); } + @Override + public String getContactAccessToken(boolean forceRefresh) throws WxErrorException { + if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getContactAccessToken(); + } + + Lock lock = this.configStorage.getContactAccessTokenLock(); + lock.lock(); + try { + // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷 + if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getContactAccessToken(); + } + // 使用通讯录同步secret获取access_token + String contactSecret = this.configStorage.getContactSecret(); + if (contactSecret == null || contactSecret.trim().isEmpty()) { + throw new WxErrorException("通讯录同步secret未配置"); + } + HttpRequest request = HttpRequest.get(String.format(this.configStorage.getApiUrl(WxCpApiPathConsts.GET_TOKEN), + this.configStorage.getCorpId(), contactSecret)); + 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.updateContactAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); + } finally { + lock.unlock(); + } + return this.configStorage.getContactAccessToken(); + } + + @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) { 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 5fb5a73756..76847f1f93 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.enums.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,15 +52,19 @@ 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; - try { - Response response = client.newCall(request).execute(); + String resultContent; + try (Response response = client.newCall(request).execute()) { + if (response.body() == null) { + throw new WxErrorException("请求access token失败:响应内容为空"); + } resultContent = response.body().string(); } catch (IOException e) { log.error(e.getMessage(), e); + throw new WxErrorException(e); } WxError error = WxError.fromJson(resultContent, WxType.CP); @@ -70,6 +78,105 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException { return this.configStorage.getAccessToken(); } + @Override + public String getContactAccessToken(boolean forceRefresh) throws WxErrorException { + if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getContactAccessToken(); + } + + Lock lock = this.configStorage.getContactAccessTokenLock(); + lock.lock(); + try { + // 拿到锁之后,再次判断一下最新的token是否过期,避免重刷 + if (!this.configStorage.isContactAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getContactAccessToken(); + } + // 使用通讯录同步secret获取access_token + String contactSecret = this.configStorage.getContactSecret(); + if (contactSecret == null || contactSecret.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(), + contactSecret)) + .get() + .build(); + String resultContent; + try (Response response = client.newCall(request).execute()) { + if (response.body() == null) { + throw new WxErrorException("请求通讯录同步access token失败:响应内容为空"); + } + resultContent = response.body().string(); + } catch (IOException e) { + log.error(e.getMessage(), e); + throw new WxErrorException(e); + } + WxError error = WxError.fromJson(resultContent, WxType.CP); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); + this.configStorage.updateContactAccessToken(accessToken.getAccessToken(), + accessToken.getExpiresIn()); + } finally { + lock.unlock(); + } + return this.configStorage.getContactAccessToken(); + } + + @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; + try (Response response = client.newCall(request).execute()) { + if (response.body() == null) { + throw new WxErrorException("请求会话存档access token失败:响应内容为空"); + } + resultContent = response.body().string(); + } catch (IOException e) { + log.error(e.getMessage(), e); + throw new WxErrorException(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 +186,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 aa30385d6c..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 @@ -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 0e079160fb..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,6 +1,8 @@ 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; 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 384a3d30cd..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,12 +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.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; /** *

@@ -18,8 +20,7 @@
  *  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 { @@ -33,9 +34,37 @@ public void update(List userIds, String taskId, String replaceName) thro data.put("userids", userIds); data.put("agentid", agentId); data.put("task_id", taskId); - data.put("replace_name", replaceName); + // 文档地址: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 d0648b21ec..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 @@ -11,10 +11,17 @@ 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.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; @@ -29,6 +36,8 @@ */ @RequiredArgsConstructor public class WxCpUserServiceImpl implements WxCpUserService { + private final Format dateFormat = FastDateFormat.getInstance("yyyy-MM-dd"); + private final WxCpService mainService; @Override @@ -194,6 +203,17 @@ public String getUserId(String mobile) throws WxErrorException { 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 WxCpExternalContactInfo getExternalContact(String userId) throws WxErrorException { String url = this.mainService.getWxCpConfigStorage().getApiUrl(GET_EXTERNAL_CONTACT + userId); @@ -208,4 +228,57 @@ public String getJoinQrCode(int sizeType) throws WxErrorException { 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 99da962628..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 @@ -30,6 +30,12 @@ public enum Gender { private final String genderName; private final String code; + /** + * From code gender. + * + * @param code the code + * @return the gender + */ public static Gender fromCode(String code) { for (Gender a : Gender.values()) { if (a.code.equals(code)) { 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 04b0dd72e3..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 @@ -70,14 +70,31 @@ 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; @@ -86,6 +103,9 @@ public static class Users implements Serializable { private List users; } + /** + * The type User. + */ @Data public static class User implements Serializable { private static final long serialVersionUID = 7287632514385508024L; @@ -94,12 +114,18 @@ public static class User implements Serializable { private String userId; } + /** + * The type Parties. + */ @Data public static class Parties { @SerializedName("partyid") private List partyIds = null; } + /** + * The type Tags. + */ @Data public static class Tags { @SerializedName("tagid") 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 index bda927a800..4c17397ecd 100644 --- 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 @@ -6,6 +6,7 @@ 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; @@ -14,9 +15,9 @@ import java.util.List; /** - * @author songshiyu - * @date : create in 16:09 2020/9/27 - * 工作台自定义展示 + * The type Wx cp agent work bench. + * + * @author songshiyu created on : create in 16:09 2020/9/27 工作台自定义展示 */ @Data @Builder @@ -33,6 +34,10 @@ public class WxCpAgentWorkBench implements Serializable { * 用户的userid */ private String userId; + /** + * 用户的userIds + */ + private List useridList; /** * 应用id */ @@ -53,6 +58,20 @@ public class WxCpAgentWorkBench implements Serializable { * 是否覆盖用户工作台的数据。设置为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; @@ -60,6 +79,8 @@ public class WxCpAgentWorkBench implements Serializable { /** * 生成模板Json字符串 + * + * @return the string */ public String toTemplateString() { JsonObject templateObject = new JsonObject(); @@ -74,6 +95,8 @@ public String toTemplateString() { /** * 生成用户数据Json字符串 + * + * @return the string */ public String toUserDataString() { JsonObject userDataObject = new JsonObject(); @@ -84,6 +107,20 @@ public String toUserDataString() { 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(); + } + /** * 处理不用类型的工作台数据 */ @@ -131,6 +168,13 @@ private void handle(JsonObject templateObject) { 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; } @@ -140,4 +184,79 @@ private void handle(JsonObject templateObject) { } } + /** + * 处理不用类型的工作台数据 + */ + 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/WxCpBaseResp.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpBaseResp.java index 254260ea36..a895c38a8f 100644 --- 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 @@ -8,25 +8,54 @@ import java.io.Serializable; /** - * @author yqx - * @date 2020/3/16 + * 返回结果 + * + * @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/WxCpDepart.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpDepart.java index 5c640c51ce..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 @@ -21,10 +21,21 @@ public class WxCpDepart implements Serializable { 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 5ab4f5246b..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 @@ -21,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 7291489d9b..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 @@ -27,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/WxCpOauth2UserInfo.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpOauth2UserInfo.java index 0e10737bf6..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 @@ -13,6 +13,8 @@ * 用oauth2获取用户信息的结果类 * Created by BinaryWang on 2019/5/26. *
+ *

+ * 文档1:https://developer.work.weixin.qq.com/document/path/91707 * * @author Binary Wang */ @@ -30,4 +32,7 @@ public class WxCpOauth2UserInfo implements Serializable { 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 7b2887f03e..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 @@ -9,8 +9,7 @@ /** * 服务商凭证. * - * @author Binary Wang - * @date 2019-11-02 + * @author Binary Wang created on 2019-11-02 */ @Data public class WxCpProviderToken implements Serializable { @@ -28,6 +27,12 @@ public class WxCpProviderToken implements Serializable { @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 8649f0ced0..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 @@ -23,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 adac174884..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 @@ -25,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); } @@ -41,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 index a950e0c3f4..776726de80 100644 --- 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 @@ -23,6 +23,9 @@ public class WxCpTpAdmin extends WxCpBaseResp { @SerializedName("admin") private List admin; + /** + * The type Admin. + */ @Getter @Setter public static class Admin extends WxCpBaseResp { @@ -31,6 +34,9 @@ public static class Admin extends WxCpBaseResp { @SerializedName("userid") private String userId; + @SerializedName("open_userid") + private String openUserId; + @SerializedName("auth_type") private Integer authType; @@ -39,6 +45,12 @@ public String toJson() { } } + /** + * 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); } 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 index 9ca4971754..9919fd72b8 100644 --- 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 @@ -36,6 +36,16 @@ public class WxCpTpAuthInfo extends WxCpBaseResp { @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 { @@ -48,6 +58,9 @@ public static class DealerCorpInfo extends WxCpBaseResp { private String corpName; } + /** + * The type Auth corp info. + */ @Getter @Setter public static class AuthCorpInfo implements Serializable { @@ -128,6 +141,25 @@ public static class AuthInfo implements Serializable { } + /** + * 企业当前生效的版本信息 + */ + @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 { @@ -170,6 +202,68 @@ public static class Agent implements Serializable { @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; } /** @@ -212,10 +306,17 @@ public static class Privilege implements Serializable { } + /** + * 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 index cc84dfd4de..11c653a433 100644 --- 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 @@ -8,9 +8,10 @@ import java.io.Serializable; /** + * The type Wx cp tp contact search. + * * @author uianz - * @description - * @since 2020/12/23 下午 02:43 + * @since 2020 /12/23 下午 02:43 */ @Data @Accessors(chain = true) @@ -41,12 +42,6 @@ public class WxCpTpContactSearch implements Serializable { @SerializedName("agentid") private Integer agentId; - /** - * 查询的偏移量,每次调用的offset在上一次offset基础上加上limit - */ - @SerializedName("offset") - private Integer offset; - /** * 查询返回的最大数量,默认为50,最多为200,查询返回的数量可能小于limit指定的值 */ @@ -59,6 +54,17 @@ public class WxCpTpContactSearch implements Serializable { @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 index 21db4e0833..074b30bc0e 100644 --- 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 @@ -9,9 +9,10 @@ import java.util.List; /** + * The type Wx cp tp contact search resp. + * * @author uianz - * @description - * @since 2020/12/23 下午 02:55 + * @since 2020 /12/23 下午 02:55 */ @EqualsAndHashCode(callSuper = true) @Data @@ -23,6 +24,12 @@ public class WxCpTpContactSearchResp extends WxCpBaseResp { @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; @@ -32,6 +39,9 @@ public static class QueryResult implements Serializable { @SerializedName("party") private Party party; + /** + * The type User. + */ @Data public static class User implements Serializable { private static final long serialVersionUID = -4301684507150486556L; @@ -41,6 +51,9 @@ public static class User implements Serializable { private List openUserId; } + /** + * The type Party. + */ @Data public static class Party implements Serializable { private static final long serialVersionUID = -4301684507150486556L; @@ -51,6 +64,12 @@ public static class Party implements Serializable { } + /** + * 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 efe6d8285c..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 @@ -32,10 +32,21 @@ public class WxCpTpCorp implements Serializable { @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 index ab94a6b6b4..39d3601a2f 100644 --- 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 @@ -20,10 +20,21 @@ public class WxCpTpDepart implements Serializable { 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 index 9774f6230b..5330194abe 100644 --- 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 @@ -46,7 +46,30 @@ public class WxCpTpPermanentCodeInfo extends WxCpBaseResp { @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 { @@ -127,6 +150,25 @@ public static class AuthInfo implements Serializable { } + /** + * 企业当前生效的版本信息 + */ + @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 { @@ -169,6 +211,69 @@ public static class Agent implements Serializable { @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; + } /** @@ -187,6 +292,40 @@ public static class AuthUserInfo implements Serializable { @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; + } /** @@ -229,10 +368,17 @@ public static class Privilege implements Serializable { } + /** + * 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 index 82df9f4565..31c61b3a2b 100644 --- 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 @@ -8,19 +8,30 @@ /** * 预授权码返回 * - * @author yqx - * @date 2020/3/19 + * @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 index 73d7a51578..a73ec171b4 100644 --- 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 @@ -7,8 +7,10 @@ import java.io.Serializable; /** + * The type Wx cp tp tag. + * * @author zhangq - * @since 2021-02-14 16:15 16:15 + * @since 2021-02-14 16:15 */ @Data public class WxCpTpTag implements Serializable { @@ -20,6 +22,12 @@ public class WxCpTpTag implements Serializable { @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 index 8a9fecf21c..565cbb408c 100644 --- 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 @@ -11,6 +11,12 @@ 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 index 4fdc9a58ac..134656e438 100644 --- 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 @@ -11,6 +11,12 @@ 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