using WorkCameraExport.Models; using WorkCameraExport.Services; using WorkCameraExport.Services.Interfaces; namespace WorkCameraExport.Forms { /// /// 迁移窗体 - 历史数据迁移功能 /// public partial class MigrationForm : Form { private readonly ApiService _apiService; private readonly CosService _cosService; private readonly ILogService? _logService; private CancellationTokenSource? _migrationCts; private bool _isMigrating; // 记录数据 private List _records = new(); private int _totalRecords; public MigrationForm(ApiService apiService, ILogService? logService = null) { InitializeComponent(); _apiService = apiService; _logService = logService; _cosService = new CosService(logService); } /// /// 窗体加载事件 /// private void MigrationForm_Load(object sender, EventArgs e) { InitializeDefaults(); } /// /// 初始化默认值 /// private void InitializeDefaults() { // 设置默认日期范围(最近90天) dtpEndDate.Value = DateTime.Today; dtpStartDate.Value = DateTime.Today.AddDays(-90); // 设置默认状态为"未迁移" cboStatus.SelectedIndex = 1; // 未迁移 UpdateMigrateButtonState(); } #region 查询功能 /// /// 查询按钮点击事件 /// private async void btnQuery_Click(object sender, EventArgs e) { await DoQueryAsync(); } /// /// 执行查询 /// private async Task DoQueryAsync() { SetQueryControlsEnabled(false); lblStatusBar.Text = "正在查询..."; lblStatusBar.ForeColor = Color.Blue; dgvRecords.Rows.Clear(); _records.Clear(); _totalRecords = 0; _logService?.Info("[迁移界面] 开始查询迁移记录"); try { var query = BuildQuery(); query.PageNum = 1; query.PageSize = 50; _logService?.Info($"[迁移界面] 查询条件: 开始={query.StartDate}, 结束={query.EndDate}, 状态={query.Status}"); // 获取所有记录(分页获取) var allRecords = new List(); int pageNum = 1; while (true) { query.PageNum = pageNum; var (success, message, data) = await _apiService.GetMigrationListAsync(query); if (!success || data == null) { _logService?.Error($"[迁移界面] 查询失败: {message}"); ShowError(message); return; } allRecords.AddRange(data.Result); _totalRecords = data.TotalNum; _logService?.Info($"[迁移界面] 第{pageNum}页获取 {data.Result.Count} 条, 累计 {allRecords.Count}/{_totalRecords}"); if (data.Result.Count < 50 || pageNum >= data.TotalPage) break; pageNum++; // 更新状态 lblStatusBar.Text = $"正在查询... 已获取 {allRecords.Count} 条"; } _records = allRecords; // 显示数据 foreach (var record in _records) { dgvRecords.Rows.Add( false, // 选择框 record.Id, record.RecordTime?.ToString("yyyy-MM-dd HH:mm") ?? "", record.DeptName, TruncateContent(record.Content, 40), record.ImageCount, GetStatusText(record.MigrationStatus) ); } // 更新统计信息 lblTotalRecords.Text = $"共 {_totalRecords} 条记录"; UpdateSelectedCount(); lblStatusBar.Text = $"查询完成,共 {_totalRecords} 条记录"; lblStatusBar.ForeColor = Color.Green; _logService?.Info($"[迁移界面] 查询完成, 共 {_totalRecords} 条记录"); } catch (Exception ex) { ShowError($"查询异常: {ex.Message}"); } finally { SetQueryControlsEnabled(true); UpdateMigrateButtonState(); } } /// /// 构建查询参数 /// private MigrationQuery BuildQuery() { int? status = cboStatus.SelectedIndex switch { 1 => 0, // 未迁移 2 => 1, // 已迁移 3 => 2, // 迁移失败 _ => null // 全部 }; return new MigrationQuery { StartDate = dtpStartDate.Value.Date, EndDate = dtpEndDate.Value.Date.AddDays(1).AddSeconds(-1), Status = status }; } /// /// 获取状态文本 /// private static string GetStatusText(int status) { return status switch { 0 => "未迁移", 1 => "已迁移", 2 => "迁移失败", _ => "未知" }; } #endregion #region 选择功能 /// /// 全选/取消复选框变更事件 /// private void chkSelectAll_CheckedChanged(object sender, EventArgs e) { foreach (DataGridViewRow row in dgvRecords.Rows) { row.Cells[0].Value = chkSelectAll.Checked; } UpdateSelectedCount(); UpdateMigrateButtonState(); } /// /// 数据网格单元格值变更事件(用于复选框) /// private void dgvRecords_CellValueChanged(object sender, DataGridViewCellEventArgs e) { if (e.ColumnIndex == 0 && e.RowIndex >= 0) { UpdateSelectedCount(); UpdateMigrateButtonState(); } } /// /// 数据网格当前单元格脏状态变更事件(立即提交复选框编辑) /// private void dgvRecords_CurrentCellDirtyStateChanged(object sender, EventArgs e) { if (dgvRecords.IsCurrentCellDirty && dgvRecords.CurrentCell is DataGridViewCheckBoxCell) { dgvRecords.CommitEdit(DataGridViewDataErrorContexts.Commit); } } /// /// 更新已选数量 /// private void UpdateSelectedCount() { int selectedCount = GetSelectedRecordIds().Count; lblSelectedRecords.Text = $"已选 {selectedCount} 条"; } /// /// 获取选中的记录ID列表 /// private List GetSelectedRecordIds() { var selectedIds = new List(); foreach (DataGridViewRow row in dgvRecords.Rows) { if (row.Cells[0].Value is true) { if (int.TryParse(row.Cells[1].Value?.ToString(), out int id)) { selectedIds.Add(id); } } } return selectedIds; } /// /// 获取选中的记录 /// private List GetSelectedRecords() { var selectedIds = GetSelectedRecordIds(); return _records.Where(r => selectedIds.Contains(r.Id)).ToList(); } #endregion #region 迁移功能 /// /// 迁移按钮点击事件 /// private async void btnMigrate_Click(object sender, EventArgs e) { if (_isMigrating) { // 取消迁移 _migrationCts?.Cancel(); return; } var selectedRecords = GetSelectedRecords(); if (selectedRecords.Count == 0) { MessageBox.Show("请先选择要迁移的记录", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var result = MessageBox.Show( $"确定要迁移选中的 {selectedRecords.Count} 条记录吗?\n\n迁移过程中请勿关闭程序。", "确认迁移", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result != DialogResult.Yes) return; await DoMigrationAsync(selectedRecords); } /// /// 执行迁移 /// private async Task DoMigrationAsync(List records) { _isMigrating = true; _migrationCts = new CancellationTokenSource(); SetMigratingState(true); int successCount = 0; int failCount = 0; int totalRecords = records.Count; int processedRecords = 0; _logService?.Info($"[迁移界面] 开始迁移, 共 {totalRecords} 条记录"); try { // 获取 COS 临时密钥 lblCurrentRecord.Text = "正在获取 COS 临时密钥..."; _logService?.Info("[迁移界面] 正在获取 COS 临时密钥..."); var (credSuccess, credMessage, credentials) = await _apiService.GetTempCredentialsAsync(); if (!credSuccess || credentials == null) { _logService?.Error($"[迁移界面] 获取 COS 临时密钥失败: {credMessage}"); ShowError($"获取 COS 临时密钥失败: {credMessage}"); return; } _logService?.Info($"[迁移界面] COS 临时密钥获取成功, Bucket={credentials.Bucket}, Region={credentials.Region}"); // 初始化 COS 服务 _cosService.Initialize(credentials); // 逐条迁移记录 foreach (var record in records) { if (_migrationCts.Token.IsCancellationRequested) { _logService?.Info("[迁移界面] 迁移已取消"); lblStatusBar.Text = "迁移已取消"; lblStatusBar.ForeColor = Color.Orange; break; } processedRecords++; lblCurrentRecord.Text = $"正在迁移记录 {record.Id} ({processedRecords}/{totalRecords})..."; _logService?.Info($"[迁移界面] 开始迁移记录 {record.Id}, 图片数={record.Images?.Count ?? 0}"); try { var migrationResult = await MigrateRecordAsync(record, _migrationCts.Token); if (migrationResult) { successCount++; _logService?.Info($"[迁移界面] 记录 {record.Id} 迁移成功"); UpdateRowStatus(record.Id, "已迁移", Color.Green); } else { failCount++; _logService?.Warn($"[迁移界面] 记录 {record.Id} 迁移失败"); UpdateRowStatus(record.Id, "迁移失败", Color.Red); } } catch (OperationCanceledException) { _logService?.Info("[迁移界面] 迁移操作被取消"); break; } catch (Exception ex) { failCount++; _logService?.Error($"[迁移界面] 记录 {record.Id} 迁移异常: {ex.Message}", ex); UpdateRowStatus(record.Id, "迁移失败", Color.Red); } // 更新进度 int progress = (int)((processedRecords * 100.0) / totalRecords); progressBar.Value = progress; lblProgress.Text = $"{progress}%"; } // 完成 if (!_migrationCts.Token.IsCancellationRequested) { progressBar.Value = 100; lblProgress.Text = "100%"; lblCurrentRecord.Text = $"迁移完成!成功: {successCount}, 失败: {failCount}"; lblStatusBar.Text = $"迁移完成,成功 {successCount} 条,失败 {failCount} 条"; lblStatusBar.ForeColor = failCount > 0 ? Color.Orange : Color.Green; _logService?.Info($"[迁移界面] 迁移完成, 成功={successCount}, 失败={failCount}"); MessageBox.Show( $"迁移完成!\n\n成功: {successCount} 条\n失败: {failCount} 条", "迁移结果", MessageBoxButtons.OK, failCount > 0 ? MessageBoxIcon.Warning : MessageBoxIcon.Information); } } catch (Exception ex) { ShowError($"迁移异常: {ex.Message}"); } finally { _isMigrating = false; _migrationCts?.Dispose(); _migrationCts = null; SetMigratingState(false); } } /// /// 迁移单条记录 /// private async Task MigrateRecordAsync(MigrationRecordDto record, CancellationToken cancellationToken) { var urlPairs = new List(); // 获取需要迁移的图片(未迁移的) var imagesToMigrate = record.Images.Where(img => !img.IsMigrated).ToList(); _logService?.Info($"[迁移] 记录 {record.Id}: 总图片={record.Images?.Count ?? 0}, 待迁移={imagesToMigrate.Count}"); if (imagesToMigrate.Count == 0) { // 没有需要迁移的图片,直接返回成功 _logService?.Info($"[迁移] 记录 {record.Id}: 无需迁移的图片, 跳过"); return true; } int imageIndex = 0; int totalImages = imagesToMigrate.Count; foreach (var image in imagesToMigrate) { if (cancellationToken.IsCancellationRequested) throw new OperationCanceledException(); imageIndex++; lblCurrentRecord.Text = $"正在迁移记录 {record.Id} - 图片 {imageIndex}/{totalImages}..."; try { _logService?.Info($"[迁移] 记录 {record.Id} 图片 {imageIndex}/{totalImages}: 开始下载 {image.Url}"); // 1. 下载原图片 var (downloadSuccess, imageData, downloadMessage) = await _apiService.DownloadImageAsync(image.Url, cancellationToken); if (!downloadSuccess || imageData == null) { _logService?.Warn($"[迁移] 记录 {record.Id} 图片下载失败: {downloadMessage}, URL={image.Url}"); continue; // 跳过失败的图片,继续处理其他图片 } _logService?.Info($"[迁移] 记录 {record.Id} 图片下载成功, 大小={imageData.Length} bytes"); // 2. 生成 COS 路径 var recordTime = record.RecordTime ?? DateTime.Now; var fileName = CosService.GenerateFileName(".jpg"); var cosKey = CosService.GenerateCosKey(recordTime, "当日照片", "", fileName); _logService?.Info($"[迁移] 记录 {record.Id} 准备上传到 COS, Key={cosKey}"); // 3. 上传到 COS var (uploadSuccess, uploadMessage, cosUrl) = await _cosService.UploadBytesAsync(imageData, cosKey, cancellationToken: cancellationToken); if (!uploadSuccess || string.IsNullOrEmpty(cosUrl)) { _logService?.Warn($"[迁移] 记录 {record.Id} 上传COS失败: {uploadMessage}"); continue; // 跳过失败的图片,继续处理其他图片 } _logService?.Info($"[迁移] 记录 {record.Id} 上传COS成功, NewUrl={cosUrl}"); // 4. 记录 URL 映射 urlPairs.Add(new MigrationUrlPair { OldUrl = image.Url, NewUrl = cosUrl }); } catch (OperationCanceledException) { throw; } catch (Exception ex) { _logService?.Error($"[迁移] 记录 {record.Id} 处理图片异常: {ex.Message}, URL={image.Url}", ex); continue; // 跳过失败的图片,继续处理其他图片 } } _logService?.Info($"[迁移] 记录 {record.Id}: 成功处理 {urlPairs.Count}/{totalImages} 张图片"); // 5. 调用 API 更新 URL if (urlPairs.Count > 0) { var updateRequest = new MigrationUpdateRequest { RecordId = record.Id, ImageUrls = urlPairs }; _logService?.Info($"[迁移] 记录 {record.Id}: 调用API更新URL, 数量={urlPairs.Count}"); var (updateSuccess, updateMessage) = await _apiService.UpdateMigrationUrlsAsync(updateRequest); if (!updateSuccess) { _logService?.Error($"[迁移] 记录 {record.Id} 更新URL失败: {updateMessage}"); return false; } _logService?.Info($"[迁移] 记录 {record.Id}: API更新URL成功"); } else { _logService?.Warn($"[迁移] 记录 {record.Id}: 没有成功迁移的图片, 不调用更新API"); } // 如果所有图片都成功迁移,返回成功 return urlPairs.Count == imagesToMigrate.Count; } /// /// 更新行状态 /// private void UpdateRowStatus(int recordId, string status, Color color) { foreach (DataGridViewRow row in dgvRecords.Rows) { if (row.Cells[1].Value?.ToString() == recordId.ToString()) { row.Cells[6].Value = status; row.Cells[6].Style.ForeColor = color; break; } } } #endregion #region UI 辅助方法 /// /// 设置查询控件启用状态 /// private void SetQueryControlsEnabled(bool enabled) { dtpStartDate.Enabled = enabled; dtpEndDate.Enabled = enabled; cboStatus.Enabled = enabled; btnQuery.Enabled = enabled; } /// /// 设置迁移状态 /// private void SetMigratingState(bool migrating) { SetQueryControlsEnabled(!migrating); dgvRecords.Enabled = !migrating; chkSelectAll.Enabled = !migrating; if (migrating) { btnMigrate.Text = "取消迁移"; btnMigrate.BackColor = Color.FromArgb(220, 53, 69); } else { btnMigrate.Text = "开始迁移"; btnMigrate.BackColor = Color.FromArgb(40, 167, 69); progressBar.Value = 0; lblProgress.Text = ""; } } /// /// 更新迁移按钮状态 /// private void UpdateMigrateButtonState() { btnMigrate.Enabled = GetSelectedRecordIds().Count > 0 || _isMigrating; } /// /// 显示错误信息 /// private void ShowError(string message) { lblStatusBar.Text = message; lblStatusBar.ForeColor = Color.Red; } /// /// 截断内容 /// private static string TruncateContent(string content, int maxLength) { if (string.IsNullOrEmpty(content)) return ""; return content.Length <= maxLength ? content : content[..maxLength] + "..."; } /// /// 窗体关闭事件 /// private void MigrationForm_FormClosing(object sender, FormClosingEventArgs e) { if (_isMigrating) { var result = MessageBox.Show( "正在迁移中,确定要取消并退出吗?", "确认", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.No) { e.Cancel = true; return; } _migrationCts?.Cancel(); } // 释放 COS 服务 _cosService.Dispose(); } #endregion } }