From 2d2ba67f0cfc42849ae62faa864e38e2594b90f6 Mon Sep 17 00:00:00 2001 From: zhuotianyuan Date: Tue, 6 Jan 2026 18:43:30 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(webhook):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=AF=B9shoppy=E5=B9=B3=E5=8F=B0webhook=E7=9A=84=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在site.entity.ts中添加webhookUrl字段 - 在auth.middleware.ts中添加/shoppy路由到白名单 - 在webhook.controller.ts中实现shoppy平台webhook处理逻辑 fix(webhook): 更新webhook控制器中的密钥值 refactor(entity): 将可选字段明确标记为可选类型 feat(adapter): 公开映射方法以支持统一接口调用 将各适配器中的私有映射方法改为公开,并在接口中定义统一方法签名 修改webhook控制器以使用适配器映射方法处理订单数据 feat: 添加订单支付日期字段并支持国家筛选 - 在ShopyyOrder接口中添加date_paid字段 - 在OrderStatisticsParams中添加country数组字段用于国家筛选 - 修改统计服务以支持按国家筛选订单数据 - 更新数据库配置和同步设置 - 优化订单服务中的类型定义和查询条件 refactor(webhook): 移除未使用的shoppy webhook处理逻辑 fix(订单服务): 修复订单内容括号处理并添加同步日志 添加订单同步过程的调试日志 修复订单内容中括号内容的处理逻辑 修正控制器方法名拼写错误 --- src/adapter/shopyy.adapter.ts | 2 +- src/config/config.local.ts | 9 +- src/controller/statistics.controller.ts | 2 +- src/controller/webhook.controller.ts | 19 ++-- src/dto/shopyy.dto.ts | 1 + src/dto/site.dto.ts | 6 +- src/dto/statistics.dto.ts | 4 + src/service/order.service.ts | 113 ++++++++++++++++++++---- src/service/statistics.service.ts | 61 +++++++++++-- 9 files changed, 175 insertions(+), 42 deletions(-) diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index d9215bb..2843547 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -367,7 +367,6 @@ export class ShopyyAdapter implements ISiteAdapter { date_paid: typeof item.pay_at === 'number' ? item.pay_at === 0 ? null : new Date(item.pay_at * 1000).toISOString() : null, - refunds: [], currency_symbol: (currencySymbols[item.currency] || '$') || '', date_created: @@ -387,6 +386,7 @@ export class ShopyyAdapter implements ISiteAdapter { tracking_number: f.tracking_number || '', shipping_provider: f.tracking_company || '', shipping_method: f.tracking_company || '', + date_created: typeof f.created_at === 'number' ? new Date(f.created_at * 1000).toISOString() : f.created_at || '', diff --git a/src/config/config.local.ts b/src/config/config.local.ts index b02f922..67785b5 100644 --- a/src/config/config.local.ts +++ b/src/config/config.local.ts @@ -12,14 +12,15 @@ export default { // }, // }, // }, - typeorm: { + typeorm: { dataSource: { default: { - host: 'localhost', - port: "23306", + host: '13.212.62.127', + port: "3306", username: 'root', - password: '12345678', + password: 'Yoone!@.2025', database: 'inventory_v2', + synchronize: true, }, }, }, diff --git a/src/controller/statistics.controller.ts b/src/controller/statistics.controller.ts index 424c380..c873c78 100644 --- a/src/controller/statistics.controller.ts +++ b/src/controller/statistics.controller.ts @@ -79,7 +79,7 @@ export class StatisticsController { @ApiOkResponse() @Get('/orderSource') - async getOrderSorce(@Query() params) { + async getOrderSource(@Query() params) { try { return successResponse(await this.statisticsService.getOrderSorce(params)); } catch (error) { diff --git a/src/controller/webhook.controller.ts b/src/controller/webhook.controller.ts index 6132182..28ab2e6 100644 --- a/src/controller/webhook.controller.ts +++ b/src/controller/webhook.controller.ts @@ -14,6 +14,8 @@ import { SiteService } from '../service/site.service'; import { OrderService } from '../service/order.service'; import { SiteApiService } from '../service/site-api.service'; + + @Controller('/webhook') export class WebhookController { private secret = 'YOONE24kd$kjcdjflddd'; @@ -177,20 +179,15 @@ export class WebhookController { console.log('Unhandled event:', topic); } - return { - code: 200, - success: true, - message: 'Webhook processed successfully', - }; - } else { - return { - code: 403, - success: false, - message: 'Webhook verification failed', - }; + return { + code: 200, + success: true, + message: 'Webhook processed successfully', + }; } } catch (error) { console.log(error); } } + } diff --git a/src/dto/shopyy.dto.ts b/src/dto/shopyy.dto.ts index e775bdd..b434fa8 100644 --- a/src/dto/shopyy.dto.ts +++ b/src/dto/shopyy.dto.ts @@ -346,6 +346,7 @@ export interface ShopyyOrder { financial_status?: number; fulfillment_status?: number; // 创建与更新时间可能为时间戳 + date_paid?: number | string; created_at?: number | string; date_added?: string; updated_at?: number | string; diff --git a/src/dto/site.dto.ts b/src/dto/site.dto.ts index 7c0267d..c8509d7 100644 --- a/src/dto/site.dto.ts +++ b/src/dto/site.dto.ts @@ -121,7 +121,7 @@ export class UpdateSiteDTO { skuPrefix?: string; // 区域 - @ApiProperty({ description: '区域' }) + @ApiProperty({ description: '区域', required: false }) @Rule(RuleType.array().items(RuleType.string()).optional()) areas?: string[]; @@ -133,6 +133,10 @@ export class UpdateSiteDTO { @ApiProperty({ description: '站点网站URL', required: false }) @Rule(RuleType.string().optional()) websiteUrl?: string; + + @ApiProperty({ description: 'Webhook URL', required: false }) + @Rule(RuleType.string().optional()) + webhookUrl?: string; } export class QuerySiteDTO { diff --git a/src/dto/statistics.dto.ts b/src/dto/statistics.dto.ts index 2bd6250..da6f46a 100644 --- a/src/dto/statistics.dto.ts +++ b/src/dto/statistics.dto.ts @@ -19,6 +19,10 @@ export class OrderStatisticsParams { @Rule(RuleType.number().allow(null)) siteId?: number; + @ApiProperty() + @Rule(RuleType.array().allow(null)) + country?: any[]; + @ApiProperty({ enum: ['all', 'first_purchase', 'repeat_purchase'], default: 'all', diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 3dfd68b..24293eb 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -138,7 +138,7 @@ export class OrderService { updated: 0, errors: [] }; - + console.log('开始进入循环同步订单', result.length, '个订单') // 遍历每个订单进行同步 for (const order of result) { try { @@ -162,6 +162,7 @@ export class OrderService { } else { syncResult.created++; } + // console.log('updated', syncResult.updated, 'created:', syncResult.created) } catch (error) { // 记录错误但不中断整个同步过程 syncResult.errors.push({ @@ -171,6 +172,8 @@ export class OrderService { syncResult.processed++; } } + console.log('同步完成', syncResult.updated, 'created:', syncResult.created) + this.logger.debug('syncOrders result', syncResult) return syncResult; } @@ -301,7 +304,7 @@ export class OrderService { * @param order 订单数据 * @param forceUpdate 是否强制更新 */ - async syncSingleOrder(siteId: number, order: any, forceUpdate = false) { + async syncSingleOrder(siteId: number, order: UnifiedOrderDTO, forceUpdate = false) { // 从订单数据中解构出各个子项 let { line_items, @@ -319,7 +322,7 @@ export class OrderService { } // 检查数据库中是否已存在该订单 const existingOrder = await this.orderModel.findOne({ - where: { externalOrderId: order.id, siteId: siteId }, + where: { externalOrderId: String(order.id), siteId: siteId }, }); // 自动更新订单状态(如果需要) await this.autoUpdateOrderStatus(siteId, order); @@ -328,10 +331,10 @@ export class OrderService { // 矫正数据库中的订单数据 const updateData: any = { status: order.status }; if (this.canUpdateErpStatus(existingOrder.orderStatus)) { - updateData.orderStatus = this.mapOrderStatus(order.status); + updateData.orderStatus = this.mapOrderStatus(order.status as any); } - // 更新 - await this.orderModel.update({ externalOrderId: order.id, siteId: siteId }, updateData); + // 更新订单主数据 + await this.orderModel.update({ externalOrderId: String(order.id), siteId: siteId }, updateData); // 更新 fulfillments 数据 await this.saveOrderFulfillments({ siteId, @@ -340,7 +343,7 @@ export class OrderService { fulfillments: fulfillments, }); } - const externalOrderId = order.id; + const externalOrderId = String(order.id); // 如果订单从未完成变为完成状态,则更新库存 if ( existingOrder && @@ -361,14 +364,14 @@ export class OrderService { await this.saveOrderItems({ siteId, orderId, - externalOrderId, + externalOrderId: String(externalOrderId), orderItems: line_items, }); // 保存退款信息 await this.saveOrderRefunds({ siteId, orderId, - externalOrderId, + externalOrderId , refunds, }); // 保存费用信息 @@ -459,7 +462,7 @@ export class OrderService { * @param order 订单数据 * @returns 保存后的订单实体 */ - async saveOrder(siteId: number, order: UnifiedOrderDTO): Promise { + async saveOrder(siteId: number, order: Partial): Promise { // 将外部订单ID转换为字符串 const externalOrderId = String(order.id) delete order.id @@ -1234,13 +1237,13 @@ export class OrderService { parameters.push(siteId); } if (startDate) { - sqlQuery += ` AND o.date_created >= ?`; - totalQuery += ` AND o.date_created >= ?`; + sqlQuery += ` AND o.date_paid >= ?`; + totalQuery += ` AND o.date_paid >= ?`; parameters.push(startDate); } if (endDate) { - sqlQuery += ` AND o.date_created <= ?`; - totalQuery += ` AND o.date_created <= ?`; + sqlQuery += ` AND o.date_paid <= ?`; + totalQuery += ` AND o.date_paid <= ?`; parameters.push(endDate); } // 支付方式筛选(使用参数化,避免SQL注入) @@ -1328,7 +1331,7 @@ export class OrderService { // 添加分页到主查询 sqlQuery += ` GROUP BY o.id - ORDER BY o.date_created DESC + ORDER BY o.date_paid DESC LIMIT ? OFFSET ? `; parameters.push(pageSize, (current - 1) * pageSize); @@ -2545,7 +2548,7 @@ export class OrderService { '姓名地址': nameAddress, '邮箱': order.customer_email || '', '号码': phone, - '订单内容': orderContent, + '订单内容': this.removeLastParenthesesContent(orderContent), '盒数': boxCount, '换盒数': exchangeBoxCount, '换货内容': exchangeContent, @@ -2646,6 +2649,84 @@ async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: } } + /** + * 删除每个分号前面一个左右括号和最后一个左右括号包含的内容(包括括号本身) + * @param str 输入字符串 + * @returns 删除后的字符串 + */ + removeLastParenthesesContent(str: string): string { + if (!str || typeof str !== 'string') { + return str; + } + + // 辅助函数:删除指定位置的括号对及其内容 + const removeParenthesesAt = (s: string, leftIndex: number): string => { + if (leftIndex === -1) return s; + + let rightIndex = -1; + let parenCount = 0; + + for (let i = leftIndex; i < s.length; i++) { + const char = s[i]; + if (char === '(') { + parenCount++; + } else if (char === ')') { + parenCount--; + if (parenCount === 0) { + rightIndex = i; + break; + } + } + } + + if (rightIndex !== -1) { + return s.substring(0, leftIndex) + s.substring(rightIndex + 1); + } + + return s; + }; + + // 1. 处理每个分号前面的括号对 + let result = str; + + // 找出所有分号的位置 + const semicolonIndices: number[] = []; + for (let i = 0; i < result.length; i++) { + if (result[i] === ';') { + semicolonIndices.push(i); + } + } + + // 从后向前处理每个分号,避免位置变化影响后续处理 + for (let i = semicolonIndices.length - 1; i >= 0; i--) { + const semicolonIndex = semicolonIndices[i]; + + // 从分号位置向前查找最近的左括号 + let lastLeftParenIndex = -1; + for (let j = semicolonIndex - 1; j >= 0; j--) { + if (result[j] === '(') { + lastLeftParenIndex = j; + break; + } + } + + // 如果找到左括号,删除该括号对及其内容 + if (lastLeftParenIndex !== -1) { + result = removeParenthesesAt(result, lastLeftParenIndex); + } + } + + // 2. 处理整个字符串的最后一个括号对 + let lastLeftParenIndex = result.lastIndexOf('('); + if (lastLeftParenIndex !== -1) { + result = removeParenthesesAt(result, lastLeftParenIndex); + } + + return result; + } + + + } diff --git a/src/service/statistics.service.ts b/src/service/statistics.service.ts index 4352633..fd0f741 100644 --- a/src/service/statistics.service.ts +++ b/src/service/statistics.service.ts @@ -15,8 +15,19 @@ export class StatisticsService { orderItemRepository: Repository; async getOrderStatistics(params: OrderStatisticsParams) { - const { startDate, endDate, grouping, siteId } = params; + const { startDate, endDate, grouping, siteId, country } = params; // const keywords = keyword ? keyword.split(' ').filter(Boolean) : []; + + let siteIds = [] + if (country) { + siteIds = await this.getSiteIds(country) + } + + if (siteId) { + siteIds.push(siteId) + } + + const start = dayjs(startDate).format('YYYY-MM-DD'); const end = dayjs(endDate).add(1, 'd').format('YYYY-MM-DD'); let sql @@ -54,6 +65,8 @@ export class StatisticsService { AND o.status IN('processing','completed') `; if (siteId) sql += ` AND o.siteId=${siteId}`; + if (siteIds.length) sql += ` AND o.siteId IN (${siteIds.join(',')})`; + sql += ` GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source ), @@ -247,7 +260,10 @@ export class StatisticsService { LEFT JOIN order_item oi ON o.id = oi.orderId WHERE o.date_paid IS NOT NULL AND o.date_paid >= '${start}' AND o.date_paid < '${end}' - AND o.status IN ('processing','completed') + AND o.status IN ('processing','completed')`; + if (siteId) sql += ` AND o.siteId=${siteId}`; + if (siteIds.length) sql += ` AND o.siteId IN (${siteIds.join(',')})`; + sql +=` GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source ), order_sales_summary AS ( @@ -439,7 +455,11 @@ export class StatisticsService { LEFT JOIN first_order f ON o.customer_email = f.customer_email LEFT JOIN order_item oi ON o.id = oi.orderId WHERE o.date_paid IS NOT NULL - AND o.date_paid >= '${start}' AND o.date_paid < '${end}' + AND o.date_paid >= '${start}' AND o.date_paid < '${end}' + `; + if (siteId) sql += ` AND o.siteId=${siteId}`; + if (siteIds.length) sql += ` AND o.siteId IN (${siteIds.join(',')})`; + sql +=` AND o.status IN ('processing','completed') GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source ), @@ -1314,7 +1334,14 @@ export class StatisticsService { } async getOrderSorce(params) { - const sql = ` + const { country } = params; + + let siteIds = [] + if (country) { + siteIds = await this.getSiteIds(country) + } + + let sql = ` WITH cutoff_months AS ( SELECT DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 7 MONTH), '%Y-%m') AS start_month, @@ -1326,7 +1353,10 @@ export class StatisticsService { DATE_FORMAT(MIN(date_paid), '%Y-%m') AS first_order_month, SUM(total) AS first_order_total FROM \`order\` - WHERE status IN ('processing', 'completed') + WHERE status IN ('processing', 'completed')`; + if (siteIds.length!=0) sql += ` AND siteId IN ('${siteIds.join("','")}')`; + else sql += ` AND siteId IS NULL `; + sql += ` GROUP BY customer_email ), order_months AS ( @@ -1334,7 +1364,10 @@ export class StatisticsService { customer_email, DATE_FORMAT(date_paid, '%Y-%m') AS order_month FROM \`order\` - WHERE status IN ('processing', 'completed') + WHERE status IN ('processing', 'completed')`; + if (siteIds.length!=0) sql += ` AND siteId IN ('${siteIds.join("','")}')`; + else sql += ` AND siteId IS NULL `; + sql += ` ), filtered_orders AS ( SELECT o.customer_email, o.order_month, u.first_order_month,u.first_order_total, c.start_month @@ -1366,7 +1399,7 @@ export class StatisticsService { ORDER BY order_month DESC, first_order_month_group ` - const inactiveSql = ` + let inactiveSql = ` WITH cutoff_months AS ( SELECT @@ -1381,7 +1414,10 @@ export class StatisticsService { date_paid, total FROM \`order\` - WHERE status IN ('processing', 'completed') + WHERE status IN ('processing', 'completed')`; + if (siteIds.length!=0) inactiveSql += ` AND siteId IN ('${siteIds.join("','")}')`; + else inactiveSql += ` AND siteId IS NULL `; + inactiveSql += ` ), filtered_users AS ( @@ -1524,4 +1560,13 @@ export class StatisticsService { } + async getSiteIds(country: any[]) { + const sql = ` + SELECT DISTINCT sa.siteId as site_id FROM area a left join site_areas_area sa on a.id = sa.areaId WHERE a.code IN ('${country.join("','")}') + ` + const res = await this.orderRepository.query(sql) + return res.map(item => item.site_id) + } + + } -- 2.40.1 From c1ecebb341ee14858c543fedfb3794f1037c7c8f Mon Sep 17 00:00:00 2001 From: zhuotianyuan Date: Sat, 10 Jan 2026 14:27:08 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(config):=20=E5=B0=86=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E9=85=8D=E7=BD=AE=E6=9B=B4=E6=94=B9=E4=B8=BA=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E5=BC=80=E5=8F=91=E7=8E=AF=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更新数据库连接配置为本地开发环境,包括主机、端口和密码 移除自动同步数据库的配置项 --- src/config/config.local.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/config/config.local.ts b/src/config/config.local.ts index 67785b5..d64e2ba 100644 --- a/src/config/config.local.ts +++ b/src/config/config.local.ts @@ -15,12 +15,11 @@ export default { typeorm: { dataSource: { default: { - host: '13.212.62.127', - port: "3306", + host: 'localhost', + port: "23306", username: 'root', - password: 'Yoone!@.2025', + password: '12345678', database: 'inventory_v2', - synchronize: true, }, }, }, -- 2.40.1 From e939e3e978c0574f5b3b164fd9ae11bf69be0e90 Mon Sep 17 00:00:00 2001 From: zhuotianyuan Date: Sat, 10 Jan 2026 14:32:59 +0800 Subject: [PATCH 3/3] =?UTF-8?q?style:=20=E4=BF=AE=E5=A4=8D=20typeorm=20?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E7=BC=A9=E8=BF=9B=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/config/config.local.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/config.local.ts b/src/config/config.local.ts index d64e2ba..b02f922 100644 --- a/src/config/config.local.ts +++ b/src/config/config.local.ts @@ -12,7 +12,7 @@ export default { // }, // }, // }, - typeorm: { + typeorm: { dataSource: { default: { host: 'localhost', -- 2.40.1