From 6ef410fdfa8b6917ddac597b41fb37b7cd49ffdc Mon Sep 17 00:00:00 2001
From: luoqian <2769838458@qq.com>
Date: Fri, 22 May 2026 17:44:04 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=87=E4=BB=B6=E6=90=9C=E7=B4=A2?=
=?UTF-8?q?=E3=80=81=E8=A7=86=E9=A2=91=E7=BB=AD=E6=92=AD=E4=B8=8E=E7=9B=AE?=
=?UTF-8?q?=E5=BD=95=E6=96=87=E4=BB=B6=E8=BF=87=E6=BB=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 前端 Header 新增搜索栏,接入已有 SearchFiles API,结果支持列表/网格视图
- 新增 PlaybackPosition 数据库列与 /api/files/progress 端点,播放进度存服务端
- 播放中每 5 秒自动保存进度,再次打开视频时弹出"继续播放"提示
- 目录浏览新增媒体类型过滤条(全部/视频/音频/文本),前端即时过滤
- 新增 4 种数据库迁移(AddPlaybackPosition)
---
...0522092342_AddPlaybackPosition.Designer.cs | 448 +++++++++++++++++
.../20260522092342_AddPlaybackPosition.cs | 28 ++
.../MySQL/MySqlAppDataContextModelSnapshot.cs | 4 +
...0522092330_AddPlaybackPosition.Designer.cs | 463 ++++++++++++++++++
.../20260522092330_AddPlaybackPosition.cs | 28 ++
.../PostgreSqlAppDataContextModelSnapshot.cs | 4 +
...0522092304_AddPlaybackPosition.Designer.cs | 446 +++++++++++++++++
.../20260522092304_AddPlaybackPosition.cs | 28 ++
.../SqliteAppDataContextModelSnapshot.cs | 4 +
...0522092317_AddPlaybackPosition.Designer.cs | 463 ++++++++++++++++++
.../20260522092317_AddPlaybackPosition.cs | 28 ++
.../SqlServerAppDataContextModelSnapshot.cs | 4 +
FileShare-EFCore/Models/ManagedFileRecord.cs | 4 +
FileShare-Services/Endpoints/AppEndpoints.cs | 4 +
.../FileLibrary/FileLibraryContracts.cs | 10 +-
.../FileLibrary/FileLibraryEndpointService.cs | 7 +
.../FileLibrary/FileLibraryService.cs | 14 +-
.../IFileLibraryEndpointService.cs | 7 +
.../FileLibrary/IFileLibraryService.cs | 8 +
FileShare-Web-VUE/src/api/index.ts | 3 +
FileShare-Web-VUE/src/assets/main.css | 144 ++++++
.../src/components/ClientPage.vue | 251 +++++++++-
22 files changed, 2386 insertions(+), 14 deletions(-)
create mode 100644 FileShare-EFCore/Migrations/MySQL/20260522092342_AddPlaybackPosition.Designer.cs
create mode 100644 FileShare-EFCore/Migrations/MySQL/20260522092342_AddPlaybackPosition.cs
create mode 100644 FileShare-EFCore/Migrations/PostgreSQL/20260522092330_AddPlaybackPosition.Designer.cs
create mode 100644 FileShare-EFCore/Migrations/PostgreSQL/20260522092330_AddPlaybackPosition.cs
create mode 100644 FileShare-EFCore/Migrations/SQLite/20260522092304_AddPlaybackPosition.Designer.cs
create mode 100644 FileShare-EFCore/Migrations/SQLite/20260522092304_AddPlaybackPosition.cs
create mode 100644 FileShare-EFCore/Migrations/SqlServer/20260522092317_AddPlaybackPosition.Designer.cs
create mode 100644 FileShare-EFCore/Migrations/SqlServer/20260522092317_AddPlaybackPosition.cs
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);
+ ///