Compare commits
5
Commits
fc6f9f6bc3
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79fed6cf5d | ||
|
|
5cdc7052e0 | ||
|
|
e72bff954b | ||
|
|
d19de41272 | ||
|
|
1ab9a90831 |
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "10.0.0",
|
||||
"version": "10.0.7",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
],
|
||||
|
||||
@@ -26,3 +26,9 @@
|
||||
/Avalonia-API/avalonia-api.db
|
||||
/Avalonia-API/avalonia-api.db-shm
|
||||
/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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -4,7 +4,6 @@ using Avalonia_EFCore.Models;
|
||||
using Avalonia_Services.Core;
|
||||
using Avalonia_Services.Services.AuthService;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Avalonia_API.Authentication
|
||||
{
|
||||
@@ -17,21 +16,15 @@ namespace Avalonia_API.Authentication
|
||||
JwtTokenService jwtTokenService,
|
||||
RefreshTokenService refreshTokenService) : IApiAuthEndpointService
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 处理用户登录请求。根据账号(邮箱或用户名)查找或创建用户,
|
||||
/// 生成 JWT Access Token 和 Refresh Token 并返回。
|
||||
/// </summary>
|
||||
/// <param name="ctx">服务端点上下文,包含请求体、请求头等信息。</param>
|
||||
/// <returns>包含 AccessToken、RefreshToken 及过期时间的认证响应。</returns>
|
||||
public async Task<object?> LoginAsync(ServiceEndpointContext ctx)
|
||||
public async Task<IApiResponse> LoginAsync(ApiLoginRequest request, ServiceEndpointContext ctx)
|
||||
{
|
||||
var request = Deserialize<ApiLoginRequest>(ctx.Body);
|
||||
if (string.IsNullOrWhiteSpace(request?.Account))
|
||||
if (string.IsNullOrWhiteSpace(request.Account))
|
||||
{
|
||||
ctx.StatusCode = 400;
|
||||
return ResponseHelper.Failure(400, "账号不能为空");
|
||||
@@ -72,11 +65,10 @@ namespace Avalonia_API.Authentication
|
||||
/// </summary>
|
||||
/// <param name="ctx">服务端点上下文,包含请求体中的 RefreshToken。</param>
|
||||
/// <returns>新的 Token 对;若 Refresh Token 无效则返回 401 错误。</returns>
|
||||
public async Task<object?> RefreshAsync(ServiceEndpointContext ctx)
|
||||
public async Task<IApiResponse> RefreshAsync(ApiRefreshTokenRequest request, ServiceEndpointContext ctx)
|
||||
{
|
||||
var request = Deserialize<ApiRefreshTokenRequest>(ctx.Body);
|
||||
var rotated = await refreshTokenService.RotateAsync(
|
||||
request?.RefreshToken,
|
||||
request.RefreshToken,
|
||||
ctx.GetHeader("User-Agent"),
|
||||
GetRemoteIpAddress(ctx));
|
||||
|
||||
@@ -109,26 +101,12 @@ namespace Avalonia_API.Authentication
|
||||
/// </summary>
|
||||
/// <param name="ctx">服务端点上下文,包含请求体中的 RefreshToken。</param>
|
||||
/// <returns>登出成功的响应。</returns>
|
||||
public async Task<object?> LogoutAsync(ServiceEndpointContext ctx)
|
||||
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("退出成功");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 JSON 请求体反序列化为指定类型。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标类型。</typeparam>
|
||||
/// <param name="body">JSON 请求体字符串,可为空。</param>
|
||||
/// <returns>反序列化后的对象;若 body 为空则返回默认值。</returns>
|
||||
private static T? Deserialize<T>(string? body)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(body)
|
||||
? default
|
||||
: JsonSerializer.Deserialize<T>(body, JsonOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从上下文的 Items 中提取 ASP.NET Core HttpContext,并获取客户端远程 IP 地址。
|
||||
/// </summary>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" 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>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
|
||||
@@ -81,7 +81,8 @@ namespace Avalonia_API.Extensions
|
||||
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");
|
||||
}
|
||||
@@ -131,9 +132,11 @@ namespace Avalonia_API.Extensions
|
||||
{
|
||||
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["User"] = httpContext.User;
|
||||
httpContext.Items["UnifiedContext"] = ctx;
|
||||
|
||||
var result = await unifiedHandler(ctx);
|
||||
|
||||
@@ -144,6 +147,23 @@ namespace Avalonia_API.Extensions
|
||||
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();
|
||||
};
|
||||
}
|
||||
@@ -173,6 +193,14 @@ namespace Avalonia_API.Extensions
|
||||
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)
|
||||
{
|
||||
using var reader = new StreamReader(httpContext.Request.Body);
|
||||
@@ -204,6 +232,7 @@ namespace Avalonia_API.Extensions
|
||||
|
||||
httpContext.Items["UnifiedContext"] = ctx;
|
||||
|
||||
object? nextResult = null;
|
||||
await unifiedFilter.InvokeAsync(ctx, async (c) =>
|
||||
{
|
||||
httpContext.Response.StatusCode = c.StatusCode;
|
||||
@@ -211,7 +240,7 @@ namespace Avalonia_API.Extensions
|
||||
{
|
||||
httpContext.Response.Headers[kvp.Key] = kvp.Value;
|
||||
}
|
||||
await aspNext(aspContext);
|
||||
nextResult = await aspNext(aspContext);
|
||||
});
|
||||
|
||||
if (ctx.ResponseBody is not null)
|
||||
@@ -219,7 +248,7 @@ namespace Avalonia_API.Extensions
|
||||
return Results.Json(ctx.ResponseBody, statusCode: ctx.StatusCode);
|
||||
}
|
||||
|
||||
return null!;
|
||||
return nextResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
"RefreshTokenDays": 30
|
||||
},
|
||||
"DatabaseConfiguration": {
|
||||
"Provider": "SQLite",
|
||||
"ConnectionString": "Data Source=avalonia-api.db",
|
||||
"Provider": "MySQL",
|
||||
"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,
|
||||
"RecreateDatabase": false,
|
||||
"EnableDetailedLog": false,
|
||||
|
||||
@@ -2,12 +2,24 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace Avalonia_Common.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 统一端点响应契约。
|
||||
/// </summary>
|
||||
public interface IApiResponse
|
||||
{
|
||||
/// <summary>是否成功。</summary>
|
||||
bool Success { get; }
|
||||
|
||||
/// <summary>业务状态码。</summary>
|
||||
int Code { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 统一 API 返回格式。
|
||||
/// 所有接口的返回都包装为此格式,确保前端收到一致的数据结构。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">业务数据类型</typeparam>
|
||||
public class ApiResponse<T>
|
||||
public class ApiResponse<T> : IApiResponse
|
||||
{
|
||||
/// <summary>是否成功</summary>
|
||||
[JsonPropertyName("success")]
|
||||
@@ -113,7 +125,7 @@ namespace Avalonia_Common.Core
|
||||
/// <summary>
|
||||
/// 分页返回格式
|
||||
/// </summary>
|
||||
public class PagedResponse<T>
|
||||
public class PagedResponse<T> : IApiResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取或设置操作是否成功。
|
||||
|
||||
@@ -8,17 +8,17 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.7" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.0">
|
||||
<PackageReference Include="MySql.EntityFrameworkCore" Version="10.0.7" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.7" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -13,9 +13,92 @@ namespace Avalonia_EFCore.Database
|
||||
/// <param name="args">命令行参数。</param>
|
||||
/// <returns>配置好的数据上下文实例。</returns>
|
||||
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();
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Avalonia_EFCore.Database
|
||||
@@ -19,6 +20,14 @@ namespace Avalonia_EFCore.Database
|
||||
// 注册配置
|
||||
services.AddSingleton(config);
|
||||
|
||||
if (typeof(TContext) == typeof(AppDataContext))
|
||||
{
|
||||
services.AddProviderAppDataContext(config);
|
||||
services.AddScoped<DatabaseManager<TContext>>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
// 注册 DbContext
|
||||
services.AddDbContext<TContext>(options =>
|
||||
{
|
||||
@@ -31,6 +40,31 @@ namespace Avalonia_EFCore.Database
|
||||
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>
|
||||
|
||||
@@ -52,7 +52,7 @@ namespace Avalonia_EFCore.Database
|
||||
opts.UseNpgsql(cs, o => { o.CommandTimeout(timeout); o.EnableRetryOnFailure(3); }));
|
||||
|
||||
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
|
||||
|
||||
namespace Avalonia_EFCore.Migrations
|
||||
namespace Avalonia_EFCore.Migrations.SQLite
|
||||
{
|
||||
[DbContext(typeof(AppDataContext))]
|
||||
[DbContext(typeof(SqliteAppDataContext))]
|
||||
[Migration("20260514000100_InitialCreate")]
|
||||
partial class InitialCreate
|
||||
{
|
||||
+3
-6
@@ -1,11 +1,10 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Metadata;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Avalonia_EFCore.Migrations
|
||||
namespace Avalonia_EFCore.Migrations.SQLite
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始数据库基线。后续软件版本只追加新的 Migration,不修改已发布 Migration。
|
||||
@@ -21,8 +20,7 @@ namespace Avalonia_EFCore.Migrations
|
||||
Id = table.Column<int>(name: "id", nullable: false, comment: "用户主键")
|
||||
.Annotation("SqlServer:Identity", "1, 1")
|
||||
.Annotation("Sqlite:Autoincrement", true)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Name = table.Column<string>(name: "name", maxLength: 100, nullable: true, comment: "用户名称"),
|
||||
Email = table.Column<string>(name: "email", maxLength: 200, nullable: true, 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: "天气预报主键")
|
||||
.Annotation("SqlServer:Identity", "1, 1")
|
||||
.Annotation("Sqlite:Autoincrement", true)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
|
||||
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Date = table.Column<DateOnly>(name: "date", nullable: false, comment: "预报日期"),
|
||||
TemperatureC = table.Column<int>(name: "temperature-c", nullable: false, 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
|
||||
|
||||
namespace Avalonia_EFCore.Migrations
|
||||
namespace Avalonia_EFCore.Migrations.SQLite
|
||||
{
|
||||
[DbContext(typeof(AppDataContext))]
|
||||
[DbContext(typeof(SqliteAppDataContext))]
|
||||
[Migration("20260515072045_AutoMigration_20260515152037")]
|
||||
partial class AutoMigration_20260515152037
|
||||
{
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Avalonia_EFCore.Migrations
|
||||
namespace Avalonia_EFCore.Migrations.SQLite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AutoMigration_20260515152037 : Migration
|
||||
+2
-2
@@ -8,9 +8,9 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Avalonia_EFCore.Migrations
|
||||
namespace Avalonia_EFCore.Migrations.SQLite
|
||||
{
|
||||
[DbContext(typeof(AppDataContext))]
|
||||
[DbContext(typeof(SqliteAppDataContext))]
|
||||
[Migration("20260515085847_AutoMigration_20260515165835")]
|
||||
partial class AutoMigration_20260515165835
|
||||
{
|
||||
+1
-1
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Avalonia_EFCore.Migrations
|
||||
namespace Avalonia_EFCore.Migrations.SQLite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
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
|
||||
|
||||
namespace Avalonia_EFCore.Migrations
|
||||
namespace Avalonia_EFCore.Migrations.SQLite
|
||||
{
|
||||
[DbContext(typeof(AppDataContext))]
|
||||
partial class AppDataContextModelSnapshot : ModelSnapshot
|
||||
[DbContext(typeof(SqliteAppDataContext))]
|
||||
partial class SqliteAppDataContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
|
||||
|
||||
modelBuilder.Entity("Avalonia_EFCore.Models.ApiRefreshTokenEntity", b =>
|
||||
{
|
||||
@@ -102,6 +102,12 @@ namespace Avalonia_EFCore.Migrations
|
||||
.HasColumnName("name")
|
||||
.HasComment("用户名称");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("password-hash")
|
||||
.HasComment("密码哈希值");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasMaxLength(50)
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,14 @@ namespace Avalonia_EFCore.Models
|
||||
[MaxLength(100)]
|
||||
public string? Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置用户密码哈希值。
|
||||
/// </summary>
|
||||
[Comment("密码哈希值")]
|
||||
[Column("password-hash")]
|
||||
[MaxLength(200)]
|
||||
public string? PasswordHash { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 获取或设置用户邮箱。
|
||||
/// </summary>
|
||||
|
||||
@@ -3,7 +3,6 @@ using Avalonia_Common.Core;
|
||||
using Avalonia_Services.Core;
|
||||
using Avalonia_Services.Services.AuthService;
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Avalonia_PC.Authentication
|
||||
@@ -14,16 +13,10 @@ namespace Avalonia_PC.Authentication
|
||||
/// </summary>
|
||||
public sealed class PcAuthEndpointService(PcGlobalTokenService tokenService) : IPcAuthEndpointService
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<object?> AuthorizeAsync(ServiceEndpointContext ctx)
|
||||
public async Task<IApiResponse> AuthorizeAsync(PcAuthorizeRequest request, ServiceEndpointContext ctx)
|
||||
{
|
||||
var request = Deserialize<PcAuthorizeRequest>(ctx.Body);
|
||||
var token = await tokenService.AuthorizeAsync(request?.AuthorizationCode);
|
||||
var token = await tokenService.AuthorizeAsync(request.AuthorizationCode);
|
||||
if (token is null)
|
||||
{
|
||||
ctx.StatusCode = 401;
|
||||
@@ -34,10 +27,9 @@ namespace Avalonia_PC.Authentication
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<object?> RefreshAsync(ServiceEndpointContext ctx)
|
||||
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);
|
||||
if (refreshed is null)
|
||||
{
|
||||
@@ -49,25 +41,11 @@ namespace Avalonia_PC.Authentication
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<object?> LogoutAsync(ServiceEndpointContext ctx)
|
||||
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);
|
||||
return Task.FromResult<object?>(ResponseHelper.Succeed("退出成功"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 JSON 请求体反序列化为指定类型。
|
||||
/// </summary>
|
||||
/// <typeparam name="T">目标类型。</typeparam>
|
||||
/// <param name="body">JSON 请求体字符串,可为空。</param>
|
||||
/// <returns>反序列化后的对象;若 body 为空则返回默认值。</returns>
|
||||
private static T? Deserialize<T>(string? body)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(body)
|
||||
? default
|
||||
: JsonSerializer.Deserialize<T>(body, JsonOptions);
|
||||
return Task.FromResult<IApiResponse>(ResponseHelper.Succeed("退出成功"));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<ApplicationIcon>Assets\avalonia-logo.ico</ApplicationIcon>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -36,7 +37,7 @@
|
||||
</PackageReference>
|
||||
<PackageReference Include="Avalonia.Controls.WebView" Version="12.0.0" />
|
||||
<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>
|
||||
|
||||
@@ -91,9 +91,6 @@ namespace Avalonia_PC
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
#if DEBUG
|
||||
.WithDeveloperTools()
|
||||
#endif
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<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.Sinks.Console" 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);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
using Avalonia_Common.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
@@ -177,6 +178,14 @@ namespace Avalonia_Services.Core
|
||||
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>
|
||||
@@ -192,6 +201,33 @@ namespace Avalonia_Services.Core
|
||||
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>
|
||||
/// 注册一个 POST 端点。
|
||||
/// </summary>
|
||||
@@ -200,6 +236,14 @@ namespace Avalonia_Services.Core
|
||||
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>
|
||||
@@ -215,6 +259,33 @@ namespace Avalonia_Services.Core
|
||||
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>
|
||||
/// 注册一个 PUT 端点。
|
||||
/// </summary>
|
||||
@@ -223,6 +294,14 @@ namespace Avalonia_Services.Core
|
||||
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>
|
||||
@@ -238,6 +317,33 @@ namespace Avalonia_Services.Core
|
||||
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>
|
||||
/// 注册一个 DELETE 端点。
|
||||
/// </summary>
|
||||
@@ -246,6 +352,14 @@ namespace Avalonia_Services.Core
|
||||
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>
|
||||
@@ -261,6 +375,33 @@ namespace Avalonia_Services.Core
|
||||
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>
|
||||
@@ -318,6 +459,33 @@ namespace Avalonia_Services.Core
|
||||
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>
|
||||
|
||||
@@ -32,6 +32,11 @@ namespace Avalonia_Services.Core
|
||||
/// </summary>
|
||||
public Dictionary<string, string> Query { get; init; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// 路由路径参数。
|
||||
/// </summary>
|
||||
public Dictionary<string, string> RouteValues { get; init; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <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>
|
||||
private static async Task<object?> GetWeatherForecastsAsync(ServiceEndpointContext ctx)
|
||||
private static async Task<IApiResponse> GetWeatherForecastsAsync(ServiceEndpointContext ctx)
|
||||
{
|
||||
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
|
||||
|
||||
@@ -94,7 +94,7 @@ namespace Avalonia_Services.Endpoints
|
||||
/// </summary>
|
||||
/// <param name="ctx">服务端点上下文。</param>
|
||||
/// <returns>用户信息。</returns>
|
||||
private static async Task<object?> GetUserFromDatabaseAsync(ServiceEndpointContext ctx)
|
||||
private static async Task<IApiResponse> GetUserFromDatabaseAsync(ServiceEndpointContext ctx)
|
||||
{
|
||||
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
|
||||
|
||||
@@ -119,7 +119,7 @@ namespace Avalonia_Services.Endpoints
|
||||
/// </summary>
|
||||
/// <param name="ctx">服务端点上下文。</param>
|
||||
/// <returns>处理结果。</returns>
|
||||
private static async Task<object?> ProcessDataAsync(ServiceEndpointContext ctx)
|
||||
private static async Task<IApiResponse> ProcessDataAsync(ServiceEndpointContext ctx)
|
||||
{
|
||||
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
|
||||
|
||||
|
||||
@@ -16,17 +16,23 @@ namespace Avalonia_Services.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")
|
||||
.WithOpenApi("Auth", "API 登录,返回 access token 和 refresh token。", "", typeof(ApiLoginRequest), typeof(AuthTokenResponse))
|
||||
.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")
|
||||
.WithOpenApi("Auth", "API refresh token 轮换。", "", typeof(ApiRefreshTokenRequest), typeof(AuthTokenResponse))
|
||||
.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")
|
||||
.WithOpenApi("Auth", "API 退出登录并吊销 refresh token。", "", typeof(ApiLogoutRequest))
|
||||
.ApiOnly();
|
||||
@@ -41,17 +47,23 @@ namespace Avalonia_Services.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")
|
||||
.WithOpenApi("Auth", "PC 授权码登录,生成本地全局 token。", "", typeof(PcAuthorizeRequest), typeof(PcTokenResponse))
|
||||
.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")
|
||||
.WithOpenApi("Auth", "PC 全局 token 刷新。", "", typeof(PcRefreshRequest), typeof(PcTokenResponse))
|
||||
.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")
|
||||
.WithOpenApi("Auth", "PC 退出登录。", "", typeof(PcLogoutRequest))
|
||||
.PcOnly();
|
||||
|
||||
@@ -109,10 +109,19 @@ namespace Avalonia_Services.Extensions
|
||||
Dictionary<string, string>? query = null)
|
||||
{
|
||||
// 查找匹配的端点(忽略大小写 + 方法匹配)
|
||||
var endpoint = _endpoints.Endpoints.FirstOrDefault(e =>
|
||||
var match = _endpoints.Endpoints
|
||||
.Where(e =>
|
||||
e.SupportsHost(EndpointHostTarget.Pc) &&
|
||||
string.Equals(e.Pattern, path, StringComparison.OrdinalIgnoreCase) &&
|
||||
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)
|
||||
{
|
||||
@@ -127,6 +136,7 @@ namespace Avalonia_Services.Extensions
|
||||
Body = body,
|
||||
Headers = headers ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
|
||||
Query = query ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase),
|
||||
RouteValues = match!.RouteValues,
|
||||
Items = { ["ServiceProvider"] = _serviceProvider },
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Avalonia_Common.Core;
|
||||
using Avalonia_Services.Core;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -13,21 +14,21 @@ namespace Avalonia_Services.Services.AuthService
|
||||
/// </summary>
|
||||
/// <param name="ctx">服务端点上下文。</param>
|
||||
/// <returns>包含 Token 的认证响应。</returns>
|
||||
Task<object?> LoginAsync(ServiceEndpointContext ctx);
|
||||
Task<IApiResponse> LoginAsync(ApiLoginRequest request, ServiceEndpointContext ctx);
|
||||
|
||||
/// <summary>
|
||||
/// 使用 Refresh Token 刷新 Access Token。
|
||||
/// </summary>
|
||||
/// <param name="ctx">服务端点上下文。</param>
|
||||
/// <returns>新的 Token 对。</returns>
|
||||
Task<object?> RefreshAsync(ServiceEndpointContext ctx);
|
||||
Task<IApiResponse> RefreshAsync(ApiRefreshTokenRequest request, ServiceEndpointContext ctx);
|
||||
|
||||
/// <summary>
|
||||
/// 处理用户登出请求。
|
||||
/// </summary>
|
||||
/// <param name="ctx">服务端点上下文。</param>
|
||||
/// <returns>登出结果。</returns>
|
||||
Task<object?> LogoutAsync(ServiceEndpointContext ctx);
|
||||
Task<IApiResponse> LogoutAsync(ApiLogoutRequest request, ServiceEndpointContext ctx);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -40,20 +41,20 @@ namespace Avalonia_Services.Services.AuthService
|
||||
/// </summary>
|
||||
/// <param name="ctx">服务端点上下文。</param>
|
||||
/// <returns>包含 Token 的认证响应。</returns>
|
||||
Task<object?> AuthorizeAsync(ServiceEndpointContext ctx);
|
||||
Task<IApiResponse> AuthorizeAsync(PcAuthorizeRequest request, ServiceEndpointContext ctx);
|
||||
|
||||
/// <summary>
|
||||
/// 刷新当前 Token。
|
||||
/// </summary>
|
||||
/// <param name="ctx">服务端点上下文。</param>
|
||||
/// <returns>新的 Token 响应。</returns>
|
||||
Task<object?> RefreshAsync(ServiceEndpointContext ctx);
|
||||
Task<IApiResponse> RefreshAsync(PcRefreshRequest request, ServiceEndpointContext ctx);
|
||||
|
||||
/// <summary>
|
||||
/// 处理用户登出请求。
|
||||
/// </summary>
|
||||
/// <param name="ctx">服务端点上下文。</param>
|
||||
/// <returns>登出结果。</returns>
|
||||
Task<object?> LogoutAsync(ServiceEndpointContext ctx);
|
||||
Task<IApiResponse> LogoutAsync(PcLogoutRequest request, ServiceEndpointContext ctx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
+54
-27
@@ -1,6 +1,7 @@
|
||||
param(
|
||||
[string]$Name,
|
||||
[string]$Context = "AppDataContext",
|
||||
[ValidateSet("SQLite", "SqlServer", "PostgreSQL", "MySQL", "All")]
|
||||
[string]$Provider = "All",
|
||||
[string]$Project = "Avalonia-EFCore/Avalonia-EFCore.csproj",
|
||||
[string]$StartupProject = "Avalonia-API/Avalonia-API.csproj",
|
||||
[string]$OutputDir = "Migrations"
|
||||
@@ -21,45 +22,71 @@ if ($LASTEXITCODE -ne 0) {
|
||||
throw "dotnet tool restore failed."
|
||||
}
|
||||
|
||||
Write-Host "Generating migration '$Name'..."
|
||||
dotnet tool run dotnet-ef migrations add $Name `
|
||||
--project $Project `
|
||||
--startup-project $StartupProject `
|
||||
--context $Context `
|
||||
--output-dir $OutputDir
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "dotnet ef migrations add failed."
|
||||
function Get-ContextName([string]$providerName) {
|
||||
switch ($providerName) {
|
||||
"SQLite" { return "SqliteAppDataContext" }
|
||||
"SqlServer" { return "SqlServerAppDataContext" }
|
||||
"PostgreSQL" { return "PostgreSqlAppDataContext" }
|
||||
"MySQL" { return "MySqlAppDataContext" }
|
||||
default { throw "Unsupported provider '$providerName'." }
|
||||
}
|
||||
}
|
||||
|
||||
$migrationDir = Join-Path (Split-Path $Project -Parent) $OutputDir
|
||||
$migrationFile = Get-ChildItem $migrationDir -Filter "*_$Name.cs" |
|
||||
function Add-ProviderMigration([string]$providerName) {
|
||||
$context = Get-ContextName $providerName
|
||||
$providerOutputDir = Join-Path $OutputDir $providerName
|
||||
|
||||
Write-Host "Generating migration '$Name' for $providerName..."
|
||||
dotnet tool run dotnet-ef migrations add $Name `
|
||||
--project $Project `
|
||||
--startup-project $StartupProject `
|
||||
--context $context `
|
||||
--output-dir $providerOutputDir
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "dotnet ef migrations add failed for $providerName."
|
||||
}
|
||||
|
||||
$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'."
|
||||
}
|
||||
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")
|
||||
$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 { "" }
|
||||
$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'..."
|
||||
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
|
||||
--context $context
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "dotnet ef migrations remove failed."
|
||||
throw "dotnet ef migrations remove failed for $providerName."
|
||||
}
|
||||
exit 0
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "Migration generated for ${providerName}:"
|
||||
Write-Host " $($migrationFile.FullName)"
|
||||
}
|
||||
|
||||
Write-Host "Migration generated:"
|
||||
Write-Host " $($migrationFile.FullName)"
|
||||
Write-Host "Review the migration, then start the app. Startup will automatically apply pending migrations."
|
||||
$providers = if ($Provider -eq "All") {
|
||||
@("SQLite", "SqlServer", "PostgreSQL", "MySQL")
|
||||
} 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."
|
||||
|
||||
Reference in New Issue
Block a user