feat: 完成支持微信扫码功能

This commit is contained in:
ccnetcore
2025-08-27 23:42:46 +08:00
parent 28fcd6c9ce
commit b768bca638
21 changed files with 618 additions and 17 deletions

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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);
}
}
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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.ThorDeepSeek.Chats;
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.Dtos.OpenAi;
using Yi.Framework.Mapster;
@@ -79,6 +80,9 @@ namespace Yi.Framework.AiHub.Domain
//配置支付宝支付
var config = configuration.GetSection("Alipay").Get<Config>();
Factory.SetOptions(config);
//配置服务号
Configure<FuwuhaoOptions>(configuration.GetSection("Fuwuhao"));
}
public override async Task OnApplicationInitializationAsync(ApplicationInitializationContext context)