fix: avoid cross-module write lock inversion

This commit is contained in:
2026-09-03 22:24:03 +08:00
parent 3ff729cbc6
commit cfc82231b3

View File

@@ -1,4 +1,5 @@
import { Provide } from '@midwayjs/core'; import { Provide } from '@midwayjs/core';
import { createHash } from 'node:crypto';
import { import {
InjectSaasAction, InjectSaasAction,
InjectSaasCrud, InjectSaasCrud,
@@ -56,20 +57,24 @@ export class OrdersService extends SaasCrudServiceBase<typeof Crud.Orders> {
const unitPrice = decimal(goods.price, '商品价格'); const unitPrice = decimal(goods.price, '商品价格');
const amount = multiplyDecimal(unitPrice, input.quantity); const amount = multiplyDecimal(unitPrice, input.quantity);
// The order row is kept in the outer managed transaction. If the remote // Complete the remote write before the local eventized CRUD acquires the
// stock reservation fails, Runtime rolls this local CRUD mutation back. // 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({ const order = await this.orders.add({
order_no: input.orderNo, order_no: input.orderNo,
goods_id: String(input.goodsId), goods_id: String(input.goodsId),
quantity: input.quantity, quantity: input.quantity,
unit_price: unitPrice, unit_price: unitPrice,
amount, amount,
status: 'pending', status: 'created',
});
await this.reserveStock({
goodsId: input.goodsId,
quantity: input.quantity,
}); });
return { return {
@@ -98,6 +103,16 @@ export class OrdersService extends SaasCrudServiceBase<typeof Crud.Orders> {
return { id: input.id, status: 'cancelled', restoredStock: 0 }; 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({ const update = await this.orders.update({
id: order.id, id: order.id,
status: 'cancelled', status: 'cancelled',
@@ -106,13 +121,6 @@ export class OrdersService extends SaasCrudServiceBase<typeof Crud.Orders> {
return { id: input.id, status: 'cancelled', restoredStock: 0 }; 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 { return {
id: input.id, id: input.id,
status: 'cancelled', status: 'cancelled',
@@ -134,6 +142,12 @@ export class OrdersService extends SaasCrudServiceBase<typeof Crud.Orders> {
} }
} }
function businessKey(operation: string, identity: string): string {
return createHash('sha256')
.update(`${operation}:${identity}`, 'utf8')
.digest('hex');
}
function decimal(value: unknown, label: string): string { function decimal(value: unknown, label: string): string {
const normalized = String(value ?? '').trim(); const normalized = String(value ?? '').trim();
if (!/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(normalized)) { if (!/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(normalized)) {