using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using NCC.Common.Extension; using NCC.Dependency; using NCC.DynamicApiController; using NCC.Extend.Entitys.lq_kd_kdjlb; using NCC.FriendlyException; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using SqlSugar; namespace NCC.Extend { /// /// 文件URL迁移服务 - 将本地文件路径转换为OSS路径 /// [ApiDescriptionSettings(Tag = "数据迁移", Name = "FileUrlMigration", Order = 999)] [Route("api/Extend/[controller]")] public class FileUrlMigrationService : IDynamicApiController, ITransient { private readonly ISqlSugarRepository _repository; private readonly SqlSugarScope _db; private readonly ILogger _logger; private const string OSS_BASE_URL = "http://oss.lvqianmeiye.com/Files/SystemFile"; private const string OLD_URL_PREFIX = "/api/File/Image/annexpic/"; private const string OLD_OSS_URL = "http://oss.lvqianmeiye.com"; private const string NEW_OSS_URL = "https://lvqian-erip.oss-cn-chengdu.aliyuncs.com"; /// /// 初始化一个类型的新实例 /// /// SqlSugar仓储 /// 日志记录器 public FileUrlMigrationService(ISqlSugarRepository repository, ILogger logger) { _repository = repository; _db = _repository.Context; _logger = logger; } /// /// 迁移所有表的文件URL路径 /// /// /// 将数据库中的文件URL从本地路径格式转换为OSS路径格式 /// /// 处理的表和字段: /// 1. lq_kd_kdjlb: scwj, hyqz, F_FIleUrl /// 2. lq_xh_feedback: F_BeforeImage, F_AfterImage /// 3. lq_hytk_hytk: F_FileUrl, F_SignatureFile /// 4. lq_purchase_records: F_Attachment /// /// URL转换规则: /// 原格式:/api/File/Image/annexpic/文件名 /// 新格式:http://oss.lvqianmeiye.com/Files/SystemFile/文件名 /// /// 迁移结果统计 [HttpPost("MigrateAllFileUrls")] public async Task MigrateAllFileUrls() { var startTime = DateTime.Now; _logger.LogInformation("开始迁移文件URL路径..."); var totalResults = new { lq_kd_kdjlb_scwj = await MigrateTableField("lq_kd_kdjlb", "scwj", "F_Id"), lq_kd_kdjlb_hyqz = await MigrateTableField("lq_kd_kdjlb", "hyqz", "F_Id"), lq_kd_kdjlb_F_FIleUrl = await MigrateTableField("lq_kd_kdjlb", "F_FIleUrl", "F_Id"), lq_xh_feedback_F_BeforeImage = await MigrateTableField("lq_xh_feedback", "F_BeforeImage", "F_Id"), lq_xh_feedback_F_AfterImage = await MigrateTableField("lq_xh_feedback", "F_AfterImage", "F_Id"), lq_hytk_hytk_F_FileUrl = await MigrateTableField("lq_hytk_hytk", "F_FileUrl", "F_Id"), lq_hytk_hytk_F_SignatureFile = await MigrateTableField("lq_hytk_hytk", "F_SignatureFile", "F_Id"), lq_purchase_records_F_Attachment = await MigrateTableField("lq_purchase_records", "F_Attachment", "F_Id") }; var endTime = DateTime.Now; var duration = (endTime - startTime).TotalSeconds; // 统计总数 var totalProcessed = 0; var totalUpdated = 0; var totalErrors = 0; var resultsList = new List { totalResults.lq_kd_kdjlb_scwj, totalResults.lq_kd_kdjlb_hyqz, totalResults.lq_kd_kdjlb_F_FIleUrl, totalResults.lq_xh_feedback_F_BeforeImage, totalResults.lq_xh_feedback_F_AfterImage, totalResults.lq_hytk_hytk_F_FileUrl, totalResults.lq_hytk_hytk_F_SignatureFile, totalResults.lq_purchase_records_F_Attachment }; foreach (var result in resultsList) { totalProcessed += result.TotalProcessed; totalUpdated += result.TotalUpdated; totalErrors += result.TotalErrors; } _logger.LogInformation($"文件URL迁移完成,耗时:{duration:F2}秒,总处理:{totalProcessed},总更新:{totalUpdated},总错误:{totalErrors}"); return new { success = true, duration = $"{duration:F2}秒", summary = new { totalProcessed, totalUpdated, totalErrors }, details = totalResults }; } /// /// 迁移指定表的指定字段 /// private async Task MigrateTableField(string tableName, string fieldName, string primaryKey) { var result = new MigrationResult { TableName = tableName, FieldName = fieldName }; try { _logger.LogInformation($"开始处理表 {tableName}.{fieldName}"); // 查询需要处理的数据 var sql = $@" SELECT {primaryKey} as Id, {fieldName} as FieldValue FROM {tableName} WHERE {fieldName} IS NOT NULL AND {fieldName} != '' AND {fieldName} != '[]' AND {fieldName} LIKE '%{OLD_URL_PREFIX}%'"; var records = await _db.Ado.SqlQueryAsync(sql); result.TotalProcessed = records.Count; if (records.Count == 0) { _logger.LogInformation($"表 {tableName}.{fieldName} 没有需要处理的数据"); return result; } _logger.LogInformation($"表 {tableName}.{fieldName} 找到 {records.Count} 条需要处理的数据"); // 分批处理,每批100条 const int batchSize = 100; var totalBatches = (int)Math.Ceiling((double)records.Count / batchSize); for (int batchIndex = 0; batchIndex < totalBatches; batchIndex++) { var batch = records.Skip(batchIndex * batchSize).Take(batchSize).ToList(); var updateList = new List<(string id, string newValue)>(); foreach (var record in batch) { try { var id = record.Id?.ToString(); var fieldValue = record.FieldValue?.ToString(); if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(fieldValue)) continue; var newValue = ConvertFileUrlJson(fieldValue); if (newValue != null && newValue != fieldValue) { updateList.Add((id, newValue)); } } catch (Exception ex) { result.TotalErrors++; _logger.LogError(ex, $"处理记录时出错,表:{tableName},字段:{fieldName},记录ID:{record.Id}"); } } // 批量更新 if (updateList.Any()) { try { _db.BeginTran(); foreach (var (id, newValue) in updateList) { // 使用参数化查询防止SQL注入 var updateSql = $"UPDATE `{tableName}` SET `{fieldName}` = @Value WHERE `{primaryKey}` = @Id"; await _db.Ado.ExecuteCommandAsync(updateSql, new { Value = newValue, Id = id }); } _db.CommitTran(); result.TotalUpdated += updateList.Count; _logger.LogInformation($"表 {tableName}.{fieldName} 批次 {batchIndex + 1} 成功更新 {updateList.Count} 条记录"); } catch (Exception ex) { _db.RollbackTran(); result.TotalErrors += updateList.Count; _logger.LogError(ex, $"批量更新时出错,表:{tableName},字段:{fieldName},批次:{batchIndex + 1}"); } } _logger.LogInformation($"表 {tableName}.{fieldName} 批次 {batchIndex + 1}/{totalBatches} 处理完成"); } _logger.LogInformation($"表 {tableName}.{fieldName} 处理完成,处理:{result.TotalProcessed},更新:{result.TotalUpdated},错误:{result.TotalErrors}"); } catch (Exception ex) { _logger.LogError(ex, $"处理表 {tableName}.{fieldName} 时发生错误"); result.TotalErrors = result.TotalProcessed; } return result; } /// /// 转换文件URL JSON字符串 /// private string ConvertFileUrlJson(string jsonString) { try { if (string.IsNullOrWhiteSpace(jsonString) || jsonString.Trim() == "[]") return jsonString; // 解析JSON数组 var jsonArray = JArray.Parse(jsonString); bool hasChanges = false; foreach (var item in jsonArray) { if (item is JObject obj && obj["url"] != null) { var url = obj["url"].ToString(); if (url.StartsWith(OLD_URL_PREFIX)) { // 提取文件名 var fileName = url.Substring(OLD_URL_PREFIX.Length); // 构建新的OSS URL var newUrl = $"{OSS_BASE_URL}/{fileName}"; obj["url"] = newUrl; hasChanges = true; } } } return hasChanges ? jsonArray.ToString(Formatting.None) : jsonString; } catch (Exception ex) { _logger.LogError(ex, $"转换JSON时出错:{jsonString}"); return jsonString; // 转换失败时返回原值 } } /// /// 迁移所有表的OSS URL(将旧OSS地址替换为新OSS地址) /// /// /// 将数据库中的文件URL从旧OSS地址格式转换为新OSS地址格式 /// /// 处理的表和字段: /// 1. lq_kd_kdjlb: scwj, hyqz, F_FIleUrl /// 2. lq_xh_feedback: F_BeforeImage, F_AfterImage /// 3. lq_hytk_hytk: F_FileUrl, F_SignatureFile /// 4. lq_purchase_records: F_Attachment /// /// URL转换规则: /// 原格式:http://oss.lvqianmeiye.com/... /// 新格式:https://lvqian-erip.oss-cn-chengdu.aliyuncs.com/... /// /// 迁移结果统计 [HttpPost("MigrateOssUrls")] public async Task MigrateOssUrls() { var startTime = DateTime.Now; _logger.LogInformation("开始迁移OSS URL路径..."); var totalResults = new { lq_kd_kdjlb_scwj = await MigrateOssUrlInTableField("lq_kd_kdjlb", "scwj", "F_Id"), lq_kd_kdjlb_hyqz = await MigrateOssUrlInTableField("lq_kd_kdjlb", "hyqz", "F_Id"), lq_kd_kdjlb_F_FIleUrl = await MigrateOssUrlInTableField("lq_kd_kdjlb", "F_FIleUrl", "F_Id"), lq_xh_feedback_F_BeforeImage = await MigrateOssUrlInTableField("lq_xh_feedback", "F_BeforeImage", "F_Id"), lq_xh_feedback_F_AfterImage = await MigrateOssUrlInTableField("lq_xh_feedback", "F_AfterImage", "F_Id"), lq_hytk_hytk_F_FileUrl = await MigrateOssUrlInTableField("lq_hytk_hytk", "F_FileUrl", "F_Id"), lq_hytk_hytk_F_SignatureFile = await MigrateOssUrlInTableField("lq_hytk_hytk", "F_SignatureFile", "F_Id"), lq_purchase_records_F_Attachment = await MigrateOssUrlInTableField("lq_purchase_records", "F_Attachment", "F_Id") }; var endTime = DateTime.Now; var duration = (endTime - startTime).TotalSeconds; // 统计总数 var totalProcessed = 0; var totalUpdated = 0; var totalErrors = 0; var resultsList = new List { totalResults.lq_kd_kdjlb_scwj, totalResults.lq_kd_kdjlb_hyqz, totalResults.lq_kd_kdjlb_F_FIleUrl, totalResults.lq_xh_feedback_F_BeforeImage, totalResults.lq_xh_feedback_F_AfterImage, totalResults.lq_hytk_hytk_F_FileUrl, totalResults.lq_hytk_hytk_F_SignatureFile, totalResults.lq_purchase_records_F_Attachment }; foreach (var result in resultsList) { totalProcessed += result.TotalProcessed; totalUpdated += result.TotalUpdated; totalErrors += result.TotalErrors; } _logger.LogInformation($"OSS URL迁移完成,耗时:{duration:F2}秒,总处理:{totalProcessed},总更新:{totalUpdated},总错误:{totalErrors}"); return new { success = true, duration = $"{duration:F2}秒", summary = new { totalProcessed, totalUpdated, totalErrors }, details = totalResults }; } /// /// 迁移指定表的指定字段中的OSS URL /// private async Task MigrateOssUrlInTableField(string tableName, string fieldName, string primaryKey) { var result = new MigrationResult { TableName = tableName, FieldName = fieldName }; try { _logger.LogInformation($"开始处理表 {tableName}.{fieldName} 的OSS URL迁移"); // 查询需要处理的数据(包含旧OSS URL的记录) var sql = $@" SELECT {primaryKey} as Id, {fieldName} as FieldValue FROM {tableName} WHERE {fieldName} IS NOT NULL AND {fieldName} != '' AND {fieldName} != '[]' AND {fieldName} LIKE '%{OLD_OSS_URL}%'"; var records = await _db.Ado.SqlQueryAsync(sql); result.TotalProcessed = records.Count; if (records.Count == 0) { _logger.LogInformation($"表 {tableName}.{fieldName} 没有需要处理的数据"); return result; } _logger.LogInformation($"表 {tableName}.{fieldName} 找到 {records.Count} 条需要处理的数据"); // 分批处理,每批100条 const int batchSize = 100; var totalBatches = (int)Math.Ceiling((double)records.Count / batchSize); for (int batchIndex = 0; batchIndex < totalBatches; batchIndex++) { var batch = records.Skip(batchIndex * batchSize).Take(batchSize).ToList(); var updateList = new List<(string id, string newValue)>(); foreach (var record in batch) { try { var id = record.Id?.ToString(); var fieldValue = record.FieldValue?.ToString(); if (string.IsNullOrEmpty(id) || string.IsNullOrEmpty(fieldValue)) continue; var newValue = ReplaceOssUrlInJson(fieldValue); if (newValue != null && newValue != fieldValue) { updateList.Add((id, newValue)); } } catch (Exception ex) { result.TotalErrors++; _logger.LogError(ex, $"处理记录时出错,表:{tableName},字段:{fieldName},记录ID:{record.Id}"); } } // 批量更新 if (updateList.Any()) { try { _db.BeginTran(); foreach (var (id, newValue) in updateList) { // 使用参数化查询防止SQL注入 var updateSql = $"UPDATE `{tableName}` SET `{fieldName}` = @Value WHERE `{primaryKey}` = @Id"; await _db.Ado.ExecuteCommandAsync(updateSql, new { Value = newValue, Id = id }); } _db.CommitTran(); result.TotalUpdated += updateList.Count; _logger.LogInformation($"表 {tableName}.{fieldName} 批次 {batchIndex + 1} 成功更新 {updateList.Count} 条记录"); } catch (Exception ex) { _db.RollbackTran(); result.TotalErrors += updateList.Count; _logger.LogError(ex, $"批量更新时出错,表:{tableName},字段:{fieldName},批次:{batchIndex + 1}"); } } _logger.LogInformation($"表 {tableName}.{fieldName} 批次 {batchIndex + 1}/{totalBatches} 处理完成"); } _logger.LogInformation($"表 {tableName}.{fieldName} 处理完成,处理:{result.TotalProcessed},更新:{result.TotalUpdated},错误:{result.TotalErrors}"); } catch (Exception ex) { _logger.LogError(ex, $"处理表 {tableName}.{fieldName} 时发生错误"); result.TotalErrors = result.TotalProcessed; } return result; } /// /// 替换JSON字符串中的OSS URL /// private string ReplaceOssUrlInJson(string jsonString) { try { if (string.IsNullOrWhiteSpace(jsonString) || jsonString.Trim() == "[]") return jsonString; // 如果包含旧OSS URL,直接替换 if (jsonString.Contains(OLD_OSS_URL)) { var newJsonString = jsonString.Replace(OLD_OSS_URL, NEW_OSS_URL); // 如果是JSON数组格式,需要解析并替换 try { var jsonArray = JArray.Parse(jsonString); bool hasChanges = false; foreach (var item in jsonArray) { if (item is JObject obj && obj["url"] != null) { var url = obj["url"].ToString(); if (url.Contains(OLD_OSS_URL)) { obj["url"] = url.Replace(OLD_OSS_URL, NEW_OSS_URL); hasChanges = true; } } } return hasChanges ? jsonArray.ToString(Formatting.None) : jsonString; } catch { // 如果不是JSON格式,直接替换字符串 return newJsonString; } } return jsonString; } catch (Exception ex) { _logger.LogError(ex, $"替换OSS URL时出错:{jsonString}"); return jsonString; // 转换失败时返回原值 } } /// /// 迁移结果 /// private class MigrationResult { public string TableName { get; set; } public string FieldName { get; set; } public int TotalProcessed { get; set; } public int TotalUpdated { get; set; } public int TotalErrors { get; set; } } } }