Compare commits
2 Commits
b3b7ee4793
...
8881bd9da6
| Author | SHA1 | Date |
|---|---|---|
|
|
8881bd9da6 | |
|
|
71b2c249be |
|
|
@ -28,6 +28,7 @@ import {
|
||||||
WooWebhook,
|
WooWebhook,
|
||||||
WooOrderSearchParams,
|
WooOrderSearchParams,
|
||||||
WooProductSearchParams,
|
WooProductSearchParams,
|
||||||
|
WpMediaGetListParams,
|
||||||
} from '../dto/woocommerce.dto';
|
} from '../dto/woocommerce.dto';
|
||||||
import { Site } from '../entity/site.entity';
|
import { Site } from '../entity/site.entity';
|
||||||
import { WPService } from '../service/wp.service';
|
import { WPService } from '../service/wp.service';
|
||||||
|
|
@ -249,13 +250,25 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
date_modified: item.date_modified ?? item.modified,
|
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>> {
|
async getMedia(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<UnifiedMediaDTO>> {
|
||||||
// 获取媒体列表并映射为统一媒体DTO集合
|
// 获取媒体列表并映射为统一媒体DTO集合
|
||||||
const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged(
|
const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged(
|
||||||
this.site,
|
this.site,
|
||||||
params
|
this.mapMediaSearchParams(params)
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
items: items.map(this.mapPlatformToUnifiedMedia.bind(this)),
|
items: items.map(this.mapPlatformToUnifiedMedia.bind(this)),
|
||||||
|
|
@ -633,7 +646,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
type: data.type,
|
type: data.type,
|
||||||
status: data.status,
|
status: data.status,
|
||||||
sku: data.sku,
|
sku: data.sku,
|
||||||
regular_price: data.regular_price,
|
regular_price: data.regular_price,
|
||||||
sale_price: data.sale_price,
|
sale_price: data.sale_price,
|
||||||
price: data.price,
|
price: data.price,
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,12 @@ export default {
|
||||||
// dataSource: {
|
// dataSource: {
|
||||||
// default: {
|
// default: {
|
||||||
// host: '13.212.62.127',
|
// host: '13.212.62.127',
|
||||||
|
// port: '3306',
|
||||||
// username: 'root',
|
// username: 'root',
|
||||||
// password: 'Yoone!@.2025',
|
// password: 'Yoone!@.2025',
|
||||||
|
// database: 'inventory_v2',
|
||||||
|
// synchronize: true,
|
||||||
|
// logging: true,
|
||||||
// },
|
// },
|
||||||
// },
|
// },
|
||||||
// },
|
// },
|
||||||
|
|
@ -20,7 +24,8 @@ export default {
|
||||||
username: 'root',
|
username: 'root',
|
||||||
password: 'Yoone!@.2025',
|
password: 'Yoone!@.2025',
|
||||||
database: 'inventory_v2',
|
database: 'inventory_v2',
|
||||||
synchronize: true
|
synchronize: true,
|
||||||
|
logging: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,31 @@ 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 })
|
@ApiOkResponse({ type: ProductRes })
|
||||||
@Post('/')
|
@Post('/')
|
||||||
async createProduct(@Body() productData: CreateProductDTO) {
|
async createProduct(@Body() productData: CreateProductDTO) {
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,13 @@ export class UnifiedSearchParamsDTO<Where=Record<string, any>> {
|
||||||
required: false,
|
required: false,
|
||||||
})
|
})
|
||||||
orderBy?: Record<string, 'asc' | 'desc'> | string;
|
orderBy?: Record<string, 'asc' | 'desc'> | string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '分组字段,例如 "categoryId"',
|
||||||
|
type: 'string',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
groupBy?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import {
|
||||||
UnifiedPaginationDTO,
|
UnifiedPaginationDTO,
|
||||||
} from './api.dto';
|
} from './api.dto';
|
||||||
import { Dict } from '../entity/dict.entity';
|
import { Dict } from '../entity/dict.entity';
|
||||||
|
import { Product } from '../entity/product.entity';
|
||||||
// export class UnifiedOrderWhere{
|
// export class UnifiedOrderWhere{
|
||||||
// []
|
// []
|
||||||
// }
|
// }
|
||||||
|
|
@ -306,17 +307,7 @@ export class UnifiedProductDTO {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: false,
|
required: false,
|
||||||
})
|
})
|
||||||
erpProduct?: {
|
erpProduct?: Product
|
||||||
id: number;
|
|
||||||
sku: string;
|
|
||||||
name: string;
|
|
||||||
nameCn?: string;
|
|
||||||
category?: any;
|
|
||||||
attributes?: any[];
|
|
||||||
components?: any[];
|
|
||||||
price: number;
|
|
||||||
promotionPrice: number;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnifiedOrderRefundDTO {
|
export class UnifiedOrderRefundDTO {
|
||||||
|
|
|
||||||
|
|
@ -616,6 +616,83 @@ export interface ListParams {
|
||||||
parant: string[];
|
parant: string[];
|
||||||
parent_exclude: 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 {
|
export enum WooContext {
|
||||||
view,
|
view,
|
||||||
edit
|
edit
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ export class CategoryService {
|
||||||
order: {
|
order: {
|
||||||
sort: 'DESC',
|
sort: 'DESC',
|
||||||
createdAt: 'DESC'
|
createdAt: 'DESC'
|
||||||
}
|
},
|
||||||
|
relations: ['attributes', 'attributes.attributeDict']
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -240,7 +240,7 @@ export class ProductService {
|
||||||
const pageSize = query.per_page || 10;
|
const pageSize = query.per_page || 10;
|
||||||
|
|
||||||
// 处理搜索参数
|
// 处理搜索参数
|
||||||
const name = query.where?.name || query.search || '';
|
const name = query.where?.name || '';
|
||||||
|
|
||||||
// 处理品牌过滤
|
// 处理品牌过滤
|
||||||
const brandId = query.where?.brandId;
|
const brandId = query.where?.brandId;
|
||||||
|
|
@ -343,7 +343,7 @@ export class ProductService {
|
||||||
.select('product_attributes_dict_item.productId')
|
.select('product_attributes_dict_item.productId')
|
||||||
.from('product_attributes_dict_item', 'product_attributes_dict_item')
|
.from('product_attributes_dict_item', 'product_attributes_dict_item')
|
||||||
.innerJoin('dict_item', 'dict_item', 'product_attributes_dict_item.dictItemId = dict_item.id')
|
.innerJoin('dict_item', 'dict_item', 'product_attributes_dict_item.dictItemId = dict_item.id')
|
||||||
.innerJoin('dict', 'dict', 'dict_item.dictId = dict.id')
|
.innerJoin('dict', 'dict', 'dict_item.dict_id = dict.id')
|
||||||
.where('dict.name = :attributeName', {
|
.where('dict.name = :attributeName', {
|
||||||
attributeName,
|
attributeName,
|
||||||
})
|
})
|
||||||
|
|
@ -468,6 +468,299 @@ 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(
|
async getOrCreateAttribute(
|
||||||
dictName: string,
|
dictName: string,
|
||||||
itemTitle: string,
|
itemTitle: string,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import { SiteService } from './site.service';
|
||||||
import { WPService } from './wp.service';
|
import { WPService } from './wp.service';
|
||||||
import { ProductService } from './product.service';
|
import { ProductService } from './product.service';
|
||||||
import { UnifiedProductDTO } from '../dto/site-api.dto';
|
import { UnifiedProductDTO } from '../dto/site-api.dto';
|
||||||
|
import { Product } from '../entity/product.entity';
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
export class SiteApiService {
|
export class SiteApiService {
|
||||||
|
|
@ -52,7 +53,7 @@ export class SiteApiService {
|
||||||
* @param siteProduct 站点商品信息
|
* @param siteProduct 站点商品信息
|
||||||
* @returns 包含ERP产品信息的站点商品
|
* @returns 包含ERP产品信息的站点商品
|
||||||
*/
|
*/
|
||||||
async enrichSiteProductWithErpInfo(siteId: number, siteProduct: any): Promise<any> {
|
async enrichSiteProductWithErpInfo(siteId: number, siteProduct: UnifiedProductDTO): Promise<UnifiedProductDTO & { erpProduct?: Product }> {
|
||||||
if (!siteProduct || !siteProduct.sku) {
|
if (!siteProduct || !siteProduct.sku) {
|
||||||
return siteProduct;
|
return siteProduct;
|
||||||
}
|
}
|
||||||
|
|
@ -64,18 +65,7 @@ export class SiteApiService {
|
||||||
// 将ERP产品信息合并到站点商品中
|
// 将ERP产品信息合并到站点商品中
|
||||||
return {
|
return {
|
||||||
...siteProduct,
|
...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) {
|
} catch (error) {
|
||||||
// 如果找不到对应的ERP产品,返回原始站点商品
|
// 如果找不到对应的ERP产品,返回原始站点商品
|
||||||
|
|
@ -90,7 +80,7 @@ export class SiteApiService {
|
||||||
* @param siteProducts 站点商品列表
|
* @param siteProducts 站点商品列表
|
||||||
* @returns 包含ERP产品信息的站点商品列表
|
* @returns 包含ERP产品信息的站点商品列表
|
||||||
*/
|
*/
|
||||||
async enrichSiteProductsWithErpInfo(siteId: number, siteProducts: any[]): Promise<any[]> {
|
async enrichSiteProductsWithErpInfo(siteId: number, siteProducts: UnifiedProductDTO[]): Promise<(UnifiedProductDTO & { erpProduct?: Product })[]> {
|
||||||
if (!siteProducts || !siteProducts.length) {
|
if (!siteProducts || !siteProducts.length) {
|
||||||
return siteProducts;
|
return siteProducts;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
/**
|
/**
|
||||||
*
|
* wp 接口参考:
|
||||||
* https://developer.wordpress.org/rest-api/reference/media/
|
* https://developer.wordpress.org/rest-api/reference/media/
|
||||||
|
* woocommerce:
|
||||||
|
*
|
||||||
*/
|
*/
|
||||||
import { Inject, Provide } from '@midwayjs/core';
|
import { Inject, Provide } from '@midwayjs/core';
|
||||||
import axios, { AxiosRequestConfig } from 'axios';
|
import axios, { AxiosRequestConfig } from 'axios';
|
||||||
|
|
@ -10,7 +12,7 @@ import { IPlatformService } from '../interface/platform.interface';
|
||||||
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||||
import * as FormData from 'form-data';
|
import * as FormData from 'form-data';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { WooProduct, WooVariation } from '../dto/woocommerce.dto';
|
import { WooProduct, WooVariation, WpMediaGetListParams } from '../dto/woocommerce.dto';
|
||||||
const MAX_PAGE_SIZE = 100;
|
const MAX_PAGE_SIZE = 100;
|
||||||
@Provide()
|
@Provide()
|
||||||
export class WPService implements IPlatformService {
|
export class WPService implements IPlatformService {
|
||||||
|
|
@ -1044,20 +1046,7 @@ export class WPService implements IPlatformService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async fetchMediaPaged(site: any, params: Record<string, any> = {}) {
|
public async fetchMediaPaged(site: any, params: Partial<WpMediaGetListParams> = {}) {
|
||||||
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 apiUrl = site.apiUrl;
|
||||||
const { consumerKey, consumerSecret } = site as any;
|
const { consumerKey, consumerSecret } = site as any;
|
||||||
const endpoint = 'wp/v2/media';
|
const endpoint = 'wp/v2/media';
|
||||||
|
|
@ -1066,17 +1055,21 @@ export class WPService implements IPlatformService {
|
||||||
const response = await axios.get(url, {
|
const response = await axios.get(url, {
|
||||||
headers: { Authorization: `Basic ${auth}` },
|
headers: { Authorization: `Basic ${auth}` },
|
||||||
params: {
|
params: {
|
||||||
...where,
|
...params,
|
||||||
...(params.search ? { search: params.search } : {}),
|
page: params.page ?? 1,
|
||||||
...(orderby ? { orderby } : {}),
|
per_page: params.per_page ?? 20,
|
||||||
...(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 total = Number(response.headers['x-wp-total'] || 0);
|
||||||
const totalPages = Number(response.headers['x-wp-totalpages'] || 0);
|
const totalPages = Number(response.headers['x-wp-totalpages'] || 0);
|
||||||
return { items: response.data, total, totalPages, page, per_page, page_size: per_page };
|
return { items: response.data, total, totalPages, page:params.page ?? 1, per_page: params.per_page ?? 20, page_size: params.per_page ?? 20 };
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 上传媒体文件
|
* 上传媒体文件
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue