You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何避免未集成(Integration=False)的支付数据重复发送至SAP及验证逻辑位置确认

解决支付数据重复发送至SAP的问题

这是典型的异步并发场景下的状态竞态问题,核心原因在于定时器的高频触发+异步操作的时序差,导致同一条Integration=False的数据被多次读取并发送。下面我会拆解问题根源,给出具体的验证逻辑实现,以及明确每个环节的部署位置:

问题根源分析

  1. 定时器与异步操作的冲突:定时器每5秒触发一次,但发送SAP的异步操作可能还未完成(未更新数据库Integration状态),下一次定时器回调就已启动,此时DataGridView中的数据仍显示Integration=False,导致重复处理。
  2. 依赖UI数据不可靠:仅通过DataGridView中的Integration值判断是否发送,而UI数据和数据库实时状态存在延迟(比如ListarEmple()刷新UI不及时)。
  3. 无原子性状态校验:没有在数据库层面做「校验+更新」的原子操作,可能多个线程同时读取到Integration=0,进而重复发送。

解决方案:分层验证+原子锁机制

1. 核心:数据库层面添加原子性状态锁定(必须部署)

这是避免重复发送的关键,我们需要一个存储过程,只有当数据当前Integration=0时,才将其改为1,且这个操作是原子的,确保同一时间只有一个线程能锁定这条数据。

创建存储过程:

CREATE PROCEDURE [dbo].[SP_TryLockPayment]
    @DocEntry INT,
    @IsLocked BIT OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    -- 原子更新:仅当Integration为0时,才修改为1
    UPDATE PaymentsReceived
    SET Integration = 1
    WHERE DocEntry = @DocEntry AND Integration = 0;
    
    -- 返回是否锁定成功:1=成功锁定,0=已被其他线程处理
    SET @IsLocked = CASE WHEN @@ROWCOUNT > 0 THEN 1 ELSE 0 END;
END

2. 业务逻辑层:发送前先执行锁定校验(部署在EnviarSAP方法)

在调用SAP接口之前,先调用上述存储过程尝试锁定数据,只有锁定成功的请求才继续发送;如果锁定失败,直接跳过(说明已被其他线程处理)。

修改后的EnviarSAP方法:

Consultas c = new Consultas();
public string XmlSerializado = null;

public async Task<string> EnviarSAP(PagosRecibidos detalle)
{
    bool isLocked = false;
    try
    {
        // 第一步:尝试锁定数据
        using (var conn = new SqlConnection("你的数据库连接字符串"))
        {
            await conn.OpenAsync();
            using (var cmd = new SqlCommand("SP_TryLockPayment", conn))
            {
                cmd.CommandType = CommandType.StoredProcedure;
                cmd.Parameters.Add("@DocEntry", SqlDbType.Int).Value = detalle.DocEntry;
                var lockedParam = cmd.Parameters.Add("@IsLocked", SqlDbType.Bit);
                lockedParam.Direction = ParameterDirection.Output;
                
                await cmd.ExecuteNonQueryAsync();
                isLocked = Convert.ToBoolean(lockedParam.Value);
                
                if (!isLocked)
                {
                    return "数据已被其他进程处理,跳过发送";
                }
            }
        }

        // 第二步:锁定成功,调用SAP接口
        using (WSSincronizacionClient clienteSAP = new WSSincronizacionClient())
        {
            XmlSerializado = this.SerializarXml(detalle);
            var respuesta = await clienteSAP.EnviarDatosSAPAsync(XmlSerializado);
            
            // 发送失败时回滚状态,允许后续重试
            if (!respuesta.Contains("true|Operación Exitosa|"))
            {
                using (var conn = new SqlConnection("你的数据库连接字符串"))
                {
                    await conn.OpenAsync();
                    using (var cmd = new SqlCommand("UPDATE PaymentsReceived SET Integration = 0 WHERE DocEntry = @DocEntry", conn))
                    {
                        cmd.Parameters.Add("@DocEntry", SqlDbType.Int).Value = detalle.DocEntry;
                        await cmd.ExecuteNonQueryAsync();
                    }
                }
            }
            
            c.InsertarLog(1, DateTime.Now, respuesta, XmlSerializado);
            return respuesta;
        }
    }
    catch (Exception ex)
    {
        // 异常时回滚状态
        if (isLocked)
        {
            try
            {
                using (var conn = new SqlConnection("你的数据库连接字符串"))
                {
                    await conn.OpenAsync();
                    using (var cmd = new SqlCommand("UPDATE PaymentsReceived SET Integration = 0 WHERE DocEntry = @DocEntry", conn))
                    {
                        cmd.Parameters.Add("@DocEntry", SqlDbType.Int).Value = detalle.DocEntry;
                        await cmd.ExecuteNonQueryAsync();
                    }
                }
            }
            catch { } // 忽略回滚异常,避免覆盖原错误信息
        }
        throw ex;
    }
}

3. 定时器回调层:优化数据获取与触发逻辑(部署在Envio_Timer_Tick)

  • 定时器触发时先禁用自身,避免上一次任务未完成时再次触发;
  • 直接从数据库拉取最新的Integration=0数据,不再依赖DataGridView的缓存;
  • 批量处理异步任务,提升效率。

修改后的定时器回调:

private async void Envio_Timer_Tick(object sender, EventArgs e)
{
    // 先禁用定时器,防止并发触发
    Envio_Timer.Enabled = false;
    try
    {
        ProxyBL proxy = new ProxyBL();
        // 直接从数据库获取待发送数据,避免UI缓存的延迟
        var pendingPayments = GetPendingPaymentsFromDB();
        
        // 批量处理异步任务
        var sendTasks = pendingPayments.Select(pagos => proxy.EnviarPago(pagos)).ToList();
        await Task.WhenAll(sendTasks);
        
        // 刷新UI和日志
        ListarEmple();
        ListarLog();
    }
    catch (Exception ex)
    {
        MessageBox.Show($"发送任务出错:{ex.Message}");
    }
    finally
    {
        // 任务完成后重新启用定时器
        Envio_Timer.Enabled = true;
    }
}

// 新增从数据库获取待发送数据的方法
private List<PagosRecibidos> GetPendingPaymentsFromDB()
{
    var paymentsList = new List<PagosRecibidos>();
    using (var conn = new SqlConnection("你的数据库连接字符串"))
    {
        conn.Open();
        var query = @"SELECT 'CE-'+CardCode AS CardCode,DocType,Series,DocDate,
                      dbo.udf_GetNumeric(DocNum) AS DocNum, DocEntry,TrsfrAcct,TrsfrDate,
                      TrsfrSum, Integration,Comments,SumApplied 
                      FROM PaymentsReceived WHERE Integration = 0";
        using (var cmd = new SqlCommand(query, conn))
        {
            using (var reader = cmd.ExecuteReader())
            {
                while (reader.Read())
                {
                    paymentsList.Add(new PagosRecibidos
                    {
                        CardCode = reader["CardCode"].ToString(),
                        DocType = reader["DocType"].ToString(),
                        Series = Convert.ToInt32(reader["Series"]),
                        DocDate = Convert.ToDateTime(reader["DocDate"]),
                        DocEntry = Convert.ToInt32(reader["DocEntry"]),
                        TrsfrAcct = reader["TrsfrAcct"].ToString(),
                        TrsfrDate = Convert.ToDateTime(reader["TrsfrDate"]),
                        TrsfrSum = Convert.ToDecimal(reader["TrsfrSum"]),
                        Integration = Convert.ToBoolean(reader["Integration"]),
                        Comments = reader["Comments"].ToString(),
                        SumApplied = Convert.ToDecimal(reader["SumApplied"])
                    });
                }
            }
        }
    }
    return paymentsList;
}

验证逻辑的部署环节总结

  1. 数据库层:原子性锁定存储过程是核心,确保数据状态的唯一性和一致性;
  2. 业务逻辑层(EnviarSAP):发送前执行锁定校验,过滤已被处理的数据;
  3. 定时器回调层:优化数据来源和触发机制,避免并发冲突和UI缓存依赖。

内容的提问来源于stack exchange,提问作者Esteban Giraldo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.29 12:22:30