feat(wechat): Add access token refresh control and improve template message error handling
All checks were successful
continuous-integration/drone/push Build is passing

- Add forceRefresh parameter to GetServiceAccountAccessTokenAsync for cache bypass capability
- Implement automatic token refresh retry logic for HTTP 412 and token expiration error codes (40001, 42001)
- Extract SendServiceAccountTemplateMessageAsync into internal method to support retry mechanism
- Enhance logging with contextual parameters (AppId, ToUser, TemplateId, AccessToken prefix) for better diagnostics
- Add conditional logging for failed requests with full diagnostic information
- Improve error handling to distinguish between token expiration scenarios and other failures
- Add informational logging when requesting new access tokens
This commit is contained in:
zpc 2026-03-29 20:51:06 +08:00
parent e34d90886d
commit 9be72eb106
2 changed files with 49 additions and 16 deletions

View File

@ -67,8 +67,9 @@ public interface IWeChatService
/// <summary>
/// 获取服务号AccessToken
/// </summary>
/// <param name="forceRefresh">是否强制刷新(忽略缓存)</param>
/// <returns>AccessToken</returns>
Task<string?> GetServiceAccountAccessTokenAsync();
Task<string?> GetServiceAccountAccessTokenAsync(bool forceRefresh = false);
/// <summary>
/// 获取服务号关注用户信息包含UnionId

View File

@ -371,7 +371,7 @@ public class WeChatService : IWeChatService
private const string ServiceAccountAccessTokenCacheKey = "wechat:service_account:access_token";
public async Task<string?> GetServiceAccountAccessTokenAsync()
public async Task<string?> GetServiceAccountAccessTokenAsync(bool forceRefresh = false)
{
// 检查服务号配置
if (string.IsNullOrEmpty(_options.ServiceAccount?.AppId) ||
@ -381,10 +381,13 @@ public class WeChatService : IWeChatService
return null;
}
// 先从缓存获取
var cached = await _cache.GetStringAsync(ServiceAccountAccessTokenCacheKey);
if (!string.IsNullOrEmpty(cached))
return cached;
// 先从缓存获取(非强制刷新时)
if (!forceRefresh)
{
var cached = await _cache.GetStringAsync(ServiceAccountAccessTokenCacheKey);
if (!string.IsNullOrEmpty(cached))
return cached;
}
// 请求新的AccessToken
var url = $"https://api.weixin.qq.com/cgi-bin/token" +
@ -392,6 +395,8 @@ public class WeChatService : IWeChatService
$"&appid={_options.ServiceAccount.AppId}" +
$"&secret={_options.ServiceAccount.AppSecret}";
_logger.LogInformation("请求服务号AccessToken: AppId={AppId}", _options.ServiceAccount.AppId);
var httpResponse = await _httpClient.GetAsync(url);
var responseContent = await httpResponse.Content.ReadAsStringAsync();
var response = JsonSerializer.Deserialize<AccessTokenResponse>(responseContent);
@ -402,6 +407,8 @@ public class WeChatService : IWeChatService
return null;
}
_logger.LogInformation("获取服务号AccessToken成功");
// 缓存AccessToken提前5分钟过期
var expireSeconds = response.ExpiresIn - 300;
await _cache.SetStringAsync(
@ -416,13 +423,20 @@ public class WeChatService : IWeChatService
}
public async Task<bool> SendServiceAccountTemplateMessageAsync(ServiceAccountTemplateMessageRequest request)
{
return await SendServiceAccountTemplateMessageInternalAsync(request, isRetry: false);
}
private async Task<bool> SendServiceAccountTemplateMessageInternalAsync(
ServiceAccountTemplateMessageRequest request, bool isRetry)
{
try
{
var accessToken = await GetServiceAccountAccessTokenAsync();
if (string.IsNullOrEmpty(accessToken))
{
_logger.LogError("获取服务号AccessToken失败");
_logger.LogError("获取服务号AccessToken失败, AppId={AppId}, ToUser={ToUser}, TemplateId={TemplateId}",
_options.ServiceAccount?.AppId, request.ToUser, request.TemplateId);
return false;
}
@ -454,33 +468,51 @@ public class WeChatService : IWeChatService
var response = await _httpClient.PostAsJsonAsync(url, requestBody);
var responseContent = await response.Content.ReadAsStringAsync();
_logger.LogInformation("发送服务号模板消息: StatusCode={StatusCode}, Response={Response}",
(int)response.StatusCode, responseContent);
// 非200时打印完整诊断信息
if (!response.IsSuccessStatusCode || string.IsNullOrWhiteSpace(responseContent))
{
_logger.LogWarning("发送服务号模板消息失败: StatusCode={StatusCode}, AppId={AppId}, ToUser={ToUser}, TemplateId={TemplateId}, AccessToken={AccessToken}, IsRetry={IsRetry}, Response={Response}",
(int)response.StatusCode, _options.ServiceAccount?.AppId, request.ToUser, request.TemplateId,
accessToken?[..Math.Min(accessToken.Length, 20)] + "...", isRetry, responseContent);
}
// 打印请求体方便排查
var requestJson = System.Text.Json.JsonSerializer.Serialize(requestBody);
_logger.LogInformation("发送服务号模板消息请求体: {RequestBody}", requestJson);
// HTTP 412 表示 access_token 失效,强制刷新后重试一次
if ((int)response.StatusCode == 412 && !isRetry)
{
_logger.LogWarning("服务号AccessToken可能已失效(412),强制刷新后重试");
await _cache.RemoveAsync(ServiceAccountAccessTokenCacheKey);
return await SendServiceAccountTemplateMessageInternalAsync(request, isRetry: true);
}
if (string.IsNullOrWhiteSpace(responseContent))
{
_logger.LogWarning("发送服务号模板消息失败: 微信返回空响应");
return false;
}
var result = System.Text.Json.JsonSerializer.Deserialize<WeChatApiResponse>(responseContent);
// errcode 40001/42001 也是 token 失效,重试
if (result != null && (result.ErrCode == 40001 || result.ErrCode == 42001) && !isRetry)
{
_logger.LogWarning("服务号AccessToken失效({ErrCode}),强制刷新后重试, AppId={AppId}", result.ErrCode, _options.ServiceAccount?.AppId);
await _cache.RemoveAsync(ServiceAccountAccessTokenCacheKey);
return await SendServiceAccountTemplateMessageInternalAsync(request, isRetry: true);
}
if (result?.ErrCode != 0)
{
_logger.LogWarning("发送服务号模板消息失败: {ErrCode} - {ErrMsg}", result?.ErrCode, result?.ErrMsg);
_logger.LogWarning("发送服务号模板消息失败: ErrCode={ErrCode}, ErrMsg={ErrMsg}, AppId={AppId}, ToUser={ToUser}, TemplateId={TemplateId}",
result?.ErrCode, result?.ErrMsg, _options.ServiceAccount?.AppId, request.ToUser, request.TemplateId);
return false;
}
_logger.LogInformation("发送服务号模板消息成功: {ToUser}", request.ToUser);
_logger.LogInformation("发送服务号模板消息成功: ToUser={ToUser}, TemplateId={TemplateId}", request.ToUser, request.TemplateId);
return true;
}
catch (Exception ex)
{
_logger.LogError(ex, "发送服务号模板消息异常");
_logger.LogError(ex, "发送服务号模板消息异常: AppId={AppId}, ToUser={ToUser}, TemplateId={TemplateId}",
_options.ServiceAccount?.AppId, request.ToUser, request.TemplateId);
return false;
}
}