forked from yoone/API
1
0
Fork 0

客户列表

This commit is contained in:
cll 2025-07-19 11:42:02 +08:00
parent 3750b15298
commit ba777c3563
16 changed files with 988 additions and 171 deletions

View File

@ -27,6 +27,8 @@ import { Transfer } from '../entity/transfer.entity';
import { TransferItem } from '../entity/transfer_item.entity'; import { TransferItem } from '../entity/transfer_item.entity';
import { Strength } from '../entity/strength.entity'; import { Strength } from '../entity/strength.entity';
import { Flavors } from '../entity/flavors.entity'; import { Flavors } from '../entity/flavors.entity';
import { CustomerTag } from '../entity/customer_tag.entity';
import { Customer } from '../entity/customer.entity';
export default { export default {
// use for cookie sign key, should change to your own and keep security // use for cookie sign key, should change to your own and keep security
@ -62,6 +64,8 @@ export default {
OrderNote, OrderNote,
Transfer, Transfer,
TransferItem, TransferItem,
CustomerTag,
Customer,
], ],
synchronize: true, synchronize: true,
logging: false, logging: false,

View File

@ -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);
}
}
}

View File

@ -194,4 +194,14 @@ export class OrderController {
return errorResponse(error?.message || '创建失败'); 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,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({ @ApiOkResponse({
type: BooleanRes, type: BooleanRes,
}) })

View File

@ -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 { StatisticsService } from '../service/statistics.service';
import { OrderStatisticsParams } from '../dto/statistics.dto'; import { OrderStatisticsParams } from '../dto/statistics.dto';
import { errorResponse, successResponse } from '../utils/response.util'; import { errorResponse, successResponse } from '../utils/response.util';
@ -76,4 +76,24 @@ export class StatisticsController {
return errorResponse(error?.message || '获取失败'); 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,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: '获取库存列表' }) @ApiOkResponse({ type: StockListRes, description: '获取库存列表' })
@Get('/') @Get('/')
async getStocks(@Query() query: QueryStockDTO) { async getStocks(@Query() query: QueryStockDTO) {

38
src/dto/customer.dto.ts Normal file
View File

@ -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;
}

View File

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

View File

@ -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;
}

View File

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

View File

@ -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'
}`
: ''
}
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('订单状态不正确 '); throw new Error('订单状态不正确 ');
} }
for (const item of data?.sales) { // for (const item of data?.sales) {
const stock = await this.stockModel.findOne({ // const stock = await this.stockModel.findOne({
where: { // where: {
stockPointId: data.stockPointId, // stockPointId: data.stockPointId,
productSku: item.sku, // productSku: item.sku,
}, // },
}); // });
if (!stock || stock.quantity < item.quantity) // if (!stock || stock.quantity < item.quantity)
throw new Error(item.name + '库存不足'); // throw new Error(item.name + '库存不足');
} // }
let shipment: Shipment; let shipment: Shipment;
if (data.service_type === ShipmentType.FREIGHTCOM) { if (data.service_type === ShipmentType.FREIGHTCOM) {

View File

@ -14,6 +14,7 @@ import { OrderRefundItem } from '../entity/order_retund_item.entity';
import { OrderCoupon } from '../entity/order_copon.entity'; import { OrderCoupon } from '../entity/order_copon.entity';
import { OrderShipping } from '../entity/order_shipping.entity'; import { OrderShipping } from '../entity/order_shipping.entity';
import { Shipment } from '../entity/shipment.entity'; import { Shipment } from '../entity/shipment.entity';
import { Customer } from '../entity/customer.entity';
import { import {
ErpOrderStatus, ErpOrderStatus,
OrderStatus, OrderStatus,
@ -85,6 +86,9 @@ export class OrderService {
@Inject() @Inject()
dataSourceManager: TypeORMDataSourceManager; dataSourceManager: TypeORMDataSourceManager;
@InjectEntityModel(Customer)
customerModel: Repository<Customer>;
async syncOrders(siteId: string) { async syncOrders(siteId: string) {
const orders = await this.wPService.getOrders(siteId); // 调用 WooCommerce API 获取订单 const orders = await this.wPService.getOrders(siteId); // 调用 WooCommerce API 获取订单
for (const order of orders) { for (const order of orders) {
@ -159,15 +163,16 @@ export class OrderService {
where: { orderId: existingOrder.id }, where: { orderId: existingOrder.id },
}); });
if (!items) return; if (!items) return;
const stockPointId = ['YT', 'NT', 'BC', 'AB', 'SK'].some( const stockPointId = 2;
v => // ['YT', 'NT', 'BC', 'AB', 'SK'].some(
v.toLowerCase() === // v =>
( // v.toLowerCase() ===
existingOrder?.shipping?.state || existingOrder?.billing?.state // (
).toLowerCase() // existingOrder?.shipping?.state || existingOrder?.billing?.state
) // ).toLowerCase()
? 3 // )
: 2; // ? 3
// : 2;
for (const item of items) { for (const item of items) {
const updateStock = new UpdateStockDTO(); const updateStock = new UpdateStockDTO();
updateStock.stockPointId = stockPointId; updateStock.stockPointId = stockPointId;
@ -211,6 +216,14 @@ export class OrderService {
return entity; return entity;
} }
entity.orderStatus = this.mapOrderStatus(entity.status); 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); return await this.orderModel.save(entity);
} }
@ -721,29 +734,28 @@ export class OrderService {
return await query.getRawMany(); return await query.getRawMany();
} }
async getOrderSales({ async getOrderSales({ siteId, startDate, endDate, current, pageSize, name }: QueryOrderSalesDTO) {
siteId,
startDate,
endDate,
current,
pageSize,
name,
}: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : []; const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
// 分页查询
const parameters: any[] = [startDate, endDate];
// 主查询:带分页
let sqlQuery = ` let sqlQuery = `
WITH product_purchase_counts AS ( WITH product_purchase_counts AS (
SELECT o.customer_email,os.productId, os.name, COUNT(DISTINCT o.id,os.productId) AS order_count SELECT
o.customer_email,
os.productId,
COUNT(DISTINCT o.id) AS order_count
FROM \`order\` o FROM \`order\` o
JOIN order_sale os ON o.id = os.orderId JOIN order_sale os ON o.id = os.orderId
WHERE o.status IN ('completed', 'processing') WHERE o.status IN ('completed', 'processing')
GROUP BY o.customer_email, os.productId, os.name GROUP BY o.customer_email, os.productId
) )
SELECT SELECT
os.productId AS productId, os.productId AS productId,
os.name AS name, os.name AS name,
SUM(os.quantity) AS totalQuantity, SUM(os.quantity) AS totalQuantity,
COUNT(distinct os.orderId) AS totalOrders, COUNT(DISTINCT os.orderId) AS totalOrders,
c.name AS categoryName, c.name AS categoryName,
COUNT(DISTINCT CASE WHEN pc.order_count = 1 THEN o.id END) AS firstOrderCount, 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, SUM(CASE WHEN pc.order_count = 1 THEN os.quantity ELSE 0 END) AS firstOrderYOONEBoxCount,
@ -759,135 +771,255 @@ export class OrderService {
INNER JOIN category c ON p.categoryId = c.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 INNER JOIN product_purchase_counts pc ON pc.customer_email = o.customer_email AND pc.productId = os.productId
WHERE o.date_paid BETWEEN ? AND ? WHERE o.date_paid BETWEEN ? AND ?
AND o.status IN ('processing', 'completed') AND o.status IN ('completed', 'processing')
`; `;
const parameters: any[] = [startDate, endDate];
if (siteId) { if (siteId) {
sqlQuery += ' AND os.siteId = ?'; sqlQuery += ' AND os.siteId = ?';
parameters.push(siteId); parameters.push(siteId);
} }
if (nameKeywords.length > 0) { if (nameKeywords.length > 0) {
sqlQuery += sqlQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')';
' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND ');
parameters.push(...nameKeywords.map(word => `%${word}%`)); parameters.push(...nameKeywords.map(word => `%${word}%`));
} }
sqlQuery += ` sqlQuery += `
GROUP BY os.productId, os.name, c.name GROUP BY os.productId, os.name, c.name
ORDER BY totalQuantity DESC 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 items = await this.orderSaleModel.query(sqlQuery, parameters);
// 总条数
const countParams: any[] = [startDate, endDate];
let totalCountQuery = ` let totalCountQuery = `
SELECT COUNT(DISTINCT os.productId) AS totalCount SELECT COUNT(DISTINCT os.productId) AS totalCount
FROM order_sale os FROM order_sale os
INNER JOIN \`order\` o ON o.id = os.orderId INNER JOIN \`order\` o ON o.id = os.orderId
INNER JOIN product p ON os.productId = p.id WHERE o.date_paid BETWEEN ? AND ?
INNER JOIN category c ON p.categoryId = c.id AND o.status IN ('completed', 'processing')
WHERE o.date_created BETWEEN ? AND ?
AND o.status IN ('processing', 'completed')
`; `;
const totalCountParameters: any[] = [startDate, endDate];
if (siteId) { if (siteId) {
totalCountQuery += ' AND os.siteId = ?'; totalCountQuery += ' AND os.siteId = ?';
totalCountParameters.push(siteId); countParams.push(siteId);
} }
if (nameKeywords.length > 0) { if (nameKeywords.length > 0) {
totalCountQuery += totalCountQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')';
' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND '); countParams.push(...nameKeywords.map(word => `%${word}%`));
totalCountParameters.push(...nameKeywords.map(word => `%${word}%`));
} }
const totalCountResult = await this.orderSaleModel.query( const totalCountResult = await this.orderSaleModel.query(totalCountQuery, countParams);
totalCountQuery,
totalCountParameters
);
// 一次查询获取所有 yoone box 数量
const totalQuantityParams: any[] = [startDate, endDate];
let totalQuantityQuery = ` 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 FROM order_sale os
INNER JOIN \`order\` o ON o.id = os.orderId INNER JOIN \`order\` o ON o.id = os.orderId
INNER JOIN product p ON os.productId = p.id WHERE o.date_paid BETWEEN ? AND ?
INNER JOIN category c ON p.categoryId = c.id AND o.status IN ('completed', 'processing')
WHERE o.date_created BETWEEN ? AND ?
AND o.status IN ('processing', 'completed')
`; `;
const totalQuantityParameters: any[] = [startDate, endDate];
if (siteId) { if (siteId) {
totalQuantityQuery += ' AND os.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) { if (nameKeywords.length > 0) {
totalQuantityQuery += totalQuantityQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')';
' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND '); totalQuantityParams.push(...nameKeywords.map(word => `%${word}%`));
totalQuantityParameters.push(...nameKeywords.map(word => `%${word}%`));
} }
const totalQuantityResult = await this.orderSaleModel.query( const [totalQuantityResult] = await this.orderSaleModel.query(totalQuantityQuery, totalQuantityParams);
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 { return {
items, items,
total: totalCountResult[0]?.totalCount, total: totalCountResult[0]?.totalCount || 0,
totalQuantity: Number( totalQuantity: Number(totalQuantityResult.totalQuantity || 0),
totalQuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0) yoone3Quantity: Number(totalQuantityResult.yoone3Quantity || 0),
), yoone6Quantity: Number(totalQuantityResult.yoone6Quantity || 0),
yoone3Quantity: Number( yoone9Quantity: Number(totalQuantityResult.yoone9Quantity || 0),
yoone3QuantityResult.reduce((sum, row) => sum + row.totalQuantity, 0) yoone12Quantity: Number(totalQuantityResult.yoone12Quantity || 0),
), yoone15Quantity: Number(totalQuantityResult.yoone15Quantity || 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, current,
pageSize, 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({ async getOrderItems({
siteId, siteId,
startDate, 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,
};
}
} }

View File

@ -1,5 +1,5 @@
import { Provide } from '@midwayjs/core'; 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 { Product } from '../entity/product.entty';
import { Category } from '../entity/category.entity'; import { Category } from '../entity/category.entity';
import { paginate } from '../utils/paginate.util'; import { paginate } from '../utils/paginate.util';
@ -46,19 +46,58 @@ export class ProductService {
@InjectEntityModel(Variation) @InjectEntityModel(Variation)
variationModel: Repository<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[]> { async findProductsByName(name: string): Promise<Product[]> {
const where: any = {};
const nameFilter = name ? name.split(' ').filter(Boolean) : []; 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) { if (nameFilter.length > 0) {
const nameConditions = nameFilter.map(word => Like(`%${word}%`)); const nameConds = nameFilter.map((word, index) => {
where.name = And(...nameConditions); const key = `name${index}`;
} params[key] = `%${word}%`;
where.sku = Not(IsNull()); return `product.name LIKE :${key}`;
// 查询 SKU 不为空且 name 包含关键字的产品,最多返回 50 条
return this.productModel.find({
where,
take: 50,
}); });
conditions.push(`(${nameConds.join(' AND ')})`);
}
// 中文名模糊匹配
if (name) {
params['nameCn'] = `%${name}%`;
conditions.push(`product.nameCn LIKE :nameCn`);
}
// 英文名关键词匹配 OR 中文名匹配
query.andWhere(`(${conditions.join(' OR ')})`, params);
}
query.take(50);
return await query.getMany();
} }
async findProductBySku(sku: string): Promise<Product> { async findProductBySku(sku: string): Promise<Product> {
@ -84,6 +123,7 @@ export class ProductService {
.select([ .select([
'product.id as id', 'product.id as id',
'product.name as name', 'product.name as name',
'product.nameCn as nameCn',
'product.description as description', 'product.description as description',
'product.humidity as humidity', 'product.humidity as humidity',
'product.sku as sku', 'product.sku as sku',
@ -169,6 +209,18 @@ export class ProductService {
return await this.productModel.findOneBy({ id }); 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> { async deleteProduct(id: number): Promise<boolean> {
// 检查产品是否存在 // 检查产品是否存在
const product = await this.productModel.findOneBy({ id }); const product = await this.productModel.findOneBy({ id });

View File

@ -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 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 = '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 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 FROM daily_orders
GROUP BY order_date 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.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 = '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.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.total_orders,
dt.togo_total_orders, dt.togo_total_orders,
dt.can_total_orders, dt.can_total_orders,
@ -141,7 +143,8 @@ export class StatisticsService {
dt.non_yoone_total, dt.non_yoone_total,
dt.zex_total, dt.zex_total,
dt.non_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, 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 = '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, 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.non_yoone_total,
dt.zex_total, dt.zex_total,
dt.non_zex_total, dt.non_zex_total,
dt.direct_first_total, dt.direct_total,
dt.organic_total,
dt.total_orders, dt.total_orders,
dt.togo_total_orders, dt.togo_total_orders,
dt.can_total_orders dt.can_total_orders
@ -315,6 +319,11 @@ export class StatisticsService {
FROM \`order\` FROM \`order\`
GROUP BY customer_email 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 ( customer_stats AS (
SELECT SELECT
customer_email, customer_email,
@ -330,14 +339,21 @@ export class StatisticsService {
JSON_OBJECT('name', oi.name, 'quantity', oi.quantity) JSON_OBJECT('name', oi.name, 'quantity', oi.quantity)
) AS orderItems, ) AS orderItems,
f.first_purchase_date, f.first_purchase_date,
l.last_purchase_date,
CASE CASE
WHEN o.date_paid = f.first_purchase_date THEN 'first_purchase' WHEN o.date_paid = f.first_purchase_date THEN 'first_purchase'
ELSE 'repeat_purchase' ELSE 'repeat_purchase'
END AS purchase_type, END AS purchase_type,
cs.order_count, 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 FROM \`order\` o
LEFT JOIN first_order f ON o.customer_email = f.customer_email 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 order_item oi ON oi.orderId = o.id
LEFT JOIN customer_stats cs ON o.customer_email = cs.customer_email LEFT JOIN customer_stats cs ON o.customer_email = cs.customer_email
WHERE o.date_paid BETWEEN ? AND ? WHERE o.date_paid BETWEEN ? AND ?
@ -357,7 +373,12 @@ export class StatisticsService {
o.*, o.*,
JSON_ARRAYAGG( JSON_ARRAYAGG(
JSON_OBJECT('name', oi.name, 'quantity', oi.quantity, 'total', oi.total) 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 FROM \`order\` o
LEFT JOIN order_item oi ON oi.orderId = o.id LEFT JOIN order_item oi ON oi.orderId = o.id
WHERE o.customer_email='${email}' WHERE o.customer_email='${email}'
@ -901,4 +922,206 @@ export class StatisticsService {
pageSize, 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,6 +218,16 @@ export class StockService {
await this.purchaseOrderModel.save(purchaseOrder); 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) { async getStocks(query: QueryStockDTO) {
const { current = 1, pageSize = 10, productName } = query; const { current = 1, pageSize = 10, productName } = query;
@ -231,13 +241,15 @@ export class StockService {
// 'stock.id as id', // 'stock.id as id',
'stock.productSku as productSku', 'stock.productSku as productSku',
'product.name as productName', 'product.name as productName',
'product.nameCn as productNameCn',
'JSON_ARRAYAGG(JSON_OBJECT("id", stock.stockPointId, "quantity", stock.quantity)) as stockPoint', 'JSON_ARRAYAGG(JSON_OBJECT("id", stock.stockPointId, "quantity", stock.quantity)) as stockPoint',
'MIN(stock.updatedAt) as updatedAt', 'MIN(stock.updatedAt) as updatedAt',
'MAX(stock.createdAt) as createdAt', 'MAX(stock.createdAt) as createdAt',
]) ])
.leftJoin(Product, 'product', 'product.sku = stock.productSku') .leftJoin(Product, 'product', 'product.sku = stock.productSku')
.groupBy('stock.productSku') .groupBy('stock.productSku')
.addGroupBy('product.name'); .addGroupBy('product.name')
.addGroupBy('product.nameCn');
let totalQueryBuilder = this.stockModel let totalQueryBuilder = this.stockModel
.createQueryBuilder('stock') .createQueryBuilder('stock')
.select('COUNT(DISTINCT stock.productSku)', 'count') .select('COUNT(DISTINCT stock.productSku)', 'count')
@ -312,9 +324,9 @@ export class StockService {
// 更新库存 // 更新库存
stock.quantity += stock.quantity +=
operationType === 'in' ? quantityChange : -quantityChange; operationType === 'in' ? quantityChange : -quantityChange;
if (stock.quantity < 0) { // if (stock.quantity < 0) {
throw new Error('库存不足,无法完成操作'); // throw new Error('库存不足,无法完成操作');
} // }
await this.stockModel.save(stock); await this.stockModel.save(stock);
} }
@ -375,14 +387,14 @@ export class StockService {
async createTransfer(data: Record<string, any>, userId: number) { async createTransfer(data: Record<string, any>, userId: number) {
const { sourceStockPointId, destStockPointId, sendAt, items, note } = data; const { sourceStockPointId, destStockPointId, sendAt, items, note } = data;
for (const item of items) { // for (const item of items) {
const stock = await this.stockModel.findOneBy({ // const stock = await this.stockModel.findOneBy({
stockPointId: sourceStockPointId, // stockPointId: sourceStockPointId,
productSku: item.productSku, // productSku: item.productSku,
}); // });
if (!stock || stock.quantity < item.quantity) // if (!stock || stock.quantity < item.quantity)
throw new Error(`${item.productName} 库存不足`); // throw new Error(`${item.productName} 库存不足`);
} // }
const now = dayjs().format('YYYY-MM-DD'); const now = dayjs().format('YYYY-MM-DD');
const count = await this.transferModel.count({ const count = await this.transferModel.count({
where: { where: {