forked from yoone/API
1
0
Fork 0

Compare commits

..

10 Commits

Author SHA1 Message Date
黄珑 8a1b929692 撒点记录登陆ip等信息 2025-09-02 11:57:42 +08:00
cll d23acbee1c fix: 接口参数调整 2025-09-01 14:40:21 +08:00
cll 96eb7768f3 fix: track查询更换后产品 2025-09-01 11:53:44 +08:00
cll c55eefa3de fix: 查询10天bug 2025-08-30 09:46:33 +08:00
黄珑 6c65f5be85 Fix:尝试修复筛选最近10天数据的bug 2025-08-29 17:59:10 +08:00
黄珑 b0e2c42ad0 为添加permission增加了只查询10天订单的逻辑 2025-08-28 18:40:06 +08:00
黄珑 015113d3b3 订单页增加判断,非管理员只显示最近10天数据 2025-08-25 18:17:49 +08:00
黄珑 39afbae7cf Fix: 获取运费增加正确提示 2025-08-25 01:41:33 +00:00
cll b2cddad10a fix: 销售统计总数 2025-08-23 16:44:18 +08:00
cll e4e4ceb7c1 fix:优化销售统计接口 2025-08-23 11:48:45 +08:00
9 changed files with 326 additions and 358 deletions

View File

