From 8ef150c1ba83d2806d08c0900c4579f4d99b6635 Mon Sep 17 00:00:00 2001 From: tikkhun Date: Mon, 15 Dec 2025 15:18:12 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=9F=E4=B8=80=E5=88=86=E9=A1=B5?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E5=8F=82=E6=95=B0=E5=A4=84=E7=90=86=E4=B8=8E?= =?UTF-8?q?DTO=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refactor(service): 重构分页查询参数处理逻辑 feat(dto): 增强统一分页DTO与搜索参数DTO feat(controller): 实现批量导出与分页查询优化 docs(dto): 添加DTO字段注释与类型定义 --- .gitignore | 1 + debug_sync.log | 54 ++++ src/adapter/shopyy.adapter.ts | 70 +++-- src/adapter/woocommerce.adapter.ts | 92 ++++-- src/controller/site-api.controller.ts | 208 ++++++++++++-- src/dto/shopyy.dto.ts | 282 +++++++++++++++++++ src/dto/site-api.dto copy.ts | 264 ----------------- src/dto/site-api.dto.ts | 64 ++++- src/dto/woocommerce.dto.ts | 389 ++++++++++++++++++++++++++ src/service/shopyy.service.ts | 28 +- src/service/wp.service.ts | 62 +++- 11 files changed, 1178 insertions(+), 336 deletions(-) create mode 100644 debug_sync.log create mode 100644 src/dto/shopyy.dto.ts delete mode 100644 src/dto/site-api.dto copy.ts create mode 100644 src/dto/woocommerce.dto.ts diff --git a/.gitignore b/.gitignore index 9f2715f..e2c1f24 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ container scripts ai tmp_uploads/ +.trae \ No newline at end of file diff --git a/debug_sync.log b/debug_sync.log new file mode 100644 index 0000000..e39a7b1 --- /dev/null +++ b/debug_sync.log @@ -0,0 +1,54 @@ +[2025-12-12T10:44:39.963Z] [BatchSync] Starting sync to site 1 for products: [561,560] +[2025-12-12T10:44:39.978Z] [BatchSync] Found 2 products in local DB +[2025-12-12T10:44:39.992Z] [BatchSync] Payload - Create: 2, Update: 0 +[2025-12-12T10:44:39.993Z] [BatchSync] Create Payload: [{"name":"Pablo-Mango lce-30MG-wet","type":"simple","regular_price":"0.00","sale_price":"0.00","sku":"SPPablo-Mango lce-30MG-wet","status":"publish"},{"name":"YOONE- ICE WINTERGREEN-9MG-wet","type":"simple","regular_price":"0.00","sale_price":"0.00","sku":"SPYOONE- ICE WINTERGREEN-9MG-wet","status":"publish"}] +[2025-12-12T10:44:41.048Z] [BatchSync] API Success. Result: {"create":[{"id":279,"name":"Pablo-Mango lce-30MG-wet","slug":"pablo-mango-lce-30mg-wet","permalink":"http://simple.local/product/pablo-mango-lce-30mg-wet/","date_created":"2025-12-12T10:44:40","date_created_gmt":"2025-12-12T10:44:40","date_modified":"2025-12-12T10:44:40","date_modified_gmt":"2025-12-12T10:44:40","type":"simple","status":"publish","featured":false,"catalog_visibility":"visible","description":"","short_description":"","sku":"SPPablo-Mango lce-30MG-wet","price":"0.00","regular_price":"0.00","sale_price":"","date_on_sale_from":null,"date_on_sale_from_gmt":null,"date_on_sale_to":null,"date_on_sale_to_gmt":null,"on_sale":false,"purchasable":true,"total_sales":0,"virtual":false,"downloadable":false,"downloads":[],"download_limit":-1,"download_expiry":-1,"external_url":"","button_text":"","tax_status":"taxable","tax_class":"","manage_stock":false,"stock_quantity":null,"backorders":"no","backorders_allowed":false,"backordered":false,"low_stock_amount":null,"sold_individually":false,"weight":"","dimensions":{"length":"","width":"","height":""},"shipping_required":true,"shipping_taxable":true,"shipping_class":"","shipping_class_id":0,"reviews_allowed":true,"average_rating":"0","rating_count":0,"upsell_ids":[],"cross_sell_ids":[],"parent_id":0,"purchase_note":"","categories":[{"id":15,"name":"Uncategorized","slug":"uncategorized"}],"brands":[],"tags":[],"images":[],"attributes":[],"default_attributes":[],"variations":[],"grouped_products":[],"menu_order":0,"price_html":"$0.00 available on subscription","related_ids":[271,276,81,64,60],"meta_data":[{"key":"_subscription_payment_sync_date","value":0}],"stock_status":"instock","has_options":false,"post_password":"","global_unique_id":"","permalink_template":"http://simple.local/product/%pagename%/","generated_slug":"pablo-mango-lce-30mg-wet","bundled_by":[],"bundle_stock_status":"instock","bundle_stock_quantity":null,"bundle_virtual":false,"bundle_layout":"","bundle_add_to_cart_form_location":"","bundle_editable_in_cart":false,"bundle_sold_individually_context":"","bundle_item_grouping":"","bundle_min_size":"","bundle_max_size":"","bundled_items":[],"bundle_sell_ids":[],"_links":{"self":[{"href":"http://simple.local/wp-json/wc/v3/products/279","targetHints":{"allow":["GET","POST","PUT","PATCH","DELETE"]}}],"collection":[{"href":"http://simple.local/wp-json/wc/v3/products"}]}},{"id":280,"name":"YOONE- ICE WINTERGREEN-9MG-wet","slug":"yoone-ice-wintergreen-9mg-wet","permalink":"http://simple.local/product/yoone-ice-wintergreen-9mg-wet/","date_created":"2025-12-12T10:44:40","date_created_gmt":"2025-12-12T10:44:40","date_modified":"2025-12-12T10:44:40","date_modified_gmt":"2025-12-12T10:44:40","type":"simple","status":"publish","featured":false,"catalog_visibility":"visible","description":"","short_description":"","sku":"SPYOONE- ICE WINTERGREEN-9MG-wet","price":"0.00","regular_price":"0.00","sale_price":"","date_on_sale_from":null,"date_on_sale_from_gmt":null,"date_on_sale_to":null,"date_on_sale_to_gmt":null,"on_sale":false,"purchasable":true,"total_sales":0,"virtual":false,"downloadable":false,"downloads":[],"download_limit":-1,"download_expiry":-1,"external_url":"","button_text":"","tax_status":"taxable","tax_class":"","manage_stock":false,"stock_quantity":null,"backorders":"no","backorders_allowed":false,"backordered":false,"low_stock_amount":null,"sold_individually":false,"weight":"","dimensions":{"length":"","width":"","height":""},"shipping_required":true,"shipping_taxable":true,"shipping_class":"","shipping_class_id":0,"reviews_allowed":true,"average_rating":"0","rating_count":0,"upsell_ids":[],"cross_sell_ids":[],"parent_id":0,"purchase_note":"","categories":[{"id":15,"name":"Uncategorized","slug":"uncategorized"}],"brands":[],"tags":[],"images":[],"attributes":[],"default_attributes":[],"variations":[],"grouped_products":[],"menu_order":0,"price_html":"$0.00 available on subscription","related_ids":[276,279,271,60,64],"meta_data":[{"key":"_subscription_payment_sync_date","value":0}],"stock_status":"instock","has_options":false,"post_password":"","global_unique_id":"","permalink_template":"http://simple.local/product/%pagename%/","generated_slug":"yoone-ice-wintergreen-9mg-wet","bundled_by":[],"bundle_stock_status":"instock","bundle_stock_quantity":null,"bundle_virtual":false,"bundle_layout":"","bundle_add_to_cart_form_location":"","bundle_editable_in_cart":false,"bundle_sold_individually_context":"","bundle_item_grouping":"","bundle_min_size":"","bundle_max_size":"","bundled_items":[],"bundle_sell_ids":[],"_links":{"self":[{"href":"http://simple.local/wp-json/wc/v3/products/280","targetHints":{"allow":["GET","POST","PUT","PATCH","DELETE"]}}],"collection":[{"href":"http://simple.local/wp-json/wc/v3/products"}]}}]} +[2025-12-12T10:44:41.049Z] [BatchSync] Processing success item: ID=279, SKU=SPPablo-Mango lce-30MG-wet +[2025-12-12T10:44:41.049Z] [BatchSync] Found local product ID=560 for SKU=SPPablo-Mango lce-30MG-wet +[2025-12-12T10:44:41.054Z] [BatchSync] Creating ProductSiteSku for productId=560 code=SPPablo-Mango lce-30MG-wet +[2025-12-12T10:44:41.102Z] [BatchSync] Processing success item: ID=280, SKU=SPYOONE- ICE WINTERGREEN-9MG-wet +[2025-12-12T10:44:41.102Z] [BatchSync] Found local product ID=561 for SKU=SPYOONE- ICE WINTERGREEN-9MG-wet +[2025-12-12T10:44:41.104Z] [BatchSync] Creating ProductSiteSku for productId=561 code=SPYOONE- ICE WINTERGREEN-9MG-wet +[2025-12-12T10:59:13.077Z] [BatchSync] Starting sync to site 1 for products: [361] +[2025-12-12T10:59:13.091Z] [BatchSync] Found 1 products in local DB +[2025-12-12T10:59:13.094Z] [BatchSync] Payload - Create: 1, Update: 0 +[2025-12-12T10:59:13.094Z] [BatchSync] Create Payload: [{"name":"YOONE-NP-S-MA-6MG-DRY","type":"simple","regular_price":"0.00","sale_price":"0.00","sku":"SPYOONE-NP-S-MA-6MG-DRY","status":"publish"}] +[2025-12-12T10:59:13.917Z] [BatchSync] API Success. Result: {"create":[{"id":281,"name":"YOONE-NP-S-MA-6MG-DRY","slug":"yoone-np-s-ma-6mg-dry","permalink":"http://simple.local/product/yoone-np-s-ma-6mg-dry/","date_created":"2025-12-12T10:59:13","date_created_gmt":"2025-12-12T10:59:13","date_modified":"2025-12-12T10:59:13","date_modified_gmt":"2025-12-12T10:59:13","type":"simple","status":"publish","featured":false,"catalog_visibility":"visible","description":"","short_description":"","sku":"SPYOONE-NP-S-MA-6MG-DRY","price":"0.00","regular_price":"0.00","sale_price":"","date_on_sale_from":null,"date_on_sale_from_gmt":null,"date_on_sale_to":null,"date_on_sale_to_gmt":null,"on_sale":false,"purchasable":true,"total_sales":0,"virtual":false,"downloadable":false,"downloads":[],"download_limit":-1,"download_expiry":-1,"external_url":"","button_text":"","tax_status":"taxable","tax_class":"","manage_stock":false,"stock_quantity":null,"backorders":"no","backorders_allowed":false,"backordered":false,"low_stock_amount":null,"sold_individually":false,"weight":"","dimensions":{"length":"","width":"","height":""},"shipping_required":true,"shipping_taxable":true,"shipping_class":"","shipping_class_id":0,"reviews_allowed":true,"average_rating":"0","rating_count":0,"upsell_ids":[],"cross_sell_ids":[],"parent_id":0,"purchase_note":"","categories":[{"id":15,"name":"Uncategorized","slug":"uncategorized"}],"brands":[],"tags":[],"images":[],"attributes":[],"default_attributes":[],"variations":[],"grouped_products":[],"menu_order":0,"price_html":"$0.00 available on subscription","related_ids":[81,60,271,276,64],"meta_data":[{"key":"_subscription_payment_sync_date","value":0}],"stock_status":"instock","has_options":false,"post_password":"","global_unique_id":"","permalink_template":"http://simple.local/product/%pagename%/","generated_slug":"yoone-np-s-ma-6mg-dry","bundled_by":[],"bundle_stock_status":"instock","bundle_stock_quantity":null,"bundle_virtual":false,"bundle_layout":"","bundle_add_to_cart_form_location":"","bundle_editable_in_cart":false,"bundle_sold_individually_context":"","bundle_item_grouping":"","bundle_min_size":"","bundle_max_size":"","bundled_items":[],"bundle_sell_ids":[],"_links":{"self":[{"href":"http://simple.local/wp-json/wc/v3/products/281","targetHints":{"allow":["GET","POST","PUT","PATCH","DELETE"]}}],"collection":[{"href":"http://simple.local/wp-json/wc/v3/products"}]}}]} +[2025-12-12T10:59:13.918Z] [BatchSync] Processing success item: ID=281, SKU=SPYOONE-NP-S-MA-6MG-DRY +[2025-12-12T10:59:13.919Z] [BatchSync] Found local product ID=361 for SKU=SPYOONE-NP-S-MA-6MG-DRY +[2025-12-12T10:59:13.922Z] [BatchSync] Creating ProductSiteSku for productId=361 code=SPYOONE-NP-S-MA-6MG-DRY +[2025-12-12T11:03:42.518Z] [BatchSync] Starting sync to site 1 for products: [361] +[2025-12-12T11:03:42.529Z] [BatchSync] Found 1 products in local DB +[2025-12-12T11:03:42.534Z] [BatchSync] Payload - Create: 0, Update: 1 +[2025-12-12T11:03:42.534Z] [BatchSync] Update Payload: [{"id":"281","name":"YOONE-NP-S-MA-6MG-DRY","type":"simple","regular_price":"0.00","sale_price":"0.00","sku":"SPYOONE-NP-S-MA-6MG-DRY","status":"publish"}] +[2025-12-12T11:03:43.283Z] [BatchSync] API Success. Result: {"update":[{"id":281,"name":"YOONE-NP-S-MA-6MG-DRY","slug":"yoone-np-s-ma-6mg-dry","permalink":"http://simple.local/product/yoone-np-s-ma-6mg-dry/","date_created":"2025-12-12T10:59:13","date_created_gmt":"2025-12-12T10:59:13","date_modified":"2025-12-12T11:03:43","date_modified_gmt":"2025-12-12T11:03:43","type":"simple","status":"publish","featured":false,"catalog_visibility":"visible","description":"","short_description":"","sku":"SPYOONE-NP-S-MA-6MG-DRY","price":"0.00","regular_price":"0.00","sale_price":"","date_on_sale_from":null,"date_on_sale_from_gmt":null,"date_on_sale_to":null,"date_on_sale_to_gmt":null,"on_sale":false,"purchasable":true,"total_sales":0,"virtual":false,"downloadable":false,"downloads":[],"download_limit":-1,"download_expiry":-1,"external_url":"","button_text":"","tax_status":"taxable","tax_class":"","manage_stock":false,"stock_quantity":null,"backorders":"no","backorders_allowed":false,"backordered":false,"low_stock_amount":null,"sold_individually":false,"weight":"","dimensions":{"length":"","width":"","height":""},"shipping_required":true,"shipping_taxable":true,"shipping_class":"","shipping_class_id":0,"reviews_allowed":true,"average_rating":"0","rating_count":0,"upsell_ids":[],"cross_sell_ids":[],"parent_id":0,"purchase_note":"","categories":[{"id":15,"name":"Uncategorized","slug":"uncategorized"}],"brands":[],"tags":[],"images":[],"attributes":[],"default_attributes":[],"variations":[],"grouped_products":[],"menu_order":0,"price_html":"$0.00 available on subscription","related_ids":[60,276,279,64,271],"meta_data":[{"key":"_subscription_payment_sync_date","value":0}],"stock_status":"instock","has_options":false,"post_password":"","global_unique_id":"","permalink_template":"http://simple.local/product/%pagename%/","generated_slug":"yoone-np-s-ma-6mg-dry","bundled_by":[],"bundle_stock_status":"instock","bundle_stock_quantity":null,"bundle_virtual":false,"bundle_layout":"","bundle_add_to_cart_form_location":"","bundle_editable_in_cart":false,"bundle_sold_individually_context":"","bundle_item_grouping":"","bundle_min_size":"","bundle_max_size":"","bundled_items":[],"bundle_sell_ids":[],"_links":{"self":[{"href":"http://simple.local/wp-json/wc/v3/products/281","targetHints":{"allow":["GET","POST","PUT","PATCH","DELETE"]}}],"collection":[{"href":"http://simple.local/wp-json/wc/v3/products"}]}}]} +[2025-12-12T11:03:43.284Z] [BatchSync] Processing success item: ID=281, SKU=SPYOONE-NP-S-MA-6MG-DRY +[2025-12-12T11:03:43.285Z] [BatchSync] Found local product ID=361 for SKU=SPYOONE-NP-S-MA-6MG-DRY +[2025-12-12T11:03:43.290Z] [BatchSync] ProductSiteSku already exists for productId=361 code=SPYOONE-NP-S-MA-6MG-DRY +[2025-12-12T11:05:01.270Z] [BatchSync] Starting sync to site 1 for products: [561] +[2025-12-12T11:05:01.282Z] [BatchSync] Found 1 products in local DB +[2025-12-12T11:05:01.285Z] [BatchSync] Payload - Create: 0, Update: 1 +[2025-12-12T11:05:01.285Z] [BatchSync] Update Payload: [{"id":"280","name":"YOONE- ICE WINTERGREEN-9MG-wet","type":"simple","regular_price":"0.00","sale_price":"0.00","sku":"SPYOONE- ICE WINTERGREEN-9MG-wet","status":"publish"}] +[2025-12-12T11:05:02.050Z] [BatchSync] API Success. Result: {"update":[{"id":280,"name":"YOONE- ICE WINTERGREEN-9MG-wet","slug":"yoone-ice-wintergreen-9mg-wet","permalink":"http://simple.local/product/yoone-ice-wintergreen-9mg-wet/","date_created":"2025-12-12T10:44:40","date_created_gmt":"2025-12-12T10:44:40","date_modified":"2025-12-12T11:05:02","date_modified_gmt":"2025-12-12T11:05:02","type":"simple","status":"publish","featured":false,"catalog_visibility":"visible","description":"","short_description":"","sku":"SPYOONE- ICE WINTERGREEN-9MG-wet","price":"0.00","regular_price":"0.00","sale_price":"","date_on_sale_from":null,"date_on_sale_from_gmt":null,"date_on_sale_to":null,"date_on_sale_to_gmt":null,"on_sale":false,"purchasable":true,"total_sales":0,"virtual":false,"downloadable":false,"downloads":[],"download_limit":-1,"download_expiry":-1,"external_url":"","button_text":"","tax_status":"taxable","tax_class":"","manage_stock":false,"stock_quantity":null,"backorders":"no","backorders_allowed":false,"backordered":false,"low_stock_amount":null,"sold_individually":false,"weight":"","dimensions":{"length":"","width":"","height":""},"shipping_required":true,"shipping_taxable":true,"shipping_class":"","shipping_class_id":0,"reviews_allowed":true,"average_rating":"0","rating_count":0,"upsell_ids":[],"cross_sell_ids":[],"parent_id":0,"purchase_note":"","categories":[{"id":15,"name":"Uncategorized","slug":"uncategorized"}],"brands":[],"tags":[],"images":[],"attributes":[],"default_attributes":[],"variations":[],"grouped_products":[],"menu_order":0,"price_html":"$0.00 available on subscription","related_ids":[271,281,60,279,64],"meta_data":[{"key":"_subscription_payment_sync_date","value":0}],"stock_status":"instock","has_options":false,"post_password":"","global_unique_id":"","permalink_template":"http://simple.local/product/%pagename%/","generated_slug":"yoone-ice-wintergreen-9mg-wet","bundled_by":[],"bundle_stock_status":"instock","bundle_stock_quantity":null,"bundle_virtual":false,"bundle_layout":"","bundle_add_to_cart_form_location":"","bundle_editable_in_cart":false,"bundle_sold_individually_context":"","bundle_item_grouping":"","bundle_min_size":"","bundle_max_size":"","bundled_items":[],"bundle_sell_ids":[],"_links":{"self":[{"href":"http://simple.local/wp-json/wc/v3/products/280","targetHints":{"allow":["GET","POST","PUT","PATCH","DELETE"]}}],"collection":[{"href":"http://simple.local/wp-json/wc/v3/products"}]}}]} +[2025-12-12T11:05:02.051Z] [BatchSync] Processing success item: ID=280, SKU=SPYOONE- ICE WINTERGREEN-9MG-wet +[2025-12-12T11:05:02.051Z] [BatchSync] Found local product ID=561 for SKU=SPYOONE- ICE WINTERGREEN-9MG-wet +[2025-12-12T11:05:02.054Z] [BatchSync] ProductSiteSku already exists for productId=561 code=SPYOONE- ICE WINTERGREEN-9MG-wet +[2025-12-12T11:05:18.179Z] [BatchSync] Starting sync to site 1 for products: [561] +[2025-12-12T11:05:18.188Z] [BatchSync] Found 1 products in local DB +[2025-12-12T11:05:18.193Z] [BatchSync] Payload - Create: 0, Update: 1 +[2025-12-12T11:05:18.194Z] [BatchSync] Update Payload: [{"id":"280","name":"YOONE- ICE WINTERGREEN-9MG-wet","type":"simple","regular_price":"0.00","sale_price":"0.00","sku":"SPYOONE- ICE WINTERGREEN-9MG-wet","status":"publish"}] +[2025-12-12T11:05:19.031Z] [BatchSync] API Success. Result: {"update":[{"id":280,"name":"YOONE- ICE WINTERGREEN-9MG-wet","slug":"yoone-ice-wintergreen-9mg-wet","permalink":"http://simple.local/product/yoone-ice-wintergreen-9mg-wet/","date_created":"2025-12-12T10:44:40","date_created_gmt":"2025-12-12T10:44:40","date_modified":"2025-12-12T11:05:19","date_modified_gmt":"2025-12-12T11:05:19","type":"simple","status":"publish","featured":false,"catalog_visibility":"visible","description":"","short_description":"","sku":"SPYOONE- ICE WINTERGREEN-9MG-wet","price":"0.00","regular_price":"0.00","sale_price":"","date_on_sale_from":null,"date_on_sale_from_gmt":null,"date_on_sale_to":null,"date_on_sale_to_gmt":null,"on_sale":false,"purchasable":true,"total_sales":0,"virtual":false,"downloadable":false,"downloads":[],"download_limit":-1,"download_expiry":-1,"external_url":"","button_text":"","tax_status":"taxable","tax_class":"","manage_stock":false,"stock_quantity":null,"backorders":"no","backorders_allowed":false,"backordered":false,"low_stock_amount":null,"sold_individually":false,"weight":"","dimensions":{"length":"","width":"","height":""},"shipping_required":true,"shipping_taxable":true,"shipping_class":"","shipping_class_id":0,"reviews_allowed":true,"average_rating":"0","rating_count":0,"upsell_ids":[],"cross_sell_ids":[],"parent_id":0,"purchase_note":"","categories":[{"id":15,"name":"Uncategorized","slug":"uncategorized"}],"brands":[],"tags":[],"images":[],"attributes":[],"default_attributes":[],"variations":[],"grouped_products":[],"menu_order":0,"price_html":"$0.00 available on subscription","related_ids":[279,276,271,60,81],"meta_data":[{"key":"_subscription_payment_sync_date","value":0}],"stock_status":"instock","has_options":false,"post_password":"","global_unique_id":"","permalink_template":"http://simple.local/product/%pagename%/","generated_slug":"yoone-ice-wintergreen-9mg-wet","bundled_by":[],"bundle_stock_status":"instock","bundle_stock_quantity":null,"bundle_virtual":false,"bundle_layout":"","bundle_add_to_cart_form_location":"","bundle_editable_in_cart":false,"bundle_sold_individually_context":"","bundle_item_grouping":"","bundle_min_size":"","bundle_max_size":"","bundled_items":[],"bundle_sell_ids":[],"_links":{"self":[{"href":"http://simple.local/wp-json/wc/v3/products/280","targetHints":{"allow":["GET","POST","PUT","PATCH","DELETE"]}}],"collection":[{"href":"http://simple.local/wp-json/wc/v3/products"}]}}]} +[2025-12-12T11:05:19.032Z] [BatchSync] Processing success item: ID=280, SKU=SPYOONE- ICE WINTERGREEN-9MG-wet +[2025-12-12T11:05:19.032Z] [BatchSync] Found local product ID=561 for SKU=SPYOONE- ICE WINTERGREEN-9MG-wet +[2025-12-12T11:05:19.035Z] [BatchSync] Creating ProductSiteSku for productId=561 code=SPYOONE- ICE WINTERGREEN-9MG-wet +[2025-12-12T11:05:48.915Z] [BatchSync] Starting sync to site 1 for products: [561,560] +[2025-12-12T11:05:48.923Z] [BatchSync] Found 2 products in local DB +[2025-12-12T11:05:48.931Z] [BatchSync] Payload - Create: 0, Update: 2 +[2025-12-12T11:05:48.932Z] [BatchSync] Update Payload: [{"id":"279","name":"Pablo-Mango lce-30MG-wet","type":"simple","regular_price":"0.00","sale_price":"0.00","sku":"SPPablo-Mango lce-30MG-wet","status":"publish"},{"id":"280","name":"YOONE- ICE WINTERGREEN-9MG-wet","type":"simple","regular_price":"0.00","sale_price":"0.00","sku":"SPYOONE- ICE WINTERGREEN-9MG-wet","status":"publish"}] +[2025-12-12T11:05:49.703Z] [BatchSync] API Success. Result: {"update":[{"id":279,"name":"Pablo-Mango lce-30MG-wet","slug":"pablo-mango-lce-30mg-wet","permalink":"http://simple.local/product/pablo-mango-lce-30mg-wet/","date_created":"2025-12-12T10:44:40","date_created_gmt":"2025-12-12T10:44:40","date_modified":"2025-12-12T11:05:49","date_modified_gmt":"2025-12-12T11:05:49","type":"simple","status":"publish","featured":false,"catalog_visibility":"visible","description":"","short_description":"","sku":"SPPablo-Mango lce-30MG-wet","price":"0.00","regular_price":"0.00","sale_price":"","date_on_sale_from":null,"date_on_sale_from_gmt":null,"date_on_sale_to":null,"date_on_sale_to_gmt":null,"on_sale":false,"purchasable":true,"total_sales":0,"virtual":false,"downloadable":false,"downloads":[],"download_limit":-1,"download_expiry":-1,"external_url":"","button_text":"","tax_status":"taxable","tax_class":"","manage_stock":false,"stock_quantity":null,"backorders":"no","backorders_allowed":false,"backordered":false,"low_stock_amount":null,"sold_individually":false,"weight":"","dimensions":{"length":"","width":"","height":""},"shipping_required":true,"shipping_taxable":true,"shipping_class":"","shipping_class_id":0,"reviews_allowed":true,"average_rating":"0","rating_count":0,"upsell_ids":[],"cross_sell_ids":[],"parent_id":0,"purchase_note":"","categories":[{"id":15,"name":"Uncategorized","slug":"uncategorized"}],"brands":[],"tags":[],"images":[],"attributes":[],"default_attributes":[],"variations":[],"grouped_products":[],"menu_order":0,"price_html":"$0.00 available on subscription","related_ids":[64,280,276,271,60],"meta_data":[{"key":"_subscription_payment_sync_date","value":0}],"stock_status":"instock","has_options":false,"post_password":"","global_unique_id":"","permalink_template":"http://simple.local/product/%pagename%/","generated_slug":"pablo-mango-lce-30mg-wet","bundled_by":[],"bundle_stock_status":"instock","bundle_stock_quantity":null,"bundle_virtual":false,"bundle_layout":"","bundle_add_to_cart_form_location":"","bundle_editable_in_cart":false,"bundle_sold_individually_context":"","bundle_item_grouping":"","bundle_min_size":"","bundle_max_size":"","bundled_items":[],"bundle_sell_ids":[],"_links":{"self":[{"href":"http://simple.local/wp-json/wc/v3/products/279","targetHints":{"allow":["GET","POST","PUT","PATCH","DELETE"]}}],"collection":[{"href":"http://simple.local/wp-json/wc/v3/products"}]}},{"id":280,"name":"YOONE- ICE WINTERGREEN-9MG-wet","slug":"yoone-ice-wintergreen-9mg-wet","permalink":"http://simple.local/product/yoone-ice-wintergreen-9mg-wet/","date_created":"2025-12-12T10:44:40","date_created_gmt":"2025-12-12T10:44:40","date_modified":"2025-12-12T11:05:49","date_modified_gmt":"2025-12-12T11:05:49","type":"simple","status":"publish","featured":false,"catalog_visibility":"visible","description":"","short_description":"","sku":"SPYOONE- ICE WINTERGREEN-9MG-wet","price":"0.00","regular_price":"0.00","sale_price":"","date_on_sale_from":null,"date_on_sale_from_gmt":null,"date_on_sale_to":null,"date_on_sale_to_gmt":null,"on_sale":false,"purchasable":true,"total_sales":0,"virtual":false,"downloadable":false,"downloads":[],"download_limit":-1,"download_expiry":-1,"external_url":"","button_text":"","tax_status":"taxable","tax_class":"","manage_stock":false,"stock_quantity":null,"backorders":"no","backorders_allowed":false,"backordered":false,"low_stock_amount":null,"sold_individually":false,"weight":"","dimensions":{"length":"","width":"","height":""},"shipping_required":true,"shipping_taxable":true,"shipping_class":"","shipping_class_id":0,"reviews_allowed":true,"average_rating":"0","rating_count":0,"upsell_ids":[],"cross_sell_ids":[],"parent_id":0,"purchase_note":"","categories":[{"id":15,"name":"Uncategorized","slug":"uncategorized"}],"brands":[],"tags":[],"images":[],"attributes":[],"default_attributes":[],"variations":[],"grouped_products":[],"menu_order":0,"price_html":"$0.00 available on subscription","related_ids":[60,81,279,64,276],"meta_data":[{"key":"_subscription_payment_sync_date","value":0}],"stock_status":"instock","has_options":false,"post_password":"","global_unique_id":"","permalink_template":"http://simple.local/product/%pagename%/","generated_slug":"yoone-ice-wintergreen-9mg-wet","bundled_by":[],"bundle_stock_status":"instock","bundle_stock_quantity":null,"bundle_virtual":false,"bundle_layout":"","bundle_add_to_cart_form_location":"","bundle_editable_in_cart":false,"bundle_sold_individually_context":"","bundle_item_grouping":"","bundle_min_size":"","bundle_max_size":"","bundled_items":[],"bundle_sell_ids":[],"_links":{"self":[{"href":"http://simple.local/wp-json/wc/v3/products/280","targetHints":{"allow":["GET","POST","PUT","PATCH","DELETE"]}}],"collection":[{"href":"http://simple.local/wp-json/wc/v3/products"}]}}]} +[2025-12-12T11:05:49.704Z] [BatchSync] Processing success item: ID=279, SKU=SPPablo-Mango lce-30MG-wet +[2025-12-12T11:05:49.704Z] [BatchSync] Found local product ID=560 for SKU=SPPablo-Mango lce-30MG-wet +[2025-12-12T11:05:49.709Z] [BatchSync] ProductSiteSku already exists for productId=560 code=SPPablo-Mango lce-30MG-wet +[2025-12-12T11:05:49.738Z] [BatchSync] Processing success item: ID=280, SKU=SPYOONE- ICE WINTERGREEN-9MG-wet +[2025-12-12T11:05:49.739Z] [BatchSync] Found local product ID=561 for SKU=SPYOONE- ICE WINTERGREEN-9MG-wet +[2025-12-12T11:05:49.741Z] [BatchSync] Creating ProductSiteSku for productId=561 code=SPYOONE- ICE WINTERGREEN-9MG-wet diff --git a/src/adapter/shopyy.adapter.ts b/src/adapter/shopyy.adapter.ts index dc820d4..ed39140 100644 --- a/src/adapter/shopyy.adapter.ts +++ b/src/adapter/shopyy.adapter.ts @@ -9,6 +9,12 @@ import { UnifiedSubscriptionDTO, UnifiedCustomerDTO, } from '../dto/site-api.dto'; +import { + ShopyyProduct, + ShopyyOrder, + ShopyyCustomer, + ShopyyVariant, +} from '../dto/shopyy.dto'; export class ShopyyAdapter implements ISiteAdapter { constructor(private site: any, private shopyyService: ShopyyService) { } @@ -16,19 +22,19 @@ export class ShopyyAdapter implements ISiteAdapter { // return status === 1 ? 'publish' : 'draft'; // } - private mapProduct(item: any): UnifiedProductDTO { + private mapProduct(item: ShopyyProduct): UnifiedProductDTO { function mapProductStatus(status: number) { return status === 1 ? 'publish' : 'draft'; } return { id: item.id, name: item.name || item.title, - type: item.product_type, + type: String(item.product_type ?? ''), status: mapProductStatus(item.status), sku: item.variant?.sku || '', - regular_price: item.variant?.price, - sale_price: item.special_price, - price: item.price, + regular_price: String(item.variant?.price ?? ''), + sale_price: String(item.special_price ?? ''), + price: String(item.price ?? ''), stock_status: item.inventory_tracking === 1 ? 'instock' : 'outofstock', stock_quantity: item.inventory_quantity, images: (item.images || []).map((img: any) => ({ @@ -42,26 +48,26 @@ export class ShopyyAdapter implements ISiteAdapter { attributes: [], tags: item.tags || [], variations: item.variants?.map(this.mapVariation.bind(this)) || [], - date_created: item.created_at, - date_modified: item.updated_at, + date_created: typeof item.created_at === 'number' ? new Date(item.created_at * 1000).toISOString() : String(item.created_at ?? ''), + date_modified: typeof item.updated_at === 'number' ? new Date(item.updated_at * 1000).toISOString() : String(item.updated_at ?? ''), raw: item, }; } - mapVariation(mapVariation: any) { + mapVariation(mapVariation: ShopyyVariant) { return { id: mapVariation.id, sku: mapVariation.sku || '', - regular_price: mapVariation.price, - sale_price: mapVariation.special_price, - price: mapVariation.price, + regular_price: String(mapVariation.price ?? ''), + sale_price: String(mapVariation.special_price ?? ''), + price: String(mapVariation.price ?? ''), stock_status: mapVariation.inventory_tracking === 1 ? 'instock' : 'outofstock', stock_quantity: mapVariation.inventory_quantity, } } - private mapOrder(item: any): UnifiedOrderDTO { - const billing = item.billing_address || {}; - const shipping = item.shipping_address || {}; + private mapOrder(item: ShopyyOrder): UnifiedOrderDTO { + const billing = (item as any).billing_address || {}; + const shipping = (item as any).shipping_address || {}; const billingObj = { first_name: billing.first_name || item.firstname || '', @@ -69,7 +75,7 @@ export class ShopyyAdapter implements ISiteAdapter { fullname: billing.name || `${item.firstname} ${item.lastname}`.trim(), company: billing.company || '', email: item.customer_email || item.email || '', - phone: billing.phone || item.telephone || '', + phone: billing.phone || (item as any).telephone || '', address_1: billing.address1 || item.payment_address || '', address_2: billing.address2 || '', city: billing.city || item.payment_city || '', @@ -83,7 +89,7 @@ export class ShopyyAdapter implements ISiteAdapter { last_name: shipping.last_name || item.lastname || '', fullname: shipping.name || '', company: shipping.company || '', - address_1: shipping.address1 || item.shipping_address || '', + address_1: shipping.address1 || (typeof item.shipping_address === 'string' ? item.shipping_address : '') || '', address_2: shipping.address2 || '', city: shipping.city || item.shipping_city || '', state: shipping.province || item.shipping_zone || '', @@ -110,7 +116,7 @@ export class ShopyyAdapter implements ISiteAdapter { number: item.order_number || item.order_sn, status: String(item.status || item.order_status), currency: item.currency_code || item.currency, - total: String(item.total_price || item.total_amount), + total: String(item.total_price ?? item.total_amount ?? ''), customer_id: item.customer_id || item.user_id, customer_name: item.customer_name || `${item.firstname} ${item.lastname}`.trim(), email: item.customer_email || item.email, @@ -119,7 +125,7 @@ export class ShopyyAdapter implements ISiteAdapter { name: p.product_title || p.name, product_id: p.product_id, quantity: p.quantity, - total: String(p.price), + total: String(p.price ?? ''), sku: p.sku || p.sku_code || '' })), sales: (item.products || []).map((p: any) => ({ @@ -128,7 +134,7 @@ export class ShopyyAdapter implements ISiteAdapter { product_id: p.product_id, productId: p.product_id, quantity: p.quantity, - total: String(p.price), + total: String(p.price ?? ''), sku: p.sku || p.sku_code || '' })), billing: billingObj, @@ -136,12 +142,19 @@ export class ShopyyAdapter implements ISiteAdapter { billing_full_address: formatAddress(billingObj), shipping_full_address: formatAddress(shippingObj), payment_method: item.payment_method, - date_created: item.created_at ? new Date(item.created_at * 1000).toISOString() : item.date_added, + date_created: + typeof item.created_at === 'number' + ? new Date(item.created_at * 1000).toISOString() + : item.date_added || (typeof item.created_at === 'string' ? item.created_at : ''), + date_modified: + typeof item.updated_at === 'number' + ? new Date(item.updated_at * 1000).toISOString() + : item.date_updated || item.last_modified || (typeof item.updated_at === 'string' ? item.updated_at : ''), raw: item, }; } - private mapCustomer(item: any): UnifiedCustomerDTO { + private mapCustomer(item: ShopyyCustomer): UnifiedCustomerDTO { // 处理多地址结构 const addresses = item.addresses || []; const defaultAddress = item.default_address || (addresses.length > 0 ? addresses[0] : {}); @@ -153,6 +166,8 @@ export class ShopyyAdapter implements ISiteAdapter { return { id: item.id || item.customer_id, + orders: Number(item.orders_count ?? item.order_count ?? item.orders ?? 0), + total_spend: Number(item.total_spent ?? item.total_spend_amount ?? item.total_spend_money ?? 0), first_name: item.first_name || item.firstname || '', last_name: item.last_name || item.lastname || '', fullname: item.fullname || item.customer_name || `${item.first_name || item.firstname || ''} ${item.last_name || item.lastname || ''}`.trim(), @@ -184,6 +199,14 @@ export class ShopyyAdapter implements ISiteAdapter { postcode: shipping.zip || '', country: shipping.country_name || shipping.country_code || item.country?.country_name || '' }, + date_created: + typeof item.created_at === 'number' + ? new Date(item.created_at * 1000).toISOString() + : (typeof item.created_at === 'string' ? item.created_at : item.date_added || ''), + date_modified: + typeof item.updated_at === 'number' + ? new Date(item.updated_at * 1000).toISOString() + : (typeof item.updated_at === 'string' ? item.updated_at : item.date_updated || ''), raw: item, }; } @@ -203,6 +226,7 @@ export class ShopyyAdapter implements ISiteAdapter { totalPages, page, per_page, + page_size: per_page, }; } @@ -269,6 +293,7 @@ export class ShopyyAdapter implements ISiteAdapter { totalPages, page, per_page, + page_size: per_page, }; } @@ -310,7 +335,8 @@ export class ShopyyAdapter implements ISiteAdapter { total, totalPages, page, - per_page + per_page, + page_size: per_page }; } diff --git a/src/adapter/woocommerce.adapter.ts b/src/adapter/woocommerce.adapter.ts index 1e9d00a..a2779a9 100644 --- a/src/adapter/woocommerce.adapter.ts +++ b/src/adapter/woocommerce.adapter.ts @@ -9,17 +9,31 @@ import { UnifiedSubscriptionDTO, UnifiedCustomerDTO, } from '../dto/site-api.dto'; +import { + WooProduct, + WooOrder, + WooSubscription, + WpMedia, + WooCustomer, +} from '../dto/woocommerce.dto'; export class WooCommerceAdapter implements ISiteAdapter { + // 构造函数接收站点配置与服务实例 constructor(private site: any, private wpService: WPService) {} - private mapProduct(item: any): UnifiedProductDTO { + private mapProduct(item: WooProduct): UnifiedProductDTO { + // 将 WooCommerce 产品数据映射为统一产品DTO + // 保留常用字段与时间信息以便前端统一展示 + // https://woocommerce.github.io/woocommerce-rest-api-docs/?javascript#product-properties return { id: item.id, - name: item.name, - type: item.type, - status: item.status, + date_created: item.date_created, + date_modified: item.date_modified, + type: item.type, // simple grouped external variable + status: item.status, // draft pending private publish sku: item.sku, + name: item.name, + //价格 regular_price: item.regular_price, sale_price: item.sale_price, price: item.price, @@ -33,13 +47,13 @@ export class WooCommerceAdapter implements ISiteAdapter { })), attributes: item.attributes, variations: item.variations, - date_created: item.date_created, - date_modified: item.date_modified, + raw: item, }; } - private mapOrder(item: any): UnifiedOrderDTO { + private mapOrder(item: WooOrder): UnifiedOrderDTO { + // 地址格式化函数用于生成完整地址字符串 const formatAddress = (addr: any) => { if (!addr) return ''; const name = addr.fullname || `${addr.first_name || ''} ${addr.last_name || ''}`.trim(); @@ -56,6 +70,8 @@ export class WooCommerceAdapter implements ISiteAdapter { ].filter(Boolean).join(', '); }; + // 将 WooCommerce 订单数据映射为统一订单DTO + // 包含账单地址与收货地址以及创建与更新时间 return { id: item.id, number: item.number, @@ -79,17 +95,22 @@ export class WooCommerceAdapter implements ISiteAdapter { shipping_full_address: formatAddress(item.shipping), payment_method: item.payment_method_title, date_created: item.date_created, + date_modified: item.date_modified, raw: item, }; } - private mapSubscription(item: any): UnifiedSubscriptionDTO { + private mapSubscription(item: WooSubscription): UnifiedSubscriptionDTO { + // 将 WooCommerce 订阅数据映射为统一订阅DTO + // 若缺少创建时间则回退为开始时间 return { id: item.id, status: item.status, customer_id: item.customer_id, billing_period: item.billing_period, billing_interval: item.billing_interval, + date_created: item.date_created ?? item.start_date, + date_modified: item.date_modified, start_date: item.start_date, next_payment_date: item.next_payment_date, line_items: item.line_items, @@ -97,20 +118,27 @@ export class WooCommerceAdapter implements ISiteAdapter { }; } - private mapMedia(item: any): UnifiedMediaDTO { + private mapMedia(item: WpMedia): UnifiedMediaDTO { + // 将 WordPress 媒体数据映射为统一媒体DTO + // 兼容不同字段命名的时间信息 return { id: item.id, - title: item.title?.rendered || '', + title: + typeof item.title === 'string' + ? item.title + : item.title?.rendered || '', media_type: item.media_type, mime_type: item.mime_type, source_url: item.source_url, - date_created: item.date_created, + date_created: item.date_created ?? item.date, + date_modified: item.date_modified ?? item.modified, }; } async getProducts( params: UnifiedSearchParamsDTO ): Promise> { + // 获取产品列表并使用统一分页结构返回 const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( this.site, @@ -123,43 +151,51 @@ export class WooCommerceAdapter implements ISiteAdapter { totalPages, page, per_page, + page_size: per_page, }; } 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); } async createProduct(data: Partial): Promise { + // 创建产品并返回统一产品DTO const res = await this.wpService.createProduct(this.site, data); return this.mapProduct(res); } async updateProduct(id: string | number, data: Partial): Promise { + // 更新产品并返回统一产品DTO const res = await this.wpService.updateProduct(this.site, String(id), data as any); return this.mapProduct(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'); const res = await api.get(`orders/${orderId}/notes`); return res.data; } async createOrderNote(orderId: string | number, data: any): Promise { + // 创建订单备注 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const res = await api.post(`orders/${orderId}/notes`, data); return res.data; } async deleteProduct(id: string | number): Promise { + // 删除产品 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); try { await api.delete(`products/${id}`, { force: true }); @@ -172,12 +208,14 @@ export class WooCommerceAdapter implements ISiteAdapter { async batchProcessProducts( data: { create?: any[]; update?: any[]; delete?: Array } ): Promise { + // 批量处理产品增删改 return await this.wpService.batchProcessProducts(this.site, data); } async getOrders( params: UnifiedSearchParamsDTO ): Promise> { + // 获取订单列表并映射为统一订单DTO集合 const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged(this.site, 'orders', params); return { @@ -186,26 +224,31 @@ export class WooCommerceAdapter implements ISiteAdapter { totalPages, page, per_page, + page_size: per_page, }; } async getOrder(id: string | number): Promise { + // 获取单个订单详情 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const res = await api.get(`orders/${id}`); return this.mapOrder(res.data); } async createOrder(data: Partial): Promise { + // 创建订单并返回统一订单DTO const api = (this.wpService as any).createApi(this.site, 'wc/v3'); const res = await api.post('orders', data); return this.mapOrder(res.data); } async updateOrder(id: string | number, data: Partial): Promise { + // 更新订单并返回布尔结果 return await this.wpService.updateOrder(this.site, String(id), data as any); } async deleteOrder(id: string | number): Promise { + // 删除订单 const api = (this.wpService as any).createApi(this.site, 'wc/v3'); await api.delete(`orders/${id}`, { force: true }); return true; @@ -214,6 +257,7 @@ export class WooCommerceAdapter implements ISiteAdapter { async getSubscriptions( params: UnifiedSearchParamsDTO ): Promise> { + // 获取订阅列表并映射为统一订阅DTO集合 const { items, total, totalPages, page, per_page } = await this.wpService.fetchResourcePaged( this.site, @@ -226,45 +270,56 @@ export class WooCommerceAdapter implements ISiteAdapter { totalPages, page, per_page, + page_size: per_page, }; } async getMedia( params: UnifiedSearchParamsDTO ): Promise> { - const { items, total, totalPages } = await this.wpService.getMedia( - this.site.id, - params.page || 1, - params.per_page || 20 + // 获取媒体列表并映射为统一媒体DTO集合 + const { items, total, totalPages, page, per_page } = await this.wpService.fetchMediaPaged( + this.site, + params ); return { items: items.map(this.mapMedia), total, totalPages, - page: params.page || 1, - per_page: params.per_page || 20, + page, + per_page, + page_size: per_page, }; } async deleteMedia(id: string | number): Promise { + // 删除媒体资源 await this.wpService.deleteMedia(Number(this.site.id), Number(id), true); return true; } async updateMedia(id: string | number, data: any): Promise { + // 更新媒体信息 return await this.wpService.updateMedia(Number(this.site.id), Number(id), data); } - private mapCustomer(item: any): UnifiedCustomerDTO { + private mapCustomer(item: WooCustomer): UnifiedCustomerDTO { + // 将 WooCommerce 客户数据映射为统一客户DTO + // 包含基础信息地址信息与时间信息 return { id: item.id, + avatar: item.avatar_url, email: item.email, + orders: Number(item.orders?? 0), + total_spend: Number(item.total_spent ?? 0), first_name: item.first_name, last_name: item.last_name, username: item.username, phone: item.billing?.phone || item.shipping?.phone, billing: item.billing, shipping: item.shipping, + date_created: item.date_created, + date_modified: item.date_modified, raw: item, }; } @@ -281,6 +336,7 @@ export class WooCommerceAdapter implements ISiteAdapter { totalPages, page, per_page, + page_size: per_page, }; } diff --git a/src/controller/site-api.controller.ts b/src/controller/site-api.controller.ts index a0e91bc..efb70ca 100644 --- a/src/controller/site-api.controller.ts +++ b/src/controller/site-api.controller.ts @@ -49,10 +49,42 @@ export class SiteApiController { ) { try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.getProducts(query); - const header = ['id','name','type','status','sku','regular_price','sale_price','price','stock_status','stock_quantity']; - const rows = data.items.map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.regular_price,p.sale_price,p.price,p.stock_status,p.stock_quantity]); - const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n'); + const perPage = (query.page_size ?? query.per_page) || 100; + let page = 1; + const all: any[] = []; + while (true) { + const data = await adapter.getProducts({ ...query, page, per_page: perPage, page_size: perPage }); + const items = data.items || []; + all.push(...items); + const totalPages = data.totalPages || Math.ceil((data.total || 0) / (data.per_page || perPage)); + if (!items.length || page >= totalPages) break; + page += 1; + } + let items = all; + if (query.ids) { + const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean)); + items = items.filter(i => ids.has(String(i.id))); + } + const header = ['id','name','type','status','sku','regular_price','sale_price','price','stock_status','stock_quantity','image_src']; + const rows = items.map((p: any) => [ + p.id, + p.name, + p.type, + p.status, + p.sku, + p.regular_price, + p.sale_price, + p.price, + p.stock_status, + p.stock_quantity, + ((p.images && p.images[0]?.src) || ''), + ]); + const toCsvValue = (val: any) => { + const s = String(val ?? ''); + const escaped = s.replace(/"/g, '""'); + return `"${escaped}"`; + }; + const csv = [header.map(toCsvValue).join(','), ...rows.map(r => r.map(toCsvValue).join(','))].join('\n'); return successResponse({ csv }); } catch (error) { return errorResponse(error.message); @@ -69,15 +101,20 @@ export class SiteApiController { const site = await this.siteApiService.siteService.get(siteId, true); if (site.type === 'woocommerce') { const page = query.page || 1; - const per_page = query.per_page || 100; - const res = await this.siteApiService.wpService.getProducts(site, page, per_page); + const perPage = (query.page_size ?? query.per_page) || 100; + const res = await this.siteApiService.wpService.getProducts(site, page, perPage); const header = ['id','name','type','status','sku','regular_price','sale_price','stock_status','stock_quantity']; const rows = (res.items || []).map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.regular_price,p.sale_price,p.stock_status,p.stock_quantity]); - const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n'); - return successResponse({ csv }); + const toCsvValue = (val: any) => { + const s = String(val ?? ''); + const escaped = s.replace(/"/g, '""'); + return `"${escaped}"`; + }; + const csv = [header.map(toCsvValue).join(','), ...rows.map(r => r.map(toCsvValue).join(','))].join('\n'); + return successResponse({ csv }); } if (site.type === 'shopyy') { - const res = await this.siteApiService.shopyyService.getProducts(site, query.page || 1, query.per_page || 100); + const res = await this.siteApiService.shopyyService.getProducts(site, query.page || 1, (query.page_size ?? query.per_page) || 100); const header = ['id','name','type','status','sku','price','stock_status','stock_quantity']; const rows = (res.items || []).map((p: any) => [p.id,p.name,p.type,p.status,p.sku,p.price,p.stock_status,p.stock_quantity]); const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n'); @@ -329,7 +366,12 @@ export class SiteApiController { this.logger.info(`[Site API] 获取订单列表开始, siteId: ${siteId}, query: ${JSON.stringify(query)}`); try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.getOrders(query); + const where = { ...(query.where || {}) }; + if (query.customer_id) { + where.customer = query.customer_id; + where.customer_id = query.customer_id; + } + const data = await adapter.getOrders({ ...query, where }); this.logger.info(`[Site API] 获取订单列表成功, siteId: ${siteId}, 共获取到 ${data.total} 个订单`); return successResponse(data); } catch (error) { @@ -338,6 +380,26 @@ export class SiteApiController { } } + @Get('/:siteId/customers/:customerId/orders') + @ApiOkResponse({ type: UnifiedOrderPaginationDTO }) + async getCustomerOrders( + @Param('siteId') siteId: number, + @Param('customerId') customerId: number, + @Query() query: UnifiedSearchParamsDTO + ) { + this.logger.info(`[Site API] 获取客户订单列表开始, siteId: ${siteId}, customerId: ${customerId}, query: ${JSON.stringify(query)}`); + try { + const adapter = await this.siteApiService.getAdapter(siteId); + const where = { ...(query.where || {}), customer: customerId, customer_id: customerId }; + const data = await adapter.getOrders({ ...query, where, customer_id: customerId }); + this.logger.info(`[Site API] 获取客户订单列表成功, siteId: ${siteId}, customerId: ${customerId}, 共获取到 ${data.total} 个订单`); + return successResponse(data); + } catch (error) { + this.logger.error(`[Site API] 获取客户订单列表失败, siteId: ${siteId}, customerId: ${customerId}, 错误信息: ${error.message}`); + return errorResponse(error.message); + } + } + @Get('/:siteId/orders/export') async exportOrders( @Param('siteId') siteId: number, @@ -345,10 +407,44 @@ export class SiteApiController { ) { try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.getOrders(query); - const header = ['id','number','status','currency','total','customer_id','customer_name','email','date_created']; - const rows = data.items.map((o: any) => [o.id,o.number,o.status,o.currency,o.total,o.customer_id,o.customer_name,o.email,o.date_created]); - const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n'); + const perPage = (query.page_size ?? query.per_page) || 100; + let page = 1; + const all: any[] = []; + while (true) { + const data = await adapter.getOrders({ ...query, page, per_page: perPage, page_size: perPage }); + const items = data.items || []; + all.push(...items); + const totalPages = data.totalPages || Math.ceil((data.total || 0) / (data.per_page || perPage)); + if (!items.length || page >= totalPages) break; + page += 1; + } + let items = all; + if (query.ids) { + const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean)); + items = items.filter(i => ids.has(String(i.id))); + } + const header = ['id','number','status','currency','total','customer_id','customer_name','email','payment_method','phone','billing_full_address','shipping_full_address','date_created']; + const rows = items.map((o: any) => [ + o.id, + o.number, + o.status, + o.currency, + o.total, + o.customer_id, + o.customer_name, + o.email, + o.payment_method, + (o.shipping?.phone || o.billing?.phone || ''), + (o.billing_full_address || ''), + (o.shipping_full_address || ''), + o.date_created, + ]); + const toCsvValue = (val: any) => { + const s = String(val ?? ''); + const escaped = s.replace(/"/g, '""'); + return `"${escaped}"`; + }; + const csv = [header.map(toCsvValue).join(','), ...rows.map(r => r.map(toCsvValue).join(','))].join('\n'); return successResponse({ csv }); } catch (error) { return errorResponse(error.message); @@ -579,9 +675,24 @@ export class SiteApiController { ) { try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.getSubscriptions(query); + const perPage = (query.page_size ?? query.per_page) || 100; + let page = 1; + const all: any[] = []; + while (true) { + const data = await adapter.getSubscriptions({ ...query, page, per_page: perPage, page_size: perPage }); + const items = data.items || []; + all.push(...items); + const totalPages = data.totalPages || Math.ceil((data.total || 0) / (data.per_page || perPage)); + if (!items.length || page >= totalPages) break; + page += 1; + } + let items = all; + if (query.ids) { + const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean)); + items = items.filter(i => ids.has(String(i.id))); + } const header = ['id','status','customer_id','billing_period','billing_interval','start_date','next_payment_date']; - const rows = data.items.map((s: any) => [s.id,s.status,s.customer_id,s.billing_period,s.billing_interval,s.start_date,s.next_payment_date]); + const rows = items.map((s: any) => [s.id,s.status,s.customer_id,s.billing_period,s.billing_interval,s.start_date,s.next_payment_date]); const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n'); return successResponse({ csv }); } catch (error) { @@ -614,9 +725,24 @@ export class SiteApiController { ) { try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.getMedia(query); + const perPage = (query.page_size ?? query.per_page) || 100; + let page = 1; + const all: any[] = []; + while (true) { + const data = await adapter.getMedia({ ...query, page, per_page: perPage, page_size: perPage }); + const items = data.items || []; + all.push(...items); + const totalPages = data.totalPages || Math.ceil((data.total || 0) / (data.per_page || perPage)); + if (!items.length || page >= totalPages) break; + page += 1; + } + let items = all; + if (query.ids) { + const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean)); + items = items.filter(i => ids.has(String(i.id))); + } const header = ['id','title','media_type','mime_type','source_url','date_created']; - const rows = data.items.map((m: any) => [m.id,m.title,m.media_type,m.mime_type,m.source_url,m.date_created]); + const rows = items.map((m: any) => [m.id,m.title,m.media_type,m.mime_type,m.source_url,m.date_created]); const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n'); return successResponse({ csv }); } catch (error) { @@ -738,9 +864,49 @@ export class SiteApiController { ) { try { const adapter = await this.siteApiService.getAdapter(siteId); - const data = await adapter.getCustomers(query); - const header = ['id','email','first_name','last_name','fullname','username','phone']; - const rows = data.items.map((c: any) => [c.id,c.email,c.first_name,c.last_name,c.fullname,c.username,c.phone]); + const perPage = (query.page_size ?? query.per_page) || 100; + let page = 1; + const all: any[] = []; + while (true) { + const data = await adapter.getCustomers({ ...query, page, per_page: perPage, page_size: perPage }); + const items = data.items || []; + all.push(...items); + const totalPages = data.totalPages || Math.ceil((data.total || 0) / (data.per_page || perPage)); + if (!items.length || page >= totalPages) break; + page += 1; + } + let items = all; + if (query.ids) { + const ids = new Set(String(query.ids).split(',').map(v => v.trim()).filter(Boolean)); + items = items.filter(i => ids.has(String(i.id))); + } + const header = ['id','email','first_name','last_name','fullname','username','phone','orders','total_spend','role','billing_full_address','shipping_full_address','date_created']; + const formatAddress = (addr: any) => [ + addr?.fullname, + addr?.company, + addr?.address_1, + addr?.address_2, + addr?.city, + addr?.state, + addr?.postcode, + addr?.country, + addr?.phone, + ].filter(Boolean).join(', '); + const rows = items.map((c: any) => [ + c.id, + c.email, + c.first_name, + c.last_name, + c.fullname, + (c.username || c.raw?.username || ''), + (c.phone || c.billing?.phone || c.shipping?.phone || ''), + c.orders, + c.total_spend, + (c.role || c.raw?.role || ''), + formatAddress(c.billing || {}), + formatAddress(c.shipping || {}), + c.date_created, + ]); const csv = [header.join(','), ...rows.map(r => r.map(v => String(v ?? '')).join(','))].join('\n'); return successResponse({ csv }); } catch (error) { diff --git a/src/dto/shopyy.dto.ts b/src/dto/shopyy.dto.ts new file mode 100644 index 0000000..50ad239 --- /dev/null +++ b/src/dto/shopyy.dto.ts @@ -0,0 +1,282 @@ +// Shopyy 平台原始数据类型定义 +// 仅包含当前映射逻辑所需字段以保持简洁与类型安全 + +// 产品类型 +export interface ShopyyProduct { + // 产品主键 + id: number; + // 产品名称或标题 + name?: string; + title?: string; + // 产品类型 + product_type?: string | number; + // 产品状态数值 1为发布 其他为草稿 + status: number; + // 变体信息 + variant?: { + sku?: string; + price?: string; + }; + // 价格 + special_price?: string; + price?: string; + // 库存追踪标识 1表示跟踪 + inventory_tracking?: number; + // 库存数量 + inventory_quantity?: number; + // 图片列表 + images?: Array<{ + id?: number; + src: string; + alt?: string; + position?: string | number; + }>; + // 主图 + image?: { + src: string; + file_name?: string; + alt?: string; + file_size?: number; + width?: number; + height?: number; + id?: number; + position?: number | string; + file_type?: string; + }; + // 标签 + tags?: string[]; + // 变体列表 + variants?: ShopyyVariant[]; + // 分类集合 + collections?: Array<{ id?: number; title?: string }>; + // 规格选项列表 + options?: Array<{ + id?: number; + position?: number | string; + option_name?: string; + values?: Array<{ option_value?: string; id?: number; position?: number }>; + }>; + // 发布与标识 + published_at?: string; + handle?: string; + spu?: string; + // 创建与更新时间 + created_at?: string | number; + updated_at?: string | number; +} + +// 变体类型 +export interface ShopyyVariant { + id: number; + sku?: string; + price?: string; + special_price?: string; + inventory_tracking?: number; + inventory_quantity?: number; + available?: number; + barcode?: string; + weight?: number; + image?: { src: string; id?: number; file_name?: string; alt?: string; position?: number | string }; + position?: number | string; + sku_code?: string; +} + +// 订单类型 +export interface ShopyyOrder { + // 主键与外部ID + id?: number; + order_id?: number; + // 订单号 + order_number?: string; + order_sn?: string; + // 状态 + status?: number | string; + order_status?: number | string; + // 币种 + currency_code?: string; + currency?: string; + // 金额 + total_price?: string | number; + total_amount?: string | number; + current_total_price?: string | number; + current_subtotal_price?: string | number; + current_shipping_price?: string | number; + current_tax_price?: string | number; + current_coupon_price?: string | number; + current_payment_price?: string | number; + // 客户ID + customer_id?: number; + user_id?: number; + // 客户信息 + customer_name?: string; + firstname?: string; + lastname?: string; + customer_email?: string; + email?: string; + // 地址字段 + billing_address?: { + first_name?: string; + last_name?: string; + name?: string; + company?: string; + phone?: string; + address1?: string; + address2?: string; + city?: string; + province?: string; + zip?: string; + country_name?: string; + country_code?: string; + }; + shipping_address?: { + first_name?: string; + last_name?: string; + name?: string; + company?: string; + phone?: string; + address1?: string; + address2?: string; + city?: string; + province?: string; + zip?: string; + country_name?: string; + country_code?: string; + } | string; + telephone?: string; + payment_address?: string; + payment_city?: string; + payment_zone?: string; + payment_postcode?: string; + payment_country?: string; + shipping_city?: string; + shipping_zone?: string; + shipping_postcode?: string; + shipping_country?: string; + // 订单项集合 + products?: Array<{ + id?: number; + name?: string; + product_title?: string; + product_id?: number; + quantity?: number; + price?: string | number; + sku?: string; + sku_code?: string; + }>; + // 支付方式 + payment_method?: string; + payment_id?: number; + payment_cards?: Array<{ + store_id?: number; + card_len?: number; + card_suffix?: number; + year?: number; + payment_status?: number; + created_at?: number; + month?: number; + updated_at?: number; + payment_id?: number; + payment_interface?: string; + card_prefix?: number; + id?: number; + order_id?: number; + card?: string; + transaction_no?: string; + }>; + fulfillments?: Array<{ + payment_tracking_status?: number; + note?: string; + updated_at?: number; + courier_code?: string; + courier_id?: number; + created_at?: number; + tracking_number?: string; + id?: number; + tracking_company?: string; + payment_tracking_result?: string; + payment_tracking_at?: number; + products?: Array<{ order_product_id?: number; quantity?: number; updated_at?: number; created_at?: number; id?: number }>; + }>; + shipping_zone_plans?: Array<{ + shipping_price?: number | string; + updated_at?: number; + created_at?: number; + id?: number; + shipping_zone_name?: string; + shipping_zone_id?: number; + shipping_zone_plan_id?: number; + shipping_zone_plan_name?: string; + }>; + transaction?: { + note?: string; + amount?: number | string; + created_at?: number; + merchant_id?: string; + payment_type?: string; + merchant_account?: string; + updated_at?: number; + payment_id?: number; + admin_id?: number; + admin_name?: string; + id?: number; + payment_method?: string; + transaction_no?: string; + }; + coupon_code?: string; + coupon_name?: string; + store_id?: number; + visitor_id?: string; + currency_rate?: string | number; + landing_page?: string; + note?: string; + admin_note?: string; + source_device?: string; + checkout_type?: string; + version?: string; + brand_id?: number; + tags?: string[]; + financial_status?: number; + fulfillment_status?: number; + // 创建与更新时间可能为时间戳 + created_at?: number | string; + date_added?: string; + updated_at?: number | string; + date_updated?: string; + last_modified?: string; +} + +// 客户类型 +export interface ShopyyCustomer { + // 主键与兼容ID + id?: number; + customer_id?: number; + // 姓名 + first_name?: string; + firstname?: string; + last_name?: string; + lastname?: string; + fullname?: string; + customer_name?: string; + // 联系信息 + email?: string; + customer_email?: string; + contact?: string; + phone?: string; + // 地址集合 + addresses?: any[]; + default_address?: any; + // 国家 + country?: { country_name?: string }; + // 统计字段 + orders_count?: number; + order_count?: number; + orders?: number; + total_spent?: number | string; + total_spend_amount?: number | string; + total_spend_money?: number | string; + // 创建与更新时间可能为时间戳 + created_at?: number | string; + date_added?: string; + updated_at?: number | string; + date_updated?: string; +} diff --git a/src/dto/site-api.dto copy.ts b/src/dto/site-api.dto copy.ts deleted file mode 100644 index 066cba5..0000000 --- a/src/dto/site-api.dto copy.ts +++ /dev/null @@ -1,264 +0,0 @@ -import { ApiProperty } from '@midwayjs/swagger'; - -export class UnifiedPaginationDTO { - @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 UnifiedImageDTO { - @ApiProperty({ description: '图片ID' }) - id: number | string; - - @ApiProperty({ description: '图片URL' }) - src: string; - - @ApiProperty({ description: '图片名称' }) - name?: string; - - @ApiProperty({ description: '替代文本' }) - alt?: string; -} - -export class UnifiedProductDTO { - @ApiProperty({ description: '产品ID' }) - id: string | number; - - @ApiProperty({ description: '产品名称' }) - name: string; - - @ApiProperty({ description: '产品类型' }) - type: string; - - @ApiProperty({ description: '产品状态' }) - status: string; - - @ApiProperty({ description: '产品SKU' }) - sku: string; - - @ApiProperty({ description: '常规价格' }) - regular_price: string; - - @ApiProperty({ description: '销售价格' }) - sale_price: string; - - @ApiProperty({ description: '当前价格' }) - price: string; - - @ApiProperty({ description: '库存状态' }) - stock_status: string; - - @ApiProperty({ description: '库存数量' }) - stock_quantity: number; - - @ApiProperty({ description: '产品图片', type: [UnifiedImageDTO] }) - images: UnifiedImageDTO[]; - - @ApiProperty({ description: '产品标签', type: 'json' }) - tags?: string[]; - - @ApiProperty({ description: '产品属性', type: 'json' }) - attributes: any[]; - - @ApiProperty({ description: '产品变体', type: 'json' }) - variations?: any[]; - - @ApiProperty({ description: '创建时间' }) - date_created: string; - - @ApiProperty({ description: '更新时间' }) - date_modified: string; - - @ApiProperty({ description: '原始数据(保留备用)', type: 'json' }) - raw?: any; -} - -export class UnifiedOrderDTO { - @ApiProperty({ description: '订单ID' }) - id: string | number; - - @ApiProperty({ description: '订单号' }) - number: string; - - @ApiProperty({ description: '订单状态' }) - status: string; - - @ApiProperty({ description: '货币' }) - currency: string; - - @ApiProperty({ description: '总金额' }) - total: string; - - @ApiProperty({ description: '客户ID' }) - customer_id: number; - - @ApiProperty({ description: '客户姓名' }) - customer_name: string; - - @ApiProperty({ description: '客户邮箱' }) - email: string; - - @ApiProperty({ description: '订单项', type: 'json' }) - line_items: any[]; - - @ApiProperty({ description: '销售项(兼容前端)', type: 'json' }) - sales?: any[]; - - @ApiProperty({ description: '账单地址', type: 'json' }) - billing: any; - - @ApiProperty({ description: '收货地址', type: 'json' }) - shipping: any; - - @ApiProperty({ description: '账单地址全称' }) - billing_full_address?: string; - - @ApiProperty({ description: '收货地址全称' }) - shipping_full_address?: string; - - @ApiProperty({ description: '支付方式' }) - payment_method: string; - - @ApiProperty({ description: '创建时间' }) - date_created: string; - - @ApiProperty({ description: '原始数据', type: 'json' }) - raw?: any; -} - -export class UnifiedCustomerDTO { - @ApiProperty({ description: '客户ID' }) - id: string | number; - - @ApiProperty({ description: '邮箱' }) - email: string; - - @ApiProperty({ description: '名' }) - first_name?: string; - - @ApiProperty({ description: '姓' }) - last_name?: string; - - @ApiProperty({ description: '名字' }) - fullname?: string; - - @ApiProperty({ description: '用户名' }) - username?: string; - - @ApiProperty({ description: '电话' }) - phone?: string; - - @ApiProperty({ description: '账单地址', type: 'json' }) - billing?: any; - - @ApiProperty({ description: '收货地址', type: 'json' }) - shipping?: any; - - @ApiProperty({ description: '原始数据', type: 'json' }) - raw?: any; -} - -export class UnifiedSubscriptionDTO { - @ApiProperty({ description: '订阅ID' }) - id: string | number; - - @ApiProperty({ description: '订阅状态' }) - status: string; - - @ApiProperty({ description: '客户ID' }) - customer_id: number; - - @ApiProperty({ description: '计费周期' }) - billing_period: string; - - @ApiProperty({ description: '计费间隔' }) - billing_interval: number; - - @ApiProperty({ description: '开始时间' }) - start_date: string; - - @ApiProperty({ description: '下次支付时间' }) - next_payment_date: string; - - @ApiProperty({ description: '订单项', type: 'json' }) - line_items: any[]; - - @ApiProperty({ description: '原始数据', type: 'json' }) - raw?: any; -} - -export class UnifiedMediaDTO { - @ApiProperty({ description: '媒体ID' }) - id: number; - - @ApiProperty({ description: '标题' }) - title: string; - - @ApiProperty({ description: '媒体类型' }) - media_type: string; - - @ApiProperty({ description: 'MIME类型' }) - mime_type: string; - - @ApiProperty({ description: '源URL' }) - source_url: string; - - @ApiProperty({ description: '创建时间' }) - date_created: string; -} - -export class UnifiedProductPaginationDTO extends UnifiedPaginationDTO { - @ApiProperty({ description: '列表数据', type: [UnifiedProductDTO] }) - items: UnifiedProductDTO[]; -} - -export class UnifiedOrderPaginationDTO extends UnifiedPaginationDTO { - @ApiProperty({ description: '列表数据', type: [UnifiedOrderDTO] }) - items: UnifiedOrderDTO[]; -} - -export class UnifiedCustomerPaginationDTO extends UnifiedPaginationDTO { - @ApiProperty({ description: '列表数据', type: [UnifiedCustomerDTO] }) - items: UnifiedCustomerDTO[]; -} - -export class UnifiedSubscriptionPaginationDTO extends UnifiedPaginationDTO { - @ApiProperty({ description: '列表数据', type: [UnifiedSubscriptionDTO] }) - items: UnifiedSubscriptionDTO[]; -} - -export class UnifiedMediaPaginationDTO extends UnifiedPaginationDTO { - @ApiProperty({ description: '列表数据', type: [UnifiedMediaDTO] }) - items: UnifiedMediaDTO[]; -} - -export class UnifiedSearchParamsDTO { - @ApiProperty({ description: '页码', example: 1 }) - page?: number; - - @ApiProperty({ description: '每页数量', example: 20 }) - per_page?: number; - - @ApiProperty({ description: '搜索关键词' }) - search?: string; - - @ApiProperty({ description: '状态' }) - status?: string; - - @ApiProperty({ description: '排序字段' }) - orderby?: string; - - @ApiProperty({ description: '排序方式' }) - order?: string; -} diff --git a/src/dto/site-api.dto.ts b/src/dto/site-api.dto.ts index 066cba5..5b4c7bc 100644 --- a/src/dto/site-api.dto.ts +++ b/src/dto/site-api.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty } from '@midwayjs/swagger'; export class UnifiedPaginationDTO { + // 分页DTO用于承载统一分页信息与列表数据 @ApiProperty({ description: '列表数据' }) items: T[]; @@ -13,11 +14,15 @@ export class UnifiedPaginationDTO { @ApiProperty({ description: '每页数量', example: 20 }) per_page: number; + @ApiProperty({ description: '每页数量别名', example: 20 }) + page_size?: number; + @ApiProperty({ description: '总页数', example: 5 }) totalPages: number; } export class UnifiedImageDTO { + // 图片DTO用于承载统一图片数据 @ApiProperty({ description: '图片ID' }) id: number | string; @@ -32,6 +37,7 @@ export class UnifiedImageDTO { } export class UnifiedProductDTO { + // 产品DTO用于承载统一产品数据 @ApiProperty({ description: '产品ID' }) id: string | number; @@ -85,6 +91,7 @@ export class UnifiedProductDTO { } export class UnifiedOrderDTO { + // 订单DTO用于承载统一订单数据 @ApiProperty({ description: '订单ID' }) id: string | number; @@ -133,16 +140,35 @@ export class UnifiedOrderDTO { @ApiProperty({ description: '创建时间' }) date_created: string; + @ApiProperty({ description: '更新时间' }) + date_modified?: string; + @ApiProperty({ description: '原始数据', type: 'json' }) raw?: any; } export class UnifiedCustomerDTO { + // 客户DTO用于承载统一客户数据 @ApiProperty({ description: '客户ID' }) id: string | number; + @ApiProperty({ description: '头像URL' }) + avatar?: string; + @ApiProperty({ description: '邮箱' }) email: string; + + @ApiProperty({ description: '订单总数' }) + orders?: number; + + @ApiProperty({ description: '总花费' }) + total_spend?: number; + + @ApiProperty({ description: '创建时间' }) + date_created?: string; + + @ApiProperty({ description: '更新时间' }) + date_modified?: string; @ApiProperty({ description: '名' }) first_name?: string; @@ -170,6 +196,7 @@ export class UnifiedCustomerDTO { } export class UnifiedSubscriptionDTO { + // 订阅DTO用于承载统一订阅数据 @ApiProperty({ description: '订阅ID' }) id: string | number; @@ -185,6 +212,12 @@ export class UnifiedSubscriptionDTO { @ApiProperty({ description: '计费间隔' }) billing_interval: number; + @ApiProperty({ description: '创建时间' }) + date_created?: string; + + @ApiProperty({ description: '更新时间' }) + date_modified?: string; + @ApiProperty({ description: '开始时间' }) start_date: string; @@ -199,6 +232,7 @@ export class UnifiedSubscriptionDTO { } export class UnifiedMediaDTO { + // 媒体DTO用于承载统一媒体数据 @ApiProperty({ description: '媒体ID' }) id: number; @@ -216,49 +250,73 @@ export class UnifiedMediaDTO { @ApiProperty({ description: '创建时间' }) date_created: string; + + @ApiProperty({ description: '更新时间' }) + date_modified?: string; } export class UnifiedProductPaginationDTO extends UnifiedPaginationDTO { + // 产品分页DTO用于承载产品列表分页数据 @ApiProperty({ description: '列表数据', type: [UnifiedProductDTO] }) items: UnifiedProductDTO[]; } export class UnifiedOrderPaginationDTO extends UnifiedPaginationDTO { + // 订单分页DTO用于承载订单列表分页数据 @ApiProperty({ description: '列表数据', type: [UnifiedOrderDTO] }) items: UnifiedOrderDTO[]; } export class UnifiedCustomerPaginationDTO extends UnifiedPaginationDTO { + // 客户分页DTO用于承载客户列表分页数据 @ApiProperty({ description: '列表数据', type: [UnifiedCustomerDTO] }) items: UnifiedCustomerDTO[]; } export class UnifiedSubscriptionPaginationDTO extends UnifiedPaginationDTO { + // 订阅分页DTO用于承载订阅列表分页数据 @ApiProperty({ description: '列表数据', type: [UnifiedSubscriptionDTO] }) items: UnifiedSubscriptionDTO[]; } export class UnifiedMediaPaginationDTO extends UnifiedPaginationDTO { + // 媒体分页DTO用于承载媒体列表分页数据 @ApiProperty({ description: '列表数据', type: [UnifiedMediaDTO] }) items: UnifiedMediaDTO[]; } export class UnifiedSearchParamsDTO { + // 统一查询参数DTO用于承载分页与筛选与排序参数 @ApiProperty({ description: '页码', example: 1 }) page?: number; @ApiProperty({ description: '每页数量', example: 20 }) per_page?: number; + @ApiProperty({ description: '每页数量别名', example: 20 }) + page_size?: number; + @ApiProperty({ description: '搜索关键词' }) search?: string; @ApiProperty({ description: '状态' }) status?: string; - @ApiProperty({ description: '排序字段' }) + @ApiProperty({ description: '客户ID,用于筛选订单' }) + customer_id?: number; + + @ApiProperty({ description: '过滤条件对象' }) + where?: Record; + + @ApiProperty({ description: '排序对象,例如 { "sku": "desc" }' }) + order?: Record | string; + + @ApiProperty({ description: '排序字段(兼容旧入参)' }) orderby?: string; - @ApiProperty({ description: '排序方式' }) - order?: string; + @ApiProperty({ description: '排序方式(兼容旧入参)' }) + orderDir?: 'asc' | 'desc'; + + @ApiProperty({ description: '选中ID列表,逗号分隔', required: false }) + ids?: string; } diff --git a/src/dto/woocommerce.dto.ts b/src/dto/woocommerce.dto.ts new file mode 100644 index 0000000..c2ce624 --- /dev/null +++ b/src/dto/woocommerce.dto.ts @@ -0,0 +1,389 @@ +// WooCommerce 平台原始数据类型定义 +// 仅包含当前映射逻辑所需字段以保持简洁与类型安全 + +// 产品类型 +export interface WooProduct { + // 产品主键 + id: number; + // 创建时间 + date_created: string; + // 创建时间(GMT) + date_created_gmt: string; + // 更新时间 + date_modified: string; + // 更新时间(GMT) + date_modified_gmt: string; + // 产品类型 simple grouped external variable + type: string; + // 产品状态 draft pending private publish + status: string; + // 是否为特色产品 + featured: boolean; + // 目录可见性选项:visible, catalog, search and hidden. Default is visible. + catalog_visibility: string; + + // 常规价格 + regular_price?: string; + // 促销价格 + sale_price?: string; + // 当前价格 + price?: string; + price_html?: string; + date_on_sale_from?: string; // Date the product is on sale from. + date_on_sale_from_gmt?: string; // Date the product is on sale from (GMT). + date_on_sale_to?: string; // Date the product is on sale to. + date_on_sale_to_gmt?: string; // Date the product is on sale to (GMT). + on_sale: boolean; // Whether the product is on sale. + purchasable: boolean; // Whether the product is purchasable. + total_sales: number; // Total sales for this product. + virtual: boolean; // Whether the product is virtual. + downloadable: boolean; // Whether the product is downloadable. + downloads: Array<{ id?: number; name?: string; file?: string }>; // Downloadable files for the product. + download_limit: number; // Download limit. + download_expiry: number; // Download expiry days. + external_url: string; // URL of the external product. + + global_unique_id: string; // GTIN, UPC, EAN or ISBN - a unique identifier for each distinct product and service that can be purchased. + // 产品SKU + sku: string; + // 产品名称 + name: string; + // 产品描述 + description: string; + // 产品短描述 + short_description: string; + + // 产品永久链接 + permalink: string; + // 产品URL路径 + slug: string; + + // 库存状态 + stock_status?: 'instock' | 'outofstock' | 'onbackorder'; + // 库存数量 + stock_quantity?: number; + // 是否管理库存 + manage_stock?: boolean; + // 缺货预定设置 no notify yes + 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; + // 运输类别 slug + shipping_class?: string; + // 运输类别ID 只读 + shipping_class_id?: number; + // 图片列表 + images?: Array<{ id: number; src: string; name?: string; alt?: string }>; + // 属性列表 + attributes?: Array<{ + id?: number; + name?: string; + position?: number; + visible?: boolean; + variation?: boolean; + options?: string[]; + }>; + // 变体列表 + variations?: number[]; + // 默认变体属性 + default_attributes?: Array<{ id?: number; name?: string; option?: string }>; + // 允许评论 + reviews_allowed?: boolean; + // 平均评分 只读 + average_rating?: string; + // 评分数量 只读 + rating_count?: number; + // 相关产品ID列表 只读 + related_ids?: number[]; + // 追加销售产品ID列表 + upsell_ids?: number[]; + // 交叉销售产品ID列表 + cross_sell_ids?: number[]; + // 父产品ID + parent_id?: number; + // 购买备注 + purchase_note?: string; + // 分类列表 + categories?: Array<{ id: number; name?: string; slug?: string }>; + // 标签列表 + tags?: Array<{ id: number; name?: string; slug?: string }>; + // 菜单排序 + menu_order?: number; + // 元数据 + meta_data?: Array<{ id?: number; key: string; value: any }>; +} + +// 订单类型 +export interface WooOrder { + // 订单主键 + id: number; + // 父订单ID + parent_id?: number; + // 订单号 + number: string; + // 订单键 只读 + order_key?: string; + // 创建来源 + created_via?: string; + // WooCommerce版本 只读 + version?: string; + // 状态 + status: string; + // 币种 + currency: string; + // 价格是否含税 只读 + prices_include_tax?: boolean; + // 总金额 + total: string; + // 总税额 只读 + total_tax?: string; + // 折扣总额 只读 + discount_total?: string; + // 折扣税额 只读 + discount_tax?: string; + // 运费总额 只读 + shipping_total?: string; + // 运费税额 只读 + shipping_tax?: string; + // 购物车税额 只读 + cart_tax?: string; + // 客户ID + customer_id: number; + // 客户IP 只读 + customer_ip_address?: string; + // 客户UA 只读 + customer_user_agent?: string; + // 客户备注 + customer_note?: string; + // 账单信息 + billing?: { + first_name?: string; + last_name?: string; + email?: string; + company?: string; + address_1?: string; + address_2?: string; + city?: string; + state?: string; + postcode?: string; + country?: string; + phone?: string; + fullname?: string; + }; + // 收货信息 + shipping?: { + first_name?: string; + last_name?: string; + company?: string; + address_1?: string; + address_2?: string; + city?: string; + state?: string; + postcode?: string; + country?: string; + phone?: string; + fullname?: string; + }; + // 订单项 + line_items?: Array<{ + product_id?: number; + variation_id?: number; + quantity?: number; + subtotal?: string; + subtotal_tax?: string; + total?: string; + total_tax?: string; + name?: string; + sku?: string; + price?: number; + meta_data?: Array<{ key: string; value: any }>; + [key: string]: any; + }>; + // 税费行 只读 + tax_lines?: Array<{ + id?: number; + rate_code?: string; + rate_id?: number; + label?: string; + tax_total?: string; + shipping_tax_total?: string; + compound?: boolean; + meta_data?: any[]; + }>; + // 物流费用行 + shipping_lines?: Array<{ + id?: number; + method_title?: string; + method_id?: string; + total?: string; + total_tax?: string; + taxes?: any[]; + meta_data?: any[]; + }>; + // 手续费行 + fee_lines?: Array<{ + id?: number; + name?: string; + tax_class?: string; + tax_status?: string; + total?: string; + total_tax?: string; + taxes?: any[]; + meta_data?: any[]; + }>; + // 优惠券行 + coupon_lines?: Array<{ + id?: number; + code?: string; + discount?: string; + discount_tax?: string; + meta_data?: any[]; + }>; + // 退款列表 只读 + refunds?: Array<{ + id?: number; + reason?: string; + total?: string; + [key: string]: any; + }>; + // 支付方式标题 + payment_method_title?: string; + // 支付方式ID + payment_method?: string; + // 交易ID + transaction_id?: string; + // 已支付时间 + date_paid?: string; + date_paid_gmt?: string; + // 完成时间 + date_completed?: string; + date_completed_gmt?: string; + // 购物车hash 只读 + cart_hash?: string; + // 设置为已支付 写入专用 + set_paid?: boolean; + // 元数据 + meta_data?: Array<{ id?: number; key: string; value: any }>; + // 创建与更新时间 + date_created: string; + date_created_gmt?: string; + date_modified?: string; + date_modified_gmt?: string; +} + +// 订阅类型 +export interface WooSubscription { + // 订阅主键 + id: number; + // 订阅状态 + status: string; + // 客户ID + customer_id: number; + // 计费周期 + billing_period?: string; + // 计费间隔 + billing_interval?: number; + // 开始时间 + start_date?: string; + // 下次支付时间 + next_payment_date?: string; + // 订阅项 + line_items?: any[]; + // 创建时间 + date_created?: string; + // 更新时间 + date_modified?: string; +} + +// WordPress 媒体类型 +export interface WpMedia { + // 媒体主键 + id: number; + // 标题可能为字符串或包含rendered的对象 + title?: { rendered?: string } | string; + // 媒体类型 + media_type?: string; + // MIME类型 + mime_type?: string; + // 源地址 + source_url?: string; + // 创建时间兼容date字段 + date_created?: string; + date?: string; + // 更新时间兼容modified字段 + date_modified?: string; + modified?: string; +} + +// 客户类型 +export interface WooCustomer { + // 客户主键 + id: number; + // 头像URL + avatar_url?: string; + // 邮箱 + email: string; + // 订单总数 + orders?: number; + // 总花费 + total_spent?: number | string; + // 名 + first_name?: string; + // 姓 + last_name?: string; + // 用户名 + username?: string; + // 角色 只读 + role?: string; + // 密码 写入专用 + password?: string; + // 账单信息 + billing?: { + first_name?: string; + last_name?: string; + email?: string; + company?: string; + phone?: string; + address_1?: string; + address_2?: string; + city?: string; + state?: string; + postcode?: string; + country?: string; + }; + // 收货信息 + shipping?: { + first_name?: string; + last_name?: string; + company?: string; + phone?: string; + address_1?: string; + address_2?: string; + city?: string; + state?: string; + postcode?: string; + country?: string; + }; + // 是否为付费客户 只读 + is_paying_customer?: boolean; + // 元数据 + meta_data?: Array<{ id?: number; key: string; value: any }>; + // 创建时间 + date_created?: string; + date_created_gmt?: string; + // 更新时间 + date_modified?: string; + date_modified_gmt?: string; +} diff --git a/src/service/shopyy.service.ts b/src/service/shopyy.service.ts index b0bba1a..fbd2fe8 100644 --- a/src/service/shopyy.service.ts +++ b/src/service/shopyy.service.ts @@ -74,11 +74,28 @@ export class ShopyyService implements IPlatformService { * 通用分页获取资源 */ public async fetchResourcePaged(site: any, endpoint: string, params: Record = {}) { - // 映射 params 字段: page -> page, per_page -> limit + const page = Number(params.page || 1); + const limit = Number(params.page_size ?? params.per_page ?? 20); + const where = params.where && typeof params.where === 'object' ? params.where : {}; + let orderby: string | undefined = params.orderby; + let order: 'asc' | 'desc' | undefined = params.orderDir as any; + if (!orderby && params.order && typeof params.order === 'object') { + const entries = Object.entries(params.order as Record); + if (entries.length > 0) { + const [field, dir] = entries[0]; + orderby = field; + order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc'; + } + } + // 映射统一入参到平台入参 const requestParams = { - ...params, - page: params.page || 1, - limit: params.per_page || 20 + ...where, + ...(params.search ? { search: params.search } : {}), + ...(params.status ? { status: params.status } : {}), + ...(orderby ? { orderby } : {}), + ...(order ? { order } : {}), + page, + limit }; const response = await this.request(site, endpoint, 'GET', null, requestParams); if (response?.code !== 0) { @@ -89,7 +106,8 @@ export class ShopyyService implements IPlatformService { total: response.data?.paginate?.total || 0, totalPages: response.data?.paginate?.pageTotal || 0, page: response.data?.paginate?.current || requestParams.page, - per_page: response.data?.paginate?.pagesize || requestParams.limit + per_page: response.data?.paginate?.pagesize || requestParams.limit, + page_size: response.data?.paginate?.pagesize || requestParams.limit }; } diff --git a/src/service/wp.service.ts b/src/service/wp.service.ts index e88c501..3cc894d 100644 --- a/src/service/wp.service.ts +++ b/src/service/wp.service.ts @@ -51,7 +51,29 @@ export class WPService implements IPlatformService { */ public async fetchResourcePaged(site: any, resource: string, params: Record = {}) { const api = this.createApi(site, 'wc/v3'); - return this.sdkGetPage(api, resource, params); + const page = Number(params.page ?? 1); + const per_page = Number(params.page_size ?? params.per_page ?? 20); + const where = params.where && typeof params.where === 'object' ? params.where : {}; + let orderby: string | undefined = params.orderby; + let order: 'asc' | 'desc' | undefined = params.orderDir as any; + if (!orderby && params.order && typeof params.order === 'object') { + const entries = Object.entries(params.order as Record); + if (entries.length > 0) { + const [field, dir] = entries[0]; + orderby = field; + order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc'; + } + } + const requestParams = { + ...where, + ...(params.search ? { search: params.search } : {}), + ...(params.status ? { status: params.status } : {}), + ...(orderby ? { orderby } : {}), + ...(order ? { order } : {}), + page, + per_page + }; + return this.sdkGetPage(api, resource, requestParams); } /** @@ -59,7 +81,7 @@ export class WPService implements IPlatformService { */ private async sdkGetPage(api: any, resource: string, params: Record = {}) { const page = params.page ?? 1; - const per_page = params.per_page ?? 100; + const per_page = params.per_page ?? params.page_size ?? 100; const res = await api.get(resource.replace(/^\/+/, ''), { ...params, page, per_page }); if (res?.headers?.['content-type']?.includes('text/html')) { throw new Error('接口返回了 text/html,可能为 WordPress 登录页或错误页,请检查站点配置或权限'); @@ -67,7 +89,7 @@ export class WPService implements IPlatformService { const data = res.data as T[]; const totalPages = Number(res.headers?.['x-wp-totalpages'] ?? 1); const total = Number(res.headers?.['x-wp-total']?? 1) - return { items: data, total, totalPages, page, per_page }; + return { items: data, total, totalPages, page, per_page, page_size: per_page }; } /** @@ -630,6 +652,40 @@ export class WPService implements IPlatformService { }; } + public async fetchMediaPaged(site: any, params: Record = {}) { + const page = Number(params.page ?? 1); + const per_page = Number(params.page_size ?? params.per_page ?? 20); + const where = params.where && typeof params.where === 'object' ? params.where : {}; + let orderby: string | undefined = params.orderby; + let order: 'asc' | 'desc' | undefined = params.orderDir as any; + if (!orderby && params.order && typeof params.order === 'object') { + const entries = Object.entries(params.order as Record); + if (entries.length > 0) { + const [field, dir] = entries[0]; + orderby = field; + order = String(dir).toLowerCase() === 'desc' ? 'desc' : 'asc'; + } + } + const apiUrl = site.apiUrl; + const { consumerKey, consumerSecret } = site as any; + const endpoint = 'wp/v2/media'; + const url = this.buildURL(apiUrl, '/wp-json', endpoint); + const auth = Buffer.from(`${consumerKey}:${consumerSecret}`).toString('base64'); + const response = await axios.get(url, { + headers: { Authorization: `Basic ${auth}` }, + params: { + ...where, + ...(params.search ? { search: params.search } : {}), + ...(orderby ? { orderby } : {}), + ...(order ? { order } : {}), + page, + per_page + } + }); + const total = Number(response.headers['x-wp-total'] || 0); + const totalPages = Number(response.headers['x-wp-totalpages'] || 0); + return { items: response.data, total, totalPages, page, per_page, page_size: per_page }; + } /** * 上传媒体文件 * @param siteId 站点 ID