32 Commits

Author SHA1 Message Date
Binary Wang
cfe1f6dc95 🔖 发布 4.7.8.B 测试版本
Some checks failed
Publish to Maven Central / build-and-publish (push) Has been cancelled
2025-09-24 21:55:26 +08:00
Copilot
c0edd9fd16 🎨 #3698 【微信支付】 修复字符编码导致的解析报文异常问题 2025-09-24 21:43:30 +08:00
Copilot
1788d9071e 🎨 #3715 企业微信回调消息中的MsgId改为字符串类型 2025-09-24 21:40:27 +08:00
Copilot
e6a844ab70 🐛 #3631 修复tryLockunlock中针对Key的序列化方式不一致导致无法unlock的问题
Some checks failed
Publish to Maven Central / build-and-publish (push) Has been cancelled
2025-09-23 10:53:09 +08:00
Copilot
6fc17201e1 🎨 #3697 【企业微信】修复自定义工作台enable_webview_click等参数设置无效的问题 2025-09-23 10:40:53 +08:00
Binary Wang
ad19f653c7 🎨 优化代码,修复过期方法引用 2025-09-23 10:31:35 +08:00
wangshuai
7018dceb1c :art:【企业微信】企微客户群回调事件添加LastMemVer、CurMemVer字段
Some checks failed
Publish to Maven Central / build-and-publish (push) Has been cancelled
2025-09-22 16:59:32 +00:00
ihao
399f3648c0 :art:【微信支付】公众号签约缺失执行签名方法,增加非必填参数判断;微信信h5纯签约接口,增加非必填字段判断 2025-09-23 00:58:11 +08:00
graalvm-samples
88bdd4a36e 🆕 #3703 【企业微信】第三方应用基础接口实现 2025-09-23 00:25:39 +08:00
imyzt
213cf6fd67 🐛 #3631 修复tryLockunlock中针对Key的序列化方式不一致导致无法unlock的问题 2025-09-23 00:24:02 +08:00
hb0730
c8d84da2ab 🎨 #3702 【小程序】多小程序spring starter增加Redis Template支持 2025-09-15 09:45:15 +08:00
dengfengfeng
118839a49d 🐛 #3692【小程序】【公众号】修复图片智能裁剪接口请求类型错误的问题
Co-authored-by: dengfeng <dengfeng@huaxincem.com>
2025-09-04 09:19:20 +08:00
水依寒
8447caa75f 🎨 #3694 统一各模块微信接口请求的基础地址字段命名为apiHostUrl,并完善Spring Boot starter配置支持 2025-09-04 09:17:36 +08:00
LiYaoheng
1eefd11e03 🎨 修复文档错误,关于channel-springboot-starter的依赖 2025-09-04 09:13:27 +08:00
tryking123
781798345e 🆕 #3681 【小程序】增加企微客服增删查3个接口 2025-08-31 21:44:24 +08:00
Copilot
0424d75882 🎨 #3323 【企业微信】获取企业所有打卡规则接口增加官方新添加的late_rule等字段 2025-08-08 18:21:59 +08:00
Copilot
e61ad44d27 📝 Add comprehensive GitHub Copilot instructions for WxJava SDK development 2025-08-08 18:18:39 +08:00
Copilot
dc46d9de75 🎨 #3640 【微信支付】使用HttpClient发送http请求时调整为使用连接池的形式 2025-08-08 17:47:20 +08:00
Copilot
bc6fb7b58e 🎨 #3656 【微信支付】 修复平台收付通 - 合单支付 - 关闭订单接口,增加缺少的两个参数
Some checks failed
Publish to Maven Central / build-and-publish (push) Has been cancelled
2025-08-08 17:40:28 +08:00
Copilot
983e4f1e66 🎨 #3659 【微信支付】新增BaseWxPayV3Result基类,提供rawJsonString字段保存原始API响应 2025-08-08 17:25:40 +08:00
痴货
7ffc1b3719 :new:【开放平台】添加获取换绑管理员 URL 的接口 2025-08-08 06:14:25 +00:00
Binary Wang
8bb4a21598 🎨 #3662 【企业微信】修复获取审批模板详情接口异常的问题:Expected BEGIN_OBJECT but was BEGIN_ARRAY
Some checks failed
Publish to Maven Central / build-and-publish (push) Has been cancelled
2025-07-30 23:29:59 +08:00
Copilot
a881de59a5 🎨 #3567 【企业微信】获取企业所有打卡规则接口调整 2025-07-30 23:10:30 +08:00
Copilot
14f8c8ebc2 🎨 #3655 通过支持现代 TLS 版本修复 SSL 握手失败问题
Some checks failed
Publish to Maven Central / build-and-publish (push) Has been cancelled
2025-07-25 11:41:06 +08:00
Winnie
875c35e745 🎨 #3660【企业微信】获取会议详情接口响应类补充官方新加的若干字段 2025-07-25 11:35:46 +08:00
Copilot
262a02b606 📝 为微信支付新增商户转账接口添加全面的文档和示例 2025-07-24 16:43:23 +08:00
Copilot
3a1d8075fc 🎨 Fix StackOverflowError in WxCpRedissonConfigImpl and WxCpRedisTemplateConfigImpl toString() methods 2025-07-24 16:39:56 +08:00
Binary Wang
2e952c76c8 🎨 修复错误的变量命名
Some checks are pending
Publish to Maven Central / build-and-publish (push) Waiting to run
2025-07-24 15:49:15 +08:00
Farahani
e5ef34553d 🆕 #3645【小程序】增加虚拟支付的更多接口 2025-07-24 15:29:25 +08:00
匠心
4cdd056a3e 🎨 修复代码缩进格式 2025-07-24 15:18:42 +08:00
痴货
57a6ef1179 🆕 【小程序】增加小程序基本信息通知事件相关的属性
Some checks failed
Publish to Maven Central / build-and-publish (push) Has been cancelled
2025-07-16 15:06:45 +00:00
痴货
4754a562ca 🎨 调整部分starter模块接口连接的 soTimeout 默认值为 5000毫秒 2025-07-16 15:05:00 +00:00
179 changed files with 6745 additions and 622 deletions

198
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,198 @@
# WxJava - WeChat Java SDK Development Instructions
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.**
## Working Effectively
### 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)
### Bootstrap, Build, and Validate
Execute these commands in sequence after cloning:
```bash
# 1. Basic compilation (NEVER CANCEL - takes 4-5 minutes)
mvn clean compile -DskipTests=true --no-transfer-progress
# Timeout: Set 8+ minutes. Actual time: ~4 minutes
# 2. Full package build (NEVER CANCEL - takes 2-3 minutes)
mvn clean package -DskipTests=true --no-transfer-progress
# Timeout: Set 5+ minutes. Actual time: ~2 minutes
# 3. Code quality validation (NEVER CANCEL - takes 45-60 seconds)
mvn checkstyle:check --no-transfer-progress
# Timeout: Set 3+ minutes. Actual time: ~50 seconds
```
**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
### 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
## 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 (企点)
### Framework Integration Modules
- `spring-boot-starters/` - Spring Boot auto-configuration starters
- `solon-plugins/` - Solon framework plugins
- `weixin-graal/` - GraalVM native image support
### Configuration and Quality
- `quality-checks/google_checks.xml` - Checkstyle configuration
- `.editorconfig` - Code formatting rules (2 spaces = 1 tab)
- `pom.xml` - Root Maven configuration
## Development Workflow
### Making Code Changes
1. **Always build first** to establish clean baseline:
```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
3. **Validate changes incrementally**:
```bash
# After each change:
mvn compile --no-transfer-progress
mvn checkstyle:check --no-transfer-progress
```
### Before Submitting Changes
**ALWAYS run these validation steps in sequence:**
1. **Code Style Validation**:
```bash
mvn checkstyle:check --no-transfer-progress
# Must pass - takes ~50 seconds
```
2. **Full Clean Build**:
```bash
mvn clean package -DskipTests=true --no-transfer-progress
# Must succeed - takes ~2 minutes
```
3. **Documentation**: Update javadoc for public methods and classes
4. **Contribution Guidelines**: Follow `CONTRIBUTING.md` - PRs must target `develop` branch
## 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)
### 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
## Common Tasks and Commands
### Validate Specific Module
```bash
# Build single module (replace 'weixin-java-mp' with target module):
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
### 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
### Test Resources
- `*/src/test/resources/test-config.sample.xml` - Template for test configuration
- Tests require real WeChat API credentials to run
## SDK Usage Patterns
### Maven Dependency Usage
```xml
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId> <!-- or other modules -->
<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
## 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
### 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`
## 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
Remember: This is a SDK library project, not a runnable application. Changes should focus on API functionality, not application behavior.

129
NEW_TRANSFER_API_SUPPORT.md Normal file
View File

@@ -0,0 +1,129 @@
# 微信支付新版商户转账API支持
## 问题解答
**问题**: 新开通的商户号只能使用最新版本的商户转账接口WxJava是否支持
**答案**: **WxJava 已经完整支持新版商户转账API** 从2025年1月15日开始生效的新版转账API已在WxJava中实现。
## 新版转账API特性
### 1. API接口对比
| 特性 | 传统转账API | 新版转账API (2025.1.15+) |
|------|-------------|-------------------------|
| **服务类** | `MerchantTransferService` | `TransferService` |
| **API路径** | `/v3/transfer/batches` | `/v3/fund-app/mch-transfer/transfer-bills` |
| **转账方式** | 批量转账 | 单笔转账 |
| **场景支持** | 基础场景 | 丰富场景(如佣金报酬等) |
| **撤销功能** | ❌ 不支持 | ✅ 支持 |
| **适用范围** | 所有商户 | **新开通商户必须使用** |
### 2. 新版API功能列表
**发起转账** - `transferBills()`
**查询转账** - `getBillsByOutBillNo()` / `getBillsByTransferBillNo()`
**撤销转账** - `transformBillsCancel()`
**回调通知** - `parseTransferBillsNotifyResult()`
**RSA加密** - 自动处理用户姓名加密
**场景支持** - 支持多种转账场景ID
## 快速开始
### 1. 获取服务实例
```java
// 获取WxPayService实例
WxPayService wxPayService = new WxPayServiceImpl();
wxPayService.setConfig(config);
// 获取新版转账服务 - 这就是新开通商户需要使用的服务!
TransferService transferService = wxPayService.getTransferService();
```
### 2. 发起转账新版API
```java
// 构建转账请求
TransferBillsRequest request = TransferBillsRequest.newBuilder()
.appid("your_appid") // 应用ID
.outBillNo("T" + System.currentTimeMillis()) // 商户转账单号
.transferSceneId("1005") // 转账场景ID佣金报酬
.openid("user_openid") // 用户openid
.userName("张三") // 收款用户姓名(可选,自动加密)
.transferAmount(100) // 转账金额(分)
.transferRemark("佣金报酬") // 转账备注
.build();
// 发起转账
TransferBillsResult result = transferService.transferBills(request);
System.out.println("转账成功,微信转账单号:" + result.getTransferBillNo());
```
### 3. 查询转账结果
```java
// 通过商户单号查询
TransferBillsGetResult result = transferService.getBillsByOutBillNo("T1642567890123");
// 通过微信转账单号查询
TransferBillsGetResult result2 = transferService.getBillsByTransferBillNo("wx_transfer_bill_no");
System.out.println("转账状态:" + result.getState());
```
### 4. 撤销转账(新功能)
```java
// 撤销转账
TransferBillsCancelResult cancelResult = transferService.transformBillsCancel("T1642567890123");
System.out.println("撤销状态:" + cancelResult.getState());
```
## 重要说明
### 转账场景ID (transfer_scene_id)
- **1005**: 佣金报酬(常用场景)
- 其他场景需要在微信商户平台申请
### 转账状态说明
- **ACCEPTED**: 转账已受理
- **PROCESSING**: 转账处理中
- **SUCCESS**: 转账成功
- **FAIL**: 转账失败
- **CANCELLED**: 转账撤销完成
### 新开通商户使用建议
1. **优先使用** `TransferService` 新版API
2. **不要使用** `MerchantTransferService` (可能不支持)
3. **必须设置** 转账场景ID (`transfer_scene_id`)
4. **建议开启** 回调通知以实时获取转账结果
## 完整示例代码
详细的使用示例请参考:
- 📄 [NEW_TRANSFER_API_USAGE.md](./NEW_TRANSFER_API_USAGE.md) - 详细使用指南
- 💻 [NewTransferApiExample.java](./weixin-java-pay/src/main/java/com/github/binarywang/wxpay/example/NewTransferApiExample.java) - 完整代码示例
## 常见问题
**Q: 我是新开通的商户,应该使用哪个服务?**
A: 使用 `TransferService`这是专为新版API设计的服务。
**Q: 新版API和旧版API有什么区别**
A: 新版API使用单笔转账模式支持更丰富的转账场景并且支持撤销功能。
**Q: 如何设置转账场景ID**
A: 在商户平台申请相应场景常用的佣金报酬场景ID是"1005"。
**Q: 用户姓名需要加密吗?**
A: WxJava会自动处理RSA加密您只需要传入明文姓名即可。
## 版本要求
- WxJava 版本4.7.0+
- 支持时间2025年1月15日+
- 适用商户:所有商户(新开通商户强制使用)
通过以上说明新开通的微信支付商户可以放心使用WxJava进行商户转账操作

148
NEW_TRANSFER_API_USAGE.md Normal file
View File

@@ -0,0 +1,148 @@
# 微信支付新版商户转账API使用指南
## 概述
从2025年1月15日开始微信支付推出了新版的商户转账API。新开通的商户号只能使用最新版本的商户转账接口。WxJava 已经完整支持新版转账API。
## API对比
### 传统转账API (仍然支持)
- **服务类**: `MerchantTransferService`
- **API前缀**: `/v3/transfer/batches`
- **特点**: 支持批量转账,一次可以转账给多个用户
### 新版转账API (2025.1.15+)
- **服务类**: `TransferService`
- **API前缀**: `/v3/fund-app/mch-transfer/transfer-bills`
- **特点**: 单笔转账,支持更丰富的转账场景
## 使用新版转账API
### 1. 获取服务实例
```java
// 获取WxPayService实例
WxPayService wxPayService = new WxPayServiceImpl();
wxPayService.setConfig(config);
// 获取新版转账服务
TransferService transferService = wxPayService.getTransferService();
```
### 2. 发起转账
```java
// 构建转账请求
TransferBillsRequest request = TransferBillsRequest.newBuilder()
.appid("your_appid") // 应用ID
.outBillNo("T" + System.currentTimeMillis()) // 商户转账单号
.transferSceneId("1005") // 转账场景ID佣金报酬
.openid("user_openid") // 用户openid
.userName("张三") // 收款用户姓名(可选,需要加密)
.transferAmount(100) // 转账金额(分)
.transferRemark("佣金报酬") // 转账备注
.notifyUrl("https://your-domain.com/notify") // 回调地址(可选)
.userRecvPerception("Y") // 用户收款感知(可选)
.build();
try {
TransferBillsResult result = transferService.transferBills(request);
System.out.println("转账成功,微信转账单号:" + result.getTransferBillNo());
System.out.println("状态:" + result.getState());
} catch (WxPayException e) {
System.err.println("转账失败:" + e.getMessage());
}
```
### 3. 查询转账结果
```java
// 通过商户单号查询
String outBillNo = "T1642567890123";
TransferBillsGetResult result = transferService.getBillsByOutBillNo(outBillNo);
// 通过微信转账单号查询
String transferBillNo = "1000000000000000000000000001";
TransferBillsGetResult result2 = transferService.getBillsByTransferBillNo(transferBillNo);
System.out.println("转账状态:" + result.getState());
System.out.println("转账金额:" + result.getTransferAmount());
```
### 4. 撤销转账
```java
// 撤销转账(仅在特定状态下可撤销)
String outBillNo = "T1642567890123";
TransferBillsCancelResult cancelResult = transferService.transformBillsCancel(outBillNo);
System.out.println("撤销结果:" + cancelResult.getState());
```
### 5. 处理回调通知
```java
// 在回调接口中处理通知
@PostMapping("/transfer/notify")
public String handleTransferNotify(HttpServletRequest request) throws Exception {
String notifyData = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);
// 构建签名头
SignatureHeader header = new SignatureHeader();
header.setTimeStamp(request.getHeader("Wechatpay-Timestamp"));
header.setNonce(request.getHeader("Wechatpay-Nonce"));
header.setSignature(request.getHeader("Wechatpay-Signature"));
header.setSerial(request.getHeader("Wechatpay-Serial"));
try {
TransferBillsNotifyResult notifyResult = transferService.parseTransferBillsNotifyResult(notifyData, header);
// 处理业务逻辑
String outBillNo = notifyResult.getOutBillNo();
String state = notifyResult.getState();
System.out.println("转账单号:" + outBillNo + ",状态:" + state);
return "SUCCESS";
} catch (WxPayException e) {
System.err.println("验签失败:" + e.getMessage());
return "FAIL";
}
}
```
## 重要参数说明
### 转账场景ID (transfer_scene_id)
- **1005**: 佣金报酬(常用)
- 其他场景ID需要在商户平台申请
### 转账状态
- **PROCESSING**: 转账中
- **SUCCESS**: 转账成功
- **FAILED**: 转账失败
- **REFUNDED**: 已退款
### 用户收款感知 (user_recv_perception)
- **Y**: 用户会收到微信转账通知
- **N**: 用户不会收到微信转账通知
## 新旧API对比总结
| 特性 | 传统API (MerchantTransferService) | 新版API (TransferService) |
|------|----------------------------------|---------------------------|
| 发起方式 | 批量转账 | 单笔转账 |
| API路径 | `/v3/transfer/batches` | `/v3/fund-app/mch-transfer/transfer-bills` |
| 场景支持 | 基础转账场景 | 丰富的转账场景 |
| 回调通知 | 支持 | 支持 |
| 撤销功能 | 不支持 | 支持 |
| 适用商户 | 所有商户 | 新开通商户必须使用 |
## 注意事项
1. **新开通的商户号**: 必须使用新版API (`TransferService`)
2. **转账场景ID**: 需要在商户平台申请相应的转账场景
3. **用户姓名加密**: 如果传入用户姓名会自动进行RSA加密
4. **回调验签**: 建议开启回调验签以确保安全性
5. **错误处理**: 妥善处理各种异常情况
通过以上指南您可以轻松使用WxJava的新版商户转账API功能。

0
others/mvnw vendored Normal file → Executable file
View File

17
pom.xml
View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java</artifactId>
<version>4.7.7.B</version>
<version>4.7.8.B</version>
<packaging>pom</packaging>
<name>WxJava - Weixin/Wechat Java SDK</name>
<description>微信开发Java SDK</description>
@@ -468,6 +468,21 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.0</version> <!-- 建议使用最新版本 -->
<executions>
<execution>
<id>attach-sources</id>
<!-- 绑定到 verify 阶段,在 install 之前执行 -->
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal> <!-- 使用 jar-no-fork 效率更高 -->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

View File

@@ -6,7 +6,7 @@
<parent>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java</artifactId>
<version>4.7.7.B</version>
<version>4.7.8.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.7.B</version>
<version>4.7.8.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.7.B</version>
<version>4.7.8.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.7.B</version>
<version>4.7.8.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.7.B</version>
<version>4.7.8.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.7.B</version>
<version>4.7.8.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.7.B</version>
<version>4.7.8.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.7.B</version>
<version>4.7.8.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.7.B</version>
<version>4.7.8.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.7.B</version>
<version>4.7.8.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.7.B</version>
<version>4.7.8.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.7.B</version>
<version>4.7.8.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.7.B</version>
<version>4.7.8.B</version>
</parent>
<packaging>pom</packaging>
<artifactId>wx-java-spring-boot-starters</artifactId>
@@ -26,6 +26,7 @@
<module>wx-java-open-spring-boot-starter</module>
<module>wx-java-qidian-spring-boot-starter</module>
<module>wx-java-cp-multi-spring-boot-starter</module>
<module>wx-java-cp-tp-multi-spring-boot-starter</module>
<module>wx-java-cp-spring-boot-starter</module>
<module>wx-java-channel-spring-boot-starter</module>
<module>wx-java-channel-multi-spring-boot-starter</module>

View File

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

View File

