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;
///
/// 构造函数
///
/// 数据库上下文
/// 配置
/// Web主机环境
/// 操作日志服务
/// 文件存储服务
/// 上传跟踪服务
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>> 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>.Success(settings);
}
public async Task 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();
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> GetSettingAsync(string key)
{
var setting = await _db.SystemSettings.FirstOrDefaultAsync(s => s.Key == key);
if (setting == null)
return ApiResponse.Fail("设置项不存在", 404);
return ApiResponse.Success(new SystemSettingDto
{
Key = setting.Key,
Value = setting.Value,
Description = setting.Description
});
}
///
/// 获取公开系统设置(无需鉴权):返回平台名、Logo(签名URL)、备案号等供前台显示
/// Logo 字段在库中存储的是上传时返回的签名URL,这里通过 ExtractKey + GetSignedUrl 刷新为最新签名URL,避免过期
///
public async Task> GetPublicSettingsAsync()
{
var keys = new List
{
"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.Success(dto);
}
// ===== 关于我们内容管理 =====
///
/// 获取关于我们设置(公开端点,前台 About 页消费)
///
public async Task> GetAboutSettingsAsync()
{
var keys = new List { "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(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.Success(dto);
}
///
/// 更新关于我们设置(Admin 端点)
///
public async Task UpdateAboutSettingsAsync(AboutSettingsDto dto)
{
var keys = new List { "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("保存成功");
}
// ===== 工厂重置 =====
///
/// 恢复出厂设置:清空所有业务数据(保留 admin 账户)+ 删除 COS 资源(保留 backups/)
/// 重置前自动创建数据库备份作为安全网
///
public async Task> FactoryResetAsync(FactoryResetRequest request)
{
if (request?.ConfirmText != "RESET")
{
return ApiResponse.Fail("确认文本不正确,请输入 RESET");
}
// 1. 重置前先创建数据库备份
var backupResult = await CreateBackupAsync();
var backupId = backupResult.Data?.Id ?? 0;
// 2. 列出所有 COS 对象(在清库前读取配置,保证配置可用)
// 容错:即使 COS 列举失败,仍继续清库,确保数据库恢复出厂状态
List 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 删除失败不回滚 DB(DB 已是干净状态),仅记录日志
await _operationLog.LogAsync("工厂重置", "COS清理失败", ex.Message);
}
await _operationLog.LogAsync("工厂重置", "执行", $"清空 {tables.Length + 1} 张表,删除 {deletedCount} 个 COS 对象,备份ID {backupId}");
return ApiResponse.Success(new FactoryResetResultDto
{
TablesCleared = tables.Length + 1, // +1 for Users
ObjectsDeleted = deletedCount,
BackupId = backupId
}, "工厂重置完成");
}
// ===== 操作日志 =====
public async Task>> 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>.Success(
PagedResult.Create(items, total, request.PageIndex, request.PageSize));
}
public async Task 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 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>> 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>.Success(backups);
}
///
/// 将备份类型映射为中文:Manual->手动,Auto->自动,其余原样返回
///
private static string MapBackupType(string type) => type switch
{
"Manual" => "手动",
"Auto" => "自动",
_ => string.IsNullOrEmpty(type) ? "手动" : type
};
public async Task> 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.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.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 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> 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>> 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>.Success(list);
}
public async Task> 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.Success(MapToDto(wp));
}
public async Task> UpdateWakeProtocolAsync(long id, CreateWakeProtocolRequest request)
{
var wp = await _db.WakeProtocols.FindAsync(id);
if (wp == null)
return ApiResponse.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.Success(MapToDto(wp));
}
public async Task 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("删除成功");
}
// ===== 本地存储目录测试 =====
///
/// 测试本地存储目录是否可读写(保存存储配置前校验)
/// 支持相对路径(相对于 ContentRootPath)或绝对路径
///
public Task 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 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
};
///
/// 尝试使用 mysqldump 导出数据库
///
private async Task 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;
}
}
///
/// 回退方案:直接查询数据库生成 SQL 脚本
///
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();
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();
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();
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") + "'";
}
}