2025-08-27 17:46:39 +08:00
|
|
|
|
using System.Text;
|
|
|
|
|
|
using System.Xml.Serialization;
|
2025-08-28 15:20:15 +08:00
|
|
|
|
using Medallion.Threading;
|
2025-08-27 17:46:39 +08:00
|
|
|
|
using Microsoft.AspNetCore.Http;
|
|
|
|
|
|
using Microsoft.AspNetCore.Mvc;
|
2025-08-27 23:42:46 +08:00
|
|
|
|
using Microsoft.Extensions.Caching.Distributed;
|
2025-08-27 17:46:39 +08:00
|
|
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
|
|
using Volo.Abp.Application.Services;
|
2025-08-27 23:42:46 +08:00
|
|
|
|
using Volo.Abp.Caching;
|
|
|
|
|
|
using Volo.Abp.Users;
|
|
|
|
|
|
using Yi.Framework.AiHub.Application.Contracts.Dtos.Fuwuhao;
|
2025-08-29 13:54:02 +08:00
|
|
|
|
using Yi.Framework.AiHub.Domain.Entities;
|
2025-08-27 23:42:46 +08:00
|
|
|
|
using Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
|
2025-08-29 11:01:09 +08:00
|
|
|
|
using Yi.Framework.AiHub.Domain.Shared.Consts;
|
2025-08-27 23:42:46 +08:00
|
|
|
|
using Yi.Framework.AiHub.Domain.Shared.Enums.Fuwuhao;
|
|
|
|
|
|
using Yi.Framework.Rbac.Application.Contracts.Dtos.Account;
|
|
|
|
|
|
using Yi.Framework.Rbac.Application.Contracts.IServices;
|
2025-08-29 13:54:02 +08:00
|
|
|
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
2025-08-27 17:46:39 +08:00
|
|
|
|
|
2025-10-14 23:02:44 +08:00
|
|
|
|
namespace Yi.Framework.AiHub.Application.Services.Fuwuhao;
|
2025-08-27 17:46:39 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 服务号服务
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
public class FuwuhaoService : ApplicationService
|
|
|
|
|
|
{
|
|
|
|
|
|
private readonly ILogger<FuwuhaoService> _logger;
|
|
|
|
|
|
private readonly IHttpContextAccessor _accessor;
|
2025-08-27 23:42:46 +08:00
|
|
|
|
private readonly FuwuhaoManager _fuwuhaoManager;
|
|
|
|
|
|
private IDistributedCache<SceneCacheDto> _sceneCache;
|
2025-08-29 11:01:09 +08:00
|
|
|
|
private IDistributedCache<string> _openIdToSceneCache;
|
2025-08-29 13:54:02 +08:00
|
|
|
|
private ISqlSugarRepository<AiUserExtraInfoEntity> _userRepository;
|
2025-08-27 23:42:46 +08:00
|
|
|
|
private IAccountService _accountService;
|
|
|
|
|
|
private IFileService _fileService;
|
2025-08-28 15:20:15 +08:00
|
|
|
|
public IDistributedLockProvider DistributedLock => LazyServiceProvider.LazyGetService<IDistributedLockProvider>();
|
2025-08-27 17:46:39 +08:00
|
|
|
|
|
2025-08-27 23:42:46 +08:00
|
|
|
|
public FuwuhaoService(ILogger<FuwuhaoService> logger, IHttpContextAccessor accessor, FuwuhaoManager fuwuhaoManager,
|
2025-08-29 11:01:09 +08:00
|
|
|
|
IDistributedCache<SceneCacheDto> sceneCache, IAccountService accountService, IFileService fileService,
|
2025-08-29 13:54:02 +08:00
|
|
|
|
IDistributedCache<string> openIdToSceneCache, ISqlSugarRepository<AiUserExtraInfoEntity> userRepositroy)
|
2025-08-27 17:46:39 +08:00
|
|
|
|
{
|
|
|
|
|
|
_logger = logger;
|
|
|
|
|
|
_accessor = accessor;
|
2025-08-27 23:42:46 +08:00
|
|
|
|
_fuwuhaoManager = fuwuhaoManager;
|
|
|
|
|
|
_sceneCache = sceneCache;
|
|
|
|
|
|
_accountService = accountService;
|
|
|
|
|
|
_fileService = fileService;
|
2025-08-29 11:01:09 +08:00
|
|
|
|
_openIdToSceneCache = openIdToSceneCache;
|
2025-08-29 13:54:02 +08:00
|
|
|
|
_userRepository = userRepositroy;
|
2025-08-27 17:46:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
2025-08-27 23:42:46 +08:00
|
|
|
|
/// 服务器号测试回调
|
2025-08-27 17:46:39 +08:00
|
|
|
|
/// </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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-27 23:42:46 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 服务号关注回调
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="signature"></param>
|
|
|
|
|
|
/// <param name="timestamp"></param>
|
|
|
|
|
|
/// <param name="nonce"></param>
|
|
|
|
|
|
/// <returns></returns>
|
2025-08-27 17:46:39 +08:00
|
|
|
|
[HttpPost("fuwuhao/callback")]
|
|
|
|
|
|
public async Task<string> PostCallbackAsync([FromQuery] string signature, [FromQuery] string timestamp,
|
|
|
|
|
|
[FromQuery] string nonce)
|
|
|
|
|
|
{
|
2025-08-29 11:01:09 +08:00
|
|
|
|
_fuwuhaoManager.ValidateCallback(signature, timestamp, nonce);
|
2025-08-27 17:46:39 +08:00
|
|
|
|
var request = _accessor.HttpContext.Request;
|
|
|
|
|
|
// 1. 读取原始 XML 内容
|
|
|
|
|
|
using var reader = new StreamReader(request.Body, Encoding.UTF8);
|
|
|
|
|
|
var xmlString = await reader.ReadToEndAsync();
|
2025-08-27 23:42:46 +08:00
|
|
|
|
|
2025-08-27 17:46:39 +08:00
|
|
|
|
var serializer = new XmlSerializer(typeof(FuwuhaoCallModel));
|
|
|
|
|
|
using var stringReader = new StringReader(xmlString);
|
|
|
|
|
|
var body = (FuwuhaoCallModel)serializer.Deserialize(stringReader);
|
2025-08-27 23:42:46 +08:00
|
|
|
|
|
|
|
|
|
|
//获取场景值,后续通过场景值设置缓存状态,前端轮询这个场景值用户是否已操作即可
|
2025-08-29 11:01:09 +08:00
|
|
|
|
var scene = body.EventKey.Replace("qrscene_", "");
|
2025-08-28 15:20:15 +08:00
|
|
|
|
if (!(body.Event is "SCAN" or "subscribe"))
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new UserFriendlyException("当前回调只处理扫码 与 关注");
|
|
|
|
|
|
}
|
2025-08-29 11:01:09 +08:00
|
|
|
|
|
2025-08-27 23:42:46 +08:00
|
|
|
|
if (scene is null)
|
2025-08-27 17:46:39 +08:00
|
|
|
|
{
|
2025-08-27 23:42:46 +08:00
|
|
|
|
throw new UserFriendlyException("服务号返回无场景值");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-28 15:20:15 +08:00
|
|
|
|
//制作幂等
|
|
|
|
|
|
await using (var handle =
|
2025-08-30 21:17:25 +08:00
|
|
|
|
await DistributedLock.TryAcquireLockAsync($"Yi:fuwuhao:callbacklock:{scene}"))
|
2025-08-28 15:20:15 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (handle == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return "success"; // 跳过直接返回成功
|
|
|
|
|
|
}
|
2025-08-27 23:42:46 +08:00
|
|
|
|
|
2025-08-29 11:01:09 +08:00
|
|
|
|
var cache = await _sceneCache.GetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}");
|
2025-08-29 11:34:57 +08:00
|
|
|
|
if (cache == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
return "success"; // 跳过直接返回成功
|
|
|
|
|
|
}
|
2025-08-27 23:42:46 +08:00
|
|
|
|
|
2025-08-30 22:07:09 +08:00
|
|
|
|
if (cache.SceneResult != SceneResultEnum.Wait)
|
|
|
|
|
|
{
|
|
|
|
|
|
return "success"; // 跳过直接返回成功
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-28 15:20:15 +08:00
|
|
|
|
//根据操作类型,进行业务处理,返回处理结果,再写入缓存,10s过去,相当于用户10s扫完app后,轮询要在10秒内完成
|
|
|
|
|
|
var scenResult =
|
|
|
|
|
|
await _fuwuhaoManager.CallBackHandlerAsync(cache.SceneType, body.FromUserName, cache.UserId);
|
2025-08-29 21:55:07 +08:00
|
|
|
|
cache.SceneResult = scenResult.SceneResult;
|
|
|
|
|
|
cache.UserId = scenResult.UserId;
|
2025-08-29 11:01:09 +08:00
|
|
|
|
await _sceneCache.SetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}", cache,
|
2025-08-29 23:32:40 +08:00
|
|
|
|
new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(120) });
|
2025-08-29 11:01:09 +08:00
|
|
|
|
|
|
|
|
|
|
//如果是注册,将OpenId与Scene进行绑定,代表用户有30分钟进行注册
|
2025-08-29 21:55:07 +08:00
|
|
|
|
if (scenResult.SceneResult == SceneResultEnum.Register)
|
2025-08-29 11:01:09 +08:00
|
|
|
|
{
|
|
|
|
|
|
await _openIdToSceneCache.SetAsync($"{FuwuhaoConst.OpenIdToSceneCacheKey}{body.FromUserName}", scene,
|
|
|
|
|
|
new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(30) });
|
|
|
|
|
|
|
|
|
|
|
|
var replyMessage =
|
|
|
|
|
|
_fuwuhaoManager.BuildRegisterMessage(body.FromUserName);
|
|
|
|
|
|
return replyMessage;
|
|
|
|
|
|
}
|
2025-08-28 15:20:15 +08:00
|
|
|
|
}
|
2025-08-27 23:42:46 +08:00
|
|
|
|
|
2025-08-27 17:46:39 +08:00
|
|
|
|
return "success";
|
|
|
|
|
|
}
|
2025-08-27 23:42:46 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 创建带参数的二维码
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns>二维码URL</returns>
|
|
|
|
|
|
[HttpPost("fuwuhao/qrcode")]
|
|
|
|
|
|
public async Task<QrCodeOutput> GetQrCodeAsync([FromQuery] SceneTypeEnum sceneType)
|
|
|
|
|
|
{
|
2025-08-30 18:02:46 +08:00
|
|
|
|
if (sceneType == SceneTypeEnum.Bind && CurrentUser.Id is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new UserFriendlyException("绑定微信,需登录用户,请重新登录后重试");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-27 23:42:46 +08:00
|
|
|
|
//生成一个随机场景值
|
|
|
|
|
|
var scene = Guid.NewGuid().ToString("N");
|
|
|
|
|
|
var qrCodeUrl = await _fuwuhaoManager.CreateQrCodeAsync(scene);
|
2025-08-29 11:01:09 +08:00
|
|
|
|
await _sceneCache.SetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}", new SceneCacheDto()
|
2025-08-27 23:42:46 +08:00
|
|
|
|
{
|
|
|
|
|
|
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();
|
2025-08-29 11:01:09 +08:00
|
|
|
|
var cache = await _sceneCache.GetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}");
|
2025-08-27 23:42:46 +08:00
|
|
|
|
if (cache is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
output.SceneResult = SceneResultEnum.Expired;
|
|
|
|
|
|
return output;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
output.SceneResult = cache.SceneResult;
|
|
|
|
|
|
switch (output.SceneResult)
|
|
|
|
|
|
{
|
|
|
|
|
|
case SceneResultEnum.Login:
|
2025-08-29 21:55:07 +08:00
|
|
|
|
if (cache.UserId is null)
|
|
|
|
|
|
{
|
|
|
|
|
|
throw new ApplicationException("获取用户id异常,请重试");
|
|
|
|
|
|
}
|
2025-08-30 00:02:27 +08:00
|
|
|
|
|
2025-08-27 23:42:46 +08:00
|
|
|
|
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>
|
2025-08-29 11:01:09 +08:00
|
|
|
|
[HttpGet("fuwuhao/register")]
|
2025-08-30 00:02:27 +08:00
|
|
|
|
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)
|
2025-08-27 23:42:46 +08:00
|
|
|
|
{
|
|
|
|
|
|
//根据code获取到openid、微信用户昵称、头像
|
|
|
|
|
|
var userInfo = await _fuwuhaoManager.GetUserInfoByCodeAsync(code);
|
2025-08-29 11:01:09 +08:00
|
|
|
|
if (userInfo is null)
|
|
|
|
|
|
{
|
2025-08-30 00:02:27 +08:00
|
|
|
|
return "当前注册已经失效,请重新扫码注册!";
|
2025-08-29 11:01:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var scene = await _openIdToSceneCache.GetAsync($"{FuwuhaoConst.OpenIdToSceneCacheKey}{userInfo.OpenId}");
|
|
|
|
|
|
if (scene is null)
|
|
|
|
|
|
{
|
2025-08-30 00:02:27 +08:00
|
|
|
|
return "当前注册已经过期,请重新扫码注册!";
|
2025-08-29 11:01:09 +08:00
|
|
|
|
}
|
2025-08-28 15:20:15 +08:00
|
|
|
|
|
2025-08-29 11:01:09 +08:00
|
|
|
|
var files = new FormFileCollection();
|
2025-08-27 23:42:46 +08:00
|
|
|
|
// 下载头像并添加到系统文件中
|
|
|
|
|
|
if (!string.IsNullOrEmpty(userInfo.HeadImgUrl))
|
|
|
|
|
|
{
|
|
|
|
|
|
using var httpClient = new HttpClient();
|
|
|
|
|
|
var imageBytes = await httpClient.GetByteArrayAsync(userInfo.HeadImgUrl);
|
|
|
|
|
|
var imageStream = new MemoryStream(imageBytes);
|
2025-08-28 15:20:15 +08:00
|
|
|
|
|
2025-08-27 23:42:46 +08:00
|
|
|
|
// 从URL中提取文件扩展名,默认为png
|
2025-08-28 15:20:15 +08:00
|
|
|
|
var fileName = $"avatar_{userInfo.OpenId}.png";
|
2025-08-27 23:42:46 +08:00
|
|
|
|
var formFile = new FormFile(imageStream, 0, imageBytes.Length, "avatar", fileName)
|
|
|
|
|
|
{
|
|
|
|
|
|
Headers = new HeaderDictionary(),
|
|
|
|
|
|
ContentType = "image/png"
|
|
|
|
|
|
};
|
|
|
|
|
|
files.Add(formFile);
|
|
|
|
|
|
}
|
2025-08-28 15:20:15 +08:00
|
|
|
|
|
2025-08-27 23:42:46 +08:00
|
|
|
|
var result = await _fileService.Post(files);
|
|
|
|
|
|
|
2025-08-29 14:33:35 +08:00
|
|
|
|
//由于存在查询/编辑在同一个事务操作,上锁防止并发
|
2025-08-30 00:02:27 +08:00
|
|
|
|
await using (await DistributedLock.AcquireLockAsync($"fuwuhao:RegisterLock:{userInfo.OpenId}",
|
|
|
|
|
|
TimeSpan.FromMinutes(1)))
|
2025-08-29 14:31:04 +08:00
|
|
|
|
{
|
2025-08-29 14:33:35 +08:00
|
|
|
|
if (await _userRepository.IsAnyAsync(x => x.FuwuhaoOpenId == userInfo.OpenId))
|
2025-08-29 11:34:57 +08:00
|
|
|
|
{
|
2025-08-30 00:02:27 +08:00
|
|
|
|
return "你已注册过意社区账号!";
|
2025-08-29 14:33:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-30 00:02:27 +08:00
|
|
|
|
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)
|
2025-08-29 14:33:35 +08:00
|
|
|
|
{
|
2025-08-30 00:02:27 +08:00
|
|
|
|
return e.Message;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-08-29 14:33:35 +08:00
|
|
|
|
|
|
|
|
|
|
await _userRepository.InsertAsync(new AiUserExtraInfoEntity(userId, userInfo.OpenId));
|
2025-08-29 23:32:40 +08:00
|
|
|
|
await _sceneCache.SetAsync($"{FuwuhaoConst.SceneCacheKey}{scene}", new SceneCacheDto
|
2025-08-29 14:33:35 +08:00
|
|
|
|
{
|
2025-08-29 22:20:52 +08:00
|
|
|
|
UserId = userId,
|
|
|
|
|
|
SceneResult = SceneResultEnum.Login
|
2025-08-29 14:33:35 +08:00
|
|
|
|
},
|
2025-08-29 23:32:40 +08:00
|
|
|
|
new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(120) });
|
2025-08-29 14:33:35 +08:00
|
|
|
|
}
|
2025-08-29 11:01:09 +08:00
|
|
|
|
|
2025-08-30 00:02:27 +08:00
|
|
|
|
return "恭喜你成功注册意社区账号!";
|
2025-08-27 23:42:46 +08:00
|
|
|
|
}
|
2025-08-27 17:46:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[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; }
|
|
|
|
|
|
}
|