986 lines
36 KiB
C#
Raw Permalink 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 System.Diagnostics;
using System.Text;
using Microsoft.EntityFrameworkCore;
using MilliSim.Common.Dtos;
using MilliSim.Common.Entities;
using MilliSim.Common.Models;
using MilliSim.Data;
using MilliSim.Services.Infrastructure;
namespace MilliSim.Services;
public class SystemService : ISystemService
{
private readonly MilliSimDbContext _db;
private readonly IConfiguration _config;
private readonly IWebHostEnvironment _env;
private readonly IOperationLogService _operationLog;
private readonly IFileStorageService _fileStorage;
private readonly IUploadTrackingService _uploadTracking;
/// <summary>
/// 构造函数
/// </summary>
/// <param name="db">数据库上下文</param>
/// <param name="config">配置</param>
/// <param name="env">Web主机环境</param>
/// <param name="operationLog">操作日志服务</param>
/// <param name="fileStorage">文件存储服务</param>
/// <param name="uploadTracking">上传跟踪服务</param>
public SystemService(MilliSimDbContext db, IConfiguration config, IWebHostEnvironment env,
IOperationLogService operationLog, IFileStorageService fileStorage,
IUploadTrackingService uploadTracking)
{
_db = db;
_config = config;
_env = env;
_operationLog = operationLog;
_fileStorage = fileStorage;
_uploadTracking = uploadTracking;
}
// ===== 系统设置 =====
public async Task<ApiResponse<List<SystemSettingDto>>> GetSettingsAsync()
{
var settings = await _db.SystemSettings
.OrderBy(s => s.Id)
.Select(s => new SystemSettingDto
{
Key = s.Key,
Value = s.Value,
Description = s.Description
})
.ToListAsync();
// Logo 字段在 DB 中存储的是对象键,返回前需通过 GetSignedUrl 转换为可访问的 URL
var logoKeys = new[] { "LogoFrontend", "LogoBackend", "LogoFavicon", "LogoLogin", "LogoFooter" };
foreach (var s in settings)
{
if (logoKeys.Contains(s.Key) && !string.IsNullOrEmpty(s.Value))
{
s.Value = _fileStorage.GetSignedUrl(s.Value);
}
}
return ApiResponse<List<SystemSettingDto>>.Success(settings);
}
public async Task<ApiResponse> UpdateSettingsAsync(UpdateSettingsRequest request)
{
if (request.Settings == null || request.Settings.Count == 0)
return ApiResponse.Fail("没有需要更新的设置");
var keys = request.Settings.Keys.ToList();
var settings = await _db.SystemSettings
.Where(s => keys.Contains(s.Key))
.ToListAsync();
// Logo 五项的 key 集合(用于对比旧值并清理旧文件)
var logoKeys = new[] { "LogoFrontend", "LogoBackend", "LogoFavicon", "LogoLogin", "LogoFooter" };
// 保存前记录旧 Logo 值(用于对比变更后删除旧文件)
var oldLogoValues = new Dictionary<string, string>();
foreach (var setting in settings)
{
if (logoKeys.Contains(setting.Key))
{
oldLogoValues[setting.Key] = setting.Value;
}
}
foreach (var setting in settings)
{
if (request.Settings.TryGetValue(setting.Key, out var value))
{
// Logo 字段需通过 ExtractKey 转为对象键存储,避免签名 URL 过期导致数据丢失
setting.Value = logoKeys.Contains(setting.Key) ? _fileStorage.ExtractKey(value) : value;
setting.UpdatedAt = DateTime.Now;
}
}
await _db.SaveChangesAsync();
// 保存后对比 Logo 变化:替换则删旧文件,新文件消费标记
foreach (var logoKey in logoKeys)
{
if (!request.Settings.TryGetValue(logoKey, out var newValue)) continue;
var oldKey = _fileStorage.ExtractKey(oldLogoValues.GetValueOrDefault(logoKey) ?? "");
var newKey = _fileStorage.ExtractKey(newValue);
if (!string.IsNullOrEmpty(oldKey) && oldKey != newKey)
{
await _fileStorage.DeleteAsync(oldKey);
}
if (!string.IsNullOrEmpty(newKey))
{
await _uploadTracking.ConsumeAsync(newKey);
}
}
// 存储相关设置变更时清除缓存,确保后续请求读取最新配置
var storageKeys = new[] { "StorageMode", "CosSecretId", "CosSecretKey", "CosBucket", "CosRegion",
"CosSignedUrlExpireMinutes", "LocalBasePath", "LocalBaseUrl" };
if (request.Settings.Keys.Any(k => storageKeys.Contains(k)))
{
_fileStorage.ClearConfigCache();
}
await _operationLog.LogAsync("系统设置", "更新", $"更新了 {settings.Count} 项设置");
return ApiResponse.Success("设置更新成功");
}
public async Task<ApiResponse<SystemSettingDto>> GetSettingAsync(string key)
{
var setting = await _db.SystemSettings.FirstOrDefaultAsync(s => s.Key == key);
if (setting == null)
return ApiResponse<SystemSettingDto>.Fail("设置项不存在", 404);
return ApiResponse<SystemSettingDto>.Success(new SystemSettingDto
{
Key = setting.Key,
Value = setting.Value,
Description = setting.Description
});
}
/// <summary>
/// 获取公开系统设置无需鉴权返回平台名、Logo签名URL、备案号等供前台显示
/// Logo 字段在库中存储的是上传时返回的签名URL这里通过 ExtractKey + GetSignedUrl 刷新为最新签名URL避免过期
/// </summary>
public async Task<ApiResponse<PublicSettingsDto>> GetPublicSettingsAsync()
{
var keys = new List<string>
{
"PlatformName", "Slogan", "CompanyName", "Contact", "ICP",
"ContactPhone", "ContactEmail", "ContactAddress", "ContactWebsite", "ContactWorkTime",
"LogoFrontend", "LogoBackend", "LogoFavicon", "LogoLogin", "LogoFooter",
"ShowPartners", "ShowAbout"
};
var settings = await _db.SystemSettings
.Where(s => keys.Contains(s.Key))
.Select(s => new { s.Key, s.Value })
.ToListAsync();
string Get(string key) => settings.FirstOrDefault(s => s.Key == key)?.Value ?? string.Empty;
string RefreshLogo(string key)
{
var raw = Get(key);
if (string.IsNullOrEmpty(raw)) return string.Empty;
var objectKey = _fileStorage.ExtractKey(raw);
return string.IsNullOrEmpty(objectKey) ? raw : _fileStorage.GetSignedUrl(objectKey);
}
var dto = new PublicSettingsDto
{
PlatformName = Get("PlatformName"),
Slogan = Get("Slogan"),
CompanyName = Get("CompanyName"),
Contact = Get("Contact"),
ContactPhone = Get("ContactPhone"),
ContactEmail = Get("ContactEmail"),
ContactAddress = Get("ContactAddress"),
ContactWebsite = Get("ContactWebsite"),
ContactWorkTime = Get("ContactWorkTime"),
ICP = Get("ICP"),
LogoFrontend = RefreshLogo("LogoFrontend"),
LogoBackend = RefreshLogo("LogoBackend"),
LogoFavicon = RefreshLogo("LogoFavicon"),
LogoLogin = RefreshLogo("LogoLogin"),
LogoFooter = RefreshLogo("LogoFooter"),
ShowPartners = Get("ShowPartners") != "false",
ShowAbout = Get("ShowAbout") != "false",
};
return ApiResponse<PublicSettingsDto>.Success(dto);
}
// ===== 关于我们内容管理 =====
/// <summary>
/// 获取关于我们设置(公开端点,前台 About 页消费)
/// </summary>
public async Task<ApiResponse<AboutSettingsDto>> GetAboutSettingsAsync()
{
var keys = new List<string> { "ShowAbout", "AboutContent" };
var settings = await _db.SystemSettings
.Where(s => keys.Contains(s.Key))
.Select(s => new { s.Key, s.Value })
.ToListAsync();
string Get(string key) => settings.FirstOrDefault(s => s.Key == key)?.Value ?? string.Empty;
var dto = new AboutSettingsDto
{
ShowAbout = Get("ShowAbout") != "false",
};
var contentJson = Get("AboutContent");
if (!string.IsNullOrEmpty(contentJson))
{
try
{
dto.Content = System.Text.Json.JsonSerializer.Deserialize<AboutContentDto>(contentJson) ?? new AboutContentDto();
// 刷新使命卡片图标的签名URL存储的是COS对象键读取时转换为签名URL
if (dto.Content.MissionCards != null)
{
foreach (var card in dto.Content.MissionCards)
{
if (!string.IsNullOrEmpty(card.Icon) && card.Icon.Contains('/'))
{
card.Icon = _fileStorage.GetSignedUrl(card.Icon);
}
}
}
}
catch
{
dto.Content = new AboutContentDto();
}
}
return ApiResponse<AboutSettingsDto>.Success(dto);
}
/// <summary>
/// 更新关于我们设置Admin 端点)
/// </summary>
public async Task<ApiResponse> UpdateAboutSettingsAsync(AboutSettingsDto dto)
{
var keys = new List<string> { "ShowAbout", "AboutContent" };
var settings = await _db.SystemSettings
.Where(s => keys.Contains(s.Key))
.ToListAsync();
var settingDict = settings.ToDictionary(s => s.Key, s => s);
if (!settingDict.TryGetValue("ShowAbout", out var showAboutSetting))
{
showAboutSetting = new SystemSetting { Key = "ShowAbout", Description = "前台是否显示关于我们页面" };
_db.SystemSettings.Add(showAboutSetting);
}
showAboutSetting.Value = dto.ShowAbout ? "true" : "false";
showAboutSetting.UpdatedAt = DateTime.Now;
if (!settingDict.TryGetValue("AboutContent", out var contentSetting))
{
contentSetting = new SystemSetting { Key = "AboutContent", Description = "关于我们页面内容JSON" };
_db.SystemSettings.Add(contentSetting);
}
// 保存前将使命卡片图标的签名URL转换为COS对象键避免签名过期
var contentToSave = dto.Content ?? new AboutContentDto();
if (contentToSave.MissionCards != null)
{
foreach (var card in contentToSave.MissionCards)
{
if (!string.IsNullOrEmpty(card.Icon))
{
card.Icon = _fileStorage.ExtractKey(card.Icon);
}
}
}
contentSetting.Value = System.Text.Json.JsonSerializer.Serialize(contentToSave);
contentSetting.UpdatedAt = DateTime.Now;
await _db.SaveChangesAsync();
await _operationLog.LogAsync("关于我们", "更新", "更新了关于我们页面内容");
return ApiResponse.Success("保存成功");
}
// ===== 工厂重置 =====
/// <summary>
/// 恢复出厂设置:清空所有业务数据(保留 admin 账户)+ 删除 COS 资源(保留 backups/
/// 重置前自动创建数据库备份作为安全网
/// </summary>
public async Task<ApiResponse<FactoryResetResultDto>> FactoryResetAsync(FactoryResetRequest request)
{
if (request?.ConfirmText != "RESET")
{
return ApiResponse<FactoryResetResultDto>.Fail("确认文本不正确,请输入 RESET");
}
// 1. 重置前先创建数据库备份
var backupResult = await CreateBackupAsync();
var backupId = backupResult.Data?.Id ?? 0;
// 2. 列出所有 COS 对象(在清库前读取配置,保证配置可用)
// 容错:即使 COS 列举失败,仍继续清库,确保数据库恢复出厂状态
List<string> keysToDelete = new();
try
{
var allKeys = await _fileStorage.ListAllObjectsAsync();
// 过滤掉 backups/ 前缀(保留数据库备份作为安全网)
keysToDelete = allKeys.Where(k => !k.StartsWith("backups/", StringComparison.OrdinalIgnoreCase)).ToList();
}
catch (Exception ex)
{
await _operationLog.LogAsync("工厂重置", "COS列举失败", $"列举对象失败,将继续清库。错误:{ex.Message}");
}
// 3. 清空所有业务表(保留 admin 账户)
var tables = new[]
{
"ProjectTagRelations", "ProjectScreenshots", "ProjectPackages",
"Favorites", "ExperienceLogs", "Authorizations",
"NotificationReads", "Notifications",
"Consultations", "OperationLogs",
"PendingUploads",
"Projects", "Banners", "Partners",
"ProjectCategories", "PlatformTypes", "FeatureTags",
"WakeProtocols",
"Backups",
"SystemSettings"
};
await _db.Database.ExecuteSqlRawAsync("SET FOREIGN_KEY_CHECKS = 0");
try
{
foreach (var table in tables)
{
await _db.Database.ExecuteSqlRawAsync("DELETE FROM `" + table + "`");
}
// Users 表:仅删除非 admin 用户,并清空 admin 的 avatar避免引用已删除的 COS 文件)
await _db.Database.ExecuteSqlRawAsync("DELETE FROM `Users` WHERE `Username` != 'admin'");
await _db.Database.ExecuteSqlRawAsync("UPDATE `Users` SET `Avatar` = '' WHERE `Username` = 'admin'");
}
finally
{
await _db.Database.ExecuteSqlRawAsync("SET FOREIGN_KEY_CHECKS = 1");
}
// 4. 重新执行种子admin 已存在跳过;分类/平台/标签/项目/轮播图/合作伙伴/设置按为空才补)
await DbSeeder.SeedAsync(_db);
// 5. 批量删除 COS 资源
var deletedCount = 0;
try
{
deletedCount = await _fileStorage.DeleteObjectsAsync(keysToDelete);
}
catch (Exception ex)
{
// COS 删除失败不回滚 DBDB 已是干净状态),仅记录日志
await _operationLog.LogAsync("工厂重置", "COS清理失败", ex.Message);
}
await _operationLog.LogAsync("工厂重置", "执行", $"清空 {tables.Length + 1} 张表,删除 {deletedCount} 个 COS 对象备份ID {backupId}");
return ApiResponse<FactoryResetResultDto>.Success(new FactoryResetResultDto
{
TablesCleared = tables.Length + 1, // +1 for Users
ObjectsDeleted = deletedCount,
BackupId = backupId
}, "工厂重置完成");
}
// ===== 操作日志 =====
public async Task<ApiResponse<PagedResult<OperationLogDto>>> GetOperationLogsAsync(OperationLogQueryRequest request)
{
var query = _db.OperationLogs.AsQueryable();
if (request.UserId.HasValue)
query = query.Where(l => l.UserId == request.UserId.Value);
if (!string.IsNullOrEmpty(request.Username))
query = query.Where(l => l.Username.Contains(request.Username));
if (!string.IsNullOrEmpty(request.Module))
query = query.Where(l => l.Module.Contains(request.Module));
if (request.StartDate.HasValue)
query = query.Where(l => l.CreatedAt >= request.StartDate.Value);
if (request.EndDate.HasValue)
query = query.Where(l => l.CreatedAt <= request.EndDate.Value);
if (!string.IsNullOrEmpty(request.Keyword))
query = query.Where(l =>
l.Username.Contains(request.Keyword) ||
l.Operation.Contains(request.Keyword) ||
l.Detail.Contains(request.Keyword));
var total = await query.CountAsync();
var logEntities = await query
.OrderByDescending(l => l.CreatedAt)
.Skip((request.PageIndex - 1) * request.PageSize)
.Take(request.PageSize)
.ToListAsync();
var userIds = logEntities.Select(l => l.UserId).Distinct().ToList();
var nicknames = await _db.Users
.Where(u => userIds.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, u => u.Nickname);
var items = logEntities.Select(l => new OperationLogDto
{
Id = l.Id,
UserId = l.UserId,
Username = l.Username,
Nickname = nicknames.GetValueOrDefault(l.UserId) ?? string.Empty,
Module = l.Module,
Operation = l.Operation,
Detail = l.Detail,
Ip = l.Ip,
CreatedAt = l.CreatedAt
}).ToList();
return ApiResponse<PagedResult<OperationLogDto>>.Success(
PagedResult<OperationLogDto>.Create(items, total, request.PageIndex, request.PageSize));
}
public async Task<ApiResponse> ClearLogsAsync(DateTime? beforeDate)
{
int count;
if (beforeDate.HasValue)
{
count = await _db.OperationLogs
.Where(l => l.CreatedAt < beforeDate.Value)
.ExecuteDeleteAsync();
await _operationLog.LogAsync("操作日志", "清理", $"清理了 {beforeDate.Value:yyyy-MM-dd} 之前的日志 {count} 条");
}
else
{
count = await _db.OperationLogs.ExecuteDeleteAsync();
await _operationLog.LogAsync("操作日志", "清理", $"清空了全部日志 {count} 条");
}
return ApiResponse.Success($"已清理 {count} 条日志");
}
public async Task<ApiResponse> CleanExpiredLogsAsync()
{
var retentionDays = int.Parse(await GetSettingValueAsync("LogRetentionDays", "90"));
var cutoff = DateTime.Now.AddDays(-retentionDays);
var count = await _db.OperationLogs
.Where(l => l.CreatedAt < cutoff)
.ExecuteDeleteAsync();
return ApiResponse.Success($"已清理 {retentionDays} 天前的过期日志 {count} 条");
}
// ===== 数据备份 =====
public async Task<ApiResponse<List<BackupDto>>> GetBackupsAsync()
{
var backups = await _db.Backups
.OrderByDescending(b => b.CreatedAt)
.Select(b => new BackupDto
{
Id = b.Id,
FileName = b.FileName,
FileSize = b.FileSize,
Type = b.Type,
Url = b.Url,
CreatedAt = b.CreatedAt
})
.ToListAsync();
foreach (var b in backups)
{
b.Type = MapBackupType(b.Type);
}
return ApiResponse<List<BackupDto>>.Success(backups);
}
/// <summary>
/// 将备份类型映射为中文Manual->手动Auto->自动,其余原样返回
/// </summary>
private static string MapBackupType(string type) => type switch
{
"Manual" => "手动",
"Auto" => "自动",
_ => string.IsNullOrEmpty(type) ? "手动" : type
};
public async Task<ApiResponse<BackupDto>> CreateBackupAsync()
{
// 生成本地备份文件
var dir = GetBackupDir();
var fileName = $"backup_{DateTime.Now:yyyyMMddHHmmss}.sql";
var filePath = Path.Combine(dir, fileName);
var localUrl = $"/backups/{fileName}";
// 尝试使用 mysqldump 导出
var success = await TryMySqlDumpAsync(filePath);
if (!success)
{
// 回退:使用 SQL 脚本生成
try
{
await GenerateSqlScriptAsync(filePath);
success = true;
}
catch (Exception ex)
{
if (File.Exists(filePath)) File.Delete(filePath);
return ApiResponse<BackupDto>.Fail($"备份失败:{ex.Message}");
}
}
var fileInfo = new FileInfo(filePath);
// 在 COS 上传前先保存文件大小,避免上传后本地文件被删除导致 FileNotFoundException
var fileSize = fileInfo.Length;
// 读取存储模式Local=本地存储Cos=腾讯云COS
var storageMode = await GetSettingValueAsync("StorageMode", "Cos");
string backupUrl = localUrl;
// 如果是COS模式将备份文件上传到COS
if (storageMode.Equals("Cos", StringComparison.OrdinalIgnoreCase))
{
try
{
// 创建FormFile用于上传到COS
FileStream? fileStream = null;
try
{
fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
var formFile = new FormFile(fileStream, 0, fileStream.Length, "file", fileName)
{
Headers = new HeaderDictionary(),
ContentType = "application/sql"
};
// 上传备份文件到COS的backups目录
backupUrl = await _fileStorage.UploadAsync(formFile, "backups");
}
finally
{
// 确保文件流被关闭
fileStream?.Dispose();
}
// 上传成功后删除本地临时文件
if (File.Exists(filePath)) File.Delete(filePath);
}
catch
{
// COS上传失败使用本地文件作为兜底
backupUrl = localUrl;
}
}
// 创建备份记录(使用预先保存的文件大小)
var backup = new Backup
{
FileName = fileName,
FileSize = fileSize,
Type = "Manual",
Url = backupUrl
};
_db.Backups.Add(backup);
await _db.SaveChangesAsync();
await _operationLog.LogAsync("数据备份", "创建", $"手动创建备份 {fileName}");
return ApiResponse<BackupDto>.Success(new BackupDto
{
Id = backup.Id,
FileName = backup.FileName,
FileSize = backup.FileSize,
Type = MapBackupType(backup.Type),
Url = backup.Url,
CreatedAt = backup.CreatedAt
});
}
public async Task<ApiResponse> DeleteBackupAsync(long id)
{
var backup = await _db.Backups.FindAsync(id);
if (backup == null)
return ApiResponse.Fail("备份不存在", 404);
// 判断备份文件存储位置URL以"/"开头为本地文件否则为COS对象键
if (backup.Url.StartsWith("/"))
{
// 本地文件删除
var dir = GetBackupDir();
var filePath = Path.Combine(dir, backup.FileName);
if (File.Exists(filePath))
File.Delete(filePath);
}
else
{
// COS文件删除
await _fileStorage.DeleteAsync(backup.Url);
}
_db.Backups.Remove(backup);
await _db.SaveChangesAsync();
await _operationLog.LogAsync("数据备份", "删除", $"删除备份 {backup.FileName}");
return ApiResponse.Success("备份已删除");
}
public async Task<ApiResponse<(Stream Stream, string FileName)>> DownloadBackupAsync(long id)
{
var backup = await _db.Backups.FindAsync(id);
if (backup == null)
return ApiResponse<(Stream, string)>.Fail("备份不存在", 404);
// 判断备份文件存储位置URL以"/"开头为本地文件否则为COS对象键
if (backup.Url.StartsWith("/"))
{
// 本地文件下载
var dir = GetBackupDir();
var filePath = Path.Combine(dir, backup.FileName);
if (!File.Exists(filePath))
return ApiResponse<(Stream, string)>.Fail("备份文件不存在", 404);
var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
return ApiResponse<(Stream, string)>.Success((stream, backup.FileName));
}
else
{
// COS文件下载
try
{
var (stream, _, _) = await _fileStorage.DownloadAsync(backup.Url);
return ApiResponse<(Stream, string)>.Success((stream, backup.FileName));
}
catch
{
return ApiResponse<(Stream, string)>.Fail("备份文件不存在", 404);
}
}
}
public async Task CleanExpiredBackupsAsync()
{
// 读取备份保留天数
var retentionDays = int.Parse(await GetSettingValueAsync("BackupRetentionDays", "30"));
var cutoff = DateTime.Now.AddDays(-retentionDays);
// 查询过期备份
var expired = await _db.Backups
.Where(b => b.CreatedAt < cutoff)
.ToListAsync();
// 遍历删除过期备份文件
foreach (var b in expired)
{
// 判断备份文件存储位置URL以"/"开头为本地文件否则为COS对象键
if (b.Url.StartsWith("/"))
{
// 本地文件删除
var dir = GetBackupDir();
var filePath = Path.Combine(dir, b.FileName);
if (File.Exists(filePath))
File.Delete(filePath);
}
else
{
// COS文件删除
await _fileStorage.DeleteAsync(b.Url);
}
}
// 删除数据库记录
_db.Backups.RemoveRange(expired);
await _db.SaveChangesAsync();
}
// ===== 唤醒协议 =====
public async Task<ApiResponse<List<WakeProtocolDto>>> GetWakeProtocolsAsync()
{
var list = await _db.WakeProtocols
.OrderBy(w => w.Id)
.Select(w => new WakeProtocolDto
{
Id = w.Id,
TerminalType = w.TerminalType,
ProtocolPrefix = w.ProtocolPrefix,
ParamTemplate = w.ParamTemplate,
Status = w.Status
})
.ToListAsync();
return ApiResponse<List<WakeProtocolDto>>.Success(list);
}
public async Task<ApiResponse<WakeProtocolDto>> CreateWakeProtocolAsync(CreateWakeProtocolRequest request)
{
var wp = new WakeProtocol
{
TerminalType = request.TerminalType,
ProtocolPrefix = request.ProtocolPrefix,
ParamTemplate = request.ParamTemplate,
Status = request.Status
};
_db.WakeProtocols.Add(wp);
await _db.SaveChangesAsync();
await _operationLog.LogAsync("唤醒协议", "创建", $"创建唤醒协议 {wp.TerminalType}");
return ApiResponse<WakeProtocolDto>.Success(MapToDto(wp));
}
public async Task<ApiResponse<WakeProtocolDto>> UpdateWakeProtocolAsync(long id, CreateWakeProtocolRequest request)
{
var wp = await _db.WakeProtocols.FindAsync(id);
if (wp == null)
return ApiResponse<WakeProtocolDto>.Fail("唤醒协议不存在", 404);
wp.TerminalType = request.TerminalType;
wp.ProtocolPrefix = request.ProtocolPrefix;
wp.ParamTemplate = request.ParamTemplate;
wp.Status = request.Status;
wp.UpdatedAt = DateTime.Now;
await _db.SaveChangesAsync();
await _operationLog.LogAsync("唤醒协议", "更新", $"更新唤醒协议 {wp.TerminalType}");
return ApiResponse<WakeProtocolDto>.Success(MapToDto(wp));
}
public async Task<ApiResponse> DeleteWakeProtocolAsync(long id)
{
var wp = await _db.WakeProtocols.FindAsync(id);
if (wp == null)
return ApiResponse.Fail("唤醒协议不存在", 404);
wp.IsDeleted = true;
wp.UpdatedAt = DateTime.Now;
await _db.SaveChangesAsync();
await _operationLog.LogAsync("唤醒协议", "删除", $"删除唤醒协议 {wp.TerminalType}");
return ApiResponse.Success("删除成功");
}
// ===== 本地存储目录测试 =====
/// <summary>
/// 测试本地存储目录是否可读写(保存存储配置前校验)
/// 支持相对路径(相对于 ContentRootPath或绝对路径
/// </summary>
public Task<ApiResponse> TestLocalPathAsync(string localBasePath)
{
if (string.IsNullOrWhiteSpace(localBasePath))
return Task.FromResult(ApiResponse.Fail("存储根目录不能为空"));
try
{
// 解析为绝对路径
var absolutePath = Path.IsPathRooted(localBasePath)
? localBasePath
: Path.Combine(_env.ContentRootPath, localBasePath);
// 确保目录存在
Directory.CreateDirectory(absolutePath);
// 测试写入
var testFile = Path.Combine(absolutePath, $".millisim_test_{Guid.NewGuid():N}");
File.WriteAllText(testFile, "test");
// 测试读取
var content = File.ReadAllText(testFile);
// 清理测试文件
File.Delete(testFile);
if (content != "test")
return Task.FromResult(ApiResponse.Fail("目录可写但读取内容不匹配,请检查目录权限"));
return Task.FromResult(ApiResponse.Success($"目录可读写:{absolutePath}"));
}
catch (UnauthorizedAccessException)
{
return Task.FromResult(ApiResponse.Fail("目录无访问权限,请选择其他目录或修改权限"));
}
catch (ArgumentException ex)
{
return Task.FromResult(ApiResponse.Fail($"路径格式无效:{ex.Message}"));
}
catch (Exception ex)
{
return Task.FromResult(ApiResponse.Fail($"目录测试失败:{ex.Message}"));
}
}
// ===== 辅助方法 =====
private async Task<string> GetSettingValueAsync(string key, string defaultValue)
{
var setting = await _db.SystemSettings.FirstOrDefaultAsync(s => s.Key == key);
return setting?.Value ?? defaultValue;
}
private string GetBackupDir()
{
var root = _env.WebRootPath ?? Path.Combine(_env.ContentRootPath, "wwwroot");
var dir = Path.Combine(root, "backups");
Directory.CreateDirectory(dir);
return dir;
}
private static WakeProtocolDto MapToDto(WakeProtocol wp) => new()
{
Id = wp.Id,
TerminalType = wp.TerminalType,
ProtocolPrefix = wp.ProtocolPrefix,
ParamTemplate = wp.ParamTemplate,
Status = wp.Status
};
/// <summary>
/// 尝试使用 mysqldump 导出数据库
/// </summary>
private async Task<bool> TryMySqlDumpAsync(string filePath)
{
try
{
var connectionString = _config.GetConnectionString("DefaultConnection")!;
var builder = new MySqlConnector.MySqlConnectionStringBuilder(connectionString);
var host = builder.Server;
var port = builder.Port;
var database = builder.Database;
var user = builder.UserID;
var password = builder.Password;
var args = new StringBuilder();
args.Append($"-h{host} -P{port} -u{user}");
if (!string.IsNullOrEmpty(password))
args.Append($" -p\"{password}\"");
args.Append(" --default-character-set=utf8mb4 --single-transaction --routines --triggers --skip-lock-tables");
args.Append($" \"{database}\"");
var psi = new ProcessStartInfo
{
FileName = "mysqldump",
Arguments = args.ToString(),
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
using var process = Process.Start(psi);
if (process == null) return false;
// 并发读取 stderr 防止缓冲区死锁
var stderrTask = process.StandardError.ReadToEndAsync();
await using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
await process.StandardOutput.BaseStream.CopyToAsync(fileStream);
await fileStream.FlushAsync();
}
await process.WaitForExitAsync();
await stderrTask;
if (process.ExitCode != 0)
{
if (File.Exists(filePath)) File.Delete(filePath);
return false;
}
var fi = new FileInfo(filePath);
if (fi.Length == 0)
{
File.Delete(filePath);
return false;
}
return true;
}
catch
{
if (File.Exists(filePath)) File.Delete(filePath);
return false;
}
}
/// <summary>
/// 回退方案:直接查询数据库生成 SQL 脚本
/// </summary>
private async Task GenerateSqlScriptAsync(string filePath)
{
var connectionString = _config.GetConnectionString("DefaultConnection")!;
await using var connection = new MySqlConnector.MySqlConnection(connectionString);
await connection.OpenAsync();
var sb = new StringBuilder();
sb.AppendLine("-- MilliSim Database Backup");
sb.AppendLine($"-- Generated: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
sb.AppendLine("SET NAMES utf8mb4;");
sb.AppendLine("SET FOREIGN_KEY_CHECKS=0;");
sb.AppendLine();
var tables = new List<string>();
await using (var cmd = connection.CreateCommand())
{
cmd.CommandText = "SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_TYPE = 'BASE TABLE'";
await using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
tables.Add(reader.GetString(0));
}
}
foreach (var table in tables)
{
// 表结构
await using (var createCmd = connection.CreateCommand())
{
createCmd.CommandText = $"SHOW CREATE TABLE `{table}`";
await using var createReader = await createCmd.ExecuteReaderAsync();
if (await createReader.ReadAsync())
{
sb.AppendLine($"-- ----------------------------");
sb.AppendLine($"-- Table structure for {table}");
sb.AppendLine($"-- ----------------------------");
sb.AppendLine($"DROP TABLE IF EXISTS `{table}`;");
sb.AppendLine(createReader.GetString(1) + ";");
sb.AppendLine();
}
}
// 表数据
await using (var dataCmd = connection.CreateCommand())
{
dataCmd.CommandText = $"SELECT * FROM `{table}`";
await using var dataReader = await dataCmd.ExecuteReaderAsync();
var columns = new List<string>();
for (var i = 0; i < dataReader.FieldCount; i++)
{
columns.Add(dataReader.GetName(i));
}
var hasData = false;
while (await dataReader.ReadAsync())
{
hasData = true;
var values = new List<string>();
for (var i = 0; i < dataReader.FieldCount; i++)
{
values.Add(FormatSqlValue(dataReader.GetValue(i)));
}
sb.AppendLine($"INSERT INTO `{table}` (`{string.Join("`, `", columns)}`) VALUES ({string.Join(", ", values)});");
}
if (hasData) sb.AppendLine();
}
}
sb.AppendLine("SET FOREIGN_KEY_CHECKS=1;");
await File.WriteAllTextAsync(filePath, sb.ToString());
}
private static string FormatSqlValue(object? value)
{
if (value == null || value == DBNull.Value) return "NULL";
if (value is bool b) return b ? "1" : "0";
if (value is DateTime dt) return $"'{dt:yyyy-MM-dd HH:mm:ss}'";
if (value is byte[] bytes) return $"0x{Convert.ToHexString(bytes)}";
if (value is sbyte or short or int or long or byte or ushort or uint or ulong) return value.ToString()!;
if (value is float or double or decimal) return value.ToString()!;
var s = value.ToString() ?? string.Empty;
return "'" + s.Replace("\\", "\\\\").Replace("'", "\\'").Replace("\n", "\\n").Replace("\r", "\\r") + "'";
}
}