632 lines
22 KiB
C#
632 lines
22 KiB
C#
using WorkCameraExport.Models;
|
||
using WorkCameraExport.Services;
|
||
using WorkCameraExport.Services.Interfaces;
|
||
|
||
namespace WorkCameraExport.Forms
|
||
{
|
||
/// <summary>
|
||
/// 迁移窗体 - 历史数据迁移功能
|
||
/// </summary>
|
||
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<MigrationRecordDto> _records = new();
|
||
private int _totalRecords;
|
||
|
||
public MigrationForm(ApiService apiService, ILogService? logService = null)
|
||
{
|
||
InitializeComponent();
|
||
_apiService = apiService;
|
||
_logService = logService;
|
||
_cosService = new CosService(logService);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 窗体加载事件
|
||
/// </summary>
|
||
private void MigrationForm_Load(object sender, EventArgs e)
|
||
{
|
||
InitializeDefaults();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化默认值
|
||
/// </summary>
|
||
private void InitializeDefaults()
|
||
{
|
||
// 设置默认日期范围(最近90天)
|
||
dtpEndDate.Value = DateTime.Today;
|
||
dtpStartDate.Value = DateTime.Today.AddDays(-90);
|
||
|
||
// 设置默认状态为"未迁移"
|
||
cboStatus.SelectedIndex = 1; // 未迁移
|
||
|
||
UpdateMigrateButtonState();
|
||
}
|
||
|
||
#region 查询功能
|
||
|
||
/// <summary>
|
||
/// 查询按钮点击事件
|
||
/// </summary>
|
||
private async void btnQuery_Click(object sender, EventArgs e)
|
||
{
|
||
await DoQueryAsync();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行查询
|
||
/// </summary>
|
||
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<MigrationRecordDto>();
|
||
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();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构建查询参数
|
||
/// </summary>
|
||
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
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取状态文本
|
||
/// </summary>
|
||
private static string GetStatusText(int status)
|
||
{
|
||
return status switch
|
||
{
|
||
0 => "未迁移",
|
||
1 => "已迁移",
|
||
2 => "迁移失败",
|
||
_ => "未知"
|
||
};
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 选择功能
|
||
|
||
/// <summary>
|
||
/// 全选/取消复选框变更事件
|
||
/// </summary>
|
||
private void chkSelectAll_CheckedChanged(object sender, EventArgs e)
|
||
{
|
||
foreach (DataGridViewRow row in dgvRecords.Rows)
|
||
{
|
||
row.Cells[0].Value = chkSelectAll.Checked;
|
||
}
|
||
UpdateSelectedCount();
|
||
UpdateMigrateButtonState();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 数据网格单元格值变更事件(用于复选框)
|
||
/// </summary>
|
||
private void dgvRecords_CellValueChanged(object sender, DataGridViewCellEventArgs e)
|
||
{
|
||
if (e.ColumnIndex == 0 && e.RowIndex >= 0)
|
||
{
|
||
UpdateSelectedCount();
|
||
UpdateMigrateButtonState();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 数据网格当前单元格脏状态变更事件(立即提交复选框编辑)
|
||
/// </summary>
|
||
private void dgvRecords_CurrentCellDirtyStateChanged(object sender, EventArgs e)
|
||
{
|
||
if (dgvRecords.IsCurrentCellDirty && dgvRecords.CurrentCell is DataGridViewCheckBoxCell)
|
||
{
|
||
dgvRecords.CommitEdit(DataGridViewDataErrorContexts.Commit);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新已选数量
|
||
/// </summary>
|
||
private void UpdateSelectedCount()
|
||
{
|
||
int selectedCount = GetSelectedRecordIds().Count;
|
||
lblSelectedRecords.Text = $"已选 {selectedCount} 条";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取选中的记录ID列表
|
||
/// </summary>
|
||
private List<int> GetSelectedRecordIds()
|
||
{
|
||
var selectedIds = new List<int>();
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取选中的记录
|
||
/// </summary>
|
||
private List<MigrationRecordDto> GetSelectedRecords()
|
||
{
|
||
var selectedIds = GetSelectedRecordIds();
|
||
return _records.Where(r => selectedIds.Contains(r.Id)).ToList();
|
||
}
|
||
|
||
#endregion
|
||
|
||
#region 迁移功能
|
||
|
||
/// <summary>
|
||
/// 迁移按钮点击事件
|
||
/// </summary>
|
||
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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行迁移
|
||
/// </summary>
|
||
private async Task DoMigrationAsync(List<MigrationRecordDto> 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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 迁移单条记录
|
||
/// </summary>
|
||
private async Task<bool> MigrateRecordAsync(MigrationRecordDto record, CancellationToken cancellationToken)
|
||
{
|
||
var urlPairs = new List<MigrationUrlPair>();
|
||
|
||
// 获取需要迁移的图片(未迁移的)
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新行状态
|
||
/// </summary>
|
||
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 辅助方法
|
||
|
||
/// <summary>
|
||
/// 设置查询控件启用状态
|
||
/// </summary>
|
||
private void SetQueryControlsEnabled(bool enabled)
|
||
{
|
||
dtpStartDate.Enabled = enabled;
|
||
dtpEndDate.Enabled = enabled;
|
||
cboStatus.Enabled = enabled;
|
||
btnQuery.Enabled = enabled;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置迁移状态
|
||
/// </summary>
|
||
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 = "";
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新迁移按钮状态
|
||
/// </summary>
|
||
private void UpdateMigrateButtonState()
|
||
{
|
||
btnMigrate.Enabled = GetSelectedRecordIds().Count > 0 || _isMigrating;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 显示错误信息
|
||
/// </summary>
|
||
private void ShowError(string message)
|
||
{
|
||
lblStatusBar.Text = message;
|
||
lblStatusBar.ForeColor = Color.Red;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 截断内容
|
||
/// </summary>
|
||
private static string TruncateContent(string content, int maxLength)
|
||
{
|
||
if (string.IsNullOrEmpty(content)) return "";
|
||
return content.Length <= maxLength ? content : content[..maxLength] + "...";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 窗体关闭事件
|
||
/// </summary>
|
||
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
|
||
}
|
||
}
|