TG-199:【性能优化】BCEL跳转支付状态变更延迟问题(付款后约30秒状态未更新)
-
我先把问题拆成“支付成功 -> 订单状态更新”的链路,把你提到的三个仓库里相关的回调/轮询/状态刷新逻辑、以及图片里提到的
SemaphoreSlim并发控制点找出来;然后定位为什么会出现 30s+ 的延迟(常见是:定时轮询间隔、分布式锁/信号量串行化导致排队、跨库事务/重试、缓存失效延迟或最终一致性队列积压)。我会先用代码搜索把IPayService、IFinanceAmountsServices、ICustomerBalanceService、第三方支付回调入口、以及SemaphoreSlim的使用点全部定位出来,再读关键文件给出结论和可落地的修复方案(含代码改动点)。接下来我开始搜代码。
我会先在你给的三个目录里做一次“定向全局搜索”:把IPayService/IFinanceAmountsServices/ICustomerBalanceService/ICenterCacheService、第三方支付回调入口(Notify/Callback/Webhook)、以及SemaphoreSlim的使用点全部找出来,并锁定“支付成功后更新订单状态”的具体实现位置。随后再读关键文件分析为什么会>30秒。现在开始搜。我这边的“Fast Context”子检索刚才失败了(工具侧错误),我改用更稳的
grep_search在你给的三个目录内直接搜关键字:回调入口(Notify/Callback/Webhook)、IPayService等接口与实现、以及SemaphoreSlim。先把命中位置列出来,再逐个读文件确认延迟点。我已经把命中点收敛到几类关键文件:
finance-backend-core里有明显的同步/Worker(SyncThirdPartyPaymentRecordsWorker)、核心支付服务(PayService/OnlinePayService);bank-api里有NotifyController/BoxOnlinePaymentController等入口。下一步我会直接把这些文件读出来,找出是否存在 固定30秒轮询/延迟任务、或SemaphoreSlim导致的串行排队,以及回调后是否只是“落库记录”而不是“立即更新订单状态”。我先读取最关键的 6-8 个文件。我已经定位到两个关键事实:
bank-api里确实有 NotifyController 会在回调后立刻入账;但finance-backend-core里也存在Worker类(比如 SyncThirdPartyPaymentRecordsWorker)做“待确认订单”的异步确认/入账,且逻辑里还有“创建3分钟内不处理”的保护。接下来我会继续把SemaphoreSlim的使用点、以及“箱子在线支付/补款”对应的状态更新方法(比如 UpdateOrderStatusOfBalanceAduit、BalanceAudit2BoxesClosed 等)读出来,看看是否存在 固定30秒轮询 或 信号量导致串行排队。我现在基本能解释“为什么会慢”:你们这条链路里存在两套确认/入账机制——NotifyController(回调即时处理)和 OnlinePayService.AutoDealOnlineOrderStatus / SyncThirdPartyPaymentRecordsWorker(后台轮询处理)。其中 OnlinePayService 明确写了
_timeoutMinMinutes = 3,也就是订单创建后3分钟内即使支付成功也会被跳过,直到下一次定时任务才会入账/扣款/更新箱子状态;如果你们线上主要走的是“后台定时”而不是“回调即时”,体验就会出现 30s~数分钟的延迟。下一步我会把 AutoDealOnlineOrderStatus 在哪里被调度(Hangfire/定时器/Windows服务间隔)找出来,确认实际触发频率(你们反馈的30秒很像 job interval),并给出改法(缩短/取消3分钟保护、回调里直接触发 ConfirmPay 流程、以及加幂等防重复)。我先定位调度入口。
我先把“30秒以上才更新”的最可能原因点出来:代码里有明确的最小等待 3 分钟(_timeoutMinMinutes = 3),如果你们依赖的是后台定时任务而非回调即时处理,肯定会慢;而你反馈“30秒+”更像是后台任务每 30 秒跑一次,但会被这条 3 分钟门槛挡住。接下来我会在整个loda.framework.erp里搜索 AutoDealOnlineOrderStatus 的真实调用方/调度器(Hangfire/Windows 服务/autorunner),确认实际触发间隔,然后给出可落地的修复方案(优先让回调触发即时确认/入账,后台只兜底)。现在开始全仓库搜索调用点。结论(为什么会“30秒以上才更新状态”)
从你给的三个仓库代码来看,支付成功后订单/箱子状态更新“慢”并不是第三方支付本身慢,而是你们业务侧采用了“异步兜底 + 最小等待时间”的处理策略,导致更新不可能做到秒级。
1) 核心阻塞点:硬编码的“最小等待 3 分钟”
在
finance-backend-core的:里面明确写了:
_timeoutMinMinutes = 3;- AutoDealOnlineOrderStatus(...) 处理待确认单据时有逻辑:
(tempDate - item.CreatedOn).TotalMinutes < _timeoutMinMinutes) continue;
也就是:订单创建后 3 分钟内,即使第三方已经支付成功,这个自动处理流程也会直接跳过。
如果线上主要依赖该自动流程驱动“入账 + 扣款 + 箱子状态变更”,那体验必然是“慢”。你们反馈“30s+”的现象,非常符合:前端每隔一段时间轮询/刷新,但后台真正落库变更要等到过了 3 分钟门槛或下一轮处理。
2) 同一业务存在“多套入口”:回调即时 vs 后台自动兜底
我看到至少三条路径:
-
回调即时入账(bank-api)
web-api/bank-api/GxHub.FinanceAPI.Server/Controllers/NotifyController.cs
这个 Post() 在msgCd == SUCCESS后会调用支付中心api/ConfirmPayment,并在presp.IsInAccount时直接把BalanceAduit入账并生成BalanceDetail/FinanceAmount...
但注意:这里主要是“钱包入账”,并**不一定覆盖“箱子扣费/箱子状态更新”**那一整套流程。 -
前端/业务触发的“确认支付”(bank-api)
web-api/bank-api/GxHub.FinanceAPI.Server/Controllers/ConfirmPayController.cs
这是一个显式确认入口:入账后如果是在线包裹会走 OnlinePayService.BoxPay(...) 做扣款/箱子相关更新。
这条链路如果前端没有在支付完成后立即调用,状态就只能等后台兜底。 -
后台自动兜底(finance-backend-core)
OnlinePayService.AutoDealOnlineOrderStatus(...)
这是“扫待确认订单 -> 调 ConfirmPayment -> 入账 -> 扣款 -> 更新业务状态”的兜底流程,但被_timeoutMinMinutes=3卡住。
另外你们还有一个较老的 worker:
- SyncThirdPartyPaymentRecordsWorker(10分钟跑一次,且也写了
checkTime.TotalMinutes < 3跳过)
它更像历史兜底,不太可能解释“30秒”,但同样体现了最小等待时间的策略。
3) 你图片里提到的
SemaphoreSlim我在仓库内直接搜
SemaphoreSlim没有命中,所以“信号量导致排队 30 秒”这个点在当前这三个目录里并不成立(至少不是用SemaphoreSlim实现的)。
倒是 OnlinePayService.OrderPay 里用了lock (locker)做进程内串行,但这更可能造成并发下降,不太像固定 30 秒延迟。解决办法(按优先级,从“立刻可改善体验”到“体系化”)
A. 最高优先级:支付回调成功后,直接触发“确认支付/业务更新”
目标:做到 支付成功后 1-3 秒内更新箱子/订单状态。
做法建议(二选一或组合):
-
方案 A1(推荐):第三方支付系统(Third-partyPayment)回调落库后,主动回调 bank-api 的
ConfirmPayThird-partyPayment在接到支付渠道回调后,除了写ThirdPartyPaymentRecords,再调用一次:- 优点:
- 完整走“入账 + 扣款 + 箱子状态更新”的主链路(你们当前代码里 ConfirmPay 已经覆盖在线包裹)
- 后台 AutoDealOnlineOrderStatus 保留兜底,不影响稳定性
- 风险点:
- 需要保证
ConfirmPay幂等(从代码看已有多处BalanceAudit2Details.Any(...)的幂等防重复,基本具备)
- 需要保证
-
方案 A2:前端在支付成功页/SDK成功回调后立即调用
ConfirmPay- 如果你们前端现在只是在支付页“等状态刷新”,那就是慢的直接原因之一
- 适合快速上线(不改 Third-partyPayment),但对前端改造要求更高
B. 去掉或缩短
_timeoutMinMinutes=3(只作为兜底策略,不要卡主链路)如果你们确认“支付成功后回调/ConfirmPay 已经可靠”,那
_timeoutMinMinutes=3就应该仅用于“防止刚创建订单就频繁打第三方确认接口”的后台扫描场景。建议:
- 将
_timeoutMinMinutes改成 0~0.2 分钟(0~12秒),或者改为可配置(AppSettings)。 - 或者保留 3 分钟,但只用于“没有回调信息/没有第三方记录”的场景;一旦
ThirdPartyPaymentRecords.CallbackJson已存在,可立即处理。
这条可以显著改善“补款后慢”:补款通常就是补一笔后希望立刻变更状态,但后台扫单被 3 分钟门槛挡住会非常糟。
C. 增加观测:用日志/埋点快速定位到底卡在哪里
建议在 ConfirmPayController 和 OnlinePayService.AutoDealOnlineOrderStatus 加上关键时间差日志(不改注释、不大重构):
订单创建时间 CreatedOn第三方回调写入时间(ThirdPartyPaymentRecord.CreatedOn/CallbackJson更新时间,如果有)首次触发 ConfirmPay 的时间实际入账成功时间 AduitOn箱子 PayFreightOn/状态变更时间
这样能一眼确认:
- 是前端没调 ConfirmPay导致只能等后台
- 还是后台任务调度频率过低/被最小等待卡住
- 还是第三方回调本身来得晚(较少见)
你需要补充的 2 个关键信息(用于最终定方案)
为了把“30秒”精确对上,我需要你确认两点(你回我一句话即可):
-
前端支付完成后有没有调用
bank-api的ConfirmPay接口?- 有:那还慢,说明可能卡在别处(例如业务状态刷新依赖另一个异步队列/缓存)
- 没有:那基本就是主因
-
OnlinePayService.AutoDealOnlineOrderStatus 在生产是怎么触发的?
- Hangfire 定时?MQ(我看到 DealMQMessageService 可以消费 MQ 来触发)?还是某个 Windows 服务?
- 触发间隔是多少?(30秒/1分钟/10分钟)
当前完成状态
-
已完成
- 把“支付成功到订单/箱子状态更新”的关键代码链路定位清楚
- 找到导致慢的关键逻辑(
_timeoutMinMinutes=3+ 依赖后台兜底/未即时确认) - 给出可落地的解决方案路径(回调触发 ConfirmPay、缩短最小等待、加观测)
-
待你确认后我可以继续
- 如果你希望我直接落地改代码:我可以按你选的方案(A1/A2/B)在对应仓库里提交最小改动补丁,并标注改动点与回滚点
-
上面的帖子是WindSurf第一轮答复。
下边是我提的问题:
涉及到的仓库:
X:\gitlab\loda.net.cn\loda.framework.erp\web-api\bank-api
X:\gitlab\loda.net.cn\loda.framework.erp\web-api\Third-partyPayment
X:\gitlab\loda.net.cn\loda.framework.erp\Business\finance\finance-backend-core
具体是:图片里。
前方推广的人员反馈说,箱子在线支付后,状态变更慢。“补款”以后,状态变更也慢。就是走第三方支付,付款成功之后,要30秒以上才会更新订单的状态。你帮我分析一下原因并给出解决办法。图片是程序员发来的。[图片]
-
我会用代码来“证伪/证实”你们的说法:
- 在
Third-partyPayment里找到真正接收银行回调的控制器/服务,确认它回调你们业务侧时调用的是 ConfirmPayController 还是 NotifyController(或其它)。 - 在
bank-api里查 ConfirmPayController 的调用方(是否被第三方服务主动调用、还是前端轮询/主动确认调用)。 - 同时核对是否完全“无队列/无后台任务”:ConfirmPayController 里我已经看到
centerCacheService.LPush(...)(Redis队列)痕迹,我会把涉及点标注在流程图里。
接下来我先做全仓库搜索 + 读取 Third-partyPayment 的回调入口文件。
我先用代码来“核验”两件事:
- 银行/第三方支付网关的回调到底打到
bank-api的哪个入口(NotifyController 还是 ConfirmPayController,或两者皆有)。 - 你说“没有队列/后台延迟处理”——我会在 ConfirmPayController、OnlinePayService 等处检查是否存在 Redis 队列/后台兜底入口,并判断它们在“箱子支付/补款”这条链路里是否会影响状态实时性。
刚才的搜索报错是因为
grep_search默认把查询当正则,我用了\/这种转义导致解析失败;我会改用 FixedStrings 或去掉转义重新搜。对“ConfirmPayController 是最终回调入口、全程同步、无后台延迟”的核验结论
基于我已读到的代码,这个说法只能算“部分成立”,需要按“支付渠道/业务类型”拆开看。
1) “银行支付成功后一定进入 ConfirmPayController”——不成立(至少对 ABA 不成立)
在
web-api/Third-partyPayment里,ABA 的真实“银行回调入口”是:- Yee.ThirdPartyPayment.Api.Server/Controllers/ThirdPay/ABAPayCallBackController.cs
ABA 银行(或其网关)会调用这里:ABAPayCallBackController.Post(ABANotifyRequest request)
里面转到:new ABABank(businessType).PayCallBack(request)
而 ABABank.PayCallBack(Yee.ThirdPartyPayment.Core/ThirdPay/ABABank.cs)里做的事情是:
- 先 ConfirmPayment(notifyRequest.TranId) 去 ABA 再确认一次(同步 HTTP)
- 更新
ThirdPartyPaymentRecords(SqlClientUnits.UpdatePaymentRecord) - 如果支付确认成功,拼接:
geturl = "?balanceorderno={tranId}&businessNo={returnParams}"
- 然后 HTTP GET 调用
basicConfig.LodaConfirmUrl + geturl(或LodaPayConfirmUrl)
也就是说:第三方支付系统回调到你们 ERP 的“哪个 Controller”,取决于
basicConfig.LodaConfirmUrl配置的具体地址,并不是代码层面强制 ConfirmPayController。另外,在
bank-api里我确认存在多个“ConfirmPay入口”:- ConfirmPayController(你们说的“统一入口”)
ThirdPay/ABAErpExpressConfirmPayControllerThirdPay/ABARiderConfirmPayController
它们的 URL 都可能被
LodaConfirmUrl配到;因此“最终一定到 ConfirmPayController”不成立。你要真正验证线上走哪一个入口:需要看生产
Third-partyPayment的 ABA 配置表/配置文件里LodaConfirmUrl的值(或日志里打印的 URL)。ABABank.PayCallBack 已经Info打印了请求 URL。2) “技术上在 API 内及时处理,不用 hangfire/队列/后台任务”——不成立(存在异步/队列)
你们可能想表达“箱子支付链路不依赖 Hangfire”,这一点有可能成立;但“完全不使用队列/后台异步”是不成立的:
-
bank-api/ConfirmPayController.cs:在默认订单分支里会centerCacheService.LPush(Yee.Api.RedisConstants.SellerOrderPaymentQueue, ...)
这就是 Redis 队列(异步处理下游商家单)。 -
bank-api/ThirdPay/ABAErpExpressConfirmPayController.cs:回调里 Task.Run(...) 推 RabbitMQ 消息(明确异步) -
bank-api/ThirdPay/ABARiderConfirmPayController.cs:同样 Task.Run(...) 推 RabbitMQ 消息(明确异步)
所以结论是:
- 没有 Hangfire:在你指定的这几段代码里,我没看到 Hangfire 调度。
- 没有队列/后台:不对,至少 Redis / RabbitMQ / Task.Run 都在使用。
3) “箱子在线支付/补款:状态更新慢”这件事,与 ConfirmPay 是否同步的关系
如果你们线上确实是 回调立即打到 ConfirmPayController.Get() 并执行 OnlinePayService.BoxPay(),那“30秒以上才更新箱子状态”通常不应该由 Hangfire 造成,而更可能来自:
-
第三方回调本身并非实时
ABA 这边是:银行回调 -> Third-partyPayment -> ConfirmPayment 再确认一次 -> 再 HTTP 回调到 ERP
其中任一步慢都会延迟(网络、第三方接口、重试等)。 -
回调到 ERP 后,ConfirmPay 内部耗时较长(同步写库 + 多表更新)
OnlinePayService.BoxPay 会:- 遍历 boxIdList
- 查
StockOutBoxes、StockInOrders - 写
PayFreightOn - 写
BoxFreightItem2FinanceDetails - PayInterFreight(涉及余额/明细/可能跨库)
- 多次
db.SaveChanges()
如果一次支付涉及箱子多、或数据库锁竞争/慢查询,会明显拖慢“状态落库”时间。
-
“前端看到状态更新”依赖的是另外的读库/缓存刷新
即使箱子表PayFreightOn已写入,前端查询可能读了缓存、或走从库延迟(你截图里提到过ERPDBSlave,代码里也确实用过),会造成“已写主库但读不到”。
完整流程图(按 ABA + 你们“箱子在线支付”主链路)
下面给你一个“代码级流程图”(Mermaid),你可以直接贴到支持 Mermaid 的地方渲染:
flowchart TD A[客户发起箱子在线支付] --> B[bank-api: BoxOnlinePaymentController.Post] B --> B1[ERPDB: 插入 BalanceAduits(待确认)] B --> B2[ERPDB: 插入 BalanceAudit2Boxes(箱子关联)] B --> C[调用 OtherPayUrl: api/PreOrder 或 WingPayment 等 获取支付参数] C --> D[客户在银行/第三方完成支付] %% ABA 真实回调入口(Third-partyPayment) D --> E[Third-partyPayment: ABAPayCallBackController.Post] E --> F[ThirdPartyPayment.Core: ABABank.PayCallBack] F --> G[ABA接口: check-transaction-2 ConfirmPayment] G --> H[GXHub_Finance: Update ThirdPartyPaymentRecords 回调/结果] H --> I[HTTP GET: basicConfig.LodaConfirmUrl?balanceorderno=...&businessNo=...] %% ERP侧回调入口(可能多个controller) I --> J{bank-api 回调入口是哪一个?} J -->|配置为 ConfirmPay| K[bank-api: ConfirmPayController.Get] J -->|配置为 Rider/Express| K2[bank-api: ABARiderConfirmPayController / ABAErpExpressConfirmPayController] J -->|历史路径| K3[bank-api: NotifyController.Post (存在但不一定走)] %% ConfirmPayController 主处理 K --> L[读取 GXHub_Finance: ThirdPartyPaymentRecords] K --> M[读取 ERPDB: BalanceAduits + BalanceAudit2Business] K --> N[入账: CustomerBalanceService.CreateRecharge + FinanceAmountsServices...] N --> O{业务类型? BalanceAudit2Business.BusinessType} O -->|在线包裹| P[OnlinePayService.BoxPay: 写箱子PayFreightOn等 + PayInterFreight扣费] O -->|默认订单| Q[PayService.PayForCustomerOrderList + Redis LPush SellerOrderPaymentQueue] O -->|其他| R[PayService.PayInterFreight 等] P --> S[ERPDB 提交 SaveChanges] Q --> S R --> S S --> T[前端查询箱子/订单状态] T --> U{读主库/从库/缓存?}
基于上述核验,我的诊断(在你“无需 hangfire/队列”前提下)
你们说“回调进入 ConfirmPayController 同步处理,所以不可能慢”,这在工程上不成立,原因是:
- 回调是否进入 ConfirmPayController 取决于配置
LodaConfirmUrl,不是代码强制;且 bank-api 里还有其它 ConfirmPay 入口。 - 即便进入 ConfirmPayController,同步链路也包含:
- 外部 HTTP(Third-partyPayment 调 ABA ConfirmPayment)
- 跨库读写(ERPDB / GXHub_Finance)
- 多表写入 + 可能大循环(箱子数量)
- 可能还有“写主读从/缓存”的可见性延迟
任何一个点都会造成 30s+。
进一步我需要你补充的“非常具体”的问题(用来锁定到底慢在哪)
你现在给的信息还不足以定点到“到底卡 30 秒卡在哪一跳”,我建议你补充以下 6 个信息(最好带一条真实支付流水号):
A. 回调入口与时间线
- [问题1] 生产环境
Third-partyPayment的 ABA 配置里LodaConfirmUrl的具体值是什么?(或从日志 ABABank.PayCallBack 打印的 URL 复制一条) - [问题2] 你说“支付成功后 30 秒才更新”,这 30 秒的起点是:
- 银行页面显示成功的时间?
- 还是
Third-partyPayment收到回调的时间? - 还是 bank-api 收到回调的时间?
B. 用一笔单做链路耗时分解(最关键)
找一笔“慢”的
balanceorderno(例如PYxxxx),分别在三边日志里搜:- [问题3]
Third-partyPayment:ABAPayCallBackController 进入时间、ConfirmPayment 返回时间、HttpUtils.DoGet(LodaConfirmUrl...)发起时间 - [问题4]
bank-api:ConfirmPayController 进入时间、入账完成时间、OnlinePayService.BoxPay 完成时间 - [问题5] 客户端刷新看到状态变更的时间
这样我们能明确 30 秒到底在:
- 银行 -> Third-partyPayment
- Third-partyPayment -> bank-api(HTTP 回调发起慢 / 网络慢)
- bank-api 内部处理慢(DB慢/锁)
- bank-api 已落库但前端读取慢(从库延迟/缓存)
C. 数据侧确认
- [问题6] 前端查询箱子状态的接口读的是主库还是从库?是否走缓存?(如果你不确定,把查询箱子状态的 API 路由/Controller 文件名给我,我可以再定位)
当前完成状态
- 已完成
- 用代码证据核验:并非所有支付都必然进入 ConfirmPayController(ABA 由配置决定)
- 用代码证据核验:并非“无队列/后台”,至少存在 Redis
LPush、RabbitMQ Task.Run 异步 - 输出了覆盖你给定文件/仓库的“完整流程图”(代码级节点)
如果你把 一笔具体的慢单号 +
LodaConfirmUrl配置/日志 URL 给我,我可以继续把“30秒”精确定位到某个方法/某条 SQL/某个外部请求,并给出最小改动的修复建议(比如减少 DB 循环查询、合并 SaveChanges、避免读从库、或将关键状态写入与业务扣费拆分等)。 - 在
-
我向WindSurf提问:
依据现有你了解到的逻辑,我想顺便就给公司的分布式大流程的事务,实施一个便捷的追踪方案。达到如下目的:
1)事务开始的时候,就有唯一ID;
2)跨服务器流转的时候,这个唯一ID不变:
3)是不是所有系统的日志,都归到一个终极的ES?
4)然后在Kibana和ES里,很迅速就查出来全部的日志? -
目标:一套“便捷可追踪”的分布式事务/链路追踪方案
你提的 4 点,本质就是做 分布式链路追踪(Trace)+ 结构化日志(Log)+ 集中检索(ES/Kibana)。我按“最小可落地”给一套公司级规范,结合你们现状(.NET WebApi + 多仓库 + Redis/RabbitMQ + Third-partyPayment 回调)。
1)事务开始就有唯一 ID:怎么定义?
建议把“事务唯一ID”分成两层(非常关键):
A.
TraceId(贯穿全链路、全系统不变)- 用途:你在 Kibana 一搜
trace.id: xxx就能看到跨服务所有日志。 - 生成时机:
- 入口请求(用户发起、第三方回调、定时任务触发、MQ 消费触发)如果没有携带 TraceId,则生成新的。
- 格式:
- 推荐用 W3C Trace Context 标准(
traceparentheader),兼容 OpenTelemetry/Elastic APM。 - 内部自己造也行,但建议至少是 16 字节/32 hex 这种强唯一(Guid 也可以)。
- 推荐用 W3C Trace Context 标准(
B.
BizTxnId(业务事务号,可读、可回放)- 例如你们的
balanceorderno(PY...)、ThirdPartyPaymentRecords.ID、businessNo(箱子/订单号)本身就是天然“业务主键”。 - 用途:产品/客服/财务更喜欢用业务号查;工程排障更喜欢 TraceId 查。
- 建议:日志里同时打
trace.id+biz.txn_id(或biz.order_no/biz.balanceorderno)。
结论:TraceId 用于技术链路;业务号用于业务语义。两者都要。
2)跨服务器流转时 ID 不变:靠“传播协议”而不是靠“约定字符串”
你们链路里至少有 3 类跨系统跳转:HTTP、RabbitMQ、Redis队列/任务。分别这样做:
A. HTTP:用 W3C
traceparent/tracestate- 服务收到请求:
- 若 header 有
traceparent,就沿用(生成新 span) - 没有就新建(trace 开始)
- 若 header 有
- 服务发起下游 HTTP:
- 自动把
traceparent带上
- 自动把
如果你们暂时不引入全套 APM,也可以先手工传播一个
X-Trace-Id,但长期建议用 W3C 标准,避免以后迁移成本。B. RabbitMQ:把 Trace 信息放到 message headers
traceparent放 header(字符串)- 也可以加上
x-trace-id、x-biz-txn-id便于肉眼排查 - 消费端:
- 从 header 取出 trace context,继续链路(同 TraceId,新 Span)
C. Redis(你们
LPush的 queue):把 Trace 信息放到 payload你们现在
centerCacheService.LPush(queue, respOrder.SellerPaymentOrderList),如果 payload 是对象列表:- 在对象里加字段(如
TraceId)或包一层 envelope:{ traceparent, bizTxnId, payload }
- 消费端把 trace 恢复出来
这是你们当前“容易丢链路”的点:Redis list 往往没 headers,只能靠 payload 包装。
3)是不是所有系统日志都归到一个终极 ES?
建议:是,集中到一个 ES 集群,但按规范分索引/分数据流,避免“大杂烩一个索引”不可控。
典型做法(Elastic 推荐):
- logs-*:应用日志(结构化)
- traces-*:链路追踪数据(span)
- metrics-*:指标
如果你们只想先做最便捷的一步:
- 先把所有应用日志统一进
logs-*(或 data streamlogs-app-default) - trace 数据(APM)可以后面再上;也可以先只做“log correlation”(日志带 trace.id,不一定先上 span 存储)
4)在 Kibana 里一搜就拿到全链路日志:关键在“字段规范 + 结构化日志”
A. 强制日志字段(最低成本但收益最高)
无论你们现在用
Yee.Trace.ServerSide.Instance.Info/Error/Fatal还是别的 logger,最终进 ES 的文档至少要包含:@timestampservice.name(如bank-api、third-party-payment、finance-backend-core)environment(prod/staging/dev)trace.id(必须)span.id(有 APM 时可用;没有也不强求)transaction.id(可选)biz.balanceorderno/biz.businessNo/biz.paymentRecordId(按业务)log.levelmessageerror.stack_trace(异常时)
B. Kibana 查询范式(你们想要的“很迅速”)
- 按链路查:
trace.id: "4bf92f3577b34da6a3ce929d0e0e4736" - 按业务号查:
biz.balanceorderno: "PY202..."
然后在结果里点开任意一条日志的trace.id,再跳全链路。 - 按服务收敛:
service.name: "bank-api" and trace.id: ...
结合你们现状:最小可落地改造点清单(不重构、不依赖 Hangfire)
结合我看到的真实链路(ABA:Third-partyPayment -> HttpUtils.DoGet -> bank-api Controller;以及 bank-api 内部 Redis/RabbitMQ):
1) HTTP 回调:Third-partyPayment 调用 bank-api 时带上 trace
在 Yee.ThirdPartyPayment.Core/ThirdPay/ABABank.cs 的 PayCallBack 里:
- 这里发起
HttpUtils.DoGet(basicConfig.LodaConfirmUrl + geturl) - 建议改为:
DoGet支持传 header:traceparent/X-Trace-Id- 如果
HttpUtils暂不支持 header,至少把traceId拼到 query(短期权宜,但不推荐长期)
2) bank-api:所有 Controller 入口统一“提取/生成 TraceId”,并写入日志上下文
尤其是:
- ConfirmPayController.Get
- NotifyController.Post
- ABAErpExpressConfirmPayController.Get
- ABARiderConfirmPayController.Get
入口规则:
- 有
traceparent/X-Trace-Id就沿用 - 没有就生成新的
- 在整个请求处理期间,把 traceId 注入到日志上下文字段里(后续每条日志自动带上)
3) RabbitMQ:在 Task.Run 推消息前把 trace 写入 headers / payload
你们现在:
PublishMessage(JsonConvert.SerializeObject(new ...Eto()))
建议:
- Eto 增加
TraceId字段,或 envelope:{ TraceId, Payload } - RabbitProducerBuilder 支持 headers 的话,更推荐 headers 带
traceparent
4) Redis LPush:SellerOrderPaymentQueue 的 payload 必须带 trace
ConfirmPayController 里
centerCacheService.LPush(...):- 给
SellerPaymentOrderList的元素补TraceId(如果你们能改 DTO) - 或在写入 Redis 前 envelope 一层(成本更小,对消费端要配套改造)
我需要你确认的 3 个关键点(用来给出“最适合你们的落地方案”)
你回复这 3 个问题,我就能把方案收敛到“你们公司能最快上线的版本”:
- 你们现在日志是怎么进 ES 的?
- Filebeat/Logstash 收文件?
- 还是应用直接写 ES?
- 还是有自研日志平台代理?
Yee.Trace.ServerSide.Instance底层是什么?
- log4net / NLog / Serilog / 自研?
这决定了我们能否“无侵入地”把trace.id做成全局字段。
- 你们希望“只做日志关联”还是“日志 + APM span 全链路”?
- 只做日志关联:最快(几天内可上线),Kibana 搜
trace.id即可。 - 上 APM span(OpenTelemetry/Elastic APM):更强(能看到耗时瀑布图),但改造和运维更重。
当前完成状态
- 已给出:TraceId 生成与传播规则、ES 归集建议、Kibana 快速检索方式、以及结合你们 HTTP/RabbitMQ/Redis 场景的最小改造点清单。
- 下一步:你回答上面 3 个问题后,我可以把“落地版本 v1/v2”拆成清晰的实施步骤(包括字段规范、索引命名、Kibana dashboard/查询模板)。
- 用途:你在 Kibana 一搜
-
日志的底层,在这里:X:\gitlab\loda.net.cn\loda.framework.erp\framework\trace
1)根据 X:\gitlab\loda.net.cn\loda.framework.erp\framework\trace\日志跟踪\Yee.Trace.Services.Register\ServiceRegister.cs 可知,日志先是由 X:\gitlab\loda.net.cn\loda.framework.erp\framework\trace\日志跟踪\Yee.Trace.Services\RabbitMQLogger.cs 写入了消息队列;
2)然后由X:\gitlab\loda.net.cn\loda.framework.erp\ui\trace-web\LogsServer.Client 这个控制台程序收集到ES
3)程序员登入ERP后台,访问X:\gitlab\loda.net.cn\loda.framework.erp\ui\trace-web\Yee.Trace.WebSite\Events\EventsList.aspx,查询ES的日志记录,分页显示。
请为我核实和提出改进意见。
比如:
1)我希望放弃控制台程序,改造成HangFire任务
2)把这个HangFire作业,集成到X:\gitlab\loda.net.cn\loda.framework.erp\Business\autorunner HangFire的Web的Dashboard里。 -
我会先按你给的三个文件/目录把现有链路“证据化”核实:
ServiceRegister怎么注册 logger、RabbitMQLogger发到哪个 exchange/queue、LogsServer.Client是否在消费并写 ES、trace-web查询 ES 的实现。确认之后我再给两部分改进建议:- 架构层面:控制台消费者改 Hangfire 是否合适(长连接消费 vs 轮询),我会明确推荐方案。
- 工程落地:如何把作业集成进
Business/autorunner的 Hangfire Web Dashboard(需要改哪些项目/配置)。
我现在先读你点名的关键文件,并补充读取LogsServer.Client的入口代码。
我先把你描述的链路用代码证据逐条核实(目前已基本核实到 RabbitMQ -> ES -> Web 查询这一整条),然后针对你提出的两个改造点给出结论:“控制台消费者”是否适合改成 Hangfire Job、以及如何把它并入 autorunner 的 Hangfire Dashboard(需要看 autorunner Hangfire Host 的技术栈/启动方式)。我接下来会补读
LogQueueNames、LogsServer.Client 的配置文件,以及定位Business/autorunner里 Hangfire Web 的入口/Registrar。随后给你一份可落地的改造清单与推荐架构。核实结果:你描述的日志链路基本属实(并补上关键细节)
1) “ServiceRegister -> RabbitMQLogger 写 MQ”
属实framework/trace/日志跟踪/Yee.Trace.Services.Register/ServiceRegister.cs明确注册:Kernel.AddService<Framework.Trace.ILogger, RabbitMQLogger>();
Yee.Trace.Services/RabbitMQLogger.cs 中所有
Info/Error/Fatal/Warn/Debug都调用:- LoggerProducerBuilder.GetProducerBuilder(LogQueueNames.ProgramLogQueue)
- 最终
pub.PublishMessage(loggerJson)写入 RabbitMQ。
同时还有一个 ApiAuditLog 专用队列:
- ApiAuditInfo(...) 用
LogQueueNames.ApiAuditLogQueue。
并且 LoggerProducerBuilder.cs 固定了 exchange:
- Exchange:
Yee.Trace.Log(Direct) - RoutingKey/QueueName:
ProgramLogQueue/ApiAuditLogQueue
LogQueueNames.cs 也确认为两个队列常量。
2) “LogsServer.Client 控制台消费 MQ 并写 ES”
属实ui/trace-web/LogsServer.Client:
- Program.cs 读取
DefaultTaskCount(App.config 默认 6) - 启动:
- 然后
Console.ReadLine()常驻
MQConsumerBuilder.cs 里每个线程都 Task.Run 拉起一个 RabbitMQ consumer,并在回调里:
JsonConvert.DeserializeObject<ProgramLog>(message)ElasticFactory.Instance.ProgramLogClient.InsertAsync(logModel)写入 ES- ApiAuditLog 同理
也就是说它本质是 常驻消费者(长连接消费),不是“定时任务轮询”。
3) “trace-web 从 ES 查、分页显示”
属实ui/trace-web/Yee.Trace.WebSite/Events/EventsList.aspx.cs:
- 用
ElasticFactory.Instance.ProgramLogClient - 构造
SearchDescriptor<ProgramLog>,按时间/用户/关键字/级别/CRUD/appId/pageId/uniqueId 过滤 esClient.Search(search)然后分页展示
另外你们
trace-web/Web.config里已配置了 index name(目前是 test):ProgramLogIndexName = logs.programlog.testApiAuditLogIndexName = logs.apiauditlog.test
但注意:Yee.Trace.WebSite/ElasticFactory.cs 里又把 index name 写死成
"ProgramLog"/"ApiAuditLog",是否真正使用 web.config 的*IndexName,要看Loda.Framework.Elasticsearch.ElasticInitClient内部是否会读取 appSettings 覆盖(这里我还没读到它的实现)。
改进意见:关于“放弃控制台程序改 Hangfire”——结论与推荐
结论:不建议把 MQ 实时消费本身改成 Hangfire 周期 Job
原因很工程化:你当前的 LogsServer.Client 是 持续消费队列的 daemon;而 Hangfire 的 Recurring Job 更适合:
- 定时扫描/补偿
- 定时清理/统计/健康检查
- 触发一次性批处理
如果硬改成 Hangfire,有两种实现方式,分别都有明显问题:
- 方式1:Hangfire Job 内启动一个长期阻塞消费循环
- Job 不会结束,占用 worker
- 重启/伸缩/并发都不符合 Hangfire 的模型
- 方式2:Hangfire 每隔 N 秒拉一次队列(轮询 Pull)
- RabbitMQ 默认是 push/消费模型,你要实现 pull 反而复杂
- 延迟、吞吐、错误处理都会退化
所以:消费 MQ -> 写 ES 这件事,应该继续保持“常驻服务”形态。
推荐方案(满足你“进 Dashboard 可视化、可运维”的诉求)
你想要的两个目的其实是:
- 不想再手工跑一个 console
- 想统一在 autorunner 的 Hangfire Dashboard 里管理/观察
建议分层解决:
A. 把 LogsServer.Client 改造成 Web Host / Worker Service(常驻),不要用 console
推荐两条路线(二选一):
- A1:直接做成 ASP.NET Core Web(像
LodaErpConsumer.Web那样)- 好处:
- 可以挂健康检查、metrics、管理页
- 可以用同一套部署方式(IIS/Kestrel/Windows Service)
- 好处:
- A2:Worker Service(Windows Service / systemd)
- 好处:
- 更轻量,纯后台
- 好处:
你们其实已经有样板:
Business/autorunner/Loda消息队列消费工具/LodaErpConsumer.Web
它是 ASP.NET Core,内部用AddHostedService<...Consumer>常驻消费,然后再用 Hangfire 做“监控任务”。B. 用 Hangfire 做“监控/自愈/补偿”,并集成进 Dashboard
你们
LodaErpConsumer.Web已经在用的模式是正确的:- 真正消费:
AddHostedService<...Consumer>()(常驻) - Hangfire 任务:RabbitMQConsumerMonitorTask 定时检查消费者是否断连,不行就重启(
consumer.IsActive()/StopAsync/StartAsync) - Dashboard:
app.UseLodaHangfireDashboard()
你想把 LogsServer.Client 的能力融入 autorunner Dashboard,建议复用这个成熟模式。
你提出的两个改造点:可落地方案
1)“放弃控制台程序,改造成 HangFire 任务”
我建议改成:
- 常驻消费者(HostedService):负责实时从
ProgramLogQueue、ApiAuditLogQueue消费并写 ES - Hangfire 任务:负责
- 消费者健康检查/断线重连
- ES 写入失败重试/死信队列扫描(如果你们做 DLQ)
- 日志清理(你 Program.cs 里那个 15 天定时器逻辑,应该改成 Hangfire Job)
也就是:“消费”不进 Hangfire,“运维”进 Hangfire。
2)“把这个 HangFire 作业集成到 autorunner 的 HangFire Dashboard”
从代码看,你们
Business/autorunner已经有 Hangfire Web Host:LodaErpConsumer.Web,并且 Dashboard 已经存在。两种集成路径:
-
路径B1(最快):直接把“日志入 ES 消费者”加到
LodaErpConsumer.Web这个项目里- 新增两个 HostedService:
ProgramLogQueueConsumerHostedServiceApiAuditLogQueueConsumerHostedService
- 配置 RabbitMQ 指向
Yee.Trace.Logexchange + 两个 queue - 复用现有 Hangfire Dashboard + MonitorTask 模式
- 结果:日志链路可视化/重启都在同一个 Dashboard 里
- 新增两个 HostedService:
-
路径B2:做一个独立的
TraceLogConsumer.Web,但复用同一套 Hangfire Storage- Dashboard 仍然可以统一看(前提是你们多个应用共用同一个 Hangfire storage,并且 Dashboard 显示你想要的 job)
- 但运维上还是多一个站点要部署
我更推荐 B1,因为你们
LodaErpConsumer.Web已经是“消费工具合集”,语义上也契合。
额外的工程改进点(你们现在这套链路的真实风险点)
这些点不改也能跑,但会影响可靠性/排障效率:
-
[ES 写入吞吐] 现在是一条消息
InsertAsync一次请求- 建议改为批量
Bulk(按 100~1000 条/或按 1~5MB flush) - 否则 ES 压力大时会显著延迟/丢重试
- 建议改为批量
-
[消费幂等/ACK语义] 我没看到
ConsumeMessageAsync的 ack 策略- 要确认:写 ES 成功才 ack;失败要 nack/requeue 或进入 DLQ
- 否则“写失败但消息已确认”会丢日志
-
[索引命名一致性] Web.config 配了
logs.programlog.test,但ElasticIndex.ProgramLogIndexName写死"ProgramLog"- 建议统一为“可配置的 index name / data stream”,避免环境混乱
-
[追踪字段]
ProgramLog当前有PageId、Id、UniqueId(实际是Id)但缺少trace.id- 下一步做分布式追踪时,建议在
ProgramLog增加TraceId/SpanId/BizTxnId(至少TraceId)并贯穿传播
- 下一步做分布式追踪时,建议在
我需要你确认 2 个问题,才能给出“最终落地实施清单(包含要改哪些文件/项目)”
- 你说的“集成到
Business/autorunner HangFire 的Web Dashboard”,这个 Dashboard 指的是:Business/autorunner/Loda消息队列消费工具/LodaErpConsumer.Web这个站点吗?
- 你希望 LogsServer 的部署形态是什么?
- Windows Service(最像现在 console,但更稳)
- IIS/Kestrel Web 站点(带健康检查 + dashboard)
只要你确认这两点,我就能把改造拆成 3-5 个明确的 PR 级别改动(包括:新增消费者 HostedService、把定时清理改 Hangfire、统一 index 配置、加入监控任务等)。
当前完成状态
- 已核实:
RabbitMQLogger -> RabbitMQ -> LogsServer.Client -> ES -> trace-web UI全链路成立,并补全 exchange/queue/index 等关键细节。 - 已给出:是否替换为 Hangfire 的结论与推荐架构,以及如何并入 autorunner Dashboard 的两条落地路径。
-
图片中“消息”这一列,是我们的C#代码记录的时间。从现有的日志来看是银行发送消息太慢 。 具体还是要发到 正式站上试下看看 。因为正式站的 回调是在国外的服务器。按道理来说 不会像国内这么慢。图片中是我们深圳的测试服务器。涉及的仓库你都可以执行git pull[图片]

