diff --git a/src/controller/order.controller.ts b/src/controller/order.controller.ts index 8aa7b5f..f55e62b 100644 --- a/src/controller/order.controller.ts +++ b/src/controller/order.controller.ts @@ -252,4 +252,21 @@ export class OrderController { return errorResponse(error?.message || '获取失败'); } } + + + @ApiOkResponse() + @Get('/order/export') + async exportOrder( + @Query() queryParams: any + ) { + // 处理 ids 参数,支持多种格式:ids=1,2,3、ids[]=1&ids[]=2、ids=1 + + try { + const csv = await this.orderService.exportOrder(queryParams?.ids); + // 返回CSV内容给前端,由前端决定是否下载 + return successResponse({ csv }); + } catch (error) { + return errorResponse(error?.message || '导出失败'); + } + } } diff --git a/src/dto/statistics.dto.ts b/src/dto/statistics.dto.ts index 31e10a8..77ee5b7 100644 --- a/src/dto/statistics.dto.ts +++ b/src/dto/statistics.dto.ts @@ -33,4 +33,8 @@ export class OrderStatisticsParams { @ApiProperty({ enum: ['all', 'zyn', 'yoone', 'zolt'], default: 'all' }) @Rule(RuleType.string().valid('all', 'zyn', 'yoone', 'zolt')) brand: string; + + @ApiProperty({ enum: ['day', 'week', 'month'], default: 'day' }) + @Rule(RuleType.string().valid('day', 'week', 'month')) + grouping: string; } diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 76e630a..c2a4a5f 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -33,7 +33,9 @@ import { ShipmentItem } from '../entity/shipment_item.entity'; import { UpdateStockDTO } from '../dto/stock.dto'; import { StockService } from './stock.service'; import { OrderItemOriginal } from '../entity/order_item_original.entity'; - +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; @Provide() export class OrderService { @@ -1702,4 +1704,204 @@ export class OrderService { } } + + + async exportOrder(ids: number[]) { + // 日期 订单号 姓名地址 邮箱 号码 订单内容 盒数 换盒数 换货内容 快递号 + interface ExportData { + '日期': string; + '订单号': string; + '姓名地址': string; + '邮箱': string; + '号码': string; + '订单内容': string; + '盒数': number; + '换盒数': number; + '换货内容': string; + '快递号': string; + } + + try { + // 空值检查 + const dataSource = this.dataSourceManager.getDataSource('default'); + + // 优化事务使用 + return await dataSource.transaction(async manager => { + // 准备查询条件 + const whereCondition: any = {}; + if (ids && ids.length > 0) { + whereCondition.id = In(ids); + } + + // 获取订单、订单项和物流信息 + const orders = await manager.getRepository(Order).find({ + where: whereCondition, + relations: ['shipment'] + }); + + if (orders.length === 0) { + throw new Error('未找到匹配的订单'); + } + + // 获取所有订单ID + const orderIds = orders.map(order => order.id); + + // 获取所有订单项 + const orderItems = await manager.getRepository(OrderItem).find({ + where: { + orderId: In(orderIds) + } + }); + + // 按订单ID分组订单项 + const orderItemsByOrderId = orderItems.reduce((acc, item) => { + if (!acc[item.orderId]) { + acc[item.orderId] = []; + } + acc[item.orderId].push(item); + return acc; + }, {} as Record); + + // 构建导出数据 + 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; + const firstName = shipping?.first_name || billing?.first_name || ''; + const lastName = shipping?.last_name || billing?.last_name || ''; + const name = `${firstName} ${lastName}`.trim() || ''; + const address = shipping?.address_1 || billing?.address_1 || ''; + const address2 = shipping?.address_2 || billing?.address_2 || ''; + const city = shipping?.city || billing?.city || ''; + const state = shipping?.state || billing?.state || ''; + 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 || ''; + + // 暂时没有换货相关数据,默认为0和空字符串 + const exchangeBoxCount = 0; + const exchangeContent = ''; + + return { + '日期': order.date_created?.toISOString().split('T')[0] || '', + '订单号': order.externalOrderId || '', + '姓名地址': nameAddress, + '邮箱': order.customer_email || '', + '号码': phone, + '订单内容': orderContent, + '盒数': boxCount, + '换盒数': exchangeBoxCount, + '换货内容': exchangeContent, + '快递号': trackingNumber + }; + }); + + // 返回CSV字符串内容给前端 + const csvContent = await this.exportToCsv(exportDataList, { type: 'string' }); + return csvContent; + }); + } catch (error) { + throw new Error(`导出订单失败:${error.message}`); + } + } + + + /** + * 导出数据为CSV格式 + * @param {any[]} data 数据数组 + * @param {Object} options 配置选项 + * @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 { + 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 }); + } + + 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}`); + } +} + } diff --git a/src/service/site.service.ts b/src/service/site.service.ts index 4585bf0..53a89f9 100644 --- a/src/service/site.service.ts +++ b/src/service/site.service.ts @@ -22,6 +22,10 @@ export class SiteService { async syncFromConfig(sites: WpSite[] = []) { // 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化) for (const siteConfig of sites) { + if (!siteConfig.name) { + console.warn('跳过空名称的站点配置'); + continue; + } // 按站点名称查询是否已存在记录 const exist = await this.siteModel.findOne({ where: { name: siteConfig.name }, diff --git a/src/service/statistics.service.ts b/src/service/statistics.service.ts index 71804ce..be8939f 100644 --- a/src/service/statistics.service.ts +++ b/src/service/statistics.service.ts @@ -15,12 +15,14 @@ export class StatisticsService { orderItemRepository: Repository; async getOrderStatistics(params: OrderStatisticsParams) { - const { startDate, endDate, siteId } = params; + const { startDate, endDate, siteId ,grouping} = params; // const keywords = keyword ? keyword.split(' ').filter(Boolean) : []; const start = dayjs(startDate).format('YYYY-MM-DD'); const end = dayjs(endDate).add(1, 'd').format('YYYY-MM-DD'); - let sql = ` - WITH first_order AS ( + + let sql = ''; + if (!grouping || grouping === 'day') { + sql = `WITH first_order AS ( SELECT customer_email, MIN(date_paid) AS first_purchase_date FROM \`order\` GROUP BY customer_email @@ -214,6 +216,393 @@ export class StatisticsService { dt.can_total_orders ORDER BY d.order_date DESC; `; + }else if (grouping === 'week') { + sql = `WITH first_order AS ( + SELECT customer_email, MIN(date_paid) AS first_purchase_date + FROM \`order\` + GROUP BY customer_email + ), + weekly_orders AS ( + SELECT + o.id AS order_id, + DATE_FORMAT(o.date_paid, '%Y-%u') AS order_date, + o.customer_email, + o.total, + o.source_type, + o.utm_source, + o.siteId, + CASE + WHEN o.date_paid = f.first_purchase_date THEN 'first_purchase' + ELSE 'repeat_purchase' + END AS purchase_type, + CASE + WHEN o.source_type = 'utm' AND o.utm_source = 'google' THEN 'cpc' + ELSE 'non_cpc' + END AS order_type, + MAX(CASE WHEN oi.name LIKE '%zyn%' THEN 'zyn' ELSE 'non_zyn' END) AS zyn_type, + MAX(CASE WHEN oi.name LIKE '%yoone%' THEN 'yoone' ELSE 'non_yoone' END) AS yoone_type, + MAX(CASE WHEN oi.name LIKE '%zex%' THEN 'zex' ELSE 'non_zex' END) AS zex_type + FROM \`order\` o + 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.status IN ('processing','completed') + GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source + ), + order_sales_summary AS ( + SELECT + orderId, + SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity, + SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity, + SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity + FROM order_sale + GROUP BY orderId + ), + order_items_summary AS ( + SELECT + orderId, + SUM(CASE WHEN name LIKE '%zyn%' THEN total + total_tax ELSE 0 END) AS zyn_amount, + SUM(CASE WHEN name LIKE '%yoone%' THEN total + total_tax ELSE 0 END) AS yoone_amount, + SUM(CASE WHEN name LIKE '%zex%' THEN total + total_tax ELSE 0 END) AS zex_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%package%' THEN total + total_tax ELSE 0 END) AS yoone_G_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name NOT LIKE '%package%' THEN total + total_tax ELSE 0 END) AS yoone_S_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN total + total_tax ELSE 0 END) AS yoone_3_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN total + total_tax ELSE 0 END) AS yoone_6_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN total + total_tax ELSE 0 END) AS yoone_9_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN total + total_tax ELSE 0 END) AS yoone_12_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN total + total_tax ELSE 0 END) AS yoone_15_amount + FROM order_item + GROUP BY orderId + ), + weekly_totals AS ( + SELECT order_date, SUM(total) AS total_amount, + SUM(CASE WHEN siteId = 1 THEN total ELSE 0 END) AS togo_total_amount, + SUM(CASE WHEN siteId = 2 THEN total ELSE 0 END) AS can_total_amount, + COUNT(DISTINCT order_id) AS total_orders, + COUNT(DISTINCT CASE WHEN siteId = 1 THEN order_id END) AS togo_total_orders, + COUNT(DISTINCT CASE WHEN siteId = 2 THEN order_id END) AS can_total_orders, + SUM(CASE WHEN purchase_type = 'first_purchase' THEN total ELSE 0 END) AS first_purchase_total, + SUM(CASE WHEN purchase_type = 'repeat_purchase' THEN total ELSE 0 END) AS repeat_purchase_total, + SUM(CASE WHEN order_type = 'cpc' THEN total ELSE 0 END) AS cpc_total, + SUM(CASE WHEN order_type = 'non_cpc' THEN total ELSE 0 END) AS non_cpc_total, + SUM(CASE WHEN zyn_type = 'zyn' AND order_type = 'cpc' THEN total ELSE 0 END) AS zyn_total, + SUM(CASE WHEN zyn_type = 'zyn' AND order_type = 'non_cpc' THEN total ELSE 0 END) AS non_zyn_total, + SUM(CASE WHEN yoone_type = 'yoone' AND order_type = 'cpc' THEN total ELSE 0 END) AS yoone_total, + SUM(CASE WHEN yoone_type = 'yoone' AND order_type = 'non_cpc' THEN total ELSE 0 END) AS non_yoone_total, + SUM(CASE WHEN zex_type = 'zex' AND order_type = 'cpc' THEN total ELSE 0 END) AS zex_total, + SUM(CASE WHEN zex_type = 'zex' AND order_type = 'non_cpc' THEN total ELSE 0 END) AS non_zex_total, + SUM(CASE WHEN source_type = 'typein' THEN total ELSE 0 END) AS direct_total, + SUM(CASE WHEN source_type = 'organic' THEN total ELSE 0 END) AS organic_total + FROM weekly_orders + GROUP BY order_date + ) + SELECT + wt.order_date, + COUNT(DISTINCT CASE WHEN wo.purchase_type = 'first_purchase' THEN wo.order_id END) AS first_purchase_orders, + COUNT(DISTINCT CASE WHEN wo.purchase_type = 'repeat_purchase' THEN wo.order_id END) AS repeat_purchase_orders, + COUNT(DISTINCT CASE WHEN wo.order_type = 'cpc' THEN wo.order_id END) AS cpc_orders, + COUNT(DISTINCT CASE WHEN wo.order_type = 'cpc' AND wo.siteId = 1 THEN wo.order_id END) AS togo_cpc_orders, + COUNT(DISTINCT CASE WHEN wo.order_type = 'cpc' AND wo.siteId = 2 THEN wo.order_id END) AS can_cpc_orders, + COUNT(DISTINCT CASE WHEN wo.order_type = 'non_cpc' THEN wo.order_id END) AS non_cpc_orders, + COUNT(DISTINCT CASE WHEN wo.order_type = 'non_cpc' AND wo.siteId = 1 THEN wo.order_id END) AS non_togo_cpc_orders, + COUNT(DISTINCT CASE WHEN wo.order_type = 'non_cpc' AND wo.siteId = 2 THEN wo.order_id END) AS non_can_cpc_orders, + COUNT(DISTINCT CASE WHEN wo.zyn_type = 'zyn' AND wo.order_type = 'cpc' THEN wo.order_id END) AS zyn_orders, + COUNT(DISTINCT CASE WHEN wo.zyn_type = 'zyn' AND wo.order_type = 'non_cpc' THEN wo.order_id END) AS non_zyn_orders, + COUNT(DISTINCT CASE WHEN wo.yoone_type = 'yoone' AND wo.order_type = 'cpc' THEN wo.order_id END) AS yoone_orders, + COUNT(DISTINCT CASE WHEN wo.yoone_type = 'yoone' AND wo.order_type = 'non_cpc' THEN wo.order_id END) AS non_yoone_orders, + COUNT(DISTINCT CASE WHEN wo.zex_type = 'zex' AND wo.order_type = 'cpc' THEN wo.order_id END) AS zex_orders, + COUNT(DISTINCT CASE WHEN wo.zex_type = 'zex' AND wo.order_type = 'non_cpc' THEN wo.order_id END) AS non_zex_orders, + COUNT(DISTINCT CASE WHEN wo.source_type = 'typein' THEN wo.order_id END) AS direct_orders, + COUNT(DISTINCT CASE WHEN wo.source_type = 'organic' THEN wo.order_id END) AS organic_orders, + wt.total_orders, + wt.togo_total_orders, + wt.can_total_orders, + wt.total_amount, + wt.togo_total_amount, + wt.can_total_amount, + wt.first_purchase_total, + wt.repeat_purchase_total, + wt.cpc_total, + wt.non_cpc_total, + wt.zyn_total, + wt.non_zyn_total, + wt.yoone_total, + wt.non_yoone_total, + wt.zex_total, + wt.non_zex_total, + wt.direct_total, + wt.organic_total, + COALESCE(SUM(os.zyn_quantity), 0) AS zyn_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.zyn_quantity ELSE 0 END) AS cpc_zyn_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.zyn_quantity ELSE 0 END) AS non_cpc_zyn_quantity, + COALESCE(SUM(os.yoone_quantity), 0) AS yoone_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.yoone_quantity ELSE 0 END) AS cpc_yoone_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.yoone_quantity ELSE 0 END) AS non_cpc_yoone_quantity, + COALESCE(SUM(os.yoone_G_quantity), 0) AS yoone_G_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.yoone_G_quantity ELSE 0 END) AS cpc_yoone_G_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.yoone_G_quantity ELSE 0 END) AS non_cpc_yoone_G_quantity, + COALESCE(SUM(os.yoone_S_quantity), 0) AS yoone_S_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.yoone_S_quantity ELSE 0 END) AS cpc_yoone_S_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.yoone_S_quantity ELSE 0 END) AS non_cpc_yoone_S_quantity, + COALESCE(SUM(os.yoone_3_quantity), 0) AS yoone_3_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.yoone_3_quantity ELSE 0 END) AS cpc_yoone_3_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.yoone_3_quantity ELSE 0 END) AS non_cpc_yoone_3_quantity, + COALESCE(SUM(os.yoone_6_quantity), 0) AS yoone_6_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.yoone_6_quantity ELSE 0 END) AS cpc_yoone_6_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.yoone_6_quantity ELSE 0 END) AS non_cpc_yoone_6_quantity, + COALESCE(SUM(os.yoone_9_quantity), 0) AS yoone_9_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.yoone_9_quantity ELSE 0 END) AS cpc_yoone_9_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.yoone_9_quantity ELSE 0 END) AS non_cpc_yoone_9_quantity, + COALESCE(SUM(os.yoone_12_quantity), 0) AS yoone_12_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.yoone_12_quantity ELSE 0 END) AS cpc_yoone_12_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.yoone_12_quantity ELSE 0 END) AS non_cpc_yoone_12_quantity, + COALESCE(SUM(os.yoone_15_quantity), 0) AS yoone_15_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.yoone_15_quantity ELSE 0 END) AS cpc_yoone_15_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.yoone_15_quantity ELSE 0 END) AS non_cpc_yoone_15_quantity, + COALESCE(SUM(os.zex_quantity), 0) AS zex_quantity, + SUM(CASE WHEN wo.order_type = 'cpc' THEN os.zex_quantity ELSE 0 END) AS cpc_zex_quantity, + SUM(CASE WHEN wo.order_type = 'non_cpc' THEN os.zex_quantity ELSE 0 END) AS non_cpc_zex_quantity, + COALESCE(SUM(oi.zyn_amount), 0) AS zyn_amount, + COALESCE(SUM(oi.yoone_amount), 0) AS yoone_amount, + COALESCE(SUM(oi.zex_amount), 0) AS zex_amount, + COALESCE(SUM(oi.yoone_G_amount), 0) AS yoone_G_amount, + COALESCE(SUM(oi.yoone_S_amount), 0) AS yoone_S_amount, + COALESCE(SUM(oi.yoone_3_amount), 0) AS yoone_3_amount, + COALESCE(SUM(oi.yoone_6_amount), 0) AS yoone_6_amount, + COALESCE(SUM(oi.yoone_9_amount), 0) AS yoone_9_amount, + COALESCE(SUM(oi.yoone_12_amount), 0) AS yoone_12_amount, + COALESCE(SUM(oi.yoone_15_amount), 0) AS yoone_15_amount, + ROUND(COALESCE(wt.total_amount / wt.total_orders, 0), 2) AS avg_total_amount, + ROUND(COALESCE(wt.togo_total_amount / wt.togo_total_orders, 0), 2) AS avg_togo_total_amount, + ROUND(COALESCE(wt.can_total_amount / wt.can_total_orders, 0), 2) AS avg_can_total_amount + FROM weekly_orders wo + LEFT JOIN weekly_totals wt ON wo.order_date = wt.order_date + LEFT JOIN order_sales_summary os ON wo.order_id = os.orderId + LEFT JOIN order_items_summary oi ON wo.order_id = oi.orderId + GROUP BY + wt.order_date, + wt.total_amount, + wt.togo_total_amount, + wt.can_total_amount, + wt.first_purchase_total, + wt.repeat_purchase_total, + wt.cpc_total, + wt.non_cpc_total, + wt.zyn_total, + wt.non_zyn_total, + wt.yoone_total, + wt.non_yoone_total, + wt.zex_total, + wt.non_zex_total, + wt.direct_total, + wt.organic_total, + wt.total_orders, + wt.togo_total_orders, + wt.can_total_orders + ORDER BY wt.order_date DESC; + `; + }else if (grouping === 'month') { + sql = `WITH first_order AS ( + SELECT customer_email, MIN(date_paid) AS first_purchase_date + FROM \`order\` + GROUP BY customer_email + ), + monthly_orders AS ( + SELECT + o.id AS order_id, + DATE_FORMAT(o.date_paid, '%Y-%m') AS order_date, + o.customer_email, + o.total, + o.source_type, + o.utm_source, + o.siteId, + CASE + WHEN o.date_paid = f.first_purchase_date THEN 'first_purchase' + ELSE 'repeat_purchase' + END AS purchase_type, + CASE + WHEN o.source_type = 'utm' AND o.utm_source = 'google' THEN 'cpc' + ELSE 'non_cpc' + END AS order_type, + MAX(CASE WHEN oi.name LIKE '%zyn%' THEN 'zyn' ELSE 'non_zyn' END) AS zyn_type, + MAX(CASE WHEN oi.name LIKE '%yoone%' THEN 'yoone' ELSE 'non_yoone' END) AS yoone_type, + MAX(CASE WHEN oi.name LIKE '%zex%' THEN 'zex' ELSE 'non_zex' END) AS zex_type + FROM \`order\` o + 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.status IN ('processing','completed') + GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source + ), + order_sales_summary AS ( + SELECT + orderId, + SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity, + SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity, + SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity + FROM order_sale + GROUP BY orderId + ), + order_items_summary AS ( + SELECT + orderId, + SUM(CASE WHEN name LIKE '%zyn%' THEN total + total_tax ELSE 0 END) AS zyn_amount, + SUM(CASE WHEN name LIKE '%yoone%' THEN total + total_tax ELSE 0 END) AS yoone_amount, + SUM(CASE WHEN name LIKE '%zex%' THEN total + total_tax ELSE 0 END) AS zex_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%package%' THEN total + total_tax ELSE 0 END) AS yoone_G_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name NOT LIKE '%package%' THEN total + total_tax ELSE 0 END) AS yoone_S_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN total + total_tax ELSE 0 END) AS yoone_3_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN total + total_tax ELSE 0 END) AS yoone_6_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN total + total_tax ELSE 0 END) AS yoone_9_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN total + total_tax ELSE 0 END) AS yoone_12_amount, + SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN total + total_tax ELSE 0 END) AS yoone_15_amount + FROM order_item + GROUP BY orderId + ), + monthly_totals AS ( + SELECT order_date, SUM(total) AS total_amount, + SUM(CASE WHEN siteId = 1 THEN total ELSE 0 END) AS togo_total_amount, + SUM(CASE WHEN siteId = 2 THEN total ELSE 0 END) AS can_total_amount, + COUNT(DISTINCT order_id) AS total_orders, + COUNT(DISTINCT CASE WHEN siteId = 1 THEN order_id END) AS togo_total_orders, + COUNT(DISTINCT CASE WHEN siteId = 2 THEN order_id END) AS can_total_orders, + SUM(CASE WHEN purchase_type = 'first_purchase' THEN total ELSE 0 END) AS first_purchase_total, + SUM(CASE WHEN purchase_type = 'repeat_purchase' THEN total ELSE 0 END) AS repeat_purchase_total, + SUM(CASE WHEN order_type = 'cpc' THEN total ELSE 0 END) AS cpc_total, + SUM(CASE WHEN order_type = 'non_cpc' THEN total ELSE 0 END) AS non_cpc_total, + SUM(CASE WHEN zyn_type = 'zyn' AND order_type = 'cpc' THEN total ELSE 0 END) AS zyn_total, + SUM(CASE WHEN zyn_type = 'zyn' AND order_type = 'non_cpc' THEN total ELSE 0 END) AS non_zyn_total, + SUM(CASE WHEN yoone_type = 'yoone' AND order_type = 'cpc' THEN total ELSE 0 END) AS yoone_total, + SUM(CASE WHEN yoone_type = 'yoone' AND order_type = 'non_cpc' THEN total ELSE 0 END) AS non_yoone_total, + SUM(CASE WHEN zex_type = 'zex' AND order_type = 'cpc' THEN total ELSE 0 END) AS zex_total, + SUM(CASE WHEN zex_type = 'zex' AND order_type = 'non_cpc' THEN total ELSE 0 END) AS non_zex_total, + SUM(CASE WHEN source_type = 'typein' THEN total ELSE 0 END) AS direct_total, + SUM(CASE WHEN source_type = 'organic' THEN total ELSE 0 END) AS organic_total + FROM monthly_orders + GROUP BY order_date + ) + SELECT + mt.order_date, + COUNT(DISTINCT CASE WHEN mo.purchase_type = 'first_purchase' THEN mo.order_id END) AS first_purchase_orders, + COUNT(DISTINCT CASE WHEN mo.purchase_type = 'repeat_purchase' THEN mo.order_id END) AS repeat_purchase_orders, + COUNT(DISTINCT CASE WHEN mo.order_type = 'cpc' THEN mo.order_id END) AS cpc_orders, + COUNT(DISTINCT CASE WHEN mo.order_type = 'cpc' AND mo.siteId = 1 THEN mo.order_id END) AS togo_cpc_orders, + COUNT(DISTINCT CASE WHEN mo.order_type = 'cpc' AND mo.siteId = 2 THEN mo.order_id END) AS can_cpc_orders, + COUNT(DISTINCT CASE WHEN mo.order_type = 'non_cpc' THEN mo.order_id END) AS non_cpc_orders, + COUNT(DISTINCT CASE WHEN mo.order_type = 'non_cpc' AND mo.siteId = 1 THEN mo.order_id END) AS non_togo_cpc_orders, + COUNT(DISTINCT CASE WHEN mo.order_type = 'non_cpc' AND mo.siteId = 2 THEN mo.order_id END) AS non_can_cpc_orders, + COUNT(DISTINCT CASE WHEN mo.zyn_type = 'zyn' AND mo.order_type = 'cpc' THEN mo.order_id END) AS zyn_orders, + COUNT(DISTINCT CASE WHEN mo.zyn_type = 'zyn' AND mo.order_type = 'non_cpc' THEN mo.order_id END) AS non_zyn_orders, + COUNT(DISTINCT CASE WHEN mo.yoone_type = 'yoone' AND mo.order_type = 'cpc' THEN mo.order_id END) AS yoone_orders, + COUNT(DISTINCT CASE WHEN mo.yoone_type = 'yoone' AND mo.order_type = 'non_cpc' THEN mo.order_id END) AS non_yoone_orders, + COUNT(DISTINCT CASE WHEN mo.zex_type = 'zex' AND mo.order_type = 'cpc' THEN mo.order_id END) AS zex_orders, + COUNT(DISTINCT CASE WHEN mo.zex_type = 'zex' AND mo.order_type = 'non_cpc' THEN mo.order_id END) AS non_zex_orders, + COUNT(DISTINCT CASE WHEN mo.source_type = 'typein' THEN mo.order_id END) AS direct_orders, + COUNT(DISTINCT CASE WHEN mo.source_type = 'organic' THEN mo.order_id END) AS organic_orders, + mt.total_orders, + mt.togo_total_orders, + mt.can_total_orders, + mt.total_amount, + mt.togo_total_amount, + mt.can_total_amount, + mt.first_purchase_total, + mt.repeat_purchase_total, + mt.cpc_total, + mt.non_cpc_total, + mt.zyn_total, + mt.non_zyn_total, + mt.yoone_total, + mt.non_yoone_total, + mt.zex_total, + mt.non_zex_total, + mt.direct_total, + mt.organic_total, + COALESCE(SUM(os.zyn_quantity), 0) AS zyn_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.zyn_quantity ELSE 0 END) AS cpc_zyn_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.zyn_quantity ELSE 0 END) AS non_cpc_zyn_quantity, + COALESCE(SUM(os.yoone_quantity), 0) AS yoone_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.yoone_quantity ELSE 0 END) AS cpc_yoone_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.yoone_quantity ELSE 0 END) AS non_cpc_yoone_quantity, + COALESCE(SUM(os.yoone_G_quantity), 0) AS yoone_G_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.yoone_G_quantity ELSE 0 END) AS cpc_yoone_G_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.yoone_G_quantity ELSE 0 END) AS non_cpc_yoone_G_quantity, + COALESCE(SUM(os.yoone_S_quantity), 0) AS yoone_S_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.yoone_S_quantity ELSE 0 END) AS cpc_yoone_S_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.yoone_S_quantity ELSE 0 END) AS non_cpc_yoone_S_quantity, + COALESCE(SUM(os.yoone_3_quantity), 0) AS yoone_3_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.yoone_3_quantity ELSE 0 END) AS cpc_yoone_3_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.yoone_3_quantity ELSE 0 END) AS non_cpc_yoone_3_quantity, + COALESCE(SUM(os.yoone_6_quantity), 0) AS yoone_6_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.yoone_6_quantity ELSE 0 END) AS cpc_yoone_6_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.yoone_6_quantity ELSE 0 END) AS non_cpc_yoone_6_quantity, + COALESCE(SUM(os.yoone_9_quantity), 0) AS yoone_9_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.yoone_9_quantity ELSE 0 END) AS cpc_yoone_9_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.yoone_9_quantity ELSE 0 END) AS non_cpc_yoone_9_quantity, + COALESCE(SUM(os.yoone_12_quantity), 0) AS yoone_12_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.yoone_12_quantity ELSE 0 END) AS cpc_yoone_12_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.yoone_12_quantity ELSE 0 END) AS non_cpc_yoone_12_quantity, + COALESCE(SUM(os.yoone_15_quantity), 0) AS yoone_15_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.yoone_15_quantity ELSE 0 END) AS cpc_yoone_15_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.yoone_15_quantity ELSE 0 END) AS non_cpc_yoone_15_quantity, + COALESCE(SUM(os.zex_quantity), 0) AS zex_quantity, + SUM(CASE WHEN mo.order_type = 'cpc' THEN os.zex_quantity ELSE 0 END) AS cpc_zex_quantity, + SUM(CASE WHEN mo.order_type = 'non_cpc' THEN os.zex_quantity ELSE 0 END) AS non_cpc_zex_quantity, + COALESCE(SUM(oi.zyn_amount), 0) AS zyn_amount, + COALESCE(SUM(oi.yoone_amount), 0) AS yoone_amount, + COALESCE(SUM(oi.zex_amount), 0) AS zex_amount, + COALESCE(SUM(oi.yoone_G_amount), 0) AS yoone_G_amount, + COALESCE(SUM(oi.yoone_S_amount), 0) AS yoone_S_amount, + COALESCE(SUM(oi.yoone_3_amount), 0) AS yoone_3_amount, + COALESCE(SUM(oi.yoone_6_amount), 0) AS yoone_6_amount, + COALESCE(SUM(oi.yoone_9_amount), 0) AS yoone_9_amount, + COALESCE(SUM(oi.yoone_12_amount), 0) AS yoone_12_amount, + COALESCE(SUM(oi.yoone_15_amount), 0) AS yoone_15_amount, + ROUND(COALESCE(mt.total_amount / mt.total_orders, 0), 2) AS avg_total_amount, + ROUND(COALESCE(mt.togo_total_amount / mt.togo_total_orders, 0), 2) AS avg_togo_total_amount, + ROUND(COALESCE(mt.can_total_amount / mt.can_total_orders, 0), 2) AS avg_can_total_amount + FROM monthly_orders mo + LEFT JOIN monthly_totals mt ON mo.order_date = mt.order_date + LEFT JOIN order_sales_summary os ON mo.order_id = os.orderId + LEFT JOIN order_items_summary oi ON mo.order_id = oi.orderId + GROUP BY + mt.order_date, + mt.total_amount, + mt.togo_total_amount, + mt.can_total_amount, + mt.first_purchase_total, + mt.repeat_purchase_total, + mt.cpc_total, + mt.non_cpc_total, + mt.zyn_total, + mt.non_zyn_total, + mt.yoone_total, + mt.non_yoone_total, + mt.zex_total, + mt.non_zex_total, + mt.direct_total, + mt.organic_total, + mt.total_orders, + mt.togo_total_orders, + mt.can_total_orders + ORDER BY mt.order_date DESC; + `;} + return this.orderRepository.query(sql); } // async getOrderStatistics(params: OrderStatisticsParams) { @@ -656,10 +1045,10 @@ export class StatisticsService { const offset = (current - 1) * pageSize; const countSql = ` WITH product_list AS ( - SELECT DISTINCT s.sku + SELECT DISTINCT s.productSku FROM stock s LEFT JOIN stock_point sp ON s.stockPointId = sp.id - LEFT JOIN product p ON s.sku = p.sku + LEFT JOIN product p ON s.productSku = p.sku WHERE sp.ignore = FALSE ${countnameFilter} ) @@ -674,27 +1063,27 @@ export class StatisticsService { const sql = ` WITH stock_summary AS ( SELECT - s.sku, + s.productSku, JSON_ARRAYAGG(JSON_OBJECT('id', sp.id, 'quantity', s.quantity)) AS stockDetails, SUM(s.quantity) AS totalStock, SUM(CASE WHEN sp.inCanada THEN s.quantity ELSE 0 END) AS caTotalStock FROM stock s JOIN stock_point sp ON s.stockPointId = sp.id WHERE sp.ignore = FALSE - GROUP BY s.sku + GROUP BY s.productSku ), transfer_stock AS ( SELECT - ti.sku, + ti.productSku, SUM(ti.quantity) AS transitStock FROM transfer_item ti JOIN transfer t ON ti.transferId = t.id WHERE t.isCancel = FALSE AND t.isArrived = FALSE - GROUP BY ti.sku + GROUP BY ti.productSku ), 30_sales_summary AS ( SELECT - os.sku AS sku, + os.sku AS productSku, SUM(os.quantity) AS totalSales FROM order_sale os JOIN \`order\` o ON os.orderId = o.id @@ -704,7 +1093,7 @@ export class StatisticsService { ), 15_sales_summary AS ( SELECT - os.sku AS sku, + os.sku AS productSku, 2 * SUM(os.quantity) AS totalSales FROM order_sale os JOIN \`order\` o ON os.orderId = o.id @@ -714,36 +1103,36 @@ export class StatisticsService { ), sales_max_summary AS ( SELECT - s30.sku AS sku, + s30.productSku AS productSku, COALESCE(s30.totalSales, 0) AS totalSales_30, COALESCE(s15.totalSales, 0) AS totalSales_15, GREATEST(COALESCE(s30.totalSales, 0), COALESCE(s15.totalSales, 0)) AS maxSales FROM 30_sales_summary s30 LEFT JOIN 15_sales_summary s15 - ON s30.sku = s15.sku + ON s30.productSku = s15.productSku UNION ALL SELECT - s15.sku AS sku, + s15.productSku AS productSku, 0 AS totalSales_30, COALESCE(s15.totalSales, 0) AS totalSales_15, COALESCE(s15.totalSales, 0) AS maxSales FROM 15_sales_summary s15 LEFT JOIN 30_sales_summary s30 - ON s30.sku = s15.sku - WHERE s30.sku IS NULL + ON s30.productSku = s15.productSku + WHERE s30.productSku IS NULL ), product_name_summary AS ( SELECT - p.sku AS sku, + p.sku AS productSku, COALESCE(MAX(os.name), MAX(p.name)) AS productName FROM product p LEFT JOIN order_sale os ON p.sku = os.sku GROUP BY p.sku ) SELECT - ss.sku, + ss.productSku, ss.stockDetails, COALESCE(ts.transitStock, 0) AS transitStock, (COALESCE(ss.totalStock, 0) + COALESCE(ts.transitStock, 0)) AS totalStock, @@ -761,9 +1150,9 @@ export class StatisticsService { sales.maxSales * 4 AS restockQuantity, pns.productName FROM stock_summary ss - LEFT JOIN transfer_stock ts ON ss.sku = ts.sku - LEFT JOIN sales_max_summary sales ON ss.sku = sales.sku - LEFT JOIN product_name_summary pns ON ss.sku = pns.sku + LEFT JOIN transfer_stock ts ON ss.productSku = ts.productSku + LEFT JOIN sales_max_summary sales ON ss.productSku = sales.productSku + LEFT JOIN product_name_summary pns ON ss.productSku = pns.productSku WHERE 1 = 1 ${nameFilter} ORDER BY caAvailableDays @@ -791,10 +1180,10 @@ export class StatisticsService { const offset = (current - 1) * pageSize; const countSql = ` WITH product_list AS ( - SELECT DISTINCT s.sku + SELECT DISTINCT s.productSku FROM stock s LEFT JOIN stock_point sp ON s.stockPointId = sp.id - LEFT JOIN product p ON s.sku = p.sku + LEFT JOIN product p ON s.productSku = p.sku WHERE sp.ignore = FALSE ${countnameFilter} ) @@ -810,36 +1199,36 @@ export class StatisticsService { const sql = ` WITH stock_summary AS ( SELECT - s.sku, + s.productSku, SUM(s.quantity) AS totalStock FROM stock s JOIN stock_point sp ON s.stockPointId = sp.id WHERE sp.ignore = FALSE - GROUP BY s.sku + GROUP BY s.productSku ), transfer_stock AS ( SELECT - ti.sku, + ti.productSku, SUM(ti.quantity) AS transitStock FROM transfer_item ti JOIN transfer t ON ti.transferId = t.id WHERE t.isCancel = FALSE AND t.isArrived = FALSE - GROUP BY ti.sku + GROUP BY ti.productSku ), b_sales_data_raw As ( SELECT - sr.sku, + sr.productSku, DATE_FORMAT(sr.createdAt, '%Y-%m') AS month, SUM(sr.quantityChange) AS sales FROM stock_record sr JOIN stock_point sp ON sr.stockPointId = sp.id WHERE sp.isB AND sr.createdAt >= DATE_FORMAT(NOW() - INTERVAL 2 MONTH, '%Y-%m-01') - GROUP BY sr.sku, month + GROUP BY sr.productSku, month ), sales_data_raw AS ( SELECT - os.sku AS sku, + os.sku AS productSku, DATE_FORMAT(o.date_paid, '%Y-%m') AS month, SUM(CASE WHEN DAY(o.date_paid) <= 10 THEN os.quantity ELSE 0 END) AS early_sales, SUM(CASE WHEN DAY(o.date_paid) > 10 AND DAY(o.date_paid) <= 20 THEN os.quantity ELSE 0 END) AS mid_sales, @@ -852,7 +1241,7 @@ export class StatisticsService { ), monthly_sales_summary AS ( SELECT - sdr.sku, + sdr.productSku, JSON_ARRAYAGG( JSON_OBJECT( 'month', sdr.month, @@ -863,12 +1252,12 @@ export class StatisticsService { ) ) AS sales_data FROM sales_data_raw sdr - LEFT JOIN b_sales_data_raw b ON sdr.sku = b.sku AND sdr.month = b.month - GROUP BY sdr.sku + LEFT JOIN b_sales_data_raw b ON sdr.productSku = b.productSku AND sdr.month = b.month + GROUP BY sdr.productSku ), sales_summary AS ( SELECT - os.sku AS sku, + os.sku AS productSku, SUM(CASE WHEN o.date_paid >= CURDATE() - INTERVAL 30 DAY THEN os.quantity ELSE 0 END) AS last_30_days_sales, SUM(CASE WHEN o.date_paid >= CURDATE() - INTERVAL 15 DAY THEN os.quantity ELSE 0 END) AS last_15_days_sales, SUM(CASE WHEN DATE_FORMAT(o.date_paid, '%Y-%m') = DATE_FORMAT(CURDATE() - INTERVAL 1 MONTH, '%Y-%m') THEN os.quantity ELSE 0 END) AS last_month_sales @@ -880,14 +1269,14 @@ export class StatisticsService { ), product_name_summary AS ( SELECT - p.sku AS sku, + p.sku AS productSku, COALESCE(MAX(os.name), MAX(p.name)) AS productName FROM product p LEFT JOIN order_sale os ON p.sku = os.sku GROUP BY p.sku ) SELECT - ss.sku, + ss.productSku, (COALESCE(ss.totalStock, 0) + COALESCE(ts.transitStock, 0)) AS totalStock, ms.sales_data AS monthlySalesData, pns.productName, @@ -900,10 +1289,10 @@ export class StatisticsService { ELSE NULL END AS stock_ratio FROM stock_summary ss - LEFT JOIN transfer_stock ts ON ss.sku = ts.sku - LEFT JOIN monthly_sales_summary ms ON ss.sku = ms.sku - LEFT JOIN product_name_summary pns ON ss.sku = pns.sku - LEFT JOIN sales_summary ssum ON ss.sku = ssum.sku + LEFT JOIN transfer_stock ts ON ss.productSku = ts.productSku + LEFT JOIN monthly_sales_summary ms ON ss.productSku = ms.productSku + LEFT JOIN product_name_summary pns ON ss.productSku = pns.productSku + LEFT JOIN sales_summary ssum ON ss.productSku = ssum.productSku WHERE 1 = 1 ${nameFilter} ORDER BY @@ -933,7 +1322,8 @@ export class StatisticsService { user_first_order AS ( SELECT customer_email, - DATE_FORMAT(MIN(date_paid), '%Y-%m') AS first_order_month + DATE_FORMAT(MIN(date_paid), '%Y-%m') AS first_order_month, + SUM(total) AS first_order_total FROM \`order\` WHERE status IN ('processing', 'completed') GROUP BY customer_email @@ -946,7 +1336,7 @@ export class StatisticsService { WHERE status IN ('processing', 'completed') ), filtered_orders AS ( - SELECT o.customer_email, o.order_month, u.first_order_month, c.start_month + SELECT o.customer_email, o.order_month, u.first_order_month,u.first_order_total, c.start_month FROM order_months o JOIN user_first_order u ON o.customer_email = u.customer_email JOIN cutoff_months c ON 1=1 @@ -958,14 +1348,16 @@ export class StatisticsService { CASE WHEN first_order_month < start_month THEN CONCAT('>', start_month) ELSE first_order_month - END AS first_order_month_group + END AS first_order_month_group, + first_order_total FROM filtered_orders ), final_counts AS ( SELECT order_month, first_order_month_group, - COUNT(*) AS order_count + COUNT(*) AS order_count, + SUM(first_order_total) AS total FROM classified GROUP BY order_month, first_order_month_group ) @@ -985,7 +1377,8 @@ export class StatisticsService { SELECT customer_email, DATE_FORMAT(date_paid, '%Y-%m') AS order_month, - date_paid + date_paid, + total FROM \`order\` WHERE status IN ('processing', 'completed') ), @@ -998,7 +1391,8 @@ export class StatisticsService { monthly_users AS ( SELECT DISTINCT customer_email, - order_month + order_month, + total FROM filtered_users ), @@ -1011,11 +1405,12 @@ export class StatisticsService { GROUP BY customer_email ), - -- 标注每个用户每月是"新客户"还是"老客户" + -- 标注每个用户每月是“新客户”还是“老客户” labeled_users AS ( SELECT m.customer_email, m.order_month, + m.total, CASE WHEN f.first_order_month = m.order_month THEN 'new' ELSE 'returning' @@ -1029,7 +1424,9 @@ export class StatisticsService { SELECT order_month, SUM(CASE WHEN customer_type = 'new' THEN 1 ELSE 0 END) AS new_user_count, - SUM(CASE WHEN customer_type = 'returning' THEN 1 ELSE 0 END) AS old_user_count + SUM(CASE WHEN customer_type = 'new' THEN total ELSE 0 END) AS new_user_total, + SUM(CASE WHEN customer_type = 'returning' THEN 1 ELSE 0 END) AS old_user_count, + SUM(CASE WHEN customer_type = 'returning' THEN total ELSE 0 END) AS old_user_total FROM labeled_users GROUP BY order_month ), @@ -1056,12 +1453,14 @@ export class StatisticsService { GROUP BY current_month ) - -- 最终结果:每月新客户,老客户,未来未复购客户 + -- 最终结果:每月新客户、老客户、未来未复购客户 SELECT m.order_month, m.new_user_count, m.old_user_count, - COALESCE(i.inactive_user_count, 0) AS inactive_user_count + COALESCE(i.inactive_user_count, 0) AS inactive_user_count, + m.new_user_total, + m.old_user_total FROM monthly_new_old_counts m LEFT JOIN users_without_future_orders i ON m.order_month = i.order_month