49 Commits

Author SHA1 Message Date
Binary Wang
fd3bbde725 🔖 发布 4.7.9.B 测试版本
Some checks failed
Publish to Maven Central / build-and-publish (push) Has been cancelled
2025-11-28 22:37:15 +08:00
Binary Wang
84e70febdf update agent description
更新代理名称和描述为中文,并强调提交信息也需使用中文。
2025-11-28 21:54:48 +08:00
Copilot
155f3f778f 🎨 #3570 【小程序】添加微信云托管支持 2025-11-28 21:38:57 +08:00
Copilot
26f5887e22 📝 为现有的企业微信流程审批功能添加完整的使用文档
Some checks failed
Publish to Maven Central / build-and-publish (push) Has been cancelled
2025-11-28 11:52:33 +08:00
Copilot
23a2e18f1c 🆕 #3682 【企业微信】新增智能机器人的接口支持 2025-11-28 11:50:50 +08:00
Copilot
8f20f3baba 🆕 #3678 【小程序】新增客服管理相关接口 2025-11-28 11:49:15 +08:00
Copilot
93a2fa701a 🆕 #3519 【小程序】实现交易投诉的相关接口 2025-11-28 11:46:10 +08:00
Copilot
1f4ed68750 🎨 #3750 【微信支付】修复 V2 支付回调签名验证失败的问题 2025-11-28 11:29:22 +08:00
Copilot
c4f383462e 🆕 #3737 【微信支付】增加V3服务商申请退款的接口 2025-11-28 11:26:46 +08:00
Copilot
03114b9900 🎨 #3753 【微信支付】添加商家转账用户授权免确认模式相关接口 2025-11-28 11:23:05 +08:00
Copilot
f6d9e8ba4d 🎨 #3738 【微信支付】修复V3新商户完全公钥模式下publicKey为空的问题 2025-11-28 11:20:51 +08:00
Copilot
1da6cf56ff 🎨 #3755 【企业微信】修复会话存档SDK重复初始化导致接口超限问题 2025-11-28 11:19:38 +08:00
Copilot
e655a33956 🎨 #3752 修复 Gson 在 Java 9+ 环境下反射访问 java.io.File#path 字段失败的问题 2025-11-28 11:17:33 +08:00
Copilot
9940546956 🐛 #3675 【小程序】修复退货组件 API 接口请求方法错误的问题 2025-11-28 11:15:41 +08:00
chu
1bd50d4d2f 🆕 #3770 【视频号】新增售后单商家协商、换货发货与拒绝发货等接口 2025-11-28 10:08:55 +08:00
Copilot
8e46da5003 🎨 #3683 【开放平台】修改component_verify_ticket 的有效时间为12小时 2025-11-27 23:20:04 +08:00
Dell Well
b1e7cd2cd7 🐛 #3664: 【微信支付】修复微信支付分订单接口提示“商户暂无权限使用此服务” 的问题 2025-11-27 23:16:49 +08:00
hyf1844
7fcd1ed5b3 🎨 #3767 【视频号】微信小店订单详情接口新增会员权益等优惠金额字段 2025-11-27 23:14:16 +08:00
Copilot
21d5f2fdd2 🆕 #3764 【开放平台】添加小程序类目管理 - 获取类目名称信息的接口 2025-11-27 23:06:19 +08:00
Copilot
e57b7e1eb5 🎨 #3765 【微信支付】修复从 base64 字符串加载私钥时的双重解码问题 2025-11-27 23:04:11 +08:00
Copilot
833f60c68d 🆕 #3736 【小程序】增加解密群入口敏感数据的方法 2025-11-17 12:01:15 +08:00
Copilot
1922aaaa94 🎨 #3751 【企业微信】提交审批申请接口增加process参数以支持新版的审批流程信息 2025-11-17 11:59:51 +08:00
Copilot
3c440eacb3 🎨 修复 GsonParser 与低版本 Gson 的兼容性问题 2025-11-17 11:11:51 +08:00
Binary Wang
b8d2bb345a 📝 添加自定义agent的文档 2025-11-17 11:00:07 +08:00
Binary Wang
730ac24c3f 🎨 优化小程序配置类的javadoc 2025-11-17 10:47:29 +08:00
Copilot
ebb9ccdebe 🎨 #3376 【小程序】完善事件消息推送对json数据格式的支持 2025-11-17 10:33:09 +08:00
Copilot
76c7e994a5 🐛 #3700 修复空指针问题
Some checks failed
Publish to Maven Central / build-and-publish (push) Has been cancelled
2025-11-15 17:14:45 +08:00
Copilot
092e992817 🆕 #3688 【微信支付】 实现预约扣费服务的相关接口 2025-11-15 17:12:21 +08:00
Copilot
69a2ab9cc7 🆕 #3720 【微信支付】实现微信押金支付的相关功能接口 2025-11-15 17:09:29 +08:00
Copilot
0854e4d971 🆕 #3507 【微信支付】实现运营工具-商家转账API相关接口 2025-11-15 17:04:27 +08:00
Copilot
57e12dee29 🐛 #3704 【小程序】修复物流服务-同城配送服务-查询门店余额接口错误的逻辑 2025-11-15 17:03:08 +08:00
Copilot
4d4294707b 🎨 #3728 【微信支付】修复V3支付公钥转账出现的签名验证失败问题 2025-11-15 16:52:49 +08:00
Copilot
9ab64167d5 🎨 #3680 【微信支付】修复PEM格式的私钥和证书处理的问题 2025-11-15 16:47:06 +08:00
helloJetBase-tech
28fac4ec2e 🎨 #3756 【企业微信】修复企业微信 API 回调验签过程中 WxCryptUtil.decrypt 方法可能抛出异常的问题 2025-11-12 13:44:18 +08:00
kongkong
b94106fd57 🎨 #3757 【微信支付】修复当只设置 privateCertString 或 PrivateCertContent 时 certSerialNo 没有被正确生成的问题 2025-11-12 13:41:52 +08:00
Copilot
d4186c49c1 🆕 #3494 【微信支付】平台收付通提现接口新增回调参数支持及补充日终余额提现API 2025-11-05 10:45:50 +08:00
Copilot
bb573afcaf 🎨 #3732 Add Quarkus/GraalVM Native Image support - Fix Random instance initialization issues 2025-11-02 16:30:13 +08:00
troubleMTT
e9bc5d0109 🎨 #3746 【企业微信】获取企业已配置的「联系我」列表接口返回值增加next_cursor字段
Some checks failed
Publish to Maven Central / build-and-publish (push) Has been cancelled
2025-10-30 19:43:35 +08:00
Copilot
738bcf7755 🆕 #3739 【开放平台】补充完善第三方平台小程序快速配置域名的相关接口 2025-10-28 11:59:56 +08:00
Binary Wang
de4848b2f7 🎨 规范mp模块的javadoc接口注释文档
Some checks failed
Publish to Maven Central / build-and-publish (push) Has been cancelled
2025-10-28 11:55:15 +08:00
accept mediocrity
b26c786797 🎨 #3745 【微信支付】修复请求微信仿真测试系统时验签密钥接口的 Content-Type 问题 2025-10-28 11:51:26 +08:00
Binary Wang
b9b4f00438 📝 Revise development instructions with Chinese translations
Some checks failed
Publish to Maven Central / build-and-publish (push) Has been cancelled
2025-10-23 11:00:01 +08:00
小付同学
98329b11da 🐛 #3640 【微信支付】修复使用连接池后SSL客户端证书未正确发送导致退款接口报错的问题 2025-10-23 10:12:23 +08:00
Copilot
db85c0ab8a 🎨 完善关于开放平台功能的说明文档 2025-10-04 01:47:50 +08:00
Copilot
ca567ce310 🆕 #3618 【微信支付】增加境外微信支付的支持 2025-10-04 01:44:56 +08:00
Copilot
a6825a62bb 🎨 #3628【企业微信】修复更新用户接口重置用户直属领导字段的问题 2025-10-04 01:39:35 +08:00
Mr.Robot
9fd12b2a09 🎨 WxOpenMessageRouter增加注解ConditionalOnMissingBean 2025-10-04 01:32:23 +08:00
xiaoyun461
10f71234e4 🆕 #3725【企业微信】 增加markdown_v2的消息类型支持 2025-10-04 01:31:24 +08:00
Copilot
9aa2781e68 🎨 #3634 【微信支付】尝试修复服务商模式分账动账通知非法请求、头部信息验证失败的问题 2025-09-27 17:11:32 +08:00
245 changed files with 13192 additions and 1120 deletions

13
.github/agents/my-agent.agent.md vendored Normal file
View File

@@ -0,0 +1,13 @@
---
# Fill in the fields below to create a basic custom agent for your repository.
# The Copilot CLI can be used for local testing: https://gh.io/customagents/cli
# To make this agent available, merge this file into the default repository branch.
# For format details, see: https://gh.io/customagents/config
name: 全部用中文
description: 需要用中文包括PR标题和分析总结过程
---
# My Agent
请使用中文输出思考过程和总结包括PR标题提交commit信息也要使用中文

View File

