feat: 完成支持微信扫码功能
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Fuwuhao;
|
||||||
|
|
||||||
|
public class QrCodeOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Qrcode url
|
||||||
|
/// </summary>
|
||||||
|
public string QrCodeUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 场景值
|
||||||
|
/// </summary>
|
||||||
|
public string Scene { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using Yi.Framework.AiHub.Domain.Shared.Enums.Fuwuhao;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Fuwuhao;
|
||||||
|
|
||||||
|
public class QrCodeResultOutput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 返回状态
|
||||||
|
/// </summary>
|
||||||
|
public SceneResultEnum SceneResult { get; set; } = SceneResultEnum.Wait;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 如果是已登录,返回token
|
||||||
|
/// </summary>
|
||||||
|
public string? Token { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 刷新token
|
||||||
|
/// </summary>
|
||||||
|
public string? RefreshToken { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using Yi.Framework.AiHub.Domain.Shared.Enums.Fuwuhao;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Fuwuhao;
|
||||||
|
|
||||||
|
public class SceneCacheDto
|
||||||
|
{
|
||||||
|
public string Scene { get; set; }
|
||||||
|
|
||||||
|
public SceneResultEnum SceneResult { get; set; } = SceneResultEnum.Wait;
|
||||||
|
|
||||||
|
public SceneTypeEnum SceneType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 如果是绑定类型,需要用户id
|
||||||
|
/// </summary>
|
||||||
|
public Guid? UserId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
@@ -3,8 +3,16 @@ using System.Text.Json;
|
|||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Volo.Abp.Application.Services;
|
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.Managers.Fuwuhao;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Enums.Fuwuhao;
|
||||||
|
using Yi.Framework.Rbac.Application.Contracts.Dtos.Account;
|
||||||
|
using Yi.Framework.Rbac.Application.Contracts.IServices;
|
||||||
|
|
||||||
namespace Yi.Framework.AiHub.Application.Services;
|
namespace Yi.Framework.AiHub.Application.Services;
|
||||||
|
|
||||||
@@ -15,25 +23,41 @@ public class FuwuhaoService : ApplicationService
|
|||||||
{
|
{
|
||||||
private readonly ILogger<FuwuhaoService> _logger;
|
private readonly ILogger<FuwuhaoService> _logger;
|
||||||
private readonly IHttpContextAccessor _accessor;
|
private readonly IHttpContextAccessor _accessor;
|
||||||
|
private readonly FuwuhaoManager _fuwuhaoManager;
|
||||||
|
private IDistributedCache<SceneCacheDto> _sceneCache;
|
||||||
|
private IAccountService _accountService;
|
||||||
|
private IFileService _fileService;
|
||||||
|
|
||||||
public FuwuhaoService(ILogger<FuwuhaoService> logger, IHttpContextAccessor accessor)
|
public FuwuhaoService(ILogger<FuwuhaoService> logger, IHttpContextAccessor accessor, FuwuhaoManager fuwuhaoManager,
|
||||||
|
IDistributedCache<SceneCacheDto> sceneCache, IAccountService accountService, IFileService fileService)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_accessor = accessor;
|
_accessor = accessor;
|
||||||
|
_fuwuhaoManager = fuwuhaoManager;
|
||||||
|
_sceneCache = sceneCache;
|
||||||
|
_accountService = accountService;
|
||||||
|
_fileService = fileService;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询已登录的账户信息
|
/// 服务器号测试回调
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
[HttpGet("fuwuhao/callback")]
|
[HttpGet("fuwuhao/callback")]
|
||||||
public async Task<string> GetCallbackAsync([FromQuery] string signature, [FromQuery] string timestamp,
|
public async Task<string> GetCallbackAsync([FromQuery] string signature, [FromQuery] string timestamp,
|
||||||
[FromQuery] string nonce, [FromQuery] string echostr)
|
[FromQuery] string nonce, [FromQuery] string echostr)
|
||||||
{
|
{
|
||||||
|
|
||||||
return echostr;
|
return echostr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 服务号关注回调
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="signature"></param>
|
||||||
|
/// <param name="timestamp"></param>
|
||||||
|
/// <param name="nonce"></param>
|
||||||
|
/// <param name="openId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
[HttpPost("fuwuhao/callback")]
|
[HttpPost("fuwuhao/callback")]
|
||||||
public async Task<string> PostCallbackAsync([FromQuery] string signature, [FromQuery] string timestamp,
|
public async Task<string> PostCallbackAsync([FromQuery] string signature, [FromQuery] string timestamp,
|
||||||
[FromQuery] string nonce)
|
[FromQuery] string nonce)
|
||||||
@@ -46,12 +70,120 @@ public class FuwuhaoService : ApplicationService
|
|||||||
var serializer = new XmlSerializer(typeof(FuwuhaoCallModel));
|
var serializer = new XmlSerializer(typeof(FuwuhaoCallModel));
|
||||||
using var stringReader = new StringReader(xmlString);
|
using var stringReader = new StringReader(xmlString);
|
||||||
var body = (FuwuhaoCallModel)serializer.Deserialize(stringReader);
|
var body = (FuwuhaoCallModel)serializer.Deserialize(stringReader);
|
||||||
_logger.LogError("服务号Post回调通知:" + JsonSerializer.Serialize(body, new JsonSerializerOptions()
|
|
||||||
|
//获取场景值,后续通过场景值设置缓存状态,前端轮询这个场景值用户是否已操作即可
|
||||||
|
var scene = body.EventKey;
|
||||||
|
|
||||||
|
if (scene is null)
|
||||||
{
|
{
|
||||||
WriteIndented = true
|
throw new UserFriendlyException("服务号返回无场景值");
|
||||||
}));
|
}
|
||||||
|
|
||||||
|
var cache = await _sceneCache.GetAsync($"fuwuhao:{scene}");
|
||||||
|
|
||||||
|
//根据操作类型,进行业务处理,返回处理结果,再写入缓存,10s过去,相当于用户10s扫完app后,轮询要在10秒内完成
|
||||||
|
var scenResult = await _fuwuhaoManager.CallBackHandlerAsync(cache.SceneType, body.FromUserName, cache.UserId);
|
||||||
|
cache.SceneResult = scenResult;
|
||||||
|
|
||||||
|
await _sceneCache.SetAsync($"fuwuhao:{scene}", cache,
|
||||||
|
new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(10) });
|
||||||
|
|
||||||
return "success";
|
return "success";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建带参数的二维码
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>二维码URL</returns>
|
||||||
|
[HttpPost("fuwuhao/qrcode")]
|
||||||
|
public async Task<QrCodeOutput> GetQrCodeAsync([FromQuery] SceneTypeEnum sceneType)
|
||||||
|
{
|
||||||
|
//生成一个随机场景值
|
||||||
|
var scene = Guid.NewGuid().ToString("N");
|
||||||
|
var qrCodeUrl = await _fuwuhaoManager.CreateQrCodeAsync(scene);
|
||||||
|
await _sceneCache.SetAsync($"fuwuhao:{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($"fuwuhao:{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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册账号,需要微信code
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpPost("fuwuhao/register")]
|
||||||
|
public async Task<LoginOutputDto> RegisterByCodeAsync([FromQuery] string code)
|
||||||
|
{
|
||||||
|
//根据code获取到openid、微信用户昵称、头像
|
||||||
|
var userInfo = await _fuwuhaoManager.GetUserInfoByCodeAsync(code);
|
||||||
|
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);
|
||||||
|
|
||||||
|
var userId = await _accountService.PostSystemRegisterAsync(new RegisterDto
|
||||||
|
{
|
||||||
|
UserName = userInfo.Nickname,
|
||||||
|
Password = Guid.NewGuid().ToString("N"),
|
||||||
|
Phone = null,
|
||||||
|
Email = null,
|
||||||
|
Nick = userInfo.Nickname,
|
||||||
|
Icon = result.FirstOrDefault()?.Id.ToString()
|
||||||
|
});
|
||||||
|
return await _accountService.PostLoginAsync(userId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[XmlRoot("xml")]
|
[XmlRoot("xml")]
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Enums.Fuwuhao;
|
||||||
|
|
||||||
|
public enum SceneResultEnum
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 等待,用户未扫码
|
||||||
|
/// </summary>
|
||||||
|
Wait = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已扫码完成登录
|
||||||
|
/// </summary>
|
||||||
|
Login = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已扫码完成注册
|
||||||
|
/// </summary>
|
||||||
|
Register = 2,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已扫码完成绑定
|
||||||
|
/// </summary>
|
||||||
|
Bind = 3,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 已过期
|
||||||
|
/// </summary>
|
||||||
|
Expired = 10
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Domain.Shared.Enums.Fuwuhao;
|
||||||
|
|
||||||
|
public enum SceneTypeEnum
|
||||||
|
{
|
||||||
|
LoginOrRegister,
|
||||||
|
Bind
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
using Volo.Abp.Auditing;
|
||||||
|
using Volo.Abp.Domain.Entities;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ai用户表
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("Ai_UserExtraInfo")]
|
||||||
|
[SugarIndex($"index_{nameof(UserId)}", nameof(UserId), OrderByType.Asc)]
|
||||||
|
public class AiUserExtraInfoEntity : Entity<Guid>, IHasCreationTime, ISoftDelete
|
||||||
|
{
|
||||||
|
public AiUserExtraInfoEntity()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public AiUserExtraInfoEntity(Guid userId, string fuwuhaoOpenId)
|
||||||
|
{
|
||||||
|
this.UserId = userId;
|
||||||
|
this.FuwuhaoOpenId = fuwuhaoOpenId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户id
|
||||||
|
/// </summary>
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 服务号,openid
|
||||||
|
/// </summary>
|
||||||
|
public string FuwuhaoOpenId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreationTime { get; set; }
|
||||||
|
|
||||||
|
public bool IsDeleted { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AccessToken响应对象
|
||||||
|
/// </summary>
|
||||||
|
public class AccessTokenResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 访问令牌
|
||||||
|
/// </summary>
|
||||||
|
public string AccessToken { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 过期时间(秒)
|
||||||
|
/// </summary>
|
||||||
|
public int ExpiresIn { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Volo.Abp.Caching;
|
||||||
|
using Volo.Abp.Domain.Services;
|
||||||
|
using Yi.Framework.AiHub.Domain.Entities;
|
||||||
|
using Yi.Framework.AiHub.Domain.Shared.Enums.Fuwuhao;
|
||||||
|
using Yi.Framework.SqlSugarCore.Abstractions;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
|
||||||
|
|
||||||
|
public class FuwuhaoManager : DomainService
|
||||||
|
{
|
||||||
|
private readonly FuwuhaoOptions _options;
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private IDistributedCache<AccessTokenResponse> _accessTokenCache;
|
||||||
|
private ISqlSugarRepository<AiUserExtraInfoEntity> _userRepository;
|
||||||
|
|
||||||
|
public FuwuhaoManager(IOptions<FuwuhaoOptions> options, IHttpClientFactory httpClientFactory,
|
||||||
|
ISqlSugarRepository<AiUserExtraInfoEntity> userRepository)
|
||||||
|
{
|
||||||
|
_options = options.Value;
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
_userRepository = userRepository;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取微信公众号AccessToken
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>AccessToken响应对象</returns>
|
||||||
|
private async Task<string> GetAccessTokenAsync()
|
||||||
|
{
|
||||||
|
var output = await _accessTokenCache.GetOrAddAsync("Fuwuhao", async () =>
|
||||||
|
{
|
||||||
|
var url =
|
||||||
|
$"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={_options.AppId}&secret={_options.Secret}";
|
||||||
|
|
||||||
|
var response = await _httpClientFactory.CreateClient().GetAsync(url);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var jsonContent = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonSerializer.Deserialize<AccessTokenResponse>(jsonContent, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}, () => new DistributedCacheEntryOptions()
|
||||||
|
{
|
||||||
|
AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(3600)
|
||||||
|
});
|
||||||
|
|
||||||
|
return output.AccessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建带参数的二维码
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="scene">场景值</param>
|
||||||
|
/// <returns>二维码URL</returns>
|
||||||
|
public async Task<string> CreateQrCodeAsync(string scene)
|
||||||
|
{
|
||||||
|
var accessToken = await GetAccessTokenAsync();
|
||||||
|
var url = $"https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={accessToken}";
|
||||||
|
|
||||||
|
var requestBody = new
|
||||||
|
{
|
||||||
|
action_name = "QR_STR_SCENE",
|
||||||
|
action_info = new
|
||||||
|
{
|
||||||
|
scene = new
|
||||||
|
{
|
||||||
|
expire_seconds = 600,
|
||||||
|
scene_str = scene
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var jsonContent = JsonSerializer.Serialize(requestBody);
|
||||||
|
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
|
var response = await _httpClientFactory.CreateClient().PostAsync(url, content);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var responseContent = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonSerializer.Deserialize<QrCodeResponse>(responseContent);
|
||||||
|
|
||||||
|
return $"https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={result.Ticket}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过code获取用户基础信息接口(为了获取用户access_token和openid)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code">授权码</param>
|
||||||
|
/// <returns>用户基础信息响应对象</returns>
|
||||||
|
private async Task<UserBaseInfoResponse> GetBaseUserInfoByCodeAsync(string code)
|
||||||
|
{
|
||||||
|
var url = $"https://api.weixin.qq.com/sns/oauth2/access_token?appid={_options.AppId}&secret={_options.Secret}&code={code}&grant_type=authorization_code";
|
||||||
|
|
||||||
|
var response = await _httpClientFactory.CreateClient().GetAsync(url);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var jsonContent = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonSerializer.Deserialize<UserBaseInfoResponse>(jsonContent, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过code获取用户信息接口
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="code">授权码</param>
|
||||||
|
/// <returns>用户信息响应对象</returns>
|
||||||
|
public async Task<UserInfoResponse> GetUserInfoByCodeAsync(string code)
|
||||||
|
{
|
||||||
|
var baseUserInfo = await GetBaseUserInfoByCodeAsync(code);
|
||||||
|
var url = $"https://api.weixin.qq.com/sns/userinfo?access_token={baseUserInfo.AccessToken}&openid={baseUserInfo.OpenId}&lang=zh_CN";
|
||||||
|
|
||||||
|
var response = await _httpClientFactory.CreateClient().GetAsync(url);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var jsonContent = await response.Content.ReadAsStringAsync();
|
||||||
|
var result = JsonSerializer.Deserialize<UserInfoResponse>(jsonContent, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 处理回调逻辑
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sceneType"></param>
|
||||||
|
/// <param name="openId"></param>
|
||||||
|
/// <param name="bindUserId"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<SceneResultEnum> CallBackHandlerAsync(SceneTypeEnum sceneType, string openId, Guid? bindUserId)
|
||||||
|
{
|
||||||
|
var aiUserInfo = await _userRepository._DbQueryable.Where(x => x.FuwuhaoOpenId == openId).FirstAsync();
|
||||||
|
switch (sceneType)
|
||||||
|
{
|
||||||
|
case SceneTypeEnum.LoginOrRegister:
|
||||||
|
//有openid,说明登录成功
|
||||||
|
if (aiUserInfo is not null)
|
||||||
|
{
|
||||||
|
return SceneResultEnum.Login;
|
||||||
|
}
|
||||||
|
//无openid,说明需要进行注册
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return SceneResultEnum.Register;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
case SceneTypeEnum.Bind:
|
||||||
|
//说明已经有微信号,直接换绑
|
||||||
|
|
||||||
|
if (aiUserInfo is not null)
|
||||||
|
{
|
||||||
|
await _userRepository.DeleteByIdAsync(aiUserInfo.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bindUserId is null)
|
||||||
|
{
|
||||||
|
throw new UserFriendlyException("绑定用户,需要传入绑定的用户id");
|
||||||
|
}
|
||||||
|
|
||||||
|
//说明没有绑定过,直接绑定
|
||||||
|
await _userRepository.InsertAsync(new AiUserExtraInfoEntity(bindUserId.Value, openId));
|
||||||
|
return SceneResultEnum.Bind;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(sceneType), sceneType, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
|
||||||
|
|
||||||
|
public class FuwuhaoOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 微信公众号AppId
|
||||||
|
/// </summary>
|
||||||
|
public string AppId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信公众号AppSecret
|
||||||
|
/// </summary>
|
||||||
|
public string Secret { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 二维码响应对象
|
||||||
|
/// </summary>
|
||||||
|
public class QrCodeResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 二维码票据
|
||||||
|
/// </summary>
|
||||||
|
public string Ticket { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 过期时间(秒)
|
||||||
|
/// </summary>
|
||||||
|
public int ExpireSeconds { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 二维码URL
|
||||||
|
/// </summary>
|
||||||
|
public string Url { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户基础信息响应对象
|
||||||
|
/// </summary>
|
||||||
|
public class UserBaseInfoResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 访问令牌
|
||||||
|
/// </summary>
|
||||||
|
public string AccessToken { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 过期时间(秒)
|
||||||
|
/// </summary>
|
||||||
|
public int ExpiresIn { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 刷新令牌
|
||||||
|
/// </summary>
|
||||||
|
public string RefreshToken { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户OpenID
|
||||||
|
/// </summary>
|
||||||
|
public string OpenId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 授权作用域
|
||||||
|
/// </summary>
|
||||||
|
public string Scope { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否为快照用户
|
||||||
|
/// </summary>
|
||||||
|
public int IsSnapshotUser { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户UnionID
|
||||||
|
/// </summary>
|
||||||
|
public string UnionId { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户信息响应对象
|
||||||
|
/// </summary>
|
||||||
|
public class UserInfoResponse
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 用户OpenID
|
||||||
|
/// </summary>
|
||||||
|
public string OpenId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户昵称
|
||||||
|
/// </summary>
|
||||||
|
public string Nickname { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户性别,值为1时是男性,值为2时是女性,值为0时是未知
|
||||||
|
/// </summary>
|
||||||
|
public int Sex { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户个人资料填写的省份
|
||||||
|
/// </summary>
|
||||||
|
public string Province { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 普通用户个人资料填写的城市
|
||||||
|
/// </summary>
|
||||||
|
public string City { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 国家,如中国为CN
|
||||||
|
/// </summary>
|
||||||
|
public string Country { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像)
|
||||||
|
/// </summary>
|
||||||
|
public string HeadImgUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户特权信息
|
||||||
|
/// </summary>
|
||||||
|
public string[] Privilege { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用户UnionID
|
||||||
|
/// </summary>
|
||||||
|
public string UnionId { get; set; }
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Chats;
|
|||||||
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Images;
|
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorAzureOpenAI.Images;
|
||||||
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorDeepSeek.Chats;
|
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorDeepSeek.Chats;
|
||||||
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorSiliconFlow.Embeddings;
|
using Yi.Framework.AiHub.Domain.AiGateWay.Impl.ThorSiliconFlow.Embeddings;
|
||||||
|
using Yi.Framework.AiHub.Domain.Managers.Fuwuhao;
|
||||||
using Yi.Framework.AiHub.Domain.Shared;
|
using Yi.Framework.AiHub.Domain.Shared;
|
||||||
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
using Yi.Framework.AiHub.Domain.Shared.Dtos.OpenAi;
|
||||||
using Yi.Framework.Mapster;
|
using Yi.Framework.Mapster;
|
||||||
@@ -79,6 +80,9 @@ namespace Yi.Framework.AiHub.Domain
|
|||||||
//配置支付宝支付
|
//配置支付宝支付
|
||||||
var config = configuration.GetSection("Alipay").Get<Config>();
|
var config = configuration.GetSection("Alipay").Get<Config>();
|
||||||
Factory.SetOptions(config);
|
Factory.SetOptions(config);
|
||||||
|
|
||||||
|
//配置服务号
|
||||||
|
Configure<FuwuhaoOptions>(configuration.GetSection("Fuwuhao"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
|
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ public class WeChatMiniProgramAccountService : ApplicationService
|
|||||||
//走普通注册流程
|
//走普通注册流程
|
||||||
//同时再加一个小程序绑定即可
|
//同时再加一个小程序绑定即可
|
||||||
var userName = GenerateRandomString(6);
|
var userName = GenerateRandomString(6);
|
||||||
await _accountService.PostTempRegisterAsync(new RegisterDto
|
await _accountService.PostSystemRegisterAsync(new RegisterDto
|
||||||
{
|
{
|
||||||
UserName = $"ls_{userName}",
|
UserName = $"ls_{userName}",
|
||||||
Password = GenerateRandomString(20),
|
Password = GenerateRandomString(20),
|
||||||
|
|||||||
@@ -37,5 +37,10 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Nick{ get; set; }
|
public string? Nick{ get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 头像
|
||||||
|
/// </summary>
|
||||||
|
public string? Icon { get; set; }
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,6 @@ namespace Yi.Framework.Rbac.Application.Contracts.IServices
|
|||||||
/// 不需要验证,为了给第三方使用,例如微信小程序,后续可通过绑定操作,进行账号合并
|
/// 不需要验证,为了给第三方使用,例如微信小程序,后续可通过绑定操作,进行账号合并
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input"></param>
|
/// <param name="input"></param>
|
||||||
Task PostTempRegisterAsync(RegisterDto input);
|
Task<Guid> PostSystemRegisterAsync(RegisterDto input);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -246,20 +246,21 @@ namespace Yi.Framework.Rbac.Application.Services
|
|||||||
|
|
||||||
//注册之后,免再次登录,直接给前端token
|
//注册之后,免再次登录,直接给前端token
|
||||||
var userId = await _accountManager.RegisterAsync(input.UserName, input.Password, input.Phone, input.Email,
|
var userId = await _accountManager.RegisterAsync(input.UserName, input.Password, input.Phone, input.Email,
|
||||||
input.Nick);
|
input.Nick, null);
|
||||||
return await this.PostLoginAsync(userId);
|
return await this.PostLoginAsync(userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 临时注册
|
/// 系统直接注册用户
|
||||||
/// 不需要验证,为了给第三方使用,例如微信小程序,后续可通过绑定操作,进行账号合并
|
/// 不需要验证,为了给第三方使用,例如微信小程序,后续可通过绑定操作,进行账号合并
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="input"></param>
|
/// <param name="input"></param>
|
||||||
[RemoteService(isEnabled: false)]
|
[RemoteService(isEnabled: false)]
|
||||||
public async Task PostTempRegisterAsync(RegisterDto input)
|
public async Task<Guid> PostSystemRegisterAsync(RegisterDto input)
|
||||||
{
|
{
|
||||||
//注册领域逻辑
|
//注册领域逻辑
|
||||||
await _accountManager.RegisterAsync(input.UserName, input.Password, input.Phone, input.Email, input.Nick);
|
return await _accountManager.RegisterAsync(input.UserName, input.Password, input.Phone, input.Email,
|
||||||
|
input.Nick, input.Icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -19,12 +19,14 @@ namespace Yi.Framework.Rbac.Domain.Entities
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserAggregateRoot(string userName, string password, long? phone, string? email, string? nick = null)
|
public UserAggregateRoot(string userName, string password, long? phone, string? email, string? nick = null,
|
||||||
|
string? icon = null)
|
||||||
{
|
{
|
||||||
UserName = userName;
|
UserName = userName;
|
||||||
EncryPassword.Password = password;
|
EncryPassword.Password = password;
|
||||||
Phone = phone;
|
Phone = phone;
|
||||||
Email = email;
|
Email = email;
|
||||||
|
Icon = icon;
|
||||||
Nick = string.IsNullOrWhiteSpace(nick) ? "萌新-" + userName : nick.Trim();
|
Nick = string.IsNullOrWhiteSpace(nick) ? "萌新-" + userName : nick.Trim();
|
||||||
BuildPassword();
|
BuildPassword();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -285,9 +285,10 @@ namespace Yi.Framework.Rbac.Domain.Managers
|
|||||||
/// <param name="phone"></param>
|
/// <param name="phone"></param>
|
||||||
/// <param name="email"></param>
|
/// <param name="email"></param>
|
||||||
/// <param name="nick"></param>
|
/// <param name="nick"></param>
|
||||||
|
/// <param name="icon"></param>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task<Guid> RegisterAsync(string userName, string password, long? phone, string? email,
|
public async Task<Guid> RegisterAsync(string userName, string password, long? phone, string? email,
|
||||||
string? nick)
|
string? nick,string? icon)
|
||||||
{
|
{
|
||||||
if (phone is null && string.IsNullOrWhiteSpace(email))
|
if (phone is null && string.IsNullOrWhiteSpace(email))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ namespace Yi.Framework.Rbac.Domain.Managers
|
|||||||
string CreateRefreshToken(Guid userId);
|
string CreateRefreshToken(Guid userId);
|
||||||
Task<string> GetTokenByUserIdAsync(Guid userId,Action<UserRoleMenuDto>? getUserInfo=null);
|
Task<string> GetTokenByUserIdAsync(Guid userId,Action<UserRoleMenuDto>? getUserInfo=null);
|
||||||
Task LoginValidationAsync(string userName, string password, Action<UserAggregateRoot>? userAction = null);
|
Task LoginValidationAsync(string userName, string password, Action<UserAggregateRoot>? userAction = null);
|
||||||
Task<Guid> RegisterAsync(string userName, string password, long? phone, string? email, string? nick);
|
Task<Guid> RegisterAsync(string userName, string password, long? phone, string? email, string? nick,
|
||||||
|
string? icon);
|
||||||
Task<bool> RestPasswordAsync(Guid userId, string password);
|
Task<bool> RestPasswordAsync(Guid userId, string password);
|
||||||
Task UpdatePasswordAsync(Guid userId, string newPassword, string oldPassword);
|
Task UpdatePasswordAsync(Guid userId, string newPassword, string oldPassword);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user