feat(wechat): Integrate service account user info API for UnionId retrieval
All checks were successful
continuous-integration/drone/push Build is passing

- Add IWeChatService dependency injection to WeChatEventController
- Implement GetServiceAccountUserInfoAsync method to fetch user info including UnionId from WeChat API
- Add ServiceAccountUserInfo DTO class with Subscribe, OpenId, and UnionId properties
- Refactor follow event handler to use WeChat API for UnionId instead of XML parsing
- Add validation to ensure UnionId is retrieved before attempting user association
- Update notification URLs in appsettings.json to remove redundant path segment
- Improve error logging when UnionId retrieval fails or user association is not found
This commit is contained in:
zpc 2026-03-29 20:23:54 +08:00
parent 4af5ae8065
commit 3c53cddd0b
4 changed files with 111 additions and 24 deletions

View File

@ -6,6 +6,7 @@ using System.Xml.Linq;
using XiangYi.Application.Interfaces;
using XiangYi.Core.Entities.Biz;
using XiangYi.Core.Interfaces;
using XiangYi.Infrastructure.WeChat;
namespace XiangYi.AppApi.Controllers;
@ -21,17 +22,20 @@ public class WeChatEventController : ControllerBase
private readonly ILogger<WeChatEventController> _logger;
private readonly IConfiguration _configuration;
private readonly ISystemConfigService _configService;
private readonly IWeChatService _weChatService;
public WeChatEventController(
IRepository<User> userRepository,
ILogger<WeChatEventController> logger,
IConfiguration configuration,
ISystemConfigService configService)
ISystemConfigService configService,
IWeChatService weChatService)
{
_userRepository = userRepository;
_logger = logger;
_configuration = configuration;
_configService = configService;
_weChatService = weChatService;
}
/// <summary>
@ -109,34 +113,33 @@ public class WeChatEventController : ControllerBase
_logger.LogInformation("用户关注服务号: ServiceAccountOpenId={OpenId}", serviceAccountOpenId);
// 尝试通过UnionId关联用户
// 注意需要服务号和小程序绑定到同一个开放平台才能获取UnionId
var unionId = root?.Element("UnionId")?.Value;
// 通过服务号接口获取用户信息包含UnionId
var userInfo = await _weChatService.GetServiceAccountUserInfoAsync(serviceAccountOpenId);
var unionId = userInfo?.UnionId;
if (!string.IsNullOrEmpty(unionId))
if (string.IsNullOrEmpty(unionId))
{
// 通过UnionId查找用户
var users = await _userRepository.GetListAsync(u => u.UnionId == unionId);
var user = users.FirstOrDefault();
_logger.LogWarning("用户关注服务号但获取UnionId失败: ServiceAccountOpenId={OpenId},请确认服务号已绑定微信开放平台", serviceAccountOpenId);
return;
}
if (user != null)
{
user.ServiceAccountOpenId = serviceAccountOpenId;
user.IsFollowServiceAccount = true;
user.UpdateTime = DateTime.Now;
await _userRepository.UpdateAsync(user);
// 通过UnionId查找小程序用户
var users = await _userRepository.GetListAsync(u => u.UnionId == unionId);
var user = users.FirstOrDefault();
_logger.LogInformation("用户关注服务号并关联成功: UserId={UserId}, UnionId={UnionId}, ServiceAccountOpenId={OpenId}",
user.Id, unionId, serviceAccountOpenId);
}
else
{
_logger.LogInformation("用户关注服务号但未找到关联用户: UnionId={UnionId}", unionId);
}
if (user != null)
{
user.ServiceAccountOpenId = serviceAccountOpenId;
user.IsFollowServiceAccount = true;
user.UpdateTime = DateTime.Now;
await _userRepository.UpdateAsync(user);
_logger.LogInformation("用户关注服务号并关联成功: UserId={UserId}, UnionId={UnionId}, ServiceAccountOpenId={OpenId}",
user.Id, unionId, serviceAccountOpenId);
}
else
{
_logger.LogInformation("用户关注服务号但无UnionId: ServiceAccountOpenId={OpenId}", serviceAccountOpenId);
_logger.LogInformation("用户关注服务号但未找到关联小程序用户: UnionId={UnionId}", unionId);
}
}

