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