Compare commits
9 Commits
8d0eec06fd
...
5d7e0090aa
| Author | SHA1 | Date |
|---|---|---|
|
|
5d7e0090aa | |
|
|
ecdedcc041 | |
|
|
b2ee61e47d | |
|
|
64c1d1afe5 | |
|
|
4eb45af452 | |
|
|
a8d12a695e | |
|
|
a00a95c9a3 | |
|
|
82c8640f0c | |
|
|
cb00076bd3 |
|
|
@ -367,7 +367,6 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
date_paid: typeof item.pay_at === 'number'
|
||||
? item.pay_at === 0 ? null : new Date(item.pay_at * 1000).toISOString()
|
||||
: null,
|
||||
|
||||
refunds: [],
|
||||
currency_symbol: (currencySymbols[item.currency] || '$') || '',
|
||||
date_created:
|
||||
|
|
@ -387,6 +386,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
tracking_number: f.tracking_number || '',
|
||||
shipping_provider: f.tracking_company || '',
|
||||
shipping_method: f.tracking_company || '',
|
||||
|
||||
date_created: typeof f.created_at === 'number'
|
||||
? new Date(f.created_at * 1000).toISOString()
|
||||
: f.created_at || '',
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export class StatisticsController {
|
|||
|
||||
@ApiOkResponse()
|
||||
@Get('/orderSource')
|
||||
async getOrderSorce(@Query() params) {
|
||||
async getOrderSource(@Query() params) {
|
||||
try {
|
||||
return successResponse(await this.statisticsService.getOrderSorce(params));
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import { SiteService } from '../service/site.service';
|
|||
import { OrderService } from '../service/order.service';
|
||||
import { SiteApiService } from '../service/site-api.service';
|
||||
|
||||
|
||||
|
||||
@Controller('/webhook')
|
||||
export class WebhookController {
|
||||
private secret = 'YOONE24kd$kjcdjflddd';
|
||||
|
|
@ -177,20 +179,15 @@ export class WebhookController {
|
|||
console.log('Unhandled event:', topic);
|
||||
}
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
success: true,
|
||||
message: 'Webhook processed successfully',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
code: 403,
|
||||
success: false,
|
||||
message: 'Webhook verification failed',
|
||||
};
|
||||
return {
|
||||
code: 200,
|
||||
success: true,
|
||||
message: 'Webhook processed successfully',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -98,13 +98,9 @@ export class QueryOrderDTO {
|
|||
}
|
||||
|
||||
export class QueryOrderSalesDTO {
|
||||
@ApiProperty()
|
||||
@ApiProperty({ description: '是否为原产品还是库存产品' })
|
||||
@Rule(RuleType.bool().default(false))
|
||||
isSource: boolean;
|
||||
|
||||
@ApiProperty()
|
||||
@Rule(RuleType.bool().default(false))
|
||||
exceptPackage: boolean;
|
||||
isSource: boolean;
|
||||
|
||||
@ApiProperty({ example: '1', description: '页码' })
|
||||
@Rule(RuleType.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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -346,6 +346,7 @@ export interface ShopyyOrder {
|
|||
financial_status?: number;
|
||||
fulfillment_status?: number;
|
||||
// 创建与更新时间可能为时间戳
|
||||
date_paid?: number | string;
|
||||
created_at?: number | string;
|
||||
date_added?: string;
|
||||
updated_at?: number | string;
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export class UpdateSiteDTO {
|
|||
skuPrefix?: string;
|
||||
|
||||
// 区域
|
||||
@ApiProperty({ description: '区域' })
|
||||
@ApiProperty({ description: '区域', required: false })
|
||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||
areas?: string[];
|
||||
|
||||
|
|
@ -133,6 +133,10 @@ export class UpdateSiteDTO {
|
|||
@ApiProperty({ description: '站点网站URL', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
websiteUrl?: string;
|
||||
|
||||
@ApiProperty({ description: 'Webhook URL', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
webhookUrl?: string;
|
||||
}
|
||||
|
||||
export class QuerySiteDTO {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,10 @@ export class OrderStatisticsParams {
|
|||
@Rule(RuleType.number().allow(null))
|
||||
siteId?: number;
|
||||
|
||||
@ApiProperty()
|
||||
@Rule(RuleType.array().allow(null))
|
||||
country?: any[];
|
||||
|
||||
@ApiProperty({
|
||||
enum: ['all', 'first_purchase', 'repeat_purchase'],
|
||||
default: 'all',
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
import { Exclude, Expose } from 'class-transformer';
|
||||
import {
|
||||
BeforeInsert,
|
||||
BeforeUpdate,
|
||||
// BeforeInsert,
|
||||
// BeforeUpdate,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
|
|
@ -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
|
|
@ -39,6 +39,7 @@ import * as path from 'path';
|
|||
import * as os from 'os';
|
||||
import { UnifiedOrderDTO } from '../dto/site-api.dto';
|
||||
import { CustomerService } from './customer.service';
|
||||
import { ProductService } from './product.service';
|
||||
@Provide()
|
||||
export class OrderService {
|
||||
|
||||
|
|
@ -110,7 +111,9 @@ export class OrderService {
|
|||
|
||||
@Logger()
|
||||
logger; // 注入 Logger 实例
|
||||
|
||||
@Inject()
|
||||
productService: ProductService;
|
||||
|
||||
/**
|
||||
* 批量同步订单
|
||||
* 流程说明:
|
||||
|
|
@ -146,8 +149,8 @@ export class OrderService {
|
|||
const existingOrder = await this.orderModel.findOne({
|
||||
where: { externalOrderId: String(order.id), siteId: siteId },
|
||||
});
|
||||
if(!existingOrder){
|
||||
console.log("数据库中不存在",order.id, '订单状态:', order.status )
|
||||
if (!existingOrder) {
|
||||
console.log("数据库中不存在", order.id, '订单状态:', order.status)
|
||||
}
|
||||
// 同步单个订单
|
||||
await this.syncSingleOrder(siteId, order);
|
||||
|
|
@ -208,8 +211,8 @@ export class OrderService {
|
|||
const existingOrder = await this.orderModel.findOne({
|
||||
where: { externalOrderId: String(order.id), siteId: siteId },
|
||||
});
|
||||
if(!existingOrder){
|
||||
console.log("数据库不存在", siteId , "订单:",order.id, '订单状态:' + order.status )
|
||||
if (!existingOrder) {
|
||||
console.log("数据库不存在", siteId, "订单:", order.id, '订单状态:' + order.status)
|
||||
}
|
||||
// 同步单个订单
|
||||
await this.syncSingleOrder(siteId, order, true);
|
||||
|
|
@ -268,7 +271,7 @@ export class OrderService {
|
|||
try {
|
||||
const site = await this.siteService.get(siteId);
|
||||
// 仅处理 WooCommerce 站点
|
||||
if(site.type !== 'woocommerce'){
|
||||
if (site.type !== 'woocommerce') {
|
||||
return
|
||||
}
|
||||
// 将订单状态同步到 WooCommerce,然后切换至下一状态
|
||||
|
|
@ -278,6 +281,11 @@ export class OrderService {
|
|||
console.error('更新订单状态失败,原因为:', error)
|
||||
}
|
||||
}
|
||||
async getOrderByExternalOrderId(siteId: number, externalOrderId: string) {
|
||||
return await this.orderModel.findOne({
|
||||
where: { externalOrderId: String(externalOrderId), siteId },
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 同步单个订单
|
||||
* 流程说明:
|
||||
|
|
@ -301,7 +309,7 @@ export class OrderService {
|
|||
* @param order 订单数据
|
||||
* @param forceUpdate 是否强制更新
|
||||
*/
|
||||
async syncSingleOrder(siteId: number, order: any, forceUpdate = false) {
|
||||
async syncSingleOrder(siteId: number, order: UnifiedOrderDTO, forceUpdate = false) {
|
||||
// 从订单数据中解构出各个子项
|
||||
let {
|
||||
line_items,
|
||||
|
|
@ -315,47 +323,28 @@ 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: order.id, siteId: siteId },
|
||||
});
|
||||
// 自动更新订单状态(如果需要)
|
||||
// 这里其实不用过滤不可编辑的行为,而是应在 save 中做判断
|
||||
// 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);
|
||||
}
|
||||
// 更新
|
||||
await this.orderModel.update({ externalOrderId: order.id, siteId: siteId }, updateData);
|
||||
// 更新 fulfillments 数据
|
||||
await this.saveOrderFulfillments({
|
||||
siteId,
|
||||
orderId: existingOrder.id,
|
||||
externalOrderId:order.id,
|
||||
fulfillments: fulfillments,
|
||||
});
|
||||
}
|
||||
const externalOrderId = 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({
|
||||
|
|
@ -459,13 +448,14 @@ export class OrderService {
|
|||
* @param order 订单数据
|
||||
* @returns 保存后的订单实体
|
||||
*/
|
||||
async saveOrder(siteId: number, order: UnifiedOrderDTO): Promise<Order> {
|
||||
// 这里 omit 是因为处理在外头了 其实 saveOrder 应该包括 savelineitems 等
|
||||
async saveOrder(siteId: number, order: Omit<UnifiedOrderDTO, 'line_items' | 'refunds'>): Promise<Order> {
|
||||
// 将外部订单ID转换为字符串
|
||||
const externalOrderId = String(order.id)
|
||||
const externalOrderId = String(order.id)
|
||||
delete order.id
|
||||
|
||||
|
||||
// 创建订单实体对象
|
||||
const entity = plainToClass(Order, {...order, externalOrderId, siteId});
|
||||
const entity = plainToClass(Order, { ...order, externalOrderId, siteId });
|
||||
// 检查数据库中是否已存在该订单
|
||||
const existingOrder = await this.orderModel.findOne({
|
||||
where: { externalOrderId, siteId: siteId },
|
||||
|
|
@ -708,6 +698,8 @@ export class OrderService {
|
|||
*
|
||||
* @param orderItem 订单项实体
|
||||
*/
|
||||
// TODO 这里存的是库存商品实际
|
||||
// 所以叫做 orderInventoryItems 可能更合适
|
||||
async saveOrderSale(orderItem: OrderItem) {
|
||||
const currentOrderSale = await this.orderSaleModel.find({
|
||||
where: {
|
||||
|
|
@ -726,46 +718,47 @@ 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);
|
||||
}
|
||||
relations: ['components', 'attributes'],
|
||||
}),
|
||||
quantity: comp.quantity * orderItem.quantity,
|
||||
}
|
||||
} else {
|
||||
const orderSaleItem: OrderSale = plainToClass(OrderSale, {
|
||||
})) : [{ product, quantity: orderItem.quantity }]
|
||||
|
||||
const orderSales: OrderSale[] = componentDetails.map(componentDetail => {
|
||||
if (!componentDetail.product) return null
|
||||
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',
|
||||
isZex: attrsObj?.['brand']?.name === 'zex',
|
||||
isYooneNew: attrsObj?.['brand']?.name === 'yoone' && attrsObj?.['version']?.name === 'new',
|
||||
size: this.extractNumberFromString(attrsObj?.['strength']?.name) || null,
|
||||
});
|
||||
orderSales.push(orderSaleItem);
|
||||
}
|
||||
return orderSale
|
||||
}).filter(v => v !== null)
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存订单退款信息
|
||||
|
|
@ -1426,7 +1419,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');
|
||||
|
|
@ -1642,11 +1635,12 @@ export class OrderService {
|
|||
* @returns 订单项统计和分页信息
|
||||
*/
|
||||
async getOrderItems({
|
||||
current,
|
||||
pageSize,
|
||||
siteId,
|
||||
startDate,
|
||||
endDate,
|
||||
current,
|
||||
pageSize,
|
||||
sku,
|
||||
name,
|
||||
}: QueryOrderSalesDTO) {
|
||||
const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
|
||||
|
|
@ -1904,8 +1898,8 @@ export class OrderService {
|
|||
const key = it?.externalSubscriptionId
|
||||
? `sub:${it.externalSubscriptionId}`
|
||||
: it?.externalOrderId
|
||||
? `ord:${it.externalOrderId}`
|
||||
: `id:${it?.id}`;
|
||||
? `ord:${it.externalOrderId}`
|
||||
: `id:${it?.id}`;
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
relatedList.push(it);
|
||||
|
|
@ -2199,14 +2193,14 @@ export class OrderService {
|
|||
for (const sale of sales) {
|
||||
const product = await productRepo.findOne({ where: { sku: sale.sku } });
|
||||
const saleItem = {
|
||||
orderId: order.id,
|
||||
siteId: order.siteId,
|
||||
externalOrderItemId: '-1',
|
||||
productId: product.id,
|
||||
name: product.name,
|
||||
sku: sale.sku,
|
||||
quantity: sale.quantity,
|
||||
};
|
||||
orderId: order.id,
|
||||
siteId: order.siteId,
|
||||
externalOrderItemId: '-1',
|
||||
productId: product.id,
|
||||
name: product.name,
|
||||
sku: sale.sku,
|
||||
quantity: sale.quantity,
|
||||
};
|
||||
await orderSaleRepo.save(saleItem);
|
||||
}
|
||||
});
|
||||
|
|
@ -2339,83 +2333,83 @@ export class OrderService {
|
|||
//换货功能更新OrderSale和Orderitem数据
|
||||
async updateExchangeOrder(orderId: number, data: any) {
|
||||
throw new Error('暂未实现')
|
||||
// try {
|
||||
// const dataSource = this.dataSourceManager.getDataSource('default');
|
||||
// let transactionError = undefined;
|
||||
// try {
|
||||
// const dataSource = this.dataSourceManager.getDataSource('default');
|
||||
// let transactionError = undefined;
|
||||
|
||||
// await dataSource.transaction(async manager => {
|
||||
// const orderRepo = manager.getRepository(Order);
|
||||
// const orderSaleRepo = manager.getRepository(OrderSale);
|
||||
// const orderItemRepo = manager.getRepository(OrderItem);
|
||||
// await dataSource.transaction(async manager => {
|
||||
// const orderRepo = manager.getRepository(Order);
|
||||
// const orderSaleRepo = manager.getRepository(OrderSale);
|
||||
// const orderItemRepo = manager.getRepository(OrderItem);
|
||||
|
||||
|
||||
// const productRepo = manager.getRepository(ProductV2);
|
||||
// const productRepo = manager.getRepository(ProductV2);
|
||||
|
||||
// const order = await orderRepo.findOneBy({ id: orderId });
|
||||
// let product: ProductV2;
|
||||
// const order = await orderRepo.findOneBy({ id: orderId });
|
||||
// let product: ProductV2;
|
||||
|
||||
// await orderSaleRepo.delete({ orderId });
|
||||
// await orderItemRepo.delete({ orderId });
|
||||
// for (const sale of data['sales']) {
|
||||
// product = await productRepo.findOneBy({ sku: sale['sku'] });
|
||||
// await orderSaleRepo.save({
|
||||
// orderId,
|
||||
// siteId: order.siteId,
|
||||
// productId: product.id,
|
||||
// name: product.name,
|
||||
// sku: sale['sku'],
|
||||
// quantity: sale['quantity'],
|
||||
// });
|
||||
// };
|
||||
// await orderSaleRepo.delete({ orderId });
|
||||
// await orderItemRepo.delete({ orderId });
|
||||
// for (const sale of data['sales']) {
|
||||
// product = await productRepo.findOneBy({ sku: sale['sku'] });
|
||||
// await orderSaleRepo.save({
|
||||
// orderId,
|
||||
// siteId: order.siteId,
|
||||
// productId: product.id,
|
||||
// name: product.name,
|
||||
// sku: sale['sku'],
|
||||
// quantity: sale['quantity'],
|
||||
// });
|
||||
// };
|
||||
|
||||
// for (const item of data['items']) {
|
||||
// product = await productRepo.findOneBy({ sku: item['sku'] });
|
||||
// for (const item of data['items']) {
|
||||
// product = await productRepo.findOneBy({ sku: item['sku'] });
|
||||
|
||||
// await orderItemRepo.save({
|
||||
// orderId,
|
||||
// siteId: order.siteId,
|
||||
// productId: product.id,
|
||||
// name: product.name,
|
||||
// externalOrderId: order.externalOrderId,
|
||||
// externalProductId: product.externalProductId,
|
||||
// await orderItemRepo.save({
|
||||
// orderId,
|
||||
// siteId: order.siteId,
|
||||
// productId: product.id,
|
||||
// name: product.name,
|
||||
// externalOrderId: order.externalOrderId,
|
||||
// externalProductId: product.externalProductId,
|
||||
|
||||
// sku: item['sku'],
|
||||
// quantity: item['quantity'],
|
||||
// });
|
||||
// sku: item['sku'],
|
||||
// quantity: item['quantity'],
|
||||
// });
|
||||
|
||||
// };
|
||||
// };
|
||||
|
||||
// //将是否换货状态改为true
|
||||
// await orderRepo.update(
|
||||
// order.id
|
||||
// , {
|
||||
// is_exchange: true
|
||||
// });
|
||||
// //将是否换货状态改为true
|
||||
// await orderRepo.update(
|
||||
// order.id
|
||||
// , {
|
||||
// is_exchange: true
|
||||
// });
|
||||
|
||||
// //查询这个用户换过多少次货
|
||||
// const counts = await orderRepo.countBy({
|
||||
// is_editable: true,
|
||||
// customer_email: order.customer_email,
|
||||
// });
|
||||
// //查询这个用户换过多少次货
|
||||
// const counts = await orderRepo.countBy({
|
||||
// is_editable: true,
|
||||
// customer_email: order.customer_email,
|
||||
// });
|
||||
|
||||
// //批量更新当前用户换货次数
|
||||
// await orderRepo.update({
|
||||
// customer_email: order.customer_email
|
||||
// }, {
|
||||
// exchange_frequency: counts
|
||||
// });
|
||||
// //批量更新当前用户换货次数
|
||||
// await orderRepo.update({
|
||||
// customer_email: order.customer_email
|
||||
// }, {
|
||||
// exchange_frequency: counts
|
||||
// });
|
||||
|
||||
// }).catch(error => {
|
||||
// transactionError = error;
|
||||
// });
|
||||
// }).catch(error => {
|
||||
// transactionError = error;
|
||||
// });
|
||||
|
||||
// if (transactionError !== undefined) {
|
||||
// throw new Error(`更新物流信息错误:${transactionError.message}`);
|
||||
// }
|
||||
// return true;
|
||||
// } catch (error) {
|
||||
// throw new Error(`更新发货产品失败:${error.message}`);
|
||||
// }
|
||||
// if (transactionError !== undefined) {
|
||||
// throw new Error(`更新物流信息错误:${transactionError.message}`);
|
||||
// }
|
||||
// return true;
|
||||
// } catch (error) {
|
||||
// throw new Error(`更新发货产品失败:${error.message}`);
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -2461,17 +2455,17 @@ export class OrderService {
|
|||
}
|
||||
|
||||
try {
|
||||
|
||||
|
||||
// 过滤掉NaN和非数字值,只保留有效的数字ID
|
||||
const validIds = ids?.filter?.(id => Number.isFinite(id) && id > 0);
|
||||
|
||||
|
||||
const dataSource = this.dataSourceManager.getDataSource('default');
|
||||
|
||||
|
||||
// 优化事务使用
|
||||
return await dataSource.transaction(async manager => {
|
||||
// 准备查询条件
|
||||
const whereCondition: any = {};
|
||||
if(validIds.length > 0){
|
||||
if (validIds.length > 0) {
|
||||
whereCondition.id = In(validIds);
|
||||
}
|
||||
|
||||
|
|
@ -2487,7 +2481,7 @@ export class OrderService {
|
|||
|
||||
// 获取所有订单ID
|
||||
const orderIds = orders.map(order => order.id);
|
||||
|
||||
|
||||
// 获取所有订单项
|
||||
const orderItems = await manager.getRepository(OrderItem).find({
|
||||
where: {
|
||||
|
|
@ -2508,13 +2502,13 @@ export class OrderService {
|
|||
const exportDataList: ExportData[] = orders.map(order => {
|
||||
// 获取订单的订单项
|
||||
const items = orderItemsByOrderId[order.id] || [];
|
||||
|
||||
|
||||
// 计算总盒数
|
||||
const boxCount = items.reduce((total, item) => total + item.quantity, 0);
|
||||
|
||||
|
||||
// 构建订单内容
|
||||
const orderContent = items.map(item => `${item.name} (${item.sku || ''}) x ${item.quantity}`).join('; ');
|
||||
|
||||
|
||||
// 构建姓名地址
|
||||
const shipping = order.shipping;
|
||||
const billing = order.billing;
|
||||
|
|
@ -2528,10 +2522,10 @@ export class OrderService {
|
|||
const postcode = shipping?.postcode || billing?.postcode || '';
|
||||
const country = shipping?.country || billing?.country || '';
|
||||
const nameAddress = `${name} ${address} ${address2} ${city} ${state} ${postcode} ${country}`;
|
||||
|
||||
|
||||
// 获取电话号码
|
||||
const phone = shipping?.phone || billing?.phone || '';
|
||||
|
||||
|
||||
// 获取快递号
|
||||
const trackingNumber = order.shipment?.tracking_id || '';
|
||||
|
||||
|
|
@ -2567,85 +2561,82 @@ export class OrderService {
|
|||
* 导出数据为CSV格式
|
||||
* @param {any[]} data 数据数组
|
||||
* @param {Object} options 配置选项
|
||||
* @param {string} [options.type='string'] 输出类型:'string' | 'buffer'
|
||||
* @param {string} [options.fileName] 文件名(仅当需要写入文件时使用)
|
||||
* @param {string} [options.type='string'] 输出类型:'string' | 'buffer'
|
||||
* @param {string} [options.fileName] 文件名(仅当需要写入文件时使用)
|
||||
* @param {boolean} [options.writeFile=false] 是否写入文件
|
||||
* @returns {string|Buffer} 根据type返回字符串或Buffer
|
||||
*/
|
||||
async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: string; writeFile?: boolean } = {}): Promise<string | Buffer> {
|
||||
try {
|
||||
// 检查数据是否为空
|
||||
if (!data || data.length === 0) {
|
||||
throw new Error('导出数据不能为空');
|
||||
}
|
||||
|
||||
const { type = 'string', fileName, writeFile = false } = options;
|
||||
|
||||
// 生成表头
|
||||
const headers = Object.keys(data[0]);
|
||||
let csvContent = headers.join(',') + '\n';
|
||||
|
||||
// 处理数据行
|
||||
data.forEach(item => {
|
||||
const row = headers.map(key => {
|
||||
const value = item[key as keyof any];
|
||||
// 处理特殊字符
|
||||
if (typeof value === 'string') {
|
||||
// 转义双引号,将"替换为""
|
||||
const escapedValue = value.replace(/"/g, '""');
|
||||
// 如果包含逗号或换行符,需要用双引号包裹
|
||||
if (escapedValue.includes(',') || escapedValue.includes('\n')) {
|
||||
return `"${escapedValue}"`;
|
||||
}
|
||||
return escapedValue;
|
||||
}
|
||||
// 处理日期类型
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
// 处理undefined和null
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
return String(value);
|
||||
}).join(',');
|
||||
csvContent += row + '\n';
|
||||
});
|
||||
|
||||
// 如果需要写入文件
|
||||
if (writeFile && fileName) {
|
||||
// 获取当前用户目录
|
||||
const userHomeDir = os.homedir();
|
||||
|
||||
// 构建目标路径(下载目录)
|
||||
const downloadsDir = path.join(userHomeDir, 'Downloads');
|
||||
|
||||
// 确保下载目录存在
|
||||
if (!fs.existsSync(downloadsDir)) {
|
||||
fs.mkdirSync(downloadsDir, { recursive: true });
|
||||
async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: string; writeFile?: boolean } = {}): Promise<string | Buffer> {
|
||||
try {
|
||||
// 检查数据是否为空
|
||||
if (!data || data.length === 0) {
|
||||
throw new Error('导出数据不能为空');
|
||||
}
|
||||
|
||||
const filePath = path.join(downloadsDir, fileName);
|
||||
|
||||
// 写入文件
|
||||
fs.writeFileSync(filePath, csvContent, 'utf8');
|
||||
|
||||
console.log(`数据已成功导出至 ${filePath}`);
|
||||
return filePath;
|
||||
|
||||
const { type = 'string', fileName, writeFile = false } = options;
|
||||
|
||||
// 生成表头
|
||||
const headers = Object.keys(data[0]);
|
||||
let csvContent = headers.join(',') + '\n';
|
||||
|
||||
// 处理数据行
|
||||
data.forEach(item => {
|
||||
const row = headers.map(key => {
|
||||
const value = item[key as keyof any];
|
||||
// 处理特殊字符
|
||||
if (typeof value === 'string') {
|
||||
// 转义双引号,将"替换为""
|
||||
const escapedValue = value.replace(/"/g, '""');
|
||||
// 如果包含逗号或换行符,需要用双引号包裹
|
||||
if (escapedValue.includes(',') || escapedValue.includes('\n')) {
|
||||
return `"${escapedValue}"`;
|
||||
}
|
||||
return escapedValue;
|
||||
}
|
||||
// 处理日期类型
|
||||
if (value instanceof Date) {
|
||||
return value.toISOString();
|
||||
}
|
||||
// 处理undefined和null
|
||||
if (value === undefined || value === null) {
|
||||
return '';
|
||||
}
|
||||
return String(value);
|
||||
}).join(',');
|
||||
csvContent += row + '\n';
|
||||
});
|
||||
|
||||
// 如果需要写入文件
|
||||
if (writeFile && fileName) {
|
||||
// 获取当前用户目录
|
||||
const userHomeDir = os.homedir();
|
||||
|
||||
// 构建目标路径(下载目录)
|
||||
const downloadsDir = path.join(userHomeDir, 'Downloads');
|
||||
|
||||
// 确保下载目录存在
|
||||
if (!fs.existsSync(downloadsDir)) {
|
||||
fs.mkdirSync(downloadsDir, { recursive: true });
|
||||
}
|
||||
|
||||
const filePath = path.join(downloadsDir, fileName);
|
||||
|
||||
// 写入文件
|
||||
fs.writeFileSync(filePath, csvContent, 'utf8');
|
||||
|
||||
console.log(`数据已成功导出至 ${filePath}`);
|
||||
return filePath;
|
||||
}
|
||||
|
||||
// 根据类型返回不同结果
|
||||
if (type === 'buffer') {
|
||||
return Buffer.from(csvContent, 'utf8');
|
||||
}
|
||||
|
||||
return csvContent;
|
||||
} catch (error) {
|
||||
console.error('导出CSV时出错:', error);
|
||||
throw new Error(`导出CSV文件失败: ${error.message}`);
|
||||
}
|
||||
|
||||
// 根据类型返回不同结果
|
||||
if (type === 'buffer') {
|
||||
return Buffer.from(csvContent, 'utf8');
|
||||
}
|
||||
|
||||
return csvContent;
|
||||
} catch (error) {
|
||||
console.error('导出CSV时出错:', error);
|
||||
throw new Error(`导出CSV文件失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1461,12 +1461,17 @@ export class ProductService {
|
|||
return {
|
||||
sku,
|
||||
name: val(rec.name),
|
||||
nameCn: val(rec.nameCn),
|
||||
nameCn: val(rec.nameCn),
|
||||
description: val(rec.description),
|
||||
price: num(rec.price),
|
||||
promotionPrice: num(rec.promotionPrice),
|
||||
type: val(rec.type),
|
||||
siteSkus: rec.siteSkus ? String(rec.siteSkus).split(',').map(s => s.trim()).filter(Boolean) : undefined,
|
||||
siteSkus: rec.siteSkus
|
||||
? String(rec.siteSkus)
|
||||
.split(/[;,]/) // 支持英文分号或英文逗号分隔
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
: undefined,
|
||||
category, // 添加分类字段
|
||||
|
||||
attributes: attributes.length > 0 ? attributes : undefined,
|
||||
|
|
@ -1531,7 +1536,14 @@ export class ProductService {
|
|||
|
||||
return dto;
|
||||
}
|
||||
|
||||
getAttributesObject(attributes:DictItem[]){
|
||||
if(!attributes) return {}
|
||||
const obj:any = {}
|
||||
attributes.forEach(attr=>{
|
||||
obj[attr.dict.name] = attr
|
||||
})
|
||||
return obj
|
||||
}
|
||||
// 将单个产品转换为 CSV 行数组
|
||||
transformProductToCsvRow(
|
||||
p: Product,
|
||||
|
|
|
|||
|
|
@ -15,8 +15,19 @@ export class StatisticsService {
|
|||
orderItemRepository: Repository<OrderItem>;
|
||||
|
||||
async getOrderStatistics(params: OrderStatisticsParams) {
|
||||
const { startDate, endDate, grouping, siteId } = params;
|
||||
const { startDate, endDate, grouping, siteId, country } = params;
|
||||
// const keywords = keyword ? keyword.split(' ').filter(Boolean) : [];
|
||||
|
||||
let siteIds = []
|
||||
if (country) {
|
||||
siteIds = await this.getSiteIds(country)
|
||||
}
|
||||
|
||||
if (siteId) {
|
||||
siteIds.push(siteId)
|
||||
}
|
||||
|
||||
|
||||
const start = dayjs(startDate).format('YYYY-MM-DD');
|
||||
const end = dayjs(endDate).add(1, 'd').format('YYYY-MM-DD');
|
||||
let sql
|
||||
|
|
@ -54,6 +65,8 @@ export class StatisticsService {
|
|||
AND o.status IN('processing','completed')
|
||||
`;
|
||||
if (siteId) sql += ` AND o.siteId=${siteId}`;
|
||||
if (siteIds.length) sql += ` AND o.siteId IN (${siteIds.join(',')})`;
|
||||
|
||||
sql += `
|
||||
GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source
|
||||
),
|
||||
|
|
@ -247,7 +260,10 @@ export class StatisticsService {
|
|||
LEFT JOIN order_item oi ON o.id = oi.orderId
|
||||
WHERE o.date_paid IS NOT NULL
|
||||
AND o.date_paid >= '${start}' AND o.date_paid < '${end}'
|
||||
AND o.status IN ('processing','completed')
|
||||
AND o.status IN ('processing','completed')`;
|
||||
if (siteId) sql += ` AND o.siteId=${siteId}`;
|
||||
if (siteIds.length) sql += ` AND o.siteId IN (${siteIds.join(',')})`;
|
||||
sql +=`
|
||||
GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source
|
||||
),
|
||||
order_sales_summary AS (
|
||||
|
|
@ -439,7 +455,11 @@ export class StatisticsService {
|
|||
LEFT JOIN first_order f ON o.customer_email = f.customer_email
|
||||
LEFT JOIN order_item oi ON o.id = oi.orderId
|
||||
WHERE o.date_paid IS NOT NULL
|
||||
AND o.date_paid >= '${start}' AND o.date_paid < '${end}'
|
||||
AND o.date_paid >= '${start}' AND o.date_paid < '${end}'
|
||||
`;
|
||||
if (siteId) sql += ` AND o.siteId=${siteId}`;
|
||||
if (siteIds.length) sql += ` AND o.siteId IN (${siteIds.join(',')})`;
|
||||
sql +=`
|
||||
AND o.status IN ('processing','completed')
|
||||
GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source
|
||||
),
|
||||
|
|
@ -1314,7 +1334,14 @@ export class StatisticsService {
|
|||
}
|
||||
|
||||
async getOrderSorce(params) {
|
||||
const sql = `
|
||||
const { country } = params;
|
||||
|
||||
let siteIds = []
|
||||
if (country) {
|
||||
siteIds = await this.getSiteIds(country)
|
||||
}
|
||||
|
||||
let sql = `
|
||||
WITH cutoff_months AS (
|
||||
SELECT
|
||||
DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 7 MONTH), '%Y-%m') AS start_month,
|
||||
|
|
@ -1326,7 +1353,10 @@ export class StatisticsService {
|
|||
DATE_FORMAT(MIN(date_paid), '%Y-%m') AS first_order_month,
|
||||
SUM(total) AS first_order_total
|
||||
FROM \`order\`
|
||||
WHERE status IN ('processing', 'completed')
|
||||
WHERE status IN ('processing', 'completed')`;
|
||||
if (siteIds.length!=0) sql += ` AND siteId IN ('${siteIds.join("','")}')`;
|
||||
else sql += ` AND siteId IS NULL `;
|
||||
sql += `
|
||||
GROUP BY customer_email
|
||||
),
|
||||
order_months AS (
|
||||
|
|
@ -1334,7 +1364,10 @@ export class StatisticsService {
|
|||
customer_email,
|
||||
DATE_FORMAT(date_paid, '%Y-%m') AS order_month
|
||||
FROM \`order\`
|
||||
WHERE status IN ('processing', 'completed')
|
||||
WHERE status IN ('processing', 'completed')`;
|
||||
if (siteIds.length!=0) sql += ` AND siteId IN ('${siteIds.join("','")}')`;
|
||||
else sql += ` AND siteId IS NULL `;
|
||||
sql += `
|
||||
),
|
||||
filtered_orders AS (
|
||||
SELECT o.customer_email, o.order_month, u.first_order_month,u.first_order_total, c.start_month
|
||||
|
|
@ -1366,7 +1399,7 @@ export class StatisticsService {
|
|||
ORDER BY order_month DESC, first_order_month_group
|
||||
`
|
||||
|
||||
const inactiveSql = `
|
||||
let inactiveSql = `
|
||||
WITH
|
||||
cutoff_months AS (
|
||||
SELECT
|
||||
|
|
@ -1381,7 +1414,10 @@ export class StatisticsService {
|
|||
date_paid,
|
||||
total
|
||||
FROM \`order\`
|
||||
WHERE status IN ('processing', 'completed')
|
||||
WHERE status IN ('processing', 'completed')`;
|
||||
if (siteIds.length!=0) inactiveSql += ` AND siteId IN ('${siteIds.join("','")}')`;
|
||||
else inactiveSql += ` AND siteId IS NULL `;
|
||||
inactiveSql += `
|
||||
),
|
||||
|
||||
filtered_users AS (
|
||||
|
|
@ -1524,4 +1560,13 @@ export class StatisticsService {
|
|||
|
||||
}
|
||||
|
||||
async getSiteIds(country: any[]) {
|
||||
const sql = `
|
||||
SELECT DISTINCT sa.siteId as site_id FROM area a left join site_areas_area sa on a.id = sa.areaId WHERE a.code IN ('${country.join("','")}')
|
||||
`
|
||||
const res = await this.orderRepository.query(sql)
|
||||
return res.map(item => item.site_id)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue