From cfc82231b325fc763fcb706559e442325e6defe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E7=85=9C?= <2082529121@qq.com> Date: Thu, 3 Sep 2026 22:24:03 +0800 Subject: [PATCH] fix: avoid cross-module write lock inversion --- src/service/orders.ts | 44 ++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/src/service/orders.ts b/src/service/orders.ts index 8ba456e..207b314 100644 --- a/src/service/orders.ts +++ b/src/service/orders.ts @@ -1,4 +1,5 @@ import { Provide } from '@midwayjs/core'; +import { createHash } from 'node:crypto'; import { InjectSaasAction, InjectSaasCrud, @@ -56,20 +57,24 @@ export class OrdersService extends SaasCrudServiceBase { const unitPrice = decimal(goods.price, '商品价格'); const amount = multiplyDecimal(unitPrice, input.quantity); - // The order row is kept in the outer managed transaction. If the remote - // stock reservation fails, Runtime rolls this local CRUD mutation back. + // Complete the remote write before the local eventized CRUD acquires the + // tenant-wide outbox sequence lock. Holding that lock across a synchronous + // module call would make the provider wait for this transaction itself. + await this.reserveStock( + { + goodsId: input.goodsId, + quantity: input.quantity, + }, + { idempotencyKey: businessKey('reserve', input.orderNo) } + ); + const order = await this.orders.add({ order_no: input.orderNo, goods_id: String(input.goodsId), quantity: input.quantity, unit_price: unitPrice, amount, - status: 'pending', - }); - - await this.reserveStock({ - goodsId: input.goodsId, - quantity: input.quantity, + status: 'created', }); return { @@ -98,6 +103,16 @@ export class OrdersService extends SaasCrudServiceBase { return { id: input.id, status: 'cancelled', restoredStock: 0 }; } + const goodsId = safeInteger(order.goods_id, '商品 ID'); + await this.releaseStock( + { + goodsId, + quantity: order.quantity, + orderNo: order.order_no, + }, + { idempotencyKey: businessKey('cancel', String(order.id)) } + ); + const update = await this.orders.update({ id: order.id, status: 'cancelled', @@ -106,13 +121,6 @@ export class OrdersService extends SaasCrudServiceBase { return { id: input.id, status: 'cancelled', restoredStock: 0 }; } - const goodsId = safeInteger(order.goods_id, '商品 ID'); - await this.releaseStock({ - goodsId, - quantity: order.quantity, - orderNo: order.order_no, - }); - return { id: input.id, status: 'cancelled', @@ -134,6 +142,12 @@ export class OrdersService extends SaasCrudServiceBase { } } +function businessKey(operation: string, identity: string): string { + return createHash('sha256') + .update(`${operation}:${identity}`, 'utf8') + .digest('hex'); +} + function decimal(value: unknown, label: string): string { const normalized = String(value ?? '').trim(); if (!/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(normalized)) {