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 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);
}
if (fileExtension == ".xlsx" || fileExtension == ".xls")
{
return await ParseExcelAsync(file);
}
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 [];
}
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 [];
}
var rowCount = worksheet.Dimension.End.Row;
var colCount = worksheet.Dimension.End.Column;
if (rowCount < 2)
{
return [];
}
// 读取表头(第一行)
var headers = new List();
for (var col = 1; col <= colCount; col++)
{
var header = worksheet.Cells[1, col].Text?.Trim() ?? string.Empty;
headers.Add(header);
}
var result = new List>();
// 读取数据行(从第二行开始)
for (var row = 2; row <= rowCount; row++)
{
var rowData = new Dictionary();
for (var 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",
];
}