@@ -1,198 +1,202 @@
# WxJava - WeChat Java SDK Development Instructions
# Copilot Instruction
请始终使用中文生成 Pull Request 的标题、描述和提交信息
WxJava is a comprehensive WeChat Java SDK supporting multiple WeChat platforms including Official Accounts (公众号), Mini Programs (小程序), WeChat Pay (微信支付), Enterprise WeChat (企业微信), Open Platform (开放平台), and Channel/Video (视频号). This is a Maven multi-module project with Spring Boot and Solon framework integrations.
**ALWAYS reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the information here.**
# WxJava - 微信 Java SDK 开发说明
## Working Effectively
WxJava 是一个支持多种微信平台的完整 Java SDK包含公众号、小程序、微信支付、企业微信、开放平台、视频号、企点等多种功能模块。
### Prerequisites and Environment Setup
- **Java Requirements**: JDK 8+ required (project uses Java 8 as minimum target)
- **Maven**: Maven 3.6+ recommended (Maven 3.9.11 validated)
- **IDE**: IntelliJ IDEA recommended (project optimized for IDEA)
**请始终优先参考本说明,只有在遇到与此内容不一致的意外信息时,才退而使用搜索或 bash 命令。**
### Bootstrap, Build, and Validate
Execute these commands in sequence after cloning:
## 高效开发指南
### 前置条件与环境准备
- **Java 要求**JDK 8+(项目最低目标为 Java 8
- **Maven**:推荐 Maven 3.6+(已验证 Maven 3.9.11
- **IDE**:推荐使用 IntelliJ IDEA项目针对 IDEA 优化)
### 引导、构建与校验
克隆仓库后按顺序执行以下命令:
```bash
# 1. Basic compilation (NEVER CANCEL - takes 4-5 minutes)
# 1. 基础编译(请勿中断 - 约需 4-5 分钟)
mvn clean compile -DskipTests=true --no-transfer-progress
# Timeout: Set 8+ minutes. Actual time: ~4 minutes
# 超时时间:建议设置 8 分钟以上。实际时间:约 4 分钟
# 2. Full package build (NEVER CANCEL - takes 2-3 minutes)
# 2. 完整打包(请勿中断 - 约需 2-3 分钟)
mvn clean package -DskipTests=true --no-transfer-progress
# Timeout: Set 5+ minutes. Actual time: ~2 minutes
# 超时时间:建议设置 5 分钟以上。实际时间:约 2 分钟
# 3. Code quality validation (NEVER CANCEL - takes 45-60 seconds)
# 3. 代码质量校验(请勿中断 - 约需 45-60 秒)
mvn checkstyle:check --no-transfer-progress
# Timeout: Set 3+ minutes. Actual time: ~50 seconds
# 超时时间:建议设置 3 分钟以上。实际时间:约 50 秒
```
**CRITICAL TIMING NOTES:**
- **NEVER CANCEL** any Maven build command
- Compilation phase takes longest (~4 minutes) due to 34 modules
- Full builds are faster on subsequent runs due to incremental compilation
- Always use `--no-transfer-progress` to reduce log noise
重要时间说明:
- 绝对不要中断任意 Maven 构建命令
- 编译阶段耗时最长(约 4 分钟),原因是项目包含 34 个模块
- 后续构建会更快,因为存在增量编译
- 始终使用 `--no-transfer-progress` 以减少日志噪音
### Testing Structure
- **Test Framework**: TestNG (not JUnit)
- **Test Files**: 298 test files across all modules
- **Default Behavior**: Tests are DISABLED by default in pom.xml (`<skip>true</skip>`)
- **Test Configuration**: Tests require external WeChat API credentials via test-config.xml files
- **DO NOT** attempt to run tests without proper WeChat API credentials as they will fail
### 测试结构
- **测试框架**TestNG(非 JUnit
- **测试文件**:共有 298 个测试文件
- **默认行为**pom.xml 中默认禁用测试(`<skip>true</skip>`
- **测试配置**:测试需要通过 test-config.xml 提供真实的微信 API 凭据
- **注意**:没有真实微信 API 凭据请不要尝试运行测试,测试将会失败
## Project Structure and Navigation
## 项目结构与导航
### Core SDK Modules (Main Development Areas)
- `weixin-java-common/` - Common utilities and base classes (most important)
- `weixin-java-mp/` - WeChat Official Account SDK (公众号)
- `weixin-java-pay/` - WeChat Pay SDK (微信支付)
- `weixin-java-miniapp/` - Mini Program SDK (小程序)
- `weixin-java-cp/` - Enterprise WeChat SDK (企业微信)
- `weixin-java-open/` - Open Platform SDK (开放平台)
- `weixin-java-channel/` - Channel/Video SDK (视频号)
- `weixin-java-qidian/` - Qidian SDK (企点)
### 核心 SDK 模块(主要开发区)
- `weixin-java-common/` - 通用工具与基础类(最重要)
- `weixin-java-mp/` - 公众号 SDK
- `weixin-java-pay/` - 微信支付 SDK
- `weixin-java-miniapp/` - 小程序 SDK
- `weixin-java-cp/` - 企业微信 SDK
- `weixin-java-open/` - 开放平台 SDK
- `weixin-java-channel/` - 视频号 / Channel SDK
- `weixin-java-qidian/` - 企点 SDK
### Framework Integration Modules
- `spring-boot-starters/` - Spring Boot auto-configuration starters
- `solon-plugins/` - Solon framework plugins
- `weixin-graal/` - GraalVM native image support
### 框架集成模块
- `spring-boot-starters/` - Spring Boot 自动配置 starter
- `solon-plugins/` - Solon 框架插件
- `weixin-graal/` - GraalVM 本地镜像支持
### Configuration and Quality
- `quality-checks/google_checks.xml` - Checkstyle configuration
- `.editorconfig` - Code formatting rules (2 spaces = 1 tab)
- `pom.xml` - Root Maven configuration
### 配置与质量控制
- `quality-checks/google_checks.xml` - Checkstyle 配置
- `.editorconfig` - 代码格式规则2 个空格等于 1 个制表)
- `pom.xml` - 根级 Maven 配置
## Development Workflow
## 开发工作流
### Making Code Changes
1. **Always build first** to establish clean baseline:
### 修改代码的流程
1. 修改前务必先构建以建立干净基线:
```bash
mvn clean compile --no-transfer-progress
```
2. **Follow code style** (enforced by checkstyle):
- Use 2 spaces for indentation (not tabs)
- Follow Google Java Style Guide
- Install EditorConfig plugin in your IDE
2. 遵循代码风格(由 checkstyle 强制):
- 缩进使用 2 个空格(不要用制表符)
- 遵循 Google Java 风格指南
- 在 IDE 中安装 EditorConfig 插件
3. **Validate changes incrementally**:
3. 增量验证修改:
```bash
# After each change:
# 每次修改后运行:
mvn compile --no-transfer-progress
mvn checkstyle:check --no-transfer-progress
mvn checkstyle:check --no-transfer-progress
```
### Before Submitting Changes
**ALWAYS run these validation steps in sequence:**
### 提交修改前的必须校验
请务必按顺序完成以下校验步骤:
1. **Code Style Validation**:
1. 代码风格校验:
```bash
mvn checkstyle:check --no-transfer-progress
# Must pass - takes ~50 seconds
# 必须通过 - 约需 50 秒
```
2. **Full Clean Build**:
2. 完整清理构建:
```bash
mvn clean package -DskipTests=true --no-transfer-progress
# Must succeed - takes ~2 minutes
# 必须成功 - 约需 2 分钟
```
3. **Documentation**: Update javadoc for public methods and classes
4. **Contribution Guidelines**: Follow `CONTRIBUTING.md` - PRs must target `develop` branch
3. 文档:为公共方法和类补充或更新 javadoc
4. 贡献规范:遵循 `CONTRIBUTING.md`Pull Request 必须以 `develop` 分支为目标
## Module Dependencies and Build Order
## 模块依赖与构建顺序
### Core Module Dependencies (Build Order)
1. `weixin-graal` (GraalVM support)
2. `weixin-java-common` (foundation for all other modules)
3. Core SDK modules (mp, pay, miniapp, cp, open, channel, qidian)
4. Framework integrations (spring-boot-starters, solon-plugins)
### 核心模块依赖(构建顺序)
1. `weixin-graal`GraalVM 支持)
2. `weixin-java-common`(所有模块的基础)
3. 核心 SDK 模块mp、payminiapp、cp、openchannelqidian
4. 框架集成(spring-boot-starterssolon-plugins
### Key Relationship Patterns
- All SDK modules depend on `weixin-java-common`
- Spring Boot starters depend on corresponding SDK modules
- Solon plugins follow same pattern as Spring Boot starters
- Each module has both single and multi-account configurations
### 主要关系模式
- 所有 SDK 模块都依赖于 `weixin-java-common`
- Spring Boot starters 依赖对应的 SDK 模块
- Solon 插件遵循与 Spring Boot starters 相同的依赖模式
- 每个模块都有单账号与多账号配置支持
## Common Tasks and Commands
## 常见任务与命令
### Validate Specific Module
### 验证指定模块
```bash
# Build single module (replace 'weixin-java-mp' with target module):
# 构建单个模块(将 'weixin-java-mp' 替换为目标模块):
cd weixin-java-mp
mvn clean compile --no-transfer-progress
```
### Check Dependencies
### 检查依赖
```bash
# Analyze dependencies:
# 分析依赖树:
mvn dependency:tree --no-transfer-progress
# Check for dependency updates:
# 检查依赖更新:
./others/check-dependency-updates.sh
```
### Release and Publishing
### 发布与发布准备
```bash
# Version check:
# 版本检查:
mvn versions:display-property-updates --no-transfer-progress
# Deploy (requires credentials):
# 部署(需要凭据):
mvn clean deploy -P release --no-transfer-progress
```
## Important Files and Locations
## 重要文件与位置
### Configuration Files
- `pom.xml` - Root Maven configuration with dependency management
- `quality-checks/google_checks.xml` - Checkstyle rules
- `.editorconfig` - IDE formatting configuration
- `.github/workflows/maven-publish.yml` - CI/CD pipeline
### 配置文件
- `pom.xml` - 根级 Maven 配置与依赖管理
- `quality-checks/google_checks.xml` - Checkstyle 规则
- `.editorconfig` - IDE 格式化配置
- `.github/workflows/maven-publish.yml` - CI/CD 工作流
### Documentation
- `README.md` - Project overview and usage (Chinese)
- `CONTRIBUTING.md` - Development contribution guidelines
- `demo.md` - Links to demo projects and examples
- Each module has dedicated documentation and examples
### 文档
- `README.md` - 项目概览与使用说明(中文)
- `CONTRIBUTING.md` - 贡献指南
- `demo.md` - 示例项目与演示链接
- 每个模块均有单独的文档与示例
### Test Resources
- `*/src/test/resources/test-config.sample.xml` - Template for test configuration
- Tests require real WeChat API credentials to run
### 测试资源
- `*/src/test/resources/test-config.sample.xml` - 测试配置模板
- 测试运行需要真实的微信 API 凭据
## SDK Usage Patterns
## SDK 使用模式
### Maven Dependency Usage
### Maven 依赖示例
```xml
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId> <!-- or other modules -->
<artifactId>weixin-java-mp</artifactId> <!-- 或其他模块 -->
<version>4.7.0</version>
</dependency>
```
### Common Development Areas
- **API Client Implementation**: Located in `*/service/impl/` directories
- **Model Classes**: Located in `*/bean/` directories
- **Configuration**: Located in `*/config/` directories
- **Utilities**: Located in `*/util/` directories in weixin-java-common
### 常见开发区域
- **API 客户端实现**:位于 `*/service/impl/` 目录
- **模型类**:位于 `*/bean/` 目录
- **配置**:位于 `*/config/` 目录
- **工具类**:位于 `weixin-java-common` 的 `*/util/` 目录
## Troubleshooting
## 故障排查
### Build Issues
- **OutOfMemoryError**: Increase Maven memory: `export MAVEN_OPTS="-Xmx2g"`
- **Compilation Failures**: Usually dependency issues - run `mvn clean` first
- **Checkstyle Failures**: Check `.editorconfig` settings in IDE
### 构建问题
- **OutOfMemoryError**:增加 Maven 内存:`export MAVEN_OPTS="-Xmx2g"`
- **编译失败**:通常为依赖问题 - 先执行 `mvn clean`
- **Checkstyle 失败**:检查 IDE 的 `.editorconfig` 设置
### Common Gotchas
- **Tests Always Skip**: This is normal - tests require WeChat API credentials
- **Multi-Module Changes**: Always build from root, not individual modules
- **Branch Target**: PRs must target `develop` branch, not `master`/`release`
### 常见陷阱
- **测试默认跳过**:这是正常现象 — 测试需要微信 API 凭据
- **多模块变更**:总是在仓库根目录构建,而不是单独模块
- **分支目标**Pull Request 必须以 `develop` 分支为目标,而不是 `master``release`
## Performance Notes
- **First Build**: Takes 4-5 minutes due to dependency downloads
- **Incremental Builds**: Much faster (~30-60 seconds)
- **Checkstyle**: Runs quickly (~50 seconds) and should be run frequently
- **IDE Performance**: Project uses Lombok - ensure annotation processing is enabled
## 性能说明
- **首次构建**:由于依赖下载,耗时 4-5 分钟
- **增量构建**:通常更快(约 30-60 秒)
- **Checkstyle**:运行迅速(约 50 秒),应当经常运行
- **IDE 性能**:项目使用 Lombok请确保启用注解处理
Remember: This is a SDK library project, not a runnable application. Changes should focus on API functionality, not application behavior.
注意:本项目为 SDK 库项目,而非可运行应用。修改应以 API 功能为主,不要改动应用级行为。

View File

@@ -65,13 +65,13 @@
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)**
4. 贡献源码可以参考视频:[【贡献源码全过程(上集)】](https://mp.weixin.qq.com/s/3xUZSATWwHR_gZZm207h7Q)、[【贡献源码全过程(下集)】](https://mp.weixin.qq.com/s/nyzJwVVoYSJ4hSbwyvTx9A) ,友情提供:[程序员小山与Bug](https://space.bilibili.com/473631007)
5. 新手重要提示本项目仅是一个SDK开发工具包未提供Web实现建议使用 `maven``gradle` 引用本项目即可使用本SDK提供的各种功能详情可参考 **[【Demo项目】](demo.md)** 或本项目中的部分单元测试代码;
6. 微信开发新手请务必阅读【开发文档】([Gitee Wiki](https://gitee.com/binary/weixin-java-tools/wikis/Home) 或者 [Github Wiki](https://github.com/binarywang/WxJava/wiki))的常见问题部分,可以少走很多弯路,节省不少时间。
7. 技术交流群想获得QQ群/微信群/钉钉企业群等信息的同学,请使用微信扫描上面的微信公众号二维码关注 `WxJava` 后点击相关菜单即可获取加入方式,同时也可以在微信中搜索 `weixin-java-tools``WxJava` 后选择正确的公众号进行关注该公众号会及时通知SDK相关更新信息并不定期分享微信Java开发相关技术知识
8. 钉钉技术交流群:`32206329`技术交流2群, `30294972`技术交流1群目前已满`35724728`通知群实时通知Github项目变更记录
9. 微信开发新手或者Java开发新手在群内提问或新开Issue提问前请先阅读[【提问的智慧】](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md),并确保已查阅过 [【开发文档Wiki】](https://github.com/binarywang/WxJava/wiki) ,避免浪费大家的宝贵时间;
10. 寻求帮助时需贴代码或大长串异常信息的,请利用 http://paste.ubuntu.com
5. 贡献源码可以参考视频:[【贡献源码全过程(上集)】](https://mp.weixin.qq.com/s/3xUZSATWwHR_gZZm207h7Q)、[【贡献源码全过程(下集)】](https://mp.weixin.qq.com/s/nyzJwVVoYSJ4hSbwyvTx9A) ,友情提供:[程序员小山与Bug](https://space.bilibili.com/473631007)
6. 新手重要提示本项目仅是一个SDK开发工具包未提供Web实现建议使用 `maven``gradle` 引用本项目即可使用本SDK提供的各种功能详情可参考 **[【Demo项目】](demo.md)** 或本项目中的部分单元测试代码;
7. 微信开发新手请务必阅读【开发文档】([Gitee Wiki](https://gitee.com/binary/weixin-java-tools/wikis/Home) 或者 [Github Wiki](https://github.com/binarywang/WxJava/wiki))的常见问题部分,可以少走很多弯路,节省不少时间。
8. 技术交流群想获得QQ群/微信群/钉钉企业群等信息的同学,请使用微信扫描上面的微信公众号二维码关注 `WxJava` 后点击相关菜单即可获取加入方式,同时也可以在微信中搜索 `weixin-java-tools``WxJava` 后选择正确的公众号进行关注该公众号会及时通知SDK相关更新信息并不定期分享微信Java开发相关技术知识
9. 钉钉技术交流群:`32206329`技术交流2群, `30294972`技术交流1群目前已满`35724728`通知群实时通知Github项目变更记录
10. 微信开发新手或者Java开发新手在群内提问或新开Issue提问前请先阅读[【提问的智慧】](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md),并确保已查阅过 [【开发文档Wiki】](https://github.com/binarywang/WxJava/wiki) ,避免浪费大家的宝贵时间;
11. 寻求帮助时需贴代码或大长串异常信息的,请利用 http://paste.ubuntu.com
--------------------------------
### 其他说明
@@ -106,6 +106,13 @@
- 企业微信:`weixin-java-cp`
- 微信视频号/微信小店:`weixin-java-channel`
**注意**
- **移动应用开发**如果你的移动应用iOS/Android App需要接入微信登录、分享等功能
- 微信登录(网页授权):使用 `weixin-java-open` 模块,在服务端处理 OAuth 授权
- 微信支付:使用 `weixin-java-pay` 模块
- 客户端集成需使用微信官方提供的移动端SDKiOS/Android本项目为服务端SDK
- **微信开放平台**`weixin-java-open`)主要用于第三方平台,代公众号或小程序进行开发和管理
---------------------------------
### 版本说明

View File

@@ -0,0 +1,80 @@
# WeChat Mini Program Customer Service Management
This document describes the new customer service management functionality added to the WxJava Mini Program SDK.
## Overview
Previously, the mini program module only had:
- `WxMaCustomserviceWorkService` - For binding mini programs to enterprise WeChat customer service
- `WxMaMsgService.sendKefuMsg()` - For sending customer service messages
The new `WxMaKefuService` adds comprehensive customer service management capabilities:
## Features
### Customer Service Account Management
- `kfList()` - Get list of customer service accounts
- `kfAccountAdd()` - Add new customer service account
- `kfAccountUpdate()` - Update customer service account
- `kfAccountDel()` - Delete customer service account
### Session Management
- `kfSessionCreate()` - Create customer service session
- `kfSessionClose()` - Close customer service session
- `kfSessionGet()` - Get customer session status
- `kfSessionList()` - Get customer service session list
## Usage Example
```java
// Get the customer service management service
WxMaKefuService kefuService = wxMaService.getKefuService();
// Add a new customer service account
WxMaKfAccountRequest request = WxMaKfAccountRequest.builder()
.kfAccount("service001@example")
.kfNick("Customer Service 001")
.kfPwd("password123")
.build();
boolean result = kefuService.kfAccountAdd(request);
// Create a session between user and customer service
boolean sessionResult = kefuService.kfSessionCreate("user_openid", "service001@example");
// Get customer service list
WxMaKfList kfList = kefuService.kfList();
```
## Bean Classes
### Request Objects
- `WxMaKfAccountRequest` - For customer service account operations
- `WxMaKfSessionRequest` - For session operations
### Response Objects
- `WxMaKfInfo` - Customer service account information
- `WxMaKfList` - List of customer service accounts
- `WxMaKfSession` - Session information
- `WxMaKfSessionList` - List of sessions
## API Endpoints
The service uses the following WeChat Mini Program API endpoints:
- `https://api.weixin.qq.com/cgi-bin/customservice/getkflist` - Get customer service list
- `https://api.weixin.qq.com/customservice/kfaccount/add` - Add customer service account
- `https://api.weixin.qq.com/customservice/kfaccount/update` - Update customer service account
- `https://api.weixin.qq.com/customservice/kfaccount/del` - Delete customer service account
- `https://api.weixin.qq.com/customservice/kfsession/create` - Create session
- `https://api.weixin.qq.com/customservice/kfsession/close` - Close session
- `https://api.weixin.qq.com/customservice/kfsession/getsession` - Get session
- `https://api.weixin.qq.com/customservice/kfsession/getsessionlist` - Get session list
## Integration
The service is automatically available through the main `WxMaService` interface:
```java
WxMaKefuService kefuService = wxMaService.getKefuService();
```
This fills the gap mentioned in the original issue and provides full customer service management capabilities for WeChat Mini Programs.

112
docs/QUARKUS_SUPPORT.md Normal file
View File

@@ -0,0 +1,112 @@
# WxJava Quarkus/GraalVM Native Image Support
## 概述
从 4.7.8.B 版本开始WxJava 提供了对 Quarkus 和 GraalVM Native Image 的支持。这允许您将使用 WxJava 的应用程序编译为原生可执行文件,从而获得更快的启动速度和更低的内存占用。
## 问题背景
在之前的版本中,使用 Quarkus 构建 Native Image 时会遇到以下错误:
```
Error: Unsupported features in 3 methods
Detailed message:
Error: Detected an instance of Random/SplittableRandom class in the image heap.
Instances created during image generation have cached seed values and don't behave as expected.
The culprit object has been instantiated by the 'org.apache.http.impl.auth.NTLMEngineImpl' class initializer
```
## 解决方案
为了解决这个问题WxJava 进行了以下改进:
### 1. Random 实例的延迟初始化
所有 `java.util.Random` 实例都已改为延迟初始化,避免在类加载时创建:
- `RandomUtils` - 使用双重检查锁定模式延迟初始化
- `SignUtils` - 使用双重检查锁定模式延迟初始化
- `WxCryptUtil` - 使用双重检查锁定模式延迟初始化
### 2. Native Image 配置
`weixin-java-common` 模块中添加了 GraalVM Native Image 配置文件:
- `META-INF/native-image/com.github.binarywang/weixin-java-common/native-image.properties`
- 配置 Apache HttpClient 相关类在运行时初始化,避免在构建时创建 SecureRandom 实例
- `META-INF/native-image/com.github.binarywang/weixin-java-common/reflect-config.json`
- 配置反射访问的类和方法
## 使用方式
### Quarkus 项目配置
在您的 Quarkus 项目中使用 WxJava只需正常引入依赖即可
```xml
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-miniapp</artifactId> <!-- 或其他模块 -->
<version>4.7.8.B</version>
</dependency>
```
### 构建 Native Image
使用 Quarkus 构建原生可执行文件:
```bash
./mvnw package -Pnative
```
或使用容器构建:
```bash
./mvnw package -Pnative -Dquarkus.native.container-build=true
```
### GraalVM Native Image
如果直接使用 GraalVM Native Image 工具:
```bash
native-image --no-fallback \
-H:+ReportExceptionStackTraces \
-jar your-application.jar
```
WxJava 的配置文件会自动被 Native Image 工具识别和应用。
## 测试验证
建议在构建 Native Image 后进行以下测试:
1. 验证应用程序可以正常启动
2. 验证微信 API 调用功能正常
3. 验证随机字符串生成功能正常
4. 验证加密/解密功能正常
## 已知限制
- 本配置主要针对 Quarkus 3.x 和 GraalVM 22.x+ 版本进行测试
- 如果使用其他 Native Image 构建工具(如 Spring Native可能需要额外配置
- 部分反射使用可能需要根据实际使用的 WxJava 功能进行调整
## 问题反馈
如果在使用 Quarkus/GraalVM Native Image 时遇到问题,请通过以下方式反馈:
1. 在 [GitHub Issues](https://github.com/binarywang/WxJava/issues) 提交问题
2. 提供详细的错误信息和 Native Image 构建日志
3. 说明使用的 Quarkus 版本和 GraalVM 版本
## 参考资料
- [Quarkus 官方文档](https://quarkus.io/)
- [GraalVM Native Image 文档](https://www.graalvm.org/latest/reference-manual/native-image/)
- [Quarkus Tips for Writing Native Applications](https://quarkus.io/guides/writing-native-applications-tips)
## 贡献
欢迎提交 PR 完善 Quarkus/GraalVM 支持!如果您发现了新的兼容性问题或有改进建议,请参考 [代码贡献指南](CONTRIBUTING.md)。

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java</artifactId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
<packaging>pom</packaging>
<name>WxJava - Weixin/Wechat Java SDK</name>
<description>微信开发Java SDK</description>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java</artifactId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<packaging>pom</packaging>
<artifactId>wx-java-solon-plugins</artifactId>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>wx-java-solon-plugins</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>wx-java-solon-plugins</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -4,7 +4,7 @@
<parent>
<artifactId>wx-java-solon-plugins</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -4,7 +4,7 @@
<parent>
<artifactId>wx-java-solon-plugins</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>wx-java-solon-plugins</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -4,7 +4,7 @@
<parent>
<artifactId>wx-java-solon-plugins</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>wx-java-solon-plugins</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>wx-java-solon-plugins</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>wx-java-solon-plugins</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>wx-java-solon-plugins</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>wx-java-solon-plugins</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java</artifactId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<packaging>pom</packaging>
<artifactId>wx-java-spring-boot-starters</artifactId>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>wx-java-spring-boot-starters</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>wx-java-spring-boot-starters</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -4,7 +4,7 @@
<parent>
<artifactId>wx-java-spring-boot-starters</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -4,7 +4,7 @@
<parent>
<artifactId>wx-java-spring-boot-starters</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -4,7 +4,7 @@
<parent>
<artifactId>wx-java-spring-boot-starters</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>wx-java-spring-boot-starters</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -4,7 +4,7 @@
<parent>
<artifactId>wx-java-spring-boot-starters</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>wx-java-spring-boot-starters</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>wx-java-spring-boot-starters</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>wx-java-spring-boot-starters</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -28,6 +28,7 @@ public class WxOpenServiceAutoConfiguration {
}
@Bean
@ConditionalOnMissingBean
public WxOpenMessageRouter wxOpenMessageRouter(WxOpenService wxOpenService) {
return new WxOpenMessageRouter(wxOpenService);
}

View File

@@ -5,7 +5,7 @@
<parent>
<artifactId>wx-java-spring-boot-starters</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>wx-java-spring-boot-starters</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java</artifactId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<artifactId>weixin-graal</artifactId>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java</artifactId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<artifactId>weixin-java-channel</artifactId>

View File

@@ -2,11 +2,8 @@ package me.chanjar.weixin.channel.api;
import java.util.List;
import me.chanjar.weixin.channel.bean.after.AfterSaleInfoResponse;
import me.chanjar.weixin.channel.bean.after.AfterSaleListParam;
import me.chanjar.weixin.channel.bean.after.AfterSaleListResponse;
import me.chanjar.weixin.channel.bean.after.AfterSaleReasonResponse;
import me.chanjar.weixin.channel.bean.after.AfterSaleRejectReasonResponse;
import me.chanjar.weixin.channel.bean.after.*;
import me.chanjar.weixin.channel.bean.base.WxChannelBaseResponse;
import me.chanjar.weixin.channel.bean.complaint.ComplaintOrderResponse;
import me.chanjar.weixin.common.error.WxErrorException;
@@ -149,4 +146,41 @@ public interface WxChannelAfterSaleService {
* @throws WxErrorException 异常
*/
AfterSaleRejectReasonResponse getRejectReason() throws WxErrorException;
/**
* 换货发货
* 文档地址https://developers.weixin.qq.com/doc/store/shop/API/channels-shop-aftersale/api_acceptexchangereship.html
*
* @param afterSaleOrderId 售后单号
* @param waybillId 快递单号
* @param deliveryId 快递公司id
* @return BaseResponse
*
* @throws WxErrorException 异常
*/
WxChannelBaseResponse acceptExchangeReship(String afterSaleOrderId, String waybillId, String deliveryId) throws WxErrorException;
/**
* 换货拒绝发货
* 文档地址https://developers.weixin.qq.com/doc/store/shop/API/channels-shop-aftersale/api_rejectexchangereship.html
*
* @param afterSaleOrderId 售后单号
* @param rejectReason 拒绝原因具体描述 ,可使用默认描述,也可以自定义描述
* @param rejectReasonType 拒绝原因枚举值
* @param rejectCertificates 退款凭证可使用图片上传接口获取media_id数据类型填0
* @return BaseResponse
*
* @throws WxErrorException 异常
*/
WxChannelBaseResponse rejectExchangeReship(String afterSaleOrderId, String rejectReason, Integer rejectReasonType, List<String> rejectCertificates) throws WxErrorException;
/**
* 商家协商
* 文档地址https://developers.weixin.qq.com/doc/store/shop/API/channels-shop-aftersale/api_merchantupdateaftersale.html
* @param param 参数
* @return BaseResponse
*
* @throws WxErrorException 异常
*/
WxChannelBaseResponse merchantUpdateAfterSale(AfterSaleMerchantUpdateParam param) throws WxErrorException;
}

View File

@@ -22,7 +22,9 @@ import static me.chanjar.weixin.channel.constant.WxChannelApiUrlConstants.Compla
@Slf4j
public class WxChannelAfterSaleServiceImpl implements WxChannelAfterSaleService {
/** 微信商店服务 */
/**
* 微信商店服务
*/
private final BaseWxChannelServiceImpl<?, ?> shopService;
public WxChannelAfterSaleServiceImpl(BaseWxChannelServiceImpl<?, ?> shopService) {
@@ -107,4 +109,25 @@ public class WxChannelAfterSaleServiceImpl implements WxChannelAfterSaleService
String resJson = shopService.post(AFTER_SALE_REJECT_REASON_GET_URL, "{}");
return ResponseUtils.decode(resJson, AfterSaleRejectReasonResponse.class);
}
@Override
public WxChannelBaseResponse acceptExchangeReship(String afterSaleOrderId, String waybillId, String deliveryId) throws WxErrorException {
AfterSaleAcceptExchangeReshipParam param = new AfterSaleAcceptExchangeReshipParam(afterSaleOrderId, waybillId, deliveryId);
String resJson = shopService.post(AFTER_SALE_ACCEPT_EXCHANGE_RESHIP_URL, param);
return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
}
@Override
public WxChannelBaseResponse rejectExchangeReship(String afterSaleOrderId, String rejectReason, Integer rejectReasonType, List<String> rejectCertificates) throws WxErrorException {
AfterSaleRejectExchangeReshipParam param = new AfterSaleRejectExchangeReshipParam(afterSaleOrderId, rejectReason, rejectReasonType, rejectCertificates);
String resJson = shopService.post(AFTER_SALE_REJECT_EXCHANGE_RESHIP_URL, param);
return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
}
@Override
public WxChannelBaseResponse merchantUpdateAfterSale(AfterSaleMerchantUpdateParam param) throws WxErrorException {
String resJson = shopService.post(AFTER_SALE_MERCHANT_UPDATE_URL, param);
return ResponseUtils.decode(resJson, WxChannelBaseResponse.class);
}
}

View File

@@ -0,0 +1,35 @@
package me.chanjar.weixin.channel.bean.after;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
/**
* 售后单换货发货信息
*
* @author <a href="https://gitee.com/cchengg">Chu</a>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AfterSaleAcceptExchangeReshipParam extends AfterSaleIdParam {
private static final long serialVersionUID = -7946679037747710613L;
/** 快递单号*/
@JsonProperty("waybill_id")
private String waybillId;
/** 快递公司id通过获取快递公司列表接口获得非主流快递公司可以填OTHER*/
@JsonProperty("delivery_id")
private String deliveryId;
public AfterSaleAcceptExchangeReshipParam() {
}
public AfterSaleAcceptExchangeReshipParam(String afterSaleOrderId, String waybillId, String deliveryId) {
super(afterSaleOrderId);
this.waybillId = waybillId;
this.deliveryId = deliveryId;
}
}

View File

@@ -31,4 +31,12 @@ public class AfterSaleExchangeProductInfo implements Serializable {
/** 数量 */
@JsonProperty("product_cnt")
private String productCnt;
/** 旧商品价格 */
@JsonProperty("old_sku_price")
private Integer oldSkuPrice;
/** 新商品价格 */
@JsonProperty("new_sku_price")
private Integer newSkuPrice;
}

View File

@@ -0,0 +1,57 @@
package me.chanjar.weixin.channel.bean.after;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* 售后单商家协商信息
*
* @author <a href="https://gitee.com/cchengg">Chu</a>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AfterSaleMerchantUpdateParam extends AfterSaleIdParam {
private static final long serialVersionUID = -3672834150982780L;
/** 协商修改把售后单修改成该售后类型。1:退款2:退货退款*/
@JsonProperty("type")
private Integer type;
/** 金额(单位:分)*/
@JsonProperty("amount")
private Integer amount;
/** 协商描述*/
@JsonProperty("merchant_update_desc")
private String merchantUpdateDesc;
/** 协商原因*/
@JsonProperty("update_reason_type")
private Integer updateReasonType;
/** 1:已协商一致,邀请买家取消售后; 2:邀请买家核实与补充凭证; 3:修改买家售后申请*/
@JsonProperty("merchant_update_type")
private Integer merchantUpdateType;
/** 协商凭证id列表可使用图片上传接口获取media_id数据类型填0当update_reason_type对应的need_image为1时必填*/
@JsonProperty("media_ids")
private List<String> mediaIds;
public AfterSaleMerchantUpdateParam() {
}
public AfterSaleMerchantUpdateParam(String afterSaleOrderId, Integer type, Integer updateReasonType, Integer merchantUpdateType
, Integer amount, String merchantUpdateDesc, List<String> mediaIds) {
super(afterSaleOrderId);
this.type = type;
this.updateReasonType = updateReasonType;
this.merchantUpdateType = merchantUpdateType;
this.amount = amount;
this.merchantUpdateDesc = merchantUpdateDesc;
this.mediaIds = mediaIds;
}
}

View File

@@ -0,0 +1,41 @@
package me.chanjar.weixin.channel.bean.after;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* 售后单换货拒绝发货信息
*
* @author <a href="https://gitee.com/cchengg">Chu</a>
*/
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class AfterSaleRejectExchangeReshipParam extends AfterSaleIdParam {
private static final long serialVersionUID = -7946679037747710613L;
/** 拒绝原因具体描述 ,可使用默认描述,也可以自定义描述*/
@JsonProperty("reject_reason")
private String rejectReason;
/** 拒绝原因枚举 */
@JsonProperty("reject_reason_type")
private Integer rejectReasonType;
/** 退款凭证可使用图片上传接口获取media_id数据类型填0*/
@JsonProperty("reject_certificates")
private List<String> rejectCertificates;
public AfterSaleRejectExchangeReshipParam() {
}
public AfterSaleRejectExchangeReshipParam(String afterSaleOrderId, String rejectReason, Integer rejectReasonType, List<String> rejectCertificates) {
super(afterSaleOrderId);
this.rejectReason = rejectReason;
this.rejectReasonType = rejectReasonType;
this.rejectCertificates = rejectCertificates;
}
}

View File

@@ -36,4 +36,9 @@ public class AfterSaleRejectReason implements Serializable {
@JsonProperty("reject_reason")
private String rejectReason;
/**
* 售后拒绝原因适用场景
*/
@JsonProperty("reject_scene")
private Integer rejectScene;
}

View File

@@ -26,7 +26,7 @@ public class ComplaintHistory implements Serializable {
/** 用户联系电话 */
@JsonProperty("phone_number")
private Integer phoneNumber;
private String phoneNumber;
/** 相关文本内容 */
@JsonProperty("content")

View File

@@ -0,0 +1,42 @@
package me.chanjar.weixin.channel.bean.order;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 更换sku信息
*/
@Data
@NoArgsConstructor
public class ChangeSkuInfo implements Serializable {
private static final long serialVersionUID = 8783442929429377519L;
/**
* 发货前更换sku状态。3等待商家处理4商家审核通过5商家拒绝6用户主动取消7超时默认拒绝
*/
@JsonProperty("preshipment_change_sku_state")
private Integer preshipmentChangeSkuState;
/**
* 原sku_id
*/
@JsonProperty("old_sku_id")
private String oldSkuId;
/**
* 用户申请更换的sku_id
*/
@JsonProperty("new_sku_id")
private String newSkuId;
/**
* 商家处理请求的最后时间,只有当前换款请求处于"等待商家处理"才有值
*/
@JsonProperty("ddl_time_stamp")
private Integer deadlineTimeStamp;
}

View File

@@ -0,0 +1,24 @@
package me.chanjar.weixin.channel.bean.order;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 代发相关信息
*/
@Data
@NoArgsConstructor
public class DropshipInfo implements Serializable {
private static final long serialVersionUID = -4562618835611282016L;
/**
* 代发单号
*/
@JsonProperty("ds_order_id")
private Long dsOrderId;
}

View File

@@ -0,0 +1,25 @@
package me.chanjar.weixin.channel.bean.order;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.List;
/**
* 赠品信息
*/
@Data
@NoArgsConstructor
public class FreeGiftInfo implements Serializable {
private static final long serialVersionUID = 2024061212345678901L;
/**
* 赠品对应的主品信息
*/
@JsonProperty("main_product_list")
private List<MainProductInfo> mainProductList;
}

View File

@@ -0,0 +1,42 @@
package me.chanjar.weixin.channel.bean.order;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 赠品对应的主品信息
*/
@Data
@NoArgsConstructor
public class MainProductInfo implements Serializable {
private static final long serialVersionUID = 2024061212345678901L;
/**
* 赠品数量
*/
@JsonProperty("gift_cnt")
private Integer giftCnt;
/**
* 活动id
*/
@JsonProperty("task_id")
private Integer taskId;
/**
* 商品id
*/
@JsonProperty("product_id")
private Integer productId;
/**
* 主品sku_id
*/
@JsonProperty("sku_id")
private Integer skuId;
}

View File

@@ -18,4 +18,24 @@ public class OrderCouponInfo implements Serializable {
/** 用户优惠券id */
@JsonProperty("user_coupon_id")
private String userCouponId;
/**
* 优惠券类型
* 1 商家优惠
* 2 达人优惠
* 3 平台优惠
* 4 国家补贴
* 5 地方补贴
*/
@JsonProperty("coupon_type")
private Integer couponType;
/** 优惠金额,单位为分,该张优惠券、抵扣该商品的金额 */
@JsonProperty("discounted_price")
private Integer discountedPrice;
/** 优惠券id */
@JsonProperty("coupon_id")
private String couponId;
}

View File

@@ -16,12 +16,8 @@ public class OrderPayInfo implements Serializable {
private static final long serialVersionUID = -5085386252699113948L;
/** 预支付id */
@JsonProperty("prepayId")
private String prepayId;
/** 预支付时间,秒级时间戳 */
@JsonProperty("prepay_time")
private Long prepayTime;
@JsonProperty("payment_method")
private Integer paymentMethod;
/** 支付时间,秒级时间戳 */
@JsonProperty("pay_time")

View File

@@ -107,4 +107,34 @@ public class OrderPriceInfo implements Serializable {
@JsonProperty("finder_discounted_price")
private Integer finderDiscountedPrice;
/**
* 订单维度会员权益优惠金额
*/
@JsonProperty("vip_discounted_price")
private Integer vipDiscountedPrice;
/**
* 订单维度一起买优惠金额,单位为分
*/
@JsonProperty("bulkbuy_discounted_price")
private Integer bulkbuyDiscountedPrice;
/**
* 订单维度国补优惠金额
*/
@JsonProperty("national_subsidy_discounted_price")
private Integer nationalSubsidyDiscountedPrice;
/**
* 订单维度平台券优惠金额,单位为分
*/
@JsonProperty("cash_coupon_discounted_price")
private Integer cashCouponDiscountedPrice;
/**
* 订单维度地方补贴优惠金额(商家出资),单位为分
*/
@JsonProperty("national_subsidy_merchant_discounted_price")
private Integer nationalSubsidyMerchantDiscountedPrice;
}

View File

@@ -176,7 +176,7 @@ public class OrderProductInfo implements Serializable {
private Integer merchantDiscountedPrice;
/**
* 商家优惠金额,单位为分
* 达人优惠金额,单位为分
*/
@JsonProperty("finder_discounted_price")
private Integer finderDiscountedPrice;
@@ -186,4 +186,59 @@ public class OrderProductInfo implements Serializable {
*/
@JsonProperty("is_free_gift")
private Boolean freeGift;
/**
* 订单内商品维度会员权益优惠金额,单位为分
*/
@JsonProperty("vip_discounted_price")
private Integer vipDiscountedPrice;
/**
* 商品常量编号,订单内商品唯一标识,下单后不会发生变化
*/
@JsonProperty("product_unique_id")
private String productUniqueId;
/**
* 更换sku信息
*/
@JsonProperty("change_sku_info")
private ChangeSkuInfo changeSkuInfo;
/**
* 赠品信息
*/
@JsonProperty("free_gift_info")
private FreeGiftInfo freeGiftInfo;
/**
* 订单内商品维度一起买优惠金额,单位为分
*/
@JsonProperty("bulkbuy_discounted_price")
private Integer bulkbuyDiscountedPrice;
/**
* 订单内商品维度国补优惠金额,单位为分
*/
@JsonProperty("national_subsidy_discounted_price")
private Integer nationalSubsidyDiscountedPrice;
/**
* 代发相关信息
*/
@JsonProperty("dropship_info")
private DropshipInfo dropshipInfo;
/**
* 是否闪购商品
*/
@JsonProperty("is_flash_sale")
private Boolean flashSale;
/**
* 订单内商品维度地方补贴优惠金额(商家出资),单位为分
*/
@JsonProperty("national_subsidy_merchant_discounted_price")
private Integer nationalSubsidyMerchantDiscountedPrice;
}

View File

@@ -139,4 +139,16 @@ public class SpuInfo extends SpuSimpleInfo {
/** 尺码表信息 */
@JsonProperty("size_chart")
private SpuSizeChart sizeChart;
/** 短标题 */
@JsonProperty("short_title")
private String shortTitle;
/** 销量 */
@JsonProperty("total_sold_num")
private Integer totalSoldNum;
/** 发布模式0: 普通模式1: 极简模式 */
@JsonProperty("release_mode")
private Integer releaseMode;
}

View File

@@ -232,6 +232,12 @@ public class WxChannelApiUrlConstants {
String AFTER_SALE_REASON_GET_URL = "https://api.weixin.qq.com/channels/ec/aftersale/reason/get";
/** 获取拒绝售后原因*/
String AFTER_SALE_REJECT_REASON_GET_URL = "https://api.weixin.qq.com/channels/ec/aftersale/rejectreason/get";
/** 换货发货*/
String AFTER_SALE_ACCEPT_EXCHANGE_RESHIP_URL = "https://api.weixin.qq.com/channels/ec/aftersale/acceptexchangereship";
/** 换货拒绝发货*/
String AFTER_SALE_REJECT_EXCHANGE_RESHIP_URL = "https://api.weixin.qq.com/channels/ec/aftersale/rejectexchangereship";
/** 商家协商*/
String AFTER_SALE_MERCHANT_UPDATE_URL = "https://api.weixin.qq.com/channels/ec/aftersale/merchantupdateaftersale";
}
/** 纠纷相关接口 */

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java</artifactId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<artifactId>weixin-java-common</artifactId>

View File

@@ -4,12 +4,24 @@ public class RandomUtils {
private static final String RANDOM_STR = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
private static final java.util.Random RANDOM = new java.util.Random();
private static volatile java.util.Random random;
private static java.util.Random getRandom() {
if (random == null) {
synchronized (RandomUtils.class) {
if (random == null) {
random = new java.util.Random();
}
}
}
return random;
}
public static String getRandomStr() {
StringBuilder sb = new StringBuilder();
java.util.Random r = getRandom();
for (int i = 0; i < 16; i++) {
sb.append(RANDOM_STR.charAt(RANDOM.nextInt(RANDOM_STR.length())));
sb.append(RANDOM_STR.charAt(r.nextInt(RANDOM_STR.length())));
}
return sb.toString();
}

View File

@@ -37,6 +37,19 @@ public class WxCryptUtil {
private static final Base64 BASE64 = new Base64();
private static final Charset CHARSET = StandardCharsets.UTF_8;
private static volatile Random random;
private static Random getRandom() {
if (random == null) {
synchronized (WxCryptUtil.class) {
if (random == null) {
random = new Random();
}
}
}
return random;
}
private static final ThreadLocal<DocumentBuilder> BUILDER_LOCAL = ThreadLocal.withInitial(() -> {
try {
final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
@@ -109,10 +122,10 @@ public class WxCryptUtil {
*/
private static String genRandomStr() {
String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
Random random = new Random();
Random r = getRandom();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 16; i++) {
int number = random.nextInt(base.length());
int number = r.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
@@ -320,14 +333,28 @@ public class WxCryptUtil {
byte[] bytes = PKCS7Encoder.decode(original);
// 分离16位随机字符串,网络字节序和AppId
if (bytes == null || bytes.length < 20) {
throw new WxRuntimeException("解密后数据长度异常可能为错误的密文或EncodingAESKey");
}
byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);
int xmlLength = bytesNetworkOrder2Number(networkOrder);
xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), CHARSET);
fromAppid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length), CHARSET);
// 长度边界校验,避免非法长度导致的越界/参数异常
int startIndex = 20;
int endIndex = startIndex + xmlLength;
if (xmlLength < 0 || endIndex > bytes.length) {
throw new WxRuntimeException("解密后数据格式非法消息长度不正确可能为错误的密文或EncodingAESKey");
}
xmlContent = new String(Arrays.copyOfRange(bytes, startIndex, endIndex), CHARSET);
fromAppid = new String(Arrays.copyOfRange(bytes, endIndex, bytes.length), CHARSET);
} catch (Exception e) {
throw new WxRuntimeException(e);
if (e instanceof WxRuntimeException) {
throw (WxRuntimeException) e;
} else {
throw new WxRuntimeException(e);
}
}
// appid不相同的情况 暂时忽略这段判断

View File

@@ -12,14 +12,14 @@ import java.io.Reader;
public class GsonParser {
public static JsonObject parse(String json) {
return JsonParser.parseString(json).getAsJsonObject();
return new JsonParser().parse(json).getAsJsonObject();
}
public static JsonObject parse(Reader json) {
return JsonParser.parseReader(json).getAsJsonObject();
return new JsonParser().parse(json).getAsJsonObject();
}
public static JsonObject parse(JsonReader json) {
return JsonParser.parseReader(json).getAsJsonObject();
return new JsonParser().parse(json).getAsJsonObject();
}
}

View File

@@ -1,5 +1,7 @@
package me.chanjar.weixin.common.util.json;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import me.chanjar.weixin.common.bean.WxAccessToken;
@@ -7,6 +9,9 @@ import me.chanjar.weixin.common.bean.WxNetCheckResult;
import me.chanjar.weixin.common.bean.menu.WxMenu;
import me.chanjar.weixin.common.error.WxError;
import me.chanjar.weixin.common.bean.result.WxMediaUploadResult;
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
import java.io.File;
import java.util.Objects;
/**
@@ -25,6 +30,17 @@ public class WxGsonBuilder {
INSTANCE.registerTypeAdapter(WxMediaUploadResult.class, new WxMediaUploadResultAdapter());
INSTANCE.registerTypeAdapter(WxNetCheckResult.class, new WxNetCheckResultGsonAdapter());
INSTANCE.setExclusionStrategies(new ExclusionStrategy() {
@Override
public boolean shouldSkipField(FieldAttributes fieldAttributes) {
return false;
}
@Override
public boolean shouldSkipClass(Class<?> aClass) {
return aClass == File.class || aClass == ApacheHttpClientBuilder.class;
}
});
}
public static Gson create() {

View File

@@ -0,0 +1,4 @@
Args = --initialize-at-run-time=org.apache.http.impl.auth.NTLMEngineImpl \
--initialize-at-run-time=org.apache.http.impl.auth.NTLMEngine \
--initialize-at-run-time=org.apache.http.impl.auth.KerberosScheme \
--initialize-at-run-time=org.apache.http.impl.auth.SPNegoScheme

View File

@@ -0,0 +1,14 @@
[
{
"name": "me.chanjar.weixin.common.util.RandomUtils",
"methods": [
{"name": "getRandomStr", "parameterTypes": []}
]
},
{
"name": "me.chanjar.weixin.common.util.crypto.WxCryptUtil",
"allDeclaredConstructors": true,
"allDeclaredMethods": true,
"allDeclaredFields": true
}
]

View File

@@ -0,0 +1,47 @@
package me.chanjar.weixin.common.util.json;
import com.google.gson.JsonObject;
import com.google.gson.stream.JsonReader;
import org.testng.annotations.Test;
import java.io.StringReader;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertNotNull;
/**
* GsonParser 测试类
*
* @author <a href="https://github.com/binarywang">Binary Wang</a>
*/
public class GsonParserTest {
@Test
public void testParseString() {
String json = "{\"code\":\"ALREADY_EXISTS\",\"message\":\"当前订单已关闭,可查询订单了解关闭原因\"}";
JsonObject jsonObject = GsonParser.parse(json);
assertNotNull(jsonObject);
assertEquals(jsonObject.get("code").getAsString(), "ALREADY_EXISTS");
assertEquals(jsonObject.get("message").getAsString(), "当前订单已关闭,可查询订单了解关闭原因");
}
@Test
public void testParseReader() {
String json = "{\"code\":\"SUCCESS\",\"message\":\"处理成功\"}";
StringReader reader = new StringReader(json);
JsonObject jsonObject = GsonParser.parse(reader);
assertNotNull(jsonObject);
assertEquals(jsonObject.get("code").getAsString(), "SUCCESS");
assertEquals(jsonObject.get("message").getAsString(), "处理成功");
}
@Test
public void testParseJsonReader() {
String json = "{\"errcode\":0,\"errmsg\":\"ok\"}";
JsonReader jsonReader = new JsonReader(new StringReader(json));
JsonObject jsonObject = GsonParser.parse(jsonReader);
assertNotNull(jsonObject);
assertEquals(jsonObject.get("errcode").getAsInt(), 0);
assertEquals(jsonObject.get("errmsg").getAsString(), "ok");
}
}

View File

@@ -0,0 +1,141 @@
# WeChat Enterprise Workflow Approval Guide
# 企业微信流程审批功能使用指南
## Overview / 概述
WxJava SDK provides comprehensive support for WeChat Enterprise workflow approval (企业微信流程审批), including both traditional OA approval and the approval process engine.
WxJava SDK 提供全面的企业微信流程审批支持包括传统OA审批和审批流程引擎。
## Current Implementation Status / 当前实现状态
### ✅ Fully Implemented APIs / 已完整实现的API
1. **Submit Approval Application / 提交审批申请**
- Endpoint: `/cgi-bin/oa/applyevent`
- Documentation: [91853](https://work.weixin.qq.com/api/doc/90000/90135/91853)
- Implementation: `WxCpOaService.apply(WxCpOaApplyEventRequest)`
2. **Get Approval Details / 获取审批申请详情**
- Endpoint: `/cgi-bin/oa/getapprovaldetail`
- Implementation: `WxCpOaService.getApprovalDetail(String spNo)`
3. **Batch Get Approval Numbers / 批量获取审批单号**
- Endpoint: `/cgi-bin/oa/getapprovalinfo`
- Implementation: `WxCpOaService.getApprovalInfo(...)`
4. **Approval Process Engine / 审批流程引擎**
- Endpoint: `/cgi-bin/corp/getopenapprovaldata`
- Implementation: `WxCpOaAgentService.getOpenApprovalData(String thirdNo)`
5. **Template Management / 模板管理**
- Create: `WxCpOaService.createOaApprovalTemplate(...)`
- Update: `WxCpOaService.updateOaApprovalTemplate(...)`
- Get Details: `WxCpOaService.getTemplateDetail(...)`
## Usage Examples / 使用示例
### 1. Submit Approval Application / 提交审批申请
```java
// Create approval request
WxCpOaApplyEventRequest request = new WxCpOaApplyEventRequest()
.setCreatorUserId("userId")
.setTemplateId("templateId")
.setUseTemplateApprover(0)
.setApprovers(Arrays.asList(
new WxCpOaApplyEventRequest.Approver()
.setAttr(2)
.setUserIds(new String[]{"approver1", "approver2"})
))
.setNotifiers(new String[]{"notifier1", "notifier2"})
.setNotifyType(1)
.setApplyData(new WxCpOaApplyEventRequest.ApplyData()
.setContents(Arrays.asList(
new ApplyDataContent()
.setControl("Text")
.setId("Text-1234567890")
.setValue(new ContentValue().setText("Approval content"))
))
);
// Submit approval
String spNo = wxCpService.getOaService().apply(request);
```
### 2. Get Approval Details / 获取审批详情
```java
// Get approval details by approval number
WxCpApprovalDetailResult result = wxCpService.getOaService()
.getApprovalDetail("approval_number");
WxCpApprovalDetailResult.WxCpApprovalDetail detail = result.getInfo();
System.out.println("Approval Status: " + detail.getSpStatus());
System.out.println("Approval Name: " + detail.getSpName());
```
### 3. Batch Get Approval Information / 批量获取审批信息
```java
// Get approval info with filters
Date startTime = new Date(System.currentTimeMillis() - 7 * 24 * 60 * 60 * 1000);
Date endTime = new Date();
WxCpApprovalInfo approvalInfo = wxCpService.getOaService()
.getApprovalInfo(startTime, endTime, "0", 100, null);
List<String> spNumbers = approvalInfo.getSpNoList();
```
### 4. Third-Party Application Support / 第三方应用支持
```java
// For third-party applications
WxCpTpOAService tpOaService = wxCpTpService.getOaService();
// Submit approval for specific corp
String spNo = tpOaService.apply(request, "corpId");
// Get approval details for specific corp
WxCpApprovalDetailResult detail = tpOaService.getApprovalDetail("spNo", "corpId");
```
## Multi-Account Configuration / 多账号配置支持
WxJava supports multi-account configurations for enterprise scenarios:
```java
// Spring Boot configuration example
@Autowired
private WxCpMultiServices wxCpMultiServices;
// Get service for specific corp
WxCpService wxCpService = wxCpMultiServices.getWxCpService("corpId");
WxCpOaService oaService = wxCpService.getOaService();
```
## Available Data Models / 可用数据模型
- `WxCpOaApplyEventRequest` - Approval application request
- `WxCpApprovalDetailResult` - Approval details response
- `WxCpApprovalInfo` - Batch approval information
- `WxCpXmlApprovalInfo` - XML approval message handling
- `WxCpOaApprovalTemplate` - Approval template management
## Documentation References / 文档参考
- [Submit Approval Application (91853)](https://work.weixin.qq.com/api/doc/90000/90135/91853)
- [Get Approval Details (91983)](https://work.weixin.qq.com/api/doc/90000/90135/91983)
- [Batch Get Approval Numbers (91816)](https://work.weixin.qq.com/api/doc/90000/90135/91816)
- [Approval Process Engine (90269)](https://developer.work.weixin.qq.com/document/path/90269)
## Conclusion / 结论
WxJava already provides comprehensive support for WeChat Enterprise workflow approval. The "new version" (新版) approval functionality referenced in issue requests is **already fully implemented** and available for use.
WxJava 已经提供了企业微信流程审批的全面支持。问题中提到的"新版"流程审批功能**已经完全实现**并可使用。
For questions about specific usage, please refer to the test cases in `WxCpOaServiceImplTest` and the comprehensive API documentation.
有关具体使用问题,请参考 `WxCpOaServiceImplTest` 中的测试用例和全面的API文档。

View File

@@ -0,0 +1,107 @@
# 企业微信智能机器人接口
本模块提供企业微信智能机器人相关的API接口实现。
## 官方文档
- [企业微信智能机器人接口](https://developer.work.weixin.qq.com/document/path/101039)
## 接口说明
### 获取服务实例
```java
WxCpService wxCpService = ...; // 初始化企业微信服务
WxCpIntelligentRobotService robotService = wxCpService.getIntelligentRobotService();
```
### 创建智能机器人
```java
WxCpIntelligentRobotCreateRequest request = new WxCpIntelligentRobotCreateRequest();
request.setName("我的智能机器人");
request.setDescription("这是一个智能客服机器人");
request.setAvatar("http://example.com/avatar.jpg");
WxCpIntelligentRobotCreateResponse response = robotService.createRobot(request);
String robotId = response.getRobotId();
```
### 更新智能机器人
```java
WxCpIntelligentRobotUpdateRequest request = new WxCpIntelligentRobotUpdateRequest();
request.setRobotId("robot_id_here");
request.setName("更新后的机器人名称");
request.setDescription("更新后的描述");
request.setStatus(1); // 1:启用, 0:停用
robotService.updateRobot(request);
```
### 查询智能机器人
```java
String robotId = "robot_id_here";
WxCpIntelligentRobot robot = robotService.getRobot(robotId);
System.out.println("机器人名称: " + robot.getName());
System.out.println("机器人状态: " + robot.getStatus());
```
### 智能对话
```java
WxCpIntelligentRobotChatRequest request = new WxCpIntelligentRobotChatRequest();
request.setRobotId("robot_id_here");
request.setUserid("user123");
request.setMessage("你好,请问如何使用这个功能?");
request.setSessionId("session123"); // 可选,用于保持会话连续性
WxCpIntelligentRobotChatResponse response = robotService.chat(request);
String reply = response.getReply();
String sessionId = response.getSessionId();
```
### 重置会话
```java
String robotId = "robot_id_here";
String userid = "user123";
String sessionId = "session123";
robotService.resetSession(robotId, userid, sessionId);
```
### 删除智能机器人
```java
String robotId = "robot_id_here";
robotService.deleteRobot(robotId);
```
## 主要类说明
### 请求类
- `WxCpIntelligentRobotCreateRequest`: 创建机器人请求
- `WxCpIntelligentRobotUpdateRequest`: 更新机器人请求
- `WxCpIntelligentRobotChatRequest`: 智能对话请求
### 响应类
- `WxCpIntelligentRobotCreateResponse`: 创建机器人响应
- `WxCpIntelligentRobotChatResponse`: 智能对话响应
- `WxCpIntelligentRobot`: 机器人信息实体
### 服务接口
- `WxCpIntelligentRobotService`: 智能机器人服务接口
- `WxCpIntelligentRobotServiceImpl`: 智能机器人服务实现
## 注意事项
1. 需要确保企业微信应用具有智能机器人相关权限
2. 智能机器人功能可能需要特定的企业微信版本支持
3. 会话ID可以用于保持对话的连续性提升用户体验
4. 机器人状态: 0表示停用1表示启用

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java</artifactId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<artifactId>weixin-java-cp</artifactId>

View File

@@ -70,6 +70,23 @@ public interface WxCpGroupRobotService {
*/
void sendMarkdown(String webhookUrl, String content) throws WxErrorException;
/**
* 发送markdown_v2类型的消息
*
* @param content markdown内容最长不超过4096个字节必须是utf8编码
* @throws WxErrorException 异常
*/
void sendMarkdownV2(String content) throws WxErrorException;
/**
* 发送markdown_v2类型的消息
*
* @param webhookUrl webhook地址
* @param content markdown内容最长不超过4096个字节必须是utf8编码
* @throws WxErrorException 异常
*/
void sendMarkdownV2(String webhookUrl, String content) throws WxErrorException;
/**
* 发送image类型的消息
*

View File

@@ -0,0 +1,67 @@
package me.chanjar.weixin.cp.api;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.cp.bean.intelligentrobot.*;
/**
* 企业微信智能机器人接口
* 官方文档: https://developer.work.weixin.qq.com/document/path/101039
*
* @author Binary Wang
*/
public interface WxCpIntelligentRobotService {
/**
* 创建智能机器人
*
* @param request 创建请求参数
* @return 创建结果
* @throws WxErrorException 微信接口异常
*/
WxCpIntelligentRobotCreateResponse createRobot(WxCpIntelligentRobotCreateRequest request) throws WxErrorException;
/**
* 删除智能机器人
*
* @param robotId 机器人ID
* @throws WxErrorException 微信接口异常
*/
void deleteRobot(String robotId) throws WxErrorException;
/**
* 更新智能机器人
*
* @param request 更新请求参数
* @throws WxErrorException 微信接口异常
*/
void updateRobot(WxCpIntelligentRobotUpdateRequest request) throws WxErrorException;
/**
* 查询智能机器人
*
* @param robotId 机器人ID
* @return 机器人信息
* @throws WxErrorException 微信接口异常
*/
WxCpIntelligentRobot getRobot(String robotId) throws WxErrorException;
/**
* 智能机器人会话
*
* @param request 聊天请求参数
* @return 聊天响应
* @throws WxErrorException 微信接口异常
*/
WxCpIntelligentRobotChatResponse chat(WxCpIntelligentRobotChatRequest request) throws WxErrorException;
/**
* 重置智能机器人会话
*
* @param robotId 机器人ID
* @param userid 用户ID
* @param sessionId 会话ID
* @throws WxErrorException 微信接口异常
*/
void resetSession(String robotId, String userid, String sessionId) throws WxErrorException;
}

View File

@@ -587,4 +587,11 @@ public interface WxCpService extends WxService {
* @return
*/
WxCpCorpGroupService getCorpGroupService();
/**
* 获取智能机器人服务
*
* @return 智能机器人服务 intelligent robot service
*/
WxCpIntelligentRobotService getIntelligentRobotService();
}

View File

@@ -74,6 +74,7 @@ public abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH
private final WxCpMeetingService meetingService = new WxCpMeetingServiceImpl(this);
private final WxCpCorpGroupService corpGroupService = new WxCpCorpGroupServiceImpl(this);
private final WxCpIntelligentRobotService intelligentRobotService = new WxCpIntelligentRobotServiceImpl(this);
/**
* 全局的是否正在刷新access token的锁.
@@ -702,4 +703,9 @@ public abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH
public WxCpCorpGroupService getCorpGroupService() {
return corpGroupService;
}
@Override
public WxCpIntelligentRobotService getIntelligentRobotService() {
return this.intelligentRobotService;
}
}

View File

@@ -42,6 +42,11 @@ public class WxCpGroupRobotServiceImpl implements WxCpGroupRobotService {
this.sendMarkdown(this.getWebhookUrl(), content);
}
@Override
public void sendMarkdownV2(String content) throws WxErrorException {
this.sendMarkdownV2(this.getWebhookUrl(), content);
}
@Override
public void sendImage(String base64, String md5) throws WxErrorException {
this.sendImage(this.getWebhookUrl(), base64, md5);
@@ -70,6 +75,14 @@ public class WxCpGroupRobotServiceImpl implements WxCpGroupRobotService {
.toJson());
}
@Override
public void sendMarkdownV2(String webhookUrl, String content) throws WxErrorException {
this.cpService.postWithoutToken(webhookUrl, new WxCpGroupRobotMessage()
.setMsgType(GroupRobotMsgType.MARKDOWN_V2)
.setContent(content)
.toJson());
}
@Override
public void sendImage(String webhookUrl, String base64, String md5) throws WxErrorException {
this.cpService.postWithoutToken(webhookUrl, new WxCpGroupRobotMessage()

View File

@@ -0,0 +1,64 @@
package me.chanjar.weixin.cp.api.impl;
import com.google.gson.JsonObject;
import lombok.RequiredArgsConstructor;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.cp.api.WxCpIntelligentRobotService;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.intelligentrobot.*;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.IntelligentRobot.*;
/**
* 企业微信智能机器人接口实现
*
* @author Binary Wang
*/
@RequiredArgsConstructor
public class WxCpIntelligentRobotServiceImpl implements WxCpIntelligentRobotService {
private final WxCpService cpService;
@Override
public WxCpIntelligentRobotCreateResponse createRobot(WxCpIntelligentRobotCreateRequest request) throws WxErrorException {
String responseText = this.cpService.post(CREATE_ROBOT, request.toJson());
return WxCpIntelligentRobotCreateResponse.fromJson(responseText);
}
@Override
public void deleteRobot(String robotId) throws WxErrorException {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("robot_id", robotId);
this.cpService.post(DELETE_ROBOT, jsonObject.toString());
}
@Override
public void updateRobot(WxCpIntelligentRobotUpdateRequest request) throws WxErrorException {
this.cpService.post(UPDATE_ROBOT, request.toJson());
}
@Override
public WxCpIntelligentRobot getRobot(String robotId) throws WxErrorException {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("robot_id", robotId);
String responseText = this.cpService.post(GET_ROBOT, jsonObject.toString());
return WxCpIntelligentRobot.fromJson(responseText);
}
@Override
public WxCpIntelligentRobotChatResponse chat(WxCpIntelligentRobotChatRequest request) throws WxErrorException {
String responseText = this.cpService.post(CHAT, request.toJson());
return WxCpIntelligentRobotChatResponse.fromJson(responseText);
}
@Override
public void resetSession(String robotId, String userid, String sessionId) throws WxErrorException {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("robot_id", robotId);
jsonObject.addProperty("userid", userid);
jsonObject.addProperty("session_id", sessionId);
this.cpService.post(RESET_SESSION, jsonObject.toString());
}
}

View File

@@ -11,6 +11,7 @@ import me.chanjar.weixin.common.util.json.GsonParser;
import me.chanjar.weixin.cp.api.WxCpMsgAuditService;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.msgaudit.*;
import me.chanjar.weixin.cp.config.WxCpConfigStorage;
import me.chanjar.weixin.cp.util.crypto.WxCpCryptUtil;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
import org.apache.commons.lang3.StringUtils;
@@ -35,20 +36,59 @@ import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.MsgAudit.*;
public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService {
private final WxCpService cpService;
/**
* SDK初始化有效期根据企微文档为7200秒
*/
private static final int SDK_EXPIRES_TIME = 7200;
@Override
public WxCpChatDatas getChatDatas(long seq, @NonNull long limit, String proxy, String passwd,
@NonNull long timeout) throws Exception {
String configPath = cpService.getWxCpConfigStorage().getMsgAuditLibPath();
// 获取或初始化SDK
long sdk = this.initSdk();
long slice = Finance.NewSlice();
long ret = Finance.GetChatData(sdk, seq, limit, proxy, passwd, timeout, slice);
if (ret != 0) {
Finance.FreeSlice(slice);
throw new WxErrorException("getchatdata err ret " + ret);
}
// 拉取会话存档
String content = Finance.GetContentFromSlice(slice);
Finance.FreeSlice(slice);
WxCpChatDatas chatDatas = WxCpChatDatas.fromJson(content);
if (chatDatas.getErrCode().intValue() != 0) {
throw new WxErrorException(chatDatas.toJson());
}
chatDatas.setSdk(sdk);
return chatDatas;
}
/**
* 获取或初始化SDK如果SDK已过期则重新初始化
*
* @return sdk id
* @throws WxErrorException 初始化失败时抛出异常
*/
private synchronized long initSdk() throws WxErrorException {
WxCpConfigStorage configStorage = cpService.getWxCpConfigStorage();
// 检查SDK是否已缓存且未过期
if (!configStorage.isMsgAuditSdkExpired()) {
long cachedSdk = configStorage.getMsgAuditSdk();
if (cachedSdk > 0) {
return cachedSdk;
}
}
// SDK未初始化或已过期需要重新初始化
String configPath = configStorage.getMsgAuditLibPath();
if (StringUtils.isEmpty(configPath)) {
throw new WxErrorException("请配置会话存档sdk文件的路径不要配错了");
}
/**
* 完整的文件库路径:
*
* /www/osfile/libcrypto-1_1-x64.dll,libssl-1_1-x64.dll,libcurl-x64.dll,WeWorkFinanceSdk.dll,
* libWeWorkFinanceSdk_Java.so
*/
// 替换斜杠
String replacePath = configPath.replace("\\", "/");
// 获取最后一个斜杠的下标,用作分割路径
@@ -79,36 +119,22 @@ public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService {
Finance.loadingLibraries(osLib, prefixPath);
long sdk = Finance.NewSdk();
//因为会话存档单独有个secret,优先使用会话存档的secret
String msgAuditSecret = cpService.getWxCpConfigStorage().getMsgAuditSecret();
// 因为会话存档单独有个secret,优先使用会话存档的secret
String msgAuditSecret = configStorage.getMsgAuditSecret();
if (StringUtils.isEmpty(msgAuditSecret)) {
msgAuditSecret = cpService.getWxCpConfigStorage().getCorpSecret();
msgAuditSecret = configStorage.getCorpSecret();
}
long ret = Finance.Init(sdk, cpService.getWxCpConfigStorage().getCorpId(), msgAuditSecret);
long ret = Finance.Init(sdk, configStorage.getCorpId(), msgAuditSecret);
if (ret != 0) {
Finance.DestroySdk(sdk);
throw new WxErrorException("init sdk err ret " + ret);
}
long slice = Finance.NewSlice();
ret = Finance.GetChatData(sdk, seq, limit, proxy, passwd, timeout, slice);
if (ret != 0) {
Finance.FreeSlice(slice);
Finance.DestroySdk(sdk);
throw new WxErrorException("getchatdata err ret " + ret);
}
// 缓存SDK
configStorage.updateMsgAuditSdk(sdk, SDK_EXPIRES_TIME);
log.debug("初始化会话存档SDK成功sdk={}", sdk);
// 拉取会话存档
String content = Finance.GetContentFromSlice(slice);
Finance.FreeSlice(slice);
WxCpChatDatas chatDatas = WxCpChatDatas.fromJson(content);
if (chatDatas.getErrCode().intValue() != 0) {
Finance.DestroySdk(sdk);
throw new WxErrorException(chatDatas.toJson());
}
chatDatas.setSdk(sdk);
return chatDatas;
return sdk;
}
@Override
@@ -128,36 +154,27 @@ public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService {
* @throws Exception the exception
*/
public String decryptChatData(long sdk, WxCpChatDatas.WxCpChatData chatData, Integer pkcs1) throws Exception {
/**
* 企业获取的会话内容,使用企业自行配置的消息加密公钥进行加密,企业可用自行保存的私钥解开会话内容数据。
* msgAuditPriKey 会话存档私钥不能为空
*/
// 企业获取的会话内容,使用企业自行配置的消息加密公钥进行加密,企业可用自行保存的私钥解开会话内容数据。
// msgAuditPriKey 会话存档私钥不能为空
String priKey = cpService.getWxCpConfigStorage().getMsgAuditPriKey();
if (StringUtils.isEmpty(priKey)) {
throw new WxErrorException("请配置会话存档私钥【msgAuditPriKey】");
}
String decryptByPriKey = WxCpCryptUtil.decryptPriKey(chatData.getEncryptRandomKey(), priKey, pkcs1);
/**
* 每次使用DecryptData解密会话存档前需要调用NewSlice获取一个slice在使用完slice中数据后还需要调用FreeSlice释放。
*/
// 每次使用DecryptData解密会话存档前需要调用NewSlice获取一个slice在使用完slice中数据后还需要调用FreeSlice释放。
long msg = Finance.NewSlice();
/**
* 解密会话存档内容
* sdk不会要求用户传入rsa私钥保证用户会话存档数据只有自己能够解密。
* 此处需要用户先用rsa私钥解密encrypt_random_key后作为encrypt_key参数传入sdk来解密encrypt_chat_msg获取会话存档明文。
*/
// 解密会话存档内容
// sdk不会要求用户传入rsa私钥保证用户会话存档数据只有自己能够解密。
// 此处需要用户先用rsa私钥解密encrypt_random_key后作为encrypt_key参数传入sdk来解密encrypt_chat_msg获取会话存档明文。
int ret = Finance.DecryptData(sdk, decryptByPriKey, chatData.getEncryptChatMsg(), msg);
if (ret != 0) {
Finance.FreeSlice(msg);
Finance.DestroySdk(sdk);
throw new WxErrorException("msg err ret " + ret);
}
/**
* 明文
*/
// 明文
String plainText = Finance.GetContentFromSlice(msg);
Finance.FreeSlice(msg);
return plainText;
@@ -209,7 +226,6 @@ public class WxCpMsgAuditServiceImpl implements WxCpMsgAuditService {
ret = Finance.GetMediaData(sdk, indexbuf, sdkfileid, proxy, passwd, timeout, mediaData);
if (ret != 0) {
Finance.FreeMediaData(mediaData);
Finance.DestroySdk(sdk);
throw new WxErrorException("getmediadata err ret " + ret);
}

View File

@@ -26,6 +26,12 @@ public class WxCpContactWayList extends WxCpBaseResp implements Serializable {
@SerializedName("contact_way")
private List<ContactWay> contactWay;
/**
* 分页参数,用于查询下一个分页的数据,为空时表示没有更多的分页
*/
@SerializedName("next_cursor")
private String nextCursor;
/**
* The type Contact way.
*/

View File

@@ -0,0 +1,77 @@
package me.chanjar.weixin.cp.bean.intelligentrobot;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import me.chanjar.weixin.cp.bean.WxCpBaseResp;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
import java.io.Serializable;
/**
* 智能机器人信息
*
* @author Binary Wang
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WxCpIntelligentRobot extends WxCpBaseResp implements Serializable {
private static final long serialVersionUID = -1L;
/**
* 机器人ID
*/
@SerializedName("robot_id")
private String robotId;
/**
* 机器人名称
*/
@SerializedName("name")
private String name;
/**
* 机器人描述
*/
@SerializedName("description")
private String description;
/**
* 机器人头像
*/
@SerializedName("avatar")
private String avatar;
/**
* 机器人状态 0:停用 1:启用
*/
@SerializedName("status")
private Integer status;
/**
* 创建时间
*/
@SerializedName("create_time")
private Long createTime;
/**
* 更新时间
*/
@SerializedName("update_time")
private Long updateTime;
/**
* From json wx cp intelligent robot.
*
* @param json the json
* @return the wx cp intelligent robot
*/
public static WxCpIntelligentRobot fromJson(String json) {
return WxCpGsonBuilder.create().fromJson(json, WxCpIntelligentRobot.class);
}
public String toJson() {
return WxCpGsonBuilder.create().toJson(this);
}
}

View File

@@ -0,0 +1,49 @@
package me.chanjar.weixin.cp.bean.intelligentrobot;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
import java.io.Serializable;
/**
* 智能机器人聊天请求
*
* @author Binary Wang
*/
@Data
public class WxCpIntelligentRobotChatRequest implements Serializable {
private static final long serialVersionUID = -1L;
/**
* 机器人ID
*/
@SerializedName("robot_id")
private String robotId;
/**
* 用户ID
*/
@SerializedName("userid")
private String userid;
/**
* 消息内容
*/
@SerializedName("message")
private String message;
/**
* 会话ID可选用于保持会话连续性
*/
@SerializedName("session_id")
private String sessionId;
public String toJson() {
return WxCpGsonBuilder.create().toJson(this);
}
public static WxCpIntelligentRobotChatRequest fromJson(String json) {
return WxCpGsonBuilder.create().fromJson(json, WxCpIntelligentRobotChatRequest.class);
}
}

View File

@@ -0,0 +1,46 @@
package me.chanjar.weixin.cp.bean.intelligentrobot;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import me.chanjar.weixin.cp.bean.WxCpBaseResp;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
import java.io.Serializable;
/**
* 智能机器人聊天响应
*
* @author Binary Wang
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WxCpIntelligentRobotChatResponse extends WxCpBaseResp implements Serializable {
private static final long serialVersionUID = -1L;
/**
* 机器人回复内容
*/
@SerializedName("reply")
private String reply;
/**
* 会话ID
*/
@SerializedName("session_id")
private String sessionId;
/**
* 消息ID
*/
@SerializedName("msg_id")
private String msgId;
public static WxCpIntelligentRobotChatResponse fromJson(String json) {
return WxCpGsonBuilder.create().fromJson(json, WxCpIntelligentRobotChatResponse.class);
}
public String toJson() {
return WxCpGsonBuilder.create().toJson(this);
}
}

View File

@@ -0,0 +1,43 @@
package me.chanjar.weixin.cp.bean.intelligentrobot;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
import java.io.Serializable;
/**
* 创建智能机器人请求
*
* @author Binary Wang
*/
@Data
public class WxCpIntelligentRobotCreateRequest implements Serializable {
private static final long serialVersionUID = -1L;
/**
* 机器人名称
*/
@SerializedName("name")
private String name;
/**
* 机器人描述
*/
@SerializedName("description")
private String description;
/**
* 机器人头像
*/
@SerializedName("avatar")
private String avatar;
public String toJson() {
return WxCpGsonBuilder.create().toJson(this);
}
public static WxCpIntelligentRobotCreateRequest fromJson(String json) {
return WxCpGsonBuilder.create().fromJson(json, WxCpIntelligentRobotCreateRequest.class);
}
}

View File

@@ -0,0 +1,34 @@
package me.chanjar.weixin.cp.bean.intelligentrobot;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import me.chanjar.weixin.cp.bean.WxCpBaseResp;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
import java.io.Serializable;
/**
* 创建智能机器人响应
*
* @author Binary Wang
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class WxCpIntelligentRobotCreateResponse extends WxCpBaseResp implements Serializable {
private static final long serialVersionUID = -1L;
/**
* 机器人ID
*/
@SerializedName("robot_id")
private String robotId;
public static WxCpIntelligentRobotCreateResponse fromJson(String json) {
return WxCpGsonBuilder.create().fromJson(json, WxCpIntelligentRobotCreateResponse.class);
}
public String toJson() {
return WxCpGsonBuilder.create().toJson(this);
}
}

View File

@@ -0,0 +1,55 @@
package me.chanjar.weixin.cp.bean.intelligentrobot;
import com.google.gson.annotations.SerializedName;
import lombok.Data;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
import java.io.Serializable;
/**
* 更新智能机器人请求
*
* @author Binary Wang
*/
@Data
public class WxCpIntelligentRobotUpdateRequest implements Serializable {
private static final long serialVersionUID = -1L;
/**
* 机器人ID
*/
@SerializedName("robot_id")
private String robotId;
/**
* 机器人名称
*/
@SerializedName("name")
private String name;
/**
* 机器人描述
*/
@SerializedName("description")
private String description;
/**
* 机器人头像
*/
@SerializedName("avatar")
private String avatar;
/**
* 机器人状态 0:停用 1:启用
*/
@SerializedName("status")
private Integer status;
public String toJson() {
return WxCpGsonBuilder.create().toJson(this);
}
public static WxCpIntelligentRobotUpdateRequest fromJson(String json) {
return WxCpGsonBuilder.create().fromJson(json, WxCpIntelligentRobotUpdateRequest.class);
}
}

View File

@@ -252,6 +252,12 @@ public class WxCpGroupRobotMessage implements Serializable {
messageJson.add("markdown", text);
break;
}
case MARKDOWN_V2: {
JsonObject text = new JsonObject();
text.addProperty("content", this.getContent());
messageJson.add("markdown_v2", text);
break;
}
case IMAGE: {
JsonObject text = new JsonObject();
text.addProperty("base64", this.getBase64());

View File

@@ -44,7 +44,13 @@ public class WxCpOaApplyEventRequest implements Serializable {
private Integer chooseDepartment;
/**
* 审批流程信息用于指定审批申请的审批流程支持单人审批、多人会签、多人或签可能有多个审批节点仅use_template_approver为0时生效。
* 审批流程信息(新版流程列表)用于指定审批申请的审批流程支持单人审批、多人会签、多人或签可能有多个审批节点仅use_template_approver为0时生效。
*/
@SerializedName("process")
private Process process;
/**
* 审批流程信息旧版用于指定审批申请的审批流程支持单人审批、多人会签、多人或签可能有多个审批节点仅use_template_approver为0时生效。
*/
@SerializedName("approver")
private List<Approver> approvers;
@@ -118,4 +124,46 @@ public class WxCpOaApplyEventRequest implements Serializable {
private List<ApplyDataContent> contents;
}
/**
* 审批流程信息(新版).
*/
@Data
@Accessors(chain = true)
public static class Process implements Serializable {
private static final long serialVersionUID = 4758206091546930988L;
/**
* 审批流程节点列表当use_template_approver为0时必填
*/
@SerializedName("node_list")
private List<ProcessNode> nodeList;
}
/**
* 审批流程节点.
*/
@Data
@Accessors(chain = true)
public static class ProcessNode implements Serializable {
private static final long serialVersionUID = 1758206091546930988L;
/**
* 节点类型1-审批人2-抄送人3-抄送人
*/
@SerializedName("type")
private Integer type;
/**
* 多人审批方式1-全签2-或签3-依次审批
*/
@SerializedName("apv_rel")
private Integer apvRel;
/**
* 审批节点审批人userid列表若为多人会签、多人或签需填写每个人的userid
*/
@SerializedName("userid")
private String[] userIds;
}
}

View File

@@ -260,7 +260,36 @@ public interface WxCpConfigStorage {
/**
* 获取会话存档的secret
*
* @return msg audit secret
*/
String getMsgAuditSecret();
/**
* 获取会话存档SDK
* 会话存档SDK初始化后有效期为7200秒无需每次重新初始化
*
* @return sdk id如果未初始化或已过期返回0
*/
long getMsgAuditSdk();
/**
* 检查会话存档SDK是否已过期
*
* @return true: 已过期, false: 未过期
*/
boolean isMsgAuditSdkExpired();
/**
* 更新会话存档SDK
*
* @param sdk sdk id
* @param expiresInSeconds 过期时间(秒)
*/
void updateMsgAuditSdk(long sdk, int expiresInSeconds);
/**
* 使会话存档SDK过期
*/
void expireMsgAuditSdk();
}

View File

@@ -49,6 +49,11 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
private volatile String msgAuditSecret;
private volatile String msgAuditPriKey;
private volatile String msgAuditLibPath;
/**
* 会话存档SDK及其过期时间
*/
private volatile long msgAuditSdk;
private volatile long msgAuditSdkExpiresTime;
private volatile String oauth2redirectUri;
private volatile String httpProxyHost;
private volatile int httpProxyPort;
@@ -444,10 +449,34 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
/**
* 设置会话存档secret
* @param msgAuditSecret
*
* @param msgAuditSecret the msg audit secret
* @return this
*/
public WxCpDefaultConfigImpl setMsgAuditSecret(String msgAuditSecret) {
this.msgAuditSecret = msgAuditSecret;
return this;
}
@Override
public long getMsgAuditSdk() {
return this.msgAuditSdk;
}
@Override
public boolean isMsgAuditSdkExpired() {
return System.currentTimeMillis() > this.msgAuditSdkExpiresTime;
}
@Override
public synchronized void updateMsgAuditSdk(long sdk, int expiresInSeconds) {
this.msgAuditSdk = sdk;
// 预留200秒的时间
this.msgAuditSdkExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
}
@Override
public void expireMsgAuditSdk() {
this.msgAuditSdkExpiresTime = 0;
}
}

View File

@@ -50,6 +50,11 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage {
private volatile File tmpDirFile;
private volatile ApacheHttpClientBuilder apacheHttpClientBuilder;
private volatile String webhookKey;
/**
* 会话存档SDK及其过期时间SDK是本地JVM变量不适合存储到Redis
*/
private volatile long msgAuditSdk;
private volatile long msgAuditSdkExpiresTime;
/**
* Instantiates a new Wx cp redis config.
@@ -470,4 +475,26 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage {
public String getMsgAuditSecret() {
return null;
}
@Override
public long getMsgAuditSdk() {
return this.msgAuditSdk;
}
@Override
public boolean isMsgAuditSdkExpired() {
return System.currentTimeMillis() > this.msgAuditSdkExpiresTime;
}
@Override
public synchronized void updateMsgAuditSdk(long sdk, int expiresInSeconds) {
this.msgAuditSdk = sdk;
// 预留200秒的时间
this.msgAuditSdkExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L;
}
@Override
public void expireMsgAuditSdk() {
this.msgAuditSdkExpiresTime = 0;
}
}

View File

@@ -1631,4 +1631,40 @@ public interface WxCpApiPathConsts {
*/
String CONVERT_TMP_EXTERNAL_USER_ID = "/cgi-bin/idconvert/convert_tmp_external_userid";
}
/**
* 智能机器人相关接口
* 官方文档: https://developer.work.weixin.qq.com/document/path/101039
*/
interface IntelligentRobot {
/**
* 创建智能机器人
*/
String CREATE_ROBOT = "/cgi-bin/intelligent_robot/create";
/**
* 删除智能机器人
*/
String DELETE_ROBOT = "/cgi-bin/intelligent_robot/delete";
/**
* 更新智能机器人
*/
String UPDATE_ROBOT = "/cgi-bin/intelligent_robot/update";
/**
* 查询智能机器人
*/
String GET_ROBOT = "/cgi-bin/intelligent_robot/get";
/**
* 智能机器人会话
*/
String CHAT = "/cgi-bin/intelligent_robot/chat";
/**
* 重置智能机器人会话
*/
String RESET_SESSION = "/cgi-bin/intelligent_robot/reset_session";
}
}

View File

@@ -630,6 +630,11 @@ public class WxCpConsts {
*/
public static final String MARKDOWN = "markdown";
/**
* markdown_v2消息.
*/
public static final String MARKDOWN_V2 = "markdown_v2";
/**
* 图文消息(点击跳转到外链).
*/

View File

@@ -1,9 +1,12 @@
package me.chanjar.weixin.cp.util.json;
import com.google.gson.ExclusionStrategy;
import com.google.gson.FieldAttributes;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import me.chanjar.weixin.common.bean.menu.WxMenu;
import me.chanjar.weixin.common.error.WxError;
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
import me.chanjar.weixin.common.util.json.WxErrorAdapter;
import me.chanjar.weixin.cp.bean.WxCpChat;
import me.chanjar.weixin.cp.bean.WxCpDepart;
@@ -11,6 +14,7 @@ import me.chanjar.weixin.cp.bean.WxCpTag;
import me.chanjar.weixin.cp.bean.WxCpUser;
import me.chanjar.weixin.cp.bean.kf.WxCpKfGetCorpStatisticResp;
import java.io.File;
import java.util.Objects;
/**
@@ -32,6 +36,18 @@ public class WxCpGsonBuilder {
INSTANCE.registerTypeAdapter(WxMenu.class, new WxCpMenuGsonAdapter());
INSTANCE.registerTypeAdapter(WxCpTag.class, new WxCpTagGsonAdapter());
INSTANCE.registerTypeAdapter(WxCpKfGetCorpStatisticResp.StatisticList.class, new StatisticListAdapter());
INSTANCE.setExclusionStrategies(new ExclusionStrategy() {
@Override
public boolean shouldSkipField(FieldAttributes fieldAttributes) {
return false;
}
@Override
public boolean shouldSkipClass(Class<?> aClass) {
return aClass == File.class || aClass == ApacheHttpClientBuilder.class;
}
});
}
/**

View File

@@ -281,7 +281,12 @@ public class WxCpUserGsonAdapter implements JsonDeserializer<WxCpUser>, JsonSeri
}
addProperty(o, MAIN_DEPARTMENT, user.getMainDepartment());
addArrayProperty(o, DIRECT_LEADER, user.getDirectLeader());
// Special handling for directLeader: include empty arrays to support WeChat Work API reset functionality
if (user.getDirectLeader() != null) {
JsonArray directLeaderArray = new JsonArray();
Arrays.stream(user.getDirectLeader()).forEach(directLeaderArray::add);
o.add(DIRECT_LEADER, directLeaderArray);
}
if (!user.getExtAttrs().isEmpty()) {
JsonArray attrsJsonArray = new JsonArray();

View File

@@ -64,6 +64,51 @@ public class WxCpGroupRobotServiceImplTest {
robotService.sendMarkdown(content);
}
/**
* Test send mark down v2.
*
* @throws WxErrorException the wx error exception
*/
@Test
public void testSendMarkDownV2() throws WxErrorException {
String content = "# 一、标题\n" +
"## 二级标题\n" +
"### 三级标题\n" +
"# 二、字体\n" +
"*斜体*\n" +
"\n" +
"**加粗**\n" +
"# 三、列表 \n" +
"- 无序列表 1 \n" +
"- 无序列表 2\n" +
" - 无序列表 2.1\n" +
" - 无序列表 2.2\n" +
"1. 有序列表 1\n" +
"2. 有序列表 2\n" +
"# 四、引用\n" +
"> 一级引用\n" +
">>二级引用\n" +
">>>三级引用\n" +
"# 五、链接\n" +
"[这是一个链接](https://work.weixin.qq.com/api/doc)\n" +
"![](https://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png)\n" +
"# 六、分割线\n" +
"\n" +
"---\n" +
"# 七、代码\n" +
"`这是行内代码`\n" +
"```\n" +
"这是独立代码块\n" +
"```\n" +
"\n" +
"# 八、表格\n" +
"| 姓名 | 文化衫尺寸 | 收货地址 |\n" +
"| :----- | :----: | -------: |\n" +
"| 张三 | S | 广州 |\n" +
"| 李四 | L | 深圳 |";
robotService.sendMarkdownV2(content);
}
/**
* Test send image.
*

View File

@@ -0,0 +1,88 @@
package me.chanjar.weixin.cp.api.impl;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.api.ApiTestModule;
import me.chanjar.weixin.cp.bean.intelligentrobot.*;
import org.testng.annotations.Guice;
import org.testng.annotations.Test;
import javax.inject.Inject;
/**
* 智能机器人接口测试
*
* @author Binary Wang
*/
@Test
@Guice(modules = ApiTestModule.class)
public class WxCpIntelligentRobotServiceImplTest {
@Inject
private WxCpService wxCpService;
@Test
public void testCreateRobot() {
// 测试创建智能机器人请求对象创建
WxCpIntelligentRobotCreateRequest request = new WxCpIntelligentRobotCreateRequest();
request.setName("测试机器人");
request.setDescription("这是一个测试的智能机器人");
request.setAvatar("avatar_url");
// 验证JSON序列化
String json = request.toJson();
assert json.contains("测试机器人");
assert json.contains("这是一个测试的智能机器人");
// 验证反序列化
WxCpIntelligentRobotCreateRequest fromJson = WxCpIntelligentRobotCreateRequest.fromJson(json);
assert fromJson.getName().equals("测试机器人");
}
@Test
public void testChatRequest() {
// 测试聊天请求对象创建
WxCpIntelligentRobotChatRequest request = new WxCpIntelligentRobotChatRequest();
request.setRobotId("robot123");
request.setUserid("user123");
request.setMessage("你好,机器人");
request.setSessionId("session123");
// 验证JSON序列化
String json = request.toJson();
assert json.contains("robot123");
assert json.contains("你好,机器人");
// 验证反序列化
WxCpIntelligentRobotChatRequest fromJson = WxCpIntelligentRobotChatRequest.fromJson(json);
assert fromJson.getRobotId().equals("robot123");
assert fromJson.getMessage().equals("你好,机器人");
}
@Test
public void testUpdateRequest() {
// 测试更新请求对象创建
WxCpIntelligentRobotUpdateRequest request = new WxCpIntelligentRobotUpdateRequest();
request.setRobotId("robot123");
request.setName("更新后的机器人");
request.setDescription("更新后的描述");
request.setStatus(1);
// 验证JSON序列化
String json = request.toJson();
assert json.contains("robot123");
assert json.contains("更新后的机器人");
// 验证反序列化
WxCpIntelligentRobotUpdateRequest fromJson = WxCpIntelligentRobotUpdateRequest.fromJson(json);
assert fromJson.getRobotId().equals("robot123");
assert fromJson.getName().equals("更新后的机器人");
assert fromJson.getStatus().equals(1);
}
@Test
public void testServiceIntegration() {
// 验证服务可以正确获取
assert this.wxCpService.getIntelligentRobotService() != null;
assert this.wxCpService.getIntelligentRobotService() instanceof WxCpIntelligentRobotServiceImpl;
}
}

View File

@@ -95,4 +95,99 @@ public class WxCpOaApplyEventRequestTest {
assertThat(request.toJson()).isEqualTo(GsonParser.parse(json).toString());
}
/**
* Test to json with process.
*/
@Test
public void testToJsonWithProcess() {
String json = "{\n" +
" \"creator_userid\": \"WangXiaoMing\",\n" +
" \"template_id\": \"3Tka1eD6v6JfzhDMqPd3aMkFdxqtJMc2ZRioeFXkaaa\",\n" +
" \"use_template_approver\":0,\n" +
" \"process\": {\n" +
" \"node_list\": [\n" +
" {\n" +
" \"type\": 1,\n" +
" \"apv_rel\": 2,\n" +
" \"userid\": [\"WuJunJie\",\"WangXiaoMing\"]\n" +
" },\n" +
" {\n" +
" \"type\": 1,\n" +
" \"apv_rel\": 1,\n" +
" \"userid\": [\"LiuXiaoGang\"]\n" +
" },\n" +
" {\n" +
" \"type\": 2,\n" +
" \"userid\": [\"ZhangSan\",\"LiSi\"]\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"apply_data\": {\n" +
" \"contents\": [\n" +
" {\n" +
" \"control\": \"Text\",\n" +
" \"id\": \"Text-15111111111\",\n" +
" \"value\": {\n" +
" \"text\": \"文本填写的内容\"\n" +
" }\n" +
" }\n" +
" ]\n" +
" },\n" +
" \"summary_list\": [\n" +
" {\n" +
" \"summary_info\": [{\n" +
" \"text\": \"摘要第1行\",\n" +
" \"lang\": \"zh_CN\"\n" +
" }]\n" +
" },\n" +
" {\n" +
" \"summary_info\": [{\n" +
" \"text\": \"摘要第2行\",\n" +
" \"lang\": \"zh_CN\"\n" +
" }]\n" +
" },\n" +
" {\n" +
" \"summary_info\": [{\n" +
" \"text\": \"摘要第3行\",\n" +
" \"lang\": \"zh_CN\"\n" +
" }]\n" +
" }\n" +
" ]\n" +
"}";
WxCpOaApplyEventRequest request = new WxCpOaApplyEventRequest();
request.setCreatorUserId("WangXiaoMing")
.setTemplateId("3Tka1eD6v6JfzhDMqPd3aMkFdxqtJMc2ZRioeFXkaaa")
.setUseTemplateApprover(0)
.setProcess(new WxCpOaApplyEventRequest.Process()
.setNodeList(Arrays.asList(
new WxCpOaApplyEventRequest.ProcessNode()
.setType(1)
.setApvRel(2)
.setUserIds(new String[]{"WuJunJie", "WangXiaoMing"}),
new WxCpOaApplyEventRequest.ProcessNode()
.setType(1)
.setApvRel(1)
.setUserIds(new String[]{"LiuXiaoGang"}),
new WxCpOaApplyEventRequest.ProcessNode()
.setType(2)
.setUserIds(new String[]{"ZhangSan", "LiSi"})
)))
.setApplyData(new WxCpOaApplyEventRequest.ApplyData()
.setContents(Collections.singletonList(new ApplyDataContent()
.setControl("Text").setId("Text-15111111111").setValue(new ContentValue().setText("文本填写的内容")))))
.setSummaryList(Arrays.asList(new SummaryInfo()
.setSummaryInfoData(Collections.singletonList(new SummaryInfo.SummaryInfoData().setLang("zh_CN").setText(
"摘要第1行"))),
new SummaryInfo()
.setSummaryInfoData(Collections.singletonList(new SummaryInfo.SummaryInfoData().setLang("zh_CN").setText(
"摘要第2行"))),
new SummaryInfo()
.setSummaryInfoData(Collections.singletonList(new SummaryInfo.SummaryInfoData().setLang("zh_CN").setText(
"摘要第3行")))))
;
assertThat(request.toJson()).isEqualTo(GsonParser.parse(json).toString());
}
}

View File

@@ -0,0 +1,189 @@
package me.chanjar.weixin.cp.demo;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.bean.oa.WxCpApprovalDetailResult;
import me.chanjar.weixin.cp.bean.oa.WxCpApprovalInfo;
import me.chanjar.weixin.cp.bean.oa.WxCpOaApplyEventRequest;
import me.chanjar.weixin.cp.bean.oa.applydata.ApplyDataContent;
import me.chanjar.weixin.cp.bean.oa.applydata.ContentValue;
import java.util.Arrays;
import java.util.Date;
/**
* 企业微信流程审批功能演示代码
* WeChat Enterprise Workflow Approval Demo
*
* 演示如何使用WxJava SDK中已实现的完整审批流程功能
* Demonstrates how to use the comprehensive approval workflow features already implemented in WxJava SDK
*
* 文档参考 Documentation Reference: https://work.weixin.qq.com/api/doc/90000/90135/91853
*/
public class WxCpApprovalWorkflowDemo {
private WxCpService wxCpService;
public WxCpApprovalWorkflowDemo(WxCpService wxCpService) {
this.wxCpService = wxCpService;
}
/**
* 示例1: 提交审批申请
* Example 1: Submit Approval Application
* API: /cgi-bin/oa/applyevent (Document 91853)
*/
public String submitApprovalApplication() throws Exception {
// 构建审批申请请求
WxCpOaApplyEventRequest request = new WxCpOaApplyEventRequest()
.setCreatorUserId("creator_user_id") // 申请人userid
.setTemplateId("3Tka1eD6v6JfzhDMqPd3aMkFdxqtJMc2ZRioUBGCNS") // 模板id
.setUseTemplateApprover(0) // 不使用模板中的审批流
.setApprovers(Arrays.asList(
new WxCpOaApplyEventRequest.Approver()
.setAttr(2) // 审批类型: 或签
.setUserIds(new String[]{"approver1", "approver2"})
))
.setNotifiers(new String[]{"notifier1", "notifier2"}) // 抄送人
.setNotifyType(1) // 抄送方式: 提单时抄送
.setApplyData(new WxCpOaApplyEventRequest.ApplyData()
.setContents(Arrays.asList(
// 文本控件
new ApplyDataContent()
.setControl("Text")
.setId("Text-1234567890")
.setValue(new ContentValue().setText("这是一个审批申请的文本内容")),
// 数字控件
new ApplyDataContent()
.setControl("Number")
.setId("Number-1234567890")
.setValue(new ContentValue().setNewNumber("1000")),
// 金额控件
new ApplyDataContent()
.setControl("Money")
.setId("Money-1234567890")
.setValue(new ContentValue().setNewMoney("10000"))
))
);
// 提交审批申请
String spNo = wxCpService.getOaService().apply(request);
System.out.println("审批申请提交成功,审批单号: " + spNo);
return spNo;
}
/**
* 示例2: 获取审批申请详情
* Example 2: Get Approval Application Details
* API: /cgi-bin/oa/getapprovaldetail
*/
public void getApprovalDetails(String spNo) throws Exception {
WxCpApprovalDetailResult result = wxCpService.getOaService().getApprovalDetail(spNo);
WxCpApprovalDetailResult.WxCpApprovalDetail detail = result.getInfo();
System.out.println("审批单号: " + detail.getSpNo());
System.out.println("审批名称: " + detail.getSpName());
System.out.println("审批状态: " + detail.getSpStatus().getCode());
System.out.println("申请人: " + detail.getApplyer().getUserId());
System.out.println("申请时间: " + detail.getApplyTime());
// 打印审批记录
if (detail.getSpRecord() != null) {
detail.getSpRecord().forEach(record -> {
System.out.println("审批节点状态: " + record.getSpStatus());
System.out.println("审批人: " + record.getDetails());
});
}
}
/**
* 示例3: 批量获取审批单号
* Example 3: Batch Get Approval Numbers
* API: /cgi-bin/oa/getapprovalinfo
*/
public void batchGetApprovalInfo() throws Exception {
// 获取最近7天的审批单
Date startTime = new Date(System.currentTimeMillis() - 7 * 24 * 60 * 60 * 1000L);
Date endTime = new Date();
WxCpApprovalInfo approvalInfo = wxCpService.getOaService()
.getApprovalInfo(startTime, endTime, "0", 100, null);
System.out.println("获取到的审批单数量: " + approvalInfo.getCount());
// 遍历审批单号
if (approvalInfo.getSpNoList() != null) {
approvalInfo.getSpNoList().forEach(spNo -> {
System.out.println("审批单号: " + spNo);
// 可以进一步调用 getApprovalDetails 获取详情
});
}
}
/**
* 示例4: 模板管理
* Example 4: Template Management
*/
public void templateManagement() throws Exception {
// 获取模板详情
String templateId = "3Tka1eD6v6JfzhDMqPd3aMkFdxqtJMc2ZRioUBGCNS";
var templateResult = wxCpService.getOaService().getTemplateDetail(templateId);
System.out.println("模板名称: " + templateResult.getTemplateNames());
System.out.println("模板内容: " + templateResult.getTemplateContent());
}
/**
* 完整的审批流程演示
* Complete Approval Workflow Demo
*/
public void completeWorkflowDemo() {
try {
System.out.println("=== 企业微信流程审批完整演示 ===");
// 1. 提交审批申请
System.out.println("\n1. 提交审批申请...");
String spNo = submitApprovalApplication();
// 2. 获取审批详情
System.out.println("\n2. 获取审批详情...");
getApprovalDetails(spNo);
// 3. 批量获取审批信息
System.out.println("\n3. 批量获取审批信息...");
batchGetApprovalInfo();
// 4. 模板管理
System.out.println("\n4. 模板管理...");
templateManagement();
System.out.println("\n=== 演示完成 ===");
System.out.println("WxJava SDK 已经完整支持企业微信流程审批功能!");
} catch (Exception e) {
System.err.println("演示过程中发生错误: " + e.getMessage());
e.printStackTrace();
}
}
public static void main(String[] args) {
// 注意: 这里需要配置真实的企业微信服务
// Note: You need to configure real WeChat Enterprise service here
System.out.println("企业微信流程审批功能演示");
System.out.println("该演示代码展示了WxJava SDK中已经完整实现的审批流程功能");
System.out.println("包括文档91853中描述的所有核心功能");
System.out.println("");
System.out.println("主要功能:");
System.out.println("- 提交审批申请 (/cgi-bin/oa/applyevent)");
System.out.println("- 获取审批详情 (/cgi-bin/oa/getapprovaldetail)");
System.out.println("- 批量获取审批单号 (/cgi-bin/oa/getapprovalinfo)");
System.out.println("- 模板管理功能");
System.out.println("- 审批流程引擎支持");
System.out.println("");
System.out.println("如需运行演示,请配置正确的企业微信服务参数。");
}
}

View File

@@ -180,4 +180,31 @@ public class WxCpUserGsonAdapterTest {
"{\"type\":2,\"name\":\"测试app\"," +
"\"miniprogram\":{\"appid\":\"wx8bd80126147df384\",\"pagepath\":\"/index\",\"title\":\"my miniprogram\"}}]}}");
}
/**
* Test directLeader empty array serialization.
* This test verifies that empty directLeader arrays are included in JSON as "direct_leader":[]
* instead of being omitted, which is required for WeChat Work API to reset user direct leaders.
*/
@Test
public void testDirectLeaderEmptyArraySerialization() {
WxCpUser user = new WxCpUser();
user.setUserId("testuser");
user.setName("Test User");
// Test with empty array - should be serialized as "direct_leader":[]
user.setDirectLeader(new String[]{});
String json = user.toJson();
assertThat(json).contains("\"direct_leader\":[]");
// Test with null - should not include direct_leader field
user.setDirectLeader(null);
json = user.toJson();
assertThat(json).doesNotContain("direct_leader");
// Test with non-empty array - should be serialized normally
user.setDirectLeader(new String[]{"leader1", "leader2"});
json = user.toJson();
assertThat(json).contains("\"direct_leader\":[\"leader1\",\"leader2\"]");
}
}

View File

@@ -7,7 +7,7 @@
<parent>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java</artifactId>
<version>4.7.8.B</version>
<version>4.7.9.B</version>
</parent>
<artifactId>weixin-java-miniapp</artifactId>

View File

@@ -0,0 +1,159 @@
package cn.binarywang.wx.miniapp.api;
import cn.binarywang.wx.miniapp.bean.complaint.*;
import me.chanjar.weixin.common.error.WxErrorException;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
/**
* 小程序交易投诉接口
*
* @author <a href="https://github.com/binarywang">Binary Wang</a>
* created on 2025-01-01
*/
public interface WxMaComplaintService {
/**
* <pre>
* 查询投诉单列表API
* 商户可通过调用此接口,查询指定时间段的所有用户投诉信息,以分页输出查询结果。
* 文档详见: https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/guarantee/complaint.html
* </pre>
*
* @param request {@link WxMaComplaintRequest} 查询投诉单列表请求数据
* @return {@link WxMaComplaintResult} 微信返回的投诉单列表
* @throws WxErrorException the wx error exception
*/
WxMaComplaintResult queryComplaints(WxMaComplaintRequest request) throws WxErrorException;
/**
* <pre>
* 查询投诉单详情API
* 商户可通过调用此接口,查询指定投诉单的用户投诉详情,包含投诉内容、投诉关联订单、投诉人联系方式等信息,方便商户处理投诉。
* 文档详见: https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/guarantee/complaint.html
* </pre>
*
* @param request {@link WxMaComplaintDetailRequest} 投诉单详情请求数据
* @return {@link WxMaComplaintDetailResult} 微信返回的投诉单详情
* @throws WxErrorException the wx error exception
*/
WxMaComplaintDetailResult getComplaint(WxMaComplaintDetailRequest request) throws WxErrorException;
/**
* <pre>
* 查询投诉协商历史API
* 商户可通过调用此接口,查询指定投诉的用户商户协商历史,以分页输出查询结果,方便商户根据处理历史来制定后续处理方案。
* 文档详见: https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/guarantee/complaint.html
* </pre>
*
* @param request {@link WxMaNegotiationHistoryRequest} 请求数据
* @return {@link WxMaNegotiationHistoryResult} 微信返回结果
* @throws WxErrorException the wx error exception
*/
WxMaNegotiationHistoryResult queryNegotiationHistorys(WxMaNegotiationHistoryRequest request) throws WxErrorException;
/**
* <pre>
* 创建投诉通知回调地址API
* 商户通过调用此接口创建投诉通知回调URL当用户产生新投诉且投诉状态已变更时微信会通过回调URL通知商户。
* 文档详见: https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/guarantee/complaint.html
* </pre>
*
* @param request {@link WxMaComplaintNotifyUrlRequest} 请求数据
* @return {@link WxMaComplaintNotifyUrlResult} 微信返回结果
* @throws WxErrorException the wx error exception
*/
WxMaComplaintNotifyUrlResult addComplaintNotifyUrl(WxMaComplaintNotifyUrlRequest request) throws WxErrorException;
/**
* <pre>
* 查询投诉通知回调地址API
* 商户通过调用此接口查询投诉通知的回调URL。
* 文档详见: https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/guarantee/complaint.html
* </pre>
*
* @return {@link WxMaComplaintNotifyUrlResult} 微信返回结果
* @throws WxErrorException the wx error exception
*/
WxMaComplaintNotifyUrlResult getComplaintNotifyUrl() throws WxErrorException;
/**
* <pre>
* 更新投诉通知回调地址API
* 商户通过调用此接口更新投诉通知的回调URL。
* 文档详见: https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/guarantee/complaint.html
* </pre>
*
* @param request {@link WxMaComplaintNotifyUrlRequest} 请求数据
* @return {@link WxMaComplaintNotifyUrlResult} 微信返回结果
* @throws WxErrorException the wx error exception
*/
WxMaComplaintNotifyUrlResult updateComplaintNotifyUrl(WxMaComplaintNotifyUrlRequest request) throws WxErrorException;
/**
* <pre>
* 删除投诉通知回调地址API
* 当商户不再需要推送通知时可通过调用此接口删除投诉通知的回调URL取消通知回调。
* 文档详见: https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/guarantee/complaint.html
* </pre>
*
* @throws WxErrorException the wx error exception
*/
void deleteComplaintNotifyUrl() throws WxErrorException;
/**
* <pre>
* 提交回复API
* 商户可通过调用此接口提交回复内容。其中上传图片凭证需首先调用商户上传反馈图片接口得到图片id再将id填入请求。
* 回复可配置文字链,传入跳转链接文案和跳转链接字段,用户点击即可跳转对应页面
* 文档详见: https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/guarantee/complaint.html
* </pre>
*
* @param request {@link WxMaResponseRequest} 请求数据
* @throws WxErrorException the wx error exception
*/
void submitResponse(WxMaResponseRequest request) throws WxErrorException;
/**
* <pre>
* 反馈处理完成API
* 商户可通过调用此接口,反馈投诉单已处理完成。
* 文档详见: https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/guarantee/complaint.html
* </pre>
*
* @param request {@link WxMaCompleteRequest} 请求数据
* @throws WxErrorException the wx error exception
*/
void complete(WxMaCompleteRequest request) throws WxErrorException;
/**
* <pre>
* 商户上传反馈图片API
* 商户可通过调用此接口上传反馈图片凭证,上传成功后可在提交回复时使用。
* 文档详见: https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/guarantee/complaint.html
* </pre>
*
* @param imageFile 需要上传的图片文件
* @return String 微信返回的媒体文件标识Id
* @throws WxErrorException the wx error exception
* @throws IOException IO异常
*/
String uploadResponseImage(File imageFile) throws WxErrorException, IOException;
/**
* <pre>
* 商户上传反馈图片API
* 商户可通过调用此接口上传反馈图片凭证,上传成功后可在提交回复时使用。
* 文档详见: https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/guarantee/complaint.html
* </pre>
*
* @param inputStream 需要上传的图片文件流
* @param fileName 需要上传的图片文件名
* @return String 微信返回的媒体文件标识Id
* @throws WxErrorException the wx error exception
* @throws IOException IO异常
*/
String uploadResponseImage(InputStream inputStream, String fileName) throws WxErrorException, IOException;
}

View File

@@ -0,0 +1,128 @@
package cn.binarywang.wx.miniapp.api;
import cn.binarywang.wx.miniapp.bean.kefu.WxMaKfInfo;
import cn.binarywang.wx.miniapp.bean.kefu.WxMaKfList;
import cn.binarywang.wx.miniapp.bean.kefu.WxMaKfSession;
import cn.binarywang.wx.miniapp.bean.kefu.WxMaKfSessionList;
import cn.binarywang.wx.miniapp.bean.kefu.request.WxMaKfAccountRequest;
import me.chanjar.weixin.common.error.WxErrorException;
/**
* <pre>
* 小程序客服管理接口.
* 不同于 WxMaCustomserviceWorkService (企业微信客服绑定) 和 WxMaMsgService.sendKefuMsg (发送客服消息),
* 此接口专门处理小程序客服账号管理、会话管理等功能。
*
* 注意小程序客服管理接口与公众号客服管理接口在API端点和功能上有所不同。
* </pre>
*
* @author <a href="https://github.com/binarywang">Binary Wang</a>
*/
public interface WxMaKefuService {
/**
* <pre>
* 获取客服基本信息
* 详情请见:<a href="https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/customer-service/customerServiceMessage.getContactList.html">获取客服基本信息</a>
* 接口url格式https://api.weixin.qq.com/cgi-bin/customservice/getkflist?access_token=ACCESS_TOKEN
* </pre>
*
* @return 客服列表
* @throws WxErrorException 异常
*/
WxMaKfList kfList() throws WxErrorException;
/**
* <pre>
* 添加客服账号
* 详情请见:<a href="https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/customer-service/customerServiceMessage.addKfAccount.html">添加客服账号</a>
* 接口url格式https://api.weixin.qq.com/customservice/kfaccount/add?access_token=ACCESS_TOKEN
* </pre>
*
* @param request 客服账号信息
* @return 是否成功
* @throws WxErrorException 异常
*/
boolean kfAccountAdd(WxMaKfAccountRequest request) throws WxErrorException;
/**
* <pre>
* 修改客服账号
* 详情请见:<a href="https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/customer-service/customerServiceMessage.updateKfAccount.html">修改客服账号</a>
* 接口url格式https://api.weixin.qq.com/customservice/kfaccount/update?access_token=ACCESS_TOKEN
* </pre>
*
* @param request 客服账号信息
* @return 是否成功
* @throws WxErrorException 异常
*/
boolean kfAccountUpdate(WxMaKfAccountRequest request) throws WxErrorException;
/**
* <pre>
* 删除客服账号
* 详情请见:<a href="https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/customer-service/customerServiceMessage.deleteKfAccount.html">删除客服账号</a>
* 接口url格式https://api.weixin.qq.com/customservice/kfaccount/del?access_token=ACCESS_TOKEN&kf_account=KFACCOUNT
* </pre>
*
* @param kfAccount 客服账号
* @return 是否成功
* @throws WxErrorException 异常
*/
boolean kfAccountDel(String kfAccount) throws WxErrorException;
/**
* <pre>
* 创建会话
* 详情请见:<a href="https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/customer-service/customerServiceMessage.createSession.html">创建会话</a>
* 接口url格式https://api.weixin.qq.com/customservice/kfsession/create?access_token=ACCESS_TOKEN
* </pre>
*
* @param openid 用户openid
* @param kfAccount 客服账号
* @return 是否成功
* @throws WxErrorException 异常
*/
boolean kfSessionCreate(String openid, String kfAccount) throws WxErrorException;
/**
* <pre>
* 关闭会话
* 详情请见:<a href="https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/customer-service/customerServiceMessage.closeSession.html">关闭会话</a>
* 接口url格式https://api.weixin.qq.com/customservice/kfsession/close?access_token=ACCESS_TOKEN
* </pre>
*
* @param openid 用户openid
* @param kfAccount 客服账号
* @return 是否成功
* @throws WxErrorException 异常
*/
boolean kfSessionClose(String openid, String kfAccount) throws WxErrorException;
/**
* <pre>
* 获取客户的会话状态
* 详情请见:<a href="https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/customer-service/customerServiceMessage.getSession.html">获取客户的会话状态</a>
* 接口url格式https://api.weixin.qq.com/customservice/kfsession/getsession?access_token=ACCESS_TOKEN&openid=OPENID
* </pre>
*
* @param openid 用户openid
* @return 会话信息
* @throws WxErrorException 异常
*/
WxMaKfSession kfSessionGet(String openid) throws WxErrorException;
/**
* <pre>
* 获取客服的会话列表
* 详情请见:<a href="https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/customer-service/customerServiceMessage.getSessionList.html">获取客服的会话列表</a>
* 接口url格式https://api.weixin.qq.com/customservice/kfsession/getsessionlist?access_token=ACCESS_TOKEN&kf_account=KFACCOUNT
* </pre>
*
* @param kfAccount 客服账号
* @return 会话列表
* @throws WxErrorException 异常
*/
WxMaKfSessionList kfSessionList(String kfAccount) throws WxErrorException;
}

View File

@@ -285,6 +285,13 @@ public interface WxMaService extends WxService {
*/
WxMaCustomserviceWorkService getCustomserviceWorkService();
/**
* 获取小程序客服管理服务。
*
* @return 客服管理服务对象WxMaKefuService
*/
WxMaKefuService getKefuService();
/**
* 获取jsapi操作相关服务对象。
*
@@ -605,4 +612,13 @@ public interface WxMaService extends WxService {
* @return 同城配送服务对象WxMaIntracityService
*/
WxMaIntracityService getIntracityService();
/**
* 获取交易投诉服务对象。
* <br>
* 文档https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/guarantee/complaint.html
*
* @return 交易投诉服务对象WxMaComplaintService
*/
WxMaComplaintService getComplaintService();
}

View File

@@ -1,5 +1,6 @@
package cn.binarywang.wx.miniapp.api;
import cn.binarywang.wx.miniapp.bean.WxMaGroupEnterInfo;
import cn.binarywang.wx.miniapp.bean.WxMaShareInfo;
/**
@@ -18,4 +19,16 @@ public interface WxMaShareService {
*/
WxMaShareInfo getShareInfo(String sessionKey, String encryptedData, String ivStr);
/**
* 解密群入口敏感数据.
* 对应 wx.getGroupEnterInfo 接口返回的 encryptedData 解密
*
* @param sessionKey 会话密钥
* @param encryptedData 消息密文
* @param ivStr 加密算法的初始向量
* @return 群入口信息
* @see <a href="https://developers.weixin.qq.com/miniprogram/dev/api/open-api/group/wx.getGroupEnterInfo.html">wx.getGroupEnterInfo 官方文档</a>
*/
WxMaGroupEnterInfo getGroupEnterInfo(String sessionKey, String encryptedData, String ivStr);
}

View File

@@ -113,6 +113,7 @@ public abstract class BaseWxMaServiceImpl<H, P> implements WxMaService, RequestH
private final WxMaAnalysisService analysisService = new WxMaAnalysisServiceImpl(this);
private final WxMaCodeService codeService = new WxMaCodeServiceImpl(this);
private final WxMaCustomserviceWorkService customserviceWorkService = new WxMaCustomserviceWorkServiceImpl(this);
private final WxMaKefuService maKefuService = new WxMaKefuServiceImpl(this);
private final WxMaInternetService internetService = new WxMaInternetServiceImpl(this);
private final WxMaSettingService settingService = new WxMaSettingServiceImpl(this);
private final WxMaJsapiService jsapiService = new WxMaJsapiServiceImpl(this);
@@ -165,6 +166,7 @@ public abstract class BaseWxMaServiceImpl<H, P> implements WxMaService, RequestH
new WxMaExpressDeliveryReturnServiceImpl(this);
private final WxMaPromotionService wxMaPromotionService = new WxMaPromotionServiceImpl(this);
private final WxMaIntracityService intracityService = new WxMaIntracityServiceImpl(this);
private final WxMaComplaintService complaintService = new WxMaComplaintServiceImpl(this);
private Map<String, WxMaConfig> configMap = new HashMap<>();
private int retrySleepMillis = 1000;
@@ -425,8 +427,9 @@ public abstract class BaseWxMaServiceImpl<H, P> implements WxMaService, RequestH
}
String accessToken = getAccessToken(false);
if (StringUtils.isNotEmpty(this.getWxMaConfig().getApiHostUrl())) {
uri = uri.replace("https://api.weixin.qq.com", this.getWxMaConfig().getApiHostUrl());
String effectiveApiHostUrl = this.getWxMaConfig().getEffectiveApiHostUrl();
if (!WxMaConfig.DEFAULT_API_HOST_URL.equals(effectiveApiHostUrl)) {
uri = uri.replace(WxMaConfig.DEFAULT_API_HOST_URL, effectiveApiHostUrl);
}
String uriWithAccessToken =
@@ -657,6 +660,11 @@ public abstract class BaseWxMaServiceImpl<H, P> implements WxMaService, RequestH
return this.customserviceWorkService;
}
@Override
public WxMaKefuService getKefuService() {
return this.maKefuService;
}
@Override
public WxMaJsapiService getJsapiService() {
return this.jsapiService;
@@ -1030,4 +1038,9 @@ public abstract class BaseWxMaServiceImpl<H, P> implements WxMaService, RequestH
public WxMaIntracityService getIntracityService() {
return this.intracityService;
}
@Override
public WxMaComplaintService getComplaintService() {
return this.complaintService;
}
}

View File

@@ -0,0 +1,99 @@
package cn.binarywang.wx.miniapp.api.impl;
import cn.binarywang.wx.miniapp.api.WxMaComplaintService;
import cn.binarywang.wx.miniapp.api.WxMaService;
import cn.binarywang.wx.miniapp.bean.complaint.*;
import cn.binarywang.wx.miniapp.json.WxMaGsonBuilder;
import com.google.gson.JsonObject;
import lombok.RequiredArgsConstructor;
import me.chanjar.weixin.common.bean.CommonUploadParam;
import me.chanjar.weixin.common.error.WxError;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.util.fs.FileUtils;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.UUID;
import static cn.binarywang.wx.miniapp.constant.WxMaApiUrlConstants.Complaint.*;
/**
* 小程序交易投诉接口实现
*
* @author <a href="https://github.com/binarywang">Binary Wang</a>
* created on 2025-01-01
*/
@RequiredArgsConstructor
public class WxMaComplaintServiceImpl implements WxMaComplaintService {
private static final String JSON_CONTENT_TYPE = "application/json";
private final WxMaService wxMaService;
@Override
public WxMaComplaintResult queryComplaints(WxMaComplaintRequest request) throws WxErrorException {
String responseContent = this.wxMaService.post(QUERY_COMPLAINTS_URL, request.toJson());
return WxMaGsonBuilder.create().fromJson(responseContent, WxMaComplaintResult.class);
}
@Override
public WxMaComplaintDetailResult getComplaint(WxMaComplaintDetailRequest request) throws WxErrorException {
String responseContent = this.wxMaService.post(GET_COMPLAINT_URL, request.toJson());
return WxMaGsonBuilder.create().fromJson(responseContent, WxMaComplaintDetailResult.class);
}
@Override
public WxMaNegotiationHistoryResult queryNegotiationHistorys(WxMaNegotiationHistoryRequest request) throws WxErrorException {
String responseContent = this.wxMaService.post(QUERY_NEGOTIATION_HISTORY_URL, request.toJson());
return WxMaGsonBuilder.create().fromJson(responseContent, WxMaNegotiationHistoryResult.class);
}
@Override
public WxMaComplaintNotifyUrlResult addComplaintNotifyUrl(WxMaComplaintNotifyUrlRequest request) throws WxErrorException {
String responseContent = this.wxMaService.post(ADD_COMPLAINT_NOTIFY_URL, request.toJson());
return WxMaGsonBuilder.create().fromJson(responseContent, WxMaComplaintNotifyUrlResult.class);
}
@Override
public WxMaComplaintNotifyUrlResult getComplaintNotifyUrl() throws WxErrorException {
String responseContent = this.wxMaService.get(GET_COMPLAINT_NOTIFY_URL, null);
return WxMaGsonBuilder.create().fromJson(responseContent, WxMaComplaintNotifyUrlResult.class);
}
@Override
public WxMaComplaintNotifyUrlResult updateComplaintNotifyUrl(WxMaComplaintNotifyUrlRequest request) throws WxErrorException {
String responseContent = this.wxMaService.post(UPDATE_COMPLAINT_NOTIFY_URL, request.toJson());
return WxMaGsonBuilder.create().fromJson(responseContent, WxMaComplaintNotifyUrlResult.class);
}
@Override
public void deleteComplaintNotifyUrl() throws WxErrorException {
this.wxMaService.post(DELETE_COMPLAINT_NOTIFY_URL, "{}");
}
@Override
public void submitResponse(WxMaResponseRequest request) throws WxErrorException {
this.wxMaService.post(SUBMIT_RESPONSE_URL, request.toJson());
}
@Override
public void complete(WxMaCompleteRequest request) throws WxErrorException {
this.wxMaService.post(COMPLETE_COMPLAINT_URL, request.toJson());
}
@Override
public String uploadResponseImage(File imageFile) throws WxErrorException, IOException {
String result = this.wxMaService.upload(UPLOAD_RESPONSE_IMAGE_URL,
CommonUploadParam.fromFile("image", imageFile));
JsonObject jsonResult = WxMaGsonBuilder.create().fromJson(result, JsonObject.class);
return jsonResult.get("media_id").getAsString();
}
@Override
public String uploadResponseImage(InputStream inputStream, String fileName) throws WxErrorException, IOException {
try {
return this.uploadResponseImage(FileUtils.createTmpFile(inputStream, UUID.randomUUID().toString(), fileName));
} catch (IOException e) {
throw new WxErrorException(WxError.builder().errorMsg(e.getMessage()).build(), e);
}
}
}

Some files were not shown because too many files have changed in this diff Show More