diff --git a/FileShare-EFCore/Migrations/MySQL/20260522092342_AddPlaybackPosition.Designer.cs b/FileShare-EFCore/Migrations/MySQL/20260522092342_AddPlaybackPosition.Designer.cs
new file mode 100644
index 0000000..91c2f38
--- /dev/null
+++ b/FileShare-EFCore/Migrations/MySQL/20260522092342_AddPlaybackPosition.Designer.cs
@@ -0,0 +1,448 @@
+//
+using System;
+using FileShare_EFCore.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace FileShare_EFCore.Migrations.MySQL
+{
+ [DbContext(typeof(MySqlAppDataContext))]
+ [Migration("20260522092342_AddPlaybackPosition")]
+ partial class AddPlaybackPosition
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.7")
+ .HasAnnotation("Relational:MaxIdentifierLength", 64);
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)")
+ .HasColumnName("created-at");
+
+ b.Property("Device")
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)")
+ .HasColumnName("device");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("datetime(6)")
+ .HasColumnName("expires-at");
+
+ b.Property("IpAddress")
+ .HasMaxLength(64)
+ .HasColumnType("varchar(64)")
+ .HasColumnName("ip-address");
+
+ b.Property("ReplacedByTokenHash")
+ .HasMaxLength(128)
+ .HasColumnType("varchar(128)")
+ .HasColumnName("replaced-by-token-hash");
+
+ b.Property("RevokedAt")
+ .HasColumnType("datetime(6)")
+ .HasColumnName("revoked-at");
+
+ b.Property("TokenHash")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("varchar(128)")
+ .HasColumnName("token-hash");
+
+ b.Property("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("FileShare_EFCore.Models.ManagedFileRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id");
+
+ b.Property("AbsolutePath")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("varchar(2048)")
+ .HasColumnName("absolute-path");
+
+ b.Property("ContentType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)")
+ .HasColumnName("content-type");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)")
+ .HasColumnName("created-at");
+
+ b.Property("Exists")
+ .HasColumnType("tinyint(1)")
+ .HasColumnName("exists");
+
+ b.Property("Extension")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("varchar(32)")
+ .HasColumnName("extension");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasMaxLength(260)
+ .HasColumnType("varchar(260)")
+ .HasColumnName("file-name");
+
+ b.Property("LastPlayedAt")
+ .HasColumnType("datetime(6)")
+ .HasColumnName("last-played-at");
+
+ b.Property("LastSeenAt")
+ .HasColumnType("datetime(6)")
+ .HasColumnName("last-seen-at");
+
+ b.Property("LastWriteTimeUtc")
+ .HasColumnType("datetime(6)")
+ .HasColumnName("last-write-time-utc");
+
+ b.Property("LibraryRootId")
+ .HasColumnType("int")
+ .HasColumnName("library-root-id");
+
+ b.Property("MediaType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("varchar(20)")
+ .HasColumnName("media-type");
+
+ b.Property("PlaybackPosition")
+ .HasColumnType("double")
+ .HasColumnName("playback-position");
+
+ b.Property("RelativePath")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("varchar(1024)")
+ .HasColumnName("relative-path");
+
+ b.Property("SizeBytes")
+ .HasColumnType("bigint")
+ .HasColumnName("size-bytes");
+
+ b.Property("ThumbnailId")
+ .HasColumnType("int")
+ .HasColumnName("thumbnail-id");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)")
+ .HasColumnName("updated-at");
+
+ b.Property("VideoDuration")
+ .HasColumnType("double")
+ .HasColumnName("video-duration");
+
+ b.HasKey("Id")
+ .HasName("pk-managed-file-record");
+
+ b.HasIndex("AbsolutePath")
+ .IsUnique()
+ .HasDatabaseName("idx-managed-file-record-absolute-path");
+
+ b.HasIndex("LastPlayedAt")
+ .HasDatabaseName("idx-managed-file-record-last-played-at");
+
+ b.HasIndex("LibraryRootId")
+ .HasDatabaseName("idx-managed-file-record-root-id");
+
+ b.HasIndex("ThumbnailId")
+ .HasDatabaseName("idx-managed-file-record-thumbnail-id");
+
+ b.HasIndex("MediaType", "Exists")
+ .HasDatabaseName("idx-managed-file-record-media-type-exists");
+
+ b.ToTable("managed-file-record", t =>
+ {
+ t.HasComment("文件库文件记录");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)")
+ .HasColumnName("created-at");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)")
+ .HasColumnName("display-name");
+
+ b.Property("IsAvailable")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("tinyint(1)")
+ .HasDefaultValue(true)
+ .HasColumnName("is-available");
+
+ b.Property("IsEnabled")
+ .HasColumnType("tinyint(1)")
+ .HasColumnName("is-enabled");
+
+ b.Property("LastScanCompletedAt")
+ .HasColumnType("datetime(6)")
+ .HasColumnName("last-scan-completed-at");
+
+ b.Property("LastScanError")
+ .HasMaxLength(2000)
+ .HasColumnType("varchar(2000)")
+ .HasColumnName("last-scan-error");
+
+ b.Property("LastScanStartedAt")
+ .HasColumnType("datetime(6)")
+ .HasColumnName("last-scan-started-at");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("varchar(1024)")
+ .HasColumnName("path");
+
+ b.Property("ScanIntervalMinutes")
+ .HasColumnType("int")
+ .HasColumnName("scan-interval-minutes");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)")
+ .HasColumnName("updated-at");
+
+ b.HasKey("Id")
+ .HasName("pk-managed-library-root");
+
+ b.HasIndex("Path")
+ .IsUnique()
+ .HasDatabaseName("idx-managed-library-root-path");
+
+ b.ToTable("managed-library-root", t =>
+ {
+ t.HasComment("文件库根目录");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id");
+
+ b.Property("ContentType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)")
+ .HasColumnName("content-type");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)")
+ .HasColumnName("created-at");
+
+ b.Property("LibraryRootId")
+ .HasColumnType("int")
+ .HasColumnName("library-root-id");
+
+ b.Property("RelativePath")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("varchar(1024)")
+ .HasColumnName("relative-path");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)")
+ .HasColumnName("updated-at");
+
+ b.HasKey("Id")
+ .HasName("pk-managed-thumbnail-map");
+
+ b.HasIndex("LibraryRootId")
+ .HasDatabaseName("idx-managed-thumbnail-map-root-id");
+
+ b.HasIndex("RelativePath")
+ .IsUnique()
+ .HasDatabaseName("idx-managed-thumbnail-map-relative-path");
+
+ b.ToTable("managed-thumbnail-map", t =>
+ {
+ t.HasComment("文件缩略图映射记录");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id")
+ .HasComment("用户主键");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)")
+ .HasColumnName("created-at")
+ .HasComment("创建时间");
+
+ b.Property("Email")
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)")
+ .HasColumnName("email")
+ .HasComment("用户邮箱");
+
+ b.Property("Name")
+ .HasMaxLength(100)
+ .HasColumnType("varchar(100)")
+ .HasColumnName("name")
+ .HasComment("用户名称");
+
+ b.Property("PasswordHash")
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)")
+ .HasColumnName("password-hash")
+ .HasComment("密码哈希值");
+
+ b.Property("PhoneNumber")
+ .HasMaxLength(50)
+ .HasColumnType("varchar(50)")
+ .HasColumnName("phone-number")
+ .HasComment("电话号码");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)")
+ .HasColumnName("updated-at")
+ .HasComment("更新时间");
+
+ b.HasKey("Id")
+ .HasName("pk-user");
+
+ b.ToTable("user", t =>
+ {
+ t.HasComment("用户实体,演示数据库 CRUD 操作");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id")
+ .HasComment("天气预报主键");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime(6)")
+ .HasColumnName("created-at")
+ .HasComment("创建时间");
+
+ b.Property("Date")
+ .HasColumnType("date")
+ .HasColumnName("date")
+ .HasComment("预报日期");
+
+ b.Property("Summary")
+ .HasMaxLength(200)
+ .HasColumnType("varchar(200)")
+ .HasColumnName("summary")
+ .HasComment("天气摘要");
+
+ b.Property("TemperatureC")
+ .HasColumnType("int")
+ .HasColumnName("temperature-c")
+ .HasComment("摄氏温度");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime(6)")
+ .HasColumnName("updated-at")
+ .HasComment("更新时间");
+
+ b.HasKey("Id")
+ .HasName("pk-weather-forecast");
+
+ b.ToTable("weather-forecast", t =>
+ {
+ t.HasComment("天气预报数据实体");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b =>
+ {
+ b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
+ .WithMany("Files")
+ .HasForeignKey("LibraryRootId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("FileShare_EFCore.Models.ManagedThumbnailMap", "Thumbnail")
+ .WithMany("Files")
+ .HasForeignKey("ThumbnailId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.Navigation("LibraryRoot");
+
+ b.Navigation("Thumbnail");
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
+ {
+ b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
+ .WithMany("Thumbnails")
+ .HasForeignKey("LibraryRootId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("LibraryRoot");
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
+ {
+ b.Navigation("Files");
+
+ b.Navigation("Thumbnails");
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
+ {
+ b.Navigation("Files");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/FileShare-EFCore/Migrations/MySQL/20260522092342_AddPlaybackPosition.cs b/FileShare-EFCore/Migrations/MySQL/20260522092342_AddPlaybackPosition.cs
new file mode 100644
index 0000000..6cae1f0
--- /dev/null
+++ b/FileShare-EFCore/Migrations/MySQL/20260522092342_AddPlaybackPosition.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace FileShare_EFCore.Migrations.MySQL
+{
+ ///
+ public partial class AddPlaybackPosition : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "playback-position",
+ table: "managed-file-record",
+ type: "double",
+ nullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "playback-position",
+ table: "managed-file-record");
+ }
+ }
+}
diff --git a/FileShare-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs b/FileShare-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs
index d8f3226..add6e0d 100644
--- a/FileShare-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs
+++ b/FileShare-EFCore/Migrations/MySQL/MySqlAppDataContextModelSnapshot.cs
@@ -140,6 +140,10 @@ namespace FileShare_EFCore.Migrations.MySQL
.HasColumnType("varchar(20)")
.HasColumnName("media-type");
+ b.Property("PlaybackPosition")
+ .HasColumnType("double")
+ .HasColumnName("playback-position");
+
b.Property("RelativePath")
.IsRequired()
.HasMaxLength(1024)
diff --git a/FileShare-EFCore/Migrations/PostgreSQL/20260522092330_AddPlaybackPosition.Designer.cs b/FileShare-EFCore/Migrations/PostgreSQL/20260522092330_AddPlaybackPosition.Designer.cs
new file mode 100644
index 0000000..88e03c3
--- /dev/null
+++ b/FileShare-EFCore/Migrations/PostgreSQL/20260522092330_AddPlaybackPosition.Designer.cs
@@ -0,0 +1,463 @@
+//
+using System;
+using FileShare_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 FileShare_EFCore.Migrations.PostgreSQL
+{
+ [DbContext(typeof(PostgreSqlAppDataContext))]
+ [Migration("20260522092330_AddPlaybackPosition")]
+ partial class AddPlaybackPosition
+ {
+ ///
+ 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("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created-at");
+
+ b.Property("Device")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("device");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expires-at");
+
+ b.Property("IpAddress")
+ .HasMaxLength(64)
+ .HasColumnType("character varying(64)")
+ .HasColumnName("ip-address");
+
+ b.Property("ReplacedByTokenHash")
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasColumnName("replaced-by-token-hash");
+
+ b.Property("RevokedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("revoked-at");
+
+ b.Property("TokenHash")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("character varying(128)")
+ .HasColumnName("token-hash");
+
+ b.Property("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("FileShare_EFCore.Models.ManagedFileRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AbsolutePath")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("character varying(2048)")
+ .HasColumnName("absolute-path");
+
+ b.Property("ContentType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("content-type");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created-at");
+
+ b.Property("Exists")
+ .HasColumnType("boolean")
+ .HasColumnName("exists");
+
+ b.Property("Extension")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("character varying(32)")
+ .HasColumnName("extension");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasMaxLength(260)
+ .HasColumnType("character varying(260)")
+ .HasColumnName("file-name");
+
+ b.Property("LastPlayedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last-played-at");
+
+ b.Property("LastSeenAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last-seen-at");
+
+ b.Property("LastWriteTimeUtc")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last-write-time-utc");
+
+ b.Property("LibraryRootId")
+ .HasColumnType("integer")
+ .HasColumnName("library-root-id");
+
+ b.Property("MediaType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasColumnName("media-type");
+
+ b.Property("PlaybackPosition")
+ .HasColumnType("double precision")
+ .HasColumnName("playback-position");
+
+ b.Property("RelativePath")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)")
+ .HasColumnName("relative-path");
+
+ b.Property("SizeBytes")
+ .HasColumnType("bigint")
+ .HasColumnName("size-bytes");
+
+ b.Property("ThumbnailId")
+ .HasColumnType("integer")
+ .HasColumnName("thumbnail-id");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated-at");
+
+ b.Property("VideoDuration")
+ .HasColumnType("double precision")
+ .HasColumnName("video-duration");
+
+ b.HasKey("Id")
+ .HasName("pk-managed-file-record");
+
+ b.HasIndex("AbsolutePath")
+ .IsUnique()
+ .HasDatabaseName("idx-managed-file-record-absolute-path");
+
+ b.HasIndex("LastPlayedAt")
+ .HasDatabaseName("idx-managed-file-record-last-played-at");
+
+ b.HasIndex("LibraryRootId")
+ .HasDatabaseName("idx-managed-file-record-root-id");
+
+ b.HasIndex("ThumbnailId")
+ .HasDatabaseName("idx-managed-file-record-thumbnail-id");
+
+ b.HasIndex("MediaType", "Exists")
+ .HasDatabaseName("idx-managed-file-record-media-type-exists");
+
+ b.ToTable("managed-file-record", t =>
+ {
+ t.HasComment("文件库文件记录");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created-at");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("display-name");
+
+ b.Property("IsAvailable")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("boolean")
+ .HasDefaultValue(true)
+ .HasColumnName("is-available");
+
+ b.Property("IsEnabled")
+ .HasColumnType("boolean")
+ .HasColumnName("is-enabled");
+
+ b.Property("LastScanCompletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last-scan-completed-at");
+
+ b.Property("LastScanError")
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)")
+ .HasColumnName("last-scan-error");
+
+ b.Property("LastScanStartedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last-scan-started-at");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)")
+ .HasColumnName("path");
+
+ b.Property("ScanIntervalMinutes")
+ .HasColumnType("integer")
+ .HasColumnName("scan-interval-minutes");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated-at");
+
+ b.HasKey("Id")
+ .HasName("pk-managed-library-root");
+
+ b.HasIndex("Path")
+ .IsUnique()
+ .HasDatabaseName("idx-managed-library-root-path");
+
+ b.ToTable("managed-library-root", t =>
+ {
+ t.HasComment("文件库根目录");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ContentType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("content-type");
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created-at");
+
+ b.Property("LibraryRootId")
+ .HasColumnType("integer")
+ .HasColumnName("library-root-id");
+
+ b.Property("RelativePath")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("character varying(1024)")
+ .HasColumnName("relative-path");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated-at");
+
+ b.HasKey("Id")
+ .HasName("pk-managed-thumbnail-map");
+
+ b.HasIndex("LibraryRootId")
+ .HasDatabaseName("idx-managed-thumbnail-map-root-id");
+
+ b.HasIndex("RelativePath")
+ .IsUnique()
+ .HasDatabaseName("idx-managed-thumbnail-map-relative-path");
+
+ b.ToTable("managed-thumbnail-map", t =>
+ {
+ t.HasComment("文件缩略图映射记录");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id")
+ .HasComment("用户主键");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created-at")
+ .HasComment("创建时间");
+
+ b.Property("Email")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("email")
+ .HasComment("用户邮箱");
+
+ b.Property("Name")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("name")
+ .HasComment("用户名称");
+
+ b.Property("PasswordHash")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("password-hash")
+ .HasComment("密码哈希值");
+
+ b.Property("PhoneNumber")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("phone-number")
+ .HasComment("电话号码");
+
+ b.Property("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("FileShare_EFCore.Models.WeatherForecastEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("id")
+ .HasComment("天气预报主键");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created-at")
+ .HasComment("创建时间");
+
+ b.Property("Date")
+ .HasColumnType("date")
+ .HasColumnName("date")
+ .HasComment("预报日期");
+
+ b.Property("Summary")
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("summary")
+ .HasComment("天气摘要");
+
+ b.Property("TemperatureC")
+ .HasColumnType("integer")
+ .HasColumnName("temperature-c")
+ .HasComment("摄氏温度");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("updated-at")
+ .HasComment("更新时间");
+
+ b.HasKey("Id")
+ .HasName("pk-weather-forecast");
+
+ b.ToTable("weather-forecast", t =>
+ {
+ t.HasComment("天气预报数据实体");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b =>
+ {
+ b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
+ .WithMany("Files")
+ .HasForeignKey("LibraryRootId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("FileShare_EFCore.Models.ManagedThumbnailMap", "Thumbnail")
+ .WithMany("Files")
+ .HasForeignKey("ThumbnailId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.Navigation("LibraryRoot");
+
+ b.Navigation("Thumbnail");
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
+ {
+ b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
+ .WithMany("Thumbnails")
+ .HasForeignKey("LibraryRootId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("LibraryRoot");
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
+ {
+ b.Navigation("Files");
+
+ b.Navigation("Thumbnails");
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
+ {
+ b.Navigation("Files");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/FileShare-EFCore/Migrations/PostgreSQL/20260522092330_AddPlaybackPosition.cs b/FileShare-EFCore/Migrations/PostgreSQL/20260522092330_AddPlaybackPosition.cs
new file mode 100644
index 0000000..8a9e89a
--- /dev/null
+++ b/FileShare-EFCore/Migrations/PostgreSQL/20260522092330_AddPlaybackPosition.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace FileShare_EFCore.Migrations.PostgreSQL
+{
+ ///
+ public partial class AddPlaybackPosition : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "playback-position",
+ table: "managed-file-record",
+ type: "double precision",
+ nullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "playback-position",
+ table: "managed-file-record");
+ }
+ }
+}
diff --git a/FileShare-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs b/FileShare-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs
index 2134fc5..f84c2a0 100644
--- a/FileShare-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs
+++ b/FileShare-EFCore/Migrations/PostgreSQL/PostgreSqlAppDataContextModelSnapshot.cs
@@ -147,6 +147,10 @@ namespace FileShare_EFCore.Migrations.PostgreSQL
.HasColumnType("character varying(20)")
.HasColumnName("media-type");
+ b.Property("PlaybackPosition")
+ .HasColumnType("double precision")
+ .HasColumnName("playback-position");
+
b.Property("RelativePath")
.IsRequired()
.HasMaxLength(1024)
diff --git a/FileShare-EFCore/Migrations/SQLite/20260522092304_AddPlaybackPosition.Designer.cs b/FileShare-EFCore/Migrations/SQLite/20260522092304_AddPlaybackPosition.Designer.cs
new file mode 100644
index 0000000..1c7da55
--- /dev/null
+++ b/FileShare-EFCore/Migrations/SQLite/20260522092304_AddPlaybackPosition.Designer.cs
@@ -0,0 +1,446 @@
+//
+using System;
+using FileShare_EFCore.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace FileShare_EFCore.Migrations.SQLite
+{
+ [DbContext(typeof(SqliteAppDataContext))]
+ [Migration("20260522092304_AddPlaybackPosition")]
+ partial class AddPlaybackPosition
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.7");
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created-at");
+
+ b.Property("Device")
+ .HasMaxLength(200)
+ .HasColumnType("TEXT")
+ .HasColumnName("device");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("expires-at");
+
+ b.Property("IpAddress")
+ .HasMaxLength(64)
+ .HasColumnType("TEXT")
+ .HasColumnName("ip-address");
+
+ b.Property("ReplacedByTokenHash")
+ .HasMaxLength(128)
+ .HasColumnType("TEXT")
+ .HasColumnName("replaced-by-token-hash");
+
+ b.Property("RevokedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("revoked-at");
+
+ b.Property("TokenHash")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("TEXT")
+ .HasColumnName("token-hash");
+
+ b.Property("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("FileShare_EFCore.Models.ManagedFileRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id");
+
+ b.Property("AbsolutePath")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("TEXT")
+ .HasColumnName("absolute-path");
+
+ b.Property("ContentType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT")
+ .HasColumnName("content-type");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created-at");
+
+ b.Property("Exists")
+ .HasColumnType("INTEGER")
+ .HasColumnName("exists");
+
+ b.Property("Extension")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("TEXT")
+ .HasColumnName("extension");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasMaxLength(260)
+ .HasColumnType("TEXT")
+ .HasColumnName("file-name");
+
+ b.Property("LastPlayedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("last-played-at");
+
+ b.Property("LastSeenAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("last-seen-at");
+
+ b.Property("LastWriteTimeUtc")
+ .HasColumnType("TEXT")
+ .HasColumnName("last-write-time-utc");
+
+ b.Property("LibraryRootId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("library-root-id");
+
+ b.Property("MediaType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("TEXT")
+ .HasColumnName("media-type");
+
+ b.Property("PlaybackPosition")
+ .HasColumnType("REAL")
+ .HasColumnName("playback-position");
+
+ b.Property("RelativePath")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("TEXT")
+ .HasColumnName("relative-path");
+
+ b.Property("SizeBytes")
+ .HasColumnType("INTEGER")
+ .HasColumnName("size-bytes");
+
+ b.Property("ThumbnailId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("thumbnail-id");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("updated-at");
+
+ b.Property("VideoDuration")
+ .HasColumnType("REAL")
+ .HasColumnName("video-duration");
+
+ b.HasKey("Id")
+ .HasName("pk-managed-file-record");
+
+ b.HasIndex("AbsolutePath")
+ .IsUnique()
+ .HasDatabaseName("idx-managed-file-record-absolute-path");
+
+ b.HasIndex("LastPlayedAt")
+ .HasDatabaseName("idx-managed-file-record-last-played-at");
+
+ b.HasIndex("LibraryRootId")
+ .HasDatabaseName("idx-managed-file-record-root-id");
+
+ b.HasIndex("ThumbnailId")
+ .HasDatabaseName("idx-managed-file-record-thumbnail-id");
+
+ b.HasIndex("MediaType", "Exists")
+ .HasDatabaseName("idx-managed-file-record-media-type-exists");
+
+ b.ToTable("managed-file-record", t =>
+ {
+ t.HasComment("文件库文件记录");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created-at");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("TEXT")
+ .HasColumnName("display-name");
+
+ b.Property("IsAvailable")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasDefaultValue(true)
+ .HasColumnName("is-available");
+
+ b.Property("IsEnabled")
+ .HasColumnType("INTEGER")
+ .HasColumnName("is-enabled");
+
+ b.Property("LastScanCompletedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("last-scan-completed-at");
+
+ b.Property("LastScanError")
+ .HasMaxLength(2000)
+ .HasColumnType("TEXT")
+ .HasColumnName("last-scan-error");
+
+ b.Property("LastScanStartedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("last-scan-started-at");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("TEXT")
+ .HasColumnName("path");
+
+ b.Property("ScanIntervalMinutes")
+ .HasColumnType("INTEGER")
+ .HasColumnName("scan-interval-minutes");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("updated-at");
+
+ b.HasKey("Id")
+ .HasName("pk-managed-library-root");
+
+ b.HasIndex("Path")
+ .IsUnique()
+ .HasDatabaseName("idx-managed-library-root-path");
+
+ b.ToTable("managed-library-root", t =>
+ {
+ t.HasComment("文件库根目录");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id");
+
+ b.Property("ContentType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("TEXT")
+ .HasColumnName("content-type");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created-at");
+
+ b.Property("LibraryRootId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("library-root-id");
+
+ b.Property("RelativePath")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("TEXT")
+ .HasColumnName("relative-path");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("updated-at");
+
+ b.HasKey("Id")
+ .HasName("pk-managed-thumbnail-map");
+
+ b.HasIndex("LibraryRootId")
+ .HasDatabaseName("idx-managed-thumbnail-map-root-id");
+
+ b.HasIndex("RelativePath")
+ .IsUnique()
+ .HasDatabaseName("idx-managed-thumbnail-map-relative-path");
+
+ b.ToTable("managed-thumbnail-map", t =>
+ {
+ t.HasComment("文件缩略图映射记录");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id")
+ .HasComment("用户主键");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created-at")
+ .HasComment("创建时间");
+
+ b.Property("Email")
+ .HasMaxLength(200)
+ .HasColumnType("TEXT")
+ .HasColumnName("email")
+ .HasComment("用户邮箱");
+
+ b.Property("Name")
+ .HasMaxLength(100)
+ .HasColumnType("TEXT")
+ .HasColumnName("name")
+ .HasComment("用户名称");
+
+ b.Property("PasswordHash")
+ .HasMaxLength(200)
+ .HasColumnType("TEXT")
+ .HasColumnName("password-hash")
+ .HasComment("密码哈希值");
+
+ b.Property("PhoneNumber")
+ .HasMaxLength(50)
+ .HasColumnType("TEXT")
+ .HasColumnName("phone-number")
+ .HasComment("电话号码");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("updated-at")
+ .HasComment("更新时间");
+
+ b.HasKey("Id")
+ .HasName("pk-user");
+
+ b.ToTable("user", t =>
+ {
+ t.HasComment("用户实体,演示数据库 CRUD 操作");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("INTEGER")
+ .HasColumnName("id")
+ .HasComment("天气预报主键");
+
+ b.Property("CreatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("created-at")
+ .HasComment("创建时间");
+
+ b.Property("Date")
+ .HasColumnType("TEXT")
+ .HasColumnName("date")
+ .HasComment("预报日期");
+
+ b.Property("Summary")
+ .HasMaxLength(200)
+ .HasColumnType("TEXT")
+ .HasColumnName("summary")
+ .HasComment("天气摘要");
+
+ b.Property("TemperatureC")
+ .HasColumnType("INTEGER")
+ .HasColumnName("temperature-c")
+ .HasComment("摄氏温度");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("TEXT")
+ .HasColumnName("updated-at")
+ .HasComment("更新时间");
+
+ b.HasKey("Id")
+ .HasName("pk-weather-forecast");
+
+ b.ToTable("weather-forecast", t =>
+ {
+ t.HasComment("天气预报数据实体");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b =>
+ {
+ b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
+ .WithMany("Files")
+ .HasForeignKey("LibraryRootId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("FileShare_EFCore.Models.ManagedThumbnailMap", "Thumbnail")
+ .WithMany("Files")
+ .HasForeignKey("ThumbnailId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.Navigation("LibraryRoot");
+
+ b.Navigation("Thumbnail");
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
+ {
+ b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
+ .WithMany("Thumbnails")
+ .HasForeignKey("LibraryRootId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("LibraryRoot");
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
+ {
+ b.Navigation("Files");
+
+ b.Navigation("Thumbnails");
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
+ {
+ b.Navigation("Files");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/FileShare-EFCore/Migrations/SQLite/20260522092304_AddPlaybackPosition.cs b/FileShare-EFCore/Migrations/SQLite/20260522092304_AddPlaybackPosition.cs
new file mode 100644
index 0000000..50bee7e
--- /dev/null
+++ b/FileShare-EFCore/Migrations/SQLite/20260522092304_AddPlaybackPosition.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace FileShare_EFCore.Migrations.SQLite
+{
+ ///
+ public partial class AddPlaybackPosition : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "playback-position",
+ table: "managed-file-record",
+ type: "REAL",
+ nullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "playback-position",
+ table: "managed-file-record");
+ }
+ }
+}
diff --git a/FileShare-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs b/FileShare-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs
index 788881c..b71579a 100644
--- a/FileShare-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs
+++ b/FileShare-EFCore/Migrations/SQLite/SqliteAppDataContextModelSnapshot.cs
@@ -138,6 +138,10 @@ namespace FileShare_EFCore.Migrations.SQLite
.HasColumnType("TEXT")
.HasColumnName("media-type");
+ b.Property("PlaybackPosition")
+ .HasColumnType("REAL")
+ .HasColumnName("playback-position");
+
b.Property("RelativePath")
.IsRequired()
.HasMaxLength(1024)
diff --git a/FileShare-EFCore/Migrations/SqlServer/20260522092317_AddPlaybackPosition.Designer.cs b/FileShare-EFCore/Migrations/SqlServer/20260522092317_AddPlaybackPosition.Designer.cs
new file mode 100644
index 0000000..ef70aff
--- /dev/null
+++ b/FileShare-EFCore/Migrations/SqlServer/20260522092317_AddPlaybackPosition.Designer.cs
@@ -0,0 +1,463 @@
+//
+using System;
+using FileShare_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 FileShare_EFCore.Migrations.SqlServer
+{
+ [DbContext(typeof(SqlServerAppDataContext))]
+ [Migration("20260522092317_AddPlaybackPosition")]
+ partial class AddPlaybackPosition
+ {
+ ///
+ 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("FileShare_EFCore.Models.ApiRefreshTokenEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint")
+ .HasColumnName("id");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2")
+ .HasColumnName("created-at");
+
+ b.Property("Device")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)")
+ .HasColumnName("device");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("datetime2")
+ .HasColumnName("expires-at");
+
+ b.Property("IpAddress")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)")
+ .HasColumnName("ip-address");
+
+ b.Property("ReplacedByTokenHash")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)")
+ .HasColumnName("replaced-by-token-hash");
+
+ b.Property("RevokedAt")
+ .HasColumnType("datetime2")
+ .HasColumnName("revoked-at");
+
+ b.Property("TokenHash")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)")
+ .HasColumnName("token-hash");
+
+ b.Property("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("FileShare_EFCore.Models.ManagedFileRecord", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AbsolutePath")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("nvarchar(2048)")
+ .HasColumnName("absolute-path");
+
+ b.Property("ContentType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)")
+ .HasColumnName("content-type");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2")
+ .HasColumnName("created-at");
+
+ b.Property("Exists")
+ .HasColumnType("bit")
+ .HasColumnName("exists");
+
+ b.Property("Extension")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)")
+ .HasColumnName("extension");
+
+ b.Property("FileName")
+ .IsRequired()
+ .HasMaxLength(260)
+ .HasColumnType("nvarchar(260)")
+ .HasColumnName("file-name");
+
+ b.Property("LastPlayedAt")
+ .HasColumnType("datetime2")
+ .HasColumnName("last-played-at");
+
+ b.Property("LastSeenAt")
+ .HasColumnType("datetime2")
+ .HasColumnName("last-seen-at");
+
+ b.Property("LastWriteTimeUtc")
+ .HasColumnType("datetime2")
+ .HasColumnName("last-write-time-utc");
+
+ b.Property("LibraryRootId")
+ .HasColumnType("int")
+ .HasColumnName("library-root-id");
+
+ b.Property("MediaType")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)")
+ .HasColumnName("media-type");
+
+ b.Property("PlaybackPosition")
+ .HasColumnType("float")
+ .HasColumnName("playback-position");
+
+ b.Property("RelativePath")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("nvarchar(1024)")
+ .HasColumnName("relative-path");
+
+ b.Property("SizeBytes")
+ .HasColumnType("bigint")
+ .HasColumnName("size-bytes");
+
+ b.Property("ThumbnailId")
+ .HasColumnType("int")
+ .HasColumnName("thumbnail-id");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2")
+ .HasColumnName("updated-at");
+
+ b.Property("VideoDuration")
+ .HasColumnType("float")
+ .HasColumnName("video-duration");
+
+ b.HasKey("Id")
+ .HasName("pk-managed-file-record");
+
+ b.HasIndex("AbsolutePath")
+ .IsUnique()
+ .HasDatabaseName("idx-managed-file-record-absolute-path");
+
+ b.HasIndex("LastPlayedAt")
+ .HasDatabaseName("idx-managed-file-record-last-played-at");
+
+ b.HasIndex("LibraryRootId")
+ .HasDatabaseName("idx-managed-file-record-root-id");
+
+ b.HasIndex("ThumbnailId")
+ .HasDatabaseName("idx-managed-file-record-thumbnail-id");
+
+ b.HasIndex("MediaType", "Exists")
+ .HasDatabaseName("idx-managed-file-record-media-type-exists");
+
+ b.ToTable("managed-file-record", t =>
+ {
+ t.HasComment("文件库文件记录");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2")
+ .HasColumnName("created-at");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)")
+ .HasColumnName("display-name");
+
+ b.Property("IsAvailable")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(true)
+ .HasColumnName("is-available");
+
+ b.Property("IsEnabled")
+ .HasColumnType("bit")
+ .HasColumnName("is-enabled");
+
+ b.Property("LastScanCompletedAt")
+ .HasColumnType("datetime2")
+ .HasColumnName("last-scan-completed-at");
+
+ b.Property("LastScanError")
+ .HasMaxLength(2000)
+ .HasColumnType("nvarchar(2000)")
+ .HasColumnName("last-scan-error");
+
+ b.Property("LastScanStartedAt")
+ .HasColumnType("datetime2")
+ .HasColumnName("last-scan-started-at");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("nvarchar(1024)")
+ .HasColumnName("path");
+
+ b.Property("ScanIntervalMinutes")
+ .HasColumnType("int")
+ .HasColumnName("scan-interval-minutes");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2")
+ .HasColumnName("updated-at");
+
+ b.HasKey("Id")
+ .HasName("pk-managed-library-root");
+
+ b.HasIndex("Path")
+ .IsUnique()
+ .HasDatabaseName("idx-managed-library-root-path");
+
+ b.ToTable("managed-library-root", t =>
+ {
+ t.HasComment("文件库根目录");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("ContentType")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)")
+ .HasColumnName("content-type");
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2")
+ .HasColumnName("created-at");
+
+ b.Property("LibraryRootId")
+ .HasColumnType("int")
+ .HasColumnName("library-root-id");
+
+ b.Property("RelativePath")
+ .IsRequired()
+ .HasMaxLength(1024)
+ .HasColumnType("nvarchar(1024)")
+ .HasColumnName("relative-path");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2")
+ .HasColumnName("updated-at");
+
+ b.HasKey("Id")
+ .HasName("pk-managed-thumbnail-map");
+
+ b.HasIndex("LibraryRootId")
+ .HasDatabaseName("idx-managed-thumbnail-map-root-id");
+
+ b.HasIndex("RelativePath")
+ .IsUnique()
+ .HasDatabaseName("idx-managed-thumbnail-map-relative-path");
+
+ b.ToTable("managed-thumbnail-map", t =>
+ {
+ t.HasComment("文件缩略图映射记录");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.UserEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id")
+ .HasComment("用户主键");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2")
+ .HasColumnName("created-at")
+ .HasComment("创建时间");
+
+ b.Property("Email")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)")
+ .HasColumnName("email")
+ .HasComment("用户邮箱");
+
+ b.Property("Name")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)")
+ .HasColumnName("name")
+ .HasComment("用户名称");
+
+ b.Property("PasswordHash")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)")
+ .HasColumnName("password-hash")
+ .HasComment("密码哈希值");
+
+ b.Property("PhoneNumber")
+ .HasMaxLength(50)
+ .HasColumnType("nvarchar(50)")
+ .HasColumnName("phone-number")
+ .HasComment("电话号码");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2")
+ .HasColumnName("updated-at")
+ .HasComment("更新时间");
+
+ b.HasKey("Id")
+ .HasName("pk-user");
+
+ b.ToTable("user", t =>
+ {
+ t.HasComment("用户实体,演示数据库 CRUD 操作");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.WeatherForecastEntity", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int")
+ .HasColumnName("id")
+ .HasComment("天气预报主键");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("datetime2")
+ .HasColumnName("created-at")
+ .HasComment("创建时间");
+
+ b.Property("Date")
+ .HasColumnType("date")
+ .HasColumnName("date")
+ .HasComment("预报日期");
+
+ b.Property("Summary")
+ .HasMaxLength(200)
+ .HasColumnType("nvarchar(200)")
+ .HasColumnName("summary")
+ .HasComment("天气摘要");
+
+ b.Property("TemperatureC")
+ .HasColumnType("int")
+ .HasColumnName("temperature-c")
+ .HasComment("摄氏温度");
+
+ b.Property("UpdatedAt")
+ .HasColumnType("datetime2")
+ .HasColumnName("updated-at")
+ .HasComment("更新时间");
+
+ b.HasKey("Id")
+ .HasName("pk-weather-forecast");
+
+ b.ToTable("weather-forecast", t =>
+ {
+ t.HasComment("天气预报数据实体");
+ });
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedFileRecord", b =>
+ {
+ b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
+ .WithMany("Files")
+ .HasForeignKey("LibraryRootId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("FileShare_EFCore.Models.ManagedThumbnailMap", "Thumbnail")
+ .WithMany("Files")
+ .HasForeignKey("ThumbnailId")
+ .OnDelete(DeleteBehavior.SetNull);
+
+ b.Navigation("LibraryRoot");
+
+ b.Navigation("Thumbnail");
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
+ {
+ b.HasOne("FileShare_EFCore.Models.ManagedLibraryRoot", "LibraryRoot")
+ .WithMany("Thumbnails")
+ .HasForeignKey("LibraryRootId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("LibraryRoot");
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedLibraryRoot", b =>
+ {
+ b.Navigation("Files");
+
+ b.Navigation("Thumbnails");
+ });
+
+ modelBuilder.Entity("FileShare_EFCore.Models.ManagedThumbnailMap", b =>
+ {
+ b.Navigation("Files");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/FileShare-EFCore/Migrations/SqlServer/20260522092317_AddPlaybackPosition.cs b/FileShare-EFCore/Migrations/SqlServer/20260522092317_AddPlaybackPosition.cs
new file mode 100644
index 0000000..30fe291
--- /dev/null
+++ b/FileShare-EFCore/Migrations/SqlServer/20260522092317_AddPlaybackPosition.cs
@@ -0,0 +1,28 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace FileShare_EFCore.Migrations.SqlServer
+{
+ ///
+ public partial class AddPlaybackPosition : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "playback-position",
+ table: "managed-file-record",
+ type: "float",
+ nullable: true);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "playback-position",
+ table: "managed-file-record");
+ }
+ }
+}
diff --git a/FileShare-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs b/FileShare-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs
index 90669f8..04eb52d 100644
--- a/FileShare-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs
+++ b/FileShare-EFCore/Migrations/SqlServer/SqlServerAppDataContextModelSnapshot.cs
@@ -147,6 +147,10 @@ namespace FileShare_EFCore.Migrations.SqlServer
.HasColumnType("nvarchar(20)")
.HasColumnName("media-type");
+ b.Property("PlaybackPosition")
+ .HasColumnType("float")
+ .HasColumnName("playback-position");
+
b.Property("RelativePath")
.IsRequired()
.HasMaxLength(1024)
diff --git a/FileShare-EFCore/Models/ManagedFileRecord.cs b/FileShare-EFCore/Models/ManagedFileRecord.cs
index 1f37e8f..d21f66e 100644
--- a/FileShare-EFCore/Models/ManagedFileRecord.cs
+++ b/FileShare-EFCore/Models/ManagedFileRecord.cs
@@ -79,6 +79,10 @@ namespace FileShare_EFCore.Models
[Column("last-played-at")]
public DateTime? LastPlayedAt { get; set; }
+ /// 视频播放位置(秒)。
+ [Column("playback-position")]
+ public double? PlaybackPosition { get; set; }
+
/// 创建时间。
[Column("created-at")]
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
diff --git a/FileShare-Services/Endpoints/AppEndpoints.cs b/FileShare-Services/Endpoints/AppEndpoints.cs
index 90caea8..b77627c 100644
--- a/FileShare-Services/Endpoints/AppEndpoints.cs
+++ b/FileShare-Services/Endpoints/AppEndpoints.cs
@@ -77,6 +77,10 @@ namespace FileShare_Services.Endpoints
.WithOpenApi("FileLibrary", "标记文件已播放。")
.WithName("MarkFilePlayed");
+ endpoints.MapPost("api/files/progress", (service, request, _) => service.SaveFileProgressAsync(request))
+ .WithOpenApi("FileLibrary", "保存文件播放进度。")
+ .WithName("SaveFileProgress");
+
endpoints.MapGet("api/files/detail", (service, request, _) => service.GetFileAsync(request))
.WithOpenApi("FileLibrary", "查询文件详情。")
.WithName("GetFileDetail");
diff --git a/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs b/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs
index 5f2e06d..98daf1b 100644
--- a/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs
+++ b/FileShare-Services/Services/FileLibrary/FileLibraryContracts.cs
@@ -59,6 +59,13 @@ namespace FileShare_Services.Services.FileLibrary
public sealed record MarkFilePlayedRequest(
[property: JsonPropertyName("id")] int Id);
+ ///