feat(wechat): 完善扫码登录缓存账号信息与轮询状态接口
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
bf81f00c91
commit
02cb06b86c
@ -26,6 +26,7 @@ public class WeChatRouteIntegrationTests
|
|||||||
host.RoutePatterns.Should().Contain("wechat/oauth/user-info");
|
host.RoutePatterns.Should().Contain("wechat/oauth/user-info");
|
||||||
host.RoutePatterns.Should().Contain("wechat/customer/register");
|
host.RoutePatterns.Should().Contain("wechat/customer/register");
|
||||||
host.RoutePatterns.Should().Contain("wechat/customer/bind");
|
host.RoutePatterns.Should().Contain("wechat/customer/bind");
|
||||||
|
host.RoutePatterns.Should().Contain("rbac/auth/wx-login/status");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|||||||
@ -0,0 +1,107 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using FluentAssertions;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
using Moq;
|
||||||
|
using Oms.Domain.CustomersAggregate.Entities;
|
||||||
|
using Oms.Domain.CustomersAggregate.Enums;
|
||||||
|
using Oms.Domain.CustomersAggregate.Repositories;
|
||||||
|
using Oms.Domain.SystemAggregate.Consts;
|
||||||
|
using Oms.Domain.SystemAggregate.Entities;
|
||||||
|
using Oms.Domain.SystemAggregate.Repositories;
|
||||||
|
using SimpleAdmin.Api.IntegrationTests.Support;
|
||||||
|
using SimpleAdmin.Api.WeChat.Options;
|
||||||
|
|
||||||
|
namespace SimpleAdmin.Api.IntegrationTests;
|
||||||
|
|
||||||
|
public class WeChatScanLoginIntegrationTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task ScanEvent_ShouldWriteSession_AndStatusReturnsReady()
|
||||||
|
{
|
||||||
|
const string loginKey = "abc123scankey";
|
||||||
|
const string openId = "o-test-openid";
|
||||||
|
var customer = new Customer
|
||||||
|
{
|
||||||
|
Id = 42,
|
||||||
|
CustomerCode = "C001",
|
||||||
|
UserId = 100,
|
||||||
|
WechatOpenid = openId,
|
||||||
|
Status = CustomerStatus.Normal
|
||||||
|
};
|
||||||
|
|
||||||
|
var repository = new Mock<ICustomerRepository>(MockBehavior.Strict);
|
||||||
|
repository
|
||||||
|
.Setup(x => x.GetByWechatOpenidAsync(openId, It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync(customer);
|
||||||
|
|
||||||
|
await using var host = await SimpleAdminApiTestHost.CreateAsync(configureServices: services =>
|
||||||
|
{
|
||||||
|
ConfigureWeChatFallback(services);
|
||||||
|
services.RemoveAll<ICustomerRepository>();
|
||||||
|
services.AddSingleton(repository.Object);
|
||||||
|
});
|
||||||
|
|
||||||
|
const string timestamp = "1400000000";
|
||||||
|
const string nonce = "nonce";
|
||||||
|
var signature = SignWeChat("test-token", timestamp, nonce);
|
||||||
|
|
||||||
|
var scanXml = $"""
|
||||||
|
<xml>
|
||||||
|
<ToUserName><![CDATA[gh_test]]></ToUserName>
|
||||||
|
<FromUserName><![CDATA[{openId}]]></FromUserName>
|
||||||
|
<CreateTime>1400000000</CreateTime>
|
||||||
|
<MsgType><![CDATA[event]]></MsgType>
|
||||||
|
<Event><![CDATA[SCAN]]></Event>
|
||||||
|
<EventKey><![CDATA[{loginKey}]]></EventKey>
|
||||||
|
</xml>
|
||||||
|
""";
|
||||||
|
|
||||||
|
using var postContent = new StringContent(scanXml, Encoding.UTF8, "text/xml");
|
||||||
|
var postResponse = await host.Client.PostAsync(
|
||||||
|
$"/apiwechat/verification?signature={signature}×tamp={timestamp}&nonce={nonce}",
|
||||||
|
postContent);
|
||||||
|
|
||||||
|
postResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||||
|
|
||||||
|
var statusResponse = await host.Client.GetAsync(
|
||||||
|
$"/rbac/auth/wx-login/status?loginUuid={loginKey}");
|
||||||
|
|
||||||
|
statusResponse.StatusCode.Should().Be(HttpStatusCode.OK);
|
||||||
|
var statusJson = await statusResponse.Content.ReadAsStringAsync();
|
||||||
|
statusJson.Should().Contain("\"ready\":true");
|
||||||
|
statusJson.Should().Contain("C001");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task WeChatRoutes_ShouldIncludeWxLoginStatus()
|
||||||
|
{
|
||||||
|
await using var host = await SimpleAdminApiTestHost.CreateAsync(configureServices: ConfigureWeChatFallback);
|
||||||
|
host.RoutePatterns.Should().Contain("rbac/auth/wx-login/status");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureWeChatFallback(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.PostConfigure<WeChatMpOptions>(options =>
|
||||||
|
{
|
||||||
|
options.Token = "test-token";
|
||||||
|
options.AppId = "wx-test";
|
||||||
|
});
|
||||||
|
|
||||||
|
var configRepository = new Mock<ISystemOtherConfigRepository>();
|
||||||
|
configRepository
|
||||||
|
.Setup(x => x.GetByConfigCodeAsync(SystemOtherConfigCodes.WeChatMp, It.IsAny<CancellationToken>()))
|
||||||
|
.ReturnsAsync((SystemOtherConfig?)null);
|
||||||
|
services.RemoveAll<ISystemOtherConfigRepository>();
|
||||||
|
services.AddSingleton(configRepository.Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SignWeChat(string token, string timestamp, string nonce)
|
||||||
|
{
|
||||||
|
var parts = new[] { token, timestamp, nonce }.OrderBy(x => x, StringComparer.Ordinal).ToArray();
|
||||||
|
var hash = SHA1.HashData(Encoding.UTF8.GetBytes(string.Concat(parts)));
|
||||||
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -156,21 +156,21 @@ public partial class AuthService
|
|||||||
[HttpGet("/wechat/mp/login-qrcode")]
|
[HttpGet("/wechat/mp/login-qrcode")]
|
||||||
[ApiExplorerSettings(GroupName = "WeChat")]
|
[ApiExplorerSettings(GroupName = "WeChat")]
|
||||||
public async Task<WxLoginQrcodeOutput> GetWeChatLoginQrcodeAsync(
|
public async Task<WxLoginQrcodeOutput> GetWeChatLoginQrcodeAsync(
|
||||||
[FromQuery] string loginUuid,
|
[FromQuery] string? loginUuid,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(loginUuid))
|
var loginKey = string.IsNullOrWhiteSpace(loginUuid)
|
||||||
{
|
? Guid.NewGuid().ToString("N")
|
||||||
throw new BusinessException("缺少 loginUuid");
|
: loginUuid.Trim();
|
||||||
}
|
|
||||||
|
|
||||||
const int expireSeconds = 600;
|
const int expireSeconds = 600;
|
||||||
var qrCodeUrl = await WeChatMpApiClient.CreateLoginQrCodeUrlAsync(
|
var qrCodeUrl = await WeChatMpApiClient.CreateLoginQrCodeUrlAsync(
|
||||||
loginUuid.Trim(),
|
loginKey,
|
||||||
expireSeconds,
|
expireSeconds,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
return new WxLoginQrcodeOutput
|
return new WxLoginQrcodeOutput
|
||||||
{
|
{
|
||||||
|
LoginKey = loginKey,
|
||||||
QrCodeUrl = qrCodeUrl,
|
QrCodeUrl = qrCodeUrl,
|
||||||
ExpiresIn = expireSeconds
|
ExpiresIn = expireSeconds
|
||||||
};
|
};
|
||||||
|
|||||||
@ -18,6 +18,7 @@ using Oms.Application.Shared;
|
|||||||
using Oms.Domain.CustomersAggregate.Entities;
|
using Oms.Domain.CustomersAggregate.Entities;
|
||||||
using Oms.Domain.CustomersAggregate.Enums;
|
using Oms.Domain.CustomersAggregate.Enums;
|
||||||
using Oms.Domain.CustomersAggregate.Repositories;
|
using Oms.Domain.CustomersAggregate.Repositories;
|
||||||
|
using SimpleAdmin.Api.WeChat.Dtos;
|
||||||
using SimpleAdmin.Api.WeChat.Services;
|
using SimpleAdmin.Api.WeChat.Services;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.Processing;
|
using SixLabors.ImageSharp.Processing;
|
||||||
@ -61,6 +62,33 @@ public partial class AuthService(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 微信扫码登录轮询状态(key 是否已有绑定账号)。
|
||||||
|
/// </summary>
|
||||||
|
[AllowAnonymous]
|
||||||
|
[HttpGet("/rbac/auth/wx-login/status")]
|
||||||
|
public async Task<WxLoginStatusOutput> WxLoginStatusAsync(
|
||||||
|
[FromQuery] string loginUuid,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(loginUuid))
|
||||||
|
{
|
||||||
|
throw new BusinessException("缺少 loginUuid");
|
||||||
|
}
|
||||||
|
|
||||||
|
var session = await weChatLoginSessionStore.GetSessionAsync(loginUuid.Trim(), cancellationToken);
|
||||||
|
if (session == null)
|
||||||
|
{
|
||||||
|
return new WxLoginStatusOutput { Ready = false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WxLoginStatusOutput
|
||||||
|
{
|
||||||
|
Ready = true,
|
||||||
|
Account = string.IsNullOrWhiteSpace(session.CustomerCode) ? session.OpenId : session.CustomerCode
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 微信扫码登录(轮询)。
|
/// 微信扫码登录(轮询)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -75,14 +103,14 @@ public partial class AuthService(
|
|||||||
throw new BusinessException("缺少 loginUuid");
|
throw new BusinessException("缺少 loginUuid");
|
||||||
}
|
}
|
||||||
|
|
||||||
var loginUuid = input.LoginUuid.Trim();
|
var loginKey = input.LoginUuid.Trim();
|
||||||
var openId = await weChatLoginSessionStore.GetOpenIdAsync(loginUuid, cancellationToken);
|
var session = await weChatLoginSessionStore.GetSessionAsync(loginKey, cancellationToken);
|
||||||
if (string.IsNullOrWhiteSpace(openId))
|
if (session == null)
|
||||||
{
|
{
|
||||||
throw new BusinessException("请扫码登录");
|
throw new BusinessException("请扫码登录");
|
||||||
}
|
}
|
||||||
|
|
||||||
var customer = await customerRepository.GetByWechatOpenidAsync(openId, cancellationToken);
|
var customer = await ResolveCustomerFromWxSessionAsync(session, cancellationToken);
|
||||||
if (customer == null)
|
if (customer == null)
|
||||||
{
|
{
|
||||||
throw new BusinessException("未绑定微信,请先注册或绑定账号");
|
throw new BusinessException("未绑定微信,请先注册或绑定账号");
|
||||||
@ -105,12 +133,31 @@ public partial class AuthService(
|
|||||||
throw new BusinessException("客户未关联用户");
|
throw new BusinessException("客户未关联用户");
|
||||||
}
|
}
|
||||||
|
|
||||||
await weChatLoginSessionStore.RemoveAsync(loginUuid, cancellationToken);
|
await weChatLoginSessionStore.RemoveAsync(loginKey, cancellationToken);
|
||||||
|
|
||||||
var claims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken);
|
var claims = await BuildClaimsAsync(userGuid.Value, OmsLoginType.Customer, cancellationToken);
|
||||||
return BuildLoginResult(userGuid.Value, claims);
|
return BuildLoginResult(userGuid.Value, claims);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<Customer?> ResolveCustomerFromWxSessionAsync(
|
||||||
|
WeChatLoginSessionPayload session,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (session.CustomerId > 0)
|
||||||
|
{
|
||||||
|
return await customerRepository.FirstOrDefaultAsync(
|
||||||
|
x => x.Id == session.CustomerId,
|
||||||
|
cancellationToken: cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(session.OpenId))
|
||||||
|
{
|
||||||
|
return await customerRepository.GetByWechatOpenidAsync(session.OpenId, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 刷新登录令牌。
|
/// 刷新登录令牌。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -4,11 +4,21 @@ namespace SimpleAdmin.Api.WeChat.Dtos;
|
|||||||
|
|
||||||
public class WxLoginQrcodeOutput
|
public class WxLoginQrcodeOutput
|
||||||
{
|
{
|
||||||
|
/// <summary>PC 端轮询用的扫码 key(与二维码 scene 一致)。</summary>
|
||||||
|
public string LoginKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
public string QrCodeUrl { get; set; } = string.Empty;
|
public string QrCodeUrl { get; set; } = string.Empty;
|
||||||
|
|
||||||
public int ExpiresIn { get; set; } = 600;
|
public int ExpiresIn { get; set; } = 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class WxLoginStatusOutput
|
||||||
|
{
|
||||||
|
public bool Ready { get; set; }
|
||||||
|
|
||||||
|
public string? Account { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
public class WeChatOAuthCodeInput
|
public class WeChatOAuthCodeInput
|
||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
|
|||||||
@ -1,13 +1,19 @@
|
|||||||
namespace SimpleAdmin.Api.WeChat.Services;
|
namespace SimpleAdmin.Api.WeChat.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 微信扫码登录会话:loginUuid → openId 桥接。
|
/// 微信扫码登录会话:loginUuid(key)→ 已绑定账号信息,TTL 默认 10 分钟。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IWeChatLoginSessionStore
|
public interface IWeChatLoginSessionStore
|
||||||
{
|
{
|
||||||
Task SetOpenIdAsync(string loginUuid, string openId, TimeSpan ttl, CancellationToken cancellationToken = default);
|
Task SetSessionAsync(
|
||||||
|
string loginKey,
|
||||||
|
WeChatLoginSessionPayload payload,
|
||||||
|
TimeSpan ttl,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task<string?> GetOpenIdAsync(string loginUuid, CancellationToken cancellationToken = default);
|
Task<WeChatLoginSessionPayload?> GetSessionAsync(
|
||||||
|
string loginKey,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
Task RemoveAsync(string loginUuid, CancellationToken cancellationToken = default);
|
Task RemoveAsync(string loginKey, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,8 +12,6 @@ public class WeChatEventHandler(
|
|||||||
IWeChatLoginSessionStore sessionStore,
|
IWeChatLoginSessionStore sessionStore,
|
||||||
ILogger<WeChatEventHandler> logger) : IWeChatEventHandler
|
ILogger<WeChatEventHandler> logger) : IWeChatEventHandler
|
||||||
{
|
{
|
||||||
private const string QrScenePrefix = "qrscene_";
|
|
||||||
|
|
||||||
public async Task<string?> HandleAsync(WeChatIncomingMessage message, CancellationToken cancellationToken = default)
|
public async Task<string?> HandleAsync(WeChatIncomingMessage message, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (!string.Equals(message.MsgType, "event", StringComparison.OrdinalIgnoreCase))
|
if (!string.Equals(message.MsgType, "event", StringComparison.OrdinalIgnoreCase))
|
||||||
@ -40,14 +38,8 @@ public class WeChatEventHandler(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var existing = await customerRepository.GetByWechatOpenidAsync(openId, cancellationToken);
|
var loginKey = WeChatQrSceneParser.ExtractLoginKey(message.EventKey);
|
||||||
if (existing == null)
|
if (string.IsNullOrWhiteSpace(loginKey))
|
||||||
{
|
|
||||||
return BuildTextReply(message, "未绑定账号,请在公众号菜单注册账号或绑定已有账号。");
|
|
||||||
}
|
|
||||||
|
|
||||||
var scene = NormalizeScene(message.EventKey);
|
|
||||||
if (string.IsNullOrWhiteSpace(scene))
|
|
||||||
{
|
{
|
||||||
if (string.Equals(message.Event, "subscribe", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(message.Event, "subscribe", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@ -57,8 +49,26 @@ public class WeChatEventHandler(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
await sessionStore.SetOpenIdAsync(scene, openId, TimeSpan.FromMinutes(10), cancellationToken);
|
var customer = await customerRepository.GetByWechatOpenidAsync(openId, cancellationToken);
|
||||||
logger.LogInformation("微信扫码登录会话已写入: loginUuid={LoginUuid}, openId={OpenId}", scene, openId);
|
if (customer == null)
|
||||||
|
{
|
||||||
|
return BuildTextReply(message, "未绑定账号,请在公众号菜单注册账号或绑定已有账号。");
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload = new WeChatLoginSessionPayload
|
||||||
|
{
|
||||||
|
CustomerId = customer.Id,
|
||||||
|
CustomerCode = customer.CustomerCode,
|
||||||
|
UserId = customer.UserId,
|
||||||
|
OpenId = openId
|
||||||
|
};
|
||||||
|
|
||||||
|
await sessionStore.SetSessionAsync(loginKey, payload, TimeSpan.FromMinutes(10), cancellationToken);
|
||||||
|
logger.LogInformation(
|
||||||
|
"微信扫码登录会话已写入: loginKey={LoginKey}, customerCode={CustomerCode}, openId={OpenId}",
|
||||||
|
loginKey,
|
||||||
|
customer.CustomerCode,
|
||||||
|
openId);
|
||||||
|
|
||||||
if (string.Equals(message.Event, "subscribe", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(message.Event, "subscribe", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@ -68,18 +78,6 @@ public class WeChatEventHandler(
|
|||||||
return BuildTextReply(message, "扫码成功,请返回电脑端完成登录。");
|
return BuildTextReply(message, "扫码成功,请返回电脑端完成登录。");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? NormalizeScene(string? eventKey)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(eventKey))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return eventKey.StartsWith(QrScenePrefix, StringComparison.Ordinal)
|
|
||||||
? eventKey[QrScenePrefix.Length..]
|
|
||||||
: eventKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildTextReply(WeChatIncomingMessage message, string content)
|
private static string BuildTextReply(WeChatIncomingMessage message, string content)
|
||||||
{
|
{
|
||||||
return WeChatXmlMessageBuilder.BuildTextReply(
|
return WeChatXmlMessageBuilder.BuildTextReply(
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
namespace SimpleAdmin.Api.WeChat.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 扫码成功后写入缓存的会话数据(key = loginUuid)。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class WeChatLoginSessionPayload
|
||||||
|
{
|
||||||
|
public int CustomerId { get; init; }
|
||||||
|
|
||||||
|
public string CustomerCode { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
public int UserId { get; init; }
|
||||||
|
|
||||||
|
public string OpenId { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
using System.Text.Json;
|
||||||
using Microsoft.Extensions.Caching.Distributed;
|
using Microsoft.Extensions.Caching.Distributed;
|
||||||
|
|
||||||
namespace SimpleAdmin.Api.WeChat.Services;
|
namespace SimpleAdmin.Api.WeChat.Services;
|
||||||
@ -6,24 +7,48 @@ public class WeChatLoginSessionStore(IDistributedCache cache) : IWeChatLoginSess
|
|||||||
{
|
{
|
||||||
private const string KeyPrefix = "oms:wechat:login:";
|
private const string KeyPrefix = "oms:wechat:login:";
|
||||||
|
|
||||||
public Task SetOpenIdAsync(string loginUuid, string openId, TimeSpan ttl, CancellationToken cancellationToken = default)
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
|
||||||
|
public Task SetSessionAsync(
|
||||||
|
string loginKey,
|
||||||
|
WeChatLoginSessionPayload payload,
|
||||||
|
TimeSpan ttl,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var options = new DistributedCacheEntryOptions
|
var options = new DistributedCacheEntryOptions
|
||||||
{
|
{
|
||||||
AbsoluteExpirationRelativeToNow = ttl
|
AbsoluteExpirationRelativeToNow = ttl
|
||||||
};
|
};
|
||||||
return cache.SetStringAsync(BuildKey(loginUuid), openId, options, cancellationToken);
|
var json = JsonSerializer.Serialize(payload, JsonOptions);
|
||||||
|
return cache.SetStringAsync(BuildKey(loginKey), json, options, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<string?> GetOpenIdAsync(string loginUuid, CancellationToken cancellationToken = default)
|
public async Task<WeChatLoginSessionPayload?> GetSessionAsync(
|
||||||
|
string loginKey,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return cache.GetStringAsync(BuildKey(loginUuid), cancellationToken);
|
var raw = await cache.GetStringAsync(BuildKey(loginKey), cancellationToken);
|
||||||
}
|
if (string.IsNullOrWhiteSpace(raw))
|
||||||
|
|
||||||
public Task RemoveAsync(string loginUuid, CancellationToken cancellationToken = default)
|
|
||||||
{
|
{
|
||||||
return cache.RemoveAsync(BuildKey(loginUuid), cancellationToken);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildKey(string loginUuid) => $"{KeyPrefix}{loginUuid}";
|
if (raw.StartsWith('{'))
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<WeChatLoginSessionPayload>(raw, JsonOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容旧版仅缓存 openId 的会话
|
||||||
|
return new WeChatLoginSessionPayload { OpenId = raw };
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task RemoveAsync(string loginKey, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return cache.RemoveAsync(BuildKey(loginKey), cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildKey(string loginKey) => $"{KeyPrefix}{loginKey}";
|
||||||
}
|
}
|
||||||
|
|||||||
57
src/SimpleAdmin.Api/WeChat/Services/WeChatQrSceneParser.cs
Normal file
57
src/SimpleAdmin.Api/WeChat/Services/WeChatQrSceneParser.cs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace SimpleAdmin.Api.WeChat.Services;
|
||||||
|
|
||||||
|
internal static class WeChatQrSceneParser
|
||||||
|
{
|
||||||
|
private const string QrScenePrefix = "qrscene_";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从微信 SCAN / SUBSCRIBE 事件的 EventKey 解析 PC 端轮询用的 loginUuid(key)。
|
||||||
|
/// </summary>
|
||||||
|
public static string? ExtractLoginKey(string? eventKey)
|
||||||
|
{
|
||||||
|
var scene = NormalizeScene(eventKey);
|
||||||
|
if (string.IsNullOrWhiteSpace(scene))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scene.StartsWith('{') && scene.EndsWith('}'))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = JsonDocument.Parse(scene);
|
||||||
|
var root = doc.RootElement;
|
||||||
|
if (root.TryGetProperty("loginName", out var loginName)
|
||||||
|
&& loginName.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return loginName.GetString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (root.TryGetProperty("key", out var key) && key.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
return key.GetString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeScene(string? eventKey)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(eventKey))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventKey.StartsWith(QrScenePrefix, StringComparison.Ordinal)
|
||||||
|
? eventKey[QrScenePrefix.Length..]
|
||||||
|
: eventKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user