import { Inject, Provide } from '@midwayjs/core'; import { InjectSaasAction, InjectSaasCrud, ModuleRuntimeError, SaasAction, SaasBusinessReject, type SaasActionCaller, type SaasCrudCaller, SaasCrudService, SaasCrudServiceBase, SaasWorkflow, SaasActionContext, } from '@cool-midway/module-runtime'; import { Actions, Crud, SaasActions, type OrdersCancelOrderInput, type OrdersCancelOrderOutput, type OrdersCreateOrderInput, type OrdersCreateOrderOutput, type OrdersCrudBeforeChange, type OrdersDetailWithGoodsInput, type OrdersDetailWithGoodsOutput, } from '../saas/generated/order'; type GoodsRecord = Record; @Provide() @SaasCrudService(Crud.Orders) export class OrdersService extends SaasCrudServiceBase { @Inject() private readonly workflow!: SaasWorkflow; @Inject() private readonly actionContext!: SaasActionContext; @InjectSaasCrud(Crud.Orders) private readonly orders!: SaasCrudCaller; @InjectSaasAction(SaasActions.Demo.GoodsBatchInfo) private readonly batchInfo!: SaasActionCaller< typeof SaasActions.Demo.GoodsBatchInfo >; protected async modifyBefore(change: OrdersCrudBeforeChange): Promise { const caller = this.actionContext.managedCrudCaller; if (change.operation === 'add') { if (caller?.kind !== 'business' || caller.actionName !== 'orders.createOrder' || change.input.status !== 'pending_reservation') throw new SaasBusinessReject('ORDER_COMMAND_REQUIRED','创建订单必须使用下单业务命令'); return; } if (change.operation === 'delete') throw new SaasBusinessReject('ORDER_COMMAND_REQUIRED','订单保留作为库存预留及工作流凭据,不能通过通用CRUD删除'); const protectedFields = ['order_no','goods_id','quantity','unit_price','amount','status']; if (!protectedFields.some(field => Object.prototype.hasOwnProperty.call(change.input,field))) return; const keys = Object.keys(change.input); if (keys.some(key => key !== 'id' && key !== 'status')) throw new SaasBusinessReject('ORDER_COMMAND_REQUIRED','订单商品、数量和金额在创建后不可修改'); const status = change.input.status; if (caller?.kind === 'business' && caller.actionName === 'orders.cancelOrder' && status === 'cancel_pending') return; const outcomes: Record = { 'saas.saga.step.completed':['confirmed','cancelled'], 'saas.saga.step.failed':['rejected','manual_review'], 'saas.saga.compensation.completed':['failed','manual_review'], }; if (caller?.kind === 'saga' && outcomes[caller.eventType || '']?.includes(String(status))) return; throw new SaasBusinessReject('ORDER_COMMAND_REQUIRED','订单受管字段只能由业务命令和工作流回执变更'); } @SaasAction(Actions.OrdersCreateOrder) async createOrder( input: OrdersCreateOrderInput ): Promise { const goods = await this.requireGoods(input.goodsId); if (integer(goods.status, '商品状态') !== 1) { throw new SaasBusinessReject('GOODS_UNAVAILABLE', '商品未上架,不能下单'); } const unitPrice = decimal(goods.price, '商品价格'); const amount = multiplyDecimal(unitPrice, input.quantity); const order = await this.orders.add({ order_no: input.orderNo, goods_id: String(input.goodsId), quantity: input.quantity, unit_price: unitPrice, amount, status: 'pending_reservation', }); await this.workflow.start({ type: 'order.create@1', businessKey: input.orderNo, steps: [{key:'reserve',action:SaasActions.Demo.GoodsReserveStock, input:{goodsId:input.goodsId,quantity:input.quantity}, compensate:{action:SaasActions.Demo.GoodsReleaseStock,input:{goodsId:input.goodsId,quantity:input.quantity,orderNo:input.orderNo}}}], terminal: Crud.Orders, outcomes: { completed:{id:order.id,status:'confirmed'}, failed:{id:order.id,status:'rejected'}, compensated:{id:order.id,status:'failed'}, }, }); return { id: safeInteger(order.id, '订单 ID'), orderNo: order.order_no, amount: finiteNumber(order.amount, '订单金额'), status: order.status, }; } @SaasAction(Actions.OrdersDetailWithGoods) async detailWithGoods( input: OrdersDetailWithGoodsInput ): Promise { const order = await this.requireOrder(input.id); const goods = await this.requireGoods(safeInteger(order.goods_id, '商品 ID')); return { order, goods }; } @SaasAction(Actions.OrdersCancelOrder) async cancelOrder( input: OrdersCancelOrderInput ): Promise { const order = await this.orders.lock({id:String(input.id)}); if (!order) throw new SaasBusinessReject('ORDER_NOT_FOUND', `订单不存在: ${input.id}`); if (order.status === 'cancelled') { return { id: input.id, status: 'cancelled', restoredStock: 0 }; } if (order.status === 'cancel_pending') return {id:input.id,status:'cancel_pending',restoredStock:0}; if (order.status !== 'confirmed') throw new SaasBusinessReject('ORDER_STATE_CONFLICT','订单当前状态不能取消;历史订单需要先核对预留记录'); const goodsId = safeInteger(order.goods_id, '商品 ID'); await this.orders.update({id:order.id,status:'cancel_pending'}); await this.workflow.start({ type:'order.cancel@1',businessKey:String(order.id), steps:[{key:'release',action:SaasActions.Demo.GoodsReleaseStock,input:{goodsId,quantity:order.quantity,orderNo:order.order_no}}], terminal:Crud.Orders, outcomes:{completed:{id:order.id,status:'cancelled'},failed:{id:order.id,status:'manual_review'},compensated:{id:order.id,status:'manual_review'}}, }); return {id:input.id,status:'cancel_pending',restoredStock:0}; } private async requireOrder(id: number) { // The locked info contract requires an object; its empty result would fail // output validation before business code can classify a missing order. // The existing list contract permits an empty array and this exact ID filter. const [order] = await this.orders.list({ where: { id: String(id) }, take: 1 }); if (!order) throw new SaasBusinessReject('ORDER_NOT_FOUND', `订单不存在: ${id}`); return order; } private async requireGoods(goodsId: number): Promise { const result = await this.batchInfo({ ids: [goodsId] }); const goods = result.list.find(item => String(item.id) === String(goodsId)); if (!goods) throw new SaasBusinessReject('GOODS_NOT_FOUND', `商品不存在: ${goodsId}`); return goods; } } function decimal(value: unknown, label: string): string { const normalized = String(value ?? '').trim(); if (!/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(normalized)) { throw conflict(`${label}非法`); } return normalized; } function multiplyDecimal(value: string, multiplier: number): string { const negative = value.startsWith('-'); const unsigned = negative ? value.slice(1) : value; const [integerPart, fractionPart = ''] = unsigned.split('.'); const scale = fractionPart.length; const digits = BigInt(`${integerPart}${fractionPart}`); const product = digits * BigInt(multiplier); const padded = product.toString().padStart(scale + 1, '0'); const absolute = scale ? `${padded.slice(0, -scale)}.${padded.slice(-scale)}` : padded; return negative && product !== BigInt(0) ? `-${absolute}` : absolute; } function integer(value: unknown, label: string): number { const normalized = Number(value); if (!Number.isInteger(normalized)) throw conflict(`${label}非法`); return normalized; } function safeInteger(value: unknown, label: string): number { const normalized = integer(value, label); if (!Number.isSafeInteger(normalized) || normalized < 1) { throw conflict(`${label}超出安全范围`); } return normalized; } function finiteNumber(value: unknown, label: string): number { const normalized = Number(value); if (!Number.isFinite(normalized)) throw conflict(`${label}非法`); return normalized; } function conflict(message: string) { return new ModuleRuntimeError('MODULE_ACTION_INPUT_INVALID', message, 409); }