Compare commits

..

No commits in common. "5d7e0090aa0bf84585d0b5a0c0c14eed56e13191" and "a00a95c9a314cd1ad0a4c5c7bec6f7a965552357" have entirely different histories.

6 changed files with 360 additions and 2949 deletions

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -39,7 +39,6 @@ import * as path from 'path';
import * as os from 'os'; import * as os from 'os';
import { UnifiedOrderDTO } from '../dto/site-api.dto'; import { UnifiedOrderDTO } from '../dto/site-api.dto';
import { CustomerService } from './customer.service'; import { CustomerService } from './customer.service';
import { ProductService } from './product.service';
@Provide() @Provide()
export class OrderService { export class OrderService {
@ -111,8 +110,6 @@ export class OrderService {
@Logger() @Logger()
logger; // 注入 Logger 实例 logger; // 注入 Logger 实例
@Inject()
productService: ProductService;
/** /**
* *
@ -141,7 +138,7 @@ export class OrderService {
updated: 0, updated: 0,
errors: [] errors: []
}; };
console.log('开始进入循环同步订单', result.length, '个订单')
// 遍历每个订单进行同步 // 遍历每个订单进行同步
for (const order of result) { for (const order of result) {
try { try {
@ -165,6 +162,7 @@ export class OrderService {
} else { } else {
syncResult.created++; syncResult.created++;
} }
// console.log('updated', syncResult.updated, 'created:', syncResult.created)
} catch (error) { } catch (error) {
// 记录错误但不中断整个同步过程 // 记录错误但不中断整个同步过程
syncResult.errors.push({ syncResult.errors.push({
@ -174,6 +172,8 @@ export class OrderService {
syncResult.processed++; syncResult.processed++;
} }
} }
console.log('同步完成', syncResult.updated, 'created:', syncResult.created)
this.logger.debug('syncOrders result', syncResult) this.logger.debug('syncOrders result', syncResult)
return syncResult; return syncResult;
} }
@ -281,11 +281,6 @@ export class OrderService {
console.error('更新订单状态失败,原因为:', error) console.error('更新订单状态失败,原因为:', error)
} }
} }
async getOrderByExternalOrderId(siteId: number, externalOrderId: string) {
return await this.orderModel.findOne({
where: { externalOrderId: String(externalOrderId), siteId },
});
}
/** /**
* *
* : * :
@ -323,34 +318,53 @@ export class OrderService {
// console.log('同步进单个订单', order) // console.log('同步进单个订单', order)
// 如果订单状态为 AUTO_DRAFT,则跳过处理 // 如果订单状态为 AUTO_DRAFT,则跳过处理
if (order.status === OrderStatus.AUTO_DRAFT) { if (order.status === OrderStatus.AUTO_DRAFT) {
this.logger.debug('订单状态为 AUTO_DRAFT,跳过处理', siteId, order.id)
return; return;
} }
// 这里其实不用过滤不可编辑的行为,而是应在 save 中做判断 // 检查数据库中是否已存在该订单
// if(!order.is_editable && !forceUpdate){ const existingOrder = await this.orderModel.findOne({
// this.logger.debug('订单不可编辑,跳过处理', siteId, order.id) where: { externalOrderId: String(order.id), siteId: siteId },
// return; });
// } // 自动更新订单状态(如果需要)
// 自动转换远程订单的状态(如果需要)
await this.autoUpdateOrderStatus(siteId, order); await this.autoUpdateOrderStatus(siteId, order);
// 这里的 saveOrder 已经包括了创建订单和更新订单
let orderRecord: Order = await this.saveOrder(siteId, orderData); if(existingOrder){
// 如果订单从未完成变为完成状态,则更新库存 // 矫正数据库中的订单数据
if ( const updateData: any = { status: order.status };
orderRecord && if (this.canUpdateErpStatus(existingOrder.orderStatus)) {
orderRecord.orderStatus !== ErpOrderStatus.COMPLETED && updateData.orderStatus = this.mapOrderStatus(order.status as any);
orderData.status === OrderStatus.COMPLETED }
) { // 更新订单主数据
await this.updateStock(orderRecord); 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); const externalOrderId = String(order.id);
// 如果订单从未完成变为完成状态,则更新库存
if (
existingOrder &&
existingOrder.orderStatus !== ErpOrderStatus.COMPLETED &&
orderData.status === OrderStatus.COMPLETED
) {
this.updateStock(existingOrder);
// 不再直接返回,继续执行后续的更新操作
}
// 如果订单不可编辑且不强制更新,则跳过处理
if (existingOrder && !existingOrder.is_editable && !forceUpdate) {
return;
}
// 保存订单主数据
const orderRecord = await this.saveOrder(siteId, orderData);
const orderId = orderRecord.id; const orderId = orderRecord.id;
// 保存订单项 // 保存订单项
await this.saveOrderItems({ await this.saveOrderItems({
siteId, siteId,
orderId, orderId,
externalOrderId, externalOrderId: String(externalOrderId),
orderItems: line_items, orderItems: line_items,
}); });
// 保存退款信息 // 保存退款信息
@ -448,8 +462,7 @@ export class OrderService {
* @param order * @param order
* @returns * @returns
*/ */
// 这里 omit 是因为处理在外头了 其实 saveOrder 应该包括 savelineitems 等 async saveOrder(siteId: number, order: Partial<UnifiedOrderDTO>): Promise<Order> {
async saveOrder(siteId: number, order: Omit<UnifiedOrderDTO, 'line_items' | 'refunds'>): Promise<Order> {
// 将外部订单ID转换为字符串 // 将外部订单ID转换为字符串
const externalOrderId = String(order.id) const externalOrderId = String(order.id)
delete order.id delete order.id
@ -698,8 +711,6 @@ export class OrderService {
* *
* @param orderItem * @param orderItem
*/ */
// TODO 这里存的是库存商品实际
// 所以叫做 orderInventoryItems 可能更合适
async saveOrderSale(orderItem: OrderItem) { async saveOrderSale(orderItem: OrderItem) {
const currentOrderSale = await this.orderSaleModel.find({ const currentOrderSale = await this.orderSaleModel.find({
where: { where: {
@ -718,47 +729,46 @@ export class OrderService {
}); });
if (!product) return; if (!product) return;
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 },
relations: ['components', 'attributes'],
}),
quantity: comp.quantity * orderItem.quantity,
}
})) : [{ product, quantity: orderItem.quantity }]
const orderSales: OrderSale[] = componentDetails.map(componentDetail => { const orderSales: OrderSale[] = [];
if (!componentDetail.product) return null
const attrsObj = this.productService.getAttributesObject(product.attributes) if (product.components && product.components.length > 0) {
const orderSale = plainToClass(OrderSale, { for (const comp of product.components) {
const baseProduct = await this.productModel.findOne({
where: { sku: comp.sku },
});
if (baseProduct) {
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
orderId: orderItem.orderId, orderId: orderItem.orderId,
siteId: orderItem.siteId, siteId: orderItem.siteId,
externalOrderItemId: orderItem.externalOrderItemId, externalOrderItemId: orderItem.externalOrderItemId,
productId: componentDetail.product.id, productId: baseProduct.id,
name: componentDetail.product.name, name: baseProduct.name,
quantity: componentDetail.quantity * orderItem.quantity, quantity: comp.quantity * orderItem.quantity,
sku: componentDetail.product.sku, sku: comp.sku,
isPackage: componentDetail.product.type === 'bundle', isPackage: orderItem.name.toLowerCase().includes('package'),
isYoone: attrsObj?.['brand']?.name === 'yoone',
isZyn: attrsObj?.['brand']?.name === 'zyn',
isZex: attrsObj?.['brand']?.name === 'zex',
isYooneNew: attrsObj?.['brand']?.name === 'yoone' && attrsObj?.['version']?.name === 'new',
size: this.extractNumberFromString(attrsObj?.['strength']?.name) || null,
}); });
return orderSale orderSales.push(orderSaleItem);
}).filter(v => v !== null) }
}
} else {
const orderSaleItem: 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'),
});
orderSales.push(orderSaleItem);
}
if (orderSales.length > 0) { if (orderSales.length > 0) {
await this.orderSaleModel.save(orderSales); await this.orderSaleModel.save(orderSales);
} }
} }
extractNumberFromString(str: string): number {
if (!str) return 0;
const num = parseInt(str, 10);
return isNaN(num) ? 0 : num;
}
/** /**
* 退 * 退
@ -1227,13 +1237,13 @@ export class OrderService {
parameters.push(siteId); parameters.push(siteId);
} }
if (startDate) { if (startDate) {
sqlQuery += ` AND o.date_created >= ?`; sqlQuery += ` AND o.date_paid >= ?`;
totalQuery += ` AND o.date_created >= ?`; totalQuery += ` AND o.date_paid >= ?`;
parameters.push(startDate); parameters.push(startDate);
} }
if (endDate) { if (endDate) {
sqlQuery += ` AND o.date_created <= ?`; sqlQuery += ` AND o.date_paid <= ?`;
totalQuery += ` AND o.date_created <= ?`; totalQuery += ` AND o.date_paid <= ?`;
parameters.push(endDate); parameters.push(endDate);
} }
// 支付方式筛选(使用参数化,避免SQL注入) // 支付方式筛选(使用参数化,避免SQL注入)
@ -1321,7 +1331,7 @@ export class OrderService {
// 添加分页到主查询 // 添加分页到主查询
sqlQuery += ` sqlQuery += `
GROUP BY o.id GROUP BY o.id
ORDER BY o.date_created DESC ORDER BY o.date_paid DESC
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
`; `;
parameters.push(pageSize, (current - 1) * pageSize); parameters.push(pageSize, (current - 1) * pageSize);
@ -1419,7 +1429,7 @@ export class OrderService {
* @param params * @param params
* @returns * @returns
*/ */
async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage, orderBy }: QueryOrderSalesDTO) { async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : []; const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
const defaultStart = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss'); 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'); const defaultEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
@ -1635,12 +1645,11 @@ export class OrderService {
* @returns * @returns
*/ */
async getOrderItems({ async getOrderItems({
current,
pageSize,
siteId, siteId,
startDate, startDate,
endDate, endDate,
sku, current,
pageSize,
name, name,
}: QueryOrderSalesDTO) { }: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : []; const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
@ -2539,7 +2548,7 @@ export class OrderService {
'姓名地址': nameAddress, '姓名地址': nameAddress,
'邮箱': order.customer_email || '', '邮箱': order.customer_email || '',
'号码': phone, '号码': phone,
'订单内容': orderContent, '订单内容': this.removeLastParenthesesContent(orderContent),
'盒数': boxCount, '盒数': boxCount,
'换盒数': exchangeBoxCount, '换盒数': exchangeBoxCount,
'换货内容': exchangeContent, '换货内容': exchangeContent,
@ -2639,4 +2648,85 @@ export class OrderService {
throw new Error(`导出CSV文件失败: ${error.message}`); throw new Error(`导出CSV文件失败: ${error.message}`);
} }
} }
/**
*
* @param str
* @returns
*/
removeLastParenthesesContent(str: string): string {
if (!str || typeof str !== 'string') {
return str;
}
// 辅助函数:删除指定位置的括号对及其内容
const removeParenthesesAt = (s: string, leftIndex: number): string => {
if (leftIndex === -1) return s;
let rightIndex = -1;
let parenCount = 0;
for (let i = leftIndex; i < s.length; i++) {
const char = s[i];
if (char === '(') {
parenCount++;
} else if (char === ')') {
parenCount--;
if (parenCount === 0) {
rightIndex = i;
break;
}
}
}
if (rightIndex !== -1) {
return s.substring(0, leftIndex) + s.substring(rightIndex + 1);
}
return s;
};
// 1. 处理每个分号前面的括号对
let result = str;
// 找出所有分号的位置
const semicolonIndices: number[] = [];
for (let i = 0; i < result.length; i++) {
if (result[i] === ';') {
semicolonIndices.push(i);
}
}
// 从后向前处理每个分号,避免位置变化影响后续处理
for (let i = semicolonIndices.length - 1; i >= 0; i--) {
const semicolonIndex = semicolonIndices[i];
// 从分号位置向前查找最近的左括号
let lastLeftParenIndex = -1;
for (let j = semicolonIndex - 1; j >= 0; j--) {
if (result[j] === '(') {
lastLeftParenIndex = j;
break;
}
}
// 如果找到左括号,删除该括号对及其内容
if (lastLeftParenIndex !== -1) {
result = removeParenthesesAt(result, lastLeftParenIndex);
}
}
// 2. 处理整个字符串的最后一个括号对
let lastLeftParenIndex = result.lastIndexOf('(');
if (lastLeftParenIndex !== -1) {
result = removeParenthesesAt(result, lastLeftParenIndex);
}
return result;
}
} }

View File

@ -1466,12 +1466,7 @@ export class ProductService {
price: num(rec.price), price: num(rec.price),
promotionPrice: num(rec.promotionPrice), promotionPrice: num(rec.promotionPrice),
type: val(rec.type), type: val(rec.type),
siteSkus: rec.siteSkus siteSkus: rec.siteSkus ? String(rec.siteSkus).split(',').map(s => s.trim()).filter(Boolean) : undefined,
? String(rec.siteSkus)
.split(/[;,]/) // 支持英文分号或英文逗号分隔
.map(s => s.trim())
.filter(Boolean)
: undefined,
category, // 添加分类字段 category, // 添加分类字段
attributes: attributes.length > 0 ? attributes : undefined, attributes: attributes.length > 0 ? attributes : undefined,
@ -1536,14 +1531,7 @@ export class ProductService {
return dto; return dto;
} }
getAttributesObject(attributes:DictItem[]){
if(!attributes) return {}
const obj:any = {}
attributes.forEach(attr=>{
obj[attr.dict.name] = attr
})
return obj
}
// 将单个产品转换为 CSV 行数组 // 将单个产品转换为 CSV 行数组
transformProductToCsvRow( transformProductToCsvRow(
p: Product, p: Product,