@ -22,7 +22,7 @@ import { errorResponse, successResponse } from '../utils/response.util';
import { LogisticsService } from '../service/logistics.service'; import { LogisticsService } from '../service/logistics.service';
import { ShippingDetailsDTO } from '../dto/freightcom.dto'; import { ShippingDetailsDTO } from '../dto/freightcom.dto';
import { ShippingAddress } from '../entity/shipping_address.entity'; import { ShippingAddress } from '../entity/shipping_address.entity';
import { QueryServiceDTO, ShipmentBookDTO } from '../dto/logistics.dto'; import { QueryServiceDTO, ShipmentBookDTO, ShipmentFeeBookDTO } from '../dto/logistics.dto';
import { User } from '../decorator/user.decorator'; import { User } from '../decorator/user.decorator';
@Controller('/logistics') @Controller('/logistics')
@ -180,7 +180,7 @@ export class LogisticsController {
) )
@Post('/getShipmentFee') @Post('/getShipmentFee')
async getShipmentFee( async getShipmentFee(
@Body() data: ShipmentBookDTO @Body() data: ShipmentFeeBookDTO
) { ) {
try { try {
const fee = await this.logisticsService.getShipmentFee(data); const fee = await this.logisticsService.getShipmentFee(data);
@ -222,7 +222,7 @@ export class LogisticsController {
} }
@ApiOkResponse() @ApiOkResponse()
@Post('/updateState/:id') @Post('/updateState/:shipmentId')
async updateShipmentState( async updateShipmentState(
@Param('shipmentId') shipmentId: number @Param('shipmentId') shipmentId: number
) { ) {
@ -247,11 +247,11 @@ export class LogisticsController {
} }
@ApiOkResponse() @ApiOkResponse()
@Post('/getTrackingNumber') @Post('/getOrderList')
async getTrackingNumber(@Query('number') number: string) { async getOrderList(@Query('number') number: string) {
try { try {
return successResponse( return successResponse(
await this.logisticsService.getTrackingNumber(number) await this.logisticsService.getOrderList(number)
); );
} catch (error) { } catch (error) {
return errorResponse(error?.message || '获取失败'); return errorResponse(error?.message || '获取失败');
@ -259,11 +259,11 @@ export class LogisticsController {
} }
@ApiOkResponse() @ApiOkResponse()
@Post('/getListByTrackingId') @Post('/getListByOrderId')
async getListByTrackingId(@Query('shipment_id') shipment_id: number) { async getListByOrderId(@Query('id') orderId: number) {
try { try {
return successResponse( return successResponse(
await this.logisticsService.getListByTrackingId(shipment_id) await this.logisticsService.getListByOrderId(orderId)
); );
} catch (error) { } catch (error) {
return errorResponse(error?.message || '获取失败'); return errorResponse(error?.message || '获取失败');

View File

@ -68,11 +68,12 @@ export class OrderController {
@Get('/getOrders') @Get('/getOrders')
async getOrders( async getOrders(
@Query() @Query()
param: QueryOrderDTO param: QueryOrderDTO,
@User() user
) { ) {
try { try {
const count = await this.orderService.getOrderStatus(param); const count = await this.orderService.getOrderStatus(param);
const data = await this.orderService.getOrders(param); const data = await this.orderService.getOrders(param, user.id);
return successResponse({ return successResponse({
...data, ...data,
count, count,

View File

@ -12,11 +12,15 @@ export class UserController {
@Inject() @Inject()
userService: UserService; userService: UserService;
@Inject()
ctx;
@ApiOkResponse({ @ApiOkResponse({
type: LoginRes, type: LoginRes,
}) })
@Post('/login') @Post('/login')
async login(@Body() body) { async login(@Body() body) {
this.ctx.logger.info('ip:', this.ctx.ip, '; path:', this.ctx.path, '; user:', body?.username);
try { try {
const result = await this.userService.login(body); const result = await this.userService.login(body);
return successResponse(result, '登录成功'); return successResponse(result, '登录成功');

View File

@ -21,6 +21,50 @@ export class ShipmentBookDTO {
orderIds?: number[]; orderIds?: number[];
} }
export class ShipmentFeeBookDTO {
@ApiProperty()
stockPointId: number;
@ApiProperty()
sender: string;
@ApiProperty()
startPhone: string;
@ApiProperty()
startPostalCode: string;
@ApiProperty()
pickupAddress: string;
// pickupWarehouse: number; // 此处用 stockPointId 到后端解析
@ApiProperty()
shipperCountryCode: string;
@ApiProperty()
receiver: string;
@ApiProperty()
city: string;
@ApiProperty()
province: string;
@ApiProperty()
country: string;
@ApiProperty()
postalCode: string;
@ApiProperty()
deliveryAddress: string;
@ApiProperty()
receiverPhone: string;
@ApiProperty()
receiverEmail: string;
@ApiProperty()
length: number;
@ApiProperty()
width: number;
@ApiProperty()
height: number;
@ApiProperty()
dimensionUom: string;
@ApiProperty()
weight: number;
@ApiProperty()
weightUom: string;
}
export class PaymentMethodDTO { export class PaymentMethodDTO {
@ApiProperty() @ApiProperty()
id: string; id: string;

View File

@ -1,6 +1,8 @@
import { ApiProperty } from '@midwayjs/swagger'; import { ApiProperty } from '@midwayjs/swagger';
import { Exclude, Expose } from 'class-transformer'; import { Exclude, Expose } from 'class-transformer';
import { import {
BeforeInsert,
BeforeUpdate,
Column, Column,
CreateDateColumn, CreateDateColumn,
Entity, Entity,
@ -56,6 +58,26 @@ export class OrderSale {
@Expose() @Expose()
isPackage: boolean; isPackage: boolean;
@ApiProperty()
@Column({ default: false })
@Expose()
isYoone: boolean;
@ApiProperty()
@Column({ default: false })
@Expose()
isZex: boolean;
@ApiProperty({ nullable: true })
@Column({ type: 'int', nullable: true })
@Expose()
size: number | null;
@ApiProperty()
@Column({ default: false })
@Expose()
isYooneNew: boolean;
@ApiProperty({ @ApiProperty({
example: '2022-12-12 11:11:11', example: '2022-12-12 11:11:11',
description: '创建时间', description: '创建时间',
@ -71,4 +93,25 @@ export class OrderSale {
@UpdateDateColumn() @UpdateDateColumn()
@Expose() @Expose()
updatedAt?: Date; updatedAt?: Date;
// === 自动计算逻辑 ===
@BeforeInsert()
@BeforeUpdate()
setFlags() {
if (!this.name) return;
const lower = this.name.toLowerCase();
this.isYoone = lower.includes('yoone');
this.isZex = lower.includes('zex');
this.isYooneNew = this.isYoone && lower.includes('new');
let size: number | null = null;
const sizes = [3, 6, 9, 12, 15, 18];
for (const s of sizes) {
if (lower.includes(s.toString())) {
size = s;
break;
}
}
this.size = size;
}
} }

View File

@ -8,7 +8,7 @@ import { Order } from '../entity/order.entity';
import { Shipment } from '../entity/shipment.entity'; import { Shipment } from '../entity/shipment.entity';
import { ShipmentItem } from '../entity/shipment_item.entity'; import { ShipmentItem } from '../entity/shipment_item.entity';
import { OrderShipment } from '../entity/order_shipment.entity'; import { OrderShipment } from '../entity/order_shipment.entity';
import { QueryServiceDTO, ShipmentBookDTO } from '../dto/logistics.dto'; import { QueryServiceDTO, ShipmentBookDTO, ShipmentFeeBookDTO } from '../dto/logistics.dto';
import { import {
ErpOrderStatus, ErpOrderStatus,
OrderStatus, OrderStatus,
@ -30,6 +30,7 @@ import { OrderSale } from '../entity/order_sale.entity';
import { UniExpressService } from './uni_express.service'; import { UniExpressService } from './uni_express.service';
import { StockPoint } from '../entity/stock_point.entity'; import { StockPoint } from '../entity/stock_point.entity';
import { OrderService } from './order.service'; import { OrderService } from './order.service';
import { convertKeysFromCamelToSnake } from '../utils/object-transform.util';
@Provide() @Provide()
export class LogisticsService { export class LogisticsService {
@ -135,12 +136,13 @@ export class LogisticsService {
this.shipmentModel.save(shipment); this.shipmentModel.save(shipment);
return shipment.state; return shipment.state;
} catch (error) { } catch (error) {
throw new Error(`更新运单状态失败 ${error.message}`); throw error;
// throw new Error(`更新运单状态失败 ${error.message}`);
} }
} }
async updateShipmentStateById(id: number) { async updateShipmentStateById(id: number) {
const shipment:Shipment = await this.shipmentModel.findOneBy({ id : id }); const shipment: Shipment = await this.shipmentModel.findOneBy({ id: id });
return this.updateShipmentState(shipment); return this.updateShipmentState(shipment);
} }
@ -217,7 +219,7 @@ export class LogisticsService {
async getShipmentLabel(shipmentId) { async getShipmentLabel(shipmentId) {
try { try {
const shipment:Shipment = await this.shipmentModel.findOneBy({id: shipmentId}); const shipment: Shipment = await this.shipmentModel.findOneBy({ id: shipmentId });
if (!shipment) { if (!shipment) {
throw new Error('运单不存在'); throw new Error('运单不存在');
} }
@ -229,11 +231,11 @@ export class LogisticsService {
async removeShipment(shipmentId: number) { async removeShipment(shipmentId: number) {
try { try {
const shipment:Shipment = await this.shipmentModel.findOneBy({id: shipmentId}); const shipment: Shipment = await this.shipmentModel.findOneBy({ id: shipmentId });
if (shipment.state !== '190') { // todo写常数 if (shipment.state !== '190') { // todo写常数
throw new Error('订单当前状态无法删除'); throw new Error('订单当前状态无法删除');
} }
const order:Order = await this.orderModel.findOneBy({id: shipment.order_id}); const order: Order = await this.orderModel.findOneBy({ id: shipment.order_id });
const dataSource = this.dataSourceManager.getDataSource('default'); const dataSource = this.dataSourceManager.getDataSource('default');
let transactionError = undefined; let transactionError = undefined;
await dataSource.transaction(async manager => { await dataSource.transaction(async manager => {
@ -283,34 +285,19 @@ export class LogisticsService {
} }
} }
async getShipmentFee(data: ShipmentBookDTO) { async getShipmentFee(data: ShipmentFeeBookDTO) {
try { try {
const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId}); const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId });
const reqBody = { const reqBody = {
sender: data.details.origin.contact_name, ...convertKeysFromCamelToSnake(data),
start_phone: data.details.origin.phone_number,
start_postal_code: data.details.origin.address.postal_code.replace(/\s/g, ''),
pickup_address: data.details.origin.address.address_line_1,
pickup_warehouse: stock_point.upStreamStockPointId, pickup_warehouse: stock_point.upStreamStockPointId,
shipper_country_code: data.details.origin.address.country,
receiver: data.details.destination.contact_name,
city: data.details.destination.address.city,
province: data.details.destination.address.region,
country: data.details.destination.address.country,
postal_code: data.details.destination.address.postal_code.replace(/\s/g, ''),
delivery_address: data.details.destination.address.address_line_1,
receiver_phone: data.details.destination.phone_number.number,
receiver_email: data.details.destination.email_addresses,
// item_description: data.sales, // todo: 货品信息
length: data.details.packaging_properties.packages[0].measurements.cuboid.l,
width: data.details.packaging_properties.packages[0].measurements.cuboid.w,
height: data.details.packaging_properties.packages[0].measurements.cuboid.h,
dimension_uom: data.details.packaging_properties.packages[0].measurements.cuboid.unit,
weight: data.details.packaging_properties.packages[0].measurements.weight.value,
weight_uom: data.details.packaging_properties.packages[0].measurements.weight.unit,
currency: 'CAD', currency: 'CAD',
// item_description: data.sales, // todo: 货品信息
} }
const resShipmentFee = await this.uniExpressService.getRates(reqBody); const resShipmentFee = await this.uniExpressService.getRates(reqBody);
if (resShipmentFee.status !== 'SUCCESS') {
throw new Error(resShipmentFee.ret_msg);
}
return resShipmentFee.data.totalAfterTax * 100; return resShipmentFee.data.totalAfterTax * 100;
} catch (e) { } catch (e) {
throw e; throw e;
@ -332,7 +319,7 @@ export class LogisticsService {
let resShipmentOrder; let resShipmentOrder;
try { try {
const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId}); const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId });
const reqBody = { const reqBody = {
sender: data.details.origin.contact_name, sender: data.details.origin.contact_name,
start_phone: data.details.origin.phone_number, start_phone: data.details.origin.phone_number,
@ -422,10 +409,12 @@ export class LogisticsService {
// 更新产品发货信息 // 更新产品发货信息
this.orderService.updateOrderSales(order.id, sales); this.orderService.updateOrderSales(order.id, sales);
return { data: { return {
shipmentId data: {
} }; shipmentId
} catch(error) { }
};
} catch (error) {
if (resShipmentOrder.status === 'SUCCESS') { if (resShipmentOrder.status === 'SUCCESS') {
await this.uniExpressService.deleteShipment(resShipmentOrder.data.tno); await this.uniExpressService.deleteShipment(resShipmentOrder.data.tno);
} }
@ -482,7 +471,7 @@ export class LogisticsService {
} }
async getShipment(id: number) { async getShipment(id: number) {
const orderShipments:OrderShipment[] = await this.orderShipmentModel.find({ const orderShipments: OrderShipment[] = await this.orderShipmentModel.find({
where: { shipment_id: id }, where: { shipment_id: id },
}); });
if (!orderShipments || orderShipments.length === 0) return; if (!orderShipments || orderShipments.length === 0) return;
@ -566,7 +555,7 @@ export class LogisticsService {
}); });
} }
async getTrackingNumber(number: string) { async getOrderList(number: string) {
const orders = await this.orderModel.find({ const orders = await this.orderModel.find({
where: { where: {
externalOrderId: Like(`%${number}%`), externalOrderId: Like(`%${number}%`),
@ -581,56 +570,14 @@ export class LogisticsService {
})); }));
} }
async getListByTrackingId(id: number) { async getListByOrderId(id: number) {
const qb = ` const item = await this.orderItem.find({ where: { orderId: id } });
SELECT const saleItem = await this.orderSaleModel.find({ where: { orderId: id } });
oi.name,
oi.quantity,
CASE
WHEN oi.externalVariationId != 0 THEN v.constitution
ELSE p.constitution
END AS constitution
FROM order_item oi
LEFT JOIN wp_product p ON oi.siteId=p.siteId AND oi.externalProductId=p.externalProductId
LEFT JOIN variation v ON oi.siteId=v.siteId AND oi.externalVariationId=v.externalVariationId
WHERE oi.orderId=?
`;
const saleItem = await this.orderSaleModel.query(qb, [id]);
const allSkus = new Set<string>();
for (const item of saleItem) {
if (!item.constitution) continue;
try {
item.constitution.forEach(c => allSkus.add(c.sku));
} catch (e) {
console.warn('Invalid constitution JSON:', item.constitution);
}
}
console.log(allSkus);
const skuList = Array.from(allSkus);
let skuNameMap = new Map<string, string>();
if (skuList.length > 0) { return {
const placeholders = skuList.map(() => '?').join(', '); item,
const productRows = await this.orderSaleModel.query( saleItem
`SELECT sku, name FROM product WHERE sku IN (${placeholders})`, };
skuList
);
skuNameMap = new Map(productRows.map(p => [p.sku, p.name]));
}
for (const item of saleItem) {
if (!item.constitution) continue;
try {
item.constitution = item.constitution.map(c => ({
...c,
name: skuNameMap.get(c.sku) || null,
}));
} catch (e) {
item.constitution = [];
}
}
return saleItem;
} }
async getList(param: Record<string, any>) { async getList(param: Record<string, any>) {

View File

@ -45,6 +45,9 @@ export class OrderService {
@InjectEntityModel(Order) @InjectEntityModel(Order)
orderModel: Repository<Order>; orderModel: Repository<Order>;
@InjectEntityModel(User)
userModel: Repository<User>;
@InjectEntityModel(OrderItem) @InjectEntityModel(OrderItem)
orderItemModel: Repository<OrderItem>; orderItemModel: Repository<OrderItem>;
@ -546,7 +549,7 @@ export class OrderService {
current, current,
pageSize, pageSize,
customer_email, customer_email,
}) { }, userId = undefined) {
const parameters: any[] = []; const parameters: any[] = [];
// 基础查询 // 基础查询
@ -629,6 +632,14 @@ export class OrderService {
totalQuery += ` AND o.date_created <= ?`; totalQuery += ` AND o.date_created <= ?`;
parameters.push(endDate); parameters.push(endDate);
} }
const user = await this.userModel.findOneBy({id: userId});
if (user?.permissions?.includes('order-10-days')) {
sqlQuery += ` AND o.date_created >= ?`;
totalQuery += ` AND o.date_created >= ?`;
const tenDaysAgo = new Date();
tenDaysAgo.setDate(tenDaysAgo.getDate() - 10);
parameters.push(tenDaysAgo.toISOString());
}
// 处理 status 参数 // 处理 status 参数
if (status) { if (status) {
@ -740,296 +751,177 @@ export class OrderService {
async getOrderSales({ siteId, startDate, endDate, current, pageSize, name }: QueryOrderSalesDTO) { async getOrderSales({ siteId, startDate, endDate, current, pageSize, name }: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : []; const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
const offset = (current - 1) * pageSize;
const parameters: any[] = [startDate, endDate]; // -------------------------
// 1. 查询总条数
// 主查询:带分页 // -------------------------
let sqlQuery = `
WITH product_purchase_counts AS (
SELECT
o.customer_email,
os.productId,
COUNT(DISTINCT o.id) AS order_count
FROM \`order\` o
JOIN order_sale os ON o.id = os.orderId
WHERE o.status IN ('completed', 'processing')
GROUP BY o.customer_email, os.productId
)
SELECT
os.productId AS productId,
os.name AS name,
SUM(os.quantity) AS totalQuantity,
COUNT(DISTINCT os.orderId) AS totalOrders,
c.name AS categoryName,
COUNT(DISTINCT CASE WHEN pc.order_count = 1 THEN o.id END) AS firstOrderCount,
SUM(CASE WHEN pc.order_count = 1 THEN os.quantity ELSE 0 END) AS firstOrderYOONEBoxCount,
COUNT(DISTINCT CASE WHEN pc.order_count = 2 THEN o.id END) AS secondOrderCount,
SUM(CASE WHEN pc.order_count = 2 THEN os.quantity ELSE 0 END) AS secondOrderYOONEBoxCount,
COUNT(DISTINCT CASE WHEN pc.order_count = 3 THEN o.id END) AS thirdOrderCount,
SUM(CASE WHEN pc.order_count = 3 THEN os.quantity ELSE 0 END) AS thirdOrderYOONEBoxCount,
COUNT(DISTINCT CASE WHEN pc.order_count > 3 THEN o.id END) AS moreThirdOrderCount,
SUM(CASE WHEN pc.order_count > 3 THEN os.quantity ELSE 0 END) AS moreThirdOrderYOONEBoxCount
FROM order_sale os
INNER JOIN \`order\` o ON o.id = os.orderId
INNER JOIN product p ON os.productId = p.id
INNER JOIN category c ON p.categoryId = c.id
INNER JOIN product_purchase_counts pc ON pc.customer_email = o.customer_email AND pc.productId = os.productId
WHERE o.date_paid BETWEEN ? AND ?
AND o.status IN ('completed', 'processing')
`;
if (siteId) {
sqlQuery += ' AND os.siteId = ?';
parameters.push(siteId);
}
if (nameKeywords.length > 0) {
sqlQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')';
parameters.push(...nameKeywords.map(word => `%${word}%`));
}
sqlQuery += `
GROUP BY os.productId, os.name, c.name
ORDER BY totalQuantity DESC
LIMIT ? OFFSET ?
`;
parameters.push(pageSize, (current - 1) * pageSize);
const items = await this.orderSaleModel.query(sqlQuery, parameters);
// 总条数
const countParams: any[] = [startDate, endDate]; const countParams: any[] = [startDate, endDate];
let totalCountQuery = ` let countSql = `
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
WHERE o.date_paid BETWEEN ? AND ? WHERE o.date_paid BETWEEN ? AND ?
AND o.status IN ('completed', 'processing') AND o.status IN ('completed','processing')
`; `;
if (siteId) { if (siteId) {
totalCountQuery += ' AND os.siteId = ?'; countSql += ' AND os.siteId = ?';
countParams.push(siteId); countParams.push(siteId);
} }
if (nameKeywords.length > 0) { if (nameKeywords.length > 0) {
totalCountQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')'; countSql += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' AND ') + ')';
countParams.push(...nameKeywords.map(word => `%${word}%`)); countParams.push(...nameKeywords.map(w => `%${w}%`));
}
const [countResult] = await this.orderSaleModel.query(countSql, countParams);
const totalCount = Number(countResult?.totalCount || 0);
// -------------------------
// 2. 分页查询 product 基础信息
// -------------------------
const itemParams: any[] = [startDate, endDate];
let nameCondition = '';
if (nameKeywords.length > 0) {
nameCondition = ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' AND ') + ')';
itemParams.push(...nameKeywords.map(w => `%${w}%`));
} }
const totalCountResult = await this.orderSaleModel.query(totalCountQuery, countParams); let itemSql = `
SELECT os.productId, os.name, SUM(os.quantity) AS totalQuantity, COUNT(DISTINCT os.orderId) AS totalOrders
// 一次查询获取所有 yoone box 数量
const totalQuantityParams: any[] = [startDate, endDate];
let totalQuantityQuery = `
SELECT
SUM(os.quantity) AS totalQuantity,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%3%' THEN os.quantity ELSE 0 END) AS yoone3Quantity,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%6%' THEN os.quantity ELSE 0 END) AS yoone6Quantity,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%9%' THEN os.quantity ELSE 0 END) AS yoone9Quantity,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%12%' THEN os.quantity ELSE 0 END) AS yoone12Quantity,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%12%' AND os.name LIKE '%NEW%' THEN os.quantity ELSE 0 END) AS yoone12QuantityNew,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%15%' THEN os.quantity ELSE 0 END) AS yoone15Quantity,
SUM(CASE WHEN os.name LIKE '%yoone%' AND os.name LIKE '%18%' THEN os.quantity ELSE 0 END) AS yoone18Quantity,
SUM(CASE WHEN os.name LIKE '%zex%' THEN os.quantity ELSE 0 END) AS zexQuantity
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
WHERE o.date_paid BETWEEN ? AND ? WHERE o.date_paid BETWEEN ? AND ?
AND o.status IN ('completed', 'processing') AND o.status IN ('completed','processing')
`; `;
if (siteId) { if (siteId) {
totalQuantityQuery += ' AND os.siteId = ?'; itemSql += ' AND os.siteId = ?';
totalQuantityParams.push(siteId); itemParams.push(siteId);
} }
if (nameKeywords.length > 0) { itemSql += nameCondition;
totalQuantityQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')'; itemSql += `
totalQuantityParams.push(...nameKeywords.map(word => `%${word}%`)); GROUP BY os.productId, os.name
ORDER BY totalQuantity DESC
LIMIT ? OFFSET ?
`;
itemParams.push(pageSize, offset);
const items = await this.orderSaleModel.query(itemSql, itemParams);
// -------------------------
// 3. 批量统计当前页 product 历史复购
// -------------------------
if (items.length > 0) {
const productIds = items.map(i => i.productId);
const pcParams: any[] = [...productIds, startDate, endDate];
if (siteId) pcParams.push(siteId);
const pcSql = `
SELECT
os.productId,
SUM(CASE WHEN t.purchaseIndex = 1 THEN os.quantity ELSE 0 END) AS firstOrderYOONEBoxCount,
COUNT(CASE WHEN t.purchaseIndex = 1 THEN 1 END) AS firstOrderCount,
SUM(CASE WHEN t.purchaseIndex = 2 THEN os.quantity ELSE 0 END) AS secondOrderYOONEBoxCount,
COUNT(CASE WHEN t.purchaseIndex = 2 THEN 1 END) AS secondOrderCount,
SUM(CASE WHEN t.purchaseIndex = 3 THEN os.quantity ELSE 0 END) AS thirdOrderYOONEBoxCount,
COUNT(CASE WHEN t.purchaseIndex = 3 THEN 1 END) AS thirdOrderCount,
SUM(CASE WHEN t.purchaseIndex > 3 THEN os.quantity ELSE 0 END) AS moreThirdOrderYOONEBoxCount,
COUNT(CASE WHEN t.purchaseIndex > 3 THEN 1 END) AS moreThirdOrderCount
FROM order_sale os
INNER JOIN (
SELECT o2.id AS orderId,
@idx := IF(@prev_email = o2.customer_email, @idx + 1, 1) AS purchaseIndex,
@prev_email := o2.customer_email
FROM \`order\` o2
CROSS JOIN (SELECT @idx := 0, @prev_email := '') vars
WHERE o2.status IN ('completed','processing')
ORDER BY o2.customer_email, o2.date_paid
) t ON t.orderId = os.orderId
WHERE os.productId IN (${productIds.map(() => '?').join(',')})
AND os.orderId IN (
SELECT id FROM \`order\`
WHERE date_paid BETWEEN ? AND ?
${siteId ? 'AND siteId = ?' : ''}
)
GROUP BY os.productId
`;
const pcResults = await this.orderSaleModel.query(pcSql, pcParams);
const pcMap = new Map<number, any>();
pcResults.forEach(r => pcMap.set(r.productId, r));
items.forEach(i => {
const r = pcMap.get(i.productId) || {};
i.firstOrderYOONEBoxCount = Number(r.firstOrderYOONEBoxCount || 0);
i.firstOrderCount = Number(r.firstOrderCount || 0);
i.secondOrderYOONEBoxCount = Number(r.secondOrderYOONEBoxCount || 0);
i.secondOrderCount = Number(r.secondOrderCount || 0);
i.thirdOrderYOONEBoxCount = Number(r.thirdOrderYOONEBoxCount || 0);
i.thirdOrderCount = Number(r.thirdOrderCount || 0);
i.moreThirdOrderYOONEBoxCount = Number(r.moreThirdOrderYOONEBoxCount || 0);
i.moreThirdOrderCount = Number(r.moreThirdOrderCount || 0);
});
} }
const [totalQuantityResult] = await this.orderSaleModel.query(totalQuantityQuery, totalQuantityParams); // -------------------------
// 4. 总量统计(时间段 + siteId
// -------------------------
const totalParams: any[] = [startDate, endDate];
const yooneParams: any[] = [startDate, endDate];
let totalSql = `
SELECT
SUM(os.quantity) AS totalQuantity
FROM order_sale os
INNER JOIN \`order\` o ON o.id = os.orderId
WHERE o.date_paid BETWEEN ? AND ?
AND o.status IN ('completed','processing')
`;
let yooneSql = `
SELECT
SUM(CASE WHEN os.isYoone = 1 AND os.size = 3 THEN os.quantity ELSE 0 END) AS yoone3Quantity,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 6 THEN os.quantity ELSE 0 END) AS yoone6Quantity,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 9 THEN os.quantity ELSE 0 END) AS yoone9Quantity,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 12 THEN os.quantity ELSE 0 END) AS yoone12Quantity,
SUM(CASE WHEN os.isYooneNew = 1 AND os.size = 12 THEN os.quantity ELSE 0 END) AS yoone12QuantityNew,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 15 THEN os.quantity ELSE 0 END) AS yoone15Quantity,
SUM(CASE WHEN os.isYoone = 1 AND os.size = 18 THEN os.quantity ELSE 0 END) AS yoone18Quantity,
SUM(CASE WHEN os.isZex = 1 THEN os.quantity ELSE 0 END) AS zexQuantity
FROM order_sale os
INNER JOIN \`order\` o ON o.id = os.orderId
WHERE o.date_paid BETWEEN ? AND ?
AND o.status IN ('completed','processing')
`;
if (siteId) {
totalSql += ' AND os.siteId = ?';
totalParams.push(siteId);
yooneSql += ' AND os.siteId = ?';
yooneParams.push(siteId);
}
if (nameKeywords.length > 0) {
totalSql += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' AND ') + ')';
totalParams.push(...nameKeywords.map(w => `%${w}%`));
}
const [totalResult] = await this.orderSaleModel.query(totalSql, totalParams);
const [yooneResult] = await this.orderSaleModel.query(yooneSql, yooneParams);
return { return {
items, items,
total: totalCountResult[0]?.totalCount || 0, total: totalCount, // ✅ 总条数
totalQuantity: Number(totalQuantityResult.totalQuantity || 0), totalQuantity: Number(totalResult.totalQuantity || 0),
yoone3Quantity: Number(totalQuantityResult.yoone3Quantity || 0), yoone3Quantity: Number(yooneResult.yoone3Quantity || 0),
yoone6Quantity: Number(totalQuantityResult.yoone6Quantity || 0), yoone6Quantity: Number(yooneResult.yoone6Quantity || 0),
yoone9Quantity: Number(totalQuantityResult.yoone9Quantity || 0), yoone9Quantity: Number(yooneResult.yoone9Quantity || 0),
yoone12Quantity: Number(totalQuantityResult.yoone12Quantity || 0), yoone12Quantity: Number(yooneResult.yoone12Quantity || 0),
yoone12QuantityNew: Number(totalQuantityResult.yoone12QuantityNew || 0), yoone12QuantityNew: Number(yooneResult.yoone12QuantityNew || 0),
yoone15Quantity: Number(totalQuantityResult.yoone15Quantity || 0), yoone15Quantity: Number(yooneResult.yoone15Quantity || 0),
yoone18Quantity: Number(totalQuantityResult.yoone18Quantity || 0), yoone18Quantity: Number(yooneResult.yoone18Quantity || 0),
zexQuantity: Number(totalQuantityResult.zexQuantity || 0), zexQuantity: Number(yooneResult.zexQuantity || 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,

View File

@ -70,6 +70,7 @@ export class UserService {
id: user.id, id: user.id,
deviceId, deviceId,
username: user.username, username: user.username,
isSuper: user.isSuper,
}); });
return { return {

View File

@ -0,0 +1,36 @@
function camelToSnake(str: string): string {
return str.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
}
function snakeToCamel(str: string): string {
return str.replace(/(_[\w])/g, (match) =>
match[1].toUpperCase()
);
}
export function convertKeysFromSnakeToCamel<T>(obj: T): T {
if (Array.isArray(obj)) {
return obj.map(convertKeysFromSnakeToCamel) as unknown as T;
} else if (obj !== null && typeof obj === 'object') {
return Object.keys(obj).reduce((acc, key) => {
const newKey = snakeToCamel(key);
acc[newKey] = convertKeysFromSnakeToCamel((obj as any)[key]);
return acc;
}, {} as any);
}
return obj;
}
export function convertKeysFromCamelToSnake<T>(obj: T): T {
if (Array.isArray(obj)) {
return obj.map(convertKeysFromCamelToSnake) as unknown as T;
} else if (obj !== null && typeof obj === 'object') {
return Object.keys(obj).reduce((acc, key) => {
const newKey = camelToSnake(key);
acc[newKey] = convertKeysFromCamelToSnake((obj as any)[key]);
return acc;
}, {} as any);
}
return obj;
}