diff --git a/.gitignore b/.gitignore index 9f2715f..6298f56 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ container scripts ai tmp_uploads/ +*.json +*config* \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4f93b95..70aed39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,7 @@ "mysql2": "^3.15.3", "nodemailer": "^7.0.5", "npm-check-updates": "^19.1.2", + "qs": "^6.14.0", "swagger-ui-dist": "^5.18.2", "typeorm": "^0.3.27", "typeorm-extension": "^3.7.2", @@ -3757,7 +3758,7 @@ }, "node_modules/qs": { "version": "6.14.0", - "resolved": "https://registry.npmmirror.com/qs/-/qs-6.14.0.tgz", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", "license": "BSD-3-Clause", "dependencies": { diff --git a/package.json b/package.json index 1270b87..9601e34 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "mysql2": "^3.15.3", "nodemailer": "^7.0.5", "npm-check-updates": "^19.1.2", + "qs": "^6.14.0", "swagger-ui-dist": "^5.18.2", "typeorm": "^0.3.27", "typeorm-extension": "^3.7.2", diff --git a/src/config/config.default.ts b/src/config/config.default.ts index e16fa81..5402adf 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -152,4 +152,8 @@ export default { tmpdir: join(__dirname, '../../tmp_uploads'), cleanTimeout: 5 * 60 * 1000, }, + koa: { + queryParseMode: 'extended', // 使用 qs 模块 + // 其他选项:'strict', 'first' + }, } as MidwayConfig; diff --git a/src/config/config.local.ts b/src/config/config.local.ts index 0c962ce..a0c01e7 100644 --- a/src/config/config.local.ts +++ b/src/config/config.local.ts @@ -3,26 +3,26 @@ export default { koa: { port: 7001, }, - // typeorm: { - // dataSource: { - // default: { - // host: '13.212.62.127', - // username: 'root', - // password: 'Yoone!@.2025', - // }, - // }, - // }, typeorm: { dataSource: { default: { - host: 'localhost', - port: "23306", + host: '13.212.62.127', username: 'root', - password: '12345678', - database: 'inventory', + password: 'Yoone!@.2025', }, }, }, + // typeorm: { + // dataSource: { + // default: { + // host: 'localhost', + // port: "33306", + // username: 'root', + // password: 'root', + // database: 'inventory', + // }, + // }, + // }, cors: { origin: '*', // 允许所有来源跨域请求 allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], // 允许的 HTTP 方法 @@ -34,15 +34,15 @@ export default { expiresIn: '7d', }, wpSite: [ - { - id: '200', - wpApiUrl: "http://simple.local", - consumerKey: 'ck_11b446d0dfd221853830b782049cf9a17553f886', - consumerSecret: 'cs_2b06729269f659dcef675b8cdff542bf3c1da7e8', - name: 'LocalSimple', - email: '2469687281@qq.com', - emailPswd: 'lulin91.', - }, + // { + // id: '200', + // wpApiUrl: "http://simple.local", + // consumerKey: 'ck_11b446d0dfd221853830b782049cf9a17553f886', + // consumerSecret: 'cs_2b06729269f659dcef675b8cdff542bf3c1da7e8', + // name: 'LocalSimple', + // email: '2469687281@qq.com', + // emailPswd: 'lulin91.', + // }, // { // id: '2', // wpApiUrl: 'http://t2-shop.local/', diff --git a/src/controller/order.controller.ts b/src/controller/order.controller.ts index e5dc3c6..e4ce4f2 100644 --- a/src/controller/order.controller.ts +++ b/src/controller/order.controller.ts @@ -252,4 +252,21 @@ export class OrderController { return errorResponse(error?.message || '获取失败'); } } + + @ApiOkResponse() + @Get('/order/export') + async exportOrder( + @Query() queryParams: any + ) { + // 处理 ids 参数,支持多种格式:ids=1,2,3、ids[]=1&ids[]=2、ids=1 + + try { + const csv = await this.orderService.exportOrder(queryParams?.ids); + // 返回CSV内容给前端,由前端决定是否下载 + return successResponse({ csv }); + } catch (error) { + return errorResponse(error?.message || '导出失败'); + } + } + } diff --git a/src/dto/site.dto.ts b/src/dto/site.dto.ts index bf76f40..8ee2642 100644 --- a/src/dto/site.dto.ts +++ b/src/dto/site.dto.ts @@ -44,7 +44,7 @@ export class CreateSiteDTO { consumerSecret?: string; @Rule(RuleType.string().optional()) token?: string; - @Rule(RuleType.string()) + @Rule(RuleType.string().min(1)) name: string; @Rule(RuleType.string().allow('').optional()) description?: string; @@ -73,7 +73,7 @@ export class UpdateSiteDTO { consumerSecret?: string; @Rule(RuleType.string().optional()) token?: string; - @Rule(RuleType.string().optional()) + @Rule(RuleType.string().min(1).optional()) name?: string; @Rule(RuleType.string().allow('').optional()) description?: string; diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 4305bd8..2ef3f55 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -33,7 +33,9 @@ import { ShipmentItem } from '../entity/shipment_item.entity'; import { UpdateStockDTO } from '../dto/stock.dto'; import { StockService } from './stock.service'; import { OrderItemOriginal } from '../entity/order_item_original.entity'; - +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; @Provide() export class OrderService { @@ -1698,4 +1700,201 @@ export class OrderService { } } + async exportOrder(ids: number[]) { + // 日期 订单号 姓名地址 邮箱 号码 订单内容 盒数 换盒数 换货内容 快递号 + interface ExportData { + '日期': string; + '订单号': string; + '姓名地址': string; + '邮箱': string; + '号码': string; + '订单内容': string; + '盒数': number; + '换盒数': number; + '换货内容': string; + '快递号': string; + } + + try { + // 空值检查 + const dataSource = this.dataSourceManager.getDataSource('default'); + + // 优化事务使用 + return await dataSource.transaction(async manager => { + // 准备查询条件 + const whereCondition: any = {}; + if (ids && ids.length > 0) { + whereCondition.id = In(ids); + } + + // 获取订单、订单项和物流信息 + const orders = await manager.getRepository(Order).find({ + where: whereCondition, + relations: ['shipment'] + }); + + if (orders.length === 0) { + throw new Error('未找到匹配的订单'); + } + + // 获取所有订单ID + const orderIds = orders.map(order => order.id); + + // 获取所有订单项 + const orderItems = await manager.getRepository(OrderItem).find({ + where: { + orderId: In(orderIds) + } + }); + + // 按订单ID分组订单项 + const orderItemsByOrderId = orderItems.reduce((acc, item) => { + if (!acc[item.orderId]) { + acc[item.orderId] = []; + } + acc[item.orderId].push(item); + return acc; + }, {} as Record); + + // 构建导出数据 + const exportDataList: ExportData[] = orders.map(order => { + // 获取订单的订单项 + const items = orderItemsByOrderId[order.id] || []; + + // 计算总盒数 + const boxCount = items.reduce((total, item) => total + item.quantity, 0); + + // 构建订单内容 + const orderContent = items.map(item => `${item.name} (${item.sku || ''}) x ${item.quantity}`).join('; '); + + // 构建姓名地址 + const shipping = order.shipping; + const billing = order.billing; + const firstName = shipping?.first_name || billing?.first_name || ''; + const lastName = shipping?.last_name || billing?.last_name || ''; + const name = `${firstName} ${lastName}`.trim() || ''; + const address = shipping?.address_1 || billing?.address_1 || ''; + const address2 = shipping?.address_2 || billing?.address_2 || ''; + const city = shipping?.city || billing?.city || ''; + const state = shipping?.state || billing?.state || ''; + const postcode = shipping?.postcode || billing?.postcode || ''; + const country = shipping?.country || billing?.country || ''; + const nameAddress = `${name} ${address} ${address2} ${city} ${state} ${postcode} ${country}`; + + // 获取电话号码 + const phone = shipping?.phone || billing?.phone || ''; + + // 获取快递号 + const trackingNumber = order.shipment?.tracking_id || ''; + + // 暂时没有换货相关数据,默认为0和空字符串 + const exchangeBoxCount = 0; + const exchangeContent = ''; + + return { + '日期': order.date_created?.toISOString().split('T')[0] || '', + '订单号': order.externalOrderId || '', + '姓名地址': nameAddress, + '邮箱': order.customer_email || '', + '号码': phone, + '订单内容': orderContent, + '盒数': boxCount, + '换盒数': exchangeBoxCount, + '换货内容': exchangeContent, + '快递号': trackingNumber + }; + }); + + // 返回CSV字符串内容给前端 + const csvContent = await this.exportToCsv(exportDataList, { type: 'string' }); + return csvContent; + }); + } catch (error) { + throw new Error(`导出订单失败:${error.message}`); + } + } + + + /** + * 导出数据为CSV格式 + * @param {any[]} data 数据数组 + * @param {Object} options 配置选项 + * @param {string} [options.type='string'] 输出类型:'string' | 'buffer' + * @param {string} [options.fileName] 文件名(仅当需要写入文件时使用) + * @param {boolean} [options.writeFile=false] 是否写入文件 + * @returns {string|Buffer} 根据type返回字符串或Buffer + */ +async exportToCsv(data: any[], options: { type?: 'string' | 'buffer'; fileName?: string; writeFile?: boolean } = {}): Promise { + try { + // 检查数据是否为空 + if (!data || data.length === 0) { + throw new Error('导出数据不能为空'); + } + + const { type = 'string', fileName, writeFile = false } = options; + + // 生成表头 + const headers = Object.keys(data[0]); + let csvContent = headers.join(',') + '\n'; + + // 处理数据行 + data.forEach(item => { + const row = headers.map(key => { + const value = item[key as keyof any]; + // 处理特殊字符 + if (typeof value === 'string') { + // 转义双引号,将"替换为"" + const escapedValue = value.replace(/"/g, '""'); + // 如果包含逗号或换行符,需要用双引号包裹 + if (escapedValue.includes(',') || escapedValue.includes('\n')) { + return `"${escapedValue}"`; + } + return escapedValue; + } + // 处理日期类型 + if (value instanceof Date) { + return value.toISOString(); + } + // 处理undefined和null + if (value === undefined || value === null) { + return ''; + } + return String(value); + }).join(','); + csvContent += row + '\n'; + }); + + // 如果需要写入文件 + if (writeFile && fileName) { + // 获取当前用户目录 + const userHomeDir = os.homedir(); + + // 构建目标路径(下载目录) + const downloadsDir = path.join(userHomeDir, 'Downloads'); + + // 确保下载目录存在 + if (!fs.existsSync(downloadsDir)) { + fs.mkdirSync(downloadsDir, { recursive: true }); + } + + const filePath = path.join(downloadsDir, fileName); + + // 写入文件 + fs.writeFileSync(filePath, csvContent, 'utf8'); + + console.log(`数据已成功导出至 ${filePath}`); + return filePath; + } + + // 根据类型返回不同结果 + if (type === 'buffer') { + return Buffer.from(csvContent, 'utf8'); + } + + return csvContent; + } catch (error) { + console.error('导出CSV时出错:', error); + throw new Error(`导出CSV文件失败: ${error.message}`); + } +} } diff --git a/src/service/site.service.ts b/src/service/site.service.ts index f7f883d..c30616d 100644 --- a/src/service/site.service.ts +++ b/src/service/site.service.ts @@ -22,6 +22,11 @@ export class SiteService { async syncFromConfig(sites: WpSite[] = []) { // 将配置中的 WpSite 同步到数据库 Site 表(用于一次性导入或初始化) for (const siteConfig of sites) { + // 跳过name为空的站点配置 + if (!siteConfig.name) { + console.warn('跳过空名称的站点配置'); + continue; + } // 按站点名称查询是否已存在记录 const exist = await this.siteModel.findOne({ where: { name: siteConfig.name }, @@ -146,7 +151,7 @@ export class SiteService { return site; } // 默认不返回密钥,进行字段脱敏 - const { consumerKey, consumerSecret, ...rest } = site; + const { ...rest } = site; return rest; }