using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using MilliSim.Common.Dtos; using MilliSim.Common.Models; using MilliSim.Services; namespace MilliSim.Controllers; [ApiController] [Route("api/projects")] public class ProjectsController : ControllerBase { private readonly IProjectService _projectService; public ProjectsController(IProjectService projectService) { _projectService = projectService; } // ===== 前台接口 ===== /// /// 公开项目列表 /// [HttpGet("public")] public async Task>> GetPublicProjects([FromQuery] ProjectQueryRequest request) { return await _projectService.GetPublishedProjectsAsync(request); } /// /// 公开项目详情(自增浏览数) /// [HttpGet("public/{id:long}")] public async Task> GetPublicProject(long id) { return await _projectService.GetProjectByIdAsync(id, incrementView: true); } /// /// 相关项目 /// [HttpGet("related/{id:long}")] public async Task>> GetRelatedProjects(long id, [FromQuery] int count = 4) { return await _projectService.GetRelatedProjectsAsync(id, count); } /// /// 收藏切换 /// [HttpPost("{id:long}/favorite")] [Authorize] public async Task ToggleFavorite(long id) { return await _projectService.ToggleFavoriteAsync(id); } /// /// 我的收藏 /// [HttpGet("favorites")] [Authorize] public async Task>> GetFavorites([FromQuery] PageRequest request) { return await _projectService.GetFavoritesAsync(request); } /// /// 记录体验 /// [HttpPost("{id:long}/experience")] [Authorize] public async Task RecordExperience(long id, [FromBody] RecordExperienceRequest request) { return await _projectService.RecordExperienceAsync(id, request.DurationSeconds); } /// /// 体验记录 /// [HttpGet("experience-logs")] [Authorize] public async Task>> GetExperienceLogs([FromQuery] PageRequest request) { return await _projectService.GetExperienceLogsAsync(request); } // ===== 后台接口 ===== /// /// 全部项目列表 /// [HttpGet] [Authorize(Policy = "ProductManager")] public async Task>> GetProjects([FromQuery] ProjectQueryRequest request) { return await _projectService.GetProjectsAsync(request); } /// /// 项目详情 /// [HttpGet("{id:long}")] [Authorize(Policy = "ProductManager")] public async Task> GetProject(long id) { return await _projectService.GetProjectByIdAsync(id, incrementView: false); } /// /// 创建项目 /// [HttpPost] [Authorize(Policy = "ProductManager")] public async Task> CreateProject([FromBody] CreateProjectRequest request) { return await _projectService.CreateProjectAsync(request); } /// /// 更新项目 /// [HttpPost("{id:long}")] [Authorize(Policy = "ProductManager")] public async Task> UpdateProject(long id, [FromBody] UpdateProjectRequest request) { return await _projectService.UpdateProjectAsync(id, request); } /// /// 删除项目 /// [HttpPost("{id:long}/delete")] [Authorize(Policy = "ProductManager")] public async Task DeleteProject(long id) { return await _projectService.DeleteProjectAsync(id); } /// /// 发布/下架切换 /// [HttpPost("{id:long}/toggle-publish")] [Authorize(Policy = "ProductManager")] public async Task TogglePublish(long id) { return await _projectService.TogglePublishAsync(id); } /// /// 推荐切换 /// [HttpPost("{id:long}/toggle-featured")] [Authorize(Policy = "ProductManager")] public async Task ToggleFeatured(long id) { return await _projectService.ToggleFeaturedAsync(id); } /// /// 资源包列表(公开接口,前台项目详情页需要显示运行按钮) /// [HttpGet("{id:long}/packages")] public async Task>> GetPackages(long id) { return await _projectService.GetPackagesAsync(id); } /// /// 上传资源包 /// [HttpPost("{id:long}/packages")] [Authorize(Policy = "ProductManager")] public async Task> UploadPackage(long id, [FromForm] PackageType type, [FromForm] string? wakeProtocolPrefix, [FromForm] string? wakeProtocolParam, IFormFile file) { return await _projectService.UploadPackageAsync(id, type, file, wakeProtocolPrefix ?? string.Empty, wakeProtocolParam ?? string.Empty); } /// /// 获取 WebGL 在线运行入口 URL(公开接口,前台在线运行使用) /// [HttpGet("{id:long}/webgl-run-url")] public async Task> GetWebGLRunUrl(long id) { return await _projectService.GetWebGLRunUrlAsync(id); } /// /// WebGL 在线运行 - 代理 index.html(公开接口) /// 浏览器无法直接渲染 COS 私有桶的 index.html,通过后端代理读取并注入 base 标签 /// 资源文件通过 webgl-resource 端点 302 重定向到签名 URL 加载 /// [HttpGet("{id:long}/webgl-index")] public async Task GetWebGLIndex(long id) { try { var html = await _projectService.GetWebGLIndexHtmlAsync(id); return Content(html, "text/html", System.Text.Encoding.UTF8); } catch (InvalidOperationException ex) { return NotFound(new { message = ex.Message }); } } /// /// WebGL 资源代理 - 302 重定向到签名 URL(公开接口) /// index.html 中的 base 标签将相对路径指向此端点 /// 浏览器请求资源时,此端点生成签名 URL 并重定向,浏览器直接从 COS 加载 /// [HttpGet("{id:long}/webgl-resource/{*path}")] public async Task GetWebGLResource(long id, string path) { try { var signedUrl = await _projectService.GetWebGLResourceUrlAsync(id, path); return Redirect(signedUrl); } catch (InvalidOperationException ex) { return NotFound(new { message = ex.Message }); } } /// /// 视频代理 - 流式转发视频内容,不暴露 COS 签名直链(防盗链) /// 前端 video src 指向此端点,后端流式代理 COS 文件 /// 支持 Range 请求(视频拖动进度条) /// [HttpGet("{id:long}/video")] public async Task GetVideo(long id) { try { var rangeHeader = Request.Headers.Range.ToString(); var (stream, contentType, fileSize, isPartial, contentRange) = await _projectService.GetVideoStreamAsync(id, rangeHeader); // 防止浏览器缓存视频到磁盘 Response.Headers.CacheControl = "no-store, no-cache, must-revalidate"; Response.Headers.Pragma = "no-cache"; Response.Headers.Expires = "0"; // 内联播放,不触发下载 Response.Headers.ContentDisposition = "inline"; // 支持 Range 请求时设置 Content-Range 和 206 状态码 if (isPartial && !string.IsNullOrEmpty(contentRange)) { Response.Headers.ContentRange = contentRange; Response.StatusCode = 206; } if (fileSize.HasValue) { Response.Headers.ContentLength = fileSize.Value; } return new FileStreamResult(stream, contentType); } catch (InvalidOperationException ex) { return NotFound(new { message = ex.Message }); } } /// /// 删除资源包 /// [HttpPost("packages/{id:long}/delete")] [Authorize(Policy = "ProductManager")] public async Task DeletePackage(long id) { return await _projectService.DeletePackageAsync(id); } /// /// 下载资源包(需要登录) /// 通过后端代理流式返回文件,使用原始文件名设置 Content-Disposition /// 解决:1.安卓端直接打开 COS 签名URL下载失败 2.下载文件名使用原始文件名而非 COS 对象键 /// [HttpGet("packages/{id:long}/download")] [Authorize] public async Task DownloadPackage(long id) { try { var (stream, contentType, fileName) = await _projectService.DownloadPackageAsync(id); // attachment 触发浏览器下载,filename 使用原始文件名 return File(stream, contentType, fileName); } catch (InvalidOperationException ex) { return NotFound(new { message = ex.Message }); } catch (Exception ex) { return StatusCode(500, new { message = $"下载失败: {ex.Message}" }); } } } /// /// 记录体验请求 /// public record RecordExperienceRequest(int DurationSeconds);