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

C#如何避免多层循环重构批量提交订单至外部API的代码

订单批量提交外部API重构方案

现有实现的问题

  • 三层嵌套循环可读性差,分组逻辑和执行逻辑耦合在一起,后续调整规则成本高
  • 对订单集合做了多次重复遍历:先筛配送时段、再筛门店,数据量大时存在不必要的性能损耗,且硬编码配送时段枚举,后续新增时段需要修改代码
  • 同步串行调用接口,批次多时总耗时为所有接口调用耗时之和,没有利用IO等待的空闲时间
  • 无并发控制、异常处理、重试等生产环境必要逻辑,稳定性不足

重构核心思路

1. 分组逻辑简化

直接用LINQ的复合键分组,一次遍历就把所有「同门店+同配送时段」的订单归为一组,完全替代两层循环筛选的逻辑,不会漏数据,性能更优。

2. 职责拆分

把单批次提交逻辑抽成独立方法,和分组、分批逻辑解耦,后续调整接口参数、加测试、改重试逻辑都只需要改单个方法。

3. 受控异步并发

用SemaphoreSlim控制接口调用的最大并发数,既通过异步IO大幅缩短总处理耗时,又避免并发过高触发外部API的限流规则。

4. 预留容错扩展点

在提交方法里预留异常捕获、日志、取消令牌的位置,方便后续接入重试、熔断、失败补偿逻辑。

重构后实现代码

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MoreLinq;

class Program
{
    // 可迁移到配置文件管理
    private const int BatchSize = 5;
    private const int MaxConcurrentApiCalls = 5; // 根据外部API限流阈值调整
    private static readonly SemaphoreSlim _apiConcurrencyLimiter = new SemaphoreSlim(MaxConcurrentApiCalls);

    static async Task Main(string[] args)
    {
        var allOrders = GetProductOrders();
        
        // 单次遍历完成 配送时段+门店 维度分组
        var orderGroups = allOrders
            .GroupBy(o => new { o.DeliveryWindow, o.StoreName })
            .ToList();

        var batchSubmitTasks = new List<Task>();
        foreach (var group in orderGroups)
        {
            // 每组按规则拆分为最多5笔的批次
            foreach (var orderBatch in group.Batch(BatchSize))
            {
                var batchOrders = orderBatch.ToList();
                batchSubmitTasks.Add(SubmitOrderBatchAsync(
                    batchOrders, 
                    group.Key.DeliveryWindow, 
                    group.Key.StoreName));
            }
        }

        // 等待所有批次提交完成
        await Task.WhenAll(batchSubmitTasks);
    }

    /// <summary>
    /// 单批次订单提交到外部API
    /// </summary>
    private static async Task SubmitOrderBatchAsync(
        List<ProductOrder> orders, 
        string deliveryWindow, 
        string storeName, 
        CancellationToken ct = default)
    {
        await _apiConcurrencyLimiter.WaitAsync(ct);
        try
        {
            // 此处替换为实际的HttpClient异步POST逻辑
            // 例:await httpClient.PostAsJsonAsync(submitUrl, new { StoreName = storeName, DeliveryWindow = deliveryWindow, Orders = orders }, ct);
            Console.WriteLine($"提交批次:门店[{storeName}],时段[{deliveryWindow}],订单数[{orders.Count}],订单ID:{string.Join(",", orders.Select(o=>o.ID))}");
            await Task.Delay(100, ct); // 模拟接口IO耗时
        }
        catch (Exception ex)
        {
            // 记录错误日志,需包含门店、时段、订单ID、异常信息,方便后续补偿
            Console.WriteLine($"批次提交失败:门店[{storeName}],时段[{deliveryWindow}],错误:{ex.Message}");
            // 可在此接入Polly重试策略
        }
        finally
        {
            _apiConcurrencyLimiter.Release();
        }
    }

    public static List<ProductOrder> GetProductOrders()
    {
        return new List<ProductOrder>()
        {
            new ProductOrder(){ ID = 1, DeliveryWindow ="Morning", StoreName = "StoreA", customerDetails = "Cust1"},
            new ProductOrder(){ ID = 2, DeliveryWindow ="Morning", StoreName = "StoreA",customerDetails = "Cust2"},
            new ProductOrder(){ ID = 3, DeliveryWindow ="Morning", StoreName = "StoreA",customerDetails = "Cust3"},
            new ProductOrder(){ ID = 4, DeliveryWindow ="AfterNoon", StoreName = "StoreA",customerDetails = "Cust4"},
            new ProductOrder(){ ID = 5, DeliveryWindow ="AfterNoon", StoreName = "StoreA",customerDetails = "Cust5"},
            new ProductOrder(){ ID = 6, DeliveryWindow ="Morning", StoreName = "StoreB",customerDetails = "Cust6"},
            new ProductOrder(){ ID = 7, DeliveryWindow ="Morning", StoreName = "StoreB",customerDetails = "Cust7"},
            new ProductOrder(){ ID = 8, DeliveryWindow ="AfterNoon", StoreName = "StoreB",customerDetails = "Cust8"},
            new ProductOrder(){ ID = 9, DeliveryWindow ="AfterNoon", StoreName = "StoreB",customerDetails = "Cust9"},
            new ProductOrder(){ ID = 10, DeliveryWindow ="AfterNoon", StoreName = "StoreC",customerDetails = "Cust10"},
        };
    }
}

public class ProductOrder
{
    public int ID { set; get; }
    public string StoreName { set;get;}
    public string DeliveryWindow { set; get; }
    public string customerDetails { set; get; }
    public string ProductDetails { set; get; }
}

进一步优化建议

  • 把批次大小、最大并发数、接口地址、超时时间等参数抽到配置文件,不需要改代码就能适配外部API的规则调整
  • 推荐给接口调用增加重试、熔断、超时策略:比如遇到网络错误、5xx响应时自动重试2-3次,连续失败时暂停提交一段时间,避免雪崩
  • 每个批次提交时记录完整日志:包含门店、配送时段、批次内订单ID列表、请求响应内容、耗时,出问题可以快速定位
  • 如果订单量达到十万级以上,可以采用流式处理,不需要一次性把所有订单加载到内存分组,进一步降低内存占用
  • 字符串比较时建议传入StringComparison.OrdinalIgnoreCase,避免因为配送时段、门店名的大小写差异导致分组错误
  • 失败的批次建议持久化存储对应订单ID,后续走补偿流程重新提交,避免丢单
  • 如果不想引入MoreLinq依赖,可以用以下扩展方法实现同等分批功能:
public static IEnumerable<List<T>> Batch<T>(this IEnumerable<T> source, int batchSize)
{
    var currentBatch = new List<T>(batchSize);
    foreach (var item in source)
    {
        currentBatch.Add(item);
        if (currentBatch.Count == batchSize)
        {
            yield return currentBatch;
            currentBatch = new List<T>(batchSize);
        }
    }
    if (currentBatch.Count > 0) yield return currentBatch;
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 09:36:17