View File

@ -59,7 +59,7 @@
"CertSerialNo": "429F8544BF89D61B1A98643277A8DC7E5C4B1DAA",
"PrivateKey": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDoFVHlfDo3flrT\nXLEbAcoQvh3bG/TJq38sjim0Frk740zkRQfkYMIUqtkPZBLl47MSPwaOKjxIyYJB\nr4CIW10rg7pA67pnpnRgYoPGF/DMT5Mle+pxWt94xGT8ircwx7WwTBHDthJiGhQN\nwTiy7dSH8Nbk14fcK6AN794dNRVwILon0O8z2osuoQngjVDLXB0BVYqZjV7/js99\nyXRZnZVuxiuvnbHzNxxe86qZgXjgFKpiey5sinx+CRjTyDD7CCVOVKwr7h7cKqVC\nbbnN20DWzLundRkBJFh3dbUcX4Pp3gV9m6B0UURARjpLkym6j3fRDLGJHWREeQ7N\no6qsaUdtAgMBAAECggEBAMuOD4OQ/tq3Z2Ak122RlzIyHauVDJFpaqSgl/FNUPA2\n/7Ti2vYy62cHJlR6eJzLpr8lKlG8t507qJSGItz2DXTiF5Vja94HP+Fd5qfzTY9V\naAEje1Aq3QBmeRCLdftB3pifT6FxaxRCPT6HL3y4XoVQ9ppGc/HnDX3L2euSKJhr\nYCZa+kB5L4FtM0JDGTnx+Q9fuCuKtCcT3YHaryildwiz4WiQxp1kvXj9bK+kNbBO\nPi79Kui8mRY3KDYaccxBgmqR9JkJ2/l52kKlJb5HWoRS3jh2/MulNj7gpWVy6KNb\ni6OMWs548EJRw9jrZu1cGmlThrguX9XaGWFvFfcRx0ECgYEA/ENIW7mm8cVnTCsu\n30qzQlJ7plljTIaan+TmVead8KK3fiRjUg4jK1Zh0JYFRaA4lC0M2mitsNKw0zHk\nKM3sh4ZIbzzh6CCOkUd0L7+3p4v9U3Bm6sjTS5WLy/MusPQEd4W86Gmn8LWcPx5q\n9Tz2hpbAyDals9AZEjgWu/rgWrECgYEA64WBaE+EEDkAtqkmpn8OLdlN5K51ncLe\n9Q4nv+NP9AtlwCmd3SBKv6qAEviS/hM4m+tjNlvS/UwP+6SPXTcnaBaCrpYdgv7e\nkVSZHboxHdRnYqReg6WhnOErol2GLqe/9+gc70x97T/KCIZ+/nE2MnQy2uFZVm12\nkxMvj1g+r30CgYAPb2Z8Bk4KuRNq+7FwhDeXtUhPk2SaCBpp8i2N0ACV+r7TfxJ8\nsNTCEBUIGEXWTslnd6Izsvf9u8aKBaF6Ra9VU4gXFliURXmztfWL/mUUYWJsupHx\nh7w2Ab5+CjEvLp8fWRWH+v8FoXcf/ZJ50vMapRrCpWVaLT97d+ccNWuI4QKBgQDO\nR4goDDzm2IY/dbdcbDvG/GS0vfhVzK/qghNehYEphjIANHMHkZjmdjbmZsCXt84F\nAg1LNvF82HnHNUI7qmrhR5X9w4zlhsT5FNdmqgUK01YZl00QkKkT9kN5WeCETHhe\ncPWmwaApg404GlRwFkgZuJwyCN1uTUFlX5BwRCHjIQKBgHTXcrlGfW5U2piJGdBs\nbi+I3nYPioyyHM9jUmdBtEtR04pXVV2590KZL2TknPB1dN2yhv9FUt4XO5+baoie\nas6QkQGrtOtVnO2X/oVOZQBmPG3RGZAMcWgYXJeLCxlf+DZ0OZNn0/V3od39WN7t\n84/yPSRGUr71Q48atr9N9N9x\n-----END PRIVATE KEY-----",
"ApiV3Key": "1230uaPcnzdh3lkxjcoiddUBXddWkpx2",
"NotifyUrl": "https://app.zpc-xy.com/xyqj/api/order/payNotify"
"NotifyUrl": "https://app.zpc-xy.com/api/order/payNotify"
}
},
"WeChatPay": {
@ -71,7 +71,7 @@
"CertPath": "apiclient_cert.pem",
"PlatformCertPath": "pub_key.pem",
"PlatformCertSerialNo": "PUB_KEY_ID_0117379432252026012200382382002003",
"NotifyUrl": "https://app.zpc-xy.com/xyqj/api/app/pay/notify"
"NotifyUrl": "https://app.zpc-xy.com/api/app/pay/notify"
},
"Storage": {
"Provider": "TencentCos",

View File

@ -69,6 +69,13 @@ public interface IWeChatService
/// </summary>
/// <returns>AccessToken</returns>
Task<string?> GetServiceAccountAccessTokenAsync();
/// <summary>
/// 获取服务号关注用户信息包含UnionId
/// </summary>
/// <param name="openId">用户在服务号的OpenId</param>
/// <returns>用户信息</returns>
Task<ServiceAccountUserInfo?> GetServiceAccountUserInfoAsync(string openId);
}
/// <summary>
@ -328,3 +335,24 @@ public class MiniProgramInfo
/// </summary>
public string? PagePath { get; set; }
}
/// <summary>
/// 服务号关注用户信息
/// </summary>
public class ServiceAccountUserInfo
{
/// <summary>
/// 是否关注0未关注1已关注
/// </summary>
public int Subscribe { get; set; }
/// <summary>
/// 用户OpenId
/// </summary>
public string OpenId { get; set; } = string.Empty;
/// <summary>
/// UnionId绑定开放平台后才有
/// </summary>
public string? UnionId { get; set; }
}

