308 lines
12 KiB
C#
308 lines
12 KiB
C#
using System.Text;
|
||
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.Fuwuhao;
|
||
|
||
/// <summary>
|
||
/// 服务号服务
|
||
/// </summary>
|
||
public class FuwuhaoService : ApplicationService
|
||
{
|
||
private readonly ILogger<FuwuhaoService> _logger;
|
||
private readonly IHttpContextAccessor _accessor;
|
||
private readonly FuwuhaoManager _fuwuhaoManager;
|
||
private IDistributedCache<SceneCacheDto> _sceneCache;
|
||
private IDistributedCache<string> _openIdToSceneCache;
|
||
private ISqlSugarRepository<AiUserExtraInfoEntity> _userRepository;
|
||
private IAccountService _accountService;
|
||
private IFileService _fileService;
|
||
public IDistributedLockProvider DistributedLock => LazyServiceProvider.LazyGetService<IDistributedLockProvider>();
|
||
|
||
public FuwuhaoService(ILogger<FuwuhaoService> logger, IHttpContextAccessor accessor, FuwuhaoManager fuwuhaoManager,
|
||
IDistributedCache<SceneCacheDto> sceneCache, IAccountService accountService, IFileService fileService,
|
||
IDistributedCache<string> openIdToSceneCache, ISqlSugarRepository<AiUserExtraInfoEntity> userRepositroy)
|
||
{
|
||
_logger = logger;
|
||
_accessor = accessor;
|
||
_fuwuhaoManager = fuwuhaoManager;
|
||
_sceneCache = sceneCache;
|
||
_accountService = accountService;
|
||
_fileService = fileService;
|
||
_openIdToSceneCache = openIdToSceneCache;
|
||
_userRepository = userRepositroy;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 服务器号测试回调
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
[HttpGet("fuwuhao/callback")]
|
||
public async Task<string> GetCallbackAsync([FromQuery] string signature, [FromQuery] string timestamp,
|
||
[FromQuery] string nonce, [FromQuery] string echostr)
|
||
{
|
||
return echostr;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 服务号关注回调
|
||
/// </summary>
|
||
/// <param name="signature"></param>
|
||
/// <param name="timestamp"></param>
|
||
/// <param name="nonce"></param>
|
||
/// <returns></returns>
|
||
[HttpPost("fuwuhao/callback")]
|
||
public async Task<string> 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}"))
|
||
{
|
||
if (handle == null)
|
||
{
|
||
return "success"; // 跳过直接返回成功
|
||
}
|
||
|
||
var cache = await _sceneCache.GetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}");
|
||
if (cache == null)
|
||
{
|
||
return "success"; // 跳过直接返回成功
|
||
}
|
||
|
||
if (cache.SceneResult != SceneResultEnum.Wait)
|
||
{
|
||
return "success"; // 跳过直接返回成功
|
||
}
|
||
|
||
//根据操作类型,进行业务处理,返回处理结果,再写入缓存,10s过去,相当于用户10s扫完app后,轮询要在10秒内完成
|
||
var scenResult =
|
||
await _fuwuhaoManager.CallBackHandlerAsync(cache.SceneType, body.FromUserName, cache.UserId);
|
||
cache.SceneResult = scenResult.SceneResult;
|
||
cache.UserId = scenResult.UserId;
|
||
await _sceneCache.SetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}", cache,
|
||
new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(120) });
|
||
|
||
//如果是注册,将OpenId与Scene进行绑定,代表用户有30分钟进行注册
|
||
if (scenResult.SceneResult == 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";
|
||
}
|
||
|
||
|
||
/// <summary>
|
||
/// 创建带参数的二维码
|
||
/// </summary>
|
||
/// <returns>二维码URL</returns>
|
||
[HttpPost("fuwuhao/qrcode")]
|
||
public async Task<QrCodeOutput> GetQrCodeAsync([FromQuery] SceneTypeEnum sceneType)
|
||
{
|
||
if (sceneType == SceneTypeEnum.Bind && CurrentUser.Id is null)
|
||
{
|
||
throw new UserFriendlyException("绑定微信,需登录用户,请重新登录后重试");
|
||
}
|
||
|
||
//生成一个随机场景值
|
||
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
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 扫码登录/注册/绑定,轮询接口
|
||
/// </summary>
|
||
/// <param name="scene"></param>
|
||
/// <returns></returns>
|
||
[HttpGet("fuwuhao/qrcode/result")]
|
||
public async Task<QrCodeResultOutput> 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:
|
||
if (cache.UserId is null)
|
||
{
|
||
throw new ApplicationException("获取用户id异常,请重试");
|
||
}
|
||
|
||
var loginInfo = await _accountService.PostLoginAsync(cache.UserId!.Value);
|
||
output.Token = loginInfo.Token;
|
||
output.RefreshToken = loginInfo.RefreshToken;
|
||
break;
|
||
}
|
||
|
||
return output;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 注册账号,需要微信code
|
||
/// </summary>
|
||
/// <param name="code"></param>
|
||
/// <returns></returns>
|
||
[HttpGet("fuwuhao/register")]
|
||
public async Task<IActionResult> RegisterByCodeAsync([FromQuery] string code)
|
||
{
|
||
var message = await RegisterByCodeForMessageAsync(code);
|
||
//var message = "恭喜注册";
|
||
var filePath = Path.Combine("wwwroot", "aihub", "auth.html");
|
||
var html = await File.ReadAllTextAsync(filePath);
|
||
var result = new ContentResult
|
||
{
|
||
Content = html.Replace("{{message}}", message),
|
||
ContentType = "text/html",
|
||
StatusCode = 200
|
||
};
|
||
return result;
|
||
}
|
||
|
||
|
||
private async Task<string> RegisterByCodeForMessageAsync(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);
|
||
|
||
//由于存在查询/编辑在同一个事务操作,上锁防止并发
|
||
await using (await DistributedLock.AcquireLockAsync($"fuwuhao:RegisterLock:{userInfo.OpenId}",
|
||
TimeSpan.FromMinutes(1)))
|
||
{
|
||
if (await _userRepository.IsAnyAsync(x => x.FuwuhaoOpenId == userInfo.OpenId))
|
||
{
|
||
return "你已注册过意社区账号!";
|
||
}
|
||
|
||
Guid userId;
|
||
try
|
||
{
|
||
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()
|
||
});
|
||
}
|
||
catch (UserFriendlyException e)
|
||
{
|
||
return e.Message;
|
||
}
|
||
|
||
|
||
await _userRepository.InsertAsync(new AiUserExtraInfoEntity(userId, userInfo.OpenId));
|
||
await _sceneCache.SetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}", new SceneCacheDto
|
||
{
|
||
UserId = userId,
|
||
SceneResult = SceneResultEnum.Login
|
||
},
|
||
new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(120) });
|
||
}
|
||
|
||
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; }
|
||
} |