From 43e0d8d40d716745cc9c35ba72b7c9286b1c64a9 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 31 Dec 2025 11:55:59 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E4=BF=AE=E5=A4=8D=E4=BA=A7?= =?UTF-8?q?=E5=93=81=E4=B8=8E=E7=AB=99=E7=82=B9=E5=90=8C=E6=AD=A5=E8=AF=B8?= =?UTF-8?q?=E5=A4=9A=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 新增产品与站点同步相关DTO和服务方法 2. 重构产品实体与站点SKU的关联关系 3. 优化分类实体,增加短名字段用于SKU生成 4. 完善API响应DTO的Swagger注解 5. 新增Dockerfile支持容器化部署 6. 重构订单同步接口,返回更详细的同步结果 7. 优化物流服务接口命名,使用fulfillment替代shipment 8. 新增数据库初始化逻辑,自动创建数据库 9. 重构产品控制器,支持批量同步操作 10. 更新模板配置,支持站点SKU前缀 11. 删除废弃的迁移文件和实体 12. 优化产品查询接口,支持更灵活的过滤条件 --- Dockerfile | 23 + src/adapter/shopyy.adapter.ts | 168 +++- src/adapter/woocommerce.adapter.ts | 545 ++++++++++-- src/config/config.default.ts | 4 +- src/configuration.ts | 81 ++ src/controller/category.controller.ts | 5 +- src/controller/customer.controller.ts | 174 +++- src/controller/dict.controller.ts | 30 +- src/controller/order.controller.ts | 8 +- src/controller/product.controller.ts | 112 ++- src/controller/site-api.controller.ts | 209 ++++- src/controller/site.controller.ts | 3 +- ...38434984-product-dict-item-many-to-many.ts | 32 - src/db/migrations/1764294088896-Area.ts | 45 - .../migrations/1764299629279-ProductStock.ts | 16 - ...7170-update-dict-item-unique-constraint.ts | 46 - ...1765275715762-add_test_data_to_template.ts | 68 -- .../1765330208213-add-site-description.ts | 46 - ...1765358400000-update-product-table-name.ts | 91 -- src/db/seeds/dict.seeder.ts | 841 ++++++++++++++++-- src/db/seeds/template.seeder.ts | 41 +- src/dto/api.dto.ts | 182 ++++ src/dto/customer.dto.ts | 386 +++++++- src/dto/product.dto.ts | 122 ++- src/dto/reponse.dto.ts | 3 + src/dto/shopyy.dto.ts | 50 +- src/dto/site-api.dto.ts | 296 ++++-- src/dto/site-sync.dto.ts | 51 ++ src/dto/woocommerce.dto.ts | 100 ++- src/entity/area.entity.ts | 10 +- src/entity/category.entity.ts | 4 + src/entity/order.entity.ts | 6 +- src/entity/product.entity.ts | 19 +- src/entity/product_site_sku.entity.ts | 36 - src/entity/site.entity.ts | 24 +- src/entity/stock_point.entity.ts | 12 +- src/entity/user.entity.ts | 2 +- src/entity/wp_product.entity.ts | 227 ----- src/interface/platform.interface.ts | 16 +- src/interface/site-adapter.interface.ts | 85 +- src/service/customer.service.ts | 396 +++++---- src/service/dict.service.ts | 191 +++- src/service/logistics.service.ts | 6 +- src/service/order.service.ts | 127 ++- src/service/product.service.ts | 626 ++++++++++--- src/service/shopyy.service.ts | 161 +++- src/service/site-api.service.ts | 112 +++ src/service/statistics.service.ts | 37 +- src/service/wp.service.ts | 177 +++- src/utils/response.util.ts | 6 + 50 files changed, 4583 insertions(+), 1475 deletions(-) create mode 100644 Dockerfile delete mode 100644 src/db/migrations/1764238434984-product-dict-item-many-to-many.ts delete mode 100644 src/db/migrations/1764294088896-Area.ts delete mode 100644 src/db/migrations/1764299629279-ProductStock.ts delete mode 100644 src/db/migrations/1764569947170-update-dict-item-unique-constraint.ts delete mode 100644 src/db/migrations/1765275715762-add_test_data_to_template.ts delete mode 100644 src/db/migrations/1765330208213-add-site-description.ts delete mode 100644 src/db/migrations/1765358400000-update-product-table-name.ts create mode 100644 src/dto/api.dto.ts create mode 100644 src/dto/site-sync.dto.ts delete mode 100644 src/entity/product_site_sku.entity.ts delete mode 100644 src/entity/wp_product.entity.ts diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..14133d6 --- /dev/null +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 528d750..178dfc7 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -1,15 +1,12 @@ import { ISiteAdapter } from '../interface/site-adapter.interface'; import { ShopyyService } from '../service/shopyy.service'; import { - UnifiedAddressDTO, UnifiedCustomerDTO, UnifiedMediaDTO, UnifiedOrderDTO, UnifiedOrderLineItemDTO, - UnifiedPaginationDTO, UnifiedProductDTO, UnifiedProductVariationDTO, - UnifiedSearchParamsDTO, UnifiedSubscriptionDTO, UnifiedReviewPaginationDTO, UnifiedReviewDTO, @@ -17,8 +14,9 @@ import { UnifiedWebhookPaginationDTO, CreateWebhookDTO, UpdateWebhookDTO, - UnifiedShippingLineDTO, + UnifiedAddressDTO } from '../dto/site-api.dto'; +import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; import { ShopyyCustomer, ShopyyOrder, @@ -138,7 +136,6 @@ export class ShopyyAdapter implements ISiteAdapter { [180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed [190]: OrderStatus.CANCEL // 190 取消 转为 cancelled } - private mapOrder(item: ShopyyOrder): UnifiedOrderDTO { // 提取账单和送货地址 如果不存在则为空对象 const billing = (item as any).billing_address || {}; @@ -157,6 +154,7 @@ export class ShopyyAdapter implements ISiteAdapter { city: billing.city || item.payment_city || '', state: billing.province || item.payment_zone || '', postcode: billing.zip || item.payment_postcode || '', + method_title: item.payment_method || '', country: billing.country_name || billing.country_code || @@ -294,7 +292,6 @@ export class ShopyyAdapter implements ISiteAdapter { } - private mapCustomer(item: ShopyyCustomer): UnifiedCustomerDTO { // 处理多地址结构 const addresses = item.addresses || []; @@ -470,7 +467,7 @@ export class ShopyyAdapter implements ISiteAdapter { return await this.shopyyService.deleteOrder(this.site, id); } - async shipOrder(orderId: string | number, data: { + async fulfillOrder(orderId: string | number, data: { tracking_number?: string; shipping_provider?: string; shipping_method?: string; @@ -479,66 +476,123 @@ export class ShopyyAdapter implements ISiteAdapter { quantity: number; }>; }): Promise { - // 订单发货 + // 订单履行(发货) try { - // 更新订单状态为已发货 - await this.shopyyService.updateOrder(this.site, String(orderId), { - status: 'completed', - meta_data: [ - { key: '_tracking_number', value: data.tracking_number }, - { key: '_shipping_provider', value: data.shipping_provider }, - { key: '_shipping_method', value: data.shipping_method } - ] - }); - - // 添加发货备注 - const note = `订单已发货${data.tracking_number ? `,物流单号:${data.tracking_number}` : ''}${data.shipping_provider ? `,物流公司:${data.shipping_provider}` : ''}`; - 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() - }; + // 判断是否为部分发货(包含 items) + if (data.items && data.items.length > 0) { + // 部分发货 + const partShipData = { + order_number: String(orderId), + note: data.shipping_method || '', + tracking_company: data.shipping_provider || '', + tracking_number: data.tracking_number || '', + courier_code: '1', // 默认快递公司代码 + products: data.items.map(item => ({ + quantity: item.quantity, + order_product_id: String(item.order_item_id) + })) + }; + 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) { - throw new Error(`发货失败: ${error.message}`); + throw new Error(`履行失败: ${error.message}`); } } - async cancelShipOrder(orderId: string | number, data: { + async cancelFulfillment(orderId: string | number, data: { reason?: string; shipment_id?: string; }): Promise { - // 取消订单发货 + // 取消订单履行 try { - // 将订单状态改回处理中 - await this.shopyyService.updateOrder(this.site, String(orderId), { - status: 'processing', - meta_data: [ - { key: '_shipment_cancelled', value: 'yes' }, - { key: '_shipment_cancelled_reason', value: data.reason } - ] - }); - - // 添加取消发货的备注 - const note = `订单发货已取消${data.reason ? `,原因:${data.reason}` : ''}`; - await this.shopyyService.createOrderNote(this.site, orderId, { note, customer_note: true }); + // 调用 ShopyyService 的取消履行方法 + const cancelShipData = { + order_id: String(orderId), + fullfillment_id: data.shipment_id || '' + }; + const result = await this.shopyyService.cancelFulfillment(this.site, cancelShipData); return { - success: true, + success: result, order_id: orderId, shipment_id: data.shipment_id, reason: data.reason, cancelled_at: new Date().toISOString() }; } catch (error) { - throw new Error(`取消发货失败: ${error.message}`); + throw new Error(`取消履行失败: ${error.message}`); } } + /** + * 获取订单履行信息 + * @param orderId 订单ID + * @returns 履行信息列表 + */ + async getOrderFulfillments(orderId: string | number): Promise { + 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 { + // 调用 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 { + 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 { + return await this.shopyyService.deleteFulfillment(this.site, String(orderId), fulfillmentId); + } + async getSubscriptions( params: UnifiedSearchParamsDTO ): Promise> { @@ -767,4 +821,24 @@ export class ShopyyAdapter implements ISiteAdapter { async deleteCustomer(id: string | number): Promise { return await this.shopyyService.deleteCustomer(this.site, id); } + + async getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise { + throw new Error('Shopyy getVariations 暂未实现'); + } + + async getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise { + throw new Error('Shopyy getAllVariations 暂未实现'); + } + + async getVariation(productId: string | number, variationId: string | number): Promise { + throw new Error('Shopyy getVariation 暂未实现'); + } + + async createVariation(productId: string | number, data: any): Promise { + throw new Error('Shopyy createVariation 暂未实现'); + } + + async deleteVariation(productId: string | number, variationId: string | number): Promise { + throw new Error('Shopyy deleteVariation 暂未实现'); + } } diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index be2a9f6..30860b3 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -2,9 +2,7 @@ import { ISiteAdapter } from '../interface/site-adapter.interface'; import { UnifiedMediaDTO, UnifiedOrderDTO, - UnifiedPaginationDTO, UnifiedProductDTO, - UnifiedSearchParamsDTO, UnifiedSubscriptionDTO, UnifiedCustomerDTO, UnifiedReviewPaginationDTO, @@ -13,7 +11,12 @@ import { UnifiedWebhookPaginationDTO, CreateWebhookDTO, UpdateWebhookDTO, + CreateVariationDTO, + UpdateVariationDTO, + UnifiedProductVariationDTO, + UnifiedVariationPaginationDTO, } from '../dto/site-api.dto'; +import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { WooProduct, WooOrder, @@ -138,7 +141,7 @@ export class WooCommerceAdapter implements ISiteAdapter { } } - async getLinks(): Promise> { + async getLinks(): Promise> { const baseUrl = this.site.apiUrl; const links = [ { title: '访问网站', url: baseUrl }, @@ -164,13 +167,13 @@ export class WooCommerceAdapter implements ISiteAdapter { throw new Error('Method not implemented.'); } - + private mapProductSearchParams(params: UnifiedSearchParamsDTO): Partial { const page = Number(params.page ?? 1); - const per_page = Number( params.per_page ?? 20); + const per_page = Number(params.per_page ?? 20); const where = params.where && typeof params.where === 'object' ? params.where : {}; - + const mapped: any = { ...(params.search ? { search: params.search } : {}), ...(where.status ? { status: where.status } : {}), @@ -225,10 +228,10 @@ export class WooCommerceAdapter implements ISiteAdapter { private mapOrderSearchParams(params: UnifiedSearchParamsDTO): Partial { // 计算分页参数 const page = Number(params.page ?? 1); - const per_page = Number( params.per_page ?? 20); + const per_page = Number(params.per_page ?? 20); // 解析排序参数 支持从 order 对象推导 const where = params.where && typeof params.where === 'object' ? params.where : {}; - + // if (params.orderBy && typeof params.orderBy === 'object') { // } const mapped: any = { @@ -294,7 +297,7 @@ export class WooCommerceAdapter implements ISiteAdapter { const page = Number(params.page ?? 1); const per_page = Number(params.per_page ?? 20); const where = params.where && typeof params.where === 'object' ? params.where : {}; - + const mapped: any = { ...(params.search ? { search: params.search } : {}), @@ -302,6 +305,23 @@ export class WooCommerceAdapter implements ISiteAdapter { 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[] => { if (Array.isArray(value)) return value; if (value === undefined || value === null) return []; @@ -330,6 +350,49 @@ export class WooCommerceAdapter implements ISiteAdapter { // 将 WooCommerce 产品数据映射为统一产品DTO // 保留常用字段与时间信息以便前端统一展示 // 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 { id: item.id, date_created: item.date_created, @@ -358,37 +421,47 @@ export class WooCommerceAdapter implements ISiteAdapter { id: t.id, name: t.name, })), - attributes: (item.attributes || []).map(attr => ({ + attributes: (item.attributes || []).map(attr => ({ id: attr.id, name: attr.name || '', position: attr.position, visible: attr.visible, variation: attr.variation, - options: attr.options || [] + options: attr.options || [] })), - variations: item.variations as any, + variations: mappedVariations, permalink: item.permalink, raw: item, }; } private buildFullAddress(addr: any): string { - if (!addr) return ''; - const name = addr.fullname || `${addr.first_name || ''} ${addr.last_name || ''}`.trim(); - return [ - name, - addr.company, - addr.address_1, - addr.address_2, - addr.city, - addr.state, - addr.postcode, - addr.country, - addr.phone - ].filter(Boolean).join(', '); + if (!addr) return ''; + const name = addr.fullname || `${addr.first_name || ''} ${addr.last_name || ''}`.trim(); + return [ + name, + addr.company, + addr.address_1, + addr.address_2, + addr.city, + addr.state, + addr.postcode, + addr.country, + addr.phone + ].filter(Boolean).join(', '); } private mapOrder(item: WooOrder): UnifiedOrderDTO { // 将 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 { id: item.id, number: item.number, @@ -396,20 +469,19 @@ export class WooCommerceAdapter implements ISiteAdapter { currency: item.currency, total: item.total, customer_id: item.customer_id, - customer_name: `${item.billing?.first_name || ''} ${ - item.billing?.last_name || '' - }`.trim(), + customer_email: item.billing?.email || '', // TODO 与 email 重复 保留一个即可 + email: item.billing?.email || '', + customer_name: `${item.billing?.first_name || ''} ${item.billing?.last_name || ''}`.trim(), refunds: item.refunds?.map?.(refund => ({ id: refund.id, reason: refund.reason, total: refund.total, })), - email: item.billing?.email || '', line_items: (item.line_items as any[]).map(li => ({ - ...li, + ...li, productId: li.product_id, })), - + billing: item.billing, shipping: item.shipping, billing_full_address: this.buildFullAddress(item.billing), @@ -420,17 +492,11 @@ export class WooCommerceAdapter implements ISiteAdapter { shipping_lines: item.shipping_lines, fee_lines: item.fee_lines, coupon_lines: item.coupon_lines, - utm_source: item?.meta_data?.find(el => el.key === '_wc_order_attribution_utm_source')?.value || '', - 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 || '', + tracking: tracking, raw: item, }; } - - - private mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO { // 将 WooCommerce 订阅数据映射为统一订阅DTO // 若缺少创建时间则回退为开始时间 @@ -477,8 +543,31 @@ export class WooCommerceAdapter implements ISiteAdapter { 'products', 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 { - items: items.map(this.mapProduct), + items: productsWithVariations.map(this.mapProduct), total, totalPages, page, @@ -491,14 +580,55 @@ export class WooCommerceAdapter implements ISiteAdapter { // 使用sdkGetAll获取所有产品数据,不受分页限制 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); 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 { // 获取单个产品详情并映射为统一产品DTO const api = (this.wpService as any).createApi(this.site, 'wc/v3'); 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): Promise { @@ -513,12 +643,6 @@ export class WooCommerceAdapter implements ISiteAdapter { return res } - async updateVariation(productId: string | number, variationId: string | number, data: any): Promise { - // 更新变体信息并返回结果 - const res = await this.wpService.updateVariation(this.site, String(productId), String(variationId), data); - return res; - } - async getOrderNotes(orderId: string | number): Promise { // 获取订单备注列表 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 { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged(this.site, 'orders', requestParams); + + // 并行获取所有订单的履行信息 + const ordersWithTracking = await Promise.all( + items.map(async (order: any) => { + try { + // 获取订单的履行信息 + const trackings = await this.getOrderFulfillments(order.id); + // 将履行信息添加到订单对象中 + return { + ...order, + trackings: trackings || [] + }; + } catch (error) { + // 如果获取履行信息失败,仍然返回订单,只是履行信息为空数组 + console.error(`获取订单 ${order.id} 的履行信息失败:`, error); + return { + ...order, + trackings: [] + }; + } + }) + ); + return { - items: items.map(this.mapOrder), + items: ordersWithTracking.map(this.mapOrder), total, totalPages, page, per_page, - }; } @@ -600,7 +746,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return true; } - async shipOrder(orderId: string | number, data: { + async fulfillOrder(orderId: string | number, data: { tracking_number?: string; shipping_provider?: string; shipping_method?: string; @@ -610,13 +756,13 @@ export class WooCommerceAdapter implements ISiteAdapter { }>; }): Promise { throw new Error('暂无实现') - // 订单发货 + // 订单履行(发货) // const api = (this.wpService as any).createApi(this.site, 'wc/v3'); // try { // // 更新订单状态为已完成 // await api.put(`orders/${orderId}`, { status: 'completed' }); - + // // 如果提供了物流信息,添加到订单备注 // if (data.tracking_number || data.shipping_provider) { // const note = `订单已发货${data.tracking_number ? `,物流单号:${data.tracking_number}` : ''}${data.shipping_provider ? `,物流公司:${data.shipping_provider}` : ''}`; @@ -626,30 +772,30 @@ export class WooCommerceAdapter implements ISiteAdapter { // return { // success: true, // order_id: orderId, - // shipment_id: `shipment_${orderId}_${Date.now()}`, + // fulfillment_id: `fulfillment_${orderId}_${Date.now()}`, // tracking_number: data.tracking_number, // shipping_provider: data.shipping_provider, - // shipped_at: new Date().toISOString() + // fulfilled_at: new Date().toISOString() // }; // } 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; shipment_id?: string; }): Promise { throw new Error('暂未实现') - // 取消订单发货 + // 取消订单履行 // const api = (this.wpService as any).createApi(this.site, 'wc/v3'); - + // try { // // 将订单状态改回处理中 // 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 }); // return { @@ -660,7 +806,7 @@ export class WooCommerceAdapter implements ISiteAdapter { // cancelled_at: new Date().toISOString() // }; // } catch (error) { - // throw new Error(`取消发货失败: ${error.message}`); + // throw new Error(`取消履行失败: ${error.message}`); // } } @@ -715,7 +861,7 @@ export class WooCommerceAdapter implements ISiteAdapter { return media.map((mediaItem: any) => this.mapMedia(mediaItem)); } - private mapReview(item: any): UnifiedReviewDTO & {raw: any} { + private mapReview(item: any): UnifiedReviewDTO & { raw: any } { // 将 WooCommerce 评论数据映射为统一评论DTO return { id: item.id, @@ -795,7 +941,7 @@ export class WooCommerceAdapter implements ISiteAdapter { id: item.id, avatar: item.avatar_url, email: item.email, - orders: Number(item.orders?? 0), + orders: Number(item.orders ?? 0), total_spend: Number(item.total_spent ?? 0), first_name: item.first_name, last_name: item.last_name, @@ -828,7 +974,11 @@ export class WooCommerceAdapter implements ISiteAdapter { async getAllCustomers(params?: UnifiedSearchParamsDTO): Promise { // 使用sdkGetAll获取所有客户数据,不受分页限制 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)); } @@ -855,5 +1005,270 @@ export class WooCommerceAdapter implements ISiteAdapter { await api.delete(`customers/${id}`, { force: true }); return true; } + + async getOrderFulfillments(orderId: string | number): Promise { + 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 { + 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 { + return await this.wpService.updateFulfillment(this.site, String(orderId), fulfillmentId, data); + } + + async deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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)}`); + } + } } diff --git a/src/config/config.default.ts b/src/config/config.default.ts index 9d86ff2..fe9e8bb 100644 --- a/src/config/config.default.ts +++ b/src/config/config.default.ts @@ -35,7 +35,6 @@ import { DictItem } from '../entity/dict_item.entity'; import { Template } from '../entity/template.entity'; import { Area } from '../entity/area.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 { Category } from '../entity/category.entity'; import DictSeeder from '../db/seeds/dict.seeder'; @@ -50,7 +49,6 @@ export default { entities: [ Product, ProductStockComponent, - ProductSiteSku, User, PurchaseOrder, PurchaseOrderItem, @@ -133,7 +131,7 @@ export default { // mode: 'file', // 默认为file,即上传到服务器临时目录,可以配置为 stream mode: 'file', fileSize: '10mb', // 最大支持的文件大小,默认为 10mb - whitelist: ['.csv'], // 支持的文件后缀 + whitelist: ['.csv', '.xlsx'], // 支持的文件后缀 tmpdir: join(__dirname, '../../tmp_uploads'), cleanTimeout: 5 * 60 * 1000, }, diff --git a/src/configuration.ts b/src/configuration.ts index b64dbd0..16a5716 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -3,11 +3,14 @@ import { App, Inject, MidwayDecoratorService, + Logger, + Config, } from '@midwayjs/core'; import * as koa from '@midwayjs/koa'; import * as validate from '@midwayjs/validate'; import * as info from '@midwayjs/info'; import * as orm from '@midwayjs/typeorm'; +import { DataSource } from 'typeorm'; import { join } from 'path'; import { DefaultErrorFilter } from './filter/default.filter'; import { NotFoundFilter } from './filter/notfound.filter'; @@ -52,7 +55,19 @@ export class MainConfiguration { @Inject() siteService: SiteService; + @Logger() + logger; // 注入 Logger 实例 + + @Config('typeorm.dataSource.default') + typeormDataSourceConfig; // 注入 TypeORM 数据源配置 + + async onConfigLoad() { + // 在组件初始化之前,先检查并创建数据库 + await this.initializeDatabase(); + } + async onReady() { + // add middleware this.app.useMiddleware([QueryNormalizeMiddleware, ReportMiddleware, AuthMiddleware]); // add filter @@ -82,4 +97,70 @@ export class MainConfiguration { } ); } + + /** + * 初始化数据库(如果不存在则创建) + */ + private async initializeDatabase(): Promise { + // 使用注入的数据库配置 + 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(); + } + } + } } diff --git a/src/controller/category.controller.ts b/src/controller/category.controller.ts index e8b625f..e10533a 100644 --- a/src/controller/category.controller.ts +++ b/src/controller/category.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Post, Put, Del, Body, Query, Inject, Param } from '@mi import { CategoryService } from '../service/category.service'; import { successResponse, errorResponse } from '../utils/response.util'; import { ApiOkResponse } from '@midwayjs/swagger'; +import { CreateCategoryDTO, UpdateCategoryDTO } from '../dto/product.dto'; @Controller('/category') export class CategoryController { @@ -32,7 +33,7 @@ export class CategoryController { @ApiOkResponse() @Post('/') - async create(@Body() body: any) { + async create(@Body() body: CreateCategoryDTO) { try { const data = await this.categoryService.create(body); return successResponse(data); @@ -43,7 +44,7 @@ export class CategoryController { @ApiOkResponse() @Put('/:id') - async update(@Param('id') id: number, @Body() body: any) { + async update(@Param('id') id: number, @Body() body: UpdateCategoryDTO) { try { const data = await this.categoryService.update(id, body); return successResponse(data); diff --git a/src/controller/customer.controller.ts b/src/controller/customer.controller.ts index 96c96ff..94a19d2 100644 --- a/src/controller/customer.controller.ts +++ b/src/controller/customer.controller.ts @@ -1,18 +1,27 @@ -import { Controller, Get, Post, Inject, Query, Body } from '@midwayjs/core'; -import { successResponse, errorResponse } from '../utils/response.util'; +import { Controller, Get, Post, Inject, Query, Body, Put, Del, Param } from '@midwayjs/core'; +import { successResponse, errorResponse, ApiResponse } from '../utils/response.util'; 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 { 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') export class CustomerController { @Inject() customerService: CustomerService; - @ApiOkResponse({ type: Object }) - @Get('/getcustomerlist') - async getCustomerList(@Query() query: QueryCustomerListDTO) { + @ApiOkResponse({ type: ApiResponse> }) + @Get('/list') + async getCustomerList(@Query() query: CustomerQueryParamsDTO) { try { const result = await this.customerService.getCustomerList(query) return successResponse(result); @@ -22,8 +31,8 @@ export class CustomerController { } @ApiOkResponse({ type: Object }) - @Get('/getcustomerstatisticlist') - async getCustomerStatisticList(@Query() query: QueryCustomerListDTO) { + @Get('/statistic/list') + async getCustomerStatisticList(@Query() query: CustomerQueryParamsDTO) { try { const result = await this.customerService.getCustomerStatisticList(query as any); return successResponse(result); @@ -79,11 +88,11 @@ export class CustomerController { /** * 同步客户数据 * 从指定站点获取客户数据并保存到本地数据库 - * 业务逻辑已移到service层,controller只负责参数传递和响应 + * 支持通过where和orderBy参数筛选和排序要同步的客户数据 */ @ApiOkResponse({ type: Object }) @Post('/sync') - async syncCustomers(@Body() body: { siteId: number; params?: UnifiedSearchParamsDTO }) { + async syncCustomers(@Body() body: SyncCustomersDTO) { try { const { siteId, params = {} } = body; @@ -95,4 +104,147 @@ export class CustomerController { 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); + } + } } \ No newline at end of file diff --git a/src/controller/dict.controller.ts b/src/controller/dict.controller.ts index 8413ca2..c3257fd 100644 --- a/src/controller/dict.controller.ts +++ b/src/controller/dict.controller.ts @@ -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 { CreateDictDTO, UpdateDictDTO, CreateDictItemDTO, UpdateDictItemDTO } from '../dto/dict.dto'; import { Validate } from '@midwayjs/validate'; 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 body 请求体,包含字典ID + * @param fields FormData中的字段 */ + @ApiOkResponse({type:ApiResponse}) @Post('/item/import') @Validate() - async importDictItems(@Files() files: any, @Body() body: { dictId: number }) { + async importDictItems(@Files() files: any, @Fields() fields: { dictId: number }) { // 获取第一个文件 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(); } + /** + * 导出字典项为 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 (可选) diff --git a/src/controller/order.controller.ts b/src/controller/order.controller.ts index 8aa7b5f..842fcea 100644 --- a/src/controller/order.controller.ts +++ b/src/controller/order.controller.ts @@ -35,8 +35,8 @@ export class OrderController { @ApiOkResponse({ type: BooleanRes, }) - @Post('/syncOrder/:siteId') - async syncOrder(@Param('siteId') siteId: number, @Body() params: Record) { + @Post('/sync/:siteId') + async syncOrders(@Param('siteId') siteId: number, @Body() params: Record) { try { const result = await this.orderService.syncOrders(siteId, params); return successResponse(result); @@ -55,8 +55,8 @@ export class OrderController { @Param('orderId') orderId: string ) { try { - await this.orderService.syncOrderById(siteId, orderId); - return successResponse(true); + const result = await this.orderService.syncOrderById(siteId, orderId); + return successResponse(result); } catch (error) { console.log(error); return errorResponse('同步失败'); diff --git a/src/controller/product.controller.ts b/src/controller/product.controller.ts index 9d294d0..aef81f8 100644 --- a/src/controller/product.controller.ts +++ b/src/controller/product.controller.ts @@ -1,21 +1,26 @@ import { + Body, + ContentType, + Controller, + Del, + Files, + Get, Inject, + Param, Post, Put, - Get, - Body, - Param, - Del, Query, - Controller, } 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 { 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') export class ProductController { @@ -25,6 +30,9 @@ export class ProductController { @Inject() ctx: Context; + @Inject() + logger: ILogger; + @ApiOkResponse({ description: '通过name搜索产品', type: ProductsRes, @@ -60,20 +68,13 @@ export class ProductController { }) @Get('/list') async getProductList( - @Query() query: QueryProductDTO + @Query() query: UnifiedSearchParamsDTO ): Promise { - const { current = 1, pageSize = 10, name, brandId, sortField, sortOrder } = query; try { - const data = await this.productService.getProductList( - { current, pageSize }, - name, - brandId, - sortField, - sortOrder - ); + const data = await this.productService.getProductList(query); return successResponse(data); } catch (error) { - console.log(error); + this.logger.error('获取产品列表失败', error); return errorResponse(error?.message || error); } } @@ -613,7 +614,7 @@ export class ProductController { // 创建分类 @ApiOkResponse({ description: '创建分类' }) @Post('/category') - async createCategory(@Body() body: any) { + async createCategory(@Body() body: CreateCategoryDTO) { try { const data = await this.productService.createCategory(body); return successResponse(data); @@ -625,7 +626,7 @@ export class ProductController { // 更新分类 @ApiOkResponse({ description: '更新分类' }) @Put('/category/:id') - async updateCategory(@Param('id') id: number, @Body() body: any) { + async updateCategory(@Param('id') id: number, @Body() body: UpdateCategoryDTO) { try { const data = await this.productService.updateCategory(id, body); return successResponse(data); @@ -681,4 +682,71 @@ export class ProductController { 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); + } + } } diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts index 0cfa695..a98c958 100644 --- a/src/controller/site-api.controller.ts +++ b/src/controller/site-api.controller.ts @@ -1,32 +1,31 @@ -import { Controller, Get, Inject, Param, Query, Body, Post, Put, Del } from '@midwayjs/core'; -import { ApiOkResponse, ApiBody } from '@midwayjs/swagger'; +import { Body, Controller, Del, Get, ILogger, Inject, Param, Post, Put, Query } from '@midwayjs/core'; +import { ApiBody, ApiOkResponse } from '@midwayjs/swagger'; +import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; +import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; import { - UploadMediaDTO, + BatchFulfillmentsDTO, + CancelFulfillmentDTO, + CreateReviewDTO, + CreateWebhookDTO, + FulfillmentDTO, + UnifiedCustomerDTO, + UnifiedCustomerPaginationDTO, UnifiedMediaPaginationDTO, UnifiedOrderDTO, UnifiedOrderPaginationDTO, + UnifiedOrderTrackingDTO, UnifiedProductDTO, UnifiedProductPaginationDTO, - UnifiedSearchParamsDTO, - UnifiedSubscriptionPaginationDTO, - UnifiedCustomerDTO, - UnifiedCustomerPaginationDTO, - UnifiedReviewPaginationDTO, UnifiedReviewDTO, - CreateReviewDTO, - UpdateReviewDTO, + UnifiedReviewPaginationDTO, + UnifiedSubscriptionPaginationDTO, UnifiedWebhookDTO, - CreateWebhookDTO, + UpdateReviewDTO, UpdateWebhookDTO, - UnifiedPaginationDTO, - ShipOrderDTO, - CancelShipOrderDTO, - BatchShipOrdersDTO, + UploadMediaDTO, } from '../dto/site-api.dto'; -import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; import { SiteApiService } from '../service/site-api.service'; import { errorResponse, successResponse } from '../utils/response.util'; -import { ILogger } from '@midwayjs/core'; @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') @ApiOkResponse({ type: Object }) 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') @ApiOkResponse({ type: UnifiedOrderPaginationDTO }) async getOrders( @@ -925,59 +967,58 @@ export class SiteApiController { } } - @Post('/:siteId/orders/:id/ship') + @Post('/:siteId/orders/:id/fulfill') @ApiOkResponse({ type: Object }) - async shipOrder( + async fulfillOrder( @Param('siteId') siteId: number, @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 { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.shipOrder(id, body); - this.logger.info(`[Site API] 订单发货成功, siteId: ${siteId}, orderId: ${id}`); + const data = await adapter.fulfillOrder(id, body); + this.logger.info(`[Site API] 订单履约成功, siteId: ${siteId}, orderId: ${id}`); return successResponse(data); } 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); } } - @Post('/:siteId/orders/:id/cancel-ship') + @Post('/:siteId/orders/:id/cancel-fulfill') @ApiOkResponse({ type: Object }) - async cancelShipOrder( + async cancelFulfillment( @Param('siteId') siteId: number, @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 { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.cancelShipOrder(id, body); - this.logger.info(`[Site API] 取消订单发货成功, siteId: ${siteId}, orderId: ${id}`); + const data = await adapter.cancelFulfillment(id, body); + this.logger.info(`[Site API] 取消订单履约成功, siteId: ${siteId}, orderId: ${id}`); return successResponse(data); } 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); } } - @Post('/:siteId/orders/batch-ship') + @Post('/:siteId/orders/batch-fulfill') @ApiOkResponse({ type: Object }) - async batchShipOrders( + async batchFulfillOrders( @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 { const adapter = await this.siteApiService.getAdapter(siteId); const results = await Promise.allSettled( body.orders.map(order => - adapter.shipOrder(order.order_id, { + adapter.createOrderFulfillment(order.order_id, { tracking_number: order.tracking_number, - shipping_provider: order.shipping_provider, - shipping_method: order.shipping_method, + tracking_provider: order.shipping_provider, items: order.items, }).catch(error => ({ order_id: order.order_id, @@ -995,7 +1036,7 @@ export class SiteApiController { .filter(result => result.status === 'rejected') .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({ successful: successful.length, failed: failed.length, @@ -1005,7 +1046,95 @@ export class SiteApiController { } }); } 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); } } diff --git a/src/controller/site.controller.ts b/src/controller/site.controller.ts index 3427a33..b30d4ab 100644 --- a/src/controller/site.controller.ts +++ b/src/controller/site.controller.ts @@ -15,7 +15,8 @@ export class SiteController { async all() { try { 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) { return errorResponse(error?.message || '获取失败'); } diff --git a/src/db/migrations/1764238434984-product-dict-item-many-to-many.ts b/src/db/migrations/1764238434984-product-dict-item-many-to-many.ts deleted file mode 100644 index 1011b18..0000000 --- a/src/db/migrations/1764238434984-product-dict-item-many-to-many.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class ProductDictItemManyToMany1764238434984 implements MigrationInterface { - name = 'ProductDictItemManyToMany1764238434984' - - public async up(queryRunner: QueryRunner): Promise { - 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 { - 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\``); - } - -} diff --git a/src/db/migrations/1764294088896-Area.ts b/src/db/migrations/1764294088896-Area.ts deleted file mode 100644 index e2948bb..0000000 --- a/src/db/migrations/1764294088896-Area.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class Area1764294088896 implements MigrationInterface { - name = 'Area1764294088896' - - public async up(queryRunner: QueryRunner): Promise { - // 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 { - 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\`)`); - } - -} diff --git a/src/db/migrations/1764299629279-ProductStock.ts b/src/db/migrations/1764299629279-ProductStock.ts deleted file mode 100644 index 9767e45..0000000 --- a/src/db/migrations/1764299629279-ProductStock.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class ProductStock1764299629279 implements MigrationInterface { - name = 'ProductStock1764299629279' - - public async up(queryRunner: QueryRunner): Promise { - 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 { - await queryRunner.query(`ALTER TABLE \`order_item_original\` DROP FOREIGN KEY \`FK_ca48e4bce0bb8cecd24cc8081e5\``); - await queryRunner.query(`DROP TABLE \`order_item_original\``); - } - -} diff --git a/src/db/migrations/1764569947170-update-dict-item-unique-constraint.ts b/src/db/migrations/1764569947170-update-dict-item-unique-constraint.ts deleted file mode 100644 index e71f676..0000000 --- a/src/db/migrations/1764569947170-update-dict-item-unique-constraint.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class UpdateDictItemUniqueConstraint1764569947170 implements MigrationInterface { - name = 'UpdateDictItemUniqueConstraint1764569947170' - - public async up(queryRunner: QueryRunner): Promise { - 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 { - 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`); - } - -} diff --git a/src/db/migrations/1765275715762-add_test_data_to_template.ts b/src/db/migrations/1765275715762-add_test_data_to_template.ts deleted file mode 100644 index 24956b6..0000000 --- a/src/db/migrations/1765275715762-add_test_data_to_template.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AddTestDataToTemplate1765275715762 implements MigrationInterface { - name = 'AddTestDataToTemplate1765275715762' - - public async up(queryRunner: QueryRunner): Promise { - 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 { - 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`); - } - -} diff --git a/src/db/migrations/1765330208213-add-site-description.ts b/src/db/migrations/1765330208213-add-site-description.ts deleted file mode 100644 index 148ce00..0000000 --- a/src/db/migrations/1765330208213-add-site-description.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class AddSiteDescription1765330208213 implements MigrationInterface { - name = 'AddSiteDescription1765330208213' - - public async up(queryRunner: QueryRunner): Promise { - 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 { - 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`); - } - -} diff --git a/src/db/migrations/1765358400000-update-product-table-name.ts b/src/db/migrations/1765358400000-update-product-table-name.ts deleted file mode 100644 index 51deefd..0000000 --- a/src/db/migrations/1765358400000-update-product-table-name.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { MigrationInterface, QueryRunner } from "typeorm"; - -export class UpdateProductTableName1765358400000 implements MigrationInterface { - name = 'UpdateProductTableName1765358400000' - - public async up(queryRunner: QueryRunner): Promise { - // 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 { - // 回滚操作 - // 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.'); - } - } -} \ No newline at end of file diff --git a/src/db/seeds/dict.seeder.ts b/src/db/seeds/dict.seeder.ts index 0201bd7..d015a05 100644 --- a/src/db/seeds/dict.seeder.ts +++ b/src/db/seeds/dict.seeder.ts @@ -10,7 +10,6 @@ export default class DictSeeder implements Seeder { * @returns 格式化后的名称 */ private formatName(name: string): string { - // 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 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 = [ { name: 'zh-cn', title: '简体中文', titleCn: '简体中文', shortName: 'CN' }, @@ -173,3 +104,775 @@ export default class DictSeeder implements Seeder { } } } + +// 口味数据 +const flavorsData = [ + { name: 'all-white', title: 'all white', titleCn: '全白', shortName: 'AL' }, + { name: 'amazing-apple-blackcurrant', title: 'amazing apple blackcurrant', titleCn: '惊艳苹果黑加仑', shortName: 'AM' }, + { name: 'apple-&-mint', title: 'apple & mint', titleCn: '苹果薄荷', shortName: 'AP' }, + { name: 'applemint', title: 'applemint', titleCn: '苹果薄荷混合', shortName: 'AP' }, + { name: 'apple-berry-ice', title: 'apple berry ice', titleCn: '苹果莓冰', shortName: 'AP' }, + { name: 'apple-bomb', title: 'apple bomb', titleCn: '苹果炸弹', shortName: 'AP' }, + { name: 'apple-kiwi-melon-ice', title: 'apple kiwi melon ice', titleCn: '苹果奇异瓜冰', shortName: 'AP' }, + { name: 'apple-mango-pear', title: 'apple mango pear', titleCn: '苹果芒果梨', shortName: 'AP' }, + { name: 'apple-melon-ice', title: 'apple melon ice', titleCn: '苹果瓜冰', shortName: 'AP' }, + { name: 'apple-mint', title: 'apple mint', titleCn: '苹果薄荷', shortName: 'AP' }, + { name: 'apple-peach', title: 'apple peach', titleCn: '苹果桃子', shortName: 'AP' }, + { name: 'apple-peach-pear', title: 'apple peach pear', titleCn: '苹果桃梨', shortName: 'AP' }, + { name: 'apple-peach-strawww', title: 'apple peach strawww', titleCn: '苹果桃草莓', shortName: 'AP' }, + { name: 'apple-pom-passion-ice', title: 'apple pom passion ice', titleCn: '苹果石榴激情冰', shortName: 'AP' }, + { name: 'arctic-banana-glaze', title: 'arctic banana glaze', titleCn: '北极香蕉釉', shortName: 'AR' }, + { name: 'arctic-grapefruit', title: 'arctic grapefruit', titleCn: '北极葡萄柚', shortName: 'AR' }, + { name: 'arctic-mint', title: 'arctic mint', titleCn: '北极薄荷', shortName: 'AR' }, + { name: 'baddie-blueberries', title: 'baddie blueberries', titleCn: '时髦蓝莓', shortName: 'BA' }, + { name: 'banana', title: 'banana', titleCn: '香蕉', shortName: 'BA' }, + { name: 'banana-(solid)', title: 'banana (solid)', titleCn: '香蕉(固体)', shortName: 'BA' }, + { name: 'banana-berry', title: 'banana berry', titleCn: '香蕉莓果', shortName: 'BA' }, + { name: 'banana-berry-melon-ice', title: 'banana berry melon ice', titleCn: '香蕉莓果瓜冰', shortName: 'BA' }, + { name: 'banana-blackberry', title: 'banana blackberry', titleCn: '香蕉黑莓', shortName: 'BA' }, + { name: 'banana-ice', title: 'banana ice', titleCn: '香蕉冰', shortName: 'BA' }, + { name: 'banana-milkshake', title: 'banana milkshake', titleCn: '香蕉奶昔', shortName: 'BA' }, + { name: 'banana-pnck-dude', title: 'banana pnck dude', titleCn: '香蕉粉红小子', shortName: 'BA' }, + { name: 'banana-pomegranate-cherry-ice', title: 'banana pomegranate cherry ice', titleCn: '香蕉石榴樱桃冰', shortName: 'BA' }, + { name: 'bangin-blood-orange-iced', title: 'bangin blood orange iced', titleCn: '爆炸血橙冰', shortName: 'BA' }, + { name: 'berries-in-the-6ix', title: 'berries in the 6ix', titleCn: '多伦多莓果', shortName: 'BE' }, + { name: 'berry-burst', title: 'berry burst', titleCn: '浆果爆发', shortName: 'BE' }, + { name: 'berry-burst-(thermal)', title: 'berry burst (thermal)', titleCn: '浆果爆发(热感)', shortName: 'BE' }, + { name: 'berry-ice', title: 'berry ice', titleCn: '浆果冰', shortName: 'BE' }, + { name: 'berry-lime-ice', title: 'berry lime ice', titleCn: '浆果青柠冰', shortName: 'BE' }, + { name: 'berry-trio-ice', title: 'berry trio ice', titleCn: '三重浆果冰', shortName: 'BE' }, + { name: 'black', title: 'black', titleCn: '黑色', shortName: 'BL' }, + { name: 'black-cherry', title: 'black cherry', titleCn: '黑樱桃', shortName: 'BL' }, + { name: 'blackcherry', title: 'blackcherry', titleCn: '黑樱桃混合', shortName: 'BL' }, + { name: 'blackcurrant-ice', title: 'blackcurrant ice', titleCn: '黑加仑冰', shortName: 'BL' }, + { name: 'black-currant-ice', title: 'black currant ice', titleCn: '黑加仑冰(空格版)', shortName: 'BL' }, + { name: 'black-licorice', title: 'black licorice', titleCn: '黑甘草', shortName: 'BL' }, + { name: 'black-tea', title: 'black tea', titleCn: '红茶', shortName: 'BL' }, + { name: 'blackberry-ice', title: 'blackberry ice', titleCn: '黑莓冰', shortName: 'BL' }, + { name: 'blackberry-raspberry-lemon', title: 'blackberry raspberry lemon', titleCn: '黑莓覆盆子柠檬', shortName: 'BL' }, + { name: 'blackcurrant-lychee-berries', title: 'blackcurrant lychee berries', titleCn: '黑加仑荔枝莓', shortName: 'BL' }, + { name: 'blackcurrant-pineapple-ice', title: 'blackcurrant pineapple ice', titleCn: '黑加仑菠萝冰', shortName: 'BL' }, + { name: 'blackcurrant-quench-ice', title: 'blackcurrant quench ice', titleCn: '黑加仑清爽冰', shortName: 'BL' }, + { name: 'blastin-banana-mango-iced', title: 'blastin banana mango iced', titleCn: '香蕉芒果爆炸冰', shortName: 'BL' }, + { name: 'blazin-banana-blackberry-iced', title: 'blazin banana blackberry iced', titleCn: '香蕉黑莓火焰冰', shortName: 'BL' }, + { name: 'blessed-blueberry-mint-iced', title: 'blessed blueberry mint iced', titleCn: '蓝莓薄荷冰', shortName: 'BL' }, + { name: 'bliss-iced', title: 'bliss iced', titleCn: '极乐冰', shortName: 'BL' }, + { name: 'blood-orange', title: 'blood orange', titleCn: '血橙', shortName: 'BL' }, + { name: 'blood-orange-ice', title: 'blood orange ice', titleCn: '血橙冰', shortName: 'BL' }, + { name: 'blue-dragon-fruit-peach', title: 'blue dragon fruit peach', titleCn: '蓝色龙果桃', shortName: 'BL' }, + { name: 'blue-lemon', title: 'blue lemon', titleCn: '蓝柠檬', shortName: 'BL' }, + { name: 'blue-raspberry', title: 'blue raspberry', titleCn: '蓝覆盆子', shortName: 'BL' }, + { name: 'blue-raspberry-apple', title: 'blue raspberry apple', titleCn: '蓝覆盆子苹果', shortName: 'BL' }, + { name: 'blue-raspberry-lemon', title: 'blue raspberry lemon', titleCn: '蓝覆盆子柠檬', shortName: 'BL' }, + { name: 'blue-raspberry-magic-cotton-ice', title: 'blue raspberry magic cotton ice', titleCn: '蓝覆盆子魔法棉花糖冰', shortName: 'BL' }, + { name: 'blue-razz', title: 'blue razz', titleCn: '蓝覆盆子', shortName: 'BL' }, + { name: 'blue-razz-hype', title: 'blue razz hype', titleCn: '蓝覆盆子热情', shortName: 'BL' }, + { name: 'blue-razz-ice', title: 'blue razz ice', titleCn: '蓝覆盆子冰', shortName: 'BL' }, + { name: 'blue-razz-ice-(solid)', title: 'blue razz ice (solid)', titleCn: '蓝覆盆子冰(固体)', shortName: 'BL' }, + { name: 'blue-razz-ice-glace', title: 'blue razz ice glace', titleCn: '蓝覆盆子冰格', shortName: 'BL' }, + { name: 'blue-razz-lemon-ice', title: 'blue razz lemon ice', titleCn: '蓝覆盆子柠檬冰', shortName: 'BL' }, + { name: 'blue-razz-lemonade', title: 'blue razz lemonade', titleCn: '蓝覆盆子柠檬水', shortName: 'BL' }, + { name: 'blueberry', title: 'blueberry', titleCn: '蓝莓', shortName: 'BL' }, + { name: 'blueberry-banana', title: 'blueberry banana', titleCn: '蓝莓香蕉', shortName: 'BL' }, + { name: 'blueberry-cloudz', title: 'blueberry cloudz', titleCn: '蓝莓云', shortName: 'BL' }, + { name: 'blueberry-ice', title: 'blueberry ice', titleCn: '蓝莓冰', shortName: 'BL' }, + { name: 'blueberry-kiwi-ice', title: 'blueberry kiwi ice', titleCn: '蓝莓奇异果冰', shortName: 'BL' }, + { name: 'blueberry-lemon', title: 'blueberry lemon', titleCn: '蓝莓柠檬', shortName: 'BL' }, + { name: 'blueberry-lemon-ice', title: 'blueberry lemon ice', titleCn: '蓝莓柠檬冰', shortName: 'BL' }, + { name: 'blueberry-mint', title: 'blueberry mint', titleCn: '蓝莓薄荷', shortName: 'BL' }, + { name: 'blueberry-pear', title: 'blueberry pear', titleCn: '蓝莓梨', shortName: 'BL' }, + { name: 'blueberry-razz-cc', title: 'blueberry razz cc', titleCn: '蓝莓覆盆子混合', shortName: 'BL' }, + { name: 'blueberry-sour-raspberry', title: 'blueberry sour raspberry', titleCn: '蓝莓酸覆盆子', shortName: 'BL' }, + { name: 'blueberry-storm', title: 'blueberry storm', titleCn: '蓝莓风暴', shortName: 'BL' }, + { name: 'blueberry-swirl-ice', title: 'blueberry swirl ice', titleCn: '蓝莓漩涡冰', shortName: 'BL' }, + { name: 'blueberry-watermelon', title: 'blueberry watermelon', titleCn: '蓝莓西瓜', shortName: 'BL' }, + { name: 'bold-tobacco', title: 'bold tobacco', titleCn: '浓烈烟草', shortName: 'BO' }, + { name: 'bomb-blue-razz', title: 'bomb blue razz', titleCn: '蓝覆盆子炸弹', shortName: 'BO' }, + { name: 'boss-blueberry-iced', title: 'boss blueberry iced', titleCn: '老板蓝莓冰', shortName: 'BO' }, + { name: 'boss-blueberry-lced', title: 'boss blueberry lced', titleCn: '老板蓝莓冷饮', shortName: 'BO' }, + { name: 'bright-peppermint', title: 'bright peppermint', titleCn: '清爽薄荷', shortName: 'BR' }, + { name: 'bright-spearmint', title: 'bright spearmint', titleCn: '清爽留兰香', shortName: 'BR' }, + { name: 'brisky-classic-red', title: 'brisky classic red', titleCn: '经典红色烈酒', shortName: 'BR' }, + { name: 'bumpin-blackcurrant-iced', title: 'bumpin blackcurrant iced', titleCn: '黑加仑热烈冰', shortName: 'BU' }, + { name: 'burst-ice', title: 'burst ice', titleCn: '爆炸冰', shortName: 'BU' }, + { name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰', shortName: 'BU' }, + { name: 'bussin-banana-iced', title: 'bussin banana iced', titleCn: '香蕉热烈冰(重复)', shortName: 'BU' }, + { name: 'california-cherry', title: 'california cherry', titleCn: '加州樱桃', shortName: 'CA' }, + { name: 'cantaloupe-mango-banana', title: 'cantaloupe mango banana', titleCn: '香瓜芒果香蕉', shortName: 'CA' }, + { name: 'caramel', title: 'caramel', titleCn: '焦糖', shortName: 'CA' }, + { name: 'caribbean-spirit', title: 'caribbean spirit', titleCn: '加勒比风情', shortName: 'CA' }, + { name: 'caribbean-white', title: 'caribbean white', titleCn: '加勒比白', shortName: 'CA' }, + { name: 'cherry', title: 'cherry', titleCn: '樱桃', shortName: 'CH' }, + { name: 'cherry-blast-ice', title: 'cherry blast ice', titleCn: '樱桃爆炸冰', shortName: 'CH' }, + { name: 'cherry-classic-cola', title: 'cherry classic cola', titleCn: '樱桃经典可乐', shortName: 'CH' }, + { name: 'cherry-classic-red', title: 'cherry classic red', titleCn: '樱桃经典红', shortName: 'CH' }, + { name: 'cherry-cola-ice', title: 'cherry cola ice', titleCn: '樱桃可乐冰', shortName: 'CH' }, + { name: 'cherry-ice', title: 'cherry ice', titleCn: '樱桃冰', shortName: 'CH' }, + { name: 'cherry-lemon', title: 'cherry lemon', titleCn: '樱桃柠檬', shortName: 'CH' }, + { name: 'cherry-lime-classic', title: 'cherry lime classic', titleCn: '樱桃青柠经典', shortName: 'CH' }, + { name: 'cherry-lime-ice', title: 'cherry lime ice', titleCn: '樱桃青柠冰', shortName: 'CH' }, + { name: 'cherry-lychee', title: 'cherry lychee', titleCn: '樱桃荔枝', shortName: 'CH' }, + { name: 'cherry-peach-lemon', title: 'cherry peach lemon', titleCn: '樱桃桃子柠檬', shortName: 'CH' }, + { name: 'cherry-red-classic', title: 'cherry red classic', titleCn: '红樱桃经典', shortName: 'CH' }, + { name: 'cherry-strazz', title: 'cherry strazz', titleCn: '樱桃草莓', shortName: 'CH' }, + { name: 'cherry-watermelon', title: 'cherry watermelon', titleCn: '樱桃西瓜', shortName: 'CH' }, + { name: 'chill', title: 'chill', titleCn: '冰爽', shortName: 'CH' }, + { name: 'chilled-classic-red', title: 'chilled classic red', titleCn: '冰镇经典红', shortName: 'CH' }, + { name: 'chillin-coffee-iced', title: 'chillin coffee iced', titleCn: '冰镇咖啡', shortName: 'CH' }, + { name: 'chilly-jiggle-b', title: 'chilly jiggle b', titleCn: '清凉果冻 B', shortName: 'CH' }, + { name: 'churned-peanut', title: 'churned peanut', titleCn: '搅拌花生', shortName: 'CH' }, + { name: 'cinnamon', title: 'cinnamon', titleCn: '肉桂', shortName: 'CI' }, + { name: 'cinnamon-flame', title: 'cinnamon flame', titleCn: '肉桂火焰', shortName: 'CI' }, + { name: 'cinnamon-roll', title: 'cinnamon roll', titleCn: '肉桂卷', shortName: 'CI' }, + { name: 'circle-of-life', title: 'circle of life', titleCn: '生命循环', shortName: 'CI' }, + { name: 'citrus', title: 'citrus', titleCn: '柑橘', shortName: 'CI' }, + { name: 'citrus-burst-ice', title: 'citrus burst ice', titleCn: '柑橘爆发冰', shortName: 'CI' }, + { name: 'citrus-chill', title: 'citrus chill', titleCn: '柑橘清凉', shortName: 'CI' }, + { name: 'citrus-smash-ice', title: 'citrus smash ice', titleCn: '柑橘冲击冰', shortName: 'CI' }, + { name: 'citrus-sunrise', title: 'citrus sunrise', titleCn: '柑橘日出', shortName: 'CI' }, + { name: 'citrus-sunrise-(thermal)', title: 'citrus sunrise (thermal)', titleCn: '柑橘日出(热感)', shortName: 'CI' }, + { name: 'classic', title: 'classic', titleCn: '经典', shortName: 'CL' }, + { name: 'classic-ice', title: 'classic ice', titleCn: '经典冰', shortName: 'CL' }, + { name: 'classic-mint-ice', title: 'classic mint ice', titleCn: '经典薄荷冰', shortName: 'CL' }, + { name: 'classic-tobacco', title: 'classic tobacco', titleCn: '经典烟草', shortName: 'CL' }, + { name: 'classical-tobacco', title: 'classical tobacco', titleCn: '古典烟草', shortName: 'CL' }, + { name: 'coconut-ice', title: 'coconut ice', titleCn: '椰子冰', shortName: 'CO' }, + { name: 'coconut-water-ice', title: 'coconut water ice', titleCn: '椰子水冰', shortName: 'CO' }, + { name: 'coffee', title: 'coffee', titleCn: '咖啡', shortName: 'CO' }, + { name: 'coffee-stout', title: 'coffee stout', titleCn: '咖啡烈酒', shortName: 'CO' }, + { name: 'cola', title: 'cola', titleCn: '可乐', shortName: 'CO' }, + { name: 'cola-&-cherry', title: 'cola & cherry', titleCn: '可乐樱桃', shortName: 'CO' }, + { name: 'cola-&-vanilla', title: 'cola & vanilla', titleCn: '可乐香草', shortName: 'CO' }, + { name: 'cola-ice', title: 'cola ice', titleCn: '可乐冰', shortName: 'CO' }, + { name: 'cool-frost', title: 'cool frost', titleCn: '酷霜', shortName: 'CO' }, + { name: 'cool-mint', title: 'cool mint', titleCn: '酷薄荷', shortName: 'CO' }, + { name: 'cool-mint-ice', title: 'cool mint ice', titleCn: '酷薄荷冰', shortName: 'CO' }, + { name: 'cool-storm', title: 'cool storm', titleCn: '酷风暴', shortName: 'CO' }, + { name: 'cool-tropical', title: 'cool tropical', titleCn: '酷热带', shortName: 'CO' }, + { name: 'cool-watermelon', title: 'cool watermelon', titleCn: '酷西瓜', shortName: 'CO' }, + { name: 'cotton-clouds', title: 'cotton clouds', titleCn: '棉花云', shortName: 'CO' }, + { name: 'cranberry-blackcurrant', title: 'cranberry blackcurrant', titleCn: '蔓越莓黑加仑', shortName: 'CR' }, + { name: 'cranberry-lemon', title: 'cranberry lemon', titleCn: '蔓越莓柠檬', shortName: 'CR' }, + { name: 'cranberry-lemon-ice', title: 'cranberry lemon ice', titleCn: '蔓越莓柠檬冰', shortName: 'CR' }, + { name: 'creamy-maple', title: 'creamy maple', titleCn: '奶香枫糖', shortName: 'CR' }, + { name: 'creamy-vanilla', title: 'creamy vanilla', titleCn: '奶香香草', shortName: 'CR' }, + { name: 'crispy-peppermint', title: 'crispy peppermint', titleCn: '脆薄荷', shortName: 'CR' }, + { name: 'cuban-tobacco', title: 'cuban tobacco', titleCn: '古巴烟草', shortName: 'CU' }, + { name: 'cucumber-lime', title: 'cucumber lime', titleCn: '黄瓜青柠', shortName: 'CU' }, + { name: 'dark-blackcurrant', title: 'dark blackcurrant', titleCn: '深黑加仑', shortName: 'DA' }, + { name: 'dark-forest', title: 'dark forest', titleCn: '深林', shortName: 'DA' }, + { name: 'deep-freeze', title: 'deep freeze', titleCn: '极冻', shortName: 'DE' }, + { name: 'dope-double-kiwi-iced', title: 'dope double kiwi iced', titleCn: '双奇异果冰', shortName: 'DO' }, + { name: 'dope-double-kiwi-lced', title: 'dope double kiwi lced', titleCn: '双奇异果冷饮', shortName: 'DO' }, + { name: 'double-apple', title: 'double apple', titleCn: '双苹果', shortName: 'DO' }, + { name: 'double-apple-ice', title: 'double apple ice', titleCn: '双苹果冰', shortName: 'DO' }, + { name: 'double-berry-twist-ice', title: 'double berry twist ice', titleCn: '双浆果扭曲冰', shortName: 'DO' }, + { name: 'double-ice', title: 'double ice', titleCn: '双冰', shortName: 'DO' }, + { name: 'double-mango', title: 'double mango', titleCn: '双芒果', shortName: 'DO' }, + { name: 'double-mint', title: 'double mint', titleCn: '双薄荷', shortName: 'DO' }, + { name: 'double-mocha', title: 'double mocha', titleCn: '双摩卡', shortName: 'DO' }, + { name: 'double-shot-espresso', title: 'double shot espresso', titleCn: '双份浓缩咖啡', shortName: 'DO' }, + { name: 'dragon-berry-mango-ice', title: 'dragon berry mango ice', titleCn: '龙莓芒果冰', shortName: 'DR' }, + { name: 'dragon-fruit', title: 'dragon fruit', titleCn: '龙果', shortName: 'DR' }, + { name: 'dragon-fruit-lychee-ice', title: 'dragon fruit lychee ice', titleCn: '龙果荔枝冰', shortName: 'DR' }, + { name: 'dragon-fruit-strawberry-ice', title: 'dragon fruit strawberry ice', titleCn: '龙果草莓冰', shortName: 'DR' }, + { name: 'dragon-fruit-strawnana', title: 'dragon fruit strawnana', titleCn: '龙果香蕉', shortName: 'DR' }, + { name: 'dragon-melon-ice', title: 'dragon melon ice', titleCn: '龙瓜冰', shortName: 'DR' }, + { name: 'dragonfruit-lychee', title: 'dragonfruit lychee', titleCn: '龙果荔枝', shortName: 'DR' }, + { name: 'dreamy-dragonfruit-lychee-iced', title: 'dreamy dragonfruit lychee iced', titleCn: '梦幻龙果荔枝冰', shortName: 'DR' }, + { name: 'dub-dub', title: 'dub dub', titleCn: '双重', shortName: 'DU' }, + { name: 'durian', title: 'durian', titleCn: '榴莲', shortName: 'DU' }, + { name: 'electric-fruit-blast', title: 'electric fruit blast', titleCn: '电果爆炸', shortName: 'EL' }, + { name: 'electric-orange', title: 'electric orange', titleCn: '电橙', shortName: 'EL' }, + { name: 'energy-drink', title: 'energy drink', titleCn: '能量饮料', shortName: 'EN' }, + { name: 'epic-apple', title: 'epic apple', titleCn: '极致苹果', shortName: 'EP' }, + { name: 'epic-apple-peach', title: 'epic apple peach', titleCn: '极致苹果桃', shortName: 'EP' }, + { name: 'epic-banana', title: 'epic banana', titleCn: '极致香蕉', shortName: 'EP' }, + { name: 'epic-berry-swirl', title: 'epic berry swirl', titleCn: '极致浆果旋风', shortName: 'EP' }, + { name: 'epic-blue-razz', title: 'epic blue razz', titleCn: '极致蓝覆盆子', shortName: 'EP' }, + { name: 'epic-fruit-bomb', title: 'epic fruit bomb', titleCn: '极致水果炸弹', shortName: 'EP' }, + { name: 'epic-grape', title: 'epic grape', titleCn: '极致葡萄', shortName: 'EP' }, + { name: 'epic-honeydew-blackcurrant', title: 'epic honeydew blackcurrant', titleCn: '极致蜜瓜黑加仑', shortName: 'EP' }, + { name: 'epic-kiwi-mango', title: 'epic kiwi mango', titleCn: '极致奇异果芒果', shortName: 'EP' }, + { name: 'epic-peach-mango', title: 'epic peach mango', titleCn: '极致桃芒果', shortName: 'EP' }, + { name: 'epic-peppermint', title: 'epic peppermint', titleCn: '极致薄荷', shortName: 'EP' }, + { name: 'epic-sour-berries', title: 'epic sour berries', titleCn: '极致酸浆果', shortName: 'EP' }, + { name: 'epic-strawberry', title: 'epic strawberry', titleCn: '极致草莓', shortName: 'EP' }, + { name: 'epic-strawberry-watermelon', title: 'epic strawberry watermelon', titleCn: '极致草莓西瓜', shortName: 'EP' }, + { name: 'epic-watermelon-kiwi', title: 'epic watermelon kiwi', titleCn: '极致西瓜奇异果', shortName: 'EP' }, + { name: 'exotic-mango', title: 'exotic mango', titleCn: '异国芒果', shortName: 'EX' }, + { name: 'extreme-chill-mint', title: 'extreme chill mint', titleCn: '极寒薄荷', shortName: 'EX' }, + { name: 'extreme-cinnamon', title: 'extreme cinnamon', titleCn: '极寒肉桂', shortName: 'EX' }, + { name: 'extreme-mint', title: 'extreme mint', titleCn: '极寒薄荷', shortName: 'EX' }, + { name: 'extreme-mint-iced', title: 'extreme mint iced', titleCn: '极寒薄荷冰', shortName: 'EX' }, + { name: 'famous-fruit-ko-iced', title: 'famous fruit ko iced', titleCn: '知名水果 KO 冰', shortName: 'FA' }, + { name: 'famous-fruit-ko-lced', title: 'famous fruit ko lced', titleCn: '知名水果 KO 冷饮', shortName: 'FA' }, + { name: 'fizzy', title: 'fizzy', titleCn: '汽水', shortName: 'FI' }, + { name: 'flavourless', title: 'flavourless', titleCn: '无味', shortName: 'FL' }, + { name: 'flippin-fruit-flash', title: 'flippin fruit flash', titleCn: '翻转水果闪电', shortName: 'FL' }, + { name: 'flippin-fruit-flash-(rainbow-burst)', title: 'flippin fruit flash (rainbow burst)', titleCn: '翻转水果闪电(彩虹爆发)', shortName: 'FL' }, + { name: 'forest-fruits', title: 'forest fruits', titleCn: '森林水果', shortName: 'FO' }, + { name: 'fragrant-grapefruit', title: 'fragrant grapefruit', titleCn: '香气葡萄柚', shortName: 'FR' }, + { name: 'freeze', title: 'freeze', titleCn: '冰冻', shortName: 'FR' }, + { name: 'freeze-mint', title: 'freeze mint', titleCn: '冰薄荷', shortName: 'FR' }, + { name: 'freeze-mint-salty', title: 'freeze mint salty', titleCn: '冰薄荷咸味', shortName: 'FR' }, + { name: 'freezing-peppermint', title: 'freezing peppermint', titleCn: '冰爽薄荷', shortName: 'FR' }, + { name: 'freezy-berry-peachy', title: 'freezy berry peachy', titleCn: '冰冻浆果桃', shortName: 'FR' }, + { name: 'fresh-fruit', title: 'fresh fruit', titleCn: '新鲜水果', shortName: 'FR' }, + { name: 'fresh-mint', title: 'fresh mint', titleCn: '新鲜薄荷', shortName: 'FR' }, + { name: 'fresh-mint-ice', title: 'fresh mint ice', titleCn: '新鲜薄荷冰', shortName: 'FR' }, + { name: 'froot-b', title: 'froot b', titleCn: '水果 B', shortName: 'FR' }, + { name: 'frost', title: 'frost', titleCn: '霜冻', shortName: 'FR' }, + { name: 'frost-mint', title: 'frost mint', titleCn: '霜薄荷', shortName: 'FR' }, + { name: 'frosted-strawberries', title: 'frosted strawberries', titleCn: '霜冻草莓', shortName: 'FR' }, + { name: 'frosty-grapefruit', title: 'frosty grapefruit', titleCn: '冰爽葡萄柚', shortName: 'FR' }, + { name: 'frozen-classical-ice', title: 'frozen classical ice', titleCn: '冷冻经典冰', shortName: 'FR' }, + { name: 'frozen-cloudberry', title: 'frozen cloudberry', titleCn: '冷冻云莓', shortName: 'FR' }, + { name: 'frozen-mint', title: 'frozen mint', titleCn: '冷冻薄荷', shortName: 'FR' }, + { name: 'frozen-pineapple', title: 'frozen pineapple', titleCn: '冷冻菠萝', shortName: 'FR' }, + { name: 'frozen-strawberry', title: 'frozen strawberry', titleCn: '冷冻草莓', shortName: 'FR' }, + { name: 'frozen-strawberrygb(gummy-bear)', title: 'frozen strawberrygb(gummy bear)', titleCn: '冷冻草莓软糖', shortName: 'FR' }, + { name: 'grapefruit-grape-gb(gummy-bear)', title: 'grapefruit grape gb(gummy bear)', titleCn: '葡萄柚葡萄软糖', shortName: 'GR' }, + { name: 'fruit-flash-ice', title: 'fruit flash ice', titleCn: '水果闪电冰', shortName: 'FR' }, + { name: 'fruity-explosion', title: 'fruity explosion', titleCn: '水果爆炸', shortName: 'FR' }, + { name: 'fuji-apple-ice', title: 'fuji apple ice', titleCn: '富士苹果冰', shortName: 'FU' }, + { name: 'fuji-ice', title: 'fuji ice', titleCn: '富士冰', shortName: 'FU' }, + { name: 'fuji-melon-ice', title: 'fuji melon ice', titleCn: '富士瓜冰', shortName: 'FU' }, + { name: 'full-charge', title: 'full charge', titleCn: '满电', shortName: 'FU' }, + { name: 'gb', title: 'gb', titleCn: '软糖', shortName: 'GB' }, + { name: 'gb(gummy-bear)', title: 'gb(gummy bear)', titleCn: '软糖(Gummy Bear)', shortName: 'GB' }, + { name: 'gentle-mint', title: 'gentle mint', titleCn: '温和薄荷', shortName: 'GE' }, + { name: 'ghost-cola-&-vanilla', title: 'ghost cola & vanilla', titleCn: '幽灵可乐香草', shortName: 'GH' }, + { name: 'ghost-cola-ice', title: 'ghost cola ice', titleCn: '幽灵可乐冰', shortName: 'GH' }, + { name: 'ghost-mango', title: 'ghost mango', titleCn: '幽灵芒果', shortName: 'GH' }, + { name: 'ghost-original', title: 'ghost original', titleCn: '幽灵原味', shortName: 'GH' }, + { name: 'ghost-watermelon-ice', title: 'ghost watermelon ice', titleCn: '幽灵西瓜冰', shortName: 'GH' }, + { name: 'gnarly-green-d-(green-dew)', title: 'gnarly green d (green dew)', titleCn: '狂野绿 D(绿色露水)', shortName: 'GN' }, + { name: 'gold-edition', title: 'gold edition', titleCn: '金版', shortName: 'GO' }, + { name: 'grape', title: 'grape', titleCn: '葡萄', shortName: 'GR' }, + { name: 'grape-cherry', title: 'grape cherry', titleCn: '葡萄樱桃', shortName: 'GR' }, + { name: 'grape-fury-ice', title: 'grape fury ice', titleCn: '葡萄狂怒冰', shortName: 'GR' }, + { name: 'grape-honeydew-ice', title: 'grape honeydew ice', titleCn: '葡萄蜜瓜冰', shortName: 'GR' }, + { name: 'grape-ice', title: 'grape ice', titleCn: '葡萄冰', shortName: 'GR' }, + { name: 'grape-pomegranate-ice', title: 'grape pomegranate ice', titleCn: '葡萄石榴冰', shortName: 'GR' }, + { name: 'grapefruit-grape', title: 'grapefruit grape', titleCn: '葡萄柚葡萄', shortName: 'GR' }, + { name: 'grapefruit-ice', title: 'grapefruit ice', titleCn: '葡萄柚冰', shortName: 'GR' }, + { name: 'grapes', title: 'grapes', titleCn: '葡萄', shortName: 'GR' }, + { name: 'grapplin-grape-sour-apple-iced', title: 'grapplin grape sour apple iced', titleCn: '葡萄酸苹果冰', shortName: 'GR' }, + { name: 'green-apple', title: 'green apple', titleCn: '青苹果', shortName: 'GR' }, + { name: 'green-apple-ice', title: 'green apple ice', titleCn: '青苹果冰', shortName: 'GR' }, + { name: 'green-grape-ice', title: 'green grape ice', titleCn: '青葡萄冰', shortName: 'GR' }, + { name: 'green-mango-ice', title: 'green mango ice', titleCn: '青芒果冰', shortName: 'GR' }, + { name: 'green-mint', title: 'green mint', titleCn: '青薄荷', shortName: 'GR' }, + { name: 'green-spearmint', title: 'green spearmint', titleCn: '青留兰香', shortName: 'GR' }, + { name: 'green-tea', title: 'green tea', titleCn: '绿茶', shortName: 'GR' }, + { name: 'groovy-grape', title: 'groovy grape', titleCn: '活力葡萄', shortName: 'GR' }, + { name: 'groovy-grape-passionfruit-iced', title: 'groovy grape passionfruit iced', titleCn: '活力葡萄激情果冰', shortName: 'GR' }, + { name: 'guava-ice', title: 'guava ice', titleCn: '番石榴冰', shortName: 'GU' }, + { name: 'guava-ice-t', title: 'guava ice t', titleCn: '番石榴冰 T', shortName: 'GU' }, + { name: 'guava-mango-peach', title: 'guava mango peach', titleCn: '番石榴芒果桃', shortName: 'GU' }, + { name: 'gusto-green-apple', title: 'gusto green apple', titleCn: '绿苹果狂热', shortName: 'GU' }, + { name: 'hakuna', title: 'hakuna', titleCn: '哈库纳', shortName: 'HA' }, + { name: 'harambae', title: 'harambae', titleCn: '哈兰贝', shortName: 'HA' }, + { name: 'harmony', title: 'harmony', titleCn: '和谐', shortName: 'HA' }, + { name: 'haven', title: 'haven', titleCn: '避风港', shortName: 'HA' }, + { name: 'haven-iced', title: 'haven iced', titleCn: '避风港冰', shortName: 'HA' }, + { name: 'hawaiian-blue', title: 'hawaiian blue', titleCn: '夏威夷蓝', shortName: 'HA' }, + { name: 'hawaiian-mist-ice', title: 'hawaiian mist ice', titleCn: '夏威夷薄雾冰', shortName: 'HA' }, + { name: 'hawaiian-storm', title: 'hawaiian storm', titleCn: '夏威夷风暴', shortName: 'HA' }, + { name: 'hip-honeydew-mango-iced', title: 'hip honeydew mango iced', titleCn: '蜜瓜芒果冰', shortName: 'HI' }, + { name: 'hokkaido-milk', title: 'hokkaido milk', titleCn: '北海道牛奶', shortName: 'HO' }, + { name: 'honeydew-blackcurrant', title: 'honeydew blackcurrant', titleCn: '蜜瓜黑加仑', shortName: 'HO' }, + { name: 'honeydew-mango-ice', title: 'honeydew mango ice', titleCn: '蜜瓜芒果冰', shortName: 'HO' }, + { name: 'hype', title: 'hype', titleCn: '狂热', shortName: 'HY' }, + { name: 'ice-blast', title: 'ice blast', titleCn: '冰爆', shortName: 'IC' }, + { name: 'ice-cool', title: 'ice cool', titleCn: '冰凉', shortName: 'IC' }, + { name: 'ice-cream', title: 'ice cream', titleCn: '冰淇淋', shortName: 'IC' }, + { name: 'ice-mint', title: 'ice mint', titleCn: '冰薄荷', shortName: 'IC' }, + { name: 'ice-wintergreen', title: 'ice wintergreen', titleCn: '冰冬青', shortName: 'IC' }, + { name: 'iced-americano', title: 'iced americano', titleCn: '冰美式', shortName: 'IC' }, + { name: 'icy-berries', title: 'icy berries', titleCn: '冰爽浆果', shortName: 'IC' }, + { name: 'icy-blackcurrant', title: 'icy blackcurrant', titleCn: '冰爽黑加仑', shortName: 'IC' }, + { name: 'icy-cherry', title: 'icy cherry', titleCn: '冰爽樱桃', shortName: 'IC' }, + { name: 'icy-mint', title: 'icy mint', titleCn: '冰爽薄荷', shortName: 'IC' }, + { name: 'icy-pink-clouds', title: 'icy pink clouds', titleCn: '冰粉云', shortName: 'IC' }, + { name: 'intense-blue-razz', title: 'intense blue razz', titleCn: '强烈蓝覆盆子', shortName: 'IN' }, + { name: 'intense-blueberry-lemon', title: 'intense blueberry lemon', titleCn: '强烈蓝莓柠檬', shortName: 'IN' }, + { name: 'intense-flavourless', title: 'intense flavourless', titleCn: '强烈无味', shortName: 'IN' }, + { name: 'intense-fruity-explosion', title: 'intense fruity explosion', titleCn: '强烈水果爆炸', shortName: 'IN' }, + { name: 'intense-juicy-peach', title: 'intense juicy peach', titleCn: '强烈多汁桃', shortName: 'IN' }, + { name: 'intense-red-apple', title: 'intense red apple', titleCn: '强烈红苹果', shortName: 'IN' }, + { name: 'intense-ripe-mango', title: 'intense ripe mango', titleCn: '强烈熟芒果', shortName: 'IN' }, + { name: 'intense-strawberry-watermelon', title: 'intense strawberry watermelon', titleCn: '强烈草莓西瓜', shortName: 'IN' }, + { name: 'intense-white-grape', title: 'intense white grape', titleCn: '强烈白葡萄', shortName: 'IN' }, + { name: 'intense-white-mint', title: 'intense white mint', titleCn: '强烈白薄荷', shortName: 'IN' }, + { name: 'jasmine-tea', title: 'jasmine tea', titleCn: '茉莉茶', shortName: 'JA' }, + { name: 'jiggly-b', title: 'jiggly b', titleCn: '果冻 B', shortName: 'JI' }, + { name: 'jiggly-sting', title: 'jiggly sting', titleCn: '果冻刺', shortName: 'JI' }, + { name: 'juicy-mango', title: 'juicy mango', titleCn: '多汁芒果', shortName: 'JU' }, + { name: 'juicy-peach', title: 'juicy peach', titleCn: '多汁桃', shortName: 'JU' }, + { name: 'juicy-peach-ice', title: 'juicy peach ice', titleCn: '多汁桃冰', shortName: 'JU' }, + { name: 'jungle-secrets', title: 'jungle secrets', titleCn: '丛林秘密', shortName: 'JU' }, + { name: 'kanzi', title: 'kanzi', titleCn: '甘之', shortName: 'KA' }, + { name: 'kewl-kiwi-passionfruit-iced', title: 'kewl kiwi passionfruit iced', titleCn: '酷奇奇', shortName: 'KE' }, + { name: 'kiwi-berry-ice', title: 'kiwi berry ice', titleCn: '奇异果浆果冰', shortName: 'KI' }, + { name: 'kiwi-dragon-berry', title: 'kiwi dragon berry', titleCn: '奇异果龙莓', shortName: 'KI' }, + { name: 'kiwi-green-t', title: 'kiwi green t', titleCn: '奇异果绿茶', shortName: 'KI' }, + { name: 'kiwi-guava-ice', title: 'kiwi guava ice', titleCn: '奇异果番石榴冰', shortName: 'KI' }, + { name: 'kiwi-guava-passionfruit-ice', title: 'kiwi guava passionfruit ice', titleCn: '奇异果番石榴激情果冰', shortName: 'KI' }, + { name: 'kiwi-passion-fruit-guava', title: 'kiwi passion fruit guava', titleCn: '奇异果激情果番石榴', shortName: 'KI' }, + { name: 'kyoho-grape', title: 'kyoho grape', titleCn: '巨峰葡萄', shortName: 'KY' }, + { name: 'kyoho-grape-ice', title: 'kyoho grape ice', titleCn: '巨峰葡萄冰', shortName: 'KY' }, + { name: 'lemon', title: 'lemon', titleCn: '柠檬', shortName: 'LE' }, + { name: 'lemon-berry', title: 'lemon berry', titleCn: '柠檬浆果', shortName: 'LE' }, + { name: 'lemon-blue-razz-ice', title: 'lemon blue razz ice', titleCn: '柠檬蓝覆盆子冰', shortName: 'LE' }, + { name: 'lemon-lime-cranberry', title: 'lemon lime cranberry', titleCn: '柠檬青柠蔓越莓', shortName: 'LE' }, + { name: 'lemon-lime-ice', title: 'lemon lime ice', titleCn: '柠檬青柠冰', shortName: 'LE' }, + { name: 'lemon-sprite', title: 'lemon sprite', titleCn: '柠檬汽水', shortName: 'LE' }, + { name: 'lemon-spritz', title: 'lemon spritz', titleCn: '柠檬气泡', shortName: 'LE' }, + { name: 'lemon-squeeze-ice', title: 'lemon squeeze ice', titleCn: '柠檬榨汁冰', shortName: 'LE' }, + { name: 'lemon-squeeze-iced', title: 'lemon squeeze iced', titleCn: '柠檬榨汁冷饮', shortName: 'LE' }, + { name: 'lemon-t', title: 'lemon t', titleCn: '柠檬 T', shortName: 'LE' }, + { name: 'lemon-tea-ice', title: 'lemon tea ice', titleCn: '柠檬茶冰', shortName: 'LE' }, + { name: 'lemon-twist-ice', title: 'lemon twist ice', titleCn: '柠檬扭转冰', shortName: 'LE' }, + { name: 'lemur', title: 'lemur', titleCn: '狐猴', shortName: 'LE' }, + { name: 'lime-berry-orange-ice', title: 'lime berry orange ice', titleCn: '青柠浆果橙冰', shortName: 'LI' }, + { name: 'lime-flame', title: 'lime flame', titleCn: '青柠火焰', shortName: 'LI' }, + { name: 'liquorice', title: 'liquorice', titleCn: '甘草', shortName: 'LI' }, + { name: 'lit-lychee-watermelon-iced', title: 'lit lychee watermelon iced', titleCn: '荔枝西瓜冰', shortName: 'LI' }, + { name: 'loco-cocoa-latte-iced', title: 'loco cocoa latte iced', titleCn: '可可拿铁冷饮', shortName: 'LO' }, + { name: 'lofty-liquorice', title: 'lofty liquorice', titleCn: '高挑甘草', shortName: 'LO' }, + { name: 'lush-ice', title: 'lush ice', titleCn: '冰爽浓郁', shortName: 'LU' }, + { name: 'lychee-ice', title: 'lychee ice', titleCn: '荔枝冰', shortName: 'LY' }, + { name: 'lychee-mango-ice', title: 'lychee mango ice', titleCn: '荔枝芒果冰', shortName: 'LY' }, + { name: 'lychee-mango-melon', title: 'lychee mango melon', titleCn: '荔枝芒果瓜', shortName: 'LY' }, + { name: 'lychee-melon-ice', title: 'lychee melon ice', titleCn: '荔枝瓜冰', shortName: 'LY' }, + { name: 'lychee-watermelon-strawberry', title: 'lychee watermelon strawberry', titleCn: '荔枝西瓜草莓', shortName: 'LY' }, + { name: 'mad-mango-peach', title: 'mad mango peach', titleCn: '疯狂芒果桃', shortName: 'MA' }, + { name: 'mangabeys', title: 'mangabeys', titleCn: '长臂猿', shortName: 'MA' }, + { name: 'mango', title: 'mango', titleCn: '芒果', shortName: 'MA' }, + { name: 'mango-berry', title: 'mango berry', titleCn: '芒果浆果', shortName: 'MA' }, + { name: 'mango-blueberry', title: 'mango blueberry', titleCn: '芒果蓝莓', shortName: 'MA' }, + { name: 'mango-dragon-fruit-lemon-ice', title: 'mango dragon fruit lemon ice', titleCn: '芒果龙果柠檬冰', shortName: 'MA' }, + { name: 'mango-flame', title: 'mango flame', titleCn: '芒果火焰', shortName: 'MA' }, + { name: 'mango-honeydew-ice', title: 'mango honeydew ice', titleCn: '芒果蜜瓜冰', shortName: 'MA' }, + { name: 'mango-ice', title: 'mango ice', titleCn: '芒果冰', shortName: 'MA' }, + { name: 'mango-madness', title: 'mango madness', titleCn: '芒果狂热', shortName: 'MA' }, + { name: 'mango-nectar-ice', title: 'mango nectar ice', titleCn: '芒果花蜜冰', shortName: 'MA' }, + { name: 'mango-on-ice', title: 'mango on ice', titleCn: '芒果冰镇', shortName: 'MA' }, + { name: 'mango-melon', title: 'mango melon', titleCn: '芒果瓜', shortName: 'MA' }, + { name: 'mango-peach', title: 'mango peach', titleCn: '芒果桃', shortName: 'MA' }, + { name: 'mango-peach-apricot-ice', title: 'mango peach apricot ice', titleCn: '芒果桃杏冰', shortName: 'MA' }, + { name: 'mango-peach-orange', title: 'mango peach orange', titleCn: '芒果桃橙', shortName: 'MA' }, + { name: 'mango-peach-tings', title: 'mango peach tings', titleCn: '芒果桃滋味', shortName: 'MA' }, + { name: 'mango-peach-watermelon', title: 'mango peach watermelon', titleCn: '芒果桃西瓜', shortName: 'MA' }, + { name: 'mango-pineapple', title: 'mango pineapple', titleCn: '芒果菠萝', shortName: 'MA' }, + { name: 'mango-pineapple-guava-ice', title: 'mango pineapple guava ice', titleCn: '芒果菠萝番石榴冰', shortName: 'MA' }, + { name: 'mango-pineapple-ice', title: 'mango pineapple ice', titleCn: '芒果菠萝冰', shortName: 'MA' }, + { name: 'mango-squared', title: 'mango squared', titleCn: '芒果平方', shortName: 'MA' }, + { name: 'matata', title: 'matata', titleCn: '马塔塔', shortName: 'MA' }, + { name: 'max-freeze', title: 'max freeze', titleCn: '极冻', shortName: 'MA' }, + { name: 'max-polar-mint', title: 'max polar mint', titleCn: '极地薄荷', shortName: 'MA' }, + { name: 'max-polarmint', title: 'max polarmint', titleCn: '极地薄荷', shortName: 'MA' }, + { name: 'mclaren-sweet-papaya', title: 'mclaren sweet papaya', titleCn: '迈凯轮甜木瓜', shortName: 'MC' }, + { name: 'mega-mixed-berries', title: 'mega mixed berries', titleCn: '超级混合浆果', shortName: 'ME' }, + { name: 'melon-&-mint', title: 'melon & mint', titleCn: '瓜与薄荷', shortName: 'ME' }, + { name: 'melon-ice', title: 'melon ice', titleCn: '瓜冰', shortName: 'ME' }, + { name: 'menthol', title: 'menthol', titleCn: '薄荷', shortName: 'ME' }, + { name: 'menthol-ice', title: 'menthol ice', titleCn: '薄荷冰', shortName: 'ME' }, + { name: 'mexican-mango-ice', title: 'mexican mango ice', titleCn: '墨西哥芒果冰', shortName: 'ME' }, + { name: 'miami-mint', title: 'miami mint', titleCn: '迈阿密薄荷', shortName: 'MI' }, + { name: 'mint', title: 'mint', titleCn: '薄荷', shortName: 'MI' }, + { name: 'mint-energy', title: 'mint energy', titleCn: '薄荷 能量', shortName: 'MI' }, + { name: 'mint-tobacco', title: 'mint tobacco', titleCn: '薄荷烟草', shortName: 'MI' }, + { name: 'mirage', title: 'mirage', titleCn: '海市蜃楼', shortName: 'MI' }, + { name: 'mix-berries', title: 'mix berries', titleCn: '混合浆果', shortName: 'MI' }, + { name: 'mixed-barries', title: 'mixed barries', titleCn: '混合浆果', shortName: 'MI' }, + { name: 'mixed-berry', title: 'mixed berry', titleCn: '混合浆果', shortName: 'MI' }, + { name: 'mixed-fruit', title: 'mixed fruit', titleCn: '混合水果', shortName: 'MI' }, + { name: 'mocha-ice', title: 'mocha ice', titleCn: '摩卡冰', shortName: 'MO' }, + { name: 'morocco-mint', title: 'morocco mint', titleCn: '摩洛哥薄荷', shortName: 'MO' }, + { name: 'morocco-mint-(thermal)', title: 'morocco mint (thermal)', titleCn: '摩洛哥薄荷(热感)', shortName: 'MO' }, + { name: 'mung-beans', title: 'mung beans', titleCn: '绿豆', shortName: 'MU' }, + { name: 'nasty-tropic', title: 'nasty tropic', titleCn: '恶搞热带', shortName: 'NA' }, + { name: 'nectarine-ice', title: 'nectarine ice', titleCn: '油桃冰', shortName: 'NE' }, + { name: 'night-rider', title: 'night rider', titleCn: '夜骑', shortName: 'NI' }, + { name: 'nirvana', title: 'nirvana', titleCn: '宁静蓝莓', shortName: 'NI' }, + { name: 'north-american-style(root-beer)', title: 'north american style(root beer)', titleCn: '北美风格(根啤)', shortName: 'NO' }, + { name: 'northern-blue-razz', title: 'northern blue razz', titleCn: '北方蓝覆盆子', shortName: 'NO' }, + { name: 'nutty-virginia', title: 'nutty virginia', titleCn: '坚果弗吉尼亚', shortName: 'NU' }, + { name: 'orange', title: 'orange', titleCn: '橙子', shortName: 'OR' }, + { name: 'orange-citrus', title: 'orange citrus', titleCn: '橙子柑橘', shortName: 'OR' }, + { name: 'orange-fizz-ice', title: 'orange fizz ice', titleCn: '橙子汽水冰', shortName: 'OR' }, + { name: 'orange-ft', title: 'orange ft', titleCn: '橙子 FT', shortName: 'OR' }, + { name: 'orange-mango-guava', title: 'orange mango guava', titleCn: '橙子芒果番石榴', shortName: 'OR' }, + { name: 'orange-mango-pineapple-ice', title: 'orange mango pineapple ice', titleCn: '橙子芒果菠萝冰', shortName: 'OR' }, + { name: 'orange-p', title: 'orange p', titleCn: '橙子 P', shortName: 'OR' }, + { name: 'orange-p(fanta)', title: 'orange p(fanta)', titleCn: '橙子 P(芬达)', shortName: 'OR' }, + { name: 'orange-spark', title: 'orange spark', titleCn: '橙色火花', shortName: 'OR' }, + { name: 'orange-tangerine', title: 'orange tangerine', titleCn: '橙子柑橘', shortName: 'OR' }, + { name: 'original', title: 'original', titleCn: '原味', shortName: 'OR' }, + { name: 'packin-peach-berry', title: 'packin peach berry', titleCn: '装满桃浆果', shortName: 'PA' }, + { name: 'packin-peach-berry-(popn-peach-berry)', title: 'packin peach berry (popn peach berry)', titleCn: '装满桃浆果(Pop’n 桃浆果)', shortName: 'PA' }, + { name: 'papio', title: 'papio', titleCn: 'Papio', shortName: 'PA' }, + { name: 'paradise', title: 'paradise', titleCn: '天堂', shortName: 'PA' }, + { name: 'paradise-iced', title: 'paradise iced', titleCn: '天堂冰', shortName: 'PA' }, + { name: 'passion', title: 'passion', titleCn: '百香果', shortName: 'PA' }, + { name: 'passion-fruit', title: 'passion fruit', titleCn: '百香果冰', shortName: 'PA' }, + { name: 'passion-fruit-mango', title: 'passion fruit mango', titleCn: '百香果芒果', shortName: 'PA' }, + { name: 'passion-fruit-mango-lime', title: 'passion fruit mango lime', titleCn: '百香果芒果青柠', shortName: 'PA' }, + { name: 'passion-guava-grapefruit', title: 'passion guava grapefruit', titleCn: '百香果番石榴葡萄柚', shortName: 'PA' }, + { name: 'patas-pipe', title: 'patas pipe', titleCn: '帕塔烟斗', shortName: 'PA' }, + { name: 'peach', title: 'peach', titleCn: '桃子', shortName: 'PE' }, + { name: 'peach-&-mint', title: 'peach & mint', titleCn: '桃子薄荷', shortName: 'PE' }, + { name: 'peach-bellini', title: 'peach bellini', titleCn: '桃子贝里尼', shortName: 'PE' }, + { name: 'peach-berry', title: 'peach berry', titleCn: '桃子浆果', shortName: 'PE' }, + { name: 'peach-berry-ice', title: 'peach berry ice', titleCn: '桃子浆果冰', shortName: 'PE' }, + { name: 'peach-berry-lime-ice', title: 'peach berry lime ice', titleCn: '桃子浆果青柠冰', shortName: 'PE' }, + { name: 'peach-blossom', title: 'peach blossom', titleCn: '桃花', shortName: 'PE' }, + { name: 'peach-blue-raspberry', title: 'peach blue raspberry', titleCn: '桃子蓝莓覆盆子', shortName: 'PE' }, + { name: 'peach-blue-razz-ice', title: 'peach blue razz ice', titleCn: '桃子蓝覆盆子冰', shortName: 'PE' }, + { name: 'peach-blue-razz-mango-ice', title: 'peach blue razz mango ice', titleCn: '桃子蓝覆盆子芒果冰', shortName: 'PE' }, + { name: 'peach-blue-s', title: 'peach blue s', titleCn: '桃子蓝覆盆子 S', shortName: 'PE' }, + { name: 'peach-ice', title: 'peach ice', titleCn: '桃子冰', shortName: 'PE' }, + { name: 'peach-lychee-ice', title: 'peach lychee ice', titleCn: '桃荔枝冰', shortName: 'PE' }, + { name: 'peach-mango', title: 'peach mango', titleCn: '桃芒果', shortName: 'PE' }, + { name: 'peach-mango-ice', title: 'peach mango ice', titleCn: '桃芒果冰', shortName: 'PE' }, + { name: 'peach-mango-watermelon', title: 'peach mango watermelon', titleCn: '桃芒果西瓜', shortName: 'PE' }, + { name: 'peach-mango-watermelon-ice', title: 'peach mango watermelon ice', titleCn: '桃芒果西瓜冰', shortName: 'PE' }, + { name: 'peach-nectarine-ice', title: 'peach nectarine ice', titleCn: '桃子花蜜冰', shortName: 'PE' }, + { name: 'peach-passion-ice', title: 'peach passion ice', titleCn: '桃子桃冰', shortName: 'PE' }, + { name: 'peach-raspberry', title: 'peach raspberry', titleCn: '桃覆盆子', shortName: 'PE' }, + { name: 'peach-strawberry-ice', title: 'peach strawberry ice', titleCn: '桃草莓冰', shortName: 'PE' }, + { name: 'peach-strawberry-watermelon', title: 'peach strawberry watermelon', titleCn: '桃草莓西瓜', shortName: 'PE' }, + { name: 'peach-watermelon-ice', title: 'peach watermelon ice', titleCn: '桃西瓜冰', shortName: 'PE' }, + { name: 'peach-zing', title: 'peach zing', titleCn: '桃子滋味', shortName: 'PE' }, + { name: 'peaches-cream', title: 'peaches cream', titleCn: '桃子奶油', shortName: 'PE' }, + { name: 'peppered-mint', title: 'peppered mint', titleCn: '胡椒薄荷', shortName: 'PE' }, + { name: 'peppermint', title: 'peppermint', titleCn: '薄荷', shortName: 'PE' }, + { name: 'peppermint-salty', title: 'peppermint salty', titleCn: '薄荷咸味', shortName: 'PE' }, + { name: 'peppermint-storm', title: 'peppermint storm', titleCn: '薄荷风暴', shortName: 'PE' }, + { name: 'pina-blend', title: 'pina blend', titleCn: '菠萝混合', shortName: 'PI' }, + { name: 'pina-colada-ice', title: 'pina colada ice', titleCn: '菠萝椰子冰', shortName: 'PI' }, + { name: 'pineapple', title: 'pineapple', titleCn: '菠萝', shortName: 'PI' }, + { name: 'pineapple-blueberry-kiwi-ice', title: 'pineapple blueberry kiwi ice', titleCn: '菠萝蓝莓奇异果冰', shortName: 'PI' }, + { name: 'pineapple-citrus', title: 'pineapple citrus', titleCn: '菠萝柑橘', shortName: 'PI' }, + { name: 'pineapple-coconut', title: 'pineapple coconut', titleCn: '菠萝椰子', shortName: 'PI' }, + { name: 'pineapple-coconut-ice', title: 'pineapple coconut ice', titleCn: '菠萝椰子冰', shortName: 'PI' }, + { name: 'pineapple-ice', title: 'pineapple ice', titleCn: '菠萝冰', shortName: 'PI' }, + { name: 'pineapple-lemonade', title: 'pineapple lemonade', titleCn: '菠萝柠檬水', shortName: 'PI' }, + { name: 'pineapple-orange-cherry', title: 'pineapple orange cherry', titleCn: '菠萝橙樱桃', shortName: 'PI' }, + { name: 'pink-lemon', title: 'pink lemon', titleCn: '粉柠檬', shortName: 'PI' }, + { name: 'pink-lemon-ice', title: 'pink lemon ice', titleCn: '粉柠檬冰', shortName: 'PI' }, + { name: 'pink-lemonade', title: 'pink lemonade', titleCn: '粉红柠檬水', shortName: 'PI' }, + { name: 'pink-punch', title: 'pink punch', titleCn: '粉红拳', shortName: 'PI' }, + { name: 'polar-chill', title: 'polar chill', titleCn: '极地清凉', shortName: 'PO' }, + { name: 'polar-mint-max', title: 'polar mint max', titleCn: '极地薄荷', shortName: 'PO' }, + { name: 'pomegranate-ice', title: 'pomegranate ice', titleCn: '石榴冰', shortName: 'PO' }, + { name: 'poppin-strawkiwi', title: 'poppin strawkiwi', titleCn: '草莓猕猴', shortName: 'PO' }, + { name: 'prism-ice', title: 'prism ice', titleCn: '棱镜冰', shortName: 'PR' }, + { name: 'punch', title: 'punch', titleCn: '果汁', shortName: 'PU' }, + { name: 'punch-ice', title: 'punch ice', titleCn: '果汁冰', shortName: 'PU' }, + { name: 'pure-tobacco', title: 'pure tobacco', titleCn: '纯烟草', shortName: 'PU' }, + { name: 'puris', title: 'puris', titleCn: '纯味', shortName: 'PU' }, + { name: 'purple-grape', title: 'purple grape', titleCn: '紫葡萄', shortName: 'PU' }, + { name: 'quad-berry', title: 'quad berry', titleCn: '四重浆果', shortName: 'QU' }, + { name: 'queen-soko', title: 'queen soko', titleCn: '女王索科', shortName: 'QU' }, + { name: 'rad-razz-melon-iced', title: 'rad razz melon iced', titleCn: '疯狂覆盆子瓜冰', shortName: 'RA' }, + { name: 'ragin-razz-mango-iced', title: 'ragin razz mango iced', titleCn: '狂暴覆盆子芒果冰', shortName: 'RA' }, + { name: 'rainbow-candy', title: 'rainbow candy', titleCn: '彩虹糖', shortName: 'RA' }, + { name: 'raspberry-blast', title: 'raspberry blast', titleCn: '覆盆子爆炸', shortName: 'RA' }, + { name: 'raspberry-buzz-ice', title: 'raspberry buzz ice', titleCn: '覆盆子嗡嗡冰', shortName: 'RA' }, + { name: 'raspberry-dragon-fruit-ice', title: 'raspberry dragon fruit ice', titleCn: '覆盆子龙果冰', shortName: 'RA' }, + { name: 'raspberry-ice', title: 'raspberry ice', titleCn: '覆盆子冰', shortName: 'RA' }, + { name: 'raspberry-lemon', title: 'raspberry lemon', titleCn: '覆盆子柠檬', shortName: 'RA' }, + { name: 'raspberry-mango-ice', title: 'raspberry mango ice', titleCn: '覆盆子芒果冰', shortName: 'RA' }, + { name: 'raspberry-peach-mango-ice', title: 'raspberry peach mango ice', titleCn: '覆盆子桃芒果冰', shortName: 'RA' }, + { name: 'raspberry-pomegranate', title: 'raspberry pomegranate', titleCn: '覆盆子石榴', shortName: 'RA' }, + { name: 'raspberry-vanilla', title: 'raspberry vanilla', titleCn: '覆盆子香草', shortName: 'RA' }, + { name: 'raspberry-watermelon', title: 'raspberry watermelon', titleCn: '覆盆子西瓜', shortName: 'RA' }, + { name: 'raspberry-watermelon-ice', title: 'raspberry watermelon ice', titleCn: '覆盆子西瓜冰', shortName: 'RA' }, + { name: 'raspberry-zing', title: 'raspberry zing', titleCn: '覆盆子滋味', shortName: 'RA' }, + { name: 'razz-apple-ice', title: 'razz apple ice', titleCn: '覆盆子苹果冰', shortName: 'RA' }, + { name: 'razz-currant-ice', title: 'razz currant ice', titleCn: '红苹果冰', shortName: 'RA' }, + { name: 'red-apple-ice', title: 'red apple ice', titleCn: '红豆', shortName: 'RE' }, + { name: 'red-bean', title: 'red bean', titleCn: '红枣 ', shortName: 'RE' }, + { name: 'red-berry-cherry', title: 'red berry cherry', titleCn: '红浆果樱桃', shortName: 'RE' }, + { name: 'red-date-yg', title: 'red date yg', titleCn: '红枣 Y', shortName: 'RE' }, + { name: 'red-eye-espresso', title: 'red eye espresso', titleCn: '红眼浓缩咖啡', shortName: 'RE' }, + { name: 'red-fruits', title: 'red fruits', titleCn: '红色水果', shortName: 'RE' }, + { name: 'red-lightning', title: 'red lightning', titleCn: '红色闪电', shortName: 'RE' }, + { name: 'red-line', title: 'red line', titleCn: '红线', shortName: 'RE' }, + { name: 'red-line-(energy-drink)', title: 'red line (energy drink)', titleCn: '红线(能量饮料)', shortName: 'RE' }, + { name: 'red-magic', title: 'red magic', titleCn: '红魔', shortName: 'RE' }, + { name: 'rich-tobacco', title: 'rich tobacco', titleCn: '浓烈烟草', shortName: 'RI' }, + { name: 'root-beer', title: 'root beer', titleCn: '根啤', shortName: 'RO' }, + { name: 'rose-grape', title: 'rose grape', titleCn: '玫瑰葡萄', shortName: 'RO' }, + { name: 'rosemary', title: 'rosemary', titleCn: '迷迭香', shortName: 'RO' }, + { name: 'royal-violet', title: 'royal violet', titleCn: '皇家紫罗兰', shortName: 'RO' }, + { name: 'ruby-berry', title: 'ruby berry', titleCn: '红宝石浆果', shortName: 'RU' }, + { name: 's-apple-ice', title: 's apple ice', titleCn: 'S 苹果冰', shortName: 'SA' }, + { name: 's-watermelon-peach', title: 's watermelon peach', titleCn: 'S 西瓜桃', shortName: 'SW' }, + { name: 'saimiri', title: 'saimiri', titleCn: '卷尾猴', shortName: 'SA' }, + { name: 'sakura-grap', title: 'sakura grap', titleCn: '樱花葡萄', shortName: 'SA' }, + { name: 'sakura-grape', title: 'sakura grape', titleCn: '樱花葡萄', shortName: 'SA' }, + { name: 'salt', title: 'salt', titleCn: '盐', shortName: 'SA' }, + { name: 'salted-caramel', title: 'salted caramel', titleCn: '咸焦糖', shortName: 'SA' }, + { name: 'salty-liquorice', title: 'salty liquorice', titleCn: '咸甘草', shortName: 'SA' }, + { name: 'sanctuary', title: 'sanctuary', titleCn: '避风港', shortName: 'SA' }, + { name: 'savage-strawberry-watermelon-iced', title: 'savage strawberry watermelon iced', titleCn: '狂野草莓西瓜冰', shortName: 'SA' }, + { name: 'shoku', title: 'shoku', titleCn: 'Shoku', shortName: 'SH' }, + { name: 'sic-strawberry-iced', title: 'sic strawberry iced', titleCn: '意大利草莓冰', shortName: 'SI' }, + { name: 'simply-spearmint', title: 'simply spearmint', titleCn: '清爽留兰香', shortName: 'SI' }, + { name: 'skc', title: 'skc', titleCn: 'SKC', shortName: 'SK' }, + { name: 'skc(skittles-candy)', title: 'skc(skittles candy)', titleCn: 'SKC(彩虹糖)', shortName: 'SK' }, + { name: 'slammin-sts-(sour-snap)', title: 'slammin sts (sour snap)', titleCn: '热烈 STS(酸糖)', shortName: 'SL' }, + { name: 'slammin-sts-iced', title: 'slammin sts iced', titleCn: '热烈 STS 冰', shortName: 'SL' }, + { name: 'smooth', title: 'smooth', titleCn: '顺滑', shortName: 'SM' }, + { name: 'smooth-mint', title: 'smooth mint', titleCn: '顺滑薄荷', shortName: 'SM' }, + { name: 'smooth-strawberry', title: 'smooth strawberry', titleCn: '顺滑草莓', shortName: 'SM' }, + { name: 'smooth-tobacco', title: 'smooth tobacco', titleCn: '顺滑烟草', shortName: 'SM' }, + { name: 'snazzy-razz', title: 'snazzy razz', titleCn: '炫酷覆盆子', shortName: 'SN' }, + { name: 'snazzy-s-storm', title: 'snazzy s storm', titleCn: '炫酷风暴', shortName: 'SN' }, + { name: 'snazzy-strawberrry-citrus', title: 'snazzy strawberrry citrus', titleCn: '炫酷草莓柑橘', shortName: 'SN' }, + { name: 'snow-pear', title: 'snow pear', titleCn: '酸梨', shortName: 'SN' }, + { name: 'sour', title: 'sour', titleCn: '酸', shortName: 'SO' }, + { name: 'sour-apple', title: 'sour apple', titleCn: '酸苹果', shortName: 'SO' }, + { name: 'sour-blue-razz', title: 'sour blue razz', titleCn: '酸蓝覆盆子', shortName: 'SO' }, + { name: 'sour-cherry', title: 'sour cherry', titleCn: '酸樱桃', shortName: 'SO' }, + { name: 'sour-lime', title: 'sour lime', titleCn: '酸青柠', shortName: 'SO' }, + { name: 'sour-ruby', title: 'sour ruby', titleCn: '酸红宝石', shortName: 'SO' }, + { name: 'spearmint', title: 'spearmint', titleCn: '留兰香', shortName: 'SP' }, + { name: 'spearmint-blast-ice', title: 'spearmint blast ice', titleCn: '留兰香爆发冰', shortName: 'SP' }, + { name: 'star-coffee', title: 'star coffee', titleCn: '星辰咖啡', shortName: 'ST' }, + { name: 'straw-kiwi-melon-ice', title: 'straw kiwi melon ice', titleCn: '草莓奇异果瓜冰', shortName: 'ST' }, + { name: 'strawanna-ice', title: 'strawanna ice', titleCn: '草莓香蕉冰', shortName: 'ST' }, + { name: 'strawberry', title: 'strawberry', titleCn: '草莓', shortName: 'ST' }, + { name: 'strawberry-&-watermelon', title: 'strawberry & watermelon', titleCn: '草莓西瓜', shortName: 'ST' }, + { name: 'strawberry-apple-grape', title: 'strawberry apple grape', titleCn: '草莓苹果葡萄', shortName: 'ST' }, + { name: 'strawberry-apricot-ice', title: 'strawberry apricot ice', titleCn: '草莓杏子冰', shortName: 'ST' }, + { name: 'strawberry-banana', title: 'strawberry banana', titleCn: '草莓香蕉', shortName: 'ST' }, + { name: 'strawberry-banana-ice', title: 'strawberry banana ice', titleCn: '草莓香蕉冰', shortName: 'ST' }, + { name: 'strawberry-banana-mango-ice', title: 'strawberry banana mango ice', titleCn: '草莓香蕉芒果冰', shortName: 'ST' }, + { name: 'strawberry-berry', title: 'strawberry berry', titleCn: '草莓浆果', shortName: 'ST' }, + { name: 'strawberry-burst-ice', title: 'strawberry burst ice', titleCn: '草莓爆发冰', shortName: 'ST' }, + { name: 'strawberry-cherry-lemon', title: 'strawberry cherry lemon', titleCn: '草莓樱桃柠檬', shortName: 'ST' }, + { name: 'strawberry-dragon-fruit', title: 'strawberry dragon fruit', titleCn: '草莓龙果', shortName: 'ST' }, + { name: 'strawberry-ft', title: 'strawberry ft', titleCn: '草莓 FT', shortName: 'ST' }, + { name: 'strawberry-grapefruit', title: 'strawberry grapefruit', titleCn: '草莓葡萄柚', shortName: 'ST' }, + { name: 'strawberry-ice', title: 'strawberry ice', titleCn: '草莓冰', shortName: 'ST' }, + { name: 'strawberry-jasmine-t', title: 'strawberry jasmine t', titleCn: '草莓茉莉茶', shortName: 'ST' }, + { name: 'strawberry-jasmine-tea', title: 'strawberry jasmine tea', titleCn: '草莓茉莉茶', shortName: 'ST' }, + { name: 'strawberry-kiwi', title: 'strawberry kiwi', titleCn: '草莓奇异果', shortName: 'ST' }, + { name: 'strawberry-kiwi-(solid)', title: 'strawberry kiwi (solid)', titleCn: '草莓奇异果(固体)', shortName: 'ST' }, + { name: 'strawberry-kiwi-banana-ice', title: 'strawberry kiwi banana ice', titleCn: '草莓奇异果香蕉冰', shortName: 'ST' }, + { name: 'strawberry-kiwi-guava-ice', title: 'strawberry kiwi guava ice', titleCn: '草莓奇异果番石榴冰', shortName: 'ST' }, + { name: 'strawberry-kiwi-ice', title: 'strawberry kiwi ice', titleCn: '草莓奇异果冰', shortName: 'ST' }, + { name: 'strawberry-lemon', title: 'strawberry lemon', titleCn: '草莓柠檬', shortName: 'ST' }, + { name: 'strawberry-lime-ice', title: 'strawberry lime ice', titleCn: '草莓青柠冰', shortName: 'ST' }, + { name: 'strawberry-lychee-ice', title: 'strawberry lychee ice', titleCn: '草莓荔枝冰', shortName: 'ST' }, + { name: 'strawberry-mango-ice', title: 'strawberry mango ice', titleCn: '草莓芒果冰', shortName: 'ST' }, + { name: 'strawberry-mint', title: 'strawberry mint', titleCn: '草莓薄荷', shortName: 'ST' }, + { name: 'strawberry-orange', title: 'strawberry orange', titleCn: '草莓橙', shortName: 'ST' }, + { name: 'strawberry-peach-mint', title: 'strawberry peach mint', titleCn: '草莓桃薄荷', shortName: 'ST' }, + { name: 'strawberry-raspberry', title: 'strawberry raspberry', titleCn: '草莓覆盆子', shortName: 'ST' }, + { name: 'strawberry-twist-ice', title: 'strawberry twist ice', titleCn: '草莓扭转冰', shortName: 'ST' }, + { name: 'strawberry-watermelon', title: 'strawberry watermelon', titleCn: '草莓西瓜', shortName: 'ST' }, + { name: 'strawberry-watermelon-ice', title: 'strawberry watermelon ice', titleCn: '草莓西瓜冰', shortName: 'ST' }, + { name: 'strawmelon-peach', title: 'strawmelon peach', titleCn: '草莓桃', shortName: 'ST' }, + { name: 'strawmelon-peach-(solid)', title: 'strawmelon peach (solid)', titleCn: '草莓桃(固体)', shortName: 'ST' }, + { name: 'strawnana-orange', title: 'strawnana orange', titleCn: '草莓香蕉橙', shortName: 'ST' }, + { name: 'summer-grape', title: 'summer grape', titleCn: '夏日葡萄', shortName: 'SU' }, + { name: 'summer-grape-(thermal)', title: 'summer grape (thermal)', titleCn: '夏日葡萄(热感)', shortName: 'SU' }, + { name: 'super-sour-blueberry-iced', title: 'super sour blueberry iced', titleCn: '超级酸蓝莓冰', shortName: 'SU' }, + { name: 'super-spearmint', title: 'super spearmint', titleCn: '超级留兰香', shortName: 'SU' }, + { name: 'super-spearmint-iced', title: 'super spearmint iced', titleCn: '超级留兰香冰', shortName: 'SU' }, + { name: 'sweet-blackcurrant', title: 'sweet blackcurrant', titleCn: '甜黑加仑', shortName: 'SW' }, + { name: 'sweet-mint', title: 'sweet mint', titleCn: '甜薄荷', shortName: 'SW' }, + { name: 't-berries', title: 't berries', titleCn: 'T 浆果', shortName: 'TB' }, + { name: 'taste-of-gods-x', title: 'taste of gods x', titleCn: '神之味 X', shortName: 'TA' }, + { name: 'the-prophet', title: 'the prophet', titleCn: '先知', shortName: 'TH' }, + { name: 'tiki-punch-ice', title: 'tiki punch ice', titleCn: 'Tiki 冲击冰', shortName: 'TI' }, + { name: 'triple-berry', title: 'triple berry', titleCn: '三重浆果', shortName: 'TR' }, + { name: 'triple-berry-ice', title: 'triple berry ice', titleCn: '三重浆果冰', shortName: 'TR' }, + { name: 'triple-mango', title: 'triple mango', titleCn: '三重芒果', shortName: 'TR' }, + { name: "trippin'-triple-berry", title: "trippin' triple berry", titleCn: '三重浆果旋风', shortName: 'TR' }, + { name: 'tropical', title: 'tropical', titleCn: '热带', shortName: 'TR' }, + { name: 'tropical-burst-ice', title: 'tropical burst ice', titleCn: '热带爆发冰', shortName: 'TR' }, + { name: 'tropical-mango', title: 'tropical mango', titleCn: '热带芒果', shortName: 'TR' }, + { name: 'tropical-mango-ice', title: 'tropical mango ice', titleCn: '热带芒果冰', shortName: 'TR' }, + { name: 'tropical-orang-ice', title: 'tropical orang ice', titleCn: '热带橙冰', shortName: 'TR' }, + { name: 'tropical-prism-blast', title: 'tropical prism blast', titleCn: '热带棱镜爆炸', shortName: 'TR' }, + { name: 'tropical-splash', title: 'tropical splash', titleCn: '热带飞溅', shortName: 'TR' }, + { name: 'tropical-splash-(solid)', title: 'tropical splash (solid)', titleCn: '热带飞溅(固体)', shortName: 'TR' }, + { name: 'tropical-storm-ice', title: 'tropical storm ice', titleCn: '热带风暴冰', shortName: 'TR' }, + { name: 'tropical-summer', title: 'tropical summer', titleCn: '热带夏日', shortName: 'TR' }, + { name: 'tropika', title: 'tropika', titleCn: '热带果', shortName: 'TR' }, + { name: 'twisted-apple', title: 'twisted apple', titleCn: '扭苹果', shortName: 'TW' }, + { name: 'twisted-pineapple', title: 'twisted pineapple', titleCn: '扭菠萝', shortName: 'TW' }, + { name: 'ultra-fresh-mint', title: 'ultra fresh mint', titleCn: '极新鲜薄荷', shortName: 'UL' }, + { name: 'vanilla', title: 'vanilla', titleCn: '香草', shortName: 'VA' }, + { name: 'vanilla-classic', title: 'vanilla classic', titleCn: '香草经典', shortName: 'VA' }, + { name: 'vanilla-classic-cola', title: 'vanilla classic cola', titleCn: '香草经典可乐', shortName: 'VA' }, + { name: 'vanilla-classic-red', title: 'vanilla classic red', titleCn: '香草经典红', shortName: 'VA' }, + { name: 'vanilla-tobacco', title: 'vanilla tobacco', titleCn: '香草烟草', shortName: 'VA' }, + { name: 'vb-arctic-berry', title: 'vb arctic berry', titleCn: 'VB 北极浆果', shortName: 'VB' }, + { name: 'vb-arctic-mint', title: 'vb arctic mint', titleCn: 'VB 北极薄荷', shortName: 'VB' }, + { name: 'vb-spearmint-salty', title: 'vb spearmint salty', titleCn: 'VB 留兰香咸味', shortName: 'VB' }, + { name: 'vc-delight', title: 'vc delight', titleCn: 'VC 美味', shortName: 'VC' }, + { name: 'vintage', title: 'vintage', titleCn: '复古', shortName: 'VI' }, + { name: 'violet-licorice', title: 'violet licorice', titleCn: '紫罗兰甘草', shortName: 'VI' }, + { name: 'watermelon', title: 'watermelon', titleCn: '西瓜', shortName: 'WA' }, + { name: 'watermelon-bbg', title: 'watermelon bbg', titleCn: '西瓜 BBG', shortName: 'WA' }, + { name: 'watermelon-bubble-gum', title: 'watermelon bubble gum', titleCn: '西瓜泡泡糖', shortName: 'WA' }, + { name: 'watermelon-cantaloupe-honeydew-ice', title: 'watermelon cantaloupe honeydew ice', titleCn: '西瓜香瓜蜜瓜冰', shortName: 'WA' }, + { name: 'watermelon-g', title: 'watermelon g', titleCn: '西瓜 G', shortName: 'WA' }, + { name: 'watermelon-ice', title: 'watermelon ice', titleCn: '西瓜冰', shortName: 'WA' }, + { name: 'watermelon-ice-(solid)', title: 'watermelon ice (solid)', titleCn: '西瓜冰(固体)', shortName: 'WA' }, + { name: 'watermelon-lime-ice', title: 'watermelon lime ice', titleCn: '西瓜青柠冰', shortName: 'WA' }, + { name: 'watermelon-mango-tango', title: 'watermelon mango tango', titleCn: '西瓜芒果探戈', shortName: 'WA' }, + { name: 'watermelona-cg', title: 'watermelona cg', titleCn: '西瓜 CG', shortName: 'WA' }, + { name: 'weekend-watermelon', title: 'weekend watermelon', titleCn: '周末西瓜', shortName: 'WE' }, + { name: 'weekend-watermelon-iced', title: 'weekend watermelon iced', titleCn: '周末西瓜冰', shortName: 'WE' }, + { name: 'white-grape', title: 'white grape', titleCn: '白葡萄', shortName: 'WH' }, + { name: 'white-grape-ice', title: 'white grape ice', titleCn: '白葡萄冰', shortName: 'WH' }, + { name: 'white-ice', title: 'white ice', titleCn: '白冰', shortName: 'WH' }, + { name: 'white-peach-ice', title: 'white peach ice', titleCn: '白桃冰', shortName: 'WH' }, + { name: 'white-peach-splash', title: 'white peach splash', titleCn: '白桃飞溅', shortName: 'WH' }, + { name: 'white-peach-yaklt', title: 'white peach yaklt', titleCn: '白桃益菌乳', shortName: 'WH' }, + { name: 'wicked-white-peach', title: 'wicked white peach', titleCn: '邪恶白桃', shortName: 'WI' }, + { name: 'wild-blue-raspberry', title: 'wild blue raspberry', titleCn: '野生蓝覆盆子', shortName: 'WI' }, + { name: 'wild-blueberry-ice', title: 'wild blueberry ice', titleCn: '野生蓝莓冰', shortName: 'WI' }, + { name: 'wild-cherry-cola', title: 'wild cherry cola', titleCn: '野樱桃可乐', shortName: 'WI' }, + { name: 'wild-dragonfruit-lychee', title: 'wild dragonfruit lychee', titleCn: '野生龙果荔枝', shortName: 'WI' }, + { name: 'wild-strawberry-banana', title: 'wild strawberry banana', titleCn: '野生草莓香蕉', shortName: 'WI' }, + { name: 'wild-strawberry-ice', title: 'wild strawberry ice', titleCn: '野生草莓冰', shortName: 'WI' }, + { name: 'wild-strawberry-watermelon', title: 'wild strawberry watermelon', titleCn: '野生草莓西瓜', shortName: 'WI' }, + { name: 'wild-white-grape', title: 'wild white grape', titleCn: '野生白葡萄', shortName: 'WI' }, + { name: 'wild-white-grape-ice', title: 'wild white grape ice', titleCn: '野生白葡萄冰', shortName: 'WI' }, + { name: 'wild-white-grape-iced', title: 'wild white grape iced', titleCn: '野生白葡萄冰(冷饮)', shortName: 'WI' }, + { name: 'winter-berry-ice', title: 'winter berry ice', titleCn: '冬季浆果冰', shortName: 'WI' }, + { name: 'winter-green', title: 'winter green', titleCn: '冬青', shortName: 'WI' }, + { name: 'wintergreen', title: 'wintergreen', titleCn: '冬青薄荷', shortName: 'WI' }, + { name: 'wintery-watermelon', title: 'wintery watermelon', titleCn: '冬季西瓜', shortName: 'WI' }, + { name: 'woke-watermelon-tropica-iced', title: 'woke watermelon tropica iced', titleCn: '觉醒西瓜热带冰', shortName: 'WO' }, + { name: 'wonder', title: 'wonder', titleCn: '奇迹', shortName: 'WO' }, + { name: 'x-freeze', title: 'x freeze', titleCn: 'X 冰冻', shortName: 'XF' }, + { name: 'zen', title: 'zen', titleCn: '禅', shortName: 'ZE' }, + { name: 'zest-flame', title: 'zest flame', titleCn: '清新火焰', shortName: 'ZE' }, + { name: 'zesty-elderflower', title: 'zesty elderflower', titleCn: '活力接骨木花', shortName: 'ZE' }, + { name: 'zingy-eucalyptus', title: 'zingy eucalyptus', titleCn: '清爽桉树', shortName: 'ZI' }, +]; + +// Total flavors: 655 + +// 强度数据 +const strengthsData = [ + { name: '1.5mg', title: '1.5mg', titleCn: '1.5毫克', shortName: '1.5' }, + { name: '2mg', title: '2mg', titleCn: '2毫克', shortName: '2MG' }, + { name: '3mg', title: '3mg', titleCn: '3毫克', shortName: '3MG' }, + { name: '3.5mg', title: '3.5mg', titleCn: '3.5毫克', shortName: '3.5' }, + { name: '4mg', title: '4mg', titleCn: '4毫克', shortName: '4MG' }, + { name: '5,2 mg', title: '5,2 mg', titleCn: '5,2毫克', shortName: '5,2' }, + { name: '5.6mg', title: '5.6mg', titleCn: '5.6毫克', shortName: '5.6' }, + { name: '6mg', title: '6mg', titleCn: '6毫克', shortName: '6MG' }, + { name: '6.5mg', title: '6.5mg', titleCn: '6.5毫克', shortName: '6.5' }, + { name: '8mg', title: '8mg', titleCn: '8毫克', shortName: '8MG' }, + { name: '9mg', title: '9mg', titleCn: '9毫克', shortName: '9MG' }, + { name: '10mg', title: '10mg', titleCn: '10毫克', shortName: '10M' }, + { name: '10,4 mg', title: '10,4 mg', titleCn: '10,4 毫克', shortName: '10,' }, + { name: '10,9mg', title: '10,9mg', titleCn: '10,9毫克', shortName: '10,' }, + { name: '11mg', title: '11mg', titleCn: '11毫克', shortName: '11M' }, + { name: '12mg', title: '12mg', titleCn: '12毫克', shortName: '12M' }, + { name: '12.5mg', title: '12.5mg', titleCn: '12.5毫克', shortName: '12.' }, + { name: '13.5mg', title: '13.5mg', titleCn: '13.5毫克', shortName: '13.' }, + { name: '14mg', title: '14mg', titleCn: '14毫克', shortName: '14M' }, + { name: '15mg', title: '15mg', titleCn: '15毫克', shortName: '15M' }, + { name: '16mg', title: '16mg', titleCn: '16毫克', shortName: '16M' }, + { name: '16.5mg', title: '16.5mg', titleCn: '16.5毫克', shortName: '16.' }, + { name: '16.6mg', title: '16.6mg', titleCn: '16.6毫克', shortName: '16.' }, + { name: '17mg', title: '17mg', titleCn: '17毫克', shortName: '17M' }, + { name: '18mg', title: '18mg', titleCn: '18毫克', shortName: '18M' }, + { name: '20mg', title: '20mg', titleCn: '20毫克', shortName: '20M' }, + { name: '30mg', title: '30mg', titleCn: '30毫克', shortName: '30M' }, + { name: 'extra-strong', title: 'extra strong', titleCn: '超强', shortName: 'EXT' }, + { name: 'low', title: 'low', titleCn: '低', shortName: 'LOW' }, + { name: 'max', title: 'max', titleCn: '最大', shortName: 'MAX' }, + { name: 'medium', title: 'medium', titleCn: '中等', shortName: 'MED' }, + { name: 'normal', title: 'normal', titleCn: '普通', shortName: 'NOR' }, + { name: 'strong', title: 'strong', titleCn: '强', shortName: 'STR' }, + { name: 'super-strong', title: 'super strong', titleCn: '特强', shortName: 'SUP' }, + { name: 'ultra-strong', title: 'ultra strong', titleCn: '极强', shortName: 'ULT' }, + { name: 'xx-strong', title: 'xx strong', titleCn: '超超强', shortName: 'XXS' }, + { name: 'x-intense', title: 'x intense', titleCn: '强', shortName: 'XIN' }, +]; + +// Total strengths: 37 + +// 品牌数据 +const brandsData = [ + { name: 'yoone', title: 'yoone', titleCn: '', shortName: 'YO' }, + { name: 'zyn', title: 'zyn', titleCn: '', shortName: 'ZY' }, + { name: 'on!', title: 'on!', titleCn: '', shortName: 'ON' }, + { name: 'alibarbar', title: 'alibarbar', titleCn: '', shortName: 'AL' }, + { name: 'iget-pro', title: 'iget pro', titleCn: '', shortName: 'IG' }, + { name: 'jux', title: 'jux', titleCn: '', shortName: 'JU' }, + { name: 'velo', title: 'velo', titleCn: '', shortName: 'VE' }, + { name: 'white-fox', title: 'white fox', titleCn: '', shortName: 'WH' }, + { name: 'zolt', title: 'zolt', titleCn: '', shortName: 'ZO' }, + { name: '77', title: '77', titleCn: '', shortName: '77' }, + { name: 'xqs', title: 'xqs', titleCn: '', shortName: 'XQ' }, + { name: 'zex', title: 'zex', titleCn: '', shortName: 'ZE' }, + { name: 'zonnic', title: 'zonnic', titleCn: '', shortName: 'ZO' }, + { name: 'lucy', title: 'Lucy', titleCn: '', shortName: 'LU' }, + { name: 'egp', title: 'EGP', titleCn: '', shortName: 'EG' }, + { name: 'bridge', title: 'Bridge', titleCn: '', shortName: 'BR' }, + { name: 'sesh', title: 'Sesh', titleCn: '', shortName: 'SE' }, + { name: 'pablo', title: 'Pablo', titleCn: '', shortName: 'PA' }, + { name: 'elfbar', title: 'elfbar', titleCn: '', shortName: 'EL' }, + { name: 'chacha', title: 'chacha', titleCn: '', shortName: 'CH' }, + { name: 'yoone-wave', title: 'yoone wave', titleCn: '', shortName: 'YO' }, + { name: 'yoone-e-liquid', title: 'yoone e-liquid', titleCn: '', shortName: 'YO' }, + { name: 'geek-bar', title: 'geek bar', titleCn: '', shortName: 'GE' }, + { name: 'iget-bar', title: 'iget bar', titleCn: '', shortName: 'IG' }, + { name: 'twelve-monkeys', title: 'twelve monkeys', titleCn: '', shortName: 'TW' }, + { name: 'z-pods', title: 'z pods', titleCn: '', shortName: 'ZP' }, + { name: 'yoone-y-pods', title: 'yoone y-pods', titleCn: '', shortName: 'YO' }, + { name: 'allo-e-liquid', title: 'allo e-liquid', titleCn: '', shortName: 'AL' }, + { name: 'allo-ultra', title: 'allo ultra', titleCn: '', shortName: 'AL' }, + { name: 'base-x', title: 'base x', titleCn: '', shortName: 'BA' }, + { name: 'breeze-pro', title: 'breeze pro', titleCn: '', shortName: 'BR' }, + { name: 'deu', title: 'deu', titleCn: '', shortName: 'DE' }, + { name: 'evo', title: 'evo', titleCn: '', shortName: 'EV' }, + { name: 'elf-bar', title: 'elf bar', titleCn: '', shortName: 'EL' }, + { name: 'feed', title: 'feed', titleCn: '', shortName: 'FE' }, + { name: 'flavour-beast', title: 'flavour beast', titleCn: '', shortName: 'FL' }, + { name: 'fog-formulas', title: 'fog formulas', titleCn: '', shortName: 'FO' }, + { name: 'fruitii', title: 'fruitii', titleCn: '', shortName: 'FR' }, + { name: 'gcore', title: 'gcore', titleCn: '', shortName: 'GC' }, + { name: 'gr1nds', title: 'gr1nds', titleCn: '', shortName: 'GR' }, + { name: 'hqd', title: 'hqd', titleCn: '', shortName: 'HQ' }, + { name: 'illusions', title: 'illusions', titleCn: '', shortName: 'IL' }, + { name: 'kraze', title: 'kraze', titleCn: '', shortName: 'KR' }, + { name: 'level-x', title: 'level x', titleCn: '', shortName: 'LE' }, + { name: 'lfgo-energy', title: 'lfgo energy', titleCn: '', shortName: 'LF' }, + { name: 'lost-mary', title: 'lost mary', titleCn: '', shortName: 'LO' }, + { name: 'mr-fog', title: 'mr fog', titleCn: '', shortName: 'MR' }, + { name: 'nicorette', title: 'nicorette', titleCn: '', shortName: 'NI' }, + { name: 'oxbar', title: 'oxbar', titleCn: '', shortName: 'OX' }, + { name: 'rabeats', title: 'rabeats', titleCn: '', shortName: 'RA' }, + { name: 'yoone-vapengin', title: 'yoone vapengin', titleCn: '', shortName: 'YO' }, + { name: 'sesh', title: 'sesh', titleCn: '', shortName: 'SE' }, + { name: 'spin', title: 'spin', titleCn: '', shortName: 'SP' }, + { name: 'stlth', title: 'stlth', titleCn: '', shortName: 'ST' }, + { name: 'tornado', title: 'tornado', titleCn: '', shortName: 'TO' }, + { name: 'uwell', title: 'uwell', titleCn: '', shortName: 'UW' }, + { name: 'vanza', title: 'vanza', titleCn: '', shortName: 'VA' }, + { name: 'vapgo', title: 'vapgo', titleCn: '', shortName: 'VA' }, + { name: 'vase', title: 'vase', titleCn: '', shortName: 'VA' }, + { name: 'vice-boost', title: 'vice boost', titleCn: '', shortName: 'VI' }, + { name: 'vozol-star', title: 'vozol star', titleCn: '', shortName: 'VO' }, + { name: 'zpods', title: 'zpods', titleCn: '', shortName: 'ZP' }, +]; + +// Total brands: 62 diff --git a/src/db/seeds/template.seeder.ts b/src/db/seeds/template.seeder.ts index be51218..f12a208 100644 --- a/src/db/seeds/template.seeder.ts +++ b/src/db/seeds/template.seeder.ts @@ -23,25 +23,44 @@ export default class TemplateSeeder implements Seeder { const templates = [ { 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模板', testData: JSON.stringify({ - brand: 'Brand', - category: 'Category', - flavor: 'Flavor', - strength: '10mg', - humidity: 'Dry', + category: { + shortName: 'CAT', + }, + attributes: [ + { shortName: 'BR' }, + { shortName: 'FL' }, + { shortName: '10MG' }, + { shortName: 'DRY' }, + ], }), }, { name: 'product.title', - value: '<%= it.brand %> <%= it.flavor %> <%= it.strength %> <%= it.humidity %>', + value: "<%= it.attributes.map(a => a.title).join(' ') %>", description: '产品标题模板', testData: JSON.stringify({ - brand: 'Brand', - flavor: 'Flavor', - strength: '10mg', - humidity: 'Dry', + attributes: [ + { title: 'Brand' }, + { title: 'Flavor' }, + { 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', + }, }), }, ]; diff --git a/src/dto/api.dto.ts b/src/dto/api.dto.ts new file mode 100644 index 0000000..ccda845 --- /dev/null +++ b/src/dto/api.dto.ts @@ -0,0 +1,182 @@ +import { ApiProperty } from '@midwayjs/swagger'; +import { Rule, RuleType } from '@midwayjs/validate'; + +export class UnifiedPaginationDTO { + // 分页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> { + // 统一查询参数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; +} + +/** + * 批量操作错误项 + */ +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; + + @ApiProperty({ description: '包含关联数据', type: Boolean, required: false, default: false }) + @Rule(RuleType.boolean().optional()) + includeRelations?: boolean = false; +} + +/** + * 批量操作结果类(泛型支持) + */ +export class BatchOperationResultDTOGeneric extends BatchOperationResultDTO { + @ApiProperty({ description: '操作成功的数据列表', type: Array }) + data?: T[]; +} + +/** + * 同步操作结果类(泛型支持) + */ +export class SyncOperationResultDTOGeneric extends SyncOperationResultDTO { + @ApiProperty({ description: '同步成功的数据列表', type: Array }) + data?: T[]; +} diff --git a/src/dto/customer.dto.ts b/src/dto/customer.dto.ts index 17404cd..353c77a 100644 --- a/src/dto/customer.dto.ts +++ b/src/dto/customer.dto.ts @@ -1,70 +1,364 @@ import { ApiProperty } from '@midwayjs/swagger'; +import { UnifiedSearchParamsDTO } from './api.dto'; +import { Customer } from '../entity/customer.entity'; -export class QueryCustomerListDTO { - @ApiProperty() - current: string; +// 客户基本信息DTO(用于响应) +export class CustomerDTO extends Customer{ + @ApiProperty({ description: '客户ID' }) + id: number; - @ApiProperty() - pageSize: string; + @ApiProperty({ description: '站点ID', required: false }) + 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; - @ApiProperty() - tags: string; + @ApiProperty({ description: '名字', required: false }) + first_name: string; - @ApiProperty() - sorterKey: string; + @ApiProperty({ description: '姓氏', required: false }) + last_name: string; - @ApiProperty() - sorterValue: string; + @ApiProperty({ description: '全名', required: false }) + fullname: string; - @ApiProperty() - state: string; + @ApiProperty({ description: '用户名', required: false }) + username: string; - @ApiProperty() - first_purchase_date: string; + @ApiProperty({ description: '电话', required: false }) + phone: string; - @ApiProperty() - customerId: number; + @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: '创建时间' }) + 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; - @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; + +// 客户统计列表响应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; +} + +// 批量更新客户请求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; + +// 客户列表响应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; } -export class CustomerDto { - @ApiProperty() - id: number; +// 批量添加客户标签请求DTO +export class BatchAddCustomerTagDTO { + @ApiProperty({ description: '客户ID' }) + customer_id: number; - @ApiProperty() - site_id: number; - - @ApiProperty() - email: string; - - @ApiProperty() - avatar: string; - - @ApiProperty() + @ApiProperty({ description: '标签列表', type: [String] }) tags: string[]; - - @ApiProperty() - rate: number; - - @ApiProperty() - state: string; - } -export class CustomerListResponseDTO { - @ApiProperty() - total: number; +// 删除客户标签请求DTO +export class DeleteCustomerTagDTO { + @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; } \ No newline at end of file diff --git a/src/dto/product.dto.ts b/src/dto/product.dto.ts index 07a8df2..a24e7e0 100644 --- a/src/dto/product.dto.ts +++ b/src/dto/product.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@midwayjs/swagger'; import { Rule, RuleType } from '@midwayjs/validate'; +import { UnifiedSearchParamsDTO } from './api.dto'; /** * 属性输入DTO @@ -251,35 +252,104 @@ export class CreateCategoryAttributeDTO { } /** - * DTO 用于分页查询产品 + * 产品查询过滤条件接口 */ -export class QueryProductDTO { - @ApiProperty({ description: '当前页', example: 1 }) - @Rule(RuleType.number().default(1)) - current: number; - - @ApiProperty({ description: '每页数量', example: 10 }) - @Rule(RuleType.number().default(10)) - pageSize: number; - - @ApiProperty({ description: '搜索关键字', required: false }) - @Rule(RuleType.string()) +export interface ProductWhereFilter { + // 产品ID + id?: number; + // 产品ID列表 + ids?: number[]; + // SKU + sku?: string; + // SKU列表 + skus?: string[]; + // 产品名称 name?: string; - - @ApiProperty({ description: '分类ID', required: false }) - @Rule(RuleType.number()) + // 产品中文名称 + nameCn?: string; + // 分类ID categoryId?: number; - - @ApiProperty({ description: '品牌ID', required: false }) - @Rule(RuleType.number()) + // 分类ID列表 + categoryIds?: number[]; + // 品牌ID brandId?: number; - - @ApiProperty({ description: '排序字段', required: false }) - @Rule(RuleType.string()) - sortField?: string; - - @ApiProperty({ description: '排序方式', required: false }) - @Rule(RuleType.string().valid('ascend', 'descend')) - sortOrder?: string; + // 品牌ID列表 + brandIds?: number[]; + // 产品类型 + type?: string; + // 价格最小值 + minPrice?: number; + // 价格最大值 + maxPrice?: number; + // 促销价格最小值 + minPromotionPrice?: number; + // 促销价格最大值 + maxPromotionPrice?: number; + // 创建时间范围开始 + createdAtStart?: string; + // 创建时间范围结束 + createdAtEnd?: string; + // 更新时间范围开始 + updatedAtStart?: string; + // 更新时间范围结束 + updatedAtEnd?: string; +} + +/** + * DTO 用于分页查询产品 + * 支持灵活的where条件、分页和排序 + */ +export class QueryProductDTO extends UnifiedSearchParamsDTO { + +} + +/** + * 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; } diff --git a/src/dto/reponse.dto.ts b/src/dto/reponse.dto.ts index 4d565e3..d0d7f3a 100644 --- a/src/dto/reponse.dto.ts +++ b/src/dto/reponse.dto.ts @@ -21,8 +21,11 @@ import { OrderNote } from '../entity/order_note.entity'; import { PaymentMethodDTO } from './logistics.dto'; import { Subscription } from '../entity/subscription.entity'; import { Dict } from '../entity/dict.entity'; +import { SyncOperationResultDTO } from './api.dto'; export class BooleanRes extends SuccessWrapper(Boolean) {} +// 同步操作结果返回数据 +export class SyncOperationResultRes extends SuccessWrapper(SyncOperationResultDTO) {} //网站配置返回数据 export class SitesResponse extends SuccessArrayWrapper(SiteConfig) {} //产品分页数据 diff --git a/src/dto/shopyy.dto.ts b/src/dto/shopyy.dto.ts index a2fba02..d48f2bb 100644 --- a/src/dto/shopyy.dto.ts +++ b/src/dto/shopyy.dto.ts @@ -103,7 +103,7 @@ export interface ShopyyOrder { total_amount?: string | number; current_total_price?: string | number; current_subtotal_price?: string | number; - current_shipping_price?: number; + current_shipping_price?: string | number; current_tax_price?: string | number; current_coupon_price?: string | number; current_payment_price?: string | number; @@ -400,32 +400,34 @@ export interface ShopyyWebhook { } // 发货相关DTO -export class ShopyyShipOrderItemDTO { - order_item_id: number; - quantity: number; +// 批量履行 +// https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse +export class ShopyyFulfillmentDTO { + "order_number": string; + "tracking_company": string; + "tracking_number": string; + "courier_code": number; + "note": string; + "mode": "replace" | 'cover' | null// 模式 replace(替换) cover (覆盖) 空(新增) } - -export class ShopyyShipOrderDTO { - tracking_number?: string; - shipping_provider?: string; - shipping_method?: string; - items?: ShopyyShipOrderItemDTO[]; +// https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse +export class ShopyPartFulfillmentDTO { + order_number: string; + note: string; + tracking_company: string; + tracking_number: string; + courier_code: string; + products: ({ + quantity: number, + order_product_id: string + })[] } - -export class ShopyyCancelShipOrderDTO { - reason?: string; - shipment_id?: string; -} - -export class ShopyyBatchShipOrderItemDTO { +// https://www.apizza.net/project/e114fb8e628e0f604379f5b26f0d8330/browse +export class ShopyyCancelFulfillmentDTO { order_id: string; - tracking_number?: string; - shipping_provider?: string; - shipping_method?: string; - items?: ShopyyShipOrderItemDTO[]; + fullfillment_id: string; } -export class ShopyyBatchShipOrdersDTO { - orders: ShopyyBatchShipOrderItemDTO[]; +export class ShopyyBatchFulfillmentItemDTO { + fullfillments: ShopyPartFulfillmentDTO[] } - diff --git a/src/dto/site-api.dto.ts b/src/dto/site-api.dto.ts index ad8fcd2..448bb39 100644 --- a/src/dto/site-api.dto.ts +++ b/src/dto/site-api.dto.ts @@ -1,28 +1,8 @@ import { ApiProperty } from '@midwayjs/swagger'; +import { + UnifiedPaginationDTO, +} from './api.dto'; -export class UnifiedPaginationDTO { - // 分页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 { // 标签DTO用于承载统一标签数据 @ApiProperty({ description: '标签ID' }) @@ -39,6 +19,24 @@ export class UnifiedCategoryDTO { @ApiProperty({ description: '分类名称' }) 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 { // 图片DTO用于承载统一图片数据 @ApiProperty({ description: '图片ID' }) @@ -139,6 +137,9 @@ export class UnifiedProductAttributeDTO { @ApiProperty({ description: '属性选项', type: [String] }) options: string[]; + + @ApiProperty({ description: '变体属性值(单个值)', required: false }) + option?: string; } export class UnifiedProductVariationDTO { @@ -146,6 +147,9 @@ export class UnifiedProductVariationDTO { @ApiProperty({ description: '变体ID' }) id: number | string; + @ApiProperty({ description: '变体名称' }) + name: string; + @ApiProperty({ description: '变体SKU' }) sku: string; @@ -164,8 +168,47 @@ export class UnifiedProductVariationDTO { @ApiProperty({ description: '库存数量' }) 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 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 { // 订单DTO用于承载统一订单数据 @ApiProperty({ description: '订单ID' }) @@ -309,6 +363,8 @@ export class UnifiedOrderDTO { @ApiProperty({ description: '支付方式' }) payment_method: string; + + @ApiProperty({ description: '退款列表', type: () => [UnifiedOrderRefundDTO] }) refunds: UnifiedOrderRefundDTO[]; @ApiProperty({ description: '创建时间' }) date_created: string; @@ -328,6 +384,9 @@ export class UnifiedOrderDTO { @ApiProperty({ description: '优惠券项', type: () => [UnifiedCouponLineDTO], required: false }) coupon_lines?: UnifiedCouponLineDTO[]; + @ApiProperty({ description: '物流追踪信息', type: () => [UnifiedOrderTrackingDTO], required: false }) + tracking?: UnifiedOrderTrackingDTO[]; + @ApiProperty({ description: '支付时间', required: false }) date_paid?: string | null; @@ -366,7 +425,6 @@ export class UnifiedShippingLineDTO { @ApiProperty({ description: '配送方式元数据' }) meta_data?: any[]; - } export class UnifiedFeeLineDTO { // 费用项DTO用于承载统一费用项数据 @@ -589,7 +647,7 @@ export class UnifiedReviewDTO { @ApiProperty({ description: '更新时间', required: false }) date_modified?: string; - @ApiProperty({ description: '原始数据', type: 'object', required: false }) + @ApiProperty({ description: '原始数据', type: 'object', required: false }) raw?: Record; } @@ -635,34 +693,6 @@ export class UploadMediaDTO { filename: string; } -export class UnifiedSearchParamsDTO> { - // 统一查询参数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; -} - export class UnifiedWebhookDTO { // Webhook DTO用于承载统一webhook数据 @ApiProperty({ description: 'Webhook ID' }) @@ -747,18 +777,8 @@ export class UpdateWebhookDTO { api_version?: string; } -export class UnifiedOrderRefundDTO { - @ApiProperty({ description: '退款ID' }) - id: number | string; - @ApiProperty({ description: '退款原因' }) - reason: string; - - @ApiProperty({ description: '退款金额' }) - total: string; -} - -export class ShipOrderItemDTO { +export class FulfillmentItemDTO { @ApiProperty({ description: '订单项ID' }) order_item_id: number; @@ -766,7 +786,7 @@ export class ShipOrderItemDTO { quantity: number; } -export class ShipOrderDTO { +export class FulfillmentDTO { @ApiProperty({ description: '物流单号', required: false }) tracking_number?: string; @@ -776,11 +796,11 @@ export class ShipOrderDTO { @ApiProperty({ description: '发货方式', required: false }) shipping_method?: string; - @ApiProperty({ description: '发货商品项', type: () => [ShipOrderItemDTO], required: false }) - items?: ShipOrderItemDTO[]; + @ApiProperty({ description: '发货商品项', type: () => [FulfillmentItemDTO], required: false }) + items?: FulfillmentItemDTO[]; } -export class CancelShipOrderDTO { +export class CancelFulfillmentDTO { @ApiProperty({ description: '取消原因', required: false }) reason?: string; @@ -788,7 +808,7 @@ export class CancelShipOrderDTO { shipment_id?: string; } -export class BatchShipOrderItemDTO { +export class BatchFulfillmentItemDTO { @ApiProperty({ description: '订单ID' }) order_id: string; @@ -801,11 +821,137 @@ export class BatchShipOrderItemDTO { @ApiProperty({ description: '发货方式', required: false }) shipping_method?: string; - @ApiProperty({ description: '发货商品项', type: () => [ShipOrderItemDTO], required: false }) - items?: ShipOrderItemDTO[]; + @ApiProperty({ description: '发货商品项', type: () => [FulfillmentItemDTO], required: false }) + items?: FulfillmentItemDTO[]; } -export class BatchShipOrdersDTO { - @ApiProperty({ description: '批量发货订单列表', type: () => [BatchShipOrderItemDTO] }) - orders: BatchShipOrderItemDTO[]; -} \ No newline at end of file +export class CreateVariationDTO { + // 创建产品变体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 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 { + // 产品变体分页DTO用于承载产品变体列表分页数据 + @ApiProperty({ description: '列表数据', type: () => [UnifiedProductVariationDTO] }) + items: UnifiedProductVariationDTO[]; +} + +export class BatchFulfillmentsDTO { + @ApiProperty({ description: '批量发货订单列表', type: () => [BatchFulfillmentItemDTO] }) + orders: BatchFulfillmentItemDTO[]; +} diff --git a/src/dto/site-sync.dto.ts b/src/dto/site-sync.dto.ts new file mode 100644 index 0000000..f6dcafc --- /dev/null +++ b/src/dto/site-sync.dto.ts @@ -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[]; +} + diff --git a/src/dto/woocommerce.dto.ts b/src/dto/woocommerce.dto.ts index 9686ee5..58b3c32 100644 --- a/src/dto/woocommerce.dto.ts +++ b/src/dto/woocommerce.dto.ts @@ -125,8 +125,97 @@ export interface WooProduct { // 元数据 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_modified?: string; date_modified_gmt?: string; + // 物流追踪信息 + trackings?: Array<{ + tracking_provider?: string; + tracking_number?: string; + date_shipped?: string; + status_shipped?: string; + }>; } export interface WooOrderRefund { id?: number; diff --git a/src/entity/area.entity.ts b/src/entity/area.entity.ts index e66de47..a4f27a4 100644 --- a/src/entity/area.entity.ts +++ b/src/entity/area.entity.ts @@ -1,6 +1,8 @@ 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') export class Area { @@ -14,4 +16,10 @@ export class Area { @ApiProperty({ description: '编码' }) @Column({ unique: true }) code: string; + + @ManyToMany(() => Site, site => site.areas) + sites: Site[]; + + @ManyToMany(() => StockPoint, stockPoint => stockPoint.areas) + stockPoints: StockPoint[]; } diff --git a/src/entity/category.entity.ts b/src/entity/category.entity.ts index c0c86ae..79d18c7 100644 --- a/src/entity/category.entity.ts +++ b/src/entity/category.entity.ts @@ -20,6 +20,10 @@ export class Category { @ApiProperty({ description: '分类唯一标识' }) @Column({ unique: true }) name: string; + // 分类短名称, 用于生成SKU + @ApiProperty({ description: '分类短名称' }) + @Column({ nullable: true }) + shortName: string; @ApiProperty({ description: '排序' }) @Column({ default: 0 }) diff --git a/src/entity/order.entity.ts b/src/entity/order.entity.ts index 09fd126..5c603dc 100644 --- a/src/entity/order.entity.ts +++ b/src/entity/order.entity.ts @@ -62,14 +62,14 @@ export class Order { currency: string; @ApiProperty() - @Column() + @Column({ nullable: true }) @Expose() - currency_symbol: string; + currency_symbol?: string; @ApiProperty() @Column({ default: false }) @Expose() - prices_include_tax: boolean; + prices_include_tax?: boolean; @ApiProperty() @Column({ type: 'timestamp', nullable: true }) diff --git a/src/entity/product.entity.ts b/src/entity/product.entity.ts index ce0cd84..8b117d7 100644 --- a/src/entity/product.entity.ts +++ b/src/entity/product.entity.ts @@ -13,7 +13,6 @@ import { import { ApiProperty } from '@midwayjs/swagger'; import { DictItem } from './dict_item.entity'; import { ProductStockComponent } from './product_stock_component.entity'; -import { ProductSiteSku } from './product_site_sku.entity'; import { Category } from './category.entity'; @Entity('product') @@ -77,7 +76,17 @@ export class Product { @ManyToMany(() => DictItem, dictItem => dictItem.products, { cascade: true, }) - @JoinTable() + @JoinTable({ + name: 'product_attributes_dict_item', + joinColumn: { + name: 'productId', + referencedColumnName: 'id' + }, + inverseJoinColumn: { + name: 'dictItemId', + referencedColumnName: 'id' + } + }) attributes: DictItem[]; // 产品的库存组成,一对多关系(使用独立表) @@ -85,9 +94,9 @@ export class Product { @OneToMany(() => ProductStockComponent, (component) => component.product, { cascade: true }) components: ProductStockComponent[]; - @ApiProperty({ description: '站点 SKU 列表', type: ProductSiteSku, isArray: true }) - @OneToMany(() => ProductSiteSku, (siteSku) => siteSku.product, { cascade: true }) - siteSkus: ProductSiteSku[]; + @ApiProperty({ description: '站点 SKU 列表', type: 'string', isArray: true }) + @Column({ type: 'simple-array' ,nullable:true}) + siteSkus: string[]; // 来源 @ApiProperty({ description: '来源', example: '1' }) diff --git a/src/entity/product_site_sku.entity.ts b/src/entity/product_site_sku.entity.ts deleted file mode 100644 index f6b3c40..0000000 --- a/src/entity/product_site_sku.entity.ts +++ /dev/null @@ -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; -} diff --git a/src/entity/site.entity.ts b/src/entity/site.entity.ts index 7e03f4b..316263a 100644 --- a/src/entity/site.entity.ts +++ b/src/entity/site.entity.ts @@ -38,10 +38,30 @@ export class Site { isDisabled: boolean; @ManyToMany(() => Area) - @JoinTable() + @JoinTable({ + name: 'site_areas_area', + joinColumn: { + name: 'siteId', + referencedColumnName: 'id' + }, + inverseJoinColumn: { + name: 'areaId', + referencedColumnName: 'id' + } + }) areas: Area[]; @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[]; } \ No newline at end of file diff --git a/src/entity/stock_point.entity.ts b/src/entity/stock_point.entity.ts index dda7ea8..a074643 100644 --- a/src/entity/stock_point.entity.ts +++ b/src/entity/stock_point.entity.ts @@ -78,7 +78,17 @@ export class StockPoint extends BaseEntity { deletedAt: Date; // 软删除时间 @ManyToMany(() => Area) - @JoinTable() + @JoinTable({ + name: 'stock_point_areas_area', + joinColumn: { + name: 'stockPointId', + referencedColumnName: 'id' + }, + inverseJoinColumn: { + name: 'areaId', + referencedColumnName: 'id' + } + }) areas: Area[]; @ManyToMany(() => Site, site => site.stockPoints) diff --git a/src/entity/user.entity.ts b/src/entity/user.entity.ts index 3b4045f..fad3c4a 100644 --- a/src/entity/user.entity.ts +++ b/src/entity/user.entity.ts @@ -20,7 +20,7 @@ export class User { @Column({ type: 'simple-array', nullable: true }) permissions: string[]; // 自定义权限 (如:['user:add', 'user:edit']) - // 新增邮箱字段,可选且唯一 + // 邮箱字段,可选且唯一 @Column({ unique: true, nullable: true }) email?: string; diff --git a/src/entity/wp_product.entity.ts b/src/entity/wp_product.entity.ts deleted file mode 100644 index b0327db..0000000 --- a/src/entity/wp_product.entity.ts +++ /dev/null @@ -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; // 产品的其他扩展字段 - - @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; -} diff --git a/src/interface/platform.interface.ts b/src/interface/platform.interface.ts index bc5e4d8..f1681ed 100644 --- a/src/interface/platform.interface.ts +++ b/src/interface/platform.interface.ts @@ -107,9 +107,9 @@ export interface IPlatformService { * @param productId 产品ID * @param variationId 变体ID * @param data 更新数据 - * @returns 更新结果 + * @returns 更新后的变体数据 */ - updateVariation(site: any, productId: string, variationId: string, data: any): Promise; + updateVariation(site: any, productId: string, variationId: string, data: any): Promise; /** * 更新订单 @@ -121,22 +121,22 @@ export interface IPlatformService { updateOrder(site: any, orderId: string, data: Record): Promise; /** - * 创建物流信息 + * 创建履约信息 * @param site 站点配置信息 * @param orderId 订单ID - * @param data 物流数据 + * @param data 履约数据 * @returns 创建结果 */ - createShipment(site: any, orderId: string, data: any): Promise; + createFulfillment(site: any, orderId: string, data: any): Promise; /** - * 删除物流信息 + * 删除履约信息 * @param site 站点配置信息 * @param orderId 订单ID - * @param trackingId 物流跟踪ID + * @param fulfillmentId 履约跟踪ID * @returns 删除结果 */ - deleteShipment(site: any, orderId: string, trackingId: string): Promise; + deleteFulfillment(site: any, orderId: string, fulfillmentId: string): Promise; /** * 批量处理产品 diff --git a/src/interface/site-adapter.interface.ts b/src/interface/site-adapter.interface.ts index a3b5fb4..0283ad4 100644 --- a/src/interface/site-adapter.interface.ts +++ b/src/interface/site-adapter.interface.ts @@ -3,17 +3,20 @@ import { UpdateReviewDTO, UnifiedMediaDTO, UnifiedOrderDTO, - UnifiedPaginationDTO, UnifiedProductDTO, UnifiedReviewDTO, - UnifiedSearchParamsDTO, UnifiedSubscriptionDTO, UnifiedCustomerDTO, UnifiedWebhookDTO, UnifiedWebhookPaginationDTO, CreateWebhookDTO, UpdateWebhookDTO, + CreateVariationDTO, + UpdateVariationDTO, + UnifiedProductVariationDTO, + UnifiedVariationPaginationDTO, } from '../dto/site-api.dto'; +import { UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/api.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; export interface ISiteAdapter { @@ -45,7 +48,7 @@ export interface ISiteAdapter { /** * 获取单个订单 */ - getOrder(id: string | number): Promise; + getOrder(id: string | number): Promise; /** * 获取订阅列表 @@ -107,10 +110,40 @@ export interface ISiteAdapter { */ updateProduct(id: string | number, data: Partial): Promise; + /** + * 删除产品 + */ + deleteProduct(id: string | number): Promise; + + /** + * 获取产品变体列表 + */ + getVariations(productId: string | number, params: UnifiedSearchParamsDTO): Promise; + + /** + * 获取所有产品变体 + */ + getAllVariations(productId: string | number, params?: UnifiedSearchParamsDTO): Promise; + + /** + * 获取单个产品变体 + */ + getVariation(productId: string | number, variationId: string | number): Promise; + + /** + * 创建产品变体 + */ + createVariation(productId: string | number, data: CreateVariationDTO): Promise; + /** * 更新产品变体 */ - updateVariation(productId: string | number, variationId: string | number, data: any): Promise; + updateVariation(productId: string | number, variationId: string | number, data: UpdateVariationDTO): Promise; + + /** + * 删除产品变体 + */ + deleteVariation(productId: string | number, variationId: string | number): Promise; /** * 获取订单备注 @@ -122,11 +155,6 @@ export interface ISiteAdapter { */ createOrderNote(orderId: string | number, data: any): Promise; - /** - * 删除产品 - */ - deleteProduct(id: string | number): Promise; - batchProcessProducts?(data: BatchOperationDTO): Promise; createOrder(data: Partial): Promise; @@ -180,9 +208,9 @@ export interface ISiteAdapter { getLinks(): Promise>; /** - * 订单发货 + * 订单履行(发货) */ - shipOrder(orderId: string | number, data: { + fulfillOrder(orderId: string | number, data: { tracking_number?: string; shipping_provider?: string; shipping_method?: string; @@ -193,10 +221,41 @@ export interface ISiteAdapter { }): Promise; /** - * 取消订单发货 + * 取消订单履行 */ - cancelShipOrder(orderId: string | number, data: { + cancelFulfillment(orderId: string | number, data: { reason?: string; shipment_id?: string; }): Promise; + + /** + * 获取订单履行信息 + */ + getOrderFulfillments(orderId: string | number): Promise; + + /** + * 创建订单履行信息 + */ + createOrderFulfillment(orderId: string | number, data: { + tracking_number: string; + tracking_provider: string; + date_shipped?: string; + status_shipped?: string; + items?: any[]; + }): Promise; + + /** + * 更新订单履行信息 + */ + updateOrderFulfillment(orderId: string | number, fulfillmentId: string, data: { + tracking_number?: string; + tracking_provider?: string; + date_shipped?: string; + status_shipped?: string; + }): Promise; + + /** + * 删除订单履行信息 + */ + deleteOrderFulfillment(orderId: string | number, fulfillmentId: string): Promise; } diff --git a/src/service/customer.service.ts b/src/service/customer.service.ts index c23f260..48f378a 100644 --- a/src/service/customer.service.ts +++ b/src/service/customer.service.ts @@ -1,12 +1,13 @@ -import { Provide, Inject } from '@midwayjs/core'; +import { Inject, Provide } from '@midwayjs/core'; import { InjectEntityModel } from '@midwayjs/typeorm'; -import { Order } from '../entity/order.entity'; 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 { CustomerTag } from '../entity/customer_tag.entity'; +import { Order } from '../entity/order.entity'; import { SiteApiService } from './site-api.service'; -import { UnifiedCustomerDTO, UnifiedPaginationDTO, UnifiedSearchParamsDTO } from '../dto/site-api.dto'; -import { SyncOperationResult, BatchErrorItem } from '../dto/batch.dto'; +import { CreateCustomerDTO, CustomerDTO, CustomerStatisticDTO, CustomerStatisticQueryParamsDTO } from '../dto/customer.dto'; @Provide() export class CustomerService { @@ -33,10 +34,12 @@ export class CustomerService { * 将站点客户数据映射为本地客户实体数据 * 处理字段映射和数据转换,确保所有字段正确同步 */ - private mapSiteCustomerToCustomer(siteCustomer: UnifiedCustomerDTO, siteId: number): Partial { + private mapSiteCustomerToCustomer(siteCustomer: UnifiedCustomerDTO, siteId: number): Partial { return { 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, first_name: siteCustomer.first_name, last_name: siteCustomer.last_name, @@ -47,8 +50,6 @@ export class CustomerService { billing: siteCustomer.billing, shipping: siteCustomer.shipping, 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( customersData: Array> - ): Promise<{ - customers: Customer[]; - created: number; - updated: number; - processed: number; - errors: BatchErrorItem[]; - }> { + ): Promise { const results = { - customers: [], + total: customersData.length, + processed: 0, created: 0, updated: 0, - processed: 0, errors: [] }; @@ -139,7 +134,6 @@ export class CustomerService { for (const customerData of customersData) { try { const result = await this.upsertCustomer(customerData); - results.customers.push(result.customer); if (result.isCreated) { results.created++; @@ -153,6 +147,7 @@ export class CustomerService { identifier: customerData.email || String(customerData.id) || 'unknown', error: error.message }); + results.processed++; } } @@ -182,8 +177,8 @@ export class CustomerService { const upsertResult = await this.upsertManyCustomers(customersData); return { total: siteCustomers.length, - processed: upsertResult.customers.length, - synced: upsertResult.customers.length, + processed: upsertResult.processed, + synced: upsertResult.processed, updated: upsertResult.updated, created: upsertResult.created, errors: upsertResult.errors @@ -195,65 +190,76 @@ export class CustomerService { } } - async getCustomerStatisticList(param: Record) { + /** + * 获取客户统计列表(包含订单统计信息) + * 支持分页、搜索和排序功能 + * 使用原生SQL查询实现复杂的统计逻辑 + */ + async getCustomerStatisticList(param: CustomerStatisticQueryParamsDTO): Promise<{ + items: CustomerStatisticDTO[]; + total: number; + current: number; + pageSize: number; + }> { const { - current = 1, - pageSize = 10, - email, - tags, - sorterKey, - sorterValue, - state, - first_purchase_date, - customerId, - rate, + page = 1, + per_page = 10, + search, + where, + orderBy, } = param; + // 将page和per_page转换为current和pageSize + const current = page; + const pageSize = per_page; + const whereConds: string[] = []; const havingConds: string[] = []; - // 邮箱搜索 - if (email) { - whereConds.push(`o.customer_email LIKE '%${email}%'`); + // 全局搜索关键词 + if (search) { + whereConds.push(`o.customer_email LIKE '%${search}%'`); } - // 省份搜索 - if (state) { - whereConds.push( - `JSON_UNQUOTE(JSON_EXTRACT(o.billing, '$.state')) = '${state}'` - ); - } + // where条件过滤 + if (where) { + // 邮箱搜索 + if (where.email) { + whereConds.push(`o.customer_email LIKE '%${where.email}%'`); + } - // customerId 过滤 - if (customerId) { - whereConds.push(`c.id = ${Number(customerId)}`); - } - // rate 过滤 - if (rate) { - whereConds.push(`c.rate = ${Number(rate)}`); - } + // customerId 过滤 + if (where.customerId) { + whereConds.push(`c.id = ${Number(where.customerId)}`); + } - // tags 过滤 - if (tags) { - const tagList = tags - .split(',') - .map(tag => `'${tag.trim()}'`) - .join(','); - havingConds.push(` - EXISTS ( - SELECT 1 FROM customer_tag ct - WHERE ct.email = o.customer_email - AND ct.tag IN (${tagList}) - ) - `); - } + // rate 过滤 + if (where.rate) { + whereConds.push(`c.rate = ${Number(where.rate)}`); + } - // 首次购买时间过滤 - if (first_purchase_date) { - havingConds.push( - `DATE_FORMAT(MIN(o.date_paid), '%Y-%m') = '${first_purchase_date}'` - ); + // tags 过滤 + if (where.tags) { + const tagList = where.tags + .split(',') + .map(tag => `'${tag.trim()}'`) + .join(','); + havingConds.push(` + EXISTS ( + SELECT 1 FROM customer_tag ct + WHERE ct.email = o.customer_email + AND ct.tag IN (${tagList}) + ) + `); + } + + // 首次购买时间过滤 + if (where.first_purchase_date) { + havingConds.push( + `DATE_FORMAT(MIN(o.date_paid), '%Y-%m') = '${where.first_purchase_date}'` + ); + } } // 公用过滤 @@ -263,6 +269,22 @@ export class CustomerService { ${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 = ` SELECT @@ -296,9 +318,7 @@ export class CustomerService { GROUP BY oo.customer_email ) yoone_stats ON yoone_stats.customer_email = o.customer_email ${baseQuery} - ${sorterKey - ? `ORDER BY ${sorterKey} ${sorterValue === 'descend' ? 'DESC' : 'ASC'}` - : 'ORDER BY orders ASC, yoone_total DESC'} + ${orderByClause} LIMIT ${pageSize} OFFSET ${(current - 1) * pageSize} `; @@ -319,8 +339,22 @@ export class CustomerService { 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 { - items, + items: processedItems, total, current, pageSize, @@ -332,104 +366,56 @@ export class CustomerService { * 支持基本的分页、搜索和排序功能 * 使用TypeORM查询构建器实现 */ - async getCustomerList(param: Record): Promise>{ + async getCustomerList(params: UnifiedSearchParamsDTO): Promise>{ const { - current = 1, - pageSize = 10, - email, - firstName, - lastName, - phone, - state, - rate, - sorterKey, - sorterValue, - } = param; - - // 创建查询构建器 - const queryBuilder = this.customerModel - .createQueryBuilder('c') - .leftJoinAndSelect( - 'customer_tag', - 'ct', - '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; + page = 1, + per_page = 20, + where ={}, + } = params; + + // 查询客户列表和总数 + const [customers, total] = await this.customerModel.findAndCount({ + where, + // order: orderBy, + skip: (page - 1) * per_page, + take: per_page, }); + // 获取所有客户的邮箱列表 + const emailList = customers.map(customer => customer.email); + + // 查询所有客户的标签 + let customerTagsMap: Record = {}; + 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); + } + + // 将标签合并到客户数据中 + const customersWithTags = customers.map(customer => ({ + ...customer, + tags: customerTagsMap[customer.email] || [] + })); + return { - items: processedItems, + items: customersWithTags, total, - page: current, - per_page: pageSize, - totalPages: Math.ceil(total / pageSize), + page, + per_page, + totalPages: Math.ceil(total / per_page), }; } @@ -457,4 +443,84 @@ export class CustomerService { async setRate(params: { id: number; rate: number }) { return await this.customerModel.update(params.id, { rate: params.rate }); } + + /** + * 批量更新客户 + * 每个客户可以有独立的更新字段 + * 支持对多个客户进行统一化修改或分别更新 + */ + async batchUpdateCustomers( + updateItems: Array<{ id: number; update_data: Partial }> + ): Promise { + 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 { + 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; + } } \ No newline at end of file diff --git a/src/service/dict.service.ts b/src/service/dict.service.ts index 38e1944..2f34a7c 100644 --- a/src/service/dict.service.ts +++ b/src/service/dict.service.ts @@ -6,6 +6,19 @@ import { DictItem } from '../entity/dict_item.entity'; import { CreateDictDTO, UpdateDictDTO } from '../dto/dict.dto'; import { CreateDictItemDTO, UpdateDictItemDTO } from '../dto/dict.dto'; 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() export class DictService { @@ -37,17 +50,27 @@ export class DictService { } // 从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 wsname = wb.SheetNames[0]; // 获取第一个工作表 const ws = wb.Sheets[wsname]; - // 将工作表转换为JSON对象数组 - const data = xlsx.utils.sheet_to_json(ws, { header: ['name', 'title'] }).slice(1); + // 将工作表转换为JSON对象数组,xlsx会自动将第一行作为表头 + 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(); dict.name = this.formatName(row.name); dict.title = row.title; @@ -69,32 +92,71 @@ export class DictService { } // 从XLSX文件导入字典项 - async importDictItemsFromXLSX(buffer: Buffer, dictId: number) { + async importDictItemsFromXLSX(bufferOrPath: Buffer | string, dictId: number): Promise { + if(!dictId){ + throw new Error("引入失败, 请输入字典 ID") + } + const dict = await this.dictModel.findOneBy({ id: dictId }); if (!dict) { 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 wsname = wb.SheetNames[0]; const ws = wb.Sheets[wsname]; - // 支持titleCN字段的导入 - const data = xlsx.utils.sheet_to_json(ws, { header: ['name', 'title', 'titleCN', 'value', 'sort', 'image', 'shortName'] }).slice(1); + // 使用默认的header解析方式,xlsx会自动将第一行作为表头 + const data = xlsx.utils.sheet_to_json(ws) as ExcelRow[]; - const items = data.map((row: any) => { - const item = new DictItem(); - item.name = this.formatName(row.name); - item.title = row.title; - item.titleCN = row.titleCN; // 保存中文名称 - item.value = row.value; - item.image = row.image; - item.shortName = row.shortName; - item.sort = row.sort || 0; - item.dict = dict; - return item; - }); + // 使用 upsertDictItem 方法逐个处理,存在则更新,不存在则创建 + const createdItems = []; + const updatedItems = []; + const errors = []; + + for (const row of data) { + try { + const result = await this.upsertDictItem(dictId, { + name: row.name, + title: row.title, + 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); - return { success: true, count: items.length }; + const processed = createdItems.length + updatedItems.length; + + return { + total: data.length, + processed: processed, + updated: updatedItems.length, + created: createdItems.length, + errors: errors + }; } getDict(where: { name?: string; id?: number; }, relations: string[]) { if (!where.name && !where.id) { @@ -176,6 +238,58 @@ export class DictService { 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) { if (updateDictItemDTO.name) { @@ -202,4 +316,39 @@ export class DictService { // 返回该字典下的所有字典项 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' }); + } } diff --git a/src/service/logistics.service.ts b/src/service/logistics.service.ts index 067f4e5..ce4fc70 100644 --- a/src/service/logistics.service.ts +++ b/src/service/logistics.service.ts @@ -269,7 +269,7 @@ export class LogisticsService { this.orderModel.save(order); // todo 同步到wooccommerce删除运单信息 - await this.wpService.deleteShipment(site, order.externalOrderId, shipment.tracking_id); + await this.wpService.deleteFulfillment(site, order.externalOrderId, shipment.tracking_id); } catch (error) { console.log('同步到woocommerce失败', error); return true; @@ -363,7 +363,7 @@ export class LogisticsService { // 同步物流信息到woocommerce 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_provider: tracking_provider, }); @@ -492,7 +492,7 @@ export class LogisticsService { await this.wpService.updateOrder(site, order.externalOrderId, { status: OrderStatus.COMPLETED, }); - await this.wpService.createShipment(site, order.externalOrderId, { + await this.wpService.createFulfillment(site, order.externalOrderId, { tracking_number: shipment.primary_tracking_number, tracking_provider: shipment?.rate?.carrier_name, }); diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 5c51bc9..6569114 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -31,6 +31,8 @@ import { UpdateStockDTO } from '../dto/stock.dto'; import { StockService } from './stock.service'; import { OrderItemOriginal } from '../entity/order_item_original.entity'; import { SiteApiService } from './site-api.service'; +import { SyncOperationResult } from '../dto/api.dto'; + import * as fs from 'fs'; import * as path from 'path'; import * as os from 'os'; @@ -98,55 +100,98 @@ export class OrderService { @Inject() siteApiService: SiteApiService; - async syncOrders(siteId: number, params: Record = {}) { - 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(); + async syncOrders(siteId: number, params: Record = {}): Promise { // 调用 WooCommerce API 获取订单 - const result = await (await this.siteApiService.getAdapter(siteId)).getAllOrders({ - ...params, - after, - before, - }); + const result = await (await this.siteApiService.getAdapter(siteId)).getAllOrders(params); - let successCount = 0; - let failureCount = 0; + const syncResult: SyncOperationResult = { + total: result.length, + processed: 0, + synced: 0, + created: 0, + updated: 0, + errors: [] + }; + + // 遍历每个订单进行同步 for (const order of result) { 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); - successCount++; + + // 统计结果 + syncResult.processed++; + syncResult.synced++; + + if (existingOrder) { + syncResult.updated++; + } else { + syncResult.created++; + } } 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}`, - }; + + return syncResult; } - async syncOrderById(siteId: number, orderId: string) { + async syncOrderById(siteId: number, orderId: string): Promise { + const syncResult: SyncOperationResult = { + total: 1, + processed: 0, + synced: 0, + created: 0, + updated: 0, + errors: [] + }; + try { // 调用 WooCommerce API 获取订单 - //const order = await this.wpService.getOrder(siteId, orderId); - const order = await (await this.siteApiService.getAdapter(siteId)).getOrder( - orderId, - ); + const order = await this.wpService.getOrder(siteId, 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); - return { success: true, message: '同步成功' }; + + // 统计结果 + syncResult.processed = 1; + syncResult.synced = 1; + + if (existingOrder) { + syncResult.updated = 1; + } else { + syncResult.created = 1; + } + + return syncResult; } 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 } - // 由于 wordpress 订单状态和 我们的订单状态 不一致,需要做转换 async autoUpdateOrderStatus(siteId: number, order: any) { console.log('更新订单状态', order) @@ -184,7 +228,7 @@ export class OrderService { refunds, ...orderData } = order; - console.log('同步进单个订单', order) + // console.log('同步进单个订单', order) const existingOrder = await this.orderModel.findOne({ where: { externalOrderId: order.id, siteId: siteId }, }); @@ -224,7 +268,6 @@ export class OrderService { externalOrderId, orderItems: line_items, }); - console.log('同步进单个订单1') await this.saveOrderRefunds({ siteId, orderId, @@ -303,7 +346,8 @@ export class OrderService { if (!customer) { await this.customerModel.save({ email: order.customer_email, - rate: 0, + site_id: siteId, + origin_id: order.customer_id, }); } return await this.orderModel.save(entity); @@ -437,8 +481,9 @@ export class OrderService { await this.orderSaleModel.delete(currentOrderSale.map(v => v.id)); } if (!orderItem.sku) return; + // 从数据库查询产品,关联查询组件 const product = await this.productModel.findOne({ - where: { sku: orderItem.sku }, + where: { siteSkus: Like(`%${orderItem.sku}%`) }, relations: ['components'], }); @@ -1715,7 +1760,7 @@ export class OrderService { // } } - + // TODO async exportOrder(ids: number[]) { // 日期 订单号 姓名地址 邮箱 号码 订单内容 盒数 换盒数 换货内容 快递号 interface ExportData { diff --git a/src/service/product.service.ts b/src/service/product.service.ts index 1ea6e34..6a8d7dd 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -1,35 +1,40 @@ import { Inject, Provide } from '@midwayjs/core'; +import { parse } from 'csv-parse'; import * as fs from 'fs'; import { In, Like, Not, Repository } from 'typeorm'; import { Product } from '../entity/product.entity'; -import { paginate } from '../utils/paginate.util'; 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 { - CreateProductDTO, - UpdateProductDTO, BatchUpdateProductDTO, + CreateProductDTO, + ProductWhereFilter, + UpdateProductDTO } from '../dto/product.dto'; import { BrandPaginatedResponse, FlavorsPaginatedResponse, ProductPaginatedResponse, - StrengthPaginatedResponse, SizePaginatedResponse, + StrengthPaginatedResponse, } from '../dto/reponse.dto'; -import { InjectEntityModel } from '@midwayjs/typeorm'; import { Dict } from '../entity/dict.entity'; import { DictItem } from '../entity/dict_item.entity'; -import { Context } from '@midwayjs/koa'; -import { TemplateService } from './template.service'; -import { StockService } from './stock.service'; +import { ProductStockComponent } from '../entity/product_stock_component.entity'; import { Stock } from '../entity/stock.entity'; import { StockPoint } from '../entity/stock_point.entity'; -import { ProductStockComponent } from '../entity/product_stock_component.entity'; -import { ProductSiteSku } from '../entity/product_site_sku.entity'; +import { StockService } from './stock.service'; +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 { CategoryAttribute } from '../entity/category_attribute.entity'; +import { SiteApiService } from './site-api.service'; @Provide() export class ProductService { @@ -60,12 +65,12 @@ export class ProductService { @InjectEntityModel(ProductStockComponent) productStockComponentModel: Repository; - @InjectEntityModel(ProductSiteSku) - productSiteSkuModel: Repository; - @InjectEntityModel(Category) categoryModel: Repository; + @Inject() + siteApiService: SiteApiService; + // 获取所有分类 async getCategoriesAll(): Promise { return this.categoryModel.find({ @@ -179,8 +184,7 @@ export class ProductService { async findProductsByName(name: string): Promise { const nameFilter = name ? name.split(' ').filter(Boolean) : []; const query = this.productModel.createQueryBuilder('product') - .leftJoinAndSelect('product.category', 'category') - .leftJoinAndSelect('product.siteSkus', 'siteSku'); + .leftJoinAndSelect('product.category', 'category'); // 保证 sku 不为空 query.where('product.sku IS NOT NULL'); @@ -205,8 +209,11 @@ export class ProductService { conditions.push(`product.nameCn LIKE :nameCn`); } - // 英文名关键词匹配 OR 中文名匹配 - query.andWhere(`(${conditions.join(' OR ')})`, params); + // 只有当 conditions 不为空时才添加 AND 条件 + if (conditions.length > 0) { + // 英文名关键词匹配 OR 中文名匹配 + query.andWhere(`(${conditions.join(' OR ')})`, params); + } } query.take(50); @@ -223,19 +230,30 @@ export class ProductService { }); } - async getProductList( - pagination: PaginationParams, - name?: string, - brandId?: number, - sortField?: string, - sortOrder?: string - ): Promise { + async getProductList(query: UnifiedSearchParamsDTO): Promise { const qb = this.productModel .createQueryBuilder('product') .leftJoinAndSelect('product.attributes', 'attribute') .leftJoinAndSelect('attribute.dict', 'dict') - .leftJoinAndSelect('product.category', 'category') - .leftJoinAndSelect('product.siteSkus', 'siteSku'); + .leftJoinAndSelect('product.category', 'category'); + + // 处理分页参数(支持新旧两种格式) + 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,支持多个关键词 const nameFilter = name ? name.split(' ').filter(Boolean) : []; @@ -250,7 +268,134 @@ export class ProductService { 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) { qb.andWhere(qb => { const subQuery = qb @@ -264,22 +409,75 @@ export class ProductService { return 'product.id IN ' + subQuery; }); } + + // 处理品牌ID列表过滤 + if (brandIds && brandIds.length > 0) { + qb.andWhere(qb => { + const subQuery = qb + .subQuery() + .select('product_attributes_dict_item.productId') + .from('product_attributes_dict_item', 'product_attributes_dict_item') + .where('product_attributes_dict_item.dictItemId IN (:...brandIds)', { + brandIds, + }) + .getQuery(); + return 'product.id IN ' + subQuery; + }); + } - // 排序 - if (sortField && sortOrder) { - const order = sortOrder === 'ascend' ? 'ASC' : 'DESC'; - const allowedSortFields = ['price', 'promotionPrice', 'createdAt', 'updatedAt', 'sku', 'name']; - if (allowedSortFields.includes(sortField)) { - qb.orderBy(`product.${sortField}`, order); + // 分类过滤(向后兼容) + 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']; + if (allowedSortFields.includes(key)) { + qb.addOrderBy(`product.${key}`, order as 'ASC' | 'DESC'); + } + }); + } catch (e) { + // 解析失败,使用默认排序 + qb.orderBy('product.createdAt', 'DESC'); + } + } else if (typeof orderBy === 'object') { + // 如果orderBy是对象,直接使用 + Object.keys(orderBy).forEach(key => { + const order = orderBy[key].toUpperCase(); + const allowedSortFields = ['price', 'promotionPrice', 'createdAt', 'updatedAt', 'sku', 'name']; + if (allowedSortFields.includes(key)) { + qb.addOrderBy(`product.${key}`, order as 'ASC' | 'DESC'); + } + }); } } else { qb.orderBy('product.createdAt', 'DESC'); } // 分页 - qb.skip((pagination.current - 1) * pagination.pageSize).take( - pagination.pageSize - ); + qb.skip((page - 1) * pageSize).take(pageSize); const [items, total] = await qb.getManyAndCount(); @@ -303,7 +501,8 @@ export class ProductService { return { items, total, - ...pagination, + current: page, + pageSize, }; } @@ -438,20 +637,16 @@ export class ProductService { if (sku) { product.sku = sku; } 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); - // 保存站点 SKU 列表 - if (createProductDTO.siteSkus && createProductDTO.siteSkus.length > 0) { - const siteSkus = createProductDTO.siteSkus.map(code => { - const s = new ProductSiteSku(); - s.siteSku = code; - s.product = savedProduct; - return s; - }); - await this.productSiteSkuModel.save(siteSkus); + // 设置站点 SKU 列表(确保始终设置,即使为空数组) + if (createProductDTO.siteSkus !== undefined) { + product.siteSkus = createProductDTO.siteSkus; + // 重新保存产品以更新 siteSkus + await this.productModel.save(product); } // 保存组件信息 @@ -492,19 +687,7 @@ export class ProductService { // 处理站点 SKU 更新 if (updateProductDTO.siteSkus !== undefined) { - // 删除旧的 siteSkus - await this.productSiteSkuModel.delete({ productId: id }); - - // 如果有新的 siteSkus,则保存 - if (updateProductDTO.siteSkus.length > 0) { - const siteSkus = updateProductDTO.siteSkus.map(code => { - const s = new ProductSiteSku(); - s.siteSku = code; - s.productId = id; - return s; - }); - await this.productSiteSkuModel.save(siteSkus); - } + product.siteSkus = updateProductDTO.siteSkus; } // 处理 SKU 更新 @@ -771,40 +954,6 @@ export class ProductService { return await this.getProductComponents(productId); } - // 站点SKU绑定:覆盖式绑定一组站点SKU到产品 - async bindSiteSkus(productId: number, codes: string[]): Promise { - 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 { - 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 已合并到前面的实现(移除重复) @@ -1404,7 +1553,7 @@ export class ProductService { // 基础数据 const rowData = [ esc(p.sku), - esc(p.siteSkus ? p.siteSkus.map(s => s.siteSku).join(',') : ''), + esc(p.siteSkus ? p.siteSkus.join(',') : ''), esc(p.name), esc(p.nameCn), esc(p.price), @@ -1611,20 +1760,12 @@ export class ProductService { return { added, errors }; } - // 获取产品的站点SKU列表 - async getProductSiteSkus(productId: number): Promise { - return this.productSiteSkuModel.find({ - where: { productId }, - relations: ['product'], - order: { createdAt: 'ASC' } - }); - } // 根据ID获取产品详情(包含站点SKU) async getProductById(id: number): Promise { const product = await this.productModel.findOne({ where: { id }, - relations: ['category', 'attributes', 'attributes.dict', 'siteSkus', 'components'] + relations: ['category', 'attributes', 'attributes.dict', 'components'] }); if (!product) { @@ -1651,16 +1792,281 @@ export class ProductService { // 根据站点SKU查询产品 async findProductBySiteSku(siteSku: string): Promise { - const siteSkuEntity = await this.productSiteSkuModel.findOne({ - where: { siteSku }, - relations: ['product'] + const product = await this.productModel.findOne({ + where: { siteSkus: Like(`%${siteSku}%`) }, + relations: ['category', 'attributes', 'attributes.dict', 'components'] }); - if (!siteSkuEntity) { + if (!product) { throw new Error(`站点SKU ${siteSku} 不存在`); } // 获取完整的产品信息,包含所有关联数据 - return this.getProductById(siteSkuEntity.product.id); + return this.getProductById(product.id); + } + + // 获取产品的站点SKU列表 + async getProductSiteSkus(productId: number): Promise { + 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 { + 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 { + // 获取本地产品信息 + 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 { + 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 { + // 从站点获取产品信息 + 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 { + 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 { + try { + return await this.categoryModel.findOne({ + where: { name } + }); + } catch (error) { + return null; + } + } + + /** + * 将本地产品转换为统一产品格式 + * @param localProduct 本地产品对象 + * @returns 统一产品对象 + */ + private async convertLocalProductToUnifiedProduct(localProduct: Product,siteSku?: string): Promise> { + // 将本地产品数据转换为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; } } diff --git a/src/service/shopyy.service.ts b/src/service/shopyy.service.ts index 926cd64..f4bb39e 100644 --- a/src/service/shopyy.service.ts +++ b/src/service/shopyy.service.ts @@ -7,7 +7,7 @@ import { Site } from '../entity/site.entity'; import { UnifiedReviewDTO } from '../dto/site-api.dto'; import { ShopyyReview } from '../dto/shopyy.dto'; import { BatchOperationDTO, BatchOperationResultDTO } from '../dto/batch.dto'; -import { UnifiedSearchParamsDTO } from '../dto/site-api.dto'; +import { UnifiedSearchParamsDTO } from '../dto/api.dto'; /** * ShopYY平台服务实现 */ @@ -323,7 +323,7 @@ export class ShopyyService { }; } - async getAllOrders(site: any | number, params: Record = {}, maxPages: number = 100, concurrencyLimit: number = 100): Promise { + async getAllOrders(site: any | number, params: Record = {}, maxPages: number = 10, concurrencyLimit: number = 100): Promise { const firstPage = await this.getOrders(site, 1, 100); const { items: firstPageItems, totalPages} = firstPage; @@ -482,40 +482,102 @@ export class ShopyyService { } /** - * 创建ShopYY物流信息 + * 创建ShopYY履约信息 * @param site 站点配置 * @param orderId 订单ID - * @param data 物流数据 + * @param data 履约数据 * @returns 创建结果 */ - async createShipment(site: any, orderId: string, data: any): Promise { + async createFulfillment(site: Site, orderId: string, data: any): Promise { // ShopYY API: POST /orders/{id}/shipments - const shipmentData = { + const fulfillmentData = { tracking_number: data.tracking_number, carrier_code: data.carrier_code, carrier_name: data.carrier_name, 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; } /** - * 删除ShopYY物流信息 + * 删除ShopYY履约信息 * @param site 站点配置 * @param orderId 订单ID - * @param trackingId 物流跟踪ID + * @param fulfillmentId 履约跟踪ID * @returns 删除结果 */ - async deleteShipment(site: any, orderId: string, trackingId: string): Promise { + async deleteFulfillment(site: any, orderId: string, fulfillmentId: string): Promise { try { - // ShopYY API: DELETE /orders/{order_id}/shipments/{tracking_id} - await this.request(site, `orders/${orderId}/shipments/${trackingId}`, 'DELETE'); + // ShopYY API: DELETE /orders/{order_id}/shipments/{fulfillment_id} + await this.request(site, `orders/${orderId}/fulfillments/${fulfillmentId}`, 'DELETE'); return true; } catch (error) { - const errorMessage = error.response?.data?.msg || error.message || '删除ShopYY物流信息失败'; - throw new Error(`删除ShopYY物流信息失败: ${errorMessage}`); + const errorMessage = error.response?.data?.msg || error.message || '删除ShopYY履约信息失败'; + throw new Error(`删除ShopYY履约信息失败: ${errorMessage}`); + } + } + + /** + * 获取ShopYY订单履约跟踪信息 + * @param site 站点配置 + * @param orderId 订单ID + * @returns 履约跟踪信息列表 + */ + async getFulfillments(site: any, orderId: string): Promise { + 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 { + 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 { + 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 { + 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 { + 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}`); + } + } + } diff --git a/src/service/site-api.service.ts b/src/service/site-api.service.ts index 4c74849..c57a899 100644 --- a/src/service/site-api.service.ts +++ b/src/service/site-api.service.ts @@ -6,6 +6,7 @@ import { ShopyyService } from './shopyy.service'; import { SiteService } from './site.service'; import { WPService } from './wp.service'; import { ProductService } from './product.service'; +import { UnifiedProductDTO } from '../dto/site-api.dto'; @Provide() export class SiteApiService { @@ -98,4 +99,115 @@ export class SiteApiService { return enrichedProducts; } + + /** + * 更新或创建产品 + * @param siteId 站点ID + * @param product 产品数据 + * @returns 更新或创建后的产品 + */ + async upsertProduct(siteId: number, product: Partial): Promise { + 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[]): 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 { + 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 { + const adapter = await this.getAdapter(siteId); + return await adapter.getProduct(productId); + } + + /** + * 从站点获取所有产品 + * @param siteId 站点ID + * @param params 查询参数 + * @returns 站点产品列表 + */ + async getAllProductsFromSite(siteId: number, params?: any): Promise { + const adapter = await this.getAdapter(siteId); + return await adapter.getAllProducts(params); + } } diff --git a/src/service/statistics.service.ts b/src/service/statistics.service.ts index 688ed3d..4352633 100644 --- a/src/service/statistics.service.ts +++ b/src/service/statistics.service.ts @@ -15,11 +15,11 @@ export class StatisticsService { orderItemRepository: Repository; async getOrderStatistics(params: OrderStatisticsParams) { - const { startDate, endDate, siteId ,grouping} = params; + const { startDate, endDate, grouping, siteId } = params; // const keywords = keyword ? keyword.split(' ').filter(Boolean) : []; const start = dayjs(startDate).format('YYYY-MM-DD'); const end = dayjs(endDate).add(1, 'd').format('YYYY-MM-DD'); - let sql = ''; + let sql if (!grouping || grouping === 'day') { sql = ` WITH first_order AS ( @@ -53,8 +53,8 @@ export class StatisticsService { WHERE o.date_paid IS NOT NULL AND o.date_paid >= '${start}' AND o.date_paid < '${end}' AND o.status IN('processing','completed') `; - if (siteId) sql += ` AND o.siteId=${siteId}`; - sql += ` + if (siteId) sql += ` AND o.siteId=${siteId}`; + sql += ` GROUP BY o.id, o.date_paid, o.customer_email, o.total, o.source_type, o.siteId, o.utm_source ), order_sales_summary AS ( @@ -216,8 +216,8 @@ export class StatisticsService { dt.can_total_orders ORDER BY d.order_date DESC; `; - }else if (grouping === 'week') { - sql = `WITH first_order AS ( + } else if (grouping === 'week') { + sql = `WITH first_order AS ( SELECT customer_email, MIN(date_paid) AS first_purchase_date FROM \`order\` GROUP BY customer_email @@ -409,8 +409,8 @@ export class StatisticsService { wt.can_total_orders ORDER BY wt.order_date DESC; `; - }else if (grouping === 'month') { - sql = `WITH first_order AS ( + } else if (grouping === 'month') { + sql = `WITH first_order AS ( SELECT customer_email, MIN(date_paid) AS first_purchase_date FROM \`order\` GROUP BY customer_email @@ -601,8 +601,9 @@ export class StatisticsService { mt.togo_total_orders, mt.can_total_orders ORDER BY mt.order_date DESC; - `;} - + `; + } + return this.orderRepository.query(sql); } // async getOrderStatistics(params: OrderStatisticsParams) { @@ -781,8 +782,8 @@ export class StatisticsService { async getCustomerOrders(month) { const timeWhere = month ? `AND o.date_paid BETWEEN '${dayjs(month[0]) - .startOf('month') - .format('YYYY-MM-DD HH:mm:ss')}' AND '${dayjs(month[1]) + .startOf('month') + .format('YYYY-MM-DD HH:mm:ss')}' AND '${dayjs(month[1]) .endOf('month') .format('YYYY-MM-DD HH:mm:ss')}'` : ''; @@ -1312,7 +1313,7 @@ export class StatisticsService { }; } - async getOrderSorce(params){ + async getOrderSorce(params) { const sql = ` WITH cutoff_months AS ( SELECT @@ -1467,14 +1468,14 @@ export class StatisticsService { ORDER BY m.order_month DESC; ` - - const [res, inactiveRes ] = await Promise.all([ - this.orderRepository.query(sql), - this.orderRepository.query(inactiveSql), + + const [res, inactiveRes] = await Promise.all([ + this.orderRepository.query(sql), + this.orderRepository.query(inactiveSql), ]) return { - res,inactiveRes + res, inactiveRes } } diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index 9eff41c..24b4083 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -509,7 +509,7 @@ export class WPService implements IPlatformService { productId: string, variationId: string, data: Partial - ): Promise { + ): Promise { const { regular_price, sale_price, ...params } = data; const api = this.createApi(site, 'wc/v3'); const updateData: any = { ...params }; @@ -521,14 +521,65 @@ export class WPService implements IPlatformService { } try { - await api.put(`products/${productId}/variations/${variationId}`, updateData); - return true; + const res = await api.put(`products/${productId}/variations/${variationId}`, updateData); + 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 data 创建变体的数据 + */ + async createVariation( + site: any, + productId: string, + data: Partial + ): Promise { + 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 { + 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 */ @@ -547,7 +598,7 @@ export class WPService implements IPlatformService { } } - async createShipment( + async createFulfillment( site: any, orderId: string, data: Record @@ -575,10 +626,10 @@ export class WPService implements IPlatformService { return await axios.request(config); } - async deleteShipment( + async deleteFulfillment( site: any, orderId: string, - trackingId: string, + fulfillmentId: string, ): Promise { const apiUrl = site.apiUrl; const { consumerKey, consumerSecret } = site; @@ -586,7 +637,7 @@ export class WPService implements IPlatformService { 'base64' ); - console.log('del', orderId, trackingId); + console.log('del', orderId, fulfillmentId); // 删除接口: DELETE /wp-json/wc-shipment-tracking/v3/orders//shipment-trackings/ const config: AxiosRequestConfig = { method: 'DELETE', @@ -597,7 +648,7 @@ export class WPService implements IPlatformService { 'wc-ast/v3/orders', orderId, 'shipment-trackings', - trackingId + fulfillmentId ), headers: { 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 { + 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 { + 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) * @param site 站点配置 diff --git a/src/utils/response.util.ts b/src/utils/response.util.ts index 3959616..0987202 100644 --- a/src/utils/response.util.ts +++ b/src/utils/response.util.ts @@ -1,11 +1,17 @@ +import { ApiProperty } from "@midwayjs/swagger"; + // 通用响应结构 export class ApiResponse { + @ApiProperty({ description: '状态码' }) code: number; + @ApiProperty({ description: '是否成功' }) success: boolean; + @ApiProperty({ description: '提示信息' }) message: string; + @ApiProperty({ description: '返回数据' }) data: T; } From 28fb8e4ce62c6f66d71637fbe932a11b9b3b9f0b Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 31 Dec 2025 14:33:53 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=E5=B0=86origin=5Fid=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E7=BB=9F=E4=B8=80=E8=BD=AC=E6=8D=A2=E4=B8=BA=E5=AD=97?= =?UTF-8?q?=E7=AC=A6=E4=B8=B2=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复订单和客户服务中origin_id字段类型不一致的问题,确保所有相关操作中origin_id都作为字符串处理 --- src/adapter/shopyy.adapter.ts | 8 +++++--- src/controller/customer.controller.ts | 4 ++-- src/dto/api.dto.ts | 8 +++++++- src/dto/customer.dto.ts | 4 ++-- src/service/customer.service.ts | 5 ++++- src/service/order.service.ts | 2 +- 6 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index 178dfc7..cafc345 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -14,7 +14,8 @@ import { UnifiedWebhookPaginationDTO, CreateWebhookDTO, UpdateWebhookDTO, - UnifiedAddressDTO + UnifiedAddressDTO, + UnifiedShippingLineDTO } from '../dto/site-api.dto'; import { UnifiedPaginationDTO, UnifiedSearchParamsDTO, } from '../dto/api.dto'; import { @@ -120,6 +121,7 @@ export class ShopyyAdapter implements ISiteAdapter { // 映射变体 return { id: variant.id, + name: variant.sku || '', sku: variant.sku || '', regular_price: String(variant.price ?? ''), sale_price: String(variant.special_price ?? ''), @@ -130,7 +132,7 @@ export class ShopyyAdapter implements ISiteAdapter { }; } - shopyyOrderAutoNextStatusMap = {//订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消; + shopyyOrderAutoNextStatusMap = {//订单状态 100 未完成;110 待处理;180 已完成(确认收货); 190 取消; [100]: OrderStatus.PENDING, // 100 未完成 转为 pending [110]: OrderStatus.PROCESSING, // 110 待处理 转为 processing [180]: OrderStatus.COMPLETED, // 180 已完成(确认收货) 转为 completed @@ -192,7 +194,7 @@ export class ShopyyAdapter implements ISiteAdapter { id: item.fulfillments?.[0]?.id || 0, method_title: item.payment_method || '', method_id: item.payment_method || '', - total: item.current_shipping_price.toExponential(2) || '0.00', + total: Number(item.current_shipping_price).toExponential(2) || '0.00', total_tax: '0.00', taxes: [], meta_data: [], diff --git a/src/controller/customer.controller.ts b/src/controller/customer.controller.ts index 94a19d2..6c37da7 100644 --- a/src/controller/customer.controller.ts +++ b/src/controller/customer.controller.ts @@ -116,7 +116,7 @@ export class CustomerController { async createCustomer(@Body() body: CreateCustomerDTO) { try { // 调用service层的upsertCustomer方法 - const result = await this.customerService.upsertCustomer(body); + const result = await this.customerService.upsertCustomer({...body, origin_id: String(body.origin_id)}); return successResponse(result.customer); } catch (error) { return errorResponse(error.message); @@ -190,7 +190,7 @@ export class CustomerController { @Post('/batch') async batchCreateCustomers(@Body() body: BatchCreateCustomerDTO) { try { - const result = await this.customerService.upsertManyCustomers(body.customers); + const result = await this.customerService.upsertManyCustomers(body.customers.map(c => ({ ...c, origin_id: String(c.origin_id) }))); return successResponse(result); } catch (error) { return errorResponse(error.message); diff --git a/src/dto/api.dto.ts b/src/dto/api.dto.ts index ccda845..6af71f4 100644 --- a/src/dto/api.dto.ts +++ b/src/dto/api.dto.ts @@ -27,7 +27,13 @@ export class UnifiedSearchParamsDTO> { @ApiProperty({ description: '每页数量', example: 20, required: false }) per_page?: number; - + + @ApiProperty({ description: '查询时间范围开始', example: '2023-01-01T00:00:00Z', required: false }) + after?: string; + + @ApiProperty({ description: '查询时间范围结束', example: '2023-01-01T23:59:59Z', required: false }) + before?: string; + @ApiProperty({ description: '搜索关键词', required: false }) search?: string; diff --git a/src/dto/customer.dto.ts b/src/dto/customer.dto.ts index 353c77a..d7ed2e4 100644 --- a/src/dto/customer.dto.ts +++ b/src/dto/customer.dto.ts @@ -11,7 +11,7 @@ export class CustomerDTO extends Customer{ site_id: number; @ApiProperty({ description: '原始ID', required: false }) - origin_id: number; + origin_id: string; @ApiProperty({ description: '站点创建时间', required: false }) site_created_at: Date; @@ -124,7 +124,7 @@ export class UpdateCustomerDTO { site_id?: number; @ApiProperty({ description: '原始ID', required: false }) - origin_id?: number; + origin_id?: string; @ApiProperty({ description: '邮箱', required: false }) email?: string; diff --git a/src/service/customer.service.ts b/src/service/customer.service.ts index 48f378a..56b3b01 100644 --- a/src/service/customer.service.ts +++ b/src/service/customer.service.ts @@ -171,7 +171,10 @@ export class CustomerService { // 第二步:将站点客户数据转换为客户实体数据 const customersData = siteCustomers.map(siteCustomer => { return this.mapSiteCustomerToCustomer(siteCustomer, siteId); - }); + }).map(customer => ({ + ...customer, + origin_id: String(customer.origin_id), + })); // 第三步:批量upsert客户数据 const upsertResult = await this.upsertManyCustomers(customersData); diff --git a/src/service/order.service.ts b/src/service/order.service.ts index 6569114..d822c20 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -347,7 +347,7 @@ export class OrderService { await this.customerModel.save({ email: order.customer_email, site_id: siteId, - origin_id: order.customer_id, + origin_id: String(order.customer_id), }); } return await this.orderModel.save(entity); From 58ae594d5ef0f9339d0fb7ee7e5d1077942df058 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Wed, 31 Dec 2025 15:05:50 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat(=E5=AE=9E=E4=BD=93):=20=E5=9C=A8?= =?UTF-8?q?=E5=AD=97=E5=85=B8=E9=A1=B9=E5=AE=9E=E4=BD=93=E4=B8=AD=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E6=8F=8F=E8=BF=B0=E5=AD=97=E6=AE=B5=E5=B9=B6=E8=B0=83?= =?UTF-8?q?=E6=95=B4=E5=AD=97=E6=AE=B5=E9=A1=BA=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加 description 字段以支持字典项描述信息 将 shortName 字段调整至与其他字段更合理的顺序 --- src/entity/dict_item.entity.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/entity/dict_item.entity.ts b/src/entity/dict_item.entity.ts index ffa6896..544c625 100644 --- a/src/entity/dict_item.entity.ts +++ b/src/entity/dict_item.entity.ts @@ -34,16 +34,19 @@ export class DictItem { @Column({ comment: '字典唯一标识名称' }) name: string; + @Column({ nullable: true, comment: '简称' }) + shortName: string; + + @Column({ nullable: true, comment: '字典项描述' }) + description?: string + // 字典项值 @Column({ nullable: true, comment: '字典项值' }) value?: string; @Column({ nullable: true, comment: '图片' }) image: string; - - @Column({ nullable: true, comment: '简称' }) - shortName: string; - + // 排序 @Column({ default: 0, comment: '排序' }) sort: number; From 338625c3d202773626ad5a577ff496fe6bc3324a Mon Sep 17 00:00:00 2001 From: tikkhun Date: Sun, 4 Jan 2026 20:05:37 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix(product):=20=E4=BF=AE=E5=A4=8D=E4=BA=A7?= =?UTF-8?q?=E5=93=81=E5=88=9B=E5=BB=BA=E5=92=8C=E6=9B=B4=E6=96=B0=E6=97=B6?= =?UTF-8?q?=E7=9A=84=E5=B1=9E=E6=80=A7=E6=A0=A1=E9=AA=8C=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 调整产品DTO中attributes字段的校验规则,使其在type为'single'时必填,为'bundle'时可选 移除不必要的siteSkus处理逻辑,简化产品创建和更新流程 --- src/dto/product.dto.ts | 14 ++++++-- src/dto/site.dto.ts | 47 +++++++++++++++++++++++++- src/service/order.service.ts | 4 +-- src/service/product.service.ts | 60 ++++++++++++++-------------------- src/service/site.service.ts | 6 ++-- 5 files changed, 87 insertions(+), 44 deletions(-) diff --git a/src/dto/product.dto.ts b/src/dto/product.dto.ts index a24e7e0..1feccfe 100644 --- a/src/dto/product.dto.ts +++ b/src/dto/product.dto.ts @@ -64,9 +64,17 @@ export class CreateProductDTO { siteSkus?: string[]; // 通用属性输入(通过 attributes 统一提交品牌/口味/强度/尺寸/干湿等) - @ApiProperty({ description: '属性列表', type: 'array' }) - @Rule(RuleType.array().required()) - attributes: AttributeInputDTO[]; + // 当 type 为 'single' 时必填,当 type 为 'bundle' 时可选 + @ApiProperty({ description: '属性列表', type: 'array', required: false }) + @Rule( + RuleType.array() + .when('type', { + is: 'single', + then: RuleType.array().required(), + otherwise: RuleType.array().optional() + }) + ) + attributes?: AttributeInputDTO[]; // 商品价格 @ApiProperty({ description: '价格', example: 99.99, required: false }) diff --git a/src/dto/site.dto.ts b/src/dto/site.dto.ts index b45a3e3..27d8bde 100644 --- a/src/dto/site.dto.ts +++ b/src/dto/site.dto.ts @@ -36,22 +36,39 @@ export class SiteConfig { } export class CreateSiteDTO { + @ApiProperty({ description: '站点 API URL', required: false }) @Rule(RuleType.string().optional()) apiUrl?: string; + + @ApiProperty({ description: '站点网站 URL', required: false }) @Rule(RuleType.string().optional()) websiteUrl?: string; + + @ApiProperty({ description: '站点 REST Key', required: false }) @Rule(RuleType.string().optional()) consumerKey?: string; + + @ApiProperty({ description: '站点 REST 秘钥', required: false }) @Rule(RuleType.string().optional()) consumerSecret?: string; + + @ApiProperty({ description: '访问令牌', required: false }) @Rule(RuleType.string().optional()) token?: string; + + @ApiProperty({ description: '站点名称' }) @Rule(RuleType.string()) name: string; + + @ApiProperty({ description: '站点描述', required: false }) @Rule(RuleType.string().allow('').optional()) description?: string; + + @ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'], required: false }) @Rule(RuleType.string().valid('woocommerce', 'shopyy').optional()) type?: string; + + @ApiProperty({ description: 'SKU 前缀', required: false }) @Rule(RuleType.string().optional()) skuPrefix?: string; @@ -67,22 +84,39 @@ export class CreateSiteDTO { } export class UpdateSiteDTO { + @ApiProperty({ description: '站点 API URL', required: false }) @Rule(RuleType.string().optional()) apiUrl?: string; + + @ApiProperty({ description: '站点 REST Key', required: false }) @Rule(RuleType.string().optional()) consumerKey?: string; + + @ApiProperty({ description: '站点 REST 秘钥', required: false }) @Rule(RuleType.string().optional()) consumerSecret?: string; + + @ApiProperty({ description: '访问令牌', required: false }) @Rule(RuleType.string().optional()) token?: string; + + @ApiProperty({ description: '站点名称', required: false }) @Rule(RuleType.string().optional()) name?: string; + + @ApiProperty({ description: '站点描述', required: false }) @Rule(RuleType.string().allow('').optional()) description?: string; + + @ApiProperty({ description: '是否禁用', required: false }) @Rule(RuleType.boolean().optional()) isDisabled?: boolean; + + @ApiProperty({ description: '平台类型', enum: ['woocommerce', 'shopyy'], required: false }) @Rule(RuleType.string().valid('woocommerce', 'shopyy').optional()) type?: string; + + @ApiProperty({ description: 'SKU 前缀', required: false }) @Rule(RuleType.string().optional()) skuPrefix?: string; @@ -95,25 +129,36 @@ export class UpdateSiteDTO { @ApiProperty({ description: '绑定仓库ID列表' }) @Rule(RuleType.array().items(RuleType.number()).optional()) stockPointIds?: number[]; - @ApiProperty({ description: '站点网站URL' }) + + @ApiProperty({ description: '站点网站URL', required: false }) @Rule(RuleType.string().optional()) websiteUrl?: string; } export class QuerySiteDTO { + @ApiProperty({ description: '当前页码', required: false }) @Rule(RuleType.number().optional()) current?: number; + + @ApiProperty({ description: '每页数量', required: false }) @Rule(RuleType.number().optional()) pageSize?: number; + + @ApiProperty({ description: '搜索关键词', required: false }) @Rule(RuleType.string().optional()) keyword?: string; + + @ApiProperty({ description: '是否禁用', required: false }) @Rule(RuleType.boolean().optional()) isDisabled?: boolean; + + @ApiProperty({ description: '站点ID列表(逗号分隔)', required: false }) @Rule(RuleType.string().optional()) ids?: string; } export class DisableSiteDTO { + @ApiProperty({ description: '是否禁用' }) @Rule(RuleType.boolean()) disabled: boolean; } diff --git a/src/service/order.service.ts b/src/service/order.service.ts index d822c20..775b341 100644 --- a/src/service/order.service.ts +++ b/src/service/order.service.ts @@ -102,7 +102,7 @@ export class OrderService { async syncOrders(siteId: number, params: Record = {}): Promise { // 调用 WooCommerce API 获取订单 - const result = await (await this.siteApiService.getAdapter(siteId)).getAllOrders(params); + const result = await (await this.siteApiService.getAdapter(siteId)).getAllOrders(params); const syncResult: SyncOperationResult = { total: result.length, @@ -202,7 +202,7 @@ export class OrderService { // 由于 wordpress 订单状态和 我们的订单状态 不一致,需要做转换 async autoUpdateOrderStatus(siteId: number, order: any) { - console.log('更新订单状态', order) + console.log('更新订单状态', order.status, '=>', this.orderAutoNextStatusMap[order.status]) // 其他状态保持不变 const originStatus = order.status; // 如果有值就赋值 diff --git a/src/service/product.service.ts b/src/service/product.service.ts index 6a8d7dd..1a06051 100644 --- a/src/service/product.service.ts +++ b/src/service/product.service.ts @@ -5,7 +5,6 @@ import { In, Like, Not, Repository } from 'typeorm'; import { Product } from '../entity/product.entity'; import { PaginationParams } from '../interface'; import { paginate } from '../utils/paginate.util'; - import { Context } from '@midwayjs/koa'; import { InjectEntityModel } from '@midwayjs/typeorm'; import { @@ -538,15 +537,14 @@ export class ProductService { async createProduct(createProductDTO: CreateProductDTO): Promise { - const { attributes, sku, categoryId } = createProductDTO; + const { attributes, sku, categoryId, type } = createProductDTO; // 条件判断(校验属性输入) - if (!Array.isArray(attributes) || attributes.length === 0) { - // 如果提供了 categoryId 但没有 attributes,初始化为空数组 - if (!attributes && categoryId) { - // 继续执行,下面会处理 categoryId - } else { - throw new Error('属性列表不能为空'); + // 当产品类型为 'bundle' 时,attributes 可以为空 + // 当产品类型为 'single' 时,attributes 必须提供且不能为空 + if (type === 'single') { + if (!Array.isArray(attributes) || attributes.length === 0) { + throw new Error('单品类型的属性列表不能为空'); } } @@ -607,23 +605,26 @@ export class ProductService { } // 检查完全相同属性组合是否已存在(避免重复) - const qb = this.productModel.createQueryBuilder('product'); - resolvedAttributes.forEach((attr, index) => { - qb.innerJoin( - 'product.attributes', - `attr${index}`, - `attr${index}.id = :attrId${index}`, - { [`attrId${index}`]: attr.id } - ); - }); - const isExist = await qb.getOne(); - if (isExist) throw new Error('相同产品属性的产品已存在'); + // 仅当产品类型为 'single' 且有属性时才检查重复 + if (type === 'single' && resolvedAttributes.length > 0) { + const qb = this.productModel.createQueryBuilder('product'); + resolvedAttributes.forEach((attr, index) => { + qb.innerJoin( + 'product.attributes', + `attr${index}`, + `attr${index}.id = :attrId${index}`, + { [`attrId${index}`]: attr.id } + ); + }); + const isExist = await qb.getOne(); + if (isExist) throw new Error('相同产品属性的产品已存在'); + } // 创建新产品实例(绑定属性与基础字段) const product = new Product(); // 使用 merge 填充基础字段,排除特殊处理字段 - const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, siteSkus: _siteSkus, ...simpleFields } = createProductDTO; + const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, ...simpleFields } = createProductDTO; this.productModel.merge(product, simpleFields); product.attributes = resolvedAttributes; @@ -642,13 +643,6 @@ export class ProductService { const savedProduct = await this.productModel.save(product); - // 设置站点 SKU 列表(确保始终设置,即使为空数组) - if (createProductDTO.siteSkus !== undefined) { - product.siteSkus = createProductDTO.siteSkus; - // 重新保存产品以更新 siteSkus - await this.productModel.save(product); - } - // 保存组件信息 if (createProductDTO.components && createProductDTO.components.length > 0) { await this.setProductComponents(savedProduct.id, createProductDTO.components); @@ -670,7 +664,7 @@ export class ProductService { } // 使用 merge 更新基础字段,排除特殊处理字段 - const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, siteSkus: _siteSkus, ...simpleFields } = updateProductDTO; + const { attributes: _attrs, categoryId: _cid, sku: _sku, components: _components, ...simpleFields } = updateProductDTO; this.productModel.merge(product, simpleFields); // 处理分类更新 @@ -685,11 +679,6 @@ export class ProductService { } } - // 处理站点 SKU 更新 - if (updateProductDTO.siteSkus !== undefined) { - product.siteSkus = updateProductDTO.siteSkus; - } - // 处理 SKU 更新 if (updateProductDTO.sku !== undefined) { // 校验 SKU 唯一性(如变更) @@ -786,7 +775,7 @@ export class ProductService { } } else { // 简单字段,直接批量更新以提高性能 - // UpdateProductDTO 里的简单字段: name, nameCn, description, price, promotionPrice + // UpdateProductDTO 里的简单字段: name, nameCn, description, price, promotionPrice, siteSkus const simpleUpdate: any = {}; if (updateData.name !== undefined) simpleUpdate.name = updateData.name; @@ -795,6 +784,7 @@ export class ProductService { if (updateData.shortDescription !== undefined) simpleUpdate.shortDescription = updateData.shortDescription; if (updateData.price !== undefined) simpleUpdate.price = updateData.price; if (updateData.promotionPrice !== undefined) simpleUpdate.promotionPrice = updateData.promotionPrice; + if (updateData.siteSkus !== undefined) simpleUpdate.siteSkus = updateData.siteSkus; if (Object.keys(simpleUpdate).length > 0) { await this.productModel.update({ id: In(ids) }, simpleUpdate); @@ -1588,7 +1578,7 @@ export class ProductService { async exportProductsCSV(): Promise { // 查询所有产品及其属性(包含字典关系)和组成 const products = await this.productModel.find({ - relations: ['attributes', 'attributes.dict', 'components', 'siteSkus'], + relations: ['attributes', 'attributes.dict', 'components'], order: { id: 'ASC' }, }); diff --git a/src/service/site.service.ts b/src/service/site.service.ts index a8b8fe4..58664d7 100644 --- a/src/service/site.service.ts +++ b/src/service/site.service.ts @@ -21,9 +21,9 @@ export class SiteService { async create(data: CreateSiteDTO) { // 从 DTO 中分离出区域代码和其他站点数据 - const { areas: areaCodes, stockPointIds, websiteUrl, ...restData } = data; + const { areas: areaCodes, stockPointIds, ...restData } = data; const newSite = new Site(); - Object.assign(newSite, restData, { websiteUrl }); + Object.assign(newSite, restData); // 如果传入了区域代码,则查询并关联 Area 实体 if (areaCodes && areaCodes.length > 0) { @@ -163,7 +163,7 @@ export class SiteService { const data = includeSecret ? items : items.map((item: any) => { - const { consumerKey, consumerSecret, ...rest } = item; + const { consumerKey, consumerSecret, token, ...rest } = item; return rest; }); return { items: data, total, current, pageSize };