This commit is contained in:
zpc 2026-01-06 00:42:25 +08:00
parent 8aaf0396f1
commit e443805972
13 changed files with 231 additions and 195 deletions

View File

@ -80,12 +80,8 @@ namespace WorkCameraExport.Forms
_apiService.SetBaseUrl(_settings.ServerUrl);
_apiService.SetToken(_settings.SavedToken);
// 验证 Token 是否有效(通过调用一个简单的 API
var (success, _, _) = await _apiService.GetExportCountAsync(new WorkRecordExportQuery
{
PageNum = 1,
PageSize = 1
});
// 验证 Token 是否有效(通过获取当前用户信息)
var (success, _, _) = await _apiService.GetCurrentUserAsync();
if (success)
{

View File

@ -42,7 +42,7 @@ namespace WorkCameraExport.Forms
}
else
{
txtExportPath.Text = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
txtExportPath.Text = PathService.ExportDirectory;
}
UpdateExportButtonState();

View File

@ -194,10 +194,11 @@ namespace WorkCameraExport.Forms
return;
}
var outputPath = GetExcelExportPath();
if (string.IsNullOrEmpty(outputPath)) return;
// 直接生成导出路径,不使用文件对话框
var fileName = $"月报表_{dtpMonth.Value:yyyyMM}_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx";
var outputPath = Path.Combine(PathService.ExportDirectory, fileName);
_logService?.Info($"开始导出月报表 Excel共 {_currentData.Count} 条记录");
_logService?.Info($"开始导出月报表 Excel共 {_currentData.Count} 条记录,路径: {outputPath}");
SetLoadingState(true);
lblStatusBar.Text = "正在导出 Excel...";
@ -221,7 +222,7 @@ namespace WorkCameraExport.Forms
_logService?.Info($"导出完成: {outputPath}");
var result = MessageBox.Show(
"导出完成!是否打开文件所在目录?",
$"导出完成!\n文件已保存到{outputPath}\n\n是否打开文件所在目录?",
"提示",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
@ -242,27 +243,6 @@ namespace WorkCameraExport.Forms
}
}
/// <summary>
/// 获取 Excel 导出路径
/// </summary>
private string? GetExcelExportPath()
{
using var dialog = new SaveFileDialog
{
Title = "选择导出位置",
Filter = "Excel 文件 (*.xlsx)|*.xlsx",
FileName = $"月报表_{dtpMonth.Value:yyyyMM}_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx",
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
};
if (dialog.ShowDialog() == DialogResult.OK)
{
return dialog.FileName;
}
return null;
}
#endregion
#region ZIP
@ -281,8 +261,10 @@ namespace WorkCameraExport.Forms
public async Task DownloadPhotosZipAsync()
{
var yearMonth = dtpMonth.Value.ToString("yyyy-MM");
var outputPath = GetZipExportPath(yearMonth);
if (string.IsNullOrEmpty(outputPath)) return;
// 直接生成导出路径,不使用文件对话框
var fileName = $"工作照片_{yearMonth.Replace("-", "")}_{DateTime.Now:yyyyMMdd_HHmmss}.zip";
var outputPath = Path.Combine(PathService.ExportDirectory, fileName);
if (_isExporting)
{
@ -290,7 +272,7 @@ namespace WorkCameraExport.Forms
return;
}
_logService?.Info($"开始下载照片 ZIP月份: {yearMonth}");
_logService?.Info($"开始下载照片 ZIP月份: {yearMonth},路径: {outputPath}");
_isExporting = true;
_exportCts = new CancellationTokenSource();
@ -318,7 +300,7 @@ namespace WorkCameraExport.Forms
_logService?.Info($"下载完成: {outputPath}");
var result = MessageBox.Show(
"下载完成!是否打开文件所在目录?",
$"下载完成!\n文件已保存到{outputPath}\n\n是否打开文件所在目录?",
"提示",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);
@ -349,27 +331,6 @@ namespace WorkCameraExport.Forms
}
}
/// <summary>
/// 获取 ZIP 导出路径
/// </summary>
private string? GetZipExportPath(string yearMonth)
{
using var dialog = new SaveFileDialog
{
Title = "选择保存位置",
Filter = "ZIP 文件 (*.zip)|*.zip",
FileName = $"工作照片_{yearMonth.Replace("-", "")}_{DateTime.Now:yyyyMMdd_HHmmss}.zip",
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
};
if (dialog.ShowDialog() == DialogResult.OK)
{
return dialog.FileName;
}
return null;
}
/// <summary>
/// 更新下载进度
/// </summary>

