增加初始化类文件
xiyunfei authored at 2026-02-27 00:31:51
21.71 KiB
NewLife.WeChat
# 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 开发团队