From ba777c3563f487cca4f72f1abbdb5e61b256388d Mon Sep 17 00:00:00 2001 From: cll <931958862@qq.com> Date: Sat, 19 Jul 2025 11:42:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=A2=E6=88=B7=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config.default.ts | 4 + src/controller/customer.controller.ts | 70 ++++ src/controller/order.controller.ts | 10 + src/controller/product.controller.ts | 18 + src/controller/statistics.controller.ts | 22 +- src/controller/stock.controller.ts | 11 + src/dto/customer.dto.ts | 38 +++ src/entity/customer.entity.ts | 10 + src/entity/customer_tag.entity.ts | 25 ++ src/entity/product.entty.ts | 4 + src/service/customer.service.ts | 151 +++++++++ src/service/logistics.service.ts | 20 +- src/service/order.service.ts | 431 +++++++++++++++++------- src/service/product.service.ts | 74 +++- src/service/statistics.service.ts | 235 ++++++++++++- src/service/stock.service.ts | 36 +- 16 files changed, 988 insertions(+), 171 deletions(-) create mode 100644 src/controller/customer.controller.ts create mode 100644 src/dto/customer.dto.ts create mode 100644 src/entity/customer.entity.ts create mode 100644 src/entity/customer_tag.entity.ts create mode 100644 src/service/customer.service.ts diff --git a/src/config/config.default.ts b/src/config/config.default.ts index f4e6d34..41fe292 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -27,6 +27,8 @@ import { Transfer } from '../entity/transfer.entity'; import { TransferItem } from '../entity/transfer_item.entity'; import { Strength } from '../entity/strength.entity'; import { Flavors } from '../entity/flavors.entity'; +import { CustomerTag } from '../entity/customer_tag.entity'; +import { Customer } from '../entity/customer.entity'; export default { // use for cookie sign key, should change to your own and keep security @@ -62,6 +64,8 @@ export default { OrderNote, Transfer, TransferItem, + CustomerTag, + Customer, ], synchronize: true, logging: false, diff --git a/src/controller/customer.controller.ts b/src/controller/customer.controller.ts new file mode 100644 index 0000000..2c2b2de --- /dev/null +++ b/src/controller/customer.controller.ts @@ -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); + } + } +} diff --git a/src/controller/order.controller.ts b/src/controller/order.controller.ts index 1c09f3e..904ae9d 100644 --- a/src/controller/order.controller.ts +++ b/src/controller/order.controller.ts @@ -194,4 +194,14 @@ export class OrderController { return errorResponse(error?.message || '创建失败'); } } + + @ApiOkResponse() + @Post('/order/pengding/items') + async pengdingItems(@Body() data: Record) { + try { + return successResponse(await this.orderService.pengdingItems(data)); + } catch (error) { + return errorResponse(error?.message || '获取失败'); + } + } } diff --git a/src/controller/product.controller.ts b/src/controller/product.controller.ts index 7e85072..cfdedb6 100644 --- a/src/controller/product.controller.ts +++ b/src/controller/product.controller.ts @@ -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({ type: BooleanRes, }) diff --git a/src/controller/statistics.controller.ts b/src/controller/statistics.controller.ts index 1e41bbf..424c380 100644 --- a/src/controller/statistics.controller.ts +++ b/src/controller/statistics.controller.ts @@ -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 { OrderStatisticsParams } from '../dto/statistics.dto'; import { errorResponse, successResponse } from '../utils/response.util'; @@ -76,4 +76,24 @@ export class StatisticsController { 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 || '获取失败'); + } + } } diff --git a/src/controller/stock.controller.ts b/src/controller/stock.controller.ts index 9db7768..a407df0 100644 --- a/src/controller/stock.controller.ts +++ b/src/controller/stock.controller.ts @@ -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: '获取库存列表' }) @Get('/') async getStocks(@Query() query: QueryStockDTO) { diff --git a/src/dto/customer.dto.ts b/src/dto/customer.dto.ts new file mode 100644 index 0000000..9fa62cd --- /dev/null +++ b/src/dto/customer.dto.ts @@ -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; +} diff --git a/src/entity/customer.entity.ts b/src/entity/customer.entity.ts new file mode 100644 index 0000000..a018a79 --- /dev/null +++ b/src/entity/customer.entity.ts @@ -0,0 +1,10 @@ +import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; + +@Entity('customer') +export class Customer { + @PrimaryGeneratedColumn() + id: number; + + @Column({ unique: true }) + email: string; +} \ No newline at end of file diff --git a/src/entity/customer_tag.entity.ts b/src/entity/customer_tag.entity.ts new file mode 100644 index 0000000..3203676 --- /dev/null +++ b/src/entity/customer_tag.entity.ts @@ -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; +} diff --git a/src/entity/product.entty.ts b/src/entity/product.entty.ts index 04bba4b..e328725 100644 --- a/src/entity/product.entty.ts +++ b/src/entity/product.entty.ts @@ -27,6 +27,10 @@ export class Product { @Column() name: string; + @ApiProperty() + @Column({ default: ''}) + nameCn: string; + @ApiProperty({ example: '产品描述', description: '产品描述', type: 'string' }) @Column({ nullable: true }) description?: string; diff --git a/src/service/customer.service.ts b/src/service/customer.service.ts new file mode 100644 index 0000000..b054e68 --- /dev/null +++ b/src/service/customer.service.ts @@ -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; + + @InjectEntityModel(CustomerTag) + customerTagModel: Repository; + + async getCustomerList(param: Record) { + 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' + }` + : '' + } + 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); + } +} diff --git a/src/service/logistics.service.ts b/src/service/logistics.service.ts index 4da40b0..1e24517 100644 --- a/src/service/logistics.service.ts +++ b/src/service/logistics.service.ts @@ -195,16 +195,16 @@ export class LogisticsService { ) { throw new Error('订单状态不正确 '); } - for (const item of data?.sales) { - const stock = await this.stockModel.findOne({ - where: { - stockPointId: data.stockPointId, - productSku: item.sku, - }, - }); - if (!stock || stock.quantity < item.quantity) - throw new Error(item.name + '库存不足'); - } + // for (const item of data?.sales) { + // const stock = await this.stockModel.findOne({ + // where: { + // stockPointId: data.stockPointId, + // productSku: item.sku, + // }, + // }); + // if (!stock || stock.quantity < item.quantity) + // throw new Error(item.name + '库存不足'); + // } let shipment: Shipment; if (data.service_type === ShipmentType.FREIGHTCOM) { diff --git a/src/service/order.service.ts b/src/service/order.service.ts index dd59194..47bd234 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -14,6 +14,7 @@ import { OrderRefundItem } from '../entity/order_retund_item.entity'; import { OrderCoupon } from '../entity/order_copon.entity'; import { OrderShipping } from '../entity/order_shipping.entity'; import { Shipment } from '../entity/shipment.entity'; +import { Customer } from '../entity/customer.entity'; import { ErpOrderStatus, OrderStatus, @@ -85,6 +86,9 @@ export class OrderService { @Inject() dataSourceManager: TypeORMDataSourceManager; + @InjectEntityModel(Customer) + customerModel: Repository; + async syncOrders(siteId: string) { const orders = await this.wPService.getOrders(siteId); // 调用 WooCommerce API 获取订单 for (const order of orders) { @@ -159,15 +163,16 @@ export class OrderService { where: { orderId: existingOrder.id }, }); if (!items) return; - const stockPointId = ['YT', 'NT', 'BC', 'AB', 'SK'].some( - v => - v.toLowerCase() === - ( - existingOrder?.shipping?.state || existingOrder?.billing?.state - ).toLowerCase() - ) - ? 3 - : 2; + const stockPointId = 2; + // ['YT', 'NT', 'BC', 'AB', 'SK'].some( + // v => + // v.toLowerCase() === + // ( + // existingOrder?.shipping?.state || existingOrder?.billing?.state + // ).toLowerCase() + // ) + // ? 3 + // : 2; for (const item of items) { const updateStock = new UpdateStockDTO(); updateStock.stockPointId = stockPointId; @@ -211,6 +216,14 @@ export class OrderService { return entity; } 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); } @@ -721,173 +734,292 @@ export class OrderService { return await query.getRawMany(); } - 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) : []; - // 分页查询 - 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]; + + // 主查询:带分页 + 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(' AND '); + 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 ? `; - sqlQuery += ' LIMIT ? OFFSET ?'; - parameters.push(pageSize, (current - 1) * pageSize); - // 执行查询并传递参数 + parameters.push(pageSize, (current - 1) * pageSize); const items = await this.orderSaleModel.query(sqlQuery, parameters); + // 总条数 + const countParams: any[] = [startDate, endDate]; 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') + WHERE o.date_paid BETWEEN ? AND ? + AND o.status IN ('completed', 'processing') `; - const totalCountParameters: any[] = [startDate, endDate]; if (siteId) { totalCountQuery += ' AND os.siteId = ?'; - totalCountParameters.push(siteId); + countParams.push(siteId); } if (nameKeywords.length > 0) { - totalCountQuery += - ' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND '); - totalCountParameters.push(...nameKeywords.map(word => `%${word}%`)); + totalCountQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')'; + countParams.push(...nameKeywords.map(word => `%${word}%`)); } - const totalCountResult = await this.orderSaleModel.query( - totalCountQuery, - totalCountParameters - ); + const totalCountResult = await this.orderSaleModel.query(totalCountQuery, countParams); + // 一次查询获取所有 yoone box 数量 + const totalQuantityParams: any[] = [startDate, endDate]; 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 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') + WHERE o.date_paid BETWEEN ? AND ? + AND o.status IN ('completed', 'processing') `; - - const totalQuantityParameters: any[] = [startDate, endDate]; if (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) { - totalQuantityQuery += - ' AND ' + nameKeywords.map(() => `os.name LIKE ?`).join(' AND '); - totalQuantityParameters.push(...nameKeywords.map(word => `%${word}%`)); + totalQuantityQuery += ' AND (' + nameKeywords.map(() => 'os.name LIKE ?').join(' OR ') + ')'; + totalQuantityParams.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 - ); + const [totalQuantityResult] = await this.orderSaleModel.query(totalQuantityQuery, totalQuantityParams); 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) - ), + total: totalCountResult[0]?.totalCount || 0, + totalQuantity: Number(totalQuantityResult.totalQuantity || 0), + yoone3Quantity: Number(totalQuantityResult.yoone3Quantity || 0), + yoone6Quantity: Number(totalQuantityResult.yoone6Quantity || 0), + yoone9Quantity: Number(totalQuantityResult.yoone9Quantity || 0), + yoone12Quantity: Number(totalQuantityResult.yoone12Quantity || 0), + yoone15Quantity: Number(totalQuantityResult.yoone15Quantity || 0), current, 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({ siteId, startDate, @@ -1186,4 +1318,41 @@ export class OrderService { } }); } + + async pengdingItems(data: Record) { + 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, + }; + } } diff --git a/src/service/product.service.ts b/src/service/product.service.ts index 4b9807b..509af9a 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -1,5 +1,5 @@ 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 { Category } from '../entity/category.entity'; import { paginate } from '../utils/paginate.util'; @@ -46,19 +46,58 @@ export class ProductService { @InjectEntityModel(Variation) variationModel: Repository; + // async findProductsByName(name: string): Promise { + // 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 { - 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); + const query = this.productModel.createQueryBuilder('product'); + + // 保证 sku 不为空 + query.where('product.sku IS NOT NULL'); + + if (nameFilter.length > 0 || name) { + const params: Record = {}; + const conditions: string[] = []; + + // 英文名关键词全部匹配(AND) + if (nameFilter.length > 0) { + const nameConds = nameFilter.map((word, index) => { + const key = `name${index}`; + params[key] = `%${word}%`; + return `product.name LIKE :${key}`; + }); + conditions.push(`(${nameConds.join(' AND ')})`); + } + + // 中文名模糊匹配 + if (name) { + params['nameCn'] = `%${name}%`; + conditions.push(`product.nameCn LIKE :nameCn`); + } + + // 英文名关键词匹配 OR 中文名匹配 + query.andWhere(`(${conditions.join(' OR ')})`, params); } - where.sku = Not(IsNull()); - // 查询 SKU 不为空且 name 包含关键字的产品,最多返回 50 条 - return this.productModel.find({ - where, - take: 50, - }); + + query.take(50); + + return await query.getMany(); } async findProductBySku(sku: string): Promise { @@ -84,6 +123,7 @@ export class ProductService { .select([ 'product.id as id', 'product.name as name', + 'product.nameCn as nameCn', 'product.description as description', 'product.humidity as humidity', 'product.sku as sku', @@ -168,6 +208,18 @@ export class ProductService { // 返回更新后的产品 return await this.productModel.findOneBy({ id }); } + + async updateProductNameCn(id: number, nameCn: string): Promise { + // 确认产品是否存在 + 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 { // 检查产品是否存在 diff --git a/src/service/statistics.service.ts b/src/service/statistics.service.ts index 03b6eff..b7ce60d 100644 --- a/src/service/statistics.service.ts +++ b/src/service/statistics.service.ts @@ -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 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 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 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.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.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.togo_total_orders, dt.can_total_orders, @@ -141,7 +143,8 @@ export class StatisticsService { dt.non_yoone_total, dt.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, 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, @@ -204,7 +207,8 @@ export class StatisticsService { dt.non_yoone_total, dt.zex_total, dt.non_zex_total, - dt.direct_first_total, + dt.direct_total, + dt.organic_total, dt.total_orders, dt.togo_total_orders, dt.can_total_orders @@ -315,6 +319,11 @@ export class StatisticsService { FROM \`order\` 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 ( SELECT customer_email, @@ -330,14 +339,21 @@ export class StatisticsService { JSON_OBJECT('name', oi.name, 'quantity', oi.quantity) ) AS orderItems, f.first_purchase_date, + l.last_purchase_date, CASE WHEN o.date_paid = f.first_purchase_date THEN 'first_purchase' ELSE 'repeat_purchase' END AS purchase_type, 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 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 customer_stats cs ON o.customer_email = cs.customer_email WHERE o.date_paid BETWEEN ? AND ? @@ -357,7 +373,12 @@ export class StatisticsService { o.*, JSON_ARRAYAGG( 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 LEFT JOIN order_item oi ON oi.orderId = o.id WHERE o.customer_email='${email}' @@ -901,4 +922,206 @@ export class StatisticsService { 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 + + } + } diff --git a/src/service/stock.service.ts b/src/service/stock.service.ts index 1d12333..2895286 100644 --- a/src/service/stock.service.ts +++ b/src/service/stock.service.ts @@ -218,6 +218,16 @@ export class StockService { 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) { const { current = 1, pageSize = 10, productName } = query; @@ -231,13 +241,15 @@ export class StockService { // 'stock.id as id', 'stock.productSku as productSku', 'product.name as productName', + 'product.nameCn as productNameCn', 'JSON_ARRAYAGG(JSON_OBJECT("id", stock.stockPointId, "quantity", stock.quantity)) as stockPoint', 'MIN(stock.updatedAt) as updatedAt', 'MAX(stock.createdAt) as createdAt', ]) .leftJoin(Product, 'product', 'product.sku = stock.productSku') .groupBy('stock.productSku') - .addGroupBy('product.name'); + .addGroupBy('product.name') + .addGroupBy('product.nameCn'); let totalQueryBuilder = this.stockModel .createQueryBuilder('stock') .select('COUNT(DISTINCT stock.productSku)', 'count') @@ -312,9 +324,9 @@ export class StockService { // 更新库存 stock.quantity += operationType === 'in' ? quantityChange : -quantityChange; - if (stock.quantity < 0) { - throw new Error('库存不足,无法完成操作'); - } + // if (stock.quantity < 0) { + // throw new Error('库存不足,无法完成操作'); + // } await this.stockModel.save(stock); } @@ -375,14 +387,14 @@ export class StockService { async createTransfer(data: Record, userId: number) { const { sourceStockPointId, destStockPointId, sendAt, items, note } = data; - for (const item of items) { - const stock = await this.stockModel.findOneBy({ - stockPointId: sourceStockPointId, - productSku: item.productSku, - }); - if (!stock || stock.quantity < item.quantity) - throw new Error(`${item.productName} 库存不足`); - } + // for (const item of items) { + // const stock = await this.stockModel.findOneBy({ + // stockPointId: sourceStockPointId, + // productSku: item.productSku, + // }); + // if (!stock || stock.quantity < item.quantity) + // throw new Error(`${item.productName} 库存不足`); + // } const now = dayjs().format('YYYY-MM-DD'); const count = await this.transferModel.count({ where: {