Merge pull request 'feat: 修复产品与站点同步诸多问题' (#39) from zksu/API:main into main
Reviewed-on: #39 Reviewed-by: longbot <444693295@qq.com>
This commit is contained in:
commit
65fd1aec1e
|
|
@ -0,0 +1,23 @@
|
||||||
|
# 使用 Node.js 作为基础镜像
|
||||||
|
FROM node:22-alpine
|
||||||
|
|
||||||
|
# 设置工作目录
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# 复制 package.json 和 package-lock.json
|
||||||
|
COPY package*.json ./
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
RUN npm install --production
|
||||||
|
|
||||||
|
# 复制源代码
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# 构建项目
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# 暴露端口
|
||||||
|
EXPOSE 7001
|
||||||
|
|
||||||
|
# 启动服务
|
||||||
|
CMD ["npm", "run", "prod"]
|
||||||
|
|
@ -1,15 +1,12 @@
|
||||||
import { ISiteAdapter } from '../interface/site-adapter.interface';
|
import { ISiteAdapter } from '../interface/site-adapter.interface';
|
||||||
import { ShopyyService } from '../service/shopyy.service';
|
import { ShopyyService } from '../service/shopyy.service';
|
||||||
import {
|
import {
|
||||||
UnifiedAddressDTO,
|
|
||||||
UnifiedCustomerDTO,
|
UnifiedCustomerDTO,
|
||||||
UnifiedMediaDTO,
|
UnifiedMediaDTO,
|
||||||
UnifiedOrderDTO,
|
UnifiedOrderDTO,
|
||||||
UnifiedOrderLineItemDTO,
|
UnifiedOrderLineItemDTO,
|
||||||
UnifiedPaginationDTO,
|
|
||||||
UnifiedProductDTO,
|
UnifiedProductDTO,
|
||||||
UnifiedProductVariationDTO,
|
UnifiedProductVariationDTO,
|
||||||
UnifiedSearchParamsDTO,
|
|
||||||
UnifiedSubscriptionDTO,
|
UnifiedSubscriptionDTO,
|
||||||
UnifiedReviewPaginationDTO,
|
UnifiedReviewPaginationDTO,
|
||||||
UnifiedReviewDTO,
|
UnifiedReviewDTO,
|
||||||
|
|
@ -17,8 +14,10 @@ import {
|
||||||
UnifiedWebhookPaginationDTO,
|
UnifiedWebhookPaginationDTO,
|
||||||
CreateWebhookDTO,
|
CreateWebhookDTO,
|
||||||
UpdateWebhookDTO,
|
UpdateWebhookDTO,
|
||||||
UnifiedShippingLineDTO,
|
UnifiedAddressDTO,
|
||||||
|
UnifiedShippingLineDTO
|
||||||
} from '../dto/site-api.dto';
|
} from '../dto/site-api.dto';
|
||||||
|
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto';
|
||||||
import {
|
import {
|
||||||
ShopyyCustomer,
|
ShopyyCustomer,
|
||||||
ShopyyOrder,
|
ShopyyOrder,
|
||||||
|
|
@ -122,6 +121,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
||||||
// 映射变体
|
// 映射变体
|
||||||
return {
|
return {
|
||||||
id: variant.id,
|
id: variant.id,
|
||||||
|
name: variant.sku || '',
|
||||||
sku: variant.sku || '',
|
sku: variant.sku || '',
|
||||||
regular_price: String(variant.price ?? ''),
|
regular_price: String(variant.price ?? ''),
|
||||||
sale_price: String(variant.special_price ?? ''),
|
sale_price: String(variant.special_price ?? ''),
|
||||||
|
|
@ -138,7 +138,6 @@ export class ShopyyAdapter implements ISiteAdapter {
|
||||||
[180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed
|
[180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed
|
||||||
[190]: OrderStatus.CANCEL // 190 取消 转为 cancelled
|
[190]: OrderStatus.CANCEL // 190 取消 转为 cancelled
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapOrder(item: ShopyyOrder): UnifiedOrderDTO {
|
private mapOrder(item: ShopyyOrder): UnifiedOrderDTO {
|
||||||
// 提取账单和送货地址 如果不存在则为空对象
|
// 提取账单和送货地址 如果不存在则为空对象
|
||||||
const billing = (item as any).billing_address || {};
|
const billing = (item as any).billing_address || {};
|
||||||
|
|
@ -157,6 +156,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
||||||
city: billing.city || item.payment_city || '',
|
city: billing.city || item.payment_city || '',
|
||||||
state: billing.province || item.payment_zone || '',
|
state: billing.province || item.payment_zone || '',
|
||||||
postcode: billing.zip || item.payment_postcode || '',
|
postcode: billing.zip || item.payment_postcode || '',
|
||||||
|
method_title: item.payment_method || '',
|
||||||
country:
|
country:
|
||||||
billing.country_name ||
|
billing.country_name ||
|
||||||
billing.country_code ||
|
billing.country_code ||
|
||||||
|
|
@ -194,7 +194,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
||||||
id: item.fulfillments?.[0]?.id || 0,
|
id: item.fulfillments?.[0]?.id || 0,
|
||||||
method_title: item.payment_method || '',
|
method_title: item.payment_method || '',
|
||||||
method_id: item.payment_method || '',
|
method_id: item.payment_method || '',
|
||||||
total: item.current_shipping_price.toExponential(2) || '0.00',
|
total: Number(item.current_shipping_price).toExponential(2) || '0.00',
|
||||||
total_tax: '0.00',
|
total_tax: '0.00',
|
||||||
taxes: [],
|
taxes: [],
|
||||||
meta_data: [],
|
meta_data: [],
|
||||||
|
|
@ -294,7 +294,6 @@ export class ShopyyAdapter implements ISiteAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private mapCustomer(item: ShopyyCustomer): UnifiedCustomerDTO {
|
private mapCustomer(item: ShopyyCustomer): UnifiedCustomerDTO {
|
||||||
// 处理多地址结构
|
// 处理多地址结构
|
||||||
const addresses = item.addresses || [];
|
const addresses = item.addresses || [];
|
||||||
|
|
@ -470,7 +469,7 @@ export class ShopyyAdapter implements ISiteAdapter {
|
||||||
return await this.shopyyService.deleteOrder(this.site, id);
|
return await this.shopyyService.deleteOrder(this.site, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async shipOrder(orderId: string | number, data: {
|
async fulfillOrder(orderId: string | number, data: {
|
||||||
tracking_number?: string;
|
tracking_number?: string;
|
||||||
shipping_provider?: string;
|
shipping_provider?: string;
|
||||||
shipping_method?: string;
|
shipping_method?: string;
|
||||||
|
|
@ -479,66 +478,123 @@ export class ShopyyAdapter implements ISiteAdapter {
|
||||||
quantity: number;
|
quantity: number;
|
||||||
}>;
|
}>;
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
// 订单发货
|
// 订单履行(发货)
|
||||||
try {
|
try {
|
||||||
// 更新订单状态为已发货
|
// 判断是否为部分发货(包含 items)
|
||||||
await this.shopyyService.updateOrder(this.site, String(orderId), {
|
if (data.items && data.items.length > 0) {
|
||||||
status: 'completed',
|
// 部分发货
|
||||||
meta_data: [
|
const partShipData = {
|
||||||
{ key: '_tracking_number', value: data.tracking_number },
|
order_number: String(orderId),
|
||||||
{ key: '_shipping_provider', value: data.shipping_provider },
|
note: data.shipping_method || '',
|
||||||
{ key: '_shipping_method', value: data.shipping_method }
|
tracking_company: data.shipping_provider || '',
|
||||||
]
|
tracking_number: data.tracking_number || '',
|
||||||
});
|
courier_code: '1', // 默认快递公司代码
|
||||||
|
products: data.items.map(item => ({
|
||||||
// 添加发货备注
|
quantity: item.quantity,
|
||||||
const note = `订单已发货${data.tracking_number ? `,物流单号:${data.tracking_number}` : ''}${data.shipping_provider ? `,物流公司:${data.shipping_provider}` : ''}`;
|
order_product_id: String(item.order_item_id)
|
||||||
await this.shopyyService.createOrderNote(this.site, orderId, { note, customer_note: true });
|
}))
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
order_id: orderId,
|
|
||||||
shipment_id: `shipment_${orderId}_${Date.now()}`,
|
|
||||||
tracking_number: data.tracking_number,
|
|
||||||
shipping_provider: data.shipping_provider,
|
|
||||||
shipped_at: new Date().toISOString()
|
|
||||||
};
|
};
|
||||||
|
return await this.shopyyService.partFulfillOrder(this.site, partShipData);
|
||||||
|
} else {
|
||||||
|
// 批量发货(完整发货)
|
||||||
|
const batchShipData = {
|
||||||
|
order_number: String(orderId),
|
||||||
|
tracking_company: data.shipping_provider || '',
|
||||||
|
tracking_number: data.tracking_number || '',
|
||||||
|
courier_code: 1, // 默认快递公司代码
|
||||||
|
note: data.shipping_method || '',
|
||||||
|
mode: null // 新增模式
|
||||||
|
};
|
||||||
|
return await this.shopyyService.batchFulfillOrders(this.site, batchShipData);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`发货失败: ${error.message}`);
|
throw new Error(`履行失败: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelShipOrder(orderId: string | number, data: {
|
async cancelFulfillment(orderId: string | number, data: {
|
||||||
reason?: string;
|
reason?: string;
|
||||||
shipment_id?: string;
|
shipment_id?: string;
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
// 取消订单发货
|
// 取消订单履行
|
||||||
try {
|
try {
|
||||||
// 将订单状态改回处理中
|
// 调用 ShopyyService 的取消履行方法
|
||||||
await this.shopyyService.updateOrder(this.site, String(orderId), {
|
const cancelShipData = {
|
||||||
status: 'processing',
|
order_id: String(orderId),
|
||||||
meta_data: [
|
fullfillment_id: data.shipment_id || ''
|
||||||
{ key: '_shipment_cancelled', value: 'yes' },
|
};
|
||||||
{ key: '_shipment_cancelled_reason', value: data.reason }
|
const result = await this.shopyyService.cancelFulfillment(this.site, cancelShipData);
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// 添加取消发货的备注
|
|
||||||
const note = `订单发货已取消${data.reason ? `,原因:${data.reason}` : ''}`;
|
|
||||||
await this.shopyyService.createOrderNote(this.site, orderId, { note, customer_note: true });
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: result,
|
||||||
order_id: orderId,
|
order_id: orderId,
|
||||||
shipment_id: data.shipment_id,
|
shipment_id: data.shipment_id,
|
||||||
reason: data.reason,
|
reason: data.reason,
|
||||||
cancelled_at: new Date().toISOString()
|
cancelled_at: new Date().toISOString()
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`取消发货失败: ${error.message}`);
|
throw new Error(`取消履行失败: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单履行信息
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @returns 履行信息列表
|
||||||
|
*/
|
||||||
|
async getOrderFulfillments(orderId: string | number): Promise<any[]> {
|
||||||
|
return await this.shopyyService.getFulfillments(this.site, String(orderId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建订单履行信息
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @param data 履行数据
|
||||||
|
* @returns 创建结果
|
||||||
|
*/
|
||||||
|
async createOrderFulfillment(orderId: string | number, data: {
|
||||||
|
tracking_number: string;
|
||||||
|
tracking_provider: string;
|
||||||
|
date_shipped?: string;
|
||||||
|
status_shipped?: string;
|
||||||
|
}): Promise<any> {
|
||||||
|
// 调用 Shopyy Service 的 createFulfillment 方法
|
||||||
|
const fulfillmentData = {
|
||||||
|
tracking_number: data.tracking_number,
|
||||||
|
carrier_code: data.tracking_provider,
|
||||||
|
carrier_name: data.tracking_provider,
|
||||||
|
shipping_method: data.status_shipped || 'standard'
|
||||||
|
};
|
||||||
|
|
||||||
|
return await this.shopyyService.createFulfillment(this.site, String(orderId), fulfillmentData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新订单履行信息
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @param fulfillmentId 履行ID
|
||||||
|
* @param data 更新数据
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: {
|
||||||
|
tracking_number?: string;
|
||||||
|
tracking_provider?: string;
|
||||||
|
date_shipped?: string;
|
||||||
|
status_shipped?: string;
|
||||||
|
}): Promise<any> {
|
||||||
|
return await this.shopyyService.updateFulfillment(this.site, String(orderId), fulfillmentId, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除订单履行信息
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @param fulfillmentId 履行ID
|
||||||
|
* @returns 删除结果
|
||||||
|
*/
|
||||||
|
async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise<boolean> {
|
||||||
|
return await this.shopyyService.deleteFulfillment(this.site, String(orderId), fulfillmentId);
|
||||||
|
}
|
||||||
|
|
||||||
async getSubscriptions(
|
async getSubscriptions(
|
||||||
params: UnifiedSearchParamsDTO
|
params: UnifiedSearchParamsDTO
|
||||||
): Promise<UnifiedPaginationDTO<UnifiedSubscriptionDTO>> {
|
): Promise<UnifiedPaginationDTO<UnifiedSubscriptionDTO>> {
|
||||||
|
|
@ -767,4 +823,24 @@ export class ShopyyAdapter implements ISiteAdapter {
|
||||||
async deleteCustomer(id: string | number): Promise<boolean> {
|
async deleteCustomer(id: string | number): Promise<boolean> {
|
||||||
return await this.shopyyService.deleteCustomer(this.site, id);
|
return await this.shopyyService.deleteCustomer(this.site, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise<any> {
|
||||||
|
throw new Error('Shopyy getVariations 暂未实现');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise<UnifiedProductVariationDTO[]> {
|
||||||
|
throw new Error('Shopyy getAllVariations 暂未实现');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getVariation(productId: string | number, variationId: string | number): Promise<UnifiedProductVariationDTO> {
|
||||||
|
throw new Error('Shopyy getVariation 暂未实现');
|
||||||
|
}
|
||||||
|
|
||||||
|
async createVariation(productId: string | number, data: any): Promise<UnifiedProductVariationDTO> {
|
||||||
|
throw new Error('Shopyy createVariation 暂未实现');
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteVariation(productId: string | number, variationId: string | number): Promise<boolean> {
|
||||||
|
throw new Error('Shopyy deleteVariation 暂未实现');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,7 @@ import { ISiteAdapter } from '../interface/site-adapter.interface';
|
||||||
import {
|
import {
|
||||||
UnifiedMediaDTO,
|
UnifiedMediaDTO,
|
||||||
UnifiedOrderDTO,
|
UnifiedOrderDTO,
|
||||||
UnifiedPaginationDTO,
|
|
||||||
UnifiedProductDTO,
|
UnifiedProductDTO,
|
||||||
UnifiedSearchParamsDTO,
|
|
||||||
UnifiedSubscriptionDTO,
|
UnifiedSubscriptionDTO,
|
||||||
UnifiedCustomerDTO,
|
UnifiedCustomerDTO,
|
||||||
UnifiedReviewPaginationDTO,
|
UnifiedReviewPaginationDTO,
|
||||||
|
|
@ -13,7 +11,12 @@ import {
|
||||||
UnifiedWebhookPaginationDTO,
|
UnifiedWebhookPaginationDTO,
|
||||||
CreateWebhookDTO,
|
CreateWebhookDTO,
|
||||||
UpdateWebhookDTO,
|
UpdateWebhookDTO,
|
||||||
|
CreateVariationDTO,
|
||||||
|
UpdateVariationDTO,
|
||||||
|
UnifiedProductVariationDTO,
|
||||||
|
UnifiedVariationPaginationDTO,
|
||||||
} from '../dto/site-api.dto';
|
} from '../dto/site-api.dto';
|
||||||
|
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto';
|
||||||
import {
|
import {
|
||||||
WooProduct,
|
WooProduct,
|
||||||
WooOrder,
|
WooOrder,
|
||||||
|
|
@ -138,7 +141,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLinks(): Promise<Array<{title: string, url: string}>> {
|
async getLinks(): Promise<Array<{ title: string, url: string }>> {
|
||||||
const baseUrl = this.site.apiUrl;
|
const baseUrl = this.site.apiUrl;
|
||||||
const links = [
|
const links = [
|
||||||
{ title: '访问网站', url: baseUrl },
|
{ title: '访问网站', url: baseUrl },
|
||||||
|
|
@ -168,7 +171,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
|
|
||||||
private mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial<WooProductSearchParams> {
|
private mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial<WooProductSearchParams> {
|
||||||
const page = Number(params.page ?? 1);
|
const page = Number(params.page ?? 1);
|
||||||
const per_page = Number( params.per_page ?? 20);
|
const per_page = Number(params.per_page ?? 20);
|
||||||
const where = params.where && typeof params.where === 'object' ? params.where : {};
|
const where = params.where && typeof params.where === 'object' ? params.where : {};
|
||||||
|
|
||||||
const mapped: any = {
|
const mapped: any = {
|
||||||
|
|
@ -225,7 +228,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial<WooOrderSearchParams> {
|
private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial<WooOrderSearchParams> {
|
||||||
// 计算分页参数
|
// 计算分页参数
|
||||||
const page = Number(params.page ?? 1);
|
const page = Number(params.page ?? 1);
|
||||||
const per_page = Number( params.per_page ?? 20);
|
const per_page = Number(params.per_page ?? 20);
|
||||||
// 解析排序参数 支持从 order 对象推导
|
// 解析排序参数 支持从 order 对象推导
|
||||||
const where = params.where && typeof params.where === 'object' ? params.where : {};
|
const where = params.where && typeof params.where === 'object' ? params.where : {};
|
||||||
|
|
||||||
|
|
@ -302,6 +305,23 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
per_page,
|
per_page,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 处理orderBy参数,转换为WooCommerce API的order和orderby格式
|
||||||
|
if (params.orderBy) {
|
||||||
|
// 支持字符串格式 "field:desc" 或对象格式 { "field": "desc" }
|
||||||
|
if (typeof params.orderBy === 'string') {
|
||||||
|
const [field, direction = 'desc'] = params.orderBy.split(':');
|
||||||
|
mapped.orderby = field;
|
||||||
|
mapped.order = direction.toLowerCase() === 'asc' ? 'asc' : 'desc';
|
||||||
|
} else if (typeof params.orderBy === 'object') {
|
||||||
|
const entries = Object.entries(params.orderBy);
|
||||||
|
if (entries.length > 0) {
|
||||||
|
const [field, direction] = entries[0];
|
||||||
|
mapped.orderby = field;
|
||||||
|
mapped.order = direction === 'asc' ? 'asc' : 'desc';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const toArray = (value: any): any[] => {
|
const toArray = (value: any): any[] => {
|
||||||
if (Array.isArray(value)) return value;
|
if (Array.isArray(value)) return value;
|
||||||
if (value === undefined || value === null) return [];
|
if (value === undefined || value === null) return [];
|
||||||
|
|
@ -330,6 +350,49 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
// 将 WooCommerce 产品数据映射为统一产品DTO
|
// 将 WooCommerce 产品数据映射为统一产品DTO
|
||||||
// 保留常用字段与时间信息以便前端统一展示
|
// 保留常用字段与时间信息以便前端统一展示
|
||||||
// https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#product-properties
|
// https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#product-properties
|
||||||
|
|
||||||
|
// 映射变体数据
|
||||||
|
const mappedVariations = item.variations && Array.isArray(item.variations)
|
||||||
|
? item.variations
|
||||||
|
.filter((variation: any) => typeof variation !== 'number') // 过滤掉数字类型的变体ID
|
||||||
|
.map((variation: any) => {
|
||||||
|
// 将变体属性转换为统一格式
|
||||||
|
const mappedAttributes = variation.attributes && Array.isArray(variation.attributes)
|
||||||
|
? variation.attributes.map((attr: any) => ({
|
||||||
|
id: attr.id,
|
||||||
|
name: attr.name || '',
|
||||||
|
position: attr.position,
|
||||||
|
visible: attr.visible,
|
||||||
|
variation: attr.variation,
|
||||||
|
option: attr.option || '' // 变体属性使用 option 而不是 options
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// 映射变体图片
|
||||||
|
const mappedImage = variation.image
|
||||||
|
? {
|
||||||
|
id: variation.image.id,
|
||||||
|
src: variation.image.src,
|
||||||
|
name: variation.image.name,
|
||||||
|
alt: variation.image.alt,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: variation.id,
|
||||||
|
name: variation.name || item.name, // 如果变体没有名称,使用父产品名称
|
||||||
|
sku: variation.sku || '',
|
||||||
|
regular_price: String(variation.regular_price || ''),
|
||||||
|
sale_price: String(variation.sale_price || ''),
|
||||||
|
price: String(variation.price || ''),
|
||||||
|
stock_status: variation.stock_status || 'outofstock',
|
||||||
|
stock_quantity: variation.stock_quantity || 0,
|
||||||
|
attributes: mappedAttributes,
|
||||||
|
image: mappedImage
|
||||||
|
};
|
||||||
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
date_created: item.date_created,
|
date_created: item.date_created,
|
||||||
|
|
@ -366,7 +429,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
variation: attr.variation,
|
variation: attr.variation,
|
||||||
options: attr.options || []
|
options: attr.options || []
|
||||||
})),
|
})),
|
||||||
variations: item.variations as any,
|
variations: mappedVariations,
|
||||||
permalink: item.permalink,
|
permalink: item.permalink,
|
||||||
raw: item,
|
raw: item,
|
||||||
};
|
};
|
||||||
|
|
@ -389,6 +452,16 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
private mapOrder(item: WooOrder): UnifiedOrderDTO {
|
private mapOrder(item: WooOrder): UnifiedOrderDTO {
|
||||||
// 将 WooCommerce 订单数据映射为统一订单DTO
|
// 将 WooCommerce 订单数据映射为统一订单DTO
|
||||||
// 包含账单地址与收货地址以及创建与更新时间
|
// 包含账单地址与收货地址以及创建与更新时间
|
||||||
|
|
||||||
|
// 映射物流追踪信息,将后端格式转换为前端期望的格式
|
||||||
|
const tracking = (item.trackings || []).map((track: any) => ({
|
||||||
|
order_id: String(item.id),
|
||||||
|
tracking_provider: track.tracking_provider || '',
|
||||||
|
tracking_number: track.tracking_number || '',
|
||||||
|
date_shipped: track.date_shipped || '',
|
||||||
|
status_shipped: track.status_shipped || '',
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
number: item.number,
|
number: item.number,
|
||||||
|
|
@ -396,15 +469,14 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
currency: item.currency,
|
currency: item.currency,
|
||||||
total: item.total,
|
total: item.total,
|
||||||
customer_id: item.customer_id,
|
customer_id: item.customer_id,
|
||||||
customer_name: `${item.billing?.first_name || ''} ${
|
customer_email: item.billing?.email || '', // TODO 与 email 重复 保留一个即可
|
||||||
item.billing?.last_name || ''
|
email: item.billing?.email || '',
|
||||||
}`.trim(),
|
customer_name: `${item.billing?.first_name || ''} ${item.billing?.last_name || ''}`.trim(),
|
||||||
refunds: item.refunds?.map?.(refund => ({
|
refunds: item.refunds?.map?.(refund => ({
|
||||||
id: refund.id,
|
id: refund.id,
|
||||||
reason: refund.reason,
|
reason: refund.reason,
|
||||||
total: refund.total,
|
total: refund.total,
|
||||||
})),
|
})),
|
||||||
email: item.billing?.email || '',
|
|
||||||
line_items: (item.line_items as any[]).map(li => ({
|
line_items: (item.line_items as any[]).map(li => ({
|
||||||
...li,
|
...li,
|
||||||
productId: li.product_id,
|
productId: li.product_id,
|
||||||
|
|
@ -420,17 +492,11 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
shipping_lines: item.shipping_lines,
|
shipping_lines: item.shipping_lines,
|
||||||
fee_lines: item.fee_lines,
|
fee_lines: item.fee_lines,
|
||||||
coupon_lines: item.coupon_lines,
|
coupon_lines: item.coupon_lines,
|
||||||
utm_source: item?.meta_data?.find(el => el.key === '_wc_order_attribution_utm_source')?.value || '',
|
tracking: tracking,
|
||||||
device_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_device_type')?.value || '',
|
|
||||||
customer_email: item?.billing?.email || '',
|
|
||||||
source_type: item?.meta_data?.find(el => el.key === '_wc_order_attribution_source_type')?.value || '',
|
|
||||||
raw: item,
|
raw: item,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO {
|
private mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO {
|
||||||
// 将 WooCommerce 订阅数据映射为统一订阅DTO
|
// 将 WooCommerce 订阅数据映射为统一订阅DTO
|
||||||
// 若缺少创建时间则回退为开始时间
|
// 若缺少创建时间则回退为开始时间
|
||||||
|
|
@ -477,8 +543,31 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
'products',
|
'products',
|
||||||
requestParams
|
requestParams
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 对于类型为 variable 的产品,需要加载完整的变体数据
|
||||||
|
const productsWithVariations = await Promise.all(
|
||||||
|
items.map(async (item: any) => {
|
||||||
|
// 如果产品类型是 variable 且有变体 ID 列表,则加载完整的变体数据
|
||||||
|
if (item.type === 'variable' && item.variations && Array.isArray(item.variations) && item.variations.length > 0) {
|
||||||
|
try {
|
||||||
|
// 批量获取该产品的所有变体数据
|
||||||
|
const variations = await this.wpService.sdkGetAll(
|
||||||
|
(this.wpService as any).createApi(this.site, 'wc/v3'),
|
||||||
|
`products/${item.id}/variations`
|
||||||
|
);
|
||||||
|
// 将完整的变体数据添加到产品对象中
|
||||||
|
item.variations = variations;
|
||||||
|
} catch (error) {
|
||||||
|
// 如果获取变体失败,保持原有的 ID 数组
|
||||||
|
console.error(`获取产品 ${item.id} 的变体数据失败:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: items.map(this.mapProduct),
|
items: productsWithVariations.map(this.mapProduct),
|
||||||
total,
|
total,
|
||||||
totalPages,
|
totalPages,
|
||||||
page,
|
page,
|
||||||
|
|
@ -491,14 +580,55 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
// 使用sdkGetAll获取所有产品数据,不受分页限制
|
// 使用sdkGetAll获取所有产品数据,不受分页限制
|
||||||
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||||
const products = await this.wpService.sdkGetAll(api, 'products', params);
|
const products = await this.wpService.sdkGetAll(api, 'products', params);
|
||||||
return products.map((product: any) => this.mapProduct(product));
|
|
||||||
|
// 对于类型为 variable 的产品,需要加载完整的变体数据
|
||||||
|
const productsWithVariations = await Promise.all(
|
||||||
|
products.map(async (product: any) => {
|
||||||
|
// 如果产品类型是 variable 且有变体 ID 列表,则加载完整的变体数据
|
||||||
|
if (product.type === 'variable' && product.variations && Array.isArray(product.variations) && product.variations.length > 0) {
|
||||||
|
try {
|
||||||
|
// 批量获取该产品的所有变体数据
|
||||||
|
const variations = await this.wpService.sdkGetAll(
|
||||||
|
api,
|
||||||
|
`products/${product.id}/variations`
|
||||||
|
);
|
||||||
|
// 将完整的变体数据添加到产品对象中
|
||||||
|
product.variations = variations;
|
||||||
|
} catch (error) {
|
||||||
|
// 如果获取变体失败,保持原有的 ID 数组
|
||||||
|
console.error(`获取产品 ${product.id} 的变体数据失败:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return product;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return productsWithVariations.map((product: any) => this.mapProduct(product));
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProduct(id: string | number): Promise<UnifiedProductDTO> {
|
async getProduct(id: string | number): Promise<UnifiedProductDTO> {
|
||||||
// 获取单个产品详情并映射为统一产品DTO
|
// 获取单个产品详情并映射为统一产品DTO
|
||||||
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||||
const res = await api.get(`products/${id}`);
|
const res = await api.get(`products/${id}`);
|
||||||
return this.mapProduct(res.data);
|
const product = res.data;
|
||||||
|
|
||||||
|
// 如果产品类型是 variable 且有变体 ID 列表,则加载完整的变体数据
|
||||||
|
if (product.type === 'variable' && product.variations && Array.isArray(product.variations) && product.variations.length > 0) {
|
||||||
|
try {
|
||||||
|
// 批量获取该产品的所有变体数据
|
||||||
|
const variations = await this.wpService.sdkGetAll(
|
||||||
|
api,
|
||||||
|
`products/${product.id}/variations`
|
||||||
|
);
|
||||||
|
// 将完整的变体数据添加到产品对象中
|
||||||
|
product.variations = variations;
|
||||||
|
} catch (error) {
|
||||||
|
// 如果获取变体失败,保持原有的 ID 数组
|
||||||
|
console.error(`获取产品 ${product.id} 的变体数据失败:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.mapProduct(product);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createProduct(data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO> {
|
async createProduct(data: Partial<UnifiedProductDTO>): Promise<UnifiedProductDTO> {
|
||||||
|
|
@ -513,12 +643,6 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateVariation(productId: string | number, variationId: string | number, data: any): Promise<any> {
|
|
||||||
// 更新变体信息并返回结果
|
|
||||||
const res = await this.wpService.updateVariation(this.site, String(productId), String(variationId), data);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOrderNotes(orderId: string | number): Promise<any[]> {
|
async getOrderNotes(orderId: string | number): Promise<any[]> {
|
||||||
// 获取订单备注列表
|
// 获取订单备注列表
|
||||||
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||||
|
|
@ -557,13 +681,35 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
const requestParams = this.mapOrderSearchParams(params);
|
const requestParams = this.mapOrderSearchParams(params);
|
||||||
const { items, total, totalPages, page, per_page } =
|
const { items, total, totalPages, page, per_page } =
|
||||||
await this.wpService.fetchResourcePaged<any>(this.site, 'orders', requestParams);
|
await this.wpService.fetchResourcePaged<any>(this.site, 'orders', requestParams);
|
||||||
|
|
||||||
|
// 并行获取所有订单的履行信息
|
||||||
|
const ordersWithTracking = await Promise.all(
|
||||||
|
items.map(async (order: any) => {
|
||||||
|
try {
|
||||||
|
// 获取订单的履行信息
|
||||||
|
const trackings = await this.getOrderFulfillments(order.id);
|
||||||
|
// 将履行信息添加到订单对象中
|
||||||
return {
|
return {
|
||||||
items: items.map(this.mapOrder),
|
...order,
|
||||||
|
trackings: trackings || []
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
// 如果获取履行信息失败,仍然返回订单,只是履行信息为空数组
|
||||||
|
console.error(`获取订单 ${order.id} 的履行信息失败:`, error);
|
||||||
|
return {
|
||||||
|
...order,
|
||||||
|
trackings: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: ordersWithTracking.map(this.mapOrder),
|
||||||
total,
|
total,
|
||||||
totalPages,
|
totalPages,
|
||||||
page,
|
page,
|
||||||
per_page,
|
per_page,
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -600,7 +746,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async shipOrder(orderId: string | number, data: {
|
async fulfillOrder(orderId: string | number, data: {
|
||||||
tracking_number?: string;
|
tracking_number?: string;
|
||||||
shipping_provider?: string;
|
shipping_provider?: string;
|
||||||
shipping_method?: string;
|
shipping_method?: string;
|
||||||
|
|
@ -610,7 +756,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
}>;
|
}>;
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
throw new Error('暂无实现')
|
throw new Error('暂无实现')
|
||||||
// 订单发货
|
// 订单履行(发货)
|
||||||
// const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
// const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||||
|
|
||||||
// try {
|
// try {
|
||||||
|
|
@ -626,30 +772,30 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
// return {
|
// return {
|
||||||
// success: true,
|
// success: true,
|
||||||
// order_id: orderId,
|
// order_id: orderId,
|
||||||
// shipment_id: `shipment_${orderId}_${Date.now()}`,
|
// fulfillment_id: `fulfillment_${orderId}_${Date.now()}`,
|
||||||
// tracking_number: data.tracking_number,
|
// tracking_number: data.tracking_number,
|
||||||
// shipping_provider: data.shipping_provider,
|
// shipping_provider: data.shipping_provider,
|
||||||
// shipped_at: new Date().toISOString()
|
// fulfilled_at: new Date().toISOString()
|
||||||
// };
|
// };
|
||||||
// } catch (error) {
|
// } catch (error) {
|
||||||
// throw new Error(`发货失败: ${error.message}`);
|
// throw new Error(`履行失败: ${error.message}`);
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancelShipOrder(orderId: string | number, data: {
|
async cancelFulfillment(orderId: string | number, data: {
|
||||||
reason?: string;
|
reason?: string;
|
||||||
shipment_id?: string;
|
shipment_id?: string;
|
||||||
}): Promise<any> {
|
}): Promise<any> {
|
||||||
throw new Error('暂未实现')
|
throw new Error('暂未实现')
|
||||||
// 取消订单发货
|
// 取消订单履行
|
||||||
// const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
// const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||||
|
|
||||||
// try {
|
// try {
|
||||||
// // 将订单状态改回处理中
|
// // 将订单状态改回处理中
|
||||||
// await api.put(`orders/${orderId}`, { status: 'processing' });
|
// await api.put(`orders/${orderId}`, { status: 'processing' });
|
||||||
|
|
||||||
// // 添加取消发货的备注
|
// // 添加取消履行的备注
|
||||||
// const note = `订单发货已取消${data.reason ? `,原因:${data.reason}` : ''}`;
|
// const note = `订单履行已取消${data.reason ? `,原因:${data.reason}` : ''}`;
|
||||||
// await api.post(`orders/${orderId}/notes`, { note, customer_note: true });
|
// await api.post(`orders/${orderId}/notes`, { note, customer_note: true });
|
||||||
|
|
||||||
// return {
|
// return {
|
||||||
|
|
@ -660,7 +806,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
// cancelled_at: new Date().toISOString()
|
// cancelled_at: new Date().toISOString()
|
||||||
// };
|
// };
|
||||||
// } catch (error) {
|
// } catch (error) {
|
||||||
// throw new Error(`取消发货失败: ${error.message}`);
|
// throw new Error(`取消履行失败: ${error.message}`);
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -715,7 +861,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
return media.map((mediaItem: any) => this.mapMedia(mediaItem));
|
return media.map((mediaItem: any) => this.mapMedia(mediaItem));
|
||||||
}
|
}
|
||||||
|
|
||||||
private mapReview(item: any): UnifiedReviewDTO & {raw: any} {
|
private mapReview(item: any): UnifiedReviewDTO & { raw: any } {
|
||||||
// 将 WooCommerce 评论数据映射为统一评论DTO
|
// 将 WooCommerce 评论数据映射为统一评论DTO
|
||||||
return {
|
return {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
|
|
@ -795,7 +941,7 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
avatar: item.avatar_url,
|
avatar: item.avatar_url,
|
||||||
email: item.email,
|
email: item.email,
|
||||||
orders: Number(item.orders?? 0),
|
orders: Number(item.orders ?? 0),
|
||||||
total_spend: Number(item.total_spent ?? 0),
|
total_spend: Number(item.total_spent ?? 0),
|
||||||
first_name: item.first_name,
|
first_name: item.first_name,
|
||||||
last_name: item.last_name,
|
last_name: item.last_name,
|
||||||
|
|
@ -828,7 +974,11 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise<UnifiedCustomerDTO[]> {
|
async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise<UnifiedCustomerDTO[]> {
|
||||||
// 使用sdkGetAll获取所有客户数据,不受分页限制
|
// 使用sdkGetAll获取所有客户数据,不受分页限制
|
||||||
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||||
const customers = await this.wpService.sdkGetAll(api, 'customers', params);
|
|
||||||
|
// 处理orderBy参数,转换为WooCommerce API需要的格式
|
||||||
|
const requestParams = this.mapCustomerSearchParams(params || {});
|
||||||
|
|
||||||
|
const customers = await this.wpService.sdkGetAll(api, 'customers', requestParams);
|
||||||
return customers.map((customer: any) => this.mapCustomer(customer));
|
return customers.map((customer: any) => this.mapCustomer(customer));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -855,5 +1005,270 @@ export class WooCommerceAdapter implements ISiteAdapter {
|
||||||
await api.delete(`customers/${id}`, { force: true });
|
await api.delete(`customers/${id}`, { force: true });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOrderFulfillments(orderId: string | number): Promise<any[]> {
|
||||||
|
return await this.wpService.getFulfillments(this.site, String(orderId));
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOrderFulfillment(orderId: string | number, data: {
|
||||||
|
tracking_number: string;
|
||||||
|
tracking_provider: string;
|
||||||
|
date_shipped?: string;
|
||||||
|
status_shipped?: string;
|
||||||
|
}): Promise<any> {
|
||||||
|
const shipmentData: any = {
|
||||||
|
tracking_provider: data.tracking_provider,
|
||||||
|
tracking_number: data.tracking_number,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (data.date_shipped) {
|
||||||
|
shipmentData.date_shipped = data.date_shipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status_shipped) {
|
||||||
|
shipmentData.status_shipped = data.status_shipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.wpService.createFulfillment(this.site, String(orderId), shipmentData);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: {
|
||||||
|
tracking_number?: string;
|
||||||
|
tracking_provider?: string;
|
||||||
|
date_shipped?: string;
|
||||||
|
status_shipped?: string;
|
||||||
|
}): Promise<any> {
|
||||||
|
return await this.wpService.updateFulfillment(this.site, String(orderId), fulfillmentId, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise<boolean> {
|
||||||
|
return await this.wpService.deleteFulfillment(this.site, String(orderId), fulfillmentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 映射 WooCommerce 变体到统一格式
|
||||||
|
private mapVariation(variation: any, productName?: string): UnifiedProductVariationDTO {
|
||||||
|
// 将变体属性转换为统一格式
|
||||||
|
const mappedAttributes = variation.attributes && Array.isArray(variation.attributes)
|
||||||
|
? variation.attributes.map((attr: any) => ({
|
||||||
|
id: attr.id,
|
||||||
|
name: attr.name || '',
|
||||||
|
position: attr.position,
|
||||||
|
visible: attr.visible,
|
||||||
|
variation: attr.variation,
|
||||||
|
option: attr.option || ''
|
||||||
|
}))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
// 映射变体图片
|
||||||
|
const mappedImage = variation.image
|
||||||
|
? {
|
||||||
|
id: variation.image.id,
|
||||||
|
src: variation.image.src,
|
||||||
|
name: variation.image.name,
|
||||||
|
alt: variation.image.alt,
|
||||||
|
}
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: variation.id,
|
||||||
|
name: variation.name || productName || '',
|
||||||
|
sku: variation.sku || '',
|
||||||
|
regular_price: String(variation.regular_price || ''),
|
||||||
|
sale_price: String(variation.sale_price || ''),
|
||||||
|
price: String(variation.price || ''),
|
||||||
|
stock_status: variation.stock_status || 'outofstock',
|
||||||
|
stock_quantity: variation.stock_quantity || 0,
|
||||||
|
attributes: mappedAttributes,
|
||||||
|
image: mappedImage,
|
||||||
|
description: variation.description || '',
|
||||||
|
enabled: variation.status === 'publish',
|
||||||
|
downloadable: variation.downloadable || false,
|
||||||
|
virtual: variation.virtual || false,
|
||||||
|
manage_stock: variation.manage_stock || false,
|
||||||
|
weight: variation.weight || '',
|
||||||
|
length: variation.dimensions?.length || '',
|
||||||
|
width: variation.dimensions?.width || '',
|
||||||
|
height: variation.dimensions?.height || '',
|
||||||
|
shipping_class: variation.shipping_class || '',
|
||||||
|
tax_class: variation.tax_class || '',
|
||||||
|
menu_order: variation.menu_order || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取产品变体列表
|
||||||
|
async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise<UnifiedVariationPaginationDTO> {
|
||||||
|
try {
|
||||||
|
const page = Number(params.page ?? 1);
|
||||||
|
const per_page = Number(params.per_page ?? 20);
|
||||||
|
const result = await this.wpService.getVariations(this.site, Number(productId), page, per_page);
|
||||||
|
|
||||||
|
// 获取产品名称用于变体显示
|
||||||
|
const product = await this.wpService.getProduct(this.site, Number(productId));
|
||||||
|
const productName = product?.name || '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: (result.items as any[]).map((variation: any) => this.mapVariation(variation, productName)),
|
||||||
|
total: result.total,
|
||||||
|
page: result.page,
|
||||||
|
per_page: result.per_page,
|
||||||
|
totalPages: result.totalPages,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`获取产品变体列表失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有产品变体
|
||||||
|
async getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise<UnifiedProductVariationDTO[]> {
|
||||||
|
try {
|
||||||
|
const api = (this.wpService as any).createApi(this.site, 'wc/v3');
|
||||||
|
const variations = await this.wpService.sdkGetAll(api, `products/${productId}/variations`, params);
|
||||||
|
|
||||||
|
// 获取产品名称用于变体显示
|
||||||
|
const product = await this.wpService.getProduct(this.site, Number(productId));
|
||||||
|
const productName = product?.name || '';
|
||||||
|
|
||||||
|
return variations.map((variation: any) => this.mapVariation(variation, productName));
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`获取所有产品变体失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取单个产品变体
|
||||||
|
async getVariation(productId: string | number, variationId: string | number): Promise<UnifiedProductVariationDTO> {
|
||||||
|
try {
|
||||||
|
const variation = await this.wpService.getVariation(this.site, Number(productId), Number(variationId));
|
||||||
|
|
||||||
|
// 获取产品名称用于变体显示
|
||||||
|
const product = await this.wpService.getProduct(this.site, Number(productId));
|
||||||
|
const productName = product?.name || '';
|
||||||
|
|
||||||
|
return this.mapVariation(variation, productName);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`获取产品变体失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建产品变体
|
||||||
|
async createVariation(productId: string | number, data: CreateVariationDTO): Promise<UnifiedProductVariationDTO> {
|
||||||
|
try {
|
||||||
|
// 将统一DTO转换为WooCommerce API格式
|
||||||
|
const createData: any = {
|
||||||
|
sku: data.sku,
|
||||||
|
regular_price: data.regular_price,
|
||||||
|
sale_price: data.sale_price,
|
||||||
|
stock_status: data.stock_status,
|
||||||
|
stock_quantity: data.stock_quantity,
|
||||||
|
description: data.description,
|
||||||
|
status: data.enabled ? 'publish' : 'draft',
|
||||||
|
downloadable: data.downloadable,
|
||||||
|
virtual: data.virtual,
|
||||||
|
manage_stock: data.manage_stock,
|
||||||
|
weight: data.weight,
|
||||||
|
dimensions: {
|
||||||
|
length: data.length,
|
||||||
|
width: data.width,
|
||||||
|
height: data.height,
|
||||||
|
},
|
||||||
|
shipping_class: data.shipping_class,
|
||||||
|
tax_class: data.tax_class,
|
||||||
|
menu_order: data.menu_order,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 映射属性
|
||||||
|
if (data.attributes && Array.isArray(data.attributes)) {
|
||||||
|
createData.attributes = data.attributes.map(attr => ({
|
||||||
|
id: attr.id,
|
||||||
|
name: attr.name,
|
||||||
|
option: attr.option || attr.options?.[0] || '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 映射图片
|
||||||
|
if (data.image) {
|
||||||
|
createData.image = {
|
||||||
|
id: data.image.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const variation = await this.wpService.createVariation(this.site, String(productId), createData);
|
||||||
|
|
||||||
|
// 获取产品名称用于变体显示
|
||||||
|
const product = await this.wpService.getProduct(this.site, Number(productId));
|
||||||
|
const productName = product?.name || '';
|
||||||
|
|
||||||
|
return this.mapVariation(variation, productName);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`创建产品变体失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新产品变体
|
||||||
|
async updateVariation(productId: string | number, variationId: string | number, data: UpdateVariationDTO): Promise<UnifiedProductVariationDTO> {
|
||||||
|
try {
|
||||||
|
// 将统一DTO转换为WooCommerce API格式
|
||||||
|
const updateData: any = {
|
||||||
|
sku: data.sku,
|
||||||
|
regular_price: data.regular_price,
|
||||||
|
sale_price: data.sale_price,
|
||||||
|
stock_status: data.stock_status,
|
||||||
|
stock_quantity: data.stock_quantity,
|
||||||
|
description: data.description,
|
||||||
|
status: data.enabled !== undefined ? (data.enabled ? 'publish' : 'draft') : undefined,
|
||||||
|
downloadable: data.downloadable,
|
||||||
|
virtual: data.virtual,
|
||||||
|
manage_stock: data.manage_stock,
|
||||||
|
weight: data.weight,
|
||||||
|
shipping_class: data.shipping_class,
|
||||||
|
tax_class: data.tax_class,
|
||||||
|
menu_order: data.menu_order,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 映射尺寸
|
||||||
|
if (data.length || data.width || data.height) {
|
||||||
|
updateData.dimensions = {};
|
||||||
|
if (data.length) updateData.dimensions.length = data.length;
|
||||||
|
if (data.width) updateData.dimensions.width = data.width;
|
||||||
|
if (data.height) updateData.dimensions.height = data.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 映射属性
|
||||||
|
if (data.attributes && Array.isArray(data.attributes)) {
|
||||||
|
updateData.attributes = data.attributes.map(attr => ({
|
||||||
|
id: attr.id,
|
||||||
|
name: attr.name,
|
||||||
|
option: attr.option || attr.options?.[0] || '',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 映射图片
|
||||||
|
if (data.image) {
|
||||||
|
updateData.image = {
|
||||||
|
id: data.image.id,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const variation = await this.wpService.updateVariation(this.site, String(productId), String(variationId), updateData);
|
||||||
|
|
||||||
|
// 获取产品名称用于变体显示
|
||||||
|
const product = await this.wpService.getProduct(this.site, Number(productId));
|
||||||
|
const productName = product?.name || '';
|
||||||
|
|
||||||
|
return this.mapVariation(variation, productName);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`更新产品变体失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除产品变体
|
||||||
|
async deleteVariation(productId: string | number, variationId: string | number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await this.wpService.deleteVariation(this.site, String(productId), String(variationId));
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`删除产品变体失败: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,6 @@ import { DictItem } from '../entity/dict_item.entity';
|
||||||
import { Template } from '../entity/template.entity';
|
import { Template } from '../entity/template.entity';
|
||||||
import { Area } from '../entity/area.entity';
|
import { Area } from '../entity/area.entity';
|
||||||
import { ProductStockComponent } from '../entity/product_stock_component.entity';
|
import { ProductStockComponent } from '../entity/product_stock_component.entity';
|
||||||
import { ProductSiteSku } from '../entity/product_site_sku.entity';
|
|
||||||
import { CategoryAttribute } from '../entity/category_attribute.entity';
|
import { CategoryAttribute } from '../entity/category_attribute.entity';
|
||||||
import { Category } from '../entity/category.entity';
|
import { Category } from '../entity/category.entity';
|
||||||
import DictSeeder from '../db/seeds/dict.seeder';
|
import DictSeeder from '../db/seeds/dict.seeder';
|
||||||
|
|
@ -50,7 +49,6 @@ export default {
|
||||||
entities: [
|
entities: [
|
||||||
Product,
|
Product,
|
||||||
ProductStockComponent,
|
ProductStockComponent,
|
||||||
ProductSiteSku,
|
|
||||||
User,
|
User,
|
||||||
PurchaseOrder,
|
PurchaseOrder,
|
||||||
PurchaseOrderItem,
|
PurchaseOrderItem,
|
||||||
|
|
@ -133,7 +131,7 @@ export default {
|
||||||
// mode: 'file', // 默认为file,即上传到服务器临时目录,可以配置为 stream
|
// mode: 'file', // 默认为file,即上传到服务器临时目录,可以配置为 stream
|
||||||
mode: 'file',
|
mode: 'file',
|
||||||
fileSize: '10mb', // 最大支持的文件大小,默认为 10mb
|
fileSize: '10mb', // 最大支持的文件大小,默认为 10mb
|
||||||
whitelist: ['.csv'], // 支持的文件后缀
|
whitelist: ['.csv', '.xlsx'], // 支持的文件后缀
|
||||||
tmpdir: join(__dirname, '../../tmp_uploads'),
|
tmpdir: join(__dirname, '../../tmp_uploads'),
|
||||||
cleanTimeout: 5 * 60 * 1000,
|
cleanTimeout: 5 * 60 * 1000,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,14 @@ import {
|
||||||
App,
|
App,
|
||||||
Inject,
|
Inject,
|
||||||
MidwayDecoratorService,
|
MidwayDecoratorService,
|
||||||
|
Logger,
|
||||||
|
Config,
|
||||||
} from '@midwayjs/core';
|
} from '@midwayjs/core';
|
||||||
import * as koa from '@midwayjs/koa';
|
import * as koa from '@midwayjs/koa';
|
||||||
import * as validate from '@midwayjs/validate';
|
import * as validate from '@midwayjs/validate';
|
||||||
import * as info from '@midwayjs/info';
|
import * as info from '@midwayjs/info';
|
||||||
import * as orm from '@midwayjs/typeorm';
|
import * as orm from '@midwayjs/typeorm';
|
||||||
|
import { DataSource } from 'typeorm';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { DefaultErrorFilter } from './filter/default.filter';
|
import { DefaultErrorFilter } from './filter/default.filter';
|
||||||
import { NotFoundFilter } from './filter/notfound.filter';
|
import { NotFoundFilter } from './filter/notfound.filter';
|
||||||
|
|
@ -52,7 +55,19 @@ export class MainConfiguration {
|
||||||
@Inject()
|
@Inject()
|
||||||
siteService: SiteService;
|
siteService: SiteService;
|
||||||
|
|
||||||
|
@Logger()
|
||||||
|
logger; // 注入 Logger 实例
|
||||||
|
|
||||||
|
@Config('typeorm.dataSource.default')
|
||||||
|
typeormDataSourceConfig; // 注入 TypeORM 数据源配置
|
||||||
|
|
||||||
|
async onConfigLoad() {
|
||||||
|
// 在组件初始化之前,先检查并创建数据库
|
||||||
|
await this.initializeDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
async onReady() {
|
async onReady() {
|
||||||
|
|
||||||
// add middleware
|
// add middleware
|
||||||
this.app.useMiddleware([QueryNormalizeMiddleware, ReportMiddleware, AuthMiddleware]);
|
this.app.useMiddleware([QueryNormalizeMiddleware, ReportMiddleware, AuthMiddleware]);
|
||||||
// add filter
|
// add filter
|
||||||
|
|
@ -82,4 +97,70 @@ export class MainConfiguration {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化数据库(如果不存在则创建)
|
||||||
|
*/
|
||||||
|
private async initializeDatabase(): Promise<void> {
|
||||||
|
// 使用注入的数据库配置
|
||||||
|
const typeormConfig = this.typeormDataSourceConfig;
|
||||||
|
|
||||||
|
// 创建一个临时的 DataSource,不指定数据库,用于创建数据库
|
||||||
|
const tempDataSource = new DataSource({
|
||||||
|
type: 'mysql',
|
||||||
|
host: typeormConfig.host,
|
||||||
|
port: typeormConfig.port,
|
||||||
|
username: typeormConfig.username,
|
||||||
|
password: typeormConfig.password,
|
||||||
|
database: undefined, // 不指定数据库
|
||||||
|
synchronize: true,
|
||||||
|
logging: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.logger.info('正在检查数据库是否存在...');
|
||||||
|
|
||||||
|
// 初始化临时数据源
|
||||||
|
await tempDataSource.initialize();
|
||||||
|
|
||||||
|
// 创建查询运行器
|
||||||
|
const queryRunner = tempDataSource.createQueryRunner();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查数据库是否存在
|
||||||
|
const databases = await queryRunner.query(
|
||||||
|
`SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?`,
|
||||||
|
[typeormConfig.database]
|
||||||
|
);
|
||||||
|
|
||||||
|
const databaseExists = Array.isArray(databases) && databases.length > 0;
|
||||||
|
|
||||||
|
if (!databaseExists) {
|
||||||
|
this.logger.info(`数据库 ${typeormConfig.database} 不存在,正在创建...`);
|
||||||
|
|
||||||
|
// 创建数据库
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE DATABASE \`${typeormConfig.database}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.info(`数据库 ${typeormConfig.database} 创建成功`);
|
||||||
|
} else {
|
||||||
|
this.logger.info(`数据库 ${typeormConfig.database} 已存在`);
|
||||||
|
}
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
// 释放查询运行器
|
||||||
|
await queryRunner.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('数据库初始化失败:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// 关闭临时数据源
|
||||||
|
if (tempDataSource.isInitialized) {
|
||||||
|
await tempDataSource.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Controller, Get, Post, Put, Del, Body, Query, Inject, Param } from '@mi
|
||||||
import { CategoryService } from '../service/category.service';
|
import { CategoryService } from '../service/category.service';
|
||||||
import { successResponse, errorResponse } from '../utils/response.util';
|
import { successResponse, errorResponse } from '../utils/response.util';
|
||||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||||
|
import { CreateCategoryDTO, UpdateCategoryDTO } from '../dto/product.dto';
|
||||||
|
|
||||||
@Controller('/category')
|
@Controller('/category')
|
||||||
export class CategoryController {
|
export class CategoryController {
|
||||||
|
|
@ -32,7 +33,7 @@ export class CategoryController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Post('/')
|
@Post('/')
|
||||||
async create(@Body() body: any) {
|
async create(@Body() body: CreateCategoryDTO) {
|
||||||
try {
|
try {
|
||||||
const data = await this.categoryService.create(body);
|
const data = await this.categoryService.create(body);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
|
|
@ -43,7 +44,7 @@ export class CategoryController {
|
||||||
|
|
||||||
@ApiOkResponse()
|
@ApiOkResponse()
|
||||||
@Put('/:id')
|
@Put('/:id')
|
||||||
async update(@Param('id') id: number, @Body() body: any) {
|
async update(@Param('id') id: number, @Body() body: UpdateCategoryDTO) {
|
||||||
try {
|
try {
|
||||||
const data = await this.categoryService.update(id, body);
|
const data = await this.categoryService.update(id, body);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,27 @@
|
||||||
import { Controller, Get, Post, Inject, Query, Body } from '@midwayjs/core';
|
import { Controller, Get, Post, Inject, Query, Body, Put, Del, Param } from '@midwayjs/core';
|
||||||
import { successResponse, errorResponse } from '../utils/response.util';
|
import { successResponse, errorResponse, ApiResponse } from '../utils/response.util';
|
||||||
import { CustomerService } from '../service/customer.service';
|
import { CustomerService } from '../service/customer.service';
|
||||||
import { QueryCustomerListDTO, CustomerTagDTO } from '../dto/customer.dto';
|
import { CustomerQueryParamsDTO, CreateCustomerDTO, UpdateCustomerDTO, GetCustomerDTO, BatchCreateCustomerDTO, BatchUpdateCustomerDTO, BatchDeleteCustomerDTO, SyncCustomersDTO } from '../dto/customer.dto';
|
||||||
|
import { ApiProperty } from '@midwayjs/swagger';
|
||||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||||
import { UnifiedSearchParamsDTO } from '../dto/site-api.dto';
|
import { UnifiedPaginationDTO } from '../dto/api.dto';
|
||||||
|
|
||||||
|
export class CustomerTagDTO {
|
||||||
|
@ApiProperty({ description: '客户邮箱' })
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '标签名称' })
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Controller('/customer')
|
@Controller('/customer')
|
||||||
export class CustomerController {
|
export class CustomerController {
|
||||||
@Inject()
|
@Inject()
|
||||||
customerService: CustomerService;
|
customerService: CustomerService;
|
||||||
|
|
||||||
@ApiOkResponse({ type: Object })
|
@ApiOkResponse({ type: ApiResponse<UnifiedPaginationDTO<GetCustomerDTO>> })
|
||||||
@Get('/getcustomerlist')
|
@Get('/list')
|
||||||
async getCustomerList(@Query() query: QueryCustomerListDTO) {
|
async getCustomerList(@Query() query: CustomerQueryParamsDTO) {
|
||||||
try {
|
try {
|
||||||
const result = await this.customerService.getCustomerList(query)
|
const result = await this.customerService.getCustomerList(query)
|
||||||
return successResponse(result);
|
return successResponse(result);
|
||||||
|
|
@ -22,8 +31,8 @@ export class CustomerController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOkResponse({ type: Object })
|
@ApiOkResponse({ type: Object })
|
||||||
@Get('/getcustomerstatisticlist')
|
@Get('/statistic/list')
|
||||||
async getCustomerStatisticList(@Query() query: QueryCustomerListDTO) {
|
async getCustomerStatisticList(@Query() query: CustomerQueryParamsDTO) {
|
||||||
try {
|
try {
|
||||||
const result = await this.customerService.getCustomerStatisticList(query as any);
|
const result = await this.customerService.getCustomerStatisticList(query as any);
|
||||||
return successResponse(result);
|
return successResponse(result);
|
||||||
|
|
@ -79,11 +88,11 @@ export class CustomerController {
|
||||||
/**
|
/**
|
||||||
* 同步客户数据
|
* 同步客户数据
|
||||||
* 从指定站点获取客户数据并保存到本地数据库
|
* 从指定站点获取客户数据并保存到本地数据库
|
||||||
* 业务逻辑已移到service层,controller只负责参数传递和响应
|
* 支持通过where和orderBy参数筛选和排序要同步的客户数据
|
||||||
*/
|
*/
|
||||||
@ApiOkResponse({ type: Object })
|
@ApiOkResponse({ type: Object })
|
||||||
@Post('/sync')
|
@Post('/sync')
|
||||||
async syncCustomers(@Body() body: { siteId: number; params?: UnifiedSearchParamsDTO }) {
|
async syncCustomers(@Body() body: SyncCustomersDTO) {
|
||||||
try {
|
try {
|
||||||
const { siteId, params = {} } = body;
|
const { siteId, params = {} } = body;
|
||||||
|
|
||||||
|
|
@ -95,4 +104,147 @@ export class CustomerController {
|
||||||
return errorResponse(error.message);
|
return errorResponse(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ====================== 单个客户CRUD操作 ======================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建单个客户
|
||||||
|
* 使用CreateCustomerDTO进行数据验证
|
||||||
|
*/
|
||||||
|
@ApiOkResponse({ type: GetCustomerDTO })
|
||||||
|
@Post('/')
|
||||||
|
async createCustomer(@Body() body: CreateCustomerDTO) {
|
||||||
|
try {
|
||||||
|
// 调用service层的upsertCustomer方法
|
||||||
|
const result = await this.customerService.upsertCustomer({...body, origin_id: String(body.origin_id)});
|
||||||
|
return successResponse(result.customer);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID获取单个客户
|
||||||
|
* 返回GetCustomerDTO格式的客户信息
|
||||||
|
*/
|
||||||
|
@ApiOkResponse({ type: GetCustomerDTO })
|
||||||
|
@Get('/:id')
|
||||||
|
async getCustomerById(@Param('id') id: number) {
|
||||||
|
try {
|
||||||
|
const customer = await this.customerService.customerModel.findOne({ where: { id } });
|
||||||
|
if (!customer) {
|
||||||
|
return errorResponse('客户不存在');
|
||||||
|
}
|
||||||
|
return successResponse(customer);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新单个客户
|
||||||
|
* 使用UpdateCustomerDTO进行数据验证
|
||||||
|
*/
|
||||||
|
@ApiOkResponse({ type: GetCustomerDTO })
|
||||||
|
@Put('/:id')
|
||||||
|
async updateCustomer(@Param('id') id: number, @Body() body: UpdateCustomerDTO) {
|
||||||
|
try {
|
||||||
|
const customer = await this.customerService.updateCustomer(id, body);
|
||||||
|
if (!customer) {
|
||||||
|
return errorResponse('客户不存在');
|
||||||
|
}
|
||||||
|
return successResponse(customer);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除单个客户
|
||||||
|
* 根据客户ID删除客户记录
|
||||||
|
*/
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
@Del('/:id')
|
||||||
|
async deleteCustomer(@Param('id') id: number) {
|
||||||
|
try {
|
||||||
|
// 先检查客户是否存在
|
||||||
|
const customer = await this.customerService.customerModel.findOne({ where: { id } });
|
||||||
|
if (!customer) {
|
||||||
|
return errorResponse('客户不存在');
|
||||||
|
}
|
||||||
|
// 删除客户
|
||||||
|
await this.customerService.customerModel.delete(id);
|
||||||
|
return successResponse({ message: '删除成功' });
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 批量客户操作 ======================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量创建客户
|
||||||
|
* 使用BatchCreateCustomerDTO进行数据验证
|
||||||
|
*/
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
@Post('/batch')
|
||||||
|
async batchCreateCustomers(@Body() body: BatchCreateCustomerDTO) {
|
||||||
|
try {
|
||||||
|
const result = await this.customerService.upsertManyCustomers(body.customers.map(c => ({ ...c, origin_id: String(c.origin_id) })));
|
||||||
|
return successResponse(result);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新客户
|
||||||
|
* 使用BatchUpdateCustomerDTO进行数据验证
|
||||||
|
* 每个客户可以有独立的更新字段,支持统一化修改或分别更新
|
||||||
|
*/
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
@Put('/batch')
|
||||||
|
async batchUpdateCustomers(@Body() body: BatchUpdateCustomerDTO) {
|
||||||
|
try {
|
||||||
|
const { customers } = body;
|
||||||
|
|
||||||
|
// 调用service层的批量更新方法
|
||||||
|
const result = await this.customerService.batchUpdateCustomers(customers);
|
||||||
|
|
||||||
|
return successResponse({
|
||||||
|
total: result.total,
|
||||||
|
updated: result.updated,
|
||||||
|
processed: result.processed,
|
||||||
|
errors: result.errors,
|
||||||
|
message: `成功更新${result.updated}个客户,共处理${result.processed}个`
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除客户
|
||||||
|
* 使用BatchDeleteCustomerDTO进行数据验证
|
||||||
|
*/
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
@Del('/batch')
|
||||||
|
async batchDeleteCustomers(@Body() body: BatchDeleteCustomerDTO) {
|
||||||
|
try {
|
||||||
|
const { ids } = body;
|
||||||
|
|
||||||
|
// 调用service层的批量删除方法
|
||||||
|
const result = await this.customerService.batchDeleteCustomers(ids);
|
||||||
|
|
||||||
|
return successResponse({
|
||||||
|
total: result.total,
|
||||||
|
updated: result.updated,
|
||||||
|
processed: result.processed,
|
||||||
|
errors: result.errors,
|
||||||
|
message: `成功删除${result.updated}个客户,共处理${result.processed}个`
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
|
|
||||||
import { Inject, Controller, Get, Post, Put, Del, Query, Body, Param, Files, ContentType } from '@midwayjs/core';
|
import { Inject, Controller, Get, Post, Put, Del, Query, Body, Param, Files, Fields, ContentType } from '@midwayjs/core';
|
||||||
import { DictService } from '../service/dict.service';
|
import { DictService } from '../service/dict.service';
|
||||||
import { CreateDictDTO, UpdateDictDTO, CreateDictItemDTO, UpdateDictItemDTO } from '../dto/dict.dto';
|
import { CreateDictDTO, UpdateDictDTO, CreateDictItemDTO, UpdateDictItemDTO } from '../dto/dict.dto';
|
||||||
import { Validate } from '@midwayjs/validate';
|
import { Validate } from '@midwayjs/validate';
|
||||||
import { Context } from '@midwayjs/koa';
|
import { Context } from '@midwayjs/koa';
|
||||||
import { successResponse, errorResponse } from '../utils/response.util';
|
import { successResponse, errorResponse, ApiResponse } from '../utils/response.util';
|
||||||
|
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||||
|
import { BatchOperationResult } from '../dto/api.dto';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 字典管理
|
* 字典管理
|
||||||
|
|
@ -117,17 +119,20 @@ export class DictController {
|
||||||
/**
|
/**
|
||||||
* 批量导入字典项
|
* 批量导入字典项
|
||||||
* @param files 上传的文件
|
* @param files 上传的文件
|
||||||
* @param body 请求体,包含字典ID
|
* @param fields FormData中的字段
|
||||||
*/
|
*/
|
||||||
|
@ApiOkResponse({type:ApiResponse<BatchOperationResult>})
|
||||||
@Post('/item/import')
|
@Post('/item/import')
|
||||||
@Validate()
|
@Validate()
|
||||||
async importDictItems(@Files() files: any, @Body() body: { dictId: number }) {
|
async importDictItems(@Files() files: any, @Fields() fields: { dictId: number }) {
|
||||||
// 获取第一个文件
|
// 获取第一个文件
|
||||||
const file = files[0];
|
const file = files[0];
|
||||||
|
// 从fields中获取字典ID
|
||||||
|
const dictId = fields.dictId;
|
||||||
// 调用服务层方法
|
// 调用服务层方法
|
||||||
const result = await this.dictService.importDictItemsFromXLSX(file.data, body.dictId);
|
const result = await this.dictService.importDictItemsFromXLSX(file.data, dictId);
|
||||||
// 返回结果
|
// 返回结果
|
||||||
return result;
|
return successResponse(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -142,6 +147,19 @@ export class DictController {
|
||||||
return this.dictService.getDictItemXLSXTemplate();
|
return this.dictService.getDictItemXLSXTemplate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出字典项为 XLSX 文件
|
||||||
|
* @param dictId 字典ID
|
||||||
|
*/
|
||||||
|
@Get('/item/export')
|
||||||
|
@ContentType('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||||
|
async exportDictItems(@Query('dictId') dictId: number) {
|
||||||
|
// 设置下载文件名
|
||||||
|
this.ctx.set('Content-Disposition', 'attachment; filename=dict-items.xlsx');
|
||||||
|
// 返回导出的 XLSX 文件内容
|
||||||
|
return this.dictService.exportDictItemsToXLSX(dictId);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取字典项列表
|
* 获取字典项列表
|
||||||
* @param dictId 字典ID (可选)
|
* @param dictId 字典ID (可选)
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@ export class OrderController {
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
type: BooleanRes,
|
type: BooleanRes,
|
||||||
})
|
})
|
||||||
@Post('/syncOrder/:siteId')
|
@Post('/sync/:siteId')
|
||||||
async syncOrder(@Param('siteId') siteId: number, @Body() params: Record<string, any>) {
|
async syncOrders(@Param('siteId') siteId: number, @Body() params: Record<string, any>) {
|
||||||
try {
|
try {
|
||||||
const result = await this.orderService.syncOrders(siteId, params);
|
const result = await this.orderService.syncOrders(siteId, params);
|
||||||
return successResponse(result);
|
return successResponse(result);
|
||||||
|
|
@ -55,8 +55,8 @@ export class OrderController {
|
||||||
@Param('orderId') orderId: string
|
@Param('orderId') orderId: string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
await this.orderService.syncOrderById(siteId, orderId);
|
const result = await this.orderService.syncOrderById(siteId, orderId);
|
||||||
return successResponse(true);
|
return successResponse(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
return errorResponse('同步失败');
|
return errorResponse('同步失败');
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,26 @@
|
||||||
import {
|
import {
|
||||||
|
Body,
|
||||||
|
ContentType,
|
||||||
|
Controller,
|
||||||
|
Del,
|
||||||
|
Files,
|
||||||
|
Get,
|
||||||
Inject,
|
Inject,
|
||||||
|
Param,
|
||||||
Post,
|
Post,
|
||||||
Put,
|
Put,
|
||||||
Get,
|
|
||||||
Body,
|
|
||||||
Param,
|
|
||||||
Del,
|
|
||||||
Query,
|
Query,
|
||||||
Controller,
|
|
||||||
} from '@midwayjs/core';
|
} from '@midwayjs/core';
|
||||||
|
import { Context } from '@midwayjs/koa';
|
||||||
|
import { ILogger } from '@midwayjs/logger';
|
||||||
|
import { ApiOkResponse } from '@midwayjs/swagger';
|
||||||
|
import { UnifiedSearchParamsDTO } from '../dto/api.dto';
|
||||||
|
import { SyncOperationResultDTO } from '../dto/batch.dto';
|
||||||
|
import { BatchDeleteProductDTO, BatchUpdateProductDTO, CreateCategoryDTO, CreateProductDTO, ProductWhereFilter, UpdateCategoryDTO, UpdateProductDTO } from '../dto/product.dto';
|
||||||
|
import { BooleanRes, ProductListRes, ProductRes, ProductsRes } from '../dto/reponse.dto';
|
||||||
|
import { BatchSyncProductToSiteDTO, SyncProductToSiteDTO, SyncProductToSiteResultDTO } from '../dto/site-sync.dto';
|
||||||
import { ProductService } from '../service/product.service';
|
import { ProductService } from '../service/product.service';
|
||||||
import { errorResponse, successResponse } from '../utils/response.util';
|
import { errorResponse, successResponse } from '../utils/response.util';
|
||||||
import { CreateProductDTO, QueryProductDTO, UpdateProductDTO, BatchUpdateProductDTO, BatchDeleteProductDTO } from '../dto/product.dto';
|
|
||||||
import { ApiOkResponse } from '@midwayjs/swagger';
|
|
||||||
import { BooleanRes, ProductListRes, ProductRes, ProductsRes } from '../dto/reponse.dto';
|
|
||||||
import { ContentType, Files } from '@midwayjs/core';
|
|
||||||
import { Context } from '@midwayjs/koa';
|
|
||||||
|
|
||||||
@Controller('/product')
|
@Controller('/product')
|
||||||
export class ProductController {
|
export class ProductController {
|
||||||
|
|
@ -25,6 +30,9 @@ export class ProductController {
|
||||||
@Inject()
|
@Inject()
|
||||||
ctx: Context;
|
ctx: Context;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
logger: ILogger;
|
||||||
|
|
||||||
@ApiOkResponse({
|
@ApiOkResponse({
|
||||||
description: '通过name搜索产品',
|
description: '通过name搜索产品',
|
||||||
type: ProductsRes,
|
type: ProductsRes,
|
||||||
|
|
@ -60,20 +68,13 @@ export class ProductController {
|
||||||
})
|
})
|
||||||
@Get('/list')
|
@Get('/list')
|
||||||
async getProductList(
|
async getProductList(
|
||||||
@Query() query: QueryProductDTO
|
@Query() query: UnifiedSearchParamsDTO<ProductWhereFilter>
|
||||||
): Promise<ProductListRes> {
|
): Promise<ProductListRes> {
|
||||||
const { current = 1, pageSize = 10, name, brandId, sortField, sortOrder } = query;
|
|
||||||
try {
|
try {
|
||||||
const data = await this.productService.getProductList(
|
const data = await this.productService.getProductList(query);
|
||||||
{ current, pageSize },
|
|
||||||
name,
|
|
||||||
brandId,
|
|
||||||
sortField,
|
|
||||||
sortOrder
|
|
||||||
);
|
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
this.logger.error('获取产品列表失败', error);
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -613,7 +614,7 @@ export class ProductController {
|
||||||
// 创建分类
|
// 创建分类
|
||||||
@ApiOkResponse({ description: '创建分类' })
|
@ApiOkResponse({ description: '创建分类' })
|
||||||
@Post('/category')
|
@Post('/category')
|
||||||
async createCategory(@Body() body: any) {
|
async createCategory(@Body() body: CreateCategoryDTO) {
|
||||||
try {
|
try {
|
||||||
const data = await this.productService.createCategory(body);
|
const data = await this.productService.createCategory(body);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
|
|
@ -625,7 +626,7 @@ export class ProductController {
|
||||||
// 更新分类
|
// 更新分类
|
||||||
@ApiOkResponse({ description: '更新分类' })
|
@ApiOkResponse({ description: '更新分类' })
|
||||||
@Put('/category/:id')
|
@Put('/category/:id')
|
||||||
async updateCategory(@Param('id') id: number, @Body() body: any) {
|
async updateCategory(@Param('id') id: number, @Body() body: UpdateCategoryDTO) {
|
||||||
try {
|
try {
|
||||||
const data = await this.productService.updateCategory(id, body);
|
const data = await this.productService.updateCategory(id, body);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
|
|
@ -681,4 +682,71 @@ export class ProductController {
|
||||||
return errorResponse(error?.message || error);
|
return errorResponse(error?.message || error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同步单个产品到站点
|
||||||
|
@ApiOkResponse({ description: '同步单个产品到站点', type: SyncProductToSiteResultDTO })
|
||||||
|
@Post('/sync-to-site')
|
||||||
|
async syncToSite(@Body() body: SyncProductToSiteDTO) {
|
||||||
|
try {
|
||||||
|
const result = await this.productService.syncToSite(body);
|
||||||
|
return successResponse(result);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从站点同步产品到本地
|
||||||
|
@ApiOkResponse({ description: '从站点同步产品到本地', type: ProductRes })
|
||||||
|
@Post('/sync-from-site')
|
||||||
|
async syncProductFromSite(@Body() body: { siteId: number; siteProductId: string | number }) {
|
||||||
|
try {
|
||||||
|
const { siteId, siteProductId } = body;
|
||||||
|
const product = await this.productService.syncProductFromSite(siteId, siteProductId);
|
||||||
|
return successResponse(product);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量从站点同步产品到本地
|
||||||
|
@ApiOkResponse({ description: '批量从站点同步产品到本地', type: SyncOperationResultDTO })
|
||||||
|
@Post('/batch-sync-from-site')
|
||||||
|
async batchSyncFromSite(@Body() body: { siteId: number; siteProductIds: (string | number)[] }) {
|
||||||
|
try {
|
||||||
|
const { siteId, siteProductIds } = body;
|
||||||
|
const result = await this.productService.batchSyncFromSite(siteId, siteProductIds);
|
||||||
|
// 将服务层返回的结果转换为统一格式
|
||||||
|
const errors = result.errors.map((error: string) => {
|
||||||
|
// 提取产品ID部分作为标识符
|
||||||
|
const match = error.match(/站点产品ID (\d+) /);
|
||||||
|
const identifier = match ? match[1] : 'unknown';
|
||||||
|
return {
|
||||||
|
identifier: identifier,
|
||||||
|
error: error
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return successResponse({
|
||||||
|
total: siteProductIds.length,
|
||||||
|
processed: result.synced + errors.length,
|
||||||
|
synced: result.synced,
|
||||||
|
errors: errors
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量同步产品到站点
|
||||||
|
@ApiOkResponse({ description: '批量同步产品到站点', type: SyncOperationResultDTO })
|
||||||
|
@Post('/batch-sync-to-site')
|
||||||
|
async batchSyncToSite(@Body() body: BatchSyncProductToSiteDTO) {
|
||||||
|
try {
|
||||||
|
const { siteId, data } = body;
|
||||||
|
const result = await this.productService.batchSyncToSite(siteId, data);
|
||||||
|
return successResponse(result);
|
||||||
|
} catch (error) {
|
||||||
|
return errorResponse(error?.message || error);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +1,31 @@
|
||||||
import { Controller, Get, Inject, Param, Query, Body, Post, Put, Del } from '@midwayjs/core';
|
import { Body, Controller, Del, Get, ILogger, Inject, Param, Post, Put, Query } from '@midwayjs/core';
|
||||||
import { ApiOkResponse, ApiBody } from '@midwayjs/swagger';
|
import { ApiBody, ApiOkResponse } from '@midwayjs/swagger';
|
||||||
|
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto';
|
||||||
|
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||||
import {
|
import {
|
||||||
UploadMediaDTO,
|
BatchFulfillmentsDTO,
|
||||||
|
CancelFulfillmentDTO,
|
||||||
|
CreateReviewDTO,
|
||||||
|
CreateWebhookDTO,
|
||||||
|
FulfillmentDTO,
|
||||||
|
UnifiedCustomerDTO,
|
||||||
|
UnifiedCustomerPaginationDTO,
|
||||||
UnifiedMediaPaginationDTO,
|
UnifiedMediaPaginationDTO,
|
||||||
UnifiedOrderDTO,
|
UnifiedOrderDTO,
|
||||||
UnifiedOrderPaginationDTO,
|
UnifiedOrderPaginationDTO,
|
||||||
|
UnifiedOrderTrackingDTO,
|
||||||
UnifiedProductDTO,
|
UnifiedProductDTO,
|
||||||
UnifiedProductPaginationDTO,
|
UnifiedProductPaginationDTO,
|
||||||
UnifiedSearchParamsDTO,
|
|
||||||
UnifiedSubscriptionPaginationDTO,
|
|
||||||
UnifiedCustomerDTO,
|
|
||||||
UnifiedCustomerPaginationDTO,
|
|
||||||
UnifiedReviewPaginationDTO,
|
|
||||||
UnifiedReviewDTO,
|
UnifiedReviewDTO,
|
||||||
CreateReviewDTO,
|
UnifiedReviewPaginationDTO,
|
||||||
UpdateReviewDTO,
|
UnifiedSubscriptionPaginationDTO,
|
||||||
UnifiedWebhookDTO,
|
UnifiedWebhookDTO,
|
||||||
CreateWebhookDTO,
|
UpdateReviewDTO,
|
||||||
UpdateWebhookDTO,
|
UpdateWebhookDTO,
|
||||||
UnifiedPaginationDTO,
|
UploadMediaDTO,
|
||||||
ShipOrderDTO,
|
|
||||||
CancelShipOrderDTO,
|
|
||||||
BatchShipOrdersDTO,
|
|
||||||
} from '../dto/site-api.dto';
|
} from '../dto/site-api.dto';
|
||||||
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
|
||||||
import { SiteApiService } from '../service/site-api.service';
|
import { SiteApiService } from '../service/site-api.service';
|
||||||
import { errorResponse, successResponse } from '../utils/response.util';
|
import { errorResponse, successResponse } from '../utils/response.util';
|
||||||
import { ILogger } from '@midwayjs/core';
|
|
||||||
|
|
||||||
|
|
||||||
@Controller('/site-api')
|
@Controller('/site-api')
|
||||||
|
|
@ -495,6 +494,23 @@ export class SiteApiController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/products/upsert')
|
||||||
|
@ApiOkResponse({ type: UnifiedProductDTO })
|
||||||
|
async upsertProduct(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: UnifiedProductDTO
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 更新或创建产品开始, siteId: ${siteId}, 产品名称: ${body.name}`);
|
||||||
|
try {
|
||||||
|
const data = await this.siteApiService.upsertProduct(siteId, body);
|
||||||
|
this.logger.info(`[Site API] 更新或创建产品成功, siteId: ${siteId}, 产品ID: ${data.id}`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 更新或创建产品失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Put('/:siteId/products/:productId/variations/:variationId')
|
@Put('/:siteId/products/:productId/variations/:variationId')
|
||||||
@ApiOkResponse({ type: Object })
|
@ApiOkResponse({ type: Object })
|
||||||
async updateVariation(
|
async updateVariation(
|
||||||
|
|
@ -612,6 +628,32 @@ export class SiteApiController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/products/batch-upsert')
|
||||||
|
@ApiOkResponse({ type: BatchOperationResultDTO })
|
||||||
|
async batchUpsertProduct(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Body() body: { items: UnifiedProductDTO[] }
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 批量更新或创建产品开始, siteId: ${siteId}, 产品数量: ${body.items?.length || 0}`);
|
||||||
|
try {
|
||||||
|
const result = await this.siteApiService.batchUpsertProduct(siteId, body.items || []);
|
||||||
|
this.logger.info(`[Site API] 批量更新或创建产品完成, siteId: ${siteId}, 创建: ${result.created.length}, 更新: ${result.updated.length}, 错误: ${result.errors.length}`);
|
||||||
|
return successResponse({
|
||||||
|
total: (body.items || []).length,
|
||||||
|
processed: result.created.length + result.updated.length,
|
||||||
|
created: result.created.length,
|
||||||
|
updated: result.updated.length,
|
||||||
|
errors: result.errors.map(err => ({
|
||||||
|
identifier: String(err.product.id || err.product.sku || 'unknown'),
|
||||||
|
error: err.error
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 批量更新或创建产品失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Get('/:siteId/orders')
|
@Get('/:siteId/orders')
|
||||||
@ApiOkResponse({ type: UnifiedOrderPaginationDTO })
|
@ApiOkResponse({ type: UnifiedOrderPaginationDTO })
|
||||||
async getOrders(
|
async getOrders(
|
||||||
|
|
@ -925,59 +967,58 @@ export class SiteApiController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/:siteId/orders/:id/ship')
|
@Post('/:siteId/orders/:id/fulfill')
|
||||||
@ApiOkResponse({ type: Object })
|
@ApiOkResponse({ type: Object })
|
||||||
async shipOrder(
|
async fulfillOrder(
|
||||||
@Param('siteId') siteId: number,
|
@Param('siteId') siteId: number,
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Body() body: ShipOrderDTO
|
@Body() body: FulfillmentDTO
|
||||||
) {
|
) {
|
||||||
this.logger.info(`[Site API] 订单发货开始, siteId: ${siteId}, orderId: ${id}`);
|
this.logger.info(`[Site API] 订单履约开始, siteId: ${siteId}, orderId: ${id}`);
|
||||||
try {
|
try {
|
||||||
const adapter = await this.siteApiService.getAdapter(siteId);
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
const data = await adapter.shipOrder(id, body);
|
const data = await adapter.fulfillOrder(id, body);
|
||||||
this.logger.info(`[Site API] 订单发货成功, siteId: ${siteId}, orderId: ${id}`);
|
this.logger.info(`[Site API] 订单履约成功, siteId: ${siteId}, orderId: ${id}`);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`[Site API] 订单发货失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`);
|
this.logger.error(`[Site API] 订单履约失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`);
|
||||||
return errorResponse(error.message);
|
return errorResponse(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/:siteId/orders/:id/cancel-ship')
|
@Post('/:siteId/orders/:id/cancel-fulfill')
|
||||||
@ApiOkResponse({ type: Object })
|
@ApiOkResponse({ type: Object })
|
||||||
async cancelShipOrder(
|
async cancelFulfillment(
|
||||||
@Param('siteId') siteId: number,
|
@Param('siteId') siteId: number,
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Body() body: CancelShipOrderDTO
|
@Body() body: CancelFulfillmentDTO
|
||||||
) {
|
) {
|
||||||
this.logger.info(`[Site API] 取消订单发货开始, siteId: ${siteId}, orderId: ${id}`);
|
this.logger.info(`[Site API] 取消订单履约开始, siteId: ${siteId}, orderId: ${id}`);
|
||||||
try {
|
try {
|
||||||
const adapter = await this.siteApiService.getAdapter(siteId);
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
const data = await adapter.cancelShipOrder(id, body);
|
const data = await adapter.cancelFulfillment(id, body);
|
||||||
this.logger.info(`[Site API] 取消订单发货成功, siteId: ${siteId}, orderId: ${id}`);
|
this.logger.info(`[Site API] 取消订单履约成功, siteId: ${siteId}, orderId: ${id}`);
|
||||||
return successResponse(data);
|
return successResponse(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`[Site API] 取消订单发货失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`);
|
this.logger.error(`[Site API] 取消订单履约失败, siteId: ${siteId}, orderId: ${id}, 错误信息: ${error.message}`);
|
||||||
return errorResponse(error.message);
|
return errorResponse(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('/:siteId/orders/batch-ship')
|
@Post('/:siteId/orders/batch-fulfill')
|
||||||
@ApiOkResponse({ type: Object })
|
@ApiOkResponse({ type: Object })
|
||||||
async batchShipOrders(
|
async batchFulfillOrders(
|
||||||
@Param('siteId') siteId: number,
|
@Param('siteId') siteId: number,
|
||||||
@Body() body: BatchShipOrdersDTO
|
@Body() body: BatchFulfillmentsDTO
|
||||||
) {
|
) {
|
||||||
this.logger.info(`[Site API] 批量订单发货开始, siteId: ${siteId}, 订单数量: ${body.orders.length}`);
|
this.logger.info(`[Site API] 批量订单履约开始, siteId: ${siteId}, 订单数量: ${body.orders.length}`);
|
||||||
try {
|
try {
|
||||||
const adapter = await this.siteApiService.getAdapter(siteId);
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
const results = await Promise.allSettled(
|
const results = await Promise.allSettled(
|
||||||
body.orders.map(order =>
|
body.orders.map(order =>
|
||||||
adapter.shipOrder(order.order_id, {
|
adapter.createOrderFulfillment(order.order_id, {
|
||||||
tracking_number: order.tracking_number,
|
tracking_number: order.tracking_number,
|
||||||
shipping_provider: order.shipping_provider,
|
tracking_provider: order.shipping_provider,
|
||||||
shipping_method: order.shipping_method,
|
|
||||||
items: order.items,
|
items: order.items,
|
||||||
}).catch(error => ({
|
}).catch(error => ({
|
||||||
order_id: order.order_id,
|
order_id: order.order_id,
|
||||||
|
|
@ -995,7 +1036,7 @@ export class SiteApiController {
|
||||||
.filter(result => result.status === 'rejected')
|
.filter(result => result.status === 'rejected')
|
||||||
.map(result => (result as PromiseRejectedResult).reason);
|
.map(result => (result as PromiseRejectedResult).reason);
|
||||||
|
|
||||||
this.logger.info(`[Site API] 批量订单发货完成, siteId: ${siteId}, 成功: ${successful.length}, 失败: ${failed.length}`);
|
this.logger.info(`[Site API] 批量订单履约完成, siteId: ${siteId}, 成功: ${successful.length}, 失败: ${failed.length}`);
|
||||||
return successResponse({
|
return successResponse({
|
||||||
successful: successful.length,
|
successful: successful.length,
|
||||||
failed: failed.length,
|
failed: failed.length,
|
||||||
|
|
@ -1005,7 +1046,95 @@ export class SiteApiController {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`[Site API] 批量订单发货失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
this.logger.error(`[Site API] 批量订单履约失败, siteId: ${siteId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('/:siteId/orders/:orderId/trackings')
|
||||||
|
@ApiOkResponse({ type: Object })
|
||||||
|
async getOrderTrackings(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Param('orderId') orderId: string
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 获取订单物流跟踪信息开始, siteId: ${siteId}, orderId: ${orderId}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.getOrderFulfillments(orderId);
|
||||||
|
this.logger.info(`[Site API] 获取订单物流跟踪信息成功, siteId: ${siteId}, orderId: ${orderId}, 共获取到 ${data.length} 条跟踪信息`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 获取订单物流跟踪信息失败, siteId: ${siteId}, orderId: ${orderId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/:siteId/orders/:orderId/fulfillments')
|
||||||
|
@ApiOkResponse({ type: UnifiedOrderTrackingDTO })
|
||||||
|
@ApiBody({ type: UnifiedOrderTrackingDTO })
|
||||||
|
async createOrderFulfillment(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Param('orderId') orderId: string,
|
||||||
|
@Body() body: UnifiedOrderTrackingDTO
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 创建订单履约跟踪信息开始, siteId: ${siteId}, orderId: ${orderId}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.createOrderFulfillment(orderId, {
|
||||||
|
tracking_number: body.tracking_number,
|
||||||
|
tracking_provider: body.tracking_provider,
|
||||||
|
date_shipped: body.date_shipped,
|
||||||
|
status_shipped: body.status_shipped,
|
||||||
|
});
|
||||||
|
this.logger.info(`[Site API] 创建订单履约跟踪信息成功, siteId: ${siteId}, orderId: ${orderId}`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 创建订单履约跟踪信息失败, siteId: ${siteId}, orderId: ${orderId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put('/:siteId/orders/:orderId/fulfillments/:fulfillmentId')
|
||||||
|
@ApiOkResponse({ type: UnifiedOrderTrackingDTO })
|
||||||
|
@ApiBody({ type: UnifiedOrderTrackingDTO })
|
||||||
|
async updateOrderFulfillment(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Param('orderId') orderId: string,
|
||||||
|
@Param('fulfillmentId') fulfillmentId: string,
|
||||||
|
@Body() body: UnifiedOrderTrackingDTO
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 更新订单履约跟踪信息开始, siteId: ${siteId}, orderId: ${orderId}, fulfillmentId: ${fulfillmentId}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const data = await adapter.updateOrderFulfillment(orderId, fulfillmentId, {
|
||||||
|
tracking_number: body.tracking_number,
|
||||||
|
tracking_provider: body.tracking_provider,
|
||||||
|
date_shipped: body.date_shipped,
|
||||||
|
status_shipped: body.status_shipped,
|
||||||
|
});
|
||||||
|
this.logger.info(`[Site API] 更新订单履约跟踪信息成功, siteId: ${siteId}, orderId: ${orderId}, fulfillmentId: ${fulfillmentId}`);
|
||||||
|
return successResponse(data);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 更新订单履约跟踪信息失败, siteId: ${siteId}, orderId: ${orderId}, fulfillmentId: ${fulfillmentId}, 错误信息: ${error.message}`);
|
||||||
|
return errorResponse(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Del('/:siteId/orders/:orderId/fulfillments/:fulfillmentId')
|
||||||
|
@ApiOkResponse({ type: Boolean })
|
||||||
|
async deleteOrderFulfillment(
|
||||||
|
@Param('siteId') siteId: number,
|
||||||
|
@Param('orderId') orderId: string,
|
||||||
|
@Param('fulfillmentId') fulfillmentId: string
|
||||||
|
) {
|
||||||
|
this.logger.info(`[Site API] 删除订单履约跟踪信息开始, siteId: ${siteId}, orderId: ${orderId}, fulfillmentId: ${fulfillmentId}`);
|
||||||
|
try {
|
||||||
|
const adapter = await this.siteApiService.getAdapter(siteId);
|
||||||
|
const ok = await adapter.deleteOrderFulfillment(orderId, fulfillmentId);
|
||||||
|
this.logger.info(`[Site API] 删除订单履约跟踪信息成功, siteId: ${siteId}, orderId: ${orderId}, fulfillmentId: ${fulfillmentId}`);
|
||||||
|
return successResponse(ok);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`[Site API] 删除订单履约跟踪信息失败, siteId: ${siteId}, orderId: ${orderId}, fulfillmentId: ${fulfillmentId}, 错误信息: ${error.message}`);
|
||||||
return errorResponse(error.message);
|
return errorResponse(error.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,8 @@ export class SiteController {
|
||||||
async all() {
|
async all() {
|
||||||
try {
|
try {
|
||||||
const { items } = await this.siteService.list({ current: 1, pageSize: 1000, isDisabled: false });
|
const { items } = await this.siteService.list({ current: 1, pageSize: 1000, isDisabled: false });
|
||||||
return successResponse(items.map((v: any) => ({ id: v.id, name: v.name })));
|
// 返回完整的站点对象,包括 skuPrefix 等字段
|
||||||
|
return successResponse(items);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return errorResponse(error?.message || '获取失败');
|
return errorResponse(error?.message || '获取失败');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
export class ProductDictItemManyToMany1764238434984 implements MigrationInterface {
|
|
||||||
name = 'ProductDictItemManyToMany1764238434984'
|
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`CREATE TABLE \`product_attributes_dict_item\` (\`productId\` int NOT NULL, \`dictItemId\` int NOT NULL, INDEX \`IDX_592cdbdaebfec346c202ffb82c\` (\`productId\`), INDEX \`IDX_406c1da5b6de45fecb7967c3ec\` (\`dictItemId\`), PRIMARY KEY (\`productId\`, \`dictItemId\`)) ENGINE=InnoDB`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`brandId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`flavorsId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`strengthId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`humidity\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`product\` ADD \`sku\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`product\` ADD UNIQUE INDEX \`IDX_34f6ca1cd897cc926bdcca1ca3\` (\`sku\`)`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`product_attributes_dict_item\` ADD CONSTRAINT \`FK_592cdbdaebfec346c202ffb82ca\` FOREIGN KEY (\`productId\`) REFERENCES \`product\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`product_attributes_dict_item\` ADD CONSTRAINT \`FK_406c1da5b6de45fecb7967c3ec0\` FOREIGN KEY (\`dictItemId\`) REFERENCES \`dict_item\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`ALTER TABLE \`product_attributes_dict_item\` DROP FOREIGN KEY \`FK_406c1da5b6de45fecb7967c3ec0\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`product_attributes_dict_item\` DROP FOREIGN KEY \`FK_592cdbdaebfec346c202ffb82ca\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`product\` DROP INDEX \`IDX_34f6ca1cd897cc926bdcca1ca3\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`sku\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`product\` ADD \`humidity\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`product\` ADD \`strengthId\` int NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`product\` ADD \`flavorsId\` int NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`product\` ADD \`brandId\` int NOT NULL`);
|
|
||||||
await queryRunner.query(`DROP INDEX \`IDX_406c1da5b6de45fecb7967c3ec\` ON \`product_attributes_dict_item\``);
|
|
||||||
await queryRunner.query(`DROP INDEX \`IDX_592cdbdaebfec346c202ffb82c\` ON \`product_attributes_dict_item\``);
|
|
||||||
await queryRunner.query(`DROP TABLE \`product_attributes_dict_item\``);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
export class Area1764294088896 implements MigrationInterface {
|
|
||||||
name = 'Area1764294088896'
|
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
// await queryRunner.query(`DROP INDEX \`IDX_4ca3fbc46d2dbf393ff4ebddbb\` ON \`site\``);
|
|
||||||
// await queryRunner.query(`CREATE TABLE \`area\` (\`id\` int NOT NULL AUTO_INCREMENT, \`name\` varchar(255) NOT NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), UNIQUE INDEX \`IDX_644ffaf8fbde4db798cb47712f\` (\`name\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
|
||||||
// await queryRunner.query(`CREATE TABLE \`stock_point_areas_area\` (\`stockPointId\` int NOT NULL, \`areaId\` int NOT NULL, INDEX \`IDX_07d2db2150151e2ef341d2f1de\` (\`stockPointId\`), INDEX \`IDX_92707ea81fc19dc707dba24819\` (\`areaId\`), PRIMARY KEY (\`stockPointId\`, \`areaId\`)) ENGINE=InnoDB`);
|
|
||||||
// await queryRunner.query(`CREATE TABLE \`site_areas_area\` (\`siteId\` int NOT NULL, \`areaId\` int NOT NULL, INDEX \`IDX_926a14ac4c91f38792831acd2a\` (\`siteId\`), INDEX \`IDX_7c26c582048e3ecd3cd5938cb9\` (\`areaId\`), PRIMARY KEY (\`siteId\`, \`areaId\`)) ENGINE=InnoDB`);
|
|
||||||
// await queryRunner.query(`ALTER TABLE \`site\` DROP COLUMN \`name\``);
|
|
||||||
// await queryRunner.query(`ALTER TABLE `product` ADD `promotionPrice` decimal(10,2) NOT NULL DEFAULT '0.00'`);
|
|
||||||
// await queryRunner.query(`ALTER TABLE `product` ADD `source` int NOT NULL DEFAULT '0'`);
|
|
||||||
// await queryRunner.query(`ALTER TABLE \`site\` ADD \`token\` varchar(255) NULL`);
|
|
||||||
// await queryRunner.query(`ALTER TABLE `site` ADD `name` varchar(255) NOT NULL`);
|
|
||||||
// await queryRunner.query(`ALTER TABLE \`site\` ADD UNIQUE INDEX \`IDX_9669a09fcc0eb6d2794a658f64\` (\`name\`)`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`stock_point_areas_area\` ADD CONSTRAINT \`FK_07d2db2150151e2ef341d2f1de1\` FOREIGN KEY (\`stockPointId\`) REFERENCES \`stock_point\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`stock_point_areas_area\` ADD CONSTRAINT \`FK_92707ea81fc19dc707dba24819c\` FOREIGN KEY (\`areaId\`) REFERENCES \`area\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`site_areas_area\` ADD CONSTRAINT \`FK_926a14ac4c91f38792831acd2a6\` FOREIGN KEY (\`siteId\`) REFERENCES \`site\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`site_areas_area\` ADD CONSTRAINT \`FK_7c26c582048e3ecd3cd5938cb9f\` FOREIGN KEY (\`areaId\`) REFERENCES \`area\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`ALTER TABLE \`site_areas_area\` DROP FOREIGN KEY \`FK_7c26c582048e3ecd3cd5938cb9f\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`site_areas_area\` DROP FOREIGN KEY \`FK_926a14ac4c91f38792831acd2a6\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`stock_point_areas_area\` DROP FOREIGN KEY \`FK_92707ea81fc19dc707dba24819c\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`stock_point_areas_area\` DROP FOREIGN KEY \`FK_07d2db2150151e2ef341d2f1de1\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`site\` DROP INDEX \`IDX_9669a09fcc0eb6d2794a658f64\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`site\` DROP COLUMN \`name\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`site\` DROP COLUMN \`token\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`source\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`product\` DROP COLUMN \`promotionPrice\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`site\` ADD \`name\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`DROP INDEX \`IDX_7c26c582048e3ecd3cd5938cb9\` ON \`site_areas_area\``);
|
|
||||||
await queryRunner.query(`DROP INDEX \`IDX_926a14ac4c91f38792831acd2a\` ON \`site_areas_area\``);
|
|
||||||
await queryRunner.query(`DROP TABLE \`site_areas_area\``);
|
|
||||||
await queryRunner.query(`DROP INDEX \`IDX_92707ea81fc19dc707dba24819\` ON \`stock_point_areas_area\``);
|
|
||||||
await queryRunner.query(`DROP INDEX \`IDX_07d2db2150151e2ef341d2f1de\` ON \`stock_point_areas_area\``);
|
|
||||||
await queryRunner.query(`DROP TABLE \`stock_point_areas_area\``);
|
|
||||||
await queryRunner.query(`DROP INDEX \`IDX_644ffaf8fbde4db798cb47712f\` ON \`area\``);
|
|
||||||
await queryRunner.query(`DROP TABLE \`area\``);
|
|
||||||
await queryRunner.query(`CREATE UNIQUE INDEX \`IDX_4ca3fbc46d2dbf393ff4ebddbb\` ON \`site\` (\`name\`)`);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
export class ProductStock1764299629279 implements MigrationInterface {
|
|
||||||
name = 'ProductStock1764299629279'
|
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`CREATE TABLE \`order_item_original\` (\`id\` int NOT NULL AUTO_INCREMENT, \`order_id\` int NOT NULL, \`name\` varchar(255) NOT NULL, \`siteId\` varchar(255) NOT NULL, \`externalOrderId\` varchar(255) NOT NULL, \`externalOrderItemId\` varchar(255) NULL, \`externalProductId\` varchar(255) NOT NULL, \`externalVariationId\` varchar(255) NOT NULL, \`quantity\` int NOT NULL, \`subtotal\` decimal(10,2) NULL, \`subtotal_tax\` decimal(10,2) NULL, \`total\` decimal(10,2) NULL, \`total_tax\` decimal(10,2) NULL, \`sku\` varchar(255) NULL, \`price\` decimal(10,2) NOT NULL, \`createdAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`updatedAt\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD CONSTRAINT \`FK_ca48e4bce0bb8cecd24cc8081e5\` FOREIGN KEY (\`order_id\`) REFERENCES \`order\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP FOREIGN KEY \`FK_ca48e4bce0bb8cecd24cc8081e5\``);
|
|
||||||
await queryRunner.query(`DROP TABLE \`order_item_original\``);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
export class UpdateDictItemUniqueConstraint1764569947170 implements MigrationInterface {
|
|
||||||
name = 'UpdateDictItemUniqueConstraint1764569947170'
|
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`productId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`isPackage\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalOrderId\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalProductId\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalVariationId\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal\` decimal(10,2) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal_tax\` decimal(10,2) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total\` decimal(10,2) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total_tax\` decimal(10,2) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`price\` decimal(10,2) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`productId\` int NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`isPackage\` tinyint NOT NULL DEFAULT 0`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` CHANGE \`sku\` \`sku\` varchar(255) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` int NULL`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` CHANGE \`sku\` \`sku\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` int NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`isPackage\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`productId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`price\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total_tax\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal_tax\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalVariationId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalProductId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalOrderId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`isPackage\` tinyint NOT NULL DEFAULT '0'`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`productId\` int NOT NULL`);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
export class AddTestDataToTemplate1765275715762 implements MigrationInterface {
|
|
||||||
name = 'AddTestDataToTemplate1765275715762'
|
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`ALTER TABLE \`site_stock_points_stock_point\` DROP FOREIGN KEY \`FK_e93d8c42c9baf5a0dade42c59ae\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`isPackage\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`productId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalOrderId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalProductId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalVariationId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`price\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal_tax\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total_tax\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`template\` ADD \`testData\` text NULL COMMENT '测试数据JSON'`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalOrderId\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalProductId\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalVariationId\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal\` decimal(10,2) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal_tax\` decimal(10,2) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total\` decimal(10,2) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total_tax\` decimal(10,2) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`price\` decimal(10,2) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`productId\` int NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`isPackage\` tinyint NOT NULL DEFAULT 0`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` int NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` CHANGE \`sku\` \`sku\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`site_stock_points_stock_point\` ADD CONSTRAINT \`FK_e93d8c42c9baf5a0dade42c59ae\` FOREIGN KEY (\`stockPointId\`) REFERENCES \`stock_point\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`ALTER TABLE \`site_stock_points_stock_point\` DROP FOREIGN KEY \`FK_e93d8c42c9baf5a0dade42c59ae\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` CHANGE \`sku\` \`sku\` varchar(255) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` int NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`isPackage\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`productId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`price\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total_tax\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal_tax\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalVariationId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalProductId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalOrderId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`template\` DROP COLUMN \`testData\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total_tax\` decimal(10,2) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total\` decimal(10,2) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal_tax\` decimal(10,2) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal\` decimal(10,2) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`price\` decimal(10,2) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalVariationId\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalProductId\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalOrderId\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`productId\` int NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`isPackage\` tinyint NOT NULL DEFAULT '0'`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`site_stock_points_stock_point\` ADD CONSTRAINT \`FK_e93d8c42c9baf5a0dade42c59ae\` FOREIGN KEY (\`stockPointId\`) REFERENCES \`stock_point\`(\`id\`) ON DELETE CASCADE ON UPDATE CASCADE`);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
export class AddSiteDescription1765330208213 implements MigrationInterface {
|
|
||||||
name = 'AddSiteDescription1765330208213'
|
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`productId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`isPackage\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalOrderId\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalProductId\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`externalVariationId\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal\` decimal(10,2) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`subtotal_tax\` decimal(10,2) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total\` decimal(10,2) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`total_tax\` decimal(10,2) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`price\` decimal(10,2) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`productId\` int NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`isPackage\` tinyint NOT NULL DEFAULT 0`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` CHANGE \`sku\` \`sku\` varchar(255) NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` int NULL`);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` CHANGE \`sku\` \`sku\` varchar(255) NOT NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`siteId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`siteId\` int NULL`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`isPackage\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`productId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`price\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total_tax\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`total\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal_tax\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`subtotal\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalVariationId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalProductId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP COLUMN \`externalOrderId\``);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`isPackage\` tinyint NOT NULL DEFAULT '0'`);
|
|
||||||
await queryRunner.query(`ALTER TABLE \`order_item_original\` ADD \`productId\` int NOT NULL`);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,91 +0,0 @@
|
||||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
|
||||||
|
|
||||||
export class UpdateProductTableName1765358400000 implements MigrationInterface {
|
|
||||||
name = 'UpdateProductTableName1765358400000'
|
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
// 1. 使用 try-catch 方式删除外键约束,避免因外键不存在导致迁移失败
|
|
||||||
try {
|
|
||||||
await queryRunner.query("ALTER TABLE `product_attributes_dict_item` DROP FOREIGN KEY `FK_592cdbdaebfec346c202ffb82ca`");
|
|
||||||
} catch (error) {
|
|
||||||
// 忽略外键不存在的错误
|
|
||||||
console.log('Warning: Failed to drop foreign key on product_attributes_dict_item. It may not exist.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await queryRunner.query("ALTER TABLE `product_stock_component` DROP FOREIGN KEY `FK_6fe75663083f572a49e7f46909b`");
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Warning: Failed to drop foreign key on product_stock_component. It may not exist.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await queryRunner.query("ALTER TABLE `product_site_sku` DROP FOREIGN KEY `FK_3b9b7f3d8a6d9f3e2c0b1a4d5e6`");
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Warning: Failed to drop foreign key on product_site_sku. It may not exist.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await queryRunner.query("ALTER TABLE `order_sale` DROP FOREIGN KEY `FK_order_sale_product`");
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Warning: Failed to drop foreign key on order_sale. It may not exist.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 将 product 表重命名为 product_v2
|
|
||||||
await queryRunner.query("ALTER TABLE `product` RENAME TO `product_v2`");
|
|
||||||
|
|
||||||
// 3. 重新创建所有外键约束,引用新的 product_v2 表
|
|
||||||
await queryRunner.query("ALTER TABLE `product_attributes_dict_item` ADD CONSTRAINT `FK_592cdbdaebfec346c202ffb82ca` FOREIGN KEY (`productId`) REFERENCES `product_v2`(`id`) ON DELETE CASCADE ON UPDATE CASCADE");
|
|
||||||
await queryRunner.query("ALTER TABLE `product_stock_component` ADD CONSTRAINT `FK_6fe75663083f572a49e7f46909b` FOREIGN KEY (`productId`) REFERENCES `product_v2`(`id`) ON DELETE CASCADE");
|
|
||||||
await queryRunner.query("ALTER TABLE `product_site_sku` ADD CONSTRAINT `FK_3b9b7f3d8a6d9f3e2c0b1a4d5e6` FOREIGN KEY (`productId`) REFERENCES `product_v2`(`id`) ON DELETE CASCADE");
|
|
||||||
|
|
||||||
// 4. 为 order_sale 表添加外键约束
|
|
||||||
try {
|
|
||||||
await queryRunner.query("ALTER TABLE `order_sale` ADD CONSTRAINT `FK_order_sale_product` FOREIGN KEY (`productId`) REFERENCES `product_v2`(`id`) ON DELETE CASCADE");
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Warning: Failed to add foreign key on order_sale. It may already exist.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
||||||
// 回滚操作
|
|
||||||
// 1. 删除外键约束
|
|
||||||
try {
|
|
||||||
await queryRunner.query("ALTER TABLE `product_attributes_dict_item` DROP FOREIGN KEY `FK_592cdbdaebfec346c202ffb82ca`");
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Warning: Failed to drop foreign key on product_attributes_dict_item during rollback.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await queryRunner.query("ALTER TABLE `product_stock_component` DROP FOREIGN KEY `FK_6fe75663083f572a49e7f46909b`");
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Warning: Failed to drop foreign key on product_stock_component during rollback.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await queryRunner.query("ALTER TABLE `product_site_sku` DROP FOREIGN KEY `FK_3b9b7f3d8a6d9f3e2c0b1a4d5e6`");
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Warning: Failed to drop foreign key on product_site_sku during rollback.');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await queryRunner.query("ALTER TABLE `order_sale` DROP FOREIGN KEY `FK_order_sale_product`");
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Warning: Failed to drop foreign key on order_sale during rollback.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 将 product_v2 表重命名回 product
|
|
||||||
await queryRunner.query("ALTER TABLE `product_v2` RENAME TO `product`");
|
|
||||||
|
|
||||||
// 3. 重新创建外键约束,引用回原来的 product 表
|
|
||||||
await queryRunner.query("ALTER TABLE `product_attributes_dict_item` ADD CONSTRAINT `FK_592cdbdaebfec346c202ffb82ca` FOREIGN KEY (`productId`) REFERENCES `product`(`id`) ON DELETE CASCADE ON UPDATE CASCADE");
|
|
||||||
await queryRunner.query("ALTER TABLE `product_stock_component` ADD CONSTRAINT `FK_6fe75663083f572a49e7f46909b` FOREIGN KEY (`productId`) REFERENCES `product`(`id`) ON DELETE CASCADE");
|
|
||||||
await queryRunner.query("ALTER TABLE `product_site_sku` ADD CONSTRAINT `FK_3b9b7f3d8a6d9f3e2c0b1a4d5e6` FOREIGN KEY (`productId`) REFERENCES `product`(`id`) ON DELETE CASCADE");
|
|
||||||
|
|
||||||
// 4. 为 order_sale 表重新创建外键约束
|
|
||||||
try {
|
|
||||||
await queryRunner.query("ALTER TABLE `order_sale` ADD CONSTRAINT `FK_order_sale_product` FOREIGN KEY (`productId`) REFERENCES `product`(`id`) ON DELETE CASCADE");
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Warning: Failed to add foreign key on order_sale during rollback. It may already exist.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -10,7 +10,6 @@ export default class DictSeeder implements Seeder {
|
||||||
* @returns 格式化后的名称
|
* @returns 格式化后的名称
|
||||||
*/
|
*/
|
||||||
private formatName(name: string): string {
|
private formatName(name: string): string {
|
||||||
// return String(name).replace(/[\_\s.]+/g, '-').toLowerCase();
|
|
||||||
// 只替换空格和下划线
|
// 只替换空格和下划线
|
||||||
return String(name).replace(/[\_\s]+/g, '-').toLowerCase();
|
return String(name).replace(/[\_\s]+/g, '-').toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
@ -21,74 +20,6 @@ export default class DictSeeder implements Seeder {
|
||||||
const dictRepository = dataSource.getRepository(Dict);
|
const dictRepository = dataSource.getRepository(Dict);
|
||||||
const dictItemRepository = dataSource.getRepository(DictItem);
|
const dictItemRepository = dataSource.getRepository(DictItem);
|
||||||
|
|
||||||
const flavorsData = [
|
|
||||||
{ name: 'bellini', title: 'Bellini', titleCn: '贝利尼', shortName: 'BL' },
|
|
||||||
{ name: 'max-polarmint', title: 'Max Polarmint', titleCn: '马克斯薄荷', shortName: 'MP' },
|
|
||||||
{ name: 'blueberry', title: 'Blueberry', titleCn: '蓝莓', shortName: 'BB' },
|
|
||||||
{ name: 'citrus', title: 'Citrus', titleCn: '柑橘', shortName: 'CT' },
|
|
||||||
{ name: 'wintergreen', title: 'Wintergreen', titleCn: '冬绿薄荷', shortName: 'WG' },
|
|
||||||
{ name: 'cool-mint', title: 'COOL MINT', titleCn: '清凉薄荷', shortName: 'CM' },
|
|
||||||
{ name: 'juicy-peach', title: 'JUICY PEACH', titleCn: '多汁蜜桃', shortName: 'JP' },
|
|
||||||
{ name: 'orange', title: 'ORANGE', titleCn: '橙子', shortName: 'OR' },
|
|
||||||
{ name: 'peppermint', title: 'PEPPERMINT', titleCn: '胡椒薄荷', shortName: 'PP' },
|
|
||||||
{ name: 'spearmint', title: 'SPEARMINT', titleCn: '绿薄荷', shortName: 'SM' },
|
|
||||||
{ name: 'strawberry', title: 'STRAWBERRY', titleCn: '草莓', shortName: 'SB' },
|
|
||||||
{ name: 'watermelon', title: 'WATERMELON', titleCn: '西瓜', shortName: 'WM' },
|
|
||||||
{ name: 'coffee', title: 'COFFEE', titleCn: '咖啡', shortName: 'CF' },
|
|
||||||
{ name: 'lemonade', title: 'LEMONADE', titleCn: '柠檬水', shortName: 'LN' },
|
|
||||||
{ name: 'apple-mint', title: 'apple mint', titleCn: '苹果薄荷', shortName: 'AM' },
|
|
||||||
{ name: 'peach', title: 'PEACH', titleCn: '桃子', shortName: 'PC' },
|
|
||||||
{ name: 'mango', title: 'Mango', titleCn: '芒果', shortName: 'MG' },
|
|
||||||
{ name: 'ice-wintergreen', title: 'ICE WINTERGREEN', titleCn: '冰冬绿薄荷', shortName: 'IWG' },
|
|
||||||
{ name: 'pink-lemonade', title: 'Pink Lemonade', titleCn: '粉红柠檬水', shortName: 'PLN' },
|
|
||||||
{ name: 'blackcherry', title: 'Blackcherry', titleCn: '黑樱桃', shortName: 'BC' },
|
|
||||||
{ name: 'fresh-mint', title: 'fresh mint', titleCn: '清新薄荷', shortName: 'FM' },
|
|
||||||
{ name: 'strawberry-lychee', title: 'Strawberry Lychee', titleCn: '草莓荔枝', shortName: 'SBL' },
|
|
||||||
{ name: 'passion-fruit', title: 'Passion Fruit', titleCn: '百香果', shortName: 'PF' },
|
|
||||||
{ name: 'banana-lce', title: 'Banana lce', titleCn: '香蕉冰', shortName: 'BI' },
|
|
||||||
{ name: 'bubblegum', title: 'Bubblegum', titleCn: '泡泡糖', shortName: 'BG' },
|
|
||||||
{ name: 'mango-lce', title: 'Mango lce', titleCn: '芒果冰', shortName: 'MI' },
|
|
||||||
{ name: 'grape-lce', title: 'Grape lce', titleCn: '葡萄冰', shortName: 'GI' },
|
|
||||||
{ name: 'apple', title: 'apple', titleCn: '苹果', shortName: 'AP' },
|
|
||||||
{ name: 'grape', title: 'grape', titleCn: '葡萄', shortName: 'GR' },
|
|
||||||
{ name: 'cherry', title: 'cherry', titleCn: '樱桃', shortName: 'CH' },
|
|
||||||
{ name: 'lemon', title: 'lemon', titleCn: '柠檬', shortName: 'LM' },
|
|
||||||
{ name: 'razz', title: 'razz', titleCn: '覆盆子', shortName: 'RZ' },
|
|
||||||
{ name: 'pineapple', title: 'pineapple', titleCn: '菠萝', shortName: 'PA' },
|
|
||||||
{ name: 'berry', title: 'berry', titleCn: '浆果', shortName: 'BR' },
|
|
||||||
{ name: 'fruit', title: 'fruit', titleCn: '水果', shortName: 'FR' },
|
|
||||||
{ name: 'mint', title: 'mint', titleCn: '薄荷', shortName: 'MT' },
|
|
||||||
{ name: 'menthol', title: 'menthol', titleCn: '薄荷醇', shortName: 'MH' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const brandsData = [
|
|
||||||
{ name: 'yoone', title: 'Yoone', titleCn: '', shortName: 'YN' },
|
|
||||||
{ name: 'white-fox', title: 'White Fox', titleCn: '', shortName: 'WF' },
|
|
||||||
{ name: 'zyn', title: 'ZYN', titleCn: '', shortName: 'ZN' },
|
|
||||||
{ name: 'zonnic', title: 'Zonnic', titleCn: '', shortName: 'ZC' },
|
|
||||||
{ name: 'zolt', title: 'Zolt', titleCn: '', shortName: 'ZT' },
|
|
||||||
{ name: 'velo', title: 'Velo', titleCn: '', shortName: 'VL' },
|
|
||||||
{ name: 'lucy', title: 'Lucy', titleCn: '', shortName: 'LC' },
|
|
||||||
{ name: 'egp', title: 'EGP', titleCn: '', shortName: 'EP' },
|
|
||||||
{ name: 'bridge', title: 'Bridge', titleCn: '', shortName: 'BR' },
|
|
||||||
{ name: 'zex', title: 'ZEX', titleCn: '', shortName: 'ZX' },
|
|
||||||
{ name: 'sesh', title: 'Sesh', titleCn: '', shortName: 'SH' },
|
|
||||||
{ name: 'pablo', title: 'Pablo', titleCn: '', shortName: 'PB' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const strengthsData = [
|
|
||||||
{ name: '2mg', title: '2MG', titleCn: '2毫克', shortName: '2M' },
|
|
||||||
{ name: '3mg', title: '3MG', titleCn: '3毫克', shortName: '3M' },
|
|
||||||
{ name: '4mg', title: '4MG', titleCn: '4毫克', shortName: '4M' },
|
|
||||||
{ name: '6mg', title: '6MG', titleCn: '6毫克', shortName: '6M' },
|
|
||||||
{ name: '6.5mg', title: '6.5MG', titleCn: '6.5毫克', shortName: '6.5M' },
|
|
||||||
{ name: '9mg', title: '9MG', titleCn: '9毫克', shortName: '9M' },
|
|
||||||
{ name: '12mg', title: '12MG', titleCn: '12毫克', shortName: '12M' },
|
|
||||||
{ name: '16.5mg', title: '16.5MG', titleCn: '16.5毫克', shortName: '16.5M' },
|
|
||||||
{ name: '18mg', title: '18MG', titleCn: '18毫克', shortName: '18M' },
|
|
||||||
{ name: '30mg', title: '30MG', titleCn: '30毫克', shortName: '30M' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 初始化语言字典
|
// 初始化语言字典
|
||||||
const locales = [
|
const locales = [
|
||||||
{ name: 'zh-cn', title: '简体中文', titleCn: '简体中文', shortName: 'CN' },
|
{ name: 'zh-cn', title: '简体中文', titleCn: '简体中文', shortName: 'CN' },
|
||||||
|
|
@ -173,3 +104,775 @@ export default class DictSeeder implements Seeder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 口味数据
|
||||||
|
const flavorsData = [
|
||||||
|
{ name: 'all-white', title: 'all white', titleCn: '全白', shortName: 'AL' },
|
||||||
|
{ name: 'amazing-apple-blackcurrant', title: 'amazing apple blackcurrant', titleCn: '惊艳苹果黑加仑', shortName: 'AM' },
|
||||||
|
{ name: 'apple-&-mint', title: 'apple & mint', titleCn: '苹果薄荷', shortName: 'AP' },
|
||||||
|
{ name: 'applemint', title: 'applemint', titleCn: '苹果薄荷混合', shortName: 'AP' },
|
||||||
|
{ name: 'apple-berry-ice', title: 'apple berry ice', titleCn: '苹果莓冰', shortName: 'AP' },
|
||||||
|
{ name: 'apple-bomb', title: 'apple bomb', titleCn: '苹果炸弹', shortName: 'AP' },
|
||||||
|
{ name: 'apple-kiwi-melon-ice', title: 'apple kiwi melon ice', titleCn: '苹果奇异瓜冰', shortName: 'AP' },
|
||||||
|
{ name: 'apple-mango-pear', title: 'apple mango pear', titleCn: '苹果芒果梨', shortName: 'AP' },
|
||||||
|
{ name: 'apple-melon-ice', title: 'apple melon ice', titleCn: '苹果瓜冰', shortName: 'AP' },
|
||||||
|
{ name: 'apple-mint', title: 'apple mint', titleCn: '苹果薄荷', shortName: 'AP' },
|
||||||
|
{ name: 'apple-peach', title: 'apple peach', titleCn: '苹果桃子', shortName: 'AP' },
|
||||||
|
{ name: 'apple-peach-pear', title: 'apple peach pear', titleCn: '苹果桃梨', shortName: 'AP' },
|
||||||
|
{ name: 'apple-peach-strawww', title: 'apple peach strawww', titleCn: '苹果桃草莓', shortName: 'AP' },
|
||||||
|
{ name: 'apple-pom-passion-ice', title: 'apple pom passion ice', titleCn: '苹果石榴激情冰', shortName: 'AP' },
|
||||||
|
{ name: 'arctic-banana-glaze', title: 'arctic banana glaze', titleCn: '北极香蕉釉', shortName: 'AR' },
|
||||||
|
{ name: 'arctic-grapefruit', title: 'arctic grapefruit', titleCn: '北极葡萄柚', shortName: 'AR' },
|
||||||
|
{ name: 'arctic-mint', title: 'arctic mint', titleCn: '北极薄荷', shortName: 'AR' },
|
||||||
|
{ name: 'baddie-blueberries', title: 'baddie blueberries', titleCn: '时髦蓝莓', shortName: 'BA' },
|
||||||
|
{ name: 'banana', title: 'banana', titleCn: '香蕉', shortName: 'BA' },
|
||||||
|
{ name: 'banana-(solid)', title: 'banana (solid)', titleCn: '香蕉(固体)', shortName: 'BA' },
|
||||||
|
{ name: 'banana-berry', title: 'banana berry', titleCn: '香蕉莓果', shortName: 'BA' },
|
||||||
|
{ name: 'banana-berry-melon-ice', title: 'banana berry melon ice', titleCn: '香蕉莓果瓜冰', shortName: 'BA' },
|
||||||
|
{ name: 'banana-blackberry', title: 'banana blackberry', titleCn: '香蕉黑莓', shortName: 'BA' },
|
||||||
|
{ name: 'banana-ice', title: 'banana ice', titleCn: '香蕉冰', shortName: 'BA' },
|
||||||
|
{ name: 'banana-milkshake', title: 'banana milkshake', titleCn: '香蕉奶昔', shortName: 'BA' },
|
||||||
|
{ name: 'banana-pnck-dude', title: 'banana pnck dude', titleCn: '香蕉粉红小子', shortName: 'BA' },
|
||||||
|
{ name: 'banana-pomegranate-cherry-ice', title: 'banana pomegranate cherry ice', titleCn: '香蕉石榴樱桃冰', shortName: 'BA' },
|
||||||
|
{ name: 'bangin-blood-orange-iced', title: 'bangin blood orange iced', titleCn: '爆炸血橙冰', shortName: 'BA' },
|
||||||
|
{ name: 'berries-in-the-6ix', title: 'berries in the 6ix', titleCn: '多伦多莓果', shortName: 'BE' },
|
||||||
|
{ name: 'berry-burst', title: 'berry burst', titleCn: '浆果爆发', shortName: 'BE' },
|
||||||
|
{ name: 'berry-burst-(thermal)', title: 'berry burst (thermal)', titleCn: '浆果爆发(热感)', shortName: 'BE' },
|
||||||
|
{ name: 'berry-ice', title: 'berry ice', titleCn: '浆果冰', shortName: 'BE' },
|
||||||
|
{ name: 'berry-lime-ice', title: 'berry lime ice', titleCn: '浆果青柠冰', shortName: 'BE' },
|
||||||
|
{ name: 'berry-trio-ice', title: 'berry trio ice', titleCn: '三重浆果冰', shortName: 'BE' },
|
||||||
|
{ name: 'black', title: 'black', titleCn: '黑色', shortName: 'BL' },
|
||||||
|
{ name: 'black-cherry', title: 'black cherry', titleCn: '黑樱桃', shortName: 'BL' },
|
||||||
|
{ name: 'blackcherry', title: 'blackcherry', titleCn: '黑樱桃混合', shortName: 'BL' },
|
||||||
|
{ name: 'blackcurrant-ice', title: 'blackcurrant ice', titleCn: '黑加仑冰', shortName: 'BL' },
|
||||||
|
{ name: 'black-currant-ice', title: 'black currant ice', titleCn: '黑加仑冰(空格版)', shortName: 'BL' },
|
||||||
|
{ name: 'black-licorice', title: 'black licorice', titleCn: '黑甘草', shortName: 'BL' },
|
||||||
|
{ name: 'black-tea', title: 'black tea', titleCn: '红茶', shortName: 'BL' },
|
||||||
|
{ name: 'blackberry-ice', title: 'blackberry ice', titleCn: '黑莓冰', shortName: 'BL' },
|
||||||
|
{ name: 'blackberry-raspberry-lemon', title: 'blackberry raspberry lemon', titleCn: '黑莓覆盆子柠檬', shortName: 'BL' },
|
||||||
|
{ name: 'blackcurrant-lychee-berries', title: 'blackcurrant lychee berries', titleCn: '黑加仑荔枝莓', shortName: 'BL' },
|
||||||
|
{ name: 'blackcurrant-pineapple-ice', title: 'blackcurrant pineapple ice', titleCn: '黑加仑菠萝冰', shortName: 'BL' },
|
||||||
|
{ name: 'blackcurrant-quench-ice', title: 'blackcurrant quench ice', titleCn: '黑加仑清爽冰', shortName: 'BL' },
|
||||||
|
{ name: 'blastin-banana-mango-iced', title: 'blastin banana mango iced', titleCn: '香蕉芒果爆炸冰', shortName: 'BL' },
|
||||||
|
{ name: 'blazin-banana-blackberry-iced', title: 'blazin banana blackberry iced', titleCn: '香蕉黑莓火焰冰', shortName: 'BL' },
|
||||||
|
{ name: 'blessed-blueberry-mint-iced', title: 'blessed blueberry mint iced', titleCn: '蓝莓薄荷冰', shortName: 'BL' },
|
||||||
|
{ name: 'bliss-iced', title: 'bliss iced', titleCn: '极乐冰', shortName: 'BL' },
|
||||||
|
{ name: 'blood-orange', title: 'blood orange', titleCn: '血橙', shortName: 'BL' },
|
||||||
|
{ name: 'blood-orange-ice', title: 'blood orange ice', titleCn: '血橙冰', shortName: 'BL' },
|
||||||
|
{ name: 'blue-dragon-fruit-peach', title: 'blue dragon fruit peach', titleCn: '蓝色龙果桃', shortName: 'BL' },
|
||||||
|
{ name: 'blue-lemon', title: 'blue lemon', titleCn: '蓝柠檬', shortName: 'BL' },
|
||||||
|
{ name: 'blue-raspberry', title: 'blue raspberry', titleCn: '蓝覆盆子', shortName: 'BL' },
|
||||||
|
{ name: 'blue-raspberry-apple', title: 'blue raspberry apple', titleCn: '蓝覆盆子苹果', shortName: 'BL' },
|
||||||
|
{ name: 'blue-raspberry-lemon', title: 'blue raspberry lemon', titleCn: '蓝覆盆子柠檬', shortName: 'BL' },
|
||||||
|
{ name: 'blue-raspberry-magic-cotton-ice', title: 'blue raspberry magic cotton ice', titleCn: '蓝覆盆子魔法棉花糖冰', shortName: 'BL' },
|
||||||
|
{ name: 'blue-razz', title: 'blue razz', titleCn: '蓝覆盆子', shortName: 'BL' },
|
||||||
|
{ name: 'blue-razz-hype', title: 'blue razz hype', titleCn: '蓝覆盆子热情', shortName: 'BL' },
|
||||||
|
{ name: 'blue-razz-ice', title: 'blue razz ice', titleCn: '蓝覆盆子冰', shortName: 'BL' },
|
||||||
|
{ name: 'blue-razz-ice-(solid)', title: 'blue razz ice (solid)', titleCn: '蓝覆盆子冰(固体)', shortName: 'BL' },
|
||||||
|
{ name: 'blue-razz-ice-glace', title: 'blue razz ice glace', titleCn: '蓝覆盆子冰格', shortName: 'BL' },
|
||||||
|
{ name: 'blue-razz-lemon-ice', title: 'blue razz lemon ice', titleCn: '蓝覆盆子柠檬冰', shortName: 'BL' },
|
||||||
|
{ name: 'blue-razz-lemonade', title: 'blue razz lemonade', titleCn: '蓝覆盆子柠檬水', shortName: 'BL' },
|
||||||
|
{ name: 'blueberry', title: 'blueberry', titleCn: '蓝莓', shortName: 'BL' },
|
||||||
|
{ name: 'blueberry-banana', title: 'blueberry banana', titleCn: '蓝莓香蕉', shortName: 'BL' },
|
||||||
|
{ name: 'blueberry-cloudz', title: 'blueberry cloudz', titleCn: '蓝莓云', shortName: 'BL' },
|
||||||
|
{ name: 'blueberry-ice', title: 'blueberry ice', titleCn: '蓝莓冰', shortName: 'BL' },
|
||||||
|
{ name: 'blueberry-kiwi-ice', title: 'blueberry kiwi ice', titleCn: '蓝莓奇异果冰', shortName: 'BL' },
|
||||||
|
{ name: 'blueberry-lemon', title: 'blueberry lemon', titleCn: '蓝莓柠檬', shortName: 'BL' },
|
||||||
|
{ name: 'blueberry-lemon-ice', title: 'blueberry lemon ice', titleCn: '蓝莓柠檬冰', shortName: 'BL' },
|
||||||
|
{ name: 'blueberry-mint', title: 'blueberry mint', titleCn: '蓝莓薄荷', shortName: 'BL' },
|
||||||
|
{ name: 'blueberry-pear', title: 'blueberry pear', titleCn: '蓝莓梨', shortName: 'BL' },
|
||||||
|
{ name: 'blueberry-razz-cc', title: 'blueberry razz cc', titleCn: '蓝莓覆盆子混合', shortName: 'BL' },
|
||||||
|
{ name: 'blueberry-sour-raspberry', title: 'blueberry sour raspberry', titleCn: '蓝莓酸覆盆子', shortName: 'BL' },
|
||||||
|
{ name: 'blueberry-storm', title: 'blueberry storm', titleCn: '蓝莓风暴', shortName: 'BL' },
|
||||||
|
{ name: 'blueberry-swirl-ice', title: 'blueberry swirl ice', titleCn: '蓝莓漩涡冰', shortName: 'BL' },
|
||||||
|
{ name: 'blueberry-watermelon', title: 'blueberry watermelon', titleCn: '蓝莓西瓜', shortName: 'BL' },
|
||||||
|
{ name: 'bold-tobacco', title: 'bold tobacco', titleCn: '浓烈烟草', shortName: 'BO' },
|
||||||
|
{ name: 'bomb-blue-razz', title: 'bomb blue razz', titleCn: '蓝覆盆子炸弹', shortName: 'BO' },
|
||||||
|
{ name: 'boss-blueberry-iced', title: 'boss blueberry iced', titleCn: '老板蓝莓冰', shortName: 'BO' },
|
||||||
|
{ name: 'boss-blueberry-lced', title: 'boss blueberry lced', titleCn: '老板蓝莓冷饮', shortName: 'BO' },
|
||||||
|
{ name: 'bright-peppermint', title: 'bright peppermint', titleCn: '清爽薄荷', shortName: 'BR' },
|
||||||
|
{ name: 'bright-spearmint', title: 'bright spearmint', titleCn: '清爽留兰香', shortName: 'BR' },
|
||||||
|
{ name: 'brisky-classic-red', title: 'brisky classic red', titleCn: '经典红色烈酒', shortName: 'BR' },
|
||||||
|
{ name: 'bumpin-blackcurrant-iced', title: 'bumpin blackcurrant iced', titleCn: '黑加仑热烈冰', shortName: 'BU' },
|
||||||
|
{ name: 'burst-ice', title: 'burst ice', titleCn: '爆炸冰', shortName: 'BU' },
|
||||||
|
{ name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰', shortName: 'BU' },
|
||||||
|
{ name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰(重复)', shortName: 'BU' },
|
||||||
|
{ name: 'california-cherry', title: 'california cherry', titleCn: '加州樱桃', shortName: 'CA' },
|
||||||
|
{ name: 'cantaloupe-mango-banana', title: 'cantaloupe mango banana', titleCn: '香瓜芒果香蕉', shortName: 'CA' },
|
||||||
|
{ name: 'caramel', title: 'caramel', titleCn: '焦糖', shortName: 'CA' },
|
||||||
|
{ name: 'caribbean-spirit', title: 'caribbean spirit', titleCn: '加勒比风情', shortName: 'CA' },
|
||||||
|
{ name: 'caribbean-white', title: 'caribbean white', titleCn: '加勒比白', shortName: 'CA' },
|
||||||
|
{ name: 'cherry', title: 'cherry', titleCn: '樱桃', shortName: 'CH' },
|
||||||
|
{ name: 'cherry-blast-ice', title: 'cherry blast ice', titleCn: '樱桃爆炸冰', shortName: 'CH' },
|
||||||
|
{ name: 'cherry-classic-cola', title: 'cherry classic cola', titleCn: '樱桃经典可乐', shortName: 'CH' },
|
||||||
|
{ name: 'cherry-classic-red', title: 'cherry classic red', titleCn: '樱桃经典红', shortName: 'CH' },
|
||||||
|
{ name: 'cherry-cola-ice', title: 'cherry cola ice', titleCn: '樱桃可乐冰', shortName: 'CH' },
|
||||||
|
{ name: 'cherry-ice', title: 'cherry ice', titleCn: '樱桃冰', shortName: 'CH' },
|
||||||
|
{ name: 'cherry-lemon', title: 'cherry lemon', titleCn: '樱桃柠檬', shortName: 'CH' },
|
||||||
|
{ name: 'cherry-lime-classic', title: 'cherry lime classic', titleCn: '樱桃青柠经典', shortName: 'CH' },
|
||||||
|
{ name: 'cherry-lime-ice', title: 'cherry lime ice', titleCn: '樱桃青柠冰', shortName: 'CH' },
|
||||||
|
{ name: 'cherry-lychee', title: 'cherry lychee', titleCn: '樱桃荔枝', shortName: 'CH' },
|
||||||
|
{ name: 'cherry-peach-lemon', title: 'cherry peach lemon', titleCn: '樱桃桃子柠檬', shortName: 'CH' },
|
||||||
|
{ name: 'cherry-red-classic', title: 'cherry red classic', titleCn: '红樱桃经典', shortName: 'CH' },
|
||||||
|
{ name: 'cherry-strazz', title: 'cherry strazz', titleCn: '樱桃草莓', shortName: 'CH' },
|
||||||
|
{ name: 'cherry-watermelon', title: 'cherry watermelon', titleCn: '樱桃西瓜', shortName: 'CH' },
|
||||||
|
{ name: 'chill', title: 'chill', titleCn: '冰爽', shortName: 'CH' },
|
||||||
|
{ name: 'chilled-classic-red', title: 'chilled classic red', titleCn: '冰镇经典红', shortName: 'CH' },
|
||||||
|
{ name: 'chillin-coffee-iced', title: 'chillin coffee iced', titleCn: '冰镇咖啡', shortName: 'CH' },
|
||||||
|
{ name: 'chilly-jiggle-b', title: 'chilly jiggle b', titleCn: '清凉果冻 B', shortName: 'CH' },
|
||||||
|
{ name: 'churned-peanut', title: 'churned peanut', titleCn: '搅拌花生', shortName: 'CH' },
|
||||||
|
{ name: 'cinnamon', title: 'cinnamon', titleCn: '肉桂', shortName: 'CI' },
|
||||||
|
{ name: 'cinnamon-flame', title: 'cinnamon flame', titleCn: '肉桂火焰', shortName: 'CI' },
|
||||||
|
{ name: 'cinnamon-roll', title: 'cinnamon roll', titleCn: '肉桂卷', shortName: 'CI' },
|
||||||
|
{ name: 'circle-of-life', title: 'circle of life', titleCn: '生命循环', shortName: 'CI' },
|
||||||
|
{ name: 'citrus', title: 'citrus', titleCn: '柑橘', shortName: 'CI' },
|
||||||
|
{ name: 'citrus-burst-ice', title: 'citrus burst ice', titleCn: '柑橘爆发冰', shortName: 'CI' },
|
||||||
|
{ name: 'citrus-chill', title: 'citrus chill', titleCn: '柑橘清凉', shortName: 'CI' },
|
||||||
|
{ name: 'citrus-smash-ice', title: 'citrus smash ice', titleCn: '柑橘冲击冰', shortName: 'CI' },
|
||||||
|
{ name: 'citrus-sunrise', title: 'citrus sunrise', titleCn: '柑橘日出', shortName: 'CI' },
|
||||||
|
{ name: 'citrus-sunrise-(thermal)', title: 'citrus sunrise (thermal)', titleCn: '柑橘日出(热感)', shortName: 'CI' },
|
||||||
|
{ name: 'classic', title: 'classic', titleCn: '经典', shortName: 'CL' },
|
||||||
|
{ name: 'classic-ice', title: 'classic ice', titleCn: '经典冰', shortName: 'CL' },
|
||||||
|
{ name: 'classic-mint-ice', title: 'classic mint ice', titleCn: '经典薄荷冰', shortName: 'CL' },
|
||||||
|
{ name: 'classic-tobacco', title: 'classic tobacco', titleCn: '经典烟草', shortName: 'CL' },
|
||||||
|
{ name: 'classical-tobacco', title: 'classical tobacco', titleCn: '古典烟草', shortName: 'CL' },
|
||||||
|
{ name: 'coconut-ice', title: 'coconut ice', titleCn: '椰子冰', shortName: 'CO' },
|
||||||
|
{ name: 'coconut-water-ice', title: 'coconut water ice', titleCn: '椰子水冰', shortName: 'CO' },
|
||||||
|
{ name: 'coffee', title: 'coffee', titleCn: '咖啡', shortName: 'CO' },
|
||||||
|
{ name: 'coffee-stout', title: 'coffee stout', titleCn: '咖啡烈酒', shortName: 'CO' },
|
||||||
|
{ name: 'cola', title: 'cola', titleCn: '可乐', shortName: 'CO' },
|
||||||
|
{ name: 'cola-&-cherry', title: 'cola & cherry', titleCn: '可乐樱桃', shortName: 'CO' },
|
||||||
|
{ name: 'cola-&-vanilla', title: 'cola & vanilla', titleCn: '可乐香草', shortName: 'CO' },
|
||||||
|
{ name: 'cola-ice', title: 'cola ice', titleCn: '可乐冰', shortName: 'CO' },
|
||||||
|
{ name: 'cool-frost', title: 'cool frost', titleCn: '酷霜', shortName: 'CO' },
|
||||||
|
{ name: 'cool-mint', title: 'cool mint', titleCn: '酷薄荷', shortName: 'CO' },
|
||||||
|
{ name: 'cool-mint-ice', title: 'cool mint ice', titleCn: '酷薄荷冰', shortName: 'CO' },
|
||||||
|
{ name: 'cool-storm', title: 'cool storm', titleCn: '酷风暴', shortName: 'CO' },
|
||||||
|
{ name: 'cool-tropical', title: 'cool tropical', titleCn: '酷热带', shortName: 'CO' },
|
||||||
|
{ name: 'cool-watermelon', title: 'cool watermelon', titleCn: '酷西瓜', shortName: 'CO' },
|
||||||
|
{ name: 'cotton-clouds', title: 'cotton clouds', titleCn: '棉花云', shortName: 'CO' },
|
||||||
|
{ name: 'cranberry-blackcurrant', title: 'cranberry blackcurrant', titleCn: '蔓越莓黑加仑', shortName: 'CR' },
|
||||||
|
{ name: 'cranberry-lemon', title: 'cranberry lemon', titleCn: '蔓越莓柠檬', shortName: 'CR' },
|
||||||
|
{ name: 'cranberry-lemon-ice', title: 'cranberry lemon ice', titleCn: '蔓越莓柠檬冰', shortName: 'CR' },
|
||||||
|
{ name: 'creamy-maple', title: 'creamy maple', titleCn: '奶香枫糖', shortName: 'CR' },
|
||||||
|
{ name: 'creamy-vanilla', title: 'creamy vanilla', titleCn: '奶香香草', shortName: 'CR' },
|
||||||
|
{ name: 'crispy-peppermint', title: 'crispy peppermint', titleCn: '脆薄荷', shortName: 'CR' },
|
||||||
|
{ name: 'cuban-tobacco', title: 'cuban tobacco', titleCn: '古巴烟草', shortName: 'CU' },
|
||||||
|
{ name: 'cucumber-lime', title: 'cucumber lime', titleCn: '黄瓜青柠', shortName: 'CU' },
|
||||||
|
{ name: 'dark-blackcurrant', title: 'dark blackcurrant', titleCn: '深黑加仑', shortName: 'DA' },
|
||||||
|
{ name: 'dark-forest', title: 'dark forest', titleCn: '深林', shortName: 'DA' },
|
||||||
|
{ name: 'deep-freeze', title: 'deep freeze', titleCn: '极冻', shortName: 'DE' },
|
||||||
|
{ name: 'dope-double-kiwi-iced', title: 'dope double kiwi iced', titleCn: '双奇异果冰', shortName: 'DO' },
|
||||||
|
{ name: 'dope-double-kiwi-lced', title: 'dope double kiwi lced', titleCn: '双奇异果冷饮', shortName: 'DO' },
|
||||||
|
{ name: 'double-apple', title: 'double apple', titleCn: '双苹果', shortName: 'DO' },
|
||||||
|
{ name: 'double-apple-ice', title: 'double apple ice', titleCn: '双苹果冰', shortName: 'DO' },
|
||||||
|
{ name: 'double-berry-twist-ice', title: 'double berry twist ice', titleCn: '双浆果扭曲冰', shortName: 'DO' },
|
||||||
|
{ name: 'double-ice', title: 'double ice', titleCn: '双冰', shortName: 'DO' },
|
||||||
|
{ name: 'double-mango', title: 'double mango', titleCn: '双芒果', shortName: 'DO' },
|
||||||
|
{ name: 'double-mint', title: 'double mint', titleCn: '双薄荷', shortName: 'DO' },
|
||||||
|
{ name: 'double-mocha', title: 'double mocha', titleCn: '双摩卡', shortName: 'DO' },
|
||||||
|
{ name: 'double-shot-espresso', title: 'double shot espresso', titleCn: '双份浓缩咖啡', shortName: 'DO' },
|
||||||
|
{ name: 'dragon-berry-mango-ice', title: 'dragon berry mango ice', titleCn: '龙莓芒果冰', shortName: 'DR' },
|
||||||
|
{ name: 'dragon-fruit', title: 'dragon fruit', titleCn: '龙果', shortName: 'DR' },
|
||||||
|
{ name: 'dragon-fruit-lychee-ice', title: 'dragon fruit lychee ice', titleCn: '龙果荔枝冰', shortName: 'DR' },
|
||||||
|
{ name: 'dragon-fruit-strawberry-ice', title: 'dragon fruit strawberry ice', titleCn: '龙果草莓冰', shortName: 'DR' },
|
||||||
|
{ name: 'dragon-fruit-strawnana', title: 'dragon fruit strawnana', titleCn: '龙果香蕉', shortName: 'DR' },
|
||||||
|
{ name: 'dragon-melon-ice', title: 'dragon melon ice', titleCn: '龙瓜冰', shortName: 'DR' },
|
||||||
|
{ name: 'dragonfruit-lychee', title: 'dragonfruit lychee', titleCn: '龙果荔枝', shortName: 'DR' },
|
||||||
|
{ name: 'dreamy-dragonfruit-lychee-iced', title: 'dreamy dragonfruit lychee iced', titleCn: '梦幻龙果荔枝冰', shortName: 'DR' },
|
||||||
|
{ name: 'dub-dub', title: 'dub dub', titleCn: '双重', shortName: 'DU' },
|
||||||
|
{ name: 'durian', title: 'durian', titleCn: '榴莲', shortName: 'DU' },
|
||||||
|
{ name: 'electric-fruit-blast', title: 'electric fruit blast', titleCn: '电果爆炸', shortName: 'EL' },
|
||||||
|
{ name: 'electric-orange', title: 'electric orange', titleCn: '电橙', shortName: 'EL' },
|
||||||
|
{ name: 'energy-drink', title: 'energy drink', titleCn: '能量饮料', shortName: 'EN' },
|
||||||
|
{ name: 'epic-apple', title: 'epic apple', titleCn: '极致苹果', shortName: 'EP' },
|
||||||
|
{ name: 'epic-apple-peach', title: 'epic apple peach', titleCn: '极致苹果桃', shortName: 'EP' },
|
||||||
|
{ name: 'epic-banana', title: 'epic banana', titleCn: '极致香蕉', shortName: 'EP' },
|
||||||
|
{ name: 'epic-berry-swirl', title: 'epic berry swirl', titleCn: '极致浆果旋风', shortName: 'EP' },
|
||||||
|
{ name: 'epic-blue-razz', title: 'epic blue razz', titleCn: '极致蓝覆盆子', shortName: 'EP' },
|
||||||
|
{ name: 'epic-fruit-bomb', title: 'epic fruit bomb', titleCn: '极致水果炸弹', shortName: 'EP' },
|
||||||
|
{ name: 'epic-grape', title: 'epic grape', titleCn: '极致葡萄', shortName: 'EP' },
|
||||||
|
{ name: 'epic-honeydew-blackcurrant', title: 'epic honeydew blackcurrant', titleCn: '极致蜜瓜黑加仑', shortName: 'EP' },
|
||||||
|
{ name: 'epic-kiwi-mango', title: 'epic kiwi mango', titleCn: '极致奇异果芒果', shortName: 'EP' },
|
||||||
|
{ name: 'epic-peach-mango', title: 'epic peach mango', titleCn: '极致桃芒果', shortName: 'EP' },
|
||||||
|
{ name: 'epic-peppermint', title: 'epic peppermint', titleCn: '极致薄荷', shortName: 'EP' },
|
||||||
|
{ name: 'epic-sour-berries', title: 'epic sour berries', titleCn: '极致酸浆果', shortName: 'EP' },
|
||||||
|
{ name: 'epic-strawberry', title: 'epic strawberry', titleCn: '极致草莓', shortName: 'EP' },
|
||||||
|
{ name: 'epic-strawberry-watermelon', title: 'epic strawberry watermelon', titleCn: '极致草莓西瓜', shortName: 'EP' },
|
||||||
|
{ name: 'epic-watermelon-kiwi', title: 'epic watermelon kiwi', titleCn: '极致西瓜奇异果', shortName: 'EP' },
|
||||||
|
{ name: 'exotic-mango', title: 'exotic mango', titleCn: '异国芒果', shortName: 'EX' },
|
||||||
|
{ name: 'extreme-chill-mint', title: 'extreme chill mint', titleCn: '极寒薄荷', shortName: 'EX' },
|
||||||
|
{ name: 'extreme-cinnamon', title: 'extreme cinnamon', titleCn: '极寒肉桂', shortName: 'EX' },
|
||||||
|
{ name: 'extreme-mint', title: 'extreme mint', titleCn: '极寒薄荷', shortName: 'EX' },
|
||||||
|
{ name: 'extreme-mint-iced', title: 'extreme mint iced', titleCn: '极寒薄荷冰', shortName: 'EX' },
|
||||||
|
{ name: 'famous-fruit-ko-iced', title: 'famous fruit ko iced', titleCn: '知名水果 KO 冰', shortName: 'FA' },
|
||||||
|
{ name: 'famous-fruit-ko-lced', title: 'famous fruit ko lced', titleCn: '知名水果 KO 冷饮', shortName: 'FA' },
|
||||||
|
{ name: 'fizzy', title: 'fizzy', titleCn: '汽水', shortName: 'FI' },
|
||||||
|
{ name: 'flavourless', title: 'flavourless', titleCn: '无味', shortName: 'FL' },
|
||||||
|
{ name: 'flippin-fruit-flash', title: 'flippin fruit flash', titleCn: '翻转水果闪电', shortName: 'FL' },
|
||||||
|
{ name: 'flippin-fruit-flash-(rainbow-burst)', title: 'flippin fruit flash (rainbow burst)', titleCn: '翻转水果闪电(彩虹爆发)', shortName: 'FL' },
|
||||||
|
{ name: 'forest-fruits', title: 'forest fruits', titleCn: '森林水果', shortName: 'FO' },
|
||||||
|
{ name: 'fragrant-grapefruit', title: 'fragrant grapefruit', titleCn: '香气葡萄柚', shortName: 'FR' },
|
||||||
|
{ name: 'freeze', title: 'freeze', titleCn: '冰冻', shortName: 'FR' },
|
||||||
|
{ name: 'freeze-mint', title: 'freeze mint', titleCn: '冰薄荷', shortName: 'FR' },
|
||||||
|
{ name: 'freeze-mint-salty', title: 'freeze mint salty', titleCn: '冰薄荷咸味', shortName: 'FR' },
|
||||||
|
{ name: 'freezing-peppermint', title: 'freezing peppermint', titleCn: '冰爽薄荷', shortName: 'FR' },
|
||||||
|
{ name: 'freezy-berry-peachy', title: 'freezy berry peachy', titleCn: '冰冻浆果桃', shortName: 'FR' },
|
||||||
|
{ name: 'fresh-fruit', title: 'fresh fruit', titleCn: '新鲜水果', shortName: 'FR' },
|
||||||
|
{ name: 'fresh-mint', title: 'fresh mint', titleCn: '新鲜薄荷', shortName: 'FR' },
|
||||||
|
{ name: 'fresh-mint-ice', title: 'fresh mint ice', titleCn: '新鲜薄荷冰', shortName: 'FR' },
|
||||||
|
{ name: 'froot-b', title: 'froot b', titleCn: '水果 B', shortName: 'FR' },
|
||||||
|
{ name: 'frost', title: 'frost', titleCn: '霜冻', shortName: 'FR' },
|
||||||
|
{ name: 'frost-mint', title: 'frost mint', titleCn: '霜薄荷', shortName: 'FR' },
|
||||||
|
{ name: 'frosted-strawberries', title: 'frosted strawberries', titleCn: '霜冻草莓', shortName: 'FR' },
|
||||||
|
{ name: 'frosty-grapefruit', title: 'frosty grapefruit', titleCn: '冰爽葡萄柚', shortName: 'FR' },
|
||||||
|
{ name: 'frozen-classical-ice', title: 'frozen classical ice', titleCn: '冷冻经典冰', shortName: 'FR' },
|
||||||
|
{ name: 'frozen-cloudberry', title: 'frozen cloudberry', titleCn: '冷冻云莓', shortName: 'FR' },
|
||||||
|
{ name: 'frozen-mint', title: 'frozen mint', titleCn: '冷冻薄荷', shortName: 'FR' },
|
||||||
|
{ name: 'frozen-pineapple', title: 'frozen pineapple', titleCn: '冷冻菠萝', shortName: 'FR' },
|
||||||
|
{ name: 'frozen-strawberry', title: 'frozen strawberry', titleCn: '冷冻草莓', shortName: 'FR' },
|
||||||
|
{ name: 'frozen-strawberrygb(gummy-bear)', title: 'frozen strawberrygb(gummy bear)', titleCn: '冷冻草莓软糖', shortName: 'FR' },
|
||||||
|
{ name: 'grapefruit-grape-gb(gummy-bear)', title: 'grapefruit grape gb(gummy bear)', titleCn: '葡萄柚葡萄软糖', shortName: 'GR' },
|
||||||
|
{ name: 'fruit-flash-ice', title: 'fruit flash ice', titleCn: '水果闪电冰', shortName: 'FR' },
|
||||||
|
{ name: 'fruity-explosion', title: 'fruity explosion', titleCn: '水果爆炸', shortName: 'FR' },
|
||||||
|
{ name: 'fuji-apple-ice', title: 'fuji apple ice', titleCn: '富士苹果冰', shortName: 'FU' },
|
||||||
|
{ name: 'fuji-ice', title: 'fuji ice', titleCn: '富士冰', shortName: 'FU' },
|
||||||
|
{ name: 'fuji-melon-ice', title: 'fuji melon ice', titleCn: '富士瓜冰', shortName: 'FU' },
|
||||||
|
{ name: 'full-charge', title: 'full charge', titleCn: '满电', shortName: 'FU' },
|
||||||
|
{ name: 'gb', title: 'gb', titleCn: '软糖', shortName: 'GB' },
|
||||||
|
{ name: 'gb(gummy-bear)', title: 'gb(gummy bear)', titleCn: '软糖(Gummy Bear)', shortName: 'GB' },
|
||||||
|
{ name: 'gentle-mint', title: 'gentle mint', titleCn: '温和薄荷', shortName: 'GE' },
|
||||||
|
{ name: 'ghost-cola-&-vanilla', title: 'ghost cola & vanilla', titleCn: '幽灵可乐香草', shortName: 'GH' },
|
||||||
|
{ name: 'ghost-cola-ice', title: 'ghost cola ice', titleCn: '幽灵可乐冰', shortName: 'GH' },
|
||||||
|
{ name: 'ghost-mango', title: 'ghost mango', titleCn: '幽灵芒果', shortName: 'GH' },
|
||||||
|
{ name: 'ghost-original', title: 'ghost original', titleCn: '幽灵原味', shortName: 'GH' },
|
||||||
|
{ name: 'ghost-watermelon-ice', title: 'ghost watermelon ice', titleCn: '幽灵西瓜冰', shortName: 'GH' },
|
||||||
|
{ name: 'gnarly-green-d-(green-dew)', title: 'gnarly green d (green dew)', titleCn: '狂野绿 D(绿色露水)', shortName: 'GN' },
|
||||||
|
{ name: 'gold-edition', title: 'gold edition', titleCn: '金版', shortName: 'GO' },
|
||||||
|
{ name: 'grape', title: 'grape', titleCn: '葡萄', shortName: 'GR' },
|
||||||
|
{ name: 'grape-cherry', title: 'grape cherry', titleCn: '葡萄樱桃', shortName: 'GR' },
|
||||||
|
{ name: 'grape-fury-ice', title: 'grape fury ice', titleCn: '葡萄狂怒冰', shortName: 'GR' },
|
||||||
|
{ name: 'grape-honeydew-ice', title: 'grape honeydew ice', titleCn: '葡萄蜜瓜冰', shortName: 'GR' },
|
||||||
|
{ name: 'grape-ice', title: 'grape ice', titleCn: '葡萄冰', shortName: 'GR' },
|
||||||
|
{ name: 'grape-pomegranate-ice', title: 'grape pomegranate ice', titleCn: '葡萄石榴冰', shortName: 'GR' },
|
||||||
|
{ name: 'grapefruit-grape', title: 'grapefruit grape', titleCn: '葡萄柚葡萄', shortName: 'GR' },
|
||||||
|
{ name: 'grapefruit-ice', title: 'grapefruit ice', titleCn: '葡萄柚冰', shortName: 'GR' },
|
||||||
|
{ name: 'grapes', title: 'grapes', titleCn: '葡萄', shortName: 'GR' },
|
||||||
|
{ name: 'grapplin-grape-sour-apple-iced', title: 'grapplin grape sour apple iced', titleCn: '葡萄酸苹果冰', shortName: 'GR' },
|
||||||
|
{ name: 'green-apple', title: 'green apple', titleCn: '青苹果', shortName: 'GR' },
|
||||||
|
{ name: 'green-apple-ice', title: 'green apple ice', titleCn: '青苹果冰', shortName: 'GR' },
|
||||||
|
{ name: 'green-grape-ice', title: 'green grape ice', titleCn: '青葡萄冰', shortName: 'GR' },
|
||||||
|
{ name: 'green-mango-ice', title: 'green mango ice', titleCn: '青芒果冰', shortName: 'GR' },
|
||||||
|
{ name: 'green-mint', title: 'green mint', titleCn: '青薄荷', shortName: 'GR' },
|
||||||
|
{ name: 'green-spearmint', title: 'green spearmint', titleCn: '青留兰香', shortName: 'GR' },
|
||||||
|
{ name: 'green-tea', title: 'green tea', titleCn: '绿茶', shortName: 'GR' },
|
||||||
|
{ name: 'groovy-grape', title: 'groovy grape', titleCn: '活力葡萄', shortName: 'GR' },
|
||||||
|
{ name: 'groovy-grape-passionfruit-iced', title: 'groovy grape passionfruit iced', titleCn: '活力葡萄激情果冰', shortName: 'GR' },
|
||||||
|
{ name: 'guava-ice', title: 'guava ice', titleCn: '番石榴冰', shortName: 'GU' },
|
||||||
|
{ name: 'guava-ice-t', title: 'guava ice t', titleCn: '番石榴冰 T', shortName: 'GU' },
|
||||||
|
{ name: 'guava-mango-peach', title: 'guava mango peach', titleCn: '番石榴芒果桃', shortName: 'GU' },
|
||||||
|
{ name: 'gusto-green-apple', title: 'gusto green apple', titleCn: '绿苹果狂热', shortName: 'GU' },
|
||||||
|
{ name: 'hakuna', title: 'hakuna', titleCn: '哈库纳', shortName: 'HA' },
|
||||||
|
{ name: 'harambae', title: 'harambae', titleCn: '哈兰贝', shortName: 'HA' },
|
||||||
|
{ name: 'harmony', title: 'harmony', titleCn: '和谐', shortName: 'HA' },
|
||||||
|
{ name: 'haven', title: 'haven', titleCn: '避风港', shortName: 'HA' },
|
||||||
|
{ name: 'haven-iced', title: 'haven iced', titleCn: '避风港冰', shortName: 'HA' },
|
||||||
|
{ name: 'hawaiian-blue', title: 'hawaiian blue', titleCn: '夏威夷蓝', shortName: 'HA' },
|
||||||
|
{ name: 'hawaiian-mist-ice', title: 'hawaiian mist ice', titleCn: '夏威夷薄雾冰', shortName: 'HA' },
|
||||||
|
{ name: 'hawaiian-storm', title: 'hawaiian storm', titleCn: '夏威夷风暴', shortName: 'HA' },
|
||||||
|
{ name: 'hip-honeydew-mango-iced', title: 'hip honeydew mango iced', titleCn: '蜜瓜芒果冰', shortName: 'HI' },
|
||||||
|
{ name: 'hokkaido-milk', title: 'hokkaido milk', titleCn: '北海道牛奶', shortName: 'HO' },
|
||||||
|
{ name: 'honeydew-blackcurrant', title: 'honeydew blackcurrant', titleCn: '蜜瓜黑加仑', shortName: 'HO' },
|
||||||
|
{ name: 'honeydew-mango-ice', title: 'honeydew mango ice', titleCn: '蜜瓜芒果冰', shortName: 'HO' },
|
||||||
|
{ name: 'hype', title: 'hype', titleCn: '狂热', shortName: 'HY' },
|
||||||
|
{ name: 'ice-blast', title: 'ice blast', titleCn: '冰爆', shortName: 'IC' },
|
||||||
|
{ name: 'ice-cool', title: 'ice cool', titleCn: '冰凉', shortName: 'IC' },
|
||||||
|
{ name: 'ice-cream', title: 'ice cream', titleCn: '冰淇淋', shortName: 'IC' },
|
||||||
|
{ name: 'ice-mint', title: 'ice mint', titleCn: '冰薄荷', shortName: 'IC' },
|
||||||
|
{ name: 'ice-wintergreen', title: 'ice wintergreen', titleCn: '冰冬青', shortName: 'IC' },
|
||||||
|
{ name: 'iced-americano', title: 'iced americano', titleCn: '冰美式', shortName: 'IC' },
|
||||||
|
{ name: 'icy-berries', title: 'icy berries', titleCn: '冰爽浆果', shortName: 'IC' },
|
||||||
|
{ name: 'icy-blackcurrant', title: 'icy blackcurrant', titleCn: '冰爽黑加仑', shortName: 'IC' },
|
||||||
|
{ name: 'icy-cherry', title: 'icy cherry', titleCn: '冰爽樱桃', shortName: 'IC' },
|
||||||
|
{ name: 'icy-mint', title: 'icy mint', titleCn: '冰爽薄荷', shortName: 'IC' },
|
||||||
|
{ name: 'icy-pink-clouds', title: 'icy pink clouds', titleCn: '冰粉云', shortName: 'IC' },
|
||||||
|
{ name: 'intense-blue-razz', title: 'intense blue razz', titleCn: '强烈蓝覆盆子', shortName: 'IN' },
|
||||||
|
{ name: 'intense-blueberry-lemon', title: 'intense blueberry lemon', titleCn: '强烈蓝莓柠檬', shortName: 'IN' },
|
||||||
|
{ name: 'intense-flavourless', title: 'intense flavourless', titleCn: '强烈无味', shortName: 'IN' },
|
||||||
|
{ name: 'intense-fruity-explosion', title: 'intense fruity explosion', titleCn: '强烈水果爆炸', shortName: 'IN' },
|
||||||
|
{ name: 'intense-juicy-peach', title: 'intense juicy peach', titleCn: '强烈多汁桃', shortName: 'IN' },
|
||||||
|
{ name: 'intense-red-apple', title: 'intense red apple', titleCn: '强烈红苹果', shortName: 'IN' },
|
||||||
|
{ name: 'intense-ripe-mango', title: 'intense ripe mango', titleCn: '强烈熟芒果', shortName: 'IN' },
|
||||||
|
{ name: 'intense-strawberry-watermelon', title: 'intense strawberry watermelon', titleCn: '强烈草莓西瓜', shortName: 'IN' },
|
||||||
|
{ name: 'intense-white-grape', title: 'intense white grape', titleCn: '强烈白葡萄', shortName: 'IN' },
|
||||||
|
{ name: 'intense-white-mint', title: 'intense white mint', titleCn: '强烈白薄荷', shortName: 'IN' },
|
||||||
|
{ name: 'jasmine-tea', title: 'jasmine tea', titleCn: '茉莉茶', shortName: 'JA' },
|
||||||
|
{ name: 'jiggly-b', title: 'jiggly b', titleCn: '果冻 B', shortName: 'JI' },
|
||||||
|
{ name: 'jiggly-sting', title: 'jiggly sting', titleCn: '果冻刺', shortName: 'JI' },
|
||||||
|
{ name: 'juicy-mango', title: 'juicy mango', titleCn: '多汁芒果', shortName: 'JU' },
|
||||||
|
{ name: 'juicy-peach', title: 'juicy peach', titleCn: '多汁桃', shortName: 'JU' },
|
||||||
|
{ name: 'juicy-peach-ice', title: 'juicy peach ice', titleCn: '多汁桃冰', shortName: 'JU' },
|
||||||
|
{ name: 'jungle-secrets', title: 'jungle secrets', titleCn: '丛林秘密', shortName: 'JU' },
|
||||||
|
{ name: 'kanzi', title: 'kanzi', titleCn: '甘之', shortName: 'KA' },
|
||||||
|
{ name: 'kewl-kiwi-passionfruit-iced', title: 'kewl kiwi passionfruit iced', titleCn: '酷奇奇', shortName: 'KE' },
|
||||||
|
{ name: 'kiwi-berry-ice', title: 'kiwi berry ice', titleCn: '奇异果浆果冰', shortName: 'KI' },
|
||||||
|
{ name: 'kiwi-dragon-berry', title: 'kiwi dragon berry', titleCn: '奇异果龙莓', shortName: 'KI' },
|
||||||
|
{ name: 'kiwi-green-t', title: 'kiwi green t', titleCn: '奇异果绿茶', shortName: 'KI' },
|
||||||
|
{ name: 'kiwi-guava-ice', title: 'kiwi guava ice', titleCn: '奇异果番石榴冰', shortName: 'KI' },
|
||||||
|
{ name: 'kiwi-guava-passionfruit-ice', title: 'kiwi guava passionfruit ice', titleCn: '奇异果番石榴激情果冰', shortName: 'KI' },
|
||||||
|
{ name: 'kiwi-passion-fruit-guava', title: 'kiwi passion fruit guava', titleCn: '奇异果激情果番石榴', shortName: 'KI' },
|
||||||
|
{ name: 'kyoho-grape', title: 'kyoho grape', titleCn: '巨峰葡萄', shortName: 'KY' },
|
||||||
|
{ name: 'kyoho-grape-ice', title: 'kyoho grape ice', titleCn: '巨峰葡萄冰', shortName: 'KY' },
|
||||||
|
{ name: 'lemon', title: 'lemon', titleCn: '柠檬', shortName: 'LE' },
|
||||||
|
{ name: 'lemon-berry', title: 'lemon berry', titleCn: '柠檬浆果', shortName: 'LE' },
|
||||||
|
{ name: 'lemon-blue-razz-ice', title: 'lemon blue razz ice', titleCn: '柠檬蓝覆盆子冰', shortName: 'LE' },
|
||||||
|
{ name: 'lemon-lime-cranberry', title: 'lemon lime cranberry', titleCn: '柠檬青柠蔓越莓', shortName: 'LE' },
|
||||||
|
{ name: 'lemon-lime-ice', title: 'lemon lime ice', titleCn: '柠檬青柠冰', shortName: 'LE' },
|
||||||
|
{ name: 'lemon-sprite', title: 'lemon sprite', titleCn: '柠檬汽水', shortName: 'LE' },
|
||||||
|
{ name: 'lemon-spritz', title: 'lemon spritz', titleCn: '柠檬气泡', shortName: 'LE' },
|
||||||
|
{ name: 'lemon-squeeze-ice', title: 'lemon squeeze ice', titleCn: '柠檬榨汁冰', shortName: 'LE' },
|
||||||
|
{ name: 'lemon-squeeze-iced', title: 'lemon squeeze iced', titleCn: '柠檬榨汁冷饮', shortName: 'LE' },
|
||||||
|
{ name: 'lemon-t', title: 'lemon t', titleCn: '柠檬 T', shortName: 'LE' },
|
||||||
|
{ name: 'lemon-tea-ice', title: 'lemon tea ice', titleCn: '柠檬茶冰', shortName: 'LE' },
|
||||||
|
{ name: 'lemon-twist-ice', title: 'lemon twist ice', titleCn: '柠檬扭转冰', shortName: 'LE' },
|
||||||
|
{ name: 'lemur', title: 'lemur', titleCn: '狐猴', shortName: 'LE' },
|
||||||
|
{ name: 'lime-berry-orange-ice', title: 'lime berry orange ice', titleCn: '青柠浆果橙冰', shortName: 'LI' },
|
||||||
|
{ name: 'lime-flame', title: 'lime flame', titleCn: '青柠火焰', shortName: 'LI' },
|
||||||
|
{ name: 'liquorice', title: 'liquorice', titleCn: '甘草', shortName: 'LI' },
|
||||||
|
{ name: 'lit-lychee-watermelon-iced', title: 'lit lychee watermelon iced', titleCn: '荔枝西瓜冰', shortName: 'LI' },
|
||||||
|
{ name: 'loco-cocoa-latte-iced', title: 'loco cocoa latte iced', titleCn: '可可拿铁冷饮', shortName: 'LO' },
|
||||||
|
{ name: 'lofty-liquorice', title: 'lofty liquorice', titleCn: '高挑甘草', shortName: 'LO' },
|
||||||
|
{ name: 'lush-ice', title: 'lush ice', titleCn: '冰爽浓郁', shortName: 'LU' },
|
||||||
|
{ name: 'lychee-ice', title: 'lychee ice', titleCn: '荔枝冰', shortName: 'LY' },
|
||||||
|
{ name: 'lychee-mango-ice', title: 'lychee mango ice', titleCn: '荔枝芒果冰', shortName: 'LY' },
|
||||||
|
{ name: 'lychee-mango-melon', title: 'lychee mango melon', titleCn: '荔枝芒果瓜', shortName: 'LY' },
|
||||||
|
{ name: 'lychee-melon-ice', title: 'lychee melon ice', titleCn: '荔枝瓜冰', shortName: 'LY' },
|
||||||
|
{ name: 'lychee-watermelon-strawberry', title: 'lychee watermelon strawberry', titleCn: '荔枝西瓜草莓', shortName: 'LY' },
|
||||||
|
{ name: 'mad-mango-peach', title: 'mad mango peach', titleCn: '疯狂芒果桃', shortName: 'MA' },
|
||||||
|
{ name: 'mangabeys', title: 'mangabeys', titleCn: '长臂猿', shortName: 'MA' },
|
||||||
|
{ name: 'mango', title: 'mango', titleCn: '芒果', shortName: 'MA' },
|
||||||
|
{ name: 'mango-berry', title: 'mango berry', titleCn: '芒果浆果', shortName: 'MA' },
|
||||||
|
{ name: 'mango-blueberry', title: 'mango blueberry', titleCn: '芒果蓝莓', shortName: 'MA' },
|
||||||
|
{ name: 'mango-dragon-fruit-lemon-ice', title: 'mango dragon fruit lemon ice', titleCn: '芒果龙果柠檬冰', shortName: 'MA' },
|
||||||
|
{ name: 'mango-flame', title: 'mango flame', titleCn: '芒果火焰', shortName: 'MA' },
|
||||||
|
{ name: 'mango-honeydew-ice', title: 'mango honeydew ice', titleCn: '芒果蜜瓜冰', shortName: 'MA' },
|
||||||
|
{ name: 'mango-ice', title: 'mango ice', titleCn: '芒果冰', shortName: 'MA' },
|
||||||
|
{ name: 'mango-madness', title: 'mango madness', titleCn: '芒果狂热', shortName: 'MA' },
|
||||||
|
{ name: 'mango-nectar-ice', title: 'mango nectar ice', titleCn: '芒果花蜜冰', shortName: 'MA' },
|
||||||
|
{ name: 'mango-on-ice', title: 'mango on ice', titleCn: '芒果冰镇', shortName: 'MA' },
|
||||||
|
{ name: 'mango-melon', title: 'mango melon', titleCn: '芒果瓜', shortName: 'MA' },
|
||||||
|
{ name: 'mango-peach', title: 'mango peach', titleCn: '芒果桃', shortName: 'MA' },
|
||||||
|
{ name: 'mango-peach-apricot-ice', title: 'mango peach apricot ice', titleCn: '芒果桃杏冰', shortName: 'MA' },
|
||||||
|
{ name: 'mango-peach-orange', title: 'mango peach orange', titleCn: '芒果桃橙', shortName: 'MA' },
|
||||||
|
{ name: 'mango-peach-tings', title: 'mango peach tings', titleCn: '芒果桃滋味', shortName: 'MA' },
|
||||||
|
{ name: 'mango-peach-watermelon', title: 'mango peach watermelon', titleCn: '芒果桃西瓜', shortName: 'MA' },
|
||||||
|
{ name: 'mango-pineapple', title: 'mango pineapple', titleCn: '芒果菠萝', shortName: 'MA' },
|
||||||
|
{ name: 'mango-pineapple-guava-ice', title: 'mango pineapple guava ice', titleCn: '芒果菠萝番石榴冰', shortName: 'MA' },
|
||||||
|
{ name: 'mango-pineapple-ice', title: 'mango pineapple ice', titleCn: '芒果菠萝冰', shortName: 'MA' },
|
||||||
|
{ name: 'mango-squared', title: 'mango squared', titleCn: '芒果平方', shortName: 'MA' },
|
||||||
|
{ name: 'matata', title: 'matata', titleCn: '马塔塔', shortName: 'MA' },
|
||||||
|
{ name: 'max-freeze', title: 'max freeze', titleCn: '极冻', shortName: 'MA' },
|
||||||
|
{ name: 'max-polar-mint', title: 'max polar mint', titleCn: '极地薄荷', shortName: 'MA' },
|
||||||
|
{ name: 'max-polarmint', title: 'max polarmint', titleCn: '极地薄荷', shortName: 'MA' },
|
||||||
|
{ name: 'mclaren-sweet-papaya', title: 'mclaren sweet papaya', titleCn: '迈凯轮甜木瓜', shortName: 'MC' },
|
||||||
|
{ name: 'mega-mixed-berries', title: 'mega mixed berries', titleCn: '超级混合浆果', shortName: 'ME' },
|
||||||
|
{ name: 'melon-&-mint', title: 'melon & mint', titleCn: '瓜与薄荷', shortName: 'ME' },
|
||||||
|
{ name: 'melon-ice', title: 'melon ice', titleCn: '瓜冰', shortName: 'ME' },
|
||||||
|
{ name: 'menthol', title: 'menthol', titleCn: '薄荷', shortName: 'ME' },
|
||||||
|
{ name: 'menthol-ice', title: 'menthol ice', titleCn: '薄荷冰', shortName: 'ME' },
|
||||||
|
{ name: 'mexican-mango-ice', title: 'mexican mango ice', titleCn: '墨西哥芒果冰', shortName: 'ME' },
|
||||||
|
{ name: 'miami-mint', title: 'miami mint', titleCn: '迈阿密薄荷', shortName: 'MI' },
|
||||||
|
{ name: 'mint', title: 'mint', titleCn: '薄荷', shortName: 'MI' },
|
||||||
|
{ name: 'mint-energy', title: 'mint energy', titleCn: '薄荷 能量', shortName: 'MI' },
|
||||||
|
{ name: 'mint-tobacco', title: 'mint tobacco', titleCn: '薄荷烟草', shortName: 'MI' },
|
||||||
|
{ name: 'mirage', title: 'mirage', titleCn: '海市蜃楼', shortName: 'MI' },
|
||||||
|
{ name: 'mix-berries', title: 'mix berries', titleCn: '混合浆果', shortName: 'MI' },
|
||||||
|
{ name: 'mixed-barries', title: 'mixed barries', titleCn: '混合浆果', shortName: 'MI' },
|
||||||
|
{ name: 'mixed-berry', title: 'mixed berry', titleCn: '混合浆果', shortName: 'MI' },
|
||||||
|
{ name: 'mixed-fruit', title: 'mixed fruit', titleCn: '混合水果', shortName: 'MI' },
|
||||||
|
{ name: 'mocha-ice', title: 'mocha ice', titleCn: '摩卡冰', shortName: 'MO' },
|
||||||
|
{ name: 'morocco-mint', title: 'morocco mint', titleCn: '摩洛哥薄荷', shortName: 'MO' },
|
||||||
|
{ name: 'morocco-mint-(thermal)', title: 'morocco mint (thermal)', titleCn: '摩洛哥薄荷(热感)', shortName: 'MO' },
|
||||||
|
{ name: 'mung-beans', title: 'mung beans', titleCn: '绿豆', shortName: 'MU' },
|
||||||
|
{ name: 'nasty-tropic', title: 'nasty tropic', titleCn: '恶搞热带', shortName: 'NA' },
|
||||||
|
{ name: 'nectarine-ice', title: 'nectarine ice', titleCn: '油桃冰', shortName: 'NE' },
|
||||||
|
{ name: 'night-rider', title: 'night rider', titleCn: '夜骑', shortName: 'NI' },
|
||||||
|
{ name: 'nirvana', title: 'nirvana', titleCn: '宁静蓝莓', shortName: 'NI' },
|
||||||
|
{ name: 'north-american-style(root-beer)', title: 'north american style(root beer)', titleCn: '北美风格(根啤)', shortName: 'NO' },
|
||||||
|
{ name: 'northern-blue-razz', title: 'northern blue razz', titleCn: '北方蓝覆盆子', shortName: 'NO' },
|
||||||
|
{ name: 'nutty-virginia', title: 'nutty virginia', titleCn: '坚果弗吉尼亚', shortName: 'NU' },
|
||||||
|
{ name: 'orange', title: 'orange', titleCn: '橙子', shortName: 'OR' },
|
||||||
|
{ name: 'orange-citrus', title: 'orange citrus', titleCn: '橙子柑橘', shortName: 'OR' },
|
||||||
|
{ name: 'orange-fizz-ice', title: 'orange fizz ice', titleCn: '橙子汽水冰', shortName: 'OR' },
|
||||||
|
{ name: 'orange-ft', title: 'orange ft', titleCn: '橙子 FT', shortName: 'OR' },
|
||||||
|
{ name: 'orange-mango-guava', title: 'orange mango guava', titleCn: '橙子芒果番石榴', shortName: 'OR' },
|
||||||
|
{ name: 'orange-mango-pineapple-ice', title: 'orange mango pineapple ice', titleCn: '橙子芒果菠萝冰', shortName: 'OR' },
|
||||||
|
{ name: 'orange-p', title: 'orange p', titleCn: '橙子 P', shortName: 'OR' },
|
||||||
|
{ name: 'orange-p(fanta)', title: 'orange p(fanta)', titleCn: '橙子 P(芬达)', shortName: 'OR' },
|
||||||
|
{ name: 'orange-spark', title: 'orange spark', titleCn: '橙色火花', shortName: 'OR' },
|
||||||
|
{ name: 'orange-tangerine', title: 'orange tangerine', titleCn: '橙子柑橘', shortName: 'OR' },
|
||||||
|
{ name: 'original', title: 'original', titleCn: '原味', shortName: 'OR' },
|
||||||
|
{ name: 'packin-peach-berry', title: 'packin peach berry', titleCn: '装满桃浆果', shortName: 'PA' },
|
||||||
|
{ name: 'packin-peach-berry-(popn-peach-berry)', title: 'packin peach berry (popn peach berry)', titleCn: '装满桃浆果(Pop’n 桃浆果)', shortName: 'PA' },
|
||||||
|
{ name: 'papio', title: 'papio', titleCn: 'Papio', shortName: 'PA' },
|
||||||
|
{ name: 'paradise', title: 'paradise', titleCn: '天堂', shortName: 'PA' },
|
||||||
|
{ name: 'paradise-iced', title: 'paradise iced', titleCn: '天堂冰', shortName: 'PA' },
|
||||||
|
{ name: 'passion', title: 'passion', titleCn: '百香果', shortName: 'PA' },
|
||||||
|
{ name: 'passion-fruit', title: 'passion fruit', titleCn: '百香果冰', shortName: 'PA' },
|
||||||
|
{ name: 'passion-fruit-mango', title: 'passion fruit mango', titleCn: '百香果芒果', shortName: 'PA' },
|
||||||
|
{ name: 'passion-fruit-mango-lime', title: 'passion fruit mango lime', titleCn: '百香果芒果青柠', shortName: 'PA' },
|
||||||
|
{ name: 'passion-guava-grapefruit', title: 'passion guava grapefruit', titleCn: '百香果番石榴葡萄柚', shortName: 'PA' },
|
||||||
|
{ name: 'patas-pipe', title: 'patas pipe', titleCn: '帕塔烟斗', shortName: 'PA' },
|
||||||
|
{ name: 'peach', title: 'peach', titleCn: '桃子', shortName: 'PE' },
|
||||||
|
{ name: 'peach-&-mint', title: 'peach & mint', titleCn: '桃子薄荷', shortName: 'PE' },
|
||||||
|
{ name: 'peach-bellini', title: 'peach bellini', titleCn: '桃子贝里尼', shortName: 'PE' },
|
||||||
|
{ name: 'peach-berry', title: 'peach berry', titleCn: '桃子浆果', shortName: 'PE' },
|
||||||
|
{ name: 'peach-berry-ice', title: 'peach berry ice', titleCn: '桃子浆果冰', shortName: 'PE' },
|
||||||
|
{ name: 'peach-berry-lime-ice', title: 'peach berry lime ice', titleCn: '桃子浆果青柠冰', shortName: 'PE' },
|
||||||
|
{ name: 'peach-blossom', title: 'peach blossom', titleCn: '桃花', shortName: 'PE' },
|
||||||
|
{ name: 'peach-blue-raspberry', title: 'peach blue raspberry', titleCn: '桃子蓝莓覆盆子', shortName: 'PE' },
|
||||||
|
{ name: 'peach-blue-razz-ice', title: 'peach blue razz ice', titleCn: '桃子蓝覆盆子冰', shortName: 'PE' },
|
||||||
|
{ name: 'peach-blue-razz-mango-ice', title: 'peach blue razz mango ice', titleCn: '桃子蓝覆盆子芒果冰', shortName: 'PE' },
|
||||||
|
{ name: 'peach-blue-s', title: 'peach blue s', titleCn: '桃子蓝覆盆子 S', shortName: 'PE' },
|
||||||
|
{ name: 'peach-ice', title: 'peach ice', titleCn: '桃子冰', shortName: 'PE' },
|
||||||
|
{ name: 'peach-lychee-ice', title: 'peach lychee ice', titleCn: '桃荔枝冰', shortName: 'PE' },
|
||||||
|
{ name: 'peach-mango', title: 'peach mango', titleCn: '桃芒果', shortName: 'PE' },
|
||||||
|
{ name: 'peach-mango-ice', title: 'peach mango ice', titleCn: '桃芒果冰', shortName: 'PE' },
|
||||||
|
{ name: 'peach-mango-watermelon', title: 'peach mango watermelon', titleCn: '桃芒果西瓜', shortName: 'PE' },
|
||||||
|
{ name: 'peach-mango-watermelon-ice', title: 'peach mango watermelon ice', titleCn: '桃芒果西瓜冰', shortName: 'PE' },
|
||||||
|
{ name: 'peach-nectarine-ice', title: 'peach nectarine ice', titleCn: '桃子花蜜冰', shortName: 'PE' },
|
||||||
|
{ name: 'peach-passion-ice', title: 'peach passion ice', titleCn: '桃子桃冰', shortName: 'PE' },
|
||||||
|
{ name: 'peach-raspberry', title: 'peach raspberry', titleCn: '桃覆盆子', shortName: 'PE' },
|
||||||
|
{ name: 'peach-strawberry-ice', title: 'peach strawberry ice', titleCn: '桃草莓冰', shortName: 'PE' },
|
||||||
|
{ name: 'peach-strawberry-watermelon', title: 'peach strawberry watermelon', titleCn: '桃草莓西瓜', shortName: 'PE' },
|
||||||
|
{ name: 'peach-watermelon-ice', title: 'peach watermelon ice', titleCn: '桃西瓜冰', shortName: 'PE' },
|
||||||
|
{ name: 'peach-zing', title: 'peach zing', titleCn: '桃子滋味', shortName: 'PE' },
|
||||||
|
{ name: 'peaches-cream', title: 'peaches cream', titleCn: '桃子奶油', shortName: 'PE' },
|
||||||
|
{ name: 'peppered-mint', title: 'peppered mint', titleCn: '胡椒薄荷', shortName: 'PE' },
|
||||||
|
{ name: 'peppermint', title: 'peppermint', titleCn: '薄荷', shortName: 'PE' },
|
||||||
|
{ name: 'peppermint-salty', title: 'peppermint salty', titleCn: '薄荷咸味', shortName: 'PE' },
|
||||||
|
{ name: 'peppermint-storm', title: 'peppermint storm', titleCn: '薄荷风暴', shortName: 'PE' },
|
||||||
|
{ name: 'pina-blend', title: 'pina blend', titleCn: '菠萝混合', shortName: 'PI' },
|
||||||
|
{ name: 'pina-colada-ice', title: 'pina colada ice', titleCn: '菠萝椰子冰', shortName: 'PI' },
|
||||||
|
{ name: 'pineapple', title: 'pineapple', titleCn: '菠萝', shortName: 'PI' },
|
||||||
|
{ name: 'pineapple-blueberry-kiwi-ice', title: 'pineapple blueberry kiwi ice', titleCn: '菠萝蓝莓奇异果冰', shortName: 'PI' },
|
||||||
|
{ name: 'pineapple-citrus', title: 'pineapple citrus', titleCn: '菠萝柑橘', shortName: 'PI' },
|
||||||
|
{ name: 'pineapple-coconut', title: 'pineapple coconut', titleCn: '菠萝椰子', shortName: 'PI' },
|
||||||
|
{ name: 'pineapple-coconut-ice', title: 'pineapple coconut ice', titleCn: '菠萝椰子冰', shortName: 'PI' },
|
||||||
|
{ name: 'pineapple-ice', title: 'pineapple ice', titleCn: '菠萝冰', shortName: 'PI' },
|
||||||
|
{ name: 'pineapple-lemonade', title: 'pineapple lemonade', titleCn: '菠萝柠檬水', shortName: 'PI' },
|
||||||
|
{ name: 'pineapple-orange-cherry', title: 'pineapple orange cherry', titleCn: '菠萝橙樱桃', shortName: 'PI' },
|
||||||
|
{ name: 'pink-lemon', title: 'pink lemon', titleCn: '粉柠檬', shortName: 'PI' },
|
||||||
|
{ name: 'pink-lemon-ice', title: 'pink lemon ice', titleCn: '粉柠檬冰', shortName: 'PI' },
|
||||||
|
{ name: 'pink-lemonade', title: 'pink lemonade', titleCn: '粉红柠檬水', shortName: 'PI' },
|
||||||
|
{ name: 'pink-punch', title: 'pink punch', titleCn: '粉红拳', shortName: 'PI' },
|
||||||
|
{ name: 'polar-chill', title: 'polar chill', titleCn: '极地清凉', shortName: 'PO' },
|
||||||
|
{ name: 'polar-mint-max', title: 'polar mint max', titleCn: '极地薄荷', shortName: 'PO' },
|
||||||
|
{ name: 'pomegranate-ice', title: 'pomegranate ice', titleCn: '石榴冰', shortName: 'PO' },
|
||||||
|
{ name: 'poppin-strawkiwi', title: 'poppin strawkiwi', titleCn: '草莓猕猴', shortName: 'PO' },
|
||||||
|
{ name: 'prism-ice', title: 'prism ice', titleCn: '棱镜冰', shortName: 'PR' },
|
||||||
|
{ name: 'punch', title: 'punch', titleCn: '果汁', shortName: 'PU' },
|
||||||
|
{ name: 'punch-ice', title: 'punch ice', titleCn: '果汁冰', shortName: 'PU' },
|
||||||
|
{ name: 'pure-tobacco', title: 'pure tobacco', titleCn: '纯烟草', shortName: 'PU' },
|
||||||
|
{ name: 'puris', title: 'puris', titleCn: '纯味', shortName: 'PU' },
|
||||||
|
{ name: 'purple-grape', title: 'purple grape', titleCn: '紫葡萄', shortName: 'PU' },
|
||||||
|
{ name: 'quad-berry', title: 'quad berry', titleCn: '四重浆果', shortName: 'QU' },
|
||||||
|
{ name: 'queen-soko', title: 'queen soko', titleCn: '女王索科', shortName: 'QU' },
|
||||||
|
{ name: 'rad-razz-melon-iced', title: 'rad razz melon iced', titleCn: '疯狂覆盆子瓜冰', shortName: 'RA' },
|
||||||
|
{ name: 'ragin-razz-mango-iced', title: 'ragin razz mango iced', titleCn: '狂暴覆盆子芒果冰', shortName: 'RA' },
|
||||||
|
{ name: 'rainbow-candy', title: 'rainbow candy', titleCn: '彩虹糖', shortName: 'RA' },
|
||||||
|
{ name: 'raspberry-blast', title: 'raspberry blast', titleCn: '覆盆子爆炸', shortName: 'RA' },
|
||||||
|
{ name: 'raspberry-buzz-ice', title: 'raspberry buzz ice', titleCn: '覆盆子嗡嗡冰', shortName: 'RA' },
|
||||||
|
{ name: 'raspberry-dragon-fruit-ice', title: 'raspberry dragon fruit ice', titleCn: '覆盆子龙果冰', shortName: 'RA' },
|
||||||
|
{ name: 'raspberry-ice', title: 'raspberry ice', titleCn: '覆盆子冰', shortName: 'RA' },
|
||||||
|
{ name: 'raspberry-lemon', title: 'raspberry lemon', titleCn: '覆盆子柠檬', shortName: 'RA' },
|
||||||
|
{ name: 'raspberry-mango-ice', title: 'raspberry mango ice', titleCn: '覆盆子芒果冰', shortName: 'RA' },
|
||||||
|
{ name: 'raspberry-peach-mango-ice', title: 'raspberry peach mango ice', titleCn: '覆盆子桃芒果冰', shortName: 'RA' },
|
||||||
|
{ name: 'raspberry-pomegranate', title: 'raspberry pomegranate', titleCn: '覆盆子石榴', shortName: 'RA' },
|
||||||
|
{ name: 'raspberry-vanilla', title: 'raspberry vanilla', titleCn: '覆盆子香草', shortName: 'RA' },
|
||||||
|
{ name: 'raspberry-watermelon', title: 'raspberry watermelon', titleCn: '覆盆子西瓜', shortName: 'RA' },
|
||||||
|
{ name: 'raspberry-watermelon-ice', title: 'raspberry watermelon ice', titleCn: '覆盆子西瓜冰', shortName: 'RA' },
|
||||||
|
{ name: 'raspberry-zing', title: 'raspberry zing', titleCn: '覆盆子滋味', shortName: 'RA' },
|
||||||
|
{ name: 'razz-apple-ice', title: 'razz apple ice', titleCn: '覆盆子苹果冰', shortName: 'RA' },
|
||||||
|
{ name: 'razz-currant-ice', title: 'razz currant ice', titleCn: '红苹果冰', shortName: 'RA' },
|
||||||
|
{ name: 'red-apple-ice', title: 'red apple ice', titleCn: '红豆', shortName: 'RE' },
|
||||||
|
{ name: 'red-bean', title: 'red bean', titleCn: '红枣 ', shortName: 'RE' },
|
||||||
|
{ name: 'red-berry-cherry', title: 'red berry cherry', titleCn: '红浆果樱桃', shortName: 'RE' },
|
||||||
|
{ name: 'red-date-yg', title: 'red date yg', titleCn: '红枣 Y', shortName: 'RE' },
|
||||||
|
{ name: 'red-eye-espresso', title: 'red eye espresso', titleCn: '红眼浓缩咖啡', shortName: 'RE' },
|
||||||
|
{ name: 'red-fruits', title: 'red fruits', titleCn: '红色水果', shortName: 'RE' },
|
||||||
|
{ name: 'red-lightning', title: 'red lightning', titleCn: '红色闪电', shortName: 'RE' },
|
||||||
|
{ name: 'red-line', title: 'red line', titleCn: '红线', shortName: 'RE' },
|
||||||
|
{ name: 'red-line-(energy-drink)', title: 'red line (energy drink)', titleCn: '红线(能量饮料)', shortName: 'RE' },
|
||||||
|
{ name: 'red-magic', title: 'red magic', titleCn: '红魔', shortName: 'RE' },
|
||||||
|
{ name: 'rich-tobacco', title: 'rich tobacco', titleCn: '浓烈烟草', shortName: 'RI' },
|
||||||
|
{ name: 'root-beer', title: 'root beer', titleCn: '根啤', shortName: 'RO' },
|
||||||
|
{ name: 'rose-grape', title: 'rose grape', titleCn: '玫瑰葡萄', shortName: 'RO' },
|
||||||
|
{ name: 'rosemary', title: 'rosemary', titleCn: '迷迭香', shortName: 'RO' },
|
||||||
|
{ name: 'royal-violet', title: 'royal violet', titleCn: '皇家紫罗兰', shortName: 'RO' },
|
||||||
|
{ name: 'ruby-berry', title: 'ruby berry', titleCn: '红宝石浆果', shortName: 'RU' },
|
||||||
|
{ name: 's-apple-ice', title: 's apple ice', titleCn: 'S 苹果冰', shortName: 'SA' },
|
||||||
|
{ name: 's-watermelon-peach', title: 's watermelon peach', titleCn: 'S 西瓜桃', shortName: 'SW' },
|
||||||
|
{ name: 'saimiri', title: 'saimiri', titleCn: '卷尾猴', shortName: 'SA' },
|
||||||
|
{ name: 'sakura-grap', title: 'sakura grap', titleCn: '樱花葡萄', shortName: 'SA' },
|
||||||
|
{ name: 'sakura-grape', title: 'sakura grape', titleCn: '樱花葡萄', shortName: 'SA' },
|
||||||
|
{ name: 'salt', title: 'salt', titleCn: '盐', shortName: 'SA' },
|
||||||
|
{ name: 'salted-caramel', title: 'salted caramel', titleCn: '咸焦糖', shortName: 'SA' },
|
||||||
|
{ name: 'salty-liquorice', title: 'salty liquorice', titleCn: '咸甘草', shortName: 'SA' },
|
||||||
|
{ name: 'sanctuary', title: 'sanctuary', titleCn: '避风港', shortName: 'SA' },
|
||||||
|
{ name: 'savage-strawberry-watermelon-iced', title: 'savage strawberry watermelon iced', titleCn: '狂野草莓西瓜冰', shortName: 'SA' },
|
||||||
|
{ name: 'shoku', title: 'shoku', titleCn: 'Shoku', shortName: 'SH' },
|
||||||
|
{ name: 'sic-strawberry-iced', title: 'sic strawberry iced', titleCn: '意大利草莓冰', shortName: 'SI' },
|
||||||
|
{ name: 'simply-spearmint', title: 'simply spearmint', titleCn: '清爽留兰香', shortName: 'SI' },
|
||||||
|
{ name: 'skc', title: 'skc', titleCn: 'SKC', shortName: 'SK' },
|
||||||
|
{ name: 'skc(skittles-candy)', title: 'skc(skittles candy)', titleCn: 'SKC(彩虹糖)', shortName: 'SK' },
|
||||||
|
{ name: 'slammin-sts-(sour-snap)', title: 'slammin sts (sour snap)', titleCn: '热烈 STS(酸糖)', shortName: 'SL' },
|
||||||
|
{ name: 'slammin-sts-iced', title: 'slammin sts iced', titleCn: '热烈 STS 冰', shortName: 'SL' },
|
||||||
|
{ name: 'smooth', title: 'smooth', titleCn: '顺滑', shortName: 'SM' },
|
||||||
|
{ name: 'smooth-mint', title: 'smooth mint', titleCn: '顺滑薄荷', shortName: 'SM' },
|
||||||
|
{ name: 'smooth-strawberry', title: 'smooth strawberry', titleCn: '顺滑草莓', shortName: 'SM' },
|
||||||
|
{ name: 'smooth-tobacco', title: 'smooth tobacco', titleCn: '顺滑烟草', shortName: 'SM' },
|
||||||
|
{ name: 'snazzy-razz', title: 'snazzy razz', titleCn: '炫酷覆盆子', shortName: 'SN' },
|
||||||
|
{ name: 'snazzy-s-storm', title: 'snazzy s storm', titleCn: '炫酷风暴', shortName: 'SN' },
|
||||||
|
{ name: 'snazzy-strawberrry-citrus', title: 'snazzy strawberrry citrus', titleCn: '炫酷草莓柑橘', shortName: 'SN' },
|
||||||
|
{ name: 'snow-pear', title: 'snow pear', titleCn: '酸梨', shortName: 'SN' },
|
||||||
|
{ name: 'sour', title: 'sour', titleCn: '酸', shortName: 'SO' },
|
||||||
|
{ name: 'sour-apple', title: 'sour apple', titleCn: '酸苹果', shortName: 'SO' },
|
||||||
|
{ name: 'sour-blue-razz', title: 'sour blue razz', titleCn: '酸蓝覆盆子', shortName: 'SO' },
|
||||||
|
{ name: 'sour-cherry', title: 'sour cherry', titleCn: '酸樱桃', shortName: 'SO' },
|
||||||
|
{ name: 'sour-lime', title: 'sour lime', titleCn: '酸青柠', shortName: 'SO' },
|
||||||
|
{ name: 'sour-ruby', title: 'sour ruby', titleCn: '酸红宝石', shortName: 'SO' },
|
||||||
|
{ name: 'spearmint', title: 'spearmint', titleCn: '留兰香', shortName: 'SP' },
|
||||||
|
{ name: 'spearmint-blast-ice', title: 'spearmint blast ice', titleCn: '留兰香爆发冰', shortName: 'SP' },
|
||||||
|
{ name: 'star-coffee', title: 'star coffee', titleCn: '星辰咖啡', shortName: 'ST' },
|
||||||
|
{ name: 'straw-kiwi-melon-ice', title: 'straw kiwi melon ice', titleCn: '草莓奇异果瓜冰', shortName: 'ST' },
|
||||||
|
{ name: 'strawanna-ice', title: 'strawanna ice', titleCn: '草莓香蕉冰', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry', title: 'strawberry', titleCn: '草莓', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-&-watermelon', title: 'strawberry & watermelon', titleCn: '草莓西瓜', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-apple-grape', title: 'strawberry apple grape', titleCn: '草莓苹果葡萄', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-apricot-ice', title: 'strawberry apricot ice', titleCn: '草莓杏子冰', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-banana', title: 'strawberry banana', titleCn: '草莓香蕉', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-banana-ice', title: 'strawberry banana ice', titleCn: '草莓香蕉冰', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-banana-mango-ice', title: 'strawberry banana mango ice', titleCn: '草莓香蕉芒果冰', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-berry', title: 'strawberry berry', titleCn: '草莓浆果', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-burst-ice', title: 'strawberry burst ice', titleCn: '草莓爆发冰', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-cherry-lemon', title: 'strawberry cherry lemon', titleCn: '草莓樱桃柠檬', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-dragon-fruit', title: 'strawberry dragon fruit', titleCn: '草莓龙果', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-ft', title: 'strawberry ft', titleCn: '草莓 FT', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-grapefruit', title: 'strawberry grapefruit', titleCn: '草莓葡萄柚', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-ice', title: 'strawberry ice', titleCn: '草莓冰', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-jasmine-t', title: 'strawberry jasmine t', titleCn: '草莓茉莉茶', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-jasmine-tea', title: 'strawberry jasmine tea', titleCn: '草莓茉莉茶', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-kiwi', title: 'strawberry kiwi', titleCn: '草莓奇异果', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-kiwi-(solid)', title: 'strawberry kiwi (solid)', titleCn: '草莓奇异果(固体)', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-kiwi-banana-ice', title: 'strawberry kiwi banana ice', titleCn: '草莓奇异果香蕉冰', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-kiwi-guava-ice', title: 'strawberry kiwi guava ice', titleCn: '草莓奇异果番石榴冰', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-kiwi-ice', title: 'strawberry kiwi ice', titleCn: '草莓奇异果冰', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-lemon', title: 'strawberry lemon', titleCn: '草莓柠檬', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-lime-ice', title: 'strawberry lime ice', titleCn: '草莓青柠冰', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-lychee-ice', title: 'strawberry lychee ice', titleCn: '草莓荔枝冰', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-mango-ice', title: 'strawberry mango ice', titleCn: '草莓芒果冰', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-mint', title: 'strawberry mint', titleCn: '草莓薄荷', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-orange', title: 'strawberry orange', titleCn: '草莓橙', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-peach-mint', title: 'strawberry peach mint', titleCn: '草莓桃薄荷', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-raspberry', title: 'strawberry raspberry', titleCn: '草莓覆盆子', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-twist-ice', title: 'strawberry twist ice', titleCn: '草莓扭转冰', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-watermelon', title: 'strawberry watermelon', titleCn: '草莓西瓜', shortName: 'ST' },
|
||||||
|
{ name: 'strawberry-watermelon-ice', title: 'strawberry watermelon ice', titleCn: '草莓西瓜冰', shortName: 'ST' },
|
||||||
|
{ name: 'strawmelon-peach', title: 'strawmelon peach', titleCn: '草莓桃', shortName: 'ST' },
|
||||||
|
{ name: 'strawmelon-peach-(solid)', title: 'strawmelon peach (solid)', titleCn: '草莓桃(固体)', shortName: 'ST' },
|
||||||
|
{ name: 'strawnana-orange', title: 'strawnana orange', titleCn: '草莓香蕉橙', shortName: 'ST' },
|
||||||
|
{ name: 'summer-grape', title: 'summer grape', titleCn: '夏日葡萄', shortName: 'SU' },
|
||||||
|
{ name: 'summer-grape-(thermal)', title: 'summer grape (thermal)', titleCn: '夏日葡萄(热感)', shortName: 'SU' },
|
||||||
|
{ name: 'super-sour-blueberry-iced', title: 'super sour blueberry iced', titleCn: '超级酸蓝莓冰', shortName: 'SU' },
|
||||||
|
{ name: 'super-spearmint', title: 'super spearmint', titleCn: '超级留兰香', shortName: 'SU' },
|
||||||
|
{ name: 'super-spearmint-iced', title: 'super spearmint iced', titleCn: '超级留兰香冰', shortName: 'SU' },
|
||||||
|
{ name: 'sweet-blackcurrant', title: 'sweet blackcurrant', titleCn: '甜黑加仑', shortName: 'SW' },
|
||||||
|
{ name: 'sweet-mint', title: 'sweet mint', titleCn: '甜薄荷', shortName: 'SW' },
|
||||||
|
{ name: 't-berries', title: 't berries', titleCn: 'T 浆果', shortName: 'TB' },
|
||||||
|
{ name: 'taste-of-gods-x', title: 'taste of gods x', titleCn: '神之味 X', shortName: 'TA' },
|
||||||
|
{ name: 'the-prophet', title: 'the prophet', titleCn: '先知', shortName: 'TH' },
|
||||||
|
{ name: 'tiki-punch-ice', title: 'tiki punch ice', titleCn: 'Tiki 冲击冰', shortName: 'TI' },
|
||||||
|
{ name: 'triple-berry', title: 'triple berry', titleCn: '三重浆果', shortName: 'TR' },
|
||||||
|
{ name: 'triple-berry-ice', title: 'triple berry ice', titleCn: '三重浆果冰', shortName: 'TR' },
|
||||||
|
{ name: 'triple-mango', title: 'triple mango', titleCn: '三重芒果', shortName: 'TR' },
|
||||||
|
{ name: "trippin'-triple-berry", title: "trippin' triple berry", titleCn: '三重浆果旋风', shortName: 'TR' },
|
||||||
|
{ name: 'tropical', title: 'tropical', titleCn: '热带', shortName: 'TR' },
|
||||||
|
{ name: 'tropical-burst-ice', title: 'tropical burst ice', titleCn: '热带爆发冰', shortName: 'TR' },
|
||||||
|
{ name: 'tropical-mango', title: 'tropical mango', titleCn: '热带芒果', shortName: 'TR' },
|
||||||
|
{ name: 'tropical-mango-ice', title: 'tropical mango ice', titleCn: '热带芒果冰', shortName: 'TR' },
|
||||||
|
{ name: 'tropical-orang-ice', title: 'tropical orang ice', titleCn: '热带橙冰', shortName: 'TR' },
|
||||||
|
{ name: 'tropical-prism-blast', title: 'tropical prism blast', titleCn: '热带棱镜爆炸', shortName: 'TR' },
|
||||||
|
{ name: 'tropical-splash', title: 'tropical splash', titleCn: '热带飞溅', shortName: 'TR' },
|
||||||
|
{ name: 'tropical-splash-(solid)', title: 'tropical splash (solid)', titleCn: '热带飞溅(固体)', shortName: 'TR' },
|
||||||
|
{ name: 'tropical-storm-ice', title: 'tropical storm ice', titleCn: '热带风暴冰', shortName: 'TR' },
|
||||||
|
{ name: 'tropical-summer', title: 'tropical summer', titleCn: '热带夏日', shortName: 'TR' },
|
||||||
|
{ name: 'tropika', title: 'tropika', titleCn: '热带果', shortName: 'TR' },
|
||||||
|
{ name: 'twisted-apple', title: 'twisted apple', titleCn: '扭苹果', shortName: 'TW' },
|
||||||
|
{ name: 'twisted-pineapple', title: 'twisted pineapple', titleCn: '扭菠萝', shortName: 'TW' },
|
||||||
|
{ name: 'ultra-fresh-mint', title: 'ultra fresh mint', titleCn: '极新鲜薄荷', shortName: 'UL' },
|
||||||
|
{ name: 'vanilla', title: 'vanilla', titleCn: '香草', shortName: 'VA' },
|
||||||
|
{ name: 'vanilla-classic', title: 'vanilla classic', titleCn: '香草经典', shortName: 'VA' },
|
||||||
|
{ name: 'vanilla-classic-cola', title: 'vanilla classic cola', titleCn: '香草经典可乐', shortName: 'VA' },
|
||||||
|
{ name: 'vanilla-classic-red', title: 'vanilla classic red', titleCn: '香草经典红', shortName: 'VA' },
|
||||||
|
{ name: 'vanilla-tobacco', title: 'vanilla tobacco', titleCn: '香草烟草', shortName: 'VA' },
|
||||||
|
{ name: 'vb-arctic-berry', title: 'vb arctic berry', titleCn: 'VB 北极浆果', shortName: 'VB' },
|
||||||
|
{ name: 'vb-arctic-mint', title: 'vb arctic mint', titleCn: 'VB 北极薄荷', shortName: 'VB' },
|
||||||
|
{ name: 'vb-spearmint-salty', title: 'vb spearmint salty', titleCn: 'VB 留兰香咸味', shortName: 'VB' },
|
||||||
|
{ name: 'vc-delight', title: 'vc delight', titleCn: 'VC 美味', shortName: 'VC' },
|
||||||
|
{ name: 'vintage', title: 'vintage', titleCn: '复古', shortName: 'VI' },
|
||||||
|
{ name: 'violet-licorice', title: 'violet licorice', titleCn: '紫罗兰甘草', shortName: 'VI' },
|
||||||
|
{ name: 'watermelon', title: 'watermelon', titleCn: '西瓜', shortName: 'WA' },
|
||||||
|
{ name: 'watermelon-bbg', title: 'watermelon bbg', titleCn: '西瓜 BBG', shortName: 'WA' },
|
||||||
|
{ name: 'watermelon-bubble-gum', title: 'watermelon bubble gum', titleCn: '西瓜泡泡糖', shortName: 'WA' },
|
||||||
|
{ name: 'watermelon-cantaloupe-honeydew-ice', title: 'watermelon cantaloupe honeydew ice', titleCn: '西瓜香瓜蜜瓜冰', shortName: 'WA' },
|
||||||
|
{ name: 'watermelon-g', title: 'watermelon g', titleCn: '西瓜 G', shortName: 'WA' },
|
||||||
|
{ name: 'watermelon-ice', title: 'watermelon ice', titleCn: '西瓜冰', shortName: 'WA' },
|
||||||
|
{ name: 'watermelon-ice-(solid)', title: 'watermelon ice (solid)', titleCn: '西瓜冰(固体)', shortName: 'WA' },
|
||||||
|
{ name: 'watermelon-lime-ice', title: 'watermelon lime ice', titleCn: '西瓜青柠冰', shortName: 'WA' },
|
||||||
|
{ name: 'watermelon-mango-tango', title: 'watermelon mango tango', titleCn: '西瓜芒果探戈', shortName: 'WA' },
|
||||||
|
{ name: 'watermelona-cg', title: 'watermelona cg', titleCn: '西瓜 CG', shortName: 'WA' },
|
||||||
|
{ name: 'weekend-watermelon', title: 'weekend watermelon', titleCn: '周末西瓜', shortName: 'WE' },
|
||||||
|
{ name: 'weekend-watermelon-iced', title: 'weekend watermelon iced', titleCn: '周末西瓜冰', shortName: 'WE' },
|
||||||
|
{ name: 'white-grape', title: 'white grape', titleCn: '白葡萄', shortName: 'WH' },
|
||||||
|
{ name: 'white-grape-ice', title: 'white grape ice', titleCn: '白葡萄冰', shortName: 'WH' },
|
||||||
|
{ name: 'white-ice', title: 'white ice', titleCn: '白冰', shortName: 'WH' },
|
||||||
|
{ name: 'white-peach-ice', title: 'white peach ice', titleCn: '白桃冰', shortName: 'WH' },
|
||||||
|
{ name: 'white-peach-splash', title: 'white peach splash', titleCn: '白桃飞溅', shortName: 'WH' },
|
||||||
|
{ name: 'white-peach-yaklt', title: 'white peach yaklt', titleCn: '白桃益菌乳', shortName: 'WH' },
|
||||||
|
{ name: 'wicked-white-peach', title: 'wicked white peach', titleCn: '邪恶白桃', shortName: 'WI' },
|
||||||
|
{ name: 'wild-blue-raspberry', title: 'wild blue raspberry', titleCn: '野生蓝覆盆子', shortName: 'WI' },
|
||||||
|
{ name: 'wild-blueberry-ice', title: 'wild blueberry ice', titleCn: '野生蓝莓冰', shortName: 'WI' },
|
||||||
|
{ name: 'wild-cherry-cola', title: 'wild cherry cola', titleCn: '野樱桃可乐', shortName: 'WI' },
|
||||||
|
{ name: 'wild-dragonfruit-lychee', title: 'wild dragonfruit lychee', titleCn: '野生龙果荔枝', shortName: 'WI' },
|
||||||
|
{ name: 'wild-strawberry-banana', title: 'wild strawberry banana', titleCn: '野生草莓香蕉', shortName: 'WI' },
|
||||||
|
{ name: 'wild-strawberry-ice', title: 'wild strawberry ice', titleCn: '野生草莓冰', shortName: 'WI' },
|
||||||
|
{ name: 'wild-strawberry-watermelon', title: 'wild strawberry watermelon', titleCn: '野生草莓西瓜', shortName: 'WI' },
|
||||||
|
{ name: 'wild-white-grape', title: 'wild white grape', titleCn: '野生白葡萄', shortName: 'WI' },
|
||||||
|
{ name: 'wild-white-grape-ice', title: 'wild white grape ice', titleCn: '野生白葡萄冰', shortName: 'WI' },
|
||||||
|
{ name: 'wild-white-grape-iced', title: 'wild white grape iced', titleCn: '野生白葡萄冰(冷饮)', shortName: 'WI' },
|
||||||
|
{ name: 'winter-berry-ice', title: 'winter berry ice', titleCn: '冬季浆果冰', shortName: 'WI' },
|
||||||
|
{ name: 'winter-green', title: 'winter green', titleCn: '冬青', shortName: 'WI' },
|
||||||
|
{ name: 'wintergreen', title: 'wintergreen', titleCn: '冬青薄荷', shortName: 'WI' },
|
||||||
|
{ name: 'wintery-watermelon', title: 'wintery watermelon', titleCn: '冬季西瓜', shortName: 'WI' },
|
||||||
|
{ name: 'woke-watermelon-tropica-iced', title: 'woke watermelon tropica iced', titleCn: '觉醒西瓜热带冰', shortName: 'WO' },
|
||||||
|
{ name: 'wonder', title: 'wonder', titleCn: '奇迹', shortName: 'WO' },
|
||||||
|
{ name: 'x-freeze', title: 'x freeze', titleCn: 'X 冰冻', shortName: 'XF' },
|
||||||
|
{ name: 'zen', title: 'zen', titleCn: '禅', shortName: 'ZE' },
|
||||||
|
{ name: 'zest-flame', title: 'zest flame', titleCn: '清新火焰', shortName: 'ZE' },
|
||||||
|
{ name: 'zesty-elderflower', title: 'zesty elderflower', titleCn: '活力接骨木花', shortName: 'ZE' },
|
||||||
|
{ name: 'zingy-eucalyptus', title: 'zingy eucalyptus', titleCn: '清爽桉树', shortName: 'ZI' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Total flavors: 655
|
||||||
|
|
||||||
|
// 强度数据
|
||||||
|
const strengthsData = [
|
||||||
|
{ name: '1.5mg', title: '1.5mg', titleCn: '1.5毫克', shortName: '1.5' },
|
||||||
|
{ name: '2mg', title: '2mg', titleCn: '2毫克', shortName: '2MG' },
|
||||||
|
{ name: '3mg', title: '3mg', titleCn: '3毫克', shortName: '3MG' },
|
||||||
|
{ name: '3.5mg', title: '3.5mg', titleCn: '3.5毫克', shortName: '3.5' },
|
||||||
|
{ name: '4mg', title: '4mg', titleCn: '4毫克', shortName: '4MG' },
|
||||||
|
{ name: '5,2 mg', title: '5,2 mg', titleCn: '5,2毫克', shortName: '5,2' },
|
||||||
|
{ name: '5.6mg', title: '5.6mg', titleCn: '5.6毫克', shortName: '5.6' },
|
||||||
|
{ name: '6mg', title: '6mg', titleCn: '6毫克', shortName: '6MG' },
|
||||||
|
{ name: '6.5mg', title: '6.5mg', titleCn: '6.5毫克', shortName: '6.5' },
|
||||||
|
{ name: '8mg', title: '8mg', titleCn: '8毫克', shortName: '8MG' },
|
||||||
|
{ name: '9mg', title: '9mg', titleCn: '9毫克', shortName: '9MG' },
|
||||||
|
{ name: '10mg', title: '10mg', titleCn: '10毫克', shortName: '10M' },
|
||||||
|
{ name: '10,4 mg', title: '10,4 mg', titleCn: '10,4 毫克', shortName: '10,' },
|
||||||
|
{ name: '10,9mg', title: '10,9mg', titleCn: '10,9毫克', shortName: '10,' },
|
||||||
|
{ name: '11mg', title: '11mg', titleCn: '11毫克', shortName: '11M' },
|
||||||
|
{ name: '12mg', title: '12mg', titleCn: '12毫克', shortName: '12M' },
|
||||||
|
{ name: '12.5mg', title: '12.5mg', titleCn: '12.5毫克', shortName: '12.' },
|
||||||
|
{ name: '13.5mg', title: '13.5mg', titleCn: '13.5毫克', shortName: '13.' },
|
||||||
|
{ name: '14mg', title: '14mg', titleCn: '14毫克', shortName: '14M' },
|
||||||
|
{ name: '15mg', title: '15mg', titleCn: '15毫克', shortName: '15M' },
|
||||||
|
{ name: '16mg', title: '16mg', titleCn: '16毫克', shortName: '16M' },
|
||||||
|
{ name: '16.5mg', title: '16.5mg', titleCn: '16.5毫克', shortName: '16.' },
|
||||||
|
{ name: '16.6mg', title: '16.6mg', titleCn: '16.6毫克', shortName: '16.' },
|
||||||
|
{ name: '17mg', title: '17mg', titleCn: '17毫克', shortName: '17M' },
|
||||||
|
{ name: '18mg', title: '18mg', titleCn: '18毫克', shortName: '18M' },
|
||||||
|
{ name: '20mg', title: '20mg', titleCn: '20毫克', shortName: '20M' },
|
||||||
|
{ name: '30mg', title: '30mg', titleCn: '30毫克', shortName: '30M' },
|
||||||
|
{ name: 'extra-strong', title: 'extra strong', titleCn: '超强', shortName: 'EXT' },
|
||||||
|
{ name: 'low', title: 'low', titleCn: '低', shortName: 'LOW' },
|
||||||
|
{ name: 'max', title: 'max', titleCn: '最大', shortName: 'MAX' },
|
||||||
|
{ name: 'medium', title: 'medium', titleCn: '中等', shortName: 'MED' },
|
||||||
|
{ name: 'normal', title: 'normal', titleCn: '普通', shortName: 'NOR' },
|
||||||
|
{ name: 'strong', title: 'strong', titleCn: '强', shortName: 'STR' },
|
||||||
|
{ name: 'super-strong', title: 'super strong', titleCn: '特强', shortName: 'SUP' },
|
||||||
|
{ name: 'ultra-strong', title: 'ultra strong', titleCn: '极强', shortName: 'ULT' },
|
||||||
|
{ name: 'xx-strong', title: 'xx strong', titleCn: '超超强', shortName: 'XXS' },
|
||||||
|
{ name: 'x-intense', title: 'x intense', titleCn: '强', shortName: 'XIN' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Total strengths: 37
|
||||||
|
|
||||||
|
// 品牌数据
|
||||||
|
const brandsData = [
|
||||||
|
{ name: 'yoone', title: 'yoone', titleCn: '', shortName: 'YO' },
|
||||||
|
{ name: 'zyn', title: 'zyn', titleCn: '', shortName: 'ZY' },
|
||||||
|
{ name: 'on!', title: 'on!', titleCn: '', shortName: 'ON' },
|
||||||
|
{ name: 'alibarbar', title: 'alibarbar', titleCn: '', shortName: 'AL' },
|
||||||
|
{ name: 'iget-pro', title: 'iget pro', titleCn: '', shortName: 'IG' },
|
||||||
|
{ name: 'jux', title: 'jux', titleCn: '', shortName: 'JU' },
|
||||||
|
{ name: 'velo', title: 'velo', titleCn: '', shortName: 'VE' },
|
||||||
|
{ name: 'white-fox', title: 'white fox', titleCn: '', shortName: 'WH' },
|
||||||
|
{ name: 'zolt', title: 'zolt', titleCn: '', shortName: 'ZO' },
|
||||||
|
{ name: '77', title: '77', titleCn: '', shortName: '77' },
|
||||||
|
{ name: 'xqs', title: 'xqs', titleCn: '', shortName: 'XQ' },
|
||||||
|
{ name: 'zex', title: 'zex', titleCn: '', shortName: 'ZE' },
|
||||||
|
{ name: 'zonnic', title: 'zonnic', titleCn: '', shortName: 'ZO' },
|
||||||
|
{ name: 'lucy', title: 'Lucy', titleCn: '', shortName: 'LU' },
|
||||||
|
{ name: 'egp', title: 'EGP', titleCn: '', shortName: 'EG' },
|
||||||
|
{ name: 'bridge', title: 'Bridge', titleCn: '', shortName: 'BR' },
|
||||||
|
{ name: 'sesh', title: 'Sesh', titleCn: '', shortName: 'SE' },
|
||||||
|
{ name: 'pablo', title: 'Pablo', titleCn: '', shortName: 'PA' },
|
||||||
|
{ name: 'elfbar', title: 'elfbar', titleCn: '', shortName: 'EL' },
|
||||||
|
{ name: 'chacha', title: 'chacha', titleCn: '', shortName: 'CH' },
|
||||||
|
{ name: 'yoone-wave', title: 'yoone wave', titleCn: '', shortName: 'YO' },
|
||||||
|
{ name: 'yoone-e-liquid', title: 'yoone e-liquid', titleCn: '', shortName: 'YO' },
|
||||||
|
{ name: 'geek-bar', title: 'geek bar', titleCn: '', shortName: 'GE' },
|
||||||
|
{ name: 'iget-bar', title: 'iget bar', titleCn: '', shortName: 'IG' },
|
||||||
|
{ name: 'twelve-monkeys', title: 'twelve monkeys', titleCn: '', shortName: 'TW' },
|
||||||
|
{ name: 'z-pods', title: 'z pods', titleCn: '', shortName: 'ZP' },
|
||||||
|
{ name: 'yoone-y-pods', title: 'yoone y-pods', titleCn: '', shortName: 'YO' },
|
||||||
|
{ name: 'allo-e-liquid', title: 'allo e-liquid', titleCn: '', shortName: 'AL' },
|
||||||
|
{ name: 'allo-ultra', title: 'allo ultra', titleCn: '', shortName: 'AL' },
|
||||||
|
{ name: 'base-x', title: 'base x', titleCn: '', shortName: 'BA' },
|
||||||
|
{ name: 'breeze-pro', title: 'breeze pro', titleCn: '', shortName: 'BR' },
|
||||||
|
{ name: 'deu', title: 'deu', titleCn: '', shortName: 'DE' },
|
||||||
|
{ name: 'evo', title: 'evo', titleCn: '', shortName: 'EV' },
|
||||||
|
{ name: 'elf-bar', title: 'elf bar', titleCn: '', shortName: 'EL' },
|
||||||
|
{ name: 'feed', title: 'feed', titleCn: '', shortName: 'FE' },
|
||||||
|
{ name: 'flavour-beast', title: 'flavour beast', titleCn: '', shortName: 'FL' },
|
||||||
|
{ name: 'fog-formulas', title: 'fog formulas', titleCn: '', shortName: 'FO' },
|
||||||
|
{ name: 'fruitii', title: 'fruitii', titleCn: '', shortName: 'FR' },
|
||||||
|
{ name: 'gcore', title: 'gcore', titleCn: '', shortName: 'GC' },
|
||||||
|
{ name: 'gr1nds', title: 'gr1nds', titleCn: '', shortName: 'GR' },
|
||||||
|
{ name: 'hqd', title: 'hqd', titleCn: '', shortName: 'HQ' },
|
||||||
|
{ name: 'illusions', title: 'illusions', titleCn: '', shortName: 'IL' },
|
||||||
|
{ name: 'kraze', title: 'kraze', titleCn: '', shortName: 'KR' },
|
||||||
|
{ name: 'level-x', title: 'level x', titleCn: '', shortName: 'LE' },
|
||||||
|
{ name: 'lfgo-energy', title: 'lfgo energy', titleCn: '', shortName: 'LF' },
|
||||||
|
{ name: 'lost-mary', title: 'lost mary', titleCn: '', shortName: 'LO' },
|
||||||
|
{ name: 'mr-fog', title: 'mr fog', titleCn: '', shortName: 'MR' },
|
||||||
|
{ name: 'nicorette', title: 'nicorette', titleCn: '', shortName: 'NI' },
|
||||||
|
{ name: 'oxbar', title: 'oxbar', titleCn: '', shortName: 'OX' },
|
||||||
|
{ name: 'rabeats', title: 'rabeats', titleCn: '', shortName: 'RA' },
|
||||||
|
{ name: 'yoone-vapengin', title: 'yoone vapengin', titleCn: '', shortName: 'YO' },
|
||||||
|
{ name: 'sesh', title: 'sesh', titleCn: '', shortName: 'SE' },
|
||||||
|
{ name: 'spin', title: 'spin', titleCn: '', shortName: 'SP' },
|
||||||
|
{ name: 'stlth', title: 'stlth', titleCn: '', shortName: 'ST' },
|
||||||
|
{ name: 'tornado', title: 'tornado', titleCn: '', shortName: 'TO' },
|
||||||
|
{ name: 'uwell', title: 'uwell', titleCn: '', shortName: 'UW' },
|
||||||
|
{ name: 'vanza', title: 'vanza', titleCn: '', shortName: 'VA' },
|
||||||
|
{ name: 'vapgo', title: 'vapgo', titleCn: '', shortName: 'VA' },
|
||||||
|
{ name: 'vase', title: 'vase', titleCn: '', shortName: 'VA' },
|
||||||
|
{ name: 'vice-boost', title: 'vice boost', titleCn: '', shortName: 'VI' },
|
||||||
|
{ name: 'vozol-star', title: 'vozol star', titleCn: '', shortName: 'VO' },
|
||||||
|
{ name: 'zpods', title: 'zpods', titleCn: '', shortName: 'ZP' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Total brands: 62
|
||||||
|
|
|
||||||
|
|
@ -23,25 +23,44 @@ export default class TemplateSeeder implements Seeder {
|
||||||
const templates = [
|
const templates = [
|
||||||
{
|
{
|
||||||
name: 'product.sku',
|
name: 'product.sku',
|
||||||
value: '<%= it.brand %>-<%=it.category%>-<%= it.flavor %>-<%= it.strength %>-<%= it.humidity %>',
|
value: "<%= [it.category.shortName].concat(it.attributes.map(a => a.shortName)).join('-') %>",
|
||||||
description: '产品SKU模板',
|
description: '产品SKU模板',
|
||||||
testData: JSON.stringify({
|
testData: JSON.stringify({
|
||||||
brand: 'Brand',
|
category: {
|
||||||
category: 'Category',
|
shortName: 'CAT',
|
||||||
flavor: 'Flavor',
|
},
|
||||||
strength: '10mg',
|
attributes: [
|
||||||
humidity: 'Dry',
|
{ shortName: 'BR' },
|
||||||
|
{ shortName: 'FL' },
|
||||||
|
{ shortName: '10MG' },
|
||||||
|
{ shortName: 'DRY' },
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'product.title',
|
name: 'product.title',
|
||||||
value: '<%= it.brand %> <%= it.flavor %> <%= it.strength %> <%= it.humidity %>',
|
value: "<%= it.attributes.map(a => a.title).join(' ') %>",
|
||||||
description: '产品标题模板',
|
description: '产品标题模板',
|
||||||
testData: JSON.stringify({
|
testData: JSON.stringify({
|
||||||
brand: 'Brand',
|
attributes: [
|
||||||
flavor: 'Flavor',
|
{ title: 'Brand' },
|
||||||
strength: '10mg',
|
{ title: 'Flavor' },
|
||||||
humidity: 'Dry',
|
{ title: '10mg' },
|
||||||
|
{ title: 'Dry' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'site.product.sku',
|
||||||
|
value: '<%= it.site.skuPrefix %><%= it.product.sku %>',
|
||||||
|
description: '站点产品SKU模板',
|
||||||
|
testData: JSON.stringify({
|
||||||
|
site: {
|
||||||
|
skuPrefix: 'SITE-',
|
||||||
|
},
|
||||||
|
product: {
|
||||||
|
sku: 'PRODUCT-SKU-001',
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,188 @@
|
||||||
|
import { ApiProperty } from '@midwayjs/swagger';
|
||||||
|
import { Rule, RuleType } from '@midwayjs/validate';
|
||||||
|
|
||||||
|
export class UnifiedPaginationDTO<T> {
|
||||||
|
// 分页DTO用于承载统一分页信息与列表数据
|
||||||
|
@ApiProperty({ description: '列表数据' })
|
||||||
|
items: T[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '总数', example: 100 })
|
||||||
|
total: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '当前页', example: 1 })
|
||||||
|
page: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '每页数量', example: 20 })
|
||||||
|
per_page: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '总页数', example: 5 })
|
||||||
|
totalPages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class UnifiedSearchParamsDTO<Where=Record<string, any>> {
|
||||||
|
// 统一查询参数DTO用于承载分页与筛选与排序参数
|
||||||
|
@ApiProperty({ description: '页码', example: 1, required: false })
|
||||||
|
page?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '每页数量', example: 20, required: false })
|
||||||
|
per_page?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '查询时间范围开始', example: '2023-01-01T00:00:00Z', required: false })
|
||||||
|
after?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '查询时间范围结束', example: '2023-01-01T23:59:59Z', required: false })
|
||||||
|
before?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '搜索关键词', required: false })
|
||||||
|
search?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '过滤条件对象', type: 'object', required: false })
|
||||||
|
where?: Where;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '排序对象,例如 { "sku": "desc" }',
|
||||||
|
type: 'object',
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
orderBy?: Record<string, 'asc' | 'desc'> | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量操作错误项
|
||||||
|
*/
|
||||||
|
export interface BatchErrorItem {
|
||||||
|
// 错误项标识(可以是ID、邮箱等)
|
||||||
|
identifier: string;
|
||||||
|
// 错误信息
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量操作结果基础接口
|
||||||
|
*/
|
||||||
|
export interface BatchOperationResult {
|
||||||
|
// 总处理数量
|
||||||
|
total: number;
|
||||||
|
// 成功处理数量
|
||||||
|
processed: number;
|
||||||
|
// 创建数量
|
||||||
|
created?: number;
|
||||||
|
// 更新数量
|
||||||
|
updated?: number;
|
||||||
|
// 删除数量
|
||||||
|
deleted?: number;
|
||||||
|
// 跳过的数量(如数据已存在或无需处理)
|
||||||
|
skipped?: number;
|
||||||
|
// 错误列表
|
||||||
|
errors: BatchErrorItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步操作结果接口
|
||||||
|
*/
|
||||||
|
export interface SyncOperationResult extends BatchOperationResult {
|
||||||
|
// 同步成功数量
|
||||||
|
synced: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量操作错误项DTO
|
||||||
|
*/
|
||||||
|
export class BatchErrorItemDTO {
|
||||||
|
@ApiProperty({ description: '错误项标识(如ID、邮箱等)', type: String })
|
||||||
|
@Rule(RuleType.string().required())
|
||||||
|
identifier: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '错误信息', type: String })
|
||||||
|
@Rule(RuleType.string().required())
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量操作结果基础DTO
|
||||||
|
*/
|
||||||
|
export class BatchOperationResultDTO {
|
||||||
|
@ApiProperty({ description: '总处理数量', type: Number })
|
||||||
|
total: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '成功处理数量', type: Number })
|
||||||
|
processed: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '创建数量', type: Number, required: false })
|
||||||
|
created?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '更新数量', type: Number, required: false })
|
||||||
|
updated?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '删除数量', type: Number, required: false })
|
||||||
|
deleted?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '跳过的数量', type: Number, required: false })
|
||||||
|
skipped?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '错误列表', type: [BatchErrorItemDTO] })
|
||||||
|
errors: BatchErrorItemDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步操作结果DTO
|
||||||
|
*/
|
||||||
|
export class SyncOperationResultDTO extends BatchOperationResultDTO {
|
||||||
|
@ApiProperty({ description: '同步成功数量', type: Number })
|
||||||
|
synced: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步参数DTO
|
||||||
|
*/
|
||||||
|
export class SyncParamsDTO {
|
||||||
|
@ApiProperty({ description: '页码', type: Number, required: false, default: 1 })
|
||||||
|
@Rule(RuleType.number().integer().min(1).optional())
|
||||||
|
page?: number = 1;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '每页数量', type: Number, required: false, default: 100 })
|
||||||
|
@Rule(RuleType.number().integer().min(1).max(1000).optional())
|
||||||
|
pageSize?: number = 100;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '开始时间', type: String, required: false })
|
||||||
|
@Rule(RuleType.string().optional())
|
||||||
|
startDate?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '结束时间', type: String, required: false })
|
||||||
|
@Rule(RuleType.string().optional())
|
||||||
|
endDate?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '强制同步(忽略缓存)', type: Boolean, required: false, default: false })
|
||||||
|
@Rule(RuleType.boolean().optional())
|
||||||
|
force?: boolean = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量查询DTO
|
||||||
|
*/
|
||||||
|
export class BatchQueryDTO {
|
||||||
|
@ApiProperty({ description: 'ID列表', type: [String, Number] })
|
||||||
|
@Rule(RuleType.array().items(RuleType.alternatives().try(RuleType.string(), RuleType.number())).required())
|
||||||
|
ids: Array<string | number>;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '包含关联数据', type: Boolean, required: false, default: false })
|
||||||
|
@Rule(RuleType.boolean().optional())
|
||||||
|
includeRelations?: boolean = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量操作结果类(泛型支持)
|
||||||
|
*/
|
||||||
|
export class BatchOperationResultDTOGeneric<T> extends BatchOperationResultDTO {
|
||||||
|
@ApiProperty({ description: '操作成功的数据列表', type: Array })
|
||||||
|
data?: T[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步操作结果类(泛型支持)
|
||||||
|
*/
|
||||||
|
export class SyncOperationResultDTOGeneric<T> extends SyncOperationResultDTO {
|
||||||
|
@ApiProperty({ description: '同步成功的数据列表', type: Array })
|
||||||
|
data?: T[];
|
||||||
|
}
|
||||||
|
|
@ -1,70 +1,364 @@
|
||||||
import { ApiProperty } from '@midwayjs/swagger';
|
import { ApiProperty } from '@midwayjs/swagger';
|
||||||
|
import { UnifiedSearchParamsDTO } from './api.dto';
|
||||||
|
import { Customer } from '../entity/customer.entity';
|
||||||
|
|
||||||
export class QueryCustomerListDTO {
|
// 客户基本信息DTO(用于响应)
|
||||||
@ApiProperty()
|
export class CustomerDTO extends Customer{
|
||||||
current: string;
|
@ApiProperty({ description: '客户ID' })
|
||||||
|
id: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty({ description: '站点ID', required: false })
|
||||||
pageSize: string;
|
site_id: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty({ description: '原始ID', required: false })
|
||||||
|
origin_id: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点创建时间', required: false })
|
||||||
|
site_created_at: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点更新时间', required: false })
|
||||||
|
site_updated_at: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '邮箱' })
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty({ description: '名字', required: false })
|
||||||
tags: string;
|
first_name: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty({ description: '姓氏', required: false })
|
||||||
sorterKey: string;
|
last_name: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty({ description: '全名', required: false })
|
||||||
sorterValue: string;
|
fullname: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty({ description: '用户名', required: false })
|
||||||
state: string;
|
username: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty({ description: '电话', required: false })
|
||||||
first_purchase_date: string;
|
phone: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty({ description: '头像URL', required: false })
|
||||||
customerId: number;
|
avatar: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '账单信息', type: 'object', required: false })
|
||||||
|
billing: any;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '配送信息', type: 'object', required: false })
|
||||||
|
shipping: any;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '原始数据', type: 'object', required: false })
|
||||||
|
raw: any;
|
||||||
|
|
||||||
|
|
||||||
|
@ApiProperty({ description: '创建时间' })
|
||||||
|
created_at: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '更新时间' })
|
||||||
|
updated_at: Date;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ApiProperty({ description: '评分' })
|
||||||
|
rate: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '标签列表', type: [String], required: false })
|
||||||
|
tags: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CustomerTagDTO {
|
// ====================== 单条操作 ======================
|
||||||
@ApiProperty()
|
|
||||||
|
// 创建客户请求DTO
|
||||||
|
export class CreateCustomerDTO {
|
||||||
|
@ApiProperty({ description: '站点ID' })
|
||||||
|
site_id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '原始ID', required: false })
|
||||||
|
origin_id?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '邮箱' })
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty({ description: '名字', required: false })
|
||||||
|
first_name?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '姓氏', required: false })
|
||||||
|
last_name?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '全名', required: false })
|
||||||
|
fullname?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '用户名', required: false })
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '电话', required: false })
|
||||||
|
phone?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '头像URL', required: false })
|
||||||
|
avatar?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '账单信息', type: 'object', required: false })
|
||||||
|
billing?: any;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '配送信息', type: 'object', required: false })
|
||||||
|
shipping?: any;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '原始数据', type: 'object', required: false })
|
||||||
|
raw?: any;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '评分', required: false })
|
||||||
|
rate?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '标签列表', type: [String], required: false })
|
||||||
|
tags?: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点创建时间', required: false })
|
||||||
|
site_created_at?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点更新时间', required: false })
|
||||||
|
site_updated_at?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新客户请求DTO
|
||||||
|
export class UpdateCustomerDTO {
|
||||||
|
@ApiProperty({ description: '站点ID', required: false })
|
||||||
|
site_id?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '原始ID', required: false })
|
||||||
|
origin_id?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '邮箱', required: false })
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '名字', required: false })
|
||||||
|
first_name?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '姓氏', required: false })
|
||||||
|
last_name?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '全名', required: false })
|
||||||
|
fullname?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '用户名', required: false })
|
||||||
|
username?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '电话', required: false })
|
||||||
|
phone?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '头像URL', required: false })
|
||||||
|
avatar?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '账单信息', type: 'object', required: false })
|
||||||
|
billing?: any;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '配送信息', type: 'object', required: false })
|
||||||
|
shipping?: any;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '原始数据', type: 'object', required: false })
|
||||||
|
raw?: any;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '评分', required: false })
|
||||||
|
rate?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '标签列表', type: [String], required: false })
|
||||||
|
tags?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询单个客户响应DTO(继承基本信息)
|
||||||
|
export class GetCustomerDTO extends CustomerDTO {
|
||||||
|
// 可以添加额外的详细信息字段
|
||||||
|
}
|
||||||
|
// 客户统计信息DTO(包含订单统计)
|
||||||
|
export class CustomerStatisticDTO extends CustomerDTO {
|
||||||
|
@ApiProperty({ description: '创建日期' })
|
||||||
|
date_created: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '首次购买日期' })
|
||||||
|
first_purchase_date: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '最后购买日期' })
|
||||||
|
last_purchase_date: Date;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '订单数量' })
|
||||||
|
orders: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '总消费金额' })
|
||||||
|
total: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Yoone订单数量', required: false })
|
||||||
|
yoone_orders?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'Yoone总金额', required: false })
|
||||||
|
yoone_total?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 客户统计查询条件DTO
|
||||||
|
export class CustomerStatisticWhereDTO {
|
||||||
|
@ApiProperty({ description: '邮箱筛选', required: false })
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '标签筛选', required: false })
|
||||||
|
tags?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '首次购买日期筛选', required: false })
|
||||||
|
first_purchase_date?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '评分筛选', required: false })
|
||||||
|
rate?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '客户ID筛选', required: false })
|
||||||
|
customerId?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 客户统计查询参数DTO(继承通用查询参数)
|
||||||
|
export type CustomerStatisticQueryParamsDTO = UnifiedSearchParamsDTO<CustomerStatisticWhereDTO>;
|
||||||
|
|
||||||
|
// 客户统计列表响应DTO
|
||||||
|
export class CustomerStatisticListResponseDTO {
|
||||||
|
@ApiProperty({ description: '客户统计列表', type: [CustomerStatisticDTO] })
|
||||||
|
items: CustomerStatisticDTO[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '总数', example: 100 })
|
||||||
|
total: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '当前页', example: 1 })
|
||||||
|
current: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '每页数量', example: 20 })
|
||||||
|
pageSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ====================== 批量操作 ======================
|
||||||
|
|
||||||
|
// 批量创建客户请求DTO
|
||||||
|
export class BatchCreateCustomerDTO {
|
||||||
|
@ApiProperty({ description: '客户列表', type: [CreateCustomerDTO] })
|
||||||
|
customers: CreateCustomerDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单个客户更新项DTO
|
||||||
|
export class UpdateCustomerItemDTO {
|
||||||
|
@ApiProperty({ description: '客户ID' })
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '更新字段', type: UpdateCustomerDTO })
|
||||||
|
update_data: Partial<Customer>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量更新客户请求DTO - 每个对象包含id和要更新的字段
|
||||||
|
export class BatchUpdateCustomerDTO {
|
||||||
|
@ApiProperty({ description: '客户更新列表', type: [UpdateCustomerItemDTO] })
|
||||||
|
customers: UpdateCustomerItemDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除客户请求DTO
|
||||||
|
export class BatchDeleteCustomerDTO {
|
||||||
|
@ApiProperty({ description: '客户ID列表', type: [Number] })
|
||||||
|
ids: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 查询操作 ======================
|
||||||
|
|
||||||
|
// 客户查询条件DTO(用于UnifiedSearchParamsDTO的where参数)
|
||||||
|
export class CustomerWhereDTO {
|
||||||
|
@ApiProperty({ description: '邮箱筛选', required: false })
|
||||||
|
email?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '标签筛选', required: false })
|
||||||
|
tags?: string;
|
||||||
|
|
||||||
|
|
||||||
|
@ApiProperty({ description: '评分筛选', required: false })
|
||||||
|
rate?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点ID筛选', required: false })
|
||||||
|
site_id?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '客户ID筛选', required: false })
|
||||||
|
customerId?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '首次购买日期筛选', required: false })
|
||||||
|
first_purchase_date?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '角色筛选', required: false })
|
||||||
|
role?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 客户查询参数DTO(继承通用查询参数)
|
||||||
|
export type CustomerQueryParamsDTO = UnifiedSearchParamsDTO<CustomerWhereDTO>;
|
||||||
|
|
||||||
|
// 客户列表响应DTO(参考site-api.dto.ts中的分页格式)
|
||||||
|
export class CustomerListResponseDTO {
|
||||||
|
@ApiProperty({ description: '客户列表', type: [CustomerDTO] })
|
||||||
|
items: CustomerDTO[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '总数', example: 100 })
|
||||||
|
total: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '页码', example: 1 })
|
||||||
|
page: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '每页数量', example: 20 })
|
||||||
|
per_page: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '总页数', example: 5 })
|
||||||
|
total_pages: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 客户标签相关 ======================
|
||||||
|
|
||||||
|
// 客户标签基本信息DTO
|
||||||
|
export class CustomerTagBasicDTO {
|
||||||
|
@ApiProperty({ description: '标签ID' })
|
||||||
|
id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '客户ID' })
|
||||||
|
customer_id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '标签名称' })
|
||||||
|
tag: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '创建时间', required: false })
|
||||||
|
created_at?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加客户标签请求DTO
|
||||||
|
export class AddCustomerTagDTO {
|
||||||
|
@ApiProperty({ description: '客户ID' })
|
||||||
|
customer_id: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '标签名称' })
|
||||||
tag: string;
|
tag: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CustomerDto {
|
// 批量添加客户标签请求DTO
|
||||||
@ApiProperty()
|
export class BatchAddCustomerTagDTO {
|
||||||
id: number;
|
@ApiProperty({ description: '客户ID' })
|
||||||
|
customer_id: number;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty({ description: '标签列表', type: [String] })
|
||||||
site_id: number;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
email: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
avatar: string;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
rate: number;
|
|
||||||
|
|
||||||
@ApiProperty()
|
|
||||||
state: string;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CustomerListResponseDTO {
|
// 删除客户标签请求DTO
|
||||||
@ApiProperty()
|
export class DeleteCustomerTagDTO {
|
||||||
total: number;
|
@ApiProperty({ description: '标签ID' })
|
||||||
|
tag_id: number;
|
||||||
@ApiProperty({ type: [CustomerDto] })
|
}
|
||||||
list: CustomerDto[];
|
|
||||||
|
// 批量删除客户标签请求DTO
|
||||||
|
export class BatchDeleteCustomerTagDTO {
|
||||||
|
@ApiProperty({ description: '标签ID列表', type: [Number] })
|
||||||
|
tag_ids: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================== 同步操作 ======================
|
||||||
|
|
||||||
|
// 同步客户数据请求DTO
|
||||||
|
export class SyncCustomersDTO {
|
||||||
|
@ApiProperty({ description: '站点ID' })
|
||||||
|
siteId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '查询参数(支持where和orderBy)', type: UnifiedSearchParamsDTO, required: false })
|
||||||
|
params?: UnifiedSearchParamsDTO<CustomerWhereDTO>;
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { ApiProperty } from '@midwayjs/swagger';
|
import { ApiProperty } from '@midwayjs/swagger';
|
||||||
import { Rule, RuleType } from '@midwayjs/validate';
|
import { Rule, RuleType } from '@midwayjs/validate';
|
||||||
|
import { UnifiedSearchParamsDTO } from './api.dto';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 属性输入DTO
|
* 属性输入DTO
|
||||||
|
|
@ -63,9 +64,17 @@ export class CreateProductDTO {
|
||||||
siteSkus?: string[];
|
siteSkus?: string[];
|
||||||
|
|
||||||
// 通用属性输入(通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
|
// 通用属性输入(通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等)
|
||||||
@ApiProperty({ description: '属性列表', type: 'array' })
|
// 当 type 为 'single' 时必填,当 type 为 'bundle' 时可选
|
||||||
@Rule(RuleType.array().required())
|
@ApiProperty({ description: '属性列表', type: 'array', required: false })
|
||||||
attributes: AttributeInputDTO[];
|
@Rule(
|
||||||
|
RuleType.array()
|
||||||
|
.when('type', {
|
||||||
|
is: 'single',
|
||||||
|
then: RuleType.array().required(),
|
||||||
|
otherwise: RuleType.array().optional()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
attributes?: AttributeInputDTO[];
|
||||||
|
|
||||||
// 商品价格
|
// 商品价格
|
||||||
@ApiProperty({ description: '价格', example: 99.99, required: false })
|
@ApiProperty({ description: '价格', example: 99.99, required: false })
|
||||||
|
|
@ -251,35 +260,104 @@ export class CreateCategoryAttributeDTO {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DTO 用于分页查询产品
|
* 产品查询过滤条件接口
|
||||||
*/
|
*/
|
||||||
export class QueryProductDTO {
|
export interface ProductWhereFilter {
|
||||||
@ApiProperty({ description: '当前页', example: 1 })
|
// 产品ID
|
||||||
@Rule(RuleType.number().default(1))
|
id?: number;
|
||||||
current: number;
|
// 产品ID列表
|
||||||
|
ids?: number[];
|
||||||
@ApiProperty({ description: '每页数量', example: 10 })
|
// SKU
|
||||||
@Rule(RuleType.number().default(10))
|
sku?: string;
|
||||||
pageSize: number;
|
// SKU列表
|
||||||
|
skus?: string[];
|
||||||
@ApiProperty({ description: '搜索关键字', required: false })
|
// 产品名称
|
||||||
@Rule(RuleType.string())
|
|
||||||
name?: string;
|
name?: string;
|
||||||
|
// 产品中文名称
|
||||||
@ApiProperty({ description: '分类ID', required: false })
|
nameCn?: string;
|
||||||
@Rule(RuleType.number())
|
// 分类ID
|
||||||
categoryId?: number;
|
categoryId?: number;
|
||||||
|
// 分类ID列表
|
||||||
@ApiProperty({ description: '品牌ID', required: false })
|
categoryIds?: number[];
|
||||||
@Rule(RuleType.number())
|
// 品牌ID
|
||||||
brandId?: number;
|
brandId?: number;
|
||||||
|
// 品牌ID列表
|
||||||
@ApiProperty({ description: '排序字段', required: false })
|
brandIds?: number[];
|
||||||
@Rule(RuleType.string())
|
// 产品类型
|
||||||
sortField?: string;
|
type?: string;
|
||||||
|
// 价格最小值
|
||||||
@ApiProperty({ description: '排序方式', required: false })
|
minPrice?: number;
|
||||||
@Rule(RuleType.string().valid('ascend', 'descend'))
|
// 价格最大值
|
||||||
sortOrder?: string;
|
maxPrice?: number;
|
||||||
|
// 促销价格最小值
|
||||||
|
minPromotionPrice?: number;
|
||||||
|
// 促销价格最大值
|
||||||
|
maxPromotionPrice?: number;
|
||||||
|
// 创建时间范围开始
|
||||||
|
createdAtStart?: string;
|
||||||
|
// 创建时间范围结束
|
||||||
|
createdAtEnd?: string;
|
||||||
|
// 更新时间范围开始
|
||||||
|
updatedAtStart?: string;
|
||||||
|
// 更新时间范围结束
|
||||||
|
updatedAtEnd?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO 用于分页查询产品
|
||||||
|
* 支持灵活的where条件、分页和排序
|
||||||
|
*/
|
||||||
|
export class QueryProductDTO extends UnifiedSearchParamsDTO<ProductWhereFilter> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO 用于创建分类
|
||||||
|
*/
|
||||||
|
export class CreateCategoryDTO {
|
||||||
|
@ApiProperty({ description: '分类显示名称', required: true })
|
||||||
|
@Rule(RuleType.string().required())
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '分类中文名称', required: false })
|
||||||
|
@Rule(RuleType.string().allow('').optional())
|
||||||
|
titleCN?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '分类唯一标识', required: true })
|
||||||
|
@Rule(RuleType.string().required())
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '分类短名称,用于生成SKU', required: false })
|
||||||
|
@Rule(RuleType.string().allow('').optional())
|
||||||
|
shortName?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '排序', required: false })
|
||||||
|
@Rule(RuleType.number().optional())
|
||||||
|
sort?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DTO 用于更新分类
|
||||||
|
*/
|
||||||
|
export class UpdateCategoryDTO {
|
||||||
|
@ApiProperty({ description: '分类显示名称', required: false })
|
||||||
|
@Rule(RuleType.string().optional())
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '分类中文名称', required: false })
|
||||||
|
@Rule(RuleType.string().allow('').optional())
|
||||||
|
titleCN?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '分类唯一标识', required: false })
|
||||||
|
@Rule(RuleType.string().optional())
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '分类短名称,用于生成SKU', required: false })
|
||||||
|
@Rule(RuleType.string().allow('').optional())
|
||||||
|
shortName?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '排序', required: false })
|
||||||
|
@Rule(RuleType.number().optional())
|
||||||
|
sort?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,11 @@ import { OrderNote } from '../entity/order_note.entity';
|
||||||
import { PaymentMethodDTO } from './logistics.dto';
|
import { PaymentMethodDTO } from './logistics.dto';
|
||||||
import { Subscription } from '../entity/subscription.entity';
|
import { Subscription } from '../entity/subscription.entity';
|
||||||
import { Dict } from '../entity/dict.entity';
|
import { Dict } from '../entity/dict.entity';
|
||||||
|
import { SyncOperationResultDTO } from './api.dto';
|
||||||
|
|
||||||
export class BooleanRes extends SuccessWrapper(Boolean) {}
|
export class BooleanRes extends SuccessWrapper(Boolean) {}
|
||||||
|
// 同步操作结果返回数据
|
||||||
|
export class SyncOperationResultRes extends SuccessWrapper(SyncOperationResultDTO) {}
|
||||||
//网站配置返回数据
|
//网站配置返回数据
|
||||||
export class SitesResponse extends SuccessArrayWrapper(SiteConfig) {}
|
export class SitesResponse extends SuccessArrayWrapper(SiteConfig) {}
|
||||||
//产品分页数据
|
//产品分页数据
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,7 @@ export interface ShopyyOrder {
|
||||||
total_amount?: string | number;
|
total_amount?: string | number;
|
||||||
current_total_price?: string | number;
|
current_total_price?: string | number;
|
||||||
current_subtotal_price?: string | number;
|
current_subtotal_price?: string | number;
|
||||||
current_shipping_price?: number;
|
current_shipping_price?: string | number;
|
||||||
current_tax_price?: string | number;
|
current_tax_price?: string | number;
|
||||||
current_coupon_price?: string | number;
|
current_coupon_price?: string | number;
|
||||||
current_payment_price?: string | number;
|
current_payment_price?: string | number;
|
||||||
|
|
@ -400,32 +400,34 @@ export interface ShopyyWebhook {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发货相关DTO
|
// 发货相关DTO
|
||||||
export class ShopyyShipOrderItemDTO {
|
// 批量履行
|
||||||
order_item_id: number;
|
// https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse
|
||||||
quantity: number;
|
export class ShopyyFulfillmentDTO {
|
||||||
|
"order_number": string;
|
||||||
|
"tracking_company": string;
|
||||||
|
"tracking_number": string;
|
||||||
|
"courier_code": number;
|
||||||
|
"note": string;
|
||||||
|
"mode": "replace" | 'cover' | null// 模式 replace(替换) cover (覆盖) 空(新增)
|
||||||
}
|
}
|
||||||
|
// https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse
|
||||||
export class ShopyyShipOrderDTO {
|
export class ShopyPartFulfillmentDTO {
|
||||||
tracking_number?: string;
|
order_number: string;
|
||||||
shipping_provider?: string;
|
note: string;
|
||||||
shipping_method?: string;
|
tracking_company: string;
|
||||||
items?: ShopyyShipOrderItemDTO[];
|
tracking_number: string;
|
||||||
|
courier_code: string;
|
||||||
|
products: ({
|
||||||
|
quantity: number,
|
||||||
|
order_product_id: string
|
||||||
|
})[]
|
||||||
}
|
}
|
||||||
|
// https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse
|
||||||
export class ShopyyCancelShipOrderDTO {
|
export class ShopyyCancelFulfillmentDTO {
|
||||||
reason?: string;
|
|
||||||
shipment_id?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ShopyyBatchShipOrderItemDTO {
|
|
||||||
order_id: string;
|
order_id: string;
|
||||||
tracking_number?: string;
|
fullfillment_id: string;
|
||||||
shipping_provider?: string;
|
|
||||||
shipping_method?: string;
|
|
||||||
items?: ShopyyShipOrderItemDTO[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ShopyyBatchShipOrdersDTO {
|
export class ShopyyBatchFulfillmentItemDTO {
|
||||||
orders: ShopyyBatchShipOrderItemDTO[];
|
fullfillments: ShopyPartFulfillmentDTO[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,8 @@
|
||||||
import { ApiProperty } from '@midwayjs/swagger';
|
import { ApiProperty } from '@midwayjs/swagger';
|
||||||
|
import {
|
||||||
|
UnifiedPaginationDTO,
|
||||||
|
} from './api.dto';
|
||||||
|
|
||||||
export class UnifiedPaginationDTO<T> {
|
|
||||||
// 分页DTO用于承载统一分页信息与列表数据
|
|
||||||
@ApiProperty({ description: '列表数据' })
|
|
||||||
items: T[];
|
|
||||||
|
|
||||||
@ApiProperty({ description: '总数', example: 100 })
|
|
||||||
total: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '当前页', example: 1 })
|
|
||||||
page: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '每页数量', example: 20 })
|
|
||||||
per_page: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '总页数', example: 5 })
|
|
||||||
totalPages: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '分页后的数据', required: false })
|
|
||||||
after?: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '分页前的数据', required: false })
|
|
||||||
before?: string;
|
|
||||||
}
|
|
||||||
export class UnifiedTagDTO {
|
export class UnifiedTagDTO {
|
||||||
// 标签DTO用于承载统一标签数据
|
// 标签DTO用于承载统一标签数据
|
||||||
@ApiProperty({ description: '标签ID' })
|
@ApiProperty({ description: '标签ID' })
|
||||||
|
|
@ -39,6 +19,24 @@ export class UnifiedCategoryDTO {
|
||||||
@ApiProperty({ description: '分类名称' })
|
@ApiProperty({ description: '分类名称' })
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
// 订单跟踪号
|
||||||
|
export class UnifiedOrderTrackingDTO {
|
||||||
|
@ApiProperty({ description: '订单ID' })
|
||||||
|
order_id: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '快递公司' })
|
||||||
|
tracking_provider: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '运单跟踪号' })
|
||||||
|
tracking_number: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '发货日期' })
|
||||||
|
date_shipped: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '发货状态' })
|
||||||
|
status_shipped: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class UnifiedImageDTO {
|
export class UnifiedImageDTO {
|
||||||
// 图片DTO用于承载统一图片数据
|
// 图片DTO用于承载统一图片数据
|
||||||
@ApiProperty({ description: '图片ID' })
|
@ApiProperty({ description: '图片ID' })
|
||||||
|
|
@ -139,6 +137,9 @@ export class UnifiedProductAttributeDTO {
|
||||||
|
|
||||||
@ApiProperty({ description: '属性选项', type: [String] })
|
@ApiProperty({ description: '属性选项', type: [String] })
|
||||||
options: string[];
|
options: string[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '变体属性值(单个值)', required: false })
|
||||||
|
option?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnifiedProductVariationDTO {
|
export class UnifiedProductVariationDTO {
|
||||||
|
|
@ -146,6 +147,9 @@ export class UnifiedProductVariationDTO {
|
||||||
@ApiProperty({ description: '变体ID' })
|
@ApiProperty({ description: '变体ID' })
|
||||||
id: number | string;
|
id: number | string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '变体名称' })
|
||||||
|
name: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '变体SKU' })
|
@ApiProperty({ description: '变体SKU' })
|
||||||
sku: string;
|
sku: string;
|
||||||
|
|
||||||
|
|
@ -164,8 +168,47 @@ export class UnifiedProductVariationDTO {
|
||||||
@ApiProperty({ description: '库存数量' })
|
@ApiProperty({ description: '库存数量' })
|
||||||
stock_quantity: number;
|
stock_quantity: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '变体属性', type: () => [UnifiedProductAttributeDTO], required: false })
|
||||||
|
attributes?: UnifiedProductAttributeDTO[];
|
||||||
|
|
||||||
@ApiProperty({ description: '变体图片', type: () => UnifiedImageDTO, required: false })
|
@ApiProperty({ description: '变体图片', type: () => UnifiedImageDTO, required: false })
|
||||||
image?: UnifiedImageDTO;
|
image?: UnifiedImageDTO;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '变体描述', required: false })
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否启用', required: false })
|
||||||
|
enabled?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否可下载', required: false })
|
||||||
|
downloadable?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否为虚拟商品', required: false })
|
||||||
|
virtual?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '管理库存', required: false })
|
||||||
|
manage_stock?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '重量', required: false })
|
||||||
|
weight?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '长度', required: false })
|
||||||
|
length?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '宽度', required: false })
|
||||||
|
width?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '高度', required: false })
|
||||||
|
height?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '运输类别', required: false })
|
||||||
|
shipping_class?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '税类别', required: false })
|
||||||
|
tax_class?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '菜单顺序', required: false })
|
||||||
|
menu_order?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnifiedProductDTO {
|
export class UnifiedProductDTO {
|
||||||
|
|
@ -253,6 +296,17 @@ export class UnifiedProductDTO {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class UnifiedOrderRefundDTO {
|
||||||
|
@ApiProperty({ description: '退款ID' })
|
||||||
|
id: number | string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '退款原因' })
|
||||||
|
reason: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '退款金额' })
|
||||||
|
total: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class UnifiedOrderDTO {
|
export class UnifiedOrderDTO {
|
||||||
// 订单DTO用于承载统一订单数据
|
// 订单DTO用于承载统一订单数据
|
||||||
@ApiProperty({ description: '订单ID' })
|
@ApiProperty({ description: '订单ID' })
|
||||||
|
|
@ -309,6 +363,8 @@ export class UnifiedOrderDTO {
|
||||||
|
|
||||||
@ApiProperty({ description: '支付方式' })
|
@ApiProperty({ description: '支付方式' })
|
||||||
payment_method: string;
|
payment_method: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '退款列表', type: () => [UnifiedOrderRefundDTO] })
|
||||||
refunds: UnifiedOrderRefundDTO[];
|
refunds: UnifiedOrderRefundDTO[];
|
||||||
@ApiProperty({ description: '创建时间' })
|
@ApiProperty({ description: '创建时间' })
|
||||||
date_created: string;
|
date_created: string;
|
||||||
|
|
@ -328,6 +384,9 @@ export class UnifiedOrderDTO {
|
||||||
@ApiProperty({ description: '优惠券项', type: () => [UnifiedCouponLineDTO], required: false })
|
@ApiProperty({ description: '优惠券项', type: () => [UnifiedCouponLineDTO], required: false })
|
||||||
coupon_lines?: UnifiedCouponLineDTO[];
|
coupon_lines?: UnifiedCouponLineDTO[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '物流追踪信息', type: () => [UnifiedOrderTrackingDTO], required: false })
|
||||||
|
tracking?: UnifiedOrderTrackingDTO[];
|
||||||
|
|
||||||
@ApiProperty({ description: '支付时间', required: false })
|
@ApiProperty({ description: '支付时间', required: false })
|
||||||
date_paid?: string | null;
|
date_paid?: string | null;
|
||||||
|
|
||||||
|
|
@ -366,7 +425,6 @@ export class UnifiedShippingLineDTO {
|
||||||
|
|
||||||
@ApiProperty({ description: '配送方式元数据' })
|
@ApiProperty({ description: '配送方式元数据' })
|
||||||
meta_data?: any[];
|
meta_data?: any[];
|
||||||
|
|
||||||
}
|
}
|
||||||
export class UnifiedFeeLineDTO {
|
export class UnifiedFeeLineDTO {
|
||||||
// 费用项DTO用于承载统一费用项数据
|
// 费用项DTO用于承载统一费用项数据
|
||||||
|
|
@ -635,34 +693,6 @@ export class UploadMediaDTO {
|
||||||
filename: string;
|
filename: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnifiedSearchParamsDTO<Where=Record<string, any>> {
|
|
||||||
// 统一查询参数DTO用于承载分页与筛选与排序参数
|
|
||||||
@ApiProperty({ description: '页码', example: 1, required: false })
|
|
||||||
page?: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '每页数量', example: 20, required: false })
|
|
||||||
per_page?: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '搜索关键词', required: false })
|
|
||||||
search?: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '过滤条件对象', type: 'object', required: false })
|
|
||||||
where?: Where;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '创建时间后', required: false })
|
|
||||||
after?: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '创建时间前', required: false })
|
|
||||||
before?: string;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
description: '排序对象,例如 { "sku": "desc" }',
|
|
||||||
type: 'object',
|
|
||||||
required: false,
|
|
||||||
})
|
|
||||||
orderBy?: Record<string, 'asc' | 'desc'> | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class UnifiedWebhookDTO {
|
export class UnifiedWebhookDTO {
|
||||||
// Webhook DTO用于承载统一webhook数据
|
// Webhook DTO用于承载统一webhook数据
|
||||||
@ApiProperty({ description: 'Webhook ID' })
|
@ApiProperty({ description: 'Webhook ID' })
|
||||||
|
|
@ -747,18 +777,8 @@ export class UpdateWebhookDTO {
|
||||||
api_version?: string;
|
api_version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UnifiedOrderRefundDTO {
|
|
||||||
@ApiProperty({ description: '退款ID' })
|
|
||||||
id: number | string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '退款原因' })
|
export class FulfillmentItemDTO {
|
||||||
reason: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '退款金额' })
|
|
||||||
total: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ShipOrderItemDTO {
|
|
||||||
@ApiProperty({ description: '订单项ID' })
|
@ApiProperty({ description: '订单项ID' })
|
||||||
order_item_id: number;
|
order_item_id: number;
|
||||||
|
|
||||||
|
|
@ -766,7 +786,7 @@ export class ShipOrderItemDTO {
|
||||||
quantity: number;
|
quantity: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ShipOrderDTO {
|
export class FulfillmentDTO {
|
||||||
@ApiProperty({ description: '物流单号', required: false })
|
@ApiProperty({ description: '物流单号', required: false })
|
||||||
tracking_number?: string;
|
tracking_number?: string;
|
||||||
|
|
||||||
|
|
@ -776,11 +796,11 @@ export class ShipOrderDTO {
|
||||||
@ApiProperty({ description: '发货方式', required: false })
|
@ApiProperty({ description: '发货方式', required: false })
|
||||||
shipping_method?: string;
|
shipping_method?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '发货商品项', type: () => [ShipOrderItemDTO], required: false })
|
@ApiProperty({ description: '发货商品项', type: () => [FulfillmentItemDTO], required: false })
|
||||||
items?: ShipOrderItemDTO[];
|
items?: FulfillmentItemDTO[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CancelShipOrderDTO {
|
export class CancelFulfillmentDTO {
|
||||||
@ApiProperty({ description: '取消原因', required: false })
|
@ApiProperty({ description: '取消原因', required: false })
|
||||||
reason?: string;
|
reason?: string;
|
||||||
|
|
||||||
|
|
@ -788,7 +808,7 @@ export class CancelShipOrderDTO {
|
||||||
shipment_id?: string;
|
shipment_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BatchShipOrderItemDTO {
|
export class BatchFulfillmentItemDTO {
|
||||||
@ApiProperty({ description: '订单ID' })
|
@ApiProperty({ description: '订单ID' })
|
||||||
order_id: string;
|
order_id: string;
|
||||||
|
|
||||||
|
|
@ -801,11 +821,137 @@ export class BatchShipOrderItemDTO {
|
||||||
@ApiProperty({ description: '发货方式', required: false })
|
@ApiProperty({ description: '发货方式', required: false })
|
||||||
shipping_method?: string;
|
shipping_method?: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '发货商品项', type: () => [ShipOrderItemDTO], required: false })
|
@ApiProperty({ description: '发货商品项', type: () => [FulfillmentItemDTO], required: false })
|
||||||
items?: ShipOrderItemDTO[];
|
items?: FulfillmentItemDTO[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BatchShipOrdersDTO {
|
export class CreateVariationDTO {
|
||||||
@ApiProperty({ description: '批量发货订单列表', type: () => [BatchShipOrderItemDTO] })
|
// 创建产品变体DTO用于承载创建产品变体的请求数据
|
||||||
orders: BatchShipOrderItemDTO[];
|
@ApiProperty({ description: '变体SKU', required: false })
|
||||||
|
sku?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '常规价格', required: false })
|
||||||
|
regular_price?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '销售价格', required: false })
|
||||||
|
sale_price?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '库存状态', required: false })
|
||||||
|
stock_status?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '库存数量', required: false })
|
||||||
|
stock_quantity?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '变体属性', type: () => [UnifiedProductAttributeDTO], required: false })
|
||||||
|
attributes?: UnifiedProductAttributeDTO[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '变体图片', type: () => UnifiedImageDTO, required: false })
|
||||||
|
image?: UnifiedImageDTO;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '变体描述', required: false })
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否启用', required: false })
|
||||||
|
enabled?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否可下载', required: false })
|
||||||
|
downloadable?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否为虚拟商品', required: false })
|
||||||
|
virtual?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '管理库存', required: false })
|
||||||
|
manage_stock?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '重量', required: false })
|
||||||
|
weight?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '长度', required: false })
|
||||||
|
length?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '宽度', required: false })
|
||||||
|
width?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '高度', required: false })
|
||||||
|
height?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '运输类别', required: false })
|
||||||
|
shipping_class?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '税类别', required: false })
|
||||||
|
tax_class?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '菜单顺序', required: false })
|
||||||
|
menu_order?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateVariationDTO {
|
||||||
|
// 更新产品变体DTO用于承载更新产品变体的请求数据
|
||||||
|
@ApiProperty({ description: '变体SKU', required: false })
|
||||||
|
sku?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '常规价格', required: false })
|
||||||
|
regular_price?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '销售价格', required: false })
|
||||||
|
sale_price?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '库存状态', required: false })
|
||||||
|
stock_status?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '库存数量', required: false })
|
||||||
|
stock_quantity?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '变体属性', type: () => [UnifiedProductAttributeDTO], required: false })
|
||||||
|
attributes?: UnifiedProductAttributeDTO[];
|
||||||
|
|
||||||
|
@ApiProperty({ description: '变体图片', type: () => UnifiedImageDTO, required: false })
|
||||||
|
image?: UnifiedImageDTO;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '变体描述', required: false })
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否启用', required: false })
|
||||||
|
enabled?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否可下载', required: false })
|
||||||
|
downloadable?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否为虚拟商品', required: false })
|
||||||
|
virtual?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '管理库存', required: false })
|
||||||
|
manage_stock?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '重量', required: false })
|
||||||
|
weight?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '长度', required: false })
|
||||||
|
length?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '宽度', required: false })
|
||||||
|
width?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '高度', required: false })
|
||||||
|
height?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '运输类别', required: false })
|
||||||
|
shipping_class?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '税类别', required: false })
|
||||||
|
tax_class?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '菜单顺序', required: false })
|
||||||
|
menu_order?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnifiedVariationPaginationDTO extends UnifiedPaginationDTO<UnifiedProductVariationDTO> {
|
||||||
|
// 产品变体分页DTO用于承载产品变体列表分页数据
|
||||||
|
@ApiProperty({ description: '列表数据', type: () => [UnifiedProductVariationDTO] })
|
||||||
|
items: UnifiedProductVariationDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BatchFulfillmentsDTO {
|
||||||
|
@ApiProperty({ description: '批量发货订单列表', type: () => [BatchFulfillmentItemDTO] })
|
||||||
|
orders: BatchFulfillmentItemDTO[];
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { ApiProperty } from '@midwayjs/swagger';
|
||||||
|
import { Rule, RuleType } from '@midwayjs/validate';
|
||||||
|
/**
|
||||||
|
* 产品站点SKU信息DTO
|
||||||
|
*/
|
||||||
|
export class ProductSiteSkuDTO {
|
||||||
|
@ApiProperty({ description: '产品ID', example: 1 })
|
||||||
|
@Rule(RuleType.number().required())
|
||||||
|
productId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点SKU',nullable:true, example: 'SKU-001' })
|
||||||
|
siteSku?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步单个产品到站点的请求DTO
|
||||||
|
*/
|
||||||
|
export class SyncProductToSiteDTO extends ProductSiteSkuDTO {
|
||||||
|
@ApiProperty({ description: '站点ID', example: 1 })
|
||||||
|
@Rule(RuleType.number().required())
|
||||||
|
siteId: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步到站点的结果DTO
|
||||||
|
*/
|
||||||
|
export class SyncProductToSiteResultDTO {
|
||||||
|
@ApiProperty({ description: '同步状态', example: true })
|
||||||
|
success: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '远程产品ID', example: '123', required: false })
|
||||||
|
remoteId?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '错误信息', required: false })
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量同步产品到站点的请求DTO
|
||||||
|
*/
|
||||||
|
export class BatchSyncProductToSiteDTO {
|
||||||
|
@ApiProperty({ description: '站点ID', example: 1 })
|
||||||
|
@Rule(RuleType.number().required())
|
||||||
|
siteId: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '产品站点SKU列表', type: [ProductSiteSkuDTO] })
|
||||||
|
@Rule(RuleType.array().items(RuleType.object()).required().min(1))
|
||||||
|
data: ProductSiteSkuDTO[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -36,22 +36,39 @@ export class SiteConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateSiteDTO {
|
export class CreateSiteDTO {
|
||||||
|
@ApiProperty({ description: '站点 API URL', required: false })
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
apiUrl?: string;
|
apiUrl?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点网站 URL', required: false })
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
websiteUrl?: string;
|
websiteUrl?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点 REST Key', required: false })
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
consumerKey?: string;
|
consumerKey?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点 REST 秘钥', required: false })
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
consumerSecret?: string;
|
consumerSecret?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '访问令牌', required: false })
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
token?: string;
|
token?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点名称' })
|
||||||
@Rule(RuleType.string())
|
@Rule(RuleType.string())
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点描述', required: false })
|
||||||
@Rule(RuleType.string().allow('').optional())
|
@Rule(RuleType.string().allow('').optional())
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'], required: false })
|
||||||
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
|
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
|
||||||
type?: string;
|
type?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'SKU 前缀', required: false })
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
skuPrefix?: string;
|
skuPrefix?: string;
|
||||||
|
|
||||||
|
|
@ -67,22 +84,39 @@ export class CreateSiteDTO {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UpdateSiteDTO {
|
export class UpdateSiteDTO {
|
||||||
|
@ApiProperty({ description: '站点 API URL', required: false })
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
apiUrl?: string;
|
apiUrl?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点 REST Key', required: false })
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
consumerKey?: string;
|
consumerKey?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点 REST 秘钥', required: false })
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
consumerSecret?: string;
|
consumerSecret?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '访问令牌', required: false })
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
token?: string;
|
token?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点名称', required: false })
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点描述', required: false })
|
||||||
@Rule(RuleType.string().allow('').optional())
|
@Rule(RuleType.string().allow('').optional())
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否禁用', required: false })
|
||||||
@Rule(RuleType.boolean().optional())
|
@Rule(RuleType.boolean().optional())
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'], required: false })
|
||||||
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
|
@Rule(RuleType.string().valid('woocommerce', 'shopyy').optional())
|
||||||
type?: string;
|
type?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: 'SKU 前缀', required: false })
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
skuPrefix?: string;
|
skuPrefix?: string;
|
||||||
|
|
||||||
|
|
@ -95,25 +129,36 @@ export class UpdateSiteDTO {
|
||||||
@ApiProperty({ description: '绑定仓库ID列表' })
|
@ApiProperty({ description: '绑定仓库ID列表' })
|
||||||
@Rule(RuleType.array().items(RuleType.number()).optional())
|
@Rule(RuleType.array().items(RuleType.number()).optional())
|
||||||
stockPointIds?: number[];
|
stockPointIds?: number[];
|
||||||
@ApiProperty({ description: '站点网站URL' })
|
|
||||||
|
@ApiProperty({ description: '站点网站URL', required: false })
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
websiteUrl?: string;
|
websiteUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QuerySiteDTO {
|
export class QuerySiteDTO {
|
||||||
|
@ApiProperty({ description: '当前页码', required: false })
|
||||||
@Rule(RuleType.number().optional())
|
@Rule(RuleType.number().optional())
|
||||||
current?: number;
|
current?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '每页数量', required: false })
|
||||||
@Rule(RuleType.number().optional())
|
@Rule(RuleType.number().optional())
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '搜索关键词', required: false })
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
keyword?: string;
|
keyword?: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否禁用', required: false })
|
||||||
@Rule(RuleType.boolean().optional())
|
@Rule(RuleType.boolean().optional())
|
||||||
isDisabled?: boolean;
|
isDisabled?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '站点ID列表(逗号分隔)', required: false })
|
||||||
@Rule(RuleType.string().optional())
|
@Rule(RuleType.string().optional())
|
||||||
ids?: string;
|
ids?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DisableSiteDTO {
|
export class DisableSiteDTO {
|
||||||
|
@ApiProperty({ description: '是否禁用' })
|
||||||
@Rule(RuleType.boolean())
|
@Rule(RuleType.boolean())
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,8 +125,97 @@ export interface WooProduct {
|
||||||
// 元数据
|
// 元数据
|
||||||
meta_data?: Array<{ id?: number; key: string; value: any }>;
|
meta_data?: Array<{ id?: number; key: string; value: any }>;
|
||||||
}
|
}
|
||||||
export interface WooVariation{
|
export interface WooVariation {
|
||||||
|
// 变体主键
|
||||||
|
id: number;
|
||||||
|
// 创建时间
|
||||||
|
date_created: string;
|
||||||
|
// 创建时间(GMT)
|
||||||
|
date_created_gmt: string;
|
||||||
|
// 更新时间
|
||||||
|
date_modified: string;
|
||||||
|
// 更新时间(GMT)
|
||||||
|
date_modified_gmt: string;
|
||||||
|
// 变体描述
|
||||||
|
description: string;
|
||||||
|
// 变体SKU
|
||||||
|
sku: string;
|
||||||
|
// 常规价格
|
||||||
|
regular_price?: string;
|
||||||
|
// 促销价格
|
||||||
|
sale_price?: string;
|
||||||
|
// 当前价格
|
||||||
|
price?: string;
|
||||||
|
// 价格HTML
|
||||||
|
price_html?: string;
|
||||||
|
// 促销开始日期
|
||||||
|
date_on_sale_from?: string;
|
||||||
|
// 促销开始日期(GMT)
|
||||||
|
date_on_sale_from_gmt?: string;
|
||||||
|
// 促销结束日期
|
||||||
|
date_on_sale_to?: string;
|
||||||
|
// 促销结束日期(GMT)
|
||||||
|
date_on_sale_to_gmt?: string;
|
||||||
|
// 是否在促销中
|
||||||
|
on_sale: boolean;
|
||||||
|
// 是否可购买
|
||||||
|
purchasable: boolean;
|
||||||
|
// 总销量
|
||||||
|
total_sales: number;
|
||||||
|
// 是否为虚拟商品
|
||||||
|
virtual: boolean;
|
||||||
|
// 是否可下载
|
||||||
|
downloadable: boolean;
|
||||||
|
// 下载文件
|
||||||
|
downloads: Array<{ id?: number; name?: string; file?: string }>;
|
||||||
|
// 下载限制
|
||||||
|
download_limit: number;
|
||||||
|
// 下载过期天数
|
||||||
|
download_expiry: number;
|
||||||
|
// 库存状态
|
||||||
|
stock_status?: 'instock' | 'outofstock' | 'onbackorder';
|
||||||
|
// 库存数量
|
||||||
|
stock_quantity?: number;
|
||||||
|
// 是否管理库存
|
||||||
|
manage_stock?: boolean;
|
||||||
|
// 缺货预定设置
|
||||||
|
backorders?: 'no' | 'notify' | 'yes';
|
||||||
|
// 是否允许缺货预定
|
||||||
|
backorders_allowed?: boolean;
|
||||||
|
// 是否处于缺货预定状态
|
||||||
|
backordered?: boolean;
|
||||||
|
// 是否单独出售
|
||||||
|
sold_individually?: boolean;
|
||||||
|
// 重量
|
||||||
|
weight?: string;
|
||||||
|
// 尺寸
|
||||||
|
dimensions?: { length?: string; width?: string; height?: string };
|
||||||
|
// 是否需要运输
|
||||||
|
shipping_required?: boolean;
|
||||||
|
// 运输是否计税
|
||||||
|
shipping_taxable?: boolean;
|
||||||
|
// 运输类别
|
||||||
|
shipping_class?: string;
|
||||||
|
// 运输类别ID
|
||||||
|
shipping_class_id?: number;
|
||||||
|
// 变体图片
|
||||||
|
image?: { id: number; src: string; name?: string; alt?: string };
|
||||||
|
// 变体属性列表
|
||||||
|
attributes?: Array<{
|
||||||
|
id?: number;
|
||||||
|
name?: string;
|
||||||
|
option?: string;
|
||||||
|
}>;
|
||||||
|
// 菜单排序
|
||||||
|
menu_order?: number;
|
||||||
|
// 元数据
|
||||||
|
meta_data?: Array<{ id?: number; key: string; value: any }>;
|
||||||
|
// 父产品ID
|
||||||
|
parent_id?: number;
|
||||||
|
// 变体名称
|
||||||
|
name?: string;
|
||||||
|
// 是否启用
|
||||||
|
status?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 订单类型
|
// 订单类型
|
||||||
|
|
@ -280,6 +369,13 @@ export interface WooOrder {
|
||||||
date_created_gmt?: string;
|
date_created_gmt?: string;
|
||||||
date_modified?: string;
|
date_modified?: string;
|
||||||
date_modified_gmt?: string;
|
date_modified_gmt?: string;
|
||||||
|
// 物流追踪信息
|
||||||
|
trackings?: Array<{
|
||||||
|
tracking_provider?: string;
|
||||||
|
tracking_number?: string;
|
||||||
|
date_shipped?: string;
|
||||||
|
status_shipped?: string;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
export interface WooOrderRefund {
|
export interface WooOrderRefund {
|
||||||
id?: number;
|
id?: number;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
|
|
||||||
import { ApiProperty } from '@midwayjs/swagger';
|
import { ApiProperty } from '@midwayjs/swagger';
|
||||||
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
|
import { Entity, PrimaryGeneratedColumn, Column, ManyToMany } from 'typeorm';
|
||||||
|
import { Site } from './site.entity';
|
||||||
|
import { StockPoint } from './stock_point.entity';
|
||||||
|
|
||||||
@Entity('area')
|
@Entity('area')
|
||||||
export class Area {
|
export class Area {
|
||||||
|
|
@ -14,4 +16,10 @@ export class Area {
|
||||||
@ApiProperty({ description: '编码' })
|
@ApiProperty({ description: '编码' })
|
||||||
@Column({ unique: true })
|
@Column({ unique: true })
|
||||||
code: string;
|
code: string;
|
||||||
|
|
||||||
|
@ManyToMany(() => Site, site => site.areas)
|
||||||
|
sites: Site[];
|
||||||
|
|
||||||
|
@ManyToMany(() => StockPoint, stockPoint => stockPoint.areas)
|
||||||
|
stockPoints: StockPoint[];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ export class Category {
|
||||||
@ApiProperty({ description: '分类唯一标识' })
|
@ApiProperty({ description: '分类唯一标识' })
|
||||||
@Column({ unique: true })
|
@Column({ unique: true })
|
||||||
name: string;
|
name: string;
|
||||||
|
// 分类短名称, 用于生成SKU
|
||||||
|
@ApiProperty({ description: '分类短名称' })
|
||||||
|
@Column({ nullable: true })
|
||||||
|
shortName: string;
|
||||||
|
|
||||||
@ApiProperty({ description: '排序' })
|
@ApiProperty({ description: '排序' })
|
||||||
@Column({ default: 0 })
|
@Column({ default: 0 })
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,12 @@ export class DictItem {
|
||||||
@Column({ comment: '字典唯一标识名称' })
|
@Column({ comment: '字典唯一标识名称' })
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, comment: '简称' })
|
||||||
|
shortName: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true, comment: '字典项描述' })
|
||||||
|
description?: string
|
||||||
|
|
||||||
// 字典项值
|
// 字典项值
|
||||||
@Column({ nullable: true, comment: '字典项值' })
|
@Column({ nullable: true, comment: '字典项值' })
|
||||||
value?: string;
|
value?: string;
|
||||||
|
|
@ -41,9 +47,6 @@ export class DictItem {
|
||||||
@Column({ nullable: true, comment: '图片' })
|
@Column({ nullable: true, comment: '图片' })
|
||||||
image: string;
|
image: string;
|
||||||
|
|
||||||
@Column({ nullable: true, comment: '简称' })
|
|
||||||
shortName: string;
|
|
||||||
|
|
||||||
// 排序
|
// 排序
|
||||||
@Column({ default: 0, comment: '排序' })
|
@Column({ default: 0, comment: '排序' })
|
||||||
sort: number;
|
sort: number;
|
||||||
|
|
|
||||||
|
|
@ -62,14 +62,14 @@ export class Order {
|
||||||
currency: string;
|
currency: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@Column()
|
@Column({ nullable: true })
|
||||||
@Expose()
|
@Expose()
|
||||||
currency_symbol: string;
|
currency_symbol?: string;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@Column({ default: false })
|
@Column({ default: false })
|
||||||
@Expose()
|
@Expose()
|
||||||
prices_include_tax: boolean;
|
prices_include_tax?: boolean;
|
||||||
|
|
||||||
@ApiProperty()
|
@ApiProperty()
|
||||||
@Column({ type: 'timestamp', nullable: true })
|
@Column({ type: 'timestamp', nullable: true })
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import {
|
||||||
import { ApiProperty } from '@midwayjs/swagger';
|
import { ApiProperty } from '@midwayjs/swagger';
|
||||||
import { DictItem } from './dict_item.entity';
|
import { DictItem } from './dict_item.entity';
|
||||||
import { ProductStockComponent } from './product_stock_component.entity';
|
import { ProductStockComponent } from './product_stock_component.entity';
|
||||||
import { ProductSiteSku } from './product_site_sku.entity';
|
|
||||||
import { Category } from './category.entity';
|
import { Category } from './category.entity';
|
||||||
|
|
||||||
@Entity('product')
|
@Entity('product')
|
||||||
|
|
@ -77,7 +76,17 @@ export class Product {
|
||||||
@ManyToMany(() => DictItem, dictItem => dictItem.products, {
|
@ManyToMany(() => DictItem, dictItem => dictItem.products, {
|
||||||
cascade: true,
|
cascade: true,
|
||||||
})
|
})
|
||||||
@JoinTable()
|
@JoinTable({
|
||||||
|
name: 'product_attributes_dict_item',
|
||||||
|
joinColumn: {
|
||||||
|
name: 'productId',
|
||||||
|
referencedColumnName: 'id'
|
||||||
|
},
|
||||||
|
inverseJoinColumn: {
|
||||||
|
name: 'dictItemId',
|
||||||
|
referencedColumnName: 'id'
|
||||||
|
}
|
||||||
|
})
|
||||||
attributes: DictItem[];
|
attributes: DictItem[];
|
||||||
|
|
||||||
// 产品的库存组成,一对多关系(使用独立表)
|
// 产品的库存组成,一对多关系(使用独立表)
|
||||||
|
|
@ -85,9 +94,9 @@ export class Product {
|
||||||
@OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true })
|
@OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true })
|
||||||
components: ProductStockComponent[];
|
components: ProductStockComponent[];
|
||||||
|
|
||||||
@ApiProperty({ description: '站点 SKU 列表', type: ProductSiteSku, isArray: true })
|
@ApiProperty({ description: '站点 SKU 列表', type: 'string', isArray: true })
|
||||||
@OneToMany(() => ProductSiteSku, (siteSku) => siteSku.product, { cascade: true })
|
@Column({ type: 'simple-array' ,nullable:true})
|
||||||
siteSkus: ProductSiteSku[];
|
siteSkus: string[];
|
||||||
|
|
||||||
// 来源
|
// 来源
|
||||||
@ApiProperty({ description: '来源', example: '1' })
|
@ApiProperty({ description: '来源', example: '1' })
|
||||||
|
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import {
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
Entity,
|
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { ApiProperty } from '@midwayjs/swagger';
|
|
||||||
import { Product } from './product.entity';
|
|
||||||
|
|
||||||
@Entity('product_site_sku')
|
|
||||||
export class ProductSiteSku {
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '站点 SKU' })
|
|
||||||
@Column({ length: 100, comment: '站点 SKU' })
|
|
||||||
siteSku: string;
|
|
||||||
|
|
||||||
@ManyToOne(() => Product, product => product.siteSkus, {
|
|
||||||
onDelete: 'CASCADE',
|
|
||||||
})
|
|
||||||
@JoinColumn({ name: 'productId' })
|
|
||||||
product: Product;
|
|
||||||
|
|
||||||
@Column()
|
|
||||||
productId: number;
|
|
||||||
|
|
||||||
@CreateDateColumn()
|
|
||||||
createdAt: Date;
|
|
||||||
|
|
||||||
@UpdateDateColumn()
|
|
||||||
updatedAt: Date;
|
|
||||||
}
|
|
||||||
|
|
@ -38,10 +38,30 @@ export class Site {
|
||||||
isDisabled: boolean;
|
isDisabled: boolean;
|
||||||
|
|
||||||
@ManyToMany(() => Area)
|
@ManyToMany(() => Area)
|
||||||
@JoinTable()
|
@JoinTable({
|
||||||
|
name: 'site_areas_area',
|
||||||
|
joinColumn: {
|
||||||
|
name: 'siteId',
|
||||||
|
referencedColumnName: 'id'
|
||||||
|
},
|
||||||
|
inverseJoinColumn: {
|
||||||
|
name: 'areaId',
|
||||||
|
referencedColumnName: 'id'
|
||||||
|
}
|
||||||
|
})
|
||||||
areas: Area[];
|
areas: Area[];
|
||||||
|
|
||||||
@ManyToMany(() => StockPoint, stockPoint => stockPoint.sites)
|
@ManyToMany(() => StockPoint, stockPoint => stockPoint.sites)
|
||||||
@JoinTable()
|
@JoinTable({
|
||||||
|
name: 'site_stock_points_stock_point',
|
||||||
|
joinColumn: {
|
||||||
|
name: 'siteId',
|
||||||
|
referencedColumnName: 'id'
|
||||||
|
},
|
||||||
|
inverseJoinColumn: {
|
||||||
|
name: 'stockPointId',
|
||||||
|
referencedColumnName: 'id'
|
||||||
|
}
|
||||||
|
})
|
||||||
stockPoints: StockPoint[];
|
stockPoints: StockPoint[];
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +78,17 @@ export class StockPoint extends BaseEntity {
|
||||||
deletedAt: Date; // 软删除时间
|
deletedAt: Date; // 软删除时间
|
||||||
|
|
||||||
@ManyToMany(() => Area)
|
@ManyToMany(() => Area)
|
||||||
@JoinTable()
|
@JoinTable({
|
||||||
|
name: 'stock_point_areas_area',
|
||||||
|
joinColumn: {
|
||||||
|
name: 'stockPointId',
|
||||||
|
referencedColumnName: 'id'
|
||||||
|
},
|
||||||
|
inverseJoinColumn: {
|
||||||
|
name: 'areaId',
|
||||||
|
referencedColumnName: 'id'
|
||||||
|
}
|
||||||
|
})
|
||||||
areas: Area[];
|
areas: Area[];
|
||||||
|
|
||||||
@ManyToMany(() => Site, site => site.stockPoints)
|
@ManyToMany(() => Site, site => site.stockPoints)
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export class User {
|
||||||
@Column({ type: 'simple-array', nullable: true })
|
@Column({ type: 'simple-array', nullable: true })
|
||||||
permissions: string[]; // 自定义权限 (如:['user:add', 'user:edit'])
|
permissions: string[]; // 自定义权限 (如:['user:add', 'user:edit'])
|
||||||
|
|
||||||
// 新增邮箱字段,可选且唯一
|
// 邮箱字段,可选且唯一
|
||||||
@Column({ unique: true, nullable: true })
|
@Column({ unique: true, nullable: true })
|
||||||
email?: string;
|
email?: string;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,227 +0,0 @@
|
||||||
import { Site } from './site.entity';
|
|
||||||
import {
|
|
||||||
PrimaryGeneratedColumn,
|
|
||||||
Column,
|
|
||||||
CreateDateColumn,
|
|
||||||
UpdateDateColumn,
|
|
||||||
Unique,
|
|
||||||
Entity,
|
|
||||||
ManyToOne,
|
|
||||||
JoinColumn,
|
|
||||||
} from 'typeorm';
|
|
||||||
import { ApiProperty } from '@midwayjs/swagger';
|
|
||||||
import { ProductStatus, ProductStockStatus, ProductType } from '../enums/base.enum';
|
|
||||||
|
|
||||||
@Entity('wp_product')
|
|
||||||
@Unique(['siteId', 'externalProductId']) // 确保产品的唯一性
|
|
||||||
export class WpProduct {
|
|
||||||
@ApiProperty({
|
|
||||||
example: '1',
|
|
||||||
description: 'ID',
|
|
||||||
type: 'number',
|
|
||||||
required: true,
|
|
||||||
})
|
|
||||||
@PrimaryGeneratedColumn()
|
|
||||||
id: number;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
example: 1,
|
|
||||||
description: 'wp网站ID',
|
|
||||||
type: 'number',
|
|
||||||
required: true,
|
|
||||||
})
|
|
||||||
@Column({ type: 'int', nullable: true })
|
|
||||||
siteId: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '站点信息', type: Site })
|
|
||||||
@ManyToOne(() => Site)
|
|
||||||
@JoinColumn({ name: 'siteId', referencedColumnName: 'id' })
|
|
||||||
site: Site;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
example: '1',
|
|
||||||
description: 'wp产品ID',
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
})
|
|
||||||
@Column()
|
|
||||||
externalProductId: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '商店sku', type: 'string' })
|
|
||||||
@Column({ nullable: true })
|
|
||||||
sku?: string;
|
|
||||||
|
|
||||||
@ApiProperty({
|
|
||||||
example: 'ZYN 6MG WINTERGREEN',
|
|
||||||
description: '产品名称',
|
|
||||||
type: 'string',
|
|
||||||
required: true,
|
|
||||||
})
|
|
||||||
@Column()
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '产品状态', enum: ProductStatus })
|
|
||||||
@Column({ type: 'enum', enum: ProductStatus, comment: '产品状态: draft, pending, private, publish' })
|
|
||||||
status: ProductStatus;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '是否为特色产品', type: 'boolean' })
|
|
||||||
@Column({ default: false, comment: '是否为特色产品' })
|
|
||||||
featured: boolean;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '目录可见性', type: 'string' })
|
|
||||||
@Column({ default: 'visible', comment: '目录可见性: visible, catalog, search, hidden' })
|
|
||||||
catalog_visibility: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '产品描述', type: 'string' })
|
|
||||||
@Column({ type: 'text', nullable: true, comment: '产品描述' })
|
|
||||||
description: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '产品短描述', type: 'string' })
|
|
||||||
@Column({ type: 'text', nullable: true, comment: '产品短描述' })
|
|
||||||
short_description: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '上下架状态', enum: ProductStockStatus })
|
|
||||||
@Column({
|
|
||||||
name: 'stock_status',
|
|
||||||
type: 'enum',
|
|
||||||
enum: ProductStockStatus,
|
|
||||||
default: ProductStockStatus.INSTOCK,
|
|
||||||
comment: '库存状态: instock, outofstock, onbackorder',
|
|
||||||
})
|
|
||||||
stockStatus: ProductStockStatus;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '库存数量', type: 'number' })
|
|
||||||
@Column({ type: 'int', nullable: true, comment: '库存数量' })
|
|
||||||
stock_quantity: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '允许缺货下单', type: 'string' })
|
|
||||||
@Column({ nullable: true, comment: '允许缺货下单: no, notify, yes' })
|
|
||||||
backorders: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '是否单独出售', type: 'boolean' })
|
|
||||||
@Column({ default: false, comment: '是否单独出售' })
|
|
||||||
sold_individually: boolean;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '常规价格', type: Number })
|
|
||||||
@Column('decimal', { precision: 10, scale: 2, nullable: true, comment: '常规价格' })
|
|
||||||
regular_price: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '销售价格', type: Number })
|
|
||||||
@Column('decimal', { precision: 10, scale: 2, nullable: true, comment: '销售价格' })
|
|
||||||
sale_price: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '促销开始日期', type: 'datetime' })
|
|
||||||
@Column({ type: 'datetime', nullable: true, comment: '促销开始日期' })
|
|
||||||
date_on_sale_from: Date| null;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '促销结束日期', type: 'datetime' })
|
|
||||||
@Column({ type: 'datetime', nullable: true, comment: '促销结束日期' })
|
|
||||||
date_on_sale_to: Date|null;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '是否促销中', type: Boolean })
|
|
||||||
@Column({ nullable: true, type: 'boolean', comment: '是否促销中' })
|
|
||||||
on_sale: boolean;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '税务状态', type: 'string' })
|
|
||||||
@Column({ default: 'taxable', comment: '税务状态: taxable, shipping, none' })
|
|
||||||
tax_status: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '税类', type: 'string' })
|
|
||||||
@Column({ nullable: true, comment: '税类' })
|
|
||||||
tax_class: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '重量(g)', type: 'number' })
|
|
||||||
@Column('decimal', { precision: 10, scale: 2, nullable: true, comment: '重量(g)' })
|
|
||||||
weight: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '尺寸(长宽高)', type: 'json' })
|
|
||||||
@Column({ type: 'json', nullable: true, comment: '尺寸' })
|
|
||||||
dimensions: { length: string; width: string; height: string };
|
|
||||||
|
|
||||||
@ApiProperty({ description: '允许评论', type: 'boolean' })
|
|
||||||
@Column({ default: true, comment: '允许客户评论' })
|
|
||||||
reviews_allowed: boolean;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '购买备注', type: 'string' })
|
|
||||||
@Column({ nullable: true, comment: '购买备注' })
|
|
||||||
purchase_note: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '菜单排序', type: 'number' })
|
|
||||||
@Column({ default: 0, comment: '菜单排序' })
|
|
||||||
menu_order: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '产品类型', enum: ProductType })
|
|
||||||
@Column({ type: 'enum', enum: ProductType, comment: '产品类型: simple, grouped, external, variable' })
|
|
||||||
type: ProductType;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '父产品ID', type: 'number' })
|
|
||||||
@Column({ default: 0, comment: '父产品ID' })
|
|
||||||
parent_id: number;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '外部产品URL', type: 'string' })
|
|
||||||
@Column({ type: 'text', nullable: true, comment: '外部产品URL' })
|
|
||||||
external_url: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '外部产品按钮文本', type: 'string' })
|
|
||||||
@Column({ nullable: true, comment: '外部产品按钮文本' })
|
|
||||||
button_text: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '分组产品', type: 'json' })
|
|
||||||
@Column({ type: 'json', nullable: true, comment: '分组产品' })
|
|
||||||
grouped_products: number[];
|
|
||||||
|
|
||||||
@ApiProperty({ description: '追加销售', type: 'json' })
|
|
||||||
@Column({ type: 'json', nullable: true, comment: '追加销售' })
|
|
||||||
upsell_ids: number[];
|
|
||||||
|
|
||||||
@ApiProperty({ description: '交叉销售', type: 'json' })
|
|
||||||
@Column({ type: 'json', nullable: true, comment: '交叉销售' })
|
|
||||||
cross_sell_ids: number[];
|
|
||||||
|
|
||||||
@ApiProperty({ description: '分类', type: 'json' })
|
|
||||||
@Column({ type: 'json', nullable: true, comment: '分类' })
|
|
||||||
categories: { id: number; name: string; slug: string }[];
|
|
||||||
|
|
||||||
@ApiProperty({ description: '标签', type: 'json' })
|
|
||||||
@Column({ type: 'json', nullable: true, comment: '标签' })
|
|
||||||
tags: { id: number; name: string; slug: string }[];
|
|
||||||
|
|
||||||
@ApiProperty({ description: '图片', type: 'json' })
|
|
||||||
@Column({ type: 'json', nullable: true, comment: '图片' })
|
|
||||||
images: { id: number; src: string; name: string; alt: string }[];
|
|
||||||
|
|
||||||
@ApiProperty({ description: '产品属性', type: 'json' })
|
|
||||||
@Column({ type: 'json', nullable: true, comment: '产品属性' })
|
|
||||||
attributes: { id: number; name: string; position: number; visible: boolean; variation: boolean; options: string[] }[];
|
|
||||||
|
|
||||||
@ApiProperty({ description: '默认属性', type: 'json' })
|
|
||||||
@Column({ type: 'json', nullable: true, comment: '默认属性' })
|
|
||||||
default_attributes: { id: number; name: string; option: string }[];
|
|
||||||
|
|
||||||
@ApiProperty({ description: 'GTIN', type: 'string' })
|
|
||||||
@Column({ nullable: true, comment: 'GTIN, UPC, EAN, or ISBN' })
|
|
||||||
gtin: string;
|
|
||||||
|
|
||||||
@ApiProperty({ description: '是否删除', type: 'boolean' })
|
|
||||||
@Column({ nullable: true, type: 'boolean', default: false, comment: '是否删除' })
|
|
||||||
on_delete: boolean;
|
|
||||||
|
|
||||||
@Column({ type: 'json', nullable: true })
|
|
||||||
metadata: Record<string, any>; // 产品的其他扩展字段
|
|
||||||
|
|
||||||
@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;
|
|
||||||
}
|
|
||||||
|
|
@ -107,9 +107,9 @@ export interface IPlatformService {
|
||||||
* @param productId 产品ID
|
* @param productId 产品ID
|
||||||
* @param variationId 变体ID
|
* @param variationId 变体ID
|
||||||
* @param data 更新数据
|
* @param data 更新数据
|
||||||
* @returns 更新结果
|
* @returns 更新后的变体数据
|
||||||
*/
|
*/
|
||||||
updateVariation(site: any, productId: string, variationId: string, data: any): Promise<boolean>;
|
updateVariation(site: any, productId: string, variationId: string, data: any): Promise<any>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新订单
|
* 更新订单
|
||||||
|
|
@ -121,22 +121,22 @@ export interface IPlatformService {
|
||||||
updateOrder(site: any, orderId: string, data: Record<string, any>): Promise<boolean>;
|
updateOrder(site: any, orderId: string, data: Record<string, any>): Promise<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建物流信息
|
* 创建履约信息
|
||||||
* @param site 站点配置信息
|
* @param site 站点配置信息
|
||||||
* @param orderId 订单ID
|
* @param orderId 订单ID
|
||||||
* @param data 物流数据
|
* @param data 履约数据
|
||||||
* @returns 创建结果
|
* @returns 创建结果
|
||||||
*/
|
*/
|
||||||
createShipment(site: any, orderId: string, data: any): Promise<any>;
|
createFulfillment(site: any, orderId: string, data: any): Promise<any>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除物流信息
|
* 删除履约信息
|
||||||
* @param site 站点配置信息
|
* @param site 站点配置信息
|
||||||
* @param orderId 订单ID
|
* @param orderId 订单ID
|
||||||
* @param trackingId 物流跟踪ID
|
* @param fulfillmentId 履约跟踪ID
|
||||||
* @returns 删除结果
|
* @returns 删除结果
|
||||||
*/
|
*/
|
||||||
deleteShipment(site: any, orderId: string, trackingId: string): Promise<boolean>;
|
deleteFulfillment(site: any, orderId: string, fulfillmentId: string): Promise<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量处理产品
|
* 批量处理产品
|
||||||
|
|
|
||||||
|
|
@ -3,17 +3,20 @@ import {
|
||||||
UpdateReviewDTO,
|
UpdateReviewDTO,
|
||||||
UnifiedMediaDTO,
|
UnifiedMediaDTO,
|
||||||
UnifiedOrderDTO,
|
UnifiedOrderDTO,
|
||||||
UnifiedPaginationDTO,
|
|
||||||
UnifiedProductDTO,
|
UnifiedProductDTO,
|
||||||
UnifiedReviewDTO,
|
UnifiedReviewDTO,
|
||||||
UnifiedSearchParamsDTO,
|
|
||||||
UnifiedSubscriptionDTO,
|
UnifiedSubscriptionDTO,
|
||||||
UnifiedCustomerDTO,
|
UnifiedCustomerDTO,
|
||||||
UnifiedWebhookDTO,
|
UnifiedWebhookDTO,
|
||||||
UnifiedWebhookPaginationDTO,
|
UnifiedWebhookPaginationDTO,
|
||||||
CreateWebhookDTO,
|
CreateWebhookDTO,
|
||||||
UpdateWebhookDTO,
|
UpdateWebhookDTO,
|
||||||
|
CreateVariationDTO,
|
||||||
|
UpdateVariationDTO,
|
||||||
|
UnifiedProductVariationDTO,
|
||||||
|
UnifiedVariationPaginationDTO,
|
||||||
} from '../dto/site-api.dto';
|
} from '../dto/site-api.dto';
|
||||||
|
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto';
|
||||||
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||||
|
|
||||||
export interface ISiteAdapter {
|
export interface ISiteAdapter {
|
||||||
|
|
@ -107,10 +110,40 @@ export interface ISiteAdapter {
|
||||||
*/
|
*/
|
||||||
updateProduct(id: string | number, data: Partial<UnifiedProductDTO>): Promise<boolean>;
|
updateProduct(id: string | number, data: Partial<UnifiedProductDTO>): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除产品
|
||||||
|
*/
|
||||||
|
deleteProduct(id: string | number): Promise<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取产品变体列表
|
||||||
|
*/
|
||||||
|
getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise<UnifiedVariationPaginationDTO>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有产品变体
|
||||||
|
*/
|
||||||
|
getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise<UnifiedProductVariationDTO[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取单个产品变体
|
||||||
|
*/
|
||||||
|
getVariation(productId: string | number, variationId: string | number): Promise<UnifiedProductVariationDTO>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建产品变体
|
||||||
|
*/
|
||||||
|
createVariation(productId: string | number, data: CreateVariationDTO): Promise<UnifiedProductVariationDTO>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新产品变体
|
* 更新产品变体
|
||||||
*/
|
*/
|
||||||
updateVariation(productId: string | number, variationId: string | number, data: any): Promise<any>;
|
updateVariation(productId: string | number, variationId: string | number, data: UpdateVariationDTO): Promise<UnifiedProductVariationDTO>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除产品变体
|
||||||
|
*/
|
||||||
|
deleteVariation(productId: string | number, variationId: string | number): Promise<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取订单备注
|
* 获取订单备注
|
||||||
|
|
@ -122,11 +155,6 @@ export interface ISiteAdapter {
|
||||||
*/
|
*/
|
||||||
createOrderNote(orderId: string | number, data: any): Promise<any>;
|
createOrderNote(orderId: string | number, data: any): Promise<any>;
|
||||||
|
|
||||||
/**
|
|
||||||
* 删除产品
|
|
||||||
*/
|
|
||||||
deleteProduct(id: string | number): Promise<boolean>;
|
|
||||||
|
|
||||||
batchProcessProducts?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
|
batchProcessProducts?(data: BatchOperationDTO): Promise<BatchOperationResultDTO>;
|
||||||
|
|
||||||
createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO>;
|
createOrder(data: Partial<UnifiedOrderDTO>): Promise<UnifiedOrderDTO>;
|
||||||
|
|
@ -180,9 +208,9 @@ export interface ISiteAdapter {
|
||||||
getLinks(): Promise<Array<{title: string, url: string}>>;
|
getLinks(): Promise<Array<{title: string, url: string}>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 订单发货
|
* 订单履行(发货)
|
||||||
*/
|
*/
|
||||||
shipOrder(orderId: string | number, data: {
|
fulfillOrder(orderId: string | number, data: {
|
||||||
tracking_number?: string;
|
tracking_number?: string;
|
||||||
shipping_provider?: string;
|
shipping_provider?: string;
|
||||||
shipping_method?: string;
|
shipping_method?: string;
|
||||||
|
|
@ -193,10 +221,41 @@ export interface ISiteAdapter {
|
||||||
}): Promise<any>;
|
}): Promise<any>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取消订单发货
|
* 取消订单履行
|
||||||
*/
|
*/
|
||||||
cancelShipOrder(orderId: string | number, data: {
|
cancelFulfillment(orderId: string | number, data: {
|
||||||
reason?: string;
|
reason?: string;
|
||||||
shipment_id?: string;
|
shipment_id?: string;
|
||||||
}): Promise<any>;
|
}): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单履行信息
|
||||||
|
*/
|
||||||
|
getOrderFulfillments(orderId: string | number): Promise<any[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建订单履行信息
|
||||||
|
*/
|
||||||
|
createOrderFulfillment(orderId: string | number, data: {
|
||||||
|
tracking_number: string;
|
||||||
|
tracking_provider: string;
|
||||||
|
date_shipped?: string;
|
||||||
|
status_shipped?: string;
|
||||||
|
items?: any[];
|
||||||
|
}): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新订单履行信息
|
||||||
|
*/
|
||||||
|
updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: {
|
||||||
|
tracking_number?: string;
|
||||||
|
tracking_provider?: string;
|
||||||
|
date_shipped?: string;
|
||||||
|
status_shipped?: string;
|
||||||
|
}): Promise<any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除订单履行信息
|
||||||
|
*/
|
||||||
|
deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import { Provide, Inject } from '@midwayjs/core';
|
import { Inject, Provide } from '@midwayjs/core';
|
||||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
import { Order } from '../entity/order.entity';
|
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { CustomerTag } from '../entity/customer_tag.entity';
|
import { SyncOperationResult, UnifiedPaginationDTO, UnifiedSearchParamsDTO, BatchOperationResult } from '../dto/api.dto';
|
||||||
|
import { UnifiedCustomerDTO } from '../dto/site-api.dto';
|
||||||
import { Customer } from '../entity/customer.entity';
|
import { Customer } from '../entity/customer.entity';
|
||||||
|
import { CustomerTag } from '../entity/customer_tag.entity';
|
||||||
|
import { Order } from '../entity/order.entity';
|
||||||
import { SiteApiService } from './site-api.service';
|
import { SiteApiService } from './site-api.service';
|
||||||
import { UnifiedCustomerDTO, UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/site-api.dto';
|
import { CreateCustomerDTO, CustomerDTO, CustomerStatisticDTO, CustomerStatisticQueryParamsDTO } from '../dto/customer.dto';
|
||||||
import { SyncOperationResult, BatchErrorItem } from '../dto/batch.dto';
|
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
export class CustomerService {
|
export class CustomerService {
|
||||||
|
|
@ -33,10 +34,12 @@ export class CustomerService {
|
||||||
* 将站点客户数据映射为本地客户实体数据
|
* 将站点客户数据映射为本地客户实体数据
|
||||||
* 处理字段映射和数据转换,确保所有字段正确同步
|
* 处理字段映射和数据转换,确保所有字段正确同步
|
||||||
*/
|
*/
|
||||||
private mapSiteCustomerToCustomer(siteCustomer: UnifiedCustomerDTO, siteId: number): Partial<Customer> {
|
private mapSiteCustomerToCustomer(siteCustomer: UnifiedCustomerDTO, siteId: number): Partial<CreateCustomerDTO> {
|
||||||
return {
|
return {
|
||||||
site_id: siteId, // 使用站点ID而不是客户ID
|
site_id: siteId, // 使用站点ID而不是客户ID
|
||||||
origin_id: "" + siteCustomer.id,
|
site_created_at: this.parseDate(siteCustomer.date_created),
|
||||||
|
site_updated_at: this.parseDate(siteCustomer.date_modified),
|
||||||
|
origin_id: Number(siteCustomer.id),
|
||||||
email: siteCustomer.email,
|
email: siteCustomer.email,
|
||||||
first_name: siteCustomer.first_name,
|
first_name: siteCustomer.first_name,
|
||||||
last_name: siteCustomer.last_name,
|
last_name: siteCustomer.last_name,
|
||||||
|
|
@ -47,8 +50,6 @@ export class CustomerService {
|
||||||
billing: siteCustomer.billing,
|
billing: siteCustomer.billing,
|
||||||
shipping: siteCustomer.shipping,
|
shipping: siteCustomer.shipping,
|
||||||
raw: siteCustomer.raw || siteCustomer,
|
raw: siteCustomer.raw || siteCustomer,
|
||||||
site_created_at: this.parseDate(siteCustomer.date_created),
|
|
||||||
site_updated_at: this.parseDate(siteCustomer.date_modified)
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -120,18 +121,12 @@ export class CustomerService {
|
||||||
*/
|
*/
|
||||||
async upsertManyCustomers(
|
async upsertManyCustomers(
|
||||||
customersData: Array<Partial<Customer>>
|
customersData: Array<Partial<Customer>>
|
||||||
): Promise<{
|
): Promise<BatchOperationResult> {
|
||||||
customers: Customer[];
|
|
||||||
created: number;
|
|
||||||
updated: number;
|
|
||||||
processed: number;
|
|
||||||
errors: BatchErrorItem[];
|
|
||||||
}> {
|
|
||||||
const results = {
|
const results = {
|
||||||
customers: [],
|
total: customersData.length,
|
||||||
|
processed: 0,
|
||||||
created: 0,
|
created: 0,
|
||||||
updated: 0,
|
updated: 0,
|
||||||
processed: 0,
|
|
||||||
errors: []
|
errors: []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -139,7 +134,6 @@ export class CustomerService {
|
||||||
for (const customerData of customersData) {
|
for (const customerData of customersData) {
|
||||||
try {
|
try {
|
||||||
const result = await this.upsertCustomer(customerData);
|
const result = await this.upsertCustomer(customerData);
|
||||||
results.customers.push(result.customer);
|
|
||||||
|
|
||||||
if (result.isCreated) {
|
if (result.isCreated) {
|
||||||
results.created++;
|
results.created++;
|
||||||
|
|
@ -153,6 +147,7 @@ export class CustomerService {
|
||||||
identifier: customerData.email || String(customerData.id) || 'unknown',
|
identifier: customerData.email || String(customerData.id) || 'unknown',
|
||||||
error: error.message
|
error: error.message
|
||||||
});
|
});
|
||||||
|
results.processed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -176,14 +171,17 @@ export class CustomerService {
|
||||||
// 第二步:将站点客户数据转换为客户实体数据
|
// 第二步:将站点客户数据转换为客户实体数据
|
||||||
const customersData = siteCustomers.map(siteCustomer => {
|
const customersData = siteCustomers.map(siteCustomer => {
|
||||||
return this.mapSiteCustomerToCustomer(siteCustomer, siteId);
|
return this.mapSiteCustomerToCustomer(siteCustomer, siteId);
|
||||||
});
|
}).map(customer => ({
|
||||||
|
...customer,
|
||||||
|
origin_id: String(customer.origin_id),
|
||||||
|
}));
|
||||||
|
|
||||||
// 第三步:批量upsert客户数据
|
// 第三步:批量upsert客户数据
|
||||||
const upsertResult = await this.upsertManyCustomers(customersData);
|
const upsertResult = await this.upsertManyCustomers(customersData);
|
||||||
return {
|
return {
|
||||||
total: siteCustomers.length,
|
total: siteCustomers.length,
|
||||||
processed: upsertResult.customers.length,
|
processed: upsertResult.processed,
|
||||||
synced: upsertResult.customers.length,
|
synced: upsertResult.processed,
|
||||||
updated: upsertResult.updated,
|
updated: upsertResult.updated,
|
||||||
created: upsertResult.created,
|
created: upsertResult.created,
|
||||||
errors: upsertResult.errors
|
errors: upsertResult.errors
|
||||||
|
|
@ -195,48 +193,58 @@ export class CustomerService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCustomerStatisticList(param: Record<string, any>) {
|
/**
|
||||||
|
* 获取客户统计列表(包含订单统计信息)
|
||||||
|
* 支持分页、搜索和排序功能
|
||||||
|
* 使用原生SQL查询实现复杂的统计逻辑
|
||||||
|
*/
|
||||||
|
async getCustomerStatisticList(param: CustomerStatisticQueryParamsDTO): Promise<{
|
||||||
|
items: CustomerStatisticDTO[];
|
||||||
|
total: number;
|
||||||
|
current: number;
|
||||||
|
pageSize: number;
|
||||||
|
}> {
|
||||||
const {
|
const {
|
||||||
current = 1,
|
page = 1,
|
||||||
pageSize = 10,
|
per_page = 10,
|
||||||
email,
|
search,
|
||||||
tags,
|
where,
|
||||||
sorterKey,
|
orderBy,
|
||||||
sorterValue,
|
|
||||||
state,
|
|
||||||
first_purchase_date,
|
|
||||||
customerId,
|
|
||||||
rate,
|
|
||||||
} = param;
|
} = param;
|
||||||
|
|
||||||
|
// 将page和per_page转换为current和pageSize
|
||||||
|
const current = page;
|
||||||
|
const pageSize = per_page;
|
||||||
|
|
||||||
const whereConds: string[] = [];
|
const whereConds: string[] = [];
|
||||||
const havingConds: string[] = [];
|
const havingConds: string[] = [];
|
||||||
|
|
||||||
// 邮箱搜索
|
// 全局搜索关键词
|
||||||
if (email) {
|
if (search) {
|
||||||
whereConds.push(`o.customer_email LIKE '%${email}%'`);
|
whereConds.push(`o.customer_email LIKE '%${search}%'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 省份搜索
|
// where条件过滤
|
||||||
if (state) {
|
if (where) {
|
||||||
whereConds.push(
|
// 邮箱搜索
|
||||||
`JSON_UNQUOTE(JSON_EXTRACT(o.billing, '$.state')) = '${state}'`
|
if (where.email) {
|
||||||
);
|
whereConds.push(`o.customer_email LIKE '%${where.email}%'`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// customerId 过滤
|
// customerId 过滤
|
||||||
if (customerId) {
|
if (where.customerId) {
|
||||||
whereConds.push(`c.id = ${Number(customerId)}`);
|
whereConds.push(`c.id = ${Number(where.customerId)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// rate 过滤
|
// rate 过滤
|
||||||
if (rate) {
|
if (where.rate) {
|
||||||
whereConds.push(`c.rate = ${Number(rate)}`);
|
whereConds.push(`c.rate = ${Number(where.rate)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// tags 过滤
|
// tags 过滤
|
||||||
if (tags) {
|
if (where.tags) {
|
||||||
const tagList = tags
|
const tagList = where.tags
|
||||||
.split(',')
|
.split(',')
|
||||||
.map(tag => `'${tag.trim()}'`)
|
.map(tag => `'${tag.trim()}'`)
|
||||||
.join(',');
|
.join(',');
|
||||||
|
|
@ -250,11 +258,12 @@ export class CustomerService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 首次购买时间过滤
|
// 首次购买时间过滤
|
||||||
if (first_purchase_date) {
|
if (where.first_purchase_date) {
|
||||||
havingConds.push(
|
havingConds.push(
|
||||||
`DATE_FORMAT(MIN(o.date_paid), '%Y-%m') = '${first_purchase_date}'`
|
`DATE_FORMAT(MIN(o.date_paid), '%Y-%m') = '${where.first_purchase_date}'`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 公用过滤
|
// 公用过滤
|
||||||
const baseQuery = `
|
const baseQuery = `
|
||||||
|
|
@ -263,6 +272,22 @@ export class CustomerService {
|
||||||
${havingConds.length ? `HAVING ${havingConds.join(' AND ')}` : ''}
|
${havingConds.length ? `HAVING ${havingConds.join(' AND ')}` : ''}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// 排序处理
|
||||||
|
let orderByClause = '';
|
||||||
|
if (orderBy) {
|
||||||
|
if (typeof orderBy === 'string') {
|
||||||
|
const [field, direction] = orderBy.split(':');
|
||||||
|
orderByClause = `ORDER BY ${field} ${direction === 'desc' ? 'DESC' : 'ASC'}`;
|
||||||
|
} else if (typeof orderBy === 'object') {
|
||||||
|
const orderClauses = Object.entries(orderBy).map(([field, direction]) =>
|
||||||
|
`${field} ${direction === 'desc' ? 'DESC' : 'ASC'}`
|
||||||
|
);
|
||||||
|
orderByClause = `ORDER BY ${orderClauses.join(', ')}`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
orderByClause = 'ORDER BY orders ASC, yoone_total DESC';
|
||||||
|
}
|
||||||
|
|
||||||
// 主查询
|
// 主查询
|
||||||
const sql = `
|
const sql = `
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -296,9 +321,7 @@ export class CustomerService {
|
||||||
GROUP BY oo.customer_email
|
GROUP BY oo.customer_email
|
||||||
) yoone_stats ON yoone_stats.customer_email = o.customer_email
|
) yoone_stats ON yoone_stats.customer_email = o.customer_email
|
||||||
${baseQuery}
|
${baseQuery}
|
||||||
${sorterKey
|
${orderByClause}
|
||||||
? `ORDER BY ${sorterKey} ${sorterValue === 'descend' ? 'DESC' : 'ASC'}`
|
|
||||||
: 'ORDER BY orders ASC, yoone_total DESC'}
|
|
||||||
LIMIT ${pageSize} OFFSET ${(current - 1) * pageSize}
|
LIMIT ${pageSize} OFFSET ${(current - 1) * pageSize}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
@ -319,8 +342,22 @@ export class CustomerService {
|
||||||
|
|
||||||
const total = countResult[0]?.total || 0;
|
const total = countResult[0]?.total || 0;
|
||||||
|
|
||||||
|
// 处理tags字段,将JSON字符串转换为数组
|
||||||
|
const processedItems = items.map(item => {
|
||||||
|
if (item.tags) {
|
||||||
|
try {
|
||||||
|
item.tags = JSON.parse(item.tags);
|
||||||
|
} catch (e) {
|
||||||
|
item.tags = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.tags = [];
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items,
|
items: processedItems,
|
||||||
total,
|
total,
|
||||||
current,
|
current,
|
||||||
pageSize,
|
pageSize,
|
||||||
|
|
@ -332,104 +369,56 @@ export class CustomerService {
|
||||||
* 支持基本的分页、搜索和排序功能
|
* 支持基本的分页、搜索和排序功能
|
||||||
* 使用TypeORM查询构建器实现
|
* 使用TypeORM查询构建器实现
|
||||||
*/
|
*/
|
||||||
async getCustomerList(param: Record<string, any>): Promise<UnifiedPaginationDTO<any>>{
|
async getCustomerList(params: UnifiedSearchParamsDTO): Promise<UnifiedPaginationDTO<CustomerDTO>>{
|
||||||
const {
|
const {
|
||||||
current = 1,
|
page = 1,
|
||||||
pageSize = 10,
|
per_page = 20,
|
||||||
email,
|
where ={},
|
||||||
firstName,
|
} = params;
|
||||||
lastName,
|
|
||||||
phone,
|
|
||||||
state,
|
|
||||||
rate,
|
|
||||||
sorterKey,
|
|
||||||
sorterValue,
|
|
||||||
} = param;
|
|
||||||
|
|
||||||
// 创建查询构建器
|
// 查询客户列表和总数
|
||||||
const queryBuilder = this.customerModel
|
const [customers, total] = await this.customerModel.findAndCount({
|
||||||
.createQueryBuilder('c')
|
where,
|
||||||
.leftJoinAndSelect(
|
// order: orderBy,
|
||||||
'customer_tag',
|
skip: (page - 1) * per_page,
|
||||||
'ct',
|
take: per_page,
|
||||||
'ct.email = c.email'
|
|
||||||
)
|
|
||||||
.select([
|
|
||||||
'c.id',
|
|
||||||
'c.email',
|
|
||||||
'c.first_name',
|
|
||||||
'c.last_name',
|
|
||||||
'c.fullname',
|
|
||||||
'c.username',
|
|
||||||
'c.phone',
|
|
||||||
'c.avatar',
|
|
||||||
'c.billing',
|
|
||||||
'c.shipping',
|
|
||||||
'c.rate',
|
|
||||||
'c.site_id',
|
|
||||||
'c.created_at',
|
|
||||||
'c.updated_at',
|
|
||||||
'c.site_created_at',
|
|
||||||
'c.site_updated_at'
|
|
||||||
])
|
|
||||||
.groupBy('c.id');
|
|
||||||
|
|
||||||
// 邮箱搜索
|
|
||||||
if (email) {
|
|
||||||
queryBuilder.andWhere('c.email LIKE :email', { email: `%${email}%` });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 姓名搜索
|
|
||||||
if (firstName) {
|
|
||||||
queryBuilder.andWhere('c.first_name LIKE :firstName', { firstName: `%${firstName}%` });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastName) {
|
|
||||||
queryBuilder.andWhere('c.last_name LIKE :lastName', { lastName: `%${lastName}%` });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 电话搜索
|
|
||||||
if (phone) {
|
|
||||||
queryBuilder.andWhere('c.phone LIKE :phone', { phone: `%${phone}%` });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 省份搜索
|
|
||||||
if (state) {
|
|
||||||
queryBuilder.andWhere("JSON_UNQUOTE(JSON_EXTRACT(c.billing, '$.state')) = :state", { state });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 评分过滤
|
|
||||||
if (rate !== undefined && rate !== null) {
|
|
||||||
queryBuilder.andWhere('c.rate = :rate', { rate: Number(rate) });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排序处理
|
|
||||||
if (sorterKey) {
|
|
||||||
const order = sorterValue === 'descend' ? 'DESC' : 'ASC';
|
|
||||||
queryBuilder.orderBy(`c.${sorterKey}`, order);
|
|
||||||
} else {
|
|
||||||
queryBuilder.orderBy('c.created_at', 'DESC');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页
|
|
||||||
queryBuilder.skip((current - 1) * pageSize).take(pageSize);
|
|
||||||
|
|
||||||
// 执行查询
|
|
||||||
const [items, total] = await queryBuilder.getManyAndCount();
|
|
||||||
|
|
||||||
// 处理tags字段,将逗号分隔的字符串转换为数组
|
|
||||||
const processedItems = items.map(item => {
|
|
||||||
const plainItem = JSON.parse(JSON.stringify(item));
|
|
||||||
plainItem.tags = plainItem.tags ? plainItem.tags.split(',').filter(tag => tag) : [];
|
|
||||||
return plainItem;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取所有客户的邮箱列表
|
||||||
|
const emailList = customers.map(customer => customer.email);
|
||||||
|
|
||||||
|
// 查询所有客户的标签
|
||||||
|
let customerTagsMap: Record<string, string[]> = {};
|
||||||
|
if (emailList.length > 0) {
|
||||||
|
const customerTags = await this.customerTagModel
|
||||||
|
.createQueryBuilder('tag')
|
||||||
|
.select('tag.email', 'email')
|
||||||
|
.addSelect('tag.tag', 'tag')
|
||||||
|
.where('tag.email IN (:...emailList)', { emailList })
|
||||||
|
.getRawMany();
|
||||||
|
|
||||||
|
// 将标签按邮箱分组
|
||||||
|
customerTagsMap = customerTags.reduce((acc, item) => {
|
||||||
|
if (!acc[item.email]) {
|
||||||
|
acc[item.email] = [];
|
||||||
|
}
|
||||||
|
acc[item.email].push(item.tag);
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, string[]>);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将标签合并到客户数据中
|
||||||
|
const customersWithTags = customers.map(customer => ({
|
||||||
|
...customer,
|
||||||
|
tags: customerTagsMap[customer.email] || []
|
||||||
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
items: processedItems,
|
items: customersWithTags,
|
||||||
total,
|
total,
|
||||||
page: current,
|
page,
|
||||||
per_page: pageSize,
|
per_page,
|
||||||
totalPages: Math.ceil(total / pageSize),
|
totalPages: Math.ceil(total / per_page),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -457,4 +446,84 @@ export class CustomerService {
|
||||||
async setRate(params: { id: number; rate: number }) {
|
async setRate(params: { id: number; rate: number }) {
|
||||||
return await this.customerModel.update(params.id, { rate: params.rate });
|
return await this.customerModel.update(params.id, { rate: params.rate });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新客户
|
||||||
|
* 每个客户可以有独立的更新字段
|
||||||
|
* 支持对多个客户进行统一化修改或分别更新
|
||||||
|
*/
|
||||||
|
async batchUpdateCustomers(
|
||||||
|
updateItems: Array<{ id: number; update_data: Partial<Customer> }>
|
||||||
|
): Promise<BatchOperationResult> {
|
||||||
|
const results = {
|
||||||
|
total: updateItems.length,
|
||||||
|
processed: 0,
|
||||||
|
updated: 0,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// 批量处理每个客户的更新
|
||||||
|
for (const item of updateItems) {
|
||||||
|
try {
|
||||||
|
// 检查客户是否存在
|
||||||
|
const existingCustomer = await this.customerModel.findOne({ where: { id: item.id } });
|
||||||
|
if (!existingCustomer) {
|
||||||
|
throw new Error(`客户ID ${item.id} 不存在`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新客户信息
|
||||||
|
await this.updateCustomer(item.id, item.update_data);
|
||||||
|
results.updated++;
|
||||||
|
results.processed++;
|
||||||
|
} catch (error) {
|
||||||
|
// 记录错误但不中断整个批量操作
|
||||||
|
results.errors.push({
|
||||||
|
identifier: String(item.id),
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
results.processed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除客户
|
||||||
|
* 支持对多个客户进行批量删除操作
|
||||||
|
* 返回操作结果,包括成功和失败的数量
|
||||||
|
*/
|
||||||
|
async batchDeleteCustomers(ids: number[]): Promise<BatchOperationResult> {
|
||||||
|
const results = {
|
||||||
|
total: ids.length,
|
||||||
|
processed: 0,
|
||||||
|
updated: 0,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// 批量处理每个客户的删除
|
||||||
|
for (const id of ids) {
|
||||||
|
try {
|
||||||
|
// 检查客户是否存在
|
||||||
|
const existingCustomer = await this.customerModel.findOne({ where: { id } });
|
||||||
|
if (!existingCustomer) {
|
||||||
|
throw new Error(`客户ID ${id} 不存在`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除客户
|
||||||
|
await this.customerModel.delete(id);
|
||||||
|
results.updated++;
|
||||||
|
results.processed++;
|
||||||
|
} catch (error) {
|
||||||
|
// 记录错误但不中断整个批量操作
|
||||||
|
results.errors.push({
|
||||||
|
identifier: String(id),
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
results.processed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6,6 +6,19 @@ import { DictItem } from '../entity/dict_item.entity';
|
||||||
import { CreateDictDTO, UpdateDictDTO } from '../dto/dict.dto';
|
import { CreateDictDTO, UpdateDictDTO } from '../dto/dict.dto';
|
||||||
import { CreateDictItemDTO, UpdateDictItemDTO } from '../dto/dict.dto';
|
import { CreateDictItemDTO, UpdateDictItemDTO } from '../dto/dict.dto';
|
||||||
import * as xlsx from 'xlsx';
|
import * as xlsx from 'xlsx';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { BatchOperationResultDTO } from '../dto/api.dto';
|
||||||
|
|
||||||
|
// 定义 Excel 行数据的类型接口
|
||||||
|
interface ExcelRow {
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
titleCN?: string;
|
||||||
|
value?: string;
|
||||||
|
image?: string;
|
||||||
|
shortName?: string;
|
||||||
|
sort?: number;
|
||||||
|
}
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
export class DictService {
|
export class DictService {
|
||||||
|
|
@ -37,17 +50,27 @@ export class DictService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从XLSX文件导入字典
|
// 从XLSX文件导入字典
|
||||||
async importDictsFromXLSX(buffer: Buffer) {
|
async importDictsFromXLSX(bufferOrPath: Buffer | string) {
|
||||||
|
// 判断传入的是 Buffer 还是文件路径字符串
|
||||||
|
let buffer: Buffer;
|
||||||
|
if (typeof bufferOrPath === 'string') {
|
||||||
|
// 如果是文件路径,读取文件内容
|
||||||
|
buffer = fs.readFileSync(bufferOrPath);
|
||||||
|
} else {
|
||||||
|
// 如果是 Buffer,直接使用
|
||||||
|
buffer = bufferOrPath;
|
||||||
|
}
|
||||||
|
|
||||||
// 读取缓冲区中的工作簿
|
// 读取缓冲区中的工作簿
|
||||||
const wb = xlsx.read(buffer, { type: 'buffer' });
|
const wb = xlsx.read(buffer, { type: 'buffer' });
|
||||||
// 获取第一个工作表的名称
|
// 获取第一个工作表的名称
|
||||||
const wsname = wb.SheetNames[0];
|
const wsname = wb.SheetNames[0];
|
||||||
// 获取第一个工作表
|
// 获取第一个工作表
|
||||||
const ws = wb.Sheets[wsname];
|
const ws = wb.Sheets[wsname];
|
||||||
// 将工作表转换为JSON对象数组
|
// 将工作表转换为JSON对象数组,xlsx会自动将第一行作为表头
|
||||||
const data = xlsx.utils.sheet_to_json(ws, { header: ['name', 'title'] }).slice(1);
|
const data = xlsx.utils.sheet_to_json(ws) as { name: string; title: string }[];
|
||||||
// 创建要保存的字典实体数组
|
// 创建要保存的字典实体数组
|
||||||
const dicts = data.map((row: any) => {
|
const dicts = data.map((row: { name: string; title: string }) => {
|
||||||
const dict = new Dict();
|
const dict = new Dict();
|
||||||
dict.name = this.formatName(row.name);
|
dict.name = this.formatName(row.name);
|
||||||
dict.title = row.title;
|
dict.title = row.title;
|
||||||
|
|
@ -69,32 +92,71 @@ export class DictService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从XLSX文件导入字典项
|
// 从XLSX文件导入字典项
|
||||||
async importDictItemsFromXLSX(buffer: Buffer, dictId: number) {
|
async importDictItemsFromXLSX(bufferOrPath: Buffer | string, dictId: number): Promise<BatchOperationResultDTO> {
|
||||||
|
if(!dictId){
|
||||||
|
throw new Error("引入失败, 请输入字典 ID")
|
||||||
|
}
|
||||||
|
|
||||||
const dict = await this.dictModel.findOneBy({ id: dictId });
|
const dict = await this.dictModel.findOneBy({ id: dictId });
|
||||||
if (!dict) {
|
if (!dict) {
|
||||||
throw new Error('指定的字典不存在');
|
throw new Error('指定的字典不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 判断传入的是 Buffer 还是文件路径字符串
|
||||||
|
let buffer: Buffer;
|
||||||
|
if (typeof bufferOrPath === 'string') {
|
||||||
|
// 如果是文件路径,读取文件内容
|
||||||
|
buffer = fs.readFileSync(bufferOrPath);
|
||||||
|
} else {
|
||||||
|
// 如果是 Buffer,直接使用
|
||||||
|
buffer = bufferOrPath;
|
||||||
|
}
|
||||||
|
|
||||||
const wb = xlsx.read(buffer, { type: 'buffer' });
|
const wb = xlsx.read(buffer, { type: 'buffer' });
|
||||||
const wsname = wb.SheetNames[0];
|
const wsname = wb.SheetNames[0];
|
||||||
const ws = wb.Sheets[wsname];
|
const ws = wb.Sheets[wsname];
|
||||||
// 支持titleCN字段的导入
|
// 使用默认的header解析方式,xlsx会自动将第一行作为表头
|
||||||
const data = xlsx.utils.sheet_to_json(ws, { header: ['name', 'title', 'titleCN', 'value', 'sort', 'image', 'shortName'] }).slice(1);
|
const data = xlsx.utils.sheet_to_json(ws) as ExcelRow[];
|
||||||
|
|
||||||
const items = data.map((row: any) => {
|
// 使用 upsertDictItem 方法逐个处理,存在则更新,不存在则创建
|
||||||
const item = new DictItem();
|
const createdItems = [];
|
||||||
item.name = this.formatName(row.name);
|
const updatedItems = [];
|
||||||
item.title = row.title;
|
const errors = [];
|
||||||
item.titleCN = row.titleCN; // 保存中文名称
|
|
||||||
item.value = row.value;
|
for (const row of data) {
|
||||||
item.image = row.image;
|
try {
|
||||||
item.shortName = row.shortName;
|
const result = await this.upsertDictItem(dictId, {
|
||||||
item.sort = row.sort || 0;
|
name: row.name,
|
||||||
item.dict = dict;
|
title: row.title,
|
||||||
return item;
|
titleCN: row.titleCN,
|
||||||
|
value: row.value,
|
||||||
|
image: row.image,
|
||||||
|
shortName: row.shortName,
|
||||||
|
sort: row.sort || 0,
|
||||||
});
|
});
|
||||||
|
if (result.action === 'created') {
|
||||||
|
createdItems.push(result.item);
|
||||||
|
} else {
|
||||||
|
updatedItems.push(result.item);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 记录错误信息
|
||||||
|
errors.push({
|
||||||
|
identifier: row.name || 'unknown',
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await this.dictItemModel.save(items);
|
const processed = createdItems.length + updatedItems.length;
|
||||||
return { success: true, count: items.length };
|
|
||||||
|
return {
|
||||||
|
total: data.length,
|
||||||
|
processed: processed,
|
||||||
|
updated: updatedItems.length,
|
||||||
|
created: createdItems.length,
|
||||||
|
errors: errors
|
||||||
|
};
|
||||||
}
|
}
|
||||||
getDict(where: { name?: string; id?: number; }, relations: string[]) {
|
getDict(where: { name?: string; id?: number; }, relations: string[]) {
|
||||||
if (!where.name && !where.id) {
|
if (!where.name && !where.id) {
|
||||||
|
|
@ -176,6 +238,58 @@ export class DictService {
|
||||||
return this.dictItemModel.save(item);
|
return this.dictItemModel.save(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 更新或创建字典项 (Upsert)
|
||||||
|
// 如果字典项已存在(根据 name 和 dictId 判断),则更新;否则创建新的
|
||||||
|
async upsertDictItem(dictId: number, itemData: {
|
||||||
|
name: string;
|
||||||
|
title: string;
|
||||||
|
titleCN?: string;
|
||||||
|
value?: string;
|
||||||
|
image?: string;
|
||||||
|
shortName?: string;
|
||||||
|
sort?: number;
|
||||||
|
}) {
|
||||||
|
// 格式化 name
|
||||||
|
const formattedName = this.formatName(itemData.name);
|
||||||
|
|
||||||
|
// 查找是否已存在该字典项(根据 name 和 dictId)
|
||||||
|
const existingItem = await this.dictItemModel.findOne({
|
||||||
|
where: {
|
||||||
|
name: formattedName,
|
||||||
|
dict: { id: dictId }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (existingItem) {
|
||||||
|
// 如果存在,则更新
|
||||||
|
existingItem.title = itemData.title;
|
||||||
|
existingItem.titleCN = itemData.titleCN;
|
||||||
|
existingItem.value = itemData.value;
|
||||||
|
existingItem.image = itemData.image;
|
||||||
|
existingItem.shortName = itemData.shortName;
|
||||||
|
existingItem.sort = itemData.sort || 0;
|
||||||
|
const savedItem = await this.dictItemModel.save(existingItem);
|
||||||
|
return { item: savedItem, action: 'updated' };
|
||||||
|
} else {
|
||||||
|
// 如果不存在,则创建新的
|
||||||
|
const dict = await this.dictModel.findOneBy({ id: dictId });
|
||||||
|
if (!dict) {
|
||||||
|
throw new Error(`指定的字典ID为${dictId},但不存在`);
|
||||||
|
}
|
||||||
|
const item = new DictItem();
|
||||||
|
item.name = formattedName;
|
||||||
|
item.title = itemData.title;
|
||||||
|
item.titleCN = itemData.titleCN;
|
||||||
|
item.value = itemData.value;
|
||||||
|
item.image = itemData.image;
|
||||||
|
item.shortName = itemData.shortName;
|
||||||
|
item.sort = itemData.sort || 0;
|
||||||
|
item.dict = dict;
|
||||||
|
const savedItem = await this.dictItemModel.save(item);
|
||||||
|
return { item: savedItem, action: 'created' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 更新字典项
|
// 更新字典项
|
||||||
async updateDictItem(id: number, updateDictItemDTO: UpdateDictItemDTO) {
|
async updateDictItem(id: number, updateDictItemDTO: UpdateDictItemDTO) {
|
||||||
if (updateDictItemDTO.name) {
|
if (updateDictItemDTO.name) {
|
||||||
|
|
@ -202,4 +316,39 @@ export class DictService {
|
||||||
// 返回该字典下的所有字典项
|
// 返回该字典下的所有字典项
|
||||||
return this.dictItemModel.find({ where: { dict: { id: dict.id } } });
|
return this.dictItemModel.find({ where: { dict: { id: dict.id } } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 导出字典项为 XLSX 文件
|
||||||
|
async exportDictItemsToXLSX(dictId: number) {
|
||||||
|
// 查找字典
|
||||||
|
const dict = await this.dictModel.findOneBy({ id: dictId });
|
||||||
|
// 如果字典不存在,则抛出错误
|
||||||
|
if (!dict) {
|
||||||
|
throw new Error('指定的字典不存在');
|
||||||
|
}
|
||||||
|
// 获取该字典下的所有字典项
|
||||||
|
const items = await this.dictItemModel.find({
|
||||||
|
where: { dict: { id: dictId } },
|
||||||
|
order: { sort: 'ASC', id: 'DESC' },
|
||||||
|
});
|
||||||
|
// 定义表头
|
||||||
|
const headers = ['name', 'title', 'titleCN', 'value', 'sort', 'image', 'shortName'];
|
||||||
|
// 将字典项转换为二维数组
|
||||||
|
const data = items.map((item) => [
|
||||||
|
item.name,
|
||||||
|
item.title,
|
||||||
|
item.titleCN || '',
|
||||||
|
item.value || '',
|
||||||
|
item.sort,
|
||||||
|
item.image || '',
|
||||||
|
item.shortName || '',
|
||||||
|
]);
|
||||||
|
// 创建工作表
|
||||||
|
const ws = xlsx.utils.aoa_to_sheet([headers, ...data]);
|
||||||
|
// 创建工作簿
|
||||||
|
const wb = xlsx.utils.book_new();
|
||||||
|
// 将工作表添加到工作簿
|
||||||
|
xlsx.utils.book_append_sheet(wb, ws, 'DictItems');
|
||||||
|
// 将工作簿写入缓冲区
|
||||||
|
return xlsx.write(wb, { type: 'buffer', bookType: 'xlsx' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -269,7 +269,7 @@ export class LogisticsService {
|
||||||
this.orderModel.save(order);
|
this.orderModel.save(order);
|
||||||
|
|
||||||
// todo 同步到wooccommerce删除运单信息
|
// todo 同步到wooccommerce删除运单信息
|
||||||
await this.wpService.deleteShipment(site, order.externalOrderId, shipment.tracking_id);
|
await this.wpService.deleteFulfillment(site, order.externalOrderId, shipment.tracking_id);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log('同步到woocommerce失败', error);
|
console.log('同步到woocommerce失败', error);
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -363,7 +363,7 @@ export class LogisticsService {
|
||||||
|
|
||||||
// 同步物流信息到woocommerce
|
// 同步物流信息到woocommerce
|
||||||
const site = await this.siteService.get(Number(order.siteId), true);
|
const site = await this.siteService.get(Number(order.siteId), true);
|
||||||
const res = await this.wpService.createShipment(site, order.externalOrderId, {
|
const res = await this.wpService.createFulfillment(site, order.externalOrderId, {
|
||||||
tracking_number: resShipmentOrder.data.tno,
|
tracking_number: resShipmentOrder.data.tno,
|
||||||
tracking_provider: tracking_provider,
|
tracking_provider: tracking_provider,
|
||||||
});
|
});
|
||||||
|
|
@ -492,7 +492,7 @@ export class LogisticsService {
|
||||||
await this.wpService.updateOrder(site, order.externalOrderId, {
|
await this.wpService.updateOrder(site, order.externalOrderId, {
|
||||||
status: OrderStatus.COMPLETED,
|
status: OrderStatus.COMPLETED,
|
||||||
});
|
});
|
||||||
await this.wpService.createShipment(site, order.externalOrderId, {
|
await this.wpService.createFulfillment(site, order.externalOrderId, {
|
||||||
tracking_number: shipment.primary_tracking_number,
|
tracking_number: shipment.primary_tracking_number,
|
||||||
tracking_provider: shipment?.rate?.carrier_name,
|
tracking_provider: shipment?.rate?.carrier_name,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,8 @@ import { UpdateStockDTO } from '../dto/stock.dto';
|
||||||
import { StockService } from './stock.service';
|
import { StockService } from './stock.service';
|
||||||
import { OrderItemOriginal } from '../entity/order_item_original.entity';
|
import { OrderItemOriginal } from '../entity/order_item_original.entity';
|
||||||
import { SiteApiService } from './site-api.service';
|
import { SiteApiService } from './site-api.service';
|
||||||
|
import { SyncOperationResult } from '../dto/api.dto';
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
|
|
@ -98,55 +100,98 @@ export class OrderService {
|
||||||
@Inject()
|
@Inject()
|
||||||
siteApiService: SiteApiService;
|
siteApiService: SiteApiService;
|
||||||
|
|
||||||
async syncOrders(siteId: number, params: Record<string, any> = {}) {
|
async syncOrders(siteId: number, params: Record<string, any> = {}): Promise<SyncOperationResult> {
|
||||||
const daysRange = 7;
|
|
||||||
|
|
||||||
// 获取当前时间和7天前时间
|
|
||||||
const now = new Date();
|
|
||||||
const sevenDaysAgo = new Date();
|
|
||||||
sevenDaysAgo.setDate(now.getDate() - daysRange);
|
|
||||||
|
|
||||||
// 格式化时间为ISO 8601
|
|
||||||
const after = sevenDaysAgo.toISOString();
|
|
||||||
const before = now.toISOString();
|
|
||||||
// 调用 WooCommerce API 获取订单
|
// 调用 WooCommerce API 获取订单
|
||||||
const result = await (await this.siteApiService.getAdapter(siteId)).getAllOrders({
|
const result = await (await this.siteApiService.getAdapter(siteId)).getAllOrders(params);
|
||||||
...params,
|
|
||||||
after,
|
|
||||||
before,
|
|
||||||
});
|
|
||||||
|
|
||||||
let successCount = 0;
|
const syncResult: SyncOperationResult = {
|
||||||
let failureCount = 0;
|
total: result.length,
|
||||||
|
processed: 0,
|
||||||
|
synced: 0,
|
||||||
|
created: 0,
|
||||||
|
updated: 0,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
|
||||||
|
// 遍历每个订单进行同步
|
||||||
for (const order of result) {
|
for (const order of result) {
|
||||||
try {
|
try {
|
||||||
|
// 检查订单是否已存在,以区分创建和更新
|
||||||
|
const existingOrder = await this.orderModel.findOne({
|
||||||
|
where: { externalOrderId: String(order.id), siteId: siteId },
|
||||||
|
});
|
||||||
|
if(!existingOrder){
|
||||||
|
console.log("数据库中不存在",order.id, '订单状态:', order.status )
|
||||||
|
}
|
||||||
|
// 同步单个订单
|
||||||
await this.syncSingleOrder(siteId, order);
|
await this.syncSingleOrder(siteId, order);
|
||||||
successCount++;
|
|
||||||
|
// 统计结果
|
||||||
|
syncResult.processed++;
|
||||||
|
syncResult.synced++;
|
||||||
|
|
||||||
|
if (existingOrder) {
|
||||||
|
syncResult.updated++;
|
||||||
|
} else {
|
||||||
|
syncResult.created++;
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`同步订单 ${order.id} 失败:`, error);
|
// 记录错误但不中断整个同步过程
|
||||||
failureCount++;
|
syncResult.errors.push({
|
||||||
|
identifier: String(order.id),
|
||||||
|
error: error.message || '同步失败'
|
||||||
|
});
|
||||||
|
syncResult.processed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
success: failureCount === 0,
|
|
||||||
successCount,
|
|
||||||
failureCount,
|
|
||||||
message: `同步完成: 成功 ${successCount}, 失败 ${failureCount}`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async syncOrderById(siteId: number, orderId: string) {
|
return syncResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncOrderById(siteId: number, orderId: string): Promise<SyncOperationResult> {
|
||||||
|
const syncResult: SyncOperationResult = {
|
||||||
|
total: 1,
|
||||||
|
processed: 0,
|
||||||
|
synced: 0,
|
||||||
|
created: 0,
|
||||||
|
updated: 0,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 调用 WooCommerce API 获取订单
|
// 调用 WooCommerce API 获取订单
|
||||||
//const order = await this.wpService.getOrder(siteId, orderId);
|
const order = await this.wpService.getOrder(siteId, orderId);
|
||||||
const order = await (await this.siteApiService.getAdapter(siteId)).getOrder(
|
|
||||||
orderId,
|
// 检查订单是否已存在,以区分创建和更新
|
||||||
);
|
const existingOrder = await this.orderModel.findOne({
|
||||||
|
where: { externalOrderId: String(order.id), siteId: siteId },
|
||||||
|
});
|
||||||
|
if(!existingOrder){
|
||||||
|
console.log("数据库不存在", siteId , "订单:",order.id, '订单状态:' + order.status )
|
||||||
|
}
|
||||||
|
// 同步单个订单
|
||||||
await this.syncSingleOrder(siteId, order, true);
|
await this.syncSingleOrder(siteId, order, true);
|
||||||
return { success: true, message: '同步成功' };
|
|
||||||
|
// 统计结果
|
||||||
|
syncResult.processed = 1;
|
||||||
|
syncResult.synced = 1;
|
||||||
|
|
||||||
|
if (existingOrder) {
|
||||||
|
syncResult.updated = 1;
|
||||||
|
} else {
|
||||||
|
syncResult.created = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return syncResult;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`同步订单 ${orderId} 失败:`, error);
|
// 记录错误
|
||||||
return { success: false, message: `同步失败: ${error.message}` };
|
syncResult.errors.push({
|
||||||
|
identifier: orderId,
|
||||||
|
error: error.message || '同步失败'
|
||||||
|
});
|
||||||
|
syncResult.processed = 1;
|
||||||
|
|
||||||
|
return syncResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 订单状态切换表
|
// 订单状态切换表
|
||||||
|
|
@ -155,10 +200,9 @@ export class OrderService {
|
||||||
[OrderStatus.RETURN_CANCELLED]: OrderStatus.REFUNDED // 已取消退款转为 refunded
|
[OrderStatus.RETURN_CANCELLED]: OrderStatus.REFUNDED // 已取消退款转为 refunded
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 由于 wordpress 订单状态和 我们的订单状态 不一致,需要做转换
|
// 由于 wordpress 订单状态和 我们的订单状态 不一致,需要做转换
|
||||||
async autoUpdateOrderStatus(siteId: number, order: any) {
|
async autoUpdateOrderStatus(siteId: number, order: any) {
|
||||||
console.log('更新订单状态', order)
|
console.log('更新订单状态', order.status, '=>', this.orderAutoNextStatusMap[order.status])
|
||||||
// 其他状态保持不变
|
// 其他状态保持不变
|
||||||
const originStatus = order.status;
|
const originStatus = order.status;
|
||||||
// 如果有值就赋值
|
// 如果有值就赋值
|
||||||
|
|
@ -184,7 +228,7 @@ export class OrderService {
|
||||||
refunds,
|
refunds,
|
||||||
...orderData
|
...orderData
|
||||||
} = order;
|
} = order;
|
||||||
console.log('同步进单个订单', order)
|
// console.log('同步进单个订单', order)
|
||||||
const existingOrder = await this.orderModel.findOne({
|
const existingOrder = await this.orderModel.findOne({
|
||||||
where: { externalOrderId: order.id, siteId: siteId },
|
where: { externalOrderId: order.id, siteId: siteId },
|
||||||
});
|
});
|
||||||
|
|
@ -224,7 +268,6 @@ export class OrderService {
|
||||||
externalOrderId,
|
externalOrderId,
|
||||||
orderItems: line_items,
|
orderItems: line_items,
|
||||||
});
|
});
|
||||||
console.log('同步进单个订单1')
|
|
||||||
await this.saveOrderRefunds({
|
await this.saveOrderRefunds({
|
||||||
siteId,
|
siteId,
|
||||||
orderId,
|
orderId,
|
||||||
|
|
@ -303,7 +346,8 @@ export class OrderService {
|
||||||
if (!customer) {
|
if (!customer) {
|
||||||
await this.customerModel.save({
|
await this.customerModel.save({
|
||||||
email: order.customer_email,
|
email: order.customer_email,
|
||||||
rate: 0,
|
site_id: siteId,
|
||||||
|
origin_id: String(order.customer_id),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return await this.orderModel.save(entity);
|
return await this.orderModel.save(entity);
|
||||||
|
|
@ -437,8 +481,9 @@ export class OrderService {
|
||||||
await this.orderSaleModel.delete(currentOrderSale.map(v => v.id));
|
await this.orderSaleModel.delete(currentOrderSale.map(v => v.id));
|
||||||
}
|
}
|
||||||
if (!orderItem.sku) return;
|
if (!orderItem.sku) return;
|
||||||
|
// 从数据库查询产品,关联查询组件
|
||||||
const product = await this.productModel.findOne({
|
const product = await this.productModel.findOne({
|
||||||
where: { sku: orderItem.sku },
|
where: { siteSkus: Like(`%${orderItem.sku}%`) },
|
||||||
relations: ['components'],
|
relations: ['components'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1715,7 +1760,7 @@ export class OrderService {
|
||||||
// }
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
async exportOrder(ids: number[]) {
|
async exportOrder(ids: number[]) {
|
||||||
// 日期 订单号 姓名地址 邮箱 号码 订单内容 盒数 换盒数 换货内容 快递号
|
// 日期 订单号 姓名地址 邮箱 号码 订单内容 盒数 换盒数 换货内容 快递号
|
||||||
interface ExportData {
|
interface ExportData {
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,39 @@
|
||||||
import { Inject, Provide } from '@midwayjs/core';
|
import { Inject, Provide } from '@midwayjs/core';
|
||||||
|
import { parse } from 'csv-parse';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import { In, Like, Not, Repository } from 'typeorm';
|
import { In, Like, Not, Repository } from 'typeorm';
|
||||||
import { Product } from '../entity/product.entity';
|
import { Product } from '../entity/product.entity';
|
||||||
import { paginate } from '../utils/paginate.util';
|
|
||||||
import { PaginationParams } from '../interface';
|
import { PaginationParams } from '../interface';
|
||||||
import { parse } from 'csv-parse';
|
import { paginate } from '../utils/paginate.util';
|
||||||
|
import { Context } from '@midwayjs/koa';
|
||||||
|
import { InjectEntityModel } from '@midwayjs/typeorm';
|
||||||
import {
|
import {
|
||||||
CreateProductDTO,
|
|
||||||
UpdateProductDTO,
|
|
||||||
BatchUpdateProductDTO,
|
BatchUpdateProductDTO,
|
||||||
|
CreateProductDTO,
|
||||||
|
ProductWhereFilter,
|
||||||
|
UpdateProductDTO
|
||||||
} from '../dto/product.dto';
|
} from '../dto/product.dto';
|
||||||
import {
|
import {
|
||||||
BrandPaginatedResponse,
|
BrandPaginatedResponse,
|
||||||
FlavorsPaginatedResponse,
|
FlavorsPaginatedResponse,
|
||||||
ProductPaginatedResponse,
|
ProductPaginatedResponse,
|
||||||
StrengthPaginatedResponse,
|
|
||||||
SizePaginatedResponse,
|
SizePaginatedResponse,
|
||||||
|
StrengthPaginatedResponse,
|
||||||
} from '../dto/reponse.dto';
|
} from '../dto/reponse.dto';
|
||||||
import { InjectEntityModel } from '@midwayjs/typeorm';
|
|
||||||
import { Dict } from '../entity/dict.entity';
|
import { Dict } from '../entity/dict.entity';
|
||||||
import { DictItem } from '../entity/dict_item.entity';
|
import { DictItem } from '../entity/dict_item.entity';
|
||||||
import { Context } from '@midwayjs/koa';
|
import { ProductStockComponent } from '../entity/product_stock_component.entity';
|
||||||
import { TemplateService } from './template.service';
|
|
||||||
import { StockService } from './stock.service';
|
|
||||||
import { Stock } from '../entity/stock.entity';
|
import { Stock } from '../entity/stock.entity';
|
||||||
import { StockPoint } from '../entity/stock_point.entity';
|
import { StockPoint } from '../entity/stock_point.entity';
|
||||||
import { ProductStockComponent } from '../entity/product_stock_component.entity';
|
import { StockService } from './stock.service';
|
||||||
import { ProductSiteSku } from '../entity/product_site_sku.entity';
|
import { TemplateService } from './template.service';
|
||||||
|
|
||||||
|
import { SyncOperationResultDTO, UnifiedSearchParamsDTO } from '../dto/api.dto';
|
||||||
|
import { UnifiedProductDTO } from '../dto/site-api.dto';
|
||||||
|
import { ProductSiteSkuDTO, SyncProductToSiteDTO } from '../dto/site-sync.dto';
|
||||||
import { Category } from '../entity/category.entity';
|
import { Category } from '../entity/category.entity';
|
||||||
import { CategoryAttribute } from '../entity/category_attribute.entity';
|
import { CategoryAttribute } from '../entity/category_attribute.entity';
|
||||||
|
import { SiteApiService } from './site-api.service';
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
export class ProductService {
|
export class ProductService {
|
||||||
|
|
@ -60,12 +64,12 @@ export class ProductService {
|
||||||
@InjectEntityModel(ProductStockComponent)
|
@InjectEntityModel(ProductStockComponent)
|
||||||
productStockComponentModel: Repository<ProductStockComponent>;
|
productStockComponentModel: Repository<ProductStockComponent>;
|
||||||
|
|
||||||
@InjectEntityModel(ProductSiteSku)
|
|
||||||
productSiteSkuModel: Repository<ProductSiteSku>;
|
|
||||||
|
|
||||||
@InjectEntityModel(Category)
|
@InjectEntityModel(Category)
|
||||||
categoryModel: Repository<Category>;
|
categoryModel: Repository<Category>;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
siteApiService: SiteApiService;
|
||||||
|
|
||||||
// 获取所有分类
|
// 获取所有分类
|
||||||
async getCategoriesAll(): Promise<Category[]> {
|
async getCategoriesAll(): Promise<Category[]> {
|
||||||
return this.categoryModel.find({
|
return this.categoryModel.find({
|
||||||
|
|
@ -179,8 +183,7 @@ export class ProductService {
|
||||||
async findProductsByName(name: string): Promise<Product[]> {
|
async findProductsByName(name: string): Promise<Product[]> {
|
||||||
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||||||
const query = this.productModel.createQueryBuilder('product')
|
const query = this.productModel.createQueryBuilder('product')
|
||||||
.leftJoinAndSelect('product.category', 'category')
|
.leftJoinAndSelect('product.category', 'category');
|
||||||
.leftJoinAndSelect('product.siteSkus', 'siteSku');
|
|
||||||
|
|
||||||
// 保证 sku 不为空
|
// 保证 sku 不为空
|
||||||
query.where('product.sku IS NOT NULL');
|
query.where('product.sku IS NOT NULL');
|
||||||
|
|
@ -205,9 +208,12 @@ export class ProductService {
|
||||||
conditions.push(`product.nameCn LIKE :nameCn`);
|
conditions.push(`product.nameCn LIKE :nameCn`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 只有当 conditions 不为空时才添加 AND 条件
|
||||||
|
if (conditions.length > 0) {
|
||||||
// 英文名关键词匹配 OR 中文名匹配
|
// 英文名关键词匹配 OR 中文名匹配
|
||||||
query.andWhere(`(${conditions.join(' OR ')})`, params);
|
query.andWhere(`(${conditions.join(' OR ')})`, params);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
query.take(50);
|
query.take(50);
|
||||||
|
|
||||||
|
|
@ -223,19 +229,30 @@ export class ProductService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProductList(
|
async getProductList(query: UnifiedSearchParamsDTO<ProductWhereFilter>): Promise<ProductPaginatedResponse> {
|
||||||
pagination: PaginationParams,
|
|
||||||
name?: string,
|
|
||||||
brandId?: number,
|
|
||||||
sortField?: string,
|
|
||||||
sortOrder?: string
|
|
||||||
): Promise<ProductPaginatedResponse> {
|
|
||||||
const qb = this.productModel
|
const qb = this.productModel
|
||||||
.createQueryBuilder('product')
|
.createQueryBuilder('product')
|
||||||
.leftJoinAndSelect('product.attributes', 'attribute')
|
.leftJoinAndSelect('product.attributes', 'attribute')
|
||||||
.leftJoinAndSelect('attribute.dict', 'dict')
|
.leftJoinAndSelect('attribute.dict', 'dict')
|
||||||
.leftJoinAndSelect('product.category', 'category')
|
.leftJoinAndSelect('product.category', 'category');
|
||||||
.leftJoinAndSelect('product.siteSkus', 'siteSku');
|
|
||||||
|
// 处理分页参数(支持新旧两种格式)
|
||||||
|
const page = query.page || 1;
|
||||||
|
const pageSize = query.per_page || 10;
|
||||||
|
|
||||||
|
// 处理搜索参数
|
||||||
|
const name = query.where?.name || query.search || '';
|
||||||
|
|
||||||
|
// 处理品牌过滤
|
||||||
|
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,支持多个关键词
|
// 模糊搜索 name,支持多个关键词
|
||||||
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
const nameFilter = name ? name.split(' ').filter(Boolean) : [];
|
||||||
|
|
@ -250,7 +267,134 @@ export class ProductService {
|
||||||
qb.where(`(${nameConditions})`, nameParams);
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理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 = :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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理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}%` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理产品类型过滤
|
||||||
|
if (query.where?.type) {
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.where?.maxPrice !== undefined) {
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.where?.maxPromotionPrice !== undefined) {
|
||||||
|
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) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.where?.createdAtEnd) {
|
||||||
|
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) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.where?.updatedAtEnd) {
|
||||||
|
qb.andWhere('product.updatedAt <= :updatedAtEnd', { updatedAtEnd: new Date(query.where.updatedAtEnd) });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理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) {
|
if (brandId) {
|
||||||
qb.andWhere(qb => {
|
qb.andWhere(qb => {
|
||||||
const subQuery = qb
|
const subQuery = qb
|
||||||
|
|
@ -265,21 +409,74 @@ export class ProductService {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 排序
|
// 处理品牌ID列表过滤
|
||||||
if (sortField && sortOrder) {
|
if (brandIds && brandIds.length > 0) {
|
||||||
const order = sortOrder === 'ascend' ? 'ASC' : 'DESC';
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理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') {
|
||||||
|
// 如果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'];
|
const allowedSortFields = ['price', 'promotionPrice', 'createdAt', 'updatedAt', 'sku', 'name'];
|
||||||
if (allowedSortFields.includes(sortField)) {
|
if (allowedSortFields.includes(key)) {
|
||||||
qb.orderBy(`product.${sortField}`, order);
|
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 {
|
} else {
|
||||||
qb.orderBy('product.createdAt', 'DESC');
|
qb.orderBy('product.createdAt', 'DESC');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分页
|
// 分页
|
||||||
qb.skip((pagination.current - 1) * pagination.pageSize).take(
|
qb.skip((page - 1) * pageSize).take(pageSize);
|
||||||
pagination.pageSize
|
|
||||||
);
|
|
||||||
|
|
||||||
const [items, total] = await qb.getManyAndCount();
|
const [items, total] = await qb.getManyAndCount();
|
||||||
|
|
||||||
|
|
@ -303,7 +500,8 @@ export class ProductService {
|
||||||
return {
|
return {
|
||||||
items,
|
items,
|
||||||
total,
|
total,
|
||||||
...pagination,
|
current: page,
|
||||||
|
pageSize,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -339,15 +537,14 @@ export class ProductService {
|
||||||
|
|
||||||
|
|
||||||
async createProduct(createProductDTO: CreateProductDTO): Promise<Product> {
|
async createProduct(createProductDTO: CreateProductDTO): Promise<Product> {
|
||||||
const { attributes, sku, categoryId } = createProductDTO;
|
const { attributes, sku, categoryId, type } = createProductDTO;
|
||||||
|
|
||||||
// 条件判断(校验属性输入)
|
// 条件判断(校验属性输入)
|
||||||
|
// 当产品类型为 'bundle' 时,attributes 可以为空
|
||||||
|
// 当产品类型为 'single' 时,attributes 必须提供且不能为空
|
||||||
|
if (type === 'single') {
|
||||||
if (!Array.isArray(attributes) || attributes.length === 0) {
|
if (!Array.isArray(attributes) || attributes.length === 0) {
|
||||||
// 如果提供了 categoryId 但没有 attributes,初始化为空数组
|
throw new Error('单品类型的属性列表不能为空');
|
||||||
if (!attributes && categoryId) {
|
|
||||||
// 继续执行,下面会处理 categoryId
|
|
||||||
} else {
|
|
||||||
throw new Error('属性列表不能为空');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -408,6 +605,8 @@ export class ProductService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查完全相同属性组合是否已存在(避免重复)
|
// 检查完全相同属性组合是否已存在(避免重复)
|
||||||
|
// 仅当产品类型为 'single' 且有属性时才检查重复
|
||||||
|
if (type === 'single' && resolvedAttributes.length > 0) {
|
||||||
const qb = this.productModel.createQueryBuilder('product');
|
const qb = this.productModel.createQueryBuilder('product');
|
||||||
resolvedAttributes.forEach((attr, index) => {
|
resolvedAttributes.forEach((attr, index) => {
|
||||||
qb.innerJoin(
|
qb.innerJoin(
|
||||||
|
|
@ -419,12 +618,13 @@ export class ProductService {
|
||||||
});
|
});
|
||||||
const isExist = await qb.getOne();
|
const isExist = await qb.getOne();
|
||||||
if (isExist) throw new Error('相同产品属性的产品已存在');
|
if (isExist) throw new Error('相同产品属性的产品已存在');
|
||||||
|
}
|
||||||
|
|
||||||
// 创建新产品实例(绑定属性与基础字段)
|
// 创建新产品实例(绑定属性与基础字段)
|
||||||
const product = new Product();
|
const product = new Product();
|
||||||
|
|
||||||
// 使用 merge 填充基础字段,排除特殊处理字段
|
// 使用 merge 填充基础字段,排除特殊处理字段
|
||||||
const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, siteSkus: _siteSkus, ...simpleFields } = createProductDTO;
|
const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, ...simpleFields } = createProductDTO;
|
||||||
this.productModel.merge(product, simpleFields);
|
this.productModel.merge(product, simpleFields);
|
||||||
|
|
||||||
product.attributes = resolvedAttributes;
|
product.attributes = resolvedAttributes;
|
||||||
|
|
@ -438,22 +638,11 @@ export class ProductService {
|
||||||
if (sku) {
|
if (sku) {
|
||||||
product.sku = sku;
|
product.sku = sku;
|
||||||
} else {
|
} else {
|
||||||
product.sku = await this.templateService.render('product.sku', product);
|
product.sku = await this.templateService.render('product.sku', {product});
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedProduct = await this.productModel.save(product);
|
const savedProduct = await this.productModel.save(product);
|
||||||
|
|
||||||
// 保存站点 SKU 列表
|
|
||||||
if (createProductDTO.siteSkus && createProductDTO.siteSkus.length > 0) {
|
|
||||||
const siteSkus = createProductDTO.siteSkus.map(code => {
|
|
||||||
const s = new ProductSiteSku();
|
|
||||||
s.siteSku = code;
|
|
||||||
s.product = savedProduct;
|
|
||||||
return s;
|
|
||||||
});
|
|
||||||
await this.productSiteSkuModel.save(siteSkus);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存组件信息
|
// 保存组件信息
|
||||||
if (createProductDTO.components && createProductDTO.components.length > 0) {
|
if (createProductDTO.components && createProductDTO.components.length > 0) {
|
||||||
await this.setProductComponents(savedProduct.id, createProductDTO.components);
|
await this.setProductComponents(savedProduct.id, createProductDTO.components);
|
||||||
|
|
@ -475,7 +664,7 @@ export class ProductService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用 merge 更新基础字段,排除特殊处理字段
|
// 使用 merge 更新基础字段,排除特殊处理字段
|
||||||
const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, siteSkus: _siteSkus, ...simpleFields } = updateProductDTO;
|
const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, ...simpleFields } = updateProductDTO;
|
||||||
this.productModel.merge(product, simpleFields);
|
this.productModel.merge(product, simpleFields);
|
||||||
|
|
||||||
// 处理分类更新
|
// 处理分类更新
|
||||||
|
|
@ -490,23 +679,6 @@ export class ProductService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理站点 SKU 更新
|
|
||||||
if (updateProductDTO.siteSkus !== undefined) {
|
|
||||||
// 删除旧的 siteSkus
|
|
||||||
await this.productSiteSkuModel.delete({ productId: id });
|
|
||||||
|
|
||||||
// 如果有新的 siteSkus,则保存
|
|
||||||
if (updateProductDTO.siteSkus.length > 0) {
|
|
||||||
const siteSkus = updateProductDTO.siteSkus.map(code => {
|
|
||||||
const s = new ProductSiteSku();
|
|
||||||
s.siteSku = code;
|
|
||||||
s.productId = id;
|
|
||||||
return s;
|
|
||||||
});
|
|
||||||
await this.productSiteSkuModel.save(siteSkus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理 SKU 更新
|
// 处理 SKU 更新
|
||||||
if (updateProductDTO.sku !== undefined) {
|
if (updateProductDTO.sku !== undefined) {
|
||||||
// 校验 SKU 唯一性(如变更)
|
// 校验 SKU 唯一性(如变更)
|
||||||
|
|
@ -603,7 +775,7 @@ export class ProductService {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 简单字段,直接批量更新以提高性能
|
// 简单字段,直接批量更新以提高性能
|
||||||
// UpdateProductDTO 里的简单字段: name, nameCn, description, price, promotionPrice
|
// UpdateProductDTO 里的简单字段: name, nameCn, description, price, promotionPrice, siteSkus
|
||||||
|
|
||||||
const simpleUpdate: any = {};
|
const simpleUpdate: any = {};
|
||||||
if (updateData.name !== undefined) simpleUpdate.name = updateData.name;
|
if (updateData.name !== undefined) simpleUpdate.name = updateData.name;
|
||||||
|
|
@ -612,6 +784,7 @@ export class ProductService {
|
||||||
if (updateData.shortDescription !== undefined) simpleUpdate.shortDescription = updateData.shortDescription;
|
if (updateData.shortDescription !== undefined) simpleUpdate.shortDescription = updateData.shortDescription;
|
||||||
if (updateData.price !== undefined) simpleUpdate.price = updateData.price;
|
if (updateData.price !== undefined) simpleUpdate.price = updateData.price;
|
||||||
if (updateData.promotionPrice !== undefined) simpleUpdate.promotionPrice = updateData.promotionPrice;
|
if (updateData.promotionPrice !== undefined) simpleUpdate.promotionPrice = updateData.promotionPrice;
|
||||||
|
if (updateData.siteSkus !== undefined) simpleUpdate.siteSkus = updateData.siteSkus;
|
||||||
|
|
||||||
if (Object.keys(simpleUpdate).length > 0) {
|
if (Object.keys(simpleUpdate).length > 0) {
|
||||||
await this.productModel.update({ id: In(ids) }, simpleUpdate);
|
await this.productModel.update({ id: In(ids) }, simpleUpdate);
|
||||||
|
|
@ -771,40 +944,6 @@ export class ProductService {
|
||||||
return await this.getProductComponents(productId);
|
return await this.getProductComponents(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 站点SKU绑定:覆盖式绑定一组站点SKU到产品
|
|
||||||
async bindSiteSkus(productId: number, codes: string[]): Promise<ProductSiteSku[]> {
|
|
||||||
const product = await this.productModel.findOne({ where: { id: productId } });
|
|
||||||
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
|
|
||||||
const normalized = (codes || [])
|
|
||||||
.map(c => String(c).trim())
|
|
||||||
.filter(c => c.length > 0);
|
|
||||||
await this.productSiteSkuModel.delete({ productId });
|
|
||||||
if (normalized.length === 0) return [];
|
|
||||||
const entities = normalized.map(code => {
|
|
||||||
const e = new ProductSiteSku();
|
|
||||||
e.productId = productId;
|
|
||||||
e.siteSku = code;
|
|
||||||
return e;
|
|
||||||
});
|
|
||||||
return await this.productSiteSkuModel.save(entities);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 站点SKU绑定:按单个 code 绑定到指定产品(若已有则更新归属)
|
|
||||||
async bindProductBySiteSku(code: string, productId: number): Promise<ProductSiteSku> {
|
|
||||||
const product = await this.productModel.findOne({ where: { id: productId } });
|
|
||||||
if (!product) throw new Error(`产品 ID ${productId} 不存在`);
|
|
||||||
const skuCode = String(code || '').trim();
|
|
||||||
if (!skuCode) throw new Error('站点SKU不能为空');
|
|
||||||
const existing = await this.productSiteSkuModel.findOne({ where: { siteSku: skuCode } });
|
|
||||||
if (existing) {
|
|
||||||
existing.productId = productId;
|
|
||||||
return await this.productSiteSkuModel.save(existing);
|
|
||||||
}
|
|
||||||
const e = new ProductSiteSku();
|
|
||||||
e.productId = productId;
|
|
||||||
e.siteSku = skuCode;
|
|
||||||
return await this.productSiteSkuModel.save(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重复定义的 getProductList 已合并到前面的实现(移除重复)
|
// 重复定义的 getProductList 已合并到前面的实现(移除重复)
|
||||||
|
|
||||||
|
|
@ -1404,7 +1543,7 @@ export class ProductService {
|
||||||
// 基础数据
|
// 基础数据
|
||||||
const rowData = [
|
const rowData = [
|
||||||
esc(p.sku),
|
esc(p.sku),
|
||||||
esc(p.siteSkus ? p.siteSkus.map(s => s.siteSku).join(',') : ''),
|
esc(p.siteSkus ? p.siteSkus.join(',') : ''),
|
||||||
esc(p.name),
|
esc(p.name),
|
||||||
esc(p.nameCn),
|
esc(p.nameCn),
|
||||||
esc(p.price),
|
esc(p.price),
|
||||||
|
|
@ -1439,7 +1578,7 @@ export class ProductService {
|
||||||
async exportProductsCSV(): Promise<string> {
|
async exportProductsCSV(): Promise<string> {
|
||||||
// 查询所有产品及其属性(包含字典关系)和组成
|
// 查询所有产品及其属性(包含字典关系)和组成
|
||||||
const products = await this.productModel.find({
|
const products = await this.productModel.find({
|
||||||
relations: ['attributes', 'attributes.dict', 'components', 'siteSkus'],
|
relations: ['attributes', 'attributes.dict', 'components'],
|
||||||
order: { id: 'ASC' },
|
order: { id: 'ASC' },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1611,20 +1750,12 @@ export class ProductService {
|
||||||
return { added, errors };
|
return { added, errors };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取产品的站点SKU列表
|
|
||||||
async getProductSiteSkus(productId: number): Promise<ProductSiteSku[]> {
|
|
||||||
return this.productSiteSkuModel.find({
|
|
||||||
where: { productId },
|
|
||||||
relations: ['product'],
|
|
||||||
order: { createdAt: 'ASC' }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据ID获取产品详情(包含站点SKU)
|
// 根据ID获取产品详情(包含站点SKU)
|
||||||
async getProductById(id: number): Promise<Product> {
|
async getProductById(id: number): Promise<Product> {
|
||||||
const product = await this.productModel.findOne({
|
const product = await this.productModel.findOne({
|
||||||
where: { id },
|
where: { id },
|
||||||
relations: ['category', 'attributes', 'attributes.dict', 'siteSkus', 'components']
|
relations: ['category', 'attributes', 'attributes.dict', 'components']
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!product) {
|
if (!product) {
|
||||||
|
|
@ -1651,16 +1782,281 @@ export class ProductService {
|
||||||
|
|
||||||
// 根据站点SKU查询产品
|
// 根据站点SKU查询产品
|
||||||
async findProductBySiteSku(siteSku: string): Promise<Product> {
|
async findProductBySiteSku(siteSku: string): Promise<Product> {
|
||||||
const siteSkuEntity = await this.productSiteSkuModel.findOne({
|
const product = await this.productModel.findOne({
|
||||||
where: { siteSku },
|
where: { siteSkus: Like(`%${siteSku}%`) },
|
||||||
relations: ['product']
|
relations: ['category', 'attributes', 'attributes.dict', 'components']
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!siteSkuEntity) {
|
if (!product) {
|
||||||
throw new Error(`站点SKU ${siteSku} 不存在`);
|
throw new Error(`站点SKU ${siteSku} 不存在`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取完整的产品信息,包含所有关联数据
|
// 获取完整的产品信息,包含所有关联数据
|
||||||
return this.getProductById(siteSkuEntity.product.id);
|
return this.getProductById(product.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取产品的站点SKU列表
|
||||||
|
async getProductSiteSkus(productId: number): Promise<string[]> {
|
||||||
|
const product = await this.productModel.findOne({ where: { id: productId } });
|
||||||
|
if (!product) {
|
||||||
|
throw new Error(`产品 ID ${productId} 不存在`);
|
||||||
|
}
|
||||||
|
return product.siteSkus || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定产品的站点SKU列表
|
||||||
|
async bindSiteSkus(productId: number, siteSkus: string[]): Promise<string[]> {
|
||||||
|
const product = await this.productModel.findOne({ where: { id: productId } });
|
||||||
|
if (!product) {
|
||||||
|
throw new Error(`产品 ID ${productId} 不存在`);
|
||||||
|
}
|
||||||
|
const normalizedSiteSkus = (siteSkus || [])
|
||||||
|
.map(c => String(c).trim())
|
||||||
|
.filter(c => c.length > 0);
|
||||||
|
product.siteSkus = normalizedSiteSkus;
|
||||||
|
await this.productModel.save(product);
|
||||||
|
return product.siteSkus || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将本地产品同步到站点
|
||||||
|
* @param productId 本地产品ID
|
||||||
|
* @param siteId 站点ID
|
||||||
|
* @returns 同步结果
|
||||||
|
*/
|
||||||
|
async syncToSite(params: SyncProductToSiteDTO): Promise<any> {
|
||||||
|
// 获取本地产品信息
|
||||||
|
const localProduct = await this.getProductById(params.productId);
|
||||||
|
if (!localProduct) {
|
||||||
|
throw new Error(`本地产品 ID ${params.productId} 不存在`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将本地产品转换为站点API所需格式
|
||||||
|
const unifiedProduct = await this.convertLocalProductToUnifiedProduct(localProduct, params.siteSku);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// 调用站点API的upsertProduct方法
|
||||||
|
try {
|
||||||
|
const result = await this.siteApiService.upsertProduct(params.siteId, unifiedProduct);
|
||||||
|
// 绑定站点SKU
|
||||||
|
await this.bindSiteSkus(localProduct.id, [unifiedProduct.sku]);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`同步产品到站点失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量将本地产品同步到站点
|
||||||
|
* @param siteId 站点ID
|
||||||
|
* @param data 产品站点SKU列表
|
||||||
|
* @returns 批量同步结果
|
||||||
|
*/
|
||||||
|
async batchSyncToSite(siteId: number, data: ProductSiteSkuDTO[]): Promise<SyncOperationResultDTO> {
|
||||||
|
const results: SyncOperationResultDTO = {
|
||||||
|
total: data.length,
|
||||||
|
processed: 0,
|
||||||
|
synced: 0,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const item of data) {
|
||||||
|
try {
|
||||||
|
// 先同步产品到站点
|
||||||
|
await this.syncToSite({
|
||||||
|
productId: item.productId,
|
||||||
|
siteId,
|
||||||
|
siteSku: item.siteSku
|
||||||
|
});
|
||||||
|
|
||||||
|
// 然后绑定站点SKU
|
||||||
|
await this.bindSiteSkus(item.productId, [item.siteSku]);
|
||||||
|
|
||||||
|
results.synced++;
|
||||||
|
results.processed++;
|
||||||
|
} catch (error) {
|
||||||
|
results.processed++;
|
||||||
|
results.errors.push({
|
||||||
|
identifier: String(item.productId),
|
||||||
|
error: `产品ID ${item.productId} 同步失败: ${error.message}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从站点同步产品到本地
|
||||||
|
* @param siteId 站点ID
|
||||||
|
* @param siteProductId 站点产品ID
|
||||||
|
* @returns 同步后的本地产品
|
||||||
|
*/
|
||||||
|
async syncProductFromSite(siteId: number, siteProductId: string | number): Promise<any> {
|
||||||
|
// 从站点获取产品信息
|
||||||
|
const siteProduct = await this.siteApiService.getProductFromSite(siteId, siteProductId);
|
||||||
|
if (!siteProduct) {
|
||||||
|
throw new Error(`站点产品 ID ${siteProductId} 不存在`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已存在相同SKU的本地产品
|
||||||
|
let localProduct = null;
|
||||||
|
if (siteProduct.sku) {
|
||||||
|
try {
|
||||||
|
localProduct = await this.findProductBySku(siteProduct.sku);
|
||||||
|
} catch (error) {
|
||||||
|
// 产品不存在,继续创建
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将站点产品转换为本地产品格式
|
||||||
|
const productData = await this.convertSiteProductToLocalProduct(siteProduct);
|
||||||
|
|
||||||
|
if (localProduct) {
|
||||||
|
// 更新现有产品
|
||||||
|
const updateData: UpdateProductDTO = productData;
|
||||||
|
return await this.updateProduct(localProduct.id, updateData);
|
||||||
|
} else {
|
||||||
|
// 创建新产品
|
||||||
|
const createData: CreateProductDTO = productData;
|
||||||
|
return await this.createProduct(createData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量从站点同步产品到本地
|
||||||
|
* @param siteId 站点ID
|
||||||
|
* @param siteProductIds 站点产品ID数组
|
||||||
|
* @returns 批量同步结果
|
||||||
|
*/
|
||||||
|
async batchSyncFromSite(siteId: number, siteProductIds: (string | number)[]): Promise<{ synced: number, errors: string[] }> {
|
||||||
|
const results = {
|
||||||
|
synced: 0,
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const siteProductId of siteProductIds) {
|
||||||
|
try {
|
||||||
|
await this.syncProductFromSite(siteId, siteProductId);
|
||||||
|
results.synced++;
|
||||||
|
} catch (error) {
|
||||||
|
results.errors.push(`站点产品ID ${siteProductId} 同步失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将站点产品转换为本地产品格式
|
||||||
|
* @param siteProduct 站点产品对象
|
||||||
|
* @returns 本地产品数据
|
||||||
|
*/
|
||||||
|
private async convertSiteProductToLocalProduct(siteProduct: any): Promise<CreateProductDTO> {
|
||||||
|
const productData: any = {
|
||||||
|
sku: siteProduct.sku,
|
||||||
|
name: siteProduct.name,
|
||||||
|
nameCn: siteProduct.name,
|
||||||
|
price: siteProduct.price ? parseFloat(siteProduct.price) : 0,
|
||||||
|
promotionPrice: siteProduct.sale_price ? parseFloat(siteProduct.sale_price) : 0,
|
||||||
|
description: siteProduct.description || '',
|
||||||
|
images: [],
|
||||||
|
attributes: [],
|
||||||
|
categoryId: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理图片
|
||||||
|
if (siteProduct.images && Array.isArray(siteProduct.images)) {
|
||||||
|
productData.images = siteProduct.images.map((img: any) => ({
|
||||||
|
url: img.src || img.url,
|
||||||
|
name: img.name || img.alt || '',
|
||||||
|
alt: img.alt || ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理分类
|
||||||
|
if (siteProduct.categories && Array.isArray(siteProduct.categories) && siteProduct.categories.length > 0) {
|
||||||
|
// 尝试通过分类名称匹配本地分类
|
||||||
|
const categoryName = siteProduct.categories[0].name;
|
||||||
|
const category = await this.findCategoryByName(categoryName);
|
||||||
|
if (category) {
|
||||||
|
productData.categoryId = category.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理属性
|
||||||
|
if (siteProduct.attributes && Array.isArray(siteProduct.attributes)) {
|
||||||
|
productData.attributes = siteProduct.attributes.map((attr: any) => ({
|
||||||
|
name: attr.name,
|
||||||
|
value: attr.options && attr.options.length > 0 ? attr.options[0] : ''
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return productData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据分类名称查找分类
|
||||||
|
* @param name 分类名称
|
||||||
|
* @returns 分类对象
|
||||||
|
*/
|
||||||
|
private async findCategoryByName(name: string): Promise<Category | null> {
|
||||||
|
try {
|
||||||
|
return await this.categoryModel.findOne({
|
||||||
|
where: { name }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将本地产品转换为统一产品格式
|
||||||
|
* @param localProduct 本地产品对象
|
||||||
|
* @returns 统一产品对象
|
||||||
|
*/
|
||||||
|
private async convertLocalProductToUnifiedProduct(localProduct: Product,siteSku?: string): Promise<Partial<UnifiedProductDTO>> {
|
||||||
|
// 将本地产品数据转换为UnifiedProductDTO格式
|
||||||
|
const unifiedProduct: any = {
|
||||||
|
id: localProduct.id ? String(localProduct.id) : undefined, // 如果产品已存在,使用现有ID
|
||||||
|
name: localProduct.nameCn || localProduct.name || localProduct.sku,
|
||||||
|
type: 'simple', // 默认类型,可以根据实际需要调整
|
||||||
|
status: 'publish', // 默认状态,可以根据实际需要调整
|
||||||
|
sku: siteSku || await this.templateService.render('site.product.sku', { sku: localProduct.sku }),
|
||||||
|
regular_price: String(localProduct.price || 0),
|
||||||
|
sale_price: String(localProduct.promotionPrice || localProduct.price || 0),
|
||||||
|
price: String(localProduct.price || 0),
|
||||||
|
// stock_status: localProduct.stockQuantity && localProduct.stockQuantity > 0 ? 'instock' : 'outofstock',
|
||||||
|
// stock_quantity: localProduct.stockQuantity || 0,
|
||||||
|
// images: localProduct.images ? localProduct.images.map(img => ({
|
||||||
|
// id: img.id,
|
||||||
|
// src: img.url,
|
||||||
|
// name: img.name || '',
|
||||||
|
// alt: img.alt || ''
|
||||||
|
// })) : [],
|
||||||
|
tags: [],
|
||||||
|
categories: localProduct.category ? [{
|
||||||
|
id: localProduct.category.id,
|
||||||
|
name: localProduct.category.name
|
||||||
|
}] : [],
|
||||||
|
attributes: localProduct.attributes ? localProduct.attributes.map(attr => ({
|
||||||
|
id: attr.id,
|
||||||
|
name: attr.name,
|
||||||
|
position: 0,
|
||||||
|
visible: true,
|
||||||
|
variation: false,
|
||||||
|
options: [attr.value]
|
||||||
|
})) : [],
|
||||||
|
variations: [],
|
||||||
|
date_created: localProduct.createdAt ? new Date(localProduct.createdAt).toISOString() : new Date().toISOString(),
|
||||||
|
date_modified: localProduct.updatedAt ? new Date(localProduct.updatedAt).toISOString() : new Date().toISOString(),
|
||||||
|
raw: {
|
||||||
|
localProductId: localProduct.id,
|
||||||
|
localProductSku: localProduct.sku
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return unifiedProduct;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { Site } from '../entity/site.entity';
|
||||||
import { UnifiedReviewDTO } from '../dto/site-api.dto';
|
import { UnifiedReviewDTO } from '../dto/site-api.dto';
|
||||||
import { ShopyyReview } from '../dto/shopyy.dto';
|
import { ShopyyReview } from '../dto/shopyy.dto';
|
||||||
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto';
|
||||||
import { UnifiedSearchParamsDTO } from '../dto/site-api.dto';
|
import { UnifiedSearchParamsDTO } from '../dto/api.dto';
|
||||||
/**
|
/**
|
||||||
* ShopYY平台服务实现
|
* ShopYY平台服务实现
|
||||||
*/
|
*/
|
||||||
|
|
@ -323,7 +323,7 @@ export class ShopyyService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getAllOrders(site: any | number, params: Record<string, any> = {}, maxPages: number = 100, concurrencyLimit: number = 100): Promise<any> {
|
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 firstPage = await this.getOrders(site, 1, 100);
|
||||||
|
|
||||||
const { items: firstPageItems, totalPages} = firstPage;
|
const { items: firstPageItems, totalPages} = firstPage;
|
||||||
|
|
@ -482,40 +482,102 @@ export class ShopyyService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建ShopYY物流信息
|
* 创建ShopYY履约信息
|
||||||
* @param site 站点配置
|
* @param site 站点配置
|
||||||
* @param orderId 订单ID
|
* @param orderId 订单ID
|
||||||
* @param data 物流数据
|
* @param data 履约数据
|
||||||
* @returns 创建结果
|
* @returns 创建结果
|
||||||
*/
|
*/
|
||||||
async createShipment(site: any, orderId: string, data: any): Promise<any> {
|
async createFulfillment(site: Site, orderId: string, data: any): Promise<any> {
|
||||||
// ShopYY API: POST /orders/{id}/shipments
|
// ShopYY API: POST /orders/{id}/shipments
|
||||||
const shipmentData = {
|
const fulfillmentData = {
|
||||||
tracking_number: data.tracking_number,
|
tracking_number: data.tracking_number,
|
||||||
carrier_code: data.carrier_code,
|
carrier_code: data.carrier_code,
|
||||||
carrier_name: data.carrier_name,
|
carrier_name: data.carrier_name,
|
||||||
shipping_method: data.shipping_method
|
shipping_method: data.shipping_method
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await this.request(site, `orders/${orderId}/shipments`, 'POST', shipmentData);
|
const response = await this.request(site, `orders/${orderId}/shipments`, 'POST', fulfillmentData);
|
||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除ShopYY物流信息
|
* 删除ShopYY履约信息
|
||||||
* @param site 站点配置
|
* @param site 站点配置
|
||||||
* @param orderId 订单ID
|
* @param orderId 订单ID
|
||||||
* @param trackingId 物流跟踪ID
|
* @param fulfillmentId 履约跟踪ID
|
||||||
* @returns 删除结果
|
* @returns 删除结果
|
||||||
*/
|
*/
|
||||||
async deleteShipment(site: any, orderId: string, trackingId: string): Promise<boolean> {
|
async deleteFulfillment(site: any, orderId: string, fulfillmentId: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
// ShopYY API: DELETE /orders/{order_id}/shipments/{tracking_id}
|
// ShopYY API: DELETE /orders/{order_id}/shipments/{fulfillment_id}
|
||||||
await this.request(site, `orders/${orderId}/shipments/${trackingId}`, 'DELETE');
|
await this.request(site, `orders/${orderId}/fulfillments/${fulfillmentId}`, 'DELETE');
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = error.response?.data?.msg || error.message || '删除ShopYY物流信息失败';
|
const errorMessage = error.response?.data?.msg || error.message || '删除ShopYY履约信息失败';
|
||||||
throw new Error(`删除ShopYY物流信息失败: ${errorMessage}`);
|
throw new Error(`删除ShopYY履约信息失败: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取ShopYY订单履约跟踪信息
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @returns 履约跟踪信息列表
|
||||||
|
*/
|
||||||
|
async getFulfillments(site: any, orderId: string): Promise<any[]> {
|
||||||
|
try {
|
||||||
|
// ShopYY API: GET /orders/{id}/shipments
|
||||||
|
const response = await this.request(site, `orders/${orderId}/fulfillments`, 'GET');
|
||||||
|
// 返回履约跟踪信息列表
|
||||||
|
return response.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
// 如果订单没有履约跟踪信息,返回空数组
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const errorMessage = error.response?.data?.msg || error.message || '获取履约跟踪信息失败';
|
||||||
|
throw new Error(`获取履约跟踪信息失败: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新ShopYY订单履约跟踪信息
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @param trackingId 履约跟踪ID
|
||||||
|
* @param data 更新数据
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
async updateFulfillment(site: any, orderId: string, trackingId: string, data: {
|
||||||
|
tracking_number?: string;
|
||||||
|
tracking_provider?: string;
|
||||||
|
date_shipped?: string;
|
||||||
|
status_shipped?: string;
|
||||||
|
}): Promise<any> {
|
||||||
|
try {
|
||||||
|
// ShopYY API: PUT /orders/{order_id}/shipments/{tracking_id}
|
||||||
|
const fulfillmentData: any = {};
|
||||||
|
|
||||||
|
// 只传递有值的字段
|
||||||
|
if (data.tracking_number !== undefined) {
|
||||||
|
fulfillmentData.tracking_number = data.tracking_number;
|
||||||
|
}
|
||||||
|
if (data.tracking_provider !== undefined) {
|
||||||
|
fulfillmentData.carrier_name = data.tracking_provider;
|
||||||
|
}
|
||||||
|
if (data.date_shipped !== undefined) {
|
||||||
|
fulfillmentData.shipped_at = data.date_shipped;
|
||||||
|
}
|
||||||
|
if (data.status_shipped !== undefined) {
|
||||||
|
fulfillmentData.status = data.status_shipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await this.request(site, `orders/${orderId}/fulfillments/${trackingId}`, 'PUT', fulfillmentData);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.msg || error.message || '更新履约跟踪信息失败';
|
||||||
|
throw new Error(`更新履约跟踪信息失败: ${errorMessage}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -900,4 +962,75 @@ export class ShopyyService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量履约订单
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param data 履约数据
|
||||||
|
* @returns 履约结果
|
||||||
|
*/
|
||||||
|
async batchFulfillOrders(site: any, data: {
|
||||||
|
order_number: string;
|
||||||
|
tracking_company: string;
|
||||||
|
tracking_number: string;
|
||||||
|
courier_code: number;
|
||||||
|
note: string;
|
||||||
|
mode: "replace" | 'cover' | null;
|
||||||
|
}): Promise<any> {
|
||||||
|
try {
|
||||||
|
// ShopYY API: POST /orders/fulfillment/batch
|
||||||
|
const response = await this.request(site, 'orders/fulfillment/batch', 'POST', data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.msg || error.message || '批量履约订单失败';
|
||||||
|
throw new Error(`批量履约订单失败: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 部分履约订单
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param data 部分履约数据
|
||||||
|
* @returns 履约结果
|
||||||
|
*/
|
||||||
|
async partFulfillOrder(site: any, data: {
|
||||||
|
order_number: string;
|
||||||
|
note: string;
|
||||||
|
tracking_company: string;
|
||||||
|
tracking_number: string;
|
||||||
|
courier_code: string;
|
||||||
|
products: Array<{
|
||||||
|
quantity: number;
|
||||||
|
order_product_id: string;
|
||||||
|
}>;
|
||||||
|
}): Promise<any> {
|
||||||
|
try {
|
||||||
|
// ShopYY API: POST /orders/fulfillment/part
|
||||||
|
const response = await this.request(site, 'orders/fulfillment/part', 'POST', data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.msg || error.message || '部分履约订单失败';
|
||||||
|
throw new Error(`部分履约订单失败: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消履约订单
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param data 取消履约数据
|
||||||
|
* @returns 取消结果
|
||||||
|
*/
|
||||||
|
async cancelFulfillment(site: any, data: {
|
||||||
|
order_id: string;
|
||||||
|
fullfillment_id: string;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
// ShopYY API: POST /orders/fulfillment/cancel
|
||||||
|
const response = await this.request(site, 'orders/fulfillment/cancel', 'POST', data);
|
||||||
|
return response.code === 0;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.msg || error.message || '取消履约订单失败';
|
||||||
|
throw new Error(`取消履约订单失败: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { ShopyyService } from './shopyy.service';
|
||||||
import { SiteService } from './site.service';
|
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';
|
||||||
|
|
||||||
@Provide()
|
@Provide()
|
||||||
export class SiteApiService {
|
export class SiteApiService {
|
||||||
|
|
@ -98,4 +99,115 @@ export class SiteApiService {
|
||||||
|
|
||||||
return enrichedProducts;
|
return enrichedProducts;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新或创建产品
|
||||||
|
* @param siteId 站点ID
|
||||||
|
* @param product 产品数据
|
||||||
|
* @returns 更新或创建后的产品
|
||||||
|
*/
|
||||||
|
async upsertProduct(siteId: number, product: Partial<UnifiedProductDTO>): Promise<any> {
|
||||||
|
const adapter = await this.getAdapter(siteId);
|
||||||
|
|
||||||
|
// 首先尝试查找产品
|
||||||
|
if (product.id) {
|
||||||
|
try {
|
||||||
|
// 尝试获取产品以确认它是否存在
|
||||||
|
const existingProduct = await adapter.getProduct(product.id);
|
||||||
|
if (existingProduct) {
|
||||||
|
// 产品存在,执行更新
|
||||||
|
return await adapter.updateProduct(product.id, product);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 如果获取产品失败,可能是因为产品不存在,继续执行创建逻辑
|
||||||
|
console.log(`产品 ${product.id} 不存在,将创建新产品:`, error.message);
|
||||||
|
}
|
||||||
|
} else if (product.sku) {
|
||||||
|
// 如果没有提供ID但提供了SKU,尝试通过SKU查找产品
|
||||||
|
try {
|
||||||
|
// 尝试搜索具有相同SKU的产品
|
||||||
|
const searchResult = await adapter.getProducts({ where: { sku: product.sku } });
|
||||||
|
if (searchResult.items && searchResult.items.length > 0) {
|
||||||
|
const existingProduct = searchResult.items[0];
|
||||||
|
// 找到现有产品,更新它
|
||||||
|
return await adapter.updateProduct(existingProduct.id, product);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 搜索失败,继续执行创建逻辑
|
||||||
|
console.log(`通过SKU搜索产品失败:`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 产品不存在,执行创建
|
||||||
|
return await adapter.createProduct(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新或创建产品
|
||||||
|
* @param siteId 站点ID
|
||||||
|
* @param products 产品数据数组
|
||||||
|
* @returns 批量操作结果
|
||||||
|
*/
|
||||||
|
async batchUpsertProduct(siteId: number, products: Partial<UnifiedProductDTO>[]): Promise<{ created: any[], updated: any[], errors: any[] }> {
|
||||||
|
const results = {
|
||||||
|
created: [],
|
||||||
|
updated: [],
|
||||||
|
errors: []
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const product of products) {
|
||||||
|
try {
|
||||||
|
const result = await this.upsertProduct(siteId, product);
|
||||||
|
// 判断是创建还是更新
|
||||||
|
if (result && result.id) {
|
||||||
|
// 简单判断:如果产品原本没有ID而现在有了,说明是创建的
|
||||||
|
if (!product.id || !product.id.toString().trim()) {
|
||||||
|
results.created.push(result);
|
||||||
|
} else {
|
||||||
|
results.updated.push(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
results.errors.push({
|
||||||
|
product: product,
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从站点获取产品
|
||||||
|
* @param siteId 站点ID
|
||||||
|
* @param params 查询参数
|
||||||
|
* @returns 站点产品列表
|
||||||
|
*/
|
||||||
|
async getProductsFromSite(siteId: number, params?: any): Promise<any> {
|
||||||
|
const adapter = await this.getAdapter(siteId);
|
||||||
|
return await adapter.getProducts(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从站点获取单个产品
|
||||||
|
* @param siteId 站点ID
|
||||||
|
* @param productId 产品ID
|
||||||
|
* @returns 站点产品
|
||||||
|
*/
|
||||||
|
async getProductFromSite(siteId: number, productId: string | number): Promise<any> {
|
||||||
|
const adapter = await this.getAdapter(siteId);
|
||||||
|
return await adapter.getProduct(productId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从站点获取所有产品
|
||||||
|
* @param siteId 站点ID
|
||||||
|
* @param params 查询参数
|
||||||
|
* @returns 站点产品列表
|
||||||
|
*/
|
||||||
|
async getAllProductsFromSite(siteId: number, params?: any): Promise<any[]> {
|
||||||
|
const adapter = await this.getAdapter(siteId);
|
||||||
|
return await adapter.getAllProducts(params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,9 @@ export class SiteService {
|
||||||
|
|
||||||
async create(data: CreateSiteDTO) {
|
async create(data: CreateSiteDTO) {
|
||||||
// 从 DTO 中分离出区域代码和其他站点数据
|
// 从 DTO 中分离出区域代码和其他站点数据
|
||||||
const { areas: areaCodes, stockPointIds, websiteUrl, ...restData } = data;
|
const { areas: areaCodes, stockPointIds, ...restData } = data;
|
||||||
const newSite = new Site();
|
const newSite = new Site();
|
||||||
Object.assign(newSite, restData, { websiteUrl });
|
Object.assign(newSite, restData);
|
||||||
|
|
||||||
// 如果传入了区域代码,则查询并关联 Area 实体
|
// 如果传入了区域代码,则查询并关联 Area 实体
|
||||||
if (areaCodes && areaCodes.length > 0) {
|
if (areaCodes && areaCodes.length > 0) {
|
||||||
|
|
@ -163,7 +163,7 @@ export class SiteService {
|
||||||
const data = includeSecret
|
const data = includeSecret
|
||||||
? items
|
? items
|
||||||
: items.map((item: any) => {
|
: items.map((item: any) => {
|
||||||
const { consumerKey, consumerSecret, ...rest } = item;
|
const { consumerKey, consumerSecret, token, ...rest } = item;
|
||||||
return rest;
|
return rest;
|
||||||
});
|
});
|
||||||
return { items: data, total, current, pageSize };
|
return { items: data, total, current, pageSize };
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,11 @@ export class StatisticsService {
|
||||||
orderItemRepository: Repository<OrderItem>;
|
orderItemRepository: Repository<OrderItem>;
|
||||||
|
|
||||||
async getOrderStatistics(params: OrderStatisticsParams) {
|
async getOrderStatistics(params: OrderStatisticsParams) {
|
||||||
const { startDate, endDate, siteId ,grouping} = params;
|
const { startDate, endDate, grouping, siteId } = params;
|
||||||
// const keywords = keyword ? keyword.split(' ').filter(Boolean) : [];
|
// const keywords = keyword ? keyword.split(' ').filter(Boolean) : [];
|
||||||
const start = dayjs(startDate).format('YYYY-MM-DD');
|
const start = dayjs(startDate).format('YYYY-MM-DD');
|
||||||
const end = dayjs(endDate).add(1, 'd').format('YYYY-MM-DD');
|
const end = dayjs(endDate).add(1, 'd').format('YYYY-MM-DD');
|
||||||
let sql = '';
|
let sql
|
||||||
if (!grouping || grouping === 'day') {
|
if (!grouping || grouping === 'day') {
|
||||||
sql = `
|
sql = `
|
||||||
WITH first_order AS (
|
WITH first_order AS (
|
||||||
|
|
@ -216,7 +216,7 @@ export class StatisticsService {
|
||||||
dt.can_total_orders
|
dt.can_total_orders
|
||||||
ORDER BY d.order_date DESC;
|
ORDER BY d.order_date DESC;
|
||||||
`;
|
`;
|
||||||
}else if (grouping === 'week') {
|
} else if (grouping === 'week') {
|
||||||
sql = `WITH first_order AS (
|
sql = `WITH first_order AS (
|
||||||
SELECT customer_email, MIN(date_paid) AS first_purchase_date
|
SELECT customer_email, MIN(date_paid) AS first_purchase_date
|
||||||
FROM \`order\`
|
FROM \`order\`
|
||||||
|
|
@ -409,7 +409,7 @@ export class StatisticsService {
|
||||||
wt.can_total_orders
|
wt.can_total_orders
|
||||||
ORDER BY wt.order_date DESC;
|
ORDER BY wt.order_date DESC;
|
||||||
`;
|
`;
|
||||||
}else if (grouping === 'month') {
|
} else if (grouping === 'month') {
|
||||||
sql = `WITH first_order AS (
|
sql = `WITH first_order AS (
|
||||||
SELECT customer_email, MIN(date_paid) AS first_purchase_date
|
SELECT customer_email, MIN(date_paid) AS first_purchase_date
|
||||||
FROM \`order\`
|
FROM \`order\`
|
||||||
|
|
@ -601,7 +601,8 @@ export class StatisticsService {
|
||||||
mt.togo_total_orders,
|
mt.togo_total_orders,
|
||||||
mt.can_total_orders
|
mt.can_total_orders
|
||||||
ORDER BY mt.order_date DESC;
|
ORDER BY mt.order_date DESC;
|
||||||
`;}
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
return this.orderRepository.query(sql);
|
return this.orderRepository.query(sql);
|
||||||
}
|
}
|
||||||
|
|
@ -1312,7 +1313,7 @@ export class StatisticsService {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOrderSorce(params){
|
async getOrderSorce(params) {
|
||||||
const sql = `
|
const sql = `
|
||||||
WITH cutoff_months AS (
|
WITH cutoff_months AS (
|
||||||
SELECT
|
SELECT
|
||||||
|
|
@ -1468,13 +1469,13 @@ export class StatisticsService {
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
const [res, inactiveRes ] = await Promise.all([
|
const [res, inactiveRes] = await Promise.all([
|
||||||
this.orderRepository.query(sql),
|
this.orderRepository.query(sql),
|
||||||
this.orderRepository.query(inactiveSql),
|
this.orderRepository.query(inactiveSql),
|
||||||
])
|
])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
res,inactiveRes
|
res, inactiveRes
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -509,7 +509,7 @@ export class WPService implements IPlatformService {
|
||||||
productId: string,
|
productId: string,
|
||||||
variationId: string,
|
variationId: string,
|
||||||
data: Partial<WooVariation & any>
|
data: Partial<WooVariation & any>
|
||||||
): Promise<boolean> {
|
): Promise<WooVariation> {
|
||||||
const { regular_price, sale_price, ...params } = data;
|
const { regular_price, sale_price, ...params } = data;
|
||||||
const api = this.createApi(site, 'wc/v3');
|
const api = this.createApi(site, 'wc/v3');
|
||||||
const updateData: any = { ...params };
|
const updateData: any = { ...params };
|
||||||
|
|
@ -521,14 +521,65 @@ export class WPService implements IPlatformService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.put(`products/${productId}/variations/${variationId}`, updateData);
|
const res = await api.put(`products/${productId}/variations/${variationId}`, updateData);
|
||||||
return true;
|
return res.data as WooVariation;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('更新产品变体失败:', error.response?.data || error.message);
|
console.error('更新产品变体失败:', error.response?.data || error.message);
|
||||||
throw new Error(`更新产品变体失败: ${error.response?.data?.message || error.message}`);
|
throw new Error(`更新产品变体失败: ${error.response?.data?.message || error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建 WooCommerce 产品变体
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param productId 产品 ID
|
||||||
|
* @param data 创建变体的数据
|
||||||
|
*/
|
||||||
|
async createVariation(
|
||||||
|
site: any,
|
||||||
|
productId: string,
|
||||||
|
data: Partial<WooVariation & any>
|
||||||
|
): Promise<WooVariation> {
|
||||||
|
const { regular_price, sale_price, ...params } = data;
|
||||||
|
const api = this.createApi(site, 'wc/v3');
|
||||||
|
const createData: any = { ...params };
|
||||||
|
if (regular_price !== undefined && regular_price !== null) {
|
||||||
|
createData.regular_price = String(regular_price);
|
||||||
|
}
|
||||||
|
if (sale_price !== undefined && sale_price !== null) {
|
||||||
|
createData.sale_price = String(sale_price);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.post(`products/${productId}/variations`, createData);
|
||||||
|
return res.data as WooVariation;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('创建产品变体失败:', error.response?.data || error.message);
|
||||||
|
throw new Error(`创建产品变体失败: ${error.response?.data?.message || error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除 WooCommerce 产品变体
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param productId 产品 ID
|
||||||
|
* @param variationId 变体 ID
|
||||||
|
*/
|
||||||
|
async deleteVariation(
|
||||||
|
site: any,
|
||||||
|
productId: string,
|
||||||
|
variationId: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
const api = this.createApi(site, 'wc/v3');
|
||||||
|
try {
|
||||||
|
await api.delete(`products/${productId}/variations/${variationId}`, { force: true });
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除产品变体失败:', error.response?.data || error.message);
|
||||||
|
throw new Error(`删除产品变体失败: ${error.response?.data?.message || error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新 Order
|
* 更新 Order
|
||||||
*/
|
*/
|
||||||
|
|
@ -547,7 +598,7 @@ export class WPService implements IPlatformService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async createShipment(
|
async createFulfillment(
|
||||||
site: any,
|
site: any,
|
||||||
orderId: string,
|
orderId: string,
|
||||||
data: Record<string, any>
|
data: Record<string, any>
|
||||||
|
|
@ -575,10 +626,10 @@ export class WPService implements IPlatformService {
|
||||||
return await axios.request(config);
|
return await axios.request(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteShipment(
|
async deleteFulfillment(
|
||||||
site: any,
|
site: any,
|
||||||
orderId: string,
|
orderId: string,
|
||||||
trackingId: string,
|
fulfillmentId: string,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const apiUrl = site.apiUrl;
|
const apiUrl = site.apiUrl;
|
||||||
const { consumerKey, consumerSecret } = site;
|
const { consumerKey, consumerSecret } = site;
|
||||||
|
|
@ -586,7 +637,7 @@ export class WPService implements IPlatformService {
|
||||||
'base64'
|
'base64'
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('del', orderId, trackingId);
|
console.log('del', orderId, fulfillmentId);
|
||||||
// 删除接口: DELETE /wp-json/wc-shipment-tracking/v3/orders/<order_id>/shipment-trackings/<tracking_id>
|
// 删除接口: DELETE /wp-json/wc-shipment-tracking/v3/orders/<order_id>/shipment-trackings/<tracking_id>
|
||||||
const config: AxiosRequestConfig = {
|
const config: AxiosRequestConfig = {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
|
|
@ -597,7 +648,7 @@ export class WPService implements IPlatformService {
|
||||||
'wc-ast/v3/orders',
|
'wc-ast/v3/orders',
|
||||||
orderId,
|
orderId,
|
||||||
'shipment-trackings',
|
'shipment-trackings',
|
||||||
trackingId
|
fulfillmentId
|
||||||
),
|
),
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Basic ${auth}`,
|
Authorization: `Basic ${auth}`,
|
||||||
|
|
@ -612,6 +663,116 @@ export class WPService implements IPlatformService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取订单履约跟踪信息
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @returns 履约跟踪信息列表
|
||||||
|
*/
|
||||||
|
async getFulfillments(
|
||||||
|
site: any,
|
||||||
|
orderId: string,
|
||||||
|
): Promise<any[]> {
|
||||||
|
const apiUrl = site.apiUrl;
|
||||||
|
const { consumerKey, consumerSecret } = site;
|
||||||
|
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
|
||||||
|
'base64'
|
||||||
|
);
|
||||||
|
|
||||||
|
const config: AxiosRequestConfig = {
|
||||||
|
method: 'GET',
|
||||||
|
url: this.buildURL(
|
||||||
|
apiUrl,
|
||||||
|
'/wp-json',
|
||||||
|
'wc-ast/v3/orders',
|
||||||
|
orderId,
|
||||||
|
'shipment-trackings'
|
||||||
|
),
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${auth}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.request(config);
|
||||||
|
return response.data || [];
|
||||||
|
} catch (error) {
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const errorMessage = error.response?.data?.message || error.message || '获取履约跟踪信息失败';
|
||||||
|
throw new Error(`获取履约跟踪信息失败: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新订单履约跟踪信息
|
||||||
|
* @param site 站点配置
|
||||||
|
* @param orderId 订单ID
|
||||||
|
* @param fulfillmentId 跟踪ID
|
||||||
|
* @param data 更新数据
|
||||||
|
* @returns 更新结果
|
||||||
|
*/
|
||||||
|
async updateFulfillment(
|
||||||
|
site: any,
|
||||||
|
orderId: string,
|
||||||
|
fulfillmentId: string,
|
||||||
|
data: {
|
||||||
|
tracking_number?: string;
|
||||||
|
tracking_provider?: string;
|
||||||
|
date_shipped?: string;
|
||||||
|
status_shipped?: string;
|
||||||
|
}
|
||||||
|
): Promise<any> {
|
||||||
|
const apiUrl = site.apiUrl;
|
||||||
|
const { consumerKey, consumerSecret } = site;
|
||||||
|
const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString(
|
||||||
|
'base64'
|
||||||
|
);
|
||||||
|
|
||||||
|
const fulfillmentData: any = {};
|
||||||
|
|
||||||
|
if (data.tracking_provider !== undefined) {
|
||||||
|
fulfillmentData.tracking_provider = data.tracking_provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.tracking_number !== undefined) {
|
||||||
|
fulfillmentData.tracking_number = data.tracking_number;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.date_shipped !== undefined) {
|
||||||
|
fulfillmentData.date_shipped = data.date_shipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.status_shipped !== undefined) {
|
||||||
|
fulfillmentData.status_shipped = data.status_shipped;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: AxiosRequestConfig = {
|
||||||
|
method: 'PUT',
|
||||||
|
url: this.buildURL(
|
||||||
|
apiUrl,
|
||||||
|
'/wp-json',
|
||||||
|
'wc-ast/v3/orders',
|
||||||
|
orderId,
|
||||||
|
'shipment-trackings',
|
||||||
|
fulfillmentId
|
||||||
|
),
|
||||||
|
headers: {
|
||||||
|
Authorization: `Basic ${auth}`,
|
||||||
|
},
|
||||||
|
data: fulfillmentData,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.request(config);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error.response?.data?.message || error.message || '更新履约跟踪信息失败';
|
||||||
|
throw new Error(`更新履约跟踪信息失败: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 批量处理产品 (Create, Update, Delete)
|
* 批量处理产品 (Create, Update, Delete)
|
||||||
* @param site 站点配置
|
* @param site 站点配置
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
|
import { ApiProperty } from "@midwayjs/swagger";
|
||||||
|
|
||||||
// 通用响应结构
|
// 通用响应结构
|
||||||
export class ApiResponse<T> {
|
export class ApiResponse<T> {
|
||||||
|
@ApiProperty({ description: '状态码' })
|
||||||
code: number;
|
code: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '是否成功' })
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '提示信息' })
|
||||||
message: string;
|
message: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '返回数据' })
|
||||||
data: T;
|
data: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue