import { Inject, Provide } from '@midwayjs/core'; import { InjectSaasCrud, SaasAction, SaasActionContext, SaasBusinessReject, SaasCrudCaller, SaasCrudService, SaasCrudServiceBase, SaasInventoryReservation, saasWorkflowBusinessId, } from '@cool-midway/module-runtime'; import { Actions, Crud, Events, type GoodsBatchInfoInput, type GoodsBatchInfoOutput, type GoodsCrudBeforeChange, type GoodsInfoOutput, type GoodsReleaseStockInput, type GoodsReleaseStockOutput, type GoodsReserveStockInput, type GoodsReserveStockOutput, } from '../saas/generated'; @Provide() @SaasCrudService(Crud.Goods) export class GoodsService extends SaasCrudServiceBase { @Inject() private readonly inventory!: SaasInventoryReservation; @InjectSaasCrud(Crud.Goods) private readonly goods!: SaasCrudCaller; @Inject() private readonly saasActionContext!: SaasActionContext; protected async modifyBefore(change: GoodsCrudBeforeChange): Promise { 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','现有商品库存只能通过受管库存命令变更'); } } if (change.operation === 'add' || change.operation === 'update') { const input = change.input; if (Object.prototype.hasOwnProperty.call(input, 'title') && !input.title.trim()) { throw new SaasBusinessReject('GOODS_INPUT_INVALID', '商品名称不能为空'); } if (Object.prototype.hasOwnProperty.call(input, 'price') && !/^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(input.price)) { throw new SaasBusinessReject('GOODS_INPUT_INVALID', '商品价格必须是非负金额'); } if (Object.prototype.hasOwnProperty.call(input, 'stock') && (!Number.isSafeInteger(input.stock) || input.stock < 0 || input.stock > 2147483647)) { throw new SaasBusinessReject('GOODS_INPUT_INVALID', '商品库存必须是有效的非负整数'); } if (Object.prototype.hasOwnProperty.call(input, 'status') && input.status !== 0 && input.status !== 1) { throw new SaasBusinessReject('GOODS_INPUT_INVALID', '商品状态仅允许下架或上架'); } } } @SaasAction(Actions.GoodsBatchInfo) async batchInfo( input: GoodsBatchInfoInput ): Promise { const list: GoodsInfoOutput[] = []; for (const id of input.ids) { const goods = await this.findGoods(id); if (goods) list.push(goods); } return { list }; } @SaasAction(Actions.GoodsReserveStock) async reserveStock( input: GoodsReserveStockInput ): Promise { 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', `商品 ${input.goodsId} 当前不可售` ); } if (goods.stock < input.quantity) { throw new SaasBusinessReject( 'INSUFFICIENT_STOCK', `商品 ${input.goodsId} 库存不足`, { available: goods.stock, requested: input.quantity } ); } const remainingStock = goods.stock - input.quantity; this.saasActionContext.raise(Events.DemoGoodsStockReserved, { goodsId: input.goodsId, quantity: input.quantity, remainingStock, }); await this.updateStock(input.goodsId, remainingStock); return { goodsId: input.goodsId, remainingStock }; }); } @SaasAction(Actions.GoodsReleaseStock) async releaseStock( input: GoodsReleaseStockInput ): Promise { 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, quantity: input.quantity, remainingStock: currentStock, }); await this.updateStock(input.goodsId, currentStock); return { goodsId: input.goodsId, currentStock }; }); } private async findGoods(goodsId: number): Promise { const rows = await this.goods.list({ where: { id: String(goodsId) }, take: 1, }); return rows[0] || null; } private async requireGoods(goodsId: number, lock = false): Promise { const goods = lock ? await this.goods.lock({ id: String(goodsId) }) : await this.findGoods(goodsId); if (!goods) { throw new SaasBusinessReject( 'GOODS_NOT_FOUND', `商品 ${goodsId} 不存在` ); } return goods; } private async updateStock(goodsId: number, stock: number): Promise { const result = await this.goods.update({ id: String(goodsId), stock }); if (result.affected !== 1) { throw new SaasBusinessReject( 'GOODS_STOCK_UPDATE_FAILED', `商品 ${goodsId} 库存更新失败` ); } } }