增加初始化类文件
|
# 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 开发团队
|