Compare commits

..

No commits in common. "343379519b29ade77003910bb5aea5b49b9504cc" and "3750b15298e2257301676a499fe66de4b27ad0a9" have entirely different histories.

16 changed files with 170 additions and 987 deletions

View File

@ -27,8 +27,6 @@ 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
@ -64,8 +62,6 @@ export default {
OrderNote,
Transfer,
TransferItem,
CustomerTag,
Customer,
],
synchronize: true,
logging: false,

View File

@ -1,70 +0,0 @@
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);
}
}
}

View File

@ -194,14 +194,4 @@ 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 || '获取失败');
}
}
}

View File

@ -122,24 +122,6 @@ 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,
})

View File

@ -1,4 +1,4 @@
import { Body, Controller, Get, Inject, Post, Query } from '@midwayjs/core';
import { Body, Controller, Inject, Post } from '@midwayjs/core';
import { StatisticsService } from '../service/statistics.service';
import { OrderStatisticsParams } from '../dto/statistics.dto';
import { errorResponse, successResponse } from '../utils/response.util';
@ -76,24 +76,4 @@ 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 || '获取失败');
}
}
}

View File

@ -154,17 +154,6 @@ 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) {

View File

@ -1,38 +0,0 @@
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;
}

View File

@ -1,10 +0,0 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('customer')
export class Customer {
@PrimaryGeneratedColumn()
id: number;
@Column({ unique: true })
email: string;
}

View File

@ -1,25 +0,0 @@
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;
}

View File

@ -27,10 +27,6 @@ export class Product {
@Column()
name: string;
@ApiProperty()
@Column({ default: ''})
nameCn: string;
@ApiProperty({ example: '产品描述', description: '产品描述', type: 'string' })
@Column({ nullable: true })
description?: string;

View File

@ -1,151 +0,0 @@
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);
}
}

View File

@ -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) {

View File

@ -14,7 +14,6 @@ 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,
@ -86,9 +85,6 @@ 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) {
@ -163,16 +159,15 @@ export class OrderService {
where: { orderId: existingOrder.id },
});
if (!items) return;
const stockPointId = 2;
// ['YT', 'NT', 'BC', 'AB', 'SK'].some(
// v =>
// v.toLowerCase() ===
// (
// existingOrder?.shipping?.state || existingOrder?.billing?.state
// ).toLowerCase()
// )
// ? 3
// : 2;
const stockPointId = ['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;
@ -216,14 +211,6 @@ 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);
}
@ -734,292 +721,173 @@ 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) : [];
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')
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(' OR ') + ')';
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
LIMIT ? OFFSET ?
`;
sqlQuery += ' LIMIT ? OFFSET ?';
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
WHERE o.date_paid BETWEEN ? AND ?
AND o.status IN ('completed', 'processing')
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 = ?';
countParams.push(siteId);
totalCountParameters.push(siteId);
}
if (nameKeywords.length > 0) {
totalCountQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')';
countParams.push(...nameKeywords.map(word => `%${word}%`));
totalCountQuery +=
' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND ');
totalCountParameters.push(...nameKeywords.map(word => `%${word}%`));
}
const totalCountResult = await this.orderSaleModel.query(totalCountQuery, countParams);
const totalCountResult = await this.orderSaleModel.query(
totalCountQuery,
totalCountParameters
);
// 一次查询获取所有 yoone box 数量
const totalQuantityParams: any[] = [startDate, endDate];
let totalQuantityQuery = `
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
SELECT SUM(os.quantity) AS totalQuantity
FROM order_sale os
INNER JOIN \`order\` o ON o.id = os.orderId
WHERE o.date_paid BETWEEN ? AND ?
AND o.status IN ('completed', 'processing')
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 = ?';
totalQuantityParams.push(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(' OR ') + ')';
totalQuantityParams.push(...nameKeywords.map(word => `%${word}%`));
totalQuantityQuery +=
' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND ');
totalQuantityParameters.push(...nameKeywords.map(word => `%${word}%`));
}
const [totalQuantityResult] = await this.orderSaleModel.query(totalQuantityQuery, totalQuantityParams);
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 || 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),
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 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,
@ -1318,41 +1186,4 @@ 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,
};
}
}

View File

@ -1,5 +1,5 @@
import { Provide } from '@midwayjs/core';
import { In, Like, Not, Repository } from 'typeorm';
import { And, In, IsNull, Like, Not, Repository } from 'typeorm';
import { Product } from '../entity/product.entty';
import { Category } from '../entity/category.entity';
import { paginate } from '../utils/paginate.util';
@ -46,58 +46,19 @@ 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) : [];
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);
if (nameFilter.length > 0) {
const nameConditions = nameFilter.map(word => Like(`%${word}%`));
where.name = And(...nameConditions);
}
query.take(50);
return await query.getMany();
where.sku = Not(IsNull());
// 查询 SKU 不为空且 name 包含关键字的产品,最多返回 50 条
return this.productModel.find({
where,
take: 50,
});
}
async findProductBySku(sku: string): Promise<Product> {
@ -123,7 +84,6 @@ 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',
@ -209,18 +169,6 @@ 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> {
// 检查产品是否存在
const product = await this.productModel.findOneBy({ id });

View File

@ -104,8 +104,7 @@ 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' THEN total ELSE 0 END) AS direct_total,
SUM(CASE WHEN source_type = 'organic' THEN total ELSE 0 END) AS organic_total
SUM(CASE WHEN source_type = 'typein' AND purchase_type = 'first_purchase' THEN total ELSE 0 END) AS direct_first_total
FROM daily_orders
GROUP BY order_date
)
@ -125,8 +124,7 @@ 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' THEN d.order_id END) AS direct_orders,
COUNT(DISTINCT CASE WHEN d.source_type = 'organic' THEN d.order_id END) AS organic_orders,
COUNT(DISTINCT CASE WHEN d.source_type = 'typein' AND d.purchase_type = 'first_purchase' THEN d.order_id END) AS direct_first_orders,
dt.total_orders,
dt.togo_total_orders,
dt.can_total_orders,
@ -143,8 +141,7 @@ export class StatisticsService {
dt.non_yoone_total,
dt.zex_total,
dt.non_zex_total,
dt.direct_total,
dt.organic_total,
dt.direct_first_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,
@ -207,8 +204,7 @@ export class StatisticsService {
dt.non_yoone_total,
dt.zex_total,
dt.non_zex_total,
dt.direct_total,
dt.organic_total,
dt.direct_first_total,
dt.total_orders,
dt.togo_total_orders,
dt.can_total_orders
@ -319,11 +315,6 @@ 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,
@ -339,21 +330,14 @@ 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,
(
SELECT JSON_ARRAYAGG(tag)
FROM customer_tag ct
WHERE ct.email = o.customer_email
) AS tags
cs.total_spent
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 ?
@ -373,12 +357,7 @@ export class StatisticsService {
o.*,
JSON_ARRAYAGG(
JSON_OBJECT('name', oi.name, 'quantity', oi.quantity, 'total', oi.total)
) AS orderItems,
(
SELECT JSON_ARRAYAGG(tag)
FROM customer_tag ct
WHERE ct.email = o.customer_email
) AS tags
) AS orderItems
FROM \`order\` o
LEFT JOIN order_item oi ON oi.orderId = o.id
WHERE o.customer_email='${email}'
@ -922,206 +901,4 @@ 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
}
}

View File

@ -218,16 +218,6 @@ 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;
@ -241,15 +231,13 @@ 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.nameCn');
.addGroupBy('product.name');
let totalQueryBuilder = this.stockModel
.createQueryBuilder('stock')
.select('COUNT(DISTINCT stock.productSku)', 'count')
@ -324,9 +312,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);
}
@ -387,14 +375,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: {