diff --git a/src/controller/order.controller.ts b/src/controller/order.controller.ts index 79f1a61..cdeb3ad 100644 --- a/src/controller/order.controller.ts +++ b/src/controller/order.controller.ts @@ -22,6 +22,7 @@ import { CreateOrderNoteDTO, QueryOrderDTO, QueryOrderSalesDTO, + QueryOrderItemDTO, } from '../dto/order.dto'; import { User } from '../decorator/user.decorator'; 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({ 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({ type: BooleanRes, }) diff --git a/src/dto/order.dto.ts b/src/dto/order.dto.ts index 38ca086..0f0b133 100644 --- a/src/dto/order.dto.ts +++ b/src/dto/order.dto.ts @@ -91,6 +91,10 @@ export class QueryOrderDTO { @ApiProperty() @Rule(RuleType.string()) payment_method: string; + + @ApiProperty({ description: '仅订阅订单(父订阅订单或包含订阅商品)' }) + @Rule(RuleType.bool().default(false)) + isSubscriptionOnly?: boolean; } export class QueryOrderSalesDTO { @@ -119,11 +123,11 @@ export class QueryOrderSalesDTO { name: string; @ApiProperty() - @Rule(RuleType.date().required()) + @Rule(RuleType.date()) startDate: Date; @ApiProperty() - @Rule(RuleType.date().required()) + @Rule(RuleType.date()) endDate: Date; } @@ -141,3 +145,37 @@ export class CreateOrderNoteDTO { @Rule(RuleType.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; +} diff --git a/src/service/order.service.ts b/src/service/order.service.ts index ff6bb4b..e5dd3f3 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -23,6 +23,7 @@ import { } from '../enums/base.enum'; import { Variation } from '../entity/variation.entity'; import { CreateOrderNoteDTO, QueryOrderSalesDTO } from '../dto/order.dto'; +import dayjs = require('dayjs'); import { OrderDetailRes } from '../dto/reponse.dto'; import { OrderNote } from '../entity/order_note.entity'; import { User } from '../entity/user.entity'; @@ -310,6 +311,7 @@ export class OrderService { externalOrderId: string; orderItems: Record[]; }) { + console.log('saveOrderItems params',params) const { siteId, orderId, externalOrderId, orderItems } = params; const currentOrderItems = await this.orderItemModel.find({ where: { siteId, externalOrderId: externalOrderId }, @@ -614,6 +616,7 @@ export class OrderService { customer_email, payment_method, billing_phone, + isSubscriptionOnly = false, }, userId = undefined) { const parameters: any[] = []; @@ -699,12 +702,14 @@ export class OrderService { totalQuery += ` AND o.date_created <= ?`; parameters.push(endDate); } + // 支付方式筛选(使用参数化,避免SQL注入) if (payment_method) { - sqlQuery += ` AND o.payment_method like "%${payment_method}%" `; - totalQuery += ` AND o.payment_method like "%${payment_method}%" `; + sqlQuery += ` AND o.payment_method LIKE ?`; + totalQuery += ` AND o.payment_method LIKE ?`; + parameters.push(`%${payment_method}%`); } 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 >= ?`; totalQuery += ` AND o.date_created >= ?`; 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) { sqlQuery += ` 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); - return { items: orders, total, current, pageSize }; } @@ -786,7 +805,8 @@ export class OrderService { keyword, customer_email, billing_phone, - }) { + isSubscriptionOnly = false, + }: any) { const query = this.orderModel .createQueryBuilder('order') .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(); } async getOrderSales({ siteId, startDate, endDate, current, pageSize, name, exceptPackage }: QueryOrderSalesDTO) { 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; // ------------------------- @@ -1032,6 +1065,10 @@ export class OrderService { name, }: QueryOrderSalesDTO) { 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 = ` WITH product_purchase_counts AS ( @@ -1134,6 +1171,64 @@ export class OrderService { 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 { const order = await this.orderModel.findOne({ where: { id } }); 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(); + 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) { const order = await this.orderModel.findOne({ where: { id } }); if (!order) throw new Error('订单不存在');