View File

@ -470,6 +470,44 @@ public class WeChatService : IWeChatService
}
}
public async Task<ServiceAccountUserInfo?> GetServiceAccountUserInfoAsync(string openId)
{
try
{
var accessToken = await GetServiceAccountAccessTokenAsync();
if (string.IsNullOrEmpty(accessToken))
{
_logger.LogError("获取服务号用户信息失败: AccessToken为空");
return null;
}
var url = $"https://api.weixin.qq.com/cgi-bin/user/info?access_token={accessToken}&openid={openId}&lang=zh_CN";
var httpResponse = await _httpClient.GetAsync(url);
var responseContent = await httpResponse.Content.ReadAsStringAsync();
_logger.LogInformation("获取服务号用户信息响应: {Response}", responseContent);
var result = System.Text.Json.JsonSerializer.Deserialize<ServiceAccountUserInfoResponse>(responseContent);
if (result == null || result.ErrCode != 0)
{
_logger.LogWarning("获取服务号用户信息失败: {ErrCode} - {ErrMsg}", result?.ErrCode, result?.ErrMsg);
return null;
}
return new ServiceAccountUserInfo
{
Subscribe = result.Subscribe,
OpenId = result.OpenId ?? openId,
UnionId = result.UnionId
};
}
catch (Exception ex)
{
_logger.LogError(ex, "获取服务号用户信息异常: OpenId={OpenId}", openId);
return null;
}
}
#endregion
#region
@ -752,5 +790,23 @@ public class WeChatService : IWeChatService
public string? OpenId { get; set; }
}
private class ServiceAccountUserInfoResponse
{
[JsonPropertyName("subscribe")]
public int Subscribe { get; set; }
[JsonPropertyName("openid")]
public string? OpenId { get; set; }
[JsonPropertyName("unionid")]
public string? UnionId { get; set; }
[JsonPropertyName("errcode")]
public int ErrCode { get; set; }
[JsonPropertyName("errmsg")]
public string? ErrMsg { get; set; }
}
#endregion
}