feat(订单): 添加订单商品列表和关联订单查询功能

- 新增getOrderItemList接口用于查询订单商品列表
- 新增getRelatedByOrder接口用于查询关联订单
- 在QueryOrderDTO中添加isSubscriptionOnly字段用于筛选订阅订单
- 优化订单查询SQL,添加订阅订单过滤条件
- 为日期参数添加默认值处理
This commit is contained in:
tikkhun 2025-11-19 15:51:10 +08:00
parent eff9efc2c3
commit 8778b8138d
3 changed files with 230 additions and 7 deletions

View File

@ -22,6 +22,7 @@ import {
CreateOrderNoteDTO, CreateOrderNoteDTO,
QueryOrderDTO, QueryOrderDTO,
QueryOrderSalesDTO, QueryOrderSalesDTO,
QueryOrderItemDTO,
} from '../dto/order.dto'; } from '../dto/order.dto';
import { User } from '../decorator/user.decorator'; import { User } from '../decorator/user.decorator';
import { ErpOrderStatus } from '../enums/base.enum'; import { ErpOrderStatus } from '../enums/base.enum';
@ -107,6 +108,16 @@ export class OrderController {
} }
} }
@ApiOkResponse()
@Get('/getOrderItemList')
async getOrderItemList(@Query() param: QueryOrderItemDTO) {
try {
return successResponse(await this.orderService.getOrderItemList(param));
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse({ @ApiOkResponse({
type: OrderDetailRes, type: OrderDetailRes,
}) })
@ -119,6 +130,16 @@ export class OrderController {
} }
} }
@ApiOkResponse()
@Get('/:orderId/related')
async getRelatedByOrder(@Param('orderId') orderId: number) {
try {
return successResponse(await this.orderService.getRelatedByOrder(orderId));
} catch (error) {
return errorResponse(error?.message || '获取失败');
}
}
@ApiOkResponse({ @ApiOkResponse({
type: BooleanRes, type: BooleanRes,
}) })

View File

@ -91,6 +91,10 @@ export class QueryOrderDTO {
@ApiProperty() @ApiProperty()
@Rule(RuleType.string()) @Rule(RuleType.string())
payment_method: string; payment_method: string;
@ApiProperty({ description: '仅订阅订单(父订阅订单或包含订阅商品)' })
@Rule(RuleType.bool().default(false))
isSubscriptionOnly?: boolean;
} }
export class QueryOrderSalesDTO { export class QueryOrderSalesDTO {
@ -119,11 +123,11 @@ export class QueryOrderSalesDTO {
name: string; name: string;
@ApiProperty() @ApiProperty()
@Rule(RuleType.date().required()) @Rule(RuleType.date())
startDate: Date; startDate: Date;
@ApiProperty() @ApiProperty()
@Rule(RuleType.date().required()) @Rule(RuleType.date())
endDate: Date; endDate: Date;
} }
@ -141,3 +145,37 @@ export class CreateOrderNoteDTO {
@Rule(RuleType.string()) @Rule(RuleType.string())
content: string; content: string;
} }
export class QueryOrderItemDTO {
@ApiProperty({ example: '1', description: '页码' })
@Rule(RuleType.number())
current: number;
@ApiProperty({ example: '10', description: '每页大小' })
@Rule(RuleType.number())
pageSize: number;
@ApiProperty()
@Rule(RuleType.string().allow(''))
siteId: string;
@ApiProperty()
@Rule(RuleType.string().allow(''))
name: string; // 商品名称关键字
@ApiProperty()
@Rule(RuleType.string().allow(''))
externalProductId: string;
@ApiProperty()
@Rule(RuleType.string().allow(''))
externalVariationId: string;
@ApiProperty()
@Rule(RuleType.date())
startDate: Date;
@ApiProperty()
@Rule(RuleType.date())
endDate: Date;
}

View File