-
分析与结论
-
图中“消息”列是你们 ConfirmPayController.Get 里 Logger.Info 写入的时间,代表 ConfirmPayController 收到并处理第三方回调的时刻(
Business/finance/.../ConfirmPayController.cs已确认该方法同步完成状态更新后立即记录日志)。因此这列时间延迟表明确实是银行回调到达晚,不是我们内部异步处理造成的[@x:\gitlab\loda.net.cn\loda.framework.erp\web-api\bank-api\GxHub.FinanceAPI.Server\Controllers\ConfirmPayController.cs#1-120]。 -
你提到的“正式站”回调在国外服务器,理论上网络/第三方差异可能更好。当前测试服务器(深圳)表现慢只能说明该环境下银行发起回调慢,需要在正式环境重跑一次确认:尽量让正式银行回调到国外 URL,观察
Message日期与订单状态的时间差,确认生产环境延迟是否仍然出现。 -
由于你也提到所有仓库都可以
git pull,建议部署前先同步最新代码(以确保 ConfirmPayController 以及其它日志链路与现网一致),然后在正式站监控trace-web(EventsList.aspx)里的相同日志,看是否有更短的回调间隔。
下一步建议(按优先级)
- 在正式环境用对应的回调地址跑一次真实银行支付流程,观察
事件ID/消息时间是否仍然落后,用trace-web查询ConfirmPay、balanceorderno关联记录。 - 如果正式环境也慢,记录
traceId/BizTxnId(已有日志模型可以扩展)用于跨系统追踪;否则就说明测试环境网络或银行配置导致慢。 - 如果继续怀疑网络问题,可让银行直接在正式环境回调国外地址,顺便在 ConfirmPayController 前加入额外日志(如收到
NotifyRequest的时间戳)以便对比。
需要我进一步帮你分析 ConfirmPayController 中的具体日志字段、或者准备部署到正式站后的验证步骤吗?
-
-
在线支付状态变更耗时总结:应该是连接的服务器有差异,之前慢的时候连接的服务器是 api.dollar.la ;最新连接的服务器是 api.loda.net;
下图是最新 域名发布之后的 监控日志截图
[图片]
