ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

命令模式实战:把“下单后的动作“打包成可执行的任务

命令模式实战:把“下单后的动作“打包成可执行的任务 一句话定位将一个请求封装为一个对象从而可用不同的请求对客户进行参数化对请求排队或记录请求日志以及支持可撤销的操作。一、实战场景用户下单成功后系统要做一连串后置动作发送订单确认短信/邮件发放积分更新会员等级通知仓储系统备货记录运营埋点。如果直接在主流程里逐个调用publicasyncTaskOnOrderCreatedAsync(Orderorder){await_smsService.SendAsync(order.CustomerPhone,下单成功……);await_pointsService.GrantAsync(order.CustomerId,100);await_memberService.UpgradeAsync(order.CustomerId);await_warehouseService.NotifyAsync(order);await_trackingService.TrackAsync(order.created,order);// 每加一个动作这里就多一行失败处理也各不相同……}问题主流程被业务动作塞满订单核心逻辑和后置动作耦合在一起这些动作往往需要异步执行、失败重试、排队削峰直接内联调用做不到想批量重放比如系统恢复后重跑某天的后置动作无从下手每个动作的失败处理策略不同短信失败可忽略、积分发放失败要重试散落在主流程里。命令模式的解法把每个动作封装成一个命令对象实现统一接口主流程只负责创建命令并提交。命令可以被排队、被记录、被重放、被批量执行——动作本身变成了数据。二、设计思路调用方下单服务 │ 创建并提交 ▼ ┌─────────────────────────────┐ │ CommandQueue命令队列 │ │ 排队 → 逐个执行 → 失败重试 │ └──────────────┬──────────────┘ ▼ 执行Invoke ┌─────────────────────────────┐ │ IOrderCommand命令接口 │ │ ├─ SendSmsCommand │ │ ├─ GrantPointsCommand │ │ ├─ UpgradeMemberCommand │ │ ├─ NotifyWarehouseCommand │ │ └─ TrackEventCommand │ └─────────────────────────────┘命令模式的精髓做什么被封装成对象可以存储、传输、排队、重放。三、代码实现.NET 10 / C# 143.1 命令接口与执行上下文namespaceOrders.AfterOrder;publicinterfaceIOrderCommand{stringName{get;}TaskExecuteAsync(OrderContextcontext,CancellationTokenctdefault);TaskUndoAsync(OrderContextcontext,CancellationTokenctdefault);// 可选的补偿}publicsealedrecordOrderContext(OrderSnapshotOrder);publicsealedrecordOrderSnapshot(stringOrderId,stringCustomerId,stringCustomerPhone,decimalAmount);3.2 具体命令们每个动作一个命令namespaceOrders.AfterOrder.Commands;// 命令1发短信publicsealedclassSendSmsCommand(ISmsSendersms):IOrderCommand{publicstringNameSendSms;publicTaskExecuteAsync(OrderContextctx,CancellationTokenctdefault)sms.SendAsync(ctx.Order.CustomerPhone,$您已成功下单{ctx.Order.OrderId}金额{ctx.Order.Amount:C},ct);publicTaskUndoAsync(OrderContextctx,CancellationTokenctdefault)Task.CompletedTask;// 短信无法撤回}// 命令2发积分失败可重试publicsealedclassGrantPointsCommand(IPointsServicepoints):IOrderCommand{publicstringNameGrantPoints;publicTaskExecuteAsync(OrderContextctx,CancellationTokenctdefault)points.GrantAsync(ctx.Order.CustomerId,(int)(ctx.Order.Amount/10),ct);publicTaskUndoAsync(OrderContextctx,CancellationTokenctdefault)points.RecoverAsync(ctx.Order.CustomerId,(int)(ctx.Order.Amount/10),ct);}// 命令3通知仓储publicsealedclassNotifyWarehouseCommand(IWarehouseServicewarehouse):IOrderCommand{publicstringNameNotifyWarehouse;publicTaskExecuteAsync(OrderContextctx,CancellationTokenctdefault)warehouse.NotifyAsync(ctx.Order.OrderId,ct);publicTaskUndoAsync(OrderContextctx,CancellationTokenctdefault)Task.CompletedTask;}// 命令4运营埋点可忽略失败publicsealedclassTrackEventCommand(ITrackingServicetracking):IOrderCommand{publicstringNameTrackEvent;publicTaskExecuteAsync(OrderContextctx,CancellationTokenctdefault)tracking.TrackAsync(order.created,ctx.Order,ct);publicTaskUndoAsync(OrderContextctx,CancellationTokenctdefault)Task.CompletedTask;}3.3 执行器命令队列排队、重试、记录namespaceOrders.AfterOrder;publicsealedclassCommandQueue(ILoggerlogger){privatereadonlyQueueIOrderCommand_commandsnew();publicvoidEnqueue(IOrderCommandcommand)_commands.Enqueue(command);publicasyncTaskIReadOnlyListCommandExecutionResultFlushAsync(CancellationTokenctdefault){varresultsnewListCommandExecutionResult();while(_commands.Count0){varcommand_commands.Dequeue();varswStopwatch.StartNew();try{awaitExecuteWithRetryAsync(command,ct);results.Add(newCommandExecutionResult(command.Name,true,sw.ElapsedMilliseconds,null));}catch(Exceptionex){logger.LogError(ex,命令 {Name} 执行失败,command.Name);results.Add(newCommandExecutionResult(command.Name,false,sw.ElapsedMilliseconds,ex.Message));}}returnresults;}privateasyncTaskExecuteWithRetryAsync(IOrderCommandcommand,CancellationTokenct,intmaxRetries3){for(varattempt1;;attempt){try{awaitcommand.ExecuteAsync(CommandContext.Current,ct);return;}catchwhen(attemptmaxRetries){awaitTask.Delay(TimeSpan.FromSeconds(attempt),ct);// 指数退避的简化版}}}}publicsealedrecordCommandExecutionResult(stringName,boolSuccess,longElapsedMs,string?Error);// 简单模拟全局执行上下文真实项目可用 AsyncLocal / DI ScopepublicstaticclassCommandContext{publicstaticOrderContextCurrent{get;set;}default!;}3.4 主流程下单后把动作打包提交namespaceOrders;publicsealedclassOrderService(CommandQueuequeue,SendSmsCommandsmsCommand,GrantPointsCommandpointsCommand,NotifyWarehouseCommandwarehouseCommand,TrackEventCommandtrackCommand){publicasyncTaskOrderSnapshotCreateOrderAsync(CreateOrderRequestreq,CancellationTokenctdefault){// 1. 核心下单逻辑校验、扣库存、建单……省略varordernewOrderSnapshot(OrderId:$ORD-{Guid.NewGuid():N}[..18],CustomerId:req.CustomerId,CustomerPhone:req.Phone,Amount:req.Amount);// 2. 后置动作全部打包成命令进队列CommandContext.CurrentnewOrderContext(order);queue.Enqueue(smsCommand);queue.Enqueue(pointsCommand);queue.Enqueue(warehouseCommand);queue.Enqueue(trackCommand);// 3. 统一执行真实项目队列可换成 Hangfire / RabbitMQ实现异步削峰varresultsawaitqueue.FlushAsync(ct);returnorderwith{/* 可附加 results 状态 */};}}3.5 组装与运行// Program.cs.NET 10 顶层语句usingOrders;usingOrders.AfterOrder;usingOrders.AfterOrder.Commands;varbuilderWebApplication.CreateBuilder(args);builder.Services.AddLogging();// 注册各种服务与命令builder.Services.AddSingletonISmsSender,FakeSmsSender();builder.Services.AddSingletonIPointsService,FakePointsService();builder.Services.AddSingletonIWarehouseService,FakeWarehouseService();builder.Services.AddSingletonITrackingService,FakeTrackingService();builder.Services.AddSingletonSendSmsCommand();builder.Services.AddSingletonGrantPointsCommand();builder.Services.AddSingletonNotifyWarehouseCommand();builder.Services.AddSingletonTrackEventCommand();builder.Services.AddSingletonCommandQueue();builder.Services.AddScopedOrderService();varappbuilder.Build();app.MapPost(/orders,async(CreateOrderRequestreq,OrderServiceorders){varorderawaitorders.CreateOrderAsync(req);returnResults.Ok(new{order.OrderId,Statuscreated});});app.Run();// 各 Fake 服务的实现演示用真实项目替换为实际客户端publicinterfaceISmsSender{TaskSendAsync(stringphone,stringtext,CancellationTokenct);}publicsealedclassFakeSmsSender:ISmsSender{publicTaskSendAsync(stringphone,stringtext,CancellationTokenct){Console.WriteLine($[短信]{phone}:{text});returnTask.CompletedTask;}}publicinterfaceIPointsService{TaskGrantAsync(stringcustomer,intpoints,CancellationTokenct);TaskRecoverAsync(stringcustomer,intpoints,CancellationTokenct);}publicsealedclassFakePointsService:IPointsService{publicTaskGrantAsync(stringcustomer,intpoints,CancellationTokenct){Console.WriteLine($[积分] 用户{customer}{points});returnTask.CompletedTask;}publicTaskRecoverAsync(stringcustomer,intpoints,CancellationTokenct)Task.CompletedTask;}publicinterfaceIWarehouseService{TaskNotifyAsync(stringorderId,CancellationTokenct);}publicsealedclassFakeWarehouseService:IWarehouseService{publicTaskNotifyAsync(stringorderId,CancellationTokenct){Console.WriteLine($[仓储] 备货通知{orderId});returnTask.CompletedTask;}}publicinterfaceITrackingService{TaskTrackAsync(stringevt,objectpayload,CancellationTokenct);}publicsealedclassFakeTrackingService:ITrackingService{publicTaskTrackAsync(stringevt,objectpayload,CancellationTokenct){Console.WriteLine($[埋点]{evt});returnTask.CompletedTask;}}publicsealedrecordCreateOrderRequest(stringCustomerId,stringPhone,decimalAmount);四、应用要点与注意事项命令模式的三大价值解耦主流程不再知道后置动作的实现、可排队命令是对象能进队列、异步执行、削峰、可重放命令可记录、可批量重跑、可做补偿。与消息队列的关系命令模式是代码层面的封装消息队列是执行层面的载体。两者常组合使用——把命令序列化成消息投递到 RabbitMQ/Kafka消费者反序列化后执行。理解命令模式就理解了任务系统的核心。Undo 是可选能力不是所有命令都能撤销短信发出去撤不回。UndoAsync返回空实现即可但设计命令接口时保留这个钩子未来做补偿/回滚时不用改接口。区分命令 vs 事件命令是明确的意图期望被执行发积分事件是已经发生的事实可以触发副作用订单已创建。下单后置动作既可以用命令模式也可以用观察者模式事件驱动实现——命令模式更强调可排队、可重放事件驱动更强调解耦发布订阅。两者可以并存。每个命令保持单一职责一个命令只做一件事失败重试策略可以在执行器层面统一如示例的ExecuteWithRetryAsync不要让命令内部各自实现重试。五、小结命令模式把动作从代码升级成了数据可排队、可记录、可重放、可补偿。下单后置动作、定时任务、批量操作、撤销重做都是它的经典舞台。配合消息队列、Hangfire 这类执行载体它几乎构成了现代后端异步任务体系的全部雏形。
返回列表