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);