333 lines
12 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 Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MilliSim.Common.Dtos;
using MilliSim.Common.Models;
using MilliSim.Data;
using MilliSim.Services;
using MilliSim.Services.Infrastructure;
namespace MilliSim.Controllers;
[ApiController]
[Route("api/system")]
public class SystemController : ControllerBase
{
private readonly ISystemService _systemService;
private readonly IFileStorageService _fileStorage;
private readonly IUploadTrackingService _uploadTracking;
private readonly ICurrentUserService _currentUser;
private readonly IOperationLogService _operationLog;
private readonly MilliSimDbContext _db;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="systemService">系统服务</param>
/// <param name="fileStorage">文件存储服务</param>
/// <param name="uploadTracking">上传跟踪服务</param>
/// <param name="currentUser">当前用户服务</param>
/// <param name="operationLog">操作日志服务</param>
/// <param name="db">数据库上下文</param>
public SystemController(
ISystemService systemService,
IFileStorageService fileStorage,
IUploadTrackingService uploadTracking,
ICurrentUserService currentUser,
IOperationLogService operationLog,
MilliSimDbContext db)
{
_systemService = systemService;
_fileStorage = fileStorage;
_uploadTracking = uploadTracking;
_currentUser = currentUser;
_operationLog = operationLog;
_db = db;
}
// ===== 系统设置 =====
[HttpGet("settings")]
[Authorize(Policy = "Admin")]
public async Task<ApiResponse<List<SystemSettingDto>>> GetSettings()
{
return await _systemService.GetSettingsAsync();
}
[HttpPost("settings")]
[Authorize(Policy = "Admin")]
public async Task<ApiResponse> UpdateSettings([FromBody] UpdateSettingsRequest request)
{
return await _systemService.UpdateSettingsAsync(request);
}
[HttpGet("settings/{key}")]
[Authorize(Policy = "Admin")]
public async Task<ApiResponse<SystemSettingDto>> GetSetting(string key)
{
return await _systemService.GetSettingAsync(key);
}
/// <summary>
/// 获取公开系统设置无需鉴权返回平台名、Logo、备案号等供前台/后台 Layout 显示
/// </summary>
[HttpGet("public-settings")]
[AllowAnonymous]
public async Task<ApiResponse<PublicSettingsDto>> GetPublicSettings()
{
return await _systemService.GetPublicSettingsAsync();
}
// ===== 关于我们内容管理 =====
/// <summary>
/// 获取关于我们设置(公开端点,前台 About 页消费)
/// </summary>
[HttpGet("about")]
[AllowAnonymous]
public async Task<ApiResponse<AboutSettingsDto>> GetAboutSettings()
{
return await _systemService.GetAboutSettingsAsync();
}
/// <summary>
/// 更新关于我们设置(仅管理员)
/// </summary>
[HttpPost("about")]
[Authorize(Policy = "Admin")]
public async Task<ApiResponse> UpdateAboutSettings([FromBody] AboutSettingsDto dto)
{
return await _systemService.UpdateAboutSettingsAsync(dto);
}
// ===== 工厂重置 =====
/// <summary>
/// 恢复出厂设置:清空所有业务数据(保留 admin+ 删除 COS 资源(保留 backups/
/// 需要在请求体中输入 ConfirmText = "RESET" 确认
/// </summary>
[HttpPost("factory-reset")]
[Authorize(Policy = "Admin")]
public async Task<ApiResponse<FactoryResetResultDto>> FactoryReset([FromBody] FactoryResetRequest request)
{
return await _systemService.FactoryResetAsync(request);
}
// ===== 操作日志 =====
[HttpGet("logs")]
[Authorize(Policy = "Admin")]
public async Task<ApiResponse<PagedResult<OperationLogDto>>> GetLogs([FromQuery] OperationLogQueryRequest request)
{
return await _systemService.GetOperationLogsAsync(request);
}
[HttpPost("logs/delete")]
[Authorize(Policy = "Admin")]
public async Task<ApiResponse> ClearLogs([FromQuery] DateTime? beforeDate)
{
return await _systemService.ClearLogsAsync(beforeDate);
}
// ===== 数据备份 =====
[HttpGet("backups")]
[Authorize(Policy = "Admin")]
public async Task<ApiResponse<List<BackupDto>>> GetBackups()
{
return await _systemService.GetBackupsAsync();
}
[HttpPost("backups")]
[Authorize(Policy = "Admin")]
public async Task<ApiResponse<BackupDto>> CreateBackup()
{
return await _systemService.CreateBackupAsync();
}
[HttpPost("backups/{id}/delete")]
[Authorize(Policy = "Admin")]
public async Task<ApiResponse> DeleteBackup(long id)
{
return await _systemService.DeleteBackupAsync(id);
}
[HttpGet("backups/{id}/download")]
[Authorize(Policy = "Admin")]
public async Task<IActionResult> DownloadBackup(long id)
{
var result = await _systemService.DownloadBackupAsync(id);
if (result.Code != 200 || result.Data.Stream == null)
return NotFound(new { code = result.Code, message = result.Message });
return File(result.Data.Stream, "application/octet-stream", result.Data.FileName);
}
// ===== 唤醒协议 =====
[HttpGet("wake-protocols")]
public async Task<ApiResponse<List<WakeProtocolDto>>> GetWakeProtocols()
{
return await _systemService.GetWakeProtocolsAsync();
}
[HttpPost("wake-protocols")]
[Authorize(Policy = "Admin")]
public async Task<ApiResponse<WakeProtocolDto>> CreateWakeProtocol([FromBody] CreateWakeProtocolRequest request)
{
return await _systemService.CreateWakeProtocolAsync(request);
}
[HttpPost("wake-protocols/{id}")]
[Authorize(Policy = "Admin")]
public async Task<ApiResponse<WakeProtocolDto>> UpdateWakeProtocol(long id, [FromBody] CreateWakeProtocolRequest request)
{
return await _systemService.UpdateWakeProtocolAsync(id, request);
}
[HttpPost("wake-protocols/{id}/delete")]
[Authorize(Policy = "Admin")]
public async Task<ApiResponse> DeleteWakeProtocol(long id)
{
return await _systemService.DeleteWakeProtocolAsync(id);
}
// ===== 本地存储目录测试 =====
/// <summary>
/// 测试本地存储目录是否可读写(保存存储配置前校验)
/// </summary>
/// <param name="localBasePath">本地存储根目录(相对或绝对路径)</param>
[HttpPost("test-local-path")]
[Authorize(Policy = "Admin")]
public async Task<ApiResponse> TestLocalPath([FromQuery] string localBasePath)
{
return await _systemService.TestLocalPathAsync(localBasePath);
}
// ===== 通用文件上传 =====
[HttpPost("upload")]
[Authorize]
[RequestSizeLimit(2147483648)] // 2GB
[RequestFormLimits(MultipartBodyLengthLimit = 2147483648)]
public async Task<ApiResponse<UploadResultDto>> Upload([FromForm] IFormFile file, [FromForm] string folder)
{
// 检查文件是否为空
if (file == null || file.Length == 0)
return ApiResponse<UploadResultDto>.Fail("请选择要上传的文件");
// 默认目录
if (string.IsNullOrWhiteSpace(folder))
folder = "common";
// 从数据库读取上传大小限制,并验证文件大小
var limit = await GetUploadLimitAsync(file.FileName);
if (file.Length > limit)
{
var limitMB = limit / 1024.0 / 1024.0;
return ApiResponse<UploadResultDto>.Fail($"文件大小超过限制(最大 {limitMB:F1}MB");
}
// 上传文件获取对象键或本地URL
var fileUrl = await _fileStorage.UploadAsync(file, folder);
// 生成可访问的签名URLCOS模式下必须本地模式直接返回
var signedUrl = _fileStorage.GetSignedUrl(fileUrl);
// 注册到 PendingUpload 跟踪表(用于垃圾资源清理)
await _uploadTracking.RegisterAsync(fileUrl, folder, _currentUser.IsAuthenticated ? _currentUser.UserId : null);
// 返回对象键用于存储和签名URL用于预览
return ApiResponse<UploadResultDto>.Success(new UploadResultDto { Url = signedUrl, Key = fileUrl });
}
/// <summary>
/// 删除未使用的上传文件(仅限 pending 状态,已被实体消费的文件不会被删除)
/// 用于前端取消表单、替换文件时即时清理垃圾资源
/// </summary>
/// <param name="key">文件对象键</param>
[HttpPost("upload/delete")]
[Authorize]
public async Task<ApiResponse> DeleteUpload([FromQuery] string key)
{
if (string.IsNullOrWhiteSpace(key))
return ApiResponse.Fail("文件 key 不能为空");
var deleted = await _uploadTracking.DeletePendingAsync(key);
return ApiResponse.Success(deleted ? "已删除" : "文件不存在或已使用");
}
/// <summary>
/// 根据文件扩展名从数据库读取上传大小限制
/// </summary>
/// <param name="fileName">文件名</param>
/// <returns>上传大小限制(字节)</returns>
private async Task<long> GetUploadLimitAsync(string fileName)
{
// 根据文件扩展名确定限制类型
var ext = Path.GetExtension(fileName).ToLower();
var key = ext switch
{
// 图片类型
".jpg" or ".jpeg" or ".png" or ".gif" or ".svg" or ".webp" => "UploadLimitImage",
// 视频类型
".mp4" or ".webm" or ".avi" or ".mov" or ".mkv" => "UploadLimitVideo",
// 资源包类型
".zip" or ".rar" or ".7z" or ".apk" or ".exe" or ".dmg" => "UploadLimitPackage",
// 文档类型(默认)
_ => "UploadLimitDocument"
};
// 从数据库读取限制值
var setting = await _db.SystemSettings.FirstOrDefaultAsync(s => s.Key == key);
if (setting != null && long.TryParse(setting.Value, out var limit))
return limit;
// 默认限制 20MB
return 20L * 1024 * 1024;
}
// ===== COS → Local 迁移 =====
/// <summary>
/// 将 COS 中所有文件迁移到本地存储
/// 用于从 COS 切换到 Local 模式后,补齐旧数据对应的本地文件
/// </summary>
[HttpPost("migrate-cos-to-local")]
[Authorize(Policy = "Admin")]
public async Task<ApiResponse> MigrateCosToLocal()
{
try
{
var count = await _fileStorage.MigrateCosToLocalAsync();
await _operationLog.LogAsync("系统设置", "迁移",
$"COS 迁移到本地存储完成,共迁移 {count} 个文件");
return ApiResponse.Success($"迁移完成,共下载 {count} 个文件到本地");
}
catch (Exception ex)
{
return ApiResponse.Fail($"迁移失败:{ex.Message}");
}
}
/// <summary>
/// 迁移已有的 APK/IPA 文件扩展名
/// COS 禁止通过默认域名分发 APK/IPA 文件(返回 DownloadForbidden 403
/// 将对象键从 xxx.apk 复制为 xxx.apk.bin删除旧对象更新数据库
/// </summary>
[HttpPost("migrate-apk-extensions")]
[Authorize(Policy = "Admin")]
public async Task<ApiResponse> MigrateApkExtensions()
{
try
{
var count = await _fileStorage.MigrateApkExtensionsAsync();
await _operationLog.LogAsync("系统设置", "迁移",
$"APK/IPA 扩展名迁移完成,共迁移 {count} 个文件");
return ApiResponse.Success($"迁移完成,共处理 {count} 个 APK/IPA 文件");
}
catch (Exception ex)
{
return ApiResponse.Fail($"迁移失败:{ex.Message}");
}
}
}