forked from yoone/API
1
0
Fork 0

feat: 添加订单导出功能并支持按周/月分组统计

feat(订单): 在订单DTO中添加分组字段
feat(订单服务): 实现订单导出为CSV功能
feat(统计服务): 支持按周/月分组统计订单数据
fix(站点服务): 跳过名称为空的站点配置
chore: 更新gitignore忽略本地配置文件
This commit is contained in:
zhuotianyuan 2025-12-23 16:54:29 +08:00
parent 450105972b
commit c68b7583af
5 changed files with 677 additions and 51 deletions

View File

@ -252,4 +252,21 @@ export class OrderController {
return errorResponse(error?.message || '获取失败'); 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 || '导出失败');
}
}
} }

View File

@ -33,4 +33,8 @@ export class OrderStatisticsParams {
@ApiProperty({ enum: ['all', 'zyn', 'yoone', 'zolt'], default: 'all' }) @ApiProperty({ enum: ['all', 'zyn', 'yoone', 'zolt'], default: 'all' })
@Rule(RuleType.string().valid('all', 'zyn', 'yoone', 'zolt')) @Rule(RuleType.string().valid('all', 'zyn', 'yoone', 'zolt'))
brand: string; brand: string;
@ApiProperty({ enum: ['day', 'week', 'month'], default: 'day' })
@Rule(RuleType.string().valid('day', 'week', 'month'))
grouping: string;
} }

View File

