forked from yoone/API
1
0
Fork 0

feat(订单): 增强订单相关功能及数据模型

- 在订单实体中添加orderItems和orderSales字段
- 优化QueryOrderSalesDTO,增加排序字段和更多描述信息
- 重构saveOrderSale方法,使用产品属性自动设置品牌和强度
- 在订单查询中返回关联的orderItems和orderSales数据
- 添加getAttributesObject方法处理产品属性
This commit is contained in:
tikkhun 2026-01-10 11:15:24 +08:00
parent a8d12a695e
commit 4eb45af452
6 changed files with 2972 additions and 263 deletions

View File

@ -98,14 +98,10 @@ export class QueryOrderDTO {
}
export class QueryOrderSalesDTO {
@ApiProperty()
@ApiProperty({ description: '是否为原产品还是库存产品' })
@Rule(RuleType.bool().default(false))
isSource: boolean;
@ApiProperty()
@Rule(RuleType.bool().default(false))
exceptPackage: boolean;
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number;
@ -114,19 +110,31 @@ export class QueryOrderSalesDTO {
@Rule(RuleType.number())
pageSize: number;
@ApiProperty()
@ApiProperty({ description: '排序对象,格式如 { productName: "asc", sku: "desc" }',type: 'any', required: false })
@Rule(RuleType.object().allow(null))
orderBy?: Record<string, 'asc' | 'desc'>;
// filter
@ApiProperty({ description: '是否排除套餐' })
@Rule(RuleType.bool().default(false))
exceptPackage: boolean;
@ApiProperty({ description: '站点ID' })
@Rule(RuleType.number())
siteId: number;
@ApiProperty()
@ApiProperty({ description: '名称' })
@Rule(RuleType.string())
name: string;
@ApiProperty()
@ApiProperty({ description: 'SKU' })
@Rule(RuleType.string())
sku: string;
@ApiProperty({ description: '开始日期' })
@Rule(RuleType.date())
startDate: Date;
@ApiProperty()
@ApiProperty({ description: '结束日期' })
@Rule(RuleType.date())
endDate: Date;
}

View File

@ -272,6 +272,14 @@ export class Order {
@Expose()
updatedAt: Date;
@ApiProperty({ type: 'json', nullable: true, description: '订单项列表' })
@Expose()
orderItems?: any[];
@ApiProperty({ type: 'json', nullable: true, description: '销售项列表' })
@Expose()
orderSales?: any[];
// 在插入或更新前处理用户代理字符串
@BeforeInsert()
@BeforeUpdate()

View File

@ -75,7 +75,7 @@ export class OrderSale {
@ApiProperty({ nullable: true })
@Column({ type: 'int', nullable: true })
@Expose()
size: number | null;
size: number | null; // 其实是 strength
@ApiProperty()
@Column({ default: false })
@ -98,24 +98,24 @@ export class OrderSale {
@Expose()
updatedAt?: Date;
// === 自动计算逻辑 ===
@BeforeInsert()
@BeforeUpdate()
setFlags() {
if (!this.name) return;
const lower = this.name.toLowerCase();
this.isYoone = lower.includes('yoone');
this.isZex = lower.includes('zex');
this.isYooneNew = this.isYoone && lower.includes('new');
let size: number | null = null;
const sizes = [3, 6, 9, 12, 15, 18];
for (const s of sizes) {
if (lower.includes(s.toString())) {
size = s;
break;
}
}
this.size = size;
}
// // === 自动计算逻辑 ===
// @BeforeInsert()
// @BeforeUpdate()
// setFlags() {
// if (!this.name) return;
// const lower = this.name.toLowerCase();
// this.isYoone = lower.includes('yoone');
// this.isZex = lower.includes('zex');
// this.isYooneNew = this.isYoone && lower.includes('new');
// let size: number | null = null;
// const sizes = [3, 6, 9, 12, 15, 18];
// for (const s of sizes) {
// if (lower.includes(s.toString())) {
// size = s;
// break;
// }
// }
// this.size = size;
// }
}

File diff suppressed because it is too large Load Diff

View File

@ -110,6 +110,8 @@ export class OrderService {
@Logger()
logger; // 注入 Logger 实例
@Inject()
productService: ProductService;
/**
*
@ -281,6 +283,11 @@ export class OrderService {
console.error('更新订单状态失败,原因为:', error)
}
}
async getOrderByExternalOrderId(siteId: number, externalOrderId: string) {
return await this.orderModel.findOne({
where: { externalOrderId: String(externalOrderId), siteId },
});
}
/**
*
* :
@ -304,6 +311,7 @@ export class OrderService {
* @param order
* @param forceUpdate
*/
async syncSingleOrder(siteId: number, order: UnifiedOrderDTO, forceUpdate = false) {
async syncSingleOrder(siteId: number, order: UnifiedOrderDTO, forceUpdate = false) {
// 从订单数据中解构出各个子项
let {
@ -318,47 +326,27 @@ export class OrderService {
// console.log('同步进单个订单', order)
// 如果订单状态为 AUTO_DRAFT,则跳过处理
if (order.status === OrderStatus.AUTO_DRAFT) {
this.logger.debug('订单状态为 AUTO_DRAFT,跳过处理', siteId, order.id)
return;
}
// 检查数据库中是否已存在该订单
const existingOrder = await this.orderModel.findOne({
where: { externalOrderId: String(order.id), siteId: siteId },
});
// 自动更新订单状态(如果需要)
// if(!order.is_editable && !forceUpdate){
// this.logger.debug('订单不可编辑,跳过处理', siteId, order.id)
// return;
// }
// 自动转换远程订单的状态(如果需要)
await this.autoUpdateOrderStatus(siteId, order);
if(existingOrder){
// 矫正数据库中的订单数据
const updateData: any = { status: order.status };
if (this.canUpdateErpStatus(existingOrder.orderStatus)) {
updateData.orderStatus = this.mapOrderStatus(order.status as any);
}
// 更新订单主数据
await this.orderModel.update({ externalOrderId: String(order.id), siteId: siteId }, updateData);
// 更新 fulfillments 数据
await this.saveOrderFulfillments({
siteId,
orderId: existingOrder.id,
externalOrderId:order.id,
fulfillments: fulfillments,
});
}
const externalOrderId = String(order.id);
// 这里的 saveOrder 已经包括了创建订单和更新订单
let orderRecord: Order = await this.saveOrder(siteId, orderData);
// 如果订单从未完成变为完成状态,则更新库存
if (
existingOrder &&
existingOrder.orderStatus !== ErpOrderStatus.COMPLETED &&
orderRecord &&
orderRecord.orderStatus !== ErpOrderStatus.COMPLETED &&
orderData.status === OrderStatus.COMPLETED
) {
this.updateStock(existingOrder);
await this.updateStock(orderRecord);
// 不再直接返回,继续执行后续的更新操作
}
// 如果订单不可编辑且不强制更新,则跳过处理
if (existingOrder && !existingOrder.is_editable && !forceUpdate) {
return;
}
// 保存订单主数据
const orderRecord = await this.saveOrder(siteId, orderData);
const externalOrderId = String(order.id);
const orderId = orderRecord.id;
// 保存订单项
await this.saveOrderItems({
@ -462,7 +450,8 @@ export class OrderService {
* @param order
* @returns
*/
async saveOrder(siteId: number, order: Partial<UnifiedOrderDTO>): Promise<Order> {
// 这里 omit 是因为处理在外头了 其实 saveOrder 应该包括 savelineitems 等
async saveOrder(siteId: number, order: Omit<UnifiedOrderDTO, 'line_items'|'refunds'>): Promise<Order> {
// 将外部订单ID转换为字符串
const externalOrderId = String(order.id)
delete order.id
@ -711,6 +700,8 @@ export class OrderService {
*
* @param orderItem
*/
// TODO 这里存的是库存商品实际
// 所以叫做 orderInventoryItems 可能更合适
async saveOrderSale(orderItem: OrderItem) {
const currentOrderSale = await this.orderSaleModel.find({
where: {
@ -729,46 +720,44 @@ export class OrderService {
});
if (!product) return;
const orderSales: OrderSale[] = [];
if (product.components && product.components.length > 0) {
for (const comp of product.components) {
const baseProduct = await this.productModel.findOne({
const componentDetails: { product: Product, quantity: number }[] = product.components?.length > 0 ? await Promise.all(product.components.map(async comp => {
return {
product: await this.productModel.findOne({
where: { sku: comp.sku },
});
if (baseProduct) {
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
orderId: orderItem.orderId,
siteId: orderItem.siteId,
externalOrderItemId: orderItem.externalOrderItemId,
productId: baseProduct.id,
name: baseProduct.name,
}),
quantity: comp.quantity * orderItem.quantity,
sku: comp.sku,
isPackage: orderItem.name.toLowerCase().includes('package'),
});
orderSales.push(orderSaleItem);
}
}
} else {
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
})) : [{ product, quantity: orderItem.quantity }];
const orderSales: OrderSale[] = componentDetails.map(componentDetail => {
const attrsObj = this.productService.getAttributesObject(product.attributes)
const orderSale = plainToClass(OrderSale, {
orderId: orderItem.orderId,
siteId: orderItem.siteId,
externalOrderItemId: orderItem.externalOrderItemId,
productId: product.id,
name: product.name,
quantity: orderItem.quantity,
sku: product.sku,
isPackage: orderItem.name.toLowerCase().includes('package'),
productId: componentDetail.product.id,
name: componentDetail.product.name,
quantity: componentDetail.quantity * orderItem.quantity,
sku: componentDetail.product.sku,
isPackage: componentDetail.product.type === 'bundle',
isYoone: attrsObj?.['brand']?.name === 'yoone',
isZyn: attrsObj?.['brand']?.name === 'zyn',
isYooneNew: attrsObj?.['brand']?.name === 'yoone' && attrsObj?.['version']?.name === 'new',
size: this.extractNumberFromString(attrsObj?.['strength']?.name) || null,
});
orderSales.push(orderSaleItem);
}
return orderSale
})
if (orderSales.length > 0) {
await this.orderSaleModel.save(orderSales);
}
}
extractNumberFromString(str: string): number {
if (!str) return 0;
const num = parseInt(str, 10);
return isNaN(num) ? 0 : num;
}
/**
* 退
@ -1195,7 +1184,53 @@ export class OrderService {
) END
),
JSON_ARRAY()
) as fulfillments
) as fulfillments,
(
SELECT COALESCE(
JSON_ARRAYAGG(
JSON_OBJECT(
'id', oi.id,
'name', oi.name,
'orderId', oi.orderId,
'siteId', oi.siteId,
'externalOrderId', oi.externalOrderId,
'externalOrderItemId', oi.externalOrderItemId,
'externalProductId', oi.externalProductId,
'externalVariationId', oi.externalVariationId,
'quantity', oi.quantity,
'subtotal', oi.subtotal,
'subtotal_tax', oi.subtotal_tax,
'total', oi.total,
'total_tax', oi.total_tax,
'sku', oi.sku,
'price', oi.price
)
),
JSON_ARRAY()
)
FROM order_item oi
WHERE oi.orderId = o.id
) AS orderItems,
(
SELECT COALESCE(
JSON_ARRAYAGG(
JSON_OBJECT(
'id', os.id,
'orderId', os.orderId,
'siteId', os.siteId,
'externalOrderItemId', os.externalOrderItemId,
'productId', os.productId,
'name', os.name,
'sku', os.sku,
'quantity', os.quantity,
'isPackage', os.isPackage
)
),
JSON_ARRAY()
)
FROM order_sale os
WHERE os.orderId = o.id
) AS orderSales
FROM \`order\` o
LEFT JOIN (
SELECT
@ -1429,7 +1464,7 @@ export class OrderService {
* @param params
* @returns
*/
async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: QueryOrderSalesDTO) {
async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage, orderBy }: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
const defaultStart = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss');
const defaultEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
@ -1645,11 +1680,12 @@ export class OrderService {
* @returns
*/
async getOrderItems({
current,
pageSize,
siteId,
startDate,
endDate,
current,
pageSize,
sku,
name,
}: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : [];

View File

@ -1536,7 +1536,13 @@ export class ProductService {
return dto;
}
getAttributesObject(attributes:DictItem[]){
const obj:any = {}
attributes.forEach(attr=>{
obj[attr.dict.name] = attr
})
return obj
}
// 将单个产品转换为 CSV 行数组
transformProductToCsvRow(
p: Product,