Compare commits
7
Commits
b49e3b30b3
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79fed6cf5d | ||
|
|
5cdc7052e0 | ||
|
|
e72bff954b | ||
|
|
d19de41272 | ||
|
|
1ab9a90831 | ||
|
|
fc6f9f6bc3 | ||
|
|
446502b0e9 |
@@ -3,7 +3,7 @@
|
|||||||
"isRoot": true,
|
"isRoot": true,
|
||||||
"tools": {
|
"tools": {
|
||||||
"dotnet-ef": {
|
"dotnet-ef": {
|
||||||
"version": "10.0.0",
|
"version": "10.0.7",
|
||||||
"commands": [
|
"commands": [
|
||||||
"dotnet-ef"
|
"dotnet-ef"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -26,3 +26,9 @@
|
|||||||
/Avalonia-API/avalonia-api.db
|
/Avalonia-API/avalonia-api.db
|
||||||
/Avalonia-API/avalonia-api.db-shm
|
/Avalonia-API/avalonia-api.db-shm
|
||||||
/Avalonia-API/avalonia-api.db-wal
|
/Avalonia-API/avalonia-api.db-wal
|
||||||
|
/package-output
|
||||||
|
/package-scripts/tools
|
||||||
|
/.vs
|
||||||
|
/.template-hive
|
||||||
|
/bin
|
||||||
|
/obj
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json.schemastore.org/template",
|
||||||
|
"author": "Qiang",
|
||||||
|
"classifications": [
|
||||||
|
"Avalonia",
|
||||||
|
"ASP.NET Core",
|
||||||
|
"Desktop",
|
||||||
|
"Template"
|
||||||
|
],
|
||||||
|
"identity": "Qiang.Avalonia.Stack",
|
||||||
|
"name": "Avalonia Stack",
|
||||||
|
"shortName": "Qiang-avalonia-stack",
|
||||||
|
"preferNameDirectory": true,
|
||||||
|
"forms": {
|
||||||
|
"appendHyphen": {
|
||||||
|
"identifier": "replace",
|
||||||
|
"pattern": "$",
|
||||||
|
"replacement": "-"
|
||||||
|
},
|
||||||
|
"keepOriginalCase": {
|
||||||
|
"identifier": "replace",
|
||||||
|
"pattern": "$",
|
||||||
|
"replacement": ""
|
||||||
|
},
|
||||||
|
"appendUnderscore": {
|
||||||
|
"identifier": "replace",
|
||||||
|
"pattern": "$",
|
||||||
|
"replacement": "_"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"symbols": {
|
||||||
|
"projectFileName": {
|
||||||
|
"type": "derived",
|
||||||
|
"valueSource": "name",
|
||||||
|
"valueTransform": "keepOriginalCase",
|
||||||
|
"fileRename": "Avalonia"
|
||||||
|
},
|
||||||
|
"projectPrefix": {
|
||||||
|
"type": "derived",
|
||||||
|
"valueSource": "name",
|
||||||
|
"valueTransform": "appendHyphen",
|
||||||
|
"replaces": "Avalonia-"
|
||||||
|
},
|
||||||
|
"namespacePrefix": {
|
||||||
|
"type": "derived",
|
||||||
|
"valueSource": "name",
|
||||||
|
"valueTransform": "appendUnderscore",
|
||||||
|
"replaces": "Avalonia_"
|
||||||
|
},
|
||||||
|
"lowerCaseProjectName": {
|
||||||
|
"type": "generated",
|
||||||
|
"generator": "casing",
|
||||||
|
"parameters": {
|
||||||
|
"source": "name",
|
||||||
|
"toLower": true
|
||||||
|
},
|
||||||
|
"fileRename": "avalonia"
|
||||||
|
},
|
||||||
|
"lowerCaseProjectPrefix": {
|
||||||
|
"type": "derived",
|
||||||
|
"valueSource": "lowerCaseProjectName",
|
||||||
|
"valueTransform": "appendHyphen",
|
||||||
|
"replaces": "avalonia-"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sources": [
|
||||||
|
{
|
||||||
|
"modifiers": [
|
||||||
|
{
|
||||||
|
"exclude": [
|
||||||
|
".git/**",
|
||||||
|
".vs/**",
|
||||||
|
".template-hive/**",
|
||||||
|
".template.config/**",
|
||||||
|
"**/bin/**",
|
||||||
|
"**/.vs/**",
|
||||||
|
"**/logs/**",
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/obj/**",
|
||||||
|
"**/*.user",
|
||||||
|
"README.md",
|
||||||
|
"package-output/**",
|
||||||
|
"package-scripts/tools/**",
|
||||||
|
"Avalonia.Stack.TemplatePack.csproj"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Vendored
+2
-1
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"chat.tools.terminal.autoApprove": {
|
"chat.tools.terminal.autoApprove": {
|
||||||
"ForEach-Object": true,
|
"ForEach-Object": true,
|
||||||
"dotnet list": true
|
"dotnet list": true,
|
||||||
|
"dotnet build": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,24 +4,27 @@ using Avalonia_EFCore.Models;
|
|||||||
using Avalonia_Services.Core;
|
using Avalonia_Services.Core;
|
||||||
using Avalonia_Services.Services.AuthService;
|
using Avalonia_Services.Services.AuthService;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace Avalonia_API.Authentication
|
namespace Avalonia_API.Authentication
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// API 鉴权端点服务,实现 <see cref="IApiAuthEndpointService"/>,
|
||||||
|
/// 处理登录、刷新 Token 和登出操作,使用 JWT 与 Refresh Token 机制。
|
||||||
|
/// </summary>
|
||||||
public sealed class ApiAuthEndpointService(
|
public sealed class ApiAuthEndpointService(
|
||||||
AppDataContext db,
|
AppDataContext db,
|
||||||
JwtTokenService jwtTokenService,
|
JwtTokenService jwtTokenService,
|
||||||
RefreshTokenService refreshTokenService) : IApiAuthEndpointService
|
RefreshTokenService refreshTokenService) : IApiAuthEndpointService
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
/// <summary>
|
||||||
|
/// 处理用户登录请求。根据账号(邮箱或用户名)查找或创建用户,
|
||||||
|
/// 生成 JWT Access Token 和 Refresh Token 并返回。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ctx">服务端点上下文,包含请求体、请求头等信息。</param>
|
||||||
|
/// <returns>包含 AccessToken、RefreshToken 及过期时间的认证响应。</returns>
|
||||||
|
public async Task<IApiResponse> LoginAsync(ApiLoginRequest request, ServiceEndpointContext ctx)
|
||||||
{
|
{
|
||||||
PropertyNameCaseInsensitive = true,
|
if (string.IsNullOrWhiteSpace(request.Account))
|
||||||
};
|
|
||||||
|
|
||||||
public async Task<object?> LoginAsync(ServiceEndpointContext ctx)
|
|
||||||
{
|
|
||||||
var request = Deserialize<ApiLoginRequest>(ctx.Body);
|
|
||||||
if (string.IsNullOrWhiteSpace(request?.Account))
|
|
||||||
{
|
{
|
||||||
ctx.StatusCode = 400;
|
ctx.StatusCode = 400;
|
||||||
return ResponseHelper.Failure(400, "账号不能为空");
|
return ResponseHelper.Failure(400, "账号不能为空");
|
||||||
@@ -56,11 +59,16 @@ namespace Avalonia_API.Authentication
|
|||||||
roles), "登录成功");
|
roles), "登录成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<object?> RefreshAsync(ServiceEndpointContext ctx)
|
/// <summary>
|
||||||
|
/// 使用 Refresh Token 轮换新的 Access Token 和 Refresh Token。
|
||||||
|
/// 旧的 Refresh Token 会被撤销并替换。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ctx">服务端点上下文,包含请求体中的 RefreshToken。</param>
|
||||||
|
/// <returns>新的 Token 对;若 Refresh Token 无效则返回 401 错误。</returns>
|
||||||
|
public async Task<IApiResponse> RefreshAsync(ApiRefreshTokenRequest request, ServiceEndpointContext ctx)
|
||||||
{
|
{
|
||||||
var request = Deserialize<ApiRefreshTokenRequest>(ctx.Body);
|
|
||||||
var rotated = await refreshTokenService.RotateAsync(
|
var rotated = await refreshTokenService.RotateAsync(
|
||||||
request?.RefreshToken,
|
request.RefreshToken,
|
||||||
ctx.GetHeader("User-Agent"),
|
ctx.GetHeader("User-Agent"),
|
||||||
GetRemoteIpAddress(ctx));
|
GetRemoteIpAddress(ctx));
|
||||||
|
|
||||||
@@ -88,20 +96,22 @@ namespace Avalonia_API.Authentication
|
|||||||
roles), "刷新成功");
|
roles), "刷新成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<object?> LogoutAsync(ServiceEndpointContext ctx)
|
/// <summary>
|
||||||
|
/// 处理用户登出请求,撤销指定的 Refresh Token。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ctx">服务端点上下文,包含请求体中的 RefreshToken。</param>
|
||||||
|
/// <returns>登出成功的响应。</returns>
|
||||||
|
public async Task<IApiResponse> LogoutAsync(ApiLogoutRequest request, ServiceEndpointContext ctx)
|
||||||
{
|
{
|
||||||
var request = Deserialize<ApiLogoutRequest>(ctx.Body);
|
await refreshTokenService.RevokeAsync(request.RefreshToken);
|
||||||
await refreshTokenService.RevokeAsync(request?.RefreshToken);
|
|
||||||
return ResponseHelper.Succeed("退出成功");
|
return ResponseHelper.Succeed("退出成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static T? Deserialize<T>(string? body)
|
/// <summary>
|
||||||
{
|
/// 从上下文的 Items 中提取 ASP.NET Core HttpContext,并获取客户端远程 IP 地址。
|
||||||
return string.IsNullOrWhiteSpace(body)
|
/// </summary>
|
||||||
? default
|
/// <param name="ctx">服务端点上下文。</param>
|
||||||
: JsonSerializer.Deserialize<T>(body, JsonOptions);
|
/// <returns>客户端 IP 地址字符串;若无法获取则返回 null。</returns>
|
||||||
}
|
|
||||||
|
|
||||||
private static string? GetRemoteIpAddress(ServiceEndpointContext ctx)
|
private static string? GetRemoteIpAddress(ServiceEndpointContext ctx)
|
||||||
{
|
{
|
||||||
return ctx.Items.TryGetValue("HttpContext", out var value) && value is HttpContext httpContext
|
return ctx.Items.TryGetValue("HttpContext", out var value) && value is HttpContext httpContext
|
||||||
@@ -109,6 +119,11 @@ namespace Avalonia_API.Authentication
|
|||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 规范化角色数组:去空白、去重(忽略大小写),为空时默认返回 Admin 角色。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="roles">原始角色数组,可为 null。</param>
|
||||||
|
/// <returns>规范化后的角色数组。</returns>
|
||||||
private static string[] NormalizeRoles(string[]? roles)
|
private static string[] NormalizeRoles(string[]? roles)
|
||||||
{
|
{
|
||||||
var normalized = roles?
|
var normalized = roles?
|
||||||
|
|||||||
@@ -1,15 +1,33 @@
|
|||||||
namespace Avalonia_API.Authentication
|
namespace Avalonia_API.Authentication
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JWT 鉴权配置选项,从 appsettings.json 的 Jwt 节绑定。
|
||||||
|
/// </summary>
|
||||||
public sealed class JwtOptions
|
public sealed class JwtOptions
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置 Token 签发者。
|
||||||
|
/// </summary>
|
||||||
public string Issuer { get; set; } = "Avalonia-API";
|
public string Issuer { get; set; } = "Avalonia-API";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置 Token 受众。
|
||||||
|
/// </summary>
|
||||||
public string Audience { get; set; } = "Avalonia-Client";
|
public string Audience { get; set; } = "Avalonia-Client";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置签名密钥(至少 32 字节)。
|
||||||
|
/// </summary>
|
||||||
public string SigningKey { get; set; } = "change-this-development-signing-key-at-least-32-bytes";
|
public string SigningKey { get; set; } = "change-this-development-signing-key-at-least-32-bytes";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置 Access Token 有效期(分钟),默认 60 分钟。
|
||||||
|
/// </summary>
|
||||||
public int AccessTokenMinutes { get; set; } = 60;
|
public int AccessTokenMinutes { get; set; } = 60;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置 Refresh Token 有效期(天),默认 30 天。
|
||||||
|
/// </summary>
|
||||||
public int RefreshTokenDays { get; set; } = 30;
|
public int RefreshTokenDays { get; set; } = 30;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,10 +7,22 @@ using System.Text;
|
|||||||
|
|
||||||
namespace Avalonia_API.Authentication
|
namespace Avalonia_API.Authentication
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JWT Token 服务,负责创建包含用户声明和角色的 Access Token。
|
||||||
|
/// </summary>
|
||||||
public sealed class JwtTokenService(IOptions<JwtOptions> options)
|
public sealed class JwtTokenService(IOptions<JwtOptions> options)
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JWT 配置选项。
|
||||||
|
/// </summary>
|
||||||
private readonly JwtOptions _options = options.Value;
|
private readonly JwtOptions _options = options.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建包含用户声明和角色的 JWT Access Token。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">用户实体。</param>
|
||||||
|
/// <param name="roles">角色集合。</param>
|
||||||
|
/// <returns>包含 Token 字符串和过期时间的元组。</returns>
|
||||||
public (string Token, DateTime ExpiresAt) CreateAccessToken(UserEntity user, IReadOnlyCollection<string> roles)
|
public (string Token, DateTime ExpiresAt) CreateAccessToken(UserEntity user, IReadOnlyCollection<string> roles)
|
||||||
{
|
{
|
||||||
var expiresAt = DateTime.UtcNow.AddMinutes(_options.AccessTokenMinutes);
|
var expiresAt = DateTime.UtcNow.AddMinutes(_options.AccessTokenMinutes);
|
||||||
|
|||||||
@@ -7,10 +7,25 @@ using System.Text;
|
|||||||
|
|
||||||
namespace Avalonia_API.Authentication
|
namespace Avalonia_API.Authentication
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Refresh Token 服务,负责创建、查找、撤销和轮换 Refresh Token,
|
||||||
|
/// Token 原文经 SHA256 哈希后存入数据库以保证安全性。
|
||||||
|
/// </summary>
|
||||||
public sealed class RefreshTokenService(AppDataContext db, IOptions<JwtOptions> options)
|
public sealed class RefreshTokenService(AppDataContext db, IOptions<JwtOptions> options)
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// JWT 配置选项。
|
||||||
|
/// </summary>
|
||||||
private readonly JwtOptions _options = options.Value;
|
private readonly JwtOptions _options = options.Value;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建一个新的 Refresh Token,生成随机 Token 原文并存储其哈希到数据库。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="userId">关联的用户 ID。</param>
|
||||||
|
/// <param name="device">创建设备标识(如 User-Agent)。</param>
|
||||||
|
/// <param name="ipAddress">客户端 IP 地址。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>包含 Token 原文和实体记录的元组。</returns>
|
||||||
public async Task<(string Token, ApiRefreshTokenEntity Entity)> CreateAsync(
|
public async Task<(string Token, ApiRefreshTokenEntity Entity)> CreateAsync(
|
||||||
int userId,
|
int userId,
|
||||||
string? device,
|
string? device,
|
||||||
@@ -32,6 +47,13 @@ namespace Avalonia_API.Authentication
|
|||||||
return (token, entity);
|
return (token, entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查找有效的 Refresh Token 实体。Token 原文会被哈希后查询数据库,
|
||||||
|
/// 仅返回未过期且未被撤销的 Token。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">Refresh Token 原文。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>有效的 Token 实体;若无效或不存在则返回 null。</returns>
|
||||||
public async Task<ApiRefreshTokenEntity?> FindActiveAsync(string? token, CancellationToken cancellationToken = default)
|
public async Task<ApiRefreshTokenEntity?> FindActiveAsync(string? token, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(token))
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
@@ -44,6 +66,11 @@ namespace Avalonia_API.Authentication
|
|||||||
return entity?.IsActive == true ? entity : null;
|
return entity?.IsActive == true ? entity : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 撤销指定的 Refresh Token,将其 RevokedAt 设为当前时间。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">要撤销的 Refresh Token 原文。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
public async Task RevokeAsync(string? token, CancellationToken cancellationToken = default)
|
public async Task RevokeAsync(string? token, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var entity = await FindActiveAsync(token, cancellationToken);
|
var entity = await FindActiveAsync(token, cancellationToken);
|
||||||
@@ -56,6 +83,14 @@ namespace Avalonia_API.Authentication
|
|||||||
await db.SaveChangesAsync(cancellationToken);
|
await db.SaveChangesAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 轮换 Refresh Token:撤销旧的并创建新的,将新 Token 的哈希关联到旧记录。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">旧的 Refresh Token 原文。</param>
|
||||||
|
/// <param name="device">当前设备标识。</param>
|
||||||
|
/// <param name="ipAddress">当前客户端 IP 地址。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>新的 Token 对;若旧 Token 无效则返回 null。</returns>
|
||||||
public async Task<(string Token, ApiRefreshTokenEntity Entity)?> RotateAsync(
|
public async Task<(string Token, ApiRefreshTokenEntity Entity)?> RotateAsync(
|
||||||
string? token,
|
string? token,
|
||||||
string? device,
|
string? device,
|
||||||
@@ -75,6 +110,11 @@ namespace Avalonia_API.Authentication
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 对 Token 原文进行 SHA256 哈希,返回十六进制字符串。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">Token 原文。</param>
|
||||||
|
/// <returns>SHA256 哈希后的十六进制字符串。</returns>
|
||||||
private static string HashToken(string token)
|
private static string HashToken(string token)
|
||||||
{
|
{
|
||||||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token));
|
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token));
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.5" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="10.0.5" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ using System.Text;
|
|||||||
|
|
||||||
namespace Avalonia_API.Configuration
|
namespace Avalonia_API.Configuration
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// API 项目服务配置扩展类,负责注册数据库、鉴权、业务服务和统一端点。
|
||||||
|
/// </summary>
|
||||||
public static class ServicesConfiguration
|
public static class ServicesConfiguration
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ namespace Avalonia_API.Extensions
|
|||||||
routeHandlerBuilder.WithSummary(endpoint.OpenApiSummary);
|
routeHandlerBuilder.WithSummary(endpoint.OpenApiSummary);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (endpoint.OpenApiRequestType is not null)
|
if (endpoint.OpenApiRequestType is not null
|
||||||
|
&& endpoint.HttpMethod is "POST" or "PUT")
|
||||||
{
|
{
|
||||||
routeHandlerBuilder.Accepts(endpoint.OpenApiRequestType, "application/json");
|
routeHandlerBuilder.Accepts(endpoint.OpenApiRequestType, "application/json");
|
||||||
}
|
}
|
||||||
@@ -95,6 +96,13 @@ namespace Avalonia_API.Extensions
|
|||||||
return routeBuilder;
|
return routeBuilder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据端点的 HTTP 方法(GET/POST/PUT/DELETE)将其映射到 ASP.NET Core 路由。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="group">路由组。</param>
|
||||||
|
/// <param name="endpoint">统一端点定义。</param>
|
||||||
|
/// <param name="serviceProvider">服务提供程序。</param>
|
||||||
|
/// <returns>路由处理器构建器,用于叠加过滤器等配置。</returns>
|
||||||
private static RouteHandlerBuilder MapEndpoint(
|
private static RouteHandlerBuilder MapEndpoint(
|
||||||
IEndpointRouteBuilder group,
|
IEndpointRouteBuilder group,
|
||||||
ServiceEndpoint endpoint,
|
ServiceEndpoint endpoint,
|
||||||
@@ -112,15 +120,23 @@ namespace Avalonia_API.Extensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建适配 ASP.NET Core 的委托处理器,将统一处理器包装为 ASP.NET Core 可识别的委托。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="unifiedHandler">统一端点处理器。</param>
|
||||||
|
/// <param name="serviceProvider">服务提供程序。</param>
|
||||||
|
/// <returns>ASP.NET Core 兼容的委托。</returns>
|
||||||
private static Delegate CreateAspNetCoreHandler(
|
private static Delegate CreateAspNetCoreHandler(
|
||||||
Func<ServiceEndpointContext, Task<object?>> unifiedHandler,
|
Func<ServiceEndpointContext, Task<object?>> unifiedHandler,
|
||||||
IServiceProvider serviceProvider)
|
IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
return async (HttpContext httpContext) =>
|
return async (HttpContext httpContext) =>
|
||||||
{
|
{
|
||||||
var ctx = await BuildContextFromHttpContext(httpContext);
|
var ctx = httpContext.Items["UnifiedContext"] as ServiceEndpointContext
|
||||||
|
?? await BuildContextFromHttpContext(httpContext);
|
||||||
ctx.Items["ServiceProvider"] = serviceProvider;
|
ctx.Items["ServiceProvider"] = serviceProvider;
|
||||||
ctx.Items["User"] = httpContext.User;
|
ctx.Items["User"] = httpContext.User;
|
||||||
|
httpContext.Items["UnifiedContext"] = ctx;
|
||||||
|
|
||||||
var result = await unifiedHandler(ctx);
|
var result = await unifiedHandler(ctx);
|
||||||
|
|
||||||
@@ -131,10 +147,33 @@ namespace Avalonia_API.Extensions
|
|||||||
httpContext.Response.Headers[kvp.Key] = kvp.Value;
|
httpContext.Response.Headers[kvp.Key] = kvp.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result is FileStreamResponse fileResponse)
|
||||||
|
{
|
||||||
|
if (!System.IO.File.Exists(fileResponse.FilePath))
|
||||||
|
return Results.NotFound();
|
||||||
|
|
||||||
|
var stream = System.IO.File.Open(fileResponse.FilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||||
|
httpContext.Response.Headers.ContentDisposition = $"inline; filename=\"{Uri.EscapeDataString(fileResponse.FileName)}\"";
|
||||||
|
httpContext.Response.Headers.AcceptRanges = "bytes";
|
||||||
|
httpContext.Response.Headers.CacheControl = "public, max-age=3600";
|
||||||
|
|
||||||
|
return Results.File(
|
||||||
|
stream,
|
||||||
|
contentType: fileResponse.ContentType,
|
||||||
|
lastModified: fileResponse.LastModified,
|
||||||
|
enableRangeProcessing: true);
|
||||||
|
}
|
||||||
|
|
||||||
return result is not null ? Results.Json(result) : Results.Ok();
|
return result is not null ? Results.Json(result) : Results.Ok();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 ASP.NET Core 的 HttpContext 构建统一的 ServiceEndpointContext,
|
||||||
|
/// 提取路径、方法、请求头、查询参数和请求体。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpContext">ASP.NET Core 的 HttpContext。</param>
|
||||||
|
/// <returns>构建好的统一端点上下文。</returns>
|
||||||
private static async Task<ServiceEndpointContext> BuildContextFromHttpContext(HttpContext httpContext)
|
private static async Task<ServiceEndpointContext> BuildContextFromHttpContext(HttpContext httpContext)
|
||||||
{
|
{
|
||||||
var ctx = new ServiceEndpointContext
|
var ctx = new ServiceEndpointContext
|
||||||
@@ -154,6 +193,14 @@ namespace Avalonia_API.Extensions
|
|||||||
ctx.Query[query.Key] = query.Value.ToString();
|
ctx.Query[query.Key] = query.Value.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var routeValue in httpContext.Request.RouteValues)
|
||||||
|
{
|
||||||
|
if (routeValue.Value is not null)
|
||||||
|
{
|
||||||
|
ctx.RouteValues[routeValue.Key] = routeValue.Value.ToString() ?? string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (httpContext.Request.ContentLength > 0)
|
if (httpContext.Request.ContentLength > 0)
|
||||||
{
|
{
|
||||||
using var reader = new StreamReader(httpContext.Request.Body);
|
using var reader = new StreamReader(httpContext.Request.Body);
|
||||||
@@ -166,6 +213,14 @@ namespace Avalonia_API.Extensions
|
|||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将统一过滤器转换为 ASP.NET Core 端点过滤器,
|
||||||
|
/// 在调用统一过滤器前后桥接上下文和状态。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="unifiedFilter">统一过滤器。</param>
|
||||||
|
/// <param name="aspContext">ASP.NET Core 过滤器调用上下文。</param>
|
||||||
|
/// <param name="aspNext">ASP.NET Core 过滤器管道中的下一个委托。</param>
|
||||||
|
/// <returns>过滤器执行结果,可能包含短路响应体。</returns>
|
||||||
private static async ValueTask<object?> ConvertFilterAsync(
|
private static async ValueTask<object?> ConvertFilterAsync(
|
||||||
UnifiedFilter unifiedFilter,
|
UnifiedFilter unifiedFilter,
|
||||||
AspNetCoreFilterContext aspContext,
|
AspNetCoreFilterContext aspContext,
|
||||||
@@ -177,6 +232,7 @@ namespace Avalonia_API.Extensions
|
|||||||
|
|
||||||
httpContext.Items["UnifiedContext"] = ctx;
|
httpContext.Items["UnifiedContext"] = ctx;
|
||||||
|
|
||||||
|
object? nextResult = null;
|
||||||
await unifiedFilter.InvokeAsync(ctx, async (c) =>
|
await unifiedFilter.InvokeAsync(ctx, async (c) =>
|
||||||
{
|
{
|
||||||
httpContext.Response.StatusCode = c.StatusCode;
|
httpContext.Response.StatusCode = c.StatusCode;
|
||||||
@@ -184,7 +240,7 @@ namespace Avalonia_API.Extensions
|
|||||||
{
|
{
|
||||||
httpContext.Response.Headers[kvp.Key] = kvp.Value;
|
httpContext.Response.Headers[kvp.Key] = kvp.Value;
|
||||||
}
|
}
|
||||||
await aspNext(aspContext);
|
nextResult = await aspNext(aspContext);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ctx.ResponseBody is not null)
|
if (ctx.ResponseBody is not null)
|
||||||
@@ -192,7 +248,7 @@ namespace Avalonia_API.Extensions
|
|||||||
return Results.Json(ctx.ResponseBody, statusCode: ctx.StatusCode);
|
return Results.Json(ctx.ResponseBody, statusCode: ctx.StatusCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null!;
|
return nextResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@
|
|||||||
"RefreshTokenDays": 30
|
"RefreshTokenDays": 30
|
||||||
},
|
},
|
||||||
"DatabaseConfiguration": {
|
"DatabaseConfiguration": {
|
||||||
"Provider": "SQLite",
|
"Provider": "MySQL",
|
||||||
"ConnectionString": "Data Source=avalonia-api.db",
|
"ConnectionString": "Server=127.0.0.1;Port=3306;Database=avalonia-api;Uid=root;Pwd=123456;Max Pool Size=100;Min Pool Size=5;AllowZeroDateTime=True;AllowLoadLocalInfile=true;SslMode=Required",
|
||||||
"AutoMigrate": true,
|
"AutoMigrate": true,
|
||||||
"RecreateDatabase": false,
|
"RecreateDatabase": false,
|
||||||
"EnableDetailedLog": false,
|
"EnableDetailedLog": false,
|
||||||
|
|||||||
@@ -2,12 +2,24 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace Avalonia_Common.Core
|
namespace Avalonia_Common.Core
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 统一端点响应契约。
|
||||||
|
/// </summary>
|
||||||
|
public interface IApiResponse
|
||||||
|
{
|
||||||
|
/// <summary>是否成功。</summary>
|
||||||
|
bool Success { get; }
|
||||||
|
|
||||||
|
/// <summary>业务状态码。</summary>
|
||||||
|
int Code { get; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 统一 API 返回格式。
|
/// 统一 API 返回格式。
|
||||||
/// 所有接口的返回都包装为此格式,确保前端收到一致的数据结构。
|
/// 所有接口的返回都包装为此格式,确保前端收到一致的数据结构。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T">业务数据类型</typeparam>
|
/// <typeparam name="T">业务数据类型</typeparam>
|
||||||
public class ApiResponse<T>
|
public class ApiResponse<T> : IApiResponse
|
||||||
{
|
{
|
||||||
/// <summary>是否成功</summary>
|
/// <summary>是否成功</summary>
|
||||||
[JsonPropertyName("success")]
|
[JsonPropertyName("success")]
|
||||||
@@ -113,29 +125,58 @@ namespace Avalonia_Common.Core
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 分页返回格式
|
/// 分页返回格式
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class PagedResponse<T>
|
public class PagedResponse<T> : IApiResponse
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置操作是否成功。
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("success")]
|
[JsonPropertyName("success")]
|
||||||
public bool Success { get; set; } = true;
|
public bool Success { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置业务状态码,默认 200。
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("code")]
|
[JsonPropertyName("code")]
|
||||||
public int Code { get; set; } = 200;
|
public int Code { get; set; } = 200;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置分页数据项列表。
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("items")]
|
[JsonPropertyName("items")]
|
||||||
public List<T> Items { get; set; } = new();
|
public List<T> Items { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置数据总条数。
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("total")]
|
[JsonPropertyName("total")]
|
||||||
public int Total { get; set; }
|
public int Total { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置当前页码,从 1 开始。
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("page")]
|
[JsonPropertyName("page")]
|
||||||
public int Page { get; set; } = 1;
|
public int Page { get; set; } = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置每页条数,默认 20。
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("pageSize")]
|
[JsonPropertyName("pageSize")]
|
||||||
public int PageSize { get; set; } = 20;
|
public int PageSize { get; set; } = 20;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取总页数(根据 Total 和 PageSize 自动计算)。
|
||||||
|
/// </summary>
|
||||||
[JsonPropertyName("totalPages")]
|
[JsonPropertyName("totalPages")]
|
||||||
public int TotalPages => PageSize > 0 ? (int)Math.Ceiling((double)Total / PageSize) : 0;
|
public int TotalPages => PageSize > 0 ? (int)Math.Ceiling((double)Total / PageSize) : 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从数据列表和分页参数创建分页响应。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="items">当前页数据项。</param>
|
||||||
|
/// <param name="total">数据总条数。</param>
|
||||||
|
/// <param name="page">当前页码。</param>
|
||||||
|
/// <param name="pageSize">每页条数。</param>
|
||||||
|
/// <returns>分页响应实例。</returns>
|
||||||
public static PagedResponse<T> From(List<T> items, int total, int page, int pageSize)
|
public static PagedResponse<T> From(List<T> items, int total, int page, int pageSize)
|
||||||
{
|
{
|
||||||
return new PagedResponse<T>
|
return new PagedResponse<T>
|
||||||
|
|||||||
@@ -87,6 +87,9 @@ namespace Avalonia_Common.Infrastructure
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class AppLog
|
public static class AppLog
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 保存全局日志记录器实例。
|
||||||
|
/// </summary>
|
||||||
private static ILogger? _logger;
|
private static ILogger? _logger;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -98,26 +101,66 @@ namespace Avalonia_Common.Infrastructure
|
|||||||
Log.Logger = logger;
|
Log.Logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取全局日志记录器。若未初始化则回退到 Serilog.Log.Logger。
|
||||||
|
/// </summary>
|
||||||
public static ILogger Logger => _logger ?? Log.Logger;
|
public static ILogger Logger => _logger ?? Log.Logger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入 Debug 级别日志。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageTemplate">消息模板。</param>
|
||||||
|
/// <param name="propertyValues">属性值。</param>
|
||||||
public static void Debug(string messageTemplate, params object?[] propertyValues)
|
public static void Debug(string messageTemplate, params object?[] propertyValues)
|
||||||
=> Logger.Debug(messageTemplate, propertyValues);
|
=> Logger.Debug(messageTemplate, propertyValues);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入 Information 级别日志。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageTemplate">消息模板。</param>
|
||||||
|
/// <param name="propertyValues">属性值。</param>
|
||||||
public static void Information(string messageTemplate, params object?[] propertyValues)
|
public static void Information(string messageTemplate, params object?[] propertyValues)
|
||||||
=> Logger.Information(messageTemplate, propertyValues);
|
=> Logger.Information(messageTemplate, propertyValues);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入 Warning 级别日志。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageTemplate">消息模板。</param>
|
||||||
|
/// <param name="propertyValues">属性值。</param>
|
||||||
public static void Warning(string messageTemplate, params object?[] propertyValues)
|
public static void Warning(string messageTemplate, params object?[] propertyValues)
|
||||||
=> Logger.Warning(messageTemplate, propertyValues);
|
=> Logger.Warning(messageTemplate, propertyValues);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入 Error 级别日志。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageTemplate">消息模板。</param>
|
||||||
|
/// <param name="propertyValues">属性值。</param>
|
||||||
public static void Error(string messageTemplate, params object?[] propertyValues)
|
public static void Error(string messageTemplate, params object?[] propertyValues)
|
||||||
=> Logger.Error(messageTemplate, propertyValues);
|
=> Logger.Error(messageTemplate, propertyValues);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入 Error 级别日志,并附带异常信息。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="exception">异常对象。</param>
|
||||||
|
/// <param name="messageTemplate">消息模板。</param>
|
||||||
|
/// <param name="propertyValues">属性值。</param>
|
||||||
public static void Error(Exception exception, string messageTemplate, params object?[] propertyValues)
|
public static void Error(Exception exception, string messageTemplate, params object?[] propertyValues)
|
||||||
=> Logger.Error(exception, messageTemplate, propertyValues);
|
=> Logger.Error(exception, messageTemplate, propertyValues);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入 Fatal 级别日志。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="messageTemplate">消息模板。</param>
|
||||||
|
/// <param name="propertyValues">属性值。</param>
|
||||||
public static void Fatal(string messageTemplate, params object?[] propertyValues)
|
public static void Fatal(string messageTemplate, params object?[] propertyValues)
|
||||||
=> Logger.Fatal(messageTemplate, propertyValues);
|
=> Logger.Fatal(messageTemplate, propertyValues);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 写入 Fatal 级别日志,并附带异常信息。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="exception">异常对象。</param>
|
||||||
|
/// <param name="messageTemplate">消息模板。</param>
|
||||||
|
/// <param name="propertyValues">属性值。</param>
|
||||||
public static void Fatal(Exception exception, string messageTemplate, params object?[] propertyValues)
|
public static void Fatal(Exception exception, string messageTemplate, params object?[] propertyValues)
|
||||||
=> Logger.Fatal(exception, messageTemplate, propertyValues);
|
=> Logger.Fatal(exception, messageTemplate, propertyValues);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,17 +8,17 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.7" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
<PackageReference Include="MySql.EntityFrameworkCore" Version="10.0.7" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.7" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ namespace Avalonia_EFCore.Database
|
|||||||
/// <summary>API refresh token 数据</summary>
|
/// <summary>API refresh token 数据</summary>
|
||||||
public DbSet<ApiRefreshTokenEntity> ApiRefreshTokens => Set<ApiRefreshTokenEntity>();
|
public DbSet<ApiRefreshTokenEntity> ApiRefreshTokens => Set<ApiRefreshTokenEntity>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配置实体映射,包括主键、索引和属性约束。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="modelBuilder">模型构建器。</param>
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
base.OnModelCreating(modelBuilder);
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|||||||
@@ -2,12 +2,103 @@ using Microsoft.EntityFrameworkCore.Design;
|
|||||||
|
|
||||||
namespace Avalonia_EFCore.Database
|
namespace Avalonia_EFCore.Database
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设计时 DbContext 工厂,用于 EF Core 迁移工具生成迁移代码。
|
||||||
|
/// </summary>
|
||||||
public class AppDataContextFactory : IDesignTimeDbContextFactory<AppDataContext>
|
public class AppDataContextFactory : IDesignTimeDbContextFactory<AppDataContext>
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 创建用于设计时的 AppDataContext 实例,默认使用 SQLite 提供程序。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">命令行参数。</param>
|
||||||
|
/// <returns>配置好的数据上下文实例。</returns>
|
||||||
public AppDataContext CreateDbContext(string[] args)
|
public AppDataContext CreateDbContext(string[] args)
|
||||||
|
{
|
||||||
|
return new AppDataContext(DesignTimeDatabaseConfiguration.Create(args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SQLite 迁移设计时工厂。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SqliteAppDataContextFactory : IDesignTimeDbContextFactory<SqliteAppDataContext>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public SqliteAppDataContext CreateDbContext(string[] args)
|
||||||
|
=> new(DesignTimeDatabaseConfiguration.Create(args, DatabaseProvider.SQLite));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SQL Server 迁移设计时工厂。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SqlServerAppDataContextFactory : IDesignTimeDbContextFactory<SqlServerAppDataContext>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public SqlServerAppDataContext CreateDbContext(string[] args)
|
||||||
|
=> new(DesignTimeDatabaseConfiguration.Create(args, DatabaseProvider.SqlServer));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PostgreSQL 迁移设计时工厂。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PostgreSqlAppDataContextFactory : IDesignTimeDbContextFactory<PostgreSqlAppDataContext>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public PostgreSqlAppDataContext CreateDbContext(string[] args)
|
||||||
|
=> new(DesignTimeDatabaseConfiguration.Create(args, DatabaseProvider.PostgreSQL));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MySQL 迁移设计时工厂。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MySqlAppDataContextFactory : IDesignTimeDbContextFactory<MySqlAppDataContext>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public MySqlAppDataContext CreateDbContext(string[] args)
|
||||||
|
=> new(DesignTimeDatabaseConfiguration.Create(args, DatabaseProvider.MySQL));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static class DesignTimeDatabaseConfiguration
|
||||||
|
{
|
||||||
|
public static DatabaseConfiguration Create(string[] args, DatabaseProvider defaultProvider = DatabaseProvider.SQLite)
|
||||||
{
|
{
|
||||||
DatabaseProviderRegistry.RegisterDefaults();
|
DatabaseProviderRegistry.RegisterDefaults();
|
||||||
return new AppDataContext(DatabaseConfiguration.ForSQLite("avalonia-api.db"));
|
|
||||||
|
var provider = GetProvider(args) ?? defaultProvider;
|
||||||
|
return provider switch
|
||||||
|
{
|
||||||
|
DatabaseProvider.SQLite => DatabaseConfiguration.ForSQLite("avalonia-api.db"),
|
||||||
|
DatabaseProvider.SqlServer => DatabaseConfiguration.ForSqlServer("(localdb)\\MSSQLLocalDB", "AvaloniaApi"),
|
||||||
|
DatabaseProvider.PostgreSQL => DatabaseConfiguration.ForPostgreSQL("localhost", "avalonia_api", "postgres", "postgres"),
|
||||||
|
DatabaseProvider.MySQL => DatabaseConfiguration.ForMySQL("localhost", "avalonia_api", "root", "root"),
|
||||||
|
_ => DatabaseConfiguration.ForSQLite("avalonia-api.db"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DatabaseProvider? GetProvider(string[] args)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < args.Length; i++)
|
||||||
|
{
|
||||||
|
var arg = args[i];
|
||||||
|
string? value = null;
|
||||||
|
|
||||||
|
if (arg.Equals("--provider", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length)
|
||||||
|
{
|
||||||
|
value = args[i + 1];
|
||||||
|
}
|
||||||
|
else if (arg.StartsWith("--provider=", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
value = arg["--provider=".Length..];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(value)
|
||||||
|
&& Enum.TryParse<DatabaseProvider>(value, ignoreCase: true, out var provider))
|
||||||
|
{
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,15 @@ namespace Avalonia_EFCore.Database
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public abstract class AppDbContext(DatabaseConfiguration dbConfig) : DbContext
|
public abstract class AppDbContext(DatabaseConfiguration dbConfig) : DbContext
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库配置。
|
||||||
|
/// </summary>
|
||||||
private readonly DatabaseConfiguration _dbConfig = dbConfig;
|
private readonly DatabaseConfiguration _dbConfig = dbConfig;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配置数据库提供程序和连接选项。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="optionsBuilder">选项构建器。</param>
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
if (optionsBuilder.IsConfigured) return;
|
if (optionsBuilder.IsConfigured) return;
|
||||||
@@ -65,12 +72,21 @@ namespace Avalonia_EFCore.Database
|
|||||||
return base.SaveChanges(acceptAllChangesOnSuccess);
|
return base.SaveChanges(acceptAllChangesOnSuccess);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 异步保存更改,自动设置时间戳。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="acceptAllChangesOnSuccess">是否在成功时接受所有更改。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>受影响的行数。</returns>
|
||||||
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
|
public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
SetTimestamps();
|
SetTimestamps();
|
||||||
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自动设置新增或修改实体的 CreatedAt 和 UpdatedAt 时间戳。
|
||||||
|
/// </summary>
|
||||||
private void SetTimestamps()
|
private void SetTimestamps()
|
||||||
{
|
{
|
||||||
var entries = ChangeTracker.Entries()
|
var entries = ChangeTracker.Entries()
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Avalonia_EFCore.Database
|
namespace Avalonia_EFCore.Database
|
||||||
@@ -19,6 +20,14 @@ namespace Avalonia_EFCore.Database
|
|||||||
// 注册配置
|
// 注册配置
|
||||||
services.AddSingleton(config);
|
services.AddSingleton(config);
|
||||||
|
|
||||||
|
if (typeof(TContext) == typeof(AppDataContext))
|
||||||
|
{
|
||||||
|
services.AddProviderAppDataContext(config);
|
||||||
|
services.AddScoped<DatabaseManager<TContext>>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
// 注册 DbContext
|
// 注册 DbContext
|
||||||
services.AddDbContext<TContext>(options =>
|
services.AddDbContext<TContext>(options =>
|
||||||
{
|
{
|
||||||
@@ -31,6 +40,31 @@ namespace Avalonia_EFCore.Database
|
|||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void AddProviderAppDataContext(this IServiceCollection services, DatabaseConfiguration config)
|
||||||
|
{
|
||||||
|
switch (config.Provider)
|
||||||
|
{
|
||||||
|
case DatabaseProvider.SQLite:
|
||||||
|
services.AddDbContext<AppDataContext, SqliteAppDataContext>(options =>
|
||||||
|
AppDbContext.ConfigureProvider(options, config));
|
||||||
|
break;
|
||||||
|
case DatabaseProvider.SqlServer:
|
||||||
|
services.AddDbContext<AppDataContext, SqlServerAppDataContext>(options =>
|
||||||
|
AppDbContext.ConfigureProvider(options, config));
|
||||||
|
break;
|
||||||
|
case DatabaseProvider.PostgreSQL:
|
||||||
|
services.AddDbContext<AppDataContext, PostgreSqlAppDataContext>(options =>
|
||||||
|
AppDbContext.ConfigureProvider(options, config));
|
||||||
|
break;
|
||||||
|
case DatabaseProvider.MySQL:
|
||||||
|
services.AddDbContext<AppDataContext, MySqlAppDataContext>(options =>
|
||||||
|
AppDbContext.ConfigureProvider(options, config));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new NotSupportedException($"数据库提供程序 {config.Provider} 未注册。");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 初始化数据库(在应用启动时调用一次)。
|
/// 初始化数据库(在应用启动时调用一次)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -17,10 +17,25 @@ namespace Avalonia_EFCore.Database
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class DatabaseManager<TContext> where TContext : AppDbContext
|
public class DatabaseManager<TContext> where TContext : AppDbContext
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库上下文实例。
|
||||||
|
/// </summary>
|
||||||
private readonly TContext _context;
|
private readonly TContext _context;
|
||||||
|
/// <summary>
|
||||||
|
/// 数据库配置。
|
||||||
|
/// </summary>
|
||||||
private readonly DatabaseConfiguration _config;
|
private readonly DatabaseConfiguration _config;
|
||||||
|
/// <summary>
|
||||||
|
/// DI 服务提供程序(可选,用于种子数据中解析服务)。
|
||||||
|
/// </summary>
|
||||||
private readonly IServiceProvider? _serviceProvider;
|
private readonly IServiceProvider? _serviceProvider;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化数据库管理器。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">数据库上下文。</param>
|
||||||
|
/// <param name="config">数据库配置。</param>
|
||||||
|
/// <param name="serviceProvider">可选的 DI 容器。</param>
|
||||||
public DatabaseManager(TContext context, DatabaseConfiguration config, IServiceProvider? serviceProvider = null)
|
public DatabaseManager(TContext context, DatabaseConfiguration config, IServiceProvider? serviceProvider = null)
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
@@ -128,6 +143,10 @@ namespace Avalonia_EFCore.Database
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取当前应用程序的版本号,优先读取 AssemblyInformationalVersion,回退到 AssemblyVersion。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>应用程序版本字符串。</returns>
|
||||||
private static string GetApplicationVersion()
|
private static string GetApplicationVersion()
|
||||||
{
|
{
|
||||||
var assembly = Assembly.GetEntryAssembly() ?? typeof(TContext).Assembly;
|
var assembly = Assembly.GetEntryAssembly() ?? typeof(TContext).Assembly;
|
||||||
@@ -181,10 +200,25 @@ namespace Avalonia_EFCore.Database
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class DatabaseVersionInfo
|
public class DatabaseVersionInfo
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置数据库提供程序名称。
|
||||||
|
/// </summary>
|
||||||
public string Provider { get; set; } = string.Empty;
|
public string Provider { get; set; } = string.Empty;
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置已应用的迁移列表。
|
||||||
|
/// </summary>
|
||||||
public List<string> AppliedMigrations { get; set; } = new();
|
public List<string> AppliedMigrations { get; set; } = new();
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置待应用的迁移列表。
|
||||||
|
/// </summary>
|
||||||
public List<string> PendingMigrations { get; set; } = new();
|
public List<string> PendingMigrations { get; set; } = new();
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置是否为最新版本。
|
||||||
|
/// </summary>
|
||||||
public bool IsLatest { get; set; }
|
public bool IsLatest { get; set; }
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置数据库是否可连接。
|
||||||
|
/// </summary>
|
||||||
public bool CanConnect { get; set; }
|
public bool CanConnect { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ namespace Avalonia_EFCore.Database
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public delegate void ProviderConfigurator(DbContextOptionsBuilder optionsBuilder, string connectionString, int timeout);
|
public delegate void ProviderConfigurator(DbContextOptionsBuilder optionsBuilder, string connectionString, int timeout);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存已注册的数据库提供程序及其配置委托。
|
||||||
|
/// </summary>
|
||||||
private static readonly Dictionary<DatabaseProvider, ProviderConfigurator> _providers = new();
|
private static readonly Dictionary<DatabaseProvider, ProviderConfigurator> _providers = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -49,7 +52,7 @@ namespace Avalonia_EFCore.Database
|
|||||||
opts.UseNpgsql(cs, o => { o.CommandTimeout(timeout); o.EnableRetryOnFailure(3); }));
|
opts.UseNpgsql(cs, o => { o.CommandTimeout(timeout); o.EnableRetryOnFailure(3); }));
|
||||||
|
|
||||||
Register(DatabaseProvider.MySQL, (opts, cs, timeout) =>
|
Register(DatabaseProvider.MySQL, (opts, cs, timeout) =>
|
||||||
opts.UseMySql(cs, ServerVersion.AutoDetect(cs), o => { o.CommandTimeout(timeout); o.EnableRetryOnFailure(3); }));
|
opts.UseMySQL(cs, o => o.CommandTimeout(timeout)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
namespace Avalonia_EFCore.Database
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// SQLite 专用 DbContext,用于隔离 SQLite 迁移集。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SqliteAppDataContext(DatabaseConfiguration dbConfig) : AppDataContext(dbConfig)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SQL Server 专用 DbContext,用于隔离 SQL Server 迁移集。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class SqlServerAppDataContext(DatabaseConfiguration dbConfig) : AppDataContext(dbConfig)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PostgreSQL 专用 DbContext,用于隔离 PostgreSQL 迁移集。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PostgreSqlAppDataContext(DatabaseConfiguration dbConfig) : AppDataContext(dbConfig)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MySQL 专用 DbContext,用于隔离 MySQL 迁移集。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class MySqlAppDataContext(DatabaseConfiguration dbConfig) : AppDataContext(dbConfig)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
+175
@@ -0,0 +1,175 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Avalonia_EFCore.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Avalonia_EFCore.Migrations.MySQL
|
||||||
|
{
|
||||||
|
[DbContext(typeof(MySqlAppDataContext))]
|
||||||
|
[Migration("20260520082626_AutoMigration_20260520162543")]
|
||||||
|
partial class AutoMigration_20260520162543
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<string>("Device")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)")
|
||||||
|
.HasColumnName("device");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("expires-at");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("varchar(64)")
|
||||||
|
.HasColumnName("ip-address");
|
||||||
|
|
||||||
|
b.Property<string>("ReplacedByTokenHash")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)")
|
||||||
|
.HasColumnName("replaced-by-token-hash");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RevokedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("revoked-at");
|
||||||
|
|
||||||
|
b.Property<string>("TokenHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)")
|
||||||
|
.HasColumnName("token-hash");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("user-id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-api-refresh-token");
|
||||||
|
|
||||||
|
b.HasIndex("TokenHash")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-hash");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-user-id");
|
||||||
|
|
||||||
|
b.ToTable("api-refresh-token", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("API refresh token");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("用户主键");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)")
|
||||||
|
.HasColumnName("email")
|
||||||
|
.HasComment("用户邮箱");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)")
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasComment("用户名称");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("varchar(50)")
|
||||||
|
.HasColumnName("phone-number")
|
||||||
|
.HasComment("电话号码");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-user");
|
||||||
|
|
||||||
|
b.ToTable("user", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("用户实体,演示数据库 CRUD 操作");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("天气预报主键");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("date")
|
||||||
|
.HasColumnName("date")
|
||||||
|
.HasComment("预报日期");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)")
|
||||||
|
.HasColumnName("summary")
|
||||||
|
.HasComment("天气摘要");
|
||||||
|
|
||||||
|
b.Property<int>("TemperatureC")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("temperature-c")
|
||||||
|
.HasComment("摄氏温度");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-weather-forecast");
|
||||||
|
|
||||||
|
b.ToTable("weather-forecast", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("天气预报数据实体");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using MySql.EntityFrameworkCore.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Avalonia_EFCore.Migrations.MySQL
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AutoMigration_20260520162543 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AlterDatabase()
|
||||||
|
.Annotation("MySQL:Charset", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "api-refresh-token",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn),
|
||||||
|
userid = table.Column<int>(name: "user-id", type: "int", nullable: false),
|
||||||
|
tokenhash = table.Column<string>(name: "token-hash", type: "varchar(128)", maxLength: 128, nullable: false),
|
||||||
|
createdat = table.Column<DateTime>(name: "created-at", type: "datetime(6)", nullable: false),
|
||||||
|
expiresat = table.Column<DateTime>(name: "expires-at", type: "datetime(6)", nullable: false),
|
||||||
|
revokedat = table.Column<DateTime>(name: "revoked-at", type: "datetime(6)", nullable: true),
|
||||||
|
replacedbytokenhash = table.Column<string>(name: "replaced-by-token-hash", type: "varchar(128)", maxLength: 128, nullable: true),
|
||||||
|
device = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: true),
|
||||||
|
ipaddress = table.Column<string>(name: "ip-address", type: "varchar(64)", maxLength: 64, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk-api-refresh-token", x => x.id);
|
||||||
|
},
|
||||||
|
comment: "API refresh token")
|
||||||
|
.Annotation("MySQL:Charset", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "user",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "int", nullable: false, comment: "用户主键")
|
||||||
|
.Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn),
|
||||||
|
name = table.Column<string>(type: "varchar(100)", maxLength: 100, nullable: true, comment: "用户名称"),
|
||||||
|
email = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: true, comment: "用户邮箱"),
|
||||||
|
phonenumber = table.Column<string>(name: "phone-number", type: "varchar(50)", maxLength: 50, nullable: true, comment: "电话号码"),
|
||||||
|
createdat = table.Column<DateTime>(name: "created-at", type: "datetime(6)", nullable: false, comment: "创建时间"),
|
||||||
|
updatedat = table.Column<DateTime>(name: "updated-at", type: "datetime(6)", nullable: false, comment: "更新时间")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk-user", x => x.id);
|
||||||
|
},
|
||||||
|
comment: "用户实体,演示数据库 CRUD 操作")
|
||||||
|
.Annotation("MySQL:Charset", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "weather-forecast",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "int", nullable: false, comment: "天气预报主键")
|
||||||
|
.Annotation("MySQL:ValueGenerationStrategy", MySQLValueGenerationStrategy.IdentityColumn),
|
||||||
|
date = table.Column<DateOnly>(type: "date", nullable: false, comment: "预报日期"),
|
||||||
|
temperaturec = table.Column<int>(name: "temperature-c", type: "int", nullable: false, comment: "摄氏温度"),
|
||||||
|
summary = table.Column<string>(type: "varchar(200)", maxLength: 200, nullable: true, comment: "天气摘要"),
|
||||||
|
createdat = table.Column<DateTime>(name: "created-at", type: "datetime(6)", nullable: false, comment: "创建时间"),
|
||||||
|
updatedat = table.Column<DateTime>(name: "updated-at", type: "datetime(6)", nullable: false, comment: "更新时间")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk-weather-forecast", x => x.id);
|
||||||
|
},
|
||||||
|
comment: "天气预报数据实体")
|
||||||
|
.Annotation("MySQL:Charset", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx-api-refresh-token-hash",
|
||||||
|
table: "api-refresh-token",
|
||||||
|
column: "token-hash",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx-api-refresh-token-user-id",
|
||||||
|
table: "api-refresh-token",
|
||||||
|
column: "user-id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "api-refresh-token");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "user");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "weather-forecast");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+181
@@ -0,0 +1,181 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Avalonia_EFCore.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Avalonia_EFCore.Migrations.MySQL
|
||||||
|
{
|
||||||
|
[DbContext(typeof(MySqlAppDataContext))]
|
||||||
|
[Migration("20260520083306_AutoMigration_20260520163216")]
|
||||||
|
partial class AutoMigration_20260520163216
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<string>("Device")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)")
|
||||||
|
.HasColumnName("device");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("expires-at");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("varchar(64)")
|
||||||
|
.HasColumnName("ip-address");
|
||||||
|
|
||||||
|
b.Property<string>("ReplacedByTokenHash")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)")
|
||||||
|
.HasColumnName("replaced-by-token-hash");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RevokedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("revoked-at");
|
||||||
|
|
||||||
|
b.Property<string>("TokenHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)")
|
||||||
|
.HasColumnName("token-hash");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("user-id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-api-refresh-token");
|
||||||
|
|
||||||
|
b.HasIndex("TokenHash")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-hash");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-user-id");
|
||||||
|
|
||||||
|
b.ToTable("api-refresh-token", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("API refresh token");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("用户主键");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)")
|
||||||
|
.HasColumnName("email")
|
||||||
|
.HasComment("用户邮箱");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)")
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasComment("用户名称");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)")
|
||||||
|
.HasColumnName("password-hash")
|
||||||
|
.HasComment("密码哈希值");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("varchar(50)")
|
||||||
|
.HasColumnName("phone-number")
|
||||||
|
.HasComment("电话号码");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-user");
|
||||||
|
|
||||||
|
b.ToTable("user", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("用户实体,演示数据库 CRUD 操作");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("天气预报主键");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("date")
|
||||||
|
.HasColumnName("date")
|
||||||
|
.HasComment("预报日期");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)")
|
||||||
|
.HasColumnName("summary")
|
||||||
|
.HasComment("天气摘要");
|
||||||
|
|
||||||
|
b.Property<int>("TemperatureC")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("temperature-c")
|
||||||
|
.HasComment("摄氏温度");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-weather-forecast");
|
||||||
|
|
||||||
|
b.ToTable("weather-forecast", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("天气预报数据实体");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Avalonia_EFCore.Migrations.MySQL
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AutoMigration_20260520163216 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "password-hash",
|
||||||
|
table: "user",
|
||||||
|
type: "varchar(200)",
|
||||||
|
maxLength: 200,
|
||||||
|
nullable: true,
|
||||||
|
comment: "密码哈希值");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "password-hash",
|
||||||
|
table: "user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Avalonia_EFCore.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Avalonia_EFCore.Migrations.MySQL
|
||||||
|
{
|
||||||
|
[DbContext(typeof(MySqlAppDataContext))]
|
||||||
|
partial class MySqlAppDataContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 64);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<string>("Device")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)")
|
||||||
|
.HasColumnName("device");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("expires-at");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("varchar(64)")
|
||||||
|
.HasColumnName("ip-address");
|
||||||
|
|
||||||
|
b.Property<string>("ReplacedByTokenHash")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)")
|
||||||
|
.HasColumnName("replaced-by-token-hash");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RevokedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("revoked-at");
|
||||||
|
|
||||||
|
b.Property<string>("TokenHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("varchar(128)")
|
||||||
|
.HasColumnName("token-hash");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("user-id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-api-refresh-token");
|
||||||
|
|
||||||
|
b.HasIndex("TokenHash")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-hash");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-user-id");
|
||||||
|
|
||||||
|
b.ToTable("api-refresh-token", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("API refresh token");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("用户主键");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)")
|
||||||
|
.HasColumnName("email")
|
||||||
|
.HasComment("用户邮箱");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("varchar(100)")
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasComment("用户名称");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)")
|
||||||
|
.HasColumnName("password-hash")
|
||||||
|
.HasComment("密码哈希值");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("varchar(50)")
|
||||||
|
.HasColumnName("phone-number")
|
||||||
|
.HasComment("电话号码");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-user");
|
||||||
|
|
||||||
|
b.ToTable("user", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("用户实体,演示数据库 CRUD 操作");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("天气预报主键");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("date")
|
||||||
|
.HasColumnName("date")
|
||||||
|
.HasComment("预报日期");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)")
|
||||||
|
.HasColumnName("summary")
|
||||||
|
.HasComment("天气摘要");
|
||||||
|
|
||||||
|
b.Property<int>("TemperatureC")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("temperature-c")
|
||||||
|
.HasComment("摄氏温度");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime(6)")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-weather-forecast");
|
||||||
|
|
||||||
|
b.ToTable("weather-forecast", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("天气预报数据实体");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+184
@@ -0,0 +1,184 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Avalonia_EFCore.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Avalonia_EFCore.Migrations.PostgreSQL
|
||||||
|
{
|
||||||
|
[DbContext(typeof(PostgreSqlAppDataContext))]
|
||||||
|
[Migration("20260520082617_AutoMigration_20260520162543")]
|
||||||
|
partial class AutoMigration_20260520162543
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<string>("Device")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasColumnName("device");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expires-at");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasColumnName("ip-address");
|
||||||
|
|
||||||
|
b.Property<string>("ReplacedByTokenHash")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)")
|
||||||
|
.HasColumnName("replaced-by-token-hash");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RevokedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("revoked-at");
|
||||||
|
|
||||||
|
b.Property<string>("TokenHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)")
|
||||||
|
.HasColumnName("token-hash");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user-id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-api-refresh-token");
|
||||||
|
|
||||||
|
b.HasIndex("TokenHash")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-hash");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-user-id");
|
||||||
|
|
||||||
|
b.ToTable("api-refresh-token", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("API refresh token");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("用户主键");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasColumnName("email")
|
||||||
|
.HasComment("用户邮箱");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasComment("用户名称");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)")
|
||||||
|
.HasColumnName("phone-number")
|
||||||
|
.HasComment("电话号码");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-user");
|
||||||
|
|
||||||
|
b.ToTable("user", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("用户实体,演示数据库 CRUD 操作");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("天气预报主键");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("date")
|
||||||
|
.HasColumnName("date")
|
||||||
|
.HasComment("预报日期");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasColumnName("summary")
|
||||||
|
.HasComment("天气摘要");
|
||||||
|
|
||||||
|
b.Property<int>("TemperatureC")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("temperature-c")
|
||||||
|
.HasComment("摄氏温度");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-weather-forecast");
|
||||||
|
|
||||||
|
b.ToTable("weather-forecast", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("天气预报数据实体");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Avalonia_EFCore.Migrations.PostgreSQL
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AutoMigration_20260520162543 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "api-refresh-token",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
userid = table.Column<int>(name: "user-id", type: "integer", nullable: false),
|
||||||
|
tokenhash = table.Column<string>(name: "token-hash", type: "character varying(128)", maxLength: 128, nullable: false),
|
||||||
|
createdat = table.Column<DateTime>(name: "created-at", type: "timestamp with time zone", nullable: false),
|
||||||
|
expiresat = table.Column<DateTime>(name: "expires-at", type: "timestamp with time zone", nullable: false),
|
||||||
|
revokedat = table.Column<DateTime>(name: "revoked-at", type: "timestamp with time zone", nullable: true),
|
||||||
|
replacedbytokenhash = table.Column<string>(name: "replaced-by-token-hash", type: "character varying(128)", maxLength: 128, nullable: true),
|
||||||
|
device = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||||
|
ipaddress = table.Column<string>(name: "ip-address", type: "character varying(64)", maxLength: 64, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk-api-refresh-token", x => x.id);
|
||||||
|
},
|
||||||
|
comment: "API refresh token");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "user",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false, comment: "用户主键")
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true, comment: "用户名称"),
|
||||||
|
email = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true, comment: "用户邮箱"),
|
||||||
|
phonenumber = table.Column<string>(name: "phone-number", type: "character varying(50)", maxLength: 50, nullable: true, comment: "电话号码"),
|
||||||
|
createdat = table.Column<DateTime>(name: "created-at", type: "timestamp with time zone", nullable: false, comment: "创建时间"),
|
||||||
|
updatedat = table.Column<DateTime>(name: "updated-at", type: "timestamp with time zone", nullable: false, comment: "更新时间")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk-user", x => x.id);
|
||||||
|
},
|
||||||
|
comment: "用户实体,演示数据库 CRUD 操作");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "weather-forecast",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "integer", nullable: false, comment: "天气预报主键")
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
date = table.Column<DateOnly>(type: "date", nullable: false, comment: "预报日期"),
|
||||||
|
temperaturec = table.Column<int>(name: "temperature-c", type: "integer", nullable: false, comment: "摄氏温度"),
|
||||||
|
summary = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true, comment: "天气摘要"),
|
||||||
|
createdat = table.Column<DateTime>(name: "created-at", type: "timestamp with time zone", nullable: false, comment: "创建时间"),
|
||||||
|
updatedat = table.Column<DateTime>(name: "updated-at", type: "timestamp with time zone", nullable: false, comment: "更新时间")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk-weather-forecast", x => x.id);
|
||||||
|
},
|
||||||
|
comment: "天气预报数据实体");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx-api-refresh-token-hash",
|
||||||
|
table: "api-refresh-token",
|
||||||
|
column: "token-hash",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx-api-refresh-token-user-id",
|
||||||
|
table: "api-refresh-token",
|
||||||
|
column: "user-id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "api-refresh-token");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "user");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "weather-forecast");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+190
@@ -0,0 +1,190 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Avalonia_EFCore.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Avalonia_EFCore.Migrations.PostgreSQL
|
||||||
|
{
|
||||||
|
[DbContext(typeof(PostgreSqlAppDataContext))]
|
||||||
|
[Migration("20260520083254_AutoMigration_20260520163216")]
|
||||||
|
partial class AutoMigration_20260520163216
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<string>("Device")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasColumnName("device");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expires-at");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasColumnName("ip-address");
|
||||||
|
|
||||||
|
b.Property<string>("ReplacedByTokenHash")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)")
|
||||||
|
.HasColumnName("replaced-by-token-hash");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RevokedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("revoked-at");
|
||||||
|
|
||||||
|
b.Property<string>("TokenHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)")
|
||||||
|
.HasColumnName("token-hash");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user-id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-api-refresh-token");
|
||||||
|
|
||||||
|
b.HasIndex("TokenHash")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-hash");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-user-id");
|
||||||
|
|
||||||
|
b.ToTable("api-refresh-token", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("API refresh token");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("用户主键");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasColumnName("email")
|
||||||
|
.HasComment("用户邮箱");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasComment("用户名称");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasColumnName("password-hash")
|
||||||
|
.HasComment("密码哈希值");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)")
|
||||||
|
.HasColumnName("phone-number")
|
||||||
|
.HasComment("电话号码");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-user");
|
||||||
|
|
||||||
|
b.ToTable("user", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("用户实体,演示数据库 CRUD 操作");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("天气预报主键");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("date")
|
||||||
|
.HasColumnName("date")
|
||||||
|
.HasComment("预报日期");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasColumnName("summary")
|
||||||
|
.HasComment("天气摘要");
|
||||||
|
|
||||||
|
b.Property<int>("TemperatureC")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("temperature-c")
|
||||||
|
.HasComment("摄氏温度");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-weather-forecast");
|
||||||
|
|
||||||
|
b.ToTable("weather-forecast", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("天气预报数据实体");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Avalonia_EFCore.Migrations.PostgreSQL
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AutoMigration_20260520163216 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "password-hash",
|
||||||
|
table: "user",
|
||||||
|
type: "character varying(200)",
|
||||||
|
maxLength: 200,
|
||||||
|
nullable: true,
|
||||||
|
comment: "密码哈希值");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "password-hash",
|
||||||
|
table: "user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Avalonia_EFCore.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Avalonia_EFCore.Migrations.PostgreSQL
|
||||||
|
{
|
||||||
|
[DbContext(typeof(PostgreSqlAppDataContext))]
|
||||||
|
partial class PostgreSqlAppDataContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<string>("Device")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasColumnName("device");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("expires-at");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("character varying(64)")
|
||||||
|
.HasColumnName("ip-address");
|
||||||
|
|
||||||
|
b.Property<string>("ReplacedByTokenHash")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)")
|
||||||
|
.HasColumnName("replaced-by-token-hash");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RevokedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("revoked-at");
|
||||||
|
|
||||||
|
b.Property<string>("TokenHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)")
|
||||||
|
.HasColumnName("token-hash");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("user-id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-api-refresh-token");
|
||||||
|
|
||||||
|
b.HasIndex("TokenHash")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-hash");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-user-id");
|
||||||
|
|
||||||
|
b.ToTable("api-refresh-token", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("API refresh token");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("用户主键");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasColumnName("email")
|
||||||
|
.HasComment("用户邮箱");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)")
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasComment("用户名称");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasColumnName("password-hash")
|
||||||
|
.HasComment("密码哈希值");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)")
|
||||||
|
.HasColumnName("phone-number")
|
||||||
|
.HasComment("电话号码");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-user");
|
||||||
|
|
||||||
|
b.ToTable("user", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("用户实体,演示数据库 CRUD 操作");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("天气预报主键");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("date")
|
||||||
|
.HasColumnName("date")
|
||||||
|
.HasComment("预报日期");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)")
|
||||||
|
.HasColumnName("summary")
|
||||||
|
.HasComment("天气摘要");
|
||||||
|
|
||||||
|
b.Property<int>("TemperatureC")
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasColumnName("temperature-c")
|
||||||
|
.HasComment("摄氏温度");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-weather-forecast");
|
||||||
|
|
||||||
|
b.ToTable("weather-forecast", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("天气预报数据实体");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -6,9 +6,9 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Avalonia_EFCore.Migrations
|
namespace Avalonia_EFCore.Migrations.SQLite
|
||||||
{
|
{
|
||||||
[DbContext(typeof(AppDataContext))]
|
[DbContext(typeof(SqliteAppDataContext))]
|
||||||
[Migration("20260514000100_InitialCreate")]
|
[Migration("20260514000100_InitialCreate")]
|
||||||
partial class InitialCreate
|
partial class InitialCreate
|
||||||
{
|
{
|
||||||
+3
-6
@@ -1,11 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Avalonia_EFCore.Migrations
|
namespace Avalonia_EFCore.Migrations.SQLite
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 初始数据库基线。后续软件版本只追加新的 Migration,不修改已发布 Migration。
|
/// 初始数据库基线。后续软件版本只追加新的 Migration,不修改已发布 Migration。
|
||||||
@@ -21,8 +20,7 @@ namespace Avalonia_EFCore.Migrations
|
|||||||
Id = table.Column<int>(name: "id", nullable: false, comment: "用户主键")
|
Id = table.Column<int>(name: "id", nullable: false, comment: "用户主键")
|
||||||
.Annotation("SqlServer:Identity", "1, 1")
|
.Annotation("SqlServer:Identity", "1, 1")
|
||||||
.Annotation("Sqlite:Autoincrement", true)
|
.Annotation("Sqlite:Autoincrement", true)
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
|
||||||
Name = table.Column<string>(name: "name", maxLength: 100, nullable: true, comment: "用户名称"),
|
Name = table.Column<string>(name: "name", maxLength: 100, nullable: true, comment: "用户名称"),
|
||||||
Email = table.Column<string>(name: "email", maxLength: 200, nullable: true, comment: "用户邮箱"),
|
Email = table.Column<string>(name: "email", maxLength: 200, nullable: true, comment: "用户邮箱"),
|
||||||
CreatedAt = table.Column<DateTime>(name: "created-at", nullable: false, comment: "创建时间"),
|
CreatedAt = table.Column<DateTime>(name: "created-at", nullable: false, comment: "创建时间"),
|
||||||
@@ -41,8 +39,7 @@ namespace Avalonia_EFCore.Migrations
|
|||||||
Id = table.Column<int>(name: "id", nullable: false, comment: "天气预报主键")
|
Id = table.Column<int>(name: "id", nullable: false, comment: "天气预报主键")
|
||||||
.Annotation("SqlServer:Identity", "1, 1")
|
.Annotation("SqlServer:Identity", "1, 1")
|
||||||
.Annotation("Sqlite:Autoincrement", true)
|
.Annotation("Sqlite:Autoincrement", true)
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
|
||||||
Date = table.Column<DateOnly>(name: "date", nullable: false, comment: "预报日期"),
|
Date = table.Column<DateOnly>(name: "date", nullable: false, comment: "预报日期"),
|
||||||
TemperatureC = table.Column<int>(name: "temperature-c", nullable: false, comment: "摄氏温度"),
|
TemperatureC = table.Column<int>(name: "temperature-c", nullable: false, comment: "摄氏温度"),
|
||||||
Summary = table.Column<string>(name: "summary", maxLength: 200, nullable: true, comment: "天气摘要"),
|
Summary = table.Column<string>(name: "summary", maxLength: 200, nullable: true, comment: "天气摘要"),
|
||||||
+2
-2
@@ -8,9 +8,9 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Avalonia_EFCore.Migrations
|
namespace Avalonia_EFCore.Migrations.SQLite
|
||||||
{
|
{
|
||||||
[DbContext(typeof(AppDataContext))]
|
[DbContext(typeof(SqliteAppDataContext))]
|
||||||
[Migration("20260515072045_AutoMigration_20260515152037")]
|
[Migration("20260515072045_AutoMigration_20260515152037")]
|
||||||
partial class AutoMigration_20260515152037
|
partial class AutoMigration_20260515152037
|
||||||
{
|
{
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Avalonia_EFCore.Migrations
|
namespace Avalonia_EFCore.Migrations.SQLite
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class AutoMigration_20260515152037 : Migration
|
public partial class AutoMigration_20260515152037 : Migration
|
||||||
+2
-2
@@ -8,9 +8,9 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Avalonia_EFCore.Migrations
|
namespace Avalonia_EFCore.Migrations.SQLite
|
||||||
{
|
{
|
||||||
[DbContext(typeof(AppDataContext))]
|
[DbContext(typeof(SqliteAppDataContext))]
|
||||||
[Migration("20260515085847_AutoMigration_20260515165835")]
|
[Migration("20260515085847_AutoMigration_20260515165835")]
|
||||||
partial class AutoMigration_20260515165835
|
partial class AutoMigration_20260515165835
|
||||||
{
|
{
|
||||||
+1
-1
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Avalonia_EFCore.Migrations
|
namespace Avalonia_EFCore.Migrations.SQLite
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class AutoMigration_20260515165835 : Migration
|
public partial class AutoMigration_20260515165835 : Migration
|
||||||
+179
@@ -0,0 +1,179 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Avalonia_EFCore.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Avalonia_EFCore.Migrations.SQLite
|
||||||
|
{
|
||||||
|
[DbContext(typeof(SqliteAppDataContext))]
|
||||||
|
[Migration("20260520083230_AutoMigration_20260520163216")]
|
||||||
|
partial class AutoMigration_20260520163216
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<string>("Device")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("device");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("expires-at");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("ip-address");
|
||||||
|
|
||||||
|
b.Property<string>("ReplacedByTokenHash")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("replaced-by-token-hash");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RevokedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("revoked-at");
|
||||||
|
|
||||||
|
b.Property<string>("TokenHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("token-hash");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("user-id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-api-refresh-token");
|
||||||
|
|
||||||
|
b.HasIndex("TokenHash")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-hash");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-user-id");
|
||||||
|
|
||||||
|
b.ToTable("api-refresh-token", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("API refresh token");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("用户主键");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("email")
|
||||||
|
.HasComment("用户邮箱");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasComment("用户名称");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("password-hash")
|
||||||
|
.HasComment("密码哈希值");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("phone-number")
|
||||||
|
.HasComment("电话号码");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-user");
|
||||||
|
|
||||||
|
b.ToTable("user", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("用户实体,演示数据库 CRUD 操作");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("天气预报主键");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("date")
|
||||||
|
.HasComment("预报日期");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("summary")
|
||||||
|
.HasComment("天气摘要");
|
||||||
|
|
||||||
|
b.Property<int>("TemperatureC")
|
||||||
|
.HasColumnType("INTEGER")
|
||||||
|
.HasColumnName("temperature-c")
|
||||||
|
.HasComment("摄氏温度");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-weather-forecast");
|
||||||
|
|
||||||
|
b.ToTable("weather-forecast", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("天气预报数据实体");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Avalonia_EFCore.Migrations.SQLite
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AutoMigration_20260520163216 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "password-hash",
|
||||||
|
table: "user",
|
||||||
|
type: "TEXT",
|
||||||
|
maxLength: 200,
|
||||||
|
nullable: true,
|
||||||
|
comment: "密码哈希值");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "password-hash",
|
||||||
|
table: "user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-4
@@ -7,15 +7,15 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace Avalonia_EFCore.Migrations
|
namespace Avalonia_EFCore.Migrations.SQLite
|
||||||
{
|
{
|
||||||
[DbContext(typeof(AppDataContext))]
|
[DbContext(typeof(SqliteAppDataContext))]
|
||||||
partial class AppDataContextModelSnapshot : ModelSnapshot
|
partial class SqliteAppDataContextModelSnapshot : ModelSnapshot
|
||||||
{
|
{
|
||||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
|
modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
|
||||||
|
|
||||||
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
|
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
|
||||||
{
|
{
|
||||||
@@ -102,6 +102,12 @@ namespace Avalonia_EFCore.Migrations
|
|||||||
.HasColumnName("name")
|
.HasColumnName("name")
|
||||||
.HasComment("用户名称");
|
.HasComment("用户名称");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT")
|
||||||
|
.HasColumnName("password-hash")
|
||||||
|
.HasComment("密码哈希值");
|
||||||
|
|
||||||
b.Property<string>("PhoneNumber")
|
b.Property<string>("PhoneNumber")
|
||||||
.HasMaxLength(50)
|
.HasMaxLength(50)
|
||||||
.HasColumnType("TEXT")
|
.HasColumnType("TEXT")
|
||||||
Generated
+184
@@ -0,0 +1,184 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Avalonia_EFCore.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Avalonia_EFCore.Migrations.SqlServer
|
||||||
|
{
|
||||||
|
[DbContext(typeof(SqlServerAppDataContext))]
|
||||||
|
[Migration("20260520082607_AutoMigration_20260520162543")]
|
||||||
|
partial class AutoMigration_20260520162543
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<string>("Device")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)")
|
||||||
|
.HasColumnName("device");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("expires-at");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)")
|
||||||
|
.HasColumnName("ip-address");
|
||||||
|
|
||||||
|
b.Property<string>("ReplacedByTokenHash")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)")
|
||||||
|
.HasColumnName("replaced-by-token-hash");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RevokedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("revoked-at");
|
||||||
|
|
||||||
|
b.Property<string>("TokenHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)")
|
||||||
|
.HasColumnName("token-hash");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("user-id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-api-refresh-token");
|
||||||
|
|
||||||
|
b.HasIndex("TokenHash")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-hash");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-user-id");
|
||||||
|
|
||||||
|
b.ToTable("api-refresh-token", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("API refresh token");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("用户主键");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)")
|
||||||
|
.HasColumnName("email")
|
||||||
|
.HasComment("用户邮箱");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)")
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasComment("用户名称");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)")
|
||||||
|
.HasColumnName("phone-number")
|
||||||
|
.HasComment("电话号码");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-user");
|
||||||
|
|
||||||
|
b.ToTable("user", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("用户实体,演示数据库 CRUD 操作");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("天气预报主键");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("date")
|
||||||
|
.HasColumnName("date")
|
||||||
|
.HasComment("预报日期");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)")
|
||||||
|
.HasColumnName("summary")
|
||||||
|
.HasComment("天气摘要");
|
||||||
|
|
||||||
|
b.Property<int>("TemperatureC")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("temperature-c")
|
||||||
|
.HasComment("摄氏温度");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-weather-forecast");
|
||||||
|
|
||||||
|
b.ToTable("weather-forecast", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("天气预报数据实体");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Avalonia_EFCore.Migrations.SqlServer
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AutoMigration_20260520162543 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "api-refresh-token",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<long>(type: "bigint", nullable: false)
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
userid = table.Column<int>(name: "user-id", type: "int", nullable: false),
|
||||||
|
tokenhash = table.Column<string>(name: "token-hash", type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||||
|
createdat = table.Column<DateTime>(name: "created-at", type: "datetime2", nullable: false),
|
||||||
|
expiresat = table.Column<DateTime>(name: "expires-at", type: "datetime2", nullable: false),
|
||||||
|
revokedat = table.Column<DateTime>(name: "revoked-at", type: "datetime2", nullable: true),
|
||||||
|
replacedbytokenhash = table.Column<string>(name: "replaced-by-token-hash", type: "nvarchar(128)", maxLength: 128, nullable: true),
|
||||||
|
device = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||||
|
ipaddress = table.Column<string>(name: "ip-address", type: "nvarchar(64)", maxLength: 64, nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk-api-refresh-token", x => x.id);
|
||||||
|
},
|
||||||
|
comment: "API refresh token");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "user",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "int", nullable: false, comment: "用户主键")
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true, comment: "用户名称"),
|
||||||
|
email = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true, comment: "用户邮箱"),
|
||||||
|
phonenumber = table.Column<string>(name: "phone-number", type: "nvarchar(50)", maxLength: 50, nullable: true, comment: "电话号码"),
|
||||||
|
createdat = table.Column<DateTime>(name: "created-at", type: "datetime2", nullable: false, comment: "创建时间"),
|
||||||
|
updatedat = table.Column<DateTime>(name: "updated-at", type: "datetime2", nullable: false, comment: "更新时间")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk-user", x => x.id);
|
||||||
|
},
|
||||||
|
comment: "用户实体,演示数据库 CRUD 操作");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "weather-forecast",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
id = table.Column<int>(type: "int", nullable: false, comment: "天气预报主键")
|
||||||
|
.Annotation("SqlServer:Identity", "1, 1"),
|
||||||
|
date = table.Column<DateOnly>(type: "date", nullable: false, comment: "预报日期"),
|
||||||
|
temperaturec = table.Column<int>(name: "temperature-c", type: "int", nullable: false, comment: "摄氏温度"),
|
||||||
|
summary = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true, comment: "天气摘要"),
|
||||||
|
createdat = table.Column<DateTime>(name: "created-at", type: "datetime2", nullable: false, comment: "创建时间"),
|
||||||
|
updatedat = table.Column<DateTime>(name: "updated-at", type: "datetime2", nullable: false, comment: "更新时间")
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("pk-weather-forecast", x => x.id);
|
||||||
|
},
|
||||||
|
comment: "天气预报数据实体");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx-api-refresh-token-hash",
|
||||||
|
table: "api-refresh-token",
|
||||||
|
column: "token-hash",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "idx-api-refresh-token-user-id",
|
||||||
|
table: "api-refresh-token",
|
||||||
|
column: "user-id");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "api-refresh-token");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "user");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "weather-forecast");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+190
@@ -0,0 +1,190 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Avalonia_EFCore.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Avalonia_EFCore.Migrations.SqlServer
|
||||||
|
{
|
||||||
|
[DbContext(typeof(SqlServerAppDataContext))]
|
||||||
|
[Migration("20260520083242_AutoMigration_20260520163216")]
|
||||||
|
partial class AutoMigration_20260520163216
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<string>("Device")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)")
|
||||||
|
.HasColumnName("device");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("expires-at");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)")
|
||||||
|
.HasColumnName("ip-address");
|
||||||
|
|
||||||
|
b.Property<string>("ReplacedByTokenHash")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)")
|
||||||
|
.HasColumnName("replaced-by-token-hash");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RevokedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("revoked-at");
|
||||||
|
|
||||||
|
b.Property<string>("TokenHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)")
|
||||||
|
.HasColumnName("token-hash");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("user-id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-api-refresh-token");
|
||||||
|
|
||||||
|
b.HasIndex("TokenHash")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-hash");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-user-id");
|
||||||
|
|
||||||
|
b.ToTable("api-refresh-token", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("API refresh token");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("用户主键");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)")
|
||||||
|
.HasColumnName("email")
|
||||||
|
.HasComment("用户邮箱");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)")
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasComment("用户名称");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)")
|
||||||
|
.HasColumnName("password-hash")
|
||||||
|
.HasComment("密码哈希值");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)")
|
||||||
|
.HasColumnName("phone-number")
|
||||||
|
.HasComment("电话号码");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-user");
|
||||||
|
|
||||||
|
b.ToTable("user", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("用户实体,演示数据库 CRUD 操作");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("天气预报主键");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("date")
|
||||||
|
.HasColumnName("date")
|
||||||
|
.HasComment("预报日期");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)")
|
||||||
|
.HasColumnName("summary")
|
||||||
|
.HasComment("天气摘要");
|
||||||
|
|
||||||
|
b.Property<int>("TemperatureC")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("temperature-c")
|
||||||
|
.HasComment("摄氏温度");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-weather-forecast");
|
||||||
|
|
||||||
|
b.ToTable("weather-forecast", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("天气预报数据实体");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Avalonia_EFCore.Migrations.SqlServer
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AutoMigration_20260520163216 : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "password-hash",
|
||||||
|
table: "user",
|
||||||
|
type: "nvarchar(200)",
|
||||||
|
maxLength: 200,
|
||||||
|
nullable: true,
|
||||||
|
comment: "密码哈希值");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "password-hash",
|
||||||
|
table: "user");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Avalonia_EFCore.Database;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Avalonia_EFCore.Migrations.SqlServer
|
||||||
|
{
|
||||||
|
[DbContext(typeof(SqlServerAppDataContext))]
|
||||||
|
partial class SqlServerAppDataContextModelSnapshot : ModelSnapshot
|
||||||
|
{
|
||||||
|
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 128);
|
||||||
|
|
||||||
|
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<long>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("bigint")
|
||||||
|
.HasColumnName("id");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("created-at");
|
||||||
|
|
||||||
|
b.Property<string>("Device")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)")
|
||||||
|
.HasColumnName("device");
|
||||||
|
|
||||||
|
b.Property<DateTime>("ExpiresAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("expires-at");
|
||||||
|
|
||||||
|
b.Property<string>("IpAddress")
|
||||||
|
.HasMaxLength(64)
|
||||||
|
.HasColumnType("nvarchar(64)")
|
||||||
|
.HasColumnName("ip-address");
|
||||||
|
|
||||||
|
b.Property<string>("ReplacedByTokenHash")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)")
|
||||||
|
.HasColumnName("replaced-by-token-hash");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("RevokedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("revoked-at");
|
||||||
|
|
||||||
|
b.Property<string>("TokenHash")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("nvarchar(128)")
|
||||||
|
.HasColumnName("token-hash");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("user-id");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-api-refresh-token");
|
||||||
|
|
||||||
|
b.HasIndex("TokenHash")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-hash");
|
||||||
|
|
||||||
|
b.HasIndex("UserId")
|
||||||
|
.HasDatabaseName("idx-api-refresh-token-user-id");
|
||||||
|
|
||||||
|
b.ToTable("api-refresh-token", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("API refresh token");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.UserEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("用户主键");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)")
|
||||||
|
.HasColumnName("email")
|
||||||
|
.HasComment("用户邮箱");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("nvarchar(100)")
|
||||||
|
.HasColumnName("name")
|
||||||
|
.HasComment("用户名称");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)")
|
||||||
|
.HasColumnName("password-hash")
|
||||||
|
.HasComment("密码哈希值");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("nvarchar(50)")
|
||||||
|
.HasColumnName("phone-number")
|
||||||
|
.HasComment("电话号码");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-user");
|
||||||
|
|
||||||
|
b.ToTable("user", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("用户实体,演示数据库 CRUD 操作");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Avalonia_EFCore.Models.WeatherForecastEntity", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("id")
|
||||||
|
.HasComment("天气预报主键");
|
||||||
|
|
||||||
|
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("created-at")
|
||||||
|
.HasComment("创建时间");
|
||||||
|
|
||||||
|
b.Property<DateOnly>("Date")
|
||||||
|
.HasColumnType("date")
|
||||||
|
.HasColumnName("date")
|
||||||
|
.HasComment("预报日期");
|
||||||
|
|
||||||
|
b.Property<string>("Summary")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("nvarchar(200)")
|
||||||
|
.HasColumnName("summary")
|
||||||
|
.HasComment("天气摘要");
|
||||||
|
|
||||||
|
b.Property<int>("TemperatureC")
|
||||||
|
.HasColumnType("int")
|
||||||
|
.HasColumnName("temperature-c")
|
||||||
|
.HasComment("摄氏温度");
|
||||||
|
|
||||||
|
b.Property<DateTime>("UpdatedAt")
|
||||||
|
.HasColumnType("datetime2")
|
||||||
|
.HasColumnName("updated-at")
|
||||||
|
.HasComment("更新时间");
|
||||||
|
|
||||||
|
b.HasKey("Id")
|
||||||
|
.HasName("pk-weather-forecast");
|
||||||
|
|
||||||
|
b.ToTable("weather-forecast", t =>
|
||||||
|
{
|
||||||
|
t.HasComment("天气预报数据实体");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,39 +11,69 @@ namespace Avalonia_EFCore.Models
|
|||||||
[Table("api-refresh-token")]
|
[Table("api-refresh-token")]
|
||||||
public class ApiRefreshTokenEntity
|
public class ApiRefreshTokenEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置主键 ID(自增)。
|
||||||
|
/// </summary>
|
||||||
[Key]
|
[Key]
|
||||||
[Column("id")]
|
[Column("id")]
|
||||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
public long Id { get; set; }
|
public long Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置关联的用户 ID。
|
||||||
|
/// </summary>
|
||||||
[Column("user-id")]
|
[Column("user-id")]
|
||||||
public int UserId { get; set; }
|
public int UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置 Token 的 SHA256 哈希值,用于安全存储和查询。
|
||||||
|
/// </summary>
|
||||||
[Column("token-hash")]
|
[Column("token-hash")]
|
||||||
[MaxLength(128)]
|
[MaxLength(128)]
|
||||||
public string TokenHash { get; set; } = string.Empty;
|
public string TokenHash { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置 Token 创建时间。
|
||||||
|
/// </summary>
|
||||||
[Column("created-at")]
|
[Column("created-at")]
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置 Token 过期时间。
|
||||||
|
/// </summary>
|
||||||
[Column("expires-at")]
|
[Column("expires-at")]
|
||||||
public DateTime ExpiresAt { get; set; }
|
public DateTime ExpiresAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置 Token 撤销时间,null 表示尚未撤销。
|
||||||
|
/// </summary>
|
||||||
[Column("revoked-at")]
|
[Column("revoked-at")]
|
||||||
public DateTime? RevokedAt { get; set; }
|
public DateTime? RevokedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置替换此 Token 的新 Token 哈希值(轮换时设置)。
|
||||||
|
/// </summary>
|
||||||
[Column("replaced-by-token-hash")]
|
[Column("replaced-by-token-hash")]
|
||||||
[MaxLength(128)]
|
[MaxLength(128)]
|
||||||
public string? ReplacedByTokenHash { get; set; }
|
public string? ReplacedByTokenHash { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置创建设备标识(如 User-Agent)。
|
||||||
|
/// </summary>
|
||||||
[Column("device")]
|
[Column("device")]
|
||||||
[MaxLength(200)]
|
[MaxLength(200)]
|
||||||
public string? Device { get; set; }
|
public string? Device { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置创建时的客户端 IP 地址。
|
||||||
|
/// </summary>
|
||||||
[Column("ip-address")]
|
[Column("ip-address")]
|
||||||
[MaxLength(64)]
|
[MaxLength(64)]
|
||||||
public string? IpAddress { get; set; }
|
public string? IpAddress { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 Token 是否有效(未被撤销且未过期)。
|
||||||
|
/// </summary>
|
||||||
public bool IsActive => RevokedAt is null && ExpiresAt > DateTime.UtcNow;
|
public bool IsActive => RevokedAt is null && ExpiresAt > DateTime.UtcNow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,31 +11,57 @@ namespace Avalonia_EFCore.Models
|
|||||||
[Table("user")]
|
[Table("user")]
|
||||||
public class UserEntity
|
public class UserEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置用户主键 ID(自增)。
|
||||||
|
/// </summary>
|
||||||
[Key]
|
[Key]
|
||||||
[Comment("用户主键")]
|
[Comment("用户主键")]
|
||||||
[Column("id")]
|
[Column("id")]
|
||||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置用户名称。
|
||||||
|
/// </summary>
|
||||||
[Comment("用户名称")]
|
[Comment("用户名称")]
|
||||||
[Column("name")]
|
[Column("name")]
|
||||||
[MaxLength(100)]
|
[MaxLength(100)]
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置用户密码哈希值。
|
||||||
|
/// </summary>
|
||||||
|
[Comment("密码哈希值")]
|
||||||
|
[Column("password-hash")]
|
||||||
|
[MaxLength(200)]
|
||||||
|
public string? PasswordHash { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置用户邮箱。
|
||||||
|
/// </summary>
|
||||||
[Comment("用户邮箱")]
|
[Comment("用户邮箱")]
|
||||||
[Column("email")]
|
[Column("email")]
|
||||||
[MaxLength(200)]
|
[MaxLength(200)]
|
||||||
public string? Email { get; set; }
|
public string? Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置用户电话号码。
|
||||||
|
/// </summary>
|
||||||
[Comment("电话号码")]
|
[Comment("电话号码")]
|
||||||
[Column("phone-number")]
|
[Column("phone-number")]
|
||||||
[MaxLength(50)]
|
[MaxLength(50)]
|
||||||
public string? PhoneNumber { get; set; }
|
public string? PhoneNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置用户创建时间。
|
||||||
|
/// </summary>
|
||||||
[Comment("创建时间")]
|
[Comment("创建时间")]
|
||||||
[Column("created-at")]
|
[Column("created-at")]
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置用户最后更新时间。
|
||||||
|
/// </summary>
|
||||||
[Comment("更新时间")]
|
[Comment("更新时间")]
|
||||||
[Column("updated-at")]
|
[Column("updated-at")]
|
||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|||||||
@@ -1,13 +1,28 @@
|
|||||||
namespace Avalonia_EFCore.Models
|
namespace Avalonia_EFCore.Models
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 天气预报数据模型(内存/DTO 用,非数据库实体)。
|
||||||
|
/// </summary>
|
||||||
public class WeatherForecast
|
public class WeatherForecast
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置预报日期。
|
||||||
|
/// </summary>
|
||||||
public DateOnly Date { get; set; }
|
public DateOnly Date { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置摄氏温度。
|
||||||
|
/// </summary>
|
||||||
public int TemperatureC { get; set; }
|
public int TemperatureC { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取华氏温度(根据摄氏温度自动计算)。
|
||||||
|
/// </summary>
|
||||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置天气摘要。
|
||||||
|
/// </summary>
|
||||||
public string? Summary { get; set; }
|
public string? Summary { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,29 +11,47 @@ namespace Avalonia_EFCore.Models
|
|||||||
[Table("weather-forecast")]
|
[Table("weather-forecast")]
|
||||||
public class WeatherForecastEntity
|
public class WeatherForecastEntity
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置天气预报主键 ID(自增)。
|
||||||
|
/// </summary>
|
||||||
[Key]
|
[Key]
|
||||||
[Comment("天气预报主键")]
|
[Comment("天气预报主键")]
|
||||||
[Column("id")]
|
[Column("id")]
|
||||||
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置预报日期。
|
||||||
|
/// </summary>
|
||||||
[Comment("预报日期")]
|
[Comment("预报日期")]
|
||||||
[Column("date")]
|
[Column("date")]
|
||||||
public DateOnly Date { get; set; }
|
public DateOnly Date { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置摄氏温度。
|
||||||
|
/// </summary>
|
||||||
[Comment("摄氏温度")]
|
[Comment("摄氏温度")]
|
||||||
[Column("temperature-c")]
|
[Column("temperature-c")]
|
||||||
public int TemperatureC { get; set; }
|
public int TemperatureC { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置天气摘要。
|
||||||
|
/// </summary>
|
||||||
[Comment("天气摘要")]
|
[Comment("天气摘要")]
|
||||||
[Column("summary")]
|
[Column("summary")]
|
||||||
[MaxLength(200)]
|
[MaxLength(200)]
|
||||||
public string? Summary { get; set; }
|
public string? Summary { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置记录创建时间。
|
||||||
|
/// </summary>
|
||||||
[Comment("创建时间")]
|
[Comment("创建时间")]
|
||||||
[Column("created-at")]
|
[Column("created-at")]
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置记录最后更新时间。
|
||||||
|
/// </summary>
|
||||||
[Comment("更新时间")]
|
[Comment("更新时间")]
|
||||||
[Column("updated-at")]
|
[Column("updated-at")]
|
||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|||||||
@@ -7,13 +7,22 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
|
|
||||||
namespace Avalonia_PC
|
namespace Avalonia_PC
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Avalonia 应用程序入口类,负责初始化 XAML 资源和设置主窗口。
|
||||||
|
/// </summary>
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 加载 Avalonia XAML 资源。
|
||||||
|
/// </summary>
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 框架初始化完成后设置主窗口和数据上下文。
|
||||||
|
/// </summary>
|
||||||
public override void OnFrameworkInitializationCompleted()
|
public override void OnFrameworkInitializationCompleted()
|
||||||
{
|
{
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
|
|||||||
@@ -10,6 +10,12 @@ namespace Avalonia_PC.Authentication
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class DefaultPcThirdPartyAuthorizationClient : IPcThirdPartyAuthorizationClient
|
public sealed class DefaultPcThirdPartyAuthorizationClient : IPcThirdPartyAuthorizationClient
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 验证第三方授权码是否有效。默认实现将 "invalid" 视为授权丢失,其余视为有效。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="authorizationCode">第三方授权码。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>授权检查结果。</returns>
|
||||||
public Task<ThirdPartyAuthCheckResult> ValidateAuthorizationCodeAsync(
|
public Task<ThirdPartyAuthCheckResult> ValidateAuthorizationCodeAsync(
|
||||||
string authorizationCode,
|
string authorizationCode,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
@@ -23,6 +29,12 @@ namespace Avalonia_PC.Authentication
|
|||||||
return Task.FromResult(ThirdPartyAuthCheckResult.Valid);
|
return Task.FromResult(ThirdPartyAuthCheckResult.Valid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 刷新第三方授权。默认实现总是返回 TemporaryFailure,表示暂时无法刷新。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="authorizationReference">授权引用标识。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>授权检查结果。</returns>
|
||||||
public Task<ThirdPartyAuthCheckResult> RefreshAuthorizationAsync(
|
public Task<ThirdPartyAuthCheckResult> RefreshAuthorizationAsync(
|
||||||
string authorizationReference,
|
string authorizationReference,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
|
|||||||
@@ -3,22 +3,20 @@ using Avalonia_Common.Core;
|
|||||||
using Avalonia_Services.Core;
|
using Avalonia_Services.Core;
|
||||||
using Avalonia_Services.Services.AuthService;
|
using Avalonia_Services.Services.AuthService;
|
||||||
using System;
|
using System;
|
||||||
using System.Text.Json;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Avalonia_PC.Authentication
|
namespace Avalonia_PC.Authentication
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PC 端鉴权端点服务,实现 <see cref="IPcAuthEndpointService"/>,
|
||||||
|
/// 处理授权码登录、Token 刷新和登出操作。
|
||||||
|
/// </summary>
|
||||||
public sealed class PcAuthEndpointService(PcGlobalTokenService tokenService) : IPcAuthEndpointService
|
public sealed class PcAuthEndpointService(PcGlobalTokenService tokenService) : IPcAuthEndpointService
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
/// <inheritdoc />
|
||||||
|
public async Task<IApiResponse> AuthorizeAsync(PcAuthorizeRequest request, ServiceEndpointContext ctx)
|
||||||
{
|
{
|
||||||
PropertyNameCaseInsensitive = true,
|
var token = await tokenService.AuthorizeAsync(request.AuthorizationCode);
|
||||||
};
|
|
||||||
|
|
||||||
public async Task<object?> AuthorizeAsync(ServiceEndpointContext ctx)
|
|
||||||
{
|
|
||||||
var request = Deserialize<PcAuthorizeRequest>(ctx.Body);
|
|
||||||
var token = await tokenService.AuthorizeAsync(request?.AuthorizationCode);
|
|
||||||
if (token is null)
|
if (token is null)
|
||||||
{
|
{
|
||||||
ctx.StatusCode = 401;
|
ctx.StatusCode = 401;
|
||||||
@@ -28,10 +26,10 @@ namespace Avalonia_PC.Authentication
|
|||||||
return ResponseHelper.Ok(token, "授权成功");
|
return ResponseHelper.Ok(token, "授权成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<object?> RefreshAsync(ServiceEndpointContext ctx)
|
/// <inheritdoc />
|
||||||
|
public async Task<IApiResponse> RefreshAsync(PcRefreshRequest request, ServiceEndpointContext ctx)
|
||||||
{
|
{
|
||||||
var request = Deserialize<PcRefreshRequest>(ctx.Body);
|
var token = request.Token ?? ExtractBearerToken(ctx.GetHeader("Authorization"));
|
||||||
var token = request?.Token ?? ExtractBearerToken(ctx.GetHeader("Authorization"));
|
|
||||||
var refreshed = await tokenService.RefreshAsync(token);
|
var refreshed = await tokenService.RefreshAsync(token);
|
||||||
if (refreshed is null)
|
if (refreshed is null)
|
||||||
{
|
{
|
||||||
@@ -42,21 +40,19 @@ namespace Avalonia_PC.Authentication
|
|||||||
return ResponseHelper.Ok(refreshed, "刷新成功");
|
return ResponseHelper.Ok(refreshed, "刷新成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<object?> LogoutAsync(ServiceEndpointContext ctx)
|
/// <inheritdoc />
|
||||||
|
public Task<IApiResponse> LogoutAsync(PcLogoutRequest request, ServiceEndpointContext ctx)
|
||||||
{
|
{
|
||||||
var request = Deserialize<PcLogoutRequest>(ctx.Body);
|
var token = request.Token ?? ExtractBearerToken(ctx.GetHeader("Authorization"));
|
||||||
var token = request?.Token ?? ExtractBearerToken(ctx.GetHeader("Authorization"));
|
|
||||||
tokenService.Logout(token);
|
tokenService.Logout(token);
|
||||||
return Task.FromResult<object?>(ResponseHelper.Succeed("退出成功"));
|
return Task.FromResult<IApiResponse>(ResponseHelper.Succeed("退出成功"));
|
||||||
}
|
|
||||||
|
|
||||||
private static T? Deserialize<T>(string? body)
|
|
||||||
{
|
|
||||||
return string.IsNullOrWhiteSpace(body)
|
|
||||||
? default
|
|
||||||
: JsonSerializer.Deserialize<T>(body, JsonOptions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 Authorization 头中提取 Bearer Token。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="authorization">Authorization 头的值。</param>
|
||||||
|
/// <returns>提取的 Token 字符串;若无法提取则返回 null。</returns>
|
||||||
private static string? ExtractBearerToken(string? authorization)
|
private static string? ExtractBearerToken(string? authorization)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(authorization))
|
if (string.IsNullOrWhiteSpace(authorization))
|
||||||
@@ -64,7 +60,10 @@ namespace Avalonia_PC.Authentication
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const string prefix = "Bearer ";
|
/// <summary>
|
||||||
|
/// Bearer Token 的前缀常量。
|
||||||
|
/// </summary>
|
||||||
|
const string prefix = "Bearer ";
|
||||||
return authorization.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
|
return authorization.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
|
||||||
? authorization[prefix.Length..].Trim()
|
? authorization[prefix.Length..].Trim()
|
||||||
: authorization.Trim();
|
: authorization.Trim();
|
||||||
|
|||||||
@@ -6,8 +6,12 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace Avalonia_PC.Authentication
|
namespace Avalonia_PC.Authentication
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PC 端鉴权服务,基于全局 Token 验证用户身份,实现 <see cref="IAuthService"/>。
|
||||||
|
/// </summary>
|
||||||
public sealed class PcAuthService(PcGlobalTokenService tokenService) : IAuthService
|
public sealed class PcAuthService(PcGlobalTokenService tokenService) : IAuthService
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task<ClaimsPrincipal?> AuthenticateAsync(ServiceEndpointContext context)
|
public async Task<ClaimsPrincipal?> AuthenticateAsync(ServiceEndpointContext context)
|
||||||
{
|
{
|
||||||
var token = ExtractBearerToken(context.GetHeader("Authorization"));
|
var token = ExtractBearerToken(context.GetHeader("Authorization"));
|
||||||
@@ -29,11 +33,17 @@ namespace Avalonia_PC.Authentication
|
|||||||
return new ClaimsPrincipal(identity);
|
return new ClaimsPrincipal(identity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<bool> AuthorizeAsync(ClaimsPrincipal user, string policy)
|
public Task<bool> AuthorizeAsync(ClaimsPrincipal user, string policy)
|
||||||
{
|
{
|
||||||
return Task.FromResult(user.Identity?.IsAuthenticated == true);
|
return Task.FromResult(user.Identity?.IsAuthenticated == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 Authorization 头中提取 Bearer Token。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="authorization">Authorization 头的值。</param>
|
||||||
|
/// <returns>提取的 Token 字符串;若无法提取则返回 null。</returns>
|
||||||
private static string? ExtractBearerToken(string? authorization)
|
private static string? ExtractBearerToken(string? authorization)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(authorization))
|
if (string.IsNullOrWhiteSpace(authorization))
|
||||||
@@ -41,7 +51,7 @@ namespace Avalonia_PC.Authentication
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const string prefix = "Bearer ";
|
string prefix = "Bearer ";
|
||||||
return authorization.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
|
return authorization.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)
|
||||||
? authorization[prefix.Length..].Trim()
|
? authorization[prefix.Length..].Trim()
|
||||||
: authorization.Trim();
|
: authorization.Trim();
|
||||||
|
|||||||
@@ -7,16 +7,45 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace Authentication
|
namespace Authentication
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// PC 端全局 Token 服务,管理全局唯一的访问 Token。
|
||||||
|
/// 支持授权码登录、Token 刷新、有效性验证和登出,
|
||||||
|
/// 在第三方授权暂时失败时使用缩短有效期的临时 Token。
|
||||||
|
/// </summary>
|
||||||
public sealed class PcGlobalTokenService(IPcThirdPartyAuthorizationClient thirdPartyClient)
|
public sealed class PcGlobalTokenService(IPcThirdPartyAuthorizationClient thirdPartyClient)
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 超级管理员角色集合。
|
||||||
|
/// </summary>
|
||||||
private static readonly string[] SuperRoles = ["SuperAdmin", "Admin"];
|
private static readonly string[] SuperRoles = ["SuperAdmin", "Admin"];
|
||||||
|
/// <summary>
|
||||||
|
/// 线程同步锁。
|
||||||
|
/// </summary>
|
||||||
private readonly object _syncRoot = new();
|
private readonly object _syncRoot = new();
|
||||||
|
/// <summary>
|
||||||
|
/// 当前 Token 状态。
|
||||||
|
/// </summary>
|
||||||
private PcTokenState? _current;
|
private PcTokenState? _current;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 正常 Token 有效期(8 小时)。
|
||||||
|
/// </summary>
|
||||||
private static readonly TimeSpan NormalLifetime = TimeSpan.FromHours(8);
|
private static readonly TimeSpan NormalLifetime = TimeSpan.FromHours(8);
|
||||||
|
/// <summary>
|
||||||
|
/// 第三方暂时失败时的 Token 有效期(20 分钟)。
|
||||||
|
/// </summary>
|
||||||
private static readonly TimeSpan TemporaryFailureLifetime = TimeSpan.FromMinutes(20);
|
private static readonly TimeSpan TemporaryFailureLifetime = TimeSpan.FromMinutes(20);
|
||||||
|
/// <summary>
|
||||||
|
/// 第三方暂时失败的最长容忍窗口(24 小时),超出后清除 Token。
|
||||||
|
/// </summary>
|
||||||
private static readonly TimeSpan MaxTemporaryFailureWindow = TimeSpan.FromHours(24);
|
private static readonly TimeSpan MaxTemporaryFailureWindow = TimeSpan.FromHours(24);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用授权码进行登录授权,验证成功后颁发全局 Token。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="authorizationCode">第三方授权码。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>Token 响应;若授权码无效则返回 null。</returns>
|
||||||
public async Task<PcTokenResponse?> AuthorizeAsync(string? authorizationCode, CancellationToken cancellationToken = default)
|
public async Task<PcTokenResponse?> AuthorizeAsync(string? authorizationCode, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(authorizationCode))
|
if (string.IsNullOrWhiteSpace(authorizationCode))
|
||||||
@@ -33,6 +62,13 @@ namespace Authentication
|
|||||||
return IssueToken(authorizationCode, NormalLifetime, resetTemporaryFailureWindow: true);
|
return IssueToken(authorizationCode, NormalLifetime, resetTemporaryFailureWindow: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 刷新当前 Token,向第三方验证授权引用是否仍然有效。
|
||||||
|
/// 根据第三方返回结果决定是续期、降级为临时 Token 还是清除。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">当前 Token。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>新的 Token 响应;若授权丢失则返回 null。</returns>
|
||||||
public async Task<PcTokenResponse?> RefreshAsync(string? token, CancellationToken cancellationToken = default)
|
public async Task<PcTokenResponse?> RefreshAsync(string? token, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
PcTokenState? current;
|
PcTokenState? current;
|
||||||
@@ -56,6 +92,12 @@ namespace Authentication
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 验证 Token 是否有效,若已过期则尝试自动刷新。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">要验证的 Token。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>Token 是否有效。</returns>
|
||||||
public async Task<bool> ValidateAsync(string? token, CancellationToken cancellationToken = default)
|
public async Task<bool> ValidateAsync(string? token, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
PcTokenState? current;
|
PcTokenState? current;
|
||||||
@@ -76,6 +118,10 @@ namespace Authentication
|
|||||||
return await RefreshAsync(token, cancellationToken) is not null;
|
return await RefreshAsync(token, cancellationToken) is not null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 登出并清除当前 Token。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">要清除的 Token。</param>
|
||||||
public void Logout(string? token)
|
public void Logout(string? token)
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@@ -87,6 +133,13 @@ namespace Authentication
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 颁发新的全局 Token。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="authorizationReference">授权引用标识。</param>
|
||||||
|
/// <param name="lifetime">Token 有效期。</param>
|
||||||
|
/// <param name="resetTemporaryFailureWindow">是否重置暂时失败窗口。</param>
|
||||||
|
/// <returns>包含 Token 和过期时间的响应。</returns>
|
||||||
private PcTokenResponse IssueToken(string authorizationReference, TimeSpan lifetime, bool resetTemporaryFailureWindow)
|
private PcTokenResponse IssueToken(string authorizationReference, TimeSpan lifetime, bool resetTemporaryFailureWindow)
|
||||||
{
|
{
|
||||||
var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
|
var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
|
||||||
@@ -105,6 +158,11 @@ namespace Authentication
|
|||||||
return new PcTokenResponse(token, state.ExpiresAt, SuperRoles);
|
return new PcTokenResponse(token, state.ExpiresAt, SuperRoles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 在第三方暂时失败时刷新 Token。若超出最大容忍窗口则清除 Token。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="current">当前 Token 状态。</param>
|
||||||
|
/// <returns>新的临时 Token 响应;若超出容忍窗口则返回 null。</returns>
|
||||||
private PcTokenResponse? RefreshAfterTemporaryFailure(PcTokenState current)
|
private PcTokenResponse? RefreshAfterTemporaryFailure(PcTokenState current)
|
||||||
{
|
{
|
||||||
var startedAt = current.TemporaryFailureStartedAt ?? DateTime.UtcNow;
|
var startedAt = current.TemporaryFailureStartedAt ?? DateTime.UtcNow;
|
||||||
@@ -116,6 +174,10 @@ namespace Authentication
|
|||||||
return IssueToken(current.AuthorizationReference, TemporaryFailureLifetime, resetTemporaryFailureWindow: false);
|
return IssueToken(current.AuthorizationReference, TemporaryFailureLifetime, resetTemporaryFailureWindow: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清除当前 Token 并返回 null。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>始终返回 null。</returns>
|
||||||
private PcTokenResponse? ClearAndReturnNull()
|
private PcTokenResponse? ClearAndReturnNull()
|
||||||
{
|
{
|
||||||
lock (_syncRoot)
|
lock (_syncRoot)
|
||||||
@@ -126,6 +188,11 @@ namespace Authentication
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查给定 Token 是否与当前持有的 Token 匹配。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">要检查的 Token。</param>
|
||||||
|
/// <returns>是否匹配。</returns>
|
||||||
private bool IsCurrentToken(string? token)
|
private bool IsCurrentToken(string? token)
|
||||||
{
|
{
|
||||||
return !string.IsNullOrWhiteSpace(token) &&
|
return !string.IsNullOrWhiteSpace(token) &&
|
||||||
@@ -133,12 +200,24 @@ namespace Authentication
|
|||||||
string.Equals(_current.TokenHash, HashToken(token), StringComparison.Ordinal);
|
string.Equals(_current.TokenHash, HashToken(token), StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 对 Token 原文进行 SHA256 哈希,返回十六进制字符串。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="token">Token 原文。</param>
|
||||||
|
/// <returns>SHA256 哈希后的十六进制字符串。</returns>
|
||||||
private static string HashToken(string token)
|
private static string HashToken(string token)
|
||||||
{
|
{
|
||||||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token));
|
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(token));
|
||||||
return Convert.ToHexString(bytes);
|
return Convert.ToHexString(bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存当前全局 Token 的内部状态。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="TokenHash">Token 的 SHA256 哈希值。</param>
|
||||||
|
/// <param name="AuthorizationReference">授权引用标识。</param>
|
||||||
|
/// <param name="ExpiresAt">过期时间。</param>
|
||||||
|
/// <param name="TemporaryFailureStartedAt">第三方暂时失败的起始时间。</param>
|
||||||
private sealed record PcTokenState(
|
private sealed record PcTokenState(
|
||||||
string TokenHash,
|
string TokenHash,
|
||||||
string AuthorizationReference,
|
string AuthorizationReference,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<TargetFramework>net10.0</TargetFramework>
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
|
<ApplicationIcon>Assets\avalonia-logo.ico</ApplicationIcon>
|
||||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Avalonia.Controls.WebView" Version="12.0.0" />
|
<PackageReference Include="Avalonia.Controls.WebView" Version="12.0.0" />
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
+17
-4
@@ -14,10 +14,20 @@ using System;
|
|||||||
|
|
||||||
namespace Avalonia_PC
|
namespace Avalonia_PC
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 桌面应用程序入口类,负责配置 DI 容器、初始化数据库和启动 Avalonia 框架。
|
||||||
|
/// </summary>
|
||||||
internal sealed class Program
|
internal sealed class Program
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取全局 DI 服务提供程序。
|
||||||
|
/// </summary>
|
||||||
public static IServiceProvider Services { get; private set; } = null!;
|
public static IServiceProvider Services { get; private set; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用程序主入口点。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">命令行参数。</param>
|
||||||
[STAThread]
|
[STAThread]
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
@@ -40,6 +50,9 @@ namespace Avalonia_PC
|
|||||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配置 DI 容器,注册数据库、业务服务、鉴权服务和统一端点。
|
||||||
|
/// </summary>
|
||||||
private static void ConfigureServices()
|
private static void ConfigureServices()
|
||||||
{
|
{
|
||||||
var services = new ServiceCollection();
|
var services = new ServiceCollection();
|
||||||
@@ -71,13 +84,13 @@ namespace Avalonia_PC
|
|||||||
Services = services.BuildServiceProvider();
|
Services = services.BuildServiceProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avalonia configuration, don't remove; also used by visual designer.
|
/// <summary>
|
||||||
|
/// 构建 Avalonia 应用程序(供可视化设计器使用,请勿删除)。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Avalonia 应用构建器。</returns>
|
||||||
public static AppBuilder BuildAvaloniaApp()
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
=> AppBuilder.Configure<App>()
|
=> AppBuilder.Configure<App>()
|
||||||
.UsePlatformDetect()
|
.UsePlatformDetect()
|
||||||
#if DEBUG
|
|
||||||
.WithDeveloperTools()
|
|
||||||
#endif
|
|
||||||
.WithInterFont()
|
.WithInterFont()
|
||||||
.LogToTrace();
|
.LogToTrace();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,18 @@ namespace Avalonia_PC
|
|||||||
[RequiresUnreferencedCode(
|
[RequiresUnreferencedCode(
|
||||||
"Default implementation of ViewLocator involves reflection which may be trimmed away.",
|
"Default implementation of ViewLocator involves reflection which may be trimmed away.",
|
||||||
Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")]
|
Url = "https://docs.avaloniaui.net/docs/concepts/view-locator")]
|
||||||
|
/// <summary>
|
||||||
|
/// 视图定位器,根据 ViewModel 类型自动查找对应的 View,
|
||||||
|
/// 实现 IDataTemplate 以支持 Avalonia 的数据模板机制。
|
||||||
|
/// </summary>
|
||||||
public class ViewLocator : IDataTemplate
|
public class ViewLocator : IDataTemplate
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 根据 ViewModel 实例构建对应的 View 控件。
|
||||||
|
/// 约定:将 ViewModels 命名空间中的 ViewModel 替换为 Views 命名空间中的同名 View。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="param">ViewModel 实例。</param>
|
||||||
|
/// <returns>对应的 View 控件;若未找到则返回 TextBlock 显示错误信息。</returns>
|
||||||
public Control? Build(object? param)
|
public Control? Build(object? param)
|
||||||
{
|
{
|
||||||
if (param is null)
|
if (param is null)
|
||||||
@@ -30,6 +40,11 @@ namespace Avalonia_PC
|
|||||||
return new TextBlock { Text = "Not Found: " + name };
|
return new TextBlock { Text = "Not Found: " + name };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断数据对象是否为 ViewModel 类型(以 "ViewModel" 结尾)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">要判断的数据对象。</param>
|
||||||
|
/// <returns>是否为 ViewModel。</returns>
|
||||||
public bool Match(object? data)
|
public bool Match(object? data)
|
||||||
{
|
{
|
||||||
return data is ViewModelBase;
|
return data is ViewModelBase;
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
namespace Avalonia_PC.ViewModels
|
namespace Avalonia_PC.ViewModels
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 主窗口的 ViewModel,提供问候语等绑定属性。
|
||||||
|
/// </summary>
|
||||||
public partial class MainWindowViewModel : ViewModelBase
|
public partial class MainWindowViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取问候语文本。
|
||||||
|
/// </summary>
|
||||||
public string Greeting { get; } = "Welcome to Avalonia!";
|
public string Greeting { get; } = "Welcome to Avalonia!";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
namespace Avalonia_PC.ViewModels
|
namespace Avalonia_PC.ViewModels
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// ViewModel 基类,继承自 CommunityToolkit.Mvvm 的 ObservableObject,
|
||||||
|
/// 提供属性变更通知功能。
|
||||||
|
/// </summary>
|
||||||
public abstract class ViewModelBase : ObservableObject
|
public abstract class ViewModelBase : ObservableObject
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
namespace Avalonia_PC.Views
|
namespace Avalonia_PC.Views
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// MainWindow 的分部类,定义注入 WebView2 的 JavaScript Bridge 脚本。
|
||||||
|
/// </summary>
|
||||||
public partial class MainWindow
|
public partial class MainWindow
|
||||||
{
|
{
|
||||||
private const string BridgeScript = """
|
private const string BridgeScript = """
|
||||||
@@ -106,6 +109,9 @@ if (!window.__appBridgeInstalled) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WebView2 Bridge 中的 XMLHttpRequest 替代实现,将 app:// 请求拦截并转为 C# 调用。
|
||||||
|
/// </summary>
|
||||||
class BridgeXMLHttpRequest {
|
class BridgeXMLHttpRequest {
|
||||||
constructor() {
|
constructor() {
|
||||||
this._native = new NativeXMLHttpRequest();
|
this._native = new NativeXMLHttpRequest();
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace Avalonia_PC.Views
|
namespace Avalonia_PC.Views
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// MainWindow 的分部类,负责路由注册和统一端点适配。
|
||||||
|
/// </summary>
|
||||||
public partial class MainWindow
|
public partial class MainWindow
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -22,6 +25,9 @@ namespace Avalonia_PC.Views
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private IServiceProvider _services = null!;
|
private IServiceProvider _services = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从 DI 获取统一端点集合并构建桌面适配器。
|
||||||
|
/// </summary>
|
||||||
private void RegisterRoutes()
|
private void RegisterRoutes()
|
||||||
{
|
{
|
||||||
// 从 DI 获取已构建的端点集合
|
// 从 DI 获取已构建的端点集合
|
||||||
|
|||||||
@@ -12,23 +12,56 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace Avalonia_PC.Views
|
namespace Avalonia_PC.Views
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 主窗口,承载 WebView2 控件并管理前后端 Bridge 通信。
|
||||||
|
/// </summary>
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 自定义协议方案名称。
|
||||||
|
/// </summary>
|
||||||
private const string AppScheme = "app";
|
private const string AppScheme = "app";
|
||||||
|
/// <summary>
|
||||||
|
/// 在线模式下的前端启动 URL。
|
||||||
|
/// </summary>
|
||||||
private const string? OnlineStartupUrl = "http://localhost:51240";
|
private const string? OnlineStartupUrl = "http://localhost:51240";
|
||||||
//private const string? OnlineStartupUrl = null;
|
//private const string? OnlineStartupUrl = null;
|
||||||
|
/// <summary>
|
||||||
|
/// 离线模式下的前端本地文件路径,为空则使用在线模式。
|
||||||
|
/// </summary>
|
||||||
private const string? LocalStartupPath = null;
|
private const string? LocalStartupPath = null;
|
||||||
private static readonly JsonSerializerOptions BridgeJsonSerializerOptions = new()
|
private static readonly JsonSerializerOptions BridgeJsonSerializerOptions = new()
|
||||||
{
|
{
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// WebView2 原生控件实例。
|
||||||
|
/// </summary>
|
||||||
private NativeWebView? _webView;
|
private NativeWebView? _webView;
|
||||||
|
/// <summary>
|
||||||
|
/// 标记 WebView 事件是否已绑定。
|
||||||
|
/// </summary>
|
||||||
private bool _eventsAttached;
|
private bool _eventsAttached;
|
||||||
|
/// <summary>
|
||||||
|
/// WebView 适配器对象。
|
||||||
|
/// </summary>
|
||||||
private object? _webViewAdapter;
|
private object? _webViewAdapter;
|
||||||
|
/// <summary>
|
||||||
|
/// 本地 HTTP 服务器实例(离线模式)。
|
||||||
|
/// </summary>
|
||||||
private HttpListener? _localHttpServer;
|
private HttpListener? _localHttpServer;
|
||||||
|
/// <summary>
|
||||||
|
/// 本地 HTTP 服务器的取消令牌源。
|
||||||
|
/// </summary>
|
||||||
private CancellationTokenSource? _localHttpServerCts;
|
private CancellationTokenSource? _localHttpServerCts;
|
||||||
|
/// <summary>
|
||||||
|
/// 本地 HTTP 服务器的基础 URL。
|
||||||
|
/// </summary>
|
||||||
private string? _localHttpBaseUrl;
|
private string? _localHttpBaseUrl;
|
||||||
|
/// <summary>
|
||||||
|
/// 本地 HTTP 服务器的根目录路径。
|
||||||
|
/// </summary>
|
||||||
private string? _localHttpRoot;
|
private string? _localHttpRoot;
|
||||||
|
|
||||||
#region 生命周期与 WebView 事件
|
#region 生命周期与 WebView 事件
|
||||||
@@ -610,18 +643,39 @@ namespace Avalonia_PC.Views
|
|||||||
|
|
||||||
#region DTO / 路由上下文模型
|
#region DTO / 路由上下文模型
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bridge 通信响应 DTO,用于序列化返回给前端的数据。
|
||||||
|
/// </summary>
|
||||||
private sealed class AppResponse
|
private sealed class AppResponse
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置响应类型标识。
|
||||||
|
/// </summary>
|
||||||
public string Kind { get; set; } = string.Empty;
|
public string Kind { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置请求 ID(对应前端请求)。
|
||||||
|
/// </summary>
|
||||||
public string? Id { get; set; }
|
public string? Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置 HTTP 状态码。
|
||||||
|
/// </summary>
|
||||||
public int StatusCode { get; set; }
|
public int StatusCode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置状态描述文本。
|
||||||
|
/// </summary>
|
||||||
public string StatusMessage { get; set; } = string.Empty;
|
public string StatusMessage { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置响应体 JSON 字符串。
|
||||||
|
/// </summary>
|
||||||
public string Body { get; set; } = string.Empty;
|
public string Body { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取或设置响应头字典。
|
||||||
|
/// </summary>
|
||||||
public Dictionary<string, string> Headers { get; set; } = new();
|
public Dictionary<string, string> Headers { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.7" />
|
||||||
<PackageReference Include="Serilog" Version="4.2.0" />
|
<PackageReference Include="Serilog" Version="4.2.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Avalonia_Services.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 文件流响应 —— 管道检测到此类型时将返回原始文件而非 JSON。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record FileStreamResponse(
|
||||||
|
string FilePath,
|
||||||
|
string FileName,
|
||||||
|
string ContentType,
|
||||||
|
DateTime LastModified);
|
||||||
|
}
|
||||||
@@ -10,9 +10,13 @@ namespace Avalonia_Services.Core
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GlobalExceptionFilter : IEndpointFilter
|
public sealed class GlobalExceptionFilter : IEndpointFilter
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 是否在错误响应中包含异常详情。
|
||||||
|
/// </summary>
|
||||||
private readonly bool _includeDetails;
|
private readonly bool _includeDetails;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// 初始化全局异常过滤器。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="includeDetails">是否在响应中包含异常详情(开发环境建议 true,生产环境 false)</param>
|
/// <param name="includeDetails">是否在响应中包含异常详情(开发环境建议 true,生产环境 false)</param>
|
||||||
public GlobalExceptionFilter(bool includeDetails = false)
|
public GlobalExceptionFilter(bool includeDetails = false)
|
||||||
@@ -20,6 +24,11 @@ namespace Avalonia_Services.Core
|
|||||||
_includeDetails = includeDetails;
|
_includeDetails = includeDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 执行过滤器逻辑:包裹下一个委托,捕获所有未处理异常并转换为统一错误响应。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">请求上下文。</param>
|
||||||
|
/// <param name="next">管道中的下一个委托。</param>
|
||||||
public async Task InvokeAsync(ServiceEndpointContext context, EndpointFilterDelegate next)
|
public async Task InvokeAsync(ServiceEndpointContext context, EndpointFilterDelegate next)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -73,6 +82,11 @@ namespace Avalonia_Services.Core
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 记录异常日志,优先使用 Serilog,不可用时回退到 Console。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">请求上下文。</param>
|
||||||
|
/// <param name="ex">异常对象。</param>
|
||||||
private static void LogException(ServiceEndpointContext context, Exception ex)
|
private static void LogException(ServiceEndpointContext context, Exception ex)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -23,13 +23,14 @@ namespace Avalonia_Services.Core
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class AnonymousAuthService : IAuthService
|
public sealed class AnonymousAuthService : IAuthService
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<ClaimsPrincipal?> AuthenticateAsync(ServiceEndpointContext context)
|
public Task<ClaimsPrincipal?> AuthenticateAsync(ServiceEndpointContext context)
|
||||||
{
|
{
|
||||||
// 匿名用户,始终通过
|
// 匿名用户,始终通过
|
||||||
var identity = new ClaimsIdentity("anonymous");
|
var identity = new ClaimsIdentity("anonymous");
|
||||||
return Task.FromResult<ClaimsPrincipal?>(new ClaimsPrincipal(identity));
|
return Task.FromResult<ClaimsPrincipal?>(new ClaimsPrincipal(identity));
|
||||||
}
|
}
|
||||||
|
/// <inheritdoc />
|
||||||
public Task<bool> AuthorizeAsync(ClaimsPrincipal user, string policy)
|
public Task<bool> AuthorizeAsync(ClaimsPrincipal user, string policy)
|
||||||
{
|
{
|
||||||
return Task.FromResult(true);
|
return Task.FromResult(true);
|
||||||
|
|||||||
@@ -25,13 +25,21 @@ namespace Avalonia_Services.Core
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal sealed class AnonymousEndpointFilter : IEndpointFilter
|
internal sealed class AnonymousEndpointFilter : IEndpointFilter
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 匿名过滤器的委托实现。
|
||||||
|
/// </summary>
|
||||||
private readonly Func<ServiceEndpointContext, EndpointFilterDelegate, Task> _filter;
|
private readonly Func<ServiceEndpointContext, EndpointFilterDelegate, Task> _filter;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 使用匿名函数创建过滤器。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter">过滤器委托。</param>
|
||||||
public AnonymousEndpointFilter(Func<ServiceEndpointContext, EndpointFilterDelegate, Task> filter)
|
public AnonymousEndpointFilter(Func<ServiceEndpointContext, EndpointFilterDelegate, Task> filter)
|
||||||
{
|
{
|
||||||
_filter = filter;
|
_filter = filter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public Task InvokeAsync(ServiceEndpointContext context, EndpointFilterDelegate next)
|
public Task InvokeAsync(ServiceEndpointContext context, EndpointFilterDelegate next)
|
||||||
{
|
{
|
||||||
return _filter(context, next);
|
return _filter(context, next);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Avalonia_Common.Core;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -5,11 +6,17 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
|
|
||||||
namespace Avalonia_Services.Core
|
namespace Avalonia_Services.Core
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 端点挂载的宿主目标。
|
||||||
|
/// </summary>
|
||||||
[Flags]
|
[Flags]
|
||||||
public enum EndpointHostTarget
|
public enum EndpointHostTarget
|
||||||
{
|
{
|
||||||
|
/// <summary>挂载到 Avalonia-API(ASP.NET Core Web API)。</summary>
|
||||||
Api = 1,
|
Api = 1,
|
||||||
|
/// <summary>挂载到 Avalonia-PC(桌面 WebView)。</summary>
|
||||||
Pc = 2,
|
Pc = 2,
|
||||||
|
/// <summary>同时挂载到 API 和 PC。</summary>
|
||||||
All = Api | Pc,
|
All = Api | Pc,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +76,15 @@ namespace Avalonia_Services.Core
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置端点的 OpenAPI 元数据(标签、摘要、描述、请求/响应类型)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="tag">OpenAPI 分组标签。</param>
|
||||||
|
/// <param name="summary">简要摘要。</param>
|
||||||
|
/// <param name="description">详细描述。</param>
|
||||||
|
/// <param name="requestType">请求体类型。</param>
|
||||||
|
/// <param name="responseType">成功响应类型。</param>
|
||||||
|
/// <returns>当前端点实例(Fluent API)。</returns>
|
||||||
public ServiceEndpoint WithOpenApi(
|
public ServiceEndpoint WithOpenApi(
|
||||||
string tag,
|
string tag,
|
||||||
string summary,
|
string summary,
|
||||||
@@ -122,6 +138,11 @@ namespace Avalonia_Services.Core
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 判断端点是否支持指定的宿主目标。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="host">要检查的宿主目标。</param>
|
||||||
|
/// <returns>是否支持。</returns>
|
||||||
public bool SupportsHost(EndpointHostTarget host)
|
public bool SupportsHost(EndpointHostTarget host)
|
||||||
{
|
{
|
||||||
return (HostTarget & host) != 0;
|
return (HostTarget & host) != 0;
|
||||||
@@ -136,6 +157,11 @@ namespace Avalonia_Services.Core
|
|||||||
/// <summary>所有已注册的端点</summary>
|
/// <summary>所有已注册的端点</summary>
|
||||||
public List<ServiceEndpoint> Endpoints { get; } = new();
|
public List<ServiceEndpoint> Endpoints { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取指定宿主目标的所有端点。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="host">宿主目标。</param>
|
||||||
|
/// <returns>匹配的端点集合。</returns>
|
||||||
public IEnumerable<ServiceEndpoint> ForHost(EndpointHostTarget host)
|
public IEnumerable<ServiceEndpoint> ForHost(EndpointHostTarget host)
|
||||||
{
|
{
|
||||||
return Endpoints.Where(endpoint => endpoint.SupportsHost(host));
|
return Endpoints.Where(endpoint => endpoint.SupportsHost(host));
|
||||||
@@ -152,6 +178,21 @@ namespace Avalonia_Services.Core
|
|||||||
return AddEndpoint(pattern, "GET", handler);
|
return AddEndpoint(pattern, "GET", handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个返回统一响应契约的 GET 端点。
|
||||||
|
/// </summary>
|
||||||
|
public ServiceEndpoint MapGet(string pattern, Func<ServiceEndpointContext, Task<IApiResponse>> handler)
|
||||||
|
{
|
||||||
|
return MapGet(pattern, CreateApiResponseHandler(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个带服务依赖注入的 GET 端点。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TService">服务类型。</typeparam>
|
||||||
|
/// <param name="pattern">路由路径。</param>
|
||||||
|
/// <param name="handler">接受服务实例和上下文的处理器。</param>
|
||||||
|
/// <returns>已注册的端点实例。</returns>
|
||||||
public ServiceEndpoint MapGet<TService>(
|
public ServiceEndpoint MapGet<TService>(
|
||||||
string pattern,
|
string pattern,
|
||||||
Func<TService, ServiceEndpointContext, Task<object?>> handler)
|
Func<TService, ServiceEndpointContext, Task<object?>> handler)
|
||||||
@@ -160,6 +201,33 @@ namespace Avalonia_Services.Core
|
|||||||
return MapGet(pattern, CreateServiceHandler(handler));
|
return MapGet(pattern, CreateServiceHandler(handler));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个带服务依赖注入且返回统一响应契约的 GET 端点。
|
||||||
|
/// </summary>
|
||||||
|
public ServiceEndpoint MapGet<TService>(
|
||||||
|
string pattern,
|
||||||
|
Func<TService, ServiceEndpointContext, Task<IApiResponse>> handler)
|
||||||
|
where TService : notnull
|
||||||
|
{
|
||||||
|
return MapGet(pattern, CreateServiceHandler(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个带查询请求 DTO 和服务依赖注入的 GET 端点。
|
||||||
|
/// </summary>
|
||||||
|
public ServiceEndpoint MapGet<TService, TRequest>(
|
||||||
|
string pattern,
|
||||||
|
Func<TService, TRequest, ServiceEndpointContext, Task<IApiResponse>> handler)
|
||||||
|
where TService : notnull
|
||||||
|
{
|
||||||
|
var endpoint = MapGet(
|
||||||
|
pattern,
|
||||||
|
CreateServiceHandler<TService>((service, ctx) =>
|
||||||
|
handler(service, ServiceRequestBinder.BindQuery<TRequest>(ctx), ctx)));
|
||||||
|
endpoint.OpenApiRequestType ??= typeof(TRequest);
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册一个 POST 端点。
|
/// 注册一个 POST 端点。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -168,6 +236,21 @@ namespace Avalonia_Services.Core
|
|||||||
return AddEndpoint(pattern, "POST", handler);
|
return AddEndpoint(pattern, "POST", handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个返回统一响应契约的 POST 端点。
|
||||||
|
/// </summary>
|
||||||
|
public ServiceEndpoint MapPost(string pattern, Func<ServiceEndpointContext, Task<IApiResponse>> handler)
|
||||||
|
{
|
||||||
|
return MapPost(pattern, CreateApiResponseHandler(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个带服务依赖注入的 POST 端点。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TService">服务类型。</typeparam>
|
||||||
|
/// <param name="pattern">路由路径。</param>
|
||||||
|
/// <param name="handler">接受服务实例和上下文的处理器。</param>
|
||||||
|
/// <returns>已注册的端点实例。</returns>
|
||||||
public ServiceEndpoint MapPost<TService>(
|
public ServiceEndpoint MapPost<TService>(
|
||||||
string pattern,
|
string pattern,
|
||||||
Func<TService, ServiceEndpointContext, Task<object?>> handler)
|
Func<TService, ServiceEndpointContext, Task<object?>> handler)
|
||||||
@@ -176,6 +259,33 @@ namespace Avalonia_Services.Core
|
|||||||
return MapPost(pattern, CreateServiceHandler(handler));
|
return MapPost(pattern, CreateServiceHandler(handler));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个带服务依赖注入且返回统一响应契约的 POST 端点。
|
||||||
|
/// </summary>
|
||||||
|
public ServiceEndpoint MapPost<TService>(
|
||||||
|
string pattern,
|
||||||
|
Func<TService, ServiceEndpointContext, Task<IApiResponse>> handler)
|
||||||
|
where TService : notnull
|
||||||
|
{
|
||||||
|
return MapPost(pattern, CreateServiceHandler(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个带 JSON 请求 DTO 和服务依赖注入的 POST 端点。
|
||||||
|
/// </summary>
|
||||||
|
public ServiceEndpoint MapPost<TService, TRequest>(
|
||||||
|
string pattern,
|
||||||
|
Func<TService, TRequest, ServiceEndpointContext, Task<IApiResponse>> handler)
|
||||||
|
where TService : notnull
|
||||||
|
{
|
||||||
|
var endpoint = MapPost(
|
||||||
|
pattern,
|
||||||
|
CreateServiceHandler<TService>((service, ctx) =>
|
||||||
|
handler(service, ServiceRequestBinder.BindBody<TRequest>(ctx), ctx)));
|
||||||
|
endpoint.OpenApiRequestType ??= typeof(TRequest);
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册一个 PUT 端点。
|
/// 注册一个 PUT 端点。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -184,6 +294,21 @@ namespace Avalonia_Services.Core
|
|||||||
return AddEndpoint(pattern, "PUT", handler);
|
return AddEndpoint(pattern, "PUT", handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个返回统一响应契约的 PUT 端点。
|
||||||
|
/// </summary>
|
||||||
|
public ServiceEndpoint MapPut(string pattern, Func<ServiceEndpointContext, Task<IApiResponse>> handler)
|
||||||
|
{
|
||||||
|
return MapPut(pattern, CreateApiResponseHandler(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个带服务依赖注入的 PUT 端点。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TService">服务类型。</typeparam>
|
||||||
|
/// <param name="pattern">路由路径。</param>
|
||||||
|
/// <param name="handler">接受服务实例和上下文的处理器。</param>
|
||||||
|
/// <returns>已注册的端点实例。</returns>
|
||||||
public ServiceEndpoint MapPut<TService>(
|
public ServiceEndpoint MapPut<TService>(
|
||||||
string pattern,
|
string pattern,
|
||||||
Func<TService, ServiceEndpointContext, Task<object?>> handler)
|
Func<TService, ServiceEndpointContext, Task<object?>> handler)
|
||||||
@@ -192,6 +317,33 @@ namespace Avalonia_Services.Core
|
|||||||
return MapPut(pattern, CreateServiceHandler(handler));
|
return MapPut(pattern, CreateServiceHandler(handler));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个带服务依赖注入且返回统一响应契约的 PUT 端点。
|
||||||
|
/// </summary>
|
||||||
|
public ServiceEndpoint MapPut<TService>(
|
||||||
|
string pattern,
|
||||||
|
Func<TService, ServiceEndpointContext, Task<IApiResponse>> handler)
|
||||||
|
where TService : notnull
|
||||||
|
{
|
||||||
|
return MapPut(pattern, CreateServiceHandler(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个带 JSON 请求 DTO 和服务依赖注入的 PUT 端点。
|
||||||
|
/// </summary>
|
||||||
|
public ServiceEndpoint MapPut<TService, TRequest>(
|
||||||
|
string pattern,
|
||||||
|
Func<TService, TRequest, ServiceEndpointContext, Task<IApiResponse>> handler)
|
||||||
|
where TService : notnull
|
||||||
|
{
|
||||||
|
var endpoint = MapPut(
|
||||||
|
pattern,
|
||||||
|
CreateServiceHandler<TService>((service, ctx) =>
|
||||||
|
handler(service, ServiceRequestBinder.BindBody<TRequest>(ctx), ctx)));
|
||||||
|
endpoint.OpenApiRequestType ??= typeof(TRequest);
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 注册一个 DELETE 端点。
|
/// 注册一个 DELETE 端点。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -200,6 +352,21 @@ namespace Avalonia_Services.Core
|
|||||||
return AddEndpoint(pattern, "DELETE", handler);
|
return AddEndpoint(pattern, "DELETE", handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个返回统一响应契约的 DELETE 端点。
|
||||||
|
/// </summary>
|
||||||
|
public ServiceEndpoint MapDelete(string pattern, Func<ServiceEndpointContext, Task<IApiResponse>> handler)
|
||||||
|
{
|
||||||
|
return MapDelete(pattern, CreateApiResponseHandler(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个带服务依赖注入的 DELETE 端点。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TService">服务类型。</typeparam>
|
||||||
|
/// <param name="pattern">路由路径。</param>
|
||||||
|
/// <param name="handler">接受服务实例和上下文的处理器。</param>
|
||||||
|
/// <returns>已注册的端点实例。</returns>
|
||||||
public ServiceEndpoint MapDelete<TService>(
|
public ServiceEndpoint MapDelete<TService>(
|
||||||
string pattern,
|
string pattern,
|
||||||
Func<TService, ServiceEndpointContext, Task<object?>> handler)
|
Func<TService, ServiceEndpointContext, Task<object?>> handler)
|
||||||
@@ -208,6 +375,33 @@ namespace Avalonia_Services.Core
|
|||||||
return MapDelete(pattern, CreateServiceHandler(handler));
|
return MapDelete(pattern, CreateServiceHandler(handler));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个带服务依赖注入且返回统一响应契约的 DELETE 端点。
|
||||||
|
/// </summary>
|
||||||
|
public ServiceEndpoint MapDelete<TService>(
|
||||||
|
string pattern,
|
||||||
|
Func<TService, ServiceEndpointContext, Task<IApiResponse>> handler)
|
||||||
|
where TService : notnull
|
||||||
|
{
|
||||||
|
return MapDelete(pattern, CreateServiceHandler(handler));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 注册一个带查询请求 DTO 和服务依赖注入的 DELETE 端点。
|
||||||
|
/// </summary>
|
||||||
|
public ServiceEndpoint MapDelete<TService, TRequest>(
|
||||||
|
string pattern,
|
||||||
|
Func<TService, TRequest, ServiceEndpointContext, Task<IApiResponse>> handler)
|
||||||
|
where TService : notnull
|
||||||
|
{
|
||||||
|
var endpoint = MapDelete(
|
||||||
|
pattern,
|
||||||
|
CreateServiceHandler<TService>((service, ctx) =>
|
||||||
|
handler(service, ServiceRequestBinder.BindQuery<TRequest>(ctx), ctx)));
|
||||||
|
endpoint.OpenApiRequestType ??= typeof(TRequest);
|
||||||
|
return endpoint;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 添加全局过滤器(作用于所有端点)。
|
/// 添加全局过滤器(作用于所有端点)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -226,6 +420,13 @@ namespace Avalonia_Services.Core
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 内部方法,创建端点并添加到集合。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="pattern">路由路径。</param>
|
||||||
|
/// <param name="method">HTTP 方法。</param>
|
||||||
|
/// <param name="handler">端点处理器。</param>
|
||||||
|
/// <returns>已创建的端点实例。</returns>
|
||||||
private ServiceEndpoint AddEndpoint(string pattern, string method, Func<ServiceEndpointContext, Task<object?>> handler)
|
private ServiceEndpoint AddEndpoint(string pattern, string method, Func<ServiceEndpointContext, Task<object?>> handler)
|
||||||
{
|
{
|
||||||
var endpoint = new ServiceEndpoint
|
var endpoint = new ServiceEndpoint
|
||||||
@@ -238,6 +439,12 @@ namespace Avalonia_Services.Core
|
|||||||
return endpoint;
|
return endpoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建自动从 DI 解析服务实例并调用处理器的委托包装。
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TService">服务类型。</typeparam>
|
||||||
|
/// <param name="handler">接受服务实例和上下文的处理器。</param>
|
||||||
|
/// <returns>包装后的处理器委托。</returns>
|
||||||
private static Func<ServiceEndpointContext, Task<object?>> CreateServiceHandler<TService>(
|
private static Func<ServiceEndpointContext, Task<object?>> CreateServiceHandler<TService>(
|
||||||
Func<TService, ServiceEndpointContext, Task<object?>> handler)
|
Func<TService, ServiceEndpointContext, Task<object?>> handler)
|
||||||
where TService : notnull
|
where TService : notnull
|
||||||
@@ -252,6 +459,33 @@ namespace Avalonia_Services.Core
|
|||||||
return await handler(service, ctx);
|
return await handler(service, ctx);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 将统一响应契约适配为端点集合内部使用的异构响应类型。
|
||||||
|
/// </summary>
|
||||||
|
private static Func<ServiceEndpointContext, Task<object?>> CreateApiResponseHandler(
|
||||||
|
Func<ServiceEndpointContext, Task<IApiResponse>> handler)
|
||||||
|
{
|
||||||
|
return async ctx => await handler(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 为服务端点创建统一响应契约的 DI 包装。
|
||||||
|
/// </summary>
|
||||||
|
private static Func<ServiceEndpointContext, Task<IApiResponse>> CreateServiceHandler<TService>(
|
||||||
|
Func<TService, ServiceEndpointContext, Task<IApiResponse>> handler)
|
||||||
|
where TService : notnull
|
||||||
|
{
|
||||||
|
return async ctx =>
|
||||||
|
{
|
||||||
|
var serviceProvider = ctx.Items["ServiceProvider"] as IServiceProvider
|
||||||
|
?? throw new InvalidOperationException("ServiceProvider 未注入。");
|
||||||
|
|
||||||
|
await using var scope = serviceProvider.CreateAsyncScope();
|
||||||
|
var service = scope.ServiceProvider.GetRequiredService<TService>();
|
||||||
|
return await handler(service, ctx);
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -32,6 +32,11 @@ namespace Avalonia_Services.Core
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Dictionary<string, string> Query { get; init; } = new(StringComparer.OrdinalIgnoreCase);
|
public Dictionary<string, string> Query { get; init; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 路由路径参数。
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, string> RouteValues { get; init; } = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 响应状态码
|
/// 响应状态码
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
namespace Avalonia_Services.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Matches unified endpoint patterns and extracts simple route values.
|
||||||
|
/// </summary>
|
||||||
|
internal static class ServiceEndpointPatternMatcher
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Match literal segments and single-segment route parameters such as {id} or {id:int}.
|
||||||
|
/// </summary>
|
||||||
|
public static bool TryMatch(
|
||||||
|
string pattern,
|
||||||
|
string path,
|
||||||
|
out Dictionary<string, string> routeValues)
|
||||||
|
{
|
||||||
|
routeValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var patternSegments = SplitSegments(pattern);
|
||||||
|
var pathSegments = SplitSegments(path);
|
||||||
|
if (patternSegments.Length != pathSegments.Length)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var index = 0; index < patternSegments.Length; index++)
|
||||||
|
{
|
||||||
|
var patternSegment = patternSegments[index];
|
||||||
|
var pathSegment = pathSegments[index];
|
||||||
|
|
||||||
|
if (TryGetParameterName(patternSegment, out var parameterName))
|
||||||
|
{
|
||||||
|
if (!MatchesConstraint(patternSegment, pathSegment))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
routeValues[parameterName] = Uri.UnescapeDataString(pathSegment);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.Equals(patternSegment, pathSegment, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string[] SplitSegments(string value)
|
||||||
|
{
|
||||||
|
return value.Split('/', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetParameterName(string segment, out string parameterName)
|
||||||
|
{
|
||||||
|
parameterName = string.Empty;
|
||||||
|
if (segment.Length < 3 || segment[0] != '{' || segment[^1] != '}')
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var token = segment[1..^1];
|
||||||
|
var constraintIndex = token.IndexOf(':');
|
||||||
|
parameterName = constraintIndex >= 0 ? token[..constraintIndex] : token;
|
||||||
|
return !string.IsNullOrWhiteSpace(parameterName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesConstraint(string segment, string value)
|
||||||
|
{
|
||||||
|
return !segment.EndsWith(":int}", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| int.TryParse(value, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Avalonia_Services.Core
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Binds unified endpoint request models from JSON bodies or query parameters.
|
||||||
|
/// </summary>
|
||||||
|
internal static class ServiceRequestBinder
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||||
|
{
|
||||||
|
NumberHandling = JsonNumberHandling.AllowReadingFromString,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bind a JSON request body. Empty bodies are treated as an empty JSON object.
|
||||||
|
/// </summary>
|
||||||
|
public static T BindBody<T>(ServiceEndpointContext context)
|
||||||
|
{
|
||||||
|
var json = string.IsNullOrWhiteSpace(context.Body) ? "{}" : context.Body;
|
||||||
|
return Deserialize<T>(json, "body");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bind route and query parameters to a request DTO.
|
||||||
|
/// </summary>
|
||||||
|
public static T BindQuery<T>(ServiceEndpointContext context)
|
||||||
|
{
|
||||||
|
var values = new Dictionary<string, string>(context.Query, StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var routeValue in context.RouteValues)
|
||||||
|
{
|
||||||
|
values[routeValue.Key] = routeValue.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(values, JsonOptions);
|
||||||
|
return Deserialize<T>(json, "query");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static T Deserialize<T>(string json, string source)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JsonSerializer.Deserialize<T>(json, JsonOptions)
|
||||||
|
?? throw new ArgumentException($"Request {source} cannot be bound to {typeof(T).Name}.");
|
||||||
|
}
|
||||||
|
catch (JsonException ex)
|
||||||
|
{
|
||||||
|
throw new ArgumentException($"Request {source} cannot be bound to {typeof(T).Name}.", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,7 +63,7 @@ namespace Avalonia_Services.Endpoints
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 从数据库查询天气预报(优先数据库,回退到内存生成)。
|
/// 从数据库查询天气预报(优先数据库,回退到内存生成)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task<object?> GetWeatherForecastsAsync(ServiceEndpointContext ctx)
|
private static async Task<IApiResponse> GetWeatherForecastsAsync(ServiceEndpointContext ctx)
|
||||||
{
|
{
|
||||||
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
|
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
|
||||||
|
|
||||||
@@ -89,7 +89,12 @@ namespace Avalonia_Services.Endpoints
|
|||||||
return ResponseHelper.Ok(forecasts, "获取天气预报成功(内存生成)");
|
return ResponseHelper.Ok(forecasts, "获取天气预报成功(内存生成)");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<object?> GetUserFromDatabaseAsync(ServiceEndpointContext ctx)
|
/// <summary>
|
||||||
|
/// 从数据库获取用户信息(演示数据库查询),若无数据则返回演示用户。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ctx">服务端点上下文。</param>
|
||||||
|
/// <returns>用户信息。</returns>
|
||||||
|
private static async Task<IApiResponse> GetUserFromDatabaseAsync(ServiceEndpointContext ctx)
|
||||||
{
|
{
|
||||||
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
|
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
|
||||||
|
|
||||||
@@ -109,7 +114,12 @@ namespace Avalonia_Services.Endpoints
|
|||||||
return ResponseHelper.Ok(user);
|
return ResponseHelper.Ok(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<object?> ProcessDataAsync(ServiceEndpointContext ctx)
|
/// <summary>
|
||||||
|
/// 处理前端发送的数据(POST 演示),将数据存入数据库或转为大写返回。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ctx">服务端点上下文。</param>
|
||||||
|
/// <returns>处理结果。</returns>
|
||||||
|
private static async Task<IApiResponse> ProcessDataAsync(ServiceEndpointContext ctx)
|
||||||
{
|
{
|
||||||
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
|
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
|
||||||
|
|
||||||
|
|||||||
@@ -8,42 +8,62 @@ namespace Avalonia_Services.Endpoints
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static class AuthEndpoints
|
public static class AuthEndpoints
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 配置 API 端鉴权端点(登录、刷新、登出)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">端点构建器。</param>
|
||||||
public static void ConfigureApi(ServiceEndpointBuilder builder)
|
public static void ConfigureApi(ServiceEndpointBuilder builder)
|
||||||
{
|
{
|
||||||
builder.ConfigureEndpoints(endpoints =>
|
builder.ConfigureEndpoints(endpoints =>
|
||||||
{
|
{
|
||||||
endpoints.MapPost<IApiAuthEndpointService>("api/auth/login", (service, ctx) => service.LoginAsync(ctx))
|
endpoints.MapPost<IApiAuthEndpointService, ApiLoginRequest>(
|
||||||
|
"api/auth/login",
|
||||||
|
(service, request, ctx) => service.LoginAsync(request, ctx))
|
||||||
.WithName("ApiLogin")
|
.WithName("ApiLogin")
|
||||||
.WithOpenApi("Auth", "API 登录,返回 access token 和 refresh token。", "", typeof(ApiLoginRequest), typeof(AuthTokenResponse))
|
.WithOpenApi("Auth", "API 登录,返回 access token 和 refresh token。", "", typeof(ApiLoginRequest), typeof(AuthTokenResponse))
|
||||||
.ApiOnly();
|
.ApiOnly();
|
||||||
|
|
||||||
endpoints.MapPost<IApiAuthEndpointService>("api/auth/refresh", (service, ctx) => service.RefreshAsync(ctx))
|
endpoints.MapPost<IApiAuthEndpointService, ApiRefreshTokenRequest>(
|
||||||
|
"api/auth/refresh",
|
||||||
|
(service, request, ctx) => service.RefreshAsync(request, ctx))
|
||||||
.WithName("ApiRefresh")
|
.WithName("ApiRefresh")
|
||||||
.WithOpenApi("Auth", "API refresh token 轮换。", "", typeof(ApiRefreshTokenRequest), typeof(AuthTokenResponse))
|
.WithOpenApi("Auth", "API refresh token 轮换。", "", typeof(ApiRefreshTokenRequest), typeof(AuthTokenResponse))
|
||||||
.ApiOnly();
|
.ApiOnly();
|
||||||
|
|
||||||
endpoints.MapPost<IApiAuthEndpointService>("api/auth/logout", (service, ctx) => service.LogoutAsync(ctx))
|
endpoints.MapPost<IApiAuthEndpointService, ApiLogoutRequest>(
|
||||||
|
"api/auth/logout",
|
||||||
|
(service, request, ctx) => service.LogoutAsync(request, ctx))
|
||||||
.WithName("ApiLogout")
|
.WithName("ApiLogout")
|
||||||
.WithOpenApi("Auth", "API 退出登录并吊销 refresh token。", "", typeof(ApiLogoutRequest))
|
.WithOpenApi("Auth", "API 退出登录并吊销 refresh token。", "", typeof(ApiLogoutRequest))
|
||||||
.ApiOnly();
|
.ApiOnly();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 配置 PC 端鉴权端点(授权码登录、刷新、登出)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">端点构建器。</param>
|
||||||
public static void ConfigurePc(ServiceEndpointBuilder builder)
|
public static void ConfigurePc(ServiceEndpointBuilder builder)
|
||||||
{
|
{
|
||||||
builder.ConfigureEndpoints(endpoints =>
|
builder.ConfigureEndpoints(endpoints =>
|
||||||
{
|
{
|
||||||
endpoints.MapPost<IPcAuthEndpointService>("api/pc/auth/authorize", (service, ctx) => service.AuthorizeAsync(ctx))
|
endpoints.MapPost<IPcAuthEndpointService, PcAuthorizeRequest>(
|
||||||
|
"api/pc/auth/authorize",
|
||||||
|
(service, request, ctx) => service.AuthorizeAsync(request, ctx))
|
||||||
.WithName("PcAuthorize")
|
.WithName("PcAuthorize")
|
||||||
.WithOpenApi("Auth", "PC 授权码登录,生成本地全局 token。", "", typeof(PcAuthorizeRequest), typeof(PcTokenResponse))
|
.WithOpenApi("Auth", "PC 授权码登录,生成本地全局 token。", "", typeof(PcAuthorizeRequest), typeof(PcTokenResponse))
|
||||||
.PcOnly();
|
.PcOnly();
|
||||||
|
|
||||||
endpoints.MapPost<IPcAuthEndpointService>("api/pc/auth/refresh", (service, ctx) => service.RefreshAsync(ctx))
|
endpoints.MapPost<IPcAuthEndpointService, PcRefreshRequest>(
|
||||||
|
"api/pc/auth/refresh",
|
||||||
|
(service, request, ctx) => service.RefreshAsync(request, ctx))
|
||||||
.WithName("PcRefresh")
|
.WithName("PcRefresh")
|
||||||
.WithOpenApi("Auth", "PC 全局 token 刷新。", "", typeof(PcRefreshRequest), typeof(PcTokenResponse))
|
.WithOpenApi("Auth", "PC 全局 token 刷新。", "", typeof(PcRefreshRequest), typeof(PcTokenResponse))
|
||||||
.PcOnly();
|
.PcOnly();
|
||||||
|
|
||||||
endpoints.MapPost<IPcAuthEndpointService>("api/pc/auth/logout", (service, ctx) => service.LogoutAsync(ctx))
|
endpoints.MapPost<IPcAuthEndpointService, PcLogoutRequest>(
|
||||||
|
"api/pc/auth/logout",
|
||||||
|
(service, request, ctx) => service.LogoutAsync(request, ctx))
|
||||||
.WithName("PcLogout")
|
.WithName("PcLogout")
|
||||||
.WithOpenApi("Auth", "PC 退出登录。", "", typeof(PcLogoutRequest))
|
.WithOpenApi("Auth", "PC 退出登录。", "", typeof(PcLogoutRequest))
|
||||||
.PcOnly();
|
.PcOnly();
|
||||||
|
|||||||
@@ -8,8 +8,17 @@ namespace Avalonia_Services.Extensions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class DesktopEndpointAdapter
|
public class DesktopEndpointAdapter
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 统一端点集合。
|
||||||
|
/// </summary>
|
||||||
private readonly ServiceEndpointCollection _endpoints;
|
private readonly ServiceEndpointCollection _endpoints;
|
||||||
|
/// <summary>
|
||||||
|
/// 鉴权服务。
|
||||||
|
/// </summary>
|
||||||
private readonly IAuthService _authService;
|
private readonly IAuthService _authService;
|
||||||
|
/// <summary>
|
||||||
|
/// DI 服务提供程序。
|
||||||
|
/// </summary>
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -17,12 +26,33 @@ namespace Avalonia_Services.Extensions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class RouteResult
|
public class RouteResult
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 获取是否匹配到路由。
|
||||||
|
/// </summary>
|
||||||
public bool IsMatched { get; init; }
|
public bool IsMatched { get; init; }
|
||||||
|
/// <summary>
|
||||||
|
/// 获取 HTTP 状态码。
|
||||||
|
/// </summary>
|
||||||
public int StatusCode { get; init; } = 200;
|
public int StatusCode { get; init; } = 200;
|
||||||
public string StatusMessage { get; init; } = "OK";
|
/// <summary>
|
||||||
|
/// 获取状态描述文本。
|
||||||
|
/// </summary>
|
||||||
|
public string StatusMessage { get; init; } = "";
|
||||||
|
/// <summary>
|
||||||
|
/// 获取响应数据。
|
||||||
|
/// </summary>
|
||||||
public object? Data { get; init; }
|
public object? Data { get; init; }
|
||||||
|
/// <summary>
|
||||||
|
/// 获取响应头字典。
|
||||||
|
/// </summary>
|
||||||
public Dictionary<string, string> ResponseHeaders { get; init; } = new();
|
public Dictionary<string, string> ResponseHeaders { get; init; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建成功响应结果。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="data">响应数据。</param>
|
||||||
|
/// <param name="ctx">端点上下文。</param>
|
||||||
|
/// <returns>路由结果。</returns>
|
||||||
public static RouteResult Success(object? data, ServiceEndpointContext ctx)
|
public static RouteResult Success(object? data, ServiceEndpointContext ctx)
|
||||||
{
|
{
|
||||||
return new RouteResult
|
return new RouteResult
|
||||||
@@ -35,6 +65,10 @@ namespace Avalonia_Services.Extensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建 404 未找到响应。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>表示未匹配的路由结果。</returns>
|
||||||
public static RouteResult NotFound() => new()
|
public static RouteResult NotFound() => new()
|
||||||
{
|
{
|
||||||
IsMatched = false,
|
IsMatched = false,
|
||||||
@@ -43,6 +77,12 @@ namespace Avalonia_Services.Extensions
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 初始化桌面端点适配器。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="endpoints">端点集合。</param>
|
||||||
|
/// <param name="authService">鉴权服务。</param>
|
||||||
|
/// <param name="serviceProvider">DI 服务提供程序。</param>
|
||||||
public DesktopEndpointAdapter(
|
public DesktopEndpointAdapter(
|
||||||
ServiceEndpointCollection endpoints,
|
ServiceEndpointCollection endpoints,
|
||||||
IAuthService authService,
|
IAuthService authService,
|
||||||
@@ -69,10 +109,19 @@ namespace Avalonia_Services.Extensions
|
|||||||
Dictionary<string, string>? query = null)
|
Dictionary<string, string>? query = null)
|
||||||
{
|
{
|
||||||
// 查找匹配的端点(忽略大小写 + 方法匹配)
|
// 查找匹配的端点(忽略大小写 + 方法匹配)
|
||||||
var endpoint = _endpoints.Endpoints.FirstOrDefault(e =>
|
var match = _endpoints.Endpoints
|
||||||
e.SupportsHost(EndpointHostTarget.Pc) &&
|
.Where(e =>
|
||||||
string.Equals(e.Pattern, path, StringComparison.OrdinalIgnoreCase) &&
|
e.SupportsHost(EndpointHostTarget.Pc) &&
|
||||||
string.Equals(e.HttpMethod, method, StringComparison.OrdinalIgnoreCase));
|
string.Equals(e.HttpMethod, method, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Select(e => new
|
||||||
|
{
|
||||||
|
Endpoint = e,
|
||||||
|
IsMatched = ServiceEndpointPatternMatcher.TryMatch(e.Pattern, path, out var routeValues),
|
||||||
|
RouteValues = routeValues,
|
||||||
|
})
|
||||||
|
.FirstOrDefault(candidate => candidate.IsMatched);
|
||||||
|
|
||||||
|
var endpoint = match?.Endpoint;
|
||||||
|
|
||||||
if (endpoint is null)
|
if (endpoint is null)
|
||||||
{
|
{
|
||||||
@@ -87,6 +136,7 @@ namespace Avalonia_Services.Extensions
|
|||||||
Body = body,
|
Body = body,
|
||||||
Headers = headers ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
|
Headers = headers ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
|
||||||
Query = query ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
|
Query = query ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
|
||||||
|
RouteValues = match!.RouteValues,
|
||||||
Items = { ["ServiceProvider"] = _serviceProvider },
|
Items = { ["ServiceProvider"] = _serviceProvider },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,33 @@
|
|||||||
namespace Avalonia_Services.Services.AuthService
|
namespace Avalonia_Services.Services.AuthService
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// API 登录请求。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Account">账号(邮箱或用户名)。</param>
|
||||||
|
/// <param name="Password">密码。</param>
|
||||||
|
/// <param name="Roles">请求的角色列表。</param>
|
||||||
public sealed record ApiLoginRequest(string? Account, string? Password, string[]? Roles = null);
|
public sealed record ApiLoginRequest(string? Account, string? Password, string[]? Roles = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// API Refresh Token 请求。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="RefreshToken">刷新令牌。</param>
|
||||||
public sealed record ApiRefreshTokenRequest(string? RefreshToken);
|
public sealed record ApiRefreshTokenRequest(string? RefreshToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// API 登出请求。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="RefreshToken">要撤销的刷新令牌。</param>
|
||||||
public sealed record ApiLogoutRequest(string? RefreshToken);
|
public sealed record ApiLogoutRequest(string? RefreshToken);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 认证 Token 响应,包含 Access Token 和 Refresh Token 及其过期时间。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="AccessToken">访问令牌。</param>
|
||||||
|
/// <param name="RefreshToken">刷新令牌。</param>
|
||||||
|
/// <param name="AccessTokenExpiresAt">访问令牌过期时间。</param>
|
||||||
|
/// <param name="RefreshTokenExpiresAt">刷新令牌过期时间。</param>
|
||||||
|
/// <param name="Roles">用户角色列表。</param>
|
||||||
public sealed record AuthTokenResponse(
|
public sealed record AuthTokenResponse(
|
||||||
string AccessToken,
|
string AccessToken,
|
||||||
string RefreshToken,
|
string RefreshToken,
|
||||||
@@ -13,25 +35,64 @@ namespace Avalonia_Services.Services.AuthService
|
|||||||
DateTime RefreshTokenExpiresAt,
|
DateTime RefreshTokenExpiresAt,
|
||||||
string[] Roles);
|
string[] Roles);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PC 端授权码登录请求。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="AuthorizationCode">第三方授权码。</param>
|
||||||
public sealed record PcAuthorizeRequest(string? AuthorizationCode);
|
public sealed record PcAuthorizeRequest(string? AuthorizationCode);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PC 端 Token 刷新请求。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Token">当前 Token。</param>
|
||||||
public sealed record PcRefreshRequest(string? Token);
|
public sealed record PcRefreshRequest(string? Token);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PC 端登出请求。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Token">要清除的 Token。</param>
|
||||||
public sealed record PcLogoutRequest(string? Token);
|
public sealed record PcLogoutRequest(string? Token);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PC 端 Token 响应。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Token">访问令牌。</param>
|
||||||
|
/// <param name="ExpiresAt">过期时间。</param>
|
||||||
|
/// <param name="Roles">用户角色列表。</param>
|
||||||
public sealed record PcTokenResponse(string Token, DateTime ExpiresAt, string[] Roles);
|
public sealed record PcTokenResponse(string Token, DateTime ExpiresAt, string[] Roles);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 第三方授权检查结果。
|
||||||
|
/// </summary>
|
||||||
public enum ThirdPartyAuthCheckResult
|
public enum ThirdPartyAuthCheckResult
|
||||||
{
|
{
|
||||||
|
/// <summary>授权有效。</summary>
|
||||||
Valid,
|
Valid,
|
||||||
|
/// <summary>授权已丢失。</summary>
|
||||||
AuthorizationLost,
|
AuthorizationLost,
|
||||||
|
/// <summary>暂时性失败。</summary>
|
||||||
TemporaryFailure,
|
TemporaryFailure,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 第三方授权客户端接口,用于验证和刷新第三方授权。
|
||||||
|
/// </summary>
|
||||||
public interface IPcThirdPartyAuthorizationClient
|
public interface IPcThirdPartyAuthorizationClient
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 验证第三方授权码是否有效。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="authorizationCode">第三方授权码。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>授权检查结果。</returns>
|
||||||
Task<ThirdPartyAuthCheckResult> ValidateAuthorizationCodeAsync(string authorizationCode, CancellationToken cancellationToken = default);
|
Task<ThirdPartyAuthCheckResult> ValidateAuthorizationCodeAsync(string authorizationCode, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 刷新第三方授权。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="authorizationReference">授权引用标识。</param>
|
||||||
|
/// <param name="cancellationToken">取消令牌。</param>
|
||||||
|
/// <returns>授权检查结果。</returns>
|
||||||
Task<ThirdPartyAuthCheckResult> RefreshAuthorizationAsync(string authorizationReference, CancellationToken cancellationToken = default);
|
Task<ThirdPartyAuthCheckResult> RefreshAuthorizationAsync(string authorizationReference, CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,60 @@
|
|||||||
|
using Avalonia_Common.Core;
|
||||||
using Avalonia_Services.Core;
|
using Avalonia_Services.Core;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Avalonia_Services.Services.AuthService
|
namespace Avalonia_Services.Services.AuthService
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// API 鉴权端点服务接口,定义登录、刷新 Token 和登出操作。
|
||||||
|
/// </summary>
|
||||||
public interface IApiAuthEndpointService
|
public interface IApiAuthEndpointService
|
||||||
{
|
{
|
||||||
Task<object?> LoginAsync(ServiceEndpointContext ctx);
|
/// <summary>
|
||||||
|
/// 处理用户登录请求。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ctx">服务端点上下文。</param>
|
||||||
|
/// <returns>包含 Token 的认证响应。</returns>
|
||||||
|
Task<IApiResponse> LoginAsync(ApiLoginRequest request, ServiceEndpointContext ctx);
|
||||||
|
|
||||||
Task<object?> RefreshAsync(ServiceEndpointContext ctx);
|
/// <summary>
|
||||||
|
/// 使用 Refresh Token 刷新 Access Token。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ctx">服务端点上下文。</param>
|
||||||
|
/// <returns>新的 Token 对。</returns>
|
||||||
|
Task<IApiResponse> RefreshAsync(ApiRefreshTokenRequest request, ServiceEndpointContext ctx);
|
||||||
|
|
||||||
Task<object?> LogoutAsync(ServiceEndpointContext ctx);
|
/// <summary>
|
||||||
|
/// 处理用户登出请求。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ctx">服务端点上下文。</param>
|
||||||
|
/// <returns>登出结果。</returns>
|
||||||
|
Task<IApiResponse> LogoutAsync(ApiLogoutRequest request, ServiceEndpointContext ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PC 端鉴权端点服务接口,定义授权码登录、Token 刷新和登出操作。
|
||||||
|
/// </summary>
|
||||||
public interface IPcAuthEndpointService
|
public interface IPcAuthEndpointService
|
||||||
{
|
{
|
||||||
Task<object?> AuthorizeAsync(ServiceEndpointContext ctx);
|
/// <summary>
|
||||||
|
/// 使用授权码进行登录授权。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ctx">服务端点上下文。</param>
|
||||||
|
/// <returns>包含 Token 的认证响应。</returns>
|
||||||
|
Task<IApiResponse> AuthorizeAsync(PcAuthorizeRequest request, ServiceEndpointContext ctx);
|
||||||
|
|
||||||
Task<object?> RefreshAsync(ServiceEndpointContext ctx);
|
/// <summary>
|
||||||
|
/// 刷新当前 Token。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ctx">服务端点上下文。</param>
|
||||||
|
/// <returns>新的 Token 响应。</returns>
|
||||||
|
Task<IApiResponse> RefreshAsync(PcRefreshRequest request, ServiceEndpointContext ctx);
|
||||||
|
|
||||||
Task<object?> LogoutAsync(ServiceEndpointContext ctx);
|
/// <summary>
|
||||||
|
/// 处理用户登出请求。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ctx">服务端点上下文。</param>
|
||||||
|
/// <returns>登出结果。</returns>
|
||||||
|
Task<IApiResponse> LogoutAsync(PcLogoutRequest request, ServiceEndpointContext ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ using Avalonia_EFCore.Models;
|
|||||||
|
|
||||||
namespace Avalonia_Services.Services
|
namespace Avalonia_Services.Services
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 天气预报服务,随机生成未来 5 天的天气预报数据。
|
||||||
|
/// </summary>
|
||||||
public class WeatherForecastService
|
public class WeatherForecastService
|
||||||
{
|
{
|
||||||
private static readonly string[] Summaries =
|
private static readonly string[] Summaries =
|
||||||
@@ -9,6 +12,10 @@ namespace Avalonia_Services.Services
|
|||||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 生成未来 5 天的随机天气预报数据。
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>天气预报数据集合。</returns>
|
||||||
public IEnumerable<WeatherForecast> GetWeatherForecasts()
|
public IEnumerable<WeatherForecast> GetWeatherForecasts()
|
||||||
{
|
{
|
||||||
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="Avalonia-API/Avalonia-API.csproj" Id="e33aba9a-a56b-4f6b-8eaa-3acbed65ebad" />
|
||||||
|
<Project Path="Avalonia-Common/Avalonia-Common.csproj" Id="caed4118-2161-4382-90b8-35fb4efe3b5f" />
|
||||||
|
<Project Path="Avalonia-EFCore/Avalonia-EFCore.csproj" Id="64557501-62a7-4863-b2bf-1570b8c6fecb" />
|
||||||
|
<Project Path="Avalonia-Services/Avalonia-Services.csproj" Id="b8757cf9-5422-4c67-acae-3c967c95f866" />
|
||||||
|
<Project Path="avalonia-web-react/avalonia-web-react.esproj">
|
||||||
|
<Build />
|
||||||
|
<Deploy />
|
||||||
|
</Project>
|
||||||
|
<Project Path="Avalonia-Web-VUE/avalonia-web-vue.esproj">
|
||||||
|
<Build />
|
||||||
|
<Deploy />
|
||||||
|
</Project>
|
||||||
|
<Project Path="Avalonia-PC/Avalonia-PC.csproj" />
|
||||||
|
</Solution>
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<PackageId>Qiang.Avalonia.Stack.Templates</PackageId>
|
||||||
|
<PackageVersion>1.0.0</PackageVersion>
|
||||||
|
<Title>Qiang Avalonia Stack Templates</Title>
|
||||||
|
<Authors>Qiang</Authors>
|
||||||
|
<Description>Project templates for the Qiang Avalonia Stack.</Description>
|
||||||
|
<PackageTags>dotnet-new;templates;avalonia;aspnetcore</PackageTags>
|
||||||
|
<PackageType>Template</PackageType>
|
||||||
|
<IncludeContentInPack>true</IncludeContentInPack>
|
||||||
|
<IncludeBuildOutput>false</IncludeBuildOutput>
|
||||||
|
<ContentTargetFolders>content</ContentTargetFolders>
|
||||||
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
|
<NoDefaultExcludes>true</NoDefaultExcludes>
|
||||||
|
<NoWarn>$(NoWarn);NU5110;NU5111;NU5128</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Content Include="**\*" Exclude=".git\**;.vs\**;.template-hive\**;**\.vs\**;**\bin\**;**\logs\**;**\node_modules\**;**\obj\**;package-output\**;package-scripts\tools\**" />
|
||||||
|
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||||
|
<Compile Remove="**\*" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Avalonia Stack
|
||||||
|
|
||||||
|
This repository can be used as a `dotnet new` project template. The template
|
||||||
|
replaces the `Avalonia` prefix in solution files, project files, folder names,
|
||||||
|
project references, C# namespaces, packaging scripts, and frontend names when a
|
||||||
|
new project is created.
|
||||||
|
|
||||||
|
Use a C# namespace friendly project name such as `DemoApp` or `QiChengApp`.
|
||||||
|
Avoid names with `-`, spaces, or a leading digit because the generated name is
|
||||||
|
also used inside C# namespaces.
|
||||||
|
|
||||||
|
## Generate from a local clone
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet new install .
|
||||||
|
dotnet new Qiang-avalonia-stack -n DemoApp -o ..\DemoApp
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated solution entry point is `DemoApp-Stack.slnx`. Project folders,
|
||||||
|
project files, and generated C# namespaces keep the project name casing from
|
||||||
|
`-n`.
|
||||||
|
|
||||||
|
## Build an installable template package
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet pack .\Avalonia.Stack.TemplatePack.csproj -c Release
|
||||||
|
dotnet new install .\bin\Release\QiCheng.Avalonia.Stack.Templates.1.0.0.nupkg
|
||||||
|
dotnet new Qiang-avalonia-stack -n DemoApp -o .\DemoApp
|
||||||
|
```
|
||||||
|
|
||||||
|
Publish the generated `.nupkg` to a private or public NuGet feed when you want
|
||||||
|
to create named projects without cloning this template repository first.
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#ifndef AppName
|
||||||
|
#define AppName "Avalonia-PC"
|
||||||
|
#endif
|
||||||
|
#ifndef AppVersion
|
||||||
|
#define AppVersion "1.0.0"
|
||||||
|
#endif
|
||||||
|
#ifndef AppPublisher
|
||||||
|
#define AppPublisher "QiCheng"
|
||||||
|
#endif
|
||||||
|
#ifndef AppExeName
|
||||||
|
#define AppExeName "Avalonia-PC.exe"
|
||||||
|
#endif
|
||||||
|
#ifndef SourceDir
|
||||||
|
#define SourceDir "..\..\package-output\publish\Avalonia-PC\win-x64"
|
||||||
|
#endif
|
||||||
|
#ifndef OutputDir
|
||||||
|
#define OutputDir "..\..\package-output\installer"
|
||||||
|
#endif
|
||||||
|
#ifndef RepoRoot
|
||||||
|
#define RepoRoot "..\.."
|
||||||
|
#endif
|
||||||
|
#ifndef ChineseLanguageFile
|
||||||
|
#define ChineseLanguageFile "compiler:Default.isl"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
[Setup]
|
||||||
|
AppId={{7E41DD4C-FBF3-4C65-8D9F-4F2D794BC284}
|
||||||
|
AppName={#AppName}
|
||||||
|
AppVersion={#AppVersion}
|
||||||
|
AppPublisher={#AppPublisher}
|
||||||
|
DefaultDirName={autopf}\{#AppName}
|
||||||
|
DefaultGroupName={#AppName}
|
||||||
|
OutputDir={#OutputDir}
|
||||||
|
OutputBaseFilename={#AppName}-Setup-{#AppVersion}-win-x64
|
||||||
|
SetupIconFile={#RepoRoot}\Avalonia-PC\Assets\avalonia-logo.ico
|
||||||
|
Compression=lzma2
|
||||||
|
SolidCompression=yes
|
||||||
|
WizardStyle=modern
|
||||||
|
PrivilegesRequired=admin
|
||||||
|
ArchitecturesAllowed=x64compatible
|
||||||
|
ArchitecturesInstallIn64BitMode=x64compatible
|
||||||
|
DisableProgramGroupPage=yes
|
||||||
|
UninstallDisplayIcon={app}\{#AppExeName}
|
||||||
|
|
||||||
|
[Languages]
|
||||||
|
Name: "chinesesimp"; MessagesFile: "{#ChineseLanguageFile}"
|
||||||
|
|
||||||
|
[Tasks]
|
||||||
|
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
|
||||||
|
|
||||||
|
[Files]
|
||||||
|
Source: "{#SourceDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||||
|
|
||||||
|
[Icons]
|
||||||
|
Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"
|
||||||
|
Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Tasks: desktopicon
|
||||||
|
|
||||||
|
[Run]
|
||||||
|
Filename: "{app}\{#AppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(AppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
cd /d "%~dp0.."
|
||||||
|
|
||||||
|
set "APP_VERSION=1.0.0"
|
||||||
|
set "APP_NAME=Avalonia-PC"
|
||||||
|
set "APP_PUBLISHER=QiCheng"
|
||||||
|
|
||||||
|
echo Packaging %APP_NAME% %APP_VERSION% for Windows PC...
|
||||||
|
echo.
|
||||||
|
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0package-pc.ps1" -Version "%APP_VERSION%" -AppName "%APP_NAME%" -Publisher "%APP_PUBLISHER%" -SingleFile -InstallInnoSetupIfMissing
|
||||||
|
|
||||||
|
set "EXIT_CODE=%ERRORLEVEL%"
|
||||||
|
echo.
|
||||||
|
|
||||||
|
if "%EXIT_CODE%"=="0" (
|
||||||
|
echo Done.
|
||||||
|
echo Installer output: %CD%\package-output\installer
|
||||||
|
) else if "%EXIT_CODE%"=="2" (
|
||||||
|
echo Publish completed, but installer was not created because Inno Setup 6 is not installed.
|
||||||
|
echo This BAT can download Inno Setup into package-scripts\tools. Run it again and allow network access.
|
||||||
|
echo.
|
||||||
|
echo Publish output: %CD%\package-output\publish\Avalonia-PC
|
||||||
|
) else (
|
||||||
|
echo Packaging failed. Exit code: %EXIT_CODE%
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string]$Configuration = "Release",
|
||||||
|
[string]$Runtime = "win-x64",
|
||||||
|
[string]$Version = "1.0.0",
|
||||||
|
[string]$AppName = "Avalonia-PC",
|
||||||
|
[string]$Publisher = "QiCheng",
|
||||||
|
[bool]$SelfContained = $true,
|
||||||
|
[switch]$SingleFile,
|
||||||
|
[switch]$InstallInnoSetupIfMissing,
|
||||||
|
[switch]$SkipInstaller
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
$projectPath = Join-Path $repoRoot "Avalonia-PC\Avalonia-PC.csproj"
|
||||||
|
$installerScript = Join-Path $PSScriptRoot "installer\Avalonia-PC.iss"
|
||||||
|
$buildStamp = Get-Date -Format "yyyyMMddHHmmss"
|
||||||
|
$publishDir = Join-Path $repoRoot "package-output\publish\Avalonia-PC\$Runtime-$buildStamp"
|
||||||
|
$installerDir = Join-Path $repoRoot "package-output\installer"
|
||||||
|
$appExeName = "Avalonia-PC.exe"
|
||||||
|
$toolsDir = Join-Path $PSScriptRoot "tools"
|
||||||
|
$innoSetupDir = Join-Path $toolsDir "InnoSetup6"
|
||||||
|
$innoSetupInstaller = Join-Path $toolsDir "downloads\innosetup-6.7.2.exe"
|
||||||
|
$innoSetupDownloadUrl = "https://github.com/jrsoftware/issrc/releases/download/is-6_7_2/innosetup-6.7.2.exe"
|
||||||
|
$chineseSimplifiedLanguageFile = Join-Path $innoSetupDir "Languages\ChineseSimplified.isl"
|
||||||
|
$chineseSimplifiedLanguageUrl = "https://raw.githubusercontent.com/kira-96/Inno-Setup-Chinese-Simplified-Translation/main/ChineseSimplified.isl"
|
||||||
|
|
||||||
|
function Find-InnoSetupCompiler {
|
||||||
|
$localCompiler = Join-Path $innoSetupDir "ISCC.exe"
|
||||||
|
if (Test-Path $localCompiler) {
|
||||||
|
return $localCompiler
|
||||||
|
}
|
||||||
|
|
||||||
|
$command = Get-Command "iscc" -ErrorAction SilentlyContinue
|
||||||
|
if ($command) {
|
||||||
|
return $command.Source
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidates = @(
|
||||||
|
"${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe",
|
||||||
|
"$env:ProgramFiles\Inno Setup 6\ISCC.exe",
|
||||||
|
"$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe"
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($candidate in $candidates) {
|
||||||
|
if ($candidate -and (Test-Path $candidate)) {
|
||||||
|
return $candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Install-LocalInnoSetup {
|
||||||
|
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $innoSetupInstaller), $innoSetupDir | Out-Null
|
||||||
|
|
||||||
|
if (-not (Test-Path $innoSetupInstaller)) {
|
||||||
|
Write-Host "Downloading Inno Setup 6 to: $innoSetupInstaller"
|
||||||
|
Invoke-WebRequest -Uri $innoSetupDownloadUrl -OutFile $innoSetupInstaller
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Installing local Inno Setup 6 to: $innoSetupDir"
|
||||||
|
& $innoSetupInstaller /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /CURRENTUSER /DIR="$innoSetupDir"
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Inno Setup local install failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-ChineseSimplifiedLanguageFile {
|
||||||
|
if (Test-Path $chineseSimplifiedLanguageFile) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$languageDir = Split-Path -Parent $chineseSimplifiedLanguageFile
|
||||||
|
New-Item -ItemType Directory -Force -Path $languageDir | Out-Null
|
||||||
|
|
||||||
|
Write-Host "Downloading Inno Setup Chinese language file to: $chineseSimplifiedLanguageFile"
|
||||||
|
Invoke-WebRequest -Uri $chineseSimplifiedLanguageUrl -OutFile $chineseSimplifiedLanguageFile
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $projectPath)) {
|
||||||
|
throw "Project file not found: $projectPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path $installerScript)) {
|
||||||
|
throw "Installer script not found: $installerScript"
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path $publishDir, $installerDir | Out-Null
|
||||||
|
|
||||||
|
Write-Host "Publishing $AppName ($Configuration, $Runtime)..."
|
||||||
|
|
||||||
|
$publishArgs = @(
|
||||||
|
"publish",
|
||||||
|
$projectPath,
|
||||||
|
"-c", $Configuration,
|
||||||
|
"-r", $Runtime,
|
||||||
|
"--self-contained", $SelfContained.ToString().ToLowerInvariant(),
|
||||||
|
"-o", $publishDir,
|
||||||
|
"/p:Version=$Version",
|
||||||
|
"/p:PublishSingleFile=$($SingleFile.IsPresent.ToString().ToLowerInvariant())",
|
||||||
|
"/p:IncludeNativeLibrariesForSelfExtract=true",
|
||||||
|
"/p:PublishTrimmed=false",
|
||||||
|
"/p:DebugType=None",
|
||||||
|
"/p:DebugSymbols=false"
|
||||||
|
)
|
||||||
|
|
||||||
|
dotnet @publishArgs
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "dotnet publish failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
Get-ChildItem -Path $publishDir -Filter "*.pdb" -Recurse -File | Remove-Item -Force
|
||||||
|
|
||||||
|
$publishedExe = Join-Path $publishDir $appExeName
|
||||||
|
if (-not (Test-Path $publishedExe)) {
|
||||||
|
throw "Publish completed, but executable was not found: $publishedExe"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Publish output: $publishDir"
|
||||||
|
|
||||||
|
$localInnoCompiler = Join-Path $innoSetupDir "ISCC.exe"
|
||||||
|
|
||||||
|
if ($SkipInstaller) {
|
||||||
|
Write-Host "SkipInstaller was specified. Installer package was not created."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($InstallInnoSetupIfMissing -and -not (Test-Path $localInnoCompiler)) {
|
||||||
|
Install-LocalInnoSetup
|
||||||
|
}
|
||||||
|
|
||||||
|
$iscc = Find-InnoSetupCompiler
|
||||||
|
if (-not $iscc) {
|
||||||
|
if ($InstallInnoSetupIfMissing) {
|
||||||
|
Install-LocalInnoSetup
|
||||||
|
$iscc = Find-InnoSetupCompiler
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $iscc) {
|
||||||
|
Write-Warning "Inno Setup compiler (ISCC.exe) was not found. Rerun package-scripts\package-pc.bat and let it download Inno Setup into package-scripts\tools."
|
||||||
|
Write-Host "The publish output is ready at: $publishDir"
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Building installer with Inno Setup..."
|
||||||
|
Write-Host "Using Inno Setup compiler: $iscc"
|
||||||
|
Ensure-ChineseSimplifiedLanguageFile
|
||||||
|
|
||||||
|
$isccArgs = @(
|
||||||
|
"/DAppName=$AppName",
|
||||||
|
"/DAppVersion=$Version",
|
||||||
|
"/DAppPublisher=$Publisher",
|
||||||
|
"/DAppExeName=$appExeName",
|
||||||
|
"/DSourceDir=$publishDir",
|
||||||
|
"/DOutputDir=$installerDir",
|
||||||
|
"/DRepoRoot=$repoRoot",
|
||||||
|
"/DChineseLanguageFile=$chineseSimplifiedLanguageFile",
|
||||||
|
$installerScript
|
||||||
|
)
|
||||||
|
|
||||||
|
& $iscc @isccArgs
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "Inno Setup failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
$setupFile = Join-Path $installerDir "$AppName-Setup-$Version-$Runtime.exe"
|
||||||
|
Write-Host "Installer created: $setupFile"
|
||||||
+61
-34
@@ -1,6 +1,7 @@
|
|||||||
param(
|
param(
|
||||||
[string]$Name,
|
[string]$Name,
|
||||||
[string]$Context = "AppDataContext",
|
[ValidateSet("SQLite", "SqlServer", "PostgreSQL", "MySQL", "All")]
|
||||||
|
[string]$Provider = "All",
|
||||||
[string]$Project = "Avalonia-EFCore/Avalonia-EFCore.csproj",
|
[string]$Project = "Avalonia-EFCore/Avalonia-EFCore.csproj",
|
||||||
[string]$StartupProject = "Avalonia-API/Avalonia-API.csproj",
|
[string]$StartupProject = "Avalonia-API/Avalonia-API.csproj",
|
||||||
[string]$OutputDir = "Migrations"
|
[string]$OutputDir = "Migrations"
|
||||||
@@ -21,45 +22,71 @@ if ($LASTEXITCODE -ne 0) {
|
|||||||
throw "dotnet tool restore failed."
|
throw "dotnet tool restore failed."
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Generating migration '$Name'..."
|
function Get-ContextName([string]$providerName) {
|
||||||
dotnet tool run dotnet-ef migrations add $Name `
|
switch ($providerName) {
|
||||||
--project $Project `
|
"SQLite" { return "SqliteAppDataContext" }
|
||||||
--startup-project $StartupProject `
|
"SqlServer" { return "SqlServerAppDataContext" }
|
||||||
--context $Context `
|
"PostgreSQL" { return "PostgreSqlAppDataContext" }
|
||||||
--output-dir $OutputDir
|
"MySQL" { return "MySqlAppDataContext" }
|
||||||
if ($LASTEXITCODE -ne 0) {
|
default { throw "Unsupported provider '$providerName'." }
|
||||||
throw "dotnet ef migrations add failed."
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$migrationDir = Join-Path (Split-Path $Project -Parent) $OutputDir
|
function Add-ProviderMigration([string]$providerName) {
|
||||||
$migrationFile = Get-ChildItem $migrationDir -Filter "*_$Name.cs" |
|
$context = Get-ContextName $providerName
|
||||||
Where-Object { $_.Name -notlike "*.Designer.cs" } |
|
$providerOutputDir = Join-Path $OutputDir $providerName
|
||||||
Sort-Object LastWriteTime -Descending |
|
|
||||||
Select-Object -First 1
|
|
||||||
|
|
||||||
if ($null -eq $migrationFile) {
|
Write-Host "Generating migration '$Name' for $providerName..."
|
||||||
throw "Migration file was not found for '$Name'."
|
dotnet tool run dotnet-ef migrations add $Name `
|
||||||
}
|
|
||||||
|
|
||||||
$content = Get-Content $migrationFile.FullName -Raw
|
|
||||||
$upMatch = [regex]::Match($content, "protected override void Up\(MigrationBuilder migrationBuilder\)\s*\{(?<body>.*?)\n\s*\}", "Singleline")
|
|
||||||
$downMatch = [regex]::Match($content, "protected override void Down\(MigrationBuilder migrationBuilder\)\s*\{(?<body>.*?)\n\s*\}", "Singleline")
|
|
||||||
|
|
||||||
$upBody = if ($upMatch.Success) { $upMatch.Groups["body"].Value.Trim() } else { "" }
|
|
||||||
$downBody = if ($downMatch.Success) { $downMatch.Groups["body"].Value.Trim() } else { "" }
|
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($upBody) -and [string]::IsNullOrWhiteSpace($downBody)) {
|
|
||||||
Write-Host "No model changes were detected. Removing empty migration '$Name'..."
|
|
||||||
dotnet tool run dotnet-ef migrations remove --force `
|
|
||||||
--project $Project `
|
--project $Project `
|
||||||
--startup-project $StartupProject `
|
--startup-project $StartupProject `
|
||||||
--context $Context
|
--context $context `
|
||||||
|
--output-dir $providerOutputDir
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
throw "dotnet ef migrations remove failed."
|
throw "dotnet ef migrations add failed for $providerName."
|
||||||
}
|
}
|
||||||
exit 0
|
|
||||||
|
$migrationDir = Join-Path (Split-Path $Project -Parent) $providerOutputDir
|
||||||
|
$migrationFile = Get-ChildItem $migrationDir -Filter "*_$Name.cs" |
|
||||||
|
Where-Object { $_.Name -notlike "*.Designer.cs" } |
|
||||||
|
Sort-Object LastWriteTime -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if ($null -eq $migrationFile) {
|
||||||
|
throw "Migration file was not found for '$Name' ($providerName)."
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = Get-Content $migrationFile.FullName -Raw
|
||||||
|
$upMatch = [regex]::Match($content, "protected override void Up\(MigrationBuilder migrationBuilder\)\s*\{(?<body>.*?)\n\s*\}", "Singleline")
|
||||||
|
$downMatch = [regex]::Match($content, "protected override void Down\(MigrationBuilder migrationBuilder\)\s*\{(?<body>.*?)\n\s*\}", "Singleline")
|
||||||
|
|
||||||
|
$upBody = if ($upMatch.Success) { $upMatch.Groups["body"].Value.Trim() } else { "" }
|
||||||
|
$downBody = if ($downMatch.Success) { $downMatch.Groups["body"].Value.Trim() } else { "" }
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($upBody) -and [string]::IsNullOrWhiteSpace($downBody)) {
|
||||||
|
Write-Host "No model changes were detected for $providerName. Removing empty migration '$Name'..."
|
||||||
|
dotnet tool run dotnet-ef migrations remove --force `
|
||||||
|
--project $Project `
|
||||||
|
--startup-project $StartupProject `
|
||||||
|
--context $context
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
throw "dotnet ef migrations remove failed for $providerName."
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Migration generated for ${providerName}:"
|
||||||
|
Write-Host " $($migrationFile.FullName)"
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Migration generated:"
|
$providers = if ($Provider -eq "All") {
|
||||||
Write-Host " $($migrationFile.FullName)"
|
@("SQLite", "SqlServer", "PostgreSQL", "MySQL")
|
||||||
Write-Host "Review the migration, then start the app. Startup will automatically apply pending migrations."
|
} else {
|
||||||
|
@($Provider)
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($providerName in $providers) {
|
||||||
|
Add-ProviderMigration $providerName
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Review the migration files, then start the app. Startup will apply the migration set matching DatabaseConfiguration.Provider."
|
||||||
|
|||||||
@@ -40,11 +40,11 @@ $memberRegexes = @(
|
|||||||
},
|
},
|
||||||
@{
|
@{
|
||||||
Kind = "Property"
|
Kind = "Property"
|
||||||
Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*(?:(?:public|private|protected|internal|static|virtual|abstract|sealed|override|new|readonly|required|unsafe)\s+)+[A-Za-z_][A-Za-z0-9_<>,\[\]\?\.\s]*\s+[A-Za-z_][A-Za-z0-9_]*\s*\{\s*(?:get|set|init)\b'
|
Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*(?:(?:public|private|protected|internal|static|virtual|abstract|sealed|override|new|readonly|required|unsafe)\s+)+(?:[A-Za-z_][A-Za-z0-9_<>,\[\]\?\.\s\(\)]*|\([^\)]*\)\??)\s+[A-Za-z_][A-Za-z0-9_]*\s*\{\s*(?:get|set|init)\b'
|
||||||
},
|
},
|
||||||
@{
|
@{
|
||||||
Kind = "InterfaceProperty"
|
Kind = "InterfaceProperty"
|
||||||
Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*[A-Za-z_][A-Za-z0-9_<>,\[\]\?\.\s]*\s+[A-Za-z_][A-Za-z0-9_]*\s*\{\s*(?:get|set|init)\b'
|
Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*(?:[A-Za-z_][A-Za-z0-9_<>,\[\]\?\.\s\(\)]*|\([^\)]*\)\??)\s+[A-Za-z_][A-Za-z0-9_]*\s*\{\s*(?:get|set|init)\b'
|
||||||
},
|
},
|
||||||
@{
|
@{
|
||||||
Kind = "Constructor"
|
Kind = "Constructor"
|
||||||
@@ -52,15 +52,15 @@ $memberRegexes = @(
|
|||||||
},
|
},
|
||||||
@{
|
@{
|
||||||
Kind = "Method"
|
Kind = "Method"
|
||||||
Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*(?:(?:public|private|protected|internal|static|virtual|abstract|sealed|override|async|extern|new|unsafe|partial)\s+)+[A-Za-z_][A-Za-z0-9_<>,\[\]\?\.\s]*\s+(?:operator\s*[^\s\(]+|[A-Za-z_][A-Za-z0-9_]*)\s*(?:<[^>]+>)?\s*\('
|
Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*(?:(?:public|private|protected|internal|static|virtual|abstract|sealed|override|async|extern|new|unsafe|partial)\s+)+(?:[A-Za-z_][A-Za-z0-9_<>,\[\]\?\.\s\(\)]*|\([^\)]*\)\??)\s+(?:operator\s*[^\s\(]+|[A-Za-z_][A-Za-z0-9_]*)\s*(?:<[^>]+>)?\s*\('
|
||||||
},
|
},
|
||||||
@{
|
@{
|
||||||
Kind = "InterfaceMethod"
|
Kind = "InterfaceMethod"
|
||||||
Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*[A-Za-z_][A-Za-z0-9_<>,\[\]\?\.\s]*\s+[A-Za-z_][A-Za-z0-9_]*(?:<[^>]+>)?\s*\([^;{}]*\)\s*;'
|
Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*(?:[A-Za-z_][A-Za-z0-9_<>,\[\]\?\.\s\(\)]*|\([^\)]*\)\??)\s+[A-Za-z_][A-Za-z0-9_]*(?:<[^>]+>)?\s*\([^;{}]*\)\s*;'
|
||||||
},
|
},
|
||||||
@{
|
@{
|
||||||
Kind = "Field"
|
Kind = "Field"
|
||||||
Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*(?:(?:public|private|protected|internal|static|readonly|const|volatile|new|unsafe)\s+)+[A-Za-z_][A-Za-z0-9_<>,\[\]\?\.\s]*\s+[A-Za-z_][A-Za-z0-9_]*(?:\s*=\s*[^;]+)?\s*;'
|
Pattern = '^\s*(?:\[(?:[^\]]+)\]\s*)*(?:(?:public|private|protected|internal|static|readonly|const|volatile|new|unsafe)\s+)+(?:[A-Za-z_][A-Za-z0-9_<>,\[\]\?\.\s\(\)]*|\([^\)]*\)\??)\s+[A-Za-z_][A-Za-z0-9_]*(?:\s*=\s*[^;]+)?\s*;'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -230,17 +230,15 @@ function Get-MemberName {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"Method" {
|
"Method" {
|
||||||
$openParenIndex = $Declaration.IndexOf("(")
|
$matches = [regex]::Matches($Declaration, '\s(?<name>operator\s*[^\s\(]+|[A-Za-z_][A-Za-z0-9_]*)\s*(?:<[^>]+>)?\s*\(')
|
||||||
$prefix = if ($openParenIndex -ge 0) { $Declaration.Substring(0, $openParenIndex) } else { $Declaration }
|
if ($matches.Count -gt 0) {
|
||||||
if ($prefix -match '(?<name>operator\s*[^\s]+|[A-Za-z_][A-Za-z0-9_]*)\s*(?:<[^>]+>)?$') {
|
return $matches[$matches.Count - 1].Groups["name"].Value
|
||||||
return $Matches["name"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"InterfaceMethod" {
|
"InterfaceMethod" {
|
||||||
$openParenIndex = $Declaration.IndexOf("(")
|
$matches = [regex]::Matches($Declaration, '\s(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*(?:<[^>]+>)?\s*\(')
|
||||||
$prefix = if ($openParenIndex -ge 0) { $Declaration.Substring(0, $openParenIndex) } else { $Declaration }
|
if ($matches.Count -gt 0) {
|
||||||
if ($prefix -match '(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*(?:<[^>]+>)?$') {
|
return $matches[$matches.Count - 1].Groups["name"].Value
|
||||||
return $Matches["name"]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default {
|
default {
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"File": "Avalonia-API\\Authentication\\JwtTokenService.cs",
|
||||||
|
"Line": 20,
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "CreateAccessToken",
|
||||||
|
"Declaration": "public (string Token, DateTime ExpiresAt) CreateAccessToken(UserEntity user, IReadOnlyCollection\u003cstring\u003e roles) {"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"File": "Avalonia-API\\Authentication\\RefreshTokenService.cs",
|
||||||
|
"Line": 21,
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "CreateAsync",
|
||||||
|
"Declaration": "public async Task\u003c(string Token, ApiRefreshTokenEntity Entity)\u003e CreateAsync( int userId, string? device, string? ipAddress, CancellationToken cancellationToken = default) {"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"File": "Avalonia-API\\Authentication\\RefreshTokenService.cs",
|
||||||
|
"Line": 78,
|
||||||
|
"Kind": "Method",
|
||||||
|
"Name": "RotateAsync",
|
||||||
|
"Declaration": "public async Task\u003c(string Token, ApiRefreshTokenEntity Entity)?\u003e RotateAsync( string? token, string? device, string? ipAddress, CancellationToken cancellationToken = default) {"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -1,215 +1 @@
|
|||||||
|
|
||||||
File Line Kind Name Declaration
|
|
||||||
---- ---- ---- ---- -----------
|
|
||||||
Avalonia-API\Authentication\ApiAuthEndpointService.cs 11 Type ApiAuthEndpointService public sealed class ApiAuthEndpointService( AppDataContext db, JwtTokenService jwtTokenService, RefreshTokenServ...
|
|
||||||
Avalonia-API\Authentication\ApiAuthEndpointService.cs 21 Method LoginAsync public async Task<object?> LoginAsync(ServiceEndpointContext ctx) {
|
|
||||||
Avalonia-API\Authentication\ApiAuthEndpointService.cs 59 Method RefreshAsync public async Task<object?> RefreshAsync(ServiceEndpointContext ctx) {
|
|
||||||
Avalonia-API\Authentication\ApiAuthEndpointService.cs 91 Method LogoutAsync public async Task<object?> LogoutAsync(ServiceEndpointContext ctx) {
|
|
||||||
Avalonia-API\Authentication\ApiAuthEndpointService.cs 98 Method Deserialize private static T? Deserialize<T>(string? body) {
|
|
||||||
Avalonia-API\Authentication\ApiAuthEndpointService.cs 105 Method GetRemoteIpAddress private static string? GetRemoteIpAddress(ServiceEndpointContext ctx) {
|
|
||||||
Avalonia-API\Authentication\ApiAuthEndpointService.cs 112 Method NormalizeRoles private static string[] NormalizeRoles(string[]? roles) {
|
|
||||||
Avalonia-API\Authentication\JwtOptions.cs 3 Type JwtOptions public sealed class JwtOptions {
|
|
||||||
Avalonia-API\Authentication\JwtOptions.cs 5 Property Issuer public string Issuer { get; set; } = "";
|
|
||||||
Avalonia-API\Authentication\JwtOptions.cs 7 Property Audience public string Audience { get; set; } = "";
|
|
||||||
Avalonia-API\Authentication\JwtOptions.cs 9 Property SigningKey public string SigningKey { get; set; } = "";
|
|
||||||
Avalonia-API\Authentication\JwtOptions.cs 11 Property AccessTokenMinutes public int AccessTokenMinutes { get; set; } = 60;
|
|
||||||
Avalonia-API\Authentication\JwtOptions.cs 13 Property RefreshTokenDays public int RefreshTokenDays { get; set; } = 30;
|
|
||||||
Avalonia-API\Authentication\JwtTokenService.cs 10 Type JwtTokenService public sealed class JwtTokenService(IOptions<JwtOptions> options) {
|
|
||||||
Avalonia-API\Authentication\JwtTokenService.cs 12 Field _options private readonly JwtOptions _options = options.Value;
|
|
||||||
Avalonia-API\Authentication\RefreshTokenService.cs 10 Type RefreshTokenService public sealed class RefreshTokenService(AppDataContext db, IOptions<JwtOptions> options) {
|
|
||||||
Avalonia-API\Authentication\RefreshTokenService.cs 12 Field _options private readonly JwtOptions _options = options.Value;
|
|
||||||
Avalonia-API\Authentication\RefreshTokenService.cs 35 Method FindActiveAsync public async Task<ApiRefreshTokenEntity?> FindActiveAsync(string? token, CancellationToken cancellationToken = d...
|
|
||||||
Avalonia-API\Authentication\RefreshTokenService.cs 47 Method RevokeAsync public async Task RevokeAsync(string? token, CancellationToken cancellationToken = default) {
|
|
||||||
Avalonia-API\Authentication\RefreshTokenService.cs 78 Method HashToken private static string HashToken(string token) {
|
|
||||||
Avalonia-API\Configuration\ServicesConfiguration.cs 13 Type ServicesConfiguration public static class ServicesConfiguration {
|
|
||||||
Avalonia-API\Extensions\UnifiedEndpointExtensions.cs 98 Method MapEndpoint private static RouteHandlerBuilder MapEndpoint( IEndpointRouteBuilder group, ServiceEndpoint endpoint, IServiceP...
|
|
||||||
Avalonia-API\Extensions\UnifiedEndpointExtensions.cs 115 Method CreateAspNetCoreHandler private static Delegate CreateAspNetCoreHandler( Func<ServiceEndpointContext, Task<object?>> unifiedHandler, ISe...
|
|
||||||
Avalonia-API\Extensions\UnifiedEndpointExtensions.cs 138 Method BuildContextFromHttpContext private static async Task<ServiceEndpointContext> BuildContextFromHttpContext(HttpContext httpContext) {
|
|
||||||
Avalonia-API\Extensions\UnifiedEndpointExtensions.cs 169 Method ConvertFilterAsync private static async ValueTask<object?> ConvertFilterAsync( UnifiedFilter unifiedFilter, AspNetCoreFilterContext...
|
|
||||||
Avalonia-Common\Core\ApiResponse.cs 119 Property Success public bool Success { get; set; } = true;
|
|
||||||
Avalonia-Common\Core\ApiResponse.cs 122 Property Code public int Code { get; set; } = 200;
|
|
||||||
Avalonia-Common\Core\ApiResponse.cs 125 Property Items public List<T> Items { get; set; } = new();
|
|
||||||
Avalonia-Common\Core\ApiResponse.cs 128 Property Total public int Total { get; set; }
|
|
||||||
Avalonia-Common\Core\ApiResponse.cs 131 Property Page public int Page { get; set; } = 1;
|
|
||||||
Avalonia-Common\Core\ApiResponse.cs 134 Property PageSize public int PageSize { get; set; } = 20;
|
|
||||||
Avalonia-Common\Core\ApiResponse.cs 137 Field TotalPages public int TotalPages => PageSize > 0 ? (int)Math.Ceiling((double)Total / PageSize) : 0;
|
|
||||||
Avalonia-Common\Core\ApiResponse.cs 139 Method From public static PagedResponse<T> From(List<T> items, int total, int page, int pageSize) {
|
|
||||||
Avalonia-Common\Infrastructure\LoggingConfiguration.cs 90 Field _logger private static ILogger? _logger;
|
|
||||||
Avalonia-Common\Infrastructure\LoggingConfiguration.cs 101 Field Logger public static ILogger Logger => _logger ?? Log.Logger;
|
|
||||||
Avalonia-Common\Infrastructure\LoggingConfiguration.cs 103 Method Debug public static void Debug(string messageTemplate, params object?[] propertyValues) => Logger.Debug(messageTemplat...
|
|
||||||
Avalonia-Common\Infrastructure\LoggingConfiguration.cs 106 Method Information public static void Information(string messageTemplate, params object?[] propertyValues) => Logger.Information(me...
|
|
||||||
Avalonia-Common\Infrastructure\LoggingConfiguration.cs 109 Method Warning public static void Warning(string messageTemplate, params object?[] propertyValues) => Logger.Warning(messageTem...
|
|
||||||
Avalonia-Common\Infrastructure\LoggingConfiguration.cs 112 Method Error public static void Error(string messageTemplate, params object?[] propertyValues) => Logger.Error(messageTemplat...
|
|
||||||
Avalonia-Common\Infrastructure\LoggingConfiguration.cs 115 Method Error public static void Error(Exception exception, string messageTemplate, params object?[] propertyValues) => Logger...
|
|
||||||
Avalonia-Common\Infrastructure\LoggingConfiguration.cs 118 Method Fatal public static void Fatal(string messageTemplate, params object?[] propertyValues) => Logger.Fatal(messageTemplat...
|
|
||||||
Avalonia-Common\Infrastructure\LoggingConfiguration.cs 121 Method Fatal public static void Fatal(Exception exception, string messageTemplate, params object?[] propertyValues) => Logger...
|
|
||||||
Avalonia-EFCore\Database\AppDataContext.cs 22 Method OnModelCreating protected override void OnModelCreating(ModelBuilder modelBuilder) {
|
|
||||||
Avalonia-EFCore\Database\AppDataContextFactory.cs 5 Type AppDataContextFactory public class AppDataContextFactory : IDesignTimeDbContextFactory<AppDataContext> {
|
|
||||||
Avalonia-EFCore\Database\AppDataContextFactory.cs 7 Method CreateDbContext public AppDataContext CreateDbContext(string[] args) {
|
|
||||||
Avalonia-EFCore\Database\AppDbContext.cs 15 Field _dbConfig private readonly DatabaseConfiguration _dbConfig = dbConfig;
|
|
||||||
Avalonia-EFCore\Database\AppDbContext.cs 17 Method OnConfiguring protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
|
|
||||||
Avalonia-EFCore\Database\AppDbContext.cs 68 Method SaveChangesAsync public override Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken =...
|
|
||||||
Avalonia-EFCore\Database\AppDbContext.cs 74 Method SetTimestamps private void SetTimestamps() {
|
|
||||||
Avalonia-EFCore\Database\DatabaseManager.cs 20 Field _context private readonly TContext _context;
|
|
||||||
Avalonia-EFCore\Database\DatabaseManager.cs 21 Field _config private readonly DatabaseConfiguration _config;
|
|
||||||
Avalonia-EFCore\Database\DatabaseManager.cs 22 Field _serviceProvider private readonly IServiceProvider? _serviceProvider;
|
|
||||||
Avalonia-EFCore\Database\DatabaseManager.cs 24 Constructor DatabaseManager public DatabaseManager(TContext context, DatabaseConfiguration config, IServiceProvider? serviceProvider = null) {
|
|
||||||
Avalonia-EFCore\Database\DatabaseManager.cs 131 Method GetApplicationVersion private static string GetApplicationVersion() {
|
|
||||||
Avalonia-EFCore\Database\DatabaseManager.cs 184 Property Provider public string Provider { get; set; } = string.Empty;
|
|
||||||
Avalonia-EFCore\Database\DatabaseManager.cs 185 Property AppliedMigrations public List<string> AppliedMigrations { get; set; } = new();
|
|
||||||
Avalonia-EFCore\Database\DatabaseManager.cs 186 Property PendingMigrations public List<string> PendingMigrations { get; set; } = new();
|
|
||||||
Avalonia-EFCore\Database\DatabaseManager.cs 187 Property IsLatest public bool IsLatest { get; set; }
|
|
||||||
Avalonia-EFCore\Database\DatabaseManager.cs 188 Property CanConnect public bool CanConnect { get; set; }
|
|
||||||
Avalonia-EFCore\Database\DatabaseProviderRegistry.cs 18 Field _providers private static readonly Dictionary<DatabaseProvider, ProviderConfigurator> _providers = new();
|
|
||||||
Avalonia-EFCore\Models\ApiRefreshTokenEntity.cs 17 Property Id public long Id { get; set; }
|
|
||||||
Avalonia-EFCore\Models\ApiRefreshTokenEntity.cs 20 Property UserId public int UserId { get; set; }
|
|
||||||
Avalonia-EFCore\Models\ApiRefreshTokenEntity.cs 24 Property TokenHash public string TokenHash { get; set; } = string.Empty;
|
|
||||||
Avalonia-EFCore\Models\ApiRefreshTokenEntity.cs 27 Property CreatedAt public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
||||||
Avalonia-EFCore\Models\ApiRefreshTokenEntity.cs 30 Property ExpiresAt public DateTime ExpiresAt { get; set; }
|
|
||||||
Avalonia-EFCore\Models\ApiRefreshTokenEntity.cs 33 Property RevokedAt public DateTime? RevokedAt { get; set; }
|
|
||||||
Avalonia-EFCore\Models\ApiRefreshTokenEntity.cs 37 Property ReplacedByTokenHash public string? ReplacedByTokenHash { get; set; }
|
|
||||||
Avalonia-EFCore\Models\ApiRefreshTokenEntity.cs 41 Property Device public string? Device { get; set; }
|
|
||||||
Avalonia-EFCore\Models\ApiRefreshTokenEntity.cs 45 Property IpAddress public string? IpAddress { get; set; }
|
|
||||||
Avalonia-EFCore\Models\ApiRefreshTokenEntity.cs 47 Field IsActive public bool IsActive => RevokedAt is null && ExpiresAt > DateTime.UtcNow;
|
|
||||||
Avalonia-EFCore\Models\UserEntity.cs 18 Property Id public int Id { get; set; }
|
|
||||||
Avalonia-EFCore\Models\UserEntity.cs 23 Property Name public string? Name { get; set; }
|
|
||||||
Avalonia-EFCore\Models\UserEntity.cs 28 Property Email public string? Email { get; set; }
|
|
||||||
Avalonia-EFCore\Models\UserEntity.cs 33 Property PhoneNumber public string? PhoneNumber { get; set; }
|
|
||||||
Avalonia-EFCore\Models\UserEntity.cs 37 Property CreatedAt public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
||||||
Avalonia-EFCore\Models\UserEntity.cs 41 Property UpdatedAt public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
|
||||||
Avalonia-EFCore\Models\WeatherForecast.cs 3 Type WeatherForecast public class WeatherForecast {
|
|
||||||
Avalonia-EFCore\Models\WeatherForecast.cs 5 Property Date public DateOnly Date { get; set; }
|
|
||||||
Avalonia-EFCore\Models\WeatherForecast.cs 7 Property TemperatureC public int TemperatureC { get; set; }
|
|
||||||
Avalonia-EFCore\Models\WeatherForecast.cs 9 Field TemperatureF public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
|
||||||
Avalonia-EFCore\Models\WeatherForecast.cs 11 Property Summary public string? Summary { get; set; }
|
|
||||||
Avalonia-EFCore\Models\WeatherForecastEntity.cs 18 Property Id public int Id { get; set; }
|
|
||||||
Avalonia-EFCore\Models\WeatherForecastEntity.cs 22 Property Date public DateOnly Date { get; set; }
|
|
||||||
Avalonia-EFCore\Models\WeatherForecastEntity.cs 26 Property TemperatureC public int TemperatureC { get; set; }
|
|
||||||
Avalonia-EFCore\Models\WeatherForecastEntity.cs 31 Property Summary public string? Summary { get; set; }
|
|
||||||
Avalonia-EFCore\Models\WeatherForecastEntity.cs 35 Property CreatedAt public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
|
||||||
Avalonia-EFCore\Models\WeatherForecastEntity.cs 39 Property UpdatedAt public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
|
||||||
Avalonia-PC\App.axaml.cs 10 Type App public partial class App : Application {
|
|
||||||
Avalonia-PC\App.axaml.cs 12 Method Initialize public override void Initialize() {
|
|
||||||
Avalonia-PC\App.axaml.cs 17 Method OnFrameworkInitializationCompleted public override void OnFrameworkInitializationCompleted() {
|
|
||||||
Avalonia-PC\Authentication\DefaultPcThirdPartyAuthorizationClient.cs 13 Method ValidateAuthorizationCodeAsync public Task<ThirdPartyAuthCheckResult> ValidateAuthorizationCodeAsync( string authorizationCode, CancellationTok...
|
|
||||||
Avalonia-PC\Authentication\DefaultPcThirdPartyAuthorizationClient.cs 26 Method RefreshAuthorizationAsync public Task<ThirdPartyAuthCheckResult> RefreshAuthorizationAsync( string authorizationReference, CancellationTok...
|
|
||||||
Avalonia-PC\Authentication\PcAuthEndpointService.cs 11 Type PcAuthEndpointService public sealed class PcAuthEndpointService(PcGlobalTokenService tokenService) : IPcAuthEndpointService {
|
|
||||||
Avalonia-PC\Authentication\PcAuthEndpointService.cs 18 Method AuthorizeAsync public async Task<object?> AuthorizeAsync(ServiceEndpointContext ctx) {
|
|
||||||
Avalonia-PC\Authentication\PcAuthEndpointService.cs 31 Method RefreshAsync public async Task<object?> RefreshAsync(ServiceEndpointContext ctx) {
|
|
||||||
Avalonia-PC\Authentication\PcAuthEndpointService.cs 45 Method LogoutAsync public Task<object?> LogoutAsync(ServiceEndpointContext ctx) {
|
|
||||||
Avalonia-PC\Authentication\PcAuthEndpointService.cs 53 Method Deserialize private static T? Deserialize<T>(string? body) {
|
|
||||||
Avalonia-PC\Authentication\PcAuthEndpointService.cs 60 Method ExtractBearerToken private static string? ExtractBearerToken(string? authorization) {
|
|
||||||
Avalonia-PC\Authentication\PcAuthEndpointService.cs 67 Field prefix const string prefix = "";
|
|
||||||
Avalonia-PC\Authentication\PcAuthService.cs 9 Type PcAuthService public sealed class PcAuthService(PcGlobalTokenService tokenService) : IAuthService {
|
|
||||||
Avalonia-PC\Authentication\PcAuthService.cs 11 Method AuthenticateAsync public async Task<ClaimsPrincipal?> AuthenticateAsync(ServiceEndpointContext context) {
|
|
||||||
Avalonia-PC\Authentication\PcAuthService.cs 32 Method AuthorizeAsync public Task<bool> AuthorizeAsync(ClaimsPrincipal user, string policy) {
|
|
||||||
Avalonia-PC\Authentication\PcAuthService.cs 37 Method ExtractBearerToken private static string? ExtractBearerToken(string? authorization) {
|
|
||||||
Avalonia-PC\Authentication\PcAuthService.cs 44 Field prefix const string prefix = "";
|
|
||||||
Avalonia-PC\Authentication\PcGlobalTokenService.cs 10 Type PcGlobalTokenService public sealed class PcGlobalTokenService(IPcThirdPartyAuthorizationClient thirdPartyClient) {
|
|
||||||
Avalonia-PC\Authentication\PcGlobalTokenService.cs 12 Field SuperRoles private static readonly string[] SuperRoles = ["", ""];
|
|
||||||
Avalonia-PC\Authentication\PcGlobalTokenService.cs 13 Field _syncRoot private readonly object _syncRoot = new();
|
|
||||||
Avalonia-PC\Authentication\PcGlobalTokenService.cs 14 Field _current private PcTokenState? _current;
|
|
||||||
Avalonia-PC\Authentication\PcGlobalTokenService.cs 16 Field NormalLifetime private static readonly TimeSpan NormalLifetime = TimeSpan.FromHours(8);
|
|
||||||
Avalonia-PC\Authentication\PcGlobalTokenService.cs 17 Field TemporaryFailureLifetime private static readonly TimeSpan TemporaryFailureLifetime = TimeSpan.FromMinutes(20);
|
|
||||||
Avalonia-PC\Authentication\PcGlobalTokenService.cs 18 Field MaxTemporaryFailureWindow private static readonly TimeSpan MaxTemporaryFailureWindow = TimeSpan.FromHours(24);
|
|
||||||
Avalonia-PC\Authentication\PcGlobalTokenService.cs 20 Method AuthorizeAsync public async Task<PcTokenResponse?> AuthorizeAsync(string? authorizationCode, CancellationToken cancellationToke...
|
|
||||||
Avalonia-PC\Authentication\PcGlobalTokenService.cs 36 Method RefreshAsync public async Task<PcTokenResponse?> RefreshAsync(string? token, CancellationToken cancellationToken = default) {
|
|
||||||
Avalonia-PC\Authentication\PcGlobalTokenService.cs 59 Method ValidateAsync public async Task<bool> ValidateAsync(string? token, CancellationToken cancellationToken = default) {
|
|
||||||
Avalonia-PC\Authentication\PcGlobalTokenService.cs 79 Method Logout public void Logout(string? token) {
|
|
||||||
Avalonia-PC\Authentication\PcGlobalTokenService.cs 90 Method IssueToken private PcTokenResponse IssueToken(string authorizationReference, TimeSpan lifetime, bool resetTemporaryFailureW...
|
|
||||||
Avalonia-PC\Authentication\PcGlobalTokenService.cs 108 Method RefreshAfterTemporaryFailure private PcTokenResponse? RefreshAfterTemporaryFailure(PcTokenState current) {
|
|
||||||
Avalonia-PC\Authentication\PcGlobalTokenService.cs 119 Method ClearAndReturnNull private PcTokenResponse? ClearAndReturnNull() {
|
|
||||||
Avalonia-PC\Authentication\PcGlobalTokenService.cs 129 Method IsCurrentToken private bool IsCurrentToken(string? token) {
|
|
||||||
Avalonia-PC\Authentication\PcGlobalTokenService.cs 136 Method HashToken private static string HashToken(string token) {
|
|
||||||
Avalonia-PC\Authentication\PcGlobalTokenService.cs 142 Type PcTokenState private sealed record PcTokenState( string TokenHash, string AuthorizationReference, DateTime ExpiresAt, DateTim...
|
|
||||||
Avalonia-PC\Program.cs 17 Type Program internal sealed class Program {
|
|
||||||
Avalonia-PC\Program.cs 19 Property Services public static IServiceProvider Services { get; private set; } = null!;
|
|
||||||
Avalonia-PC\Program.cs 22 Method Main public static void Main(string[] args) {
|
|
||||||
Avalonia-PC\Program.cs 43 Method ConfigureServices private static void ConfigureServices() {
|
|
||||||
Avalonia-PC\Program.cs 75 Method BuildAvaloniaApp public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<App>()
|
|
||||||
Avalonia-PC\ViewLocator.cs 15 Type ViewLocator public class ViewLocator : IDataTemplate {
|
|
||||||
Avalonia-PC\ViewLocator.cs 17 Method Build public Control? Build(object? param) {
|
|
||||||
Avalonia-PC\ViewLocator.cs 33 Method Match public bool Match(object? data) {
|
|
||||||
Avalonia-PC\ViewModels\MainWindowViewModel.cs 3 Type MainWindowViewModel public partial class MainWindowViewModel : ViewModelBase {
|
|
||||||
Avalonia-PC\ViewModels\MainWindowViewModel.cs 5 Property Greeting public string Greeting { get; } = "";
|
|
||||||
Avalonia-PC\ViewModels\ViewModelBase.cs 5 Type ViewModelBase public abstract class ViewModelBase : ObservableObject {
|
|
||||||
Avalonia-PC\Views\MainWindow.axaml.cs 15 Type MainWindow public partial class MainWindow : Window {
|
|
||||||
Avalonia-PC\Views\MainWindow.axaml.cs 17 Field AppScheme private const string AppScheme = "";
|
|
||||||
Avalonia-PC\Views\MainWindow.axaml.cs 18 Field OnlineStartupUrl private const string? OnlineStartupUrl = "";
|
|
||||||
Avalonia-PC\Views\MainWindow.axaml.cs 20 Field LocalStartupPath private const string? LocalStartupPath = null;
|
|
||||||
Avalonia-PC\Views\MainWindow.axaml.cs 26 Field _webView private NativeWebView? _webView;
|
|
||||||
Avalonia-PC\Views\MainWindow.axaml.cs 27 Field _eventsAttached private bool _eventsAttached;
|
|
||||||
Avalonia-PC\Views\MainWindow.axaml.cs 28 Field _webViewAdapter private object? _webViewAdapter;
|
|
||||||
Avalonia-PC\Views\MainWindow.axaml.cs 29 Field _localHttpServer private HttpListener? _localHttpServer;
|
|
||||||
Avalonia-PC\Views\MainWindow.axaml.cs 30 Field _localHttpServerCts private CancellationTokenSource? _localHttpServerCts;
|
|
||||||
Avalonia-PC\Views\MainWindow.axaml.cs 31 Field _localHttpBaseUrl private string? _localHttpBaseUrl;
|
|
||||||
Avalonia-PC\Views\MainWindow.axaml.cs 32 Field _localHttpRoot private string? _localHttpRoot;
|
|
||||||
Avalonia-PC\Views\MainWindow.axaml.cs 613 Type AppResponse private sealed class AppResponse {
|
|
||||||
Avalonia-PC\Views\MainWindow.axaml.cs 615 Property Kind public string Kind { get; set; } = string.Empty;
|
|
||||||
Avalonia-PC\Views\MainWindow.axaml.cs 617 Property Id public string? Id { get; set; }
|
|
||||||
Avalonia-PC\Views\MainWindow.axaml.cs 619 Property StatusCode public int StatusCode { get; set; }
|
|
||||||
Avalonia-PC\Views\MainWindow.axaml.cs 621 Property StatusMessage public string StatusMessage { get; set; } = string.Empty;
|
|
||||||
Avalonia-PC\Views\MainWindow.axaml.cs 623 Property Body public string Body { get; set; } = string.Empty;
|
|
||||||
Avalonia-PC\Views\MainWindow.axaml.cs 625 Property Headers public Dictionary<string, string> Headers { get; set; } = new();
|
|
||||||
Avalonia-PC\Views\MainWindow.BridgeScript.cs 3 Type MainWindow public partial class MainWindow {
|
|
||||||
Avalonia-PC\Views\MainWindow.BridgeScript.cs 109 Type BridgeXMLHttpRequest class BridgeXMLHttpRequest {
|
|
||||||
Avalonia-PC\Views\MainWindow.Routes.cs 12 Type MainWindow public partial class MainWindow {
|
|
||||||
Avalonia-PC\Views\MainWindow.Routes.cs 25 Method RegisterRoutes private void RegisterRoutes() {
|
|
||||||
Avalonia-Services\Core\GlobalExceptionFilter.cs 13 Field _includeDetails private readonly bool _includeDetails;
|
|
||||||
Avalonia-Services\Core\GlobalExceptionFilter.cs 23 Method InvokeAsync public async Task InvokeAsync(ServiceEndpointContext context, EndpointFilterDelegate next) {
|
|
||||||
Avalonia-Services\Core\GlobalExceptionFilter.cs 76 Method LogException private static void LogException(ServiceEndpointContext context, Exception ex) {
|
|
||||||
Avalonia-Services\Core\IAuthService.cs 26 Method AuthenticateAsync public Task<ClaimsPrincipal?> AuthenticateAsync(ServiceEndpointContext context) {
|
|
||||||
Avalonia-Services\Core\IAuthService.cs 33 Method AuthorizeAsync public Task<bool> AuthorizeAsync(ClaimsPrincipal user, string policy) {
|
|
||||||
Avalonia-Services\Core\IEndpointFilter.cs 28 Field _filter private readonly Func<ServiceEndpointContext, EndpointFilterDelegate, Task> _filter;
|
|
||||||
Avalonia-Services\Core\IEndpointFilter.cs 30 Constructor AnonymousEndpointFilter public AnonymousEndpointFilter(Func<ServiceEndpointContext, EndpointFilterDelegate, Task> filter) {
|
|
||||||
Avalonia-Services\Core\IEndpointFilter.cs 35 Method InvokeAsync public Task InvokeAsync(ServiceEndpointContext context, EndpointFilterDelegate next) {
|
|
||||||
Avalonia-Services\Core\ServiceEndpointCollection.cs 9 Type EndpointHostTarget public enum EndpointHostTarget {
|
|
||||||
Avalonia-Services\Core\ServiceEndpointCollection.cs 72 Method WithOpenApi public ServiceEndpoint WithOpenApi( string tag, string summary, string? description = null, Type? requestType = ...
|
|
||||||
Avalonia-Services\Core\ServiceEndpointCollection.cs 125 Method SupportsHost public bool SupportsHost(EndpointHostTarget host) {
|
|
||||||
Avalonia-Services\Core\ServiceEndpointCollection.cs 139 Method ForHost public IEnumerable<ServiceEndpoint> ForHost(EndpointHostTarget host) {
|
|
||||||
Avalonia-Services\Core\ServiceEndpointCollection.cs 155 Method MapGet public ServiceEndpoint MapGet<TService>( string pattern, Func<TService, ServiceEndpointContext, Task<object?>> h...
|
|
||||||
Avalonia-Services\Core\ServiceEndpointCollection.cs 171 Method MapPost public ServiceEndpoint MapPost<TService>( string pattern, Func<TService, ServiceEndpointContext, Task<object?>> ...
|
|
||||||
Avalonia-Services\Core\ServiceEndpointCollection.cs 187 Method MapPut public ServiceEndpoint MapPut<TService>( string pattern, Func<TService, ServiceEndpointContext, Task<object?>> h...
|
|
||||||
Avalonia-Services\Core\ServiceEndpointCollection.cs 203 Method MapDelete public ServiceEndpoint MapDelete<TService>( string pattern, Func<TService, ServiceEndpointContext, Task<object?>...
|
|
||||||
Avalonia-Services\Core\ServiceEndpointCollection.cs 229 Method AddEndpoint private ServiceEndpoint AddEndpoint(string pattern, string method, Func<ServiceEndpointContext, Task<object?>> h...
|
|
||||||
Avalonia-Services\Core\ServiceEndpointCollection.cs 241 Method CreateServiceHandler private static Func<ServiceEndpointContext, Task<object?>> CreateServiceHandler<TService>( Func<TService, Servic...
|
|
||||||
Avalonia-Services\Endpoints\AppEndpoints.cs 92 Method GetUserFromDatabaseAsync private static async Task<object?> GetUserFromDatabaseAsync(ServiceEndpointContext ctx) {
|
|
||||||
Avalonia-Services\Endpoints\AppEndpoints.cs 112 Method ProcessDataAsync private static async Task<object?> ProcessDataAsync(ServiceEndpointContext ctx) {
|
|
||||||
Avalonia-Services\Endpoints\AuthEndpoints.cs 11 Method ConfigureApi public static void ConfigureApi(ServiceEndpointBuilder builder) {
|
|
||||||
Avalonia-Services\Endpoints\AuthEndpoints.cs 32 Method ConfigurePc public static void ConfigurePc(ServiceEndpointBuilder builder) {
|
|
||||||
Avalonia-Services\Extensions\DesktopEndpointAdapter.cs 11 Field _endpoints private readonly ServiceEndpointCollection _endpoints;
|
|
||||||
Avalonia-Services\Extensions\DesktopEndpointAdapter.cs 12 Field _authService private readonly IAuthService _authService;
|
|
||||||
Avalonia-Services\Extensions\DesktopEndpointAdapter.cs 13 Field _serviceProvider private readonly IServiceProvider _serviceProvider;
|
|
||||||
Avalonia-Services\Extensions\DesktopEndpointAdapter.cs 20 Property IsMatched public bool IsMatched { get; init; }
|
|
||||||
Avalonia-Services\Extensions\DesktopEndpointAdapter.cs 21 Property StatusCode public int StatusCode { get; init; } = 200;
|
|
||||||
Avalonia-Services\Extensions\DesktopEndpointAdapter.cs 22 Property StatusMessage public string StatusMessage { get; init; } = "";
|
|
||||||
Avalonia-Services\Extensions\DesktopEndpointAdapter.cs 23 Property Data public object? Data { get; init; }
|
|
||||||
Avalonia-Services\Extensions\DesktopEndpointAdapter.cs 24 Property ResponseHeaders public Dictionary<string, string> ResponseHeaders { get; init; } = new();
|
|
||||||
Avalonia-Services\Extensions\DesktopEndpointAdapter.cs 26 Method Success public static RouteResult Success(object? data, ServiceEndpointContext ctx) {
|
|
||||||
Avalonia-Services\Extensions\DesktopEndpointAdapter.cs 38 Method NotFound public static RouteResult NotFound() => new()
|
|
||||||
Avalonia-Services\Extensions\DesktopEndpointAdapter.cs 46 Constructor DesktopEndpointAdapter public DesktopEndpointAdapter( ServiceEndpointCollection endpoints, IAuthService authService, IServiceProvider s...
|
|
||||||
Avalonia-Services\Services\AuthService\AuthContracts.cs 3 Type ApiLoginRequest public sealed record ApiLoginRequest(string? Account, string? Password, string[]? Roles = null);
|
|
||||||
Avalonia-Services\Services\AuthService\AuthContracts.cs 5 Type ApiRefreshTokenRequest public sealed record ApiRefreshTokenRequest(string? RefreshToken);
|
|
||||||
Avalonia-Services\Services\AuthService\AuthContracts.cs 7 Type ApiLogoutRequest public sealed record ApiLogoutRequest(string? RefreshToken);
|
|
||||||
Avalonia-Services\Services\AuthService\AuthContracts.cs 9 Type AuthTokenResponse public sealed record AuthTokenResponse( string AccessToken, string RefreshToken, DateTime AccessTokenExpiresAt, ...
|
|
||||||
Avalonia-Services\Services\AuthService\AuthContracts.cs 16 Type PcAuthorizeRequest public sealed record PcAuthorizeRequest(string? AuthorizationCode);
|
|
||||||
Avalonia-Services\Services\AuthService\AuthContracts.cs 18 Type PcRefreshRequest public sealed record PcRefreshRequest(string? Token);
|
|
||||||
Avalonia-Services\Services\AuthService\AuthContracts.cs 20 Type PcLogoutRequest public sealed record PcLogoutRequest(string? Token);
|
|
||||||
Avalonia-Services\Services\AuthService\AuthContracts.cs 22 Type PcTokenResponse public sealed record PcTokenResponse(string Token, DateTime ExpiresAt, string[] Roles);
|
|
||||||
Avalonia-Services\Services\AuthService\AuthContracts.cs 24 Type ThirdPartyAuthCheckResult public enum ThirdPartyAuthCheckResult {
|
|
||||||
Avalonia-Services\Services\AuthService\AuthContracts.cs 31 Type IPcThirdPartyAuthorizationClient public interface IPcThirdPartyAuthorizationClient {
|
|
||||||
Avalonia-Services\Services\AuthService\AuthContracts.cs 33 InterfaceMethod ValidateAuthorizationCodeAsync Task<ThirdPartyAuthCheckResult> ValidateAuthorizationCodeAsync(string authorizationCode, CancellationToken cance...
|
|
||||||
Avalonia-Services\Services\AuthService\AuthContracts.cs 35 InterfaceMethod RefreshAuthorizationAsync Task<ThirdPartyAuthCheckResult> RefreshAuthorizationAsync(string authorizationReference, CancellationToken cance...
|
|
||||||
Avalonia-Services\Services\AuthService\AuthEndpointServices.cs 6 Type IApiAuthEndpointService public interface IApiAuthEndpointService {
|
|
||||||
Avalonia-Services\Services\AuthService\AuthEndpointServices.cs 8 InterfaceMethod LoginAsync Task<object?> LoginAsync(ServiceEndpointContext ctx);
|
|
||||||
Avalonia-Services\Services\AuthService\AuthEndpointServices.cs 10 InterfaceMethod RefreshAsync Task<object?> RefreshAsync(ServiceEndpointContext ctx);
|
|
||||||
Avalonia-Services\Services\AuthService\AuthEndpointServices.cs 12 InterfaceMethod LogoutAsync Task<object?> LogoutAsync(ServiceEndpointContext ctx);
|
|
||||||
Avalonia-Services\Services\AuthService\AuthEndpointServices.cs 15 Type IPcAuthEndpointService public interface IPcAuthEndpointService {
|
|
||||||
Avalonia-Services\Services\AuthService\AuthEndpointServices.cs 17 InterfaceMethod AuthorizeAsync Task<object?> AuthorizeAsync(ServiceEndpointContext ctx);
|
|
||||||
Avalonia-Services\Services\AuthService\AuthEndpointServices.cs 19 InterfaceMethod RefreshAsync Task<object?> RefreshAsync(ServiceEndpointContext ctx);
|
|
||||||
Avalonia-Services\Services\AuthService\AuthEndpointServices.cs 21 InterfaceMethod LogoutAsync Task<object?> LogoutAsync(ServiceEndpointContext ctx);
|
|
||||||
Avalonia-Services\Services\WeatherForecastService.cs 5 Type WeatherForecastService public class WeatherForecastService {
|
|
||||||
Avalonia-Services\Services\WeatherForecastService.cs 12 Method GetWeatherForecasts public IEnumerable<WeatherForecast> GetWeatherForecasts() {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user