@ -23,6 +23,7 @@ import {
} from '../enums/base.enum'; } from '../enums/base.enum';
import { Variation } from '../entity/variation.entity'; import { Variation } from '../entity/variation.entity';
import { CreateOrderNoteDTO, QueryOrderSalesDTO } from '../dto/order.dto'; import { CreateOrderNoteDTO, QueryOrderSalesDTO } from '../dto/order.dto';
import dayjs = require('dayjs');
import { OrderDetailRes } from '../dto/reponse.dto'; import { OrderDetailRes } from '../dto/reponse.dto';
import { OrderNote } from '../entity/order_note.entity'; import { OrderNote } from '../entity/order_note.entity';
import { User } from '../entity/user.entity'; import { User } from '../entity/user.entity';
@ -310,6 +311,7 @@ export class OrderService {
externalOrderId: string; externalOrderId: string;
orderItems: Record<string, any>[]; orderItems: Record<string, any>[];
}) { }) {
console.log('saveOrderItems params',params)
const { siteId, orderId, externalOrderId, orderItems } = params; const { siteId, orderId, externalOrderId, orderItems } = params;
const currentOrderItems = await this.orderItemModel.find({ const currentOrderItems = await this.orderItemModel.find({
where: { siteId, externalOrderId: externalOrderId }, where: { siteId, externalOrderId: externalOrderId },
@ -614,6 +616,7 @@ export class OrderService {
customer_email, customer_email,
payment_method, payment_method,
billing_phone, billing_phone,
isSubscriptionOnly = false,
}, userId = undefined) { }, userId = undefined) {
const parameters: any[] = []; const parameters: any[] = [];
@ -699,12 +702,14 @@ export class OrderService {
totalQuery += ` AND o.date_created <= ?`; totalQuery += ` AND o.date_created <= ?`;
parameters.push(endDate); parameters.push(endDate);
} }
// 支付方式筛选使用参数化避免SQL注入
if (payment_method) { if (payment_method) {
sqlQuery += ` AND o.payment_method like "%${payment_method}%" `; sqlQuery += ` AND o.payment_method LIKE ?`;
totalQuery += ` AND o.payment_method like "%${payment_method}%" `; totalQuery += ` AND o.payment_method LIKE ?`;
parameters.push(`%${payment_method}%`);
} }
const user = await this.userModel.findOneBy({ id: userId }); const user = await this.userModel.findOneBy({ id: userId });
if (user?.permissions?.includes('order-10-days')) { if (user?.permissions?.includes('order-10-days') && !startDate && !endDate) {
sqlQuery += ` AND o.date_created >= ?`; sqlQuery += ` AND o.date_created >= ?`;
totalQuery += ` AND o.date_created >= ?`; totalQuery += ` AND o.date_created >= ?`;
const tenDaysAgo = new Date(); const tenDaysAgo = new Date();
@ -729,6 +734,21 @@ export class OrderService {
} }
} }
// 仅订阅订单过滤:父订阅订单 或 行项目包含订阅相关元数据(兼容 JSON 与字符串存储)
if (isSubscriptionOnly) {
const subCond = `
AND (
EXISTS (
SELECT 1 FROM subscription s
WHERE s.siteId = o.siteId AND s.parent_id = o.externalOrderId
)
)
`;
sqlQuery += subCond;
totalQuery += subCond;
}
if (customer_email) { if (customer_email) {
sqlQuery += ` AND o.customer_email LIKE ?`; sqlQuery += ` AND o.customer_email LIKE ?`;
totalQuery += ` AND o.customer_email LIKE ?`; totalQuery += ` AND o.customer_email LIKE ?`;
@ -774,7 +794,6 @@ export class OrderService {
// 执行查询 // 执行查询
const orders = await this.orderModel.query(sqlQuery, parameters); const orders = await this.orderModel.query(sqlQuery, parameters);
return { items: orders, total, current, pageSize }; return { items: orders, total, current, pageSize };
} }
@ -786,7 +805,8 @@ export class OrderService {
keyword, keyword,
customer_email, customer_email,
billing_phone, billing_phone,
}) { isSubscriptionOnly = false,
}: any) {
const query = this.orderModel const query = this.orderModel
.createQueryBuilder('order') .createQueryBuilder('order')
.select('order.orderStatus', 'status') .select('order.orderStatus', 'status')
@ -824,11 +844,24 @@ export class OrderService {
); );
} }
if (isSubscriptionOnly) {
query.andWhere(`(
EXISTS (
SELECT 1 FROM subscription s
WHERE s.siteId = order.siteId AND s.parent_id = order.externalOrderId
)
)`);
}
return await query.getRawMany(); return await query.getRawMany();
} }
async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: QueryOrderSalesDTO) { async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : []; const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
const defaultStart = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss');
const defaultEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
startDate = (startDate as any) || defaultStart as any;
endDate = (endDate as any) || defaultEnd as any;
const offset = (current - 1) * pageSize; const offset = (current - 1) * pageSize;
// ------------------------- // -------------------------
@ -1032,6 +1065,10 @@ export class OrderService {
name, name,
}: QueryOrderSalesDTO) { }: QueryOrderSalesDTO) {
const nameKeywords = name ? name.split(' ').filter(Boolean) : []; const nameKeywords = name ? name.split(' ').filter(Boolean) : [];
const defaultStart = dayjs().subtract(30, 'day').startOf('day').format('YYYY-MM-DD HH:mm:ss');
const defaultEnd = dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss');
startDate = (startDate as any) || defaultStart as any;
endDate = (endDate as any) || defaultEnd as any;
// 分页查询 // 分页查询
let sqlQuery = ` let sqlQuery = `
WITH product_purchase_counts AS ( WITH product_purchase_counts AS (
@ -1134,6 +1171,64 @@ export class OrderService {
pageSize, pageSize,
}; };
} }
async getOrderItemList({
siteId,
startDate,
endDate,
current,
pageSize,
name,
externalProductId,
externalVariationId,
}: any) {
const params: any[] = [];
let sql = `
SELECT
oi.*,
o.id AS orderId,
o.externalOrderId AS orderExternalOrderId,
o.date_created AS orderDateCreated,
o.customer_email AS orderCustomerEmail,
o.orderStatus AS orderStatus,
o.siteId AS orderSiteId,
CASE WHEN
JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"is_subscription"')
OR JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"_wcs_bought_as_subscription"')
OR JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"_wcsatt_scheme"')
OR JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"_subscription"')
THEN 1 ELSE 0 END AS isSubscriptionItem
FROM order_item oi
INNER JOIN \`order\` o ON o.id = oi.orderId
WHERE 1=1
`;
let countSql = `
SELECT COUNT(*) AS total
FROM order_item oi
INNER JOIN \`order\` o ON o.id = oi.orderId
WHERE 1=1
`;
const pushFilter = (cond: string, value: any) => {
sql += cond; countSql += cond; params.push(value);
};
if (startDate) pushFilter(' AND o.date_created >= ?', startDate);
if (endDate) pushFilter(' AND o.date_created <= ?', endDate);
if (siteId) pushFilter(' AND oi.siteId = ?', siteId);
if (name) {
pushFilter(' AND oi.name LIKE ?', `%${name}%`);
}
if (externalProductId) pushFilter(' AND oi.externalProductId = ?', externalProductId);
if (externalVariationId) pushFilter(' AND oi.externalVariationId = ?', externalVariationId);
sql += ' ORDER BY o.date_created DESC LIMIT ? OFFSET ?';
const listParams = [...params, pageSize, (current - 1) * pageSize];
const items = await this.orderItemModel.query(sql, listParams);
const [countRow] = await this.orderItemModel.query(countSql, params);
const total = Number(countRow?.total || 0);
return { items, total, current, pageSize };
}
async getOrderDetail(id: number): Promise<OrderDetailRes> { async getOrderDetail(id: number): Promise<OrderDetailRes> {
const order = await this.orderModel.findOne({ where: { id } }); const order = await this.orderModel.findOne({ where: { id } });
const site = this.sites.find(site => site.id === order.siteId); const site = this.sites.find(site => site.id === order.siteId);
@ -1212,6 +1307,75 @@ export class OrderService {
}; };
} }
async getRelatedByOrder(orderId: number) {
const order = await this.orderModel.findOne({ where: { id: orderId } });
if (!order) throw new Error('订单不存在');
const items = await this.orderItemModel.find({ where: { orderId } });
const siteId = order.siteId;
const productIds = items.map(i => i.externalProductId).filter(Boolean);
const variationIds = items.map(i => i.externalVariationId).filter(Boolean);
const subSql = `
SELECT * FROM subscription s
WHERE s.siteId = ? AND s.parent_id = ?
`;
const subscriptions = await this.orderModel.query(subSql, [siteId, order.externalOrderId]);
let conds: string[] = [];
let params: any[] = [siteId, orderId];
if (productIds.length > 0) {
conds.push(`oi.externalProductId IN (${productIds.map(() => '?').join(',')})`);
params = [...productIds, ...params];
}
if (variationIds.length > 0) {
conds.push(`oi.externalVariationId IN (${variationIds.map(() => '?').join(',')})`);
params = [...variationIds, ...params];
}
const whereCond = conds.length ? `AND (${conds.join(' OR ')})` : '';
const relatedItemOrdersSql = `
SELECT DISTINCT o.*
FROM order_item oi
INNER JOIN \`order\` o ON o.id = oi.orderId
WHERE oi.siteId = ?
${whereCond}
AND o.id <> ?
ORDER BY o.date_created DESC
LIMIT 100
`;
const relatedByItems = await this.orderItemModel.query(relatedItemOrdersSql, params);
const relatedBySubscriptionSql = `
SELECT DISTINCT o.*
FROM \`order\` o
WHERE o.siteId = ?
AND o.customer_email = ?
AND o.id <> ?
AND EXISTS (
SELECT 1 FROM order_item oi
WHERE oi.orderId = o.id
AND (
JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"is_subscription"')
OR JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"_wcs_bought_as_subscription"')
OR JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"_wcsatt_scheme"')
OR JSON_CONTAINS(JSON_EXTRACT(oi.meta_data, '$[*].key'), '"_subscription"')
)
)
ORDER BY o.date_created DESC
LIMIT 100
`;
const relatedBySubscription = await this.orderModel.query(relatedBySubscriptionSql, [siteId, order.customer_email, orderId]);
const allOrdersMap = new Map<number, any>();
relatedByItems.forEach(o => allOrdersMap.set(o.id, o));
relatedBySubscription.forEach(o => allOrdersMap.set(o.id, o));
return {
order,
subscriptions,
orders: Array.from(allOrdersMap.values()),
};
}
async delOrder(id: number) { async delOrder(id: number) {
const order = await this.orderModel.findOne({ where: { id } }); const order = await this.orderModel.findOne({ where: { id } });
if (!order) throw new Error('订单不存在'); if (!order) throw new Error('订单不存在');