feat: 添加订单导出功能并优化配置
添加订单导出为CSV的功能,支持多种格式的ids参数 更新.gitignore忽略json和config文件 添加qs依赖用于解析查询参数 优化站点服务逻辑,跳过空名称站点 更新订单和站点DTO的验证规则 调整本地数据库配置
This commit is contained in:
parent
3f3569995d
commit
2508164395
|
|
@ -18,3 +18,5 @@ container
|
|||
scripts
|
||||
ai
|
||||
tmp_uploads/
|
||||
*.json
|
||||
*config*
|
||||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -152,4 +152,8 @@ export default {
|
|||
tmpdir: join(__dirname, '../../tmp_uploads'),
|
||||
cleanTimeout: 5 * 60 * 1000,
|
||||
},
|
||||
koa: {
|
||||
queryParseMode: 'extended', // 使用 qs 模块
|
||||
// 其他选项:'strict', 'first'
|
||||
},
|
||||
} as MidwayConfig;
|
||||
|
|
|
|||
|
|
@ -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/',
|
||||
|
|
|
|||
|
|
@ -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 || '导出失败');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<number, OrderItem[]>);
|
||||
|
||||
// 构建导出数据
|
||||
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<string | Buffer> {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue