Compare commits
2 Commits
3750b15298
...
343379519b
| Author | SHA1 | Date |
|---|---|---|
|
|
343379519b | |
|
|
ba777c3563 |
|
|
@ -27,6 +27,8 @@ import { Transfer } from '../entity/transfer.entity';
|
|||
import { TransferItem } from '../entity/transfer_item.entity';
|
||||
import { Strength } from '../entity/strength.entity';
|
||||
import { Flavors } from '../entity/flavors.entity';
|
||||
import { CustomerTag } from '../entity/customer_tag.entity';
|
||||
import { Customer } from '../entity/customer.entity';
|
||||
|
||||
export default {
|
||||
// use for cookie sign key, should change to your own and keep security
|
||||
|
|
@ -62,6 +64,8 @@ export default {
|
|||
OrderNote,
|
||||
Transfer,
|
||||
TransferItem,
|
||||
CustomerTag,
|
||||
Customer,
|
||||
],
|
||||
synchronize: true,
|
||||
logging: false,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
import {
|
||||
Body,
|
||||
Context,
|
||||
Controller,
|
||||
Del,
|
||||
Get,
|
||||
Inject,
|
||||
Post,
|
||||
Query,
|
||||
} from '@midwayjs/core';
|
||||
import { CustomerService } from '../service/customer.service';
|
||||
import { errorResponse, successResponse } from '../utils/response.util';
|
||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||
import { BooleanRes } from '../dto/reponse.dto';
|
||||
import { CustomerTagDTO, QueryCustomerListDTO } from '../dto/customer.dto';
|
||||
|
||||
@Controller('/customer')
|
||||
export class CustomerController {
|
||||
@Inject()
|
||||
ctx: Context;
|
||||
|
||||
@Inject()
|
||||
customerService: CustomerService;
|
||||
|
||||
@ApiOkResponse()
|
||||
@Get('/list')
|
||||
async getCustomerList(@Query() param: QueryCustomerListDTO) {
|
||||
try {
|
||||
console.log(param);
|
||||
const data = await this.customerService.getCustomerList(param);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({ type: BooleanRes })
|
||||
@Post('/tag/add')
|
||||
async addTag(@Body() dto: CustomerTagDTO) {
|
||||
try {
|
||||
await this.customerService.addTag(dto.email, dto.tag);
|
||||
return successResponse(true);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({ type: BooleanRes })
|
||||
@Del('/tag/del')
|
||||
async delTag(@Body() dto: CustomerTagDTO) {
|
||||
try {
|
||||
await this.customerService.delTag(dto.email, dto.tag);
|
||||
return successResponse(true);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Get('/tags')
|
||||
async getTags() {
|
||||
try {
|
||||
const data = await this.customerService.getTags();
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -194,4 +194,14 @@ export class OrderController {
|
|||
return errorResponse(error?.message || '创建失败');
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Post('/order/pengding/items')
|
||||
async pengdingItems(@Body() data: Record<string, any>) {
|
||||
try {
|
||||
return successResponse(await this.orderService.pengdingItems(data));
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || '获取失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -122,6 +122,24 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
type: ProductRes,
|
||||
})
|
||||
@Put('updateNameCn/:id/:nameCn')
|
||||
async updateProductNameCn(
|
||||
@Param('id') id: number,
|
||||
@Param('nameCn') nameCn: string
|
||||
) {
|
||||
try {
|
||||
const data = this.productService.updateProductNameCn(id, nameCn);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ApiOkResponse({
|
||||
type: BooleanRes,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Body, Controller, Inject, Post } from '@midwayjs/core';
|
||||
import { Body, Controller, Get, Inject, Post, Query } from '@midwayjs/core';
|
||||
import { StatisticsService } from '../service/statistics.service';
|
||||
import { OrderStatisticsParams } from '../dto/statistics.dto';
|
||||
import { errorResponse, successResponse } from '../utils/response.util';
|
||||
|
|
@ -76,4 +76,24 @@ export class StatisticsController {
|
|||
return errorResponse(error?.message || '获取失败');
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Get('/orderSource')
|
||||
async getOrderSorce(@Query() params) {
|
||||
try {
|
||||
return successResponse(await this.statisticsService.getOrderSorce(params));
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || '获取失败');
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse()
|
||||
@Get('/inactiveUsersByMonth')
|
||||
async getInativeUsersByMonth(@Query('month') month: string) {
|
||||
try {
|
||||
return successResponse(await this.statisticsService.getInativeUsersByMonth(month));
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || '获取失败');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -154,6 +154,17 @@ export class StockController {
|
|||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({ type: BooleanRes })
|
||||
@Get('/purchase-order/:orderNumber')
|
||||
async getPurchaseOrder(@Param('orderNumber') orderNumber: string) {
|
||||
try {
|
||||
const data = await this.stockService.getPurchaseOrder(orderNumber);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || '更新失败');
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({ type: StockListRes, description: '获取库存列表' })
|
||||
@Get('/')
|
||||
async getStocks(@Query() query: QueryStockDTO) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
|
||||
export class QueryCustomerListDTO {
|
||||
@ApiProperty()
|
||||
current: string;
|
||||
|
||||
@ApiProperty()
|
||||
pageSize: string;
|
||||
|
||||
@ApiProperty()
|
||||
email: string;
|
||||
|
||||
@ApiProperty()
|
||||
tags: string;
|
||||
|
||||
@ApiProperty()
|
||||
sorterKey: string;
|
||||
|
||||
@ApiProperty()
|
||||
sorterValue: string;
|
||||
|
||||
@ApiProperty()
|
||||
state: string;
|
||||
|
||||
@ApiProperty()
|
||||
first_purchase_date: string;
|
||||
|
||||
@ApiProperty()
|
||||
customerId: number;
|
||||
}
|
||||
|
||||
export class CustomerTagDTO {
|
||||
@ApiProperty()
|
||||
email: string;
|
||||
|
||||
@ApiProperty()
|
||||
tag: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity('customer')
|
||||
export class Customer {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column({ unique: true })
|
||||
email: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity('customer_tag')
|
||||
export class CustomerTag {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@Column()
|
||||
email: string;
|
||||
|
||||
@Column()
|
||||
tag: string;
|
||||
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
|
@ -27,6 +27,10 @@ export class Product {
|
|||
@Column()
|
||||
name: string;
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ default: ''})
|
||||
nameCn: string;
|
||||
|
||||
@ApiProperty({ example: '产品描述', description: '产品描述', type: 'string' })
|
||||
@Column({ nullable: true })
|
||||
description?: string;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,151 @@
|
|||
import { Provide } from '@midwayjs/core';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
import { Order } from '../entity/order.entity';
|
||||
import { Repository } from 'typeorm';
|
||||
import { CustomerTag } from '../entity/customer_tag.entity';
|
||||
|
||||
@Provide()
|
||||
export class CustomerService {
|
||||
@InjectEntityModel(Order)
|
||||
orderModel: Repository<Order>;
|
||||
|
||||
@InjectEntityModel(CustomerTag)
|
||||
customerTagModel: Repository<CustomerTag>;
|
||||
|
||||
async getCustomerList(param: Record<string, any>) {
|
||||
const {
|
||||
current = 1,
|
||||
pageSize = 10,
|
||||
email,
|
||||
tags,
|
||||
sorterKey,
|
||||
sorterValue,
|
||||
state,
|
||||
first_purchase_date,
|
||||
customerId,
|
||||
} = param;
|
||||
const whereConds: string[] = [];
|
||||
const havingConds: string[] = [];
|
||||
|
||||
if (email) {
|
||||
whereConds.push(`o.customer_email LIKE '%${email}%'`);
|
||||
}
|
||||
if (state) {
|
||||
whereConds.push(
|
||||
`JSON_UNQUOTE(JSON_EXTRACT(o.billing, '$.state')) = '${state}'`
|
||||
);
|
||||
}
|
||||
if (customerId) {
|
||||
whereConds.push(`
|
||||
o.customer_email = (
|
||||
SELECT email FROM customer WHERE id = ${Number(customerId)}
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
const tagList = tags
|
||||
.split(',')
|
||||
.map(tag => `'${tag.trim()}'`)
|
||||
.join(',');
|
||||
havingConds.push(`
|
||||
EXISTS (
|
||||
SELECT 1 FROM customer_tag ct
|
||||
WHERE ct.email = o.customer_email
|
||||
AND ct.tag IN (${tagList})
|
||||
)
|
||||
`);
|
||||
}
|
||||
if (first_purchase_date) {
|
||||
havingConds.push(
|
||||
`DATE_FORMAT(MIN(o.date_paid), '%Y-%m') = '${first_purchase_date}'`
|
||||
);
|
||||
}
|
||||
const baseQuery = `
|
||||
${whereConds.length ? `WHERE ${whereConds.join(' AND ')}` : ''}
|
||||
GROUP BY o.customer_email
|
||||
${havingConds.length ? `HAVING ${havingConds.join(' AND ')}` : ''}
|
||||
`;
|
||||
|
||||
let sql = `
|
||||
select
|
||||
o.customer_email as email,
|
||||
MIN(date_created) as date_created,
|
||||
MIN(date_paid) as first_purchase_date,
|
||||
MAX(date_paid) as last_purchase_date,
|
||||
COUNT(DISTINCT o.id) as orders,
|
||||
SUM(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,
|
||||
(
|
||||
SELECT id FROM customer c WHERE c.email = o.customer_email
|
||||
) as customerId,
|
||||
yoone_stats.yoone_orders,
|
||||
yoone_stats.yoone_total
|
||||
FROM \`order\` o
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
oo.customer_email,
|
||||
COUNT(DISTINCT oi.orderId) AS yoone_orders,
|
||||
SUM(oi.total) AS yoone_total
|
||||
FROM order_item oi
|
||||
JOIN \`order\` oo ON oi.orderId = oo.id
|
||||
WHERE oi.name LIKE '%yoone%'
|
||||
GROUP BY oo.customer_email
|
||||
) yoone_stats ON yoone_stats.customer_email = o.customer_email
|
||||
${baseQuery}
|
||||
${
|
||||
sorterKey
|
||||
? `ORDER BY ${sorterKey} ${
|
||||
sorterValue === 'descend' ? 'DESC' : 'ASC'
|
||||
}`
|
||||
: 'ORDER BY orders ASC, yoone_total DESC'
|
||||
}
|
||||
limit ${pageSize} offset ${(current - 1) * pageSize}
|
||||
`;
|
||||
const countSql = `
|
||||
SELECT COUNT(*) AS total FROM (
|
||||
SELECT o.customer_email
|
||||
FROM \`order\` o
|
||||
${baseQuery}
|
||||
) AS sub
|
||||
`;
|
||||
const [items, countResult] = await Promise.all([
|
||||
this.orderModel.query(sql),
|
||||
this.orderModel.query(countSql),
|
||||
]);
|
||||
|
||||
const total = countResult[0]?.total || 0;
|
||||
return {
|
||||
items,
|
||||
total,
|
||||
current,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
async addTag(email: string, tag: string) {
|
||||
const isExist = await this.customerTagModel.findOneBy({ email, tag });
|
||||
if (isExist) throw new Error(`${tag}已存在`);
|
||||
return await this.customerTagModel.save({ email, tag });
|
||||
}
|
||||
|
||||
async delTag(email: string, tag: string) {
|
||||
const isExist = await this.customerTagModel.findOneBy({ email, tag });
|
||||
if (!isExist) throw new Error(`${tag}不存在`);
|
||||
return await this.customerTagModel.delete({ email, tag });
|
||||
}
|
||||
|
||||
async getTags() {
|
||||
const tags = await this.customerTagModel
|
||||
.createQueryBuilder('tag')
|
||||
.select('DISTINCT tag.tag', 'tag')
|
||||
.getRawMany();
|
||||
return tags.map(t => t.tag);
|
||||
}
|
||||
}
|
||||
|
|
@ -195,16 +195,16 @@ export class LogisticsService {
|
|||
) {
|
||||
throw new Error('订单状态不正确 ');
|
||||
}
|
||||
for (const item of data?.sales) {
|
||||
const stock = await this.stockModel.findOne({
|
||||
where: {
|
||||
stockPointId: data.stockPointId,
|
||||
productSku: item.sku,
|
||||
},
|
||||
});
|
||||
if (!stock || stock.quantity < item.quantity)
|
||||
throw new Error(item.name + '库存不足');
|
||||
}
|
||||
// for (const item of data?.sales) {
|
||||
// const stock = await this.stockModel.findOne({
|
||||
// where: {
|
||||
// stockPointId: data.stockPointId,
|
||||
// productSku: item.sku,
|
||||
// },
|
||||
// });
|
||||
// if (!stock || stock.quantity < item.quantity)
|
||||
// throw new Error(item.name + '库存不足');
|
||||
// }
|
||||
let shipment: Shipment;
|
||||
|
||||
if (data.service_type === ShipmentType.FREIGHTCOM) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import { OrderRefundItem } from '../entity/order_retund_item.entity';
|
|||
import { OrderCoupon } from '../entity/order_copon.entity';
|
||||
import { OrderShipping } from '../entity/order_shipping.entity';
|
||||
import { Shipment } from '../entity/shipment.entity';
|
||||
import { Customer } from '../entity/customer.entity';
|
||||
import {
|
||||
ErpOrderStatus,
|
||||
OrderStatus,
|
||||
|
|
@ -85,6 +86,9 @@ export class OrderService {
|
|||
@Inject()
|
||||
dataSourceManager: TypeORMDataSourceManager;
|
||||
|
||||
@InjectEntityModel(Customer)
|
||||
customerModel: Repository<Customer>;
|
||||
|
||||
async syncOrders(siteId: string) {
|
||||
const orders = await this.wPService.getOrders(siteId); // 调用 WooCommerce API 获取订单
|
||||
for (const order of orders) {
|
||||
|
|
@ -159,15 +163,16 @@ export class OrderService {
|
|||
where: { orderId: existingOrder.id },
|
||||
});
|
||||
if (!items) return;
|
||||
const stockPointId = ['YT', 'NT', 'BC', 'AB', 'SK'].some(
|
||||
v =>
|
||||
v.toLowerCase() ===
|
||||
(
|
||||
existingOrder?.shipping?.state || existingOrder?.billing?.state
|
||||
).toLowerCase()
|
||||
)
|
||||
? 3
|
||||
: 2;
|
||||
const stockPointId = 2;
|
||||
// ['YT', 'NT', 'BC', 'AB', 'SK'].some(
|
||||
// v =>
|
||||
// v.toLowerCase() ===
|
||||
// (
|
||||
// existingOrder?.shipping?.state || existingOrder?.billing?.state
|
||||
// ).toLowerCase()
|
||||
// )
|
||||
// ? 3
|
||||
// : 2;
|
||||
for (const item of items) {
|
||||
const updateStock = new UpdateStockDTO();
|
||||
updateStock.stockPointId = stockPointId;
|
||||
|
|
@ -211,6 +216,14 @@ export class OrderService {
|
|||
return entity;
|
||||
}
|
||||
entity.orderStatus = this.mapOrderStatus(entity.status);
|
||||
const customer = await this.customerModel.findOne({
|
||||
where: { email: order.customer_email },
|
||||
});
|
||||
if(!customer) {
|
||||
await this.customerModel.save({
|
||||
email: order.customer_email,
|
||||
});
|
||||
}
|
||||
return await this.orderModel.save(entity);
|
||||
}
|
||||
|
||||
|
|
@ -721,173 +734,292 @@ export class OrderService {
|
|||
return await query.getRawMany();
|
||||
}
|
||||
|
||||
async getOrderSales({
|
||||
siteId,
|
||||
startDate,
|
||||
endDate,
|
||||
current,
|
||||
pageSize,
|
||||
name,
|
||||
}: QueryOrderSalesDTO) {
|
||||
async getOrderSales({ siteId, startDate, endDate, current, pageSize, name }: QueryOrderSalesDTO) {
|
||||
const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
|
||||
// 分页查询
|
||||
let sqlQuery = `
|
||||
WITH product_purchase_counts AS (
|
||||
SELECT o.customer_email,os.productId, os.name, COUNT(DISTINCT o.id,os.productId) AS order_count
|
||||
FROM \`order\` o
|
||||
JOIN order_sale os ON o.id = os.orderId
|
||||
WHERE o.status IN ('completed', 'processing')
|
||||
GROUP BY o.customer_email, os.productId, os.name
|
||||
)
|
||||
SELECT
|
||||
os.productId AS productId,
|
||||
os.name AS name,
|
||||
SUM(os.quantity) AS totalQuantity,
|
||||
COUNT(distinct os.orderId) AS totalOrders,
|
||||
c.name AS categoryName,
|
||||
COUNT(DISTINCT CASE WHEN pc.order_count = 1 THEN o.id END) AS firstOrderCount,
|
||||
SUM(CASE WHEN pc.order_count = 1 THEN os.quantity ELSE 0 END) AS firstOrderYOONEBoxCount,
|
||||
COUNT(DISTINCT CASE WHEN pc.order_count = 2 THEN o.id END) AS secondOrderCount,
|
||||
SUM(CASE WHEN pc.order_count = 2 THEN os.quantity ELSE 0 END) AS secondOrderYOONEBoxCount,
|
||||
COUNT(DISTINCT CASE WHEN pc.order_count = 3 THEN o.id END) AS thirdOrderCount,
|
||||
SUM(CASE WHEN pc.order_count = 3 THEN os.quantity ELSE 0 END) AS thirdOrderYOONEBoxCount,
|
||||
COUNT(DISTINCT CASE WHEN pc.order_count > 3 THEN o.id END) AS moreThirdOrderCount,
|
||||
SUM(CASE WHEN pc.order_count > 3 THEN os.quantity ELSE 0 END) AS moreThirdOrderYOONEBoxCount
|
||||
FROM order_sale os
|
||||
INNER JOIN \`order\` o ON o.id = os.orderId
|
||||
INNER JOIN product p ON os.productId = p.id
|
||||
INNER JOIN category c ON p.categoryId = c.id
|
||||
INNER JOIN product_purchase_counts pc ON pc.customer_email = o.customer_email AND pc.productId = os.productId
|
||||
WHERE o.date_paid BETWEEN ? AND ?
|
||||
AND o.status IN ('processing', 'completed')
|
||||
`;
|
||||
|
||||
const parameters: any[] = [startDate, endDate];
|
||||
|
||||
// 主查询:带分页
|
||||
let sqlQuery = `
|
||||
WITH product_purchase_counts AS (
|
||||
SELECT
|
||||
o.customer_email,
|
||||
os.productId,
|
||||
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')
|
||||
GROUP BY o.customer_email, os.productId
|
||||
)
|
||||
SELECT
|
||||
os.productId AS productId,
|
||||
os.name AS name,
|
||||
SUM(os.quantity) AS totalQuantity,
|
||||
COUNT(DISTINCT os.orderId) AS totalOrders,
|
||||
c.name AS categoryName,
|
||||
COUNT(DISTINCT CASE WHEN pc.order_count = 1 THEN o.id END) AS firstOrderCount,
|
||||
SUM(CASE WHEN pc.order_count = 1 THEN os.quantity ELSE 0 END) AS firstOrderYOONEBoxCount,
|
||||
COUNT(DISTINCT CASE WHEN pc.order_count = 2 THEN o.id END) AS secondOrderCount,
|
||||
SUM(CASE WHEN pc.order_count = 2 THEN os.quantity ELSE 0 END) AS secondOrderYOONEBoxCount,
|
||||
COUNT(DISTINCT CASE WHEN pc.order_count = 3 THEN o.id END) AS thirdOrderCount,
|
||||
SUM(CASE WHEN pc.order_count = 3 THEN os.quantity ELSE 0 END) AS thirdOrderYOONEBoxCount,
|
||||
COUNT(DISTINCT CASE WHEN pc.order_count > 3 THEN o.id END) AS moreThirdOrderCount,
|
||||
SUM(CASE WHEN pc.order_count > 3 THEN os.quantity ELSE 0 END) AS moreThirdOrderYOONEBoxCount
|
||||
FROM order_sale os
|
||||
INNER JOIN \`order\` o ON o.id = os.orderId
|
||||
INNER JOIN product p ON os.productId = p.id
|
||||
INNER JOIN category c ON p.categoryId = c.id
|
||||
INNER JOIN product_purchase_counts pc ON pc.customer_email = o.customer_email AND pc.productId = os.productId
|
||||
WHERE o.date_paid BETWEEN ? AND ?
|
||||
AND o.status IN ('completed', 'processing')
|
||||
`;
|
||||
|
||||
if (siteId) {
|
||||
sqlQuery += ' AND os.siteId = ?';
|
||||
parameters.push(siteId);
|
||||
}
|
||||
|
||||
if (nameKeywords.length > 0) {
|
||||
sqlQuery +=
|
||||
' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND ');
|
||||
sqlQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')';
|
||||
parameters.push(...nameKeywords.map(word => `%${word}%`));
|
||||
}
|
||||
|
||||
sqlQuery += `
|
||||
GROUP BY os.productId, os.name, c.name
|
||||
ORDER BY totalQuantity DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
sqlQuery += ' LIMIT ? OFFSET ?';
|
||||
parameters.push(pageSize, (current - 1) * pageSize);
|
||||
|
||||
// 执行查询并传递参数
|
||||
parameters.push(pageSize, (current - 1) * pageSize);
|
||||
const items = await this.orderSaleModel.query(sqlQuery, parameters);
|
||||
|
||||
// 总条数
|
||||
const countParams: any[] = [startDate, endDate];
|
||||
let totalCountQuery = `
|
||||
SELECT COUNT(DISTINCT os.productId) AS totalCount
|
||||
FROM order_sale os
|
||||
INNER JOIN \`order\` o ON o.id = os.orderId
|
||||
INNER JOIN product p ON os.productId = p.id
|
||||
INNER JOIN category c ON p.categoryId = c.id
|
||||
WHERE o.date_created BETWEEN ? AND ?
|
||||
AND o.status IN ('processing', 'completed')
|
||||
WHERE o.date_paid BETWEEN ? AND ?
|
||||
AND o.status IN ('completed', 'processing')
|
||||
`;
|
||||
const totalCountParameters: any[] = [startDate, endDate];
|
||||
if (siteId) {
|
||||
totalCountQuery += ' AND os.siteId = ?';
|
||||
totalCountParameters.push(siteId);
|
||||
countParams.push(siteId);
|
||||
}
|
||||
if (nameKeywords.length > 0) {
|
||||
totalCountQuery +=
|
||||
' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND ');
|
||||
totalCountParameters.push(...nameKeywords.map(word => `%${word}%`));
|
||||
totalCountQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')';
|
||||
countParams.push(...nameKeywords.map(word => `%${word}%`));
|
||||
}
|
||||
|
||||
const totalCountResult = await this.orderSaleModel.query(
|
||||
totalCountQuery,
|
||||
totalCountParameters
|
||||
);
|
||||
const totalCountResult = await this.orderSaleModel.query(totalCountQuery, countParams);
|
||||
|
||||
// 一次查询获取所有 yoone box 数量
|
||||
const totalQuantityParams: any[] = [startDate, endDate];
|
||||
let totalQuantityQuery = `
|
||||
SELECT SUM(os.quantity) AS totalQuantity
|
||||
SELECT
|
||||
SUM(os.quantity) AS totalQuantity,
|
||||
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%3%' THEN os.quantity ELSE 0 END) AS yoone3Quantity,
|
||||
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%6%' THEN os.quantity ELSE 0 END) AS yoone6Quantity,
|
||||
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%9%' THEN os.quantity ELSE 0 END) AS yoone9Quantity,
|
||||
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%12%' THEN os.quantity ELSE 0 END) AS yoone12Quantity,
|
||||
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%15%' THEN os.quantity ELSE 0 END) AS yoone15Quantity
|
||||
FROM order_sale os
|
||||
INNER JOIN \`order\` o ON o.id = os.orderId
|
||||
INNER JOIN product p ON os.productId = p.id
|
||||
INNER JOIN category c ON p.categoryId = c.id
|
||||
WHERE o.date_created BETWEEN ? AND ?
|
||||
AND o.status IN ('processing', 'completed')
|
||||
WHERE o.date_paid BETWEEN ? AND ?
|
||||
AND o.status IN ('completed', 'processing')
|
||||
`;
|
||||
|
||||
const totalQuantityParameters: any[] = [startDate, endDate];
|
||||
if (siteId) {
|
||||
totalQuantityQuery += ' AND os.siteId = ?';
|
||||
totalQuantityParameters.push(siteId);
|
||||
totalQuantityParams.push(siteId);
|
||||
}
|
||||
const yoone3QuantityQuery =
|
||||
totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%3%"';
|
||||
const yoone6QuantityQuery =
|
||||
totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%6%"';
|
||||
const yoone9QuantityQuery =
|
||||
totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%9%"';
|
||||
const yoone12QuantityQuery =
|
||||
totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%12%"';
|
||||
const yoone15QuantityQuery =
|
||||
totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%15%"';
|
||||
const yooneParameters = [...totalQuantityParameters];
|
||||
if (nameKeywords.length > 0) {
|
||||
totalQuantityQuery +=
|
||||
' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND ');
|
||||
totalQuantityParameters.push(...nameKeywords.map(word => `%${word}%`));
|
||||
totalQuantityQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')';
|
||||
totalQuantityParams.push(...nameKeywords.map(word => `%${word}%`));
|
||||
}
|
||||
|
||||
const totalQuantityResult = await this.orderSaleModel.query(
|
||||
totalQuantityQuery,
|
||||
totalQuantityParameters
|
||||
);
|
||||
const yoone3QuantityResult = await this.orderSaleModel.query(
|
||||
yoone3QuantityQuery,
|
||||
yooneParameters
|
||||
);
|
||||
const yoone6QuantityResult = await this.orderSaleModel.query(
|
||||
yoone6QuantityQuery,
|
||||
yooneParameters
|
||||
);
|
||||
const yoone9QuantityResult = await this.orderSaleModel.query(
|
||||
yoone9QuantityQuery,
|
||||
yooneParameters
|
||||
);
|
||||
const yoone12QuantityResult = await this.orderSaleModel.query(
|
||||
yoone12QuantityQuery,
|
||||
yooneParameters
|
||||
);
|
||||
const yoone15QuantityResult = await this.orderSaleModel.query(
|
||||
yoone15QuantityQuery,
|
||||
yooneParameters
|
||||
);
|
||||
const [totalQuantityResult] = await this.orderSaleModel.query(totalQuantityQuery, totalQuantityParams);
|
||||
|
||||
return {
|
||||
items,
|
||||
total: totalCountResult[0]?.totalCount,
|
||||
totalQuantity: Number(
|
||||
totalQuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
|
||||
),
|
||||
yoone3Quantity: Number(
|
||||
yoone3QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
|
||||
),
|
||||
yoone6Quantity: Number(
|
||||
yoone6QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
|
||||
),
|
||||
yoone9Quantity: Number(
|
||||
yoone9QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
|
||||
),
|
||||
yoone12Quantity: Number(
|
||||
yoone12QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
|
||||
),
|
||||
yoone15Quantity: Number(
|
||||
yoone15QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
|
||||
),
|
||||
total: totalCountResult[0]?.totalCount || 0,
|
||||
totalQuantity: Number(totalQuantityResult.totalQuantity || 0),
|
||||
yoone3Quantity: Number(totalQuantityResult.yoone3Quantity || 0),
|
||||
yoone6Quantity: Number(totalQuantityResult.yoone6Quantity || 0),
|
||||
yoone9Quantity: Number(totalQuantityResult.yoone9Quantity || 0),
|
||||
yoone12Quantity: Number(totalQuantityResult.yoone12Quantity || 0),
|
||||
yoone15Quantity: Number(totalQuantityResult.yoone15Quantity || 0),
|
||||
current,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
// async getOrderSales({
|
||||
// siteId,
|
||||
// startDate,
|
||||
// endDate,
|
||||
// current,
|
||||
// pageSize,
|
||||
// name,
|
||||
// }: QueryOrderSalesDTO) {
|
||||
// const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
|
||||
// // 分页查询
|
||||
// let sqlQuery = `
|
||||
// WITH product_purchase_counts AS (
|
||||
// SELECT o.customer_email,os.productId, os.name, COUNT(DISTINCT o.id,os.productId) AS order_count
|
||||
// FROM \`order\` o
|
||||
// JOIN order_sale os ON o.id = os.orderId
|
||||
// WHERE o.status IN ('completed', 'processing')
|
||||
// GROUP BY o.customer_email, os.productId, os.name
|
||||
// )
|
||||
// SELECT
|
||||
// os.productId AS productId,
|
||||
// os.name AS name,
|
||||
// SUM(os.quantity) AS totalQuantity,
|
||||
// COUNT(distinct os.orderId) AS totalOrders,
|
||||
// c.name AS categoryName,
|
||||
// COUNT(DISTINCT CASE WHEN pc.order_count = 1 THEN o.id END) AS firstOrderCount,
|
||||
// SUM(CASE WHEN pc.order_count = 1 THEN os.quantity ELSE 0 END) AS firstOrderYOONEBoxCount,
|
||||
// COUNT(DISTINCT CASE WHEN pc.order_count = 2 THEN o.id END) AS secondOrderCount,
|
||||
// SUM(CASE WHEN pc.order_count = 2 THEN os.quantity ELSE 0 END) AS secondOrderYOONEBoxCount,
|
||||
// COUNT(DISTINCT CASE WHEN pc.order_count = 3 THEN o.id END) AS thirdOrderCount,
|
||||
// SUM(CASE WHEN pc.order_count = 3 THEN os.quantity ELSE 0 END) AS thirdOrderYOONEBoxCount,
|
||||
// COUNT(DISTINCT CASE WHEN pc.order_count > 3 THEN o.id END) AS moreThirdOrderCount,
|
||||
// SUM(CASE WHEN pc.order_count > 3 THEN os.quantity ELSE 0 END) AS moreThirdOrderYOONEBoxCount
|
||||
// FROM order_sale os
|
||||
// INNER JOIN \`order\` o ON o.id = os.orderId
|
||||
// INNER JOIN product p ON os.productId = p.id
|
||||
// INNER JOIN category c ON p.categoryId = c.id
|
||||
// INNER JOIN product_purchase_counts pc ON pc.customer_email = o.customer_email AND pc.productId = os.productId
|
||||
// WHERE o.date_paid BETWEEN ? AND ?
|
||||
// AND o.status IN ('processing', 'completed')
|
||||
// `;
|
||||
// const parameters: any[] = [startDate, endDate];
|
||||
// if (siteId) {
|
||||
// sqlQuery += ' AND os.siteId = ?';
|
||||
// parameters.push(siteId);
|
||||
// }
|
||||
// if (nameKeywords.length > 0) {
|
||||
// sqlQuery +=
|
||||
// ' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND ');
|
||||
// parameters.push(...nameKeywords.map(word => `%${word}%`));
|
||||
// }
|
||||
// sqlQuery += `
|
||||
// GROUP BY os.productId, os.name, c.name
|
||||
// ORDER BY totalQuantity DESC
|
||||
// `;
|
||||
// sqlQuery += ' LIMIT ? OFFSET ?';
|
||||
// parameters.push(pageSize, (current - 1) * pageSize);
|
||||
|
||||
// // 执行查询并传递参数
|
||||
// const items = await this.orderSaleModel.query(sqlQuery, parameters);
|
||||
|
||||
// let totalCountQuery = `
|
||||
// SELECT COUNT(DISTINCT os.productId) AS totalCount
|
||||
// FROM order_sale os
|
||||
// INNER JOIN \`order\` o ON o.id = os.orderId
|
||||
// INNER JOIN product p ON os.productId = p.id
|
||||
// INNER JOIN category c ON p.categoryId = c.id
|
||||
// WHERE o.date_created BETWEEN ? AND ?
|
||||
// AND o.status IN ('processing', 'completed')
|
||||
// `;
|
||||
// const totalCountParameters: any[] = [startDate, endDate];
|
||||
// if (siteId) {
|
||||
// totalCountQuery += ' AND os.siteId = ?';
|
||||
// totalCountParameters.push(siteId);
|
||||
// }
|
||||
// if (nameKeywords.length > 0) {
|
||||
// totalCountQuery +=
|
||||
// ' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND ');
|
||||
// totalCountParameters.push(...nameKeywords.map(word => `%${word}%`));
|
||||
// }
|
||||
|
||||
// const totalCountResult = await this.orderSaleModel.query(
|
||||
// totalCountQuery,
|
||||
// totalCountParameters
|
||||
// );
|
||||
|
||||
// let totalQuantityQuery = `
|
||||
// SELECT SUM(os.quantity) AS totalQuantity
|
||||
// FROM order_sale os
|
||||
// INNER JOIN \`order\` o ON o.id = os.orderId
|
||||
// INNER JOIN product p ON os.productId = p.id
|
||||
// INNER JOIN category c ON p.categoryId = c.id
|
||||
// WHERE o.date_created BETWEEN ? AND ?
|
||||
// AND o.status IN ('processing', 'completed')
|
||||
// `;
|
||||
|
||||
// const totalQuantityParameters: any[] = [startDate, endDate];
|
||||
// if (siteId) {
|
||||
// totalQuantityQuery += ' AND os.siteId = ?';
|
||||
// totalQuantityParameters.push(siteId);
|
||||
// }
|
||||
// const yoone3QuantityQuery =
|
||||
// totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%3%"';
|
||||
// const yoone6QuantityQuery =
|
||||
// totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%6%"';
|
||||
// const yoone9QuantityQuery =
|
||||
// totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%9%"';
|
||||
// const yoone12QuantityQuery =
|
||||
// totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%12%"';
|
||||
// const yoone15QuantityQuery =
|
||||
// totalQuantityQuery + 'AND os.name LIKE "%yoone%" AND os.name LIKE "%15%"';
|
||||
// const yooneParameters = [...totalQuantityParameters];
|
||||
// if (nameKeywords.length > 0) {
|
||||
// totalQuantityQuery +=
|
||||
// ' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND ');
|
||||
// totalQuantityParameters.push(...nameKeywords.map(word => `%${word}%`));
|
||||
// }
|
||||
|
||||
// const totalQuantityResult = await this.orderSaleModel.query(
|
||||
// totalQuantityQuery,
|
||||
// totalQuantityParameters
|
||||
// );
|
||||
// const yoone3QuantityResult = await this.orderSaleModel.query(
|
||||
// yoone3QuantityQuery,
|
||||
// yooneParameters
|
||||
// );
|
||||
// const yoone6QuantityResult = await this.orderSaleModel.query(
|
||||
// yoone6QuantityQuery,
|
||||
// yooneParameters
|
||||
// );
|
||||
// const yoone9QuantityResult = await this.orderSaleModel.query(
|
||||
// yoone9QuantityQuery,
|
||||
// yooneParameters
|
||||
// );
|
||||
// const yoone12QuantityResult = await this.orderSaleModel.query(
|
||||
// yoone12QuantityQuery,
|
||||
// yooneParameters
|
||||
// );
|
||||
// const yoone15QuantityResult = await this.orderSaleModel.query(
|
||||
// yoone15QuantityQuery,
|
||||
// yooneParameters
|
||||
// );
|
||||
|
||||
// return {
|
||||
// items,
|
||||
// total: totalCountResult[0]?.totalCount,
|
||||
// totalQuantity: Number(
|
||||
// totalQuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
|
||||
// ),
|
||||
// yoone3Quantity: Number(
|
||||
// yoone3QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
|
||||
// ),
|
||||
// yoone6Quantity: Number(
|
||||
// yoone6QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
|
||||
// ),
|
||||
// yoone9Quantity: Number(
|
||||
// yoone9QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
|
||||
// ),
|
||||
// yoone12Quantity: Number(
|
||||
// yoone12QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
|
||||
// ),
|
||||
// yoone15Quantity: Number(
|
||||
// yoone15QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0)
|
||||
// ),
|
||||
// current,
|
||||
// pageSize,
|
||||
// };
|
||||
// }
|
||||
async getOrderItems({
|
||||
siteId,
|
||||
startDate,
|
||||
|
|
@ -1186,4 +1318,41 @@ export class OrderService {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
async pengdingItems(data: Record<string, any>) {
|
||||
const { current = 1, pageSize = 10 } = data;
|
||||
const sql = `
|
||||
SELECT
|
||||
os.name,
|
||||
SUM(os.quantity) AS quantity,
|
||||
JSON_ARRAYAGG(os.orderId) AS numbers
|
||||
FROM \`order\` o
|
||||
INNER JOIN order_sale os ON os.orderId = o.id
|
||||
WHERE o.status = 'processing'
|
||||
GROUP BY os.name
|
||||
LIMIT ${pageSize} OFFSET ${(current - 1) * pageSize}
|
||||
`;
|
||||
|
||||
const countSql = `
|
||||
SELECT COUNT(*) AS total FROM (
|
||||
SELECT 1
|
||||
FROM \`order\` o
|
||||
INNER JOIN order_sale os ON os.orderId = o.id
|
||||
WHERE o.status = 'processing'
|
||||
GROUP BY os.name
|
||||
) AS temp
|
||||
`;
|
||||
const [items, countResult] = await Promise.all([
|
||||
this.orderModel.query(sql),
|
||||
this.orderModel.query(countSql),
|
||||
]);
|
||||
|
||||
const total = countResult[0]?.total || 0;
|
||||
return {
|
||||
items,
|
||||
total,
|
||||
current,
|
||||
pageSize,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Provide } from '@midwayjs/core';
|
||||
import { And, In, IsNull, Like, Not, Repository } from 'typeorm';
|
||||
import { In, Like, Not, Repository } from 'typeorm';
|
||||
import { Product } from '../entity/product.entty';
|
||||
import { Category } from '../entity/category.entity';
|
||||
import { paginate } from '../utils/paginate.util';
|
||||
|
|
@ -46,19 +46,58 @@ export class ProductService {
|
|||
@InjectEntityModel(Variation)
|
||||
variationModel: Repository<Variation>;
|
||||
|
||||
// async findProductsByName(name: string): Promise<Product[]> {
|
||||
// const where: any = {};
|
||||
// const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||||
// if (nameFilter.length > 0) {
|
||||
// const nameConditions = nameFilter.map(word => Like(`%${word}%`));
|
||||
// where.name = And(...nameConditions);
|
||||
// }
|
||||
// if(name){
|
||||
// where.nameCn = Like(`%${name}%`)
|
||||
// }
|
||||
// where.sku = Not(IsNull());
|
||||
// // 查询 SKU 不为空且 name 包含关键字的产品,最多返回 50 条
|
||||
// return this.productModel.find({
|
||||
// where,
|
||||
// take: 50,
|
||||
// });
|
||||
// }
|
||||
|
||||
async findProductsByName(name: string): Promise<Product[]> {
|
||||
const where: any = {};
|
||||
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||||
if (nameFilter.length > 0) {
|
||||
const nameConditions = nameFilter.map(word => Like(`%${word}%`));
|
||||
where.name = And(...nameConditions);
|
||||
const query = this.productModel.createQueryBuilder('product');
|
||||
|
||||
// 保证 sku 不为空
|
||||
query.where('product.sku IS NOT NULL');
|
||||
|
||||
if (nameFilter.length > 0 || name) {
|
||||
const params: Record<string, string> = {};
|
||||
const conditions: string[] = [];
|
||||
|
||||
// 英文名关键词全部匹配(AND)
|
||||
if (nameFilter.length > 0) {
|
||||
const nameConds = nameFilter.map((word, index) => {
|
||||
const key = `name${index}`;
|
||||
params[key] = `%${word}%`;
|
||||
return `product.name LIKE :${key}`;
|
||||
});
|
||||
conditions.push(`(${nameConds.join(' AND ')})`);
|
||||
}
|
||||
|
||||
// 中文名模糊匹配
|
||||
if (name) {
|
||||
params['nameCn'] = `%${name}%`;
|
||||
conditions.push(`product.nameCn LIKE :nameCn`);
|
||||
}
|
||||
|
||||
// 英文名关键词匹配 OR 中文名匹配
|
||||
query.andWhere(`(${conditions.join(' OR ')})`, params);
|
||||
}
|
||||
where.sku = Not(IsNull());
|
||||
// 查询 SKU 不为空且 name 包含关键字的产品,最多返回 50 条
|
||||
return this.productModel.find({
|
||||
where,
|
||||
take: 50,
|
||||
});
|
||||
|
||||
query.take(50);
|
||||
|
||||
return await query.getMany();
|
||||
}
|
||||
|
||||
async findProductBySku(sku: string): Promise<Product> {
|
||||
|
|
@ -84,6 +123,7 @@ export class ProductService {
|
|||
.select([
|
||||
'product.id as id',
|
||||
'product.name as name',
|
||||
'product.nameCn as nameCn',
|
||||
'product.description as description',
|
||||
'product.humidity as humidity',
|
||||
'product.sku as sku',
|
||||
|
|
@ -168,6 +208,18 @@ export class ProductService {
|
|||
// 返回更新后的产品
|
||||
return await this.productModel.findOneBy({ id });
|
||||
}
|
||||
|
||||
async updateProductNameCn(id: number, nameCn: string): Promise<Product> {
|
||||
// 确认产品是否存在
|
||||
const product = await this.productModel.findOneBy({ id });
|
||||
if (!product) {
|
||||
throw new Error(`产品 ID ${id} 不存在`);
|
||||
}
|
||||
// 更新产品
|
||||
await this.productModel.update(id, { nameCn });
|
||||
// 返回更新后的产品
|
||||
return await this.productModel.findOneBy({ id });
|
||||
}
|
||||
|
||||
async deleteProduct(id: number): Promise<boolean> {
|
||||
// 检查产品是否存在
|
||||
|
|
|
|||
|
|
@ -104,7 +104,8 @@ export class StatisticsService {
|
|||
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' AND purchase_type = 'first_purchase' THEN total ELSE 0 END) AS direct_first_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
|
||||
)
|
||||
|
|
@ -124,7 +125,8 @@ export class StatisticsService {
|
|||
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' AND d.purchase_type = 'first_purchase' THEN d.order_id END) AS direct_first_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,
|
||||
|
|
@ -141,7 +143,8 @@ export class StatisticsService {
|
|||
dt.non_yoone_total,
|
||||
dt.zex_total,
|
||||
dt.non_zex_total,
|
||||
dt.direct_first_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,
|
||||
|
|
@ -204,7 +207,8 @@ export class StatisticsService {
|
|||
dt.non_yoone_total,
|
||||
dt.zex_total,
|
||||
dt.non_zex_total,
|
||||
dt.direct_first_total,
|
||||
dt.direct_total,
|
||||
dt.organic_total,
|
||||
dt.total_orders,
|
||||
dt.togo_total_orders,
|
||||
dt.can_total_orders
|
||||
|
|
@ -315,6 +319,11 @@ export class StatisticsService {
|
|||
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,
|
||||
|
|
@ -330,14 +339,21 @@ export class StatisticsService {
|
|||
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
|
||||
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 ?
|
||||
|
|
@ -357,7 +373,12 @@ export class StatisticsService {
|
|||
o.*,
|
||||
JSON_ARRAYAGG(
|
||||
JSON_OBJECT('name', oi.name, 'quantity', oi.quantity, 'total', oi.total)
|
||||
) AS orderItems
|
||||
) 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}'
|
||||
|
|
@ -901,4 +922,206 @@ export class StatisticsService {
|
|||
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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -218,6 +218,16 @@ export class StockService {
|
|||
await this.purchaseOrderModel.save(purchaseOrder);
|
||||
}
|
||||
|
||||
async getPurchaseOrder(orderNumber: string) {
|
||||
const sql = `
|
||||
SELECT poi.* FROM purchase_order po
|
||||
left join purchase_order_item poi on po.id = poi.purchaseOrderId
|
||||
WHERE po.orderNumber = '${orderNumber}'
|
||||
`
|
||||
const data = await this.stockModel.query(sql);
|
||||
return data;
|
||||
}
|
||||
|
||||
// 获取库存列表
|
||||
async getStocks(query: QueryStockDTO) {
|
||||
const { current = 1, pageSize = 10, productName } = query;
|
||||
|
|
@ -231,13 +241,15 @@ export class StockService {
|
|||
// 'stock.id as id',
|
||||
'stock.productSku as productSku',
|
||||
'product.name as productName',
|
||||
'product.nameCn as productNameCn',
|
||||
'JSON_ARRAYAGG(JSON_OBJECT("id", stock.stockPointId, "quantity", stock.quantity)) as stockPoint',
|
||||
'MIN(stock.updatedAt) as updatedAt',
|
||||
'MAX(stock.createdAt) as createdAt',
|
||||
])
|
||||
.leftJoin(Product, 'product', 'product.sku = stock.productSku')
|
||||
.groupBy('stock.productSku')
|
||||
.addGroupBy('product.name');
|
||||
.addGroupBy('product.name')
|
||||
.addGroupBy('product.nameCn');
|
||||
let totalQueryBuilder = this.stockModel
|
||||
.createQueryBuilder('stock')
|
||||
.select('COUNT(DISTINCT stock.productSku)', 'count')
|
||||
|
|
@ -312,9 +324,9 @@ export class StockService {
|
|||
// 更新库存
|
||||
stock.quantity +=
|
||||
operationType === 'in' ? quantityChange : -quantityChange;
|
||||
if (stock.quantity < 0) {
|
||||
throw new Error('库存不足,无法完成操作');
|
||||
}
|
||||
// if (stock.quantity < 0) {
|
||||
// throw new Error('库存不足,无法完成操作');
|
||||
// }
|
||||
await this.stockModel.save(stock);
|
||||
}
|
||||
|
||||
|
|
@ -375,14 +387,14 @@ export class StockService {
|
|||
|
||||
async createTransfer(data: Record<string, any>, userId: number) {
|
||||
const { sourceStockPointId, destStockPointId, sendAt, items, note } = data;
|
||||
for (const item of items) {
|
||||
const stock = await this.stockModel.findOneBy({
|
||||
stockPointId: sourceStockPointId,
|
||||
productSku: item.productSku,
|
||||
});
|
||||
if (!stock || stock.quantity < item.quantity)
|
||||
throw new Error(`${item.productName} 库存不足`);
|
||||
}
|
||||
// for (const item of items) {
|
||||
// const stock = await this.stockModel.findOneBy({
|
||||
// stockPointId: sourceStockPointId,
|
||||
// productSku: item.productSku,
|
||||
// });
|
||||
// if (!stock || stock.quantity < item.quantity)
|
||||
// throw new Error(`${item.productName} 库存不足`);
|
||||
// }
|
||||
const now = dayjs().format('YYYY-MM-DD');
|
||||
const count = await this.transferModel.count({
|
||||
where: {
|
||||
|
|
|
|||
Loading…
Reference in New Issue