lq1405 c6b05c12e5 fix(web): 恢复视频继续播放和进度保存
- 将播放器收敛为单个共享实例,避免列表/网格内多个 video ref 导致进度保存失效
- 恢复视频播放器上方的继续播放提示
- 在播放、暂停、拖动、结束、切换页面和离开页面时保存播放位置
- 保留文件浏览分页和排序参数的前端调用
2026-05-23 11:03:51 +08:00

138 lines
7.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using FileShare_Common.Core;
using FileShare_Services.Core;
using FileShare_Services.Services.FileLibrary;
using FileShare_Services.Services.QrCode;
using Microsoft.Extensions.DependencyInjection;
namespace FileShare_Services.Endpoints
{
/// <summary>
/// 统一端点配置 —— 所有业务端点在此定义一次。
/// 这是 FileShare-API 和 FileShare-PC 的唯一入口。
/// </summary>
public static class AppEndpoints
{
/// <summary>
/// 配置所有业务端点。调用方传入 builder按需叠加鉴权、过滤器等。
/// </summary>
/// <param name="builder">端点构建器</param>
/// <param name="includeDetails">是否在错误响应中包含异常详情(开发环境 true</param>
public static ServiceEndpointBuilder Configure(ServiceEndpointBuilder builder, bool includeDetails = false)
{
// ---- 全局异常拦截(自动捕获所有端点中未处理的异常) ----
builder.Endpoints.AddGlobalFilter(new GlobalExceptionFilter(includeDetails));
builder.ConfigureEndpoints(endpoints =>
{
// ---- 全局日志过滤器(记录每个请求) ----
endpoints.AddGlobalFilter(async (ctx, next) =>
{
Serilog.Log.Information("→ {Method} {Path}", ctx.Method, ctx.Path);
await next(ctx);
Serilog.Log.Information("← {Method} {Path} | {StatusCode}", ctx.Method, ctx.Path, ctx.StatusCode);
});
// ---- 业务端点注册 ----
endpoints.MapGet<IFileLibraryEndpointService>("api/library/drives", (service, ctx) => service.GetDrivesAsync(ctx))
.WithOpenApi("FileLibrary", "查询服务器磁盘。")
.WithName("GetLibraryDrives");
endpoints.MapGet<IFileLibraryEndpointService, DirectoryQueryRequest>("api/library/directories", (service, request, _) => service.GetDirectoriesAsync(request))
.WithOpenApi("FileLibrary", "查询服务器目录。")
.WithName("GetLibraryDirectories");
endpoints.MapGet<IFileLibraryEndpointService>("api/library/roots", (service, ctx) => service.GetRootsAsync(ctx))
.WithOpenApi("FileLibrary", "查询文件库目录。")
.WithName("GetLibraryRoots");
endpoints.MapPost<IFileLibraryEndpointService, AddLibraryRootRequest>("api/library/roots", (service, request, _) => service.AddRootAsync(request))
.WithOpenApi("FileLibrary", "添加文件库目录。")
.WithName("AddLibraryRoot");
endpoints.MapPost<IFileLibraryEndpointService, UpdateLibraryRootRequest>("api/library/roots/enabled", (service, request, _) => service.SetRootEnabledAsync(request))
.WithOpenApi("FileLibrary", "启用或禁用文件库目录。")
.WithName("SetLibraryRootEnabled");
endpoints.MapPost<IFileLibraryEndpointService, DeleteLibraryRootRequest>("api/library/roots/delete", (service, request, _) => service.DeleteRootAsync(request))
.WithOpenApi("FileLibrary", "删除文件库目录。")
.WithName("DeleteLibraryRoot");
endpoints.MapPost<IFileLibraryEndpointService, ScanLibraryRootRequest>("api/library/roots/scan", (service, request, _) => service.ScanRootAsync(request))
.WithOpenApi("FileLibrary", "立即扫描文件库目录。")
.WithName("ScanLibraryRoot");
endpoints.MapGet<IFileLibraryEndpointService, SearchFilesRequest>("api/files", (service, request, _) => service.SearchFilesAsync(request))
.WithOpenApi("FileLibrary", "分页查询已扫描文件。")
.WithName("SearchFiles");
endpoints.MapGet<IFileLibraryEndpointService, BrowseDirectoryRequest>("api/files/browse", (service, request, _) => service.BrowseDirectoryAsync(request))
.WithOpenApi("FileLibrary", "浏览文件库目录结构。")
.WithName("BrowseDirectory");
endpoints.MapGet<IFileLibraryEndpointService, RecentFilesRequest>("api/files/recent", (service, request, _) => service.GetRecentFilesAsync(request))
.WithOpenApi("FileLibrary", "获取最近添加或最近播放的文件。")
.WithName("GetRecentFiles");
endpoints.MapPost<IFileLibraryEndpointService, MarkFilePlayedRequest>("api/files/played", (service, request, _) => service.MarkFilePlayedAsync(request))
.WithOpenApi("FileLibrary", "标记文件已播放。")
.WithName("MarkFilePlayed");
endpoints.MapPost<IFileLibraryEndpointService, SaveFileProgressRequest>("api/files/progress", (service, request, _) => service.SaveFileProgressAsync(request))
.WithOpenApi("FileLibrary", "保存文件播放进度。")
.WithName("SaveFileProgress");
endpoints.MapGet<IFileLibraryEndpointService, FileQueryRequest>("api/files/detail", (service, request, _) => service.GetFileAsync(request))
.WithOpenApi("FileLibrary", "查询文件详情。")
.WithName("GetFileDetail");
endpoints.MapGet<IFileLibraryEndpointService, FileQueryRequest>("api/files/text", (service, request, _) => service.GetTextPreviewAsync(request))
.WithOpenApi("FileLibrary", "预览文本文件。")
.WithName("GetTextPreview");
endpoints.MapGet("api/files/stream", GetFileStreamAsync)
.WithOpenApi("FileLibrary", "流式传输文件(支持 Range 请求)。")
.WithName("StreamManagedFile");
endpoints.MapGet<IQrCodeService>("api/qrcode", (service, ctx) => service.GenerateQrCodeAsync(ctx))
.WithOpenApi("Utility", "生成局域网访问二维码。")
.WithName("GetQrCode");
// ---- 需要鉴权的端点示例 ----
// endpoints.MapGet("api/admin/dashboard", AdminDashboardAsync)
// .WithName("AdminDashboard")
// .RequireAuthorization = true
// .Policy = "AdminOnly";
});
return builder;
}
#region
/// <summary>
/// 从 <see cref="ServiceEndpointContext.Items"/> 中解析 <see cref="IFileStreamService"/>
/// 读取查询参数中的文件 ID返回文件流响应。
/// </summary>
/// <param name="ctx">端点上下文。</param>
/// <returns>文件流响应对象,服务不可用或 ID 无效时返回 null。</returns>
private static async Task<object?> GetFileStreamAsync(ServiceEndpointContext ctx)
{
var sp = ctx.Items["ServiceProvider"] as IServiceProvider;
if (sp is null) return null;
if (!int.TryParse(ctx.Query.GetValueOrDefault("id"), out var id) || id <= 0)
return null;
using var scope = sp.CreateScope();
var service = scope.ServiceProvider.GetService<IFileStreamService>();
if (service is null) return null;
return await service.GetFileStreamAsync(id);
}
#endregion
}
}