fix: secure inventory workflow for lifecycle candidate

This commit is contained in:
2026-09-07 18:47:04 +08:00
parent 8ea2ec726e
commit 55f5cebc86
11 changed files with 418 additions and 36 deletions

View File

@@ -18,8 +18,8 @@ export class Goods extends SaasTableModel<GoodsRow> {
static readonly definition = defineSaasTableModel({
"kind": "table",
"moduleCode": "demo",
"revision": 2,
"schemaVersion": 2,
"revision": 3,
"schemaVersion": 3,
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
"tableCode": "goods",
"fields": [

View File

@@ -4,8 +4,8 @@ import { defineGeneratedSaasModule } from '@cool-midway/module-runtime';
export const GeneratedSaasModule = defineGeneratedSaasModule({
"formatVersion": 2,
"moduleCode": "demo",
"revision": 2,
"schemaVersion": 2,
"revision": 3,
"schemaVersion": 3,
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
"runtimeContractChecksum": "c1327810dd08dfd0cc1afeee99d7e9707fe14595272a3aa92ee13fc1d54aa0e5",
"implementationManifestChecksum": "d18964a3d40e3c9a65d83996ca3141a56043986c395f5d4cac57c60aa72f4236"

View File

@@ -1,7 +1,7 @@
{
"manifestVersion": 1,
"moduleCode": "demo",
"revision": 2,
"revision": 3,
"schemaChecksum": null,
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
"actionManifestChecksum": null,

View File

@@ -1,9 +1,9 @@
{
"formatVersion": 2,
"moduleCode": "demo",
"revision": 2,
"revision": 3,
"releaseVersion": null,
"schemaVersion": 2,
"schemaVersion": 3,
"schemaInputChecksum": "8a55cecb643f802ef0f39ade2f31fe8313ad437d72d12b64525c0b1a6892813d",
"schemaChecksum": null,
"actionManifestChecksum": null,

View File

@@ -7,6 +7,8 @@ import {
SaasCrudCaller,
SaasCrudService,
SaasCrudServiceBase,
SaasInventoryReservation,
saasWorkflowBusinessId,
} from '@cool-midway/module-runtime';
import {
Actions,
@@ -14,6 +16,7 @@ import {
Events,
type GoodsBatchInfoInput,
type GoodsBatchInfoOutput,
type GoodsCrudBeforeChange,
type GoodsInfoOutput,
type GoodsReleaseStockInput,
type GoodsReleaseStockOutput,
@@ -24,12 +27,25 @@ import {
@Provide()
@SaasCrudService(Crud.Goods)
export class GoodsService extends SaasCrudServiceBase<typeof Crud.Goods> {
@Inject() private readonly inventory!: SaasInventoryReservation;
@InjectSaasCrud(Crud.Goods)
private readonly goods!: SaasCrudCaller<typeof Crud.Goods>;
@Inject()
private readonly saasActionContext!: SaasActionContext;
protected async modifyBefore(change: GoodsCrudBeforeChange): Promise<void> {
if (change.operation === 'delete') {
await this.inventory.assertNoActiveReservations(change.input.id);
}
if (change.operation === 'update' && Object.prototype.hasOwnProperty.call(change.input, 'stock')) {
const caller = this.saasActionContext.managedCrudCaller;
if (caller?.kind !== 'business' || !['goods.reserveStock','goods.releaseStock'].includes(caller.actionName) || this.saasActionContext.workflow?.coordinatorModule !== 'order') {
throw new SaasBusinessReject('STOCK_COMMAND_REQUIRED','现有商品库存只能通过受管库存命令变更');
}
}
}
@SaasAction(Actions.GoodsBatchInfo)
async batchInfo(
input: GoodsBatchInfoInput
@@ -46,7 +62,10 @@ export class GoodsService extends SaasCrudServiceBase<typeof Crud.Goods> {
async reserveStock(
input: GoodsReserveStockInput
): Promise<GoodsReserveStockOutput> {
const goods = await this.requireGoods(input.goodsId);
const workflow = this.saasActionContext.workflow;
if (!workflow || workflow.coordinatorModule !== 'order' || workflow.sagaType !== 'order.create@1') throw new SaasBusinessReject('STOCK_RESERVATION_CONFLICT','库存扣减需要订单创建工作流');
return this.inventory.reserve({reservationId:workflow.sagaId,goodsId:input.goodsId,quantity:input.quantity},async () => {
const goods = await this.requireGoods(input.goodsId, true);
if (goods.status !== 1) {
throw new SaasBusinessReject(
'GOODS_UNAVAILABLE',
@@ -69,14 +88,20 @@ export class GoodsService extends SaasCrudServiceBase<typeof Crud.Goods> {
});
await this.updateStock(input.goodsId, remainingStock);
return { goodsId: input.goodsId, remainingStock };
});
}
@SaasAction(Actions.GoodsReleaseStock)
async releaseStock(
input: GoodsReleaseStockInput
): Promise<GoodsReleaseStockOutput> {
const goods = await this.requireGoods(input.goodsId);
const workflow = this.saasActionContext.workflow;
const reservationId = saasWorkflowBusinessId('order','order.create@1',input.orderNo);
if (!workflow || workflow.coordinatorModule !== 'order' || !['order.create@1','order.cancel@1'].includes(workflow.sagaType) || (workflow.sagaType === 'order.create@1' && workflow.sagaId !== reservationId)) throw new SaasBusinessReject('STOCK_RESERVATION_CONFLICT','库存释放需要匹配的订单工作流');
return this.inventory.release({reservationId,goodsId:input.goodsId,quantity:input.quantity},async () => {
const goods = await this.requireGoods(input.goodsId, true);
const currentStock = goods.stock + input.quantity;
if (!Number.isSafeInteger(currentStock)) throw new SaasBusinessReject('STOCK_RESERVATION_CONFLICT','库存数量溢出');
this.saasActionContext.raise(Events.DemoGoodsStockReserved, {
goodsId: input.goodsId,
@@ -85,6 +110,7 @@ export class GoodsService extends SaasCrudServiceBase<typeof Crud.Goods> {
});
await this.updateStock(input.goodsId, currentStock);
return { goodsId: input.goodsId, currentStock };
});
}
private async findGoods(goodsId: number): Promise<GoodsInfoOutput | null> {
@@ -95,8 +121,10 @@ export class GoodsService extends SaasCrudServiceBase<typeof Crud.Goods> {
return rows[0] || null;
}
private async requireGoods(goodsId: number): Promise<GoodsInfoOutput> {
const goods = await this.findGoods(goodsId);
private async requireGoods(goodsId: number, lock = false): Promise<GoodsInfoOutput> {
const goods = lock
? await this.goods.lock({ id: String(goodsId) })
: await this.findGoods(goodsId);
if (!goods) {
throw new SaasBusinessReject(
'GOODS_NOT_FOUND',