feat: 修复产品与站点同步诸多问题

1. 新增产品与站点同步相关DTO和服务方法
2. 重构产品实体与站点SKU的关联关系
3. 优化分类实体,增加短名字段用于SKU生成
4. 完善API响应DTO的Swagger注解
5. 新增Dockerfile支持容器化部署
6. 重构订单同步接口,返回更详细的同步结果
7. 优化物流服务接口命名,使用fulfillment替代shipment
8. 新增数据库初始化逻辑,自动创建数据库
9. 重构产品控制器,支持批量同步操作
10. 更新模板配置,支持站点SKU前缀
11. 删除废弃的迁移文件和实体
12. 优化产品查询接口,支持更灵活的过滤条件
This commit is contained in:
tikkhun 2025-12-31 11:55:59 +08:00
parent 84beb1a65e
commit 43e0d8d40d
50 changed files with 4583 additions and 1475 deletions

23
Dockerfile Normal file
View File

@ -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"]

View File

@ -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,9 @@ import {
UnifiedWebhookPaginationDTO, UnifiedWebhookPaginationDTO,
CreateWebhookDTO, CreateWebhookDTO,
UpdateWebhookDTO, UpdateWebhookDTO,
UnifiedShippingLineDTO, UnifiedAddressDTO
} from '../dto/site-api.dto'; } from '../dto/site-api.dto';
import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto';
import { import {
ShopyyCustomer, ShopyyCustomer,
ShopyyOrder, ShopyyOrder,
@ -138,7 +136,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 +154,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 ||
@ -294,7 +292,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 +467,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 +476,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 +821,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 暂未实现');
}
} }

View File

@ -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,
@ -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}`);
// } // }
} }
@ -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)}`);
}
}
} }

View File

@ -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,
}, },

View File

@ -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();
}
}
}
} }

View File

@ -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);

View File

@ -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);
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);
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);
}
}
} }

View File

@ -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 ()

View File

@ -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('同步失败');

View File

@ -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);
}
}
} }

View File

@ -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);
} }
} }

View File

@ -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 || '获取失败');
} }

View File

@ -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\``);
}
}

View File

@ -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\`)`);
}
}

View File

@ -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\``);
}
}

View File

@ -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`);
}
}

View File

@ -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`);
}
}

View File

@ -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`);
}
}

View File

@ -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.');
}
}
}

View File

@ -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: '装满桃浆果Popn 桃浆果)', 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

View File

@ -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',
},
}), }),
}, },
]; ];

182
src/dto/api.dto.ts Normal file
View File

@ -0,0 +1,182 @@
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: '搜索关键词', 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[];
}

View File

@ -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: number;
@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?: number;
@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>;
} }

View File

@ -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
@ -251,35 +252,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;
} }

View File

@ -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) {}
//产品分页数据 //产品分页数据

View File

@ -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[]
} }

View File

@ -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[];
} }

51
src/dto/site-sync.dto.ts Normal file
View File

@ -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[];
}

View File

@ -126,7 +126,96 @@ 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;

View File

@ -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[];
} }

View File

@ -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 })

View File

@ -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 })

View File

@ -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' })

View File

@ -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;
}

View File

@ -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[];
} }

View File

@ -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)

View File

@ -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;

View File

@ -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;
}

View File

@ -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>;
/** /**
* *

View File

@ -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>;
} }

View File

@ -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++;
} }
} }
@ -182,8 +177,8 @@ export class CustomerService {
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 +190,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 +255,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 +269,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 +318,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 +339,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 +366,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 +443,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;
}
} }

View File

@ -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' });
}
} }

View File

@ -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,
}); });

View File

@ -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,7 +200,6 @@ 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)
@ -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: 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 {

View File

@ -1,35 +1,40 @@
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 +65,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 +184,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 +209,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 +230,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 +268,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 +410,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 +501,8 @@ export class ProductService {
return { return {
items, items,
total, total,
...pagination, current: page,
pageSize,
}; };
} }
@ -438,20 +637,16 @@ 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 列表 // 设置站点 SKU 列表(确保始终设置,即使为空数组)
if (createProductDTO.siteSkus && createProductDTO.siteSkus.length > 0) { if (createProductDTO.siteSkus !== undefined) {
const siteSkus = createProductDTO.siteSkus.map(code => { product.siteSkus = createProductDTO.siteSkus;
const s = new ProductSiteSku(); // 重新保存产品以更新 siteSkus
s.siteSku = code; await this.productModel.save(product);
s.product = savedProduct;
return s;
});
await this.productSiteSkuModel.save(siteSkus);
} }
// 保存组件信息 // 保存组件信息
@ -492,19 +687,7 @@ export class ProductService {
// 处理站点 SKU 更新 // 处理站点 SKU 更新
if (updateProductDTO.siteSkus !== undefined) { if (updateProductDTO.siteSkus !== undefined) {
// 删除旧的 siteSkus product.siteSkus = updateProductDTO.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 更新
@ -771,40 +954,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 +1553,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),
@ -1611,20 +1760,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 +1792,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;
} }
} }

View File

@ -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}`);
}
}
} }

View File

@ -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);
}
} }

View File

@ -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 (
@ -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);
} }

View File

@ -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

View File

@ -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;
} }