增加初始化类文件
xiyunfei authored at 2026-02-27 00:31:51
14.16 KiB
NewLife.WeChat
# NewLife.WeChat 核心功能实现说明 ## 功能概览 本文档详细说明 NewLife.WeChat 四大核心功能的实现方案和使用方法。 ## 1. 获取 AccessToken ### 功能说明 AccessToken 是调用微信 API 的凭证,有效期为 7200 秒(2小时)。 ### 实现方式 #### 1.1 缓存机制 - 使用 NewLife.Caching 组件进行缓存 - 缓存键格式:`WeChat:AccessToken:{AppId}` - 缓存时长:7200秒 - 300秒(提前5分钟过期,避免临界问题) - 支持分布式缓存(Redis) #### 1.2 刷新策略 ```csharp public async Task<String> GetAccessTokenAsync(String appId, Boolean forceRefresh = false) { // 1. 检查缓存 if (!forceRefresh) { var cached = Cache.Default.Get<String>($"WeChat:AccessToken:{appId}"); if (!cached.IsNullOrEmpty()) return cached; } // 2. 查询配置 var config = 微信配置.FindByAppId(appId); // 3. 请求微信 API var url = $"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={config.AppId}&secret={config.AppSecret}"; var result = await GetAsync<AccessTokenResponse>(url); // 4. 缓存结果 Cache.Default.Set($"WeChat:AccessToken:{appId}", result.AccessToken, result.ExpiresIn - 300); return result.AccessToken; } ``` #### 1.3 使用示例 ```csharp var service = new WeChatService(); // 自动缓存 var token = await service.GetAccessTokenAsync("wx1234567890"); // 强制刷新 var newToken = await service.GetAccessTokenAsync("wx1234567890", forceRefresh: true); ``` ### 优势 - ✅ 自动缓存,减少 API 调用 - ✅ 提前过期,避免临界问题 - ✅ 并发安全,防止重复请求 - ✅ 支持强制刷新 --- ## 2. 获取用户 OpenId 及 UnionId ### 功能说明 通过微信网页授权获取用户的 OpenId(应用唯一标识)和 UnionId(开放平台统一标识)。 ### 实现流程 #### 2.1 授权流程 ``` 1. 引导用户授权 ↓ https://open.weixin.qq.com/connect/oauth2/authorize? appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect ↓ 2. 用户同意授权 ↓ 3. 微信回调返回 code ↓ 4. 通过 code 换取 access_token 和 openid ↓ 5. 获取用户详细信息(包含 UnionId) ``` #### 2.2 代码实现 ```csharp public async Task<WeChatUserInfo> GetUserInfoByCodeAsync(String appId, String code) { var config = 微信配置.FindByAppId(appId); // 1. 通过 code 获取 access_token 和 openid var tokenUrl = $"https://api.weixin.qq.com/sns/oauth2/access_token?appid={config.AppId}&secret={config.AppSecret}&code={code}&grant_type=authorization_code"; var tokenResult = await GetAsync<OAuthTokenResponse>(tokenUrl); // 2. 获取用户信息 var userUrl = $"https://api.weixin.qq.com/sns/userinfo?access_token={tokenResult.AccessToken}&openid={tokenResult.OpenId}&lang=zh_CN"; var userInfo = await GetAsync<WeChatUserInfo>(userUrl); // 3. 保存到数据库 var user = new 微信用户 { AppId = appId, OpenId = userInfo.OpenId, UnionId = userInfo.UnionId }; user.Insert(); return userInfo; } ``` #### 2.3 使用示例 ```csharp // 控制器中处理微信回调 [HttpGet] public async Task<IActionResult> WeChatCallback(String code, String state) { var service = new WeChatService(); var userInfo = await service.GetUserInfoByCodeAsync("wx1234567890", code); // 登录处理 // HttpContext.Session.SetString("OpenId", userInfo.OpenId); // HttpContext.Session.SetString("UnionId", userInfo.UnionId); return Redirect("/home"); } ``` ### 注意事项 - ⚠️ scope=snsapi_base 只能获取 OpenId - ⚠️ scope=snsapi_userinfo 需要用户确认授权 - ⚠️ UnionId 只有绑定开放平台后才返回 - ⚠️ code 只能使用一次,5分钟内有效 --- ## 3. 根据 UnionId 获取对应 APP 的 OpenId ### 功能说明 通过 UnionId 查询用户在不同应用(公众号、小程序、APP)下的 OpenId,实现跨应用用户身份识别。 ### 应用场景 - **跨应用消息推送**:用户在公众号授权后,可以向其小程序推送消息 - **用户身份统一**:统一管理用户在不同应用的数据 - **营销活动**:跨平台的用户营销和触达 ### 实现方式 #### 3.1 数据库设计 ```sql -- 微信用户表 CREATE TABLE WeChatUser ( Id INT PRIMARY KEY, AppId VARCHAR(50), OpenId VARCHAR(50), UnionId VARCHAR(50), CreateTime DATETIME, UpdateTime DATETIME, INDEX IX_UnionId (UnionId) ); ``` #### 3.2 查询逻辑 ```csharp // 根据 UnionId 获取指定应用的 OpenId public String GetOpenIdByUnionId(String unionId, String targetAppId) { var users = 微信用户.FindAllByUnionId(unionId); var targetUser = users.FirstOrDefault(u => u.AppId == targetAppId); return targetUser?.OpenId; } // 获取 UnionId 关联的所有应用 public IList<微信用户> GetAllOpenIdsByUnionId(String unionId) { return 微信用户.FindAllByUnionId(unionId); } ``` #### 3.3 使用示例 ```csharp // 场景:用户在公众号登录,获取其在小程序的 OpenId // 1. 用户在公众号授权登录 var officialUserInfo = await service.GetUserInfoByCodeAsync("wx_official_123", code); var unionId = officialUserInfo.UnionId; // 2. 查找用户在小程序的 OpenId var miniOpenId = service.GetOpenIdByUnionId(unionId, "wx_mini_456"); if (!miniOpenId.IsNullOrEmpty()) { // 3. 向小程序推送消息 await service.SendSubscribeMessageAsync("wx_mini_456", miniOpenId, "template_id", data); } // 4. 或者向所有应用推送 var allUsers = service.GetAllOpenIdsByUnionId(unionId); foreach (var user in allUsers) { // 根据应用类型发送不同消息 if (user.AppId.StartsWith("wx")) await service.SendTemplateMessageAsync(user.AppId, user.OpenId, "template_id", data); } ``` ### 数据维护 ```csharp // 用户授权时自动保存或更新 public async Task SaveUserInfo(String appId, String code) { var userInfo = await GetUserInfoByCodeAsync(appId, code); // 检查是否已存在 var user = 微信用户.FindByOpenId(appId, userInfo.OpenId); if (user == null) { user = new 微信用户 { AppId = appId, OpenId = userInfo.OpenId, UnionId = userInfo.UnionId }; user.Insert(); } else { // 更新 UnionId user.UnionId = userInfo.UnionId; user.Update(); } } ``` ### 注意事项 - ⚠️ 需要应用绑定到同一微信开放平台 - ⚠️ UnionId 可能为空(未关注或未授权) - ⚠️ 定期清理无效的用户记录 - ⚠️ 不同应用的 OpenId 完全不同 --- ## 4. 模板消息发送 ### 功能说明 向用户发送模板消息(公众号)或订阅消息(小程序),用于业务通知。 ### 消息类型对比 | 特性 | 公众号模板消息 | 小程序订阅消息 | |------|---------------|---------------| | 触发方式 | 用户操作或事件 | 用户主动订阅 | | 发送频率 | 相对宽松 | 每次订阅消耗一次 | | 跳转方式 | URL 链接 | 小程序页面 | | 字段限制 | 较灵活 | 严格限制 | ### 实现流程 #### 4.1 配置模板 ```csharp // 在数据库中配置模板 var template = new 微信模板消息配置 { AppId = "wx1234567890", TemplateId = "template_order_success", TemplateName = "订单完成通知", TemplateType = 1, // 1=公众号,2=小程序 Fields = @"{ ""first"": ""标题"", ""keyword1"": ""订单号"", ""keyword2"": ""金额"", ""remark"": ""备注"" }", IsEnabled = true }; template.Insert(); ``` #### 4.2 发送公众号模板消息 ```csharp public async Task<Boolean> SendTemplateMessageAsync( String appId, String openId, String templateId, Object data, String url = null) { // 1. 查询模板配置 var template = 微信模板消息配置.FindByTemplateId(appId, templateId); if (template == null || !template.IsEnabled) return false; // 2. 获取 AccessToken var token = await GetAccessTokenAsync(appId); // 3. 构造请求 var body = new { touser = openId, template_id = templateId, url = url, data = data }; // 4. 发送请求 var apiUrl = $"https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={token}"; var result = await PostAsync<WeChatResponse>(apiUrl, body); return result.ErrCode == 0; } ``` #### 4.3 发送小程序订阅消息 ```csharp public async Task<Boolean> SendSubscribeMessageAsync( String appId, String openId, String templateId, Object data, String page = null) { var token = await GetAccessTokenAsync(appId); var body = new { touser = openId, template_id = templateId, page = page, data = data }; var apiUrl = $"https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={token}"; var result = await PostAsync<WeChatResponse>(apiUrl, body); return result.ErrCode == 0; } ``` #### 4.4 使用示例 **公众号模板消息** ```csharp 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", templateId: "template_order_success", data: data, url: "https://example.com/order/202401010001" ); ``` **小程序订阅消息** ```csharp var data = new { thing1 = new { value = "订单支付成功" }, amount2 = new { value = "99.00元" }, date3 = new { value = "2024年01月01日 12:00" } }; var success = await service.SendSubscribeMessageAsync( appId: "wx_mini_456", openId: "mini_user_openid", templateId: "mini_template_order", data: data, page: "pages/order/detail?id=202401010001" ); ``` #### 4.5 批量发送 ```csharp public async Task<IDictionary<String, Boolean>> SendBatchTemplateMessagesAsync( String appId, IList<String> openIds, String templateId, Object data) { var results = new Dictionary<String, Boolean>(); // 分批处理,每批50个 var batches = openIds.Split(50); foreach (var batch in batches) { var tasks = batch.Select(openId => SendTemplateMessageAsync(appId, openId, templateId, data) .ContinueWith(t => new { OpenId = openId, Success = t.Result }) ); var batchResults = await Task.WhenAll(tasks); foreach (var result in batchResults) { results[result.OpenId] = result.Success; } } return results; } ``` ### 完整业务示例 **订单完成后跨应用推送** ```csharp public async Task NotifyOrderComplete(String unionId, String orderId, Decimal amount) { // 1. 获取用户在所有应用的 OpenId var users = 微信用户.FindAllByUnionId(unionId); foreach (var user in users) { var config = 微信配置.FindByAppId(user.AppId); if (!config.IsEnabled) continue; var service = new WeChatService(config); // 2. 根据应用类型发送不同消息 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}" ); } } } ``` ### 错误处理 **常见错误码** | 错误码 | 说明 | 处理方式 | |--------|------|---------| | 40001 | access_token 无效 | 刷新 Token | | 40003 | openid 错误 | 检查 OpenId | | 43004 | 用户未授权 | 引导用户授权 | | 47001 | 模板库 ID 不存在 | 检查模板配置 | | 43101 | 用户拒绝订阅 | 小程序需要用户订阅 | **重试策略** ```csharp public async Task<Boolean> SendWithRetryAsync(/*...*/, Int32 maxRetries = 3) { for (var i = 0; i < maxRetries; i++) { try { return await SendTemplateMessageAsync(/*...*/); } catch (Exception ex) { _log.Warn($"发送失败,第 {i + 1} 次重试: {ex.Message}"); if (i == maxRetries - 1) throw; await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i))); // 指数退避 } } return false; } ``` --- ## 总结 ### 核心价值 1. **AccessToken 管理** - 自动缓存,减少 API 调用 2. **用户信息获取** - 统一存储 OpenId 和 UnionId 3. **跨应用关联** - 通过 UnionId 实现用户身份统一 4. **消息推送** - 支持公众号和小程序消息发送 ### 技术特点 - ✅ 基于 XCode 实体框架 - ✅ 使用 NewLife.Caching 缓存 - ✅ 完整的错误处理和重试机制 - ✅ 支持批量操作和并发处理 - ✅ 详细的日志记录 ### 最佳实践 1. 配置缓存时长:提前 5 分钟过期 2. UnionId 查询:定期清理无效记录 3. 批量发送:分批处理,避免并发过高 4. 错误处理:记录详细日志,实现重试机制 --- **文档版本**: v1.0 **更新时间**: 2024-01 **维护者**: NewLife 开发团队