using System; using System.Collections.Generic; using System.Threading.Tasks; using MDL.Models; using MDL.DTOs; using DAL.Interfaces; using DB.Database; using SqlSugar; namespace DAL.Repositories { /// /// 标签替换请求的数据访问实现类 /// public class LabelReplaceRepository : ILabelReplaceRepository { private readonly ISqlSugarProvider _provider; /// /// 构造函数 /// /// SqlSugar数据库提供程序 public LabelReplaceRepository(ISqlSugarProvider provider) { _provider = provider; } /// /// 创建标签替换请求记录 /// /// 标签替换请求实体 /// 创建的记录ID public async Task CreateAsync(LabelReplaceEntity entity) { var db = _provider.GetClient(); // 确保表存在 // 使用更安全的表初始化方式 if (!db.DbMaintenance.IsAnyTable("label_replace_requests")) { db.CodeFirst.InitTables(typeof(LabelReplaceEntity)); } // 设置时间戳 entity.CreatedAt = DateTime.UtcNow; entity.UpdatedAt = DateTime.UtcNow; // 插入数据并返回自增ID return await db.Insertable(entity).ExecuteReturnIdentityAsync(); } /// /// 根据ID获取标签替换请求记录 /// /// 记录ID /// 标签替换请求实体 public async Task GetByIdAsync(int id) { var db = _provider.GetClient(); return await db.Queryable() .Where(x => x.Id == id) .FirstAsync(); } /// /// 根据中性面单单号获取标签替换请求记录 /// /// 中性面单单号 /// 标签替换请求实体 public async Task GetByWaybillNumberAsync(string waybillNumber) { var db = _provider.GetClient(); return await db.Queryable() .Where(x => x.NeutralWaybillNumber == waybillNumber) .FirstAsync(); } /// /// 根据跟踪单号获取标签替换请求记录 /// /// 跟踪单号 /// 标签替换请求实体列表 public async Task> GetByTrackingNumberAsync(string trackingNumber) { var db = _provider.GetClient(); return await db.Queryable() .Where(x => x.FinalMileTrackingNumber == trackingNumber) .OrderByDescending(x => x.CreatedAt) .ToListAsync(); } /// /// 更新标签替换请求记录 /// /// 标签替换请求实体 /// 更新是否成功 public async Task UpdateAsync(LabelReplaceEntity entity) { var db = _provider.GetClient(); // 更新时间戳 entity.UpdatedAt = DateTime.UtcNow; // 更新数据并返回影响行数 var rows = await db.Updateable(entity).ExecuteCommandAsync(); return rows > 0; } /// /// 删除标签替换请求记录 /// /// 记录ID /// 删除是否成功 public async Task DeleteAsync(int id) { var db = _provider.GetClient(); var rows = await db.Deleteable() .Where(x => x.Id == id) .ExecuteCommandAsync(); return rows > 0; } /// /// 获取所有标签替换请求记录 /// /// 标签替换请求实体列表 public async Task> GetAllAsync() { var db = _provider.GetClient(); return await db.Queryable() .OrderByDescending(x => x.CreatedAt) .ToListAsync(); } /// /// 分页获取标签替换请求记录(包含客户信息) /// /// 页码 /// 每页大小 /// 排序字段 /// 排序顺序 /// 提单号 /// 大包号 /// 参考号 /// 中性面单单号 /// 尾程跟踪单号 /// 换单状态 /// 客户ID /// 创建时间开始 /// 创建时间结束 /// 换单时间开始 /// 换单时间结束 /// 包含客户信息的标签替换请求DTO列表和总记录数 public async Task<(List, int)> GetByPageAsync(int page, int pageSize, string sortBy, string sortOrder, string billOfLadingNumber, string masterPackageNumber, string referenceNumber, string neutralWaybillNumber, string finalMileTrackingNumber, string replaceStatus, int? customerId, string startCreatedAt, string endCreatedAt, string startReplacedAt, string endReplacedAt) { var db = _provider.GetClient(); // 解析时间筛选参数 DateTime? parsedStartCreatedAt = null; DateTime? parsedEndCreatedAt = null; DateTime? parsedStartReplacedAt = null; DateTime? parsedEndReplacedAt = null; if (!string.IsNullOrEmpty(startCreatedAt) && DateTime.TryParse(startCreatedAt, out DateTime startCreatedAtDate)) { parsedStartCreatedAt = startCreatedAtDate; } if (!string.IsNullOrEmpty(endCreatedAt) && DateTime.TryParse(endCreatedAt, out DateTime endCreatedAtDate)) { parsedEndCreatedAt = endCreatedAtDate; } if (!string.IsNullOrEmpty(startReplacedAt) && DateTime.TryParse(startReplacedAt, out DateTime startReplacedAtDate)) { parsedStartReplacedAt = startReplacedAtDate; } if (!string.IsNullOrEmpty(endReplacedAt) && DateTime.TryParse(endReplacedAt, out DateTime endReplacedAtDate)) { parsedEndReplacedAt = endReplacedAtDate; } // 先创建基础查询 var baseQuery = db.Queryable(); // 应用基础筛选条件 if (!string.IsNullOrEmpty(billOfLadingNumber)) { baseQuery = baseQuery.Where(l => l.BillOfLadingNumber.Contains(billOfLadingNumber)); } if (!string.IsNullOrEmpty(masterPackageNumber)) { baseQuery = baseQuery.Where(l => l.MasterPackageNumber.Contains(masterPackageNumber)); } if (!string.IsNullOrEmpty(referenceNumber)) { baseQuery = baseQuery.Where(l => l.ReferenceNumber.Contains(referenceNumber)); } if (!string.IsNullOrEmpty(neutralWaybillNumber)) { // 处理多个单号的情况,支持逗号分隔 var waybillNumbers = neutralWaybillNumber.Split(new[] { ',', '\n', '\r', ' ' }, StringSplitOptions.RemoveEmptyEntries); if (waybillNumbers.Length > 1) { baseQuery = baseQuery.Where(l => waybillNumbers.Contains(l.NeutralWaybillNumber)); } else if (waybillNumbers.Length == 1) { baseQuery = baseQuery.Where(l => l.NeutralWaybillNumber.Contains(waybillNumbers[0])); } } if (!string.IsNullOrEmpty(finalMileTrackingNumber)) { baseQuery = baseQuery.Where(l => l.FinalMileTrackingNumber.Contains(finalMileTrackingNumber)); } if (!string.IsNullOrEmpty(replaceStatus)) { baseQuery = baseQuery.Where(l => l.ReplaceStatus == replaceStatus); } if (customerId.HasValue) { baseQuery = baseQuery.Where(l => l.CustomerId == customerId); } // 只应用 CreatedAt 筛选条件,ReplacedAt 稍后在内存中筛选 if (parsedStartCreatedAt.HasValue) { baseQuery = baseQuery.Where(l => l.CreatedAt >= parsedStartCreatedAt.Value); } if (parsedEndCreatedAt.HasValue) { baseQuery = baseQuery.Where(l => l.CreatedAt <= parsedEndCreatedAt.Value); } // 先获取所有符合基础条件的记录(不分页) var allLabelReplaceEntities = await baseQuery.ToListAsync(); // 如果没有数据,直接返回 if (allLabelReplaceEntities.Count == 0) { return (new List(), 0); } // 提取所有中性面单单号,用于批量查询最新扫描记录 var scanWaybillNumbers = allLabelReplaceEntities.Select(l => l.NeutralWaybillNumber).Distinct().ToList(); // 批量查询每个中性面单单号的最新扫描成功记录 var latestScans = new List(); if (scanWaybillNumbers.Count > 0) { // 先获取所有符合条件的扫描记录 var allScans = await db.Queryable() .Where(s => scanWaybillNumbers.Contains(s.NeutralWaybillNumber) && s.Result == 0) .OrderByDescending(s => s.CreatedAt) .ToListAsync(); // 按中性面单单号分组,取每组的第一条记录(最新的) var groupedScans = allScans.GroupBy(s => s.NeutralWaybillNumber); foreach (var group in groupedScans) { latestScans.Add(group.First()); } } // 创建中性面单单号到最新扫描时间的映射,确保类型正确 var latestScanMap = new Dictionary(); foreach (var scan in latestScans) { latestScanMap[scan.NeutralWaybillNumber] = scan.CreatedAt; } // 根据 ReplacedAt 筛选记录 var filteredEntities = new List(); foreach (var entity in allLabelReplaceEntities) { bool shouldInclude = true; // 获取此订单的ReplacedAt时间 DateTime? replacedAt = null; if (latestScanMap.TryGetValue(entity.NeutralWaybillNumber, out var latestCreatedAt)) { replacedAt = latestCreatedAt; } // 应用 ReplacedAt 筛选 if (parsedStartReplacedAt.HasValue) { if (!replacedAt.HasValue || replacedAt.Value < parsedStartReplacedAt.Value) { shouldInclude = false; } } if (parsedEndReplacedAt.HasValue) { if (!replacedAt.HasValue || replacedAt.Value > parsedEndReplacedAt.Value) { shouldInclude = false; } } if (shouldInclude) { filteredEntities.Add(entity); } } // 计算筛选后的总记录数 int totalCount = filteredEntities.Count; // 对筛选后的实体进行排序 if (!string.IsNullOrEmpty(sortBy)) { if (sortOrder.ToLower() == "asc") { // 根据常用排序字段排序 filteredEntities = sortBy switch { "CreatedAt" => filteredEntities.OrderBy(e => e.CreatedAt).ToList(), "UpdatedAt" => filteredEntities.OrderBy(e => e.UpdatedAt).ToList(), "Id" => filteredEntities.OrderBy(e => e.Id).ToList(), _ => filteredEntities.OrderBy(e => e.CreatedAt).ToList() }; } else { filteredEntities = sortBy switch { "CreatedAt" => filteredEntities.OrderByDescending(e => e.CreatedAt).ToList(), "UpdatedAt" => filteredEntities.OrderByDescending(e => e.UpdatedAt).ToList(), "Id" => filteredEntities.OrderByDescending(e => e.Id).ToList(), _ => filteredEntities.OrderByDescending(e => e.CreatedAt).ToList() }; } } else { filteredEntities = filteredEntities.OrderByDescending(e => e.CreatedAt).ToList(); } // 应用分页 var pagedEntities = filteredEntities .Skip((page - 1) * pageSize) .Take(pageSize) .ToList(); // 如果分页后没有数据,直接返回 if (pagedEntities.Count == 0) { return (new List(), totalCount); } // 提取所有非null的客户ID,用于批量查询 var customerIds = pagedEntities.Where(l => l.CustomerId.HasValue).Select(l => l.CustomerId.Value).Distinct().ToList(); // 批量查询客户信息 var customerList = await db.Queryable() .Where(c => customerIds.Contains(c.Id)) .ToListAsync(); // 手动构建字典,确保类型正确 Dictionary customers = new Dictionary(); foreach (var customer in customerList) { customers[customer.Id] = customer.CustomerCode; } // 构建DTO列表 var dtos = new List(); foreach (var l in pagedEntities) { var dto = new MDL.DTOs.LabelReplaceWithCustomerDto { Id = l.Id, BillOfLadingNumber = l.BillOfLadingNumber, MasterPackageNumber = l.MasterPackageNumber, ReferenceNumber = l.ReferenceNumber, NeutralWaybillNumber = l.NeutralWaybillNumber, FinalMileTrackingNumber = l.FinalMileTrackingNumber, Label = l.Label, HasLabel = !string.IsNullOrEmpty(l.Label), ReplaceStatus = l.ReplaceStatus, LabelRetrievedAt = l.LabelRetrievedAt, CreatedAt = l.CreatedAt, UpdatedAt = l.UpdatedAt }; // 处理客户代码 if (l.CustomerId.HasValue) { if (customers.TryGetValue(l.CustomerId.Value, out var customerCode)) { dto.CustomerCode = customerCode; } } // 处理扫描时间 if (latestScanMap.TryGetValue(l.NeutralWaybillNumber, out var latestCreatedAt)) { dto.ReplacedAt = latestCreatedAt; } dtos.Add(dto); } return (dtos, totalCount); } /// /// 批量查询换单状态 /// /// 客户ID /// 中性面单单号列表 /// 尾程跟踪单号列表 /// 标签替换请求实体列表 public async Task<(List, Dictionary)> GetLabelReplaceStatusAsync(int customerId, List waybillNumbers, List trackingNumbers) { var db = _provider.GetClient(); // 构建查询条件 var query = db.Queryable() .Where(x => x.CustomerId == customerId); // 添加单号查询条件 if (waybillNumbers != null && waybillNumbers.Count > 0) { query = query.Where(x => waybillNumbers.Contains(x.NeutralWaybillNumber)); } if (trackingNumbers != null && trackingNumbers.Count > 0) { query = query.Where(x => x.FinalMileTrackingNumber != null && trackingNumbers.Contains(x.FinalMileTrackingNumber)); } // 执行查询 var labelReplaceEntities = await query.ToListAsync(); // 提取所有中性面单单号,用于批量查询最新扫描记录 var scanWaybillNumbers = labelReplaceEntities.Select(l => l.NeutralWaybillNumber).Distinct().ToList(); // 批量查询每个中性面单单号的最新扫描成功记录 var latestScanMap = new Dictionary(); if (scanWaybillNumbers.Count > 0) { // 先获取所有符合条件的扫描记录 var allScans = await db.Queryable() .Where(s => scanWaybillNumbers.Contains(s.NeutralWaybillNumber) && s.Result == 0) .OrderByDescending(s => s.CreatedAt) .ToListAsync(); // 按中性面单单号分组,取每组的第一条记录(最新的) var groupedScans = allScans.GroupBy(s => s.NeutralWaybillNumber); foreach (var group in groupedScans) { var latestScan = group.First(); latestScanMap[latestScan.NeutralWaybillNumber] = latestScan.CreatedAt; } } return (labelReplaceEntities, latestScanMap); } /// /// 根据交接单号列表批量获取标签替换请求记录 /// /// 交接单号列表 /// 客户ID /// 标签替换请求实体列表 public async Task> GetByHandoverNumbersAsync(List handoverNumbers, int? customerId) { var db = _provider.GetClient(); if (handoverNumbers == null || handoverNumbers.Count == 0) return new List(); var query = db.Queryable() .Where(x => handoverNumbers.Contains(x.BillOfLadingNumber) || handoverNumbers.Contains(x.MasterPackageNumber)); if (customerId.HasValue) { query = query.Where(x => x.CustomerId == customerId.Value); } return await query.ToListAsync(); } /// /// 将base64编码转换为PDF文件并保存 /// /// base64编码的PDF内容 /// 文件名 /// 保存的文件路径 public async Task ConvertBase64ToPdfAsync(string base64Content, string fileName) { // 定义保存目录 string saveDirectory = @"C:\TESYSTEM\OMS\api-lable\pdf\20260302"; // 确保目录存在 if (!System.IO.Directory.Exists(saveDirectory)) { System.IO.Directory.CreateDirectory(saveDirectory); } // 构建完整的文件路径 string filePath = System.IO.Path.Combine(saveDirectory, fileName); // 处理base64内容,移除可能的前缀 if (base64Content.StartsWith("data:application/pdf;base64,")) { base64Content = base64Content.Substring("data:application/pdf;base64,".Length); } // 转换base64为字节数组 byte[] pdfBytes = Convert.FromBase64String(base64Content); // 保存到文件 await System.IO.File.WriteAllBytesAsync(filePath, pdfBytes); return filePath; } /// /// 更新Label字段但保持LabelRetrievedAt不变 /// /// 记录ID /// 新的Label值 /// 更新是否成功 public async Task UpdateLabelWithoutChangingRetrievedAtAsync(int id, string newLabel) { var db = _provider.GetClient(); // 首先获取现有记录,以便获取LabelRetrievedAt的当前值 var existing = await db.Queryable() .Where(x => x.Id == id) .FirstAsync(); if (existing == null) { return false; } // 显式更新Label字段,同时将LabelRetrievedAt设置为原值, // 这样触发器就不会改变它 var rows = await db.Updateable() .SetColumns(x => x.Label == newLabel) .SetColumns(x => x.LabelRetrievedAt == existing.LabelRetrievedAt) .SetColumns(x => x.UpdatedAt == DateTime.UtcNow) .Where(x => x.Id == id) .ExecuteCommandAsync(); return rows > 0; } /// /// 获取每日标签统计数据 /// /// 开始日期 /// 结束日期 /// 客户ID /// 每日标签统计数据列表 public async Task> GetDailyLabelStatsAsync(string startDate, string endDate, int? customerId) { var db = _provider.GetClient(); // 构建日期范围条件 DateTime? start = null; DateTime? end = null; if (!string.IsNullOrEmpty(startDate)) { start = DateTime.Parse(startDate); } if (!string.IsNullOrEmpty(endDate)) { end = DateTime.Parse(endDate).AddDays(1).AddSeconds(-1); } // 获取所有符合条件的 label_replace_requests(label不为空) var labelRequestsQuery = db.Queryable() .Where(x => x.Label != null && x.Label != ""); if (customerId.HasValue) { labelRequestsQuery = labelRequestsQuery.Where(x => x.CustomerId == customerId); } var labelRequests = await labelRequestsQuery.ToListAsync(); var waybillNumbers = labelRequests.Select(x => x.NeutralWaybillNumber).Distinct().ToList(); if (waybillNumbers.Count == 0) { return new List(); } // 获取所有相关的扫描记录 var scansQuery = db.Queryable() .Where(x => waybillNumbers.Contains(x.NeutralWaybillNumber)); if (customerId.HasValue) { scansQuery = scansQuery.Where(x => x.CustomerId == customerId); } var allScans = await scansQuery.OrderBy(x => x.CreatedAt).ToListAsync(); // 构建统计字典 var statsDict = new Dictionary(); // 获取数据拉取时间(当前UTC时间减5小时) var dataFetchTime = DateTime.UtcNow.AddHours(-5); // 1. 统计换单失败未完结数(所有有扫描记录但未能成功换单的数量) // 对于每个中性面单单号,检查是否有Result = 0的记录,如果没有就算失败未完结 var waybillScanGroups = allScans.GroupBy(x => x.NeutralWaybillNumber); var unfinishedFailureSet = new HashSet(); foreach (var group in waybillScanGroups) { var hasSuccess = group.Any(x => x.Result == 0); if (!hasSuccess) { unfinishedFailureSet.Add(group.Key); } } // 2. 按日期统计各项指标 foreach (var scan in allScans) { // 转换为UTC-5时区的日期 var dateKey = scan.CreatedAt.AddHours(-5).ToString("yyyy-MM-dd"); // 应用日期范围过滤 if (start.HasValue && scan.CreatedAt.AddHours(-5) < start.Value) continue; if (end.HasValue && scan.CreatedAt.AddHours(-5) > end.Value) continue; if (!statsDict.ContainsKey(dateKey)) { statsDict[dateKey] = new DailyLabelStatsDto { Date = dateKey, DataFetchTime = dataFetchTime }; } var stats = statsDict[dateKey]; // 当日扫描数 stats.DailyScanCount++; // 当日换单成功数(Result = 0) if (scan.Result == 0) { stats.DailySuccessCount++; // 当日STOP数(Result = 0且描述包含"成功返回STOP标签") if (!string.IsNullOrEmpty(scan.Description) && scan.Description.Contains("成功返回STOP标签")) { stats.DailyStopCount++; } } else { // 当日换单失败数(Result != 0) stats.DailyFailureCount++; } } // 3. 统计当日标签推送数(按LabelRetrievedAt统计) foreach (var request in labelRequests) { if (request.LabelRetrievedAt.HasValue) { var dateKey = request.LabelRetrievedAt.Value.AddHours(-5).ToString("yyyy-MM-dd"); // 应用日期范围过滤 if (start.HasValue && request.LabelRetrievedAt.Value.AddHours(-5) < start.Value) continue; if (end.HasValue && request.LabelRetrievedAt.Value.AddHours(-5) > end.Value) continue; if (!statsDict.ContainsKey(dateKey)) { statsDict[dateKey] = new DailyLabelStatsDto { Date = dateKey, DataFetchTime = dataFetchTime }; } statsDict[dateKey].DailyLabelPushCount++; } } // 4. 为每个日期设置换单失败未完结数(这是一个全局统计,适用于所有日期) foreach (var stats in statsDict.Values) { stats.UnfinishedFailureCount = unfinishedFailureSet.Count; } // 转换为列表并按日期排序 var result = statsDict.Values.OrderBy(x => x.Date).ToList(); return result; } /// /// 获取每日标签统计数据(中文版本,使用自定义SQL) /// /// 每日标签统计数据列表 public async Task> GetDailyLabelStatsChineseAsync() { // 使用固定的数据库连接 string customConnectionString = "server=172.233.222.200;port=6033;user id=oms_user;password=oms_user@pwd;database=lr01mainusa;CharSet=utf8;allow zero datetime=true;Convert Zero Datetime=true;Max Pool Size=100;Min Pool Size=10;Connection Timeout=30;Allow User Variables=True;"; // 直接使用 MySqlConnection 来执行 SQL,绕过 SqlSugar 初始化问题 var result = new List(); using (var connection = new MySql.Data.MySqlClient.MySqlConnection(customConnectionString)) { await connection.OpenAsync(); // 读取 SQL 文件内容 string sql = @" WITH -- 步骤1:获取所有到货交接单,日期已是UTC-5 ArrivalFormsWithDate AS ( SELECT a.Id, a.HandoverNumber, DATE(a.ReceiptTime) AS 到货日期, a.ReceiptTime AS 到货时间 FROM arrival_handover_forms a ), -- 步骤2:关联到货交接单与换单请求(只取Label有值的),并记录订单级别的信息,计算考核时间 ArrivalRequests AS ( SELECT a.到货日期, a.到货时间, l.Id AS RequestId, l.NeutralWaybillNumber, l.BillOfLadingNumber, l.MasterPackageNumber, l.Label, l.LabelRetrievedAt, l.CustomerId, -- 考核时间:标签推送时间与到仓时间比较,哪个最新用哪个 CASE WHEN l.LabelRetrievedAt IS NULL THEN a.到货时间 WHEN l.LabelRetrievedAt > a.到货时间 THEN l.LabelRetrievedAt ELSE a.到货时间 END AS 考核时间 FROM ArrivalFormsWithDate a INNER JOIN label_replace_requests l ON l.BillOfLadingNumber = a.HandoverNumber OR l.MasterPackageNumber = a.HandoverNumber WHERE l.Label IS NOT NULL AND l.Label != '' ), -- 步骤3:获取每个订单的扫描记录情况(按天) DailyScanStatus AS ( SELECT s.NeutralWaybillNumber, DATE(CONVERT_TZ(s.CreatedAt, '+00:00', '-05:00')) AS 日期, MAX(CASE WHEN s.Result = 0 THEN 1 ELSE 0 END) AS 当日是否成功, MAX(CASE WHEN s.Result != 0 THEN 1 ELSE 0 END) AS 当日是否失败 FROM label_scan_history s INNER JOIN label_replace_requests l ON s.NeutralWaybillNumber = l.NeutralWaybillNumber WHERE l.Label IS NOT NULL AND l.Label != '' GROUP BY s.NeutralWaybillNumber, DATE(CONVERT_TZ(s.CreatedAt, '+00:00', '-05:00')) ), -- 步骤4:获取每个订单是否曾经成功,以及首次成功日期和时间 OverallScanStatus AS ( SELECT s.NeutralWaybillNumber, MAX(CASE WHEN s.Result = 0 THEN 1 ELSE 0 END) AS 曾成功, MIN(CASE WHEN s.Result = 0 THEN DATE(CONVERT_TZ(s.CreatedAt, '+00:00', '-05:00')) ELSE NULL END) AS 首次成功日期, MIN(CASE WHEN s.Result = 0 THEN s.CreatedAt ELSE NULL END) AS 首次成功时间 FROM label_scan_history s GROUP BY s.NeutralWaybillNumber ), -- 步骤5:从数据中收集所有日期 AllDates AS ( SELECT 到货日期 AS 日期 FROM ArrivalRequests UNION SELECT 日期 FROM DailyScanStatus UNION SELECT DATE(CONVERT_TZ(l.LabelRetrievedAt, '+00:00', '-05:00')) AS 日期 FROM label_replace_requests l WHERE l.LabelRetrievedAt IS NOT NULL ), -- 步骤6:去重并排序日期 DistinctDates AS ( SELECT DISTINCT 日期 FROM AllDates ORDER BY 日期 ), -- 步骤7:获取最新日期 LatestDate AS ( SELECT MAX(日期) AS 日期 FROM DistinctDates ), -- 步骤8:每日基础统计 - 当日新增换单数 DailyBase AS ( SELECT dd.日期, -- 当日新增换单数:当天到货并且推送了标签数据的订单 COUNT(DISTINCT CASE WHEN ar.到货日期 = dd.日期 AND ar.LabelRetrievedAt IS NOT NULL THEN ar.RequestId END) AS 当日新增换单数, -- 当天标签推送数 COUNT(DISTINCT CASE WHEN DATE(CONVERT_TZ(ar.LabelRetrievedAt, '+00:00', '-05:00')) = dd.日期 THEN ar.RequestId END) AS 当日标签推送数 FROM DistinctDates dd CROSS JOIN ArrivalRequests ar GROUP BY dd.日期 ), -- 步骤9:每日扫描统计(扫描次数),换单成功数去重 DailyScanMetrics AS ( SELECT DATE(CONVERT_TZ(s.CreatedAt, '+00:00', '-05:00')) AS 日期, COUNT(*) AS 当日扫描数, -- 当日STOP数(同样去重) COUNT(DISTINCT CASE WHEN s.Result = 0 AND s.Description LIKE '%成功返回STOP标签%' THEN s.NeutralWaybillNumber END) AS 当日STOP数 FROM label_scan_history s INNER JOIN label_replace_requests l ON s.NeutralWaybillNumber = l.NeutralWaybillNumber WHERE l.Label IS NOT NULL AND l.Label != '' GROUP BY DATE(CONVERT_TZ(s.CreatedAt, '+00:00', '-05:00')) ), -- 步骤9b:每日换单成功数(去重) DailySuccessCount AS ( SELECT scan_date AS 日期, COUNT(DISTINCT NeutralWaybillNumber) AS 当日换单成功数 FROM ( -- 找出每个包裹每天最新的成功记录 SELECT s.NeutralWaybillNumber, DATE(CONVERT_TZ(s.CreatedAt, '+00:00', '-05:00')) AS scan_date, ROW_NUMBER() OVER (PARTITION BY s.NeutralWaybillNumber, DATE(CONVERT_TZ(s.CreatedAt, '+00:00', '-05:00')) ORDER BY s.CreatedAt DESC) AS rn FROM label_scan_history s INNER JOIN label_replace_requests l ON s.NeutralWaybillNumber = l.NeutralWaybillNumber WHERE l.Label IS NOT NULL AND l.Label != '' AND s.Result = 0 ) AS s WHERE rn = 1 GROUP BY scan_date ), -- 步骤10:历史日期的换单失败未完结统计(非最新日期) HistoryUnfinished AS ( SELECT dd.日期, COUNT(DISTINCT CASE -- 当天失败并且当天没有成功的订单 WHEN dss.日期 = dd.日期 AND dss.当日是否失败 = 1 AND dss.当日是否成功 = 0 THEN dss.NeutralWaybillNumber END) AS 换单失败未完结订单 FROM DistinctDates dd LEFT JOIN DailyScanStatus dss ON dd.日期 = dss.日期 CROSS JOIN LatestDate ld WHERE dd.日期 != ld.日期 GROUP BY dd.日期 ), -- 步骤11:最新日期的换单失败未完结统计(所有历史从未成功的) LatestUnfinished AS ( SELECT ld.日期, COUNT(DISTINCT CASE -- 必须同时满足: -- 1. 有过扫描记录(OverallScanStatus中有该订单) -- 2. 从未成功(曾成功 = 0) -- 3. 并且至少有一次失败记录 WHEN oss.曾成功 = 0 THEN ar.NeutralWaybillNumber END) AS 换单失败未完结订单 FROM LatestDate ld CROSS JOIN ArrivalRequests ar INNER JOIN OverallScanStatus oss ON ar.NeutralWaybillNumber = oss.NeutralWaybillNumber GROUP BY ld.日期 ), -- 步骤12:每日换单失败订单统计(当天失败并且当天没成功的订单数) DailyFailedOrders AS ( SELECT 日期, COUNT(DISTINCT CASE WHEN 当日是否失败 = 1 AND 当日是否成功 = 0 THEN NeutralWaybillNumber END) AS 当日换单失败 FROM DailyScanStatus GROUP BY 日期 ), -- 步骤13:每日完成订单统计 - 订单必须在我们关联的ArrivalRequests中 DailyCompletedOrders AS ( SELECT oss.首次成功日期 AS 日期, COUNT(DISTINCT oss.NeutralWaybillNumber) AS 当日完成数 FROM OverallScanStatus oss INNER JOIN ArrivalRequests ar ON oss.NeutralWaybillNumber = ar.NeutralWaybillNumber WHERE oss.曾成功 = 1 GROUP BY oss.首次成功日期 ), -- 步骤14:24小时换单完成订单统计 - 根据考核时间判断 Daily24HCompletedOrders AS ( SELECT oss.首次成功日期 AS 日期, COUNT(DISTINCT ar.NeutralWaybillNumber) AS 24H内完成数 FROM ArrivalRequests ar INNER JOIN OverallScanStatus oss ON ar.NeutralWaybillNumber = oss.NeutralWaybillNumber WHERE oss.曾成功 = 1 AND oss.首次成功时间 IS NOT NULL AND ar.考核时间 IS NOT NULL -- 考核时间减去换单完成时间小于等于24小时 AND TIMESTAMPDIFF(HOUR, ar.考核时间, oss.首次成功时间) <= 24 GROUP BY oss.首次成功日期 ), -- 步骤15:完整的每日统计基础 - 准备每日的新增和完成,并获取前一日数据 DailyStatsWithPrev AS ( SELECT db.日期, db.当日新增换单数, db.当日标签推送数, COALESCE(dco.当日完成数, 0) AS 当日完成数, COALESCE(dc24h.24H内完成数, 0) AS 24H内完成数, -- 获取前一日的新增 LAG(db.当日新增换单数, 1, 0) OVER (ORDER BY db.日期) AS 前一日新增, -- 获取前一日的完成数 LAG(COALESCE(dco.当日完成数, 0), 1, 0) OVER (ORDER BY db.日期) AS 前一日完成数, -- 行号 ROW_NUMBER() OVER (ORDER BY db.日期) AS rn FROM DailyBase db LEFT JOIN DailyCompletedOrders dco ON db.日期 = dco.日期 LEFT JOIN Daily24HCompletedOrders dc24h ON db.日期 = dc24h.日期 ORDER BY db.日期 ) -- 步骤16:计算累计数据并最终输出 SELECT 日期, 当日新增换单数, 累计要换的总单数, 换单失败未完结订单, 当日换单失败, 当日换单成功数, 当日STOP数, -- 24小时换单率 CASE WHEN 当日完成数 = 0 THEN '0.00%' ELSE CONCAT(ROUND(24H内完成数 / 当日完成数 * 100, 2), '%') END AS 24H换单率, -- 当天换单完成率 CASE WHEN (当日新增换单数 + 累计要换的总单数) = 0 THEN '0.00%' ELSE CONCAT(ROUND(当日完成数 / (当日新增换单数 + 累计要换的总单数) * 100, 2), '%') END AS 当天换单完成率, 当日标签推送数, 当日扫描数, 数据拉取时间(UTC_5) FROM ( SELECT t.日期, t.当日新增换单数, -- 使用变量保持状态,每次计算前一天的累计 -- 公式:累计 = MAX(0, 前一日累计 + 前一日新增 - 前一日完成) -- 第1天直接用当日新增 @running_total := GREATEST(0, CASE WHEN t.rn = 1 THEN t.当日新增换单数 ELSE @running_total + t.前一日新增 - t.前一日完成数 END) AS 累计要换的总单数, -- 根据是否是最新日期选择不同的未完结统计 COALESCE( CASE WHEN t.日期 = (SELECT 日期 FROM LatestDate) THEN lu.换单失败未完结订单 ELSE hu.换单失败未完结订单 END, 0 ) AS 换单失败未完结订单, COALESCE(dfo.当日换单失败, 0) AS 当日换单失败, COALESCE(dsc.当日换单成功数, 0) AS 当日换单成功数, COALESCE(dsm.当日STOP数, 0) AS 当日STOP数, t.24H内完成数, t.当日完成数, t.当日标签推送数, COALESCE(dsm.当日扫描数, 0) AS 当日扫描数, (UTC_TIMESTAMP() - INTERVAL 5 HOUR) AS 数据拉取时间(UTC_5) FROM DailyStatsWithPrev t LEFT JOIN DailyScanMetrics dsm ON t.日期 = dsm.日期 LEFT JOIN DailySuccessCount dsc ON t.日期 = dsc.日期 LEFT JOIN HistoryUnfinished hu ON t.日期 = hu.日期 LEFT JOIN LatestUnfinished lu ON t.日期 = lu.日期 LEFT JOIN DailyFailedOrders dfo ON t.日期 = dfo.日期 -- 初始化变量 CROSS JOIN (SELECT @running_total := 0) AS init ORDER BY t.日期 ) AS subquery ORDER BY 日期 DESC "; using (var command = new MySql.Data.MySqlClient.MySqlCommand(sql, connection)) using (var reader = await command.ExecuteReaderAsync()) { while (await reader.ReadAsync()) { var dto = new DailyLabelStatsChineseDto { Date = reader["日期"] != DBNull.Value ? Convert.ToDateTime(reader["日期"]).ToString("yyyy-MM-dd") : string.Empty, DailyNewReplaceCount = reader["当日新增换单数"] != DBNull.Value ? Convert.ToInt32(reader["当日新增换单数"]) : 0, CumulativeTotalReplaceCount = reader["累计要换的总单数"] != DBNull.Value ? Convert.ToInt32(reader["累计要换的总单数"]) : 0, UnfinishedFailureCount = reader["换单失败未完结订单"] != DBNull.Value ? Convert.ToInt32(reader["换单失败未完结订单"]) : 0, DailyFailureCount = reader["当日换单失败"] != DBNull.Value ? Convert.ToInt32(reader["当日换单失败"]) : 0, DailySuccessCount = reader["当日换单成功数"] != DBNull.Value ? Convert.ToInt32(reader["当日换单成功数"]) : 0, DailyStopCount = reader["当日STOP数"] != DBNull.Value ? Convert.ToInt32(reader["当日STOP数"]) : 0, Rate24Hour = reader["24H换单率"] as string, DailyCompletionRate = reader["当天换单完成率"] as string, DailyLabelPushCount = reader["当日标签推送数"] != DBNull.Value ? Convert.ToInt32(reader["当日标签推送数"]) : 0, DailyScanCount = reader["当日扫描数"] != DBNull.Value ? Convert.ToInt32(reader["当日扫描数"]) : 0, DataFetchTime = reader["数据拉取时间(UTC_5)"] != DBNull.Value ? Convert.ToDateTime(reader["数据拉取时间(UTC_5)"]) : DateTime.Now }; result.Add(dto); } } } return result; } } }