View File

@ -687,10 +687,11 @@ namespace WorkCameraExport.Forms
return;
}
var outputPath = GetExportPath();
if (string.IsNullOrEmpty(outputPath)) return;
// 直接生成导出路径,不使用文件对话框
var fileName = $"工作记录导出_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx";
var outputPath = Path.Combine(PathService.ExportDirectory, fileName);
_logService?.Info($"开始导出全部数据,共 {_totalRecords} 条记录");
_logService?.Info($"开始导出全部数据,共 {_totalRecords} 条记录,路径: {outputPath}");
await DoExportAsync(outputPath, null);
}
@ -701,34 +702,14 @@ namespace WorkCameraExport.Forms
{
if (ids.Count == 0) return;
var outputPath = GetExportPath();
if (string.IsNullOrEmpty(outputPath)) return;
// 直接生成导出路径,不使用文件对话框
var fileName = $"工作记录导出_选中{ids.Count}条_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx";
var outputPath = Path.Combine(PathService.ExportDirectory, fileName);
_logService?.Info($"开始导出选中数据,共 {ids.Count} 条记录");
_logService?.Info($"开始导出选中数据,共 {ids.Count} 条记录,路径: {outputPath}");
await DoExportAsync(outputPath, ids);
}
/// <summary>
/// 获取导出路径
/// </summary>
private string? GetExportPath()
{
using var dialog = new SaveFileDialog
{
Title = "选择导出位置",
Filter = "Excel 文件 (*.xlsx)|*.xlsx",
FileName = $"工作记录导出_{DateTime.Now:yyyyMMdd_HHmmss}.xlsx",
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.Desktop)
};
if (dialog.ShowDialog() == DialogResult.OK)
{
return dialog.FileName;
}
return null;
}
/// <summary>
/// 执行导出
/// </summary>
@ -775,7 +756,7 @@ namespace WorkCameraExport.Forms
_logService?.Info($"导出完成: {outputPath}");
var result = MessageBox.Show(
"导出完成!是否打开文件所在目录?",
$"导出完成!\n文件已保存到{outputPath}\n\n是否打开文件所在目录?",
"提示",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question);

View File

@ -89,6 +89,30 @@ namespace WorkCameraExport.Models
public string NickName { get; set; } = "";
}
/// <summary>
/// 用户信息响应(/system/user/profile 接口)
/// </summary>
public class UserProfileResponse
{
public UserProfileDto? User { get; set; }
public List<string>? Roles { get; set; }
public string? PostGroup { get; set; }
}
/// <summary>
/// 用户详细信息
/// </summary>
public class UserProfileDto
{
public long UserId { get; set; }
public string? UserName { get; set; }
public string? NickName { get; set; }
public string? DeptName { get; set; }
public string? Email { get; set; }
public string? Phonenumber { get; set; }
public string? Avatar { get; set; }
}
/// <summary>
/// 工作记录导出查询请求
/// </summary>
@ -116,14 +140,17 @@ namespace WorkCameraExport.Models
public string Address { get; set; } = "";
public string Content { get; set; } = "";
public string StatusName { get; set; } = "";
public List<WorkerDto> Workers { get; set; } = new();
/// <summary>
/// 施工人员名称列表(与服务端保持一致,为字符串列表)
/// </summary>
public List<string> Workers { get; set; } = new();
public List<string> Images { get; set; } = new();
public DateTime? CreateTime { get; set; }
public DateTime? UpdateTime { get; set; }
/// <summary>
/// 获取工作人员名称列表(便捷属性
/// 获取工作人员名称列表(便捷属性,保持向后兼容
/// </summary>
public List<string> WorkerNames => Workers?.Select(w => w.WorkerName).ToList() ?? new List<string>();
public List<string> WorkerNames => Workers ?? new List<string>();
}
}

