import { InjectEntityModel } from '@midwayjs/typeorm'; import { Order } from '../entity/order.entity'; import { Provide } from '@midwayjs/core'; import { Repository } from 'typeorm'; import { OrderStatisticsParams } from '../dto/statistics.dto'; import { OrderItem } from '../entity/order_item.entity'; import dayjs = require('dayjs'); @Provide() export class StatisticsService { @InjectEntityModel(Order) orderRepository: Repository; @InjectEntityModel(OrderItem) orderItemRepository: Repository; async getOrderStatistics(params: OrderStatisticsParams) { const { startDate, endDate, siteId } = 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 ( SELECT customer_email, MIN(date_paid) AS first_purchase_date FROM \`order\` GROUP BY customer_email ), daily_orders AS ( SELECT o.id AS order_id, DATE(o.date_paid) 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') `; if (siteId) sql += ` AND o.siteId=${siteId}`; 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 ( 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 ), daily_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 daily_orders GROUP BY order_date ) SELECT d.order_date, COUNT(DISTINCT CASE WHEN d.purchase_type = 'first_purchase' THEN d.order_id END) AS first_purchase_orders, COUNT(DISTINCT CASE WHEN d.purchase_type = 'repeat_purchase' THEN d.order_id END) AS repeat_purchase_orders, COUNT(DISTINCT CASE WHEN d.order_type = 'cpc' THEN d.order_id END) AS cpc_orders, COUNT(DISTINCT CASE WHEN d.order_type = 'cpc' AND d.siteId = 1 THEN d.order_id END) AS togo_cpc_orders, COUNT(DISTINCT CASE WHEN d.order_type = 'cpc' AND d.siteId = 2 THEN d.order_id END) AS can_cpc_orders, COUNT(DISTINCT CASE WHEN d.order_type = 'non_cpc' THEN d.order_id END) AS non_cpc_orders, COUNT(DISTINCT CASE WHEN d.order_type = 'non_cpc' AND d.siteId = 1 THEN d.order_id END) AS non_togo_cpc_orders, COUNT(DISTINCT CASE WHEN d.order_type = 'non_cpc' AND d.siteId = 2 THEN d.order_id END) AS non_can_cpc_orders, COUNT(DISTINCT CASE WHEN d.zyn_type = 'zyn' AND d.order_type = 'cpc' THEN d.order_id END) AS zyn_orders, COUNT(DISTINCT CASE WHEN d.zyn_type = 'zyn' AND d.order_type = 'non_cpc' THEN d.order_id END) AS non_zyn_orders, COUNT(DISTINCT CASE WHEN d.yoone_type = 'yoone' AND d.order_type = 'cpc' THEN d.order_id END) AS yoone_orders, COUNT(DISTINCT CASE WHEN d.yoone_type = 'yoone' AND d.order_type = 'non_cpc' THEN d.order_id END) AS non_yoone_orders, COUNT(DISTINCT CASE WHEN d.zex_type = 'zex' AND d.order_type = 'cpc' THEN d.order_id END) AS zex_orders, COUNT(DISTINCT CASE WHEN d.zex_type = 'zex' AND d.order_type = 'non_cpc' THEN d.order_id END) AS non_zex_orders, COUNT(DISTINCT CASE WHEN d.source_type = 'typein' THEN d.order_id END) AS direct_orders, COUNT(DISTINCT CASE WHEN d.source_type = 'organic' THEN d.order_id END) AS organic_orders, dt.total_orders, dt.togo_total_orders, dt.can_total_orders, dt.total_amount, dt.togo_total_amount, dt.can_total_amount, dt.first_purchase_total, dt.repeat_purchase_total, dt.cpc_total, dt.non_cpc_total, dt.zyn_total, dt.non_zyn_total, dt.yoone_total, dt.non_yoone_total, dt.zex_total, dt.non_zex_total, dt.direct_total, dt.organic_total, COALESCE(SUM(os.zyn_quantity), 0) AS zyn_quantity, SUM(CASE WHEN d.order_type = 'cpc' THEN os.zyn_quantity ELSE 0 END) AS cpc_zyn_quantity, SUM(CASE WHEN d.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 d.order_type = 'cpc' THEN os.yoone_quantity ELSE 0 END) AS cpc_yoone_quantity, SUM(CASE WHEN d.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 d.order_type = 'cpc' THEN os.yoone_G_quantity ELSE 0 END) AS cpc_yoone_G_quantity, SUM(CASE WHEN d.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 d.order_type = 'cpc' THEN os.yoone_S_quantity ELSE 0 END) AS cpc_yoone_S_quantity, SUM(CASE WHEN d.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 d.order_type = 'cpc' THEN os.yoone_3_quantity ELSE 0 END) AS cpc_yoone_3_quantity, SUM(CASE WHEN d.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 d.order_type = 'cpc' THEN os.yoone_6_quantity ELSE 0 END) AS cpc_yoone_6_quantity, SUM(CASE WHEN d.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 d.order_type = 'cpc' THEN os.yoone_9_quantity ELSE 0 END) AS cpc_yoone_9_quantity, SUM(CASE WHEN d.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 d.order_type = 'cpc' THEN os.yoone_12_quantity ELSE 0 END) AS cpc_yoone_12_quantity, SUM(CASE WHEN d.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 d.order_type = 'cpc' THEN os.yoone_15_quantity ELSE 0 END) AS cpc_yoone_15_quantity, SUM(CASE WHEN d.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 d.order_type = 'cpc' THEN os.zex_quantity ELSE 0 END) AS cpc_zex_quantity, SUM(CASE WHEN d.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(dt.total_amount / dt.total_orders,0), 2) AS avg_total_amount, ROUND(COALESCE(dt.togo_total_amount / dt.togo_total_orders,0), 2) AS avg_togo_total_amount, ROUND(COALESCE(dt.can_total_amount / dt.can_total_orders,0), 2) AS avg_can_total_amount FROM daily_orders d LEFT JOIN daily_totals dt ON d.order_date = dt.order_date LEFT JOIN order_sales_summary os ON d.order_id = os.orderId LEFT JOIN order_items_summary oi ON d.order_id = oi.orderId GROUP BY d.order_date, dt.total_amount, dt.togo_total_amount, dt.can_total_amount, dt.first_purchase_total, dt.repeat_purchase_total, dt.cpc_total, dt.non_cpc_total, dt.zyn_total, dt.non_zyn_total, dt.yoone_total, dt.non_yoone_total, dt.zex_total, dt.non_zex_total, dt.direct_total, dt.organic_total, dt.total_orders, dt.togo_total_orders, dt.can_total_orders ORDER BY d.order_date DESC; `; return this.orderRepository.query(sql); } // async getOrderStatistics(params: OrderStatisticsParams) { // const { // startDate, // endDate, // siteId, // purchaseType = 'all', // orderType = 'all', // brand = 'all', // } = params; // const start = dayjs(startDate).format('YYYY-MM-DD'); // const end = dayjs(endDate).add(1, 'day').format('YYYY-MM-DD'); // // 条件拼接 // const siteFilter = siteId ? `AND o.siteId = '${siteId}'` : ''; // const purchaseTypeFilter = // purchaseType && purchaseType !== 'all' // ? `AND purchase_type = '${purchaseType}'` // : ''; // const orderTypeFilter = // orderType && orderType !== 'all' ? `AND order_type = '${orderType}'` : ''; // const brandFilter = brand !== 'all' ? `WHERE name LIKE '%${brand}%'` : ''; // const sql = ` // WITH first_order AS ( // SELECT customer_email, MIN(date_paid) AS first_purchase_date // FROM \`order\` // GROUP BY customer_email // ), // all_orders AS ( // SELECT // o.id AS order_id, // DATE(o.date_paid) AS order_date, // o.customer_email, // o.total, // o.source_type, // o.utm_source, // 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 // FROM \`order\` o // LEFT JOIN first_order f ON o.customer_email = f.customer_email // WHERE o.date_paid IS NOT NULL // AND o.date_paid >= '${start}' AND o.date_paid < '${end}' // AND o.status IN ('processing','completed') // ${siteFilter} // ), // filtered_orders AS ( // SELECT ao.* // FROM all_orders ao // WHERE 1=1 // ${purchaseTypeFilter} // ${orderTypeFilter} // ${ // brand !== 'all' // ? ` // AND EXISTS ( // SELECT 1 FROM order_item oi // WHERE oi.orderId = ao.order_id // AND oi.name LIKE '%${brand}%' // ) // ` // : '' // } // ), // brand_quantity AS ( // SELECT // orderId, // SUM(quantity) AS total_quantity // FROM order_sale // ${brandFilter} // GROUP BY orderId // ) // SELECT // d.order_date, // COUNT(DISTINCT d.order_id) AS total_orders, // ROUND(SUM(d.total), 2) AS total_amount, // ROUND(SUM(d.total) / COUNT(DISTINCT d.order_id), 2) AS avg_order_amount, // COALESCE(SUM(bq.total_quantity), 0) AS total_quantity // FROM filtered_orders d // LEFT JOIN brand_quantity bq ON d.order_id = bq.orderId // GROUP BY d.order_date // ORDER BY d.order_date DESC // `; // return this.orderRepository.query(sql); // } async getOrderByDate(date: string) { const startOfDay = new Date(`${date}T00:00:00`); const endOfDay = new Date(`${date}T23:59:59`); const sql = ` WITH first_order AS ( SELECT customer_email, MIN(date_paid) AS first_purchase_date FROM \`order\` GROUP BY customer_email ), last_order AS ( SELECT customer_email, MAX(date_paid) AS last_purchase_date FROM \`order\` GROUP BY customer_email ), customer_stats AS ( SELECT customer_email, COUNT(o.id) AS order_count, SUM(o.total) AS total_spent FROM \`order\` o WHERE o.status IN ('processing', 'completed') GROUP BY customer_email ) SELECT o.*, JSON_ARRAYAGG( JSON_OBJECT('name', oi.name, 'quantity', oi.quantity) ) AS orderItems, f.first_purchase_date, l.last_purchase_date, CASE WHEN o.date_paid = f.first_purchase_date THEN 'first_purchase' ELSE 'repeat_purchase' END AS purchase_type, cs.order_count, cs.total_spent, ( SELECT JSON_ARRAYAGG(tag) FROM customer_tag ct WHERE ct.email = o.customer_email ) AS tags FROM \`order\` o LEFT JOIN first_order f ON o.customer_email = f.customer_email LEFT JOIN last_order l ON o.customer_email = l.customer_email LEFT JOIN order_item oi ON oi.orderId = o.id LEFT JOIN customer_stats cs ON o.customer_email = cs.customer_email WHERE o.date_paid BETWEEN ? AND ? AND o.status IN ('processing', 'completed') GROUP BY o.id, f.first_purchase_date, cs.order_count, cs.total_spent `; const orders = await this.orderRepository.query(sql, [ startOfDay, endOfDay, ]); return orders; } async getOrderByEmail(email: string) { const sql = ` SELECT o.*, JSON_ARRAYAGG( JSON_OBJECT('name', oi.name, 'quantity', oi.quantity, 'total', oi.total) ) AS orderItems, ( SELECT JSON_ARRAYAGG(tag) FROM customer_tag ct WHERE ct.email = o.customer_email ) AS tags FROM \`order\` o LEFT JOIN order_item oi ON oi.orderId = o.id WHERE o.customer_email='${email}' GROUP BY o.id ORDER BY o.date_paid DESC; `; const orders = await this.orderRepository.query(sql, [email]); return orders; } async getCustomerOrders(month) { const timeWhere = month ? `AND o.date_paid BETWEEN '${dayjs(month[0]) .startOf('month') .format('YYYY-MM-DD HH:mm:ss')}' AND '${dayjs(month[1]) .endOf('month') .format('YYYY-MM-DD HH:mm:ss')}'` : ''; const sql = `WITH orders AS ( SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count FROM \`order\` o WHERE 1=1 ${timeWhere} GROUP BY o.customer_email ), completed_orders AS ( SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count FROM \`order\` o WHERE o.status IN ('completed', 'processing') ${timeWhere} GROUP BY o.customer_email ), yoone_orders AS ( SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count FROM \`order\` o JOIN order_sale os ON o.id = os.orderId WHERE o.status IN ('completed', 'processing') AND os.name LIKE '%yoone%' ${timeWhere} GROUP BY o.customer_email ), zyn_orders AS ( SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count FROM \`order\` o JOIN order_sale os ON o.id = os.orderId WHERE o.status IN ('completed', 'processing') AND os.name LIKE '%zyn%' ${timeWhere} GROUP BY o.customer_email ), zex_orders AS ( SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count FROM \`order\` o JOIN order_sale os ON o.id = os.orderId WHERE o.status IN ('completed', 'processing') AND os.name LIKE '%zex%' ${timeWhere} GROUP BY o.customer_email ), yoone_3mg_orders AS ( SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count FROM \`order\` o JOIN order_sale os ON o.id = os.orderId WHERE o.status IN ('completed', 'processing') AND os.name LIKE '%3mg%' AND os.name LIKE '%yoone%' ${timeWhere} GROUP BY o.customer_email ), yoone_6mg_orders AS ( SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count FROM \`order\` o JOIN order_sale os ON o.id = os.orderId WHERE o.status IN ('completed', 'processing') AND os.name LIKE '%6mg%' AND os.name LIKE '%yoone%' ${timeWhere} GROUP BY o.customer_email ), yoone_9mg_orders AS ( SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count FROM \`order\` o JOIN order_sale os ON o.id = os.orderId WHERE o.status IN ('completed', 'processing') AND os.name LIKE '%9mg%' AND os.name LIKE '%yoone%' ${timeWhere} GROUP BY o.customer_email ), yoone_12mg_orders AS ( SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count FROM \`order\` o JOIN order_sale os ON o.id = os.orderId WHERE o.status IN ('completed', 'processing') AND os.name LIKE '%12mg%' AND os.name LIKE '%yoone%' ${timeWhere} GROUP BY o.customer_email ), yoone_15mg_orders AS ( SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count FROM \`order\` o JOIN order_sale os ON o.id = os.orderId WHERE o.status IN ('completed', 'processing') AND os.name LIKE '%15mg%' AND os.name LIKE '%yoone%' ${timeWhere} GROUP BY o.customer_email ), zyn_3mg_orders AS ( SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count FROM \`order\` o JOIN order_sale os ON o.id = os.orderId WHERE o.status IN ('completed', 'processing') AND os.name LIKE '%3mg%' AND os.name LIKE '%zyn%' ${timeWhere} GROUP BY o.customer_email ), zyn_6mg_orders AS ( SELECT o.customer_email, COUNT(DISTINCT o.id) AS order_count FROM \`order\` o JOIN order_sale os ON o.id = os.orderId WHERE o.status IN ('completed', 'processing') AND os.name LIKE '%6mg%' AND os.name LIKE '%zyn%' ${timeWhere} GROUP BY o.customer_email ) SELECT -- 总完成的用户数 (SELECT COUNT(DISTINCT customer_email) FROM orders) AS users, -- 完成至少一次购买的用户数 (SELECT COUNT(DISTINCT customer_email) FROM completed_orders WHERE order_count >= 1) AS users_one_purchase, -- 完成至少两次购买的用户数 (SELECT COUNT(DISTINCT customer_email) FROM completed_orders WHERE order_count >= 2) AS users_two_purchases, -- 完成至少三次购买的用户数 (SELECT COUNT(DISTINCT customer_email) FROM completed_orders WHERE order_count >= 3) AS users_three_purchases, -- 包含 'yoone' 的订单 (SELECT COUNT(DISTINCT customer_email) FROM yoone_orders WHERE order_count >= 1) AS users_with_yoone_one_purchase, (SELECT COUNT(DISTINCT customer_email) FROM yoone_orders WHERE order_count >= 2) AS users_with_yoone_two_purchases, (SELECT COUNT(DISTINCT customer_email) FROM yoone_orders WHERE order_count >= 3) AS users_with_yoone_three_purchases, (SELECT COUNT(DISTINCT customer_email) FROM yoone_3mg_orders WHERE order_count >= 1) AS users_with_yoone_3mg_one_purchase, (SELECT COUNT(DISTINCT customer_email) FROM yoone_3mg_orders WHERE order_count >= 2) AS users_with_yoone_3mg_two_purchases, (SELECT COUNT(DISTINCT customer_email) FROM yoone_3mg_orders WHERE order_count >= 3) AS users_with_yoone_3mg_three_purchases, (SELECT COUNT(DISTINCT customer_email) FROM yoone_6mg_orders WHERE order_count >= 1) AS users_with_yoone_6mg_one_purchase, (SELECT COUNT(DISTINCT customer_email) FROM yoone_6mg_orders WHERE order_count >= 2) AS users_with_yoone_6mg_two_purchases, (SELECT COUNT(DISTINCT customer_email) FROM yoone_6mg_orders WHERE order_count >= 3) AS users_with_yoone_6mg_three_purchases, (SELECT COUNT(DISTINCT customer_email) FROM yoone_9mg_orders WHERE order_count >= 1) AS users_with_yoone_9mg_one_purchase, (SELECT COUNT(DISTINCT customer_email) FROM yoone_9mg_orders WHERE order_count >= 2) AS users_with_yoone_9mg_two_purchases, (SELECT COUNT(DISTINCT customer_email) FROM yoone_9mg_orders WHERE order_count >= 3) AS users_with_yoone_9mg_three_purchases, (SELECT COUNT(DISTINCT customer_email) FROM yoone_12mg_orders WHERE order_count >= 1) AS users_with_yoone_12mg_one_purchase, (SELECT COUNT(DISTINCT customer_email) FROM yoone_12mg_orders WHERE order_count >= 2) AS users_with_yoone_12mg_two_purchases, (SELECT COUNT(DISTINCT customer_email) FROM yoone_12mg_orders WHERE order_count >= 3) AS users_with_yoone_12mg_three_purchases, (SELECT COUNT(DISTINCT customer_email) FROM yoone_15mg_orders WHERE order_count >= 1) AS users_with_yoone_15mg_one_purchase, (SELECT COUNT(DISTINCT customer_email) FROM yoone_15mg_orders WHERE order_count >= 2) AS users_with_yoone_15mg_two_purchases, (SELECT COUNT(DISTINCT customer_email) FROM yoone_15mg_orders WHERE order_count >= 3) AS users_with_yoone_15mg_three_purchases, -- 包含 'zyn' 的订单 (SELECT COUNT(DISTINCT customer_email) FROM zyn_orders WHERE order_count >= 1) AS users_with_zyn_one_purchase, (SELECT COUNT(DISTINCT customer_email) FROM zyn_orders WHERE order_count >= 2) AS users_with_zyn_two_purchases, (SELECT COUNT(DISTINCT customer_email) FROM zyn_orders WHERE order_count >= 3) AS users_with_zyn_three_purchases, (SELECT COUNT(DISTINCT customer_email) FROM zyn_3mg_orders WHERE order_count >= 1) AS users_with_zyn_3mg_one_purchase, (SELECT COUNT(DISTINCT customer_email) FROM zyn_3mg_orders WHERE order_count >= 2) AS users_with_zyn_3mg_two_purchases, (SELECT COUNT(DISTINCT customer_email) FROM zyn_3mg_orders WHERE order_count >= 3) AS users_with_zyn_3mg_three_purchases, (SELECT COUNT(DISTINCT customer_email) FROM zyn_6mg_orders WHERE order_count >= 1) AS users_with_zyn_6mg_one_purchase, (SELECT COUNT(DISTINCT customer_email) FROM zyn_6mg_orders WHERE order_count >= 2) AS users_with_zyn_6mg_two_purchases, (SELECT COUNT(DISTINCT customer_email) FROM zyn_6mg_orders WHERE order_count >= 3) AS users_with_zyn_6mg_three_purchases, -- 包含 'zex' 的订单 (SELECT COUNT(DISTINCT customer_email) FROM zex_orders WHERE order_count >= 1) AS users_with_zex_one_purchase, (SELECT COUNT(DISTINCT customer_email) FROM zex_orders WHERE order_count >= 2) AS users_with_zex_two_purchases, (SELECT COUNT(DISTINCT customer_email) FROM zex_orders WHERE order_count >= 3) AS users_with_zex_three_purchases, (SELECT SUM(order_count) FROM zyn_3mg_orders) AS zyn_3mg_orders, (SELECT SUM(order_count) FROM zyn_6mg_orders) AS zyn_6mg_orders, (SELECT SUM(order_count) FROM zex_orders) AS zex_15mg_orders, (SELECT SUM(order_count) FROM yoone_3mg_orders) AS order_with_3mg, (SELECT SUM(order_count) FROM yoone_6mg_orders) AS order_with_6mg, (SELECT SUM(order_count) FROM yoone_9mg_orders) AS order_with_9mg, (SELECT SUM(order_count) FROM yoone_12mg_orders) AS order_with_12mg, (SELECT SUM(order_count) FROM yoone_15mg_orders) AS order_with_15mg `; // 执行 SQL 查询 const result = await this.orderRepository.query(sql); const sql1 = ` WITH product_purchase_counts AS ( SELECT o.customer_email,os.productId, os.name, COUNT(DISTINCT o.id) AS order_count FROM \`order\` o JOIN order_sale os ON o.id = os.orderId WHERE o.status IN ('completed', 'processing') ${timeWhere} GROUP BY o.customer_email, os.productId, os.name HAVING order_count > 0 ) SELECT productId, name, COUNT(DISTINCT customer_email) AS user_count FROM product_purchase_counts GROUP BY productId, name ORDER BY user_count DESC LIMIT 30 `; const sql2 = ` WITH product_purchase_counts AS ( SELECT o.customer_email,os.productId, os.name, COUNT(DISTINCT o.id) AS order_count FROM \`order\` o JOIN order_sale os ON o.id = os.orderId WHERE o.status IN ('completed', 'processing') ${timeWhere} GROUP BY o.customer_email, os.productId, os.name HAVING order_count > 1 ) SELECT productId, name, COUNT(DISTINCT customer_email) AS user_count FROM product_purchase_counts GROUP BY productId, name ORDER BY user_count DESC LIMIT 30 `; const sql3 = ` WITH product_purchase_counts AS ( SELECT o.customer_email,os.productId, os.name, COUNT(DISTINCT o.id) AS order_count FROM \`order\` o JOIN order_sale os ON o.id = os.orderId WHERE o.status IN ('completed', 'processing') ${timeWhere} GROUP BY o.customer_email, os.productId, os.name HAVING order_count > 2 ) SELECT productId, name, COUNT(DISTINCT customer_email) AS user_count FROM product_purchase_counts GROUP BY productId, name ORDER BY user_count DESC LIMIT 30 `; const first_hot_purchase = await this.orderRepository.query(sql1); const second_hot_purchase = await this.orderRepository.query(sql2); const third_hot_purchase = await this.orderRepository.query(sql3); return { ...result[0], first_hot_purchase, second_hot_purchase, third_hot_purchase, }; } async stockForecast(params) { const { productName, pageSize, current } = params; const countnameFilter = ( productName ? productName.split(' ').filter(Boolean) : [] ) .map(name => `AND p.name LIKE '%${name}%'`) .join(' '); const offset = (current - 1) * pageSize; const countSql = ` WITH product_list AS ( SELECT DISTINCT s.productSku FROM stock s LEFT JOIN stock_point sp ON s.stockPointId = sp.id LEFT JOIN product p ON s.productSku = p.sku WHERE sp.ignore = FALSE ${countnameFilter} ) SELECT COUNT(*) AS total FROM product_list; `; const nameFilter = ( productName ? productName.split(' ').filter(Boolean) : [] ) .map(name => `AND pns.productName LIKE '%${name}%'`) .join(' '); const sql = ` WITH stock_summary AS ( SELECT 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.productSku ), transfer_stock AS ( SELECT 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.productSku ), 30_sales_summary AS ( SELECT os.sku AS productSku, SUM(os.quantity) AS totalSales FROM order_sale os JOIN \`order\` o ON os.orderId = o.id WHERE o.status IN ('completed', 'refunded', 'processing') AND o.date_paid >= NOW() - INTERVAL 30 DAY GROUP BY os.sku ), 15_sales_summary AS ( SELECT os.sku AS productSku, 2 * SUM(os.quantity) AS totalSales FROM order_sale os JOIN \`order\` o ON os.orderId = o.id WHERE o.status IN ('completed', 'refunded', 'processing') AND o.date_paid >= NOW() - INTERVAL 15 DAY GROUP BY os.sku ), sales_max_summary AS ( SELECT 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.productSku = s15.productSku UNION ALL SELECT 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.productSku = s15.productSku WHERE s30.productSku IS NULL ), product_name_summary AS ( SELECT 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.productSku, ss.stockDetails, COALESCE(ts.transitStock, 0) AS transitStock, (COALESCE(ss.totalStock, 0) + COALESCE(ts.transitStock, 0)) AS totalStock, sales.totalSales_30, sales.totalSales_15, CASE WHEN sales.maxSales = 0 THEN NULL ELSE FLOOR((COALESCE(ss.totalStock, 0) + COALESCE(ts.transitStock, 0)) / sales.maxSales * 30) END AS availableDays, CASE WHEN sales.maxSales = 0 THEN NULL ELSE FLOOR(COALESCE(ss.caTotalStock, 0) / sales.maxSales * 30) END AS caAvailableDays, ss.caTotalStock, sales.maxSales * 4 AS restockQuantity, pns.productName FROM stock_summary ss 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 LIMIT ${pageSize} OFFSET ${offset}; `; const totalResult = await this.orderRepository.query(countSql); const total = totalResult[0]?.total || 0; const items = await this.orderRepository.query(sql); return { items, total, current, pageSize, }; } async restocking(params) { const { productName, pageSize, current } = params; const countnameFilter = ( productName ? productName.split(' ').filter(Boolean) : [] ) .map(name => `AND p.name LIKE '%${name}%'`) .join(' '); const offset = (current - 1) * pageSize; const countSql = ` WITH product_list AS ( SELECT DISTINCT s.productSku FROM stock s LEFT JOIN stock_point sp ON s.stockPointId = sp.id LEFT JOIN product p ON s.productSku = p.sku WHERE sp.ignore = FALSE ${countnameFilter} ) SELECT COUNT(*) AS total FROM product_list; `; const nameFilter = ( productName ? productName.split(' ').filter(Boolean) : [] ) .map(name => `AND pns.productName LIKE '%${name}%'`) .join(' '); const sql = ` WITH stock_summary AS ( SELECT 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.productSku ), transfer_stock AS ( SELECT 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.productSku ), b_sales_data_raw As ( SELECT 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.productSku, month ), sales_data_raw AS ( SELECT 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, SUM(CASE WHEN DAY(o.date_paid) > 20 THEN os.quantity ELSE 0 END) AS late_sales FROM order_sale os JOIN \`order\` o ON os.orderId = o.id WHERE o.status IN ('completed', 'refunded', 'processing') AND o.date_paid >= DATE_FORMAT(NOW() - INTERVAL 2 MONTH, '%Y-%m-01') GROUP BY os.sku, month ), monthly_sales_summary AS ( SELECT sdr.productSku, JSON_ARRAYAGG( JSON_OBJECT( 'month', sdr.month, 'early_sales', sdr.early_sales, 'mid_sales', sdr.mid_sales, 'late_sales', sdr.late_sales, 'b_sales', COALESCE(b.sales,0) ) ) AS sales_data FROM sales_data_raw sdr 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 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 FROM order_sale os JOIN \`order\` o ON os.orderId = o.id WHERE o.status IN ('completed', 'refunded', 'processing') AND o.date_paid >= CURDATE() - INTERVAL 2 MONTH GROUP BY os.sku ), product_name_summary AS ( SELECT 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.productSku, (COALESCE(ss.totalStock, 0) + COALESCE(ts.transitStock, 0)) AS totalStock, ms.sales_data AS monthlySalesData, pns.productName, COALESCE(ssum.last_30_days_sales, 0) AS last30DaysSales, COALESCE(ssum.last_15_days_sales, 0) AS last15DaysSales, COALESCE(ssum.last_month_sales, 0) AS lastMonthSales, CASE WHEN COALESCE(ssum.last_month_sales, 0) > 0 THEN (COALESCE(ss.totalStock, 0) + COALESCE(ts.transitStock, 0)) / ssum.last_month_sales ELSE NULL END AS stock_ratio FROM stock_summary ss 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 stock_ratio IS NULL ASC, stock_ratio ASC LIMIT ${pageSize} OFFSET ${offset}; `; const totalResult = await this.orderRepository.query(countSql); const total = totalResult[0]?.total || 0; const items = await this.orderRepository.query(sql); return { items, total, current, pageSize, }; } async getOrderSorce(params){ const sql = ` WITH cutoff_months AS ( SELECT DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 7 MONTH), '%Y-%m') AS start_month, DATE_FORMAT(CURDATE(), '%Y-%m') AS end_month ), user_first_order AS ( SELECT customer_email, DATE_FORMAT(MIN(date_paid), '%Y-%m') AS first_order_month FROM \`order\` WHERE status IN ('processing', 'completed') GROUP BY customer_email ), order_months AS ( SELECT customer_email, DATE_FORMAT(date_paid, '%Y-%m') AS order_month FROM \`order\` WHERE status IN ('processing', 'completed') ), filtered_orders AS ( SELECT o.customer_email, o.order_month, u.first_order_month, 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 WHERE o.order_month >= c.start_month ), classified AS ( SELECT order_month, CASE WHEN first_order_month < start_month THEN CONCAT('>', start_month) ELSE first_order_month END AS first_order_month_group FROM filtered_orders ), final_counts AS ( SELECT order_month, first_order_month_group, COUNT(*) AS order_count FROM classified GROUP BY order_month, first_order_month_group ) SELECT * FROM final_counts ORDER BY order_month DESC, first_order_month_group ` const inactiveSql = ` WITH cutoff_months AS ( SELECT DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 7 MONTH), '%Y-%m') AS start_month, DATE_FORMAT(CURDATE(), '%Y-%m') AS end_month ), user_orders AS ( SELECT customer_email, DATE_FORMAT(date_paid, '%Y-%m') AS order_month, date_paid FROM \`order\` WHERE status IN ('processing', 'completed') ), filtered_users AS ( SELECT * FROM user_orders u JOIN cutoff_months c ON u.order_month >= c.start_month ), monthly_users AS ( SELECT DISTINCT customer_email, order_month FROM filtered_users ), -- 每个用户的首单月份 first_order_months AS ( SELECT customer_email, MIN(DATE_FORMAT(date_paid, '%Y-%m')) AS first_order_month FROM user_orders GROUP BY customer_email ), -- 标注每个用户每月是“新客户”还是“老客户” labeled_users AS ( SELECT m.customer_email, m.order_month, CASE WHEN f.first_order_month = m.order_month THEN 'new' ELSE 'returning' END AS customer_type FROM monthly_users m JOIN first_order_months f ON m.customer_email = f.customer_email ), -- 每月新老客户统计 monthly_new_old_counts AS ( 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 FROM labeled_users GROUP BY order_month ), -- 未来是否下单的检测 user_future_orders AS ( SELECT u1.customer_email, u1.order_month AS current_month, COUNT(u2.order_month) AS future_order_count FROM monthly_users u1 LEFT JOIN user_orders u2 ON u1.customer_email = u2.customer_email AND u2.order_month > u1.order_month GROUP BY u1.customer_email, u1.order_month ), users_without_future_orders AS ( SELECT current_month AS order_month, COUNT(DISTINCT customer_email) AS inactive_user_count FROM user_future_orders WHERE future_order_count = 0 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 FROM monthly_new_old_counts m LEFT JOIN users_without_future_orders i ON m.order_month = i.order_month ORDER BY m.order_month DESC; ` const [res, inactiveRes ] = await Promise.all([ this.orderRepository.query(sql), this.orderRepository.query(inactiveSql), ]) return { res,inactiveRes } } async getInativeUsersByMonth(month: string) { const sql = ` WITH current_month_orders AS ( SELECT DISTINCT customer_email FROM \`order\` WHERE status IN ('processing', 'completed') AND DATE_FORMAT(date_paid, '%Y-%m') = '${month}' ), future_orders AS ( SELECT DISTINCT customer_email FROM \`order\` WHERE status IN ('processing', 'completed') AND DATE_FORMAT(date_paid, '%Y-%m') > '${month}' ), inactive_customers AS ( SELECT c.customer_email FROM current_month_orders c LEFT JOIN future_orders f ON c.customer_email = f.customer_email WHERE f.customer_email IS NULL ) SELECT o.customer_email AS email, MIN(o.date_paid) AS first_purchase_date, MAX(o.date_paid) AS last_purchase_date, COUNT(DISTINCT o.id) AS orders, SUM(o.total) AS total, ANY_VALUE(o.shipping) AS shipping, ANY_VALUE(o.billing) AS billing, ( SELECT JSON_ARRAYAGG(tag) FROM customer_tag ct WHERE ct.email = o.customer_email ) AS tags FROM \`order\` o JOIN inactive_customers i ON o.customer_email = i.customer_email WHERE o.status IN ('processing', 'completed') GROUP BY o.customer_email ` const res = await this.orderRepository.query(sql) return res } }