@@ -119,6 +119,8 @@ public abstract class AbstractWxChannelConfiguration {
config.setAesKey(aesKey);
}
config.setStableAccessToken(useStableAccessToken);
config.setApiHostUrl(StringUtils.trimToNull(wxChannelSingleProperties.getApiHostUrl()));
config.setAccessTokenUrl(StringUtils.trimToNull(wxChannelSingleProperties.getAccessTokenUrl()));
}
private void configHttp(WxChannelDefaultConfigImpl config, WxChannelMultiProperties.ConfigStorage storage) {

View File

@@ -40,4 +40,16 @@ public class WxChannelSingleProperties implements Serializable {
* 是否使用稳定版 Access Token
*/
private boolean useStableAccessToken = false;
/**
* 自定义API主机地址用于替换默认的 https://api.weixin.qq.com
* 例如http://proxy.company.com:8080
*/
private String apiHostUrl;
/**
* 自定义获取AccessToken地址用于向自定义统一服务获取AccessToken
* 例如http://proxy.company.com:8080/oauth/token
*/
private String accessTokenUrl;
}

View File

@@ -6,7 +6,7 @@
<dependencies>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-channel-multi-spring-boot-starter</artifactId>
<artifactId>wx-java-channel-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>

View File

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

View File

@@ -16,6 +16,8 @@ public abstract class AbstractWxChannelConfigStorageConfiguration {
config.setAesKey(StringUtils.trimToNull(properties.getAesKey()));
config.setMsgDataFormat(StringUtils.trimToNull(properties.getMsgDataFormat()));
config.setStableAccessToken(properties.isUseStableAccessToken());
config.setApiHostUrl(StringUtils.trimToNull(properties.getApiHostUrl()));
config.setAccessTokenUrl(StringUtils.trimToNull(properties.getAccessTokenUrl()));
WxChannelProperties.ConfigStorage configStorageProperties = properties.getConfigStorage();
config.setHttpProxyHost(configStorageProperties.getHttpProxyHost());

View File

@@ -46,6 +46,18 @@ public class WxChannelProperties {
*/
private boolean useStableAccessToken = false;
/**
* 自定义API主机地址用于替换默认的 https://api.weixin.qq.com
* 例如http://proxy.company.com:8080
*/
private String apiHostUrl;
/**
* 自定义获取AccessToken地址用于向自定义统一服务获取AccessToken
* 例如http://proxy.company.com:8080/oauth/token
*/
private String accessTokenUrl;
/**
* 存储策略
*/

View File

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

View File

@@ -139,6 +139,9 @@ public abstract class AbstractWxCpConfiguration {
if (StringUtils.isNotBlank(msgAuditLibPath)) {
config.setMsgAuditLibPath(msgAuditLibPath);
}
if (StringUtils.isNotBlank(wxCpSingleProperties.getBaseApiUrl())) {
config.setBaseApiUrl(wxCpSingleProperties.getBaseApiUrl());
}
}
private void configHttp(WxCpDefaultConfigImpl config, WxCpMultiProperties.ConfigStorage storage) {

View File

@@ -43,4 +43,10 @@ public class WxCpSingleProperties implements Serializable {
* 微信企业号应用 会话存档类库路径
*/
private String msgAuditLibPath;
/**
* 自定义企业微信服务器baseUrl用于替换默认的 https://qyapi.weixin.qq.com
* 例如http://proxy.company.com:8080
*/
private String baseApiUrl;
}

View File

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

View File

@@ -48,6 +48,12 @@ public class WxCpProperties {
*/
private String msgAuditLibPath;
/**
* 自定义企业微信服务器baseUrl用于替换默认的 https://qyapi.weixin.qq.com
* 例如http://proxy.company.com:8080
*/
private String baseApiUrl;
/**
* 配置存储策略,默认内存
*/

View File

@@ -37,6 +37,9 @@ public abstract class AbstractWxCpConfigStorageConfiguration {
if (StringUtils.isNotBlank(msgAuditLibPath)) {
config.setMsgAuditLibPath(msgAuditLibPath);
}
if (StringUtils.isNotBlank(properties.getBaseApiUrl())) {
config.setBaseApiUrl(properties.getBaseApiUrl());
}
WxCpProperties.ConfigStorage storage = properties.getConfigStorage();
String httpProxyHost = storage.getHttpProxyHost();

View File

@@ -0,0 +1,97 @@
# wx-java-cp-multi-spring-boot-starter
企业微信多账号配置
- 实现多 WxCpService 初始化。
- 未实现 WxCpTpService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。
- 未实现 WxCpCgService 初始化,需要的小伙伴可以参考多 WxCpService 配置的实现。
## 快速开始
1. 引入依赖
```xml
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-cp-multi-spring-boot-starter</artifactId>
<version>${version}</version>
</dependency>
```
2. 添加配置(application.properties)
```properties
# 应用 1 配置
wx.cp.corps.tenantId1.corp-id = @corp-id
wx.cp.corps.tenantId1.corp-secret = @corp-secret
## 选填
wx.cp.corps.tenantId1.agent-id = @agent-id
wx.cp.corps.tenantId1.token = @token
wx.cp.corps.tenantId1.aes-key = @aes-key
wx.cp.corps.tenantId1.msg-audit-priKey = @msg-audit-priKey
wx.cp.corps.tenantId1.msg-audit-lib-path = @msg-audit-lib-path
# 应用 2 配置
wx.cp.corps.tenantId2.corp-id = @corp-id
wx.cp.corps.tenantId2.corp-secret = @corp-secret
## 选填
wx.cp.corps.tenantId2.agent-id = @agent-id
wx.cp.corps.tenantId2.token = @token
wx.cp.corps.tenantId2.aes-key = @aes-key
wx.cp.corps.tenantId2.msg-audit-priKey = @msg-audit-priKey
wx.cp.corps.tenantId2.msg-audit-lib-path = @msg-audit-lib-path
# 公共配置
## ConfigStorage 配置(选填)
wx.cp.config-storage.type=memory # 配置类型: memory(默认), jedis, redisson, redistemplate
## http 客户端配置(选填)
## # http客户端类型: http_client(默认), ok_http, jodd_http
wx.cp.config-storage.http-client-type=http_client
wx.cp.config-storage.http-proxy-host=
wx.cp.config-storage.http-proxy-port=
wx.cp.config-storage.http-proxy-username=
wx.cp.config-storage.http-proxy-password=
## 最大重试次数默认5 次,如果小于 0则为 0
wx.cp.config-storage.max-retry-times=5
## 重试时间间隔步进默认1000 毫秒,如果小于 0则为 1000
wx.cp.config-storage.retry-sleep-millis=1000
```
3. 支持自动注入的类型: `WxCpMultiServices`
4. 使用样例
```java
import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServices;
import me.chanjar.weixin.cp.api.WxCpService;
import me.chanjar.weixin.cp.api.WxCpUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class DemoService {
@Autowired
private WxCpTpMultiServices wxCpTpMultiServices;
public void test() {
// 应用 1 的 WxCpService
WxCpService wxCpService1 = wxCpMultiServices.getWxCpService("tenantId1");
WxCpUserService userService1 = wxCpService1.getUserService();
userService1.getUserId("xxx");
// todo ...
// 应用 2 的 WxCpService
WxCpService wxCpService2 = wxCpMultiServices.getWxCpService("tenantId2");
WxCpUserService userService2 = wxCpService2.getUserService();
userService2.getUserId("xxx");
// todo ...
// 应用 3 的 WxCpService
WxCpService wxCpService3 = wxCpMultiServices.getWxCpService("tenantId3");
// 判断是否为空
if (wxCpService3 == null) {
// todo wxCpService3 为空,请先配置 tenantId3 企业微信应用参数
return;
}
WxCpUserService userService3 = wxCpService3.getUserService();
userService3.getUserId("xxx");
// todo ...
}
}
```

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>wx-java-spring-boot-starters</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.8.B</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>wx-java-cp-tp-multi-spring-boot-starter</artifactId>
<name>WxJava - Spring Boot Starter for WxCp::支持多账号配置</name>
<description>微信企业号开发的 Spring Boot Starter::支持多账号配置</description>
<dependencies>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-cp</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,16 @@
package com.binarywang.spring.starter.wxjava.cp.autoconfigure;
import com.binarywang.spring.starter.wxjava.cp.configuration.WxCpTpMultiServicesAutoConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/**
* 企业微信自动注册
*
* @author yl
* created on 2023/10/16
*/
@Configuration
@Import(WxCpTpMultiServicesAutoConfiguration.class)
public class WxCpTpMultiAutoConfiguration {
}

View File

@@ -0,0 +1,27 @@
package com.binarywang.spring.starter.wxjava.cp.configuration;
import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpTpInJedisTpConfiguration;
import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpTpInMemoryTpConfiguration;
import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpTpInRedisTemplateTpConfiguration;
import com.binarywang.spring.starter.wxjava.cp.configuration.services.WxCpTpInRedissonTpConfiguration;
import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/**
* 企业微信平台相关服务自动注册
*
* @author yl
* created on 2023/10/16
*/
@Configuration
@EnableConfigurationProperties(WxCpTpMultiProperties.class)
@Import({
WxCpTpInJedisTpConfiguration.class,
WxCpTpInMemoryTpConfiguration.class,
WxCpTpInRedissonTpConfiguration.class,
WxCpTpInRedisTemplateTpConfiguration.class
})
public class WxCpTpMultiServicesAutoConfiguration {
}

View File

@@ -0,0 +1,139 @@
package com.binarywang.spring.starter.wxjava.cp.configuration.services;
import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiProperties;
import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpSingleProperties;
import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServices;
import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServicesImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl;
import me.chanjar.weixin.cp.tp.service.WxCpTpService;
import me.chanjar.weixin.cp.tp.service.impl.WxCpTpServiceApacheHttpClientImpl;
import me.chanjar.weixin.cp.tp.service.impl.WxCpTpServiceImpl;
import me.chanjar.weixin.cp.tp.service.impl.WxCpTpServiceJoddHttpImpl;
import me.chanjar.weixin.cp.tp.service.impl.WxCpTpServiceOkHttpImpl;
import org.apache.commons.lang3.StringUtils;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* WxCpConfigStorage 抽象配置类
*
* @author yl
* created on 2023/10/16
*/
@RequiredArgsConstructor
@Slf4j
public abstract class AbstractWxCpTpConfiguration {
/**
*
* @param wxCpTpMultiProperties 应用列表配置
* @param services 用于支持,应用启动之后,可以调用这个接口添加新服务对象。主要是配置是从数据库中读取的
* @return
*/
public WxCpTpMultiServices wxCpMultiServices(WxCpTpMultiProperties wxCpTpMultiProperties,WxCpTpMultiServices services) {
Map<String, WxCpTpSingleProperties> corps = wxCpTpMultiProperties.getCorps();
if (corps == null || corps.isEmpty()) {
log.warn("企业微信应用参数未配置,通过 WxCpMultiServices#getWxCpTpService(\"tenantId\")获取实例将返回空");
return new WxCpTpMultiServicesImpl();
}
if (services == null) {
services = new WxCpTpMultiServicesImpl();
}
Set<Map.Entry<String, WxCpTpSingleProperties>> entries = corps.entrySet();
for (Map.Entry<String, WxCpTpSingleProperties> entry : entries) {
String tenantId = entry.getKey();
WxCpTpSingleProperties wxCpTpSingleProperties = entry.getValue();
WxCpTpDefaultConfigImpl storage = this.wxCpTpConfigStorage(wxCpTpMultiProperties);
this.configCorp(storage, wxCpTpSingleProperties);
this.configHttp(storage, wxCpTpMultiProperties.getConfigStorage());
WxCpTpService wxCpTpService = this.wxCpTpService(storage, wxCpTpMultiProperties.getConfigStorage());
if (services.getWxCpTpService(tenantId) == null) {
// 不存在的才会添加到服务列表中
services.addWxCpTpService(tenantId, wxCpTpService);
}
}
return services;
}
/**
* 配置 WxCpDefaultConfigImpl
*
* @param wxCpTpMultiProperties 参数
* @return WxCpDefaultConfigImpl
*/
protected abstract WxCpTpDefaultConfigImpl wxCpTpConfigStorage(WxCpTpMultiProperties wxCpTpMultiProperties);
private WxCpTpService wxCpTpService(WxCpTpConfigStorage wxCpTpConfigStorage, WxCpTpMultiProperties.ConfigStorage storage) {
WxCpTpMultiProperties.HttpClientType httpClientType = storage.getHttpClientType();
WxCpTpService cpTpService;
switch (httpClientType) {
case OK_HTTP:
cpTpService = new WxCpTpServiceOkHttpImpl();
break;
case JODD_HTTP:
cpTpService = new WxCpTpServiceJoddHttpImpl();
break;
case HTTP_CLIENT:
cpTpService = new WxCpTpServiceApacheHttpClientImpl();
break;
default:
cpTpService = new WxCpTpServiceImpl();
break;
}
cpTpService.setWxCpTpConfigStorage(wxCpTpConfigStorage);
int maxRetryTimes = storage.getMaxRetryTimes();
if (maxRetryTimes < 0) {
maxRetryTimes = 0;
}
int retrySleepMillis = storage.getRetrySleepMillis();
if (retrySleepMillis < 0) {
retrySleepMillis = 1000;
}
cpTpService.setRetrySleepMillis(retrySleepMillis);
cpTpService.setMaxRetryTimes(maxRetryTimes);
return cpTpService;
}
private void configCorp(WxCpTpDefaultConfigImpl config, WxCpTpSingleProperties wxCpTpSingleProperties) {
String corpId = wxCpTpSingleProperties.getCorpId();
String providerSecret = wxCpTpSingleProperties.getProviderSecret();
String suiteId = wxCpTpSingleProperties.getSuiteId();
String token = wxCpTpSingleProperties.getToken();
String suiteSecret = wxCpTpSingleProperties.getSuiteSecret();
// 企业微信,私钥,会话存档路径
config.setCorpId(corpId);
config.setProviderSecret(providerSecret);
config.setEncodingAESKey(wxCpTpSingleProperties.getEncodingAESKey());
config.setSuiteId(suiteId);
config.setToken(token);
config.setSuiteSecret(suiteSecret);
}
private void configHttp(WxCpTpDefaultConfigImpl config, WxCpTpMultiProperties.ConfigStorage storage) {
String httpProxyHost = storage.getHttpProxyHost();
Integer httpProxyPort = storage.getHttpProxyPort();
String httpProxyUsername = storage.getHttpProxyUsername();
String httpProxyPassword = storage.getHttpProxyPassword();
if (StringUtils.isNotBlank(httpProxyHost)) {
config.setHttpProxyHost(httpProxyHost);
if (httpProxyPort != null) {
config.setHttpProxyPort(httpProxyPort);
}
if (StringUtils.isNotBlank(httpProxyUsername)) {
config.setHttpProxyUsername(httpProxyUsername);
}
if (StringUtils.isNotBlank(httpProxyPassword)) {
config.setHttpProxyPassword(httpProxyPassword);
}
}
}
}

View File

@@ -0,0 +1,78 @@
package com.binarywang.spring.starter.wxjava.cp.configuration.services;
import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiProperties;
import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiRedisProperties;
import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServices;
import lombok.RequiredArgsConstructor;
import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
import me.chanjar.weixin.cp.config.impl.WxCpJedisConfigImpl;
import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl;
import me.chanjar.weixin.cp.config.impl.WxCpTpJedisConfigImpl;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
* 自动装配基于 jedis 策略配置
*
* @author yl
* created on 2023/10/16
*/
@Configuration
@ConditionalOnProperty(
prefix = WxCpTpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "jedis"
)
@RequiredArgsConstructor
public class WxCpTpInJedisTpConfiguration extends AbstractWxCpTpConfiguration {
private final WxCpTpMultiProperties wxCpTpMultiProperties;
private final ApplicationContext applicationContext;
@Bean
public WxCpTpMultiServices wxCpMultiServices() {
return this.wxCpMultiServices(wxCpTpMultiProperties,null);
}
@Override
protected WxCpTpDefaultConfigImpl wxCpTpConfigStorage(WxCpTpMultiProperties wxCpTpMultiProperties) {
return this.configRedis(wxCpTpMultiProperties);
}
private WxCpTpDefaultConfigImpl configRedis(WxCpTpMultiProperties wxCpTpMultiProperties) {
WxCpTpMultiRedisProperties wxCpTpMultiRedisProperties = wxCpTpMultiProperties.getConfigStorage().getRedis();
JedisPool jedisPool;
if (wxCpTpMultiRedisProperties != null && StringUtils.isNotEmpty(wxCpTpMultiRedisProperties.getHost())) {
jedisPool = getJedisPool(wxCpTpMultiProperties);
} else {
jedisPool = applicationContext.getBean(JedisPool.class);
}
return new WxCpTpJedisConfigImpl(jedisPool, wxCpTpMultiProperties.getConfigStorage().getKeyPrefix());
}
private JedisPool getJedisPool(WxCpTpMultiProperties wxCpTpMultiProperties) {
WxCpTpMultiProperties.ConfigStorage storage = wxCpTpMultiProperties.getConfigStorage();
WxCpTpMultiRedisProperties redis = storage.getRedis();
JedisPoolConfig config = new JedisPoolConfig();
if (redis.getMaxActive() != null) {
config.setMaxTotal(redis.getMaxActive());
}
if (redis.getMaxIdle() != null) {
config.setMaxIdle(redis.getMaxIdle());
}
if (redis.getMaxWaitMillis() != null) {
config.setMaxWaitMillis(redis.getMaxWaitMillis());
}
if (redis.getMinIdle() != null) {
config.setMinIdle(redis.getMinIdle());
}
config.setTestOnBorrow(true);
config.setTestWhileIdle(true);
return new JedisPool(config, redis.getHost(), redis.getPort(),
redis.getTimeout(), redis.getPassword(), redis.getDatabase());
}
}

View File

@@ -0,0 +1,39 @@
package com.binarywang.spring.starter.wxjava.cp.configuration.services;
import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiProperties;
import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServices;
import lombok.RequiredArgsConstructor;
import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 自动装配基于内存策略配置
*
* @author yl
* created on 2023/10/16
*/
@Configuration
@ConditionalOnProperty(
prefix = WxCpTpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "memory", matchIfMissing = true
)
@RequiredArgsConstructor
public class WxCpTpInMemoryTpConfiguration extends AbstractWxCpTpConfiguration {
private final WxCpTpMultiProperties wxCpTpMultiProperties;
@Bean
public WxCpTpMultiServices wxCpMultiServices() {
return this.wxCpMultiServices(wxCpTpMultiProperties,null);
}
@Override
protected WxCpTpDefaultConfigImpl wxCpTpConfigStorage(WxCpTpMultiProperties wxCpTpMultiProperties) {
return this.configInMemory();
}
private WxCpTpDefaultConfigImpl configInMemory() {
return new WxCpTpDefaultConfigImpl();
}
}

View File

@@ -0,0 +1,45 @@
package com.binarywang.spring.starter.wxjava.cp.configuration.services;
import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiProperties;
import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServices;
import lombok.RequiredArgsConstructor;
import me.chanjar.weixin.cp.config.impl.WxCpDefaultConfigImpl;
import me.chanjar.weixin.cp.config.impl.WxCpRedisTemplateConfigImpl;
import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl;
import me.chanjar.weixin.cp.config.impl.WxCpTpRedisTemplateConfigImpl;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* 自动装配基于 redisTemplate 策略配置
*
* @author yl
* created on 2023/10/16
*/
@Configuration
@ConditionalOnProperty(
prefix = WxCpTpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redistemplate"
)
@RequiredArgsConstructor
public class WxCpTpInRedisTemplateTpConfiguration extends AbstractWxCpTpConfiguration {
private final WxCpTpMultiProperties wxCpTpMultiProperties;
private final ApplicationContext applicationContext;
@Bean
public WxCpTpMultiServices wxCpMultiServices() {
return this.wxCpMultiServices(wxCpTpMultiProperties,null);
}
@Override
protected WxCpTpDefaultConfigImpl wxCpTpConfigStorage(WxCpTpMultiProperties wxCpTpMultiProperties) {
return this.configRedisTemplate(wxCpTpMultiProperties);
}
private WxCpTpDefaultConfigImpl configRedisTemplate(WxCpTpMultiProperties wxCpTpMultiProperties) {
StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
return new WxCpTpRedisTemplateConfigImpl(redisTemplate, wxCpTpMultiProperties.getConfigStorage().getKeyPrefix());
}
}

View File

@@ -0,0 +1,68 @@
package com.binarywang.spring.starter.wxjava.cp.configuration.services;
import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiProperties;
import com.binarywang.spring.starter.wxjava.cp.properties.WxCpTpMultiRedisProperties;
import com.binarywang.spring.starter.wxjava.cp.service.WxCpTpMultiServices;
import lombok.RequiredArgsConstructor;
import me.chanjar.weixin.cp.config.impl.WxCpTpDefaultConfigImpl;
import me.chanjar.weixin.cp.config.impl.AbstractWxCpTpInRedisConfigImpl;
import me.chanjar.weixin.cp.config.impl.WxCpTpRedissonConfigImpl;
import org.apache.commons.lang3.StringUtils;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.redisson.config.TransportMode;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 自动装配基于 redisson 策略配置
*
* @author yl
* created on 2023/10/16
*/
@Configuration
@ConditionalOnProperty(
prefix = WxCpTpMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redisson"
)
@RequiredArgsConstructor
public class WxCpTpInRedissonTpConfiguration extends AbstractWxCpTpConfiguration {
private final WxCpTpMultiProperties wxCpTpMultiProperties;
private final ApplicationContext applicationContext;
@Bean
public WxCpTpMultiServices wxCpMultiServices() {
return this.wxCpMultiServices(wxCpTpMultiProperties,null);
}
@Override
protected WxCpTpDefaultConfigImpl wxCpTpConfigStorage(WxCpTpMultiProperties wxCpTpMultiProperties) {
return this.configRedisson(wxCpTpMultiProperties);
}
private WxCpTpDefaultConfigImpl configRedisson(WxCpTpMultiProperties wxCpTpMultiProperties) {
WxCpTpMultiRedisProperties redisProperties = wxCpTpMultiProperties.getConfigStorage().getRedis();
RedissonClient redissonClient;
if (redisProperties != null && StringUtils.isNotEmpty(redisProperties.getHost())) {
redissonClient = getRedissonClient(wxCpTpMultiProperties);
} else {
redissonClient = applicationContext.getBean(RedissonClient.class);
}
return new WxCpTpRedissonConfigImpl(redissonClient, wxCpTpMultiProperties.getConfigStorage().getKeyPrefix());
}
private RedissonClient getRedissonClient(WxCpTpMultiProperties wxCpTpMultiProperties) {
WxCpTpMultiProperties.ConfigStorage storage = wxCpTpMultiProperties.getConfigStorage();
WxCpTpMultiRedisProperties redis = storage.getRedis();
Config config = new Config();
config.useSingleServer()
.setAddress("redis://" + redis.getHost() + ":" + redis.getPort())
.setDatabase(redis.getDatabase())
.setPassword(redis.getPassword());
config.setTransportMode(TransportMode.NIO);
return Redisson.create(config);
}
}

View File

@@ -0,0 +1,129 @@
package com.binarywang.spring.starter.wxjava.cp.properties;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.NestedConfigurationProperty;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* 企业微信多企业接入相关配置属性
*
* @author yl
* created on 2023/10/16
*/
@Data
@NoArgsConstructor
@ConfigurationProperties(prefix = WxCpTpMultiProperties.PREFIX)
public class WxCpTpMultiProperties implements Serializable {
private static final long serialVersionUID = -1569510477055668503L;
public static final String PREFIX = "wx.cp.tp";
private Map<String, WxCpTpSingleProperties> corps = new HashMap<>();
/**
* 配置存储策略,默认内存
*/
private ConfigStorage configStorage = new ConfigStorage();
@Data
@NoArgsConstructor
public static class ConfigStorage implements Serializable {
private static final long serialVersionUID = 4815731027000065434L;
/**
* 存储类型
*/
private StorageType type = StorageType.memory;
/**
* 指定key前缀
*/
private String keyPrefix = "wx:cp:tp";
/**
* redis连接配置
*/
@NestedConfigurationProperty
private WxCpTpMultiRedisProperties redis = new WxCpTpMultiRedisProperties();
/**
* http客户端类型.
*/
private HttpClientType httpClientType = HttpClientType.HTTP_CLIENT;
/**
* http代理主机
*/
private String httpProxyHost;
/**
* http代理端口
*/
private Integer httpProxyPort;
/**
* http代理用户名
*/
private String httpProxyUsername;
/**
* http代理密码
*/
private String httpProxyPassword;
/**
* http 请求最大重试次数
* <pre>
* {@link me.chanjar.weixin.cp.api.WxCpService#setMaxRetryTimes(int)}
* {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setMaxRetryTimes(int)}
* </pre>
*/
private int maxRetryTimes = 5;
/**
* http 请求重试间隔
* <pre>
* {@link me.chanjar.weixin.cp.api.WxCpService#setRetrySleepMillis(int)}
* {@link me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl#setRetrySleepMillis(int)}
* </pre>
*/
private int retrySleepMillis = 1000;
}
public enum StorageType {
/**
* 内存
*/
memory,
/**
* jedis
*/
jedis,
/**
* redisson
*/
redisson,
/**
* redistemplate
*/
redistemplate
}
public enum HttpClientType {
/**
* HttpClient
*/
HTTP_CLIENT,
/**
* OkHttp
*/
OK_HTTP,
/**
* JoddHttp
*/
JODD_HTTP
}
}

View File

@@ -0,0 +1,48 @@
package com.binarywang.spring.starter.wxjava.cp.properties;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* Redis配置.
*
* @author yl
* created on 2023/10/16
*/
@Data
@NoArgsConstructor
public class WxCpTpMultiRedisProperties implements Serializable {
private static final long serialVersionUID = -5924815351660074401L;
/**
* 主机地址.
*/
private String host;
/**
* 端口号.
*/
private int port = 6379;
/**
* 密码.
*/
private String password;
/**
* 超时.
*/
private int timeout = 2000;
/**
* 数据库.
*/
private int database = 0;
private Integer maxActive;
private Integer maxIdle;
private Integer maxWaitMillis;
private Integer minIdle;
}

View File

@@ -0,0 +1,43 @@
package com.binarywang.spring.starter.wxjava.cp.properties;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 企业微信企业相关配置属性
*
* @author yl
* created on 2023/10/16
*/
@Data
@NoArgsConstructor
public class WxCpTpSingleProperties implements Serializable {
private static final long serialVersionUID = -7502823825007859418L;
/**
* 微信企业号 corpId
*/
private String corpId;
/**
* 微信企业号 服务商 providerSecret
*/
private String providerSecret;
/**
* 微信企业号应用 token
*/
private String token;
private String encodingAESKey;
/**
* 微信企业号 第三方 应用 ID
*/
private String suiteId;
/**
* 微信企业号应用
*/
private String suiteSecret;
}

View File

@@ -0,0 +1,29 @@
package com.binarywang.spring.starter.wxjava.cp.service;
import me.chanjar.weixin.cp.tp.service.WxCpTpService;
/**
* 企业微信 {@link WxCpTpService} 所有实例存放类.
*
* @author yl
* created on 2023/10/16
*/
public interface WxCpTpMultiServices {
/**
* 通过租户 Id 获取 WxCpTpService
*
* @param tenantId 租户 Id
* @return WxCpTpService
*/
WxCpTpService getWxCpTpService(String tenantId);
void addWxCpTpService(String tenantId, WxCpTpService wxCpService);
/**
* 根据租户 Id从列表中移除一个 WxCpTpService 实例
*
* @param tenantId 租户 Id
*/
void removeWxCpTpService(String tenantId);
}

View File

@@ -0,0 +1,44 @@
package com.binarywang.spring.starter.wxjava.cp.service;
import me.chanjar.weixin.cp.tp.service.WxCpTpService;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 企业微信 {@link WxCpTpMultiServices} 默认实现
*
* @author yl
* created on 2023/10/16
*/
public class WxCpTpMultiServicesImpl implements WxCpTpMultiServices {
private final Map<String, WxCpTpService> services = new ConcurrentHashMap<>();
/**
* 通过租户 Id 获取 WxCpTpService
*
* @param tenantId 租户 Id
* @return WxCpTpService
*/
@Override
public WxCpTpService getWxCpTpService(String tenantId) {
return this.services.get(tenantId);
}
/**
* 根据租户 Id添加一个 WxCpTpService 到列表
*
* @param tenantId 租户 Id
* @param wxCpService WxCpTpService 实例
*/
@Override
public void addWxCpTpService(String tenantId, WxCpTpService wxCpService) {
this.services.put(tenantId, wxCpService);
}
@Override
public void removeWxCpTpService(String tenantId) {
this.services.remove(tenantId);
}
}

View File

@@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.binarywang.spring.starter.wxjava.cp.autoconfigure.WxCpTpMultiAutoConfiguration

View File

@@ -0,0 +1 @@
com.binarywang.spring.starter.wxjava.cp.autoconfigure.WxCpTpMultiAutoConfiguration

View File

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

View File

@@ -2,6 +2,7 @@ package com.binarywang.spring.starter.wxjava.miniapp.configuration;
import com.binarywang.spring.starter.wxjava.miniapp.configuration.services.WxMaInJedisConfiguration;
import com.binarywang.spring.starter.wxjava.miniapp.configuration.services.WxMaInMemoryConfiguration;
import com.binarywang.spring.starter.wxjava.miniapp.configuration.services.WxMaInRedisTemplateConfiguration;
import com.binarywang.spring.starter.wxjava.miniapp.configuration.services.WxMaInRedissonConfiguration;
import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@@ -17,9 +18,10 @@ import org.springframework.context.annotation.Import;
@Configuration
@EnableConfigurationProperties(WxMaMultiProperties.class)
@Import({
WxMaInJedisConfiguration.class,
WxMaInMemoryConfiguration.class,
WxMaInRedissonConfiguration.class,
WxMaInJedisConfiguration.class,
WxMaInMemoryConfiguration.class,
WxMaInRedissonConfiguration.class,
WxMaInRedisTemplateConfiguration.class
})
public class WxMaMultiServiceConfiguration {
}

View File

@@ -125,6 +125,8 @@ public abstract class AbstractWxMaConfiguration {
}
config.setMsgDataFormat(properties.getMsgDataFormat());
config.useStableAccessToken(useStableAccessToken);
config.setApiHostUrl(StringUtils.trimToNull(properties.getApiHostUrl()));
config.setAccessTokenUrl(StringUtils.trimToNull(properties.getAccessTokenUrl()));
}
private void configHttp(WxMaDefaultConfigImpl config, WxMaMultiProperties.ConfigStorage storage) {

View File

@@ -0,0 +1,43 @@
package com.binarywang.spring.starter.wxjava.miniapp.configuration.services;
import cn.binarywang.wx.miniapp.config.impl.WxMaDefaultConfigImpl;
import cn.binarywang.wx.miniapp.config.impl.WxMaRedisBetterConfigImpl;
import com.binarywang.spring.starter.wxjava.miniapp.properties.WxMaMultiProperties;
import com.binarywang.spring.starter.wxjava.miniapp.service.WxMaMultiServices;
import lombok.RequiredArgsConstructor;
import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* 自动装配基于 redisTemplate 策略配置
*
* @author <a href="mailto:huangbing0730@gmail">hb0730</a> 2025/9/10
*/
@Configuration
@ConditionalOnProperty(prefix = WxMaMultiProperties.PREFIX + ".config-storage", name = "type", havingValue = "redis_template")
@RequiredArgsConstructor
public class WxMaInRedisTemplateConfiguration extends AbstractWxMaConfiguration {
private final WxMaMultiProperties wxMaMultiProperties;
private final ApplicationContext applicationContext;
@Bean
public WxMaMultiServices wxMaMultiServices() {
return this.wxMaMultiServices(wxMaMultiProperties);
}
@Override
protected WxMaDefaultConfigImpl wxMaConfigStorage(WxMaMultiProperties wxMaMultiProperties) {
return this.configRedisTemplate(wxMaMultiProperties);
}
private WxMaDefaultConfigImpl configRedisTemplate(WxMaMultiProperties wxMaMultiProperties) {
StringRedisTemplate redisTemplate = applicationContext.getBean(StringRedisTemplate.class);
RedisTemplateWxRedisOps wxRedisOps = new RedisTemplateWxRedisOps(redisTemplate);
return new WxMaRedisBetterConfigImpl(wxRedisOps, wxMaMultiProperties.getConfigStorage().getKeyPrefix());
}
}

View File

@@ -42,4 +42,16 @@ public class WxMaSingleProperties implements Serializable {
* 是否使用稳定版 Access Token
*/
private boolean useStableAccessToken = false;
/**
* 自定义API主机地址用于替换默认的 https://api.weixin.qq.com
* 例如http://proxy.company.com:8080
*/
private String apiHostUrl;
/**
* 自定义获取AccessToken地址用于向自定义统一服务获取AccessToken
* 例如http://proxy.company.com:8080/oauth/token
*/
private String accessTokenUrl;
}

View File

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

View File

@@ -19,6 +19,8 @@ public abstract class AbstractWxMaConfigStorageConfiguration {
config.setAesKey(StringUtils.trimToNull(properties.getAesKey()));
config.setMsgDataFormat(StringUtils.trimToNull(properties.getMsgDataFormat()));
config.useStableAccessToken(properties.isUseStableAccessToken());
config.setApiHostUrl(StringUtils.trimToNull(properties.getApiHostUrl()));
config.setAccessTokenUrl(StringUtils.trimToNull(properties.getAccessTokenUrl()));
WxMaProperties.ConfigStorage configStorageProperties = properties.getConfigStorage();
config.setHttpProxyHost(configStorageProperties.getHttpProxyHost());

View File

@@ -49,6 +49,18 @@ public class WxMaProperties {
*/
private boolean useStableAccessToken = false;
/**
* 自定义API主机地址用于替换默认的 https://api.weixin.qq.com
* 例如http://proxy.company.com:8080
*/
private String apiHostUrl;
/**
* 自定义获取AccessToken地址用于向自定义统一服务获取AccessToken
* 例如http://proxy.company.com:8080/oauth/token
*/
private String accessTokenUrl;
/**
* 存储策略
*/

View File

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

View File

@@ -110,7 +110,7 @@ public class WxMpProperties {
/**
* 读数据超时时间即socketTimeout单位毫秒
*/
private int soTimeout = 1;
private int soTimeout = 5000;
/**
* 从连接池获取链接的超时时间,单位毫秒

View File

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

View File

@@ -4,6 +4,7 @@ import com.binarywang.spring.starter.wxjava.open.properties.WxOpenProperties;
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder;
import me.chanjar.weixin.open.api.impl.WxOpenInMemoryConfigStorage;
import org.apache.commons.lang3.StringUtils;
/**
* @author yl
@@ -31,6 +32,10 @@ public abstract class AbstractWxOpenConfigStorageConfiguration {
config.setRetrySleepMillis(retrySleepMillis);
config.setMaxRetryTimes(maxRetryTimes);
// 设置URL配置
config.setApiHostUrl(StringUtils.trimToNull(properties.getApiHostUrl()));
config.setAccessTokenUrl(StringUtils.trimToNull(properties.getAccessTokenUrl()));
// 设置自定义的HttpClient超时配置
ApacheHttpClientBuilder clientBuilder = config.getApacheHttpClientBuilder();
if (clientBuilder == null) {

View File

@@ -40,6 +40,18 @@ public class WxOpenProperties {
*/
private String aesKey;
/**
* 自定义API主机地址用于替换默认的 https://api.weixin.qq.com
* 例如http://proxy.company.com:8080
*/
private String apiHostUrl;
/**
* 自定义获取AccessToken地址用于向自定义统一服务获取AccessToken
* 例如http://proxy.company.com:8080/oauth/token
*/
private String accessTokenUrl;
/**
* 存储策略.
*/
@@ -116,7 +128,7 @@ public class WxOpenProperties {
/**
* 读数据超时时间即socketTimeout单位毫秒
*/
private int soTimeout = 1;
private int soTimeout = 5000;
/**
* 从连接池获取链接的超时时间,单位毫秒

View File

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

View File

@@ -50,15 +50,18 @@ public class WxPayAutoConfiguration {
payConfig.setSubMchId(StringUtils.trimToNull(this.properties.getSubMchId()));
payConfig.setKeyPath(StringUtils.trimToNull(this.properties.getKeyPath()));
payConfig.setUseSandboxEnv(this.properties.isUseSandboxEnv());
payConfig.setNotifyUrl(StringUtils.trimToNull(this.properties.getNotifyUrl()));
//以下是apiv3以及支付分相关
payConfig.setServiceId(StringUtils.trimToNull(this.properties.getServiceId()));
payConfig.setPayScoreNotifyUrl(StringUtils.trimToNull(this.properties.getPayScoreNotifyUrl()));
payConfig.setPayScorePermissionNotifyUrl(StringUtils.trimToNull(this.properties.getPayScorePermissionNotifyUrl()));
payConfig.setPrivateKeyPath(StringUtils.trimToNull(this.properties.getPrivateKeyPath()));
payConfig.setPrivateCertPath(StringUtils.trimToNull(this.properties.getPrivateCertPath()));
payConfig.setCertSerialNo(StringUtils.trimToNull(this.properties.getCertSerialNo()));
payConfig.setApiV3Key(StringUtils.trimToNull(this.properties.getApiv3Key()));
payConfig.setPublicKeyId(StringUtils.trimToNull(this.properties.getPublicKeyId()));
payConfig.setPublicKeyPath(StringUtils.trimToNull(this.properties.getPublicKeyPath()));
payConfig.setApiHostUrl(StringUtils.trimToNull(this.properties.getApiHostUrl()));
wxPayService.setConfig(payConfig);
return wxPayService;

View File

@@ -59,11 +59,21 @@ public class WxPayProperties {
*/
private String apiv3Key;
/**
* 微信支付异步回调地址通知url必须为直接可访问的url不能携带参数
*/
private String notifyUrl;
/**
* 微信支付分回调地址
*/
private String payScoreNotifyUrl;
/**
* 微信支付分授权回调地址
*/
private String payScorePermissionNotifyUrl;
/**
* apiv3 商户apiclient_key.pem
*/
@@ -90,4 +100,10 @@ public class WxPayProperties {
*/
private boolean useSandboxEnv;
/**
* 自定义API主机地址用于替换默认的 https://api.mch.weixin.qq.com
* 例如http://proxy.company.com:8080
*/
private String apiHostUrl;
}

View File

@@ -3,7 +3,7 @@
<parent>
<artifactId>wx-java-spring-boot-starters</artifactId>
<groupId>com.github.binarywang</groupId>
<version>4.7.7.B</version>
<version>4.7.8.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.7.B</version>
<version>4.7.8.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.7.B</version>
<version>4.7.8.B</version>
</parent>
<artifactId>weixin-java-channel</artifactId>

View File

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

View File

@@ -53,4 +53,10 @@ public interface ApacheHttpClientBuilder {
* ssl连接socket工厂.
*/
ApacheHttpClientBuilder sslConnectionSocketFactory(SSLConnectionSocketFactory sslConnectionSocketFactory);
/**
* 支持的TLS协议版本.
* Supported TLS protocol versions.
*/
ApacheHttpClientBuilder supportedProtocols(String[] supportedProtocols);
}

View File

@@ -117,6 +117,13 @@ public class ApacheHttpDnsClientBuilder implements ApacheHttpClientBuilder {
return this;
}
@Override
public ApacheHttpClientBuilder supportedProtocols(String[] supportedProtocols) {
// This implementation doesn't use the supportedProtocols parameter as it relies on the provided SSLConnectionSocketFactory
// Users should configure the SSLConnectionSocketFactory with desired protocols before setting it
return this;
}
/**
* 获取链接的超时时间设置,默认3000ms
* <p>

View File

@@ -93,6 +93,12 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder {
*/
private String userAgent;
/**
* 支持的TLS协议版本默认支持现代TLS版本
* Supported TLS protocol versions, defaults to modern TLS versions
*/
private String[] supportedProtocols = {"TLSv1.2", "TLSv1.3", "TLSv1.1", "TLSv1"};
/**
* 自定义请求拦截器
*/
@@ -179,6 +185,12 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder {
return this;
}
@Override
public ApacheHttpClientBuilder supportedProtocols(String[] supportedProtocols) {
this.supportedProtocols = supportedProtocols;
return this;
}
public IdleConnectionMonitorThread getIdleConnectionMonitorThread() {
return this.idleConnectionMonitorThread;
}
@@ -257,7 +269,7 @@ public class DefaultApacheHttpClientBuilder implements ApacheHttpClientBuilder {
return new SSLConnectionSocketFactory(
sslcontext,
new String[]{"TLSv1"},
this.supportedProtocols,
null,
SSLConnectionSocketFactory.getDefaultHostnameVerifier());
} catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {

View File

@@ -1,16 +1,11 @@
package me.chanjar.weixin.common.util.locks;
import lombok.Getter;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.data.redis.core.types.Expiration;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
@@ -70,15 +65,16 @@ public class RedisTemplateSimpleDistributedLock implements Lock {
value = UUID.randomUUID().toString();
valueThreadLocal.set(value);
}
final byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
final byte[] valueBytes = value.getBytes(StandardCharsets.UTF_8);
List<Object> redisResults = redisTemplate.executePipelined((RedisCallback<String>) connection -> {
connection.set(keyBytes, valueBytes, Expiration.milliseconds(leaseMilliseconds), RedisStringCommands.SetOption.SET_IF_ABSENT);
connection.get(keyBytes);
return null;
});
Object currentLockSecret = redisResults.size() > 1 ? redisResults.get(1) : redisResults.get(0);
return currentLockSecret != null && currentLockSecret.toString().equals(value);
// Use high-level StringRedisTemplate API to ensure consistent key serialization
Boolean lockAcquired = redisTemplate.opsForValue().setIfAbsent(key, value, leaseMilliseconds, TimeUnit.MILLISECONDS);
if (Boolean.TRUE.equals(lockAcquired)) {
return true;
}
// Check if we already hold the lock (reentrant behavior)
String currentValue = redisTemplate.opsForValue().get(key);
return value.equals(currentValue);
}
@Override

View File

@@ -0,0 +1,116 @@
package me.chanjar.weixin.common.util.http.apache;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.impl.client.CloseableHttpClient;
import org.testng.Assert;
import org.testng.annotations.Test;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.SSLSocketFactory;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
/**
* 测试SSL配置特别是TLS协议版本配置
* Test SSL configuration, especially TLS protocol version configuration
*/
public class SSLConfigurationTest {
@Test
public void testDefaultTLSProtocols() throws Exception {
// Create a new instance to check the default configuration
Class<?> builderClass = DefaultApacheHttpClientBuilder.class;
Object builder = builderClass.getDeclaredMethod("get").invoke(null);
// 验证默认支持的TLS协议版本包含现代版本
Field supportedProtocolsField = builderClass.getDeclaredField("supportedProtocols");
supportedProtocolsField.setAccessible(true);
String[] supportedProtocols = (String[]) supportedProtocolsField.get(builder);
List<String> protocolList = Arrays.asList(supportedProtocols);
System.out.println("Default supported TLS protocols: " + Arrays.toString(supportedProtocols));
// 主要验证应该支持TLS 1.2和/或1.3 (现代安全版本)
// Main validation: Should support TLS 1.2 and/or 1.3 (modern secure versions)
Assert.assertTrue(protocolList.contains("TLSv1.2"), "Should support TLS 1.2");
Assert.assertTrue(protocolList.contains("TLSv1.3"), "Should support TLS 1.3");
// 验证不再是只有TLS 1.0 (这是导致原问题的根本原因)
// Verify it's no longer just TLS 1.0 (which was the root cause of the original issue)
Assert.assertTrue(protocolList.size() > 0, "Should support at least one TLS version");
boolean hasModernTLS = protocolList.contains("TLSv1.2") || protocolList.contains("TLSv1.3");
Assert.assertTrue(hasModernTLS, "Should support at least one modern TLS version (1.2 or 1.3)");
// 验证不是原来的老旧配置 (只有 "TLSv1")
// Verify it's not the old configuration (only "TLSv1")
boolean isOldConfig = protocolList.size() == 1 && protocolList.contains("TLSv1");
Assert.assertFalse(isOldConfig, "Should not be the old configuration that only supported TLS 1.0");
}
@Test
public void testCustomTLSProtocols() throws Exception {
// Test that we can set custom TLS protocols
String[] customProtocols = {"TLSv1.2", "TLSv1.3"};
// Create a new builder instance using reflection to avoid singleton issues in testing
Class<?> builderClass = DefaultApacheHttpClientBuilder.class;
Constructor<?> constructor = builderClass.getDeclaredConstructor();
constructor.setAccessible(true);
Object builder = constructor.newInstance();
// Set custom protocols
builderClass.getMethod("supportedProtocols", String[].class).invoke(builder, (Object) customProtocols);
Field supportedProtocolsField = builderClass.getDeclaredField("supportedProtocols");
supportedProtocolsField.setAccessible(true);
String[] actualProtocols = (String[]) supportedProtocolsField.get(builder);
Assert.assertEquals(actualProtocols, customProtocols, "Custom protocols should be set correctly");
System.out.println("Custom supported TLS protocols: " + Arrays.toString(actualProtocols));
}
@Test
public void testSSLContextCreation() throws Exception {
DefaultApacheHttpClientBuilder builder = DefaultApacheHttpClientBuilder.get();
// 构建HTTP客户端以验证SSL工厂是否正确创建
CloseableHttpClient client = builder.build();
Assert.assertNotNull(client, "HTTP client should be created successfully");
// 验证SSL上下文支持现代TLS协议
SSLContext sslContext = SSLContext.getDefault();
SSLSocketFactory socketFactory = sslContext.getSocketFactory();
// 创建一个SSL socket来检查支持的协议
try (SSLSocket socket = (SSLSocket) socketFactory.createSocket()) {
String[] supportedProtocols = socket.getSupportedProtocols();
List<String> supportedList = Arrays.asList(supportedProtocols);
// JVM应该支持TLS 1.2在JDK 8+中默认可用)
Assert.assertTrue(supportedList.contains("TLSv1.2"),
"JVM should support TLS 1.2. Supported protocols: " + Arrays.toString(supportedProtocols));
System.out.println("JVM supported TLS protocols: " + Arrays.toString(supportedProtocols));
}
client.close();
}
@Test
public void testBuilderChaining() {
DefaultApacheHttpClientBuilder builder = DefaultApacheHttpClientBuilder.get();
// 测试方法链调用
ApacheHttpClientBuilder result = builder
.supportedProtocols(new String[]{"TLSv1.2", "TLSv1.3"})
.httpProxyHost("proxy.example.com")
.httpProxyPort(8080);
Assert.assertSame(result, builder, "Builder methods should return the same instance for method chaining");
}
}

View File

@@ -0,0 +1,73 @@
package me.chanjar.weixin.common.util.http.apache;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.testng.Assert;
import org.testng.annotations.Test;
/**
* 集成测试 - 验证SSL配置可以正常访问HTTPS网站
* Integration test - Verify SSL configuration can access HTTPS websites properly
*/
public class SSLIntegrationTest {
@Test
public void testHTTPSConnectionWithModernTLS() throws Exception {
DefaultApacheHttpClientBuilder builder = DefaultApacheHttpClientBuilder.get();
// 使用默认配置支持现代TLS版本创建客户端
CloseableHttpClient client = builder.build();
// 测试访问一个需要现代TLS的网站
// Test accessing a website that requires modern TLS
HttpGet httpGet = new HttpGet("https://api.weixin.qq.com/");
try (CloseableHttpResponse response = client.execute(httpGet)) {
// 验证能够成功建立HTTPS连接不管响应内容是什么
// Verify that HTTPS connection can be established successfully (regardless of response content)
Assert.assertNotNull(response, "Should be able to establish HTTPS connection");
Assert.assertNotNull(response.getStatusLine(), "Should receive a status response");
int statusCode = response.getStatusLine().getStatusCode();
// 任何HTTP状态码都表示SSL握手成功
// Any HTTP status code indicates successful SSL handshake
Assert.assertTrue(statusCode > 0, "Should receive a valid HTTP status code, got: " + statusCode);
System.out.println("HTTPS connection test successful. Status: " + response.getStatusLine());
} catch (javax.net.ssl.SSLHandshakeException e) {
Assert.fail("SSL handshake should not fail with modern TLS configuration. Error: " + e.getMessage());
} finally {
client.close();
}
}
@Test
public void testCustomTLSConfiguration() throws Exception {
DefaultApacheHttpClientBuilder builder = DefaultApacheHttpClientBuilder.get();
// 配置为只支持TLS 1.2和1.3(最安全的配置)
// Configure to only support TLS 1.2 and 1.3 (most secure configuration)
builder.supportedProtocols(new String[]{"TLSv1.2", "TLSv1.3"});
CloseableHttpClient client = builder.build();
// 测试这个配置是否能正常工作
HttpGet httpGet = new HttpGet("https://httpbin.org/get");
try (CloseableHttpResponse response = client.execute(httpGet)) {
Assert.assertNotNull(response, "Should be able to establish HTTPS connection with TLS 1.2/1.3");
int statusCode = response.getStatusLine().getStatusCode();
Assert.assertEquals(statusCode, 200, "Should get HTTP 200 response from httpbin.org");
System.out.println("Custom TLS configuration test successful. Status: " + response.getStatusLine());
} catch (javax.net.ssl.SSLHandshakeException e) {
// 这个测试可能会因为网络环境而失败,所以我们只是记录警告
// This test might fail due to network environment, so we just log a warning
System.out.println("Warning: SSL handshake failed with custom TLS config: " + e.getMessage());
System.out.println("This might be due to network restrictions in the test environment.");
} finally {
client.close();
}
}
}

View File

@@ -0,0 +1,100 @@
package me.chanjar.weixin.common.util.locks;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
import static org.testng.Assert.*;
/**
* 测试 RedisTemplateSimpleDistributedLock 在自定义 Key 序列化时的兼容性
*
* 这个测试验证修复后的实现确保 tryLock 和 unlock 使用一致的键序列化方式
*/
@Test(enabled = false) // 默认禁用需要Redis实例才能运行
public class RedisTemplateSimpleDistributedLockSerializationTest {
private RedisTemplateSimpleDistributedLock redisLock;
private StringRedisTemplate redisTemplate;
@BeforeTest
public void init() {
JedisConnectionFactory connectionFactory = new JedisConnectionFactory();
connectionFactory.setHostName("127.0.0.1");
connectionFactory.setPort(6379);
connectionFactory.afterPropertiesSet();
// 创建一个带自定义键序列化的 StringRedisTemplate
StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory);
// 使用自定义键序列化器,模拟在键前面添加前缀的场景
redisTemplate.setKeySerializer(new StringRedisSerializer() {
@Override
public byte[] serialize(String string) {
if (string == null) return null;
// 添加 "System:" 前缀,模拟用户自定义的键序列化
return super.serialize("System:" + string);
}
@Override
public String deserialize(byte[] bytes) {
if (bytes == null) return null;
String result = super.deserialize(bytes);
// 移除前缀进行反序列化
return result != null && result.startsWith("System:") ? result.substring(7) : result;
}
});
this.redisTemplate = redisTemplate;
this.redisLock = new RedisTemplateSimpleDistributedLock(redisTemplate, "test_lock_key", 60000);
}
@Test(description = "测试自定义键序列化器下的锁操作一致性")
public void testLockConsistencyWithCustomKeySerializer() {
// 1. 获取锁应该成功
assertTrue(redisLock.tryLock(), "第一次获取锁应该成功");
assertNotNull(redisLock.getLockSecretValue(), "锁值应该存在");
// 2. 验证键已正确存储(通过 redisTemplate 直接查询)
String actualValue = redisTemplate.opsForValue().get("test_lock_key");
assertEquals(actualValue, redisLock.getLockSecretValue(), "通过 redisTemplate 查询的值应该与锁值相同");
// 3. 再次尝试获取同一把锁应该成功(可重入)
assertTrue(redisLock.tryLock(), "可重入锁应该再次获取成功");
// 4. 释放锁应该成功
redisLock.unlock();
assertNull(redisLock.getLockSecretValue(), "释放锁后锁值应该为空");
// 5. 验证键已被删除
actualValue = redisTemplate.opsForValue().get("test_lock_key");
assertNull(actualValue, "释放锁后 Redis 中的键应该被删除");
// 6. 释放已释放的锁应该是安全的
redisLock.unlock(); // 不应该抛出异常
}
@Test(description = "测试不同线程使用相同键的锁排他性")
public void testLockExclusivityWithCustomKeySerializer() throws InterruptedException {
// 第一个锁实例获取锁
assertTrue(redisLock.tryLock(), "第一个锁实例应该成功获取锁");
// 创建第二个锁实例使用相同的键
RedisTemplateSimpleDistributedLock anotherLock = new RedisTemplateSimpleDistributedLock(
redisTemplate, "test_lock_key", 60000);
// 第二个锁实例不应该能获取锁
assertFalse(anotherLock.tryLock(), "第二个锁实例不应该能获取已被占用的锁");
// 释放第一个锁
redisLock.unlock();
// 现在第二个锁实例应该能获取锁
assertTrue(anotherLock.tryLock(), "第一个锁释放后,第二个锁实例应该能获取锁");
// 清理
anotherLock.unlock();
}
}

View File

@@ -1,8 +1,10 @@
package me.chanjar.weixin.common.util.locks;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
@@ -13,9 +15,10 @@ import java.util.concurrent.atomic.AtomicInteger;
import static org.testng.Assert.*;
@Slf4j
@Test(enabled = false)
@Test(enabled = true)
public class RedisTemplateSimpleDistributedLockTest {
private static final String KEY_PREFIX = "System:";
RedisTemplateSimpleDistributedLock redisLock;
StringRedisTemplate redisTemplate;
@@ -29,6 +32,28 @@ public class RedisTemplateSimpleDistributedLockTest {
connectionFactory.setPort(6379);
connectionFactory.afterPropertiesSet();
StringRedisTemplate redisTemplate = new StringRedisTemplate(connectionFactory);
// 自定义序列化器,为 key 自动加前缀
redisTemplate.setKeySerializer(new StringRedisSerializer() {
@NotNull
@Override
public byte[] serialize(String string) {
if (string == null) {
return super.serialize(null);
}
// 添加前缀
return super.serialize(KEY_PREFIX + string);
}
@NotNull
@Override
public String deserialize(byte[] bytes) {
String key = super.deserialize(bytes);
if (key.startsWith(KEY_PREFIX)) {
return key.substring(KEY_PREFIX.length());
}
return key;
}
});
this.redisTemplate = redisTemplate;
this.redisLock = new RedisTemplateSimpleDistributedLock(redisTemplate, 60000);
this.lockCurrentExecuteCounter = new AtomicInteger(0);

View File

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

View File

@@ -168,9 +168,13 @@ public class WxCpAgentWorkBench implements Serializable {
webview.addProperty("url", this.url);
webview.addProperty("jump_url", this.jumpUrl);
webview.addProperty("pagepath", this.pagePath);
webview.addProperty("enable_webview_click", this.enableWebviewClick);
if (this.enableWebviewClick != null) {
webview.addProperty("enable_webview_click", this.enableWebviewClick);
}
webview.addProperty("height", this.height);
webview.addProperty("hide_title", this.hideTitle);
if (this.hideTitle != null) {
webview.addProperty("hide_title", this.hideTitle);
}
templateObject.add("webview", webview);
break;
}
@@ -236,9 +240,13 @@ public class WxCpAgentWorkBench implements Serializable {
webview.addProperty("url", this.url);
webview.addProperty("jump_url", this.jumpUrl);
webview.addProperty("pagepath", this.pagePath);
webview.addProperty("enable_webview_click", this.enableWebviewClick);
if (this.enableWebviewClick != null) {
webview.addProperty("enable_webview_click", this.enableWebviewClick);
}
webview.addProperty("height", this.height);
webview.addProperty("hide_title", this.hideTitle);
if (this.hideTitle != null) {
webview.addProperty("hide_title", this.hideTitle);
}
JsonObject dataObject = new JsonObject();
dataObject.addProperty("type", WxCpConsts.WorkBenchType.WEBVIEW);
dataObject.add("webview", webview);

View File

@@ -793,4 +793,6 @@ public class WxCpTpXmlMessage implements Serializable {
log.debug("解密后的原始xml消息内容{}", plainText);
return fromXml(plainText);
}
}

View File

@@ -92,7 +92,7 @@ public class WxCpXmlMessage implements Serializable {
private String content;
@XStreamAlias("MsgId")
private Long msgId;
private String msgId;
@XStreamAlias("PicUrl")
@XStreamConverter(value = XStreamCDataConverter.class)
@@ -159,6 +159,14 @@ public class WxCpXmlMessage implements Serializable {
@XStreamConverter(value = XStreamCDataConverter.class)
private String MemChangeList;
@XStreamAlias("LastMemVer")
@XStreamConverter(value = XStreamCDataConverter.class)
private String lastMemVer;
@XStreamAlias("CurMemVer")
@XStreamConverter(value = XStreamCDataConverter.class)
private String curMemVer;
@XStreamAlias("Source")
@XStreamConverter(value = XStreamCDataConverter.class)
private String source;

View File

@@ -151,6 +151,30 @@ public class WxCpCheckinGroupBase implements Serializable {
*/
@SerializedName("flex_off_duty_time")
private Integer flexOffDutyTime;
/**
* 是否允许弹性时间
*/
@SerializedName("allow_flex")
private Boolean allowFlex;
/**
* 迟到规则
*/
@SerializedName("late_rule")
private LateRule lateRule;
/**
* 最早可打卡时间限制
*/
@SerializedName("max_allow_arrive_early")
private Integer maxAllowArriveEarly;
/**
* 最晚可打卡时间限制
*/
@SerializedName("max_allow_arrive_late")
private Integer maxAllowArriveLate;
}
/**
@@ -160,6 +184,13 @@ public class WxCpCheckinGroupBase implements Serializable {
public static class CheckinTime implements Serializable {
private static final long serialVersionUID = -5507709858609705279L;
/**
* 时段id为班次中某一堆上下班时间组合的id
*/
@SerializedName("time_id")
private Integer timeId;
/**
* 上班时间表示为距离当天0点的秒数。
*/
@@ -183,6 +214,60 @@ public class WxCpCheckinGroupBase implements Serializable {
*/
@SerializedName("remind_off_work_sec")
private Integer remindOffWorkSec;
/**
* 休息开始时间仅单时段支持距离0点的秒
*/
@SerializedName("rest_begin_time")
private Integer restBeginTime;
/**
* 休息结束时间仅单时段支持距离0点的秒
*/
@SerializedName("rest_end_time")
private Integer restEndTime;
/**
* 是否允许休息
*/
@SerializedName("allow_rest")
private Boolean allowRest;
/**
* 最早可打卡时间距离0点的秒数
*/
@SerializedName("earliest_work_sec")
private Integer earliestWorkSec;
/**
* 最晚可打卡时间距离0点的秒数
*/
@SerializedName("latest_work_sec")
private Integer latestWorkSec;
/**
* 最早可下班打卡时间距离0点的秒数
*/
@SerializedName("earliest_off_work_sec")
private Integer earliestOffWorkSec;
/**
* 最晚可下班打卡时间距离0点的秒数
*/
@SerializedName("latest_off_work_sec")
private Integer latestOffWorkSec;
/**
* 上班无需打卡
*/
@SerializedName("no_need_checkon")
private Boolean noNeedCheckon;
/**
* 下班无需打卡
*/
@SerializedName("no_need_checkoff")
private Boolean noNeedCheckoff;
}
/**
@@ -438,6 +523,17 @@ public class WxCpCheckinGroupBase implements Serializable {
private static final long serialVersionUID = 5604969713950037053L;
/**
* 晚走的时间 距离最晚一个下班的时间单位:秒
*/
@SerializedName("offwork_after_time")
private Integer offWorkAfterTime;
/**
* 第二天第一个班次允许迟到的弹性时间单位:秒
*/
@SerializedName("onwork_flex_time")
private Integer onWorkFlexTime;
/**
* 是否允许超时下班下班晚走次日晚到允许时onwork_flex_timeoffwork_after_time才有意义

View File

@@ -53,6 +53,12 @@ public class WxCpCropCheckinOption extends WxCpCheckinGroupBase implements Seria
@SerializedName("ot_info")
private OtInfo otInfo;
/**
* 加班信息V2新版API返回的加班信息结构
*/
@SerializedName("ot_info_v2")
private OtInfoV2 otInfoV2;
/**
* 每月最多补卡次数,默认-1表示不限制
*/
@@ -418,4 +424,94 @@ public class WxCpCropCheckinOption extends WxCpCheckinGroupBase implements Seria
private Integer otNonworkingDaySpanDayTime;
}
/**
* 加班信息V2新版API返回的加班信息结构
*/
@Data
public static class OtInfoV2 implements Serializable {
private static final long serialVersionUID = 1610150484871066200L;
/**
* 工作日加班配置
*/
@SerializedName("workdayconf")
private WorkdayConf workdayConf;
/**
* 非工作日加班配置
*/
@SerializedName("restdayconf")
private RestdayConf restdayConf;
/**
* 节假日加班配置
*/
@SerializedName("holidayconf")
private HolidayConf holidayConf;
/**
* 工作日加班配置
*/
@Data
public static class WorkdayConf implements Serializable {
private static final long serialVersionUID = 1610150484871066201L;
/**
* 是否允许工作日加班true为允许false为不允许
*/
@SerializedName("allow_ot")
private Boolean allowOt;
/**
* 加班类型
* 0以加班申请核算打卡记录根据打卡记录和加班申请核算,
* 1以打卡时间为准根据打卡时间计算
* 2: 以加班申请审批为准(只根据加班申请计算)
*/
@SerializedName("type")
private Integer type;
}
/**
* 非工作日加班配置
*/
@Data
public static class RestdayConf implements Serializable {
private static final long serialVersionUID = 1610150484871066202L;
/**
* 是否允许非工作日加班true为允许false为不允许
*/
@SerializedName("allow_ot")
private Boolean allowOt;
/**
* 加班类型
*/
@SerializedName("type")
private Integer type;
}
/**
* 节假日加班配置
*/
@Data
public static class HolidayConf implements Serializable {
private static final long serialVersionUID = 1610150484871066203L;
/**
* 是否允许节假日加班true为允许false为不允许
*/
@SerializedName("allow_ot")
private Boolean allowOt;
/**
* 加班类型
*/
@SerializedName("type")
private Integer type;
}
}
}

View File

@@ -17,7 +17,7 @@ import java.util.List;
/**
* 审批模板详情
*
* @author gyv12345 @163.com / Wang_Wong
* @author gyv12345@163.com / Wang_Wong
*/
@Data
@Builder
@@ -121,7 +121,7 @@ public class WxCpOaApprovalTemplateResult implements Serializable {
/**
* 获取审批模板详情value为list类型
* https://developer.work.weixin.qq.com/document/path/91982
* <a href="https://developer.work.weixin.qq.com/document/path/91982">文档链接</a>
*/
@SerializedName("value")
private List<TemplateTitle> value;

View File

@@ -65,6 +65,35 @@ public class WxCpMeeting implements Serializable, ToJson {
@SerializedName("agentid")
private Integer agentId;
/**
* 发起人所在部门
*/
@SerializedName("main_department")
private Integer mainDepartment;
/**
* 会议的状态。
* 1待开始
* 2会议中
* 3已结束
* 4已取消
* 5已过期
*/
@SerializedName("status")
public Integer status;
/**
* 会议类型。
* 0一次性会议
* 1周期性会议
* 2微信专属会议
* 3Rooms 投屏会议
* 5个人会议号会议
* 6网络研讨会
*/
@SerializedName("meeting_type")
private Integer meetingType;
/**
* 参与会议的成员。会议人数上限以指定的「管理员」可预约的人数上限来校验普通企业与会人员最多100

View File

@@ -3,6 +3,7 @@ package me.chanjar.weixin.cp.bean.oa.templatedata;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* The type Template options.
@@ -17,11 +18,8 @@ public class TemplateOptions implements Serializable {
private String key;
/**
* 创建审批模板value为对象类型
* https://developer.work.weixin.qq.com/document/path/97437#%E9%99%845-%E5%8D%95%E9%80%89%E5%A4%9A%E9%80%89%E6%8E%A7%E4%BB%B6%EF%BC%88control%E5%8F%82%E6%95%B0%E4%B8%BAselector%EF%BC%89
*
* 获取审批模板详情value为list类型
* https://developer.work.weixin.qq.com/document/path/91982
* <a href="https://developer.work.weixin.qq.com/document/path/97437#%E9%99%845-%E5%8D%95%E9%80%89%E5%A4%9A%E9%80%89%E6%8E%A7%E4%BB%B6%EF%BC%88control%E5%8F%82%E6%95%B0%E4%B8%BAselector%EF%BC%89">创建审批模板value为对象类型</a>
* <a href="https://developer.work.weixin.qq.com/document/path/91982">获取审批模板详情value为list类型</a>
**/
private TemplateTitle value;
private List<TemplateTitle> value;
}

View File

@@ -130,7 +130,7 @@ public interface WxCpTpConfigStorage {
* @return the aes key
*/
//第三方应用的EncodingAESKey用来检查签名
String getAesKey();
String getEncodingAESKey();
/**
* 企微服务商企业ID & 企业secret

View File

@@ -180,4 +180,17 @@ public abstract class AbstractWxCpInRedisConfigImpl extends WxCpDefaultConfigImp
Long expire = redisOps.getExpire(this.agentJsapiTicketKey);
return expire == null || expire < 2;
}
@Override
public String toString() {
return "AbstractWxCpInRedisConfigImpl{" +
"corpId='" + getCorpId() + '\'' +
", agentId=" + getAgentId() +
", keyPrefix='" + keyPrefix + '\'' +
", accessTokenKey='" + accessTokenKey + '\'' +
", jsapiTicketKey='" + jsapiTicketKey + '\'' +
", agentJsapiTicketKey='" + agentJsapiTicketKey + '\'' +
", lockKey='" + lockKey + '\'' +
'}';
}
}

View File

@@ -0,0 +1,431 @@
package me.chanjar.weixin.cp.config.impl;
import lombok.Builder;
import lombok.NonNull;
import lombok.Setter;
import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.common.redis.RedissonWxRedisOps;
import me.chanjar.weixin.common.redis.WxRedisOps;
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
import me.chanjar.weixin.cp.bean.WxCpProviderToken;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
import org.apache.commons.lang3.StringUtils;
import org.redisson.api.RedissonClient;
import java.io.File;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
/**
* 企业微信各种固定、授权配置的Redisson存储实现
*/
public abstract class AbstractWxCpTpInRedisConfigImpl extends WxCpTpDefaultConfigImpl implements Serializable {
private static final long serialVersionUID = -5385639031981770319L;
public AbstractWxCpTpInRedisConfigImpl(@NonNull WxRedisOps wxRedisOps) {
this(wxRedisOps, null);
}
public AbstractWxCpTpInRedisConfigImpl(@NonNull WxRedisOps wxRedisOps, String keyPrefix) {
this.wxRedisOps = wxRedisOps;
this.keyPrefix = keyPrefix;
}
/**
* The constant LOCK_KEY.
*/
// lock key
protected static final String LOCK_KEY = "wechat_tp_lock:";
/**
* The constant LOCKER_PROVIDER_ACCESS_TOKEN.
*/
protected static final String LOCKER_PROVIDER_ACCESS_TOKEN = "providerAccessTokenLock";
/**
* The constant LOCKER_SUITE_ACCESS_TOKEN.
*/
protected static final String LOCKER_SUITE_ACCESS_TOKEN = "suiteAccessTokenLock";
/**
* The constant LOCKER_ACCESS_TOKEN.
*/
protected static final String LOCKER_ACCESS_TOKEN = "accessTokenLock";
/**
* The constant LOCKER_CORP_JSAPI_TICKET.
*/
protected static final String LOCKER_CORP_JSAPI_TICKET = "corpJsapiTicketLock";
/**
* The constant LOCKER_SUITE_JSAPI_TICKET.
*/
protected static final String LOCKER_SUITE_JSAPI_TICKET = "suiteJsapiTicketLock";
@NonNull
private final WxRedisOps wxRedisOps;
private final String suiteAccessTokenKey = ":suiteAccessTokenKey:";
private final String suiteTicketKey = ":suiteTicketKey:";
private final String accessTokenKey = ":accessTokenKey:";
private final String authCorpJsApiTicketKey = ":authCorpJsApiTicketKey:";
private final String authSuiteJsApiTicketKey = ":authSuiteJsApiTicketKey:";
private final String providerTokenKey = ":providerTokenKey:";
/**
* redis里面key的统一前缀
*/
@Setter
private String keyPrefix = "";
private volatile String baseApiUrl;
private volatile String httpProxyHost;
private volatile int httpProxyPort;
private volatile String httpProxyUsername;
private volatile String httpProxyPassword;
private volatile ApacheHttpClientBuilder apacheHttpClientBuilder;
private volatile File tmpDirFile;
@Override
public void setBaseApiUrl(String baseUrl) {
this.baseApiUrl = baseUrl;
}
@Override
public String getApiUrl(String path) {
if (baseApiUrl == null) {
baseApiUrl = "https://qyapi.weixin.qq.com";
}
return baseApiUrl + path;
}
/**
* 第三方应用的suite access token相关
*/
@Override
public String getSuiteAccessToken() {
return wxRedisOps.getValue(keyWithPrefix(suiteAccessTokenKey));
}
@Override
public WxAccessToken getSuiteAccessTokenEntity() {
String suiteAccessToken = wxRedisOps.getValue(keyWithPrefix(suiteAccessTokenKey));
Long expireIn = wxRedisOps.getExpire(keyWithPrefix(suiteAccessTokenKey));
if (StringUtils.isBlank(suiteAccessToken) || expireIn == null || expireIn == 0 || expireIn == -2) {
return new WxAccessToken();
}
WxAccessToken suiteAccessTokenEntity = new WxAccessToken();
suiteAccessTokenEntity.setAccessToken(suiteAccessToken);
suiteAccessTokenEntity.setExpiresIn(Math.max(Math.toIntExact(expireIn), 0));
return suiteAccessTokenEntity;
}
@Override
public boolean isSuiteAccessTokenExpired() {
//remain time to live in seconds, or key not exist
return wxRedisOps.getExpire(keyWithPrefix(suiteAccessTokenKey)) == 0L || wxRedisOps.getExpire(keyWithPrefix(suiteAccessTokenKey)) == -2;
}
@Override
public void expireSuiteAccessToken() {
wxRedisOps.expire(keyWithPrefix(suiteAccessTokenKey), 0, TimeUnit.SECONDS);
}
@Override
public void updateSuiteAccessToken(WxAccessToken suiteAccessToken) {
updateSuiteAccessToken(suiteAccessToken.getAccessToken(), suiteAccessToken.getExpiresIn());
}
@Override
public void updateSuiteAccessToken(String suiteAccessToken, int expiresInSeconds) {
wxRedisOps.setValue(keyWithPrefix(suiteAccessTokenKey), suiteAccessToken, expiresInSeconds, TimeUnit.SECONDS);
}
/**
* 第三方应用的suite ticket相关
*/
@Override
public String getSuiteTicket() {
return wxRedisOps.getValue(keyWithPrefix(suiteTicketKey));
}
@Override
public boolean isSuiteTicketExpired() {
//remain time to live in seconds, or key not exist
return wxRedisOps.getExpire(keyWithPrefix(suiteTicketKey)) == 0L || wxRedisOps.getExpire(keyWithPrefix(suiteTicketKey)) == -2;
}
@Override
public void expireSuiteTicket() {
wxRedisOps.expire(keyWithPrefix(suiteTicketKey), 0, TimeUnit.SECONDS);
}
@Override
public void updateSuiteTicket(String suiteTicket, int expiresInSeconds) {
wxRedisOps.setValue(keyWithPrefix(suiteTicketKey), suiteTicket, expiresInSeconds, TimeUnit.SECONDS);
}
/**
* 第三方应用的其他配置,来自于企微配置
*/
@Override
public String getSuiteId() {
return suiteId;
}
@Override
public String getSuiteSecret() {
return suiteSecret;
}
// 第三方应用的token用来检查应用的签名
@Override
public String getToken() {
return token;
}
//第三方应用的EncodingAESKey用来检查签名
@Override
public String getEncodingAESKey() {
return encodingAESKey;
}
/**
* 企微服务商企业ID & 企业secret, 来自于企微配置
*/
@Override
public String getCorpId() {
return corpId;
}
@Override
public String getProviderSecret() {
return providerSecret;
}
@Override
public void setProviderSecret(String providerSecret) {
this.providerSecret = providerSecret;
}
/**
* 授权企业的access token相关
*/
@Override
public String getAccessToken(String authCorpId) {
return wxRedisOps.getValue(keyWithPrefix(authCorpId) + accessTokenKey);
}
@Override
public WxAccessToken getAccessTokenEntity(String authCorpId) {
String accessToken = wxRedisOps.getValue(keyWithPrefix(authCorpId) + accessTokenKey);
Long expire = wxRedisOps.getExpire(keyWithPrefix(authCorpId) + accessTokenKey);
if (StringUtils.isBlank(accessToken) || expire == null || expire == 0 || expire == -2) {
return new WxAccessToken();
}
WxAccessToken accessTokenEntity = new WxAccessToken();
accessTokenEntity.setAccessToken(accessToken);
accessTokenEntity.setExpiresIn((int) ((expire - System.currentTimeMillis()) / 1000 + 200));
return accessTokenEntity;
}
@Override
public boolean isAccessTokenExpired(String authCorpId) {
//没有设置或者TTL为0都是过期
return wxRedisOps.getExpire(keyWithPrefix(authCorpId) + accessTokenKey) == 0L
|| wxRedisOps.getExpire(keyWithPrefix(authCorpId) + accessTokenKey) == -2;
}
@Override
public void expireAccessToken(String authCorpId) {
wxRedisOps.expire(keyWithPrefix(authCorpId) + accessTokenKey, 0, TimeUnit.SECONDS);
}
@Override
public void updateAccessToken(String authCorpId, String accessToken, int expiredInSeconds) {
wxRedisOps.setValue(keyWithPrefix(authCorpId) + accessTokenKey, accessToken, expiredInSeconds, TimeUnit.SECONDS);
}
/**
* 授权企业的js api ticket相关
*/
@Override
public String getAuthCorpJsApiTicket(String authCorpId) {
return wxRedisOps.getValue(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey);
}
@Override
public boolean isAuthCorpJsApiTicketExpired(String authCorpId) {
//没有设置或TTL为0,都是过期
return wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey) == 0L
|| wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey) == -2;
}
@Override
public void expireAuthCorpJsApiTicket(String authCorpId) {
wxRedisOps.expire(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey, 0, TimeUnit.SECONDS);
}
@Override
public void updateAuthCorpJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds) {
wxRedisOps.setValue(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey, jsApiTicket, expiredInSeconds,
TimeUnit.SECONDS);
}
/**
* 授权企业的第三方应用js api ticket相关
*/
@Override
public String getAuthSuiteJsApiTicket(String authCorpId) {
return wxRedisOps.getValue(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey);
}
@Override
public boolean isAuthSuiteJsApiTicketExpired(String authCorpId) {
//没有设置或者TTL为0都是过期
return wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey) == 0L
|| wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey) == -2;
}
@Override
public void expireAuthSuiteJsApiTicket(String authCorpId) {
wxRedisOps.expire(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey, 0, TimeUnit.SECONDS);
}
@Override
public void updateAuthSuiteJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds) {
wxRedisOps.setValue(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey, jsApiTicket, expiredInSeconds,
TimeUnit.SECONDS);
}
@Override
public boolean isProviderTokenExpired() {
//remain time to live in seconds, or key not exist
return wxRedisOps.getExpire(providerKeyWithPrefix(providerTokenKey)) == 0L || wxRedisOps.getExpire(providerKeyWithPrefix(providerTokenKey)) == -2;
}
@Override
public void updateProviderToken(String providerToken, int expiredInSeconds) {
wxRedisOps.setValue(providerKeyWithPrefix(providerTokenKey), providerToken, expiredInSeconds, TimeUnit.SECONDS);
}
@Override
public String getProviderToken() {
return wxRedisOps.getValue(providerKeyWithPrefix(providerTokenKey));
}
@Override
public WxCpProviderToken getProviderTokenEntity() {
String providerToken = wxRedisOps.getValue(providerKeyWithPrefix(providerTokenKey));
Long expire = wxRedisOps.getExpire(providerKeyWithPrefix(providerTokenKey));
if (StringUtils.isBlank(providerToken) || expire == null || expire == 0 || expire == -2) {
return new WxCpProviderToken();
}
WxCpProviderToken wxCpProviderToken = new WxCpProviderToken();
wxCpProviderToken.setProviderAccessToken(providerToken);
wxCpProviderToken.setExpiresIn(Math.max(Math.toIntExact(expire), 0));
return wxCpProviderToken;
}
@Override
public void expireProviderToken() {
wxRedisOps.expire(providerKeyWithPrefix(providerTokenKey), 0, TimeUnit.SECONDS);
}
/**
* 网络代理相关
*/
@Override
public String getHttpProxyHost() {
return this.httpProxyHost;
}
@Override
public int getHttpProxyPort() {
return this.httpProxyPort;
}
@Override
public String getHttpProxyUsername() {
return this.httpProxyUsername;
}
@Override
public String getHttpProxyPassword() {
return this.httpProxyPassword;
}
@Override
public File getTmpDirFile() {
return tmpDirFile;
}
@Override
public Lock getProviderAccessTokenLock() {
return getProviderLockByKey(String.join(":", this.corpId, LOCKER_PROVIDER_ACCESS_TOKEN));
}
@Override
public Lock getSuiteAccessTokenLock() {
return getLockByKey(LOCKER_SUITE_ACCESS_TOKEN);
}
@Override
public Lock getAccessTokenLock(String authCorpId) {
return getLockByKey(String.join(":", authCorpId, LOCKER_ACCESS_TOKEN));
}
@Override
public Lock getAuthCorpJsapiTicketLock(String authCorpId) {
return getLockByKey(String.join(":", authCorpId, LOCKER_CORP_JSAPI_TICKET));
}
@Override
public Lock getSuiteJsapiTicketLock(String authCorpId) {
return getLockByKey(String.join(":", authCorpId, LOCKER_SUITE_JSAPI_TICKET));
}
private Lock getLockByKey(String key) {
// 最终key的模式(keyPrefix:)wechat_tp_lock:suiteId:(authCorpId):lockKey
// 其中keyPrefix目前不支持外部配置authCorpId只有涉及到corpAccessToken, suiteJsapiTicket, authCorpJsapiTicket时才会拼上
return this.wxRedisOps.getLock(String.join(":", keyWithPrefix(LOCK_KEY + this.suiteId), key));
}
/**
* 单独处理provider,且不应和suite 有关系
*/
private Lock getProviderLockByKey(String key) {
return this.wxRedisOps.getLock(String.join(":", providerKeyWithPrefix(LOCK_KEY), key));
}
@Override
public ApacheHttpClientBuilder getApacheHttpClientBuilder() {
return this.apacheHttpClientBuilder;
}
@Override
public boolean autoRefreshToken() {
return false;
}
@Override
public String toString() {
return WxCpGsonBuilder.create().toJson(this);
}
/**
* 一个provider 会有多个suite,需要唯一标识作为前缀
*/
private String keyWithPrefix(String key) {
return keyPrefix + ":" + suiteId + ":" + key;
}
/**
* provider 应该独享一个key,且不和任何suite关联
* 一个provider 会有多个suite,不同的suite 都应该指向同一个provider 的数据
*/
private String providerKeyWithPrefix(String key) {
return keyPrefix + ":" + corpId + ":" + key;
}
}

View File

@@ -28,20 +28,32 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl
private final transient Map<String, Lock> accessTokenLocker = new ConcurrentHashMap<>();
private final transient Map<String, Lock> authCorpJsapiTicketLocker = new ConcurrentHashMap<>();
private final transient Map<String, Lock> authSuiteJsapiTicketLocker = new ConcurrentHashMap<>();
private volatile String corpId;
private volatile String corpSecret;
/**
* 企微服务商企业ID & 企业secret来自于企微配置
*/
protected volatile String corpId;
/**
* 服务商secret
*/
private volatile String providerSecret;
protected volatile String providerSecret;
private volatile String providerToken;
private volatile long providerTokenExpiresTime;
private volatile String suiteId;
private volatile String suiteSecret;
private volatile String token;
/**
* 第三方应用的其他配置,来自于企微配置
*/
protected volatile String suiteId;
protected volatile String suiteSecret;
/**
* 第三方应用的token用来检查应用的签名
*/
protected volatile String token;
private volatile String suiteAccessToken;
private volatile long suiteAccessTokenExpiresTime;
private volatile String aesKey;
/**
* 第三方应用的EncodingAESKey用来检查签名
*/
protected volatile String encodingAESKey;
private volatile String suiteTicket;
private volatile long suiteTicketExpiresTime;
private volatile String oauth2redirectUri;
@@ -186,11 +198,10 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl
/**
* Sets suite id.
*
* @param corpId the corp id
* @param suiteId
*/
@Deprecated
public void setSuiteId(String corpId) {
this.suiteId = corpId;
public void setSuiteId(String suiteId) {
this.suiteId = suiteId;
}
@Override
@@ -200,10 +211,7 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl
/**
* Sets suite secret.
*
* @param corpSecret the corp secret
*/
@Deprecated
public void setSuiteSecret(String corpSecret) {
this.suiteSecret = corpSecret;
}
@@ -218,24 +226,22 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl
*
* @param token the token
*/
@Deprecated
public void setToken(String token) {
this.token = token;
}
@Override
public String getAesKey() {
return this.aesKey;
public String getEncodingAESKey() {
return this.encodingAESKey;
}
/**
* Sets aes key.
* Sets aes key. encodingAESKey
*
* @param aesKey the aes key
* @param encodingAESKey the aes key
*/
@Deprecated
public void setAesKey(String aesKey) {
this.aesKey = aesKey;
public void setEncodingAESKey(String encodingAESKey) {
this.encodingAESKey = encodingAESKey;
}
@@ -249,25 +255,15 @@ public class WxCpTpDefaultConfigImpl implements WxCpTpConfigStorage, Serializabl
*
* @param corpId the corp id
*/
@Deprecated
public void setCorpId(String corpId) {
this.corpId = corpId;
}
@Override
public String getCorpSecret() {
return this.corpSecret;
return this.providerSecret;
}
/**
* Sets corp secret.
*
* @param corpSecret the corp secret
*/
@Deprecated
public void setCorpSecret(String corpSecret) {
this.corpSecret = corpSecret;
}
@Override
public void setProviderSecret(String providerSecret) {

View File

@@ -0,0 +1,24 @@
package me.chanjar.weixin.cp.config.impl;
import lombok.NonNull;
import me.chanjar.weixin.common.redis.JedisWxRedisOps;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.util.Pool;
/**
* 基于 jedis 的实现
*
* @author yl
* created on 2023/04/23
*/
public class WxCpTpJedisConfigImpl extends AbstractWxCpTpInRedisConfigImpl {
private static final long serialVersionUID = -1869372247414407433L;
public WxCpTpJedisConfigImpl(Pool<Jedis> jedisPool) {
this(jedisPool, null);
}
public WxCpTpJedisConfigImpl(@NonNull Pool<Jedis> jedisPool, String keyPrefix) {
super(new JedisWxRedisOps(jedisPool), keyPrefix);
}
}

View File

@@ -0,0 +1,24 @@
package me.chanjar.weixin.cp.config.impl;
import lombok.Builder;
import lombok.NonNull;
import me.chanjar.weixin.common.redis.RedisTemplateWxRedisOps;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* 基于 RedisTemplate 的实现
*
* @author yl
* created on 2023/04/23
*/
public class WxCpTpRedisTemplateConfigImpl extends AbstractWxCpTpInRedisConfigImpl {
private static final long serialVersionUID = -1660004125413310620L;
public WxCpTpRedisTemplateConfigImpl(@NonNull StringRedisTemplate stringRedisTemplate) {
this(stringRedisTemplate, null);
}
public WxCpTpRedisTemplateConfigImpl(@NonNull StringRedisTemplate stringRedisTemplate, String keyPrefix) {
super(new RedisTemplateWxRedisOps(stringRedisTemplate), keyPrefix);
}
}

View File

@@ -1,449 +1,25 @@
package me.chanjar.weixin.cp.config.impl;
import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
import lombok.Setter;
import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.common.redis.WxRedisOps;
import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder;
import me.chanjar.weixin.cp.bean.WxCpProviderToken;
import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import me.chanjar.weixin.common.redis.RedissonWxRedisOps;
import org.redisson.api.RedissonClient;
/**
* 企业微信各种固定、授权配置的Redisson存储实现
* 基于Redisson实现
*
* @author yuanqixun created on 2020 /5/13
* @author yl
*/
@Builder
public class WxCpTpRedissonConfigImpl implements WxCpTpConfigStorage, Serializable {
private static final long serialVersionUID = -5385639031981770319L;
public class WxCpTpRedissonConfigImpl extends AbstractWxCpTpInRedisConfigImpl {
private static final long serialVersionUID = -5674792341070783967L;
/**
* The constant LOCK_KEY.
*/
// lock key
protected static final String LOCK_KEY = "wechat_tp_lock:";
/**
* The constant LOCKER_PROVIDER_ACCESS_TOKEN.
*/
protected static final String LOCKER_PROVIDER_ACCESS_TOKEN = "providerAccessTokenLock";
/**
* The constant LOCKER_SUITE_ACCESS_TOKEN.
*/
protected static final String LOCKER_SUITE_ACCESS_TOKEN = "suiteAccessTokenLock";
/**
* The constant LOCKER_ACCESS_TOKEN.
*/
protected static final String LOCKER_ACCESS_TOKEN = "accessTokenLock";
/**
* The constant LOCKER_CORP_JSAPI_TICKET.
*/
protected static final String LOCKER_CORP_JSAPI_TICKET = "corpJsapiTicketLock";
/**
* The constant LOCKER_SUITE_JSAPI_TICKET.
*/
protected static final String LOCKER_SUITE_JSAPI_TICKET = "suiteJsapiTicketLock";
@NonNull
private final WxRedisOps wxRedisOps;
private final String suiteAccessTokenKey = ":suiteAccessTokenKey:";
private final String suiteTicketKey = ":suiteTicketKey:";
private final String accessTokenKey = ":accessTokenKey:";
private final String authCorpJsApiTicketKey = ":authCorpJsApiTicketKey:";
private final String authSuiteJsApiTicketKey = ":authSuiteJsApiTicketKey:";
private final String providerTokenKey = ":providerTokenKey:";
/**
* redis里面key的统一前缀
*/
@Setter
private String keyPrefix = "";
private volatile String baseApiUrl;
private volatile String httpProxyHost;
private volatile int httpProxyPort;
private volatile String httpProxyUsername;
private volatile String httpProxyPassword;
private volatile ApacheHttpClientBuilder apacheHttpClientBuilder;
private volatile File tmpDirFile;
/**
* 第三方应用的其他配置,来自于企微配置
*/
private volatile String suiteId;
private volatile String suiteSecret;
/**
* 第三方应用的token用来检查应用的签名
*/
private volatile String token;
/**
* 第三方应用的EncodingAESKey用来检查签名
*/
private volatile String aesKey;
/**
* 企微服务商企业ID & 企业secret来自于企微配置
*/
private volatile String corpId;
private volatile String corpSecret;
/**
* 服务商secret
*/
private volatile String providerSecret;
@Override
public void setBaseApiUrl(String baseUrl) {
this.baseApiUrl = baseUrl;
public WxCpTpRedissonConfigImpl(@NonNull RedissonClient redissonClient) {
this(redissonClient, null);
}
@Override
public String getApiUrl(String path) {
if (baseApiUrl == null) {
baseApiUrl = "https://qyapi.weixin.qq.com";
}
return baseApiUrl + path;
}
/**
* 第三方应用的suite access token相关
*/
@Override
public String getSuiteAccessToken() {
return wxRedisOps.getValue(keyWithPrefix(suiteAccessTokenKey));
}
@Override
public WxAccessToken getSuiteAccessTokenEntity() {
String suiteAccessToken = wxRedisOps.getValue(keyWithPrefix(suiteAccessTokenKey));
Long expireIn = wxRedisOps.getExpire(keyWithPrefix(suiteAccessTokenKey));
if (StringUtils.isBlank(suiteAccessToken) || expireIn == null || expireIn == 0 || expireIn == -2) {
return new WxAccessToken();
}
WxAccessToken suiteAccessTokenEntity = new WxAccessToken();
suiteAccessTokenEntity.setAccessToken(suiteAccessToken);
suiteAccessTokenEntity.setExpiresIn(Math.max(Math.toIntExact(expireIn), 0));
return suiteAccessTokenEntity;
}
@Override
public boolean isSuiteAccessTokenExpired() {
//remain time to live in seconds, or key not exist
return wxRedisOps.getExpire(keyWithPrefix(suiteAccessTokenKey)) == 0L || wxRedisOps.getExpire(keyWithPrefix(suiteAccessTokenKey)) == -2;
}
@Override
public void expireSuiteAccessToken() {
wxRedisOps.expire(keyWithPrefix(suiteAccessTokenKey), 0, TimeUnit.SECONDS);
}
@Override
public void updateSuiteAccessToken(WxAccessToken suiteAccessToken) {
updateSuiteAccessToken(suiteAccessToken.getAccessToken(), suiteAccessToken.getExpiresIn());
}
@Override
public void updateSuiteAccessToken(String suiteAccessToken, int expiresInSeconds) {
wxRedisOps.setValue(keyWithPrefix(suiteAccessTokenKey), suiteAccessToken, expiresInSeconds, TimeUnit.SECONDS);
}
/**
* 第三方应用的suite ticket相关
*/
@Override
public String getSuiteTicket() {
return wxRedisOps.getValue(keyWithPrefix(suiteTicketKey));
}
@Override
public boolean isSuiteTicketExpired() {
//remain time to live in seconds, or key not exist
return wxRedisOps.getExpire(keyWithPrefix(suiteTicketKey)) == 0L || wxRedisOps.getExpire(keyWithPrefix(suiteTicketKey)) == -2;
}
@Override
public void expireSuiteTicket() {
wxRedisOps.expire(keyWithPrefix(suiteTicketKey), 0, TimeUnit.SECONDS);
}
@Override
public void updateSuiteTicket(String suiteTicket, int expiresInSeconds) {
wxRedisOps.setValue(keyWithPrefix(suiteTicketKey), suiteTicket, expiresInSeconds, TimeUnit.SECONDS);
}
/**
* 第三方应用的其他配置,来自于企微配置
*/
@Override
public String getSuiteId() {
return suiteId;
}
@Override
public String getSuiteSecret() {
return suiteSecret;
}
// 第三方应用的token用来检查应用的签名
@Override
public String getToken() {
return token;
}
//第三方应用的EncodingAESKey用来检查签名
@Override
public String getAesKey() {
return aesKey;
}
/**
* 企微服务商企业ID & 企业secret, 来自于企微配置
*/
@Override
public String getCorpId() {
return corpId;
}
@Override
public String getCorpSecret() {
return corpSecret;
}
@Override
public void setProviderSecret(String providerSecret) {
this.providerSecret = providerSecret;
}
@Override
public String getProviderSecret() {
return providerSecret;
}
/**
* 授权企业的access token相关
*/
@Override
public String getAccessToken(String authCorpId) {
return wxRedisOps.getValue(keyWithPrefix(authCorpId) + accessTokenKey);
}
@Override
public WxAccessToken getAccessTokenEntity(String authCorpId) {
String accessToken = wxRedisOps.getValue(keyWithPrefix(authCorpId) + accessTokenKey);
Long expire = wxRedisOps.getExpire(keyWithPrefix(authCorpId) + accessTokenKey);
if (StringUtils.isBlank(accessToken) || expire == null || expire == 0 || expire == -2) {
return new WxAccessToken();
}
WxAccessToken accessTokenEntity = new WxAccessToken();
accessTokenEntity.setAccessToken(accessToken);
accessTokenEntity.setExpiresIn((int) ((expire - System.currentTimeMillis()) / 1000 + 200));
return accessTokenEntity;
}
@Override
public boolean isAccessTokenExpired(String authCorpId) {
//没有设置或者TTL为0都是过期
return wxRedisOps.getExpire(keyWithPrefix(authCorpId) + accessTokenKey) == 0L
|| wxRedisOps.getExpire(keyWithPrefix(authCorpId) + accessTokenKey) == -2;
}
@Override
public void expireAccessToken(String authCorpId) {
wxRedisOps.expire(keyWithPrefix(authCorpId) + accessTokenKey, 0, TimeUnit.SECONDS);
}
@Override
public void updateAccessToken(String authCorpId, String accessToken, int expiredInSeconds) {
wxRedisOps.setValue(keyWithPrefix(authCorpId) + accessTokenKey, accessToken, expiredInSeconds, TimeUnit.SECONDS);
}
/**
* 授权企业的js api ticket相关
*/
@Override
public String getAuthCorpJsApiTicket(String authCorpId) {
return wxRedisOps.getValue(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey);
}
@Override
public boolean isAuthCorpJsApiTicketExpired(String authCorpId) {
//没有设置或TTL为0,都是过期
return wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey) == 0L
|| wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey) == -2;
}
@Override
public void expireAuthCorpJsApiTicket(String authCorpId) {
wxRedisOps.expire(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey, 0, TimeUnit.SECONDS);
}
@Override
public void updateAuthCorpJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds) {
wxRedisOps.setValue(keyWithPrefix(authCorpId) + authCorpJsApiTicketKey, jsApiTicket, expiredInSeconds,
TimeUnit.SECONDS);
}
/**
* 授权企业的第三方应用js api ticket相关
*/
@Override
public String getAuthSuiteJsApiTicket(String authCorpId) {
return wxRedisOps.getValue(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey);
}
@Override
public boolean isAuthSuiteJsApiTicketExpired(String authCorpId) {
//没有设置或者TTL为0都是过期
return wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey) == 0L
|| wxRedisOps.getExpire(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey) == -2;
}
@Override
public void expireAuthSuiteJsApiTicket(String authCorpId) {
wxRedisOps.expire(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey, 0, TimeUnit.SECONDS);
}
@Override
public void updateAuthSuiteJsApiTicket(String authCorpId, String jsApiTicket, int expiredInSeconds) {
wxRedisOps.setValue(keyWithPrefix(authCorpId) + authSuiteJsApiTicketKey, jsApiTicket, expiredInSeconds,
TimeUnit.SECONDS);
}
@Override
public boolean isProviderTokenExpired() {
//remain time to live in seconds, or key not exist
return wxRedisOps.getExpire(providerKeyWithPrefix(providerTokenKey)) == 0L || wxRedisOps.getExpire(providerKeyWithPrefix(providerTokenKey)) == -2;
}
@Override
public void updateProviderToken(String providerToken, int expiredInSeconds) {
wxRedisOps.setValue(providerKeyWithPrefix(providerTokenKey), providerToken, expiredInSeconds, TimeUnit.SECONDS);
}
@Override
public String getProviderToken() {
return wxRedisOps.getValue(providerKeyWithPrefix(providerTokenKey));
}
@Override
public WxCpProviderToken getProviderTokenEntity() {
String providerToken = wxRedisOps.getValue(providerKeyWithPrefix(providerTokenKey));
Long expire = wxRedisOps.getExpire(providerKeyWithPrefix(providerTokenKey));
if (StringUtils.isBlank(providerToken) || expire == null || expire == 0 || expire == -2) {
return new WxCpProviderToken();
}
WxCpProviderToken wxCpProviderToken = new WxCpProviderToken();
wxCpProviderToken.setProviderAccessToken(providerToken);
wxCpProviderToken.setExpiresIn(Math.max(Math.toIntExact(expire), 0));
return wxCpProviderToken;
}
@Override
public void expireProviderToken() {
wxRedisOps.expire(providerKeyWithPrefix(providerTokenKey), 0, TimeUnit.SECONDS);
}
/**
* 网络代理相关
*/
@Override
public String getHttpProxyHost() {
return this.httpProxyHost;
}
@Override
public int getHttpProxyPort() {
return this.httpProxyPort;
}
@Override
public String getHttpProxyUsername() {
return this.httpProxyUsername;
}
@Override
public String getHttpProxyPassword() {
return this.httpProxyPassword;
}
@Override
public File getTmpDirFile() {
return tmpDirFile;
}
@Override
public Lock getProviderAccessTokenLock() {
return getProviderLockByKey(String.join(":", this.corpId, LOCKER_PROVIDER_ACCESS_TOKEN));
}
@Override
public Lock getSuiteAccessTokenLock() {
return getLockByKey(LOCKER_SUITE_ACCESS_TOKEN);
}
@Override
public Lock getAccessTokenLock(String authCorpId) {
return getLockByKey(String.join(":", authCorpId, LOCKER_ACCESS_TOKEN));
}
@Override
public Lock getAuthCorpJsapiTicketLock(String authCorpId) {
return getLockByKey(String.join(":", authCorpId, LOCKER_CORP_JSAPI_TICKET));
}
@Override
public Lock getSuiteJsapiTicketLock(String authCorpId) {
return getLockByKey(String.join(":", authCorpId, LOCKER_SUITE_JSAPI_TICKET));
}
private Lock getLockByKey(String key) {
// 最终key的模式(keyPrefix:)wechat_tp_lock:suiteId:(authCorpId):lockKey
// 其中keyPrefix目前不支持外部配置authCorpId只有涉及到corpAccessToken, suiteJsapiTicket, authCorpJsapiTicket时才会拼上
return this.wxRedisOps.getLock(String.join(":", keyWithPrefix(LOCK_KEY + this.suiteId), key));
}
/**
* 单独处理provider,且不应和suite 有关系
*/
private Lock getProviderLockByKey(String key) {
return this.wxRedisOps.getLock(String.join(":", providerKeyWithPrefix(LOCK_KEY), key));
}
@Override
public ApacheHttpClientBuilder getApacheHttpClientBuilder() {
return this.apacheHttpClientBuilder;
}
@Override
public boolean autoRefreshToken() {
return false;
}
@Override
public String toString() {
return WxCpGsonBuilder.create().toJson(this);
}
/**
* 一个provider 会有多个suite,需要唯一标识作为前缀
*/
private String keyWithPrefix(String key) {
return keyPrefix + ":" + suiteId + ":" + key;
}
/**
* provider 应该独享一个key,且不和任何suite关联
* 一个provider 会有多个suite,不同的suite 都应该指向同一个provider 的数据
*/
private String providerKeyWithPrefix(String key) {
return keyPrefix + ":" + corpId + ":" + key;
public WxCpTpRedissonConfigImpl(@NonNull RedissonClient redissonClient, String keyPrefix) {
super(new RedissonWxRedisOps(redissonClient), keyPrefix);
}
}

View File

@@ -855,6 +855,10 @@ public interface WxCpApiPathConsts {
* The constant GET_PERMANENT_CODE.
*/
String GET_PERMANENT_CODE = "/cgi-bin/service/get_permanent_code";
/**
* The constant GET_V2_PERMANENT_CODE.
*/
String GET_V2_PERMANENT_CODE = "/cgi-bin/service/v2/get_permanent_code";
/**
* The constant GET_SUITE_TOKEN.
*/

View File

@@ -8,6 +8,7 @@ import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor;
import me.chanjar.weixin.common.util.http.RequestExecutor;
import me.chanjar.weixin.common.util.http.RequestHttp;
import me.chanjar.weixin.cp.bean.*;
import me.chanjar.weixin.cp.bean.message.WxCpTpXmlMessage;
import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
import java.util.List;
@@ -186,6 +187,8 @@ public interface WxCpTpService {
@Deprecated
WxCpTpCorp getPermanentCode(String authCode) throws WxErrorException;
WxCpTpCorp getV2PermanentCode(String authCode) throws WxErrorException;
/**
* 获取企业永久授权码信息
* <pre>
@@ -200,6 +203,8 @@ public interface WxCpTpService {
*/
WxCpTpPermanentCodeInfo getPermanentCodeInfo(String authCode) throws WxErrorException;
WxCpTpPermanentCodeInfo getV2PermanentCodeInfo(String authCode) throws WxErrorException;
/**
* <pre>
* 获取预授权链接
@@ -343,9 +348,7 @@ public interface WxCpTpService {
* 获取WxCpTpConfigStorage 对象.
*
* @return WxCpTpConfigStorage wx cp tp config storage
* @deprecated storage应该在service内部使用 ,提供这个接口,容易破坏这个封装
*/
@Deprecated
WxCpTpConfigStorage getWxCpTpConfigStorage();
/**
@@ -527,6 +530,11 @@ public interface WxCpTpService {
*/
WxCpTpLicenseService getWxCpTpLicenseService();
WxCpTpXmlMessage fromEncryptedXml(String encryptedXml,
String timestamp, String nonce, String msgSignature);
String getVerifyDecrypt(String sVerifyEchoStr);
/**
* 获取应用的管理员列表
*

View File

@@ -25,8 +25,10 @@ import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor;
import me.chanjar.weixin.common.util.json.GsonParser;
import me.chanjar.weixin.common.util.json.WxGsonBuilder;
import me.chanjar.weixin.cp.bean.*;
import me.chanjar.weixin.cp.bean.message.WxCpTpXmlMessage;
import me.chanjar.weixin.cp.config.WxCpTpConfigStorage;
import me.chanjar.weixin.cp.tp.service.*;
import me.chanjar.weixin.cp.util.crypto.WxCpTpCryptUtil;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
@@ -91,6 +93,7 @@ public abstract class BaseWxCpTpServiceImpl<H, P> implements WxCpTpService, Requ
private final WxSessionManager sessionManager = new StandardSessionManager();
/**
* 临时文件目录.
*/
@@ -259,6 +262,18 @@ public abstract class BaseWxCpTpServiceImpl<H, P> implements WxCpTpService, Requ
return wxCpTpCorp;
}
@Override
public WxCpTpCorp getV2PermanentCode(String authCode) throws WxErrorException {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("auth_code", authCode);
String result = post(configStorage.getApiUrl(GET_V2_PERMANENT_CODE), jsonObject.toString());
jsonObject = GsonParser.parse(result);
WxCpTpCorp wxCpTpCorp = WxCpTpCorp.fromJson(jsonObject.get("auth_corp_info").getAsJsonObject().toString());
wxCpTpCorp.setPermanentCode(jsonObject.get("permanent_code").getAsString());
return wxCpTpCorp;
}
@Override
public WxCpTpPermanentCodeInfo getPermanentCodeInfo(String authCode) throws WxErrorException {
JsonObject jsonObject = new JsonObject();
@@ -267,6 +282,14 @@ public abstract class BaseWxCpTpServiceImpl<H, P> implements WxCpTpService, Requ
return WxCpTpPermanentCodeInfo.fromJson(result);
}
@Override
public WxCpTpPermanentCodeInfo getV2PermanentCodeInfo(String authCode) throws WxErrorException {
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("auth_code", authCode);
String result = post(configStorage.getApiUrl(GET_V2_PERMANENT_CODE), jsonObject.toString());
return WxCpTpPermanentCodeInfo.fromJson(result);
}
@Override
@SneakyThrows
public String getPreAuthUrl(String redirectUri, String state) throws WxErrorException {
@@ -452,7 +475,7 @@ public abstract class BaseWxCpTpServiceImpl<H, P> implements WxCpTpService, Requ
if (error.getErrorCode() == WxCpErrorMsgEnum.CODE_42009.getCode()) {
// 强制设置wxCpTpConfigStorage它的suite access token过期了这样在下一次请求里就会刷新suite access token
this.configStorage.expireSuiteAccessToken();
if (this.getWxCpTpConfigStorage().autoRefreshToken()) {
if (this.configStorage.autoRefreshToken()) {
log.warn("即将重新获取新的access_token错误代码{},错误信息:{}", error.getErrorCode(), error.getErrorMsg());
return this.execute(executor, uri, data);
}
@@ -646,6 +669,27 @@ public abstract class BaseWxCpTpServiceImpl<H, P> implements WxCpTpService, Requ
this.wxCpTpUserService = wxCpTpUserService;
}
/**
*
* @param encryptedXml the encrypted xml
* @param timestamp the timestamp
* @param nonce the nonce
* @param msgSignature the msg signature
* @return the wx cp tp xml message
*/
@Override
public WxCpTpXmlMessage fromEncryptedXml(String encryptedXml,
String timestamp, String nonce, String msgSignature) {
return WxCpTpXmlMessage.fromEncryptedXml(encryptedXml,this.configStorage,timestamp,nonce,msgSignature);
}
@Override
public String getVerifyDecrypt(String sVerifyEchoStr) {
WxCpTpCryptUtil cryptUtil = new WxCpTpCryptUtil(this.configStorage);
return cryptUtil.decrypt(sVerifyEchoStr);
}
@Override
public WxCpTpAdmin getAdminList(String authCorpId, Integer agentId) throws WxErrorException {
JsonObject jsonObject = new JsonObject();
@@ -764,4 +808,9 @@ public abstract class BaseWxCpTpServiceImpl<H, P> implements WxCpTpService, Requ
public void setWxCpTpOAuth2Service(WxCpTpOAuth2Service wxCpTpOAuth2Service) {
this.wxCpTpOAuth2Service = wxCpTpOAuth2Service;
}
@Override
public WxCpTpConfigStorage getWxCpTpConfigStorage() {
return this.configStorage;
}
}

View File

@@ -101,9 +101,9 @@ public class WxCpTpServiceApacheHttpClientImpl extends BaseWxCpTpServiceImpl<Clo
this.httpClient = apacheHttpClientBuilder.build();
}
@Override
public WxCpTpConfigStorage getWxCpTpConfigStorage() {
return this.configStorage;
}
// @Override
// public WxCpTpConfigStorage getWxCpTpConfigStorage() {
// return this.configStorage;
// }
}

View File

@@ -98,9 +98,9 @@ public class WxCpTpServiceHttpComponentsImpl extends BaseWxCpTpServiceImpl<Close
this.httpClient = apacheHttpClientBuilder.build();
}
@Override
public WxCpTpConfigStorage getWxCpTpConfigStorage() {
return this.configStorage;
}
// @Override
// public WxCpTpConfigStorage getWxCpTpConfigStorage() {
// return this.configStorage;
// }
}

View File

@@ -0,0 +1,104 @@
package me.chanjar.weixin.cp.tp.service.impl;
import com.google.gson.JsonObject;
import jodd.http.HttpConnectionProvider;
import jodd.http.HttpRequest;
import jodd.http.HttpResponse;
import jodd.http.ProxyInfo;
import jodd.http.net.SocketHttpConnectionProvider;
import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.common.enums.WxType;
import me.chanjar.weixin.common.error.WxError;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.util.http.HttpClientType;
import me.chanjar.weixin.common.util.json.GsonParser;
import me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl;
import me.chanjar.weixin.cp.config.WxCpConfigStorage;
import me.chanjar.weixin.cp.constant.WxCpApiPathConsts;
/**
* The type Wx cp service jodd http.
*
* @author someone
*/
public class WxCpTpServiceJoddHttpImpl extends BaseWxCpTpServiceImpl<HttpConnectionProvider, ProxyInfo> {
private HttpConnectionProvider httpClient;
private ProxyInfo httpProxy;
@Override
public HttpConnectionProvider getRequestHttpClient() {
return httpClient;
}
@Override
public ProxyInfo getRequestHttpProxy() {
return httpProxy;
}
@Override
public HttpClientType getRequestType() {
return HttpClientType.JODD_HTTP;
}
@Override
public String getSuiteAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isSuiteAccessTokenExpired() && !forceRefresh) {
return this.configStorage.getSuiteAccessToken();
}
synchronized (this.globalSuiteAccessTokenRefreshLock) {
// 构建请求 URL
String url = this.configStorage.getApiUrl(WxCpApiPathConsts.Tp.GET_SUITE_TOKEN);
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("suite_id", this.configStorage.getSuiteId());
jsonObject.addProperty("suite_secret", this.configStorage.getSuiteSecret());
jsonObject.addProperty("suite_ticket", this.getSuiteTicket());
String jsonBody = jsonObject.toString();
if (this.httpProxy != null) {
httpClient.useProxy(this.httpProxy);
}
// 创建 POST 请求
HttpRequest request = HttpRequest
.post(url)
.contentType("application/json")
.body(jsonBody); // 使用 .body() 设置请求体
request.withConnectionProvider(httpClient);
// 发送请求
HttpResponse response = request.send();
// 解析响应
String resultContent = response.bodyText();
WxError error = WxError.fromJson(resultContent, WxType.CP);
if (error.getErrorCode() != 0) {
throw new WxErrorException(error);
}
// 更新 access token
jsonObject = GsonParser.parse(resultContent);
String suiteAccussToken = jsonObject.get("suite_access_token").getAsString();
int expiresIn = jsonObject.get("expires_in").getAsInt();
this.configStorage.updateSuiteAccessToken(suiteAccussToken, expiresIn);
}
return this.configStorage.getSuiteAccessToken();
}
@Override
public void initHttp() {
if (this.configStorage.getHttpProxyHost() != null && this.configStorage.getHttpProxyPort() > 0) {
httpProxy = new ProxyInfo(ProxyInfo.ProxyType.HTTP, configStorage.getHttpProxyHost(),
configStorage.getHttpProxyPort(), configStorage.getHttpProxyUsername(), configStorage.getHttpProxyPassword());
}
httpClient = new SocketHttpConnectionProvider();
}
//
// @Override
// public WxCpConfigStorage getWxCpConfigStorage() {
// return this.configStorage;
// }
}

View File

@@ -0,0 +1,130 @@
package me.chanjar.weixin.cp.tp.service.impl;
import com.google.gson.JsonObject;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.bean.WxAccessToken;
import me.chanjar.weixin.common.enums.WxType;
import me.chanjar.weixin.common.error.WxError;
import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.error.WxRuntimeException;
import me.chanjar.weixin.common.util.http.HttpClientType;
import me.chanjar.weixin.common.util.http.okhttp.DefaultOkHttpClientBuilder;
import me.chanjar.weixin.common.util.http.okhttp.OkHttpProxyInfo;
import me.chanjar.weixin.common.util.json.GsonParser;
import me.chanjar.weixin.cp.api.impl.BaseWxCpServiceImpl;
import me.chanjar.weixin.cp.config.WxCpConfigStorage;
import me.chanjar.weixin.cp.constant.WxCpApiPathConsts;
import okhttp3.*;
import java.io.IOException;
import static me.chanjar.weixin.cp.constant.WxCpApiPathConsts.GET_TOKEN;
/**
* The type Wx cp service ok http.
*
* @author someone
*/
@Slf4j
public class WxCpTpServiceOkHttpImpl extends BaseWxCpTpServiceImpl<OkHttpClient, OkHttpProxyInfo> {
private OkHttpClient httpClient;
private OkHttpProxyInfo httpProxy;
@Override
public OkHttpClient getRequestHttpClient() {
return httpClient;
}
@Override
public OkHttpProxyInfo getRequestHttpProxy() {
return httpProxy;
}
@Override
public HttpClientType getRequestType() {
return HttpClientType.OK_HTTP;
}
@Override
public String getSuiteAccessToken(boolean forceRefresh) throws WxErrorException {
if (!this.configStorage.isSuiteAccessTokenExpired() && !forceRefresh) {
return this.configStorage.getSuiteAccessToken();
}
synchronized (this.globalSuiteAccessTokenRefreshLock) {
// 得到 httpClient
OkHttpClient client = getRequestHttpClient();
JsonObject jsonObject = new JsonObject();
jsonObject.addProperty("suite_id", this.configStorage.getSuiteId());
jsonObject.addProperty("suite_secret", this.configStorage.getSuiteSecret());
jsonObject.addProperty("suite_ticket", this.getSuiteTicket());
String jsonBody = jsonObject.toString();
RequestBody requestBody = RequestBody.create(
MediaType.get("application/json; charset=utf-8"),
jsonBody
);
// 构建 POST 请求
Request request = new Request.Builder()
.url(this.configStorage.getApiUrl(WxCpApiPathConsts.Tp.GET_SUITE_TOKEN)) // URL 不包含查询参数
.post(requestBody) // 使用 POST 方法
.build();
String resultContent = null;
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("Unexpected response code: " + response);
}
resultContent = response.body().string();
} catch (IOException e) {
log.error("获取 suite token 失败: {}", e.getMessage(), e);
throw new WxRuntimeException("获取 suite token 失败", e);
}
WxError error = WxError.fromJson(resultContent, WxType.CP);
if (error.getErrorCode() != 0) {
throw new WxErrorException(error);
}
jsonObject = GsonParser.parse(resultContent);
String suiteAccussToken = jsonObject.get("suite_access_token").getAsString();
int expiresIn = jsonObject.get("expires_in").getAsInt();
this.configStorage.updateSuiteAccessToken(suiteAccussToken, expiresIn);
}
return this.configStorage.getSuiteAccessToken();
}
@Override
public void initHttp() {
log.debug("WxCpServiceOkHttpImpl initHttp");
//设置代理
if (configStorage.getHttpProxyHost() != null && configStorage.getHttpProxyPort() > 0) {
httpProxy = OkHttpProxyInfo.httpProxy(configStorage.getHttpProxyHost(),
configStorage.getHttpProxyPort(),
configStorage.getHttpProxyUsername(),
configStorage.getHttpProxyPassword());
OkHttpClient.Builder clientBuilder = new OkHttpClient.Builder();
clientBuilder.proxy(getRequestHttpProxy().getProxy());
//设置授权
clientBuilder.authenticator(new Authenticator() {
@Override
public Request authenticate(Route route, Response response) throws IOException {
String credential = Credentials.basic(httpProxy.getProxyUsername(), httpProxy.getProxyPassword());
return response.request().newBuilder()
.header("Authorization", credential)
.build();
}
});
httpClient = clientBuilder.build();
} else {
httpClient = DefaultOkHttpClientBuilder.get().build();
}
}
// @Override
// public WxCpConfigStorage getWxCpConfigStorage() {
// return this.configStorage;
// }
}

View File

@@ -23,7 +23,7 @@ public class WxCpTpCryptUtil extends WxCryptUtil {
* @param encodingAesKey 公众平台上开发者设置的EncodingAESKey
* @param appidOrCorpid 公众平台corpId
*/
String encodingAesKey = wxCpTpConfigStorage.getAesKey();
String encodingAesKey = wxCpTpConfigStorage.getEncodingAESKey();
String token = wxCpTpConfigStorage.getToken();
String corpId = wxCpTpConfigStorage.getCorpId();

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