增加初始化类文件
|
# NewLife.WeChat 架构设计文档
## 项目概述
NewLife.WeChat 是一个基于 NewLife.Core 和 XCode 的微信开发工具类库,提供微信公众号、小程序等功能的集成支持。
## 技术栈
- **.NET 10.0**: 目标框架
- **NewLife.Core**: 核心基础库,提供日志、配置、扩展方法等基础功能
- **XCode**: 数据访问层框架,提供实体模型和数据库操作支持
## 架构设计
### 1. 整体架构
```
NewLife.WeChat/
├── Entities/ # 实体模型层
│ ├── 微信配置.cs # 微信应用配置表
│ ├── 微信配置.Biz.cs # 业务逻辑扩展
│ ├── 微信用户.cs # 微信用户表
│ ├── 微信用户.Biz.cs # 用户业务逻辑
│ ├── 微信模板消息配置.cs # 模板消息配置表
│ └── 微信模板消息配置.Biz.cs # 模板消息业务逻辑
├── Services/ # 服务层
│ ├── WeChatService.cs # 微信核心服务
│ └── ApiClient.cs # API 客户端
├── Models/ # 数据模型
│ ├── Request/ # 请求模型
│ └── Response/ # 响应模型
├── Utils/ # 工具类
│ ├── SignHelper.cs # 签名辅助类
│ └── CryptoHelper.cs # 加密辅助类
└── docs/ # 文档
└── Architecture.md # 架构文档
```
### 2. 核心模块
#### 2.1 实体模型层 (Entities)
使用 XCode 实体模型管理微信相关配置信息。
**微信配置表 (WeChatConfig)**
- 存储微信应用的 AppId 和 AppSecret
- 支持多应用配置
- 提供配置缓存机制
- 支持多租户隔离
- 支持应用分类管理
主要字段:
- `Id`: 主键
- `AppName`: 应用名称
- `AppId`: 微信 AppId
- `AppSecret`: 微信 AppSecret
- `AppType`: 应用类型(公众号/小程序/企业微信)
- `AppCategory`: 应用分类枚举(1=公众号,2=小程序,3=APP)
- `TenantId`: 租户编号(用于多租户场景的数据隔离)
- `IsEnabled`: 是否启用
- `Token`: 消息校验 Token
- `EncodingAESKey`: 消息加解密密钥
- `CreateTime`: 创建时间
- `UpdateTime`: 更新时间
- `Remark`: 备注
索引设计:
- `AppId`: 唯一索引,确保 AppId 唯一
- `IsEnabled`: 普通索引,快速查询启用的配置
- `TenantId`: 普通索引,支持租户隔离查询
- `AppCategory`: 普通索引,按应用分类查询
**微信用户表 (WeChatUser)**
- 存储微信用户的 OpenId、UnionId 基本信息
- 支持公众号和小程序用户统一管理
- 通过 UnionId 关联同一用户在不同应用下的身份
- 轻量级设计,仅保留核心关联字段
主要字段:
- `Id`: 主键
- `AppId`: 微信 AppId
- `OpenId`: 用户 OpenId(应用唯一标识)
- `UnionId`: 用户 UnionId(开放平台统一标识)
- `CreateTime`: 创建时间
- `UpdateTime`: 更新时间
- `Remark`: 备注
索引设计:
- `AppId + OpenId`: 唯一索引,确保同一应用下 OpenId 唯一
- `UnionId`: 普通索引,用于关联查询
**微信模板消息配置表 (WeChatTemplateConfig)**
- 存储微信模板消息的配置信息
- 支持公众号模板消息和小程序订阅消息
- 管理模板字段配置和说明
- 便于统一管理和维护模板消息
主要字段:
- `Id`: 主键
- `AppId`: 微信 AppId
- `TemplateId`: 模板消息编号(微信平台的模板ID)
- `TemplateName`: 模板名称
- `TemplateType`: 模板类型(1=公众号模板消息,2=小程序订阅消息)
- `Fields`: 字段配置(JSON格式,存储字段名称和对应的key)
- `FieldsDesc`: 字段说明(JSON格式,说明每个字段的含义和示例)
- `IsEnabled`: 是否启用
- `CreateTime`: 创建时间
- `UpdateTime`: 更新时间
- `Remark`: 备注
索引设计:
- `AppId + TemplateId`: 唯一索引,确保同一应用下模板ID唯一
- `AppId`: 普通索引,按应用查询
- `IsEnabled`: 普通索引,快速查询启用的模板
#### 2.2 服务层 (Services)
**WeChatService**
- 提供微信 API 调用封装
- 管理 AccessToken 获取和刷新
- 实现 API 调用重试机制
核心功能:
**1. AccessToken 管理**
```csharp
// 获取访问令牌(自动缓存)
Task<String> GetAccessTokenAsync(String appId, Boolean forceRefresh = false);
// 刷新访问令牌
Task<String> RefreshAccessTokenAsync(String appId);
```
- 自动缓存 AccessToken,默认缓存时长为 7200 秒(提前5分钟过期)
- 支持强制刷新
- 失败自动重试机制
**2. 用户信息获取**
```csharp
// 通过授权码获取用户 OpenId 和 UnionId
Task<WeChatUserInfo> GetUserInfoByCodeAsync(String appId, String code);
// 获取用户详细信息(需要用户授权)
Task<WeChatUserDetail> GetUserDetailAsync(String appId, String openId);
```
- 支持网页授权获取用户信息
- 自动解析 OpenId 和 UnionId
- 存储到微信用户表
**3. UnionId 关联查询**
```csharp
// 根据 UnionId 查询用户在指定应用下的 OpenId
String GetOpenIdByUnionId(String unionId, String targetAppId);
// 获取 UnionId 关联的所有 OpenId
IList<WeChatUser> GetAllOpenIdsByUnionId(String unionId);
```
- 通过微信用户表关联查询
- 实现跨应用用户身份识别
- 用于消息推送、数据同步等场景
**4. 模板消息发送**
```csharp
// 发送公众号模板消息
Task<Boolean> SendTemplateMessageAsync(String appId, String openId, String templateId, Object data, String url = null);
// 发送小程序订阅消息
Task<Boolean> SendSubscribeMessageAsync(String appId, String openId, String templateId, Object data, String page = null);
// 批量发送模板消息
Task<IDictionary<String, Boolean>> SendBatchTemplateMessagesAsync(String appId, IList<String> openIds, String templateId, Object data);
```
- 自动获取模板配置
- 参数验证和格式化
- 支持跳转链接或小程序页面
- 批量发送支持(分批处理)
- 发送结果记录
**ApiClient**
- 封装 HTTP 请求
- 处理请求签名
- 统一异常处理
- 请求日志记录
#### 2.3 工具类 (Utils)
**SignHelper**
- 实现微信签名算法
- 参数排序和拼接
**CryptoHelper**
- 消息加解密
- AES 加密支持
### 3. 数据流程
**整体架构流程**
```
用户请求 -> WeChatService -> ApiClient -> 微信 API
↓
实体模型层 (获取配置)
↓
XCode (数据库)
```
**1. AccessToken 获取流程**
```
WeChatService.GetAccessTokenAsync()
↓
检查缓存 (Cache.Default)
↓ (未命中或过期)
查询微信配置表 (微信配置.FindByAppId)
↓
调用微信 API (/cgi-bin/token)
↓
缓存 AccessToken (7200秒-300秒)
↓
返回 AccessToken
```
**2. 用户信息获取流程**
```
用户授权 -> 获取 Code
↓
WeChatService.GetUserInfoByCodeAsync(appId, code)
↓
调用微信 API (/sns/oauth2/access_token)
↓
解析 OpenId 和 UnionId
↓
保存到微信用户表 (微信用户.Sync)
↓
返回用户信息
```
**3. UnionId 关联查询流程**
```
业务层调用 GetOpenIdByUnionId(unionId, targetAppId)
↓
查询微信用户表 (微信用户.FindAllByUnionId)
↓
筛选指定 AppId 的记录
↓
返回对应的 OpenId
```
**4. 模板消息发送流程**
```
业务层调用 SendTemplateMessageAsync()
↓
查询模板配置 (微信模板消息配置.FindByTemplateId)
↓
验证字段配置
↓
获取 AccessToken
↓
构造请求参数
↓
调用微信 API (/cgi-bin/message/template/send)
↓
记录发送结果
↓
返回发送状态
```
### 4. 配置管理
使用 XCode 实体模型实现配置的持久化存储:
- 支持动态配置更新
- 配置缓存提升性能
- 多应用配置隔离
### 5. 扩展性设计
- **插件化**: 支持自定义扩展服务
- **多应用支持**: 可配置多个微信应用
- **日志记录**: 集成 NewLife.Core 日志系统
- **异常处理**: 统一异常处理机制
### 6. 核心功能实现细节
#### 6.1 AccessToken 管理机制
**缓存策略**
- 使用 NewLife.Caching 缓存组件
- 缓存键格式:`WeChat:AccessToken:{AppId}`
- 默认缓存时长:7200秒(微信官方)- 300秒(提前5分钟过期)
- 支持分布式缓存(Redis)
**刷新策略**
- 自动刷新:缓存过期时自动请求新 Token
- 手动刷新:支持强制刷新参数
- 并发控制:使用锁机制避免重复请求
- 失败重试:最多重试3次,指数退避
**实现要点**
```csharp
// 缓存键生成
private String GetTokenCacheKey(String appId)
=> $"WeChat:AccessToken:{appId}";
// 获取 Token
public async Task<String> GetAccessTokenAsync(String appId, Boolean forceRefresh = false)
{
var key = GetTokenCacheKey(appId);
// 检查缓存
if (!forceRefresh)
{
var cached = Cache.Default.Get<String>(key);
if (!cached.IsNullOrEmpty()) return cached;
}
// 刷新 Token
return await RefreshAccessTokenAsync(appId);
}
```
#### 6.2 用户信息获取与存储
**获取流程**
1. 网页授权:引导用户授权(scope=snsapi_userinfo)
2. 获取 Code:从回调 URL 获取授权码
3. 换取 Token:通过 Code 换取 access_token 和 openid
4. 获取信息:使用 access_token 获取用户详细信息
5. 解析保存:解析 OpenId 和 UnionId 并存储
**数据同步**
```csharp
public async Task<WeChatUserInfo> GetUserInfoByCodeAsync(String appId, String code)
{
// 1. 通过 Code 获取 access_token 和 openid
var tokenResult = await GetOAuthAccessTokenAsync(appId, code);
// 2. 获取用户信息
var userInfo = await GetOAuthUserInfoAsync(tokenResult.AccessToken, tokenResult.OpenId);
// 3. 保存到数据库
var user = 微信用户.Sync(appId, userInfo);
return user;
}
```
**UnionId 说明**
- UnionId 是微信开放平台的唯一标识
- 同一开放平台下的不同应用,同一用户的 UnionId 相同
- 只有在用户关注公众号或授权登录时才返回
- 未绑定开放平台的应用无 UnionId
#### 6.3 UnionId 关联查询实现
**核心逻辑**
```csharp
public String GetOpenIdByUnionId(String unionId, String targetAppId)
{
if (unionId.IsNullOrEmpty() || targetAppId.IsNullOrEmpty())
return null;
// 查询该 UnionId 下所有用户
var users = 微信用户.FindAllByUnionId(unionId);
// 筛选目标应用
var targetUser = users.FirstOrDefault(u => u.AppId == targetAppId);
return targetUser?.OpenId;
}
```
**应用场景**
- **跨应用消息推送**:公众号获取用户信息后,推送小程序消息
- **用户身份关联**:统一用户在不同应用的身份
- **数据同步**:同步用户在不同应用的数据
- **营销活动**:跨应用的用户营销
**注意事项**
- 需要应用绑定到同一微信开放平台
- UnionId 可能为空(未关注或未授权)
- 定期清理无效的用户记录
#### 6.4 模板消息发送实现
**发送流程**
1. 查询模板配置(微信模板消息配置表)
2. 验证模板是否启用
3. 获取 AccessToken
4. 构造请求参数(data 字段格式化)
5. 调用微信 API
6. 记录发送结果
**参数格式**
```json
{
"touser": "openid",
"template_id": "template_xxx",
"url": "https://example.com/page",
"data": {
"first": {
"value": "恭喜你购买成功!",
"color": "#173177"
},
"keyword1": {
"value": "巧克力",
"color": "#173177"
}
}
}
```
**批量发送优化**
- 分批处理:每批50个,避免并发过高
- 异步处理:使用 Task.WhenAll 并发发送
- 失败重试:记录失败的 OpenId,稍后重试
- 结果统计:返回成功和失败的数量
**错误处理**
```csharp
public async Task<Boolean> SendTemplateMessageAsync(/*...*/)
{
try
{
// 1. 查询模板配置
var template = 微信模板消息配置.FindByTemplateId(appId, templateId);
if (template == null || !template.IsEnabled)
{
_log.Warn($"模板 {templateId} 不存在或未启用");
return false;
}
// 2. 获取 AccessToken
var token = await GetAccessTokenAsync(appId);
// 3. 构造请求
var body = new
{
touser = openId,
template_id = templateId,
url = url,
data = data
};
// 4. 发送请求
var result = await PostAsync<TemplateMessageResult>(
$"/cgi-bin/message/template/send?access_token={token}",
body
);
return result.ErrCode == 0;
}
catch (Exception ex)
{
_log.Error($"发送模板消息失败: {ex.Message}");
return false;
}
}
```
**公众号与小程序差异**
| 项目 | 公众号模板消息 | 小程序订阅消息 |
|------|---------------|---------------|
| API 路径 | /cgi-bin/message/template/send | /cgi-bin/message/subscribe/send |
| 跳转参数 | url | page |
| 触发方式 | 用户操作或事件 | 用户订阅 |
| 发送频率 | 较宽松 | 需要用户订阅 |
| 字段限制 | 相对灵活 | 严格限制 |
## 功能模块
### 阶段一:基础功能 ✅
- [x] 项目架构搭建
- [x] 实体模型设计
- [x] 配置管理
- [x] 模板消息配置管理
- [x] AccessToken 管理(获取、缓存、刷新)
- [x] 用户信息获取(OpenId、UnionId)
- [x] UnionId 关联查询
- [x] 模板消息发送
### 阶段二:公众号功能
- [x] 用户管理(OpenId、UnionId 存储)
- [x] 模板消息发送
- [ ] 消息管理(接收、回复)
- [ ] 菜单管理
- [ ] 素材管理
- [ ] 用户标签管理
- [ ] 客服消息
### 阶段三:小程序功能
- [x] 订阅消息发送
- [ ] 登录授权(完整实现)
- [ ] 数据解密(手机号、用户信息)
- [ ] 小程序码生成
- [ ] 客服消息
### 阶段四:高级功能
- [ ] 支付功能(公众号支付、小程序支付)
- [ ] 企业微信支持
- [ ] 开放平台支持
- [ ] 消息推送队列
- [ ] 发送记录和统计
## 使用示例
### 1. 获取 AccessToken
```csharp
using NewLife.WeChat.Services;
using NewLife.WeChat.Entities;
// 方式1:通过 AppId 获取
var service = new WeChatService();
var token = await service.GetAccessTokenAsync("wx1234567890");
// 方式2:通过配置对象
var config = 微信配置.FindByAppId("wx1234567890");
var service2 = new WeChatService(config);
var token2 = await service2.GetAccessTokenAsync();
// 强制刷新 Token
var newToken = await service.GetAccessTokenAsync("wx1234567890", forceRefresh: true);
```
### 2. 获取用户 OpenId 和 UnionId
```csharp
// 网页授权后获取用户信息
var code = Request.Query["code"]; // 从微信回调获取 code
var userInfo = await service.GetUserInfoByCodeAsync("wx1234567890", code);
Console.WriteLine($"OpenId: {userInfo.OpenId}");
Console.WriteLine($"UnionId: {userInfo.UnionId}");
// 自动保存到数据库
var user = 微信用户.FindByOpenId("wx1234567890", userInfo.OpenId);
Console.WriteLine($"用户已保存: {user.Id}");
```
### 3. 根据 UnionId 获取对应应用的 OpenId
```csharp
// 场景:用户在公众号授权登录,获取其在小程序中的 OpenId
// 获取用户在公众号的信息(已有 UnionId)
var officialUser = 微信用户.FindByOpenId("wx_official_123", "openid_official");
var unionId = officialUser.UnionId;
// 查找该用户在小程序的 OpenId
var miniOpenId = service.GetOpenIdByUnionId(unionId, "wx_mini_456");
if (!miniOpenId.IsNullOrEmpty())
{
Console.WriteLine($"用户在小程序的 OpenId: {miniOpenId}");
// 可以向小程序推送消息
}
// 或者获取该用户在所有应用的信息
var allUsers = 微信用户.FindAllByUnionId(unionId);
foreach (var u in allUsers)
{
Console.WriteLine($"AppId: {u.AppId}, OpenId: {u.OpenId}");
}
```
### 4. 发送模板消息
```csharp
// 先配置模板
var templateConfig = new 微信模板消息配置
{
AppId = "wx1234567890",
TemplateId = "template_001",
TemplateName = "订单通知",
TemplateType = 1, // 公众号模板消息
Fields = "{\"first\":\"您的订单已完成\",\"keyword1\":\"订单号\",\"keyword2\":\"金额\",\"remark\":\"感谢您的支持\"}",
FieldsDesc = "{\"first\":\"标题\",\"keyword1\":\"订单编号\",\"keyword2\":\"订单金额\",\"remark\":\"备注说明\"}",
IsEnabled = true
};
templateConfig.Insert();
// 发送公众号模板消息
var data = new
{
first = new { value = "您的订单已完成", color = "#173177" },
keyword1 = new { value = "202401010001", color = "#173177" },
keyword2 = new { value = "¥99.00", color = "#173177" },
remark = new { value = "感谢您的支持!", color = "#173177" }
};
var success = await service.SendTemplateMessageAsync(
appId: "wx1234567890",
openId: "user_openid_123",
templateId: "template_001",
data: data,
url: "https://example.com/order/202401010001"
);
Console.WriteLine($"发送结果: {success}");
// 发送小程序订阅消息
var miniData = new
{
thing1 = new { value = "订单支付成功" },
amount2 = new { value = "99.00元" },
date3 = new { value = "2024年01月01日 12:00" }
};
var miniSuccess = await service.SendSubscribeMessageAsync(
appId: "wx_mini_456",
openId: "mini_user_openid",
templateId: "mini_template_001",
data: miniData,
page: "pages/order/detail?id=202401010001"
);
// 批量发送
var openIds = new List<String> { "openid1", "openid2", "openid3" };
var results = await service.SendBatchTemplateMessagesAsync(
"wx1234567890",
openIds,
"template_001",
data
);
foreach (var result in results)
{
Console.WriteLine($"OpenId: {result.Key}, 成功: {result.Value}");
}
```
### 完整业务示例
```csharp
/// <summary>订单完成后跨应用推送消息</summary>
public async Task NotifyOrderComplete(String unionId, String orderId, Decimal amount)
{
// 1. 根据 UnionId 获取用户在不同应用的 OpenId
var users = 微信用户.FindAllByUnionId(unionId);
foreach (var user in users)
{
// 2. 获取对应应用的配置
var config = 微信配置.FindByAppId(user.AppId);
if (!config.IsEnabled) continue;
// 3. 根据应用类型发送不同的消息
var service = new WeChatService(config);
if (config.AppCategory == WeChatAppCategory.公众号)
{
// 发送公众号模板消息
var data = new
{
first = new { value = "订单支付成功" },
keyword1 = new { value = orderId },
keyword2 = new { value = $"¥{amount}" },
remark = new { value = "感谢您的购买!" }
};
await service.SendTemplateMessageAsync(
user.AppId,
user.OpenId,
"order_complete_template",
data,
$"https://example.com/order/{orderId}"
);
}
else if (config.AppCategory == WeChatAppCategory.MiniProgram)
{
// 发送小程序订阅消息
var data = new
{
thing1 = new { value = "订单支付成功" },
amount2 = new { value = $"{amount}元" },
date3 = new { value = DateTime.Now.ToString("yyyy-MM-dd HH:mm") }
};
await service.SendSubscribeMessageAsync(
user.AppId,
user.OpenId,
"order_complete_subscribe",
data,
$"pages/order/detail?id={orderId}"
);
}
}
}
```
## 数据库设计
使用 XCode 自动管理数据库表结构,支持:
- 自动建表
- 表结构同步
- 多数据库支持(MySQL、SQL Server、SQLite 等)
**核心数据表**
1. **微信配置表 (WeChatConfig)** - 存储应用配置和密钥
2. **微信用户表 (WeChatUser)** - 存储用户 OpenId 和 UnionId 关联
3. **微信模板消息配置表 (WeChatTemplateConfig)** - 存储模板配置
**表关系**
```
微信配置表 (1) ----< (N) 微信用户表
|
| AppId 关联
|
微信配置表 (1) ----< (N) 微信模板消息配置表
微信用户表通过 UnionId 实现跨应用关联
```
## 微信 API 接口
### Token 相关
| 接口 | 地址 | 说明 |
|------|------|------|
| 获取 AccessToken | GET /cgi-bin/token | grant_type=client_credential |
### 用户信息相关
| 接口 | 地址 | 说明 |
|------|------|------|
| 网页授权 access_token | GET /sns/oauth2/access_token | 通过 code 换取 |
| 获取用户信息 | GET /sns/userinfo | 需要网页授权 access_token |
### 消息推送相关
| 接口 | 地址 | 说明 |
|------|------|------|
| 发送模板消息 | POST /cgi-bin/message/template/send | 公众号模板消息 |
| 发送订阅消息 | POST /cgi-bin/message/subscribe/send | 小程序订阅消息 |
## 性能优化
- **配置缓存**: 减少数据库查询
- **Token 缓存**: AccessToken 定时刷新
- **连接池**: 复用 HTTP 连接
- **异步 IO**: 全异步 API 调用
## 安全性
- **配置加密**: AppSecret 加密存储
- **传输加密**: HTTPS 通信
- **签名验证**: 消息签名校验
- **权限控制**: 配置访问权限
## 依赖说明
- **NewLife.Core**: 提供基础工具类、日志、配置等
- **XCode**: 提供实体模型、数据访问等
## 更新日志
### v1.2.0 (核心功能实现) - 当前版本
- ✅ 实现 AccessToken 管理(获取、缓存、自动刷新)
- ✅ 实现用户信息获取(网页授权、OpenId、UnionId)
- ✅ 实现 UnionId 关联查询(跨应用用户识别)
- ✅ 实现模板消息发送(公众号、小程序)
- ✅ 支持批量发送模板消息
- ✅ 完善数据流程设计
- ✅ 添加详细的使用示例和业务场景
### v1.1.0 (功能增强)
- 添加模板消息配置表
- 支持公众号模板消息和小程序订阅消息管理
- 完善实体模型设计
### v1.0.0 (初始版本)
- 完成项目架构设计
- 实现微信配置实体模型
- 基础依赖集成
---
**文档更新时间**: 2024-01
**维护者**: NewLife 开发团队
|