diff --git a/.devin/wiki.json b/.devin/wiki.json
new file mode 100644
index 0000000000..4a56bf4028
--- /dev/null
+++ b/.devin/wiki.json
@@ -0,0 +1,128 @@
+{
+ "repo_notes": [
+ {
+ "content": "请将整个 WxJava Wiki 以简体中文生成。所有页面标题、正文、图表标题、表格标题和说明文字均使用中文。代码、类名、方法名、包名、配置键和 Maven artifactId 保持原样;首次出现的专业术语可写为中文(English)。内容应面向 Java 开发者,准确说明公众号、小程序、企业微信、微信支付等模块。"
+ }
+ ],
+ "pages": [
+ {
+ "title": "WxJava 概览",
+ "purpose": "介绍 WxJava 仓库,说明其核心用途、模块结构和一般使用方式,并链接到模块架构与快速入门等子页面。",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "模块架构",
+ "purpose": "说明 WxJava 的整体模块架构及组件依赖关系,包括通用核心库、服务模块、Spring Boot Starter、Solon 插件和 GraalVM 支持。",
+ "parent": "WxJava 概览",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "快速入门",
+ "purpose": "提供在不同场景中使用 WxJava 的快速入门说明,包括 Maven/Gradle 依赖配置、基础服务初始化和 HTTP 客户端选择。",
+ "parent": "WxJava 概览",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "通用组件",
+ "purpose": "记录所有 WxJava 模块共用的工具类、常量和通用模式,并链接到消息处理与 HTTP 基础设施等子页面。",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "消息处理框架",
+ "purpose": "说明不同模块使用的核心消息处理与路由架构,包括 WxConsts 消息类型、XML 消息解析、XStream 序列化,以及路由器、处理器和拦截器模式。",
+ "parent": "通用组件",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "HTTP 客户端基础设施",
+ "purpose": "记录 HTTP 客户端抽象层,包括 RequestExecutor 策略模式、Apache HttpClient 4/5、OkHttp 和 Jodd-Http 支持、代理配置及 HttpClient 升级指南。",
+ "parent": "通用组件",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "分布式状态与锁",
+ "purpose": "说明 WxJava 如何借助 Redis(Jedis、Redisson、Spring Data Redis)管理 access token 等凭据的分布式状态,包括 WxRedisOps 抽象和分布式锁实现。",
+ "parent": "通用组件",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "微信公众号(MP)模块",
+ "purpose": "记录用于接入微信公众号的 MP 模块,概述 WxMpService、配置方式,并链接到服务实现、消息路由和子服务等页面。",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "MP 服务实现",
+ "purpose": "详述 WxMpService 及其 Apache、OkHttp、Jodd HTTP 客户端实现、access token 生命周期、多账号切换,以及 BaseWxMpServiceImpl 请求处理流程。",
+ "parent": "微信公众号(MP)模块",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "MP 消息路由",
+ "purpose": "说明 MP 模块的消息路由、处理器和拦截器,包括 WxMpMessageRouter 规则配置、WxMpXmlMessage 解析和 XML 被动回复消息构建器。",
+ "parent": "微信公众号(MP)模块",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "MP 子服务",
+ "purpose": "记录 MP 模块的专用子服务,包括用户管理、客服(Kefu)、模板消息、群发消息、素材管理、卡券、菜单、二维码、数据立方体分析和订阅通知。",
+ "parent": "微信公众号(MP)模块",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "企业微信(CP)模块",
+ "purpose": "记录接入企业微信的 CP 模块,概述 WxCpService、配置存储,并链接到客户联系、用户管理、OA 功能和第三方平台等子页面。",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "CP 客户联系管理",
+ "purpose": "说明如何管理企业微信外部联系人(客户),包括 ContactWay 配置、客户详情获取、ID 迁移、群聊管理、客户转接、拦截规则和商品图册。",
+ "parent": "企业微信(CP)模块",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "CP 用户管理",
+ "purpose": "详述企业微信的用户管理操作,包括 WxCpUserService、WxCpUser 对象、OAuth2 认证、部门与标签管理、应用管理和人事员工字段管理。",
+ "parent": "企业微信(CP)模块",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "CP OA 与消息能力",
+ "purpose": "记录办公自动化(OA)能力,包括打卡、审批、WeDrive 文件管理,以及 CP 消息能力,包括群机器人、客服(KF)、智能机器人和消息审计(会话存档)。",
+ "parent": "企业微信(CP)模块",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "CP 第三方平台(TP)",
+ "purpose": "记录面向企业微信 ISV 的 CP 第三方平台服务(WxCpTpService),包括永久授权码管理、消息路由、定制应用、版本与订单管理和许可服务。",
+ "parent": "企业微信(CP)模块",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "微信支付模块",
+ "purpose": "记录接入微信支付 API 的支付模块,概述 WxPayService、V2/V3 API 差异,并链接到配置、核心操作和专用支付服务等子页面。",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "微信支付配置",
+ "purpose": "说明微信支付的配置选项和安全设置,包括 WxPayConfig、多商户/多 appId 支持、V3 证书管理、AutoUpdateCertificatesVerifier、SignatureExec 和可信主机配置。",
+ "parent": "微信支付模块",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "电商服务",
+ "purpose": "记录微信支付中的专用电商操作,包括服务商模式的 EcommerceService、二级商户进件(Applyment4Sub)、分账(V2/V3)、转账、投诉处理和微信支付分。",
+ "parent": "微信支付模块",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "支付核心操作与数据对象",
+ "purpose": "记录核心支付操作(统一下单、退款、查询、回调通知)以及 V2/V3 请求和结果数据对象的层次结构,包括 WxPayUnifiedOrderRequest、退款对象、订单通知结果和企业付款(EntPay)。",
+ "parent": "微信支付模块",
+ "page_notes": [{ "content": "" }]
+ },
+ {
+ "title": "微信小程序模块",
+ "purpose": "记录面向微信小程序的 MiniApp 模块,概述 WxMaService、配置方式,并链接到服务实现、消息处理和专用子服务等页面。",
+ "page_notes": [{ "content": "" }]
+ },
+ {
diff --git a/.github/agents/my-agent.agent.md b/.github/agents/my-agent.agent.md
index 0c8481288a..bd1b4572eb 100644
--- a/.github/agents/my-agent.agent.md
+++ b/.github/agents/my-agent.agent.md
@@ -10,5 +10,7 @@ description: 需要用中文,包括PR标题和分析总结过程
# My Agent
-1、请使用中文输出思考过程和总结,包括PR标题,提交commit信息也要使用中文;
-2、生成代码时需要提供必要的单元测试代码。
+- 1、请使用中文输出思考过程和总结,包括PR标题,提交commit信息也要使用中文;
+- 2、生成代码时需要提供必要的单元测试代码;
+- 3、实现接口时请严格按照官方文档编写代码,严禁瞎编乱造、臆想并实现不存在的接口;
+- 4、新增加的代码如果标记作者信息,请注意不要把作者名设为binarywang或者其他无关人员,要改为 GitHub Copilot。
diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml
index a12c20b112..3dcd4db0eb 100644
--- a/.github/workflows/maven-publish.yml
+++ b/.github/workflows/maven-publish.yml
@@ -17,7 +17,7 @@ jobs:
steps:
- name: Checkout Code
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
with:
fetch-depth: 0
@@ -64,7 +64,7 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Set up Java
- uses: actions/setup-java@v4
+ uses: actions/setup-java@v5
with:
java-version: '8'
distribution: 'temurin'
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000000..aaec80bb5c
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,130 @@
+# WxJava Agent 指南
+
+## 适用范围与指令优先级
+
+- 本文件适用于整个仓库,供本地编码 Agent、自动化 Agent 和 Pull Request Review Agent 使用。
+- 开始工作前先阅读与任务直接相关的 `README.md`、`CONTRIBUTING.md`、模块 `pom.xml`、现有实现和测试;不要仅凭通用经验推断项目约定。
+- 若子目录存在更具体的 `AGENTS.md`,处理该目录文件时优先遵循距离目标文件最近的说明。
+- 用户的明确要求优先于本文件;若要求与兼容性、安全性或仓库约定冲突,应先说明风险,不要静默偏离。
+- 只修改完成任务所必需的文件,不处理无关格式、重构或历史遗留问题。
+
+## 项目概览
+
+- WxJava 是面向微信生态的 Java SDK,采用 Maven 多模块结构。
+- 当前根项目要求 Java 8(`maven.compiler.source` 和 `maven.compiler.target` 均为 `1.8`)。除非任务明确要求升级,否则新增代码、依赖和 API 必须保持 Java 8 兼容。
+- 主要 SDK 模块包括:
+ - `weixin-java-common`:各模块共用的基础类型、工具、异常、HTTP 与配置能力。
+ - `weixin-java-mp`:微信公众号。
+ - `weixin-java-miniapp`:微信小程序。
+ - `weixin-java-pay`:微信支付。
+ - `weixin-java-cp`:企业微信。
+ - `weixin-java-open`:微信开放平台。
+ - `weixin-java-channel`:微信视频号、微信小店。
+ - `weixin-java-qidian`:微信客服相关能力。
+ - `weixin-java-aispeech`:微信智能语音。
+ - `weixin-graal`:GraalVM 相关支持。
+- 集成模块主要位于:
+ - `spring-boot-starters`:Spring Boot Starter 及多账号 Starter。
+ - `solon-plugins`:Solon 插件及多账号插件。
+ - `wx-java-bom`:统一管理对外模块版本的 BOM。
+- `README.md` 说明整体能力和使用方式,`CONTRIBUTING.md` 规定代码贡献要求,`docs` 保存补充文档。
+
+## 开发流程
+
+1. 确认任务涉及的微信产品和 Maven 模块,先查找同模块中的相似接口、Bean、实现类和测试。
+2. 阅读目标模块及其父级 `pom.xml`,确认依赖、测试框架和已有实现方式。
+3. 优先沿用现有的包结构、命名、序列化、HTTP 执行器、异常处理及配置模式。
+4. 以最小变更完成任务;不要在功能修改中夹带依赖升级、全局格式化或无关重构。
+5. 为修复或新增行为添加有针对性的测试,并先运行受影响模块的验证。
+6. 交付前检查 diff、测试结果、兼容性和文档影响,明确报告未执行或无法执行的验证。
+
+## 代码风格与兼容性
+
+- 遵循 `.editorconfig`:使用空格缩进、缩进宽度为 2、UTF-8、LF、文件末尾保留换行,并清除非 Markdown 文件的行尾空格。
+- 保持目标文件现有代码风格;避免仅为个人偏好调整 import、换行、注释或成员顺序。
+- 不使用 Java 9 及以上语言特性或仅在新版本 JDK 中存在的 API。
+- 项目使用 Lombok;新增或修改 Lombok 用法时遵循相邻代码模式,不要在同一变更中无理由改写为手工样板代码。
+- 公共 API、序列化字段、枚举值、常量、默认实现和依赖范围都可能影响下游用户,修改时优先保持源码、二进制和行为兼容。
+- 不要随意改变已有异常类型、空值语义、默认 HTTP 客户端、JSON/XML 映射或配置加载行为。
+- 新增依赖前先确认现有依赖能否满足需求;依赖版本应在合适的父 POM 或 BOM 中统一管理,避免模块间版本漂移。
+
+## API 与实现约定
+
+- 对接微信接口时,以对应产品的官方接口定义和仓库内现有同类实现为依据,核对请求路径、HTTP 方法、字段名、必填项和返回结构。
+- Java 字段名可以符合项目命名习惯,但传输层字段名必须与微信接口保持一致;需要时使用项目现有的 Gson、Jackson 或 XStream 映射方式。
+- 新增 Service API 时同步检查接口、实现类、请求/响应 Bean、URL 常量、序列化适配器和测试是否都需要更新。
+- 涉及多个 HTTP 客户端实现时,保持 Apache HttpClient、HttpComponents、OkHttp 和 Jodd 等现有实现的能力一致;不要只修复其中一种而遗漏其他可选实现。
+- 涉及 Starter 或插件时,检查单账号与多账号版本以及自动配置、配置属性和示例测试是否需要同步。
+- 公共方法需要清晰的 Javadoc,至少准确描述参数、返回值、异常和必要约束;不要编写与实现不一致的模板化注释。
+- 不吞掉异常,不使用空 `catch`,不在日志中泄露 token、secret、密钥、签名原文或用户敏感数据。
+
+## 测试与验证
+
+- 项目测试主要使用 TestNG;沿用目标模块已有测试基类、数据提供器、Mock 方式和资源布局。
+- 修复 bug 时应添加能够复现旧行为并验证修复结果的回归测试;新增公共方法必须配套单元测试。
+- 优先执行受影响模块及其依赖的测试:
+
+```shell
+mvn -pl -am test
+```
+
+- 运行单个测试类时,可在对应模块范围内执行:
+
+```shell
+mvn -pl -am -Dtest= -Dsurefire.failIfNoSpecifiedTests=false test
+```
+
+- 跨公共模块、父 POM、BOM 或多个产品模块的变更应扩大验证范围;条件允许时执行:
+
+```shell
+mvn test
+```
+
+- 对仅需确认编译且测试依赖外部凭据的场景,可执行模块级编译或打包,但不得把跳过测试的构建描述为“测试通过”。
+- 部分测试可能依赖微信凭据、网络或本地配置。不得提交真实凭据;无法运行时应说明原因以及已完成的替代验证。
+- 完成前至少运行 `git diff --check`,并人工检查 `git diff`,确认没有意外文件、调试代码、生成物或敏感信息。
+
+## 文档与依赖变更
+
+- 用户可见的 API、配置项、模块使用方式或兼容性发生变化时,同步更新相关 Javadoc、README 或 `docs` 文档。
+- 示例中的版本号、模块名和配置键应与当前项目保持一致;不要复制未经验证的外部示例。
+- 修改根 POM、父 POM 或 `wx-java-bom` 时,检查所有子模块和对外依赖管理的影响。
+- 不提交构建输出、IDE 临时文件、测试报告、真实配置文件或本地备份文件。
+
+## Git 与 Pull Request 规范
+
+- 贡献目标分支为 `develop`;`release` 用于正式版本发布,不应作为常规 Pull Request 的目标分支。
+- 提交前保持工作区变更聚焦,不覆盖或回退用户已有的无关修改。
+- Commit 和 Pull Request 应说明变更动机、影响模块、兼容性风险和验证方式;关联已有 Issue 时写明编号。
+- 不得在未经用户明确授权的情况下执行 `git push`、创建或合并 Pull Request、改写历史或执行破坏性 Git 操作。
+- 不要使用 `git reset --hard`、强制 checkout 等方式清理不属于当前任务的改动。
+
+## 安全与敏感数据
+
+- 禁止提交 AppID 对应的 secret、access token、API v3 密钥、商户私钥、证书私钥、用户数据或其他真实凭据。
+- 测试和文档使用明显的占位值或脱敏数据;日志应避免输出查询串、请求体或响应体中的敏感字段。
+- 涉及签名、验签、加解密、证书、回调通知和支付金额时,重点检查字符编码、字段排序、精度、时区、重放风险和资源关闭。
+- 涉及网络请求、文件或流时,检查超时、异常路径、资源释放、响应关闭和大数据量下的内存行为。
+
+## Review 指南
+
+- 重点检查空指针、并发、资源释放、兼容性问题。
+- 不要只做代码风格建议,优先指出真实 bug 和回归风险。
+- 所有 Pull Request Review 的总结、结论和行内评论必须使用简体中文。
+- 技术标识符、类名、方法名、变量名、日志、错误信息及代码片段保持原文,不要翻译。
+- 严重程度标识可以保留 `P0`、`P1`、`P2` 等英文缩写。
+- 如果没有发现需要阻止合并的问题,也必须使用简体中文给出结论。
+- Review 结论必须基于当前 diff 和仓库中可验证的行为;不确定时明确说明假设,不要把推测写成确定缺陷。
+- 仅报告由本次变更引入或暴露、且作者可以采取行动的问题,并指出具体文件、位置、触发条件和影响。
+- 重点关注微信接口契约、Java 8 兼容性、公共 API 兼容性、序列化字段、HTTP 客户端实现一致性、Starter 多账号场景及敏感信息泄露。
+- 不要仅因缺少全仓库测试、个人风格偏好或与本次变更无关的历史代码而阻止合并。
+
+## Agent 完成检查清单
+
+- 已确认并遵循相关模块、相邻代码和更具体的 `AGENTS.md`。
+- 变更范围与任务直接相关,没有覆盖用户的无关修改。
+- 新增代码保持 Java 8、公共 API 和序列化兼容性。
+- 必要的测试与文档已经更新。
+- 已执行与风险匹配的构建或测试,并如实记录结果。
+- 已检查完整 diff、格式、生成物和敏感信息。
+- 最终回复使用简体中文,简要列出修改内容、验证结果和任何剩余风险。
diff --git a/README.md b/README.md
index f1cccac4b3..849abb6b18 100644
--- a/README.md
+++ b/README.md
@@ -8,63 +8,96 @@
[](https://circleci.com/gh/binarywang/WxJava/tree/develop)
[](https://www.jetbrains.com/?from=WxJava-weixin-java-tools)
[](https://opensource.org/licenses/Apache-2.0)
+[](https://deepwiki.com/binarywang/WxJava)
-
-
-### 微信`Java`开发工具包,支持包括微信支付、开放平台、公众号、企业微信、视频号、小程序等微信功能模块的后端开发。
-
+[
](https://hellogithub.com/repository/6de6147050c94db4aedfd7098d19f8d8)
+[
](https://trendshift.io/repositories/12152)
+[
](https://www.star-history.com/binarywang/wxjava)
+
+### 微信 `Java` 开发工具包,支持包括微信支付、开放平台、公众号、企业微信、视频号、小程序等微信功能模块的后端开发。
+
+### 特别赞助
+-
+-
+
+> 赞助商招募中,欢迎联系合作。
+
+### 关注公众号
+想获得技术交流群(QQ群/微信群/钉钉群)等信息,请扫码关注 `WxJava` 公众号:
+
+[](https://mp.weixin.qq.com/mp/homepage?__biz=MzI3MzAwMzk4OA==&hid=1&sn=f31af3bf562b116b061c9ab4edf70b61&scene=18#wechat_redirect)
+
+### 目录索引
+- [快速开始(3分钟)](#快速开始3分钟)
+- [AI 编程智能体 SKILL 安装](#ai-编程智能体-skill-安装)
+- [我该选哪个模块?](#我该选哪个模块)
+- [Maven 引用方式](#maven-引用方式)
+- [最小示例](#最小示例)
+- [重要信息](#重要信息)
+- [其他说明](#其他说明)
+- [版本说明](#版本说明)
+- [应用案例](#应用案例)
+- [特别赞助](#特别赞助)
+- [关注公众号](#关注公众号)
+- [贡献者列表](#贡献者列表)
+
+### 快速开始(3分钟)
+1. 根据业务场景选择模块(见下方“我该选哪个模块?”)
+2. 引入 Maven 依赖并选择对应模块
+3. 参考最小示例完成初始化并调用 API
+
+### AI 编程智能体 SKILL 安装
+
+仓库的 [`skills`](skills) 目录提供面向 WxJava 用户和贡献者的通用 SKILL,包括模块选择、接入、排障、接口贡献和升级迁移。每个 SKILL 都以 `SKILL.md` 为入口,可用于支持该约定的 AI 编程智能体。
+
+支持远程安装 SKILL 的智能体,可以直接使用自然语言指令安装所需目录。例如:
+
+> 安装 https://github.com/binarywang/WxJava/tree/develop/skills/wxjava-integration-guide 中的技能。
+
+可安装的 SKILL 包括:
+
+- [模块选择](https://github.com/binarywang/WxJava/tree/develop/skills/wxjava-module-selector)
+- [接入指南](https://github.com/binarywang/WxJava/tree/develop/skills/wxjava-integration-guide)
+- [故障排查](https://github.com/binarywang/WxJava/tree/develop/skills/wxjava-troubleshooter)
+- [接口贡献](https://github.com/binarywang/WxJava/tree/develop/skills/wxjava-api-contributor)
+- [升级迁移](https://github.com/binarywang/WxJava/tree/develop/skills/wxjava-upgrade-guide)
+
+不支持远程安装时,可将所需的 `skills/wxjava-*` 目录复制到智能体的 SKILL 目录或工作区配置目录;不同智能体的目录和启用方式请以其官方文档为准。
+
+以 Codex 为例,可复制到个人 SKILL 目录:
+
+```shell
+git clone https://github.com/binarywang/WxJava.git
+mkdir -p "${CODEX_HOME:-$HOME/.codex}/skills"
+cp -R WxJava/skills/wxjava-* "${CODEX_HOME:-$HOME/.codex}/skills/"
+```
+
+如果已在本仓库根目录,可直接执行:
+
+```shell
+mkdir -p "${CODEX_HOME:-$HOME/.codex}/skills"
+cp -R skills/wxjava-* "${CODEX_HOME:-$HOME/.codex}/skills/"
+```
+
+重启或新建智能体会话后,即可按需使用。例如:`使用 wxjava-integration-guide 为我的 Spring Boot 项目接入微信支付。`
+
+### 我该选哪个模块?
+
+| 业务场景 | 模块 | 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. [`WxJava` 荣获 `GitCode` 2024年度十大开源社区奖项](https://mp.weixin.qq.com/s/wM_UlMsDm3IZ1CPPDvcvQw)。
2. 项目合作洽谈请联系微信`binary0000`(在微信里自行搜索并添加好友,请注明来意,如有关于SDK问题需讨论请参考下文入群讨论,不要加此微信)。
-3. **2024-12-30 发布 [【4.7.0正式版】](https://mp.weixin.qq.com/s/_7k-XLYBqeJJhvHWCsdT0A)**!
+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))的常见问题部分,可以少走很多弯路,节省不少时间。
@@ -91,11 +124,49 @@
### Maven 引用方式
注意:最新版本(包括测试版)为 [](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.7.0
+ 4.8.0
```
@@ -114,6 +185,44 @@
- **微信开放平台**(`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());
+```
+
+
+
---------------------------------
### 版本说明
@@ -209,4 +318,11 @@
### GitHub Stargazers over time
-[](https://star-history.com/#binarywang/WxJava&Date)
+
+
+
+
+
+
+
+
diff --git a/demo.md b/demo.md
index d6b55b89e2..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) [](https://app.travis-ci.com/binarywang/weixin-java-pay-demo)
-1. 企业号/企业微信 Demo:[GitHub](http://github.com/binarywang/weixin-java-cp-demo)、[码云](http://gitee.com/binary/weixin-java-cp-demo) [](https://app.travis-ci.com/binarywang/weixin-java-cp-demo)
-1. 微信小程序 Demo:[GitHub](http://github.com/binarywang/weixin-java-miniapp-demo)、[码云](http://gitee.com/binary/weixin-java-miniapp-demo) [](https://app.travis-ci.com/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) [](https://app.travis-ci.com/Wechat-Group/weixin-java-open-demo)
+1. 微信支付 Demo:[GitHub](http://github.com/binarywang/weixin-java-pay-demo)、[码云](http://gitee.com/binary/weixin-java-pay-demo)
+1. 企业号/企业微信 Demo:[GitHub](http://github.com/binarywang/weixin-java-cp-demo)、[码云](http://gitee.com/binary/weixin-java-cp-demo)
+1. 微信小程序 Demo:[GitHub](http://github.com/binarywang/weixin-java-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) [](https://app.travis-ci.com/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) [](https://app.travis-ci.com/binarywang/weixin-java-mp-demo)
- - 含公众号和部分微信支付代码的 Demo:[GitHub](http://github.com/Wechat-Group/weixin-java-demo-springmvc)、[码云](http://gitee.com/binary/weixin-java-tools-springmvc) [](https://app.travis-ci.com/Wechat-Group/weixin-java-demo-springmvc)
-
+ - 使用 `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/ELECTRONIC_INVOICE_API_COMPARISON.md b/docs/ELECTRONIC_INVOICE_API_COMPARISON.md
new file mode 100644
index 0000000000..54568878f9
--- /dev/null
+++ b/docs/ELECTRONIC_INVOICE_API_COMPARISON.md
@@ -0,0 +1,66 @@
+# 服务商电子发票接口体系对比
+
+## 结论
+
+Gitee Issue `IDMETW` 补充的官方链接
+[获取开通服务商电子发票能力邀请链接](https://pay.weixin.qq.com/doc/v3/partner/4015941495)
+与 GitHub Issue [#4066](https://github.com/binarywang/WxJava/issues/4066) 给出的
+[开发接入准备](https://pay.weixin.qq.com/doc/v3/partner/4015792554) 是**同一套**微信支付
+V3 服务商电子发票产品文档:前者是 API 列表中的具体接口,后者是该产品的接入总览。
+
+但是,当前 `weixin-java-mp` 的 `WxMpMerchantInvoiceService` 是另一套旧的公众号
+`/card/invoice/*` 接口;不能把它当作这两个 Issue 所要求的支付 V3 能力实现。
+
+## 当前仓库既有的公众号体系
+
+- 当前代码将该能力归入公众号模块:
+ `weixin-java-mp` 的
+ `WxMpMerchantInvoiceService`。
+- 该接口的 Javadoc 明确引用公众号官方文档:
+ [商户开票模式说明](https://developers.weixin.qq.com/doc/offiaccount/WeChat_Invoice/E_Invoice/Vendor_and_Invoicing_Platform_Mode_Instruction.html)
+ 和
+ [商户开票接口列表](https://developers.weixin.qq.com/doc/offiaccount/WeChat_Invoice/E_Invoice/Vendor_API_List.html)。
+ 后者当前重定向至
+ [新版服务号文档](https://developers.weixin.qq.com/doc/service/guide/product/weixin_invoice/E_Invoice/Vendor_API_List.html)。
+- 代码实际请求公众号 API:`/card/invoice/getauthurl`、
+ `/card/invoice/getauthdata`、`/card/invoice/makeoutinvoice`、
+ `/card/invoice/clearoutinvoice`、`/card/invoice/queryinvoceinfo` 等;见
+ `WxMpApiUrl.Invoice`。这套接口围绕授权页、获取用户授权数据、开票、冲红和
+ 查询发票信息。
+- 历史提交 `058ce62a2b932633931e30762f44c561481dde5f` 将该服务及其请求/响应
+ Bean 加入 `weixin-java-mp`;提交说明关联 #1305。
+
+> 说明:调研时 Gitee 页面/API 的 TLS 连接失败,未能独立读取 IDMETW 的评论;
+> 但用户随后提供的 `4015941495` 链接已明确该 Issue 实际指向支付 V3 服务商电子发票。
+
+## 两个 Issue 共同指向的支付 V3 体系
+
+- #4066 链接的[开发接入准备](https://pay.weixin.qq.com/doc/v3/partner/4015792554)
+ 属于
+ “微信电子发票”产品,要求在**微信支付服务商号**申请“服务商电子发票”权限,
+ 并提到数电发票资源。
+- IDMETW 链接的[获取开通服务商电子发票能力邀请链接](https://pay.weixin.qq.com/doc/v3/partner/4015941495)
+ 位于相同产品的“API 列表”下,路径为
+ `GET /v3/new-tax-control-fapiao/fapiaomerchant/getspinviteurl`。
+- 同一官方文档导航的 API 列表包含:邀请子商户开通、检查子商户开票状态、创建电子
+ 发票卡券模板、配置开发选项、抬头填写链接/信息、各行业开票、冲红、查询、下载/上传
+ 发票文件、插入用户卡包,以及多个异步通知。
+- 例如“开具通用行业电子发票”接口为
+ `POST /v3/new-tax-control-fapiao/fapiao-applications/issue-general`,请求域名为
+ `https://api.mch.weixin.qq.com`,并要求微信支付 API 证书签名、服务商模式的
+ `sub_mchid` 和唯一开票申请单号 `fapiao_apply_id`;官方文档:
+ [开具通用行业电子发票](https://pay.weixin.qq.com/doc/v3/partner/4015792574)。
+ 该页还规定敏感字段使用微信支付公钥或平台证书加密。
+
+## 直接差异
+
+| 维度 | 既有公众号能力 | 两个 Issue 指向的微信支付能力 |
+| --- | --- | --- |
+| SDK 模块 | `weixin-java-mp` | 应位于 `weixin-java-pay` |
+| 官方文档产品线 | `developers.weixin.qq.com` 的公众号电子发票 | `pay.weixin.qq.com/doc/v3/partner` 的微信支付合作伙伴电子发票 |
+| API 形态 | `/card/invoice/*` | `/v3/new-tax-control-fapiao/*` |
+| 身份/鉴权上下文 | 公众号 access token、用户授权页/授权数据 | 微信支付服务商号、子商户号、V3 签名与敏感字段加密 |
+| 覆盖范围 | 授权、开票、冲红、查询及公众号商户配置 | 子商户邀约/状态、模板/开发配置、行业开票、文件和卡包、通知 |
+
+因此,两个 Issue 都是同一个微信支付 V3 服务商电子发票接入需求,当前代码库检索未
+发现对应的 `new-tax-control-fapiao` API 实现。
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/pom.xml b/pom.xml
index 0a33fbdf16..188e0d627e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
4.0.0
com.github.binarywang
wx-java
- 4.8.0
+ 4.8.5.B
pom
WxJava - Weixin/Wechat Java SDK
微信开发Java SDK
@@ -124,9 +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
@@ -136,7 +138,10 @@
UTF-8
4.5.13
+ 5.5.2
9.4.57.v20241219
+ 1.84
+ 2.3.3.RELEASE
@@ -157,13 +162,14 @@
4.12.0
provided
+
org.apache.httpcomponents.client5
httpclient5
- 5.5
- provided
+ ${httpclient5.version}
+
org.apache.httpcomponents
httpclient
@@ -217,13 +223,14 @@
import
-
+
joda-time
joda-time
2.10.6
- test
+
+
ch.qos.logback
logback-classic
@@ -255,8 +262,8 @@
org.mockito
- mockito-all
- 1.10.19
+ mockito-core
+ 4.11.0
test
@@ -317,13 +324,6 @@
-
- org.springframework.data
- spring-data-redis
- 2.3.3.RELEASE
- true
- provided
-
org.projectlombok
lombok
@@ -333,7 +333,12 @@
org.bouncycastle
bcpkix-jdk18on
- 1.80
+ ${bouncycastle.version}
+
+
+ org.bouncycastle
+ bcprov-jdk18on
+ ${bouncycastle.version}
diff --git a/skills/wxjava-api-contributor/SKILL.md b/skills/wxjava-api-contributor/SKILL.md
new file mode 100644
index 0000000000..b6896f105d
--- /dev/null
+++ b/skills/wxjava-api-contributor/SKILL.md
@@ -0,0 +1,14 @@
+---
+name: wxjava-api-contributor
+description: 按 WxJava 的 Maven 多模块、Java 8、公共 API 兼容性和 TestNG 约定,为微信官方接口新增或维护 SDK 支持。适用于新增 Service API、请求响应 Bean、序列化、HTTP 实现、Starter 配置或回归测试时。
+---
+
+# WxJava 接口贡献
+
+1. 先搜索开放与已关闭 Issue,确认需求是否已有讨论、实现、回归用例或官方接口变动;再确认微信产品和目标模块。
+2. 阅读对应 README、POM、相似接口、实现与测试,并读取 [贡献约定](references/contribution.md)。
+3. 将官方接口契约映射到 Service、实现、Bean、URL、序列化和测试;列出所有可能受影响的 HTTP 客户端和 Starter。
+4. 只做完成需求所需的最小改动;更新必要 Javadoc、测试与用户可见文档。
+5. 执行 `mvn -pl -am test`,并检查 `git diff --check`。
+
+保持 Java 8 与公共 API、异常语义、JSON/XML 字段兼容性。涉及多 HTTP 客户端、单/多账号 Starter 或 Solon 插件时,检查等价实现是否需要同步。PR 应关联对应 Issue,目标分支为 `develop`。
diff --git a/skills/wxjava-api-contributor/agents/openai.yaml b/skills/wxjava-api-contributor/agents/openai.yaml
new file mode 100644
index 0000000000..acefb1b7e9
--- /dev/null
+++ b/skills/wxjava-api-contributor/agents/openai.yaml
@@ -0,0 +1,4 @@
+interface:
+ display_name: "WxJava 接口贡献"
+ short_description: "按 WxJava 项目约定新增或维护 SDK 接口能力"
+ default_prompt: "使用 $wxjava-api-contributor 为 WxJava 新增微信接口支持。"
diff --git a/skills/wxjava-api-contributor/references/contribution.md b/skills/wxjava-api-contributor/references/contribution.md
new file mode 100644
index 0000000000..6b8571e28e
--- /dev/null
+++ b/skills/wxjava-api-contributor/references/contribution.md
@@ -0,0 +1,19 @@
+# 贡献约定
+
+以微信官方接口定义和本仓库同类实现为准,核对路径、方法、字段名、必填项和响应结构。优先复用既有 HTTP 执行器、异常、配置、Gson/Jackson/XStream 映射和 TestNG 测试模式。
+
+公共方法需要准确 Javadoc。不要吞异常、记录敏感值或无关重构。新增公开方法和 bug 修复均应有针对性测试;涉及公共模块、BOM 或多模块时扩大验证范围。
+
+## 接口增量检查表
+
+1. 从微信官方文档和相关 Issue 中确认接口可用条件、HTTP 方法、URL、必填字段、签名/加密要求和响应示例。
+2. 搜索同产品的相邻能力,复用其 Service 分层、Bean 命名、请求执行和错误处理模式;不要只新增 Bean 而遗漏 Service 暴露。
+3. 若接口尚未支持,优先使用现有通用执行能力;MP/CP Wiki 说明通用执行器会处理 access token 刷新及 `errcode` 到异常的转换。
+4. 为字段边界、空值、JSON/XML 映射和异常路径添加 TestNG 回归测试。API 路径或字段问题是历史 bug 的高频来源,例如 [#3982](https://github.com/binarywang/WxJava/issues/3982)、[#4000](https://github.com/binarywang/WxJava/issues/4000)。
+5. 按 [贡献指南](https://github.com/binarywang/WxJava/blob/develop/CONTRIBUTING.md) 使用 `develop` 作为 PR 目标;说明 Issue、兼容性影响和验证命令。
+
+## 一手资料入口
+
+- [如何调用 MP 未支持接口](https://github.com/binarywang/WxJava/wiki/MP_%E5%A6%82%E4%BD%95%E8%B0%83%E7%94%A8%E6%9C%AA%E6%94%AF%E6%8C%81%E7%9A%84%E6%8E%A5%E5%8F%A3)
+- [如何调用 CP 未支持接口](https://github.com/binarywang/WxJava/wiki/CP_%E5%A6%82%E4%BD%95%E8%B0%83%E7%94%A8%E6%9C%AA%E6%94%AF%E6%8C%81%E7%9A%84%E6%8E%A5%E5%8F%A3)
+- [关闭的新接口 Issue](https://github.com/binarywang/WxJava/issues?q=is%3Aissue%20state%3Aclosed%20label%3A%E6%96%B0%E6%8E%A5%E5%8F%A3)
diff --git a/skills/wxjava-integration-guide/SKILL.md b/skills/wxjava-integration-guide/SKILL.md
new file mode 100644
index 0000000000..393438768f
--- /dev/null
+++ b/skills/wxjava-integration-guide/SKILL.md
@@ -0,0 +1,14 @@
+---
+name: wxjava-integration-guide
+description: 为 Java、Spring Boot 或 Solon 项目生成可验证的 WxJava 接入方案,包括模块选择、BOM、配置、最小调用代码以及单/多账号集成。适用于用户要求接入公众号、小程序、支付、企业微信、开放平台、视频号或微信小店时。
+---
+
+# WxJava 接入指南
+
+1. 确认微信产品、框架、单/多账号、部署形态和首个 API 调用。
+2. 读取 [接入约束](references/integration.md),选择模块和配置方式。
+3. 先输出依赖与配置,再输出最小调用;每段示例注明应放置的位置和所依赖的模块。
+4. 输出脱敏配置和最小服务端代码;凭据一律用占位符。
+5. 给出本地验证步骤与安全提醒;支付、回调和证书示例不得直接用于生产。
+
+保持 Java 8 兼容。生产集群必须使用可共享的配置存储或 token 存储;不要把内存实现当作多节点部署方案。引用已有 Demo、Wiki 或 README 作为继续阅读入口;不虚构配置键、SDK 方法或版本号。
diff --git a/skills/wxjava-integration-guide/agents/openai.yaml b/skills/wxjava-integration-guide/agents/openai.yaml
new file mode 100644
index 0000000000..622ec2ac98
--- /dev/null
+++ b/skills/wxjava-integration-guide/agents/openai.yaml
@@ -0,0 +1,4 @@
+interface:
+ display_name: "WxJava 接入指南"
+ short_description: "生成可验证的 WxJava 最小接入方案与配置示例"
+ default_prompt: "使用 $wxjava-integration-guide 为我的项目生成 WxJava 接入代码。"
diff --git a/skills/wxjava-integration-guide/references/integration.md b/skills/wxjava-integration-guide/references/integration.md
new file mode 100644
index 0000000000..ae441cc278
--- /dev/null
+++ b/skills/wxjava-integration-guide/references/integration.md
@@ -0,0 +1,22 @@
+# 接入约束
+
+优先通过 `wx-java-bom` 统一管理多个 WxJava 模块版本。核心 SDK 模块是 MP、MiniApp、Pay、CP、Open、Channel、Qidian、Aispeech;框架集成模块位于 `spring-boot-starters` 与 `solon-plugins`。
+
+查阅相应模块 README、根目录 [demo.md](https://github.com/binarywang/WxJava/blob/develop/demo.md) 和目标模块测试中的示例,确认配置属性与初始化模式。所有 `appId`、`secret`、商户私钥、API v3 密钥和证书均使用占位符,不得输出到日志。
+
+## 实施检查表
+
+1. 先完成一条只读或低风险 API 调用,再接入消息、支付或异步回调。
+2. 单实例可使用默认配置存储;多实例或集群需使用共享的 token/config storage,避免节点各自刷新 access token。
+3. 公众号、企业微信等回调必须先按平台要求校验消息合法性,再进入业务路由;支付回调还必须按商户单号实现幂等。
+4. 需要代理或私有网络出口时,明确区分正向代理与反向代理。支付 V3 的签名路径不能因反向代理路径前缀而被错误改写。
+5. HTTP 客户端类型、Starter 配置键和 Service 实现必须取自目标模块的当前 README、POM 或相邻 Demo;不要混用不同产品模块的配置前缀。
+6. Starter 自动配置与 Demo 手动初始化必须二选一后再给示例。发生空 key、注入为空或 NPE 时,先核对启动模块、profile、配置前缀和当前 `*Properties` 类;历史 [#2177](https://github.com/binarywang/WxJava/issues/2177) 是混用两种配置模型的案例。
+7. Quarkus 或 GraalVM 场景转到 [Quarkus 支持文档](https://github.com/binarywang/WxJava/blob/develop/docs/QUARKUS_SUPPORT.md),不要套用 Spring Boot Starter 配置。
+
+## 一手资料入口
+
+- [MP Quick Start](https://github.com/binarywang/WxJava/wiki/MP_Quick-Start)
+- [微信支付说明](https://github.com/binarywang/WxJava/wiki/%E5%BE%AE%E4%BF%A1%E6%94%AF%E4%BB%98)
+- [SDK 正反向代理支持](https://github.com/binarywang/WxJava/wiki/SDK-%E9%92%88%E5%AF%B9%E5%BE%AE%E4%BF%A1-%E6%AD%A3%E5%90%91%E4%BB%A3%E7%90%86%E5%92%8C%E5%8F%8D%E5%90%91%E4%BB%A3%E7%90%86%E6%94%AF%E6%8C%81)
+- [HTTP 客户端升级指南](https://github.com/binarywang/WxJava/blob/develop/docs/HTTPCLIENT_UPGRADE_GUIDE.md)
diff --git a/skills/wxjava-module-selector/SKILL.md b/skills/wxjava-module-selector/SKILL.md
new file mode 100644
index 0000000000..3d78c0ebf6
--- /dev/null
+++ b/skills/wxjava-module-selector/SKILL.md
@@ -0,0 +1,15 @@
+---
+name: wxjava-module-selector
+description: 根据微信公众号、小程序、微信支付、企业微信、开放平台、视频号或微信小店、腾讯企点和微信智能对话等业务场景,为用户选择合适的 WxJava Maven 模块、BOM 和示例入口。适用于用户询问“该用哪个模块”、依赖坐标、产品边界或单/多账号 Starter 选择时。
+---
+
+# WxJava 模块选择
+
+1. 识别微信产品、服务端框架、是否多账号、是否包含支付或回调;信息不足时只询问必要问题。
+2. 读取 [模块映射](references/modules.md),给出一个主推荐,以及组合模块的理由。
+3. 先区分产品边界,再选择核心 SDK;仅当项目确实依赖框架自动配置时才额外推荐 Starter 或 Solon 插件。
+4. 优先推荐 BOM;给出准确的 `groupId`、`artifactId`、相应 Demo、Wiki 或仓库文档入口。
+5. 说明服务端 SDK 的边界:移动端登录、分享等能力仍需微信官方客户端 SDK。
+6. 不臆测版本号;建议以 Maven Central 或项目 README 的当前版本为准。
+
+使用“场景 → 模块 → 集成选项 → 下一步”的简短结构。涉及多账号时说明单账号与 multi Starter 的区别;不要在示例中泄露凭据。支付和回调场景必须提醒用户验证通知 URL、验签与幂等处理。
diff --git a/skills/wxjava-module-selector/agents/openai.yaml b/skills/wxjava-module-selector/agents/openai.yaml
new file mode 100644
index 0000000000..a3744af616
--- /dev/null
+++ b/skills/wxjava-module-selector/agents/openai.yaml
@@ -0,0 +1,4 @@
+interface:
+ display_name: "WxJava 模块选择"
+ short_description: "按微信业务场景选择 WxJava 模块、依赖与示例"
+ default_prompt: "使用 $wxjava-module-selector 为我的微信业务选择合适的 WxJava 模块。"
diff --git a/skills/wxjava-module-selector/references/modules.md b/skills/wxjava-module-selector/references/modules.md
new file mode 100644
index 0000000000..563cdbb954
--- /dev/null
+++ b/skills/wxjava-module-selector/references/modules.md
@@ -0,0 +1,37 @@
+# WxJava 模块映射
+
+| 场景 | 核心模块 |
+| --- | --- |
+| 微信公众号 | `weixin-java-mp` |
+| 微信小程序 | `weixin-java-miniapp` |
+| 微信支付 | `weixin-java-pay` |
+| 企业微信 | `weixin-java-cp` |
+| 微信开放平台/第三方平台 | `weixin-java-open` |
+| 视频号/微信小店 | `weixin-java-channel` |
+| 腾讯企点/微信客服 | `weixin-java-qidian` |
+| 微信智能对话/智能语音 | `weixin-java-aispeech` |
+
+多个模块并用时优先使用 `com.github.binarywang:wx-java-bom`。Spring Boot 集成从 `spring-boot-starters` 选择;Solon 集成从 `solon-plugins` 选择。
+
+## 多账号集成范围
+
+只有多个独立微信应用配置时才选择 multi 集成,并先按框架与产品核对目录是否存在:
+
+- Spring Boot:MP、MiniApp、CP、自建/第三方 CP、Pay、Open、Channel 都有相应 multi Starter。
+- Solon:仅 MP、MiniApp、CP、Channel 有 multi 插件;Pay、Open、Qidian 目前只有单账号插件,不要推荐不存在的 multi artifact。
+
+## 选择检查点
+
+- 公众号、小程序和企业微信的消息回调、token 与加解密配置彼此独立;不要因同属一个公司而复用不兼容的凭据或配置对象。
+- 企业微信的多应用应使用独立的 `WxCpConfigStorage` 与 `WxCpServiceImpl`;Wiki 明确指出复用 token、AES key 和 URL 会造成安全边界问题。
+- 支付能力通常与 MP、MiniApp 或 Open 同时使用:前者处理业务身份和消息,`weixin-java-pay` 处理商户签名、证书与支付回调。
+- 视频号/微信小店接口属于 `weixin-java-channel`;不要误归入 MP 或 Pay。
+- 腾讯企点与微信智能对话是独立 SDK 模块;不要将客服或智能对话需求默认归入 MP、CP 或 Channel。
+- 当能力在 MP 与 Open 等模块可能重叠时,按授权主体、官方 API 域和回调场景选择,不要只按“移动端”或“登录”字样判断。先在当前源码和 Issue 中确认覆盖状态,并标注“已确认 / 待查 / 需自行调用底层接口”。
+- BOM 适合同时使用多个 WxJava 模块。若项目还导入 Spring Boot 等上游 BOM,升级后执行 `mvn help:effective-pom` 与 `mvn dependency:tree`;历史 [#4058](https://github.com/binarywang/WxJava/issues/4058) 表明依赖管理顺序可能影响 Spring Data Redis 等依赖。
+
+## 一手资料入口
+
+- [WxJava Wiki 首页](https://github.com/binarywang/WxJava/wiki)
+- [企业微信多应用配置](https://github.com/binarywang/WxJava/wiki/CP_%E5%A6%82%E4%BD%95%E6%94%AF%E6%8C%81%E5%A4%9A%E4%B8%AA%E4%BC%81%E4%B8%9A%E5%8F%B7%E5%BA%94%E7%94%A8%E6%88%96%E4%BC%81%E4%B8%9A%E5%8F%B7)
+- [视频号/微信小店开发文档](https://github.com/binarywang/WxJava/wiki/0_%E8%A7%86%E9%A2%91%E5%8F%B7_%E5%BE%AE%E4%BF%A1%E5%B0%8F%E5%BA%97%E5%BC%80%E5%8F%91%E6%96%87%E6%A1%A3)
diff --git a/skills/wxjava-troubleshooter/SKILL.md b/skills/wxjava-troubleshooter/SKILL.md
new file mode 100644
index 0000000000..26d8c941ed
--- /dev/null
+++ b/skills/wxjava-troubleshooter/SKILL.md
@@ -0,0 +1,14 @@
+---
+name: wxjava-troubleshooter
+description: 排查 WxJava 在配置初始化、access token、签名验签、支付证书、回调通知、序列化、网络请求和多账号隔离方面的问题。适用于用户提供异常、日志、请求响应或“WxJava 为什么不能调用”的场景。
+---
+
+# WxJava 故障排查
+
+1. 收集最小复现:产品模块、SDK 版本、JDK、框架、依赖树、异常堆栈、请求 ID 与脱敏后的配置形状。
+2. 将故障归类为依赖/初始化、token 与存储、请求契约、HTTP/代理、回调验签、支付证书或并发与生命周期。
+3. 依据 [诊断清单](references/diagnostics.md),按最低成本、最可验证的顺序排查。
+4. 每次只提出一个可验证根因,给出最小修复、预期现象和回归验证动作。
+5. 签名、支付、回调和加密场景须核对编码、字段排序、金额精度、时间戳、证书链和重放保护。
+
+要求遮蔽 secret、token、私钥、证书、签名原文和个人数据。不要建议关闭 TLS 校验或验签;区分已证实结论与待验证假设。遇到历史同类问题时,给出对应 Issue 或 Wiki 链接,但必须检查当前版本的实现后再套用修复。
diff --git a/skills/wxjava-troubleshooter/agents/openai.yaml b/skills/wxjava-troubleshooter/agents/openai.yaml
new file mode 100644
index 0000000000..f473f862fe
--- /dev/null
+++ b/skills/wxjava-troubleshooter/agents/openai.yaml
@@ -0,0 +1,4 @@
+interface:
+ display_name: "WxJava 故障排查"
+ short_description: "定位 WxJava 配置、回调、签名与接口调用问题"
+ default_prompt: "使用 $wxjava-troubleshooter 排查我的 WxJava 调用异常。"
diff --git a/skills/wxjava-troubleshooter/references/diagnostics.md b/skills/wxjava-troubleshooter/references/diagnostics.md
new file mode 100644
index 0000000000..acf115ae92
--- /dev/null
+++ b/skills/wxjava-troubleshooter/references/diagnostics.md
@@ -0,0 +1,27 @@
+# 诊断清单
+
+- 配置:模块是否正确,应用标识和凭据是否配对,多账号配置是否被正确选择。
+- 请求:HTTP 方法、路径、必填字段和 JSON/XML 映射是否符合微信接口契约。
+- 认证:token 缓存、签名字段、时间戳、nonce、证书和密钥格式是否有效。
+- 网络:DNS、代理、超时、TLS 证书校验和响应关闭是否正常。
+- 回调:回调 URL 可访问、验签参数完整、幂等处理和资源释放是否存在。
+
+从精确异常和最小请求入手,不用“重试”替代根因分析。
+
+## 高频场景
+
+| 症状 | 优先检查 | 依据 |
+| --- | --- | --- |
+| 多节点间歇性 token 失效 | 是否共享 token/config storage,是否让各节点并发刷新 | Wiki 的 MP/CP 配置存储说明与集群案例 |
+| `NoClassDefFoundError`、`NoSuchMethodError` | `mvn dependency:tree`、HttpClient/commons-lang/xstream 冲突及 BOM 是否统一版本 | Wiki 的异常排查页与 HTTP 客户端升级指南 |
+| 回调验签或重复处理 | 原始请求体、时间戳/nonce/签名、回调 URL、业务幂等键 | MP 合法性校验与支付回调说明 |
+| HTTP 连接、超时或代理问题 | 连接/读取超时、代理、TLS、正反向代理是否混用 | Wiki 的 HttpClient 参数与代理文档 |
+| 企业微信会话存档崩溃 | 是否仍手动销毁旧 SDK 实例,是否已迁移安全 API | [会话存档安全使用指南](https://github.com/binarywang/WxJava/blob/develop/docs/CP_MSG_AUDIT_SDK_SAFE_USAGE.md) |
+| 视频号/小店接口 404 或字段不匹配 | 当前官方路径与请求字段、目标 SDK 版本、历史 Issue 是否已修复 | 关闭的 bug Issue,如 [#3982](https://github.com/binarywang/WxJava/issues/3982) |
+
+## 一手资料入口
+
+- [常见异常首页](https://github.com/binarywang/WxJava/wiki)
+- [MP 消息合法性验证](https://github.com/binarywang/WxJava/wiki/MP_%E9%AA%8C%E8%AF%81%E6%B6%88%E6%81%AF%E5%90%88%E6%B3%95%E6%80%A7)
+- [HttpClient 参数配置](https://github.com/binarywang/WxJava/wiki/HttpClient%E7%9B%B8%E5%85%B3%E5%8F%82%E6%95%B0%E7%9A%84%E8%AE%BE%E7%BD%AE%E6%96%B9%E6%B3%95)
+- [关闭的 bug Issue](https://github.com/binarywang/WxJava/issues?q=is%3Aissue%20state%3Aclosed%20label%3Abug)
diff --git a/skills/wxjava-upgrade-guide/SKILL.md b/skills/wxjava-upgrade-guide/SKILL.md
new file mode 100644
index 0000000000..624a752338
--- /dev/null
+++ b/skills/wxjava-upgrade-guide/SKILL.md
@@ -0,0 +1,14 @@
+---
+name: wxjava-upgrade-guide
+description: 规划 WxJava 的版本升级与迁移,检查 BOM、模块依赖、Java 版本、配置与公共 API 兼容性,并提供可回滚的验证步骤。适用于用户从旧版升级、切换依赖管理方式、处理兼容性告警或制定升级发布计划时。
+---
+
+# WxJava 升级迁移
+
+1. 收集当前与目标版本、已用模块、JDK、框架、BOM 使用情况、依赖树、代理配置和关键调用路径。
+2. 读取 [迁移检查表](references/migration.md),识别版本、依赖和配置边界。
+3. 按 HTTP 客户端、BOM、支付/回调、多账号、企业微信会话存档等变化类型选择迁移分支。
+4. 输出分阶段变更:依赖调整、编译、关键回归测试、灰度与明确回滚条件。
+5. 将不确定项标为待查,并引导查看目标版本的 release notes、README、Javadoc 与变更记录。
+
+不要猜测废弃 API 或破坏性变更;以官方发布信息、当前代码与实际编译结果为准。默认保持 Java 8,除非目标版本明确改变此约束。不能以关闭验签、固定 token 或跳过测试作为回滚方案。
diff --git a/skills/wxjava-upgrade-guide/agents/openai.yaml b/skills/wxjava-upgrade-guide/agents/openai.yaml
new file mode 100644
index 0000000000..fdf8b71ecb
--- /dev/null
+++ b/skills/wxjava-upgrade-guide/agents/openai.yaml
@@ -0,0 +1,4 @@
+interface:
+ display_name: "WxJava 升级迁移"
+ short_description: "规划 WxJava 版本升级、依赖调整与兼容性迁移"
+ default_prompt: "使用 $wxjava-upgrade-guide 制定 WxJava 版本升级方案。"
diff --git a/skills/wxjava-upgrade-guide/references/migration.md b/skills/wxjava-upgrade-guide/references/migration.md
new file mode 100644
index 0000000000..443ac73a0a
--- /dev/null
+++ b/skills/wxjava-upgrade-guide/references/migration.md
@@ -0,0 +1,37 @@
+# 迁移检查表
+
+- 使用多个 WxJava 模块时,优先迁移到 `wx-java-bom`,避免模块版本漂移。
+- 清点 MP、MiniApp、Pay、CP、Open、Channel 及框架 Starter/插件的实际依赖。
+- 更新后先执行编译和受影响模块测试,再验证 token、回调、支付和关键业务链路。
+- 为依赖版本保留可快速恢复的变更记录;按灰度策略验证生产环境,且不将凭据写入代码或日志。
+
+## 升级分支
+
+### HTTP 客户端
+
+从 4.7.x 起,项目支持并推荐 Apache HttpClient 5.x,同时保留部分 4.x 兼容性。先按 [HTTP 客户端升级指南](https://github.com/binarywang/WxJava/blob/develop/docs/HTTPCLIENT_UPGRADE_GUIDE.md) 核对模块、依赖与 `http-client-type`,再检查代理 host、port、username、password 的完整性。历史 [#3836](https://github.com/binarywang/WxJava/issues/3836) 表明可选代理配置也需要回归。
+
+### BOM 与依赖冲突
+
+`wx-java-bom` 从 4.8.3.B 起提供。导入 BOM 前后均执行:
+
+```shell
+mvn help:effective-pom
+mvn dependency:tree
+```
+
+特别回归由 Spring Boot BOM 管理的 Redis、HTTP 客户端和序列化依赖;[#4058](https://github.com/binarywang/WxJava/issues/4058) 是历史冲突案例。
+
+### 企业微信会话存档
+
+升级到 4.8.0 或更高版本时,查找旧的 `getChatDatas`、`getDecryptData`、`getChatPlainText`、`getMediaFile` 和手动 `Finance.DestroySdk()`。按 [ThreadLocal 生命周期迁移文档](https://github.com/binarywang/WxJava/blob/develop/docs/CP_MSG_AUDIT_THREADLOCAL_LIFECYCLE_REFACTOR.md) 切换至新 API,并做并发验证。
+
+ThreadLocal SDK 不会在线程结束时自动释放。在线程池、定时任务或一次性线程中,必须在任务的 `finally` 块调用 `msgAuditService.closeThreadLocalSdk()`;应用停止时再用 `closeAllSdks()` 做全局兜底。不要在业务代码中直接调用 `Finance.DestroySdk()`。
+
+### 回调与支付
+
+验证不止于 HTTP 2xx:分别覆盖 token 获取、签名/证书校验、重复回调、业务状态转换和多账号路由。多账号异步任务必须显式传递 appId 或服务上下文,不能假定 ThreadLocal 自动继承。
+
+## 验证与回滚
+
+按“编译和单测 → 非生产凭据冒烟 → 灰度和监控”执行。出现依赖树冲突、验签/证书异常、回调错误或 native 崩溃时停止扩大灰度,恢复上一个已验证依赖组合后再以最小复现定位。
diff --git a/solon-plugins/pom.xml b/solon-plugins/pom.xml
index d0ca564c24..9a54c787db 100644
--- a/solon-plugins/pom.xml
+++ b/solon-plugins/pom.xml
@@ -6,7 +6,7 @@
com.github.binarywang
wx-java
- 4.8.0
+ 4.8.5.B
pom
wx-java-solon-plugins
diff --git a/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml
index 995ecbd532..7f144022e0 100644
--- a/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-channel-multi-solon-plugin/pom.xml
@@ -5,7 +5,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.0
+ 4.8.5.B
4.0.0
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
index 2e2da1add7..ca99e522b9 100644
--- 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
@@ -55,7 +55,7 @@ public static class ConfigStorage implements Serializable {
/**
* http客户端类型.
*/
- private HttpClientType httpClientType = HttpClientType.HTTP_CLIENT;
+ private HttpClientType httpClientType = HttpClientType.HTTP_COMPONENTS;
/**
* http代理主机.
diff --git a/solon-plugins/wx-java-channel-solon-plugin/pom.xml b/solon-plugins/wx-java-channel-solon-plugin/pom.xml
index b2ca356692..ee463f661b 100644
--- a/solon-plugins/wx-java-channel-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-channel-solon-plugin/pom.xml
@@ -3,7 +3,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.0
+ 4.8.5.B
4.0.0
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
index 0c00dbcaa7..5614f63e86 100644
--- 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
@@ -7,7 +7,11 @@
*/
public enum HttpClientType {
/**
- * HttpClient
+ * HttpClient.
*/
- HttpClient
+ HttpClient,
+ /**
+ * HttpComponents.
+ */
+ HttpComponents,
}
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
index 6562a02e9d..89b81b7d9f 100644
--- 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
@@ -73,7 +73,7 @@ public static class ConfigStorage {
/**
* http客户端类型
*/
- private HttpClientType httpClientType = HttpClientType.HttpClient;
+ private HttpClientType httpClientType = HttpClientType.HttpComponents;
/**
* http代理主机
diff --git a/solon-plugins/wx-java-cp-multi-solon-plugin/README.md b/solon-plugins/wx-java-cp-multi-solon-plugin/README.md
index 97bcf0723f..8eb467f98f 100644
--- a/solon-plugins/wx-java-cp-multi-solon-plugin/README.md
+++ b/solon-plugins/wx-java-cp-multi-solon-plugin/README.md
@@ -6,6 +6,25 @@
- 未实现 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. 引入依赖
@@ -18,25 +37,21 @@
```
2. 添加配置(app.properties)
```properties
- # 应用 1 配置
- wx.cp.corps.tenantId1.corp-id = @corp-id
- wx.cp.corps.tenantId1.corp-secret = @corp-secret
+ # 自建应用 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.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
+ 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 配置(选填)
@@ -59,8 +74,10 @@
```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;
@@ -70,27 +87,21 @@ public class DemoService {
private WxCpMultiServices wxCpMultiServices;
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");
+ // 使用自建应用的 WxCpService(对应 corp-secret 为自建应用 Secret)
+ WxCpService appService = wxCpMultiServices.getWxCpService("app1");
+ WxCpUserService userService = appService.getUserService();
+ userService.getUserId("xxx");
// todo ...
- // 应用 3 的 WxCpService
- WxCpService wxCpService3 = wxCpMultiServices.getWxCpService("tenantId3");
- // 判断是否为空
- if (wxCpService3 == null) {
- // todo wxCpService3 为空,请先配置 tenantId3 企业微信应用参数
- return;
- }
- WxCpUserService userService3 = wxCpService3.getUserService();
- userService3.getUserId("xxx");
+ // 使用通讯录同步的 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
index 17e24bfe2d..bf52ba0508 100644
--- a/solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-cp-multi-solon-plugin/pom.xml
@@ -4,7 +4,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.0
+ 4.8.5.B
4.0.0
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
index ada4ac504c..25b4ab3747 100644
--- 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
@@ -15,6 +15,7 @@
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;
@@ -37,6 +38,13 @@ protected WxCpMultiServices wxCpMultiServices(WxCpMultiProperties wxCpMultiPrope
/**
* 校验同一个企业下,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();
@@ -49,8 +57,8 @@ protected WxCpMultiServices wxCpMultiServices(WxCpMultiProperties wxCpMultiPrope
String corpId = entry.getKey();
// 校验每个企业下,agentId 是否唯一
boolean multi = entry.getValue().stream()
- // 通讯录没有 agentId,如果不判断是否为空,这里会报 NPE 异常
- .collect(Collectors.groupingBy(c -> c.getAgentId() == null ? 0 : c.getAgentId(), Collectors.counting()))
+ // 通讯录没有 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 + "]");
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
index 821f885f98..2d4bffae66 100644
--- 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
@@ -52,7 +52,7 @@ public static class ConfigStorage implements Serializable {
/**
* http客户端类型.
*/
- private HttpClientType httpClientType = HttpClientType.HTTP_CLIENT;
+ private HttpClientType httpClientType = HttpClientType.HTTP_COMPONENTS;
/**
* http代理主机
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
index e761a09062..6f7f633c3f 100644
--- 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
@@ -8,6 +8,16 @@
/**
* 企业微信企业相关配置属性
*
+ * 企业微信中不同的 corpSecret 对应不同的权限范围,常见的有:
+ *
+ * - 自建应用 Secret:在"应用管理 - 自建应用"中查看,只能调用该应用有权限的接口
+ * - 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看,用于管理部门和成员(增删改查)
+ * - 客户联系 Secret:在"客户联系"中查看,用于客户联系相关接口
+ *
+ * 如需同时使用多种权限范围(例如:既要操作通讯录,又要调用自建应用接口),
+ * 可在 {@code wx.cp.corps} 下配置多个条目,每个条目使用对应权限的 {@code corpSecret},
+ * 其中通讯录同步的条目无需填写 {@code agentId}。
+ *
* @author yl
* created on 2023/10/16
*/
@@ -20,7 +30,16 @@ public class WxCpSingleProperties implements Serializable {
*/
private String corpId;
/**
- * 微信企业号 corpSecret
+ * 微信企业号 corpSecret(权限密钥)
+ *
+ * 企业微信针对不同的功能模块提供了不同的 Secret,每种 Secret 只对对应模块的接口有调用权限:
+ *
+ * - 自建应用 Secret:在"应用管理 - 自建应用"中找到对应应用,查看其 Secret,
+ * 使用时需同时配置对应的 {@code agentId}
+ * - 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看,
+ * 使用此 Secret 可管理部门、成员,无需配置 {@code agentId}
+ * - 其他 Secret(客户联系等):根据需要在企业微信后台查看对应 Secret
+ *
*/
private String corpSecret;
/**
@@ -28,7 +47,10 @@ public class WxCpSingleProperties implements Serializable {
*/
private String token;
/**
- * 微信企业号应用 ID
+ * 微信企业号应用 ID(AgentId)
+ *
+ * 使用自建应用 Secret 时,需要填写对应应用的 AgentId。
+ * 使用通讯录同步 Secret 时,无需填写此字段。
*/
private Integer agentId;
/**
diff --git a/solon-plugins/wx-java-cp-solon-plugin/pom.xml b/solon-plugins/wx-java-cp-solon-plugin/pom.xml
index 7e6f2f8164..78d994f172 100644
--- a/solon-plugins/wx-java-cp-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-cp-solon-plugin/pom.xml
@@ -4,7 +4,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.0
+ 4.8.5.B
4.0.0
diff --git a/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml
index 932f9244ce..38569db2a7 100644
--- a/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-miniapp-multi-solon-plugin/pom.xml
@@ -5,7 +5,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.0
+ 4.8.5.B
4.0.0
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
index fd94200e58..8ad85c96b8 100644
--- 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
@@ -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;
@@ -89,6 +90,9 @@ public WxMaService wxMaService(WxMaConfig wxMaConfig, WxMaMultiProperties wxMaMu
case HTTP_CLIENT:
wxMaService = new WxMaServiceHttpClientImpl();
break;
+ case HTTP_COMPONENTS:
+ wxMaService = new WxMaServiceHttpComponentsImpl();
+ break;
default:
wxMaService = new WxMaServiceImpl();
break;
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
index 87fcd42f03..f99d6280ec 100644
--- 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
@@ -77,7 +77,7 @@ public static class ConfigStorage implements Serializable {
/**
* http客户端类型.
*/
- private HttpClientType httpClientType = HttpClientType.HTTP_CLIENT;
+ private HttpClientType httpClientType = HttpClientType.HTTP_COMPONENTS;
/**
* http代理主机.
@@ -149,6 +149,10 @@ public enum HttpClientType {
/**
* JoddHttp
*/
- JODD_HTTP
+ JODD_HTTP,
+ /**
+ * HttpComponents
+ */
+ HTTP_COMPONENTS
}
}
diff --git a/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml b/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml
index 5ad8da85e6..0879651425 100644
--- a/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-miniapp-solon-plugin/pom.xml
@@ -4,7 +4,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.0
+ 4.8.5.B
4.0.0
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
index 5463ec08e9..78f95380b2 100644
--- 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
@@ -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;
@@ -44,6 +45,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/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
index a4475a02c7..d116a30cf6 100644
--- 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
@@ -19,4 +19,8 @@ public enum HttpClientType {
* JoddHttp.
*/
JoddHttp,
+ /**
+ * HttpComponents.
+ */
+ HttpComponents,
}
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
index 1c3e495f4e..4493b6aec5 100644
--- 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
@@ -76,7 +76,7 @@ public static class ConfigStorage {
/**
* http客户端类型.
*/
- private HttpClientType httpClientType = HttpClientType.HttpClient;
+ private HttpClientType httpClientType = HttpClientType.HttpComponents;
/**
* http代理主机.
diff --git a/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml b/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml
index 7c02acdfef..47ae9e94a9 100644
--- a/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-mp-multi-solon-plugin/pom.xml
@@ -5,7 +5,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.0
+ 4.8.5.B
4.0.0
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
index d534b98746..a51c6eaaea 100644
--- 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
@@ -8,6 +8,7 @@
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;
@@ -91,6 +92,9 @@ public WxMpService wxMpService(WxMpConfigStorage configStorage, WxMpMultiPropert
case HTTP_CLIENT:
wxMpService = new WxMpServiceHttpClientImpl();
break;
+ case HTTP_COMPONENTS:
+ wxMpService = new WxMpServiceHttpComponentsImpl();
+ break;
default:
wxMpService = new WxMpServiceImpl();
break;
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
index 1929e92607..3d47f71381 100644
--- 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
@@ -77,7 +77,7 @@ public static class ConfigStorage implements Serializable {
/**
* http客户端类型.
*/
- private HttpClientType httpClientType = HttpClientType.HTTP_CLIENT;
+ private HttpClientType httpClientType = HttpClientType.HTTP_COMPONENTS;
/**
* http代理主机.
@@ -149,6 +149,10 @@ public enum HttpClientType {
/**
* JoddHttp
*/
- JODD_HTTP
+ JODD_HTTP,
+ /**
+ * HttpComponents
+ */
+ HTTP_COMPONENTS
}
}
diff --git a/solon-plugins/wx-java-mp-solon-plugin/pom.xml b/solon-plugins/wx-java-mp-solon-plugin/pom.xml
index d2507cc0db..90787bdb04 100644
--- a/solon-plugins/wx-java-mp-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-mp-solon-plugin/pom.xml
@@ -5,7 +5,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.0
+ 4.8.5.B
4.0.0
@@ -22,7 +22,12 @@
redis.clients
jedis
- compile
+ provided
+
+
+ org.redisson
+ redisson
+ provided
org.jodd
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
index 3e7a598494..334ccf7abe 100644
--- 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
@@ -4,6 +4,7 @@
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;
@@ -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/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/WxMpStorageAutoConfiguration.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/WxMpStorageAutoConfiguration.java
deleted file mode 100644
index ac995dd1ec..0000000000
--- a/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/config/WxMpStorageAutoConfiguration.java
+++ /dev/null
@@ -1,128 +0,0 @@
-package com.binarywang.solon.wxjava.mp.config;
-
-import com.binarywang.solon.wxjava.mp.enums.StorageType;
-import com.binarywang.solon.wxjava.mp.properties.RedisProperties;
-import com.binarywang.solon.wxjava.mp.properties.WxMpProperties;
-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.mp.config.WxMpConfigStorage;
-import me.chanjar.weixin.mp.config.WxMpHostConfig;
-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.Jedis;
-import redis.clients.jedis.JedisPool;
-import redis.clients.jedis.JedisPoolConfig;
-import redis.clients.jedis.JedisSentinelPool;
-import redis.clients.jedis.util.Pool;
-
-import java.util.Set;
-
-/**
- * 微信公众号存储策略自动配置.
- *
- * @author Luo
- */
-@Slf4j
-@Configuration
-@RequiredArgsConstructor
-public class WxMpStorageAutoConfiguration {
- private final AppContext applicationContext;
-
- private final WxMpProperties wxMpProperties;
-
- @Bean
- @Condition(onMissingBean=WxMpConfigStorage.class)
- public WxMpConfigStorage wxMpConfigStorage() {
- StorageType type = wxMpProperties.getConfigStorage().getType();
- WxMpConfigStorage config;
- switch (type) {
- case Jedis:
- config = jedisConfigStorage();
- 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() {
- Pool 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 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.setUseStableAccessToken(wxMpProperties.isUseStableAccessToken());
- config.setHttpProxyHost(configStorageProperties.getHttpProxyHost());
- config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername());
- config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword());
- if (configStorageProperties.getHttpProxyPort() != null) {
- config.setHttpProxyPort(configStorageProperties.getHttpProxyPort());
- }
- }
-
- private Pool 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/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
index 9b1a8ccbf4..2858d77e43 100644
--- 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
@@ -19,4 +19,8 @@ public enum HttpClientType {
* JoddHttp.
*/
JoddHttp,
+ /**
+ * HttpComponents.
+ */
+ HttpComponents,
}
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
index 3368d34269..285d871f25 100644
--- 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
@@ -1,10 +1,13 @@
package com.binarywang.solon.wxjava.mp.integration;
import com.binarywang.solon.wxjava.mp.config.WxMpServiceAutoConfiguration;
-import com.binarywang.solon.wxjava.mp.config.WxMpStorageAutoConfiguration;
+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
@@ -13,8 +16,14 @@ public class WxMpPluginImpl implements Plugin {
@Override
public void start(AppContext context) throws Throwable {
context.beanMake(WxMpProperties.class);
-
- context.beanMake(WxMpStorageAutoConfiguration.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/WxMpProperties.java b/solon-plugins/wx-java-mp-solon-plugin/src/main/java/com/binarywang/solon/wxjava/mp/properties/WxMpProperties.java
index cda0aa88e7..0dcc6b41d3 100644
--- 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
@@ -79,7 +79,7 @@ public static class ConfigStorage implements Serializable {
/**
* http客户端类型.
*/
- private HttpClientType httpClientType = HttpClientType.HttpClient;
+ private HttpClientType httpClientType = HttpClientType.HttpComponents;
/**
* http代理主机.
diff --git a/solon-plugins/wx-java-open-solon-plugin/pom.xml b/solon-plugins/wx-java-open-solon-plugin/pom.xml
index 0f0527183f..45bf5e0c52 100644
--- a/solon-plugins/wx-java-open-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-open-solon-plugin/pom.xml
@@ -5,7 +5,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.0
+ 4.8.5.B
4.0.0
diff --git a/solon-plugins/wx-java-pay-solon-plugin/README.md b/solon-plugins/wx-java-pay-solon-plugin/README.md
index b0e212593b..8ff3416293 100644
--- a/solon-plugins/wx-java-pay-solon-plugin/README.md
+++ b/solon-plugins/wx-java-pay-solon-plugin/README.md
@@ -23,6 +23,8 @@ 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:开头的类路径
diff --git a/solon-plugins/wx-java-pay-solon-plugin/pom.xml b/solon-plugins/wx-java-pay-solon-plugin/pom.xml
index 7c1cb4e850..7bd0a955f1 100644
--- a/solon-plugins/wx-java-pay-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-pay-solon-plugin/pom.xml
@@ -5,7 +5,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.0
+ 4.8.5.B
4.0.0
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
index 94112c7d9d..c311a099a2 100644
--- 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
@@ -47,6 +47,7 @@ public WxPayService wxPayService() {
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()));
@@ -54,10 +55,11 @@ public WxPayService wxPayService() {
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());
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
index 0b035e983e..fe024f59f1 100644
--- 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
@@ -59,7 +59,7 @@ public class WxPayProperties {
/**
* apiV3秘钥
*/
- private String apiv3Key;
+ private String apiV3Key;
/**
* 微信支付分回调地址
@@ -87,6 +87,11 @@ public class WxPayProperties {
*/
private String notifyUrl;
+ /**
+ * 退款结果异步回调地址,通知url必须为直接可访问的url,不能携带参数.
+ */
+ private String refundNotifyUrl;
+
/**
* 微信支付分授权回调地址
*/
@@ -109,13 +114,19 @@ public class WxPayProperties {
private String apiHostUrl;
/**
- * 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认不添加
+ * 自定义API主机路径前缀(用于代理入口前缀)
+ * 例如:/api-weixin
+ */
+ private String apiHostUrlPath;
+
+ /**
+ * 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认添加
*/
- private boolean strictlyNeedWechatPaySerial = false;
+ private boolean strictlyNeedWechatPaySerial = true;
/**
- * 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认不使用
+ * 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认使用
*/
- private boolean fullPublicKeyModel = false;
+ private boolean fullPublicKeyModel = true;
}
diff --git a/solon-plugins/wx-java-qidian-solon-plugin/pom.xml b/solon-plugins/wx-java-qidian-solon-plugin/pom.xml
index 724bdf4ac5..0e47ac7c4e 100644
--- a/solon-plugins/wx-java-qidian-solon-plugin/pom.xml
+++ b/solon-plugins/wx-java-qidian-solon-plugin/pom.xml
@@ -3,7 +3,7 @@
wx-java-solon-plugins
com.github.binarywang
- 4.8.0
+ 4.8.5.B
4.0.0
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
index f3dce59a73..02ec06cd25 100644
--- 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
@@ -4,6 +4,7 @@
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;
@@ -35,6 +36,9 @@ public WxQidianService wxQidianService(WxQidianConfigStorage configStorage, WxQi
case HttpClient:
wxQidianService = newWxQidianServiceHttpClientImpl();
break;
+ case HttpComponents:
+ wxQidianService = newWxQidianServiceHttpComponentsImpl();
+ break;
default:
wxQidianService = newWxQidianServiceImpl();
break;
@@ -60,4 +64,8 @@ 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/enums/HttpClientType.java b/solon-plugins/wx-java-qidian-solon-plugin/src/main/java/com/binarywang/solon/wxjava/qidian/enums/HttpClientType.java
index 0b94821da7..5729ab8fda 100644
--- 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
@@ -19,4 +19,8 @@ public enum HttpClientType {
* JoddHttp.
*/
JoddHttp,
+ /**
+ * HttpComponents.
+ */
+ HttpComponents,
}
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
index 67c4dba543..e99f882e6f 100644
--- 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
@@ -74,7 +74,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/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 1f4881e9dd..9de6f2d82d 100644
--- a/spring-boot-starters/pom.xml
+++ b/spring-boot-starters/pom.xml
@@ -6,7 +6,7 @@
com.github.binarywang
wx-java
- 4.8.0
+ 4.8.5.B
pom
wx-java-spring-boot-starters
@@ -23,6 +23,8 @@
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
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
index b44f597d22..a1acf3a571 100644
--- 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
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.0
+ 4.8.5.B
4.0.0
@@ -32,6 +32,8 @@
org.springframework.data
spring-data-redis
+ ${spring-data-redis.version}
+ true
provided
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
index 3462bbc25e..e2f9f87f5a 100644
--- 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
@@ -9,6 +9,7 @@
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;
@@ -84,6 +85,9 @@ public WxChannelService wxChannelService(WxChannelConfig wxChannelConfig, WxChan
case HTTP_CLIENT:
wxChannelService = new WxChannelServiceHttpClientImpl();
break;
+ case HTTP_COMPONENTS:
+ wxChannelService = new WxChannelServiceHttpComponentsImpl();
+ break;
default:
wxChannelService = new WxChannelServiceImpl();
break;
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
index 6ca09354a3..adc8a2b52b 100644
--- 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
@@ -8,7 +8,7 @@
*/
public enum HttpClientType {
/**
- * HttpClient
+ * HttpClient.
*/
HTTP_CLIENT,
// WxChannelServiceOkHttpImpl 实现经测试无法正常完成业务固暂不支持OK_HTTP方式
@@ -16,4 +16,8 @@ public enum HttpClientType {
// * OkHttp.
// */
// OK_HTTP,
+ /**
+ * HttpComponents.
+ */
+ HTTP_COMPONENTS,
}
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
index 95021e2d22..8c8620f072 100644
--- a/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml
+++ b/spring-boot-starters/wx-java-channel-spring-boot-starter/pom.xml
@@ -3,7 +3,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.0
+ 4.8.5.B
4.0.0
@@ -30,6 +30,8 @@
org.springframework.data
spring-data-redis
+ ${spring-data-redis.version}
+ true
provided
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
index 63a7bf0c24..e4b3f3ad16 100644
--- 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
@@ -7,7 +7,11 @@
*/
public enum HttpClientType {
/**
- * HttpClient
+ * HttpClient.
*/
- 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/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
index 43c35fbd1d..f43d297e0b 100644
--- 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
@@ -85,7 +85,7 @@ public static class ConfigStorage {
/**
* http客户端类型
*/
- private HttpClientType httpClientType = HttpClientType.HttpClient;
+ private HttpClientType httpClientType = HttpClientType.HttpComponents;
/**
* http代理主机
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
index e3ea7bf0f8..0f0b74695e 100644
--- 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
@@ -6,6 +6,29 @@
- 未实现 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. 引入依赖
@@ -18,25 +41,21 @@
```
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
+ # 自建应用 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.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
+ 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 配置(选填)
@@ -59,8 +78,10 @@
```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;
@@ -70,27 +91,21 @@ public class DemoService {
private WxCpMultiServices wxCpMultiServices;
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");
+ // 使用自建应用的 WxCpService(对应 corp-secret 为自建应用 Secret)
+ WxCpService appService = wxCpMultiServices.getWxCpService("app1");
+ WxCpUserService userService = appService.getUserService();
+ userService.getUserId("xxx");
// todo ...
- // 应用 3 的 WxCpService
- WxCpService wxCpService3 = wxCpMultiServices.getWxCpService("tenantId3");
- // 判断是否为空
- if (wxCpService3 == null) {
- // todo wxCpService3 为空,请先配置 tenantId3 企业微信应用参数
- return;
- }
- WxCpUserService userService3 = wxCpService3.getUserService();
- userService3.getUserId("xxx");
+ // 使用通讯录同步的 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
index 550a14d2ad..f00b416488 100644
--- 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
@@ -4,7 +4,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.0
+ 4.8.5.B
4.0.0
@@ -31,6 +31,8 @@
org.springframework.data
spring-data-redis
+ ${spring-data-redis.version}
+ true
provided
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
index 9b959222e0..a793a8501f 100644
--- 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
@@ -18,6 +18,7 @@
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;
@@ -40,6 +41,13 @@ protected WxCpMultiServices wxCpMultiServices(WxCpMultiProperties wxCpMultiPrope
/**
* 校验同一个企业下,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();
@@ -52,8 +60,8 @@ protected WxCpMultiServices wxCpMultiServices(WxCpMultiProperties wxCpMultiPrope
String corpId = entry.getKey();
// 校验每个企业下,agentId 是否唯一
boolean multi = entry.getValue().stream()
- // 通讯录没有 agentId,如果不判断是否为空,这里会报 NPE 异常
- .collect(Collectors.groupingBy(c -> c.getAgentId() == null ? 0 : c.getAgentId(), Collectors.counting()))
+ // 通讯录没有 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 + "]");
@@ -117,7 +125,7 @@ private WxCpService wxCpService(WxCpConfigStorage wxCpConfigStorage, WxCpMultiPr
private void configCorp(WxCpDefaultConfigImpl config, WxCpSingleProperties wxCpSingleProperties) {
String corpId = wxCpSingleProperties.getCorpId();
String corpSecret = wxCpSingleProperties.getCorpSecret();
- Integer agentId = wxCpSingleProperties.getAgentId();
+ Long agentId = wxCpSingleProperties.getAgentId();
String token = wxCpSingleProperties.getToken();
String aesKey = wxCpSingleProperties.getAesKey();
// 企业微信,私钥,会话存档路径
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
index 8ad7149fe6..740f5a2a3b 100644
--- 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
@@ -8,6 +8,16 @@
/**
* 企业微信企业相关配置属性
*
+ * 企业微信中不同的 corpSecret 对应不同的权限范围,常见的有:
+ *
+ * - 自建应用 Secret:在"应用管理 - 自建应用"中查看,只能调用该应用有权限的接口
+ * - 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看,用于管理部门和成员(增删改查)
+ * - 客户联系 Secret:在"客户联系"中查看,用于客户联系相关接口
+ *
+ * 如需同时使用多种权限范围(例如:既要操作通讯录,又要调用自建应用接口),
+ * 可在 {@code wx.cp.corps} 下配置多个条目,每个条目使用对应权限的 {@code corpSecret},
+ * 其中通讯录同步的条目无需填写 {@code agentId}。
+ *
* @author yl
* created on 2023/10/16
*/
@@ -20,7 +30,16 @@ public class WxCpSingleProperties implements Serializable {
*/
private String corpId;
/**
- * 微信企业号 corpSecret
+ * 微信企业号 corpSecret(权限密钥)
+ *
+ * 企业微信针对不同的功能模块提供了不同的 Secret,每种 Secret 只对对应模块的接口有调用权限:
+ *
+ * - 自建应用 Secret:在"应用管理 - 自建应用"中找到对应应用,查看其 Secret,
+ * 使用时需同时配置对应的 {@code agentId}
+ * - 通讯录同步 Secret:在"管理工具 - 通讯录同步"中查看,
+ * 使用此 Secret 可管理部门、成员,无需配置 {@code agentId}
+ * - 其他 Secret(客户联系等):根据需要在企业微信后台查看对应 Secret
+ *
*/
private String corpSecret;
/**
@@ -28,9 +47,12 @@ public class WxCpSingleProperties implements Serializable {
*/
private String token;
/**
- * 微信企业号应用 ID
+ * 微信企业号应用 ID(AgentId)
+ *
+ * 使用自建应用 Secret 时,需要填写对应应用的 AgentId。
+ * 使用通讯录同步 Secret 时,无需填写此字段。
*/
- private Integer agentId;
+ private Long agentId;
/**
* 微信企业号应用 EncodingAESKey
*/
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 81f68274c5..9e204e82cb 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.8.0
+ 4.8.5.B
4.0.0
@@ -29,6 +29,9 @@
org.springframework.data
spring-data-redis
+ ${spring-data-redis.version}
+ true
+ provided
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 c93a7e187f..63e730d1b4 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
@@ -34,7 +34,7 @@ public class WxCpProperties {
/**
* 微信企业号应用 ID
*/
- private Integer agentId;
+ private Long agentId;
/**
* 微信企业号应用 EncodingAESKey
*/
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 2b1d8c13c5..70f620a58d 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
@@ -15,7 +15,7 @@ public abstract class AbstractWxCpConfigStorageConfiguration {
protected WxCpDefaultConfigImpl config(WxCpDefaultConfigImpl config, WxCpProperties properties) {
String corpId = properties.getCorpId();
String corpSecret = properties.getCorpSecret();
- Integer agentId = properties.getAgentId();
+ Long agentId = properties.getAgentId();
String token = properties.getToken();
String aesKey = properties.getAesKey();
// 企业微信,私钥,会话存档路径
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
index f1cc1fba13..3a66ab1b8a 100644
--- 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
@@ -4,7 +4,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.0
+ 4.8.5.B
4.0.0
@@ -31,6 +31,8 @@
org.springframework.data
spring-data-redis
+ ${spring-data-redis.version}
+ true
provided
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/pom.xml b/spring-boot-starters/wx-java-miniapp-multi-spring-boot-starter/pom.xml
index 8c8854067f..7f561529b0 100644
--- 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
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.0
+ 4.8.5.B
4.0.0
@@ -32,6 +32,8 @@
org.springframework.data
spring-data-redis
+ ${spring-data-redis.version}
+ true
provided
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
index 15e638f89e..fba9d875ee 100644
--- 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
@@ -4,6 +4,7 @@
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;
@@ -16,8 +17,10 @@
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;
/**
@@ -33,9 +36,10 @@ public abstract class AbstractWxMaConfiguration {
protected WxMaMultiServices wxMaMultiServices(WxMaMultiProperties wxMaMultiProperties) {
Map appsMap = wxMaMultiProperties.getApps();
if (appsMap == null || appsMap.isEmpty()) {
- log.warn("微信公众号应用参数未配置,通过 WxMaMultiServices#getWxMaService(\"tenantId\")获取实例将返回空");
+ log.warn("微信小程序应用参数未配置,通过 WxMaMultiServices#getWxMaService(\"tenantId\")获取实例将返回空");
return new WxMaMultiServicesImpl();
}
+
/**
* 校验 appId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。
*
@@ -49,12 +53,29 @@ protected WxMaMultiServices wxMaMultiServices(WxMaMultiProperties wxMaMultiPrope
.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 的唯一性");
+ throw new RuntimeException("请确保微信小程序配置 appId 的唯一性");
}
}
- WxMaMultiServicesImpl services = new WxMaMultiServicesImpl();
+ // 根据配置选择多租户模式
+ 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();
@@ -64,37 +85,63 @@ protected WxMaMultiServices wxMaMultiServices(WxMaMultiProperties wxMaMultiPrope
WxMaService wxMaService = this.wxMaService(storage, wxMaMultiProperties);
services.addWxMaService(tenantId, wxMaService);
}
+
+ log.info("微信小程序多租户服务初始化完成,使用隔离模式(ISOLATED),共配置 {} 个租户", appsMap.size());
return services;
}
/**
- * 配置 WxMaDefaultConfigImpl
- *
- * @param wxMaMultiProperties 参数
- * @return WxMaDefaultConfigImpl
+ * 创建共享模式的多租户服务(单个 WxMaService 实例管理多个配置)
*/
- protected abstract WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties);
-
- public WxMaService wxMaService(WxMaConfig wxMaConfig, WxMaMultiProperties wxMaMultiProperties) {
+ private WxMaMultiServices createSharedMultiServices(
+ Map appsMap,
+ WxMaMultiProperties wxMaMultiProperties) {
+
+ // 创建共享的 WxMaService 实例
WxMaMultiProperties.ConfigStorage storage = wxMaMultiProperties.getConfigStorage();
- WxMaMultiProperties.HttpClientType httpClientType = storage.getHttpClientType();
- WxMaService wxMaService;
+ 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:
- wxMaService = new WxMaServiceOkHttpImpl();
- break;
+ return new WxMaServiceOkHttpImpl();
case JODD_HTTP:
- wxMaService = new WxMaServiceJoddHttpImpl();
- break;
+ return new WxMaServiceJoddHttpImpl();
case HTTP_CLIENT:
- wxMaService = new WxMaServiceHttpClientImpl();
- break;
+ return new WxMaServiceHttpClientImpl();
default:
- wxMaService = new WxMaServiceImpl();
- break;
+ return new WxMaServiceImpl();
}
+ }
- wxMaService.setWxMaConfig(wxMaConfig);
+ /**
+ * 配置 WxMaService 的通用参数
+ */
+ private void configureWxMaService(WxMaService wxMaService, WxMaMultiProperties.ConfigStorage storage) {
int maxRetryTimes = storage.getMaxRetryTimes();
if (maxRetryTimes < 0) {
maxRetryTimes = 0;
@@ -105,6 +152,21 @@ public WxMaService wxMaService(WxMaConfig wxMaConfig, WxMaMultiProperties wxMaMu
}
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;
}
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
index 6dae33d584..201aceb8bf 100644
--- 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
@@ -116,6 +116,15 @@ public static class ConfigStorage implements Serializable {
*
*/
private int retrySleepMillis = 1000;
+
+ /**
+ * 多租户实现模式.
+ *
+ * - ISOLATED: 为每个租户创建独立的 WxMaService 实例(默认)
+ * - SHARED: 使用单个 WxMaService 实例管理所有租户配置,共享 HTTP 客户端
+ *
+ */
+ private MultiTenantMode multiTenantMode = MultiTenantMode.ISOLATED;
}
public enum StorageType {
@@ -151,4 +160,19 @@ public enum HttpClientType {
*/
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/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-spring-boot-starter/pom.xml b/spring-boot-starters/wx-java-miniapp-spring-boot-starter/pom.xml
index bcc61b0309..40a18523e9 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
@@ -4,7 +4,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.0
+ 4.8.5.B
4.0.0
@@ -31,6 +31,8 @@
org.springframework.data
spring-data-redis
+ ${spring-data-redis.version}
+ true
provided
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/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 b3e4b464fe..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
@@ -8,7 +8,7 @@
*/
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/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 7e88db904f..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
@@ -88,7 +88,7 @@ public static class ConfigStorage {
/**
* http客户端类型.
*/
- private HttpClientType httpClientType = HttpClientType.HttpClient;
+ private HttpClientType httpClientType = HttpClientType.HttpComponents;
/**
* http代理主机.
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
index 6323ae4b6a..90318a0d57 100644
--- 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
@@ -5,7 +5,7 @@
wx-java-spring-boot-starters
com.github.binarywang
- 4.8.0
+ 4.8.5.B
4.0.0
@@ -32,6 +32,8 @@
org.springframework.data
spring-data-redis
+ ${spring-data-redis.version}
+ true
provided
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
index 1f431b645d..46724c625f 100644
--- 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
@@ -4,18 +4,25 @@
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.*;
+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;
/**
@@ -34,6 +41,7 @@ protected WxMpMultiServices wxMpMultiServices(WxMpMultiProperties wxMpMultiPrope
log.warn("微信公众号应用参数未配置,通过 WxMpMultiServices#getWxMpService(\"tenantId\")获取实例将返回空");
return new WxMpMultiServicesImpl();
}
+
/**
* 校验 appId 是否唯一,避免使用 redis 缓存 token、ticket 时错乱。
*
@@ -50,9 +58,26 @@ protected WxMpMultiServices wxMpMultiServices(WxMpMultiProperties wxMpMultiPrope
throw new RuntimeException("请确保微信公众号配置 appId 的唯一性");
}
}
- WxMpMultiServicesImpl services = new WxMpMultiServicesImpl();
+ // 根据配置选择多租户模式
+ 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();
@@ -63,40 +88,66 @@ protected WxMpMultiServices wxMpMultiServices(WxMpMultiProperties wxMpMultiPrope
WxMpService wxMpService = this.wxMpService(storage, wxMpMultiProperties);
services.addWxMpService(tenantId, wxMpService);
}
+
+ log.info("微信公众号多租户服务初始化完成,使用隔离模式(ISOLATED),共配置 {} 个租户", appsMap.size());
return services;
}
/**
- * 配置 WxMpDefaultConfigImpl
- *
- * @param wxMpMultiProperties 参数
- * @return WxMpDefaultConfigImpl
+ * 创建共享模式的多租户服务(单个 WxMpService 实例管理多个配置)
*/
- protected abstract WxMpDefaultConfigImpl wxMpConfigStorage(WxMpMultiProperties wxMpMultiProperties);
-
- public WxMpService wxMpService(WxMpConfigStorage configStorage, WxMpMultiProperties wxMpMultiProperties) {
+ private WxMpMultiServices createSharedMultiServices(
+ Map appsMap,
+ WxMpMultiProperties wxMpMultiProperties) {
+
+ // 创建共享的 WxMpService 实例
WxMpMultiProperties.ConfigStorage storage = wxMpMultiProperties.getConfigStorage();
- WxMpMultiProperties.HttpClientType httpClientType = storage.getHttpClientType();
- WxMpService wxMpService;
+ 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:
- wxMpService = new WxMpServiceOkHttpImpl();
- break;
+ return new WxMpServiceOkHttpImpl();
case JODD_HTTP:
- wxMpService = new WxMpServiceJoddHttpImpl();
- break;
+ return new WxMpServiceJoddHttpImpl();
case HTTP_CLIENT:
- wxMpService = new WxMpServiceHttpClientImpl();
- break;
+ return new WxMpServiceHttpClientImpl();
case HTTP_COMPONENTS:
- wxMpService = new WxMpServiceHttpComponentsImpl();
- break;
+ return new WxMpServiceHttpComponentsImpl();
default:
- wxMpService = new WxMpServiceImpl();
- break;
+ return new WxMpServiceImpl();
}
+ }
- wxMpService.setWxMpConfigStorage(configStorage);
+ /**
+ * 配置 WxMpService 的通用参数
+ */
+ private void configureWxMpService(WxMpService wxMpService, WxMpMultiProperties.ConfigStorage storage) {
int maxRetryTimes = storage.getMaxRetryTimes();
if (maxRetryTimes < 0) {
maxRetryTimes = 0;
@@ -107,6 +158,21 @@ public WxMpService wxMpService(WxMpConfigStorage configStorage, WxMpMultiPropert
}
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;
}
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
index 8b2fa58aa3..9dd95f9531 100644
--- 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
@@ -116,6 +116,15 @@ public static class ConfigStorage implements Serializable {
*
*/
private int retrySleepMillis = 1000;
+
+ /**
+ * 多租户实现模式.
+ *
+ * - ISOLATED: 为每个租户创建独立的 WxMpService 实例(默认)
+ * - SHARED: 使用单个 WxMpService 实例管理所有租户配置,共享 HTTP 客户端
+ *
+ */
+ private MultiTenantMode multiTenantMode = MultiTenantMode.ISOLATED;
}
public enum StorageType {
@@ -155,4 +164,19 @@ public enum HttpClientType {
*/
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/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-spring-boot-starter/README.md b/spring-boot-starters/wx-java-mp-spring-boot-starter/README.md
index 3e14f499d9..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
@@ -27,7 +27,7 @@
#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=
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 273364c9a7..6b4993ca77 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.8.0
+ 4.8.5.B
4.0.0
@@ -22,11 +22,13 @@
redis.clients
jedis
- compile
+ provided
org.springframework.data
spring-data-redis
+ ${spring-data-redis.version}
+ true
provided
@@ -44,6 +46,11 @@
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/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 4c0938454a..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,175 +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.common.util.http.apache.ApacheHttpClientBuilder;
-import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder;
-import me.chanjar.weixin.mp.config.WxMpConfigStorage;
-import me.chanjar.weixin.mp.config.WxMpHostConfig;
-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.ConditionalOnMissingBean;
-import org.springframework.context.ApplicationContext;
-import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-import org.springframework.data.redis.core.StringRedisTemplate;
-import redis.clients.jedis.Jedis;
-import redis.clients.jedis.JedisPool;
-import redis.clients.jedis.JedisPoolConfig;
-import redis.clients.jedis.JedisSentinelPool;
-import redis.clients.jedis.util.Pool;
-
-import java.util.Set;
+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() {
- Pool 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());
- WxMpProperties.ConfigStorage storage = properties.getConfigStorage();
- // 设置自定义的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);
- }
- config.setUseStableAccessToken(wxMpProperties.isUseStableAccessToken());
- config.setHttpProxyHost(configStorageProperties.getHttpProxyHost());
- config.setHttpProxyUsername(configStorageProperties.getHttpProxyUsername());
- config.setHttpProxyPassword(configStorageProperties.getHttpProxyPassword());
- if (configStorageProperties.getHttpProxyPort() != null) {
- config.setHttpProxyPort(configStorageProperties.getHttpProxyPort());
- }
- }
-
- private Pool 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/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 377fb5b6ab..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
@@ -80,7 +80,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-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..5b9e2d4df7
--- /dev/null
+++ b/spring-boot-starters/wx-java-open-multi-spring-boot-starter/pom.xml
@@ -0,0 +1,64 @@
+
+
+
+ wx-java-spring-boot-starters
+ com.github.binarywang
+ 4.8.5.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
+ ${spring-data-redis.version}
+ true
+ 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..f4245402e0
--- /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,162 @@
+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.WxOpenServiceApacheHttpClientImpl;
+import me.chanjar.weixin.open.api.impl.WxOpenServiceHttpComponentsImpl;
+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) {
+ WxOpenMultiProperties.HttpClientType httpClientType = wxOpenMultiProperties.getConfigStorage().getHttpClientType();
+ WxOpenService wxOpenService;
+ if (httpClientType == WxOpenMultiProperties.HttpClientType.APACHE_HTTP) {
+ wxOpenService = new WxOpenServiceApacheHttpClientImpl();
+ } else {
+ wxOpenService = new WxOpenServiceHttpComponentsImpl();
+ }
+ 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);
+
+ // 仅在使用 Apache HttpClient 4.x 时配置 ApacheHttpClientBuilder 超时参数
+ if (storage.getHttpClientType() == WxOpenMultiProperties.HttpClientType.APACHE_HTTP) {
+ 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..0c6a222891
--- /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,141 @@
+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;
+
+ /**
+ * http客户端类型.
+ */
+ private HttpClientType httpClientType = HttpClientType.HTTP_COMPONENTS;
+
+ /**
+ * 指定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
+ }
+
+ public enum HttpClientType {
+ /**
+ * Apache HttpClient 4.x
+ */
+ APACHE_HTTP,
+ /**
+ * Apache HttpClient 5.x (HttpComponents)
+ */
+ HTTP_COMPONENTS
+ }
+
+}
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 9a25cd89d7..e6cc597ebc 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.8.0
+ 4.8.5.B
4.0.0
@@ -30,6 +30,9 @@
org.springframework.data
spring-data-redis
+ ${spring-data-redis.version}
+ true
+ provided
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..39cb67b4a8
--- /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.5.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 d87a38fb9c..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
@@ -23,6 +23,8 @@ 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:开头的类路径
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 8b67ade1ea..4c1a99c0a5 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.8.0
+ 4.8.5.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 5a794de7e8..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
@@ -51,6 +51,7 @@ public WxPayService wxPayService() {
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()));
@@ -58,10 +59,11 @@ public WxPayService wxPayService() {
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());
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 8212e3b013..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,18 @@ public class WxPayProperties {
/**
* apiV3秘钥
*/
- private String apiv3Key;
+ private String apiV3Key;
/**
* 微信支付异步回调地址,通知url必须为直接可访问的url,不能携带参数
*/
private String notifyUrl;
+ /**
+ * 退款结果异步回调地址,通知url必须为直接可访问的url,不能携带参数
+ */
+ private String refundNotifyUrl;
+
/**
* 微信支付分回调地址
*/
@@ -107,13 +112,19 @@ public class WxPayProperties {
private String apiHostUrl;
/**
- * 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认不添加
+ * 自定义API主机路径前缀(用于代理入口前缀)
+ * 例如:/api-weixin
+ */
+ private String apiHostUrlPath;
+
+ /**
+ * 是否将全部v3接口的请求都添加Wechatpay-Serial请求头,默认添加
*/
- private boolean strictlyNeedWechatPaySerial = false;
+ private boolean strictlyNeedWechatPaySerial = true;
/**
- * 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认不使用
+ * 是否完全使用公钥模式(用以微信从平台证书到公钥的灰度切换),默认使用
*/
- private boolean fullPublicKeyModel = false;
+ private boolean fullPublicKeyModel = true;
}
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 a0fc329434..71851fd59d 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.8.0
+ 4.8.5.B
4.0.0
@@ -26,6 +26,8 @@
org.springframework.data
spring-data-redis
+ ${spring-data-redis.version}
+ true
provided
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 1a927211cc..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
@@ -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/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/weixin-graal/pom.xml b/weixin-graal/pom.xml
index 3a220b2888..881dc911fe 100644
--- a/weixin-graal/pom.xml
+++ b/weixin-graal/pom.xml
@@ -6,7 +6,7 @@
com.github.binarywang
wx-java
- 4.8.0
+ 4.8.5.B
weixin-graal
diff --git a/weixin-java-aispeech/pom.xml b/weixin-java-aispeech/pom.xml
new file mode 100644
index 0000000000..6b5e68a1fa
--- /dev/null
+++ b/weixin-java-aispeech/pom.xml
@@ -0,0 +1,83 @@
+
+
+ 4.0.0
+
+ com.github.binarywang
+ wx-java
+ 4.8.5.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.apache.httpcomponents
+ httpclient
+
+
+ org.apache.httpcomponents
+ httpmime
+
org.apache.httpcomponents.client5
httpclient5
@@ -111,12 +119,7 @@
jedis-lock
true
-
- org.mockito
- mockito-core
- 3.3.3
- test
-
+
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelAfterSaleService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelAfterSaleService.java
index 85c945d428..b8d1156b66 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelAfterSaleService.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelAfterSaleService.java
@@ -77,6 +77,22 @@ AfterSaleListResponse listIds(Long beginCreateTime, Long endCreateTime, String n
*/
WxChannelBaseResponse reject(String afterSaleOrderId, String rejectReason, Integer rejectReasonType) throws WxErrorException;
+ /**
+ * 拒绝售后(支持拒绝凭证)
+ * 文档地址 https://developers.weixin.qq.com/doc/channels/API/aftersale/rejectapply.html
+ *
+ * @param afterSaleOrderId 售后单号
+ * @param rejectReason 拒绝原因
+ * @param rejectReasonType 拒绝原因枚举值
+ * @param rejectCertificates 拒绝凭证图片列表,可使用图片上传接口获取media_id
+ * @see #getRejectReason()
+ * @return BaseResponse
+ *
+ * @throws WxErrorException 异常
+ */
+ WxChannelBaseResponse reject(String afterSaleOrderId, String rejectReason, Integer rejectReasonType,
+ List rejectCertificates) throws WxErrorException;
+
/**
* 上传退款凭证
*
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelBasicService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelBasicService.java
index a687aaeb5c..4d2133a56d 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelBasicService.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelBasicService.java
@@ -5,7 +5,10 @@
import me.chanjar.weixin.channel.bean.image.ChannelImageInfo;
import me.chanjar.weixin.channel.bean.image.ChannelImageResponse;
import me.chanjar.weixin.channel.bean.image.QualificationFileResponse;
+import me.chanjar.weixin.channel.bean.shop.ShopH5UrlResponse;
import me.chanjar.weixin.channel.bean.shop.ShopInfoResponse;
+import me.chanjar.weixin.channel.bean.shop.ShopQrCodeResponse;
+import me.chanjar.weixin.channel.bean.shop.ShopTagLinkResponse;
import me.chanjar.weixin.common.error.WxErrorException;
/**
@@ -22,6 +25,31 @@ public interface WxChannelBasicService {
*/
ShopInfoResponse getShopInfo() throws WxErrorException;
+ /**
+ * 获取店铺H5链接
+ *
+ * @return 店铺H5链接
+ * @throws WxErrorException 异常
+ */
+ ShopH5UrlResponse getShopH5Url() throws WxErrorException;
+
+ /**
+ * 获取店铺二维码
+ *
+ * @param qrcodeType 二维码类型,1:二维码;2:标准物料;3:送礼物物料
+ * @return 店铺二维码
+ * @throws WxErrorException 异常
+ */
+ ShopQrCodeResponse getShopQrCode(int qrcodeType) throws WxErrorException;
+
+ /**
+ * 获取店铺口令
+ *
+ * @return 店铺口令
+ * @throws WxErrorException 异常
+ */
+ ShopTagLinkResponse getShopTagLink() throws WxErrorException;
+
/**
* 上传图片
*
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelCategoryService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelCategoryService.java
index 0b357a5d1c..ad86697614 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelCategoryService.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelCategoryService.java
@@ -6,11 +6,7 @@
import me.chanjar.weixin.channel.bean.audit.AuditResponse;
import me.chanjar.weixin.channel.bean.audit.CategoryAuditInfo;
import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
-import me.chanjar.weixin.channel.bean.category.CategoryDetailResult;
-import me.chanjar.weixin.channel.bean.category.CategoryQualificationResponse;
-import me.chanjar.weixin.channel.bean.category.PassCategoryResponse;
-import me.chanjar.weixin.channel.bean.category.ShopCategory;
-import me.chanjar.weixin.channel.bean.category.ShopCategoryResponse;
+import me.chanjar.weixin.channel.bean.category.*;
import me.chanjar.weixin.common.error.WxErrorException;
/**
@@ -121,4 +117,16 @@ AuditApplyResponse addCategory(String level1, String level2, String level3, List
* @throws WxErrorException 异常
*/
PassCategoryResponse listPassCategory() throws WxErrorException;
+
+ /**
+ * 获取店铺的类目权限列表
+ *
+ * @param isFilterStatus 是否按状态筛选
+ * @param status 类目状态(当 isFilterStatus 为 true 时有效)
+ * @return 类目权限列表
+ *
+ * @throws WxErrorException 异常
+ */
+ RelationCategoryResponse listRelationCategory(Boolean isFilterStatus, Integer status) throws WxErrorException;
+
}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelFavoriteService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelFavoriteService.java
new file mode 100644
index 0000000000..c1c03cc615
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelFavoriteService.java
@@ -0,0 +1,21 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.favorite.FavoriteCountResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号小店 收藏管理接口
+ *
+ * @author GitHub Copilot
+ * @link 收藏管理接口文档
+ */
+public interface WxChannelFavoriteService {
+
+ /**
+ * 获取店铺收藏的人数
+ *
+ * @return 店铺收藏人数响应
+ * @throws WxErrorException 异常
+ */
+ FavoriteCountResponse getFavoriteCount() throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelOrderService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelOrderService.java
index 7be0382bac..e2da5c7a5b 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelOrderService.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxChannelOrderService.java
@@ -9,10 +9,15 @@
import me.chanjar.weixin.channel.bean.order.ChangeOrderInfo;
import me.chanjar.weixin.channel.bean.order.DecodeSensitiveInfoResponse;
import me.chanjar.weixin.channel.bean.order.DeliveryUpdateParam;
+import me.chanjar.weixin.channel.bean.order.OrderCompensationDeliveryParam;
import me.chanjar.weixin.channel.bean.order.OrderInfoResponse;
import me.chanjar.weixin.channel.bean.order.OrderListParam;
import me.chanjar.weixin.channel.bean.order.OrderListResponse;
import me.chanjar.weixin.channel.bean.order.OrderSearchParam;
+import me.chanjar.weixin.channel.bean.order.PreShipmentChangeSkuResponse;
+import me.chanjar.weixin.channel.bean.order.PresentSubOrderResponse;
+import me.chanjar.weixin.channel.bean.order.PrivateNumberGetPhoneResponse;
+import me.chanjar.weixin.channel.bean.order.RealNumberViewAuditResponse;
import me.chanjar.weixin.channel.bean.order.VirtualTelNumberResponse;
import me.chanjar.weixin.common.error.WxErrorException;
@@ -200,4 +205,122 @@ WxChannelBaseResponse updatePrice(String orderId, Integer expressFee, ListGitHub Copilot
+ * @see 代发管理接口文档
+ */
+public interface WxChannelSupplierService {
+
+ /** 获取供货商列表。 */
+ SupplierListResponse getSupplierList() throws WxErrorException;
+
+ /**
+ * 获取供货商列表。
+ *
+ * @param pageSize 每页数量
+ * @param nextKey 由上次请求返回,记录翻页的上下文。传入时会从上次返回的结果往后翻一页
+ * @return 供货商列表响应
+ * @throws WxErrorException 异常
+ */
+ SupplierListResponse getSupplierList(Integer pageSize, String nextKey) throws WxErrorException;
+
+ /** 获取分配方式。 */
+ DistributeTypeResponse getDistribute() throws WxErrorException;
+
+ /** 设置全店订单手动分配。 */
+ WxChannelBaseResponse setManuallyDistribute() throws WxErrorException;
+
+ /** 设置全店订单自动分配。 */
+ WxChannelBaseResponse setAllDistribute(String supplierId) throws WxErrorException;
+
+ /** 设置按商品自动分配。 */
+ WxChannelBaseResponse setProductDistribute(ProductDistributeRequest req) throws WxErrorException;
+
+ /** 获取商品对应的自动分配供货商。 */
+ SupplierInfoResponse getProductDefaultDistribute(String productId) throws WxErrorException;
+
+ /** 获取按商品自动分配的商品列表。 */
+ ProductListResponse getProductList(String supplierId) throws WxErrorException;
+
+ /** 分配订单代发。 */
+ DropshipResponse assignOrder(DropshipAssignRequest req) throws WxErrorException;
+
+ /** 取消分配代发单。 */
+ WxChannelBaseResponse cancelDropship(String orderId) throws WxErrorException;
+
+ /** 查询代发单详情。 */
+ DropshipDetailResponse getDropship(String orderId) throws WxErrorException;
+
+ /** 拉取代发单列表。 */
+ DropshipListResponse listDropship(DropshipListRequest req) throws WxErrorException;
+
+ /** 搜索代发单。 */
+ DropshipListResponse searchDropship(DropshipSearchRequest req) throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxTalentService.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxTalentService.java
new file mode 100644
index 0000000000..e03cdbf502
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/WxTalentService.java
@@ -0,0 +1,56 @@
+package me.chanjar.weixin.channel.api;
+
+import me.chanjar.weixin.channel.bean.talent.TalentOrderDetailParam;
+import me.chanjar.weixin.channel.bean.talent.TalentOrderDetailResponse;
+import me.chanjar.weixin.channel.bean.talent.TalentOrderListParam;
+import me.chanjar.weixin.channel.bean.talent.TalentOrderListResponse;
+import me.chanjar.weixin.channel.bean.talent.TalentWindowProductDetailParam;
+import me.chanjar.weixin.channel.bean.talent.TalentWindowProductDetailResponse;
+import me.chanjar.weixin.channel.bean.talent.TalentWindowProductListParam;
+import me.chanjar.weixin.channel.bean.talent.TalentWindowProductListResponse;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 微信小店-带货助手服务接口
+ *
+ * @author GitHub Copilot
+ */
+public interface WxTalentService {
+
+ /**
+ * 获取佣金单列表
+ *
+ * @param param 查询参数
+ * @return 佣金单列表
+ * @throws WxErrorException 接口调用异常
+ */
+ TalentOrderListResponse getOrderList(TalentOrderListParam param) throws WxErrorException;
+
+ /**
+ * 获取佣金单详情
+ *
+ * @param param 查询参数
+ * @return 佣金单详情
+ * @throws WxErrorException 接口调用异常
+ */
+ TalentOrderDetailResponse getOrderDetail(TalentOrderDetailParam param) throws WxErrorException;
+
+ /**
+ * 获取达人橱窗商品列表
+ *
+ * @param param 查询参数
+ * @return 橱窗商品列表
+ * @throws WxErrorException 接口调用异常
+ */
+ TalentWindowProductListResponse getWindowProductList(TalentWindowProductListParam param) throws WxErrorException;
+
+ /**
+ * 获取达人橱窗商品详情
+ *
+ * @param param 查询参数
+ * @return 橱窗商品详情
+ * @throws WxErrorException 接口调用异常
+ */
+ TalentWindowProductDetailResponse getWindowProductDetail(TalentWindowProductDetailParam param)
+ throws WxErrorException;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/BaseWxChannelServiceImpl.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/BaseWxChannelServiceImpl.java
index 1a608e1f6a..5283769932 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/BaseWxChannelServiceImpl.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/BaseWxChannelServiceImpl.java
@@ -50,6 +50,7 @@ public abstract class BaseWxChannelServiceImpl implements WxChannelService
private WxStoreHomePageService homePageService = null;
private WxStoreCooperationService cooperationService = null;
private WxChannelCompassShopService compassShopService = null;
+ private WxChannelSupplierService supplierService = null;
private WxLeagueWindowService leagueWindowService = null;
private WxLeagueSupplierService leagueSupplierService = null;
private WxLeaguePromoterService leaguePromoterService = null;
@@ -60,6 +61,9 @@ public abstract class BaseWxChannelServiceImpl implements WxChannelService
private WxChannelVipService vipService = null;
private WxChannelCompassFinderService compassFinderService = null;
private WxChannelLiveDashboardService liveDashboardService = null;
+ private WxChannelQicService qicService = null;
+ private WxTalentService talentService = null;
+ private WxChannelFavoriteService favoriteService = null;
protected WxChannelConfig config;
private int retrySleepMillis = 1000;
@@ -392,6 +396,14 @@ public synchronized WxChannelCompassShopService getCompassShopService() {
return compassShopService;
}
+ @Override
+ public synchronized WxChannelSupplierService getSupplierService() {
+ if (supplierService == null) {
+ supplierService = new WxChannelSupplierServiceImpl(this);
+ }
+ return supplierService;
+ }
+
@Override
public synchronized WxLeagueWindowService getLeagueWindowService() {
if (leagueWindowService == null) {
@@ -473,4 +485,28 @@ public synchronized WxChannelLiveDashboardService getLiveDashboardService() {
return liveDashboardService;
}
+ @Override
+ public synchronized WxChannelQicService getQicService() {
+ if (qicService == null) {
+ qicService = new WxChannelQicServiceImpl(this);
+ }
+ return qicService;
+ }
+
+ @Override
+ public synchronized WxTalentService getTalentService() {
+ if (talentService == null) {
+ talentService = new WxTalentServiceImpl(this);
+ }
+ return talentService;
+ }
+
+ @Override
+ public synchronized WxChannelFavoriteService getFavoriteService() {
+ if (favoriteService == null) {
+ favoriteService = new WxChannelFavoriteServiceImpl(this);
+ }
+ return favoriteService;
+ }
+
}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxAssistantServiceImpl.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxAssistantServiceImpl.java
index 55be5abcca..58d6d45269 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxAssistantServiceImpl.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxAssistantServiceImpl.java
@@ -31,25 +31,25 @@ public class WxAssistantServiceImpl implements WxAssistantService {
private final BaseWxChannelServiceImpl, ?> shopService;
@Override
public WxChannelBaseResponse addWindowProduct(AddWindowProductRequest req) throws WxErrorException {
- String resJson = shopService.post(ADD_WINDOW_PRODUCT_URL, "{}");
+ String resJson = shopService.post(ADD_WINDOW_PRODUCT_URL, req);
return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
}
@Override
public GetWindowProductResponse getWindowProduct(WindowProductRequest req) throws WxErrorException {
- String resJson = shopService.post(GET_WINDOW_PRODUCT_URL, "{}");
+ String resJson = shopService.post(GET_WINDOW_PRODUCT_URL, req);
return ResponseUtils.decode(resJson, GetWindowProductResponse.class);
}
@Override
public GetWindowProductListResponse getWindowProductList(GetWindowProductListRequest req) throws WxErrorException {
- String resJson = shopService.post(LIST_WINDOW_PRODUCT_URL, "{}");
+ String resJson = shopService.post(LIST_WINDOW_PRODUCT_URL, req);
return ResponseUtils.decode(resJson, GetWindowProductListResponse.class);
}
@Override
public WxChannelBaseResponse offWindowProduct(WindowProductRequest req) throws WxErrorException {
- String resJson = shopService.post(OFF_WINDOW_PRODUCT_URL, "{}");
+ String resJson = shopService.post(OFF_WINDOW_PRODUCT_URL, req);
return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
}
}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelAfterSaleServiceImpl.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelAfterSaleServiceImpl.java
index 92f865444b..c7cdf9167a 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelAfterSaleServiceImpl.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelAfterSaleServiceImpl.java
@@ -61,7 +61,13 @@ public WxChannelBaseResponse accept(String afterSaleOrderId, String addressId, I
@Override
public WxChannelBaseResponse reject(String afterSaleOrderId, String rejectReason, Integer rejectReasonType) throws WxErrorException {
- AfterSaleRejectParam param = new AfterSaleRejectParam(afterSaleOrderId, rejectReason, rejectReasonType);
+ return reject(afterSaleOrderId, rejectReason, rejectReasonType, null);
+ }
+
+ @Override
+ public WxChannelBaseResponse reject(String afterSaleOrderId, String rejectReason, Integer rejectReasonType,
+ List rejectCertificates) throws WxErrorException {
+ AfterSaleRejectParam param = new AfterSaleRejectParam(afterSaleOrderId, rejectReason, rejectReasonType, rejectCertificates);
String resJson = shopService.post(AFTER_SALE_REJECT_URL, param);
return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelBasicServiceImpl.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelBasicServiceImpl.java
index 6eb699da23..45a869216e 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelBasicServiceImpl.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelBasicServiceImpl.java
@@ -2,7 +2,10 @@
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Basics.GET_ADDRESS_CODE;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Basics.GET_IMG_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Basics.GET_SHOP_H5URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Basics.GET_SHOP_INFO;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Basics.GET_SHOP_QRCODE;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Basics.GET_SHOP_TAGLINK;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Basics.IMG_UPLOAD_URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Basics.UPLOAD_QUALIFICATION_FILE;
@@ -16,7 +19,10 @@
import me.chanjar.weixin.channel.bean.image.ChannelImageResponse;
import me.chanjar.weixin.channel.bean.image.QualificationFileResponse;
import me.chanjar.weixin.channel.bean.image.UploadImageResponse;
+import me.chanjar.weixin.channel.bean.shop.ShopH5UrlResponse;
import me.chanjar.weixin.channel.bean.shop.ShopInfoResponse;
+import me.chanjar.weixin.channel.bean.shop.ShopQrCodeResponse;
+import me.chanjar.weixin.channel.bean.shop.ShopTagLinkResponse;
import me.chanjar.weixin.channel.executor.ChannelFileUploadRequestExecutor;
import me.chanjar.weixin.channel.executor.ChannelMediaDownloadRequestExecutor;
import me.chanjar.weixin.channel.util.ResponseUtils;
@@ -93,4 +99,23 @@ public AddressCodeResponse getAddressCode(Integer code) throws WxErrorException
return ResponseUtils.decode(resJson, AddressCodeResponse.class);
}
+ @Override
+ public ShopH5UrlResponse getShopH5Url() throws WxErrorException {
+ String resJson = shopService.post(GET_SHOP_H5URL, "{}");
+ return ResponseUtils.decode(resJson, ShopH5UrlResponse.class);
+ }
+
+ @Override
+ public ShopQrCodeResponse getShopQrCode(int qrcodeType) throws WxErrorException {
+ String reqJson = "{\"qrcode_type\":" + qrcodeType + "}";
+ String resJson = shopService.post(GET_SHOP_QRCODE, reqJson);
+ return ResponseUtils.decode(resJson, ShopQrCodeResponse.class);
+ }
+
+ @Override
+ public ShopTagLinkResponse getShopTagLink() throws WxErrorException {
+ String resJson = shopService.post(GET_SHOP_TAGLINK, "{}");
+ return ResponseUtils.decode(resJson, ShopTagLinkResponse.class);
+ }
+
}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelCategoryServiceImpl.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelCategoryServiceImpl.java
index 23cd839848..7070ab9298 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelCategoryServiceImpl.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelCategoryServiceImpl.java
@@ -1,13 +1,5 @@
package me.chanjar.weixin.channel.api.impl;
-import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Category.ADD_CATEGORY_URL;
-import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Category.AVAILABLE_CATEGORY_URL;
-import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Category.CANCEL_CATEGORY_AUDIT_URL;
-import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Category.GET_CATEGORY_AUDIT_URL;
-import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Category.GET_CATEGORY_DETAIL_URL;
-import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Category.LIST_ALL_CATEGORY_URL;
-import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Category.LIST_PASS_CATEGORY_URL;
-
import java.util.Collections;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
@@ -17,17 +9,15 @@
import me.chanjar.weixin.channel.bean.audit.CategoryAuditInfo;
import me.chanjar.weixin.channel.bean.audit.CategoryAuditRequest;
import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
-import me.chanjar.weixin.channel.bean.category.CategoryDetailResult;
-import me.chanjar.weixin.channel.bean.category.CategoryQualificationResponse;
-import me.chanjar.weixin.channel.bean.category.PassCategoryResponse;
-import me.chanjar.weixin.channel.bean.category.ShopCategory;
-import me.chanjar.weixin.channel.bean.category.ShopCategoryResponse;
+import me.chanjar.weixin.channel.bean.category.*;
import me.chanjar.weixin.channel.util.JsonUtils;
import me.chanjar.weixin.channel.util.ResponseUtils;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor;
import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Category.*;
+
/**
* 视频号小店 商品类目相关接口
*
@@ -135,4 +125,15 @@ public PassCategoryResponse listPassCategory() throws WxErrorException {
return ResponseUtils.decode(resJson, PassCategoryResponse.class);
}
+ @Override
+ public RelationCategoryResponse listRelationCategory(Boolean isFilterStatus, Integer status) throws WxErrorException {
+ RelationCategoryRequest request = new RelationCategoryRequest(
+ isFilterStatus != null ? isFilterStatus : false,
+ status != null ? status : 0
+ );
+ String reqJson = JsonUtils.encode(request);
+ String resJson = shopService.post(LIST_RELATION_CATEGORY_URL, reqJson);
+ return ResponseUtils.decode(resJson, RelationCategoryResponse.class);
+ }
+
}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelFavoriteServiceImpl.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelFavoriteServiceImpl.java
new file mode 100644
index 0000000000..8165f47987
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelFavoriteServiceImpl.java
@@ -0,0 +1,29 @@
+package me.chanjar.weixin.channel.api.impl;
+
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Favorite.GET_FAVORITE_COUNT;
+
+import me.chanjar.weixin.channel.api.WxChannelFavoriteService;
+import me.chanjar.weixin.channel.bean.favorite.FavoriteCountResponse;
+import me.chanjar.weixin.channel.util.ResponseUtils;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+/**
+ * 视频号小店 收藏管理接口实现
+ *
+ * @author GitHub Copilot
+ */
+public class WxChannelFavoriteServiceImpl implements WxChannelFavoriteService {
+
+ /** 微信商店服务 */
+ private final BaseWxChannelServiceImpl, ?> shopService;
+
+ public WxChannelFavoriteServiceImpl(BaseWxChannelServiceImpl, ?> shopService) {
+ this.shopService = shopService;
+ }
+
+ @Override
+ public FavoriteCountResponse getFavoriteCount() throws WxErrorException {
+ String resJson = shopService.post(GET_FAVORITE_COUNT, "{}");
+ return ResponseUtils.decode(resJson, FavoriteCountResponse.class);
+ }
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelOrderServiceImpl.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelOrderServiceImpl.java
index fd26268333..eff93082d7 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelOrderServiceImpl.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelOrderServiceImpl.java
@@ -5,16 +5,29 @@
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Delivery.GET_DELIVERY_COMPANY_URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.ACCEPT_ADDRESS_MODIFY_URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.DECODE_SENSITIVE_INFO_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.DELIVERY_COMPENSATION_URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.ORDER_GET_URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.ORDER_LIST_URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.ORDER_SEARCH_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.PRE_SHIPMENT_CHANGE_SKU_APPROVE_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.PRE_SHIPMENT_CHANGE_SKU_GET_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.PRE_SHIPMENT_CHANGE_SKU_REJECT_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.PRESENT_NOTE_ADD_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.PRESENT_SUB_ORDER_GET_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.REAL_NUMBER_APPLY_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.REAL_NUMBER_VIEW_AUDIT_GET_URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.REJECT_ADDRESS_MODIFY_URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.UPDATE_ADDRESS_URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.UPDATE_EXPRESS_URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.UPDATE_PRICE_URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.UPDATE_REMARK_URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.UPLOAD_FRESH_INSPECT_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.VIRTUAL_NUMBER_APPLY_AGAIN_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.VIRTUAL_NUMBER_DELAY_URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Order.VIRTUAL_TEL_NUMBER_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.PrivateNumber.ADD_PHONE_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.PrivateNumber.GET_PHONE_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.PrivateNumber.SEND_VERIFY_CODE_URL;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
@@ -30,6 +43,7 @@
import me.chanjar.weixin.channel.bean.order.DecodeSensitiveInfoResponse;
import me.chanjar.weixin.channel.bean.order.DeliveryUpdateParam;
import me.chanjar.weixin.channel.bean.order.OrderAddressParam;
+import me.chanjar.weixin.channel.bean.order.OrderCompensationDeliveryParam;
import me.chanjar.weixin.channel.bean.order.OrderIdParam;
import me.chanjar.weixin.channel.bean.order.OrderInfoParam;
import me.chanjar.weixin.channel.bean.order.OrderInfoResponse;
@@ -38,6 +52,14 @@
import me.chanjar.weixin.channel.bean.order.OrderPriceParam;
import me.chanjar.weixin.channel.bean.order.OrderRemarkParam;
import me.chanjar.weixin.channel.bean.order.OrderSearchParam;
+import me.chanjar.weixin.channel.bean.order.PreShipmentChangeSkuRejectParam;
+import me.chanjar.weixin.channel.bean.order.PreShipmentChangeSkuResponse;
+import me.chanjar.weixin.channel.bean.order.PresentNoteAddParam;
+import me.chanjar.weixin.channel.bean.order.PresentSubOrderResponse;
+import me.chanjar.weixin.channel.bean.order.PrivateNumberAddPhoneParam;
+import me.chanjar.weixin.channel.bean.order.PrivateNumberGetPhoneResponse;
+import me.chanjar.weixin.channel.bean.order.PrivateNumberSendVerifyCodeParam;
+import me.chanjar.weixin.channel.bean.order.RealNumberViewAuditResponse;
import me.chanjar.weixin.channel.bean.order.VirtualTelNumberResponse;
import me.chanjar.weixin.channel.util.ResponseUtils;
import me.chanjar.weixin.common.error.WxErrorException;
@@ -178,4 +200,95 @@ public DecodeSensitiveInfoResponse decodeSensitiveInfo(String orderId) throws Wx
String resJson = shopService.post(DECODE_SENSITIVE_INFO_URL, reqJson);
return ResponseUtils.decode(resJson, DecodeSensitiveInfoResponse.class);
}
+
+ @Override
+ public WxChannelBaseResponse addPresentNote(String orderId, String note) throws WxErrorException {
+ PresentNoteAddParam param = new PresentNoteAddParam(orderId, note);
+ String resJson = shopService.post(PRESENT_NOTE_ADD_URL, param);
+ return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
+ }
+
+ @Override
+ public PresentSubOrderResponse getPresentSubOrders(String orderId) throws WxErrorException {
+ OrderIdParam param = new OrderIdParam(orderId);
+ String resJson = shopService.post(PRESENT_SUB_ORDER_GET_URL, param);
+ return ResponseUtils.decode(resJson, PresentSubOrderResponse.class);
+ }
+
+ @Override
+ public PreShipmentChangeSkuResponse getPreShipmentChangeSku(String orderId) throws WxErrorException {
+ OrderIdParam param = new OrderIdParam(orderId);
+ String resJson = shopService.post(PRE_SHIPMENT_CHANGE_SKU_GET_URL, param);
+ return ResponseUtils.decode(resJson, PreShipmentChangeSkuResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse approvePreShipmentChangeSku(String orderId) throws WxErrorException {
+ OrderIdParam param = new OrderIdParam(orderId);
+ String resJson = shopService.post(PRE_SHIPMENT_CHANGE_SKU_APPROVE_URL, param);
+ return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse rejectPreShipmentChangeSku(String orderId, String rejectReason)
+ throws WxErrorException {
+ PreShipmentChangeSkuRejectParam param = new PreShipmentChangeSkuRejectParam(orderId, rejectReason);
+ String resJson = shopService.post(PRE_SHIPMENT_CHANGE_SKU_REJECT_URL, param);
+ return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse applyRealNumber(String orderId) throws WxErrorException {
+ OrderIdParam param = new OrderIdParam(orderId);
+ String resJson = shopService.post(REAL_NUMBER_APPLY_URL, param);
+ return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
+ }
+
+ @Override
+ public RealNumberViewAuditResponse getRealNumberViewAudit(String orderId) throws WxErrorException {
+ OrderIdParam param = new OrderIdParam(orderId);
+ String resJson = shopService.post(REAL_NUMBER_VIEW_AUDIT_GET_URL, param);
+ return ResponseUtils.decode(resJson, RealNumberViewAuditResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse applyVirtualNumberAgain(String orderId) throws WxErrorException {
+ OrderIdParam param = new OrderIdParam(orderId);
+ String resJson = shopService.post(VIRTUAL_NUMBER_APPLY_AGAIN_URL, param);
+ return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse delayVirtualNumber(String orderId) throws WxErrorException {
+ OrderIdParam param = new OrderIdParam(orderId);
+ String resJson = shopService.post(VIRTUAL_NUMBER_DELAY_URL, param);
+ return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse addPrivatePhone(String phone) throws WxErrorException {
+ PrivateNumberAddPhoneParam param = new PrivateNumberAddPhoneParam(phone);
+ String resJson = shopService.post(ADD_PHONE_URL, param);
+ return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse sendPrivatePhoneVerifyCode(String phone) throws WxErrorException {
+ PrivateNumberSendVerifyCodeParam param = new PrivateNumberSendVerifyCodeParam(phone);
+ String resJson = shopService.post(SEND_VERIFY_CODE_URL, param);
+ return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
+ }
+
+ @Override
+ public PrivateNumberGetPhoneResponse getPrivatePhone() throws WxErrorException {
+ String resJson = shopService.post(GET_PHONE_URL, "{}");
+ return ResponseUtils.decode(resJson, PrivateNumberGetPhoneResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse compensationDelivery(OrderCompensationDeliveryParam param)
+ throws WxErrorException {
+ String resJson = shopService.post(DELIVERY_COMPENSATION_URL, param);
+ return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
+ }
}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelProductServiceImpl.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelProductServiceImpl.java
index 08c9638f0c..b3974e0aa7 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelProductServiceImpl.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelProductServiceImpl.java
@@ -4,6 +4,15 @@
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Spu.ADD_LIMIT_TASK_URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Spu.CANCEL_AUDIT_URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Spu.DELETE_LIMIT_TASK_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Spu.GIFT_ACTIVITY_ADD_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Spu.GIFT_ACTIVITY_DELETE_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Spu.GIFT_ACTIVITY_STOP_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Spu.GIFT_PRODUCT_ADD_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Spu.GIFT_PRODUCT_GET_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Spu.GIFT_PRODUCT_LIST_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Spu.GIFT_PRODUCT_ON_SALE_SET_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Spu.GIFT_PRODUCT_STOCK_UPDATE_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Spu.GIFT_PRODUCT_UPDATE_URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Spu.LIST_LIMIT_TASK_URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Spu.SPU_ADD_URL;
import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Spu.SPU_AUDIT_FREE_UPDATE_URL;
@@ -29,6 +38,14 @@
import me.chanjar.weixin.channel.bean.limit.LimitTaskListParam;
import me.chanjar.weixin.channel.bean.limit.LimitTaskListResponse;
import me.chanjar.weixin.channel.bean.limit.LimitTaskParam;
+import me.chanjar.weixin.channel.bean.product.GiftActivityAddParam;
+import me.chanjar.weixin.channel.bean.product.GiftActivityAddResponse;
+import me.chanjar.weixin.channel.bean.product.GiftActivityInfo;
+import me.chanjar.weixin.channel.bean.product.GiftProductAddResponse;
+import me.chanjar.weixin.channel.bean.product.GiftProductGetResponse;
+import me.chanjar.weixin.channel.bean.product.GiftProductInfo;
+import me.chanjar.weixin.channel.bean.product.GiftProductListParam;
+import me.chanjar.weixin.channel.bean.product.GiftProductListResponse;
import me.chanjar.weixin.channel.bean.product.SkuStockBatchParam;
import me.chanjar.weixin.channel.bean.product.SkuStockBatchResponse;
import me.chanjar.weixin.channel.bean.product.SkuStockParam;
@@ -211,6 +228,72 @@ public ProductTagLinkResponse getProductTagLink(String productId) throws WxError
return ResponseUtils.decode(resJson, ProductTagLinkResponse.class);
}
+ @Override
+ public GiftProductAddResponse addGiftProduct(GiftProductInfo info) throws WxErrorException {
+ String reqJson = JsonUtils.encode(info);
+ String resJson = shopService.post(GIFT_PRODUCT_ADD_URL, reqJson);
+ return ResponseUtils.decode(resJson, GiftProductAddResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse updateGiftProduct(GiftProductInfo info) throws WxErrorException {
+ String reqJson = JsonUtils.encode(info);
+ String resJson = shopService.post(GIFT_PRODUCT_UPDATE_URL, reqJson);
+ return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse setProductAsGift(String productId) throws WxErrorException {
+ String reqJson = "{\"product_id\":\"" + productId + "\"}";
+ String resJson = shopService.post(GIFT_PRODUCT_ON_SALE_SET_URL, reqJson);
+ return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
+ }
+
+ @Override
+ public GiftProductGetResponse getGiftProduct(String productId) throws WxErrorException {
+ String reqJson = "{\"product_id\":\"" + productId + "\"}";
+ String resJson = shopService.post(GIFT_PRODUCT_GET_URL, reqJson);
+ return ResponseUtils.decode(resJson, GiftProductGetResponse.class);
+ }
+
+ @Override
+ public GiftProductListResponse listGiftProduct(GiftProductListParam param) throws WxErrorException {
+ String reqJson = JsonUtils.encode(param);
+ String resJson = shopService.post(GIFT_PRODUCT_LIST_URL, reqJson);
+ return ResponseUtils.decode(resJson, GiftProductListResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse updateGiftStock(String productId, String skuId, Integer diffType, Integer num)
+ throws WxErrorException {
+ SkuStockParam param = new SkuStockParam(productId, skuId, diffType, num);
+ String reqJson = JsonUtils.encode(param);
+ String resJson = shopService.post(GIFT_PRODUCT_STOCK_UPDATE_URL, reqJson);
+ return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
+ }
+
+ @Override
+ public GiftActivityAddResponse addGiftActivity(GiftActivityInfo info) throws WxErrorException {
+ GiftActivityAddParam param = new GiftActivityAddParam(info);
+ String reqJson = JsonUtils.encode(param);
+ String resJson = shopService.post(GIFT_ACTIVITY_ADD_URL, reqJson);
+ return ResponseUtils.decode(resJson, GiftActivityAddResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse deleteGiftActivity(String activityId) throws WxErrorException {
+ String reqJson = "{\"activity_id\":\"" + activityId + "\"}";
+ String resJson = shopService.post(GIFT_ACTIVITY_DELETE_URL, reqJson);
+ return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse stopGiftActivity(String activityId) throws WxErrorException {
+ String reqJson = "{\"activity_id\":\"" + activityId + "\"}";
+ String resJson = shopService.post(GIFT_ACTIVITY_STOP_URL, reqJson);
+ return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
+ }
+
@Override
public LimitTaskAddResponse addLimitTask(LimitTaskParam param) throws WxErrorException {
String reqJson = JsonUtils.encode(param);
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelQicServiceImpl.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelQicServiceImpl.java
new file mode 100644
index 0000000000..a23532b6fd
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelQicServiceImpl.java
@@ -0,0 +1,75 @@
+package me.chanjar.weixin.channel.api.impl;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import me.chanjar.weixin.channel.api.WxChannelQicService;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.qic.InspectCodeResponse;
+import me.chanjar.weixin.channel.bean.qic.InspectConfigResponse;
+import me.chanjar.weixin.channel.bean.qic.RegisterLogisticsRequest;
+import me.chanjar.weixin.channel.bean.qic.SubmitConfigResponse;
+import me.chanjar.weixin.channel.bean.qic.SubmitInspectRequest;
+import me.chanjar.weixin.channel.util.ResponseUtils;
+import me.chanjar.weixin.common.error.WxErrorException;
+import org.apache.commons.lang3.StringUtils;
+
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Qic.GET_INSPECT_CONFIG_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Qic.GET_SUBMIT_CONFIG_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Qic.PRINT_INSPECT_CODE_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Qic.REGISTER_LOGISTICS_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Qic.SUBMIT_INSPECT_INFO_URL;
+
+/**
+ * 视频号小店 质检管理服务实现.
+ */
+public class WxChannelQicServiceImpl implements WxChannelQicService {
+ private final BaseWxChannelServiceImpl, ?> shopService;
+
+ public WxChannelQicServiceImpl(BaseWxChannelServiceImpl, ?> shopService) {
+ this.shopService = shopService;
+ }
+
+ @Override
+ public InspectConfigResponse getInspectConfig() throws WxErrorException {
+ String respJson = shopService.get(GET_INSPECT_CONFIG_URL, null);
+ return ResponseUtils.decode(respJson, InspectConfigResponse.class);
+ }
+
+ @Override
+ public SubmitConfigResponse getSubmitConfig(String orderId) throws WxErrorException {
+ String queryParam = StringUtils.isBlank(orderId) ? null : "order_id=" + orderId;
+ String respJson = shopService.get(GET_SUBMIT_CONFIG_URL, queryParam);
+ return ResponseUtils.decode(respJson, SubmitConfigResponse.class);
+ }
+
+ @Override
+ public SubmitConfigResponse getSubmitConfig() throws WxErrorException {
+ return getSubmitConfig(null);
+ }
+
+ @Override
+ public InspectCodeResponse printInspectCode(String orderId) throws WxErrorException {
+ String respJson = shopService.post(PRINT_INSPECT_CODE_URL, new PrintInspectCodeRequest(orderId));
+ return ResponseUtils.decode(respJson, InspectCodeResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse submitInspectInfo(SubmitInspectRequest request) throws WxErrorException {
+ String respJson = shopService.post(SUBMIT_INSPECT_INFO_URL, request);
+ return ResponseUtils.decode(respJson, WxChannelBaseResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse registerLogistics(RegisterLogisticsRequest request) throws WxErrorException {
+ String respJson = shopService.post(REGISTER_LOGISTICS_URL, request);
+ return ResponseUtils.decode(respJson, WxChannelBaseResponse.class);
+ }
+
+ @Data
+ @AllArgsConstructor
+ private static class PrintInspectCodeRequest {
+ @JsonProperty("order_id")
+ private String orderId;
+ }
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelServiceHttpComponentsImpl.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelServiceHttpComponentsImpl.java
index 6cf2d38503..f4cbb04755 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelServiceHttpComponentsImpl.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelServiceHttpComponentsImpl.java
@@ -5,7 +5,6 @@
import me.chanjar.weixin.channel.config.WxChannelConfig;
import me.chanjar.weixin.channel.util.JsonUtils;
import me.chanjar.weixin.common.util.http.HttpClientType;
-import me.chanjar.weixin.common.util.http.apache.ApacheBasicResponseHandler;
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;
@@ -41,7 +40,7 @@ public void initHttp() {
apacheHttpClientBuilder.httpProxyHost(config.getHttpProxyHost())
.httpProxyPort(config.getHttpProxyPort())
.httpProxyUsername(config.getHttpProxyUsername())
- .httpProxyPassword(config.getHttpProxyPassword().toCharArray());
+ .httpProxyPassword(config.getHttpProxyPassword() == null ? null : config.getHttpProxyPassword().toCharArray());
if (config.getHttpProxyHost() != null && config.getHttpProxyPort() > 0) {
this.httpProxy = new HttpHost(config.getHttpProxyHost(), config.getHttpProxyPort());
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelSupplierServiceImpl.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelSupplierServiceImpl.java
new file mode 100644
index 0000000000..451aebc8a0
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxChannelSupplierServiceImpl.java
@@ -0,0 +1,132 @@
+package me.chanjar.weixin.channel.api.impl;
+
+import com.google.gson.JsonObject;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.channel.api.WxChannelSupplierService;
+import me.chanjar.weixin.channel.bean.base.StreamPageParam;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.supplier.DistributeTypeResponse;
+import me.chanjar.weixin.channel.bean.supplier.DropshipAssignRequest;
+import me.chanjar.weixin.channel.bean.supplier.DropshipDetailResponse;
+import me.chanjar.weixin.channel.bean.supplier.DropshipListRequest;
+import me.chanjar.weixin.channel.bean.supplier.DropshipListResponse;
+import me.chanjar.weixin.channel.bean.supplier.DropshipResponse;
+import me.chanjar.weixin.channel.bean.supplier.DropshipSearchRequest;
+import me.chanjar.weixin.channel.bean.supplier.ProductDistributeRequest;
+import me.chanjar.weixin.channel.bean.supplier.ProductListResponse;
+import me.chanjar.weixin.channel.bean.supplier.SupplierInfoResponse;
+import me.chanjar.weixin.channel.bean.supplier.SupplierListResponse;
+import me.chanjar.weixin.channel.util.ResponseUtils;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.json.GsonHelper;
+
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Supplier.ASSIGN_DROPSHIP_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Supplier.CANCEL_DROPSHIP_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Supplier.GET_DISTRIBUTE_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Supplier.GET_DROPSHIP_LIST_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Supplier.GET_DROPSHIP_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Supplier.GET_PRODUCT_DEFAULT_DISTRIBUTE_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Supplier.GET_PRODUCT_LIST_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Supplier.GET_SUPPLIER_LIST_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Supplier.SEARCH_DROPSHIP_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Supplier.SET_ALL_DISTRIBUTION_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Supplier.SET_MANUALLY_DISTRIBUTE_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Supplier.SET_PRODUCT_DISTRIBUTE_URL;
+
+/**
+ * 视频号小店代发管理服务。
+ *
+ * @author GitHub Copilot
+ */
+@Slf4j
+public class WxChannelSupplierServiceImpl implements WxChannelSupplierService {
+
+ private final BaseWxChannelServiceImpl, ?> shopService;
+
+ public WxChannelSupplierServiceImpl(BaseWxChannelServiceImpl, ?> shopService) {
+ this.shopService = shopService;
+ }
+
+ @Override
+ public SupplierListResponse getSupplierList() throws WxErrorException {
+ return getSupplierList(null, null);
+ }
+
+ @Override
+ public SupplierListResponse getSupplierList(Integer pageSize, String nextKey) throws WxErrorException {
+ StreamPageParam param = new StreamPageParam(pageSize, nextKey);
+ String respJson = shopService.post(GET_SUPPLIER_LIST_URL, param);
+ return ResponseUtils.decode(respJson, SupplierListResponse.class);
+ }
+
+ @Override
+ public DistributeTypeResponse getDistribute() throws WxErrorException {
+ String respJson = shopService.post(GET_DISTRIBUTE_URL, "{}");
+ return ResponseUtils.decode(respJson, DistributeTypeResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse setManuallyDistribute() throws WxErrorException {
+ String respJson = shopService.post(SET_MANUALLY_DISTRIBUTE_URL, "{}");
+ return ResponseUtils.decode(respJson, WxChannelBaseResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse setAllDistribute(String supplierId) throws WxErrorException {
+ JsonObject req = GsonHelper.buildJsonObject("supplier_id", supplierId);
+ String respJson = shopService.post(SET_ALL_DISTRIBUTION_URL, req);
+ return ResponseUtils.decode(respJson, WxChannelBaseResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse setProductDistribute(ProductDistributeRequest req) throws WxErrorException {
+ String respJson = shopService.post(SET_PRODUCT_DISTRIBUTE_URL, req);
+ return ResponseUtils.decode(respJson, WxChannelBaseResponse.class);
+ }
+
+ @Override
+ public SupplierInfoResponse getProductDefaultDistribute(String productId) throws WxErrorException {
+ JsonObject req = GsonHelper.buildJsonObject("product_id", productId);
+ String respJson = shopService.post(GET_PRODUCT_DEFAULT_DISTRIBUTE_URL, req);
+ return ResponseUtils.decode(respJson, SupplierInfoResponse.class);
+ }
+
+ @Override
+ public ProductListResponse getProductList(String supplierId) throws WxErrorException {
+ JsonObject req = GsonHelper.buildJsonObject("supplier_id", supplierId);
+ String respJson = shopService.post(GET_PRODUCT_LIST_URL, req);
+ return ResponseUtils.decode(respJson, ProductListResponse.class);
+ }
+
+ @Override
+ public DropshipResponse assignOrder(DropshipAssignRequest req) throws WxErrorException {
+ String respJson = shopService.post(ASSIGN_DROPSHIP_URL, req);
+ return ResponseUtils.decode(respJson, DropshipResponse.class);
+ }
+
+ @Override
+ public WxChannelBaseResponse cancelDropship(String orderId) throws WxErrorException {
+ JsonObject req = GsonHelper.buildJsonObject("order_id", orderId);
+ String respJson = shopService.post(CANCEL_DROPSHIP_URL, req);
+ return ResponseUtils.decode(respJson, WxChannelBaseResponse.class);
+ }
+
+ @Override
+ public DropshipDetailResponse getDropship(String orderId) throws WxErrorException {
+ JsonObject req = GsonHelper.buildJsonObject("order_id", orderId);
+ String respJson = shopService.post(GET_DROPSHIP_URL, req);
+ return ResponseUtils.decode(respJson, DropshipDetailResponse.class);
+ }
+
+ @Override
+ public DropshipListResponse listDropship(DropshipListRequest req) throws WxErrorException {
+ String respJson = shopService.post(GET_DROPSHIP_LIST_URL, req);
+ return ResponseUtils.decode(respJson, DropshipListResponse.class);
+ }
+
+ @Override
+ public DropshipListResponse searchDropship(DropshipSearchRequest req) throws WxErrorException {
+ String respJson = shopService.post(SEARCH_DROPSHIP_URL, req);
+ return ResponseUtils.decode(respJson, DropshipListResponse.class);
+ }
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxTalentServiceImpl.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxTalentServiceImpl.java
new file mode 100644
index 0000000000..6bb623e9a4
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/api/impl/WxTalentServiceImpl.java
@@ -0,0 +1,59 @@
+package me.chanjar.weixin.channel.api.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.channel.api.WxTalentService;
+import me.chanjar.weixin.channel.bean.talent.TalentOrderDetailParam;
+import me.chanjar.weixin.channel.bean.talent.TalentOrderDetailResponse;
+import me.chanjar.weixin.channel.bean.talent.TalentOrderListParam;
+import me.chanjar.weixin.channel.bean.talent.TalentOrderListResponse;
+import me.chanjar.weixin.channel.bean.talent.TalentWindowProductDetailParam;
+import me.chanjar.weixin.channel.bean.talent.TalentWindowProductDetailResponse;
+import me.chanjar.weixin.channel.bean.talent.TalentWindowProductListParam;
+import me.chanjar.weixin.channel.bean.talent.TalentWindowProductListResponse;
+import me.chanjar.weixin.channel.util.ResponseUtils;
+import me.chanjar.weixin.common.error.WxErrorException;
+
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Talent.GET_ORDER_DETAIL_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Talent.GET_ORDER_LIST_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Talent.GET_WINDOW_PRODUCT_DETAIL_URL;
+import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Talent.GET_WINDOW_PRODUCT_LIST_URL;
+
+/**
+ * 微信小店-带货助手服务实现
+ *
+ * @author GitHub Copilot
+ */
+@RequiredArgsConstructor
+@Slf4j
+public class WxTalentServiceImpl implements WxTalentService {
+
+ /** 微信商店服务 */
+ private final BaseWxChannelServiceImpl, ?> shopService;
+
+ @Override
+ public TalentOrderListResponse getOrderList(TalentOrderListParam param) throws WxErrorException {
+ String resJson = shopService.post(GET_ORDER_LIST_URL, param);
+ return ResponseUtils.decode(resJson, TalentOrderListResponse.class);
+ }
+
+ @Override
+ public TalentOrderDetailResponse getOrderDetail(TalentOrderDetailParam param) throws WxErrorException {
+ String resJson = shopService.post(GET_ORDER_DETAIL_URL, param);
+ return ResponseUtils.decode(resJson, TalentOrderDetailResponse.class);
+ }
+
+ @Override
+ public TalentWindowProductListResponse getWindowProductList(TalentWindowProductListParam param)
+ throws WxErrorException {
+ String resJson = shopService.post(GET_WINDOW_PRODUCT_LIST_URL, param);
+ return ResponseUtils.decode(resJson, TalentWindowProductListResponse.class);
+ }
+
+ @Override
+ public TalentWindowProductDetailResponse getWindowProductDetail(TalentWindowProductDetailParam param)
+ throws WxErrorException {
+ String resJson = shopService.post(GET_WINDOW_PRODUCT_DETAIL_URL, param);
+ return ResponseUtils.decode(resJson, TalentWindowProductDetailResponse.class);
+ }
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/after/AfterSaleRejectParam.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/after/AfterSaleRejectParam.java
index cbde459fea..6b19a8058c 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/after/AfterSaleRejectParam.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/after/AfterSaleRejectParam.java
@@ -5,6 +5,8 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
+import java.util.List;
+
/**
* 售后单拒绝信息
*
@@ -27,6 +29,12 @@ public class AfterSaleRejectParam extends AfterSaleIdParam {
@JsonProperty("reject_reason_type")
private Integer rejectReasonType;
+ /**
+ * 拒绝凭证图片列表,可使用图片上传接口获取media_id
+ */
+ @JsonProperty("reject_certificates")
+ private List rejectCertificates;
+
public AfterSaleRejectParam() {
}
@@ -40,4 +48,12 @@ public AfterSaleRejectParam(String afterSaleOrderId, String rejectReason, Intege
this.rejectReason = rejectReason;
this.rejectReasonType = rejectReasonType;
}
+
+ public AfterSaleRejectParam(String afterSaleOrderId, String rejectReason, Integer rejectReasonType,
+ List rejectCertificates) {
+ super(afterSaleOrderId);
+ this.rejectReason = rejectReason;
+ this.rejectReasonType = rejectReasonType;
+ this.rejectCertificates = rejectCertificates;
+ }
}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/category/CategoryDetailResult.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/category/CategoryDetailResult.java
index 32313b7e34..3188bd3820 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/category/CategoryDetailResult.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/category/CategoryDetailResult.java
@@ -22,6 +22,9 @@ public class CategoryDetailResult extends WxChannelBaseResponse {
@JsonProperty("attr")
private Attr attr;
+ @JsonProperty("product_qua_list")
+ private List productQuaList;
+
@Data
@NoArgsConstructor
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/category/RelationCategoryItem.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/category/RelationCategoryItem.java
new file mode 100644
index 0000000000..8e0bd1b0b5
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/category/RelationCategoryItem.java
@@ -0,0 +1,41 @@
+package me.chanjar.weixin.channel.bean.category;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 店铺类目权限列表项
+ *
+ * @author chucheng
+ */
+@Data
+@NoArgsConstructor
+public class RelationCategoryItem implements Serializable {
+
+ /** 类目id */
+ @JsonProperty("id")
+ private Long id;
+
+ /** 类目状态, 1生效中,2已失效 */
+ @JsonProperty("status")
+ private Integer status;
+
+ /** 失效原因 */
+ @JsonProperty("uneffective_reason")
+ private String uneffectiveReason;
+
+ /** 生效时间 */
+ @JsonProperty("effective_time")
+ private Long effectiveTime;
+
+ /** 失效时间 */
+ @JsonProperty("uneffective_time")
+ private Long uneffectiveTime;
+
+ /** 类目资质id */
+ @JsonProperty("qua_id")
+ private Long quaId;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/category/RelationCategoryRequest.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/category/RelationCategoryRequest.java
new file mode 100644
index 0000000000..c514e7d9ca
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/category/RelationCategoryRequest.java
@@ -0,0 +1,28 @@
+package me.chanjar.weixin.channel.bean.category;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 类目权限列表请求参数
+ *
+ * @author Zeyes
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class RelationCategoryRequest implements Serializable {
+
+ private static final long serialVersionUID = -8765432109876543210L;
+
+ /** 是否按状态筛选 */
+ @JsonProperty("is_filter_status")
+ private Boolean isFilterStatus;
+
+ /** 类目状态(当 isFilterStatus 为 true 时有效) */
+ @JsonProperty("status")
+ private Integer status;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/category/RelationCategoryResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/category/RelationCategoryResponse.java
new file mode 100644
index 0000000000..4bd1ea96d4
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/category/RelationCategoryResponse.java
@@ -0,0 +1,25 @@
+package me.chanjar.weixin.channel.bean.category;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 店铺的类目权限列表响应
+ *
+ * @author chucheng
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class RelationCategoryResponse extends WxChannelBaseResponse {
+
+ private static final long serialVersionUID = -8473920857463918245L;
+
+ @JsonProperty("list")
+ private List list;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/favorite/FavoriteCountResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/favorite/FavoriteCountResponse.java
new file mode 100644
index 0000000000..9acdf93d75
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/favorite/FavoriteCountResponse.java
@@ -0,0 +1,38 @@
+package me.chanjar.weixin.channel.bean.favorite;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 店铺收藏人数 响应
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class FavoriteCountResponse extends WxChannelBaseResponse {
+
+ /** 店铺首页收藏用户数 */
+ @JsonProperty("favor_uv_acc_shop_homepage")
+ private Long favorUvAccShopHomepage;
+
+ /** 订单详情页收藏用户数 */
+ @JsonProperty("favor_uv_acc_order_detail")
+ private Long favorUvAccOrderDetail;
+
+ /** 商品详情页收藏用户数 */
+ @JsonProperty("favor_uv_acc_product_detail")
+ private Long favorUvAccProductDetail;
+
+ /** 其他场景收藏用户数 */
+ @JsonProperty("favor_uv_acc_other_scene")
+ private Long favorUvAccOtherScene;
+
+ /** 所有收藏用户数 */
+ @JsonProperty("favor_uv_acc_all")
+ private Long favorUvAccAll;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/MainProductInfo.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/MainProductInfo.java
index bb13c0b0b7..6e47393c6b 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/MainProductInfo.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/MainProductInfo.java
@@ -31,7 +31,7 @@ public class MainProductInfo implements Serializable {
* 商品id
*/
@JsonProperty("product_id")
- private Integer productId;
+ private String productId;
/**
* 主品sku_id
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/OrderCompensationDeliveryParam.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/OrderCompensationDeliveryParam.java
new file mode 100644
index 0000000000..760762adef
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/OrderCompensationDeliveryParam.java
@@ -0,0 +1,34 @@
+package me.chanjar.weixin.channel.bean.order;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import java.util.List;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.delivery.DeliveryInfo;
+
+/**
+ * 订单补发货 请求参数
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonInclude(Include.NON_NULL)
+public class OrderCompensationDeliveryParam implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /** 订单ID */
+ @JsonProperty("order_id")
+ private String orderId;
+
+ /** 物流信息列表 */
+ @JsonProperty("delivery_list")
+ private List deliveryList;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PreShipmentChangeSkuRejectParam.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PreShipmentChangeSkuRejectParam.java
new file mode 100644
index 0000000000..cd26719e62
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PreShipmentChangeSkuRejectParam.java
@@ -0,0 +1,32 @@
+package me.chanjar.weixin.channel.bean.order;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 拒绝待发货前更换SKU请求 请求参数
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonInclude(Include.NON_NULL)
+public class PreShipmentChangeSkuRejectParam implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /** 订单ID */
+ @JsonProperty("order_id")
+ private String orderId;
+
+ /** 拒绝原因 */
+ @JsonProperty("reject_reason")
+ private String rejectReason;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PreShipmentChangeSkuResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PreShipmentChangeSkuResponse.java
new file mode 100644
index 0000000000..5aff82bd07
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PreShipmentChangeSkuResponse.java
@@ -0,0 +1,25 @@
+package me.chanjar.weixin.channel.bean.order;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 获取待发货前更换SKU待处理请求 响应
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class PreShipmentChangeSkuResponse extends WxChannelBaseResponse {
+
+ private static final long serialVersionUID = 1L;
+
+ /** 更换SKU信息 */
+ @JsonProperty("change_sku_info")
+ private ChangeSkuInfo changeSkuInfo;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PresentNoteAddParam.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PresentNoteAddParam.java
new file mode 100644
index 0000000000..1e0ea0e484
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PresentNoteAddParam.java
@@ -0,0 +1,32 @@
+package me.chanjar.weixin.channel.bean.order;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 礼物订单新增备注信息 请求参数
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonInclude(Include.NON_NULL)
+public class PresentNoteAddParam implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /** 礼物订单ID */
+ @JsonProperty("order_id")
+ private String orderId;
+
+ /** 备注内容 */
+ @JsonProperty("note")
+ private String note;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PresentSubOrderResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PresentSubOrderResponse.java
new file mode 100644
index 0000000000..f550a47a6d
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PresentSubOrderResponse.java
@@ -0,0 +1,26 @@
+package me.chanjar.weixin.channel.bean.order;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 获取礼物单的子单列表 响应
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class PresentSubOrderResponse extends WxChannelBaseResponse {
+
+ private static final long serialVersionUID = 1L;
+
+ /** 子单列表 */
+ @JsonProperty("sub_order_ids")
+ private List subOrderIds;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PrivateNumberAddPhoneParam.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PrivateNumberAddPhoneParam.java
new file mode 100644
index 0000000000..ec0d558e57
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PrivateNumberAddPhoneParam.java
@@ -0,0 +1,28 @@
+package me.chanjar.weixin.channel.bean.order;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 添加待认证手机号 请求参数
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonInclude(Include.NON_NULL)
+public class PrivateNumberAddPhoneParam implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /** 手机号 */
+ @JsonProperty("phone")
+ private String phone;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PrivateNumberGetPhoneResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PrivateNumberGetPhoneResponse.java
new file mode 100644
index 0000000000..4a531a1137
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PrivateNumberGetPhoneResponse.java
@@ -0,0 +1,26 @@
+package me.chanjar.weixin.channel.bean.order;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 获取小店手机号认证状态 响应
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class PrivateNumberGetPhoneResponse extends WxChannelBaseResponse {
+
+ private static final long serialVersionUID = 1L;
+
+ /** 手机号认证信息列表 */
+ @JsonProperty("phone_list")
+ private List phoneList;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PrivateNumberPhoneInfo.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PrivateNumberPhoneInfo.java
new file mode 100644
index 0000000000..e0dda182d5
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PrivateNumberPhoneInfo.java
@@ -0,0 +1,29 @@
+package me.chanjar.weixin.channel.bean.order;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 手机号认证信息
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+public class PrivateNumberPhoneInfo implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /** 手机号 */
+ @JsonProperty("phone")
+ private String phone;
+
+ /**
+ * 认证状态:1-待认证,2-认证成功,3-认证失败
+ */
+ @JsonProperty("auth_status")
+ private Integer authStatus;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PrivateNumberSendVerifyCodeParam.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PrivateNumberSendVerifyCodeParam.java
new file mode 100644
index 0000000000..0698d29a77
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/PrivateNumberSendVerifyCodeParam.java
@@ -0,0 +1,28 @@
+package me.chanjar.weixin.channel.bean.order;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 获取短信验证码 请求参数
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+@JsonInclude(Include.NON_NULL)
+public class PrivateNumberSendVerifyCodeParam implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /** 手机号 */
+ @JsonProperty("phone")
+ private String phone;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/RealNumberViewAuditResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/RealNumberViewAuditResponse.java
new file mode 100644
index 0000000000..4ffa77bce1
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/order/RealNumberViewAuditResponse.java
@@ -0,0 +1,31 @@
+package me.chanjar.weixin.channel.bean.order;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 查看订单真实号审核状态 响应
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class RealNumberViewAuditResponse extends WxChannelBaseResponse {
+
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 审核状态:1-审核中,2-审核通过,3-审核拒绝
+ */
+ @JsonProperty("audit_status")
+ private Integer auditStatus;
+
+ /** 真实号码(审核通过后返回)*/
+ @JsonProperty("real_number")
+ private String realNumber;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftActivityAddParam.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftActivityAddParam.java
new file mode 100644
index 0000000000..89a226efc2
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftActivityAddParam.java
@@ -0,0 +1,23 @@
+package me.chanjar.weixin.channel.bean.product;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 创建买赠活动参数
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class GiftActivityAddParam implements Serializable {
+
+ private static final long serialVersionUID = -3332952823917162308L;
+
+ @JsonProperty("gift_activity")
+ private GiftActivityInfo giftActivity;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftActivityAddResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftActivityAddResponse.java
new file mode 100644
index 0000000000..e8648246b5
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftActivityAddResponse.java
@@ -0,0 +1,23 @@
+package me.chanjar.weixin.channel.bean.product;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 创建买赠活动响应
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class GiftActivityAddResponse extends WxChannelBaseResponse {
+
+ private static final long serialVersionUID = -4527079816331082871L;
+
+ @JsonProperty("activity_id")
+ private String activityId;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftActivityInfo.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftActivityInfo.java
new file mode 100644
index 0000000000..f185fe02bc
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftActivityInfo.java
@@ -0,0 +1,92 @@
+package me.chanjar.weixin.channel.bean.product;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import java.util.List;
+import lombok.Data;
+
+/**
+ * 买赠活动信息
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class GiftActivityInfo implements Serializable {
+
+ private static final long serialVersionUID = 3970308144375119175L;
+
+ @JsonProperty("activity_id")
+ private String activityId;
+
+ @JsonProperty("title")
+ private String title;
+
+ @JsonProperty("start_time")
+ private Long startTime;
+
+ @JsonProperty("end_time")
+ private Long endTime;
+
+ @JsonProperty("detail")
+ private Detail detail;
+
+ @Data
+ public static class Detail implements Serializable {
+ private static final long serialVersionUID = 1019081831733485084L;
+
+ @JsonProperty("show_scene")
+ private Integer showScene;
+
+ @JsonProperty("receive_limit")
+ private ReceiveLimit receiveLimit;
+
+ @JsonProperty("main_products")
+ private List mainProducts;
+
+ @JsonProperty("gift_set")
+ private GiftSet giftSet;
+ }
+
+ @Data
+ public static class ReceiveLimit implements Serializable {
+ private static final long serialVersionUID = 3332293571373311829L;
+
+ @JsonProperty("is_limited")
+ private Boolean limited;
+
+ @JsonProperty("limit_num")
+ private Integer limitNum;
+ }
+
+ @Data
+ public static class MainProduct implements Serializable {
+ private static final long serialVersionUID = 6368866030784193437L;
+
+ @JsonProperty("product_id")
+ private String productId;
+ }
+
+ @Data
+ public static class GiftSet implements Serializable {
+ private static final long serialVersionUID = 8473755235926932739L;
+
+ @JsonProperty("gift_items")
+ private List giftItems;
+
+ @JsonProperty("gift_set_num")
+ private Integer giftSetNum;
+ }
+
+ @Data
+ public static class GiftItem implements Serializable {
+ private static final long serialVersionUID = -4130391476834450014L;
+
+ @JsonProperty("gift_id")
+ private String giftId;
+
+ @JsonProperty("give_num")
+ private Integer giveNum;
+ }
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftProductAddResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftProductAddResponse.java
new file mode 100644
index 0000000000..1548f2293f
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftProductAddResponse.java
@@ -0,0 +1,24 @@
+package me.chanjar.weixin.channel.bean.product;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 添加赠品响应
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class GiftProductAddResponse extends WxChannelBaseResponse {
+
+ private static final long serialVersionUID = -5971026809157610975L;
+
+ /** 赠品商品ID */
+ @JsonProperty("product_id")
+ private String productId;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftProductGetResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftProductGetResponse.java
new file mode 100644
index 0000000000..07bb99004c
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftProductGetResponse.java
@@ -0,0 +1,28 @@
+package me.chanjar.weixin.channel.bean.product;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 赠品详情响应
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class GiftProductGetResponse extends WxChannelBaseResponse {
+
+ private static final long serialVersionUID = 5331169221157446692L;
+
+ /** 赠品线上数据 */
+ @JsonProperty("product")
+ private GiftProductInfo product;
+
+ /** 赠品草稿数据 */
+ @JsonProperty("edit_product")
+ private GiftProductInfo editProduct;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftProductInfo.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftProductInfo.java
new file mode 100644
index 0000000000..732d0b2c2d
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftProductInfo.java
@@ -0,0 +1,11 @@
+package me.chanjar.weixin.channel.bean.product;
+
+/**
+ * 赠品商品信息
+ *
+ * @author GitHub Copilot
+ */
+public class GiftProductInfo extends SpuUpdateInfo {
+
+ private static final long serialVersionUID = -4366133550331058445L;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftProductListParam.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftProductListParam.java
new file mode 100644
index 0000000000..696e6fcc20
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftProductListParam.java
@@ -0,0 +1,31 @@
+package me.chanjar.weixin.channel.bean.product;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import me.chanjar.weixin.channel.bean.base.StreamPageParam;
+
+/**
+ * 赠品列表查询参数
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class GiftProductListParam extends StreamPageParam {
+
+ private static final long serialVersionUID = 7583500622060651067L;
+
+ /** 赠品状态 */
+ @JsonProperty("status")
+ private Integer status;
+
+ public GiftProductListParam() {
+ }
+
+ public GiftProductListParam(Integer pageSize, String nextKey, Integer status) {
+ this.pageSize = pageSize;
+ this.nextKey = nextKey;
+ this.status = status;
+ }
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftProductListResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftProductListResponse.java
new file mode 100644
index 0000000000..21923b9fd3
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/GiftProductListResponse.java
@@ -0,0 +1,33 @@
+package me.chanjar.weixin.channel.bean.product;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 赠品列表响应
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class GiftProductListResponse extends WxChannelBaseResponse {
+
+ private static final long serialVersionUID = -2734111694780970778L;
+
+ /** 总数 */
+ @JsonProperty("total_num")
+ private Integer totalNum;
+
+ /** 本次翻页的上下文,用于请求下一页 */
+ @JsonProperty("next_key")
+ private String nextKey;
+
+ /** 赠品商品 id 列表 */
+ @JsonProperty("product_ids")
+ private List ids;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/SkuFastInfo.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/SkuFastInfo.java
index a461e6d952..b37dfe472c 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/SkuFastInfo.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/SkuFastInfo.java
@@ -35,6 +35,14 @@ public class SkuFastInfo implements Serializable {
@JsonProperty("is_delete")
private Boolean delete;
+ /** 商品sku编码 */
+ @JsonProperty("sku_code")
+ private String skuCode;
+
+ /** 更新sku状态 0-默认值;5-上架;11-下架 */
+ @JsonProperty("status")
+ private Integer status;
+
@Data
@NoArgsConstructor
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/SkuInfo.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/SkuInfo.java
index 22e75d7afc..956b188c22 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/SkuInfo.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/SkuInfo.java
@@ -56,6 +56,10 @@ public class SkuInfo implements Serializable {
@JsonProperty("sku_id")
private String skuId;
+ /** sku条形码 */
+ @JsonProperty("bar_code")
+ private String barCode;
+
public SkuInfo() {
}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/SpuFastInfo.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/SpuFastInfo.java
index 05e107779b..23b1135ba5 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/SpuFastInfo.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/SpuFastInfo.java
@@ -25,4 +25,28 @@ public class SpuFastInfo implements Serializable {
@JsonProperty("skus")
protected List skus;
+ /** 商品编码 */
+ @JsonProperty("spu_code")
+ protected String spuCode;
+
+ /** 限购信息 */
+ @JsonProperty("limit_info")
+ protected LimitInfo limitInfo;
+
+ /** 运费信息 */
+ @JsonProperty("express_info")
+ protected ExpressInfo expressInfo;
+
+ /** 额外服务 */
+ @JsonProperty("extra_service")
+ protected ExtraServiceInfo extraService;
+
+ /** 发货方式:0-快递发货;1-无需快递,手机号发货;3-无需快递,可选发货账号类型,默认为0,若为无需快递,则无需填写运费模版id */
+ @JsonProperty("deliver_method")
+ private Integer deliverMethod;
+
+ /** 商品待开售信息 */
+ @JsonProperty("timing_onsale_info")
+ private TimingOnSaleInfo timingOnSaleInfo;
+
}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/SpuInfo.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/SpuInfo.java
index 155148c178..9b2224db94 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/SpuInfo.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/SpuInfo.java
@@ -151,4 +151,8 @@ public class SpuInfo extends SpuSimpleInfo {
/** 发布模式,0: 普通模式;1: 极简模式 */
@JsonProperty("release_mode")
private Integer releaseMode;
+
+ /** 商品待开售信息 */
+ @JsonProperty("timing_onsale_info")
+ private TimingOnSaleInfo timingOnSaleInfo;
}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/TimingOnSaleInfo.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/TimingOnSaleInfo.java
new file mode 100644
index 0000000000..29270d426c
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/product/TimingOnSaleInfo.java
@@ -0,0 +1,36 @@
+package me.chanjar.weixin.channel.bean.product;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 商品待开售信息
+ *
+ * @author chu
+ */
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class TimingOnSaleInfo implements Serializable {
+
+ /** 状态枚举 0-没有待开售;1-待开售 */
+ @JsonProperty("status")
+ private Integer status;
+
+ /** 开售时间,秒级时间戳,0为未配置时间 */
+ @JsonProperty("onsale_time")
+ private Long onSaleTime;
+
+ /** 是否隐藏价格 0-不隐藏;1-隐藏 */
+ @JsonProperty("is_hide_price")
+ private Integer isHidePrice;
+
+ /** 待开售任务ID,可用于请求立即开售 */
+ @JsonProperty("task_id")
+ private Integer taskId;
+
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/qic/InspectCodeResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/qic/InspectCodeResponse.java
new file mode 100644
index 0000000000..63ee4f0e88
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/qic/InspectCodeResponse.java
@@ -0,0 +1,114 @@
+package me.chanjar.weixin.channel.bean.qic;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class InspectCodeResponse extends WxChannelBaseResponse {
+ private static final long serialVersionUID = -6242555695898612990L;
+
+ private DataPayload data;
+
+ @Data
+ @NoArgsConstructor
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class DataPayload implements Serializable {
+ private static final long serialVersionUID = 684071509005627272L;
+
+ @JsonProperty("backupDeliveryId")
+ private String backupDeliveryId;
+
+ @JsonProperty("backupDeliveryName")
+ private String backupDeliveryName;
+
+ @JsonProperty("boxDTOList")
+ private List boxInfoList;
+
+ @JsonProperty("channelAppId")
+ private String channelAppId;
+
+ @JsonProperty("deliveryId")
+ private String deliveryId;
+
+ @JsonProperty("deliveryName")
+ private String deliveryName;
+
+ @JsonProperty("embedGoodsMaterial")
+ private String embedGoodsMaterial;
+
+ @JsonProperty("goodsDesc")
+ private String goodsDesc;
+
+ @JsonProperty("expressMerge")
+ private Boolean expressMerge;
+
+ @JsonProperty("goodsMainMaterial")
+ private String goodsMainMaterial;
+
+ @JsonProperty("goodsName")
+ private String goodsName;
+
+ @JsonProperty("goodsNum")
+ private Integer goodsNum;
+
+ @JsonProperty("goodsPartsMaterial")
+ private String goodsPartsMaterial;
+
+ @JsonProperty("inspectBaseId")
+ private String inspectBaseId;
+
+ @JsonProperty("inspectBaseName")
+ private String inspectBaseName;
+
+ @JsonProperty("inspectCode")
+ private String inspectCode;
+
+ @JsonProperty("inspectOrgId")
+ private String inspectOrgId;
+
+ @JsonProperty("inspectOrgName")
+ private String inspectOrgName;
+
+ @JsonProperty("inspectOrgShortName")
+ private String inspectOrgShortName;
+
+ @JsonProperty("merchantName")
+ private String merchantName;
+
+ @JsonProperty("orderId")
+ private String orderId;
+
+ @JsonProperty("urgentOrder")
+ private Boolean urgentOrder;
+
+ @JsonProperty("printInfo")
+ private String printInfo;
+
+ @JsonProperty("needLabel")
+ private Boolean needLabel;
+ }
+
+ @Data
+ @NoArgsConstructor
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class BoxInfo implements Serializable {
+ private static final long serialVersionUID = 4074623844069371776L;
+
+ @JsonProperty("boxId")
+ private Long boxId;
+
+ @JsonProperty("boxName")
+ private String boxName;
+
+ @JsonProperty("boxNum")
+ private Integer boxNum;
+ }
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/qic/InspectConfigResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/qic/InspectConfigResponse.java
new file mode 100644
index 0000000000..b2aa17d6ca
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/qic/InspectConfigResponse.java
@@ -0,0 +1,62 @@
+package me.chanjar.weixin.channel.bean.qic;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+import java.io.Serializable;
+
+@Data
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class InspectConfigResponse extends WxChannelBaseResponse {
+ private static final long serialVersionUID = 6463651966377955876L;
+
+ @JsonProperty("inspect_config")
+ private InspectConfig inspectConfig;
+
+ @Data
+ @NoArgsConstructor
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class InspectConfig implements Serializable {
+ private static final long serialVersionUID = 5829846300579243328L;
+
+ @JsonProperty("warehouse_id")
+ private String warehouseId;
+
+ @JsonProperty("delivery_address")
+ private Address deliveryAddress;
+
+ @JsonProperty("return_address")
+ private Address returnAddress;
+
+ @JsonProperty("warehouse_name")
+ private String warehouseName;
+
+ @JsonProperty("warehouse_addr")
+ private String warehouseAddr;
+ }
+
+ @Data
+ @NoArgsConstructor
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class Address implements Serializable {
+ private static final long serialVersionUID = -664266740472865991L;
+
+ @JsonProperty("contact_name")
+ private String contactName;
+
+ @JsonProperty("contact_phone")
+ private String contactPhone;
+
+ private String province;
+
+ private String city;
+
+ private String county;
+
+ private String detail;
+ }
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/qic/RegisterLogisticsRequest.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/qic/RegisterLogisticsRequest.java
new file mode 100644
index 0000000000..37932c2153
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/qic/RegisterLogisticsRequest.java
@@ -0,0 +1,41 @@
+package me.chanjar.weixin.channel.bean.qic;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class RegisterLogisticsRequest implements Serializable {
+ private static final long serialVersionUID = 4346443649534209624L;
+
+ @JsonProperty("order_id_list")
+ private List orderIdList;
+
+ @JsonProperty("logistics_info")
+ private LogisticsInfo logisticsInfo;
+
+ @Data
+ @NoArgsConstructor
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class LogisticsInfo implements Serializable {
+ private static final long serialVersionUID = 8677143207727485993L;
+
+ @JsonProperty("waybill_id")
+ private String waybillId;
+
+ @JsonProperty("delivery_id")
+ private String deliveryId;
+
+ @JsonProperty("delivery_name")
+ private String deliveryName;
+
+ @JsonProperty("delivery_type")
+ private Integer deliveryType;
+ }
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/qic/SubmitConfigResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/qic/SubmitConfigResponse.java
new file mode 100644
index 0000000000..8fdc691d50
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/qic/SubmitConfigResponse.java
@@ -0,0 +1,98 @@
+package me.chanjar.weixin.channel.bean.qic;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class SubmitConfigResponse extends WxChannelBaseResponse {
+ private static final long serialVersionUID = 2456553692263326158L;
+
+ @JsonProperty("submit_config")
+ private SubmitConfig submitConfig;
+
+ @Data
+ @NoArgsConstructor
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class SubmitConfig implements Serializable {
+ private static final long serialVersionUID = 3286213539172123945L;
+
+ @JsonProperty("delivery_list")
+ private List deliveryList;
+
+ @JsonProperty("inspect_org_list")
+ private List inspectOrgList;
+
+ @JsonProperty("charge_url")
+ private String chargeUrl;
+ }
+
+ @Data
+ @NoArgsConstructor
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class Delivery implements Serializable {
+ private static final long serialVersionUID = -9209694824619490683L;
+
+ private String id;
+
+ private String name;
+
+ @JsonProperty("delivery_products")
+ private List deliveryProducts;
+ }
+
+ @Data
+ @NoArgsConstructor
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class DeliveryProduct implements Serializable {
+ private static final long serialVersionUID = 6527277159948670769L;
+
+ private Long id;
+
+ private String name;
+
+ @JsonProperty("enable_insure")
+ private Integer enableInsure;
+
+ @JsonProperty("insure_type_list")
+ private List insureTypeList;
+ }
+
+ @Data
+ @NoArgsConstructor
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class InsureType implements Serializable {
+ private static final long serialVersionUID = -7788541278375899098L;
+
+ private String id;
+
+ private String name;
+
+ @JsonProperty("upper_limit_type")
+ private Integer upperLimitType;
+
+ @JsonProperty("upper_limit_amount")
+ private Long upperLimitAmount;
+ }
+
+ @Data
+ @NoArgsConstructor
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class InspectOrg implements Serializable {
+ private static final long serialVersionUID = 1723422231048685194L;
+
+ private String id;
+
+ private String name;
+
+ @JsonProperty("org_category")
+ private Integer orgCategory;
+ }
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/qic/SubmitInspectRequest.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/qic/SubmitInspectRequest.java
new file mode 100644
index 0000000000..4382298c00
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/qic/SubmitInspectRequest.java
@@ -0,0 +1,85 @@
+package me.chanjar.weixin.channel.bean.qic;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+@Data
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class SubmitInspectRequest implements Serializable {
+ private static final long serialVersionUID = 6396115469552098613L;
+
+ @JsonProperty("order_id")
+ private String orderId;
+
+ @JsonProperty("inspect_info")
+ private InspectInfo inspectInfo;
+
+ @Data
+ @NoArgsConstructor
+ @JsonInclude(JsonInclude.Include.NON_NULL)
+ public static class InspectInfo implements Serializable {
+ private static final long serialVersionUID = -3502982646296821525L;
+
+ @JsonProperty("delivery_id")
+ private String deliveryId;
+
+ @JsonProperty("backup_delivery_id")
+ private String backupDeliveryId;
+
+ @JsonProperty("express_insure")
+ private Boolean expressInsure;
+
+ @JsonProperty("express_insure_amount")
+ private Long expressInsureAmount;
+
+ @JsonProperty("express_merge")
+ private Boolean expressMerge;
+
+ @JsonProperty("inspect_org_id")
+ private String inspectOrgId;
+
+ @JsonProperty("refund_intercept")
+ private Integer refundIntercept;
+
+ @JsonProperty("inspect_org_name")
+ private String inspectOrgName;
+
+ @JsonProperty("warehouse_name")
+ private String warehouseName;
+
+ @JsonProperty("warehouse_addr")
+ private String warehouseAddr;
+
+ @JsonProperty("delivery_product_id")
+ private Long deliveryProductId;
+
+ @JsonProperty("delivery_insure_id")
+ private String deliveryInsureId;
+
+ @JsonProperty("backup_delivery_product_id")
+ private Long backupDeliveryProductId;
+
+ @JsonProperty("backup_delivery_insure_id")
+ private String backupDeliveryInsureId;
+
+ @JsonProperty("backup_express_insure")
+ private Boolean backupExpressInsure;
+
+ @JsonProperty("backup_express_insure_amount")
+ private Long backupExpressInsureAmount;
+
+ @JsonProperty("remark")
+ private String remark;
+
+ @JsonProperty("agarwood_inspect_org_id")
+ private String agarwoodInspectOrgId;
+
+ @JsonProperty("agarwood_inspect_org_name")
+ private String agarwoodInspectOrgName;
+ }
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/shop/ShopH5UrlResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/shop/ShopH5UrlResponse.java
new file mode 100644
index 0000000000..5700073c05
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/shop/ShopH5UrlResponse.java
@@ -0,0 +1,22 @@
+package me.chanjar.weixin.channel.bean.shop;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 店铺H5链接 响应
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class ShopH5UrlResponse extends WxChannelBaseResponse {
+
+ /** 店铺H5链接 */
+ @JsonProperty("shop_h5url")
+ private String shopH5url;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/shop/ShopQrCodeResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/shop/ShopQrCodeResponse.java
new file mode 100644
index 0000000000..1859f2e3f4
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/shop/ShopQrCodeResponse.java
@@ -0,0 +1,22 @@
+package me.chanjar.weixin.channel.bean.shop;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 店铺二维码 响应
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class ShopQrCodeResponse extends WxChannelBaseResponse {
+
+ /** 店铺二维码链接 */
+ @JsonProperty("shop_qrcode")
+ private String shopQrcode;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/shop/ShopTagLinkResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/shop/ShopTagLinkResponse.java
new file mode 100644
index 0000000000..8b82d21106
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/shop/ShopTagLinkResponse.java
@@ -0,0 +1,22 @@
+package me.chanjar.weixin.channel.bean.shop;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 店铺口令 响应
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class ShopTagLinkResponse extends WxChannelBaseResponse {
+
+ /** 店铺微信口令 */
+ @JsonProperty("shop_taglink")
+ private String shopTaglink;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DistributeTypeResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DistributeTypeResponse.java
new file mode 100644
index 0000000000..3537892b12
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DistributeTypeResponse.java
@@ -0,0 +1,25 @@
+package me.chanjar.weixin.channel.bean.supplier;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 分配方式响应。
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class DistributeTypeResponse extends WxChannelBaseResponse {
+ private static final long serialVersionUID = -750860556286328053L;
+
+ @JsonProperty("distribute_type")
+ private Integer distributeType;
+
+ @JsonProperty("supplier_info")
+ private SupplierInfo supplierInfo;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipAssignRequest.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipAssignRequest.java
new file mode 100644
index 0000000000..3fa3904a57
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipAssignRequest.java
@@ -0,0 +1,25 @@
+package me.chanjar.weixin.channel.bean.supplier;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 代发单分配请求。
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class DropshipAssignRequest implements Serializable {
+ private static final long serialVersionUID = 6945436332042017565L;
+
+ @JsonProperty("order_id")
+ private String orderId;
+
+ @JsonProperty("supplier_id")
+ private String supplierId;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipDetailResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipDetailResponse.java
new file mode 100644
index 0000000000..a2cd0f983e
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipDetailResponse.java
@@ -0,0 +1,22 @@
+package me.chanjar.weixin.channel.bean.supplier;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 代发单详情响应。
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class DropshipDetailResponse extends WxChannelBaseResponse {
+ private static final long serialVersionUID = 5548774863400272707L;
+
+ @JsonProperty("dropship_info")
+ private DropshipInfo dropshipInfo;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipInfo.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipInfo.java
new file mode 100644
index 0000000000..b2a0594073
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipInfo.java
@@ -0,0 +1,37 @@
+package me.chanjar.weixin.channel.bean.supplier;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 代发单信息。
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class DropshipInfo implements Serializable {
+ private static final long serialVersionUID = -7880364210849039278L;
+
+ @JsonProperty("order_id")
+ private String orderId;
+
+ @JsonProperty("supplier_id")
+ private String supplierId;
+
+ @JsonProperty("ds_order_id")
+ private String dropshipId;
+
+ @JsonProperty("status")
+ private Integer status;
+
+ @JsonProperty("create_time")
+ private Long createTime;
+
+ @JsonProperty("update_time")
+ private Long updateTime;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipListRequest.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipListRequest.java
new file mode 100644
index 0000000000..ad89ad7cd5
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipListRequest.java
@@ -0,0 +1,37 @@
+package me.chanjar.weixin.channel.bean.supplier;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 代发单列表请求。
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class DropshipListRequest implements Serializable {
+ private static final long serialVersionUID = 2638071229335192596L;
+
+ @JsonProperty("supplier_id")
+ private String supplierId;
+
+ @JsonProperty("status")
+ private Integer status;
+
+ @JsonProperty("create_time_start")
+ private Long createTimeStart;
+
+ @JsonProperty("create_time_end")
+ private Long createTimeEnd;
+
+ @JsonProperty("page_size")
+ private Integer pageSize;
+
+ @JsonProperty("next_key")
+ private String nextKey;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipListResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipListResponse.java
new file mode 100644
index 0000000000..2665f8ef15
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipListResponse.java
@@ -0,0 +1,29 @@
+package me.chanjar.weixin.channel.bean.supplier;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 代发单列表响应。
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class DropshipListResponse extends WxChannelBaseResponse {
+ private static final long serialVersionUID = -2850183412032417307L;
+
+ @JsonProperty("dropship_list")
+ private List dropshipList;
+
+ @JsonProperty("next_key")
+ private String nextKey;
+
+ @JsonProperty("has_more")
+ private Boolean hasMore;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipResponse.java
new file mode 100644
index 0000000000..65a529e865
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipResponse.java
@@ -0,0 +1,28 @@
+package me.chanjar.weixin.channel.bean.supplier;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 代发单分配响应。
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class DropshipResponse extends WxChannelBaseResponse {
+ private static final long serialVersionUID = 4376618566823584629L;
+
+ @JsonProperty("order_id")
+ private String orderId;
+
+ @JsonProperty("supplier_id")
+ private String supplierId;
+
+ @JsonProperty("dropship_id")
+ private String dropshipId;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipSearchRequest.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipSearchRequest.java
new file mode 100644
index 0000000000..c658a3c16d
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/DropshipSearchRequest.java
@@ -0,0 +1,26 @@
+package me.chanjar.weixin.channel.bean.supplier;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+
+/**
+ * 代发单搜索请求。
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class DropshipSearchRequest extends DropshipListRequest {
+ private static final long serialVersionUID = 3915264648809784742L;
+
+ @JsonProperty("order_id")
+ private String orderId;
+
+ @JsonProperty("dropship_id")
+ private String dropshipId;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/ProductDistributeRequest.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/ProductDistributeRequest.java
new file mode 100644
index 0000000000..c1018d2255
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/ProductDistributeRequest.java
@@ -0,0 +1,26 @@
+package me.chanjar.weixin.channel.bean.supplier;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import java.util.List;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 按商品自动分配请求。
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class ProductDistributeRequest implements Serializable {
+ private static final long serialVersionUID = 4201609097231290078L;
+
+ @JsonProperty("supplier_id")
+ private String supplierId;
+
+ @JsonProperty("product_id_list")
+ private List productIdList;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/ProductListResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/ProductListResponse.java
new file mode 100644
index 0000000000..f585ab0969
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/ProductListResponse.java
@@ -0,0 +1,42 @@
+package me.chanjar.weixin.channel.bean.supplier;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import java.util.List;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 按商品自动分配商品列表响应。
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class ProductListResponse extends WxChannelBaseResponse {
+ private static final long serialVersionUID = -7096250227033388295L;
+
+ @JsonProperty("product_list")
+ private List productList;
+
+ @JsonProperty("next_key")
+ private String nextKey;
+
+ @JsonProperty("has_more")
+ private Boolean hasMore;
+
+ @Data
+ @NoArgsConstructor
+ public static class ProductInfo implements Serializable {
+ private static final long serialVersionUID = -4482299212575966325L;
+
+ @JsonProperty("product_id")
+ private String productId;
+
+ @JsonProperty("supplier_id")
+ private String supplierId;
+ }
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/SupplierInfo.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/SupplierInfo.java
new file mode 100644
index 0000000000..0944107c19
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/SupplierInfo.java
@@ -0,0 +1,28 @@
+package me.chanjar.weixin.channel.bean.supplier;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 供货商信息。
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class SupplierInfo implements Serializable {
+ private static final long serialVersionUID = -6480813119738259476L;
+
+ @JsonProperty("supplier_id")
+ private String supplierId;
+
+ @JsonProperty("supplier_name")
+ private String supplierName;
+
+ @JsonProperty("status")
+ private Integer status;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/SupplierInfoResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/SupplierInfoResponse.java
new file mode 100644
index 0000000000..7f138a5929
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/SupplierInfoResponse.java
@@ -0,0 +1,22 @@
+package me.chanjar.weixin.channel.bean.supplier;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 供货商信息响应。
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class SupplierInfoResponse extends WxChannelBaseResponse {
+ private static final long serialVersionUID = -3071464065836573893L;
+
+ @JsonProperty("supplier_info")
+ private SupplierInfo supplierInfo;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/SupplierListResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/SupplierListResponse.java
new file mode 100644
index 0000000000..6bab0615a7
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/supplier/SupplierListResponse.java
@@ -0,0 +1,29 @@
+package me.chanjar.weixin.channel.bean.supplier;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.List;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 供货商列表响应。
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class SupplierListResponse extends WxChannelBaseResponse {
+ private static final long serialVersionUID = -692609589633695295L;
+
+ @JsonProperty("supplier_list")
+ private List supplierList;
+
+ @JsonProperty("next_key")
+ private String nextKey;
+
+ @JsonProperty("has_more")
+ private Boolean hasMore;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentOrderDetailParam.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentOrderDetailParam.java
new file mode 100644
index 0000000000..8f37fa0eb2
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentOrderDetailParam.java
@@ -0,0 +1,32 @@
+package me.chanjar.weixin.channel.bean.talent;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 带货助手-获取佣金单详情 请求参数
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class TalentOrderDetailParam implements Serializable {
+
+ private static final long serialVersionUID = 8741285036412736219L;
+
+ /** 订单号,可从获取佣金单列表接口获得 */
+ @JsonProperty("order_id")
+ private String orderId;
+
+ /** 商品skuid,可从获取佣金单列表接口获得 */
+ @JsonProperty("sku_id")
+ private String skuId;
+
+ /** 订单额外参数【在订单列表里面返回的参数回传】 */
+ @JsonProperty("special_id")
+ private String specialId;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentOrderDetailResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentOrderDetailResponse.java
new file mode 100644
index 0000000000..1e0f2d2c28
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentOrderDetailResponse.java
@@ -0,0 +1,203 @@
+package me.chanjar.weixin.channel.bean.talent;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 带货助手-获取佣金单详情 响应
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class TalentOrderDetailResponse extends WxChannelBaseResponse {
+
+ private static final long serialVersionUID = 2174806923145876312L;
+
+ /** 订单基础信息 */
+ @JsonProperty("base_info")
+ private BaseInfo baseInfo;
+
+ /** 订单佣金信息 */
+ @JsonProperty("commission_info")
+ private CommissionInfo commissionInfo;
+
+ /** 下单通道信息 */
+ @JsonProperty("channel_info")
+ private ChannelInfo channelInfo;
+
+ /** 内容推广推客机构信息 */
+ @JsonProperty("promotion_head_supplier_info")
+ private PromotionHeadSupplierInfo promotionHeadSupplierInfo;
+
+ /** 商品信息 */
+ @JsonProperty("product_info")
+ private ProductInfo productInfo;
+
+ /** 订单基础信息 */
+ @Data
+ @NoArgsConstructor
+ public static class BaseInfo implements Serializable {
+
+ private static final long serialVersionUID = 6382947162830498251L;
+
+ /** 订单id */
+ @JsonProperty("order_id")
+ private String orderId;
+
+ /** 商品spuid */
+ @JsonProperty("spu_id")
+ private String spuId;
+
+ /** 商品skuid */
+ @JsonProperty("sku_id")
+ private String skuId;
+
+ /** 特殊id【针对本地生活】 */
+ @JsonProperty("special_id")
+ private String specialId;
+
+ /**
+ * 订单状态:1=待支付, 2=待发货, 3=已发货, 4=已收货, 5=售后中, 6=已完成, 7=已取消, 8=已退款, 9=部分退款, 10=待使用
+ */
+ @JsonProperty("order_status")
+ private Integer orderStatus;
+
+ /** 实际支付金额【单位:分】 */
+ @JsonProperty("actual_payment")
+ private String actualPayment;
+
+ /** 订单创建时间 */
+ @JsonProperty("order_create_time")
+ private Long orderCreateTime;
+
+ /** 订单更新时间 */
+ @JsonProperty("order_update_time")
+ private Long orderUpdateTime;
+
+ /** 下单用户 */
+ @JsonProperty("buyer_info")
+ private BuyerInfo buyerInfo;
+
+ /** 订单支付时间 */
+ @JsonProperty("order_pay_time")
+ private Long orderPayTime;
+
+ /** 订单的分佣基数【单位:分】 */
+ @JsonProperty("settle_payment")
+ private String settlePayment;
+ }
+
+ /** 下单用户信息 */
+ @Data
+ @NoArgsConstructor
+ public static class BuyerInfo implements Serializable {
+
+ private static final long serialVersionUID = 4729638451027364819L;
+
+ /** 下单用户的openid */
+ @JsonProperty("open_id")
+ private String openId;
+
+ /** 下单用户的unionid */
+ @JsonProperty("union_id")
+ private String unionId;
+ }
+
+ /** 订单佣金信息 */
+ @Data
+ @NoArgsConstructor
+ public static class CommissionInfo implements Serializable {
+
+ private static final long serialVersionUID = -3819264037182640581L;
+
+ /** 佣金单状态:1=待结算, 2=已结算, 3=取消结算, 4=结算异常 */
+ @JsonProperty("state")
+ private Integer state;
+
+ /** 佣金比例 */
+ @JsonProperty("ratio")
+ private String ratio;
+
+ /** 预期结算时间 */
+ @JsonProperty("expect_settle_time")
+ private Long expectSettleTime;
+
+ /** 预期结算金额 */
+ @JsonProperty("expect_settlement")
+ private String expectSettlement;
+
+ /** 实际结算时间 */
+ @JsonProperty("actual_settle_time")
+ private Long actualSettleTime;
+
+ /** 实际结算金额 */
+ @JsonProperty("actual_settlement")
+ private String actualSettlement;
+ }
+
+ /** 下单通道信息 */
+ @Data
+ @NoArgsConstructor
+ public static class ChannelInfo implements Serializable {
+
+ private static final long serialVersionUID = 7364918204736159023L;
+
+ /** 渠道类型:1=视频号, 2=公众号 */
+ @JsonProperty("channel_type")
+ private Integer channelType;
+
+ /** 渠道id */
+ @JsonProperty("channel_id")
+ private String channelId;
+
+ /** 渠道名称 */
+ @JsonProperty("channel_name")
+ private String channelName;
+ }
+
+ /** 内容推广推客机构信息 */
+ @Data
+ @NoArgsConstructor
+ public static class PromotionHeadSupplierInfo implements Serializable {
+
+ private static final long serialVersionUID = 1826374950183647291L;
+
+ /** 机构id */
+ @JsonProperty("id")
+ private String id;
+
+ /** 机构名称 */
+ @JsonProperty("name")
+ private String name;
+
+ /** 佣金单比例 */
+ @JsonProperty("ratio")
+ private String ratio;
+
+ /** 佣金 */
+ @JsonProperty("fee")
+ private String fee;
+ }
+
+ /** 商品信息 */
+ @Data
+ @NoArgsConstructor
+ public static class ProductInfo implements Serializable {
+
+ private static final long serialVersionUID = -4920183645872916370L;
+
+ /** 商品的标题 */
+ @JsonProperty("title")
+ private String title;
+
+ /** 商品的头图 */
+ @JsonProperty("thumb_img")
+ private String thumbImg;
+ }
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentOrderListParam.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentOrderListParam.java
new file mode 100644
index 0000000000..a780d92be5
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentOrderListParam.java
@@ -0,0 +1,52 @@
+package me.chanjar.weixin.channel.bean.talent;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 带货助手-获取佣金单列表 请求参数
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class TalentOrderListParam implements Serializable {
+
+ private static final long serialVersionUID = -6218342185316399261L;
+
+ /** 佣金单创建时间范围之开始时间【和更新时间范围二选一】 */
+ @JsonProperty("create_time_gt")
+ private Long createTimeGt;
+
+ /** 佣金单创建时间范围之结束时间【和更新时间范围二选一】 */
+ @JsonProperty("create_time_lt")
+ private Long createTimeLt;
+
+ /** 订单 ID 过滤 */
+ @JsonProperty("order_id")
+ private String orderId;
+
+ /** 商品 id 过滤 */
+ @JsonProperty("spu_id")
+ private String spuId;
+
+ /** 佣金单更新时间范围之开始时间【和创建时间范围二选一】 */
+ @JsonProperty("update_time_gt")
+ private Long updateTimeGt;
+
+ /** 佣金单更新时间范围之结束时间【和创建时间范围二选一】 */
+ @JsonProperty("update_time_lt")
+ private Long updateTimeLt;
+
+ /** 单页佣金单数(不超过10) */
+ @JsonProperty("page_size")
+ private Integer pageSize;
+
+ /** 由上次请求返回,顺序翻页时需要传入, 会从上次返回的结果往后翻一页 */
+ @JsonProperty("next_key")
+ private String nextKey;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentOrderListResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentOrderListResponse.java
new file mode 100644
index 0000000000..8393ba656a
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentOrderListResponse.java
@@ -0,0 +1,54 @@
+package me.chanjar.weixin.channel.bean.talent;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import java.util.List;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 带货助手-获取佣金单列表 响应
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class TalentOrderListResponse extends WxChannelBaseResponse {
+
+ private static final long serialVersionUID = 3541802319654186172L;
+
+ /** 佣金单信息列表 */
+ @JsonProperty("order_list")
+ private List orderList;
+
+ /** 是否还有剩余订单 */
+ @JsonProperty("has_more")
+ private Boolean hasMore;
+
+ /** 本次翻页的上下文,用于顺序翻页请求 */
+ @JsonProperty("next_key")
+ private String nextKey;
+
+ /** 佣金单基础信息 */
+ @Data
+ @NoArgsConstructor
+ public static class OrderInfo implements Serializable {
+
+ private static final long serialVersionUID = 5261736494628827543L;
+
+ /** 订单id */
+ @JsonProperty("order_id")
+ private String orderId;
+
+ /** skuid */
+ @JsonProperty("sku_id")
+ private String skuId;
+
+ /** 佣金单特殊标识【目前主要用于本地生活】 */
+ @JsonProperty("special_id")
+ private String specialId;
+ }
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentWindowProductDetailParam.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentWindowProductDetailParam.java
new file mode 100644
index 0000000000..5f7bca423e
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentWindowProductDetailParam.java
@@ -0,0 +1,24 @@
+package me.chanjar.weixin.channel.bean.talent;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 带货助手-获取达人橱窗商品详情 请求参数
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class TalentWindowProductDetailParam implements Serializable {
+
+ private static final long serialVersionUID = 3849271605183749261L;
+
+ /** 橱窗商品ID(可以从"获取达人橱窗商品列表"接口获取) */
+ @JsonProperty("product_id")
+ private String productId;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentWindowProductDetailResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentWindowProductDetailResponse.java
new file mode 100644
index 0000000000..c41f2f1de5
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentWindowProductDetailResponse.java
@@ -0,0 +1,81 @@
+package me.chanjar.weixin.channel.bean.talent;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 带货助手-获取达人橱窗商品详情 响应
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class TalentWindowProductDetailResponse extends WxChannelBaseResponse {
+
+ private static final long serialVersionUID = 1634829710537264918L;
+
+ /** 橱窗商品详情 */
+ @JsonProperty("product")
+ private ProductDetail product;
+
+ /** 橱窗商品详情 */
+ @Data
+ @NoArgsConstructor
+ public static class ProductDetail implements Serializable {
+
+ private static final long serialVersionUID = 7283640192847516039L;
+
+ /** 橱窗商品ID */
+ @JsonProperty("product_id")
+ private String productId;
+
+ /** 商品来源店铺的appid */
+ @JsonProperty("appid")
+ private String appid;
+
+ /** 商品在货源店铺的商品ID,对于带货商品会返回 */
+ @JsonProperty("out_product_id")
+ private String outProductId;
+
+ /** 商品标题 */
+ @JsonProperty("title")
+ private String title;
+
+ /** 商品头图url */
+ @JsonProperty("img_url")
+ private String imgUrl;
+
+ /** 商品所属叶子类目(品类)ID */
+ @JsonProperty("leaf_category_id")
+ private Long leafCategoryId;
+
+ /** 商品状态:1=生效中,2=被禁止售卖 */
+ @JsonProperty("status")
+ private Integer status;
+
+ /** 价格区间最小值(单位分,销售价) */
+ @JsonProperty("selling_price")
+ private Long sellingPrice;
+
+ /** 剩余库存 */
+ @JsonProperty("stock")
+ private Long stock;
+
+ /** 销量 */
+ @JsonProperty("sales")
+ private Long sales;
+
+ /** 是否在橱窗设置对外隐藏 */
+ @JsonProperty("is_hide")
+ private Boolean isHide;
+
+ /** 用于在小程序跳转小店场景添加商品时传递跟佣信息 */
+ @JsonProperty("product_promotion_link")
+ private String productPromotionLink;
+ }
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentWindowProductListParam.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentWindowProductListParam.java
new file mode 100644
index 0000000000..bb6e77bb7f
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentWindowProductListParam.java
@@ -0,0 +1,32 @@
+package me.chanjar.weixin.channel.bean.talent;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 带货助手-获取达人橱窗商品列表 请求参数
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class TalentWindowProductListParam implements Serializable {
+
+ private static final long serialVersionUID = 7419836250174638291L;
+
+ /** 单页商品数(不超过500) */
+ @JsonProperty("page_size")
+ private Integer pageSize;
+
+ /** 页面下标,下标从1开始(不可以与 lastBuffer 一起填写) */
+ @JsonProperty("page_index")
+ private Integer pageIndex;
+
+ /** 由上次请求返回,顺序翻页时需要传入(不可以与 pageIndex 一起填写) */
+ @JsonProperty("last_buffer")
+ private String lastBuffer;
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentWindowProductListResponse.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentWindowProductListResponse.java
new file mode 100644
index 0000000000..5c90977a05
--- /dev/null
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/bean/talent/TalentWindowProductListResponse.java
@@ -0,0 +1,58 @@
+package me.chanjar.weixin.channel.bean.talent;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import java.io.Serializable;
+import java.util.List;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+
+/**
+ * 带货助手-获取达人橱窗商品列表 响应
+ *
+ * @author GitHub Copilot
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class TalentWindowProductListResponse extends WxChannelBaseResponse {
+
+ private static final long serialVersionUID = 8263047195826340712L;
+
+ /** 橱窗商品列表 */
+ @JsonProperty("products")
+ private List products;
+
+ /** 本次翻页的上下文,用于顺序翻页请求 */
+ @JsonProperty("last_buffer")
+ private String lastBuffer;
+
+ /** 橱窗商品基础信息 */
+ @Data
+ @NoArgsConstructor
+ public static class ProductInfo implements Serializable {
+
+ private static final long serialVersionUID = 6142837490516284039L;
+
+ /** 橱窗商品id */
+ @JsonProperty("product_id")
+ private String productId;
+
+ /** 对于自营商品会返回,代表商品来源店铺的appid */
+ @JsonProperty("appid")
+ private String appid;
+
+ /**
+ * 商品来源:1=来源店铺的自营商品,2=来源选品中心的带货商品
+ */
+ @JsonProperty("product_source")
+ private Integer productSource;
+
+ /**
+ * 对于带货商品会返回,代表商品在货源小店中的商品id
+ */
+ @JsonProperty("out_product_id")
+ private String outProductId;
+ }
+}
diff --git a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/constant/WxChannelApiUrlConstants.java b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/constant/WxChannelApiUrlConstants.java
index 2d9aa84f84..0b62bf59de 100644
--- a/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/constant/WxChannelApiUrlConstants.java
+++ b/weixin-java-channel/src/main/java/me/chanjar/weixin/channel/constant/WxChannelApiUrlConstants.java
@@ -27,22 +27,35 @@ public interface Basics {
/** 获取店铺基本信息 */
String GET_SHOP_INFO = "https://api.weixin.qq.com/channels/ec/basics/info/get";
/** 上传图片 */
- String IMG_UPLOAD_URL = "https://api.weixin.qq.com/channels/ec/basics/img/upload";
+ String IMG_UPLOAD_URL = "https://api.weixin.qq.com/shop/ec/basics/img/upload";
/** 上传资质图片 */
- String UPLOAD_QUALIFICATION_FILE = "https://api.weixin.qq.com/channels/ec/basics/qualification/upload";
+ String UPLOAD_QUALIFICATION_FILE = "https://api.weixin.qq.com/shop/ec/basics/qualification/upload";
/** 下载图片 */
String GET_IMG_URL = "https://api.weixin.qq.com/channels/ec/basics/media/get";
/** 获取地址编码 */
String GET_ADDRESS_CODE = "https://api.weixin.qq.com/channels/ec/basics/addresscode/get";
+ /** 获取店铺H5链接 */
+ String GET_SHOP_H5URL = "https://api.weixin.qq.com/channels/ec/basics/shop/h5url/get";
+ /** 获取店铺二维码 */
+ String GET_SHOP_QRCODE = "https://api.weixin.qq.com/channels/ec/basics/shop/qrcode/get";
+ /** 获取店铺口令 */
+ String GET_SHOP_TAGLINK = "https://api.weixin.qq.com/channels/ec/basics/shop/taglink/get";
+ }
+
+ /** 收藏管理相关接口 */
+ public interface Favorite {
+
+ /** 获取店铺收藏的人数 */
+ String GET_FAVORITE_COUNT = "https://api.weixin.qq.com/channels/ec/favorites/count/get";
}
/** 商品类目相关接口 */
public interface Category {
/** 获取所有的类目 */
- String LIST_ALL_CATEGORY_URL = "https://api.weixin.qq.com/channels/ec/category/all";
+ String LIST_ALL_CATEGORY_URL = "https://api.weixin.qq.com/shop/ec/category/all";
/** 获取类目详情 */
- String GET_CATEGORY_DETAIL_URL = "https://api.weixin.qq.com/channels/ec/category/detail";
+ String GET_CATEGORY_DETAIL_URL = "https://api.weixin.qq.com/shop/ec/category/detail";
/** 获取可用的子类目详情 */
String AVAILABLE_CATEGORY_URL = "https://api.weixin.qq.com/channels/ec/category/availablesoncategories/get";
/** 上传类目资质 */
@@ -50,9 +63,11 @@ public interface Category {
/** 获取类目审核结果 */
String GET_CATEGORY_AUDIT_URL = "https://api.weixin.qq.com/channels/ec/category/audit/get";
/** 取消类目提审 */
- String CANCEL_CATEGORY_AUDIT_URL = "https://api.weixin.qq.com/channels/ec/category/audit/cancel";
+ String CANCEL_CATEGORY_AUDIT_URL = "https://api.weixin.qq.com/shop/ec/category/audit/cancel";
/** 获取账号申请通过的类目和资质信息 */
String LIST_PASS_CATEGORY_URL = "https://api.weixin.qq.com/channels/ec/category/list/get";
+ /** 获取店铺的类目权限列表 */
+ String LIST_RELATION_CATEGORY_URL = "https://api.weixin.qq.com/shop/ec/category/get_category_relation_list";
}
/** 主页管理相关接口 */
@@ -101,13 +116,13 @@ public interface HomePage {
public interface Brand {
/** 获取品牌库列表 */
- String ALL_BRAND_URL = "https://api.weixin.qq.com/channels/ec/brand/all";
+ String ALL_BRAND_URL = "https://api.weixin.qq.com/shop/ec/brand/all";
/** 新增品牌资质 */
- String ADD_BRAND_URL = "https://api.weixin.qq.com/channels/ec/brand/add";
+ String ADD_BRAND_URL = "https://api.weixin.qq.com/shop/ec/brand/add";
/** 更新品牌资质 */
String UPDATE_BRAND_URL = "https://api.weixin.qq.com/channels/ec/brand/update";
/** 撤回品牌资质审核 */
- String CANCEL_BRAND_AUDIT_URL = "https://api.weixin.qq.com/channels/ec/brand/audit/cancel";
+ String CANCEL_BRAND_AUDIT_URL = "https://api.weixin.qq.com/shop/ec/brand/audit/cancel";
/** 删除品牌资质 */
String DELETE_BRAND_URL = "https://api.weixin.qq.com/channels/ec/brand/delete";
/** 获取品牌资质申请详情 */
@@ -151,6 +166,24 @@ public interface Spu {
String SPU_GET_STOCK_BATCH_URL = "https://api.weixin.qq.com/channels/ec/product/stock/batchget";
/** 更新商品库存 */
String SPU_UPDATE_STOCK_URL = "https://api.weixin.qq.com/channels/ec/product/stock/update";
+ /** 添加非卖商品 */
+ String GIFT_PRODUCT_ADD_URL = "https://api.weixin.qq.com/channels/ec/product/gift/add";
+ /** 更新非卖商品 */
+ String GIFT_PRODUCT_UPDATE_URL = "https://api.weixin.qq.com/channels/ec/product/gift/update";
+ /** 在售商品转赠品 */
+ String GIFT_PRODUCT_ON_SALE_SET_URL = "https://api.weixin.qq.com/channels/ec/product/gift/onsale/set";
+ /** 获取赠品 */
+ String GIFT_PRODUCT_GET_URL = "https://api.weixin.qq.com/channels/ec/product/gift/get";
+ /** 获取赠品列表 */
+ String GIFT_PRODUCT_LIST_URL = "https://api.weixin.qq.com/channels/ec/product/gift/list/get";
+ /** 更新赠品库存 */
+ String GIFT_PRODUCT_STOCK_UPDATE_URL = "https://api.weixin.qq.com/channels/ec/product/gift/stock/update";
+ /** 创建赠品活动 */
+ String GIFT_ACTIVITY_ADD_URL = "https://api.weixin.qq.com/channels/ec/product/activity/add";
+ /** 删除赠品活动 */
+ String GIFT_ACTIVITY_DELETE_URL = "https://api.weixin.qq.com/channels/ec/product/activity/del";
+ /** 停止赠品活动 */
+ String GIFT_ACTIVITY_STOP_URL = "https://api.weixin.qq.com/channels/ec/product/activity/stop";
/** 添加限时抢购任务 */
String ADD_LIMIT_TASK_URL = "https://api.weixin.qq.com/channels/ec/product/limiteddiscounttask/add";
/** 拉取限时抢购任务列表 */
@@ -213,6 +246,37 @@ public interface Order {
String VIRTUAL_TEL_NUMBER_URL = "https://api.weixin.qq.com/channels/ec/order/virtualtelnumber/get";
/** 解码订单包含的敏感数据 */
String DECODE_SENSITIVE_INFO_URL = "https://api.weixin.qq.com/channels/ec/order/sensitiveinfo/decode";
+ /** 礼物订单新增备注信息 */
+ String PRESENT_NOTE_ADD_URL = "https://api.weixin.qq.com/channels/ec/order/presentnote/add";
+ /** 获取礼物单的子单列表 */
+ String PRESENT_SUB_ORDER_GET_URL = "https://api.weixin.qq.com/channels/ec/order/presentsuborder/get";
+ /** 获取待发货前更换sku待处理请求 */
+ String PRE_SHIPMENT_CHANGE_SKU_GET_URL = "https://api.weixin.qq.com/channels/ec/order/preshipmentchangesku/get";
+ /** 同意待发货前更换sku请求 */
+ String PRE_SHIPMENT_CHANGE_SKU_APPROVE_URL = "https://api.weixin.qq.com/channels/ec/order/preshipmentchangesku/approve";
+ /** 拒绝待发货前更换sku请求 */
+ String PRE_SHIPMENT_CHANGE_SKU_REJECT_URL = "https://api.weixin.qq.com/channels/ec/order/preshipmentchangesku/reject";
+ /** 申请查看订单真实号码 */
+ String REAL_NUMBER_APPLY_URL = "https://api.weixin.qq.com/channels/ec/order/realnumber/apply";
+ /** 查看订单真实号审核状态 */
+ String REAL_NUMBER_VIEW_AUDIT_GET_URL = "https://api.weixin.qq.com/channels/ec/order/realnumberviewaudit/get";
+ /** 订单再次申请虚拟号 */
+ String VIRTUAL_NUMBER_APPLY_AGAIN_URL = "https://api.weixin.qq.com/channels/ec/order/virtualnumber/applyagain";
+ /** 订单虚拟号延期 */
+ String VIRTUAL_NUMBER_DELAY_URL = "https://api.weixin.qq.com/channels/ec/order/virtualnumber/delay";
+ /** 订单补发货 */
+ String DELIVERY_COMPENSATION_URL = "https://api.weixin.qq.com/channels/ec/order/delivery/compensation";
+ }
+
+ /** 虚拟号管理相关接口 */
+ public interface PrivateNumber {
+
+ /** 添加待认证的手机号 */
+ String ADD_PHONE_URL = "https://api.weixin.qq.com/channels/ec/merchant/privatenumber/addphone";
+ /** 获取短信验证码 */
+ String SEND_VERIFY_CODE_URL = "https://api.weixin.qq.com/channels/ec/merchant/privatenumber/sendverifycode";
+ /** 获取小店手机号认证状态 */
+ String GET_PHONE_URL = "https://api.weixin.qq.com/channels/ec/merchant/privatenumber/getphone";
}
/** 售后相关接口 */
@@ -427,6 +491,34 @@ public interface League {
String GET_LEAGUE_ITEM_LIST_URL = "https://api.weixin.qq.com/channels/ec/league/item/list/get";
}
+ /** 代发管理相关接口 */
+ public interface Supplier {
+ /** 获取供货商列表 */
+ String GET_SUPPLIER_LIST_URL = "https://api.weixin.qq.com/channels/ec/supplier/relation/get_supplier_list";
+ /** 获取分配方式 */
+ String GET_DISTRIBUTE_URL = "https://api.weixin.qq.com/channels/ec/supplier/relation/get_distribute";
+ /** 设置全店订单手动分配 */
+ String SET_MANUALLY_DISTRIBUTE_URL = "https://api.weixin.qq.com/channels/ec/supplier/relation/set_manually_distribute";
+ /** 设置全店订单自动分配 */
+ String SET_ALL_DISTRIBUTION_URL = "https://api.weixin.qq.com/channels/ec/supplier/relation/set_all_distribution";
+ /** 设置按商品自动分配 */
+ String SET_PRODUCT_DISTRIBUTE_URL = "https://api.weixin.qq.com/channels/ec/supplier/relation/set_product_distribute";
+ /** 获取商品对应的自动分配供货商 */
+ String GET_PRODUCT_DEFAULT_DISTRIBUTE_URL = "https://api.weixin.qq.com/channels/ec/supplier/relation/get_product_default_distribute";
+ /** 获取按商品自动分配的商品列表 */
+ String GET_PRODUCT_LIST_URL = "https://api.weixin.qq.com/channels/ec/supplier/relation/get_product_list";
+ /** 分配订单代发 */
+ String ASSIGN_DROPSHIP_URL = "https://api.weixin.qq.com/channels/ec/order/dropship/assign";
+ /** 取消分配代发单 */
+ String CANCEL_DROPSHIP_URL = "https://api.weixin.qq.com/channels/ec/order/dropship/cancel";
+ /** 查询代发单详情 */
+ String GET_DROPSHIP_URL = "https://api.weixin.qq.com/channels/ec/order/dropship/get";
+ /** 拉取代发单列表 */
+ String GET_DROPSHIP_LIST_URL = "https://api.weixin.qq.com/channels/ec/order/dropship/list";
+ /** 搜索代发单 */
+ String SEARCH_DROPSHIP_URL = "https://api.weixin.qq.com/channels/ec/order/dropship/search";
+ }
+
/** 视频号助手开放接口 */
public interface Assistant {
@@ -508,6 +600,20 @@ public interface Vip {
String GRADE_UPDATE_URL = "https://api.weixin.qq.com/channels/ec/vip/user/grade/update";
}
+ /** 质检管理相关接口 */
+ public interface Qic {
+ /** 查询质检仓配置 */
+ String GET_INSPECT_CONFIG_URL = "https://api.weixin.qq.com/channels/ec/qic/inspect/config/get";
+ /** 查询送检配置模板信息 */
+ String GET_SUBMIT_CONFIG_URL = "https://api.weixin.qq.com/channels/ec/qic/inspect/submitconfig/get";
+ /** 打印质检码 */
+ String PRINT_INSPECT_CODE_URL = "https://api.weixin.qq.com/channels/ec/qic/inspect/code/print";
+ /** 绑定送检信息 */
+ String SUBMIT_INSPECT_INFO_URL = "https://api.weixin.qq.com/channels/ec/qic/inspect/submit";
+ /** 自寄快递送检 */
+ String REGISTER_LOGISTICS_URL = "https://api.weixin.qq.com/channels/ec/qic/inspect/register_logistics";
+ }
+
/**
* 直播大屏数据
*/
@@ -548,6 +654,32 @@ public interface CompassFinder {
String GET_SALE_PROFILE_DATA_URL = "https://api.weixin.qq.com/channels/ec/compass/finder/sale/profile/data/get";
}
+ /**
+ * 带货助手API
+ */
+ public interface Talent {
+
+ /**
+ * 获取佣金单列表
+ */
+ String GET_ORDER_LIST_URL = "https://api.weixin.qq.com/channels/ec/talent/get_order_list";
+
+ /**
+ * 获取佣金单详情
+ */
+ String GET_ORDER_DETAIL_URL = "https://api.weixin.qq.com/channels/ec/talent/get_order_detail";
+
+ /**
+ * 获取达人橱窗商品列表
+ */
+ String GET_WINDOW_PRODUCT_LIST_URL = "https://api.weixin.qq.com/channels/ec/talent/window/product/list/get";
+
+ /**
+ * 获取达人橱窗商品详情
+ */
+ String GET_WINDOW_PRODUCT_DETAIL_URL = "https://api.weixin.qq.com/channels/ec/talent/window/product/get";
+ }
+
/**
* 罗盘商家版API
*/
diff --git a/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelAfterSaleServiceImplTest.java b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelAfterSaleServiceImplTest.java
index 81122f7a03..529d455d46 100644
--- a/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelAfterSaleServiceImplTest.java
+++ b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelAfterSaleServiceImplTest.java
@@ -70,6 +70,18 @@ public void testReject() throws WxErrorException {
assertTrue(response.isSuccess());
}
+ @Test
+ public void testRejectWithCertificates() throws WxErrorException {
+ WxChannelAfterSaleService afterSaleService = channelService.getAfterSaleService();
+ String afterSaleOrderId = "";
+ String rejectReason = null;
+ List rejectCertificates = new ArrayList<>(4);
+ rejectCertificates.add("THE_FILE_ID_1");
+ WxChannelBaseResponse response = afterSaleService.reject(afterSaleOrderId, rejectReason, 1, rejectCertificates);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
@Test
public void testUploadRefundEvidence() throws WxErrorException {
WxChannelAfterSaleService afterSaleService = channelService.getAfterSaleService();
diff --git a/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelCategoryServiceImplTest.java b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelCategoryServiceImplTest.java
index 125e061cd8..06afde2993 100644
--- a/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelCategoryServiceImplTest.java
+++ b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelCategoryServiceImplTest.java
@@ -158,4 +158,14 @@ public void testListPassCategory() throws WxErrorException {
assertTrue(response.isSuccess());
System.out.println(response);
}
+
+ @Test
+ public void testListRelationCategory() throws WxErrorException {
+ WxChannelCategoryService categoryService = channelService.getCategoryService();
+ me.chanjar.weixin.channel.bean.category.RelationCategoryResponse response =
+ categoryService.listRelationCategory(true, 1);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ System.out.println(response);
+ }
}
diff --git a/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelFavoriteServiceImplTest.java b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelFavoriteServiceImplTest.java
new file mode 100644
index 0000000000..1e5dfd7c5d
--- /dev/null
+++ b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelFavoriteServiceImplTest.java
@@ -0,0 +1,35 @@
+package me.chanjar.weixin.channel.api.impl;
+
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import com.google.inject.Inject;
+import me.chanjar.weixin.channel.api.WxChannelFavoriteService;
+import me.chanjar.weixin.channel.api.WxChannelService;
+import me.chanjar.weixin.channel.bean.favorite.FavoriteCountResponse;
+import me.chanjar.weixin.channel.test.ApiTestModule;
+import me.chanjar.weixin.channel.util.JsonUtils;
+import me.chanjar.weixin.common.error.WxErrorException;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+/**
+ * 收藏管理接口 测试
+ *
+ * @author GitHub Copilot
+ */
+@Guice(modules = ApiTestModule.class)
+public class WxChannelFavoriteServiceImplTest {
+
+ @Inject
+ private WxChannelService channelService;
+
+ @Test
+ public void testGetFavoriteCount() throws WxErrorException {
+ WxChannelFavoriteService favoriteService = channelService.getFavoriteService();
+ FavoriteCountResponse response = favoriteService.getFavoriteCount();
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ System.out.println(JsonUtils.encode(response));
+ }
+}
diff --git a/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelOrderServiceImplTest.java b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelOrderServiceImplTest.java
index 2c70c7bde8..c72d6da513 100644
--- a/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelOrderServiceImplTest.java
+++ b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelOrderServiceImplTest.java
@@ -17,11 +17,16 @@
import me.chanjar.weixin.channel.bean.order.DecodeSensitiveInfoResponse;
import me.chanjar.weixin.channel.bean.order.DeliveryUpdateParam;
import me.chanjar.weixin.channel.bean.order.OrderAddressInfo;
+import me.chanjar.weixin.channel.bean.order.OrderCompensationDeliveryParam;
import me.chanjar.weixin.channel.bean.order.OrderInfoResponse;
import me.chanjar.weixin.channel.bean.order.OrderListParam;
import me.chanjar.weixin.channel.bean.order.OrderListResponse;
import me.chanjar.weixin.channel.bean.order.OrderSearchCondition;
import me.chanjar.weixin.channel.bean.order.OrderSearchParam;
+import me.chanjar.weixin.channel.bean.order.PreShipmentChangeSkuResponse;
+import me.chanjar.weixin.channel.bean.order.PresentSubOrderResponse;
+import me.chanjar.weixin.channel.bean.order.PrivateNumberGetPhoneResponse;
+import me.chanjar.weixin.channel.bean.order.RealNumberViewAuditResponse;
import me.chanjar.weixin.channel.bean.order.VirtualTelNumberResponse;
import me.chanjar.weixin.channel.test.ApiTestModule;
import me.chanjar.weixin.common.error.WxErrorException;
@@ -198,4 +203,123 @@ public void testDecodeSensitiveInfo() throws WxErrorException {
assertNotNull(response);
assertTrue(response.isSuccess());
}
+
+ @Test
+ public void testAddPresentNote() throws WxErrorException {
+ WxChannelOrderService orderService = channelService.getOrderService();
+ String orderId = "";
+ String note = "测试备注";
+ WxChannelBaseResponse response = orderService.addPresentNote(orderId, note);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testGetPresentSubOrders() throws WxErrorException {
+ WxChannelOrderService orderService = channelService.getOrderService();
+ String orderId = "";
+ PresentSubOrderResponse response = orderService.getPresentSubOrders(orderId);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testGetPreShipmentChangeSku() throws WxErrorException {
+ WxChannelOrderService orderService = channelService.getOrderService();
+ String orderId = "";
+ PreShipmentChangeSkuResponse response = orderService.getPreShipmentChangeSku(orderId);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testApprovePreShipmentChangeSku() throws WxErrorException {
+ WxChannelOrderService orderService = channelService.getOrderService();
+ String orderId = "";
+ WxChannelBaseResponse response = orderService.approvePreShipmentChangeSku(orderId);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testRejectPreShipmentChangeSku() throws WxErrorException {
+ WxChannelOrderService orderService = channelService.getOrderService();
+ String orderId = "";
+ String rejectReason = "库存不足";
+ WxChannelBaseResponse response = orderService.rejectPreShipmentChangeSku(orderId, rejectReason);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testApplyRealNumber() throws WxErrorException {
+ WxChannelOrderService orderService = channelService.getOrderService();
+ String orderId = "";
+ WxChannelBaseResponse response = orderService.applyRealNumber(orderId);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testGetRealNumberViewAudit() throws WxErrorException {
+ WxChannelOrderService orderService = channelService.getOrderService();
+ String orderId = "";
+ RealNumberViewAuditResponse response = orderService.getRealNumberViewAudit(orderId);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testApplyVirtualNumberAgain() throws WxErrorException {
+ WxChannelOrderService orderService = channelService.getOrderService();
+ String orderId = "";
+ WxChannelBaseResponse response = orderService.applyVirtualNumberAgain(orderId);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testDelayVirtualNumber() throws WxErrorException {
+ WxChannelOrderService orderService = channelService.getOrderService();
+ String orderId = "";
+ WxChannelBaseResponse response = orderService.delayVirtualNumber(orderId);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testAddPrivatePhone() throws WxErrorException {
+ WxChannelOrderService orderService = channelService.getOrderService();
+ String phone = "13800138000";
+ WxChannelBaseResponse response = orderService.addPrivatePhone(phone);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testSendPrivatePhoneVerifyCode() throws WxErrorException {
+ WxChannelOrderService orderService = channelService.getOrderService();
+ String phone = "13800138000";
+ WxChannelBaseResponse response = orderService.sendPrivatePhoneVerifyCode(phone);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testGetPrivatePhone() throws WxErrorException {
+ WxChannelOrderService orderService = channelService.getOrderService();
+ PrivateNumberGetPhoneResponse response = orderService.getPrivatePhone();
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testCompensationDelivery() throws WxErrorException {
+ WxChannelOrderService orderService = channelService.getOrderService();
+ OrderCompensationDeliveryParam param = new OrderCompensationDeliveryParam();
+ param.setOrderId("");
+ WxChannelBaseResponse response = orderService.compensationDelivery(param);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
}
diff --git a/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelProductServiceImplTest.java b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelProductServiceImplTest.java
index ed83434ee0..cd5042314b 100644
--- a/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelProductServiceImplTest.java
+++ b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelProductServiceImplTest.java
@@ -12,6 +12,13 @@
import me.chanjar.weixin.channel.bean.limit.LimitTaskAddResponse;
import me.chanjar.weixin.channel.bean.limit.LimitTaskListResponse;
import me.chanjar.weixin.channel.bean.limit.LimitTaskParam;
+import me.chanjar.weixin.channel.bean.product.GiftActivityAddResponse;
+import me.chanjar.weixin.channel.bean.product.GiftActivityInfo;
+import me.chanjar.weixin.channel.bean.product.GiftProductAddResponse;
+import me.chanjar.weixin.channel.bean.product.GiftProductGetResponse;
+import me.chanjar.weixin.channel.bean.product.GiftProductInfo;
+import me.chanjar.weixin.channel.bean.product.GiftProductListParam;
+import me.chanjar.weixin.channel.bean.product.GiftProductListResponse;
import me.chanjar.weixin.channel.bean.product.SkuStockBatchResponse;
import me.chanjar.weixin.channel.bean.product.SkuStockResponse;
import me.chanjar.weixin.channel.bean.product.SpuGetResponse;
@@ -212,4 +219,80 @@ public void testDeleteLimitTask() throws WxErrorException {
assertNotNull(response);
assertTrue(response.isSuccess());
}
+
+ @Test
+ public void testAddGiftProduct() throws WxErrorException {
+ WxChannelProductService productService = channelService.getProductService();
+ GiftProductInfo info = new GiftProductInfo();
+ GiftProductAddResponse response = productService.addGiftProduct(info);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testUpdateGiftProduct() throws WxErrorException {
+ WxChannelProductService productService = channelService.getProductService();
+ GiftProductInfo info = new GiftProductInfo();
+ WxChannelBaseResponse response = productService.updateGiftProduct(info);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testSetProductAsGift() throws WxErrorException {
+ WxChannelProductService productService = channelService.getProductService();
+ WxChannelBaseResponse response = productService.setProductAsGift("");
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testGetGiftProduct() throws WxErrorException {
+ WxChannelProductService productService = channelService.getProductService();
+ GiftProductGetResponse response = productService.getGiftProduct("");
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testListGiftProduct() throws WxErrorException {
+ WxChannelProductService productService = channelService.getProductService();
+ GiftProductListParam param = new GiftProductListParam(10, null, null);
+ GiftProductListResponse response = productService.listGiftProduct(param);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testUpdateGiftStock() throws WxErrorException {
+ WxChannelProductService productService = channelService.getProductService();
+ WxChannelBaseResponse response = productService.updateGiftStock("", "", 1, 10);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testAddGiftActivity() throws WxErrorException {
+ WxChannelProductService productService = channelService.getProductService();
+ GiftActivityInfo info = new GiftActivityInfo();
+ GiftActivityAddResponse response = productService.addGiftActivity(info);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testDeleteGiftActivity() throws WxErrorException {
+ WxChannelProductService productService = channelService.getProductService();
+ WxChannelBaseResponse response = productService.deleteGiftActivity("");
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testStopGiftActivity() throws WxErrorException {
+ WxChannelProductService productService = channelService.getProductService();
+ WxChannelBaseResponse response = productService.stopGiftActivity("");
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
}
diff --git a/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelQicServiceImplTest.java b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelQicServiceImplTest.java
new file mode 100644
index 0000000000..3a2b657bc5
--- /dev/null
+++ b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelQicServiceImplTest.java
@@ -0,0 +1,93 @@
+package me.chanjar.weixin.channel.api.impl;
+
+import com.google.inject.Inject;
+import me.chanjar.weixin.channel.api.WxChannelQicService;
+import me.chanjar.weixin.channel.api.WxChannelService;
+import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
+import me.chanjar.weixin.channel.bean.qic.InspectCodeResponse;
+import me.chanjar.weixin.channel.bean.qic.InspectConfigResponse;
+import me.chanjar.weixin.channel.bean.qic.RegisterLogisticsRequest;
+import me.chanjar.weixin.channel.bean.qic.SubmitConfigResponse;
+import me.chanjar.weixin.channel.bean.qic.SubmitInspectRequest;
+import me.chanjar.weixin.channel.test.ApiTestModule;
+import me.chanjar.weixin.common.error.WxErrorException;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import java.util.Collections;
+
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+@Guice(modules = ApiTestModule.class)
+public class WxChannelQicServiceImplTest {
+
+ @Inject
+ private WxChannelService channelService;
+
+ @Test
+ public void testGetInspectConfig() throws WxErrorException {
+ WxChannelQicService qicService = channelService.getQicService();
+ InspectConfigResponse response = qicService.getInspectConfig();
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testGetSubmitConfig() throws WxErrorException {
+ WxChannelQicService qicService = channelService.getQicService();
+ SubmitConfigResponse response = qicService.getSubmitConfig("123456");
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testPrintInspectCode() throws WxErrorException {
+ WxChannelQicService qicService = channelService.getQicService();
+ InspectCodeResponse response = qicService.printInspectCode("123456");
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testSubmitInspectInfo() throws WxErrorException {
+ WxChannelQicService qicService = channelService.getQicService();
+ SubmitInspectRequest request = new SubmitInspectRequest();
+ request.setOrderId("123456");
+ SubmitInspectRequest.InspectInfo inspectInfo = new SubmitInspectRequest.InspectInfo();
+ inspectInfo.setDeliveryId("YTO");
+ inspectInfo.setBackupDeliveryId("SF");
+ inspectInfo.setExpressInsure(Boolean.FALSE);
+ inspectInfo.setExpressMerge(Boolean.FALSE);
+ inspectInfo.setInspectOrgId("ORG_1");
+ inspectInfo.setRefundIntercept(0);
+ inspectInfo.setInspectOrgName("机构A");
+ inspectInfo.setWarehouseName("质检仓");
+ inspectInfo.setWarehouseAddr("质检仓地址");
+ inspectInfo.setDeliveryProductId(1L);
+ inspectInfo.setBackupDeliveryProductId(2L);
+ request.setInspectInfo(inspectInfo);
+
+ WxChannelBaseResponse response = qicService.submitInspectInfo(request);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testRegisterLogistics() throws WxErrorException {
+ WxChannelQicService qicService = channelService.getQicService();
+ RegisterLogisticsRequest request = new RegisterLogisticsRequest();
+ request.setOrderIdList(Collections.singletonList("123456"));
+
+ RegisterLogisticsRequest.LogisticsInfo logisticsInfo = new RegisterLogisticsRequest.LogisticsInfo();
+ logisticsInfo.setWaybillId("YT1234567890");
+ logisticsInfo.setDeliveryId("YTO");
+ logisticsInfo.setDeliveryName("圆通速递");
+ logisticsInfo.setDeliveryType(1);
+ request.setLogisticsInfo(logisticsInfo);
+
+ WxChannelBaseResponse response = qicService.registerLogistics(request);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+}
diff --git a/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelShopLinkServiceImplTest.java b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelShopLinkServiceImplTest.java
new file mode 100644
index 0000000000..87785a5396
--- /dev/null
+++ b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxChannelShopLinkServiceImplTest.java
@@ -0,0 +1,56 @@
+package me.chanjar.weixin.channel.api.impl;
+
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import com.google.inject.Inject;
+import me.chanjar.weixin.channel.api.WxChannelBasicService;
+import me.chanjar.weixin.channel.api.WxChannelService;
+import me.chanjar.weixin.channel.bean.shop.ShopH5UrlResponse;
+import me.chanjar.weixin.channel.bean.shop.ShopQrCodeResponse;
+import me.chanjar.weixin.channel.bean.shop.ShopTagLinkResponse;
+import me.chanjar.weixin.channel.test.ApiTestModule;
+import me.chanjar.weixin.channel.util.JsonUtils;
+import me.chanjar.weixin.common.error.WxErrorException;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+/**
+ * 店铺链接接口 测试
+ *
+ * @author GitHub Copilot
+ */
+@Guice(modules = ApiTestModule.class)
+public class WxChannelShopLinkServiceImplTest {
+
+ @Inject
+ private WxChannelService channelService;
+
+ @Test
+ public void testGetShopH5Url() throws WxErrorException {
+ WxChannelBasicService basicService = channelService.getBasicService();
+ ShopH5UrlResponse response = basicService.getShopH5Url();
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ System.out.println(JsonUtils.encode(response));
+ }
+
+ @Test
+ public void testGetShopQrCode() throws WxErrorException {
+ WxChannelBasicService basicService = channelService.getBasicService();
+ // qrcodeType: 1=二维码, 2=标准物料, 3=送礼物物料
+ ShopQrCodeResponse response = basicService.getShopQrCode(1);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ System.out.println(JsonUtils.encode(response));
+ }
+
+ @Test
+ public void testGetShopTagLink() throws WxErrorException {
+ WxChannelBasicService basicService = channelService.getBasicService();
+ ShopTagLinkResponse response = basicService.getShopTagLink();
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ System.out.println(JsonUtils.encode(response));
+ }
+}
diff --git a/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxTalentServiceImplTest.java b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxTalentServiceImplTest.java
new file mode 100644
index 0000000000..2e0b5ba781
--- /dev/null
+++ b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/api/impl/WxTalentServiceImplTest.java
@@ -0,0 +1,70 @@
+package me.chanjar.weixin.channel.api.impl;
+
+import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertTrue;
+
+import com.google.inject.Inject;
+import me.chanjar.weixin.channel.api.WxChannelService;
+import me.chanjar.weixin.channel.api.WxTalentService;
+import me.chanjar.weixin.channel.bean.talent.TalentOrderDetailParam;
+import me.chanjar.weixin.channel.bean.talent.TalentOrderDetailResponse;
+import me.chanjar.weixin.channel.bean.talent.TalentOrderListParam;
+import me.chanjar.weixin.channel.bean.talent.TalentOrderListResponse;
+import me.chanjar.weixin.channel.bean.talent.TalentWindowProductDetailParam;
+import me.chanjar.weixin.channel.bean.talent.TalentWindowProductDetailResponse;
+import me.chanjar.weixin.channel.bean.talent.TalentWindowProductListParam;
+import me.chanjar.weixin.channel.bean.talent.TalentWindowProductListResponse;
+import me.chanjar.weixin.channel.test.ApiTestModule;
+import me.chanjar.weixin.common.error.WxErrorException;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+/**
+ * 带货助手服务测试
+ *
+ * @author GitHub Copilot
+ */
+@Guice(modules = ApiTestModule.class)
+public class WxTalentServiceImplTest {
+
+ @Inject
+ private WxChannelService channelService;
+
+ @Test
+ public void testGetOrderList() throws WxErrorException {
+ WxTalentService talentService = channelService.getTalentService();
+ TalentOrderListParam param = new TalentOrderListParam();
+ TalentOrderListResponse response = talentService.getOrderList(param);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testGetOrderDetail() throws WxErrorException {
+ WxTalentService talentService = channelService.getTalentService();
+ TalentOrderDetailParam param = new TalentOrderDetailParam();
+ param.setOrderId("");
+ TalentOrderDetailResponse response = talentService.getOrderDetail(param);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testGetWindowProductList() throws WxErrorException {
+ WxTalentService talentService = channelService.getTalentService();
+ TalentWindowProductListParam param = new TalentWindowProductListParam();
+ TalentWindowProductListResponse response = talentService.getWindowProductList(param);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+
+ @Test
+ public void testGetWindowProductDetail() throws WxErrorException {
+ WxTalentService talentService = channelService.getTalentService();
+ TalentWindowProductDetailParam param = new TalentWindowProductDetailParam();
+ param.setProductId("");
+ TalentWindowProductDetailResponse response = talentService.getWindowProductDetail(param);
+ assertNotNull(response);
+ assertTrue(response.isSuccess());
+ }
+}
diff --git a/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/bean/supplier/WxChannelSupplierBeanTest.java b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/bean/supplier/WxChannelSupplierBeanTest.java
new file mode 100644
index 0000000000..2c562b8dbc
--- /dev/null
+++ b/weixin-java-channel/src/test/java/me/chanjar/weixin/channel/bean/supplier/WxChannelSupplierBeanTest.java
@@ -0,0 +1,62 @@
+package me.chanjar.weixin.channel.bean.supplier;
+
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertNotNull;
+
+import java.util.Arrays;
+import me.chanjar.weixin.channel.util.JsonUtils;
+import org.testng.annotations.Test;
+
+/**
+ * @author GitHub Copilot
+ */
+public class WxChannelSupplierBeanTest {
+
+ @Test
+ public void testEncodeProductDistributeRequest() {
+ ProductDistributeRequest request = new ProductDistributeRequest();
+ request.setSupplierId("1001");
+ request.setProductIdList(Arrays.asList("p1", "p2"));
+
+ String json = JsonUtils.encode(request);
+ assertNotNull(json);
+ assertFalse(json.contains("supplierId"));
+ assertFalse(json.contains("productIdList"));
+ ProductDistributeRequest decoded = JsonUtils.decode(json, ProductDistributeRequest.class);
+ assertEquals(decoded.getSupplierId(), "1001");
+ assertEquals(decoded.getProductIdList(), Arrays.asList("p1", "p2"));
+ }
+
+ @Test
+ public void testEncodeDropshipAssignRequest() {
+ DropshipAssignRequest request = new DropshipAssignRequest();
+ request.setOrderId("o1");
+ request.setSupplierId("s1");
+
+ String json = JsonUtils.encode(request);
+ assertNotNull(json);
+ DropshipAssignRequest decoded = JsonUtils.decode(json, DropshipAssignRequest.class);
+ assertEquals(decoded.getOrderId(), "o1");
+ assertEquals(decoded.getSupplierId(), "s1");
+ }
+
+ @Test
+ public void testDecodeSupplierListResponse() {
+ String json = "{\"errcode\":0,\"errmsg\":\"ok\",\"supplier_list\":[{\"supplier_id\":\"s1\",\"supplier_name\":\"供货商1\",\"status\":1}],\"has_more\":false}";
+ SupplierListResponse response = JsonUtils.decode(json, SupplierListResponse.class);
+
+ assertNotNull(response);
+ assertEquals(response.getErrCode(), 0);
+ assertEquals(response.getSupplierList().size(), 1);
+ assertEquals(response.getSupplierList().get(0).getSupplierId(), "s1");
+ }
+
+ @Test
+ public void testDecodeDropshipInfo() {
+ DropshipInfo info = JsonUtils.decode("{\"ds_order_id\":\"d1\"}", DropshipInfo.class);
+
+ assertNotNull(info);
+ assertEquals(info.getDropshipId(), "d1");
+ }
+}
diff --git a/weixin-java-common/pom.xml b/weixin-java-common/pom.xml
index e5427942c4..5916ab6e37 100644
--- a/weixin-java-common/pom.xml
+++ b/weixin-java-common/pom.xml
@@ -6,7 +6,7 @@
com.github.binarywang
wx-java
- 4.8.0
+ 4.8.5.B
weixin-java-common
@@ -24,20 +24,14 @@
okhttp
provided
+
org.apache.httpcomponents.client5
httpclient5
provided
-
- org.slf4j
- slf4j-api
-
-
- com.thoughtworks.xstream
- xstream
-
+
org.apache.httpcomponents
httpclient
@@ -52,6 +46,15 @@
org.apache.httpcomponents
httpmime
+
+
+ org.slf4j
+ slf4j-api
+
+
+ com.thoughtworks.xstream
+ xstream
+
org.slf4j
jcl-over-slf4j
@@ -82,6 +85,11 @@
org.projectlombok
lombok
+
+ org.mockito
+ mockito-core
+ test
+
ch.qos.logback
@@ -93,11 +101,7 @@
testng
test
-
- org.mockito
- mockito-all
- test
-
+
com.google.inject
guice
@@ -144,6 +148,9 @@
org.springframework.data
spring-data-redis
+ ${spring-data-redis.version}
+ true
+ provided
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 70c4e47933..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
@@ -12,7 +12,7 @@
/**
* 微信开发所使用到的常量类.
*
- * @author Daniel Qian & binarywang & Wang_Wong
+ * @author Daniel Qian, binarywang, Wang_Wong
*/
@UtilityClass
public class WxConsts {
@@ -53,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";
}
@@ -498,6 +499,11 @@ public static class EventType {
* 订单完成发货时、订单结算时
*/
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";
}
/**
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
index 3a9872fc92..42e1869502 100644
--- 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
@@ -10,6 +10,8 @@
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
/**
* 通用文件上传参数
@@ -34,6 +36,27 @@ public class CommonUploadParam implements Serializable {
@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);
+ }
+
/**
* 从文件构造
*
@@ -43,7 +66,7 @@ public class CommonUploadParam implements Serializable {
*/
@SneakyThrows
public static CommonUploadParam fromFile(String name, File file) {
- return new CommonUploadParam(name, CommonUploadData.fromFile(file));
+ return new CommonUploadParam(name, CommonUploadData.fromFile(file), null);
}
/**
@@ -55,11 +78,32 @@ public static CommonUploadParam fromFile(String name, File file) {
*/
@SneakyThrows
public static CommonUploadParam fromBytes(String name, @Nullable String fileName, byte[] bytes) {
- return new CommonUploadParam(name, new CommonUploadData(fileName, new ByteArrayInputStream(bytes), bytes.length));
+ 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}", name, data);
+ 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/oauth2/WxOAuth2AccessToken.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/oauth2/WxOAuth2AccessToken.java
index c08a49063d..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
*/
@@ -36,8 +39,10 @@ public class WxOAuth2AccessToken implements Serializable {
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/WxOcrMenuResult.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrMenuResult.java
new file mode 100644
index 0000000000..6f5a2bc27e
--- /dev/null
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/bean/ocr/WxOcrMenuResult.java
@@ -0,0 +1,47 @@
+package me.chanjar.weixin.common.bean.ocr;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import me.chanjar.weixin.common.util.json.WxGsonBuilder;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * OCR菜单识别结果.
+ *
+ * @author GitHub Copilot
+ */
+@Data
+public class WxOcrMenuResult implements Serializable {
+ private static final long serialVersionUID = -8062516251827437945L;
+
+ @SerializedName("img_size")
+ private WxOcrImgSize imgSize;
+ @SerializedName("items")
+ private List items;
+
+ public static WxOcrMenuResult fromJson(String json) {
+ return WxGsonBuilder.create().fromJson(json, WxOcrMenuResult.class);
+ }
+
+ @Override
+ public String toString() {
+ return WxGsonBuilder.create().toJson(this);
+ }
+
+ @Data
+ public static class Items implements Serializable {
+ private static final long serialVersionUID = 3066181677009102792L;
+
+ @SerializedName("text")
+ private String text;
+ @SerializedName("pos")
+ private WxOcrPos pos;
+
+ @Override
+ public String toString() {
+ return WxGsonBuilder.create().toJson(this);
+ }
+ }
+}
diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxCpErrorMsgEnum.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/error/WxCpErrorMsgEnum.java
index ea1e9e7c68..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
@@ -453,7 +453,7 @@ public enum WxCpErrorMsgEnum {
*/
CODE_60008(60008, "部门已存在;部门ID或者部门名称已存在"),
/**
- * 部门名称含有非法字符;不能含有 \\:?*“< >| 等字符.
+ * {@code 部门名称含有非法字符;不能含有 \\:?*"< >| 等字符.}
*/
CODE_60009(60009, "部门名称含有非法字符;不能含有 \\ :?*“< >| 等字符"),
/**
@@ -521,7 +521,7 @@ public enum WxCpErrorMsgEnum {
*/
CODE_60124(60124, "无效的父部门id;父部门不存在通讯录中"),
/**
- * 非法部门名字;不能为空,且不能超过64字节,且不能含有\\:*?”< >|等字符.
+ * {@code 非法部门名字;不能为空,且不能超过64字节,且不能含有\\:*?"< >|等字符.}
*/
CODE_60125(60125, "非法部门名字;不能为空,且不能超过64字节,且不能含有\\:*?”< >|等字符"),
/**
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 b45fba3411..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
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 1bb3f6472b..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"),
/**
@@ -99,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, "请求的参数不正确"),
/**
@@ -138,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次"),
/**
@@ -190,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秒内跟用户有过消息交互"),
/**
@@ -686,7 +687,7 @@ public enum WxMaErrorMsgEnum {
/**
* 89252
- * 法人&企业信息一致性校验中 front checking
+ * {@code 法人&企业信息一致性校验中 front checking}
*/
CODE_89252(89252, "法人&企业信息一致性校验中"),
@@ -837,6 +838,34 @@ public enum WxMaErrorMsgEnum {
*/
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 28fb5de8ad..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"),
@@ -4242,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"),
@@ -4347,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"),
@@ -6432,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"),
@@ -6682,7 +6682,7 @@ public enum WxOpenErrorMsgEnum {
CODE_89251(89251, "模板消息已下发,待法人人脸核身校验"),
/**
- * 法人&企业信息一致性校验中 front checking
+ * {@code 法人&企业信息一致性校验中 front checking}
*/
CODE_89253(89253, "法人&企业信息一致性校验中"),
@@ -7257,7 +7257,7 @@ public enum WxOpenErrorMsgEnum {
CODE_200021(200021, "场景描述 sceneDesc 参数错误"),
/**
- * 禁止创建/更新商品(如商品创建功能被封禁) 或 禁止编辑&更新房间
+ * {@code 禁止创建/更新商品(如商品创建功能被封禁) 或 禁止编辑&更新房间}
*/
CODE_300001(300001, "禁止创建/更新商品(如商品创建功能被封禁) 或 禁止编辑&更新房间"),
@@ -8382,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"),
@@ -8397,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"),
@@ -8412,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/executor/CommonUploadRequestExecutorApacheImpl.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/executor/CommonUploadRequestExecutorApacheImpl.java
index 7f19241cdb..dba92e27da 100644
--- 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
@@ -44,11 +44,19 @@ public String execute(String uri, CommonUploadParam param, WxType wxType) throws
if (param != null) {
CommonUploadData data = param.getData();
InnerStreamBody part = new InnerStreamBody(data.getInputStream(), ContentType.DEFAULT_BINARY, data.getFileName(), data.getLength());
- HttpEntity entity = MultipartEntityBuilder
+ MultipartEntityBuilder entityBuilder = MultipartEntityBuilder
.create()
.addPart(param.getName(), part)
- .setMode(HttpMultipartMode.RFC6532)
- .build();
+ .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);
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
index f79eaa49b8..f79e4cd96f 100644
--- 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
@@ -41,11 +41,19 @@ public String execute(String uri, CommonUploadParam param, WxType wxType) throws
if (param != null) {
CommonUploadData data = param.getData();
InnerStreamBody part = new InnerStreamBody(data.getInputStream(), ContentType.DEFAULT_BINARY, data.getFileName(), data.getLength());
- HttpEntity entity = MultipartEntityBuilder
+ MultipartEntityBuilder entityBuilder = MultipartEntityBuilder
.create()
.addPart(param.getName(), part)
- .setMode(HttpMultipartMode.EXTENDED)
- .build();
+ .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);
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
index 36e8660f77..182820d076 100644
--- 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
@@ -39,6 +39,14 @@ public String execute(String uri, CommonUploadParam param, WxType wxType) throws
}
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();
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
index 40a4622b89..6a0343980f 100644
--- 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
@@ -31,10 +31,18 @@ public CommonUploadRequestExecutorOkHttpImpl(RequestHttp 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()) {
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/service/WxOcrService.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/service/WxOcrService.java
index 39a8a93754..08524eeafc 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
@@ -7,12 +7,15 @@
import me.chanjar.weixin.common.bean.ocr.WxOcrDrivingLicenseResult;
import me.chanjar.weixin.common.bean.ocr.WxOcrDrivingResult;
import me.chanjar.weixin.common.bean.ocr.WxOcrIdCardResult;
+import me.chanjar.weixin.common.bean.ocr.WxOcrMenuResult;
import java.io.File;
/**
* 基于小程序或 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
* created on 2019-06-22
@@ -128,4 +131,22 @@ public interface WxOcrService {
* @throws WxErrorException .
*/
WxOcrCommResult comm(File imgFile) throws WxErrorException;
+
+ /**
+ * 菜单OCR识别接口
+ * 文件大小限制:小于2M
+ * @param imgUrl 图片url地址
+ * @return WxOcrMenuResult
+ * @throws WxErrorException .
+ */
+ WxOcrMenuResult menu(String imgUrl) throws WxErrorException;
+
+ /**
+ * 菜单OCR识别接口
+ * 文件大小限制:小于2M
+ * @param imgFile 图片文件对象
+ * @return WxOcrMenuResult
+ * @throws WxErrorException .
+ */
+ WxOcrMenuResult menu(File imgFile) 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/DataUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/DataUtils.java
index b8fb42e0e9..2e9f5d4c7b 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/DataUtils.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/DataUtils.java
@@ -17,8 +17,10 @@ public class DataUtils {
*/
public static E handleDataWithSecret(E data) {
E dataForLog = data;
- if(data instanceof String && StringUtils.contains((String)data, "&secret=")){
- dataForLog = (E) RegExUtils.replaceAll((String)data,"&secret=\\w+&","&secret=******&");
+ if (data instanceof String) {
+ String stringData = (String) data;
+ stringData = RegExUtils.replaceAll(stringData, "(^|[?&])secret=[^&]*", "$1secret=******");
+ dataForLog = (E) RegExUtils.replaceAll(stringData, "(\\\"openid\\\"\\s*:\\s*\\\")[^\\\"]*(\\\")", "$1******$2");
}
return dataForLog;
}
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/crypto/SHA1.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/crypto/SHA1.java
index 9b9f776768..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
@@ -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 0b0590b1e6..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
@@ -197,6 +197,7 @@ public EncryptContext encryptContext(String plainText) {
/**
* 对明文进行加密.
*
+ * @param randomStr 随机字符串
* @param plainText 需要加密的明文
* @return 加密后base64编码的字符串
*/
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 4a267153c6..8419c87d5a 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
@@ -57,13 +57,10 @@ public static File createTmpFile(InputStream inputStream, String name, String ex
* @return base64编码
*/
public static String imageToBase64ByStream(InputStream in) {
- byte[] data = null;
// 读取图片字节数组
try {
- data = new byte[in.available()];
- in.read(data);
// 返回Base64编码过的字节数组字符串
- return Base64.getEncoder().encodeToString(data);
+ return Base64.getEncoder().encodeToString(IOUtils.toByteArray(in));
} catch (IOException e) {
e.printStackTrace();
} finally {
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 d07873f3c4..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,8 +10,9 @@
/**
* 输入流数据.
- *
+ *
* InputStreamData
+ *
*
* @author zichuan.zhou91@gmail.com
* created on 2022/2/15
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 de34ca5bd1..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,42 +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/json/GsonHelper.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonHelper.java
index 0d807402ac..84ebffe72f 100644
--- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonHelper.java
+++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/json/GsonHelper.java
@@ -108,7 +108,7 @@ public static float getAsPrimitiveFloat(JsonElement element) {
}
public static Integer[] getIntArray(JsonObject o, String string) {
- JsonArray jsonArray = getAsJsonArray(o.getAsJsonArray(string));
+ JsonArray jsonArray = getAsJsonArray(o.get(string));
if (jsonArray == null) {
return null;
}
@@ -122,7 +122,7 @@ public static Integer[] getIntArray(JsonObject o, String string) {
}
public static String[] getStringArray(JsonObject o, String string) {
- JsonArray jsonArray = getAsJsonArray(o.getAsJsonArray(string));
+ JsonArray jsonArray = getAsJsonArray(o.get(string));
if (jsonArray == null) {
return null;
}
@@ -136,7 +136,7 @@ public static String[] getStringArray(JsonObject o, String string) {
}
public static Long[] getLongArray(JsonObject o, String string) {
- JsonArray jsonArray = getAsJsonArray(o.getAsJsonArray(string));
+ JsonArray jsonArray = getAsJsonArray(o.get(string));
if (jsonArray == null) {
return null;
}
@@ -150,7 +150,7 @@ public static Long[] getLongArray(JsonObject o, String string) {
}
public static JsonArray getAsJsonArray(JsonElement element) {
- return element == null ? null : element.getAsJsonArray();
+ return (element == null || !element.isJsonArray()) ? null : element.getAsJsonArray();
}
/**
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 6ea269f7e4..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
@@ -43,6 +43,11 @@ public boolean shouldSkipClass(Class> aClass) {
});
}
+ /**
+ * 创建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/locks/RedisTemplateSimpleDistributedLock.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/locks/RedisTemplateSimpleDistributedLock.java
index 3f5ce4d692..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
@@ -42,13 +42,17 @@ public RedisTemplateSimpleDistributedLock( StringRedisTemplate redisTemplate, S
@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
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 3fa91fa70e..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
@@ -22,6 +22,11 @@ public class XStreamInitializer {
public static ClassLoader classLoader;
+ /**
+ * 设置类加载器
+ *
+ * @param classLoaderInfo 类加载器
+ */
public static void setClassLoader(ClassLoader classLoaderInfo) {
classLoader = classLoaderInfo;
}
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/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/DataUtilsTest.java b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/DataUtilsTest.java
index f5732d9a0b..c5088e78c9 100644
--- a/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/DataUtilsTest.java
+++ b/weixin-java-common/src/test/java/me/chanjar/weixin/common/util/DataUtilsTest.java
@@ -19,4 +19,42 @@ public void testHandleDataWithSecret() {
final String s = DataUtils.handleDataWithSecret(data);
assertTrue(s.contains("&secret=******&"));
}
+
+ @Test
+ public void testHandleDataWithSecretAtEnd() {
+ // Secret is the last parameter in the query string, so there is no trailing &
+ String data = "appid=wx123&secret=abc123";
+ final String s = DataUtils.handleDataWithSecret(data);
+ assertFalse(s.contains("abc123"), "Secret at the end of the string should be masked");
+ assertTrue(s.contains("secret=******"), "Secret should be replaced with asterisks");
+ }
+
+ @Test
+ public void testHandleDataWithSecretAsFirstParam() {
+ // Secret is the first parameter, so there is no leading &
+ String data = "secret=abc123&appid=wx123";
+ final String s = DataUtils.handleDataWithSecret(data);
+ assertFalse(s.contains("abc123"), "Secret as the first parameter should be masked");
+ assertTrue(s.contains("secret=******"), "Secret should be replaced with asterisks");
+ }
+
+ @Test
+ public void testHandleDataWithSecretEncodedValue() {
+ // The secret value contains URL-encoded and non-word characters; the whole value must be masked
+ String data = "appid=wx123&secret=abc%2Fdef-.+ghi&grant_type=client_credential";
+ final String s = DataUtils.handleDataWithSecret(data);
+ assertFalse(s.contains("def"), "The full secret value must be masked, including after non-word characters");
+ assertFalse(s.contains("%2F"), "Encoded characters in the secret must be masked too");
+ assertTrue(s.contains("&secret=******&"), "Secret should be replaced with asterisks");
+ }
+
+ @Test
+ public void testHandleDataWithOpenidInJson() {
+ String data = "{\"code\":\"phone-code\",\"openid\":\"user-openid\"}";
+
+ String result = DataUtils.handleDataWithSecret(data);
+
+ assertFalse(result.contains("user-openid"));
+ assertTrue(result.contains("\"openid\":\"******\""));
+ }
}
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
index 5a25fb1493..3ab603e47c 100644
--- 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
@@ -6,8 +6,10 @@
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
+import java.io.InputStream;
import java.nio.charset.Charset;
import java.nio.file.Files;
+import java.util.Base64;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@@ -30,5 +32,31 @@ public void testTestCreateTmpFile() {
@Test
public void testImageToBase64ByStream() {
+ byte[] original = new byte[5000];
+ for (int i = 0; i < original.length; i++) {
+ original[i] = (byte) (i % 256);
+ }
+ // A stream that returns at most 16 bytes per read() call (like a network stream),
+ // so a single in.read(buffer) does not fill the buffer.
+ InputStream chunked = new InputStream() {
+ private final ByteArrayInputStream delegate = new ByteArrayInputStream(original);
+
+ @Override
+ public int read() {
+ return delegate.read();
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) {
+ return delegate.read(b, off, Math.min(len, 16));
+ }
+
+ @Override
+ public int available() {
+ return delegate.available();
+ }
+ };
+ String result = FileUtils.imageToBase64ByStream(chunked);
+ assertThat(result).isEqualTo(Base64.getEncoder().encodeToString(original));
}
}
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 bafe3c30d1..775ac2207b 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
@@ -136,4 +136,29 @@ public void testBuildSimpleJsonObject() {
System.out.println(GsonHelper.buildJsonObject(1, true, "jsonElement", new JsonObject()));
System.out.println(GsonHelper.buildJsonObject("num", 2, "string", "cde", "char", 'a', "bool", true));
}
+
+ /**
+ * If the JSON property exists but is NOT a JSON array (e.g. a plain string),
+ * getIntArray should return null gracefully rather than throwing ClassCastException.
+ */
+ @Test
+ public void testGetIntArrayNonArrayProperty() {
+ JsonObject json = new JsonObject();
+ json.addProperty("items", "not-an-array");
+ assertThat(GsonHelper.getIntArray(json, "items")).isNull();
+ }
+
+ @Test
+ public void testGetStringArrayNonArrayProperty() {
+ JsonObject json = new JsonObject();
+ json.addProperty("items", 123);
+ assertThat(GsonHelper.getStringArray(json, "items")).isNull();
+ }
+
+ @Test
+ public void testGetLongArrayNonArrayProperty() {
+ JsonObject json = new JsonObject();
+ json.addProperty("items", "not-an-array");
+ assertThat(GsonHelper.getLongArray(json, "items")).isNull();
+ }
}
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-cp/INTELLIGENT_ROBOT.md b/weixin-java-cp/INTELLIGENT_ROBOT.md
index dcd90e1a1a..18dd0c677f 100644
--- a/weixin-java-cp/INTELLIGENT_ROBOT.md
+++ b/weixin-java-cp/INTELLIGENT_ROBOT.md
@@ -109,6 +109,16 @@ 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
@@ -146,4 +156,4 @@ robotService.deleteRobot(robotId);
1. 需要确保企业微信应用具有智能机器人相关权限
2. 智能机器人功能可能需要特定的企业微信版本支持
3. 会话ID可以用于保持对话的连续性,提升用户体验
-4. 机器人状态: 0表示停用,1表示启用
\ No newline at end of file
+4. 机器人状态: 0表示停用,1表示启用
diff --git a/weixin-java-cp/pom.xml b/weixin-java-cp/pom.xml
index 6c47176afc..791d63c7fa 100644
--- a/weixin-java-cp/pom.xml
+++ b/weixin-java-cp/pom.xml
@@ -7,7 +7,7 @@
com.github.binarywang
wx-java
- 4.8.0
+ 4.8.5.B
weixin-java-cp
@@ -30,6 +30,10 @@
okhttp
provided
+
+ org.apache.httpcomponents
+ httpclient
+
org.apache.httpcomponents.client5
httpclient5
@@ -51,7 +55,11 @@
org.springframework.data
spring-data-redis
+ ${spring-data-redis.version}
+ true
+ provided
+
org.testng
testng
@@ -59,9 +67,10 @@
org.mockito
- mockito-all
+ mockito-core
test
+
com.google.inject
guice
@@ -89,7 +98,6 @@
org.bouncycastle
bcprov-jdk18on
- 1.80
@@ -119,6 +127,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/me/chanjar/weixin/cp/api/WxCpAgentService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpAgentService.java
index 05f06f1da9..7302a88f0c 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
@@ -27,7 +27,7 @@ public interface WxCpAgentService {
* @return wx cp agent
* @throws WxErrorException the wx error exception
*/
- WxCpAgent get(Integer agentId) throws WxErrorException;
+ WxCpAgent get(Long agentId) throws WxErrorException;
/**
*
@@ -65,6 +65,6 @@ public interface WxCpAgentService {
* @return admin list
* @throws WxErrorException the wx error exception
*/
- WxCpTpAdmin getAdminList(Integer agentId) throws WxErrorException;
+ WxCpTpAdmin getAdminList(Long agentId) 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
index 4da13d3fde..16882669b7 100644
--- 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
@@ -9,7 +9,7 @@
* 企业互联相关接口
*
* @author libo <422423229@qq.com>
- * Created on 27/2/2023 9:57 PM
+ * @since 2023-02-27 9:57 PM
*/
public interface WxCpCorpGroupService {
/**
@@ -23,5 +23,5 @@ public interface WxCpCorpGroupService {
* @return the list
* @throws WxErrorException the wx error exception
*/
- List listAppShareInfo(Integer agentId, Integer businessType, String corpId, Integer limit, String cursor) throws WxErrorException;
+ List listAppShareInfo(Long 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/WxCpExportService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpExportService.java
index 24c6ea9dc1..a2c7adabea 100644
--- 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
@@ -85,7 +85,7 @@ public interface WxCpExportService {
* 获取导出结果
*
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/export/get_result?access_token=ACCESS_TOKEN&jobid=jobid_xxxxxxxxxxxxxxx
+ * {@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
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 7f3cdeab7c..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
@@ -146,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}
*
* 权限说明:
*
@@ -252,9 +252,9 @@ public interface WxCpExternalContactService {
*
* 权限说明:
*
- * 该企业授权了该服务商第三方应用,且授权的第三方应用具备“企业客户权限->客户基础信息”权限
+ * {@code 该企业授权了该服务商第三方应用,且授权的第三方应用具备“企业客户权限->客户基础信息”权限}
* 该客户的跟进人必须在应用的可见范围之内
- * 应用需具备“企业客户权限->客户基础信息”权限
+ * {@code 应用需具备“企业客户权限->客户基础信息”权限}
*
*
* @param externalUserid 代开发自建应用获取到的外部联系人ID
@@ -276,8 +276,8 @@ public interface WxCpExternalContactService {
*
* @param externalUserid 服务商主体的external_userid,必须是source_agentid对应的应用所获取
* @param sourceAgentId 企业授权的代开发自建应用或第三方应用的agentid
- * @return
- * @throws WxErrorException
+ * @return 企业的external_userid
+ * @throws WxErrorException 微信错误异常
*/
String fromServiceExternalUserid(String externalUserid, String sourceAgentId) throws WxErrorException;
@@ -362,7 +362,7 @@ public interface WxCpExternalContactService {
* 权限说明:
*
* 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)
- * 第三方应用需具有“企业客户权限->客户基础信息”权限
+ * {@code 第三方应用需具有“企业客户权限->客户基础信息”权限}
* 对于第三方/自建应用,群主必须在应用的可见范围
* 仅支持企业服务人员创建的客户群
* 仅可转换出自己企业下的客户群chat_id
@@ -410,11 +410,12 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c
* 文档地址: https://developer.work.weixin.qq.com/document/path/99434
*
*
+ * 注意:企业可通过外部联系人临时ID排除重复数据,外部联系人临时ID有效期为4小时。
+ *
* @param cursor the cursor
* @param limit the limit
* @return 已服务的外部联系人列表
* @throws WxErrorException .
- * @apiNote 企业可通过外部联系人临时ID排除重复数据,外部联系人临时ID有效期为4小时。
*/
WxCpExternalContactListInfo getContactList(String cursor, Integer limit) throws WxErrorException;
@@ -438,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}
*
* 权限说明:
*
@@ -469,7 +470,8 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c
/**
* 获取待分配的离职成员列表
* 企业和第三方可通过此接口,获取所有离职成员的客户列表,并可进一步调用分配离职成员的客户接口将这些客户重新分配给其他企业成员。
- *
+
+ *
* 请求方式:POST(HTTPS)
* 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_unassigned_list?access_token=ACCESS_TOKEN
*
@@ -496,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
@@ -516,13 +518,13 @@ WxCpExternalContactBatchInfo getContactDetailBatch(String[] userIdList, String c
/**
* 企业和第三方可通过此接口查询在职成员的客户转接情况。
- *
+ *
* 权限说明:
- *
+ *
* 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
- * 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限
+ * {@code 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限}
* 接替成员必须在此第三方应用或自建应用的可见范围内。
- *
+ *
*
* @param handOverUserid 原添加成员的userid
* @param takeOverUserid 接替成员的userid
@@ -535,19 +537,21 @@ WxCpUserTransferResultResp transferResult(String handOverUserid, String takeOver
/**
* 企业可通过此接口,分配离职成员的客户给其他成员。
- *
+ *
* handover_userid必须是已离职用户。
* external_userid必须是handover_userid的客户(即配置了客户联系功能的成员所添加的联系人)。
* 在职成员的每位客户最多被分配2次。客户被转接成功后,将有90个自然日的服务关系保护期,保护期内的客户无法再次被分配。
- *
+
+ *
* 权限说明:
- *
+
+ *
* 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
- * 第三方应用需拥有“企业客户权限->客户联系->离职分配”权限
+ * {@code 第三方应用需拥有“企业客户权限->客户联系->离职分配”权限}
* 接替成员必须在此第三方应用或自建应用的可见范围内。
* 接替成员需要配置了客户联系功能。
* 接替成员需要在企业微信激活且已经过实名认证。
- *
+ *
*
* @param req 转接在职成员的客户给其他成员请求实体
* @return wx cp base resp
@@ -557,13 +561,14 @@ WxCpUserTransferResultResp transferResult(String handOverUserid, String takeOver
/**
* 企业和第三方可通过此接口查询离职成员的客户分配情况。
- *
+ *
* 权限说明:
- *
+
+ *
* 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
- * 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限
+ * {@code 第三方应用需拥有“企业客户权限->客户联系->在职继承”权限}
* 接替成员必须在此第三方应用或自建应用的可见范围内。
- *
+ *
*
* @param handOverUserid 原添加成员的userid
* @param takeOverUserid 接替成员的userid
@@ -630,21 +635,24 @@ WxCpUserExternalGroupChatList listGroupChat(Integer pageIndex, Integer pageSize,
/**
* 企业可通过此接口,将已离职成员为群主的群,分配给另一个客服成员。
*
- *
+ *
* 注意::
- *
+
+ *
* 群主离职了的客户群,才可继承
* 继承给的新群主,必须是配置了客户联系功能的成员
* 继承给的新群主,必须有设置实名
* 继承给的新群主,必须有激活企业微信
* 同一个人的群,限制每天最多分配300个给新群主
- *
+
+ *
* 权限说明:
- *
+
+ *
* 企业需要使用“客户联系”secret或配置到“可调用应用”列表中的自建应用secret所获取的accesstoken来调用(accesstoken如何获取?)。
- * 第三方应用需拥有“企业客户权限->客户联系->分配离职成员的客户群”权限
+ * {@code 第三方应用需拥有“企业客户权限->客户联系->分配离职成员的客户群”权限}
* 对于第三方/自建应用,群主必须在应用的可见范围。
- *
+ *
*
* @param chatIds 需要转群主的客户群ID列表。取值范围: 1 ~ 100
* @param newOwner 新群主ID
@@ -656,7 +664,7 @@ WxCpUserExternalGroupChatList listGroupChat(Integer pageIndex, Integer pageSize,
/**
* 企业可通过此接口,将在职成员为群主的群,分配给另一个客服成员。
- *
+ *
* 注意:
* 继承给的新群主,必须是配置了客户联系功能的成员
* 继承给的新群主,必须有设置实名
@@ -716,11 +724,14 @@ WxCpUserExternalGroupChatStatistic getGroupChatStatistic(Date startTime, Integer
* 企业可通过此接口添加企业群发消息的任务并通知客服人员发送给相关客户或客户群。(注:企业微信终端需升级到2.7.5版本及以上)
* 注意:调用该接口并不会直接发送消息给客户/客户群,需要相关的客服人员操作以后才会实际发送(客服人员的企业微信需要升级到2.7.5及以上版本)
* 同一个企业每个自然月内仅可针对一个客户/客户群发送4条消息,超过限制的用户将会被忽略。
- *
+
+ *
* 请求方式: POST(HTTP)
- *
+
+ *
* 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_msg_template?access_token=ACCESS_TOKEN
- *
+
+ *
* 文档地址
*
* @param wxCpMsgTemplate the wx cp msg template
@@ -733,15 +744,18 @@ WxCpUserExternalGroupChatStatistic getGroupChatStatistic(Date startTime, Integer
/**
* 提醒成员群发
* 企业和第三方应用可调用此接口,重新触发群发通知,提醒成员完成群发任务,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;
@@ -753,11 +767,12 @@ WxCpUserExternalGroupChatStatistic getGroupChatStatistic(Date startTime, Integer
* 请求方式: 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;
@@ -1002,7 +1017,7 @@ WxCpGroupMsgListResult getGroupMsgListV2(String chatType, Date startTime, Date e
/**
*
* 企业和第三方应用可通过此接口获取企业与成员的群发记录。
- * 获取企业群发成员执行结果
+ * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/93338
*
*
* @param msgid 群发消息的id,通过获取群发记录列表接口返回
@@ -1031,7 +1046,7 @@ WxCpGroupMsgListResult getGroupMsgListV2(String chatType, Date startTime, Date e
/**
*
* 获取群发成员发送任务列表。
- * 获取群发成员发送任务列表
+ * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/93338
*
*
* @param msgid 群发消息的id,通过获取群发记录列表接口返回
@@ -1045,7 +1060,7 @@ WxCpGroupMsgListResult getGroupMsgListV2(String chatType, Date startTime, Date e
/**
*
* 添加入群欢迎语素材。
- * 添加入群欢迎语素材
+ * 文档地址:https://open.work.weixin.qq.com/api/doc/90000/90135/92366
*
*
* @param template 素材内容
@@ -1057,7 +1072,7 @@ WxCpGroupMsgListResult getGroupMsgListV2(String chatType, Date startTime, Date e
/**
*
* 编辑入群欢迎语素材。
- * 编辑入群欢迎语素材
+ * 文档地址:https://open.work.weixin.qq.com/api/doc/90000/90135/92366
*
*
* @param template the template
@@ -1069,7 +1084,7 @@ WxCpGroupMsgListResult getGroupMsgListV2(String chatType, Date startTime, Date e
/**
*
* 获取入群欢迎语素材。
- * 获取入群欢迎语素材
+ * 文档地址:https://open.work.weixin.qq.com/api/doc/90000/90135/92366
*
*
* @param templateId 群欢迎语的素材id
@@ -1082,7 +1097,7 @@ WxCpGroupMsgListResult getGroupMsgListV2(String chatType, Date startTime, Date e
*
* 删除入群欢迎语素材。
* 企业可通过此API删除入群欢迎语素材,且仅能删除调用方自己创建的入群欢迎语素材。
- * 删除入群欢迎语素材
+ * 文档地址:https://open.work.weixin.qq.com/api/doc/90000/90135/92366
*
*
* @param templateId 群欢迎语的素材id
@@ -1094,8 +1109,8 @@ WxCpGroupMsgListResult getGroupMsgListV2(String chatType, Date startTime, Date e
/**
*
- * 获取商品图册
- * 获取商品图册列表
+ * 获取商品图册列表
+ * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/95096
*
*
* @param limit 返回的最大记录数,整型,最大值100,默认值50,超过最大值时取默认值
@@ -1108,7 +1123,7 @@ WxCpGroupMsgListResult getGroupMsgListV2(String chatType, Date startTime, Date e
/**
*
* 获取商品图册
- * 获取商品图册
+ * 文档地址:https://work.weixin.qq.com/api/doc/90000/90135/95096
*
*
* @param productId 商品id
@@ -1155,7 +1170,7 @@ WxMediaUploadResult uploadAttachment(String mediaType, Integer attachmentType, F
* 企业和第三方应用可以通过此接口新建敏感词规则
* 请求方式: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
@@ -1169,7 +1184,7 @@ WxMediaUploadResult uploadAttachment(String mediaType, Integer attachmentType, F
* 企业和第三方应用可以通过此接口修改敏感词规则
* 请求方式: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
*/
@@ -1181,7 +1196,7 @@ WxMediaUploadResult uploadAttachment(String mediaType, Integer attachmentType, F
* 企业和第三方应用可以通过此接口修改敏感词规则
* 请求方式:POST(HTTPS)
* 请求地址
- *
+ *
* @param ruleId 规则id
* @throws WxErrorException the wx error exception
*/
@@ -1220,7 +1235,7 @@ WxMediaUploadResult uploadAttachment(String mediaType, Integer attachmentType, F
* 请求地址:
* 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
@@ -1235,7 +1250,7 @@ WxMediaUploadResult uploadAttachment(String mediaType, Integer attachmentType, F
* 请求地址:
* https://qyapi.weixin.qq.com/cgi-bin/externalcontact/update_product_album?access_token=ACCESS_TOKEN
* 文档地址
- *
+ *
* @param wxCpProductAlbumInfo 商品图册信息
* @throws WxErrorException the wx error exception
*/
@@ -1250,7 +1265,7 @@ WxMediaUploadResult uploadAttachment(String mediaType, Integer attachmentType, F
* https://qyapi.weixin.qq.com/cgi-bin/externalcontact/delete_product_album?access_token=ACCESS_TOKEN
*
* 文档地址
- *
+ *
* @param productId 商品id
* @throws WxErrorException the wx error exception
*/
@@ -1379,7 +1394,7 @@ WxMediaUploadResult uploadAttachment(String mediaType, Integer attachmentType, F
* 请求地址:https://qyapi.weixin.qq.com/cgi-bin/externalcontact/customer_acquisition/statistic?access_token=ACCESS_TOKEN
*
* @author Hugo
- * @date 2023/12/5 14:34
+ * @since 2023/12/5 14:34
* @param linkId 获客链接的id
* @param startTime 统计起始时间
* @param endTime 统计结束时间
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 c1a8d56255..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
@@ -126,9 +126,10 @@ public interface WxCpGroupRobotService {
/**
* 发送模板卡片消息
- * @param webhookUrl
- * @param wxCpGroupRobotMessage
- * @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
index bc5f3f1915..58f4373ceb 100644
--- 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
@@ -74,4 +74,12 @@ public interface WxCpIntelligentRobotService {
*/
WxCpIntelligentRobotSendMessageResponse sendMessage(WxCpIntelligentRobotSendMessageRequest request) throws WxErrorException;
-}
\ No newline at end of file
+ /**
+ * 解析智能机器人 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 5a53829dc0..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
@@ -222,7 +222,7 @@ WxCpKfCustomerBatchGetResp customerBatchGet(List externalUserIdList)
* 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
@@ -238,7 +238,7 @@ WxCpKfCustomerBatchGetResp customerBatchGet(List externalUserIdList)
* 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
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 a2e2344190..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
@@ -27,7 +27,7 @@ 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 获取的直播详情 living info
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 e874b26f42..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
@@ -110,9 +110,9 @@ 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
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 07f300dd14..bfa2c1c938 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
@@ -40,7 +40,7 @@ public interface WxCpMenuService {
* @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;
+ void create(Long agentId, WxMenu menu) throws WxErrorException;
/**
*
@@ -67,7 +67,7 @@ public interface WxCpMenuService {
* @throws WxErrorException the wx error exception
* @see #delete() #delete()
*/
- void delete(Integer agentId) throws WxErrorException;
+ void delete(Long agentId) throws WxErrorException;
/**
*
@@ -96,5 +96,5 @@ public interface WxCpMenuService {
* @throws WxErrorException the wx error exception
* @see #get() #get()
*/
- WxMenu get(Integer agentId) throws WxErrorException;
+ WxMenu get(Long 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 e49a36ba50..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
@@ -72,8 +72,9 @@ public interface WxCpMessageService {
* 请求地址: 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
+ * @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 221caf2e70..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
@@ -28,9 +28,26 @@ public interface WxCpMsgAuditService {
* @param timeout 超时时间,根据实际需要填写
* @return 返回是否调用成功 chat datas
* @throws Exception the exception
+ * @deprecated 请使用 {@link #getChatRecords(long, long, String, String, long)} 代替,
+ * 该方法会将SDK暴露给调用方,容易导致SDK生命周期管理混乱,引发JVM崩溃
*/
+ @Deprecated
WxCpChatDatas getChatDatas(long seq, @NonNull long limit, String proxy, String passwd, @NonNull long timeout) throws Exception;
+ /**
+ * 拉取聊天记录函数(推荐使用)
+ * 该方法不会将SDK暴露给调用方,SDK生命周期由框架自动管理,更加安全
+ *
+ * @param seq 从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0
+ * @param limit 一次拉取的消息条数,最大值1000条,超过1000条会返回错误
+ * @param proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081,如果没有传null
+ * @param passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123,如果没有传null
+ * @param timeout 超时时间,根据实际需要填写
+ * @return 返回聊天记录列表,不包含SDK信息
+ * @throws Exception the exception
+ */
+ List getChatRecords(long seq, @NonNull long limit, String proxy, String passwd, @NonNull long timeout) throws Exception;
+
/**
* 获取解密的聊天数据Model
*
@@ -39,10 +56,24 @@ public interface WxCpMsgAuditService {
* @param pkcs1 使用什么方式进行解密,1代表使用PKCS1进行解密,2代表PKCS8进行解密 ...
* @return 解密后的聊天数据 decrypt data
* @throws Exception the exception
+ * @deprecated 请使用 {@link #getDecryptChatData(WxCpChatDatas.WxCpChatData, Integer)} 代替,
+ * 该方法需要传入SDK,容易导致SDK生命周期管理混乱,引发JVM崩溃
*/
+ @Deprecated
WxCpChatModel getDecryptData(@NonNull long sdk, @NonNull WxCpChatDatas.WxCpChatData chatData,
@NonNull Integer pkcs1) throws Exception;
+ /**
+ * 获取解密的聊天数据Model(推荐使用)
+ * 该方法不需要传入SDK,SDK由框架自动管理,更加安全
+ *
+ * @param chatData 聊天数据
+ * @param pkcs1 使用什么方式进行解密,1代表使用PKCS1进行解密,2代表PKCS8进行解密 ...
+ * @return 解密后的聊天数据
+ * @throws Exception the exception
+ */
+ WxCpChatModel getDecryptChatData(@NonNull WxCpChatDatas.WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception;
+
/**
* 获取解密的聊天数据明文
*
@@ -51,9 +82,23 @@ WxCpChatModel getDecryptData(@NonNull long sdk, @NonNull WxCpChatDatas.WxCpChatD
* @param pkcs1 使用什么方式进行解密,1代表使用PKCS1进行解密,2代表PKCS8进行解密 ...
* @return 解密后的明文 chat plain text
* @throws Exception the exception
+ * @deprecated 请使用 {@link #getChatRecordPlainText(WxCpChatDatas.WxCpChatData, Integer)} 代替,
+ * 该方法需要传入SDK,容易导致SDK生命周期管理混乱,引发JVM崩溃
*/
+ @Deprecated
String getChatPlainText(@NonNull long sdk, @NonNull WxCpChatDatas.WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception;
+ /**
+ * 获取解密的聊天数据明文(推荐使用)
+ * 该方法不需要传入SDK,SDK由框架自动管理,更加安全
+ *
+ * @param chatData 聊天数据
+ * @param pkcs1 使用什么方式进行解密,1代表使用PKCS1进行解密,2代表PKCS8进行解密 ...
+ * @return 解密后的明文
+ * @throws Exception the exception
+ */
+ String getChatRecordPlainText(@NonNull WxCpChatDatas.WxCpChatData chatData, @NonNull Integer pkcs1) throws Exception;
+
/**
* 获取媒体文件
* 针对图片、文件等媒体数据,提供sdk接口拉取数据内容。
@@ -69,10 +114,32 @@ WxCpChatModel getDecryptData(@NonNull long sdk, @NonNull WxCpChatDatas.WxCpChatD
* @param timeout 超时时间,分片数据需累加到文件存储。单次最大返回512K字节,如果文件比较大,自行设置长一点,比如timeout=10000
* @param targetFilePath 目标文件绝对路径+实际文件名,比如:/usr/local/file/20220114/474f866b39d10718810d55262af82662.gif
* @throws WxErrorException the wx error exception
+ * @deprecated 请使用 {@link #downloadMediaFile(String, String, String, long, String)} 代替,
+ * 该方法需要传入SDK,容易导致SDK生命周期管理混乱,引发JVM崩溃
*/
+ @Deprecated
void getMediaFile(@NonNull long sdk, @NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout,
@NonNull String targetFilePath) throws WxErrorException;
+ /**
+ * 获取媒体文件(推荐使用)
+ * 该方法不需要传入SDK,SDK由框架自动管理,更加安全
+ * 针对图片、文件等媒体数据,提供sdk接口拉取数据内容。
+ *
+ * 注意:
+ * 根据上面返回的文件类型,拼接好存放文件的绝对路径即可。此时绝对路径写入文件流,来达到获取媒体文件的目的。
+ * 详情可以看官方文档,亦可阅读此接口源码。
+ *
+ * @param sdkfileid 消息体内容中的sdkfileid信息
+ * @param proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081,如果没有传null
+ * @param passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123,如果没有传null
+ * @param timeout 超时时间,分片数据需累加到文件存储。单次最大返回512K字节,如果文件比较大,自行设置长一点,比如timeout=10000
+ * @param targetFilePath 目标文件绝对路径+实际文件名,比如:/usr/local/file/20220114/474f866b39d10718810d55262af82662.gif
+ * @throws WxErrorException the wx error exception
+ */
+ void downloadMediaFile(@NonNull String sdkfileid, String proxy, String passwd, @NonNull long timeout,
+ @NonNull String targetFilePath) throws WxErrorException;
+
/**
* 获取媒体文件 传入一个lambda,each所有的数据分片byte[],更加灵活
* 针对图片、文件等媒体数据,提供sdk接口拉取数据内容。
@@ -85,10 +152,29 @@ void getMediaFile(@NonNull long sdk, @NonNull String sdkfileid, String proxy, St
* @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;
+
/**
* 获取会话内容存档开启成员列表
* 企业可通过此接口,获取企业开启会话内容存档的成员列表
@@ -129,4 +215,20 @@ void getMediaFile(@NonNull long sdk, @NonNull String sdkfileid, String proxy, St
*/
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 b7a44047aa..695534ba73 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
@@ -85,14 +85,15 @@ public interface WxCpOAuth2Service {
* @throws WxErrorException 异常
* @see #getUserInfo(String) #getUserInfo(String)
*/
- WxCpOauth2UserInfo getUserInfo(Integer agentId, String code) throws WxErrorException;
+ WxCpOauth2UserInfo getUserInfo(Long agentId, String code) throws WxErrorException;
/**
* 获取家校访问用户身份
* 该接口用于根据code获取家长或者学生信息
- *
+ *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/getuserinfo?access_token=ACCESS_TOKEN&code=CODE
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/getuserinfo?access_token=ACCESS_TOKEN&code=CODE}
+ *
*
* @param code the code
* @return school user info
@@ -123,7 +124,7 @@ public interface WxCpOAuth2Service {
/**
*
* 获取用户登录身份
- * https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=ACCESS_TOKEN&code=CODE
+ * {@code https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=ACCESS_TOKEN&code=CODE}
* 该接口可使用用户登录成功颁发的code来获取成员信息,适用于自建应用与代开发应用
*
* 注意: 旧的/user/getuserinfo 接口的url已变更为auth/getuserinfo,不过旧接口依旧可以使用,建议是关注新接口即可
@@ -140,13 +141,15 @@ public interface WxCpOAuth2Service {
/**
* 获取用户二次验证信息
- *
+ *
* api: https://qyapi.weixin.qq.com/cgi-bin/auth/get_tfa_info?access_token=ACCESS_TOKEN
* 权限说明:仅『通讯录同步』或者自建应用可调用,如用自建应用调用,用户需要在二次验证范围和应用可见范围内。
* 并发限制:20
+ *
*
* @param code 用户进入二次验证页面时,企业微信颁发的code,每次成员授权带上的code将不一样,code只能使用一次,5分钟未被使用自动过期
- * @return me.chanjar.weixin.cp.bean.workbench.WxCpSecondVerificationInfo 二次验证授权码,开发者可以调用通过二次验证接口,解锁企业微信终端.tfa_code有效期五分钟,且只能使用一次。
+ * @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/WxCpOaMeetingRoomService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaMeetingRoomService.java
index c2e6c5c872..cc039fd9f5 100644
--- 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
@@ -84,6 +84,7 @@ public interface WxCpOaMeetingRoomService {
*
*
* @param wxCpOaMeetingRoomBookingInfoRequest 会议室预定信息查询对象
+ * @return 会议室预定信息
* @throws WxErrorException .
*/
WxCpOaMeetingRoomBookingInfoResult getMeetingRoomBookingInfo(WxCpOaMeetingRoomBookingInfoRequest wxCpOaMeetingRoomBookingInfoRequest) throws WxErrorException;
@@ -99,6 +100,7 @@ public interface WxCpOaMeetingRoomService {
*
*
* @param wxCpOaMeetingRoomBookRequest 会议室预定对象
+ * @return 预定结果
* @throws WxErrorException .
*/
WxCpOaMeetingRoomBookResult bookingMeetingRoom(WxCpOaMeetingRoomBookRequest wxCpOaMeetingRoomBookRequest) throws WxErrorException;
@@ -114,6 +116,7 @@ public interface WxCpOaMeetingRoomService {
*
*
* @param wxCpOaMeetingRoomBookByScheduleRequest 会议室预定对象
+ * @return 预定结果
* @throws WxErrorException .
*/
WxCpOaMeetingRoomBookResult bookingMeetingRoomBySchedule(WxCpOaMeetingRoomBookByScheduleRequest wxCpOaMeetingRoomBookByScheduleRequest) throws WxErrorException;
@@ -129,6 +132,7 @@ public interface WxCpOaMeetingRoomService {
*
*
* @param wxCpOaMeetingRoomBookByMeetingRequest 会议室预定对象
+ * @return 预定结果
* @throws WxErrorException .
*/
WxCpOaMeetingRoomBookResult bookingMeetingRoomByMeeting(WxCpOaMeetingRoomBookByMeetingRequest wxCpOaMeetingRoomBookByMeetingRequest) throws WxErrorException;
@@ -147,10 +151,10 @@ public interface WxCpOaMeetingRoomService {
* @param wxCpOaMeetingRoomCancelBookRequest 取消预定会议室对象
* @throws WxErrorException .
*/
- void cancelBookMeetingRoom(WxCpOaMeetingRoomCancelBookRequest wxCpOaMeetingRoomCancelBookRequest) throws WxErrorException;
+ void cancelBookMeetingRoom(WxCpOaMeetingRoomCancelBookRequest wxCpOaMeetingRoomCancelBookRequest) throws WxErrorException;
- /**
+ /**
* 根据会议室预定ID查询预定详情.
*
* 企业可通过此接口根据预定id查询相关会议室的预定情况
@@ -161,8 +165,9 @@ public interface WxCpOaMeetingRoomService {
*
*
* @param wxCpOaMeetingRoomBookingInfoByBookingIdRequest 根据会议室预定ID查询预定详情对象
+ * @return 预定详情
* @throws WxErrorException .
*/
- WxCpOaMeetingRoomBookingInfoByBookingIdResult getBookingInfoByBookingId(WxCpOaMeetingRoomBookingInfoByBookingIdRequest wxCpOaMeetingRoomBookingInfoByBookingIdRequest) throws WxErrorException;
+ WxCpOaMeetingRoomBookingInfoByBookingIdResult getBookingInfoByBookingId(WxCpOaMeetingRoomBookingInfoByBookingIdRequest wxCpOaMeetingRoomBookingInfoByBookingIdRequest) throws WxErrorException;
}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpOaService.java
index ee57107b5c..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
@@ -11,7 +11,8 @@
/**
* 企业微信OA相关接口.
*
- * @author Element & Wang_Wong created on 2019-04-06 10:52
+ * @author Element, Wang_Wong
+ * @since 2019-04-06 10:52
*/
public interface WxCpOaService {
@@ -331,7 +332,7 @@ List getDialRecord(Date startTime, Date endTime, Integer offset,
* 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
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
index 1356c839b2..712bc2a89c 100644
--- 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
@@ -5,6 +5,10 @@
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;
+
/**
* 企业微信文档相关接口.
* 文档
@@ -78,4 +82,462 @@ public interface WxCpOaWeDocService {
* @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/WxCpSchoolService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpSchoolService.java
index 56687c9cb1..5f1d41c197 100644
--- 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
@@ -80,9 +80,10 @@ public interface WxCpSchoolService {
/**
* 获取直播详情
+ *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/living/get_living_info?access_token=ACCESS_TOKEN&livingid
- * =LIVINGID
+ * {@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
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
index a92bfcc100..d004ca8aa5 100644
--- 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
@@ -19,9 +19,10 @@ public interface WxCpSchoolUserService {
/**
* 获取访问用户身份
* 该接口用于根据code获取成员信息
- *
+ *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN&code=CODE
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=ACCESS_TOKEN&code=CODE}
+ *
*
* @param code the code
* @return user info
@@ -32,9 +33,10 @@ public interface WxCpSchoolUserService {
/**
* 获取家校访问用户身份
* 该接口用于根据code获取家长或者学生信息
- *
+ *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/getuserinfo?access_token=ACCESS_TOKEN&code=CODE
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/getuserinfo?access_token=ACCESS_TOKEN&code=CODE}
+ *
*
* @param code the code
* @return school user info
@@ -90,8 +92,10 @@ public interface WxCpSchoolUserService {
/**
* 删除学生
+ *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/delete_student?access_token=ACCESS_TOKEN&userid=USERID
+ * {@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
@@ -160,8 +164,10 @@ WxCpBaseResp updateStudent(@NonNull String studentUserId, String newStudentUserI
/**
* 读取学生或家长
+ *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/get?access_token=ACCESS_TOKEN&userid=USERID
+ * {@code 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/get?access_token=ACCESS_TOKEN&userid=USERID}
+ *
*
* @param userId the user id
* @return user
@@ -171,9 +177,10 @@ WxCpBaseResp updateStudent(@NonNull String studentUserId, String newStudentUserI
/**
* 获取部门成员详情
+ *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/list?access_token=ACCESS_TOKEN&department_id=DEPARTMENT_ID
- * &fetch_child=FETCH_CHILD
+ * {@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:是否递归获取子部门下面的成员
@@ -184,9 +191,10 @@ WxCpBaseResp updateStudent(@NonNull String studentUserId, String newStudentUserI
/**
* 获取部门家长详情
+ *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/list_parent?access_token=ACCESS_TOKEN&department_id
- * =DEPARTMENT_ID
+ * {@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
@@ -207,8 +215,10 @@ WxCpBaseResp updateStudent(@NonNull String studentUserId, String newStudentUserI
/**
* 删除家长
+ *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/user/delete_parent?access_token=ACCESS_TOKEN&userid=USERID
+ * {@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
@@ -256,7 +266,7 @@ WxCpBaseResp updateStudent(@NonNull String studentUserId, String newStudentUserI
/**
* 删除部门
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/department/delete?access_token=ACCESS_TOKEN&id=ID
+ * {@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
@@ -292,10 +302,9 @@ WxCpBaseResp updateStudent(@NonNull String studentUserId, String newStudentUserI
/**
* 获取外部联系人详情
* 学校可通过此接口,根据外部联系人的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}
*
* @param externalUserId 外部联系人的userid,注意不是学校成员的帐号
* @return external contact
@@ -306,9 +315,9 @@ WxCpBaseResp updateStudent(@NonNull String studentUserId, String newStudentUserI
/**
* 获取可使用的家长范围
* 获取可在微信「学校通知-学校应用」使用该应用的家长范围,以学生或部门列表的形式返回。应用只能给该列表下的家长发送「学校通知」。注意该范围只能由学校的系统管理员在「管理端-家校沟通-配置」配置。
- *
+ *
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/agent/get_allow_scope?access_token=ACCESS_TOKEN&agentid=AGENTID
+ * {@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
@@ -332,7 +341,7 @@ WxCpBaseResp updateStudent(@NonNull String studentUserId, String newStudentUserI
/**
* 获取部门列表
* 请求方式:GET(HTTPS)
- * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/school/department/list?access_token=ACCESS_TOKEN&id=ID
+ * {@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
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 0b601ca502..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
@@ -57,6 +57,32 @@ 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
*
@@ -194,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的时候,可以用这个,
@@ -455,6 +520,13 @@ public interface WxCpService extends WxService {
*/
WxCpOaWeDriveService getOaWeDriveService();
+ /**
+ * 获取OA效率工具 文档的服务类对象
+ *
+ * @return oa we doc service
+ */
+ WxCpOaWeDocService getOaWeDocService();
+
/**
* 获取会话存档相关接口的服务类对象
*
@@ -584,7 +656,7 @@ public interface WxCpService extends WxService {
/**
* 企业互联的服务类对象
*
- * @return
+ * @return 企业互联服务对象
*/
WxCpCorpGroupService getCorpGroupService();
@@ -594,4 +666,11 @@ public interface WxCpService extends WxService {
* @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/WxCpUserService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpUserService.java
index 2368386b23..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
@@ -38,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
*
@@ -213,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
*
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 bc18c9bc7a..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
@@ -59,6 +59,7 @@ public abstract class BaseWxCpServiceImpl implements WxCpService, RequestH
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);
@@ -75,6 +76,7 @@ public abstract class BaseWxCpServiceImpl implements WxCpService, RequestH
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的锁.
@@ -301,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过期时才去刷新,然后重新执行请求,而不是全局定时请求.
*/
@@ -584,6 +619,11 @@ public WxCpOaWeDriveService getOaWeDriveService() {
return oaWeDriveService;
}
+ @Override
+ public WxCpOaWeDocService getOaWeDocService() {
+ return oaWeDocService;
+ }
+
@Override
public WxCpMsgAuditService getMsgAuditService() {
return msgAuditService;
@@ -708,4 +748,9 @@ public WxCpCorpGroupService getCorpGroupService() {
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 cc08d33bb1..6f2ade2e49 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
@@ -34,7 +34,7 @@ public class WxCpAgentServiceImpl implements WxCpAgentService {
private final WxCpService mainService;
@Override
- public WxCpAgent get(Integer agentId) throws WxErrorException {
+ public WxCpAgent get(Long agentId) throws WxErrorException {
if (agentId == null) {
throw new IllegalArgumentException("缺少agentid参数");
}
@@ -67,7 +67,7 @@ public List list() throws WxErrorException {
}
@Override
- public WxCpTpAdmin getAdminList(Integer agentId) throws WxErrorException {
+ public WxCpTpAdmin getAdminList(Long agentId) throws WxErrorException {
if (agentId == null) {
throw new IllegalArgumentException("缺少agentid参数");
}
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
index 48bd952a83..73d11cc15d 100644
--- 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
@@ -18,14 +18,14 @@
* 企业互联相关接口实现类
*
* @author libo <422423229@qq.com>
- * Created on 27/2/2023 9:57 PM
+ * @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,
+ public List listAppShareInfo(Long 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();
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 8e3a8d7b95..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
@@ -38,7 +38,7 @@
/**
* The type Wx cp external contact service.
*
- * @author 曹祖鹏 & yuanqixun & Mr.Pan & Wang_Wong
+ * @author 曹祖鹏, yuanqixun, Mr.Pan, Wang_Wong
*/
@RequiredArgsConstructor
public class WxCpExternalContactServiceImpl implements WxCpExternalContactService {
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
index 8a12fa4ff4..aba1ee85c4 100644
--- 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
@@ -67,4 +67,9 @@ public WxCpIntelligentRobotSendMessageResponse sendMessage(WxCpIntelligentRobotS
return WxCpIntelligentRobotSendMessageResponse.fromJson(responseText);
}
-}
\ No newline at end of file
+ @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/WxCpMenuServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMenuServiceImpl.java
index d008e77083..0920c2b3e0 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMenuServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpMenuServiceImpl.java
@@ -27,7 +27,7 @@ public void create(WxMenu menu) throws WxErrorException {
}
@Override
- public void create(Integer agentId, WxMenu menu) throws WxErrorException {
+ public void create(Long agentId, WxMenu menu) throws WxErrorException {
String url = String.format(this.mainService.getWxCpConfigStorage().getApiUrl(MENU_CREATE), agentId);
this.mainService.post(url, menu.toJson());
}
@@ -38,7 +38,7 @@ public void delete() throws WxErrorException {
}
@Override
- public void delete(Integer agentId) throws WxErrorException {
+ public void delete(Long agentId) throws WxErrorException {
String url = String.format(this.mainService.getWxCpConfigStorage().getApiUrl(MENU_DELETE), agentId);
this.mainService.get(url, null);
}
@@ -49,7 +49,7 @@ public WxMenu get() throws WxErrorException {
}
@Override
- public WxMenu get(Integer agentId) throws WxErrorException {
+ public WxMenu get(Long agentId) throws WxErrorException {
String url = String.format(this.mainService.getWxCpConfigStorage().getApiUrl(MENU_GET), agentId);
try {
String resultContent = this.mainService.get(url, null);
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 6daea8ef2f..def73e4152 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
@@ -21,7 +21,7 @@ public class WxCpMessageServiceImpl implements WxCpMessageService {
@Override
public WxCpMessageSendResult send(WxCpMessage message) throws WxErrorException {
- Integer agentId = message.getAgentId();
+ Long agentId = message.getAgentId();
if (null == agentId) {
message.setAgentId(this.cpService.getWxCpConfigStorage().getAgentId());
}
@@ -38,7 +38,7 @@ public WxCpMessageSendStatistics getStatistics(int timeType) throws WxErrorExcep
@Override
public WxCpLinkedCorpMessageSendResult sendLinkedCorpMessage(WxCpLinkedCorpMessage message) throws WxErrorException {
- Integer agentId = message.getAgentId();
+ Long agentId = message.getAgentId();
if (null == agentId) {
message.setAgentId(this.cpService.getWxCpConfigStorage().getAgentId());
}
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 cdf559ad7a..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
@@ -20,8 +20,11 @@
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.*;
@@ -36,16 +39,17 @@
public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService {
private final WxCpService cpService;
- /**
- * SDK初始化有效期,根据企微文档为7200秒
- */
- private static final int SDK_EXPIRES_TIME = 7200;
+ /** 每个线程持有独立 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 {
- // 获取或初始化SDK
- long sdk = this.initSdk();
+ // 旧版 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);
@@ -67,23 +71,39 @@ public WxCpChatDatas getChatDatas(long seq, @NonNull long limit, String proxy, S
}
/**
- * 获取或初始化SDK,如果SDK已过期则重新初始化
+ * 获取当前线程的 SDK,不存在则初始化。
+ * SDK 在线程内跨调用复用,无需每次重新初始化。
*
* @return sdk id
* @throws WxErrorException 初始化失败时抛出异常
*/
- private synchronized long initSdk() throws WxErrorException {
- WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage();
-
- // 检查SDK是否已缓存且未过期
- if (!configStorage.isMsgAuditSdkExpired()) {
- long cachedSdk = configStorage.getMsgAuditSdk();
- if (cachedSdk > 0) {
- return cachedSdk;
+ 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();
- // SDK未初始化或已过期,需要重新初始化
String configPath = configStorage.getMsgAuditLibPath();
if (StringUtils.isEmpty(configPath)) {
throw new WxErrorException("请配置会话存档sdk文件的路径,不要配错了!!");
@@ -129,12 +149,31 @@ private synchronized long initSdk() throws WxErrorException {
Finance.DestroySdk(sdk);
throw new WxErrorException("init sdk err ret " + ret);
}
+ return sdk;
+ }
- // 缓存SDK
- configStorage.updateMsgAuditSdk(sdk, SDK_EXPIRES_TIME);
- log.debug("初始化会话存档SDK成功,sdk={}", sdk);
+ @Override
+ public void closeThreadLocalSdk() {
+ Long sdk = threadLocalSdk.get();
+ // 先从 managedSdks 摘除,摘除成功才调 DestroySdk,防止与 closeAllSdks() 并发时 double-free
+ if (sdk != null && managedSdks.remove(sdk)) {
+ Finance.DestroySdk(sdk);
+ log.info("线程 [{}] 关闭会话存档SDK,sdk={}", Thread.currentThread().getName(), sdk);
+ }
+ threadLocalSdk.remove();
+ }
- return sdk;
+ @Override
+ public void closeAllSdks() {
+ // 逐一 remove 后再 Destroy,防止与 closeThreadLocalSdk() 并发时 double-free
+ Long[] sdks = managedSdks.toArray(new Long[0]);
+ for (Long sdk : sdks) {
+ if (managedSdks.remove(sdk)) {
+ Finance.DestroySdk(sdk);
+ log.info("关闭会话存档SDK,sdk={}", sdk);
+ }
+ }
+ threadLocalSdk.remove();
}
@Override
@@ -196,17 +235,18 @@ public void getMediaFile(@NonNull long sdk, @NonNull String sdkfileid, String pr
* 为空字符串,拉取后续分片时直接填入上次返回的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的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。
- FileOutputStream outputStream = new FileOutputStream(targetFile, true);
- outputStream.write(i);
- outputStream.close();
+ try (FileOutputStream outputStream = new FileOutputStream(targetFile, true)) {
+ outputStream.write(i);
+ }
} catch (Exception e) {
- e.printStackTrace();
+ log.error("写入媒体文件分片失败,targetFilePath={}", targetFilePath, e);
}
});
}
@@ -236,7 +276,7 @@ public void getMediaFile(@NonNull long sdk, @NonNull String sdkfileid, String pr
// 大于512k的文件会分片拉取,此处需要使用追加写,避免后面的分片覆盖之前的数据。
action.accept(Finance.GetData(mediaData));
} catch (Exception e) {
- e.printStackTrace();
+ log.error("处理媒体文件分片失败,sdkfileid={}", sdkfileid, e);
}
if (Finance.IsMediaDataFinish(mediaData) == 1) {
@@ -258,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());
@@ -269,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 d04a051c0e..9b390b5ec8 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
@@ -69,7 +69,7 @@ public WxCpOauth2UserInfo getUserInfo(String code) throws WxErrorException {
}
@Override
- public WxCpOauth2UserInfo getUserInfo(Integer agentId, String code) throws WxErrorException {
+ public WxCpOauth2UserInfo getUserInfo(Long agentId, String code) throws WxErrorException {
String responseText =
this.mainService.get(String.format(this.mainService.getWxCpConfigStorage().getApiUrl(GET_USER_INFO), code,
agentId), null);
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
index 81de32453d..7f5bf004db 100644
--- 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
@@ -4,18 +4,22 @@
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
+ * @author Wang_Wong created on 2022-04-22
*/
@Slf4j
@RequiredArgsConstructor
@@ -57,10 +61,276 @@ public WxCpDocInfo docInfo(@NonNull String docId) throws WxErrorException {
@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 WxCpDocShare.fromJson(responseContent);
+ 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/WxCpServiceApacheHttpClientImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceApacheHttpClientImpl.java
index 1042f88d67..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
@@ -17,6 +17,7 @@
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.
@@ -74,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
index 92fd2dbd9b..129823df5a 100644
--- 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
@@ -17,6 +17,7 @@
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.
@@ -75,6 +76,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, 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();
@@ -82,7 +173,8 @@ public void initHttp() {
apacheHttpClientBuilder.httpProxyHost(this.configStorage.getHttpProxyHost())
.httpProxyPort(this.configStorage.getHttpProxyPort())
.httpProxyUsername(this.configStorage.getHttpProxyUsername())
- .httpProxyPassword(this.configStorage.getHttpProxyPassword().toCharArray());
+ .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());
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 f2a50db471..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
@@ -70,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 5081341851..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
@@ -13,6 +13,8 @@
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.
*
@@ -63,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 511c440e64..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
@@ -12,6 +12,7 @@
import okhttp3.*;
import java.io.IOException;
+import java.util.concurrent.locks.Lock;
import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.GET_TOKEN;
@@ -55,12 +56,15 @@ public String getAccessToken(boolean forceRefresh) throws WxErrorException {
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);
@@ -74,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");
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 8469451428..0cf0ab6665 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
@@ -28,7 +28,7 @@ public class WxCpTaskCardServiceImpl implements WxCpTaskCardService {
@Override
public void update(List userIds, String taskId, String replaceName) throws WxErrorException {
- Integer agentId = this.mainService.getWxCpConfigStorage().getAgentId();
+ Long agentId = this.mainService.getWxCpConfigStorage().getAgentId();
Map data = new HashMap<>(4);
data.put("userids", userIds);
@@ -45,7 +45,7 @@ public void update(List userIds, String taskId, String replaceName) thro
public void updateTemplateCardButton(List userIds, List partyIds,
List tagIds, Integer atAll,
String responseCode, String replaceName) throws WxErrorException {
- Integer agentId = this.mainService.getWxCpConfigStorage().getAgentId();
+ Long agentId = this.mainService.getWxCpConfigStorage().getAgentId();
Map data = new HashMap<>(7);
data.put("userids", userIds);
data.put("partyids", partyIds);
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 5d61b3a199..8f96a729a0 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
@@ -32,7 +32,7 @@ public class WxCpAgent implements Serializable {
private String errMsg;
@SerializedName("agentid")
- private Integer agentId;
+ private Long agentId;
@SerializedName("name")
private String name;
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpAgentJsapiSignature.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpAgentJsapiSignature.java
index 4562d9b9b0..8ca7a6cb95 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpAgentJsapiSignature.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpAgentJsapiSignature.java
@@ -21,7 +21,7 @@ public class WxCpAgentJsapiSignature implements Serializable {
private String corpid;
- private Integer agentid;
+ private Long agentid;
private long timestamp;
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 6bf9a30aeb..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
@@ -10,7 +10,8 @@
/**
* 返回结果
*
- * @author yqx & WangWong created on 2020/3/16
+ * @author yqx, WangWong
+ * @since 2020/3/16
*/
@Getter
@Setter
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 fa50216153..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
@@ -216,7 +216,7 @@ public static class Agent implements Serializable {
/**
* 付费状态
- *
+ *
*
* - 0-没有付费;
* - 1-限时试用;
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/WxCpTpPermanentCodeInfo.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpPermanentCodeInfo.java
index 522e606a20..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
@@ -225,7 +225,7 @@ public static class Agent implements Serializable {
/**
* 付费状态
- *
+ *
*
* - 0-没有付费;
* - 1-限时试用;
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 74e1fec3f8..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
@@ -10,7 +10,7 @@
* 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 {
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 dfbf250480..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
@@ -6,7 +6,7 @@
* 企业微信第三方开发-增加标签成员成员api响应体
*
* @author zhangq
- * @since 2021 /2/14 16:44
+ * @since 2021/2/14 16:44
*/
public class WxCpTpTagAddOrRemoveUsersResult extends WxCpTagAddOrRemoveUsersResult {
private static final long serialVersionUID = 3490401800490702052L;
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 162030c956..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
@@ -6,7 +6,7 @@
* 获取标签成员接口响应体
*
* @author zhangq
- * @since 2021 /2/14 16:28
+ * @since 2021/2/14 16:28
*/
public class WxCpTpTagGetResult extends WxCpTagGetResult {
private static final long serialVersionUID = 9051748686315562400L;
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 templateList;
+
+ /**
+ * From json wx cp tp template list.
+ *
+ * @param json the json
+ * @return the wx cp tp template list
+ */
+ public static WxCpTpTemplateList fromJson(String json) {
+ return WxCpGsonBuilder.create().fromJson(json, WxCpTpTemplateList.class);
+ }
+
+ @Override
+ public String toJson() {
+ return WxCpGsonBuilder.create().toJson(this);
+ }
+
+ /**
+ * 应用模板信息
+ */
+ @Data
+ public static class Template implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 模板id
+ */
+ @SerializedName("template_id")
+ private String templateId;
+
+ /**
+ * 模板类型
+ */
+ @SerializedName("template_type")
+ private Integer templateType;
+
+ /**
+ * 应用名称
+ */
+ @SerializedName("app_name")
+ private String appName;
+
+ /**
+ * 应用logo url
+ */
+ @SerializedName("logo_url")
+ private String logoUrl;
+
+ /**
+ * 应用简介
+ */
+ @SerializedName("app_desc")
+ private String appDesc;
+
+ /**
+ * 应用状态
+ */
+ @SerializedName("status")
+ private Integer status;
+ }
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/corpgroup/WxCpCorpGroupCorpGetTokenReq.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/corpgroup/WxCpCorpGroupCorpGetTokenReq.java
index 6370fc7c11..7fa00a3ada 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/corpgroup/WxCpCorpGroupCorpGetTokenReq.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/corpgroup/WxCpCorpGroupCorpGetTokenReq.java
@@ -17,5 +17,5 @@ public class WxCpCorpGroupCorpGetTokenReq implements Serializable {
@SerializedName("business_type")
private int businessType;
@SerializedName("agentid")
- private int agentId;
+ private long agentId;
}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/WxCpMsgTemplate.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/WxCpMsgTemplate.java
index 6c546daa83..3c2d9fb977 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/WxCpMsgTemplate.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/WxCpMsgTemplate.java
@@ -6,6 +6,7 @@
import lombok.Data;
import lombok.NoArgsConstructor;
import me.chanjar.weixin.cp.bean.external.msg.Attachment;
+import me.chanjar.weixin.cp.bean.external.msg.TagFilter;
import me.chanjar.weixin.cp.bean.external.msg.Text;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
@@ -14,10 +15,9 @@
/**
* 企业群发消息任务
- *
- * Created by songfan on 2020/7/14.
*
- * @author songfan & Mr.Pan
+ * @author songfan, Mr.Pan
+ * @since 2020/7/14
*/
@Data
@Builder
@@ -44,6 +44,12 @@ public class WxCpMsgTemplate implements Serializable {
@SerializedName("chat_id_list")
private List chatIdList;
+ /**
+ * 要进行群发的客户标签列表,同组标签之间按或关系进行筛选,不同组标签按且关系筛选,每组最多指定100个标签,支持规则组标签
+ */
+ @SerializedName("tag_filter")
+ private TagFilter tagFilter;
+
/**
* 发送企业群发消息的成员userid,当类型为发送给客户群时必填
*/
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/WxCpUserExternalUnassignList.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/WxCpUserExternalUnassignList.java
index 8605760fa7..f3fdd96ce7 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/WxCpUserExternalUnassignList.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/WxCpUserExternalUnassignList.java
@@ -12,7 +12,8 @@
/**
* 离职员工外部联系人列表
*
- * @author yqx & Wang_Wong created on 2020/3/15
+ * @author yqx, Wang_Wong
+ * @since 2020/3/15
*/
@Getter
@Setter
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/acquisition/WxCpCustomerAcquisitionStatistic.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/acquisition/WxCpCustomerAcquisitionStatistic.java
index bb02b039bd..87e3d5580a 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/acquisition/WxCpCustomerAcquisitionStatistic.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/acquisition/WxCpCustomerAcquisitionStatistic.java
@@ -10,7 +10,7 @@
* 获客链接的使用详情
*
* @author Hugo
- * @date 2023/12/11 10:31
+ * @since 2023/12/11 10:31
*/
@Data
@EqualsAndHashCode(callSuper = true)
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/interceptrule/WxCpInterceptRuleInfo.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/interceptrule/WxCpInterceptRuleInfo.java
index 20d6b32442..23bb70a240 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/interceptrule/WxCpInterceptRuleInfo.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/interceptrule/WxCpInterceptRuleInfo.java
@@ -10,9 +10,10 @@
import java.util.List;
/**
- * @Date: 2024-03-07 17:02
- * @Author: shenliuming
- * @return:
+ * 防骚扰规则详情
+ *
+ * @author shenliuming
+ * @since 2024-03-07 17:02
*/
@Data
@EqualsAndHashCode(callSuper = true)
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/interceptrule/WxCpInterceptRuleList.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/interceptrule/WxCpInterceptRuleList.java
index 6826413e13..543d32fcb9 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/interceptrule/WxCpInterceptRuleList.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/interceptrule/WxCpInterceptRuleList.java
@@ -9,9 +9,10 @@
import java.util.List;
/**
- * @Date: 2024-03-07 15:54
- * @Author: shenliuming
- * @return:
+ * 防骚扰规则列表
+ *
+ * @author shenliuming
+ * @since 2024-03-07 15:54
*/
@Data
@EqualsAndHashCode(callSuper = true)
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/msg/Attachment.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/msg/Attachment.java
index be9dcc9dd0..1fff457f97 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/msg/Attachment.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/msg/Attachment.java
@@ -33,6 +33,7 @@ public class Attachment implements Serializable {
* Sets image.
*
* @param image the image
+ * @return this
*/
public Attachment setImage(Image image) {
this.image = image;
@@ -44,6 +45,7 @@ public Attachment setImage(Image image) {
* Sets link.
*
* @param link the link
+ * @return this
*/
public Attachment setLink(Link link) {
this.link = link;
@@ -55,6 +57,7 @@ public Attachment setLink(Link link) {
* Sets mini program.
*
* @param miniProgram the mini program
+ * @return this
*/
public Attachment setMiniProgram(MiniProgram miniProgram) {
this.miniProgram = miniProgram;
@@ -66,6 +69,7 @@ public Attachment setMiniProgram(MiniProgram miniProgram) {
* Sets video.
*
* @param video the video
+ * @return this
*/
public Attachment setVideo(Video video) {
this.video = video;
@@ -77,6 +81,7 @@ public Attachment setVideo(Video video) {
* Sets file.
*
* @param file the file
+ * @return this
*/
public Attachment setFile(File file) {
this.file = file;
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/msg/TagFilter.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/msg/TagFilter.java
new file mode 100644
index 0000000000..da7851adbf
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/msg/TagFilter.java
@@ -0,0 +1,20 @@
+package me.chanjar.weixin.cp.bean.external.msg;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 群发的客户标签
+ *
+ * @author Winnie
+ */
+@Data
+public class TagFilter implements Serializable {
+ private static final long serialVersionUID = -6756444546744020234L;
+
+ @SerializedName("group_list")
+ private List groupList;
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/msg/TagList.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/msg/TagList.java
new file mode 100644
index 0000000000..10ed191e90
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/external/msg/TagList.java
@@ -0,0 +1,20 @@
+package me.chanjar.weixin.cp.bean.external.msg;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 客户标签列表
+ *
+ * @author Winnie
+ */
+@Data
+public class TagList implements Serializable {
+ private static final long serialVersionUID = 1133054307780310675L;
+
+ @SerializedName("tag_list")
+ private List tagList;
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldData.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldData.java
new file mode 100644
index 0000000000..bb4d3a60ae
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldData.java
@@ -0,0 +1,136 @@
+package me.chanjar.weixin.cp.bean.hr;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+
+/**
+ * 人事助手-员工档案数据(单个字段).
+ *
+ * @author leejoker created on 2024-01-01
+ */
+@Data
+@NoArgsConstructor
+public class WxCpHrEmployeeFieldData implements Serializable {
+ private static final long serialVersionUID = 4593693598671765396L;
+
+ /**
+ * 字段ID.
+ */
+ @SerializedName("fieldid")
+ private Integer fieldId;
+
+ /**
+ * 子字段索引.
+ */
+ @SerializedName("sub_idx")
+ private Integer subIdx;
+
+ /**
+ * 结果状态,1表示成功.
+ */
+ @SerializedName("result")
+ private Integer result;
+
+ /**
+ * 值类型:1-字符串,2-uint64,3-uint32,4-int64,5-mobile.
+ */
+ @SerializedName("value_type")
+ private Integer valueType;
+
+ /**
+ * 字符串值(value_type=1时使用).
+ */
+ @SerializedName("value_string")
+ private String valueString;
+
+ /**
+ * 无符号32位整数值(value_type=3时使用).
+ */
+ @SerializedName("value_uint32")
+ private Long valueUint32;
+
+ /**
+ * 有符号64位整数值(value_type=4时使用).
+ */
+ @SerializedName("value_int64")
+ private Long valueInt64;
+
+ /**
+ * 无符号64位整数值(value_type=2时使用).
+ */
+ @SerializedName("value_uint64")
+ private Long valueUint64;
+
+ /**
+ * 手机号值(value_type=5时使用).
+ */
+ @SerializedName("value_mobile")
+ private MobileValue valueMobile;
+
+ /**
+ * 手机号值.
+ */
+ @Data
+ @NoArgsConstructor
+ public static class MobileValue implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 国家代码.
+ */
+ @SerializedName("value_country_code")
+ private String valueCountryCode;
+
+ /**
+ * 手机号.
+ */
+ @SerializedName("value_mobile")
+ private String valueMobile;
+ }
+
+ /**
+ * 员工userid(兼容旧版本,实际API不返回此字段).
+ * @deprecated 此字段在API响应中不存在
+ */
+ @Deprecated
+ @SerializedName("userid")
+ private String userid;
+
+ /**
+ * 字段数据列表(兼容旧版本,实际API不返回此字段).
+ * @deprecated 此字段在API响应中不存在
+ */
+ @Deprecated
+ @SerializedName("field_list")
+ private java.util.List fieldList;
+
+ /**
+ * 字段数据项(用于更新员工档案).
+ */
+ @Data
+ @NoArgsConstructor
+ public static class FieldItem implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 字段ID.
+ */
+ @SerializedName("fieldid")
+ private Integer fieldId;
+
+ /**
+ * 字段值对象(推荐使用,支持多种类型).
+ */
+ @SerializedName("field_value")
+ private WxCpHrEmployeeFieldValue fieldValue;
+
+ /**
+ * 字符串值(简化用法,适用于文本类型字段).
+ */
+ @SerializedName("value_string")
+ private String valueString;
+ }
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldDataResp.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldDataResp.java
new file mode 100644
index 0000000000..f8e763c293
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldDataResp.java
@@ -0,0 +1,56 @@
+package me.chanjar.weixin.cp.bean.hr;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.NoArgsConstructor;
+import me.chanjar.weixin.cp.bean.WxCpBaseResp;
+import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
+
+import java.util.List;
+
+/**
+ * 人事助手-获取员工档案数据响应.
+ *
+ * @author leejoker created on 2024-01-01
+ */
+@Data
+@NoArgsConstructor
+@EqualsAndHashCode(callSuper = true)
+public class WxCpHrEmployeeFieldDataResp extends WxCpBaseResp {
+ private static final long serialVersionUID = 6593693598671765396L;
+
+ /**
+ * 字段数据列表(API实际返回field_info).
+ */
+ @SerializedName("field_info")
+ private List fieldInfoList;
+
+ /**
+ * 员工档案数据列表(兼容旧版本方法名).
+ * @deprecated 请使用 getFieldInfoList()
+ */
+ @Deprecated
+ public List getEmployeeFieldList() {
+ return this.fieldInfoList;
+ }
+
+ /**
+ * 员工档案数据列表(兼容旧版本方法名).
+ * @deprecated 请使用 setFieldInfoList()
+ */
+ @Deprecated
+ public void setEmployeeFieldList(List employeeFieldList) {
+ this.fieldInfoList = employeeFieldList;
+ }
+
+ /**
+ * From json wx cp hr employee field data resp.
+ *
+ * @param json the json
+ * @return the wx cp hr employee field data resp
+ */
+ public static WxCpHrEmployeeFieldDataResp fromJson(String json) {
+ return WxCpGsonBuilder.create().fromJson(json, WxCpHrEmployeeFieldDataResp.class);
+ }
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldInfo.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldInfo.java
new file mode 100644
index 0000000000..3db3f2057d
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/hr/WxCpHrEmployeeFieldInfo.java
@@ -0,0 +1,175 @@
+package me.chanjar.weixin.cp.bean.hr;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 人事助手-员工档案字段信息.
+ *
+ * @author leejoker created on 2024-01-01
+ */
+@Data
+@NoArgsConstructor
+public class WxCpHrEmployeeFieldInfo implements Serializable {
+ private static final long serialVersionUID = 2593693598671765396L;
+
+ /**
+ * 字段ID.
+ */
+ @SerializedName("fieldid")
+ private Integer fieldId;
+
+ /**
+ * 字段名称.
+ */
+ @SerializedName("field_name")
+ private String fieldName;
+
+ /**
+ * 字段类型.
+ * 1: 文本
+ * 2: 单选/多选
+ * 3: 日期
+ */
+ @SerializedName("field_type")
+ private Integer fieldType;
+
+ /**
+ * 是否必填.
+ */
+ @SerializedName("is_must")
+ private Boolean isMust;
+
+ /**
+ * 值类型.
+ * 1: 字符串
+ * 2: uint64
+ * 3: uint32
+ * 4: int64
+ * 5: mobile
+ */
+ @SerializedName("value_type")
+ private Integer valueType;
+
+ /**
+ * 获取字段类型枚举.
+ *
+ * @return 字段类型枚举,未匹配时返回 null
+ */
+ public WxCpHrFieldType getFieldTypeEnum() {
+ return fieldType == null ? null : WxCpHrFieldType.fromCode(fieldType);
+ }
+
+ /**
+ * 选项列表(单选/多选字段专用).
+ */
+ @SerializedName("option_list")
+ private List
* 注意:
*
+ * 发送应用消息(代授权企业发送).
+ * 详情请见: https://work.weixin.qq.com/api/doc/90000/90135/90236
+ *
+ *
+ * @param message 要发送的消息对象
+ * @param corpId 授权企业的 corpId
+ * @return 消息发送结果
+ * @throws WxErrorException 微信错误异常
+ */
+ WxCpMessageSendResult send(WxCpMessage message, String corpId) throws WxErrorException;
+
+ /**
+ *
+ * 查询应用消息发送统计.
+ * 请求方式:POST(HTTPS)
+ * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/message/get_statistics?access_token=ACCESS_TOKEN
+ * 详情请见: https://work.weixin.qq.com/api/doc/90000/90135/92369
+ *
+ *
+ * @param timeType 查询哪天的数据,0:当天;1:昨天。默认为0。
+ * @param corpId 授权企业的 corpId
+ * @return 统计结果
+ * @throws WxErrorException 微信错误异常
+ */
+ WxCpMessageSendStatistics getStatistics(int timeType, String corpId) throws WxErrorException;
+
+ /**
+ *
+ * 互联企业发送应用消息.
+ * 请求地址:https://qyapi.weixin.qq.com/cgi-bin/linkedcorp/message/send?access_token=ACCESS_TOKEN
+ * 文章地址:https://work.weixin.qq.com/api/doc/90000/90135/90250
+ *
+ *
+ * @param message 要发送的消息对象
+ * @param corpId 授权企业的 corpId
+ * @return 消息发送结果
+ * @throws WxErrorException 微信错误异常
+ */
+ WxCpLinkedCorpMessageSendResult sendLinkedCorpMessage(WxCpLinkedCorpMessage message, String corpId) 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 要发送的消息对象
+ * @param corpId 授权企业的 corpId
+ * @return 消息发送结果
+ * @throws WxErrorException 微信错误异常
+ */
+ WxCpSchoolContactMessageSendResult sendSchoolContactMessage(WxCpSchoolContactMessage message, String corpId) 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
+ * @param corpId 授权企业的 corpId
+ * @throws WxErrorException 微信错误异常
+ */
+ void recall(String msgId, String corpId) throws WxErrorException;
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpOAService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpOAService.java
index 85321213ab..4aed96b7b5 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpOAService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpOAService.java
@@ -51,8 +51,7 @@ public interface WxCpTpOAService {
String copyTemplate(@NonNull String openTemplateId, String corpId) throws WxErrorException;
/**
- *
- * 获取审批申请详情
+ * 获取审批申请详情
*
* @param spNo 审批单编号。
* @param corpId the corp id
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpOrderService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpOrderService.java
index 3aff90bb56..6e0acb7dee 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpOrderService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpOrderService.java
@@ -18,7 +18,7 @@ public interface WxCpTpOrderService {
* 获取订单详情
*
* 文档地址
- *
+ *
*
* @param orderId 订单号
* @return the order
@@ -31,7 +31,7 @@ public interface WxCpTpOrderService {
* 获取订单列表
*
* 文档地址
- *
+ *
*
* @param startTime 起始时间
* @param endTime 终止时间
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java
index b24be535da..5189c5d821 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpService.java
@@ -199,7 +199,7 @@ public interface WxCpTpService {
* @return permanent code info
* @throws WxErrorException the wx error exception
* @author yuan
- * @since 2020 -03-18
+ * @since 2020-03-18
*/
WxCpTpPermanentCodeInfo getPermanentCodeInfo(String authCode) throws WxErrorException;
@@ -227,7 +227,7 @@ public interface WxCpTpService {
* @param authType 授权类型:0 正式授权, 1 测试授权。
* @return pre auth url
* @throws WxErrorException the wx error exception
- * @link https ://work.weixin.qq.com/api/doc/90001/90143/90602
+ * @see 文档地址
*/
String getPreAuthUrl(String redirectUri, String state, int authType) throws WxErrorException;
@@ -530,6 +530,24 @@ public interface WxCpTpService {
*/
WxCpTpLicenseService getWxCpTpLicenseService();
+ /**
+ * get message service
+ *
+ * @return WxCpTpMessageService wx cp tp message service
+ */
+ default WxCpTpMessageService getWxCpTpMessageService() {
+ throw new UnsupportedOperationException("WxCpTpMessageService is not supported");
+ }
+
+ /**
+ * set message service
+ *
+ * @param wxCpTpMessageService the message service
+ */
+ default void setWxCpTpMessageService(WxCpTpMessageService wxCpTpMessageService) {
+ throw new UnsupportedOperationException("WxCpTpMessageService is not supported");
+ }
+
WxCpTpXmlMessage fromEncryptedXml(String encryptedXml,
String timestamp, String nonce, String msgSignature);
@@ -558,12 +576,12 @@ WxCpTpXmlMessage fromEncryptedXml(String encryptedXml,
WxCpTpAppQrcode getAppQrcode(String suiteId, String appId, String state, Integer style, Integer resultType) throws WxErrorException ;
/**
- *
* 明文corpid转换为加密corpid 为更好地保护企业与用户的数据,第三方应用获取的corpid不再是明文的corpid,将升级为第三方服务商级别的加密corpid。文档说明
* 第三方可以将已有的明文corpid转换为第三方的加密corpid。
- * @param corpId
- * @return
- * @throws WxErrorException
+ *
+ * @param corpId 企业ID
+ * @return 加密的企业ID
+ * @throws WxErrorException 微信错误异常
*/
WxCpTpCorpId2OpenCorpId corpId2OpenCorpId(String corpId) throws WxErrorException;
@@ -655,9 +673,25 @@ WxCpTpXmlMessage fromEncryptedXml(String encryptedXml,
/**
* 构造第三方应用oauth2链接
+ *
+ * @return OAuth2服务
*/
WxCpTpOAuth2Service getWxCpTpOAuth2Service();
void setWxCpTpOAuth2Service(WxCpTpOAuth2Service wxCpTpOAuth2Service);
+ /**
+ * 获取代开发服务
+ *
+ * @return 代开发服务
+ */
+ WxCpTpCustomizedService getWxCpTpCustomizedService();
+
+ /**
+ * 设置代开发服务
+ *
+ * @param wxCpTpCustomizedService 代开发服务
+ */
+ void setWxCpTpCustomizedService(WxCpTpCustomizedService wxCpTpCustomizedService);
+
}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpTagService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpTagService.java
index b508df59a1..df60041865 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpTagService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpTagService.java
@@ -13,7 +13,7 @@
*
*
* @author zhangq
- * @since 2021 -02-14 16:02
+ * @since 2021-02-14 16:02
*/
public interface WxCpTpTagService {
/**
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpUserService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpUserService.java
index 0f2fc5dd99..1359fa66e0 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpUserService.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/WxCpTpUserService.java
@@ -53,9 +53,29 @@ public interface WxCpTpUserService {
* @param departId 必填。部门id
* @param fetchChild 非必填。1/0:是否递归获取子部门下面的成员
* @param status 非必填。0获取全部员工,1获取已关注成员列表,2获取禁用成员列表,4获取未关注成员列表。status可叠加
+ * @param corpId 企业id
* @return the list
* @throws WxErrorException the wx error exception
*/
+ List listSimpleByDepartment(Long departId, Boolean fetchChild, Integer status, String corpId)
+ throws WxErrorException;
+
+ /**
+ *
+ * 获取部门成员.
+ *
+ * http://qydev.weixin.qq.com/wiki/index.php?title=管理成员#.E8.8E.B7.E5.8F.96.E9.83.A8.E9.97.A8.E6.88.90.E5.91.98
+ *
+ *
+ * @param departId 必填。部门id
+ * @param fetchChild 非必填。1/0:是否递归获取子部门下面的成员
+ * @param status 非必填。0获取全部员工,1获取已关注成员列表,2获取禁用成员列表,4获取未关注成员列表。status可叠加
+ * @return the list
+ * @throws WxErrorException the wx error exception
+ * @deprecated 第三方应用调用此接口需要使用 corpId 对应的 access_token,请使用
+ * {@link #listSimpleByDepartment(Long, Boolean, Integer, String)}
+ */
+ @Deprecated
List listSimpleByDepartment(Long departId, Boolean fetchChild, Integer status) throws WxErrorException;
/**
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java
index f8f554b81a..d8ed7c0e47 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/BaseWxCpTpServiceImpl.java
@@ -60,6 +60,8 @@ public abstract class BaseWxCpTpServiceImpl implements WxCpTpService, Requ
private WxCpTpLicenseService wxCpTpLicenseService = new WxCpTpLicenseServiceImpl(this);
private WxCpTpIdConvertService wxCpTpIdConvertService = new WxCpTpIdConvertServiceImpl(this);
private WxCpTpOAuth2Service wxCpTpOAuth2Service = new WxCpTpOAuth2ServiceImpl(this);
+ private WxCpTpCustomizedService wxCpTpCustomizedService = new WxCpTpCustomizedServiceImpl(this);
+ private WxCpTpMessageService wxCpTpMessageService = new WxCpTpMessageServiceImpl(this);
/**
* 全局的是否正在刷新access token的锁.
*/
@@ -664,6 +666,16 @@ public void setWxCpTpLicenseService(WxCpTpLicenseService wxCpTpLicenseService) {
this.wxCpTpLicenseService = wxCpTpLicenseService;
}
+ @Override
+ public WxCpTpMessageService getWxCpTpMessageService() {
+ return wxCpTpMessageService;
+ }
+
+ @Override
+ public void setWxCpTpMessageService(WxCpTpMessageService wxCpTpMessageService) {
+ this.wxCpTpMessageService = wxCpTpMessageService;
+ }
+
@Override
public void setWxCpTpUserService(WxCpTpUserService wxCpTpUserService) {
this.wxCpTpUserService = wxCpTpUserService;
@@ -809,6 +821,16 @@ public void setWxCpTpOAuth2Service(WxCpTpOAuth2Service wxCpTpOAuth2Service) {
this.wxCpTpOAuth2Service = wxCpTpOAuth2Service;
}
+ @Override
+ public WxCpTpCustomizedService getWxCpTpCustomizedService() {
+ return wxCpTpCustomizedService;
+ }
+
+ @Override
+ public void setWxCpTpCustomizedService(WxCpTpCustomizedService wxCpTpCustomizedService) {
+ this.wxCpTpCustomizedService = wxCpTpCustomizedService;
+ }
+
@Override
public WxCpTpConfigStorage getWxCpTpConfigStorage() {
return this.configStorage;
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpCustomizedServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpCustomizedServiceImpl.java
new file mode 100644
index 0000000000..54ea0fc4bc
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpCustomizedServiceImpl.java
@@ -0,0 +1,64 @@
+package me.chanjar.weixin.cp.tp.service.impl;
+
+import com.google.gson.JsonObject;
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.bean.WxCpTpCustomizedAppDetail;
+import me.chanjar.weixin.cp.bean.WxCpTpTemplateList;
+import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
+import me.chanjar.weixin.cp.tp.service.WxCpTpCustomizedService;
+import me.chanjar.weixin.cp.tp.service.WxCpTpService;
+
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Tp.GET_CUSTOMIZED_APP_DETAIL;
+import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.Tp.GET_TEMPLATE_LIST;
+
+/**
+ * 企业微信第三方应用 - 代开发相关接口实现.
+ *
+ * @author Binary Wang
+ * created on 2026-01-14
+ */
+@RequiredArgsConstructor
+public class WxCpTpCustomizedServiceImpl implements WxCpTpCustomizedService {
+
+ private final WxCpTpService mainService;
+
+ @Override
+ public WxCpTpTemplateList getTemplateList() throws WxErrorException {
+ String responseText = this.mainService.get(getWxCpTpConfigStorage().getApiUrl(GET_TEMPLATE_LIST)
+ + getProviderAccessToken(), null, true);
+ return WxCpTpTemplateList.fromJson(responseText);
+ }
+
+ @Override
+ public WxCpTpCustomizedAppDetail getCustomizedAppDetail(String authCorpId, Integer agentId) throws WxErrorException {
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty("auth_corpid", authCorpId);
+ if (agentId != null) {
+ jsonObject.addProperty("agentid", agentId);
+ }
+ String responseText = this.mainService.post(getWxCpTpConfigStorage().getApiUrl(GET_CUSTOMIZED_APP_DETAIL)
+ + getProviderAccessToken(), jsonObject.toString(), true);
+ return WxCpTpCustomizedAppDetail.fromJson(responseText);
+ }
+
+ /**
+ * 获取provider_access_token参数
+ *
+ * @return provider_access_token参数
+ * @throws WxErrorException 微信错误异常
+ */
+ private String getProviderAccessToken() throws WxErrorException {
+ return "?provider_access_token=" + mainService.getWxCpProviderToken();
+ }
+
+ /**
+ * 获取tp参数配置
+ *
+ * @return config
+ */
+ private WxCpTpConfigStorage getWxCpTpConfigStorage() {
+ return mainService.getWxCpTpConfigStorage();
+ }
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpEditionServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpEditionServiceImpl.java
index 34ca852c3f..b77fb924c2 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpEditionServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/tp/service/impl/WxCpTpEditionServiceImpl.java
@@ -26,7 +26,7 @@ public class WxCpTpEditionServiceImpl implements WxCpTpEditionService {
* 延长试用期
*
* 文档地址
- *
+ *
*