using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using NewLife;
using NewLife.Caching;
using NewLife.Log;
using NewLife.Serialization;
using NewLife.WeChat.Entities;
using NewLife.WeChat.Models;
namespace NewLife.WeChat.Services;
/// <summary>微信服务。提供微信API调用封装</summary>
public class WeChatService
{
#region 属性
/// <summary>微信配置</summary>
public 微信配置 Config { get; set; }
/// <summary>HTTP客户端</summary>
private readonly HttpClient _client;
/// <summary>缓存</summary>
private readonly ICache _cache;
/// <summary>日志</summary>
public ILog Log { get; set; }
#endregion
#region 构造
/// <summary>实例化微信服务</summary>
public WeChatService()
{
_client = new HttpClient();
_cache = Cache.Default;
Log = XTrace.Log;
}
/// <summary>实例化微信服务</summary>
/// <param name="config">微信配置</param>
public WeChatService(微信配置 config) : this()
{
Config = config;
}
#endregion
#region AccessToken 管理
/// <summary>获取访问令牌(自动缓存)</summary>
/// <param name="appid">AppId,为空时使用Config的AppId</param>
/// <param name="forceRefresh">是否强制刷新</param>
/// <returns></returns>
public async Task<String> GetAccessTokenAsync(String appid = null, Boolean forceRefresh = false)
{
appid ??= Config?.AppId;
if (appid.IsNullOrEmpty()) throw new ArgumentNullException(nameof(appid), "AppId不能为空");
var key = GetTokenCacheKey(appid);
// 检查缓存
if (!forceRefresh)
{
var cached = _cache.Get<String>(key);
if (!cached.IsNullOrEmpty())
{
Log?.Debug($"从缓存获取AccessToken: {appid}");
return cached;
}
}
// 刷新 Token
return await RefreshAccessTokenAsync(appid);
}
/// <summary>刷新访问令牌</summary>
/// <param name="appid">AppId</param>
/// <returns></returns>
public async Task<String> RefreshAccessTokenAsync(String appid)
{
var config = Config?.AppId == appid ? Config : 微信配置.FindByAppId(appid);
if (config == null) throw new InvalidOperationException($"未找到AppId为 {appid} 的配置");
Log?.Info($"刷新AccessToken: {appid}");
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);
if (!result.IsSuccess)
throw new Exception($"获取AccessToken失败: {result.ErrCode} - {result.ErrMsg}");
// 缓存 Token(提前5分钟过期)
var key = GetTokenCacheKey(appid);
_cache.Set(key, result.Access_Token, result.Expires_In - 300);
Log?.Info($"AccessToken刷新成功: {appid},有效期: {result.Expires_In}秒");
return result.Access_Token;
}
/// <summary>获取Token缓存键</summary>
/// <param name="appid">AppId</param>
/// <returns></returns>
private String GetTokenCacheKey(String appid) => $"WeChat:AccessToken:{appid}";
#endregion
#region 用户信息获取
/// <summary>通过授权码获取用户OpenId和UnionId</summary>
/// <param name="appid">AppId</param>
/// <param name="code">授权码</param>
/// <returns></returns>
public async Task<WeChatUserInfo> GetUserInfoByCodeAsync(String appid, String code)
{
if (code.IsNullOrEmpty()) throw new ArgumentNullException(nameof(code));
var config = Config?.AppId == appid ? Config : 微信配置.FindByAppId(appid);
if (config == null) throw new InvalidOperationException($"未找到AppId为 {appid} 的配置");
Log?.Info($"通过Code获取用户信息: {appid}, code: {code}");
// 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);
if (!tokenResult.IsSuccess)
throw new Exception($"获取OAuthToken失败: {tokenResult.ErrCode} - {tokenResult.ErrMsg}");
// 2. 获取用户信息
var userUrl = $"https://api.weixin.qq.com/sns/userinfo?access_token={tokenResult.Access_Token}&openid={tokenResult.OpenId}&lang=zh_CN";
var userInfo = await GetAsync<WeChatUserInfo>(userUrl);
if (!userInfo.IsSuccess)
throw new Exception($"获取用户信息失败: {userInfo.ErrCode} - {userInfo.ErrMsg}");
// 3. 保存到数据库
微信用户.Sync(appid, userInfo.OpenId, userInfo.UnionId);
Log?.Info($"获取用户信息成功: OpenId={userInfo.OpenId}, UnionId={userInfo.UnionId}");
return userInfo;
}
/// <summary>获取用户详细信息(需要access_token)</summary>
/// <param name="appid">AppId</param>
/// <param name="openid">OpenId</param>
/// <returns></returns>
public async Task<WeChatUserInfo> GetUserDetailAsync(String appid, String openid)
{
var token = await GetAccessTokenAsync(appid);
var url = $"https://api.weixin.qq.com/cgi-bin/user/info?access_token={token}&openid={openid}&lang=zh_CN";
var userInfo = await GetAsync<WeChatUserInfo>(url);
if (!userInfo.IsSuccess)
throw new Exception($"获取用户详细信息失败: {userInfo.ErrCode} - {userInfo.ErrMsg}");
// 保存到数据库
微信用户.Sync(appid, userInfo.OpenId, userInfo.UnionId);
return userInfo;
}
#endregion
#region UnionId 关联查询
/// <summary>根据UnionId查询用户在指定应用下的OpenId</summary>
/// <param name="unionid">UnionId</param>
/// <param name="targetAppId">目标应用AppId</param>
/// <returns></returns>
public String GetOpenIdByUnionId(String unionid, String targetAppId)
{
if (unionid.IsNullOrEmpty() || targetAppId.IsNullOrEmpty()) return null;
var users = 微信用户.FindAllByUnionId(unionid);
var targetUser = users.FirstOrDefault(u => u.AppId == targetAppId);
return targetUser?.OpenId;
}
/// <summary>获取UnionId关联的所有OpenId</summary>
/// <param name="unionid">UnionId</param>
/// <returns></returns>
public IList<微信用户> GetAllOpenIdsByUnionId(String unionid)
{
if (unionid.IsNullOrEmpty()) return new List<微信用户>();
return 微信用户.FindAllByUnionId(unionid);
}
#endregion
#region 模板消息发送
/// <summary>发送公众号模板消息</summary>
/// <param name="appid">AppId</param>
/// <param name="openid">用户OpenId</param>
/// <param name="templateid">模板ID</param>
/// <param name="data">消息数据</param>
/// <param name="url">跳转链接</param>
/// <returns></returns>
public async Task<Boolean> SendTemplateMessageAsync(String appid, String openid, String templateid, Object data, String url = null)
{
if (openid.IsNullOrEmpty()) throw new ArgumentNullException(nameof(openid));
if (templateid.IsNullOrEmpty()) throw new ArgumentNullException(nameof(templateid));
// 1. 查询模板配置
var template = 微信模板消息配置.FindByTemplateId(appid, templateid);
if (template == null)
{
Log?.Warn($"模板 {templateid} 不存在");
return false;
}
if (!template.IsEnabled)
{
Log?.Warn($"模板 {templateid} 未启用");
return false;
}
// 2. 获取 AccessToken
var token = await GetAccessTokenAsync(appid);
// 3. 构造请求
var request = new TemplateMessageRequest
{
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<TemplateMessageResponse>(apiUrl, request);
if (!result.IsSuccess)
{
Log?.Error($"发送模板消息失败: {result.ErrCode} - {result.ErrMsg}");
return false;
}
Log?.Info($"模板消息发送成功: MsgId={result.MsgId}");
return true;
}
/// <summary>发送小程序订阅消息</summary>
/// <param name="appid">AppId</param>
/// <param name="openid">用户OpenId</param>
/// <param name="templateid">模板ID</param>
/// <param name="data">消息数据</param>
/// <param name="page">跳转页面</param>
/// <returns></returns>
public async Task<Boolean> SendSubscribeMessageAsync(String appid, String openid, String templateid, Object data, String page = null)
{
if (openid.IsNullOrEmpty()) throw new ArgumentNullException(nameof(openid));
if (templateid.IsNullOrEmpty()) throw new ArgumentNullException(nameof(templateid));
// 1. 查询模板配置
var template = 微信模板消息配置.FindByTemplateId(appid, templateid);
if (template == null)
{
Log?.Warn($"模板 {templateid} 不存在");
return false;
}
if (!template.IsEnabled)
{
Log?.Warn($"模板 {templateid} 未启用");
return false;
}
// 2. 获取 AccessToken
var token = await GetAccessTokenAsync(appid);
// 3. 构造请求
var request = new SubscribeMessageRequest
{
ToUser = openid,
Template_Id = templateid,
Page = page,
Data = data,
MiniProgram_State = "formal",
Lang = "zh_CN"
};
// 4. 发送请求
var apiUrl = $"https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={token}";
var result = await PostAsync<WeChatResponse>(apiUrl, request);
if (!result.IsSuccess)
{
Log?.Error($"发送订阅消息失败: {result.ErrCode} - {result.ErrMsg}");
return false;
}
Log?.Info($"订阅消息发送成功");
return true;
}
/// <summary>批量发送模板消息</summary>
/// <param name="appid">AppId</param>
/// <param name="openids">用户OpenId列表</param>
/// <param name="templateid">模板ID</param>
/// <param name="data">消息数据</param>
/// <returns></returns>
public async Task<IDictionary<String, Boolean>> SendBatchTemplateMessagesAsync(String appid, IList<String> openids, String templateid, Object data)
{
var results = new Dictionary<String, Boolean>();
if (openids == null || openids.Count == 0) return results;
Log?.Info($"批量发送模板消息: {openids.Count} 个用户");
// 分批处理,每批50个
var batches = openids.Page(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;
}
}
var successCount = results.Count(r => r.Value);
Log?.Info($"批量发送完成: 成功 {successCount}/{openids.Count}");
return results;
}
#endregion
#region HTTP 请求
/// <summary>GET请求</summary>
/// <typeparam name="T">响应类型</typeparam>
/// <param name="url">请求地址</param>
/// <returns></returns>
private async Task<T> GetAsync<T>(String url) where T : class
{
try
{
Log?.Debug($"GET: {url}");
var response = await _client.GetAsync(url);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
Log?.Debug($"Response: {content}");
return content.ToJsonEntity<T>();
}
catch (Exception ex)
{
Log?.Error($"GET请求失败: {url}, {ex.Message}");
throw;
}
}
/// <summary>POST请求</summary>
/// <typeparam name="T">响应类型</typeparam>
/// <param name="url">请求地址</param>
/// <param name="body">请求体</param>
/// <returns></returns>
private async Task<T> PostAsync<T>(String url, Object body) where T : class
{
try
{
var json = body.ToJson();
Log?.Debug($"POST: {url}, Body: {json}");
var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json");
var response = await _client.PostAsync(url, content);
response.EnsureSuccessStatusCode();
var responseContent = await response.Content.ReadAsStringAsync();
Log?.Debug($"Response: {responseContent}");
return responseContent.ToJsonEntity<T>();
}
catch (Exception ex)
{
Log?.Error($"POST请求失败: {url}, {ex.Message}");
throw;
}
}
#endregion
#region 日志
/// <summary>写日志</summary>
/// <param name="message">日志信息</param>
public void WriteLog(String message)
{
Log?.Info(message);
}
#endregion
}
|