feat: implement order candidate 1.0.0-rc.1
This commit is contained in:
181
src/service/orders.ts
Normal file
181
src/service/orders.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import { Provide } from '@midwayjs/core';
|
||||
import {
|
||||
InjectSaasAction,
|
||||
InjectSaasCrud,
|
||||
ModuleRuntimeError,
|
||||
SaasAction,
|
||||
type SaasActionCaller,
|
||||
type SaasCrudCaller,
|
||||
SaasCrudService,
|
||||
SaasCrudServiceBase,
|
||||
} from '@cool-midway/module-runtime';
|
||||
import {
|
||||
Actions,
|
||||
Crud,
|
||||
SaasActions,
|
||||
type OrdersCancelOrderInput,
|
||||
type OrdersCancelOrderOutput,
|
||||
type OrdersCreateOrderInput,
|
||||
type OrdersCreateOrderOutput,
|
||||
type OrdersDetailWithGoodsInput,
|
||||
type OrdersDetailWithGoodsOutput,
|
||||
} from '../saas/generated/order';
|
||||
|
||||
type GoodsRecord = Record<string, unknown>;
|
||||
|
||||
@Provide()
|
||||
@SaasCrudService(Crud.Orders)
|
||||
export class OrdersService extends SaasCrudServiceBase<typeof Crud.Orders> {
|
||||
@InjectSaasCrud(Crud.Orders)
|
||||
private readonly orders!: SaasCrudCaller<typeof Crud.Orders>;
|
||||
|
||||
@InjectSaasAction(SaasActions.Demo.GoodsBatchInfo)
|
||||
private readonly batchInfo!: SaasActionCaller<
|
||||
typeof SaasActions.Demo.GoodsBatchInfo
|
||||
>;
|
||||
|
||||
@InjectSaasAction(SaasActions.Demo.GoodsReserveStock)
|
||||
private readonly reserveStock!: SaasActionCaller<
|
||||
typeof SaasActions.Demo.GoodsReserveStock
|
||||
>;
|
||||
|
||||
@InjectSaasAction(SaasActions.Demo.GoodsReleaseStock)
|
||||
private readonly releaseStock!: SaasActionCaller<
|
||||
typeof SaasActions.Demo.GoodsReleaseStock
|
||||
>;
|
||||
|
||||
@SaasAction(Actions.OrdersCreateOrder)
|
||||
async createOrder(
|
||||
input: OrdersCreateOrderInput
|
||||
): Promise<OrdersCreateOrderOutput> {
|
||||
const goods = await this.requireGoods(input.goodsId);
|
||||
if (integer(goods.status, '商品状态') !== 1) {
|
||||
throw conflict('商品未上架,不能下单');
|
||||
}
|
||||
|
||||
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.
|
||||
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,
|
||||
});
|
||||
|
||||
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<OrdersDetailWithGoodsOutput> {
|
||||
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<OrdersCancelOrderOutput> {
|
||||
const order = await this.requireOrder(input.id);
|
||||
if (order.status === 'cancelled') {
|
||||
return { id: input.id, status: 'cancelled', restoredStock: 0 };
|
||||
}
|
||||
|
||||
const update = await this.orders.update({
|
||||
id: order.id,
|
||||
status: 'cancelled',
|
||||
});
|
||||
if (update.affected === 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 {
|
||||
id: input.id,
|
||||
status: 'cancelled',
|
||||
restoredStock: order.quantity,
|
||||
};
|
||||
}
|
||||
|
||||
private async requireOrder(id: number) {
|
||||
const order = await this.orders.info({ id: String(id) });
|
||||
if (!order) throw conflict(`订单不存在: ${id}`);
|
||||
return order;
|
||||
}
|
||||
|
||||
private async requireGoods(goodsId: number): Promise<GoodsRecord> {
|
||||
const result = await this.batchInfo({ ids: [goodsId] });
|
||||
const goods = result.list.find(item => String(item.id) === String(goodsId));
|
||||
if (!goods) throw conflict(`商品不存在: ${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);
|
||||
}
|
||||
Reference in New Issue
Block a user