forked from yoone/API
1
0
Fork 0

feat: 添加订单支付日期字段并支持国家筛选

- 在ShopyyOrder接口中添加date_paid字段
- 在OrderStatisticsParams中添加country数组字段用于国家筛选
- 修改统计服务以支持按国家筛选订单数据
- 更新数据库配置和同步设置
- 优化订单服务中的类型定义和查询条件
This commit is contained in:
zhuotianyuan 2026-01-09 10:55:16 +08:00
parent f54b3c3617
commit 56ed3437c3
7 changed files with 83 additions and 28 deletions

View File

@ -367,7 +367,6 @@ export class ShopyyAdapter implements ISiteAdapter {
date_paid: typeof item.pay_at === 'number' date_paid: typeof item.pay_at === 'number'
? item.pay_at === 0 ? null : new Date(item.pay_at * 1000).toISOString() ? item.pay_at === 0 ? null : new Date(item.pay_at * 1000).toISOString()
: null, : null,
refunds: [], refunds: [],
currency_symbol: (currencySymbols[item.currency] || '$') || '', currency_symbol: (currencySymbols[item.currency] || '$') || '',
date_created: date_created:
@ -387,6 +386,7 @@ export class ShopyyAdapter implements ISiteAdapter {
tracking_number: f.tracking_number || '', tracking_number: f.tracking_number || '',
shipping_provider: f.tracking_company || '', shipping_provider: f.tracking_company || '',
shipping_method: f.tracking_company || '', shipping_method: f.tracking_company || '',
date_created: typeof f.created_at === 'number' date_created: typeof f.created_at === 'number'
? new Date(f.created_at * 1000).toISOString() ? new Date(f.created_at * 1000).toISOString()
: f.created_at || '', : f.created_at || '',

View File

@ -12,14 +12,15 @@ export default {
// }, // },
// }, // },
// }, // },
typeorm: { typeorm: {
dataSource: { dataSource: {
default: { default: {
host: 'localhost', host: '13.212.62.127',
port: "23306", port: "3306",
username: 'root', username: 'root',
password: '12345678', password: 'Yoone!@.2025',
database: 'inventory_v2', database: 'inventory_v2',
synchronize: true,
}, },
}, },
}, },

View File

@ -346,6 +346,7 @@ export interface ShopyyOrder {
financial_status?: number; financial_status?: number;
fulfillment_status?: number; fulfillment_status?: number;
// 创建与更新时间可能为时间戳 // 创建与更新时间可能为时间戳
date_paid?: number | string;
created_at?: number | string; created_at?: number | string;
date_added?: string; date_added?: string;
updated_at?: number | string; updated_at?: number | string;

View File

@ -121,7 +121,7 @@ export class UpdateSiteDTO {
skuPrefix?: string; skuPrefix?: string;
// 区域 // 区域
@ApiProperty({ description: '区域' }) @ApiProperty({ description: '区域', required: false })
@Rule(RuleType.array().items(RuleType.string()).optional()) @Rule(RuleType.array().items(RuleType.string()).optional())
areas?: string[]; areas?: string[];
@ -133,6 +133,10 @@ export class UpdateSiteDTO {
@ApiProperty({ description: '站点网站URL', required: false }) @ApiProperty({ description: '站点网站URL', required: false })
@Rule(RuleType.string().optional()) @Rule(RuleType.string().optional())
websiteUrl?: string; websiteUrl?: string;
@ApiProperty({ description: 'Webhook URL', required: false })
@Rule(RuleType.string().optional())
webhookUrl?: string;
} }
export class QuerySiteDTO { export class QuerySiteDTO {

View File

@ -19,6 +19,10 @@ export class OrderStatisticsParams {
@Rule(RuleType.number().allow(null)) @Rule(RuleType.number().allow(null))
siteId?: number; siteId?: number;
@ApiProperty()
@Rule(RuleType.array().allow(null))
country?: any[];
@ApiProperty({ @ApiProperty({
enum: ['all', 'first_purchase', 'repeat_purchase'], enum: ['all', 'first_purchase', 'repeat_purchase'],
default: 'all', default: 'all',

View File

@ -301,7 +301,7 @@ export class OrderService {
* @param order * @param order
* @param forceUpdate * @param forceUpdate
*/ */
async syncSingleOrder(siteId: number, order: any, forceUpdate = false) { async syncSingleOrder(siteId: number, order: UnifiedOrderDTO, forceUpdate = false) {
// 从订单数据中解构出各个子项 // 从订单数据中解构出各个子项
let { let {
line_items, line_items,
@ -319,7 +319,7 @@ export class OrderService {
} }
// 检查数据库中是否已存在该订单 // 检查数据库中是否已存在该订单
const existingOrder = await this.orderModel.findOne({ const existingOrder = await this.orderModel.findOne({
where: { externalOrderId: order.id, siteId: siteId }, where: { externalOrderId: String(order.id), siteId: siteId },
}); });
// 自动更新订单状态(如果需要) // 自动更新订单状态(如果需要)
await this.autoUpdateOrderStatus(siteId, order); await this.autoUpdateOrderStatus(siteId, order);
@ -328,10 +328,10 @@ export class OrderService {
// 矫正数据库中的订单数据 // 矫正数据库中的订单数据
const updateData: any = { status: order.status }; const updateData: any = { status: order.status };
if (this.canUpdateErpStatus(existingOrder.orderStatus)) { 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 数据 // 更新 fulfillments 数据
await this.saveOrderFulfillments({ await this.saveOrderFulfillments({
siteId, siteId,
@ -340,7 +340,7 @@ export class OrderService {
fulfillments: fulfillments, fulfillments: fulfillments,
}); });
} }
const externalOrderId = order.id; const externalOrderId = String(order.id);
// 如果订单从未完成变为完成状态,则更新库存 // 如果订单从未完成变为完成状态,则更新库存
if ( if (
existingOrder && existingOrder &&
@ -361,14 +361,14 @@ export class OrderService {
await this.saveOrderItems({ await this.saveOrderItems({
siteId, siteId,
orderId, orderId,
externalOrderId, externalOrderId: String(externalOrderId),
orderItems: line_items, orderItems: line_items,
}); });
// 保存退款信息 // 保存退款信息
await this.saveOrderRefunds({ await this.saveOrderRefunds({
siteId, siteId,
orderId, orderId,
externalOrderId, externalOrderId ,
refunds, refunds,
}); });
// 保存费用信息 // 保存费用信息
@ -459,7 +459,7 @@ export class OrderService {
* @param order * @param order
* @returns * @returns
*/ */
async saveOrder(siteId: number, order: UnifiedOrderDTO): Promise<Order> { async saveOrder(siteId: number, order: Partial<UnifiedOrderDTO>): Promise<Order> {
// 将外部订单ID转换为字符串 // 将外部订单ID转换为字符串
const externalOrderId = String(order.id) const externalOrderId = String(order.id)
delete order.id delete order.id
@ -1234,13 +1234,13 @@ export class OrderService {
parameters.push(siteId); parameters.push(siteId);
} }
if (startDate) { if (startDate) {
sqlQuery += ` AND o.date_created >= ?`; sqlQuery += ` AND o.date_paid >= ?`;
totalQuery += ` AND o.date_created >= ?`; totalQuery += ` AND o.date_paid >= ?`;
parameters.push(startDate); parameters.push(startDate);
} }
if (endDate) { if (endDate) {
sqlQuery += ` AND o.date_created <= ?`; sqlQuery += ` AND o.date_paid <= ?`;
totalQuery += ` AND o.date_created <= ?`; totalQuery += ` AND o.date_paid <= ?`;
parameters.push(endDate); parameters.push(endDate);
} }
// 支付方式筛选(使用参数化,避免SQL注入) // 支付方式筛选(使用参数化,避免SQL注入)
@ -1328,7 +1328,7 @@ export class OrderService {
// 添加分页到主查询 // 添加分页到主查询
sqlQuery += ` sqlQuery += `
GROUP BY o.id GROUP BY o.id
ORDER BY o.date_created DESC ORDER BY o.date_paid DESC
LIMIT ? OFFSET ? LIMIT ? OFFSET ?
`; `;
parameters.push(pageSize, (current - 1) * pageSize); parameters.push(pageSize, (current - 1) * pageSize);

View File

@ -15,8 +15,19 @@ export class StatisticsService {
orderItemRepository: Repository<OrderItem>; orderItemRepository: Repository<OrderItem>;
async getOrderStatistics(params: OrderStatisticsParams) { async getOrderStatistics(params: OrderStatisticsParams) {
const { startDate, endDate, grouping, siteId } = params; const { startDate, endDate, grouping, siteId, country } = params;
// const keywords = keyword ? keyword.split(' ').filter(Boolean) : []; // 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 start = dayjs(startDate).format('YYYY-MM-DD');
const end = dayjs(endDate).add(1, 'd').format('YYYY-MM-DD'); const end = dayjs(endDate).add(1, 'd').format('YYYY-MM-DD');
let sql let sql
@ -54,6 +65,8 @@ export class StatisticsService {
AND o.status IN('processing','completed') AND o.status IN('processing','completed')
`; `;
if (siteId) sql += ` AND o.siteId=${siteId}`; if (siteId) sql += ` AND o.siteId=${siteId}`;
if (siteIds.length) sql += ` AND o.siteId IN (${siteIds.join(',')})`;
sql += ` sql += `
GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source 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 LEFT JOIN order_item oi ON o.id = oi.orderId
WHERE o.date_paid IS NOT NULL 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}'
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 GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source
), ),
order_sales_summary AS ( 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 first_order f ON o.customer_email = f.customer_email
LEFT JOIN order_item oi ON o.id = oi.orderId LEFT JOIN order_item oi ON o.id = oi.orderId
WHERE o.date_paid IS NOT NULL 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') 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 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) { async getOrderSorce(params) {
const sql = ` const { country } = params;
let siteIds = []
if (country) {
siteIds = await this.getSiteIds(country)
}
let sql = `
WITH cutoff_months AS ( WITH cutoff_months AS (
SELECT SELECT
DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 7 MONTH), '%Y-%m') AS start_month, 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, DATE_FORMAT(MIN(date_paid), '%Y-%m') AS first_order_month,
SUM(total) AS first_order_total SUM(total) AS first_order_total
FROM \`order\` 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 GROUP BY customer_email
), ),
order_months AS ( order_months AS (
@ -1334,7 +1364,10 @@ export class StatisticsService {
customer_email, customer_email,
DATE_FORMAT(date_paid, '%Y-%m') AS order_month DATE_FORMAT(date_paid, '%Y-%m') AS order_month
FROM \`order\` 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 ( filtered_orders AS (
SELECT o.customer_email, o.order_month, u.first_order_month,u.first_order_total, c.start_month 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 ORDER BY order_month DESC, first_order_month_group
` `
const inactiveSql = ` let inactiveSql = `
WITH WITH
cutoff_months AS ( cutoff_months AS (
SELECT SELECT
@ -1381,7 +1414,10 @@ export class StatisticsService {
date_paid, date_paid,
total total
FROM \`order\` 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 ( 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)
}
} }