View File

@ -1,3 +1,5 @@
using WorkCameraExport.Services;
namespace WorkCameraExport.Models
{
/// <summary>
@ -30,18 +32,15 @@ namespace WorkCameraExport.Models
/// </summary>
public void Validate()
{
// 并发数限制在 1-10
if (ImageDownloadConcurrency < 1) ImageDownloadConcurrency = 1;
if (ImageDownloadConcurrency > 10) ImageDownloadConcurrency = 10;
// 压缩质量限制在 30-100
if (ImageCompressQuality < 30) ImageCompressQuality = 30;
if (ImageCompressQuality > 100) ImageCompressQuality = 100;
// 默认保存路径为空时使用桌面
if (string.IsNullOrWhiteSpace(DefaultSavePath))
{
DefaultSavePath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
DefaultSavePath = PathService.ExportDirectory;
}
}
@ -52,7 +51,7 @@ namespace WorkCameraExport.Models
{
return new AppConfig
{
DefaultSavePath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
DefaultSavePath = PathService.ExportDirectory,
ImageDownloadConcurrency = 5,
ImageCompressQuality = 50,
AutoCleanTempFiles = true

View File

@ -1,4 +1,5 @@
using System.Text.Json;
using WorkCameraExport.Services;
namespace WorkCameraExport.Models
{
@ -7,11 +8,6 @@ namespace WorkCameraExport.Models
/// </summary>
public class AppSettings
{
private static readonly string SettingsPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"WorkCameraExport",
"settings.json");
/// <summary>
/// 服务器地址
/// </summary>
@ -49,9 +45,10 @@ namespace WorkCameraExport.Models
{
try
{
if (File.Exists(SettingsPath))
var path = PathService.SettingsFile;
if (File.Exists(path))
{
var json = File.ReadAllText(SettingsPath);
var json = File.ReadAllText(path);
return JsonSerializer.Deserialize<AppSettings>(json) ?? new AppSettings();
}
}
@ -69,17 +66,11 @@ namespace WorkCameraExport.Models
{
try
{
var directory = Path.GetDirectoryName(SettingsPath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
var json = JsonSerializer.Serialize(this, new JsonSerializerOptions
{
WriteIndented = true
});
File.WriteAllText(SettingsPath, json);
File.WriteAllText(PathService.SettingsFile, json);
}
catch
{

View File

@ -13,7 +13,7 @@ static class Program
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
static async Task Main()
{
// To customize application configuration such as set high DPI settings or default font,
// see https://aka.ms/applicationconfiguration.
@ -37,26 +37,23 @@ static class Program
// 主循环:处理登录失效后重新登录
while (true)
{
// 显示登录窗体
using var loginForm = new LoginForm(apiService, configService);
if (loginForm.ShowDialog() != DialogResult.OK)
// 先尝试自动登录
var autoLoginSuccess = await loginStateManager.TryAutoLoginAsync();
if (!autoLoginSuccess)
{
// 用户取消登录,退出应用
return;
}
// 登录成功,保存登录状态(无论是自动登录还是手动登录)
if (loginForm.Token != null)
{
var settings = Models.AppSettings.Load();
if (loginForm.IsAutoLogin)
// 自动登录失败,显示登录窗体
using var loginForm = new LoginForm(apiService, configService);
if (loginForm.ShowDialog() != DialogResult.OK)
{
// 自动登录成功,启动 Token 检查定时器(不重新保存凭证)
loginStateManager.OnAutoLoginSuccess(settings.Username);
// 用户取消登录,退出应用
return;
}
else
// 手动登录成功,保存登录状态
if (loginForm.Token != null)
{
// 手动登录成功,保存凭证并启动定时器
var settings = Models.AppSettings.Load();
loginStateManager.OnLoginSuccess(
settings.ServerUrl,
settings.Username,

View File

@ -248,6 +248,34 @@ namespace WorkCameraExport.Services
_refreshToken = "";
}
/// <summary>
/// 获取当前用户信息(用于验证 Token 有效性)
/// </summary>
public async Task<(bool Success, string Message, UserInfo? Data)> GetCurrentUserAsync()
{
try
{
var response = await GetAsync<UserProfileResponse>("/system/user/profile");
if (response.IsSuccess && response.Data?.User != null)
{
return (true, "获取成功", new UserInfo
{
UserId = response.Data.User.UserId,
UserName = response.Data.User.UserName ?? "",
NickName = response.Data.User.NickName ?? ""
});
}
return (false, response.Msg ?? "获取用户信息失败", null);
}
catch (Exception ex)
{
_logService?.Error($"获取用户信息异常: {ex.Message}", ex);
return (false, $"获取用户信息异常: {ex.Message}", null);
}
}
#endregion
#region
@ -425,7 +453,7 @@ namespace WorkCameraExport.Services
await EnsureTokenValidAsync();
var queryParams = BuildQueryString(query);
var response = await ExecuteWithRetryAsync(() =>
GetAsync<List<MonthlyReportDto>>($"/api/workrecord/monthlyReport?{queryParams}"));
GetAsync<List<MonthlyReportDto>>($"/business/CamWorkers/list?{queryParams}"));
if (response.IsSuccess && response.Data != null)
{

View File

@ -11,12 +11,6 @@ namespace WorkCameraExport.Services
/// </summary>
public class ConfigService : IConfigService
{
private readonly string _appDataPath;
private readonly string _configFilePath;
private readonly string _credentialsFilePath;
private readonly string _tempPath;
private readonly string _logPath;
// 用于加密凭证的密钥AES-256 需要 32 字节密钥16 字节 IV
private static readonly byte[] EncryptionKey = Encoding.UTF8.GetBytes("WorkCameraExport2025SecretKey!!!"); // 32 bytes
private static readonly byte[] EncryptionIV = Encoding.UTF8.GetBytes("WCE2025InitVect!"); // 16 bytes
@ -29,46 +23,31 @@ namespace WorkCameraExport.Services
public ConfigService()
{
_appDataPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"WorkCameraExport");
_configFilePath = Path.Combine(_appDataPath, "config.json");
_credentialsFilePath = Path.Combine(_appDataPath, "credentials.dat");
_tempPath = Path.Combine(_appDataPath, "temp");
_logPath = Path.Combine(_appDataPath, "logs");
EnsureDirectoriesExist();
}
/// <summary>
/// 用于测试的构造函数,允许指定自定义路径
/// 用于测试的构造函数
/// </summary>
public ConfigService(string appDataPath)
public ConfigService(string _)
{
_appDataPath = appDataPath;
_configFilePath = Path.Combine(_appDataPath, "config.json");
_credentialsFilePath = Path.Combine(_appDataPath, "credentials.dat");
_tempPath = Path.Combine(_appDataPath, "temp");
_logPath = Path.Combine(_appDataPath, "logs");
EnsureDirectoriesExist();
}
#region IConfigService
public string AppDataPath => _appDataPath;
public string TempPath => _tempPath;
public string LogPath => _logPath;
public string AppDataPath => PathService.BaseDirectory;
public string TempPath => PathService.TempDirectory;
public string LogPath => PathService.LogDirectory;
public bool HasSavedCredentials => File.Exists(_credentialsFilePath);
public bool HasSavedCredentials => File.Exists(PathService.CredentialsFile);
public AppConfig LoadConfig()
{
try
{
if (File.Exists(_configFilePath))
var path = PathService.ConfigFile;
if (File.Exists(path))
{
var json = File.ReadAllText(_configFilePath);
var json = File.ReadAllText(path);
var config = JsonSerializer.Deserialize<AppConfig>(json, JsonOptions);
if (config != null)
{
@ -89,7 +68,7 @@ namespace WorkCameraExport.Services
{
config.Validate();
var json = JsonSerializer.Serialize(config, JsonOptions);
File.WriteAllText(_configFilePath, json);
File.WriteAllText(PathService.ConfigFile, json);
}
public void SaveCredentials(string serverUrl, string token, string username)
@ -104,17 +83,18 @@ namespace WorkCameraExport.Services
var json = JsonSerializer.Serialize(credentials, JsonOptions);
var encrypted = Encrypt(json);
File.WriteAllBytes(_credentialsFilePath, encrypted);
File.WriteAllBytes(PathService.CredentialsFile, encrypted);
}
public (string ServerUrl, string Token, string Username)? LoadCredentials()
{
try
{
if (!File.Exists(_credentialsFilePath))
var path = PathService.CredentialsFile;
if (!File.Exists(path))
return null;
var encrypted = File.ReadAllBytes(_credentialsFilePath);
var encrypted = File.ReadAllBytes(path);
var json = Decrypt(encrypted);
var credentials = JsonSerializer.Deserialize<CredentialsData>(json, JsonOptions);
@ -138,9 +118,10 @@ namespace WorkCameraExport.Services
{
try
{
if (File.Exists(_credentialsFilePath))
var path = PathService.CredentialsFile;
if (File.Exists(path))
{
File.Delete(_credentialsFilePath);
File.Delete(path);
}
}
catch
@ -153,9 +134,10 @@ namespace WorkCameraExport.Services
{
try
{
if (Directory.Exists(_tempPath))
var tempPath = PathService.TempDirectory;
if (Directory.Exists(tempPath))
{
var directory = new DirectoryInfo(_tempPath);
var directory = new DirectoryInfo(tempPath);
foreach (var file in directory.GetFiles())
{
try { file.Delete(); } catch { }
@ -176,16 +158,6 @@ namespace WorkCameraExport.Services
#region
private void EnsureDirectoriesExist()
{
if (!Directory.Exists(_appDataPath))
Directory.CreateDirectory(_appDataPath);
if (!Directory.Exists(_tempPath))
Directory.CreateDirectory(_tempPath);
if (!Directory.Exists(_logPath))
Directory.CreateDirectory(_logPath);
}
private static byte[] Encrypt(string plainText)
{
using var aes = Aes.Create();

View File

@ -312,7 +312,7 @@ namespace WorkCameraExport.Services
Address = dto.Address,
Content = dto.Content,
StatusName = dto.StatusName,
Workers = dto.Workers, // 保持 List<WorkerDto> 类型
Workers = dto.Workers?.Select(w => w.WorkerName).ToList() ?? new List<string>(),
Images = dto.Images.Select(i => i.Url).ToList(),
CreateTime = dto.CreateTime,
UpdateTime = dto.UpdateTime

View File

@ -57,18 +57,23 @@ namespace WorkCameraExport.Services
/// <returns>是否自动登录成功</returns>
public async Task<bool> TryAutoLoginAsync()
{
// 每次尝试自动登录时重新加载设置,确保获取最新的保存信息
var settings = AppSettings.Load();
_logService?.Info($"[自动登录] 检查设置: RememberMe={settings.RememberMe}, ServerUrl={settings.ServerUrl}, SavedToken长度={settings.SavedToken?.Length ?? 0}");
// 首先检查 AppSettings 中的保存信息
if (!_settings.RememberMe ||
string.IsNullOrEmpty(_settings.ServerUrl) ||
string.IsNullOrEmpty(_settings.SavedToken))
if (!settings.RememberMe ||
string.IsNullOrEmpty(settings.ServerUrl) ||
string.IsNullOrEmpty(settings.SavedToken))
{
_logService?.Info("未找到保存的登录信息,跳过自动登录");
return false;
}
// 检查 Token 是否已过期
if (_settings.TokenExpireTime.HasValue &&
_settings.TokenExpireTime.Value <= DateTime.Now)
if (settings.TokenExpireTime.HasValue &&
settings.TokenExpireTime.Value <= DateTime.Now)
{
_logService?.Info("保存的 Token 已过期,清除登录信息");
ClearLoginState();
@ -79,25 +84,21 @@ namespace WorkCameraExport.Services
try
{
_apiService.SetBaseUrl(_settings.ServerUrl);
_apiService.SetToken(_settings.SavedToken);
_apiService.SetBaseUrl(settings.ServerUrl);
_apiService.SetToken(settings.SavedToken);
// 验证 Token 是否有效
var (success, _, _) = await _apiService.GetExportCountAsync(new WorkRecordExportQuery
{
PageNum = 1,
PageSize = 1
});
// 验证 Token 是否有效(通过获取当前用户信息)
var (success, message, userInfo) = await _apiService.GetCurrentUserAsync();
if (success)
if (success && userInfo != null)
{
CurrentUsername = _settings.Username;
CurrentUsername = userInfo.UserName ?? settings.Username;
StartTokenCheckTimer();
_logService?.Info($"自动登录成功,用户: {CurrentUsername}");
return true;
}
_logService?.Warn("Token 验证失败,清除登录信息");
_logService?.Warn($"Token 验证失败: {message},清除登录信息");
ClearLoginState();
return false;
}
@ -181,11 +182,8 @@ namespace WorkCameraExport.Services
try
{
var (success, _, _) = await _apiService.GetExportCountAsync(new WorkRecordExportQuery
{
PageNum = 1,
PageSize = 1
});
// 通过获取当前用户信息验证 Token 有效性
var (success, _, _) = await _apiService.GetCurrentUserAsync();
if (!success)
{

View File

@ -0,0 +1,86 @@
namespace WorkCameraExport.Services
{
/// <summary>
/// 路径服务 - 统一管理所有文件路径,全部基于 exe 所在目录
/// </summary>
public static class PathService
{
private static readonly string _baseDir;
static PathService()
{
// 使用 AppContext.BaseDirectory兼容单文件发布
_baseDir = AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
/// <summary>
/// 获取基础目录exe 所在目录)
/// </summary>
public static string BaseDirectory => _baseDir;
/// <summary>
/// 获取设置文件路径
/// </summary>
public static string SettingsFile => Path.Combine(_baseDir, "settings.json");
/// <summary>
/// 获取配置文件路径
/// </summary>
public static string ConfigFile => Path.Combine(_baseDir, "config.json");
/// <summary>
/// 获取凭证文件路径
/// </summary>
public static string CredentialsFile => Path.Combine(_baseDir, "credentials.dat");
/// <summary>
/// 获取日志目录
/// </summary>
public static string LogDirectory
{
get
{
var dir = Path.Combine(_baseDir, "logs");
EnsureDirectoryExists(dir);
return dir;
}
}
/// <summary>
/// 获取临时文件目录
/// </summary>
public static string TempDirectory
{
get
{
var dir = Path.Combine(_baseDir, "temp");
EnsureDirectoryExists(dir);
return dir;
}
}
/// <summary>
/// 获取导出目录
/// </summary>
public static string ExportDirectory
{
get
{
var dir = Path.Combine(_baseDir, "Export");
EnsureDirectoryExists(dir);
return dir;
}
}
/// <summary>
/// 确保目录存在
/// </summary>
private static void EnsureDirectoryExists(string path)
{
if (!Directory.Exists(path))
{
Directory.CreateDirectory(path);
}
}
}
}