feat(订单): 添加订单商品列表和关联订单查询功能
- 新增getOrderItemList接口用于查询订单商品列表 - 新增getRelatedByOrder接口用于查询关联订单 - 在QueryOrderDTO中添加isSubscriptionOnly字段用于筛选订阅订单 - 优化订单查询SQL,添加订阅订单过滤条件 - 为日期参数添加默认值处理
This commit is contained in:
parent
eff9efc2c3
commit
8778b8138d
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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('订单不存在');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue