333 lines
12 KiB
C#
333 lines
12 KiB
C#
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);
|
||
// 生成可访问的签名URL(COS模式下必须,本地模式直接返回)
|
||
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}");
|
||
}
|
||
}
|
||
}
|