Compare commits
6 Commits
main
...
main-backu
| Author | SHA1 | Date |
|---|---|---|
|
|
8d0eec06fd | |
|
|
78d82ffbac | |
|
|
f1c809bfd2 | |
|
|
ba1c6aafd6 | |
|
|
17435b1381 | |
|
|
c9f9310a29 |
|
|
@ -523,23 +523,6 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@faker-js/faker": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.2.0.tgz",
|
||||
"integrity": "sha512-rTXwAsIxpCqzUnZvrxVh3L0QA0NzToqWBLAhV+zDV3MIIwiQhAZHMdPCIaj5n/yADu/tyk12wIPgL6YHGXJP+g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/fakerjs"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || ^23.5.0 || >=24.0.0",
|
||||
"npm": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@hapi/bourne": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/@hapi/bourne/-/bourne-3.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -21,16 +21,13 @@ import {
|
|||
CreateReviewDTO,
|
||||
CreateVariationDTO,
|
||||
UpdateReviewDTO,
|
||||
OrderPaymentStatus,
|
||||
} from '../dto/site-api.dto';
|
||||
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, ShopyyGetAllOrdersParams } from '../dto/api.dto';
|
||||
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto';
|
||||
import {
|
||||
ShopyyAllProductQuery,
|
||||
ShopyyCustomer,
|
||||
ShopyyOrder,
|
||||
ShopyyOrderCreateParams,
|
||||
ShopyyOrderQuery,
|
||||
ShopyyOrderUpdateParams,
|
||||
ShopyyProduct,
|
||||
ShopyyProductQuery,
|
||||
ShopyyVariant,
|
||||
|
|
@ -40,7 +37,6 @@ import {
|
|||
OrderStatus,
|
||||
} from '../enums/base.enum';
|
||||
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||
import dayjs = require('dayjs');
|
||||
export class ShopyyAdapter implements ISiteAdapter {
|
||||
shopyyFinancialStatusMap= {
|
||||
'200': '待支付',
|
||||
|
|
@ -231,11 +227,9 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
|
||||
// ========== 订单映射方法 ==========
|
||||
mapPlatformToUnifiedOrder(item: ShopyyOrder): UnifiedOrderDTO {
|
||||
// console.log(item)
|
||||
if (!item) throw new Error('订单数据不能为空')
|
||||
// 提取账单和送货地址 如果不存在则为空对象
|
||||
const billing = item.billing_address || {};
|
||||
const shipping = item.shipping_address || {};
|
||||
const billing = (item as any).billing_address || {};
|
||||
const shipping = (item as any).shipping_address || {};
|
||||
|
||||
// 构建账单地址对象
|
||||
const billingObj: UnifiedAddressDTO = {
|
||||
|
|
@ -244,7 +238,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
fullname: billing.name || `${item.firstname} ${item.lastname}`.trim(),
|
||||
company: billing.company || '',
|
||||
email: item.customer_email || item.email || '',
|
||||
phone: billing.phone || item.telephone || '',
|
||||
phone: billing.phone || (item as any).telephone || '',
|
||||
address_1: billing.address1 || item.payment_address || '',
|
||||
address_2: billing.address2 || '',
|
||||
city: billing.city || item.payment_city || '',
|
||||
|
|
@ -275,7 +269,6 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
state: shipping.province || item.shipping_zone || '',
|
||||
postcode: shipping.zip || item.shipping_postcode || '',
|
||||
method_title: item.payment_method || '',
|
||||
phone: shipping.phone || item.telephone || '',
|
||||
country:
|
||||
shipping.country_name ||
|
||||
shipping.country_code ||
|
||||
|
|
@ -314,14 +307,14 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
};
|
||||
|
||||
const lineItems: UnifiedOrderLineItemDTO[] = (item.products || []).map(
|
||||
(product) => ({
|
||||
id: product.id,
|
||||
name:product.sku_value?.[0]?.value || product.product_title || product.name,
|
||||
product_id: product.product_id,
|
||||
quantity: product.quantity,
|
||||
total: String(product.price ?? ''),
|
||||
sku: product.sku || product.sku_code || '',
|
||||
price: String(product.price ?? ''),
|
||||
(p: any) => ({
|
||||
id: p.id,
|
||||
name: p.product_title || p.name,
|
||||
product_id: p.product_id,
|
||||
quantity: p.quantity,
|
||||
total: String(p.price ?? ''),
|
||||
sku: p.sku || p.sku_code || '',
|
||||
price: String(p.price ?? ''),
|
||||
})
|
||||
);
|
||||
// 货币符号
|
||||
|
|
@ -344,7 +337,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
const status = this.shopyyOrderStatusMap[item.status ?? item.order_status] || OrderStatus.PENDING;
|
||||
const finalcial_status = this.shopyyFinancialStatusMap[item.financial_status]
|
||||
// 发货状态
|
||||
const fulfillment_status = this.fulfillmentStatusMap[item.fulfillment_status];
|
||||
const fulfillment_status = this.shopyyFulfillmentStatusMap[item.fulfillment_status];
|
||||
return {
|
||||
id: item.id || item.order_id,
|
||||
number: item.order_number || item.order_sn,
|
||||
|
|
@ -374,6 +367,7 @@ 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:
|
||||
|
|
@ -393,7 +387,6 @@ 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 || '',
|
||||
|
|
@ -407,11 +400,11 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
return data
|
||||
}
|
||||
|
||||
mapCreateOrderParams(data: Partial<UnifiedOrderDTO>): ShopyyOrderCreateParams {
|
||||
mapCreateOrderParams(data: Partial<UnifiedOrderDTO>): any {
|
||||
return data
|
||||
}
|
||||
|
||||
mapUpdateOrderParams(data: Partial<UnifiedOrderDTO>): ShopyyOrderUpdateParams {
|
||||
mapUpdateOrderParams(data: Partial<UnifiedOrderDTO>): any {
|
||||
// 构建 ShopYY 订单更新参数(仅包含传入的字段)
|
||||
const params: any = {};
|
||||
|
||||
|
|
@ -447,7 +440,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
}
|
||||
|
||||
// 更新账单地址
|
||||
params.billing_address = params?.billing_address || {};
|
||||
params.billing_address = params.billing_address || {};
|
||||
if (data.billing.first_name !== undefined) {
|
||||
params.billing_address.first_name = data.billing.first_name;
|
||||
}
|
||||
|
|
@ -542,16 +535,8 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
}
|
||||
|
||||
async getOrder(where: {id: string | number}): Promise<UnifiedOrderDTO> {
|
||||
const data = await this.getOrders({
|
||||
where: {
|
||||
id: where.id,
|
||||
},
|
||||
page: 1,
|
||||
per_page: 1,
|
||||
})
|
||||
return data.items[0] || null
|
||||
// const data = await this.shopyyService.getOrder(this.site.id, String(where.id));
|
||||
// return this.mapPlatformToUnifiedOrder(data);
|
||||
const data = await this.shopyyService.getOrder(this.site.id, String(where.id));
|
||||
return this.mapPlatformToUnifiedOrder(data);
|
||||
}
|
||||
|
||||
async getOrders(
|
||||
|
|
@ -572,21 +557,9 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
per_page,
|
||||
};
|
||||
}
|
||||
mapGetAllOrdersParams(params: UnifiedSearchParamsDTO) :ShopyyGetAllOrdersParams{
|
||||
|
||||
const pay_at_min = dayjs(params.after || '').unix().toString();
|
||||
const pay_at_max = dayjs(params.before || '').unix().toString();
|
||||
|
||||
return {
|
||||
per_page: params.per_page || 100,
|
||||
pay_at_min: pay_at_min,
|
||||
pay_at_max: pay_at_max,
|
||||
order_field: 'pay_at',
|
||||
}
|
||||
}
|
||||
async getAllOrders(params?: UnifiedSearchParamsDTO): Promise<UnifiedOrderDTO[]> {
|
||||
const normalizedParams = this.mapGetAllOrdersParams(params);
|
||||
const data = await this.shopyyService.getAllOrders(this.site.id, normalizedParams);
|
||||
const data = await this.shopyyService.getAllOrders(this.site.id, params);
|
||||
return data.map(this.mapPlatformToUnifiedOrder.bind(this));
|
||||
}
|
||||
|
||||
|
|
@ -724,7 +697,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
name: item.name || item.title,
|
||||
type: String(item.product_type ?? ''),
|
||||
status: mapProductStatus(item.status),
|
||||
sku: item.variant?.sku || item.variant?.sku_code || '',
|
||||
sku: item.variant?.sku || '',
|
||||
regular_price: String(item.variant?.price ?? ''),
|
||||
sale_price: String(item.special_price ?? ''),
|
||||
price: String(item.price ?? ''),
|
||||
|
|
@ -1126,11 +1099,10 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
// ========== 产品变体映射方法 ==========
|
||||
mapPlatformToUnifiedVariation(variant: ShopyyVariant): UnifiedProductVariationDTO {
|
||||
// 映射变体
|
||||
console.log('ivarianttem', variant)
|
||||
return {
|
||||
id: variant.id,
|
||||
name: variant.title || '',
|
||||
sku: variant.sku || variant.sku_code || '',
|
||||
name: variant.sku || '',
|
||||
sku: variant.sku || '',
|
||||
regular_price: String(variant.price ?? ''),
|
||||
sale_price: String(variant.special_price ?? ''),
|
||||
price: String(variant.price ?? ''),
|
||||
|
|
@ -1328,8 +1300,8 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
[180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed
|
||||
[190]: OrderStatus.CANCEL // 190 取消 转为 cancelled
|
||||
}
|
||||
// 物流状态 300 未发货;310 部分发货;320 已发货;330(确认收货)
|
||||
fulfillmentStatusMap = {
|
||||
|
||||
shopyyFulfillmentStatusMap = {
|
||||
// 未发货
|
||||
'300': OrderFulfillmentStatus.PENDING,
|
||||
// 部分发货
|
||||
|
|
@ -1340,23 +1312,4 @@ export class ShopyyAdapter implements ISiteAdapter {
|
|||
'330': OrderFulfillmentStatus.CANCELLED,
|
||||
// 确认发货
|
||||
}
|
||||
// 支付状态 200 待支付;210 支付中;220 部分支付;230 已支付;240 支付失败;250 部分退款;260 已退款 ;290 已取消;
|
||||
financialStatusMap = {
|
||||
// 待支付
|
||||
'200': OrderPaymentStatus.PENDING,
|
||||
// 支付中
|
||||
'210': OrderPaymentStatus.PAYING,
|
||||
// 部分支付
|
||||
'220': OrderPaymentStatus.PARTIALLY_PAID,
|
||||
// 已支付
|
||||
'230': OrderPaymentStatus.PAID,
|
||||
// 支付失败
|
||||
'240': OrderPaymentStatus.FAILED,
|
||||
// 部分退款
|
||||
'250': OrderPaymentStatus.PARTIALLY_REFUNDED,
|
||||
// 已退款
|
||||
'260': OrderPaymentStatus.REFUNDED,
|
||||
// 已取消
|
||||
'290': OrderPaymentStatus.CANCELLED,
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,6 @@ import {
|
|||
UnifiedVariationPaginationDTO,
|
||||
CreateReviewDTO,
|
||||
UpdateReviewDTO,
|
||||
FulfillmentDTO,
|
||||
} from '../dto/site-api.dto';
|
||||
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto';
|
||||
import {
|
||||
|
|
@ -29,13 +28,10 @@ import {
|
|||
WooWebhook,
|
||||
WooOrderSearchParams,
|
||||
WooProductSearchParams,
|
||||
WpMediaGetListParams,
|
||||
WooFulfillment,
|
||||
} from '../dto/woocommerce.dto';
|
||||
import { Site } from '../entity/site.entity';
|
||||
import { WPService } from '../service/wp.service';
|
||||
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||
import { toArray, toNumber } from '../utils/trans.util';
|
||||
|
||||
export class WooCommerceAdapter implements ISiteAdapter {
|
||||
// 构造函数接收站点配置与服务实例
|
||||
|
|
@ -253,25 +249,13 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
date_modified: item.date_modified ?? item.modified,
|
||||
};
|
||||
}
|
||||
mapMediaSearchParams(params: UnifiedSearchParamsDTO): Partial<WpMediaGetListParams> {
|
||||
const page = params.page
|
||||
const per_page = Number( params.per_page ?? 20);
|
||||
|
||||
return {
|
||||
...params.where,
|
||||
page,
|
||||
per_page,
|
||||
// orderby,
|
||||
// order,
|
||||
};
|
||||
}
|
||||
|
||||
// 媒体操作方法
|
||||
async getMedia(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedMediaDTO>> {
|
||||
// 获取媒体列表并映射为统一媒体DTO集合
|
||||
const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged(
|
||||
this.site,
|
||||
this.mapMediaSearchParams(params)
|
||||
params
|
||||
);
|
||||
return {
|
||||
items: items.map(this.mapPlatformToUnifiedMedia.bind(this)),
|
||||
|
|
@ -333,11 +317,22 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
// }
|
||||
const mapped: any = {
|
||||
...(params.search ? { search: params.search } : {}),
|
||||
// ...(orderBy ? { orderBy } : {}),
|
||||
page,
|
||||
per_page,
|
||||
};
|
||||
|
||||
const toArray = (value: any): any[] => {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (value === undefined || value === null) return [];
|
||||
return String(value).split(',').map(v => v.trim()).filter(Boolean);
|
||||
};
|
||||
|
||||
const toNumber = (value: any): number | undefined => {
|
||||
if (value === undefined || value === null || value === '') return undefined;
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
};
|
||||
|
||||
// 时间过滤参数
|
||||
if (where.after ?? where.date_created_after ?? where.created_after) mapped.after = String(where.after ?? where.date_created_after ?? where.created_after);
|
||||
|
|
@ -348,7 +343,8 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
|
||||
// 集合过滤参数
|
||||
if (where.exclude) mapped.exclude = toArray(where.exclude);
|
||||
if (where.ids || where.number || where.id || where.include) mapped.include = [...new Set([where.number,where.id,...toArray(where.ids),...toArray(where.include)])].filter(Boolean);
|
||||
if (where.include) mapped.include = toArray(where.include);
|
||||
if (where.ids) mapped.include = toArray(where.ids);
|
||||
if (toNumber(where.offset) !== undefined) mapped.offset = Number(where.offset);
|
||||
if (where.parent ?? where.parentId) mapped.parent = toArray(where.parent ?? where.parentId);
|
||||
if (where.parent_exclude ?? where.parentExclude) mapped.parent_exclude = toArray(where.parent_exclude ?? where.parentExclude);
|
||||
|
|
@ -399,11 +395,13 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
// 包含账单地址与收货地址以及创建与更新时间
|
||||
|
||||
// 映射物流追踪信息,将后端格式转换为前端期望的格式
|
||||
const fulfillments = (item.fulfillments || []).map((track) => ({
|
||||
tracking_id: track.tracking_id,
|
||||
tracking_number: track.tracking_number,
|
||||
shipping_provider: track.tracking_provider,
|
||||
date_created: track.data_sipped,
|
||||
const fulfillments = (item.fulfillments || []).map((track: any) => ({
|
||||
tracking_number: track.tracking_number || '',
|
||||
shipping_provider: track.shipping_provider || '',
|
||||
shipping_method: track.shipping_method || '',
|
||||
status: track.status || '',
|
||||
date_created: track.date_created || '',
|
||||
items: track.items || [],
|
||||
}));
|
||||
|
||||
return {
|
||||
|
|
@ -530,25 +528,54 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
|||
return await this.wpService.getFulfillments(this.site, String(orderId));
|
||||
}
|
||||
|
||||
async createOrderFulfillment(orderId: string | number, data: FulfillmentDTO): Promise<any> {
|
||||
const shipmentData: Partial<WooFulfillment> = {
|
||||
tracking_provider: data.shipping_provider,
|
||||
async createOrderFulfillment(orderId: string | number, data: {
|
||||
tracking_number: string;
|
||||
shipping_provider: string;
|
||||
shipping_method?: string;
|
||||
status?: string;
|
||||
date_created?: string;
|
||||
items?: Array<{
|
||||
order_item_id: number;
|
||||
quantity: number;
|
||||
}>;
|
||||
}): Promise<any> {
|
||||
const shipmentData: any = {
|
||||
shipping_provider: data.shipping_provider,
|
||||
tracking_number: data.tracking_number,
|
||||
data_sipped: data.date_created,
|
||||
// items: data.items,
|
||||
};
|
||||
|
||||
if (data.shipping_method) {
|
||||
shipmentData.shipping_method = data.shipping_method;
|
||||
}
|
||||
|
||||
if (data.status) {
|
||||
shipmentData.status = data.status;
|
||||
}
|
||||
|
||||
if (data.date_created) {
|
||||
shipmentData.date_created = data.date_created;
|
||||
}
|
||||
|
||||
if (data.items) {
|
||||
shipmentData.items = data.items;
|
||||
}
|
||||
|
||||
const response = await this.wpService.createFulfillment(this.site, String(orderId), shipmentData);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: FulfillmentDTO): Promise<any> {
|
||||
const shipmentData: Partial<WooFulfillment> = {
|
||||
tracking_provider: data.shipping_provider,
|
||||
tracking_number: data.tracking_number,
|
||||
data_sipped: data.date_created,
|
||||
// items: data.items,
|
||||
}
|
||||
return await this.wpService.updateFulfillment(this.site, String(orderId), fulfillmentId, shipmentData);
|
||||
async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: {
|
||||
tracking_number?: string;
|
||||
shipping_provider?: string;
|
||||
shipping_method?: string;
|
||||
status?: string;
|
||||
date_created?: string;
|
||||
items?: Array<{
|
||||
order_item_id: number;
|
||||
quantity: number;
|
||||
}>;
|
||||
}): Promise<any> {
|
||||
return await this.wpService.updateFulfillment(this.site, String(orderId), fulfillmentId, data);
|
||||
}
|
||||
|
||||
async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise<boolean> {
|
||||
|
|
|
|||
|
|
@ -7,25 +7,19 @@ export default {
|
|||
// dataSource: {
|
||||
// default: {
|
||||
// host: '13.212.62.127',
|
||||
// port: '3306',
|
||||
// username: 'root',
|
||||
// password: 'Yoone!@.2025',
|
||||
// database: 'inventory_v2',
|
||||
// synchronize: true,
|
||||
// logging: true,
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
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,
|
||||
logging: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -118,7 +118,8 @@ export class MainConfiguration {
|
|||
});
|
||||
|
||||
try {
|
||||
this.logger.info(`正在检查数据库是否存在...`+ JSON.stringify(typeormConfig));
|
||||
this.logger.info('正在检查数据库是否存在...');
|
||||
|
||||
// 初始化临时数据源
|
||||
await tempDataSource.initialize();
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export class DictController {
|
|||
// 从上传的文件列表中获取第一个文件
|
||||
const file = files[0];
|
||||
// 调用服务层方法处理XLSX文件
|
||||
const result = await this.dictService.importDictsFromTable(file.data);
|
||||
const result = await this.dictService.importDictsFromXLSX(file.data);
|
||||
// 返回导入结果
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,31 +79,6 @@ export class ProductController {
|
|||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({
|
||||
description: '成功返回分组后的产品列表',
|
||||
schema: {
|
||||
type: 'object',
|
||||
additionalProperties: {
|
||||
type: 'array',
|
||||
items: {
|
||||
$ref: '#/components/schemas/Product',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@Get('/list/grouped')
|
||||
async getProductListGrouped(
|
||||
@Query() query: UnifiedSearchParamsDTO<ProductWhereFilter>
|
||||
): Promise<any> {
|
||||
try {
|
||||
const data = await this.productService.getProductListGrouped(query);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
this.logger.error('获取分组产品列表失败', error);
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
@ApiOkResponse({ type: ProductRes })
|
||||
@Post('/')
|
||||
async createProduct(@Body() productData: CreateProductDTO) {
|
||||
|
|
@ -142,7 +117,7 @@ export class ProductController {
|
|||
const file = files?.[0];
|
||||
if (!file) return errorResponse('未接收到上传文件');
|
||||
|
||||
const result = await this.productService.importProductsFromTable(file);
|
||||
const result = await this.productService.importProductsCSV(file);
|
||||
return successResponse(result);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
|
|
@ -775,31 +750,4 @@ export class ProductController {
|
|||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有产品,支持按品牌过滤
|
||||
@ApiOkResponse({ description: '获取所有产品', type: ProductListRes })
|
||||
@Get('/all')
|
||||
async getAllProducts(@Query('brand') brand?: string) {
|
||||
try {
|
||||
const data = await this.productService.getAllProducts(brand);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取按属性分组的产品,默认按强度划分
|
||||
@ApiOkResponse({ description: '获取按属性分组的产品' })
|
||||
@Get('/grouped')
|
||||
async getGroupedProducts(
|
||||
@Query('brand') brand?: string,
|
||||
@Query('attribute') attribute: string = 'strength'
|
||||
) {
|
||||
try {
|
||||
const data = await this.productService.getProductsGroupedByAttribute(brand, attribute);
|
||||
return successResponse(data);
|
||||
} catch (error) {
|
||||
return errorResponse(error?.message || error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export class StatisticsController {
|
|||
|
||||
@ApiOkResponse()
|
||||
@Get('/orderSource')
|
||||
async getOrderSource(@Query() params) {
|
||||
async getOrderSorce(@Query() params) {
|
||||
try {
|
||||
return successResponse(await this.statisticsService.getOrderSorce(params));
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ 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';
|
||||
|
|
@ -184,10 +182,15 @@ export class WebhookController {
|
|||
success: true,
|
||||
message: 'Webhook processed successfully',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
code: 403,
|
||||
success: false,
|
||||
message: 'Webhook verification failed',
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,30 +50,6 @@ export class UnifiedSearchParamsDTO<Where=Record<string, any>> {
|
|||
required: false,
|
||||
})
|
||||
orderBy?: Record<string, 'asc' | 'desc'> | string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '分组字段,例如 "categoryId"',
|
||||
type: 'string',
|
||||
required: false,
|
||||
})
|
||||
groupBy?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shopyy获取所有订单参数DTO
|
||||
*/
|
||||
export class ShopyyGetAllOrdersParams {
|
||||
@ApiProperty({ description: '每页数量', example: 100, required: false })
|
||||
per_page?: number;
|
||||
|
||||
@ApiProperty({ description: '支付时间范围开始', example: '2023-01-01T00:00:00Z', required: false })
|
||||
pay_at_min?: string;
|
||||
|
||||
@ApiProperty({ description: '支付时间范围结束', example: '2023-01-01T23:59:59Z', required: false })
|
||||
pay_at_max?: string;
|
||||
|
||||
@ApiProperty({ description: '排序字段', example: 'id', required: false })
|
||||
order_field?: string;//排序字段(默认id) id=订单ID updated_at=最后更新时间 pay_at=支付时间
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -19,22 +19,15 @@ export class ShipmentBookDTO {
|
|||
@ApiProperty({ type: 'number', isArray: true })
|
||||
@Rule(RuleType.array<number>().default([]))
|
||||
orderIds?: number[];
|
||||
|
||||
@ApiProperty()
|
||||
@Rule(RuleType.string())
|
||||
shipmentPlatform: string;
|
||||
}
|
||||
|
||||
export class ShipmentFeeBookDTO {
|
||||
|
||||
@ApiProperty()
|
||||
shipmentPlatform: string;
|
||||
@ApiProperty()
|
||||
stockPointId: number;
|
||||
@ApiProperty()
|
||||
sender: string;
|
||||
@ApiProperty()
|
||||
startPhone: string|any;
|
||||
startPhone: string;
|
||||
@ApiProperty()
|
||||
startPostalCode: string;
|
||||
@ApiProperty()
|
||||
|
|
@ -70,8 +63,6 @@ export class ShipmentFeeBookDTO {
|
|||
weight: number;
|
||||
@ApiProperty()
|
||||
weightUom: string;
|
||||
@ApiProperty()
|
||||
address_id: number;
|
||||
}
|
||||
|
||||
export class PaymentMethodDTO {
|
||||
|
|
|
|||
|
|
@ -59,10 +59,6 @@ export class CreateProductDTO {
|
|||
@Rule(RuleType.number())
|
||||
categoryId?: number;
|
||||
|
||||
@ApiProperty({ description: '分类名称', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
categoryName?: string;
|
||||
|
||||
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
|
||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||
siteSkus?: string[];
|
||||
|
|
@ -90,10 +86,7 @@ export class CreateProductDTO {
|
|||
@Rule(RuleType.number())
|
||||
promotionPrice?: number;
|
||||
|
||||
// 产品图片URL
|
||||
@ApiProperty({ description: '产品图片URL', example: 'https://example.com/image.jpg', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
image?: string;
|
||||
|
||||
|
||||
// 商品类型(默认 single; bundle 需手动设置组成)
|
||||
@ApiProperty({ description: '商品类型', enum: ['single', 'bundle'], default: 'single', required: false })
|
||||
|
|
@ -146,10 +139,6 @@ export class UpdateProductDTO {
|
|||
@Rule(RuleType.number())
|
||||
categoryId?: number;
|
||||
|
||||
@ApiProperty({ description: '分类名称', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
categoryName?: string;
|
||||
|
||||
@ApiProperty({ description: '站点 SKU 列表', type: 'array', required: false })
|
||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||
siteSkus?: string[];
|
||||
|
|
@ -164,10 +153,7 @@ export class UpdateProductDTO {
|
|||
@Rule(RuleType.number())
|
||||
promotionPrice?: number;
|
||||
|
||||
// 产品图片URL
|
||||
@ApiProperty({ description: '产品图片URL', example: 'https://example.com/image.jpg', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
image?: string;
|
||||
|
||||
|
||||
// 属性更新(可选, 支持增量替换指定字典的属性项)
|
||||
@ApiProperty({ description: '属性列表', type: 'array', required: false })
|
||||
|
|
@ -242,10 +228,6 @@ export class BatchUpdateProductDTO {
|
|||
@Rule(RuleType.number().optional())
|
||||
promotionPrice?: number;
|
||||
|
||||
@ApiProperty({ description: '产品图片URL', example: 'https://example.com/image.jpg', required: false })
|
||||
@Rule(RuleType.string().optional())
|
||||
image?: string;
|
||||
|
||||
@ApiProperty({ description: '属性列表', type: 'array', required: false })
|
||||
@Rule(RuleType.array().optional())
|
||||
attributes?: AttributeInputDTO[];
|
||||
|
|
@ -319,8 +301,6 @@ export interface ProductWhereFilter {
|
|||
updatedAtStart?: string;
|
||||
// 更新时间范围结束
|
||||
updatedAtEnd?: string;
|
||||
// TODO 使用 attributes 过滤
|
||||
attributes?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -3,7 +3,6 @@ import {
|
|||
UnifiedPaginationDTO,
|
||||
} from './api.dto';
|
||||
import { Dict } from '../entity/dict.entity';
|
||||
import { Product } from '../entity/product.entity';
|
||||
// export class UnifiedOrderWhere{
|
||||
// []
|
||||
// }
|
||||
|
|
@ -19,24 +18,6 @@ export enum OrderFulfillmentStatus {
|
|||
// 确认发货
|
||||
CONFIRMED,
|
||||
}
|
||||
export enum OrderPaymentStatus {
|
||||
// 待支付
|
||||
PENDING,
|
||||
// 支付中
|
||||
PAYING,
|
||||
// 部分支付
|
||||
PARTIALLY_PAID,
|
||||
// 已支付
|
||||
PAID,
|
||||
// 支付失败
|
||||
FAILED,
|
||||
// 部分退款
|
||||
PARTIALLY_REFUNDED,
|
||||
// 已退款
|
||||
REFUNDED,
|
||||
// 已取消
|
||||
CANCELLED,
|
||||
}
|
||||
//
|
||||
export class UnifiedProductWhere {
|
||||
sku?: string;
|
||||
|
|
@ -307,7 +288,17 @@ export class UnifiedProductDTO {
|
|||
type: 'object',
|
||||
required: false,
|
||||
})
|
||||
erpProduct?: Product
|
||||
erpProduct?: {
|
||||
id: number;
|
||||
sku: string;
|
||||
name: string;
|
||||
nameCn?: string;
|
||||
category?: any;
|
||||
attributes?: any[];
|
||||
components?: any[];
|
||||
price: number;
|
||||
promotionPrice: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class UnifiedOrderRefundDTO {
|
||||
|
|
@ -799,16 +790,14 @@ export class UpdateWebhookDTO {
|
|||
|
||||
|
||||
export class FulfillmentItemDTO {
|
||||
@ApiProperty({ description: '订单项ID' ,required: false})
|
||||
@ApiProperty({ description: '订单项ID' })
|
||||
order_item_id: number;
|
||||
|
||||
@ApiProperty({ description: '数量' ,required:false})
|
||||
@ApiProperty({ description: '数量' })
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export class FulfillmentDTO {
|
||||
@ApiProperty({ description: '物流id', required: false })
|
||||
tracking_id?: string;
|
||||
@ApiProperty({ description: '物流单号', required: false })
|
||||
tracking_number?: string;
|
||||
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ export class UpdateSiteDTO {
|
|||
skuPrefix?: string;
|
||||
|
||||
// 区域
|
||||
@ApiProperty({ description: '区域', required: false })
|
||||
@ApiProperty({ description: '区域' })
|
||||
@Rule(RuleType.array().items(RuleType.string()).optional())
|
||||
areas?: string[];
|
||||
|
||||
|
|
@ -133,10 +133,6 @@ 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 {
|
||||
|
|
|
|||
|
|
@ -19,10 +19,6 @@ 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',
|
||||
|
|
|
|||
|
|
@ -370,24 +370,17 @@ export interface WooOrder {
|
|||
date_modified?: string;
|
||||
date_modified_gmt?: string;
|
||||
// 物流追踪信息
|
||||
fulfillments?: WooFulfillment[];
|
||||
}
|
||||
// 这个是一个插件的物流追踪信息
|
||||
// 接口:
|
||||
export interface WooFulfillment {
|
||||
data_sipped: string;
|
||||
tracking_id: string;
|
||||
tracking_link: string;
|
||||
tracking_number: string;
|
||||
tracking_provider: string;
|
||||
}
|
||||
// https://docs.zorem.com/docs/ast-free/developers/adding-tracking-info-to-orders/
|
||||
export interface WooFulfillmentCreateParams {
|
||||
order_id: string;
|
||||
tracking_provider: string;
|
||||
tracking_number: string;
|
||||
date_shipped?: string;
|
||||
status_shipped?: string;
|
||||
fulfillments?: Array<{
|
||||
tracking_number?: string;
|
||||
shipping_provider?: string;
|
||||
shipping_method?: string;
|
||||
status?: string;
|
||||
date_created?: string;
|
||||
items?: Array<{
|
||||
order_item_id?: number;
|
||||
quantity?: number;
|
||||
}>;
|
||||
}>;
|
||||
}
|
||||
export interface WooOrderRefund {
|
||||
id?: number;
|
||||
|
|
@ -559,8 +552,7 @@ export interface WooOrderSearchParams {
|
|||
order: string;
|
||||
orderby: string;
|
||||
parant: string[];
|
||||
parent_exclude: string[];
|
||||
status: WooOrderStatusSearchParams[];
|
||||
status: (WooOrderStatusSearchParams)[];
|
||||
customer: number;
|
||||
product: number;
|
||||
dp: number;
|
||||
|
|
@ -624,83 +616,6 @@ export interface ListParams {
|
|||
parant: string[];
|
||||
parent_exclude: string[];
|
||||
}
|
||||
export interface WpMediaGetListParams {
|
||||
// 请求范围,决定响应中包含的字段
|
||||
// 默认: view
|
||||
// 可选值: view, embed, edit
|
||||
context?: 'view' | 'embed' | 'edit';
|
||||
|
||||
// 当前页码
|
||||
// 默认: 1
|
||||
page?: number;
|
||||
|
||||
// 每页最大返回数量
|
||||
// 默认: 10
|
||||
per_page?: number;
|
||||
|
||||
// 搜索字符串,限制结果匹配
|
||||
search?: string;
|
||||
|
||||
// ISO8601格式日期,限制发布时间之后的结果
|
||||
after?: string;
|
||||
|
||||
// ISO8601格式日期,限制修改时间之后的结果
|
||||
modified_after?: string;
|
||||
|
||||
// 作者ID数组,限制结果集为特定作者
|
||||
author?: number[];
|
||||
|
||||
// 作者ID数组,排除特定作者的结果
|
||||
author_exclude?: number[];
|
||||
|
||||
// ISO8601格式日期,限制发布时间之前的结果
|
||||
before?: string;
|
||||
|
||||
// ISO8601格式日期,限制修改时间之前的结果
|
||||
modified_before?: string;
|
||||
|
||||
// ID数组,排除特定ID的结果
|
||||
exclude?: number[];
|
||||
|
||||
// ID数组,限制结果集为特定ID
|
||||
include?: number[];
|
||||
|
||||
// 结果集偏移量
|
||||
offset?: number;
|
||||
|
||||
// 排序方向
|
||||
// 默认: desc
|
||||
// 可选值: asc, desc
|
||||
order?: 'asc' | 'desc';
|
||||
|
||||
// 排序字段
|
||||
// 默认: date
|
||||
// 可选值: author, date, id, include, modified, parent, relevance, slug, include_slugs, title
|
||||
orderby?: 'author' | 'date' | 'id' | 'include' | 'modified' | 'parent' | 'relevance' | 'slug' | 'include_slugs' | 'title';
|
||||
|
||||
// 父ID数组,限制结果集为特定父ID
|
||||
parent?: number[];
|
||||
|
||||
// 父ID数组,排除特定父ID的结果
|
||||
parent_exclude?: number[];
|
||||
|
||||
// 搜索的列名数组
|
||||
search_columns?: string[];
|
||||
|
||||
// slug数组,限制结果集为特定slug
|
||||
slug?: string[];
|
||||
|
||||
// 状态数组,限制结果集为特定状态
|
||||
// 默认: inherit
|
||||
status?: string[];
|
||||
|
||||
// 媒体类型,限制结果集为特定媒体类型
|
||||
// 可选值: image, video, text, application, audio
|
||||
media_type?: 'image' | 'video' | 'text' | 'application' | 'audio';
|
||||
|
||||
// MIME类型,限制结果集为特定MIME类型
|
||||
mime_type?: string;
|
||||
}
|
||||
export enum WooContext {
|
||||
view,
|
||||
edit
|
||||
|
|
|
|||
|
|
@ -22,27 +22,22 @@ export class OrderSale {
|
|||
@Expose()
|
||||
id?: number;
|
||||
|
||||
@ApiProperty({ name:'原始订单ID' })
|
||||
@ApiProperty()
|
||||
@Column()
|
||||
@Expose()
|
||||
orderId: number; // 订单 ID
|
||||
|
||||
@ApiProperty({ name:'站点' })
|
||||
@Column()
|
||||
@ApiProperty()
|
||||
@Column({ nullable: true })
|
||||
@Expose()
|
||||
siteId: number; // 来源站点唯一标识
|
||||
|
||||
@ApiProperty({name: "原始订单 itemId"})
|
||||
@ApiProperty()
|
||||
@Column({ nullable: true })
|
||||
@Expose()
|
||||
externalOrderItemId: string; // WooCommerce 订单item ID
|
||||
|
||||
@ApiProperty({name: "父产品 ID"})
|
||||
@Column({ nullable: true })
|
||||
@Expose()
|
||||
parentProductId?: number; // 父产品 ID 用于统计套餐 如果是单品则不记录
|
||||
|
||||
@ApiProperty({name: "产品 ID"})
|
||||
@ApiProperty()
|
||||
@Column()
|
||||
@Expose()
|
||||
productId: number;
|
||||
|
|
@ -55,7 +50,7 @@ export class OrderSale {
|
|||
@ApiProperty({ description: 'sku', type: 'string' })
|
||||
@Expose()
|
||||
@Column()
|
||||
sku: string;// 库存产品sku
|
||||
sku: string;
|
||||
|
||||
@ApiProperty()
|
||||
@Column()
|
||||
|
|
@ -67,40 +62,25 @@ export class OrderSale {
|
|||
@Expose()
|
||||
isPackage: boolean;
|
||||
|
||||
@ApiProperty({ description: '商品品类', type: 'string',nullable: true})
|
||||
@ApiProperty()
|
||||
@Column({ default: false })
|
||||
@Expose()
|
||||
@Column({ nullable: true })
|
||||
category?: string;
|
||||
// TODO 这个其实还是直接保存 product 比较好
|
||||
@ApiProperty({ description: '品牌', type: 'string',nullable: true})
|
||||
@Expose()
|
||||
@Column({ nullable: true })
|
||||
brand?: string;
|
||||
isYoone: boolean;
|
||||
|
||||
@ApiProperty({ description: '口味', type: 'string', nullable: true })
|
||||
@ApiProperty()
|
||||
@Column({ default: false })
|
||||
@Expose()
|
||||
@Column({ nullable: true })
|
||||
flavor?: string;
|
||||
isZex: boolean;
|
||||
|
||||
@ApiProperty({ description: '湿度', type: 'string', nullable: true })
|
||||
@ApiProperty({ nullable: true })
|
||||
@Column({ type: 'int', nullable: true })
|
||||
@Expose()
|
||||
@Column({ nullable: true })
|
||||
humidity?: string;
|
||||
size: number | null; // 其实是 strength
|
||||
|
||||
@ApiProperty({ description: '尺寸', type: 'string', nullable: true })
|
||||
@ApiProperty()
|
||||
@Column({ default: false })
|
||||
@Expose()
|
||||
@Column({ nullable: true })
|
||||
size?: string;
|
||||
|
||||
@ApiProperty({name: '强度', nullable: true })
|
||||
@Column({ nullable: true })
|
||||
@Expose()
|
||||
strength: string | null;
|
||||
|
||||
@ApiProperty({ description: '版本', type: 'string', nullable: true })
|
||||
@Expose()
|
||||
@Column({ nullable: true })
|
||||
version?: string;
|
||||
isYooneNew: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2022-12-12 11:11:11',
|
||||
|
|
@ -117,4 +97,25 @@ export class OrderSale {
|
|||
@UpdateDateColumn()
|
||||
@Expose()
|
||||
updatedAt?: Date;
|
||||
|
||||
// // === 自动计算逻辑 ===
|
||||
// @BeforeInsert()
|
||||
// @BeforeUpdate()
|
||||
// setFlags() {
|
||||
// if (!this.name) return;
|
||||
// const lower = this.name.toLowerCase();
|
||||
// this.isYoone = lower.includes('yoone');
|
||||
// this.isZex = lower.includes('zex');
|
||||
// this.isYooneNew = this.isYoone && lower.includes('new');
|
||||
// let size: number | null = null;
|
||||
// const sizes = [3, 6, 9, 12, 15, 18];
|
||||
// for (const s of sizes) {
|
||||
// if (lower.includes(s.toString())) {
|
||||
// size = s;
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// this.size = size;
|
||||
// }
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,9 +55,6 @@ export class Product {
|
|||
@Column({ nullable: true })
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ example: '图片URL', description: '产品图片URL' })
|
||||
@Column({ nullable: true })
|
||||
image?: string;
|
||||
// 商品价格
|
||||
@ApiProperty({ description: '价格', example: 99.99 })
|
||||
@Column({ type: 'decimal', precision: 10, scale: 2, default: 0 })
|
||||
|
|
@ -73,10 +70,6 @@ export class Product {
|
|||
@JoinColumn({ name: 'categoryId' })
|
||||
category: Category;
|
||||
|
||||
@ApiProperty({ description: '分类 ID', nullable: true, example: 1 })
|
||||
@Column({ nullable: true })
|
||||
categoryId?: number;
|
||||
|
||||
@ManyToMany(() => DictItem, dictItem => dictItem.products, {
|
||||
cascade: true,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -54,9 +54,9 @@ export class Shipment {
|
|||
tracking_provider?: string;
|
||||
|
||||
@ApiProperty()
|
||||
@Column({ nullable: true })
|
||||
@Column()
|
||||
@Expose()
|
||||
unique_id?: string;
|
||||
unique_id: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
@Expose()
|
||||
|
|
|
|||
|
|
@ -47,11 +47,6 @@ export class ShippingAddress {
|
|||
@Expose()
|
||||
phone_number_country: string;
|
||||
|
||||
@ApiProperty()
|
||||
@Column()
|
||||
@Expose()
|
||||
email: string;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2022-12-12 11:11:11',
|
||||
description: '创建时间',
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
Entity,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { ApiProperty } from '@midwayjs/swagger';
|
||||
import { Site } from './site.entity';
|
||||
import { Product } from './product.entity';
|
||||
|
||||
@Entity('site_product')
|
||||
export class SiteProduct {
|
||||
@ApiProperty({
|
||||
example: '12345',
|
||||
description: '站点商品ID',
|
||||
type: 'string',
|
||||
required: true,
|
||||
})
|
||||
@Column({ primary: true })
|
||||
id: string;
|
||||
|
||||
@ApiProperty({ description: '站点ID' })
|
||||
@Column()
|
||||
siteId: number;
|
||||
|
||||
@ApiProperty({ description: '商品ID' })
|
||||
@Column({ nullable: true })
|
||||
productId: number;
|
||||
|
||||
@ApiProperty({ description: 'sku'})
|
||||
@Column()
|
||||
sku: string;
|
||||
|
||||
@ApiProperty({ description: '类型' })
|
||||
@Column({ length: 16, default: 'single' })
|
||||
type: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '产品名称',
|
||||
type: 'string',
|
||||
required: true,
|
||||
})
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@ApiProperty({ description: '产品图片' })
|
||||
@Column({ default: '' })
|
||||
image: string;
|
||||
|
||||
@ApiProperty({ description: '父商品ID', example: '12345' })
|
||||
@Column({ nullable: true })
|
||||
parentId: string;
|
||||
|
||||
// 站点关联
|
||||
@ManyToOne(() => Site, site => site.id)
|
||||
@JoinColumn({ name: 'siteId' })
|
||||
site: Site;
|
||||
|
||||
// 商品关联
|
||||
@ManyToOne(() => Product, product => product.id)
|
||||
@JoinColumn({ name: 'productId' })
|
||||
product: Product;
|
||||
|
||||
// 父商品关联
|
||||
@ManyToOne(() => SiteProduct, siteProduct => siteProduct.id)
|
||||
@JoinColumn({ name: 'parentId' })
|
||||
parent: SiteProduct;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2022-12-12 11:11:11',
|
||||
description: '创建时间',
|
||||
required: true,
|
||||
})
|
||||
@CreateDateColumn()
|
||||
createdAt: Date;
|
||||
|
||||
@ApiProperty({
|
||||
example: '2022-12-12 11:11:11',
|
||||
description: '更新时间',
|
||||
required: true,
|
||||
})
|
||||
@UpdateDateColumn()
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import { ILogger, Inject, Logger } from '@midwayjs/core';
|
||||
import { IJob, Job } from '@midwayjs/cron';
|
||||
import { LogisticsService } from '../service/logistics.service';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Shipment } from '../entity/shipment.entity';
|
||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||
|
||||
|
||||
@Job({
|
||||
cronTime: '0 0 12 * * *', // 每天12点执行
|
||||
start: true
|
||||
})
|
||||
export class SyncTmsJob implements IJob {
|
||||
@Logger()
|
||||
logger: ILogger;
|
||||
|
||||
@Inject()
|
||||
logisticsService: LogisticsService;
|
||||
|
||||
@InjectEntityModel(Shipment)
|
||||
shipmentModel: Repository<Shipment>
|
||||
|
||||
async onTick() {
|
||||
const shipments:Shipment[] = await this.shipmentModel.findBy({ tracking_provider: 'freightwaves',finished: false });
|
||||
const results = await Promise.all(
|
||||
shipments.map(async shipment => {
|
||||
return await this.logisticsService.updateFreightwavesShipmentState(shipment);
|
||||
})
|
||||
)
|
||||
this.logger.info(`更新运单状态完毕 ${JSON.stringify(results)}`);
|
||||
return results
|
||||
}
|
||||
|
||||
onComplete(result: any) {
|
||||
this.logger.info(`更新运单状态完成 ${result}`);
|
||||
}
|
||||
onError(error: any) {
|
||||
this.logger.error(`更新运单状态失败 ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,8 +21,7 @@ export class CategoryService {
|
|||
order: {
|
||||
sort: 'DESC',
|
||||
createdAt: 'DESC'
|
||||
},
|
||||
relations: ['attributes', 'attributes.attributeDict']
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export class DictService {
|
|||
}
|
||||
|
||||
// 从XLSX文件导入字典
|
||||
async importDictsFromTable(bufferOrPath: Buffer | string) {
|
||||
async importDictsFromXLSX(bufferOrPath: Buffer | string) {
|
||||
// 判断传入的是 Buffer 还是文件路径字符串
|
||||
let buffer: Buffer;
|
||||
if (typeof bufferOrPath === 'string') {
|
||||
|
|
@ -216,10 +216,10 @@ export class DictService {
|
|||
|
||||
// 如果提供了 dictId,则只返回该字典下的项
|
||||
if (params.dictId) {
|
||||
return this.dictItemModel.find({ where, relations: ['dict'] });
|
||||
return this.dictItemModel.find({ where });
|
||||
}
|
||||
// 否则,返回所有字典项
|
||||
return this.dictItemModel.find({ relations: ['dict'] });
|
||||
return this.dictItemModel.find();
|
||||
}
|
||||
|
||||
// 创建新字典项
|
||||
|
|
|
|||
|
|
@ -1,332 +0,0 @@
|
|||
import { Inject, Provide } from '@midwayjs/core';
|
||||
import axios from 'axios';
|
||||
import * as crypto from 'crypto';
|
||||
import dayjs = require('dayjs');
|
||||
import utc = require('dayjs/plugin/utc');
|
||||
import timezone = require('dayjs/plugin/timezone');
|
||||
|
||||
// 扩展dayjs功能
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
// 全局参数配置接口
|
||||
interface FreightwavesConfig {
|
||||
appSecret: string;
|
||||
apiBaseUrl: string;
|
||||
partner: string;
|
||||
}
|
||||
|
||||
// 地址信息接口
|
||||
interface Address {
|
||||
name: string;
|
||||
phone: string;
|
||||
company: string;
|
||||
countryCode: string;
|
||||
city: string;
|
||||
state: string;
|
||||
address1: string;
|
||||
address2: string;
|
||||
postCode: string;
|
||||
zoneCode?: string;
|
||||
countryName: string;
|
||||
cityName: string;
|
||||
stateName: string;
|
||||
companyName: string;
|
||||
}
|
||||
|
||||
// 包裹尺寸接口
|
||||
interface Dimensions {
|
||||
length: number;
|
||||
width: number;
|
||||
height: number;
|
||||
lengthUnit: 'IN' | 'CM';
|
||||
weight: number;
|
||||
weightUnit: 'LB' | 'KG';
|
||||
}
|
||||
|
||||
// 包裹信息接口
|
||||
interface Package {
|
||||
dimensions: Dimensions;
|
||||
currency: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// 申报信息接口
|
||||
interface Declaration {
|
||||
boxNo: string;
|
||||
sku: string;
|
||||
cnname: string;
|
||||
enname: string;
|
||||
declaredPrice: number;
|
||||
declaredQty: number;
|
||||
material: string;
|
||||
intendedUse: string;
|
||||
cweight: number;
|
||||
hsCode: string;
|
||||
battery: string;
|
||||
}
|
||||
|
||||
// 费用试算请求接口
|
||||
export interface RateTryRequest {
|
||||
shipCompany: string;
|
||||
partnerOrderNumber: string;
|
||||
warehouseId?: string;
|
||||
shipper: Address;
|
||||
reciver: Address;
|
||||
packages: Package[];
|
||||
partner: string;
|
||||
signService?: 0 | 1;
|
||||
}
|
||||
|
||||
// 创建订单请求接口
|
||||
interface CreateOrderRequest extends RateTryRequest {
|
||||
declaration: Declaration;
|
||||
}
|
||||
|
||||
// 查询订单请求接口
|
||||
interface QueryOrderRequest {
|
||||
partnerOrderNumber?: string;
|
||||
shipOrderId?: string;
|
||||
partner: string;
|
||||
}
|
||||
|
||||
// 修改订单请求接口
|
||||
interface ModifyOrderRequest extends CreateOrderRequest {
|
||||
shipOrderId: string;
|
||||
}
|
||||
|
||||
// 订单退款请求接口
|
||||
interface RefundOrderRequest {
|
||||
shipOrderId: string;
|
||||
partner: string;
|
||||
}
|
||||
|
||||
// 通用响应接口
|
||||
interface FreightwavesResponse<T> {
|
||||
code: string;
|
||||
msg: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
// 费用试算响应数据接口
|
||||
interface RateTryResponseData {
|
||||
shipCompany: string;
|
||||
channelCode: string;
|
||||
totalAmount: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
// 创建订单响应数据接口
|
||||
interface CreateOrderResponseData {
|
||||
msg: string;
|
||||
data: any;
|
||||
}
|
||||
|
||||
// 查询订单响应数据接口
|
||||
interface QueryOrderResponseData {
|
||||
thirdOrderId: string;
|
||||
shipCompany: string;
|
||||
expressFinish: 0 | 1 | 2;
|
||||
expressFailMsg: string;
|
||||
expressOrder: {
|
||||
mainTrackingNumber: string;
|
||||
labelPath: string[];
|
||||
totalAmount: number;
|
||||
currency: string;
|
||||
balance: number;
|
||||
};
|
||||
partnerOrderNumber: string;
|
||||
shipOrderId: string;
|
||||
}
|
||||
|
||||
// 修改订单响应数据接口
|
||||
interface ModifyOrderResponseData extends CreateOrderResponseData { }
|
||||
|
||||
// 订单退款响应数据接口
|
||||
interface RefundOrderResponseData { }
|
||||
|
||||
@Provide()
|
||||
export class FreightwavesService {
|
||||
@Inject() logger;
|
||||
|
||||
// 默认配置
|
||||
private config: FreightwavesConfig = {
|
||||
appSecret: 'gELCHguGmdTLo!zfihfM91hae8G@9Sz23Mh6pHrt',
|
||||
apiBaseUrl: 'http://tms.freightwaves.ca:8901',
|
||||
partner: '25072621035200000060'
|
||||
};
|
||||
|
||||
// 初始化配置
|
||||
setConfig(config: Partial<FreightwavesConfig>): void {
|
||||
this.config = { ...this.config, ...config };
|
||||
}
|
||||
|
||||
// 生成签名
|
||||
private generateSignature(body: any, date: string): string {
|
||||
const bodyString = JSON.stringify(body);
|
||||
const signatureStr = `${bodyString}${this.config.appSecret}${date}`;
|
||||
return crypto.createHash('md5').update(signatureStr).digest('hex');
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
private async sendRequest<T>(url: string, data: any): Promise<FreightwavesResponse<T>> {
|
||||
try {
|
||||
// 设置请求头 - 使用太平洋时间 (America/Los_Angeles)
|
||||
const date = dayjs().tz('America/Los_Angeles').format('YYYY-MM-DD HH:mm:ss');
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'requestDate': date,
|
||||
'signature': this.generateSignature(data, date),
|
||||
};
|
||||
|
||||
// 发送请求 - 临时禁用SSL证书验证以解决UNABLE_TO_VERIFY_LEAF_SIGNATURE错误
|
||||
const response = await axios.post<FreightwavesResponse<T>>(
|
||||
`${this.config.apiBaseUrl}${url}`,
|
||||
data,
|
||||
{
|
||||
headers,
|
||||
httpsAgent: new (require('https').Agent)({
|
||||
rejectUnauthorized: false
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
// 记录响应信息
|
||||
this.log(`Received response from: ${this.config.apiBaseUrl}${url}`, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data: response.data
|
||||
});
|
||||
|
||||
// 处理响应
|
||||
if (response.data.code !== '00000200') {
|
||||
this.log(`Freightwaves API error: ${response.data.msg}`, { url, data, response: response.data });
|
||||
throw new Error(response.data.msg);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
// 更详细的错误记录
|
||||
if (error.response) {
|
||||
// 请求已发送,服务器返回错误状态码
|
||||
this.log(`Freightwaves API request failed with status: ${error.response.status}`, {
|
||||
url,
|
||||
data,
|
||||
response: error.response.data,
|
||||
status: error.response.status,
|
||||
headers: error.response.headers
|
||||
});
|
||||
} else if (error.request) {
|
||||
// 请求已发送,但没有收到响应
|
||||
this.log(`Freightwaves API request no response received`, {
|
||||
url,
|
||||
data,
|
||||
request: error.request
|
||||
});
|
||||
} else {
|
||||
// 请求配置时发生错误
|
||||
this.log(`Freightwaves API request configuration error: ${error.message}`, {
|
||||
url,
|
||||
data,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 费用试算
|
||||
* @param params 费用试算参数
|
||||
* @returns 费用试算结果
|
||||
*/
|
||||
async rateTry(params: Omit<RateTryRequest, 'partner'>): Promise<RateTryResponseData> {
|
||||
const requestData: RateTryRequest = {
|
||||
...params,
|
||||
partner: this.config.partner,
|
||||
};
|
||||
|
||||
const response = await this.sendRequest<RateTryResponseData>('/shipService/order/rateTry', requestData);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建订单
|
||||
* @param params 创建订单参数
|
||||
* @returns 创建订单结果
|
||||
*/
|
||||
async createOrder(params: Omit<CreateOrderRequest, 'partner'>): Promise<CreateOrderResponseData> {
|
||||
const requestData: CreateOrderRequest = {
|
||||
...params,
|
||||
partner: this.config.partner,
|
||||
};
|
||||
|
||||
const response = await this.sendRequest<CreateOrderResponseData>('/shipService/order/createOrder', requestData);
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询订单
|
||||
* @param params 查询订单参数
|
||||
* @returns 查询订单结果
|
||||
*/
|
||||
async queryOrder(params: Omit<QueryOrderRequest, 'partner'>): Promise<QueryOrderResponseData> {
|
||||
const requestData: QueryOrderRequest = {
|
||||
...params,
|
||||
partner: this.config.partner,
|
||||
};
|
||||
|
||||
const response = await this.sendRequest<QueryOrderResponseData>('/shipService/order/queryOrder', requestData);
|
||||
if (response.code !== '00000200') {
|
||||
throw new Error(response.msg);
|
||||
}
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改订单
|
||||
* @param params 修改订单参数
|
||||
* @returns 修改订单结果
|
||||
*/
|
||||
async modifyOrder(params: Omit<ModifyOrderRequest, 'partner'>): Promise<ModifyOrderResponseData> {
|
||||
const requestData: ModifyOrderRequest = {
|
||||
...params,
|
||||
partner: this.config.partner,
|
||||
};
|
||||
|
||||
const response = await this.sendRequest<ModifyOrderResponseData>('/shipService/order/modifyOrder', requestData);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 订单退款
|
||||
* @param params 订单退款参数
|
||||
* @returns 订单退款结果
|
||||
*/
|
||||
async refundOrder(params: Omit<RefundOrderRequest, 'partner'>): Promise<RefundOrderResponseData> {
|
||||
const requestData: RefundOrderRequest = {
|
||||
...params,
|
||||
partner: this.config.partner,
|
||||
};
|
||||
const response = await this.sendRequest<RefundOrderResponseData>('/shipService/order/refundOrder', requestData);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助日志方法,处理logger可能未定义的情况
|
||||
* @param message 日志消息
|
||||
* @param data 附加数据
|
||||
*/
|
||||
private log(message: string, data?: any) {
|
||||
if (this.logger) {
|
||||
this.logger.info(message, data);
|
||||
} else {
|
||||
// 如果logger未定义,使用console输出
|
||||
if (data) {
|
||||
console.log(message, data);
|
||||
} else {
|
||||
console.log(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -27,12 +27,10 @@ import { CanadaPostService } from './canadaPost.service';
|
|||
import { OrderItem } from '../entity/order_item.entity';
|
||||
import { OrderSale } from '../entity/order_sale.entity';
|
||||
import { UniExpressService } from './uni_express.service';
|
||||
import { FreightwavesService, RateTryRequest } from './freightwaves.service';
|
||||
import { StockPoint } from '../entity/stock_point.entity';
|
||||
import { OrderService } from './order.service';
|
||||
import { convertKeysFromCamelToSnake } from '../utils/object-transform.util';
|
||||
import { SiteService } from './site.service';
|
||||
import { ShopyyService } from './shopyy.service';
|
||||
|
||||
@Provide()
|
||||
export class LogisticsService {
|
||||
|
|
@ -75,15 +73,9 @@ export class LogisticsService {
|
|||
@Inject()
|
||||
uniExpressService: UniExpressService;
|
||||
|
||||
@Inject()
|
||||
freightwavesService: FreightwavesService;
|
||||
|
||||
@Inject()
|
||||
wpService: WPService;
|
||||
|
||||
@Inject()
|
||||
shopyyService: ShopyyService;
|
||||
|
||||
@Inject()
|
||||
orderService: OrderService;
|
||||
|
||||
|
|
@ -149,30 +141,6 @@ export class LogisticsService {
|
|||
}
|
||||
}
|
||||
|
||||
//"expressFinish": 0, //是否快递创建完成(1:完成 0:未完成,需要轮询 2:失败)
|
||||
async updateFreightwavesShipmentState(shipment: Shipment) {
|
||||
try {
|
||||
const data = await this.freightwavesService.queryOrder({ shipOrderId: shipment.order_id.toString() });
|
||||
console.log('updateFreightwavesShipmentState data:', data);
|
||||
// huo
|
||||
if (data.expressFinish === 2) {
|
||||
throw new Error('获取运单状态失败,原因为' + data.expressFailMsg)
|
||||
}
|
||||
|
||||
if (data.expressFinish === 0) {
|
||||
shipment.state = '203';
|
||||
shipment.finished = true;
|
||||
}
|
||||
|
||||
this.shipmentModel.save(shipment);
|
||||
return shipment.state;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
// throw new Error(`更新运单状态失败 ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async updateShipmentStateById(id: number) {
|
||||
const shipment: Shipment = await this.shipmentModel.findOneBy({ id: id });
|
||||
return this.updateShipmentState(shipment);
|
||||
|
|
@ -279,7 +247,8 @@ export class LogisticsService {
|
|||
|
||||
shipmentRepo.remove(shipment);
|
||||
|
||||
await this.uniExpressService.deleteShipment(shipment.return_tracking_number);
|
||||
const res = await this.uniExpressService.deleteShipment(shipment.return_tracking_number);
|
||||
console.log('res', res.data); // todo
|
||||
|
||||
await orderRepo.save(order);
|
||||
|
||||
|
|
@ -309,6 +278,7 @@ export class LogisticsService {
|
|||
console.log('同步到woocommerce失败', error);
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch {
|
||||
throw new Error('删除运单失败');
|
||||
|
|
@ -324,16 +294,7 @@ export class LogisticsService {
|
|||
currency: 'CAD',
|
||||
// item_description: data.sales, // todo: 货品信息
|
||||
}
|
||||
let resShipmentFee: any;
|
||||
if (data.shipmentPlatform === 'uniuni') {
|
||||
resShipmentFee = await this.uniExpressService.getRates(reqBody);
|
||||
} else if (data.shipmentPlatform === 'freightwaves') {
|
||||
const fre_reqBody = await this.convertToFreightwavesRateTry(data);
|
||||
resShipmentFee = await this.freightwavesService.rateTry(fre_reqBody);
|
||||
} else {
|
||||
throw new Error('不支持的运单平台');
|
||||
}
|
||||
|
||||
const resShipmentFee = await this.uniExpressService.getRates(reqBody);
|
||||
if (resShipmentFee.status !== 'SUCCESS') {
|
||||
throw new Error(resShipmentFee.ret_msg);
|
||||
}
|
||||
|
|
@ -358,10 +319,40 @@ export class LogisticsService {
|
|||
|
||||
let resShipmentOrder;
|
||||
try {
|
||||
resShipmentOrder = await this.mepShipment(data, order);
|
||||
const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId });
|
||||
const reqBody = {
|
||||
sender: data.details.origin.contact_name,
|
||||
start_phone: data.details.origin.phone_number,
|
||||
start_postal_code: data.details.origin.address.postal_code.replace(/\s/g, ''),
|
||||
pickup_address: data.details.origin.address.address_line_1,
|
||||
pickup_warehouse: stock_point.upStreamStockPointId,
|
||||
shipper_country_code: data.details.origin.address.country,
|
||||
receiver: data.details.destination.contact_name,
|
||||
city: data.details.destination.address.city,
|
||||
province: data.details.destination.address.region,
|
||||
country: data.details.destination.address.country,
|
||||
postal_code: data.details.destination.address.postal_code.replace(/\s/g, ''),
|
||||
delivery_address: data.details.destination.address.address_line_1,
|
||||
receiver_phone: data.details.destination.phone_number.number,
|
||||
receiver_email: data.details.destination.email_addresses,
|
||||
// item_description: data.sales, // todo: 货品信息
|
||||
length: data.details.packaging_properties.packages[0].measurements.cuboid.l,
|
||||
width: data.details.packaging_properties.packages[0].measurements.cuboid.w,
|
||||
height: data.details.packaging_properties.packages[0].measurements.cuboid.h,
|
||||
dimension_uom: data.details.packaging_properties.packages[0].measurements.cuboid.unit,
|
||||
weight: data.details.packaging_properties.packages[0].measurements.weight.value,
|
||||
weight_uom: data.details.packaging_properties.packages[0].measurements.weight.unit,
|
||||
currency: 'CAD',
|
||||
custom_field: {
|
||||
'order_id': order.externalOrderId
|
||||
}
|
||||
}
|
||||
|
||||
// 记录物流信息,并将订单状态转到完成,uniuni状态为SUCCESS,tms.freightwaves状态为00000200
|
||||
if (resShipmentOrder.status === 'SUCCESS' || resShipmentOrder.code === '00000200') {
|
||||
// 添加运单
|
||||
resShipmentOrder = await this.uniExpressService.createShipment(reqBody);
|
||||
|
||||
// 记录物流信息,并将订单状态转到完成
|
||||
if (resShipmentOrder.status === 'SUCCESS') {
|
||||
order.orderStatus = ErpOrderStatus.COMPLETED;
|
||||
} else {
|
||||
throw new Error('运单生成失败');
|
||||
|
|
@ -372,27 +363,12 @@ export class LogisticsService {
|
|||
await dataSource.transaction(async manager => {
|
||||
const orderRepo = manager.getRepository(Order);
|
||||
const shipmentRepo = manager.getRepository(Shipment);
|
||||
const tracking_provider = data.shipmentPlatform; // todo: id未确定,后写进常数
|
||||
const tracking_provider = 'UniUni'; // todo: id未确定,后写进常数
|
||||
|
||||
// 同步物流信息到woocommerce
|
||||
const site = await this.siteService.get(Number(order.siteId), true);
|
||||
let co: any;
|
||||
let unique_id: any;
|
||||
let state: any;
|
||||
if (data.shipmentPlatform === 'uniuni') {
|
||||
co = resShipmentOrder.data.tno;
|
||||
unique_id = resShipmentOrder.data.uni_order_sn;
|
||||
state = resShipmentOrder.data.uni_status_code;
|
||||
} else {
|
||||
co = resShipmentOrder.data?.shipOrderId;
|
||||
unique_id = resShipmentOrder.data?.shipOrderId;
|
||||
state = ErpOrderStatus.COMPLETED;
|
||||
}
|
||||
|
||||
// 同步订单状态到woocommerce
|
||||
if (order.source_type != "shopyy") {
|
||||
const res = await this.wpService.createFulfillment(site, order.externalOrderId, {
|
||||
tracking_number: co,
|
||||
tracking_number: resShipmentOrder.data.tno,
|
||||
tracking_provider: tracking_provider,
|
||||
});
|
||||
|
||||
|
|
@ -400,61 +376,36 @@ export class LogisticsService {
|
|||
const shipment = await shipmentRepo.save({
|
||||
tracking_provider: tracking_provider,
|
||||
tracking_id: res.data.tracking_id,
|
||||
unique_id: unique_id,
|
||||
unique_id: resShipmentOrder.data.uni_order_sn,
|
||||
stockPointId: String(data.stockPointId), // todo
|
||||
state: state,
|
||||
return_tracking_number: co,
|
||||
state: resShipmentOrder.data.uni_status_code,
|
||||
return_tracking_number: resShipmentOrder.data.tno,
|
||||
fee: data.details.shipmentFee,
|
||||
order: order
|
||||
});
|
||||
order.shipmentId = shipment.id;
|
||||
shipmentId = shipment.id;
|
||||
}
|
||||
|
||||
// 同步订单状态到woocommerce
|
||||
if (order.status !== OrderStatus.COMPLETED) {
|
||||
await this.wpService.updateOrder(site, order.externalOrderId, {
|
||||
status: OrderStatus.COMPLETED,
|
||||
});
|
||||
order.status = OrderStatus.COMPLETED;
|
||||
}
|
||||
}
|
||||
if (order.source_type === "shopyy") {
|
||||
const res = await this.shopyyService.createFulfillment(site, order.externalOrderId, {
|
||||
tracking_number: co,
|
||||
tracking_company: resShipmentOrder.shipCompany,
|
||||
carrier_code: resShipmentOrder.shipperOrderId,
|
||||
});
|
||||
if (order.orderStatus === ErpOrderStatus.COMPLETED) {
|
||||
const shipment = await shipmentRepo.save({
|
||||
tracking_provider: tracking_provider,
|
||||
tracking_id: res.data.tracking_id,
|
||||
unique_id: unique_id,
|
||||
stockPointId: String(data.stockPointId), // todo
|
||||
state: state,
|
||||
return_tracking_number: co,
|
||||
fee: data.details.shipmentFee,
|
||||
order: order
|
||||
});
|
||||
order.shipmentId = shipment.id;
|
||||
shipmentId = shipment.id;
|
||||
}
|
||||
if (order.status !== OrderStatus.COMPLETED) {
|
||||
// shopyy未提供更新订单接口,暂不更新订单状态
|
||||
// await this.shopyyService.updateOrder(site, order.externalOrderId, {
|
||||
// status: OrderStatus.COMPLETED,
|
||||
// });
|
||||
order.status = OrderStatus.COMPLETED;
|
||||
}
|
||||
}
|
||||
order.orderStatus = ErpOrderStatus.COMPLETED;
|
||||
|
||||
await orderRepo.save(order);
|
||||
}).catch(error => {
|
||||
transactionError = error
|
||||
throw new Error(`请求错误:${error}`);
|
||||
});
|
||||
|
||||
if (transactionError !== undefined) {
|
||||
console.log('err', transactionError);
|
||||
throw transactionError;
|
||||
}
|
||||
|
||||
// 更新产品发货信息
|
||||
this.orderService.updateOrderSales(order.id, sales);
|
||||
|
||||
|
|
@ -691,190 +642,4 @@ export class LogisticsService {
|
|||
|
||||
return { items, total, current, pageSize };
|
||||
}
|
||||
|
||||
|
||||
async mepShipment(data: ShipmentBookDTO, order: Order) {
|
||||
try {
|
||||
const stock_point = await this.stockPointModel.findOneBy({ id: data.stockPointId });
|
||||
let resShipmentOrder;
|
||||
|
||||
if (data.shipmentPlatform === 'uniuni') {
|
||||
const reqBody = {
|
||||
sender: data.details.origin.contact_name,
|
||||
start_phone: data.details.origin.phone_number,
|
||||
start_postal_code: data.details.origin.address.postal_code.replace(/\s/g, ''),
|
||||
pickup_address: data.details.origin.address.address_line_1,
|
||||
pickup_warehouse: stock_point.upStreamStockPointId,
|
||||
shipper_country_code: data.details.origin.address.country,
|
||||
receiver: data.details.destination.contact_name,
|
||||
city: data.details.destination.address.city,
|
||||
province: data.details.destination.address.region,
|
||||
country: data.details.destination.address.country,
|
||||
postal_code: data.details.destination.address.postal_code.replace(/\s/g, ''),
|
||||
delivery_address: data.details.destination.address.address_line_1,
|
||||
receiver_phone: data.details.destination.phone_number.number,
|
||||
receiver_email: data.details.destination.email_addresses,
|
||||
// item_description: data.sales, // todo: 货品信息
|
||||
length: data.details.packaging_properties.packages[0].measurements.cuboid.l,
|
||||
width: data.details.packaging_properties.packages[0].measurements.cuboid.w,
|
||||
height: data.details.packaging_properties.packages[0].measurements.cuboid.h,
|
||||
dimension_uom: data.details.packaging_properties.packages[0].measurements.cuboid.unit,
|
||||
weight: data.details.packaging_properties.packages[0].measurements.weight.value,
|
||||
weight_uom: data.details.packaging_properties.packages[0].measurements.weight.unit,
|
||||
currency: 'CAD',
|
||||
custom_field: {
|
||||
'order_id': order.externalOrderId // todo: 需要获取订单的externalOrderId
|
||||
}
|
||||
};
|
||||
// 添加运单
|
||||
resShipmentOrder = await this.uniExpressService.createShipment(reqBody);
|
||||
}
|
||||
|
||||
if (data.shipmentPlatform === 'freightwaves') {
|
||||
// 根据TMS系统对接说明文档格式化参数
|
||||
const reqBody: any = {
|
||||
shipCompany: 'UPSYYZ7000NEW',
|
||||
partnerOrderNumber: order.siteId + '-' + order.externalOrderId,
|
||||
warehouseId: '25072621030107400060',
|
||||
shipper: {
|
||||
name: data.details.origin.contact_name, // 姓名
|
||||
phone: data.details.origin.phone_number.number, // 电话(提取number属性转换为字符串)
|
||||
company: '', // 公司
|
||||
countryCode: data.details.origin.address.country, // 国家Code
|
||||
city: data.details.origin.address.city, // 城市
|
||||
state: data.details.origin.address.region, // 州/省Code,两个字母缩写
|
||||
address1: data.details.origin.address.address_line_1, // 详细地址
|
||||
address2: '', // 详细地址2(Address类型中没有address_line_2属性)
|
||||
postCode: data.details.origin.address.postal_code.replace(/\s/g, ''), // 邮编
|
||||
countryName: data.details.origin.address.country, // 国家名称(Address类型中没有country_name属性,使用country代替)
|
||||
cityName: data.details.origin.address.city, // 城市名称
|
||||
stateName: data.details.origin.address.region, // 州/省名称
|
||||
companyName: '' // 公司名称
|
||||
},
|
||||
reciver: {
|
||||
name: data.details.destination.contact_name, // 姓名
|
||||
phone: data.details.destination.phone_number.number, // 电话
|
||||
company: '', // 公司
|
||||
countryCode: data.details.destination.address.country, // 国家Code
|
||||
city: data.details.destination.address.city, // 城市
|
||||
state: data.details.destination.address.region, // 州/省Code,两个字母的缩写
|
||||
address1: data.details.destination.address.address_line_1, // 详细地址
|
||||
address2: '', // 详细地址2(Address类型中没有address_line_2属性)
|
||||
postCode: data.details.destination.address.postal_code.replace(/\s/g, ''), // 邮编
|
||||
countryName: data.details.destination.address.country, // 国家名称(Address类型中没有country_name属性,使用country代替)
|
||||
cityName: data.details.destination.address.city, // 城市名称
|
||||
stateName: data.details.destination.address.region, // 州/省名称
|
||||
companyName: '' // 公司名称
|
||||
},
|
||||
packages: [
|
||||
{
|
||||
dimensions: {
|
||||
length: data.details.packaging_properties.packages[0].measurements.cuboid.l, // 长
|
||||
width: data.details.packaging_properties.packages[0].measurements.cuboid.w, // 宽
|
||||
height: data.details.packaging_properties.packages[0].measurements.cuboid.h, // 高
|
||||
lengthUnit: (data.details.packaging_properties.packages[0].measurements.cuboid.unit === 'cm' ? 'CM' : 'IN') as 'CM' | 'IN', // 长度单位(IN,CM)
|
||||
weight: data.details.packaging_properties.packages[0].measurements.weight.value, // 重量
|
||||
weightUnit: (data.details.packaging_properties.packages[0].measurements.weight.unit === 'kg' ? 'KG' : 'LB') as 'KG' | 'LB' // 重量单位(LB,KG)
|
||||
},
|
||||
currency: 'CAD', // 币种(默认CAD)
|
||||
description: 'site:' + order.siteId + ' orderId:' + order.externalOrderId // 包裹描述(确保是字符串类型)
|
||||
}
|
||||
],
|
||||
signService: 0
|
||||
// 非跨境订单不需要declaration
|
||||
// declaration: {
|
||||
// "boxNo": "", //箱子编号
|
||||
// "sku": "", //SKU
|
||||
// "cnname": "", //中文名称
|
||||
// "enname": "", //英文名称
|
||||
// "declaredPrice": 1, //申报单价
|
||||
// "declaredQty": 1, //申报数量
|
||||
// "material": "", //材质
|
||||
// "intendedUse": "", //用途
|
||||
// "cweight": 1, //产品单重
|
||||
// "hsCode": "", //海关编码
|
||||
// "battery": "" //电池描述
|
||||
// }
|
||||
};
|
||||
|
||||
resShipmentOrder = await this.freightwavesService.createOrder(reqBody); // 创建订单
|
||||
//tms只返回了物流订单号,需要查询一次来获取完整的物流信息
|
||||
const queryRes = await this.freightwavesService.queryOrder({ shipOrderId: resShipmentOrder.shipOrderId }); // 查询订单
|
||||
resShipmentOrder.push(queryRes);
|
||||
}
|
||||
|
||||
return resShipmentOrder;
|
||||
} catch (error) {
|
||||
console.log('物流订单处理失败:', error); // 使用console.log代替this.log
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将ShipmentFeeBookDTO转换为freightwaves的RateTryRequest格式
|
||||
* @param data ShipmentFeeBookDTO数据
|
||||
* @returns RateTryRequest格式的数据
|
||||
*/
|
||||
async convertToFreightwavesRateTry(data: ShipmentFeeBookDTO): Promise<Omit<RateTryRequest, 'partner'>> {
|
||||
|
||||
const shipments = await this.shippingAddressModel.findOne({
|
||||
where: {
|
||||
id: data.address_id,
|
||||
},
|
||||
})
|
||||
|
||||
const address = shipments?.address;
|
||||
// 转换为RateTryRequest格式
|
||||
const r = {
|
||||
shipCompany: 'UPSYYZ7000NEW', // 必填,但ShipmentFeeBookDTO中缺少
|
||||
partnerOrderNumber: `order-${Date.now()}`, // 必填,使用时间戳生成
|
||||
warehouseId: '25072621030107400060', // 可选,使用stockPointId转换
|
||||
shipper: {
|
||||
name: data.sender, // 必填
|
||||
phone: data.startPhone.phone, // 必填
|
||||
company: address.country, // 必填,但ShipmentFeeBookDTO中缺少
|
||||
countryCode: data.shipperCountryCode, // 必填
|
||||
city: address.city || '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||
state: address.region || '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||
address1: address.address_line_1, // 必填
|
||||
address2: address.address_line_1 || '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||
postCode: data.startPostalCode, // 必填
|
||||
countryName: address.country || '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||
cityName: address.city || '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||
stateName: address.region || '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||
companyName: address.country || '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||
},
|
||||
reciver: {
|
||||
name: data.receiver, // 必填
|
||||
phone: data.receiverPhone, // 必填
|
||||
company: address.country,// 必填,但ShipmentFeeBookDTO中缺少
|
||||
countryCode: data.country, // 必填,使用country代替countryCode
|
||||
city: data.city, // 必填
|
||||
state: data.province, // 必填,使用province代替state
|
||||
address1: data.deliveryAddress, // 必填
|
||||
address2: data.deliveryAddress, // 必填,但ShipmentFeeBookDTO中缺少
|
||||
postCode: data.postalCode, // 必填
|
||||
countryName: address.country, // 必填,但ShipmentFeeBookDTO中缺少
|
||||
cityName: data.city || '', // 必填,使用city代替cityName
|
||||
stateName: data.province || '', // 必填,使用province代替stateName
|
||||
companyName: address.country || '', // 必填,但ShipmentFeeBookDTO中缺少
|
||||
},
|
||||
packages: [
|
||||
{
|
||||
dimensions: {
|
||||
length: data.length, // 必填
|
||||
width: data.width, // 必填
|
||||
height: data.height, // 必填
|
||||
lengthUnit: (data.dimensionUom === 'IN' ? 'IN' : 'CM') as 'IN' | 'CM', // 必填,转换为有效的单位
|
||||
weight: data.weight, // 必填
|
||||
weightUnit: (data.weightUom === 'LBS' ? 'LB' : 'KG') as 'LB' | 'KG', // 必填,转换为有效的单位
|
||||
},
|
||||
currency: 'CAD', // 必填,但ShipmentFeeBookDTO中缺少,使用默认值
|
||||
description: 'Package', // 必填,但ShipmentFeeBookDTO中缺少,使用默认值
|
||||
},
|
||||
],
|
||||
signService: 0, // 可选,默认不使用签名服务
|
||||
};
|
||||
return r as any;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -141,7 +141,7 @@ export class OrderService {
|
|||
updated: 0,
|
||||
errors: []
|
||||
};
|
||||
this.logger.info('开始进入循环同步订单', result.length, '个订单')
|
||||
|
||||
// 遍历每个订单进行同步
|
||||
for (const order of result) {
|
||||
try {
|
||||
|
|
@ -150,7 +150,7 @@ export class OrderService {
|
|||
where: { externalOrderId: String(order.id), siteId: siteId },
|
||||
});
|
||||
if (!existingOrder) {
|
||||
this.logger.debug("数据库中不存在", order.id, '订单状态:', order.status)
|
||||
console.log("数据库中不存在", order.id, '订单状态:', order.status)
|
||||
}
|
||||
// 同步单个订单
|
||||
await this.syncSingleOrder(siteId, order);
|
||||
|
|
@ -165,7 +165,6 @@ export class OrderService {
|
|||
} else {
|
||||
syncResult.created++;
|
||||
}
|
||||
// console.log('updated', syncResult.updated, 'created:', syncResult.created)
|
||||
} catch (error) {
|
||||
// 记录错误但不中断整个同步过程
|
||||
syncResult.errors.push({
|
||||
|
|
@ -175,7 +174,7 @@ export class OrderService {
|
|||
syncResult.processed++;
|
||||
}
|
||||
}
|
||||
this.logger.info('同步完成', syncResult.updated, 'created:', syncResult.created)
|
||||
this.logger.debug('syncOrders result', syncResult)
|
||||
return syncResult;
|
||||
}
|
||||
|
||||
|
|
@ -213,7 +212,7 @@ export class OrderService {
|
|||
where: { externalOrderId: String(order.id), siteId: siteId },
|
||||
});
|
||||
if (!existingOrder) {
|
||||
this.logger.debug("数据库不存在", siteId, "订单:", order.id, '订单状态:' + order.status)
|
||||
console.log("数据库不存在", siteId, "订单:", order.id, '订单状态:' + order.status)
|
||||
}
|
||||
// 同步单个订单
|
||||
await this.syncSingleOrder(siteId, order, true);
|
||||
|
|
@ -327,30 +326,13 @@ export class OrderService {
|
|||
this.logger.debug('订单状态为 AUTO_DRAFT,跳过处理', siteId, order.id)
|
||||
return;
|
||||
}
|
||||
// 检查数据库中是否已存在该订单
|
||||
const existingOrder = await this.orderModel.findOne({
|
||||
where: { externalOrderId: String(order.id), siteId: siteId },
|
||||
});
|
||||
// 自动更新订单状态(如果需要)
|
||||
// 这里其实不用过滤不可编辑的行为,而是应在 save 中做判断
|
||||
// if(!order.is_editable && !forceUpdate){
|
||||
// this.logger.debug('订单不可编辑,跳过处理', siteId, order.id)
|
||||
// return;
|
||||
// }
|
||||
// 自动转换远程订单的状态(如果需要)
|
||||
await this.autoUpdateOrderStatus(siteId, order);
|
||||
|
||||
if (existingOrder) {
|
||||
// 矫正数据库中的订单数据
|
||||
const updateData: any = { status: order.status };
|
||||
if (this.canUpdateErpStatus(existingOrder.orderStatus)) {
|
||||
updateData.orderStatus = this.mapOrderStatus(order.status as any);
|
||||
}
|
||||
// 更新订单主数据
|
||||
await this.orderModel.update({ externalOrderId: String(order.id), siteId: siteId }, updateData);
|
||||
// 更新 fulfillments 数据
|
||||
await this.saveOrderFulfillments({
|
||||
siteId,
|
||||
orderId: existingOrder.id,
|
||||
externalOrderId: order.id,
|
||||
fulfillments: fulfillments,
|
||||
});
|
||||
}
|
||||
const externalOrderId = String(order.id);
|
||||
// 这里的 saveOrder 已经包括了创建订单和更新订单
|
||||
let orderRecord: Order = await this.saveOrder(siteId, orderData);
|
||||
// 如果订单从未完成变为完成状态,则更新库存
|
||||
|
|
@ -362,12 +344,13 @@ export class OrderService {
|
|||
await this.updateStock(orderRecord);
|
||||
// 不再直接返回,继续执行后续的更新操作
|
||||
}
|
||||
const externalOrderId = String(order.id);
|
||||
const orderId = orderRecord.id;
|
||||
// 保存订单项
|
||||
await this.saveOrderItems({
|
||||
siteId,
|
||||
orderId,
|
||||
externalOrderId: String(externalOrderId),
|
||||
externalOrderId,
|
||||
orderItems: line_items,
|
||||
});
|
||||
// 保存退款信息
|
||||
|
|
@ -728,20 +711,22 @@ export class OrderService {
|
|||
await this.orderSaleModel.delete(currentOrderSale.map(v => v.id));
|
||||
}
|
||||
if (!orderItem.sku) return;
|
||||
|
||||
// 从数据库查询产品,关联查询组件
|
||||
const productDetail = await this.productService.getComponentDetailFromSiteSku({ sku: orderItem.sku, name: orderItem.name });
|
||||
const product = await this.productModel.findOne({
|
||||
where: { siteSkus: Like(`%${orderItem.sku}%`) },
|
||||
relations: ['components'],
|
||||
});
|
||||
|
||||
if (!productDetail || !productDetail.quantity) return;
|
||||
const { product, quantity } = productDetail
|
||||
if (!product) return;
|
||||
const componentDetails: { product: Product, quantity: number }[] = product.components?.length > 0 ? await Promise.all(product.components.map(async comp => {
|
||||
return {
|
||||
product: await this.productModel.findOne({
|
||||
where: { id: comp.productId },
|
||||
where: { sku: comp.sku },
|
||||
relations: ['components','attributes'],
|
||||
}),
|
||||
quantity: comp.quantity * orderItem.quantity,
|
||||
}
|
||||
})) : [{ product, quantity }]
|
||||
})) : [{ product, quantity: orderItem.quantity }]
|
||||
|
||||
const orderSales: OrderSale[] = componentDetails.map(componentDetail => {
|
||||
if(!componentDetail.product) return null
|
||||
|
|
@ -749,35 +734,31 @@ export class OrderService {
|
|||
const orderSale = plainToClass(OrderSale, {
|
||||
orderId: orderItem.orderId,
|
||||
siteId: orderItem.siteId,
|
||||
externalOrderItemId: orderItem.externalOrderItemId,// 原始 itemId
|
||||
parentProductId: product.id, // 父产品 ID 用于统计套餐 如果是单品则不记录
|
||||
externalOrderItemId: orderItem.externalOrderItemId,
|
||||
productId: componentDetail.product.id,
|
||||
isPackage: product.type === 'bundle',// 这里是否是套餐取决于父产品
|
||||
name: componentDetail.product.name,
|
||||
quantity: componentDetail.quantity * orderItem.quantity,
|
||||
sku: componentDetail.product.sku,
|
||||
// 理论上直接存 product 的全部数据才是对的,因为这样我的数据才全面。
|
||||
brand: attrsObj?.['brand']?.name,
|
||||
version: attrsObj?.['version']?.name,
|
||||
strength: attrsObj?.['strength']?.name,
|
||||
flavor: attrsObj?.['flavor']?.name,
|
||||
humidity: attrsObj?.['humidity']?.name,
|
||||
size: attrsObj?.['size']?.name,
|
||||
category: componentDetail.product.category.name,
|
||||
isPackage: componentDetail.product.type === 'bundle',
|
||||
isYoone: attrsObj?.['brand']?.name === 'yoone',
|
||||
isZyn: attrsObj?.['brand']?.name === 'zyn',
|
||||
isZex: attrsObj?.['brand']?.name === 'zex',
|
||||
isYooneNew: attrsObj?.['brand']?.name === 'yoone' && attrsObj?.['version']?.name === 'new',
|
||||
size: this.extractNumberFromString(attrsObj?.['strength']?.name) || null,
|
||||
});
|
||||
return orderSale
|
||||
}).filter(v => v !== null)
|
||||
|
||||
if (orderSales.length > 0) {
|
||||
await this.orderSaleModel.save(orderSales);
|
||||
}
|
||||
}
|
||||
// // extract stren
|
||||
// extractNumberFromString(str: string): number {
|
||||
// if (!str) return 0;
|
||||
extractNumberFromString(str: string): number {
|
||||
if (!str) return 0;
|
||||
|
||||
// const num = parseInt(str, 10);
|
||||
// return isNaN(num) ? 0 : num;
|
||||
// }
|
||||
const num = parseInt(str, 10);
|
||||
return isNaN(num) ? 0 : num;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存订单退款信息
|
||||
|
|
@ -1246,13 +1227,13 @@ export class OrderService {
|
|||
parameters.push(siteId);
|
||||
}
|
||||
if (startDate) {
|
||||
sqlQuery += ` AND o.date_paid >= ?`;
|
||||
totalQuery += ` AND o.date_paid >= ?`;
|
||||
sqlQuery += ` AND o.date_created >= ?`;
|
||||
totalQuery += ` AND o.date_created >= ?`;
|
||||
parameters.push(startDate);
|
||||
}
|
||||
if (endDate) {
|
||||
sqlQuery += ` AND o.date_paid <= ?`;
|
||||
totalQuery += ` AND o.date_paid <= ?`;
|
||||
sqlQuery += ` AND o.date_created <= ?`;
|
||||
totalQuery += ` AND o.date_created <= ?`;
|
||||
parameters.push(endDate);
|
||||
}
|
||||
// 支付方式筛选(使用参数化,避免SQL注入)
|
||||
|
|
@ -1340,7 +1321,7 @@ export class OrderService {
|
|||
// 添加分页到主查询
|
||||
sqlQuery += `
|
||||
GROUP BY o.id
|
||||
ORDER BY o.date_paid DESC
|
||||
ORDER BY o.date_created DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`;
|
||||
parameters.push(pageSize, (current - 1) * pageSize);
|
||||
|
|
@ -1558,6 +1539,7 @@ export class OrderService {
|
|||
GROUP BY os.productId
|
||||
`;
|
||||
|
||||
console.log('------3.5-----', pcSql, pcParams, exceptPackage);
|
||||
const pcResults = await this.orderSaleModel.query(pcSql, pcParams);
|
||||
|
||||
const pcMap = new Map<number, any>();
|
||||
|
|
@ -1590,14 +1572,14 @@ export class OrderService {
|
|||
`;
|
||||
let yooneSql = `
|
||||
SELECT
|
||||
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '3mg' THEN os.quantity ELSE 0 END) AS yoone3Quantity,
|
||||
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '6mg' THEN os.quantity ELSE 0 END) AS yoone6Quantity,
|
||||
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '9mg' THEN os.quantity ELSE 0 END) AS yoone9Quantity,
|
||||
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '12mg' THEN os.quantity ELSE 0 END) AS yoone12Quantity,
|
||||
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '12mg' THEN os.quantity ELSE 0 END) AS yoone12QuantityNew,
|
||||
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '15mg' THEN os.quantity ELSE 0 END) AS yoone15Quantity,
|
||||
SUM(CASE WHEN os.brand = 'yoone' AND os.strength = '18mg' THEN os.quantity ELSE 0 END) AS yoone18Quantity,
|
||||
SUM(CASE WHEN os.brand = 'zex' THEN os.quantity ELSE 0 END) AS zexQuantity
|
||||
SUM(CASE WHEN os.isYoone = 1 AND os.size = 3 THEN os.quantity ELSE 0 END) AS yoone3Quantity,
|
||||
SUM(CASE WHEN os.isYoone = 1 AND os.size = 6 THEN os.quantity ELSE 0 END) AS yoone6Quantity,
|
||||
SUM(CASE WHEN os.isYoone = 1 AND os.size = 9 THEN os.quantity ELSE 0 END) AS yoone9Quantity,
|
||||
SUM(CASE WHEN os.isYoone = 1 AND os.size = 12 THEN os.quantity ELSE 0 END) AS yoone12Quantity,
|
||||
SUM(CASE WHEN os.isYooneNew = 1 AND os.size = 12 THEN os.quantity ELSE 0 END) AS yoone12QuantityNew,
|
||||
SUM(CASE WHEN os.isYoone = 1 AND os.size = 15 THEN os.quantity ELSE 0 END) AS yoone15Quantity,
|
||||
SUM(CASE WHEN os.isYoone = 1 AND os.size = 18 THEN os.quantity ELSE 0 END) AS yoone18Quantity,
|
||||
SUM(CASE WHEN os.isZex = 1 THEN os.quantity ELSE 0 END) AS zexQuantity
|
||||
FROM order_sale os
|
||||
INNER JOIN \`order\` o ON o.id = os.orderId
|
||||
WHERE o.date_paid BETWEEN ? AND ?
|
||||
|
|
@ -2525,7 +2507,7 @@ export class OrderService {
|
|||
const boxCount = items.reduce((total, item) => total + item.quantity, 0);
|
||||
|
||||
// 构建订单内容
|
||||
const orderContent = items.map(item => `${item.name} x ${item.quantity}`).join('; ');
|
||||
const orderContent = items.map(item => `${item.name} (${item.sku || ''}) x ${item.quantity}`).join('; ');
|
||||
|
||||
// 构建姓名地址
|
||||
const shipping = order.shipping;
|
||||
|
|
@ -2636,9 +2618,13 @@ export class OrderService {
|
|||
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;
|
||||
}
|
||||
|
||||
|
|
@ -2649,83 +2635,11 @@ export class OrderService {
|
|||
|
||||
return csvContent;
|
||||
} catch (error) {
|
||||
console.error('导出CSV时出错:', error);
|
||||
throw new Error(`导出CSV文件失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除每个分号前面一个左右括号和最后一个左右括号包含的内容(包括括号本身)
|
||||
* @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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Inject, Provide } from '@midwayjs/core';
|
||||
import { parse } from 'csv-parse';
|
||||
import * as fs from 'fs';
|
||||
import * as xlsx from 'xlsx';
|
||||
import { In, Like, Not, Repository } from 'typeorm';
|
||||
import { Product } from '../entity/product.entity';
|
||||
import { PaginationParams } from '../interface';
|
||||
|
|
@ -240,7 +240,7 @@ export class ProductService {
|
|||
const pageSize = query.per_page || 10;
|
||||
|
||||
// 处理搜索参数
|
||||
const name = query.where?.name || '';
|
||||
const name = query.where?.name || query.search || '';
|
||||
|
||||
// 处理品牌过滤
|
||||
const brandId = query.where?.brandId;
|
||||
|
|
@ -276,9 +276,19 @@ export class ProductService {
|
|||
qb.andWhere('product.id IN (:...ids)', { ids: query.where.ids });
|
||||
}
|
||||
|
||||
// 处理where对象中的id过滤
|
||||
if (query.where?.id) {
|
||||
qb.andWhere('product.id = :whereId', { whereId: query.where.id });
|
||||
}
|
||||
|
||||
// 处理where对象中的ids过滤
|
||||
if (query.where?.ids && query.where.ids.length > 0) {
|
||||
qb.andWhere('product.id IN (:...whereIds)', { whereIds: query.where.ids });
|
||||
}
|
||||
|
||||
// 处理SKU过滤
|
||||
if (query.where?.sku) {
|
||||
qb.andWhere('product.sku LIKE :sku', { sku: `%${query.where.sku}%` });
|
||||
qb.andWhere('product.sku = :sku', { sku: query.where.sku });
|
||||
}
|
||||
|
||||
// 处理SKU列表过滤
|
||||
|
|
@ -286,6 +296,16 @@ export class ProductService {
|
|||
qb.andWhere('product.sku IN (:...skus)', { skus: query.where.skus });
|
||||
}
|
||||
|
||||
// 处理where对象中的sku过滤
|
||||
if (query.where?.sku) {
|
||||
qb.andWhere('product.sku = :whereSku', { whereSku: query.where.sku });
|
||||
}
|
||||
|
||||
// 处理where对象中的skus过滤
|
||||
if (query.where?.skus && query.where.skus.length > 0) {
|
||||
qb.andWhere('product.sku IN (:...whereSkus)', { whereSkus: query.where.skus });
|
||||
}
|
||||
|
||||
// 处理产品中文名称过滤
|
||||
if (query.where?.nameCn) {
|
||||
qb.andWhere('product.nameCn LIKE :nameCn', { nameCn: `%${query.where.nameCn}%` });
|
||||
|
|
@ -296,6 +316,11 @@ export class ProductService {
|
|||
qb.andWhere('product.type = :type', { type: query.where.type });
|
||||
}
|
||||
|
||||
// 处理where对象中的type过滤
|
||||
if (query.where?.type) {
|
||||
qb.andWhere('product.type = :whereType', { whereType: query.where.type });
|
||||
}
|
||||
|
||||
// 处理价格范围过滤
|
||||
if (query.where?.minPrice !== undefined) {
|
||||
qb.andWhere('product.price >= :minPrice', { minPrice: query.where.minPrice });
|
||||
|
|
@ -305,6 +330,15 @@ export class ProductService {
|
|||
qb.andWhere('product.price <= :maxPrice', { maxPrice: query.where.maxPrice });
|
||||
}
|
||||
|
||||
// 处理where对象中的价格范围过滤
|
||||
if (query.where?.minPrice !== undefined) {
|
||||
qb.andWhere('product.price >= :whereMinPrice', { whereMinPrice: query.where.minPrice });
|
||||
}
|
||||
|
||||
if (query.where?.maxPrice !== undefined) {
|
||||
qb.andWhere('product.price <= :whereMaxPrice', { whereMaxPrice: query.where.maxPrice });
|
||||
}
|
||||
|
||||
// 处理促销价格范围过滤
|
||||
if (query.where?.minPromotionPrice !== undefined) {
|
||||
qb.andWhere('product.promotionPrice >= :minPromotionPrice', { minPromotionPrice: query.where.minPromotionPrice });
|
||||
|
|
@ -314,6 +348,15 @@ export class ProductService {
|
|||
qb.andWhere('product.promotionPrice <= :maxPromotionPrice', { maxPromotionPrice: query.where.maxPromotionPrice });
|
||||
}
|
||||
|
||||
// 处理where对象中的促销价格范围过滤
|
||||
if (query.where?.minPromotionPrice !== undefined) {
|
||||
qb.andWhere('product.promotionPrice >= :whereMinPromotionPrice', { whereMinPromotionPrice: query.where.minPromotionPrice });
|
||||
}
|
||||
|
||||
if (query.where?.maxPromotionPrice !== undefined) {
|
||||
qb.andWhere('product.promotionPrice <= :whereMaxPromotionPrice', { whereMaxPromotionPrice: query.where.maxPromotionPrice });
|
||||
}
|
||||
|
||||
// 处理创建时间范围过滤
|
||||
if (query.where?.createdAtStart) {
|
||||
qb.andWhere('product.createdAt >= :createdAtStart', { createdAtStart: new Date(query.where.createdAtStart) });
|
||||
|
|
@ -323,6 +366,15 @@ export class ProductService {
|
|||
qb.andWhere('product.createdAt <= :createdAtEnd', { createdAtEnd: new Date(query.where.createdAtEnd) });
|
||||
}
|
||||
|
||||
// 处理where对象中的创建时间范围过滤
|
||||
if (query.where?.createdAtStart) {
|
||||
qb.andWhere('product.createdAt >= :whereCreatedAtStart', { whereCreatedAtStart: new Date(query.where.createdAtStart) });
|
||||
}
|
||||
|
||||
if (query.where?.createdAtEnd) {
|
||||
qb.andWhere('product.createdAt <= :whereCreatedAtEnd', { whereCreatedAtEnd: new Date(query.where.createdAtEnd) });
|
||||
}
|
||||
|
||||
// 处理更新时间范围过滤
|
||||
if (query.where?.updatedAtStart) {
|
||||
qb.andWhere('product.updatedAt >= :updatedAtStart', { updatedAtStart: new Date(query.where.updatedAtStart) });
|
||||
|
|
@ -332,40 +384,14 @@ export class ProductService {
|
|||
qb.andWhere('product.updatedAt <= :updatedAtEnd', { updatedAtEnd: new Date(query.where.updatedAtEnd) });
|
||||
}
|
||||
|
||||
// 处理属性过滤
|
||||
const attributeFilters = query.where?.attributes || {};
|
||||
Object.entries(attributeFilters).forEach(([attributeName, value], index) => {
|
||||
if (value === 'hasValue') {
|
||||
// 如果值为'hasValue',则过滤出具有该属性的产品
|
||||
qb.andWhere(qb => {
|
||||
const subQuery = qb
|
||||
.subQuery()
|
||||
.select('product_attributes_dict_item.productId')
|
||||
.from('product_attributes_dict_item', 'product_attributes_dict_item')
|
||||
.innerJoin('dict_item', 'dict_item', 'product_attributes_dict_item.dictItemId = dict_item.id')
|
||||
.innerJoin('dict', 'dict', 'dict_item.dict_id = dict.id')
|
||||
.where('dict.name = :attributeName', {
|
||||
attributeName,
|
||||
})
|
||||
.getQuery();
|
||||
return 'product.id IN ' + subQuery;
|
||||
});
|
||||
} else if (typeof value === 'number' || !isNaN(Number(value))) {
|
||||
// 如果值是数字,则过滤出该属性等于该值的产品
|
||||
const attributeValueId = Number(value);
|
||||
qb.andWhere(qb => {
|
||||
const subQuery = qb
|
||||
.subQuery()
|
||||
.select('product_attributes_dict_item.productId')
|
||||
.from('product_attributes_dict_item', 'product_attributes_dict_item')
|
||||
.where('product_attributes_dict_item.dictItemId = :attributeValueId', {
|
||||
attributeValueId,
|
||||
})
|
||||
.getQuery();
|
||||
return 'product.id IN ' + subQuery;
|
||||
});
|
||||
// 处理where对象中的更新时间范围过滤
|
||||
if (query.where?.updatedAtStart) {
|
||||
qb.andWhere('product.updatedAt >= :whereUpdatedAtStart', { whereUpdatedAtStart: new Date(query.where.updatedAtStart) });
|
||||
}
|
||||
|
||||
if (query.where?.updatedAtEnd) {
|
||||
qb.andWhere('product.updatedAt <= :whereUpdatedAtEnd', { whereUpdatedAtEnd: new Date(query.where.updatedAtEnd) });
|
||||
}
|
||||
});
|
||||
|
||||
// 品牌过滤(向后兼容)
|
||||
if (brandId) {
|
||||
|
|
@ -407,6 +433,16 @@ export class ProductService {
|
|||
qb.andWhere('product.categoryId IN (:...categoryIds)', { categoryIds });
|
||||
}
|
||||
|
||||
// 处理where对象中的分类ID过滤
|
||||
if (query.where?.categoryId) {
|
||||
qb.andWhere('product.categoryId = :whereCategoryId', { whereCategoryId: query.where.categoryId });
|
||||
}
|
||||
|
||||
// 处理where对象中的分类ID列表过滤
|
||||
if (query.where?.categoryIds && query.where.categoryIds.length > 0) {
|
||||
qb.andWhere('product.categoryId IN (:...whereCategoryIds)', { whereCategoryIds: query.where.categoryIds });
|
||||
}
|
||||
|
||||
// 处理排序(支持新旧两种格式)
|
||||
if (orderBy) {
|
||||
if (typeof orderBy === 'string') {
|
||||
|
|
@ -468,299 +504,6 @@ export class ProductService {
|
|||
};
|
||||
}
|
||||
|
||||
async getProductListGrouped(query: UnifiedSearchParamsDTO<ProductWhereFilter>): Promise<Record<string, Product[]>> {
|
||||
// 创建查询构建器
|
||||
const qb = this.productModel
|
||||
.createQueryBuilder('product')
|
||||
.leftJoinAndSelect('product.attributes', 'attribute')
|
||||
.leftJoinAndSelect('attribute.dict', 'dict')
|
||||
.leftJoinAndSelect('product.category', 'category');
|
||||
|
||||
// 验证分组字段
|
||||
const groupBy = query.groupBy;
|
||||
if (!groupBy) {
|
||||
throw new Error('分组字段不能为空');
|
||||
}
|
||||
|
||||
// 处理搜索参数
|
||||
const name = query.where?.name || '';
|
||||
|
||||
// 处理品牌过滤
|
||||
const brandId = query.where?.brandId;
|
||||
const brandIds = query.where?.brandIds;
|
||||
|
||||
// 处理分类过滤
|
||||
const categoryId = query.where?.categoryId;
|
||||
const categoryIds = query.where?.categoryIds;
|
||||
|
||||
// 处理排序参数
|
||||
const orderBy = query.orderBy;
|
||||
|
||||
// 模糊搜索 name,支持多个关键词
|
||||
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||||
if (nameFilter.length > 0) {
|
||||
const nameConditions = nameFilter
|
||||
.map((word, index) => `product.name LIKE :name${index}`)
|
||||
.join(' AND ');
|
||||
const nameParams = nameFilter.reduce(
|
||||
(params, word, index) => ({ ...params, [`name${index}`]: `%${word}%` }),
|
||||
{}
|
||||
);
|
||||
qb.where(`(${nameConditions})`, nameParams);
|
||||
}
|
||||
|
||||
// 处理产品ID过滤
|
||||
if (query.where?.id) {
|
||||
qb.andWhere('product.id = :id', { id: query.where.id });
|
||||
}
|
||||
|
||||
// 处理产品ID列表过滤
|
||||
if (query.where?.ids && query.where.ids.length > 0) {
|
||||
qb.andWhere('product.id IN (:...ids)', { ids: query.where.ids });
|
||||
}
|
||||
|
||||
// 处理SKU过滤
|
||||
if (query.where?.sku) {
|
||||
qb.andWhere('product.sku LIKE :sku', { sku: `%${query.where.sku}%` });
|
||||
}
|
||||
|
||||
// 处理SKU列表过滤
|
||||
if (query.where?.skus && query.where.skus.length > 0) {
|
||||
qb.andWhere('product.sku IN (:...skus)', { skus: query.where.skus });
|
||||
}
|
||||
|
||||
// 处理产品中文名称过滤
|
||||
if (query.where?.nameCn) {
|
||||
qb.andWhere('product.nameCn LIKE :nameCn', { nameCn: `%${query.where.nameCn}%` });
|
||||
}
|
||||
|
||||
// 处理产品类型过滤
|
||||
if (query.where?.type) {
|
||||
qb.andWhere('product.type = :type', { type: query.where.type });
|
||||
}
|
||||
|
||||
// 处理价格范围过滤
|
||||
if (query.where?.minPrice !== undefined) {
|
||||
qb.andWhere('product.price >= :minPrice', { minPrice: query.where.minPrice });
|
||||
}
|
||||
|
||||
if (query.where?.maxPrice !== undefined) {
|
||||
qb.andWhere('product.price <= :maxPrice', { maxPrice: query.where.maxPrice });
|
||||
}
|
||||
|
||||
// 处理促销价格范围过滤
|
||||
if (query.where?.minPromotionPrice !== undefined) {
|
||||
qb.andWhere('product.promotionPrice >= :minPromotionPrice', { minPromotionPrice: query.where.minPromotionPrice });
|
||||
}
|
||||
|
||||
if (query.where?.maxPromotionPrice !== undefined) {
|
||||
qb.andWhere('product.promotionPrice <= :maxPromotionPrice', { maxPromotionPrice: query.where.maxPromotionPrice });
|
||||
}
|
||||
|
||||
// 处理创建时间范围过滤
|
||||
if (query.where?.createdAtStart) {
|
||||
qb.andWhere('product.createdAt >= :createdAtStart', { createdAtStart: new Date(query.where.createdAtStart) });
|
||||
}
|
||||
|
||||
if (query.where?.createdAtEnd) {
|
||||
qb.andWhere('product.createdAt <= :createdAtEnd', { createdAtEnd: new Date(query.where.createdAtEnd) });
|
||||
}
|
||||
|
||||
// 处理更新时间范围过滤
|
||||
if (query.where?.updatedAtStart) {
|
||||
qb.andWhere('product.updatedAt >= :updatedAtStart', { updatedAtStart: new Date(query.where.updatedAtStart) });
|
||||
}
|
||||
|
||||
if (query.where?.updatedAtEnd) {
|
||||
qb.andWhere('product.updatedAt <= :updatedAtEnd', { updatedAtEnd: new Date(query.where.updatedAtEnd) });
|
||||
}
|
||||
|
||||
// 处理属性过滤
|
||||
const attributeFilters = query.where?.attributes || {};
|
||||
Object.entries(attributeFilters).forEach(([attributeName, value], index) => {
|
||||
if (value === 'hasValue') {
|
||||
// 如果值为'hasValue',则过滤出具有该属性的产品
|
||||
qb.andWhere(qb => {
|
||||
const subQuery = qb
|
||||
.subQuery()
|
||||
.select('product_attributes_dict_item.productId')
|
||||
.from('product_attributes_dict_item', 'product_attributes_dict_item')
|
||||
.innerJoin('dict_item', 'dict_item', 'product_attributes_dict_item.dictItemId = dict_item.id')
|
||||
.innerJoin('dict', 'dict', 'dict_item.dict_id = dict.id')
|
||||
.where('dict.name = :attributeName', {
|
||||
attributeName,
|
||||
})
|
||||
.getQuery();
|
||||
return 'product.id IN ' + subQuery;
|
||||
});
|
||||
} else if (typeof value === 'number' || !isNaN(Number(value))) {
|
||||
// 如果值是数字,则过滤出该属性等于该值的产品
|
||||
const attributeValueId = Number(value);
|
||||
qb.andWhere(qb => {
|
||||
const subQuery = qb
|
||||
.subQuery()
|
||||
.select('product_attributes_dict_item.productId')
|
||||
.from('product_attributes_dict_item', 'product_attributes_dict_item')
|
||||
.where('product_attributes_dict_item.dictItemId = :attributeValueId', {
|
||||
attributeValueId,
|
||||
})
|
||||
.getQuery();
|
||||
return 'product.id IN ' + subQuery;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 品牌过滤(向后兼容)
|
||||
if (brandId) {
|
||||
qb.andWhere(qb => {
|
||||
const subQuery = qb
|
||||
.subQuery()
|
||||
.select('product_attributes_dict_item.productId')
|
||||
.from('product_attributes_dict_item', 'product_attributes_dict_item')
|
||||
.where('product_attributes_dict_item.dictItemId = :brandId', {
|
||||
brandId,
|
||||
})
|
||||
.getQuery();
|
||||
return 'product.id IN ' + subQuery;
|
||||
});
|
||||
}
|
||||
|
||||
// 处理品牌ID列表过滤
|
||||
if (brandIds && brandIds.length > 0) {
|
||||
qb.andWhere(qb => {
|
||||
const subQuery = qb
|
||||
.subQuery()
|
||||
.select('product_attributes_dict_item.productId')
|
||||
.from('product_attributes_dict_item', 'product_attributes_dict_item')
|
||||
.where('product_attributes_dict_item.dictItemId IN (:...brandIds)', {
|
||||
brandIds,
|
||||
})
|
||||
.getQuery();
|
||||
return 'product.id IN ' + subQuery;
|
||||
});
|
||||
}
|
||||
|
||||
// 分类过滤(向后兼容)
|
||||
if (categoryId) {
|
||||
qb.andWhere('product.categoryId = :categoryId', { categoryId });
|
||||
}
|
||||
|
||||
// 处理分类ID列表过滤
|
||||
if (categoryIds && categoryIds.length > 0) {
|
||||
qb.andWhere('product.categoryId IN (:...categoryIds)', { categoryIds });
|
||||
}
|
||||
|
||||
// 处理排序(支持新旧两种格式)
|
||||
if (orderBy) {
|
||||
if (typeof orderBy === 'string') {
|
||||
// 如果orderBy是字符串,尝试解析JSON
|
||||
try {
|
||||
const orderByObj = JSON.parse(orderBy);
|
||||
Object.keys(orderByObj).forEach(key => {
|
||||
const order = orderByObj[key].toUpperCase();
|
||||
const allowedSortFields = ['price', 'promotionPrice', 'createdAt', 'updatedAt', 'sku', 'name'];
|
||||
if (allowedSortFields.includes(key)) {
|
||||
qb.addOrderBy(`product.${key}`, order as 'ASC' | 'DESC');
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
// 解析失败,使用默认排序
|
||||
qb.orderBy('product.createdAt', 'DESC');
|
||||
}
|
||||
} else if (typeof orderBy === 'object') {
|
||||
// 如果orderBy是对象,直接使用
|
||||
Object.keys(orderBy).forEach(key => {
|
||||
const order = orderBy[key].toUpperCase();
|
||||
const allowedSortFields = ['price', 'promotionPrice', 'createdAt', 'updatedAt', 'sku', 'name'];
|
||||
if (allowedSortFields.includes(key)) {
|
||||
qb.addOrderBy(`product.${key}`, order as 'ASC' | 'DESC');
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
qb.orderBy('product.createdAt', 'DESC');
|
||||
}
|
||||
|
||||
// 执行查询
|
||||
const items = await qb.getMany();
|
||||
|
||||
// 根据类型填充组成信息
|
||||
for (const product of items) {
|
||||
if (product.type === 'single') {
|
||||
// 单品不持久化组成,这里仅返回一个基于 SKU 的虚拟组成
|
||||
const component = new ProductStockComponent();
|
||||
component.productId = product.id;
|
||||
component.sku = product.sku;
|
||||
component.quantity = 1;
|
||||
product.components = [component];
|
||||
} else {
|
||||
// 混装商品返回持久化的 SKU 组成
|
||||
product.components = await this.productStockComponentModel.find({
|
||||
where: { productId: product.id },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 按指定字段分组
|
||||
const groupedResult: Record<string, Product[]> = {};
|
||||
|
||||
// 检查是否按属性的字典名称分组
|
||||
const isAttributeGrouping = await this.dictModel.findOne({ where: { name: groupBy } });
|
||||
|
||||
if (isAttributeGrouping) {
|
||||
// 使用原生SQL查询获取每个产品对应的分组属性值
|
||||
const attributeGroupQuery = `
|
||||
SELECT product.id as productId, dict_item.id as attributeId, dict_item.name as attributeName, dict_item.title as attributeTitle
|
||||
FROM product
|
||||
INNER JOIN product_attributes_dict_item ON product.id = product_attributes_dict_item.productId
|
||||
INNER JOIN dict_item ON product_attributes_dict_item.dictItemId = dict_item.id
|
||||
INNER JOIN dict ON dict_item.dict_id = dict.id
|
||||
WHERE dict.name = ?
|
||||
`;
|
||||
|
||||
const attributeGroupResults = await this.productModel.query(attributeGroupQuery, [groupBy]);
|
||||
|
||||
// 创建产品ID到分组值的映射
|
||||
const productGroupMap: Record<number, string> = {};
|
||||
attributeGroupResults.forEach((result: any) => {
|
||||
productGroupMap[result.productId] = result.attributeName;
|
||||
});
|
||||
|
||||
items.forEach(product => {
|
||||
// 获取分组值
|
||||
const groupValue = productGroupMap[product.id] || 'unknown';
|
||||
// 转换为字符串作为键
|
||||
const groupKey = String(groupValue);
|
||||
|
||||
// 初始化分组
|
||||
if (!groupedResult[groupKey]) {
|
||||
groupedResult[groupKey] = [];
|
||||
}
|
||||
|
||||
// 添加产品到分组
|
||||
groupedResult[groupKey].push(product);
|
||||
});
|
||||
} else {
|
||||
// 按产品自身字段分组
|
||||
items.forEach(product => {
|
||||
// 获取分组值
|
||||
const groupValue = product[groupBy as keyof Product];
|
||||
// 转换为字符串作为键
|
||||
const groupKey = String(groupValue);
|
||||
|
||||
// 初始化分组
|
||||
if (!groupedResult[groupKey]) {
|
||||
groupedResult[groupKey] = [];
|
||||
}
|
||||
|
||||
// 添加产品到分组
|
||||
groupedResult[groupKey].push(product);
|
||||
});
|
||||
}
|
||||
|
||||
return groupedResult;
|
||||
}
|
||||
|
||||
async getOrCreateAttribute(
|
||||
dictName: string,
|
||||
itemTitle: string,
|
||||
|
|
@ -791,8 +534,9 @@ export class ProductService {
|
|||
return item;
|
||||
}
|
||||
|
||||
|
||||
async createProduct(createProductDTO: CreateProductDTO): Promise<Product> {
|
||||
const { attributes, sku, categoryId, categoryName, type } = createProductDTO;
|
||||
const { attributes, sku, categoryId, type } = createProductDTO;
|
||||
|
||||
// 条件判断(校验属性输入)
|
||||
// 当产品类型为 'bundle' 时,attributes 可以为空
|
||||
|
|
@ -803,38 +547,47 @@ export class ProductService {
|
|||
}
|
||||
}
|
||||
|
||||
const safeAttributes = attributes || [];
|
||||
|
||||
// 解析属性输入(按 id 或 dictName 创建/关联字典项)
|
||||
const resolvedAttributes: DictItem[] = [];
|
||||
let categoryItem: Category | null = null;
|
||||
|
||||
// 如果提供了 categoryId,设置分类
|
||||
if (categoryId) {
|
||||
categoryItem = await this.categoryModel.findOne({
|
||||
where: { id: categoryId },
|
||||
relations: ['attributes', 'attributes.attributeDict']
|
||||
});
|
||||
if (!categoryItem) throw new Error(`分类 ID ${categoryId} 不存在`);
|
||||
}
|
||||
if (!categoryItem && categoryName) {
|
||||
categoryItem = await this.categoryModel.findOne({
|
||||
where: { name: categoryName },
|
||||
relations: ['attributes', 'attributes.attributeDict']
|
||||
});
|
||||
}
|
||||
if (!categoryItem && categoryName) {
|
||||
const category = new Category();
|
||||
category.name = categoryName || '';
|
||||
category.title = categoryName || '';
|
||||
const savedCategory = await this.categoryModel.save(category);
|
||||
categoryItem = await this.categoryModel.findOne({
|
||||
where: { id: savedCategory.id },
|
||||
relations: ['attributes', 'attributes.attributeDict']
|
||||
});
|
||||
if (!categoryItem) throw new Error(`分类名称 ${categoryName} 不存在`);
|
||||
}
|
||||
// 创造一定要有商品分类
|
||||
if (!categoryItem) throw new Error('必须提供分类 ID 或分类名称');
|
||||
|
||||
const resolvedAttributes: DictItem[] = [];
|
||||
const safeAttributes = attributes || [];
|
||||
for (const attr of safeAttributes) {
|
||||
// 如果属性是分类,特殊处理
|
||||
if (attr.dictName === 'category') {
|
||||
if (attr.id) {
|
||||
categoryItem = await this.categoryModel.findOne({
|
||||
where: { id: attr.id },
|
||||
relations: ['attributes', 'attributes.attributeDict']
|
||||
});
|
||||
} else if (attr.name) {
|
||||
categoryItem = await this.categoryModel.findOne({
|
||||
where: { name: attr.name },
|
||||
relations: ['attributes', 'attributes.attributeDict']
|
||||
});
|
||||
} else if (attr.title) {
|
||||
// 尝试用 title 匹配 name 或 title
|
||||
categoryItem = await this.categoryModel.findOne({
|
||||
where: [
|
||||
{ name: attr.title },
|
||||
{ title: attr.title }
|
||||
],
|
||||
relations: ['attributes', 'attributes.attributeDict']
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let item: DictItem | null = null;
|
||||
if (attr.id) {
|
||||
// 如果传入了 id,直接查找字典项并使用,不强制要求 dictName
|
||||
|
|
@ -910,46 +663,27 @@ export class ProductService {
|
|||
}
|
||||
|
||||
// 使用 merge 更新基础字段,排除特殊处理字段
|
||||
const { attributes, categoryId, categoryName, sku, components, ...simpleFields } = updateProductDTO;
|
||||
const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, ...simpleFields } = updateProductDTO;
|
||||
this.productModel.merge(product, simpleFields);
|
||||
// 解析属性输入(按 id 或 dictName 创建/关联字典项)
|
||||
let categoryItem: Category | null = null;
|
||||
// 如果提供了 categoryId,设置分类
|
||||
if (categoryId) {
|
||||
categoryItem = await this.categoryModel.findOne({
|
||||
where: { id: categoryId },
|
||||
relations: ['attributes', 'attributes.attributeDict']
|
||||
});
|
||||
|
||||
// 处理分类更新
|
||||
if (updateProductDTO.categoryId !== undefined) {
|
||||
if (updateProductDTO.categoryId) {
|
||||
const categoryItem = await this.categoryModel.findOne({ where: { id: updateProductDTO.categoryId } });
|
||||
if (!categoryItem) throw new Error(`分类 ID ${updateProductDTO.categoryId} 不存在`);
|
||||
product.category = categoryItem;
|
||||
} else {
|
||||
// 如果传了 0 或 null,可以清除分类(根据需求)
|
||||
// product.category = null;
|
||||
}
|
||||
if (!categoryItem && categoryName) {
|
||||
categoryItem = await this.categoryModel.findOne({
|
||||
where: { name: categoryName },
|
||||
relations: ['attributes', 'attributes.attributeDict']
|
||||
});
|
||||
}
|
||||
function nameToTitle(name: string) {
|
||||
return name.replace('-', ' ');
|
||||
}
|
||||
if (!categoryItem && categoryName) {
|
||||
const category = new Category();
|
||||
category.name = categoryName || '';
|
||||
category.title = nameToTitle(categoryName || '');
|
||||
const savedCategory = await this.categoryModel.save(category);
|
||||
categoryItem = await this.categoryModel.findOne({
|
||||
where: { id: savedCategory.id },
|
||||
relations: ['attributes', 'attributes.attributeDict']
|
||||
});
|
||||
if (!categoryItem) throw new Error(`分类名称 ${categoryName} 不存在`);
|
||||
}
|
||||
// 创造一定要有商品分类
|
||||
if (!categoryItem) throw new Error('必须提供分类 ID 或分类名称');
|
||||
product.categoryId = categoryItem.id;
|
||||
|
||||
// 处理 SKU 更新
|
||||
if (updateProductDTO.sku !== undefined) {
|
||||
// 校验 SKU 唯一性(如变更)
|
||||
const newSku = updateProductDTO.sku;
|
||||
if (newSku && newSku !== product.sku) {
|
||||
const exist = await this.productModel.findOne({ where: { id: Not(id), sku: newSku } });
|
||||
const exist = await this.productModel.findOne({ where: { sku: newSku } });
|
||||
if (exist) {
|
||||
throw new Error('SKU 已存在,请更换后重试');
|
||||
}
|
||||
|
|
@ -967,6 +701,14 @@ export class ProductService {
|
|||
};
|
||||
|
||||
for (const attr of updateProductDTO.attributes) {
|
||||
// 如果属性是分类,特殊处理
|
||||
if (attr.dictName === 'category') {
|
||||
if (attr.id) {
|
||||
const categoryItem = await this.categoryModel.findOneBy({ id: attr.id });
|
||||
if (categoryItem) product.category = categoryItem;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let item: DictItem | null = null;
|
||||
if (attr.id) {
|
||||
|
|
@ -1032,7 +774,7 @@ export class ProductService {
|
|||
}
|
||||
} else {
|
||||
// 简单字段,直接批量更新以提高性能
|
||||
// UpdateProductDTO 里的简单字段: name, nameCn, description, shortDescription, price, promotionPrice, image, siteSkus
|
||||
// UpdateProductDTO 里的简单字段: name, nameCn, description, price, promotionPrice, siteSkus
|
||||
|
||||
const simpleUpdate: any = {};
|
||||
if (updateData.name !== undefined) simpleUpdate.name = updateData.name;
|
||||
|
|
@ -1041,7 +783,6 @@ export class ProductService {
|
|||
if (updateData.shortDescription !== undefined) simpleUpdate.shortDescription = updateData.shortDescription;
|
||||
if (updateData.price !== undefined) simpleUpdate.price = updateData.price;
|
||||
if (updateData.promotionPrice !== undefined) simpleUpdate.promotionPrice = updateData.promotionPrice;
|
||||
if (updateData.image !== undefined) simpleUpdate.image = updateData.image;
|
||||
if (updateData.siteSkus !== undefined) simpleUpdate.siteSkus = updateData.siteSkus;
|
||||
|
||||
if (Object.keys(simpleUpdate).length > 0) {
|
||||
|
|
@ -1677,8 +1418,7 @@ export class ProductService {
|
|||
}
|
||||
|
||||
// 将单条 CSV 记录转换为数据对象
|
||||
mapTableRecordToProduct(rec: any): CreateProductDTO | UpdateProductDTO | null {
|
||||
const keys = Object.keys(rec);
|
||||
transformCsvRecordToData(rec: any): CreateProductDTO & { sku: string } | null {
|
||||
// 必须包含 sku
|
||||
const sku: string = (rec.sku || '').trim();
|
||||
if (!sku) {
|
||||
|
|
@ -1699,105 +1439,43 @@ export class ProductService {
|
|||
};
|
||||
|
||||
// 解析属性字段(分号分隔多值)
|
||||
const parseList = (v: string) => (v && String(v).split(/[;,]/).map(s => s.trim()).filter(Boolean));
|
||||
const parseList = (v: string) => (v ? String(v).split(';').map(s => s.trim()).filter(Boolean) : []);
|
||||
|
||||
// 将属性解析为 DTO 输入
|
||||
const attributes: any[] = [];
|
||||
|
||||
// 处理动态属性字段 (attribute_*)
|
||||
for (const key of keys) {
|
||||
for (const key of Object.keys(rec)) {
|
||||
if (key.startsWith('attribute_')) {
|
||||
const dictName = key.replace('attribute_', '');
|
||||
if (dictName) {
|
||||
const list = parseList(rec[key]) || [];
|
||||
const list = parseList(rec[key]);
|
||||
for (const item of list) attributes.push({ dictName, title: item });
|
||||
}
|
||||
}
|
||||
}
|
||||
// 目前的 components 由 component_{index}_sku和component_{index}_quantity组成
|
||||
const component_sku_keys = keys.filter(key => key.startsWith('component_') && key.endsWith('_sku'));
|
||||
const components = [];
|
||||
for (const key of component_sku_keys) {
|
||||
const index = key.replace('component_', '').replace('_sku', '');
|
||||
if (index) {
|
||||
const sku = val(rec[`component_${index}_sku`]);
|
||||
const quantity = num(rec[`component_${index}_quantity`]);
|
||||
if (sku && quantity) {
|
||||
components.push({ sku, quantity });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理分类字段
|
||||
const categoryName = val(rec.category);
|
||||
const category = val(rec.category);
|
||||
|
||||
return {
|
||||
sku,
|
||||
name: val(rec.name),
|
||||
nameCn: val(rec.nameCn),
|
||||
image: val(rec.image),
|
||||
description: val(rec.description),
|
||||
shortDescription: val(rec.shortDescription),
|
||||
price: num(rec.price),
|
||||
promotionPrice: num(rec.promotionPrice),
|
||||
type: val(rec.type),
|
||||
siteSkus: rec.siteSkus ? parseList(rec.siteSkus) : undefined,
|
||||
categoryName, // 添加分类字段
|
||||
components,
|
||||
siteSkus: rec.siteSkus
|
||||
? String(rec.siteSkus)
|
||||
.split(/[;,]/) // 支持英文分号或英文逗号分隔
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean)
|
||||
: undefined,
|
||||
category, // 添加分类字段
|
||||
|
||||
attributes: attributes.length > 0 ? attributes : undefined,
|
||||
}
|
||||
}
|
||||
isMixedSku(sku: string){
|
||||
const splitSKu = sku.split('-')
|
||||
const last = splitSKu[splitSKu.length - 1]
|
||||
const second = splitSKu[splitSKu.length - 2]
|
||||
// 这里判断 second 是否是数字
|
||||
return sku.includes('-MX-') || sku.includes('-Mixed-') || /^\d+$/.test(second) && /^\d+$/.test(last)
|
||||
}
|
||||
async getComponentDetailFromSiteSku(siteProduct: { sku: string, name: string }) {
|
||||
if (!siteProduct.sku) {
|
||||
throw new Error('siteSku 不能为空')
|
||||
}
|
||||
|
||||
let product = await this.productModel.findOne({
|
||||
where: { siteSkus: Like(`%${siteProduct.sku}%`) },
|
||||
relations: ['components', 'attributes', 'attributes.dict'],
|
||||
});
|
||||
let quantity = 1;
|
||||
// 这里处理一下特殊情况,就是无法直接通过 siteProduct.sku去获取, 但有一定规则转换成有的产品,就是 bundle 的部分
|
||||
// 考察各个站点的 bundle 规则, 会发现
|
||||
// wordpress:
|
||||
// togovape YOONE Wintergreen 9MG (Moisture) - 10 cans TV-YOONE-NP-S-WG-9MG-0010
|
||||
// togovape mixed 是这样的 TV-YOONE-NP-G-12MG-MX-0003 TV-ZEX-NP-Mixed-12MG-0001
|
||||
//
|
||||
// shopyy: shopyy 已经
|
||||
// 只有 bundle 做这个处理
|
||||
if (!product && !this.isMixedSku(siteProduct.sku)) {
|
||||
const skuSplitArr = siteProduct.sku.split('-')
|
||||
const quantityStr = skuSplitArr[skuSplitArr.length - 1]
|
||||
const isBundleSku = quantityStr.startsWith('0')
|
||||
if(!isBundleSku){
|
||||
return undefined
|
||||
}
|
||||
quantity = Number(quantityStr)
|
||||
if(!isBundleSku){
|
||||
return undefined
|
||||
}
|
||||
// 更正为正确的站点 sku
|
||||
const childSku = skuSplitArr.slice(0, skuSplitArr.length - 1).join('-')
|
||||
// 重新获取匹配的商品
|
||||
product = await this.productModel.findOne({
|
||||
where: { siteSkus: Like(`%${childSku}%`) },
|
||||
relations: ['components', 'attributes', 'attributes.dict'],
|
||||
});
|
||||
}
|
||||
|
||||
if (!product) {
|
||||
throw new Error(`产品 ${siteProduct.sku} 不存在`);
|
||||
}
|
||||
return {
|
||||
product,
|
||||
quantity,
|
||||
}
|
||||
} as any;
|
||||
}
|
||||
|
||||
// 准备创建产品的 DTO, 处理类型转换和默认值
|
||||
|
|
@ -1985,20 +1663,15 @@ export class ProductService {
|
|||
rows.push(rowData.join(','));
|
||||
}
|
||||
|
||||
// 添加UTF-8 BOM以确保中文在Excel中正确显示
|
||||
return '\ufeff' + rows.join('\n');
|
||||
|
||||
return rows.join('\n');
|
||||
}
|
||||
async getRecordsFromTable(file: any) {
|
||||
// 解析文件(使用 xlsx 包自动识别文件类型并解析)
|
||||
try {
|
||||
let buffer: Buffer;
|
||||
|
||||
// 处理文件输入,获取 buffer
|
||||
// 从 CSV 导入产品;存在则更新,不存在则创建
|
||||
async importProductsCSV(file: any): Promise<BatchOperationResult> {
|
||||
let buffer: Buffer;
|
||||
if (Buffer.isBuffer(file)) {
|
||||
buffer = file;
|
||||
}
|
||||
else if (file?.data) {
|
||||
} else if (file?.data) {
|
||||
if (typeof file.data === 'string') {
|
||||
buffer = fs.readFileSync(file.data);
|
||||
} else {
|
||||
|
|
@ -2008,35 +1681,39 @@ export class ProductService {
|
|||
throw new Error('无效的文件输入');
|
||||
}
|
||||
|
||||
let records: any[] = []
|
||||
// xlsx 包会自动根据文件内容识别文件类型(CSV 或 XLSX)
|
||||
// 添加codepage: 65001以确保正确处理UTF-8编码的中文
|
||||
const workbook = xlsx.read(buffer, { type: 'buffer', codepage: 65001 });
|
||||
// 获取第一个工作表
|
||||
const worksheet = workbook.Sheets[workbook.SheetNames[0]];
|
||||
// 将工作表转换为 JSON 数组
|
||||
records = xlsx.utils.sheet_to_json(worksheet);
|
||||
|
||||
// 解析 CSV(使用 csv-parse/sync 按表头解析)
|
||||
let records: any[] = [];
|
||||
try {
|
||||
records = await new Promise((resolve, reject) => {
|
||||
parse(buffer, {
|
||||
columns: true,
|
||||
skip_empty_lines: true,
|
||||
trim: true,
|
||||
bom: true,
|
||||
}, (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(data);
|
||||
}
|
||||
});
|
||||
})
|
||||
console.log('Parsed records count:', records.length);
|
||||
if (records.length > 0) {
|
||||
console.log('First record keys:', Object.keys(records[0]));
|
||||
}
|
||||
return records;
|
||||
} catch (e: any) {
|
||||
throw new Error(`文件解析失败:${e?.message || e}`);
|
||||
}
|
||||
throw new Error(`CSV 解析失败:${e?.message || e}`)
|
||||
}
|
||||
|
||||
// 从 CSV 导入产品;存在则更新,不存在则创建
|
||||
async importProductsFromTable(file: any): Promise<BatchOperationResult> {
|
||||
let created = 0;
|
||||
let updated = 0;
|
||||
const errors: BatchErrorItem[] = [];
|
||||
const records = await this.getRecordsFromTable(file);
|
||||
|
||||
// 逐条处理记录
|
||||
for (const rec of records) {
|
||||
try {
|
||||
const data = this.mapTableRecordToProduct(rec);
|
||||
const data = this.transformCsvRecordToData(rec);
|
||||
if (!data) {
|
||||
errors.push({ identifier: data.sku, error: '缺少 SKU 的记录已跳过'});
|
||||
continue;
|
||||
|
|
@ -2044,17 +1721,17 @@ export class ProductService {
|
|||
const { sku } = data;
|
||||
|
||||
// 查找现有产品
|
||||
const exist = await this.productModel.findOne({ where: { sku } });
|
||||
const exist = await this.productModel.findOne({ where: { sku }, relations: ['attributes', 'attributes.dict'] });
|
||||
|
||||
if (!exist) {
|
||||
// 创建新产品
|
||||
// const createDTO = this.prepareCreateProductDTO(data);
|
||||
await this.createProduct(data as CreateProductDTO)
|
||||
const createDTO = this.prepareCreateProductDTO(data);
|
||||
await this.createProduct(createDTO);
|
||||
created += 1;
|
||||
} else {
|
||||
// 更新产品
|
||||
// const updateDTO = this.prepareUpdateProductDTO(data);
|
||||
await this.updateProduct(exist.id, data);
|
||||
const updateDTO = this.prepareUpdateProductDTO(data);
|
||||
await this.updateProduct(exist.id, updateDTO);
|
||||
updated += 1;
|
||||
}
|
||||
} catch (e: any) {
|
||||
|
|
@ -2397,111 +2074,4 @@ export class ProductService {
|
|||
|
||||
return unifiedProduct;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有产品,支持按品牌过滤
|
||||
* @param brand 品牌名称
|
||||
* @returns 所有符合条件的产品
|
||||
*/
|
||||
async getAllProducts(brand?: string): Promise<{ items: Product[], total: number }> {
|
||||
const qb = this.productModel
|
||||
.createQueryBuilder('product')
|
||||
.leftJoinAndSelect('product.attributes', 'attribute')
|
||||
.leftJoinAndSelect('attribute.dict', 'dict')
|
||||
.leftJoinAndSelect('product.category', 'category');
|
||||
|
||||
// 按品牌过滤
|
||||
if (brand) {
|
||||
// 先获取品牌对应的字典项
|
||||
const brandDict = await this.dictModel.findOne({ where: { name: 'brand' } });
|
||||
if (brandDict) {
|
||||
// 查找品牌名称对应的字典项(支持标题和名称匹配)
|
||||
const brandItem = await this.dictItemModel.findOne({
|
||||
where: [
|
||||
{
|
||||
title: brand,
|
||||
dict: { id: brandDict.id }
|
||||
},
|
||||
{
|
||||
name: brand,
|
||||
dict: { id: brandDict.id }
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (brandItem) {
|
||||
qb.andWhere(qb => {
|
||||
const subQuery = qb
|
||||
.subQuery()
|
||||
.select('product_attributes_dict_item.productId')
|
||||
.from('product_attributes_dict_item', 'product_attributes_dict_item')
|
||||
.where('product_attributes_dict_item.dictItemId = :brandId', {
|
||||
brandId: brandItem.id,
|
||||
})
|
||||
.getQuery();
|
||||
return 'product.id IN ' + subQuery;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据类型填充组成信息
|
||||
const items = await qb.getMany();
|
||||
for (const product of items) {
|
||||
if (product.type === 'single') {
|
||||
// 单品不持久化组成,这里仅返回一个基于 SKU 的虚拟组成
|
||||
const component = new ProductStockComponent();
|
||||
component.productId = product.id;
|
||||
component.sku = product.sku;
|
||||
component.quantity = 1;
|
||||
product.components = [component];
|
||||
} else {
|
||||
// 混装商品返回持久化的 SKU 组成
|
||||
product.components = await this.productStockComponentModel.find({
|
||||
where: { productId: product.id },
|
||||
});
|
||||
}
|
||||
|
||||
// 确保属性按强度正确划分,只保留强度相关的属性
|
||||
// 这里根据需求,如果需要可以进一步过滤或重组属性
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
total: items.length
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取产品按属性值分组,支持按强度划分
|
||||
* @param brand 品牌名称
|
||||
* @returns 按属性值分组的产品
|
||||
*/
|
||||
async getProductsGroupedByAttribute(brand?: string, attributeName: string = 'strength'): Promise<{ [key: string]: Product[] }> {
|
||||
// 首先获取所有产品
|
||||
const { items } = await this.getAllProducts(brand);
|
||||
|
||||
// 按指定属性分组
|
||||
const groupedProducts: { [key: string]: Product[] } = {};
|
||||
|
||||
items.forEach(product => {
|
||||
// 获取产品的指定属性值
|
||||
const attribute = product.attributes.find(attr => attr.dict.name === attributeName);
|
||||
if (attribute) {
|
||||
const attributeValue = attribute.title || attribute.name;
|
||||
if (!groupedProducts[attributeValue]) {
|
||||
groupedProducts[attributeValue] = [];
|
||||
}
|
||||
groupedProducts[attributeValue].push(product);
|
||||
} else {
|
||||
// 如果没有该属性,放入未分组
|
||||
if (!groupedProducts['未分组']) {
|
||||
groupedProducts['未分组'] = [];
|
||||
}
|
||||
groupedProducts['未分组'].push(product);
|
||||
}
|
||||
});
|
||||
|
||||
return groupedProducts;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ import * as FormData from 'form-data';
|
|||
import { SiteService } from './site.service';
|
||||
import { Site } from '../entity/site.entity';
|
||||
import { UnifiedReviewDTO } from '../dto/site-api.dto';
|
||||
import { ShopyyGetOneOrderResult, ShopyyReview } from '../dto/shopyy.dto';
|
||||
import { ShopyyReview } from '../dto/shopyy.dto';
|
||||
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||
import { UnifiedSearchParamsDTO, ShopyyGetAllOrdersParams } from '../dto/api.dto';
|
||||
import { UnifiedSearchParamsDTO } from '../dto/api.dto';
|
||||
/**
|
||||
* ShopYY平台服务实现
|
||||
*/
|
||||
|
|
@ -288,7 +288,7 @@ export class ShopyyService {
|
|||
* @param pageSize 每页数量
|
||||
* @returns 分页订单列表
|
||||
*/
|
||||
async getOrders(site: any | number, page: number = 1, pageSize: number = 3000, params: ShopyyGetAllOrdersParams = {}): Promise<any> {
|
||||
async getOrders(site: any | number, page: number = 1, pageSize: number = 100, params: UnifiedSearchParamsDTO = {}): Promise<any> {
|
||||
// 如果传入的是站点ID,则获取站点配置
|
||||
const siteConfig = typeof site === 'number' ? await this.siteService.get(site) : site;
|
||||
|
||||
|
|
@ -308,11 +308,12 @@ export class ShopyyService {
|
|||
};
|
||||
}
|
||||
|
||||
async getAllOrders(site: any | number, params: ShopyyGetAllOrdersParams = {}, maxPages: number = 10, concurrencyLimit: number = 100): Promise<any> {
|
||||
const firstPage = await this.getOrders(site, 1, 100, params);
|
||||
async getAllOrders(site: any | number, params: Record<string, any> = {}, maxPages: number = 10, concurrencyLimit: number = 100): Promise<any> {
|
||||
const firstPage = await this.getOrders(site, 1, 100);
|
||||
|
||||
const { items: firstPageItems, totalPages} = firstPage;
|
||||
|
||||
// const { page = 1, per_page = 100 } = params;
|
||||
// 如果只有一页数据,直接返回
|
||||
if (totalPages <= 1) {
|
||||
return firstPageItems;
|
||||
|
|
@ -333,7 +334,7 @@ export class ShopyyService {
|
|||
// 创建当前批次的并发请求
|
||||
for (let i = 0; i < batchSize; i++) {
|
||||
const page = currentPage + i;
|
||||
const pagePromise = this.getOrders(site, page, 100, params)
|
||||
const pagePromise = this.getOrders(site, page, 100)
|
||||
.then(pageResult => pageResult.items)
|
||||
.catch(error => {
|
||||
console.error(`获取第 ${page} 页数据失败:`, error);
|
||||
|
|
@ -365,7 +366,7 @@ export class ShopyyService {
|
|||
* @param orderId 订单ID
|
||||
* @returns 订单详情
|
||||
*/
|
||||
async getOrder(siteId: string, orderId: string): Promise<ShopyyGetOneOrderResult> {
|
||||
async getOrder(siteId: string, orderId: string): Promise<any> {
|
||||
const site = await this.siteService.get(Number(siteId));
|
||||
|
||||
// ShopYY API: GET /orders/{id}
|
||||
|
|
@ -475,16 +476,13 @@ export class ShopyyService {
|
|||
async createFulfillment(site: Site, orderId: string, data: any): Promise<any> {
|
||||
// ShopYY API: POST /orders/{id}/shipments
|
||||
const fulfillmentData = {
|
||||
data: [{
|
||||
order_number: orderId,
|
||||
tracking_company: data.tracking_company,
|
||||
tracking_number: data.tracking_number,
|
||||
carrier_code: data.carrier_code,
|
||||
note: "note",
|
||||
mode: ""
|
||||
}]
|
||||
carrier_name: data.carrier_name,
|
||||
shipping_method: data.shipping_method
|
||||
};
|
||||
const response = await this.request(site, `orders/fulfillments`, 'POST', fulfillmentData);
|
||||
|
||||
const response = await this.request(site, `orders/${orderId}/shipments`, 'POST', fulfillmentData);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
|
@ -497,7 +495,7 @@ export class ShopyyService {
|
|||
*/
|
||||
async deleteFulfillment(site: any, orderId: string, fulfillmentId: string): Promise<boolean> {
|
||||
try {
|
||||
// ShopYY API: DELETE /orders/fulfillments/{fulfillment_id}
|
||||
// ShopYY API: DELETE /orders/{order_id}/shipments/{fulfillment_id}
|
||||
await this.request(site, `orders/${orderId}/fulfillments/${fulfillmentId}`, 'DELETE');
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import { SiteService } from './site.service';
|
|||
import { WPService } from './wp.service';
|
||||
import { ProductService } from './product.service';
|
||||
import { UnifiedProductDTO } from '../dto/site-api.dto';
|
||||
import { Product } from '../entity/product.entity';
|
||||
|
||||
@Provide()
|
||||
export class SiteApiService {
|
||||
|
|
@ -53,7 +52,7 @@ export class SiteApiService {
|
|||
* @param siteProduct 站点商品信息
|
||||
* @returns 包含ERP产品信息的站点商品
|
||||
*/
|
||||
async enrichSiteProductWithErpInfo(siteId: number, siteProduct: UnifiedProductDTO): Promise<UnifiedProductDTO & { erpProduct?: Product }> {
|
||||
async enrichSiteProductWithErpInfo(siteId: number, siteProduct: any): Promise<any> {
|
||||
if (!siteProduct || !siteProduct.sku) {
|
||||
return siteProduct;
|
||||
}
|
||||
|
|
@ -65,7 +64,18 @@ export class SiteApiService {
|
|||
// 将ERP产品信息合并到站点商品中
|
||||
return {
|
||||
...siteProduct,
|
||||
erpProduct,
|
||||
erpProduct: {
|
||||
id: erpProduct.id,
|
||||
sku: erpProduct.sku,
|
||||
name: erpProduct.name,
|
||||
nameCn: erpProduct.nameCn,
|
||||
category: erpProduct.category,
|
||||
attributes: erpProduct.attributes,
|
||||
components: erpProduct.components,
|
||||
price: erpProduct.price,
|
||||
promotionPrice: erpProduct.promotionPrice,
|
||||
// 可以根据需要添加更多ERP产品字段
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
// 如果找不到对应的ERP产品,返回原始站点商品
|
||||
|
|
@ -80,7 +90,7 @@ export class SiteApiService {
|
|||
* @param siteProducts 站点商品列表
|
||||
* @returns 包含ERP产品信息的站点商品列表
|
||||
*/
|
||||
async enrichSiteProductsWithErpInfo(siteId: number, siteProducts: UnifiedProductDTO[]): Promise<(UnifiedProductDTO & { erpProduct?: Product })[]> {
|
||||
async enrichSiteProductsWithErpInfo(siteId: number, siteProducts: any[]): Promise<any[]> {
|
||||
if (!siteProducts || !siteProducts.length) {
|
||||
return siteProducts;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,19 +15,8 @@ export class StatisticsService {
|
|||
orderItemRepository: Repository<OrderItem>;
|
||||
|
||||
async getOrderStatistics(params: OrderStatisticsParams) {
|
||||
const { startDate, endDate, grouping, siteId, country } = params;
|
||||
const { startDate, endDate, grouping, siteId } = 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
|
||||
|
|
@ -65,24 +54,22 @@ 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
|
||||
),
|
||||
order_sales_summary AS (
|
||||
SELECT
|
||||
orderId,
|
||||
SUM(CASE WHEN brand = 'zyn' THEN quantity ELSE 0 END) AS zyn_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' THEN quantity ELSE 0 END) AS yoone_quantity,
|
||||
SUM(CASE WHEN brand = 'zex' THEN quantity ELSE 0 END) AS zex_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '3mg' THEN quantity ELSE 0 END) AS yoone_3_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '6mg' THEN quantity ELSE 0 END) AS yoone_6_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '9mg' THEN quantity ELSE 0 END) AS yoone_9_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '12mg' THEN quantity ELSE 0 END) AS yoone_12_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '15mg' THEN quantity ELSE 0 END) AS yoone_15_quantity
|
||||
SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity,
|
||||
SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity
|
||||
FROM order_sale
|
||||
GROUP BY orderId
|
||||
),
|
||||
|
|
@ -260,25 +247,22 @@ 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')`;
|
||||
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
|
||||
),
|
||||
order_sales_summary AS (
|
||||
SELECT
|
||||
orderId,
|
||||
SUM(CASE WHEN brand = 'zyn' THEN quantity ELSE 0 END) AS zyn_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' THEN quantity ELSE 0 END) AS yoone_quantity,
|
||||
SUM(CASE WHEN brand = 'zex' THEN quantity ELSE 0 END) AS zex_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '3mg' THEN quantity ELSE 0 END) AS yoone_3_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '6mg' THEN quantity ELSE 0 END) AS yoone_6_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '9mg' THEN quantity ELSE 0 END) AS yoone_9_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '12mg' THEN quantity ELSE 0 END) AS yoone_12_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '15mg' THEN quantity ELSE 0 END) AS yoone_15_quantity
|
||||
SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity,
|
||||
SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity
|
||||
FROM order_sale
|
||||
GROUP BY orderId
|
||||
),
|
||||
|
|
@ -456,26 +440,22 @@ 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}'
|
||||
`;
|
||||
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
|
||||
),
|
||||
order_sales_summary AS (
|
||||
SELECT
|
||||
orderId,
|
||||
SUM(CASE WHEN brand = 'zyn' THEN quantity ELSE 0 END) AS zyn_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' THEN quantity ELSE 0 END) AS yoone_quantity,
|
||||
SUM(CASE WHEN brand = 'zex' THEN quantity ELSE 0 END) AS zex_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '3mg' THEN quantity ELSE 0 END) AS yoone_3_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '6mg' THEN quantity ELSE 0 END) AS yoone_6_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '9mg' THEN quantity ELSE 0 END) AS yoone_9_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '12mg' THEN quantity ELSE 0 END) AS yoone_12_quantity,
|
||||
SUM(CASE WHEN brand = 'yoone' AND strength = '15mg' THEN quantity ELSE 0 END) AS yoone_15_quantity
|
||||
SUM(CASE WHEN name LIKE '%zyn%' THEN quantity ELSE 0 END) AS zyn_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' THEN quantity ELSE 0 END) AS yoone_quantity,
|
||||
SUM(CASE WHEN name LIKE '%zex%' THEN quantity ELSE 0 END) AS zex_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 1 THEN quantity ELSE 0 END) AS yoone_G_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND isPackage = 0 THEN quantity ELSE 0 END) AS yoone_S_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%3%' THEN quantity ELSE 0 END) AS yoone_3_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%6%' THEN quantity ELSE 0 END) AS yoone_6_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%9%' THEN quantity ELSE 0 END) AS yoone_9_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%12%' THEN quantity ELSE 0 END) AS yoone_12_quantity,
|
||||
SUM(CASE WHEN name LIKE '%yoone%' AND name LIKE '%15%' THEN quantity ELSE 0 END) AS yoone_15_quantity
|
||||
FROM order_sale
|
||||
GROUP BY orderId
|
||||
),
|
||||
|
|
@ -1334,14 +1314,7 @@ export class StatisticsService {
|
|||
}
|
||||
|
||||
async getOrderSorce(params) {
|
||||
const { country } = params;
|
||||
|
||||
let siteIds = []
|
||||
if (country) {
|
||||
siteIds = await this.getSiteIds(country)
|
||||
}
|
||||
|
||||
let sql = `
|
||||
const sql = `
|
||||
WITH cutoff_months AS (
|
||||
SELECT
|
||||
DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 7 MONTH), '%Y-%m') AS start_month,
|
||||
|
|
@ -1353,10 +1326,7 @@ 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')`;
|
||||
if (siteIds.length!=0) sql += ` AND siteId IN ('${siteIds.join("','")}')`;
|
||||
else sql += ` AND siteId IS NULL `;
|
||||
sql += `
|
||||
WHERE status IN ('processing', 'completed')
|
||||
GROUP BY customer_email
|
||||
),
|
||||
order_months AS (
|
||||
|
|
@ -1364,10 +1334,7 @@ export class StatisticsService {
|
|||
customer_email,
|
||||
DATE_FORMAT(date_paid, '%Y-%m') AS order_month
|
||||
FROM \`order\`
|
||||
WHERE status IN ('processing', 'completed')`;
|
||||
if (siteIds.length!=0) sql += ` AND siteId IN ('${siteIds.join("','")}')`;
|
||||
else sql += ` AND siteId IS NULL `;
|
||||
sql += `
|
||||
WHERE status IN ('processing', 'completed')
|
||||
),
|
||||
filtered_orders AS (
|
||||
SELECT o.customer_email, o.order_month, u.first_order_month,u.first_order_total, c.start_month
|
||||
|
|
@ -1399,7 +1366,7 @@ export class StatisticsService {
|
|||
ORDER BY order_month DESC, first_order_month_group
|
||||
`
|
||||
|
||||
let inactiveSql = `
|
||||
const inactiveSql = `
|
||||
WITH
|
||||
cutoff_months AS (
|
||||
SELECT
|
||||
|
|
@ -1414,10 +1381,7 @@ export class StatisticsService {
|
|||
date_paid,
|
||||
total
|
||||
FROM \`order\`
|
||||
WHERE status IN ('processing', 'completed')`;
|
||||
if (siteIds.length!=0) inactiveSql += ` AND siteId IN ('${siteIds.join("','")}')`;
|
||||
else inactiveSql += ` AND siteId IS NULL `;
|
||||
inactiveSql += `
|
||||
WHERE status IN ('processing', 'completed')
|
||||
),
|
||||
|
||||
filtered_users AS (
|
||||
|
|
@ -1560,13 +1524,4 @@ 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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
/**
|
||||
* wp 接口参考:
|
||||
* https://developer.wordpress.org/rest-api/reference/media/
|
||||
* woocommerce:
|
||||
*
|
||||
* https://developer.wordpress.org/rest-api/reference/media/
|
||||
*/
|
||||
import { Inject, Provide } from '@midwayjs/core';
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
|
|
@ -12,7 +10,7 @@ import { IPlatformService } from '../interface/platform.interface';
|
|||
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||
import * as FormData from 'form-data';
|
||||
import * as fs from 'fs';
|
||||
import { WooProduct, WooVariation, WpMediaGetListParams } from '../dto/woocommerce.dto';
|
||||
import { WooProduct, WooVariation } from '../dto/woocommerce.dto';
|
||||
const MAX_PAGE_SIZE = 100;
|
||||
@Provide()
|
||||
export class WPService implements IPlatformService {
|
||||
|
|
@ -1046,7 +1044,20 @@ export class WPService implements IPlatformService {
|
|||
};
|
||||
}
|
||||
|
||||
public async fetchMediaPaged(site: any, params: Partial<WpMediaGetListParams> = {}) {
|
||||
public async fetchMediaPaged(site: any, params: Record<string, any> = {}) {
|
||||
const page = Number(params.page ?? 1);
|
||||
const per_page = Number( params.per_page ?? 20);
|
||||
const where = params.where && typeof params.where === 'object' ? params.where : {};
|
||||
let orderby: string | undefined = params.orderby;
|
||||
let order: 'asc' | 'desc' | undefined = params.orderDir as any;
|
||||
if (!orderby && params.order && typeof params.order === 'object') {
|
||||
const entries = Object.entries(params.order as Record<string, any>);
|
||||
if (entries.length > 0) {
|
||||
const [field, dir] = entries[0];
|
||||
orderby = field;
|
||||
order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc';
|
||||
}
|
||||
}
|
||||
const apiUrl = site.apiUrl;
|
||||
const { consumerKey, consumerSecret } = site as any;
|
||||
const endpoint = 'wp/v2/media';
|
||||
|
|
@ -1055,21 +1066,17 @@ public async fetchMediaPaged(site: any, params: Partial<WpMediaGetListParams> =
|
|||
const response = await axios.get(url, {
|
||||
headers: { Authorization: `Basic ${auth}` },
|
||||
params: {
|
||||
...params,
|
||||
page: params.page ?? 1,
|
||||
per_page: params.per_page ?? 20,
|
||||
...where,
|
||||
...(params.search ? { search: params.search } : {}),
|
||||
...(orderby ? { orderby } : {}),
|
||||
...(order ? { order } : {}),
|
||||
page,
|
||||
per_page
|
||||
}
|
||||
});
|
||||
// 检查是否有错误信息
|
||||
if(response?.data?.message){
|
||||
throw new Error(`获取${apiUrl}条媒体文件失败,原因为${response.data.message}`)
|
||||
}
|
||||
if(!Array.isArray(response.data)) {
|
||||
throw new Error(`获取${apiUrl}条媒体文件失败,原因为返回数据不是数组`);
|
||||
}
|
||||
const total = Number(response.headers['x-wp-total'] || 0);
|
||||
const totalPages = Number(response.headers['x-wp-totalpages'] || 0);
|
||||
return { items: response.data, total, totalPages, page:params.page ?? 1, per_page: params.per_page ?? 20, page_size: params.per_page ?? 20 };
|
||||
return { items: response.data, total, totalPages, page, per_page, page_size: per_page };
|
||||
}
|
||||
/**
|
||||
* 上传媒体文件
|
||||
|
|
|
|||
|
|
@ -1,11 +0,0 @@
|
|||
export const toArray = (value: any): any[] => {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (value === undefined || value === null) return [];
|
||||
return String(value).split(',').map(v => v.trim()).filter(Boolean);
|
||||
};
|
||||
|
||||
export const toNumber = (value: any): number | undefined => {
|
||||
if (value === undefined || value === null || value === '') return undefined;
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) ? n : undefined;
|
||||
};
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
// Test script for FreightwavesService createOrder method
|
||||
|
||||
const { FreightwavesService } = require('./dist/service/freightwaves.service');
|
||||
|
||||
async function testFreightwavesService() {
|
||||
try {
|
||||
// Create an instance of the FreightwavesService
|
||||
const service = new FreightwavesService();
|
||||
|
||||
// Call the test method
|
||||
console.log('Starting test for createOrder method...');
|
||||
const result = await service.testQueryOrder();
|
||||
|
||||
console.log('Test completed successfully!');
|
||||
console.log('Result:', result);
|
||||
console.log('\nTo run the actual createOrder request:');
|
||||
console.log('1. Uncomment the createOrder call in the testCreateOrder method');
|
||||
console.log('2. Update the test-secret, test-partner-id with real credentials');
|
||||
console.log('3. Run this script again');
|
||||
} catch (error) {
|
||||
console.error('Test failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the test
|
||||
testFreightwavesService();
|
||||
Loading…
Reference in New Issue