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 { 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,
|
||||||
|
|
|
||||||
|
|
@ -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 || '创建失败');
|
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({
|
@ApiOkResponse({
|
||||||
type: BooleanRes,
|
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 { 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 || '获取失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
@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;
|
||||||
|
|
|
||||||
|
|
@ -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('订单状态不正确 ');
|
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) {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue