From 3c53cddd0b6c75e6ffd95560b9257f024b2d4b94 Mon Sep 17 00:00:00 2001 From: zpc Date: Sun, 29 Mar 2026 20:23:54 +0800 Subject: [PATCH] feat(wechat): Integrate service account user info API for UnionId retrieval - 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 --- .../Controllers/WeChatEventController.cs | 47 ++++++++-------- server/src/XiangYi.AppApi/appsettings.json | 4 +- .../WeChat/IWeChatService.cs | 28 ++++++++++ .../WeChat/WeChatService.cs | 56 +++++++++++++++++++ 4 files changed, 111 insertions(+), 24 deletions(-) diff --git a/server/src/XiangYi.AppApi/Controllers/WeChatEventController.cs b/server/src/XiangYi.AppApi/Controllers/WeChatEventController.cs index 4444a1f..9930087 100644 --- a/server/src/XiangYi.AppApi/Controllers/WeChatEventController.cs +++ b/server/src/XiangYi.AppApi/Controllers/WeChatEventController.cs @@ -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 _logger; private readonly IConfiguration _configuration; private readonly ISystemConfigService _configService; + private readonly IWeChatService _weChatService; public WeChatEventController( IRepository userRepository, ILogger logger, IConfiguration configuration, - ISystemConfigService configService) + ISystemConfigService configService, + IWeChatService weChatService) { _userRepository = userRepository; _logger = logger; _configuration = configuration; _configService = configService; + _weChatService = weChatService; } /// @@ -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); } } diff --git a/server/src/XiangYi.AppApi/appsettings.json b/server/src/XiangYi.AppApi/appsettings.json index 3d17d88..e830cbc 100644 --- a/server/src/XiangYi.AppApi/appsettings.json +++ b/server/src/XiangYi.AppApi/appsettings.json @@ -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", diff --git a/server/src/XiangYi.Infrastructure/WeChat/IWeChatService.cs b/server/src/XiangYi.Infrastructure/WeChat/IWeChatService.cs index b05ced4..bca96d2 100644 --- a/server/src/XiangYi.Infrastructure/WeChat/IWeChatService.cs +++ b/server/src/XiangYi.Infrastructure/WeChat/IWeChatService.cs @@ -69,6 +69,13 @@ public interface IWeChatService /// /// AccessToken Task GetServiceAccountAccessTokenAsync(); + + /// + /// 获取服务号关注用户信息(包含UnionId) + /// + /// 用户在服务号的OpenId + /// 用户信息 + Task GetServiceAccountUserInfoAsync(string openId); } /// @@ -328,3 +335,24 @@ public class MiniProgramInfo /// public string? PagePath { get; set; } } + +/// +/// 服务号关注用户信息 +/// +public class ServiceAccountUserInfo +{ + /// + /// 是否关注(0未关注,1已关注) + /// + public int Subscribe { get; set; } + + /// + /// 用户OpenId + /// + public string OpenId { get; set; } = string.Empty; + + /// + /// UnionId(绑定开放平台后才有) + /// + public string? UnionId { get; set; } +} diff --git a/server/src/XiangYi.Infrastructure/WeChat/WeChatService.cs b/server/src/XiangYi.Infrastructure/WeChat/WeChatService.cs index 2d872e6..40ca46a 100644 --- a/server/src/XiangYi.Infrastructure/WeChat/WeChatService.cs +++ b/server/src/XiangYi.Infrastructure/WeChat/WeChatService.cs @@ -470,6 +470,44 @@ public class WeChatService : IWeChatService } } + public async Task 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(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 }