@ -33,7 +33,9 @@ import { ShipmentItem } from '../entity/shipment_item.entity';
import { UpdateStockDTO } from '../dto/stock.dto'; import { UpdateStockDTO } from '../dto/stock.dto';
import { StockService } from './stock.service'; import { StockService } from './stock.service';
import { OrderItemOriginal } from '../entity/order_item_original.entity'; import { OrderItemOriginal } from '../entity/order_item_original.entity';
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
@Provide() @Provide()
export class OrderService { 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<number, OrderItem[]>);
// 构建导出数据
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<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 });
}
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}`);
}
}
} }

View File

@ -22,6 +22,10 @@ export class SiteService {
async syncFromConfig(sites: WpSite[] = []) { async syncFromConfig(sites: WpSite[] = []) {
// 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化) // 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化)
for (const siteConfig of sites) { for (const siteConfig of sites) {
if (!siteConfig.name) {
console.warn('跳过空名称的站点配置');
continue;
}
// 按站点名称查询是否已存在记录 // 按站点名称查询是否已存在记录
const exist = await this.siteModel.findOne({ const exist = await this.siteModel.findOne({
where: { name: siteConfig.name }, where: { name: siteConfig.name },

View File

@ -15,12 +15,14 @@ export class StatisticsService {
orderItemRepository: Repository<OrderItem>; orderItemRepository: Repository<OrderItem>;
async getOrderStatistics(params: OrderStatisticsParams) { async getOrderStatistics(params: OrderStatisticsParams) {
const { startDate, endDate, siteId } = params; const { startDate, endDate, siteId ,grouping} = params;
// const keywords = keyword ? keyword.split(' ').filter(Boolean) : []; // const keywords = keyword ? keyword.split(' ').filter(Boolean) : [];
const start = dayjs(startDate).format('YYYY-MM-DD'); const start = dayjs(startDate).format('YYYY-MM-DD');
const end = dayjs(endDate).add(1, 'd').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 SELECT customer_email, MIN(date_paid) AS first_purchase_date
FROM \`order\` FROM \`order\`
GROUP BY customer_email GROUP BY customer_email
@ -214,6 +216,393 @@ export class StatisticsService {
dt.can_total_orders dt.can_total_orders
ORDER BY d.order_date DESC; 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); return this.orderRepository.query(sql);
} }
// async getOrderStatistics(params: OrderStatisticsParams) { // async getOrderStatistics(params: OrderStatisticsParams) {
@ -656,10 +1045,10 @@ export class StatisticsService {
const offset = (current - 1) * pageSize; const offset = (current - 1) * pageSize;
const countSql = ` const countSql = `
WITH product_list AS ( WITH product_list AS (
SELECT DISTINCT s.sku SELECT DISTINCT s.productSku
FROM stock s FROM stock s
LEFT JOIN stock_point sp ON s.stockPointId = sp.id 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 WHERE sp.ignore = FALSE
${countnameFilter} ${countnameFilter}
) )
@ -674,27 +1063,27 @@ export class StatisticsService {
const sql = ` const sql = `
WITH stock_summary AS ( WITH stock_summary AS (
SELECT SELECT
s.sku, s.productSku,
JSON_ARRAYAGG(JSON_OBJECT('id', sp.id, 'quantity', s.quantity)) AS stockDetails, JSON_ARRAYAGG(JSON_OBJECT('id', sp.id, 'quantity', s.quantity)) AS stockDetails,
SUM(s.quantity) AS totalStock, SUM(s.quantity) AS totalStock,
SUM(CASE WHEN sp.inCanada THEN s.quantity ELSE 0 END) AS caTotalStock SUM(CASE WHEN sp.inCanada THEN s.quantity ELSE 0 END) AS caTotalStock
FROM stock s FROM stock s
JOIN stock_point sp ON s.stockPointId = sp.id JOIN stock_point sp ON s.stockPointId = sp.id
WHERE sp.ignore = FALSE WHERE sp.ignore = FALSE
GROUP BY s.sku GROUP BY s.productSku
), ),
transfer_stock AS ( transfer_stock AS (
SELECT SELECT
ti.sku, ti.productSku,
SUM(ti.quantity) AS transitStock SUM(ti.quantity) AS transitStock
FROM transfer_item ti FROM transfer_item ti
JOIN transfer t ON ti.transferId = t.id JOIN transfer t ON ti.transferId = t.id
WHERE t.isCancel = FALSE AND t.isArrived = FALSE WHERE t.isCancel = FALSE AND t.isArrived = FALSE
GROUP BY ti.sku GROUP BY ti.productSku
), ),
30_sales_summary AS ( 30_sales_summary AS (
SELECT SELECT
os.sku AS sku, os.sku AS productSku,
SUM(os.quantity) AS totalSales SUM(os.quantity) AS totalSales
FROM order_sale os FROM order_sale os
JOIN \`order\` o ON os.orderId = o.id JOIN \`order\` o ON os.orderId = o.id
@ -704,7 +1093,7 @@ export class StatisticsService {
), ),
15_sales_summary AS ( 15_sales_summary AS (
SELECT SELECT
os.sku AS sku, os.sku AS productSku,
2 * SUM(os.quantity) AS totalSales 2 * SUM(os.quantity) AS totalSales
FROM order_sale os FROM order_sale os
JOIN \`order\` o ON os.orderId = o.id JOIN \`order\` o ON os.orderId = o.id
@ -714,36 +1103,36 @@ export class StatisticsService {
), ),
sales_max_summary AS ( sales_max_summary AS (
SELECT SELECT
s30.sku AS sku, s30.productSku AS productSku,
COALESCE(s30.totalSales, 0) AS totalSales_30, COALESCE(s30.totalSales, 0) AS totalSales_30,
COALESCE(s15.totalSales, 0) AS totalSales_15, COALESCE(s15.totalSales, 0) AS totalSales_15,
GREATEST(COALESCE(s30.totalSales, 0), COALESCE(s15.totalSales, 0)) AS maxSales GREATEST(COALESCE(s30.totalSales, 0), COALESCE(s15.totalSales, 0)) AS maxSales
FROM 30_sales_summary s30 FROM 30_sales_summary s30
LEFT JOIN 15_sales_summary s15 LEFT JOIN 15_sales_summary s15
ON s30.sku = s15.sku ON s30.productSku = s15.productSku
UNION ALL UNION ALL
SELECT SELECT
s15.sku AS sku, s15.productSku AS productSku,
0 AS totalSales_30, 0 AS totalSales_30,
COALESCE(s15.totalSales, 0) AS totalSales_15, COALESCE(s15.totalSales, 0) AS totalSales_15,
COALESCE(s15.totalSales, 0) AS maxSales COALESCE(s15.totalSales, 0) AS maxSales
FROM 15_sales_summary s15 FROM 15_sales_summary s15
LEFT JOIN 30_sales_summary s30 LEFT JOIN 30_sales_summary s30
ON s30.sku = s15.sku ON s30.productSku = s15.productSku
WHERE s30.sku IS NULL WHERE s30.productSku IS NULL
), ),
product_name_summary AS ( product_name_summary AS (
SELECT SELECT
p.sku AS sku, p.sku AS productSku,
COALESCE(MAX(os.name), MAX(p.name)) AS productName COALESCE(MAX(os.name), MAX(p.name)) AS productName
FROM product p FROM product p
LEFT JOIN order_sale os ON p.sku = os.sku LEFT JOIN order_sale os ON p.sku = os.sku
GROUP BY p.sku GROUP BY p.sku
) )
SELECT SELECT
ss.sku, ss.productSku,
ss.stockDetails, ss.stockDetails,
COALESCE(ts.transitStock, 0) AS transitStock, COALESCE(ts.transitStock, 0) AS transitStock,
(COALESCE(ss.totalStock, 0) + COALESCE(ts.transitStock, 0)) AS totalStock, (COALESCE(ss.totalStock, 0) + COALESCE(ts.transitStock, 0)) AS totalStock,
@ -761,9 +1150,9 @@ export class StatisticsService {
sales.maxSales * 4 AS restockQuantity, sales.maxSales * 4 AS restockQuantity,
pns.productName pns.productName
FROM stock_summary ss FROM stock_summary ss
LEFT JOIN transfer_stock ts ON ss.sku = ts.sku LEFT JOIN transfer_stock ts ON ss.productSku = ts.productSku
LEFT JOIN sales_max_summary sales ON ss.sku = sales.sku LEFT JOIN sales_max_summary sales ON ss.productSku = sales.productSku
LEFT JOIN product_name_summary pns ON ss.sku = pns.sku LEFT JOIN product_name_summary pns ON ss.productSku = pns.productSku
WHERE 1 = 1 WHERE 1 = 1
${nameFilter} ${nameFilter}
ORDER BY caAvailableDays ORDER BY caAvailableDays
@ -791,10 +1180,10 @@ export class StatisticsService {
const offset = (current - 1) * pageSize; const offset = (current - 1) * pageSize;
const countSql = ` const countSql = `
WITH product_list AS ( WITH product_list AS (
SELECT DISTINCT s.sku SELECT DISTINCT s.productSku
FROM stock s FROM stock s
LEFT JOIN stock_point sp ON s.stockPointId = sp.id 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 WHERE sp.ignore = FALSE
${countnameFilter} ${countnameFilter}
) )
@ -810,36 +1199,36 @@ export class StatisticsService {
const sql = ` const sql = `
WITH stock_summary AS ( WITH stock_summary AS (
SELECT SELECT
s.sku, s.productSku,
SUM(s.quantity) AS totalStock SUM(s.quantity) AS totalStock
FROM stock s FROM stock s
JOIN stock_point sp ON s.stockPointId = sp.id JOIN stock_point sp ON s.stockPointId = sp.id
WHERE sp.ignore = FALSE WHERE sp.ignore = FALSE
GROUP BY s.sku GROUP BY s.productSku
), ),
transfer_stock AS ( transfer_stock AS (
SELECT SELECT
ti.sku, ti.productSku,
SUM(ti.quantity) AS transitStock SUM(ti.quantity) AS transitStock
FROM transfer_item ti FROM transfer_item ti
JOIN transfer t ON ti.transferId = t.id JOIN transfer t ON ti.transferId = t.id
WHERE t.isCancel = FALSE AND t.isArrived = FALSE WHERE t.isCancel = FALSE AND t.isArrived = FALSE
GROUP BY ti.sku GROUP BY ti.productSku
), ),
b_sales_data_raw As ( b_sales_data_raw As (
SELECT SELECT
sr.sku, sr.productSku,
DATE_FORMAT(sr.createdAt, '%Y-%m') AS month, DATE_FORMAT(sr.createdAt, '%Y-%m') AS month,
SUM(sr.quantityChange) AS sales SUM(sr.quantityChange) AS sales
FROM stock_record sr FROM stock_record sr
JOIN stock_point sp ON sr.stockPointId = sp.id JOIN stock_point sp ON sr.stockPointId = sp.id
WHERE sp.isB WHERE sp.isB
AND sr.createdAt >= DATE_FORMAT(NOW() - INTERVAL 2 MONTH, '%Y-%m-01') 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 ( sales_data_raw AS (
SELECT SELECT
os.sku AS sku, os.sku AS productSku,
DATE_FORMAT(o.date_paid, '%Y-%m') AS month, 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 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) > 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 ( monthly_sales_summary AS (
SELECT SELECT
sdr.sku, sdr.productSku,
JSON_ARRAYAGG( JSON_ARRAYAGG(
JSON_OBJECT( JSON_OBJECT(
'month', sdr.month, 'month', sdr.month,
@ -863,12 +1252,12 @@ export class StatisticsService {
) )
) AS sales_data ) AS sales_data
FROM sales_data_raw sdr FROM sales_data_raw sdr
LEFT JOIN b_sales_data_raw b ON sdr.sku = b.sku AND sdr.month = b.month LEFT JOIN b_sales_data_raw b ON sdr.productSku = b.productSku AND sdr.month = b.month
GROUP BY sdr.sku GROUP BY sdr.productSku
), ),
sales_summary AS ( sales_summary AS (
SELECT 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 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 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 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 ( product_name_summary AS (
SELECT SELECT
p.sku AS sku, p.sku AS productSku,
COALESCE(MAX(os.name), MAX(p.name)) AS productName COALESCE(MAX(os.name), MAX(p.name)) AS productName
FROM product p FROM product p
LEFT JOIN order_sale os ON p.sku = os.sku LEFT JOIN order_sale os ON p.sku = os.sku
GROUP BY p.sku GROUP BY p.sku
) )
SELECT SELECT
ss.sku, ss.productSku,
(COALESCE(ss.totalStock, 0) + COALESCE(ts.transitStock, 0)) AS totalStock, (COALESCE(ss.totalStock, 0) + COALESCE(ts.transitStock, 0)) AS totalStock,
ms.sales_data AS monthlySalesData, ms.sales_data AS monthlySalesData,
pns.productName, pns.productName,
@ -900,10 +1289,10 @@ export class StatisticsService {
ELSE NULL ELSE NULL
END AS stock_ratio END AS stock_ratio
FROM stock_summary ss FROM stock_summary ss
LEFT JOIN transfer_stock ts ON ss.sku = ts.sku LEFT JOIN transfer_stock ts ON ss.productSku = ts.productSku
LEFT JOIN monthly_sales_summary ms ON ss.sku = ms.sku LEFT JOIN monthly_sales_summary ms ON ss.productSku = ms.productSku
LEFT JOIN product_name_summary pns ON ss.sku = pns.sku LEFT JOIN product_name_summary pns ON ss.productSku = pns.productSku
LEFT JOIN sales_summary ssum ON ss.sku = ssum.sku LEFT JOIN sales_summary ssum ON ss.productSku = ssum.productSku
WHERE 1 = 1 WHERE 1 = 1
${nameFilter} ${nameFilter}
ORDER BY ORDER BY
@ -933,7 +1322,8 @@ export class StatisticsService {
user_first_order AS ( user_first_order AS (
SELECT SELECT
customer_email, 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\` FROM \`order\`
WHERE status IN ('processing', 'completed') WHERE status IN ('processing', 'completed')
GROUP BY customer_email GROUP BY customer_email
@ -946,7 +1336,7 @@ export class StatisticsService {
WHERE status IN ('processing', 'completed') WHERE status IN ('processing', 'completed')
), ),
filtered_orders AS ( 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 FROM order_months o
JOIN user_first_order u ON o.customer_email = u.customer_email JOIN user_first_order u ON o.customer_email = u.customer_email
JOIN cutoff_months c ON 1=1 JOIN cutoff_months c ON 1=1
@ -958,14 +1348,16 @@ export class StatisticsService {
CASE CASE
WHEN first_order_month < start_month THEN CONCAT('>', start_month) WHEN first_order_month < start_month THEN CONCAT('>', start_month)
ELSE first_order_month ELSE first_order_month
END AS first_order_month_group END AS first_order_month_group,
first_order_total
FROM filtered_orders FROM filtered_orders
), ),
final_counts AS ( final_counts AS (
SELECT SELECT
order_month, order_month,
first_order_month_group, first_order_month_group,
COUNT(*) AS order_count COUNT(*) AS order_count,
SUM(first_order_total) AS total
FROM classified FROM classified
GROUP BY order_month, first_order_month_group GROUP BY order_month, first_order_month_group
) )
@ -985,7 +1377,8 @@ export class StatisticsService {
SELECT SELECT
customer_email, customer_email,
DATE_FORMAT(date_paid, '%Y-%m') AS order_month, DATE_FORMAT(date_paid, '%Y-%m') AS order_month,
date_paid date_paid,
total
FROM \`order\` FROM \`order\`
WHERE status IN ('processing', 'completed') WHERE status IN ('processing', 'completed')
), ),
@ -998,7 +1391,8 @@ export class StatisticsService {
monthly_users AS ( monthly_users AS (
SELECT DISTINCT SELECT DISTINCT
customer_email, customer_email,
order_month order_month,
total
FROM filtered_users FROM filtered_users
), ),
@ -1011,11 +1405,12 @@ export class StatisticsService {
GROUP BY customer_email GROUP BY customer_email
), ),
-- "新客户""老客户" --
labeled_users AS ( labeled_users AS (
SELECT SELECT
m.customer_email, m.customer_email,
m.order_month, m.order_month,
m.total,
CASE CASE
WHEN f.first_order_month = m.order_month THEN 'new' WHEN f.first_order_month = m.order_month THEN 'new'
ELSE 'returning' ELSE 'returning'
@ -1029,7 +1424,9 @@ export class StatisticsService {
SELECT SELECT
order_month, order_month,
SUM(CASE WHEN customer_type = 'new' THEN 1 ELSE 0 END) AS new_user_count, 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 FROM labeled_users
GROUP BY order_month GROUP BY order_month
), ),
@ -1056,12 +1453,14 @@ export class StatisticsService {
GROUP BY current_month GROUP BY current_month
) )
-- 最终结果:每月新客户,, --
SELECT SELECT
m.order_month, m.order_month,
m.new_user_count, m.new_user_count,
m.old_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 FROM monthly_new_old_counts m
LEFT JOIN users_without_future_orders i LEFT JOIN users_without_future_orders i
ON m.order_month = i.order_month ON m.order_month = i.order_month