using System.Text; using System.Text.Json; using System.Xml.Serialization; using Medallion.Threading; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Logging; using Volo.Abp.Application.Services; using Volo.Abp.Caching; using Volo.Abp.Users; using Yi.Framework.AiHub.Application.Contracts.Dtos.Fuwuhao; using Yi.Framework.AiHub.Domain.Entities; using Yi.Framework.AiHub.Domain.Managers.Fuwuhao; using Yi.Framework.AiHub.Domain.Shared.Consts; using Yi.Framework.AiHub.Domain.Shared.Enums.Fuwuhao; using Yi.Framework.Rbac.Application.Contracts.Dtos.Account; using Yi.Framework.Rbac.Application.Contracts.IServices; using Yi.Framework.SqlSugarCore.Abstractions; namespace Yi.Framework.AiHub.Application.Services; /// /// 服务号服务 /// public class FuwuhaoService : ApplicationService { private readonly ILogger _logger; private readonly IHttpContextAccessor _accessor; private readonly FuwuhaoManager _fuwuhaoManager; private IDistributedCache _sceneCache; private IDistributedCache _openIdToSceneCache; private ISqlSugarRepository _userRepository; private IAccountService _accountService; private IFileService _fileService; public IDistributedLockProvider DistributedLock => LazyServiceProvider.LazyGetService(); public FuwuhaoService(ILogger logger, IHttpContextAccessor accessor, FuwuhaoManager fuwuhaoManager, IDistributedCache sceneCache, IAccountService accountService, IFileService fileService, IDistributedCache openIdToSceneCache, ISqlSugarRepository userRepositroy) { _logger = logger; _accessor = accessor; _fuwuhaoManager = fuwuhaoManager; _sceneCache = sceneCache; _accountService = accountService; _fileService = fileService; _openIdToSceneCache = openIdToSceneCache; _userRepository = userRepositroy; } /// /// 服务器号测试回调 /// /// [HttpGet("fuwuhao/callback")] public async Task GetCallbackAsync([FromQuery] string signature, [FromQuery] string timestamp, [FromQuery] string nonce, [FromQuery] string echostr) { return echostr; } /// /// 服务号关注回调 /// /// /// /// /// [HttpPost("fuwuhao/callback")] public async Task PostCallbackAsync([FromQuery] string signature, [FromQuery] string timestamp, [FromQuery] string nonce) { _fuwuhaoManager.ValidateCallback(signature, timestamp, nonce); var request = _accessor.HttpContext.Request; // 1. 读取原始 XML 内容 using var reader = new StreamReader(request.Body, Encoding.UTF8); var xmlString = await reader.ReadToEndAsync(); var serializer = new XmlSerializer(typeof(FuwuhaoCallModel)); using var stringReader = new StringReader(xmlString); var body = (FuwuhaoCallModel)serializer.Deserialize(stringReader); //获取场景值,后续通过场景值设置缓存状态,前端轮询这个场景值用户是否已操作即可 var scene = body.EventKey.Replace("qrscene_", ""); if (!(body.Event is "SCAN" or "subscribe")) { throw new UserFriendlyException("当前回调只处理扫码 与 关注"); } if (scene is null) { throw new UserFriendlyException("服务号返回无场景值"); } //制作幂等 await using (var handle = await DistributedLock.TryAcquireLockAsync($"Yi:fuwuhao:callbacklock:{scene}", TimeSpan.FromSeconds(60))) { if (handle == null) { return "success"; // 跳过直接返回成功 } var cache = await _sceneCache.GetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}"); if (cache == null) { return "success"; // 跳过直接返回成功 } //根据操作类型,进行业务处理,返回处理结果,再写入缓存,10s过去,相当于用户10s扫完app后,轮询要在10秒内完成 var scenResult = await _fuwuhaoManager.CallBackHandlerAsync(cache.SceneType, body.FromUserName, cache.UserId); cache.SceneResult = scenResult; await _sceneCache.SetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}", cache, new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(50) }); //如果是注册,将OpenId与Scene进行绑定,代表用户有30分钟进行注册 if (scenResult == SceneResultEnum.Register) { await _openIdToSceneCache.SetAsync($"{FuwuhaoConst.OpenIdToSceneCacheKey}{body.FromUserName}", scene, new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) }); var replyMessage = _fuwuhaoManager.BuildRegisterMessage(body.FromUserName); return replyMessage; } } return "success"; } /// /// 创建带参数的二维码 /// /// 二维码URL [HttpPost("fuwuhao/qrcode")] public async Task GetQrCodeAsync([FromQuery] SceneTypeEnum sceneType) { //生成一个随机场景值 var scene = Guid.NewGuid().ToString("N"); var qrCodeUrl = await _fuwuhaoManager.CreateQrCodeAsync(scene); await _sceneCache.SetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}", new SceneCacheDto() { UserId = CurrentUser.IsAuthenticated ? CurrentUser.GetId() : null, SceneType = sceneType }, new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) }); return new QrCodeOutput() { QrCodeUrl = qrCodeUrl, Scene = scene }; } /// /// 扫码登录/注册/绑定,轮询接口 /// /// /// [HttpGet("fuwuhao/qrcode/result")] public async Task GetQrCodeResultAsync([FromQuery] string scene) { var output = new QrCodeResultOutput(); var cache = await _sceneCache.GetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}"); if (cache is null) { output.SceneResult = SceneResultEnum.Expired; return output; } output.SceneResult = cache.SceneResult; switch (output.SceneResult) { case SceneResultEnum.Login: var loginInfo = await _accountService.PostLoginAsync(cache.UserId!.Value); output.Token = loginInfo.Token; output.RefreshToken = loginInfo.RefreshToken; break; } return output; } /// /// 注册账号,需要微信code /// /// /// [HttpGet("fuwuhao/register")] public async Task RegisterByCodeAsync([FromQuery] string code) { //根据code获取到openid、微信用户昵称、头像 var userInfo = await _fuwuhaoManager.GetUserInfoByCodeAsync(code); if (userInfo is null) { return "当前注册已经失效,请重新扫码注册"; } var scene = await _openIdToSceneCache.GetAsync($"{FuwuhaoConst.OpenIdToSceneCacheKey}{userInfo.OpenId}"); if (scene is null) { return "当前注册已经过期,请重新扫码注册"; } var files = new FormFileCollection(); // 下载头像并添加到系统文件中 if (!string.IsNullOrEmpty(userInfo.HeadImgUrl)) { using var httpClient = new HttpClient(); var imageBytes = await httpClient.GetByteArrayAsync(userInfo.HeadImgUrl); var imageStream = new MemoryStream(imageBytes); // 从URL中提取文件扩展名,默认为png var fileName = $"avatar_{userInfo.OpenId}.png"; var formFile = new FormFile(imageStream, 0, imageBytes.Length, "avatar", fileName) { Headers = new HeaderDictionary(), ContentType = "image/png" }; files.Add(formFile); } var result = await _fileService.Post(files); if (await _userRepository.IsAnyAsync(x=>x.FuwuhaoOpenId==userInfo.OpenId)) { throw new UserFriendlyException("你已注册过意社区账号"); } var userId = await _accountService.PostSystemRegisterAsync(new RegisterDto { UserName = $"wx{Random.Shared.Next(100000, 999999)}", Password = Guid.NewGuid().ToString("N"), Phone = null, Email = null, Nick = userInfo.Nickname, Icon = result.FirstOrDefault()?.Id.ToString() }); await _userRepository.InsertAsync(new AiUserExtraInfoEntity(userId, userInfo.OpenId)); await _sceneCache.SetAsync($"{FuwuhaoConst.SceneCacheKey}:{scene}", new SceneCacheDto { SceneResult = SceneResultEnum.Register }, new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(50) }); return "恭喜你已注册成功意社区账号!"; } } [XmlRoot("xml")] public class FuwuhaoCallModel { [XmlElement("ToUserName")] public string ToUserName { get; set; } [XmlElement("FromUserName")] public string FromUserName { get; set; } [XmlElement("CreateTime")] public string CreateTime { get; set; } [XmlElement("MsgType")] public string MsgType { get; set; } [XmlElement("Event")] public string Event { get; set; } [XmlElement("EventKey")] public string EventKey { get; set; } [XmlElement("Ticket")] public string Ticket { get; set; } }