Commit e883198f by Neo Turing

插入发票接口增加数据库锁防止并发问题

parent 9c01c978
using Kivii.Finances.Entities;
using Kivii.Linq;
using Kivii.Linq.Dapper;
using Kivii.Web;
using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
......@@ -19,11 +21,23 @@ namespace Kivii.Finances.Transforms
public List<Payment> Payments { get; set; }
// 定义批处理大小常量
private const int BATCH_SIZE = 100;
// 定义处理超时时间
private readonly TimeSpan PROCESSING_TIMEOUT = TimeSpan.FromMinutes(2);
public override object OnExecution(IRequest req, IResponse res)
{
(Item == null && Items.IsNullOrEmpty()).ThrowIfTrue("开票结果内容不能为空!");
if (Items.IsNullOrEmpty()) Items = new List<Invoice>();
if (Item != null) Items.Add(Item);
// 性能优化:首先检查数据量,提前预警
if (Items.Count > BATCH_SIZE)
{
KiviiContext.Logger.Warn($"正在处理大批量发票数据: {Items.Count}条,可能需要较长时间");
}
foreach (var item in Items)
{
(item.OperateTime == DateTime.MinValue || item.OperateTime == DateTime.MaxValue).ThrowIfTrue("缺少开票日期信息!");
......@@ -41,6 +55,7 @@ namespace Kivii.Finances.Transforms
(Payments.Exists(o => o.Amount <= 0)).ThrowIfTrue("未指定到账开票金额!");
(Payments.Sum(o => o.Amount) != Items.Sum(o => o.Amount)).ThrowIfTrue("到账开票金额和发票金额不一致!");
}
#region 启用缓存,将当前人创建批次数据存入,处理完毕或者接口报错后清除,确保不会重复创建
var cache = KiviiContext.GetCacheClient();
var cacheKey = KiviiContext.GetUrnKey($"{this.GetType().FullName}_Request_{KiviiContext.CurrentMember.FullName}_{KiviiContext.CurrentMember.Kvid}");
......@@ -55,9 +70,33 @@ namespace Kivii.Finances.Transforms
cache.Set(cacheKey, caches, TimeSpan.FromMinutes(5));
#endregion
// 数据库连接初始化(移到外层以确保异常时能正确关闭)
var conn = KiviiContext.GetOpenedDbConnection<Invoice>();
var trans = null as IDbTransaction;
var lockAcquired = false;
var lockName = $"invoice_lock_{KiviiContext.CurrentMember.Kvid}";
try
{
var conn = KiviiContext.GetOpenedDbConnection<Invoice>();
// 优化2:使用数据库锁防止并发问题
try
{
// 使用数据库锁防止同一用户并发提交
var lockResult = conn.ExecuteScalar<int>("SELECT GET_LOCK(@lockName, 10)",
new { lockName = lockName });
if (lockResult != 1)
{
throw new Exception("系统繁忙,请稍后再试");
}
lockAcquired = true;
}
catch (Exception lockEx)
{
// 如果数据库不支持GET_LOCK,降级到缓存锁
KiviiContext.Logger.Warn($"数据库锁获取失败,降级使用缓存锁: {lockEx.Message}");
}
//var exists = conn.Select<Invoice>(o => Sql.In(o.SerialNumber, Items.ConvertAll(p => p.SerialNumber)));
//(conn.Exists<Invoice>(o => Sql.In(o.SerialNumber, Items.ConvertAll(p => p.SerialNumber)))).ThrowIfTrue("存在重复录入的发票信息");
......@@ -89,6 +128,9 @@ namespace Kivii.Finances.Transforms
}
var category = preCorrelatingPayments.IsNullOrEmpty() ? InvoiceApplyType.Debit : InvoiceApplyType.Payment;
// 创建任务以支持超时处理
var processingTask = Task.Run(() => {
foreach (var info in Items)
{
//重复的发票,如果传了对应到账,那重复的发票就要抛异常,否则跳过
......@@ -164,6 +206,22 @@ namespace Kivii.Finances.Transforms
#endregion
}
return true;
});
// 添加超时处理
var timeoutTask = Task.Delay(PROCESSING_TIMEOUT);
if (Task.WhenAny(processingTask, timeoutTask).Result == timeoutTask)
{
throw new TimeoutException($"处理发票数据超时,请减少批量处理数量(当前{Items.Count}条)");
}
// 检查处理任务是否成功
if (!processingTask.Result)
{
throw new Exception("处理发票数据失败");
}
if (!preCorrelatingPayments.IsNullOrEmpty())
{
var correlatedInvoices = insertInvoices.Correlating(preCorrelatingPayments);
......@@ -173,44 +231,143 @@ namespace Kivii.Finances.Transforms
}
var rtns = new RestfulUpdateResponse<Invoice>();
rtns.Results = new List<Invoice>();
var trans = conn.OpenTransaction();
// 开启事务
trans = conn.OpenTransaction(System.Data.IsolationLevel.ReadCommitted);
try
{
// 性能优化:批量插入发票
if (!insertInvoices.IsNullOrEmpty())
{
foreach (var item in insertInvoices)
// 分批处理数据,避免单次处理过多
for (int i = 0; i < insertInvoices.Count; i += BATCH_SIZE)
{
var batchInvoices = insertInvoices.Skip(i).Take(BATCH_SIZE).ToList();
// 处理批次中的每个发票
foreach (var item in batchInvoices)
{
if (item.Type == "Relation") item.SerialNumber = item.GetSubSerialNumber(conn);
if (item.Type == "Relation")
item.SerialNumber = item.GetSubSerialNumber(conn);
conn.Insert(item);
if (item.Type != "Relation") rtns.Results.Add(item);
if (item.Type != "Relation")
rtns.Results.Add(item);
// 更新缓存计数
caches--;
try {
cache.Replace(cacheKey, caches);
}
catch (Exception cacheEx) {
// 缓存更新失败不影响主流程
KiviiContext.Logger.Warn($"缓存更新失败: {cacheEx.Message}");
}
}
}
}
// 性能优化:批量插入发票明细
if (!insertInvoiceDetails.IsNullOrEmpty())
{
// 如果数据库支持批量插入,使用批量插入
try {
conn.InsertAll(insertInvoiceDetails);
}
catch {
// 降级到循环插入
for (int i = 0; i < insertInvoiceDetails.Count; i += BATCH_SIZE)
{
var batch = insertInvoiceDetails.Skip(i).Take(BATCH_SIZE).ToList();
foreach (var detail in batch)
{
conn.Insert(detail);
}
}
}
if (!insertInvoiceDetails.IsNullOrEmpty()) insertInvoiceDetails.ForEach(o => conn.Insert(o));
}
// 更新关联的付款记录
if (!preCorrelatingPayments.IsNullOrEmpty())
{
preCorrelatingPayments.ForEach(o => conn.UpdateOnly(o));
for (int i = 0; i < preCorrelatingPayments.Count; i += BATCH_SIZE)
{
var batch = preCorrelatingPayments.Skip(i).Take(BATCH_SIZE).ToList();
foreach (var payment in batch)
{
conn.UpdateOnly(payment);
}
}
}
// 更新父级的付款记录
if (!parentPayments.IsNullOrEmpty())
{
parentPayments.ForEach(o => o.RecalculateAmountInvoice(conn));
foreach (var payment in parentPayments)
{
payment.RecalculateAmountInvoice(conn);
}
}
trans.Commit();
// 清理缓存
try {
cache.Remove(cacheKey);
}
catch (Exception cacheEx) {
// 缓存清理失败仅记录警告
KiviiContext.Logger.Warn($"缓存清理失败: {cacheEx.Message}");
}
return rtns;
}
catch (Exception ex)
{
// 事务回滚
if (trans != null)
{
try {
trans.Rollback();
}
catch (Exception rollbackEx) {
KiviiContext.Logger.Error($"事务回滚失败: {rollbackEx.Message}");
}
}
// 清理缓存
try {
cache.Remove(cacheKey);
throw ex;
}
catch { /* 忽略缓存清理错误 */ }
// 记录错误并重新抛出异常,保留原始堆栈
KiviiContext.Logger.Error($"发票处理失败: {ex.Message}", ex);
throw; // 使用throw而不是throw ex,保留原始堆栈
}
}
catch (Exception ex)
{
// 确保缓存被清理
try {
cache.Remove(cacheKey);
throw ex;
}
catch { /* 忽略缓存清理错误 */ }
// 确保数据库锁被释放
if (lockAcquired)
{
try {
conn.ExecuteScalar<int>("SELECT RELEASE_LOCK(@lockName)",
new { lockName = lockName });
}
catch { /* 忽略锁释放错误 */ }
}
// 记录错误日志并重新抛出异常
KiviiContext.Logger.Error($"发票接收处理失败: {ex.Message}", ex);
throw; // 使用throw而不是throw ex,保留原始堆栈
}
}
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment