using CsvHelper; using CsvHelper.Configuration; using OfficeOpenXml; namespace Service; public interface IImportService { /// /// 导入支付宝账单 /// Task<(bool ok, string message)> ImportAlipayAsync(MemoryStream file, string fileExtension); /// /// 导入微信账单 /// Task<(bool ok, string message)> ImportWeChatAsync(MemoryStream file, string fileExtension); } public class ImportService( ILogger logger, ITransactionRecordRepository transactionRecordRepository ) : IImportService { public async Task<(bool ok, string message)> ImportAlipayAsync(MemoryStream file, string fileExtension) { var content = await ParseAsync(file, fileExtension); logger.LogInformation("转换后的支付宝账单数据行数: {RowCount}", content.Length); if (content.Length == 0) { logger.LogWarning("支付宝账单文件解析后无数据行"); return (false, "支付宝账单文件解析后无数据行"); } var addTransactionRecords = new List(); var updateTransactionRecords = new List(); foreach (var row in content) { var importNo = row.ContainsKey("交易号") ? row["交易号"] : string.Empty; if (string.IsNullOrWhiteSpace(importNo)) { logger.LogWarning("跳过无交易号的记录: {Row}", row); continue; } var existingRecord = await transactionRecordRepository.ExistsByImportNoAsync(importNo, "支付宝"); if (existingRecord != null) { existingRecord.Reason = GetReason(row); existingRecord.Amount = GetDecimalValue(row, "金额(元)"); existingRecord.RefundAmount = GetDecimalValue(row, "成功退款(元)"); existingRecord.OccurredAt = GetDateTimeValue(row, "交易创建时间"); existingRecord.Type = GetTransactionType(row, "收/支"); updateTransactionRecords.Add(existingRecord); continue; } var transactionRecord = new TransactionRecord { Reason = GetReason(row), Amount = GetDecimalValue(row, "金额(元)"), RefundAmount = GetDecimalValue(row, "成功退款(元)"), Balance = 0, OccurredAt = GetDateTimeValue(row, "交易创建时间"), Type = GetTransactionType(row, "收/支"), ImportNo = importNo, ImportFrom = "支付宝" }; addTransactionRecords.Add(transactionRecord); } if (addTransactionRecords.Count == 0 && updateTransactionRecords.Count == 0) { logger.LogWarning("未找到可导入或更新的支付宝交易记录"); return (false, "未找到可导入或更新的支付宝交易记录"); } var message = new StringBuilder(); if (addTransactionRecords.Count > 0) { if (await transactionRecordRepository.AddRangeAsync(addTransactionRecords)) { logger.LogInformation("成功导入支付宝交易记录数: {Count}", addTransactionRecords.Count); message.AppendLine($"成功导入支付宝交易记录数: {addTransactionRecords.Count}"); } } if (updateTransactionRecords.Count > 0) { if (await transactionRecordRepository.UpdateRangeAsync(updateTransactionRecords)) { logger.LogInformation("成功更新支付宝交易记录数: {Count}", updateTransactionRecords.Count); message.AppendLine($"成功更新支付宝交易记录数: {updateTransactionRecords.Count}"); } } return (true, message.ToString()); string GetReason(IDictionary row) { var reason = string.Empty; if (row.ContainsKey("交易对方") && !string.IsNullOrWhiteSpace(row["交易对方"])) { reason += row["交易对方"]; } if (row.ContainsKey("商品名称") && !string.IsNullOrWhiteSpace(row["商品名称"])) { reason += row["商品名称"]; } return reason; } decimal GetDecimalValue(IDictionary row, string key) { if (row.ContainsKey(key) && decimal.TryParse(row[key], out var value)) { return value; } return 0m; } DateTime GetDateTimeValue(IDictionary row, string key) { if (!row.ContainsKey(key)) { return DateTime.MinValue; } foreach (var format in DateTimeFormats) { if (DateTime.TryParseExact( row[key], format, CultureInfo.InvariantCulture, DateTimeStyles.None, out var value)) { return value; } } if (DateTime.TryParse(row[key], out var value2)) { return value2; } return DateTime.MinValue; } TransactionType GetTransactionType(IDictionary row, string key) { if (!row.ContainsKey(key)) { return TransactionType.None; } var typeStr = row[key]; return typeStr switch { "支出" => TransactionType.Expense, "收入" => TransactionType.Income, _ => TransactionType.None }; } } public async Task<(bool ok, string message)> ImportWeChatAsync(MemoryStream file, string fileExtension) { var content = await ParseAsync(file, fileExtension); logger.LogInformation("转换后的微信账单数据行数: {RowCount}", content.Length); if (content.Length == 0) { logger.LogWarning("微信账单文件解析后无数据行"); return (false, "微信账单文件解析后无数据行"); } var addTransactionRecords = new List(); var updateTransactionRecords = new List(); foreach (var row in content) { var importNo = row.ContainsKey("交易单号") ? row["交易单号"] : string.Empty; if (string.IsNullOrWhiteSpace(importNo)) { logger.LogWarning("跳过无交易单号的记录: {Row}", row); continue; } var existingRecord = await transactionRecordRepository.ExistsByImportNoAsync(importNo, "微信"); if (existingRecord != null) { existingRecord.Reason = GetReason(row); existingRecord.Amount = GetAmountValue(row, "金额(元)"); existingRecord.OccurredAt = GetDateTimeValue(row, "交易时间"); existingRecord.Type = GetTransactionType(row, "收/支"); existingRecord.RefundAmount = GetRefundAmountValue(row); updateTransactionRecords.Add(existingRecord); continue; } var transactionRecord = new TransactionRecord { Reason = GetReason(row), Amount = GetAmountValue(row, "金额(元)"), RefundAmount = GetRefundAmountValue(row), Balance = 0, OccurredAt = GetDateTimeValue(row, "交易时间"), Type = GetTransactionType(row, "收/支"), ImportNo = importNo, ImportFrom = "微信" }; addTransactionRecords.Add(transactionRecord); } if (addTransactionRecords.Count == 0 && updateTransactionRecords.Count == 0) { logger.LogWarning("未找到可导入或更新的微信交易记录"); return (false, "未找到可导入或更新的微信交易记录"); } var message = new StringBuilder(); if (addTransactionRecords.Count > 0) { if (await transactionRecordRepository.AddRangeAsync(addTransactionRecords)) { logger.LogInformation("成功导入微信交易记录数: {Count}", addTransactionRecords.Count); message.AppendLine($"成功导入微信交易记录数: {addTransactionRecords.Count}"); } } if (updateTransactionRecords.Count > 0) { if (await transactionRecordRepository.UpdateRangeAsync(updateTransactionRecords)) { logger.LogInformation("成功更新微信交易记录数: {Count}", updateTransactionRecords.Count); message.AppendLine($"成功更新微信交易记录数: {updateTransactionRecords.Count}"); } } return (true, message.ToString()); string GetReason(IDictionary row) { var reason = string.Empty; if (row.ContainsKey("交易类型") && !string.IsNullOrWhiteSpace(row["交易类型"])) { reason += row["交易类型"]; } if (row.ContainsKey("交易对方") && !string.IsNullOrWhiteSpace(row["交易对方"])) { reason += row["交易对方"]; } if (row.ContainsKey("商品") && !string.IsNullOrWhiteSpace(row["商品"])) { reason += row["商品"]; } return reason; } decimal GetAmountValue(IDictionary row, string key) { if (row.ContainsKey(key) && decimal.TryParse(row[key].TrimStart('¥').TrimStart('¥'), out var value)) { return value; } return 0m; } DateTime GetDateTimeValue(IDictionary row, string key) { if(!row.ContainsKey(key)) { return DateTime.MinValue; } foreach (var format in DateTimeFormats) { if (DateTime.TryParseExact( row[key], format, CultureInfo.InvariantCulture, DateTimeStyles.None, out var value)) { return value; } } if (DateTime.TryParse(row[key], out var value2)) { return value2; } return DateTime.MinValue; } TransactionType GetTransactionType(IDictionary row, string key) { if (!row.ContainsKey(key)) { return TransactionType.None; } var typeStr = row[key]; return typeStr switch { "支出" => TransactionType.Expense, "收入" => TransactionType.Income, _ => TransactionType.None }; } decimal GetRefundAmountValue(IDictionary row) { if (!row.ContainsKey("当前状态")) { return 0m; } var status = row["当前状态"]; if (!status.Contains("退款")) { return 0m; } // 使用正则表达式提取退款金额 var regex = new System.Text.RegularExpressions.Regex(@"¥(-?\d+(\.\d+)?)"); var match = regex.Match(status); if (match.Success && decimal.TryParse(match.Groups[1].Value, out var refundAmount)) { return refundAmount; } return 0m; } } private async Task[]> ParseAsync(MemoryStream file, string fileExtension) { if (fileExtension == ".csv") { return await ParseCsvAsync(file); } else if (fileExtension == ".xlsx" || fileExtension == ".xls") { return await ParseExcelAsync(file); } else { throw new NotSupportedException("不支持的文件格式"); } } private async Task[]> ParseCsvAsync(MemoryStream file) { file.Position = 0; using var reader = new StreamReader(file, Encoding.UTF8); var config = new CsvConfiguration(CultureInfo.InvariantCulture) { HasHeaderRecord = true, TrimOptions = TrimOptions.Trim }; using var csv = new CsvReader(reader, config); // 读取表头 await csv.ReadAsync(); csv.ReadHeader(); var headers = csv.HeaderRecord; if (headers == null || headers.Length == 0) { return Array.Empty>(); } var result = new List>(); // 读取数据行 while (await csv.ReadAsync()) { var row = new Dictionary(); foreach (var header in headers) { row[header] = csv.GetField(header) ?? string.Empty; } result.Add(row); } return result.ToArray(); } private async Task[]> ParseExcelAsync(MemoryStream file) { file.Position = 0; // 设置 EPPlus 许可证上下文 ExcelPackage.LicenseContext = LicenseContext.NonCommercial; using var package = new ExcelPackage(file); var worksheet = package.Workbook.Worksheets.FirstOrDefault(); if (worksheet == null || worksheet.Dimension == null) { return Array.Empty>(); } var rowCount = worksheet.Dimension.End.Row; var colCount = worksheet.Dimension.End.Column; if (rowCount < 2) { return Array.Empty>(); } // 读取表头(第一行) var headers = new List(); for (int col = 1; col <= colCount; col++) { var header = worksheet.Cells[1, col].Text?.Trim() ?? string.Empty; headers.Add(header); } var result = new List>(); // 读取数据行(从第二行开始) for (int row = 2; row <= rowCount; row++) { var rowData = new Dictionary(); for (int col = 1; col <= colCount; col++) { var header = headers[col - 1]; var value = worksheet.Cells[row, col].Text?.Trim() ?? string.Empty; rowData[header] = value; } result.Add(rowData); } return await Task.FromResult(result.ToArray()); } private static string[] DateTimeFormats = [ "yyyy-MM-dd", "yyyy-MM-dd HH", "yyyy-MM-dd HH:mm", "yyyy-MM-dd HH:mm:ss", "yyyy-M-d", "yyyy-M-d HH:mm", "yyyy-M-d HH:mm:ss", "yyyy/MM/dd", "yyyy/MM/dd HH", "yyyy/MM/dd HH:mm", "yyyy/MM/dd HH:mm:ss", "yyyy/M/d", "yyyy/M/d HH:mm", "yyyy/M/d HH:mm:ss", "MM/dd/yyyy", "MM/dd/yyyy HH", "MM/dd/yyyy HH:mm", "MM/dd/yyyy HH:mm:ss", "M/d/yyyy", "M/d/yyyy HH:mm", "M/d/yyyy HH:mm:ss", "MM/dd/yy", "M/d/yy H:mm", "MM/dd/yy HH", "MM/dd/yy HH:mm", "MM/dd/yy HH:mm:ss", "M/d/yy", "M/d/yy HH", "M/d/yy HH:mm", "M/d/yy HH:mm:ss", "yyyyMMdd", "yyyyMMddHH", "yyyyMMddHHmm", "yyyyMMddHHmmss", ]; }